@vellumai/assistant 0.4.46 → 0.4.49
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +7 -7
- package/README.md +2 -23
- package/docs/architecture/integrations.md +45 -41
- package/docs/architecture/keychain-broker.md +3 -3
- package/docs/architecture/security.md +5 -5
- package/docs/runbook-trusted-contacts.md +3 -8
- package/hook-templates/debug-prompt-logger/hook.json +1 -1
- package/hook-templates/debug-prompt-logger/run.sh +1 -3
- package/package.json +1 -1
- package/src/__tests__/actor-token-service.test.ts +0 -1
- package/src/__tests__/anthropic-provider.test.ts +156 -0
- package/src/__tests__/approval-cascade.test.ts +810 -0
- package/src/__tests__/approval-primitive.test.ts +0 -1
- package/src/__tests__/approval-routes-http.test.ts +2 -0
- package/src/__tests__/assistant-attachments.test.ts +12 -34
- package/src/__tests__/assistant-feature-flag-guardrails.test.ts +76 -0
- package/src/__tests__/assistant-feature-flags-integration.test.ts +0 -1
- package/src/__tests__/browser-fill-credential.test.ts +5 -2
- package/src/__tests__/browser-skill-baseline-tool-payload.test.ts +2 -2
- package/src/__tests__/bundled-skill-retrieval-guard.test.ts +2 -1
- package/src/__tests__/channel-guardian.test.ts +0 -2
- package/src/__tests__/channel-readiness-routes.test.ts +35 -25
- package/src/__tests__/channel-readiness-service.test.ts +10 -9
- package/src/__tests__/checker.test.ts +9 -29
- package/src/__tests__/cli.test.ts +23 -0
- package/src/__tests__/computer-use-skill-manifest-regression.test.ts +1 -1
- package/src/__tests__/computer-use-tools.test.ts +2 -19
- package/src/__tests__/config-watcher.test.ts +0 -1
- package/src/__tests__/confirmation-request-guardian-bridge.test.ts +0 -1
- package/src/__tests__/context-image-dimensions.test.ts +332 -0
- package/src/__tests__/context-token-estimator.test.ts +196 -13
- package/src/__tests__/conversation-attention-store.test.ts +0 -1
- package/src/__tests__/conversation-attention-telegram.test.ts +0 -1
- package/src/__tests__/conversation-routes-guardian-reply.test.ts +144 -0
- package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -0
- package/src/__tests__/credential-broker-browser-fill.test.ts +23 -22
- package/src/__tests__/credential-broker-server-use.test.ts +22 -21
- package/src/__tests__/credential-broker.test.ts +2 -1
- package/src/__tests__/credential-metadata-store.test.ts +239 -26
- package/src/__tests__/credential-resolve.test.ts +5 -4
- package/src/__tests__/credential-security-e2e.test.ts +8 -8
- package/src/__tests__/credential-security-invariants.test.ts +111 -7
- package/src/__tests__/credential-vault-unit.test.ts +287 -54
- package/src/__tests__/credential-vault.test.ts +406 -12
- package/src/__tests__/credentials-cli.test.ts +82 -6
- package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +0 -1
- package/src/__tests__/ephemeral-permissions.test.ts +3 -3
- package/src/__tests__/gateway-only-enforcement.test.ts +4 -2
- package/src/__tests__/gateway-only-guard.test.ts +0 -1
- package/src/__tests__/gemini-image-service.test.ts +75 -45
- package/src/__tests__/gemini-provider.test.ts +9 -6
- package/src/__tests__/guardian-action-conversation-turn.test.ts +1 -33
- package/src/__tests__/guardian-action-copy-generator.test.ts +0 -20
- package/src/__tests__/guardian-action-followup-executor.test.ts +1 -28
- package/src/__tests__/guardian-action-followup-store.test.ts +1 -1
- package/src/__tests__/guardian-action-grant-mint-consume.test.ts +0 -1
- package/src/__tests__/guardian-decision-primitive-canonical.test.ts +0 -1
- package/src/__tests__/guardian-grant-minting.test.ts +35 -0
- package/src/__tests__/guardian-routing-invariants.test.ts +0 -1
- package/src/__tests__/guardian-verification-voice-binding.test.ts +0 -1
- package/src/__tests__/handlers-user-message-approval-consumption.test.ts +0 -39
- package/src/__tests__/heartbeat-service.test.ts +0 -1
- package/src/__tests__/host-cu-proxy.test.ts +629 -0
- package/src/__tests__/host-shell-tool.test.ts +27 -15
- package/src/__tests__/http-user-message-parity.test.ts +1 -0
- package/src/__tests__/ingress-url-consistency.test.ts +14 -21
- package/src/__tests__/integration-status.test.ts +38 -25
- package/src/__tests__/intent-routing.test.ts +0 -1
- package/src/__tests__/invite-routes-http.test.ts +10 -9
- package/src/__tests__/keychain-broker-client.test.ts +11 -43
- package/src/__tests__/managed-proxy-context.test.ts +5 -3
- package/src/__tests__/media-generate-image.test.ts +63 -2
- package/src/__tests__/media-reuse-story.e2e.test.ts +7 -3
- package/src/__tests__/messaging-send-tool.test.ts +4 -6
- package/src/__tests__/notification-routing-intent.test.ts +0 -1
- package/src/__tests__/oauth-cli.test.ts +373 -14
- package/src/__tests__/oauth-provider-profiles.test.ts +9 -9
- package/src/__tests__/oauth-scope-policy.test.ts +4 -6
- package/src/__tests__/oauth-store.test.ts +756 -0
- package/src/__tests__/onboarding-starter-tasks.test.ts +0 -1
- package/src/__tests__/provider-error-scenarios.test.ts +0 -1
- package/src/__tests__/provider-fail-open-selection.test.ts +3 -1
- package/src/__tests__/provider-managed-proxy-integration.test.ts +70 -6
- package/src/__tests__/provider-streaming.benchmark.test.ts +0 -1
- package/src/__tests__/public-ingress-urls.test.ts +15 -21
- package/src/__tests__/recording-handler.test.ts +3 -4
- package/src/__tests__/registry.test.ts +2 -2
- package/src/__tests__/runtime-events-sse.test.ts +55 -7
- package/src/__tests__/schedule-store.test.ts +0 -1
- package/src/__tests__/scheduler-recurrence.test.ts +0 -1
- package/src/__tests__/schema-transforms.test.ts +226 -0
- package/src/__tests__/scoped-approval-grants.test.ts +0 -1
- package/src/__tests__/scoped-grant-security-matrix.test.ts +0 -1
- package/src/__tests__/script-proxy-injection-runtime.test.ts +23 -13
- package/src/__tests__/script-proxy-policy-runtime.test.ts +1 -1
- package/src/__tests__/script-proxy-session-manager.test.ts +1 -1
- package/src/__tests__/secret-ingress-handler.test.ts +0 -1
- package/src/__tests__/secret-onetime-send.test.ts +5 -3
- package/src/__tests__/send-endpoint-busy.test.ts +21 -6
- package/src/__tests__/sequence-store.test.ts +0 -1
- package/src/__tests__/session-init.benchmark.test.ts +4 -5
- package/src/__tests__/session-messaging-secret-redirect.test.ts +5 -4
- package/src/__tests__/skill-include-graph.test.ts +66 -0
- package/src/__tests__/skill-load-feature-flag.test.ts +0 -1
- package/src/__tests__/skill-load-tool.test.ts +149 -1
- package/src/__tests__/skill-projection-feature-flag.test.ts +0 -1
- package/src/__tests__/skills-uninstall.test.ts +3 -3
- package/src/__tests__/skills.test.ts +3 -12
- package/src/__tests__/slack-channel-config.test.ts +76 -11
- package/src/__tests__/slack-share-routes.test.ts +17 -14
- package/src/__tests__/system-prompt.test.ts +0 -1
- package/src/__tests__/telegram-bot-username-resolution.test.ts +3 -0
- package/src/__tests__/telegram-invite-adapter.test.ts +18 -22
- package/src/__tests__/terminal-tools.test.ts +4 -3
- package/src/__tests__/test-support/computer-use-skill-harness.ts +3 -2
- package/src/__tests__/tool-approval-handler.test.ts +0 -1
- package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +0 -1
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +0 -1
- package/src/__tests__/tool-executor-shell-integration.test.ts +0 -1
- package/src/__tests__/tool-executor.test.ts +0 -1
- package/src/__tests__/tool-grant-request-escalation.test.ts +0 -1
- package/src/__tests__/trust-store-pattern-matches.test.ts +29 -0
- package/src/__tests__/trust-store.test.ts +1 -22
- package/src/__tests__/trusted-contact-approval-notifier.test.ts +0 -1
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +0 -1
- package/src/__tests__/twilio-config.test.ts +2 -1
- package/src/__tests__/twilio-provider.test.ts +4 -2
- package/src/__tests__/twilio-routes.test.ts +5 -20
- package/src/__tests__/verification-control-plane-policy.test.ts +0 -1
- package/src/__tests__/voice-scoped-grant-consumer.test.ts +0 -1
- package/src/agent/ax-tree-compaction.test.ts +235 -0
- package/src/agent/loop.ts +76 -130
- package/src/calls/call-domain.ts +8 -10
- package/src/calls/relay-server.ts +9 -13
- package/src/calls/twilio-config.ts +4 -8
- package/src/calls/twilio-provider.ts +2 -1
- package/src/calls/twilio-rest.ts +2 -1
- package/src/calls/twilio-routes.ts +1 -2
- package/src/calls/voice-ingress-preflight.ts +1 -1
- package/src/cli/commands/browser-relay.ts +46 -15
- package/src/cli/commands/completions.ts +0 -3
- package/src/cli/commands/credentials.ts +110 -23
- package/src/cli/commands/oauth/apps.ts +255 -0
- package/src/cli/commands/oauth/connections.ts +299 -0
- package/src/cli/commands/oauth/index.ts +52 -0
- package/src/cli/commands/oauth/providers.ts +242 -0
- package/src/cli/commands/skills.ts +4 -338
- package/src/cli/program.ts +1 -5
- package/src/cli/reference.ts +1 -3
- package/src/cli.ts +3 -2
- package/src/config/assistant-feature-flags.ts +0 -3
- package/src/config/bundled-skills/_shared/CLI_RETRIEVAL_PATTERN.md +1 -1
- package/src/config/bundled-skills/claude-code/TOOLS.json +0 -4
- package/src/config/bundled-skills/computer-use/SKILL.md +3 -6
- package/src/config/bundled-skills/computer-use/TOOLS.json +22 -4
- package/src/config/bundled-skills/contacts/tools/google-contacts.ts +29 -32
- package/src/config/bundled-skills/gmail/SKILL.md +4 -4
- package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +54 -61
- package/src/config/bundled-skills/gmail/tools/gmail-attachments.ts +25 -28
- package/src/config/bundled-skills/gmail/tools/gmail-draft.ts +14 -17
- package/src/config/bundled-skills/gmail/tools/gmail-filters.ts +39 -44
- package/src/config/bundled-skills/gmail/tools/gmail-follow-up.ts +61 -58
- package/src/config/bundled-skills/gmail/tools/gmail-forward.ts +50 -49
- package/src/config/bundled-skills/gmail/tools/gmail-label.ts +11 -13
- package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +148 -146
- package/src/config/bundled-skills/gmail/tools/gmail-send-draft.ts +4 -7
- package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +175 -173
- package/src/config/bundled-skills/gmail/tools/gmail-trash.ts +4 -7
- package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +71 -76
- package/src/config/bundled-skills/gmail/tools/gmail-vacation.ts +32 -38
- package/src/config/bundled-skills/google-calendar/SKILL.md +2 -2
- package/src/config/bundled-skills/google-calendar/calendar-client.ts +90 -44
- package/src/config/bundled-skills/google-calendar/tools/calendar-check-availability.ts +9 -10
- package/src/config/bundled-skills/google-calendar/tools/calendar-create-event.ts +5 -6
- package/src/config/bundled-skills/google-calendar/tools/calendar-get-event.ts +4 -5
- package/src/config/bundled-skills/google-calendar/tools/calendar-list-events.ts +14 -15
- package/src/config/bundled-skills/google-calendar/tools/calendar-rsvp.ts +37 -37
- package/src/config/bundled-skills/google-calendar/tools/shared.ts +4 -9
- package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +24 -3
- package/src/config/bundled-skills/messaging/SKILL.md +6 -6
- package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +62 -63
- package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +15 -16
- package/src/config/bundled-skills/messaging/tools/messaging-auth-test.ts +4 -5
- package/src/config/bundled-skills/messaging/tools/messaging-list-conversations.ts +6 -7
- package/src/config/bundled-skills/messaging/tools/messaging-mark-read.ts +4 -5
- package/src/config/bundled-skills/messaging/tools/messaging-read.ts +14 -15
- package/src/config/bundled-skills/messaging/tools/messaging-search.ts +4 -5
- package/src/config/bundled-skills/messaging/tools/messaging-send.ts +128 -128
- package/src/config/bundled-skills/messaging/tools/messaging-sender-digest.ts +33 -34
- package/src/config/bundled-skills/messaging/tools/shared.ts +12 -15
- package/src/config/bundled-skills/settings/SKILL.md +1 -1
- package/src/config/bundled-skills/settings/TOOLS.json +2 -8
- package/src/config/bundled-skills/settings/tools/voice-config-update.ts +5 -33
- package/src/config/bundled-skills/slack/tools/shared.ts +4 -10
- package/src/config/bundled-skills/slack/tools/slack-add-reaction.ts +4 -5
- package/src/config/bundled-skills/slack/tools/slack-channel-details.ts +15 -16
- package/src/config/bundled-skills/slack/tools/slack-delete-message.ts +4 -5
- package/src/config/bundled-skills/slack/tools/slack-edit-message.ts +4 -5
- package/src/config/bundled-skills/slack/tools/slack-leave-channel.ts +4 -5
- package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +95 -92
- package/src/config/env-registry.ts +14 -83
- package/src/config/env.ts +11 -50
- package/src/config/feature-flag-registry.json +16 -16
- package/src/config/schema.ts +3 -1
- package/src/config/skills.ts +21 -2
- package/src/context/image-dimensions.ts +229 -0
- package/src/context/token-estimator.ts +75 -12
- package/src/context/window-manager.ts +49 -10
- package/src/daemon/assistant-attachments.ts +1 -13
- package/src/daemon/guardian-action-generators.ts +4 -5
- package/src/daemon/handlers/config-ingress.ts +8 -33
- package/src/daemon/handlers/config-slack-channel.ts +76 -56
- package/src/daemon/handlers/config-telegram.ts +53 -24
- package/src/daemon/handlers/sessions.ts +10 -24
- package/src/daemon/handlers/shared.ts +0 -130
- package/src/daemon/host-cu-proxy.ts +401 -0
- package/src/daemon/lifecycle.ts +39 -63
- package/src/daemon/message-protocol.ts +3 -0
- package/src/daemon/message-types/computer-use.ts +2 -119
- package/src/daemon/message-types/host-cu.ts +19 -0
- package/src/daemon/message-types/integrations.ts +1 -0
- package/src/daemon/message-types/messages.ts +3 -0
- package/src/daemon/server.ts +14 -21
- package/src/daemon/session-agent-loop-handlers.ts +2 -0
- package/src/daemon/session-attachments.ts +1 -2
- package/src/daemon/session-messaging.ts +3 -1
- package/src/daemon/session-slash.ts +1 -1
- package/src/daemon/session-surfaces.ts +40 -28
- package/src/daemon/session-tool-setup.ts +20 -11
- package/src/daemon/session.ts +139 -16
- package/src/daemon/tool-side-effects.ts +2 -8
- package/src/daemon/watch-handler.ts +2 -2
- package/src/email/providers/index.ts +2 -1
- package/src/events/tool-metrics-listener.ts +2 -2
- package/src/hooks/manager.ts +1 -4
- package/src/inbound/public-ingress-urls.ts +7 -7
- package/src/instrument.ts +15 -1
- package/src/logfire.ts +16 -5
- package/src/media/app-icon-generator.ts +30 -4
- package/src/media/avatar-router.ts +26 -3
- package/src/media/gemini-image-service.ts +28 -2
- package/src/memory/conversation-key-store.ts +21 -0
- package/src/memory/db-init.ts +4 -0
- package/src/memory/guardian-action-store.ts +1 -1
- package/src/memory/migrations/149-oauth-tables.ts +60 -0
- package/src/memory/migrations/index.ts +1 -0
- package/src/memory/schema/guardian.ts +1 -1
- package/src/memory/schema/index.ts +1 -0
- package/src/memory/schema/oauth.ts +65 -0
- package/src/messaging/provider.ts +19 -13
- package/src/messaging/providers/gmail/adapter.ts +40 -23
- package/src/messaging/providers/gmail/client.ts +283 -122
- package/src/messaging/providers/gmail/people-client.ts +32 -24
- package/src/messaging/providers/slack/adapter.ts +29 -19
- package/src/messaging/providers/slack/client.ts +265 -78
- package/src/messaging/providers/telegram-bot/adapter.ts +19 -18
- package/src/messaging/providers/whatsapp/adapter.ts +17 -11
- package/src/messaging/registry.ts +2 -31
- package/src/notifications/copy-composer.ts +0 -5
- package/src/notifications/signal.ts +4 -5
- package/src/oauth/byo-connection.test.ts +537 -0
- package/src/oauth/byo-connection.ts +128 -0
- package/src/oauth/connect-orchestrator.ts +139 -56
- package/src/oauth/connect-types.ts +17 -23
- package/src/oauth/connection-resolver.ts +58 -0
- package/src/oauth/connection.ts +38 -0
- package/src/oauth/manual-token-connection.ts +104 -0
- package/src/oauth/oauth-store.ts +496 -0
- package/src/oauth/platform-connection.test.ts +192 -0
- package/src/oauth/platform-connection.ts +111 -0
- package/src/oauth/provider-behaviors.ts +124 -0
- package/src/oauth/scope-policy.ts +9 -2
- package/src/oauth/seed-providers.ts +161 -0
- package/src/oauth/token-persistence.ts +74 -78
- package/src/permissions/checker.ts +8 -4
- package/src/permissions/defaults.ts +0 -1
- package/src/permissions/prompter.ts +10 -1
- package/src/permissions/trust-store.ts +13 -0
- package/src/prompts/__tests__/build-cli-reference-section.test.ts +3 -1
- package/src/prompts/system-prompt.ts +70 -45
- package/src/providers/anthropic/client.ts +133 -24
- package/src/providers/gemini/client.ts +15 -6
- package/src/providers/managed-proxy/constants.ts +2 -2
- package/src/providers/managed-proxy/context.ts +5 -1
- package/src/providers/ratelimit.ts +17 -0
- package/src/providers/registry.ts +2 -2
- package/src/providers/retry.ts +1 -27
- package/src/runtime/AGENTS.md +17 -0
- package/src/runtime/auth/route-policy.ts +0 -3
- package/src/runtime/channel-invite-transports/telegram.ts +2 -1
- package/src/runtime/channel-readiness-service.ts +168 -195
- package/src/runtime/channel-readiness-types.ts +4 -0
- package/src/runtime/channel-reply-delivery.ts +0 -40
- package/src/runtime/gateway-client.ts +0 -7
- package/src/runtime/guardian-action-conversation-turn.ts +1 -3
- package/src/runtime/guardian-action-followup-executor.ts +1 -1
- package/src/runtime/guardian-action-message-composer.ts +3 -23
- package/src/runtime/http-server.ts +17 -10
- package/src/runtime/http-types.ts +2 -3
- package/src/runtime/middleware/rate-limiter.ts +74 -20
- package/src/runtime/middleware/twilio-validation.ts +1 -11
- package/src/runtime/pending-interactions.ts +14 -12
- package/src/runtime/routes/channel-delivery-routes.ts +0 -1
- package/src/runtime/routes/channel-readiness-routes.ts +2 -0
- package/src/runtime/routes/conversation-routes.ts +73 -19
- package/src/runtime/routes/diagnostics-routes.ts +11 -9
- package/src/runtime/routes/events-routes.ts +21 -11
- package/src/runtime/routes/guardian-approval-interception.ts +20 -5
- package/src/runtime/routes/host-cu-routes.ts +97 -0
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +12 -111
- package/src/runtime/routes/integrations/slack/share.ts +6 -6
- package/src/runtime/routes/integrations/twilio.ts +6 -5
- package/src/runtime/routes/log-export-routes.ts +126 -8
- package/src/runtime/routes/secret-routes.ts +3 -2
- package/src/runtime/routes/settings-routes.ts +113 -48
- package/src/runtime/routes/surface-action-routes.ts +1 -1
- package/src/runtime/routes/watch-routes.ts +128 -0
- package/src/schedule/integration-status.ts +10 -8
- package/src/security/credential-key.ts +14 -0
- package/src/security/keychain-broker-client.ts +5 -6
- package/src/security/oauth2.ts +1 -1
- package/src/security/token-manager.ts +145 -43
- package/src/skills/catalog-install.ts +358 -0
- package/src/skills/include-graph.ts +32 -0
- package/src/telegram/bot-username.ts +2 -3
- package/src/tools/apps/definitions.ts +0 -5
- package/src/tools/assets/materialize.ts +0 -5
- package/src/tools/assets/search.ts +0 -5
- package/src/tools/browser/headless-browser.ts +1 -67
- package/src/tools/browser/network-recorder.ts +1 -1
- package/src/tools/browser/network-recording-types.ts +1 -1
- package/src/tools/claude-code/claude-code.ts +0 -5
- package/src/tools/computer-use/definitions.ts +46 -11
- package/src/tools/computer-use/registry.ts +4 -5
- package/src/tools/credentials/broker.ts +5 -4
- package/src/tools/credentials/metadata-store.ts +22 -74
- package/src/tools/credentials/resolve.ts +2 -1
- package/src/tools/credentials/vault.ts +139 -151
- package/src/tools/filesystem/edit.ts +1 -6
- package/src/tools/filesystem/read.ts +0 -5
- package/src/tools/filesystem/write.ts +1 -6
- package/src/tools/host-filesystem/edit.ts +1 -6
- package/src/tools/host-filesystem/read.ts +1 -6
- package/src/tools/host-filesystem/write.ts +1 -6
- package/src/tools/mcp/mcp-tool-factory.ts +18 -1
- package/src/tools/memory/definitions.ts +0 -5
- package/src/tools/network/web-fetch.ts +0 -5
- package/src/tools/network/web-search.ts +0 -5
- package/src/tools/registry.ts +2 -7
- package/src/tools/schema-transforms.ts +99 -0
- package/src/tools/skills/load.ts +62 -8
- package/src/tools/swarm/delegate.ts +0 -5
- package/src/tools/system/avatar-generator.ts +0 -5
- package/src/tools/ui-surface/definitions.ts +0 -15
- package/src/tools/watch/screen-watch.ts +0 -5
- package/src/tools/watch/watch-state.ts +0 -12
- package/src/util/logger.ts +7 -41
- package/src/util/platform.ts +9 -28
- package/src/version.ts +10 -0
- package/src/watcher/providers/github.ts +51 -52
- package/src/watcher/providers/gmail.ts +88 -80
- package/src/watcher/providers/google-calendar.ts +94 -86
- package/src/watcher/providers/linear.ts +87 -93
- package/src/__tests__/computer-use-session-compaction.test.ts +0 -143
- package/src/__tests__/computer-use-session-lifecycle.test.ts +0 -322
- package/src/__tests__/computer-use-session-working-dir.test.ts +0 -166
- package/src/__tests__/computer-use-skill-baseline.test.ts +0 -78
- package/src/__tests__/computer-use-skill-endstate.test.ts +0 -105
- package/src/__tests__/computer-use-skill-lifecycle-cleanup.test.ts +0 -249
- package/src/__tests__/ride-shotgun-handler.test.ts +0 -452
- package/src/cli/commands/dev.ts +0 -129
- package/src/cli/commands/map.ts +0 -391
- package/src/cli/commands/oauth.ts +0 -77
- package/src/config/bundled-skills/computer-use/tools/computer-use-request-control.ts +0 -16
- package/src/daemon/computer-use-session.ts +0 -1020
- package/src/daemon/ride-shotgun-handler.ts +0 -567
- package/src/oauth/provider-profiles.ts +0 -192
- package/src/prompts/computer-use-prompt.ts +0 -98
- package/src/runtime/routes/computer-use-routes.ts +0 -641
- package/src/runtime/telegram-streaming-delivery.test.ts +0 -597
- package/src/runtime/telegram-streaming-delivery.ts +0 -383
- package/src/tools/computer-use/request-computer-control.ts +0 -61
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CRUD store for OAuth providers, apps, and connections.
|
|
3
|
+
*
|
|
4
|
+
* Backed by Drizzle + SQLite. All JSON fields (default_scopes, scope_policy,
|
|
5
|
+
* extra_params, granted_scopes, metadata) are stored as serialized JSON strings.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { and, desc, eq, sql } from "drizzle-orm";
|
|
9
|
+
import { v4 as uuid } from "uuid";
|
|
10
|
+
|
|
11
|
+
import { getDb, rawChanges } from "../memory/db.js";
|
|
12
|
+
import {
|
|
13
|
+
oauthApps,
|
|
14
|
+
oauthConnections,
|
|
15
|
+
oauthProviders,
|
|
16
|
+
} from "../memory/schema/oauth.js";
|
|
17
|
+
import {
|
|
18
|
+
deleteSecureKeyAsync,
|
|
19
|
+
getSecureKey,
|
|
20
|
+
setSecureKeyAsync,
|
|
21
|
+
} from "../security/secure-keys.js";
|
|
22
|
+
import { getLogger } from "../util/logger.js";
|
|
23
|
+
|
|
24
|
+
const log = getLogger("oauth-store");
|
|
25
|
+
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
// Row types
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
export type OAuthProviderRow = typeof oauthProviders.$inferSelect;
|
|
31
|
+
export type OAuthAppRow = typeof oauthApps.$inferSelect;
|
|
32
|
+
export type OAuthConnectionRow = typeof oauthConnections.$inferSelect;
|
|
33
|
+
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
// Provider operations
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Seed well-known provider profiles into the database. Uses INSERT … ON
|
|
40
|
+
* CONFLICT DO UPDATE so that corrections to seed data (e.g. a fixed baseUrl)
|
|
41
|
+
* propagate to existing installations on the next startup.
|
|
42
|
+
*/
|
|
43
|
+
export function seedProviders(
|
|
44
|
+
profiles: Array<{
|
|
45
|
+
providerKey: string;
|
|
46
|
+
authUrl: string;
|
|
47
|
+
tokenUrl: string;
|
|
48
|
+
tokenEndpointAuthMethod?: string;
|
|
49
|
+
userinfoUrl?: string;
|
|
50
|
+
baseUrl?: string;
|
|
51
|
+
defaultScopes: string[];
|
|
52
|
+
scopePolicy: Record<string, unknown>;
|
|
53
|
+
extraParams?: Record<string, string>;
|
|
54
|
+
callbackTransport?: string;
|
|
55
|
+
loopbackPort?: number;
|
|
56
|
+
}>,
|
|
57
|
+
): void {
|
|
58
|
+
const db = getDb();
|
|
59
|
+
const now = Date.now();
|
|
60
|
+
for (const p of profiles) {
|
|
61
|
+
const authUrl = p.authUrl;
|
|
62
|
+
const tokenUrl = p.tokenUrl;
|
|
63
|
+
const tokenEndpointAuthMethod = p.tokenEndpointAuthMethod ?? null;
|
|
64
|
+
const userinfoUrl = p.userinfoUrl ?? null;
|
|
65
|
+
const baseUrl = p.baseUrl ?? null;
|
|
66
|
+
const defaultScopes = JSON.stringify(p.defaultScopes);
|
|
67
|
+
const scopePolicy = JSON.stringify(p.scopePolicy);
|
|
68
|
+
const extraParams = p.extraParams ? JSON.stringify(p.extraParams) : null;
|
|
69
|
+
const callbackTransport = p.callbackTransport ?? null;
|
|
70
|
+
const loopbackPort = p.loopbackPort ?? null;
|
|
71
|
+
|
|
72
|
+
db.insert(oauthProviders)
|
|
73
|
+
.values({
|
|
74
|
+
providerKey: p.providerKey,
|
|
75
|
+
authUrl,
|
|
76
|
+
tokenUrl,
|
|
77
|
+
tokenEndpointAuthMethod,
|
|
78
|
+
userinfoUrl,
|
|
79
|
+
baseUrl,
|
|
80
|
+
defaultScopes,
|
|
81
|
+
scopePolicy,
|
|
82
|
+
extraParams,
|
|
83
|
+
callbackTransport,
|
|
84
|
+
loopbackPort,
|
|
85
|
+
createdAt: now,
|
|
86
|
+
updatedAt: now,
|
|
87
|
+
})
|
|
88
|
+
.onConflictDoUpdate({
|
|
89
|
+
target: oauthProviders.providerKey,
|
|
90
|
+
set: {
|
|
91
|
+
authUrl,
|
|
92
|
+
tokenUrl,
|
|
93
|
+
tokenEndpointAuthMethod,
|
|
94
|
+
userinfoUrl,
|
|
95
|
+
baseUrl,
|
|
96
|
+
defaultScopes,
|
|
97
|
+
scopePolicy,
|
|
98
|
+
extraParams,
|
|
99
|
+
callbackTransport,
|
|
100
|
+
loopbackPort,
|
|
101
|
+
updatedAt: now,
|
|
102
|
+
},
|
|
103
|
+
})
|
|
104
|
+
.run();
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Look up a provider by its primary key. */
|
|
109
|
+
export function getProvider(providerKey: string): OAuthProviderRow | undefined {
|
|
110
|
+
const db = getDb();
|
|
111
|
+
return db
|
|
112
|
+
.select()
|
|
113
|
+
.from(oauthProviders)
|
|
114
|
+
.where(eq(oauthProviders.providerKey, providerKey))
|
|
115
|
+
.get();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Return all registered providers. */
|
|
119
|
+
export function listProviders(): OAuthProviderRow[] {
|
|
120
|
+
const db = getDb();
|
|
121
|
+
return db.select().from(oauthProviders).all();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Register a new provider (for dynamic registration). Throws if the
|
|
126
|
+
* provider_key already exists.
|
|
127
|
+
*/
|
|
128
|
+
export function registerProvider(params: {
|
|
129
|
+
providerKey: string;
|
|
130
|
+
authUrl: string;
|
|
131
|
+
tokenUrl: string;
|
|
132
|
+
tokenEndpointAuthMethod?: string;
|
|
133
|
+
userinfoUrl?: string;
|
|
134
|
+
baseUrl?: string;
|
|
135
|
+
defaultScopes: string[];
|
|
136
|
+
scopePolicy: Record<string, unknown>;
|
|
137
|
+
extraParams?: Record<string, string>;
|
|
138
|
+
callbackTransport?: string;
|
|
139
|
+
loopbackPort?: number;
|
|
140
|
+
}): OAuthProviderRow {
|
|
141
|
+
const db = getDb();
|
|
142
|
+
const now = Date.now();
|
|
143
|
+
|
|
144
|
+
const existing = getProvider(params.providerKey);
|
|
145
|
+
if (existing) {
|
|
146
|
+
throw new Error(`OAuth provider already exists: ${params.providerKey}`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const row = {
|
|
150
|
+
providerKey: params.providerKey,
|
|
151
|
+
authUrl: params.authUrl,
|
|
152
|
+
tokenUrl: params.tokenUrl,
|
|
153
|
+
tokenEndpointAuthMethod: params.tokenEndpointAuthMethod ?? null,
|
|
154
|
+
userinfoUrl: params.userinfoUrl ?? null,
|
|
155
|
+
baseUrl: params.baseUrl ?? null,
|
|
156
|
+
defaultScopes: JSON.stringify(params.defaultScopes),
|
|
157
|
+
scopePolicy: JSON.stringify(params.scopePolicy),
|
|
158
|
+
extraParams: params.extraParams ? JSON.stringify(params.extraParams) : null,
|
|
159
|
+
callbackTransport: params.callbackTransport ?? null,
|
|
160
|
+
loopbackPort: params.loopbackPort ?? null,
|
|
161
|
+
createdAt: now,
|
|
162
|
+
updatedAt: now,
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
db.insert(oauthProviders).values(row).run();
|
|
166
|
+
|
|
167
|
+
return row;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ---------------------------------------------------------------------------
|
|
171
|
+
// App operations
|
|
172
|
+
// ---------------------------------------------------------------------------
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Insert or return an existing app by (provider_key, client_id).
|
|
176
|
+
* Generates a UUID on insert.
|
|
177
|
+
*/
|
|
178
|
+
export async function upsertApp(
|
|
179
|
+
providerKey: string,
|
|
180
|
+
clientId: string,
|
|
181
|
+
clientSecret?: string,
|
|
182
|
+
): Promise<OAuthAppRow> {
|
|
183
|
+
const db = getDb();
|
|
184
|
+
|
|
185
|
+
const existing = db
|
|
186
|
+
.select()
|
|
187
|
+
.from(oauthApps)
|
|
188
|
+
.where(
|
|
189
|
+
and(
|
|
190
|
+
eq(oauthApps.providerKey, providerKey),
|
|
191
|
+
eq(oauthApps.clientId, clientId),
|
|
192
|
+
),
|
|
193
|
+
)
|
|
194
|
+
.get();
|
|
195
|
+
|
|
196
|
+
if (existing) {
|
|
197
|
+
if (clientSecret) {
|
|
198
|
+
const stored = await setSecureKeyAsync(
|
|
199
|
+
`oauth_app/${existing.id}/client_secret`,
|
|
200
|
+
clientSecret,
|
|
201
|
+
);
|
|
202
|
+
if (!stored) {
|
|
203
|
+
throw new Error("Failed to store client_secret in secure storage");
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return existing;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const now = Date.now();
|
|
210
|
+
const id = uuid();
|
|
211
|
+
const row = {
|
|
212
|
+
id,
|
|
213
|
+
providerKey,
|
|
214
|
+
clientId,
|
|
215
|
+
createdAt: now,
|
|
216
|
+
updatedAt: now,
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
db.insert(oauthApps).values(row).run();
|
|
220
|
+
|
|
221
|
+
if (clientSecret) {
|
|
222
|
+
const stored = await setSecureKeyAsync(
|
|
223
|
+
`oauth_app/${id}/client_secret`,
|
|
224
|
+
clientSecret,
|
|
225
|
+
);
|
|
226
|
+
if (!stored) {
|
|
227
|
+
throw new Error("Failed to store client_secret in secure storage");
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return row;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** Look up an app by its primary key. */
|
|
235
|
+
export function getApp(id: string): OAuthAppRow | undefined {
|
|
236
|
+
const db = getDb();
|
|
237
|
+
return db.select().from(oauthApps).where(eq(oauthApps.id, id)).get();
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** Look up an app by (provider_key, client_id). */
|
|
241
|
+
export function getAppByProviderAndClientId(
|
|
242
|
+
providerKey: string,
|
|
243
|
+
clientId: string,
|
|
244
|
+
): OAuthAppRow | undefined {
|
|
245
|
+
const db = getDb();
|
|
246
|
+
return db
|
|
247
|
+
.select()
|
|
248
|
+
.from(oauthApps)
|
|
249
|
+
.where(
|
|
250
|
+
and(
|
|
251
|
+
eq(oauthApps.providerKey, providerKey),
|
|
252
|
+
eq(oauthApps.clientId, clientId),
|
|
253
|
+
),
|
|
254
|
+
)
|
|
255
|
+
.get();
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Get the most recently created app for a provider.
|
|
260
|
+
* Returns undefined if no app exists for this provider.
|
|
261
|
+
*/
|
|
262
|
+
export function getMostRecentAppByProvider(
|
|
263
|
+
providerKey: string,
|
|
264
|
+
): OAuthAppRow | undefined {
|
|
265
|
+
const db = getDb();
|
|
266
|
+
return db
|
|
267
|
+
.select()
|
|
268
|
+
.from(oauthApps)
|
|
269
|
+
.where(eq(oauthApps.providerKey, providerKey))
|
|
270
|
+
.orderBy(desc(oauthApps.createdAt))
|
|
271
|
+
.limit(1)
|
|
272
|
+
.get();
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** Return all OAuth apps. */
|
|
276
|
+
export function listApps(): OAuthAppRow[] {
|
|
277
|
+
const db = getDb();
|
|
278
|
+
return db.select().from(oauthApps).all();
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/** Delete an app by ID. Cleans up the client_secret from secure storage. Returns true if a row was deleted. */
|
|
282
|
+
export async function deleteApp(id: string): Promise<boolean> {
|
|
283
|
+
// Delete the DB row first so that if it fails (e.g. FK constraint from
|
|
284
|
+
// existing connections), the secret in secure storage remains intact.
|
|
285
|
+
const db = getDb();
|
|
286
|
+
db.delete(oauthApps).where(eq(oauthApps.id, id)).run();
|
|
287
|
+
const deleted = rawChanges() > 0;
|
|
288
|
+
|
|
289
|
+
if (!deleted) return false;
|
|
290
|
+
|
|
291
|
+
const result = await deleteSecureKeyAsync(`oauth_app/${id}/client_secret`);
|
|
292
|
+
if (result === "error") {
|
|
293
|
+
throw new Error(
|
|
294
|
+
`Deleted app ${id} but failed to remove client_secret from secure storage`,
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return true;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// ---------------------------------------------------------------------------
|
|
302
|
+
// Connection operations
|
|
303
|
+
// ---------------------------------------------------------------------------
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Create a new OAuth connection. Generates a UUID and sets status='active'.
|
|
307
|
+
* `metadata` is an optional JSON object for provider-specific token response data.
|
|
308
|
+
*/
|
|
309
|
+
export function createConnection(params: {
|
|
310
|
+
oauthAppId: string;
|
|
311
|
+
providerKey: string;
|
|
312
|
+
accountInfo?: string;
|
|
313
|
+
grantedScopes: string[];
|
|
314
|
+
expiresAt?: number;
|
|
315
|
+
hasRefreshToken: boolean;
|
|
316
|
+
label?: string;
|
|
317
|
+
metadata?: Record<string, unknown>;
|
|
318
|
+
/** Override the creation timestamp. Useful in tests to ensure deterministic ordering. */
|
|
319
|
+
createdAt?: number;
|
|
320
|
+
}): OAuthConnectionRow {
|
|
321
|
+
const db = getDb();
|
|
322
|
+
const now = params.createdAt ?? Date.now();
|
|
323
|
+
const id = uuid();
|
|
324
|
+
|
|
325
|
+
const row = {
|
|
326
|
+
id,
|
|
327
|
+
oauthAppId: params.oauthAppId,
|
|
328
|
+
providerKey: params.providerKey,
|
|
329
|
+
accountInfo: params.accountInfo ?? null,
|
|
330
|
+
grantedScopes: JSON.stringify(params.grantedScopes),
|
|
331
|
+
expiresAt: params.expiresAt ?? null,
|
|
332
|
+
hasRefreshToken: params.hasRefreshToken ? 1 : 0,
|
|
333
|
+
status: "active" as const,
|
|
334
|
+
label: params.label ?? null,
|
|
335
|
+
metadata: params.metadata ? JSON.stringify(params.metadata) : null,
|
|
336
|
+
createdAt: now,
|
|
337
|
+
updatedAt: now,
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
db.insert(oauthConnections).values(row).run();
|
|
341
|
+
|
|
342
|
+
return row;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/** Look up a connection by its primary key. */
|
|
346
|
+
export function getConnection(id: string): OAuthConnectionRow | undefined {
|
|
347
|
+
const db = getDb();
|
|
348
|
+
return db
|
|
349
|
+
.select()
|
|
350
|
+
.from(oauthConnections)
|
|
351
|
+
.where(eq(oauthConnections.id, id))
|
|
352
|
+
.get();
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Get the most recent active connection for a provider.
|
|
357
|
+
* Returns undefined if no active connection exists.
|
|
358
|
+
*/
|
|
359
|
+
export function getConnectionByProvider(
|
|
360
|
+
providerKey: string,
|
|
361
|
+
): OAuthConnectionRow | undefined {
|
|
362
|
+
const db = getDb();
|
|
363
|
+
return db
|
|
364
|
+
.select()
|
|
365
|
+
.from(oauthConnections)
|
|
366
|
+
.where(
|
|
367
|
+
and(
|
|
368
|
+
eq(oauthConnections.providerKey, providerKey),
|
|
369
|
+
eq(oauthConnections.status, "active"),
|
|
370
|
+
),
|
|
371
|
+
)
|
|
372
|
+
.orderBy(desc(oauthConnections.createdAt), sql`rowid DESC`)
|
|
373
|
+
.limit(1)
|
|
374
|
+
.get();
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Check whether a provider has a usable OAuth connection: an active row in the
|
|
379
|
+
* database AND a corresponding access token in secure storage.
|
|
380
|
+
*
|
|
381
|
+
* This guards against the edge case where the connection row was created/updated
|
|
382
|
+
* but the secure-key write for the access token failed, which would make
|
|
383
|
+
* `resolveOAuthConnection()` throw at usage time.
|
|
384
|
+
*/
|
|
385
|
+
export function isProviderConnected(providerKey: string): boolean {
|
|
386
|
+
const conn = getConnectionByProvider(providerKey);
|
|
387
|
+
if (!conn || conn.status !== "active") return false;
|
|
388
|
+
return getSecureKey(`oauth_connection/${conn.id}/access_token`) !== undefined;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Update fields on an existing connection. Returns true if a row was updated.
|
|
393
|
+
*/
|
|
394
|
+
export function updateConnection(
|
|
395
|
+
id: string,
|
|
396
|
+
updates: Partial<{
|
|
397
|
+
oauthAppId: string;
|
|
398
|
+
accountInfo: string;
|
|
399
|
+
grantedScopes: string[];
|
|
400
|
+
/** Pass `null` to explicitly clear a stale expiresAt in the DB. */
|
|
401
|
+
expiresAt: number | null;
|
|
402
|
+
hasRefreshToken: boolean;
|
|
403
|
+
status: string;
|
|
404
|
+
label: string;
|
|
405
|
+
metadata: Record<string, unknown>;
|
|
406
|
+
}>,
|
|
407
|
+
): boolean {
|
|
408
|
+
const db = getDb();
|
|
409
|
+
const now = Date.now();
|
|
410
|
+
|
|
411
|
+
// Build the set clause, serializing JSON fields and converting booleans.
|
|
412
|
+
// For expiresAt, null means "clear the column" so we check for undefined
|
|
413
|
+
// explicitly rather than truthiness.
|
|
414
|
+
const set: Record<string, unknown> = { updatedAt: now };
|
|
415
|
+
if (updates.oauthAppId !== undefined) set.oauthAppId = updates.oauthAppId;
|
|
416
|
+
if (updates.accountInfo !== undefined) set.accountInfo = updates.accountInfo;
|
|
417
|
+
if (updates.grantedScopes !== undefined)
|
|
418
|
+
set.grantedScopes = JSON.stringify(updates.grantedScopes);
|
|
419
|
+
if (updates.expiresAt !== undefined) set.expiresAt = updates.expiresAt;
|
|
420
|
+
if (updates.hasRefreshToken !== undefined)
|
|
421
|
+
set.hasRefreshToken = updates.hasRefreshToken ? 1 : 0;
|
|
422
|
+
if (updates.status !== undefined) set.status = updates.status;
|
|
423
|
+
if (updates.label !== undefined) set.label = updates.label;
|
|
424
|
+
if (updates.metadata !== undefined)
|
|
425
|
+
set.metadata = JSON.stringify(updates.metadata);
|
|
426
|
+
|
|
427
|
+
db.update(oauthConnections).set(set).where(eq(oauthConnections.id, id)).run();
|
|
428
|
+
|
|
429
|
+
return rawChanges() > 0;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/** List connections, optionally filtered by provider key. */
|
|
433
|
+
export function listConnections(providerKey?: string): OAuthConnectionRow[] {
|
|
434
|
+
const db = getDb();
|
|
435
|
+
|
|
436
|
+
if (providerKey) {
|
|
437
|
+
return db
|
|
438
|
+
.select()
|
|
439
|
+
.from(oauthConnections)
|
|
440
|
+
.where(eq(oauthConnections.providerKey, providerKey))
|
|
441
|
+
.all();
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
return db.select().from(oauthConnections).all();
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/** Delete a connection by ID. Returns true if a row was deleted. */
|
|
448
|
+
export function deleteConnection(id: string): boolean {
|
|
449
|
+
const db = getDb();
|
|
450
|
+
db.delete(oauthConnections).where(eq(oauthConnections.id, id)).run();
|
|
451
|
+
return rawChanges() > 0;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// ---------------------------------------------------------------------------
|
|
455
|
+
// Disconnect (full cleanup)
|
|
456
|
+
// ---------------------------------------------------------------------------
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Fully disconnect an OAuth provider: delete the new-format secure keys
|
|
460
|
+
* (access_token and refresh_token) and remove the connection row from SQLite.
|
|
461
|
+
*
|
|
462
|
+
* Returns `"disconnected"` if a connection was found and cleaned up,
|
|
463
|
+
* `"not-found"` if no active connection existed for the given provider,
|
|
464
|
+
* or `"error"` if secure key deletion failed (connection row is preserved
|
|
465
|
+
* to avoid orphaning secrets).
|
|
466
|
+
*/
|
|
467
|
+
export async function disconnectOAuthProvider(
|
|
468
|
+
providerKey: string,
|
|
469
|
+
): Promise<"disconnected" | "not-found" | "error"> {
|
|
470
|
+
const conn = getConnectionByProvider(providerKey);
|
|
471
|
+
if (!conn) return "not-found";
|
|
472
|
+
|
|
473
|
+
const r1 = await deleteSecureKeyAsync(
|
|
474
|
+
`oauth_connection/${conn.id}/access_token`,
|
|
475
|
+
);
|
|
476
|
+
const r2 = await deleteSecureKeyAsync(
|
|
477
|
+
`oauth_connection/${conn.id}/refresh_token`,
|
|
478
|
+
);
|
|
479
|
+
|
|
480
|
+
if (r1 === "error" || r2 === "error") {
|
|
481
|
+
log.warn(
|
|
482
|
+
{
|
|
483
|
+
providerKey,
|
|
484
|
+
connectionId: conn.id,
|
|
485
|
+
accessTokenResult: r1,
|
|
486
|
+
refreshTokenResult: r2,
|
|
487
|
+
},
|
|
488
|
+
"Failed to delete OAuth secure keys — skipping connection row deletion to avoid orphaning secrets",
|
|
489
|
+
);
|
|
490
|
+
return "error";
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
deleteConnection(conn.id);
|
|
494
|
+
|
|
495
|
+
return "disconnected";
|
|
496
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import { BackendError, VellumError } from "../util/errors.js";
|
|
4
|
+
import {
|
|
5
|
+
CredentialRequiredError,
|
|
6
|
+
PlatformOAuthConnection,
|
|
7
|
+
ProviderUnreachableError,
|
|
8
|
+
} from "./platform-connection.js";
|
|
9
|
+
|
|
10
|
+
const DEFAULT_OPTIONS = {
|
|
11
|
+
id: "conn-1",
|
|
12
|
+
providerKey: "integration:gmail",
|
|
13
|
+
externalId: "ext-123",
|
|
14
|
+
accountInfo: "user@example.com",
|
|
15
|
+
grantedScopes: ["https://www.googleapis.com/auth/gmail.readonly"],
|
|
16
|
+
assistantId: "asst-abc",
|
|
17
|
+
platformBaseUrl: "https://platform.example.com",
|
|
18
|
+
apiKey: "test-api-key",
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
describe("PlatformOAuthConnection", () => {
|
|
22
|
+
let originalFetch: typeof globalThis.fetch;
|
|
23
|
+
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
originalFetch = globalThis.fetch;
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
afterEach(() => {
|
|
29
|
+
globalThis.fetch = originalFetch;
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("successful proxied request", async () => {
|
|
33
|
+
const upstreamBody = { messages: [{ id: "msg-1", snippet: "Hello" }] };
|
|
34
|
+
|
|
35
|
+
globalThis.fetch = mock(
|
|
36
|
+
async (url: string | URL | Request, init?: RequestInit) => {
|
|
37
|
+
expect(url).toBe(
|
|
38
|
+
"https://platform.example.com/v1/assistants/asst-abc/external-provider-proxy/gmail/",
|
|
39
|
+
);
|
|
40
|
+
expect(init?.method).toBe("POST");
|
|
41
|
+
expect(init?.headers).toEqual({
|
|
42
|
+
Authorization: "Api-Key test-api-key",
|
|
43
|
+
"Content-Type": "application/json",
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const parsed = JSON.parse(init?.body as string);
|
|
47
|
+
expect(parsed).toEqual({
|
|
48
|
+
request: {
|
|
49
|
+
method: "GET",
|
|
50
|
+
path: "/gmail/v1/users/me/messages",
|
|
51
|
+
query: { maxResults: "10" },
|
|
52
|
+
headers: {},
|
|
53
|
+
body: null,
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
return new Response(
|
|
58
|
+
JSON.stringify({
|
|
59
|
+
status: 200,
|
|
60
|
+
headers: { "content-type": "application/json" },
|
|
61
|
+
body: upstreamBody,
|
|
62
|
+
}),
|
|
63
|
+
{ status: 200 },
|
|
64
|
+
);
|
|
65
|
+
},
|
|
66
|
+
) as unknown as typeof globalThis.fetch;
|
|
67
|
+
|
|
68
|
+
const conn = new PlatformOAuthConnection(DEFAULT_OPTIONS);
|
|
69
|
+
const result = await conn.request({
|
|
70
|
+
method: "GET",
|
|
71
|
+
path: "/gmail/v1/users/me/messages",
|
|
72
|
+
query: { maxResults: "10" },
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
expect(result.status).toBe(200);
|
|
76
|
+
expect(result.headers).toEqual({ "content-type": "application/json" });
|
|
77
|
+
expect(result.body).toEqual(upstreamBody);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("forwards baseUrl when provided", async () => {
|
|
81
|
+
globalThis.fetch = mock(
|
|
82
|
+
async (_url: string | URL | Request, init?: RequestInit) => {
|
|
83
|
+
const parsed = JSON.parse(init?.body as string);
|
|
84
|
+
expect(parsed.request.baseUrl).toBe(
|
|
85
|
+
"https://www.googleapis.com/calendar/v3",
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
return new Response(
|
|
89
|
+
JSON.stringify({ status: 200, headers: {}, body: {} }),
|
|
90
|
+
{ status: 200 },
|
|
91
|
+
);
|
|
92
|
+
},
|
|
93
|
+
) as unknown as typeof globalThis.fetch;
|
|
94
|
+
|
|
95
|
+
const conn = new PlatformOAuthConnection(DEFAULT_OPTIONS);
|
|
96
|
+
await conn.request({
|
|
97
|
+
method: "GET",
|
|
98
|
+
path: "/calendars/primary/events",
|
|
99
|
+
baseUrl: "https://www.googleapis.com/calendar/v3",
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("omits baseUrl from envelope when not provided", async () => {
|
|
104
|
+
globalThis.fetch = mock(
|
|
105
|
+
async (_url: string | URL | Request, init?: RequestInit) => {
|
|
106
|
+
const parsed = JSON.parse(init?.body as string);
|
|
107
|
+
expect("baseUrl" in parsed.request).toBe(false);
|
|
108
|
+
|
|
109
|
+
return new Response(
|
|
110
|
+
JSON.stringify({ status: 200, headers: {}, body: null }),
|
|
111
|
+
{ status: 200 },
|
|
112
|
+
);
|
|
113
|
+
},
|
|
114
|
+
) as unknown as typeof globalThis.fetch;
|
|
115
|
+
|
|
116
|
+
const conn = new PlatformOAuthConnection(DEFAULT_OPTIONS);
|
|
117
|
+
await conn.request({ method: "GET", path: "/some/path" });
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("error classes extend VellumError hierarchy", () => {
|
|
121
|
+
const credErr = new CredentialRequiredError();
|
|
122
|
+
expect(credErr).toBeInstanceOf(BackendError);
|
|
123
|
+
expect(credErr).toBeInstanceOf(VellumError);
|
|
124
|
+
|
|
125
|
+
const provErr = new ProviderUnreachableError();
|
|
126
|
+
expect(provErr).toBeInstanceOf(BackendError);
|
|
127
|
+
expect(provErr).toBeInstanceOf(VellumError);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("424 response throws CredentialRequiredError", async () => {
|
|
131
|
+
globalThis.fetch = mock(async () => {
|
|
132
|
+
return new Response("", { status: 424 });
|
|
133
|
+
}) as unknown as typeof globalThis.fetch;
|
|
134
|
+
|
|
135
|
+
const conn = new PlatformOAuthConnection(DEFAULT_OPTIONS);
|
|
136
|
+
await expect(
|
|
137
|
+
conn.request({ method: "GET", path: "/test" }),
|
|
138
|
+
).rejects.toThrow(CredentialRequiredError);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test("502 response throws ProviderUnreachableError", async () => {
|
|
142
|
+
globalThis.fetch = mock(async () => {
|
|
143
|
+
return new Response("", { status: 502 });
|
|
144
|
+
}) as unknown as typeof globalThis.fetch;
|
|
145
|
+
|
|
146
|
+
const conn = new PlatformOAuthConnection(DEFAULT_OPTIONS);
|
|
147
|
+
await expect(
|
|
148
|
+
conn.request({ method: "GET", path: "/test" }),
|
|
149
|
+
).rejects.toThrow(ProviderUnreachableError);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test("withToken throws clear error", async () => {
|
|
153
|
+
const conn = new PlatformOAuthConnection(DEFAULT_OPTIONS);
|
|
154
|
+
await expect(conn.withToken(async (token) => token)).rejects.toThrow(
|
|
155
|
+
"Raw token access is not supported for platform-managed connections. Use connection.request() instead.",
|
|
156
|
+
);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("strips trailing slash from platformBaseUrl to avoid double slashes", async () => {
|
|
160
|
+
globalThis.fetch = mock(async (url: string | URL | Request) => {
|
|
161
|
+
expect(String(url)).toBe(
|
|
162
|
+
"https://platform.example.com/v1/assistants/asst-abc/external-provider-proxy/gmail/",
|
|
163
|
+
);
|
|
164
|
+
return new Response(
|
|
165
|
+
JSON.stringify({ status: 200, headers: {}, body: null }),
|
|
166
|
+
{ status: 200 },
|
|
167
|
+
);
|
|
168
|
+
}) as unknown as typeof globalThis.fetch;
|
|
169
|
+
|
|
170
|
+
const conn = new PlatformOAuthConnection({
|
|
171
|
+
...DEFAULT_OPTIONS,
|
|
172
|
+
platformBaseUrl: "https://platform.example.com/",
|
|
173
|
+
});
|
|
174
|
+
await conn.request({ method: "GET", path: "/test" });
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test("strips integration: prefix from providerKey for slug", async () => {
|
|
178
|
+
globalThis.fetch = mock(async (url: string | URL | Request) => {
|
|
179
|
+
expect(String(url)).toContain("/external-provider-proxy/slack/");
|
|
180
|
+
return new Response(
|
|
181
|
+
JSON.stringify({ status: 200, headers: {}, body: null }),
|
|
182
|
+
{ status: 200 },
|
|
183
|
+
);
|
|
184
|
+
}) as unknown as typeof globalThis.fetch;
|
|
185
|
+
|
|
186
|
+
const conn = new PlatformOAuthConnection({
|
|
187
|
+
...DEFAULT_OPTIONS,
|
|
188
|
+
providerKey: "integration:slack",
|
|
189
|
+
});
|
|
190
|
+
await conn.request({ method: "GET", path: "/test" });
|
|
191
|
+
});
|
|
192
|
+
});
|