deepspace 0.5.3 → 0.5.5

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
@@ -3,7 +3,7 @@
3
3
  // src/cli/cli.ts
4
4
  import { defineCommand as defineCommand22, runMain } from "citty";
5
5
  import { execSync as execSync4 } from "child_process";
6
- import { readFileSync as readFileSync14, existsSync as existsSync14 } from "fs";
6
+ import { readFileSync as readFileSync15, existsSync as existsSync14 } from "fs";
7
7
  import { dirname as dirname4, join as join16 } from "path";
8
8
  import { fileURLToPath as fileURLToPath2 } from "url";
9
9
 
@@ -77,8 +77,215 @@ function createSpinner() {
77
77
  // src/cli/env.ts
78
78
  import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
79
79
 
80
+ // src/server/rooms/binding-manifest.ts
81
+ var AUTO_PROVISION_SENTINEL = "auto";
82
+ var VECTORIZE_METRICS = /* @__PURE__ */ new Set(["cosine", "euclidean", "dot-product"]);
83
+ var ALLOWED_BINDING_TYPES = /* @__PURE__ */ new Set([
84
+ "vectorize",
85
+ "ai",
86
+ "r2_bucket",
87
+ "kv_namespace",
88
+ "d1",
89
+ "queue",
90
+ "browser_rendering",
91
+ "analytics_engine",
92
+ "hyperdrive"
93
+ ]);
94
+ var RESERVED_BINDING_NAMES = /* @__PURE__ */ new Set([
95
+ "ASSETS",
96
+ "PLATFORM_WORKER",
97
+ "API_WORKER",
98
+ "APP_NAME",
99
+ "OWNER_USER_ID",
100
+ "AUTH_JWT_PUBLIC_KEY",
101
+ "AUTH_JWT_ISSUER",
102
+ "AUTH_WORKER_URL",
103
+ "APP_IDENTITY_TOKEN",
104
+ "APP_OWNER_JWT",
105
+ "USAGE_EVENTS"
106
+ ]);
107
+ function validateBindingManifest(manifest) {
108
+ if (!Array.isArray(manifest)) {
109
+ return { valid: false, errors: [{ reason: "Manifest must be an array" }] };
110
+ }
111
+ const errors = [];
112
+ const seenNames = /* @__PURE__ */ new Set();
113
+ for (const entry of manifest) {
114
+ if (!entry || typeof entry !== "object") {
115
+ errors.push({ reason: `Entry must be an object (got ${entry === null ? "null" : typeof entry})` });
116
+ continue;
117
+ }
118
+ const e = entry;
119
+ const type = e.type;
120
+ const name = e.name;
121
+ if (typeof type !== "string" || !ALLOWED_BINDING_TYPES.has(type)) {
122
+ errors.push({
123
+ reason: `Disallowed binding type: ${typeof type === "string" ? type : `(${typeof type})`}. Allowed: ${[...ALLOWED_BINDING_TYPES].sort().join(", ")}`
124
+ });
125
+ continue;
126
+ }
127
+ if (typeof name !== "string" || !name) {
128
+ errors.push({ reason: `Binding of type "${type}" missing 'name' (string)` });
129
+ continue;
130
+ }
131
+ if (RESERVED_BINDING_NAMES.has(name)) {
132
+ errors.push({ reason: `Binding name "${name}" is reserved by the SDK` });
133
+ continue;
134
+ }
135
+ if (seenNames.has(name)) {
136
+ errors.push({ reason: `Duplicate binding name "${name}"` });
137
+ continue;
138
+ }
139
+ seenNames.add(name);
140
+ const binding = entry;
141
+ const fieldErr = requiredFieldError(binding);
142
+ if (fieldErr) errors.push({ binding, reason: fieldErr });
143
+ }
144
+ if (errors.length) return { valid: false, errors };
145
+ return { valid: true, bindings: manifest };
146
+ }
147
+ function requiredFieldError(b) {
148
+ switch (b.type) {
149
+ case "vectorize": {
150
+ if (!b.index_name) return `vectorize binding "${b.name}" missing index_name (or "auto")`;
151
+ if (b.index_name === AUTO_PROVISION_SENTINEL) {
152
+ if (typeof b.dimensions !== "number" || b.dimensions <= 0) {
153
+ return `vectorize binding "${b.name}" with index_name="auto" requires "dimensions" (positive number)`;
154
+ }
155
+ if (!b.metric || !VECTORIZE_METRICS.has(b.metric)) {
156
+ return `vectorize binding "${b.name}" with index_name="auto" requires "metric" (one of: ${[...VECTORIZE_METRICS].join(", ")})`;
157
+ }
158
+ }
159
+ return null;
160
+ }
161
+ case "r2_bucket":
162
+ return b.bucket_name ? null : `r2_bucket binding "${b.name}" missing bucket_name (or "auto")`;
163
+ case "kv_namespace": {
164
+ if (!b.namespace_id) return `kv_namespace binding "${b.name}" missing namespace_id (or "auto")`;
165
+ if (b.namespace_id === AUTO_PROVISION_SENTINEL && !b.title) {
166
+ return `kv_namespace binding "${b.name}" with namespace_id="auto" requires "title"`;
167
+ }
168
+ return null;
169
+ }
170
+ case "d1": {
171
+ if (!b.id) return `d1 binding "${b.name}" missing id (or "auto")`;
172
+ if (b.id === AUTO_PROVISION_SENTINEL && !b.database_name) {
173
+ return `d1 binding "${b.name}" with id="auto" requires "database_name"`;
174
+ }
175
+ return null;
176
+ }
177
+ case "queue":
178
+ return b.queue_name ? null : `queue binding "${b.name}" missing queue_name (or "auto")`;
179
+ case "hyperdrive":
180
+ if (!b.id) return `hyperdrive binding "${b.name}" missing id`;
181
+ if (b.id === AUTO_PROVISION_SENTINEL) {
182
+ return `hyperdrive binding "${b.name}": auto-provisioning not yet supported (provide a real id)`;
183
+ }
184
+ return null;
185
+ case "ai":
186
+ case "browser_rendering":
187
+ case "analytics_engine":
188
+ return null;
189
+ // no required fields beyond `name`
190
+ default: {
191
+ const _exhaustive = b;
192
+ void _exhaustive;
193
+ return null;
194
+ }
195
+ }
196
+ }
197
+ function bindingManifestFromOutputConfig(outputConfig) {
198
+ const out = [];
199
+ const aiBinding = pluckString(outputConfig.ai, "binding");
200
+ if (aiBinding) out.push({ type: "ai", name: aiBinding });
201
+ for (const v of asObjectArray(outputConfig.vectorize)) {
202
+ const name = pluckString(v, "binding");
203
+ const index_name = pluckString(v, "index_name");
204
+ if (!name || !index_name) continue;
205
+ const dimensions = pluckNumber(v, "dimensions");
206
+ const metric = pluckString(v, "metric");
207
+ out.push({
208
+ type: "vectorize",
209
+ name,
210
+ index_name,
211
+ ...dimensions != null && { dimensions },
212
+ ...metric && (metric === "cosine" || metric === "euclidean" || metric === "dot-product") && { metric }
213
+ });
214
+ }
215
+ for (const r of asObjectArray(outputConfig.r2_buckets)) {
216
+ const name = pluckString(r, "binding");
217
+ const bucket_name = pluckString(r, "bucket_name");
218
+ if (name && bucket_name) out.push({ type: "r2_bucket", name, bucket_name });
219
+ }
220
+ for (const k of asObjectArray(outputConfig.kv_namespaces)) {
221
+ const name = pluckString(k, "binding");
222
+ const namespace_id = pluckString(k, "id");
223
+ if (!name || !namespace_id) continue;
224
+ const title = pluckString(k, "title");
225
+ out.push({
226
+ type: "kv_namespace",
227
+ name,
228
+ namespace_id,
229
+ ...title && { title }
230
+ });
231
+ }
232
+ for (const d of asObjectArray(outputConfig.d1_databases)) {
233
+ const name = pluckString(d, "binding");
234
+ const id = pluckString(d, "database_id");
235
+ if (!name || !id) continue;
236
+ const database_name = pluckString(d, "database_name");
237
+ out.push({
238
+ type: "d1",
239
+ name,
240
+ id,
241
+ ...database_name && { database_name }
242
+ });
243
+ }
244
+ const queues = isObject(outputConfig.queues) ? outputConfig.queues : null;
245
+ for (const q of asObjectArray(queues?.producers)) {
246
+ const name = pluckString(q, "binding");
247
+ const queue_name = pluckString(q, "queue");
248
+ if (name && queue_name) out.push({ type: "queue", name, queue_name });
249
+ }
250
+ const browserBinding = pluckString(outputConfig.browser, "binding");
251
+ if (browserBinding) out.push({ type: "browser_rendering", name: browserBinding });
252
+ for (const a of asObjectArray(outputConfig.analytics_engine_datasets)) {
253
+ const name = pluckString(a, "binding");
254
+ const dataset = pluckString(a, "dataset");
255
+ if (name) out.push({ type: "analytics_engine", name, dataset: dataset ?? void 0 });
256
+ }
257
+ for (const h of asObjectArray(outputConfig.hyperdrive)) {
258
+ const name = pluckString(h, "binding");
259
+ const id = pluckString(h, "id");
260
+ if (name && id) out.push({ type: "hyperdrive", name, id });
261
+ }
262
+ return out;
263
+ }
264
+ function isObject(v) {
265
+ return v !== null && typeof v === "object" && !Array.isArray(v);
266
+ }
267
+ function pluckString(v, key) {
268
+ if (!isObject(v)) return null;
269
+ const val = v[key];
270
+ return typeof val === "string" && val.length > 0 ? val : null;
271
+ }
272
+ function pluckNumber(v, key) {
273
+ if (!isObject(v)) return null;
274
+ const val = v[key];
275
+ return typeof val === "number" && Number.isFinite(val) ? val : null;
276
+ }
277
+ function asObjectArray(v) {
278
+ if (!Array.isArray(v)) return [];
279
+ return v.filter(isObject);
280
+ }
281
+
80
282
  // src/cli/lib/secrets.ts
81
283
  var GENERATED_SECRETS_DIVIDER = "# --- DeepSpace secrets cache: generated by `npx deepspace secrets`; do not edit manually ---";
284
+ var RESERVED_SECRET_NAMES = /* @__PURE__ */ new Set([
285
+ ...RESERVED_BINDING_NAMES,
286
+ "API_WORKER_URL",
287
+ "PLATFORM_WORKER_URL"
288
+ ]);
82
289
  var SECRET_NAME_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
83
290
  var CONFIG_NAME_RE = /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/;
84
291
  var MAX_SECRET_VALUE_BYTES = 32 * 1024;
@@ -96,6 +303,11 @@ function validateSecretName(name) {
96
303
  `Invalid secret name "${name}". Use letters, numbers, and underscores; start with a letter or underscore.`
97
304
  );
98
305
  }
