@agent-native/core 0.26.9 → 0.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent/run-ownership.d.ts +12 -0
- package/dist/agent/run-ownership.d.ts.map +1 -0
- package/dist/agent/run-ownership.js +39 -0
- package/dist/agent/run-ownership.js.map +1 -0
- package/dist/client/AgentPanel.d.ts.map +1 -1
- package/dist/client/AgentPanel.js +1 -0
- package/dist/client/AgentPanel.js.map +1 -1
- package/dist/client/AssistantChat.d.ts.map +1 -1
- package/dist/client/AssistantChat.js +9 -6
- package/dist/client/AssistantChat.js.map +1 -1
- package/dist/client/db-admin/DataGrid.d.ts +42 -0
- package/dist/client/db-admin/DataGrid.d.ts.map +1 -0
- package/dist/client/db-admin/DataGrid.js +204 -0
- package/dist/client/db-admin/DataGrid.js.map +1 -0
- package/dist/client/db-admin/DbAdminPage.d.ts +2 -0
- package/dist/client/db-admin/DbAdminPage.d.ts.map +1 -0
- package/dist/client/db-admin/DbAdminPage.js +72 -0
- package/dist/client/db-admin/DbAdminPage.js.map +1 -0
- package/dist/client/db-admin/DevDatabaseLink.d.ts +19 -0
- package/dist/client/db-admin/DevDatabaseLink.d.ts.map +1 -0
- package/dist/client/db-admin/DevDatabaseLink.js +25 -0
- package/dist/client/db-admin/DevDatabaseLink.js.map +1 -0
- package/dist/client/db-admin/EditableCell.d.ts +26 -0
- package/dist/client/db-admin/EditableCell.d.ts.map +1 -0
- package/dist/client/db-admin/EditableCell.js +150 -0
- package/dist/client/db-admin/EditableCell.js.map +1 -0
- package/dist/client/db-admin/FilterBar.d.ts +8 -0
- package/dist/client/db-admin/FilterBar.d.ts.map +1 -0
- package/dist/client/db-admin/FilterBar.js +68 -0
- package/dist/client/db-admin/FilterBar.js.map +1 -0
- package/dist/client/db-admin/ResultsGrid.d.ts +6 -0
- package/dist/client/db-admin/ResultsGrid.d.ts.map +1 -0
- package/dist/client/db-admin/ResultsGrid.js +41 -0
- package/dist/client/db-admin/ResultsGrid.js.map +1 -0
- package/dist/client/db-admin/RowSidePanel.d.ts +18 -0
- package/dist/client/db-admin/RowSidePanel.d.ts.map +1 -0
- package/dist/client/db-admin/RowSidePanel.js +104 -0
- package/dist/client/db-admin/RowSidePanel.js.map +1 -0
- package/dist/client/db-admin/SqlEditor.d.ts +8 -0
- package/dist/client/db-admin/SqlEditor.d.ts.map +1 -0
- package/dist/client/db-admin/SqlEditor.js +350 -0
- package/dist/client/db-admin/SqlEditor.js.map +1 -0
- package/dist/client/db-admin/TableBrowser.d.ts +10 -0
- package/dist/client/db-admin/TableBrowser.d.ts.map +1 -0
- package/dist/client/db-admin/TableBrowser.js +61 -0
- package/dist/client/db-admin/TableBrowser.js.map +1 -0
- package/dist/client/db-admin/TableEditor.d.ts +9 -0
- package/dist/client/db-admin/TableEditor.d.ts.map +1 -0
- package/dist/client/db-admin/TableEditor.js +254 -0
- package/dist/client/db-admin/TableEditor.js.map +1 -0
- package/dist/client/db-admin/cell-format.d.ts +55 -0
- package/dist/client/db-admin/cell-format.d.ts.map +1 -0
- package/dist/client/db-admin/cell-format.js +223 -0
- package/dist/client/db-admin/cell-format.js.map +1 -0
- package/dist/client/db-admin/changeset.d.ts +74 -0
- package/dist/client/db-admin/changeset.d.ts.map +1 -0
- package/dist/client/db-admin/changeset.js +169 -0
- package/dist/client/db-admin/changeset.js.map +1 -0
- package/dist/client/db-admin/export-utils.d.ts +15 -0
- package/dist/client/db-admin/export-utils.d.ts.map +1 -0
- package/dist/client/db-admin/export-utils.js +62 -0
- package/dist/client/db-admin/export-utils.js.map +1 -0
- package/dist/client/db-admin/index.d.ts +7 -0
- package/dist/client/db-admin/index.d.ts.map +1 -0
- package/dist/client/db-admin/index.js +8 -0
- package/dist/client/db-admin/index.js.map +1 -0
- package/dist/client/db-admin/sql-storage.d.ts +35 -0
- package/dist/client/db-admin/sql-storage.d.ts.map +1 -0
- package/dist/client/db-admin/sql-storage.js +117 -0
- package/dist/client/db-admin/sql-storage.js.map +1 -0
- package/dist/client/db-admin/storage.d.ts +24 -0
- package/dist/client/db-admin/storage.d.ts.map +1 -0
- package/dist/client/db-admin/storage.js +50 -0
- package/dist/client/db-admin/storage.js.map +1 -0
- package/dist/client/db-admin/useAgentSync.d.ts +22 -0
- package/dist/client/db-admin/useAgentSync.d.ts.map +1 -0
- package/dist/client/db-admin/useAgentSync.js +120 -0
- package/dist/client/db-admin/useAgentSync.js.map +1 -0
- package/dist/client/db-admin/useDbAdmin.d.ts +20 -0
- package/dist/client/db-admin/useDbAdmin.d.ts.map +1 -0
- package/dist/client/db-admin/useDbAdmin.js +154 -0
- package/dist/client/db-admin/useDbAdmin.js.map +1 -0
- package/dist/client/index.d.ts +2 -1
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +2 -1
- package/dist/client/index.js.map +1 -1
- package/dist/client/settings/SettingsPanel.d.ts.map +1 -1
- package/dist/client/settings/SettingsPanel.js +8 -4
- package/dist/client/settings/SettingsPanel.js.map +1 -1
- package/dist/client/use-dev-mode.d.ts +20 -2
- package/dist/client/use-dev-mode.d.ts.map +1 -1
- package/dist/client/use-dev-mode.js +49 -14
- package/dist/client/use-dev-mode.js.map +1 -1
- package/dist/credentials/index.d.ts.map +1 -1
- package/dist/credentials/index.js +25 -5
- package/dist/credentials/index.js.map +1 -1
- package/dist/db-admin/agent-tools.d.ts +15 -0
- package/dist/db-admin/agent-tools.d.ts.map +1 -0
- package/dist/db-admin/agent-tools.js +147 -0
- package/dist/db-admin/agent-tools.js.map +1 -0
- package/dist/db-admin/operations.d.ts +17 -0
- package/dist/db-admin/operations.d.ts.map +1 -0
- package/dist/db-admin/operations.js +541 -0
- package/dist/db-admin/operations.js.map +1 -0
- package/dist/db-admin/routes.d.ts +5 -0
- package/dist/db-admin/routes.d.ts.map +1 -0
- package/dist/db-admin/routes.js +134 -0
- package/dist/db-admin/routes.js.map +1 -0
- package/dist/db-admin/types.d.ts +85 -0
- package/dist/db-admin/types.d.ts.map +1 -0
- package/dist/db-admin/types.js +9 -0
- package/dist/db-admin/types.js.map +1 -0
- package/dist/extensions/url-safety.d.ts +20 -0
- package/dist/extensions/url-safety.d.ts.map +1 -1
- package/dist/extensions/url-safety.js +43 -0
- package/dist/extensions/url-safety.js.map +1 -1
- package/dist/file-upload/actions/upload-image.d.ts.map +1 -1
- package/dist/file-upload/actions/upload-image.js +6 -1
- package/dist/file-upload/actions/upload-image.js.map +1 -1
- package/dist/integrations/adapters/email.d.ts.map +1 -1
- package/dist/integrations/adapters/email.js +112 -0
- package/dist/integrations/adapters/email.js.map +1 -1
- package/dist/integrations/types.d.ts +11 -0
- package/dist/integrations/types.d.ts.map +1 -1
- package/dist/integrations/types.js.map +1 -1
- package/dist/scripts/db/exec.d.ts.map +1 -1
- package/dist/scripts/db/exec.js +2 -1
- package/dist/scripts/db/exec.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-credentials.d.ts +28 -0
- package/dist/scripts/db/migrate-encrypt-credentials.d.ts.map +1 -0
- package/dist/scripts/db/migrate-encrypt-credentials.js +190 -0
- package/dist/scripts/db/migrate-encrypt-credentials.js.map +1 -0
- package/dist/scripts/db/query.d.ts.map +1 -1
- package/dist/scripts/db/query.js +2 -1
- package/dist/scripts/db/query.js.map +1 -1
- package/dist/scripts/db/safety.d.ts +1 -0
- package/dist/scripts/db/safety.d.ts.map +1 -1
- package/dist/scripts/db/safety.js +32 -0
- package/dist/scripts/db/safety.js.map +1 -1
- package/dist/scripts/db/scoping.d.ts.map +1 -1
- package/dist/scripts/db/scoping.js +11 -1
- package/dist/scripts/db/scoping.js.map +1 -1
- package/dist/secrets/crypto.d.ts +28 -0
- package/dist/secrets/crypto.d.ts.map +1 -0
- package/dist/secrets/crypto.js +81 -0
- package/dist/secrets/crypto.js.map +1 -0
- package/dist/secrets/storage.d.ts.map +1 -1
- package/dist/secrets/storage.js +3 -61
- package/dist/secrets/storage.js.map +1 -1
- package/dist/server/action-discovery.d.ts.map +1 -1
- package/dist/server/action-discovery.js +5 -2
- package/dist/server/action-discovery.js.map +1 -1
- package/dist/server/action-routes.d.ts.map +1 -1
- package/dist/server/action-routes.js +24 -7
- package/dist/server/action-routes.js.map +1 -1
- package/dist/server/agent-chat-plugin.d.ts.map +1 -1
- package/dist/server/agent-chat-plugin.js +49 -2
- package/dist/server/agent-chat-plugin.js.map +1 -1
- package/dist/server/auth.d.ts +1 -1
- package/dist/server/auth.d.ts.map +1 -1
- package/dist/server/auth.js.map +1 -1
- package/dist/server/better-auth-instance.js +3 -3
- package/dist/server/better-auth-instance.js.map +1 -1
- package/dist/server/core-routes-plugin.d.ts.map +1 -1
- package/dist/server/core-routes-plugin.js +5 -0
- package/dist/server/core-routes-plugin.js.map +1 -1
- package/dist/server/csrf.d.ts.map +1 -1
- package/dist/server/csrf.js +9 -1
- package/dist/server/csrf.js.map +1 -1
- package/dist/server/design-token-utils.d.ts +8 -1
- package/dist/server/design-token-utils.d.ts.map +1 -1
- package/dist/server/design-token-utils.js +12 -4
- package/dist/server/design-token-utils.js.map +1 -1
- package/dist/templates/default/AGENTS.md +4 -4
- package/dist/templates/default/app/routes/database.tsx +13 -0
- package/dist/templates/workspace-core/.agents/skills/authentication/SKILL.md +9 -2
- package/dist/templates/workspace-core/.agents/skills/sharing/SKILL.md +7 -1
- package/dist/vite/client.d.ts.map +1 -1
- package/dist/vite/client.js +4 -0
- package/dist/vite/client.js.map +1 -1
- package/docs/content/a2a-protocol.md +2 -2
- package/docs/content/actions.md +2 -54
- package/docs/content/agent-mentions.md +1 -1
- package/docs/content/agent-teams.md +1 -1
- package/docs/content/authentication.md +2 -2
- package/docs/content/cli-adapters.md +33 -17
- package/docs/content/client.md +11 -20
- package/docs/content/code-agents-ui.md +19 -6
- package/docs/content/context-awareness.md +36 -20
- package/docs/content/database.md +3 -3
- package/docs/content/deployment.md +8 -8
- package/docs/content/dispatch.md +1 -1
- package/docs/content/external-agents.md +5 -1
- package/docs/content/faq.md +1 -0
- package/docs/content/frames.md +116 -30
- package/docs/content/getting-started.md +15 -14
- package/docs/content/mcp-clients.md +1 -1
- package/docs/content/mcp-protocol.md +11 -88
- package/docs/content/messaging.md +1 -1
- package/docs/content/migration-workbench.md +13 -87
- package/docs/content/multi-app-workspace.md +2 -38
- package/docs/content/multi-tenancy.md +3 -26
- package/docs/content/onboarding.md +10 -3
- package/docs/content/recurring-jobs.md +2 -2
- package/docs/content/security.md +33 -1
- package/docs/content/server.md +1 -1
- package/docs/content/template-assets.md +9 -9
- package/docs/content/template-brain.md +114 -388
- package/docs/content/template-clips.md +42 -2
- package/docs/content/template-content.md +1 -1
- package/docs/content/template-design.md +27 -0
- package/docs/content/template-dispatch.md +3 -3
- package/docs/content/template-forms.md +6 -6
- package/docs/content/template-starter.md +2 -2
- package/docs/content/using-your-agent.md +56 -0
- package/docs/content/workspace-management.md +6 -6
- package/docs/content/workspace.md +28 -9
- package/package.json +10 -3
- package/src/templates/default/AGENTS.md +4 -4
- package/src/templates/default/app/routes/database.tsx +13 -0
- package/src/templates/workspace-core/.agents/skills/authentication/SKILL.md +9 -2
- package/src/templates/workspace-core/.agents/skills/sharing/SKILL.md +7 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export declare function assertNoSensitiveFrameworkTables(sql: string, operation: "read" | "write" | "patch"): void;
|
|
2
|
+
export declare function assertNoSchemaQualifiedTables(sql: string, operation: "read" | "write"): void;
|
|
2
3
|
export declare function assertNoRawDbAccessControlWrite(sql: string): void;
|
|
3
4
|
export declare function assertNoRawDbAccessControlPatchTarget(table: string, column: string): void;
|
|
4
5
|
//# sourceMappingURL=safety.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"safety.d.ts","sourceRoot":"","sources":["../../../src/scripts/db/safety.ts"],"names":[],"mappings":"AA+DA,wBAAgB,gCAAgC,CAC9C,GAAG,EAAE,MAAM,EACX,SAAS,EAAE,MAAM,GAAG,OAAO,GAAG,OAAO,GACpC,IAAI,CAcN;AA2HD,wBAAgB,+BAA+B,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAqBjE;AAED,wBAAgB,qCAAqC,CACnD,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,GACb,IAAI,CAaN"}
|
|
1
|
+
{"version":3,"file":"safety.d.ts","sourceRoot":"","sources":["../../../src/scripts/db/safety.ts"],"names":[],"mappings":"AA+DA,wBAAgB,gCAAgC,CAC9C,GAAG,EAAE,MAAM,EACX,SAAS,EAAE,MAAM,GAAG,OAAO,GAAG,OAAO,GACpC,IAAI,CAcN;AA4BD,wBAAgB,6BAA6B,CAC3C,GAAG,EAAE,MAAM,EACX,SAAS,EAAE,MAAM,GAAG,OAAO,GAC1B,IAAI,CAYN;AA2HD,wBAAgB,+BAA+B,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAqBjE;AAED,wBAAgB,qCAAqC,CACnD,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,GACb,IAAI,CAaN"}
|
|
@@ -64,6 +64,38 @@ export function assertNoSensitiveFrameworkTables(sql, operation) {
|
|
|
64
64
|
: "patchable";
|
|
65
65
|
fail(`Sensitive framework table "${match[1]}" is not ${verb} through raw DB tools. Use the framework auth, secrets, or OAuth APIs instead.`);
|
|
66
66
|
}
|
|
67
|
+
// Schema/database-qualified table references (e.g. `public.notes`, `main.notes`,
|
|
68
|
+
// `pg_temp.notes`) BYPASS the per-user/per-org temporary views that scope
|
|
69
|
+
// db-query / db-exec, because those views only shadow UNQUALIFIED table names.
|
|
70
|
+
// A qualified reference resolves straight to the real base table, defeating the
|
|
71
|
+
// owner_email / org_id scoping and exposing (or letting writes touch) every
|
|
72
|
+
// tenant's rows. db-patch already rejects dotted identifiers via
|
|
73
|
+
// isValidIdentifier; db-query / db-exec must reject them too.
|
|
74
|
+
//
|
|
75
|
+
// Two complementary detectors run on the comment/string-stripped SQL:
|
|
76
|
+
// 1. The schemas that actually HOLD base tables and so defeat scoping when
|
|
77
|
+
// named explicitly: `public` (Neon Postgres prod), `main` (SQLite desktop),
|
|
78
|
+
// and the Postgres system catalogs. This fires in ANY position, so it also
|
|
79
|
+
// catches comma-joins (`FROM notes, public.other`) and `USING public.x`.
|
|
80
|
+
// `temp` / `pg_temp` are intentionally NOT listed — temporary objects (our
|
|
81
|
+
// scoping views) live there, so `temp.notes` resolves to the *scoped* view,
|
|
82
|
+
// not a bypass, and `temp` is a common table alias we must not reject.
|
|
83
|
+
// The schema may be bare or double-quoted (`"public"."notes"`).
|
|
84
|
+
// 2. Any dotted reference in table position (FROM/JOIN/INTO/UPDATE, incl.
|
|
85
|
+
// ONLY/LATERAL), which also catches non-standard schema names. Column /
|
|
86
|
+
// alias references like `f.id` sit in select/where/on position, not table
|
|
87
|
+
// position, so they do not match — no false positives on ordinary joins.
|
|
88
|
+
const DANGEROUS_SCHEMA_QUALIFIER_RE = /(?:\b|")(?:main|public|pg_catalog|pg_toast|information_schema)"?\s*\.\s*(?:"|`|\[|[A-Za-z_])/i;
|
|
89
|
+
const TABLE_POSITION_QUALIFIED_RE = /\b(?:FROM|JOIN|INTO|UPDATE)\s+(?:ONLY\s+|LATERAL\s+)?(?:"[^"]+"|`[^`]+`|\[[^\]]+\]|[A-Za-z_][A-Za-z0-9_$]*)\s*\.\s*(?:"[^"]+"|`[^`]+`|\[[^\]]+\]|[A-Za-z_][A-Za-z0-9_$]*)/i;
|
|
90
|
+
export function assertNoSchemaQualifiedTables(sql, operation) {
|
|
91
|
+
const cleanSql = stripSqlNonIdentifiers(sql);
|
|
92
|
+
if (!DANGEROUS_SCHEMA_QUALIFIER_RE.test(cleanSql) &&
|
|
93
|
+
!TABLE_POSITION_QUALIFIED_RE.test(cleanSql)) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const verb = operation === "read" ? "queried" : "written";
|
|
97
|
+
fail(`Schema-qualified table references (e.g. "public.<table>" or "main.<table>") cannot be ${verb} through raw DB tools — a qualified name bypasses the per-user data scoping that isolates each tenant's rows. Use the bare table name; the current user's scoping is applied automatically.`);
|
|
98
|
+
}
|
|
67
99
|
const ACCESS_CONTROL_TABLE_TOKENS = new Set([
|
|
68
100
|
"acl",
|
|
69
101
|
"access",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"safety.js","sourceRoot":"","sources":["../../../src/scripts/db/safety.ts"],"names":[],"mappings":"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","sourcesContent":["import { fail } from \"../utils.js\";\n\n// Credential and identity tables are deliberately off-limits to the generic\n// agent DB tools. They contain OAuth tokens, encrypted API keys, sessions, or\n// auth identity data; use the framework stores/actions instead.\nconst SENSITIVE_FRAMEWORK_TABLE_RE =\n /\\b(app_secrets|oauth_tokens|user|users|session|sessions|account|accounts|verification|jwks|organization|member|invitation|org_members|org_invitations|pg_catalog|information_schema|pg_class|pg_proc|pg_namespace|pg_user|pg_roles|pg_authid|pg_shadow)\\b/i;\n\nfunction stripSqlNonIdentifiers(sql: string): string {\n let out = \"\";\n let state: \"normal\" | \"single\" | \"line-comment\" | \"block-comment\" = \"normal\";\n\n for (let i = 0; i < sql.length; i++) {\n const ch = sql[i];\n const next = sql[i + 1];\n\n if (state === \"line-comment\") {\n if (ch === \"\\n\") {\n out += \" \";\n state = \"normal\";\n }\n continue;\n }\n\n if (state === \"block-comment\") {\n if (ch === \"*\" && next === \"/\") {\n i++;\n out += \" \";\n state = \"normal\";\n }\n continue;\n }\n\n if (state === \"single\") {\n if (ch === \"'\" && next === \"'\") {\n i++;\n } else if (ch === \"'\") {\n out += \" \";\n state = \"normal\";\n }\n continue;\n }\n\n if (ch === \"-\" && next === \"-\") {\n i++;\n state = \"line-comment\";\n continue;\n }\n if (ch === \"/\" && next === \"*\") {\n i++;\n state = \"block-comment\";\n continue;\n }\n if (ch === \"'\") {\n state = \"single\";\n continue;\n }\n out += ch;\n }\n\n return out;\n}\n\nexport function assertNoSensitiveFrameworkTables(\n sql: string,\n operation: \"read\" | \"write\" | \"patch\",\n): void {\n const cleanSql = stripSqlNonIdentifiers(sql);\n const match = cleanSql.match(SENSITIVE_FRAMEWORK_TABLE_RE);\n if (!match) return;\n\n const verb =\n operation === \"read\"\n ? \"readable\"\n : operation === \"write\"\n ? \"writable\"\n : \"patchable\";\n fail(\n `Sensitive framework table \"${match[1]}\" is not ${verb} through raw DB tools. Use the framework auth, secrets, or OAuth APIs instead.`,\n );\n}\n\nconst ACCESS_CONTROL_TABLE_TOKENS = new Set([\n \"acl\",\n \"access\",\n \"admin\",\n \"admins\",\n \"grant\",\n \"grants\",\n \"invitation\",\n \"invitations\",\n \"invite\",\n \"invites\",\n \"member\",\n \"members\",\n \"permission\",\n \"permissions\",\n \"privilege\",\n \"privileges\",\n \"role\",\n \"roles\",\n \"user\",\n \"users\",\n]);\n\nconst ACCESS_CONTROL_COLUMN_TOKENS = new Set([\n \"access\",\n \"access_level\",\n \"acl\",\n \"admin\",\n \"admins\",\n \"grant\",\n \"grants\",\n \"is_admin\",\n \"is_owner\",\n \"member\",\n \"members\",\n \"owner\",\n \"owner_email\",\n \"permission\",\n \"permissions\",\n \"privilege\",\n \"privileges\",\n \"role\",\n \"roles\",\n]);\n\nfunction normalizeIdentifier(value: string): string {\n return value\n .trim()\n .replace(/^[\"'`\\[]/, \"\")\n .replace(/[\"'`\\]]$/, \"\")\n .toLowerCase();\n}\n\nfunction identifierTokens(identifier: string): Set<string> {\n const normalized = normalizeIdentifier(identifier);\n const tokens = new Set<string>([normalized]);\n for (const token of normalized.split(/[^a-z0-9]+/).filter(Boolean)) {\n tokens.add(token);\n }\n return tokens;\n}\n\nfunction hasSensitiveToken(\n identifier: string,\n sensitiveTokens: Set<string>,\n): string | null {\n for (const token of identifierTokens(identifier)) {\n if (sensitiveTokens.has(token)) return token;\n }\n return null;\n}\n\nfunction tableNameFromWriteSql(sql: string): string | null {\n const match = sql.match(\n /^\\s*(?:INSERT(?:\\s+OR\\s+\\w+)?\\s+INTO|REPLACE(?:\\s+OR\\s+\\w+)?\\s+INTO|UPDATE|DELETE\\s+FROM)\\s+((?:\"[^\"]+\"|'[^']+'|`[^`]+`|[\\w]+)(?:\\s*\\.\\s*(?:\"[^\"]+\"|'[^']+'|`[^`]+`|[\\w]+))?)/i,\n );\n if (!match) return null;\n return normalizeIdentifier(match[1].split(\".\").pop() ?? match[1]);\n}\n\nfunction splitColumnList(columns: string): string[] {\n return columns\n .split(\",\")\n .map((column) => normalizeIdentifier(column))\n .filter(Boolean);\n}\n\nfunction insertColumnsFromSql(sql: string): string[] {\n const match = sql.match(\n /^\\s*(?:INSERT(?:\\s+OR\\s+\\w+)?\\s+INTO|REPLACE(?:\\s+OR\\s+\\w+)?\\s+INTO)\\s+(?:\"[^\"]+\"|'[^']+'|`[^`]+`|[\\w]+)(?:\\s*\\.\\s*(?:\"[^\"]+\"|'[^']+'|`[^`]+`|[\\w]+))?\\s*\\(([^)]+)\\)/i,\n );\n return match ? splitColumnList(match[1]) : [];\n}\n\nfunction updateColumnsFromSql(sql: string): string[] {\n const setMatch = /\\bSET\\b/i.exec(sql);\n if (!setMatch) return [];\n const tail = sql.slice(setMatch.index + setMatch[0].length);\n const endMatch = /\\b(?:WHERE|RETURNING)\\b/i.exec(tail);\n const setClause = endMatch ? tail.slice(0, endMatch.index) : tail;\n const columns: string[] = [];\n const columnRe =\n /(?:^|,)\\s*(?:\"([^\"]+)\"|'([^']+)'|`([^`]+)`|([A-Za-z_][A-Za-z0-9_]*))\\s*=/g;\n let match: RegExpExecArray | null;\n while ((match = columnRe.exec(setClause)) !== null) {\n columns.push(\n normalizeIdentifier(match[1] ?? match[2] ?? match[3] ?? match[4]),\n );\n }\n return columns;\n}\n\nfunction writeColumnsFromSql(sql: string): string[] {\n const upper = sql.trim().toUpperCase();\n if (upper.startsWith(\"UPDATE\")) return updateColumnsFromSql(sql);\n if (upper.startsWith(\"INSERT\") || upper.startsWith(\"REPLACE\")) {\n return insertColumnsFromSql(sql);\n }\n return [];\n}\n\nexport function assertNoRawDbAccessControlWrite(sql: string): void {\n const tableName = tableNameFromWriteSql(sql);\n if (tableName) {\n const tableToken = hasSensitiveToken(\n tableName,\n ACCESS_CONTROL_TABLE_TOKENS,\n );\n if (tableToken) {\n fail(\n `Sensitive identity/access-control table \"${tableName}\" is not writable through raw DB tools. Use a dedicated app action or implement the permission change in reviewed code.`,\n );\n }\n }\n\n for (const column of writeColumnsFromSql(sql)) {\n const columnToken = hasSensitiveToken(column, ACCESS_CONTROL_COLUMN_TOKENS);\n if (!columnToken) continue;\n fail(\n `Sensitive identity/access-control column \"${column}\" is not writable through raw DB tools. Use a dedicated app action or implement the permission change in reviewed code.`,\n );\n }\n}\n\nexport function assertNoRawDbAccessControlPatchTarget(\n table: string,\n column: string,\n): void {\n const tableName = normalizeIdentifier(table);\n if (hasSensitiveToken(tableName, ACCESS_CONTROL_TABLE_TOKENS)) {\n fail(\n `Sensitive identity/access-control table \"${tableName}\" is not patchable through raw DB tools. Use a dedicated app action or implement the permission change in reviewed code.`,\n );\n }\n const columnName = normalizeIdentifier(column);\n if (hasSensitiveToken(columnName, ACCESS_CONTROL_COLUMN_TOKENS)) {\n fail(\n `Sensitive identity/access-control column \"${columnName}\" is not patchable through raw DB tools. Use a dedicated app action or implement the permission change in reviewed code.`,\n );\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"safety.js","sourceRoot":"","sources":["../../../src/scripts/db/safety.ts"],"names":[],"mappings":"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","sourcesContent":["import { fail } from \"../utils.js\";\n\n// Credential and identity tables are deliberately off-limits to the generic\n// agent DB tools. They contain OAuth tokens, encrypted API keys, sessions, or\n// auth identity data; use the framework stores/actions instead.\nconst SENSITIVE_FRAMEWORK_TABLE_RE =\n /\\b(app_secrets|oauth_tokens|user|users|session|sessions|account|accounts|verification|jwks|organization|member|invitation|org_members|org_invitations|pg_catalog|information_schema|pg_class|pg_proc|pg_namespace|pg_user|pg_roles|pg_authid|pg_shadow)\\b/i;\n\nfunction stripSqlNonIdentifiers(sql: string): string {\n let out = \"\";\n let state: \"normal\" | \"single\" | \"line-comment\" | \"block-comment\" = \"normal\";\n\n for (let i = 0; i < sql.length; i++) {\n const ch = sql[i];\n const next = sql[i + 1];\n\n if (state === \"line-comment\") {\n if (ch === \"\\n\") {\n out += \" \";\n state = \"normal\";\n }\n continue;\n }\n\n if (state === \"block-comment\") {\n if (ch === \"*\" && next === \"/\") {\n i++;\n out += \" \";\n state = \"normal\";\n }\n continue;\n }\n\n if (state === \"single\") {\n if (ch === \"'\" && next === \"'\") {\n i++;\n } else if (ch === \"'\") {\n out += \" \";\n state = \"normal\";\n }\n continue;\n }\n\n if (ch === \"-\" && next === \"-\") {\n i++;\n state = \"line-comment\";\n continue;\n }\n if (ch === \"/\" && next === \"*\") {\n i++;\n state = \"block-comment\";\n continue;\n }\n if (ch === \"'\") {\n state = \"single\";\n continue;\n }\n out += ch;\n }\n\n return out;\n}\n\nexport function assertNoSensitiveFrameworkTables(\n sql: string,\n operation: \"read\" | \"write\" | \"patch\",\n): void {\n const cleanSql = stripSqlNonIdentifiers(sql);\n const match = cleanSql.match(SENSITIVE_FRAMEWORK_TABLE_RE);\n if (!match) return;\n\n const verb =\n operation === \"read\"\n ? \"readable\"\n : operation === \"write\"\n ? \"writable\"\n : \"patchable\";\n fail(\n `Sensitive framework table \"${match[1]}\" is not ${verb} through raw DB tools. Use the framework auth, secrets, or OAuth APIs instead.`,\n );\n}\n\n// Schema/database-qualified table references (e.g. `public.notes`, `main.notes`,\n// `pg_temp.notes`) BYPASS the per-user/per-org temporary views that scope\n// db-query / db-exec, because those views only shadow UNQUALIFIED table names.\n// A qualified reference resolves straight to the real base table, defeating the\n// owner_email / org_id scoping and exposing (or letting writes touch) every\n// tenant's rows. db-patch already rejects dotted identifiers via\n// isValidIdentifier; db-query / db-exec must reject them too.\n//\n// Two complementary detectors run on the comment/string-stripped SQL:\n// 1. The schemas that actually HOLD base tables and so defeat scoping when\n// named explicitly: `public` (Neon Postgres prod), `main` (SQLite desktop),\n// and the Postgres system catalogs. This fires in ANY position, so it also\n// catches comma-joins (`FROM notes, public.other`) and `USING public.x`.\n// `temp` / `pg_temp` are intentionally NOT listed — temporary objects (our\n// scoping views) live there, so `temp.notes` resolves to the *scoped* view,\n// not a bypass, and `temp` is a common table alias we must not reject.\n// The schema may be bare or double-quoted (`\"public\".\"notes\"`).\n// 2. Any dotted reference in table position (FROM/JOIN/INTO/UPDATE, incl.\n// ONLY/LATERAL), which also catches non-standard schema names. Column /\n// alias references like `f.id` sit in select/where/on position, not table\n// position, so they do not match — no false positives on ordinary joins.\nconst DANGEROUS_SCHEMA_QUALIFIER_RE =\n /(?:\\b|\")(?:main|public|pg_catalog|pg_toast|information_schema)\"?\\s*\\.\\s*(?:\"|`|\\[|[A-Za-z_])/i;\nconst TABLE_POSITION_QUALIFIED_RE =\n /\\b(?:FROM|JOIN|INTO|UPDATE)\\s+(?:ONLY\\s+|LATERAL\\s+)?(?:\"[^\"]+\"|`[^`]+`|\\[[^\\]]+\\]|[A-Za-z_][A-Za-z0-9_$]*)\\s*\\.\\s*(?:\"[^\"]+\"|`[^`]+`|\\[[^\\]]+\\]|[A-Za-z_][A-Za-z0-9_$]*)/i;\n\nexport function assertNoSchemaQualifiedTables(\n sql: string,\n operation: \"read\" | \"write\",\n): void {\n const cleanSql = stripSqlNonIdentifiers(sql);\n if (\n !DANGEROUS_SCHEMA_QUALIFIER_RE.test(cleanSql) &&\n !TABLE_POSITION_QUALIFIED_RE.test(cleanSql)\n ) {\n return;\n }\n const verb = operation === \"read\" ? \"queried\" : \"written\";\n fail(\n `Schema-qualified table references (e.g. \"public.<table>\" or \"main.<table>\") cannot be ${verb} through raw DB tools — a qualified name bypasses the per-user data scoping that isolates each tenant's rows. Use the bare table name; the current user's scoping is applied automatically.`,\n );\n}\n\nconst ACCESS_CONTROL_TABLE_TOKENS = new Set([\n \"acl\",\n \"access\",\n \"admin\",\n \"admins\",\n \"grant\",\n \"grants\",\n \"invitation\",\n \"invitations\",\n \"invite\",\n \"invites\",\n \"member\",\n \"members\",\n \"permission\",\n \"permissions\",\n \"privilege\",\n \"privileges\",\n \"role\",\n \"roles\",\n \"user\",\n \"users\",\n]);\n\nconst ACCESS_CONTROL_COLUMN_TOKENS = new Set([\n \"access\",\n \"access_level\",\n \"acl\",\n \"admin\",\n \"admins\",\n \"grant\",\n \"grants\",\n \"is_admin\",\n \"is_owner\",\n \"member\",\n \"members\",\n \"owner\",\n \"owner_email\",\n \"permission\",\n \"permissions\",\n \"privilege\",\n \"privileges\",\n \"role\",\n \"roles\",\n]);\n\nfunction normalizeIdentifier(value: string): string {\n return value\n .trim()\n .replace(/^[\"'`\\[]/, \"\")\n .replace(/[\"'`\\]]$/, \"\")\n .toLowerCase();\n}\n\nfunction identifierTokens(identifier: string): Set<string> {\n const normalized = normalizeIdentifier(identifier);\n const tokens = new Set<string>([normalized]);\n for (const token of normalized.split(/[^a-z0-9]+/).filter(Boolean)) {\n tokens.add(token);\n }\n return tokens;\n}\n\nfunction hasSensitiveToken(\n identifier: string,\n sensitiveTokens: Set<string>,\n): string | null {\n for (const token of identifierTokens(identifier)) {\n if (sensitiveTokens.has(token)) return token;\n }\n return null;\n}\n\nfunction tableNameFromWriteSql(sql: string): string | null {\n const match = sql.match(\n /^\\s*(?:INSERT(?:\\s+OR\\s+\\w+)?\\s+INTO|REPLACE(?:\\s+OR\\s+\\w+)?\\s+INTO|UPDATE|DELETE\\s+FROM)\\s+((?:\"[^\"]+\"|'[^']+'|`[^`]+`|[\\w]+)(?:\\s*\\.\\s*(?:\"[^\"]+\"|'[^']+'|`[^`]+`|[\\w]+))?)/i,\n );\n if (!match) return null;\n return normalizeIdentifier(match[1].split(\".\").pop() ?? match[1]);\n}\n\nfunction splitColumnList(columns: string): string[] {\n return columns\n .split(\",\")\n .map((column) => normalizeIdentifier(column))\n .filter(Boolean);\n}\n\nfunction insertColumnsFromSql(sql: string): string[] {\n const match = sql.match(\n /^\\s*(?:INSERT(?:\\s+OR\\s+\\w+)?\\s+INTO|REPLACE(?:\\s+OR\\s+\\w+)?\\s+INTO)\\s+(?:\"[^\"]+\"|'[^']+'|`[^`]+`|[\\w]+)(?:\\s*\\.\\s*(?:\"[^\"]+\"|'[^']+'|`[^`]+`|[\\w]+))?\\s*\\(([^)]+)\\)/i,\n );\n return match ? splitColumnList(match[1]) : [];\n}\n\nfunction updateColumnsFromSql(sql: string): string[] {\n const setMatch = /\\bSET\\b/i.exec(sql);\n if (!setMatch) return [];\n const tail = sql.slice(setMatch.index + setMatch[0].length);\n const endMatch = /\\b(?:WHERE|RETURNING)\\b/i.exec(tail);\n const setClause = endMatch ? tail.slice(0, endMatch.index) : tail;\n const columns: string[] = [];\n const columnRe =\n /(?:^|,)\\s*(?:\"([^\"]+)\"|'([^']+)'|`([^`]+)`|([A-Za-z_][A-Za-z0-9_]*))\\s*=/g;\n let match: RegExpExecArray | null;\n while ((match = columnRe.exec(setClause)) !== null) {\n columns.push(\n normalizeIdentifier(match[1] ?? match[2] ?? match[3] ?? match[4]),\n );\n }\n return columns;\n}\n\nfunction writeColumnsFromSql(sql: string): string[] {\n const upper = sql.trim().toUpperCase();\n if (upper.startsWith(\"UPDATE\")) return updateColumnsFromSql(sql);\n if (upper.startsWith(\"INSERT\") || upper.startsWith(\"REPLACE\")) {\n return insertColumnsFromSql(sql);\n }\n return [];\n}\n\nexport function assertNoRawDbAccessControlWrite(sql: string): void {\n const tableName = tableNameFromWriteSql(sql);\n if (tableName) {\n const tableToken = hasSensitiveToken(\n tableName,\n ACCESS_CONTROL_TABLE_TOKENS,\n );\n if (tableToken) {\n fail(\n `Sensitive identity/access-control table \"${tableName}\" is not writable through raw DB tools. Use a dedicated app action or implement the permission change in reviewed code.`,\n );\n }\n }\n\n for (const column of writeColumnsFromSql(sql)) {\n const columnToken = hasSensitiveToken(column, ACCESS_CONTROL_COLUMN_TOKENS);\n if (!columnToken) continue;\n fail(\n `Sensitive identity/access-control column \"${column}\" is not writable through raw DB tools. Use a dedicated app action or implement the permission change in reviewed code.`,\n );\n }\n}\n\nexport function assertNoRawDbAccessControlPatchTarget(\n table: string,\n column: string,\n): void {\n const tableName = normalizeIdentifier(table);\n if (hasSensitiveToken(tableName, ACCESS_CONTROL_TABLE_TOKENS)) {\n fail(\n `Sensitive identity/access-control table \"${tableName}\" is not patchable through raw DB tools. Use a dedicated app action or implement the permission change in reviewed code.`,\n );\n }\n const columnName = normalizeIdentifier(column);\n if (hasSensitiveToken(columnName, ACCESS_CONTROL_COLUMN_TOKENS)) {\n fail(\n `Sensitive identity/access-control column \"${columnName}\" is not patchable through raw DB tools. Use a dedicated app action or implement the permission change in reviewed code.`,\n );\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;AA6NH,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"}
|
|
@@ -119,7 +119,17 @@ function buildScopedTables(allColumns, userEmail, orgId, isPostgres) {
|
|
|
119
119
|
.replace(/%/g, "\\%")
|
|
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. resolveCredential() stores API keys and
|
|
124
|
+
// third-party tokens here as plaintext, and the agent never needs to
|
|
125
|
+
// read them via SQL — it uses them implicitly server-side. Excluding
|
|
126
|
+
// them from the view removes a prompt-injection exfiltration channel
|
|
127
|
+
// (read own secret → send to attacker URL). Schema-qualified attempts
|
|
128
|
+
// to reach the base table (public.settings / main.settings) are
|
|
129
|
+
// rejected separately by assertNoSchemaQualifiedTables in safety.ts.
|
|
130
|
+
whereSql =
|
|
131
|
+
`"${coreScoping.column}" LIKE '${prefix}%' ESCAPE '\\'` +
|
|
132
|
+
` AND "${coreScoping.column}" NOT LIKE '${prefix}credential:%' ESCAPE '\\'`;
|
|
123
133
|
}
|
|
124
134
|
else {
|
|
125
135
|
whereSql = `"${coreScoping.column}" = '${safeEmail}'`;
|
|
@@ -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 whereSql = `\"${coreScoping.column}\" LIKE '${prefix}%' 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":"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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"]}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared AES-256-GCM encryption for secret values at rest.
|
|
3
|
+
*
|
|
4
|
+
* Used by both the framework secrets vault (`app_secrets`) and per-user/per-org
|
|
5
|
+
* credentials (`resolveCredential` / `saveCredential`, stored in `settings`) so
|
|
6
|
+
* there is a single crypto implementation and a single key story.
|
|
7
|
+
*
|
|
8
|
+
* The encryption key is derived from `SECRETS_ENCRYPTION_KEY` (preferred) or the
|
|
9
|
+
* existing `BETTER_AUTH_SECRET` env var (fallback so templates don't need a
|
|
10
|
+
* second secret during development). In production we refuse to start without
|
|
11
|
+
* one of them — a CWD-derived fallback would be effectively static (e.g.
|
|
12
|
+
* `/var/task` on Lambda), so anyone with read access to the DB could decrypt
|
|
13
|
+
* every secret.
|
|
14
|
+
*
|
|
15
|
+
* Encrypted values are tagged `v1:<iv-hex>:<ct-hex>:<tag-hex>`. The `v1:` prefix
|
|
16
|
+
* lets readers distinguish ciphertext from legacy plaintext during migration.
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* Derive a 32-byte AES key from the configured secret material via SHA-256.
|
|
20
|
+
* Re-derived per call (cheap, stateless, and makes rotation easy).
|
|
21
|
+
*/
|
|
22
|
+
export declare function getSecretEncryptionKey(): Buffer;
|
|
23
|
+
/** Encrypt a plain-text value. Returns `v1:<iv-hex>:<ct-hex>:<tag-hex>`. */
|
|
24
|
+
export declare function encryptSecretValue(plaintext: string): string;
|
|
25
|
+
/** Decrypt a value produced by `encryptSecretValue`. Throws on tampering. */
|
|
26
|
+
export declare function decryptSecretValue(encrypted: string): string;
|
|
27
|
+
export declare function isEncryptedSecretValue(value: unknown): value is string;
|
|
28
|
+
//# sourceMappingURL=crypto.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"crypto.d.ts","sourceRoot":"","sources":["../../src/secrets/crypto.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAWH;;;GAGG;AACH,wBAAgB,sBAAsB,IAAI,MAAM,CA0B/C;AAED,4EAA4E;AAC5E,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAO5D;AAED,6EAA6E;AAC7E,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAoB5D;AAUD,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAEtE"}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared AES-256-GCM encryption for secret values at rest.
|
|
3
|
+
*
|
|
4
|
+
* Used by both the framework secrets vault (`app_secrets`) and per-user/per-org
|
|
5
|
+
* credentials (`resolveCredential` / `saveCredential`, stored in `settings`) so
|
|
6
|
+
* there is a single crypto implementation and a single key story.
|
|
7
|
+
*
|
|
8
|
+
* The encryption key is derived from `SECRETS_ENCRYPTION_KEY` (preferred) or the
|
|
9
|
+
* existing `BETTER_AUTH_SECRET` env var (fallback so templates don't need a
|
|
10
|
+
* second secret during development). In production we refuse to start without
|
|
11
|
+
* one of them — a CWD-derived fallback would be effectively static (e.g.
|
|
12
|
+
* `/var/task` on Lambda), so anyone with read access to the DB could decrypt
|
|
13
|
+
* every secret.
|
|
14
|
+
*
|
|
15
|
+
* Encrypted values are tagged `v1:<iv-hex>:<ct-hex>:<tag-hex>`. The `v1:` prefix
|
|
16
|
+
* lets readers distinguish ciphertext from legacy plaintext during migration.
|
|
17
|
+
*/
|
|
18
|
+
import { randomBytes, createCipheriv, createDecipheriv, createHash, } from "node:crypto";
|
|
19
|
+
let _warnedFallback = false;
|
|
20
|
+
/**
|
|
21
|
+
* Derive a 32-byte AES key from the configured secret material via SHA-256.
|
|
22
|
+
* Re-derived per call (cheap, stateless, and makes rotation easy).
|
|
23
|
+
*/
|
|
24
|
+
export function getSecretEncryptionKey() {
|
|
25
|
+
const explicit = process.env.SECRETS_ENCRYPTION_KEY || process.env.BETTER_AUTH_SECRET;
|
|
26
|
+
if (!explicit) {
|
|
27
|
+
if (process.env.NODE_ENV === "production") {
|
|
28
|
+
throw new Error("[agent-native/secrets] Refusing to start in production without an encryption key. " +
|
|
29
|
+
"Set SECRETS_ENCRYPTION_KEY (preferred) or BETTER_AUTH_SECRET in the deploy environment. " +
|
|
30
|
+
"The previous CWD-derived fallback was effectively static (e.g. `/var/task` on Lambda), " +
|
|
31
|
+
"which means anyone with read access to the secrets table could decrypt every user's secrets.");
|
|
32
|
+
}
|
|
33
|
+
if (!_warnedFallback) {
|
|
34
|
+
_warnedFallback = true;
|
|
35
|
+
// eslint-disable-next-line no-console
|
|
36
|
+
console.warn("[agent-native/secrets] SECRETS_ENCRYPTION_KEY not set — using a machine-local fallback. " +
|
|
37
|
+
"Set SECRETS_ENCRYPTION_KEY (or BETTER_AUTH_SECRET) for production. " +
|
|
38
|
+
"Production deploys without one of these env vars now hard-fail.");
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const material = explicit || `agent-native-secrets:${process.cwd()}`;
|
|
42
|
+
return createHash("sha256").update(material).digest();
|
|
43
|
+
}
|
|
44
|
+
/** Encrypt a plain-text value. Returns `v1:<iv-hex>:<ct-hex>:<tag-hex>`. */
|
|
45
|
+
export function encryptSecretValue(plaintext) {
|
|
46
|
+
const key = getSecretEncryptionKey();
|
|
47
|
+
const iv = randomBytes(12);
|
|
48
|
+
const cipher = createCipheriv("aes-256-gcm", key, iv);
|
|
49
|
+
const ct = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
|
|
50
|
+
const tag = cipher.getAuthTag();
|
|
51
|
+
return `v1:${iv.toString("hex")}:${ct.toString("hex")}:${tag.toString("hex")}`;
|
|
52
|
+
}
|
|
53
|
+
/** Decrypt a value produced by `encryptSecretValue`. Throws on tampering. */
|
|
54
|
+
export function decryptSecretValue(encrypted) {
|
|
55
|
+
if (!encrypted.startsWith("v1:")) {
|
|
56
|
+
throw new Error("Unrecognised secret encoding");
|
|
57
|
+
}
|
|
58
|
+
const [, ivHex, ctHex, tagHex] = encrypted.split(":");
|
|
59
|
+
if (!ivHex || !ctHex || !tagHex) {
|
|
60
|
+
throw new Error("Corrupt secret payload");
|
|
61
|
+
}
|
|
62
|
+
const key = getSecretEncryptionKey();
|
|
63
|
+
const decipher = createDecipheriv("aes-256-gcm", key, Buffer.from(ivHex, "hex"));
|
|
64
|
+
decipher.setAuthTag(Buffer.from(tagHex, "hex"));
|
|
65
|
+
const pt = Buffer.concat([
|
|
66
|
+
decipher.update(Buffer.from(ctHex, "hex")),
|
|
67
|
+
decipher.final(),
|
|
68
|
+
]);
|
|
69
|
+
return pt.toString("utf8");
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Strict check for a value produced by `encryptSecretValue`: `v1:` followed by
|
|
73
|
+
* three hex segments. Intentionally strict so a legacy plaintext credential
|
|
74
|
+
* that merely happens to start with `v1:` is treated as plaintext (and read via
|
|
75
|
+
* the legacy fallback) rather than mis-decrypted.
|
|
76
|
+
*/
|
|
77
|
+
const ENCRYPTED_VALUE_RE = /^v1:[0-9a-f]+:[0-9a-f]+:[0-9a-f]+$/;
|
|
78
|
+
export function isEncryptedSecretValue(value) {
|
|
79
|
+
return typeof value === "string" && ENCRYPTED_VALUE_RE.test(value);
|
|
80
|
+
}
|
|
81
|
+
//# sourceMappingURL=crypto.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"crypto.js","sourceRoot":"","sources":["../../src/secrets/crypto.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Shared AES-256-GCM encryption for secret values at rest.\n *\n * Used by both the framework secrets vault (`app_secrets`) and per-user/per-org\n * credentials (`resolveCredential` / `saveCredential`, stored in `settings`) so\n * there is a single crypto implementation and a single key story.\n *\n * The encryption key is derived from `SECRETS_ENCRYPTION_KEY` (preferred) or the\n * existing `BETTER_AUTH_SECRET` env var (fallback so templates don't need a\n * second secret during development). In production we refuse to start without\n * one of them — a CWD-derived fallback would be effectively static (e.g.\n * `/var/task` on Lambda), so anyone with read access to the DB could decrypt\n * every secret.\n *\n * Encrypted values are tagged `v1:<iv-hex>:<ct-hex>:<tag-hex>`. The `v1:` prefix\n * lets readers distinguish ciphertext from legacy plaintext during migration.\n */\n\nimport {\n randomBytes,\n createCipheriv,\n createDecipheriv,\n createHash,\n} from \"node:crypto\";\n\nlet _warnedFallback = false;\n\n/**\n * Derive a 32-byte AES key from the configured secret material via SHA-256.\n * Re-derived per call (cheap, stateless, and makes rotation easy).\n */\nexport function getSecretEncryptionKey(): Buffer {\n const explicit =\n process.env.SECRETS_ENCRYPTION_KEY || process.env.BETTER_AUTH_SECRET;\n\n if (!explicit) {\n if (process.env.NODE_ENV === \"production\") {\n throw new Error(\n \"[agent-native/secrets] Refusing to start in production without an encryption key. \" +\n \"Set SECRETS_ENCRYPTION_KEY (preferred) or BETTER_AUTH_SECRET in the deploy environment. \" +\n \"The previous CWD-derived fallback was effectively static (e.g. `/var/task` on Lambda), \" +\n \"which means anyone with read access to the secrets table could decrypt every user's secrets.\",\n );\n }\n if (!_warnedFallback) {\n _warnedFallback = true;\n // eslint-disable-next-line no-console\n console.warn(\n \"[agent-native/secrets] SECRETS_ENCRYPTION_KEY not set — using a machine-local fallback. \" +\n \"Set SECRETS_ENCRYPTION_KEY (or BETTER_AUTH_SECRET) for production. \" +\n \"Production deploys without one of these env vars now hard-fail.\",\n );\n }\n }\n\n const material = explicit || `agent-native-secrets:${process.cwd()}`;\n return createHash(\"sha256\").update(material).digest();\n}\n\n/** Encrypt a plain-text value. Returns `v1:<iv-hex>:<ct-hex>:<tag-hex>`. */\nexport function encryptSecretValue(plaintext: string): string {\n const key = getSecretEncryptionKey();\n const iv = randomBytes(12);\n const cipher = createCipheriv(\"aes-256-gcm\", key, iv);\n const ct = Buffer.concat([cipher.update(plaintext, \"utf8\"), cipher.final()]);\n const tag = cipher.getAuthTag();\n return `v1:${iv.toString(\"hex\")}:${ct.toString(\"hex\")}:${tag.toString(\"hex\")}`;\n}\n\n/** Decrypt a value produced by `encryptSecretValue`. Throws on tampering. */\nexport function decryptSecretValue(encrypted: string): string {\n if (!encrypted.startsWith(\"v1:\")) {\n throw new Error(\"Unrecognised secret encoding\");\n }\n const [, ivHex, ctHex, tagHex] = encrypted.split(\":\");\n if (!ivHex || !ctHex || !tagHex) {\n throw new Error(\"Corrupt secret payload\");\n }\n const key = getSecretEncryptionKey();\n const decipher = createDecipheriv(\n \"aes-256-gcm\",\n key,\n Buffer.from(ivHex, \"hex\"),\n );\n decipher.setAuthTag(Buffer.from(tagHex, \"hex\"));\n const pt = Buffer.concat([\n decipher.update(Buffer.from(ctHex, \"hex\")),\n decipher.final(),\n ]);\n return pt.toString(\"utf8\");\n}\n\n/**\n * Strict check for a value produced by `encryptSecretValue`: `v1:` followed by\n * three hex segments. Intentionally strict so a legacy plaintext credential\n * that merely happens to start with `v1:` is treated as plaintext (and read via\n * the legacy fallback) rather than mis-decrypted.\n */\nconst ENCRYPTED_VALUE_RE = /^v1:[0-9a-f]+:[0-9a-f]+:[0-9a-f]+$/;\n\nexport function isEncryptedSecretValue(value: unknown): value is string {\n return typeof value === \"string\" && ENCRYPTED_VALUE_RE.test(value);\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../../src/secrets/storage.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;
|
|
1
|
+
{"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../../src/secrets/storage.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAKH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAoDjD;;;GAGG;AACH,wBAAgB,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAI3C;AAMD,MAAM,WAAW,SAAS;IACxB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,WAAW,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,eAAgB,SAAQ,SAAS;IAChD,KAAK,EAAE,MAAM,CAAC;IACd,kEAAkE;IAClE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,8DAA8D;IAC9D,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED;;;;GAIG;AACH,wBAAsB,cAAc,CAAC,IAAI,EAAE,eAAe,GAAG,OAAO,CAAC,MAAM,CAAC,CA0C3E;AAED,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;GAGG;AACH,wBAAsB,aAAa,CACjC,GAAG,EAAE,SAAS,GACb,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAqBlC;AAED;;;;GAIG;AACH,wBAAsB,gBAAgB,CACpC,GAAG,EAAE,SAAS,GACb,OAAO,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAAC,CAItD;AAED,MAAM,WAAW,UAAU;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,WAAW,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,YAAY,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;;GAIG;AACH,wBAAsB,iBAAiB,CACrC,GAAG,EAAE,SAAS,GACb,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CA2B5B;AAED;;;;GAIG;AACH,wBAAsB,sBAAsB,CAC1C,KAAK,EAAE,WAAW,EAClB,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,UAAU,EAAE,CAAC,CA0BvB;AAeD,wBAAsB,eAAe,CAAC,GAAG,EAAE,SAAS,GAAG,OAAO,CAAC,OAAO,CAAC,CAStE"}
|
package/dist/secrets/storage.js
CHANGED
|
@@ -11,9 +11,10 @@
|
|
|
11
11
|
*
|
|
12
12
|
* Secret values are NEVER logged and NEVER returned from any route handler.
|
|
13
13
|
*/
|
|
14
|
-
import { randomUUID
|
|
14
|
+
import { randomUUID } from "node:crypto";
|
|
15
15
|
import { getDbExec, isPostgres } from "../db/client.js";
|
|
16
16
|
import { APP_SECRETS_CREATE_SQL } from "./schema.js";
|
|
17
|
+
import { encryptSecretValue as encryptValue, decryptSecretValue as decryptValue, } from "./crypto.js";
|
|
17
18
|
// ---------------------------------------------------------------------------
|
|
18
19
|
// Table bootstrap
|
|
19
20
|
// ---------------------------------------------------------------------------
|
|
@@ -50,67 +51,8 @@ async function ensureTable() {
|
|
|
50
51
|
return _initPromise;
|
|
51
52
|
}
|
|
52
53
|
// ---------------------------------------------------------------------------
|
|
53
|
-
// Encryption
|
|
54
|
+
// Encryption — see ./crypto.ts (shared with per-user credentials)
|
|
54
55
|
// ---------------------------------------------------------------------------
|
|
55
|
-
/**
|
|
56
|
-
* Derive a 32-byte AES key from the configured secret material via SHA-256.
|
|
57
|
-
* Re-derived per-request (cheap, stateless, and makes rotation easy).
|
|
58
|
-
*
|
|
59
|
-
* In production we refuse to start with the CWD-derived fallback. Same
|
|
60
|
-
* posture `resolveAuthSecret` takes for `BETTER_AUTH_SECRET` — fail loud
|
|
61
|
-
* rather than encrypt every secret with a key that's effectively static
|
|
62
|
-
* across the whole deployment (Lambda CWD is `/var/task`, etc.). Anyone
|
|
63
|
-
* with read access to the DB (forgotten backup, pg_dump, downgraded env)
|
|
64
|
-
* could otherwise decrypt every user's secrets with trivial work.
|
|
65
|
-
*/
|
|
66
|
-
function getEncryptionKey() {
|
|
67
|
-
const explicit = process.env.SECRETS_ENCRYPTION_KEY || process.env.BETTER_AUTH_SECRET;
|
|
68
|
-
if (!explicit) {
|
|
69
|
-
if (process.env.NODE_ENV === "production") {
|
|
70
|
-
throw new Error("[agent-native/secrets] Refusing to start in production without an encryption key. " +
|
|
71
|
-
"Set SECRETS_ENCRYPTION_KEY (preferred) or BETTER_AUTH_SECRET in the deploy environment. " +
|
|
72
|
-
"The previous CWD-derived fallback was effectively static (e.g. `/var/task` on Lambda), " +
|
|
73
|
-
"which means anyone with read access to the secrets table could decrypt every user's secrets.");
|
|
74
|
-
}
|
|
75
|
-
if (!_warnedFallback) {
|
|
76
|
-
_warnedFallback = true;
|
|
77
|
-
// eslint-disable-next-line no-console
|
|
78
|
-
console.warn("[agent-native/secrets] SECRETS_ENCRYPTION_KEY not set — using a machine-local fallback. " +
|
|
79
|
-
"Set SECRETS_ENCRYPTION_KEY (or BETTER_AUTH_SECRET) for production. " +
|
|
80
|
-
"Production deploys without one of these env vars now hard-fail.");
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
const material = explicit || `agent-native-secrets:${process.cwd()}`;
|
|
84
|
-
return createHash("sha256").update(material).digest();
|
|
85
|
-
}
|
|
86
|
-
let _warnedFallback = false;
|
|
87
|
-
/** Encrypt a plain-text value. Returns `v1:<iv-hex>:<ct-hex>:<tag-hex>`. */
|
|
88
|
-
function encryptValue(plaintext) {
|
|
89
|
-
const key = getEncryptionKey();
|
|
90
|
-
const iv = randomBytes(12);
|
|
91
|
-
const cipher = createCipheriv("aes-256-gcm", key, iv);
|
|
92
|
-
const ct = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
|
|
93
|
-
const tag = cipher.getAuthTag();
|
|
94
|
-
return `v1:${iv.toString("hex")}:${ct.toString("hex")}:${tag.toString("hex")}`;
|
|
95
|
-
}
|
|
96
|
-
/** Decrypt a value produced by `encryptValue`. Throws on tampering. */
|
|
97
|
-
function decryptValue(encrypted) {
|
|
98
|
-
if (!encrypted.startsWith("v1:")) {
|
|
99
|
-
throw new Error("Unrecognised secret encoding");
|
|
100
|
-
}
|
|
101
|
-
const [, ivHex, ctHex, tagHex] = encrypted.split(":");
|
|
102
|
-
if (!ivHex || !ctHex || !tagHex) {
|
|
103
|
-
throw new Error("Corrupt secret payload");
|
|
104
|
-
}
|
|
105
|
-
const key = getEncryptionKey();
|
|
106
|
-
const decipher = createDecipheriv("aes-256-gcm", key, Buffer.from(ivHex, "hex"));
|
|
107
|
-
decipher.setAuthTag(Buffer.from(tagHex, "hex"));
|
|
108
|
-
const pt = Buffer.concat([
|
|
109
|
-
decipher.update(Buffer.from(ctHex, "hex")),
|
|
110
|
-
decipher.final(),
|
|
111
|
-
]);
|
|
112
|
-
return pt.toString("utf8");
|
|
113
|
-
}
|
|
114
56
|
/**
|
|
115
57
|
* Return the last 4 characters of a secret, with any leading characters
|
|
116
58
|
* masked. Used to show a preview without leaking the value.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"storage.js","sourceRoot":"","sources":["../../src/secrets/storage.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Storage layer for the framework secrets registry.\n *\n * Values are encrypted at rest with AES-256-GCM. The encryption key is\n * derived from `SECRETS_ENCRYPTION_KEY` (preferred) or the existing\n * `BETTER_AUTH_SECRET` env var (fallback so templates don't need a second\n * secret during development). If neither is set in production we fall back\n * to a machine-local key derived from the cwd — the secret is still only\n * readable on this machine, but consider setting `SECRETS_ENCRYPTION_KEY`\n * for a stable, rotatable key.\n *\n * Secret values are NEVER logged and NEVER returned from any route handler.\n */\n\nimport {\n randomUUID,\n randomBytes,\n createCipheriv,\n createDecipheriv,\n createHash,\n} from \"node:crypto\";\nimport { getDbExec, isPostgres } from \"../db/client.js\";\nimport { APP_SECRETS_CREATE_SQL } from \"./schema.js\";\nimport type { SecretScope } from \"./register.js\";\n\n// ---------------------------------------------------------------------------\n// Table bootstrap\n// ---------------------------------------------------------------------------\n\nlet _initPromise: Promise<void> | undefined;\n\nasync function ensureTable(): Promise<void> {\n if (!_initPromise) {\n _initPromise = (async () => {\n const client = getDbExec();\n // Postgres version of the CREATE TABLE — the generic `INTEGER` maps to\n // BIGINT on Postgres, which we need for millisecond timestamps.\n const sql = isPostgres()\n ? APP_SECRETS_CREATE_SQL.replace(/\\bINTEGER\\b/g, \"BIGINT\")\n : APP_SECRETS_CREATE_SQL;\n await client.execute(sql);\n\n // Additive migration: description column (for ad-hoc keys)\n try {\n await client.execute(\n `ALTER TABLE app_secrets ADD COLUMN description TEXT`,\n );\n } catch {\n // Column already exists — expected\n }\n\n // Additive migration: url_allowlist column\n try {\n await client.execute(\n `ALTER TABLE app_secrets ADD COLUMN url_allowlist TEXT`,\n );\n } catch {\n // Column already exists — expected\n }\n })().catch((err) => {\n _initPromise = undefined;\n throw err;\n });\n }\n return _initPromise;\n}\n\n// ---------------------------------------------------------------------------\n// Encryption\n// ---------------------------------------------------------------------------\n\n/**\n * Derive a 32-byte AES key from the configured secret material via SHA-256.\n * Re-derived per-request (cheap, stateless, and makes rotation easy).\n *\n * In production we refuse to start with the CWD-derived fallback. Same\n * posture `resolveAuthSecret` takes for `BETTER_AUTH_SECRET` — fail loud\n * rather than encrypt every secret with a key that's effectively static\n * across the whole deployment (Lambda CWD is `/var/task`, etc.). Anyone\n * with read access to the DB (forgotten backup, pg_dump, downgraded env)\n * could otherwise decrypt every user's secrets with trivial work.\n */\nfunction getEncryptionKey(): Buffer {\n const explicit =\n process.env.SECRETS_ENCRYPTION_KEY || process.env.BETTER_AUTH_SECRET;\n\n if (!explicit) {\n if (process.env.NODE_ENV === \"production\") {\n throw new Error(\n \"[agent-native/secrets] Refusing to start in production without an encryption key. \" +\n \"Set SECRETS_ENCRYPTION_KEY (preferred) or BETTER_AUTH_SECRET in the deploy environment. \" +\n \"The previous CWD-derived fallback was effectively static (e.g. `/var/task` on Lambda), \" +\n \"which means anyone with read access to the secrets table could decrypt every user's secrets.\",\n );\n }\n if (!_warnedFallback) {\n _warnedFallback = true;\n // eslint-disable-next-line no-console\n console.warn(\n \"[agent-native/secrets] SECRETS_ENCRYPTION_KEY not set — using a machine-local fallback. \" +\n \"Set SECRETS_ENCRYPTION_KEY (or BETTER_AUTH_SECRET) for production. \" +\n \"Production deploys without one of these env vars now hard-fail.\",\n );\n }\n }\n\n const material = explicit || `agent-native-secrets:${process.cwd()}`;\n return createHash(\"sha256\").update(material).digest();\n}\n\nlet _warnedFallback = false;\n\n/** Encrypt a plain-text value. Returns `v1:<iv-hex>:<ct-hex>:<tag-hex>`. */\nfunction encryptValue(plaintext: string): string {\n const key = getEncryptionKey();\n const iv = randomBytes(12);\n const cipher = createCipheriv(\"aes-256-gcm\", key, iv);\n const ct = Buffer.concat([cipher.update(plaintext, \"utf8\"), cipher.final()]);\n const tag = cipher.getAuthTag();\n return `v1:${iv.toString(\"hex\")}:${ct.toString(\"hex\")}:${tag.toString(\"hex\")}`;\n}\n\n/** Decrypt a value produced by `encryptValue`. Throws on tampering. */\nfunction decryptValue(encrypted: string): string {\n if (!encrypted.startsWith(\"v1:\")) {\n throw new Error(\"Unrecognised secret encoding\");\n }\n const [, ivHex, ctHex, tagHex] = encrypted.split(\":\");\n if (!ivHex || !ctHex || !tagHex) {\n throw new Error(\"Corrupt secret payload\");\n }\n const key = getEncryptionKey();\n const decipher = createDecipheriv(\n \"aes-256-gcm\",\n key,\n Buffer.from(ivHex, \"hex\"),\n );\n decipher.setAuthTag(Buffer.from(tagHex, \"hex\"));\n const pt = Buffer.concat([\n decipher.update(Buffer.from(ctHex, \"hex\")),\n decipher.final(),\n ]);\n return pt.toString(\"utf8\");\n}\n\n/**\n * Return the last 4 characters of a secret, with any leading characters\n * masked. Used to show a preview without leaking the value.\n */\nexport function last4(value: string): string {\n if (!value) return \"\";\n if (value.length <= 4) return \"••••\";\n return \"••••\" + value.slice(-4);\n}\n\n// ---------------------------------------------------------------------------\n// CRUD\n// ---------------------------------------------------------------------------\n\nexport interface SecretRef {\n key: string;\n scope: SecretScope;\n scopeId: string;\n}\n\nexport interface WriteSecretArgs extends SecretRef {\n value: string;\n /** Optional human-readable description (used for ad-hoc keys). */\n description?: string;\n /** Optional JSON-stringified array of allowed URL origins. */\n urlAllowlist?: string;\n}\n\n/**\n * Write (insert or update) a secret. The value is encrypted before being\n * stored — the caller's plaintext is never persisted. Returns the new\n * record's id.\n */\nexport async function writeAppSecret(args: WriteSecretArgs): Promise<string> {\n await ensureTable();\n const { key, value, scope, scopeId, description, urlAllowlist } = args;\n if (!key || !value || !scope || !scopeId) {\n throw new Error(\n \"writeAppSecret: key, value, scope, and scopeId are all required\",\n );\n }\n const client = getDbExec();\n const now = Date.now();\n const encrypted = encryptValue(value);\n\n // Upsert by (scope, scope_id, key). Keep the existing row's id on update so\n // references stay stable.\n const { rows } = await client.execute({\n sql: `SELECT id FROM app_secrets WHERE scope = ? AND scope_id = ? AND key = ?`,\n args: [scope, scopeId, key],\n });\n if (rows.length > 0) {\n const id = rows[0].id as string;\n await client.execute({\n sql: `UPDATE app_secrets SET encrypted_value = ?, description = ?, url_allowlist = ?, updated_at = ? WHERE id = ?`,\n args: [encrypted, description ?? null, urlAllowlist ?? null, now, id],\n });\n return id;\n }\n const id = randomUUID();\n await client.execute({\n sql: `INSERT INTO app_secrets (id, scope, scope_id, key, encrypted_value, description, url_allowlist, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n args: [\n id,\n scope,\n scopeId,\n key,\n encrypted,\n description ?? null,\n urlAllowlist ?? null,\n now,\n now,\n ],\n });\n return id;\n}\n\nexport interface ReadSecretResult {\n value: string;\n last4: string;\n updatedAt: number;\n}\n\n/**\n * Read a secret's plaintext value. Returns null when not found. The caller\n * is responsible for never logging the returned value.\n */\nexport async function readAppSecret(\n ref: SecretRef,\n): Promise<ReadSecretResult | null> {\n await ensureTable();\n const { key, scope, scopeId } = ref;\n const client = getDbExec();\n const { rows } = await client.execute({\n sql: `SELECT encrypted_value, updated_at FROM app_secrets WHERE scope = ? AND scope_id = ? AND key = ? LIMIT 1`,\n args: [scope, scopeId, key],\n });\n if (rows.length === 0) return null;\n try {\n const value = decryptValue(rows[0].encrypted_value as string);\n return {\n value,\n last4: last4(value),\n updatedAt: Number(rows[0].updated_at ?? 0),\n };\n } catch {\n // Decryption failure — key rotated, tampered row, etc. Don't throw up the\n // stack in a way that could leak the ciphertext; just report missing.\n return null;\n }\n}\n\n/**\n * Return just the metadata for a secret (no value). Used by the list route so\n * the UI can show the \"Set\" pill and last-4 without the decrypted value going\n * over the wire.\n */\nexport async function getAppSecretMeta(\n ref: SecretRef,\n): Promise<{ last4: string; updatedAt: number } | null> {\n const result = await readAppSecret(ref);\n if (!result) return null;\n return { last4: result.last4, updatedAt: result.updatedAt };\n}\n\nexport interface SecretMeta {\n key: string;\n scope: SecretScope;\n scopeId: string;\n last4: string;\n description: string | null;\n urlAllowlist: string[] | null;\n createdAt: number;\n updatedAt: number;\n}\n\n/**\n * Read a secret's metadata, including ad-hoc fields (description, allowlist),\n * without ever decrypting or returning the plaintext value. Used by the\n * ad-hoc list route and any UI that wants to render a key tile.\n */\nexport async function readAppSecretMeta(\n ref: SecretRef,\n): Promise<SecretMeta | null> {\n await ensureTable();\n const { key, scope, scopeId } = ref;\n const client = getDbExec();\n const { rows } = await client.execute({\n sql: `SELECT encrypted_value, description, url_allowlist, created_at, updated_at FROM app_secrets WHERE scope = ? AND scope_id = ? AND key = ? LIMIT 1`,\n args: [scope, scopeId, key],\n });\n if (rows.length === 0) return null;\n const row = rows[0];\n let last4Value = \"\";\n try {\n const value = decryptValue(row.encrypted_value as string);\n last4Value = last4(value);\n } catch {\n last4Value = \"\";\n }\n return {\n key,\n scope,\n scopeId,\n last4: last4Value,\n description: (row.description as string | null) ?? null,\n urlAllowlist: parseAllowlist(row.url_allowlist as string | null),\n createdAt: Number(row.created_at ?? 0),\n updatedAt: Number(row.updated_at ?? 0),\n };\n}\n\n/**\n * List all secrets for a given scope. Returns metadata only — values are\n * never decrypted or returned. Used by the ad-hoc list route to surface\n * user-created keys.\n */\nexport async function listAppSecretsForScope(\n scope: SecretScope,\n scopeId: string,\n): Promise<SecretMeta[]> {\n await ensureTable();\n const client = getDbExec();\n const { rows } = await client.execute({\n sql: `SELECT key, encrypted_value, description, url_allowlist, created_at, updated_at FROM app_secrets WHERE scope = ? AND scope_id = ? ORDER BY updated_at DESC`,\n args: [scope, scopeId],\n });\n return rows.map((row) => {\n let last4Value = \"\";\n try {\n const value = decryptValue(row.encrypted_value as string);\n last4Value = last4(value);\n } catch {\n last4Value = \"\";\n }\n return {\n key: row.key as string,\n scope,\n scopeId,\n last4: last4Value,\n description: (row.description as string | null) ?? null,\n urlAllowlist: parseAllowlist(row.url_allowlist as string | null),\n createdAt: Number(row.created_at ?? 0),\n updatedAt: Number(row.updated_at ?? 0),\n };\n });\n}\n\nfunction parseAllowlist(raw: string | null): string[] | null {\n if (!raw) return null;\n try {\n const parsed = JSON.parse(raw);\n if (Array.isArray(parsed) && parsed.every((v) => typeof v === \"string\")) {\n return parsed;\n }\n return null;\n } catch {\n return null;\n }\n}\n\nexport async function deleteAppSecret(ref: SecretRef): Promise<boolean> {\n await ensureTable();\n const { key, scope, scopeId } = ref;\n const client = getDbExec();\n const { rowsAffected } = await client.execute({\n sql: `DELETE FROM app_secrets WHERE scope = ? AND scope_id = ? AND key = ?`,\n args: [scope, scopeId, key],\n });\n return rowsAffected > 0;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"storage.js","sourceRoot":"","sources":["../../src/secrets/storage.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Storage layer for the framework secrets registry.\n *\n * Values are encrypted at rest with AES-256-GCM. The encryption key is\n * derived from `SECRETS_ENCRYPTION_KEY` (preferred) or the existing\n * `BETTER_AUTH_SECRET` env var (fallback so templates don't need a second\n * secret during development). If neither is set in production we fall back\n * to a machine-local key derived from the cwd — the secret is still only\n * readable on this machine, but consider setting `SECRETS_ENCRYPTION_KEY`\n * for a stable, rotatable key.\n *\n * Secret values are NEVER logged and NEVER returned from any route handler.\n */\n\nimport { randomUUID } from \"node:crypto\";\nimport { getDbExec, isPostgres } from \"../db/client.js\";\nimport { APP_SECRETS_CREATE_SQL } from \"./schema.js\";\nimport type { SecretScope } from \"./register.js\";\nimport {\n encryptSecretValue as encryptValue,\n decryptSecretValue as decryptValue,\n} from \"./crypto.js\";\n\n// ---------------------------------------------------------------------------\n// Table bootstrap\n// ---------------------------------------------------------------------------\n\nlet _initPromise: Promise<void> | undefined;\n\nasync function ensureTable(): Promise<void> {\n if (!_initPromise) {\n _initPromise = (async () => {\n const client = getDbExec();\n // Postgres version of the CREATE TABLE — the generic `INTEGER` maps to\n // BIGINT on Postgres, which we need for millisecond timestamps.\n const sql = isPostgres()\n ? APP_SECRETS_CREATE_SQL.replace(/\\bINTEGER\\b/g, \"BIGINT\")\n : APP_SECRETS_CREATE_SQL;\n await client.execute(sql);\n\n // Additive migration: description column (for ad-hoc keys)\n try {\n await client.execute(\n `ALTER TABLE app_secrets ADD COLUMN description TEXT`,\n );\n } catch {\n // Column already exists — expected\n }\n\n // Additive migration: url_allowlist column\n try {\n await client.execute(\n `ALTER TABLE app_secrets ADD COLUMN url_allowlist TEXT`,\n );\n } catch {\n // Column already exists — expected\n }\n })().catch((err) => {\n _initPromise = undefined;\n throw err;\n });\n }\n return _initPromise;\n}\n\n// ---------------------------------------------------------------------------\n// Encryption — see ./crypto.ts (shared with per-user credentials)\n// ---------------------------------------------------------------------------\n\n/**\n * Return the last 4 characters of a secret, with any leading characters\n * masked. Used to show a preview without leaking the value.\n */\nexport function last4(value: string): string {\n if (!value) return \"\";\n if (value.length <= 4) return \"••••\";\n return \"••••\" + value.slice(-4);\n}\n\n// ---------------------------------------------------------------------------\n// CRUD\n// ---------------------------------------------------------------------------\n\nexport interface SecretRef {\n key: string;\n scope: SecretScope;\n scopeId: string;\n}\n\nexport interface WriteSecretArgs extends SecretRef {\n value: string;\n /** Optional human-readable description (used for ad-hoc keys). */\n description?: string;\n /** Optional JSON-stringified array of allowed URL origins. */\n urlAllowlist?: string;\n}\n\n/**\n * Write (insert or update) a secret. The value is encrypted before being\n * stored — the caller's plaintext is never persisted. Returns the new\n * record's id.\n */\nexport async function writeAppSecret(args: WriteSecretArgs): Promise<string> {\n await ensureTable();\n const { key, value, scope, scopeId, description, urlAllowlist } = args;\n if (!key || !value || !scope || !scopeId) {\n throw new Error(\n \"writeAppSecret: key, value, scope, and scopeId are all required\",\n );\n }\n const client = getDbExec();\n const now = Date.now();\n const encrypted = encryptValue(value);\n\n // Upsert by (scope, scope_id, key). Keep the existing row's id on update so\n // references stay stable.\n const { rows } = await client.execute({\n sql: `SELECT id FROM app_secrets WHERE scope = ? AND scope_id = ? AND key = ?`,\n args: [scope, scopeId, key],\n });\n if (rows.length > 0) {\n const id = rows[0].id as string;\n await client.execute({\n sql: `UPDATE app_secrets SET encrypted_value = ?, description = ?, url_allowlist = ?, updated_at = ? WHERE id = ?`,\n args: [encrypted, description ?? null, urlAllowlist ?? null, now, id],\n });\n return id;\n }\n const id = randomUUID();\n await client.execute({\n sql: `INSERT INTO app_secrets (id, scope, scope_id, key, encrypted_value, description, url_allowlist, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n args: [\n id,\n scope,\n scopeId,\n key,\n encrypted,\n description ?? null,\n urlAllowlist ?? null,\n now,\n now,\n ],\n });\n return id;\n}\n\nexport interface ReadSecretResult {\n value: string;\n last4: string;\n updatedAt: number;\n}\n\n/**\n * Read a secret's plaintext value. Returns null when not found. The caller\n * is responsible for never logging the returned value.\n */\nexport async function readAppSecret(\n ref: SecretRef,\n): Promise<ReadSecretResult | null> {\n await ensureTable();\n const { key, scope, scopeId } = ref;\n const client = getDbExec();\n const { rows } = await client.execute({\n sql: `SELECT encrypted_value, updated_at FROM app_secrets WHERE scope = ? AND scope_id = ? AND key = ? LIMIT 1`,\n args: [scope, scopeId, key],\n });\n if (rows.length === 0) return null;\n try {\n const value = decryptValue(rows[0].encrypted_value as string);\n return {\n value,\n last4: last4(value),\n updatedAt: Number(rows[0].updated_at ?? 0),\n };\n } catch {\n // Decryption failure — key rotated, tampered row, etc. Don't throw up the\n // stack in a way that could leak the ciphertext; just report missing.\n return null;\n }\n}\n\n/**\n * Return just the metadata for a secret (no value). Used by the list route so\n * the UI can show the \"Set\" pill and last-4 without the decrypted value going\n * over the wire.\n */\nexport async function getAppSecretMeta(\n ref: SecretRef,\n): Promise<{ last4: string; updatedAt: number } | null> {\n const result = await readAppSecret(ref);\n if (!result) return null;\n return { last4: result.last4, updatedAt: result.updatedAt };\n}\n\nexport interface SecretMeta {\n key: string;\n scope: SecretScope;\n scopeId: string;\n last4: string;\n description: string | null;\n urlAllowlist: string[] | null;\n createdAt: number;\n updatedAt: number;\n}\n\n/**\n * Read a secret's metadata, including ad-hoc fields (description, allowlist),\n * without ever decrypting or returning the plaintext value. Used by the\n * ad-hoc list route and any UI that wants to render a key tile.\n */\nexport async function readAppSecretMeta(\n ref: SecretRef,\n): Promise<SecretMeta | null> {\n await ensureTable();\n const { key, scope, scopeId } = ref;\n const client = getDbExec();\n const { rows } = await client.execute({\n sql: `SELECT encrypted_value, description, url_allowlist, created_at, updated_at FROM app_secrets WHERE scope = ? AND scope_id = ? AND key = ? LIMIT 1`,\n args: [scope, scopeId, key],\n });\n if (rows.length === 0) return null;\n const row = rows[0];\n let last4Value = \"\";\n try {\n const value = decryptValue(row.encrypted_value as string);\n last4Value = last4(value);\n } catch {\n last4Value = \"\";\n }\n return {\n key,\n scope,\n scopeId,\n last4: last4Value,\n description: (row.description as string | null) ?? null,\n urlAllowlist: parseAllowlist(row.url_allowlist as string | null),\n createdAt: Number(row.created_at ?? 0),\n updatedAt: Number(row.updated_at ?? 0),\n };\n}\n\n/**\n * List all secrets for a given scope. Returns metadata only — values are\n * never decrypted or returned. Used by the ad-hoc list route to surface\n * user-created keys.\n */\nexport async function listAppSecretsForScope(\n scope: SecretScope,\n scopeId: string,\n): Promise<SecretMeta[]> {\n await ensureTable();\n const client = getDbExec();\n const { rows } = await client.execute({\n sql: `SELECT key, encrypted_value, description, url_allowlist, created_at, updated_at FROM app_secrets WHERE scope = ? AND scope_id = ? ORDER BY updated_at DESC`,\n args: [scope, scopeId],\n });\n return rows.map((row) => {\n let last4Value = \"\";\n try {\n const value = decryptValue(row.encrypted_value as string);\n last4Value = last4(value);\n } catch {\n last4Value = \"\";\n }\n return {\n key: row.key as string,\n scope,\n scopeId,\n last4: last4Value,\n description: (row.description as string | null) ?? null,\n urlAllowlist: parseAllowlist(row.url_allowlist as string | null),\n createdAt: Number(row.created_at ?? 0),\n updatedAt: Number(row.updated_at ?? 0),\n };\n });\n}\n\nfunction parseAllowlist(raw: string | null): string[] | null {\n if (!raw) return null;\n try {\n const parsed = JSON.parse(raw);\n if (Array.isArray(parsed) && parsed.every((v) => typeof v === \"string\")) {\n return parsed;\n }\n return null;\n } catch {\n return null;\n }\n}\n\nexport async function deleteAppSecret(ref: SecretRef): Promise<boolean> {\n await ensureTable();\n const { key, scope, scopeId } = ref;\n const client = getDbExec();\n const { rowsAffected } = await client.execute({\n sql: `DELETE FROM app_secrets WHERE scope = ? AND scope_id = ? AND key = ?`,\n args: [scope, scopeId, key],\n });\n return rowsAffected > 0;\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"action-discovery.d.ts","sourceRoot":"","sources":["../../src/server/action-discovery.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;AA4ChE;;;;;;;;;;;;;GAaG;AACH,wBAAgB,sBAAsB,CACpC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,GACnC,IAAI,CAKN;AAmOD;;;;;;;;GAQG;AACH,wBAAgB,6BAA6B,CAC3C,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC/B,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAqC7B;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAsB,mBAAmB,CACvC,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC,CAgGtC;AAED,wBAAsB,uBAAuB,CAC3C,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,GACpC,OAAO,CAAC,IAAI,CAAC,
|
|
1
|
+
{"version":3,"file":"action-discovery.d.ts","sourceRoot":"","sources":["../../src/server/action-discovery.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;AA4ChE;;;;;;;;;;;;;GAaG;AACH,wBAAgB,sBAAsB,CACpC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,GACnC,IAAI,CAKN;AAmOD;;;;;;;;GAQG;AACH,wBAAgB,6BAA6B,CAC3C,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC/B,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAqC7B;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAsB,mBAAmB,CACvC,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC,CAgGtC;AAED,wBAAsB,uBAAuB,CAC3C,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,GACpC,OAAO,CAAC,IAAI,CAAC,CA2Cf;AAED,oDAAoD;AACpD,eAAO,MAAM,mBAAmB,4BAAsB,CAAC"}
|
|
@@ -454,8 +454,11 @@ export async function mergeCoreSharingActions(registry) {
|
|
|
454
454
|
tool: def.tool,
|
|
455
455
|
run: def.run,
|
|
456
456
|
...(def.http !== undefined ? { http: def.http } : {}),
|
|
457
|
-
|
|
458
|
-
|
|
457
|
+
// Carry security-relevant flags (toolCallable, publicAgent, link,
|
|
458
|
+
// mcpApp) plus readOnly/parallelSafe. Without this, the sharing
|
|
459
|
+
// actions' `toolCallable: false` (audit-H5) is dropped and the
|
|
460
|
+
// tools-iframe bridge 403 in action-routes.ts never fires.
|
|
461
|
+
...preserveActionFlags(def),
|
|
459
462
|
};
|
|
460
463
|
}
|
|
461
464
|
}
|