deepspace 0.27.0 → 0.28.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.
Files changed (53) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/README.md +19 -8
  3. package/dist/{chunk-SF6GNPYL.js → chunk-652TIGI4.js} +2 -2
  4. package/dist/{chunk-SF6GNPYL.js.map → chunk-652TIGI4.js.map} +1 -1
  5. package/dist/cli.js +1819 -1068
  6. package/dist/cli.js.map +1 -1
  7. package/dist/documentation-client-core.js +1 -1
  8. package/dist/documentation-default-renderer.cjs +1 -1
  9. package/dist/documentation-react.js +1 -1
  10. package/dist/documentation-runtime.js +1 -1
  11. package/dist/documentation-server-core.js +1 -1
  12. package/dist/documentation.d.ts +0 -1
  13. package/dist/documentation.js +41 -21
  14. package/dist/documentation.js.map +1 -1
  15. package/dist/feature-installer.js +27 -61
  16. package/dist/feature-installer.js.map +1 -1
  17. package/dist/index.d.ts +6 -55
  18. package/dist/index.js +4 -20
  19. package/dist/index.js.map +1 -1
  20. package/dist/schema.js.map +1 -1
  21. package/dist/server.d.ts +8 -39
  22. package/dist/server.js +16 -64
  23. package/dist/server.js.map +1 -1
  24. package/dist/testing.js +2 -2
  25. package/dist/testing.js.map +1 -1
  26. package/dist/worker.d.ts +7 -80
  27. package/dist/worker.js +17 -69
  28. package/dist/worker.js.map +1 -1
  29. package/features/admin-page/feature.json +0 -1
  30. package/features/ai-chat/feature.json +0 -1
  31. package/features/canvas/feature.json +0 -1
  32. package/features/cron/feature.json +0 -1
  33. package/features/documents/feature.json +0 -1
  34. package/features/file-attachments/feature.json +0 -1
  35. package/features/file-manager/feature.json +0 -1
  36. package/features/integration-test/feature.json +0 -1
  37. package/features/items/feature.json +0 -1
  38. package/features/kanban/feature.json +0 -1
  39. package/features/landing/feature.json +0 -1
  40. package/features/leaderboard/feature.json +0 -1
  41. package/features/messaging/feature.json +0 -1
  42. package/features/search-bar/feature.json +0 -1
  43. package/features/tasks/feature.json +0 -1
  44. package/features/testing/feature.json +0 -1
  45. package/features/topbar/feature.json +1 -4
  46. package/features/tree/feature.json +1 -4
  47. package/package.json +1 -1
  48. package/dist/chunk-Q5YJGF5Z.js +0 -769
  49. package/dist/chunk-Q5YJGF5Z.js.map +0 -1
  50. package/dist/repo-api-7GDPVSFV.js +0 -14
  51. package/dist/repo-api-7GDPVSFV.js.map +0 -1
  52. package/features/presence-test/feature.json +0 -22
  53. package/features/presence-test/src/PresenceTestPage.tsx +0 -120