306
+ if (RESERVED_SECRET_NAMES.has(name)) {
307
+ throw new Error(
308
+ `"${name}" is managed by the DeepSpace SDK and can't be set as an app secret \u2014 the platform injects it at deploy time.`
309
+ );
310
+ }
99
311
  return name;
100
312
  }
101
313
  function validateSecretValue(key, value) {
@@ -117,7 +329,10 @@ async function secretsApi(deployUrl, token, path, init = {}) {
117
329
  });
118
330
  const body = await res.json().catch(() => ({}));
119
331
  if (!res.ok) {
120
- throw new Error(`Secrets API ${path} (${res.status}): ${body.error ?? "request failed"}`);
332
+ const err = new Error(body.error ?? `Secrets request failed (${res.status})`);
333
+ err.status = res.status;
334
+ err.apiPath = path;
335
+ throw err;
121
336
  }
122
337
  return body;
123
338
  }
@@ -177,8 +392,7 @@ async function pullAppSecretsCache(deployUrl, token, appId, configName) {
177
392
  const { secrets } = await fetchSecretsValues(deployUrl, token, appId, configName);
178
393
  return { appId, configName, values: secrets };
179
394
  } catch (err) {
180
- const msg = err instanceof Error ? err.message : String(err);
181
- if (msg.includes("(404)")) return null;
395
+ if (err?.status === 404) return null;
182
396
  throw err;
183
397
  }
184
398
  }
