@vellumai/assistant 0.5.1 → 0.5.2

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 (338) hide show
  1. package/ARCHITECTURE.md +54 -54
  2. package/docs/architecture/integrations.md +62 -67
  3. package/docs/credential-execution-service.md +3 -3
  4. package/package.json +1 -1
  5. package/src/__tests__/agent-loop.test.ts +111 -0
  6. package/src/__tests__/always-loaded-tools-guard.test.ts +3 -4
  7. package/src/__tests__/app-builder-tool-scripts.test.ts +13 -151
  8. package/src/__tests__/app-dir-path-guard.test.ts +78 -0
  9. package/src/__tests__/app-executors.test.ts +1 -291
  10. package/src/__tests__/app-git-history.test.ts +4 -4
  11. package/src/__tests__/app-routes-csp.test.ts +1 -0
  12. package/src/__tests__/app-store-dir-names.test.ts +426 -0
  13. package/src/__tests__/attachments-store.test.ts +169 -21
  14. package/src/__tests__/attachments.test.ts +115 -1
  15. package/src/__tests__/btw-routes.test.ts +1 -0
  16. package/src/__tests__/canonical-guardian-store.test.ts +38 -0
  17. package/src/__tests__/channel-reply-delivery.test.ts +55 -0
  18. package/src/__tests__/checker.test.ts +54 -0
  19. package/src/__tests__/claude-code-skill-regression.test.ts +2 -0
  20. package/src/__tests__/claude-code-tool-profiles.test.ts +2 -0
  21. package/src/__tests__/compaction.benchmark.test.ts +2 -1
  22. package/src/__tests__/config-schema-cmd.test.ts +68 -21
  23. package/src/__tests__/config-schema.test.ts +1 -1
  24. package/src/__tests__/conversation-agent-loop-overflow.test.ts +149 -5
  25. package/src/__tests__/conversation-agent-loop.test.ts +290 -2
  26. package/src/__tests__/conversation-attachments.test.ts +17 -19
  27. package/src/__tests__/conversation-disk-view-integration.test.ts +277 -0
  28. package/src/__tests__/conversation-disk-view.test.ts +810 -0
  29. package/src/__tests__/conversation-error.test.ts +1 -1
  30. package/src/__tests__/conversation-fork-crud.test.ts +551 -0
  31. package/src/__tests__/conversation-fork-route.test.ts +386 -0
  32. package/src/__tests__/conversation-history-web-search.test.ts +1 -1
  33. package/src/__tests__/conversation-key-store-disk-view.test.ts +130 -0
  34. package/src/__tests__/conversation-media-retry.test.ts +8 -2
  35. package/src/__tests__/conversation-queue.test.ts +36 -1
  36. package/src/__tests__/conversation-routes-disk-view.test.ts +439 -0
  37. package/src/__tests__/conversation-routes-guardian-reply.test.ts +2 -2
  38. package/src/__tests__/conversation-routes-slash-commands.test.ts +2 -7
  39. package/src/__tests__/conversation-runtime-assembly.test.ts +17 -2
  40. package/src/__tests__/conversation-skill-tools.test.ts +4 -9
  41. package/src/__tests__/conversation-slash-commands.test.ts +149 -0
  42. package/src/__tests__/conversation-store.test.ts +24 -21
  43. package/src/__tests__/conversation-surfaces-state-update.test.ts +246 -0
  44. package/src/__tests__/conversation-surfaces-task-progress.test.ts +1 -0
  45. package/src/__tests__/conversation-title-service.test.ts +137 -0
  46. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +25 -315
  47. package/src/__tests__/conversation-tool-setup-memory-scope.test.ts +1 -0
  48. package/src/__tests__/conversation-tool-setup-side-effect-flag.test.ts +1 -0
  49. package/src/__tests__/conversation-workspace-cache-state.test.ts +44 -2
  50. package/src/__tests__/conversation-workspace-injection.test.ts +11 -0
  51. package/src/__tests__/credential-security-invariants.test.ts +3 -0
  52. package/src/__tests__/credential-vault-unit.test.ts +5 -10
  53. package/src/__tests__/cu-unified-flow.test.ts +1 -0
  54. package/src/__tests__/db-conversation-fork-lineage-migration.test.ts +241 -0
  55. package/src/__tests__/db-llm-request-log-provider-migration.test.ts +214 -0
  56. package/src/__tests__/diagnostics-export.test.ts +70 -1
  57. package/src/__tests__/first-greeting.test.ts +80 -0
  58. package/src/__tests__/gateway-only-guard.test.ts +1 -0
  59. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +3 -7
  60. package/src/__tests__/history-repair.test.ts +32 -10
  61. package/src/__tests__/http-conversation-lineage.test.ts +251 -0
  62. package/src/__tests__/image-source-path-reinject.test.ts +136 -0
  63. package/src/__tests__/llm-context-normalization.test.ts +1116 -0
  64. package/src/__tests__/llm-context-route-provider.test.ts +217 -0
  65. package/src/__tests__/llm-request-log-turn-query.test.ts +270 -0
  66. package/src/__tests__/media-generate-image.test.ts +47 -94
  67. package/src/__tests__/memory-lifecycle-e2e.test.ts +3 -1
  68. package/src/__tests__/memory-recall-quality.test.ts +5 -5
  69. package/src/__tests__/migration-cross-version-compatibility.test.ts +4 -1
  70. package/src/__tests__/migration-export-http.test.ts +3 -1
  71. package/src/__tests__/migration-import-commit-http.test.ts +18 -4
  72. package/src/__tests__/migration-import-preflight-http.test.ts +1 -3
  73. package/src/__tests__/mime-builder.test.ts +3 -2
  74. package/src/__tests__/non-member-access-request.test.ts +12 -1
  75. package/src/__tests__/notification-decision-identity.test.ts +52 -0
  76. package/src/__tests__/oauth-apps-routes.test.ts +103 -0
  77. package/src/__tests__/oauth-store.test.ts +115 -0
  78. package/src/__tests__/provider-error-scenarios.test.ts +1 -3
  79. package/src/__tests__/provider-failover-actual-provider.test.ts +66 -0
  80. package/src/__tests__/recording-handler.test.ts +17 -0
  81. package/src/__tests__/registry.test.ts +3 -8
  82. package/src/__tests__/relay-server.test.ts +1 -1
  83. package/src/__tests__/runtime-attachment-metadata.test.ts +7 -3
  84. package/src/__tests__/schema-transforms.test.ts +165 -5
  85. package/src/__tests__/server-history-render.test.ts +2 -2
  86. package/src/__tests__/slack-app-setup-skill-regression.test.ts +3 -1
  87. package/src/__tests__/slack-inbound-verification.test.ts +2 -2
  88. package/src/__tests__/starter-task-flow.test.ts +1 -0
  89. package/src/__tests__/suggestion-routes.test.ts +443 -0
  90. package/src/__tests__/swarm-conversation-integration.test.ts +1 -0
  91. package/src/__tests__/swarm-recursion.test.ts +1 -0
  92. package/src/__tests__/swarm-tool.test.ts +1 -0
  93. package/src/__tests__/tool-execution-abort-cleanup.test.ts +1 -0
  94. package/src/__tests__/tool-preview-lifecycle.test.ts +32 -5
  95. package/src/__tests__/top-level-renderer.test.ts +22 -0
  96. package/src/__tests__/turn-boundary-resolution.test.ts +243 -0
  97. package/src/__tests__/web-fetch.test.ts +6 -2
  98. package/src/__tests__/workspace-migration-006-services-config.test.ts +335 -0
  99. package/src/__tests__/workspace-migration-007-web-search-provider-rename.test.ts +312 -0
  100. package/src/__tests__/workspace-migration-009-backfill-conversation-disk-view.test.ts +278 -0
  101. package/src/__tests__/workspace-migration-010-app-dir-rename.test.ts +275 -0
  102. package/src/__tests__/workspace-migration-012-rename-conversation-disk-view-dirs.test.ts +77 -0
  103. package/src/__tests__/workspace-migration-013-repair-conversation-disk-view.test.ts +401 -0
  104. package/src/__tests__/workspace-migration-backfill-installation-id.test.ts +328 -0
  105. package/src/__tests__/workspace-migration-seed-device-id.test.ts +6 -10
  106. package/src/agent/attachments.ts +27 -1
  107. package/src/agent/loop.ts +29 -1
  108. package/src/avatar/traits-png-sync.ts +80 -25
  109. package/src/bundler/app-bundler.ts +4 -4
  110. package/src/calls/call-domain.ts +1 -0
  111. package/src/calls/voice-session-bridge.ts +1 -0
  112. package/src/cli/commands/auth.ts +92 -0
  113. package/src/cli/commands/avatar.ts +7 -6
  114. package/src/cli/commands/config.ts +2 -0
  115. package/src/cli/commands/oauth/providers.ts +29 -0
  116. package/src/cli/program.ts +12 -0
  117. package/src/cli.ts +15 -48
  118. package/src/config/bundled-skills/app-builder/SKILL.md +103 -28
  119. package/src/config/bundled-skills/app-builder/TOOLS.json +5 -199
  120. package/src/config/bundled-skills/app-builder/tools/{app-query.ts → app-refresh.ts} +2 -2
  121. package/src/config/bundled-skills/contacts/tools/google-contacts.ts +2 -3
  122. package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +6 -9
  123. package/src/config/bundled-skills/gmail/tools/gmail-attachments.ts +4 -6
  124. package/src/config/bundled-skills/gmail/tools/gmail-draft.ts +2 -3
  125. package/src/config/bundled-skills/gmail/tools/gmail-filters.ts +2 -3
  126. package/src/config/bundled-skills/gmail/tools/gmail-follow-up.ts +2 -3
  127. package/src/config/bundled-skills/gmail/tools/gmail-forward.ts +2 -3
  128. package/src/config/bundled-skills/gmail/tools/gmail-label.ts +4 -6
  129. package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +2 -3
  130. package/src/config/bundled-skills/gmail/tools/gmail-send-draft.ts +2 -3
  131. package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +2 -3
  132. package/src/config/bundled-skills/gmail/tools/gmail-trash.ts +2 -3
  133. package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +2 -3
  134. package/src/config/bundled-skills/gmail/tools/gmail-vacation.ts +2 -3
  135. package/src/config/bundled-skills/google-calendar/tools/shared.ts +1 -1
  136. package/src/config/bundled-skills/image-studio/SKILL.md +2 -2
  137. package/src/config/bundled-skills/image-studio/TOOLS.json +2 -2
  138. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +45 -72
  139. package/src/config/bundled-skills/media-processing/tools/extract-keyframes.ts +2 -2
  140. package/src/config/bundled-skills/messaging/tools/shared.ts +1 -1
  141. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +19 -3
  142. package/src/config/bundled-skills/skill-management/TOOLS.json +2 -2
  143. package/src/config/bundled-skills/slack/tools/shared.ts +19 -4
  144. package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +2 -3
  145. package/src/config/bundled-skills/transcribe/SKILL.md +1 -1
  146. package/src/config/bundled-skills/transcribe/TOOLS.json +2 -6
  147. package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +19 -83
  148. package/src/config/bundled-tool-registry.ts +2 -14
  149. package/src/config/feature-flag-registry.json +8 -0
  150. package/src/config/loader.ts +64 -0
  151. package/src/config/raw-config-utils.ts +30 -0
  152. package/src/config/schema-utils.ts +28 -7
  153. package/src/config/schema.ts +8 -0
  154. package/src/config/schemas/elevenlabs.ts +18 -0
  155. package/src/config/schemas/memory-lifecycle.ts +4 -2
  156. package/src/config/schemas/memory-storage.ts +1 -1
  157. package/src/config/schemas/services.ts +8 -6
  158. package/src/contacts/contact-store.ts +13 -6
  159. package/src/contacts/contacts-write.ts +0 -1
  160. package/src/context/window-manager.ts +13 -2
  161. package/src/daemon/conversation-agent-loop-handlers.ts +48 -7
  162. package/src/daemon/conversation-agent-loop.ts +56 -19
  163. package/src/daemon/conversation-attachments.ts +18 -36
  164. package/src/daemon/conversation-error.ts +2 -1
  165. package/src/daemon/conversation-history.ts +18 -4
  166. package/src/daemon/conversation-lifecycle.ts +39 -15
  167. package/src/daemon/conversation-messaging.ts +70 -26
  168. package/src/daemon/conversation-process.ts +58 -34
  169. package/src/daemon/conversation-runtime-assembly.ts +21 -38
  170. package/src/daemon/conversation-slash.ts +121 -256
  171. package/src/daemon/conversation-surfaces.ts +143 -20
  172. package/src/daemon/conversation-tool-setup.ts +0 -6
  173. package/src/daemon/conversation-workspace.ts +21 -1
  174. package/src/daemon/conversation.ts +51 -29
  175. package/src/daemon/first-greeting.ts +35 -0
  176. package/src/daemon/handlers/config-embeddings.ts +148 -0
  177. package/src/daemon/handlers/config-model.ts +71 -26
  178. package/src/daemon/handlers/conversations.ts +0 -23
  179. package/src/daemon/handlers/recording.ts +26 -21
  180. package/src/daemon/host-cu-proxy.ts +2 -2
  181. package/src/daemon/lifecycle.ts +106 -64
  182. package/src/daemon/message-protocol.ts +3 -0
  183. package/src/daemon/message-types/conversations.ts +19 -0
  184. package/src/daemon/message-types/messages.ts +1 -0
  185. package/src/daemon/message-types/shared.ts +2 -0
  186. package/src/daemon/message-types/surfaces.ts +2 -0
  187. package/src/daemon/message-types/upgrades.ts +23 -0
  188. package/src/daemon/server.ts +83 -12
  189. package/src/daemon/shutdown-handlers.ts +8 -5
  190. package/src/daemon/startup-error.ts +9 -0
  191. package/src/daemon/tool-side-effects.ts +11 -28
  192. package/src/events/tool-permission-telemetry-listener.ts +1 -3
  193. package/src/instrument.ts +0 -4
  194. package/src/media/app-icon-generator.ts +2 -2
  195. package/src/memory/app-git-service.ts +28 -16
  196. package/src/memory/app-store.ts +230 -41
  197. package/src/memory/attachments-store.ts +558 -130
  198. package/src/memory/conversation-attention-store.ts +70 -0
  199. package/src/memory/conversation-crud.ts +442 -3
  200. package/src/memory/conversation-directories.ts +125 -0
  201. package/src/memory/conversation-disk-view.ts +390 -0
  202. package/src/memory/conversation-key-store.ts +17 -5
  203. package/src/memory/conversation-queries.ts +5 -1
  204. package/src/memory/conversation-title-service.ts +21 -49
  205. package/src/memory/db-init.ts +28 -0
  206. package/src/memory/embedding-backend.ts +42 -53
  207. package/src/memory/embedding-gemini.test.ts +4 -4
  208. package/src/memory/embedding-local.ts +1 -3
  209. package/src/memory/embedding-ollama.ts +1 -3
  210. package/src/memory/embedding-openai.ts +1 -3
  211. package/src/memory/indexer.ts +9 -7
  212. package/src/memory/items-extractor.ts +42 -13
  213. package/src/memory/job-handlers/conversation-starters.ts +6 -1
  214. package/src/memory/job-handlers/embedding.test.ts +1 -4
  215. package/src/memory/llm-request-log-store.ts +100 -1
  216. package/src/memory/migrations/102-alter-table-columns.ts +5 -0
  217. package/src/memory/migrations/146-schedule-oneshot-routing.ts +3 -3
  218. package/src/memory/migrations/147-migrate-reminders-to-schedules.ts +66 -70
  219. package/src/memory/migrations/148-drop-reminders-table.ts +5 -9
  220. package/src/memory/migrations/160-drop-loopback-port-column.ts +1 -3
  221. package/src/memory/migrations/174-rename-thread-starters-table.ts +0 -7
  222. package/src/memory/migrations/178-oauth-providers-managed-service-config-key.ts +15 -0
  223. package/src/memory/migrations/179-llm-request-log-message-id.ts +16 -0
  224. package/src/memory/migrations/180-backfill-inline-attachments-to-disk.ts +66 -0
  225. package/src/memory/migrations/181-rename-thread-starters-checkpoints.ts +46 -0
  226. package/src/memory/migrations/182-oauth-providers-display-metadata.ts +20 -0
  227. package/src/memory/migrations/183-add-conversation-fork-lineage.ts +22 -0
  228. package/src/memory/migrations/184-llm-request-log-provider.ts +12 -0
  229. package/src/memory/migrations/index.ts +7 -0
  230. package/src/memory/migrations/registry.ts +13 -0
  231. package/src/memory/retriever.test.ts +601 -2
  232. package/src/memory/retriever.ts +85 -9
  233. package/src/memory/schema/conversations.ts +6 -0
  234. package/src/memory/schema/infrastructure.ts +13 -7
  235. package/src/memory/schema/oauth.ts +6 -0
  236. package/src/messaging/providers/gmail/mime-builder.ts +3 -1
  237. package/src/notifications/copy-composer.ts +26 -0
  238. package/src/notifications/decision-engine.ts +14 -1
  239. package/src/notifications/emit-signal.ts +1 -1
  240. package/src/notifications/signal.ts +36 -0
  241. package/src/oauth/byo-connection.test.ts +1 -45
  242. package/src/oauth/byo-connection.ts +2 -8
  243. package/src/oauth/connect-orchestrator.ts +15 -11
  244. package/src/oauth/connection-resolver.test.ts +191 -0
  245. package/src/oauth/connection-resolver.ts +66 -38
  246. package/src/oauth/connection.ts +0 -1
  247. package/src/oauth/oauth-store.ts +97 -47
  248. package/src/oauth/platform-connection.test.ts +0 -1
  249. package/src/oauth/platform-connection.ts +11 -3
  250. package/src/oauth/seed-providers.ts +78 -3
  251. package/src/oauth/token-persistence.ts +16 -10
  252. package/src/permissions/checker.ts +71 -8
  253. package/src/prompts/templates/BOOTSTRAP.md +2 -0
  254. package/src/providers/anthropic/client.ts +8 -1
  255. package/src/providers/failover.ts +4 -1
  256. package/src/providers/gemini/client.ts +50 -0
  257. package/src/providers/model-catalog.ts +92 -0
  258. package/src/providers/model-intents.ts +29 -20
  259. package/src/providers/openai/client.ts +49 -0
  260. package/src/providers/types.ts +2 -0
  261. package/src/runtime/access-request-helper.ts +16 -7
  262. package/src/runtime/auth/credential-service.ts +3 -1
  263. package/src/runtime/auth/route-policy.ts +14 -1
  264. package/src/runtime/btw-sidechain.ts +101 -0
  265. package/src/runtime/channel-reply-delivery.ts +17 -1
  266. package/src/runtime/http-router.ts +3 -1
  267. package/src/runtime/http-server.ts +196 -141
  268. package/src/runtime/http-types.ts +1 -0
  269. package/src/runtime/migrations/vbundle-builder.ts +5 -1
  270. package/src/runtime/routes/access-request-decision.ts +41 -0
  271. package/src/runtime/routes/app-management-routes.ts +6 -3
  272. package/src/runtime/routes/app-routes.ts +7 -3
  273. package/src/runtime/routes/approval-routes.ts +1 -0
  274. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +34 -2
  275. package/src/runtime/routes/attachment-routes.ts +45 -15
  276. package/src/runtime/routes/btw-routes.ts +21 -61
  277. package/src/runtime/routes/conversation-management-routes.ts +68 -0
  278. package/src/runtime/routes/conversation-query-routes.ts +180 -10
  279. package/src/runtime/routes/conversation-routes.ts +222 -28
  280. package/src/runtime/routes/conversation-starter-routes.ts +9 -11
  281. package/src/runtime/routes/diagnostics-routes.ts +1 -0
  282. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +2 -2
  283. package/src/runtime/routes/llm-context-normalization.ts +1199 -0
  284. package/src/runtime/routes/log-export-routes.ts +3 -0
  285. package/src/runtime/routes/memory-item-routes.test.ts +34 -0
  286. package/src/runtime/routes/memory-item-routes.ts +4 -0
  287. package/src/runtime/routes/migration-routes.ts +4 -1
  288. package/src/runtime/routes/oauth-apps.ts +291 -0
  289. package/src/runtime/routes/secret-routes.ts +28 -1
  290. package/src/runtime/routes/settings-routes.ts +14 -0
  291. package/src/runtime/routes/trace-event-routes.ts +4 -1
  292. package/src/schedule/schedule-store.ts +9 -21
  293. package/src/security/secure-keys.ts +21 -0
  294. package/src/signals/bash.ts +1 -1
  295. package/src/swarm/backend-claude-code.ts +3 -6
  296. package/src/telemetry/usage-telemetry-reporter.test.ts +3 -2
  297. package/src/telemetry/usage-telemetry-reporter.ts +3 -1
  298. package/src/tools/AGENTS.md +6 -10
  299. package/src/tools/apps/executors.ts +17 -232
  300. package/src/tools/claude-code/claude-code.ts +2 -3
  301. package/src/tools/credentials/vault.ts +7 -12
  302. package/src/tools/host-filesystem/read.ts +13 -10
  303. package/src/tools/network/__tests__/web-search.test.ts +4 -2
  304. package/src/tools/schedule/list.ts +2 -7
  305. package/src/tools/schema-transforms.ts +5 -0
  306. package/src/tools/shared/filesystem/format-diff.ts +2 -7
  307. package/src/tools/skills/execute.ts +1 -1
  308. package/src/tools/tool-manifest.ts +0 -6
  309. package/src/tools/ui-surface/definitions.ts +2 -2
  310. package/src/util/device-id.ts +28 -5
  311. package/src/util/platform.ts +6 -0
  312. package/src/util/pricing.ts +1 -0
  313. package/src/util/retry.ts +1 -3
  314. package/src/workspace/migrations/002-backfill-installation-id.ts +23 -12
  315. package/src/workspace/migrations/003-seed-device-id.ts +3 -4
  316. package/src/workspace/migrations/006-services-config.ts +5 -0
  317. package/src/workspace/migrations/008-voice-timeout-and-max-steps.ts +12 -0
  318. package/src/workspace/migrations/009-backfill-conversation-disk-view.ts +10 -0
  319. package/src/workspace/migrations/010-app-dir-rename.ts +223 -0
  320. package/src/workspace/migrations/012-rename-conversation-disk-view-dirs.ts +64 -0
  321. package/src/workspace/migrations/013-repair-conversation-disk-view.ts +11 -0
  322. package/src/workspace/migrations/rebuild-conversation-disk-view.ts +186 -0
  323. package/src/workspace/migrations/registry.ts +10 -0
  324. package/src/workspace/top-level-renderer.ts +12 -0
  325. package/src/__tests__/asset-materialize-tool.test.ts +0 -523
  326. package/src/__tests__/asset-search-tool.test.ts +0 -536
  327. package/src/__tests__/fixtures/media-reuse-fixtures.ts +0 -56
  328. package/src/__tests__/media-reuse-story.e2e.test.ts +0 -762
  329. package/src/__tests__/media-visibility-policy.test.ts +0 -190
  330. package/src/config/bundled-skills/app-builder/tools/app-file-edit.ts +0 -14
  331. package/src/config/bundled-skills/app-builder/tools/app-file-list.ts +0 -13
  332. package/src/config/bundled-skills/app-builder/tools/app-file-read.ts +0 -21
  333. package/src/config/bundled-skills/app-builder/tools/app-file-write.ts +0 -14
  334. package/src/config/bundled-skills/app-builder/tools/app-list.ts +0 -13
  335. package/src/config/bundled-skills/app-builder/tools/app-update.ts +0 -23
  336. package/src/daemon/media-visibility-policy.ts +0 -59
  337. package/src/tools/assets/materialize.ts +0 -248
  338. package/src/tools/assets/search.ts +0 -400
