@void/deploy-core 0.25.0 → 0.26.1

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/README.md CHANGED
@@ -1,7 +1,11 @@
1
1
  # @void/deploy-core
2
2
 
3
- Shared deployment contracts for Void runtimes and deployment adapters. Includes manifests, progress events, routing rules, resource plans, and SQL and Durable Object migration metadata.
3
+ Shared deployment contracts for [Void](https://void.cloud) runtimes and deployment adapters. Includes manifests, progress events, routing rules, resource plans, and SQL and Durable Object migration metadata.
4
4
 
5
- This package works in Node.js and Cloudflare Workers. Applications normally use the `void` CLI rather than these contracts directly.
5
+ This package works in Node.js and Cloudflare Workers. To deploy an application, use the `void` CLI.
6
6
 
7
- See [Deployment](https://void.cloud/guide/deployment) for deploying an app and [Platform Development](https://void.cloud/guide/platform-development) for working on a runtime or adapter.
7
+ ## Documentation
8
+
9
+ See [Deployment](https://void.cloud/guide/deployment) for deploying an app and [Platform Development](https://void.cloud/guide/platform-development) for building a runtime or adapter.
10
+
11
+ Read the [Void documentation](https://void.cloud) online or use the Markdown guides and references in `node_modules/void/docs/`, included with the `void` package.
package/dist/index.d.mts CHANGED
@@ -2,6 +2,14 @@
2
2
  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
+ /**
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.
12
+ */
5
13
  export declare function validateManifestShape(parsed: unknown): string | null;
6
14
  //#endregion
7
15
  //#region src/resource-plan.d.ts
@@ -175,8 +183,8 @@ export declare function isRecord(value: unknown): value is Record<string, unknow
175
183
  //#endregion
176
184
  //#region src/sql-tokenize.d.ts
177
185
  /**
178
- * Char-by-char SQL tokenizer shared by the destructive-SQL guard
179
- * (projects.ts) and the single-statement classifier (remote-runner.ts).
186
+ * Char-by-char SQL tokenizer shared by the migration classifier, the
187
+ * destructive-SQL guard, and the remote migration runner.
180
188
  *
181
189
  * Walks the input and yields contiguous segments. `sql` runs are
182
190
  * emitted as `{ kind: 'sql', start, end }` so callers can `sql.slice`
@@ -192,12 +200,12 @@ export declare function isRecord(value: unknown): value is Record<string, unknow
192
200
  * `]` closes the identifier (SQLite semantics;
193
201
  * differs from MS SQL)
194
202
  *
195
- * Not handled — intentional, both current callers target SQLite only:
203
+ * Not handled — intentional, every caller targets SQLite only:
196
204
  * - Postgres `$tag$ ... $tag$` dollar-quoting
197
205
  * - Postgres `E'...'` extended string literals
198
206
  *
199
- * Adding a new quoting form anywhere should land here so both callers
200
- * stay in lockstep.
207
+ * Adding a new quoting form anywhere should land here so every caller
208
+ * stays in lockstep.
201
209
  */
202
210
  type SqlSegment = {
203
211
  kind: "sql";
@@ -247,6 +255,12 @@ type MigrationPlanHashInput = SqlMigrationClassification & {
247
255
  export declare function canonicalSql(sql: string): string;
248
256
  export declare function hasStandalonePragma(sql: string, pragma: string): boolean;
249
257
  export declare function isSingleStatement(sql: string): boolean;
258
+ /**
259
+ * Split SQL on top-level semicolons, ignoring literals and comments. Returns
260
+ * null when a statement may contain a compound `BEGIN ... END` body (such as
261
+ * CREATE TRIGGER), whose inner semicolons cannot be told apart lexically.
262
+ */
263
+ export declare function splitSqlStatements(sql: string): Array<string> | null;
250
264
  /** Classify SQL without imposing the caller's execution policy or hashing implementation. */
251
265
  export declare function classifySqlMigration(sql: string): SqlMigrationClassification;
252
266
  /** Preserve the classifier-v1 hash preimage; policy markers and warnings are not hashed. */
@@ -285,7 +299,10 @@ export type RedirectRule = {
285
299
  export type AssetConfig = {
286
300
  not_found_handling?: "none" | "single-page-application" | "404-page";
287
301
  html_handling?: "auto-trailing-slash" | "force-trailing-slash" | "drop-trailing-slash" | "none";
288
- /** Native Cloudflare Workers Assets routing; ignored by managed dispatch. */
302
+ /**
303
+ * Native Cloudflare Workers Assets routing. Managed dispatch does not route
304
+ * by it; it reads it only to tell whether the app's worker answered a request.
305
+ */
289
306
  run_worker_first?: boolean | Array<string>;
290
307
  };
291
308
  export type WebSocketRouteMeta = {
package/dist/index.mjs CHANGED
@@ -1,3 +1,9 @@
1
+ //#region src/is-record.ts
2
+ /** Narrow object-shaped input without accepting null or arrays. */
3
+ function isRecord(value) {
4
+ return typeof value === "object" && value !== null && !Array.isArray(value);
5
+ }
6
+ //#endregion
1
7
  //#region src/validate.ts
2
8
  const MANIFEST_REDIRECT_RULE_STATUSES = /* @__PURE__ */ new Set([
3
9
  200,
@@ -10,6 +16,8 @@ const MANIFEST_REDIRECT_RULE_STATUSES = /* @__PURE__ */ new Set([
10
16
  const MAX_ROUTING_RULES = 2e3;
11
17
  const MAX_REVALIDATE_QUERY_ALLOWLIST_ENTRIES = MAX_ROUTING_RULES;
12
18
  const MAX_PAGES = 5e3;
19
+ const MAX_CRON_EXPRESSIONS = 100;
20
+ const MAX_SCHEDULES = 1e3;
13
21
  /**
14
22
  * Strict literal-hostname regex used to validate `redirectRules[].host` /
15
23
  * `fallbackRules[].host` on the deploy manifest. Mirrors the regex in
@@ -35,11 +43,19 @@ function validateRuleHost(ruleObject, ruleKind, sourceRef) {
35
43
  ruleObject.host = normalized;
36
44
  return null;
37
45
  }
46
+ /**
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.
53
+ */
38
54
  function validateManifestShape(parsed) {
39
- if (parsed == null || typeof parsed !== "object" || Array.isArray(parsed)) return "invalid manifest shape: expected a JSON object";
55
+ if (!isRecord(parsed)) return "invalid manifest shape: expected a JSON object";
40
56
  const manifest = parsed;
41
57
  if (manifest.emailProtocolVersion !== void 0 && manifest.emailProtocolVersion !== 1) return "invalid manifest shape: emailProtocolVersion must be 1";
42
- if (manifest.bindings == null || typeof manifest.bindings !== "object" || Array.isArray(manifest.bindings)) return "invalid manifest shape: bindings must be an object";
58
+ if (!isRecord(manifest.bindings)) return "invalid manifest shape: bindings must be an object";
43
59
  const bindings = manifest.bindings;
44
60
  for (const key of [
45
61
  "d1",
@@ -54,7 +70,7 @@ function validateManifestShape(parsed) {
54
70
  if (manifest.queues !== void 0) {
55
71
  if (!Array.isArray(manifest.queues)) return "invalid manifest shape: queues must be an array";
56
72
  for (const queue of manifest.queues) {
57
- if (queue == null || typeof queue !== "object" || Array.isArray(queue)) return "invalid manifest shape: queues entries must be objects";
73
+ if (!isRecord(queue)) return "invalid manifest shape: queues entries must be objects";
58
74
  const queueObject = queue;
59
75
  if (typeof queueObject.name !== "string" || typeof queueObject.bindingName !== "string") return "invalid manifest shape: queues entries must have string name and bindingName";
60
76
  for (const tuning of [
@@ -71,21 +87,25 @@ function validateManifestShape(parsed) {
71
87
  }
72
88
  if (manifest.schedules !== void 0) {
73
89
  if (!Array.isArray(manifest.schedules)) return "invalid manifest shape: schedules must be an array";
90
+ if (manifest.schedules.length > MAX_SCHEDULES) return `invalid manifest shape: schedules has ${manifest.schedules.length} entries, exceeds the ${MAX_SCHEDULES} per-project limit`;
91
+ const cronExpressions = /* @__PURE__ */ new Set();
74
92
  for (const schedule of manifest.schedules) {
75
- if (schedule == null || typeof schedule !== "object" || Array.isArray(schedule)) return "invalid manifest shape: schedules entries must be objects";
93
+ if (!isRecord(schedule)) return "invalid manifest shape: schedules entries must be objects";
76
94
  const scheduleObject = schedule;
77
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);
78
97
  }
98
+ 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`;
79
99
  }
80
100
  if (manifest.prerender !== void 0) {
81
101
  if (!Array.isArray(manifest.prerender) || !manifest.prerender.every((x) => typeof x === "string")) return "invalid manifest shape: prerender must be an array of strings";
82
102
  }
83
103
  if (manifest.vars !== void 0) {
84
- if (manifest.vars == null || typeof manifest.vars !== "object" || Array.isArray(manifest.vars)) return "invalid manifest shape: vars must be an object";
104
+ if (!isRecord(manifest.vars)) return "invalid manifest shape: vars must be an object";
85
105
  for (const [k, v] of Object.entries(manifest.vars)) if (typeof v !== "string") return `invalid manifest shape: vars.${k} must be a string`;
86
106
  }
87
107
  if (manifest.migrations !== void 0) {
88
- if (manifest.migrations == null || typeof manifest.migrations !== "object" || Array.isArray(manifest.migrations)) return "invalid manifest shape: migrations must be an object";
108
+ if (!isRecord(manifest.migrations)) return "invalid manifest shape: migrations must be an object";
89
109
  const mig = manifest.migrations;
90
110
  if (mig.dialect !== void 0) {
91
111
  const allowed = [
@@ -118,7 +138,7 @@ function validateManifestShape(parsed) {
118
138
  if (!Array.isArray(manifest.compatibilityFlags) || !manifest.compatibilityFlags.every((x) => typeof x === "string")) return "invalid manifest shape: compatibilityFlags must be an array of strings";
119
139
  }
120
140
  if (manifest.limits !== void 0) {
121
- if (manifest.limits == null || typeof manifest.limits !== "object" || Array.isArray(manifest.limits)) return "invalid manifest shape: limits must be an object";
141
+ if (!isRecord(manifest.limits)) return "invalid manifest shape: limits must be an object";
122
142
  const limits = manifest.limits;
123
143
  if (limits.cpuMs !== void 0) {
124
144
  if (typeof limits.cpuMs !== "number" || !Number.isSafeInteger(limits.cpuMs) || limits.cpuMs < 1) return "invalid manifest shape: limits.cpuMs must be a positive integer";
@@ -134,7 +154,7 @@ function validateManifestShape(parsed) {
134
154
  if (typeof manifest.type !== "string" || !allowed.includes(manifest.type)) return `invalid manifest shape: type must be one of ${allowed.join(", ")}`;
135
155
  }
136
156
  if (manifest.assetConfig !== void 0) {
137
- if (manifest.assetConfig == null || typeof manifest.assetConfig !== "object" || Array.isArray(manifest.assetConfig)) return "invalid manifest shape: assetConfig must be an object";
157
+ if (!isRecord(manifest.assetConfig)) return "invalid manifest shape: assetConfig must be an object";
138
158
  const assetConfig = manifest.assetConfig;
139
159
  if (assetConfig.not_found_handling !== void 0 && ![
140
160
  "none",
@@ -149,13 +169,13 @@ function validateManifestShape(parsed) {
149
169
  ].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";
150
170
  }
151
171
  if (manifest.revalidate !== void 0) {
152
- if (typeof manifest.revalidate === "number") {} else if (manifest.revalidate == null || typeof manifest.revalidate !== "object" || Array.isArray(manifest.revalidate)) return "invalid manifest shape: revalidate must be a number or object";
172
+ if (typeof manifest.revalidate === "number") {} else if (!isRecord(manifest.revalidate)) return "invalid manifest shape: revalidate must be a number or object";
153
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`;
154
174
  }
155
175
  if (manifest.headerRules !== void 0) {
156
176
  if (!Array.isArray(manifest.headerRules)) return "invalid manifest shape: headerRules must be an array";
157
177
  for (const rule of manifest.headerRules) {
158
- if (rule == null || typeof rule !== "object" || Array.isArray(rule)) return "invalid manifest shape: headerRules entries must be objects";
178
+ if (!isRecord(rule)) return "invalid manifest shape: headerRules entries must be objects";
159
179
  const ruleObject = rule;
160
180
  if (typeof ruleObject.pattern !== "string") return "invalid manifest shape: headerRules entries must have a string pattern";
161
181
  if (!Array.isArray(ruleObject.headers)) return "invalid manifest shape: headerRules entries must have a headers array";
@@ -166,7 +186,7 @@ function validateManifestShape(parsed) {
166
186
  if (!Array.isArray(manifest.redirectRules)) return "invalid manifest shape: redirectRules must be an array";
167
187
  if (manifest.redirectRules.length > 2e3) return `invalid manifest shape: redirectRules has ${manifest.redirectRules.length} entries, exceeds the ${MAX_ROUTING_RULES} per-list limit`;
168
188
  for (const rule of manifest.redirectRules) {
169
- if (rule == null || typeof rule !== "object" || Array.isArray(rule)) return "invalid manifest shape: redirectRules entries must be objects";
189
+ if (!isRecord(rule)) return "invalid manifest shape: redirectRules entries must be objects";
170
190
  const ruleObject = rule;
171
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";
172
192
  const sourceRef = JSON.stringify(ruleObject.source);
@@ -181,7 +201,7 @@ function validateManifestShape(parsed) {
181
201
  }
182
202
  }
183
203
  if (manifest.revalidateQueryAllowlist !== void 0) {
184
- if (manifest.revalidateQueryAllowlist == null || typeof manifest.revalidateQueryAllowlist !== "object" || Array.isArray(manifest.revalidateQueryAllowlist)) return "invalid manifest shape: revalidateQueryAllowlist must be an object";
204
+ if (!isRecord(manifest.revalidateQueryAllowlist)) return "invalid manifest shape: revalidateQueryAllowlist must be an object";
185
205
  const entries = Object.entries(manifest.revalidateQueryAllowlist);
186
206
  if (entries.length > 2e3) return `invalid manifest shape: revalidateQueryAllowlist has ${entries.length} entries, exceeds the ${MAX_REVALIDATE_QUERY_ALLOWLIST_ENTRIES} entry limit`;
187
207
  for (const [pattern, value] of entries) {
@@ -194,21 +214,21 @@ function validateManifestShape(parsed) {
194
214
  if (!Array.isArray(manifest.pages)) return "invalid manifest shape: pages must be an array";
195
215
  if (manifest.pages.length > MAX_PAGES) return `invalid manifest shape: pages has ${manifest.pages.length} entries, exceeds the ${MAX_PAGES} entry limit`;
196
216
  for (const entry of manifest.pages) {
197
- if (entry == null || typeof entry !== "object" || Array.isArray(entry)) return "invalid manifest shape: each pages entry must be an object";
217
+ if (!isRecord(entry)) return "invalid manifest shape: each pages entry must be an object";
198
218
  const e = entry;
199
219
  if (typeof e.pattern !== "string" || e.pattern === "") return "invalid manifest shape: pages entry pattern must be a string";
200
220
  if (e.revalidate !== void 0 && typeof e.revalidate !== "number") return "invalid manifest shape: pages entry revalidate must be a number";
201
221
  }
202
222
  }
203
223
  if (manifest.pathRevalidate !== void 0) {
204
- if (manifest.pathRevalidate == null || typeof manifest.pathRevalidate !== "object" || Array.isArray(manifest.pathRevalidate)) return "invalid manifest shape: pathRevalidate must be an object";
224
+ if (!isRecord(manifest.pathRevalidate)) return "invalid manifest shape: pathRevalidate must be an object";
205
225
  for (const [pathname, value] of Object.entries(manifest.pathRevalidate)) if (typeof value !== "number") return `invalid manifest shape: pathRevalidate.${pathname} must be a number`;
206
226
  }
207
227
  if (manifest.fallbackRules !== void 0) {
208
228
  if (!Array.isArray(manifest.fallbackRules)) return "invalid manifest shape: fallbackRules must be an array";
209
229
  if (manifest.fallbackRules.length > 2e3) return `invalid manifest shape: fallbackRules has ${manifest.fallbackRules.length} entries, exceeds the ${MAX_ROUTING_RULES} per-list limit`;
210
230
  for (const rule of manifest.fallbackRules) {
211
- if (rule == null || typeof rule !== "object" || Array.isArray(rule)) return "invalid manifest shape: fallbackRules entries must be objects";
231
+ if (!isRecord(rule)) return "invalid manifest shape: fallbackRules entries must be objects";
212
232
  const ruleObject = rule;
213
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";
214
234
  const sourceRef = JSON.stringify(ruleObject.source);
@@ -222,11 +242,11 @@ function validateManifestShape(parsed) {
222
242
  }
223
243
  }
224
244
  if (manifest.websocket !== void 0) {
225
- if (manifest.websocket == null || typeof manifest.websocket !== "object" || Array.isArray(manifest.websocket)) return "invalid manifest shape: websocket must be an object";
245
+ if (!isRecord(manifest.websocket)) return "invalid manifest shape: websocket must be an object";
226
246
  const websocket = manifest.websocket;
227
247
  if (!Array.isArray(websocket.routes)) return "invalid manifest shape: websocket.routes must be an array";
228
248
  for (const route of websocket.routes) {
229
- if (route == null || typeof route !== "object" || Array.isArray(route)) return "invalid manifest shape: websocket.routes entries must be objects";
249
+ if (!isRecord(route)) return "invalid manifest shape: websocket.routes entries must be objects";
230
250
  const routeObject = route;
231
251
  if (typeof routeObject.className !== "string") return "invalid manifest shape: websocket.routes entries must have a string className";
232
252
  if (routeObject.bindingName !== void 0 && typeof routeObject.bindingName !== "string") return "invalid manifest shape: websocket.routes entry bindingName must be a string";
@@ -235,7 +255,7 @@ function validateManifestShape(parsed) {
235
255
  if (websocket.migrations !== void 0) {
236
256
  if (!Array.isArray(websocket.migrations)) return "invalid manifest shape: websocket.migrations must be an array";
237
257
  for (const migration of websocket.migrations) {
238
- if (migration == null || typeof migration !== "object" || Array.isArray(migration)) return "invalid manifest shape: websocket.migrations entries must be objects";
258
+ if (!isRecord(migration)) return "invalid manifest shape: websocket.migrations entries must be objects";
239
259
  const migrationObject = migration;
240
260
  if (typeof migrationObject.tag !== "string" || migrationObject.tag.length === 0) return "invalid manifest shape: websocket.migrations entries must have a non-empty string tag";
241
261
  for (const field of [
@@ -251,7 +271,7 @@ function validateManifestShape(parsed) {
251
271
  }
252
272
  if (manifest.sandbox !== void 0) {
253
273
  if (manifest.type === "static" || manifest.type === "spa") return "sandbox is not supported for static/spa deployments";
254
- if (manifest.sandbox == null || typeof manifest.sandbox !== "object" || Array.isArray(manifest.sandbox)) return "invalid manifest shape: sandbox must be an object";
274
+ if (!isRecord(manifest.sandbox)) return "invalid manifest shape: sandbox must be an object";
255
275
  const sandbox = manifest.sandbox;
256
276
  for (const key of [
257
277
  "bindingName",
@@ -265,7 +285,7 @@ function validateManifestShape(parsed) {
265
285
  if (sandbox.migrations !== void 0) {
266
286
  if (!Array.isArray(sandbox.migrations)) return "invalid manifest shape: sandbox.migrations must be an array";
267
287
  for (const migration of sandbox.migrations) {
268
- if (migration == null || typeof migration !== "object" || Array.isArray(migration)) return "invalid manifest shape: sandbox.migrations entries must be objects";
288
+ if (!isRecord(migration)) return "invalid manifest shape: sandbox.migrations entries must be objects";
269
289
  const migrationObject = migration;
270
290
  if (typeof migrationObject.tag !== "string") return "invalid manifest shape: sandbox.migrations entries must have a string tag";
271
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";
@@ -274,7 +294,7 @@ function validateManifestShape(parsed) {
274
294
  }
275
295
  }
276
296
  if (manifest.live !== void 0) {
277
- if (manifest.live == null || typeof manifest.live !== "object" || Array.isArray(manifest.live)) return "invalid manifest shape: live must be an object";
297
+ if (!isRecord(manifest.live)) return "invalid manifest shape: live must be an object";
278
298
  const live = manifest.live;
279
299
  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`;
280
300
  if (live.storage !== "sqlite") return "invalid manifest shape: live.storage must be \"sqlite\"";
@@ -282,7 +302,7 @@ function validateManifestShape(parsed) {
282
302
  if (live.migrations !== void 0) {
283
303
  if (!Array.isArray(live.migrations)) return "invalid manifest shape: live.migrations must be an array";
284
304
  for (const migration of live.migrations) {
285
- if (migration == null || typeof migration !== "object" || Array.isArray(migration)) return "invalid manifest shape: live.migrations entries must be objects";
305
+ if (!isRecord(migration)) return "invalid manifest shape: live.migrations entries must be objects";
286
306
  const migrationObject = migration;
287
307
  if (typeof migrationObject.tag !== "string") return "invalid manifest shape: live.migrations entries must have a string tag";
288
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";
@@ -292,8 +312,7 @@ function validateManifestShape(parsed) {
292
312
  return null;
293
313
  }
294
314
  function isLocalImageReference(image) {
295
- const lowerImage = image.toLowerCase();
296
- return image.startsWith(".") || image.startsWith("/") || lowerImage === "dockerfile" || lowerImage.includes("dockerfile") || lowerImage.endsWith("/dockerfile") || lowerImage.endsWith("\\dockerfile");
315
+ return image.startsWith(".") || image.startsWith("/") || image.toLowerCase().includes("dockerfile");
297
316
  }
298
317
  //#endregion
299
318
  //#region src/resource-plan.ts
@@ -407,12 +426,6 @@ function canonicalJson(value) {
407
426
  throw new TypeError(`Unsupported canonical JSON value: ${typeof value}`);
408
427
  }
409
428
  //#endregion
410
- //#region src/is-record.ts
411
- /** Narrow object-shaped input without accepting null or arrays. */
412
- function isRecord(value) {
413
- return typeof value === "object" && value !== null && !Array.isArray(value);
414
- }
415
- //#endregion
416
429
  //#region src/sql-tokenize.ts
417
430
  function* tokenizeSql(sql) {
418
431
  const sqlLength = sql.length;
@@ -519,63 +532,80 @@ const UNSPLIT_MULTI_STATEMENT_PRAGMA = "-- void:allow-unsplit-multi-statement";
519
532
  function canonicalSql(sql) {
520
533
  return (sql.charCodeAt(0) === 65279 ? sql.slice(1) : sql).replace(/\r\n?/g, "\n");
521
534
  }
522
- function isStandalonePragmaLine(line, pragma) {
523
- return line.trim() === pragma;
524
- }
525
535
  function hasStandalonePragma(sql, pragma) {
526
- return canonicalSql(sql).split("\n").some((line) => isStandalonePragmaLine(line, pragma));
536
+ return canonicalSql(sql).split("\n").some((line) => line.trim() === pragma);
527
537
  }
528
- function isBreakpointComment(text) {
529
- return text.trim() === "--> statement-breakpoint";
538
+ /** Yield `--> statement-breakpoint` line comments outside literals. */
539
+ function* breakpointComments(normalized) {
540
+ for (const segment of tokenizeSql(normalized)) if (segment.kind === "line-comment" && normalized.slice(segment.start, segment.end).trim() === "--> statement-breakpoint") yield segment;
530
541
  }
531
542
  function splitBreakpointFramedSql(sql) {
532
543
  const normalized = canonicalSql(sql);
533
544
  const statements = [];
534
545
  let cursor = 0;
535
- for (const segment of tokenizeSql(normalized)) {
536
- if (segment.kind !== "line-comment") continue;
537
- if (isBreakpointComment(normalized.slice(segment.start, segment.end))) {
538
- const statement = normalized.slice(cursor, segment.start).trim();
539
- if (statement) statements.push(statement);
540
- cursor = segment.end;
541
- }
546
+ for (const segment of breakpointComments(normalized)) {
547
+ const statement = normalized.slice(cursor, segment.start).trim();
548
+ if (statement) statements.push(statement);
549
+ cursor = segment.end;
542
550
  }
543
551
  const statement = normalized.slice(cursor).trim();
544
552
  if (statement) statements.push(statement);
545
553
  return statements;
546
554
  }
547
555
  function hasStatementBreakpointOutsideLiterals(sql) {
548
- const normalized = canonicalSql(sql);
549
- for (const segment of tokenizeSql(normalized)) {
550
- if (segment.kind !== "line-comment") continue;
551
- if (isBreakpointComment(normalized.slice(segment.start, segment.end))) return true;
552
- }
553
- return false;
556
+ return !breakpointComments(canonicalSql(sql)).next().done;
554
557
  }
555
- function isSingleStatement(sql) {
558
+ function scanStatements(sql) {
556
559
  const normalized = canonicalSql(sql);
557
- let statements = 0;
560
+ const ranges = [];
561
+ let cursor = 0;
558
562
  let inStatement = false;
563
+ let hasCompoundBody = false;
559
564
  for (const segment of tokenizeSql(normalized)) {
560
565
  if (segment.kind === "line-comment" || segment.kind === "block-comment") continue;
561
566
  if (segment.kind === "string" || segment.kind === "ident-quote") {
562
567
  inStatement = true;
563
568
  continue;
564
569
  }
570
+ if (/\b(?:BEGIN|TRIGGER)\b/i.test(normalized.slice(segment.start, segment.end))) hasCompoundBody = true;
565
571
  for (let index = segment.start; index < segment.end; index++) {
566
572
  const character = normalized[index];
567
573
  if (character === ";") {
568
574
  if (inStatement) {
569
- statements++;
575
+ ranges.push({
576
+ start: cursor,
577
+ end: index + 1
578
+ });
570
579
  inStatement = false;
571
580
  }
581
+ cursor = index + 1;
572
582
  continue;
573
583
  }
574
584
  if (character !== " " && character !== " " && character !== "\n" && character !== "\r") inStatement = true;
575
585
  }
576
586
  }
577
- if (inStatement) statements++;
578
- return statements === 1;
587
+ if (inStatement) ranges.push({
588
+ start: cursor,
589
+ end: normalized.length
590
+ });
591
+ return {
592
+ normalized,
593
+ ranges,
594
+ hasCompoundBody
595
+ };
596
+ }
597
+ function isSingleStatement(sql) {
598
+ return scanStatements(sql).ranges.length === 1;
599
+ }
600
+ /**
601
+ * Split SQL on top-level semicolons, ignoring literals and comments. Returns
602
+ * null when a statement may contain a compound `BEGIN ... END` body (such as
603
+ * CREATE TRIGGER), whose inner semicolons cannot be told apart lexically.
604
+ */
605
+ function splitSqlStatements(sql) {
606
+ const { normalized, ranges, hasCompoundBody } = scanStatements(sql);
607
+ if (hasCompoundBody) return null;
608
+ return ranges.map((range) => normalized.slice(range.start, range.end).trim());
579
609
  }
580
610
  /** Classify SQL without imposing the caller's execution policy or hashing implementation. */
581
611
  function classifySqlMigration(sql) {
@@ -688,4 +718,4 @@ function isSandboxRegistryImage(image) {
688
718
  return /^registry\.cloudflare\.com\/[^/@\s]+\/[^@\s]+@sha256:[a-f\d]{64}$/.test(image);
689
719
  }
690
720
  //#endregion
691
- 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, tokenizeSql, validateManifestShape };
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 };
package/package.json CHANGED
@@ -1,6 +1,12 @@
1
1
  {
2
2
  "name": "@void/deploy-core",
3
- "version": "0.25.0",
3
+ "version": "0.26.1",
4
+ "description": "Shared deployment contracts, manifests, and resource plans for Void.",
5
+ "homepage": "https://void.cloud",
6
+ "bugs": {
7
+ "url": "https://github.com/voidzero-dev/void/issues"
8
+ },
9
+ "license": "MIT",
4
10
  "repository": {
5
11
  "type": "git",
6
12
  "url": "git+https://github.com/voidzero-dev/void.git",
@@ -21,7 +27,7 @@
21
27
  },
22
28
  "devDependencies": {
23
29
  "typescript": "7.0.2",
24
- "vite-plus": "1.0.0"
30
+ "vite-plus": "1.1.0"
25
31
  },
26
32
  "scripts": {
27
33
  "build": "vp pack",