@void/deploy-core 0.27.0 → 1.0.3

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.
package/dist/index.d.mts CHANGED
@@ -3,14 +3,10 @@ export declare const MANIFEST_REDIRECT_RULE_STATUSES: ReadonlySet<number>;
3
3
  export declare const MAX_ROUTING_RULES = 2e3;
4
4
  export declare const MAX_REVALIDATE_QUERY_ALLOWLIST_ENTRIES: number;
5
5
  /**
6
- * Runtime shape validation for incoming deploy manifests. Downstream code
7
- * indexes deeply into the manifest (bindings.d1, queues[i].name,
8
- * websocket.classes, etc.) without re-guarding, so a malformed-but-JSON-valid
9
- * payload would otherwise crash with a 500 — sometimes after a deployment row
10
- * or CF resources were created. Everything the deploy pipeline reads must be
11
- * validated here BEFORE createDeployment runs.
6
+ * Check the shape of an incoming deploy manifest. Returns an error message, or `null` when valid.
7
+ * Deploy code reads the manifest without re-checking, so run this before creating any deploy state.
12
8
  */
13
- export declare function validateManifestShape(parsed: unknown): string | null;
9
+ export declare function validateManifestShape(manifest: unknown): string | null;
14
10
  //#endregion
15
11
  //#region src/resource-plan.d.ts
