Skip to content

Commit d5bc36b

Browse files
committed
fix publish build source tracking
1 parent 6b66e55 commit d5bc36b

3 files changed

Lines changed: 162 additions & 1 deletion

File tree

.github/workflows/publish.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
name: Publish Package
22

33
on:
4+
workflow_dispatch:
45
push:
56
branches:
67
- main
78
paths:
89
- "package.json"
10+
- "lib/**"
11+
- ".github/workflows/publish.yml"
912

1013
jobs:
1114
publish:

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@ dist/
33
.arch/
44
lib/*.ts
55
!lib/*.d.ts
6+
!lib/validate.ts
67
temp/
78

89
*.tsbuildinfo
910
.DS_Store
1011
pnpm-lock.yaml
11-
package-lock.json
12+
package-lock.json

lib/validate.ts

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
import { createHash, createHmac } from "node:crypto"
2+
3+
// === API Key Validation ===
4+
5+
export function validateApiKeyFormat(key: string): {
6+
valid: boolean
7+
reason?: string
8+
} {
9+
if (!key || typeof key !== "string") {
10+
return { valid: false, reason: "key is empty or not a string" }
11+
}
12+
if (!key.startsWith("sm_")) {
13+
return { valid: false, reason: "key must start with sm_ prefix" }
14+
}
15+
if (key.length < 20) {
16+
return { valid: false, reason: "key is too short" }
17+
}
18+
if (/\s/.test(key)) {
19+
return { valid: false, reason: "key contains whitespace" }
20+
}
21+
return { valid: true }
22+
}
23+
24+
// === Container Tag Validation ===
25+
26+
export function validateContainerTag(tag: string): {
27+
valid: boolean
28+
reason?: string
29+
} {
30+
if (!tag || typeof tag !== "string") {
31+
return { valid: false, reason: "tag is empty" }
32+
}
33+
if (tag.length > 100) {
34+
return { valid: false, reason: "tag exceeds 100 characters" }
35+
}
36+
if (!/^[a-zA-Z0-9_-]+$/.test(tag)) {
37+
return {
38+
valid: false,
39+
reason:
40+
"tag contains invalid characters (only alphanumeric, underscore, hyphen allowed)",
41+
}
42+
}
43+
if (/^[-_]|[-_]$/.test(tag)) {
44+
return { valid: false, reason: "tag must not start or end with - or _" }
45+
}
46+
return { valid: true }
47+
}
48+
49+
// === Content Sanitization ===
50+
51+
const UNSAFE_PATTERNS = [
52+
// biome-ignore lint/suspicious/noControlCharactersInRegex: sanitizer intentionally strips ASCII control characters
53+
/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, // control characters
54+
/\uFEFF/g, // BOM
55+
/[\uFFF0-\uFFFF]/g, // specials block
56+
]
57+
58+
export function sanitizeContent(content: string, maxLength = 100000): string {
59+
if (!content || typeof content !== "string") return ""
60+
let cleaned = content
61+
for (const pattern of UNSAFE_PATTERNS) {
62+
cleaned = cleaned.replace(pattern, "")
63+
}
64+
if (cleaned.length > maxLength) {
65+
cleaned = cleaned.slice(0, maxLength)
66+
}
67+
return cleaned
68+
}
69+
70+
export function validateContentLength(
71+
content: string,
72+
min = 1,
73+
max = 100000,
74+
): { valid: boolean; reason?: string } {
75+
if (content.length < min) {
76+
return { valid: false, reason: `content below minimum length (${min})` }
77+
}
78+
if (content.length > max) {
79+
return { valid: false, reason: `content exceeds maximum length (${max})` }
80+
}
81+
return { valid: true }
82+
}
83+
84+
// === Metadata Validation ===
85+
86+
const MAX_METADATA_KEYS = 50
87+
const MAX_METADATA_KEY_LENGTH = 128
88+
const MAX_METADATA_VALUE_LENGTH = 1024
89+
90+
export function sanitizeMetadata(
91+
meta: Record<string, unknown>,
92+
): Record<string, string | number | boolean> {
93+
const cleaned: Record<string, string | number | boolean> = {}
94+
let count = 0
95+
for (const [key, value] of Object.entries(meta)) {
96+
if (count >= MAX_METADATA_KEYS) break
97+
if (key.length > MAX_METADATA_KEY_LENGTH) continue
98+
if (/[^\w.-]/.test(key)) continue
99+
if (typeof value === "string") {
100+
cleaned[key] = value.slice(0, MAX_METADATA_VALUE_LENGTH)
101+
count++
102+
} else if (typeof value === "number" && Number.isFinite(value)) {
103+
cleaned[key] = value
104+
count++
105+
} else if (typeof value === "boolean") {
106+
cleaned[key] = value
107+
count++
108+
}
109+
}
110+
return cleaned
111+
}
112+
113+
// === Configuration Validation ===
114+
115+
export function validateRecallConfig(
116+
maxResults: number,
117+
frequency: number,
118+
): string[] {
119+
const warnings: string[] = []
120+
if (!Number.isInteger(maxResults) || maxResults < 1 || maxResults > 20) {
121+
warnings.push("maxRecallResults must be an integer between 1 and 20")
122+
}
123+
if (!Number.isInteger(frequency) || frequency < 1 || frequency > 500) {
124+
warnings.push("profileFrequency must be an integer between 1 and 500")
125+
}
126+
return warnings
127+
}
128+
129+
// === Request Integrity ===
130+
131+
function sha256(input: string): string {
132+
return createHash("sha256").update(input).digest("hex")
133+
}
134+
135+
const INTEGRITY_VERSION = 1
136+
const SEED =
137+
"7f2a9c4b8e1d6f3a" +
138+
"5c0b9d8e7f6a5b4c" +
139+
"3d2e1f0a9b8c7d6e" +
140+
"5f4a3b2c1d0e9f8a"
141+
142+
function deriveSignature(key: string, context: string): string {
143+
const payload = [sha256(key), sha256(context), INTEGRITY_VERSION].join(":")
144+
return createHmac("sha256", SEED).update(payload).digest("base64url")
145+
}
146+
147+
export function getRequestIntegrity(
148+
apiKey: string,
149+
containerTag: string,
150+
): Record<string, string> {
151+
const tagHash = sha256(containerTag)
152+
const signature = deriveSignature(apiKey, containerTag)
153+
return {
154+
"X-Content-Hash": tagHash,
155+
"X-Request-Integrity": [`v${INTEGRITY_VERSION}`, signature].join("."),
156+
}
157+
}

0 commit comments

Comments
 (0)