@@ -0,0 +1,191 @@
1
+ import { beforeEach, describe, expect, mock, test } from "bun:test";
2
+
3
+ // ---------------------------------------------------------------------------
4
+ // Mutable mock state
5
+ // ---------------------------------------------------------------------------
6
+
7
+ let mockProvider: Record<string, unknown> | undefined;
8
+ let mockConnection: Record<string, unknown> | undefined;
9
+ let mockAccessToken: string | undefined;
10
+ let mockConfig: Record<string, unknown> = {};
11
+ let mockManagedProxyCtx = {
12
+ enabled: false,
13
+ platformBaseUrl: "",
14
+ assistantApiKey: "",
15
+ };
16
+ let mockAssistantId = "";
17
+
18
+ // ---------------------------------------------------------------------------
19
+ // Module mocks (must precede imports of the module under test)
20
+ // ---------------------------------------------------------------------------
21
+
22
+ mock.module("../util/logger.js", () => ({
23
+ getLogger: () =>
24
+ new Proxy({} as Record<string, unknown>, {
25
+ get: () => () => {},
26
+ }),
27
+ }));
28
+
29
+ mock.module("./oauth-store.js", () => ({
30
+ getProvider: () => mockProvider,
31
+ getActiveConnection: (
32
+ _pk: string,
33
+ opts?: { clientId?: string; account?: string },
34
+ ) => {
35
+ if (opts?.clientId && mockConnection?.clientId !== opts.clientId)
36
+ return undefined;
37
+ if (opts?.account && mockConnection?.accountInfo !== opts.account)
38
+ return undefined;
39
+ return mockConnection;
40
+ },
41
+ }));
42
+
43
+ mock.module("../security/secure-keys.js", () => ({
44
+ getSecureKeyAsync: async () => mockAccessToken,
45
+ }));
46
+
47
+ mock.module("../config/loader.js", () => ({
48
+ getConfig: () => mockConfig,
49
+ }));
50
+
51
+ mock.module("../config/env.js", () => ({
52
+ getPlatformAssistantId: () => mockAssistantId,
53
+ }));
54
+
55
+ mock.module("../providers/managed-proxy/context.js", () => ({
56
+ resolveManagedProxyContext: async () => mockManagedProxyCtx,
57
+ }));
58
+
59
+ // ---------------------------------------------------------------------------
60
+ // Import the module under test (after all mocks are registered)
61
+ // ---------------------------------------------------------------------------
62
+
63
+ import { BYOOAuthConnection } from "./byo-connection.js";
64
+ import { resolveOAuthConnection } from "./connection-resolver.js";
65
+ import { PlatformOAuthConnection } from "./platform-connection.js";
66
+
67
+ // ---------------------------------------------------------------------------
68
+ // Helpers
69
+ // ---------------------------------------------------------------------------
70
+
71
+ function setupDefaults(): void {
72
+ mockProvider = {
73
+ providerKey: "integration:google",
74
+ baseUrl: "https://gmail.googleapis.com/gmail/v1/users/me",
75
+ managedServiceConfigKey: null,
76
+ };
77
+ mockConnection = {
78
+ id: "conn-1",
79
+ providerKey: "integration:google",
80
+ oauthAppId: "app-1",
81
+ accountInfo: "user@example.com",
82
+ grantedScopes: JSON.stringify(["scope-a", "scope-b"]),
83
+ status: "active",
84
+ clientId: "client-1",
85
+ };
86
+ mockAccessToken = "tok-valid";
87
+ mockConfig = {
88
+ services: {
89
+ inference: {
90
+ mode: "your-own",
91
+ provider: "anthropic",
92
+ model: "claude-opus-4-6",
93
+ },
94
+ "image-generation": {
95
+ mode: "your-own",
96
+ provider: "gemini",
97
+ model: "gemini-3.1-flash-image-preview",
98
+ },
99
+ "web-search": { mode: "your-own", provider: "inference-provider-native" },
100
+ "google-oauth": { mode: "managed" },
101
+ },
102
+ };
103
+ mockManagedProxyCtx = {
104
+ enabled: true,
105
+ platformBaseUrl: "https://platform.example.com",
106
+ assistantApiKey: "sk-test-key",
107
+ };
108
+ mockAssistantId = "asst-123";
109
+ }
110
+
111
+ // ---------------------------------------------------------------------------
112
+ // Tests
113
+ // ---------------------------------------------------------------------------
114
+
115
+ describe("resolveOAuthConnection", () => {
116
+ beforeEach(() => {
117
+ setupDefaults();
118
+ });
119
+
120
+ test("returns BYOOAuthConnection when provider has no managedServiceConfigKey", async () => {
121
+ const result = await resolveOAuthConnection("integration:google");
122
+ expect(result).toBeInstanceOf(BYOOAuthConnection);
123
+ expect(result.id).toBe("conn-1");
124
+ expect(result.providerKey).toBe("integration:google");
125
+ });
126
+
127
+ test("returns PlatformOAuthConnection when managed mode is active", async () => {
128
+ mockProvider!.managedServiceConfigKey = "google-oauth";
129
+
130
+ const result = await resolveOAuthConnection("integration:google");
131
+ expect(result).toBeInstanceOf(PlatformOAuthConnection);
132
+ expect(result.id).toBe("integration:google");
133
+ expect(result.providerKey).toBe("integration:google");
134
+ expect(result.accountInfo).toBeNull();
135
+ });
136
+
137
+ test("passes account through to PlatformOAuthConnection", async () => {
138
+ mockProvider!.managedServiceConfigKey = "google-oauth";
139
+
140
+ const result = await resolveOAuthConnection("integration:google", {
141
+ account: "user@example.com",
142
+ });
143
+ expect(result).toBeInstanceOf(PlatformOAuthConnection);
144
+ expect(result.accountInfo).toBe("user@example.com");
145
+ });
146
+
147
+ test("returns BYOOAuthConnection when service config mode is your-own", async () => {
148
+ mockProvider!.managedServiceConfigKey = "google-oauth";
149
+ (mockConfig.services as Record<string, unknown>)["google-oauth"] = {
150
+ mode: "your-own",
151
+ };
152
+
153
+ const result = await resolveOAuthConnection("integration:google");
154
+ expect(result).toBeInstanceOf(BYOOAuthConnection);
155
+ expect(result.id).toBe("conn-1");
156
+ });
157
+
158
+ test("managed path does not require a local connection row", async () => {
159
+ mockProvider!.managedServiceConfigKey = "google-oauth";
160
+ mockConnection = undefined;
161
+ mockAccessToken = undefined;
162
+
163
+ const result = await resolveOAuthConnection("integration:google");
164
+ expect(result).toBeInstanceOf(PlatformOAuthConnection);
165
+ });
166
+
167
+ test("managed path ignores clientId option", async () => {
168
+ mockProvider!.managedServiceConfigKey = "google-oauth";
169
+
170
+ const result = await resolveOAuthConnection("integration:google", {
171
+ clientId: "some-client-id",
172
+ });
173
+ expect(result).toBeInstanceOf(PlatformOAuthConnection);
174
+ });
175
+
176
+ test("BYO path narrows by clientId when provided", async () => {
177
+ const result = await resolveOAuthConnection("integration:google", {
178
+ clientId: "client-1",
179
+ });
180
+ expect(result).toBeInstanceOf(BYOOAuthConnection);
181
+ expect(result.id).toBe("conn-1");
182
+ });
183
+
184
+ test("BYO path returns no credential when clientId does not match", async () => {
185
+ await expect(
186
+ resolveOAuthConnection("integration:google", {
187
+ clientId: "wrong-client",
188
+ }),
189
+ ).rejects.toThrow(/No active OAuth connection found/);
190
+ });
191
+ });
@@ -1,72 +1,100 @@
1
+ import { getPlatformAssistantId } from "../config/env.js";
2
+ import { getConfig } from "../config/loader.js";
3
+ import { type Services, ServicesSchema } from "../config/schemas/services.js";
4
+ import { resolveManagedProxyContext } from "../providers/managed-proxy/context.js";
1
5
  import { getSecureKeyAsync } from "../security/secure-keys.js";