16
12
  type DeployResource = {
@@ -177,6 +173,10 @@ export declare function createDeployUploadPayload(artifact: DeployArtifact, meta
177
173
  //#region src/canonical-json.d.ts
178
174
  export declare function canonicalJson(value: unknown): string;
179
175
  //#endregion
176
+ //#region src/artifact-digest.d.ts
177
+ /** Commit to all executable bytes, asset hashes, bindings, migrations, and routing. */
178
+ export declare function deployArtifactDigest(artifact: DeployArtifact): Promise<string>;
179
+ //#endregion
180
180
  //#region src/is-record.d.ts
181
181
  /** Narrow object-shaped input without accepting null or arrays. */
182
182
  export declare function isRecord(value: unknown): value is Record<string, unknown>;
@@ -394,6 +394,18 @@ export type LiveManifestMeta = {
394
394
  classes: ReadonlyArray<string>;
395
395
  migrations?: ReadonlyArray<LiveMigrationMeta>;
396
396
  };
397
+ export type DurableObjectManifestMeta = {
398
+ bindings: ReadonlyArray<{
399
+ bindingName: string;
400
+ className: string;
401
+ storage: "sqlite";
402
+ }>;
403
+ /** Verified by the platform; never accepted from a deployment client. */
404
+ runtime?: {
405
+ protocol: 1;
406
+ namespaces: Record<string, string>;
407
+ };
408
+ };
397
409
  export type DeployManifest = {
398
410
  version: number;
399
411
  assetManifestVersion?: 1 | 2;
@@ -445,6 +457,7 @@ export type DeployManifest = {
445
457
  websocket?: WebSocketManifestMeta;
446
458
  sandbox?: SandboxManifestMeta;
447
459
  live?: LiveManifestMeta;
460
+ durableObjects?: DurableObjectManifestMeta;
448
461
  };
449
462
  export type DeployEvent = {
450
463
  event: "start";
package/dist/index.mjs CHANGED
@@ -18,42 +18,56 @@ const MAX_REVALIDATE_QUERY_ALLOWLIST_ENTRIES = MAX_ROUTING_RULES;
18
18
  const MAX_PAGES = 5e3;
19
19
  const MAX_CRON_EXPRESSIONS = 100;
20
20
  const MAX_SCHEDULES = 1e3;
21
- /**
22
- * Strict literal-hostname regex used to validate `redirectRules[].host` /
23
- * `fallbackRules[].host` on the deploy manifest. Mirrors the regex in
24
- * `routes/domains.ts` (custom-domain create endpoint) — the source of truth
25
- * for what counts as a valid project-owned hostname. Rejects scheme prefixes,
26
- * ports, paths, whitespace, control chars, wildcards, single-label hosts,
27
- * IPv4 literals, and userinfo. Hostnames are lowercased before testing so
28
- * the same byte-exact form is what dispatch compares against `url.hostname`
29
- * (also lowercase per WHATWG).
30
- */
31
21
  const LITERAL_HOSTNAME_REGEX = /^([a-z0-9]([a-z0-9-]*[a-z0-9])?\.)+[a-z]{2,}$/;
32
- /**
33
- * Validate and lowercase-normalize an optional `host` field on a redirect or
34
- * fallback rule. Mutates `ruleObject.host` in place so the persisted manifest carries
35
- * the canonical form. Returns an error message string on failure, or `null`
36
- * on success (including the absent-field case).
37
- */
38
- function validateRuleHost(ruleObject, ruleKind, sourceRef) {
39
- if (ruleObject.host === void 0) return null;
40
- if (typeof ruleObject.host !== "string") return `invalid manifest shape: ${ruleKind} entry ${sourceRef} host must be a string`;
41
- const normalized = ruleObject.host.toLowerCase();
42
- if (!LITERAL_HOSTNAME_REGEX.test(normalized)) return `invalid manifest shape: ${ruleKind} entry ${sourceRef} host must be a literal hostname (e.g. "www.example.com"; no scheme, port, path, or wildcards): ${JSON.stringify(ruleObject.host)}`;
43
- ruleObject.host = normalized;
22
+ function validateRuleHost(rule, kind, sourceRef) {
23
+ if (rule.host === void 0) return null;
24
+ if (typeof rule.host !== "string") return `invalid manifest shape: ${kind} entry ${sourceRef} host must be a string`;
25
+ const normalized = rule.host.toLowerCase();
26
+ if (!LITERAL_HOSTNAME_REGEX.test(normalized)) return `invalid manifest shape: ${kind} entry ${sourceRef} host must be a literal hostname (e.g. "www.example.com"; no scheme, port, path, or wildcards): ${JSON.stringify(rule.host)}`;
27
+ rule.host = normalized;
28
+ return null;
29
+ }
30
+ function validateRoutingRules(rules, kind) {
31
+ if (rules === void 0) return null;
32
+ if (!Array.isArray(rules)) return `invalid manifest shape: ${kind} must be an array`;
33
+ if (rules.length > 2e3) return `invalid manifest shape: ${kind} has ${rules.length} entries, exceeds the ${MAX_ROUTING_RULES} per-list limit`;
34
+ const rewritesOnly = kind === "fallbackRules";
35
+ for (const rule of rules) {
36
+ if (!isRecord(rule)) return `invalid manifest shape: ${kind} entries must be objects`;
37
+ if (typeof rule.source !== "string" || typeof rule.destination !== "string" || typeof rule.status !== "number") return `invalid manifest shape: ${kind} entries must have string source, string destination, and numeric status`;
38
+ const sourceRef = JSON.stringify(rule.source);
39
+ if (rewritesOnly && rule.status !== 200) return `invalid manifest shape: ${kind} entry ${sourceRef} status must be 200 (fallbacks are rewrites only)`;
40
+ if (!rewritesOnly && !MANIFEST_REDIRECT_RULE_STATUSES.has(rule.status)) return `invalid manifest shape: ${kind} entry ${sourceRef} status must be one of ${[...MANIFEST_REDIRECT_RULE_STATUSES].join(", ")} (200 = rewrite, 3xx = redirect)`;
41
+ if (rule.status === 200) {
42
+ const condition = rewritesOnly ? "" : " when status is 200";
43
+ if (rule.destination.includes("?")) return `invalid manifest shape: ${kind} entry ${sourceRef} destination must not contain a query string${condition}`;
44
+ if (!rule.destination.startsWith("/")) return `invalid manifest shape: ${kind} entry ${sourceRef} destination must be path-absolute (start with "/")${condition}`;
45
+ if (rule.destination.startsWith("//")) return `invalid manifest shape: ${kind} entry ${sourceRef} destination must not start with "//" (protocol-relative URLs parse cross-origin; use a single-slash path-absolute destination)`;
46
+ }
47
+ const hostError = validateRuleHost(rule, kind, sourceRef);
48
+ if (hostError) return hostError;
49
+ }
50
+ return null;
51
+ }
52
+ function validateClassMigrations(path, migrations, fields, requireNonEmptyTag = false) {
53
+ if (migrations === void 0) return null;
54
+ if (!Array.isArray(migrations)) return `invalid manifest shape: ${path} must be an array`;
55
+ for (const migration of migrations) {
56
+ if (!isRecord(migration)) return `invalid manifest shape: ${path} entries must be objects`;
57
+ if (typeof migration.tag !== "string" || requireNonEmptyTag && migration.tag.length === 0) return `invalid manifest shape: ${path} entries must have a ${requireNonEmptyTag ? "non-empty " : ""}string tag`;
58
+ for (const field of fields) {
59
+ const value = migration[field];
60
+ if (value !== void 0 && (!Array.isArray(value) || !value.every((className) => typeof className === "string"))) return `invalid manifest shape: ${path} ${field} must be an array of strings`;
61
+ }
62
+ }
44
63
  return null;
45
64
  }
46
65
  /**
47
- * Runtime shape validation for incoming deploy manifests. Downstream code
48
- * indexes deeply into the manifest (bindings.d1, queues[i].name,
49
- * websocket.classes, etc.) without re-guarding, so a malformed-but-JSON-valid
50
- * payload would otherwise crash with a 500 — sometimes after a deployment row
51
- * or CF resources were created. Everything the deploy pipeline reads must be
52
- * validated here BEFORE createDeployment runs.
66
+ * Check the shape of an incoming deploy manifest. Returns an error message, or `null` when valid.
67
+ * Deploy code reads the manifest without re-checking, so run this before creating any deploy state.
53
68
  */
54
- function validateManifestShape(parsed) {
55
- if (!isRecord(parsed)) return "invalid manifest shape: expected a JSON object";
56
- const manifest = parsed;
69
+ function validateManifestShape(manifest) {
70
+ if (!isRecord(manifest)) return "invalid manifest shape: expected a JSON object";
57
71
  if (manifest.emailProtocolVersion !== void 0 && manifest.emailProtocolVersion !== 1) return "invalid manifest shape: emailProtocolVersion must be 1";
58
72
  if (!isRecord(manifest.bindings)) return "invalid manifest shape: bindings must be an object";
59
73
  const bindings = manifest.bindings;
@@ -71,15 +85,14 @@ function validateManifestShape(parsed) {
71
85
  if (!Array.isArray(manifest.queues)) return "invalid manifest shape: queues must be an array";
72
86
  for (const queue of manifest.queues) {
73
87
  if (!isRecord(queue)) return "invalid manifest shape: queues entries must be objects";
74
- const queueObject = queue;
75
- if (typeof queueObject.name !== "string" || typeof queueObject.bindingName !== "string") return "invalid manifest shape: queues entries must have string name and bindingName";
88
+ if (typeof queue.name !== "string" || typeof queue.bindingName !== "string") return "invalid manifest shape: queues entries must have string name and bindingName";
76
89
  for (const tuning of [
77
90
  "maxBatchSize",
78
91
  "maxBatchTimeout",
79
92
  "maxRetries",
80
93
  "retryDelay"
81
94
  ]) {
82
- const tuningValue = queueObject[tuning];
95
+ const tuningValue = queue[tuning];
83
96
  if (tuningValue === void 0) continue;
84
97
  if (typeof tuningValue !== "number" || !Number.isFinite(tuningValue)) return `invalid manifest shape: queues entry ${tuning} must be a finite number`;
85
98
  }
@@ -91,9 +104,8 @@ function validateManifestShape(parsed) {
91
104
  const cronExpressions = /* @__PURE__ */ new Set();
92
105
  for (const schedule of manifest.schedules) {
93
106
  if (!isRecord(schedule)) return "invalid manifest shape: schedules entries must be objects";
94
- const scheduleObject = schedule;
95
- if (typeof scheduleObject.name !== "string" || typeof scheduleObject.cron !== "string") return "invalid manifest shape: schedules entries must have string name and cron";
96
- cronExpressions.add(scheduleObject.cron);
107
+ if (typeof schedule.name !== "string" || typeof schedule.cron !== "string") return "invalid manifest shape: schedules entries must have string name and cron";
108
+ cronExpressions.add(schedule.cron);
97
109
  }
98
110
  if (cronExpressions.size > MAX_CRON_EXPRESSIONS) return `invalid manifest shape: schedules has ${cronExpressions.size} distinct cron expressions, exceeds the ${MAX_CRON_EXPRESSIONS} per-project limit`;
99
111
  }
@@ -168,38 +180,21 @@ function validateManifestShape(parsed) {
168
180
  "none"
169
181
  ].includes(assetConfig.html_handling)) return "invalid manifest shape: assetConfig.html_handling must be one of auto-trailing-slash, force-trailing-slash, drop-trailing-slash, none";
170
182
  }
171
- if (manifest.revalidate !== void 0) {
172
- if (typeof manifest.revalidate === "number") {} else if (!isRecord(manifest.revalidate)) return "invalid manifest shape: revalidate must be a number or object";
173
- else for (const [pathname, value] of Object.entries(manifest.revalidate)) if (typeof value !== "number") return `invalid manifest shape: revalidate.${pathname} must be a number`;
183
+ if (manifest.revalidate !== void 0 && typeof manifest.revalidate !== "number") {
184
+ if (!isRecord(manifest.revalidate)) return "invalid manifest shape: revalidate must be a number or object";
185
+ for (const [pathname, value] of Object.entries(manifest.revalidate)) if (typeof value !== "number") return `invalid manifest shape: revalidate.${pathname} must be a number`;
174
186
  }
175
187
  if (manifest.headerRules !== void 0) {
176
188
  if (!Array.isArray(manifest.headerRules)) return "invalid manifest shape: headerRules must be an array";
177
189
  for (const rule of manifest.headerRules) {
178
190
  if (!isRecord(rule)) return "invalid manifest shape: headerRules entries must be objects";
179
- const ruleObject = rule;
180
- if (typeof ruleObject.pattern !== "string") return "invalid manifest shape: headerRules entries must have a string pattern";
181
- if (!Array.isArray(ruleObject.headers)) return "invalid manifest shape: headerRules entries must have a headers array";
182
- for (const pair of ruleObject.headers) if (!Array.isArray(pair) || pair.length !== 2 || typeof pair[0] !== "string" || typeof pair[1] !== "string") return "invalid manifest shape: headerRules headers entries must be [name, value] string pairs";
183
- }
184
- }
185
- if (manifest.redirectRules !== void 0) {
186
- if (!Array.isArray(manifest.redirectRules)) return "invalid manifest shape: redirectRules must be an array";
187
- if (manifest.redirectRules.length > 2e3) return `invalid manifest shape: redirectRules has ${manifest.redirectRules.length} entries, exceeds the ${MAX_ROUTING_RULES} per-list limit`;
188
- for (const rule of manifest.redirectRules) {
189
- if (!isRecord(rule)) return "invalid manifest shape: redirectRules entries must be objects";
190
- const ruleObject = rule;
191
- if (typeof ruleObject.source !== "string" || typeof ruleObject.destination !== "string" || typeof ruleObject.status !== "number") return "invalid manifest shape: redirectRules entries must have string source, string destination, and numeric status";
192
- const sourceRef = JSON.stringify(ruleObject.source);
193
- if (!MANIFEST_REDIRECT_RULE_STATUSES.has(ruleObject.status)) return `invalid manifest shape: redirectRules entry ${sourceRef} status must be one of ${[...MANIFEST_REDIRECT_RULE_STATUSES].join(", ")} (200 = rewrite, 3xx = redirect)`;
194
- const destination = ruleObject.destination;
195
- const isRewrite = ruleObject.status === 200;
196
- if (isRewrite && destination.includes("?")) return `invalid manifest shape: redirectRules entry ${sourceRef} destination must not contain a query string when status is 200`;
197
- if (isRewrite && !destination.startsWith("/")) return `invalid manifest shape: redirectRules entry ${sourceRef} destination must be path-absolute (start with "/") when status is 200`;
198
- if (isRewrite && destination.startsWith("//")) return `invalid manifest shape: redirectRules entry ${sourceRef} destination must not start with "//" (protocol-relative URLs parse cross-origin; use a single-slash path-absolute destination)`;
199
- const hostError = validateRuleHost(ruleObject, "redirectRules", sourceRef);
200
- if (hostError) return hostError;
191
+ if (typeof rule.pattern !== "string") return "invalid manifest shape: headerRules entries must have a string pattern";
192
+ if (!Array.isArray(rule.headers)) return "invalid manifest shape: headerRules entries must have a headers array";
193
+ for (const pair of rule.headers) if (!Array.isArray(pair) || pair.length !== 2 || typeof pair[0] !== "string" || typeof pair[1] !== "string") return "invalid manifest shape: headerRules headers entries must be [name, value] string pairs";
201
194
  }
202
195
  }
196
+ const redirectRulesError = validateRoutingRules(manifest.redirectRules, "redirectRules");
197
+ if (redirectRulesError) return redirectRulesError;
203
198
  if (manifest.revalidateQueryAllowlist !== void 0) {
204
199
  if (!isRecord(manifest.revalidateQueryAllowlist)) return "invalid manifest shape: revalidateQueryAllowlist must be an object";
205
200
  const entries = Object.entries(manifest.revalidateQueryAllowlist);
@@ -215,59 +210,32 @@ function validateManifestShape(parsed) {
215
210
  if (manifest.pages.length > MAX_PAGES) return `invalid manifest shape: pages has ${manifest.pages.length} entries, exceeds the ${MAX_PAGES} entry limit`;
216
211
  for (const entry of manifest.pages) {
217
212
  if (!isRecord(entry)) return "invalid manifest shape: each pages entry must be an object";
218
- const e = entry;
219
- if (typeof e.pattern !== "string" || e.pattern === "") return "invalid manifest shape: pages entry pattern must be a string";
220
- if (e.revalidate !== void 0 && typeof e.revalidate !== "number") return "invalid manifest shape: pages entry revalidate must be a number";
213
+ if (typeof entry.pattern !== "string" || entry.pattern === "") return "invalid manifest shape: pages entry pattern must be a string";
214
+ if (entry.revalidate !== void 0 && typeof entry.revalidate !== "number") return "invalid manifest shape: pages entry revalidate must be a number";
221
215
  }
222
216
  }
223
217
  if (manifest.pathRevalidate !== void 0) {
224
218
  if (!isRecord(manifest.pathRevalidate)) return "invalid manifest shape: pathRevalidate must be an object";
225
219
  for (const [pathname, value] of Object.entries(manifest.pathRevalidate)) if (typeof value !== "number") return `invalid manifest shape: pathRevalidate.${pathname} must be a number`;
226
220
  }
227
- if (manifest.fallbackRules !== void 0) {
228
- if (!Array.isArray(manifest.fallbackRules)) return "invalid manifest shape: fallbackRules must be an array";
229
- if (manifest.fallbackRules.length > 2e3) return `invalid manifest shape: fallbackRules has ${manifest.fallbackRules.length} entries, exceeds the ${MAX_ROUTING_RULES} per-list limit`;
230
- for (const rule of manifest.fallbackRules) {
231
- if (!isRecord(rule)) return "invalid manifest shape: fallbackRules entries must be objects";
232
- const ruleObject = rule;
233
- if (typeof ruleObject.source !== "string" || typeof ruleObject.destination !== "string" || typeof ruleObject.status !== "number") return "invalid manifest shape: fallbackRules entries must have string source, string destination, and numeric status";
234
- const sourceRef = JSON.stringify(ruleObject.source);
235
- if (ruleObject.status !== 200) return `invalid manifest shape: fallbackRules entry ${sourceRef} status must be 200 (fallbacks are rewrites only)`;
236
- const destination = ruleObject.destination;
237
- if (destination.includes("?")) return `invalid manifest shape: fallbackRules entry ${sourceRef} destination must not contain a query string`;
238
- if (!destination.startsWith("/")) return `invalid manifest shape: fallbackRules entry ${sourceRef} destination must be path-absolute (start with "/")`;
239
- if (destination.startsWith("//")) return `invalid manifest shape: fallbackRules entry ${sourceRef} destination must not start with "//" (protocol-relative URLs parse cross-origin; use a single-slash path-absolute destination)`;
240
- const hostError = validateRuleHost(ruleObject, "fallbackRules", sourceRef);
241
- if (hostError) return hostError;
242
- }
243
- }
221
+ const fallbackRulesError = validateRoutingRules(manifest.fallbackRules, "fallbackRules");
222
+ if (fallbackRulesError) return fallbackRulesError;
244
223
  if (manifest.websocket !== void 0) {
245
224
  if (!isRecord(manifest.websocket)) return "invalid manifest shape: websocket must be an object";
246
225
  const websocket = manifest.websocket;
247
226
  if (!Array.isArray(websocket.routes)) return "invalid manifest shape: websocket.routes must be an array";
248
227
  for (const route of websocket.routes) {
249
228
  if (!isRecord(route)) return "invalid manifest shape: websocket.routes entries must be objects";
250
- const routeObject = route;
251
- if (typeof routeObject.className !== "string") return "invalid manifest shape: websocket.routes entries must have a string className";
252
- if (routeObject.bindingName !== void 0 && typeof routeObject.bindingName !== "string") return "invalid manifest shape: websocket.routes entry bindingName must be a string";
229
+ if (typeof route.className !== "string") return "invalid manifest shape: websocket.routes entries must have a string className";
230
+ if (route.bindingName !== void 0 && typeof route.bindingName !== "string") return "invalid manifest shape: websocket.routes entry bindingName must be a string";
253
231
  }
254
232
  if (!Array.isArray(websocket.classes) || !websocket.classes.every((x) => typeof x === "string")) return "invalid manifest shape: websocket.classes must be an array of strings";
255
- if (websocket.migrations !== void 0) {
256
- if (!Array.isArray(websocket.migrations)) return "invalid manifest shape: websocket.migrations must be an array";
257
- for (const migration of websocket.migrations) {
258
- if (!isRecord(migration)) return "invalid manifest shape: websocket.migrations entries must be objects";
259
- const migrationObject = migration;
260
- if (typeof migrationObject.tag !== "string" || migrationObject.tag.length === 0) return "invalid manifest shape: websocket.migrations entries must have a non-empty string tag";
261
- for (const field of [
262
- "newClasses",
263
- "newSqliteClasses",
264
- "deletedClasses"
265
- ]) {
266
- const value = migrationObject[field];
267
- if (value !== void 0 && (!Array.isArray(value) || !value.every((className) => typeof className === "string"))) return `invalid manifest shape: websocket.migrations ${field} must be an array of strings`;
268
- }
269
- }
270
- }
233
+ const migrationsError = validateClassMigrations("websocket.migrations", websocket.migrations, [
234
+ "newClasses",
235
+ "newSqliteClasses",
236
+ "deletedClasses"
237
+ ], true);
238
+ if (migrationsError) return migrationsError;
271
239
  }
272
240
  if (manifest.sandbox !== void 0) {
273
241
  if (manifest.type === "static" || manifest.type === "spa") return "sandbox is not supported for static/spa deployments";
@@ -282,16 +250,8 @@ function validateManifestShape(parsed) {
282
250
  if (isLocalImageReference(sandbox.image)) return "invalid manifest shape: sandbox.image must be a registry image reference";
283
251
  if (sandbox.instanceType !== void 0 && typeof sandbox.instanceType !== "string") return "invalid manifest shape: sandbox.instanceType must be a string";
284
252
  if (sandbox.maxInstances !== void 0 && (typeof sandbox.maxInstances !== "number" || !Number.isInteger(sandbox.maxInstances) || sandbox.maxInstances < 1)) return "invalid manifest shape: sandbox.maxInstances must be a positive integer";
285
- if (sandbox.migrations !== void 0) {
286
- if (!Array.isArray(sandbox.migrations)) return "invalid manifest shape: sandbox.migrations must be an array";
287
- for (const migration of sandbox.migrations) {
288
- if (!isRecord(migration)) return "invalid manifest shape: sandbox.migrations entries must be objects";
289
- const migrationObject = migration;
290
- if (typeof migrationObject.tag !== "string") return "invalid manifest shape: sandbox.migrations entries must have a string tag";
291
- if (migrationObject.newSqliteClasses !== void 0 && (!Array.isArray(migrationObject.newSqliteClasses) || !migrationObject.newSqliteClasses.every((value) => typeof value === "string"))) return "invalid manifest shape: sandbox.migrations newSqliteClasses must be an array of strings";
292
- if (migrationObject.deletedClasses !== void 0 && (!Array.isArray(migrationObject.deletedClasses) || !migrationObject.deletedClasses.every((value) => typeof value === "string"))) return "invalid manifest shape: sandbox.migrations deletedClasses must be an array of strings";
293
- }
294
- }
253
+ const migrationsError = validateClassMigrations("sandbox.migrations", sandbox.migrations, ["newSqliteClasses", "deletedClasses"]);
254
+ if (migrationsError) return migrationsError;
295
255
  }
296
256
  if (manifest.live !== void 0) {
297
257
  if (!isRecord(manifest.live)) return "invalid manifest shape: live must be an object";
@@ -299,14 +259,35 @@ function validateManifestShape(parsed) {
299
259
  for (const key of ["bindingName", "className"]) if (typeof live[key] !== "string" || live[key].length === 0) return `invalid manifest shape: live.${key} must be a non-empty string`;
300
260
  if (live.storage !== "sqlite") return "invalid manifest shape: live.storage must be \"sqlite\"";
301
261
  if (!Array.isArray(live.classes) || !live.classes.every((x) => typeof x === "string")) return "invalid manifest shape: live.classes must be an array of strings";
302
- if (live.migrations !== void 0) {
303
- if (!Array.isArray(live.migrations)) return "invalid manifest shape: live.migrations must be an array";
304
- for (const migration of live.migrations) {
305
- if (!isRecord(migration)) return "invalid manifest shape: live.migrations entries must be objects";
306
- const migrationObject = migration;
307
- if (typeof migrationObject.tag !== "string") return "invalid manifest shape: live.migrations entries must have a string tag";
308
- if (migrationObject.newSqliteClasses !== void 0 && (!Array.isArray(migrationObject.newSqliteClasses) || !migrationObject.newSqliteClasses.every((value) => typeof value === "string"))) return "invalid manifest shape: live.migrations newSqliteClasses must be an array of strings";
309
- }
262
+ const migrationsError = validateClassMigrations("live.migrations", live.migrations, ["newSqliteClasses"]);
263
+ if (migrationsError) return migrationsError;
264
+ }
265
+ if (manifest.durableObjects !== void 0) {
266
+ const durable = manifest.durableObjects;
267
+ if (manifest.version !== 4 || !isRecord(durable) || !Array.isArray(durable.bindings)) return "invalid manifest shape: durableObjects requires manifest version 4 and a bindings array";
268
+ if (durable.bindings.length > 32) return "invalid manifest shape: at most 32 custom Durable Objects are supported";
269
+ const names = /* @__PURE__ */ new Set();
270
+ const classes = /* @__PURE__ */ new Set();
271
+ const otherBindings = /* @__PURE__ */ new Set([
272
+ ...Object.values(bindings).filter(Array.isArray).flat(),
273
+ ...Object.keys(isRecord(manifest.vars) ? manifest.vars : {}),
274
+ ...Array.isArray(manifest.queues) ? manifest.queues.map((q) => q.bindingName) : [],
275
+ ...isRecord(manifest.websocket) && Array.isArray(manifest.websocket.routes) ? manifest.websocket.routes.map((r) => r.bindingName ?? r.className) : [],
276
+ ...isRecord(manifest.live) ? [manifest.live.bindingName] : []
277
+ ]);
278
+ for (const binding of durable.bindings) {
279
+ if (!isRecord(binding) || Object.keys(binding).some((key) => ![
280
+ "bindingName",
281
+ "className",
282
+ "storage"
283
+ ].includes(key)) || typeof binding.bindingName !== "string" || !/^[A-Za-z_$][\w$]{0,127}$/.test(binding.bindingName) || binding.bindingName.startsWith("__") || [
284
+ "ASSETS",
285
+ "SEND_EMAIL",
286
+ "USAGE_EVENTS_QUEUE"
287
+ ].includes(binding.bindingName) || typeof binding.className !== "string" || !/^[A-Za-z_$][\w$]{0,127}$/.test(binding.className) || binding.storage !== "sqlite") return "invalid manifest shape: custom Durable Objects require local SQLite classes and valid binding names";
288
+ if (names.has(binding.bindingName) || classes.has(binding.className) || otherBindings.has(binding.bindingName)) return "invalid manifest shape: custom Durable Object bindings and classes must be unique";
289
+ names.add(binding.bindingName);
290
+ classes.add(binding.className);
310
291
  }
311
292
  }
312
293
  return null;
@@ -426,6 +407,19 @@ function canonicalJson(value) {
426
407
  throw new TypeError(`Unsupported canonical JSON value: ${typeof value}`);
427
408
  }
428
409
  //#endregion
410
+ //#region src/artifact-digest.ts
411
+ /** Commit to all executable bytes, asset hashes, bindings, migrations, and routing. */
412
+ async function deployArtifactDigest(artifact) {
413
+ const host = globalThis;
414
+ const data = canonicalJson({
415
+ manifest: artifact.manifest,
416
+ assetManifest: artifact.assetManifest,
417
+ workerFiles: artifact.workerFiles ?? {}
418
+ });
419
+ const bytes = await host.crypto.subtle.digest("SHA-256", new host.TextEncoder().encode(data));
420
+ return Array.from(new Uint8Array(bytes), (byte) => byte.toString(16).padStart(2, "0")).join("");
421
+ }
422
+ //#endregion
429
423
  //#region src/sql-tokenize.ts
430
424
  function* tokenizeSql(sql) {
431
425
  const sqlLength = sql.length;
@@ -639,7 +633,7 @@ function getMigrationPlanHashInput(plan) {
639
633
  }
640
634
  //#endregion
641
635
  //#region src/index.ts
642
- const SUPPORTED_MANIFEST_VERSIONS = [3];
636
+ const SUPPORTED_MANIFEST_VERSIONS = [3, 4];
643
637
  /**
644
638
  * Preserve an existing WebSocket class history while assigning every genuinely
645
639
  * new class to SQLite-backed Durable Object storage.
@@ -718,4 +712,4 @@ function isSandboxRegistryImage(image) {
718
712
  return /^registry\.cloudflare\.com\/[^/@\s]+\/[^@\s]+@sha256:[a-f\d]{64}$/.test(image);
719
713
  }
720
714
  //#endregion
721
- export { DEPLOY_ARTIFACT_VERSION, MANIFEST_REDIRECT_RULE_STATUSES, MAX_REVALIDATE_QUERY_ALLOWLIST_ENTRIES, MAX_ROUTING_RULES, MIGRATION_CLASSIFIER_VERSION, SUPPORTED_MANIFEST_VERSIONS, UNSPLIT_MULTI_STATEMENT_PRAGMA, appendSqliteClassMigration, canonicalJson, canonicalSql, classifySqlMigration, createDeployArtifact, createDeployIntent, createDeployResourcePlan, createDeployUploadPayload, getMigrationPlanHashInput, hasStandalonePragma, isRecord, isSandboxRegistryImage, isSingleStatement, planWebSocketMigrations, splitSqlStatements, tokenizeSql, validateManifestShape };
715
+ export { DEPLOY_ARTIFACT_VERSION, MANIFEST_REDIRECT_RULE_STATUSES, MAX_REVALIDATE_QUERY_ALLOWLIST_ENTRIES, MAX_ROUTING_RULES, MIGRATION_CLASSIFIER_VERSION, SUPPORTED_MANIFEST_VERSIONS, UNSPLIT_MULTI_STATEMENT_PRAGMA, appendSqliteClassMigration, canonicalJson, canonicalSql, classifySqlMigration, createDeployArtifact, createDeployIntent, createDeployResourcePlan, createDeployUploadPayload, deployArtifactDigest, getMigrationPlanHashInput, hasStandalonePragma, isRecord, isSandboxRegistryImage, isSingleStatement, planWebSocketMigrations, splitSqlStatements, tokenizeSql, validateManifestShape };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@void/deploy-core",
3
- "version": "0.27.0",
3
+ "version": "1.0.3",
4
4
  "description": "Shared deployment contracts, manifests, and resource plans for Void.",
5
5
  "homepage": "https://void.cloud",
6
6
  "bugs": {
@@ -27,7 +27,7 @@
27
27
  },
28
28
  "devDependencies": {
29
29
  "typescript": "7.0.2",
30
- "vite-plus": "1.1.0"
30
+ "vite-plus": "1.2.0"
31
31
  },
32
32
  "scripts": {
33
33
  "build": "vp pack",