@vellumai/assistant 0.6.2 → 0.6.4

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 (895) hide show
  1. package/ARCHITECTURE.md +273 -10
  2. package/Dockerfile +2 -3
  3. package/bun.lock +41 -49
  4. package/bunfig.toml +3 -0
  5. package/docs/architecture/memory.md +1 -1
  6. package/docs/backup-troubleshooting.md +52 -0
  7. package/docs/browser-use-architecture-phase2.md +174 -0
  8. package/docs/stt-provider-onboarding.md +120 -0
  9. package/knip.json +12 -2
  10. package/node_modules/@vellumai/ces-contracts/bun.lock +8 -6
  11. package/node_modules/@vellumai/ces-contracts/package.json +3 -3
  12. package/node_modules/@vellumai/ces-contracts/src/rpc.ts +42 -0
  13. package/openapi.yaml +1111 -86
  14. package/package.json +40 -42
  15. package/scripts/generate-openapi.ts +0 -2
  16. package/scripts/test.sh +73 -18
  17. package/src/__tests__/acp-session.test.ts +43 -0
  18. package/src/__tests__/agent-image-optimize.test.ts +28 -0
  19. package/src/__tests__/agent-loop.test.ts +123 -0
  20. package/src/__tests__/anthropic-provider.test.ts +263 -10
  21. package/src/__tests__/app-builder-tool-scripts.test.ts +1 -0
  22. package/src/__tests__/app-executors.test.ts +1 -0
  23. package/src/__tests__/app-source-watcher.test.ts +37 -11
  24. package/src/__tests__/approval-routes-http.test.ts +178 -1
  25. package/src/__tests__/auto-analysis-end-to-end.test.ts +550 -0
  26. package/src/__tests__/auto-analysis-prompt.test.ts +50 -0
  27. package/src/__tests__/browser-fill-credential.test.ts +240 -94
  28. package/src/__tests__/browser-manager.test.ts +40 -27
  29. package/src/__tests__/browser-skill-baseline-tool-payload.test.ts +2 -2
  30. package/src/__tests__/browser-skill-endstate.test.ts +31 -7
  31. package/src/__tests__/btw-routes.test.ts +7 -0
  32. package/src/__tests__/call-controller.test.ts +581 -20
  33. package/src/__tests__/catalog-files.test.ts +1000 -0
  34. package/src/__tests__/channel-approvals.test.ts +53 -0
  35. package/src/__tests__/channel-invite-transport.test.ts +2 -2
  36. package/src/__tests__/channel-readiness-routes.test.ts +16 -20
  37. package/src/__tests__/channel-readiness-service.test.ts +12 -7
  38. package/src/__tests__/checker.test.ts +157 -10
  39. package/src/__tests__/clawhub-files.test.ts +347 -0
  40. package/src/__tests__/commit-message-enrichment-service.test.ts +36 -19
  41. package/src/__tests__/config-analysis.test.ts +100 -0
  42. package/src/__tests__/config-managed-gemini-defaults.test.ts +326 -0
  43. package/src/__tests__/config-schema-cmd.test.ts +2 -2
  44. package/src/__tests__/config-schema.test.ts +1248 -224
  45. package/src/__tests__/config-watcher-cleanup-throttle.test.ts +339 -0
  46. package/src/__tests__/config-watcher.test.ts +43 -8
  47. package/src/__tests__/confirmation-request-guardian-bridge.test.ts +23 -0
  48. package/src/__tests__/contact-store-user-file.test.ts +512 -0
  49. package/src/__tests__/contacts-write.test.ts +197 -0
  50. package/src/__tests__/context-overflow-approval.test.ts +16 -1
  51. package/src/__tests__/context-window-manager.test.ts +88 -0
  52. package/src/__tests__/conversation-abort-tool-results.test.ts +2 -0
  53. package/src/__tests__/conversation-agent-loop-overflow.test.ts +2 -1
  54. package/src/__tests__/conversation-agent-loop.test.ts +99 -3
  55. package/src/__tests__/conversation-analysis-routes.test.ts +2 -2
  56. package/src/__tests__/conversation-attachments.test.ts +80 -4
  57. package/src/__tests__/conversation-confirmation-signals.test.ts +290 -0
  58. package/src/__tests__/conversation-error.test.ts +70 -0
  59. package/src/__tests__/conversation-fork-crud.test.ts +17 -0
  60. package/src/__tests__/conversation-history-web-search.test.ts +12 -4
  61. package/src/__tests__/conversation-host-access-routes.test.ts +229 -0
  62. package/src/__tests__/conversation-init.benchmark.test.ts +6 -1
  63. package/src/__tests__/conversation-inject-context.test.ts +103 -0
  64. package/src/__tests__/conversation-launcher-skill-regression.test.ts +51 -0
  65. package/src/__tests__/conversation-list-source.test.ts +145 -0
  66. package/src/__tests__/conversation-pre-run-repair.test.ts +2 -0
  67. package/src/__tests__/conversation-provider-retry-repair.test.ts +2 -0
  68. package/src/__tests__/conversation-queue.test.ts +946 -62
  69. package/src/__tests__/conversation-routes-disk-view.test.ts +275 -0
  70. package/src/__tests__/conversation-routes-guardian-reply.test.ts +16 -0
  71. package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -0
  72. package/src/__tests__/conversation-runtime-assembly.test.ts +324 -46
  73. package/src/__tests__/conversation-skill-tools.test.ts +7 -4
  74. package/src/__tests__/conversation-slash-commands.test.ts +33 -0
  75. package/src/__tests__/conversation-slash-queue.test.ts +89 -18
  76. package/src/__tests__/conversation-slash-unknown.test.ts +2 -0
  77. package/src/__tests__/conversation-starter-routes.test.ts +126 -0
  78. package/src/__tests__/conversation-starters-cadence.test.ts +161 -0
  79. package/src/__tests__/conversation-store.test.ts +195 -0
  80. package/src/__tests__/conversation-tool-setup-batch-authorized.test.ts +226 -0
  81. package/src/__tests__/conversation-workspace-cache-state.test.ts +193 -0
  82. package/src/__tests__/conversation-workspace-injection.test.ts +2 -0
  83. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +2 -0
  84. package/src/__tests__/credential-execution-approval-bridge.test.ts +32 -1
  85. package/src/__tests__/credential-health-service.test.ts +352 -0
  86. package/src/__tests__/credential-security-invariants.test.ts +6 -3
  87. package/src/__tests__/credential-vault-unit.test.ts +383 -7
  88. package/src/__tests__/credential-vault.test.ts +152 -13
  89. package/src/__tests__/credentials-cli.test.ts +42 -18
  90. package/src/__tests__/cross-provider-web-search.test.ts +146 -35
  91. package/src/__tests__/date-context.test.ts +4 -4
  92. package/src/__tests__/deterministic-verification-control-plane.test.ts +10 -1
  93. package/src/__tests__/device-id.test.ts +112 -0
  94. package/src/__tests__/docker-signing-key-bootstrap.test.ts +167 -4
  95. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +1 -3
  96. package/src/__tests__/email-html-renderer.test.ts +71 -0
  97. package/src/__tests__/email-invite-adapter.test.ts +36 -32
  98. package/src/__tests__/embedding-managed-proxy-selection.test.ts +256 -0
  99. package/src/__tests__/emit-event-signal.test.ts +71 -0
  100. package/src/__tests__/extension-id-sync-guard.test.ts +222 -0
  101. package/src/__tests__/fixtures/mock-chrome-extension.ts +386 -0
  102. package/src/__tests__/gateway-only-enforcement.test.ts +206 -1
  103. package/src/__tests__/gateway-only-guard.test.ts +2 -0
  104. package/src/__tests__/gemini-provider.test.ts +66 -2
  105. package/src/__tests__/get-skill-detail-audit.test.ts +325 -0
  106. package/src/__tests__/gmail-archive-fallback.test.ts +193 -0
  107. package/src/__tests__/gmail-archive-gate.test.ts +246 -0
  108. package/src/__tests__/gmail-preferences.test.ts +117 -0
  109. package/src/__tests__/guardian-routing-invariants.test.ts +70 -2
  110. package/src/__tests__/headless-browser-interactions.test.ts +738 -359
  111. package/src/__tests__/headless-browser-mode.test.ts +614 -0
  112. package/src/__tests__/headless-browser-navigate.test.ts +528 -49
  113. package/src/__tests__/headless-browser-read-tools.test.ts +274 -100
  114. package/src/__tests__/headless-browser-snapshot.test.ts +250 -77
  115. package/src/__tests__/heartbeat-service.test.ts +70 -17
  116. package/src/__tests__/home-state-routes.test.ts +162 -0
  117. package/src/__tests__/host-bash-proxy.test.ts +145 -1
  118. package/src/__tests__/host-browser-e2e-cloud.test.ts +596 -0
  119. package/src/__tests__/host-browser-e2e-self-hosted-capability.test.ts +286 -0
  120. package/src/__tests__/host-browser-e2e-self-hosted.test.ts +374 -0
  121. package/src/__tests__/host-browser-event-routes.test.ts +350 -0
  122. package/src/__tests__/host-browser-proxy.test.ts +444 -0
  123. package/src/__tests__/host-browser-routes.test.ts +198 -0
  124. package/src/__tests__/host-browser-ws-events-e2e.test.ts +423 -0
  125. package/src/__tests__/host-cu-proxy.test.ts +166 -1
  126. package/src/__tests__/host-file-proxy.test.ts +185 -1
  127. package/src/__tests__/host-file-read-tool.test.ts +52 -0
  128. package/src/__tests__/host-proxy-interface.test.ts +165 -0
  129. package/src/__tests__/host-shell-tool.test.ts +1 -11
  130. package/src/__tests__/http-user-message-parity.test.ts +1 -0
  131. package/src/__tests__/identity-intro-cache.test.ts +40 -10
  132. package/src/__tests__/init-feature-flag-overrides.test.ts +38 -112
  133. package/src/__tests__/integration-status.test.ts +6 -7
  134. package/src/__tests__/jobs-store-upsert-debounced.test.ts +141 -0
  135. package/src/__tests__/list-messages-tool-merge.test.ts +37 -12
  136. package/src/__tests__/llm-context-normalization.test.ts +488 -0
  137. package/src/__tests__/llm-context-route-provider.test.ts +86 -5
  138. package/src/__tests__/llm-usage-store.test.ts +363 -0
  139. package/src/__tests__/mcp-client-auth.test.ts +40 -4
  140. package/src/__tests__/mcp-health-check.test.ts +10 -3
  141. package/src/__tests__/media-stream-output.test.ts +555 -0
  142. package/src/__tests__/media-stream-parser.test.ts +374 -0
  143. package/src/__tests__/media-stream-server-integration.test.ts +1234 -0
  144. package/src/__tests__/media-stream-stt-session.test.ts +588 -0
  145. package/src/__tests__/media-turn-detector.test.ts +440 -0
  146. package/src/__tests__/message-queue.test.ts +125 -0
  147. package/src/__tests__/migration-cross-version-compatibility.test.ts +3 -1
  148. package/src/__tests__/migration-export-http.test.ts +67 -8
  149. package/src/__tests__/migration-export-streaming.test.ts +66 -0
  150. package/src/__tests__/migration-import-commit-http.test.ts +109 -7
  151. package/src/__tests__/migration-import-preflight-http.test.ts +6 -5
  152. package/src/__tests__/migration-validate-http.test.ts +3 -3
  153. package/src/__tests__/mock-gateway-ipc.ts +151 -0
  154. package/src/__tests__/model-intents.test.ts +2 -2
  155. package/src/__tests__/native-host-marker-sync-guard.test.ts +157 -0
  156. package/src/__tests__/oauth-apps-routes.test.ts +18 -12
  157. package/src/__tests__/oauth-cli.test.ts +709 -60
  158. package/src/__tests__/oauth-connect-orchestrator.test.ts +118 -24
  159. package/src/__tests__/oauth-provider-seed-logos.test.ts +23 -0
  160. package/src/__tests__/oauth-provider-serializer.test.ts +147 -10
  161. package/src/__tests__/oauth-provider-visibility.test.ts +19 -21
  162. package/src/__tests__/oauth-providers-routes.test.ts +52 -14
  163. package/src/__tests__/oauth-store.test.ts +1465 -176
  164. package/src/__tests__/oauth2-gateway-transport.test.ts +460 -26
  165. package/src/__tests__/onboarding-template-contract.test.ts +81 -70
  166. package/src/__tests__/openai-provider.test.ts +178 -2
  167. package/src/__tests__/openai-responses-cutover-guard.test.ts +184 -0
  168. package/src/__tests__/openai-responses-provider.test.ts +1105 -0
  169. package/src/__tests__/openrouter-token-estimation.test.ts +100 -0
  170. package/src/__tests__/outlook-categories.test.ts +1 -1
  171. package/src/__tests__/outlook-client-automation.test.ts +1 -1
  172. package/src/__tests__/outlook-compose-tools.test.ts +1 -1
  173. package/src/__tests__/outlook-email-watcher.test.ts +1 -1
  174. package/src/__tests__/outlook-follow-up.test.ts +1 -1
  175. package/src/__tests__/outlook-messaging-provider.test.ts +2 -2
  176. package/src/__tests__/outlook-trash.test.ts +1 -1
  177. package/src/__tests__/outlook-unsubscribe.test.ts +32 -3
  178. package/src/__tests__/permission-checker-host-gate.test.ts +74 -14
  179. package/src/__tests__/permission-mode.test.ts +28 -56
  180. package/src/__tests__/persona-resolver.test.ts +251 -0
  181. package/src/__tests__/platform-bash-auto-approve.test.ts +4 -0
  182. package/src/__tests__/platform-callback-registration.test.ts +19 -0
  183. package/src/__tests__/platform.test.ts +92 -1
  184. package/src/__tests__/post-turn-tool-result-truncation.test.ts +343 -0
  185. package/src/__tests__/prechat-onboarding-contract.test.ts +267 -0
  186. package/src/__tests__/pricing.test.ts +174 -0
  187. package/src/__tests__/proxy-approval-callback.test.ts +18 -0
  188. package/src/__tests__/qdrant-manager.test.ts +29 -8
  189. package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +194 -0
  190. package/src/__tests__/relationship-state-contract.test.ts +175 -0
  191. package/src/__tests__/relay-server.test.ts +423 -5
  192. package/src/__tests__/require-fresh-approval.test.ts +40 -1
  193. package/src/__tests__/sanitize-config-for-transfer.test.ts +132 -0
  194. package/src/__tests__/schedule-routes.test.ts +162 -0
  195. package/src/__tests__/search-skills-unified.test.ts +118 -0
  196. package/src/__tests__/secret-detection-handler.test.ts +84 -0
  197. package/src/__tests__/secret-ingress-http.test.ts +1 -0
  198. package/src/__tests__/secret-scanner-executor.test.ts +4 -0
  199. package/src/__tests__/secure-keys.test.ts +107 -0
  200. package/src/__tests__/send-endpoint-busy.test.ts +8 -1
  201. package/src/__tests__/sequence-store.test.ts +1 -1
  202. package/src/__tests__/server-history-render.test.ts +49 -0
  203. package/src/__tests__/set-permission-mode.test.ts +13 -250
  204. package/src/__tests__/settings-routes.test.ts +201 -0
  205. package/src/__tests__/skill-load-feature-flag.test.ts +1 -0
  206. package/src/__tests__/skills-file-content-endpoint.test.ts +801 -0
  207. package/src/__tests__/skills-files-catalog-fallback.test.ts +738 -0
  208. package/src/__tests__/skills.test.ts +5 -2
  209. package/src/__tests__/skillssh-files.test.ts +446 -0
  210. package/src/__tests__/slack-block-formatting.test.ts +110 -0
  211. package/src/__tests__/slack-channel-config.test.ts +576 -16
  212. package/src/__tests__/stt-catalog-parity.test.ts +282 -0
  213. package/src/__tests__/stt-stream-session.test.ts +535 -0
  214. package/src/__tests__/subagent-detail.test.ts +44 -2
  215. package/src/__tests__/subagent-disposal.test.ts +1 -0
  216. package/src/__tests__/subagent-fork-notifications.test.ts +291 -0
  217. package/src/__tests__/subagent-fork-spawn.test.ts +384 -0
  218. package/src/__tests__/subagent-manager-notify.test.ts +1 -0
  219. package/src/__tests__/subagent-notify-parent.test.ts +1 -0
  220. package/src/__tests__/subagent-spawn-tool-fork.test.ts +411 -0
  221. package/src/__tests__/subagent-tools.test.ts +1 -0
  222. package/src/__tests__/subagent-types.test.ts +1 -0
  223. package/src/__tests__/system-prompt-ask-mode.test.ts +27 -71
  224. package/src/__tests__/system-prompt.test.ts +184 -27
  225. package/src/__tests__/task-scheduler.test.ts +32 -6
  226. package/src/__tests__/telegram-config.test.ts +10 -13
  227. package/src/__tests__/telephony-stt-routing.test.ts +329 -0
  228. package/src/__tests__/terminal-tools.test.ts +25 -5
  229. package/src/__tests__/test-preload.ts +18 -0
  230. package/src/__tests__/test-support/browser-skill-harness.ts +4 -1
  231. package/src/__tests__/tool-approval-handler.test.ts +73 -0
  232. package/src/__tests__/tool-executor-lifecycle-events.test.ts +9 -5
  233. package/src/__tests__/tool-executor-shell-integration.test.ts +4 -0
  234. package/src/__tests__/tool-executor.test.ts +33 -24
  235. package/src/__tests__/tool-result-truncation.test.ts +36 -0
  236. package/src/__tests__/tool-side-effects-slack-dm.test.ts +22 -0
  237. package/src/__tests__/top-level-renderer.test.ts +73 -1
  238. package/src/__tests__/transport-hints-queue.test.ts +14 -29
  239. package/src/__tests__/trust-store.test.ts +7 -1
  240. package/src/__tests__/trusted-contact-approval-notifier.test.ts +1 -1
  241. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +109 -0
  242. package/src/__tests__/tts-catalog-parity.test.ts +345 -0
  243. package/src/__tests__/twilio-routes-twiml.test.ts +512 -114
  244. package/src/__tests__/twilio-routes.test.ts +376 -0
  245. package/src/__tests__/unicode.test.ts +293 -0
  246. package/src/__tests__/update-bulletin-format.test.ts +59 -0
  247. package/src/__tests__/update-bulletin.test.ts +206 -5
  248. package/src/__tests__/usage-routes.test.ts +25 -4
  249. package/src/__tests__/user-reference.test.ts +46 -61
  250. package/src/__tests__/v2-consent-policy.test.ts +103 -0
  251. package/src/__tests__/verification-control-plane-policy.test.ts +4 -0
  252. package/src/__tests__/voice-config-update.test.ts +403 -0
  253. package/src/__tests__/voice-quality.test.ts +434 -19
  254. package/src/__tests__/workspace-heartbeat-service.test.ts +7 -0
  255. package/src/__tests__/workspace-migration-033-stt-service-explicit-config.test.ts +547 -0
  256. package/src/__tests__/workspace-migration-034-remove-calls-voice-transcription-provider.test.ts +596 -0
  257. package/src/__tests__/workspace-migration-drop-user-md.test.ts +368 -0
  258. package/src/__tests__/workspace-migration-meets.test.ts +244 -0
  259. package/src/__tests__/workspace-migration-seed-device-id.test.ts +14 -20
  260. package/src/__tests__/workspace-policy.test.ts +2 -0
  261. package/src/acp/client-handler.ts +30 -4
  262. package/src/agent/image-optimize.ts +24 -12
  263. package/src/agent/loop.ts +55 -9
  264. package/src/approvals/guardian-request-resolvers.ts +21 -15
  265. package/src/backup/__tests__/backup-key.test.ts +152 -0
  266. package/src/backup/__tests__/backup-worker.test.ts +767 -0
  267. package/src/backup/__tests__/list-snapshots.test.ts +87 -0
  268. package/src/backup/__tests__/local-writer.test.ts +218 -0
  269. package/src/backup/__tests__/offsite-writer.test.ts +641 -0
  270. package/src/backup/__tests__/paths.test.ts +300 -0
  271. package/src/backup/__tests__/restore.test.ts +498 -0
  272. package/src/backup/__tests__/snapshot-lock.test.ts +352 -0
  273. package/src/backup/__tests__/stream-crypt.test.ts +228 -0
  274. package/src/backup/backup-key.ts +137 -0
  275. package/src/backup/backup-worker.ts +459 -0
  276. package/src/backup/list-snapshots.ts +147 -0
  277. package/src/backup/local-writer.ts +133 -0
  278. package/src/backup/offsite-writer.ts +222 -0
  279. package/src/backup/paths.ts +226 -0
  280. package/src/backup/restore.ts +322 -0
  281. package/src/backup/snapshot-lock.ts +431 -0
  282. package/src/backup/stream-crypt.ts +263 -0
  283. package/src/browser-session/__tests__/manager.test.ts +297 -0
  284. package/src/browser-session/backends/cdp-inspect.ts +30 -0
  285. package/src/browser-session/backends/extension.ts +26 -0
  286. package/src/browser-session/backends/local.ts +24 -0
  287. package/src/browser-session/events.ts +164 -0
  288. package/src/browser-session/index.ts +27 -0
  289. package/src/browser-session/manager.ts +159 -0
  290. package/src/browser-session/types.ts +28 -0
  291. package/src/bundler/package-resolver.ts +4 -0
  292. package/src/calls/audio-store.ts +11 -5
  293. package/src/calls/call-controller.ts +226 -71
  294. package/src/calls/call-domain.ts +9 -0
  295. package/src/calls/call-speech-output.ts +190 -0
  296. package/src/calls/call-transport.ts +77 -0
  297. package/src/calls/media-stream-audio-transcode.ts +173 -0
  298. package/src/calls/media-stream-output.ts +660 -0
  299. package/src/calls/media-stream-parser.ts +300 -0
  300. package/src/calls/media-stream-protocol.ts +166 -0
  301. package/src/calls/media-stream-server.ts +592 -0
  302. package/src/calls/media-stream-stt-session.ts +460 -0
  303. package/src/calls/media-turn-detector.ts +230 -0
  304. package/src/calls/relay-server.ts +90 -75
  305. package/src/calls/resolve-call-tts-provider.ts +136 -0
  306. package/src/calls/telephony-stt-routing.ts +145 -0
  307. package/src/calls/tts-call-strategy.ts +161 -0
  308. package/src/calls/tts-text-sanitizer.ts +32 -16
  309. package/src/calls/twilio-routes.ts +281 -17
  310. package/src/calls/voice-quality.ts +78 -35
  311. package/src/calls/voice-session-bridge.ts +8 -1
  312. package/src/channels/__tests__/types.test.ts +134 -0
  313. package/src/channels/types.ts +69 -3
  314. package/src/cli/__tests__/run-assistant-command.ts +11 -1
  315. package/src/cli/commands/__tests__/backup.test.ts +1165 -0
  316. package/src/cli/commands/__tests__/domain-register.test.ts +234 -0
  317. package/src/cli/commands/__tests__/domain-status.test.ts +132 -0
  318. package/src/cli/commands/__tests__/email-attachment.test.ts +422 -0
  319. package/src/cli/commands/__tests__/email-download.test.ts +16 -1
  320. package/src/cli/commands/__tests__/email-list.test.ts +22 -4
  321. package/src/cli/commands/__tests__/email-register.test.ts +4 -4
  322. package/src/cli/commands/__tests__/email-send.test.ts +37 -4
  323. package/src/cli/commands/__tests__/email-status.test.ts +5 -1
  324. package/src/cli/commands/__tests__/email-unregister.test.ts +34 -5
  325. package/src/cli/commands/backup.ts +993 -0
  326. package/src/cli/commands/conversations.ts +77 -0
  327. package/src/cli/commands/credentials.ts +3 -4
  328. package/src/cli/commands/domain.ts +210 -0
  329. package/src/cli/commands/email.ts +273 -16
  330. package/src/cli/commands/mcp.ts +16 -4
  331. package/src/cli/commands/oauth/__tests__/connect.test.ts +56 -44
  332. package/src/cli/commands/oauth/__tests__/disconnect.test.ts +21 -21
  333. package/src/cli/commands/oauth/__tests__/mode.test.ts +17 -17
  334. package/src/cli/commands/oauth/__tests__/ping.test.ts +16 -16
  335. package/src/cli/commands/oauth/__tests__/providers-delete.test.ts +32 -33
  336. package/src/cli/commands/oauth/__tests__/providers-register.test.ts +330 -0
  337. package/src/cli/commands/oauth/__tests__/providers-update.test.ts +117 -12
  338. package/src/cli/commands/oauth/__tests__/status.test.ts +10 -10
  339. package/src/cli/commands/oauth/__tests__/token.test.ts +7 -7
  340. package/src/cli/commands/oauth/apps.ts +7 -4
  341. package/src/cli/commands/oauth/connect.ts +6 -3
  342. package/src/cli/commands/oauth/disconnect.ts +1 -1
  343. package/src/cli/commands/oauth/mode.ts +12 -3
  344. package/src/cli/commands/oauth/providers.ts +215 -36
  345. package/src/cli/commands/oauth/shared.ts +7 -6
  346. package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +254 -0
  347. package/src/cli/commands/platform/__tests__/connect.test.ts +6 -0
  348. package/src/cli/commands/platform/__tests__/disconnect.test.ts +7 -1
  349. package/src/cli/commands/platform/__tests__/status.test.ts +6 -0
  350. package/src/cli/commands/platform/index.ts +107 -10
  351. package/src/cli/commands/usage.ts +10 -9
  352. package/src/cli/lib/daemon-credential-client.ts +4 -0
  353. package/src/cli/program.ts +30 -4
  354. package/src/config/__tests__/backup-schema.test.ts +134 -0
  355. package/src/config/assistant-feature-flags.ts +61 -62
  356. package/src/config/bundled-skills/app-builder/SKILL.md +26 -249
  357. package/src/config/bundled-skills/app-builder/references/CUSTOM_ROUTES.md +141 -0
  358. package/src/config/bundled-skills/app-builder/references/INTERACTION_HOOKS.md +56 -0
  359. package/src/config/bundled-skills/app-builder/references/WIDGETS.md +125 -0
  360. package/src/config/bundled-skills/browser/SKILL.md +30 -5
  361. package/src/config/bundled-skills/browser/TOOLS.json +123 -0
  362. package/src/config/bundled-skills/browser/tools/browser-attach.ts +12 -0
  363. package/src/config/bundled-skills/browser/tools/browser-detach.ts +12 -0
  364. package/src/config/bundled-skills/browser/tools/browser-status.ts +12 -0
  365. package/src/config/bundled-skills/browser/tools/browser-wait-for-download.ts +17 -0
  366. package/src/config/bundled-skills/contacts/SKILL.md +5 -2
  367. package/src/config/bundled-skills/document/SKILL.md +4 -0
  368. package/src/config/bundled-skills/gmail/SKILL.md +54 -8
  369. package/src/config/bundled-skills/gmail/TOOLS.json +33 -3
  370. package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +116 -9
  371. package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +138 -11
  372. package/src/config/bundled-skills/gmail/tools/gmail-preferences-tool.ts +59 -0
  373. package/src/config/bundled-skills/gmail/tools/gmail-preferences.ts +82 -0
  374. package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +113 -17
  375. package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +2 -2
  376. package/src/config/bundled-skills/media-processing/SKILL.md +3 -9
  377. package/src/config/bundled-skills/media-processing/TOOLS.json +1 -6
  378. package/src/config/bundled-skills/media-processing/__tests__/audio-transcribe.test.ts +125 -0
  379. package/src/config/bundled-skills/media-processing/__tests__/extract-keyframes.test.ts +181 -0
  380. package/src/config/bundled-skills/media-processing/__tests__/preprocess-audio.test.ts +141 -0
  381. package/src/config/bundled-skills/media-processing/services/audio-transcribe.ts +32 -87
  382. package/src/config/bundled-skills/media-processing/services/preprocess.ts +8 -4
  383. package/src/config/bundled-skills/media-processing/tools/extract-keyframes.ts +0 -10
  384. package/src/config/bundled-skills/messaging/SKILL.md +3 -3
  385. package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +2 -2
  386. package/src/config/bundled-skills/outlook/SKILL.md +9 -2
  387. package/src/config/bundled-skills/outlook/tools/outlook-unsubscribe.ts +2 -2
  388. package/src/config/bundled-skills/phone-calls/SKILL.md +2 -2
  389. package/src/config/bundled-skills/phone-calls/references/CONFIG.md +27 -18
  390. package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +3 -3
  391. package/src/config/bundled-skills/settings/TOOLS.json +3 -3
  392. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +26 -22
  393. package/src/config/bundled-skills/slack/SKILL.md +1 -0
  394. package/src/config/bundled-skills/subagent/SKILL.md +21 -0
  395. package/src/config/bundled-skills/subagent/TOOLS.json +8 -4
  396. package/src/config/bundled-skills/tasks/SKILL.md +5 -0
  397. package/src/config/bundled-skills/transcribe/SKILL.md +9 -14
  398. package/src/config/bundled-skills/transcribe/TOOLS.json +2 -7
  399. package/src/config/bundled-skills/transcribe/tools/transcribe-media.test.ts +256 -0
  400. package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +38 -188
  401. package/src/config/bundled-tool-registry.ts +8 -0
  402. package/src/config/env-registry.ts +38 -0
  403. package/src/config/env.ts +49 -4
  404. package/src/config/feature-flag-registry.json +85 -14
  405. package/src/config/loader.ts +82 -13
  406. package/src/config/sanitize-for-transfer.ts +47 -0
  407. package/src/config/schema.ts +81 -15
  408. package/src/config/schemas/__tests__/stt.test.ts +43 -0
  409. package/src/config/schemas/analysis.ts +51 -0
  410. package/src/config/schemas/backup.ts +72 -0
  411. package/src/config/schemas/calls.ts +1 -26
  412. package/src/config/schemas/elevenlabs.ts +0 -59
  413. package/src/config/schemas/filing.ts +47 -7
  414. package/src/config/schemas/heartbeat.ts +27 -5
  415. package/src/config/schemas/host-browser.ts +112 -0
  416. package/src/config/schemas/inference.ts +1 -1
  417. package/src/config/schemas/memory-lifecycle.ts +14 -2
  418. package/src/config/schemas/memory-retrieval.ts +103 -0
  419. package/src/config/schemas/security.ts +0 -6
  420. package/src/config/schemas/services.ts +52 -0
  421. package/src/config/schemas/stt.ts +59 -0
  422. package/src/config/schemas/tts.ts +230 -0
  423. package/src/config/schemas/updates.ts +14 -0
  424. package/src/config/skills.ts +4 -0
  425. package/src/config/types.ts +4 -1
  426. package/src/contacts/contact-store.ts +56 -11
  427. package/src/contacts/contacts-write.ts +38 -1
  428. package/src/context/post-turn-tool-result-truncation.ts +177 -0
  429. package/src/context/tool-result-truncation.ts +2 -1
  430. package/src/context/window-manager.ts +61 -10
  431. package/src/credential-execution/approval-bridge.ts +49 -15
  432. package/src/credential-execution/executable-discovery.ts +12 -2
  433. package/src/credential-execution/process-manager.ts +33 -2
  434. package/src/credential-health/credential-health-service.ts +366 -0
  435. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +324 -0
  436. package/src/daemon/__tests__/conversation-surfaces-launch.test.ts +497 -0
  437. package/src/daemon/__tests__/conversation-tool-setup.test.ts +195 -0
  438. package/src/daemon/__tests__/lifecycle-startup-ordering.test.ts +127 -0
  439. package/src/daemon/app-source-watcher.ts +35 -0
  440. package/src/daemon/config-watcher.ts +99 -5
  441. package/src/daemon/context-overflow-approval.ts +5 -0
  442. package/src/daemon/conversation-agent-loop-handlers.ts +23 -2
  443. package/src/daemon/conversation-agent-loop.ts +153 -42
  444. package/src/daemon/conversation-attachments.ts +40 -0
  445. package/src/daemon/conversation-error.ts +11 -0
  446. package/src/daemon/conversation-history.ts +40 -6
  447. package/src/daemon/conversation-launch.ts +220 -0
  448. package/src/daemon/conversation-lifecycle.ts +59 -9
  449. package/src/daemon/conversation-messaging.ts +37 -3
  450. package/src/daemon/conversation-notifiers.ts +5 -0
  451. package/src/daemon/conversation-process.ts +622 -13
  452. package/src/daemon/conversation-queue-manager.ts +24 -0
  453. package/src/daemon/conversation-runtime-assembly.ts +128 -36
  454. package/src/daemon/conversation-slash.ts +36 -0
  455. package/src/daemon/conversation-surfaces.ts +131 -40
  456. package/src/daemon/conversation-tool-setup.ts +99 -8
  457. package/src/daemon/conversation-usage.ts +7 -4
  458. package/src/daemon/conversation-workspace.ts +12 -0
  459. package/src/daemon/conversation.ts +292 -16
  460. package/src/daemon/date-context.ts +10 -10
  461. package/src/daemon/first-greeting.ts +3 -2
  462. package/src/daemon/handlers/config-slack-channel.ts +269 -94
  463. package/src/daemon/handlers/conversations.ts +13 -141
  464. package/src/daemon/handlers/shared.ts +80 -0
  465. package/src/daemon/handlers/skills.ts +483 -44
  466. package/src/daemon/host-bash-proxy.ts +48 -13
  467. package/src/daemon/host-browser-proxy.ts +192 -0
  468. package/src/daemon/host-cu-proxy.ts +36 -11
  469. package/src/daemon/host-file-proxy.ts +57 -9
  470. package/src/daemon/lifecycle.ts +179 -28
  471. package/src/daemon/message-protocol.ts +13 -0
  472. package/src/daemon/message-types/conversations.ts +89 -14
  473. package/src/daemon/message-types/home.ts +40 -0
  474. package/src/daemon/message-types/host-browser.ts +100 -0
  475. package/src/daemon/message-types/meet.ts +143 -0
  476. package/src/daemon/message-types/messages.ts +19 -5
  477. package/src/daemon/message-types/schedules.ts +34 -2
  478. package/src/daemon/message-types/skills.ts +26 -0
  479. package/src/daemon/message-types/subagents.ts +2 -0
  480. package/src/daemon/message-types/surfaces.ts +2 -0
  481. package/src/daemon/server.ts +439 -14
  482. package/src/daemon/shutdown-handlers.ts +32 -4
  483. package/src/daemon/shutdown-registry.ts +40 -0
  484. package/src/daemon/tool-side-effects.ts +15 -0
  485. package/src/daemon/transport-hints.ts +5 -24
  486. package/src/email/html-renderer.ts +76 -0
  487. package/src/heartbeat/heartbeat-service.ts +93 -7
  488. package/src/home/__tests__/assistant-feed-authoring.test.ts +156 -0
  489. package/src/home/__tests__/emit-feed-event.test.ts +169 -0
  490. package/src/home/__tests__/feed-scheduler.test.ts +194 -0
  491. package/src/home/__tests__/feed-types.test.ts +275 -0
  492. package/src/home/__tests__/feed-writer.test.ts +688 -0
  493. package/src/home/__tests__/phase5-exit-criteria.test.ts +212 -0
  494. package/src/home/__tests__/platform-gmail-digest.test.ts +222 -0
  495. package/src/home/__tests__/progress-formula.test.ts +213 -0
  496. package/src/home/__tests__/relationship-state-writer.test.ts +740 -0
  497. package/src/home/__tests__/rollup-producer.test.ts +398 -0
  498. package/src/home/assistant-feed-authoring.ts +124 -0
  499. package/src/home/emit-feed-event.ts +158 -0
  500. package/src/home/feed-scheduler.ts +247 -0
  501. package/src/home/feed-types.ts +181 -0
  502. package/src/home/feed-writer.ts +469 -0
  503. package/src/home/platform-gmail-digest.ts +163 -0
  504. package/src/home/progress-formula.ts +86 -0
  505. package/src/home/relationship-state-writer.ts +824 -0
  506. package/src/home/relationship-state.ts +143 -0
  507. package/src/home/rollup-producer.ts +384 -0
  508. package/src/hooks/runner.ts +7 -0
  509. package/src/inbound/platform-callback-registration.ts +30 -20
  510. package/src/inbound/public-ingress-urls.ts +12 -0
  511. package/src/instrument.ts +1 -1
  512. package/src/ipc/__tests__/cli-ipc.test.ts +200 -0
  513. package/src/ipc/cli-client.ts +151 -0
  514. package/src/ipc/cli-server.ts +234 -0
  515. package/src/ipc/gateway-client.ts +180 -0
  516. package/src/ipc/routes/index.ts +5 -0
  517. package/src/ipc/routes/wake-conversation.ts +19 -0
  518. package/src/mcp/client.ts +59 -24
  519. package/src/memory/__tests__/auto-analysis-enqueue.test.ts +356 -0
  520. package/src/memory/__tests__/auto-analysis-guard.test.ts +57 -0
  521. package/src/memory/__tests__/conversation-analyze-job.test.ts +232 -0
  522. package/src/memory/__tests__/find-analysis-conversation.test.ts +196 -0
  523. package/src/memory/app-store.ts +31 -1
  524. package/src/memory/attachments-store.ts +70 -0
  525. package/src/memory/auto-analysis-enqueue.ts +127 -0
  526. package/src/memory/auto-analysis-guard.ts +27 -0
  527. package/src/memory/cleanup-schedule-state.ts +37 -0
  528. package/src/memory/conversation-analyze-job.ts +73 -0
  529. package/src/memory/conversation-crud.ts +122 -0
  530. package/src/memory/conversation-disk-view.ts +7 -0
  531. package/src/memory/conversation-group-migration.ts +34 -2
  532. package/src/memory/conversation-queries.ts +6 -5
  533. package/src/memory/conversation-starters-cadence.ts +76 -0
  534. package/src/memory/conversation-title-service.ts +5 -2
  535. package/src/memory/db-init.ts +18 -0
  536. package/src/memory/db-maintenance.ts +108 -0
  537. package/src/memory/db.ts +1 -0
  538. package/src/memory/embedding-backend.test.ts +75 -0
  539. package/src/memory/embedding-backend.ts +131 -5
  540. package/src/memory/embedding-gemini.test.ts +54 -0
  541. package/src/memory/embedding-gemini.ts +20 -9
  542. package/src/memory/embedding-local.ts +176 -17
  543. package/src/memory/graph/consolidation.ts +10 -23
  544. package/src/memory/graph/conversation-graph-memory.ts +15 -0
  545. package/src/memory/graph/extraction-job.ts +15 -0
  546. package/src/memory/graph/extraction.test.ts +23 -0
  547. package/src/memory/graph/extraction.ts +8 -0
  548. package/src/memory/graph/retriever.ts +67 -40
  549. package/src/memory/graph/scoring.test.ts +186 -0
  550. package/src/memory/graph/scoring.ts +31 -1
  551. package/src/memory/graph/store.test.ts +7 -3
  552. package/src/memory/graph/store.ts +47 -12
  553. package/src/memory/graph/tools.ts +1 -1
  554. package/src/memory/group-crud.ts +6 -1
  555. package/src/memory/indexer.ts +95 -16
  556. package/src/memory/job-handlers/cleanup.ts +11 -8
  557. package/src/memory/job-handlers/conversation-starters.ts +16 -10
  558. package/src/memory/jobs-store.ts +64 -4
  559. package/src/memory/jobs-worker.ts +22 -9
  560. package/src/memory/llm-usage-store.ts +137 -60
  561. package/src/memory/migrations/213-oauth-providers-scope-separator.ts +13 -0
  562. package/src/memory/migrations/214-oauth-providers-refresh-url.ts +11 -0
  563. package/src/memory/migrations/215-oauth-providers-revoke.ts +14 -0
  564. package/src/memory/migrations/216-oauth-providers-token-auth-method.ts +30 -0
  565. package/src/memory/migrations/217-conversation-host-access.ts +40 -0
  566. package/src/memory/migrations/218-oauth-providers-logo-url.ts +11 -0
  567. package/src/memory/migrations/219-oauth-providers-token-exchange-body-format.ts +15 -0
  568. package/src/memory/migrations/220-normalize-user-file-by-principal.ts +190 -0
  569. package/src/memory/migrations/221-conversations-archived-at.ts +16 -0
  570. package/src/memory/migrations/index.ts +12 -0
  571. package/src/memory/migrations/registry.ts +16 -0
  572. package/src/memory/qdrant-manager.ts +43 -16
  573. package/src/memory/schema/conversations.ts +3 -0
  574. package/src/memory/schema/oauth.ts +21 -13
  575. package/src/memory/usage-buckets.ts +396 -0
  576. package/src/messaging/providers/gmail/client.ts +57 -6
  577. package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +282 -0
  578. package/src/messaging/providers/slack/adapter.ts +143 -38
  579. package/src/messaging/providers/slack/client.ts +16 -0
  580. package/src/messaging/providers/slack/types.ts +4 -0
  581. package/src/notifications/decision-engine.ts +3 -3
  582. package/src/notifications/signal.ts +5 -0
  583. package/src/oauth/AGENTS.md +76 -0
  584. package/src/oauth/__tests__/identity-verifier.test.ts +25 -19
  585. package/src/oauth/__tests__/seed-providers-managed.test.ts +32 -0
  586. package/src/oauth/byo-connection.test.ts +26 -9
  587. package/src/oauth/byo-connection.ts +10 -8
  588. package/src/oauth/connect-orchestrator.ts +25 -21
  589. package/src/oauth/connect-types.ts +3 -3
  590. package/src/oauth/connection-resolver.test.ts +17 -4
  591. package/src/oauth/connection-resolver.ts +22 -18
  592. package/src/oauth/connection.ts +3 -1
  593. package/src/oauth/manual-token-connection.ts +13 -13
  594. package/src/oauth/oauth-store.ts +223 -100
  595. package/src/oauth/platform-connection.test.ts +101 -3
  596. package/src/oauth/platform-connection.ts +56 -35
  597. package/src/oauth/provider-serializer.ts +31 -5
  598. package/src/oauth/revoke.ts +76 -0
  599. package/src/oauth/seed-providers.ts +133 -87
  600. package/src/oauth/token-persistence.ts +1 -1
  601. package/src/permissions/checker.ts +16 -6
  602. package/src/permissions/defaults.ts +49 -1
  603. package/src/permissions/permission-mode.ts +4 -11
  604. package/src/permissions/prompter.ts +13 -1
  605. package/src/permissions/trust-store.ts +3 -3
  606. package/src/permissions/v2-consent-policy.ts +87 -0
  607. package/src/permissions/workspace-policy.ts +3 -0
  608. package/src/platform/client.test.ts +10 -0
  609. package/src/platform/sync-identity.ts +129 -0
  610. package/src/prompts/persona-resolver.ts +126 -2
  611. package/src/prompts/system-prompt.ts +76 -38
  612. package/src/prompts/templates/BOOTSTRAP-REFERENCE.md +3 -65
  613. package/src/prompts/templates/BOOTSTRAP.md +59 -105
  614. package/src/prompts/templates/SOUL.md +3 -1
  615. package/src/prompts/templates/UPDATES.md +12 -0
  616. package/src/prompts/templates/channels/slack.md +20 -0
  617. package/src/prompts/update-bulletin-format.ts +26 -9
  618. package/src/prompts/update-bulletin.ts +34 -23
  619. package/src/prompts/user-reference.ts +20 -17
  620. package/src/providers/__tests__/provider-secret-catalog.test.ts +42 -0
  621. package/src/providers/anthropic/client.ts +157 -60
  622. package/src/providers/fireworks/client.ts +2 -2
  623. package/src/providers/gemini/client.ts +9 -1
  624. package/src/providers/model-catalog.ts +6 -0
  625. package/src/providers/model-intents.ts +4 -4
  626. package/src/providers/ollama/client.ts +2 -2
  627. package/src/providers/openai/chat-completions-provider.ts +474 -0
  628. package/src/providers/openai/client.ts +25 -440
  629. package/src/providers/openai/responses-provider.ts +502 -0
  630. package/src/providers/openrouter/client.ts +101 -4
  631. package/src/providers/provider-secret-catalog.ts +139 -0
  632. package/src/providers/registry.ts +2 -2
  633. package/src/providers/retry.ts +14 -3
  634. package/src/providers/speech-to-text/__tests__/provider-catalog.test.ts +251 -0
  635. package/src/providers/speech-to-text/__tests__/resolve.test.ts +828 -0
  636. package/src/providers/speech-to-text/deepgram-realtime.test.ts +980 -0
  637. package/src/providers/speech-to-text/deepgram-realtime.ts +767 -0
  638. package/src/providers/speech-to-text/deepgram.test.ts +332 -0
  639. package/src/providers/speech-to-text/deepgram.ts +115 -0
  640. package/src/providers/speech-to-text/google-gemini-live-stream.test.ts +743 -0
  641. package/src/providers/speech-to-text/google-gemini-live-stream.ts +625 -0
  642. package/src/providers/speech-to-text/google-gemini.test.ts +226 -0
  643. package/src/providers/speech-to-text/google-gemini.ts +101 -0
  644. package/src/providers/speech-to-text/openai-whisper-stream.test.ts +564 -0
  645. package/src/providers/speech-to-text/openai-whisper-stream.ts +381 -0
  646. package/src/providers/speech-to-text/openai-whisper.test.ts +1 -37
  647. package/src/providers/speech-to-text/openai-whisper.ts +63 -33
  648. package/src/providers/speech-to-text/provider-catalog.ts +306 -0
  649. package/src/providers/speech-to-text/resolve.ts +386 -6
  650. package/src/providers/types.ts +10 -1
  651. package/src/runtime/AGENTS.md +65 -0
  652. package/src/runtime/__tests__/agent-wake.test.ts +831 -0
  653. package/src/runtime/__tests__/browser-extension-pair-routes.test.ts +715 -0
  654. package/src/runtime/__tests__/capability-tokens.test.ts +258 -0
  655. package/src/runtime/__tests__/chrome-extension-registry.test.ts +518 -0
  656. package/src/runtime/__tests__/runtime-mode.test.ts +62 -0
  657. package/src/runtime/__tests__/slack-block-formatting.test.ts +481 -0
  658. package/src/runtime/agent-wake.ts +512 -0
  659. package/src/runtime/assistant-event-hub.ts +2 -2
  660. package/src/runtime/auth/__tests__/guard-tests.test.ts +1 -0
  661. package/src/runtime/auth/__tests__/middleware.test.ts +116 -1
  662. package/src/runtime/auth/__tests__/route-policy.test.ts +48 -0
  663. package/src/runtime/auth/middleware.ts +98 -0
  664. package/src/runtime/auth/route-policy.ts +33 -9
  665. package/src/runtime/auth/token-service.ts +56 -1
  666. package/src/runtime/btw-sidechain.ts +2 -0
  667. package/src/runtime/capability-tokens.ts +414 -0
  668. package/src/runtime/channel-approvals.ts +18 -5
  669. package/src/runtime/channel-invite-transport.ts +1 -1
  670. package/src/runtime/channel-invite-transports/email.ts +14 -6
  671. package/src/runtime/channel-readiness-service.ts +12 -22
  672. package/src/runtime/chrome-extension-registry.ts +368 -0
  673. package/src/runtime/confirmation-request-guardian-bridge.ts +6 -0
  674. package/src/runtime/guardian-decision-types.ts +7 -0
  675. package/src/runtime/http-server.ts +815 -75
  676. package/src/runtime/http-types.ts +6 -2
  677. package/src/runtime/migrations/__tests__/rebind-secrets-credentials.test.ts +172 -0
  678. package/src/runtime/migrations/__tests__/vbundle-builder-credentials.test.ts +276 -0
  679. package/src/runtime/migrations/__tests__/vbundle-import-credentials.test.ts +198 -0
  680. package/src/runtime/migrations/__tests__/vbundle-legacy-user-md.test.ts +360 -0
  681. package/src/runtime/migrations/migration-transport.ts +7 -0
  682. package/src/runtime/migrations/migration-wizard.ts +23 -2
  683. package/src/runtime/migrations/rebind-secrets-screen.ts +76 -15
  684. package/src/runtime/migrations/vbundle-builder.ts +145 -38
  685. package/src/runtime/migrations/vbundle-import-analyzer.ts +96 -1
  686. package/src/runtime/migrations/vbundle-importer.ts +89 -5
  687. package/src/runtime/pending-interactions.ts +18 -13
  688. package/src/runtime/routes/__tests__/backup-routes.test.ts +967 -0
  689. package/src/runtime/routes/__tests__/home-feed-routes.test.ts +507 -0
  690. package/src/runtime/routes/__tests__/migration-import-credential-filter.test.ts +208 -0
  691. package/src/runtime/routes/__tests__/stt-routes.test.ts +406 -0
  692. package/src/runtime/routes/__tests__/tts-routes.test.ts +474 -0
  693. package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +148 -17
  694. package/src/runtime/routes/app-management-routes.ts +12 -18
  695. package/src/runtime/routes/approval-routes.ts +90 -16
  696. package/src/runtime/routes/attachment-routes.test.ts +9 -3
  697. package/src/runtime/routes/attachment-routes.ts +216 -17
  698. package/src/runtime/routes/backup-routes.ts +519 -0
  699. package/src/runtime/routes/browser-extension-pair-routes.ts +556 -0
  700. package/src/runtime/routes/btw-routes.ts +8 -6
  701. package/src/runtime/routes/contact-routes.test.ts +298 -0
  702. package/src/runtime/routes/contact-routes.ts +132 -5
  703. package/src/runtime/routes/conversation-analysis-routes.ts +22 -141
  704. package/src/runtime/routes/conversation-management-routes.ts +223 -0
  705. package/src/runtime/routes/conversation-routes.ts +598 -103
  706. package/src/runtime/routes/conversation-starter-routes.ts +78 -16
  707. package/src/runtime/routes/filing-routes.ts +93 -0
  708. package/src/runtime/routes/guardian-action-routes.ts +24 -13
  709. package/src/runtime/routes/home-feed-routes.ts +334 -0
  710. package/src/runtime/routes/home-state-routes.ts +138 -0
  711. package/src/runtime/routes/host-browser-routes.ts +268 -0
  712. package/src/runtime/routes/host-file-routes.ts +9 -1
  713. package/src/runtime/routes/identity-intro-cache.ts +7 -3
  714. package/src/runtime/routes/identity-routes.ts +262 -33
  715. package/src/runtime/routes/inbound-stages/transcribe-audio.test.ts +46 -39
  716. package/src/runtime/routes/inbound-stages/transcribe-audio.ts +15 -15
  717. package/src/runtime/routes/integrations/slack/__tests__/channel.test.ts +137 -0
  718. package/src/runtime/routes/integrations/slack/__tests__/share.test.ts +179 -0
  719. package/src/runtime/routes/integrations/slack/channel.ts +11 -3
  720. package/src/runtime/routes/integrations/slack/share.ts +45 -7
  721. package/src/runtime/routes/llm-context-normalization.ts +303 -0
  722. package/src/runtime/routes/log-export-routes.ts +42 -22
  723. package/src/runtime/routes/memory-item-routes.test.ts +3 -2
  724. package/src/runtime/routes/memory-item-routes.ts +1 -7
  725. package/src/runtime/routes/migration-routes.ts +122 -2
  726. package/src/runtime/routes/oauth-apps.ts +15 -17
  727. package/src/runtime/routes/oauth-providers.ts +4 -0
  728. package/src/runtime/routes/schedule-routes.ts +24 -11
  729. package/src/runtime/routes/settings-routes.ts +31 -102
  730. package/src/runtime/routes/skills-routes.ts +128 -9
  731. package/src/runtime/routes/stt-routes.ts +233 -0
  732. package/src/runtime/routes/subagents-routes.ts +14 -10
  733. package/src/runtime/routes/surface-action-routes.ts +41 -2
  734. package/src/runtime/routes/tts-routes.ts +108 -24
  735. package/src/runtime/routes/usage-routes.ts +38 -9
  736. package/src/runtime/routes/user-route-dispatcher.ts +50 -5
  737. package/src/runtime/routes/user-routes.ts +13 -1
  738. package/src/runtime/routes/work-items-routes.ts +8 -1
  739. package/src/runtime/routes/workspace-routes.test.ts +22 -0
  740. package/src/runtime/routes/workspace-routes.ts +8 -1
  741. package/src/runtime/routes/workspace-utils.ts +2 -0
  742. package/src/runtime/runtime-mode.ts +33 -0
  743. package/src/runtime/services/__tests__/analyze-conversation.test.ts +444 -0
  744. package/src/runtime/services/__tests__/analyze-deps-singleton.test.ts +67 -0
  745. package/src/runtime/services/__tests__/auto-analysis-prompt.test.ts +53 -0
  746. package/src/runtime/services/__tests__/manual-analysis-prompt.test.ts +41 -0
  747. package/src/runtime/services/analyze-conversation.ts +344 -0
  748. package/src/runtime/services/analyze-deps-singleton.ts +32 -0
  749. package/src/runtime/services/auto-analysis-prompt.ts +55 -0
  750. package/src/runtime/skill-route-registry.ts +49 -0
  751. package/src/runtime/slack-block-formatting.ts +437 -10
  752. package/src/schedule/scheduler.ts +57 -5
  753. package/src/security/ces-credential-client.ts +20 -0
  754. package/src/security/ces-rpc-credential-backend.ts +17 -0
  755. package/src/security/credential-backend.ts +5 -0
  756. package/src/security/oauth2.ts +68 -29
  757. package/src/security/secure-keys.ts +143 -27
  758. package/src/security/token-manager.ts +31 -10
  759. package/src/sequence/engine.ts +23 -0
  760. package/src/sequence/types.ts +1 -1
  761. package/src/skills/catalog-files.ts +554 -0
  762. package/src/skills/category-inference.ts +122 -0
  763. package/src/skills/clawhub-files.ts +213 -0
  764. package/src/skills/clawhub.ts +84 -23
  765. package/src/skills/skill-file-provider.ts +40 -0
  766. package/src/skills/skillssh-files.ts +395 -0
  767. package/src/skills/skillssh-registry.ts +4 -4
  768. package/src/stt/__tests__/daemon-batch-transcriber.test.ts +392 -0
  769. package/src/stt/__tests__/types.test.ts +89 -0
  770. package/src/stt/daemon-batch-transcriber.ts +195 -0
  771. package/src/stt/stt-stream-session.ts +499 -0
  772. package/src/stt/types.ts +330 -0
  773. package/src/stt/wav-encoder.test.ts +373 -0
  774. package/src/stt/wav-encoder.ts +175 -0
  775. package/src/subagent/manager.ts +169 -40
  776. package/src/subagent/types.ts +19 -0
  777. package/src/tools/apps/executors.ts +11 -2
  778. package/src/tools/browser/__tests__/auth-detector.test.ts +202 -108
  779. package/src/tools/browser/__tests__/browser-mode.test.ts +119 -0
  780. package/src/tools/browser/__tests__/browser-status.test.ts +123 -0
  781. package/src/tools/browser/auth-detector.ts +43 -12
  782. package/src/tools/browser/browser-execution.ts +1787 -342
  783. package/src/tools/browser/browser-manager.ts +81 -12
  784. package/src/tools/browser/browser-mode-constants.ts +12 -0
  785. package/src/tools/browser/browser-mode.ts +92 -0
  786. package/src/tools/browser/browser-status-constants.ts +33 -0
  787. package/src/tools/browser/cdp-client/__tests__/accessibility-snapshot.test.ts +318 -0
  788. package/src/tools/browser/cdp-client/__tests__/cdp-dom-helpers.test.ts +1175 -0
  789. package/src/tools/browser/cdp-client/__tests__/cdp-inspect-client.test.ts +1263 -0
  790. package/src/tools/browser/cdp-client/__tests__/extension-cdp-client.test.ts +359 -0
  791. package/src/tools/browser/cdp-client/__tests__/factory.test.ts +1993 -0
  792. package/src/tools/browser/cdp-client/__tests__/fixtures/ax-tree-nested-frames.json +64 -0
  793. package/src/tools/browser/cdp-client/__tests__/fixtures/ax-tree-simple.json +69 -0
  794. package/src/tools/browser/cdp-client/__tests__/local-cdp-client.test.ts +310 -0
  795. package/src/tools/browser/cdp-client/__tests__/types.test.ts +96 -0
  796. package/src/tools/browser/cdp-client/accessibility-snapshot.ts +387 -0
  797. package/src/tools/browser/cdp-client/cdp-dom-helpers.ts +695 -0
  798. package/src/tools/browser/cdp-client/cdp-inspect/__tests__/discovery.test.ts +1007 -0
  799. package/src/tools/browser/cdp-client/cdp-inspect/__tests__/ws-transport.test.ts +580 -0
  800. package/src/tools/browser/cdp-client/cdp-inspect/discovery.ts +744 -0
  801. package/src/tools/browser/cdp-client/cdp-inspect/ws-transport.ts +579 -0
  802. package/src/tools/browser/cdp-client/cdp-inspect-client.ts +868 -0
  803. package/src/tools/browser/cdp-client/errors.ts +49 -0
  804. package/src/tools/browser/cdp-client/extension-cdp-client.ts +148 -0
  805. package/src/tools/browser/cdp-client/factory.ts +914 -0
  806. package/src/tools/browser/cdp-client/index.ts +28 -0
  807. package/src/tools/browser/cdp-client/local-cdp-client.ts +187 -0
  808. package/src/tools/browser/cdp-client/types.ts +120 -0
  809. package/src/tools/credentials/vault.ts +35 -6
  810. package/src/tools/filesystem/edit.ts +1 -1
  811. package/src/tools/filesystem/list.ts +1 -1
  812. package/src/tools/filesystem/read.ts +1 -1
  813. package/src/tools/filesystem/write.ts +2 -1
  814. package/src/tools/host-filesystem/edit.ts +1 -1
  815. package/src/tools/host-filesystem/read.ts +12 -15
  816. package/src/tools/host-filesystem/write.ts +1 -1
  817. package/src/tools/host-terminal/host-shell.ts +21 -16
  818. package/src/tools/network/web-fetch.ts +5 -2
  819. package/src/tools/network/web-search.ts +5 -2
  820. package/src/tools/permission-checker.ts +77 -82
  821. package/src/tools/registry.ts +0 -2
  822. package/src/tools/secret-detection-handler.ts +34 -0
  823. package/src/tools/shared/filesystem/image-read.ts +61 -40
  824. package/src/tools/shared/shell-output.ts +3 -1
  825. package/src/tools/side-effects.ts +2 -0
  826. package/src/tools/skills/sandbox-runner.ts +3 -2
  827. package/src/tools/subagent/spawn.ts +47 -3
  828. package/src/tools/subagent/status.ts +2 -0
  829. package/src/tools/system/register.ts +2 -16
  830. package/src/tools/terminal/safe-env.ts +15 -0
  831. package/src/tools/terminal/shell.ts +36 -20
  832. package/src/tools/tool-approval-handler.ts +48 -2
  833. package/src/tools/tool-manifest.ts +21 -0
  834. package/src/tools/types.ts +19 -0
  835. package/src/tools/ui-surface/definitions.ts +6 -1
  836. package/src/tts/__tests__/provider-adapters.test.ts +834 -0
  837. package/src/tts/__tests__/provider-catalog-consistency.test.ts +196 -0
  838. package/src/tts/__tests__/provider-catalog.test.ts +183 -0
  839. package/src/tts/__tests__/provider-registry.test.ts +90 -0
  840. package/src/tts/provider-catalog.ts +201 -0
  841. package/src/tts/provider-registry.ts +73 -0
  842. package/src/tts/providers/deepgram-provider.ts +219 -0
  843. package/src/tts/providers/elevenlabs-provider.ts +211 -0
  844. package/src/tts/providers/fish-audio-provider.ts +183 -0
  845. package/src/tts/providers/index.ts +42 -0
  846. package/src/tts/providers/register-builtins.ts +130 -0
  847. package/src/tts/synthesize-text.ts +110 -0
  848. package/src/tts/tts-config-resolver.ts +78 -0
  849. package/src/tts/types.ts +153 -0
  850. package/src/types/onboarding-context.ts +7 -0
  851. package/src/util/abort-reasons.ts +58 -0
  852. package/src/util/device-id.ts +32 -16
  853. package/src/util/errors.ts +9 -1
  854. package/src/util/platform.ts +63 -24
  855. package/src/util/pricing.ts +66 -3
  856. package/src/util/spawn.ts +1 -1
  857. package/src/util/truncate.ts +4 -2
  858. package/src/util/unicode.ts +201 -0
  859. package/src/version.ts +19 -24
  860. package/src/watcher/engine.ts +23 -0
  861. package/src/watcher/watcher-store.ts +31 -0
  862. package/src/workspace/migrations/003-seed-device-id.ts +9 -3
  863. package/src/workspace/migrations/017-seed-persona-dirs.ts +68 -4
  864. package/src/workspace/migrations/029-seed-pkb.ts +1 -1
  865. package/src/workspace/migrations/031-drop-user-md.ts +317 -0
  866. package/src/workspace/migrations/031-llm-log-retention-zero-to-null.ts +73 -0
  867. package/src/workspace/migrations/032-tts-provider-unification.ts +227 -0
  868. package/src/workspace/migrations/033-stt-service-explicit-config.ts +122 -0
  869. package/src/workspace/migrations/034-remove-calls-voice-transcription-provider.ts +215 -0
  870. package/src/workspace/migrations/035-seed-slack-channel-persona.ts +50 -0
  871. package/src/workspace/migrations/036-update-pkb-index-bar.ts +37 -0
  872. package/src/workspace/migrations/037-create-meets-dir.ts +61 -0
  873. package/src/workspace/migrations/registry.ts +16 -0
  874. package/src/workspace/top-level-renderer.ts +31 -1
  875. package/src/workspace/turn-commit.ts +31 -0
  876. package/src/__tests__/chrome-cdp.test.ts +0 -419
  877. package/src/__tests__/email-cli.test.ts +0 -297
  878. package/src/__tests__/email-service-config-fallback.test.ts +0 -102
  879. package/src/__tests__/permission-mode-sse.test.ts +0 -418
  880. package/src/__tests__/permission-mode-store.test.ts +0 -277
  881. package/src/browser-extension-relay/protocol.ts +0 -63
  882. package/src/browser-extension-relay/server.ts +0 -203
  883. package/src/cli/commands/browser-relay.ts +0 -536
  884. package/src/config/schemas/sandbox.ts +0 -14
  885. package/src/email/guardrails.ts +0 -221
  886. package/src/email/provider.ts +0 -117
  887. package/src/email/providers/agentmail.ts +0 -361
  888. package/src/email/providers/index.ts +0 -65
  889. package/src/email/service.ts +0 -384
  890. package/src/email/types.ts +0 -126
  891. package/src/permissions/permission-mode-store.ts +0 -180
  892. package/src/prompts/templates/USER.md +0 -13
  893. package/src/providers/speech-to-text/types.ts +0 -17
  894. package/src/tools/browser/chrome-cdp.ts +0 -239
  895. package/src/tools/system/set-permission-mode.ts +0 -103