@@ -230,6 +444,7 @@ function parseSecretsUpload(content) {
230
444
  if (eq <= 0) continue;
231
445
  const key = line.slice(0, eq).trim();
232
446
  if (!SECRET_NAME_RE.test(key)) continue;
447
+ validateSecretName(key);
233
448
  let value = line.slice(eq + 1).trim();
234
449
  if (value.startsWith('"') && value.endsWith('"') && value.length >= 2) {
235
450
  value = unescapeDotenv(value.slice(1, -1));
@@ -251,13 +466,16 @@ function unescapeDotenv(v) {
251
466
  }
252
467
  return out;
253
468
  }
469
+ function shellSingleQuote(value) {
470
+ return `'${value.replace(/'/g, "'\\''")}'`;
471
+ }
254
472
  function formatSecretsDownload(secrets, format) {
255
473
  const entries = Object.entries(secrets).sort(([a], [b]) => a.localeCompare(b));
256
474
  if (format === "json") {
257
475
  return JSON.stringify(Object.fromEntries(entries), null, 2) + "\n";
258
476
  }
259
477
  if (format === "shell") {
260
- return entries.map(([k, v]) => `export ${k}=${quoteDotenvValue(v)}`).join("\n") + "\n";
478
+ return entries.map(([k, v]) => `export ${k}=${shellSingleQuote(v)}`).join("\n") + "\n";
261
479
  }
262
480
  return entries.map(([k, v]) => `${k}=${quoteDotenvValue(v)}`).join("\n") + "\n";
263
481
  }
@@ -277,7 +495,7 @@ async function refreshSecretsCache(deployUrl, token, appId, wranglerEnv) {
277
495
  import { existsSync, readFileSync, writeFileSync } from "fs";
278
496
  import { join, resolve } from "path";
279
497
  import { parse as parseToml } from "smol-toml";
280
- var APP_ID_RE = /^app_[0-9A-HJKMNP-TV-Z]{26}$/;
498
+ var APP_ID_RE = /^(app_[0-9A-HJKMNP-TV-Z]{26}|[a-z0-9][a-z0-9_-]{0,63})$/;
281
499
  var CROCKFORD = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
282
500
  function mintAppId(now = Date.now()) {
283
501
  let ts = "";
@@ -690,38 +908,6 @@ function extractCustomDevVars(content) {
690
908
  while (out.length && out[out.length - 1].trim() === "") out.pop();
691
909
  return out.join("\n");
692
910
  }
693
- function removeDevVarsKeys(content, keys) {
694
- const lines = content.split("\n");
695
- const out = [];
696
- let i = 0;
697
- while (i < lines.length) {
698
- const line = lines[i];
699
- const eq = line.indexOf("=");
700
- const keyMatch = eq >= 0 ? line.slice(0, eq).match(/^\s*([A-Za-z_][A-Za-z0-9_]*)\s*$/) : null;
701
- if (!keyMatch) {
702
- out.push(line);
703
- i++;
704
- continue;
705
- }
706
- const key = keyMatch[1];
707
- const valStart = line.slice(eq + 1);
708
- const block = [line];
709
- if (valStart.startsWith('"') && !hasUnescapedQuote(valStart.slice(1))) {
710
- i++;
711
- while (i < lines.length) {
712
- block.push(lines[i]);
713
- const closed = hasUnescapedQuote(lines[i]);
714
- i++;
715
- if (closed) break;
716
- }
717
- } else {
718
- i++;
719
- }
720
- if (!keys.has(key)) out.push(...block);
721
- }
722
- while (out.length && out[out.length - 1].trim() === "") out.pop();
723
- return out.join("\n");
724
- }
725
911
  function hasUnescapedQuote(s) {
726
912
  let escaped = false;
727
913
  for (const ch of s) {
@@ -820,8 +1006,10 @@ async function writeDevVars(appDir, ownerId, callerJwt, wranglerEnv, opts = {})
820
1006
  `OWNER_USER_ID=${ownerId}`,
821
1007
  `APP_OWNER_JWT=${appOwnerJwt}`,
822
1008
  ...appIdentityToken ? [`APP_IDENTITY_TOKEN=${appIdentityToken}`] : [],
823
- // Open /api/debug/* (proxied to RecordRoom) for dev + tests only.
824
- // Production secrets never set this, so deployed apps 404 the route.
1009
+ // Open /api/debug/* (proxied to RecordRoom) locally. This is the SDK's
1010
+ // dev-only auto-value; the deploy ships the remote store, not this
1011
+ // `.dev.vars` section, so it never reaches prod. To enable it on a deployed
1012
+ // env, `deepspace secrets set ALLOW_DEBUG_ROUTES=true`.
825
1013
  `ALLOW_DEBUG_ROUTES=true`
826
1014
  ].join("\n");
827
1015
  const useSharedDevVarsCache = opts.sharedDevVarsCache ?? opts.generatedSecretsCache !== void 0;
@@ -1544,6 +1732,7 @@ var dev_default = defineCommand3({
1544
1732
  // src/cli/commands/kill.ts
1545
1733
  import { defineCommand as defineCommand4 } from "citty";
1546
1734
  import { spawnSync } from "child_process";
1735
+ import { readFileSync as readFileSync8, readdirSync as readdirSync3, readlinkSync } from "fs";
1547
1736
  import { setTimeout as wait } from "timers/promises";
1548
1737
  var SIGTERM_GRACE_MS = 1500;
1549
1738
  var IS_WIN = process.platform === "win32";
@@ -1614,7 +1803,55 @@ function listenerPids(port) {
1614
1803
  `Get-NetTCPConnection -LocalPort ${port} -State Listen -ErrorAction SilentlyContinue | Select-Object -ExpandProperty OwningProcess`
1615
1804
  );
1616
1805
  }
1617
- return run(["lsof", "-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-t"]);
1806
+ const viaLsof = run(["lsof", "-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-t"]);
1807
+ if (viaLsof.length) return viaLsof;
1808
+ return listenerPidsViaProc(port);
1809
+ }
1810
+ function listenerPidsViaProc(port) {
1811
+ try {
1812
+ const hexPort = port.toString(16).toUpperCase().padStart(4, "0");
1813
+ const inodes = /* @__PURE__ */ new Set();
1814
+ for (const f of ["/proc/net/tcp", "/proc/net/tcp6"]) {
1815
+ let data;
1816
+ try {
1817
+ data = readFileSync8(f, "utf-8");
1818
+ } catch {
1819
+ continue;
1820
+ }
1821
+ for (const line of data.split("\n").slice(1)) {
1822
+ const cols = line.trim().split(/\s+/);
1823
+ if (cols[3] !== "0A") continue;
1824
+ if (cols[1]?.split(":")[1] === hexPort && cols[9]) inodes.add(cols[9]);
1825
+ }
1826
+ }
1827
+ if (!inodes.size) return [];
1828
+ const pids = /* @__PURE__ */ new Set();
1829
+ for (const pidDir of readdirSync3("/proc")) {
1830
+ if (!/^\d+$/.test(pidDir)) continue;
1831
+ let fds;
1832
+ try {
1833
+ fds = readdirSync3(`/proc/${pidDir}/fd`);
1834
+ } catch {
1835
+ continue;
1836
+ }
1837
+ for (const fd of fds) {
1838
+ let link;
1839
+ try {
1840
+ link = readlinkSync(`/proc/${pidDir}/fd/${fd}`);
1841
+ } catch {
1842
+ continue;
1843
+ }
1844
+ const m = link.match(/^socket:\[(\d+)\]$/);
1845
+ if (m && inodes.has(m[1])) {
1846
+ pids.add(Number(pidDir));
1847
+ break;
1848
+ }
1849
+ }
1850
+ }
1851
+ return [...pids];
1852
+ } catch {
1853
+ return [];
1854
+ }
1618
1855
  }
1619
1856
  function childPids(parentPid) {
1620
1857
  if (IS_WIN) {
@@ -1935,7 +2172,7 @@ var screenshot_default = defineCommand6({
1935
2172
  // src/cli/commands/test-accounts.ts
1936
2173
  import { defineCommand as defineCommand7 } from "citty";
1937
2174
  import * as p3 from "@clack/prompts";
1938
- import { readFileSync as readFileSync8, writeFileSync as writeFileSync8, existsSync as existsSync9, mkdirSync as mkdirSync4 } from "fs";
2175
+ import { readFileSync as readFileSync9, writeFileSync as writeFileSync8, existsSync as existsSync9, mkdirSync as mkdirSync4 } from "fs";
1939
2176
  import { join as join10 } from "path";
1940
2177
  import { homedir as homedir4 } from "os";
1941
2178
  var SESSION_COOKIE2 = "__Secure-better-auth.session_token";
@@ -1945,7 +2182,7 @@ var ACCOUNTS_PATH = join10(DIR3, "test-accounts.json");
1945
2182
  function loadAccounts() {
1946
2183
  if (!existsSync9(ACCOUNTS_PATH)) return [];
1947
2184
  try {
1948
- return JSON.parse(readFileSync8(ACCOUNTS_PATH, "utf-8"));
2185
+ return JSON.parse(readFileSync9(ACCOUNTS_PATH, "utf-8"));
1949
2186
  } catch {
1950
2187
  return [];
1951
2188
  }
@@ -1955,7 +2192,7 @@ function saveAccounts(accounts) {
1955
2192
  writeFileSync8(ACCOUNTS_PATH, JSON.stringify(accounts, null, 2), { mode: 384 });
1956
2193
  }
1957
2194
  function sessionCookie() {
1958
- const token = readFileSync8(SESSION_PATH2, "utf-8").trim();
2195
+ const token = readFileSync9(SESSION_PATH2, "utf-8").trim();
1959
2196
  return `${SESSION_COOKIE2}=${encodeURIComponent(token)}`;
1960
2197
  }
1961
2198
  async function fetchRemoteAccounts() {
@@ -2225,214 +2462,10 @@ var test_accounts_default = defineCommand7({
2225
2462
  import { defineCommand as defineCommand8 } from "citty";
2226
2463
  import { execSync as execSync2 } from "child_process";
2227
2464
  import { randomUUID } from "crypto";
2228
- import { readFileSync as readFileSync9, readdirSync as readdirSync3, existsSync as existsSync10, writeFileSync as writeFileSync9 } from "fs";
2465
+ import { readFileSync as readFileSync10, readdirSync as readdirSync4, existsSync as existsSync10 } from "fs";
2229
2466
  import { join as join11, resolve as resolve7, dirname as dirname2 } from "path";
2230
2467
  import { pathToFileURL } from "url";
2231
2468
  import * as p4 from "@clack/prompts";
2232
-
2233
- // src/server/rooms/binding-manifest.ts
2234
- var AUTO_PROVISION_SENTINEL = "auto";
2235
- var VECTORIZE_METRICS = /* @__PURE__ */ new Set(["cosine", "euclidean", "dot-product"]);
2236
- var ALLOWED_BINDING_TYPES = /* @__PURE__ */ new Set([
2237
- "vectorize",
2238
- "ai",
2239
- "r2_bucket",
2240
- "kv_namespace",
2241
- "d1",
2242
- "queue",
2243
- "browser_rendering",
2244
- "analytics_engine",
2245
- "hyperdrive"
2246
- ]);
2247
- var RESERVED_BINDING_NAMES = /* @__PURE__ */ new Set([
2248
- "ASSETS",
2249
- "PLATFORM_WORKER",
2250
- "API_WORKER",
2251
- "APP_NAME",
2252
- "OWNER_USER_ID",
2253
- "AUTH_JWT_PUBLIC_KEY",
2254
- "AUTH_JWT_ISSUER",
2255
- "AUTH_WORKER_URL",
2256
- "APP_IDENTITY_TOKEN",
2257
- "APP_OWNER_JWT",
2258
- "USAGE_EVENTS"
2259
- ]);
2260
- function validateBindingManifest(manifest) {
2261
- if (!Array.isArray(manifest)) {
2262
- return { valid: false, errors: [{ reason: "Manifest must be an array" }] };
2263
- }
2264
- const errors = [];
2265
- const seenNames = /* @__PURE__ */ new Set();
2266
- for (const entry of manifest) {
2267
- if (!entry || typeof entry !== "object") {
2268
- errors.push({ reason: `Entry must be an object (got ${entry === null ? "null" : typeof entry})` });
2269
- continue;
2270
- }
2271
- const e = entry;
2272
- const type = e.type;
2273
- const name = e.name;
2274
- if (typeof type !== "string" || !ALLOWED_BINDING_TYPES.has(type)) {
2275
- errors.push({
2276
- reason: `Disallowed binding type: ${typeof type === "string" ? type : `(${typeof type})`}. Allowed: ${[...ALLOWED_BINDING_TYPES].sort().join(", ")}`
2277
- });
2278
- continue;
2279
- }
2280
- if (typeof name !== "string" || !name) {
2281
- errors.push({ reason: `Binding of type "${type}" missing 'name' (string)` });
2282
- continue;
2283
- }
2284
- if (RESERVED_BINDING_NAMES.has(name)) {
2285
- errors.push({ reason: `Binding name "${name}" is reserved by the SDK` });
2286
- continue;
2287
- }
2288
- if (seenNames.has(name)) {
2289
- errors.push({ reason: `Duplicate binding name "${name}"` });
2290
- continue;
2291
- }
2292
- seenNames.add(name);
2293
- const binding = entry;
2294
- const fieldErr = requiredFieldError(binding);
2295
- if (fieldErr) errors.push({ binding, reason: fieldErr });
2296
- }
2297
- if (errors.length) return { valid: false, errors };
2298
- return { valid: true, bindings: manifest };
2299
- }
2300
- function requiredFieldError(b) {
2301
- switch (b.type) {
2302
- case "vectorize": {
2303
- if (!b.index_name) return `vectorize binding "${b.name}" missing index_name (or "auto")`;
2304
- if (b.index_name === AUTO_PROVISION_SENTINEL) {
2305
- if (typeof b.dimensions !== "number" || b.dimensions <= 0) {
2306
- return `vectorize binding "${b.name}" with index_name="auto" requires "dimensions" (positive number)`;
2307
- }
2308
- if (!b.metric || !VECTORIZE_METRICS.has(b.metric)) {
2309
- return `vectorize binding "${b.name}" with index_name="auto" requires "metric" (one of: ${[...VECTORIZE_METRICS].join(", ")})`;
2310
- }
2311
- }
2312
- return null;
2313
- }
2314
- case "r2_bucket":
2315
- return b.bucket_name ? null : `r2_bucket binding "${b.name}" missing bucket_name (or "auto")`;
2316
- case "kv_namespace": {
2317
- if (!b.namespace_id) return `kv_namespace binding "${b.name}" missing namespace_id (or "auto")`;
2318
- if (b.namespace_id === AUTO_PROVISION_SENTINEL && !b.title) {
2319
- return `kv_namespace binding "${b.name}" with namespace_id="auto" requires "title"`;
2320
- }
2321
- return null;
2322
- }
2323
- case "d1": {
2324
- if (!b.id) return `d1 binding "${b.name}" missing id (or "auto")`;
2325
- if (b.id === AUTO_PROVISION_SENTINEL && !b.database_name) {
2326
- return `d1 binding "${b.name}" with id="auto" requires "database_name"`;
2327
- }
2328
- return null;
2329
- }
2330
- case "queue":
2331
- return b.queue_name ? null : `queue binding "${b.name}" missing queue_name (or "auto")`;
2332
- case "hyperdrive":
2333
- if (!b.id) return `hyperdrive binding "${b.name}" missing id`;
2334
- if (b.id === AUTO_PROVISION_SENTINEL) {
2335
- return `hyperdrive binding "${b.name}": auto-provisioning not yet supported (provide a real id)`;
2336
- }
2337
- return null;
2338
- case "ai":
2339
- case "browser_rendering":
2340
- case "analytics_engine":
2341
- return null;
2342
- // no required fields beyond `name`
2343
- default: {
2344
- const _exhaustive = b;
2345
- void _exhaustive;
2346
- return null;
2347
- }
2348
- }
2349
- }
2350
- function bindingManifestFromOutputConfig(outputConfig) {
2351
- const out = [];
2352
- const aiBinding = pluckString(outputConfig.ai, "binding");
2353
- if (aiBinding) out.push({ type: "ai", name: aiBinding });
2354
- for (const v of asObjectArray(outputConfig.vectorize)) {
2355
- const name = pluckString(v, "binding");
2356
- const index_name = pluckString(v, "index_name");
2357
- if (!name || !index_name) continue;
2358
- const dimensions = pluckNumber(v, "dimensions");
2359
- const metric = pluckString(v, "metric");
2360
- out.push({
2361
- type: "vectorize",
2362
- name,
2363
- index_name,
2364
- ...dimensions != null && { dimensions },
2365
- ...metric && (metric === "cosine" || metric === "euclidean" || metric === "dot-product") && { metric }
2366
- });
2367
- }
2368
- for (const r of asObjectArray(outputConfig.r2_buckets)) {
2369
- const name = pluckString(r, "binding");
2370
- const bucket_name = pluckString(r, "bucket_name");
2371
- if (name && bucket_name) out.push({ type: "r2_bucket", name, bucket_name });
2372
- }
2373
- for (const k of asObjectArray(outputConfig.kv_namespaces)) {
2374
- const name = pluckString(k, "binding");
2375
- const namespace_id = pluckString(k, "id");
2376
- if (!name || !namespace_id) continue;
2377
- const title = pluckString(k, "title");
2378
- out.push({
2379
- type: "kv_namespace",
2380
- name,
2381
- namespace_id,
2382
- ...title && { title }
2383
- });
2384
- }
2385
- for (const d of asObjectArray(outputConfig.d1_databases)) {
2386
- const name = pluckString(d, "binding");
2387
- const id = pluckString(d, "database_id");
2388
- if (!name || !id) continue;
2389
- const database_name = pluckString(d, "database_name");
2390
- out.push({
2391
- type: "d1",
2392
- name,
2393
- id,
2394
- ...database_name && { database_name }
2395
- });
2396
- }
2397
- const queues = isObject(outputConfig.queues) ? outputConfig.queues : null;
2398
- for (const q of asObjectArray(queues?.producers)) {
2399
- const name = pluckString(q, "binding");
2400
- const queue_name = pluckString(q, "queue");
2401
- if (name && queue_name) out.push({ type: "queue", name, queue_name });
2402
- }
2403
- const browserBinding = pluckString(outputConfig.browser, "binding");
2404
- if (browserBinding) out.push({ type: "browser_rendering", name: browserBinding });
2405
- for (const a of asObjectArray(outputConfig.analytics_engine_datasets)) {
2406
- const name = pluckString(a, "binding");
2407
- const dataset = pluckString(a, "dataset");
2408
- if (name) out.push({ type: "analytics_engine", name, dataset: dataset ?? void 0 });
2409
- }
2410
- for (const h of asObjectArray(outputConfig.hyperdrive)) {
2411
- const name = pluckString(h, "binding");
2412
- const id = pluckString(h, "id");
2413
- if (name && id) out.push({ type: "hyperdrive", name, id });
2414
- }
2415
- return out;
2416
- }
2417
- function isObject(v) {
2418
- return v !== null && typeof v === "object" && !Array.isArray(v);
2419
- }
2420
- function pluckString(v, key) {
2421
- if (!isObject(v)) return null;
2422
- const val = v[key];
2423
- return typeof val === "string" && val.length > 0 ? val : null;
2424
- }
2425
- function pluckNumber(v, key) {
2426
- if (!isObject(v)) return null;
2427
- const val = v[key];
2428
- return typeof val === "number" && Number.isFinite(val) ? val : null;
2429
- }
2430
- function asObjectArray(v) {
2431
- if (!Array.isArray(v)) return [];
2432
- return v.filter(isObject);
2433
- }
2434
-
2435
- // src/cli/commands/deploy.ts
2436
2469
  var DEPLOY_URL3 = process.env.DEEPSPACE_DEPLOY_URL ?? PLATFORM_URLS.deploy;
2437
2470
  var API_URL2 = process.env.DEEPSPACE_API_URL ?? PLATFORM_URLS.api;
2438
2471
  var CLOUDFLARE_DEPLOY_ERROR_HINT = "Deployment reached the DeepSpace deploy worker, but Cloudflare's deployment control-plane API failed. This is often caused by a Cloudflare Dashboard/API incident or Workers for Platforms entitlement outage. Already deployed apps should keep serving; wait for Cloudflare to recover and retry.";
@@ -2603,11 +2636,26 @@ var deploy_default = defineCommand8({
2603
2636
  );
2604
2637
  }
2605
2638
  p4.log.info(`Id: ${appId}`);
2639
+ const readCustomDevVarsAt = (path) => {
2640
+ if (!existsSync10(path)) return {};
2641
+ const section = extractCustomDevVars(stripGeneratedSecretsCache(readFileSync10(path, "utf-8")));
2642
+ if (!section) return {};
2643
+ try {
2644
+ return parseDevVars(section);
2645
+ } catch (err) {
2646
+ p4.cancel(`.dev.vars: ${errMessage(err)}`);
2647
+ process.exit(1);
2648
+ }
2649
+ };
2606
2650
  let linkedSecrets = null;
2651
+ let localOnlyDevVars = {};
2607
2652
  try {
2608
2653
  const refreshed = await refreshSecretsCache(DEPLOY_URL3, token, appId, envName);
2609
2654
  if (refreshed) {
2610
2655
  linkedSecrets = refreshed.pulled;
2656
+ localOnlyDevVars = readCustomDevVarsAt(
2657
+ devVarsPathFor(appDir, envName, { sharedDevVarsCache: true })
2658
+ );
2611
2659
  await writeDevVars(appDir, ownerId, token, envName, {
2612
2660
  appId,
2613
2661
  generatedSecretsCache: refreshed.rendered,
@@ -2616,7 +2664,14 @@ var deploy_default = defineCommand8({
2616
2664
  p4.log.info(refreshed.summary);
2617
2665
  }
2618
2666
  } catch (err) {
2619
- p4.cancel(`Failed to refresh app secrets before deploy: ${errMessage(err)}`);
2667
+ const msg = errMessage(err);
2668
+ if (err?.status === 403) {
2669
+ p4.cancel(
2670
+ "You're not authorized to deploy this app \u2014 you must be its owner or a current collaborator. If you were a collaborator, your access may have been revoked."
2671
+ );
2672
+ } else {
2673
+ p4.cancel(`Failed to refresh app secrets before deploy: ${msg}`);
2674
+ }
2620
2675
  process.exit(1);
2621
2676
  }
2622
2677
  const junk = removeMacosJunk(appDir);
@@ -2648,13 +2703,13 @@ var deploy_default = defineCommand8({
2648
2703
  p4.cancel("Build output config not found at .wrangler/deploy/config.json");
2649
2704
  process.exit(1);
2650
2705
  }
2651
- const deployConfig = JSON.parse(readFileSync9(deployConfigPath, "utf-8"));
2706
+ const deployConfig = JSON.parse(readFileSync10(deployConfigPath, "utf-8"));
2652
2707
  const outputWranglerPath = resolve7(dirname2(deployConfigPath), deployConfig.configPath);
2653
2708
  if (!existsSync10(outputWranglerPath)) {
2654
2709
  p4.cancel(`Output wrangler.json not found at ${outputWranglerPath}`);
2655
2710
  process.exit(1);
2656
2711
  }
2657
- const outputConfig = JSON.parse(readFileSync9(outputWranglerPath, "utf-8"));
2712
+ const outputConfig = JSON.parse(readFileSync10(outputWranglerPath, "utf-8"));
2658
2713
  if (outputConfig.name && outputConfig.name !== appName) {
2659
2714
  const envHint = envName ? ` (--env ${envName})` : "";
2660
2715
  p4.cancel(
@@ -2676,7 +2731,7 @@ var deploy_default = defineCommand8({
2676
2731
  s.start("Collecting assets...");
2677
2732
  const assets = collectAssets(clientDir);
2678
2733
  s.stop(`Collected ${assets.length} assets`);
2679
- const workerJs = readFileSync9(workerBundlePath, "utf-8");
2734
+ const workerJs = readFileSync10(workerBundlePath, "utf-8");
2680
2735
  const doBindings = outputConfig.durable_objects?.bindings;
2681
2736
  const sqliteClasses = new Set(
2682
2737
  outputConfig.migrations?.flatMap(
@@ -2708,18 +2763,13 @@ var deploy_default = defineCommand8({
2708
2763
  const devVarsPath = devVarsPathFor(appDir, envName, {
2709
2764
  sharedDevVarsCache: linkedSecrets !== null
2710
2765
  });
2711
- let userSecrets = {};
2712
- if (existsSync10(devVarsPath)) {
2713
- const rawDevVars = readFileSync9(devVarsPath, "utf-8");
2714
- const customSection = extractCustomDevVars(rawDevVars);
2715
- if (customSection) {
2716
- try {
2717
- userSecrets = parseDevVars(customSection);
2718
- } catch (err) {
2719
- p4.cancel(`.dev.vars: ${errMessage(err)}`);
2720
- process.exit(1);
2721
- }
2722
- }
2766
+ const userSecrets = linkedSecrets ? { ...linkedSecrets.values } : {};
2767
+ const handEditedDevVars = linkedSecrets ? localOnlyDevVars : readCustomDevVarsAt(devVarsPath);
2768
+ const strayDevVars = Object.keys(handEditedDevVars).filter((k) => !(k in userSecrets));
2769
+ if (strayDevVars.length) {
2770
+ p4.log.warn(
2771
+ `.dev.vars has ${strayDevVars.join(", ")} not in the app store \u2014 ${strayDevVars.length === 1 ? "it is" : "they are"} NOT deployed. \`.dev.vars\` is a local dev cache, not deploy config. Move ${strayDevVars.length === 1 ? "it" : "them"} into the store with \`deepspace secrets set KEY=value\` (or \`deepspace secrets upload .dev.vars\`), then redeploy.`
2772
+ );
2723
2773
  }
2724
2774
  const userSecretNames = Object.keys(userSecrets);
2725
2775
  const declaredBindingNames = /* @__PURE__ */ new Set([
@@ -2729,22 +2779,19 @@ var deploy_default = defineCommand8({
2729
2779
  for (const name of userSecretNames) {
2730
2780
  if (RESERVED_BINDING_NAMES.has(name)) {
2731
2781
  p4.cancel(
2732
- `.dev.vars: "${name}" is a reserved binding name and cannot be used as an app secret.`
2782
+ `App secret "${name}" is a reserved binding name \u2014 remove it with \`deepspace secrets delete ${name}\`.`
2733
2783
  );
2734
2784
  process.exit(1);
2735
2785
  }
2736
2786
  if (declaredBindingNames.has(name)) {
2737
2787
  p4.cancel(
2738
- `.dev.vars: "${name}" collides with a binding declared in wrangler.toml \u2014 rename one or the other.`
2788
+ `App secret "${name}" collides with a binding declared in wrangler.toml \u2014 rename one or the other.`
2739
2789
  );
2740
2790
  process.exit(1);
2741
2791
  }
2742
2792
  }
2743
2793
  if (userSecretNames.length) {
2744
- p4.log.info(`App secrets (from .dev.vars): ${userSecretNames.join(", ")}`);
2745
- if (!linkedSecrets) {
2746
- p4.log.info("Not in the app store yet \u2014 migrating after this deploy.");
2747
- }
2794
+ p4.log.info(`App secrets: ${userSecretNames.join(", ")}`);
2748
2795
  }
2749
2796
  s.start(`Deploying to ${appName}.app.space...`);
2750
2797
  const extraRoutes = extractRunWorkerFirst(wranglerConfig);
@@ -2874,16 +2921,6 @@ var deploy_default = defineCommand8({
2874
2921
  "First requests may briefly hit the assets transitional page; retry in a minute."
2875
2922
  );
2876
2923
  p4.log.success(`Live at: ${body.url}`);
2877
- await migrateLegacyDevVarsSecrets({
2878
- deployUrl: DEPLOY_URL3,
2879
- token,
2880
- appId,
2881
- envName,
2882
- devVarsPath,
2883
- userSecrets,
2884
- userSecretNames,
2885
- hadStore: linkedSecrets !== null
2886
- });
2887
2924
  await syncSubscriptionPlans(appDir, appId, token);
2888
2925
  await syncOneTimeProducts(appDir, appId, token);
2889
2926
  p4.outro("Done");
@@ -2892,41 +2929,11 @@ var deploy_default = defineCommand8({
2892
2929
  }
2893
2930
  s.stop("Deployed!");
2894
2931
  p4.log.success(`Live at: ${body.url}`);
2895
- await migrateLegacyDevVarsSecrets({
2896
- deployUrl: DEPLOY_URL3,
2897
- token,
2898
- appId,
2899
- envName,
2900
- devVarsPath,
2901
- userSecrets,
2902
- userSecretNames,
2903
- hadStore: linkedSecrets !== null
2904
- });
2905
2932
  await syncSubscriptionPlans(appDir, appId, token);
2906
2933
  await syncOneTimeProducts(appDir, appId, token);
2907
2934
  p4.outro("Done");
2908
2935
  }
2909
2936
  });
2910
- async function migrateLegacyDevVarsSecrets(opts) {
2911
- const { deployUrl, token, appId, envName, devVarsPath, userSecrets, userSecretNames, hadStore } = opts;
2912
- if (hadStore || userSecretNames.length === 0) return;
2913
- const configName = defaultConfigNameForEnv(envName);
2914
- try {
2915
- await uploadSecrets(deployUrl, token, appId, configName, userSecrets, false);
2916
- if (existsSync10(devVarsPath)) {
2917
- const cleaned = removeDevVarsKeys(readFileSync9(devVarsPath, "utf-8"), new Set(userSecretNames));
2918
- writeFileSync9(devVarsPath, cleaned.endsWith("\n") ? cleaned : `${cleaned}
2919
- `);
2920
- }
2921
- p4.log.success(
2922
- `Migrated ${userSecretNames.length} secret${userSecretNames.length === 1 ? "" : "s"} from .dev.vars into the app store (config ${configName}) \u2014 now managed by \`deepspace secrets\`.`
2923
- );
2924
- } catch (err) {
2925
- p4.log.warn(
2926
- `Couldn't migrate .dev.vars secrets into the app store (${errMessage(err)}); they deployed as bindings for now and the next deploy will retry.`
2927
- );
2928
- }
2929
- }
2930
2937
  async function syncSubscriptionPlans(appDir, appName, token) {
2931
2938
  const subsPath = join11(appDir, "src", "subscriptions.ts");
2932
2939
  if (!existsSync10(subsPath)) return;
@@ -3155,7 +3162,7 @@ async function waitForAssetsReady(url, timeoutMs) {
3155
3162
  function collectAssets(dir) {
3156
3163
  const assets = [];
3157
3164
  function walk(d, prefix) {
3158
- for (const entry of readdirSync3(d, { withFileTypes: true })) {
3165
+ for (const entry of readdirSync4(d, { withFileTypes: true })) {
3159
3166
  if (entry.name === ".assetsignore") continue;
3160
3167
  const full = join11(d, entry.name);
3161
3168
  const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
@@ -3164,7 +3171,7 @@ function collectAssets(dir) {
3164
3171
  } else {
3165
3172
  assets.push({
3166
3173
  path: "/" + rel,
3167
- contentBase64: readFileSync9(full).toString("base64")
3174
+ contentBase64: readFileSync10(full).toString("base64")
3168
3175
  });
3169
3176
  }
3170
3177
  }
@@ -3986,7 +3993,7 @@ var transfer_default = defineCommand15({
3986
3993
  import { defineCommand as defineCommand16 } from "citty";
3987
3994
 
3988
3995
  // src/cli/commands/_invoke-impl.ts
3989
- import { readFileSync as readFileSync10 } from "fs";
3996
+ import { readFileSync as readFileSync11 } from "fs";
3990
3997
  var API_URL6 = process.env.DEEPSPACE_API_URL ?? PLATFORM_URLS.api;
3991
3998
  var DEFAULT_TIMEOUT_MS = 12e4;
3992
3999
  function parseTarget(target) {
@@ -4011,7 +4018,7 @@ function resolveBody(opts) {
4011
4018
  if (opts.body != null) {
4012
4019
  raw = opts.body;
4013
4020
  } else if (opts.bodyFile != null) {
4014
- raw = opts.bodyFile === "-" ? readFileSync10(0, "utf-8") : readFileSync10(opts.bodyFile, "utf-8");
4021
+ raw = opts.bodyFile === "-" ? readFileSync11(0, "utf-8") : readFileSync11(opts.bodyFile, "utf-8");
4015
4022
  } else {
4016
4023
  return "{}";
4017
4024
  }
@@ -4356,7 +4363,7 @@ var invoke_default = defineCommand17({
4356
4363
 
4357
4364
  // src/cli/commands/library.ts
4358
4365
  import { defineCommand as defineCommand18 } from "citty";
4359
- import { existsSync as existsSync12, readFileSync as readFileSync11 } from "fs";
4366
+ import { existsSync as existsSync12, readFileSync as readFileSync12 } from "fs";
4360
4367
  import { join as join14, resolve as resolve9 } from "path";
4361
4368
  import { parse as parseToml4 } from "smol-toml";
4362
4369
  import * as p6 from "@clack/prompts";
@@ -4439,7 +4446,7 @@ var publish = defineCommand18({
4439
4446
  if (!existsSync12(wranglerPath)) {
4440
4447
  return die("No --app provided and no wrangler.toml found. Either pass --app <name> or run from your app's directory.");
4441
4448
  }
4442
- const cfg = parseToml4(readFileSync11(wranglerPath, "utf-8"));
4449
+ const cfg = parseToml4(readFileSync12(wranglerPath, "utf-8"));
4443
4450
  const nameRes = resolveAppName(cfg.name);
4444
4451
  if (!nameRes.ok) return die(`wrangler.toml: ${nameRes.reason}`);
4445
4452
  appName = nameRes.name;
@@ -4573,7 +4580,7 @@ var library_default = defineCommand18({
4573
4580
  // src/cli/commands/feedback.ts
4574
4581
  import { defineCommand as defineCommand19 } from "citty";
4575
4582
  import * as p7 from "@clack/prompts";
4576
- import { existsSync as existsSync13, readFileSync as readFileSync12 } from "fs";
4583
+ import { existsSync as existsSync13, readFileSync as readFileSync13 } from "fs";
4577
4584
  import { dirname as dirname3, join as join15 } from "path";
4578
4585
  import { fileURLToPath } from "url";
4579
4586
  import { release } from "os";
@@ -4602,7 +4609,7 @@ function readCliVersion() {
4602
4609
  ];
4603
4610
  for (const path of candidates) {
4604
4611
  if (!existsSync13(path)) continue;
4605
- const pkg2 = JSON.parse(readFileSync12(path, "utf-8"));
4612
+ const pkg2 = JSON.parse(readFileSync13(path, "utf-8"));
4606
4613
  if (pkg2.name === "deepspace" && pkg2.version) return pkg2.version;
4607
4614
  }
4608
4615
  } catch {
@@ -4812,7 +4819,7 @@ Context: deepspace ${payload.cliVersion} \xB7 ${payload.nodeVersion} \xB7 ${payl
4812
4819
 
4813
4820
  // src/cli/commands/secrets.ts
4814
4821
  import { defineCommand as defineCommand20 } from "citty";
4815
- import { readFileSync as readFileSync13 } from "fs";
4822
+ import { readFileSync as readFileSync14 } from "fs";
4816
4823
  var DEPLOY_URL6 = process.env.DEEPSPACE_DEPLOY_URL ?? PLATFORM_URLS.deploy;
4817
4824
  var COMMON_ARGS = {
4818
4825
  app: {
@@ -4844,6 +4851,11 @@ async function resolveTarget(args) {
4844
4851
  const appDir = findAppDir();
4845
4852
  appId = appDir && readAppId(appDir, wranglerEnv) || void 0;
4846
4853
  if (!appId) {
4854
+ if (wranglerEnv && appDir && readAppId(appDir, void 0)) {
4855
+ throw new Error(
4856
+ `No app id for env "${wranglerEnv}" \u2014 wrangler.toml has no [env.${wranglerEnv}] block with its own DEEPSPACE_APP_ID. To target the "${wranglerEnv}" config on this app, use \`-c ${wranglerEnv}\` instead of \`--env ${wranglerEnv}\`.`
4857
+ );
4858
+ }
4847
4859
  throw new Error(
4848
4860
  "No app id. Run from an app directory whose wrangler.toml carries DEEPSPACE_APP_ID (`deepspace init` mints one), or pass --app <appId>."
4849
4861
  );
@@ -4857,6 +4869,14 @@ function fail(err) {
4857
4869
  console.error(err instanceof Error ? err.message : String(err));
4858
4870
  process.exit(1);
4859
4871
  }
4872
+ function warnIfDebugRoutesEnabled(secrets) {
4873
+ if (secrets.ALLOW_DEBUG_ROUTES !== "true") return false;
4874
+ console.warn(
4875
+ "\n\u26A0 ALLOW_DEBUG_ROUTES=true exposes an UNAUTHENTICATED debug API on the deployed app \u2014\n anyone who can reach /api/debug/* can read and mutate any record. Redeploy to apply;\n `deepspace secrets delete ALLOW_DEBUG_ROUTES` (then redeploy) turns it back off."
4876
+ );
4877
+ return true;
4878
+ }
4879
+ var APPLY_HINT = "Run `deepspace deploy` to apply \u2014 secrets take effect at deploy time.";
4860
4880
  var list5 = defineCommand20({
4861
4881
  meta: { name: "list", description: "List masked secrets in a config" },
4862
4882
  args: {
@@ -4881,7 +4901,7 @@ var list5 = defineCommand20({
4881
4901
  return;
4882
4902
  }
4883
4903
  for (const s of secrets) {
4884
- console.log(args["only-names"] ? s.key : `${s.key} (v${s.version}, ${s.valueBytes}B, ${s.updatedAt})`);
4904
+ console.log(args["only-names"] ? s.key : `${s.key} (v${s.version}, ${s.updatedAt})`);
4885
4905
  }
4886
4906
  } catch (err) {
4887
4907
  fail(err);
@@ -4897,18 +4917,25 @@ var set = defineCommand20({
4897
4917
  async run({ args }) {
4898
4918
  try {
4899
4919
  const t = await resolveTarget(args);
4900
- const pairs = [args.secret, ...args._ ?? []].filter(
4920
+ const pairs = [.../* @__PURE__ */ new Set([args.secret, ...args._ ?? []])].filter(
4901
4921
  (v) => typeof v === "string" && v.length > 0
4902
4922
  );
4903
4923
  const secrets = {};
4924
+ const dupes = [];
4904
4925
  for (const pair of pairs) {
4905
4926
  const eq = pair.indexOf("=");
4906
4927
  if (eq <= 0) throw new Error(`Expected KEY=value, got "${pair}"`);
4907
4928
  const key = validateSecretName(pair.slice(0, eq));
4908
4929
  const value = pair.slice(eq + 1);
4909
4930
  validateSecretValue(key, value);
4931
+ if (key in secrets && !dupes.includes(key)) dupes.push(key);
4910
4932
  secrets[key] = value;
4911
4933
  }
4934
+ if (dupes.length) {
4935
+ console.warn(
4936
+ `Warning: ${dupes.join(", ")} given more than once \u2014 kept the last value for each.`
4937
+ );
4938
+ }
4912
4939
  if (Object.keys(secrets).length === 1) {
4913
4940
  const [[k, v]] = Object.entries(secrets);
4914
4941
  await setSecret(DEPLOY_URL6, t.token, t.appId, t.configName, k, v);
@@ -4916,6 +4943,7 @@ var set = defineCommand20({
4916
4943
  await uploadSecrets(DEPLOY_URL6, t.token, t.appId, t.configName, secrets, false);
4917
4944
  }
4918
4945
  console.log(`Set ${Object.keys(secrets).length} secret${Object.keys(secrets).length === 1 ? "" : "s"} in ${t.configName}.`);
4946
+ if (!warnIfDebugRoutesEnabled(secrets)) console.log(APPLY_HINT);
4919
4947
  } catch (err) {
4920
4948
  fail(err);
4921
4949
  }
@@ -4934,13 +4962,13 @@ var get = defineCommand20({
4934
4962
  const key = validateSecretName(args.key);
4935
4963
  if (args.plain) {
4936
4964
  const { value } = await getSecretPlain(DEPLOY_URL6, t.token, t.appId, t.configName, key);
4937
- process.stdout.write(value + "\n");
4965
+ process.stdout.write(process.stdout.isTTY ? value + "\n" : value);
4938
4966
  return;
4939
4967
  }
4940
4968
  const { secrets } = await listSecrets(DEPLOY_URL6, t.token, t.appId, t.configName);
4941
4969
  const item = secrets.find((s) => s.key === key);
4942
4970
  if (!item) fail(new Error(`Secret "${key}" not found in ${t.configName}`));
4943
- console.log(`${item.key} (v${item.version}, ${item.valueBytes}B, ${item.updatedAt})`);
4971
+ console.log(`${item.key} (v${item.version}, ${item.updatedAt})`);
4944
4972
  } catch (err) {
4945
4973
  fail(err);
4946
4974
  }
@@ -4955,13 +4983,24 @@ var del2 = defineCommand20({
4955
4983
  async run({ args }) {
4956
4984
  try {
4957
4985
  const t = await resolveTarget(args);
4958
- const keys = [args.key, ...args._ ?? []].filter(
4986
+ const keys = [.../* @__PURE__ */ new Set([args.key, ...args._ ?? []])].filter(
4959
4987
  (v) => typeof v === "string" && v.length > 0
4960
4988
  );
4989
+ let deleted = 0;
4990
+ let absent = 0;
4961
4991
  for (const key of keys) {
4962
- await deleteSecret(DEPLOY_URL6, t.token, t.appId, t.configName, validateSecretName(key));
4992
+ const name = validateSecretName(key);
4993
+ try {
4994
+ await deleteSecret(DEPLOY_URL6, t.token, t.appId, t.configName, name);
4995
+ deleted++;
4996
+ } catch (err) {
4997
+ if (err?.status === 404) absent++;
4998
+ else throw err;
4999
+ }
4963
5000
  }
4964
- console.log(`Deleted ${keys.length} secret${keys.length === 1 ? "" : "s"} from ${t.configName}.`);
5001
+ const note3 = absent > 0 ? ` (${absent} already absent)` : "";
5002
+ console.log(`Deleted ${deleted} secret${deleted === 1 ? "" : "s"} from ${t.configName}.${note3}`);
5003
+ if (deleted > 0) console.log(APPLY_HINT);
4965
5004
  } catch (err) {
4966
5005
  fail(err);
4967
5006
  }
@@ -4981,11 +5020,12 @@ var upload = defineCommand20({
4981
5020
  async run({ args }) {
4982
5021
  try {
4983
5022
  const t = await resolveTarget(args);
4984
- const content = args.file === "-" ? readFileSync13(0, "utf-8") : readFileSync13(args.file, "utf-8");
5023
+ const content = args.file === "-" ? readFileSync14(0, "utf-8") : readFileSync14(args.file, "utf-8");
4985
5024
  const secrets = parseSecretsUpload(content);
4986
5025
  if (Object.keys(secrets).length === 0) fail(new Error("No secrets found in the input."));
4987
5026
  await uploadSecrets(DEPLOY_URL6, t.token, t.appId, t.configName, secrets, args.replace);
4988
5027
  console.log(`Uploaded ${Object.keys(secrets).length} secrets to ${t.configName}.`);
5028
+ if (!warnIfDebugRoutesEnabled(secrets)) console.log(APPLY_HINT);
4989
5029
  } catch (err) {
4990
5030
  fail(err);
4991
5031
  }
@@ -5073,7 +5113,20 @@ var configsCreate = defineCommand20({
5073
5113
  try {
5074
5114
  const t = await resolveTarget(args);
5075
5115
  const name = validateConfigName(args.name);
5076
- await createConfig(DEPLOY_URL6, t.token, t.appId, name, args["copy-from"] || void 0);
5116
+ const copyFrom = args["copy-from"] || void 0;
5117
+ const { configs: configs2 } = await listConfigs(DEPLOY_URL6, t.token, t.appId);
5118
+ if (configs2.some((c) => c.name === name)) {
5119
+ if (copyFrom) {
5120
+ fail(
5121
+ new Error(
5122
+ `Config "${name}" already exists \u2014 refusing to copy "${copyFrom}" over it. Delete it first, or pick a new name.`
5123
+ )
5124
+ );
5125
+ }
5126
+ console.log(`Config ${name} already exists.`);
5127
+ return;
5128
+ }
5129
+ await createConfig(DEPLOY_URL6, t.token, t.appId, name, copyFrom);
5077
5130
  console.log(`Created ${name}.`);
5078
5131
  } catch (err) {
5079
5132
  fail(err);
@@ -5138,7 +5191,9 @@ var init_default = defineCommand21({
5138
5191
  async run({ args }) {
5139
5192
  const appDir = findAppDir();
5140
5193
  if (!appDir) {
5141
- console.error("Run from a DeepSpace app directory (one containing wrangler.toml).");
5194
+ console.error(
5195
+ "No wrangler.toml here \u2014 `deepspace init` stamps an app id into an existing DeepSpace app. To create a new app, run `deepspace create <name>` (or `npm create deepspace@latest <name>`), then run this from inside it."
5196
+ );
5142
5197
  process.exit(1);
5143
5198
  }
5144
5199
  const envName = args.env || void 0;
@@ -5161,7 +5216,7 @@ var init_default = defineCommand21({
5161
5216
 
5162
5217
  // src/cli/cli.ts
5163
5218
  var __dirname = dirname4(fileURLToPath2(import.meta.url));
5164
- var pkg = JSON.parse(readFileSync14(join16(__dirname, "..", "package.json"), "utf-8"));
5219
+ var pkg = JSON.parse(readFileSync15(join16(__dirname, "..", "package.json"), "utf-8"));
5165
5220
  if (process.platform === "linux" && !process.env.SSL_CERT_FILE && existsSync14("/etc/ssl/certs/ca-certificates.crt")) {
5166
5221
  process.env.SSL_CERT_FILE = "/etc/ssl/certs/ca-certificates.crt";
5167
5222
  }