code-foundry 1.11.0 → 1.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,233 @@
1
+ // @ts-check
2
+
3
+ import { createHash } from 'node:crypto'
4
+ import { existsSync, lstatSync, readFileSync, readdirSync } from 'node:fs'
5
+ import { relative, resolve, sep } from 'node:path'
6
+
7
+ const uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
8
+
9
+ /** @param {unknown} value @param {string} label */
10
+ export function versionId(value, label = 'version ID') {
11
+ if (typeof value !== 'string' || !uuid.test(value)) throw new Error(`Invalid ${label}`)
12
+ return value
13
+ }
14
+
15
+ /** @param {string} value */
16
+ export function httpsUrl(value) {
17
+ const url = new URL(value)
18
+ if (url.protocol !== 'https:' || url.username || url.password || url.hash)
19
+ throw new Error('Deployment URLs must be HTTPS without credentials or fragments')
20
+ return url.href
21
+ }
22
+
23
+ /** @param {string} content @param {string} worker */
24
+ export function parseUpload(content, worker) {
25
+ const entries = content
26
+ .split(/\r?\n/)
27
+ .filter((line) => line.trim())
28
+ .map((line) => JSON.parse(line))
29
+ if (entries.some((entry) => entry.type === 'command-failed'))
30
+ throw new Error('Wrangler reported command-failed')
31
+ const uploads = entries.filter((entry) => entry.type === 'version-upload')
32
+ if (uploads.length !== 1) throw new Error('Expected exactly one structured version-upload record')
33
+ const upload = uploads[0]
34
+ if (upload.version !== 1 || upload.worker_name !== worker)
35
+ throw new Error('Unsupported Wrangler output schema or unexpected Worker name')
36
+ const url = httpsUrl(upload.preview_url)
37
+ if (!new URL(url).hostname.endsWith('.workers.dev'))
38
+ throw new Error('Expected a Workers version preview URL')
39
+ return { worker, versionId: versionId(upload.version_id), url }
40
+ }
41
+
42
+ /** @param {string} value */
43
+ export function pinnedWrangler(value) {
44
+ if (value === 'local') return ['bunx', '--bun', '--no-install', 'wrangler']
45
+ if (!/^\d+\.\d+\.\d+(?:-[a-zA-Z0-9.-]+)?$/.test(value))
46
+ throw new Error('wrangler-version must be local or an exact version, never latest or a range')
47
+ return ['npx', '--yes', `wrangler@${value}`]
48
+ }
49
+
50
+ /** @param {string} value @param {string} label */
51
+ export function parseArgv(value, label) {
52
+ const argv = JSON.parse(value)
53
+ if (
54
+ !Array.isArray(argv) ||
55
+ !argv.length ||
56
+ argv.some((arg) => typeof arg !== 'string' || !arg.length || arg.includes('\0'))
57
+ )
58
+ throw new Error(`${label} must be a non-empty JSON argv array`)
59
+ return /** @type {string[]} */ (argv)
60
+ }
61
+
62
+ /** Hash a declared build tree, refusing symlinks rather than following external files.
63
+ * @param {string} root @param {string} directory
64
+ */
65
+ export function digestBuild(root, directory) {
66
+ const base = resolve(root)
67
+ const target = resolve(base, directory)
68
+ const location = relative(base, target)
69
+ if (!location || location === '..' || location.startsWith(`..${sep}`))
70
+ throw new Error('artifact-path must identify a build subdirectory inside the checkout')
71
+ let ancestor = base
72
+ for (const part of location.split(sep)) {
73
+ ancestor = resolve(ancestor, part)
74
+ if (lstatSync(ancestor).isSymbolicLink())
75
+ throw new Error('Build tree must not contain symlinks')
76
+ }
77
+ const hash = createHash('sha256')
78
+ let files = 0
79
+ /** @param {string} currentDirectory */
80
+ function visit(currentDirectory) {
81
+ // Sorting a fresh directory listing does not mutate shared state.
82
+ // oxlint-disable-next-line unicorn/no-array-sort
83
+ const entries = readdirSync(currentDirectory).sort()
84
+ for (const name of entries) {
85
+ const file = resolve(currentDirectory, name)
86
+ const stat = lstatSync(file)
87
+ if (stat.isSymbolicLink()) throw new Error('Build tree must not contain symlinks')
88
+ if (stat.isDirectory()) visit(file)
89
+ else if (stat.isFile()) {
90
+ const content = readFileSync(file)
91
+ hash
92
+ .update(`${relative(target, file).split(sep).join('/')}\0${content.length}\0`)
93
+ .update(content)
94
+ files += 1
95
+ } else throw new Error('Build tree contains a non-regular file')
96
+ }
97
+ }
98
+ visit(target)
99
+ if (!files) throw new Error('Build tree is empty')
100
+ return `sha256:${hash.digest('hex')}`
101
+ }
102
+
103
+ /**
104
+ * Inspect the actual uploaded version, never log binding values or secret material.
105
+ * Read-only declarations are reviewed test policy, not a sandbox for Worker code.
106
+ * @param {any} version @param {any} policy @param {'preview'|'production'} mode
107
+ */
108
+ export function inspectBindings(version, policy, mode) {
109
+ const bindings = version?.resources?.bindings
110
+ if (
111
+ Object.values(version?.resources?.script_runtime?.exports ?? {}).some(
112
+ (entry) => /** @type {any} */ (entry)?.type === 'durable-object'
113
+ )
114
+ )
115
+ throw new Error(
116
+ 'Durable Object exports require a repository-specific preview and migration workflow'
117
+ )
118
+ if (!Array.isArray(bindings)) throw new Error('Version API did not return binding metadata')
119
+ if (policy.schemaVersion !== 1) throw new Error('Unsupported binding policy schema')
120
+ if (
121
+ !Array.isArray(policy.readOnlyBindings ?? []) ||
122
+ !Array.isArray(policy.isolatedBindings ?? [])
123
+ )
124
+ throw new Error('Binding policy lists must be arrays')
125
+ const safeTypes = new Set([
126
+ 'plain_text',
127
+ 'secret_text',
128
+ 'json',
129
+ 'assets',
130
+ 'version_metadata',
131
+ 'wasm_module',
132
+ 'text_blob',
133
+ 'data_blob',
134
+ ])
135
+ let stateless = true
136
+ /** @type {{name: string, type: string, policy: string}[]} */
137
+ const result = []
138
+ for (const binding of bindings) {
139
+ if (typeof binding.name !== 'string' || typeof binding.type !== 'string')
140
+ throw new Error('Malformed binding metadata')
141
+ if (binding.type === 'durable_object_namespace')
142
+ throw new Error(
143
+ 'Durable Objects require a repository-specific preview and migration workflow'
144
+ )
145
+ if (safeTypes.has(binding.type)) {
146
+ result.push({ name: binding.name, type: binding.type, policy: 'stateless' })
147
+ continue
148
+ }
149
+ stateless = false
150
+ const isolated = (policy.isolatedBindings ?? []).find(
151
+ (/** @type {any} */ entry) => entry.name === binding.name && entry.type === binding.type
152
+ )
153
+ if (isolated) {
154
+ if (mode !== 'preview')
155
+ throw new Error('Isolated preview bindings must never be promoted into production')
156
+ const expected = isolated.expected ?? {}
157
+ const production = isolated.production ?? {}
158
+ const keys = Object.keys(expected)
159
+ if (
160
+ !keys.length ||
161
+ keys.some(
162
+ (key) =>
163
+ ['type', 'name'].includes(key) ||
164
+ typeof expected[key] !== 'string' ||
165
+ typeof production[key] !== 'string'
166
+ )
167
+ )
168
+ throw new Error('Isolated bindings require expected and production resource identifiers')
169
+ if (!keys.some((key) => expected[key] !== production[key]))
170
+ throw new Error('Preview binding reuses production resource identifiers')
171
+ if (keys.some((key) => binding[key] !== expected[key]))
172
+ throw new Error(`Isolated resource mismatch for ${binding.name}`)
173
+ result.push({ name: binding.name, type: binding.type, policy: 'isolated' })
174
+ continue
175
+ }
176
+ const reviewed = (policy.readOnlyBindings ?? []).some(
177
+ (/** @type {any} */ entry) => entry.name === binding.name && entry.type === binding.type
178
+ )
179
+ if (!reviewed) throw new Error(`Unreviewed stateful binding: ${binding.name} (${binding.type})`)
180
+ result.push({ name: binding.name, type: binding.type, policy: 'reviewed-read-only' })
181
+ }
182
+ return { stateless, rollbackSafe: stateless && policy.rollbackSafe === true, bindings: result }
183
+ }
184
+
185
+ /** @param {string} candidate @param {number} percentage @param {any[]} previous */
186
+ export function deploymentVersions(candidate, percentage, previous = []) {
187
+ versionId(candidate)
188
+ if (
189
+ !Number.isFinite(percentage) ||
190
+ percentage < 0.01 ||
191
+ percentage > 100 ||
192
+ (percentage < 100 && percentage > 99.99)
193
+ )
194
+ throw new Error('Canary percentage must be greater than zero and at most 100')
195
+ if (percentage === 100) return [{ version_id: candidate, percentage: 100 }]
196
+ if (previous.length !== 1 || previous[0].percentage !== 100)
197
+ throw new Error('A canary requires exactly one baseline version serving 100%')
198
+ const baseline = versionId(previous[0].version_id)
199
+ if (baseline === candidate) throw new Error('Canary and baseline must be different versions')
200
+ return [
201
+ { version_id: candidate, percentage },
202
+ { version_id: baseline, percentage: 100 - percentage },
203
+ ]
204
+ }
205
+
206
+ /** @param {any} previous */
207
+ export function validateRollback(previous) {
208
+ if (!Array.isArray(previous) || previous.length < 1 || previous.length > 2)
209
+ throw new Error('No valid rollback deployment is available')
210
+ let total = 0
211
+ const seen = new Set()
212
+ for (const entry of previous) {
213
+ versionId(entry.version_id)
214
+ if (seen.has(entry.version_id)) throw new Error('Duplicate rollback version')
215
+ seen.add(entry.version_id)
216
+ if (!Number.isFinite(entry.percentage) || entry.percentage <= 0 || entry.percentage > 100)
217
+ throw new Error('Invalid rollback percentage')
218
+ total += entry.percentage
219
+ }
220
+ if (Math.abs(total - 100) > 1e-8) throw new Error('Rollback percentages must total 100')
221
+ return previous.map((entry) => ({ version_id: entry.version_id, percentage: entry.percentage }))
222
+ }
223
+
224
+ /** @param {string} root @param {string} file */
225
+ export function bindingPolicy(root, file) {
226
+ if (!file) return { schemaVersion: 1, readOnlyBindings: [], isolatedBindings: [] }
227
+ const path = resolve(root, file)
228
+ const location = relative(resolve(root), path)
229
+ if (location === '..' || location.startsWith(`..${sep}`))
230
+ throw new Error('Binding policy escapes checkout')
231
+ if (!existsSync(path)) throw new Error('Binding policy file is missing')
232
+ return JSON.parse(readFileSync(path, 'utf8'))
233
+ }