@vellumai/assistant 0.4.48 → 0.4.50

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 (423) hide show
  1. package/ARCHITECTURE.md +26 -35
  2. package/README.md +5 -26
  3. package/docs/architecture/integrations.md +45 -41
  4. package/docs/architecture/keychain-broker.md +3 -3
  5. package/docs/architecture/memory.md +180 -119
  6. package/docs/runbook-trusted-contacts.md +3 -8
  7. package/hook-templates/debug-prompt-logger/hook.json +1 -1
  8. package/hook-templates/debug-prompt-logger/run.sh +1 -3
  9. package/package.json +2 -2
  10. package/src/__tests__/actor-token-service.test.ts +0 -1
  11. package/src/__tests__/agent-loop.test.ts +3 -1
  12. package/src/__tests__/anthropic-provider.test.ts +249 -2
  13. package/src/__tests__/approval-cascade.test.ts +796 -0
  14. package/src/__tests__/approval-primitive.test.ts +0 -1
  15. package/src/__tests__/approval-routes-http.test.ts +4 -0
  16. package/src/__tests__/assistant-attachments.test.ts +12 -34
  17. package/src/__tests__/assistant-feature-flag-guard.test.ts +0 -23
  18. package/src/__tests__/assistant-feature-flag-guardrails.test.ts +76 -0
  19. package/src/__tests__/assistant-feature-flags-integration.test.ts +0 -1
  20. package/src/__tests__/browser-skill-baseline-tool-payload.test.ts +2 -2
  21. package/src/__tests__/canonical-guardian-store.test.ts +95 -0
  22. package/src/__tests__/channel-guardian.test.ts +0 -2
  23. package/src/__tests__/channel-readiness-routes.test.ts +15 -6
  24. package/src/__tests__/channel-readiness-service.test.ts +10 -9
  25. package/src/__tests__/checker.test.ts +13 -20
  26. package/src/__tests__/computer-use-skill-manifest-regression.test.ts +1 -1
  27. package/src/__tests__/computer-use-tools.test.ts +2 -19
  28. package/src/__tests__/config-schema.test.ts +1 -68
  29. package/src/__tests__/config-watcher.test.ts +0 -1
  30. package/src/__tests__/confirmation-request-guardian-bridge.test.ts +0 -1
  31. package/src/__tests__/context-image-dimensions.test.ts +332 -0
  32. package/src/__tests__/context-memory-e2e.test.ts +11 -100
  33. package/src/__tests__/context-token-estimator.test.ts +196 -13
  34. package/src/__tests__/conversation-attention-store.test.ts +0 -1
  35. package/src/__tests__/conversation-attention-telegram.test.ts +0 -1
  36. package/src/__tests__/conversation-routes-guardian-reply.test.ts +152 -0
  37. package/src/__tests__/conversation-routes-slash-commands.test.ts +2 -0
  38. package/src/__tests__/credential-metadata-store.test.ts +64 -73
  39. package/src/__tests__/credential-security-e2e.test.ts +1 -0
  40. package/src/__tests__/credential-security-invariants.test.ts +13 -7
  41. package/src/__tests__/credential-vault-unit.test.ts +284 -49
  42. package/src/__tests__/credential-vault.test.ts +150 -16
  43. package/src/__tests__/credentials-cli.test.ts +71 -0
  44. package/src/__tests__/cu-unified-flow.test.ts +532 -0
  45. package/src/__tests__/date-context.test.ts +93 -77
  46. package/src/__tests__/deterministic-verification-control-plane.test.ts +64 -0
  47. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +0 -1
  48. package/src/__tests__/ephemeral-permissions.test.ts +3 -3
  49. package/src/__tests__/gateway-only-guard.test.ts +0 -1
  50. package/src/__tests__/guardian-action-grant-mint-consume.test.ts +0 -1
  51. package/src/__tests__/guardian-decision-primitive-canonical.test.ts +0 -1
  52. package/src/__tests__/guardian-routing-invariants.test.ts +93 -1
  53. package/src/__tests__/guardian-verification-voice-binding.test.ts +0 -1
  54. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +0 -39
  55. package/src/__tests__/heartbeat-service.test.ts +0 -1
  56. package/src/__tests__/history-repair.test.ts +245 -0
  57. package/src/__tests__/host-cu-proxy.test.ts +791 -0
  58. package/src/__tests__/host-shell-tool.test.ts +27 -15
  59. package/src/__tests__/http-user-message-parity.test.ts +2 -0
  60. package/src/__tests__/ingress-url-consistency.test.ts +14 -21
  61. package/src/__tests__/integration-status.test.ts +32 -51
  62. package/src/__tests__/intent-routing.test.ts +0 -1
  63. package/src/__tests__/invite-redemption-service.test.ts +65 -1
  64. package/src/__tests__/invite-routes-http.test.ts +10 -9
  65. package/src/__tests__/keychain-broker-client.test.ts +14 -46
  66. package/src/__tests__/memory-context-benchmark.benchmark.test.ts +56 -18
  67. package/src/__tests__/memory-lifecycle-e2e.test.ts +244 -387
  68. package/src/__tests__/memory-recall-quality.test.ts +244 -407
  69. package/src/__tests__/memory-regressions.experimental.test.ts +126 -101
  70. package/src/__tests__/memory-regressions.test.ts +477 -2841
  71. package/src/__tests__/memory-retrieval.benchmark.test.ts +33 -150
  72. package/src/__tests__/memory-upsert-concurrency.test.ts +5 -244
  73. package/src/__tests__/mime-builder.test.ts +28 -0
  74. package/src/__tests__/native-web-search.test.ts +1 -0
  75. package/src/__tests__/notification-routing-intent.test.ts +0 -1
  76. package/src/__tests__/oauth-cli.test.ts +941 -15
  77. package/src/__tests__/oauth-provider-profiles.test.ts +9 -9
  78. package/src/__tests__/oauth-scope-policy.test.ts +4 -6
  79. package/src/__tests__/oauth-store.test.ts +870 -0
  80. package/src/__tests__/onboarding-starter-tasks.test.ts +0 -1
  81. package/src/__tests__/provider-error-scenarios.test.ts +0 -1
  82. package/src/__tests__/provider-streaming.benchmark.test.ts +0 -1
  83. package/src/__tests__/public-ingress-urls.test.ts +15 -21
  84. package/src/__tests__/qdrant-collection-migration.test.ts +53 -8
  85. package/src/__tests__/recording-handler.test.ts +3 -4
  86. package/src/__tests__/registry.test.ts +2 -3
  87. package/src/__tests__/relay-server.test.ts +46 -1
  88. package/src/__tests__/runtime-events-sse.test.ts +55 -7
  89. package/src/__tests__/schedule-store.test.ts +0 -1
  90. package/src/__tests__/schedule-tools.test.ts +32 -0
  91. package/src/__tests__/scheduler-recurrence.test.ts +0 -1
  92. package/src/__tests__/scoped-approval-grants.test.ts +0 -1
  93. package/src/__tests__/scoped-grant-security-matrix.test.ts +0 -1
  94. package/src/__tests__/script-proxy-certs.test.ts +1 -1
  95. package/src/__tests__/secret-ingress-handler.test.ts +0 -1
  96. package/src/__tests__/secret-onetime-send.test.ts +1 -0
  97. package/src/__tests__/secure-keys.test.ts +7 -2
  98. package/src/__tests__/send-endpoint-busy.test.ts +24 -6
  99. package/src/__tests__/sequence-store.test.ts +0 -1
  100. package/src/__tests__/session-abort-tool-results.test.ts +1 -14
  101. package/src/__tests__/session-agent-loop-overflow.test.ts +1583 -0
  102. package/src/__tests__/session-agent-loop.test.ts +19 -15
  103. package/src/__tests__/session-confirmation-signals.test.ts +1 -15
  104. package/src/__tests__/session-error.test.ts +124 -2
  105. package/src/__tests__/session-history-web-search.test.ts +918 -0
  106. package/src/__tests__/session-init.benchmark.test.ts +4 -5
  107. package/src/__tests__/session-pre-run-repair.test.ts +1 -14
  108. package/src/__tests__/session-provider-retry-repair.test.ts +25 -28
  109. package/src/__tests__/session-queue.test.ts +37 -27
  110. package/src/__tests__/session-runtime-assembly.test.ts +54 -0
  111. package/src/__tests__/session-slash-known.test.ts +1 -15
  112. package/src/__tests__/session-slash-queue.test.ts +1 -15
  113. package/src/__tests__/session-slash-unknown.test.ts +1 -15
  114. package/src/__tests__/session-workspace-cache-state.test.ts +3 -33
  115. package/src/__tests__/session-workspace-injection.test.ts +3 -37
  116. package/src/__tests__/session-workspace-tool-tracking.test.ts +3 -37
  117. package/src/__tests__/skill-include-graph.test.ts +66 -0
  118. package/src/__tests__/skill-load-feature-flag.test.ts +0 -1
  119. package/src/__tests__/skill-load-tool.test.ts +149 -1
  120. package/src/__tests__/skill-projection-feature-flag.test.ts +0 -1
  121. package/src/__tests__/skills-install-extract.test.ts +93 -0
  122. package/src/__tests__/skills-uninstall.test.ts +1 -1
  123. package/src/__tests__/skills.test.ts +3 -3
  124. package/src/__tests__/skillssh-registry.test.ts +451 -0
  125. package/src/__tests__/slack-channel-config.test.ts +67 -3
  126. package/src/__tests__/slack-share-routes.test.ts +17 -19
  127. package/src/__tests__/system-prompt.test.ts +0 -1
  128. package/src/__tests__/telegram-invite-adapter.test.ts +18 -22
  129. package/src/__tests__/terminal-tools.test.ts +4 -3
  130. package/src/__tests__/test-support/computer-use-skill-harness.ts +3 -2
  131. package/src/__tests__/tool-approval-handler.test.ts +0 -1
  132. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +0 -1
  133. package/src/__tests__/tool-executor-lifecycle-events.test.ts +0 -1
  134. package/src/__tests__/tool-executor-shell-integration.test.ts +0 -1
  135. package/src/__tests__/tool-executor.test.ts +0 -1
  136. package/src/__tests__/tool-grant-request-escalation.test.ts +0 -1
  137. package/src/__tests__/trust-store-pattern-matches.test.ts +29 -0
  138. package/src/__tests__/trust-store.test.ts +7 -13
  139. package/src/__tests__/trusted-contact-approval-notifier.test.ts +0 -1
  140. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +0 -1
  141. package/src/__tests__/twilio-routes.test.ts +0 -16
  142. package/src/__tests__/verification-control-plane-policy.test.ts +0 -1
  143. package/src/__tests__/voice-invite-redemption.test.ts +32 -1
  144. package/src/__tests__/voice-scoped-grant-consumer.test.ts +0 -1
  145. package/src/agent/ax-tree-compaction.test.ts +286 -0
  146. package/src/agent/loop.ts +104 -131
  147. package/src/approvals/AGENTS.md +1 -1
  148. package/src/approvals/guardian-request-resolvers.ts +14 -2
  149. package/src/bundler/compiler-tools.ts +66 -2
  150. package/src/calls/call-domain.ts +133 -6
  151. package/src/calls/call-store.ts +6 -0
  152. package/src/calls/relay-server.ts +52 -18
  153. package/src/calls/relay-setup-router.ts +17 -1
  154. package/src/calls/twilio-config.ts +3 -8
  155. package/src/calls/twilio-routes.ts +1 -2
  156. package/src/calls/types.ts +3 -1
  157. package/src/calls/voice-ingress-preflight.ts +1 -1
  158. package/src/cli/commands/browser-relay.ts +18 -12
  159. package/src/cli/commands/completions.ts +0 -3
  160. package/src/cli/commands/credentials.ts +101 -15
  161. package/src/cli/commands/doctor.ts +4 -3
  162. package/src/cli/commands/mcp.ts +46 -59
  163. package/src/cli/commands/memory.ts +16 -165
  164. package/src/cli/commands/oauth/apps.ts +284 -0
  165. package/src/cli/commands/oauth/connections.ts +633 -0
  166. package/src/cli/commands/oauth/index.ts +52 -0
  167. package/src/cli/commands/oauth/providers.ts +256 -0
  168. package/src/cli/commands/sessions.ts +5 -2
  169. package/src/cli/commands/skills.ts +177 -339
  170. package/src/cli/http-client.ts +0 -20
  171. package/src/cli/main-screen.tsx +2 -2
  172. package/src/cli/program.ts +6 -11
  173. package/src/cli/reference.ts +1 -3
  174. package/src/cli.ts +4 -10
  175. package/src/config/assistant-feature-flags.ts +0 -3
  176. package/src/config/bundled-skills/_shared/CLI_RETRIEVAL_PATTERN.md +1 -1
  177. package/src/config/bundled-skills/computer-use/SKILL.md +3 -6
  178. package/src/config/bundled-skills/computer-use/TOOLS.json +23 -5
  179. package/src/config/bundled-skills/computer-use/tools/{computer-use-request-control.ts → computer-use-observe.ts} +1 -5
  180. package/src/config/bundled-skills/google-calendar/calendar-client.ts +21 -16
  181. package/src/config/bundled-skills/messaging/tools/shared.ts +1 -4
  182. package/src/config/bundled-skills/settings/SKILL.md +1 -1
  183. package/src/config/bundled-skills/settings/TOOLS.json +2 -8
  184. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +5 -33
  185. package/src/config/bundled-tool-registry.ts +2 -5
  186. package/src/config/env-registry.ts +14 -83
  187. package/src/config/env.ts +11 -50
  188. package/src/config/feature-flag-registry.json +16 -16
  189. package/src/config/loader.ts +0 -6
  190. package/src/config/schema.ts +4 -13
  191. package/src/config/schemas/memory-lifecycle.ts +0 -9
  192. package/src/config/schemas/memory-processing.ts +0 -180
  193. package/src/config/schemas/memory-retrieval.ts +32 -104
  194. package/src/config/schemas/memory.ts +0 -10
  195. package/src/config/skills.ts +21 -2
  196. package/src/config/types.ts +0 -4
  197. package/src/context/image-dimensions.ts +229 -0
  198. package/src/context/token-estimator.ts +75 -12
  199. package/src/context/window-manager.ts +53 -11
  200. package/src/daemon/assistant-attachments.ts +1 -13
  201. package/src/daemon/config-watcher.ts +61 -3
  202. package/src/daemon/daemon-control.ts +1 -1
  203. package/src/daemon/date-context.ts +114 -31
  204. package/src/daemon/handlers/config-ingress.ts +8 -33
  205. package/src/daemon/handlers/config-slack-channel.ts +49 -46
  206. package/src/daemon/handlers/config-telegram.ts +32 -16
  207. package/src/daemon/handlers/sessions.ts +27 -36
  208. package/src/daemon/handlers/shared.ts +0 -130
  209. package/src/daemon/handlers/skills.ts +20 -1
  210. package/src/daemon/history-repair.ts +72 -8
  211. package/src/daemon/host-cu-proxy.ts +430 -0
  212. package/src/daemon/lifecycle.ts +67 -71
  213. package/src/daemon/mcp-reload-service.ts +2 -2
  214. package/src/daemon/message-protocol.ts +3 -0
  215. package/src/daemon/message-types/computer-use.ts +1 -129
  216. package/src/daemon/message-types/host-cu.ts +19 -0
  217. package/src/daemon/message-types/memory.ts +4 -16
  218. package/src/daemon/message-types/messages.ts +4 -0
  219. package/src/daemon/message-types/sessions.ts +4 -0
  220. package/src/daemon/server.ts +25 -21
  221. package/src/daemon/session-agent-loop-handlers.ts +40 -0
  222. package/src/daemon/session-agent-loop.ts +334 -48
  223. package/src/daemon/session-attachments.ts +1 -2
  224. package/src/daemon/session-error.ts +89 -6
  225. package/src/daemon/session-history.ts +17 -7
  226. package/src/daemon/session-media-retry.ts +6 -2
  227. package/src/daemon/session-memory.ts +69 -149
  228. package/src/daemon/session-process.ts +10 -1
  229. package/src/daemon/session-runtime-assembly.ts +49 -19
  230. package/src/daemon/session-slash.ts +1 -1
  231. package/src/daemon/session-surfaces.ts +43 -28
  232. package/src/daemon/session-tool-setup.ts +9 -10
  233. package/src/daemon/session.ts +150 -17
  234. package/src/daemon/tool-side-effects.ts +2 -8
  235. package/src/daemon/watch-handler.ts +2 -2
  236. package/src/events/tool-metrics-listener.ts +2 -2
  237. package/src/hooks/manager.ts +1 -4
  238. package/src/inbound/public-ingress-urls.ts +7 -7
  239. package/src/instrument.ts +61 -1
  240. package/src/logfire.ts +16 -5
  241. package/src/memory/admin.ts +2 -191
  242. package/src/memory/canonical-guardian-store.ts +38 -2
  243. package/src/memory/conversation-crud.ts +0 -33
  244. package/src/memory/conversation-key-store.ts +21 -0
  245. package/src/memory/conversation-queries.ts +22 -3
  246. package/src/memory/db-init.ts +32 -0
  247. package/src/memory/embedding-backend.ts +84 -8
  248. package/src/memory/embedding-types.ts +9 -1
  249. package/src/memory/indexer.ts +7 -46
  250. package/src/memory/items-extractor.ts +274 -76
  251. package/src/memory/job-handlers/backfill.ts +2 -127
  252. package/src/memory/job-handlers/cleanup.ts +2 -16
  253. package/src/memory/job-handlers/extraction.ts +2 -138
  254. package/src/memory/job-handlers/index-maintenance.ts +1 -6
  255. package/src/memory/job-handlers/summarization.ts +3 -148
  256. package/src/memory/job-utils.ts +21 -59
  257. package/src/memory/jobs-store.ts +1 -159
  258. package/src/memory/jobs-worker.ts +9 -52
  259. package/src/memory/migrations/104-core-indexes.ts +3 -3
  260. package/src/memory/migrations/149-oauth-tables.ts +62 -0
  261. package/src/memory/migrations/150-oauth-apps-client-secret-path.ts +98 -0
  262. package/src/memory/migrations/151-oauth-providers-ping-url.ts +11 -0
  263. package/src/memory/migrations/152-memory-item-supersession.ts +44 -0
  264. package/src/memory/migrations/153-drop-entity-tables.ts +15 -0
  265. package/src/memory/migrations/154-drop-fts.ts +20 -0
  266. package/src/memory/migrations/155-drop-conflicts.ts +7 -0
  267. package/src/memory/migrations/156-call-session-invite-metadata.ts +24 -0
  268. package/src/memory/migrations/index.ts +8 -0
  269. package/src/memory/qdrant-client.ts +148 -51
  270. package/src/memory/raw-query.ts +1 -1
  271. package/src/memory/retriever.test.ts +294 -273
  272. package/src/memory/retriever.ts +421 -645
  273. package/src/memory/schema/calls.ts +2 -0
  274. package/src/memory/schema/index.ts +1 -0
  275. package/src/memory/schema/memory-core.ts +3 -48
  276. package/src/memory/schema/oauth.ts +67 -0
  277. package/src/memory/search/formatting.ts +263 -176
  278. package/src/memory/search/lexical.ts +1 -254
  279. package/src/memory/search/ranking.ts +0 -455
  280. package/src/memory/search/semantic.ts +100 -14
  281. package/src/memory/search/staleness.ts +47 -0
  282. package/src/memory/search/tier-classifier.ts +21 -0
  283. package/src/memory/search/types.ts +15 -77
  284. package/src/memory/task-memory-cleanup.ts +4 -6
  285. package/src/messaging/provider.ts +4 -4
  286. package/src/messaging/providers/gmail/client.ts +82 -2
  287. package/src/messaging/providers/gmail/mime-builder.ts +17 -7
  288. package/src/messaging/providers/gmail/people-client.ts +10 -10
  289. package/src/messaging/providers/telegram-bot/adapter.ts +17 -17
  290. package/src/messaging/providers/whatsapp/adapter.ts +11 -8
  291. package/src/messaging/registry.ts +2 -32
  292. package/src/notifications/copy-composer.ts +0 -5
  293. package/src/notifications/signal.ts +4 -5
  294. package/src/oauth/byo-connection.test.ts +133 -25
  295. package/src/oauth/byo-connection.ts +22 -6
  296. package/src/oauth/connect-orchestrator.ts +113 -57
  297. package/src/oauth/connect-types.ts +17 -23
  298. package/src/oauth/connection-resolver.ts +35 -11
  299. package/src/oauth/connection.ts +1 -1
  300. package/src/oauth/manual-token-connection.ts +104 -0
  301. package/src/oauth/oauth-store.ts +582 -0
  302. package/src/oauth/platform-connection.test.ts +29 -0
  303. package/src/oauth/platform-connection.ts +6 -5
  304. package/src/oauth/provider-behaviors.ts +124 -0
  305. package/src/oauth/scope-policy.ts +9 -2
  306. package/src/oauth/seed-providers.ts +167 -0
  307. package/src/oauth/token-persistence.ts +81 -77
  308. package/src/permissions/checker.ts +3 -3
  309. package/src/permissions/defaults.ts +1 -1
  310. package/src/permissions/prompter.ts +10 -1
  311. package/src/permissions/trust-store.ts +36 -1
  312. package/src/playbooks/playbook-compiler.ts +1 -1
  313. package/src/prompts/__tests__/build-cli-reference-section.test.ts +3 -1
  314. package/src/prompts/system-prompt.ts +46 -42
  315. package/src/providers/anthropic/client.ts +59 -20
  316. package/src/providers/retry.ts +1 -27
  317. package/src/providers/types.ts +7 -1
  318. package/src/runtime/AGENTS.md +9 -0
  319. package/src/runtime/auth/route-policy.ts +6 -6
  320. package/src/runtime/channel-reply-delivery.ts +0 -40
  321. package/src/runtime/gateway-client.ts +0 -7
  322. package/src/runtime/guardian-reply-router.ts +24 -22
  323. package/src/runtime/http-server.ts +10 -8
  324. package/src/runtime/http-types.ts +2 -2
  325. package/src/runtime/invite-redemption-service.ts +19 -1
  326. package/src/runtime/invite-service.ts +25 -0
  327. package/src/runtime/middleware/twilio-validation.ts +1 -11
  328. package/src/runtime/pending-interactions.ts +14 -12
  329. package/src/runtime/routes/brain-graph-routes.ts +10 -90
  330. package/src/runtime/routes/channel-delivery-routes.ts +0 -1
  331. package/src/runtime/routes/conversation-routes.ts +81 -19
  332. package/src/runtime/routes/events-routes.ts +21 -11
  333. package/src/runtime/routes/host-cu-routes.ts +97 -0
  334. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +21 -12
  335. package/src/runtime/routes/inbound-stages/background-dispatch.ts +12 -111
  336. package/src/runtime/routes/integrations/slack/share.ts +6 -7
  337. package/src/runtime/routes/log-export-routes.ts +126 -8
  338. package/src/runtime/routes/memory-item-routes.test.ts +754 -0
  339. package/src/runtime/routes/memory-item-routes.ts +503 -0
  340. package/src/runtime/routes/session-management-routes.ts +3 -3
  341. package/src/runtime/routes/settings-routes.ts +55 -48
  342. package/src/runtime/routes/surface-action-routes.ts +1 -1
  343. package/src/runtime/routes/trust-rules-routes.ts +14 -0
  344. package/src/runtime/routes/watch-routes.ts +128 -0
  345. package/src/runtime/routes/workspace-routes.ts +2 -1
  346. package/src/schedule/integration-status.ts +10 -9
  347. package/src/security/credential-key.ts +0 -156
  348. package/src/security/keychain-broker-client.ts +22 -10
  349. package/src/security/oauth2.ts +1 -1
  350. package/src/security/secure-keys.ts +25 -3
  351. package/src/security/token-manager.ts +137 -64
  352. package/src/skills/catalog-install.ts +414 -0
  353. package/src/skills/include-graph.ts +32 -0
  354. package/src/skills/skillssh-registry.ts +503 -0
  355. package/src/telegram/bot-username.ts +2 -3
  356. package/src/tools/assets/search.ts +5 -1
  357. package/src/tools/browser/network-recorder.ts +1 -1
  358. package/src/tools/browser/network-recording-types.ts +1 -1
  359. package/src/tools/computer-use/definitions.ts +36 -11
  360. package/src/tools/computer-use/registry.ts +5 -6
  361. package/src/tools/credentials/broker.ts +1 -2
  362. package/src/tools/credentials/metadata-store.ts +17 -121
  363. package/src/tools/credentials/vault.ts +92 -167
  364. package/src/tools/memory/definitions.ts +4 -13
  365. package/src/tools/memory/handlers.test.ts +83 -103
  366. package/src/tools/memory/handlers.ts +50 -85
  367. package/src/tools/registry.ts +2 -7
  368. package/src/tools/schedule/create.ts +8 -1
  369. package/src/tools/schedule/update.ts +8 -1
  370. package/src/tools/skills/load.ts +85 -3
  371. package/src/tools/watch/watch-state.ts +0 -12
  372. package/src/util/logger.ts +7 -41
  373. package/src/util/platform.ts +9 -28
  374. package/src/watcher/providers/google-calendar.ts +2 -1
  375. package/src/__tests__/clarification-resolver.test.ts +0 -193
  376. package/src/__tests__/computer-use-session-compaction.test.ts +0 -143
  377. package/src/__tests__/computer-use-session-lifecycle.test.ts +0 -322
  378. package/src/__tests__/computer-use-session-working-dir.test.ts +0 -166
  379. package/src/__tests__/computer-use-skill-baseline.test.ts +0 -78
  380. package/src/__tests__/computer-use-skill-endstate.test.ts +0 -105
  381. package/src/__tests__/computer-use-skill-lifecycle-cleanup.test.ts +0 -249
  382. package/src/__tests__/conflict-intent-tokenization.test.ts +0 -160
  383. package/src/__tests__/conflict-policy.test.ts +0 -269
  384. package/src/__tests__/conflict-store.test.ts +0 -372
  385. package/src/__tests__/contradiction-checker.test.ts +0 -361
  386. package/src/__tests__/entity-extractor.test.ts +0 -211
  387. package/src/__tests__/entity-search.test.ts +0 -1117
  388. package/src/__tests__/profile-compiler.test.ts +0 -392
  389. package/src/__tests__/ride-shotgun-handler.test.ts +0 -452
  390. package/src/__tests__/session-conflict-gate.test.ts +0 -1228
  391. package/src/__tests__/session-profile-injection.test.ts +0 -557
  392. package/src/cli/commands/dev.ts +0 -129
  393. package/src/cli/commands/map.ts +0 -391
  394. package/src/cli/commands/oauth.ts +0 -77
  395. package/src/config/bundled-skills/knowledge-graph/SKILL.md +0 -25
  396. package/src/config/bundled-skills/knowledge-graph/TOOLS.json +0 -66
  397. package/src/config/bundled-skills/knowledge-graph/tools/graph-query.ts +0 -211
  398. package/src/daemon/computer-use-session.ts +0 -1026
  399. package/src/daemon/ride-shotgun-handler.ts +0 -569
  400. package/src/daemon/session-conflict-gate.ts +0 -167
  401. package/src/daemon/session-dynamic-profile.ts +0 -77
  402. package/src/memory/clarification-resolver.ts +0 -417
  403. package/src/memory/conflict-intent.ts +0 -205
  404. package/src/memory/conflict-policy.ts +0 -127
  405. package/src/memory/conflict-store.ts +0 -410
  406. package/src/memory/contradiction-checker.ts +0 -508
  407. package/src/memory/entity-extractor.ts +0 -535
  408. package/src/memory/format-recall.ts +0 -47
  409. package/src/memory/fts-reconciler.ts +0 -165
  410. package/src/memory/job-handlers/conflict.ts +0 -200
  411. package/src/memory/profile-compiler.ts +0 -195
  412. package/src/memory/recall-cache.ts +0 -117
  413. package/src/memory/search/entity.ts +0 -535
  414. package/src/memory/search/query-expansion.test.ts +0 -70
  415. package/src/memory/search/query-expansion.ts +0 -118
  416. package/src/oauth/provider-base-urls.ts +0 -21
  417. package/src/oauth/provider-profiles.ts +0 -192
  418. package/src/prompts/computer-use-prompt.ts +0 -98
  419. package/src/runtime/routes/computer-use-routes.ts +0 -641
  420. package/src/runtime/routes/mcp-routes.ts +0 -20
  421. package/src/runtime/telegram-streaming-delivery.test.ts +0 -729
  422. package/src/runtime/telegram-streaming-delivery.ts +0 -393
  423. package/src/tools/computer-use/request-computer-control.ts +0 -56