2
6
  import { BYOOAuthConnection } from "./byo-connection.js";
3
7
  import type { OAuthConnection } from "./connection.js";
4
- import {
5
- getApp,
6
- getConnectionByProvider,
7
- getConnectionByProviderAndAccount,
8
- getProvider,
9
- } from "./oauth-store.js";
8
+ import { getActiveConnection, getProvider } from "./oauth-store.js";
9
+ import { PlatformOAuthConnection } from "./platform-connection.js";
10
+
11
+ export interface ResolveOAuthConnectionOptions {
12
+ /** OAuth app client ID — narrows to a specific app when multiple BYO apps
13
+ * exist for the same provider. */
14
+ clientId?: string;
15
+ /** Account identifier (e.g. email, username) — disambiguates when multiple
16
+ * accounts are connected for the same provider. Best-effort: not guaranteed
17
+ * to be present on all connections. */
18
+ account?: string;
19
+ }
10
20
 
11
21
  /**
12
- * Resolve an OAuthConnection for a given credential service.
22
+ * Resolve an OAuthConnection for a given provider.
23
+ *
24
+ * Managed providers (where the service config `mode` is `"managed"`) are
25
+ * routed through the platform proxy with no local state required.
13
26
  *
14
- * When `accountInfo` is provided, resolves the connection for that specific
15
- * account (e.g. "user@gmail.com"). Otherwise falls back to the most recent
16
- * active connection.
27
+ * BYO providers resolve from the local SQLite oauth-store and require an
28
+ * active connection row and a stored access token.
17
29
  *
18
- * Reads exclusively from the SQLite oauth-store. Throws if no connection
19
- * exists (authorization required).
30
+ * @param providerKey - Provider identifier (e.g. "integration:google").
31
+ * Maps to the `provider_key` primary key in the `oauth_providers` table.
32
+ * @param options.clientId - Optional OAuth app client ID. When multiple BYO
33
+ * apps exist for the same provider, narrows the connection lookup to the
34
+ * app matching this client ID. Ignored for managed providers.
35
+ * @param options.account - Optional account identifier to disambiguate
36
+ * multi-account connections.
20
37
  */
