deepspace 0.34.0 → 0.35.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/dist/testing.js CHANGED
@@ -5,11 +5,53 @@ import { test as baseTest } from "@playwright/test";
5
5
  import { existsSync, mkdirSync, readFileSync } from "fs";
6
6
  import { homedir } from "os";
7
7
  import { join } from "path";
8
+
9
+ // src/shared/planes.ts
10
+ var PLANES = {
11
+ production: {
12
+ auth: "https://auth.deep.space",
13
+ api: "https://api-worker.deep.space",
14
+ platform: "https://platform-worker.deep.space",
15
+ deploy: "https://deploy-worker.deep.space",
16
+ appDomain: "app.space",
17
+ platformDomain: "deep.space",
18
+ home: "https://deep.space",
19
+ dashboard: "https://dashboard.deep.space",
20
+ privacyPolicy: "https://deep.space/privacy"
21
+ },
22
+ // Services on deepspacesites.com, apps on spacestest.com: production's
23
+ // two-zone split, because a Worker cannot fetch one in its own zone.
24
+ staging: {
25
+ auth: "https://auth.deepspacesites.com",
26
+ api: "https://api.deepspacesites.com",
27
+ platform: "https://platform.deepspacesites.com",
28
+ deploy: "https://deploy.deepspacesites.com",
29
+ appDomain: "spacestest.com",
30
+ platformDomain: "deepspacesites.com",
31
+ home: "https://spacestest.com",
32
+ dashboard: "https://dashboard.deepspacesites.com",
33
+ privacyPolicy: "https://deep.space/privacy"
34
+ },
35
+ // Services on deepspacemedical.com, apps on deepspacemedical.app: two zones
36
+ // in the medical Cloudflare account. The plane has no dashboard.
37
+ medical: {
38
+ auth: "https://auth.deepspacemedical.com",
39
+ api: "https://api.deepspacemedical.com",
40
+ platform: "https://platform.deepspacemedical.com",
41
+ deploy: "https://deploy.deepspacemedical.com",
42
+ appDomain: "deepspacemedical.app",
43
+ platformDomain: "deepspacemedical.com",
44
+ home: "https://deepspacemedical.com",
45
+ dashboard: null,
46
+ privacyPolicy: "https://deepspacemedical.com/privacy"
47
+ }
48
+ };
49
+
50
+ // src/testing/accounts.ts
8
51
  var TEST_ACCOUNTS_PATH = join(homedir(), ".deepspace", "test-accounts.json");
9
- var PRODUCTION_AUTH_ORIGIN = "https://auth.deep.space";
10
- var STAGING_AUTH_ORIGIN = "https://auth.deepspacesites.com";
11
52
  function currentTestAccountScope(env = process.env) {
12
- const selected = env.DEEPSPACE_AUTH_URL ?? (env.DEEPSPACE_ENV === "staging" ? STAGING_AUTH_ORIGIN : PRODUCTION_AUTH_ORIGIN);
53
+ const plane = env.DEEPSPACE_ENV;
54
+ const selected = env.DEEPSPACE_AUTH_URL ?? (plane === "staging" || plane === "medical" ? PLANES[plane].auth : PLANES.production.auth);
13
55
  return normalizeTestAccountScope(selected);
14
56
  }
