deepspace 0.23.2 → 0.24.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,35 @@
1
1
  # deepspace
2
2
 
3
+ ## 0.24.1
4
+
5
+ ### Patch Changes
6
+
7
+ - Read piped CLI input asynchronously, refresh authentication and retry safe platform operations during long deploys, secrets setup, and log access, allow constrained links more time for replayable asset uploads, and preserve split Worker modules plus app-declared non-secret variables.
8
+ - Report password sign-in throttling consistently in the test helper and CLI.
9
+
10
+ ## 0.24.0
11
+
12
+ ### Minor Changes
13
+
14
+ - Admin direct gifts: admins can gift bonus credits straight to an existing user by email from the dashboard's admin Credits page (renamed from "Credit Requests"). Each gift is recorded as an already-approved `kind='gift'` row in `credit_requests` — it shows tagged in the admin list and as "Gift from the DeepSpace team" in the recipient's Billing history, and the grant shares the same expiring-bonus semantics as approvals and promo redemptions. The credits dialogs were also hardened: API errors always surface curated copy (raw server/transport messages never render), and Esc/backdrop can't dismiss a dialog while its request is in flight.
15
+ - Managed knowledge base: an app can declare one `[[ai_search]]` binding with `instance_name = "auto"` and get an isolated Cloudflare AI Search instance, provisioned on deploy and torn down on undeploy like any other `"auto"` resource. The new `knowledge(env)` helper on `deepspace/worker` and `deepspace/server` exposes `add` / `list` / `remove` / `search`, plus `scoped({ folder })` for a handle whose folder a caller cannot widen — safe to hand to an agent as a tool. Folders are recursive prefix scopes; oversized text files are split at UTF-8-safe boundaries and oversized binaries are refused rather than corrupted.
16
+
17
+ The resolved binding is control-plane metadata only: it is stripped before the Workers-for-Platforms upload, so no Cloudflare token, instance id, or `ai_search` binding ever reaches the customer Worker, and `ai_search_namespaces` is refused outright. Every operation is authenticated per app and charged in the API worker, so calling the route directly is billed exactly like the helper. Search is charged up front, ingestion reserves and settles against the real indexed-token count, and storage is sampled daily — all at the existing 10% platform binding markup (third-party integrations are unchanged at 30%) and surfaced in the dashboard's Binding Usage card. Available on every plan.
18
+
19
+ - Add `--viewport WIDTHxHEIGHT` to `deepspace test screenshot`. The helper normalizes the dimensions for Playwright's installed Chromium browser, enabling reliable mobile captures without selecting a device profile backed by an uninstalled browser.
20
+ - Promo codes: admins (and Beacon, via app identity + owner JWT) can bulk-mint single-use codes that grant expiring bonus credits — idempotent per (campaign, email) when a campaign is given — and users redeem them from a new "Redeem Code" card on the dashboard Billing page. Includes an admin mint dialog on the Credits page and a rate-limited, race-safe redeem endpoint. Each code also carries a clickable claim link (dashboard `/claim/<token>`): signed-out recipients see the offer, sign in, and the credits auto-apply — the grant attaches to the claim, not to signup, so pre-existing accounts and any click/signup order work.
21
+ - Remove the `workspace:default` shared scope — the one platform-wide Durable Object that held "cross-app shared business data" for every app. The `WORKSPACE_SCHEMAS` export (`deepspace/schema`), the `workspace` entry in `GLOBAL_DO_TYPES`, and the platform worker's acceptance of `workspace:default` as a global scope id are gone; the platform now answers `400 Invalid global scopeId` for it. Cross-app data is an app's own `dir:{appId}` room or a `conv:{convId}` room, never a single DO shared by all apps. Team-scoped RBAC (`teamField` / a `team_members` collection) is unchanged and remains available to any app's own rooms.
22
+
23
+ ### Patch Changes
24
+
25
+ - Stop gating source selection and transfers on unrelated local Git state. Initial GitHub claims and direct GitHub repository changes now require only remote reachability, matching deploys that intentionally ship dirty and unpushed bytes; provider transfers still verify the authoritative refs and release lineage before flipping authority.
26
+ - Stop credit-request approvals from resurrecting a user's expired bonus credits: when the existing bonus allocation has already expired, approving a grant now resets the balance to just the newly granted credits instead of adding to the stale amount and refreshing its expiry.
27
+ - `deepspace app update` now reports the secure-room-boundaries blocker wherever the legacy room proxy lives, not only at `src/server/realtime-routes.ts`. An app that had moved its WebSocket proxy and still forwarded identity in the URL passed `app update` clean, then reached the room as anonymous on every connection — RBAC filtered every row and signed-in users saw an empty app. The blocker scan is keyed on content (a file that forwards a request and sets `userId`/`role`… on its URL) instead of on the file path; the auto-rewrite is unchanged. Rooms also log a warning when a connection carries `?userId=` with no `x-user-id` header, so the pre-0.19 proxy shape names itself in `wrangler tail`.
28
+ - Return a stable duplicate result when an update violates a collection's `uniqueOn` constraint.
29
+ - Fixes from the v0.23.2 production AX pass. Server: the `user.list` assistant tool now returns exactly what the socket roster gives the caller (admins full rows through the read policy; everyone else the public identity projection, honouring `read: false` and `roster: 'read-policy'`; app actions keep full rows) — it used to hand every member every user's email; the public identity projection now carries `lastSeenAt`, so `usePresence()` works for non-admins (it was always offline for everyone but the owner); `timestampTrigger` writes canonical epoch seconds on a `storage: 'number'` date/datetime column instead of an ISO string that stored the year or milliseconds inconsistent with explicit writes; an empty string written to a text column is stored as `''` (it was folded into NULL, so the field read back absent and a `default: ''` never materialized); `where` on `records.query`/`deleteWhere` refuses arrays and non-objects at the one shared chokepoint; `deleteWhere` authorizes with delete permission independently of read permission; the client's `writableFields` refusal names the role and field again. CLI: `deploy` states on both surfaces that a rename does not carry the display name (`src/constants.ts` and `wrangler.toml [vars] APP_NAME`) and the `--rename` success envelope carries `renamedFrom`/`staleDisplayName`; `collaborators add` discloses that a collaborator can read and change every secret (`grants` in `--json`); the false "attributed to the app's owner" warning on collaborator deploys is gone (the ledger records the collaborator); `app source github` removes a stale `space` git remote (`spaceRemote` in `--json`), refuses stale import writes after an authority flip, rechecks GitHub after import, and preserves a successful flip when local remote reconciliation fails; `push`/`pull`/`clone` share one GitHub-source refusal with `repository` and `appId` as fields (`apiFetch` now keeps every server-provided field on `ApiError.details`); `push`'s uninitialized-scaffold refusal is `actionRequired` (exit 2) like `deploy`'s; `releases`/`status`/`activity` name a GitHub-source release's source instead of "(no source recorded)"; a GitHub-source deploy reports its branch and whether the tree was dirty (both surfaces); `app update` is now a successful read-only guide tied to the running CLI version: it reads only explicit app manifests and never rewrites source, stamps migrations, runs an installer, scans unrelated packages, imposes DeepSpace Git transport checks, or treats outstanding work as a command failure; local/file/workspace/VCS SDK specs report `dependency_unverified` instead of a false aligned state, and malformed migration ledgers refuse with `invalid_migration_manifest`; `test run --json` routes the Playwright dependency preflight to stderr through the same chokepoint as the suite and says up front when it will run `apt-get`; the `unknown_suite` refusal mentions `--grep` and single-spec paths; `test accounts create` no longer prints the password (it is saved locally; `list --reveal` shows it); `ActionTools` gains `deleteWhere` (template + upgrade guidance). Deploy locking no longer reclaims stale paths automatically, and release callbacks delete only the lock they own. Scaffolder: `create-deepspace@X` pins `deepspace` to exactly `X` (a caret let a pinned scaffolder install the newest SDK) and prints the SDK version it installed.
30
+
31
+ Second round (wave 2 of the pass — recovery, upgrade, observability). CLI: a present-but-malformed `DEEPSPACE_APP_ID` is refused as `invalid_app_id` at the one id resolver (it was reported as "no app id" and the offered `app init` orphaned the app; `app init` now refuses to overwrite it without `--new-id`); the "no app dir" vs "app dir without an id" states get one code each (`not_in_app_repo` / `app_not_initialized`) across `deploy`, `secrets`, `test`, `dev`; `secrets` and `auth login` refusals go through the shared envelope (`not_authenticated` with its action, `network_error` naming the service and env var, `file_not_found`, `invalid_credentials`); `not_authenticated` names the selected plane vs the plane holding a session (`DEEPSPACE_ENV`/`DEEPSPACE_AUTH_URL`) and the headless login form; `push`/`pull`/`deploy` refuse `merge_in_progress` mid-merge instead of pushing the pre-merge commit; `deploy` takes a local `.deepspace/deploy.lock` (`deploy_in_progress` names the other run) so two deploys in one directory no longer race on `dist/`, `release_in_progress` explains itself and carries a retry action, `forbidden` names the signed-in account and the app, and after the edge confirms a release `deploy` wakes the worker once; `undeploy`'s confirmation says what is destroyed (the app's Durable Objects: records, messages, canvas state, cron history — the old sentence said "data stays") and what stays (secrets, registration), names the app, and a second undeploy reports `alreadyUndeployed`; `logs` on a never-deployed app refuses `app_not_deployed`; `app init --json` reports the plane as `env` (the wrangler slot is `wranglerEnv`); `test accounts list --json` prints passwords only with `--reveal`; `status --help` names `env`/`services`; `integrations invoke` refuses a paid call outside a terminal or under `--json` without `--yes` (`cost_confirmation_required`; the interactive prompt defaults to No) and `integrations info` synthesizes an example body from the schema's required keys; `app update` reports guidance for the build-injected app id and action-route bearer guard without mutating the checkout; `app init`'s `app_not_registered` refusal ships the `--new-id` action; `workspace land` pins its follow-up to the surviving primary checkout instead of the managed worktree it just removed. Server: a cron schedule arms on the worker's first request (`armCronRoom`, wired in the template) instead of waiting for its Durable Object to be fetched by a visitor, and rejected or non-2xx wake attempts remain retryable; cron and job runs log one structured line each; the template's `app.onError` logs a thrown error's message (Hono's default rendered only the stack frames, so `deepspace logs` never carried it). Platform (ships with the next worker deploy): a created-never-written secrets config answers `{}` instead of "Project DEK not found"; non-request log invocations (alarms) are labelled with their entrypoint; the integrations catalog discloses the customer price after markup and currency conversion (`null` for metered `per_actual_cost` endpoints, `variesWithInput` where multipliers apply), and `integrations list`/`info`/the consent prompt label an input-dependent figure as a base rate that can move lower or higher instead of falsely calling it a floor. Template: the action route logs `[action] <name> caller=<userId>` (the platform request log carries no user), with upgrade guidance for existing apps.
32
+
3
33
  ## 0.23.2
