@agent-native/core 0.45.0 → 0.46.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/README.md +1 -0
- package/dist/action.d.ts +8 -1
- package/dist/action.d.ts.map +1 -1
- package/dist/action.js +20 -10
- package/dist/action.js.map +1 -1
- package/dist/cli/app-skill.d.ts +3 -1
- package/dist/cli/app-skill.d.ts.map +1 -1
- package/dist/cli/app-skill.js +50 -8
- package/dist/cli/app-skill.js.map +1 -1
- package/dist/cli/connect.d.ts.map +1 -1
- package/dist/cli/connect.js +39 -5
- package/dist/cli/connect.js.map +1 -1
- package/dist/cli/create.d.ts.map +1 -1
- package/dist/cli/create.js +9 -7
- package/dist/cli/create.js.map +1 -1
- package/dist/cli/index.js +42 -10
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/mcp-config-writers.d.ts +10 -0
- package/dist/cli/mcp-config-writers.d.ts.map +1 -1
- package/dist/cli/mcp-config-writers.js +60 -6
- package/dist/cli/mcp-config-writers.js.map +1 -1
- package/dist/cli/mcp.d.ts.map +1 -1
- package/dist/cli/mcp.js +4 -6
- package/dist/cli/mcp.js.map +1 -1
- package/dist/cli/plan-local.d.ts.map +1 -1
- package/dist/cli/plan-local.js +15 -2
- package/dist/cli/plan-local.js.map +1 -1
- package/dist/cli/plan-publish-store.d.ts +17 -7
- package/dist/cli/plan-publish-store.d.ts.map +1 -1
- package/dist/cli/plan-publish-store.js +33 -8
- package/dist/cli/plan-publish-store.js.map +1 -1
- package/dist/cli/pr-visual-recap-workflow.d.ts +1 -1
- package/dist/cli/pr-visual-recap-workflow.d.ts.map +1 -1
- package/dist/cli/pr-visual-recap-workflow.js +1 -1
- package/dist/cli/pr-visual-recap-workflow.js.map +1 -1
- package/dist/cli/recap.d.ts +63 -5
- package/dist/cli/recap.d.ts.map +1 -1
- package/dist/cli/recap.js +641 -48
- package/dist/cli/recap.js.map +1 -1
- package/dist/cli/skills.d.ts +26 -11
- package/dist/cli/skills.d.ts.map +1 -1
- package/dist/cli/skills.js +644 -972
- package/dist/cli/skills.js.map +1 -1
- package/dist/cli/templates-meta.d.ts.map +1 -1
- package/dist/cli/templates-meta.js +3 -2
- package/dist/cli/templates-meta.js.map +1 -1
- package/dist/client/blocks/library/AnnotatedCodeBlock.d.ts.map +1 -1
- package/dist/client/blocks/library/AnnotatedCodeBlock.js +37 -9
- package/dist/client/blocks/library/AnnotatedCodeBlock.js.map +1 -1
- package/dist/client/blocks/library/DiffBlock.d.ts.map +1 -1
- package/dist/client/blocks/library/DiffBlock.js +44 -12
- package/dist/client/blocks/library/DiffBlock.js.map +1 -1
- package/dist/client/blocks/library/annotation-rail.d.ts +12 -3
- package/dist/client/blocks/library/annotation-rail.d.ts.map +1 -1
- package/dist/client/blocks/library/annotation-rail.js +29 -3
- package/dist/client/blocks/library/annotation-rail.js.map +1 -1
- package/dist/client/blocks/library/html.d.ts.map +1 -1
- package/dist/client/blocks/library/html.js +3 -1
- package/dist/client/blocks/library/html.js.map +1 -1
- package/dist/client/blocks/library/question-form.d.ts.map +1 -1
- package/dist/client/blocks/library/question-form.js +4 -1
- package/dist/client/blocks/library/question-form.js.map +1 -1
- package/dist/client/components/LiveCursorOverlay.d.ts +46 -0
- package/dist/client/components/LiveCursorOverlay.d.ts.map +1 -0
- package/dist/client/components/LiveCursorOverlay.js +137 -0
- package/dist/client/components/LiveCursorOverlay.js.map +1 -0
- package/dist/client/components/PresenceBar.d.ts +11 -1
- package/dist/client/components/PresenceBar.d.ts.map +1 -1
- package/dist/client/components/PresenceBar.js +39 -7
- package/dist/client/components/PresenceBar.js.map +1 -1
- package/dist/client/components/RemoteSelectionRings.d.ts +43 -0
- package/dist/client/components/RemoteSelectionRings.d.ts.map +1 -0
- package/dist/client/components/RemoteSelectionRings.js +116 -0
- package/dist/client/components/RemoteSelectionRings.js.map +1 -0
- package/dist/client/index.d.ts +4 -0
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +5 -0
- package/dist/client/index.js.map +1 -1
- package/dist/collab/awareness.d.ts +25 -0
- package/dist/collab/awareness.d.ts.map +1 -1
- package/dist/collab/awareness.js +42 -5
- package/dist/collab/awareness.js.map +1 -1
- package/dist/collab/client.d.ts +19 -1
- package/dist/collab/client.d.ts.map +1 -1
- package/dist/collab/client.js +362 -57
- package/dist/collab/client.js.map +1 -1
- package/dist/collab/follow-mode.d.ts +56 -0
- package/dist/collab/follow-mode.d.ts.map +1 -0
- package/dist/collab/follow-mode.js +54 -0
- package/dist/collab/follow-mode.js.map +1 -0
- package/dist/collab/index.d.ts +3 -1
- package/dist/collab/index.d.ts.map +1 -1
- package/dist/collab/index.js +5 -1
- package/dist/collab/index.js.map +1 -1
- package/dist/collab/presence.d.ts +56 -0
- package/dist/collab/presence.d.ts.map +1 -0
- package/dist/collab/presence.js +98 -0
- package/dist/collab/presence.js.map +1 -0
- package/dist/collab/routes.d.ts.map +1 -1
- package/dist/collab/routes.js +33 -6
- package/dist/collab/routes.js.map +1 -1
- package/dist/collab/struct-routes.d.ts.map +1 -1
- package/dist/collab/struct-routes.js +24 -4
- package/dist/collab/struct-routes.js.map +1 -1
- package/dist/collab/ydoc-manager.d.ts +13 -0
- package/dist/collab/ydoc-manager.d.ts.map +1 -1
- package/dist/collab/ydoc-manager.js +51 -15
- package/dist/collab/ydoc-manager.js.map +1 -1
- package/dist/db/migrations.d.ts.map +1 -1
- package/dist/db/migrations.js +2 -1
- package/dist/db/migrations.js.map +1 -1
- package/dist/extensions/routes.d.ts +18 -0
- package/dist/extensions/routes.d.ts.map +1 -1
- package/dist/extensions/routes.js +30 -8
- package/dist/extensions/routes.js.map +1 -1
- package/dist/oauth-tokens/store.d.ts.map +1 -1
- package/dist/oauth-tokens/store.js +42 -5
- package/dist/oauth-tokens/store.js.map +1 -1
- package/dist/scripts/db/index.d.ts.map +1 -1
- package/dist/scripts/db/index.js +1 -0
- package/dist/scripts/db/index.js.map +1 -1
- package/dist/scripts/db/migrate-encrypt-oauth-tokens.d.ts +28 -0
- package/dist/scripts/db/migrate-encrypt-oauth-tokens.d.ts.map +1 -0
- package/dist/scripts/db/migrate-encrypt-oauth-tokens.js +164 -0
- package/dist/scripts/db/migrate-encrypt-oauth-tokens.js.map +1 -0
- package/dist/scripts/db/scoping.d.ts.map +1 -1
- package/dist/scripts/db/scoping.js +7 -5
- package/dist/scripts/db/scoping.js.map +1 -1
- package/dist/secrets/index.d.ts +1 -0
- package/dist/secrets/index.d.ts.map +1 -1
- package/dist/secrets/index.js +4 -0
- package/dist/secrets/index.js.map +1 -1
- package/dist/server/collab-plugin.d.ts +6 -0
- package/dist/server/collab-plugin.d.ts.map +1 -1
- package/dist/server/collab-plugin.js +105 -5
- package/dist/server/collab-plugin.js.map +1 -1
- package/dist/server/poll-events.d.ts +5 -0
- package/dist/server/poll-events.d.ts.map +1 -1
- package/dist/server/poll-events.js +27 -4
- package/dist/server/poll-events.js.map +1 -1
- package/dist/sharing/actions/set-resource-visibility.d.ts.map +1 -1
- package/dist/sharing/actions/set-resource-visibility.js +4 -1
- package/dist/sharing/actions/set-resource-visibility.js.map +1 -1
- package/dist/templates/default/.agents/skills/real-time-collab/SKILL.md +185 -37
- package/dist/templates/default/.agents/skills/real-time-sync/SKILL.md +12 -2
- package/dist/templates/workspace-core/.agents/skills/real-time-collab/SKILL.md +185 -37
- package/dist/templates/workspace-core/.agents/skills/real-time-sync/SKILL.md +12 -2
- package/docs/content/plan-plugin.md +21 -6
- package/docs/content/pr-visual-recap.md +52 -3
- package/docs/content/real-time-collaboration.md +481 -97
- package/docs/content/skills-guide.md +13 -0
- package/docs/content/template-plan.md +18 -7
- package/package.json +5 -1
- package/src/templates/default/.agents/skills/real-time-collab/SKILL.md +185 -37
- package/src/templates/default/.agents/skills/real-time-sync/SKILL.md +12 -2
- package/src/templates/workspace-core/.agents/skills/real-time-collab/SKILL.md +185 -37
- package/src/templates/workspace-core/.agents/skills/real-time-sync/SKILL.md +12 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"store.js","sourceRoot":"","sources":["../../src/oauth-tokens/store.ts"],"names":[],"mappings":"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","sourcesContent":["import { getDbExec, isPostgres, intType } from \"../db/client.js\";\n\nlet _initPromise: Promise<void> | undefined;\n\nfunction oauthTokensTable(): string {\n return isPostgres() ? \"public.oauth_tokens\" : \"oauth_tokens\";\n}\n\nasync function ensureTable(): Promise<void> {\n if (!_initPromise) {\n _initPromise = (async () => {\n const client = getDbExec();\n const table = oauthTokensTable();\n await client.execute(`\n CREATE TABLE IF NOT EXISTS ${table} (\n provider TEXT NOT NULL,\n account_id TEXT NOT NULL,\n owner TEXT,\n tokens TEXT NOT NULL,\n updated_at ${intType()} NOT NULL,\n PRIMARY KEY (provider, account_id)\n )\n `);\n // Migration: add owner column to existing tables\n try {\n await client.execute(`ALTER TABLE ${table} ADD COLUMN owner TEXT`);\n } catch {\n // Column already exists\n }\n // Migration: add display_name column\n try {\n await client.execute(\n `ALTER TABLE ${table} ADD COLUMN display_name TEXT`,\n );\n } catch {\n // Column already exists\n }\n // Backfill: set owner = account_id for existing rows without an owner\n await client.execute(\n `UPDATE ${table} SET owner = account_id WHERE owner IS NULL`,\n );\n })().catch((err) => {\n // Retry init on the next call after a failed startup.\n _initPromise = undefined;\n throw err;\n });\n }\n return _initPromise;\n}\n\nexport async function getOAuthTokens(\n provider: string,\n accountId: string,\n): Promise<Record<string, unknown> | null> {\n await ensureTable();\n const client = getDbExec();\n const table = oauthTokensTable();\n const { rows } = await client.execute({\n sql: `SELECT tokens FROM ${table} WHERE provider = ? AND account_id = ?`,\n args: [provider, accountId],\n });\n if (rows.length === 0) return null;\n return JSON.parse(rows[0].tokens as string);\n}\n\n/**\n * Thrown when an OAuth save would re-bind an `(provider, account_id)` row\n * to a different owner than already holds it. Callers should catch this and\n * surface a clean \"this account is already linked to another user\" message\n * to the requester rather than letting it propagate as a 500.\n *\n * Carries `statusCode = 409` so route handlers using h3's `createError` can\n * pass it straight through.\n */\nexport class OAuthAccountOwnedByOtherUserError extends Error {\n readonly statusCode = 409;\n readonly provider: string;\n readonly accountId: string;\n readonly existingOwner: string;\n readonly attemptedOwner: string;\n constructor(opts: {\n provider: string;\n accountId: string;\n existingOwner: string;\n attemptedOwner: string;\n }) {\n super(\n `OAuth account ${opts.provider}:${opts.accountId} is already linked to another user — refusing to overwrite the owner.`,\n );\n this.name = \"OAuthAccountOwnedByOtherUserError\";\n this.provider = opts.provider;\n this.accountId = opts.accountId;\n this.existingOwner = opts.existingOwner;\n this.attemptedOwner = opts.attemptedOwner;\n }\n}\n\n/**\n * Save OAuth tokens. The `owner` parameter specifies which user owns this\n * account — defaults to `accountId` (the account itself is the owner).\n * For multi-account support, pass the logged-in user's email as owner.\n *\n * If the account already exists and is owned by a different user, throws\n * `OAuthAccountOwnedByOtherUserError` (statusCode 409) to prevent silently\n * stealing another user's linked account.\n *\n * Read + write happen as a single linearised batch (Postgres) or paired\n * statements (SQLite). On both backends the per-row PK serialises concurrent\n * writes for the same `(provider, account_id)` so the owner check cannot be\n * raced by an attacker calling saveOAuthTokens twice in flight — the second\n * caller sees the first caller's owner row and raises 409.\n */\nexport async function saveOAuthTokens(\n provider: string,\n accountId: string,\n tokens: Record<string, unknown>,\n owner?: string,\n): Promise<void> {\n await ensureTable();\n const client = getDbExec();\n const table = oauthTokensTable();\n\n // Read the current row before deciding what to write. We use this to\n // (a) preserve owner / display_name when this is a token refresh (no\n // owner argument), and (b) reject the write when the caller is trying\n // to overwrite a row owned by someone else.\n let resolvedOwner = owner ?? accountId;\n let existingDisplayName: string | null = null;\n let existingOwner: string | null = null;\n let existingTokens: Record<string, unknown> | null = null;\n const { rows: existing } = await client.execute({\n sql: `SELECT owner, display_name, tokens FROM ${table} WHERE provider = ? AND account_id = ?`,\n args: [provider, accountId],\n });\n if (existing.length > 0) {\n existingOwner = (existing[0].owner as string) ?? null;\n existingDisplayName = (existing[0].display_name as string) ?? null;\n existingTokens = JSON.parse((existing[0].tokens as string) ?? \"{}\");\n }\n\n if (!owner) {\n // Token-refresh path: keep the existing owner/displayName unchanged.\n if (existingOwner) resolvedOwner = existingOwner;\n } else if (existingOwner && owner && existingOwner !== owner) {\n // Refuse to silently re-bind an account from one user to another.\n // This is the case the docstring promised but the previous\n // implementation didn't enforce — `ON CONFLICT DO UPDATE SET\n // owner=EXCLUDED.owner` would have overwritten the prior owner.\n throw new OAuthAccountOwnedByOtherUserError({\n provider,\n accountId,\n existingOwner,\n attemptedOwner: owner,\n });\n }\n\n const cleanedIncomingTokens = Object.fromEntries(\n Object.entries(tokens).filter(([, value]) => value !== undefined),\n );\n const tokensToStore = {\n ...(existingTokens ?? {}),\n ...cleanedIncomingTokens,\n };\n\n await client.execute({\n sql: isPostgres()\n ? `INSERT INTO ${table} (provider, account_id, owner, display_name, tokens, updated_at) VALUES (?, ?, ?, ?, ?, ?) ON CONFLICT (provider, account_id) DO UPDATE SET owner=EXCLUDED.owner, display_name=COALESCE(EXCLUDED.display_name, ${table}.display_name), tokens=EXCLUDED.tokens, updated_at=EXCLUDED.updated_at`\n : `INSERT OR REPLACE INTO ${table} (provider, account_id, owner, display_name, tokens, updated_at) VALUES (?, ?, ?, ?, ?, ?)`,\n args: [\n provider,\n accountId,\n resolvedOwner,\n existingDisplayName,\n JSON.stringify(tokensToStore),\n Date.now(),\n ],\n });\n}\n\nexport async function deleteOAuthTokens(\n provider: string,\n accountId?: string,\n): Promise<number> {\n await ensureTable();\n const client = getDbExec();\n const table = oauthTokensTable();\n if (accountId) {\n const result = await client.execute({\n sql: `DELETE FROM ${table} WHERE provider = ? AND account_id = ?`,\n args: [provider, accountId],\n });\n return result.rowsAffected;\n }\n const result = await client.execute({\n sql: `DELETE FROM ${table} WHERE provider = ?`,\n args: [provider],\n });\n return result.rowsAffected;\n}\n\nexport async function listOAuthAccounts(provider: string): Promise<\n Array<{\n accountId: string;\n owner: string | null;\n tokens: Record<string, unknown>;\n }>\n> {\n await ensureTable();\n const client = getDbExec();\n const table = oauthTokensTable();\n const { rows } = await client.execute({\n sql: `SELECT account_id, owner, tokens FROM ${table} WHERE provider = ?`,\n args: [provider],\n });\n return rows.map((row) => ({\n accountId: row.account_id as string,\n owner: (row.owner as string) ?? null,\n tokens: JSON.parse(row.tokens as string),\n }));\n}\n\n/**\n * List all OAuth accounts owned by a specific user.\n * In multi-account mode, a user may have connected multiple Google accounts.\n */\nexport async function listOAuthAccountsByOwner(\n provider: string,\n owner: string,\n): Promise<\n Array<{\n accountId: string;\n displayName: string | null;\n tokens: Record<string, unknown>;\n }>\n> {\n await ensureTable();\n const client = getDbExec();\n const table = oauthTokensTable();\n const { rows } = await client.execute({\n sql: `SELECT account_id, display_name, tokens FROM ${table} WHERE provider = ? AND owner = ?`,\n args: [provider, owner],\n });\n return rows.map((row) => ({\n accountId: row.account_id as string,\n displayName: (row.display_name as string) ?? null,\n tokens: JSON.parse(row.tokens as string),\n }));\n}\n\n/**\n * Set the display name for an OAuth account (e.g. Google profile name).\n */\nexport async function setOAuthDisplayName(\n provider: string,\n accountId: string,\n displayName: string,\n): Promise<void> {\n await ensureTable();\n const client = getDbExec();\n const table = oauthTokensTable();\n await client.execute({\n sql: `UPDATE ${table} SET display_name = ? WHERE provider = ? AND account_id = ?`,\n args: [displayName, provider, accountId],\n });\n}\n\n/**\n * Check whether a specific user has tokens for a provider.\n *\n * `owner` is REQUIRED. The previous unscoped form leaked information\n * across users — the onboarding banner would mark the OAuth secret as\n * \"set\" for user B as soon as ANY user in the deployment connected the\n * provider, and user B would never see the prompt to connect.\n */\nexport async function hasOAuthTokens(\n provider: string,\n owner: string,\n): Promise<boolean> {\n await ensureTable();\n const client = getDbExec();\n const table = oauthTokensTable();\n const { rows } = await client.execute({\n sql: `SELECT 1 FROM ${table} WHERE provider = ? AND owner = ? LIMIT 1`,\n args: [provider, owner],\n });\n return rows.length > 0;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"store.js","sourceRoot":"","sources":["../../src/oauth-tokens/store.ts"],"names":[],"mappings":"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","sourcesContent":["import { getDbExec, isPostgres, intType } from \"../db/client.js\";\nimport {\n encryptSecretValue,\n decryptSecretValue,\n isEncryptedSecretValue,\n} from \"../secrets/crypto.js\";\n\nlet _initPromise: Promise<void> | undefined;\n\n/**\n * Encrypt the token bundle (AES-256-GCM) before it goes to the `tokens`\n * column. OAuth access/refresh tokens are long-lived, high-value credentials;\n * encrypting at rest means a leaked DB backup / pg_dump / read replica no\n * longer exposes them in plaintext. {@link parseStoredTokens} decrypts\n * transparently on read.\n */\nfunction serializeTokens(tokens: Record<string, unknown>): string {\n return encryptSecretValue(JSON.stringify(tokens));\n}\n\n/**\n * Parse a stored `tokens` value. Encrypted rows are decrypted; rows written\n * before encryption — or mirrored from Better Auth's `account` table — are\n * plaintext JSON and read transparently (the `db-migrate-encrypt-oauth-tokens`\n * script re-encrypts them in place). A row that can't be decrypted (key\n * rotated / corrupt / tampered) is treated as empty rather than throwing into\n * every token lookup.\n */\nfunction parseStoredTokens(\n stored: string | null | undefined,\n): Record<string, unknown> {\n if (!stored) return {};\n if (isEncryptedSecretValue(stored)) {\n try {\n return JSON.parse(decryptSecretValue(stored));\n } catch {\n return {};\n }\n }\n try {\n return JSON.parse(stored);\n } catch {\n return {};\n }\n}\n\nfunction oauthTokensTable(): string {\n return isPostgres() ? \"public.oauth_tokens\" : \"oauth_tokens\";\n}\n\nasync function ensureTable(): Promise<void> {\n if (!_initPromise) {\n _initPromise = (async () => {\n const client = getDbExec();\n const table = oauthTokensTable();\n await client.execute(`\n CREATE TABLE IF NOT EXISTS ${table} (\n provider TEXT NOT NULL,\n account_id TEXT NOT NULL,\n owner TEXT,\n tokens TEXT NOT NULL,\n updated_at ${intType()} NOT NULL,\n PRIMARY KEY (provider, account_id)\n )\n `);\n // Migration: add owner column to existing tables\n try {\n await client.execute(`ALTER TABLE ${table} ADD COLUMN owner TEXT`);\n } catch {\n // Column already exists\n }\n // Migration: add display_name column\n try {\n await client.execute(\n `ALTER TABLE ${table} ADD COLUMN display_name TEXT`,\n );\n } catch {\n // Column already exists\n }\n // Backfill: set owner = account_id for existing rows without an owner\n await client.execute(\n `UPDATE ${table} SET owner = account_id WHERE owner IS NULL`,\n );\n })().catch((err) => {\n // Retry init on the next call after a failed startup.\n _initPromise = undefined;\n throw err;\n });\n }\n return _initPromise;\n}\n\nexport async function getOAuthTokens(\n provider: string,\n accountId: string,\n): Promise<Record<string, unknown> | null> {\n await ensureTable();\n const client = getDbExec();\n const table = oauthTokensTable();\n const { rows } = await client.execute({\n sql: `SELECT tokens FROM ${table} WHERE provider = ? AND account_id = ?`,\n args: [provider, accountId],\n });\n if (rows.length === 0) return null;\n return parseStoredTokens(rows[0].tokens as string);\n}\n\n/**\n * Thrown when an OAuth save would re-bind an `(provider, account_id)` row\n * to a different owner than already holds it. Callers should catch this and\n * surface a clean \"this account is already linked to another user\" message\n * to the requester rather than letting it propagate as a 500.\n *\n * Carries `statusCode = 409` so route handlers using h3's `createError` can\n * pass it straight through.\n */\nexport class OAuthAccountOwnedByOtherUserError extends Error {\n readonly statusCode = 409;\n readonly provider: string;\n readonly accountId: string;\n readonly existingOwner: string;\n readonly attemptedOwner: string;\n constructor(opts: {\n provider: string;\n accountId: string;\n existingOwner: string;\n attemptedOwner: string;\n }) {\n super(\n `OAuth account ${opts.provider}:${opts.accountId} is already linked to another user — refusing to overwrite the owner.`,\n );\n this.name = \"OAuthAccountOwnedByOtherUserError\";\n this.provider = opts.provider;\n this.accountId = opts.accountId;\n this.existingOwner = opts.existingOwner;\n this.attemptedOwner = opts.attemptedOwner;\n }\n}\n\n/**\n * Save OAuth tokens. The `owner` parameter specifies which user owns this\n * account — defaults to `accountId` (the account itself is the owner).\n * For multi-account support, pass the logged-in user's email as owner.\n *\n * If the account already exists and is owned by a different user, throws\n * `OAuthAccountOwnedByOtherUserError` (statusCode 409) to prevent silently\n * stealing another user's linked account.\n *\n * Read + write happen as a single linearised batch (Postgres) or paired\n * statements (SQLite). On both backends the per-row PK serialises concurrent\n * writes for the same `(provider, account_id)` so the owner check cannot be\n * raced by an attacker calling saveOAuthTokens twice in flight — the second\n * caller sees the first caller's owner row and raises 409.\n */\nexport async function saveOAuthTokens(\n provider: string,\n accountId: string,\n tokens: Record<string, unknown>,\n owner?: string,\n): Promise<void> {\n await ensureTable();\n const client = getDbExec();\n const table = oauthTokensTable();\n\n // Read the current row before deciding what to write. We use this to\n // (a) preserve owner / display_name when this is a token refresh (no\n // owner argument), and (b) reject the write when the caller is trying\n // to overwrite a row owned by someone else.\n let resolvedOwner = owner ?? accountId;\n let existingDisplayName: string | null = null;\n let existingOwner: string | null = null;\n let existingTokens: Record<string, unknown> | null = null;\n const { rows: existing } = await client.execute({\n sql: `SELECT owner, display_name, tokens FROM ${table} WHERE provider = ? AND account_id = ?`,\n args: [provider, accountId],\n });\n if (existing.length > 0) {\n existingOwner = (existing[0].owner as string) ?? null;\n existingDisplayName = (existing[0].display_name as string) ?? null;\n existingTokens = parseStoredTokens(existing[0].tokens as string);\n }\n\n if (!owner) {\n // Token-refresh path: keep the existing owner/displayName unchanged.\n if (existingOwner) resolvedOwner = existingOwner;\n } else if (existingOwner && owner && existingOwner !== owner) {\n // Refuse to silently re-bind an account from one user to another.\n // This is the case the docstring promised but the previous\n // implementation didn't enforce — `ON CONFLICT DO UPDATE SET\n // owner=EXCLUDED.owner` would have overwritten the prior owner.\n throw new OAuthAccountOwnedByOtherUserError({\n provider,\n accountId,\n existingOwner,\n attemptedOwner: owner,\n });\n }\n\n const cleanedIncomingTokens = Object.fromEntries(\n Object.entries(tokens).filter(([, value]) => value !== undefined),\n );\n const tokensToStore = {\n ...(existingTokens ?? {}),\n ...cleanedIncomingTokens,\n };\n\n await client.execute({\n sql: isPostgres()\n ? `INSERT INTO ${table} (provider, account_id, owner, display_name, tokens, updated_at) VALUES (?, ?, ?, ?, ?, ?) ON CONFLICT (provider, account_id) DO UPDATE SET owner=EXCLUDED.owner, display_name=COALESCE(EXCLUDED.display_name, ${table}.display_name), tokens=EXCLUDED.tokens, updated_at=EXCLUDED.updated_at`\n : `INSERT OR REPLACE INTO ${table} (provider, account_id, owner, display_name, tokens, updated_at) VALUES (?, ?, ?, ?, ?, ?)`,\n args: [\n provider,\n accountId,\n resolvedOwner,\n existingDisplayName,\n serializeTokens(tokensToStore),\n Date.now(),\n ],\n });\n}\n\nexport async function deleteOAuthTokens(\n provider: string,\n accountId?: string,\n): Promise<number> {\n await ensureTable();\n const client = getDbExec();\n const table = oauthTokensTable();\n if (accountId) {\n const result = await client.execute({\n sql: `DELETE FROM ${table} WHERE provider = ? AND account_id = ?`,\n args: [provider, accountId],\n });\n return result.rowsAffected;\n }\n const result = await client.execute({\n sql: `DELETE FROM ${table} WHERE provider = ?`,\n args: [provider],\n });\n return result.rowsAffected;\n}\n\nexport async function listOAuthAccounts(provider: string): Promise<\n Array<{\n accountId: string;\n owner: string | null;\n tokens: Record<string, unknown>;\n }>\n> {\n await ensureTable();\n const client = getDbExec();\n const table = oauthTokensTable();\n const { rows } = await client.execute({\n sql: `SELECT account_id, owner, tokens FROM ${table} WHERE provider = ?`,\n args: [provider],\n });\n return rows.map((row) => ({\n accountId: row.account_id as string,\n owner: (row.owner as string) ?? null,\n tokens: parseStoredTokens(row.tokens as string),\n }));\n}\n\n/**\n * List all OAuth accounts owned by a specific user.\n * In multi-account mode, a user may have connected multiple Google accounts.\n */\nexport async function listOAuthAccountsByOwner(\n provider: string,\n owner: string,\n): Promise<\n Array<{\n accountId: string;\n displayName: string | null;\n tokens: Record<string, unknown>;\n }>\n> {\n await ensureTable();\n const client = getDbExec();\n const table = oauthTokensTable();\n const { rows } = await client.execute({\n sql: `SELECT account_id, display_name, tokens FROM ${table} WHERE provider = ? AND owner = ?`,\n args: [provider, owner],\n });\n return rows.map((row) => ({\n accountId: row.account_id as string,\n displayName: (row.display_name as string) ?? null,\n tokens: parseStoredTokens(row.tokens as string),\n }));\n}\n\n/**\n * Set the display name for an OAuth account (e.g. Google profile name).\n */\nexport async function setOAuthDisplayName(\n provider: string,\n accountId: string,\n displayName: string,\n): Promise<void> {\n await ensureTable();\n const client = getDbExec();\n const table = oauthTokensTable();\n await client.execute({\n sql: `UPDATE ${table} SET display_name = ? WHERE provider = ? AND account_id = ?`,\n args: [displayName, provider, accountId],\n });\n}\n\n/**\n * Check whether a specific user has tokens for a provider.\n *\n * `owner` is REQUIRED. The previous unscoped form leaked information\n * across users — the onboarding banner would mark the OAuth secret as\n * \"set\" for user B as soon as ANY user in the deployment connected the\n * provider, and user B would never see the prompt to connect.\n */\nexport async function hasOAuthTokens(\n provider: string,\n owner: string,\n): Promise<boolean> {\n await ensureTable();\n const client = getDbExec();\n const table = oauthTokensTable();\n const { rows } = await client.execute({\n sql: `SELECT 1 FROM ${table} WHERE provider = ? AND owner = ? LIMIT 1`,\n args: [provider, owner],\n });\n return rows.length > 0;\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/scripts/db/index.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/scripts/db/index.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAkBzE,CAAC"}
|
package/dist/scripts/db/index.js
CHANGED
|
@@ -7,6 +7,7 @@ export const coreDbScripts = {
|
|
|
7
7
|
"db-wipe-leaked-builder-keys": (args) => import("./wipe-leaked-builder-keys.js").then((m) => m.default(args)),
|
|
8
8
|
"db-migrate-user-api-keys": (args) => import("./migrate-user-api-keys.js").then((m) => m.default(args)),
|
|
9
9
|
"db-migrate-encrypt-credentials": (args) => import("./migrate-encrypt-credentials.js").then((m) => m.default(args)),
|
|
10
|
+
"db-migrate-encrypt-oauth-tokens": (args) => import("./migrate-encrypt-oauth-tokens.js").then((m) => m.default(args)),
|
|
10
11
|
"db-reset-dev-owner": (args) => import("./reset-dev-owner.js").then((m) => m.default(args)),
|
|
11
12
|
};
|
|
12
13
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/scripts/db/index.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,aAAa,GACxB;IACE,WAAW,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IACzE,UAAU,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IACvE,SAAS,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IACrE,UAAU,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IACvE,kBAAkB,EAAE,CAAC,IAAI,EAAE,EAAE,CAC3B,MAAM,CAAC,oBAAoB,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3D,6BAA6B,EAAE,CAAC,IAAI,EAAE,EAAE,CACtC,MAAM,CAAC,+BAA+B,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IACtE,0BAA0B,EAAE,CAAC,IAAI,EAAE,EAAE,CACnC,MAAM,CAAC,4BAA4B,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IACnE,gCAAgC,EAAE,CAAC,IAAI,EAAE,EAAE,CACzC,MAAM,CAAC,kCAAkC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IACzE,oBAAoB,EAAE,CAAC,IAAI,EAAE,EAAE,CAC7B,MAAM,CAAC,sBAAsB,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9D,CAAC","sourcesContent":["export const coreDbScripts: Record<string, (args: string[]) => Promise<void>> =\n {\n \"db-schema\": (args) => import(\"./schema.js\").then((m) => m.default(args)),\n \"db-query\": (args) => import(\"./query.js\").then((m) => m.default(args)),\n \"db-exec\": (args) => import(\"./exec.js\").then((m) => m.default(args)),\n \"db-patch\": (args) => import(\"./patch.js\").then((m) => m.default(args)),\n \"db-check-scoping\": (args) =>\n import(\"./check-scoping.js\").then((m) => m.default(args)),\n \"db-wipe-leaked-builder-keys\": (args) =>\n import(\"./wipe-leaked-builder-keys.js\").then((m) => m.default(args)),\n \"db-migrate-user-api-keys\": (args) =>\n import(\"./migrate-user-api-keys.js\").then((m) => m.default(args)),\n \"db-migrate-encrypt-credentials\": (args) =>\n import(\"./migrate-encrypt-credentials.js\").then((m) => m.default(args)),\n \"db-reset-dev-owner\": (args) =>\n import(\"./reset-dev-owner.js\").then((m) => m.default(args)),\n };\n"]}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/scripts/db/index.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,aAAa,GACxB;IACE,WAAW,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IACzE,UAAU,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IACvE,SAAS,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IACrE,UAAU,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IACvE,kBAAkB,EAAE,CAAC,IAAI,EAAE,EAAE,CAC3B,MAAM,CAAC,oBAAoB,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3D,6BAA6B,EAAE,CAAC,IAAI,EAAE,EAAE,CACtC,MAAM,CAAC,+BAA+B,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IACtE,0BAA0B,EAAE,CAAC,IAAI,EAAE,EAAE,CACnC,MAAM,CAAC,4BAA4B,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IACnE,gCAAgC,EAAE,CAAC,IAAI,EAAE,EAAE,CACzC,MAAM,CAAC,kCAAkC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IACzE,iCAAiC,EAAE,CAAC,IAAI,EAAE,EAAE,CAC1C,MAAM,CAAC,mCAAmC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1E,oBAAoB,EAAE,CAAC,IAAI,EAAE,EAAE,CAC7B,MAAM,CAAC,sBAAsB,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9D,CAAC","sourcesContent":["export const coreDbScripts: Record<string, (args: string[]) => Promise<void>> =\n {\n \"db-schema\": (args) => import(\"./schema.js\").then((m) => m.default(args)),\n \"db-query\": (args) => import(\"./query.js\").then((m) => m.default(args)),\n \"db-exec\": (args) => import(\"./exec.js\").then((m) => m.default(args)),\n \"db-patch\": (args) => import(\"./patch.js\").then((m) => m.default(args)),\n \"db-check-scoping\": (args) =>\n import(\"./check-scoping.js\").then((m) => m.default(args)),\n \"db-wipe-leaked-builder-keys\": (args) =>\n import(\"./wipe-leaked-builder-keys.js\").then((m) => m.default(args)),\n \"db-migrate-user-api-keys\": (args) =>\n import(\"./migrate-user-api-keys.js\").then((m) => m.default(args)),\n \"db-migrate-encrypt-credentials\": (args) =>\n import(\"./migrate-encrypt-credentials.js\").then((m) => m.default(args)),\n \"db-migrate-encrypt-oauth-tokens\": (args) =>\n import(\"./migrate-encrypt-oauth-tokens.js\").then((m) => m.default(args)),\n \"db-reset-dev-owner\": (args) =>\n import(\"./reset-dev-owner.js\").then((m) => m.default(args)),\n };\n"]}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core script: db-migrate-encrypt-oauth-tokens
|
|
3
|
+
*
|
|
4
|
+
* One-shot, in-place migration: encrypt any plaintext `tokens` payloads in the
|
|
5
|
+
* `oauth_tokens` table with the same AES-256-GCM scheme the secrets vault and
|
|
6
|
+
* per-user credentials use.
|
|
7
|
+
*
|
|
8
|
+
* Background. `oauth_tokens` historically stored the full OAuth bundle —
|
|
9
|
+
* including long-lived Google refresh tokens — as plaintext JSON. Writes are
|
|
10
|
+
* now encrypted at rest, and reads transparently fall back to plaintext, so
|
|
11
|
+
* this migration is OPTIONAL: it re-encrypts existing rows so a leaked DB
|
|
12
|
+
* backup / pg_dump / read replica no longer exposes usable refresh/access
|
|
13
|
+
* tokens.
|
|
14
|
+
*
|
|
15
|
+
* Non-destructive: it only rewrites the `tokens` value of each row in place
|
|
16
|
+
* (no row is dropped). Idempotent: already-encrypted rows are skipped.
|
|
17
|
+
*
|
|
18
|
+
* IMPORTANT: run with the SAME SECRETS_ENCRYPTION_KEY / BETTER_AUTH_SECRET the
|
|
19
|
+
* app uses, or the app won't be able to decrypt the result. The script refuses
|
|
20
|
+
* to run without an explicit key.
|
|
21
|
+
*
|
|
22
|
+
* Usage:
|
|
23
|
+
* DATABASE_URL=postgres://... SECRETS_ENCRYPTION_KEY=... pnpm action db-migrate-encrypt-oauth-tokens
|
|
24
|
+
* pnpm action db-migrate-encrypt-oauth-tokens --db ./data/app.db
|
|
25
|
+
* pnpm action db-migrate-encrypt-oauth-tokens --dry-run
|
|
26
|
+
*/
|
|
27
|
+
export default function dbMigrateEncryptOAuthTokens(args: string[]): Promise<void>;
|
|
28
|
+
//# sourceMappingURL=migrate-encrypt-oauth-tokens.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"migrate-encrypt-oauth-tokens.d.ts","sourceRoot":"","sources":["../../../src/scripts/db/migrate-encrypt-oauth-tokens.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AA2BH,wBAA8B,2BAA2B,CACvD,IAAI,EAAE,MAAM,EAAE,GACb,OAAO,CAAC,IAAI,CAAC,CAiFf"}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core script: db-migrate-encrypt-oauth-tokens
|
|
3
|
+
*
|
|
4
|
+
* One-shot, in-place migration: encrypt any plaintext `tokens` payloads in the
|
|
5
|
+
* `oauth_tokens` table with the same AES-256-GCM scheme the secrets vault and
|
|
6
|
+
* per-user credentials use.
|
|
7
|
+
*
|
|
8
|
+
* Background. `oauth_tokens` historically stored the full OAuth bundle —
|
|
9
|
+
* including long-lived Google refresh tokens — as plaintext JSON. Writes are
|
|
10
|
+
* now encrypted at rest, and reads transparently fall back to plaintext, so
|
|
11
|
+
* this migration is OPTIONAL: it re-encrypts existing rows so a leaked DB
|
|
12
|
+
* backup / pg_dump / read replica no longer exposes usable refresh/access
|
|
13
|
+
* tokens.
|
|
14
|
+
*
|
|
15
|
+
* Non-destructive: it only rewrites the `tokens` value of each row in place
|
|
16
|
+
* (no row is dropped). Idempotent: already-encrypted rows are skipped.
|
|
17
|
+
*
|
|
18
|
+
* IMPORTANT: run with the SAME SECRETS_ENCRYPTION_KEY / BETTER_AUTH_SECRET the
|
|
19
|
+
* app uses, or the app won't be able to decrypt the result. The script refuses
|
|
20
|
+
* to run without an explicit key.
|
|
21
|
+
*
|
|
22
|
+
* Usage:
|
|
23
|
+
* DATABASE_URL=postgres://... SECRETS_ENCRYPTION_KEY=... pnpm action db-migrate-encrypt-oauth-tokens
|
|
24
|
+
* pnpm action db-migrate-encrypt-oauth-tokens --db ./data/app.db
|
|
25
|
+
* pnpm action db-migrate-encrypt-oauth-tokens --dry-run
|
|
26
|
+
*/
|
|
27
|
+
import path from "path";
|
|
28
|
+
import { createClient } from "@libsql/client";
|
|
29
|
+
import { getDatabaseUrl, getDatabaseAuthToken } from "../../db/client.js";
|
|
30
|
+
import { parseArgs } from "../utils.js";
|
|
31
|
+
import { encryptSecretValue, isEncryptedSecretValue, } from "../../secrets/crypto.js";
|
|
32
|
+
function isPostgresUrl(url) {
|
|
33
|
+
return url.startsWith("postgres://") || url.startsWith("postgresql://");
|
|
34
|
+
}
|
|
35
|
+
function mask(value) {
|
|
36
|
+
if (!value)
|
|
37
|
+
return "(empty)";
|
|
38
|
+
if (value.length <= 8)
|
|
39
|
+
return "***";
|
|
40
|
+
return `${value.slice(0, 3)}…${value.slice(-3)} (len=${value.length})`;
|
|
41
|
+
}
|
|
42
|
+
export default async function dbMigrateEncryptOAuthTokens(args) {
|
|
43
|
+
const parsed = parseArgs(args);
|
|
44
|
+
if (parsed.help === "true") {
|
|
45
|
+
console.log(`Usage: pnpm action db-migrate-encrypt-oauth-tokens [options]
|
|
46
|
+
|
|
47
|
+
Encrypts plaintext token payloads in the oauth_tokens table, in place.
|
|
48
|
+
Idempotent and non-destructive (skips already-encrypted rows; never deletes a
|
|
49
|
+
row).
|
|
50
|
+
|
|
51
|
+
Run with the same SECRETS_ENCRYPTION_KEY / BETTER_AUTH_SECRET the app uses.
|
|
52
|
+
|
|
53
|
+
Options:
|
|
54
|
+
--db <path> Path to SQLite database (default: data/app.db)
|
|
55
|
+
--dry-run Print what would be encrypted without writing
|
|
56
|
+
--help Show this help message`);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (!process.env.SECRETS_ENCRYPTION_KEY && !process.env.BETTER_AUTH_SECRET) {
|
|
60
|
+
console.error("[migrate-encrypt-oauth-tokens] Refusing to run without SECRETS_ENCRYPTION_KEY or BETTER_AUTH_SECRET set. " +
|
|
61
|
+
"Encrypting with the machine-local fallback would produce values the app cannot decrypt.");
|
|
62
|
+
throw new Error("Missing encryption key");
|
|
63
|
+
}
|
|
64
|
+
const dryRun = parsed["dry-run"] === "true";
|
|
65
|
+
let url;
|
|
66
|
+
if (parsed.db) {
|
|
67
|
+
url = "file:" + path.resolve(parsed.db);
|
|
68
|
+
}
|
|
69
|
+
else if (getDatabaseUrl()) {
|
|
70
|
+
url = getDatabaseUrl();
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
url = "file:" + path.resolve(process.cwd(), "data", "app.db");
|
|
74
|
+
}
|
|
75
|
+
const dbLabel = url.startsWith("file:")
|
|
76
|
+
? url.slice("file:".length)
|
|
77
|
+
: new URL(url).host || url;
|
|
78
|
+
console.log(`[migrate-encrypt-oauth-tokens] target: ${dbLabel}${dryRun ? " (dry-run)" : ""}`);
|
|
79
|
+
const rows = await fetchPlaintextTokenRows(url);
|
|
80
|
+
if (rows.length === 0) {
|
|
81
|
+
console.log("[migrate-encrypt-oauth-tokens] nothing to encrypt.");
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
console.log(`[migrate-encrypt-oauth-tokens] found ${rows.length} plaintext token row(s):`);
|
|
85
|
+
for (const row of rows) {
|
|
86
|
+
console.log(` - ${row.provider}:${row.accountId} ${mask(row.plaintext)}`);
|
|
87
|
+
}
|
|
88
|
+
if (dryRun)
|
|
89
|
+
return;
|
|
90
|
+
let encrypted = 0;
|
|
91
|
+
let failed = 0;
|
|
92
|
+
for (const row of rows) {
|
|
93
|
+
try {
|
|
94
|
+
await updateRow(url, row.provider, row.accountId, encryptSecretValue(row.plaintext));
|
|
95
|
+
encrypted++;
|
|
96
|
+
}
|
|
97
|
+
catch (err) {
|
|
98
|
+
console.error(` ! failed to encrypt ${row.provider}:${row.accountId}:`, err instanceof Error ? err.message : err);
|
|
99
|
+
failed++;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
console.log(`[migrate-encrypt-oauth-tokens] done. encrypted=${encrypted} failed=${failed}`);
|
|
103
|
+
}
|
|
104
|
+
async function fetchPlaintextTokenRows(url) {
|
|
105
|
+
const out = [];
|
|
106
|
+
const collect = (provider, accountId, tokens) => {
|
|
107
|
+
if (typeof tokens !== "string" || tokens.length === 0)
|
|
108
|
+
return;
|
|
109
|
+
// Already encrypted → leave it (idempotent).
|
|
110
|
+
if (isEncryptedSecretValue(tokens))
|
|
111
|
+
return;
|
|
112
|
+
out.push({ provider, accountId, plaintext: tokens });
|
|
113
|
+
};
|
|
114
|
+
if (isPostgresUrl(url)) {
|
|
115
|
+
const { default: pg } = await import("postgres");
|
|
116
|
+
const sql = pg(url);
|
|
117
|
+
try {
|
|
118
|
+
const rows = (await sql.unsafe(`SELECT provider, account_id, tokens FROM oauth_tokens`));
|
|
119
|
+
for (const row of rows)
|
|
120
|
+
collect(row.provider, row.account_id, row.tokens);
|
|
121
|
+
}
|
|
122
|
+
finally {
|
|
123
|
+
await sql.end();
|
|
124
|
+
}
|
|
125
|
+
return out;
|
|
126
|
+
}
|
|
127
|
+
const client = createClient({ url, authToken: getDatabaseAuthToken() });
|
|
128
|
+
try {
|
|
129
|
+
const result = await client.execute({
|
|
130
|
+
sql: `SELECT provider, account_id, tokens FROM oauth_tokens`,
|
|
131
|
+
});
|
|
132
|
+
for (const row of result.rows) {
|
|
133
|
+
collect(row.provider, row.account_id, row.tokens);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
finally {
|
|
137
|
+
client.close();
|
|
138
|
+
}
|
|
139
|
+
return out;
|
|
140
|
+
}
|
|
141
|
+
async function updateRow(url, provider, accountId, tokens) {
|
|
142
|
+
if (isPostgresUrl(url)) {
|
|
143
|
+
const { default: pg } = await import("postgres");
|
|
144
|
+
const sql = pg(url);
|
|
145
|
+
try {
|
|
146
|
+
await sql.unsafe(`UPDATE oauth_tokens SET tokens = $1 WHERE provider = $2 AND account_id = $3`, [tokens, provider, accountId]);
|
|
147
|
+
}
|
|
148
|
+
finally {
|
|
149
|
+
await sql.end();
|
|
150
|
+
}
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
const client = createClient({ url, authToken: getDatabaseAuthToken() });
|
|
154
|
+
try {
|
|
155
|
+
await client.execute({
|
|
156
|
+
sql: `UPDATE oauth_tokens SET tokens = ? WHERE provider = ? AND account_id = ?`,
|
|
157
|
+
args: [tokens, provider, accountId],
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
finally {
|
|
161
|
+
client.close();
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
//# sourceMappingURL=migrate-encrypt-oauth-tokens.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"migrate-encrypt-oauth-tokens.js","sourceRoot":"","sources":["../../../src/scripts/db/migrate-encrypt-oauth-tokens.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Core script: db-migrate-encrypt-oauth-tokens\n *\n * One-shot, in-place migration: encrypt any plaintext `tokens` payloads in the\n * `oauth_tokens` table with the same AES-256-GCM scheme the secrets vault and\n * per-user credentials use.\n *\n * Background. `oauth_tokens` historically stored the full OAuth bundle —\n * including long-lived Google refresh tokens — as plaintext JSON. Writes are\n * now encrypted at rest, and reads transparently fall back to plaintext, so\n * this migration is OPTIONAL: it re-encrypts existing rows so a leaked DB\n * backup / pg_dump / read replica no longer exposes usable refresh/access\n * tokens.\n *\n * Non-destructive: it only rewrites the `tokens` value of each row in place\n * (no row is dropped). Idempotent: already-encrypted rows are skipped.\n *\n * IMPORTANT: run with the SAME SECRETS_ENCRYPTION_KEY / BETTER_AUTH_SECRET the\n * app uses, or the app won't be able to decrypt the result. The script refuses\n * to run without an explicit key.\n *\n * Usage:\n * DATABASE_URL=postgres://... SECRETS_ENCRYPTION_KEY=... pnpm action db-migrate-encrypt-oauth-tokens\n * pnpm action db-migrate-encrypt-oauth-tokens --db ./data/app.db\n * pnpm action db-migrate-encrypt-oauth-tokens --dry-run\n */\n\nimport path from \"path\";\nimport { createClient } from \"@libsql/client\";\nimport { getDatabaseUrl, getDatabaseAuthToken } from \"../../db/client.js\";\nimport { parseArgs } from \"../utils.js\";\nimport {\n encryptSecretValue,\n isEncryptedSecretValue,\n} from \"../../secrets/crypto.js\";\n\ninterface OAuthTokenRow {\n provider: string;\n accountId: string;\n plaintext: string;\n}\n\nfunction isPostgresUrl(url: string): boolean {\n return url.startsWith(\"postgres://\") || url.startsWith(\"postgresql://\");\n}\n\nfunction mask(value: string): string {\n if (!value) return \"(empty)\";\n if (value.length <= 8) return \"***\";\n return `${value.slice(0, 3)}…${value.slice(-3)} (len=${value.length})`;\n}\n\nexport default async function dbMigrateEncryptOAuthTokens(\n args: string[],\n): Promise<void> {\n const parsed = parseArgs(args);\n\n if (parsed.help === \"true\") {\n console.log(`Usage: pnpm action db-migrate-encrypt-oauth-tokens [options]\n\nEncrypts plaintext token payloads in the oauth_tokens table, in place.\nIdempotent and non-destructive (skips already-encrypted rows; never deletes a\nrow).\n\nRun with the same SECRETS_ENCRYPTION_KEY / BETTER_AUTH_SECRET the app uses.\n\nOptions:\n --db <path> Path to SQLite database (default: data/app.db)\n --dry-run Print what would be encrypted without writing\n --help Show this help message`);\n return;\n }\n\n if (!process.env.SECRETS_ENCRYPTION_KEY && !process.env.BETTER_AUTH_SECRET) {\n console.error(\n \"[migrate-encrypt-oauth-tokens] Refusing to run without SECRETS_ENCRYPTION_KEY or BETTER_AUTH_SECRET set. \" +\n \"Encrypting with the machine-local fallback would produce values the app cannot decrypt.\",\n );\n throw new Error(\"Missing encryption key\");\n }\n\n const dryRun = parsed[\"dry-run\"] === \"true\";\n\n let url: string;\n if (parsed.db) {\n url = \"file:\" + path.resolve(parsed.db);\n } else if (getDatabaseUrl()) {\n url = getDatabaseUrl();\n } else {\n url = \"file:\" + path.resolve(process.cwd(), \"data\", \"app.db\");\n }\n\n const dbLabel = url.startsWith(\"file:\")\n ? url.slice(\"file:\".length)\n : new URL(url).host || url;\n console.log(\n `[migrate-encrypt-oauth-tokens] target: ${dbLabel}${dryRun ? \" (dry-run)\" : \"\"}`,\n );\n\n const rows = await fetchPlaintextTokenRows(url);\n if (rows.length === 0) {\n console.log(\"[migrate-encrypt-oauth-tokens] nothing to encrypt.\");\n return;\n }\n\n console.log(\n `[migrate-encrypt-oauth-tokens] found ${rows.length} plaintext token row(s):`,\n );\n for (const row of rows) {\n console.log(` - ${row.provider}:${row.accountId} ${mask(row.plaintext)}`);\n }\n if (dryRun) return;\n\n let encrypted = 0;\n let failed = 0;\n for (const row of rows) {\n try {\n await updateRow(\n url,\n row.provider,\n row.accountId,\n encryptSecretValue(row.plaintext),\n );\n encrypted++;\n } catch (err) {\n console.error(\n ` ! failed to encrypt ${row.provider}:${row.accountId}:`,\n err instanceof Error ? err.message : err,\n );\n failed++;\n }\n }\n console.log(\n `[migrate-encrypt-oauth-tokens] done. encrypted=${encrypted} failed=${failed}`,\n );\n}\n\nasync function fetchPlaintextTokenRows(url: string): Promise<OAuthTokenRow[]> {\n const out: OAuthTokenRow[] = [];\n const collect = (provider: string, accountId: string, tokens: string) => {\n if (typeof tokens !== \"string\" || tokens.length === 0) return;\n // Already encrypted → leave it (idempotent).\n if (isEncryptedSecretValue(tokens)) return;\n out.push({ provider, accountId, plaintext: tokens });\n };\n\n if (isPostgresUrl(url)) {\n const { default: pg } = await import(\"postgres\");\n const sql = pg(url);\n try {\n const rows = (await sql.unsafe(\n `SELECT provider, account_id, tokens FROM oauth_tokens`,\n )) as unknown as Array<{\n provider: string;\n account_id: string;\n tokens: string;\n }>;\n for (const row of rows) collect(row.provider, row.account_id, row.tokens);\n } finally {\n await sql.end();\n }\n return out;\n }\n\n const client = createClient({ url, authToken: getDatabaseAuthToken() });\n try {\n const result = await client.execute({\n sql: `SELECT provider, account_id, tokens FROM oauth_tokens`,\n });\n for (const row of result.rows) {\n collect(\n row.provider as string,\n row.account_id as string,\n row.tokens as string,\n );\n }\n } finally {\n client.close();\n }\n return out;\n}\n\nasync function updateRow(\n url: string,\n provider: string,\n accountId: string,\n tokens: string,\n): Promise<void> {\n if (isPostgresUrl(url)) {\n const { default: pg } = await import(\"postgres\");\n const sql = pg(url);\n try {\n await sql.unsafe(\n `UPDATE oauth_tokens SET tokens = $1 WHERE provider = $2 AND account_id = $3`,\n [tokens, provider, accountId],\n );\n } finally {\n await sql.end();\n }\n return;\n }\n const client = createClient({ url, authToken: getDatabaseAuthToken() });\n try {\n await client.execute({\n sql: `UPDATE oauth_tokens SET tokens = ? WHERE provider = ? AND account_id = ?`,\n args: [tokens, provider, accountId],\n });\n } finally {\n client.close();\n }\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scoping.d.ts","sourceRoot":"","sources":["../../../src/scripts/db/scoping.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;
|
|
1
|
+
{"version":3,"file":"scoping.d.ts","sourceRoot":"","sources":["../../../src/scripts/db/scoping.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AA+NH,MAAM,WAAW,cAAc;IAC7B,yEAAyE;IACzE,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,sEAAsE;IACtE,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,iCAAiC;IACjC,MAAM,EAAE,OAAO,CAAC;IAChB,gEAAgE;IAChE,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,4DAA4D;IAC5D,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,mEAAmE;IACnE,gBAAgB,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC9B,8DAA8D;IAC9D,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACzB,0DAA0D;IAC1D,eAAe,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACtC;AAED;;;GAGG;AACH,wBAAsB,oBAAoB,CACxC,KAAK,EAAE,GAAG,GACT,OAAO,CAAC,cAAc,CAAC,CAsCzB;AAED;;;GAGG;AACH,wBAAsB,kBAAkB,CAAC,MAAM,EAAE,GAAG,GAAG,OAAO,CAAC,cAAc,CAAC,CAiC7E"}
|
|
@@ -120,11 +120,13 @@ function buildScopedTables(allColumns, userEmail, orgId, isPostgres) {
|
|
|
120
120
|
.replace(/_/g, "\\_");
|
|
121
121
|
const prefix = `u:${likeEmail}:`;
|
|
122
122
|
// Hide per-user credential rows (u:<email>:credential:<KEY>) from the
|
|
123
|
-
// raw db-query/db-exec tools.
|
|
124
|
-
// third-party tokens
|
|
125
|
-
// read them via SQL — it uses them implicitly server-side.
|
|
126
|
-
// them from the view
|
|
127
|
-
// (read own secret → send to
|
|
123
|
+
// raw db-query/db-exec tools. saveCredential() now encrypts API keys
|
|
124
|
+
// and third-party tokens at rest (AES-256-GCM), but the agent never
|
|
125
|
+
// needs to read them via SQL — it uses them implicitly server-side.
|
|
126
|
+
// Excluding them from the view is defense-in-depth: it removes a
|
|
127
|
+
// prompt-injection exfiltration channel (read own secret → send to
|
|
128
|
+
// attacker URL) and also hides any legacy plaintext rows that predate
|
|
129
|
+
// encryption plus the recoverable last4/preview. Schema-qualified attempts
|
|
128
130
|
// to reach the base table (public.settings / main.settings) are
|
|
129
131
|
// rejected separately by assertNoSchemaQualifiedTables in safety.ts.
|
|
130
132
|
whereSql =
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scoping.js","sourceRoot":"","sources":["../../../src/scripts/db/scoping.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Per-user and per-org data scoping for db-query / db-exec.\n *\n * In production mode, creates temporary views that shadow real tables so\n * that raw SQL only sees the current user's (and org's) data.\n *\n * Convention:\n * - Template tables use an `owner_email` column for user scoping.\n * - Template tables use an `org_id` column for org scoping.\n * - Core tables have their own scoping patterns (key prefix, session_id, etc.).\n * - When both columns are present, owner_email is always required; org_id\n * narrows to the current org while preserving legacy/personal NULL rows.\n *\n * Temp views take precedence over real tables in both SQLite and Postgres,\n * so the user's SQL runs unmodified against the filtered views.\n */\n\n// Core tables with non-standard scoping (not owner_email).\n// Map of table name → { column, mode }.\nconst CORE_TABLE_SCOPING: Record<\n string,\n { column: string; mode: \"prefix\" | \"exact\" }\n> = {\n settings: { column: \"key\", mode: \"prefix\" }, // keys like u:<email>:<key>\n application_state: { column: \"session_id\", mode: \"exact\" },\n oauth_tokens: { column: \"owner\", mode: \"exact\" },\n resources: { column: \"owner\", mode: \"exact\" },\n sessions: { column: \"email\", mode: \"exact\" },\n};\n\n// The conventional column names for user/org ownership in template tables.\nimport {\n getRequestUserEmail,\n getRequestOrgId,\n} from \"../../server/request-context.js\";\n\nconst OWNER_COLUMN = \"owner_email\";\nconst ORG_COLUMN = \"org_id\";\nconst DEV_FALLBACK_EMAIL = \"local@localhost\"; // guard:allow-localhost-fallback — sentinel is rejected below so DB scripts cannot silently scope to the dev fallback tenant\n\ninterface ScopedTable {\n name: string;\n viewSql: string;\n predicate: string;\n}\n\nfunction getUserEmail(): string {\n const userEmail = getRequestUserEmail() || null;\n if (!userEmail || userEmail === DEV_FALLBACK_EMAIL) {\n throw new Error(\n \"db-exec / db-query / db-patch require an authenticated user identity. \" +\n \"Easiest fix: open the app at http://localhost:3000 and sign in — \" +\n \"the CLI then auto-loads your session. Otherwise set \" +\n \"AGENT_USER_EMAIL=<email> in the env, or invoke through an HTTP \" +\n \"action that runs under runWithRequestContext. Refusing to run unscoped — \" +\n \"an unscoped UPDATE/DELETE would touch every user's rows, and an \" +\n \"unscoped INSERT would land with the dev sentinel owner and be invisible \" +\n \"to the UI.\",\n );\n }\n return userEmail;\n}\n\nfunction getOrgId(): string | null {\n return getRequestOrgId() || null;\n}\n\n// ─── Schema introspection ───────────────────────────────────────────────────\n\ninterface TableColumn {\n table: string;\n column: string;\n}\n\nasync function discoverColumnsPostgres(pgSql: any): Promise<TableColumn[]> {\n const rows: any[] = await pgSql`\n SELECT table_name, column_name\n FROM information_schema.columns\n WHERE table_schema = 'public'\n ORDER BY table_name, ordinal_position\n `;\n return rows.map((r) => ({ table: r.table_name, column: r.column_name }));\n}\n\nasync function discoverColumnsSqlite(client: any): Promise<TableColumn[]> {\n const tablesResult = await client.execute(\n `SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'`,\n );\n const tables = tablesResult.rows.map((r: any) => (r.name ?? r[0]) as string);\n\n const result: TableColumn[] = [];\n for (const table of tables) {\n const escaped = table.replace(/\"/g, '\"\"');\n const colsResult = await client.execute(`PRAGMA table_info(\"${escaped}\")`);\n for (const row of colsResult.rows) {\n result.push({\n table,\n column: (row.name ?? row[1]) as string,\n });\n }\n }\n return result;\n}\n\n// ─── View generation ────────────────────────────────────────────────────────\n\n/** Escape a string for safe inclusion in a SQL single-quoted literal. */\nfunction escapeSqlString(value: string): string {\n return value.replace(/'/g, \"''\");\n}\n\nfunction escapeIdentifier(value: string): string {\n return value.replace(/\"/g, '\"\"');\n}\n\nfunction buildScopedTables(\n allColumns: TableColumn[],\n userEmail: string,\n orgId: string | null,\n isPostgres: boolean,\n): ScopedTable[] {\n // Group columns by table\n const columnsByTable = new Map<string, string[]>();\n for (const { table, column } of allColumns) {\n const cols = columnsByTable.get(table) || [];\n cols.push(column);\n columnsByTable.set(table, cols);\n }\n\n const scoped: ScopedTable[] = [];\n const qualifiedPrefix = isPostgres ? \"public.\" : \"main.\";\n const safeEmail = escapeSqlString(userEmail);\n const safeOrgId = orgId ? escapeSqlString(orgId) : null;\n\n // WITH CHECK OPTION ensures INSERTs/UPDATEs through the auto-updatable view\n // can't write rows that violate the WHERE filter. Without it, an attacker\n // could `INSERT INTO recordings (..., owner_email) VALUES (..., 'victim@x')`\n // through the view and the row would land in the base table under the\n // victim's identity. SQLite views are not auto-updatable in the same way\n // (they require triggers), so this clause is a no-op there but harmless.\n const checkOption = isPostgres ? \" WITH LOCAL CHECK OPTION\" : \"\";\n\n const viewFor = (table: string, whereSql: string): ScopedTable => {\n const escapedTable = escapeIdentifier(table);\n const realTable = `${qualifiedPrefix}\"${escapedTable}\"`;\n return {\n name: table,\n predicate: whereSql,\n viewSql: `${isPostgres ? \"CREATE OR REPLACE TEMPORARY\" : \"CREATE TEMPORARY\"} VIEW \"${escapedTable}\" AS SELECT * FROM ${realTable} WHERE ${whereSql}${checkOption}`,\n };\n };\n\n for (const [table, columns] of columnsByTable) {\n // Check core table scoping\n const coreScoping = CORE_TABLE_SCOPING[table];\n if (coreScoping) {\n let whereSql: string;\n if (coreScoping.mode === \"prefix\") {\n // settings: key starts with u:<email>:\n // Escape \\, % and _ in the email so LIKE treats them literally.\n const likeEmail = safeEmail\n .replace(/\\\\/g, \"\\\\\\\\\")\n .replace(/%/g, \"\\\\%\")\n .replace(/_/g, \"\\\\_\");\n const prefix = `u:${likeEmail}:`;\n // Hide per-user credential rows (u:<email>:credential:<KEY>) from the\n // raw db-query/db-exec tools. resolveCredential() stores API keys and\n // third-party tokens here as plaintext, and the agent never needs to\n // read them via SQL — it uses them implicitly server-side. Excluding\n // them from the view removes a prompt-injection exfiltration channel\n // (read own secret → send to attacker URL). Schema-qualified attempts\n // to reach the base table (public.settings / main.settings) are\n // rejected separately by assertNoSchemaQualifiedTables in safety.ts.\n whereSql =\n `\"${coreScoping.column}\" LIKE '${prefix}%' ESCAPE '\\\\'` +\n ` AND \"${coreScoping.column}\" NOT LIKE '${prefix}credential:%' ESCAPE '\\\\'`;\n } else {\n whereSql = `\"${coreScoping.column}\" = '${safeEmail}'`;\n }\n scoped.push(viewFor(table, whereSql));\n continue;\n }\n\n if (\n table === \"tool_data\" &&\n columns.includes(\"scope\") &&\n columns.includes(OWNER_COLUMN) &&\n columns.includes(ORG_COLUMN)\n ) {\n const orgClause = safeOrgId\n ? ` OR (\"scope\" = 'org' AND \"${ORG_COLUMN}\" = '${safeOrgId}')`\n : \"\";\n scoped.push(\n viewFor(\n table,\n `((\"scope\" = 'user' AND \"${OWNER_COLUMN}\" = '${safeEmail}')${orgClause})`,\n ),\n );\n continue;\n }\n\n const hasOwner = columns.includes(OWNER_COLUMN);\n const hasOrg = columns.includes(ORG_COLUMN);\n\n if (hasOwner) {\n const orgClause =\n hasOrg && safeOrgId\n ? ` AND (\"${ORG_COLUMN}\" = '${safeOrgId}' OR \"${ORG_COLUMN}\" IS NULL)`\n : \"\";\n scoped.push(\n viewFor(table, `\"${OWNER_COLUMN}\" = '${safeEmail}'${orgClause}`),\n );\n continue;\n }\n\n if (hasOrg) {\n scoped.push(\n viewFor(\n table,\n safeOrgId ? `\"${ORG_COLUMN}\" = '${safeOrgId}'` : \"1 = 0\",\n ),\n );\n continue;\n }\n\n // Fail closed for tables that do not advertise a scoping convention.\n // Without this shadow view, a forgotten owner_email/org_id column turns\n // into raw cross-tenant SELECT/UPDATE/DELETE access for db-* tools.\n scoped.push(viewFor(table, \"1 = 0\"));\n }\n\n return scoped;\n}\n\n// ─── Public API ─────────────────────────────────────────────────────────────\n\nexport interface ScopingContext {\n /** SQL statements to run before the user's query (create temp views). */\n setup: string[];\n /** SQL statements to run after the user's query (drop temp views). */\n teardown: string[];\n /** Whether scoping is active. */\n active: boolean;\n /** The current user email (for INSERT injection in db-exec). */\n userEmail: string | null;\n /** The current org ID (for INSERT injection in db-exec). */\n orgId: string | null;\n /** Tables that have owner_email columns (for INSERT injection). */\n ownerEmailTables: Set<string>;\n /** Tables that have org_id columns (for INSERT injection). */\n orgIdTables: Set<string>;\n /** Table predicates applied by the scoping temp views. */\n tablePredicates: Map<string, string>;\n}\n\n/**\n * Build scoping context for a Postgres connection.\n * Returns setup/teardown SQL to run before/after the user's query.\n */\nexport async function buildScopingPostgres(\n pgSql: any,\n): Promise<ScopingContext> {\n // getUserEmail() throws when there is no authenticated user (no request\n // context AND no AGENT_USER_EMAIL env) or when it resolves to the dev\n // sentinel `local@localhost`. We let that throw propagate: the script\n // refuses to run unscoped rather than silently writing rows that the UI\n // then can't see, or running an UPDATE/DELETE across every user's data.\n const userEmail = getUserEmail();\n\n const orgId = getOrgId();\n const allColumns = await discoverColumnsPostgres(pgSql);\n const scoped = buildScopedTables(allColumns, userEmail, orgId, true);\n\n // Track which tables have owner_email / org_id for INSERT injection\n const columnsByTable = new Map<string, string[]>();\n for (const { table, column } of allColumns) {\n const cols = columnsByTable.get(table) || [];\n cols.push(column);\n columnsByTable.set(table, cols);\n }\n const ownerEmailTables = new Set<string>();\n const orgIdTables = new Set<string>();\n for (const [table, columns] of columnsByTable) {\n if (columns.includes(OWNER_COLUMN)) ownerEmailTables.add(table);\n if (columns.includes(ORG_COLUMN)) orgIdTables.add(table);\n }\n\n return {\n setup: scoped.map((s) => s.viewSql),\n teardown: scoped.map(\n (s) => `DROP VIEW IF EXISTS pg_temp.\"${escapeIdentifier(s.name)}\"`,\n ),\n active: scoped.length > 0,\n userEmail,\n orgId,\n ownerEmailTables,\n orgIdTables,\n tablePredicates: new Map(scoped.map((s) => [s.name, s.predicate])),\n };\n}\n\n/**\n * Build scoping context for a SQLite/libsql connection.\n * Returns setup/teardown SQL to run before/after the user's query.\n */\nexport async function buildScopingSqlite(client: any): Promise<ScopingContext> {\n // See buildScopingPostgres: getUserEmail() throws on no user / dev sentinel.\n const userEmail = getUserEmail();\n\n const orgId = getOrgId();\n const allColumns = await discoverColumnsSqlite(client);\n const scoped = buildScopedTables(allColumns, userEmail, orgId, false);\n\n const columnsByTable = new Map<string, string[]>();\n for (const { table, column } of allColumns) {\n const cols = columnsByTable.get(table) || [];\n cols.push(column);\n columnsByTable.set(table, cols);\n }\n const ownerEmailTables = new Set<string>();\n const orgIdTables = new Set<string>();\n for (const [table, columns] of columnsByTable) {\n if (columns.includes(OWNER_COLUMN)) ownerEmailTables.add(table);\n if (columns.includes(ORG_COLUMN)) orgIdTables.add(table);\n }\n\n return {\n setup: scoped.map((s) => s.viewSql),\n teardown: scoped.map(\n (s) => `DROP VIEW IF EXISTS \"${escapeIdentifier(s.name)}\"`,\n ),\n active: scoped.length > 0,\n userEmail,\n orgId,\n ownerEmailTables,\n orgIdTables,\n tablePredicates: new Map(scoped.map((s) => [s.name, s.predicate])),\n };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"scoping.js","sourceRoot":"","sources":["../../../src/scripts/db/scoping.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,2DAA2D;AAC3D,wCAAwC;AACxC,MAAM,kBAAkB,GAGpB;IACF,QAAQ,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE,4BAA4B;IACzE,iBAAiB,EAAE,EAAE,MAAM,EAAE,YAAY,EAAE,IAAI,EAAE,OAAO,EAAE;IAC1D,YAAY,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE;IAChD,SAAS,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE;IAC7C,QAAQ,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE;CAC7C,CAAC;AAEF,2EAA2E;AAC3E,OAAO,EACL,mBAAmB,EACnB,eAAe,GAChB,MAAM,iCAAiC,CAAC;AAEzC,MAAM,YAAY,GAAG,aAAa,CAAC;AACnC,MAAM,UAAU,GAAG,QAAQ,CAAC;AAC5B,MAAM,kBAAkB,GAAG,iBAAiB,CAAC,CAAC,6HAA6H;AAQ3K,SAAS,YAAY;IACnB,MAAM,SAAS,GAAG,mBAAmB,EAAE,IAAI,IAAI,CAAC;IAChD,IAAI,CAAC,SAAS,IAAI,SAAS,KAAK,kBAAkB,EAAE,CAAC;QACnD,MAAM,IAAI,KAAK,CACb,wEAAwE;YACtE,mEAAmE;YACnE,sDAAsD;YACtD,iEAAiE;YACjE,2EAA2E;YAC3E,kEAAkE;YAClE,0EAA0E;YAC1E,YAAY,CACf,CAAC;IACJ,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,SAAS,QAAQ;IACf,OAAO,eAAe,EAAE,IAAI,IAAI,CAAC;AACnC,CAAC;AASD,KAAK,UAAU,uBAAuB,CAAC,KAAU;IAC/C,MAAM,IAAI,GAAU,MAAM,KAAK,CAAA;;;;;GAK9B,CAAC;IACF,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,UAAU,EAAE,MAAM,EAAE,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC;AAC3E,CAAC;AAED,KAAK,UAAU,qBAAqB,CAAC,MAAW;IAC9C,MAAM,YAAY,GAAG,MAAM,MAAM,CAAC,OAAO,CACvC,gFAAgF,CACjF,CAAC;IACF,MAAM,MAAM,GAAG,YAAY,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAM,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,CAAW,CAAC,CAAC;IAE7E,MAAM,MAAM,GAAkB,EAAE,CAAC;IACjC,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;QAC3B,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;QAC1C,MAAM,UAAU,GAAG,MAAM,MAAM,CAAC,OAAO,CAAC,sBAAsB,OAAO,IAAI,CAAC,CAAC;QAC3E,KAAK,MAAM,GAAG,IAAI,UAAU,CAAC,IAAI,EAAE,CAAC;YAClC,MAAM,CAAC,IAAI,CAAC;gBACV,KAAK;gBACL,MAAM,EAAE,CAAC,GAAG,CAAC,IAAI,IAAI,GAAG,CAAC,CAAC,CAAC,CAAW;aACvC,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,+EAA+E;AAE/E,yEAAyE;AACzE,SAAS,eAAe,CAAC,KAAa;IACpC,OAAO,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;AACnC,CAAC;AAED,SAAS,gBAAgB,CAAC,KAAa;IACrC,OAAO,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;AACnC,CAAC;AAED,SAAS,iBAAiB,CACxB,UAAyB,EACzB,SAAiB,EACjB,KAAoB,EACpB,UAAmB;IAEnB,yBAAyB;IACzB,MAAM,cAAc,GAAG,IAAI,GAAG,EAAoB,CAAC;IACnD,KAAK,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,UAAU,EAAE,CAAC;QAC3C,MAAM,IAAI,GAAG,cAAc,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;QAC7C,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QAClB,cAAc,CAAC,GAAG,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;IAClC,CAAC;IAED,MAAM,MAAM,GAAkB,EAAE,CAAC;IACjC,MAAM,eAAe,GAAG,UAAU,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC;IACzD,MAAM,SAAS,GAAG,eAAe,CAAC,SAAS,CAAC,CAAC;IAC7C,MAAM,SAAS,GAAG,KAAK,CAAC,CAAC,CAAC,eAAe,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IAExD,4EAA4E;IAC5E,0EAA0E;IAC1E,6EAA6E;IAC7E,sEAAsE;IACtE,yEAAyE;IACzE,yEAAyE;IACzE,MAAM,WAAW,GAAG,UAAU,CAAC,CAAC,CAAC,0BAA0B,CAAC,CAAC,CAAC,EAAE,CAAC;IAEjE,MAAM,OAAO,GAAG,CAAC,KAAa,EAAE,QAAgB,EAAe,EAAE;QAC/D,MAAM,YAAY,GAAG,gBAAgB,CAAC,KAAK,CAAC,CAAC;QAC7C,MAAM,SAAS,GAAG,GAAG,eAAe,IAAI,YAAY,GAAG,CAAC;QACxD,OAAO;YACL,IAAI,EAAE,KAAK;YACX,SAAS,EAAE,QAAQ;YACnB,OAAO,EAAE,GAAG,UAAU,CAAC,CAAC,CAAC,6BAA6B,CAAC,CAAC,CAAC,kBAAkB,UAAU,YAAY,sBAAsB,SAAS,UAAU,QAAQ,GAAG,WAAW,EAAE;SACnK,CAAC;IACJ,CAAC,CAAC;IAEF,KAAK,MAAM,CAAC,KAAK,EAAE,OAAO,CAAC,IAAI,cAAc,EAAE,CAAC;QAC9C,2BAA2B;QAC3B,MAAM,WAAW,GAAG,kBAAkB,CAAC,KAAK,CAAC,CAAC;QAC9C,IAAI,WAAW,EAAE,CAAC;YAChB,IAAI,QAAgB,CAAC;YACrB,IAAI,WAAW,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;gBAClC,uCAAuC;gBACvC,gEAAgE;gBAChE,MAAM,SAAS,GAAG,SAAS;qBACxB,OAAO,CAAC,KAAK,EAAE,MAAM,CAAC;qBACtB,OAAO,CAAC,IAAI,EAAE,KAAK,CAAC;qBACpB,OAAO,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;gBACxB,MAAM,MAAM,GAAG,KAAK,SAAS,GAAG,CAAC;gBACjC,sEAAsE;gBACtE,qEAAqE;gBACrE,oEAAoE;gBACpE,oEAAoE;gBACpE,iEAAiE;gBACjE,mEAAmE;gBACnE,sEAAsE;gBACtE,2EAA2E;gBAC3E,gEAAgE;gBAChE,qEAAqE;gBACrE,QAAQ;oBACN,IAAI,WAAW,CAAC,MAAM,WAAW,MAAM,gBAAgB;wBACvD,SAAS,WAAW,CAAC,MAAM,eAAe,MAAM,2BAA2B,CAAC;YAChF,CAAC;iBAAM,CAAC;gBACN,QAAQ,GAAG,IAAI,WAAW,CAAC,MAAM,QAAQ,SAAS,GAAG,CAAC;YACxD,CAAC;YACD,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC,CAAC;YACtC,SAAS;QACX,CAAC;QAED,IACE,KAAK,KAAK,WAAW;YACrB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC;YACzB,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAC;YAC9B,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAC,EAC5B,CAAC;YACD,MAAM,SAAS,GAAG,SAAS;gBACzB,CAAC,CAAC,6BAA6B,UAAU,QAAQ,SAAS,IAAI;gBAC9D,CAAC,CAAC,EAAE,CAAC;YACP,MAAM,CAAC,IAAI,CACT,OAAO,CACL,KAAK,EACL,2BAA2B,YAAY,QAAQ,SAAS,KAAK,SAAS,GAAG,CAC1E,CACF,CAAC;YACF,SAAS;QACX,CAAC;QAED,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAC,CAAC;QAChD,MAAM,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC;QAE5C,IAAI,QAAQ,EAAE,CAAC;YACb,MAAM,SAAS,GACb,MAAM,IAAI,SAAS;gBACjB,CAAC,CAAC,UAAU,UAAU,QAAQ,SAAS,SAAS,UAAU,YAAY;gBACtE,CAAC,CAAC,EAAE,CAAC;YACT,MAAM,CAAC,IAAI,CACT,OAAO,CAAC,KAAK,EAAE,IAAI,YAAY,QAAQ,SAAS,IAAI,SAAS,EAAE,CAAC,CACjE,CAAC;YACF,SAAS;QACX,CAAC;QAED,IAAI,MAAM,EAAE,CAAC;YACX,MAAM,CAAC,IAAI,CACT,OAAO,CACL,KAAK,EACL,SAAS,CAAC,CAAC,CAAC,IAAI,UAAU,QAAQ,SAAS,GAAG,CAAC,CAAC,CAAC,OAAO,CACzD,CACF,CAAC;YACF,SAAS;QACX,CAAC;QAED,qEAAqE;QACrE,wEAAwE;QACxE,oEAAoE;QACpE,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC;IACvC,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC;AAuBD;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,oBAAoB,CACxC,KAAU;IAEV,wEAAwE;IACxE,sEAAsE;IACtE,sEAAsE;IACtE,wEAAwE;IACxE,wEAAwE;IACxE,MAAM,SAAS,GAAG,YAAY,EAAE,CAAC;IAEjC,MAAM,KAAK,GAAG,QAAQ,EAAE,CAAC;IACzB,MAAM,UAAU,GAAG,MAAM,uBAAuB,CAAC,KAAK,CAAC,CAAC;IACxD,MAAM,MAAM,GAAG,iBAAiB,CAAC,UAAU,EAAE,SAAS,EAAE,KAAK,EAAE,IAAI,CAAC,CAAC;IAErE,oEAAoE;IACpE,MAAM,cAAc,GAAG,IAAI,GAAG,EAAoB,CAAC;IACnD,KAAK,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,UAAU,EAAE,CAAC;QAC3C,MAAM,IAAI,GAAG,cAAc,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;QAC7C,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QAClB,cAAc,CAAC,GAAG,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;IAClC,CAAC;IACD,MAAM,gBAAgB,GAAG,IAAI,GAAG,EAAU,CAAC;IAC3C,MAAM,WAAW,GAAG,IAAI,GAAG,EAAU,CAAC;IACtC,KAAK,MAAM,CAAC,KAAK,EAAE,OAAO,CAAC,IAAI,cAAc,EAAE,CAAC;QAC9C,IAAI,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAC;YAAE,gBAAgB,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;QAChE,IAAI,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAC;YAAE,WAAW,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;IAC3D,CAAC;IAED,OAAO;QACL,KAAK,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC;QACnC,QAAQ,EAAE,MAAM,CAAC,GAAG,CAClB,CAAC,CAAC,EAAE,EAAE,CAAC,gCAAgC,gBAAgB,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CACnE;QACD,MAAM,EAAE,MAAM,CAAC,MAAM,GAAG,CAAC;QACzB,SAAS;QACT,KAAK;QACL,gBAAgB;QAChB,WAAW;QACX,eAAe,EAAE,IAAI,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC;KACnE,CAAC;AACJ,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,kBAAkB,CAAC,MAAW;IAClD,6EAA6E;IAC7E,MAAM,SAAS,GAAG,YAAY,EAAE,CAAC;IAEjC,MAAM,KAAK,GAAG,QAAQ,EAAE,CAAC;IACzB,MAAM,UAAU,GAAG,MAAM,qBAAqB,CAAC,MAAM,CAAC,CAAC;IACvD,MAAM,MAAM,GAAG,iBAAiB,CAAC,UAAU,EAAE,SAAS,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC;IAEtE,MAAM,cAAc,GAAG,IAAI,GAAG,EAAoB,CAAC;IACnD,KAAK,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,UAAU,EAAE,CAAC;QAC3C,MAAM,IAAI,GAAG,cAAc,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;QAC7C,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QAClB,cAAc,CAAC,GAAG,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;IAClC,CAAC;IACD,MAAM,gBAAgB,GAAG,IAAI,GAAG,EAAU,CAAC;IAC3C,MAAM,WAAW,GAAG,IAAI,GAAG,EAAU,CAAC;IACtC,KAAK,MAAM,CAAC,KAAK,EAAE,OAAO,CAAC,IAAI,cAAc,EAAE,CAAC;QAC9C,IAAI,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAC;YAAE,gBAAgB,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;QAChE,IAAI,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAC;YAAE,WAAW,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;IAC3D,CAAC;IAED,OAAO;QACL,KAAK,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC;QACnC,QAAQ,EAAE,MAAM,CAAC,GAAG,CAClB,CAAC,CAAC,EAAE,EAAE,CAAC,wBAAwB,gBAAgB,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAC3D;QACD,MAAM,EAAE,MAAM,CAAC,MAAM,GAAG,CAAC;QACzB,SAAS;QACT,KAAK;QACL,gBAAgB;QAChB,WAAW;QACX,eAAe,EAAE,IAAI,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC;KACnE,CAAC;AACJ,CAAC","sourcesContent":["/**\n * Per-user and per-org data scoping for db-query / db-exec.\n *\n * In production mode, creates temporary views that shadow real tables so\n * that raw SQL only sees the current user's (and org's) data.\n *\n * Convention:\n * - Template tables use an `owner_email` column for user scoping.\n * - Template tables use an `org_id` column for org scoping.\n * - Core tables have their own scoping patterns (key prefix, session_id, etc.).\n * - When both columns are present, owner_email is always required; org_id\n * narrows to the current org while preserving legacy/personal NULL rows.\n *\n * Temp views take precedence over real tables in both SQLite and Postgres,\n * so the user's SQL runs unmodified against the filtered views.\n */\n\n// Core tables with non-standard scoping (not owner_email).\n// Map of table name → { column, mode }.\nconst CORE_TABLE_SCOPING: Record<\n string,\n { column: string; mode: \"prefix\" | \"exact\" }\n> = {\n settings: { column: \"key\", mode: \"prefix\" }, // keys like u:<email>:<key>\n application_state: { column: \"session_id\", mode: \"exact\" },\n oauth_tokens: { column: \"owner\", mode: \"exact\" },\n resources: { column: \"owner\", mode: \"exact\" },\n sessions: { column: \"email\", mode: \"exact\" },\n};\n\n// The conventional column names for user/org ownership in template tables.\nimport {\n getRequestUserEmail,\n getRequestOrgId,\n} from \"../../server/request-context.js\";\n\nconst OWNER_COLUMN = \"owner_email\";\nconst ORG_COLUMN = \"org_id\";\nconst DEV_FALLBACK_EMAIL = \"local@localhost\"; // guard:allow-localhost-fallback — sentinel is rejected below so DB scripts cannot silently scope to the dev fallback tenant\n\ninterface ScopedTable {\n name: string;\n viewSql: string;\n predicate: string;\n}\n\nfunction getUserEmail(): string {\n const userEmail = getRequestUserEmail() || null;\n if (!userEmail || userEmail === DEV_FALLBACK_EMAIL) {\n throw new Error(\n \"db-exec / db-query / db-patch require an authenticated user identity. \" +\n \"Easiest fix: open the app at http://localhost:3000 and sign in — \" +\n \"the CLI then auto-loads your session. Otherwise set \" +\n \"AGENT_USER_EMAIL=<email> in the env, or invoke through an HTTP \" +\n \"action that runs under runWithRequestContext. Refusing to run unscoped — \" +\n \"an unscoped UPDATE/DELETE would touch every user's rows, and an \" +\n \"unscoped INSERT would land with the dev sentinel owner and be invisible \" +\n \"to the UI.\",\n );\n }\n return userEmail;\n}\n\nfunction getOrgId(): string | null {\n return getRequestOrgId() || null;\n}\n\n// ─── Schema introspection ───────────────────────────────────────────────────\n\ninterface TableColumn {\n table: string;\n column: string;\n}\n\nasync function discoverColumnsPostgres(pgSql: any): Promise<TableColumn[]> {\n const rows: any[] = await pgSql`\n SELECT table_name, column_name\n FROM information_schema.columns\n WHERE table_schema = 'public'\n ORDER BY table_name, ordinal_position\n `;\n return rows.map((r) => ({ table: r.table_name, column: r.column_name }));\n}\n\nasync function discoverColumnsSqlite(client: any): Promise<TableColumn[]> {\n const tablesResult = await client.execute(\n `SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'`,\n );\n const tables = tablesResult.rows.map((r: any) => (r.name ?? r[0]) as string);\n\n const result: TableColumn[] = [];\n for (const table of tables) {\n const escaped = table.replace(/\"/g, '\"\"');\n const colsResult = await client.execute(`PRAGMA table_info(\"${escaped}\")`);\n for (const row of colsResult.rows) {\n result.push({\n table,\n column: (row.name ?? row[1]) as string,\n });\n }\n }\n return result;\n}\n\n// ─── View generation ────────────────────────────────────────────────────────\n\n/** Escape a string for safe inclusion in a SQL single-quoted literal. */\nfunction escapeSqlString(value: string): string {\n return value.replace(/'/g, \"''\");\n}\n\nfunction escapeIdentifier(value: string): string {\n return value.replace(/\"/g, '\"\"');\n}\n\nfunction buildScopedTables(\n allColumns: TableColumn[],\n userEmail: string,\n orgId: string | null,\n isPostgres: boolean,\n): ScopedTable[] {\n // Group columns by table\n const columnsByTable = new Map<string, string[]>();\n for (const { table, column } of allColumns) {\n const cols = columnsByTable.get(table) || [];\n cols.push(column);\n columnsByTable.set(table, cols);\n }\n\n const scoped: ScopedTable[] = [];\n const qualifiedPrefix = isPostgres ? \"public.\" : \"main.\";\n const safeEmail = escapeSqlString(userEmail);\n const safeOrgId = orgId ? escapeSqlString(orgId) : null;\n\n // WITH CHECK OPTION ensures INSERTs/UPDATEs through the auto-updatable view\n // can't write rows that violate the WHERE filter. Without it, an attacker\n // could `INSERT INTO recordings (..., owner_email) VALUES (..., 'victim@x')`\n // through the view and the row would land in the base table under the\n // victim's identity. SQLite views are not auto-updatable in the same way\n // (they require triggers), so this clause is a no-op there but harmless.\n const checkOption = isPostgres ? \" WITH LOCAL CHECK OPTION\" : \"\";\n\n const viewFor = (table: string, whereSql: string): ScopedTable => {\n const escapedTable = escapeIdentifier(table);\n const realTable = `${qualifiedPrefix}\"${escapedTable}\"`;\n return {\n name: table,\n predicate: whereSql,\n viewSql: `${isPostgres ? \"CREATE OR REPLACE TEMPORARY\" : \"CREATE TEMPORARY\"} VIEW \"${escapedTable}\" AS SELECT * FROM ${realTable} WHERE ${whereSql}${checkOption}`,\n };\n };\n\n for (const [table, columns] of columnsByTable) {\n // Check core table scoping\n const coreScoping = CORE_TABLE_SCOPING[table];\n if (coreScoping) {\n let whereSql: string;\n if (coreScoping.mode === \"prefix\") {\n // settings: key starts with u:<email>:\n // Escape \\, % and _ in the email so LIKE treats them literally.\n const likeEmail = safeEmail\n .replace(/\\\\/g, \"\\\\\\\\\")\n .replace(/%/g, \"\\\\%\")\n .replace(/_/g, \"\\\\_\");\n const prefix = `u:${likeEmail}:`;\n // Hide per-user credential rows (u:<email>:credential:<KEY>) from the\n // raw db-query/db-exec tools. saveCredential() now encrypts API keys\n // and third-party tokens at rest (AES-256-GCM), but the agent never\n // needs to read them via SQL — it uses them implicitly server-side.\n // Excluding them from the view is defense-in-depth: it removes a\n // prompt-injection exfiltration channel (read own secret → send to\n // attacker URL) and also hides any legacy plaintext rows that predate\n // encryption plus the recoverable last4/preview. Schema-qualified attempts\n // to reach the base table (public.settings / main.settings) are\n // rejected separately by assertNoSchemaQualifiedTables in safety.ts.\n whereSql =\n `\"${coreScoping.column}\" LIKE '${prefix}%' ESCAPE '\\\\'` +\n ` AND \"${coreScoping.column}\" NOT LIKE '${prefix}credential:%' ESCAPE '\\\\'`;\n } else {\n whereSql = `\"${coreScoping.column}\" = '${safeEmail}'`;\n }\n scoped.push(viewFor(table, whereSql));\n continue;\n }\n\n if (\n table === \"tool_data\" &&\n columns.includes(\"scope\") &&\n columns.includes(OWNER_COLUMN) &&\n columns.includes(ORG_COLUMN)\n ) {\n const orgClause = safeOrgId\n ? ` OR (\"scope\" = 'org' AND \"${ORG_COLUMN}\" = '${safeOrgId}')`\n : \"\";\n scoped.push(\n viewFor(\n table,\n `((\"scope\" = 'user' AND \"${OWNER_COLUMN}\" = '${safeEmail}')${orgClause})`,\n ),\n );\n continue;\n }\n\n const hasOwner = columns.includes(OWNER_COLUMN);\n const hasOrg = columns.includes(ORG_COLUMN);\n\n if (hasOwner) {\n const orgClause =\n hasOrg && safeOrgId\n ? ` AND (\"${ORG_COLUMN}\" = '${safeOrgId}' OR \"${ORG_COLUMN}\" IS NULL)`\n : \"\";\n scoped.push(\n viewFor(table, `\"${OWNER_COLUMN}\" = '${safeEmail}'${orgClause}`),\n );\n continue;\n }\n\n if (hasOrg) {\n scoped.push(\n viewFor(\n table,\n safeOrgId ? `\"${ORG_COLUMN}\" = '${safeOrgId}'` : \"1 = 0\",\n ),\n );\n continue;\n }\n\n // Fail closed for tables that do not advertise a scoping convention.\n // Without this shadow view, a forgotten owner_email/org_id column turns\n // into raw cross-tenant SELECT/UPDATE/DELETE access for db-* tools.\n scoped.push(viewFor(table, \"1 = 0\"));\n }\n\n return scoped;\n}\n\n// ─── Public API ─────────────────────────────────────────────────────────────\n\nexport interface ScopingContext {\n /** SQL statements to run before the user's query (create temp views). */\n setup: string[];\n /** SQL statements to run after the user's query (drop temp views). */\n teardown: string[];\n /** Whether scoping is active. */\n active: boolean;\n /** The current user email (for INSERT injection in db-exec). */\n userEmail: string | null;\n /** The current org ID (for INSERT injection in db-exec). */\n orgId: string | null;\n /** Tables that have owner_email columns (for INSERT injection). */\n ownerEmailTables: Set<string>;\n /** Tables that have org_id columns (for INSERT injection). */\n orgIdTables: Set<string>;\n /** Table predicates applied by the scoping temp views. */\n tablePredicates: Map<string, string>;\n}\n\n/**\n * Build scoping context for a Postgres connection.\n * Returns setup/teardown SQL to run before/after the user's query.\n */\nexport async function buildScopingPostgres(\n pgSql: any,\n): Promise<ScopingContext> {\n // getUserEmail() throws when there is no authenticated user (no request\n // context AND no AGENT_USER_EMAIL env) or when it resolves to the dev\n // sentinel `local@localhost`. We let that throw propagate: the script\n // refuses to run unscoped rather than silently writing rows that the UI\n // then can't see, or running an UPDATE/DELETE across every user's data.\n const userEmail = getUserEmail();\n\n const orgId = getOrgId();\n const allColumns = await discoverColumnsPostgres(pgSql);\n const scoped = buildScopedTables(allColumns, userEmail, orgId, true);\n\n // Track which tables have owner_email / org_id for INSERT injection\n const columnsByTable = new Map<string, string[]>();\n for (const { table, column } of allColumns) {\n const cols = columnsByTable.get(table) || [];\n cols.push(column);\n columnsByTable.set(table, cols);\n }\n const ownerEmailTables = new Set<string>();\n const orgIdTables = new Set<string>();\n for (const [table, columns] of columnsByTable) {\n if (columns.includes(OWNER_COLUMN)) ownerEmailTables.add(table);\n if (columns.includes(ORG_COLUMN)) orgIdTables.add(table);\n }\n\n return {\n setup: scoped.map((s) => s.viewSql),\n teardown: scoped.map(\n (s) => `DROP VIEW IF EXISTS pg_temp.\"${escapeIdentifier(s.name)}\"`,\n ),\n active: scoped.length > 0,\n userEmail,\n orgId,\n ownerEmailTables,\n orgIdTables,\n tablePredicates: new Map(scoped.map((s) => [s.name, s.predicate])),\n };\n}\n\n/**\n * Build scoping context for a SQLite/libsql connection.\n * Returns setup/teardown SQL to run before/after the user's query.\n */\nexport async function buildScopingSqlite(client: any): Promise<ScopingContext> {\n // See buildScopingPostgres: getUserEmail() throws on no user / dev sentinel.\n const userEmail = getUserEmail();\n\n const orgId = getOrgId();\n const allColumns = await discoverColumnsSqlite(client);\n const scoped = buildScopedTables(allColumns, userEmail, orgId, false);\n\n const columnsByTable = new Map<string, string[]>();\n for (const { table, column } of allColumns) {\n const cols = columnsByTable.get(table) || [];\n cols.push(column);\n columnsByTable.set(table, cols);\n }\n const ownerEmailTables = new Set<string>();\n const orgIdTables = new Set<string>();\n for (const [table, columns] of columnsByTable) {\n if (columns.includes(OWNER_COLUMN)) ownerEmailTables.add(table);\n if (columns.includes(ORG_COLUMN)) orgIdTables.add(table);\n }\n\n return {\n setup: scoped.map((s) => s.viewSql),\n teardown: scoped.map(\n (s) => `DROP VIEW IF EXISTS \"${escapeIdentifier(s.name)}\"`,\n ),\n active: scoped.length > 0,\n userEmail,\n orgId,\n ownerEmailTables,\n orgIdTables,\n tablePredicates: new Map(scoped.map((s) => [s.name, s.predicate])),\n };\n}\n"]}
|
package/dist/secrets/index.d.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
export { registerRequiredSecret, listRequiredSecrets, getRequiredSecret, __resetSecretsRegistry, type RegisteredSecret, type SecretScope, type SecretKind, type SecretValidator, type ValidatorResult, } from "./register.js";
|
|
11
11
|
export { writeAppSecret, readAppSecret, readAppSecretMeta, deleteAppSecret, getAppSecretMeta, listAppSecretsForScope, last4, type SecretRef, type WriteSecretArgs, type ReadSecretResult, type SecretMeta, } from "./storage.js";
|
|
12
12
|
export { APP_SECRETS_CREATE_SQL, appSecrets } from "./schema.js";
|
|
13
|
+
export { encryptSecretValue, decryptSecretValue, isEncryptedSecretValue, } from "./crypto.js";
|
|
13
14
|
export { createListSecretsHandler, createWriteSecretHandler, createTestSecretHandler, createAdHocSecretHandler, type SecretStatusPayload, type AdHocSecretPayload, } from "./routes.js";
|
|
14
15
|
export { resolveKeyReferences, validateUrlAllowlist, getKeyAllowlist, type ResolveKeyReferencesResult, } from "./substitution.js";
|
|
15
16
|
export { maybeRegisterSecretOnboardingStep } from "./onboarding.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/secrets/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EACL,sBAAsB,EACtB,mBAAmB,EACnB,iBAAiB,EACjB,sBAAsB,EACtB,KAAK,gBAAgB,EACrB,KAAK,WAAW,EAChB,KAAK,UAAU,EACf,KAAK,eAAe,EACpB,KAAK,eAAe,GACrB,MAAM,eAAe,CAAC;AAEvB,OAAO,EACL,cAAc,EACd,aAAa,EACb,iBAAiB,EACjB,eAAe,EACf,gBAAgB,EAChB,sBAAsB,EACtB,KAAK,EACL,KAAK,SAAS,EACd,KAAK,eAAe,EACpB,KAAK,gBAAgB,EACrB,KAAK,UAAU,GAChB,MAAM,cAAc,CAAC;AAEtB,OAAO,EAAE,sBAAsB,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/secrets/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EACL,sBAAsB,EACtB,mBAAmB,EACnB,iBAAiB,EACjB,sBAAsB,EACtB,KAAK,gBAAgB,EACrB,KAAK,WAAW,EAChB,KAAK,UAAU,EACf,KAAK,eAAe,EACpB,KAAK,eAAe,GACrB,MAAM,eAAe,CAAC;AAEvB,OAAO,EACL,cAAc,EACd,aAAa,EACb,iBAAiB,EACjB,eAAe,EACf,gBAAgB,EAChB,sBAAsB,EACtB,KAAK,EACL,KAAK,SAAS,EACd,KAAK,eAAe,EACpB,KAAK,gBAAgB,EACrB,KAAK,UAAU,GAChB,MAAM,cAAc,CAAC;AAEtB,OAAO,EAAE,sBAAsB,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAKjE,OAAO,EACL,kBAAkB,EAClB,kBAAkB,EAClB,sBAAsB,GACvB,MAAM,aAAa,CAAC;AAErB,OAAO,EACL,wBAAwB,EACxB,wBAAwB,EACxB,uBAAuB,EACvB,wBAAwB,EACxB,KAAK,mBAAmB,EACxB,KAAK,kBAAkB,GACxB,MAAM,aAAa,CAAC;AAErB,OAAO,EACL,oBAAoB,EACpB,oBAAoB,EACpB,eAAe,EACf,KAAK,0BAA0B,GAChC,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EAAE,iCAAiC,EAAE,MAAM,iBAAiB,CAAC"}
|
package/dist/secrets/index.js
CHANGED
|
@@ -10,6 +10,10 @@
|
|
|
10
10
|
export { registerRequiredSecret, listRequiredSecrets, getRequiredSecret, __resetSecretsRegistry, } from "./register.js";
|
|
11
11
|
export { writeAppSecret, readAppSecret, readAppSecretMeta, deleteAppSecret, getAppSecretMeta, listAppSecretsForScope, last4, } from "./storage.js";
|
|
12
12
|
export { APP_SECRETS_CREATE_SQL, appSecrets } from "./schema.js";
|
|
13
|
+
// AES-256-GCM helpers for column-level encryption of per-row secret values
|
|
14
|
+
// (e.g. a per-record share password) that don't fit the keyed app_secrets /
|
|
15
|
+
// credentials stores. Same key story as the vault.
|
|
16
|
+
export { encryptSecretValue, decryptSecretValue, isEncryptedSecretValue, } from "./crypto.js";
|
|
13
17
|
export { createListSecretsHandler, createWriteSecretHandler, createTestSecretHandler, createAdHocSecretHandler, } from "./routes.js";
|
|
14
18
|
export { resolveKeyReferences, validateUrlAllowlist, getKeyAllowlist, } from "./substitution.js";
|
|
15
19
|
export { maybeRegisterSecretOnboardingStep } from "./onboarding.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/secrets/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EACL,sBAAsB,EACtB,mBAAmB,EACnB,iBAAiB,EACjB,sBAAsB,GAMvB,MAAM,eAAe,CAAC;AAEvB,OAAO,EACL,cAAc,EACd,aAAa,EACb,iBAAiB,EACjB,eAAe,EACf,gBAAgB,EAChB,sBAAsB,EACtB,KAAK,GAKN,MAAM,cAAc,CAAC;AAEtB,OAAO,EAAE,sBAAsB,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAEjE,OAAO,EACL,wBAAwB,EACxB,wBAAwB,EACxB,uBAAuB,EACvB,wBAAwB,GAGzB,MAAM,aAAa,CAAC;AAErB,OAAO,EACL,oBAAoB,EACpB,oBAAoB,EACpB,eAAe,GAEhB,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EAAE,iCAAiC,EAAE,MAAM,iBAAiB,CAAC","sourcesContent":["/**\n * Framework secrets registry.\n *\n * Lets templates declaratively register required API keys / credentials so\n * they appear in the sidebar settings UI and the onboarding checklist, and\n * so actions can read them by a stable key.\n *\n * See `.agents/skills/secrets/SKILL.md` for usage.\n */\n\nexport {\n registerRequiredSecret,\n listRequiredSecrets,\n getRequiredSecret,\n __resetSecretsRegistry,\n type RegisteredSecret,\n type SecretScope,\n type SecretKind,\n type SecretValidator,\n type ValidatorResult,\n} from \"./register.js\";\n\nexport {\n writeAppSecret,\n readAppSecret,\n readAppSecretMeta,\n deleteAppSecret,\n getAppSecretMeta,\n listAppSecretsForScope,\n last4,\n type SecretRef,\n type WriteSecretArgs,\n type ReadSecretResult,\n type SecretMeta,\n} from \"./storage.js\";\n\nexport { APP_SECRETS_CREATE_SQL, appSecrets } from \"./schema.js\";\n\nexport {\n createListSecretsHandler,\n createWriteSecretHandler,\n createTestSecretHandler,\n createAdHocSecretHandler,\n type SecretStatusPayload,\n type AdHocSecretPayload,\n} from \"./routes.js\";\n\nexport {\n resolveKeyReferences,\n validateUrlAllowlist,\n getKeyAllowlist,\n type ResolveKeyReferencesResult,\n} from \"./substitution.js\";\n\nexport { maybeRegisterSecretOnboardingStep } from \"./onboarding.js\";\n"]}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/secrets/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EACL,sBAAsB,EACtB,mBAAmB,EACnB,iBAAiB,EACjB,sBAAsB,GAMvB,MAAM,eAAe,CAAC;AAEvB,OAAO,EACL,cAAc,EACd,aAAa,EACb,iBAAiB,EACjB,eAAe,EACf,gBAAgB,EAChB,sBAAsB,EACtB,KAAK,GAKN,MAAM,cAAc,CAAC;AAEtB,OAAO,EAAE,sBAAsB,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAEjE,2EAA2E;AAC3E,4EAA4E;AAC5E,mDAAmD;AACnD,OAAO,EACL,kBAAkB,EAClB,kBAAkB,EAClB,sBAAsB,GACvB,MAAM,aAAa,CAAC;AAErB,OAAO,EACL,wBAAwB,EACxB,wBAAwB,EACxB,uBAAuB,EACvB,wBAAwB,GAGzB,MAAM,aAAa,CAAC;AAErB,OAAO,EACL,oBAAoB,EACpB,oBAAoB,EACpB,eAAe,GAEhB,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EAAE,iCAAiC,EAAE,MAAM,iBAAiB,CAAC","sourcesContent":["/**\n * Framework secrets registry.\n *\n * Lets templates declaratively register required API keys / credentials so\n * they appear in the sidebar settings UI and the onboarding checklist, and\n * so actions can read them by a stable key.\n *\n * See `.agents/skills/secrets/SKILL.md` for usage.\n */\n\nexport {\n registerRequiredSecret,\n listRequiredSecrets,\n getRequiredSecret,\n __resetSecretsRegistry,\n type RegisteredSecret,\n type SecretScope,\n type SecretKind,\n type SecretValidator,\n type ValidatorResult,\n} from \"./register.js\";\n\nexport {\n writeAppSecret,\n readAppSecret,\n readAppSecretMeta,\n deleteAppSecret,\n getAppSecretMeta,\n listAppSecretsForScope,\n last4,\n type SecretRef,\n type WriteSecretArgs,\n type ReadSecretResult,\n type SecretMeta,\n} from \"./storage.js\";\n\nexport { APP_SECRETS_CREATE_SQL, appSecrets } from \"./schema.js\";\n\n// AES-256-GCM helpers for column-level encryption of per-row secret values\n// (e.g. a per-record share password) that don't fit the keyed app_secrets /\n// credentials stores. Same key story as the vault.\nexport {\n encryptSecretValue,\n decryptSecretValue,\n isEncryptedSecretValue,\n} from \"./crypto.js\";\n\nexport {\n createListSecretsHandler,\n createWriteSecretHandler,\n createTestSecretHandler,\n createAdHocSecretHandler,\n type SecretStatusPayload,\n type AdHocSecretPayload,\n} from \"./routes.js\";\n\nexport {\n resolveKeyReferences,\n validateUrlAllowlist,\n getKeyAllowlist,\n type ResolveKeyReferencesResult,\n} from \"./substitution.js\";\n\nexport { maybeRegisterSecretOnboardingStep } from \"./onboarding.js\";\n"]}
|
|
@@ -39,6 +39,12 @@ export interface CollabPluginOptions {
|
|
|
39
39
|
* deck) while sharing is enforced at the parent resource level.
|
|
40
40
|
*/
|
|
41
41
|
resolveResourceId?: (docId: string) => string | null | Promise<string | null>;
|
|
42
|
+
/**
|
|
43
|
+
* Maximum allowed body size in bytes for write operations
|
|
44
|
+
* (update/text/json/patch). Requests exceeding this are rejected with 413.
|
|
45
|
+
* Default: 2097152 (2 MB).
|
|
46
|
+
*/
|
|
47
|
+
maxPayloadBytes?: number;
|
|
42
48
|
}
|
|
43
49
|
export declare function createCollabPlugin(options?: CollabPluginOptions): NitroPluginDef;
|
|
44
50
|
export {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"collab-plugin.d.ts","sourceRoot":"","sources":["../../src/server/collab-plugin.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAgCH,KAAK,cAAc,GAAG,CAAC,QAAQ,EAAE,GAAG,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"collab-plugin.d.ts","sourceRoot":"","sources":["../../src/server/collab-plugin.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAgCH,KAAK,cAAc,GAAG,CAAC,QAAQ,EAAE,GAAG,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;AAY9D,MAAM,WAAW,mBAAmB;IAClC,mEAAmE;IACnE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,uDAAuD;IACvD,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,qDAAqD;IACrD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,wEAAwE;IACxE,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;OAGG;IACH,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/D,6EAA6E;IAC7E,WAAW,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAC9B,sEAAsE;IACtE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;OAIG;IACH,iBAAiB,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC9E;;;;OAIG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,wBAAgB,kBAAkB,CAChC,OAAO,GAAE,mBAAwB,GAChC,cAAc,CA6PhB"}
|