15
57
  function normalizeTestAccountScope(value) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/testing/fixtures.ts","../src/testing/accounts.ts","../src/testing/storage-state.ts","../src/shared/jwt.ts"],"sourcesContent":["/**\n * Multi-user Playwright fixture for DeepSpace apps.\n *\n * Usage:\n *\n * import { test, expect } from 'deepspace/testing'\n *\n * test('A sends, B sees', async ({ users }) => {\n * const [a, b] = await users(2)\n * await a.page.goto('/chat')\n * await b.page.goto('/chat')\n * await a.page.getByTestId('message-input').fill('hi')\n * await a.page.getByTestId('send-message-btn').click()\n * await expect(b.page.getByText('hi')).toBeVisible()\n * })\n *\n * `users(N)` picks the first N test accounts from\n * `~/.deepspace/test-accounts.json` (created via\n * `deepspace test accounts create`). `users(['Alice', 'Bob'])` picks\n * specific accounts by name.\n *\n * Each user gets a separate browser context with cached storageState\n * (per-account JSON in `~/.deepspace/playwright-states/`). Sign-in\n * happens once per account, not once per test, staying below the auth\n * worker's password sign-in limit.\n *\n * Contexts are auto-closed when the test finishes.\n */\n\nimport { test as baseTest, type Browser, type BrowserContext, type Page } from '@playwright/test'\nimport {\n type TestAccount,\n pickTestAccounts,\n findTestAccountByName,\n} from './accounts'\nimport { ensureStorageState, type EnsureStorageStateOptions } from './storage-state'\n\nexport interface MultiplayerUser {\n context: BrowserContext\n page: Page\n email: string\n name: string\n /** Test account user ID, if known from the accounts registry. */\n userId?: string\n}\n\n/**\n * Fixture function signature. Pass either a count (`users(2)`) or an\n * array of account names (`users(['Alice', 'Bob'])`). Returns one entry\n * per requested user.\n */\nexport type UsersFixture = (\n selector: number | string[],\n options?: {\n label?: string\n /** Sign in again even if the cached session still validates. */\n force?: boolean\n },\n) => Promise<MultiplayerUser[]>\n\ninterface MultiplayerFixtures {\n users: UsersFixture\n}\n\nasync function buildUser(\n browser: Browser,\n account: TestAccount,\n baseURL: string,\n options: EnsureStorageStateOptions = {},\n): Promise<{ user: MultiplayerUser; cleanup: () => Promise<void> }> {\n const statePath = await ensureStorageState(browser, account, baseURL, options)\n const context = await browser.newContext({ storageState: statePath, baseURL })\n const page = await context.newPage()\n return {\n user: {\n context,\n page,\n email: account.email,\n name: account.name ?? account.email.split('@')[0],\n userId: account.userId,\n },\n cleanup: async () => {\n await context.close()\n },\n }\n}\n\nexport const test = baseTest.extend<MultiplayerFixtures>({\n users: async ({ browser, baseURL }, use) => {\n if (!baseURL) {\n throw new Error(\n '`users` fixture requires a baseURL. Set it on your Playwright config ' +\n '(e.g. `use: { baseURL: \"http://localhost:5173\" }`) or via the ' +\n 'webServer.url shortcut.',\n )\n }\n\n const cleanups: Array<() => Promise<void>> = []\n\n const usersFn: UsersFixture = async (selector, options) => {\n const accounts: TestAccount[] = Array.isArray(selector)\n ? selector.map((name) => findTestAccountByName(name))\n : pickTestAccounts(selector, options)\n\n const built = await Promise.all(\n accounts.map((acct) => buildUser(browser, acct, baseURL, { force: options?.force })),\n )\n for (const b of built) cleanups.push(b.cleanup)\n return built.map((b) => b.user)\n }\n\n await use(usersFn)\n\n // Tear down any contexts we created. Errors are swallowed — a test\n // failure shouldn't be masked by a cleanup error.\n for (const cleanup of cleanups) {\n await cleanup().catch(() => {})\n }\n },\n})\n\nexport { expect } from '@playwright/test'\n","/**\n * Account-credential plumbing for the testing module.\n *\n * Reads test accounts from the auth-origin-scoped registry at\n * `~/.deepspace/test-accounts.json` (the same file\n * `deepspace test accounts create` writes). Persisted accounts are bound to a\n * remote account id; `name` and `label` are optional.\n *\n * Used by the multi-user Playwright fixture in `./fixtures.ts` and is\n * also exported standalone in case suites want to do their own\n * orchestration.\n */\n\nimport { existsSync, mkdirSync, readFileSync } from 'node:fs'\nimport { homedir } from 'node:os'\nimport { join } from 'node:path'\n// Node-only leaf (its only import is `node:fs`), and this module is Node-only\n// too — Playwright fixtures. Reusing it keeps one definition of \"write a file\n// holding plaintext secrets\" rather than a second hand-rolled copy here.\nimport { writeSecretFileSync } from '../cli/lib/secure-file'\n\nexport interface TestAccount {\n email: string\n password: string\n name?: string\n label?: string | null\n id?: string\n userId?: string\n createdAt?: number\n}\n\nexport const TEST_ACCOUNTS_PATH = join(homedir(), '.deepspace', 'test-accounts.json')\n\nconst PRODUCTION_AUTH_ORIGIN = 'https://auth.deep.space'\nconst STAGING_AUTH_ORIGIN = 'https://auth.deepspacesites.com'\n\nexport interface TestAccountCredentialStore {\n version: 2\n scopes: Record<string, TestAccount[]>\n}\n\nexport interface RemoteTestAccount {\n id: string\n email: string\n userId: string\n label: string | null\n createdAt: number\n}\n\n/**\n * Load all test accounts the developer has created. Returns an empty\n * array if the file doesn't exist yet.\n */\nexport function currentTestAccountScope(env: NodeJS.ProcessEnv = process.env): string {\n const selected =\n env.DEEPSPACE_AUTH_URL ??\n (env.DEEPSPACE_ENV === 'staging' ? STAGING_AUTH_ORIGIN : PRODUCTION_AUTH_ORIGIN)\n return normalizeTestAccountScope(selected)\n}\n\nexport function normalizeTestAccountScope(value: string): string {\n try {\n return new URL(value).origin\n } catch {\n return value\n }\n}\n\nexport function loadAllTestAccounts(scope = currentTestAccountScope()): TestAccount[] {\n const store = readTestAccountStore()\n return store.scopes[scope] ?? []\n}\n\n/**\n * Replace the local credential registry with the subset that still exists\n * remotely. Passwords and display names remain local-only; a recreated email\n * with a different id is deliberately treated as a different account.\n */\nexport function reconcileTestAccountStore(\n remote: RemoteTestAccount[],\n scope = currentTestAccountScope(),\n): {\n accounts: TestAccount[]\n removed: number\n} {\n const result = reconcileTestAccountScopes(readTestAccountStore(), remote, scope)\n saveTestAccountStore(result.store)\n return { accounts: result.accounts, removed: result.removed }\n}\n\nexport function reconcileTestAccountScopes(\n store: TestAccountCredentialStore,\n remote: RemoteTestAccount[],\n scope: string,\n): { store: TestAccountCredentialStore; accounts: TestAccount[]; removed: number } {\n const scoped = store.scopes[scope] ?? []\n const accounts = reconcileTestAccounts(scoped, remote)\n return {\n accounts,\n removed: Math.max(0, scoped.length - accounts.length),\n store: {\n version: 2,\n scopes: { ...store.scopes, [scope]: accounts },\n },\n }\n}\n\nexport function reconcileTestAccounts(\n local: TestAccount[],\n remote: RemoteTestAccount[],\n): TestAccount[] {\n const localById = new Map(\n local.filter((account) => account.id).map((account) => [account.id, account]),\n )\n return remote.flatMap((account) => {\n const stored = localById.get(account.id)\n return stored ? [{ ...stored, ...account }] : []\n })\n}\n\n/** Add or replace one locally stored credential without creating duplicates. */\nexport function upsertTestAccount(account: TestAccount, scope = currentTestAccountScope()): void {\n const store = readTestAccountStore()\n const accounts = (store.scopes[scope] ?? []).filter(\n (stored) => stored.id !== account.id && stored.email !== account.email,\n )\n saveTestAccountStore({\n version: 2,\n scopes: { ...store.scopes, [scope]: [...accounts, account] },\n })\n}\n\n/** Remove credentials after the corresponding remote delete succeeds. */\nexport function removeTestAccounts(\n ids: string[],\n emails: string[] = [],\n scope = currentTestAccountScope(),\n): void {\n const idSet = new Set(ids)\n const emailSet = new Set(emails)\n const keep = (account: TestAccount): boolean =>\n !idSet.has(account.id ?? '') && !emailSet.has(account.email)\n const store = readTestAccountStore()\n saveTestAccountStore({\n version: 2,\n scopes: { ...store.scopes, [scope]: (store.scopes[scope] ?? []).filter(keep) },\n })\n}\n\nfunction readTestAccountStore(): TestAccountCredentialStore {\n if (!existsSync(TEST_ACCOUNTS_PATH)) return { version: 2, scopes: {} }\n try {\n const raw: unknown = JSON.parse(readFileSync(TEST_ACCOUNTS_PATH, 'utf-8'))\n if (Array.isArray(raw)) return { version: 2, scopes: {} }\n if (!raw || typeof raw !== 'object') {\n return { version: 2, scopes: {} }\n }\n const candidate = raw as { scopes?: unknown }\n const scopes =\n candidate.scopes && typeof candidate.scopes === 'object' && !Array.isArray(candidate.scopes)\n ? Object.fromEntries(\n Object.entries(candidate.scopes).map(([key, value]) => [key, validTestAccounts(value)]),\n )\n : {}\n return { version: 2, scopes }\n } catch {\n return { version: 2, scopes: {} }\n }\n}\n\nfunction validTestAccounts(value: unknown): TestAccount[] {\n if (!Array.isArray(value)) return []\n return value.filter((item): item is TestAccount =>\n Boolean(\n item &&\n typeof item === 'object' &&\n typeof (item as TestAccount).id === 'string' &&\n typeof (item as TestAccount).email === 'string' &&\n typeof (item as TestAccount).password === 'string',\n ),\n )\n}\n\nfunction saveTestAccountStore(store: TestAccountCredentialStore): void {\n mkdirSync(join(homedir(), '.deepspace'), { recursive: true })\n // Holds plaintext passwords, so it must end up 0600 even when the file\n // already exists with a wider mode — `writeFileSync`'s `mode` applies only\n // at creation, which would silently leave a pre-existing world-readable\n // file world-readable while writing fresh credentials into it.\n writeSecretFileSync(TEST_ACCOUNTS_PATH, JSON.stringify(store, null, 2))\n}\n\n/**\n * Pick `count` test accounts from the local registry, optionally\n * filtered by `label`. Order-stable: sorted by `createdAt` ascending so\n * the first test account you created is always the first one returned.\n *\n * Throws with a helpful message if not enough accounts exist.\n */\nexport function pickTestAccounts(count: number, options?: { label?: string }): TestAccount[] {\n const all = loadAllTestAccounts()\n const filtered = options?.label ? all.filter((a) => a.label === options.label) : all\n const sorted = filtered.slice().sort((a, b) => (a.createdAt ?? 0) - (b.createdAt ?? 0))\n\n if (sorted.length < count) {\n const labelHint = options?.label ? ` (label=\"${options.label}\")` : ''\n throw new Error(\n `Multiplayer test needs ${count} test accounts${labelHint}, but only ${sorted.length} ` +\n `are usable from ${TEST_ACCOUNTS_PATH}.\\n` +\n `The pool is global per developer, but passwords are stored only on the machine that ` +\n `created the account — so accounts created elsewhere are listed by ` +\n `\\`deepspace test accounts list\\` and count against the 10-cap, yet cannot be signed in ` +\n `as here. Re-issue their credentials, or create more:\\n` +\n ` deepspace test accounts recover --all (note: --all only covers accounts missing a local credential; recover a stale one by its email)\\n` +\n ` deepspace test accounts create --email <name>@deepspace.test --password <pw> --name \"<name>\"${\n options?.label ? ` --label ${options.label}` : ''\n }`,\n )\n }\n\n return sorted.slice(0, count)\n}\n\n/**\n * Turn a display name into the local-part of a test-account email.\n * \"Collab A\" -> \"collab-a\". Without this the suggested command in the\n * not-found error carried a space inside the address and could not be run.\n */\nexport function testAccountEmailSlug(name: string): string {\n const slug = name\n .trim()\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-+|-+$/g, '')\n return slug || 'tester'\n}\n\n/**\n * Find a single account by name. Used by `users(['Alice', 'Bob'])`.\n * Throws if not found.\n */\nexport function findTestAccountByName(name: string): TestAccount {\n const all = loadAllTestAccounts()\n const match = all.find((a) => a.name === name)\n if (!match) {\n throw new Error(\n `No test account named \"${name}\" is usable from ${TEST_ACCOUNTS_PATH}.\\n` +\n `If it exists in the pool but was created on another machine, fetch its credential:\\n` +\n ` deepspace test accounts recover --all (note: --all only covers accounts missing a local credential; recover a stale one by its email)\\n` +\n `Otherwise create it:\\n` +\n ` deepspace test accounts create --email ${testAccountEmailSlug(name)}@deepspace.test --password <pw> --name \"${name}\"\\n` +\n `Suites that do not depend on a specific identity should ask for any N accounts with ` +\n `\\`users(2)\\` instead of naming them.`,\n )\n }\n return match\n}\n","/**\n * Per-account `storageState` cache for Playwright.\n *\n * The auth worker limits password sign-ins per account. A\n * multiplayer suite that spins up\n * many users per spec can hit the limit and start failing in non-obvious\n * ways. This module signs each account in *once*, persists the\n * resulting browser cookies + storage to disk, and reuses the file on\n * subsequent runs.\n *\n * Cache layout:\n * ~/.deepspace/playwright-states/<sha256(authScope, appOrigin, email)>.json\n *\n * Validity: a cached file is reused if Playwright successfully loads it\n * AND the resulting context produces a non-anonymous session on the\n * target app. If validation fails, we fall back to a fresh sign-in and\n * overwrite the cache.\n */\n\nimport { createHash } from 'node:crypto'\nimport { existsSync, mkdirSync } from 'node:fs'\nimport { homedir } from 'node:os'\nimport { join } from 'node:path'\nimport { request, type Browser, type BrowserContext } from '@playwright/test'\nimport { decodeJwtPayload } from '../shared/jwt'\nimport { currentTestAccountScope } from './accounts'\n\nconst STATES_DIR = join(homedir(), '.deepspace', 'playwright-states')\n\n/**\n * Validation/sign-in per state path, in flight or settled. Memoizing the\n * promise — rather than flagging a Set after the await — makes concurrent\n * callers for the same account share one probe/sign-in instead of racing\n * duplicate sign-ins into the rate limit. A rejected attempt is evicted so\n * a later call can retry.\n */\nconst stateValidations = new Map<string, Promise<string>>()\n\ninterface StorageStateAccount {\n email: string\n password: string\n /** Remote user id, when the local registry knows it. */\n userId?: string\n}\n\n/** Where the cache for `email` against `baseURL`'s origin lives. */\nexport function getStatePathForEmail(email: string, baseURL: string): string {\n // Keyed by auth scope + app origin + email: one email signed into two\n // apps (or two auth environments) must never serve the wrong session.\n const key = `${currentTestAccountScope()}\\n${new URL(baseURL).origin}\\n${email.trim().toLowerCase()}`\n const hash = createHash('sha256').update(key).digest('hex').slice(0, 24)\n return join(STATES_DIR, `${hash}.json`)\n}\n\nfunction ensureStatesDir() {\n if (!existsSync(STATES_DIR)) mkdirSync(STATES_DIR, { recursive: true })\n}\n\n/**\n * Does a token minted from a cached session belong to `account`? `sub` is\n * authoritative when the registry knows the remote user id; the `email`\n * claim covers states recorded before the id was known.\n */\nexport function tokenClaimsMatchAccount(\n claims: Record<string, unknown>,\n account: { email: string; userId?: string },\n): boolean {\n if (account.userId && claims.sub === account.userId) return true\n return (\n typeof claims.email === 'string' &&\n claims.email.trim().toLowerCase() === account.email.trim().toLowerCase()\n )\n}\n\n/**\n * Live-validate a cached state: load it into a request context and ask the\n * app to mint a token. Only a token for this account counts — a transport\n * failure, an anonymous session, or another user's session all report\n * false, so the caller signs in fresh (where a real outage surfaces as an\n * actionable sign-in error instead of silent reuse of a dead session).\n */\nasync function probeStoredSession(\n path: string,\n baseURL: string,\n account: { email: string; userId?: string },\n): Promise<boolean> {\n try {\n const ctx = await request.newContext({ storageState: path, baseURL })\n try {\n const res = await ctx.post('/api/auth/token')\n if (!res.ok()) return false\n const { token } = (await res.json()) as { token?: unknown }\n if (typeof token !== 'string') return false\n return tokenClaimsMatchAccount(decodeJwtPayload(token), account)\n } finally {\n await ctx.dispose()\n }\n } catch {\n return false\n }\n}\n\n/**\n * Sign `account` in by POSTing `/api/auth/sign-in/email` against\n * `baseURL` and capturing the resulting cookies. Returns the path to the\n * Playwright `storageState` JSON file.\n *\n * The browser-driven approach is intentional: we want the cookies set\n * on the same origin Playwright contexts will load from, otherwise the\n * Set-Cookie domain attribute won't match.\n */\nasync function signInAndSaveState(\n browser: Browser,\n account: { email: string; password: string },\n baseURL: string,\n outPath: string,\n): Promise<string> {\n ensureStatesDir()\n const ctx = await browser.newContext({ baseURL })\n try {\n const page = await ctx.newPage()\n // Hit the app first so the origin is established (some auth plugins\n // require a same-origin Origin header on sign-in).\n await page.goto('/')\n const result = await page.evaluate(\n async ({ email, password }: { email: string; password: string }) => {\n const res = await fetch('/api/auth/sign-in/email', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ email, password }),\n })\n const body: unknown = await res.json().catch(() => null)\n const error = body && typeof body === 'object' ? (body as Record<string, unknown>) : null\n return {\n ok: res.ok,\n status: res.status,\n code: typeof error?.code === 'string' ? error.code : null,\n message: typeof error?.message === 'string' ? error.message : null,\n }\n },\n { email: account.email, password: account.password },\n )\n if (!result.ok) throw new Error(formatSignInFailure(account.email, result))\n await ctx.storageState({ path: outPath })\n return outPath\n } finally {\n await ctx.close()\n }\n}\n\ninterface SignInFailure {\n status: number\n code: string | null\n message: string | null\n}\n\n/** Keep the server's safe status/code so auth failures remain actionable. */\nexport function formatSignInFailure(email: string, failure: SignInFailure): string {\n if (failure.status === 429) {\n return `Sign-in rate-limited for ${email} (HTTP 429). Wait about 60 seconds and retry.`\n }\n const code = failure.code ? ` ${failure.code}` : ''\n const detail = failure.message ? `: ${failure.message}` : ''\n const nextStep =\n failure.code === 'INVALID_ORIGIN'\n ? 'Add this app origin to the auth allowlist, then retry.'\n : 'The stored password may be stale — fetch the current credential with `deepspace test accounts recover --email <email>` (--all only covers accounts with no local credential), or create a new account with `deepspace test accounts create`.'\n return `Sign-in failed for ${email} (HTTP ${failure.status}${code})${detail}. ${nextStep}`\n}\n\nexport interface EnsureStorageStateOptions {\n /** Force a fresh sign-in even if a cached state validates. */\n force?: boolean\n}\n\n/**\n * Ensure a validated Playwright `storageState` file exists for `account`\n * and return its path. A cached file is probed against the app before\n * reuse (once per worker process); a missing, expired, or wrong-account\n * state triggers one fresh sign-in that overwrites it.\n */\nexport async function ensureStorageState(\n browser: Browser,\n account: StorageStateAccount,\n baseURL: string,\n options: EnsureStorageStateOptions = {},\n): Promise<string> {\n const path = getStatePathForEmail(account.email, baseURL)\n if (options.force) stateValidations.delete(path)\n let pending = stateValidations.get(path)\n if (!pending) {\n pending = validateOrSignIn(browser, account, baseURL, path, options.force === true)\n stateValidations.set(path, pending)\n pending.catch(() => stateValidations.delete(path))\n }\n return pending\n}\n\nasync function validateOrSignIn(\n browser: Browser,\n account: StorageStateAccount,\n baseURL: string,\n path: string,\n force: boolean,\n): Promise<string> {\n if (!force && existsSync(path) && (await probeStoredSession(path, baseURL, account))) {\n return path\n }\n return signInAndSaveState(browser, account, baseURL, path)\n}\n\n/**\n * Convenience: open a fresh `BrowserContext` for `account` using the\n * cached storage state (signing in if needed). The caller is responsible\n * for closing the context.\n */\nexport async function newSignedInContext(\n browser: Browser,\n account: StorageStateAccount,\n baseURL: string,\n options: EnsureStorageStateOptions = {},\n): Promise<BrowserContext> {\n const statePath = await ensureStorageState(browser, account, baseURL, options)\n return browser.newContext({ storageState: statePath, baseURL })\n}\n","export function decodeJwtPayload<T = Record<string, unknown>>(token: string): T {\n const payloadSegment = token.split('.')[1]\n if (!payloadSegment) {\n throw new Error('JWT payload is missing')\n }\n return JSON.parse(Buffer.from(payloadSegment, 'base64url').toString('utf8')) as T\n}\n"],"mappings":"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","names":["existsSync","mkdirSync","homedir","join","join","homedir","existsSync","mkdirSync"]}
1
+ {"version":3,"sources":["../src/testing/fixtures.ts","../src/testing/accounts.ts","../src/shared/planes.ts","../src/testing/storage-state.ts","../src/shared/jwt.ts"],"sourcesContent":["/**\n * Multi-user Playwright fixture for DeepSpace apps.\n *\n * Usage:\n *\n * import { test, expect } from 'deepspace/testing'\n *\n * test('A sends, B sees', async ({ users }) => {\n * const [a, b] = await users(2)\n * await a.page.goto('/chat')\n * await b.page.goto('/chat')\n * await a.page.getByTestId('message-input').fill('hi')\n * await a.page.getByTestId('send-message-btn').click()\n * await expect(b.page.getByText('hi')).toBeVisible()\n * })\n *\n * `users(N)` picks the first N test accounts from\n * `~/.deepspace/test-accounts.json` (created via\n * `deepspace test accounts create`). `users(['Alice', 'Bob'])` picks\n * specific accounts by name.\n *\n * Each user gets a separate browser context with cached storageState\n * (per-account JSON in `~/.deepspace/playwright-states/`). Sign-in\n * happens once per account, not once per test, staying below the auth\n * worker's password sign-in limit.\n *\n * Contexts are auto-closed when the test finishes.\n */\n\nimport { test as baseTest, type Browser, type BrowserContext, type Page } from '@playwright/test'\nimport {\n type TestAccount,\n pickTestAccounts,\n findTestAccountByName,\n} from './accounts'\nimport { ensureStorageState, type EnsureStorageStateOptions } from './storage-state'\n\nexport interface MultiplayerUser {\n context: BrowserContext\n page: Page\n email: string\n name: string\n /** Test account user ID, if known from the accounts registry. */\n userId?: string\n}\n\n/**\n * Fixture function signature. Pass either a count (`users(2)`) or an\n * array of account names (`users(['Alice', 'Bob'])`). Returns one entry\n * per requested user.\n */\nexport type UsersFixture = (\n selector: number | string[],\n options?: {\n label?: string\n /** Sign in again even if the cached session still validates. */\n force?: boolean\n },\n) => Promise<MultiplayerUser[]>\n\ninterface MultiplayerFixtures {\n users: UsersFixture\n}\n\nasync function buildUser(\n browser: Browser,\n account: TestAccount,\n baseURL: string,\n options: EnsureStorageStateOptions = {},\n): Promise<{ user: MultiplayerUser; cleanup: () => Promise<void> }> {\n const statePath = await ensureStorageState(browser, account, baseURL, options)\n const context = await browser.newContext({ storageState: statePath, baseURL })\n const page = await context.newPage()\n return {\n user: {\n context,\n page,\n email: account.email,\n name: account.name ?? account.email.split('@')[0],\n userId: account.userId,\n },\n cleanup: async () => {\n await context.close()\n },\n }\n}\n\nexport const test = baseTest.extend<MultiplayerFixtures>({\n users: async ({ browser, baseURL }, use) => {\n if (!baseURL) {\n throw new Error(\n '`users` fixture requires a baseURL. Set it on your Playwright config ' +\n '(e.g. `use: { baseURL: \"http://localhost:5173\" }`) or via the ' +\n 'webServer.url shortcut.',\n )\n }\n\n const cleanups: Array<() => Promise<void>> = []\n\n const usersFn: UsersFixture = async (selector, options) => {\n const accounts: TestAccount[] = Array.isArray(selector)\n ? selector.map((name) => findTestAccountByName(name))\n : pickTestAccounts(selector, options)\n\n const built = await Promise.all(\n accounts.map((acct) => buildUser(browser, acct, baseURL, { force: options?.force })),\n )\n for (const b of built) cleanups.push(b.cleanup)\n return built.map((b) => b.user)\n }\n\n await use(usersFn)\n\n // Tear down any contexts we created. Errors are swallowed — a test\n // failure shouldn't be masked by a cleanup error.\n for (const cleanup of cleanups) {\n await cleanup().catch(() => {})\n }\n },\n})\n\nexport { expect } from '@playwright/test'\n","/**\n * Account-credential plumbing for the testing module.\n *\n * Reads test accounts from the auth-origin-scoped registry at\n * `~/.deepspace/test-accounts.json` (the same file\n * `deepspace test accounts create` writes). Persisted accounts are bound to a\n * remote account id; `name` and `label` are optional.\n *\n * Used by the multi-user Playwright fixture in `./fixtures.ts` and is\n * also exported standalone in case suites want to do their own\n * orchestration.\n */\n\nimport { existsSync, mkdirSync, readFileSync } from 'node:fs'\nimport { homedir } from 'node:os'\nimport { join } from 'node:path'\n// Node-only leaf (its only import is `node:fs`), and this module is Node-only\n// too — Playwright fixtures. Reusing it keeps one definition of \"write a file\n// holding plaintext secrets\" rather than a second hand-rolled copy here.\nimport { writeSecretFileSync } from '../cli/lib/secure-file'\nimport { PLANES } from '../shared/planes'\n\nexport interface TestAccount {\n email: string\n password: string\n name?: string\n label?: string | null\n id?: string\n userId?: string\n createdAt?: number\n}\n\nexport const TEST_ACCOUNTS_PATH = join(homedir(), '.deepspace', 'test-accounts.json')\n\nexport interface TestAccountCredentialStore {\n version: 2\n scopes: Record<string, TestAccount[]>\n}\n\nexport interface RemoteTestAccount {\n id: string\n email: string\n userId: string\n label: string | null\n createdAt: number\n}\n\n/**\n * Load all test accounts the developer has created. Returns an empty\n * array if the file doesn't exist yet.\n */\nexport function currentTestAccountScope(env: NodeJS.ProcessEnv = process.env): string {\n const plane: string | undefined = env.DEEPSPACE_ENV\n const selected =\n env.DEEPSPACE_AUTH_URL ??\n (plane === 'staging' || plane === 'medical' ? PLANES[plane].auth : PLANES.production.auth)\n return normalizeTestAccountScope(selected)\n}\n\nexport function normalizeTestAccountScope(value: string): string {\n try {\n return new URL(value).origin\n } catch {\n return value\n }\n}\n\nexport function loadAllTestAccounts(scope = currentTestAccountScope()): TestAccount[] {\n const store = readTestAccountStore()\n return store.scopes[scope] ?? []\n}\n\n/**\n * Replace the local credential registry with the subset that still exists\n * remotely. Passwords and display names remain local-only; a recreated email\n * with a different id is deliberately treated as a different account.\n */\nexport function reconcileTestAccountStore(\n remote: RemoteTestAccount[],\n scope = currentTestAccountScope(),\n): {\n accounts: TestAccount[]\n removed: number\n} {\n const result = reconcileTestAccountScopes(readTestAccountStore(), remote, scope)\n saveTestAccountStore(result.store)\n return { accounts: result.accounts, removed: result.removed }\n}\n\nexport function reconcileTestAccountScopes(\n store: TestAccountCredentialStore,\n remote: RemoteTestAccount[],\n scope: string,\n): { store: TestAccountCredentialStore; accounts: TestAccount[]; removed: number } {\n const scoped = store.scopes[scope] ?? []\n const accounts = reconcileTestAccounts(scoped, remote)\n return {\n accounts,\n removed: Math.max(0, scoped.length - accounts.length),\n store: {\n version: 2,\n scopes: { ...store.scopes, [scope]: accounts },\n },\n }\n}\n\nexport function reconcileTestAccounts(\n local: TestAccount[],\n remote: RemoteTestAccount[],\n): TestAccount[] {\n const localById = new Map(\n local.filter((account) => account.id).map((account) => [account.id, account]),\n )\n return remote.flatMap((account) => {\n const stored = localById.get(account.id)\n return stored ? [{ ...stored, ...account }] : []\n })\n}\n\n/** Add or replace one locally stored credential without creating duplicates. */\nexport function upsertTestAccount(account: TestAccount, scope = currentTestAccountScope()): void {\n const store = readTestAccountStore()\n const accounts = (store.scopes[scope] ?? []).filter(\n (stored) => stored.id !== account.id && stored.email !== account.email,\n )\n saveTestAccountStore({\n version: 2,\n scopes: { ...store.scopes, [scope]: [...accounts, account] },\n })\n}\n\n/** Remove credentials after the corresponding remote delete succeeds. */\nexport function removeTestAccounts(\n ids: string[],\n emails: string[] = [],\n scope = currentTestAccountScope(),\n): void {\n const idSet = new Set(ids)\n const emailSet = new Set(emails)\n const keep = (account: TestAccount): boolean =>\n !idSet.has(account.id ?? '') && !emailSet.has(account.email)\n const store = readTestAccountStore()\n saveTestAccountStore({\n version: 2,\n scopes: { ...store.scopes, [scope]: (store.scopes[scope] ?? []).filter(keep) },\n })\n}\n\nfunction readTestAccountStore(): TestAccountCredentialStore {\n if (!existsSync(TEST_ACCOUNTS_PATH)) return { version: 2, scopes: {} }\n try {\n const raw: unknown = JSON.parse(readFileSync(TEST_ACCOUNTS_PATH, 'utf-8'))\n if (Array.isArray(raw)) return { version: 2, scopes: {} }\n if (!raw || typeof raw !== 'object') {\n return { version: 2, scopes: {} }\n }\n const candidate = raw as { scopes?: unknown }\n const scopes =\n candidate.scopes && typeof candidate.scopes === 'object' && !Array.isArray(candidate.scopes)\n ? Object.fromEntries(\n Object.entries(candidate.scopes).map(([key, value]) => [key, validTestAccounts(value)]),\n )\n : {}\n return { version: 2, scopes }\n } catch {\n return { version: 2, scopes: {} }\n }\n}\n\nfunction validTestAccounts(value: unknown): TestAccount[] {\n if (!Array.isArray(value)) return []\n return value.filter((item): item is TestAccount =>\n Boolean(\n item &&\n typeof item === 'object' &&\n typeof (item as TestAccount).id === 'string' &&\n typeof (item as TestAccount).email === 'string' &&\n typeof (item as TestAccount).password === 'string',\n ),\n )\n}\n\nfunction saveTestAccountStore(store: TestAccountCredentialStore): void {\n mkdirSync(join(homedir(), '.deepspace'), { recursive: true })\n // Holds plaintext passwords, so it must end up 0600 even when the file\n // already exists with a wider mode — `writeFileSync`'s `mode` applies only\n // at creation, which would silently leave a pre-existing world-readable\n // file world-readable while writing fresh credentials into it.\n writeSecretFileSync(TEST_ACCOUNTS_PATH, JSON.stringify(store, null, 2))\n}\n\n/**\n * Pick `count` test accounts from the local registry, optionally\n * filtered by `label`. Order-stable: sorted by `createdAt` ascending so\n * the first test account you created is always the first one returned.\n *\n * Throws with a helpful message if not enough accounts exist.\n */\nexport function pickTestAccounts(count: number, options?: { label?: string }): TestAccount[] {\n const all = loadAllTestAccounts()\n const filtered = options?.label ? all.filter((a) => a.label === options.label) : all\n const sorted = filtered.slice().sort((a, b) => (a.createdAt ?? 0) - (b.createdAt ?? 0))\n\n if (sorted.length < count) {\n const labelHint = options?.label ? ` (label=\"${options.label}\")` : ''\n throw new Error(\n `Multiplayer test needs ${count} test accounts${labelHint}, but only ${sorted.length} ` +\n `are usable from ${TEST_ACCOUNTS_PATH}.\\n` +\n `The pool is global per developer, but passwords are stored only on the machine that ` +\n `created the account — so accounts created elsewhere are listed by ` +\n `\\`deepspace test accounts list\\` and count against the 10-cap, yet cannot be signed in ` +\n `as here. Re-issue their credentials, or create more:\\n` +\n ` deepspace test accounts recover --all (note: --all only covers accounts missing a local credential; recover a stale one by its email)\\n` +\n ` deepspace test accounts create --email <name>@deepspace.test --password <pw> --name \"<name>\"${\n options?.label ? ` --label ${options.label}` : ''\n }`,\n )\n }\n\n return sorted.slice(0, count)\n}\n\n/**\n * Turn a display name into the local-part of a test-account email.\n * \"Collab A\" -> \"collab-a\". Without this the suggested command in the\n * not-found error carried a space inside the address and could not be run.\n */\nexport function testAccountEmailSlug(name: string): string {\n const slug = name\n .trim()\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-+|-+$/g, '')\n return slug || 'tester'\n}\n\n/**\n * Find a single account by name. Used by `users(['Alice', 'Bob'])`.\n * Throws if not found.\n */\nexport function findTestAccountByName(name: string): TestAccount {\n const all = loadAllTestAccounts()\n const match = all.find((a) => a.name === name)\n if (!match) {\n throw new Error(\n `No test account named \"${name}\" is usable from ${TEST_ACCOUNTS_PATH}.\\n` +\n `If it exists in the pool but was created on another machine, fetch its credential:\\n` +\n ` deepspace test accounts recover --all (note: --all only covers accounts missing a local credential; recover a stale one by its email)\\n` +\n `Otherwise create it:\\n` +\n ` deepspace test accounts create --email ${testAccountEmailSlug(name)}@deepspace.test --password <pw> --name \"${name}\"\\n` +\n `Suites that do not depend on a specific identity should ask for any N accounts with ` +\n `\\`users(2)\\` instead of naming them.`,\n )\n }\n return match\n}\n","/**\n * DeepSpace's deployment planes, in one place: production, staging and\n * DeepSpace Medical, the BAA-covered plane for healthcare apps\n * (docs/proposals/deepspace-medical.md).\n *\n * Side-effect free. The CLI, the browser client, the test-account helpers and\n * the platform workers all read it, so a plane's hosts and its restricted-plane\n * rule cannot drift between them.\n */\n\nexport type PlaneName = 'production' | 'staging' | 'medical'\n\nexport interface PlaneUrls {\n auth: string\n api: string\n platform: string\n deploy: string\n /** Deployed apps live on `<name>.<appDomain>`. */\n appDomain: string\n /** The zone the platform services live on. */\n platformDomain: string\n /** The plane's public home page. */\n home: string\n /** The builder dashboard; null where the plane has none. */\n dashboard: string | null\n /** The privacy policy sign-in screens link to. */\n privacyPolicy: string\n}\n\nexport const PLANES: Readonly<Record<PlaneName, PlaneUrls>> = {\n production: {\n auth: 'https://auth.deep.space',\n api: 'https://api-worker.deep.space',\n platform: 'https://platform-worker.deep.space',\n deploy: 'https://deploy-worker.deep.space',\n appDomain: 'app.space',\n platformDomain: 'deep.space',\n home: 'https://deep.space',\n dashboard: 'https://dashboard.deep.space',\n privacyPolicy: 'https://deep.space/privacy',\n },\n // Services on deepspacesites.com, apps on spacestest.com: production's\n // two-zone split, because a Worker cannot fetch one in its own zone.\n staging: {\n auth: 'https://auth.deepspacesites.com',\n api: 'https://api.deepspacesites.com',\n platform: 'https://platform.deepspacesites.com',\n deploy: 'https://deploy.deepspacesites.com',\n appDomain: 'spacestest.com',\n platformDomain: 'deepspacesites.com',\n home: 'https://spacestest.com',\n dashboard: 'https://dashboard.deepspacesites.com',\n privacyPolicy: 'https://deep.space/privacy',\n },\n // Services on deepspacemedical.com, apps on deepspacemedical.app: two zones\n // in the medical Cloudflare account. The plane has no dashboard.\n medical: {\n auth: 'https://auth.deepspacemedical.com',\n api: 'https://api.deepspacemedical.com',\n platform: 'https://platform.deepspacemedical.com',\n deploy: 'https://deploy.deepspacemedical.com',\n appDomain: 'deepspacemedical.app',\n platformDomain: 'deepspacemedical.com',\n home: 'https://deepspacemedical.com',\n dashboard: null,\n privacyPolicy: 'https://deepspacemedical.com/privacy',\n },\n}\n\n/** Planes whose workers run without the medical restrictions. */\nconst OPEN_PLANES: ReadonlySet<string> = new Set(['production', 'staging'])\n\n/**\n * Whether a platform worker runs on a restricted plane (DeepSpace Medical\n * sets `DEEPSPACE_PLANE = \"medical\"`). Unset means production. Any value other\n * than `production` or `staging` restricts, so a typo fails closed.\n */\nexport function isRestrictedPlane(env: { DEEPSPACE_PLANE?: string }): boolean {\n const plane = env.DEEPSPACE_PLANE\n return plane !== undefined && plane !== '' && !OPEN_PLANES.has(plane)\n}\n","/**\n * Per-account `storageState` cache for Playwright.\n *\n * The auth worker limits password sign-ins per account. A\n * multiplayer suite that spins up\n * many users per spec can hit the limit and start failing in non-obvious\n * ways. This module signs each account in *once*, persists the\n * resulting browser cookies + storage to disk, and reuses the file on\n * subsequent runs.\n *\n * Cache layout:\n * ~/.deepspace/playwright-states/<sha256(authScope, appOrigin, email)>.json\n *\n * Validity: a cached file is reused if Playwright successfully loads it\n * AND the resulting context produces a non-anonymous session on the\n * target app. If validation fails, we fall back to a fresh sign-in and\n * overwrite the cache.\n */\n\nimport { createHash } from 'node:crypto'\nimport { existsSync, mkdirSync } from 'node:fs'\nimport { homedir } from 'node:os'\nimport { join } from 'node:path'\nimport { request, type Browser, type BrowserContext } from '@playwright/test'\nimport { decodeJwtPayload } from '../shared/jwt'\nimport { currentTestAccountScope } from './accounts'\n\nconst STATES_DIR = join(homedir(), '.deepspace', 'playwright-states')\n\n/**\n * Validation/sign-in per state path, in flight or settled. Memoizing the\n * promise — rather than flagging a Set after the await — makes concurrent\n * callers for the same account share one probe/sign-in instead of racing\n * duplicate sign-ins into the rate limit. A rejected attempt is evicted so\n * a later call can retry.\n */\nconst stateValidations = new Map<string, Promise<string>>()\n\ninterface StorageStateAccount {\n email: string\n password: string\n /** Remote user id, when the local registry knows it. */\n userId?: string\n}\n\n/** Where the cache for `email` against `baseURL`'s origin lives. */\nexport function getStatePathForEmail(email: string, baseURL: string): string {\n // Keyed by auth scope + app origin + email: one email signed into two\n // apps (or two auth environments) must never serve the wrong session.\n const key = `${currentTestAccountScope()}\\n${new URL(baseURL).origin}\\n${email.trim().toLowerCase()}`\n const hash = createHash('sha256').update(key).digest('hex').slice(0, 24)\n return join(STATES_DIR, `${hash}.json`)\n}\n\nfunction ensureStatesDir() {\n if (!existsSync(STATES_DIR)) mkdirSync(STATES_DIR, { recursive: true })\n}\n\n/**\n * Does a token minted from a cached session belong to `account`? `sub` is\n * authoritative when the registry knows the remote user id; the `email`\n * claim covers states recorded before the id was known.\n */\nexport function tokenClaimsMatchAccount(\n claims: Record<string, unknown>,\n account: { email: string; userId?: string },\n): boolean {\n if (account.userId && claims.sub === account.userId) return true\n return (\n typeof claims.email === 'string' &&\n claims.email.trim().toLowerCase() === account.email.trim().toLowerCase()\n )\n}\n\n/**\n * Live-validate a cached state: load it into a request context and ask the\n * app to mint a token. Only a token for this account counts — a transport\n * failure, an anonymous session, or another user's session all report\n * false, so the caller signs in fresh (where a real outage surfaces as an\n * actionable sign-in error instead of silent reuse of a dead session).\n */\nasync function probeStoredSession(\n path: string,\n baseURL: string,\n account: { email: string; userId?: string },\n): Promise<boolean> {\n try {\n const ctx = await request.newContext({ storageState: path, baseURL })\n try {\n const res = await ctx.post('/api/auth/token')\n if (!res.ok()) return false\n const { token } = (await res.json()) as { token?: unknown }\n if (typeof token !== 'string') return false\n return tokenClaimsMatchAccount(decodeJwtPayload(token), account)\n } finally {\n await ctx.dispose()\n }\n } catch {\n return false\n }\n}\n\n/**\n * Sign `account` in by POSTing `/api/auth/sign-in/email` against\n * `baseURL` and capturing the resulting cookies. Returns the path to the\n * Playwright `storageState` JSON file.\n *\n * The browser-driven approach is intentional: we want the cookies set\n * on the same origin Playwright contexts will load from, otherwise the\n * Set-Cookie domain attribute won't match.\n */\nasync function signInAndSaveState(\n browser: Browser,\n account: { email: string; password: string },\n baseURL: string,\n outPath: string,\n): Promise<string> {\n ensureStatesDir()\n const ctx = await browser.newContext({ baseURL })\n try {\n const page = await ctx.newPage()\n // Hit the app first so the origin is established (some auth plugins\n // require a same-origin Origin header on sign-in).\n await page.goto('/')\n const result = await page.evaluate(\n async ({ email, password }: { email: string; password: string }) => {\n const res = await fetch('/api/auth/sign-in/email', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ email, password }),\n })\n const body: unknown = await res.json().catch(() => null)\n const error = body && typeof body === 'object' ? (body as Record<string, unknown>) : null\n return {\n ok: res.ok,\n status: res.status,\n code: typeof error?.code === 'string' ? error.code : null,\n message: typeof error?.message === 'string' ? error.message : null,\n }\n },\n { email: account.email, password: account.password },\n )\n if (!result.ok) throw new Error(formatSignInFailure(account.email, result))\n await ctx.storageState({ path: outPath })\n return outPath\n } finally {\n await ctx.close()\n }\n}\n\ninterface SignInFailure {\n status: number\n code: string | null\n message: string | null\n}\n\n/** Keep the server's safe status/code so auth failures remain actionable. */\nexport function formatSignInFailure(email: string, failure: SignInFailure): string {\n if (failure.status === 429) {\n return `Sign-in rate-limited for ${email} (HTTP 429). Wait about 60 seconds and retry.`\n }\n const code = failure.code ? ` ${failure.code}` : ''\n const detail = failure.message ? `: ${failure.message}` : ''\n const nextStep =\n failure.code === 'INVALID_ORIGIN'\n ? 'Add this app origin to the auth allowlist, then retry.'\n : 'The stored password may be stale — fetch the current credential with `deepspace test accounts recover --email <email>` (--all only covers accounts with no local credential), or create a new account with `deepspace test accounts create`.'\n return `Sign-in failed for ${email} (HTTP ${failure.status}${code})${detail}. ${nextStep}`\n}\n\nexport interface EnsureStorageStateOptions {\n /** Force a fresh sign-in even if a cached state validates. */\n force?: boolean\n}\n\n/**\n * Ensure a validated Playwright `storageState` file exists for `account`\n * and return its path. A cached file is probed against the app before\n * reuse (once per worker process); a missing, expired, or wrong-account\n * state triggers one fresh sign-in that overwrites it.\n */\nexport async function ensureStorageState(\n browser: Browser,\n account: StorageStateAccount,\n baseURL: string,\n options: EnsureStorageStateOptions = {},\n): Promise<string> {\n const path = getStatePathForEmail(account.email, baseURL)\n if (options.force) stateValidations.delete(path)\n let pending = stateValidations.get(path)\n if (!pending) {\n pending = validateOrSignIn(browser, account, baseURL, path, options.force === true)\n stateValidations.set(path, pending)\n pending.catch(() => stateValidations.delete(path))\n }\n return pending\n}\n\nasync function validateOrSignIn(\n browser: Browser,\n account: StorageStateAccount,\n baseURL: string,\n path: string,\n force: boolean,\n): Promise<string> {\n if (!force && existsSync(path) && (await probeStoredSession(path, baseURL, account))) {\n return path\n }\n return signInAndSaveState(browser, account, baseURL, path)\n}\n\n/**\n * Convenience: open a fresh `BrowserContext` for `account` using the\n * cached storage state (signing in if needed). The caller is responsible\n * for closing the context.\n */\nexport async function newSignedInContext(\n browser: Browser,\n account: StorageStateAccount,\n baseURL: string,\n options: EnsureStorageStateOptions = {},\n): Promise<BrowserContext> {\n const statePath = await ensureStorageState(browser, account, baseURL, options)\n return browser.newContext({ storageState: statePath, baseURL })\n}\n","export function decodeJwtPayload<T = Record<string, unknown>>(token: string): T {\n const payloadSegment = token.split('.')[1]\n if (!payloadSegment) {\n throw new Error('JWT payload is missing')\n }\n return JSON.parse(Buffer.from(payloadSegment, 'base64url').toString('utf8')) as T\n}\n"],"mappings":"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","names":["existsSync","mkdirSync","homedir","join","join","homedir","existsSync","mkdirSync"]}
package/dist/worker.d.ts CHANGED
@@ -2630,6 +2630,42 @@ declare function authenticatedRoomRequest(request: Request, auth: RoomAuth | nul
2630
2630
  role?: string;
2631
2631
  }): Request;
2632
2632
 
2633
+ /**
2634
+ * DeepSpace's deployment planes, in one place: production, staging and
2635
+ * DeepSpace Medical, the BAA-covered plane for healthcare apps
2636
+ * (docs/proposals/deepspace-medical.md).
2637
+ *
2638
+ * Side-effect free. The CLI, the browser client, the test-account helpers and
2639
+ * the platform workers all read it, so a plane's hosts and its restricted-plane
2640
+ * rule cannot drift between them.
2641
+ */
2642
+ type PlaneName = 'production' | 'staging' | 'medical';
2643
+ interface PlaneUrls {
2644
+ auth: string;
2645
+ api: string;
2646
+ platform: string;
2647
+ deploy: string;
2648
+ /** Deployed apps live on `<name>.<appDomain>`. */
2649
+ appDomain: string;
2650
+ /** The zone the platform services live on. */
2651
+ platformDomain: string;
2652
+ /** The plane's public home page. */
2653
+ home: string;
2654
+ /** The builder dashboard; null where the plane has none. */
2655
+ dashboard: string | null;
2656
+ /** The privacy policy sign-in screens link to. */
2657
+ privacyPolicy: string;
2658
+ }
2659
+ declare const PLANES: Readonly<Record<PlaneName, PlaneUrls>>;
2660
+ /**
2661
+ * Whether a platform worker runs on a restricted plane (DeepSpace Medical
2662
+ * sets `DEEPSPACE_PLANE = "medical"`). Unset means production. Any value other
2663
+ * than `production` or `staging` restricts, so a typo fails closed.
2664
+ */
2665
+ declare function isRestrictedPlane(env: {
2666
+ DEEPSPACE_PLANE?: string;
2667
+ }): boolean;
2668
+
2633
2669
  /**
2634
2670
  * Wildcard-aware CORS origin matching for the platform workers.
2635
2671
  *
@@ -5760,4 +5796,4 @@ declare const MAX_LOG_TEXT_LENGTH: number;
5760
5796
  */
5761
5797
  declare function workerErrorHandler(prefix: string, respond?: (c: Context) => Response): ErrorHandler;
5762
5798
 
5763
- export { ACCOUNT_STORAGE_LIMIT_BYTES, ACTION_ROUTES_BEARER_GUARD_MIGRATION_ID, ACTION_TOOLS_DELETE_WHERE_MIGRATION_ID, AGENT_TOKEN_SCOPE, AGENT_TOOL_REQUEST_BODY_CAP, AGENT_TOOL_RESPONSE_BODY_CAP, AI_CHATS_SCHEMA, AI_MESSAGES_SCHEMA, AI_SDK_7_MIGRATION_ID, ALLOWED_BINDING_TYPES, ANONYMOUS_USER_ID_PREFIX, APP_ID_RE, APP_LOG_EVENT_KEYS, APP_NAME_RULES, ASSET_NOT_FOUND_HANDLING_VALUES, AUTO_PROVISIONABLE_TYPES, AUTO_PROVISION_SENTINEL, type ActionContext, type ActionHandler, type ActionResult, type ActionTools, type AgentTargetPolicy, type AgentToolAccessResult, type AgentToolRouteEnv, type AgentToolRouteOptions, type ApiErrorIssue, type ApiWorkerEnv, type AppLogEvent, type AppLogEventKey, type AppLogsResponse, type AppMembership, type AppMigrationIdsValidation, type AppNameResolution, type AppNameValidation, type AssetNotFoundHandling, type AuthWorkerEnv, Awareness, type AwarenessState, type AwarenessStates, BASE_USERS_SCHEMA, BROWSER_PROXY_ROUTES, BUILD_INJECTED_APP_ID_MIGRATION_ID, BUILT_IN_TOOLS, type BaseMessage, BaseRoom, type BindingJsonValue, BodyTooLargeError, CHANNELS_SCHEMA, CHANNEL_MEMBERS_SCHEMA, COST_RATES, CanvasRoom, type CanvasShape, type ChatContextConfig, type ChatMessageRow, type ChatRow, type ChatTurn, type ClientMessage, type CollectionSchema, type ColumnDefinition, type ColumnInterpretation, type ComposioToolsOptions, type ConnectionAttachment, type CronContext, type CronExecution, CronRoom, type CronRoomConfig, type CronTask, type CustomBinding, type CustomBindingManifest, DEEPSPACE_AGENT_PROFILES, DEEPSPACE_AI_DEFAULTS, DEEPSPACE_AI_MODELS, DEEPSPACE_MODEL_CATALOG_PROVENANCE, DEFAULT_CONTEXT_CONFIG, DEFAULT_DELETE_WHERE_LIMIT, DEFAULT_DO_MANIFEST, DEFAULT_QUERY_LIMIT, DEPLOYED_APPS_DEFAULTS, type DOBindings, type DOManifest, type DOManifestEntry, type DeepSpaceAIEnv, type DeepSpaceAIModel, type DeepSpaceAIModelId, type DeepSpaceAIOptions, type DeepSpaceAIProvider, type DeepSpaceAgentDiagnosticContext, DeepSpaceAgentModelError, type DeepSpaceAgentProfile, DeepSpaceAgentProfileError, type DeepSpaceAgentProfileId, type DeepSpaceAgentStream, type DeepSpaceAgentStreamOptions, type DeepSpaceAgentSupport, type DeepSpaceAuth, type DeepSpaceAuthConfig, type DeepSpaceDocumentationRouteEnv, type DeepSpaceDocumentationRouteOptions, type DeepSpaceUserCreateContext, type DoMigrationDirective, type DoMigrationPlan, type ExistingDOBinding, type ExpoAuthBridgeOptions, type ExpoAuthWorkerEnv, FILES_SESSION_COOKIE_READS_MIGRATION_ID, type GetActionData, type HandlerContext, type Job, type JobContext, JobRoom, type JobRoomConfig, type JobStatus, type JwtClaims, type JwtVerifierConfig, type KnowledgeAddOptions, type KnowledgeAddResult, type KnowledgeClient, type KnowledgeEnv, KnowledgeError, type KnowledgeItem, type KnowledgeListOptions, type KnowledgeListResult, type KnowledgeSearchChunk, type KnowledgeSearchOptions, type KnowledgeSearchResult, type KnowledgeStatus, LOG_LEVELS, type LogLevel, MAX_APP_FILE_BYTES, MAX_BINDING_NAME_LENGTH, MAX_DELETE_WHERE_LIMIT, MAX_DEPLOY_ASSET_FILE_BYTES, MAX_ENV_VAR_BYTES, MAX_LOG_TEXT_LENGTH, MAX_UPLOAD_PARTS, MAX_UPLOAD_REQUEST_BYTES, MESSAGES_SCHEMA, MSG, MSG_AWARENESS, MSG_SYNC, MSG_SYNC_STEP1, MSG_SYNC_STEP2, MSG_SYNC_UPDATE, MSG_YJS_AWARENESS, MSG_YJS_SYNC, type MessageHandlers, type MsgType, type MutateActionData, type NormalizedApiError, PRODUCTION_OFFICIAL_AGENT_ORIGINS, type PermissionContext, type PermissionLevel, type PlatformWorkerEnv, type PrefixResult, type PresencePeer, PresenceRoom, type ProtocolMessage, type ProxyRoute, type QueryActionData, REACTIONS_SCHEMA, READ_RECEIPTS_SCHEMA, RECORD_NOT_FOUND, REGISTRY_INSTANCE, RELEASE_STAMP_PATH, REPO_STORE_LIMIT_BYTES, REQUIRED_COMPATIBILITY_FLAGS, RESERVED_BINDING_NAMES, RESERVED_RUN_WORKER_FIRST, RESOURCE_ID_RE, ROLE_ADMIN, ROLE_ANONYMOUS, ROLE_DEFAULT, RecordRoom, type RecordRoomConfig, RegistryClientError, type ResolvedColumn, type ResolvedDeepSpaceAgentModel, type RolePermissions, type RunMigrationsResult, SANDBOX_SCOPES, SANDBOX_SCOPE_HEADER, SDK_RUN_WORKER_FIRST, SECRET_NAME_RE, SECURE_ROOM_BOUNDARIES_MIGRATION_ID, SESSION_COOKIE, STRICT_APP_ID_RE, SYSTEM_ASSIGNED_COLUMNS, SYSTEM_COLLECTIONS, SYSTEM_MAINTAINED_COLUMNS, SYSTEM_MANAGED_COLUMNS, type SandboxFile, SandboxFileError, type SandboxFileList, type SandboxFilesClient, type SandboxFilesOptions, type SandboxScope, type SandboxUploadOptions, SchemaRegistry, type ScopeContext, type ScopedKnowledgeClient, type ScopedR2Auth, type ScopedR2Config, type ScopedR2Handler, type ScreenshotEnv, type ScreenshotResult, type ServerMessage, type StorageAdmission, type StoredRecord, type Summarizer, type SyncResult, TEST_ACCOUNT_EMAIL_SUFFIX, type TokenDebugInfo, type ToolResult, type ToolSchema, UNDECLARED_ASSET_NOT_FOUND_HANDLING, UPLOAD_PART_BYTES, USERS_COLUMNS, type User, type UserAttachment, type UserToolExecutorEnv, type ValidationError, type VerifiedAuth, type VerifyOutcome, type VerifyResult, type Viewport, WORKER_OWNED_NOT_FOUND_MIGRATION_ID, YjsRoom, agentTokenIssuer, apiWorkerFetch, appendMessage, applyAiToolDefaults, applySlidingWindow, armCronRoom, authWorkerFetch, authenticatedRoomRequest, bindingManifestFromOutputConfig, buildCronContext, buildTableSelect, buildUiParts, callRegistry, callableFromSandbox, canCreate, canDelete, canRead, canUpdate, capToolResultSize, captureScreenshot, checkFieldPermissions, checkUnclaimedOwnerTransition, clientBuild, codeExecutionTool, coerceValue, collectionTableName, columnId, composioTools, computeDoMigration, computeHmacHex, createChat, createDecoder, createDeepSpaceAI, createDeepSpaceAuth, createEncoder, createScopedR2Handler, createUserToolExecutor, dataToColumnValues, decodeJwtPayload, deepSpaceAgentErrorSummary, deleteChatCascade, dispatch, encode, encodeAwarenessMessage, encodeSyncStep1, encodeSyncStep2, encodeUpdate, enqueueJob, epochSeconds, folderFilter, formatBytes, forwardSandboxContainer, getChat, getDeepSpaceAIModel, getMessageType, getRolePermissions, handleAwarenessMessage, handleSyncMessage, hasContent, isAnonymousUserId, isAssetNotFoundHandling, isAutoProvision, isFilesVerbPath, isOwner, isPlatformReservedPath, isTestAccountClaims, isTestAccountEmail, isTestAccountTier, knowledge, lintSchema, lintSchemaSet, lintSchemas, listDeepSpaceAgentModels, loadMessages, logEventText, loggableError, makeDefaultSummarizer, matchOrigin, meterAi, meterUsage, meterVectorize, mintAppId, mintUlid, nativeAuthCallback, nativeAuthExchange, nativeAuthMe, nativeAuthSignOut, nativeAuthStart, nativeAuthToken, noopPermissionContext, normalizeAgentTargetOrigin, normalizeApiError, normalizeKnowledgeFolder, parseQuotaLimit, platformCorsPatterns, platformWorkerFetch, prepareMessagesWithCompaction, priceBindingUsageEvent, readBoundedBodyText, readVarUint, readVarUint8Array, registerAgentToolRoutes, registerDeepSpaceDocumentation, registryErrorJson, registryStub, resolveAppMembership, resolveAppName, resolveAppRole, resolveColumn, resolveCompatibilityFlags, resolveDeepSpaceAgentModel, resolveSessionReadAuth, reuseSandbox, rowToData, runMigrations, sandboxFiles, sandboxOutputs, sandboxUpload, serverBuild, storageLimitForTier, storageQuotaMessage, streamDeepSpaceAgent, timingSafeEqualHex, timingSafeEqualStrings, toUint8Array, totalChars, truncateLogText, truncateOldToolResults, turnsToCoreMessages, unwrapToolOutput, updateChat, validateAppMigrationIds, validateAppName, validateBindingManifest, validateDoManifest, verifyAgentToken, verifyJwt, workerErrorHandler, writeVarUint, writeVarUint8Array };
5799
+ export { ACCOUNT_STORAGE_LIMIT_BYTES, ACTION_ROUTES_BEARER_GUARD_MIGRATION_ID, ACTION_TOOLS_DELETE_WHERE_MIGRATION_ID, AGENT_TOKEN_SCOPE, AGENT_TOOL_REQUEST_BODY_CAP, AGENT_TOOL_RESPONSE_BODY_CAP, AI_CHATS_SCHEMA, AI_MESSAGES_SCHEMA, AI_SDK_7_MIGRATION_ID, ALLOWED_BINDING_TYPES, ANONYMOUS_USER_ID_PREFIX, APP_ID_RE, APP_LOG_EVENT_KEYS, APP_NAME_RULES, ASSET_NOT_FOUND_HANDLING_VALUES, AUTO_PROVISIONABLE_TYPES, AUTO_PROVISION_SENTINEL, type ActionContext, type ActionHandler, type ActionResult, type ActionTools, type AgentTargetPolicy, type AgentToolAccessResult, type AgentToolRouteEnv, type AgentToolRouteOptions, type ApiErrorIssue, type ApiWorkerEnv, type AppLogEvent, type AppLogEventKey, type AppLogsResponse, type AppMembership, type AppMigrationIdsValidation, type AppNameResolution, type AppNameValidation, type AssetNotFoundHandling, type AuthWorkerEnv, Awareness, type AwarenessState, type AwarenessStates, BASE_USERS_SCHEMA, BROWSER_PROXY_ROUTES, BUILD_INJECTED_APP_ID_MIGRATION_ID, BUILT_IN_TOOLS, type BaseMessage, BaseRoom, type BindingJsonValue, BodyTooLargeError, CHANNELS_SCHEMA, CHANNEL_MEMBERS_SCHEMA, COST_RATES, CanvasRoom, type CanvasShape, type ChatContextConfig, type ChatMessageRow, type ChatRow, type ChatTurn, type ClientMessage, type CollectionSchema, type ColumnDefinition, type ColumnInterpretation, type ComposioToolsOptions, type ConnectionAttachment, type CronContext, type CronExecution, CronRoom, type CronRoomConfig, type CronTask, type CustomBinding, type CustomBindingManifest, DEEPSPACE_AGENT_PROFILES, DEEPSPACE_AI_DEFAULTS, DEEPSPACE_AI_MODELS, DEEPSPACE_MODEL_CATALOG_PROVENANCE, DEFAULT_CONTEXT_CONFIG, DEFAULT_DELETE_WHERE_LIMIT, DEFAULT_DO_MANIFEST, DEFAULT_QUERY_LIMIT, DEPLOYED_APPS_DEFAULTS, type DOBindings, type DOManifest, type DOManifestEntry, type DeepSpaceAIEnv, type DeepSpaceAIModel, type DeepSpaceAIModelId, type DeepSpaceAIOptions, type DeepSpaceAIProvider, type DeepSpaceAgentDiagnosticContext, DeepSpaceAgentModelError, type DeepSpaceAgentProfile, DeepSpaceAgentProfileError, type DeepSpaceAgentProfileId, type DeepSpaceAgentStream, type DeepSpaceAgentStreamOptions, type DeepSpaceAgentSupport, type DeepSpaceAuth, type DeepSpaceAuthConfig, type DeepSpaceDocumentationRouteEnv, type DeepSpaceDocumentationRouteOptions, type DeepSpaceUserCreateContext, type DoMigrationDirective, type DoMigrationPlan, type ExistingDOBinding, type ExpoAuthBridgeOptions, type ExpoAuthWorkerEnv, FILES_SESSION_COOKIE_READS_MIGRATION_ID, type GetActionData, type HandlerContext, type Job, type JobContext, JobRoom, type JobRoomConfig, type JobStatus, type JwtClaims, type JwtVerifierConfig, type KnowledgeAddOptions, type KnowledgeAddResult, type KnowledgeClient, type KnowledgeEnv, KnowledgeError, type KnowledgeItem, type KnowledgeListOptions, type KnowledgeListResult, type KnowledgeSearchChunk, type KnowledgeSearchOptions, type KnowledgeSearchResult, type KnowledgeStatus, LOG_LEVELS, type LogLevel, MAX_APP_FILE_BYTES, MAX_BINDING_NAME_LENGTH, MAX_DELETE_WHERE_LIMIT, MAX_DEPLOY_ASSET_FILE_BYTES, MAX_ENV_VAR_BYTES, MAX_LOG_TEXT_LENGTH, MAX_UPLOAD_PARTS, MAX_UPLOAD_REQUEST_BYTES, MESSAGES_SCHEMA, MSG, MSG_AWARENESS, MSG_SYNC, MSG_SYNC_STEP1, MSG_SYNC_STEP2, MSG_SYNC_UPDATE, MSG_YJS_AWARENESS, MSG_YJS_SYNC, type MessageHandlers, type MsgType, type MutateActionData, type NormalizedApiError, PLANES, PRODUCTION_OFFICIAL_AGENT_ORIGINS, type PermissionContext, type PermissionLevel, type PlaneName, type PlaneUrls, type PlatformWorkerEnv, type PrefixResult, type PresencePeer, PresenceRoom, type ProtocolMessage, type ProxyRoute, type QueryActionData, REACTIONS_SCHEMA, READ_RECEIPTS_SCHEMA, RECORD_NOT_FOUND, REGISTRY_INSTANCE, RELEASE_STAMP_PATH, REPO_STORE_LIMIT_BYTES, REQUIRED_COMPATIBILITY_FLAGS, RESERVED_BINDING_NAMES, RESERVED_RUN_WORKER_FIRST, RESOURCE_ID_RE, ROLE_ADMIN, ROLE_ANONYMOUS, ROLE_DEFAULT, RecordRoom, type RecordRoomConfig, RegistryClientError, type ResolvedColumn, type ResolvedDeepSpaceAgentModel, type RolePermissions, type RunMigrationsResult, SANDBOX_SCOPES, SANDBOX_SCOPE_HEADER, SDK_RUN_WORKER_FIRST, SECRET_NAME_RE, SECURE_ROOM_BOUNDARIES_MIGRATION_ID, SESSION_COOKIE, STRICT_APP_ID_RE, SYSTEM_ASSIGNED_COLUMNS, SYSTEM_COLLECTIONS, SYSTEM_MAINTAINED_COLUMNS, SYSTEM_MANAGED_COLUMNS, type SandboxFile, SandboxFileError, type SandboxFileList, type SandboxFilesClient, type SandboxFilesOptions, type SandboxScope, type SandboxUploadOptions, SchemaRegistry, type ScopeContext, type ScopedKnowledgeClient, type ScopedR2Auth, type ScopedR2Config, type ScopedR2Handler, type ScreenshotEnv, type ScreenshotResult, type ServerMessage, type StorageAdmission, type StoredRecord, type Summarizer, type SyncResult, TEST_ACCOUNT_EMAIL_SUFFIX, type TokenDebugInfo, type ToolResult, type ToolSchema, UNDECLARED_ASSET_NOT_FOUND_HANDLING, UPLOAD_PART_BYTES, USERS_COLUMNS, type User, type UserAttachment, type UserToolExecutorEnv, type ValidationError, type VerifiedAuth, type VerifyOutcome, type VerifyResult, type Viewport, WORKER_OWNED_NOT_FOUND_MIGRATION_ID, YjsRoom, agentTokenIssuer, apiWorkerFetch, appendMessage, applyAiToolDefaults, applySlidingWindow, armCronRoom, authWorkerFetch, authenticatedRoomRequest, bindingManifestFromOutputConfig, buildCronContext, buildTableSelect, buildUiParts, callRegistry, callableFromSandbox, canCreate, canDelete, canRead, canUpdate, capToolResultSize, captureScreenshot, checkFieldPermissions, checkUnclaimedOwnerTransition, clientBuild, codeExecutionTool, coerceValue, collectionTableName, columnId, composioTools, computeDoMigration, computeHmacHex, createChat, createDecoder, createDeepSpaceAI, createDeepSpaceAuth, createEncoder, createScopedR2Handler, createUserToolExecutor, dataToColumnValues, decodeJwtPayload, deepSpaceAgentErrorSummary, deleteChatCascade, dispatch, encode, encodeAwarenessMessage, encodeSyncStep1, encodeSyncStep2, encodeUpdate, enqueueJob, epochSeconds, folderFilter, formatBytes, forwardSandboxContainer, getChat, getDeepSpaceAIModel, getMessageType, getRolePermissions, handleAwarenessMessage, handleSyncMessage, hasContent, isAnonymousUserId, isAssetNotFoundHandling, isAutoProvision, isFilesVerbPath, isOwner, isPlatformReservedPath, isRestrictedPlane, isTestAccountClaims, isTestAccountEmail, isTestAccountTier, knowledge, lintSchema, lintSchemaSet, lintSchemas, listDeepSpaceAgentModels, loadMessages, logEventText, loggableError, makeDefaultSummarizer, matchOrigin, meterAi, meterUsage, meterVectorize, mintAppId, mintUlid, nativeAuthCallback, nativeAuthExchange, nativeAuthMe, nativeAuthSignOut, nativeAuthStart, nativeAuthToken, noopPermissionContext, normalizeAgentTargetOrigin, normalizeApiError, normalizeKnowledgeFolder, parseQuotaLimit, platformCorsPatterns, platformWorkerFetch, prepareMessagesWithCompaction, priceBindingUsageEvent, readBoundedBodyText, readVarUint, readVarUint8Array, registerAgentToolRoutes, registerDeepSpaceDocumentation, registryErrorJson, registryStub, resolveAppMembership, resolveAppName, resolveAppRole, resolveColumn, resolveCompatibilityFlags, resolveDeepSpaceAgentModel, resolveSessionReadAuth, reuseSandbox, rowToData, runMigrations, sandboxFiles, sandboxOutputs, sandboxUpload, serverBuild, storageLimitForTier, storageQuotaMessage, streamDeepSpaceAgent, timingSafeEqualHex, timingSafeEqualStrings, toUint8Array, totalChars, truncateLogText, truncateOldToolResults, turnsToCoreMessages, unwrapToolOutput, updateChat, validateAppMigrationIds, validateAppName, validateBindingManifest, validateDoManifest, verifyAgentToken, verifyJwt, workerErrorHandler, writeVarUint, writeVarUint8Array };
package/dist/worker.js CHANGED
@@ -6059,6 +6059,52 @@ var BROWSER_PROXY_ROUTES = [
6059
6059
  var SANDBOX_SCOPES = ["user", "app"];
6060
6060
  var SANDBOX_SCOPE_HEADER = "x-deepspace-sandbox-scope";
6061
6061
 
6062
+ // src/shared/planes.ts
6063
+ var PLANES = {
6064
+ production: {
6065
+ auth: "https://auth.deep.space",
6066
+ api: "https://api-worker.deep.space",
6067
+ platform: "https://platform-worker.deep.space",
6068
+ deploy: "https://deploy-worker.deep.space",
6069
+ appDomain: "app.space",
6070
+ platformDomain: "deep.space",
6071
+ home: "https://deep.space",
6072
+ dashboard: "https://dashboard.deep.space",
6073
+ privacyPolicy: "https://deep.space/privacy"
6074
+ },
6075
+ // Services on deepspacesites.com, apps on spacestest.com: production's
6076
+ // two-zone split, because a Worker cannot fetch one in its own zone.
6077
+ staging: {
6078
+ auth: "https://auth.deepspacesites.com",
6079
+ api: "https://api.deepspacesites.com",
6080
+ platform: "https://platform.deepspacesites.com",
6081
+ deploy: "https://deploy.deepspacesites.com",
6082
+ appDomain: "spacestest.com",
6083
+ platformDomain: "deepspacesites.com",
6084
+ home: "https://spacestest.com",
6085
+ dashboard: "https://dashboard.deepspacesites.com",
6086
+ privacyPolicy: "https://deep.space/privacy"
6087
+ },
6088
+ // Services on deepspacemedical.com, apps on deepspacemedical.app: two zones
6089
+ // in the medical Cloudflare account. The plane has no dashboard.
6090
+ medical: {
6091
+ auth: "https://auth.deepspacemedical.com",
6092
+ api: "https://api.deepspacemedical.com",
6093
+ platform: "https://platform.deepspacemedical.com",
6094
+ deploy: "https://deploy.deepspacemedical.com",
6095
+ appDomain: "deepspacemedical.app",
6096
+ platformDomain: "deepspacemedical.com",
6097
+ home: "https://deepspacemedical.com",
6098
+ dashboard: null,
6099
+ privacyPolicy: "https://deepspacemedical.com/privacy"
6100
+ }
6101
+ };
6102
+ var OPEN_PLANES = /* @__PURE__ */ new Set(["production", "staging"]);
6103
+ function isRestrictedPlane(env) {
6104
+ const plane = env.DEEPSPACE_PLANE;
6105
+ return plane !== void 0 && plane !== "" && !OPEN_PLANES.has(plane);
6106
+ }
6107
+
6062
6108
  // src/server/utils/cors.ts
6063
6109
  function matchOrigin(origin, patterns) {
6064
6110
  if (!origin) return null;
@@ -11141,6 +11187,7 @@ export {
11141
11187
  MSG_SYNC_UPDATE,
11142
11188
  MSG_YJS_AWARENESS,
11143
11189
  MSG_YJS_SYNC,
11190
+ PLANES,
11144
11191
  PRODUCTION_OFFICIAL_AGENT_ORIGINS,
11145
11192
  PresenceRoom,
11146
11193
  REACTIONS_SCHEMA,
@@ -11242,6 +11289,7 @@ export {
11242
11289
  isFilesVerbPath,
11243
11290
  isOwner,
11244
11291
  isPlatformReservedPath,
11292
+ isRestrictedPlane,
11245
11293
  isTestAccountClaims,
11246
11294
  isTestAccountEmail,
11247
11295
  isTestAccountTier,