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