@@ -24,6 +24,8 @@ mock.module("../security/secure-keys.js", () => ({
24
24
  import { eq } from "drizzle-orm";
25
25
 
26
26
  import { getDb, initializeDb, resetDb, resetTestTables } from "../memory/db.js";
27
+ import { getSqliteFrom } from "../memory/db-connection.js";
28
+ import { migrateOAuthProvidersTokenAuthMethodDefault } from "../memory/migrations/216-oauth-providers-token-auth-method.js";
27
29
  import { oauthProviders } from "../memory/schema/oauth.js";
28
30
  import {
29
31
  createConnection,
@@ -43,18 +45,21 @@ import {
43
45
  registerProvider,
44
46
  seedProviders,
45
47
  updateConnection,
48
+ updateProvider,
46
49
  upsertApp,
47
50
  } from "../oauth/oauth-store.js";
51
+ import { seedOAuthProviders } from "../oauth/seed-providers.js";
52
+ import { getMockFetchCalls, mockFetch, resetMockFetch } from "./mock-fetch.js";
48
53
 
49
54
  initializeDb();
50
55
 
51
56
  /** Seed a minimal provider row for FK satisfaction. */
52
- function seedTestProvider(providerKey = "github"): void {
57
+ function seedTestProvider(provider = "github"): void {
53
58
  seedProviders([
54
59
  {
55
- providerKey,
56
- authUrl: `https://${providerKey}.example.com/authorize`,
57
- tokenUrl: `https://${providerKey}.example.com/token`,
60
+ provider,
61
+ authorizeUrl: `https://${provider}.example.com/authorize`,
62
+ tokenExchangeUrl: `https://${provider}.example.com/token`,
58
63
  defaultScopes: ["read"],
59
64
  scopePolicy: {},
60
65
  },
@@ -62,9 +67,9 @@ function seedTestProvider(providerKey = "github"): void {
62
67
  }
63
68
 
64
69
  /** Create an app linked to the given provider. Returns the app row. */
65
- async function createTestApp(providerKey = "github", clientId = "client-1") {
66
- seedTestProvider(providerKey);
67
- return await upsertApp(providerKey, clientId);
70
+ async function createTestApp(provider = "github", clientId = "client-1") {
71
+ seedTestProvider(provider);
72
+ return await upsertApp(provider, clientId);
68
73
  }
69
74
 
70
75
  beforeEach(() => {
@@ -74,6 +79,7 @@ beforeEach(() => {
74
79
  mockDeleteSecureKeyAsync.mockClear();
75
80
  mockSetSecureKeyAsync.mockClear();
76
81
  secureKeyValues.clear();
82
+ resetMockFetch();
77
83
  });
78
84
 
79
85
  afterAll(() => {
@@ -89,34 +95,36 @@ describe("provider operations", () => {
89
95
  test("creates rows for new providers", () => {
90
96
  seedProviders([
91
97
  {
92
- providerKey: "github",
93
- authUrl: "https://github.com/login/oauth/authorize",
94
- tokenUrl: "https://github.com/login/oauth/access_token",
98
+ provider: "github",
99
+ authorizeUrl: "https://github.com/login/oauth/authorize",
100
+ tokenExchangeUrl: "https://github.com/login/oauth/access_token",
95
101
  defaultScopes: ["repo", "user"],
96
102
  scopePolicy: { required: ["repo"] },
97
103
  },
98
104
  {
99
- providerKey: "google",
100
- authUrl: "https://accounts.google.com/o/oauth2/v2/auth",
101
- tokenUrl: "https://oauth2.googleapis.com/token",
105
+ provider: "google",
106
+ authorizeUrl: "https://accounts.google.com/o/oauth2/v2/auth",
107
+ tokenExchangeUrl: "https://oauth2.googleapis.com/token",
102
108
  defaultScopes: ["openid", "email"],
103
109
  scopePolicy: {},
104
- extraParams: { access_type: "offline" },
110
+ authorizeParams: { access_type: "offline" },
105
111
  },
106
112
  ]);
107
113
 
108
114
  const gh = getProvider("github");
109
115
  expect(gh).toBeDefined();
110
- expect(gh!.providerKey).toBe("github");
111
- expect(gh!.authUrl).toBe("https://github.com/login/oauth/authorize");
112
- expect(gh!.tokenUrl).toBe("https://github.com/login/oauth/access_token");
116
+ expect(gh!.provider).toBe("github");
117
+ expect(gh!.authorizeUrl).toBe("https://github.com/login/oauth/authorize");
118
+ expect(gh!.tokenExchangeUrl).toBe(
119
+ "https://github.com/login/oauth/access_token",
120
+ );
113
121
  expect(JSON.parse(gh!.defaultScopes)).toEqual(["repo", "user"]);
114
122
  expect(JSON.parse(gh!.scopePolicy)).toEqual({ required: ["repo"] });
115
123
 
116
124
  const goog = getProvider("google");
117
125
  expect(goog).toBeDefined();
118
- expect(goog!.providerKey).toBe("google");
119
- expect(JSON.parse(goog!.extraParams!)).toEqual({
126
+ expect(goog!.provider).toBe("google");
127
+ expect(JSON.parse(goog!.authorizeParams!)).toEqual({
120
128
  access_type: "offline",
121
129
  });
122
130
  });
@@ -124,9 +132,9 @@ describe("provider operations", () => {
124
132
  test("updates implementation fields while preserving user-customizable fields on re-seed", () => {
125
133
  seedProviders([
126
134
  {
127
- providerKey: "github",
128
- authUrl: "https://github.com/login/oauth/authorize",
129
- tokenUrl: "https://github.com/login/oauth/access_token",
135
+ provider: "github",
136
+ authorizeUrl: "https://github.com/login/oauth/authorize",
137
+ tokenExchangeUrl: "https://github.com/login/oauth/access_token",
130
138
  defaultScopes: ["repo"],
131
139
  scopePolicy: {},
132
140
  baseUrl: "https://api.github.com",
@@ -141,9 +149,9 @@ describe("provider operations", () => {
141
149
  // Re-seed with corrected values (simulates a code fix deployed on upgrade)
142
150
  seedProviders([
143
151
  {
144
- providerKey: "github",
145
- authUrl: "https://github.com/login/oauth/authorize-v2",
146
- tokenUrl: "https://github.com/login/oauth/access_token-v2",
152
+ provider: "github",
153
+ authorizeUrl: "https://github.com/login/oauth/authorize-v2",
154
+ tokenExchangeUrl: "https://github.com/login/oauth/access_token-v2",
147
155
  defaultScopes: ["repo", "user"],
148
156
  scopePolicy: { required: ["repo"] },
149
157
  baseUrl: "https://api.github.com/v2",
@@ -153,8 +161,10 @@ describe("provider operations", () => {
153
161
  const row = getProvider("github");
154
162
  expect(row).toBeDefined();
155
163
  // Implementation fields should be overwritten by the re-seed
156
- expect(row!.authUrl).toBe("https://github.com/login/oauth/authorize-v2");
157
- expect(row!.tokenUrl).toBe(
164
+ expect(row!.authorizeUrl).toBe(
165
+ "https://github.com/login/oauth/authorize-v2",
166
+ );
167
+ expect(row!.tokenExchangeUrl).toBe(
158
168
  "https://github.com/login/oauth/access_token-v2",
159
169
  );
160
170
  // User-customizable fields (baseUrl, defaultScopes, scopePolicy) are
@@ -166,172 +176,1069 @@ describe("provider operations", () => {
166
176
  expect(row!.createdAt).toBe(originalCreatedAt);
167
177
  });
168
178
 
169
- test("persists pingUrl when provided", () => {
179
+ test("persists pingUrl when provided", () => {
180
+ seedProviders([
181
+ {
182
+ provider: "github",
183
+ authorizeUrl: "https://github.com/authorize",
184
+ tokenExchangeUrl: "https://github.com/token",
185
+ defaultScopes: ["repo"],
186
+ scopePolicy: {},
187
+ pingUrl: "https://api.github.com/user",
188
+ },
189
+ ]);
190
+ const row = getProvider("github");
191
+ expect(row!.pingUrl).toBe("https://api.github.com/user");
192
+ });
193
+
194
+ test("pingUrl defaults to null when omitted", () => {
195
+ seedProviders([
196
+ {
197
+ provider: "github",
198
+ authorizeUrl: "https://github.com/authorize",
199
+ tokenExchangeUrl: "https://github.com/token",
200
+ defaultScopes: ["repo"],
201
+ scopePolicy: {},
202
+ },
203
+ ]);
204
+ const row = getProvider("github");
205
+ expect(row!.pingUrl).toBeNull();
206
+ });
207
+
208
+ test("preserves user-customizable fields while overwriting implementation fields on re-seed", () => {
209
+ // Initial seed with all fields
210
+ seedProviders([
211
+ {
212
+ provider: "github",
213
+ authorizeUrl: "https://github.com/authorize",
214
+ tokenExchangeUrl: "https://github.com/token",
215
+ tokenEndpointAuthMethod: "client_secret_post",
216
+ defaultScopes: ["repo"],
217
+ scopePolicy: { required: ["repo"] },
218
+ userinfoUrl: "https://api.github.com/user",
219
+ baseUrl: "https://api.github.com",
220
+ authorizeParams: { prompt: "consent" },
221
+
222
+ pingUrl: "https://api.github.com/user",
223
+ },
224
+ ]);
225
+
226
+ // Manually update user-customizable fields to simulate user edits
227
+ const db = getDb();
228
+ db.update(oauthProviders)
229
+ .set({
230
+ defaultScopes: JSON.stringify(["repo", "user", "gist"]),
231
+ scopePolicy: JSON.stringify({
232
+ required: ["repo"],
233
+ allowAdditionalScopes: true,
234
+ }),
235
+ baseUrl: "https://custom.github.com/api",
236
+ })
237
+ .where(eq(oauthProviders.provider, "github"))
238
+ .run();
239
+
240
+ // Verify the manual updates took effect
241
+ const beforeReseed = getProvider("github");
242
+ expect(JSON.parse(beforeReseed!.defaultScopes)).toEqual([
243
+ "repo",
244
+ "user",
245
+ "gist",
246
+ ]);
247
+ expect(beforeReseed!.baseUrl).toBe("https://custom.github.com/api");
248
+
249
+ // Re-seed with updated implementation fields
250
+ seedProviders([
251
+ {
252
+ provider: "github",
253
+ authorizeUrl: "https://github.com/authorize-v2",
254
+ tokenExchangeUrl: "https://github.com/token-v2",
255
+ tokenEndpointAuthMethod: "client_secret_basic",
256
+ defaultScopes: ["repo-only"],
257
+ scopePolicy: {},
258
+ userinfoUrl: "https://api.github.com/user-v2",
259
+ baseUrl: "https://api.github.com/v2",
260
+ authorizeParams: { prompt: "login" },
261
+
262
+ pingUrl: "https://api.github.com/user-v2",
263
+ },
264
+ ]);
265
+
266
+ const row = getProvider("github");
267
+ expect(row).toBeDefined();
268
+
269
+ // User-customizable fields should retain their manual values
270
+ expect(JSON.parse(row!.defaultScopes)).toEqual(["repo", "user", "gist"]);
271
+ expect(JSON.parse(row!.scopePolicy)).toEqual({
272
+ required: ["repo"],
273
+ allowAdditionalScopes: true,
274
+ });
275
+ expect(row!.baseUrl).toBe("https://custom.github.com/api");
276
+
277
+ // Implementation fields should be overwritten from the seed data
278
+ expect(row!.authorizeUrl).toBe("https://github.com/authorize-v2");
279
+ expect(row!.tokenExchangeUrl).toBe("https://github.com/token-v2");
280
+ expect(row!.tokenEndpointAuthMethod).toBe("client_secret_basic");
281
+ expect(row!.userinfoUrl).toBe("https://api.github.com/user-v2");
282
+ expect(JSON.parse(row!.authorizeParams!)).toEqual({ prompt: "login" });
283
+ expect(row!.pingUrl).toBe("https://api.github.com/user-v2");
284
+ });
285
+
286
+ test("persists custom scopeSeparator when provided", () => {
287
+ seedProviders([
288
+ {
289
+ provider: "test-provider",
290
+ authorizeUrl: "https://example.com/authorize",
291
+ tokenExchangeUrl: "https://example.com/token",
292
+ defaultScopes: ["read", "write"],
293
+ scopePolicy: {},
294
+ scopeSeparator: ",",
295
+ },
296
+ ]);
297
+
298
+ const row = getProvider("test-provider");
299
+ expect(row).toBeDefined();
300
+ expect(row!.scopeSeparator).toBe(",");
301
+ });
302
+
303
+ test("scopeSeparator defaults to ' ' when omitted", () => {
304
+ seedProviders([
305
+ {
306
+ provider: "github",
307
+ authorizeUrl: "https://github.com/authorize",
308
+ tokenExchangeUrl: "https://github.com/token",
309
+ defaultScopes: ["repo"],
310
+ scopePolicy: {},
311
+ },
312
+ ]);
313
+
314
+ const row = getProvider("github");
315
+ expect(row).toBeDefined();
316
+ expect(row!.scopeSeparator).toBe(" ");
317
+ });
318
+
319
+ test("re-seeding with a changed scopeSeparator overwrites the stored value", () => {
320
+ seedProviders([
321
+ {
322
+ provider: "linear",
323
+ authorizeUrl: "https://linear.app/oauth/authorize",
324
+ tokenExchangeUrl: "https://api.linear.app/oauth/token",
325
+ defaultScopes: ["read"],
326
+ scopePolicy: {},
327
+ scopeSeparator: " ",
328
+ },
329
+ ]);
330
+
331
+ const first = getProvider("linear");
332
+ expect(first!.scopeSeparator).toBe(" ");
333
+
334
+ // Re-seed with a different separator — it should be overwritten,
335
+ // proving scopeSeparator is in the onConflictDoUpdate set clause.
336
+ seedProviders([
337
+ {
338
+ provider: "linear",
339
+ authorizeUrl: "https://linear.app/oauth/authorize",
340
+ tokenExchangeUrl: "https://api.linear.app/oauth/token",
341
+ defaultScopes: ["read"],
342
+ scopePolicy: {},
343
+ scopeSeparator: ",",
344
+ },
345
+ ]);
346
+
347
+ const row = getProvider("linear");
348
+ expect(row!.scopeSeparator).toBe(",");
349
+ });
350
+
351
+ test("persists refreshUrl when provided", () => {
352
+ seedProviders([
353
+ {
354
+ provider: "test-provider",
355
+ authorizeUrl: "https://example.com/authorize",
356
+ tokenExchangeUrl: "https://example.com/token",
357
+ refreshUrl: "https://refresh.example.com/token",
358
+ defaultScopes: ["read"],
359
+ scopePolicy: {},
360
+ },
361
+ ]);
362
+
363
+ const row = getProvider("test-provider");
364
+ expect(row).toBeDefined();
365
+ expect(row!.refreshUrl).toBe("https://refresh.example.com/token");
366
+ });
367
+
368
+ test("refreshUrl defaults to null when omitted", () => {
369
+ seedProviders([
370
+ {
371
+ provider: "test-provider",
372
+ authorizeUrl: "https://example.com/authorize",
373
+ tokenExchangeUrl: "https://example.com/token",
374
+ defaultScopes: ["read"],
375
+ scopePolicy: {},
376
+ },
377
+ ]);
378
+
379
+ const row = getProvider("test-provider");
380
+ expect(row).toBeDefined();
381
+ expect(row!.refreshUrl).toBeNull();
382
+ });
383
+
384
+ test("re-seeding with a changed refreshUrl overwrites the stored value", () => {
385
+ seedProviders([
386
+ {
387
+ provider: "test-provider",
388
+ authorizeUrl: "https://example.com/authorize",
389
+ tokenExchangeUrl: "https://example.com/token",
390
+ refreshUrl: "https://refresh.example.com/token",
391
+ defaultScopes: ["read"],
392
+ scopePolicy: {},
393
+ },
394
+ ]);
395
+
396
+ const first = getProvider("test-provider");
397
+ expect(first!.refreshUrl).toBe("https://refresh.example.com/token");
398
+
399
+ // Re-seed with a different refreshUrl — it should be overwritten,
400
+ // proving refreshUrl is in the onConflictDoUpdate set clause (not
401
+ // preserved like defaultScopes).
402
+ seedProviders([
403
+ {
404
+ provider: "test-provider",
405
+ authorizeUrl: "https://example.com/authorize",
406
+ tokenExchangeUrl: "https://example.com/token",
407
+ refreshUrl: "https://refresh-v2.example.com/token",
408
+ defaultScopes: ["read"],
409
+ scopePolicy: {},
410
+ },
411
+ ]);
412
+
413
+ const row = getProvider("test-provider");
414
+ expect(row!.refreshUrl).toBe("https://refresh-v2.example.com/token");
415
+ });
416
+
417
+ test("persists revokeUrl and revokeBodyTemplate when provided", () => {
418
+ seedProviders([
419
+ {
420
+ provider: "test-provider",
421
+ authorizeUrl: "https://example.com/authorize",
422
+ tokenExchangeUrl: "https://example.com/token",
423
+ revokeUrl: "https://revoke.example.com",
424
+ revokeBodyTemplate: { token: "{access_token}" },
425
+ defaultScopes: ["read"],
426
+ scopePolicy: {},
427
+ },
428
+ ]);
429
+
430
+ const row = getProvider("test-provider");
431
+ expect(row).toBeDefined();
432
+ expect(row!.revokeUrl).toBe("https://revoke.example.com");
433
+ expect(JSON.parse(row!.revokeBodyTemplate!)).toEqual({
434
+ token: "{access_token}",
435
+ });
436
+ });
437
+
438
+ test("revokeUrl and revokeBodyTemplate default to null when omitted", () => {
439
+ seedProviders([
440
+ {
441
+ provider: "test-provider",
442
+ authorizeUrl: "https://example.com/authorize",
443
+ tokenExchangeUrl: "https://example.com/token",
444
+ defaultScopes: ["read"],
445
+ scopePolicy: {},
446
+ },
447
+ ]);
448
+
449
+ const row = getProvider("test-provider");
450
+ expect(row).toBeDefined();
451
+ expect(row!.revokeUrl).toBeNull();
452
+ expect(row!.revokeBodyTemplate).toBeNull();
453
+ });
454
+
455
+ test("writes logoUrl on insert", () => {
456
+ seedProviders([
457
+ {
458
+ provider: "google",
459
+ authorizeUrl: "https://accounts.google.com/o/oauth2/v2/auth",
460
+ tokenExchangeUrl: "https://oauth2.googleapis.com/token",
461
+ defaultScopes: ["openid"],
462
+ scopePolicy: {},
463
+ logoUrl: "https://cdn.simpleicons.org/google",
464
+ },
465
+ ]);
466
+
467
+ const row = getProvider("google");
468
+ expect(row).toBeDefined();
469
+ expect(row!.logoUrl).toBe("https://cdn.simpleicons.org/google");
470
+ });
471
+
472
+ test("overwrites logoUrl on conflict", () => {
473
+ seedProviders([
474
+ {
475
+ provider: "google",
476
+ authorizeUrl: "https://accounts.google.com/o/oauth2/v2/auth",
477
+ tokenExchangeUrl: "https://oauth2.googleapis.com/token",
478
+ defaultScopes: ["openid"],
479
+ scopePolicy: {},
480
+ logoUrl: "https://cdn.simpleicons.org/google",
481
+ },
482
+ ]);
483
+
484
+ expect(getProvider("google")!.logoUrl).toBe(
485
+ "https://cdn.simpleicons.org/google",
486
+ );
487
+
488
+ // Re-seed with a different logoUrl — it should be overwritten,
489
+ // proving logoUrl is in the onConflictDoUpdate set clause alongside
490
+ // the other display-metadata fields.
491
+ seedProviders([
492
+ {
493
+ provider: "google",
494
+ authorizeUrl: "https://accounts.google.com/o/oauth2/v2/auth",
495
+ tokenExchangeUrl: "https://oauth2.googleapis.com/token",
496
+ defaultScopes: ["openid"],
497
+ scopePolicy: {},
498
+ logoUrl: "https://cdn.simpleicons.org/google-v2",
499
+ },
500
+ ]);
501
+
502
+ const row = getProvider("google");
503
+ expect(row!.logoUrl).toBe("https://cdn.simpleicons.org/google-v2");
504
+ });
505
+
506
+ test("re-seeding with a changed revokeUrl overwrites the stored value", () => {
507
+ seedProviders([
508
+ {
509
+ provider: "test-provider",
510
+ authorizeUrl: "https://example.com/authorize",
511
+ tokenExchangeUrl: "https://example.com/token",
512
+ revokeUrl: "https://revoke.example.com",
513
+ defaultScopes: ["read"],
514
+ scopePolicy: {},
515
+ },
516
+ ]);
517
+
518
+ const first = getProvider("test-provider");
519
+ expect(first!.revokeUrl).toBe("https://revoke.example.com");
520
+
521
+ // Re-seed with a different revokeUrl — it should be overwritten,
522
+ // proving revokeUrl is in the onConflictDoUpdate set clause (not
523
+ // preserved like defaultScopes).
524
+ seedProviders([
525
+ {
526
+ provider: "test-provider",
527
+ authorizeUrl: "https://example.com/authorize",
528
+ tokenExchangeUrl: "https://example.com/token",
529
+ revokeUrl: "https://revoke-v2.example.com",
530
+ defaultScopes: ["read"],
531
+ scopePolicy: {},
532
+ },
533
+ ]);
534
+
535
+ const row = getProvider("test-provider");
536
+ expect(row!.revokeUrl).toBe("https://revoke-v2.example.com");
537
+ });
538
+
539
+ test("seedOAuthProviders seeds Google, Twitter, and Linear with revoke config and leaves other providers null", () => {
540
+ seedOAuthProviders();
541
+
542
+ const google = getProvider("google");
543
+ expect(google).toBeDefined();
544
+ expect(google!.revokeUrl).toBe("https://oauth2.googleapis.com/revoke");
545
+ expect(JSON.parse(google!.revokeBodyTemplate!)).toEqual({
546
+ token: "{access_token}",
547
+ });
548
+
549
+ const twitter = getProvider("twitter");
550
+ expect(twitter).toBeDefined();
551
+ expect(twitter!.revokeUrl).toBe("https://api.x.com/2/oauth2/revoke");
552
+ expect(JSON.parse(twitter!.revokeBodyTemplate!)).toEqual({
553
+ token: "{access_token}",
554
+ token_type_hint: "access_token",
555
+ client_id: "{client_id}",
556
+ });
557
+
558
+ const linear = getProvider("linear");
559
+ expect(linear).toBeDefined();
560
+ expect(linear!.revokeUrl).toBe("https://api.linear.app/oauth/revoke");
561
+ expect(JSON.parse(linear!.revokeBodyTemplate!)).toEqual({
562
+ token: "{access_token}",
563
+ });
564
+
565
+ const slack = getProvider("slack");
566
+ expect(slack).toBeDefined();
567
+ expect(slack!.revokeUrl).toBeNull();
568
+ expect(slack!.revokeBodyTemplate).toBeNull();
569
+
570
+ const github = getProvider("github");
571
+ expect(github).toBeDefined();
572
+ expect(github!.revokeUrl).toBeNull();
573
+
574
+ const outlook = getProvider("outlook");
575
+ expect(outlook).toBeDefined();
576
+ expect(outlook!.revokeUrl).toBeNull();
577
+ });
578
+
579
+ test("applies client_secret_post default when tokenEndpointAuthMethod is omitted from seed", () => {
580
+ seedProviders([
581
+ {
582
+ provider: "no-auth-method-provider",
583
+ authorizeUrl: "https://example.com/authorize",
584
+ tokenExchangeUrl: "https://example.com/token",
585
+ defaultScopes: [],
586
+ scopePolicy: {},
587
+ // Note: tokenEndpointAuthMethod intentionally omitted
588
+ },
589
+ ]);
590
+ const row = getProvider("no-auth-method-provider");
591
+ expect(row).toBeDefined();
592
+ expect(row!.tokenEndpointAuthMethod).toBe("client_secret_post");
593
+ });
594
+
595
+ test("defaults tokenExchangeBodyFormat to 'form' when omitted from seed", () => {
596
+ seedProviders([
597
+ {
598
+ provider: "no-body-format-provider",
599
+ authorizeUrl: "https://example.com/authorize",
600
+ tokenExchangeUrl: "https://example.com/token",
601
+ defaultScopes: [],
602
+ scopePolicy: {},
603
+ // Note: tokenExchangeBodyFormat intentionally omitted
604
+ },
605
+ ]);
606
+ const row = getProvider("no-body-format-provider");
607
+ expect(row).toBeDefined();
608
+ expect(row!.tokenExchangeBodyFormat).toBe("form");
609
+ });
610
+
611
+ test("persists explicit tokenExchangeBodyFormat value on seed", () => {
612
+ seedProviders([
613
+ {
614
+ provider: "json-body-format-provider",
615
+ authorizeUrl: "https://example.com/authorize",
616
+ tokenExchangeUrl: "https://example.com/token",
617
+ defaultScopes: [],
618
+ scopePolicy: {},
619
+ tokenExchangeBodyFormat: "json",
620
+ },
621
+ ]);
622
+ const row = getProvider("json-body-format-provider");
623
+ expect(row).toBeDefined();
624
+ expect(row!.tokenExchangeBodyFormat).toBe("json");
625
+ });
626
+
627
+ test("migration 216 backfills NULL token_endpoint_auth_method to client_secret_post", () => {
628
+ // Use raw SQLite to bypass Drizzle's NOT NULL enforcement and insert
629
+ // a legacy-shaped row with NULL token_endpoint_auth_method.
630
+ const db = getDb();
631
+ const raw = getSqliteFrom(db);
632
+ raw.exec(`
633
+ INSERT INTO oauth_providers (
634
+ provider_key, auth_url, token_url, token_endpoint_auth_method,
635
+ default_scopes, scope_policy, scope_separator, requires_client_secret,
636
+ created_at, updated_at
637
+ ) VALUES (
638
+ 'legacy-null-provider',
639
+ 'https://example.com/authorize',
640
+ 'https://example.com/token',
641
+ NULL,
642
+ '[]',
643
+ '{}',
644
+ ' ',
645
+ 1,
646
+ ${Date.now()},
647
+ ${Date.now()}
648
+ )
649
+ `);
650
+
651
+ // Run the migration directly
652
+ migrateOAuthProvidersTokenAuthMethodDefault(db);
653
+
654
+ // Verify the row was backfilled
655
+ const row = raw
656
+ .prepare(
657
+ `SELECT token_endpoint_auth_method FROM oauth_providers WHERE provider_key = 'legacy-null-provider'`,
658
+ )
659
+ .get() as { token_endpoint_auth_method: string };
660
+ expect(row.token_endpoint_auth_method).toBe("client_secret_post");
661
+ });
662
+
663
+ test("migration 216 is idempotent — running twice on backfilled rows is a no-op", () => {
664
+ seedProviders([
665
+ {
666
+ provider: "already-set-provider",
667
+ authorizeUrl: "https://example.com/authorize",
668
+ tokenExchangeUrl: "https://example.com/token",
669
+ tokenEndpointAuthMethod: "client_secret_basic",
670
+ defaultScopes: [],
671
+ scopePolicy: {},
672
+ },
673
+ ]);
674
+
675
+ const db = getDb();
676
+ migrateOAuthProvidersTokenAuthMethodDefault(db);
677
+ migrateOAuthProvidersTokenAuthMethodDefault(db);
678
+
679
+ const row = getProvider("already-set-provider");
680
+ expect(row!.tokenEndpointAuthMethod).toBe("client_secret_basic");
681
+ });
682
+ });
683
+
684
+ describe("getProvider", () => {
685
+ test("returns the correct row", () => {
686
+ seedProviders([
687
+ {
688
+ provider: "github",
689
+ authorizeUrl: "https://github.com/authorize",
690
+ tokenExchangeUrl: "https://github.com/token",
691
+ defaultScopes: ["repo"],
692
+ scopePolicy: {},
693
+ },
694
+ ]);
695
+
696
+ const row = getProvider("github");
697
+ expect(row).toBeDefined();
698
+ expect(row!.provider).toBe("github");
699
+ });
700
+
701
+ test("returns undefined for unknown keys", () => {
702
+ expect(getProvider("nonexistent")).toBeUndefined();
703
+ });
704
+ });
705
+
706
+ describe("registerProvider", () => {
707
+ test("creates a new row", () => {
708
+ const row = registerProvider({
709
+ provider: "linear",
710
+ authorizeUrl: "https://linear.app/oauth/authorize",
711
+ tokenExchangeUrl: "https://api.linear.app/oauth/token",
712
+ defaultScopes: ["read"],
713
+ scopePolicy: {},
714
+ });
715
+
716
+ expect(row.provider).toBe("linear");
717
+ expect(row.authorizeUrl).toBe("https://linear.app/oauth/authorize");
718
+
719
+ const fetched = getProvider("linear");
720
+ expect(fetched).toBeDefined();
721
+ expect(fetched!.provider).toBe("linear");
722
+ });
723
+
724
+ test("throws for duplicate provider_key", () => {
725
+ registerProvider({
726
+ provider: "linear",
727
+ authorizeUrl: "https://linear.app/oauth/authorize",
728
+ tokenExchangeUrl: "https://api.linear.app/oauth/token",
729
+ defaultScopes: ["read"],
730
+ scopePolicy: {},
731
+ });
732
+
733
+ expect(() =>
734
+ registerProvider({
735
+ provider: "linear",
736
+ authorizeUrl: "https://linear.app/oauth/authorize",
737
+ tokenExchangeUrl: "https://api.linear.app/oauth/token",
738
+ defaultScopes: ["read"],
739
+ scopePolicy: {},
740
+ }),
741
+ ).toThrow(/already exists.*linear/);
742
+ });
743
+
744
+ test("persists scopeSeparator and round-trips via getProvider", () => {
745
+ registerProvider({
746
+ provider: "linear",
747
+ authorizeUrl: "https://linear.app/oauth/authorize",
748
+ tokenExchangeUrl: "https://api.linear.app/oauth/token",
749
+ defaultScopes: ["read"],
750
+ scopePolicy: {},
751
+ scopeSeparator: ";",
752
+ });
753
+
754
+ const fetched = getProvider("linear");
755
+ expect(fetched).toBeDefined();
756
+ expect(fetched!.scopeSeparator).toBe(";");
757
+ });
758
+
759
+ test("scopeSeparator defaults to ' ' when omitted", () => {
760
+ registerProvider({
761
+ provider: "linear",
762
+ authorizeUrl: "https://linear.app/oauth/authorize",
763
+ tokenExchangeUrl: "https://api.linear.app/oauth/token",
764
+ defaultScopes: ["read"],
765
+ scopePolicy: {},
766
+ });
767
+
768
+ const fetched = getProvider("linear");
769
+ expect(fetched).toBeDefined();
770
+ expect(fetched!.scopeSeparator).toBe(" ");
771
+ });
772
+
773
+ test("persists refreshUrl and round-trips via getProvider", () => {
774
+ registerProvider({
775
+ provider: "linear",
776
+ authorizeUrl: "https://linear.app/oauth/authorize",
777
+ tokenExchangeUrl: "https://api.linear.app/oauth/token",
778
+ refreshUrl: "https://api.linear.app/oauth/refresh",
779
+ defaultScopes: ["read"],
780
+ scopePolicy: {},
781
+ });
782
+
783
+ const fetched = getProvider("linear");
784
+ expect(fetched).toBeDefined();
785
+ expect(fetched!.refreshUrl).toBe("https://api.linear.app/oauth/refresh");
786
+ });
787
+
788
+ test("refreshUrl defaults to null when omitted", () => {
789
+ registerProvider({
790
+ provider: "linear",
791
+ authorizeUrl: "https://linear.app/oauth/authorize",
792
+ tokenExchangeUrl: "https://api.linear.app/oauth/token",
793
+ defaultScopes: ["read"],
794
+ scopePolicy: {},
795
+ });
796
+
797
+ const fetched = getProvider("linear");
798
+ expect(fetched).toBeDefined();
799
+ expect(fetched!.refreshUrl).toBeNull();
800
+ });
801
+
802
+ test("persists revokeUrl and revokeBodyTemplate and round-trips via getProvider", () => {
803
+ registerProvider({
804
+ provider: "linear",
805
+ authorizeUrl: "https://linear.app/oauth/authorize",
806
+ tokenExchangeUrl: "https://api.linear.app/oauth/token",
807
+ revokeUrl: "https://api.linear.app/oauth/revoke",
808
+ revokeBodyTemplate: { token: "{access_token}" },
809
+ defaultScopes: ["read"],
810
+ scopePolicy: {},
811
+ });
812
+
813
+ const fetched = getProvider("linear");
814
+ expect(fetched).toBeDefined();
815
+ expect(fetched!.revokeUrl).toBe("https://api.linear.app/oauth/revoke");
816
+ expect(JSON.parse(fetched!.revokeBodyTemplate!)).toEqual({
817
+ token: "{access_token}",
818
+ });
819
+ });
820
+
821
+ test("applies client_secret_post default when tokenEndpointAuthMethod is omitted", () => {
822
+ const row = registerProvider({
823
+ provider: "custom-default-test",
824
+ authorizeUrl: "https://example.com/authorize",
825
+ tokenExchangeUrl: "https://example.com/token",
826
+ defaultScopes: [],
827
+ scopePolicy: {},
828
+ // Note: tokenEndpointAuthMethod intentionally omitted
829
+ });
830
+ expect(row.tokenEndpointAuthMethod).toBe("client_secret_post");
831
+
832
+ const fetched = getProvider("custom-default-test");
833
+ expect(fetched!.tokenEndpointAuthMethod).toBe("client_secret_post");
834
+ });
835
+
836
+ test("preserves explicit client_secret_basic when registering a provider", () => {
837
+ const row = registerProvider({
838
+ provider: "custom-basic-test",
839
+ authorizeUrl: "https://example.com/authorize",
840
+ tokenExchangeUrl: "https://example.com/token",
841
+ defaultScopes: [],
842
+ scopePolicy: {},
843
+ tokenEndpointAuthMethod: "client_secret_basic",
844
+ });
845
+ expect(row.tokenEndpointAuthMethod).toBe("client_secret_basic");
846
+ });
847
+
848
+ test("defaults tokenExchangeBodyFormat to 'form' when omitted", () => {
849
+ const row = registerProvider({
850
+ provider: "no-body-format-test",
851
+ authorizeUrl: "https://example.com/authorize",
852
+ tokenExchangeUrl: "https://example.com/token",
853
+ defaultScopes: [],
854
+ scopePolicy: {},
855
+ // Note: tokenExchangeBodyFormat intentionally omitted
856
+ });
857
+ expect(row.tokenExchangeBodyFormat).toBe("form");
858
+
859
+ const fetched = getProvider("no-body-format-test");
860
+ expect(fetched!.tokenExchangeBodyFormat).toBe("form");
861
+ });
862
+
863
+ test("persists explicit tokenExchangeBodyFormat 'json' when registering a provider", () => {
864
+ const row = registerProvider({
865
+ provider: "json-body-format-test",
866
+ authorizeUrl: "https://example.com/authorize",
867
+ tokenExchangeUrl: "https://example.com/token",
868
+ defaultScopes: [],
869
+ scopePolicy: {},
870
+ tokenExchangeBodyFormat: "json",
871
+ });
872
+ expect(row.tokenExchangeBodyFormat).toBe("json");
873
+ });
874
+
875
+ test("stores logoUrl when provided", () => {
876
+ registerProvider({
877
+ provider: "notion",
878
+ authorizeUrl: "https://api.notion.com/v1/oauth/authorize",
879
+ tokenExchangeUrl: "https://api.notion.com/v1/oauth/token",
880
+ defaultScopes: ["read"],
881
+ scopePolicy: {},
882
+ logoUrl: "https://cdn.simpleicons.org/notion",
883
+ });
884
+
885
+ const fetched = getProvider("notion");
886
+ expect(fetched).toBeDefined();
887
+ expect(fetched!.logoUrl).toBe("https://cdn.simpleicons.org/notion");
888
+ });
889
+
890
+ test("defaults logoUrl to null when omitted", () => {
891
+ registerProvider({
892
+ provider: "linear",
893
+ authorizeUrl: "https://linear.app/oauth/authorize",
894
+ tokenExchangeUrl: "https://api.linear.app/oauth/token",
895
+ defaultScopes: ["read"],
896
+ scopePolicy: {},
897
+ });
898
+
899
+ const fetched = getProvider("linear");
900
+ expect(fetched).toBeDefined();
901
+ expect(fetched!.logoUrl).toBeNull();
902
+ });
903
+ });
904
+
905
+ describe("updateProvider", () => {
906
+ test("updates scopeSeparator on an existing row", () => {
907
+ seedProviders([
908
+ {
909
+ provider: "github",
910
+ authorizeUrl: "https://github.com/authorize",
911
+ tokenExchangeUrl: "https://github.com/token",
912
+ defaultScopes: ["repo"],
913
+ scopePolicy: {},
914
+ },
915
+ ]);
916
+
917
+ const before = getProvider("github");
918
+ expect(before!.scopeSeparator).toBe(" ");
919
+
920
+ const updated = updateProvider("github", { scopeSeparator: "," });
921
+ expect(updated).toBeDefined();
922
+ expect(updated!.scopeSeparator).toBe(",");
923
+
924
+ const fetched = getProvider("github");
925
+ expect(fetched!.scopeSeparator).toBe(",");
926
+ });
927
+
928
+ test("coerces empty-string scopeSeparator to default ' '", () => {
929
+ // An empty separator would join scopes into a single concatenated token
930
+ // (e.g. ["read","write"].join("") === "readwrite") which is never a
931
+ // valid OAuth authorize URL value. Coerce to the default.
932
+ seedProviders([
933
+ {
934
+ provider: "github",
935
+ authorizeUrl: "https://github.com/authorize",
936
+ tokenExchangeUrl: "https://github.com/token",
937
+ defaultScopes: ["repo"],
938
+ scopePolicy: {},
939
+ scopeSeparator: ",",
940
+ },
941
+ ]);
942
+
943
+ expect(getProvider("github")!.scopeSeparator).toBe(",");
944
+
945
+ const updated = updateProvider("github", { scopeSeparator: "" });
946
+ expect(updated).toBeDefined();
947
+ expect(updated!.scopeSeparator).toBe(" ");
948
+ expect(getProvider("github")!.scopeSeparator).toBe(" ");
949
+ });
950
+
951
+ test("sets refreshUrl on an existing row where it was previously null", () => {
952
+ seedProviders([
953
+ {
954
+ provider: "github",
955
+ authorizeUrl: "https://github.com/authorize",
956
+ tokenExchangeUrl: "https://github.com/token",
957
+ defaultScopes: ["repo"],
958
+ scopePolicy: {},
959
+ },
960
+ ]);
961
+
962
+ const before = getProvider("github");
963
+ expect(before!.refreshUrl).toBeNull();
964
+
965
+ const updated = updateProvider("github", {
966
+ refreshUrl: "https://github.com/login/oauth/refresh",
967
+ });
968
+ expect(updated).toBeDefined();
969
+ expect(updated!.refreshUrl).toBe(
970
+ "https://github.com/login/oauth/refresh",
971
+ );
972
+
973
+ const fetched = getProvider("github");
974
+ expect(fetched!.refreshUrl).toBe(
975
+ "https://github.com/login/oauth/refresh",
976
+ );
977
+ });
978
+
979
+ test("leaves refreshUrl unchanged when not passed to updateProvider", () => {
980
+ seedProviders([
981
+ {
982
+ provider: "github",
983
+ authorizeUrl: "https://github.com/authorize",
984
+ tokenExchangeUrl: "https://github.com/token",
985
+ refreshUrl: "https://github.com/login/oauth/refresh",
986
+ defaultScopes: ["repo"],
987
+ scopePolicy: {},
988
+ },
989
+ ]);
990
+
991
+ expect(getProvider("github")!.refreshUrl).toBe(
992
+ "https://github.com/login/oauth/refresh",
993
+ );
994
+
995
+ // Update a different field — refreshUrl should be left alone.
996
+ const updated = updateProvider("github", {
997
+ displayLabel: "GitHub (updated)",
998
+ });
999
+ expect(updated).toBeDefined();
1000
+ expect(updated!.refreshUrl).toBe(
1001
+ "https://github.com/login/oauth/refresh",
1002
+ );
1003
+ expect(updated!.displayLabel).toBe("GitHub (updated)");
1004
+ });
1005
+
1006
+ test("sets revokeUrl on an existing row where it was previously null", () => {
170
1007
  seedProviders([
171
1008
  {
172
- providerKey: "github",
173
- authUrl: "https://github.com/authorize",
174
- tokenUrl: "https://github.com/token",
1009
+ provider: "github",
1010
+ authorizeUrl: "https://github.com/authorize",
1011
+ tokenExchangeUrl: "https://github.com/token",
175
1012
  defaultScopes: ["repo"],
176
1013
  scopePolicy: {},
177
- pingUrl: "https://api.github.com/user",
178
1014
  },
179
1015
  ]);
180
- const row = getProvider("github");
181
- expect(row!.pingUrl).toBe("https://api.github.com/user");
1016
+
1017
+ const before = getProvider("github");
1018
+ expect(before!.revokeUrl).toBeNull();
1019
+
1020
+ const updated = updateProvider("github", {
1021
+ revokeUrl: "https://github.com/login/oauth/revoke",
1022
+ });
1023
+ expect(updated).toBeDefined();
1024
+ expect(updated!.revokeUrl).toBe("https://github.com/login/oauth/revoke");
1025
+
1026
+ const fetched = getProvider("github");
1027
+ expect(fetched!.revokeUrl).toBe("https://github.com/login/oauth/revoke");
182
1028
  });
183
1029
 
184
- test("pingUrl defaults to null when omitted", () => {
1030
+ test("sets revokeBodyTemplate on an existing row and JSON round-trips", () => {
185
1031
  seedProviders([
186
1032
  {
187
- providerKey: "github",
188
- authUrl: "https://github.com/authorize",
189
- tokenUrl: "https://github.com/token",
1033
+ provider: "github",
1034
+ authorizeUrl: "https://github.com/authorize",
1035
+ tokenExchangeUrl: "https://github.com/token",
190
1036
  defaultScopes: ["repo"],
191
1037
  scopePolicy: {},
192
1038
  },
193
1039
  ]);
194
- const row = getProvider("github");
195
- expect(row!.pingUrl).toBeNull();
1040
+
1041
+ const before = getProvider("github");
1042
+ expect(before!.revokeBodyTemplate).toBeNull();
1043
+
1044
+ const updated = updateProvider("github", {
1045
+ revokeBodyTemplate: {
1046
+ token: "{access_token}",
1047
+ client_id: "{client_id}",
1048
+ },
1049
+ });
1050
+ expect(updated).toBeDefined();
1051
+ expect(JSON.parse(updated!.revokeBodyTemplate!)).toEqual({
1052
+ token: "{access_token}",
1053
+ client_id: "{client_id}",
1054
+ });
1055
+
1056
+ const fetched = getProvider("github");
1057
+ expect(JSON.parse(fetched!.revokeBodyTemplate!)).toEqual({
1058
+ token: "{access_token}",
1059
+ client_id: "{client_id}",
1060
+ });
196
1061
  });
197
1062
 
198
- test("preserves user-customizable fields while overwriting implementation fields on re-seed", () => {
199
- // Initial seed with all fields
1063
+ test("leaves revokeUrl and revokeBodyTemplate unchanged when not passed to updateProvider", () => {
200
1064
  seedProviders([
201
1065
  {
202
- providerKey: "github",
203
- authUrl: "https://github.com/authorize",
204
- tokenUrl: "https://github.com/token",
205
- tokenEndpointAuthMethod: "client_secret_post",
1066
+ provider: "github",
1067
+ authorizeUrl: "https://github.com/authorize",
1068
+ tokenExchangeUrl: "https://github.com/token",
1069
+ revokeUrl: "https://github.com/login/oauth/revoke",
1070
+ revokeBodyTemplate: { token: "{access_token}" },
206
1071
  defaultScopes: ["repo"],
207
- scopePolicy: { required: ["repo"] },
208
- userinfoUrl: "https://api.github.com/user",
209
- baseUrl: "https://api.github.com",
210
- extraParams: { prompt: "consent" },
211
-
212
- pingUrl: "https://api.github.com/user",
1072
+ scopePolicy: {},
213
1073
  },
214
1074
  ]);
215
1075
 
216
- // Manually update user-customizable fields to simulate user edits
217
- const db = getDb();
218
- db.update(oauthProviders)
219
- .set({
220
- defaultScopes: JSON.stringify(["repo", "user", "gist"]),
221
- scopePolicy: JSON.stringify({
222
- required: ["repo"],
223
- allowAdditionalScopes: true,
224
- }),
225
- baseUrl: "https://custom.github.com/api",
226
- })
227
- .where(eq(oauthProviders.providerKey, "github"))
228
- .run();
1076
+ expect(getProvider("github")!.revokeUrl).toBe(
1077
+ "https://github.com/login/oauth/revoke",
1078
+ );
1079
+ expect(JSON.parse(getProvider("github")!.revokeBodyTemplate!)).toEqual({
1080
+ token: "{access_token}",
1081
+ });
229
1082
 
230
- // Verify the manual updates took effect
231
- const beforeReseed = getProvider("github");
232
- expect(JSON.parse(beforeReseed!.defaultScopes)).toEqual([
233
- "repo",
234
- "user",
235
- "gist",
236
- ]);
237
- expect(beforeReseed!.baseUrl).toBe("https://custom.github.com/api");
1083
+ // Update a different field revoke fields should be left alone.
1084
+ const updated = updateProvider("github", {
1085
+ displayLabel: "GitHub (updated)",
1086
+ });
1087
+ expect(updated).toBeDefined();
1088
+ expect(updated!.revokeUrl).toBe("https://github.com/login/oauth/revoke");
1089
+ expect(JSON.parse(updated!.revokeBodyTemplate!)).toEqual({
1090
+ token: "{access_token}",
1091
+ });
1092
+ expect(updated!.displayLabel).toBe("GitHub (updated)");
1093
+ });
238
1094
 
239
- // Re-seed with updated implementation fields
1095
+ test("coerces empty string tokenEndpointAuthMethod to client_secret_post", () => {
240
1096
  seedProviders([
241
1097
  {
242
- providerKey: "github",
243
- authUrl: "https://github.com/authorize-v2",
244
- tokenUrl: "https://github.com/token-v2",
1098
+ provider: "update-empty-test",
1099
+ authorizeUrl: "https://example.com/authorize",
1100
+ tokenExchangeUrl: "https://example.com/token",
245
1101
  tokenEndpointAuthMethod: "client_secret_basic",
246
- defaultScopes: ["repo-only"],
1102
+ defaultScopes: [],
247
1103
  scopePolicy: {},
248
- userinfoUrl: "https://api.github.com/user-v2",
249
- baseUrl: "https://api.github.com/v2",
250
- extraParams: { prompt: "login" },
251
-
252
- pingUrl: "https://api.github.com/user-v2",
253
1104
  },
254
1105
  ]);
255
1106
 
256
- const row = getProvider("github");
257
- expect(row).toBeDefined();
1107
+ expect(getProvider("update-empty-test")!.tokenEndpointAuthMethod).toBe(
1108
+ "client_secret_basic",
1109
+ );
258
1110
 
259
- // User-customizable fields should retain their manual values
260
- expect(JSON.parse(row!.defaultScopes)).toEqual(["repo", "user", "gist"]);
261
- expect(JSON.parse(row!.scopePolicy)).toEqual({
262
- required: ["repo"],
263
- allowAdditionalScopes: true,
1111
+ const updated = updateProvider("update-empty-test", {
1112
+ tokenEndpointAuthMethod: "",
264
1113
  });
265
- expect(row!.baseUrl).toBe("https://custom.github.com/api");
1114
+ expect(updated).toBeDefined();
1115
+ expect(updated!.tokenEndpointAuthMethod).toBe("client_secret_post");
266
1116
 
267
- // Implementation fields should be overwritten from the seed data
268
- expect(row!.authUrl).toBe("https://github.com/authorize-v2");
269
- expect(row!.tokenUrl).toBe("https://github.com/token-v2");
270
- expect(row!.tokenEndpointAuthMethod).toBe("client_secret_basic");
271
- expect(row!.userinfoUrl).toBe("https://api.github.com/user-v2");
272
- expect(JSON.parse(row!.extraParams!)).toEqual({ prompt: "login" });
273
- expect(row!.pingUrl).toBe("https://api.github.com/user-v2");
1117
+ const row = getProvider("update-empty-test");
1118
+ expect(row!.tokenEndpointAuthMethod).toBe("client_secret_post");
274
1119
  });
275
- });
276
1120
 
277
- describe("getProvider", () => {
278
- test("returns the correct row", () => {
1121
+ test("coerces empty string tokenExchangeBodyFormat to 'form'", () => {
279
1122
  seedProviders([
280
1123
  {
281
- providerKey: "github",
282
- authUrl: "https://github.com/authorize",
283
- tokenUrl: "https://github.com/token",
284
- defaultScopes: ["repo"],
1124
+ provider: "update-empty-body-format-test",
1125
+ authorizeUrl: "https://example.com/authorize",
1126
+ tokenExchangeUrl: "https://example.com/token",
1127
+ tokenExchangeBodyFormat: "json",
1128
+ defaultScopes: [],
285
1129
  scopePolicy: {},
286
1130
  },
287
1131
  ]);
288
1132
 
289
- const row = getProvider("github");
290
- expect(row).toBeDefined();
291
- expect(row!.providerKey).toBe("github");
292
- });
1133
+ expect(
1134
+ getProvider("update-empty-body-format-test")!.tokenExchangeBodyFormat,
1135
+ ).toBe("json");
293
1136
 
294
- test("returns undefined for unknown keys", () => {
295
- expect(getProvider("nonexistent")).toBeUndefined();
1137
+ const updated = updateProvider("update-empty-body-format-test", {
1138
+ tokenExchangeBodyFormat: "",
1139
+ });
1140
+ expect(updated).toBeDefined();
1141
+ expect(updated!.tokenExchangeBodyFormat).toBe("form");
1142
+
1143
+ const row = getProvider("update-empty-body-format-test");
1144
+ expect(row!.tokenExchangeBodyFormat).toBe("form");
296
1145
  });
297
- });
298
1146
 
299
- describe("registerProvider", () => {
300
- test("creates a new row", () => {
301
- const row = registerProvider({
302
- providerKey: "linear",
303
- authUrl: "https://linear.app/oauth/authorize",
304
- tokenUrl: "https://api.linear.app/oauth/token",
1147
+ test("sets logoUrl on an existing row where it was previously null", () => {
1148
+ registerProvider({
1149
+ provider: "linear",
1150
+ authorizeUrl: "https://linear.app/oauth/authorize",
1151
+ tokenExchangeUrl: "https://api.linear.app/oauth/token",
305
1152
  defaultScopes: ["read"],
306
1153
  scopePolicy: {},
307
1154
  });
308
1155
 
309
- expect(row.providerKey).toBe("linear");
310
- expect(row.authUrl).toBe("https://linear.app/oauth/authorize");
1156
+ expect(getProvider("linear")!.logoUrl).toBeNull();
1157
+
1158
+ const updated = updateProvider("linear", {
1159
+ logoUrl: "https://cdn.simpleicons.org/linear",
1160
+ });
1161
+ expect(updated).toBeDefined();
1162
+ expect(updated!.logoUrl).toBe("https://cdn.simpleicons.org/linear");
311
1163
 
312
1164
  const fetched = getProvider("linear");
313
- expect(fetched).toBeDefined();
314
- expect(fetched!.providerKey).toBe("linear");
1165
+ expect(fetched!.logoUrl).toBe("https://cdn.simpleicons.org/linear");
315
1166
  });
316
1167
 
317
- test("throws for duplicate provider_key", () => {
1168
+ test("clears logoUrl when passed null", () => {
318
1169
  registerProvider({
319
- providerKey: "linear",
320
- authUrl: "https://linear.app/oauth/authorize",
321
- tokenUrl: "https://api.linear.app/oauth/token",
1170
+ provider: "notion",
1171
+ authorizeUrl: "https://api.notion.com/v1/oauth/authorize",
1172
+ tokenExchangeUrl: "https://api.notion.com/v1/oauth/token",
322
1173
  defaultScopes: ["read"],
323
1174
  scopePolicy: {},
1175
+ logoUrl: "https://cdn.simpleicons.org/notion",
324
1176
  });
325
1177
 
326
- expect(() =>
327
- registerProvider({
328
- providerKey: "linear",
329
- authUrl: "https://linear.app/oauth/authorize",
330
- tokenUrl: "https://api.linear.app/oauth/token",
1178
+ expect(getProvider("notion")!.logoUrl).toBe(
1179
+ "https://cdn.simpleicons.org/notion",
1180
+ );
1181
+
1182
+ const updated = updateProvider("notion", { logoUrl: null });
1183
+ expect(updated).toBeDefined();
1184
+ expect(updated!.logoUrl).toBeNull();
1185
+
1186
+ expect(getProvider("notion")!.logoUrl).toBeNull();
1187
+ });
1188
+
1189
+ test("leaves logoUrl unchanged when not passed to updateProvider", () => {
1190
+ registerProvider({
1191
+ provider: "notion",
1192
+ authorizeUrl: "https://api.notion.com/v1/oauth/authorize",
1193
+ tokenExchangeUrl: "https://api.notion.com/v1/oauth/token",
1194
+ defaultScopes: ["read"],
1195
+ scopePolicy: {},
1196
+ logoUrl: "https://cdn.simpleicons.org/notion",
1197
+ });
1198
+
1199
+ expect(getProvider("notion")!.logoUrl).toBe(
1200
+ "https://cdn.simpleicons.org/notion",
1201
+ );
1202
+
1203
+ // Update a different field — logoUrl should be left alone.
1204
+ const updated = updateProvider("notion", {
1205
+ displayLabel: "Notion (updated)",
1206
+ });
1207
+ expect(updated).toBeDefined();
1208
+ expect(updated!.logoUrl).toBe("https://cdn.simpleicons.org/notion");
1209
+ expect(updated!.displayLabel).toBe("Notion (updated)");
1210
+ });
1211
+ });
1212
+
1213
+ describe("scopeSeparator empty-string coercion", () => {
1214
+ test("seedProviders coerces empty-string scopeSeparator to ' '", () => {
1215
+ seedProviders([
1216
+ {
1217
+ provider: "linear",
1218
+ authorizeUrl: "https://linear.app/oauth/authorize",
1219
+ tokenExchangeUrl: "https://api.linear.app/oauth/token",
331
1220
  defaultScopes: ["read"],
332
1221
  scopePolicy: {},
333
- }),
334
- ).toThrow(/already exists.*linear/);
1222
+ scopeSeparator: "",
1223
+ },
1224
+ ]);
1225
+
1226
+ const row = getProvider("linear");
1227
+ expect(row!.scopeSeparator).toBe(" ");
1228
+ });
1229
+
1230
+ test("registerProvider coerces empty-string scopeSeparator to ' '", () => {
1231
+ registerProvider({
1232
+ provider: "linear",
1233
+ authorizeUrl: "https://linear.app/oauth/authorize",
1234
+ tokenExchangeUrl: "https://api.linear.app/oauth/token",
1235
+ defaultScopes: ["read"],
1236
+ scopePolicy: {},
1237
+ scopeSeparator: "",
1238
+ });
1239
+
1240
+ const row = getProvider("linear");
1241
+ expect(row!.scopeSeparator).toBe(" ");
335
1242
  });
336
1243
  });
337
1244
  });
@@ -351,13 +1258,13 @@ describe("app operations", () => {
351
1258
  expect(app.id).toMatch(
352
1259
  /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/,
353
1260
  );
354
- expect(app.providerKey).toBe("github");
1261
+ expect(app.provider).toBe("github");
355
1262
  expect(app.clientId).toBe("client-abc");
356
1263
  expect(app.createdAt).toBeGreaterThan(0);
357
1264
  expect(app.updatedAt).toBeGreaterThan(0);
358
1265
  });
359
1266
 
360
- test("returns the existing app when called again with same (providerKey, clientId)", async () => {
1267
+ test("returns the existing app when called again with same (provider, clientId)", async () => {
361
1268
  seedTestProvider("github");
362
1269
  const first = await upsertApp("github", "client-abc");
363
1270
  const second = await upsertApp("github", "client-abc");
@@ -476,7 +1383,7 @@ describe("app operations", () => {
476
1383
 
477
1384
  expect(fetched).toBeDefined();
478
1385
  expect(fetched!.id).toBe(app.id);
479
- expect(fetched!.providerKey).toBe("github");
1386
+ expect(fetched!.provider).toBe("github");
480
1387
  expect(fetched!.clientId).toBe("client-1");
481
1388
  });
482
1389
 
@@ -564,7 +1471,7 @@ describe("connection operations", () => {
564
1471
  const app = await createTestApp("github", "client-1");
565
1472
  const conn = createConnection({
566
1473
  oauthAppId: app.id,
567
- providerKey: "github",
1474
+ provider: "github",
568
1475
  grantedScopes: ["repo", "user"],
569
1476
  hasRefreshToken: true,
570
1477
  accountInfo: "user@example.com",
@@ -574,7 +1481,7 @@ describe("connection operations", () => {
574
1481
 
575
1482
  expect(conn.id).toBeTruthy();
576
1483
  expect(conn.oauthAppId).toBe(app.id);
577
- expect(conn.providerKey).toBe("github");
1484
+ expect(conn.provider).toBe("github");
578
1485
  expect(conn.status).toBe("active");
579
1486
  expect(JSON.parse(conn.grantedScopes)).toEqual(["repo", "user"]);
580
1487
  expect(conn.hasRefreshToken).toBe(1);
@@ -590,7 +1497,7 @@ describe("connection operations", () => {
590
1497
  const app = await createTestApp("github", "client-1");
591
1498
  const conn = createConnection({
592
1499
  oauthAppId: app.id,
593
- providerKey: "github",
1500
+ provider: "github",
594
1501
  grantedScopes: ["repo"],
595
1502
  hasRefreshToken: false,
596
1503
  });
@@ -598,7 +1505,7 @@ describe("connection operations", () => {
598
1505
  const fetched = getConnection(conn.id);
599
1506
  expect(fetched).toBeDefined();
600
1507
  expect(fetched!.id).toBe(conn.id);
601
- expect(fetched!.providerKey).toBe("github");
1508
+ expect(fetched!.provider).toBe("github");
602
1509
  });
603
1510
 
604
1511
  test("returns undefined for unknown id", () => {
@@ -612,7 +1519,7 @@ describe("connection operations", () => {
612
1519
 
613
1520
  createConnection({
614
1521
  oauthAppId: app.id,
615
- providerKey: "github",
1522
+ provider: "github",
616
1523
  grantedScopes: ["repo"],
617
1524
  hasRefreshToken: false,
618
1525
  createdAt: 1000,
@@ -620,7 +1527,7 @@ describe("connection operations", () => {
620
1527
 
621
1528
  const conn2 = createConnection({
622
1529
  oauthAppId: app.id,
623
- providerKey: "github",
1530
+ provider: "github",
624
1531
  grantedScopes: ["repo", "user"],
625
1532
  hasRefreshToken: true,
626
1533
  createdAt: 2000,
@@ -636,7 +1543,7 @@ describe("connection operations", () => {
636
1543
 
637
1544
  const conn1 = createConnection({
638
1545
  oauthAppId: app.id,
639
- providerKey: "github",
1546
+ provider: "github",
640
1547
  accountInfo: "user1@example.com",
641
1548
  grantedScopes: ["repo"],
642
1549
  hasRefreshToken: false,
@@ -645,7 +1552,7 @@ describe("connection operations", () => {
645
1552
 
646
1553
  createConnection({
647
1554
  oauthAppId: app.id,
648
- providerKey: "github",
1555
+ provider: "github",
649
1556
  accountInfo: "user2@example.com",
650
1557
  grantedScopes: ["repo"],
651
1558
  hasRefreshToken: false,
@@ -665,7 +1572,7 @@ describe("connection operations", () => {
665
1572
 
666
1573
  const conn1 = createConnection({
667
1574
  oauthAppId: app1.id,
668
- providerKey: "github",
1575
+ provider: "github",
669
1576
  grantedScopes: ["repo"],
670
1577
  hasRefreshToken: false,
671
1578
  createdAt: 1000,
@@ -673,7 +1580,7 @@ describe("connection operations", () => {
673
1580
 
674
1581
  createConnection({
675
1582
  oauthAppId: app2.id,
676
- providerKey: "github",
1583
+ provider: "github",
677
1584
  grantedScopes: ["repo"],
678
1585
  hasRefreshToken: false,
679
1586
  createdAt: 2000,
@@ -689,7 +1596,7 @@ describe("connection operations", () => {
689
1596
 
690
1597
  createConnection({
691
1598
  oauthAppId: app.id,
692
- providerKey: "github",
1599
+ provider: "github",
693
1600
  grantedScopes: ["repo"],
694
1601
  hasRefreshToken: false,
695
1602
  });
@@ -705,7 +1612,7 @@ describe("connection operations", () => {
705
1612
 
706
1613
  const conn = createConnection({
707
1614
  oauthAppId: app.id,
708
- providerKey: "github",
1615
+ provider: "github",
709
1616
  grantedScopes: ["repo"],
710
1617
  hasRefreshToken: false,
711
1618
  });
@@ -727,7 +1634,7 @@ describe("connection operations", () => {
727
1634
  // Create two connections with explicit timestamps so ordering is deterministic
728
1635
  createConnection({
729
1636
  oauthAppId: app.id,
730
- providerKey: "github",
1637
+ provider: "github",
731
1638
  grantedScopes: ["repo"],
732
1639
  hasRefreshToken: false,
733
1640
  createdAt: 1000,
@@ -735,7 +1642,7 @@ describe("connection operations", () => {
735
1642
 
736
1643
  const conn2 = createConnection({
737
1644
  oauthAppId: app.id,
738
- providerKey: "github",
1645
+ provider: "github",
739
1646
  grantedScopes: ["repo", "user"],
740
1647
  hasRefreshToken: true,
741
1648
  createdAt: 2000,
@@ -751,14 +1658,14 @@ describe("connection operations", () => {
751
1658
 
752
1659
  const conn1 = createConnection({
753
1660
  oauthAppId: app.id,
754
- providerKey: "github",
1661
+ provider: "github",
755
1662
  grantedScopes: ["repo"],
756
1663
  hasRefreshToken: false,
757
1664
  });
758
1665
 
759
1666
  const conn2 = createConnection({
760
1667
  oauthAppId: app.id,
761
- providerKey: "github",
1668
+ provider: "github",
762
1669
  grantedScopes: ["repo", "user"],
763
1670
  hasRefreshToken: true,
764
1671
  });
@@ -776,7 +1683,7 @@ describe("connection operations", () => {
776
1683
 
777
1684
  const conn = createConnection({
778
1685
  oauthAppId: app.id,
779
- providerKey: "github",
1686
+ provider: "github",
780
1687
  grantedScopes: ["repo"],
781
1688
  hasRefreshToken: false,
782
1689
  });
@@ -798,7 +1705,7 @@ describe("connection operations", () => {
798
1705
 
799
1706
  const conn1 = createConnection({
800
1707
  oauthAppId: app.id,
801
- providerKey: "github",
1708
+ provider: "github",
802
1709
  accountInfo: "user1@example.com",
803
1710
  grantedScopes: ["repo"],
804
1711
  hasRefreshToken: false,
@@ -807,7 +1714,7 @@ describe("connection operations", () => {
807
1714
 
808
1715
  createConnection({
809
1716
  oauthAppId: app.id,
810
- providerKey: "github",
1717
+ provider: "github",
811
1718
  accountInfo: "user2@example.com",
812
1719
  grantedScopes: ["repo"],
813
1720
  hasRefreshToken: false,
@@ -827,7 +1734,7 @@ describe("connection operations", () => {
827
1734
 
828
1735
  const conn = createConnection({
829
1736
  oauthAppId: app.id,
830
- providerKey: "github",
1737
+ provider: "github",
831
1738
  accountInfo: "user@example.com",
832
1739
  grantedScopes: ["repo"],
833
1740
  hasRefreshToken: false,
@@ -843,7 +1750,7 @@ describe("connection operations", () => {
843
1750
 
844
1751
  createConnection({
845
1752
  oauthAppId: app.id,
846
- providerKey: "github",
1753
+ provider: "github",
847
1754
  accountInfo: "user@example.com",
848
1755
  grantedScopes: ["repo"],
849
1756
  hasRefreshToken: false,
@@ -861,7 +1768,7 @@ describe("connection operations", () => {
861
1768
 
862
1769
  const conn = createConnection({
863
1770
  oauthAppId: app.id,
864
- providerKey: "github",
1771
+ provider: "github",
865
1772
  accountInfo: "user@example.com",
866
1773
  grantedScopes: ["repo"],
867
1774
  hasRefreshToken: false,
@@ -882,7 +1789,7 @@ describe("connection operations", () => {
882
1789
 
883
1790
  createConnection({
884
1791
  oauthAppId: app.id,
885
- providerKey: "github",
1792
+ provider: "github",
886
1793
  accountInfo: "user1@example.com",
887
1794
  grantedScopes: ["repo"],
888
1795
  hasRefreshToken: false,
@@ -890,7 +1797,7 @@ describe("connection operations", () => {
890
1797
 
891
1798
  createConnection({
892
1799
  oauthAppId: app.id,
893
- providerKey: "github",
1800
+ provider: "github",
894
1801
  accountInfo: "user2@example.com",
895
1802
  grantedScopes: ["repo"],
896
1803
  hasRefreshToken: false,
@@ -905,7 +1812,7 @@ describe("connection operations", () => {
905
1812
 
906
1813
  createConnection({
907
1814
  oauthAppId: app.id,
908
- providerKey: "github",
1815
+ provider: "github",
909
1816
  accountInfo: "user1@example.com",
910
1817
  grantedScopes: ["repo"],
911
1818
  hasRefreshToken: false,
@@ -913,7 +1820,7 @@ describe("connection operations", () => {
913
1820
 
914
1821
  const conn2 = createConnection({
915
1822
  oauthAppId: app.id,
916
- providerKey: "github",
1823
+ provider: "github",
917
1824
  accountInfo: "user2@example.com",
918
1825
  grantedScopes: ["repo"],
919
1826
  hasRefreshToken: false,
@@ -936,7 +1843,7 @@ describe("connection operations", () => {
936
1843
  const app = await createTestApp("github", "client-1");
937
1844
  const conn = createConnection({
938
1845
  oauthAppId: app.id,
939
- providerKey: "github",
1846
+ provider: "github",
940
1847
  grantedScopes: ["repo"],
941
1848
  hasRefreshToken: false,
942
1849
  });
@@ -950,7 +1857,7 @@ describe("connection operations", () => {
950
1857
  const app = await createTestApp("github", "client-1");
951
1858
  createConnection({
952
1859
  oauthAppId: app.id,
953
- providerKey: "github",
1860
+ provider: "github",
954
1861
  grantedScopes: ["repo"],
955
1862
  hasRefreshToken: false,
956
1863
  });
@@ -967,7 +1874,7 @@ describe("connection operations", () => {
967
1874
  const app = await createTestApp("github", "client-1");
968
1875
  const conn = createConnection({
969
1876
  oauthAppId: app.id,
970
- providerKey: "github",
1877
+ provider: "github",
971
1878
  grantedScopes: ["repo"],
972
1879
  hasRefreshToken: false,
973
1880
  });
@@ -984,7 +1891,7 @@ describe("connection operations", () => {
984
1891
  const app = await createTestApp("github", "client-1");
985
1892
  const conn = createConnection({
986
1893
  oauthAppId: app.id,
987
- providerKey: "github",
1894
+ provider: "github",
988
1895
  grantedScopes: ["repo"],
989
1896
  hasRefreshToken: false,
990
1897
  });
@@ -1021,7 +1928,7 @@ describe("connection operations", () => {
1021
1928
 
1022
1929
  const conn = createConnection({
1023
1930
  oauthAppId: app1.id,
1024
- providerKey: "github",
1931
+ provider: "github",
1025
1932
  grantedScopes: ["repo"],
1026
1933
  hasRefreshToken: false,
1027
1934
  });
@@ -1051,13 +1958,13 @@ describe("connection operations", () => {
1051
1958
 
1052
1959
  createConnection({
1053
1960
  oauthAppId: ghApp.id,
1054
- providerKey: "github",
1961
+ provider: "github",
1055
1962
  grantedScopes: ["repo"],
1056
1963
  hasRefreshToken: false,
1057
1964
  });
1058
1965
  createConnection({
1059
1966
  oauthAppId: googApp.id,
1060
- providerKey: "google",
1967
+ provider: "google",
1061
1968
  grantedScopes: ["email"],
1062
1969
  hasRefreshToken: true,
1063
1970
  });
@@ -1073,24 +1980,24 @@ describe("connection operations", () => {
1073
1980
 
1074
1981
  createConnection({
1075
1982
  oauthAppId: ghApp.id,
1076
- providerKey: "github",
1983
+ provider: "github",
1077
1984
  grantedScopes: ["repo"],
1078
1985
  hasRefreshToken: false,
1079
1986
  });
1080
1987
  createConnection({
1081
1988
  oauthAppId: googApp.id,
1082
- providerKey: "google",
1989
+ provider: "google",
1083
1990
  grantedScopes: ["email"],
1084
1991
  hasRefreshToken: true,
1085
1992
  });
1086
1993
 
1087
1994
  const ghConns = listConnections("github");
1088
1995
  expect(ghConns).toHaveLength(1);
1089
- expect(ghConns[0].providerKey).toBe("github");
1996
+ expect(ghConns[0].provider).toBe("github");
1090
1997
 
1091
1998
  const googConns = listConnections("google");
1092
1999
  expect(googConns).toHaveLength(1);
1093
- expect(googConns[0].providerKey).toBe("google");
2000
+ expect(googConns[0].provider).toBe("google");
1094
2001
  });
1095
2002
 
1096
2003
  test("returns empty array when no connections exist", () => {
@@ -1103,7 +2010,7 @@ describe("connection operations", () => {
1103
2010
  const app = await createTestApp("github", "client-1");
1104
2011
  const conn = createConnection({
1105
2012
  oauthAppId: app.id,
1106
- providerKey: "github",
2013
+ provider: "github",
1107
2014
  grantedScopes: ["repo"],
1108
2015
  hasRefreshToken: false,
1109
2016
  });
@@ -1124,17 +2031,40 @@ describe("connection operations", () => {
1124
2031
  // ---------------------------------------------------------------------------
1125
2032
 
1126
2033
  describe("disconnectOAuthProvider", () => {
2034
+ /**
2035
+ * Seed a provider with revokeUrl and (optionally) a revokeBodyTemplate.
2036
+ */
2037
+ function seedProviderWithRevoke(
2038
+ provider: string,
2039
+ revokeUrl: string | null,
2040
+ revokeBodyTemplate?: Record<string, string>,
2041
+ ): void {
2042
+ seedProviders([
2043
+ {
2044
+ provider,
2045
+ authorizeUrl: `https://${provider}.example.com/authorize`,
2046
+ tokenExchangeUrl: `https://${provider}.example.com/token`,
2047
+ defaultScopes: ["read"],
2048
+ scopePolicy: {},
2049
+ ...(revokeUrl ? { revokeUrl } : {}),
2050
+ ...(revokeBodyTemplate ? { revokeBodyTemplate } : {}),
2051
+ },
2052
+ ]);
2053
+ }
2054
+
1127
2055
  test("returns 'not-found' when no connection exists for the provider", async () => {
1128
2056
  const result = await disconnectOAuthProvider("github");
1129
2057
  expect(result).toBe("not-found");
1130
2058
  expect(mockDeleteSecureKeyAsync).not.toHaveBeenCalled();
2059
+ // No upstream call should be made when there is no connection at all.
2060
+ expect(getMockFetchCalls().length).toBe(0);
1131
2061
  });
1132
2062
 
1133
2063
  test("returns 'disconnected' and deletes connection row and secure keys when connection exists", async () => {
1134
2064
  const app = await createTestApp("github", "client-1");
1135
2065
  const conn = createConnection({
1136
2066
  oauthAppId: app.id,
1137
- providerKey: "github",
2067
+ provider: "github",
1138
2068
  grantedScopes: ["repo"],
1139
2069
  hasRefreshToken: true,
1140
2070
  });
@@ -1154,6 +2084,365 @@ describe("disconnectOAuthProvider", () => {
1154
2084
  // Verify connection row was deleted
1155
2085
  expect(getConnection(conn.id)).toBeUndefined();
1156
2086
  });
2087
+
2088
+ test("calls upstream revoke when provider has revokeUrl and access token exists", async () => {
2089
+ seedProviderWithRevoke("google", "https://oauth2.googleapis.com/revoke", {
2090
+ token: "{access_token}",
2091
+ client_id: "{client_id}",
2092
+ });
2093
+ const app = await upsertApp("google", "client-1");
2094
+ const conn = createConnection({
2095
+ oauthAppId: app.id,
2096
+ provider: "google",
2097
+ grantedScopes: ["email"],
2098
+ hasRefreshToken: true,
2099
+ });
2100
+ secureKeyValues.set(
2101
+ `oauth_connection/${conn.id}/access_token`,
2102
+ "fake-token-xyz",
2103
+ );
2104
+
2105
+ mockFetch(
2106
+ "https://oauth2.googleapis.com/revoke",
2107
+ { method: "POST" },
2108
+ { status: 200, body: {} },
2109
+ );
2110
+
2111
+ const result = await disconnectOAuthProvider("google");
2112
+ expect(result).toBe("disconnected");
2113
+
2114
+ const calls = getMockFetchCalls();
2115
+ expect(calls.length).toBe(1);
2116
+ expect(calls[0]!.path).toContain("https://oauth2.googleapis.com/revoke");
2117
+
2118
+ const body = String(calls[0]!.init.body ?? "");
2119
+ const params = new URLSearchParams(body);
2120
+ expect(params.get("token")).toBe("fake-token-xyz");
2121
+ expect(params.get("client_id")).toBe("client-1");
2122
+ });
2123
+
2124
+ test("skips upstream revoke when provider has no revokeUrl", async () => {
2125
+ // GitHub seeded by createTestApp via seedTestProvider — no revokeUrl.
2126
+ const app = await createTestApp("github", "client-1");
2127
+ const conn = createConnection({
2128
+ oauthAppId: app.id,
2129
+ provider: "github",
2130
+ grantedScopes: ["repo"],
2131
+ hasRefreshToken: false,
2132
+ });
2133
+ secureKeyValues.set(
2134
+ `oauth_connection/${conn.id}/access_token`,
2135
+ "github-token",
2136
+ );
2137
+
2138
+ const result = await disconnectOAuthProvider("github");
2139
+ expect(result).toBe("disconnected");
2140
+ expect(getMockFetchCalls().length).toBe(0);
2141
+ });
2142
+
2143
+ test("skips upstream revoke when no access token exists in secure storage", async () => {
2144
+ seedProviderWithRevoke("google", "https://oauth2.googleapis.com/revoke", {
2145
+ token: "{access_token}",
2146
+ });
2147
+ const app = await upsertApp("google", "client-1");
2148
+ createConnection({
2149
+ oauthAppId: app.id,
2150
+ provider: "google",
2151
+ grantedScopes: ["email"],
2152
+ hasRefreshToken: false,
2153
+ });
2154
+ // No access token seeded into secureKeyValues.
2155
+
2156
+ const result = await disconnectOAuthProvider("google");
2157
+ expect(result).toBe("disconnected");
2158
+ expect(getMockFetchCalls().length).toBe(0);
2159
+ });
2160
+
2161
+ test("continues local cleanup when upstream revoke returns non-2xx", async () => {
2162
+ seedProviderWithRevoke("google", "https://oauth2.googleapis.com/revoke", {
2163
+ token: "{access_token}",
2164
+ });
2165
+ const app = await upsertApp("google", "client-1");
2166
+ const conn = createConnection({
2167
+ oauthAppId: app.id,
2168
+ provider: "google",
2169
+ grantedScopes: ["email"],
2170
+ hasRefreshToken: true,
2171
+ });
2172
+ secureKeyValues.set(
2173
+ `oauth_connection/${conn.id}/access_token`,
2174
+ "fake-token-xyz",
2175
+ );
2176
+
2177
+ mockFetch(
2178
+ "https://oauth2.googleapis.com/revoke",
2179
+ { method: "POST" },
2180
+ { status: 400, body: { error: "invalid_token" } },
2181
+ );
2182
+
2183
+ const result = await disconnectOAuthProvider("google");
2184
+ expect(result).toBe("disconnected");
2185
+ expect(getMockFetchCalls().length).toBe(1);
2186
+ // Local cleanup still happened
2187
+ expect(mockDeleteSecureKeyAsync).toHaveBeenCalledWith(
2188
+ `oauth_connection/${conn.id}/access_token`,
2189
+ );
2190
+ expect(getConnection(conn.id)).toBeUndefined();
2191
+ });
2192
+
2193
+ test("continues local cleanup when upstream revoke throws (network error)", async () => {
2194
+ seedProviderWithRevoke("google", "https://oauth2.googleapis.com/revoke", {
2195
+ token: "{access_token}",
2196
+ });
2197
+ const app = await upsertApp("google", "client-1");
2198
+ const conn = createConnection({
2199
+ oauthAppId: app.id,
2200
+ provider: "google",
2201
+ grantedScopes: ["email"],
2202
+ hasRefreshToken: true,
2203
+ });
2204
+ secureKeyValues.set(
2205
+ `oauth_connection/${conn.id}/access_token`,
2206
+ "fake-token-xyz",
2207
+ );
2208
+
2209
+ // 500 exercises the same swallow path as a network error.
2210
+ mockFetch(
2211
+ "https://oauth2.googleapis.com/revoke",
2212
+ { method: "POST" },
2213
+ { status: 500, body: { error: "server_error" } },
2214
+ );
2215
+
2216
+ const result = await disconnectOAuthProvider("google");
2217
+ expect(result).toBe("disconnected");
2218
+ expect(getConnection(conn.id)).toBeUndefined();
2219
+ });
2220
+
2221
+ test("substitutes {access_token} and {client_id} in body template values", async () => {
2222
+ seedProviderWithRevoke("google", "https://oauth2.googleapis.com/revoke", {
2223
+ token: "{access_token}",
2224
+ client_id: "{client_id}",
2225
+ token_type_hint: "access_token",
2226
+ });
2227
+ const app = await upsertApp("google", "client-substitution");
2228
+ const conn = createConnection({
2229
+ oauthAppId: app.id,
2230
+ provider: "google",
2231
+ grantedScopes: ["email"],
2232
+ hasRefreshToken: false,
2233
+ });
2234
+ secureKeyValues.set(
2235
+ `oauth_connection/${conn.id}/access_token`,
2236
+ "tok-substitution",
2237
+ );
2238
+
2239
+ mockFetch(
2240
+ "https://oauth2.googleapis.com/revoke",
2241
+ { method: "POST" },
2242
+ { status: 200, body: {} },
2243
+ );
2244
+
2245
+ await disconnectOAuthProvider("google");
2246
+
2247
+ const calls = getMockFetchCalls();
2248
+ expect(calls.length).toBe(1);
2249
+ const body = String(calls[0]!.init.body ?? "");
2250
+ const params = new URLSearchParams(body);
2251
+ expect(params.get("token")).toBe("tok-substitution");
2252
+ expect(params.get("client_id")).toBe("client-substitution");
2253
+ expect(params.get("token_type_hint")).toBe("access_token");
2254
+ });
2255
+
2256
+ test("treats $-prefixed patterns in access token as literal text (String.replace gotcha)", async () => {
2257
+ // String.prototype.replace interprets $-prefixed patterns in the
2258
+ // replacement string as special sequences ($& = matched substring,
2259
+ // $' = after match, $` = before match, $$ = literal $). If the access
2260
+ // token contains "$&", a naive `.replace("{access_token}", accessToken)`
2261
+ // would expand it to "{access_token}" (the matched string) instead of
2262
+ // substituting literally. This test guards against that by asserting
2263
+ // the captured body contains the literal "tok$&abc" — which only holds
2264
+ // when we use a function-replacement callback that preserves literal
2265
+ // semantics and mirrors Python's str.replace() behavior.
2266
+ seedProviderWithRevoke("google", "https://revoke.example.com/r", {
2267
+ token: "{access_token}",
2268
+ });
2269
+ const app = await upsertApp("google", "client-1");
2270
+ const conn = createConnection({
2271
+ oauthAppId: app.id,
2272
+ provider: "google",
2273
+ grantedScopes: ["email"],
2274
+ hasRefreshToken: false,
2275
+ });
2276
+ secureKeyValues.set(`oauth_connection/${conn.id}/access_token`, "tok$&abc");
2277
+
2278
+ mockFetch(
2279
+ "https://revoke.example.com/r",
2280
+ { method: "POST" },
2281
+ { status: 200, body: {} },
2282
+ );
2283
+
2284
+ await disconnectOAuthProvider("google");
2285
+
2286
+ const calls = getMockFetchCalls();
2287
+ expect(calls.length).toBe(1);
2288
+ const body = String(calls[0]!.init.body ?? "");
2289
+ const params = new URLSearchParams(body);
2290
+ // The literal access token — not the $&-expanded version, which would
2291
+ // be "tok{access_token}abc" (where $& matched "{access_token}").
2292
+ expect(params.get("token")).toBe("tok$&abc");
2293
+ });
2294
+
2295
+ test("replaces all occurrences of {access_token} in body template values (matching Python str.replace)", async () => {
2296
+ // Python's str.replace(old, new) replaces ALL occurrences by default,
2297
+ // whereas JavaScript's String.prototype.replace with a string pattern
2298
+ // only replaces the FIRST occurrence. The platform's try_revoke_token
2299
+ // is implemented in Python, so any template value containing repeated
2300
+ // placeholders must have ALL of them substituted to preserve parity.
2301
+ // This test guards against a regression to .replace(), which would
2302
+ // leave the second {access_token} as a literal placeholder.
2303
+ seedProviderWithRevoke("google", "https://revoke.example.com/r", {
2304
+ token: "token={access_token}&also={access_token}",
2305
+ });
2306
+ const app = await upsertApp("google", "client-1");
2307
+ const conn = createConnection({
2308
+ oauthAppId: app.id,
2309
+ provider: "google",
2310
+ grantedScopes: ["email"],
2311
+ hasRefreshToken: false,
2312
+ });
2313
+ secureKeyValues.set(`oauth_connection/${conn.id}/access_token`, "fake-abc");
2314
+
2315
+ mockFetch(
2316
+ "https://revoke.example.com/r",
2317
+ { method: "POST" },
2318
+ { status: 200, body: {} },
2319
+ );
2320
+
2321
+ await disconnectOAuthProvider("google");
2322
+
2323
+ const calls = getMockFetchCalls();
2324
+ expect(calls.length).toBe(1);
2325
+ const body = String(calls[0]!.init.body ?? "");
2326
+ const params = new URLSearchParams(body);
2327
+ // Both {access_token} placeholders must be substituted. With the buggy
2328
+ // .replace() (single-occurrence), this would be
2329
+ // "token=fake-abc&also={access_token}" instead.
2330
+ expect(params.get("token")).toBe("token=fake-abc&also=fake-abc");
2331
+ });
2332
+
2333
+ test("coerces non-string body template values to strings", async () => {
2334
+ // seedProviderWithRevoke restricts to Record<string, string>; bypass it
2335
+ // here by inserting a template with a numeric value via a direct seed.
2336
+ seedProviders([
2337
+ {
2338
+ provider: "google",
2339
+ authorizeUrl: "https://google.example.com/authorize",
2340
+ tokenExchangeUrl: "https://google.example.com/token",
2341
+ defaultScopes: ["email"],
2342
+ scopePolicy: {},
2343
+ revokeUrl: "https://oauth2.googleapis.com/revoke",
2344
+ revokeBodyTemplate: {
2345
+ token: "{access_token}",
2346
+ // expires_in is a number — must be coerced via String(value).
2347
+ expires_in: 3600,
2348
+ } as unknown as Record<string, string>,
2349
+ },
2350
+ ]);
2351
+ const app = await upsertApp("google", "client-1");
2352
+ const conn = createConnection({
2353
+ oauthAppId: app.id,
2354
+ provider: "google",
2355
+ grantedScopes: ["email"],
2356
+ hasRefreshToken: false,
2357
+ });
2358
+ secureKeyValues.set(
2359
+ `oauth_connection/${conn.id}/access_token`,
2360
+ "fake-token-xyz",
2361
+ );
2362
+
2363
+ mockFetch(
2364
+ "https://oauth2.googleapis.com/revoke",
2365
+ { method: "POST" },
2366
+ { status: 200, body: {} },
2367
+ );
2368
+
2369
+ await disconnectOAuthProvider("google");
2370
+
2371
+ const calls = getMockFetchCalls();
2372
+ expect(calls.length).toBe(1);
2373
+ const body = String(calls[0]!.init.body ?? "");
2374
+ const params = new URLSearchParams(body);
2375
+ expect(params.get("token")).toBe("fake-token-xyz");
2376
+ expect(params.get("expires_in")).toBe("3600");
2377
+ });
2378
+
2379
+ test("revokes BEFORE deleting tokens from secure storage", async () => {
2380
+ seedProviderWithRevoke("google", "https://oauth2.googleapis.com/revoke", {
2381
+ token: "{access_token}",
2382
+ });
2383
+ const app = await upsertApp("google", "client-1");
2384
+ const conn = createConnection({
2385
+ oauthAppId: app.id,
2386
+ provider: "google",
2387
+ grantedScopes: ["email"],
2388
+ hasRefreshToken: true,
2389
+ });
2390
+ secureKeyValues.set(
2391
+ `oauth_connection/${conn.id}/access_token`,
2392
+ "fake-token-xyz",
2393
+ );
2394
+
2395
+ const order: string[] = [];
2396
+
2397
+ // Wrap the mockFetch entry's response handler by registering a fetch
2398
+ // mock that records the call order via the existing mockFetch helper.
2399
+ // The mock fetch records to getMockFetchCalls; we tag ordering by
2400
+ // pushing a marker as soon as the response is constructed below.
2401
+ mockFetch(
2402
+ "https://oauth2.googleapis.com/revoke",
2403
+ { method: "POST" },
2404
+ new Response(JSON.stringify({}), {
2405
+ status: 200,
2406
+ headers: { "Content-Type": "application/json" },
2407
+ }),
2408
+ );
2409
+
2410
+ // Wrap delete to record its order. We replace the mock implementation
2411
+ // for the duration of this test only.
2412
+ mockDeleteSecureKeyAsync.mockImplementation(() => {
2413
+ order.push("delete");
2414
+ return Promise.resolve("deleted" as const);
2415
+ });
2416
+
2417
+ // Wrap fetch one more layer: tap into the actual fetch call to record
2418
+ // ordering. We do this by overriding globalThis.fetch with a wrapper
2419
+ // that calls through to the existing mock and records ordering first.
2420
+ const wrappedFetch = globalThis.fetch;
2421
+ globalThis.fetch = (async (
2422
+ input: RequestInfo | URL,
2423
+ init?: RequestInit,
2424
+ ) => {
2425
+ order.push("fetch");
2426
+ return wrappedFetch(input, init);
2427
+ }) as typeof globalThis.fetch;
2428
+
2429
+ try {
2430
+ const result = await disconnectOAuthProvider("google");
2431
+ expect(result).toBe("disconnected");
2432
+ } finally {
2433
+ // Restore the wrapper layer; resetMockFetch in beforeEach will reset
2434
+ // the underlying mock for the next test.
2435
+ globalThis.fetch = wrappedFetch;
2436
+ mockDeleteSecureKeyAsync.mockImplementation(
2437
+ (): Promise<"deleted" | "not-found" | "error"> =>
2438
+ Promise.resolve("deleted" as const),
2439
+ );
2440
+ }
2441
+
2442
+ expect(order[0]).toBe("fetch");
2443
+ expect(order).toContain("delete");
2444
+ expect(order.indexOf("fetch")).toBeLessThan(order.indexOf("delete"));
2445
+ });
1157
2446
  });
1158
2447
 
1159
2448
  // ---------------------------------------------------------------------------
@@ -1172,7 +2461,7 @@ describe("FK constraints", () => {
1172
2461
  expect(() =>
1173
2462
  createConnection({
1174
2463
  oauthAppId: "nonexistent-app-id",
1175
- providerKey: "github",
2464
+ provider: "github",
1176
2465
  grantedScopes: ["repo"],
1177
2466
  hasRefreshToken: false,
1178
2467
  }),