deepspace 0.33.1 → 0.34.0
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/CHANGELOG.md +35 -0
- package/README.md +5 -1
- package/dist/build.js.map +1 -1
- package/dist/chunk-KELQ7OD5.js +1957 -0
- package/dist/chunk-KELQ7OD5.js.map +1 -0
- package/dist/{chunk-PZDVKSLI.js → chunk-SINBB5BQ.js} +1 -1
- package/dist/{chunk-PZDVKSLI.js.map → chunk-SINBB5BQ.js.map} +1 -1
- package/dist/cli.js +74 -15
- package/dist/cli.js.map +1 -1
- package/dist/documentation-client-core.js +1 -1
- package/dist/documentation-default-renderer.cjs +1 -1
- package/dist/documentation-react.js +1 -1
- package/dist/documentation-runtime.js +10 -10
- package/dist/documentation-server-core.js +1 -1
- package/dist/documentation.js +47 -22
- package/dist/documentation.js.map +1 -1
- package/dist/expo.d.ts +73 -0
- package/dist/expo.js +233 -0
- package/dist/expo.js.map +1 -0
- package/dist/index.d.ts +105 -22
- package/dist/index.js +50 -23
- package/dist/index.js.map +1 -1
- package/dist/server.d.ts +205 -37
- package/dist/server.js +290 -97
- package/dist/server.js.map +1 -1
- package/dist/worker.d.ts +312 -48
- package/dist/worker.js +499 -110
- package/dist/worker.js.map +1 -1
- package/features/documents/feature.json +15 -0
- package/features/documents/src/DocumentsListPage.tsx +22 -8
- package/features/documents/src/InviteDialog.tsx +63 -42
- package/features/documents/src/__tests__/documents-find-invitee-action.test.ts +131 -0
- package/features/documents/src/__tests__/documents-invitee-lookup.test.ts +67 -0
- package/features/documents/src/documents-find-invitee-action.ts +56 -0
- package/features/documents/src/documents-invitee-lookup.ts +38 -0
- package/features/documents/src/documents-ui.css +10 -0
- package/package.json +28 -8
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,40 @@
|
|
|
1
1
|
# deepspace
|
|
2
2
|
|
|
3
|
+
## 0.34.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- Give Claude Anthropic's hosted code-execution sandbox through the DeepSpace proxy, so an agent can read, edit and create real files (Word, Excel, PowerPoint, PDF, charts). New `deepspace/worker` helpers: `codeExecutionTool()`, `callableFromSandbox(tool)` (sandbox code may call the app's own tool), `sandboxUpload(file)`, `reuseSandbox(containerId)`, `forwardSandboxContainer`, `sandboxOutputs(steps)`, and `sandboxFiles(env)` for upload/download/list/delete. Files and containers are private to the calling user by default, or shared across the app with `createDeepSpaceAI(env, 'anthropic', { sandboxScope: 'app' })`; another app can never reach them. `createDeepSpaceAI` now sends the app's identity with every call.
|
|
8
|
+
|
|
9
|
+
**Breaking:** the SDK moves to AI SDK 7 (`ai` 7.0.107, `@ai-sdk/anthropic` 4, `@ai-sdk/openai` 4, `@ai-sdk/openai-compatible` 3). Apps must bump their own `ai` to the same version and migrate their AI code; `deepspace app update` lists the steps (migration `2026-09-ai-sdk-7`).
|
|
10
|
+
|
|
11
|
+
- Add the `deepspace/expo` client entry for Expo 57 with PKCE-protected OAuth,
|
|
12
|
+
SecureStore-backed sessions, JWT refresh, authenticated app requests, and
|
|
13
|
+
scaffolded native auth routes.
|
|
14
|
+
- Refresh the model lineup: Claude Opus 5.5 (now the default for agents and direct generation), Claude Fable 5.1, GPT-6 Sol and GPT-6 Luna join Claude Sonnet 5 and Haiku 4.5 in the model picker; GPT-6 Astra and Cerebras Qwen 3.8 27B are available for direct generation. Claude Opus 5, Claude Fable 5 and GPT-5.6 Sol/Terra/Luna leave the picker but still resolve by id on the server, so apps that pass them keep working; a chat panel whose saved choice was one of them falls back to the default. The new default costs more per token than Sonnet 5 ($4/$20 vs $2/$10 per million); pass a model id to keep Sonnet 5. The platform's `anthropic/chat-completion` and `openai/chat-completion` integrations now default to Claude Opus 5.5 and GPT-6 Sol and bill each token class at its own rate instead of the output rate.
|
|
15
|
+
|
|
16
|
+
### Patch Changes
|
|
17
|
+
|
|
18
|
+
- `deepspace agent invoke` now carries the structured `detail` an app attaches to any refused call (for example which of two 409s a conflict is, or a 503's retry guidance) in both the human line and the `--json` failure envelope, redacted, so an agent can act on the app's stated reason.
|
|
19
|
+
- Display token-priced integrations as readable per-million-token rates in the CLI.
|
|
20
|
+
- Raise two dependency floors for the September 2026 security advisories. hono moves to ^4.13.7 in the SDK's dependencies and the scaffold template: 4.13.5 is the first release fixing query parsing past the URL fragment, `toSSG()` path containment, and unbounded dot-notation nesting in `parseBody()`, and 4.13.7 adds the `hono/jsx` escaping fix. smol-toml moves to ^1.7.1 in the SDK's dependencies, the first release fixing the `parse()` infinite loop on a trailing comment inside an array or inline table (GHSA-7w5x-hrqm-74c2); the CLI parses `wrangler.toml` with it.
|
|
21
|
+
- Documents feature: an ordinary member who owns a document can now share it by email — the invite dialog resolves the address through a new owner-only server action (`documents-find-invitee`, installed with the feature) instead of a client-side read that only ever worked for the app owner, and its "People with access" list is drawn from the roster so member owners can manage collaborators. Bulleted and numbered lists in the editor render their markers again, and a refused "New document" write is reported instead of being an unhandled rejection.
|
|
22
|
+
|
|
23
|
+
Runtime (every app on this release): the record room's `user.update` frame is now only the presence heartbeat it always was on the SDK client — it no longer takes an `email`, `name`, or `imageUrl`; those are the identity the auth plane verified at connect (`email` is the key invites resolve on, `name`/`imageUrl` are what a share dialog shows about a collaborator), so a member can no longer be resolved in, or pose as, another user — and `registerUser` / the `users.register` tool store the email trimmed and lowercased so exact-match lookups agree with what the auth plane writes.
|
|
24
|
+
|
|
25
|
+
Reach: new installs, and re-installs of the feature. `deepspace add documents` on an app that already has it adds the action and the lookup module and registers the action, but keeps every feature file that already exists — delete the copied documents files you have not customized before re-running it — at least `src/pages/documents/InviteDialog.tsx` (member sharing), `src/pages/documents/documents-ui.css` (list markers) and `src/pages/documents/index.tsx` (refused-create toast), which are the three files these fixes change.
|
|
26
|
+
|
|
27
|
+
- Show free-tier and paid-tier integration prices in the CLI cost prompt.
|
|
28
|
+
- Allow selected JobRoom job types to run beyond the 15-minute alarm limit using `backgroundJobTypes`.
|
|
29
|
+
- `createDeepSpaceAI` fixes for the AI SDK 7 providers. GPT-6 Sol and Luna agents work again: `@ai-sdk/openai` 4 drops `reasoningEffort: 'none'` for GPT-6, and OpenAI refuses function tools on Chat Completions without it, so the SDK restores it for GPT-5.6/GPT-6 Sol, Terra and Luna tool calls that carry no effort.
|
|
30
|
+
|
|
31
|
+
Anthropic calls with no `maxOutputTokens` now default to 64K output tokens instead of the model's ceiling (128K for Claude 5), reducing upfront credit holds. Billing still uses actual usage. Explicit `maxOutputTokens` values override the SDK default.
|
|
32
|
+
|
|
33
|
+
Adaptive thinking shares this limit with the response. With `thinking.type: 'enabled'`, the provider adds `budgetTokens` to the output allowance, capped at the model's ceiling.
|
|
34
|
+
|
|
35
|
+
- SEO-ready scaffolds. New apps prerender their public pages to static HTML at `vite build` — the same build-time approach deep.space uses, now shipped as app code: `prerender.ts` at the app root and an explicit page list in `src/prerender-entry.tsx`. The build stamps each page's title, description, canonical, and Open Graph tags from `src/seo.ts`, writes `sitemap.xml` and a plain `_spa.html` shell for client routes, and appends the `Sitemap:` line to `public/robots.txt` (or publishes `Disallow: /` when `noindex` is set), so crawlers that do not execute JavaScript (Bing, link unfurlers, AI answer engines) read real content. `src/main.tsx` hydrates a prerendered page instead of repainting it, and the worker's client-route fallback serves the shell before `/`. The `<Seo>` head component ships in the app as well (`src/components/Seo.tsx`), keeping the SDK client chunk out of the landing bundle. `deepspace deploy` passes `DEEPSPACE_SITE_ORIGIN` to the build so staging resolves canonical URLs to `spacestest.com`. Existing apps are unchanged: the prerender is scaffold code, copied into new apps only, and the SDK build path is untouched.
|
|
36
|
+
- Allow auth workers to declare server-owned user fields and populate them in a before-create hook.
|
|
37
|
+
|
|
3
38
|
## 0.33.1
|
|
4
39
|
|
|
5
40
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -28,7 +28,7 @@ npm install deepspace
|
|
|
28
28
|
|
|
29
29
|
## Entry points
|
|
30
30
|
|
|
31
|
-
The package has
|
|
31
|
+
The package has ten supported import paths:
|
|
32
32
|
|
|
33
33
|
- **`deepspace`** — the React client SDK (hooks, providers, auth, storage,
|
|
34
34
|
messaging, theme). Runs in the browser.
|
|
@@ -38,6 +38,10 @@ The package has seven supported import paths:
|
|
|
38
38
|
- **`deepspace/server`** — app-server helpers for actions, billing, and room
|
|
39
39
|
handlers.
|
|
40
40
|
- **`deepspace/testing`** — Playwright fixtures for multi-user tests.
|
|
41
|
+
- **`deepspace/testing/mcp`** — dependency-free MCP wire client.
|
|
42
|
+
- **`deepspace/build`** — Node-only app build helpers.
|
|
43
|
+
- **`deepspace/expo`** — Expo 57 native auth client with PKCE,
|
|
44
|
+
SecureStore-backed sessions, JWT refresh, and authenticated requests.
|
|
41
45
|
- **`deepspace/documentation`** — documentation compiler and runtime helpers.
|
|
42
46
|
- **`deepspace/documentation/react`** — documentation React components.
|
|
43
47
|
|
package/dist/build.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/build/app-id.ts","../src/server/utils/registry-client.ts","../src/cli/lib/wrangler-env.ts","../src/build/plugin.ts"],"sourcesContent":["/**\n * `deepspace/build` — build-time helpers an app's Vite config imports.\n *\n * Node-only. Nothing here may be reached from client or worker code; it runs\n * while `vite build` / `vite dev` is assembling the bundle, never inside it.\n *\n * The one job today is resolving the app's immutable id for the browser.\n * `wrangler.toml` is the single source of truth for app identity — the worker\n * reads `DEEPSPACE_APP_ID` off `env`, and every durable thing (data scope\n * `app:{appId}`, secrets, billing) keys to it. The browser has no `env`, so\n * the build stamps the same value in as `__DEEPSPACE_APP_ID__`, read from the\n * config THIS build is running against. A literal baked into the client once,\n * at scaffold time, is what let `deepspace deploy --env staging` ship a worker\n * writing staging's rooms beside a browser reading production's, with nothing\n * reporting the split.\n *\n * Living in the SDK rather than in a file copied into every app means one\n * definition, no per-app duplicate, and `smol-toml` stays the SDK's dependency\n * instead of every app's.\n */\n\nimport { resolve } from 'node:path'\n\n/** `app_` + 26-char Crockford ULID. */\n// Single source: the shared registry client owns the id shape.\nimport { APP_ID_RE } from '../server/utils/registry-client'\n// Single source: `cli/lib/wrangler-env` is the SDK's only wrangler.toml reader,\n// so the build sees the same parse, the same failure shape, and the same\n// whole-file guards the CLI does. Both are Node-only. Imported as that one\n// module, never through the CLI barrel — nothing else of the CLI belongs in an\n// app's build graph.\nimport { readAppIdVar, readWranglerConfigFile } from '../cli/lib/wrangler-env'\n\n/**\n * The build's environment, typed structurally. This module is imported from an\n * app's `vite.config.ts`, which sits outside the app's tsconfig `include` and\n * runs where the app carries no `@types/node`, so nothing here leans on the\n * ambient `NodeJS` namespace.\n */\nexport type BuildEnv = Record<string, string | undefined>\n\nexport interface ResolveAppIdOptions {\n /**\n * The app root — the directory holding `wrangler.toml`. The caller passes\n * its own location (`fileURLToPath(new URL('.', import.meta.url))` from\n * `vite.config.ts`), so the config is found by an authoritative anchor\n * rather than a guessed `process.cwd()`.\n */\n appDir: string\n /** Defaults to `process.env`; injectable for tests. */\n env?: BuildEnv\n}\n\n/**\n * Resolve the app id the browser bundle must carry.\n *\n * Mirrors the Cloudflare Vite plugin so dev, a bare `vite build`, and\n * `deepspace deploy --env <name>` all agree:\n *\n * config ← CLOUDFLARE_VITE_WRANGLER_CONFIG_PATH, else <appDir>/wrangler.toml\n * section ← [env.<CLOUDFLARE_ENV>.vars] when that is set, else [vars]\n *\n * Env blocks do NOT inherit `[vars]` — each environment is its own app — so a\n * missing or malformed id throws and fails the build. It never falls back to\n * another environment's id: a fallback that silently picks the wrong one is\n * exactly the bug this exists to remove.\n */\nexport function resolveAppId({ appDir, env = process.env }: ResolveAppIdOptions): string {\n const configPath = env.CLOUDFLARE_VITE_WRANGLER_CONFIG_PATH\n ? resolve(env.CLOUDFLARE_VITE_WRANGLER_CONFIG_PATH)\n : resolve(appDir, 'wrangler.toml')\n const envName = env.CLOUDFLARE_ENV\n const section = envName ? `[env.${envName}.vars]` : '[vars]'\n\n const appId = readAppIdVar(readWranglerConfigFile(configPath), envName)\n if (typeof appId !== 'string' || !APP_ID_RE.test(appId)) {\n // A fresh-but-uninitialized scaffold (signed out at scaffold time) still\n // carries the __APP_ID__ placeholder — the remedy is registration, not\n // hand-writing an id (ids are server-minted at `deepspace app init`).\n const uninitialized = appId === undefined || appId === '__APP_ID__'\n throw new Error(\n `${configPath}: ${section} has no valid DEEPSPACE_APP_ID ` +\n `(found ${appId === undefined ? 'nothing' : JSON.stringify(appId)}). ` +\n `The browser keys its RecordRooms to this id, so the build stops rather than ship a wrong or empty one. ` +\n (uninitialized\n ? `Run \\`npx deepspace app init${envName ? ` --env ${envName}` : ''}\\` to register this app — ids are server-minted; never hand-write one.`\n : `Add the id your \\`deepspace app init\\` registered under ${section}.`) +\n (envName ? ` Environments do not inherit the top-level id; each one is its own app.` : ``),\n )\n }\n return appId\n}\n\n/**\n * Vite `define` entry consumed by an app's `vite.config.ts` and\n * `vitest.config.ts`, so unit tests see the same id the bundle is built with:\n *\n * ```ts\n * import { fileURLToPath } from 'node:url'\n * import { appIdDefine } from 'deepspace/build'\n * export default defineConfig({\n * define: appIdDefine({ appDir: fileURLToPath(new URL('.', import.meta.url)) }),\n * })\n * ```\n */\nexport function appIdDefine(options: ResolveAppIdOptions): Record<string, string> {\n return { __DEEPSPACE_APP_ID__: JSON.stringify(resolveAppId(options)) }\n}\n\n/**\n * How an app scaffolded before the build-injected id adopts it: one edit per\n * file, stated ONCE so the deploy refusals (`app_id_env_mismatch`,\n * `app_id_define_unsubstituted`) and `app update` guidance cannot disagree\n * about what it is. Keyed by the file each manual edit lands in.\n */\nexport const APP_ID_ADOPTION_EDITS = {\n 'vite.config.ts':\n `import { deepspaceBuild } from 'deepspace/build' and add\\n` +\n ` deepspaceBuild({ appDir: fileURLToPath(new URL('.', import.meta.url)) }) to plugins`,\n 'vitest.config.ts': `import { appIdDefine } and set define: appIdDefine({ appDir })`,\n 'src/constants.ts':\n `declare const __DEEPSPACE_APP_ID__: string; export it as APP_ID\\n` +\n ` (drop the frozen literal)`,\n} as const\n\n/** The three edits are one change; this is the sentence that says so. */\nexport const APP_ID_ADOPTION_TOGETHER =\n `Apply all three together: src/constants.ts on its own builds clean and then throws ` +\n `in the browser. A newly scaffolded app already has this.`\n\n/** The full retrofit as the deploy refusals print it. */\nexport const APP_ID_ADOPTION_STEPS =\n `Point the client at the build-injected id — the SDK's \\`deepspace/build\\` supplies it:\\n` +\n Object.entries(APP_ID_ADOPTION_EDITS)\n .map(([file, edit]) => ` • ${file}: ${edit}\\n`)\n .join('') +\n APP_ID_ADOPTION_TOGETHER\n","/**\n * The ONE registry client. Every platform worker reaches the app-registry\n * Durable Object (hosted by the deploy worker, singleton `v1:global`) through\n * a cross-script REGISTRY binding — and before this module, each of the five\n * workers hand-rolled its own copy of the same mechanics: the instance name\n * appeared in 6 places, the `https://registry/` URL in 6, `APP_ID_RE` in 5,\n * and `RegistryClientError` + `registryErrorJson` existed byte-identical in\n * two workers. Copies that must agree and nothing enforcing it.\n *\n * This module owns the MECHANICS: instance name, stub lookup, the wire call,\n * the error type, and the standard route mapping. Each worker still declares\n * its own typed convenience wrapper listing only the actions it uses — that\n * subset is genuinely per-worker; the plumbing is not.\n */\n\n/** The singleton registry instance — one DO holds every app/route/collaborator. */\nexport const REGISTRY_INSTANCE = 'v1:global'\n\n/** Public app ids have one canonical ULID-minted shape. */\nexport const APP_ID_RE = /^app_[0-9A-HJKMNP-TV-Z]{26}$/\nexport const STRICT_APP_ID_RE = APP_ID_RE\n\n/** Internal physical locators survive identity migration and are not public ids. */\nexport const RESOURCE_ID_RE = /^(app_[0-9A-HJKMNP-TV-Z]{26}|[a-z0-9][a-z0-9_-]{0,63})$/\n\nconst CROCKFORD = '0123456789ABCDEFGHJKMNPQRSTVWXYZ'\n\n/** A 26-char ULID (48-bit ms timestamp + 80 random bits, Crockford base32) —\n * the id shape shared by app ids (`app_…`) and workspace ids (`ws_…`). */\nexport function mintUlid(now = Date.now()): string {\n let ts = ''\n let t = now\n for (let i = 0; i < 10; i++) {\n ts = CROCKFORD[t % 32] + ts\n t = Math.floor(t / 32)\n }\n const rand = new Uint8Array(10)\n crypto.getRandomValues(rand)\n let rs = ''\n let acc = 0\n let bits = 0\n for (const byte of rand) {\n acc = (acc << 8) | byte\n bits += 8\n while (bits >= 5) {\n bits -= 5\n rs += CROCKFORD[(acc >> bits) & 31]\n }\n }\n return `${ts}${rs}`.slice(0, 26)\n}\n\n/** Mint a fresh app id: `app_` + 26-char ULID. App ids are minted ONLY by the\n * deploy worker's authenticated `POST /api/apps/mint`, which registers the id\n * to its caller in the same step — an id with no owner never exists. */\nexport function mintAppId(now = Date.now()): string {\n return `app_${mintUlid(now)}`\n}\n\nexport class RegistryClientError extends Error {\n constructor(\n message: string,\n readonly code: string,\n readonly status: number,\n ) {\n super(message)\n this.name = 'RegistryClientError'\n }\n}\n\nexport function registryStub(ns: DurableObjectNamespace): DurableObjectStub {\n return ns.get(ns.idFromName(REGISTRY_INSTANCE))\n}\n\n/**\n * One registry action over the wire. Non-2xx answers throw\n * {@link RegistryClientError} carrying the DO's structured `code` (or\n * `'error'` for a transport-shaped body) — callers distinguish a registry-\n * REPORTED miss (`code === 'not_found'`) from an outage by that code, never\n * by treating every throw as \"not found\".\n */\nexport async function callRegistry<T>(\n stub: DurableObjectStub,\n body: { action: string } & Record<string, unknown>,\n): Promise<T> {\n const res = await stub.fetch('https://registry/', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(body),\n })\n const json = (await res.json().catch(() => ({}))) as { error?: string; code?: string }\n if (!res.ok) {\n throw new RegistryClientError(\n json.error ?? `Registry error (${res.status})`,\n json.code ?? 'error',\n res.status,\n )\n }\n return json as T\n}\n\n/** Standard mapping of a registry failure onto a route response. */\nexport function registryErrorJson(\n c: { json: (body: unknown, status?: number) => Response },\n err: unknown,\n): Response {\n if (err instanceof RegistryClientError) {\n return c.json({ error: err.message, code: err.code }, err.status as 400)\n }\n throw err\n}\n","/**\n * Wrangler env resolution for DeepSpace's multi-environment deploys.\n *\n * Lets a single repo deploy to `<app>.app.space` and `<app>-staging.app.space`\n * (or any other named environment) from one `wrangler.toml`:\n *\n * name = \"hopkins\"\n * [vars]\n * APP_NAME = \"hopkins\"\n *\n * [env.staging]\n * name = \"hopkins-staging\"\n * [env.staging.vars]\n * APP_NAME = \"hopkins-staging\"\n *\n * Then `npx deepspace deploy --env staging` reads the override and ships to\n * `hopkins-staging.app.space`. The platform treats it as a fully separate\n * app — its own DO state, OAuth tokens, Stripe products, cron schedule.\n * No platform-side change needed beyond letting the CLI register a second\n * app under the same owner.\n *\n * DeepSpace keeps generated dev/test/deploy credentials in one `.dev.vars`\n * file. For `--env`, it flattens the selected Wrangler env into a generated\n * config for the Cloudflare Vite plugin instead of passing `CLOUDFLARE_ENV`;\n * otherwise Wrangler would look for a separate `.dev.vars.<env>` file.\n *\n * This module is also the ONE reader of `wrangler.toml` — `cli/lib/app-identity`\n * (ids) and `deepspace/build` (the browser's id define) both come through\n * {@link readWranglerConfigFile}, so there is one parse, one error shape, and\n * one place for a whole-file guard. It stays free of CLI-only imports for that\n * reason: the `deepspace/build` bundle pulls this in, and must not drag citty\n * or the prompt stack along with it.\n */\n\nimport { existsSync, readFileSync, rmSync, writeFileSync } from 'node:fs'\nimport { join } from 'node:path'\nimport { parse as parseToml, stringify as stringifyToml } from 'smol-toml'\nimport { resolveAppName } from '../../server/rooms/app-name'\nimport { APP_ID_RE } from '../../server/utils/registry-client'\n\n/** Shape of the bits of wrangler.toml that DeepSpace CLI commands read.\n * Add an optional field here whenever a new command starts consulting\n * the config — avoids one-off `WranglerConfig & { … }` casts at\n * call sites and keeps the canonical type in one place. */\nexport interface WranglerConfig {\n name?: string\n env?: Record<\n string,\n ({ name?: string; vars?: Record<string, unknown> } & Record<string, unknown>) | undefined\n >\n /** `[assets]` block (`directory`, `binding`, `not_found_handling`,\n * `run_worker_first`). Consumed by deploy.ts to merge extra\n * worker-first routes into the deployed metadata. */\n assets?: {\n directory?: string\n binding?: string\n not_found_handling?: string\n run_worker_first?: unknown\n }\n /** `[vars]` block — string-valued environment variables baked into\n * the worker at deploy time. */\n vars?: Record<string, unknown>\n}\n\nexport interface PreparedWranglerEnvConfig {\n configPath?: string\n cleanup(): void\n}\n\ntype TomlRecord = Record<string, unknown>\n\nconst NON_INHERITABLE_ENV_KEYS = new Set([\n 'define',\n 'vars',\n 'durable_objects',\n 'kv_namespaces',\n 'r2_buckets',\n 'ai_search_namespaces',\n 'ai_search',\n 'ratelimits',\n 'vectorize',\n 'services',\n 'queues',\n 'workflows',\n 'tail_consumers',\n 'secrets',\n 'secrets_store_secrets',\n])\n\nlet generatedConfigSeq = 0\n\n/**\n * Resolve the appName that this CLI invocation should target.\n *\n * Without `envName`: returns the top-level `name`.\n * With `envName`: returns `[env.<envName>].name` and requires that the\n * env block exists and declares a name. We refuse to\n * fall back to the top-level name because that would\n * silently deploy staging code to production.\n *\n * Returns a discriminated union so callers can surface a precise error\n * (matches the existing `resolveAppName` contract). A failure may carry a\n * `code`: the stable machine contract callers emit as `{ ok: false, code }`.\n * Absent, the failure is about the name itself (`invalid_app_name`) — a\n * missing env block is not, so it names itself.\n */\nexport function resolveAppNameForEnv(\n config: WranglerConfig,\n envName: string | undefined,\n): { ok: true; name: string; warning?: string } | { ok: false; reason: string; code?: string } {\n if (!envName) {\n return resolveAppName(config.name)\n }\n const envBlock = config.env?.[envName]\n if (!envBlock) {\n return {\n ok: false,\n code: 'missing_env_block',\n reason:\n `wrangler.toml: no [env.${envName}] block found. ` +\n `Add one with a distinct \\`name\\` to deploy this environment.`,\n }\n }\n if (typeof envBlock.name !== 'string' || envBlock.name.trim() === '') {\n return {\n ok: false,\n reason:\n `wrangler.toml: [env.${envName}].name is missing. ` +\n `Set a distinct app name (e.g. \"${typeof config.name === 'string' ? config.name : 'myapp'}-${envName}\") so this environment doesn't overwrite the default.`,\n }\n }\n if (envBlock.name === config.name) {\n return {\n ok: false,\n reason:\n `wrangler.toml: [env.${envName}].name \"${envBlock.name}\" is the same as the top-level name. ` +\n `Pick a distinct name (e.g. \"${config.name}-${envName}\") so deploys don't collide.`,\n }\n }\n return resolveAppName(envBlock.name)\n}\n\n/** DeepSpace owns one generated local env file for every Wrangler environment. */\nexport function devVarsPathFor(appDir: string): string {\n return join(appDir, '.dev.vars')\n}\n\nexport function prepareWranglerEnvConfig(\n appDir: string,\n envName: string | undefined,\n): PreparedWranglerEnvConfig {\n if (!envName) return { cleanup() {} }\n\n const config = readWranglerConfig(appDir)\n const resolved = resolveAppNameForEnv(config, envName)\n if (!resolved.ok) throw new Error(resolved.reason)\n const flattened = flattenWranglerEnvConfig(config as TomlRecord, envName)\n const configPath = join(\n appDir,\n `.wrangler.deepspace.${safeGeneratedConfigSegment(envName)}.${process.pid}.${++generatedConfigSeq}.toml`,\n )\n writeFileSync(\n configPath,\n [\n '# Generated by DeepSpace for this CLI run.',\n '# Do not edit; wrangler.toml remains the source of truth.',\n stringifyToml(flattened),\n ]\n .join('\\n')\n .replace(/\\s*$/u, '\\n'),\n )\n return {\n configPath,\n cleanup() {\n rmSync(configPath, { force: true })\n },\n }\n}\n\nexport function wranglerViteEnv(\n baseEnv: NodeJS.ProcessEnv,\n prepared: PreparedWranglerEnvConfig,\n extraEnv: NodeJS.ProcessEnv = {},\n): NodeJS.ProcessEnv {\n const env = { ...baseEnv, ...extraEnv }\n if (prepared.configPath) {\n delete env.CLOUDFLARE_ENV\n env.CLOUDFLARE_VITE_WRANGLER_CONFIG_PATH = prepared.configPath\n }\n return env\n}\n\nfunction flattenWranglerEnvConfig(config: TomlRecord, envName: string): TomlRecord {\n const envRoot = asTomlRecord(config.env)\n const envBlock = asTomlRecord(envRoot?.[envName])\n if (!envBlock) {\n throw new Error(`wrangler.toml: no [env.${envName}] block found.`)\n }\n\n const flattened: TomlRecord = {}\n for (const [key, value] of Object.entries(config)) {\n if (key === 'env' || NON_INHERITABLE_ENV_KEYS.has(key)) continue\n flattened[key] = value\n }\n for (const [key, value] of Object.entries(envBlock)) {\n flattened[key] = value\n }\n // Durable Object declarations contain class bindings, not shared resource ids.\n // A named worker gets its own namespaces, so preserving the declarations is\n // both safe and required for SDK features to work outside the default env.\n if (!('durable_objects' in envBlock) && config.durable_objects) {\n flattened.durable_objects = config.durable_objects\n }\n const envVars = asTomlRecord(envBlock.vars) ?? {}\n if (!('APP_NAME' in envVars) && typeof envBlock.name === 'string') {\n flattened.vars = { ...envVars, APP_NAME: envBlock.name }\n }\n return flattened\n}\n\nfunction asTomlRecord(value: unknown): TomlRecord | undefined {\n return value && typeof value === 'object' && !Array.isArray(value)\n ? (value as TomlRecord)\n : undefined\n}\n\nfunction safeGeneratedConfigSegment(value: string): string {\n return value.replace(/[^A-Za-z0-9._-]/gu, '_')\n}\n\n/**\n * The one failure shape for reading wrangler.toml — unreadable, malformed, or\n * self-contradictory. Carries the absolute path so the user is not left\n * staring at a raw smol-toml stack trace guessing which config in a multi-app\n * repo is broken, and a machine `code` so `--json` callers keep getting one\n * (`cli-errors.errorCode` recognizes this class by name, since this module\n * must not import the CLI error stack — see the header).\n */\nexport class WranglerConfigError extends Error {\n constructor(\n public readonly path: string,\n message: string,\n public readonly code: string = 'invalid_config',\n public readonly cause?: unknown,\n ) {\n super(message)\n this.name = 'WranglerConfigError'\n }\n}\n\nfunction causeDetail(err: unknown): string {\n return err instanceof Error ? err.message : String(err)\n}\n\n/**\n * Read + parse one wrangler.toml by absolute path. The single reader: every\n * consumer of this file in the SDK (deploy/status/dev, `readAppId`, and the\n * build's `__DEEPSPACE_APP_ID__` define) lands here, so a config that is wrong\n * is refused once, the same way, wherever it is read.\n */\nexport function readWranglerConfigFile(path: string): WranglerConfig {\n let raw: string\n try {\n raw = readFileSync(path, 'utf-8')\n } catch (err) {\n const missing = (err as NodeJS.ErrnoException | null)?.code === 'ENOENT'\n throw new WranglerConfigError(\n path,\n missing ? noWranglerConfigMessage(path) : `wrangler.toml: could not read ${path}: ${causeDetail(err)}`,\n missing ? 'not_in_app_repo' : 'invalid_config',\n err,\n )\n }\n let config: WranglerConfig\n try {\n config = parseToml(raw) as WranglerConfig\n } catch (err) {\n throw new WranglerConfigError(\n path,\n `wrangler.toml: malformed TOML at ${path}: ${causeDetail(err)}`,\n 'invalid_config',\n err,\n )\n }\n assertOneSectionPerAppId(config, path)\n return config\n}\n\n/** Read + parse `<appDir>/wrangler.toml`. Caller may check existence first via\n * `hasWranglerConfig` to distinguish \"no app here\" from \"broken app here\". */\nexport function readWranglerConfig(appDir: string): WranglerConfig {\n return readWranglerConfigFile(join(appDir, 'wrangler.toml'))\n}\n\n/**\n * The raw `DEEPSPACE_APP_ID` of one section — `[env.<envName>.vars]` when an\n * env is named, `[vars]` otherwise. Env blocks do NOT inherit the top-level\n * id: each environment is its own app, and silently inheriting is what once\n * shipped a staging worker beside a production browser.\n */\nexport function readAppIdVar(config: WranglerConfig, envName?: string): unknown {\n return (envName ? config.env?.[envName]?.vars : config.vars)?.DEEPSPACE_APP_ID\n}\n\n/**\n * Refuse a config that gives one app id to more than one section.\n *\n * Copying the `[vars]` block into `[env.staging.vars]` leaves both halves of a\n * build agreeing on the same app while the deploy targets another — the client\n * id-mismatch guard cannot fire, because there is no mismatch to see. Only\n * real (`app_…`-shaped) ids count, so a fresh scaffold's repeated `__APP_ID__`\n * placeholder is still initializable.\n */\nfunction assertOneSectionPerAppId(config: WranglerConfig, path: string): void {\n const sections = new Map<string, string[]>()\n const record = (section: string, value: unknown) => {\n if (typeof value !== 'string' || !APP_ID_RE.test(value)) return\n sections.set(value, [...(sections.get(value) ?? []), section])\n }\n record('[vars]', config.vars?.DEEPSPACE_APP_ID)\n for (const envName of Object.keys(config.env ?? {})) {\n record(`[env.${envName}.vars]`, config.env?.[envName]?.vars?.DEEPSPACE_APP_ID)\n }\n for (const [appId, where] of sections) {\n if (where.length < 2) continue\n throw new WranglerConfigError(\n path,\n `wrangler.toml: ${where.join(' and ')} ${where.length === 2 ? 'both' : 'all'} set DEEPSPACE_APP_ID = \"${appId}\" in ${path}. ` +\n `One id under several sections points every environment's data, secrets, and billing at the ` +\n `same app while the deploys go to different places — each environment is its own app — ` +\n `run \\`deepspace app init --env <name>\\` to mint one for it.`,\n 'duplicate_app_id',\n )\n }\n}\n\n/**\n * The one \"not in an app directory\" sentence — the reader's own, and what\n * every command that pre-checks for wrangler.toml says (`not_in_app_repo`),\n * so an agent meets one text with one remedy wherever it hits this.\n */\nexport function noWranglerConfigMessage(where: string): string {\n return `No wrangler.toml at ${where}. Are you in a DeepSpace app directory? (Scaffold one with \\`npm create deepspace\\`, or cd into an existing app.)`\n}\n\n/** Convenience: true when wrangler.toml exists at the standard path. */\nexport function hasWranglerConfig(appDir: string): boolean {\n return existsSync(join(appDir, 'wrangler.toml'))\n}\n","/**\n * `deepspaceBuild()` — the one Vite plugin an app's build config needs.\n *\n * It carries the build-time wiring that is identical in every DeepSpace app\n * and belongs to the SDK's contract rather than to app code:\n *\n * 1. the app id `define` (see ./app-id) — so the browser is keyed to the\n * environment THIS build targets, never a literal frozen at scaffold time;\n * 2. removing the preview `.dev.vars` the Cloudflare plugin drops beside the\n * built worker — DeepSpace has one local runtime, so that second plaintext\n * copy is not kept (`removeBuildDevVars` below; `deepspace deploy` calls\n * the same function on the artifact it ships);\n * 3. the client `dedupe` hint — two copies of React (or better-auth) break\n * the SDK's hooks, and which packages must be single-instance is the SDK's\n * knowledge, not the app's.\n *\n * Housing these here means a future fix to any of them ships in an SDK version\n * bump, with no edit to — and no migration of — a single app.\n *\n * The return is a structural Vite plugin: typing it against `vite` would make\n * the SDK carry a Vite dependency it otherwise does not need, and Vite's\n * plugin hooks are optional and method-shaped, so a compatible object is\n * assignable to `PluginOption` where an app composes it.\n */\n\nimport { existsSync, lstatSync, readdirSync, unlinkSync } from 'node:fs'\nimport { join, resolve } from 'node:path'\nimport { appIdDefine } from './app-id'\nimport type { ResolveAppIdOptions } from './app-id'\n\n/**\n * Delete Cloudflare's preview-only `.dev.vars` copy from ONE built worker dir.\n * Returns whether a file was removed; throws rather than following a symlink\n * or unlinking a directory, so a booby-trapped build output cannot make this\n * delete something else.\n *\n * TWO call sites, deliberately, and this is the single implementation they\n * share (they used to have one each, with only the CLI's carrying the symlink\n * guard):\n *\n * 1. `closeBundle` below — every `vite build` of an app that uses\n * `deepspaceBuild()`, including plain `npm run build` in CI, so a generic\n * archive step cannot pick the plaintext file up.\n * 2. `cli/commands/deploy/build.ts` — the AUTHORITATIVE one: it runs against\n * the exact worker dir the deploy is about to read, covers apps that do\n * not use this plugin, and fails the deploy (`build_output_unsafe`) when\n * the path is unsafe.\n *\n * Neither subsumes the other — they have different lifetimes (any build vs.\n * the artifact that ships). See `docs/migrations/build-preview-secrets.md`.\n */\nexport function removeBuildDevVars(workerDir: string): boolean {\n const path = join(workerDir, '.dev.vars')\n if (!existsSync(path)) return false\n // lstat, not stat: a symlink reports as a link, not as its target's file.\n if (!lstatSync(path).isFile()) {\n throw new Error(`Refusing unsafe build secret path: ${path}`)\n }\n unlinkSync(path)\n return true\n}\n\n/**\n * Packages that must resolve to a single instance in the client graph. Two\n * Reacts throw \"Cannot read properties of null (reading 'useState')\"; two\n * better-auth clients split the session. The SDK owns this list because it is\n * the SDK's hooks and auth client that break when it is wrong.\n */\nexport const CLIENT_DEDUPE: readonly string[] = ['react', 'react-dom', 'better-auth']\n\n/** The subset of Vite's `Plugin` this helper implements — declared locally so\n * `deepspace/build` needs no `vite` dependency. Assignable to `PluginOption`. */\ninterface VitePluginLike {\n name: string\n enforce?: 'pre' | 'post'\n config(): { define: Record<string, string>; resolve: { dedupe: string[] } }\n closeBundle(): void\n}\n\nexport type DeepspaceBuildOptions = ResolveAppIdOptions\n\nexport function deepspaceBuild(options: DeepspaceBuildOptions): VitePluginLike {\n const distDir = resolve(options.appDir, 'dist')\n return {\n name: 'deepspace-build',\n enforce: 'post',\n config() {\n return {\n define: appIdDefine(options),\n resolve: { dedupe: [...CLIENT_DEDUPE] },\n }\n },\n closeBundle() {\n // Cloudflare emits a preview-only `.dev.vars` beside each built worker.\n // Sweep every worker dir before reporting an unsafe one: an early throw\n // would leave the plaintext copies after it in place (fail-open).\n if (!existsSync(distDir)) return\n const unsafe: string[] = []\n for (const entry of readdirSync(distDir, { withFileTypes: true })) {\n if (!entry.isDirectory()) continue\n try {\n removeBuildDevVars(join(distDir, entry.name))\n } catch (err) {\n unsafe.push(err instanceof Error ? err.message : String(err))\n }\n }\n if (unsafe.length > 0) throw new Error(unsafe.join('\\n'))\n },\n }\n}\n"],"mappings":"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","names":["existsSync","join","resolve","join","existsSync","resolve"]}
|
|
1
|
+
{"version":3,"sources":["../src/build/app-id.ts","../src/server/utils/registry-client.ts","../src/cli/lib/wrangler-env.ts","../src/build/plugin.ts"],"sourcesContent":["/**\n * `deepspace/build` — build-time helpers an app's Vite config imports.\n *\n * Node-only. Nothing here may be reached from client or worker code; it runs\n * while `vite build` / `vite dev` is assembling the bundle, never inside it.\n *\n * The one job today is resolving the app's immutable id for the browser.\n * `wrangler.toml` is the single source of truth for app identity — the worker\n * reads `DEEPSPACE_APP_ID` off `env`, and every durable thing (data scope\n * `app:{appId}`, secrets, billing) keys to it. The browser has no `env`, so\n * the build stamps the same value in as `__DEEPSPACE_APP_ID__`, read from the\n * config THIS build is running against. A literal baked into the client once,\n * at scaffold time, is what let `deepspace deploy --env staging` ship a worker\n * writing staging's rooms beside a browser reading production's, with nothing\n * reporting the split.\n *\n * Living in the SDK rather than in a file copied into every app means one\n * definition, no per-app duplicate, and `smol-toml` stays the SDK's dependency\n * instead of every app's.\n */\n\nimport { resolve } from 'node:path'\n\n/** `app_` + 26-char Crockford ULID. */\n// Single source: the shared registry client owns the id shape.\nimport { APP_ID_RE } from '../server/utils/registry-client'\n// Single source: `cli/lib/wrangler-env` is the SDK's only wrangler.toml reader,\n// so the build sees the same parse, the same failure shape, and the same\n// whole-file guards the CLI does. Both are Node-only. Imported as that one\n// module, never through the CLI barrel — nothing else of the CLI belongs in an\n// app's build graph.\nimport { readAppIdVar, readWranglerConfigFile } from '../cli/lib/wrangler-env'\n\n/**\n * The build's environment, typed structurally. This module is imported from an\n * app's `vite.config.ts`, which sits outside the app's tsconfig `include` and\n * runs where the app carries no `@types/node`, so nothing here leans on the\n * ambient `NodeJS` namespace.\n */\nexport type BuildEnv = Record<string, string | undefined>\n\nexport interface ResolveAppIdOptions {\n /**\n * The app root — the directory holding `wrangler.toml`. The caller passes\n * its own location (`fileURLToPath(new URL('.', import.meta.url))` from\n * `vite.config.ts`), so the config is found by an authoritative anchor\n * rather than a guessed `process.cwd()`.\n */\n appDir: string\n /** Defaults to `process.env`; injectable for tests. */\n env?: BuildEnv\n}\n\n/**\n * Resolve the app id the browser bundle must carry.\n *\n * Mirrors the Cloudflare Vite plugin so dev, a bare `vite build`, and\n * `deepspace deploy --env <name>` all agree:\n *\n * config ← CLOUDFLARE_VITE_WRANGLER_CONFIG_PATH, else <appDir>/wrangler.toml\n * section ← [env.<CLOUDFLARE_ENV>.vars] when that is set, else [vars]\n *\n * Env blocks do NOT inherit `[vars]` — each environment is its own app — so a\n * missing or malformed id throws and fails the build. It never falls back to\n * another environment's id: a fallback that silently picks the wrong one is\n * exactly the bug this exists to remove.\n */\nexport function resolveAppId({ appDir, env = process.env }: ResolveAppIdOptions): string {\n const configPath = env.CLOUDFLARE_VITE_WRANGLER_CONFIG_PATH\n ? resolve(env.CLOUDFLARE_VITE_WRANGLER_CONFIG_PATH)\n : resolve(appDir, 'wrangler.toml')\n const envName = env.CLOUDFLARE_ENV\n const section = envName ? `[env.${envName}.vars]` : '[vars]'\n\n const appId = readAppIdVar(readWranglerConfigFile(configPath), envName)\n if (typeof appId !== 'string' || !APP_ID_RE.test(appId)) {\n // A fresh-but-uninitialized scaffold (signed out at scaffold time) still\n // carries the __APP_ID__ placeholder — the remedy is registration, not\n // hand-writing an id (ids are server-minted at `deepspace app init`).\n const uninitialized = appId === undefined || appId === '__APP_ID__'\n throw new Error(\n `${configPath}: ${section} has no valid DEEPSPACE_APP_ID ` +\n `(found ${appId === undefined ? 'nothing' : JSON.stringify(appId)}). ` +\n `The browser keys its RecordRooms to this id, so the build stops rather than ship a wrong or empty one. ` +\n (uninitialized\n ? `Run \\`npx deepspace app init${envName ? ` --env ${envName}` : ''}\\` to register this app — ids are server-minted; never hand-write one.`\n : `Add the id your \\`deepspace app init\\` registered under ${section}.`) +\n (envName ? ` Environments do not inherit the top-level id; each one is its own app.` : ``),\n )\n }\n return appId\n}\n\n/**\n * Vite `define` entry consumed by an app's `vite.config.ts` and\n * `vitest.config.ts`, so unit tests see the same id the bundle is built with:\n *\n * ```ts\n * import { fileURLToPath } from 'node:url'\n * import { appIdDefine } from 'deepspace/build'\n * export default defineConfig({\n * define: appIdDefine({ appDir: fileURLToPath(new URL('.', import.meta.url)) }),\n * })\n * ```\n */\nexport function appIdDefine(options: ResolveAppIdOptions): Record<string, string> {\n return { __DEEPSPACE_APP_ID__: JSON.stringify(resolveAppId(options)) }\n}\n\n/**\n * How an app scaffolded before the build-injected id adopts it: one edit per\n * file, stated ONCE so the deploy refusals (`app_id_env_mismatch`,\n * `app_id_define_unsubstituted`) and `app update` guidance cannot disagree\n * about what it is. Keyed by the file each manual edit lands in.\n */\nexport const APP_ID_ADOPTION_EDITS = {\n 'vite.config.ts':\n `import { deepspaceBuild } from 'deepspace/build' and add\\n` +\n ` deepspaceBuild({ appDir: fileURLToPath(new URL('.', import.meta.url)) }) to plugins`,\n 'vitest.config.ts': `import { appIdDefine } and set define: appIdDefine({ appDir })`,\n 'src/constants.ts':\n `declare const __DEEPSPACE_APP_ID__: string; export it as APP_ID\\n` +\n ` (drop the frozen literal)`,\n} as const\n\n/** The three edits are one change; this is the sentence that says so. */\nexport const APP_ID_ADOPTION_TOGETHER =\n `Apply all three together: src/constants.ts on its own builds clean and then throws ` +\n `in the browser. A newly scaffolded app already has this.`\n\n/** The full retrofit as the deploy refusals print it. */\nexport const APP_ID_ADOPTION_STEPS =\n `Point the client at the build-injected id — the SDK's \\`deepspace/build\\` supplies it:\\n` +\n Object.entries(APP_ID_ADOPTION_EDITS)\n .map(([file, edit]) => ` • ${file}: ${edit}\\n`)\n .join('') +\n APP_ID_ADOPTION_TOGETHER\n","/**\n * The ONE registry client. Every platform worker reaches the app-registry\n * Durable Object (hosted by the deploy worker, singleton `v1:global`) through\n * a cross-script REGISTRY binding — and before this module, each of the five\n * workers hand-rolled its own copy of the same mechanics: the instance name\n * appeared in 6 places, the `https://registry/` URL in 6, `APP_ID_RE` in 5,\n * and `RegistryClientError` + `registryErrorJson` existed byte-identical in\n * two workers. Copies that must agree and nothing enforcing it.\n *\n * This module owns the MECHANICS: instance name, stub lookup, the wire call,\n * the error type, and the standard route mapping. Each worker still declares\n * its own typed convenience wrapper listing only the actions it uses — that\n * subset is genuinely per-worker; the plumbing is not.\n */\n\n/** The singleton registry instance — one DO holds every app/route/collaborator. */\nexport const REGISTRY_INSTANCE = 'v1:global'\n\n/** Public app ids have one canonical ULID-minted shape. */\nexport const APP_ID_RE = /^app_[0-9A-HJKMNP-TV-Z]{26}$/\nexport const STRICT_APP_ID_RE = APP_ID_RE\n\n/** Internal physical locators survive identity migration and are not public ids. */\nexport const RESOURCE_ID_RE = /^(app_[0-9A-HJKMNP-TV-Z]{26}|[a-z0-9][a-z0-9_-]{0,63})$/\n\nconst CROCKFORD = '0123456789ABCDEFGHJKMNPQRSTVWXYZ'\n\n/** A 26-char ULID (48-bit ms timestamp + 80 random bits, Crockford base32) —\n * the id shape shared by app ids (`app_…`) and workspace ids (`ws_…`). */\nexport function mintUlid(now = Date.now()): string {\n let ts = ''\n let t = now\n for (let i = 0; i < 10; i++) {\n ts = CROCKFORD[t % 32] + ts\n t = Math.floor(t / 32)\n }\n const rand = new Uint8Array(10)\n crypto.getRandomValues(rand)\n let rs = ''\n let acc = 0\n let bits = 0\n for (const byte of rand) {\n acc = (acc << 8) | byte\n bits += 8\n while (bits >= 5) {\n bits -= 5\n rs += CROCKFORD[(acc >> bits) & 31]\n }\n }\n return `${ts}${rs}`.slice(0, 26)\n}\n\n/** Mint a fresh app id: `app_` + 26-char ULID. App ids are minted ONLY by the\n * deploy worker's authenticated `POST /api/apps/mint`, which registers the id\n * to its caller in the same step — an id with no owner never exists. */\nexport function mintAppId(now = Date.now()): string {\n return `app_${mintUlid(now)}`\n}\n\nexport class RegistryClientError extends Error {\n constructor(\n message: string,\n readonly code: string,\n readonly status: number,\n ) {\n super(message)\n this.name = 'RegistryClientError'\n }\n}\n\nexport function registryStub(ns: DurableObjectNamespace): DurableObjectStub {\n return ns.get(ns.idFromName(REGISTRY_INSTANCE))\n}\n\n/**\n * One registry action over the wire. Non-2xx answers throw\n * {@link RegistryClientError} carrying the DO's structured `code` (or\n * `'error'` for a transport-shaped body) — callers distinguish a registry-\n * REPORTED miss (`code === 'not_found'`) from an outage by that code, never\n * by treating every throw as \"not found\".\n */\nexport async function callRegistry<T>(\n stub: DurableObjectStub,\n body: { action: string } & Record<string, unknown>,\n): Promise<T> {\n const res = await stub.fetch('https://registry/', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(body),\n })\n const json = (await res.json().catch(() => ({}))) as { error?: string; code?: string }\n if (!res.ok) {\n throw new RegistryClientError(\n json.error ?? `Registry error (${res.status})`,\n json.code ?? 'error',\n res.status,\n )\n }\n return json as T\n}\n\n/** Standard mapping of a registry failure onto a route response. */\nexport function registryErrorJson(\n c: { json: (body: unknown, status?: number) => Response },\n err: unknown,\n): Response {\n if (err instanceof RegistryClientError) {\n return c.json({ error: err.message, code: err.code }, err.status as 400)\n }\n throw err\n}\n","/**\n * Wrangler env resolution for DeepSpace's multi-environment deploys.\n *\n * Lets a single repo deploy to `<app>.app.space` and `<app>-staging.app.space`\n * (or any other named environment) from one `wrangler.toml`:\n *\n * name = \"hopkins\"\n * [vars]\n * APP_NAME = \"hopkins\"\n *\n * [env.staging]\n * name = \"hopkins-staging\"\n * [env.staging.vars]\n * APP_NAME = \"hopkins-staging\"\n *\n * Then `npx deepspace deploy --env staging` reads the override and ships to\n * `hopkins-staging.app.space`. The platform treats it as a fully separate\n * app — its own DO state, OAuth tokens, Stripe products, cron schedule.\n * No platform-side change needed beyond letting the CLI register a second\n * app under the same owner.\n *\n * DeepSpace keeps generated dev/test/deploy credentials in one `.dev.vars`\n * file. For `--env`, it flattens the selected Wrangler env into a generated\n * config for the Cloudflare Vite plugin instead of passing `CLOUDFLARE_ENV`;\n * otherwise Wrangler would look for a separate `.dev.vars.<env>` file.\n *\n * This module is also the ONE reader of `wrangler.toml` — `cli/lib/app-identity`\n * (ids) and `deepspace/build` (the browser's id define) both come through\n * {@link readWranglerConfigFile}, so there is one parse, one error shape, and\n * one place for a whole-file guard. It stays free of CLI-only imports for that\n * reason: the `deepspace/build` bundle pulls this in, and must not drag citty\n * or the prompt stack along with it.\n */\n\nimport { existsSync, readFileSync, rmSync, writeFileSync } from 'node:fs'\nimport { join } from 'node:path'\nimport { parse as parseToml, stringify as stringifyToml } from 'smol-toml'\nimport { resolveAppName } from '../../server/rooms/app-name'\nimport { APP_ID_RE } from '../../server/utils/registry-client'\n\n/** Shape of the bits of wrangler.toml that DeepSpace CLI commands read.\n * Add an optional field here whenever a new command starts consulting\n * the config — avoids one-off `WranglerConfig & { … }` casts at\n * call sites and keeps the canonical type in one place. */\nexport interface WranglerConfig {\n name?: string\n env?: Record<\n string,\n ({ name?: string; vars?: Record<string, unknown> } & Record<string, unknown>) | undefined\n >\n /** `[assets]` block (`directory`, `binding`, `not_found_handling`,\n * `run_worker_first`). Consumed by deploy.ts to merge extra\n * worker-first routes into the deployed metadata. */\n assets?: {\n directory?: string\n binding?: string\n not_found_handling?: string\n run_worker_first?: unknown\n }\n /** `[vars]` block — string-valued environment variables baked into\n * the worker at deploy time. */\n vars?: Record<string, unknown>\n}\n\nexport interface PreparedWranglerEnvConfig {\n configPath?: string\n cleanup(): void\n}\n\ntype TomlRecord = Record<string, unknown>\n\nconst NON_INHERITABLE_ENV_KEYS = new Set([\n 'define',\n 'vars',\n 'durable_objects',\n 'kv_namespaces',\n 'r2_buckets',\n 'ai_search_namespaces',\n 'ai_search',\n 'ratelimits',\n 'vectorize',\n 'services',\n 'queues',\n 'workflows',\n 'tail_consumers',\n 'secrets',\n 'secrets_store_secrets',\n])\n\nlet generatedConfigSeq = 0\n\n/**\n * Resolve the appName that this CLI invocation should target.\n *\n * Without `envName`: returns the top-level `name`.\n * With `envName`: returns `[env.<envName>].name` and requires that the\n * env block exists and declares a name. We refuse to\n * fall back to the top-level name because that would\n * silently deploy staging code to production.\n *\n * Returns a discriminated union so callers can surface a precise error\n * (matches the existing `resolveAppName` contract). A failure may carry a\n * `code`: the stable machine contract callers emit as `{ ok: false, code }`.\n * Absent, the failure is about the name itself (`invalid_app_name`) — a\n * missing env block is not, so it names itself.\n */\nexport function resolveAppNameForEnv(\n config: WranglerConfig,\n envName: string | undefined,\n): { ok: true; name: string; warning?: string } | { ok: false; reason: string; code?: string } {\n if (!envName) {\n return resolveAppName(config.name)\n }\n const envBlock = config.env?.[envName]\n if (!envBlock) {\n return {\n ok: false,\n code: 'missing_env_block',\n reason:\n `wrangler.toml: no [env.${envName}] block found. ` +\n `Add one with a distinct \\`name\\` to deploy this environment.`,\n }\n }\n if (typeof envBlock.name !== 'string' || envBlock.name.trim() === '') {\n return {\n ok: false,\n reason:\n `wrangler.toml: [env.${envName}].name is missing. ` +\n `Set a distinct app name (e.g. \"${typeof config.name === 'string' ? config.name : 'myapp'}-${envName}\") so this environment doesn't overwrite the default.`,\n }\n }\n if (envBlock.name === config.name) {\n return {\n ok: false,\n reason:\n `wrangler.toml: [env.${envName}].name \"${envBlock.name}\" is the same as the top-level name. ` +\n `Pick a distinct name (e.g. \"${config.name}-${envName}\") so deploys don't collide.`,\n }\n }\n return resolveAppName(envBlock.name)\n}\n\n/** DeepSpace owns one generated local env file for every Wrangler environment. */\nexport function devVarsPathFor(appDir: string): string {\n return join(appDir, '.dev.vars')\n}\n\nexport function prepareWranglerEnvConfig(\n appDir: string,\n envName: string | undefined,\n): PreparedWranglerEnvConfig {\n if (!envName) return { cleanup() {} }\n\n const config = readWranglerConfig(appDir)\n const resolved = resolveAppNameForEnv(config, envName)\n if (!resolved.ok) throw new Error(resolved.reason)\n const flattened = flattenWranglerEnvConfig(config as TomlRecord, envName)\n const configPath = join(\n appDir,\n `.wrangler.deepspace.${safeGeneratedConfigSegment(envName)}.${process.pid}.${++generatedConfigSeq}.toml`,\n )\n writeFileSync(\n configPath,\n [\n '# Generated by DeepSpace for this CLI run.',\n '# Do not edit; wrangler.toml remains the source of truth.',\n stringifyToml(flattened),\n ]\n .join('\\n')\n .replace(/\\s*$/u, '\\n'),\n )\n return {\n configPath,\n cleanup() {\n rmSync(configPath, { force: true })\n },\n }\n}\n\nexport function wranglerViteEnv(\n baseEnv: Record<string, string | undefined>,\n prepared: PreparedWranglerEnvConfig,\n extraEnv: Record<string, string | undefined> = {},\n): Record<string, string | undefined> {\n const env = { ...baseEnv, ...extraEnv }\n if (prepared.configPath) {\n delete env.CLOUDFLARE_ENV\n env.CLOUDFLARE_VITE_WRANGLER_CONFIG_PATH = prepared.configPath\n }\n return env\n}\n\nfunction flattenWranglerEnvConfig(config: TomlRecord, envName: string): TomlRecord {\n const envRoot = asTomlRecord(config.env)\n const envBlock = asTomlRecord(envRoot?.[envName])\n if (!envBlock) {\n throw new Error(`wrangler.toml: no [env.${envName}] block found.`)\n }\n\n const flattened: TomlRecord = {}\n for (const [key, value] of Object.entries(config)) {\n if (key === 'env' || NON_INHERITABLE_ENV_KEYS.has(key)) continue\n flattened[key] = value\n }\n for (const [key, value] of Object.entries(envBlock)) {\n flattened[key] = value\n }\n // Durable Object declarations contain class bindings, not shared resource ids.\n // A named worker gets its own namespaces, so preserving the declarations is\n // both safe and required for SDK features to work outside the default env.\n if (!('durable_objects' in envBlock) && config.durable_objects) {\n flattened.durable_objects = config.durable_objects\n }\n const envVars = asTomlRecord(envBlock.vars) ?? {}\n if (!('APP_NAME' in envVars) && typeof envBlock.name === 'string') {\n flattened.vars = { ...envVars, APP_NAME: envBlock.name }\n }\n return flattened\n}\n\nfunction asTomlRecord(value: unknown): TomlRecord | undefined {\n return value && typeof value === 'object' && !Array.isArray(value)\n ? (value as TomlRecord)\n : undefined\n}\n\nfunction safeGeneratedConfigSegment(value: string): string {\n return value.replace(/[^A-Za-z0-9._-]/gu, '_')\n}\n\n/**\n * The one failure shape for reading wrangler.toml — unreadable, malformed, or\n * self-contradictory. Carries the absolute path so the user is not left\n * staring at a raw smol-toml stack trace guessing which config in a multi-app\n * repo is broken, and a machine `code` so `--json` callers keep getting one\n * (`cli-errors.errorCode` recognizes this class by name, since this module\n * must not import the CLI error stack — see the header).\n */\nexport class WranglerConfigError extends Error {\n constructor(\n public readonly path: string,\n message: string,\n public readonly code: string = 'invalid_config',\n public readonly cause?: unknown,\n ) {\n super(message)\n this.name = 'WranglerConfigError'\n }\n}\n\nfunction causeDetail(err: unknown): string {\n return err instanceof Error ? err.message : String(err)\n}\n\n/**\n * Read + parse one wrangler.toml by absolute path. The single reader: every\n * consumer of this file in the SDK (deploy/status/dev, `readAppId`, and the\n * build's `__DEEPSPACE_APP_ID__` define) lands here, so a config that is wrong\n * is refused once, the same way, wherever it is read.\n */\nexport function readWranglerConfigFile(path: string): WranglerConfig {\n let raw: string\n try {\n raw = readFileSync(path, 'utf-8')\n } catch (err) {\n const missing = (err as NodeJS.ErrnoException | null)?.code === 'ENOENT'\n throw new WranglerConfigError(\n path,\n missing ? noWranglerConfigMessage(path) : `wrangler.toml: could not read ${path}: ${causeDetail(err)}`,\n missing ? 'not_in_app_repo' : 'invalid_config',\n err,\n )\n }\n let config: WranglerConfig\n try {\n config = parseToml(raw) as WranglerConfig\n } catch (err) {\n throw new WranglerConfigError(\n path,\n `wrangler.toml: malformed TOML at ${path}: ${causeDetail(err)}`,\n 'invalid_config',\n err,\n )\n }\n assertOneSectionPerAppId(config, path)\n return config\n}\n\n/** Read + parse `<appDir>/wrangler.toml`. Caller may check existence first via\n * `hasWranglerConfig` to distinguish \"no app here\" from \"broken app here\". */\nexport function readWranglerConfig(appDir: string): WranglerConfig {\n return readWranglerConfigFile(join(appDir, 'wrangler.toml'))\n}\n\n/**\n * The raw `DEEPSPACE_APP_ID` of one section — `[env.<envName>.vars]` when an\n * env is named, `[vars]` otherwise. Env blocks do NOT inherit the top-level\n * id: each environment is its own app, and silently inheriting is what once\n * shipped a staging worker beside a production browser.\n */\nexport function readAppIdVar(config: WranglerConfig, envName?: string): unknown {\n return (envName ? config.env?.[envName]?.vars : config.vars)?.DEEPSPACE_APP_ID\n}\n\n/**\n * Refuse a config that gives one app id to more than one section.\n *\n * Copying the `[vars]` block into `[env.staging.vars]` leaves both halves of a\n * build agreeing on the same app while the deploy targets another — the client\n * id-mismatch guard cannot fire, because there is no mismatch to see. Only\n * real (`app_…`-shaped) ids count, so a fresh scaffold's repeated `__APP_ID__`\n * placeholder is still initializable.\n */\nfunction assertOneSectionPerAppId(config: WranglerConfig, path: string): void {\n const sections = new Map<string, string[]>()\n const record = (section: string, value: unknown) => {\n if (typeof value !== 'string' || !APP_ID_RE.test(value)) return\n sections.set(value, [...(sections.get(value) ?? []), section])\n }\n record('[vars]', config.vars?.DEEPSPACE_APP_ID)\n for (const envName of Object.keys(config.env ?? {})) {\n record(`[env.${envName}.vars]`, config.env?.[envName]?.vars?.DEEPSPACE_APP_ID)\n }\n for (const [appId, where] of sections) {\n if (where.length < 2) continue\n throw new WranglerConfigError(\n path,\n `wrangler.toml: ${where.join(' and ')} ${where.length === 2 ? 'both' : 'all'} set DEEPSPACE_APP_ID = \"${appId}\" in ${path}. ` +\n `One id under several sections points every environment's data, secrets, and billing at the ` +\n `same app while the deploys go to different places — each environment is its own app — ` +\n `run \\`deepspace app init --env <name>\\` to mint one for it.`,\n 'duplicate_app_id',\n )\n }\n}\n\n/**\n * The one \"not in an app directory\" sentence — the reader's own, and what\n * every command that pre-checks for wrangler.toml says (`not_in_app_repo`),\n * so an agent meets one text with one remedy wherever it hits this.\n */\nexport function noWranglerConfigMessage(where: string): string {\n return `No wrangler.toml at ${where}. Are you in a DeepSpace app directory? (Scaffold one with \\`npm create deepspace\\`, or cd into an existing app.)`\n}\n\n/** Convenience: true when wrangler.toml exists at the standard path. */\nexport function hasWranglerConfig(appDir: string): boolean {\n return existsSync(join(appDir, 'wrangler.toml'))\n}\n","/**\n * `deepspaceBuild()` — the one Vite plugin an app's build config needs.\n *\n * It carries the build-time wiring that is identical in every DeepSpace app\n * and belongs to the SDK's contract rather than to app code:\n *\n * 1. the app id `define` (see ./app-id) — so the browser is keyed to the\n * environment THIS build targets, never a literal frozen at scaffold time;\n * 2. removing the preview `.dev.vars` the Cloudflare plugin drops beside the\n * built worker — DeepSpace has one local runtime, so that second plaintext\n * copy is not kept (`removeBuildDevVars` below; `deepspace deploy` calls\n * the same function on the artifact it ships);\n * 3. the client `dedupe` hint — two copies of React (or better-auth) break\n * the SDK's hooks, and which packages must be single-instance is the SDK's\n * knowledge, not the app's.\n *\n * Housing these here means a future fix to any of them ships in an SDK version\n * bump, with no edit to — and no migration of — a single app.\n *\n * The return is a structural Vite plugin: typing it against `vite` would make\n * the SDK carry a Vite dependency it otherwise does not need, and Vite's\n * plugin hooks are optional and method-shaped, so a compatible object is\n * assignable to `PluginOption` where an app composes it.\n */\n\nimport { existsSync, lstatSync, readdirSync, unlinkSync } from 'node:fs'\nimport { join, resolve } from 'node:path'\nimport { appIdDefine } from './app-id'\nimport type { ResolveAppIdOptions } from './app-id'\n\n/**\n * Delete Cloudflare's preview-only `.dev.vars` copy from ONE built worker dir.\n * Returns whether a file was removed; throws rather than following a symlink\n * or unlinking a directory, so a booby-trapped build output cannot make this\n * delete something else.\n *\n * TWO call sites, deliberately, and this is the single implementation they\n * share (they used to have one each, with only the CLI's carrying the symlink\n * guard):\n *\n * 1. `closeBundle` below — every `vite build` of an app that uses\n * `deepspaceBuild()`, including plain `npm run build` in CI, so a generic\n * archive step cannot pick the plaintext file up.\n * 2. `cli/commands/deploy/build.ts` — the AUTHORITATIVE one: it runs against\n * the exact worker dir the deploy is about to read, covers apps that do\n * not use this plugin, and fails the deploy (`build_output_unsafe`) when\n * the path is unsafe.\n *\n * Neither subsumes the other — they have different lifetimes (any build vs.\n * the artifact that ships). See `docs/migrations/build-preview-secrets.md`.\n */\nexport function removeBuildDevVars(workerDir: string): boolean {\n const path = join(workerDir, '.dev.vars')\n if (!existsSync(path)) return false\n // lstat, not stat: a symlink reports as a link, not as its target's file.\n if (!lstatSync(path).isFile()) {\n throw new Error(`Refusing unsafe build secret path: ${path}`)\n }\n unlinkSync(path)\n return true\n}\n\n/**\n * Packages that must resolve to a single instance in the client graph. Two\n * Reacts throw \"Cannot read properties of null (reading 'useState')\"; two\n * better-auth clients split the session. The SDK owns this list because it is\n * the SDK's hooks and auth client that break when it is wrong.\n */\nexport const CLIENT_DEDUPE: readonly string[] = ['react', 'react-dom', 'better-auth']\n\n/** The subset of Vite's `Plugin` this helper implements — declared locally so\n * `deepspace/build` needs no `vite` dependency. Assignable to `PluginOption`. */\ninterface VitePluginLike {\n name: string\n enforce?: 'pre' | 'post'\n config(): { define: Record<string, string>; resolve: { dedupe: string[] } }\n closeBundle(): void\n}\n\nexport type DeepspaceBuildOptions = ResolveAppIdOptions\n\nexport function deepspaceBuild(options: DeepspaceBuildOptions): VitePluginLike {\n const distDir = resolve(options.appDir, 'dist')\n return {\n name: 'deepspace-build',\n enforce: 'post',\n config() {\n return {\n define: appIdDefine(options),\n resolve: { dedupe: [...CLIENT_DEDUPE] },\n }\n },\n closeBundle() {\n // Cloudflare emits a preview-only `.dev.vars` beside each built worker.\n // Sweep every worker dir before reporting an unsafe one: an early throw\n // would leave the plaintext copies after it in place (fail-open).\n if (!existsSync(distDir)) return\n const unsafe: string[] = []\n for (const entry of readdirSync(distDir, { withFileTypes: true })) {\n if (!entry.isDirectory()) continue\n try {\n removeBuildDevVars(join(distDir, entry.name))\n } catch (err) {\n unsafe.push(err instanceof Error ? err.message : String(err))\n }\n }\n if (unsafe.length > 0) throw new Error(unsafe.join('\\n'))\n },\n }\n}\n"],"mappings":"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","names":["existsSync","join","resolve","join","existsSync","resolve"]}
|