21
38
  export async function resolveOAuthConnection(
22
- credentialService: string,
23
- accountInfo?: string,
39
+ providerKey: string,
40
+ options?: ResolveOAuthConnectionOptions,
24
41
  ): Promise<OAuthConnection> {
25
- const conn = accountInfo
26
- ? getConnectionByProviderAndAccount(credentialService, accountInfo)
27
- : getConnectionByProvider(credentialService);
42
+ const { clientId, account } = options ?? {};
43
+ const provider = getProvider(providerKey);
44
+ const managedKey = provider?.managedServiceConfigKey;
45
+
46
+ if (managedKey && managedKey in ServicesSchema.shape) {
47
+ const services: Services = getConfig().services;
48
+ if (services[managedKey as keyof Services].mode === "managed") {
49
+ const ctx = await resolveManagedProxyContext();
50
+ const assistantId = getPlatformAssistantId();
51
+ return new PlatformOAuthConnection({
52
+ id: providerKey,
53
+ providerKey,
54
+ externalId: providerKey,
55
+ accountInfo: account ?? null,
56
+ assistantId,
57
+ platformBaseUrl: ctx.platformBaseUrl,
58
+ apiKey: ctx.assistantApiKey,
59
+ });
60
+ }
61
+ }
62
+
63
+ // BYO path — requires a local connection row, access token, and base URL.
64
+ const conn = getActiveConnection(providerKey, { clientId, account });
28
65
  if (!conn) {
66
+ const filters = [
67
+ account && `account "${account}"`,
68
+ clientId && `client ID "${clientId}"`,
69
+ ].filter(Boolean);
70
+ const qualifier = filters.length
71
+ ? ` matching ${filters.join(" and ")}`
72
+ : "";
29
73
  throw new Error(
30
- `No credential found for "${credentialService}". Authorization required.`,
74
+ `No active OAuth connection found for "${providerKey}"${qualifier}. Connect the service first with oauth2_connect.`,
31
75
  );
32
76
  }
33
77
 
34
78
  const accessToken = await getSecureKeyAsync(
35
79
  `oauth_connection/${conn.id}/access_token`,
36
80
  );
37
-
38
81
  if (!accessToken) {
39
82
  throw new Error(
40
- `No access token found for "${credentialService}". Authorization required.`,
83
+ `OAuth connection for "${providerKey}" exists but has no access token. Re-authorize with oauth2_connect.`,
41
84
  );
42
85
  }
43
86
 
44
- // Look up the provider by credentialService first; fall back to the
45
- // connection's app's canonical providerKey so custom credential_service
46
- // overrides (e.g. "integration:github-work") still resolve to the well-known
47
- // provider's base URL. We traverse conn -> oauthApp -> providerKey because
48
- // conn.providerKey equals credentialService (getConnectionByProvider queries
49
- // WHERE providerKey = credentialService), whereas the app's providerKey is a
50
- // foreign key to the oauthProviders table.
51
- const provider =
52
- getProvider(credentialService) ??
53
- getProvider(getApp(conn.oauthAppId)?.providerKey ?? "");
54
87
  const baseUrl = provider?.baseUrl;
55
-
56
88
  if (!baseUrl) {
57
- throw new Error(`No base URL configured for "${credentialService}".`);
89
+ throw new Error(
90
+ `OAuth provider "${providerKey}" has no base URL configured. Check provider setup.`,
91
+ );
58
92
  }
59
93
 
60
- const grantedScopes: string[] = conn.grantedScopes
61
- ? JSON.parse(conn.grantedScopes)
62
- : [];
63
-
64
94
  return new BYOOAuthConnection({
65
95
  id: conn.id,
66
96
  providerKey: conn.providerKey,
67
97
  baseUrl,
68
98
  accountInfo: conn.accountInfo,
69
- grantedScopes,
70
- credentialService,
71
99
  });
72
100
  }