package/dist/testing.js CHANGED
@@ -73,7 +73,7 @@ function findTestAccountByName(name) {
73
73
  if (!match) {
74
74
  throw new Error(
75
75
  `No test account named "${name}" is usable from ${TEST_ACCOUNTS_PATH}.
76
- If it exists in the pool but was created on another machine, its password is not stored here \u2014 re-issue it:
76
+ If it exists in the pool but was created on another machine, fetch its credential:
77
77
  deepspace test accounts recover --all (note: --all only covers accounts missing a local credential; recover a stale one by its email)
78
78
  Otherwise create it:
79
79
  deepspace test accounts create --email ${testAccountEmailSlug(name)}@deepspace.test --password <pw> --name "${name}"
@@ -169,7 +169,7 @@ function formatSignInFailure(email, failure) {
169
169
  }
170
170
  const code = failure.code ? ` ${failure.code}` : "";
171
171
  const detail = failure.message ? `: ${failure.message}` : "";
172
- const nextStep = failure.code === "INVALID_ORIGIN" ? "Add this app origin to the auth allowlist, then retry." : "The stored password may be stale \u2014 re-issue it 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`.";
172
+ const nextStep = failure.code === "INVALID_ORIGIN" ? "Add this app origin to the auth allowlist, then retry." : "The stored password may be stale \u2014 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`.";
173
173
  return `Sign-in failed for ${email} (HTTP ${failure.status}${code})${detail}. ${nextStep}`;
174
174
  }
175
175
  async function ensureStorageState(browser, account, baseURL, options = {}) {
@@ -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, its password is not ` +\n `stored here — re-issue it:\\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 — re-issue it 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/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"]}
package/dist/worker.d.ts CHANGED
@@ -69,11 +69,6 @@ declare const MSG: {
69
69
  readonly PRESENCE_JOIN: "presence.join";
70
70
  readonly PRESENCE_LEAVE: "presence.leave";
71
71
  readonly PRESENCE_UPDATE: "presence.update";
72
- readonly GW_SCOPE_CONNECT: "gateway.scope_connect";
73
- readonly GW_SCOPE_DISCONNECT: "gateway.scope_disconnect";
74
- readonly GW_SCOPE_ERROR: "gateway.scope_error";
75
- readonly GW_TOKEN_REFRESH: "gateway.token_refresh";
76
- readonly GW_USER_UPDATE: "gateway.user_update";
77
72
  };
78
73
  /**
79
74
  * Type of any message type constant — the union of every string literal
@@ -121,7 +116,7 @@ declare const RECORD_NOT_FOUND = "Record not found";
121
116
  * runtime. This module pairs every constant with its payload type, so
122
117
  * that:
123
118
  *
124
- * 1. Building a message with `clientBuild.gameInput(...)` is payload-
119
+ * 1. Building a message with `clientBuild.canvasAdd(...)` is payload-
125
120
  * checked at the call site — the compiler refuses to ship a wrong
126
121
  * shape.
127
122
  *
@@ -170,7 +165,7 @@ declare const RECORD_NOT_FOUND = "Record not found";
170
165
  *
171
166
  * Where payload shapes are stable + narrow (ids, flags), we type them
172
167
  * precisely. Where they're opaque or escape the protocol layer (record
173
- * data blobs, Yjs binary frames, game-engine state), we use `unknown` and
168
+ * data blobs, Yjs binary frames, canvas shapes), we use `unknown` and
174
169
  * defer narrowing to the caller. This is intentional: over-typing opaque
175
170
  * payloads would require the protocol layer to import application types
176
171
  * and defeat the "thin wire contract" goal.
@@ -178,7 +173,7 @@ declare const RECORD_NOT_FOUND = "Record not found";
178
173
 
179
174
  /**
180
175
  * The outer shape of every wire message. `T` is the string discriminator
181
- * (e.g. `"game.input"`) — keeping it as a generic literal type lets the
176
+ * (e.g. `"canvas.add"`) — keeping it as a generic literal type lets the
182
177
  * discriminated-union narrowing in `dispatch()` pick the right payload.
183
178
  *
184
179
  * Callers extending the protocol should pass a string-literal type for
@@ -256,15 +251,7 @@ type ClientMessage = BaseMessage<typeof MSG.SUBSCRIBE, {
256
251
  jobId: string;
257
252
  }> | BaseMessage<typeof MSG.JOB_RETRY, {
258
253
  jobId: string;
259
- }> | BaseMessage<typeof MSG.PRESENCE_UPDATE, Record<string, unknown>> | BaseMessage<typeof MSG.GW_SCOPE_CONNECT, {
260
- scopeType: string;
261
- scopeId: string;
262
- }> | BaseMessage<typeof MSG.GW_SCOPE_DISCONNECT, {
263
- scopeType: string;
264
- scopeId: string;
265
- }> | BaseMessage<typeof MSG.GW_TOKEN_REFRESH, {
266
- token: string;
267
- }>;
254
+ }> | BaseMessage<typeof MSG.PRESENCE_UPDATE, Record<string, unknown>>;
268
255
  /**
269
256
  * Every message the server can send. Room and handler `send` / `broadcast`
270
257
  * signatures are tightened to this union so outbound payloads are
@@ -358,11 +345,7 @@ type ServerMessage = BaseMessage<typeof MSG.QUERY_RESULT, {
358
345
  }> | BaseMessage<typeof MSG.PRESENCE_UPDATE, {
359
346
  userId: string;
360
347
  state: Record<string, unknown>;
361
- }> | BaseMessage<typeof MSG.GW_SCOPE_ERROR, {
362
- scopeType: string;
363
- scopeId: string;
364
- error: string;
365
- }> | BaseMessage<typeof MSG.GW_USER_UPDATE, unknown>;
348
+ }>;
366
349
  /**
367
350
  * Any wire message, regardless of direction. Use this only when the code
368
351
  * path genuinely doesn't care — otherwise pick `ClientMessage` or
@@ -529,26 +512,6 @@ declare const clientBuild: {
529
512
  readonly type: "presence.update";
530
513
  readonly payload: Record<string, unknown>;
531
514
  };
532
- gwScopeConnect: (scopeType: string, scopeId: string) => {
533
- readonly type: "gateway.scope_connect";
534
- readonly payload: {
535
- readonly scopeType: string;
536
- readonly scopeId: string;
537
- };
538
- };
539
- gwScopeDisconnect: (scopeType: string, scopeId: string) => {
540
- readonly type: "gateway.scope_disconnect";
541
- readonly payload: {
542
- readonly scopeType: string;
543
- readonly scopeId: string;
544
- };
545
- };
546
- gwTokenRefresh: (token: string) => {
547
- readonly type: "gateway.token_refresh";
548
- readonly payload: {
549
- readonly token: string;
550
- };
551
- };
552
515
  };
553
516
  /** Factories for every server → client message. Used by room DOs and
554
517
  * handler modules so server broadcasts are payload-checked too. Opaque
@@ -792,18 +755,6 @@ declare const serverBuild: {
792
755
  readonly state: Record<string, unknown>;
793
756
  };
794
757
  };
795
- gwScopeError: (scopeType: string, scopeId: string, error: string) => {
796
- readonly type: "gateway.scope_error";
797
- readonly payload: {
798
- readonly scopeType: string;
799
- readonly scopeId: string;
800
- readonly error: string;
801
- };
802
- };
803
- gwUserUpdate: (user: unknown) => {
804
- readonly type: "gateway.user_update";
805
- readonly payload: unknown;
806
- };
807
758
  };
808
759
  /**
809
760
  * Handler map keyed by a message's string `type`. Every key maps to the
@@ -833,7 +784,7 @@ declare function dispatch<M extends BaseMessage<string, unknown> = ProtocolMessa
833
784
  /**
834
785
  * Serialise a typed-built message for `ws.send`. A tiny wrapper around
835
786
  * `JSON.stringify` kept around so call sites read as
836
- * `ws.send(encode(clientBuild.gameInput(...)))` rather than paying
787
+ * `ws.send(encode(clientBuild.canvasAdd(...)))` rather than paying
837
788
  * attention to stringify arguments.
838
789
  */
839
790
  declare function encode<M extends BaseMessage<string, unknown>>(message: M): string;
@@ -1282,30 +1233,6 @@ declare class SchemaRegistry {
1282
1233
  all(): CollectionSchema[];
1283
1234
  names(): string[];
1284
1235
  }
1285
- type PermissionSource = 'explicit' | 'wildcard' | 'default-deny';
1286
- interface ResolvedPermission {
1287
- level: PermissionLevel | boolean;
1288
- source: PermissionSource;
1289
- }
1290
- interface CollectionPermissionSummary {
1291
- collection: string;
1292
- ownerField?: string;
1293
- collaboratorsField?: string;
1294
- teamField?: string;
1295
- columns: ColumnDefinition[];
1296
- permissions: Record<string, {
1297
- read: ResolvedPermission;
1298
- create: ResolvedPermission;
1299
- update: ResolvedPermission;
1300
- delete: ResolvedPermission;
1301
- writableFields?: string[];
1302
- }>;
1303
- }
1304
- interface PermissionAnalysis {
1305
- roles: string[];
1306
- collections: CollectionPermissionSummary[];
1307
- }
1308
- declare function analyzePermissions(schemas: CollectionSchema[]): PermissionAnalysis;
1309
1236
 
1310
1237
  /**
1311
1238
  * RecordRoom Durable Object
@@ -5382,4 +5309,4 @@ interface AppLogsResponse {
5382
5309
  declare const APP_LOG_EVENT_KEYS: readonly ["id", "timestamp", "level", "eventType", "message", "source", "outcome", "request", "exception"];
5383
5310
  type AppLogEventKey = (typeof APP_LOG_EVENT_KEYS)[number];
5384
5311
 
5385
- export { ACCOUNT_STORAGE_LIMIT_BYTES, ACTION_ROUTES_BEARER_GUARD_MIGRATION_ID, ACTION_TOOLS_DELETE_WHERE_MIGRATION_ID, AI_CHATS_SCHEMA, AI_MESSAGES_SCHEMA, 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 ApiWorkerEnv, type AppLogEvent, type AppLogEventKey, type AppLogsResponse, 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, CHANNELS_SCHEMA, CHANNEL_MEMBERS_SCHEMA, CLIENT_LOG_MARKER, COST_RATES, CanvasRoom, type CanvasShape, type ChatContextConfig, type ChatMessageRow, type ChatRow, type ChatTurn, type ClientErrorKind, type ClientErrorReport, type ClientMessage, type CollectionPermissionSummary, 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 DoMigrationDirective, type DoMigrationPlan, type ExistingDOBinding, 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_DELETE_WHERE_LIMIT, MAX_DEPLOY_ASSET_FILE_BYTES, MAX_ENV_VAR_BYTES, 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 PermissionAnalysis, type PermissionContext, type PermissionLevel, type PermissionSource, 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 ResolvedPermission, type RolePermissions, type RunMigrationsResult, SDK_RUN_WORKER_FIRST, SECURE_ROOM_BOUNDARIES_MIGRATION_ID, STRICT_APP_ID_RE, SYSTEM_ASSIGNED_COLUMNS, SYSTEM_COLLECTIONS, SYSTEM_MAINTAINED_COLUMNS, SYSTEM_MANAGED_COLUMNS, 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 ValidationError, type VerifiedAuth, type VerifyOutcome, type VerifyResult, type Viewport, WORKER_OWNED_NOT_FOUND_MIGRATION_ID, YjsRoom, analyzePermissions, apiWorkerFetch, appendMessage, applyAiToolDefaults, applySlidingWindow, armCronRoom, authWorkerFetch, authenticatedRoomRequest, bindingManifestFromOutputConfig, buildCronContext, buildTableSelect, buildUiParts, callRegistry, canCreate, canDelete, canRead, canUpdate, capToolResultSize, captureScreenshot, checkFieldPermissions, checkUnclaimedOwnerTransition, clientBuild, coerceValue, collectionTableName, columnId, composioTools, computeDoMigration, computeHmacHex, createChat, createDecoder, createDeepSpaceAI, createDeepSpaceAuth, createEncoder, createScopedR2Handler, dataToColumnValues, decodeJwtPayload, deepSpaceAgentErrorSummary, deleteChatCascade, dispatch, encode, encodeAwarenessMessage, encodeSyncStep1, encodeSyncStep2, encodeUpdate, enqueueJob, epochSeconds, folderFilter, formatBytes, getChat, getDeepSpaceAIModel, getMessageType, getRolePermissions, handleAwarenessMessage, handleClientErrorReport, handleSyncMessage, hasContent, isAnonymousUserId, isAssetNotFoundHandling, isAutoProvision, isFilesVerbPath, isOwner, isPlatformReservedPath, isTestAccountClaims, isTestAccountEmail, isTestAccountTier, knowledge, lintSchema, lintSchemaSet, lintSchemas, listDeepSpaceAgentModels, loadMessages, makeDefaultSummarizer, matchOrigin, meterAi, meterUsage, meterVectorize, mintAppId, mintUlid, noopPermissionContext, normalizeKnowledgeFolder, parseQuotaLimit, platformCorsPatterns, platformWorkerFetch, prepareMessagesWithCompaction, priceBindingUsageEvent, readVarUint, readVarUint8Array, registerClientErrorRoute, registerDeepSpaceDocumentation, registryErrorJson, registryStub, resolveAppName, resolveAppRole, resolveColumn, resolveCompatibilityFlags, resolveDeepSpaceAgentModel, rowToData, runMigrations, serverBuild, storageLimitForTier, storageQuotaMessage, streamDeepSpaceAgent, timingSafeEqualHex, timingSafeEqualStrings, toUint8Array, totalChars, truncateOldToolResults, turnsToCoreMessages, unwrapToolOutput, updateChat, validateAppMigrationIds, validateAppName, validateBindingManifest, validateDoManifest, verifyJwt, writeVarUint, writeVarUint8Array };
5312
+ export { ACCOUNT_STORAGE_LIMIT_BYTES, ACTION_ROUTES_BEARER_GUARD_MIGRATION_ID, ACTION_TOOLS_DELETE_WHERE_MIGRATION_ID, AI_CHATS_SCHEMA, AI_MESSAGES_SCHEMA, 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 ApiWorkerEnv, type AppLogEvent, type AppLogEventKey, type AppLogsResponse, 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, CHANNELS_SCHEMA, CHANNEL_MEMBERS_SCHEMA, CLIENT_LOG_MARKER, COST_RATES, CanvasRoom, type CanvasShape, type ChatContextConfig, type ChatMessageRow, type ChatRow, type ChatTurn, type ClientErrorKind, type ClientErrorReport, 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 DoMigrationDirective, type DoMigrationPlan, type ExistingDOBinding, 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_DELETE_WHERE_LIMIT, MAX_DEPLOY_ASSET_FILE_BYTES, MAX_ENV_VAR_BYTES, 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 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, SDK_RUN_WORKER_FIRST, SECURE_ROOM_BOUNDARIES_MIGRATION_ID, STRICT_APP_ID_RE, SYSTEM_ASSIGNED_COLUMNS, SYSTEM_COLLECTIONS, SYSTEM_MAINTAINED_COLUMNS, SYSTEM_MANAGED_COLUMNS, 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 ValidationError, type VerifiedAuth, type VerifyOutcome, type VerifyResult, type Viewport, WORKER_OWNED_NOT_FOUND_MIGRATION_ID, YjsRoom, apiWorkerFetch, appendMessage, applyAiToolDefaults, applySlidingWindow, armCronRoom, authWorkerFetch, authenticatedRoomRequest, bindingManifestFromOutputConfig, buildCronContext, buildTableSelect, buildUiParts, callRegistry, canCreate, canDelete, canRead, canUpdate, capToolResultSize, captureScreenshot, checkFieldPermissions, checkUnclaimedOwnerTransition, clientBuild, coerceValue, collectionTableName, columnId, composioTools, computeDoMigration, computeHmacHex, createChat, createDecoder, createDeepSpaceAI, createDeepSpaceAuth, createEncoder, createScopedR2Handler, dataToColumnValues, decodeJwtPayload, deepSpaceAgentErrorSummary, deleteChatCascade, dispatch, encode, encodeAwarenessMessage, encodeSyncStep1, encodeSyncStep2, encodeUpdate, enqueueJob, epochSeconds, folderFilter, formatBytes, getChat, getDeepSpaceAIModel, getMessageType, getRolePermissions, handleAwarenessMessage, handleClientErrorReport, handleSyncMessage, hasContent, isAnonymousUserId, isAssetNotFoundHandling, isAutoProvision, isFilesVerbPath, isOwner, isPlatformReservedPath, isTestAccountClaims, isTestAccountEmail, isTestAccountTier, knowledge, lintSchema, lintSchemaSet, lintSchemas, listDeepSpaceAgentModels, loadMessages, makeDefaultSummarizer, matchOrigin, meterAi, meterUsage, meterVectorize, mintAppId, mintUlid, noopPermissionContext, normalizeKnowledgeFolder, parseQuotaLimit, platformCorsPatterns, platformWorkerFetch, prepareMessagesWithCompaction, priceBindingUsageEvent, readVarUint, readVarUint8Array, registerClientErrorRoute, registerDeepSpaceDocumentation, registryErrorJson, registryStub, resolveAppName, resolveAppRole, resolveColumn, resolveCompatibilityFlags, resolveDeepSpaceAgentModel, rowToData, runMigrations, serverBuild, storageLimitForTier, storageQuotaMessage, streamDeepSpaceAgent, timingSafeEqualHex, timingSafeEqualStrings, toUint8Array, totalChars, truncateOldToolResults, turnsToCoreMessages, unwrapToolOutput, updateChat, validateAppMigrationIds, validateAppName, validateBindingManifest, validateDoManifest, verifyJwt, writeVarUint, writeVarUint8Array };
package/dist/worker.js CHANGED
@@ -15,7 +15,7 @@ var MSG = {
15
15
  USER_UPDATE: "user.update",
16
16
  // ---- Auth signal -----------------------------------------------------
17
17
  // Server → client one-shot on connect from rooms that enforce role-based
18
- // writes (Canvas, Game, Cron). Tells the client whether this connection
18
+ // writes (Canvas, Cron). Tells the client whether this connection
19
19
  // can write so its hook can disable local write APIs and the UI can
20
20
  // render controls accordingly. Without this, a viewer's click would
21
21
  // round-trip to the server and silently come back as ERROR.
@@ -59,13 +59,7 @@ var MSG = {
59
59
  PRESENCE_SYNC: "presence.sync",
60
60
  PRESENCE_JOIN: "presence.join",
61
61
  PRESENCE_LEAVE: "presence.leave",
62
- PRESENCE_UPDATE: "presence.update",
63
- // ---- Gateway multiplexing --------------------------------------------
64
- GW_SCOPE_CONNECT: "gateway.scope_connect",
65
- GW_SCOPE_DISCONNECT: "gateway.scope_disconnect",
66
- GW_SCOPE_ERROR: "gateway.scope_error",
67
- GW_TOKEN_REFRESH: "gateway.token_refresh",
68
- GW_USER_UPDATE: "gateway.user_update"
62
+ PRESENCE_UPDATE: "presence.update"
69
63
  };
70
64
  var MSG_YJS_SYNC = 22;
71
65
  var MSG_YJS_AWARENESS = 23;
@@ -695,47 +689,6 @@ var SchemaRegistry = class {
695
689
  return Array.from(this.trusted.keys());
696
690
  }
697
691
  };
698
- function analyzePermissions(schemas) {
699
- const roleSet = /* @__PURE__ */ new Set();
700
- for (const schema of schemas) {
701
- for (const role of Object.keys(schema.permissions)) roleSet.add(role);
702
- }
703
- roleSet.delete("*");
704
- const roles = Array.from(roleSet);
705
- const collections = schemas.map((schema) => {
706
- const permissions = {};
707
- for (const role of roles) {
708
- let source;
709
- let perms;
710
- if (schema.permissions[role]) {
711
- source = "explicit";
712
- perms = schema.permissions[role];
713
- } else if (schema.permissions["*"]) {
714
- source = "wildcard";
715
- perms = schema.permissions["*"];
716
- } else {
717
- source = "default-deny";
718
- perms = { read: false, create: false, update: false, delete: false };
719
- }
720
- permissions[role] = {
721
- read: { level: perms.read, source },
722
- create: { level: perms.create, source },
723
- update: { level: perms.update, source },
724
- delete: { level: perms.delete, source },
725
- writableFields: perms.writableFields
726
- };
727
- }
728
- return {
729
- collection: schema.name,
730
- ownerField: schema.ownerField,
731
- collaboratorsField: schema.collaboratorsField,
732
- teamField: schema.teamField,
733
- columns: schema.columns,
734
- permissions
735
- };
736
- });
737
- return { roles, collections };
738
- }
739
692
 
740
693
  // src/server/rooms/collection-table-migration.ts
741
694
  var NOOP_LOGGER = { warn: () => {
@@ -1222,11 +1175,7 @@ var clientBuild = {
1222
1175
  jobCancel: (jobId) => ({ type: MSG.JOB_CANCEL, payload: { jobId } }),
1223
1176
  jobRetry: (jobId) => ({ type: MSG.JOB_RETRY, payload: { jobId } }),
1224
1177
  // Presence
1225
- presenceUpdate: (state) => ({ type: MSG.PRESENCE_UPDATE, payload: state }),
1226
- // Gateway
1227
- gwScopeConnect: (scopeType, scopeId) => ({ type: MSG.GW_SCOPE_CONNECT, payload: { scopeType, scopeId } }),
1228
- gwScopeDisconnect: (scopeType, scopeId) => ({ type: MSG.GW_SCOPE_DISCONNECT, payload: { scopeType, scopeId } }),
1229
- gwTokenRefresh: (token) => ({ type: MSG.GW_TOKEN_REFRESH, payload: { token } })
1178
+ presenceUpdate: (state) => ({ type: MSG.PRESENCE_UPDATE, payload: state })
1230
1179
  };
1231
1180
  var serverBuild = {
1232
1181
  // Records / CRUD
@@ -1290,13 +1239,7 @@ var serverBuild = {
1290
1239
  presenceSync: (peers) => ({ type: MSG.PRESENCE_SYNC, payload: { peers } }),
1291
1240
  presenceJoin: (peer) => ({ type: MSG.PRESENCE_JOIN, payload: { peer } }),
1292
1241
  presenceLeave: (userId) => ({ type: MSG.PRESENCE_LEAVE, payload: { userId } }),
1293
- presenceUpdate: (userId, state) => ({ type: MSG.PRESENCE_UPDATE, payload: { userId, state } }),
1294
- // Gateway
1295
- gwScopeError: (scopeType, scopeId, error) => ({
1296
- type: MSG.GW_SCOPE_ERROR,
1297
- payload: { scopeType, scopeId, error }
1298
- }),
1299
- gwUserUpdate: (user) => ({ type: MSG.GW_USER_UPDATE, payload: user })
1242
+ presenceUpdate: (userId, state) => ({ type: MSG.PRESENCE_UPDATE, payload: { userId, state } })
1300
1243
  };
1301
1244
  function dispatch(raw, handlers) {
1302
1245
  let msg;
@@ -1402,7 +1345,7 @@ function handleDelete(ctx, ws, attachment, payload) {
1402
1345
  ctx.send(ws, serverBuild.error(result.error));
1403
1346
  }
1404
1347
  }
1405
- function putRecord(ctx, collection, recordId, data, userId, userRole, skipUserRbac = false, systemUpdate = false) {
1348
+ function putRecord(ctx, collection, recordId, data, userId, userRole, skipUserRbac = false, systemUpdate = false, updateOnly = false) {
1406
1349
  const resolved = resolveCollection(ctx, collection);
1407
1350
  if (!resolved.ok) return { success: false, error: resolved.error };
1408
1351
  const schema = resolved.schema;
@@ -1420,6 +1363,12 @@ function putRecord(ctx, collection, recordId, data, userId, userRole, skipUserRb
1420
1363
  }
1421
1364
  const existing = getRecord(ctx.sql, collection, recordId, schema);
1422
1365
  const isUpdate = existing !== null;
1366
+ if (updateOnly && !isUpdate) {
1367
+ return {
1368
+ success: false,
1369
+ error: `records.update: no record "${recordId}" in ${collection} \u2014 updates never create; use records.create.`
1370
+ };
1371
+ }
1423
1372
  const mergedData = isUpdate ? { ...existing.data, ...data } : { ...data };
1424
1373
  for (const col of columns) {
1425
1374
  if (col.readonly) continue;
@@ -2578,7 +2527,7 @@ async function executeTool(ctx, toolName, params, userId, userRole, skipUserRbac
2578
2527
  if (!collection) return { success: false, error: "Missing required param: collection" };
2579
2528
  if (!recordId) return { success: false, error: "Missing required param: recordId" };
2580
2529
  if (!data) return { success: false, error: "Missing required param: data" };
2581
- return putRecord(ctx, collection, recordId, data, userId, userRole, skipUserRbac);
2530
+ return putRecord(ctx, collection, recordId, data, userId, userRole, skipUserRbac, false, true);
2582
2531
  }
2583
2532
  case "records.delete": {
2584
2533
  const collection = params.collection;
@@ -6243,10 +6192,10 @@ async function writeQuotaSummary(bucket, key, previousEtag, summary) {
6243
6192
  });
6244
6193
  return object ? { etag: object.etag, summary } : null;
6245
6194
  }
6246
- async function ensureQuotaSummary(bucket, storage, prefixes, forceMeasure = false, excludedMarkerKey) {
6195
+ async function ensureQuotaSummary(bucket, storage, prefixes, measure = "reuse", excludedMarkerKey) {
6247
6196
  for (let attempt = 0; attempt < 4; attempt++) {
6248
6197
  const current = await readQuotaSummary(bucket, storage.quotaKey);
6249
- if (!forceMeasure && current.summary && accountPrefixesEqual(current.summary.prefixes, prefixes)) {
6198
+ if (measure !== "force" && current.summary && accountPrefixesEqual(current.summary.prefixes, prefixes) && !(measure === "repairDirty" && current.summary.dirty && Date.now() - current.summary.measuredAt >= QUOTA_REPAIR_COOLDOWN_MS)) {
6250
6199
  return current;
6251
6200
  }
6252
6201
  const usedBytes = await measuredAllocationUsage(bucket, storage, prefixes, excludedMarkerKey);
@@ -6434,7 +6383,7 @@ async function reserveQuotaSummary(bucket, storage, prefixes, initial, chargeByt
6434
6383
  bucket,
6435
6384
  storage,
6436
6385
  prefixes,
6437
- false,
6386
+ "reuse",
6438
6387
  markerKey ?? void 0
6439
6388
  );
6440
6389
  if (!ensured) {
@@ -6459,7 +6408,7 @@ async function reserveQuotaSummary(bucket, storage, prefixes, initial, chargeByt
6459
6408
  bucket,
6460
6409
  storage,
6461
6410
  prefixes,
6462
- true,
6411
+ "force",
6463
6412
  markerKey ?? void 0
6464
6413
  );
6465
6414
  if (!refreshed?.summary) {
@@ -6552,7 +6501,7 @@ async function storageUsage(bucket, storage) {
6552
6501
  try {
6553
6502
  const prefixes = await allocationPrefixes(storage);
6554
6503
  if (prefixes === null) return null;
6555
- const current = await ensureQuotaSummary(bucket, storage, prefixes);
6504
+ const current = await ensureQuotaSummary(bucket, storage, prefixes, "repairDirty");
6556
6505
  return current?.summary?.usedBytes ?? null;
6557
6506
  } catch (error) {
6558
6507
  console.error("[files:quota] usage:", error);
@@ -10355,7 +10304,6 @@ export {
10355
10304
  USERS_COLUMNS,
10356
10305
  WORKER_OWNED_NOT_FOUND_MIGRATION_ID,
10357
10306
  YjsRoom,
10358
- analyzePermissions,
10359
10307
  apiWorkerFetch,
10360
10308
  appendMessage,
10361
10309
  applyAiToolDefaults,