4
34
 
5
35
  ### Patch Changes
package/README.md CHANGED
@@ -106,8 +106,12 @@ installed binary:
106
106
  npx deepspace@latest app update
107
107
  ```
108
108
 
109
- The target CLI owns the source migrations required by the SDK version it
110
- installs. Review and commit its source changes before deploying.
109
+ The target CLI is the sole version authority. The command is read-only: it
110
+ reports dependency edits, app-owned source migrations, and validation steps,
111
+ but never rewrites or stamps the checkout. Apply the guidance, run the app's
112
+ checks, review the diff, and commit it before deploying. The inspection exits
113
+ successfully even when work remains; `ready` in `--json` reports whether the
114
+ app is already aligned.
111
115
 
112
116
  The hierarchy shown by `deepspace --help` keeps durable app lifecycle under
113
117
  `deepspace app`, while checkout-oriented Git, workspace, release, and deploy
package/dist/build.js CHANGED
@@ -90,6 +90,17 @@ function resolveAppId({ appDir, env = process.env }) {
90
90
  function appIdDefine(options) {
91
91
  return { __DEEPSPACE_APP_ID__: JSON.stringify(resolveAppId(options)) };
92
92
  }
93
+ var APP_ID_ADOPTION_EDITS = {
94
+ "vite.config.ts": `import { deepspaceBuild } from 'deepspace/build' and add
95
+ deepspaceBuild({ appDir: fileURLToPath(new URL('.', import.meta.url)) }) to plugins`,
96
+ "vitest.config.ts": `import { appIdDefine } and set define: appIdDefine({ appDir })`,
97
+ "src/constants.ts": `declare const __DEEPSPACE_APP_ID__: string; export it as APP_ID
98
+ (drop the frozen literal)`
99
+ };
100
+ var APP_ID_ADOPTION_TOGETHER = `Apply all three together: src/constants.ts on its own builds clean and then throws in the browser. A newly scaffolded app already has this.`;
101
+ var APP_ID_ADOPTION_STEPS = `Point the client at the build-injected id \u2014 the SDK's \`deepspace/build\` supplies it:
102
+ ` + Object.entries(APP_ID_ADOPTION_EDITS).map(([file, edit]) => ` \u2022 ${file}: ${edit}
103
+ `).join("") + APP_ID_ADOPTION_TOGETHER;
93
104
 
94
105
  // src/build/plugin.ts
95
106
  import { existsSync as existsSync2, lstatSync, readdirSync, unlinkSync } from "fs";
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 * 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: 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"]}
@@ -1274,7 +1274,7 @@ function renderInlineMarkdown(value) {
1274
1274
  import { useEffect as useEffect6, useRef as useRef5, useState as useState5 } from "react";
1275
1275
 
1276
1276
  // package.json
1277
- var version = "0.23.2";
1277
+ var version = "0.24.1";
1278
1278
 
1279
1279
  // src/documentation/runtime/navigation.tsx
1280
1280
  import { Fragment as Fragment4, jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
@@ -1954,4 +1954,4 @@ export {
1954
1954
  DefaultDocumentation,
1955
1955
  DocumentationContent
1956
1956
  };
1957
- //# sourceMappingURL=chunk-2U2VG6L5.js.map
1957
+ //# sourceMappingURL=chunk-6RQ4GDEH.js.map