void 0.7.13 → 0.8.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/dist/{agents-DqkFfc2c.mjs → agents-Dkgafwfr.mjs} +1 -1
- package/dist/{auth-cmd-bS88H4Lb.mjs → auth-cmd-B1-7r6ng.mjs} +3 -3
- package/dist/{better-auth-shared-D2JaoCs4.mjs → better-auth-shared-CbAF6sOM.mjs} +1 -1
- package/dist/{better-auth-shared-HXlHRw02.d.mts → better-auth-shared-Fzyk5yS1.d.mts} +2 -7
- package/dist/{cache-DUDCNty6.mjs → cache-CGYC-2LW.mjs} +5 -5
- package/dist/{cancel-deploy-DOuSbPwX.mjs → cancel-deploy-B4j31z7d.mjs} +4 -4
- package/dist/canonical-json-CEyQaVDa.mjs +13 -0
- package/dist/cli/cli.mjs +44 -31
- package/dist/{client-C_DYiXeu.mjs → client-C-nH6yz3.mjs} +5 -5
- package/dist/{collect-HqAuilH6.mjs → collect-9rO3JFNM.mjs} +1 -1
- package/dist/{config-DyUSwbeX.mjs → config--wXmVOoe.mjs} +10 -8
- package/dist/{config-CeRQg3Lz.mjs → config-BVEC0lti.mjs} +11 -2
- package/dist/{config-CvHtTM0q.mjs → config-hhMYPVRT.mjs} +3 -12
- package/dist/{create-project-hvFe6rbI.mjs → create-project-BGnQMy5N.mjs} +44 -18
- package/dist/{db-YEYSTfGw.mjs → db-CT5Xg6ds.mjs} +42 -37
- package/dist/{delete-XyM_VD9o.mjs → delete-eMGTHHB9.mjs} +5 -5
- package/dist/{deploy-CPAEgVQm.mjs → deploy-DbmbcKID.mjs} +209 -127
- package/dist/{discover-CKGQpis8.mjs → discover-C8N1I4tK.mjs} +9 -6
- package/dist/{domain-Dvj7u8tr.mjs → domain-UfLrp2vc.mjs} +64 -22
- package/dist/{drizzle-Cx7cVwrE.mjs → drizzle-DmaANP4-.mjs} +1 -1
- package/dist/{env-Bjl0rzuf.mjs → env-Cvdbbm_N.mjs} +7 -7
- package/dist/{env-helpers-Dw95CU8e.d.mts → env-helpers-Bt9yYg5I.d.mts} +1 -1
- package/dist/{env-types-BSQghXbx.mjs → env-types-D6qI1ThV.mjs} +8 -19
- package/dist/{env-validation-Buy5pe3u.mjs → env-validation-DBJsxZLz.mjs} +4 -4
- package/dist/{gen-DU8NIdrH.mjs → gen-Cn2hZ8RR.mjs} +37 -31
- package/dist/{handler-DJTuxB10.d.mts → handler-BPhL8AmU.d.mts} +3 -3
- package/dist/{headers-CuVHn6Kl.mjs → headers-wAPJigtE.mjs} +3 -3
- package/dist/index.d.mts +3 -3
- package/dist/index.mjs +273 -104
- package/dist/{init-Br5DTaG7.mjs → init-CbNCFFWx.mjs} +105 -35
- package/dist/{link-DdXqlUiJ.mjs → link-C5x1E-XZ.mjs} +5 -5
- package/dist/{list-9Bxa5Odn.mjs → list-ezL7hqDV.mjs} +5 -5
- package/dist/{login-C_MfDjhu.mjs → login-CkLr-AaI.mjs} +3 -3
- package/dist/{logs-DjqCpEER.mjs → logs-MnGILmLz.mjs} +4 -4
- package/dist/{mcp-vA0880_K.mjs → mcp-B84MmVAW.mjs} +3 -3
- package/dist/{node-gxRQXIRT.mjs → node-fpywdcbN.mjs} +6 -6
- package/dist/{output-D3g5otMC.mjs → output-DeiS4oEX.mjs} +1 -1
- package/dist/pages/client.d.mts +1 -1
- package/dist/pages/head-client.d.mts +1 -1
- package/dist/pages/head.d.mts +1 -1
- package/dist/pages/index.d.mts +28 -3
- package/dist/pages/index.mjs +6 -5
- package/dist/pages/islands-plugin.d.mts +1 -1
- package/dist/pages/islands-plugin.mjs +7 -5
- package/dist/pages/protocol.d.mts +2 -2
- package/dist/pages/protocol.mjs +1 -1
- package/dist/{pathe.M-eThtNZ-D-kmWkCS.mjs → pathe.M-eThtNZ-BrPhGF_K.mjs} +1 -1
- package/dist/plan-aD3wRDqF.mjs +271 -0
- package/dist/{plugin-inference-BexV6LL3.mjs → plugin-inference-Ba1uGdIy.mjs} +26 -20
- package/dist/{prepare-BEkH89oX.mjs → prepare-BrAISonG.mjs} +29 -19
- package/dist/{preset-CYQh93UF.mjs → preset-BRsCi8ZB.mjs} +10 -8
- package/dist/{project-DhB4A8_g.mjs → project-CH9pdo16.mjs} +1 -1
- package/dist/{project-cmd-CeXO06hG.mjs → project-cmd-JQUzv3qF.mjs} +10 -10
- package/dist/project-paths-CCMrHYQm.mjs +100 -0
- package/dist/{project-slug-KRvHQEQI.mjs → project-slug-De4UjtD9.mjs} +0 -1
- package/dist/{project-tsconfig-Bfy55qvs.mjs → project-tsconfig-HAGjfY6w.mjs} +10 -5
- package/dist/{protocol-D7r_deVZ.d.mts → protocol-mcSsdMij.d.mts} +3 -3
- package/dist/providers--_pvxpNN.d.mts +7 -0
- package/dist/{resolve-project-DdjLQ2tB.mjs → resolve-project-CNpUbUHd.mjs} +1 -1
- package/dist/{rollback-C8OSMxmq.mjs → rollback-CoAAvC18.mjs} +4 -4
- package/dist/{route-types-qVL-v-CB.mjs → route-types-jxRfWuCb.mjs} +1 -1
- package/dist/{runner-C0d3lJo1.mjs → runner-BkGBfr4e.mjs} +4 -2
- package/dist/{runner-pg-Bd_172vF.mjs → runner-pg-BXP0JpHW.mjs} +1 -1
- package/dist/runtime/ai.mjs +2 -2
- package/dist/runtime/auth.d.mts +1 -1
- package/dist/runtime/better-auth-pg.d.mts +1 -1
- package/dist/runtime/better-auth-pg.mjs +2 -2
- package/dist/runtime/better-auth.d.mts +1 -1
- package/dist/runtime/better-auth.mjs +2 -2
- package/dist/runtime/client.d.mts +2 -2
- package/dist/runtime/client.mjs +1 -1
- package/dist/runtime/env-helpers.d.mts +1 -1
- package/dist/runtime/env-public-client.d.mts +1 -1
- package/dist/runtime/env-public.d.mts +2 -2
- package/dist/runtime/env-public.mjs +1 -1
- package/dist/runtime/env.mjs +1 -1
- package/dist/runtime/fetch-stream.d.mts +1 -1
- package/dist/runtime/fetch-stream.mjs +1 -1
- package/dist/runtime/fetch.d.mts +1 -1
- package/dist/runtime/fetch.mjs +1 -1
- package/dist/runtime/handler.d.mts +1 -1
- package/dist/runtime/handler.mjs +1 -1
- package/dist/runtime/isr.mjs +1 -1
- package/dist/runtime/live.d.mts +2 -2
- package/dist/runtime/live.mjs +1 -1
- package/dist/runtime/migration-handler.d.mts +33 -1
- package/dist/runtime/migration-handler.mjs +332 -24
- package/dist/runtime/remote/index.mjs +1 -1
- package/dist/runtime/sandbox.d.mts +73 -3
- package/dist/runtime/sandbox.mjs +239 -3
- package/dist/runtime/validator.d.mts +1 -1
- package/dist/runtime/ws-server.d.mts +2 -2
- package/dist/runtime/ws.d.mts +3 -3
- package/dist/{scan-CKpylFj9.mjs → scan-2YmJkYAf.mjs} +4 -3
- package/dist/{scan-CkK1Hzpa.mjs → scan-DzzHqtiT.mjs} +82 -43
- package/dist/{secret-NDvlqAN2.mjs → secret-mz8t_CML.mjs} +5 -5
- package/dist/{skills-SAG0WyVn.mjs → skills-B_ynF4lA.mjs} +3 -3
- package/dist/{subcommand-prompt-q9T06dpg.mjs → subcommand-prompt-CPuV7tPW.mjs} +2 -2
- package/dist/sveltekit.mjs +1 -1
- package/dist/{validate-DW3ViKTA.mjs → validate-CNWm-PsL.mjs} +49 -9
- package/dist/{yarn-pnp-6LD6_3Ej.mjs → yarn-pnp-QgEYVC3V.mjs} +1 -1
- package/package.json +3 -2
- package/schema.json +5 -0
- package/skills/void/docs/guide/ai.md +1 -1
- package/skills/void/docs/guide/app-types.md +1 -1
- package/skills/void/docs/guide/database/postgresql.md +4 -3
- package/skills/void/docs/guide/edge/headers.md +1 -1
- package/skills/void/docs/guide/edge/static-assets.md +34 -0
- package/skills/void/docs/guide/sandboxes.md +3 -2
- package/skills/void/docs/guide/ssr.md +25 -2
- package/skills/void/docs/integrations/cloudflare.md +5 -3
- package/skills/void/docs/integrations/frameworks/overview.md +1 -1
- package/skills/void/docs/integrations/nodejs-bun-deno.md +10 -9
- package/skills/void/docs/reference/api.md +204 -13
- package/skills/void/docs/reference/cli.md +53 -7
- package/skills/void/docs/reference/config.md +27 -4
- package/skills/void/docs/reference/resource-inference.md +1 -1
- /package/dist/{auth-DmuALf16.d.mts → auth-SfRBhXan.d.mts} +0 -0
- /package/dist/{auth-migrations-BwLPwRgH.mjs → auth-migrations-BqJoGqGQ.mjs} +0 -0
- /package/dist/{cf-access-BW8K93Fm.mjs → cf-access-DKDsgwOU.mjs} +0 -0
- /package/dist/{defer-2ARBu8Et.mjs → defer-DkoEwda-.mjs} +0 -0
- /package/dist/{dist-q8b2Mjgb.mjs → dist-DR9sIMbM.mjs} +0 -0
- /package/dist/{dist-D2L3_KTK.mjs → dist-abzUneor.mjs} +0 -0
- /package/dist/{dotenv-HQNhRalS.mjs → dotenv-Bkoqyq9r.mjs} +0 -0
- /package/dist/{env-raw-DtfQ9E31.mjs → env-raw-zk5JQKZe.mjs} +0 -0
- /package/dist/{fetch-error-DZ868Xq4.d.mts → fetch-error-BbixZ9vW.d.mts} +0 -0
- /package/dist/{fetch-error-CEr0ACTl.mjs → fetch-error-rXermVU3.mjs} +0 -0
- /package/dist/{head-C7QW7UY1.d.mts → head-CZs8dMxG.d.mts} +0 -0
- /package/dist/{log-7ChR5Fbc.mjs → log-CWWZV4V1.mjs} +0 -0
- /package/dist/{magic-string.es-DS_lmuBe.mjs → magic-string.es-C1Fb0uxq.mjs} +0 -0
- /package/dist/{providers-BlFNPYen.mjs → providers-uC0PJg1c.mjs} +0 -0
- /package/dist/{standard-schema-D2qvCEYV.d.mts → standard-schema-WhHcCaqJ.d.mts} +0 -0
- /package/dist/{types-DG_Ynnyd.d.mts → types-C8wsi1hv.d.mts} +0 -0
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { t as canonicalJson } from "../canonical-json-CEyQaVDa.mjs";
|
|
1
2
|
//#region src/runtime/migration-handler.ts
|
|
2
|
-
|
|
3
|
+
const SUPPORTED_CLASSIFIER_VERSION = 1;
|
|
4
|
+
function createMigrationHandler(env, migrations, _dialect, runAuthMigrations, planBundle) {
|
|
3
5
|
return async (request) => {
|
|
4
6
|
const token = request.headers.get("x-void-internal");
|
|
5
7
|
if (!token || token !== env.__VOID_PROXY_TOKEN) return Response.json({
|
|
@@ -7,6 +9,7 @@ function createMigrationHandler(env, migrations, _dialect, runAuthMigrations) {
|
|
|
7
9
|
error: "unauthorized",
|
|
8
10
|
migrations: []
|
|
9
11
|
}, { status: 401 });
|
|
12
|
+
const url = new URL(request.url);
|
|
10
13
|
try {
|
|
11
14
|
const db = env.DB;
|
|
12
15
|
if (!db) return Response.json({
|
|
@@ -14,8 +17,57 @@ function createMigrationHandler(env, migrations, _dialect, runAuthMigrations) {
|
|
|
14
17
|
error: "no D1 binding found",
|
|
15
18
|
migrations: []
|
|
16
19
|
}, { status: 500 });
|
|
17
|
-
|
|
18
|
-
|
|
20
|
+
if (request.method === "GET" && url.pathname === "/__void/migrate/capabilities") {
|
|
21
|
+
const verified = await verifyPlanBundle(migrations ?? [], planBundle);
|
|
22
|
+
return Response.json({
|
|
23
|
+
classifierVersion: verified.classifierVersion,
|
|
24
|
+
destructiveValidation: verified.destructiveValidation,
|
|
25
|
+
dialect: "sqlite",
|
|
26
|
+
endpoints: {
|
|
27
|
+
app: "/__void/migrate/app",
|
|
28
|
+
auth: runAuthMigrations ? "/__void/migrate/auth" : void 0,
|
|
29
|
+
reconcile: "/__void/migrate/app/attempt/:attemptId"
|
|
30
|
+
},
|
|
31
|
+
migrationListHash: verified.migrationListHash,
|
|
32
|
+
protocolVersion: 2
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
if (request.method === "POST" && url.pathname === "/__void/migrate/app") {
|
|
36
|
+
const body = await request.json().catch(() => ({}));
|
|
37
|
+
if (!body.attemptId) return Response.json({
|
|
38
|
+
ok: false,
|
|
39
|
+
protocolVersion: 2,
|
|
40
|
+
error: "attemptId required",
|
|
41
|
+
migrations: []
|
|
42
|
+
}, { status: 400 });
|
|
43
|
+
const result = await runD1AppMigrations(db, migrations ?? [], planBundle, {
|
|
44
|
+
attemptId: body.attemptId,
|
|
45
|
+
deploymentId: body.deploymentId
|
|
46
|
+
});
|
|
47
|
+
const status = result.ok ? 200 : result.error?.startsWith("migration already in progress") ? 409 : 500;
|
|
48
|
+
return Response.json(result, { status });
|
|
49
|
+
}
|
|
50
|
+
if (request.method === "GET" && url.pathname.startsWith("/__void/migrate/app/attempt/")) {
|
|
51
|
+
const result = await reconcileAttempt(db, decodeURIComponent(url.pathname.split("/").pop() ?? ""));
|
|
52
|
+
return Response.json(result, { status: result.ok ? 200 : 409 });
|
|
53
|
+
}
|
|
54
|
+
if (request.method === "POST" && url.pathname === "/__void/migrate/auth") {
|
|
55
|
+
if (runAuthMigrations) await runAuthMigrations();
|
|
56
|
+
return Response.json({
|
|
57
|
+
ok: true,
|
|
58
|
+
protocolVersion: 2
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
const result = await runD1LegacyMigrations(db, migrations ?? []);
|
|
62
|
+
if (result.ok && runAuthMigrations) try {
|
|
63
|
+
await runAuthMigrations();
|
|
64
|
+
} catch (err) {
|
|
65
|
+
return Response.json({
|
|
66
|
+
...result,
|
|
67
|
+
ok: false,
|
|
68
|
+
error: err instanceof Error ? err.message : String(err)
|
|
69
|
+
}, { status: 500 });
|
|
70
|
+
}
|
|
19
71
|
return Response.json(result);
|
|
20
72
|
} catch (err) {
|
|
21
73
|
return Response.json({
|
|
@@ -26,10 +78,29 @@ function createMigrationHandler(env, migrations, _dialect, runAuthMigrations) {
|
|
|
26
78
|
}
|
|
27
79
|
};
|
|
28
80
|
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
81
|
+
async function createTrackingTable(db) {
|
|
82
|
+
await db.prepare(`CREATE TABLE IF NOT EXISTS _void_migrations (
|
|
83
|
+
name TEXT PRIMARY KEY,
|
|
84
|
+
applied_at TEXT NOT NULL
|
|
85
|
+
)`).run();
|
|
86
|
+
}
|
|
87
|
+
async function createAttemptTables(db) {
|
|
88
|
+
await db.batch([db.prepare(`CREATE TABLE IF NOT EXISTS _void_migration_attempts (
|
|
89
|
+
attempt_id TEXT PRIMARY KEY,
|
|
90
|
+
deployment_id TEXT,
|
|
91
|
+
migration_list_hash TEXT NOT NULL,
|
|
92
|
+
planned_names_json TEXT NOT NULL,
|
|
93
|
+
plan_hashes_json TEXT NOT NULL,
|
|
94
|
+
status TEXT NOT NULL CHECK (status IN ('running', 'completed', 'failed', 'unknown')),
|
|
95
|
+
started_at TEXT NOT NULL,
|
|
96
|
+
completed_at TEXT,
|
|
97
|
+
error TEXT
|
|
98
|
+
)`), db.prepare(`CREATE TABLE IF NOT EXISTS _void_migration_lock (
|
|
99
|
+
name TEXT PRIMARY KEY,
|
|
100
|
+
attempt_id TEXT NOT NULL,
|
|
101
|
+
acquired_at TEXT NOT NULL
|
|
102
|
+
)`)]);
|
|
103
|
+
}
|
|
33
104
|
async function upgradeTrackingTable(db) {
|
|
34
105
|
const { results } = await db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='_migrations'").all();
|
|
35
106
|
if (!results || results.length === 0) return;
|
|
@@ -42,19 +113,257 @@ async function upgradeTrackingTable(db) {
|
|
|
42
113
|
db.prepare(`DROP TABLE _migrations`)
|
|
43
114
|
]);
|
|
44
115
|
}
|
|
45
|
-
async function
|
|
46
|
-
await db.prepare(`CREATE TABLE IF NOT EXISTS _void_migrations (
|
|
47
|
-
name TEXT PRIMARY KEY,
|
|
48
|
-
applied_at TEXT NOT NULL
|
|
49
|
-
)`).run();
|
|
50
|
-
await upgradeTrackingTable(db);
|
|
116
|
+
async function readAppliedSet(db) {
|
|
51
117
|
const { results } = await db.prepare("SELECT name FROM _void_migrations").all();
|
|
52
|
-
|
|
118
|
+
return new Set((results ?? []).map((row) => row.name));
|
|
119
|
+
}
|
|
120
|
+
async function runD1AppMigrations(db, migrations, planBundle, attempt) {
|
|
121
|
+
const verified = await verifyPlanBundle(migrations, planBundle);
|
|
122
|
+
await createAttemptTables(db);
|
|
123
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
124
|
+
const lock = await acquireAttemptLock(db, attempt.attemptId, now);
|
|
125
|
+
if (lock !== "acquired") return lock;
|
|
126
|
+
const plannedNames = migrations.map((migration) => migration.name);
|
|
127
|
+
const planHashes = verified.plans.map((plan) => plan.planHash);
|
|
128
|
+
try {
|
|
129
|
+
await db.prepare(`INSERT INTO _void_migration_attempts
|
|
130
|
+
(attempt_id, deployment_id, migration_list_hash, planned_names_json, plan_hashes_json, status, started_at)
|
|
131
|
+
VALUES (?, ?, ?, ?, ?, 'running', ?)`).bind(attempt.attemptId, attempt.deploymentId ?? null, verified.migrationListHash, JSON.stringify(plannedNames), JSON.stringify(planHashes), now).run();
|
|
132
|
+
} catch {
|
|
133
|
+
await releaseAttemptLock(db, attempt.attemptId);
|
|
134
|
+
return reconcileAttempt(db, attempt.attemptId);
|
|
135
|
+
}
|
|
136
|
+
const migrationResults = [];
|
|
137
|
+
const confirmedApplied = [];
|
|
138
|
+
const confirmedSkipped = [];
|
|
139
|
+
const warnings = verified.plans.flatMap((plan) => plan.warnings ?? []);
|
|
140
|
+
let stability = "stable";
|
|
141
|
+
try {
|
|
142
|
+
await createTrackingTable(db);
|
|
143
|
+
await upgradeTrackingTable(db);
|
|
144
|
+
const applied = await readAppliedSet(db);
|
|
145
|
+
for (let index = 0; index < migrations.length; index++) {
|
|
146
|
+
const migration = migrations[index];
|
|
147
|
+
const plan = verified.plans[index];
|
|
148
|
+
if (plan.stability === "legacy") stability = "legacy";
|
|
149
|
+
if (applied.has(migration.name)) {
|
|
150
|
+
confirmedSkipped.push(migration.name);
|
|
151
|
+
migrationResults.push({
|
|
152
|
+
applied: false,
|
|
153
|
+
durationMs: 0,
|
|
154
|
+
name: migration.name,
|
|
155
|
+
stability: plan.stability,
|
|
156
|
+
status: "skipped"
|
|
157
|
+
});
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
const start = Date.now();
|
|
161
|
+
try {
|
|
162
|
+
await executeMigrationPlan(db, migration, plan);
|
|
163
|
+
applied.add(migration.name);
|
|
164
|
+
confirmedApplied.push(migration.name);
|
|
165
|
+
migrationResults.push({
|
|
166
|
+
applied: true,
|
|
167
|
+
durationMs: Date.now() - start,
|
|
168
|
+
name: migration.name,
|
|
169
|
+
stability: plan.stability,
|
|
170
|
+
status: "applied"
|
|
171
|
+
});
|
|
172
|
+
} catch (err) {
|
|
173
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
174
|
+
migrationResults.push({
|
|
175
|
+
applied: false,
|
|
176
|
+
durationMs: Date.now() - start,
|
|
177
|
+
error,
|
|
178
|
+
name: migration.name,
|
|
179
|
+
stability: plan.stability,
|
|
180
|
+
status: "failed"
|
|
181
|
+
});
|
|
182
|
+
await markAttempt(db, attempt.attemptId, "failed", error);
|
|
183
|
+
await releaseAttemptLock(db, attempt.attemptId);
|
|
184
|
+
return {
|
|
185
|
+
ok: false,
|
|
186
|
+
protocolVersion: 2,
|
|
187
|
+
attemptId: attempt.attemptId,
|
|
188
|
+
confirmedApplied,
|
|
189
|
+
confirmedSkipped,
|
|
190
|
+
error: `migration ${migration.name} failed`,
|
|
191
|
+
migrations: migrationResults,
|
|
192
|
+
stability,
|
|
193
|
+
warnings
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
await markAttempt(db, attempt.attemptId, "completed");
|
|
198
|
+
await releaseAttemptLock(db, attempt.attemptId);
|
|
199
|
+
return {
|
|
200
|
+
ok: true,
|
|
201
|
+
protocolVersion: 2,
|
|
202
|
+
attemptId: attempt.attemptId,
|
|
203
|
+
confirmedApplied,
|
|
204
|
+
confirmedSkipped,
|
|
205
|
+
migrations: migrationResults,
|
|
206
|
+
stability,
|
|
207
|
+
warnings
|
|
208
|
+
};
|
|
209
|
+
} catch (err) {
|
|
210
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
211
|
+
await markAttempt(db, attempt.attemptId, "failed", error).catch(() => {});
|
|
212
|
+
await releaseAttemptLock(db, attempt.attemptId).catch(() => {});
|
|
213
|
+
return {
|
|
214
|
+
ok: false,
|
|
215
|
+
protocolVersion: 2,
|
|
216
|
+
attemptId: attempt.attemptId,
|
|
217
|
+
confirmedApplied,
|
|
218
|
+
confirmedSkipped,
|
|
219
|
+
error,
|
|
220
|
+
migrations: migrationResults,
|
|
221
|
+
stability,
|
|
222
|
+
warnings
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
async function acquireAttemptLock(db, attemptId, acquiredAt) {
|
|
227
|
+
for (let tries = 0; tries < 2; tries++) try {
|
|
228
|
+
await db.prepare("INSERT INTO _void_migration_lock (name, attempt_id, acquired_at) VALUES (?, ?, ?)").bind("app", attemptId, acquiredAt).run();
|
|
229
|
+
return "acquired";
|
|
230
|
+
} catch {
|
|
231
|
+
const existing = await db.prepare("SELECT attempt_id FROM _void_migration_lock WHERE name = ?").bind("app").first();
|
|
232
|
+
if (existing?.attempt_id === attemptId) return reconcileAttempt(db, attemptId);
|
|
233
|
+
if (existing?.attempt_id) {
|
|
234
|
+
const row = await readAttempt(db, existing.attempt_id);
|
|
235
|
+
if (!row || row.status === "completed" || row.status === "failed" || row.status === "unknown") {
|
|
236
|
+
await releaseAttemptLock(db, existing.attempt_id).catch(() => {});
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return {
|
|
241
|
+
ok: false,
|
|
242
|
+
protocolVersion: 2,
|
|
243
|
+
attemptId,
|
|
244
|
+
migrations: [],
|
|
245
|
+
error: `migration already in progress for attempt ${existing?.attempt_id ?? "unknown"}`
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
return {
|
|
249
|
+
ok: false,
|
|
250
|
+
protocolVersion: 2,
|
|
251
|
+
attemptId,
|
|
252
|
+
migrations: [],
|
|
253
|
+
error: "migration lock could not be acquired"
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
async function executeMigrationPlan(db, migration, plan) {
|
|
257
|
+
const appliedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
258
|
+
if (plan.kind === "single-statement") {
|
|
259
|
+
await db.batch([db.prepare(migration.sql), db.prepare("INSERT INTO _void_migrations (name, applied_at) VALUES (?, ?)").bind(migration.name, appliedAt)]);
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
if (plan.kind === "breakpoint-framed") {
|
|
263
|
+
await db.batch([...plan.statements.map((statement) => db.prepare(statement)), db.prepare("INSERT INTO _void_migrations (name, applied_at) VALUES (?, ?)").bind(migration.name, appliedAt)]);
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
await db.exec(migration.sql);
|
|
267
|
+
await db.prepare("INSERT OR IGNORE INTO _void_migrations (name, applied_at) VALUES (?, ?)").bind(migration.name, appliedAt).run();
|
|
268
|
+
}
|
|
269
|
+
async function markAttempt(db, attemptId, status, error) {
|
|
270
|
+
await db.prepare(`UPDATE _void_migration_attempts
|
|
271
|
+
SET status = ?, completed_at = ?, error = ?
|
|
272
|
+
WHERE attempt_id = ?`).bind(status, (/* @__PURE__ */ new Date()).toISOString(), error ?? null, attemptId).run();
|
|
273
|
+
}
|
|
274
|
+
async function releaseAttemptLock(db, attemptId) {
|
|
275
|
+
await db.prepare("DELETE FROM _void_migration_lock WHERE name = ? AND attempt_id = ?").bind("app", attemptId).run();
|
|
276
|
+
}
|
|
277
|
+
async function readAttempt(db, attemptId) {
|
|
278
|
+
return db.prepare("SELECT * FROM _void_migration_attempts WHERE attempt_id = ?").bind(attemptId).first();
|
|
279
|
+
}
|
|
280
|
+
async function reconcileAttempt(db, attemptId) {
|
|
281
|
+
await createAttemptTables(db);
|
|
282
|
+
await createTrackingTable(db);
|
|
283
|
+
const row = await readAttempt(db, attemptId);
|
|
284
|
+
if (!row) {
|
|
285
|
+
await releaseAttemptLock(db, attemptId).catch(() => {});
|
|
286
|
+
return {
|
|
287
|
+
ok: false,
|
|
288
|
+
protocolVersion: 2,
|
|
289
|
+
attemptId,
|
|
290
|
+
migrations: [],
|
|
291
|
+
error: "migration attempt not found"
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
const plannedNames = JSON.parse(row.planned_names_json);
|
|
295
|
+
const applied = await readAppliedSet(db);
|
|
296
|
+
const confirmed = plannedNames.filter((name) => applied.has(name));
|
|
297
|
+
const complete = confirmed.length === plannedNames.length;
|
|
298
|
+
if (complete && row.status !== "completed") await markAttempt(db, attemptId, "completed").catch(() => {});
|
|
299
|
+
if (!complete && row.status === "running") await markAttempt(db, attemptId, "unknown", row.error ?? "migration outcome unknown").catch(() => {});
|
|
300
|
+
await releaseAttemptLock(db, attemptId).catch(() => {});
|
|
301
|
+
return {
|
|
302
|
+
ok: complete,
|
|
303
|
+
protocolVersion: 2,
|
|
304
|
+
attemptId,
|
|
305
|
+
confirmedApplied: [],
|
|
306
|
+
confirmedSkipped: confirmed,
|
|
307
|
+
migrations: plannedNames.map((name) => ({
|
|
308
|
+
applied: false,
|
|
309
|
+
durationMs: 0,
|
|
310
|
+
name,
|
|
311
|
+
status: applied.has(name) ? "skipped" : "failed"
|
|
312
|
+
})),
|
|
313
|
+
error: complete ? void 0 : row.error ?? "migration outcome unknown",
|
|
314
|
+
stability: "stable"
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
async function verifyPlanBundle(migrations, planBundle) {
|
|
318
|
+
if (!planBundle || planBundle.protocolVersion !== 2) throw new Error("migration protocol v2 execution plans are missing");
|
|
319
|
+
if (planBundle.classifierVersion !== SUPPORTED_CLASSIFIER_VERSION || planBundle.destructiveValidation !== "passed") throw new Error("unsupported migration execution plan metadata");
|
|
320
|
+
if (planBundle.plans.length !== migrations.length) throw new Error("migration execution plan count does not match bundled migrations");
|
|
321
|
+
for (let index = 0; index < migrations.length; index++) {
|
|
322
|
+
const migration = migrations[index];
|
|
323
|
+
const plan = planBundle.plans[index];
|
|
324
|
+
if (plan.name !== migration.name || plan.dialect !== "sqlite" || plan.classifierVersion !== SUPPORTED_CLASSIFIER_VERSION) throw new Error(`invalid migration execution plan for ${migration.name}`);
|
|
325
|
+
const sqlHash = await sha256Hex(canonicalSql(migration.sql));
|
|
326
|
+
if (plan.sqlHash !== sqlHash) throw new Error(`migration SQL hash mismatch for ${migration.name}`);
|
|
327
|
+
const planHash = await hashPlan(plan);
|
|
328
|
+
if (plan.planHash !== planHash) throw new Error(`migration plan hash mismatch for ${migration.name}`);
|
|
329
|
+
}
|
|
330
|
+
const migrationListHash = await sha256Hex(canonicalJson(planBundle.plans.map((plan) => [
|
|
331
|
+
plan.name,
|
|
332
|
+
plan.sqlHash,
|
|
333
|
+
plan.planHash
|
|
334
|
+
])));
|
|
335
|
+
if (planBundle.migrationListHash !== migrationListHash) throw new Error("migration list hash mismatch");
|
|
336
|
+
return planBundle;
|
|
337
|
+
}
|
|
338
|
+
function canonicalSql(sql) {
|
|
339
|
+
return (sql.charCodeAt(0) === 65279 ? sql.slice(1) : sql).replace(/\r\n?/g, "\n");
|
|
340
|
+
}
|
|
341
|
+
async function sha256Hex(value) {
|
|
342
|
+
const bytes = new TextEncoder().encode(value);
|
|
343
|
+
const hash = await crypto.subtle.digest("SHA-256", bytes);
|
|
344
|
+
return `sha256:${[...new Uint8Array(hash)].map((byte) => byte.toString(16).padStart(2, "0")).join("")}`;
|
|
345
|
+
}
|
|
346
|
+
async function hashPlan(plan) {
|
|
347
|
+
const hashInput = {
|
|
348
|
+
classifierVersion: plan.classifierVersion,
|
|
349
|
+
dialect: plan.dialect,
|
|
350
|
+
kind: plan.kind,
|
|
351
|
+
name: plan.name,
|
|
352
|
+
sqlHash: plan.sqlHash,
|
|
353
|
+
stability: plan.stability
|
|
354
|
+
};
|
|
355
|
+
if (plan.kind === "breakpoint-framed") hashInput.statements = plan.statements;
|
|
356
|
+
return sha256Hex(canonicalJson(hashInput));
|
|
357
|
+
}
|
|
358
|
+
async function runD1LegacyMigrations(db, migrations) {
|
|
359
|
+
await createTrackingTable(db);
|
|
360
|
+
await upgradeTrackingTable(db);
|
|
361
|
+
const applied = await readAppliedSet(db);
|
|
53
362
|
const migrationResults = [];
|
|
54
|
-
for (const
|
|
55
|
-
if (applied.has(
|
|
363
|
+
for (const migration of migrations) {
|
|
364
|
+
if (applied.has(migration.name)) {
|
|
56
365
|
migrationResults.push({
|
|
57
|
-
name:
|
|
366
|
+
name: migration.name,
|
|
58
367
|
applied: false,
|
|
59
368
|
durationMs: 0
|
|
60
369
|
});
|
|
@@ -62,19 +371,19 @@ async function runD1Migrations(db, migrations) {
|
|
|
62
371
|
}
|
|
63
372
|
const start = Date.now();
|
|
64
373
|
try {
|
|
65
|
-
const statements = splitMigrationSql(
|
|
374
|
+
const statements = splitMigrationSql(migration.sql);
|
|
66
375
|
if (statements.length === 1) if (isSingleSqlStatement(statements[0])) await db.prepare(statements[0]).run();
|
|
67
|
-
else await db.exec(
|
|
376
|
+
else await db.exec(migration.sql);
|
|
68
377
|
else for (const statement of statements) await db.prepare(statement).run();
|
|
69
|
-
await db.prepare("INSERT OR IGNORE INTO _void_migrations (name, applied_at) VALUES (?, ?)").bind(
|
|
378
|
+
await db.prepare("INSERT OR IGNORE INTO _void_migrations (name, applied_at) VALUES (?, ?)").bind(migration.name, (/* @__PURE__ */ new Date()).toISOString()).run();
|
|
70
379
|
migrationResults.push({
|
|
71
|
-
name:
|
|
380
|
+
name: migration.name,
|
|
72
381
|
applied: true,
|
|
73
382
|
durationMs: Date.now() - start
|
|
74
383
|
});
|
|
75
384
|
} catch (err) {
|
|
76
385
|
migrationResults.push({
|
|
77
|
-
name:
|
|
386
|
+
name: migration.name,
|
|
78
387
|
applied: false,
|
|
79
388
|
durationMs: Date.now() - start,
|
|
80
389
|
error: err instanceof Error ? err.message : String(err)
|
|
@@ -82,7 +391,7 @@ async function runD1Migrations(db, migrations) {
|
|
|
82
391
|
return {
|
|
83
392
|
ok: false,
|
|
84
393
|
migrations: migrationResults,
|
|
85
|
-
error: `migration ${
|
|
394
|
+
error: `migration ${migration.name} failed`
|
|
86
395
|
};
|
|
87
396
|
}
|
|
88
397
|
}
|
|
@@ -176,7 +485,6 @@ function isSingleSqlStatement(sql) {
|
|
|
176
485
|
if (character === ";") {
|
|
177
486
|
if (inStatement) {
|
|
178
487
|
statements++;
|
|
179
|
-
if (statements > 1) return false;
|
|
180
488
|
inStatement = false;
|
|
181
489
|
}
|
|
182
490
|
index++;
|
|
@@ -4,14 +4,84 @@ import { Sandbox, SandboxOptions } from "@cloudflare/sandbox";
|
|
|
4
4
|
//#region src/runtime/sandbox.d.ts
|
|
5
5
|
declare const DEFAULT_SANDBOX_ID = "default";
|
|
6
6
|
declare const DEFAULT_SANDBOX_BINDING = "SANDBOX";
|
|
7
|
-
|
|
7
|
+
declare const SANDBOX_USAGE_QUEUE_BINDING = "USAGE_EVENTS_QUEUE";
|
|
8
|
+
declare const SANDBOX_GATE_BINDING = "__VOID_PROXY";
|
|
9
|
+
declare const HEARTBEAT_MS: number;
|
|
8
10
|
type VoidSandboxOptions = SandboxOptions & {
|
|
9
11
|
binding?: string;
|
|
10
12
|
};
|
|
13
|
+
type SandboxUsageEvent = Readonly<{
|
|
14
|
+
requests: 0;
|
|
15
|
+
responseBytes: 0;
|
|
16
|
+
userId?: string;
|
|
17
|
+
projectId?: string;
|
|
18
|
+
slug?: string;
|
|
19
|
+
sandboxRuntimeSeconds: number;
|
|
20
|
+
sandboxInstanceId: string;
|
|
21
|
+
}>;
|
|
22
|
+
/**
|
|
23
|
+
* Thrown by `getSandbox()` when the platform gate denies a new acquire because
|
|
24
|
+
* the user has hit a concurrency or runtime budget cap. Surface this to the
|
|
25
|
+
* caller so they can render a friendly error; do not retry transparently.
|
|
26
|
+
*/
|
|
27
|
+
declare class SandboxLimitError extends Error {
|
|
28
|
+
override readonly name = "SandboxLimitError";
|
|
29
|
+
readonly reason: "concurrency" | "runtime_budget";
|
|
30
|
+
constructor(reason: "concurrency" | "runtime_budget", message?: string);
|
|
31
|
+
}
|
|
11
32
|
declare function __setSandboxBindingName(bindingName: string): void;
|
|
12
|
-
|
|
33
|
+
/**
|
|
34
|
+
* Sandbox variant that participates in Void platform metering: it records
|
|
35
|
+
* container runtime seconds into DO storage, emits periodic heartbeats to
|
|
36
|
+
* the usage queue, and notifies the gate when the container stops.
|
|
37
|
+
*
|
|
38
|
+
* Generated code from `void inference` wires user `Sandbox` subclasses to
|
|
39
|
+
* extend this class on platform deploys so per-tenant accounting works
|
|
40
|
+
* without user opt-in. On local dev (no usage-queue / gate bindings), all
|
|
41
|
+
* overrides degrade to no-ops and the sandbox behaves like the upstream
|
|
42
|
+
* `Sandbox` class.
|
|
43
|
+
*/
|
|
44
|
+
declare class VoidPlatformSandbox<Env = unknown> extends Sandbox<Env> {
|
|
45
|
+
override onStart(): Promise<void>;
|
|
46
|
+
override alarm(alarmProps?: {
|
|
47
|
+
isRetry: boolean;
|
|
48
|
+
retryCount: number;
|
|
49
|
+
}): Promise<void>;
|
|
50
|
+
override onStop(): Promise<void>;
|
|
51
|
+
override onActivityExpired(): Promise<void>;
|
|
52
|
+
private flushFinal;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Resolve the platform-provided sandbox stub for the given id, optionally
|
|
56
|
+
* coordinating with the platform gate to enforce per-user concurrency and
|
|
57
|
+
* runtime budgets.
|
|
58
|
+
*
|
|
59
|
+
* When the gate binding (`__VOID_PROXY` by default) is present AND the
|
|
60
|
+
* platform's `__PROJECT_ID` + `__VOID_PROXY_TOKEN` identity vars are
|
|
61
|
+
* available, we POST to `https://gate/sandbox/acquire` with the HMAC-signed
|
|
62
|
+
* body the shared proxy auth middleware expects. A `409` response means the
|
|
63
|
+
* user has exceeded a budget — we throw `SandboxLimitError` so the caller
|
|
64
|
+
* can render a clear message. Any other non-OK status (or a thrown transport
|
|
65
|
+
* error from the service binding) is treated as "gate unavailable" and we
|
|
66
|
+
* fall through to returning the stub so a broken gate doesn't break sandbox
|
|
67
|
+
* access entirely.
|
|
68
|
+
*
|
|
69
|
+
* When the gate binding is absent (local dev) or the identity vars are
|
|
70
|
+
* missing (partial deploy), the gate check is skipped entirely and we
|
|
71
|
+
* return the stub directly.
|
|
72
|
+
*
|
|
73
|
+
* The stub is constructed BEFORE calling the gate so we can derive the
|
|
74
|
+
* normalized DO name (`stub.id.name`) and pass it to the gate as the
|
|
75
|
+
* `sandboxId`. This keeps acquire and release using the same key — without
|
|
76
|
+
* it, mixed-case caller ids (e.g. `MyProject-123`) would acquire a lease
|
|
77
|
+
* under the raw id but release it under the normalized lowercase form that
|
|
78
|
+
* `ctx.id.name` reports inside the DO, leaking the lease until TTL.
|
|
79
|
+
* Constructing the stub does NOT start a container — that only happens on
|
|
80
|
+
* the first method call against the stub.
|
|
81
|
+
*/
|
|
82
|
+
declare function getSandbox<T extends Sandbox = Sandbox>(id?: string, options?: VoidSandboxOptions): Promise<T>;
|
|
13
83
|
declare const sandbox: {
|
|
14
84
|
get: typeof getSandbox;
|
|
15
85
|
};
|
|
16
86
|
//#endregion
|
|
17
|
-
export { DEFAULT_SANDBOX_BINDING, DEFAULT_SANDBOX_ID, Sandbox, type SandboxOptions,
|
|
87
|
+
export { DEFAULT_SANDBOX_BINDING, DEFAULT_SANDBOX_ID, HEARTBEAT_MS, SANDBOX_GATE_BINDING, SANDBOX_USAGE_QUEUE_BINDING, Sandbox, SandboxLimitError, type SandboxOptions, SandboxUsageEvent, VoidPlatformSandbox, VoidSandboxOptions, __setSandboxBindingName, getSandbox, sandbox };
|