@@ -0,0 +1,870 @@
1
+ import { mkdtempSync, rmSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { afterAll, beforeEach, describe, expect, mock, test } from "bun:test";
5
+
6
+ const testDir = mkdtempSync(join(tmpdir(), "oauth-store-test-"));
7
+
8
+ mock.module("../util/platform.js", () => ({
9
+ getDataDir: () => testDir,
10
+ isMacOS: () => process.platform === "darwin",
11
+ isLinux: () => process.platform === "linux",
12
+ isWindows: () => process.platform === "win32",
13
+ getPidPath: () => join(testDir, "test.pid"),
14
+ getDbPath: () => ":memory:",
15
+ getLogPath: () => join(testDir, "test.log"),
16
+ ensureDataDir: () => {},
17
+ }));
18
+
19
+ mock.module("../util/logger.js", () => ({
20
+ getLogger: () =>
21
+ new Proxy({} as Record<string, unknown>, {
22
+ get: () => () => {},
23
+ }),
24
+ }));
25
+
26
+ const mockDeleteSecureKeyAsync = mock(
27
+ (): Promise<"deleted" | "not-found" | "error"> =>
28
+ Promise.resolve("deleted" as const),
29
+ );
30
+ const mockSetSecureKeyAsync = mock(() => Promise.resolve(true));
31
+ /** Simulated secure key store for getSecureKey lookups. */
32
+ const secureKeyValues = new Map<string, string>();
33
+ mock.module("../security/secure-keys.js", () => ({
34
+ deleteSecureKeyAsync: mockDeleteSecureKeyAsync,
35
+ setSecureKeyAsync: mockSetSecureKeyAsync,
36
+ getSecureKey: (account: string) => secureKeyValues.get(account),
37
+ getSecureKeyAsync: (account: string) =>
38
+ Promise.resolve(secureKeyValues.get(account)),
39
+ }));
40
+
41
+ import { initializeDb, resetDb, resetTestTables } from "../memory/db.js";
42
+ import {
43
+ createConnection,
44
+ deleteApp,
45
+ deleteConnection,
46
+ disconnectOAuthProvider,
47
+ getApp,
48
+ getAppByProviderAndClientId,
49
+ getConnection,
50
+ getConnectionByProvider,
51
+ getProvider,
52
+ isProviderConnected,
53
+ listConnections,
54
+ registerProvider,
55
+ seedProviders,
56
+ updateConnection,
57
+ upsertApp,
58
+ } from "../oauth/oauth-store.js";
59
+
60
+ initializeDb();
61
+
62
+ /** Seed a minimal provider row for FK satisfaction. */
63
+ function seedTestProvider(providerKey = "github"): void {
64
+ seedProviders([
65
+ {
66
+ providerKey,
67
+ authUrl: `https://${providerKey}.example.com/authorize`,
68
+ tokenUrl: `https://${providerKey}.example.com/token`,
69
+ defaultScopes: ["read"],
70
+ scopePolicy: {},
71
+ },
72
+ ]);
73
+ }
74
+
75
+ /** Create an app linked to the given provider. Returns the app row. */
76
+ async function createTestApp(providerKey = "github", clientId = "client-1") {
77
+ seedTestProvider(providerKey);
78
+ return await upsertApp(providerKey, clientId);
79
+ }
80
+
81
+ beforeEach(() => {
82
+ resetDb();
83
+ initializeDb();
84
+ // Explicitly clear all OAuth tables to prevent cross-test state pollution.
85
+ // Delete in FK-dependency order: connections → apps → providers.
86
+ resetTestTables("oauth_connections", "oauth_apps", "oauth_providers");
87
+ mockDeleteSecureKeyAsync.mockClear();
88
+ mockSetSecureKeyAsync.mockClear();
89
+ secureKeyValues.clear();
90
+ });
91
+
92
+ afterAll(() => {
93
+ resetDb();
94
+ try {
95
+ rmSync(testDir, { recursive: true });
96
+ } catch {
97
+ // best-effort cleanup
98
+ }
99
+ });
100
+
101
+ // ---------------------------------------------------------------------------
102
+ // Provider operations
103
+ // ---------------------------------------------------------------------------
104
+
105
+ describe("provider operations", () => {
106
+ describe("seedProviders", () => {
107
+ test("creates rows for new providers", () => {
108
+ seedProviders([
109
+ {
110
+ providerKey: "github",
111
+ authUrl: "https://github.com/login/oauth/authorize",
112
+ tokenUrl: "https://github.com/login/oauth/access_token",
113
+ defaultScopes: ["repo", "user"],
114
+ scopePolicy: { required: ["repo"] },
115
+ },
116
+ {
117
+ providerKey: "google",
118
+ authUrl: "https://accounts.google.com/o/oauth2/v2/auth",
119
+ tokenUrl: "https://oauth2.googleapis.com/token",
120
+ defaultScopes: ["openid", "email"],
121
+ scopePolicy: {},
122
+ extraParams: { access_type: "offline" },
123
+ },
124
+ ]);
125
+
126
+ const gh = getProvider("github");
127
+ expect(gh).toBeDefined();
128
+ expect(gh!.providerKey).toBe("github");
129
+ expect(gh!.authUrl).toBe("https://github.com/login/oauth/authorize");
130
+ expect(gh!.tokenUrl).toBe("https://github.com/login/oauth/access_token");
131
+ expect(JSON.parse(gh!.defaultScopes)).toEqual(["repo", "user"]);
132
+ expect(JSON.parse(gh!.scopePolicy)).toEqual({ required: ["repo"] });
133
+
134
+ const goog = getProvider("google");
135
+ expect(goog).toBeDefined();
136
+ expect(goog!.providerKey).toBe("google");
137
+ expect(JSON.parse(goog!.extraParams!)).toEqual({
138
+ access_type: "offline",
139
+ });
140
+ });
141
+
142
+ test("updates existing provider rows with corrected seed data", () => {
143
+ seedProviders([
144
+ {
145
+ providerKey: "github",
146
+ authUrl: "https://github.com/login/oauth/authorize",
147
+ tokenUrl: "https://github.com/login/oauth/access_token",
148
+ defaultScopes: ["repo"],
149
+ scopePolicy: {},
150
+ baseUrl: "https://api.github.com",
151
+ },
152
+ ]);
153
+
154
+ const original = getProvider("github");
155
+ expect(original).toBeDefined();
156
+ expect(original!.baseUrl).toBe("https://api.github.com");
157
+ const originalCreatedAt = original!.createdAt;
158
+
159
+ // Re-seed with corrected values (simulates a code fix deployed on upgrade)
160
+ seedProviders([
161
+ {
162
+ providerKey: "github",
163
+ authUrl: "https://github.com/login/oauth/authorize-v2",
164
+ tokenUrl: "https://github.com/login/oauth/access_token-v2",
165
+ defaultScopes: ["repo", "user"],
166
+ scopePolicy: { required: ["repo"] },
167
+ baseUrl: "https://api.github.com/v2",
168
+ },
169
+ ]);
170
+
171
+ const row = getProvider("github");
172
+ expect(row).toBeDefined();
173
+ // Seed data should overwrite the existing row
174
+ expect(row!.authUrl).toBe("https://github.com/login/oauth/authorize-v2");
175
+ expect(row!.tokenUrl).toBe(
176
+ "https://github.com/login/oauth/access_token-v2",
177
+ );
178
+ expect(row!.baseUrl).toBe("https://api.github.com/v2");
179
+ expect(JSON.parse(row!.defaultScopes)).toEqual(["repo", "user"]);
180
+ expect(JSON.parse(row!.scopePolicy)).toEqual({ required: ["repo"] });
181
+ // createdAt should be preserved from the original insert
182
+ expect(row!.createdAt).toBe(originalCreatedAt);
183
+ });
184
+
185
+ test("persists pingUrl when provided", () => {
186
+ seedProviders([
187
+ {
188
+ providerKey: "github",
189
+ authUrl: "https://github.com/authorize",
190
+ tokenUrl: "https://github.com/token",
191
+ defaultScopes: ["repo"],
192
+ scopePolicy: {},
193
+ pingUrl: "https://api.github.com/user",
194
+ },
195
+ ]);
196
+ const row = getProvider("github");
197
+ expect(row!.pingUrl).toBe("https://api.github.com/user");
198
+ });
199
+
200
+ test("pingUrl defaults to null when omitted", () => {
201
+ seedProviders([
202
+ {
203
+ providerKey: "github",
204
+ authUrl: "https://github.com/authorize",
205
+ tokenUrl: "https://github.com/token",
206
+ defaultScopes: ["repo"],
207
+ scopePolicy: {},
208
+ },
209
+ ]);
210
+ const row = getProvider("github");
211
+ expect(row!.pingUrl).toBeNull();
212
+ });
213
+ });
214
+
215
+ describe("getProvider", () => {
216
+ test("returns the correct row", () => {
217
+ seedProviders([
218
+ {
219
+ providerKey: "github",
220
+ authUrl: "https://github.com/authorize",
221
+ tokenUrl: "https://github.com/token",
222
+ defaultScopes: ["repo"],
223
+ scopePolicy: {},
224
+ callbackTransport: "loopback",
225
+ loopbackPort: 8765,
226
+ },
227
+ ]);
228
+
229
+ const row = getProvider("github");
230
+ expect(row).toBeDefined();
231
+ expect(row!.providerKey).toBe("github");
232
+ expect(row!.callbackTransport).toBe("loopback");
233
+ expect(row!.loopbackPort).toBe(8765);
234
+ });
235
+
236
+ test("returns undefined for unknown keys", () => {
237
+ expect(getProvider("nonexistent")).toBeUndefined();
238
+ });
239
+ });
240
+
241
+ describe("registerProvider", () => {
242
+ test("creates a new row", () => {
243
+ const row = registerProvider({
244
+ providerKey: "linear",
245
+ authUrl: "https://linear.app/oauth/authorize",
246
+ tokenUrl: "https://api.linear.app/oauth/token",
247
+ defaultScopes: ["read"],
248
+ scopePolicy: {},
249
+ });
250
+
251
+ expect(row.providerKey).toBe("linear");
252
+ expect(row.authUrl).toBe("https://linear.app/oauth/authorize");
253
+
254
+ const fetched = getProvider("linear");
255
+ expect(fetched).toBeDefined();
256
+ expect(fetched!.providerKey).toBe("linear");
257
+ });
258
+
259
+ test("throws for duplicate provider_key", () => {
260
+ registerProvider({
261
+ providerKey: "linear",
262
+ authUrl: "https://linear.app/oauth/authorize",
263
+ tokenUrl: "https://api.linear.app/oauth/token",
264
+ defaultScopes: ["read"],
265
+ scopePolicy: {},
266
+ });
267
+
268
+ expect(() =>
269
+ registerProvider({
270
+ providerKey: "linear",
271
+ authUrl: "https://linear.app/oauth/authorize",
272
+ tokenUrl: "https://api.linear.app/oauth/token",
273
+ defaultScopes: ["read"],
274
+ scopePolicy: {},
275
+ }),
276
+ ).toThrow(/already exists.*linear/);
277
+ });
278
+ });
279
+ });
280
+
281
+ // ---------------------------------------------------------------------------
282
+ // App operations
283
+ // ---------------------------------------------------------------------------
284
+
285
+ describe("app operations", () => {
286
+ describe("upsertApp", () => {
287
+ test("creates a new app and returns it with a UUID", async () => {
288
+ seedTestProvider("github");
289
+ const app = await upsertApp("github", "client-abc");
290
+
291
+ expect(app.id).toBeTruthy();
292
+ // UUID v4 format check
293
+ expect(app.id).toMatch(
294
+ /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/,
295
+ );
296
+ expect(app.providerKey).toBe("github");
297
+ expect(app.clientId).toBe("client-abc");
298
+ expect(app.createdAt).toBeGreaterThan(0);
299
+ expect(app.updatedAt).toBeGreaterThan(0);
300
+ });
301
+
302
+ test("returns the existing app when called again with same (providerKey, clientId)", async () => {
303
+ seedTestProvider("github");
304
+ const first = await upsertApp("github", "client-abc");
305
+ const second = await upsertApp("github", "client-abc");
306
+
307
+ expect(second.id).toBe(first.id);
308
+ expect(second.createdAt).toBe(first.createdAt);
309
+ });
310
+
311
+ test("stores clientSecret in secure storage on new app creation", async () => {
312
+ seedTestProvider("github");
313
+ const app = await upsertApp("github", "client-abc", {
314
+ clientSecretValue: "my-secret",
315
+ });
316
+
317
+ expect(mockSetSecureKeyAsync).toHaveBeenCalledTimes(1);
318
+ expect(mockSetSecureKeyAsync).toHaveBeenCalledWith(
319
+ `oauth_app/${app.id}/client_secret`,
320
+ "my-secret",
321
+ );
322
+ expect(app.clientSecretCredentialPath).toBe(
323
+ `oauth_app/${app.id}/client_secret`,
324
+ );
325
+ });
326
+
327
+ test("stores clientSecret in secure storage when upserting an existing app", async () => {
328
+ seedTestProvider("github");
329
+ const first = await upsertApp("github", "client-abc");
330
+ mockSetSecureKeyAsync.mockClear();
331
+
332
+ await upsertApp("github", "client-abc", {
333
+ clientSecretValue: "updated-secret",
334
+ });
335
+
336
+ expect(mockSetSecureKeyAsync).toHaveBeenCalledTimes(1);
337
+ expect(mockSetSecureKeyAsync).toHaveBeenCalledWith(
338
+ first.clientSecretCredentialPath,
339
+ "updated-secret",
340
+ );
341
+ });
342
+
343
+ test("throws when setSecureKeyAsync returns false", async () => {
344
+ seedTestProvider("github");
345
+ mockSetSecureKeyAsync.mockResolvedValueOnce(false);
346
+
347
+ await expect(
348
+ upsertApp("github", "client-abc", { clientSecretValue: "bad-secret" }),
349
+ ).rejects.toThrow("Failed to store client_secret in secure storage");
350
+ });
351
+
352
+ test("accepts clientSecretCredentialPath and verifies existence", async () => {
353
+ seedTestProvider("github");
354
+ secureKeyValues.set("custom/path", "stored-secret");
355
+
356
+ const app = await upsertApp("github", "client-abc", {
357
+ clientSecretCredentialPath: "custom/path",
358
+ });
359
+
360
+ expect(app.clientSecretCredentialPath).toBe("custom/path");
361
+ // Should not have called setSecureKeyAsync since we only provided a path
362
+ expect(mockSetSecureKeyAsync).not.toHaveBeenCalled();
363
+ });
364
+
365
+ test("throws when clientSecretCredentialPath points to nonexistent secret", async () => {
366
+ seedTestProvider("github");
367
+
368
+ await expect(
369
+ upsertApp("github", "client-abc", {
370
+ clientSecretCredentialPath: "nonexistent/path",
371
+ }),
372
+ ).rejects.toThrow("No secret found at credential path: nonexistent/path");
373
+ });
374
+
375
+ test("throws when both clientSecretValue and clientSecretCredentialPath are provided", async () => {
376
+ seedTestProvider("github");
377
+
378
+ await expect(
379
+ upsertApp("github", "client-abc", {
380
+ clientSecretValue: "my-secret",
381
+ clientSecretCredentialPath: "custom/path",
382
+ }),
383
+ ).rejects.toThrow(
384
+ "Cannot provide both clientSecretValue and clientSecretCredentialPath",
385
+ );
386
+ });
387
+
388
+ test("records default clientSecretCredentialPath when neither value nor path is provided", async () => {
389
+ seedTestProvider("github");
390
+ const app = await upsertApp("github", "client-abc");
391
+
392
+ expect(app.clientSecretCredentialPath).toBe(
393
+ `oauth_app/${app.id}/client_secret`,
394
+ );
395
+ });
396
+
397
+ test("updates clientSecretCredentialPath on existing row when path is provided", async () => {
398
+ seedTestProvider("github");
399
+ const first = await upsertApp("github", "client-abc");
400
+ expect(first.clientSecretCredentialPath).toBe(
401
+ `oauth_app/${first.id}/client_secret`,
402
+ );
403
+
404
+ secureKeyValues.set("new/custom/path", "stored-secret");
405
+ const updated = await upsertApp("github", "client-abc", {
406
+ clientSecretCredentialPath: "new/custom/path",
407
+ });
408
+
409
+ expect(updated.id).toBe(first.id);
410
+ expect(updated.clientSecretCredentialPath).toBe("new/custom/path");
411
+ });
412
+ });
413
+
414
+ describe("getApp", () => {
415
+ test("returns the correct row by id", async () => {
416
+ const app = await createTestApp("github", "client-1");
417
+ const fetched = getApp(app.id);
418
+
419
+ expect(fetched).toBeDefined();
420
+ expect(fetched!.id).toBe(app.id);
421
+ expect(fetched!.providerKey).toBe("github");
422
+ expect(fetched!.clientId).toBe("client-1");
423
+ });
424
+
425
+ test("returns undefined for unknown id", () => {
426
+ expect(getApp("nonexistent-id")).toBeUndefined();
427
+ });
428
+ });
429
+
430
+ describe("getAppByProviderAndClientId", () => {
431
+ test("returns the correct row", async () => {
432
+ const app = await createTestApp("github", "client-1");
433
+ const fetched = getAppByProviderAndClientId("github", "client-1");
434
+
435
+ expect(fetched).toBeDefined();
436
+ expect(fetched!.id).toBe(app.id);
437
+ });
438
+
439
+ test("returns undefined for unknown combination", () => {
440
+ expect(
441
+ getAppByProviderAndClientId("github", "nonexistent"),
442
+ ).toBeUndefined();
443
+ });
444
+ });
445
+
446
+ describe("deleteApp", () => {
447
+ test("removes the row and returns true", async () => {
448
+ const app = await createTestApp("github", "client-1");
449
+ const deleted = await deleteApp(app.id);
450
+
451
+ expect(deleted).toBe(true);
452
+ expect(getApp(app.id)).toBeUndefined();
453
+ });
454
+
455
+ test("cleans up client_secret from secure storage using stored path", async () => {
456
+ const app = await createTestApp("github", "client-1");
457
+ mockDeleteSecureKeyAsync.mockClear();
458
+
459
+ await deleteApp(app.id);
460
+
461
+ expect(mockDeleteSecureKeyAsync).toHaveBeenCalledWith(
462
+ app.clientSecretCredentialPath,
463
+ );
464
+ });
465
+
466
+ test("uses custom clientSecretCredentialPath when deleting", async () => {
467
+ seedTestProvider("github");
468
+ secureKeyValues.set("custom/secret/path", "the-secret");
469
+ const app = await upsertApp("github", "client-1", {
470
+ clientSecretCredentialPath: "custom/secret/path",
471
+ });
472
+ mockDeleteSecureKeyAsync.mockClear();
473
+
474
+ await deleteApp(app.id);
475
+
476
+ expect(mockDeleteSecureKeyAsync).toHaveBeenCalledWith(
477
+ "custom/secret/path",
478
+ );
479
+ });
480
+
481
+ test("returns false for nonexistent id", async () => {
482
+ expect(await deleteApp("nonexistent-id")).toBe(false);
483
+ });
484
+
485
+ test("throws when deleteSecureKeyAsync returns error", async () => {
486
+ const app = await createTestApp("github", "client-1");
487
+ mockDeleteSecureKeyAsync.mockResolvedValueOnce("error");
488
+
489
+ await expect(deleteApp(app.id)).rejects.toThrow(
490
+ /failed to remove client_secret from secure storage/i,
491
+ );
492
+
493
+ // DB row should already be deleted (delete happens before secure key cleanup)
494
+ expect(getApp(app.id)).toBeUndefined();
495
+ });
496
+ });
497
+ });
498
+
499
+ // ---------------------------------------------------------------------------
500
+ // Connection operations
501
+ // ---------------------------------------------------------------------------
502
+
503
+ describe("connection operations", () => {
504
+ describe("createConnection", () => {
505
+ test("creates a row with status='active'", async () => {
506
+ const app = await createTestApp("github", "client-1");
507
+ const conn = createConnection({
508
+ oauthAppId: app.id,
509
+ providerKey: "github",
510
+ grantedScopes: ["repo", "user"],
511
+ hasRefreshToken: true,
512
+ accountInfo: "user@example.com",
513
+ label: "Primary GitHub",
514
+ metadata: { login: "octocat" },
515
+ });
516
+
517
+ expect(conn.id).toBeTruthy();
518
+ expect(conn.oauthAppId).toBe(app.id);
519
+ expect(conn.providerKey).toBe("github");
520
+ expect(conn.status).toBe("active");
521
+ expect(JSON.parse(conn.grantedScopes)).toEqual(["repo", "user"]);
522
+ expect(conn.hasRefreshToken).toBe(1);
523
+ expect(conn.accountInfo).toBe("user@example.com");
524
+ expect(conn.label).toBe("Primary GitHub");
525
+ expect(JSON.parse(conn.metadata!)).toEqual({ login: "octocat" });
526
+ expect(conn.createdAt).toBeGreaterThan(0);
527
+ });
528
+ });
529
+
530
+ describe("getConnection", () => {
531
+ test("returns the correct row", async () => {
532
+ const app = await createTestApp("github", "client-1");
533
+ const conn = createConnection({
534
+ oauthAppId: app.id,
535
+ providerKey: "github",
536
+ grantedScopes: ["repo"],
537
+ hasRefreshToken: false,
538
+ });
539
+
540
+ const fetched = getConnection(conn.id);
541
+ expect(fetched).toBeDefined();
542
+ expect(fetched!.id).toBe(conn.id);
543
+ expect(fetched!.providerKey).toBe("github");
544
+ });
545
+
546
+ test("returns undefined for unknown id", () => {
547
+ expect(getConnection("nonexistent-id")).toBeUndefined();
548
+ });
549
+ });
550
+
551
+ describe("getConnectionByProvider", () => {
552
+ test("returns the most recent active connection", async () => {
553
+ const app = await createTestApp("github", "client-1");
554
+
555
+ // Create two connections with explicit timestamps so ordering is deterministic
556
+ createConnection({
557
+ oauthAppId: app.id,
558
+ providerKey: "github",
559
+ grantedScopes: ["repo"],
560
+ hasRefreshToken: false,
561
+ createdAt: 1000,
562
+ });
563
+
564
+ const conn2 = createConnection({
565
+ oauthAppId: app.id,
566
+ providerKey: "github",
567
+ grantedScopes: ["repo", "user"],
568
+ hasRefreshToken: true,
569
+ createdAt: 2000,
570
+ });
571
+
572
+ const result = getConnectionByProvider("github");
573
+ expect(result).toBeDefined();
574
+ expect(result!.id).toBe(conn2.id);
575
+ });
576
+
577
+ test("skips connections with status='revoked'", async () => {
578
+ const app = await createTestApp("github", "client-1");
579
+
580
+ const conn1 = createConnection({
581
+ oauthAppId: app.id,
582
+ providerKey: "github",
583
+ grantedScopes: ["repo"],
584
+ hasRefreshToken: false,
585
+ });
586
+
587
+ const conn2 = createConnection({
588
+ oauthAppId: app.id,
589
+ providerKey: "github",
590
+ grantedScopes: ["repo", "user"],
591
+ hasRefreshToken: true,
592
+ });
593
+
594
+ // Revoke the most recent connection
595
+ updateConnection(conn2.id, { status: "revoked" });
596
+
597
+ const result = getConnectionByProvider("github");
598
+ expect(result).toBeDefined();
599
+ expect(result!.id).toBe(conn1.id);
600
+ });
601
+
602
+ test("skips connections with status='expired'", async () => {
603
+ const app = await createTestApp("github", "client-1");
604
+
605
+ const conn = createConnection({
606
+ oauthAppId: app.id,
607
+ providerKey: "github",
608
+ grantedScopes: ["repo"],
609
+ hasRefreshToken: false,
610
+ });
611
+
612
+ updateConnection(conn.id, { status: "expired" });
613
+
614
+ const result = getConnectionByProvider("github");
615
+ expect(result).toBeUndefined();
616
+ });
617
+
618
+ test("returns undefined when no active connections exist", () => {
619
+ expect(getConnectionByProvider("github")).toBeUndefined();
620
+ });
621
+ });
622
+
623
+ describe("isProviderConnected", () => {
624
+ test("returns true when active connection has an access token in secure storage", async () => {
625
+ const app = await createTestApp("github", "client-1");
626
+ const conn = createConnection({
627
+ oauthAppId: app.id,
628
+ providerKey: "github",
629
+ grantedScopes: ["repo"],
630
+ hasRefreshToken: false,
631
+ });
632
+
633
+ secureKeyValues.set(`oauth_connection/${conn.id}/access_token`, "tok");
634
+
635
+ expect(isProviderConnected("github")).toBe(true);
636
+ });
637
+
638
+ test("returns false when active connection exists but access token is missing", async () => {
639
+ const app = await createTestApp("github", "client-1");
640
+ createConnection({
641
+ oauthAppId: app.id,
642
+ providerKey: "github",
643
+ grantedScopes: ["repo"],
644
+ hasRefreshToken: false,
645
+ });
646
+
647
+ // No secure key set — simulates failed token write
648
+ expect(isProviderConnected("github")).toBe(false);
649
+ });
650
+
651
+ test("returns false when no connection exists", () => {
652
+ expect(isProviderConnected("github")).toBe(false);
653
+ });
654
+
655
+ test("returns false when connection is revoked even with token in store", async () => {
656
+ const app = await createTestApp("github", "client-1");
657
+ const conn = createConnection({
658
+ oauthAppId: app.id,
659
+ providerKey: "github",
660
+ grantedScopes: ["repo"],
661
+ hasRefreshToken: false,
662
+ });
663
+
664
+ updateConnection(conn.id, { status: "revoked" });
665
+ secureKeyValues.set(`oauth_connection/${conn.id}/access_token`, "tok");
666
+
667
+ expect(isProviderConnected("github")).toBe(false);
668
+ });
669
+ });
670
+
671
+ describe("updateConnection", () => {
672
+ test("modifies specific fields", async () => {
673
+ const app = await createTestApp("github", "client-1");
674
+ const conn = createConnection({
675
+ oauthAppId: app.id,
676
+ providerKey: "github",
677
+ grantedScopes: ["repo"],
678
+ hasRefreshToken: false,
679
+ });
680
+
681
+ const updated = updateConnection(conn.id, {
682
+ status: "revoked",
683
+ label: "Revoked account",
684
+ grantedScopes: ["repo", "user", "gist"],
685
+ hasRefreshToken: true,
686
+ metadata: { reason: "user-requested" },
687
+ });
688
+
689
+ expect(updated).toBe(true);
690
+
691
+ const fetched = getConnection(conn.id);
692
+ expect(fetched).toBeDefined();
693
+ expect(fetched!.status).toBe("revoked");
694
+ expect(fetched!.label).toBe("Revoked account");
695
+ expect(JSON.parse(fetched!.grantedScopes)).toEqual([
696
+ "repo",
697
+ "user",
698
+ "gist",
699
+ ]);
700
+ expect(fetched!.hasRefreshToken).toBe(1);
701
+ expect(JSON.parse(fetched!.metadata!)).toEqual({
702
+ reason: "user-requested",
703
+ });
704
+ expect(fetched!.updatedAt).toBeGreaterThanOrEqual(conn.createdAt);
705
+ });
706
+
707
+ test("updates oauthAppId to a different app", async () => {
708
+ const app1 = await createTestApp("github", "client-1");
709
+ const app2 = await upsertApp("github", "client-2");
710
+
711
+ const conn = createConnection({
712
+ oauthAppId: app1.id,
713
+ providerKey: "github",
714
+ grantedScopes: ["repo"],
715
+ hasRefreshToken: false,
716
+ });
717
+
718
+ expect(getConnection(conn.id)!.oauthAppId).toBe(app1.id);
719
+
720
+ const updated = updateConnection(conn.id, { oauthAppId: app2.id });
721
+ expect(updated).toBe(true);
722
+
723
+ const fetched = getConnection(conn.id);
724
+ expect(fetched).toBeDefined();
725
+ expect(fetched!.oauthAppId).toBe(app2.id);
726
+ });
727
+
728
+ test("returns false for nonexistent id", () => {
729
+ expect(updateConnection("nonexistent-id", { status: "revoked" })).toBe(
730
+ false,
731
+ );
732
+ });
733
+ });
734
+
735
+ describe("listConnections", () => {
736
+ test("returns all connections when no filter is given", async () => {
737
+ const ghApp = await createTestApp("github", "client-1");
738
+ seedTestProvider("google");
739
+ const googApp = await upsertApp("google", "client-2");
740
+
741
+ createConnection({
742
+ oauthAppId: ghApp.id,
743
+ providerKey: "github",
744
+ grantedScopes: ["repo"],
745
+ hasRefreshToken: false,
746
+ });
747
+ createConnection({
748
+ oauthAppId: googApp.id,
749
+ providerKey: "google",
750
+ grantedScopes: ["email"],
751
+ hasRefreshToken: true,
752
+ });
753
+
754
+ const all = listConnections();
755
+ expect(all).toHaveLength(2);
756
+ });
757
+
758
+ test("filters by provider key", async () => {
759
+ const ghApp = await createTestApp("github", "client-1");
760
+ seedTestProvider("google");
761
+ const googApp = await upsertApp("google", "client-2");
762
+
763
+ createConnection({
764
+ oauthAppId: ghApp.id,
765
+ providerKey: "github",
766
+ grantedScopes: ["repo"],
767
+ hasRefreshToken: false,
768
+ });
769
+ createConnection({
770
+ oauthAppId: googApp.id,
771
+ providerKey: "google",
772
+ grantedScopes: ["email"],
773
+ hasRefreshToken: true,
774
+ });
775
+
776
+ const ghConns = listConnections("github");
777
+ expect(ghConns).toHaveLength(1);
778
+ expect(ghConns[0].providerKey).toBe("github");
779
+
780
+ const googConns = listConnections("google");
781
+ expect(googConns).toHaveLength(1);
782
+ expect(googConns[0].providerKey).toBe("google");
783
+ });
784
+
785
+ test("returns empty array when no connections exist", () => {
786
+ expect(listConnections()).toEqual([]);
787
+ });
788
+ });
789
+
790
+ describe("deleteConnection", () => {
791
+ test("removes the row and returns true", async () => {
792
+ const app = await createTestApp("github", "client-1");
793
+ const conn = createConnection({
794
+ oauthAppId: app.id,
795
+ providerKey: "github",
796
+ grantedScopes: ["repo"],
797
+ hasRefreshToken: false,
798
+ });
799
+
800
+ const deleted = deleteConnection(conn.id);
801
+ expect(deleted).toBe(true);
802
+ expect(getConnection(conn.id)).toBeUndefined();
803
+ });
804
+
805
+ test("returns false for nonexistent id", () => {
806
+ expect(deleteConnection("nonexistent-id")).toBe(false);
807
+ });
808
+ });
809
+ });
810
+
811
+ // ---------------------------------------------------------------------------
812
+ // disconnectOAuthProvider
813
+ // ---------------------------------------------------------------------------
814
+
815
+ describe("disconnectOAuthProvider", () => {
816
+ test("returns 'not-found' when no connection exists for the provider", async () => {
817
+ const result = await disconnectOAuthProvider("github");
818
+ expect(result).toBe("not-found");
819
+ expect(mockDeleteSecureKeyAsync).not.toHaveBeenCalled();
820
+ });
821
+
822
+ test("returns 'disconnected' and deletes connection row and secure keys when connection exists", async () => {
823
+ const app = await createTestApp("github", "client-1");
824
+ const conn = createConnection({
825
+ oauthAppId: app.id,
826
+ providerKey: "github",
827
+ grantedScopes: ["repo"],
828
+ hasRefreshToken: true,
829
+ });
830
+
831
+ const result = await disconnectOAuthProvider("github");
832
+ expect(result).toBe("disconnected");
833
+
834
+ // Verify secure keys were deleted
835
+ expect(mockDeleteSecureKeyAsync).toHaveBeenCalledTimes(2);
836
+ expect(mockDeleteSecureKeyAsync).toHaveBeenCalledWith(
837
+ `oauth_connection/${conn.id}/access_token`,
838
+ );
839
+ expect(mockDeleteSecureKeyAsync).toHaveBeenCalledWith(
840
+ `oauth_connection/${conn.id}/refresh_token`,
841
+ );
842
+
843
+ // Verify connection row was deleted
844
+ expect(getConnection(conn.id)).toBeUndefined();
845
+ });
846
+ });
847
+
848
+ // ---------------------------------------------------------------------------
849
+ // FK constraint enforcement
850
+ // ---------------------------------------------------------------------------
851
+
852
+ describe("FK constraints", () => {
853
+ test("creating an app with a nonexistent provider_key fails", async () => {
854
+ await expect(
855
+ upsertApp("nonexistent-provider", "client-1"),
856
+ ).rejects.toThrow();
857
+ });
858
+
859
+ test("creating a connection with a nonexistent oauth_app_id fails", () => {
860
+ seedTestProvider("github");
861
+ expect(() =>
862
+ createConnection({
863
+ oauthAppId: "nonexistent-app-id",
864
+ providerKey: "github",
865
+ grantedScopes: ["repo"],
866
+ hasRefreshToken: false,
867
+ }),
868
+ ).toThrow();
869
+ });
870
+ });