@agent-native/core 0.7.20 → 0.7.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (155) hide show
  1. package/dist/agent/engine/ai-sdk-engine.d.ts.map +1 -1
  2. package/dist/agent/engine/ai-sdk-engine.js +43 -1
  3. package/dist/agent/engine/ai-sdk-engine.js.map +1 -1
  4. package/dist/agent/engine/anthropic-engine.d.ts.map +1 -1
  5. package/dist/agent/engine/anthropic-engine.js +8 -0
  6. package/dist/agent/engine/anthropic-engine.js.map +1 -1
  7. package/dist/agent/engine/builder-engine.d.ts +1 -1
  8. package/dist/agent/engine/builder-engine.d.ts.map +1 -1
  9. package/dist/agent/engine/builder-engine.js +9 -4
  10. package/dist/agent/engine/builder-engine.js.map +1 -1
  11. package/dist/agent/engine/translate-ai-sdk.d.ts.map +1 -1
  12. package/dist/agent/engine/translate-ai-sdk.js +31 -1
  13. package/dist/agent/engine/translate-ai-sdk.js.map +1 -1
  14. package/dist/agent/engine/translate-anthropic.d.ts.map +1 -1
  15. package/dist/agent/engine/translate-anthropic.js +16 -0
  16. package/dist/agent/engine/translate-anthropic.js.map +1 -1
  17. package/dist/agent/engine/types.d.ts +16 -1
  18. package/dist/agent/engine/types.d.ts.map +1 -1
  19. package/dist/agent/engine/types.js.map +1 -1
  20. package/dist/agent/production-agent.d.ts +4 -0
  21. package/dist/agent/production-agent.d.ts.map +1 -1
  22. package/dist/agent/production-agent.js +96 -4
  23. package/dist/agent/production-agent.js.map +1 -1
  24. package/dist/agent/types.d.ts +3 -0
  25. package/dist/agent/types.d.ts.map +1 -1
  26. package/dist/agent/types.js.map +1 -1
  27. package/dist/client/AgentPanel.d.ts.map +1 -1
  28. package/dist/client/AgentPanel.js +5 -5
  29. package/dist/client/AgentPanel.js.map +1 -1
  30. package/dist/client/AssistantChat.d.ts +5 -0
  31. package/dist/client/AssistantChat.d.ts.map +1 -1
  32. package/dist/client/AssistantChat.js +54 -2
  33. package/dist/client/AssistantChat.js.map +1 -1
  34. package/dist/client/MultiTabAssistantChat.d.ts.map +1 -1
  35. package/dist/client/MultiTabAssistantChat.js +33 -2
  36. package/dist/client/MultiTabAssistantChat.js.map +1 -1
  37. package/dist/client/NewWorkspaceAppFlow.d.ts.map +1 -1
  38. package/dist/client/NewWorkspaceAppFlow.js +15 -8
  39. package/dist/client/NewWorkspaceAppFlow.js.map +1 -1
  40. package/dist/client/agent-chat-adapter.d.ts +4 -0
  41. package/dist/client/agent-chat-adapter.d.ts.map +1 -1
  42. package/dist/client/agent-chat-adapter.js +5 -1
  43. package/dist/client/agent-chat-adapter.js.map +1 -1
  44. package/dist/client/composer/TiptapComposer.d.ts +6 -1
  45. package/dist/client/composer/TiptapComposer.d.ts.map +1 -1
  46. package/dist/client/composer/TiptapComposer.js +25 -17
  47. package/dist/client/composer/TiptapComposer.js.map +1 -1
  48. package/dist/client/composer/useVoiceDictation.d.ts +6 -5
  49. package/dist/client/composer/useVoiceDictation.d.ts.map +1 -1
  50. package/dist/client/composer/useVoiceDictation.js +54 -21
  51. package/dist/client/composer/useVoiceDictation.js.map +1 -1
  52. package/dist/client/notifications/NotificationsBell.d.ts.map +1 -1
  53. package/dist/client/notifications/NotificationsBell.js +28 -1
  54. package/dist/client/notifications/NotificationsBell.js.map +1 -1
  55. package/dist/client/org/OrgSwitcher.d.ts +3 -1
  56. package/dist/client/org/OrgSwitcher.d.ts.map +1 -1
  57. package/dist/client/org/OrgSwitcher.js +12 -7
  58. package/dist/client/org/OrgSwitcher.js.map +1 -1
  59. package/dist/client/settings/AutomationsSection.d.ts.map +1 -1
  60. package/dist/client/settings/AutomationsSection.js +2 -2
  61. package/dist/client/settings/AutomationsSection.js.map +1 -1
  62. package/dist/client/settings/VoiceTranscriptionSection.d.ts.map +1 -1
  63. package/dist/client/settings/VoiceTranscriptionSection.js +46 -15
  64. package/dist/client/settings/VoiceTranscriptionSection.js.map +1 -1
  65. package/dist/client/tools/ToolViewer.d.ts.map +1 -1
  66. package/dist/client/tools/ToolViewer.js +2 -2
  67. package/dist/client/tools/ToolViewer.js.map +1 -1
  68. package/dist/client/tools/ToolsListPage.d.ts.map +1 -1
  69. package/dist/client/tools/ToolsListPage.js +4 -4
  70. package/dist/client/tools/ToolsListPage.js.map +1 -1
  71. package/dist/client/tools/ToolsSidebarSection.d.ts.map +1 -1
  72. package/dist/client/tools/ToolsSidebarSection.js +2 -2
  73. package/dist/client/tools/ToolsSidebarSection.js.map +1 -1
  74. package/dist/client/transcription/use-live-transcription.d.ts +1 -0
  75. package/dist/client/transcription/use-live-transcription.d.ts.map +1 -1
  76. package/dist/client/transcription/use-live-transcription.js +41 -0
  77. package/dist/client/transcription/use-live-transcription.js.map +1 -1
  78. package/dist/integrations/adapters/email.js +81 -5
  79. package/dist/integrations/adapters/email.js.map +1 -1
  80. package/dist/integrations/plugin.d.ts.map +1 -1
  81. package/dist/integrations/plugin.js +2 -1
  82. package/dist/integrations/plugin.js.map +1 -1
  83. package/dist/integrations/types.d.ts +2 -0
  84. package/dist/integrations/types.d.ts.map +1 -1
  85. package/dist/integrations/types.js.map +1 -1
  86. package/dist/integrations/webhook-handler.js +12 -2
  87. package/dist/integrations/webhook-handler.js.map +1 -1
  88. package/dist/oauth-tokens/store.d.ts.map +1 -1
  89. package/dist/oauth-tokens/store.js +34 -16
  90. package/dist/oauth-tokens/store.js.map +1 -1
  91. package/dist/scripts/db/exec.d.ts.map +1 -1
  92. package/dist/scripts/db/exec.js +32 -23
  93. package/dist/scripts/db/exec.js.map +1 -1
  94. package/dist/scripts/db/patch.d.ts.map +1 -1
  95. package/dist/scripts/db/patch.js +48 -35
  96. package/dist/scripts/db/patch.js.map +1 -1
  97. package/dist/scripts/db/query.d.ts.map +1 -1
  98. package/dist/scripts/db/query.js +22 -13
  99. package/dist/scripts/db/query.js.map +1 -1
  100. package/dist/scripts/db/safety.d.ts +2 -0
  101. package/dist/scripts/db/safety.d.ts.map +1 -0
  102. package/dist/scripts/db/safety.js +67 -0
  103. package/dist/scripts/db/safety.js.map +1 -0
  104. package/dist/scripts/db/scoping.js +4 -4
  105. package/dist/scripts/db/scoping.js.map +1 -1
  106. package/dist/server/email-template.d.ts +5 -0
  107. package/dist/server/email-template.d.ts.map +1 -1
  108. package/dist/server/email-template.js +7 -4
  109. package/dist/server/email-template.js.map +1 -1
  110. package/dist/server/google-auth-plugin.d.ts.map +1 -1
  111. package/dist/server/google-auth-plugin.js +1 -8
  112. package/dist/server/google-auth-plugin.js.map +1 -1
  113. package/dist/server/index.d.ts +3 -2
  114. package/dist/server/index.d.ts.map +1 -1
  115. package/dist/server/index.js +3 -2
  116. package/dist/server/index.js.map +1 -1
  117. package/dist/server/onboarding-html.d.ts.map +1 -1
  118. package/dist/server/onboarding-html.js +3 -10
  119. package/dist/server/onboarding-html.js.map +1 -1
  120. package/dist/server/ssr-handler.d.ts.map +1 -1
  121. package/dist/server/ssr-handler.js +7 -2
  122. package/dist/server/ssr-handler.js.map +1 -1
  123. package/dist/server/transcribe-voice.d.ts +9 -9
  124. package/dist/server/transcribe-voice.d.ts.map +1 -1
  125. package/dist/server/transcribe-voice.js +405 -51
  126. package/dist/server/transcribe-voice.js.map +1 -1
  127. package/dist/server/voice-providers-status.d.ts.map +1 -1
  128. package/dist/server/voice-providers-status.js +13 -1
  129. package/dist/server/voice-providers-status.js.map +1 -1
  130. package/dist/settings/store.d.ts.map +1 -1
  131. package/dist/settings/store.js +14 -6
  132. package/dist/settings/store.js.map +1 -1
  133. package/dist/shared/reasoning-effort.d.ts +8 -0
  134. package/dist/shared/reasoning-effort.d.ts.map +1 -0
  135. package/dist/shared/reasoning-effort.js +94 -0
  136. package/dist/shared/reasoning-effort.js.map +1 -0
  137. package/dist/templates/default/public/favicon.svg +1 -13
  138. package/dist/templates/default/public/icon-180.svg +1 -13
  139. package/dist/templates/default/public/icon-192.svg +1 -13
  140. package/dist/templates/default/public/icon-512.svg +1 -13
  141. package/dist/templates/workspace-root/scripts/workspace-dev.ts +5 -38
  142. package/dist/transcription/builder-transcription.d.ts +2 -0
  143. package/dist/transcription/builder-transcription.d.ts.map +1 -1
  144. package/dist/transcription/builder-transcription.js +4 -0
  145. package/dist/transcription/builder-transcription.js.map +1 -1
  146. package/dist/vite/client.d.ts.map +1 -1
  147. package/dist/vite/client.js +1 -5
  148. package/dist/vite/client.js.map +1 -1
  149. package/docs/content/voice-input.md +14 -13
  150. package/package.json +1 -1
  151. package/src/templates/default/public/favicon.svg +1 -13
  152. package/src/templates/default/public/icon-180.svg +1 -13
  153. package/src/templates/default/public/icon-192.svg +1 -13
  154. package/src/templates/default/public/icon-512.svg +1 -13
  155. package/src/templates/workspace-root/scripts/workspace-dev.ts +5 -38
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/integrations/types.ts"],"names":[],"mappings":"","sourcesContent":["import type { H3Event } from \"h3\";\nimport type { EnvKeyConfig } from \"../server/create-server.js\";\n\n/**\n * Normalized incoming message from any messaging platform.\n */\nexport interface IncomingMessage {\n /** Platform identifier (e.g., \"slack\", \"telegram\", \"whatsapp\") */\n platform: string;\n /** Platform-specific thread/conversation identifier */\n externalThreadId: string;\n /** Message text content */\n text: string;\n /** Display name of the sender */\n senderName?: string;\n /** Platform-specific sender ID */\n senderId?: string;\n /** Raw platform-specific context needed for routing responses */\n platformContext: Record<string, unknown>;\n /** Message timestamp (epoch ms) */\n timestamp: number;\n}\n\n/**\n * Outgoing message to send back to a messaging platform.\n */\nexport interface OutgoingMessage {\n /** Text content of the response */\n text: string;\n /** Platform-specific payload (e.g., Slack blocks, Telegram parse_mode) */\n platformContext: Record<string, unknown>;\n}\n\n/**\n * Proactive outbound message target for a platform.\n * Used when the agent needs to send to a saved destination instead of replying\n * to the current inbound thread.\n */\nexport interface OutboundTarget {\n /** Canonical platform-specific destination id (channel, chat, thread, etc.) */\n destination: string;\n /** Optional thread reference when the destination supports threading */\n threadRef?: string | null;\n /** Optional fallback display label */\n label?: string;\n}\n\n/**\n * Connection status for a platform integration.\n */\nexport interface IntegrationStatus {\n platform: string;\n /** Human-readable label (e.g., \"Slack\", \"Telegram\") */\n label: string;\n /** Whether the integration is explicitly enabled */\n enabled: boolean;\n /** Whether all required credentials are configured */\n configured: boolean;\n /** Platform-specific details (workspace name, bot username, etc.) */\n details?: Record<string, unknown>;\n /** Error message if something is wrong */\n error?: string;\n /** The webhook URL that should be configured in the platform */\n webhookUrl?: string;\n /** The full list of env keys (required + optional) the adapter recognizes,\n * including UI hints. Surfaced on the integrations status endpoint so the\n * frontend can render fields without hard-coding them per platform. */\n requiredEnvKeys?: import(\"../server/create-server.js\").EnvKeyConfig[];\n}\n\n/**\n * Platform adapter interface — implement this for each messaging platform.\n *\n * Each adapter handles the platform-specific concerns:\n * - Webhook verification (HMAC signatures, challenge responses)\n * - Message parsing (platform events → normalized IncomingMessage)\n * - Response formatting (agent text → platform-specific format)\n * - Response delivery (POST back to platform API)\n */\nexport interface PlatformAdapter {\n /** Unique platform identifier */\n readonly platform: string;\n /** Human-readable label */\n readonly label: string;\n\n /** Env keys this adapter needs (tokens, secrets, etc.) */\n getRequiredEnvKeys(): EnvKeyConfig[];\n\n /**\n * Handle platform-specific verification challenges.\n * For example, Slack sends a `url_verification` event when setting up.\n * Return `{ handled: true, response }` to short-circuit the webhook handler.\n */\n handleVerification(event: H3Event): Promise<{\n handled: boolean;\n response?: unknown;\n }>;\n\n /**\n * Validate the webhook request signature.\n * Returns true if the request is authentic.\n */\n verifyWebhook(event: H3Event): Promise<boolean>;\n\n /**\n * Parse the webhook payload into a normalized IncomingMessage.\n * Return null to silently ignore the event (bot messages, edits, etc.).\n */\n parseIncomingMessage(event: H3Event): Promise<IncomingMessage | null>;\n\n /**\n * Send the agent's response back to the messaging platform.\n *\n * If `opts.placeholderRef` is provided (returned earlier by\n * `postProcessingPlaceholder`), adapters that support in-place edits should\n * update that placeholder message rather than posting a new one. Adapters\n * without an \"update message\" API can ignore the ref and post fresh.\n */\n sendResponse(\n message: OutgoingMessage,\n context: IncomingMessage,\n opts?: { placeholderRef?: string },\n ): Promise<void>;\n\n /**\n * Optionally post a \"working on it…\" placeholder message immediately when a\n * webhook arrives, before the agent loop runs. Adapters that support\n * in-place message edits (Slack via `chat.update`, etc.) return an opaque\n * `placeholderRef` that the webhook flow threads through to `sendResponse`\n * so the same message is updated with the final answer once ready.\n *\n * Adapters without edit support should leave this undefined; the webhook\n * handler will skip the placeholder step entirely.\n */\n postProcessingPlaceholder?(\n incoming: IncomingMessage,\n ): Promise<{ placeholderRef: string } | null>;\n\n /**\n * Send a proactive outbound message to a platform destination. Adapters that\n * only support direct replies can omit this.\n */\n sendMessageToTarget?(\n message: OutgoingMessage,\n target: OutboundTarget,\n ): Promise<void>;\n\n /**\n * Format plain agent response text into a platform-appropriate message.\n * Handles markdown conversion, message splitting for length limits, etc.\n *\n * `opts.threadDeepLinkUrl`, when present, is a URL back to the originating\n * thread in the dispatch UI. Adapters that support rich blocks should\n * render this as a button (Slack); adapters that don't may inline it as a\n * link or simply omit it.\n */\n formatAgentResponse(\n text: string,\n opts?: { threadDeepLinkUrl?: string },\n ): OutgoingMessage;\n\n /** Return current connection/configuration status for the settings UI. */\n getStatus(baseUrl?: string): Promise<IntegrationStatus>;\n}\n\n/**\n * Options for the integrations plugin.\n */\nexport interface IntegrationsPluginOptions {\n /** App identifier used by call-agent to prevent self-calls (e.g. \"dispatch\"). */\n appId?: string;\n /** Platform adapters to enable. Default: all built-in adapters with configured env keys. */\n adapters?: PlatformAdapter[];\n /** System prompt for the agent (same as agent-chat). Inherited from agent-chat plugin if not set. */\n systemPrompt?: string;\n /** Actions registry (same as agent-chat). */\n actions?: Record<string, import(\"../agent/production-agent.js\").ActionEntry>;\n /** Model to use. Default: claude-sonnet-4-6 */\n model?: string;\n /** Anthropic API key. Falls back to ANTHROPIC_API_KEY env var. */\n apiKey?: string;\n /**\n * Resolve which owner should receive personal resource context and own the\n * created chat thread for an incoming platform message.\n */\n resolveOwner?: (incoming: IncomingMessage) => string | Promise<string>;\n /**\n * Optional preprocessor for inbound platform messages. Can intercept special\n * commands (such as `/link`) before the agent loop runs.\n */\n beforeProcess?: (\n incoming: IncomingMessage,\n adapter: PlatformAdapter,\n ) => Promise<\n | {\n handled: true;\n responseText?: string;\n }\n | { handled: false }\n >;\n}\n"]}
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/integrations/types.ts"],"names":[],"mappings":"","sourcesContent":["import type { H3Event } from \"h3\";\nimport type { EnvKeyConfig } from \"../server/create-server.js\";\n\n/**\n * Normalized incoming message from any messaging platform.\n */\nexport interface IncomingMessage {\n /** Platform identifier (e.g., \"slack\", \"telegram\", \"whatsapp\") */\n platform: string;\n /** Platform-specific thread/conversation identifier */\n externalThreadId: string;\n /** Message text content */\n text: string;\n /** Display name of the sender */\n senderName?: string;\n /** Verified sender email, when the platform can provide one */\n senderEmail?: string;\n /** Platform-specific sender ID */\n senderId?: string;\n /** Raw platform-specific context needed for routing responses */\n platformContext: Record<string, unknown>;\n /** Message timestamp (epoch ms) */\n timestamp: number;\n}\n\n/**\n * Outgoing message to send back to a messaging platform.\n */\nexport interface OutgoingMessage {\n /** Text content of the response */\n text: string;\n /** Platform-specific payload (e.g., Slack blocks, Telegram parse_mode) */\n platformContext: Record<string, unknown>;\n}\n\n/**\n * Proactive outbound message target for a platform.\n * Used when the agent needs to send to a saved destination instead of replying\n * to the current inbound thread.\n */\nexport interface OutboundTarget {\n /** Canonical platform-specific destination id (channel, chat, thread, etc.) */\n destination: string;\n /** Optional thread reference when the destination supports threading */\n threadRef?: string | null;\n /** Optional fallback display label */\n label?: string;\n}\n\n/**\n * Connection status for a platform integration.\n */\nexport interface IntegrationStatus {\n platform: string;\n /** Human-readable label (e.g., \"Slack\", \"Telegram\") */\n label: string;\n /** Whether the integration is explicitly enabled */\n enabled: boolean;\n /** Whether all required credentials are configured */\n configured: boolean;\n /** Platform-specific details (workspace name, bot username, etc.) */\n details?: Record<string, unknown>;\n /** Error message if something is wrong */\n error?: string;\n /** The webhook URL that should be configured in the platform */\n webhookUrl?: string;\n /** The full list of env keys (required + optional) the adapter recognizes,\n * including UI hints. Surfaced on the integrations status endpoint so the\n * frontend can render fields without hard-coding them per platform. */\n requiredEnvKeys?: import(\"../server/create-server.js\").EnvKeyConfig[];\n}\n\n/**\n * Platform adapter interface — implement this for each messaging platform.\n *\n * Each adapter handles the platform-specific concerns:\n * - Webhook verification (HMAC signatures, challenge responses)\n * - Message parsing (platform events → normalized IncomingMessage)\n * - Response formatting (agent text → platform-specific format)\n * - Response delivery (POST back to platform API)\n */\nexport interface PlatformAdapter {\n /** Unique platform identifier */\n readonly platform: string;\n /** Human-readable label */\n readonly label: string;\n\n /** Env keys this adapter needs (tokens, secrets, etc.) */\n getRequiredEnvKeys(): EnvKeyConfig[];\n\n /**\n * Handle platform-specific verification challenges.\n * For example, Slack sends a `url_verification` event when setting up.\n * Return `{ handled: true, response }` to short-circuit the webhook handler.\n */\n handleVerification(event: H3Event): Promise<{\n handled: boolean;\n response?: unknown;\n }>;\n\n /**\n * Validate the webhook request signature.\n * Returns true if the request is authentic.\n */\n verifyWebhook(event: H3Event): Promise<boolean>;\n\n /**\n * Parse the webhook payload into a normalized IncomingMessage.\n * Return null to silently ignore the event (bot messages, edits, etc.).\n */\n parseIncomingMessage(event: H3Event): Promise<IncomingMessage | null>;\n\n /**\n * Send the agent's response back to the messaging platform.\n *\n * If `opts.placeholderRef` is provided (returned earlier by\n * `postProcessingPlaceholder`), adapters that support in-place edits should\n * update that placeholder message rather than posting a new one. Adapters\n * without an \"update message\" API can ignore the ref and post fresh.\n */\n sendResponse(\n message: OutgoingMessage,\n context: IncomingMessage,\n opts?: { placeholderRef?: string },\n ): Promise<void>;\n\n /**\n * Optionally post a \"working on it…\" placeholder message immediately when a\n * webhook arrives, before the agent loop runs. Adapters that support\n * in-place message edits (Slack via `chat.update`, etc.) return an opaque\n * `placeholderRef` that the webhook flow threads through to `sendResponse`\n * so the same message is updated with the final answer once ready.\n *\n * Adapters without edit support should leave this undefined; the webhook\n * handler will skip the placeholder step entirely.\n */\n postProcessingPlaceholder?(\n incoming: IncomingMessage,\n ): Promise<{ placeholderRef: string } | null>;\n\n /**\n * Send a proactive outbound message to a platform destination. Adapters that\n * only support direct replies can omit this.\n */\n sendMessageToTarget?(\n message: OutgoingMessage,\n target: OutboundTarget,\n ): Promise<void>;\n\n /**\n * Format plain agent response text into a platform-appropriate message.\n * Handles markdown conversion, message splitting for length limits, etc.\n *\n * `opts.threadDeepLinkUrl`, when present, is a URL back to the originating\n * thread in the dispatch UI. Adapters that support rich blocks should\n * render this as a button (Slack); adapters that don't may inline it as a\n * link or simply omit it.\n */\n formatAgentResponse(\n text: string,\n opts?: { threadDeepLinkUrl?: string },\n ): OutgoingMessage;\n\n /** Return current connection/configuration status for the settings UI. */\n getStatus(baseUrl?: string): Promise<IntegrationStatus>;\n}\n\n/**\n * Options for the integrations plugin.\n */\nexport interface IntegrationsPluginOptions {\n /** App identifier used by call-agent to prevent self-calls (e.g. \"dispatch\"). */\n appId?: string;\n /** Platform adapters to enable. Default: all built-in adapters with configured env keys. */\n adapters?: PlatformAdapter[];\n /** System prompt for the agent (same as agent-chat). Inherited from agent-chat plugin if not set. */\n systemPrompt?: string;\n /** Actions registry (same as agent-chat). */\n actions?: Record<string, import(\"../agent/production-agent.js\").ActionEntry>;\n /** Model to use. Default: claude-sonnet-4-6 */\n model?: string;\n /** Anthropic API key. Falls back to ANTHROPIC_API_KEY env var. */\n apiKey?: string;\n /**\n * Resolve which owner should receive personal resource context and own the\n * created chat thread for an incoming platform message.\n */\n resolveOwner?: (incoming: IncomingMessage) => string | Promise<string>;\n /**\n * Optional preprocessor for inbound platform messages. Can intercept special\n * commands (such as `/link`) before the agent loop runs.\n */\n beforeProcess?: (\n incoming: IncomingMessage,\n adapter: PlatformAdapter,\n ) => Promise<\n | {\n handled: true;\n responseText?: string;\n }\n | { handled: false }\n >;\n}\n"]}
@@ -297,10 +297,20 @@ async function processIncomingMessage(incoming, options, opts = {}) {
297
297
  }