@@ -34,5 +34,4 @@ export interface OAuthConnection {
34
34
  readonly id: string;
35
35
  readonly providerKey: string;
36
36
  readonly accountInfo: string | null;
37
- readonly grantedScopes: string[];
38
37
  }
@@ -45,7 +45,9 @@ export type OAuthConnectionRow = typeof oauthConnections.$inferSelect;
45
45
  * Seed well-known provider profiles into the database. Uses INSERT … ON
46
46
  * CONFLICT DO UPDATE so that implementation fields (authUrl, tokenUrl,
47
47
  * tokenEndpointAuthMethod, userinfoUrl, extraParams, callbackTransport,
48
- * pingUrl) propagate to existing installations on every startup, while
48
+ * pingUrl, managedServiceConfigKey) and display metadata (displayName,
49
+ * description, dashboardUrl, clientIdPlaceholder, requiresClientSecret)
50
+ * propagate to existing installations on every startup, while
49
51
  * user-customizable fields (defaultScopes, scopePolicy, baseUrl) are
50
52
  * only written on the initial insert.
51
53
  */
@@ -62,6 +64,12 @@ export function seedProviders(
62
64
  scopePolicy: Record<string, unknown>;
63
65
  extraParams?: Record<string, string>;
64
66
  callbackTransport?: string;
67
+ managedServiceConfigKey?: string;
68
+ displayName?: string;
69
+ description?: string;
70
+ dashboardUrl?: string | null;
71
+ clientIdPlaceholder?: string | null;
72
+ requiresClientSecret?: boolean;
65
73
  }>,
