deepspace 0.2.7 → 0.3.2

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/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli/cli.ts
4
- import { defineCommand as defineCommand13, runMain } from "citty";
4
+ import { defineCommand as defineCommand14, runMain } from "citty";
5
5
  import { execSync as execSync5 } from "child_process";
6
6
  import { readFileSync as readFileSync9, existsSync as existsSync11 } from "fs";
7
7
  import { dirname as dirname2, join as join12 } from "path";
@@ -75,12 +75,127 @@ async function mintAppOwnerJwt(authUrl, callerJwt, appName) {
75
75
  }
76
76
  return body.token;
77
77
  }
78
+ var SDK_MANAGED_KEYS = /* @__PURE__ */ new Set([
79
+ "AUTH_JWT_PUBLIC_KEY",
80
+ "AUTH_JWT_ISSUER",
81
+ "AUTH_WORKER_URL",
82
+ "API_WORKER_URL",
83
+ "PLATFORM_WORKER_URL",
84
+ "OWNER_USER_ID",
85
+ "APP_OWNER_JWT",
86
+ "INTERNAL_STORAGE_HMAC_SECRET",
87
+ "ALLOW_DEBUG_ROUTES"
88
+ ]);
89
+ var DEV_VARS_DIVIDER = "# --- not managed by the SDK; preserved across dev/test runs ---";
90
+ function extractCustomDevVars(content) {
91
+ const lines = content.split("\n");
92
+ const out = [];
93
+ let i = 0;
94
+ while (i < lines.length) {
95
+ const line = lines[i];
96
+ if (line.trim() === DEV_VARS_DIVIDER.trim()) {
97
+ i++;
98
+ continue;
99
+ }
100
+ const eq = line.indexOf("=");
101
+ const keyMatch = eq >= 0 ? line.slice(0, eq).match(/^\s*([A-Za-z_][A-Za-z0-9_]*)\s*$/) : null;
102
+ if (!keyMatch) {
103
+ out.push(line);
104
+ i++;
105
+ continue;
106
+ }
107
+ const key = keyMatch[1];
108
+ const valStart = line.slice(eq + 1);
109
+ const block = [line];
110
+ if (valStart.startsWith('"') && !hasUnescapedQuote(valStart.slice(1))) {
111
+ i++;
112
+ while (i < lines.length) {
113
+ block.push(lines[i]);
114
+ const closed = hasUnescapedQuote(lines[i]);
115
+ i++;
116
+ if (closed) break;
117
+ }
118
+ } else {
119
+ i++;
120
+ }
121
+ if (!SDK_MANAGED_KEYS.has(key)) {
122
+ out.push(...block);
123
+ }
124
+ }
125
+ while (out.length && out[out.length - 1].trim() === "") out.pop();
126
+ return out.join("\n");
127
+ }
128
+ function hasUnescapedQuote(s) {
129
+ let escaped = false;
130
+ for (const ch of s) {
131
+ if (escaped) {
132
+ escaped = false;
133
+ continue;
134
+ }
135
+ if (ch === "\\") {
136
+ escaped = true;
137
+ continue;
138
+ }
139
+ if (ch === '"') return true;
140
+ }
141
+ return false;
142
+ }
143
+ function parseDevVars(content) {
144
+ const result = {};
145
+ const lines = content.split("\n");
146
+ let i = 0;
147
+ while (i < lines.length) {
148
+ const line = lines[i];
149
+ const trimmed = line.trim();
150
+ if (!trimmed || trimmed.startsWith("#")) {
151
+ i++;
152
+ continue;
153
+ }
154
+ const eq = line.indexOf("=");
155
+ if (eq < 0) {
156
+ i++;
157
+ continue;
158
+ }
159
+ const keyMatch = line.slice(0, eq).match(/^\s*([A-Za-z_][A-Za-z0-9_]*)\s*$/);
160
+ if (!keyMatch) {
161
+ i++;
162
+ continue;
163
+ }
164
+ const key = keyMatch[1];
165
+ let value = line.slice(eq + 1);
166
+ let closed = !value.startsWith('"') || hasUnescapedQuote(value.slice(1));
167
+ if (!closed) {
168
+ i++;
169
+ while (i < lines.length) {
170
+ value += "\n" + lines[i];
171
+ if (hasUnescapedQuote(lines[i])) {
172
+ closed = true;
173
+ i++;
174
+ break;
175
+ }
176
+ i++;
177
+ }
178
+ } else {
179
+ i++;
180
+ }
181
+ if (value.startsWith('"') && !closed) {
182
+ throw new Error(
183
+ `parseDevVars: unterminated quoted value for key "${key}" \u2014 check your .dev.vars file for a missing closing quote.`
184
+ );
185
+ }
186
+ if (value.startsWith('"') && value.endsWith('"') && value.length >= 2) {
187
+ value = value.slice(1, -1).replace(/\\"/g, '"');
188
+ }
189
+ result[key] = value;
190
+ }
191
+ return result;
192
+ }
78
193
  async function writeDevVars(appDir, env, ownerId, callerJwt) {
79
194
  const urls = ENVS[env];
80
195
  const publicKey = await fetchPublicKey(urls.auth);
81
196
  const appName = readAppName(appDir);
82
197
  const appOwnerJwt = await mintAppOwnerJwt(urls.auth, callerJwt, appName);
83
- const devVars = [
198
+ const sdkVars = [
84
199
  `AUTH_JWT_PUBLIC_KEY="${publicKey}"`,
85
200
  `AUTH_JWT_ISSUER=${urls.auth}/api/auth`,
86
201
  `AUTH_WORKER_URL=${urls.auth}`,
@@ -93,7 +208,16 @@ async function writeDevVars(appDir, env, ownerId, callerJwt) {
93
208
  // Production secrets never set this, so deployed apps 404 the route.
94
209
  `ALLOW_DEBUG_ROUTES=true`
95
210
  ].join("\n");
96
- writeFileSync(join(appDir, ".dev.vars"), devVars + "\n");
211
+ const devVarsPath = join(appDir, ".dev.vars");
212
+ const existing = existsSync(devVarsPath) ? readFileSync(devVarsPath, "utf-8") : "";
213
+ const custom = extractCustomDevVars(existing);
214
+ const body = custom ? `${sdkVars}
215
+
216
+ ${DEV_VARS_DIVIDER}
217
+ ${custom}
218
+ ` : `${sdkVars}
219
+ `;
220
+ writeFileSync(devVarsPath, body);
97
221
  }
98
222
 
99
223
  // src/cli/session.ts
@@ -392,13 +516,174 @@ function resolvePort(arg) {
392
516
  return n;
393
517
  }
394
518
 
395
- // src/cli/commands/test.ts
519
+ // src/cli/commands/kill.ts
396
520
  import { defineCommand as defineCommand4 } from "citty";
521
+ import { spawnSync } from "child_process";
522
+ import { setTimeout as wait } from "timers/promises";
523
+ var DEFAULT_PORT2 = 5173;
524
+ var SIGTERM_GRACE_MS = 1500;
525
+ var IS_WIN = process.platform === "win32";
526
+ var kill_default = defineCommand4({
527
+ meta: {
528
+ name: "kill",
529
+ description: "Kill the local dev server and any orphaned workerd processes"
530
+ },
531
+ args: {
532
+ port: {
533
+ type: "string",
534
+ description: `Port the dev server is bound to (default ${DEFAULT_PORT2}, or $DEEPSPACE_PORT)`,
535
+ required: false
536
+ },
537
+ all: {
538
+ type: "boolean",
539
+ description: "Also kill stray workerd/wrangler processes across all ports",
540
+ default: false
541
+ }
542
+ },
543
+ async run({ args }) {
544
+ const port = resolvePort2(args.port);
545
+ const targets = /* @__PURE__ */ new Set();
546
+ for (const pid of listenerPids(port)) targets.add(pid);
547
+ for (const pid of [...targets]) {
548
+ for (const child of childPids(pid)) targets.add(child);
549
+ }
550
+ if (args.all) {
551
+ for (const pid of processPidsByName(["workerd", "wrangler", "vite"])) {
552
+ targets.add(pid);
553
+ }
554
+ }
555
+ if (targets.size === 0) {
556
+ console.log(
557
+ args.all ? `Nothing listening on :${port} and no stray workerd/wrangler/vite processes found.` : `Nothing listening on :${port}. (Pass --all to sweep stray workerd/wrangler.)`
558
+ );
559
+ return;
560
+ }
561
+ for (const pid of targets) {
562
+ if (sendSignal(pid, "SIGTERM")) {
563
+ console.log(`SIGTERM \u2192 pid ${pid}`);
564
+ }
565
+ }
566
+ await wait(SIGTERM_GRACE_MS);
567
+ const stillAlive = [];
568
+ for (const pid of targets) {
569
+ if (isAlive(pid)) stillAlive.push(pid);
570
+ }
571
+ for (const pid of stillAlive) {
572
+ if (sendSignal(pid, "SIGKILL")) {
573
+ console.log(`SIGKILL \u2192 pid ${pid} (did not exit on SIGTERM)`);
574
+ }
575
+ }
576
+ await wait(200);
577
+ const remaining = listenerPids(port);
578
+ if (remaining.length > 0) {
579
+ console.error(
580
+ `Port ${port} is still held by pid(s) ${remaining.join(", ")} \u2014 try \`deepspace kill --port ${port}\` again, or kill manually.`
581
+ );
582
+ process.exit(1);
583
+ }
584
+ console.log(`Port ${port} is free.`);
585
+ }
586
+ });
587
+ function resolvePort2(arg) {
588
+ const raw = arg ?? process.env.DEEPSPACE_PORT;
589
+ if (!raw) return DEFAULT_PORT2;
590
+ const n = Number(raw);
591
+ if (!Number.isInteger(n) || n < 1 || n > 65535) {
592
+ console.error(`Invalid port: ${raw}. Must be an integer between 1 and 65535.`);
593
+ process.exit(1);
594
+ }
595
+ return n;
596
+ }
597
+ function listenerPids(port) {
598
+ if (IS_WIN) {
599
+ return runPowershell(
600
+ `Get-NetTCPConnection -LocalPort ${port} -State Listen -ErrorAction SilentlyContinue | Select-Object -ExpandProperty OwningProcess`
601
+ );
602
+ }
603
+ return run(["lsof", "-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-t"]);
604
+ }
605
+ function childPids(parentPid) {
606
+ if (IS_WIN) {
607
+ return runPowershell(
608
+ `Get-CimInstance Win32_Process -Filter "ParentProcessId=${parentPid}" | Select-Object -ExpandProperty ProcessId`
609
+ );
610
+ }
611
+ return run(["pgrep", "-P", String(parentPid)]);
612
+ }
613
+ function processPidsByName(names) {
614
+ const all = /* @__PURE__ */ new Set();
615
+ if (IS_WIN) {
616
+ const orClauses = names.map((n) => `($_.Name -like '*${n}*') -or ($_.CommandLine -like '*${n}*')`).join(" -or ");
617
+ const script = `Get-CimInstance Win32_Process | Where-Object { ${orClauses} } | Select-Object -ExpandProperty ProcessId`;
618
+ for (const pid of runPowershell(script)) {
619
+ if (pid !== process.pid) all.add(pid);
620
+ }
621
+ return [...all];
622
+ }
623
+ for (const name of names) {
624
+ for (const pid of run(["pgrep", "-f", name])) {
625
+ if (pid !== process.pid) all.add(pid);
626
+ }
627
+ }
628
+ return [...all];
629
+ }
630
+ function run(cmd) {
631
+ try {
632
+ const r = spawnSync(cmd[0], cmd.slice(1), { encoding: "utf-8" });
633
+ if (r.status !== 0) return [];
634
+ return parsePidLines(r.stdout);
635
+ } catch {
636
+ return [];
637
+ }
638
+ }
639
+ function runPowershell(script) {
640
+ const exe = pwshOrPowershell();
641
+ try {
642
+ const r = spawnSync(
643
+ exe,
644
+ ["-NoProfile", "-NonInteractive", "-Command", script],
645
+ { encoding: "utf-8" }
646
+ );
647
+ if (r.status !== 0) return [];
648
+ return parsePidLines(r.stdout);
649
+ } catch {
650
+ return [];
651
+ }
652
+ }
653
+ function pwshOrPowershell() {
654
+ const probe = spawnSync("pwsh", ["-NoProfile", "-Command", "$PSVersionTable.PSVersion.Major"], {
655
+ encoding: "utf-8"
656
+ });
657
+ if (probe.status === 0) return "pwsh";
658
+ return "powershell.exe";
659
+ }
660
+ function parsePidLines(stdout) {
661
+ return stdout.split(/\s+/).map((s) => Number.parseInt(s, 10)).filter((n) => Number.isFinite(n) && n > 0);
662
+ }
663
+ function sendSignal(pid, signal) {
664
+ try {
665
+ process.kill(pid, signal);
666
+ return true;
667
+ } catch {
668
+ return false;
669
+ }
670
+ }
671
+ function isAlive(pid) {
672
+ try {
673
+ process.kill(pid, 0);
674
+ return true;
675
+ } catch {
676
+ return false;
677
+ }
678
+ }
679
+
680
+ // src/cli/commands/test.ts
681
+ import { defineCommand as defineCommand5 } from "citty";
397
682
  import { existsSync as existsSync5 } from "fs";
398
683
  import { join as join6, resolve as resolve2 } from "path";
399
- import { execSync, spawnSync } from "child_process";
400
- var DEFAULT_PORT2 = 5173;
401
- var test_default = defineCommand4({
684
+ import { execSync, spawnSync as spawnSync2 } from "child_process";
685
+ var DEFAULT_PORT3 = 5173;
686
+ var test_default = defineCommand5({
402
687
  meta: {
403
688
  name: "test",
404
689
  description: "Run tests for your DeepSpace app"
@@ -411,14 +696,14 @@ var test_default = defineCommand4({
411
696
  },
412
697
  port: {
413
698
  type: "string",
414
- description: `Port for vite/webServer (default ${DEFAULT_PORT2}, or $DEEPSPACE_PORT)`,
699
+ description: `Port for vite/webServer (default ${DEFAULT_PORT3}, or $DEEPSPACE_PORT)`,
415
700
  required: false
416
701
  }
417
702
  },
418
703
  async run({ args }) {
419
704
  const appDir = resolve2(".");
420
705
  const suite = args.suite ?? "default";
421
- const port = resolvePort2(args.port);
706
+ const port = resolvePort3(args.port);
422
707
  if (!existsSync5(join6(appDir, "wrangler.toml"))) {
423
708
  console.error("No wrangler.toml found. Are you in a DeepSpace app directory?");
424
709
  process.exit(1);
@@ -471,7 +756,7 @@ var test_default = defineCommand4({
471
756
  }
472
757
  });
473
758
  function runPlaywright(appDir, testFiles, port) {
474
- const result = spawnSync("npx", [
759
+ const result = spawnSync2("npx", [
475
760
  "playwright",
476
761
  "test",
477
762
  "--config",
@@ -486,7 +771,7 @@ function runPlaywright(appDir, testFiles, port) {
486
771
  return result.status ?? 1;
487
772
  }
488
773
  function runVitest(appDir) {
489
- const result = spawnSync("npx", ["vitest", "run", "--passWithNoTests"], {
774
+ const result = spawnSync2("npx", ["vitest", "run", "--passWithNoTests"], {
490
775
  cwd: appDir,
491
776
  stdio: "inherit",
492
777
  shell: process.platform === "win32"
@@ -503,16 +788,16 @@ function ensurePlaywright(appDir) {
503
788
  const isLinux = process.platform === "linux";
504
789
  const isRoot = isLinux && typeof process.getuid === "function" && process.getuid() === 0;
505
790
  const args = isRoot ? ["install", "--with-deps", "chromium"] : ["install", "chromium"];
506
- const result = spawnSync("npx", ["playwright", ...args], { cwd: appDir, stdio: "inherit" });
791
+ const result = spawnSync2("npx", ["playwright", ...args], { cwd: appDir, stdio: "inherit" });
507
792
  if (result.status !== 0) {
508
793
  console.error("\nPlaywright install failed. On Linux, system libs may be missing \u2014 try:");
509
794
  console.error(" sudo npx playwright install --with-deps chromium");
510
795
  process.exit(1);
511
796
  }
512
797
  }
513
- function resolvePort2(arg) {
798
+ function resolvePort3(arg) {
514
799
  const raw = arg ?? process.env.DEEPSPACE_PORT;
515
- if (!raw) return DEFAULT_PORT2;
800
+ if (!raw) return DEFAULT_PORT3;
516
801
  const n = Number(raw);
517
802
  if (!Number.isInteger(n) || n < 1 || n > 65535) {
518
803
  console.error(`Invalid port: ${raw}. Must be an integer between 1 and 65535.`);
@@ -522,7 +807,7 @@ function resolvePort2(arg) {
522
807
  }
523
808
 
524
809
  // src/cli/commands/test-accounts.ts
525
- import { defineCommand as defineCommand5 } from "citty";
810
+ import { defineCommand as defineCommand6 } from "citty";
526
811
  import * as p2 from "@clack/prompts";
527
812
  import { readFileSync as readFileSync4, writeFileSync as writeFileSync4, existsSync as existsSync6, mkdirSync as mkdirSync3 } from "fs";
528
813
  import { join as join7 } from "path";
@@ -567,7 +852,7 @@ async function deleteRemote(id) {
567
852
  throw new Error(data.error ?? `DELETE returned ${res.status}`);
568
853
  }
569
854
  }
570
- var create = defineCommand5({
855
+ var create = defineCommand6({
571
856
  meta: {
572
857
  name: "create",
573
858
  description: "Create a test account"
@@ -637,7 +922,7 @@ var create = defineCommand5({
637
922
  Saved to ${ACCOUNTS_PATH}`);
638
923
  }
639
924
  });
640
- var list = defineCommand5({
925
+ var list = defineCommand6({
641
926
  meta: {
642
927
  name: "list",
643
928
  description: "List your test accounts"
@@ -672,7 +957,7 @@ var list = defineCommand5({
672
957
  }
673
958
  }
674
959
  });
675
- var del = defineCommand5({
960
+ var del = defineCommand6({
676
961
  meta: {
677
962
  name: "delete",
678
963
  description: "Delete a test account by --email or --id"
@@ -729,7 +1014,7 @@ var del = defineCommand5({
729
1014
  console.log(`Test account deleted${targetEmail ? `: ${targetEmail}` : `: ${targetId}`}`);
730
1015
  }
731
1016
  });
732
- var clear = defineCommand5({
1017
+ var clear = defineCommand6({
733
1018
  meta: {
734
1019
  name: "clear",
735
1020
  description: "Delete all your test accounts (or those matching --label)"
@@ -795,7 +1080,7 @@ Failed:`);
795
1080
  }
796
1081
  }
797
1082
  });
798
- var test_accounts_default = defineCommand5({
1083
+ var test_accounts_default = defineCommand6({
799
1084
  meta: {
800
1085
  name: "test-accounts",
801
1086
  description: "Manage test accounts for development"
@@ -809,14 +1094,272 @@ var test_accounts_default = defineCommand5({
809
1094
  });
810
1095
 
811
1096
  // src/cli/commands/deploy.ts
812
- import { defineCommand as defineCommand6 } from "citty";
1097
+ import { defineCommand as defineCommand7 } from "citty";
813
1098
  import { execSync as execSync2 } from "child_process";
814
1099
  import { readFileSync as readFileSync5, readdirSync, existsSync as existsSync7 } from "fs";
815
1100
  import { parse as parseToml2 } from "smol-toml";
816
1101
  import { join as join8, resolve as resolve3, dirname } from "path";
817
1102
  import * as p3 from "@clack/prompts";
1103
+
1104
+ // src/server/rooms/binding-manifest.ts
1105
+ var AUTO_PROVISION_SENTINEL = "auto";
1106
+ var VECTORIZE_METRICS = /* @__PURE__ */ new Set(["cosine", "euclidean", "dot-product"]);
1107
+ var ALLOWED_BINDING_TYPES = /* @__PURE__ */ new Set([
1108
+ "vectorize",
1109
+ "ai",
1110
+ "r2_bucket",
1111
+ "kv_namespace",
1112
+ "d1",
1113
+ "queue",
1114
+ "browser_rendering",
1115
+ "analytics_engine",
1116
+ "hyperdrive"
1117
+ ]);
1118
+ var RESERVED_BINDING_NAMES = /* @__PURE__ */ new Set([
1119
+ "ASSETS",
1120
+ "PLATFORM_WORKER",
1121
+ "API_WORKER",
1122
+ "APP_NAME",
1123
+ "OWNER_USER_ID",
1124
+ "AUTH_JWT_PUBLIC_KEY",
1125
+ "AUTH_JWT_ISSUER",
1126
+ "AUTH_WORKER_URL",
1127
+ "APP_IDENTITY_TOKEN",
1128
+ "APP_OWNER_JWT",
1129
+ "INTERNAL_STORAGE_HMAC_SECRET",
1130
+ "USAGE_EVENTS"
1131
+ ]);
1132
+ function validateBindingManifest(manifest) {
1133
+ if (!Array.isArray(manifest)) {
1134
+ return { valid: false, errors: [{ reason: "Manifest must be an array" }] };
1135
+ }
1136
+ const errors = [];
1137
+ const seenNames = /* @__PURE__ */ new Set();
1138
+ for (const entry of manifest) {
1139
+ if (!entry || typeof entry !== "object") {
1140
+ errors.push({ reason: `Entry must be an object (got ${entry === null ? "null" : typeof entry})` });
1141
+ continue;
1142
+ }
1143
+ const e = entry;
1144
+ const type = e.type;
1145
+ const name = e.name;
1146
+ if (typeof type !== "string" || !ALLOWED_BINDING_TYPES.has(type)) {
1147
+ errors.push({
1148
+ reason: `Disallowed binding type: ${typeof type === "string" ? type : `(${typeof type})`}. Allowed: ${[...ALLOWED_BINDING_TYPES].sort().join(", ")}`
1149
+ });
1150
+ continue;
1151
+ }
1152
+ if (typeof name !== "string" || !name) {
1153
+ errors.push({ reason: `Binding of type "${type}" missing 'name' (string)` });
1154
+ continue;
1155
+ }
1156
+ if (RESERVED_BINDING_NAMES.has(name)) {
1157
+ errors.push({ reason: `Binding name "${name}" is reserved by the SDK` });
1158
+ continue;
1159
+ }
1160
+ if (seenNames.has(name)) {
1161
+ errors.push({ reason: `Duplicate binding name "${name}"` });
1162
+ continue;
1163
+ }
1164
+ seenNames.add(name);
1165
+ const binding = entry;
1166
+ const fieldErr = requiredFieldError(binding);
1167
+ if (fieldErr) errors.push({ binding, reason: fieldErr });
1168
+ }
1169
+ if (errors.length) return { valid: false, errors };
1170
+ return { valid: true, bindings: manifest };
1171
+ }
1172
+ function requiredFieldError(b) {
1173
+ switch (b.type) {
1174
+ case "vectorize": {
1175
+ if (!b.index_name) return `vectorize binding "${b.name}" missing index_name (or "auto")`;
1176
+ if (b.index_name === AUTO_PROVISION_SENTINEL) {
1177
+ if (typeof b.dimensions !== "number" || b.dimensions <= 0) {
1178
+ return `vectorize binding "${b.name}" with index_name="auto" requires "dimensions" (positive number)`;
1179
+ }
1180
+ if (!b.metric || !VECTORIZE_METRICS.has(b.metric)) {
1181
+ return `vectorize binding "${b.name}" with index_name="auto" requires "metric" (one of: ${[...VECTORIZE_METRICS].join(", ")})`;
1182
+ }
1183
+ }
1184
+ return null;
1185
+ }
1186
+ case "r2_bucket":
1187
+ return b.bucket_name ? null : `r2_bucket binding "${b.name}" missing bucket_name (or "auto")`;
1188
+ case "kv_namespace": {
1189
+ if (!b.namespace_id) return `kv_namespace binding "${b.name}" missing namespace_id (or "auto")`;
1190
+ if (b.namespace_id === AUTO_PROVISION_SENTINEL && !b.title) {
1191
+ return `kv_namespace binding "${b.name}" with namespace_id="auto" requires "title"`;
1192
+ }
1193
+ return null;
1194
+ }
1195
+ case "d1": {
1196
+ if (!b.id) return `d1 binding "${b.name}" missing id (or "auto")`;
1197
+ if (b.id === AUTO_PROVISION_SENTINEL && !b.database_name) {
1198
+ return `d1 binding "${b.name}" with id="auto" requires "database_name"`;
1199
+ }
1200
+ return null;
1201
+ }
1202
+ case "queue":
1203
+ return b.queue_name ? null : `queue binding "${b.name}" missing queue_name (or "auto")`;
1204
+ case "hyperdrive":
1205
+ if (!b.id) return `hyperdrive binding "${b.name}" missing id`;
1206
+ if (b.id === AUTO_PROVISION_SENTINEL) {
1207
+ return `hyperdrive binding "${b.name}": auto-provisioning not yet supported (provide a real id)`;
1208
+ }
1209
+ return null;
1210
+ case "ai":
1211
+ case "browser_rendering":
1212
+ case "analytics_engine":
1213
+ return null;
1214
+ // no required fields beyond `name`
1215
+ default: {
1216
+ const _exhaustive = b;
1217
+ void _exhaustive;
1218
+ return null;
1219
+ }
1220
+ }
1221
+ }
1222
+ function bindingManifestFromOutputConfig(outputConfig) {
1223
+ const out = [];
1224
+ const aiBinding = pluckString(outputConfig.ai, "binding");
1225
+ if (aiBinding) out.push({ type: "ai", name: aiBinding });
1226
+ for (const v of asObjectArray(outputConfig.vectorize)) {
1227
+ const name = pluckString(v, "binding");
1228
+ const index_name = pluckString(v, "index_name");
1229
+ if (!name || !index_name) continue;
1230
+ const dimensions = pluckNumber(v, "dimensions");
1231
+ const metric = pluckString(v, "metric");
1232
+ out.push({
1233
+ type: "vectorize",
1234
+ name,
1235
+ index_name,
1236
+ ...dimensions != null && { dimensions },
1237
+ ...metric && (metric === "cosine" || metric === "euclidean" || metric === "dot-product") && { metric }
1238
+ });
1239
+ }
1240
+ for (const r of asObjectArray(outputConfig.r2_buckets)) {
1241
+ const name = pluckString(r, "binding");
1242
+ const bucket_name = pluckString(r, "bucket_name");
1243
+ if (name && bucket_name) out.push({ type: "r2_bucket", name, bucket_name });
1244
+ }
1245
+ for (const k of asObjectArray(outputConfig.kv_namespaces)) {
1246
+ const name = pluckString(k, "binding");
1247
+ const namespace_id = pluckString(k, "id");
1248
+ if (!name || !namespace_id) continue;
1249
+ const title = pluckString(k, "title");
1250
+ out.push({
1251
+ type: "kv_namespace",
1252
+ name,
1253
+ namespace_id,
1254
+ ...title && { title }
1255
+ });
1256
+ }
1257
+ for (const d of asObjectArray(outputConfig.d1_databases)) {
1258
+ const name = pluckString(d, "binding");
1259
+ const id = pluckString(d, "database_id");
1260
+ if (!name || !id) continue;
1261
+ const database_name = pluckString(d, "database_name");
1262
+ out.push({
1263
+ type: "d1",
1264
+ name,
1265
+ id,
1266
+ ...database_name && { database_name }
1267
+ });
1268
+ }
1269
+ const queues = isObject(outputConfig.queues) ? outputConfig.queues : null;
1270
+ for (const q of asObjectArray(queues?.producers)) {
1271
+ const name = pluckString(q, "binding");
1272
+ const queue_name = pluckString(q, "queue");
1273
+ if (name && queue_name) out.push({ type: "queue", name, queue_name });
1274
+ }
1275
+ const browserBinding = pluckString(outputConfig.browser, "binding");
1276
+ if (browserBinding) out.push({ type: "browser_rendering", name: browserBinding });
1277
+ for (const a of asObjectArray(outputConfig.analytics_engine_datasets)) {
1278
+ const name = pluckString(a, "binding");
1279
+ const dataset = pluckString(a, "dataset");
1280
+ if (name) out.push({ type: "analytics_engine", name, dataset: dataset ?? void 0 });
1281
+ }
1282
+ for (const h of asObjectArray(outputConfig.hyperdrive)) {
1283
+ const name = pluckString(h, "binding");
1284
+ const id = pluckString(h, "id");
1285
+ if (name && id) out.push({ type: "hyperdrive", name, id });
1286
+ }
1287
+ return out;
1288
+ }
1289
+ function isObject(v) {
1290
+ return v !== null && typeof v === "object" && !Array.isArray(v);
1291
+ }
1292
+ function pluckString(v, key) {
1293
+ if (!isObject(v)) return null;
1294
+ const val = v[key];
1295
+ return typeof val === "string" && val.length > 0 ? val : null;
1296
+ }
1297
+ function pluckNumber(v, key) {
1298
+ if (!isObject(v)) return null;
1299
+ const val = v[key];
1300
+ return typeof val === "number" && Number.isFinite(val) ? val : null;
1301
+ }
1302
+ function asObjectArray(v) {
1303
+ if (!Array.isArray(v)) return [];
1304
+ return v.filter(isObject);
1305
+ }
1306
+
1307
+ // src/server/rooms/app-name.ts
1308
+ var APP_NAME_RULES = {
1309
+ /** ^[a-z0-9](-?[a-z0-9])+$ — RFC 1035 host label, no consecutive dashes. */
1310
+ pattern: /^[a-z0-9](?:-?[a-z0-9])+$/,
1311
+ minLength: 2,
1312
+ maxLength: 63
1313
+ };
1314
+ function validateAppName(raw) {
1315
+ if (typeof raw !== "string") {
1316
+ return { valid: false, reason: "App name must be a string" };
1317
+ }
1318
+ const name = raw.trim();
1319
+ if (!name) {
1320
+ return { valid: false, reason: "App name is required" };
1321
+ }
1322
+ if (name.length < APP_NAME_RULES.minLength) {
1323
+ return { valid: false, reason: `App name "${name}" is too short (min ${APP_NAME_RULES.minLength} chars)` };
1324
+ }
1325
+ if (name.length > APP_NAME_RULES.maxLength) {
1326
+ return { valid: false, reason: `App name "${name}" is too long (max ${APP_NAME_RULES.maxLength} chars)` };
1327
+ }
1328
+ if (!APP_NAME_RULES.pattern.test(name)) {
1329
+ return {
1330
+ valid: false,
1331
+ reason: `App name "${name}" is invalid. Use lowercase letters, digits, and single hyphens only (must start + end with alphanumeric). Examples: my-app, search-v2, unison-search.`
1332
+ };
1333
+ }
1334
+ return { valid: true, name };
1335
+ }
1336
+ function resolveAppName(raw) {
1337
+ const strict = validateAppName(raw);
1338
+ if (strict.valid) return { ok: true, name: strict.name };
1339
+ if (typeof raw !== "string") return { ok: false, reason: strict.reason };
1340
+ const trimmed = raw.trim();
1341
+ if (!trimmed) return { ok: false, reason: "App name is required" };
1342
+ const sanitized = sanitizeAppName(trimmed);
1343
+ const sanitizedStrict = validateAppName(sanitized);
1344
+ if (!sanitizedStrict.valid) {
1345
+ return {
1346
+ ok: false,
1347
+ reason: `App name "${trimmed}" can't be sanitized into a valid name (${sanitizedStrict.reason}). Pick something matching: lowercase letters, digits, single hyphens, ${APP_NAME_RULES.minLength}-${APP_NAME_RULES.maxLength} chars.`
1348
+ };
1349
+ }
1350
+ return {
1351
+ ok: true,
1352
+ name: sanitized,
1353
+ warning: `App name "${trimmed}" doesn't conform to deploy rules \u2014 using "${sanitized}" instead. Update wrangler.toml's \`name\` to silence this warning.`
1354
+ };
1355
+ }
1356
+ function sanitizeAppName(raw) {
1357
+ return raw.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-{2,}/g, "-").replace(/^-+|-+$/g, "");
1358
+ }
1359
+
1360
+ // src/cli/commands/deploy.ts
818
1361
  var DEPLOY_URL = process.env.DEEPSPACE_DEPLOY_URL ?? ENVS.prod.deploy;
819
- var deploy_default = defineCommand6({
1362
+ var deploy_default = defineCommand7({
820
1363
  meta: {
821
1364
  name: "deploy",
822
1365
  description: "Build and deploy your DeepSpace app"
@@ -837,11 +1380,13 @@ var deploy_default = defineCommand6({
837
1380
  process.exit(1);
838
1381
  }
839
1382
  const wranglerConfig = parseToml2(readFileSync5(wranglerPath, "utf-8"));
840
- const appName = wranglerConfig.name;
841
- if (!appName) {
842
- p3.cancel("Could not find app name in wrangler.toml");
1383
+ const nameRes = resolveAppName(wranglerConfig.name);
1384
+ if (!nameRes.ok) {
1385
+ p3.cancel(`wrangler.toml: ${nameRes.reason}`);
843
1386
  process.exit(1);
844
1387
  }
1388
+ if (nameRes.warning) p3.log.warn(nameRes.warning);
1389
+ const appName = nameRes.name;
845
1390
  p3.log.info(`App: ${appName}`);
846
1391
  let token;
847
1392
  try {
@@ -899,6 +1444,55 @@ var deploy_default = defineCommand6({
899
1444
  if (doManifest?.length) {
900
1445
  p3.log.info(`DO manifest: ${doManifest.length} binding(s)`);
901
1446
  }
1447
+ const customBindings = bindingManifestFromOutputConfig(outputConfig);
1448
+ const validation = validateBindingManifest(customBindings);
1449
+ if (!validation.valid) {
1450
+ p3.cancel(
1451
+ `Invalid binding(s) in wrangler.toml:
1452
+ ` + validation.errors.map((e) => ` \u2022 ${e.reason}`).join("\n")
1453
+ );
1454
+ process.exit(1);
1455
+ }
1456
+ if (customBindings.length) {
1457
+ p3.log.info(
1458
+ `Custom bindings: ${customBindings.map((b) => `${b.name} (${b.type})`).join(", ")}`
1459
+ );
1460
+ }
1461
+ const devVarsPath = join8(appDir, ".dev.vars");
1462
+ let userSecrets = {};
1463
+ if (existsSync7(devVarsPath)) {
1464
+ const customSection = extractCustomDevVars(readFileSync5(devVarsPath, "utf-8"));
1465
+ if (customSection) {
1466
+ try {
1467
+ userSecrets = parseDevVars(customSection);
1468
+ } catch (err) {
1469
+ p3.cancel(`.dev.vars: ${err.message}`);
1470
+ process.exit(1);
1471
+ }
1472
+ }
1473
+ }
1474
+ const userSecretNames = Object.keys(userSecrets);
1475
+ const declaredBindingNames = /* @__PURE__ */ new Set([
1476
+ ...customBindings.map((b) => b.name),
1477
+ ...(doManifest ?? []).map((d) => d.binding)
1478
+ ]);
1479
+ for (const name of userSecretNames) {
1480
+ if (RESERVED_BINDING_NAMES.has(name)) {
1481
+ p3.cancel(
1482
+ `.dev.vars: "${name}" is a reserved binding name and cannot be used as an app secret.`
1483
+ );
1484
+ process.exit(1);
1485
+ }
1486
+ if (declaredBindingNames.has(name)) {
1487
+ p3.cancel(
1488
+ `.dev.vars: "${name}" collides with a binding declared in wrangler.toml \u2014 rename one or the other.`
1489
+ );
1490
+ process.exit(1);
1491
+ }
1492
+ }
1493
+ if (userSecretNames.length) {
1494
+ p3.log.info(`App secrets (from .dev.vars): ${userSecretNames.join(", ")}`);
1495
+ }
902
1496
  s.start(`Deploying to ${appName}.app.space...`);
903
1497
  const form = new FormData();
904
1498
  form.append("worker", new Blob([workerJs], { type: "application/javascript" }), "worker.js");
@@ -906,6 +1500,12 @@ var deploy_default = defineCommand6({
906
1500
  if (doManifest) {
907
1501
  form.append("doManifest", JSON.stringify(doManifest));
908
1502
  }
1503
+ if (customBindings.length) {
1504
+ form.append("bindingManifest", JSON.stringify(customBindings));
1505
+ }
1506
+ if (userSecretNames.length) {
1507
+ form.append("userSecrets", JSON.stringify(userSecrets));
1508
+ }
909
1509
  const res = await fetch(`${DEPLOY_URL}/api/deploy/${appName}`, {
910
1510
  method: "POST",
911
1511
  headers: { Authorization: `Bearer ${token}` },
@@ -917,11 +1517,40 @@ var deploy_default = defineCommand6({
917
1517
  p3.cancel(body.error ?? `Deployment error (${res.status})`);
918
1518
  process.exit(1);
919
1519
  }
1520
+ if (body.url) {
1521
+ s.message("Waiting for edge propagation...");
1522
+ const ok = await waitForAssetsReady(body.url, 9e4);
1523
+ if (!ok) {
1524
+ s.stop("Deployed (edge propagation still in progress after 90s)");
1525
+ p3.log.warn("First requests may briefly hit the assets transitional page; retry in a minute.");
1526
+ p3.log.success(`Live at: ${body.url}`);
1527
+ p3.outro("Done");
1528
+ return;
1529
+ }
1530
+ }
920
1531
  s.stop("Deployed!");
921
1532
  p3.log.success(`Live at: ${body.url}`);
922
1533
  p3.outro("Done");
923
1534
  }
924
1535
  });
1536
+ async function waitForAssetsReady(url, timeoutMs) {
1537
+ const deadline = Date.now() + timeoutMs;
1538
+ let attempt = 0;
1539
+ while (Date.now() < deadline) {
1540
+ attempt++;
1541
+ try {
1542
+ const res = await fetch(url, { redirect: "manual" });
1543
+ const body = await res.text();
1544
+ if (!body.includes("Assets have not yet deployed")) {
1545
+ return true;
1546
+ }
1547
+ } catch {
1548
+ }
1549
+ const wait2 = Math.min(8e3, 1e3 * 2 ** (attempt - 1));
1550
+ await new Promise((r) => setTimeout(r, wait2));
1551
+ }
1552
+ return false;
1553
+ }
925
1554
  function collectAssets(dir) {
926
1555
  const assets = [];
927
1556
  function walk(d, prefix) {
@@ -944,13 +1573,13 @@ function collectAssets(dir) {
944
1573
  }
945
1574
 
946
1575
  // src/cli/commands/undeploy.ts
947
- import { defineCommand as defineCommand7 } from "citty";
1576
+ import { defineCommand as defineCommand8 } from "citty";
948
1577
  import { readFileSync as readFileSync6, existsSync as existsSync8 } from "fs";
949
1578
  import { join as join9 } from "path";
950
1579
  import { parse as parseToml3 } from "smol-toml";
951
1580
  import * as p4 from "@clack/prompts";
952
1581
  var DEPLOY_URL2 = process.env.DEEPSPACE_DEPLOY_URL ?? ENVS.prod.deploy;
953
- var undeploy_default = defineCommand7({
1582
+ var undeploy_default = defineCommand8({
954
1583
  meta: {
955
1584
  name: "undeploy",
956
1585
  description: "Remove a deployed DeepSpace app"
@@ -1001,8 +1630,8 @@ var undeploy_default = defineCommand7({
1001
1630
  });
1002
1631
 
1003
1632
  // src/cli/commands/whoami.ts
1004
- import { defineCommand as defineCommand8 } from "citty";
1005
- var whoami_default = defineCommand8({
1633
+ import { defineCommand as defineCommand9 } from "citty";
1634
+ var whoami_default = defineCommand9({
1006
1635
  meta: {
1007
1636
  name: "whoami",
1008
1637
  description: "Show the currently logged-in user"
@@ -1056,7 +1685,7 @@ var whoami_default = defineCommand8({
1056
1685
  });
1057
1686
 
1058
1687
  // src/cli/commands/add.ts
1059
- import { defineCommand as defineCommand9 } from "citty";
1688
+ import { defineCommand as defineCommand10 } from "citty";
1060
1689
  import { existsSync as existsSync9 } from "fs";
1061
1690
  import { resolve as resolve4, join as join10 } from "path";
1062
1691
  import { execSync as execSync3 } from "child_process";
@@ -1065,7 +1694,7 @@ function findScript(from) {
1065
1694
  if (existsSync9(candidate)) return candidate;
1066
1695
  return null;
1067
1696
  }
1068
- var add_default = defineCommand9({
1697
+ var add_default = defineCommand10({
1069
1698
  meta: {
1070
1699
  name: "add",
1071
1700
  description: "Add a feature to your DeepSpace app"
@@ -1119,7 +1748,7 @@ var add_default = defineCommand9({
1119
1748
  });
1120
1749
 
1121
1750
  // src/cli/commands/domain.ts
1122
- import { defineCommand as defineCommand10 } from "citty";
1751
+ import { defineCommand as defineCommand11 } from "citty";
1123
1752
  import { execSync as execSync4 } from "child_process";
1124
1753
 
1125
1754
  // src/cli/lib/app-context.ts
@@ -1188,7 +1817,7 @@ async function confirm2(message) {
1188
1817
  });
1189
1818
  });
1190
1819
  }
1191
- var search = defineCommand10({
1820
+ var search = defineCommand11({
1192
1821
  meta: { name: "search", description: "Search for available domains" },
1193
1822
  args: {
1194
1823
  query: { type: "positional", description: "Domain or keyword", required: true },
@@ -1218,7 +1847,7 @@ var search = defineCommand10({
1218
1847
  }
1219
1848
  }
1220
1849
  });
1221
- var buy = defineCommand10({
1850
+ var buy = defineCommand11({
1222
1851
  meta: { name: "buy", description: "Buy a domain and attach it to an app" },
1223
1852
  args: {
1224
1853
  domain: { type: "positional", description: "Domain to buy", required: true },
@@ -1307,7 +1936,7 @@ var buy = defineCommand10({
1307
1936
  process.exit(1);
1308
1937
  }
1309
1938
  });
1310
- var list2 = defineCommand10({
1939
+ var list2 = defineCommand11({
1311
1940
  meta: { name: "list", description: "List your domains" },
1312
1941
  args: {
1313
1942
  json: { type: "boolean", description: "Emit JSON" }
@@ -1331,7 +1960,7 @@ var list2 = defineCommand10({
1331
1960
  }
1332
1961
  }
1333
1962
  });
1334
- var status = defineCommand10({
1963
+ var status = defineCommand11({
1335
1964
  meta: { name: "status", description: "Show details for one domain" },
1336
1965
  args: {
1337
1966
  domain: { type: "positional", description: "Domain", required: true },
@@ -1359,7 +1988,7 @@ var status = defineCommand10({
1359
1988
  console.log(`Charged: ${fmtCents(found.chargedCents)}/yr`);
1360
1989
  }
1361
1990
  });
1362
- var attach = defineCommand10({
1991
+ var attach = defineCommand11({
1363
1992
  meta: { name: "attach", description: "Re-point a domain at a different app" },
1364
1993
  args: {
1365
1994
  domain: { type: "positional", description: "Domain to re-point", required: true },
@@ -1390,7 +2019,7 @@ var attach = defineCommand10({
1390
2019
  console.log(`\u2713 ${args.domain} now points at ${targetApp}`);
1391
2020
  }
1392
2021
  });
1393
- var detach = defineCommand10({
2022
+ var detach = defineCommand11({
1394
2023
  meta: {
1395
2024
  name: "detach",
1396
2025
  description: "Stop routing the domain (keeps the registration; auto-renew unchanged)"
@@ -1420,7 +2049,7 @@ var detach = defineCommand10({
1420
2049
  console.log(`\u2713 ${args.domain} detached. Use \`deepspace domain attach\` to re-route, or \`renew --auto off\` to stop auto-renewal.`);
1421
2050
  }
1422
2051
  });
1423
- var renew = defineCommand10({
2052
+ var renew = defineCommand11({
1424
2053
  meta: { name: "renew", description: "Toggle auto-renewal at the registrar" },
1425
2054
  args: {
1426
2055
  domain: { type: "positional", description: "Domain", required: true },
@@ -1454,13 +2083,13 @@ function openBrowser2(url) {
1454
2083
  } catch {
1455
2084
  }
1456
2085
  }
1457
- var domain_default = defineCommand10({
2086
+ var domain_default = defineCommand11({
1458
2087
  meta: { name: "domain", description: "Buy and manage custom domains" },
1459
2088
  subCommands: { search, buy, list: list2, status, attach, detach, renew }
1460
2089
  });
1461
2090
 
1462
2091
  // src/cli/commands/integrations.ts
1463
- import { defineCommand as defineCommand11 } from "citty";
2092
+ import { defineCommand as defineCommand12 } from "citty";
1464
2093
 
1465
2094
  // src/cli/commands/_invoke-impl.ts
1466
2095
  import { readFileSync as readFileSync8 } from "fs";
@@ -1634,7 +2263,7 @@ async function runInvoke(args) {
1634
2263
  }
1635
2264
 
1636
2265
  // src/cli/commands/integrations.ts
1637
- var list3 = defineCommand11({
2266
+ var list3 = defineCommand12({
1638
2267
  meta: {
1639
2268
  name: "list",
1640
2269
  description: "List all available integrations"
@@ -1655,7 +2284,7 @@ var list3 = defineCommand11({
1655
2284
  }
1656
2285
  }
1657
2286
  });
1658
- var info = defineCommand11({
2287
+ var info = defineCommand12({
1659
2288
  meta: {
1660
2289
  name: "info",
1661
2290
  description: "Print schema + example body for an integration endpoint"
@@ -1681,7 +2310,7 @@ var info = defineCommand11({
1681
2310
  }
1682
2311
  }
1683
2312
  });
1684
- var invoke = defineCommand11({
2313
+ var invoke = defineCommand12({
1685
2314
  meta: {
1686
2315
  name: "invoke",
1687
2316
  description: "Invoke a platform integration as the logged-in user"
@@ -1734,7 +2363,7 @@ var invoke = defineCommand11({
1734
2363
  }
1735
2364
  }
1736
2365
  });
1737
- var integrations_default = defineCommand11({
2366
+ var integrations_default = defineCommand12({
1738
2367
  meta: {
1739
2368
  name: "integrations",
1740
2369
  description: "List, inspect, and invoke platform integrations"
@@ -1747,8 +2376,8 @@ var integrations_default = defineCommand11({
1747
2376
  });
1748
2377
 
1749
2378
  // src/cli/commands/invoke.ts
1750
- import { defineCommand as defineCommand12 } from "citty";
1751
- var invoke_default = defineCommand12({
2379
+ import { defineCommand as defineCommand13 } from "citty";
2380
+ var invoke_default = defineCommand13({
1752
2381
  meta: {
1753
2382
  name: "invoke",
1754
2383
  description: "Invoke a platform integration as the logged-in user"
@@ -1826,7 +2455,7 @@ var pkg = JSON.parse(readFileSync9(join12(__dirname, "..", "package.json"), "utf
1826
2455
  if (process.platform === "linux" && !process.env.SSL_CERT_FILE && existsSync11("/etc/ssl/certs/ca-certificates.crt")) {
1827
2456
  process.env.SSL_CERT_FILE = "/etc/ssl/certs/ca-certificates.crt";
1828
2457
  }
1829
- var create2 = defineCommand13({
2458
+ var create2 = defineCommand14({
1830
2459
  meta: {
1831
2460
  name: "create",
1832
2461
  description: "Create a new DeepSpace app"
@@ -1845,7 +2474,7 @@ var create2 = defineCommand13({
1845
2474
  execSync5(`npm create deepspace@latest${name}`, { stdio: "inherit" });
1846
2475
  }
1847
2476
  });
1848
- var main = defineCommand13({
2477
+ var main = defineCommand14({
1849
2478
  meta: {
1850
2479
  name: "deepspace",
1851
2480
  version: pkg.version,
@@ -1857,6 +2486,7 @@ var main = defineCommand13({
1857
2486
  logout: logout_default,
1858
2487
  whoami: whoami_default,
1859
2488
  dev: dev_default,
2489
+ kill: kill_default,
1860
2490
  test: test_default,
1861
2491
  "test-accounts": test_accounts_default,
1862
2492
  add: add_default,