298
298
  catch { }
299
299
  }
300
- // Add the new user message
300
+ // Add the new user message. Include verified platform identity as lightweight
301
+ // context so app-specific agents can attribute requests without guessing.
302
+ const identityLines = [
303
+ `Platform: ${incoming.platform}`,
304
+ incoming.senderName ? `Sender name: ${incoming.senderName}` : null,
305
+ incoming.senderEmail ? `Sender email: ${incoming.senderEmail}` : null,
306
+ incoming.senderId ? `Sender ID: ${incoming.senderId}` : null,
307
+ ].filter(Boolean);
308
+ const userText = identityLines.length > 1
309
+ ? `<integration-context>\n${identityLines.join("\n")}\n</integration-context>\n\n${incoming.text}`
310
+ : incoming.text;
301
311
  const messages = [
302
312
  ...existingMessages,
303
- { role: "user", content: [{ type: "text", text: incoming.text }] },
313
+ { role: "user", content: [{ type: "text", text: userText }] },
304
314
  ];
305
315
  // Run agent loop via startRun, wrapped in a request context so that
306
316
  // tools (especially call-agent) can resolve the caller's org for org-scoped
@@ -1 +1 @@
1
- {"version":3,"file":"webhook-handler.js","sourceRoot":"","sources":["../../src/integrations/webhook-handler.ts"],"names":[],"mappings":"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","sourcesContent":["import type { H3Event } from \"h3\";\nimport type { PlatformAdapter, IncomingMessage } from \"./types.js\";\nimport { getThreadMapping, saveThreadMapping } from \"./thread-mapping-store.js\";\nimport { createThread, getThread } from \"../chat-threads/store.js\";\nimport {\n runAgentLoop,\n actionsToEngineTools,\n type ActionEntry,\n} from \"../agent/production-agent.js\";\nimport { createAnthropicEngine } from \"../agent/engine/index.js\";\nimport type { EngineMessage } from \"../agent/engine/types.js\";\nimport { startRun, type ActiveRun } from \"../agent/run-manager.js\";\nimport {\n buildAssistantMessage,\n extractThreadMeta,\n} from \"../agent/thread-data-builder.js\";\nimport { updateThreadData } from \"../chat-threads/store.js\";\nimport { runWithRequestContext } from \"../server/request-context.js\";\nimport { resolveOrgIdForEmail } from \"../org/context.js\";\nimport {\n insertPendingTask,\n isDuplicateEventError,\n type PendingTask,\n} from \"./pending-tasks-store.js\";\nimport { signInternalToken } from \"./internal-token.js\";\nimport { FRAMEWORK_ROUTE_PREFIX } from \"../server/core-routes-plugin.js\";\nimport { withConfiguredAppBasePath } from \"../server/app-base-path.js\";\n\n/**\n * Build a stable per-event dedup key from the incoming message. The same\n * key is computed for every retry of the same event from the platform —\n * Slack/Telegram retry on timeout (3s for Slack), so we MUST treat the\n * second delivery as a duplicate and return 200 silently.\n *\n * The `(platform, external_event_key)` UNIQUE index in\n * `integration_pending_tasks` enforces this at the SQL layer, replacing\n * the previous in-memory Map (H3 in the webhook security audit) which\n * couldn't survive serverless cold starts.\n */\nfunction buildEventDedupKey(incoming: IncomingMessage): string {\n return `${incoming.platform}:${incoming.externalThreadId}:${incoming.timestamp}`;\n}\n\nexport interface WebhookHandlerOptions {\n adapter: PlatformAdapter;\n /** Resolved system prompt string */\n systemPrompt: string;\n /** Action entries for the agent */\n actions: Record<string, ActionEntry>;\n /** Model to use */\n model: string;\n /** Anthropic API key */\n apiKey: string;\n /** Thread owner for personal/shared resource loading */\n ownerEmail: string;\n /**\n * Pre-parsed incoming message. When provided, handleWebhook skips its own\n * verification + parsing steps. Required when the caller has already read\n * the request body (h3 doesn't reliably cache parsed bodies, so re-parsing\n * the same event hangs on streaming providers).\n */\n incoming?: IncomingMessage;\n /** Optional hook to intercept inbound commands before agent execution */\n beforeProcess?: (\n incoming: IncomingMessage,\n adapter: PlatformAdapter,\n ) => Promise<\n | {\n handled: true;\n responseText?: string;\n }\n | { handled: false }\n >;\n}\n\n/**\n * Process an incoming webhook from a messaging platform.\n *\n * Flow:\n * 1. Handle verification challenges (Slack url_verification, etc.)\n * 2. Verify webhook signature\n * 3. Parse incoming message (null = ignored event)\n * 4. Persist task to SQL\n * 5. Fire-and-forget POST to /_agent-native/integrations/process-task\n * (a fresh function execution with its own timeout budget)\n * 6. Return HTTP 200 immediately (within Slack's 3s SLA)\n *\n * The processor endpoint runs the actual agent loop. This split is essential\n * for serverless platforms (Netlify Lambda, Vercel, Cloudflare Workers) which\n * freeze the function as soon as the response is returned, killing any\n * lingering background promises.\n */\nexport async function handleWebhook(\n event: H3Event,\n options: WebhookHandlerOptions,\n): Promise<{ status: number; body: unknown }> {\n const { adapter, beforeProcess } = options;\n\n let incoming: IncomingMessage | null = options.incoming ?? null;\n\n // When the caller didn't pre-parse, run the full verify + parse pipeline.\n // Otherwise skip it — h3's body stream has already been consumed and a\n // second readBody call hangs on streaming providers.\n if (!incoming) {\n // Step 1: Handle platform-specific verification challenges\n const verification = await adapter.handleVerification(event);\n if (verification.handled) {\n return { status: 200, body: verification.response ?? \"ok\" };\n }\n\n // Step 2: Verify webhook signature\n const isValid = await adapter.verifyWebhook(event);\n if (!isValid) {\n return { status: 401, body: { error: \"Invalid webhook signature\" } };\n }\n\n // Step 3: Parse the incoming message\n incoming = await adapter.parseIncomingMessage(event);\n if (!incoming) {\n // Not a user message (bot message, edit, reaction, etc.) — acknowledge silently\n return { status: 200, body: \"ok\" };\n }\n }\n\n // Dedup is enforced inside enqueueAndDispatch — the unique index on\n // `(platform, external_event_key)` raises a constraint violation we treat\n // as \"already enqueued\" and respond 200. We can't dedup BEFORE the\n // beforeProcess hook because some templates use beforeProcess for\n // command-style intercepts that are stateless and idempotent (e.g. a\n // Slack `/help` command that doesn't enqueue a task).\n\n if (beforeProcess) {\n const result = await beforeProcess(incoming, adapter);\n if (result.handled) {\n if (result.responseText?.trim()) {\n const outgoing = adapter.formatAgentResponse(result.responseText);\n await adapter.sendResponse(outgoing, incoming);\n }\n return { status: 200, body: \"ok\" };\n }\n }\n\n // Step 4 + 5: Enqueue to SQL and dispatch to processor in a fresh request.\n try {\n await enqueueAndDispatch(event, incoming, options);\n } catch (err) {\n // Duplicate event delivery: the SQL UNIQUE constraint on\n // (platform, external_event_key) rejected the second insert. This is\n // the expected path when a platform retries an event that already\n // landed (e.g. Slack 3-second timeout) — return 200 so the platform\n // stops retrying. See H3 in the webhook security audit.\n if (isDuplicateEventError(err)) {\n return { status: 200, body: \"ok\" };\n }\n console.error(\n `[integrations] Failed to enqueue/dispatch ${incoming.platform} message:`,\n err,\n );\n // Return 500 so the platform retries. If the SQL insert failed for a\n // non-dup reason, the message is genuinely lost — better to let Slack\n // retry (it will re-fire the same event_callback) than silently drop it.\n return { status: 500, body: { error: \"enqueue failed\" } };\n }\n\n return { status: 200, body: \"ok\" };\n}\n\n/**\n * Persist the task to SQL and dispatch a fresh HTTP request to the processor\n * endpoint. The dispatch is fire-and-forget — we deliberately do NOT await\n * the resulting fetch, so the current handler can return immediately.\n *\n * This pattern works on every supported host:\n * - Netlify Lambda: function returns; the dispatched request hits a fresh\n * Lambda with its own 26s budget.\n * - Vercel Functions: same.\n * - Cloudflare Workers: same (no waitUntil dependency).\n * - Self-hosted Node: a separate request comes back through the same\n * server, but each handler still runs to completion.\n */\nasync function enqueueAndDispatch(\n event: H3Event,\n incoming: IncomingMessage,\n options: WebhookHandlerOptions,\n): Promise<void> {\n const taskId = `task-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;\n\n // Resolve the org id once at enqueue-time so the processor doesn't have to\n // re-derive it (and so we can drop it on the row for observability).\n let orgId: string | null = null;\n try {\n orgId = (await resolveOrgIdForEmail(options.ownerEmail)) ?? null;\n } catch {\n orgId = null;\n }\n\n // Post a \"thinking…\" placeholder immediately if the adapter supports\n // in-place edits. The processor flow will update this same message with\n // the final answer, so users see one tidy thread reply instead of\n // \"[silence] → answer\". Adapters without edit support skip this and the\n // processor posts a fresh response.\n let placeholderRef: string | undefined;\n try {\n if (options.adapter.postProcessingPlaceholder) {\n const placeholder =\n await options.adapter.postProcessingPlaceholder(incoming);\n if (placeholder?.placeholderRef) {\n placeholderRef = placeholder.placeholderRef;\n }\n }\n } catch (err) {\n console.error(\"[integrations] postProcessingPlaceholder failed:\", err);\n }\n\n const payload = JSON.stringify({ incoming, placeholderRef });\n\n await insertPendingTask({\n id: taskId,\n platform: incoming.platform,\n externalThreadId: incoming.externalThreadId,\n payload,\n ownerEmail: options.ownerEmail,\n orgId,\n // SQL-level dedup key — duplicate webhook deliveries from the same\n // platform produce the same key, so the unique index rejects the\n // second insert (H3 in the webhook security audit).\n externalEventKey: buildEventDedupKey(incoming),\n });\n\n const baseUrl = resolveBaseUrl(event);\n const processUrl = `${baseUrl}${FRAMEWORK_ROUTE_PREFIX}/integrations/process-task`;\n\n // Sign the dispatch with an HMAC token so the processor endpoint can\n // verify the request came from us and not the public internet. The\n // processor refuses unsigned requests in production (C3 in the webhook\n // security audit). In dev, dispatching unsigned is allowed and falls\n // through to the SQL atomic claim for double-processing protection.\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n };\n try {\n headers[\"Authorization\"] = `Bearer ${signInternalToken(taskId)}`;\n } catch (err) {\n // Distinguish \"secret not configured\" (the documented dev path) from\n // a real signing failure — silently swallowing both made malformed\n // secrets fail invisibly (L5 in the audit).\n if (err instanceof Error && !/A2A_SECRET/i.test(err.message)) {\n console.error(\n `[integrations] signInternalToken failed unexpectedly for ${taskId}:`,\n err,\n );\n }\n }\n\n // Fire-and-forget: do NOT await the full response (the processor's run\n // takes minutes — we don't want to block the caller). BUT on Netlify\n // Lambda, when we return immediately, the runtime can freeze the function\n // before the outbound TCP handshake even starts, which leaves the dispatch\n // request stuck waiting for the 60s retry-sweep job. Race the fetch\n // against a short timer so the request gets at least ~250ms to leave the\n // box; the trade-off is at most ~250ms of added webhook latency.\n const dispatchPromise = fetch(processUrl, {\n method: \"POST\",\n headers,\n body: JSON.stringify({ taskId }),\n }).catch((err) => {\n console.error(\"[integrations] Failed to dispatch processor request:\", err);\n });\n await Promise.race([\n dispatchPromise,\n new Promise<void>((resolve) => setTimeout(resolve, 250)),\n ]);\n}\n\n/**\n * Resolve the base URL we should dispatch the processor request to.\n * Prefers explicit env vars (most reliable on serverless), falls back to the\n * inbound request's headers.\n */\nexport function resolveBaseUrl(event: H3Event): string {\n const fromEnv =\n process.env.APP_URL ||\n process.env.URL ||\n process.env.DEPLOY_URL ||\n process.env.BETTER_AUTH_URL;\n if (fromEnv) return withConfiguredAppBasePath(fromEnv);\n\n try {\n const headers = (event as any).node?.req?.headers ?? (event as any).headers;\n const get = (name: string): string | undefined => {\n if (!headers) return undefined;\n if (typeof headers.get === \"function\") {\n return headers.get(name) ?? undefined;\n }\n const lower = String(name).toLowerCase();\n const map = headers as Record<string, string | undefined>;\n return map[name] ?? map[lower];\n };\n const proto = get(\"x-forwarded-proto\") || \"http\";\n const host = get(\"host\") || `localhost:${process.env.PORT || 3000}`;\n return withConfiguredAppBasePath(`${proto}://${host}`);\n } catch {\n return withConfiguredAppBasePath(\n `http://localhost:${process.env.PORT || 3000}`,\n );\n }\n}\n\n/**\n * Run the actual agent loop for a previously-enqueued task. Called by the\n * processor endpoint in `plugin.ts`. This is a fresh function execution, so\n * it gets its own timeout budget independent of the inbound webhook handler.\n */\nexport async function processIntegrationTask(\n task: PendingTask,\n options: WebhookHandlerOptions,\n): Promise<void> {\n const parsed = JSON.parse(task.payload) as {\n incoming: IncomingMessage;\n placeholderRef?: string;\n };\n await processIncomingMessage(parsed.incoming, options, {\n placeholderRef: parsed.placeholderRef,\n });\n}\n\n/**\n * Resolve thread, run agent loop, post response, persist thread data.\n * Shared between the new processor endpoint and any direct callers.\n */\nasync function processIncomingMessage(\n incoming: IncomingMessage,\n options: WebhookHandlerOptions,\n opts: { placeholderRef?: string } = {},\n): Promise<void> {\n const { adapter, systemPrompt, actions, model, apiKey, ownerEmail } = options;\n\n // Resolve or create internal thread\n let mapping = await getThreadMapping(\n incoming.platform,\n incoming.externalThreadId,\n );\n\n if (!mapping) {\n const thread = await createThread(ownerEmail, {\n title: `${adapter.label}: ${incoming.senderName || incoming.senderId || \"User\"}`,\n });\n await saveThreadMapping(\n incoming.platform,\n incoming.externalThreadId,\n thread.id,\n incoming.platformContext,\n );\n mapping = {\n platform: incoming.platform,\n externalThreadId: incoming.externalThreadId,\n internalThreadId: thread.id,\n platformContext: incoming.platformContext,\n createdAt: Date.now(),\n updatedAt: Date.now(),\n };\n }\n\n const threadId = mapping.internalThreadId;\n\n // Load existing thread history for context\n const thread = await getThread(threadId);\n const existingMessages: EngineMessage[] = [];\n if (thread?.threadData) {\n try {\n const data = JSON.parse(thread.threadData);\n if (Array.isArray(data.messages)) {\n for (const msg of data.messages) {\n const m = msg.message ?? msg;\n const textContent =\n typeof m.content === \"string\"\n ? m.content\n : Array.isArray(m.content)\n ? m.content\n .filter((c: any) => c.type === \"text\")\n .map((c: any) => c.text)\n .join(\"\\n\")\n : \"\";\n if (m.role === \"user\") {\n existingMessages.push({\n role: \"user\",\n content: [{ type: \"text\", text: textContent }],\n });\n } else if (m.role === \"assistant\") {\n existingMessages.push({\n role: \"assistant\",\n content: [{ type: \"text\", text: textContent }],\n });\n }\n }\n }\n } catch {}\n }\n\n // Add the new user message\n const messages: EngineMessage[] = [\n ...existingMessages,\n { role: \"user\", content: [{ type: \"text\", text: incoming.text }] },\n ];\n\n // Run agent loop via startRun, wrapped in a request context so that\n // tools (especially call-agent) can resolve the caller's org for org-scoped\n // A2A delegation. Without this, getRequestOrgId() returns undefined and\n // call-agent can't look up the org's a2a_secret or org_domain.\n const orgId = await resolveOrgIdForEmail(ownerEmail);\n const engine = createAnthropicEngine({ apiKey });\n const tools = actionsToEngineTools(actions);\n\n const runId = `integration-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;\n\n // Wait for the run to complete inside this fresh function execution.\n // We use a Promise so the processor endpoint can await the full lifecycle.\n await new Promise<void>((resolve) => {\n startRun(\n runId,\n threadId,\n async (send, signal) => {\n await runWithRequestContext(\n {\n userEmail: ownerEmail,\n orgId: orgId ?? undefined,\n // Lets downstream callers (call-agent script) apply tighter\n // budgets on integration paths without affecting normal\n // agent-chat. See `isIntegrationCallerRequest()`.\n isIntegrationCaller: true,\n },\n () =>\n runAgentLoop({\n engine,\n model,\n systemPrompt,\n tools,\n messages,\n actions,\n send,\n signal,\n }),\n );\n },\n async (completedRun: ActiveRun) => {\n try {\n // Collect text events from the run, but drop any pre-tool-call\n // preamble so the user sees just the final answer instead of\n // \"I need to delegate this...\" run-on into the raw tool result.\n //\n // Heuristic: when the agent fired any tool, only keep text events\n // that come AFTER the last tool. The preamble (\"Let me check…\")\n // and the final answer (\"630\") are emitted as two separate text\n // events, and concatenating them with no separator was producing\n // \"...signup data.630\" in Slack.\n let lastToolIdx = -1;\n for (let i = completedRun.events.length - 1; i >= 0; i--) {\n const t = completedRun.events[i].event.type;\n if (t === \"tool_start\" || t === \"tool_done\") {\n lastToolIdx = i;\n break;\n }\n }\n const startIdx = lastToolIdx >= 0 ? lastToolIdx + 1 : 0;\n let responseText = \"\";\n for (let i = startIdx; i < completedRun.events.length; i++) {\n const ev = completedRun.events[i].event;\n if (ev.type === \"text\") responseText += ev.text;\n }\n // If the post-tool window had no text (tool spoke for itself),\n // fall back to all text events so we never leave the user with\n // an empty reply.\n if (!responseText.trim() && lastToolIdx >= 0) {\n for (const runEvent of completedRun.events) {\n if (runEvent.event.type === \"text\") {\n responseText += runEvent.event.text;\n }\n }\n }\n\n // If the run errored OR produced no text, post a graceful fallback so\n // the user isn't left wondering whether the bot saw their message.\n // Common case: an A2A delegation timed out and the agent loop bailed\n // before generating any user-facing text.\n const runErrored = completedRun.status === \"errored\";\n if (!responseText.trim() || runErrored) {\n if (runErrored) {\n responseText =\n (responseText.trim() ? responseText + \"\\n\\n\" : \"\") +\n \"I ran into a problem before I could finish that one. \" +\n \"If it was a complex analytics question, opening the analytics app \" +\n \"directly is the most reliable way to get an answer right now.\";\n } else {\n responseText = \"(No response)\";\n }\n }\n\n // Compute the deep-link to the dispatch UI for this thread, then\n // hand it to the adapter as a structured `threadDeepLinkUrl` so\n // platforms with rich blocks (Slack) can render a button instead\n // of inlining a `<url|text>` link that auto-unfurls into a giant\n // preview card.\n const baseUrl = process.env.APP_URL || process.env.URL || \"\";\n const appBaseUrl = baseUrl ? withConfiguredAppBasePath(baseUrl) : \"\";\n const threadDeepLinkUrl =\n appBaseUrl && threadId\n ? `${appBaseUrl}/?thread=${threadId}`\n : undefined;\n\n // Format and send back to platform — update the \"thinking…\"\n // placeholder in place if the adapter supplied one.\n const outgoing = adapter.formatAgentResponse(responseText, {\n threadDeepLinkUrl,\n });\n await adapter.sendResponse(outgoing, incoming, {\n placeholderRef: opts.placeholderRef,\n });\n\n // Persist thread data\n await persistThreadData(\n threadId,\n incoming.text,\n completedRun,\n thread,\n );\n } catch (err) {\n console.error(\n `[integrations] Error sending response to ${incoming.platform}:`,\n err,\n );\n // Last-ditch: try to post a brief apology so the thread isn't silent.\n try {\n const fallback = adapter.formatAgentResponse(\n \"Something went wrong on my end while replying. Please try again.\",\n );\n await adapter.sendResponse(fallback, incoming);\n } catch {}\n } finally {\n resolve();\n }\n },\n );\n });\n}\n\n/**\n * Persist the user message and agent response to the thread data,\n * so the conversation history is available in the web UI too.\n */\nasync function persistThreadData(\n threadId: string,\n userText: string,\n completedRun: ActiveRun,\n thread: any,\n): Promise<void> {\n try {\n let repo: any;\n try {\n repo = JSON.parse(thread?.threadData || \"{}\");\n } catch {\n repo = {};\n }\n if (!Array.isArray(repo.messages)) repo.messages = [];\n\n // Add user message\n const userMsg = {\n id: `msg-${Date.now()}-user`,\n role: \"user\",\n content: [{ type: \"text\", text: userText }],\n createdAt: new Date().toISOString(),\n };\n\n // Build assistant message from run events\n const assistantMsg = buildAssistantMessage(\n completedRun.events ?? [],\n completedRun.runId,\n );\n\n repo.messages.push(userMsg);\n if (assistantMsg) {\n repo.messages.push(assistantMsg);\n }\n\n const meta = extractThreadMeta(repo);\n await updateThreadData(\n threadId,\n JSON.stringify(repo),\n meta.title || thread?.title || \"Integration Chat\",\n meta.preview || thread?.preview || \"\",\n repo.messages.length,\n );\n } catch {\n // Best-effort persistence\n }\n}\n"]}
1
+ {"version":3,"file":"webhook-handler.js","sourceRoot":"","sources":["../../src/integrations/webhook-handler.ts"],"names":[],"mappings":"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","sourcesContent":["import type { H3Event } from \"h3\";\nimport type { PlatformAdapter, IncomingMessage } from \"./types.js\";\nimport { getThreadMapping, saveThreadMapping } from \"./thread-mapping-store.js\";\nimport { createThread, getThread } from \"../chat-threads/store.js\";\nimport {\n runAgentLoop,\n actionsToEngineTools,\n type ActionEntry,\n} from \"../agent/production-agent.js\";\nimport { createAnthropicEngine } from \"../agent/engine/index.js\";\nimport type { EngineMessage } from \"../agent/engine/types.js\";\nimport { startRun, type ActiveRun } from \"../agent/run-manager.js\";\nimport {\n buildAssistantMessage,\n extractThreadMeta,\n} from \"../agent/thread-data-builder.js\";\nimport { updateThreadData } from \"../chat-threads/store.js\";\nimport { runWithRequestContext } from \"../server/request-context.js\";\nimport { resolveOrgIdForEmail } from \"../org/context.js\";\nimport {\n insertPendingTask,\n isDuplicateEventError,\n type PendingTask,\n} from \"./pending-tasks-store.js\";\nimport { signInternalToken } from \"./internal-token.js\";\nimport { FRAMEWORK_ROUTE_PREFIX } from \"../server/core-routes-plugin.js\";\nimport { withConfiguredAppBasePath } from \"../server/app-base-path.js\";\n\n/**\n * Build a stable per-event dedup key from the incoming message. The same\n * key is computed for every retry of the same event from the platform —\n * Slack/Telegram retry on timeout (3s for Slack), so we MUST treat the\n * second delivery as a duplicate and return 200 silently.\n *\n * The `(platform, external_event_key)` UNIQUE index in\n * `integration_pending_tasks` enforces this at the SQL layer, replacing\n * the previous in-memory Map (H3 in the webhook security audit) which\n * couldn't survive serverless cold starts.\n */\nfunction buildEventDedupKey(incoming: IncomingMessage): string {\n return `${incoming.platform}:${incoming.externalThreadId}:${incoming.timestamp}`;\n}\n\nexport interface WebhookHandlerOptions {\n adapter: PlatformAdapter;\n /** Resolved system prompt string */\n systemPrompt: string;\n /** Action entries for the agent */\n actions: Record<string, ActionEntry>;\n /** Model to use */\n model: string;\n /** Anthropic API key */\n apiKey: string;\n /** Thread owner for personal/shared resource loading */\n ownerEmail: string;\n /**\n * Pre-parsed incoming message. When provided, handleWebhook skips its own\n * verification + parsing steps. Required when the caller has already read\n * the request body (h3 doesn't reliably cache parsed bodies, so re-parsing\n * the same event hangs on streaming providers).\n */\n incoming?: IncomingMessage;\n /** Optional hook to intercept inbound commands before agent execution */\n beforeProcess?: (\n incoming: IncomingMessage,\n adapter: PlatformAdapter,\n ) => Promise<\n | {\n handled: true;\n responseText?: string;\n }\n | { handled: false }\n >;\n}\n\n/**\n * Process an incoming webhook from a messaging platform.\n *\n * Flow:\n * 1. Handle verification challenges (Slack url_verification, etc.)\n * 2. Verify webhook signature\n * 3. Parse incoming message (null = ignored event)\n * 4. Persist task to SQL\n * 5. Fire-and-forget POST to /_agent-native/integrations/process-task\n * (a fresh function execution with its own timeout budget)\n * 6. Return HTTP 200 immediately (within Slack's 3s SLA)\n *\n * The processor endpoint runs the actual agent loop. This split is essential\n * for serverless platforms (Netlify Lambda, Vercel, Cloudflare Workers) which\n * freeze the function as soon as the response is returned, killing any\n * lingering background promises.\n */\nexport async function handleWebhook(\n event: H3Event,\n options: WebhookHandlerOptions,\n): Promise<{ status: number; body: unknown }> {\n const { adapter, beforeProcess } = options;\n\n let incoming: IncomingMessage | null = options.incoming ?? null;\n\n // When the caller didn't pre-parse, run the full verify + parse pipeline.\n // Otherwise skip it — h3's body stream has already been consumed and a\n // second readBody call hangs on streaming providers.\n if (!incoming) {\n // Step 1: Handle platform-specific verification challenges\n const verification = await adapter.handleVerification(event);\n if (verification.handled) {\n return { status: 200, body: verification.response ?? \"ok\" };\n }\n\n // Step 2: Verify webhook signature\n const isValid = await adapter.verifyWebhook(event);\n if (!isValid) {\n return { status: 401, body: { error: \"Invalid webhook signature\" } };\n }\n\n // Step 3: Parse the incoming message\n incoming = await adapter.parseIncomingMessage(event);\n if (!incoming) {\n // Not a user message (bot message, edit, reaction, etc.) — acknowledge silently\n return { status: 200, body: \"ok\" };\n }\n }\n\n // Dedup is enforced inside enqueueAndDispatch — the unique index on\n // `(platform, external_event_key)` raises a constraint violation we treat\n // as \"already enqueued\" and respond 200. We can't dedup BEFORE the\n // beforeProcess hook because some templates use beforeProcess for\n // command-style intercepts that are stateless and idempotent (e.g. a\n // Slack `/help` command that doesn't enqueue a task).\n\n if (beforeProcess) {\n const result = await beforeProcess(incoming, adapter);\n if (result.handled) {\n if (result.responseText?.trim()) {\n const outgoing = adapter.formatAgentResponse(result.responseText);\n await adapter.sendResponse(outgoing, incoming);\n }\n return { status: 200, body: \"ok\" };\n }\n }\n\n // Step 4 + 5: Enqueue to SQL and dispatch to processor in a fresh request.\n try {\n await enqueueAndDispatch(event, incoming, options);\n } catch (err) {\n // Duplicate event delivery: the SQL UNIQUE constraint on\n // (platform, external_event_key) rejected the second insert. This is\n // the expected path when a platform retries an event that already\n // landed (e.g. Slack 3-second timeout) — return 200 so the platform\n // stops retrying. See H3 in the webhook security audit.\n if (isDuplicateEventError(err)) {\n return { status: 200, body: \"ok\" };\n }\n console.error(\n `[integrations] Failed to enqueue/dispatch ${incoming.platform} message:`,\n err,\n );\n // Return 500 so the platform retries. If the SQL insert failed for a\n // non-dup reason, the message is genuinely lost — better to let Slack\n // retry (it will re-fire the same event_callback) than silently drop it.\n return { status: 500, body: { error: \"enqueue failed\" } };\n }\n\n return { status: 200, body: \"ok\" };\n}\n\n/**\n * Persist the task to SQL and dispatch a fresh HTTP request to the processor\n * endpoint. The dispatch is fire-and-forget — we deliberately do NOT await\n * the resulting fetch, so the current handler can return immediately.\n *\n * This pattern works on every supported host:\n * - Netlify Lambda: function returns; the dispatched request hits a fresh\n * Lambda with its own 26s budget.\n * - Vercel Functions: same.\n * - Cloudflare Workers: same (no waitUntil dependency).\n * - Self-hosted Node: a separate request comes back through the same\n * server, but each handler still runs to completion.\n */\nasync function enqueueAndDispatch(\n event: H3Event,\n incoming: IncomingMessage,\n options: WebhookHandlerOptions,\n): Promise<void> {\n const taskId = `task-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;\n\n // Resolve the org id once at enqueue-time so the processor doesn't have to\n // re-derive it (and so we can drop it on the row for observability).\n let orgId: string | null = null;\n try {\n orgId = (await resolveOrgIdForEmail(options.ownerEmail)) ?? null;\n } catch {\n orgId = null;\n }\n\n // Post a \"thinking…\" placeholder immediately if the adapter supports\n // in-place edits. The processor flow will update this same message with\n // the final answer, so users see one tidy thread reply instead of\n // \"[silence] → answer\". Adapters without edit support skip this and the\n // processor posts a fresh response.\n let placeholderRef: string | undefined;\n try {\n if (options.adapter.postProcessingPlaceholder) {\n const placeholder =\n await options.adapter.postProcessingPlaceholder(incoming);\n if (placeholder?.placeholderRef) {\n placeholderRef = placeholder.placeholderRef;\n }\n }\n } catch (err) {\n console.error(\"[integrations] postProcessingPlaceholder failed:\", err);\n }\n\n const payload = JSON.stringify({ incoming, placeholderRef });\n\n await insertPendingTask({\n id: taskId,\n platform: incoming.platform,\n externalThreadId: incoming.externalThreadId,\n payload,\n ownerEmail: options.ownerEmail,\n orgId,\n // SQL-level dedup key — duplicate webhook deliveries from the same\n // platform produce the same key, so the unique index rejects the\n // second insert (H3 in the webhook security audit).\n externalEventKey: buildEventDedupKey(incoming),\n });\n\n const baseUrl = resolveBaseUrl(event);\n const processUrl = `${baseUrl}${FRAMEWORK_ROUTE_PREFIX}/integrations/process-task`;\n\n // Sign the dispatch with an HMAC token so the processor endpoint can\n // verify the request came from us and not the public internet. The\n // processor refuses unsigned requests in production (C3 in the webhook\n // security audit). In dev, dispatching unsigned is allowed and falls\n // through to the SQL atomic claim for double-processing protection.\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n };\n try {\n headers[\"Authorization\"] = `Bearer ${signInternalToken(taskId)}`;\n } catch (err) {\n // Distinguish \"secret not configured\" (the documented dev path) from\n // a real signing failure — silently swallowing both made malformed\n // secrets fail invisibly (L5 in the audit).\n if (err instanceof Error && !/A2A_SECRET/i.test(err.message)) {\n console.error(\n `[integrations] signInternalToken failed unexpectedly for ${taskId}:`,\n err,\n );\n }\n }\n\n // Fire-and-forget: do NOT await the full response (the processor's run\n // takes minutes — we don't want to block the caller). BUT on Netlify\n // Lambda, when we return immediately, the runtime can freeze the function\n // before the outbound TCP handshake even starts, which leaves the dispatch\n // request stuck waiting for the 60s retry-sweep job. Race the fetch\n // against a short timer so the request gets at least ~250ms to leave the\n // box; the trade-off is at most ~250ms of added webhook latency.\n const dispatchPromise = fetch(processUrl, {\n method: \"POST\",\n headers,\n body: JSON.stringify({ taskId }),\n }).catch((err) => {\n console.error(\"[integrations] Failed to dispatch processor request:\", err);\n });\n await Promise.race([\n dispatchPromise,\n new Promise<void>((resolve) => setTimeout(resolve, 250)),\n ]);\n}\n\n/**\n * Resolve the base URL we should dispatch the processor request to.\n * Prefers explicit env vars (most reliable on serverless), falls back to the\n * inbound request's headers.\n */\nexport function resolveBaseUrl(event: H3Event): string {\n const fromEnv =\n process.env.APP_URL ||\n process.env.URL ||\n process.env.DEPLOY_URL ||\n process.env.BETTER_AUTH_URL;\n if (fromEnv) return withConfiguredAppBasePath(fromEnv);\n\n try {\n const headers = (event as any).node?.req?.headers ?? (event as any).headers;\n const get = (name: string): string | undefined => {\n if (!headers) return undefined;\n if (typeof headers.get === \"function\") {\n return headers.get(name) ?? undefined;\n }\n const lower = String(name).toLowerCase();\n const map = headers as Record<string, string | undefined>;\n return map[name] ?? map[lower];\n };\n const proto = get(\"x-forwarded-proto\") || \"http\";\n const host = get(\"host\") || `localhost:${process.env.PORT || 3000}`;\n return withConfiguredAppBasePath(`${proto}://${host}`);\n } catch {\n return withConfiguredAppBasePath(\n `http://localhost:${process.env.PORT || 3000}`,\n );\n }\n}\n\n/**\n * Run the actual agent loop for a previously-enqueued task. Called by the\n * processor endpoint in `plugin.ts`. This is a fresh function execution, so\n * it gets its own timeout budget independent of the inbound webhook handler.\n */\nexport async function processIntegrationTask(\n task: PendingTask,\n options: WebhookHandlerOptions,\n): Promise<void> {\n const parsed = JSON.parse(task.payload) as {\n incoming: IncomingMessage;\n placeholderRef?: string;\n };\n await processIncomingMessage(parsed.incoming, options, {\n placeholderRef: parsed.placeholderRef,\n });\n}\n\n/**\n * Resolve thread, run agent loop, post response, persist thread data.\n * Shared between the new processor endpoint and any direct callers.\n */\nasync function processIncomingMessage(\n incoming: IncomingMessage,\n options: WebhookHandlerOptions,\n opts: { placeholderRef?: string } = {},\n): Promise<void> {\n const { adapter, systemPrompt, actions, model, apiKey, ownerEmail } = options;\n\n // Resolve or create internal thread\n let mapping = await getThreadMapping(\n incoming.platform,\n incoming.externalThreadId,\n );\n\n if (!mapping) {\n const thread = await createThread(ownerEmail, {\n title: `${adapter.label}: ${incoming.senderName || incoming.senderId || \"User\"}`,\n });\n await saveThreadMapping(\n incoming.platform,\n incoming.externalThreadId,\n thread.id,\n incoming.platformContext,\n );\n mapping = {\n platform: incoming.platform,\n externalThreadId: incoming.externalThreadId,\n internalThreadId: thread.id,\n platformContext: incoming.platformContext,\n createdAt: Date.now(),\n updatedAt: Date.now(),\n };\n }\n\n const threadId = mapping.internalThreadId;\n\n // Load existing thread history for context\n const thread = await getThread(threadId);\n const existingMessages: EngineMessage[] = [];\n if (thread?.threadData) {\n try {\n const data = JSON.parse(thread.threadData);\n if (Array.isArray(data.messages)) {\n for (const msg of data.messages) {\n const m = msg.message ?? msg;\n const textContent =\n typeof m.content === \"string\"\n ? m.content\n : Array.isArray(m.content)\n ? m.content\n .filter((c: any) => c.type === \"text\")\n .map((c: any) => c.text)\n .join(\"\\n\")\n : \"\";\n if (m.role === \"user\") {\n existingMessages.push({\n role: \"user\",\n content: [{ type: \"text\", text: textContent }],\n });\n } else if (m.role === \"assistant\") {\n existingMessages.push({\n role: \"assistant\",\n content: [{ type: \"text\", text: textContent }],\n });\n }\n }\n }\n } catch {}\n }\n\n // Add the new user message. Include verified platform identity as lightweight\n // context so app-specific agents can attribute requests without guessing.\n const identityLines = [\n `Platform: ${incoming.platform}`,\n incoming.senderName ? `Sender name: ${incoming.senderName}` : null,\n incoming.senderEmail ? `Sender email: ${incoming.senderEmail}` : null,\n incoming.senderId ? `Sender ID: ${incoming.senderId}` : null,\n ].filter(Boolean);\n const userText =\n identityLines.length > 1\n ? `<integration-context>\\n${identityLines.join(\"\\n\")}\\n</integration-context>\\n\\n${incoming.text}`\n : incoming.text;\n\n const messages: EngineMessage[] = [\n ...existingMessages,\n { role: \"user\", content: [{ type: \"text\", text: userText }] },\n ];\n\n // Run agent loop via startRun, wrapped in a request context so that\n // tools (especially call-agent) can resolve the caller's org for org-scoped\n // A2A delegation. Without this, getRequestOrgId() returns undefined and\n // call-agent can't look up the org's a2a_secret or org_domain.\n const orgId = await resolveOrgIdForEmail(ownerEmail);\n const engine = createAnthropicEngine({ apiKey });\n const tools = actionsToEngineTools(actions);\n\n const runId = `integration-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;\n\n // Wait for the run to complete inside this fresh function execution.\n // We use a Promise so the processor endpoint can await the full lifecycle.\n await new Promise<void>((resolve) => {\n startRun(\n runId,\n threadId,\n async (send, signal) => {\n await runWithRequestContext(\n {\n userEmail: ownerEmail,\n orgId: orgId ?? undefined,\n // Lets downstream callers (call-agent script) apply tighter\n // budgets on integration paths without affecting normal\n // agent-chat. See `isIntegrationCallerRequest()`.\n isIntegrationCaller: true,\n },\n () =>\n runAgentLoop({\n engine,\n model,\n systemPrompt,\n tools,\n messages,\n actions,\n send,\n signal,\n }),\n );\n },\n async (completedRun: ActiveRun) => {\n try {\n // Collect text events from the run, but drop any pre-tool-call\n // preamble so the user sees just the final answer instead of\n // \"I need to delegate this...\" run-on into the raw tool result.\n //\n // Heuristic: when the agent fired any tool, only keep text events\n // that come AFTER the last tool. The preamble (\"Let me check…\")\n // and the final answer (\"630\") are emitted as two separate text\n // events, and concatenating them with no separator was producing\n // \"...signup data.630\" in Slack.\n let lastToolIdx = -1;\n for (let i = completedRun.events.length - 1; i >= 0; i--) {\n const t = completedRun.events[i].event.type;\n if (t === \"tool_start\" || t === \"tool_done\") {\n lastToolIdx = i;\n break;\n }\n }\n const startIdx = lastToolIdx >= 0 ? lastToolIdx + 1 : 0;\n let responseText = \"\";\n for (let i = startIdx; i < completedRun.events.length; i++) {\n const ev = completedRun.events[i].event;\n if (ev.type === \"text\") responseText += ev.text;\n }\n // If the post-tool window had no text (tool spoke for itself),\n // fall back to all text events so we never leave the user with\n // an empty reply.\n if (!responseText.trim() && lastToolIdx >= 0) {\n for (const runEvent of completedRun.events) {\n if (runEvent.event.type === \"text\") {\n responseText += runEvent.event.text;\n }\n }\n }\n\n // If the run errored OR produced no text, post a graceful fallback so\n // the user isn't left wondering whether the bot saw their message.\n // Common case: an A2A delegation timed out and the agent loop bailed\n // before generating any user-facing text.\n const runErrored = completedRun.status === \"errored\";\n if (!responseText.trim() || runErrored) {\n if (runErrored) {\n responseText =\n (responseText.trim() ? responseText + \"\\n\\n\" : \"\") +\n \"I ran into a problem before I could finish that one. \" +\n \"If it was a complex analytics question, opening the analytics app \" +\n \"directly is the most reliable way to get an answer right now.\";\n } else {\n responseText = \"(No response)\";\n }\n }\n\n // Compute the deep-link to the dispatch UI for this thread, then\n // hand it to the adapter as a structured `threadDeepLinkUrl` so\n // platforms with rich blocks (Slack) can render a button instead\n // of inlining a `<url|text>` link that auto-unfurls into a giant\n // preview card.\n const baseUrl = process.env.APP_URL || process.env.URL || \"\";\n const appBaseUrl = baseUrl ? withConfiguredAppBasePath(baseUrl) : \"\";\n const threadDeepLinkUrl =\n appBaseUrl && threadId\n ? `${appBaseUrl}/?thread=${threadId}`\n : undefined;\n\n // Format and send back to platform — update the \"thinking…\"\n // placeholder in place if the adapter supplied one.\n const outgoing = adapter.formatAgentResponse(responseText, {\n threadDeepLinkUrl,\n });\n await adapter.sendResponse(outgoing, incoming, {\n placeholderRef: opts.placeholderRef,\n });\n\n // Persist thread data\n await persistThreadData(\n threadId,\n incoming.text,\n completedRun,\n thread,\n );\n } catch (err) {\n console.error(\n `[integrations] Error sending response to ${incoming.platform}:`,\n err,\n );\n // Last-ditch: try to post a brief apology so the thread isn't silent.\n try {\n const fallback = adapter.formatAgentResponse(\n \"Something went wrong on my end while replying. Please try again.\",\n );\n await adapter.sendResponse(fallback, incoming);\n } catch {}\n } finally {\n resolve();\n }\n },\n );\n });\n}\n\n/**\n * Persist the user message and agent response to the thread data,\n * so the conversation history is available in the web UI too.\n */\nasync function persistThreadData(\n threadId: string,\n userText: string,\n completedRun: ActiveRun,\n thread: any,\n): Promise<void> {\n try {\n let repo: any;\n try {\n repo = JSON.parse(thread?.threadData || \"{}\");\n } catch {\n repo = {};\n }\n if (!Array.isArray(repo.messages)) repo.messages = [];\n\n // Add user message\n const userMsg = {\n id: `msg-${Date.now()}-user`,\n role: \"user\",\n content: [{ type: \"text\", text: userText }],\n createdAt: new Date().toISOString(),\n };\n\n // Build assistant message from run events\n const assistantMsg = buildAssistantMessage(\n completedRun.events ?? [],\n completedRun.runId,\n );\n\n repo.messages.push(userMsg);\n if (assistantMsg) {\n repo.messages.push(assistantMsg);\n }\n\n const meta = extractThreadMeta(repo);\n await updateThreadData(\n threadId,\n JSON.stringify(repo),\n meta.title || thread?.title || \"Integration Chat\",\n meta.preview || thread?.preview || \"\",\n repo.messages.length,\n );\n } catch {\n // Best-effort persistence\n }\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/oauth-tokens/store.ts"],"names":[],"mappings":"AAyCA,wBAAsB,cAAc,CAClC,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC,CASzC;AAED;;;;;;;;GAQG;AACH,qBAAa,iCAAkC,SAAQ,KAAK;IAC1D,QAAQ,CAAC,UAAU,OAAO;IAC1B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;gBACpB,IAAI,EAAE;QAChB,QAAQ,EAAE,MAAM,CAAC;QACjB,SAAS,EAAE,MAAM,CAAC;QAClB,aAAa,EAAE,MAAM,CAAC;QACtB,cAAc,EAAE,MAAM,CAAC;KACxB;CAUF;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,eAAe,CACnC,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,KAAK,CAAC,EAAE,MAAM,GACb,OAAO,CAAC,IAAI,CAAC,CA6Df;AAED,wBAAsB,iBAAiB,CACrC,QAAQ,EAAE,MAAM,EAChB,SAAS,CAAC,EAAE,MAAM,GACjB,OAAO,CAAC,MAAM,CAAC,CAejB;AAED,wBAAsB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAChE,KAAK,CAAC;IACJ,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACjC,CAAC,CACH,CAYA;AAED;;;GAGG;AACH,wBAAsB,wBAAwB,CAC5C,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,GACZ,OAAO,CACR,KAAK,CAAC;IACJ,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACjC,CAAC,CACH,CAYA;AAED;;GAEG;AACH,wBAAsB,mBAAmB,CACvC,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,EACjB,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,IAAI,CAAC,CAOf;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,cAAc,CAClC,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,OAAO,CAAC,CAelB"}
1
+ {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/oauth-tokens/store.ts"],"names":[],"mappings":"AA8CA,wBAAsB,cAAc,CAClC,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC,CAUzC;AAED;;;;;;;;GAQG;AACH,qBAAa,iCAAkC,SAAQ,KAAK;IAC1D,QAAQ,CAAC,UAAU,OAAO;IAC1B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;gBACpB,IAAI,EAAE;QAChB,QAAQ,EAAE,MAAM,CAAC;QACjB,SAAS,EAAE,MAAM,CAAC;QAClB,aAAa,EAAE,MAAM,CAAC;QACtB,cAAc,EAAE,MAAM,CAAC;KACxB;CAUF;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,eAAe,CACnC,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,KAAK,CAAC,EAAE,MAAM,GACb,OAAO,CAAC,IAAI,CAAC,CAwEf;AAED,wBAAsB,iBAAiB,CACrC,QAAQ,EAAE,MAAM,EAChB,SAAS,CAAC,EAAE,MAAM,GACjB,OAAO,CAAC,MAAM,CAAC,CAgBjB;AAED,wBAAsB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAChE,KAAK,CAAC;IACJ,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACjC,CAAC,CACH,CAaA;AAED;;;GAGG;AACH,wBAAsB,wBAAwB,CAC5C,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,GACZ,OAAO,CACR,KAAK,CAAC;IACJ,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACjC,CAAC,CACH,CAaA;AAED;;GAEG;AACH,wBAAsB,mBAAmB,CACvC,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,EACjB,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,IAAI,CAAC,CAQf;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,cAAc,CAClC,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,OAAO,CAAC,CAgBlB"}
@@ -1,11 +1,15 @@
1
1
  import { getDbExec, isPostgres, intType } from "../db/client.js";
2
2
  let _initPromise;
3
+ function oauthTokensTable() {
4
+ return isPostgres() ? "public.oauth_tokens" : "oauth_tokens";
5
+ }
3
6
  async function ensureTable() {
4
7
  if (!_initPromise) {
5
8
  _initPromise = (async () => {
6
9
  const client = getDbExec();
10
+ const table = oauthTokensTable();
7
11
  await client.execute(`
8
- CREATE TABLE IF NOT EXISTS oauth_tokens (
12
+ CREATE TABLE IF NOT EXISTS ${table} (
9
13
  provider TEXT NOT NULL,
10
14
  account_id TEXT NOT NULL,
11
15
  owner TEXT,
@@ -16,20 +20,20 @@ async function ensureTable() {
16
20
  `);
17
21
  // Migration: add owner column to existing tables
18
22
  try {
19
- await client.execute(`ALTER TABLE oauth_tokens ADD COLUMN owner TEXT`);
23
+ await client.execute(`ALTER TABLE ${table} ADD COLUMN owner TEXT`);
20
24
  }
21
25
  catch {
22
26
  // Column already exists
23
27
  }
24
28
  // Migration: add display_name column
25
29
  try {
26
- await client.execute(`ALTER TABLE oauth_tokens ADD COLUMN display_name TEXT`);
30
+ await client.execute(`ALTER TABLE ${table} ADD COLUMN display_name TEXT`);
27
31
  }
28
32
  catch {
29
33
  // Column already exists
30
34
  }
31
35
  // Backfill: set owner = account_id for existing rows without an owner
32
- await client.execute(`UPDATE oauth_tokens SET owner = account_id WHERE owner IS NULL`);
36
+ await client.execute(`UPDATE ${table} SET owner = account_id WHERE owner IS NULL`);
33
37
  })();
34
38
  }
35
39
  return _initPromise;
@@ -37,8 +41,9 @@ async function ensureTable() {
37
41
  export async function getOAuthTokens(provider, accountId) {
38
42
  await ensureTable();
39
43
  const client = getDbExec();
44
+ const table = oauthTokensTable();
40
45
  const { rows } = await client.execute({
41
- sql: `SELECT tokens FROM oauth_tokens WHERE provider = ? AND account_id = ?`,
46
+ sql: `SELECT tokens FROM ${table} WHERE provider = ? AND account_id = ?`,
42
47
  args: [provider, accountId],
43
48
  });
44
49
  if (rows.length === 0)
@@ -87,6 +92,7 @@ export class OAuthAccountOwnedByOtherUserError extends Error {
87
92
  export async function saveOAuthTokens(provider, accountId, tokens, owner) {
88
93
  await ensureTable();
89
94
  const client = getDbExec();
95
+ const table = oauthTokensTable();
90
96
  // Never store "local@localhost" as owner — it creates shared-ownership bugs
91
97
  const sanitizedOwner = owner === "local@localhost" ? accountId : owner;
92
98
  // Read the current row before deciding what to write. We use this to
@@ -96,13 +102,15 @@ export async function saveOAuthTokens(provider, accountId, tokens, owner) {
96
102
  let resolvedOwner = sanitizedOwner ?? accountId;
97
103
  let existingDisplayName = null;
98
104
  let existingOwner = null;
105
+ let existingTokens = null;
99
106
  const { rows: existing } = await client.execute({
100
- sql: `SELECT owner, display_name FROM oauth_tokens WHERE provider = ? AND account_id = ?`,
107
+ sql: `SELECT owner, display_name, tokens FROM ${table} WHERE provider = ? AND account_id = ?`,
101
108
  args: [provider, accountId],
102
109
  });
103
110
  if (existing.length > 0) {
104
111
  existingOwner = existing[0].owner ?? null;
105
112
  existingDisplayName = existing[0].display_name ?? null;
113
+ existingTokens = JSON.parse(existing[0].tokens ?? "{}");
106
114
  }
107
115
  if (!owner) {
108
116
  // Token-refresh path: keep the existing owner/displayName unchanged.
@@ -129,16 +137,21 @@ export async function saveOAuthTokens(provider, accountId, tokens, owner) {
129
137
  attemptedOwner: sanitizedOwner,
130
138
  });
131
139
  }
140
+ const cleanedIncomingTokens = Object.fromEntries(Object.entries(tokens).filter(([, value]) => value !== undefined));
141
+ const tokensToStore = {
142
+ ...(existingTokens ?? {}),
143
+ ...cleanedIncomingTokens,
144
+ };
132
145
  await client.execute({
133
146
  sql: isPostgres()
134
- ? `INSERT INTO oauth_tokens (provider, account_id, owner, display_name, tokens, updated_at) VALUES (?, ?, ?, ?, ?, ?) ON CONFLICT (provider, account_id) DO UPDATE SET owner=EXCLUDED.owner, display_name=COALESCE(EXCLUDED.display_name, oauth_tokens.display_name), tokens=EXCLUDED.tokens, updated_at=EXCLUDED.updated_at`
135
- : `INSERT OR REPLACE INTO oauth_tokens (provider, account_id, owner, display_name, tokens, updated_at) VALUES (?, ?, ?, ?, ?, ?)`,
147
+ ? `INSERT INTO ${table} (provider, account_id, owner, display_name, tokens, updated_at) VALUES (?, ?, ?, ?, ?, ?) ON CONFLICT (provider, account_id) DO UPDATE SET owner=EXCLUDED.owner, display_name=COALESCE(EXCLUDED.display_name, ${table}.display_name), tokens=EXCLUDED.tokens, updated_at=EXCLUDED.updated_at`
148
+ : `INSERT OR REPLACE INTO ${table} (provider, account_id, owner, display_name, tokens, updated_at) VALUES (?, ?, ?, ?, ?, ?)`,
136
149
  args: [
137
150
  provider,
138
151
  accountId,
139
152
  resolvedOwner,
140
153
  existingDisplayName,
141
- JSON.stringify(tokens),
154
+ JSON.stringify(tokensToStore),
142
155
  Date.now(),
143
156
  ],
144
157
  });
@@ -146,15 +159,16 @@ export async function saveOAuthTokens(provider, accountId, tokens, owner) {
146
159
  export async function deleteOAuthTokens(provider, accountId) {
147
160
  await ensureTable();
148
161
  const client = getDbExec();
162
+ const table = oauthTokensTable();
149
163
  if (accountId) {
150
164
  const result = await client.execute({
151
- sql: `DELETE FROM oauth_tokens WHERE provider = ? AND account_id = ?`,
165
+ sql: `DELETE FROM ${table} WHERE provider = ? AND account_id = ?`,
152
166
  args: [provider, accountId],
153
167
  });
154
168
  return result.rowsAffected;
155
169
  }
156
170
  const result = await client.execute({
157
- sql: `DELETE FROM oauth_tokens WHERE provider = ?`,
171
+ sql: `DELETE FROM ${table} WHERE provider = ?`,
158
172
  args: [provider],
159
173
  });
160
174
  return result.rowsAffected;
@@ -162,8 +176,9 @@ export async function deleteOAuthTokens(provider, accountId) {
162
176
  export async function listOAuthAccounts(provider) {
163
177
  await ensureTable();
164
178
  const client = getDbExec();
179
+ const table = oauthTokensTable();
165
180
  const { rows } = await client.execute({
166
- sql: `SELECT account_id, owner, tokens FROM oauth_tokens WHERE provider = ?`,
181
+ sql: `SELECT account_id, owner, tokens FROM ${table} WHERE provider = ?`,
167
182
  args: [provider],
168
183
  });
169
184
  return rows.map((row) => ({
@@ -179,8 +194,9 @@ export async function listOAuthAccounts(provider) {
179
194
  export async function listOAuthAccountsByOwner(provider, owner) {
180
195
  await ensureTable();
181
196
  const client = getDbExec();
197
+ const table = oauthTokensTable();
182
198
  const { rows } = await client.execute({
183
- sql: `SELECT account_id, display_name, tokens FROM oauth_tokens WHERE provider = ? AND owner = ?`,
199
+ sql: `SELECT account_id, display_name, tokens FROM ${table} WHERE provider = ? AND owner = ?`,
184
200
  args: [provider, owner],
185
201
  });
186
202
  return rows.map((row) => ({
@@ -195,8 +211,9 @@ export async function listOAuthAccountsByOwner(provider, owner) {
195
211
  export async function setOAuthDisplayName(provider, accountId, displayName) {
196
212
  await ensureTable();
197
213
  const client = getDbExec();
214
+ const table = oauthTokensTable();
198
215
  await client.execute({
199
- sql: `UPDATE oauth_tokens SET display_name = ? WHERE provider = ? AND account_id = ?`,
216
+ sql: `UPDATE ${table} SET display_name = ? WHERE provider = ? AND account_id = ?`,
200
217
  args: [displayName, provider, accountId],
201
218
  });
202
219
  }
@@ -215,15 +232,16 @@ export async function setOAuthDisplayName(provider, accountId, displayName) {
215
232
  export async function hasOAuthTokens(provider, owner) {
216
233
  await ensureTable();
217
234
  const client = getDbExec();
235
+ const table = oauthTokensTable();
218
236
  if (owner === "local@localhost") {
219
237
  const { rows } = await client.execute({
220
- sql: `SELECT 1 FROM oauth_tokens WHERE provider = ? LIMIT 1`,
238
+ sql: `SELECT 1 FROM ${table} WHERE provider = ? LIMIT 1`,
221
239
  args: [provider],
222
240
  });
223
241
  return rows.length > 0;
224
242
  }
225
243
  const { rows } = await client.execute({
226
- sql: `SELECT 1 FROM oauth_tokens WHERE provider = ? AND owner = ? LIMIT 1`,
244
+ sql: `SELECT 1 FROM ${table} WHERE provider = ? AND owner = ? LIMIT 1`,
227
245
  args: [provider, owner],
228
246
  });
229
247
  return rows.length > 0;
@@ -1 +1 @@
1
- {"version":3,"file":"store.js","sourceRoot":"","sources":["../../src/oauth-tokens/store.ts"],"names":[],"mappings":"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","sourcesContent":["import { getDbExec, isPostgres, intType, type DbExec } from \"../db/client.js\";\n\nlet _initPromise: Promise<void> | undefined;\n\nasync function ensureTable(): Promise<void> {\n if (!_initPromise) {\n _initPromise = (async () => {\n const client = getDbExec();\n await client.execute(`\n CREATE TABLE IF NOT EXISTS oauth_tokens (\n provider TEXT NOT NULL,\n account_id TEXT NOT NULL,\n owner TEXT,\n tokens TEXT NOT NULL,\n updated_at ${intType()} NOT NULL,\n PRIMARY KEY (provider, account_id)\n )\n `);\n // Migration: add owner column to existing tables\n try {\n await client.execute(`ALTER TABLE oauth_tokens ADD COLUMN owner TEXT`);\n } catch {\n // Column already exists\n }\n // Migration: add display_name column\n try {\n await client.execute(\n `ALTER TABLE oauth_tokens ADD COLUMN display_name TEXT`,\n );\n } catch {\n // Column already exists\n }\n // Backfill: set owner = account_id for existing rows without an owner\n await client.execute(\n `UPDATE oauth_tokens SET owner = account_id WHERE owner IS NULL`,\n );\n })();\n }\n return _initPromise;\n}\n\nexport async function getOAuthTokens(\n provider: string,\n accountId: string,\n): Promise<Record<string, unknown> | null> {\n await ensureTable();\n const client = getDbExec();\n const { rows } = await client.execute({\n sql: `SELECT tokens FROM oauth_tokens WHERE provider = ? AND account_id = ?`,\n args: [provider, accountId],\n });\n if (rows.length === 0) return null;\n return JSON.parse(rows[0].tokens as string);\n}\n\n/**\n * Thrown when an OAuth save would re-bind an `(provider, account_id)` row\n * to a different owner than already holds it. Callers should catch this and\n * surface a clean \"this account is already linked to another user\" message\n * to the requester rather than letting it propagate as a 500.\n *\n * Carries `statusCode = 409` so route handlers using h3's `createError` can\n * pass it straight through.\n */\nexport class OAuthAccountOwnedByOtherUserError extends Error {\n readonly statusCode = 409;\n readonly provider: string;\n readonly accountId: string;\n readonly existingOwner: string;\n readonly attemptedOwner: string;\n constructor(opts: {\n provider: string;\n accountId: string;\n existingOwner: string;\n attemptedOwner: string;\n }) {\n super(\n `OAuth account ${opts.provider}:${opts.accountId} is already linked to another user — refusing to overwrite the owner.`,\n );\n this.name = \"OAuthAccountOwnedByOtherUserError\";\n this.provider = opts.provider;\n this.accountId = opts.accountId;\n this.existingOwner = opts.existingOwner;\n this.attemptedOwner = opts.attemptedOwner;\n }\n}\n\n/**\n * Save OAuth tokens. The `owner` parameter specifies which user owns this\n * account — defaults to `accountId` (the account itself is the owner).\n * For multi-account support, pass the logged-in user's email as owner.\n *\n * If the account already exists and is owned by a different user, throws\n * `OAuthAccountOwnedByOtherUserError` (statusCode 409) to prevent silently\n * stealing another user's linked account.\n *\n * Read + write happen as a single linearised batch (Postgres) or paired\n * statements (SQLite). On both backends the per-row PK serialises concurrent\n * writes for the same `(provider, account_id)` so the owner check cannot be\n * raced by an attacker calling saveOAuthTokens twice in flight — the second\n * caller sees the first caller's owner row and raises 409.\n */\nexport async function saveOAuthTokens(\n provider: string,\n accountId: string,\n tokens: Record<string, unknown>,\n owner?: string,\n): Promise<void> {\n await ensureTable();\n const client = getDbExec();\n\n // Never store \"local@localhost\" as owner — it creates shared-ownership bugs\n const sanitizedOwner = owner === \"local@localhost\" ? accountId : owner;\n\n // Read the current row before deciding what to write. We use this to\n // (a) preserve owner / display_name when this is a token refresh (no\n // owner argument), and (b) reject the write when the caller is trying\n // to overwrite a row owned by someone else.\n let resolvedOwner = sanitizedOwner ?? accountId;\n let existingDisplayName: string | null = null;\n let existingOwner: string | null = null;\n const { rows: existing } = await client.execute({\n sql: `SELECT owner, display_name FROM oauth_tokens WHERE provider = ? AND account_id = ?`,\n args: [provider, accountId],\n });\n if (existing.length > 0) {\n existingOwner = (existing[0].owner as string) ?? null;\n existingDisplayName = (existing[0].display_name as string) ?? null;\n }\n\n if (!owner) {\n // Token-refresh path: keep the existing owner/displayName unchanged.\n if (existingOwner) resolvedOwner = existingOwner;\n } else if (existingOwner === \"local@localhost\" && sanitizedOwner) {\n // Legacy dev rows from older OAuth flows were sometimes stored under the\n // synthetic local user. A successful OAuth callback proves control of the\n // Google account, so let the reconnect repair that owner in place.\n resolvedOwner = sanitizedOwner;\n } else if (\n existingOwner &&\n sanitizedOwner &&\n existingOwner !== sanitizedOwner\n ) {\n // Refuse to silently re-bind an account from one user to another.\n // This is the case the docstring promised but the previous\n // implementation didn't enforce — `ON CONFLICT DO UPDATE SET\n // owner=EXCLUDED.owner` would have overwritten the prior owner.\n throw new OAuthAccountOwnedByOtherUserError({\n provider,\n accountId,\n existingOwner,\n attemptedOwner: sanitizedOwner,\n });\n }\n\n await client.execute({\n sql: isPostgres()\n ? `INSERT INTO oauth_tokens (provider, account_id, owner, display_name, tokens, updated_at) VALUES (?, ?, ?, ?, ?, ?) ON CONFLICT (provider, account_id) DO UPDATE SET owner=EXCLUDED.owner, display_name=COALESCE(EXCLUDED.display_name, oauth_tokens.display_name), tokens=EXCLUDED.tokens, updated_at=EXCLUDED.updated_at`\n : `INSERT OR REPLACE INTO oauth_tokens (provider, account_id, owner, display_name, tokens, updated_at) VALUES (?, ?, ?, ?, ?, ?)`,\n args: [\n provider,\n accountId,\n resolvedOwner,\n existingDisplayName,\n JSON.stringify(tokens),\n Date.now(),\n ],\n });\n}\n\nexport async function deleteOAuthTokens(\n provider: string,\n accountId?: string,\n): Promise<number> {\n await ensureTable();\n const client = getDbExec();\n if (accountId) {\n const result = await client.execute({\n sql: `DELETE FROM oauth_tokens WHERE provider = ? AND account_id = ?`,\n args: [provider, accountId],\n });\n return result.rowsAffected;\n }\n const result = await client.execute({\n sql: `DELETE FROM oauth_tokens WHERE provider = ?`,\n args: [provider],\n });\n return result.rowsAffected;\n}\n\nexport async function listOAuthAccounts(provider: string): Promise<\n Array<{\n accountId: string;\n owner: string | null;\n tokens: Record<string, unknown>;\n }>\n> {\n await ensureTable();\n const client = getDbExec();\n const { rows } = await client.execute({\n sql: `SELECT account_id, owner, tokens FROM oauth_tokens WHERE provider = ?`,\n args: [provider],\n });\n return rows.map((row) => ({\n accountId: row.account_id as string,\n owner: (row.owner as string) ?? null,\n tokens: JSON.parse(row.tokens as string),\n }));\n}\n\n/**\n * List all OAuth accounts owned by a specific user.\n * In multi-account mode, a user may have connected multiple Google accounts.\n */\nexport async function listOAuthAccountsByOwner(\n provider: string,\n owner: string,\n): Promise<\n Array<{\n accountId: string;\n displayName: string | null;\n tokens: Record<string, unknown>;\n }>\n> {\n await ensureTable();\n const client = getDbExec();\n const { rows } = await client.execute({\n sql: `SELECT account_id, display_name, tokens FROM oauth_tokens WHERE provider = ? AND owner = ?`,\n args: [provider, owner],\n });\n return rows.map((row) => ({\n accountId: row.account_id as string,\n displayName: (row.display_name as string) ?? null,\n tokens: JSON.parse(row.tokens as string),\n }));\n}\n\n/**\n * Set the display name for an OAuth account (e.g. Google profile name).\n */\nexport async function setOAuthDisplayName(\n provider: string,\n accountId: string,\n displayName: string,\n): Promise<void> {\n await ensureTable();\n const client = getDbExec();\n await client.execute({\n sql: `UPDATE oauth_tokens SET display_name = ? WHERE provider = ? AND account_id = ?`,\n args: [displayName, provider, accountId],\n });\n}\n\n/**\n * Check whether a specific user has tokens for a provider.\n *\n * `owner` is REQUIRED. The previous unscoped form leaked information\n * across users — the onboarding banner would mark the OAuth secret as\n * \"set\" for user B as soon as ANY user in the deployment connected the\n * provider, and user B would never see the prompt to connect.\n *\n * For the local-dev / single-tenant case, callers pass the dev sentinel\n * (`DEV_MODE_USER_EMAIL`) and we degrade to \"any row exists\" — preserving\n * the original behaviour for that single-user surface.\n */\nexport async function hasOAuthTokens(\n provider: string,\n owner: string,\n): Promise<boolean> {\n await ensureTable();\n const client = getDbExec();\n if (owner === \"local@localhost\") {\n const { rows } = await client.execute({\n sql: `SELECT 1 FROM oauth_tokens WHERE provider = ? LIMIT 1`,\n args: [provider],\n });\n return rows.length > 0;\n }\n const { rows } = await client.execute({\n sql: `SELECT 1 FROM oauth_tokens WHERE provider = ? AND owner = ? LIMIT 1`,\n args: [provider, owner],\n });\n return rows.length > 0;\n}\n"]}
1
+ {"version":3,"file":"store.js","sourceRoot":"","sources":["../../src/oauth-tokens/store.ts"],"names":[],"mappings":"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","sourcesContent":["import { getDbExec, isPostgres, intType, type DbExec } from \"../db/client.js\";\n\nlet _initPromise: Promise<void> | undefined;\n\nfunction oauthTokensTable(): string {\n return isPostgres() ? \"public.oauth_tokens\" : \"oauth_tokens\";\n}\n\nasync function ensureTable(): Promise<void> {\n if (!_initPromise) {\n _initPromise = (async () => {\n const client = getDbExec();\n const table = oauthTokensTable();\n await client.execute(`\n CREATE TABLE IF NOT EXISTS ${table} (\n provider TEXT NOT NULL,\n account_id TEXT NOT NULL,\n owner TEXT,\n tokens TEXT NOT NULL,\n updated_at ${intType()} NOT NULL,\n PRIMARY KEY (provider, account_id)\n )\n `);\n // Migration: add owner column to existing tables\n try {\n await client.execute(`ALTER TABLE ${table} ADD COLUMN owner TEXT`);\n } catch {\n // Column already exists\n }\n // Migration: add display_name column\n try {\n await client.execute(\n `ALTER TABLE ${table} ADD COLUMN display_name TEXT`,\n );\n } catch {\n // Column already exists\n }\n // Backfill: set owner = account_id for existing rows without an owner\n await client.execute(\n `UPDATE ${table} SET owner = account_id WHERE owner IS NULL`,\n );\n })();\n }\n return _initPromise;\n}\n\nexport async function getOAuthTokens(\n provider: string,\n accountId: string,\n): Promise<Record<string, unknown> | null> {\n await ensureTable();\n const client = getDbExec();\n const table = oauthTokensTable();\n const { rows } = await client.execute({\n sql: `SELECT tokens FROM ${table} WHERE provider = ? AND account_id = ?`,\n args: [provider, accountId],\n });\n if (rows.length === 0) return null;\n return JSON.parse(rows[0].tokens as string);\n}\n\n/**\n * Thrown when an OAuth save would re-bind an `(provider, account_id)` row\n * to a different owner than already holds it. Callers should catch this and\n * surface a clean \"this account is already linked to another user\" message\n * to the requester rather than letting it propagate as a 500.\n *\n * Carries `statusCode = 409` so route handlers using h3's `createError` can\n * pass it straight through.\n */\nexport class OAuthAccountOwnedByOtherUserError extends Error {\n readonly statusCode = 409;\n readonly provider: string;\n readonly accountId: string;\n readonly existingOwner: string;\n readonly attemptedOwner: string;\n constructor(opts: {\n provider: string;\n accountId: string;\n existingOwner: string;\n attemptedOwner: string;\n }) {\n super(\n `OAuth account ${opts.provider}:${opts.accountId} is already linked to another user — refusing to overwrite the owner.`,\n );\n this.name = \"OAuthAccountOwnedByOtherUserError\";\n this.provider = opts.provider;\n this.accountId = opts.accountId;\n this.existingOwner = opts.existingOwner;\n this.attemptedOwner = opts.attemptedOwner;\n }\n}\n\n/**\n * Save OAuth tokens. The `owner` parameter specifies which user owns this\n * account — defaults to `accountId` (the account itself is the owner).\n * For multi-account support, pass the logged-in user's email as owner.\n *\n * If the account already exists and is owned by a different user, throws\n * `OAuthAccountOwnedByOtherUserError` (statusCode 409) to prevent silently\n * stealing another user's linked account.\n *\n * Read + write happen as a single linearised batch (Postgres) or paired\n * statements (SQLite). On both backends the per-row PK serialises concurrent\n * writes for the same `(provider, account_id)` so the owner check cannot be\n * raced by an attacker calling saveOAuthTokens twice in flight — the second\n * caller sees the first caller's owner row and raises 409.\n */\nexport async function saveOAuthTokens(\n provider: string,\n accountId: string,\n tokens: Record<string, unknown>,\n owner?: string,\n): Promise<void> {\n await ensureTable();\n const client = getDbExec();\n const table = oauthTokensTable();\n\n // Never store \"local@localhost\" as owner — it creates shared-ownership bugs\n const sanitizedOwner = owner === \"local@localhost\" ? accountId : owner;\n\n // Read the current row before deciding what to write. We use this to\n // (a) preserve owner / display_name when this is a token refresh (no\n // owner argument), and (b) reject the write when the caller is trying\n // to overwrite a row owned by someone else.\n let resolvedOwner = sanitizedOwner ?? accountId;\n let existingDisplayName: string | null = null;\n let existingOwner: string | null = null;\n let existingTokens: Record<string, unknown> | null = null;\n const { rows: existing } = await client.execute({\n sql: `SELECT owner, display_name, tokens FROM ${table} WHERE provider = ? AND account_id = ?`,\n args: [provider, accountId],\n });\n if (existing.length > 0) {\n existingOwner = (existing[0].owner as string) ?? null;\n existingDisplayName = (existing[0].display_name as string) ?? null;\n existingTokens = JSON.parse((existing[0].tokens as string) ?? \"{}\");\n }\n\n if (!owner) {\n // Token-refresh path: keep the existing owner/displayName unchanged.\n if (existingOwner) resolvedOwner = existingOwner;\n } else if (existingOwner === \"local@localhost\" && sanitizedOwner) {\n // Legacy dev rows from older OAuth flows were sometimes stored under the\n // synthetic local user. A successful OAuth callback proves control of the\n // Google account, so let the reconnect repair that owner in place.\n resolvedOwner = sanitizedOwner;\n } else if (\n existingOwner &&\n sanitizedOwner &&\n existingOwner !== sanitizedOwner\n ) {\n // Refuse to silently re-bind an account from one user to another.\n // This is the case the docstring promised but the previous\n // implementation didn't enforce — `ON CONFLICT DO UPDATE SET\n // owner=EXCLUDED.owner` would have overwritten the prior owner.\n throw new OAuthAccountOwnedByOtherUserError({\n provider,\n accountId,\n existingOwner,\n attemptedOwner: sanitizedOwner,\n });\n }\n\n const cleanedIncomingTokens = Object.fromEntries(\n Object.entries(tokens).filter(([, value]) => value !== undefined),\n );\n const tokensToStore = {\n ...(existingTokens ?? {}),\n ...cleanedIncomingTokens,\n };\n\n await client.execute({\n sql: isPostgres()\n ? `INSERT INTO ${table} (provider, account_id, owner, display_name, tokens, updated_at) VALUES (?, ?, ?, ?, ?, ?) ON CONFLICT (provider, account_id) DO UPDATE SET owner=EXCLUDED.owner, display_name=COALESCE(EXCLUDED.display_name, ${table}.display_name), tokens=EXCLUDED.tokens, updated_at=EXCLUDED.updated_at`\n : `INSERT OR REPLACE INTO ${table} (provider, account_id, owner, display_name, tokens, updated_at) VALUES (?, ?, ?, ?, ?, ?)`,\n args: [\n provider,\n accountId,\n resolvedOwner,\n existingDisplayName,\n JSON.stringify(tokensToStore),\n Date.now(),\n ],\n });\n}\n\nexport async function deleteOAuthTokens(\n provider: string,\n accountId?: string,\n): Promise<number> {\n await ensureTable();\n const client = getDbExec();\n const table = oauthTokensTable();\n if (accountId) {\n const result = await client.execute({\n sql: `DELETE FROM ${table} WHERE provider = ? AND account_id = ?`,\n args: [provider, accountId],\n });\n return result.rowsAffected;\n }\n const result = await client.execute({\n sql: `DELETE FROM ${table} WHERE provider = ?`,\n args: [provider],\n });\n return result.rowsAffected;\n}\n\nexport async function listOAuthAccounts(provider: string): Promise<\n Array<{\n accountId: string;\n owner: string | null;\n tokens: Record<string, unknown>;\n }>\n> {\n await ensureTable();\n const client = getDbExec();\n const table = oauthTokensTable();\n const { rows } = await client.execute({\n sql: `SELECT account_id, owner, tokens FROM ${table} WHERE provider = ?`,\n args: [provider],\n });\n return rows.map((row) => ({\n accountId: row.account_id as string,\n owner: (row.owner as string) ?? null,\n tokens: JSON.parse(row.tokens as string),\n }));\n}\n\n/**\n * List all OAuth accounts owned by a specific user.\n * In multi-account mode, a user may have connected multiple Google accounts.\n */\nexport async function listOAuthAccountsByOwner(\n provider: string,\n owner: string,\n): Promise<\n Array<{\n accountId: string;\n displayName: string | null;\n tokens: Record<string, unknown>;\n }>\n> {\n await ensureTable();\n const client = getDbExec();\n const table = oauthTokensTable();\n const { rows } = await client.execute({\n sql: `SELECT account_id, display_name, tokens FROM ${table} WHERE provider = ? AND owner = ?`,\n args: [provider, owner],\n });\n return rows.map((row) => ({\n accountId: row.account_id as string,\n displayName: (row.display_name as string) ?? null,\n tokens: JSON.parse(row.tokens as string),\n }));\n}\n\n/**\n * Set the display name for an OAuth account (e.g. Google profile name).\n */\nexport async function setOAuthDisplayName(\n provider: string,\n accountId: string,\n displayName: string,\n): Promise<void> {\n await ensureTable();\n const client = getDbExec();\n const table = oauthTokensTable();\n await client.execute({\n sql: `UPDATE ${table} SET display_name = ? WHERE provider = ? AND account_id = ?`,\n args: [displayName, provider, accountId],\n });\n}\n\n/**\n * Check whether a specific user has tokens for a provider.\n *\n * `owner` is REQUIRED. The previous unscoped form leaked information\n * across users — the onboarding banner would mark the OAuth secret as\n * \"set\" for user B as soon as ANY user in the deployment connected the\n * provider, and user B would never see the prompt to connect.\n *\n * For the local-dev / single-tenant case, callers pass the dev sentinel\n * (`DEV_MODE_USER_EMAIL`) and we degrade to \"any row exists\" — preserving\n * the original behaviour for that single-user surface.\n */\nexport async function hasOAuthTokens(\n provider: string,\n owner: string,\n): Promise<boolean> {\n await ensureTable();\n const client = getDbExec();\n const table = oauthTokensTable();\n if (owner === \"local@localhost\") {\n const { rows } = await client.execute({\n sql: `SELECT 1 FROM ${table} WHERE provider = ? LIMIT 1`,\n args: [provider],\n });\n return rows.length > 0;\n }\n const { rows } = await client.execute({\n sql: `SELECT 1 FROM ${table} WHERE provider = ? AND owner = ? LIMIT 1`,\n args: [provider, owner],\n });\n return rows.length > 0;\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"exec.d.ts","sourceRoot":"","sources":["../../../src/scripts/db/exec.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAsiBH,wBAA8B,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAsJlE"}
1
+ {"version":3,"file":"exec.d.ts","sourceRoot":"","sources":["../../../src/scripts/db/exec.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAwiBH,wBAA8B,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CA4JlE"}
@@ -18,6 +18,7 @@ import { createClient } from "@libsql/client";
18
18
  import { getDatabaseUrl, getDatabaseAuthToken } from "../../db/client.js";
19
19
  import { parseArgs, fail } from "../utils.js";
20
20
  import { buildScopingPostgres, buildScopingSqlite, } from "./scoping.js";
21
+ import { assertNoSensitiveFrameworkTables } from "./safety.js";
21
22
  function isPostgresUrl(url) {
22
23
  return url.startsWith("postgres://") || url.startsWith("postgresql://");
23
24
  }
@@ -158,6 +159,7 @@ function validateWriteSql(sql, index) {
158
159
  fail(`Statement ${index}: only ${allowed.join(", ")} statements are allowed. ` +
159
160
  `Dangerous operations like DROP, ATTACH, VACUUM, DETACH, CREATE, and ALTER are blocked.`);
160
161
  }
162
+ assertNoSensitiveFrameworkTables(normalized, "write");
161
163
  return normalized;
162
164
  }
163
165
  function convertQuestionMarksToPostgresParams(sql) {
@@ -480,30 +482,37 @@ Options:
480
482
  const scoping = await buildScopingPostgres(pgSql);
481
483
  const results = [];
482
484
  await pgSql.begin(async (tx) => {
483
- // For UPDATE/DELETE: temp views scope to current user's rows. Creating
484
- // them inside the transaction keeps multi-statement batches on one
485
- // connection and avoids cross-call temp-view leakage.
486
- for (const stmt of scoping.setup) {
487
- await tx.unsafe(stmt);
488
- }
489
- for (let i = 0; i < statements.length; i++) {
490
- const statement = statements[i];
491
- const hasReturning = /\bRETURNING\b/i.test(statement.sql);
492
- const finalSql = normalizePostgresSql(injectOwnership(statement.sql, scoping), statement.args);
493
- try {
494
- const result = statement.args.length > 0
495
- ? await tx.unsafe(finalSql, statement.args)
496
- : await tx.unsafe(finalSql);
497
- const rows = hasReturning && result.length > 0 ? Array.from(result) : [];
498
- results.push({
499
- index: i + 1,
500
- sql: finalSql,
501
- changes: result.count ?? 0,
502
- rows,
503
- });
485
+ try {
486
+ // For UPDATE/DELETE: temp views scope to current user's rows.
487
+ // Creating and dropping them inside the same transaction keeps
488
+ // pooled Postgres backends from retaining session-local views.
489
+ for (const stmt of scoping.setup) {
490
+ await tx.unsafe(stmt);
504
491
  }
505
- catch (err) {
506
- throw new Error(`Statement ${i + 1} failed: ${err?.message ?? String(err)}`);
492
+ for (let i = 0; i < statements.length; i++) {
493
+ const statement = statements[i];
494
+ const hasReturning = /\bRETURNING\b/i.test(statement.sql);
495
+ const finalSql = normalizePostgresSql(injectOwnership(statement.sql, scoping), statement.args);
496
+ try {
497
+ const result = statement.args.length > 0
498
+ ? await tx.unsafe(finalSql, statement.args)
499
+ : await tx.unsafe(finalSql);
500
+ const rows = hasReturning && result.length > 0 ? Array.from(result) : [];
501
+ results.push({
502
+ index: i + 1,
503
+ sql: finalSql,
504
+ changes: result.count ?? 0,
505
+ rows,
506
+ });
507
+ }
508
+ catch (err) {
509
+ throw new Error(`Statement ${i + 1} failed: ${err?.message ?? String(err)}`);
510
+ }
511
+ }
512
+ }
513
+ finally {
514
+ for (const stmt of scoping.teardown) {
515
+ await tx.unsafe(stmt).catch(() => { });
507
516
  }
508
517
  }
509
518
  });