66
74
  ): void {
67
75
  const db = getDb();
@@ -77,6 +85,12 @@ export function seedProviders(
77
85
  const scopePolicy = JSON.stringify(p.scopePolicy);
78
86
  const extraParams = p.extraParams ? JSON.stringify(p.extraParams) : null;
79
87
  const callbackTransport = p.callbackTransport ?? null;
88
+ const managedServiceConfigKey = p.managedServiceConfigKey ?? null;
89
+ const displayName = p.displayName ?? null;
90
+ const description = p.description ?? null;
91
+ const dashboardUrl = p.dashboardUrl ?? null;
92
+ const clientIdPlaceholder = p.clientIdPlaceholder ?? null;
93
+ const requiresClientSecret = p.requiresClientSecret !== false ? 1 : 0;
80
94
 
81
95
  db.insert(oauthProviders)
82
96
  .values({
@@ -91,6 +105,12 @@ export function seedProviders(
91
105
  extraParams,
92
106
  callbackTransport,
93
107
  pingUrl,
108
+ managedServiceConfigKey,
109
+ displayName,
110
+ description,
111
+ dashboardUrl,
112
+ clientIdPlaceholder,
113
+ requiresClientSecret,
94
114
  createdAt: now,
95
115
  updatedAt: now,
96
116
  })
@@ -104,6 +124,12 @@ export function seedProviders(
104
124
  extraParams,
105
125
  callbackTransport,
106
126
  pingUrl,
127
+ managedServiceConfigKey,
128
+ displayName,
129
+ description,
130
+ dashboardUrl,
131
+ clientIdPlaceholder,
132
+ requiresClientSecret,
107
133
  updatedAt: now,
108
134
  },
109
135
  })
@@ -143,6 +169,12 @@ export function registerProvider(params: {
143
169
  scopePolicy: Record<string, unknown>;
144
170
  extraParams?: Record<string, string>;
145
171
  callbackTransport?: string;
172
+ managedServiceConfigKey?: string;
173
+ displayName?: string;
174
+ description?: string;
175
+ dashboardUrl?: string;
176
+ clientIdPlaceholder?: string;
177
+ requiresClientSecret?: number;
146
178
  }): OAuthProviderRow {
147
179
  const db = getDb();
148
180
  const now = Date.now();
@@ -164,6 +196,12 @@ export function registerProvider(params: {
164
196
  extraParams: params.extraParams ? JSON.stringify(params.extraParams) : null,
165
197
  callbackTransport: params.callbackTransport ?? null,
166
198
  pingUrl: params.pingUrl ?? null,
199
+ managedServiceConfigKey: params.managedServiceConfigKey ?? null,
200
+ displayName: params.displayName ?? null,
201
+ description: params.description ?? null,
202
+ dashboardUrl: params.dashboardUrl ?? null,
203
+ clientIdPlaceholder: params.clientIdPlaceholder ?? null,
204
+ requiresClientSecret: params.requiresClientSecret ?? 1,
167
205
  createdAt: now,
168
206
  updatedAt: now,
169
207
  };
@@ -232,6 +270,14 @@ export async function upsertApp(
232
270
  if (!stored) {
233
271
  throw new Error("Failed to store client_secret in secure storage");
234
272
  }
273
+ // Bump updatedAt so the rollback guard in the new-row insertion path
274
+ // can detect that a concurrent caller has claimed this row. Without
275
+ // this, a concurrent inserter's rollback DELETE would still match on
276
+ // the original updatedAt and delete the row we just validated.
277
+ db.update(oauthApps)
278
+ .set({ updatedAt: Date.now() })
279
+ .where(eq(oauthApps.id, existingRow.id))
280
+ .run();
235
281
  }
236
282
  if (clientSecretCredentialPath) {
237
283
  db.update(oauthApps)
@@ -272,7 +318,14 @@ export async function upsertApp(
272
318
  if (!stored) {
273
319
  // Roll back the just-inserted row to avoid an orphaned app pointing
274
320
  // at a non-existent client_secret in secure storage.
275
- db.delete(oauthApps).where(eq(oauthApps.id, id)).run();
321
+ //
322
+ // Guard: only delete if updatedAt still matches our insertion timestamp.
323
+ // A concurrent upsertApp call may have observed this row, successfully
324
+ // stored the secret, and updated the row — deleting it would orphan that
325
+ // caller's valid reference.
326
+ db.delete(oauthApps)
327
+ .where(and(eq(oauthApps.id, id), eq(oauthApps.updatedAt, now)))
328
+ .run();
276
329
  throw new Error("Failed to store client_secret in secure storage");
277
330
  }
278
331
  }
@@ -286,6 +339,15 @@ export function getApp(id: string): OAuthAppRow | undefined {
286
339
  return db.select().from(oauthApps).where(eq(oauthApps.id, id)).get();
287
340
  }
288
341
 
342
+ /** Read an app client_secret from secure storage. */
343
+ export async function getAppClientSecret(
344
+ appOrId: OAuthAppRow | string,
345
+ ): Promise<string | undefined> {
346
+ const app = typeof appOrId === "string" ? getApp(appOrId) : appOrId;
347
+ if (!app) return undefined;
348
+ return getSecureKeyAsync(app.clientSecretCredentialPath);
349
+ }
350
+
289
351
  /** Look up an app by (provider_key, client_id). */
290
352
  export function getAppByProviderAndClientId(
291
353
  providerKey: string,
@@ -407,71 +469,59 @@ export function getConnection(id: string): OAuthConnectionRow | undefined {
407
469
 
408
470
  /**
409
471
  * Get the most recent active connection for a provider.
410
- * When `clientId` is provided, only connections linked to the matching app are considered.
411
- * Returns undefined if no active connection exists.
472
+ *
473
+ * Optional filters narrow the result:
474
+ * - `account` — match a specific account identifier (e.g. email).
475
+ * - `clientId` — restrict to connections linked to a specific OAuth app.
476
+ *
477
+ * Returns `undefined` when no matching active connection exists.
412
478
  */
413
- export function getConnectionByProvider(
479
+ export function getActiveConnection(
414
480
  providerKey: string,
415
- clientId?: string,
481
+ options?: { clientId?: string; account?: string },
416
482
  ): OAuthConnectionRow | undefined {
483
+ const { clientId, account } = options ?? {};
417
484
  const db = getDb();
418
485
 
486
+ const conditions = [
487
+ eq(oauthConnections.providerKey, providerKey),
488
+ eq(oauthConnections.status, "active"),
489
+ ];
490
+
491
+ if (account) {
492
+ conditions.push(eq(oauthConnections.accountInfo, account));
493
+ }
494
+
419
495
  if (clientId) {
420
496
  const app = getAppByProviderAndClientId(providerKey, clientId);
421
497
  if (!app) return undefined;
422
- return db
423
- .select()
424
- .from(oauthConnections)
425
- .where(
426
- and(
427
- eq(oauthConnections.providerKey, providerKey),
428
- eq(oauthConnections.oauthAppId, app.id),
429
- eq(oauthConnections.status, "active"),
430
- ),
431
- )
432
- .orderBy(desc(oauthConnections.createdAt), sql`rowid DESC`)
433
- .limit(1)
434
- .get();
498
+ conditions.push(eq(oauthConnections.oauthAppId, app.id));
435
499
  }
436
500
 
437
501
  return db
438
502
  .select()
439
503
  .from(oauthConnections)
440
- .where(
441
- and(
442
- eq(oauthConnections.providerKey, providerKey),
443
- eq(oauthConnections.status, "active"),
444
- ),
445
- )
504
+ .where(and(...conditions))
446
505
  .orderBy(desc(oauthConnections.createdAt), sql`rowid DESC`)
447
506
  .limit(1)
448
507
  .get();
449
508
  }
450
509
 
451
- /**
452
- * Get the active connection for a provider matching a specific account.
453
- * Falls back to getConnectionByProvider when accountInfo is undefined.
454
- */
510
+ /** @deprecated Use {@link getActiveConnection} instead. */
511
+ export function getConnectionByProvider(
512
+ providerKey: string,
513
+ clientId?: string,
514
+ ): OAuthConnectionRow | undefined {
515
+ return getActiveConnection(providerKey, { clientId });
516
+ }
517
+
518
+ /** @deprecated Use {@link getActiveConnection} instead. */
455
519
  export function getConnectionByProviderAndAccount(
456
520
  providerKey: string,
457
521
  accountInfo?: string,
522
+ clientId?: string,
458
523
  ): OAuthConnectionRow | undefined {
459
- if (!accountInfo) return getConnectionByProvider(providerKey);
460
-
461
- const db = getDb();
462
- return db
463
- .select()
464
- .from(oauthConnections)
465
- .where(
466
- and(
467
- eq(oauthConnections.providerKey, providerKey),
468
- eq(oauthConnections.accountInfo, accountInfo),
469
- eq(oauthConnections.status, "active"),
470
- ),
471
- )
472
- .orderBy(desc(oauthConnections.createdAt), sql`rowid DESC`)
473
- .limit(1)
474
- .get();
524
+ return getActiveConnection(providerKey, { clientId, account: accountInfo });
475
525
  }
476
526
 
477
527
  /**
@@ -505,7 +555,7 @@ export function listActiveConnectionsByProvider(
505
555
  export async function isProviderConnected(
506
556
  providerKey: string,
507
557
  ): Promise<boolean> {
508
- const conn = getConnectionByProvider(providerKey);
558
+ const conn = getActiveConnection(providerKey);
509
559
  if (!conn || conn.status !== "active") return false;
510
560
  return (
511
561
  (await getSecureKeyAsync(oauthConnectionAccessTokenPath(conn.id))) !==
@@ -623,7 +673,7 @@ export async function disconnectOAuthProvider(
623
673
  ): Promise<"disconnected" | "not-found" | "error"> {
624
674
  const conn = connectionId
625
675
  ? getConnection(connectionId)
626
- : getConnectionByProvider(providerKey, clientId);
676
+ : getActiveConnection(providerKey, { clientId });
627
677
  if (!conn) return "not-found";
628
678
 
629
679
  // Wrap the assistant's secure-key functions into the SecureKeyBackend
@@ -12,7 +12,6 @@ const DEFAULT_OPTIONS = {
12
12
  providerKey: "integration:google",
13
13
  externalId: "ext-123",
14
14
  accountInfo: "user@example.com",
15
- grantedScopes: ["https://www.googleapis.com/auth/gmail.readonly"],
16
15
  assistantId: "asst-abc",
17
16
  platformBaseUrl: "https://platform.example.com",
18
17
  apiKey: "test-api-key",
@@ -24,7 +24,6 @@ export interface PlatformOAuthConnectionOptions {
24
24
  providerKey: string;
25
25
  externalId: string;
26
26
  accountInfo: string | null;
27
- grantedScopes: string[];
28
27
  assistantId: string;
29
28
  platformBaseUrl: string;
30
29
  apiKey: string;
@@ -35,18 +34,27 @@ export class PlatformOAuthConnection implements OAuthConnection {
35
34
  readonly providerKey: string;
36
35
  readonly externalId: string;
37
36
  readonly accountInfo: string | null;
38
- readonly grantedScopes: string[];
39
37
 
40
38
  private readonly assistantId: string;
41
39
  private readonly platformBaseUrl: string;
42
40
  private readonly apiKey: string;
43
41
 
44
42
  constructor(options: PlatformOAuthConnectionOptions) {
43
+ const missing: string[] = [];
44
+ if (!options.platformBaseUrl) missing.push("platform base URL");
45
+ if (!options.apiKey) missing.push("assistant API key");
46
+ if (!options.assistantId) missing.push("assistant ID");
47
+ if (missing.length > 0) {
48
+ throw new BackendError(
49
+ `Platform-managed connection for "${options.providerKey}" cannot be created: missing ${missing.join(", ")}. ` +
50
+ `Log in to the Vellum platform or switch to using your own OAuth app.`,
51
+ );
52
+ }
53
+
45
54
  this.id = options.id;
46
55
  this.providerKey = options.providerKey;
47
56
  this.externalId = options.externalId;
48
57
  this.accountInfo = options.accountInfo;
49
- this.grantedScopes = options.grantedScopes;
50
58
  this.assistantId = options.assistantId;
51
59
  this.platformBaseUrl = options.platformBaseUrl.replace(/\/+$/, "");
52
60
  this.apiKey = options.apiKey;