@agent-native/core 0.8.2 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/dist/agent/engine/builder-engine.d.ts.map +1 -1
- package/dist/agent/engine/builder-engine.js +5 -4
- package/dist/agent/engine/builder-engine.js.map +1 -1
- package/dist/agent/engine/registry.d.ts +6 -3
- package/dist/agent/engine/registry.d.ts.map +1 -1
- package/dist/agent/engine/registry.js +8 -17
- package/dist/agent/engine/registry.js.map +1 -1
- package/dist/agent/production-agent.d.ts +1 -1
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +28 -11
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/agent/run-manager.d.ts +10 -0
- package/dist/agent/run-manager.d.ts.map +1 -1
- package/dist/agent/run-manager.js +89 -7
- package/dist/agent/run-manager.js.map +1 -1
- package/dist/agent/run-store.d.ts +4 -1
- package/dist/agent/run-store.d.ts.map +1 -1
- package/dist/agent/run-store.js +6 -5
- package/dist/agent/run-store.js.map +1 -1
- package/dist/agent/thread-data-builder.d.ts +12 -0
- package/dist/agent/thread-data-builder.d.ts.map +1 -1
- package/dist/agent/thread-data-builder.js +96 -0
- package/dist/agent/thread-data-builder.js.map +1 -1
- package/dist/cli/create.d.ts +9 -0
- package/dist/cli/create.d.ts.map +1 -1
- package/dist/cli/create.js +29 -11
- package/dist/cli/create.js.map +1 -1
- package/dist/cli/index.js +177 -22
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/workspace-dev.js +66 -5
- package/dist/cli/workspace-dev.js.map +1 -1
- package/dist/client/AgentPanel.d.ts.map +1 -1
- package/dist/client/AgentPanel.js +6 -20
- package/dist/client/AgentPanel.js.map +1 -1
- package/dist/client/AssistantChat.d.ts.map +1 -1
- package/dist/client/AssistantChat.js +146 -107
- package/dist/client/AssistantChat.js.map +1 -1
- package/dist/client/agent-chat-adapter.d.ts.map +1 -1
- package/dist/client/agent-chat-adapter.js +143 -22
- package/dist/client/agent-chat-adapter.js.map +1 -1
- package/dist/client/agent-sidebar-state.d.ts +3 -0
- package/dist/client/agent-sidebar-state.d.ts.map +1 -0
- package/dist/client/agent-sidebar-state.js +24 -0
- package/dist/client/agent-sidebar-state.js.map +1 -0
- package/dist/client/analytics.d.ts +39 -0
- package/dist/client/analytics.d.ts.map +1 -1
- package/dist/client/analytics.js +74 -0
- package/dist/client/analytics.js.map +1 -1
- package/dist/client/components/PresenceBar.d.ts.map +1 -1
- package/dist/client/components/PresenceBar.js +21 -15
- package/dist/client/components/PresenceBar.js.map +1 -1
- package/dist/client/components/ui/tooltip.d.ts +2 -1
- package/dist/client/components/ui/tooltip.d.ts.map +1 -1
- package/dist/client/components/ui/tooltip.js +9 -2
- package/dist/client/components/ui/tooltip.js.map +1 -1
- package/dist/client/composer/ComposerPlusMenu.d.ts.map +1 -1
- package/dist/client/composer/ComposerPlusMenu.js +51 -17
- package/dist/client/composer/ComposerPlusMenu.js.map +1 -1
- package/dist/client/composer/PromptComposer.d.ts.map +1 -1
- package/dist/client/composer/PromptComposer.js +30 -0
- package/dist/client/composer/PromptComposer.js.map +1 -1
- package/dist/client/composer/TiptapComposer.d.ts.map +1 -1
- package/dist/client/composer/TiptapComposer.js +31 -5
- package/dist/client/composer/TiptapComposer.js.map +1 -1
- package/dist/client/composer/VoiceButton.d.ts.map +1 -1
- package/dist/client/composer/VoiceButton.js +9 -8
- package/dist/client/composer/VoiceButton.js.map +1 -1
- package/dist/client/dev-overlay/DevOverlay.d.ts.map +1 -1
- package/dist/client/dev-overlay/DevOverlay.js +4 -3
- package/dist/client/dev-overlay/DevOverlay.js.map +1 -1
- package/dist/client/error-format.d.ts.map +1 -1
- package/dist/client/error-format.js +6 -0
- package/dist/client/error-format.js.map +1 -1
- package/dist/client/extensions/EmbeddedExtension.d.ts.map +1 -1
- package/dist/client/extensions/EmbeddedExtension.js +14 -3
- package/dist/client/extensions/EmbeddedExtension.js.map +1 -1
- package/dist/client/extensions/ExtensionEditor.d.ts.map +1 -1
- package/dist/client/extensions/ExtensionEditor.js +6 -5
- package/dist/client/extensions/ExtensionEditor.js.map +1 -1
- package/dist/client/extensions/ExtensionSlot.d.ts.map +1 -1
- package/dist/client/extensions/ExtensionSlot.js +2 -1
- package/dist/client/extensions/ExtensionSlot.js.map +1 -1
- package/dist/client/extensions/ExtensionViewer.d.ts.map +1 -1
- package/dist/client/extensions/ExtensionViewer.js +40 -19
- package/dist/client/extensions/ExtensionViewer.js.map +1 -1
- package/dist/client/extensions/ExtensionsSidebarSection.d.ts.map +1 -1
- package/dist/client/extensions/ExtensionsSidebarSection.js +52 -51
- package/dist/client/extensions/ExtensionsSidebarSection.js.map +1 -1
- 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/integrations/IntegrationCard.d.ts.map +1 -1
- package/dist/client/integrations/IntegrationCard.js +2 -1
- package/dist/client/integrations/IntegrationCard.js.map +1 -1
- package/dist/client/integrations/IntegrationsPanel.d.ts.map +1 -1
- package/dist/client/integrations/IntegrationsPanel.js +3 -2
- package/dist/client/integrations/IntegrationsPanel.js.map +1 -1
- package/dist/client/notifications/NotificationsBell.d.ts.map +1 -1
- package/dist/client/notifications/NotificationsBell.js +42 -6
- package/dist/client/notifications/NotificationsBell.js.map +1 -1
- package/dist/client/onboarding/OnboardingPanel.d.ts.map +1 -1
- package/dist/client/onboarding/OnboardingPanel.js +3 -2
- package/dist/client/onboarding/OnboardingPanel.js.map +1 -1
- package/dist/client/onboarding/SetupButton.d.ts.map +1 -1
- package/dist/client/onboarding/SetupButton.js +14 -13
- package/dist/client/onboarding/SetupButton.js.map +1 -1
- package/dist/client/org/InvitationBanner.d.ts +8 -2
- package/dist/client/org/InvitationBanner.d.ts.map +1 -1
- package/dist/client/org/InvitationBanner.js +28 -7
- package/dist/client/org/InvitationBanner.js.map +1 -1
- package/dist/client/org/OrgSwitcher.d.ts.map +1 -1
- package/dist/client/org/OrgSwitcher.js +29 -5
- package/dist/client/org/OrgSwitcher.js.map +1 -1
- package/dist/client/org/TeamPage.d.ts.map +1 -1
- package/dist/client/org/TeamPage.js +9 -7
- package/dist/client/org/TeamPage.js.map +1 -1
- package/dist/client/resources/ResourceEditor.d.ts.map +1 -1
- package/dist/client/resources/ResourceEditor.js +2 -1
- package/dist/client/resources/ResourceEditor.js.map +1 -1
- package/dist/client/resources/ResourcesPanel.d.ts.map +1 -1
- package/dist/client/resources/ResourcesPanel.js +48 -14
- package/dist/client/resources/ResourcesPanel.js.map +1 -1
- package/dist/client/resources/use-mcp-servers.d.ts +2 -0
- package/dist/client/resources/use-mcp-servers.d.ts.map +1 -1
- package/dist/client/resources/use-mcp-servers.js +59 -3
- package/dist/client/resources/use-mcp-servers.js.map +1 -1
- package/dist/client/settings/AgentsSection.d.ts.map +1 -1
- package/dist/client/settings/AgentsSection.js +8 -7
- package/dist/client/settings/AgentsSection.js.map +1 -1
- package/dist/client/settings/AutomationsSection.d.ts.map +1 -1
- package/dist/client/settings/AutomationsSection.js +4 -3
- package/dist/client/settings/AutomationsSection.js.map +1 -1
- package/dist/client/settings/SecretsSection.d.ts.map +1 -1
- package/dist/client/settings/SecretsSection.js +11 -1
- package/dist/client/settings/SecretsSection.js.map +1 -1
- package/dist/client/settings/SettingsPanel.d.ts.map +1 -1
- package/dist/client/settings/SettingsPanel.js +15 -12
- package/dist/client/settings/SettingsPanel.js.map +1 -1
- package/dist/client/settings/VoiceTranscriptionSection.d.ts.map +1 -1
- package/dist/client/settings/VoiceTranscriptionSection.js +13 -30
- package/dist/client/settings/VoiceTranscriptionSection.js.map +1 -1
- package/dist/client/settings/index.d.ts +1 -1
- package/dist/client/settings/index.d.ts.map +1 -1
- package/dist/client/settings/index.js.map +1 -1
- package/dist/client/settings/useBuilderStatus.d.ts.map +1 -1
- package/dist/client/settings/useBuilderStatus.js +27 -1
- package/dist/client/settings/useBuilderStatus.js.map +1 -1
- package/dist/client/sharing/ShareButton.d.ts +4 -0
- package/dist/client/sharing/ShareButton.d.ts.map +1 -1
- package/dist/client/sharing/ShareButton.js +5 -1
- package/dist/client/sharing/ShareButton.js.map +1 -1
- package/dist/client/sse-event-processor.d.ts +1 -1
- package/dist/client/sse-event-processor.d.ts.map +1 -1
- package/dist/client/sse-event-processor.js +59 -11
- package/dist/client/sse-event-processor.js.map +1 -1
- package/dist/client/use-db-sync.d.ts.map +1 -1
- package/dist/client/use-db-sync.js +100 -19
- package/dist/client/use-db-sync.js.map +1 -1
- package/dist/client/use-session.d.ts.map +1 -1
- package/dist/client/use-session.js +14 -2
- package/dist/client/use-session.js.map +1 -1
- package/dist/collab/client.d.ts +1 -0
- package/dist/collab/client.d.ts.map +1 -1
- package/dist/collab/client.js +18 -1
- package/dist/collab/client.js.map +1 -1
- package/dist/deploy/build.d.ts.map +1 -1
- package/dist/deploy/build.js +5 -0
- package/dist/deploy/build.js.map +1 -1
- package/dist/deploy/route-discovery.d.ts.map +1 -1
- package/dist/deploy/route-discovery.js +1 -0
- package/dist/deploy/route-discovery.js.map +1 -1
- package/dist/deploy/workspace-core.d.ts +1 -1
- package/dist/deploy/workspace-core.d.ts.map +1 -1
- package/dist/deploy/workspace-core.js +1 -0
- package/dist/deploy/workspace-core.js.map +1 -1
- package/dist/extensions/actions.d.ts.map +1 -1
- package/dist/extensions/actions.js +17 -3
- package/dist/extensions/actions.js.map +1 -1
- package/dist/extensions/routes.js +1 -1
- package/dist/extensions/routes.js.map +1 -1
- package/dist/extensions/schema.d.ts +14 -14
- package/dist/extensions/schema.d.ts.map +1 -1
- package/dist/extensions/schema.js +4 -4
- package/dist/extensions/schema.js.map +1 -1
- package/dist/extensions/store.d.ts.map +1 -1
- package/dist/extensions/store.js +23 -0
- package/dist/extensions/store.js.map +1 -1
- package/dist/extensions/theme.d.ts +8 -1
- package/dist/extensions/theme.d.ts.map +1 -1
- package/dist/extensions/theme.js +43 -34
- package/dist/extensions/theme.js.map +1 -1
- package/dist/mcp-client/routes.d.ts +1 -0
- package/dist/mcp-client/routes.d.ts.map +1 -1
- package/dist/mcp-client/routes.js +28 -1
- package/dist/mcp-client/routes.js.map +1 -1
- package/dist/org/auto-join-domain.d.ts +28 -0
- package/dist/org/auto-join-domain.d.ts.map +1 -0
- package/dist/org/auto-join-domain.js +92 -0
- package/dist/org/auto-join-domain.js.map +1 -0
- package/dist/org/index.d.ts +2 -0
- package/dist/org/index.d.ts.map +1 -1
- package/dist/org/index.js +1 -0
- package/dist/org/index.js.map +1 -1
- package/dist/scripts/db/exec.d.ts.map +1 -1
- package/dist/scripts/db/exec.js +27 -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/reset-dev-owner.d.ts +27 -0
- package/dist/scripts/db/reset-dev-owner.d.ts.map +1 -0
- package/dist/scripts/db/reset-dev-owner.js +225 -0
- package/dist/scripts/db/reset-dev-owner.js.map +1 -0
- package/dist/scripts/db/scoping.d.ts.map +1 -1
- package/dist/scripts/db/scoping.js +15 -30
- package/dist/scripts/db/scoping.js.map +1 -1
- package/dist/scripts/dev-session.d.ts +46 -0
- package/dist/scripts/dev-session.d.ts.map +1 -0
- package/dist/scripts/dev-session.js +81 -0
- package/dist/scripts/dev-session.js.map +1 -0
- package/dist/scripts/runner.d.ts.map +1 -1
- package/dist/scripts/runner.js +21 -0
- package/dist/scripts/runner.js.map +1 -1
- package/dist/secrets/register.d.ts +1 -1
- package/dist/secrets/register.d.ts.map +1 -1
- package/dist/secrets/register.js +4 -2
- package/dist/secrets/register.js.map +1 -1
- package/dist/secrets/routes.d.ts.map +1 -1
- package/dist/secrets/routes.js +32 -0
- package/dist/secrets/routes.js.map +1 -1
- package/dist/server/agent-chat-plugin.d.ts.map +1 -1
- package/dist/server/agent-chat-plugin.js +77 -102
- package/dist/server/agent-chat-plugin.js.map +1 -1
- package/dist/server/auth.d.ts.map +1 -1
- package/dist/server/auth.js +33 -0
- package/dist/server/auth.js.map +1 -1
- package/dist/server/better-auth-instance.d.ts.map +1 -1
- package/dist/server/better-auth-instance.js +11 -0
- package/dist/server/better-auth-instance.js.map +1 -1
- package/dist/server/builder-browser.d.ts.map +1 -1
- package/dist/server/builder-browser.js +169 -68
- package/dist/server/builder-browser.js.map +1 -1
- package/dist/server/core-routes-plugin.d.ts.map +1 -1
- package/dist/server/core-routes-plugin.js +56 -13
- package/dist/server/core-routes-plugin.js.map +1 -1
- package/dist/server/credential-provider.d.ts +49 -6
- package/dist/server/credential-provider.d.ts.map +1 -1
- package/dist/server/credential-provider.js +133 -38
- package/dist/server/credential-provider.js.map +1 -1
- package/dist/server/design-token-utils.d.ts +13 -2
- package/dist/server/design-token-utils.d.ts.map +1 -1
- package/dist/server/design-token-utils.js +48 -16
- package/dist/server/design-token-utils.js.map +1 -1
- package/dist/server/framework-request-handler.d.ts.map +1 -1
- package/dist/server/framework-request-handler.js +31 -0
- package/dist/server/framework-request-handler.js.map +1 -1
- package/dist/server/google-realtime-session.d.ts.map +1 -1
- package/dist/server/google-realtime-session.js +19 -6
- package/dist/server/google-realtime-session.js.map +1 -1
- package/dist/server/index.d.ts +2 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +2 -0
- package/dist/server/index.js.map +1 -1
- package/dist/server/onboarding-html.d.ts.map +1 -1
- package/dist/server/onboarding-html.js +142 -14
- package/dist/server/onboarding-html.js.map +1 -1
- package/dist/server/request-context.d.ts +17 -0
- package/dist/server/request-context.d.ts.map +1 -1
- package/dist/server/request-context.js +40 -1
- package/dist/server/request-context.js.map +1 -1
- package/dist/server/sentry-plugin.d.ts +11 -0
- package/dist/server/sentry-plugin.d.ts.map +1 -0
- package/dist/server/sentry-plugin.js +116 -0
- package/dist/server/sentry-plugin.js.map +1 -0
- package/dist/server/sentry.d.ts +92 -0
- package/dist/server/sentry.d.ts.map +1 -0
- package/dist/server/sentry.js +287 -0
- package/dist/server/sentry.js.map +1 -0
- package/dist/server/transcribe-voice.d.ts +2 -4
- package/dist/server/transcribe-voice.d.ts.map +1 -1
- package/dist/server/transcribe-voice.js +4 -16
- package/dist/server/transcribe-voice.js.map +1 -1
- package/dist/server/voice-providers-status.d.ts.map +1 -1
- package/dist/server/voice-providers-status.js +19 -35
- package/dist/server/voice-providers-status.js.map +1 -1
- package/dist/styles/agent-native.css +15 -0
- package/docs/content/cloneable-saas.md +7 -9
- package/docs/content/deployment.md +6 -2
- package/docs/content/dispatch.md +1 -1
- package/docs/content/extensions.md +177 -142
- package/docs/content/faq.md +2 -2
- package/docs/content/getting-started.md +13 -11
- package/docs/content/multi-app-workspace.md +2 -2
- package/docs/content/observability.md +47 -0
- package/docs/content/pure-agent-apps.md +1 -1
- package/docs/content/template-clips.md +3 -3
- package/docs/content/template-design.md +3 -3
- package/docs/content/template-dispatch.md +1 -1
- package/docs/content/template-forms.md +1 -1
- package/docs/content/template-mail.md +1 -1
- package/docs/content/what-is-agent-native.md +4 -4
- package/docs/content/workspace.md +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core script: db-reset-dev-owner
|
|
3
|
+
*
|
|
4
|
+
* One-shot fix for local DBs that accumulated rows owned by the dev
|
|
5
|
+
* sentinel `local@localhost`. Pre-changes-53, db-exec / db-query /
|
|
6
|
+
* db-patch silently fell back to that owner when no real identity was
|
|
7
|
+
* present, so any data created via CLI runs (or by older versions of
|
|
8
|
+
* the runner) landed under the sentinel and is now invisible to the
|
|
9
|
+
* actual signed-in user.
|
|
10
|
+
*
|
|
11
|
+
* This script discovers every ownable table (those with an
|
|
12
|
+
* `owner_email` column), then re-points each `local@localhost` row to
|
|
13
|
+
* the email passed via `--to`. Optionally restricted to a single table
|
|
14
|
+
* with `--table`.
|
|
15
|
+
*
|
|
16
|
+
* Local-dev-only safety: refuses to run when `NODE_ENV=production` or
|
|
17
|
+
* when targeting a non-`file:` SQLite URL (no Postgres / Turso /
|
|
18
|
+
* shared-DB writes).
|
|
19
|
+
*
|
|
20
|
+
* Usage:
|
|
21
|
+
* pnpm action db-reset-dev-owner --to matthew@builder.io
|
|
22
|
+
* pnpm action db-reset-dev-owner --to matthew@builder.io --dry-run
|
|
23
|
+
* pnpm action db-reset-dev-owner --to matthew@builder.io --table decks
|
|
24
|
+
* pnpm action db-reset-dev-owner --to matthew@builder.io --db ./data/app.db
|
|
25
|
+
*/
|
|
26
|
+
import path from "path";
|
|
27
|
+
import { createClient } from "@libsql/client";
|
|
28
|
+
import { getDatabaseUrl, getDatabaseAuthToken } from "../../db/client.js";
|
|
29
|
+
import { parseArgs } from "../utils.js";
|
|
30
|
+
const DEV_FALLBACK_EMAIL = "local@localhost"; // guard:allow-localhost-fallback — script intentionally targets these rows
|
|
31
|
+
function isPostgresUrl(url) {
|
|
32
|
+
return url.startsWith("postgres://") || url.startsWith("postgresql://");
|
|
33
|
+
}
|
|
34
|
+
function parseScriptArgs(args) {
|
|
35
|
+
const parsed = parseArgs(args);
|
|
36
|
+
if (parsed.help === "true")
|
|
37
|
+
return null;
|
|
38
|
+
const to = parsed.to?.trim();
|
|
39
|
+
if (!to || !to.includes("@")) {
|
|
40
|
+
console.error("Error: --to <email> is required and must look like an email address.");
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
if (to === DEV_FALLBACK_EMAIL) {
|
|
44
|
+
console.error(`Error: --to cannot be ${DEV_FALLBACK_EMAIL} (that's the sentinel we're fixing).`);
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
to,
|
|
49
|
+
table: parsed.table?.trim() || undefined,
|
|
50
|
+
dryRun: parsed["dry-run"] === "true",
|
|
51
|
+
dbPath: parsed.db?.trim() || undefined,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function printHelp() {
|
|
55
|
+
console.log(`Usage: pnpm action db-reset-dev-owner --to <email> [options]
|
|
56
|
+
|
|
57
|
+
Reassigns rows owned by '${DEV_FALLBACK_EMAIL}' to the given email across
|
|
58
|
+
every table that has an 'owner_email' column. Use this once when an old
|
|
59
|
+
local DB still has rows that the new (post-changes-53) scoping won't show
|
|
60
|
+
to the actual signed-in user.
|
|
61
|
+
|
|
62
|
+
Required:
|
|
63
|
+
--to <email> Target email — usually the address you sign in with locally
|
|
64
|
+
|
|
65
|
+
Options:
|
|
66
|
+
--table <name> Only reset one table (default: every ownable table)
|
|
67
|
+
--dry-run Print what would change without writing
|
|
68
|
+
--db <path> SQLite database path (default: DATABASE_URL or ./data/app.db)
|
|
69
|
+
--help Show this help message
|
|
70
|
+
|
|
71
|
+
Refuses to run when NODE_ENV=production or against a non-local DB URL.`);
|
|
72
|
+
}
|
|
73
|
+
export default async function dbResetDevOwner(args) {
|
|
74
|
+
if (args.includes("--help") || args.length === 0) {
|
|
75
|
+
printHelp();
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const parsed = parseScriptArgs(args);
|
|
79
|
+
if (!parsed) {
|
|
80
|
+
// parseScriptArgs already printed the error; exit non-zero.
|
|
81
|
+
throw new Error("invalid arguments");
|
|
82
|
+
}
|
|
83
|
+
if (process.env.NODE_ENV === "production") {
|
|
84
|
+
console.error("Error: refusing to run db-reset-dev-owner with NODE_ENV=production.");
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
// Resolve target DB URL — same precedence as wipe-leaked-builder-keys.
|
|
88
|
+
let url;
|
|
89
|
+
if (parsed.dbPath) {
|
|
90
|
+
url = "file:" + path.resolve(parsed.dbPath);
|
|
91
|
+
}
|
|
92
|
+
else if (getDatabaseUrl()) {
|
|
93
|
+
url = getDatabaseUrl();
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
url = "file:" + path.resolve(process.cwd(), "data", "app.db");
|
|
97
|
+
}
|
|
98
|
+
const isPostgres = isPostgresUrl(url);
|
|
99
|
+
const isLocalSqlite = url.startsWith("file:");
|
|
100
|
+
if (!isPostgres && !isLocalSqlite) {
|
|
101
|
+
console.error(`Error: refusing to run against shared DB URL ${url}. ` +
|
|
102
|
+
"This script is only for local SQLite files.");
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
105
|
+
if (isPostgres && process.env.AN_ALLOW_PG_DEV_OWNER_RESET !== "1") {
|
|
106
|
+
console.error("Error: refusing to run against a Postgres DB. Set " +
|
|
107
|
+
"AN_ALLOW_PG_DEV_OWNER_RESET=1 to override (only do this on a " +
|
|
108
|
+
"local Postgres you fully own — never on Neon/prod).");
|
|
109
|
+
process.exit(1);
|
|
110
|
+
}
|
|
111
|
+
const dbLabel = isLocalSqlite
|
|
112
|
+
? url.slice("file:".length)
|
|
113
|
+
: (() => {
|
|
114
|
+
try {
|
|
115
|
+
return new URL(url).host || url;
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
return url;
|
|
119
|
+
}
|
|
120
|
+
})();
|
|
121
|
+
console.log(`[reset-dev-owner] target: ${dbLabel}` +
|
|
122
|
+
`${parsed.dryRun ? " (dry-run)" : ""}`);
|
|
123
|
+
console.log(`[reset-dev-owner] reassigning '${DEV_FALLBACK_EMAIL}' → '${parsed.to}'`);
|
|
124
|
+
if (isPostgres) {
|
|
125
|
+
await runPostgres(url, parsed);
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
await runSqlite(url, parsed);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
async function runSqlite(url, args) {
|
|
132
|
+
const client = createClient({ url, authToken: getDatabaseAuthToken() });
|
|
133
|
+
try {
|
|
134
|
+
const tables = args.table
|
|
135
|
+
? [args.table]
|
|
136
|
+
: await discoverSqliteOwnerTables(client);
|
|
137
|
+
if (tables.length === 0) {
|
|
138
|
+
console.log("[reset-dev-owner] no tables with owner_email column — nothing to do.");
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
let totalUpdated = 0;
|
|
142
|
+
for (const table of tables) {
|
|
143
|
+
const escaped = table.replace(/"/g, '""');
|
|
144
|
+
const countRes = await client.execute({
|
|
145
|
+
sql: `SELECT COUNT(*) AS c FROM "${escaped}" WHERE owner_email = ?`,
|
|
146
|
+
args: [DEV_FALLBACK_EMAIL],
|
|
147
|
+
});
|
|
148
|
+
const count = Number(countRes.rows[0]?.c ?? 0);
|
|
149
|
+
if (count === 0) {
|
|
150
|
+
console.log(` ${table}: 0 rows`);
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
console.log(` ${table}: ${count} row(s)${args.dryRun ? " (dry-run)" : ""}`);
|
|
154
|
+
if (args.dryRun)
|
|
155
|
+
continue;
|
|
156
|
+
const updateRes = await client.execute({
|
|
157
|
+
sql: `UPDATE "${escaped}" SET owner_email = ? WHERE owner_email = ?`,
|
|
158
|
+
args: [args.to, DEV_FALLBACK_EMAIL],
|
|
159
|
+
});
|
|
160
|
+
totalUpdated += updateRes.rowsAffected;
|
|
161
|
+
}
|
|
162
|
+
console.log(args.dryRun
|
|
163
|
+
? `[reset-dev-owner] dry-run complete.`
|
|
164
|
+
: `[reset-dev-owner] reassigned ${totalUpdated} row(s) across ${tables.length} table(s).`);
|
|
165
|
+
}
|
|
166
|
+
finally {
|
|
167
|
+
client.close();
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
async function runPostgres(url, args) {
|
|
171
|
+
const { default: pg } = await import("postgres");
|
|
172
|
+
const sql = pg(url);
|
|
173
|
+
try {
|
|
174
|
+
const tables = args.table
|
|
175
|
+
? [args.table]
|
|
176
|
+
: await discoverPostgresOwnerTables(sql);
|
|
177
|
+
if (tables.length === 0) {
|
|
178
|
+
console.log("[reset-dev-owner] no tables with owner_email column — nothing to do.");
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
let totalUpdated = 0;
|
|
182
|
+
for (const table of tables) {
|
|
183
|
+
const countRes = (await sql.unsafe(`SELECT COUNT(*)::int AS c FROM "${table.replace(/"/g, '""')}" WHERE owner_email = $1`, [DEV_FALLBACK_EMAIL]));
|
|
184
|
+
const count = countRes[0]?.c ?? 0;
|
|
185
|
+
if (count === 0) {
|
|
186
|
+
console.log(` ${table}: 0 rows`);
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
console.log(` ${table}: ${count} row(s)${args.dryRun ? " (dry-run)" : ""}`);
|
|
190
|
+
if (args.dryRun)
|
|
191
|
+
continue;
|
|
192
|
+
const updateRes = (await sql.unsafe(`UPDATE "${table.replace(/"/g, '""')}" SET owner_email = $1 WHERE owner_email = $2`, [args.to, DEV_FALLBACK_EMAIL]));
|
|
193
|
+
totalUpdated += updateRes.count ?? 0;
|
|
194
|
+
}
|
|
195
|
+
console.log(args.dryRun
|
|
196
|
+
? `[reset-dev-owner] dry-run complete.`
|
|
197
|
+
: `[reset-dev-owner] reassigned ${totalUpdated} row(s) across ${tables.length} table(s).`);
|
|
198
|
+
}
|
|
199
|
+
finally {
|
|
200
|
+
await sql.end();
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
async function discoverSqliteOwnerTables(client) {
|
|
204
|
+
const tablesRes = await client.execute(`SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'`);
|
|
205
|
+
const out = [];
|
|
206
|
+
for (const row of tablesRes.rows) {
|
|
207
|
+
const table = (row.name ?? row[0]);
|
|
208
|
+
const escaped = table.replace(/"/g, '""');
|
|
209
|
+
const colsRes = await client.execute(`PRAGMA table_info("${escaped}")`);
|
|
210
|
+
const hasOwner = colsRes.rows.some((r) => (r.name ?? r[1]) === "owner_email");
|
|
211
|
+
if (hasOwner)
|
|
212
|
+
out.push(table);
|
|
213
|
+
}
|
|
214
|
+
return out;
|
|
215
|
+
}
|
|
216
|
+
async function discoverPostgresOwnerTables(sql) {
|
|
217
|
+
const rows = (await sql `
|
|
218
|
+
SELECT table_name
|
|
219
|
+
FROM information_schema.columns
|
|
220
|
+
WHERE table_schema = 'public' AND column_name = 'owner_email'
|
|
221
|
+
ORDER BY table_name
|
|
222
|
+
`);
|
|
223
|
+
return Array.from(rows).map((r) => r.table_name);
|
|
224
|
+
}
|
|
225
|
+
//# sourceMappingURL=reset-dev-owner.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reset-dev-owner.js","sourceRoot":"","sources":["../../../src/scripts/db/reset-dev-owner.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Core script: db-reset-dev-owner\n *\n * One-shot fix for local DBs that accumulated rows owned by the dev\n * sentinel `local@localhost`. Pre-changes-53, db-exec / db-query /\n * db-patch silently fell back to that owner when no real identity was\n * present, so any data created via CLI runs (or by older versions of\n * the runner) landed under the sentinel and is now invisible to the\n * actual signed-in user.\n *\n * This script discovers every ownable table (those with an\n * `owner_email` column), then re-points each `local@localhost` row to\n * the email passed via `--to`. Optionally restricted to a single table\n * with `--table`.\n *\n * Local-dev-only safety: refuses to run when `NODE_ENV=production` or\n * when targeting a non-`file:` SQLite URL (no Postgres / Turso /\n * shared-DB writes).\n *\n * Usage:\n * pnpm action db-reset-dev-owner --to matthew@builder.io\n * pnpm action db-reset-dev-owner --to matthew@builder.io --dry-run\n * pnpm action db-reset-dev-owner --to matthew@builder.io --table decks\n * pnpm action db-reset-dev-owner --to matthew@builder.io --db ./data/app.db\n */\n\nimport path from \"path\";\nimport { createClient } from \"@libsql/client\";\nimport { getDatabaseUrl, getDatabaseAuthToken } from \"../../db/client.js\";\nimport { parseArgs } from \"../utils.js\";\n\nconst DEV_FALLBACK_EMAIL = \"local@localhost\"; // guard:allow-localhost-fallback — script intentionally targets these rows\n\nfunction isPostgresUrl(url: string): boolean {\n return url.startsWith(\"postgres://\") || url.startsWith(\"postgresql://\");\n}\n\ninterface Args {\n to: string;\n table?: string;\n dryRun: boolean;\n dbPath?: string;\n}\n\nfunction parseScriptArgs(args: string[]): Args | null {\n const parsed = parseArgs(args);\n if (parsed.help === \"true\") return null;\n\n const to = parsed.to?.trim();\n if (!to || !to.includes(\"@\")) {\n console.error(\n \"Error: --to <email> is required and must look like an email address.\",\n );\n return null;\n }\n if (to === DEV_FALLBACK_EMAIL) {\n console.error(\n `Error: --to cannot be ${DEV_FALLBACK_EMAIL} (that's the sentinel we're fixing).`,\n );\n return null;\n }\n\n return {\n to,\n table: parsed.table?.trim() || undefined,\n dryRun: parsed[\"dry-run\"] === \"true\",\n dbPath: parsed.db?.trim() || undefined,\n };\n}\n\nfunction printHelp(): void {\n console.log(`Usage: pnpm action db-reset-dev-owner --to <email> [options]\n\nReassigns rows owned by '${DEV_FALLBACK_EMAIL}' to the given email across\nevery table that has an 'owner_email' column. Use this once when an old\nlocal DB still has rows that the new (post-changes-53) scoping won't show\nto the actual signed-in user.\n\nRequired:\n --to <email> Target email — usually the address you sign in with locally\n\nOptions:\n --table <name> Only reset one table (default: every ownable table)\n --dry-run Print what would change without writing\n --db <path> SQLite database path (default: DATABASE_URL or ./data/app.db)\n --help Show this help message\n\nRefuses to run when NODE_ENV=production or against a non-local DB URL.`);\n}\n\nexport default async function dbResetDevOwner(args: string[]): Promise<void> {\n if (args.includes(\"--help\") || args.length === 0) {\n printHelp();\n return;\n }\n\n const parsed = parseScriptArgs(args);\n if (!parsed) {\n // parseScriptArgs already printed the error; exit non-zero.\n throw new Error(\"invalid arguments\");\n }\n\n if (process.env.NODE_ENV === \"production\") {\n console.error(\n \"Error: refusing to run db-reset-dev-owner with NODE_ENV=production.\",\n );\n process.exit(1);\n }\n\n // Resolve target DB URL — same precedence as wipe-leaked-builder-keys.\n let url: string;\n if (parsed.dbPath) {\n url = \"file:\" + path.resolve(parsed.dbPath);\n } else if (getDatabaseUrl()) {\n url = getDatabaseUrl();\n } else {\n url = \"file:\" + path.resolve(process.cwd(), \"data\", \"app.db\");\n }\n\n const isPostgres = isPostgresUrl(url);\n const isLocalSqlite = url.startsWith(\"file:\");\n\n if (!isPostgres && !isLocalSqlite) {\n console.error(\n `Error: refusing to run against shared DB URL ${url}. ` +\n \"This script is only for local SQLite files.\",\n );\n process.exit(1);\n }\n if (isPostgres && process.env.AN_ALLOW_PG_DEV_OWNER_RESET !== \"1\") {\n console.error(\n \"Error: refusing to run against a Postgres DB. Set \" +\n \"AN_ALLOW_PG_DEV_OWNER_RESET=1 to override (only do this on a \" +\n \"local Postgres you fully own — never on Neon/prod).\",\n );\n process.exit(1);\n }\n\n const dbLabel = isLocalSqlite\n ? url.slice(\"file:\".length)\n : (() => {\n try {\n return new URL(url).host || url;\n } catch {\n return url;\n }\n })();\n\n console.log(\n `[reset-dev-owner] target: ${dbLabel}` +\n `${parsed.dryRun ? \" (dry-run)\" : \"\"}`,\n );\n console.log(\n `[reset-dev-owner] reassigning '${DEV_FALLBACK_EMAIL}' → '${parsed.to}'`,\n );\n\n if (isPostgres) {\n await runPostgres(url, parsed);\n } else {\n await runSqlite(url, parsed);\n }\n}\n\nasync function runSqlite(url: string, args: Args): Promise<void> {\n const client = createClient({ url, authToken: getDatabaseAuthToken() });\n try {\n const tables = args.table\n ? [args.table]\n : await discoverSqliteOwnerTables(client);\n\n if (tables.length === 0) {\n console.log(\n \"[reset-dev-owner] no tables with owner_email column — nothing to do.\",\n );\n return;\n }\n\n let totalUpdated = 0;\n for (const table of tables) {\n const escaped = table.replace(/\"/g, '\"\"');\n const countRes = await client.execute({\n sql: `SELECT COUNT(*) AS c FROM \"${escaped}\" WHERE owner_email = ?`,\n args: [DEV_FALLBACK_EMAIL],\n });\n const count = Number((countRes.rows[0] as any)?.c ?? 0);\n if (count === 0) {\n console.log(` ${table}: 0 rows`);\n continue;\n }\n console.log(\n ` ${table}: ${count} row(s)${args.dryRun ? \" (dry-run)\" : \"\"}`,\n );\n if (args.dryRun) continue;\n const updateRes = await client.execute({\n sql: `UPDATE \"${escaped}\" SET owner_email = ? WHERE owner_email = ?`,\n args: [args.to, DEV_FALLBACK_EMAIL],\n });\n totalUpdated += updateRes.rowsAffected;\n }\n\n console.log(\n args.dryRun\n ? `[reset-dev-owner] dry-run complete.`\n : `[reset-dev-owner] reassigned ${totalUpdated} row(s) across ${tables.length} table(s).`,\n );\n } finally {\n client.close();\n }\n}\n\nasync function runPostgres(url: string, args: Args): Promise<void> {\n const { default: pg } = await import(\"postgres\");\n const sql = pg(url);\n try {\n const tables = args.table\n ? [args.table]\n : await discoverPostgresOwnerTables(sql);\n\n if (tables.length === 0) {\n console.log(\n \"[reset-dev-owner] no tables with owner_email column — nothing to do.\",\n );\n return;\n }\n\n let totalUpdated = 0;\n for (const table of tables) {\n const countRes = (await sql.unsafe(\n `SELECT COUNT(*)::int AS c FROM \"${table.replace(/\"/g, '\"\"')}\" WHERE owner_email = $1`,\n [DEV_FALLBACK_EMAIL],\n )) as unknown as Array<{ c: number }>;\n const count = countRes[0]?.c ?? 0;\n if (count === 0) {\n console.log(` ${table}: 0 rows`);\n continue;\n }\n console.log(\n ` ${table}: ${count} row(s)${args.dryRun ? \" (dry-run)\" : \"\"}`,\n );\n if (args.dryRun) continue;\n const updateRes = (await sql.unsafe(\n `UPDATE \"${table.replace(/\"/g, '\"\"')}\" SET owner_email = $1 WHERE owner_email = $2`,\n [args.to, DEV_FALLBACK_EMAIL],\n )) as unknown as { count?: number };\n totalUpdated += updateRes.count ?? 0;\n }\n\n console.log(\n args.dryRun\n ? `[reset-dev-owner] dry-run complete.`\n : `[reset-dev-owner] reassigned ${totalUpdated} row(s) across ${tables.length} table(s).`,\n );\n } finally {\n await sql.end();\n }\n}\n\nasync function discoverSqliteOwnerTables(client: any): Promise<string[]> {\n const tablesRes = await client.execute(\n `SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'`,\n );\n const out: string[] = [];\n for (const row of tablesRes.rows) {\n const table = (row.name ?? row[0]) as string;\n const escaped = table.replace(/\"/g, '\"\"');\n const colsRes = await client.execute(`PRAGMA table_info(\"${escaped}\")`);\n const hasOwner = colsRes.rows.some(\n (r: any) => (r.name ?? r[1]) === \"owner_email\",\n );\n if (hasOwner) out.push(table);\n }\n return out;\n}\n\nasync function discoverPostgresOwnerTables(sql: any): Promise<string[]> {\n const rows = (await sql`\n SELECT table_name\n FROM information_schema.columns\n WHERE table_schema = 'public' AND column_name = 'owner_email'\n ORDER BY table_name\n `) as unknown as Array<{ table_name: string }>;\n return Array.from(rows).map((r) => r.table_name);\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scoping.d.ts","sourceRoot":"","sources":["../../../src/scripts/db/scoping.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;
|
|
1
|
+
{"version":3,"file":"scoping.d.ts","sourceRoot":"","sources":["../../../src/scripts/db/scoping.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AA6LH,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;CAC1B;AAED;;;GAGG;AACH,wBAAsB,oBAAoB,CACxC,KAAK,EAAE,GAAG,GACT,OAAO,CAAC,cAAc,CAAC,CAmCzB;AAED;;;GAGG;AACH,wBAAsB,kBAAkB,CAAC,MAAM,EAAE,GAAG,GAAG,OAAO,CAAC,cAAc,CAAC,CA8B7E"}
|
|
@@ -28,8 +28,15 @@ const ORG_COLUMN = "org_id";
|
|
|
28
28
|
const DEV_FALLBACK_EMAIL = "local@localhost"; // guard:allow-localhost-fallback — sentinel is rejected below so DB scripts cannot silently scope to the dev fallback tenant
|
|
29
29
|
function getUserEmail() {
|
|
30
30
|
const userEmail = getRequestUserEmail() || null;
|
|
31
|
-
if (userEmail === DEV_FALLBACK_EMAIL) {
|
|
32
|
-
throw new Error("
|
|
31
|
+
if (!userEmail || userEmail === DEV_FALLBACK_EMAIL) {
|
|
32
|
+
throw new Error("db-exec / db-query / db-patch require an authenticated user identity. " +
|
|
33
|
+
"Easiest fix: open the app at http://localhost:3000 and sign in — " +
|
|
34
|
+
"the CLI then auto-loads your session. Otherwise set " +
|
|
35
|
+
"AGENT_USER_EMAIL=<email> in the env, or invoke through an HTTP " +
|
|
36
|
+
"action that runs under runWithRequestContext. Refusing to run unscoped — " +
|
|
37
|
+
"an unscoped UPDATE/DELETE would touch every user's rows, and an " +
|
|
38
|
+
"unscoped INSERT would land with the dev sentinel owner and be invisible " +
|
|
39
|
+
"to the UI.");
|
|
33
40
|
}
|
|
34
41
|
return userEmail;
|
|
35
42
|
}
|
|
@@ -149,21 +156,12 @@ function buildScopedTables(allColumns, userEmail, orgId, isPostgres) {
|
|
|
149
156
|
* Returns setup/teardown SQL to run before/after the user's query.
|
|
150
157
|
*/
|
|
151
158
|
export async function buildScopingPostgres(pgSql) {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
orgId: null,
|
|
158
|
-
ownerEmailTables: new Set(),
|
|
159
|
-
orgIdTables: new Set(),
|
|
160
|
-
};
|
|
161
|
-
// Scoping is always active when there is a request user (dev, preview, and
|
|
162
|
-
// prod). Previously this short-circuited outside production, which created
|
|
163
|
-
// a cross-user read in dev mode. See audit 05-tools-sandbox.md (C3.d).
|
|
159
|
+
// getUserEmail() throws when there is no authenticated user (no request
|
|
160
|
+
// context AND no AGENT_USER_EMAIL env) or when it resolves to the dev
|
|
161
|
+
// sentinel `local@localhost`. We let that throw propagate: the script
|
|
162
|
+
// refuses to run unscoped rather than silently writing rows that the UI
|
|
163
|
+
// then can't see, or running an UPDATE/DELETE across every user's data.
|
|
164
164
|
const userEmail = getUserEmail();
|
|
165
|
-
if (!userEmail)
|
|
166
|
-
return inactive;
|
|
167
165
|
const orgId = getOrgId();
|
|
168
166
|
const allColumns = await discoverColumnsPostgres(pgSql);
|
|
169
167
|
const scoped = buildScopedTables(allColumns, userEmail, orgId, true);
|
|
@@ -197,21 +195,8 @@ export async function buildScopingPostgres(pgSql) {
|
|
|
197
195
|
* Returns setup/teardown SQL to run before/after the user's query.
|
|
198
196
|
*/
|
|
199
197
|
export async function buildScopingSqlite(client) {
|
|
200
|
-
|
|
201
|
-
setup: [],
|
|
202
|
-
teardown: [],
|
|
203
|
-
active: false,
|
|
204
|
-
userEmail: null,
|
|
205
|
-
orgId: null,
|
|
206
|
-
ownerEmailTables: new Set(),
|
|
207
|
-
orgIdTables: new Set(),
|
|
208
|
-
};
|
|
209
|
-
// Scoping is always active when there is a request user (dev, preview, and
|
|
210
|
-
// prod). Previously this short-circuited outside production, which created
|
|
211
|
-
// a cross-user read in dev mode. See audit 05-tools-sandbox.md (C3.d).
|
|
198
|
+
// See buildScopingPostgres: getUserEmail() throws on no user / dev sentinel.
|
|
212
199
|
const userEmail = getUserEmail();
|
|
213
|
-
if (!userEmail)
|
|
214
|
-
return inactive;
|
|
215
200
|
const orgId = getOrgId();
|
|
216
201
|
const allColumns = await discoverColumnsSqlite(client);
|
|
217
202
|
const scoped = buildScopedTables(allColumns, userEmail, orgId, false);
|
|
@@ -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, both WHERE clauses are applied (AND).\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 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}\n\nfunction getUserEmail(): string | null {\n const userEmail = getRequestUserEmail() || null;\n if (userEmail === DEV_FALLBACK_EMAIL) {\n throw new Error(\n \"DB script scoping requires a real user identity; refusing to run with local@localhost.\",\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 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 for (const [table, columns] of columnsByTable) {\n // Check core table scoping\n const coreScoping = CORE_TABLE_SCOPING[table];\n if (coreScoping) {\n const realTable = `${qualifiedPrefix}\"${table}\"`;\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({\n name: table,\n viewSql: `${isPostgres ? \"CREATE OR REPLACE TEMPORARY\" : \"CREATE TEMPORARY\"} VIEW \"${table}\" AS SELECT * FROM ${realTable} WHERE ${whereSql}${checkOption}`,\n });\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 realTable = `${qualifiedPrefix}\"${table}\"`;\n const orgClause = safeOrgId\n ? ` OR (\"scope\" = 'org' AND \"${ORG_COLUMN}\" = '${safeOrgId}')`\n : \"\";\n scoped.push({\n name: table,\n viewSql: `${isPostgres ? \"CREATE OR REPLACE TEMPORARY\" : \"CREATE TEMPORARY\"} VIEW \"${table}\" AS SELECT * FROM ${realTable} WHERE ((\"scope\" = 'user' AND \"${OWNER_COLUMN}\" = '${safeEmail}')${orgClause})${checkOption}`,\n });\n continue;\n }\n\n // Build WHERE clauses for owner_email and org_id\n const clauses: string[] = [];\n const hasOwner = columns.includes(OWNER_COLUMN);\n const hasOrg = columns.includes(ORG_COLUMN);\n\n if (hasOwner) {\n clauses.push(`\"${OWNER_COLUMN}\" = '${safeEmail}'`);\n }\n if (hasOrg && safeOrgId) {\n clauses.push(`\"${ORG_COLUMN}\" = '${safeOrgId}'`);\n }\n\n if (clauses.length > 0) {\n const realTable = `${qualifiedPrefix}\"${table}\"`;\n scoped.push({\n name: table,\n viewSql: `${isPostgres ? \"CREATE OR REPLACE TEMPORARY\" : \"CREATE TEMPORARY\"} VIEW \"${table}\" AS SELECT * FROM ${realTable} WHERE ${clauses.join(\" AND \")}${checkOption}`,\n });\n }\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}\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 const inactive: ScopingContext = {\n setup: [],\n teardown: [],\n active: false,\n userEmail: null,\n orgId: null,\n ownerEmailTables: new Set(),\n orgIdTables: new Set(),\n };\n\n // Scoping is always active when there is a request user (dev, preview, and\n // prod). Previously this short-circuited outside production, which created\n // a cross-user read in dev mode. See audit 05-tools-sandbox.md (C3.d).\n const userEmail = getUserEmail();\n if (!userEmail) return inactive;\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((s) => `DROP VIEW IF EXISTS pg_temp.\"${s.name}\"`),\n active: scoped.length > 0,\n userEmail,\n orgId,\n ownerEmailTables,\n orgIdTables,\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 const inactive: ScopingContext = {\n setup: [],\n teardown: [],\n active: false,\n userEmail: null,\n orgId: null,\n ownerEmailTables: new Set(),\n orgIdTables: new Set(),\n };\n\n // Scoping is always active when there is a request user (dev, preview, and\n // prod). Previously this short-circuited outside production, which created\n // a cross-user read in dev mode. See audit 05-tools-sandbox.md (C3.d).\n const userEmail = getUserEmail();\n if (!userEmail) return inactive;\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((s) => `DROP VIEW IF EXISTS \"${s.name}\"`),\n active: scoped.length > 0,\n userEmail,\n orgId,\n ownerEmailTables,\n orgIdTables,\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, both WHERE clauses are applied (AND).\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 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}\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 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 for (const [table, columns] of columnsByTable) {\n // Check core table scoping\n const coreScoping = CORE_TABLE_SCOPING[table];\n if (coreScoping) {\n const realTable = `${qualifiedPrefix}\"${table}\"`;\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({\n name: table,\n viewSql: `${isPostgres ? \"CREATE OR REPLACE TEMPORARY\" : \"CREATE TEMPORARY\"} VIEW \"${table}\" AS SELECT * FROM ${realTable} WHERE ${whereSql}${checkOption}`,\n });\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 realTable = `${qualifiedPrefix}\"${table}\"`;\n const orgClause = safeOrgId\n ? ` OR (\"scope\" = 'org' AND \"${ORG_COLUMN}\" = '${safeOrgId}')`\n : \"\";\n scoped.push({\n name: table,\n viewSql: `${isPostgres ? \"CREATE OR REPLACE TEMPORARY\" : \"CREATE TEMPORARY\"} VIEW \"${table}\" AS SELECT * FROM ${realTable} WHERE ((\"scope\" = 'user' AND \"${OWNER_COLUMN}\" = '${safeEmail}')${orgClause})${checkOption}`,\n });\n continue;\n }\n\n // Build WHERE clauses for owner_email and org_id\n const clauses: string[] = [];\n const hasOwner = columns.includes(OWNER_COLUMN);\n const hasOrg = columns.includes(ORG_COLUMN);\n\n if (hasOwner) {\n clauses.push(`\"${OWNER_COLUMN}\" = '${safeEmail}'`);\n }\n if (hasOrg && safeOrgId) {\n clauses.push(`\"${ORG_COLUMN}\" = '${safeOrgId}'`);\n }\n\n if (clauses.length > 0) {\n const realTable = `${qualifiedPrefix}\"${table}\"`;\n scoped.push({\n name: table,\n viewSql: `${isPostgres ? \"CREATE OR REPLACE TEMPORARY\" : \"CREATE TEMPORARY\"} VIEW \"${table}\" AS SELECT * FROM ${realTable} WHERE ${clauses.join(\" AND \")}${checkOption}`,\n });\n }\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}\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((s) => `DROP VIEW IF EXISTS pg_temp.\"${s.name}\"`),\n active: scoped.length > 0,\n userEmail,\n orgId,\n ownerEmailTables,\n orgIdTables,\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((s) => `DROP VIEW IF EXISTS \"${s.name}\"`),\n active: scoped.length > 0,\n userEmail,\n orgId,\n ownerEmailTables,\n orgIdTables,\n };\n}\n"]}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dev-only session bootstrap for `pnpm action <name>` (and any other CLI
|
|
3
|
+
* caller of `runScript`).
|
|
4
|
+
*
|
|
5
|
+
* After changes-53, db-exec / db-query / db-patch refuse to run unless
|
|
6
|
+
* `getRequestUserEmail()` returns a real identity. In an HTTP request the
|
|
7
|
+
* Nitro plugin wraps the handler in `runWithRequestContext({ userEmail })`
|
|
8
|
+
* so scoping just works. CLI invocations have no such wrapper, so without
|
|
9
|
+
* this helper every db-* CLI run hands the user a stack trace.
|
|
10
|
+
*
|
|
11
|
+
* What this does: when the runner is about to dispatch, resolve a real
|
|
12
|
+
* email by reading the most-recent row from the legacy `sessions` table
|
|
13
|
+
* (the same table that `addSession()` writes from google-oauth.ts and the
|
|
14
|
+
* A2A receiver fallback already consults). The runner then wraps dispatch
|
|
15
|
+
* in `runWithRequestContext({ userEmail })` so the action sees a real
|
|
16
|
+
* identity.
|
|
17
|
+
*
|
|
18
|
+
* SHARED-DEV-BOX CAVEAT: the `SELECT email FROM sessions ORDER BY
|
|
19
|
+
* created_at DESC LIMIT 1` query is unscoped — on a machine where
|
|
20
|
+
* multiple developers have signed in (or after a `pnpm action …` run
|
|
21
|
+
* from another team's app), this will bind to whoever signed in most
|
|
22
|
+
* recently across *all* sessions in the DB. If that is wrong, set
|
|
23
|
+
* `AGENT_USER_EMAIL=<your-email>` in your shell or `.env`; explicit env
|
|
24
|
+
* always wins. A `[dev-session]` log line is emitted so wrong-binding
|
|
25
|
+
* is easy to spot.
|
|
26
|
+
*
|
|
27
|
+
* Strict gating mirrors the A2A precedent in
|
|
28
|
+
* `server/agent-chat-plugin.ts` (search for "latest session"):
|
|
29
|
+
* - NODE_ENV !== "production".
|
|
30
|
+
* - AUTH_MODE unset or === "local" — don't auto-impersonate when an
|
|
31
|
+
* admin or hosted auth mode is in use.
|
|
32
|
+
*
|
|
33
|
+
* If `process.env.AGENT_USER_EMAIL` is already set we return it unchanged
|
|
34
|
+
* — explicit env wins over any DB-derived guess (matches how
|
|
35
|
+
* `getRequestUserEmail()` itself behaves).
|
|
36
|
+
*/
|
|
37
|
+
/**
|
|
38
|
+
* Resolve the local dev user's email for the current CLI invocation.
|
|
39
|
+
*
|
|
40
|
+
* Returns the resolved email, or `undefined` when no real identity is
|
|
41
|
+
* available. Callers should let the downstream "no authenticated user"
|
|
42
|
+
* error propagate — its message points the user at the two fixes
|
|
43
|
+
* (sign in via the running app, or set `AGENT_USER_EMAIL`).
|
|
44
|
+
*/
|
|
45
|
+
export declare function resolveDevUserEmail(): Promise<string | undefined>;
|
|
46
|
+
//# sourceMappingURL=dev-session.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dev-session.d.ts","sourceRoot":"","sources":["../../src/scripts/dev-session.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AAIH;;;;;;;GAOG;AACH,wBAAsB,mBAAmB,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAkCvE"}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dev-only session bootstrap for `pnpm action <name>` (and any other CLI
|
|
3
|
+
* caller of `runScript`).
|
|
4
|
+
*
|
|
5
|
+
* After changes-53, db-exec / db-query / db-patch refuse to run unless
|
|
6
|
+
* `getRequestUserEmail()` returns a real identity. In an HTTP request the
|
|
7
|
+
* Nitro plugin wraps the handler in `runWithRequestContext({ userEmail })`
|
|
8
|
+
* so scoping just works. CLI invocations have no such wrapper, so without
|
|
9
|
+
* this helper every db-* CLI run hands the user a stack trace.
|
|
10
|
+
*
|
|
11
|
+
* What this does: when the runner is about to dispatch, resolve a real
|
|
12
|
+
* email by reading the most-recent row from the legacy `sessions` table
|
|
13
|
+
* (the same table that `addSession()` writes from google-oauth.ts and the
|
|
14
|
+
* A2A receiver fallback already consults). The runner then wraps dispatch
|
|
15
|
+
* in `runWithRequestContext({ userEmail })` so the action sees a real
|
|
16
|
+
* identity.
|
|
17
|
+
*
|
|
18
|
+
* SHARED-DEV-BOX CAVEAT: the `SELECT email FROM sessions ORDER BY
|
|
19
|
+
* created_at DESC LIMIT 1` query is unscoped — on a machine where
|
|
20
|
+
* multiple developers have signed in (or after a `pnpm action …` run
|
|
21
|
+
* from another team's app), this will bind to whoever signed in most
|
|
22
|
+
* recently across *all* sessions in the DB. If that is wrong, set
|
|
23
|
+
* `AGENT_USER_EMAIL=<your-email>` in your shell or `.env`; explicit env
|
|
24
|
+
* always wins. A `[dev-session]` log line is emitted so wrong-binding
|
|
25
|
+
* is easy to spot.
|
|
26
|
+
*
|
|
27
|
+
* Strict gating mirrors the A2A precedent in
|
|
28
|
+
* `server/agent-chat-plugin.ts` (search for "latest session"):
|
|
29
|
+
* - NODE_ENV !== "production".
|
|
30
|
+
* - AUTH_MODE unset or === "local" — don't auto-impersonate when an
|
|
31
|
+
* admin or hosted auth mode is in use.
|
|
32
|
+
*
|
|
33
|
+
* If `process.env.AGENT_USER_EMAIL` is already set we return it unchanged
|
|
34
|
+
* — explicit env wins over any DB-derived guess (matches how
|
|
35
|
+
* `getRequestUserEmail()` itself behaves).
|
|
36
|
+
*/
|
|
37
|
+
const DEV_FALLBACK_EMAIL = "local@localhost"; // guard:allow-localhost-fallback — sentinel intentionally rejected so the resolver doesn't return it
|
|
38
|
+
/**
|
|
39
|
+
* Resolve the local dev user's email for the current CLI invocation.
|
|
40
|
+
*
|
|
41
|
+
* Returns the resolved email, or `undefined` when no real identity is
|
|
42
|
+
* available. Callers should let the downstream "no authenticated user"
|
|
43
|
+
* error propagate — its message points the user at the two fixes
|
|
44
|
+
* (sign in via the running app, or set `AGENT_USER_EMAIL`).
|
|
45
|
+
*/
|
|
46
|
+
export async function resolveDevUserEmail() {
|
|
47
|
+
const explicit = process.env.AGENT_USER_EMAIL;
|
|
48
|
+
if (explicit)
|
|
49
|
+
return explicit;
|
|
50
|
+
// Hard refusal: this helper must never source identity in prod.
|
|
51
|
+
if (process.env.NODE_ENV === "production")
|
|
52
|
+
return undefined;
|
|
53
|
+
// AUTH_MODE may be unset (default dev shim) or "local". Anything else
|
|
54
|
+
// means a non-dev auth mode is in play; don't try to fish a session
|
|
55
|
+
// out of the DB on its behalf.
|
|
56
|
+
const authMode = process.env.AUTH_MODE;
|
|
57
|
+
if (authMode && authMode !== "local")
|
|
58
|
+
return undefined;
|
|
59
|
+
try {
|
|
60
|
+
const { getDbExec } = await import("../db/client.js");
|
|
61
|
+
const { rows } = await getDbExec().execute({
|
|
62
|
+
sql: `SELECT email FROM sessions
|
|
63
|
+
WHERE email IS NOT NULL AND email <> ?
|
|
64
|
+
ORDER BY created_at DESC LIMIT 1`,
|
|
65
|
+
args: [DEV_FALLBACK_EMAIL],
|
|
66
|
+
});
|
|
67
|
+
const email = rows[0]?.email;
|
|
68
|
+
if (!email || email.trim().length === 0)
|
|
69
|
+
return undefined;
|
|
70
|
+
console.log(`[dev-session] auto-bound to ${email} (set AGENT_USER_EMAIL to override)`);
|
|
71
|
+
return email;
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
// The sessions table doesn't exist yet (fresh install where the web
|
|
75
|
+
// server has never booted) or the DB isn't reachable. Either way,
|
|
76
|
+
// we can't produce an identity — let the caller throw with the
|
|
77
|
+
// friendlier "sign in first" hint.
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
//# sourceMappingURL=dev-session.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dev-session.js","sourceRoot":"","sources":["../../src/scripts/dev-session.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AAEH,MAAM,kBAAkB,GAAG,iBAAiB,CAAC,CAAC,qGAAqG;AAEnJ;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,mBAAmB;IACvC,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC;IAC9C,IAAI,QAAQ;QAAE,OAAO,QAAQ,CAAC;IAE9B,gEAAgE;IAChE,IAAI,OAAO,CAAC,GAAG,CAAC,QAAQ,KAAK,YAAY;QAAE,OAAO,SAAS,CAAC;IAE5D,sEAAsE;IACtE,oEAAoE;IACpE,+BAA+B;IAC/B,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC;IACvC,IAAI,QAAQ,IAAI,QAAQ,KAAK,OAAO;QAAE,OAAO,SAAS,CAAC;IAEvD,IAAI,CAAC;QACH,MAAM,EAAE,SAAS,EAAE,GAAG,MAAM,MAAM,CAAC,iBAAiB,CAAC,CAAC;QACtD,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,SAAS,EAAE,CAAC,OAAO,CAAC;YACzC,GAAG,EAAE;;6CAEkC;YACvC,IAAI,EAAE,CAAC,kBAAkB,CAAC;SAC3B,CAAC,CAAC;QACH,MAAM,KAAK,GAAG,IAAI,CAAC,CAAC,CAAC,EAAE,KAA2B,CAAC;QACnD,IAAI,CAAC,KAAK,IAAI,KAAK,CAAC,IAAI,EAAE,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,SAAS,CAAC;QAC1D,OAAO,CAAC,GAAG,CACT,+BAA+B,KAAK,qCAAqC,CAC1E,CAAC;QACF,OAAO,KAAK,CAAC;IACf,CAAC;IAAC,MAAM,CAAC;QACP,oEAAoE;QACpE,kEAAkE;QAClE,+DAA+D;QAC/D,mCAAmC;QACnC,OAAO,SAAS,CAAC;IACnB,CAAC;AACH,CAAC","sourcesContent":["/**\n * Dev-only session bootstrap for `pnpm action <name>` (and any other CLI\n * caller of `runScript`).\n *\n * After changes-53, db-exec / db-query / db-patch refuse to run unless\n * `getRequestUserEmail()` returns a real identity. In an HTTP request the\n * Nitro plugin wraps the handler in `runWithRequestContext({ userEmail })`\n * so scoping just works. CLI invocations have no such wrapper, so without\n * this helper every db-* CLI run hands the user a stack trace.\n *\n * What this does: when the runner is about to dispatch, resolve a real\n * email by reading the most-recent row from the legacy `sessions` table\n * (the same table that `addSession()` writes from google-oauth.ts and the\n * A2A receiver fallback already consults). The runner then wraps dispatch\n * in `runWithRequestContext({ userEmail })` so the action sees a real\n * identity.\n *\n * SHARED-DEV-BOX CAVEAT: the `SELECT email FROM sessions ORDER BY\n * created_at DESC LIMIT 1` query is unscoped — on a machine where\n * multiple developers have signed in (or after a `pnpm action …` run\n * from another team's app), this will bind to whoever signed in most\n * recently across *all* sessions in the DB. If that is wrong, set\n * `AGENT_USER_EMAIL=<your-email>` in your shell or `.env`; explicit env\n * always wins. A `[dev-session]` log line is emitted so wrong-binding\n * is easy to spot.\n *\n * Strict gating mirrors the A2A precedent in\n * `server/agent-chat-plugin.ts` (search for \"latest session\"):\n * - NODE_ENV !== \"production\".\n * - AUTH_MODE unset or === \"local\" — don't auto-impersonate when an\n * admin or hosted auth mode is in use.\n *\n * If `process.env.AGENT_USER_EMAIL` is already set we return it unchanged\n * — explicit env wins over any DB-derived guess (matches how\n * `getRequestUserEmail()` itself behaves).\n */\n\nconst DEV_FALLBACK_EMAIL = \"local@localhost\"; // guard:allow-localhost-fallback — sentinel intentionally rejected so the resolver doesn't return it\n\n/**\n * Resolve the local dev user's email for the current CLI invocation.\n *\n * Returns the resolved email, or `undefined` when no real identity is\n * available. Callers should let the downstream \"no authenticated user\"\n * error propagate — its message points the user at the two fixes\n * (sign in via the running app, or set `AGENT_USER_EMAIL`).\n */\nexport async function resolveDevUserEmail(): Promise<string | undefined> {\n const explicit = process.env.AGENT_USER_EMAIL;\n if (explicit) return explicit;\n\n // Hard refusal: this helper must never source identity in prod.\n if (process.env.NODE_ENV === \"production\") return undefined;\n\n // AUTH_MODE may be unset (default dev shim) or \"local\". Anything else\n // means a non-dev auth mode is in play; don't try to fish a session\n // out of the DB on its behalf.\n const authMode = process.env.AUTH_MODE;\n if (authMode && authMode !== \"local\") return undefined;\n\n try {\n const { getDbExec } = await import(\"../db/client.js\");\n const { rows } = await getDbExec().execute({\n sql: `SELECT email FROM sessions\n WHERE email IS NOT NULL AND email <> ?\n ORDER BY created_at DESC LIMIT 1`,\n args: [DEV_FALLBACK_EMAIL],\n });\n const email = rows[0]?.email as string | undefined;\n if (!email || email.trim().length === 0) return undefined;\n console.log(\n `[dev-session] auto-bound to ${email} (set AGENT_USER_EMAIL to override)`,\n );\n return email;\n } catch {\n // The sessions table doesn't exist yet (fresh install where the web\n // server has never booted) or the DB isn't reachable. Either way,\n // we can't produce an identity — let the caller throw with the\n // friendlier \"sign in first\" hint.\n return undefined;\n }\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../../src/scripts/runner.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;
|
|
1
|
+
{"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../../src/scripts/runner.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAyBH;;;;;GAKG;AACH,wBAAsB,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC,CAgE/C"}
|
package/dist/scripts/runner.js
CHANGED
|
@@ -15,6 +15,8 @@ import { pathToFileURL } from "url";
|
|
|
15
15
|
import { coreScripts, getCoreScriptNames } from "./core-scripts.js";
|
|
16
16
|
import { closeDbExec } from "../db/client.js";
|
|
17
17
|
import { loadEnv } from "./utils.js";
|
|
18
|
+
import { runWithRequestContext } from "../server/request-context.js";
|
|
19
|
+
import { resolveDevUserEmail } from "./dev-session.js";
|
|
18
20
|
// Load .env from cwd so DATABASE_URL and other vars are available to all actions.
|
|
19
21
|
loadEnv();
|
|
20
22
|
async function runAppDbPluginIfPresent() {
|
|
@@ -70,6 +72,25 @@ export async function runScript() {
|
|
|
70
72
|
process.exit(1);
|
|
71
73
|
}
|
|
72
74
|
const args = process.argv.slice(3);
|
|
75
|
+
// Establish a request context for the duration of this CLI run. Without
|
|
76
|
+
// it, db-exec / db-query / db-patch and any action that calls
|
|
77
|
+
// `getRequestUserEmail()` see no identity and refuse to run. The
|
|
78
|
+
// resolver picks up `AGENT_USER_EMAIL` if explicitly set, otherwise
|
|
79
|
+
// reads the most-recent signed-in session from the DB (dev-only,
|
|
80
|
+
// narrowly gated — see dev-session.ts).
|
|
81
|
+
//
|
|
82
|
+
// This wrap is intentionally a single point of injection: it covers
|
|
83
|
+
// both the local-action branch and the fall-through to core scripts
|
|
84
|
+
// (db-query, db-exec, …) so every CLI entrypoint runs scoped to a real
|
|
85
|
+
// user. It uses `runWithRequestContext` rather than mutating
|
|
86
|
+
// `process.env.AGENT_USER_EMAIL` because env mutation leaks across
|
|
87
|
+
// boundaries — see the cautionary comment in
|
|
88
|
+
// `server/request-context.ts` about exactly that pattern.
|
|
89
|
+
const userEmail = await resolveDevUserEmail();
|
|
90
|
+
const orgId = process.env.AGENT_ORG_ID || undefined;
|
|
91
|
+
return runWithRequestContext({ userEmail, orgId }, () => dispatchAction(actionName, args));
|
|
92
|
+
}
|
|
93
|
+
async function dispatchAction(actionName, args) {
|
|
73
94
|
// 1. Try local app action first (actions/ then scripts/ for backwards compat)
|
|
74
95
|
const actionsPath = path.resolve(process.cwd(), "actions", `${actionName}.ts`);
|
|
75
96
|
const scriptsPath = path.resolve(process.cwd(), "scripts", `${actionName}.ts`);
|