deepspace 0.22.1 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/dist/build.d.ts +115 -0
- package/dist/build.js +66 -0
- package/dist/build.js.map +1 -0
- package/dist/{chunk-XXOLIJB5.js → chunk-A7VKZRCB.js} +2 -2
- package/dist/{chunk-XXOLIJB5.js.map → chunk-A7VKZRCB.js.map} +1 -1
- package/dist/cli.js +219 -50
- package/dist/cli.js.map +1 -1
- package/dist/documentation-client-core.js +1 -1
- package/dist/documentation-default-renderer.cjs +1 -1
- package/dist/documentation-react.js +1 -1
- package/dist/documentation-runtime.js +1 -1
- package/dist/documentation-server-core.js +1 -1
- package/dist/documentation.js +1 -1
- package/dist/documentation.js.map +1 -1
- package/dist/index.d.ts +22 -6
- package/dist/index.js +23 -29
- package/dist/index.js.map +1 -1
- package/dist/schema.js +6 -0
- package/dist/schema.js.map +1 -1
- package/dist/server.d.ts +51 -12
- package/dist/server.js +193 -150
- package/dist/server.js.map +1 -1
- package/dist/testing.d.ts +2 -0
- package/dist/testing.js +21 -9
- package/dist/testing.js.map +1 -1
- package/dist/worker.d.ts +54 -12
- package/dist/worker.js +119 -75
- package/dist/worker.js.map +1 -1
- package/package.json +5 -1
package/dist/testing.d.ts
CHANGED
|
@@ -17,6 +17,8 @@ interface MultiplayerUser {
|
|
|
17
17
|
*/
|
|
18
18
|
type UsersFixture = (selector: number | string[], options?: {
|
|
19
19
|
label?: string;
|
|
20
|
+
/** Sign in again even if the cached session still validates. */
|
|
21
|
+
force?: boolean;
|
|
20
22
|
}) => Promise<MultiplayerUser[]>;
|
|
21
23
|
interface MultiplayerFixtures {
|
|
22
24
|
users: UsersFixture;
|
package/dist/testing.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { test as baseTest } from "@playwright/test";
|
|
3
3
|
|
|
4
4
|
// src/testing/accounts.ts
|
|
5
|
-
import { existsSync, mkdirSync, readFileSync
|
|
5
|
+
import { existsSync, mkdirSync, readFileSync } from "fs";
|
|
6
6
|
import { homedir } from "os";
|
|
7
7
|
import { join } from "path";
|
|
8
8
|
var TEST_ACCOUNTS_PATH = join(homedir(), ".deepspace", "test-accounts.json");
|
|
@@ -32,7 +32,9 @@ function readTestAccountStore() {
|
|
|
32
32
|
return { version: 2, scopes: {} };
|
|
33
33
|
}
|
|
34
34
|
const candidate = raw;
|
|
35
|
-
const scopes = candidate.scopes && typeof candidate.scopes === "object" && !Array.isArray(candidate.scopes) ? Object.fromEntries(
|
|
35
|
+
const scopes = candidate.scopes && typeof candidate.scopes === "object" && !Array.isArray(candidate.scopes) ? Object.fromEntries(
|
|
36
|
+
Object.entries(candidate.scopes).map(([key, value]) => [key, validTestAccounts(value)])
|
|
37
|
+
) : {};
|
|
36
38
|
return { version: 2, scopes };
|
|
37
39
|
} catch {
|
|
38
40
|
return { version: 2, scopes: {} };
|
|
@@ -53,19 +55,29 @@ function pickTestAccounts(count, options) {
|
|
|
53
55
|
if (sorted.length < count) {
|
|
54
56
|
const labelHint = options?.label ? ` (label="${options.label}")` : "";
|
|
55
57
|
throw new Error(
|
|
56
|
-
`Multiplayer test needs ${count} test accounts${labelHint}, but only ${sorted.length} are
|
|
58
|
+
`Multiplayer test needs ${count} test accounts${labelHint}, but only ${sorted.length} are usable from ${TEST_ACCOUNTS_PATH}.
|
|
59
|
+
The pool is global per developer, but passwords are stored only on the machine that created the account \u2014 so accounts created elsewhere are listed by \`deepspace test accounts list\` and count against the 10-cap, yet cannot be signed in as here. Re-issue their credentials, or create more:
|
|
60
|
+
deepspace test accounts recover --all (note: --all only covers accounts missing a local credential; recover a stale one by its email)
|
|
57
61
|
deepspace test accounts create --email <name>@deepspace.test --password <pw> --name "<name>"${options?.label ? ` --label ${options.label}` : ""}`
|
|
58
62
|
);
|
|
59
63
|
}
|
|
60
64
|
return sorted.slice(0, count);
|
|
61
65
|
}
|
|
66
|
+
function testAccountEmailSlug(name) {
|
|
67
|
+
const slug = name.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
68
|
+
return slug || "tester";
|
|
69
|
+
}
|
|
62
70
|
function findTestAccountByName(name) {
|
|
63
71
|
const all = loadAllTestAccounts();
|
|
64
72
|
const match = all.find((a) => a.name === name);
|
|
65
73
|
if (!match) {
|
|
66
74
|
throw new Error(
|
|
67
|
-
`No test account named "${name}"
|
|
68
|
-
|
|
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:
|
|
77
|
+
deepspace test accounts recover --all (note: --all only covers accounts missing a local credential; recover a stale one by its email)
|
|
78
|
+
Otherwise create it:
|
|
79
|
+
deepspace test accounts create --email ${testAccountEmailSlug(name)}@deepspace.test --password <pw> --name "${name}"
|
|
80
|
+
Suites that do not depend on a specific identity should ask for any N accounts with \`users(2)\` instead of naming them.`
|
|
69
81
|
);
|
|
70
82
|
}
|
|
71
83
|
return match;
|
|
@@ -154,7 +166,7 @@ async function signInAndSaveState(browser, account, baseURL, outPath) {
|
|
|
154
166
|
function formatSignInFailure(email, failure) {
|
|
155
167
|
const code = failure.code ? ` ${failure.code}` : "";
|
|
156
168
|
const detail = failure.message ? `: ${failure.message}` : "";
|
|
157
|
-
const nextStep = failure.code === "INVALID_ORIGIN" ? "Add this app origin to the auth allowlist, then retry." : "
|
|
169
|
+
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`.";
|
|
158
170
|
return `Sign-in failed for ${email} (HTTP ${failure.status}${code})${detail}. ${nextStep}`;
|
|
159
171
|
}
|
|
160
172
|
async function ensureStorageState(browser, account, baseURL, options = {}) {
|
|
@@ -181,8 +193,8 @@ async function newSignedInContext(browser, account, baseURL, options = {}) {
|
|
|
181
193
|
|
|
182
194
|
// src/testing/fixtures.ts
|
|
183
195
|
import { expect } from "@playwright/test";
|
|
184
|
-
async function buildUser(browser, account, baseURL) {
|
|
185
|
-
const statePath = await ensureStorageState(browser, account, baseURL);
|
|
196
|
+
async function buildUser(browser, account, baseURL, options = {}) {
|
|
197
|
+
const statePath = await ensureStorageState(browser, account, baseURL, options);
|
|
186
198
|
const context = await browser.newContext({ storageState: statePath, baseURL });
|
|
187
199
|
const page = await context.newPage();
|
|
188
200
|
return {
|
|
@@ -209,7 +221,7 @@ var test = baseTest.extend({
|
|
|
209
221
|
const usersFn = async (selector, options) => {
|
|
210
222
|
const accounts = Array.isArray(selector) ? selector.map((name) => findTestAccountByName(name)) : pickTestAccounts(selector, options);
|
|
211
223
|
const built = await Promise.all(
|
|
212
|
-
accounts.map((acct) => buildUser(browser, acct, baseURL))
|
|
224
|
+
accounts.map((acct) => buildUser(browser, acct, baseURL, { force: options?.force }))
|
|
213
225
|
);
|
|
214
226
|
for (const b of built) cleanups.push(b.cleanup);
|
|
215
227
|
return built.map((b) => b.user);
|
package/dist/testing.js.map
CHANGED
|
@@ -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, sidestepping Better\n * Auth's per-IP rate limit on `/api/auth/sign-in/email`.\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 } 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?: { label?: string },\n) => Promise<MultiplayerUser[]>\n\ninterface MultiplayerFixtures {\n users: UsersFixture\n}\n\nasync function buildUser(\n browser: Browser,\n account: TestAccount,\n baseURL: string,\n): Promise<{ user: MultiplayerUser; cleanup: () => Promise<void> }> {\n const statePath = await ensureStorageState(browser, account, baseURL)\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)),\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, writeFileSync } from 'node:fs'\nimport { homedir } from 'node:os'\nimport { join } from 'node:path'\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 = 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(local.filter((account) => account.id).map((account) => [account.id, account]))\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 = candidate.scopes && typeof candidate.scopes === 'object' && !Array.isArray(candidate.scopes)\n ? Object.fromEntries(Object.entries(candidate.scopes).map(([key, value]) => [key, validTestAccounts(value)]))\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 writeFileSync(TEST_ACCOUNTS_PATH, JSON.stringify(store, null, 2), { mode: 0o600 })\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(\n count: number,\n options?: { label?: string },\n): TestAccount[] {\n const all = loadAllTestAccounts()\n const filtered = options?.label\n ? all.filter((a) => a.label === options.label)\n : 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 present in ${TEST_ACCOUNTS_PATH}. Create more with:\\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 * 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}\" in ${TEST_ACCOUNTS_PATH}. Create with:\\n` +\n ` deepspace test accounts create --email ${name.toLowerCase()}@deepspace.test --password <pw> --name \"${name}\"`,\n )\n }\n return match\n}\n","/**\n * Per-account `storageState` cache for Playwright.\n *\n * Better Auth applies a per-IP rate limit on `/api/auth/sign-in/email`\n * (~5 sign-ins / 60s / endpoint). A multiplayer suite that spins up\n * 5+ users per spec would 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 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 : 'Check the stored credential or recreate the 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, sidestepping Better\n * Auth's per-IP rate limit on `/api/auth/sign-in/email`.\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 * Better Auth applies a per-IP rate limit on `/api/auth/sign-in/email`\n * (~5 sign-ins / 60s / endpoint). A multiplayer suite that spins up\n * 5+ users per spec would 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 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"]}
|
package/dist/worker.d.ts
CHANGED
|
@@ -91,6 +91,17 @@ declare const ROLE_ANONYMOUS = "viewer";
|
|
|
91
91
|
declare const ROLE_DEFAULT = "member";
|
|
92
92
|
/** Admin role */
|
|
93
93
|
declare const ROLE_ADMIN = "admin";
|
|
94
|
+
/**
|
|
95
|
+
* Reported when a record id resolves to nothing.
|
|
96
|
+
*
|
|
97
|
+
* This crosses the DO boundary as a response body and callers branch on it,
|
|
98
|
+
* so it is a contract rather than prose. `getChat` in particular treats it as
|
|
99
|
+
* "no such chat" and returns null — the same answer it gives for a chat owned
|
|
100
|
+
* by someone else, which is what stops chat ids being enumerable. If a room
|
|
101
|
+
* reworded this independently, a miss would start throwing while a cross-user
|
|
102
|
+
* hit kept returning null, and that difference is an existence oracle.
|
|
103
|
+
*/
|
|
104
|
+
declare const RECORD_NOT_FOUND = "Record not found";
|
|
94
105
|
|
|
95
106
|
/**
|
|
96
107
|
* Typed wire-protocol layer — discriminated unions, typed builders, and a
|
|
@@ -1281,7 +1292,24 @@ declare function checkFieldPermissions(schema: CollectionSchema, role: string, n
|
|
|
1281
1292
|
* covers both creation and updates while still allowing an owner to unclaim.
|
|
1282
1293
|
*/
|
|
1283
1294
|
declare function checkUnclaimedOwnerTransition(schema: CollectionSchema, role: string, nextData: Record<string, unknown>, userId: string): string | null;
|
|
1284
|
-
/**
|
|
1295
|
+
/**
|
|
1296
|
+
* Columns the system *assigns* — from the identity provider at registration,
|
|
1297
|
+
* or from an admin action. A caller who sends one of these has chosen a value
|
|
1298
|
+
* and means it, so a record write that would change one is refused loudly
|
|
1299
|
+
* rather than silently dropped.
|
|
1300
|
+
*/
|
|
1301
|
+
declare const SYSTEM_ASSIGNED_COLUMNS: Set<string>;
|
|
1302
|
+
/**
|
|
1303
|
+
* Columns the system *maintains* on its own schedule, without telling anyone.
|
|
1304
|
+
* `registerUser` bumps `lastSeenAt` on every WS connect and `handleUserUpdate`
|
|
1305
|
+
* rewrites it on every 60s presence heartbeat; neither broadcasts, so every
|
|
1306
|
+
* client's copy goes stale silently and there is no heal path short of a
|
|
1307
|
+
* refetch. Read-modify-write echo (`put(id, { ...rec.data, field })`) is the
|
|
1308
|
+
* SDK's own house style, so refusing these would refuse writes over a value the
|
|
1309
|
+
* caller never chose and cannot keep current. They are preserved silently.
|
|
1310
|
+
*/
|
|
1311
|
+
declare const SYSTEM_MAINTAINED_COLUMNS: Set<string>;
|
|
1312
|
+
/** Every `users` column owned by the system rather than by record writes. */
|
|
1285
1313
|
declare const SYSTEM_MANAGED_COLUMNS: Set<string>;
|
|
1286
1314
|
/** Standard user columns. Apps spread these into their users schema. */
|
|
1287
1315
|
declare const USERS_COLUMNS: ColumnDefinition[];
|
|
@@ -3661,16 +3689,20 @@ declare function makeDefaultSummarizer(env: DeepSpaceAIEnv, options?: {
|
|
|
3661
3689
|
/**
|
|
3662
3690
|
* Chat history helpers — wrap RecordRoom's tools API for ai-chats / ai-messages.
|
|
3663
3691
|
*
|
|
3664
|
-
* Trust model: every helper
|
|
3665
|
-
* RecordRoom's per-record
|
|
3666
|
-
*
|
|
3667
|
-
*
|
|
3668
|
-
*
|
|
3669
|
-
*
|
|
3670
|
-
*
|
|
3671
|
-
*
|
|
3672
|
-
*
|
|
3673
|
-
*
|
|
3692
|
+
* Trust model: every helper here runs through `executeToolAsApp`, which sends
|
|
3693
|
+
* `X-App-Action: 'true'` and therefore **bypasses RecordRoom's per-record
|
|
3694
|
+
* RBAC entirely**. The `userId` argument these helpers take is the identity
|
|
3695
|
+
* they act *as* — it is NOT an authorization boundary, and RecordRoom will
|
|
3696
|
+
* not check it. The worker is the only trust boundary.
|
|
3697
|
+
*
|
|
3698
|
+
* `getChat` is that boundary for this module: it compares the stored
|
|
3699
|
+
* `userId` against the caller and returns null on a mismatch, so the
|
|
3700
|
+
* worker's `/api/ai/chat`, `PATCH /api/ai/chats/:id`, and
|
|
3701
|
+
* `DELETE /api/ai/chats/:id` routes 404 when a row is missing *or* owned by
|
|
3702
|
+
* someone else. Every write helper (`updateChat`, `appendMessage`,
|
|
3703
|
+
* `deleteChatCascade`) is reachable only behind that precheck; a new caller
|
|
3704
|
+
* that skips it is writing across users, so route new consumers through
|
|
3705
|
+
* `getChat` first.
|
|
3674
3706
|
*
|
|
3675
3707
|
* The tools API returns records as `{ recordId, data, createdAt, updatedAt }`
|
|
3676
3708
|
* envelopes; helpers below flatten them into ChatRow / ChatMessageRow.
|
|
@@ -3694,6 +3726,16 @@ type ChatMessageRow = {
|
|
|
3694
3726
|
parts?: unknown[];
|
|
3695
3727
|
createdAt: string;
|
|
3696
3728
|
};
|
|
3729
|
+
/**
|
|
3730
|
+
* Fetch a chat the caller owns, or null.
|
|
3731
|
+
*
|
|
3732
|
+
* This is the module's authorization boundary. `records.get` runs with
|
|
3733
|
+
* `X-App-Action`, so RecordRoom applies no per-record RBAC and will happily
|
|
3734
|
+
* return another user's row — the ownership comparison below is the only
|
|
3735
|
+
* thing standing between a caller and someone else's chat. A miss and a
|
|
3736
|
+
* cross-user hit deliberately look identical to the caller (both null, both
|
|
3737
|
+
* 404 at the route) so chat ids stay unenumerable.
|
|
3738
|
+
*/
|
|
3697
3739
|
declare function getChat(stub: DurableObjectStub, chatId: string, userId: string): Promise<ChatRow | null>;
|
|
3698
3740
|
declare function createChat(stub: DurableObjectStub, userId: string, opts?: {
|
|
3699
3741
|
title?: string;
|
|
@@ -5285,4 +5327,4 @@ interface AppLogsResponse {
|
|
|
5285
5327
|
declare const APP_LOG_EVENT_KEYS: readonly ["id", "timestamp", "level", "eventType", "message", "source", "outcome", "request", "exception"];
|
|
5286
5328
|
type AppLogEventKey = (typeof APP_LOG_EVENT_KEYS)[number];
|
|
5287
5329
|
|
|
5288
|
-
export { ACCOUNT_STORAGE_LIMIT_BYTES, AI_CHATS_SCHEMA, AI_MESSAGES_SCHEMA, ALLOWED_BINDING_TYPES, 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, BUILT_IN_TOOLS, type BaseMessage, BaseRoom, CHANNELS_SCHEMA, CHANNEL_INVITATIONS_SCHEMA, CHANNEL_MEMBERS_SCHEMA, CLIENT_LOG_MARKER, CONVERSATION_SCHEMAS, 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 ConvMemberData, type ConvMessageData, type ConvReactionData, type ConvReadCursorData, type ConvVoteData, type ConversationStateData, 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_DO_MANIFEST, DEFAULT_QUERY_LIMIT, DEPLOYED_APPS_DEFAULTS, DIRECTORY_SCHEMAS, 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 DirectoryCommunityData, type DirectoryConversationData, type DirectoryMembershipData, type DirectoryPostData, type DoMigrationDirective, type DoMigrationPlan, type ExistingDOBinding, GLOBAL_DO_TYPES, GLOBAL_DO_TYPE_NAMES, type GetActionData, type GlobalDOType, type HandlerContext, type Job, type JobContext, JobRoom, type JobRoomConfig, type JobStatus, type JwtClaims, type JwtVerifierConfig, LOG_LEVELS, type LogLevel, MAX_APP_FILE_BYTES, MAX_DEPLOY_ASSET_FILE_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, REGISTRY_INSTANCE, RELEASE_STAMP_PATH, REPO_STORE_LIMIT_BYTES, REQUIRED_COMPATIBILITY_FLAGS, RESERVED_BINDING_NAMES, RESERVED_COLLECTION_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_COLLECTIONS, SYSTEM_MANAGED_COLUMNS, SchemaRegistry, type ScopeContext, type ScopedR2Auth, type ScopedR2Config, type ScopedR2Handler, type ScreenshotEnv, type ScreenshotResult, type ServerMessage, type SharedConnection, 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, VOTING_SCHEMAS, type ValidationError, type VerifiedAuth, type VerifyOutcome, type VerifyResult, type Viewport, WORKER_OWNED_NOT_FOUND_MIGRATION_ID, WORKSPACE_SCHEMAS, YjsRoom, analyzePermissions, apiWorkerFetch, appendMessage, applyAiToolDefaults, applySlidingWindow, 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, formatBytes, getChat, getDeepSpaceAIModel, getGlobalDOSchemas, getGlobalDOType, getMessageType, getRolePermissions, handleAwarenessMessage, handleClientErrorReport, handleSyncMessage, hasContent, isAssetNotFoundHandling, isAutoProvision, isFilesVerbPath, isOwner, isPlatformReservedPath, isTestAccountClaims, isTestAccountEmail, isTestAccountTier, lintSchema, listDeepSpaceAgentModels, loadMessages, makeDefaultSummarizer, matchOrigin, meterAi, meterUsage, meterVectorize, mintAppId, mintUlid, noopPermissionContext, 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, workspaceAccountsSchema, workspaceContentSharesSchema, workspaceEmailHandlesSchema, workspaceFormResponsesSchema, workspacePeopleSchema, workspaceProjectsSchema, workspaceTagsSchema, workspaceTasksSchema, workspaceTeamMembersSchema, workspaceTeamsSchema, workspaceTransactionsSchema, writeVarUint, writeVarUint8Array };
|
|
5330
|
+
export { ACCOUNT_STORAGE_LIMIT_BYTES, AI_CHATS_SCHEMA, AI_MESSAGES_SCHEMA, ALLOWED_BINDING_TYPES, 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, BUILT_IN_TOOLS, type BaseMessage, BaseRoom, CHANNELS_SCHEMA, CHANNEL_INVITATIONS_SCHEMA, CHANNEL_MEMBERS_SCHEMA, CLIENT_LOG_MARKER, CONVERSATION_SCHEMAS, 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 ConvMemberData, type ConvMessageData, type ConvReactionData, type ConvReadCursorData, type ConvVoteData, type ConversationStateData, 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_DO_MANIFEST, DEFAULT_QUERY_LIMIT, DEPLOYED_APPS_DEFAULTS, DIRECTORY_SCHEMAS, 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 DirectoryCommunityData, type DirectoryConversationData, type DirectoryMembershipData, type DirectoryPostData, type DoMigrationDirective, type DoMigrationPlan, type ExistingDOBinding, GLOBAL_DO_TYPES, GLOBAL_DO_TYPE_NAMES, type GetActionData, type GlobalDOType, type HandlerContext, type Job, type JobContext, JobRoom, type JobRoomConfig, type JobStatus, type JwtClaims, type JwtVerifierConfig, LOG_LEVELS, type LogLevel, MAX_APP_FILE_BYTES, MAX_DEPLOY_ASSET_FILE_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_COLLECTION_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 ScopedR2Auth, type ScopedR2Config, type ScopedR2Handler, type ScreenshotEnv, type ScreenshotResult, type ServerMessage, type SharedConnection, 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, VOTING_SCHEMAS, type ValidationError, type VerifiedAuth, type VerifyOutcome, type VerifyResult, type Viewport, WORKER_OWNED_NOT_FOUND_MIGRATION_ID, WORKSPACE_SCHEMAS, YjsRoom, analyzePermissions, apiWorkerFetch, appendMessage, applyAiToolDefaults, applySlidingWindow, 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, formatBytes, getChat, getDeepSpaceAIModel, getGlobalDOSchemas, getGlobalDOType, getMessageType, getRolePermissions, handleAwarenessMessage, handleClientErrorReport, handleSyncMessage, hasContent, isAssetNotFoundHandling, isAutoProvision, isFilesVerbPath, isOwner, isPlatformReservedPath, isTestAccountClaims, isTestAccountEmail, isTestAccountTier, lintSchema, listDeepSpaceAgentModels, loadMessages, makeDefaultSummarizer, matchOrigin, meterAi, meterUsage, meterVectorize, mintAppId, mintUlid, noopPermissionContext, 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, workspaceAccountsSchema, workspaceContentSharesSchema, workspaceEmailHandlesSchema, workspaceFormResponsesSchema, workspacePeopleSchema, workspaceProjectsSchema, workspaceTagsSchema, workspaceTasksSchema, workspaceTeamMembersSchema, workspaceTeamsSchema, workspaceTransactionsSchema, writeVarUint, writeVarUint8Array };
|