@vellumai/assistant 0.9.0 → 0.10.0-staging.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 +18 -34
- package/bun.lock +7 -8
- package/docs/activation-funnel-telemetry.md +28 -22
- package/docs/architecture/security.md +29 -28
- package/docs/stt-provider-onboarding.md +3 -5
- package/docs/workflows-testing.md +13 -44
- package/docs/workflows.md +3 -5
- package/node_modules/@vellumai/ces-client/src/__tests__/ces-client.test.ts +47 -0
- package/node_modules/@vellumai/ces-client/src/rpc-client.ts +28 -5
- package/node_modules/@vellumai/environments/src/seeds.ts +2 -5
- package/node_modules/@vellumai/gateway-client/src/admission-policy-contract.ts +97 -0
- package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +10 -0
- package/node_modules/@vellumai/gateway-client/src/index.ts +32 -6
- package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +119 -0
- package/node_modules/@vellumai/gateway-client/src/types.ts +15 -84
- package/openapi.yaml +976 -63
- package/package.json +2 -1
- package/scripts/sync-llm-catalog.ts +6 -15
- package/scripts/sync-web-search-catalog.ts +3 -11
- package/src/__tests__/access-request-card-view.test.ts +98 -0
- package/src/__tests__/access-request-seed-content-blocks.test.ts +2 -4
- package/src/__tests__/actor-trust-resolver-address-fallback.test.ts +72 -32
- package/src/__tests__/agent-loop-compaction-strip.test.ts +241 -0
- package/src/__tests__/agent-loop-mutable-latest-user-message.test.ts +16 -13
- package/src/__tests__/agent-loop-output-hooks.test.ts +69 -0
- package/src/__tests__/agent-loop-override-profile.test.ts +25 -0
- package/src/__tests__/always-loaded-tools-guard.test.ts +2 -3
- package/src/__tests__/app-compiler.test.ts +15 -1
- package/src/__tests__/app-dir-path-guard.test.ts +0 -1
- package/src/__tests__/assistant-feature-flag-guard.test.ts +1 -4
- package/src/__tests__/assistant-feature-flag-guardrails.test.ts +0 -2
- package/src/__tests__/auth-fallback-events-store.test.ts +6 -14
- package/src/__tests__/avatar-identity-sync.test.ts +2 -27
- package/src/__tests__/btw-routes.test.ts +6 -8
- package/src/__tests__/call-pointer-messages.test.ts +28 -0
- package/src/__tests__/cancel-clears-processing.test.ts +89 -0
- package/src/__tests__/channel-approval-routes.test.ts +0 -4
- package/src/__tests__/channel-inbound-disk-pressure.test.ts +5 -15
- package/src/__tests__/checker.test.ts +0 -3
- package/src/__tests__/cli-memory-v2-reembed-skills.test.ts +3 -4
- package/src/__tests__/compactor-image-manifest-trust.test.ts +21 -1
- package/src/__tests__/compactor-summary-call-truncation.test.ts +223 -0
- package/src/__tests__/config-loader-backfill.test.ts +268 -27
- package/src/__tests__/config-schema.test.ts +35 -0
- package/src/__tests__/config-watcher.test.ts +0 -18
- package/src/__tests__/confirmation-request-guardian-bridge.test.ts +2 -2
- package/src/__tests__/contact-store-user-file.test.ts +0 -6
- package/src/__tests__/contacts-tools.test.ts +29 -0
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +22 -0
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +1 -0
- package/src/__tests__/conversation-agent-loop.test.ts +58 -0
- package/src/__tests__/conversation-attention-telegram.test.ts +0 -1
- package/src/__tests__/conversation-lifecycle.test.ts +7 -9
- package/src/__tests__/conversation-load-history-repair.test.ts +101 -0
- package/src/__tests__/conversation-routes-guardian-reply.test.ts +15 -12
- package/src/__tests__/conversation-surfaces-activation-emit.test.ts +6 -3
- package/src/__tests__/conversation-title-service.test.ts +62 -0
- package/src/__tests__/credential-broker.test.ts +449 -1
- package/src/__tests__/credential-execution-shell-lockdown.test.ts +18 -11
- package/src/__tests__/credential-execution-tools.test.ts +0 -1
- package/src/__tests__/credential-prompt-route.test.ts +4 -4
- package/src/__tests__/credential-routes.test.ts +360 -0
- package/src/__tests__/credential-security-invariants.test.ts +4 -13
- package/src/__tests__/disk-pressure-policy.test.ts +12 -0
- package/src/__tests__/disk-usage.test.ts +65 -0
- package/src/__tests__/dynamic-page-surface.test.ts +152 -1
- package/src/__tests__/fixtures/credential-security-fixtures.ts +2 -33
- package/src/__tests__/gateway-flag-listener.test.ts +110 -1
- package/src/__tests__/gateway-only-guard.test.ts +3 -7
- package/src/__tests__/guardian-binding-drift-heal.test.ts +1 -1
- package/src/__tests__/guardian-card-withdrawal.test.ts +403 -0
- package/src/__tests__/guardian-decision-primitive-canonical.test.ts +5 -3
- package/src/__tests__/guardian-grant-minting.test.ts +3 -35
- package/src/__tests__/guardian-routing-invariants.test.ts +64 -26
- package/src/__tests__/guardian-routing-state.test.ts +0 -1
- package/src/__tests__/headless-browser-mode.test.ts +10 -0
- package/src/__tests__/headless-browser-navigate.test.ts +8 -3
- package/src/__tests__/helpers/create-guardian-binding.ts +0 -1
- package/src/__tests__/host-browser-proxy.test.ts +87 -0
- package/src/__tests__/identity-routes.test.ts +0 -189
- package/src/__tests__/inbound-invite-redemption.test.ts +4 -4
- package/src/__tests__/injector-v3-suppression.test.ts +27 -20
- package/src/__tests__/internal-telemetry-routes.test.ts +6 -14
- package/src/__tests__/invite-redemption-service.test.ts +4 -7
- package/src/__tests__/llm-callsite-catalog.test.ts +5 -6
- package/src/__tests__/llm-catalog-parity.test.ts +30 -23
- package/src/__tests__/llm-resolver.test.ts +70 -24
- package/src/__tests__/llm-schema.test.ts +1 -0
- package/src/__tests__/managed-profile-guard.test.ts +163 -4
- package/src/__tests__/mcp-health-check.test.ts +6 -7
- package/src/__tests__/media-stream-server-integration.test.ts +317 -13
- package/src/__tests__/oauth-provider-seed-logos.test.ts +4 -6
- package/src/__tests__/onboarding-persona-write.test.ts +1 -1
- package/src/__tests__/path-policy.test.ts +34 -0
- package/src/__tests__/persona-resolver.test.ts +49 -14
- package/src/__tests__/plugin-api-model-profiles.test.ts +178 -0
- package/src/__tests__/plugin-api-provider.test.ts +24 -0
- package/src/__tests__/plugin-tool-contribution.test.ts +6 -3
- package/src/__tests__/post-compaction-reinjection-idempotency.test.ts +214 -0
- package/src/__tests__/provider-send-message-override-profile.test.ts +76 -0
- package/src/__tests__/reaction-persistence.test.ts +150 -29
- package/src/__tests__/registry.test.ts +2 -7
- package/src/__tests__/relay-server.test.ts +285 -0
- package/src/__tests__/runtime-attachment-metadata.test.ts +0 -1
- package/src/__tests__/schedule-routes-workflow-validation.test.ts +1 -10
- package/src/__tests__/schedule-routes.test.ts +0 -30
- package/src/__tests__/schedule-tools.test.ts +2 -18
- package/src/__tests__/scheduler-reuse-conversation.test.ts +8 -5
- package/src/__tests__/skill-execute-input.test.ts +51 -1
- package/src/__tests__/skill-runtime-path.test.ts +2 -3
- package/src/__tests__/skills.test.ts +51 -0
- package/src/__tests__/slack-notification-approval-card.test.ts +176 -0
- package/src/__tests__/slack-reaction-canonical-approval.test.ts +285 -0
- package/src/__tests__/subagent-tools.test.ts +266 -0
- package/src/__tests__/surface-completion-nudge-hook.test.ts +367 -0
- package/src/__tests__/task-progress-nudge-hook.test.ts +1 -1
- package/src/__tests__/title-generate-hook.test.ts +100 -3
- package/src/__tests__/token-estimator-accuracy.benchmark.test.ts +1 -29
- package/src/__tests__/token-manager.test.ts +519 -0
- package/src/__tests__/tool-approval-seed-content-blocks.test.ts +1 -1
- package/src/__tests__/tool-audit-listener.test.ts +7 -7
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +6 -3
- package/src/__tests__/tool-executor.test.ts +0 -79
- package/src/__tests__/trusted-contact-approval-notifier.test.ts +4 -2
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +220 -3
- package/src/__tests__/trusted-contact-multichannel.test.ts +3 -3
- package/src/__tests__/trusted-contact-verification.test.ts +8 -10
- package/src/__tests__/twilio-routes.test.ts +81 -1
- package/src/__tests__/voice-invite-redemption.test.ts +2 -3
- package/src/__tests__/weak-open-model.test.ts +30 -0
- package/src/__tests__/web-search-catalog-parity.test.ts +6 -25
- package/src/__tests__/workspace-greetings.test.ts +152 -0
- package/src/__tests__/workspace-migration-105-enable-memory-v3-live-for-new-workspaces.test.ts +149 -0
- package/src/__tests__/workspace-migration-108-drop-balanced-economy-profile.test.ts +285 -0
- package/src/__tests__/workspace-migration-add-send-diagnostics.test.ts +1 -1
- package/src/__tests__/workspace-migration-drop-collect-usage-data.test.ts +118 -0
- package/src/__tests__/workspace-migration-drop-send-diagnostics.test.ts +118 -0
- package/src/a2a/__tests__/e2e-a2a-channel.test.ts +0 -4
- package/src/agent/loop.ts +49 -29
- package/src/api/README.md +6 -6
- package/src/api/events/tool-result.ts +6 -0
- package/src/api/events/workflow-completed.ts +53 -0
- package/src/api/events/workflow-leaf-finished.ts +38 -0
- package/src/api/events/workflow-leaf-started.ts +35 -0
- package/src/api/events/workflow-progress.ts +32 -0
- package/src/api/events/workflow-started.ts +31 -0
- package/src/api/index.ts +40 -0
- package/src/api/responses/conversation-message.ts +28 -4
- package/src/api/responses/home.ts +26 -4
- package/src/api/responses/workflow-journal.ts +53 -0
- package/src/approvals/guardian-card-withdrawal.ts +145 -0
- package/src/approvals/guardian-decision-primitive.ts +26 -3
- package/src/approvals/guardian-request-resolvers.ts +183 -80
- package/src/calls/__tests__/channel-admission-reader.test.ts +132 -0
- package/src/calls/__tests__/relay-setup-router.test.ts +350 -0
- package/src/calls/call-pointer-messages.ts +10 -4
- package/src/calls/channel-admission-reader.ts +104 -0
- package/src/calls/guardian-dispatch.ts +17 -45
- package/src/calls/media-stream-server.ts +84 -2
- package/src/calls/relay-access-wait.ts +1 -1
- package/src/calls/relay-server.ts +66 -0
- package/src/calls/relay-setup-router.ts +82 -1
- package/src/calls/twilio-routes.ts +17 -8
- package/src/calls/voice-session-bridge.ts +2 -2
- package/src/cli/commands/clients.ts +3 -0
- package/src/cli/commands/{__tests__ → memory/__tests__}/memory-v2-compare-render.test.ts +1 -1
- package/src/cli/commands/{__tests__ → memory/__tests__}/memory-v2.test.ts +8 -7
- package/src/cli/commands/{__tests__ → memory/__tests__}/memory-v3.test.ts +5 -4
- package/src/cli/commands/memory/index.ts +30 -0
- package/src/cli/commands/{memory-v2-compare-render.ts → memory/memory-v2-compare-render.ts} +1 -1
- package/src/cli/commands/{memory-v2.ts → memory/memory-v2.ts} +6 -15
- package/src/cli/commands/{memory-v3.ts → memory/memory-v3.ts} +97 -11
- package/src/cli/commands/oauth/status.test.ts +36 -0
- package/src/cli/commands/oauth/status.ts +23 -3
- package/src/cli/commands/plugins.ts +197 -4
- package/src/cli/lib/__tests__/diff-plugin.test.ts +443 -0
- package/src/cli/lib/__tests__/inspect-plugin.test.ts +54 -0
- package/src/cli/lib/__tests__/merge-plugin-tree.test.ts +443 -0
- package/src/cli/lib/__tests__/plugin-surfaces.test.ts +111 -0
- package/src/cli/lib/__tests__/upgrade-plugin.test.ts +295 -2
- package/src/cli/lib/diff-plugin.ts +346 -0
- package/src/cli/lib/inspect-plugin.ts +12 -1
- package/src/cli/lib/install-from-github.ts +105 -17
- package/src/cli/lib/merge-plugin-tree.ts +328 -0
- package/src/cli/lib/plugin-fingerprint.ts +14 -0
- package/src/cli/lib/plugin-surfaces.ts +104 -0
- package/src/cli/lib/upgrade-plugin.ts +298 -10
- package/src/cli/program.ts +2 -6
- package/src/config/__tests__/sync-gated-profiles.test.ts +368 -0
- package/src/config/assistant-feature-flags.ts +22 -7
- package/src/config/bundled-skills/contacts/tools/contact-search.ts +0 -1
- package/src/config/bundled-skills/messaging/SKILL.md +6 -4
- package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +9 -8
- package/src/config/bundled-skills/subagent/SKILL.md +4 -0
- package/src/config/bundled-skills/subagent/TOOLS.json +4 -0
- package/src/config/bundled-skills/workflows/SKILL.md +14 -8
- package/src/config/bundled-tool-registry.ts +2 -7
- package/src/config/call-site-defaults.ts +15 -2
- package/src/config/feature-flag-registry.json +46 -31
- package/src/config/inference-profile-validation.ts +26 -0
- package/src/config/llm-resolver.ts +3 -0
- package/src/config/loader.ts +4 -0
- package/src/config/memory-v3-gate.ts +11 -0
- package/src/config/profile-order.ts +28 -0
- package/src/config/schema.ts +8 -6
- package/src/config/schemas/__tests__/memory-v3.test.ts +1 -0
- package/src/config/schemas/call-site-catalog.ts +7 -0
- package/src/config/schemas/channels.ts +11 -0
- package/src/config/schemas/elevenlabs.ts +0 -1
- package/src/config/schemas/llm.ts +31 -0
- package/src/config/schemas/memory-lifecycle.ts +3 -7
- package/src/config/schemas/memory-v3.ts +6 -0
- package/src/config/schemas/platform.ts +0 -8
- package/src/config/schemas/services.ts +18 -0
- package/src/config/seed-inference-profiles.ts +109 -44
- package/src/config/skills.ts +21 -0
- package/src/config/sync-gated-profiles.ts +220 -0
- package/src/contacts/contact-store.ts +89 -106
- package/src/contacts/contacts-write.ts +5 -22
- package/src/contacts/types.ts +0 -1
- package/src/context/compactor.ts +88 -54
- package/src/context/strip-injections.ts +58 -10
- package/src/context/token-estimator.ts +1 -1
- package/src/credential-execution/process-manager.ts +55 -14
- package/src/credential-execution/prompted-credential.ts +2 -3
- package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +3 -2
- package/src/daemon/config-watcher.ts +0 -4
- package/src/daemon/conversation-agent-loop-handlers.ts +2 -0
- package/src/daemon/conversation-agent-loop.ts +114 -22
- package/src/daemon/conversation-history.ts +1 -1
- package/src/daemon/conversation-lifecycle.ts +3 -5
- package/src/daemon/conversation-process.ts +13 -5
- package/src/daemon/conversation-runtime-assembly.ts +13 -15
- package/src/daemon/conversation-slash.ts +2 -23
- package/src/daemon/conversation-surfaces.ts +26 -0
- package/src/daemon/conversation-tool-setup.ts +27 -14
- package/src/daemon/conversation.ts +66 -14
- package/src/daemon/disk-pressure-policy.ts +5 -3
- package/src/daemon/handlers/__tests__/config-a2a-complete.test.ts +0 -1
- package/src/daemon/handlers/__tests__/config-a2a-redeem.test.ts +0 -1
- package/src/daemon/handlers/config-a2a.ts +0 -2
- package/src/daemon/handlers/config-channels.ts +15 -16
- package/src/daemon/handlers/config-slack-channel.ts +22 -3
- package/src/daemon/handlers/conversations.ts +107 -0
- package/src/daemon/host-browser-proxy.ts +41 -0
- package/src/daemon/lifecycle.ts +55 -27
- package/src/daemon/message-provenance.ts +2 -0
- package/src/daemon/message-types/contacts.ts +0 -1
- package/src/daemon/message-types/conversations.ts +3 -3
- package/src/daemon/message-types/sync.ts +0 -1
- package/src/daemon/message-types/web-activity.ts +7 -1
- package/src/daemon/message-types/workflows.ts +83 -1
- package/src/daemon/orphan-reaper.test.ts +0 -19
- package/src/daemon/orphan-reaper.ts +2 -24
- package/src/daemon/server.ts +0 -10
- package/src/daemon/tool-setup-types.ts +4 -0
- package/src/daemon/trust-context.ts +1 -1
- package/src/events/tool-audit-listener.ts +2 -2
- package/src/home/feed-source-enrichment.test.ts +151 -0
- package/src/home/feed-source-enrichment.ts +176 -0
- package/src/home/relationship-state.ts +2 -4
- package/src/instrument.ts +18 -6
- package/src/ipc/__tests__/binary-result-ipc.test.ts +81 -0
- package/src/ipc/__tests__/clients-list-ipc.test.ts +20 -0
- package/src/ipc/assistant-server.ts +37 -4
- package/src/ipc/gateway-flag-listener.ts +18 -2
- package/src/memory/__tests__/auto-analysis-enqueue.test.ts +5 -16
- package/src/memory/__tests__/jobs-store-enqueue-gate.test.ts +7 -11
- package/src/memory/__tests__/memory-retrospective-enqueue.test.ts +37 -7
- package/src/memory/__tests__/memory-retrospective-job.test.ts +229 -401
- package/src/memory/__tests__/onboarding-events-store.test.ts +7 -7
- package/src/memory/auth-fallback-events-store.ts +2 -2
- package/src/memory/auto-analysis-enqueue.ts +3 -5
- package/src/memory/bookmark-crud.ts +1 -2
- package/src/memory/canonical-guardian-store.ts +39 -1
- package/src/memory/conversation-crud.ts +9 -4
- package/src/memory/conversation-key-store.ts +17 -2
- package/src/memory/conversation-title-service.ts +64 -7
- package/src/memory/db-init.ts +17 -17
- package/src/memory/embedding-backend.ts +38 -1
- package/src/memory/embedding-billing-breaker.ts +96 -0
- package/src/memory/jobs-store.ts +25 -13
- package/src/memory/jobs-worker.ts +54 -1
- package/src/memory/lifecycle-events-store.ts +2 -2
- package/src/memory/memory-retrospective-constants.ts +4 -4
- package/src/memory/memory-retrospective-enqueue.ts +31 -6
- package/src/memory/memory-retrospective-job.ts +28 -227
- package/src/memory/migrations/129-contact-channels-access-fields.ts +18 -9
- package/src/memory/migrations/131-drop-legacy-member-guardian-tables.ts +14 -2
- package/src/memory/migrations/289-contact-channels-unique-ext-user.ts +10 -0
- package/src/memory/migrations/291-contact-channels-renormalize-addresses.ts +72 -0
- package/src/memory/migrations/292-schedule-default-no-reuse-conversation.test.ts +67 -0
- package/src/memory/migrations/292-schedule-default-no-reuse-conversation.ts +25 -0
- package/src/memory/migrations/293-workflow-journal-leaf-tokens.ts +32 -0
- package/src/memory/migrations/294-drop-external-user-id.ts +31 -0
- package/src/memory/migrations/295-drop-approval-prompt-ts-tracker.ts +20 -0
- package/src/memory/migrations/296-rewrite-balanced-economy-profile-pins.test.ts +110 -0
- package/src/memory/migrations/296-rewrite-balanced-economy-profile-pins.ts +68 -0
- package/src/memory/migrations/__tests__/131-drop-legacy-member-guardian-tables.test.ts +154 -0
- package/src/memory/migrations/__tests__/289-contact-channels-unique-ext-user.test.ts +31 -0
- package/src/memory/migrations/__tests__/291-contact-channels-renormalize-addresses.test.ts +341 -0
- package/src/memory/migrations/__tests__/run-migrations.test.ts +52 -0
- package/src/memory/migrations/index.ts +6 -0
- package/src/memory/migrations/run-migrations.ts +41 -0
- package/src/memory/migrations/validate-migration-state.ts +1 -1
- package/src/memory/onboarding-events-store.ts +3 -3
- package/src/memory/schema/contacts.ts +0 -5
- package/src/memory/skill-loaded-events-store.test.ts +7 -15
- package/src/memory/skill-loaded-events-store.ts +2 -2
- package/src/memory/tool-executed-events-store.test.ts +7 -7
- package/src/memory/turn-trace-store.test.ts +736 -0
- package/src/memory/turn-trace-store.ts +364 -0
- package/src/memory/v2/__tests__/consolidation-job.test.ts +8 -0
- package/src/memory/v2/__tests__/skill-content.test.ts +30 -0
- package/src/memory/v2/consolidation-job.ts +2 -2
- package/src/memory/v2/skill-content.ts +25 -7
- package/src/memory/v2/skill-store.ts +7 -1
- package/src/memory/v3-eval/__tests__/eval-packets.test.ts +248 -0
- package/src/memory/v3-eval/eval-packets.ts +546 -0
- package/src/messaging/providers/slack/adapter.ts +1 -1
- package/src/messaging/providers/slack/api.ts +31 -0
- package/src/messaging/providers/slack/send.test.ts +114 -2
- package/src/messaging/providers/slack/send.ts +30 -7
- package/src/messaging/providers/slack/withdraw.test.ts +200 -0
- package/src/messaging/providers/slack/withdraw.ts +161 -0
- package/src/notifications/AGENTS.md +2 -0
- package/src/notifications/access-request-copy.ts +72 -59
- package/src/notifications/adapters/shared.ts +29 -0
- package/src/notifications/adapters/slack.ts +58 -103
- package/src/notifications/adapters/telegram.ts +2 -20
- package/src/notifications/approval-card-data.ts +333 -0
- package/src/notifications/broadcaster.ts +16 -3
- package/src/notifications/canonical-delivery-recorder.ts +139 -0
- package/src/notifications/copy-composer.ts +3 -3
- package/src/notifications/decision-engine.ts +4 -2
- package/src/notifications/destination-resolver.ts +4 -6
- package/src/notifications/guardian-question-mode.ts +10 -0
- package/src/notifications/home-feed-side-effect.ts +7 -16
- package/src/notifications/notification-utils.ts +19 -20
- package/src/notifications/signal.ts +79 -43
- package/src/notifications/types.ts +98 -121
- package/src/oauth/AGENTS.md +5 -24
- package/src/permissions/checker.test.ts +51 -0
- package/src/permissions/checker.ts +185 -26
- package/src/permissions/ipc-risk-types.ts +24 -0
- package/src/permissions/question-prompter.test.ts +27 -0
- package/src/permissions/question-prompter.ts +4 -0
- package/src/platform/client.test.ts +119 -0
- package/src/platform/client.ts +66 -0
- package/src/platform/consent-cache.test.ts +267 -0
- package/src/platform/consent-cache.ts +174 -0
- package/src/plugin-api/constants.ts +1 -1
- package/src/plugin-api/index.ts +33 -1
- package/src/plugin-api/model-profiles.ts +33 -0
- package/src/plugin-api/types.ts +50 -2
- package/src/plugins/defaults/advisor/__tests__/advisor-gate.test.ts +56 -0
- package/src/plugins/defaults/advisor/__tests__/advisor-state-store.test.ts +43 -0
- package/src/plugins/defaults/advisor/__tests__/agent-loop-integration.test.ts +137 -0
- package/src/plugins/defaults/advisor/__tests__/consult.test.ts +153 -0
- package/src/plugins/defaults/advisor/__tests__/hooks.test.ts +138 -0
- package/src/plugins/defaults/advisor/__tests__/transcript.test.ts +147 -0
- package/src/plugins/defaults/advisor/advisor-gate.ts +29 -0
- package/src/plugins/defaults/advisor/advisor-state-store.ts +94 -0
- package/src/plugins/defaults/advisor/config.ts +21 -0
- package/src/plugins/defaults/advisor/consult.ts +93 -0
- package/src/plugins/defaults/advisor/hooks/post-model-call.ts +34 -0
- package/src/plugins/defaults/advisor/hooks/pre-model-call.ts +30 -0
- package/src/plugins/defaults/advisor/hooks/user-prompt-submit.ts +19 -0
- package/src/plugins/defaults/advisor/package.json +14 -0
- package/src/plugins/defaults/advisor/steering.ts +67 -0
- package/src/plugins/defaults/advisor/tools/advisor.ts +65 -0
- package/src/plugins/defaults/advisor/transcript.ts +76 -0
- package/src/plugins/defaults/index.ts +60 -0
- package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +22 -9
- package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit.ts +2 -2
- package/src/plugins/defaults/memory-retrieval/tail-reinjection-strip.ts +64 -0
- package/src/plugins/defaults/memory-retrieval/unified-turn-context.ts +29 -21
- package/src/plugins/defaults/memory-v3-shadow/__tests__/carry-integration.test.ts +1 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/injection.test.ts +1 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/maintain-job.test.ts +129 -9
- package/src/plugins/defaults/memory-v3-shadow/__tests__/orchestrate.test.ts +31 -4
- package/src/plugins/defaults/memory-v3-shadow/__tests__/selection-log-store.test.ts +77 -2
- package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +1 -0
- package/src/plugins/defaults/memory-v3-shadow/injector.ts +7 -10
- package/src/plugins/defaults/memory-v3-shadow/maintain-job.ts +144 -11
- package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +32 -20
- package/src/plugins/defaults/memory-v3-shadow/selection-log-store.ts +56 -3
- package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +23 -2
- package/src/plugins/defaults/surface-completion-nudge/hooks/post-model-call.ts +276 -0
- package/src/plugins/defaults/surface-completion-nudge/hooks/stop.ts +22 -0
- package/src/plugins/defaults/surface-completion-nudge/nudge-state-store.ts +46 -0
- package/src/plugins/defaults/surface-completion-nudge/package.json +14 -0
- package/src/plugins/defaults/task-progress-nudge/hooks/post-tool-use.ts +3 -13
- package/src/plugins/defaults/title-generate/hooks/stop.ts +56 -21
- package/src/prompts/persona-resolver.ts +14 -4
- package/src/prompts/templates/system-sections.ts +7 -2
- package/src/providers/__tests__/provider-env-vars.test.ts +6 -0
- package/src/providers/__tests__/provider-secret-catalog.test.ts +1 -0
- package/src/providers/__tests__/retry-callsite.test.ts +176 -0
- package/src/providers/atlascloud/client.ts +85 -0
- package/src/providers/fetch-provider-catalog.ts +85 -0
- package/src/providers/inference/adapter-factory.ts +3 -0
- package/src/providers/model-catalog.ts +58 -0
- package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +33 -0
- package/src/providers/openai/chat-completions-provider.ts +7 -0
- package/src/providers/openai/responses-provider.ts +10 -0
- package/src/providers/provider-send-message.ts +11 -3
- package/src/providers/retry.ts +53 -12
- package/src/providers/search-provider-catalog.ts +10 -0
- package/src/providers/weak-open-model.ts +22 -0
- package/src/runtime/AGENTS.md +0 -1
- package/src/runtime/__tests__/agent-wake.test.ts +181 -0
- package/src/runtime/__tests__/client-health.test.ts +44 -0
- package/src/runtime/access-request-helper.ts +21 -53
- package/src/runtime/actor-trust-resolver.ts +59 -63
- package/src/runtime/agent-wake.ts +52 -0
- package/src/runtime/assistant-event-hub.ts +18 -4
- package/src/runtime/auth/__tests__/route-policy.test.ts +12 -0
- package/src/runtime/auth/require-bound-guardian.ts +1 -4
- package/src/runtime/btw-sidechain.ts +3 -6
- package/src/runtime/capabilities.test.ts +120 -0
- package/src/runtime/capabilities.ts +197 -0
- package/src/runtime/channel-approval-types.ts +22 -45
- package/src/runtime/channel-invite-transports/telegram.ts +4 -4
- package/src/runtime/channel-retry-sweep.ts +1 -0
- package/src/runtime/channel-verification-service.ts +3 -3
- package/src/runtime/client-health.ts +26 -0
- package/src/runtime/confirmation-request-guardian-bridge.ts +38 -29
- package/src/runtime/effective-capabilities.test.ts +128 -0
- package/src/runtime/effective-capabilities.ts +84 -0
- package/src/runtime/guardian-reply-router.ts +106 -21
- package/src/runtime/invite-redemption-service.ts +9 -25
- package/src/runtime/migrations/__tests__/vbundle-builder-fd-leak.test.ts +123 -0
- package/src/runtime/migrations/vbundle-builder.ts +49 -20
- package/src/runtime/pending-interactions.ts +15 -0
- package/src/runtime/routes/__tests__/client-routes.test.ts +13 -0
- package/src/runtime/routes/__tests__/conversation-management-routes.test.ts +67 -0
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +240 -1
- package/src/runtime/routes/app-routes.ts +1 -1
- package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +2 -2
- package/src/runtime/routes/assets/vellum-design-system.css +1959 -0
- package/src/runtime/routes/browser-tabs-routes.ts +9 -0
- package/src/runtime/routes/btw-routes.ts +1 -27
- package/src/runtime/routes/canonical-guardian-expiry-sweep.ts +17 -8
- package/src/runtime/routes/client-routes.ts +10 -0
- package/src/runtime/routes/contact-routes.ts +31 -8
- package/src/runtime/routes/conversation-compaction-routes.ts +1 -1
- package/src/runtime/routes/conversation-management-routes.ts +80 -1
- package/src/runtime/routes/conversation-query-routes.ts +68 -22
- package/src/runtime/routes/conversation-routes.ts +39 -14
- package/src/runtime/routes/credential-routes.ts +40 -16
- package/src/runtime/routes/empty-state-greeting-cache.ts +1 -2
- package/src/runtime/routes/events-routes.ts +1 -3
- package/src/runtime/routes/guardian-approval-interception.ts +14 -73
- package/src/runtime/routes/guardian-approval-prompt.ts +22 -4
- package/src/runtime/routes/home-feed-routes.ts +8 -3
- package/src/runtime/routes/identity-routes.ts +1 -296
- package/src/runtime/routes/inbound-message-handler.ts +214 -228
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +89 -7
- package/src/runtime/routes/inbound-stages/admission-policy.test.ts +154 -0
- package/src/runtime/routes/inbound-stages/admission-policy.ts +140 -0
- package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +3 -3
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +11 -6
- package/src/runtime/routes/inbound-stages/escalation-intercept.ts +1 -2
- package/src/runtime/routes/inbound-stages/guardian-activation-intercept.ts +1 -2
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.test.ts +7 -7
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +47 -28
- package/src/runtime/routes/inbound-stages/reaction-intercept.ts +358 -0
- package/src/runtime/routes/index.ts +2 -0
- package/src/runtime/routes/integrations/slack/__tests__/channel.test.ts +8 -0
- package/src/runtime/routes/integrations/slack/channel.ts +36 -0
- package/src/runtime/routes/internal-telemetry-routes.ts +1 -1
- package/src/runtime/routes/mcp-auth-routes.ts +233 -41
- package/src/runtime/routes/memory-eval-routes.ts +87 -0
- package/src/runtime/routes/notification-routes.ts +122 -133
- package/src/runtime/routes/platform-routes.ts +2 -2
- package/src/runtime/routes/plugins-routes.ts +202 -3
- package/src/runtime/routes/schedule-routes.ts +0 -22
- package/src/runtime/routes/secret-routes.ts +10 -0
- package/src/runtime/routes/surface-action-routes.ts +2 -1
- package/src/runtime/routes/tool-call-question-enrichment.test.ts +146 -0
- package/src/runtime/routes/tool-call-question-enrichment.ts +66 -0
- package/src/runtime/routes/workflow-routes.test.ts +229 -44
- package/src/runtime/routes/workflow-routes.ts +131 -29
- package/src/runtime/routes/workspace-greetings.ts +55 -0
- package/src/runtime/sync/resource-sync-events.ts +1 -11
- package/src/runtime/tool-grant-request-helper.ts +18 -16
- package/src/runtime/trust-context-resolver.ts +8 -5
- package/src/schedule/inference-profile.ts +2 -14
- package/src/schedule/schedule-store.ts +1 -1
- package/src/schedule/scheduler-types.ts +5 -1
- package/src/security/__tests__/provider-key-env-fallback.test.ts +6 -0
- package/src/security/secret-patterns.ts +3 -0
- package/src/subagent/manager.ts +17 -4
- package/src/subagent/types.ts +6 -0
- package/src/telemetry/trace-collection-policy.test.ts +28 -0
- package/src/telemetry/trace-collection-policy.ts +30 -0
- package/src/telemetry/types.ts +89 -0
- package/src/telemetry/usage-telemetry-reporter.test.ts +586 -36
- package/src/telemetry/usage-telemetry-reporter.ts +148 -41
- package/src/tools/AGENTS.md +3 -3
- package/src/tools/browser/__tests__/browser-execution-acquire.test.ts +31 -0
- package/src/tools/browser/browser-execution.ts +30 -19
- package/src/tools/document/document-tool.ts +2 -3
- package/src/tools/executor.ts +5 -3
- package/src/tools/host-terminal/host-shell.ts +5 -4
- package/src/tools/memory/register.ts +2 -2
- package/src/tools/network/__tests__/web-fetch-firecrawl.test.ts +360 -0
- package/src/tools/network/__tests__/web-search.test.ts +143 -0
- package/src/tools/network/web-fetch.ts +372 -1
- package/src/tools/network/web-search-error.ts +1 -1
- package/src/tools/network/web-search.ts +213 -10
- package/src/tools/permission-checker.ts +4 -3
- package/src/tools/registry.ts +20 -0
- package/src/tools/schedule/create.ts +7 -12
- package/src/tools/schedule/update.ts +4 -11
- package/src/tools/shared/filesystem/path-policy.ts +39 -13
- package/src/tools/side-effects.ts +2 -17
- package/src/tools/skills/execute.ts +33 -0
- package/src/tools/subagent/spawn.ts +61 -12
- package/src/tools/terminal/shell.ts +10 -4
- package/src/tools/tool-approval-handler.ts +18 -13
- package/src/tools/tool-manifest.ts +0 -2
- package/src/tools/types.ts +9 -0
- package/src/tools/ui-surface/definitions.ts +64 -3
- package/src/tools/verification-control-plane-policy.ts +3 -1
- package/src/tools/workflows/run-workflow.test.ts +8 -18
- package/src/tools/workflows/run-workflow.ts +1 -0
- package/src/util/disk-usage.ts +78 -23
- package/src/util/platform.ts +10 -3
- package/src/watcher/telemetry.ts +2 -2
- package/src/workflows/capabilities.ts +2 -3
- package/src/workflows/engine.test.ts +175 -1
- package/src/workflows/engine.ts +82 -0
- package/src/workflows/journal-store.test.ts +70 -0
- package/src/workflows/journal-store.ts +18 -3
- package/src/workflows/run-manager.test.ts +171 -28
- package/src/workflows/run-manager.ts +66 -24
- package/src/workspace/migrations/105-enable-memory-v3-live-for-new-workspaces.ts +63 -0
- package/src/workspace/migrations/106-drop-collect-usage-data.ts +47 -0
- package/src/workspace/migrations/107-drop-send-diagnostics.ts +47 -0
- package/src/workspace/migrations/108-drop-balanced-economy-profile.ts +129 -0
- package/src/workspace/migrations/registry.ts +8 -0
- package/src/__tests__/app-control-no-global-cgevent.test.ts +0 -98
- package/src/__tests__/credential-security-e2e.test.ts +0 -362
- package/src/__tests__/credential-vault-unit.test.ts +0 -1528
- package/src/__tests__/credential-vault.test.ts +0 -1706
- package/src/__tests__/identity-intro-cache.test.ts +0 -315
- package/src/__tests__/secret-onetime-send.test.ts +0 -182
- package/src/cli/commands/__tests__/task.test.ts +0 -914
- package/src/cli/commands/task.ts +0 -771
- package/src/config/bundled-skills/personal-page/SKILL.md +0 -57
- package/src/config/bundled-skills/personal-page/TOOLS.json +0 -27
- package/src/config/bundled-skills/personal-page/tools/app-refresh.ts +0 -17
- package/src/config/preloaded-apps/personal-page/src/components/About.tsx +0 -22
- package/src/config/preloaded-apps/personal-page/src/components/App.tsx +0 -16
- package/src/config/preloaded-apps/personal-page/src/components/Features.tsx +0 -77
- package/src/config/preloaded-apps/personal-page/src/components/Hero.tsx +0 -57
- package/src/config/preloaded-apps/personal-page/src/components/Pending.tsx +0 -28
- package/src/config/preloaded-apps/personal-page/src/components/animations.tsx +0 -234
- package/src/config/preloaded-apps/personal-page/src/components/icons.tsx +0 -48
- package/src/config/preloaded-apps/personal-page/src/components/media.ts +0 -16
- package/src/config/preloaded-apps/personal-page/src/index.html +0 -20
- package/src/config/preloaded-apps/personal-page/src/main.tsx +0 -7
- package/src/config/preloaded-apps/personal-page/src/profile-data.ts +0 -82
- package/src/config/preloaded-apps/personal-page/src/styles.css +0 -759
- package/src/memory/__tests__/preloaded-apps.test.ts +0 -85
- package/src/memory/preloaded-apps.ts +0 -116
- package/src/notifications/tool-approval-copy.ts +0 -142
- package/src/runtime/routes/approval-prompt-ts-tracker.ts +0 -78
- package/src/runtime/routes/identity-intro-cache.ts +0 -172
- package/src/tools/credentials/vault.ts +0 -712
|
@@ -1,1706 +0,0 @@
|
|
|
1
|
-
import { randomBytes } from "node:crypto";
|
|
2
|
-
import { mkdirSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
-
import { tmpdir } from "node:os";
|
|
4
|
-
import { join } from "node:path";
|
|
5
|
-
import {
|
|
6
|
-
afterAll,
|
|
7
|
-
afterEach,
|
|
8
|
-
beforeAll,
|
|
9
|
-
beforeEach,
|
|
10
|
-
describe,
|
|
11
|
-
expect,
|
|
12
|
-
mock,
|
|
13
|
-
test,
|
|
14
|
-
} from "bun:test";
|
|
15
|
-
|
|
16
|
-
// ---------------------------------------------------------------------------
|
|
17
|
-
// Mock logger
|
|
18
|
-
// ---------------------------------------------------------------------------
|
|
19
|
-
|
|
20
|
-
mock.module("../util/logger.js", () => ({
|
|
21
|
-
getLogger: () =>
|
|
22
|
-
new Proxy({} as Record<string, unknown>, {
|
|
23
|
-
get: () => () => {},
|
|
24
|
-
}),
|
|
25
|
-
}));
|
|
26
|
-
|
|
27
|
-
// ---------------------------------------------------------------------------
|
|
28
|
-
// Use encrypted backend with a temp store path
|
|
29
|
-
// ---------------------------------------------------------------------------
|
|
30
|
-
|
|
31
|
-
import { _resetBackend } from "../security/secure-keys.js";
|
|
32
|
-
import { setStorePathForTesting } from "./encrypted-store-test-helpers.js";
|
|
33
|
-
|
|
34
|
-
const TEST_DIR = join(
|
|
35
|
-
tmpdir(),
|
|
36
|
-
`vellum-credvault-test-${randomBytes(4).toString("hex")}`,
|
|
37
|
-
);
|
|
38
|
-
const STORE_PATH = join(TEST_DIR, "keys.enc");
|
|
39
|
-
|
|
40
|
-
// ---------------------------------------------------------------------------
|
|
41
|
-
// Mock the registry so importing vault.ts doesn't fail on double-registration
|
|
42
|
-
// ---------------------------------------------------------------------------
|
|
43
|
-
|
|
44
|
-
mock.module("../tools/registry.js", () => ({
|
|
45
|
-
registerTool: () => {},
|
|
46
|
-
}));
|
|
47
|
-
|
|
48
|
-
// ---------------------------------------------------------------------------
|
|
49
|
-
// Mock OAuth2 token refresh for token-manager deduplication tests
|
|
50
|
-
// ---------------------------------------------------------------------------
|
|
51
|
-
|
|
52
|
-
let mockRefreshOAuth2Token: ReturnType<
|
|
53
|
-
typeof mock<
|
|
54
|
-
(
|
|
55
|
-
tokenExchangeUrl: string,
|
|
56
|
-
clientId: string,
|
|
57
|
-
refreshToken: string,
|
|
58
|
-
clientSecret?: string,
|
|
59
|
-
tokenEndpointAuthMethod?: string,
|
|
60
|
-
) => Promise<{ accessToken: string; expiresIn: number }>
|
|
61
|
-
>
|
|
62
|
-
>;
|
|
63
|
-
|
|
64
|
-
mock.module("../security/oauth2.js", () => {
|
|
65
|
-
mockRefreshOAuth2Token = mock(() =>
|
|
66
|
-
Promise.resolve({
|
|
67
|
-
accessToken: "refreshed-access-token",
|
|
68
|
-
expiresIn: 3600,
|
|
69
|
-
}),
|
|
70
|
-
);
|
|
71
|
-
return {
|
|
72
|
-
refreshOAuth2Token: mockRefreshOAuth2Token,
|
|
73
|
-
};
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
// ---------------------------------------------------------------------------
|
|
77
|
-
// Mock oauth-store — token-manager reads refresh config from SQLite
|
|
78
|
-
// ---------------------------------------------------------------------------
|
|
79
|
-
|
|
80
|
-
/** Mutable per-test map of provider connections for getConnectionByProvider */
|
|
81
|
-
const mockConnections = new Map<
|
|
82
|
-
string,
|
|
83
|
-
{
|
|
84
|
-
id: string;
|
|
85
|
-
provider: string;
|
|
86
|
-
oauthAppId: string;
|
|
87
|
-
expiresAt: number | null;
|
|
88
|
-
}
|
|
89
|
-
>();
|
|
90
|
-
const mockApps = new Map<
|
|
91
|
-
string,
|
|
92
|
-
{
|
|
93
|
-
id: string;
|
|
94
|
-
provider: string;
|
|
95
|
-
clientId: string;
|
|
96
|
-
clientSecretCredentialPath: string;
|
|
97
|
-
}
|
|
98
|
-
>();
|
|
99
|
-
const mockProviders = new Map<
|
|
100
|
-
string,
|
|
101
|
-
{
|
|
102
|
-
key: string;
|
|
103
|
-
tokenExchangeUrl: string;
|
|
104
|
-
refreshUrl?: string | null;
|
|
105
|
-
tokenEndpointAuthMethod?: string;
|
|
106
|
-
}
|
|
107
|
-
>();
|
|
108
|
-
|
|
109
|
-
let mockDisconnectOAuthProvider: ReturnType<
|
|
110
|
-
typeof mock<
|
|
111
|
-
(provider: string) => Promise<"disconnected" | "not-found" | "error">
|
|
112
|
-
>
|
|
113
|
-
>;
|
|
114
|
-
|
|
115
|
-
mock.module("../oauth/oauth-store.js", () => {
|
|
116
|
-
mockDisconnectOAuthProvider = mock((provider: string) =>
|
|
117
|
-
Promise.resolve(
|
|
118
|
-
mockConnections.has(provider)
|
|
119
|
-
? ("disconnected" as const)
|
|
120
|
-
: ("not-found" as const),
|
|
121
|
-
),
|
|
122
|
-
);
|
|
123
|
-
return {
|
|
124
|
-
disconnectOAuthProvider: mockDisconnectOAuthProvider,
|
|
125
|
-
getConnectionByProvider: (service: string) => mockConnections.get(service),
|
|
126
|
-
getConnection: (id: string) => {
|
|
127
|
-
for (const conn of mockConnections.values()) {
|
|
128
|
-
if (conn.id === id) return conn;
|
|
129
|
-
}
|
|
130
|
-
return undefined;
|
|
131
|
-
},
|
|
132
|
-
getApp: (id: string) => mockApps.get(id),
|
|
133
|
-
getProvider: (key: string) => mockProviders.get(key),
|
|
134
|
-
updateConnection: () => {},
|
|
135
|
-
getMostRecentAppByProvider: () => undefined,
|
|
136
|
-
listConnections: () => [],
|
|
137
|
-
};
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
// ---------------------------------------------------------------------------
|
|
141
|
-
// Import the module under test
|
|
142
|
-
// ---------------------------------------------------------------------------
|
|
143
|
-
|
|
144
|
-
// getCredentialValue is no longer exported (sealed in PR 17) — use getSecureKeyAsync directly
|
|
145
|
-
|
|
146
|
-
import { credentialKey } from "../security/credential-key.js";
|
|
147
|
-
import {
|
|
148
|
-
deleteSecureKeyAsync,
|
|
149
|
-
getSecureKeyAsync,
|
|
150
|
-
setSecureKeyAsync,
|
|
151
|
-
} from "../security/secure-keys.js";
|
|
152
|
-
import {
|
|
153
|
-
_resetInflightRefreshes,
|
|
154
|
-
_resetRefreshBreakers,
|
|
155
|
-
withValidToken,
|
|
156
|
-
} from "../security/token-manager.js";
|
|
157
|
-
import {
|
|
158
|
-
_setMetadataPath,
|
|
159
|
-
getCredentialMetadata,
|
|
160
|
-
} from "../tools/credentials/metadata-store.js";
|
|
161
|
-
import { credentialStoreTool } from "../tools/credentials/vault.js";
|
|
162
|
-
import type { ToolContext } from "../tools/types.js";
|
|
163
|
-
|
|
164
|
-
// Create a minimal context for tool execution
|
|
165
|
-
const _ctx: ToolContext = {
|
|
166
|
-
workingDir: "/tmp",
|
|
167
|
-
conversationId: "test-conv",
|
|
168
|
-
trustClass: "guardian",
|
|
169
|
-
};
|
|
170
|
-
|
|
171
|
-
// We'll manually instantiate the tool for testing
|
|
172
|
-
// by reimporting the class behavior through the tool's execute method.
|
|
173
|
-
// Since the tool registers itself, let's capture it.
|
|
174
|
-
let _capturedTool: {
|
|
175
|
-
execute(
|
|
176
|
-
input: Record<string, unknown>,
|
|
177
|
-
context: ToolContext,
|
|
178
|
-
): Promise<{ content: string; isError: boolean }>;
|
|
179
|
-
};
|
|
180
|
-
|
|
181
|
-
// Re-mock registry to capture the tool
|
|
182
|
-
const { registerTool: _unused, ..._registryRest } =
|
|
183
|
-
await import("../tools/registry.js");
|
|
184
|
-
|
|
185
|
-
// We need to access the actual tool - let's create it directly
|
|
186
|
-
// by re-using the module. Since vault.ts calls registerTool as a side-effect,
|
|
187
|
-
// let's just use the secure-keys functions directly + test getCredentialValue.
|
|
188
|
-
// For the tool execute tests, we'll create a simple wrapper that mimics the tool.
|
|
189
|
-
|
|
190
|
-
async function executeVault(
|
|
191
|
-
input: Record<string, unknown>,
|
|
192
|
-
): Promise<{ content: string; isError: boolean }> {
|
|
193
|
-
const action = input.action as string;
|
|
194
|
-
|
|
195
|
-
switch (action) {
|
|
196
|
-
case "store": {
|
|
197
|
-
const service = input.service as string | undefined;
|
|
198
|
-
const field = input.field as string | undefined;
|
|
199
|
-
const value = input.value as string | undefined;
|
|
200
|
-
|
|
201
|
-
if (!service || typeof service !== "string") {
|
|
202
|
-
return {
|
|
203
|
-
content: "Error: service is required for store action",
|
|
204
|
-
isError: true,
|
|
205
|
-
};
|
|
206
|
-
}
|
|
207
|
-
if (!field || typeof field !== "string") {
|
|
208
|
-
return {
|
|
209
|
-
content: "Error: field is required for store action",
|
|
210
|
-
isError: true,
|
|
211
|
-
};
|
|
212
|
-
}
|
|
213
|
-
if (!value || typeof value !== "string") {
|
|
214
|
-
return {
|
|
215
|
-
content: "Error: value is required for store action",
|
|
216
|
-
isError: true,
|
|
217
|
-
};
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
const key = credentialKey(service, field);
|
|
221
|
-
const ok = await setSecureKeyAsync(key, value);
|
|
222
|
-
if (!ok) {
|
|
223
|
-
return { content: "Error: failed to store credential", isError: true };
|
|
224
|
-
}
|
|
225
|
-
return {
|
|
226
|
-
content: `Stored credential for ${service}/${field}.`,
|
|
227
|
-
isError: false,
|
|
228
|
-
};
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
case "list":
|
|
232
|
-
return credentialStoreTool.execute({ action: "list" }, _ctx);
|
|
233
|
-
|
|
234
|
-
case "delete": {
|
|
235
|
-
const service = input.service as string | undefined;
|
|
236
|
-
const field = input.field as string | undefined;
|
|
237
|
-
|
|
238
|
-
if (!service || typeof service !== "string") {
|
|
239
|
-
return {
|
|
240
|
-
content: "Error: service is required for delete action",
|
|
241
|
-
isError: true,
|
|
242
|
-
};
|
|
243
|
-
}
|
|
244
|
-
if (!field || typeof field !== "string") {
|
|
245
|
-
return {
|
|
246
|
-
content: "Error: field is required for delete action",
|
|
247
|
-
isError: true,
|
|
248
|
-
};
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
const key = credentialKey(service, field);
|
|
252
|
-
const result = await deleteSecureKeyAsync(key);
|
|
253
|
-
if (result !== "deleted") {
|
|
254
|
-
return {
|
|
255
|
-
content: `Error: credential ${service}/${field} not found`,
|
|
256
|
-
isError: true,
|
|
257
|
-
};
|
|
258
|
-
}
|
|
259
|
-
return {
|
|
260
|
-
content: `Deleted credential for ${service}/${field}.`,
|
|
261
|
-
isError: false,
|
|
262
|
-
};
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
default:
|
|
266
|
-
return { content: `Error: unknown action "${action}"`, isError: true };
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
afterAll(() => {
|
|
271
|
-
mock.restore();
|
|
272
|
-
});
|
|
273
|
-
|
|
274
|
-
describe("credential_store tool", () => {
|
|
275
|
-
beforeAll(() => {
|
|
276
|
-
mkdirSync(TEST_DIR, { recursive: true });
|
|
277
|
-
});
|
|
278
|
-
|
|
279
|
-
beforeEach(() => {
|
|
280
|
-
_resetBackend();
|
|
281
|
-
// Clear content files but preserve the directory structure
|
|
282
|
-
for (const entry of readdirSync(TEST_DIR)) {
|
|
283
|
-
rmSync(join(TEST_DIR, entry), { recursive: true, force: true });
|
|
284
|
-
}
|
|
285
|
-
setStorePathForTesting(STORE_PATH);
|
|
286
|
-
_setMetadataPath(join(TEST_DIR, "metadata.json"));
|
|
287
|
-
mockDisconnectOAuthProvider.mockClear();
|
|
288
|
-
mockConnections.clear();
|
|
289
|
-
});
|
|
290
|
-
|
|
291
|
-
afterEach(() => {
|
|
292
|
-
_setMetadataPath(null);
|
|
293
|
-
setStorePathForTesting(null);
|
|
294
|
-
_resetBackend();
|
|
295
|
-
mockConnections.clear();
|
|
296
|
-
});
|
|
297
|
-
|
|
298
|
-
afterAll(() => {
|
|
299
|
-
rmSync(TEST_DIR, { recursive: true, force: true });
|
|
300
|
-
});
|
|
301
|
-
|
|
302
|
-
// -----------------------------------------------------------------------
|
|
303
|
-
// Store
|
|
304
|
-
// -----------------------------------------------------------------------
|
|
305
|
-
describe("store action", () => {
|
|
306
|
-
test("stores a credential and returns confirmation", async () => {
|
|
307
|
-
const result = await executeVault({
|
|
308
|
-
action: "store",
|
|
309
|
-
service: "gmail",
|
|
310
|
-
field: "password",
|
|
311
|
-
value: "super-secret-123",
|
|
312
|
-
});
|
|
313
|
-
expect(result.isError).toBe(false);
|
|
314
|
-
expect(result.content).toBe("Stored credential for gmail/password.");
|
|
315
|
-
});
|
|
316
|
-
|
|
317
|
-
test("stored value NEVER appears in tool output", async () => {
|
|
318
|
-
const testValue = "my-ultra-test-value-xyz";
|
|
319
|
-
const result = await executeVault({
|
|
320
|
-
action: "store",
|
|
321
|
-
service: "github",
|
|
322
|
-
field: "token",
|
|
323
|
-
value: testValue,
|
|
324
|
-
});
|
|
325
|
-
expect(result.content).not.toContain(testValue);
|
|
326
|
-
});
|
|
327
|
-
|
|
328
|
-
test("missing service returns error", async () => {
|
|
329
|
-
const result = await executeVault({
|
|
330
|
-
action: "store",
|
|
331
|
-
field: "password",
|
|
332
|
-
value: "val",
|
|
333
|
-
});
|
|
334
|
-
expect(result.isError).toBe(true);
|
|
335
|
-
expect(result.content).toContain("service is required");
|
|
336
|
-
});
|
|
337
|
-
|
|
338
|
-
test("missing field returns error", async () => {
|
|
339
|
-
const result = await executeVault({
|
|
340
|
-
action: "store",
|
|
341
|
-
service: "gmail",
|
|
342
|
-
value: "val",
|
|
343
|
-
});
|
|
344
|
-
expect(result.isError).toBe(true);
|
|
345
|
-
expect(result.content).toContain("field is required");
|
|
346
|
-
});
|
|
347
|
-
|
|
348
|
-
test("missing value returns error", async () => {
|
|
349
|
-
const result = await executeVault({
|
|
350
|
-
action: "store",
|
|
351
|
-
service: "gmail",
|
|
352
|
-
field: "password",
|
|
353
|
-
});
|
|
354
|
-
expect(result.isError).toBe(true);
|
|
355
|
-
expect(result.content).toContain("value is required");
|
|
356
|
-
});
|
|
357
|
-
|
|
358
|
-
test("store success includes credential_id via credentialStoreTool", async () => {
|
|
359
|
-
const result = await credentialStoreTool.execute(
|
|
360
|
-
{
|
|
361
|
-
action: "store",
|
|
362
|
-
service: "test-cred-id",
|
|
363
|
-
field: "api_key",
|
|
364
|
-
value: "test-value",
|
|
365
|
-
},
|
|
366
|
-
_ctx,
|
|
367
|
-
);
|
|
368
|
-
expect(result.isError).toBe(false);
|
|
369
|
-
expect(result.content).toContain("credential_id:");
|
|
370
|
-
expect(result.content).toContain("test-cred-id/api_key");
|
|
371
|
-
// Verify the credential_id in the output matches the metadata
|
|
372
|
-
const metadata = getCredentialMetadata("test-cred-id", "api_key");
|
|
373
|
-
expect(metadata).toBeDefined();
|
|
374
|
-
expect(result.content).toContain(metadata!.credentialId);
|
|
375
|
-
});
|
|
376
|
-
});
|
|
377
|
-
|
|
378
|
-
// -----------------------------------------------------------------------
|
|
379
|
-
// List
|
|
380
|
-
// -----------------------------------------------------------------------
|
|
381
|
-
describe("list action", () => {
|
|
382
|
-
test("lists stored credentials with credential_id, service, field", async () => {
|
|
383
|
-
await credentialStoreTool.execute(
|
|
384
|
-
{
|
|
385
|
-
action: "store",
|
|
386
|
-
service: "gmail",
|
|
387
|
-
field: "password",
|
|
388
|
-
value: "secret1",
|
|
389
|
-
},
|
|
390
|
-
_ctx,
|
|
391
|
-
);
|
|
392
|
-
await credentialStoreTool.execute(
|
|
393
|
-
{
|
|
394
|
-
action: "store",
|
|
395
|
-
service: "github",
|
|
396
|
-
field: "token",
|
|
397
|
-
value: "secret2",
|
|
398
|
-
},
|
|
399
|
-
_ctx,
|
|
400
|
-
);
|
|
401
|
-
|
|
402
|
-
const result = await credentialStoreTool.execute(
|
|
403
|
-
{ action: "list" },
|
|
404
|
-
_ctx,
|
|
405
|
-
);
|
|
406
|
-
expect(result.isError).toBe(false);
|
|
407
|
-
|
|
408
|
-
const entries = JSON.parse(result.content);
|
|
409
|
-
expect(entries).toHaveLength(2);
|
|
410
|
-
|
|
411
|
-
const services = entries
|
|
412
|
-
.map((e: { service: string }) => e.service)
|
|
413
|
-
.sort();
|
|
414
|
-
expect(services).toEqual(["github", "gmail"]);
|
|
415
|
-
|
|
416
|
-
// Each entry must have credential_id, service, field
|
|
417
|
-
for (const entry of entries) {
|
|
418
|
-
expect(typeof entry.credential_id).toBe("string");
|
|
419
|
-
expect(entry.credential_id.length).toBeGreaterThan(0);
|
|
420
|
-
expect(typeof entry.service).toBe("string");
|
|
421
|
-
expect(typeof entry.field).toBe("string");
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
// Values must NOT appear in the output
|
|
425
|
-
expect(result.content).not.toContain("secret1");
|
|
426
|
-
expect(result.content).not.toContain("secret2");
|
|
427
|
-
});
|
|
428
|
-
|
|
429
|
-
test("list output includes alias when set", async () => {
|
|
430
|
-
await credentialStoreTool.execute(
|
|
431
|
-
{
|
|
432
|
-
action: "store",
|
|
433
|
-
service: "fal",
|
|
434
|
-
field: "api_key",
|
|
435
|
-
value: "fal-secret",
|
|
436
|
-
alias: "fal-primary",
|
|
437
|
-
},
|
|
438
|
-
_ctx,
|
|
439
|
-
);
|
|
440
|
-
|
|
441
|
-
const result = await credentialStoreTool.execute(
|
|
442
|
-
{ action: "list" },
|
|
443
|
-
_ctx,
|
|
444
|
-
);
|
|
445
|
-
const entries = JSON.parse(result.content);
|
|
446
|
-
const entry = entries.find(
|
|
447
|
-
(e: { service: string }) => e.service === "fal",
|
|
448
|
-
);
|
|
449
|
-
expect(entry).toBeDefined();
|
|
450
|
-
expect(entry.alias).toBe("fal-primary");
|
|
451
|
-
});
|
|
452
|
-
|
|
453
|
-
test("list output includes template summary with host patterns", async () => {
|
|
454
|
-
await credentialStoreTool.execute(
|
|
455
|
-
{
|
|
456
|
-
action: "store",
|
|
457
|
-
service: "fal",
|
|
458
|
-
field: "api_key",
|
|
459
|
-
value: "fal-secret",
|
|
460
|
-
injection_templates: [
|
|
461
|
-
{
|
|
462
|
-
hostPattern: "*.fal.ai",
|
|
463
|
-
injectionType: "header",
|
|
464
|
-
headerName: "Authorization",
|
|
465
|
-
valuePrefix: "Key ",
|
|
466
|
-
},
|
|
467
|
-
{
|
|
468
|
-
hostPattern: "gateway.fal.ai",
|
|
469
|
-
injectionType: "header",
|
|
470
|
-
headerName: "X-Key",
|
|
471
|
-
},
|
|
472
|
-
],
|
|
473
|
-
},
|
|
474
|
-
_ctx,
|
|
475
|
-
);
|
|
476
|
-
|
|
477
|
-
const result = await credentialStoreTool.execute(
|
|
478
|
-
{ action: "list" },
|
|
479
|
-
_ctx,
|
|
480
|
-
);
|
|
481
|
-
const entries = JSON.parse(result.content);
|
|
482
|
-
const entry = entries.find(
|
|
483
|
-
(e: { service: string }) => e.service === "fal",
|
|
484
|
-
);
|
|
485
|
-
expect(entry).toBeDefined();
|
|
486
|
-
expect(entry.injection_templates).toBeDefined();
|
|
487
|
-
expect(entry.injection_templates.count).toBe(2);
|
|
488
|
-
expect(entry.injection_templates.host_patterns).toEqual([
|
|
489
|
-
"*.fal.ai",
|
|
490
|
-
"gateway.fal.ai",
|
|
491
|
-
]);
|
|
492
|
-
});
|
|
493
|
-
|
|
494
|
-
test("list does not include credential values", async () => {
|
|
495
|
-
const testValue = "test-dummy-value-for-list";
|
|
496
|
-
await credentialStoreTool.execute(
|
|
497
|
-
{
|
|
498
|
-
action: "store",
|
|
499
|
-
service: "test",
|
|
500
|
-
field: "key",
|
|
501
|
-
value: testValue,
|
|
502
|
-
},
|
|
503
|
-
_ctx,
|
|
504
|
-
);
|
|
505
|
-
|
|
506
|
-
const result = await credentialStoreTool.execute(
|
|
507
|
-
{ action: "list" },
|
|
508
|
-
_ctx,
|
|
509
|
-
);
|
|
510
|
-
expect(result.content).not.toContain(testValue);
|
|
511
|
-
// Also verify no allowedTools/allowedDomains leak into list output
|
|
512
|
-
const entries = JSON.parse(result.content);
|
|
513
|
-
for (const entry of entries) {
|
|
514
|
-
expect(entry.allowedTools).toBeUndefined();
|
|
515
|
-
expect(entry.allowedDomains).toBeUndefined();
|
|
516
|
-
expect(entry.usageDescription).toBeUndefined();
|
|
517
|
-
expect(entry.value).toBeUndefined();
|
|
518
|
-
}
|
|
519
|
-
});
|
|
520
|
-
|
|
521
|
-
test("returns empty array when no credentials exist", async () => {
|
|
522
|
-
const result = await credentialStoreTool.execute(
|
|
523
|
-
{ action: "list" },
|
|
524
|
-
_ctx,
|
|
525
|
-
);
|
|
526
|
-
expect(result.isError).toBe(false);
|
|
527
|
-
expect(JSON.parse(result.content)).toEqual([]);
|
|
528
|
-
});
|
|
529
|
-
|
|
530
|
-
test("lists multiple credentials", async () => {
|
|
531
|
-
await credentialStoreTool.execute(
|
|
532
|
-
{
|
|
533
|
-
action: "store",
|
|
534
|
-
service: "gmail",
|
|
535
|
-
field: "password",
|
|
536
|
-
value: "s1",
|
|
537
|
-
},
|
|
538
|
-
_ctx,
|
|
539
|
-
);
|
|
540
|
-
await credentialStoreTool.execute(
|
|
541
|
-
{
|
|
542
|
-
action: "store",
|
|
543
|
-
service: "github",
|
|
544
|
-
field: "token",
|
|
545
|
-
value: "s2",
|
|
546
|
-
alias: "gh-main",
|
|
547
|
-
},
|
|
548
|
-
_ctx,
|
|
549
|
-
);
|
|
550
|
-
await credentialStoreTool.execute(
|
|
551
|
-
{
|
|
552
|
-
action: "store",
|
|
553
|
-
service: "fal",
|
|
554
|
-
field: "api_key",
|
|
555
|
-
value: "s3",
|
|
556
|
-
alias: "fal-primary",
|
|
557
|
-
injection_templates: [
|
|
558
|
-
{
|
|
559
|
-
hostPattern: "*.fal.ai",
|
|
560
|
-
injectionType: "header",
|
|
561
|
-
headerName: "Authorization",
|
|
562
|
-
},
|
|
563
|
-
],
|
|
564
|
-
},
|
|
565
|
-
_ctx,
|
|
566
|
-
);
|
|
567
|
-
|
|
568
|
-
const result = await credentialStoreTool.execute(
|
|
569
|
-
{ action: "list" },
|
|
570
|
-
_ctx,
|
|
571
|
-
);
|
|
572
|
-
const entries = JSON.parse(result.content);
|
|
573
|
-
expect(entries).toHaveLength(3);
|
|
574
|
-
|
|
575
|
-
const fal = entries.find((e: { service: string }) => e.service === "fal");
|
|
576
|
-
expect(fal.alias).toBe("fal-primary");
|
|
577
|
-
expect(fal.injection_templates.count).toBe(1);
|
|
578
|
-
|
|
579
|
-
const gh = entries.find(
|
|
580
|
-
(e: { service: string }) => e.service === "github",
|
|
581
|
-
);
|
|
582
|
-
expect(gh.alias).toBe("gh-main");
|
|
583
|
-
expect(gh.injection_templates).toBeUndefined();
|
|
584
|
-
|
|
585
|
-
const gmail = entries.find(
|
|
586
|
-
(e: { service: string }) => e.service === "gmail",
|
|
587
|
-
);
|
|
588
|
-
expect(gmail.alias).toBeUndefined();
|
|
589
|
-
expect(gmail.injection_templates).toBeUndefined();
|
|
590
|
-
});
|
|
591
|
-
|
|
592
|
-
test("works with metadata store fallback when listing secrets", async () => {
|
|
593
|
-
// Store a credential first (on encrypted backend)
|
|
594
|
-
await credentialStoreTool.execute(
|
|
595
|
-
{
|
|
596
|
-
action: "store",
|
|
597
|
-
service: "keychain-test",
|
|
598
|
-
field: "token",
|
|
599
|
-
value: "kc-secret",
|
|
600
|
-
},
|
|
601
|
-
_ctx,
|
|
602
|
-
);
|
|
603
|
-
|
|
604
|
-
const result = await credentialStoreTool.execute(
|
|
605
|
-
{ action: "list" },
|
|
606
|
-
_ctx,
|
|
607
|
-
);
|
|
608
|
-
expect(result.isError).toBe(false);
|
|
609
|
-
const entries = JSON.parse(result.content);
|
|
610
|
-
expect(entries).toHaveLength(1);
|
|
611
|
-
expect(entries[0].service).toBe("keychain-test");
|
|
612
|
-
expect(entries[0].field).toBe("token");
|
|
613
|
-
expect(typeof entries[0].credential_id).toBe("string");
|
|
614
|
-
});
|
|
615
|
-
|
|
616
|
-
test("returns error when metadata file has unrecognized version", async () => {
|
|
617
|
-
// Write a metadata file with a future version that the current code cannot handle
|
|
618
|
-
const metadataPath = join(TEST_DIR, "metadata.json");
|
|
619
|
-
writeFileSync(
|
|
620
|
-
metadataPath,
|
|
621
|
-
JSON.stringify({ version: 999, credentials: [] }),
|
|
622
|
-
"utf-8",
|
|
623
|
-
);
|
|
624
|
-
|
|
625
|
-
const result = await credentialStoreTool.execute(
|
|
626
|
-
{ action: "list" },
|
|
627
|
-
_ctx,
|
|
628
|
-
);
|
|
629
|
-
expect(result.isError).toBe(true);
|
|
630
|
-
expect(result.content).toContain("unrecognized version");
|
|
631
|
-
});
|
|
632
|
-
|
|
633
|
-
test("excludes metadata entries whose secret was deleted from secure storage", async () => {
|
|
634
|
-
// Store two credentials so both metadata and secrets exist
|
|
635
|
-
await credentialStoreTool.execute(
|
|
636
|
-
{
|
|
637
|
-
action: "store",
|
|
638
|
-
service: "svc-a",
|
|
639
|
-
field: "key",
|
|
640
|
-
value: "val-a",
|
|
641
|
-
},
|
|
642
|
-
_ctx,
|
|
643
|
-
);
|
|
644
|
-
await credentialStoreTool.execute(
|
|
645
|
-
{
|
|
646
|
-
action: "store",
|
|
647
|
-
service: "svc-b",
|
|
648
|
-
field: "key",
|
|
649
|
-
value: "val-b",
|
|
650
|
-
},
|
|
651
|
-
_ctx,
|
|
652
|
-
);
|
|
653
|
-
|
|
654
|
-
// Delete the secret directly without going through the tool (simulates
|
|
655
|
-
// a divergence where metadata write failed after secret deletion)
|
|
656
|
-
await deleteSecureKeyAsync(credentialKey("svc-a", "key"));
|
|
657
|
-
|
|
658
|
-
const result = await credentialStoreTool.execute(
|
|
659
|
-
{ action: "list" },
|
|
660
|
-
_ctx,
|
|
661
|
-
);
|
|
662
|
-
expect(result.isError).toBe(false);
|
|
663
|
-
const entries = JSON.parse(result.content);
|
|
664
|
-
// svc-a's secret is gone, so it should be excluded even though metadata exists
|
|
665
|
-
expect(entries).toHaveLength(1);
|
|
666
|
-
expect(entries[0].service).toBe("svc-b");
|
|
667
|
-
});
|
|
668
|
-
|
|
669
|
-
test("recovers from corrupt secure storage by resetting and returning empty list", async () => {
|
|
670
|
-
// Store a credential so metadata exists
|
|
671
|
-
await credentialStoreTool.execute(
|
|
672
|
-
{
|
|
673
|
-
action: "store",
|
|
674
|
-
service: "svc-x",
|
|
675
|
-
field: "key",
|
|
676
|
-
value: "val-x",
|
|
677
|
-
},
|
|
678
|
-
_ctx,
|
|
679
|
-
);
|
|
680
|
-
|
|
681
|
-
// Corrupt the encrypted store file — the store auto-recovers by
|
|
682
|
-
// backing up the corrupt file and creating a fresh store
|
|
683
|
-
writeFileSync(STORE_PATH, "not-valid-json!!!", "utf-8");
|
|
684
|
-
|
|
685
|
-
const result = await credentialStoreTool.execute(
|
|
686
|
-
{ action: "list" },
|
|
687
|
-
_ctx,
|
|
688
|
-
);
|
|
689
|
-
// Store auto-recovers: list succeeds but the corrupted credentials are lost
|
|
690
|
-
expect(result.isError).toBe(false);
|
|
691
|
-
});
|
|
692
|
-
});
|
|
693
|
-
|
|
694
|
-
// -----------------------------------------------------------------------
|
|
695
|
-
// Delete
|
|
696
|
-
// -----------------------------------------------------------------------
|
|
697
|
-
describe("delete action", () => {
|
|
698
|
-
test("deletes a stored credential", async () => {
|
|
699
|
-
await setSecureKeyAsync(credentialKey("gmail", "password"), "secret");
|
|
700
|
-
|
|
701
|
-
const result = await executeVault({
|
|
702
|
-
action: "delete",
|
|
703
|
-
service: "gmail",
|
|
704
|
-
field: "password",
|
|
705
|
-
});
|
|
706
|
-
expect(result.isError).toBe(false);
|
|
707
|
-
expect(result.content).toBe("Deleted credential for gmail/password.");
|
|
708
|
-
|
|
709
|
-
// Verify it's actually gone
|
|
710
|
-
expect(
|
|
711
|
-
await getSecureKeyAsync(credentialKey("gmail", "password")),
|
|
712
|
-
).toBeUndefined();
|
|
713
|
-
});
|
|
714
|
-
|
|
715
|
-
test("returns error for non-existent credential", async () => {
|
|
716
|
-
const result = await executeVault({
|
|
717
|
-
action: "delete",
|
|
718
|
-
service: "nonexistent",
|
|
719
|
-
field: "field",
|
|
720
|
-
});
|
|
721
|
-
expect(result.isError).toBe(true);
|
|
722
|
-
expect(result.content).toContain("not found");
|
|
723
|
-
});
|
|
724
|
-
|
|
725
|
-
test("missing service returns error", async () => {
|
|
726
|
-
const result = await executeVault({
|
|
727
|
-
action: "delete",
|
|
728
|
-
field: "password",
|
|
729
|
-
});
|
|
730
|
-
expect(result.isError).toBe(true);
|
|
731
|
-
expect(result.content).toContain("service is required");
|
|
732
|
-
});
|
|
733
|
-
|
|
734
|
-
test("missing field returns error", async () => {
|
|
735
|
-
const result = await executeVault({
|
|
736
|
-
action: "delete",
|
|
737
|
-
service: "gmail",
|
|
738
|
-
});
|
|
739
|
-
expect(result.isError).toBe(true);
|
|
740
|
-
expect(result.content).toContain("field is required");
|
|
741
|
-
});
|
|
742
|
-
|
|
743
|
-
test("delete also disconnects OAuth connection for the service", async () => {
|
|
744
|
-
// Store a credential via the real tool so metadata exists
|
|
745
|
-
await credentialStoreTool.execute(
|
|
746
|
-
{
|
|
747
|
-
action: "store",
|
|
748
|
-
service: "google",
|
|
749
|
-
field: "api_key",
|
|
750
|
-
value: "test-value",
|
|
751
|
-
},
|
|
752
|
-
_ctx,
|
|
753
|
-
);
|
|
754
|
-
|
|
755
|
-
// Simulate an active OAuth connection for this service
|
|
756
|
-
mockConnections.set("google", {
|
|
757
|
-
id: "conn-gmail",
|
|
758
|
-
provider: "google",
|
|
759
|
-
oauthAppId: "app-gmail",
|
|
760
|
-
expiresAt: Date.now() + 3600_000,
|
|
761
|
-
});
|
|
762
|
-
|
|
763
|
-
const result = await credentialStoreTool.execute(
|
|
764
|
-
{
|
|
765
|
-
action: "delete",
|
|
766
|
-
service: "google",
|
|
767
|
-
field: "api_key",
|
|
768
|
-
},
|
|
769
|
-
_ctx,
|
|
770
|
-
);
|
|
771
|
-
|
|
772
|
-
expect(result.isError).toBe(false);
|
|
773
|
-
expect(result.content).toContain("Deleted credential");
|
|
774
|
-
// Verify disconnectOAuthProvider was called with the service name
|
|
775
|
-
expect(mockDisconnectOAuthProvider).toHaveBeenCalledTimes(1);
|
|
776
|
-
expect(mockDisconnectOAuthProvider).toHaveBeenCalledWith("google");
|
|
777
|
-
});
|
|
778
|
-
});
|
|
779
|
-
|
|
780
|
-
// -----------------------------------------------------------------------
|
|
781
|
-
// Credential value access (sealed — only via secure-keys internally)
|
|
782
|
-
// -----------------------------------------------------------------------
|
|
783
|
-
describe("credential value access", () => {
|
|
784
|
-
test("credential values are stored via secure keys", async () => {
|
|
785
|
-
await setSecureKeyAsync(credentialKey("github", "token"), "ghp_abc123");
|
|
786
|
-
expect(await getSecureKeyAsync(credentialKey("github", "token"))).toBe(
|
|
787
|
-
"ghp_abc123",
|
|
788
|
-
);
|
|
789
|
-
});
|
|
790
|
-
|
|
791
|
-
test("returns undefined for non-existent credential", async () => {
|
|
792
|
-
expect(
|
|
793
|
-
await getSecureKeyAsync(credentialKey("nonexistent", "field")),
|
|
794
|
-
).toBeUndefined();
|
|
795
|
-
});
|
|
796
|
-
});
|
|
797
|
-
|
|
798
|
-
// -----------------------------------------------------------------------
|
|
799
|
-
// Hardening verification — getCredentialValue is no longer exported
|
|
800
|
-
// -----------------------------------------------------------------------
|
|
801
|
-
describe("hardening verification", () => {
|
|
802
|
-
test("vault module does not export getCredentialValue", async () => {
|
|
803
|
-
const vaultModule = await import("../tools/credentials/vault.js");
|
|
804
|
-
expect("getCredentialValue" in vaultModule).toBe(false);
|
|
805
|
-
});
|
|
806
|
-
|
|
807
|
-
test("store with policy fields persists metadata", async () => {
|
|
808
|
-
const result = await credentialStoreTool.execute(
|
|
809
|
-
{
|
|
810
|
-
action: "store",
|
|
811
|
-
service: "github",
|
|
812
|
-
field: "token",
|
|
813
|
-
value: "ghp_secret",
|
|
814
|
-
allowed_tools: ["browser_fill_credential"],
|
|
815
|
-
allowed_domains: ["github.com"],
|
|
816
|
-
usage_description: "GitHub login",
|
|
817
|
-
},
|
|
818
|
-
_ctx,
|
|
819
|
-
);
|
|
820
|
-
expect(result.isError).toBe(false);
|
|
821
|
-
const metadata = getCredentialMetadata("github", "token");
|
|
822
|
-
expect(metadata).toBeDefined();
|
|
823
|
-
expect(metadata!.allowedTools).toEqual(["browser_fill_credential"]);
|
|
824
|
-
expect(metadata!.allowedDomains).toEqual(["github.com"]);
|
|
825
|
-
expect(metadata!.usageDescription).toBe("GitHub login");
|
|
826
|
-
});
|
|
827
|
-
|
|
828
|
-
test("store without policy fields defaults to empty arrays", async () => {
|
|
829
|
-
const result = await credentialStoreTool.execute(
|
|
830
|
-
{
|
|
831
|
-
action: "store",
|
|
832
|
-
service: "slack",
|
|
833
|
-
field: "token",
|
|
834
|
-
value: "xoxb-secret",
|
|
835
|
-
},
|
|
836
|
-
_ctx,
|
|
837
|
-
);
|
|
838
|
-
expect(result.isError).toBe(false);
|
|
839
|
-
const metadata = getCredentialMetadata("slack", "token");
|
|
840
|
-
expect(metadata).toBeDefined();
|
|
841
|
-
expect(metadata!.allowedTools).toEqual([]);
|
|
842
|
-
expect(metadata!.allowedDomains).toEqual([]);
|
|
843
|
-
});
|
|
844
|
-
|
|
845
|
-
test("store rotation without policy fields preserves the existing policy", async () => {
|
|
846
|
-
/**
|
|
847
|
-
* Re-storing (rotating) a credential without policy fields must keep the
|
|
848
|
-
* policy it was originally stored with, rather than resetting it to a
|
|
849
|
-
* deny-all empty policy.
|
|
850
|
-
*/
|
|
851
|
-
// GIVEN a credential stored with an allowed-tools/-domains policy
|
|
852
|
-
await credentialStoreTool.execute(
|
|
853
|
-
{
|
|
854
|
-
action: "store",
|
|
855
|
-
service: "rotation-svc",
|
|
856
|
-
field: "token",
|
|
857
|
-
value: "v1",
|
|
858
|
-
allowed_tools: ["browser_fill_credential"],
|
|
859
|
-
allowed_domains: ["example.com"],
|
|
860
|
-
},
|
|
861
|
-
_ctx,
|
|
862
|
-
);
|
|
863
|
-
|
|
864
|
-
// WHEN it is re-stored with a new value but no policy fields
|
|
865
|
-
const result = await credentialStoreTool.execute(
|
|
866
|
-
{
|
|
867
|
-
action: "store",
|
|
868
|
-
service: "rotation-svc",
|
|
869
|
-
field: "token",
|
|
870
|
-
value: "v2",
|
|
871
|
-
},
|
|
872
|
-
_ctx,
|
|
873
|
-
);
|
|
874
|
-
|
|
875
|
-
// THEN the original policy is preserved
|
|
876
|
-
expect(result.isError).toBe(false);
|
|
877
|
-
const metadata = getCredentialMetadata("rotation-svc", "token");
|
|
878
|
-
expect(metadata!.allowedTools).toEqual(["browser_fill_credential"]);
|
|
879
|
-
expect(metadata!.allowedDomains).toEqual(["example.com"]);
|
|
880
|
-
});
|
|
881
|
-
|
|
882
|
-
test("store rejects invalid policy input", async () => {
|
|
883
|
-
const result = await credentialStoreTool.execute(
|
|
884
|
-
{
|
|
885
|
-
action: "store",
|
|
886
|
-
service: "test",
|
|
887
|
-
field: "token",
|
|
888
|
-
value: "val",
|
|
889
|
-
allowed_tools: "not-an-array",
|
|
890
|
-
},
|
|
891
|
-
_ctx,
|
|
892
|
-
);
|
|
893
|
-
expect(result.isError).toBe(true);
|
|
894
|
-
expect(result.content).toContain("allowed_tools must be an array");
|
|
895
|
-
});
|
|
896
|
-
|
|
897
|
-
test("list action entries do not expose policy metadata", async () => {
|
|
898
|
-
await credentialStoreTool.execute(
|
|
899
|
-
{
|
|
900
|
-
action: "store",
|
|
901
|
-
service: "myservice",
|
|
902
|
-
field: "myfield",
|
|
903
|
-
value: "secret-val",
|
|
904
|
-
allowed_tools: ["browser_fill_credential"],
|
|
905
|
-
allowed_domains: ["example.com"],
|
|
906
|
-
usage_description: "Test usage",
|
|
907
|
-
},
|
|
908
|
-
_ctx,
|
|
909
|
-
);
|
|
910
|
-
|
|
911
|
-
const result = await credentialStoreTool.execute(
|
|
912
|
-
{ action: "list" },
|
|
913
|
-
_ctx,
|
|
914
|
-
);
|
|
915
|
-
const entries = JSON.parse(result.content);
|
|
916
|
-
const entry = entries.find(
|
|
917
|
-
(e: { service: string; field: string }) =>
|
|
918
|
-
e.service === "myservice" && e.field === "myfield",
|
|
919
|
-
);
|
|
920
|
-
expect(entry).toBeDefined();
|
|
921
|
-
// List entries expose credential_id, service, field (and optionally alias,
|
|
922
|
-
// injection_templates) — never policy details.
|
|
923
|
-
expect(entry.allowedTools).toBeUndefined();
|
|
924
|
-
expect(entry.allowedDomains).toBeUndefined();
|
|
925
|
-
expect(entry.usageDescription).toBeUndefined();
|
|
926
|
-
expect(entry.createdAt).toBeUndefined();
|
|
927
|
-
expect(entry.updatedAt).toBeUndefined();
|
|
928
|
-
});
|
|
929
|
-
});
|
|
930
|
-
|
|
931
|
-
// -----------------------------------------------------------------------
|
|
932
|
-
// Alias and injection template fields
|
|
933
|
-
// -----------------------------------------------------------------------
|
|
934
|
-
describe("alias and injection template fields", () => {
|
|
935
|
-
test("store with valid alias and templates persists metadata", async () => {
|
|
936
|
-
const result = await credentialStoreTool.execute(
|
|
937
|
-
{
|
|
938
|
-
action: "store",
|
|
939
|
-
service: "fal",
|
|
940
|
-
field: "api_key",
|
|
941
|
-
value: "fal-key-123",
|
|
942
|
-
alias: "fal-primary",
|
|
943
|
-
injection_templates: [
|
|
944
|
-
{
|
|
945
|
-
hostPattern: "*.fal.ai",
|
|
946
|
-
injectionType: "header",
|
|
947
|
-
headerName: "Authorization",
|
|
948
|
-
valuePrefix: "Key ",
|
|
949
|
-
},
|
|
950
|
-
],
|
|
951
|
-
},
|
|
952
|
-
_ctx,
|
|
953
|
-
);
|
|
954
|
-
expect(result.isError).toBe(false);
|
|
955
|
-
const metadata = getCredentialMetadata("fal", "api_key");
|
|
956
|
-
expect(metadata).toBeDefined();
|
|
957
|
-
expect(metadata!.alias).toBe("fal-primary");
|
|
958
|
-
expect(metadata!.injectionTemplates).toHaveLength(1);
|
|
959
|
-
expect(metadata!.injectionTemplates![0].hostPattern).toBe("*.fal.ai");
|
|
960
|
-
expect(metadata!.injectionTemplates![0].injectionType).toBe("header");
|
|
961
|
-
expect(metadata!.injectionTemplates![0].headerName).toBe("Authorization");
|
|
962
|
-
expect(metadata!.injectionTemplates![0].valuePrefix).toBe("Key ");
|
|
963
|
-
});
|
|
964
|
-
|
|
965
|
-
test("store with alias only (no templates)", async () => {
|
|
966
|
-
const result = await credentialStoreTool.execute(
|
|
967
|
-
{
|
|
968
|
-
action: "store",
|
|
969
|
-
service: "openai",
|
|
970
|
-
field: "api_key",
|
|
971
|
-
value: "sk-test",
|
|
972
|
-
alias: "openai-main",
|
|
973
|
-
},
|
|
974
|
-
_ctx,
|
|
975
|
-
);
|
|
976
|
-
expect(result.isError).toBe(false);
|
|
977
|
-
const metadata = getCredentialMetadata("openai", "api_key");
|
|
978
|
-
expect(metadata).toBeDefined();
|
|
979
|
-
expect(metadata!.alias).toBe("openai-main");
|
|
980
|
-
expect(metadata!.injectionTemplates).toBeUndefined();
|
|
981
|
-
});
|
|
982
|
-
|
|
983
|
-
test("store with templates only (no alias)", async () => {
|
|
984
|
-
const result = await credentialStoreTool.execute(
|
|
985
|
-
{
|
|
986
|
-
action: "store",
|
|
987
|
-
service: "replicate",
|
|
988
|
-
field: "token",
|
|
989
|
-
value: "r8_test",
|
|
990
|
-
injection_templates: [
|
|
991
|
-
{
|
|
992
|
-
hostPattern: "api.replicate.com",
|
|
993
|
-
injectionType: "header",
|
|
994
|
-
headerName: "Authorization",
|
|
995
|
-
valuePrefix: "Bearer ",
|
|
996
|
-
},
|
|
997
|
-
],
|
|
998
|
-
},
|
|
999
|
-
_ctx,
|
|
1000
|
-
);
|
|
1001
|
-
expect(result.isError).toBe(false);
|
|
1002
|
-
const metadata = getCredentialMetadata("replicate", "token");
|
|
1003
|
-
expect(metadata).toBeDefined();
|
|
1004
|
-
expect(metadata!.alias).toBeUndefined();
|
|
1005
|
-
expect(metadata!.injectionTemplates).toHaveLength(1);
|
|
1006
|
-
expect(metadata!.injectionTemplates![0].injectionType).toBe("header");
|
|
1007
|
-
});
|
|
1008
|
-
|
|
1009
|
-
test("rejects template missing headerName for header type", async () => {
|
|
1010
|
-
const result = await credentialStoreTool.execute(
|
|
1011
|
-
{
|
|
1012
|
-
action: "store",
|
|
1013
|
-
service: "fal",
|
|
1014
|
-
field: "api_key",
|
|
1015
|
-
value: "fal-key-123",
|
|
1016
|
-
injection_templates: [
|
|
1017
|
-
{
|
|
1018
|
-
hostPattern: "*.fal.ai",
|
|
1019
|
-
injectionType: "header",
|
|
1020
|
-
// missing headerName
|
|
1021
|
-
},
|
|
1022
|
-
],
|
|
1023
|
-
},
|
|
1024
|
-
_ctx,
|
|
1025
|
-
);
|
|
1026
|
-
expect(result.isError).toBe(true);
|
|
1027
|
-
expect(result.content).toContain("headerName is required");
|
|
1028
|
-
});
|
|
1029
|
-
|
|
1030
|
-
test("rejects template missing queryParamName for query type", async () => {
|
|
1031
|
-
const result = await credentialStoreTool.execute(
|
|
1032
|
-
{
|
|
1033
|
-
action: "store",
|
|
1034
|
-
service: "mapbox",
|
|
1035
|
-
field: "token",
|
|
1036
|
-
value: "pk.test",
|
|
1037
|
-
injection_templates: [
|
|
1038
|
-
{
|
|
1039
|
-
hostPattern: "api.mapbox.com",
|
|
1040
|
-
injectionType: "query",
|
|
1041
|
-
// missing queryParamName
|
|
1042
|
-
},
|
|
1043
|
-
],
|
|
1044
|
-
},
|
|
1045
|
-
_ctx,
|
|
1046
|
-
);
|
|
1047
|
-
expect(result.isError).toBe(true);
|
|
1048
|
-
expect(result.content).toContain("queryParamName is required");
|
|
1049
|
-
});
|
|
1050
|
-
|
|
1051
|
-
test("round-trip: store then list shows the credential", async () => {
|
|
1052
|
-
await credentialStoreTool.execute(
|
|
1053
|
-
{
|
|
1054
|
-
action: "store",
|
|
1055
|
-
service: "anthropic",
|
|
1056
|
-
field: "api_key",
|
|
1057
|
-
value: "sk-ant-test",
|
|
1058
|
-
alias: "claude-key",
|
|
1059
|
-
injection_templates: [
|
|
1060
|
-
{
|
|
1061
|
-
hostPattern: "api.anthropic.com",
|
|
1062
|
-
injectionType: "header",
|
|
1063
|
-
headerName: "x-api-key",
|
|
1064
|
-
},
|
|
1065
|
-
],
|
|
1066
|
-
},
|
|
1067
|
-
_ctx,
|
|
1068
|
-
);
|
|
1069
|
-
|
|
1070
|
-
const listResult = await credentialStoreTool.execute(
|
|
1071
|
-
{ action: "list" },
|
|
1072
|
-
_ctx,
|
|
1073
|
-
);
|
|
1074
|
-
expect(listResult.isError).toBe(false);
|
|
1075
|
-
const entries = JSON.parse(listResult.content);
|
|
1076
|
-
const entry = entries.find(
|
|
1077
|
-
(e: { service: string; field: string }) =>
|
|
1078
|
-
e.service === "anthropic" && e.field === "api_key",
|
|
1079
|
-
);
|
|
1080
|
-
expect(entry).toBeDefined();
|
|
1081
|
-
|
|
1082
|
-
// Verify metadata persisted correctly
|
|
1083
|
-
const metadata = getCredentialMetadata("anthropic", "api_key");
|
|
1084
|
-
expect(metadata).toBeDefined();
|
|
1085
|
-
expect(metadata!.alias).toBe("claude-key");
|
|
1086
|
-
expect(metadata!.injectionTemplates).toHaveLength(1);
|
|
1087
|
-
});
|
|
1088
|
-
|
|
1089
|
-
test("update alias on existing credential", async () => {
|
|
1090
|
-
await credentialStoreTool.execute(
|
|
1091
|
-
{
|
|
1092
|
-
action: "store",
|
|
1093
|
-
service: "fal",
|
|
1094
|
-
field: "api_key",
|
|
1095
|
-
value: "fal-key-123",
|
|
1096
|
-
alias: "fal-old",
|
|
1097
|
-
},
|
|
1098
|
-
_ctx,
|
|
1099
|
-
);
|
|
1100
|
-
|
|
1101
|
-
let metadata = getCredentialMetadata("fal", "api_key");
|
|
1102
|
-
expect(metadata!.alias).toBe("fal-old");
|
|
1103
|
-
|
|
1104
|
-
// Re-store same credential with updated alias
|
|
1105
|
-
await credentialStoreTool.execute(
|
|
1106
|
-
{
|
|
1107
|
-
action: "store",
|
|
1108
|
-
service: "fal",
|
|
1109
|
-
field: "api_key",
|
|
1110
|
-
value: "fal-key-123",
|
|
1111
|
-
alias: "fal-new",
|
|
1112
|
-
},
|
|
1113
|
-
_ctx,
|
|
1114
|
-
);
|
|
1115
|
-
|
|
1116
|
-
metadata = getCredentialMetadata("fal", "api_key");
|
|
1117
|
-
expect(metadata!.alias).toBe("fal-new");
|
|
1118
|
-
});
|
|
1119
|
-
|
|
1120
|
-
test("store with query injection template", async () => {
|
|
1121
|
-
const result = await credentialStoreTool.execute(
|
|
1122
|
-
{
|
|
1123
|
-
action: "store",
|
|
1124
|
-
service: "mapbox",
|
|
1125
|
-
field: "token",
|
|
1126
|
-
value: "pk.test123",
|
|
1127
|
-
injection_templates: [
|
|
1128
|
-
{
|
|
1129
|
-
hostPattern: "api.mapbox.com",
|
|
1130
|
-
injectionType: "query",
|
|
1131
|
-
queryParamName: "access_token",
|
|
1132
|
-
},
|
|
1133
|
-
],
|
|
1134
|
-
},
|
|
1135
|
-
_ctx,
|
|
1136
|
-
);
|
|
1137
|
-
expect(result.isError).toBe(false);
|
|
1138
|
-
const metadata = getCredentialMetadata("mapbox", "token");
|
|
1139
|
-
expect(metadata!.injectionTemplates).toHaveLength(1);
|
|
1140
|
-
expect(metadata!.injectionTemplates![0].injectionType).toBe("query");
|
|
1141
|
-
expect(metadata!.injectionTemplates![0].queryParamName).toBe(
|
|
1142
|
-
"access_token",
|
|
1143
|
-
);
|
|
1144
|
-
});
|
|
1145
|
-
});
|
|
1146
|
-
|
|
1147
|
-
// -----------------------------------------------------------------------
|
|
1148
|
-
// Multi-key same-service vault storage
|
|
1149
|
-
// -----------------------------------------------------------------------
|
|
1150
|
-
describe("multi-key same-service storage", () => {
|
|
1151
|
-
test("stores two credentials with same service but different aliases", async () => {
|
|
1152
|
-
const result1 = await credentialStoreTool.execute(
|
|
1153
|
-
{
|
|
1154
|
-
action: "store",
|
|
1155
|
-
service: "openai",
|
|
1156
|
-
field: "api_key_prod",
|
|
1157
|
-
value: "sk-prod-abc",
|
|
1158
|
-
alias: "production",
|
|
1159
|
-
},
|
|
1160
|
-
_ctx,
|
|
1161
|
-
);
|
|
1162
|
-
expect(result1.isError).toBe(false);
|
|
1163
|
-
|
|
1164
|
-
const result2 = await credentialStoreTool.execute(
|
|
1165
|
-
{
|
|
1166
|
-
action: "store",
|
|
1167
|
-
service: "openai",
|
|
1168
|
-
field: "api_key_staging",
|
|
1169
|
-
value: "sk-staging-xyz",
|
|
1170
|
-
alias: "staging",
|
|
1171
|
-
},
|
|
1172
|
-
_ctx,
|
|
1173
|
-
);
|
|
1174
|
-
expect(result2.isError).toBe(false);
|
|
1175
|
-
|
|
1176
|
-
// Verify both stored independently in metadata
|
|
1177
|
-
const meta1 = getCredentialMetadata("openai", "api_key_prod");
|
|
1178
|
-
const meta2 = getCredentialMetadata("openai", "api_key_staging");
|
|
1179
|
-
expect(meta1).toBeDefined();
|
|
1180
|
-
expect(meta2).toBeDefined();
|
|
1181
|
-
expect(meta1!.alias).toBe("production");
|
|
1182
|
-
expect(meta2!.alias).toBe("staging");
|
|
1183
|
-
});
|
|
1184
|
-
|
|
1185
|
-
test("listing shows both same-service credentials independently", async () => {
|
|
1186
|
-
await credentialStoreTool.execute(
|
|
1187
|
-
{
|
|
1188
|
-
action: "store",
|
|
1189
|
-
service: "openai",
|
|
1190
|
-
field: "api_key_prod",
|
|
1191
|
-
value: "sk-prod-abc",
|
|
1192
|
-
alias: "production",
|
|
1193
|
-
},
|
|
1194
|
-
_ctx,
|
|
1195
|
-
);
|
|
1196
|
-
await credentialStoreTool.execute(
|
|
1197
|
-
{
|
|
1198
|
-
action: "store",
|
|
1199
|
-
service: "openai",
|
|
1200
|
-
field: "api_key_staging",
|
|
1201
|
-
value: "sk-staging-xyz",
|
|
1202
|
-
alias: "staging",
|
|
1203
|
-
},
|
|
1204
|
-
_ctx,
|
|
1205
|
-
);
|
|
1206
|
-
|
|
1207
|
-
const result = await credentialStoreTool.execute(
|
|
1208
|
-
{ action: "list" },
|
|
1209
|
-
_ctx,
|
|
1210
|
-
);
|
|
1211
|
-
expect(result.isError).toBe(false);
|
|
1212
|
-
|
|
1213
|
-
const entries = JSON.parse(result.content);
|
|
1214
|
-
const openaiEntries = entries.filter(
|
|
1215
|
-
(e: { service: string }) => e.service === "openai",
|
|
1216
|
-
);
|
|
1217
|
-
expect(openaiEntries).toHaveLength(2);
|
|
1218
|
-
|
|
1219
|
-
const aliases = openaiEntries
|
|
1220
|
-
.map((e: { alias?: string }) => e.alias)
|
|
1221
|
-
.sort();
|
|
1222
|
-
expect(aliases).toEqual(["production", "staging"]);
|
|
1223
|
-
});
|
|
1224
|
-
|
|
1225
|
-
test("each same-service credential has its own credential_id", async () => {
|
|
1226
|
-
await credentialStoreTool.execute(
|
|
1227
|
-
{
|
|
1228
|
-
action: "store",
|
|
1229
|
-
service: "openai",
|
|
1230
|
-
field: "api_key_prod",
|
|
1231
|
-
value: "sk-prod-abc",
|
|
1232
|
-
alias: "production",
|
|
1233
|
-
},
|
|
1234
|
-
_ctx,
|
|
1235
|
-
);
|
|
1236
|
-
await credentialStoreTool.execute(
|
|
1237
|
-
{
|
|
1238
|
-
action: "store",
|
|
1239
|
-
service: "openai",
|
|
1240
|
-
field: "api_key_staging",
|
|
1241
|
-
value: "sk-staging-xyz",
|
|
1242
|
-
alias: "staging",
|
|
1243
|
-
},
|
|
1244
|
-
_ctx,
|
|
1245
|
-
);
|
|
1246
|
-
|
|
1247
|
-
const meta1 = getCredentialMetadata("openai", "api_key_prod");
|
|
1248
|
-
const meta2 = getCredentialMetadata("openai", "api_key_staging");
|
|
1249
|
-
expect(meta1).toBeDefined();
|
|
1250
|
-
expect(meta2).toBeDefined();
|
|
1251
|
-
expect(meta1!.credentialId).not.toBe(meta2!.credentialId);
|
|
1252
|
-
// Both should be valid UUIDs (non-empty strings)
|
|
1253
|
-
expect(meta1!.credentialId.length).toBeGreaterThan(0);
|
|
1254
|
-
expect(meta2!.credentialId.length).toBeGreaterThan(0);
|
|
1255
|
-
});
|
|
1256
|
-
});
|
|
1257
|
-
|
|
1258
|
-
// -----------------------------------------------------------------------
|
|
1259
|
-
// Namespace isolation
|
|
1260
|
-
// -----------------------------------------------------------------------
|
|
1261
|
-
describe("namespace isolation", () => {
|
|
1262
|
-
test("different services with same field do not collide", async () => {
|
|
1263
|
-
await executeVault({
|
|
1264
|
-
action: "store",
|
|
1265
|
-
service: "gmail",
|
|
1266
|
-
field: "password",
|
|
1267
|
-
value: "gmail-pass",
|
|
1268
|
-
});
|
|
1269
|
-
await executeVault({
|
|
1270
|
-
action: "store",
|
|
1271
|
-
service: "github",
|
|
1272
|
-
field: "password",
|
|
1273
|
-
value: "github-pass",
|
|
1274
|
-
});
|
|
1275
|
-
|
|
1276
|
-
expect(await getSecureKeyAsync(credentialKey("gmail", "password"))).toBe(
|
|
1277
|
-
"gmail-pass",
|
|
1278
|
-
);
|
|
1279
|
-
expect(await getSecureKeyAsync(credentialKey("github", "password"))).toBe(
|
|
1280
|
-
"github-pass",
|
|
1281
|
-
);
|
|
1282
|
-
});
|
|
1283
|
-
|
|
1284
|
-
test("same service with different fields do not collide", async () => {
|
|
1285
|
-
await executeVault({
|
|
1286
|
-
action: "store",
|
|
1287
|
-
service: "gmail",
|
|
1288
|
-
field: "password",
|
|
1289
|
-
value: "pass123",
|
|
1290
|
-
});
|
|
1291
|
-
await executeVault({
|
|
1292
|
-
action: "store",
|
|
1293
|
-
service: "gmail",
|
|
1294
|
-
field: "recovery_email",
|
|
1295
|
-
value: "backup@example.com",
|
|
1296
|
-
});
|
|
1297
|
-
|
|
1298
|
-
expect(await getSecureKeyAsync(credentialKey("gmail", "password"))).toBe(
|
|
1299
|
-
"pass123",
|
|
1300
|
-
);
|
|
1301
|
-
expect(
|
|
1302
|
-
await getSecureKeyAsync(credentialKey("gmail", "recovery_email")),
|
|
1303
|
-
).toBe("backup@example.com");
|
|
1304
|
-
});
|
|
1305
|
-
});
|
|
1306
|
-
});
|
|
1307
|
-
|
|
1308
|
-
// ---------------------------------------------------------------------------
|
|
1309
|
-
// Token refresh deduplication tests
|
|
1310
|
-
// ---------------------------------------------------------------------------
|
|
1311
|
-
|
|
1312
|
-
describe("withValidToken refresh deduplication", () => {
|
|
1313
|
-
beforeAll(() => {
|
|
1314
|
-
mkdirSync(TEST_DIR, { recursive: true });
|
|
1315
|
-
});
|
|
1316
|
-
|
|
1317
|
-
beforeEach(() => {
|
|
1318
|
-
_resetBackend();
|
|
1319
|
-
for (const entry of readdirSync(TEST_DIR)) {
|
|
1320
|
-
rmSync(join(TEST_DIR, entry), { recursive: true, force: true });
|
|
1321
|
-
}
|
|
1322
|
-
setStorePathForTesting(STORE_PATH);
|
|
1323
|
-
_setMetadataPath(join(TEST_DIR, "metadata.json"));
|
|
1324
|
-
_resetRefreshBreakers();
|
|
1325
|
-
_resetInflightRefreshes();
|
|
1326
|
-
mockRefreshOAuth2Token.mockClear();
|
|
1327
|
-
// Clear mock oauth-store maps
|
|
1328
|
-
mockConnections.clear();
|
|
1329
|
-
mockApps.clear();
|
|
1330
|
-
mockProviders.clear();
|
|
1331
|
-
});
|
|
1332
|
-
|
|
1333
|
-
afterEach(() => {
|
|
1334
|
-
_setMetadataPath(null);
|
|
1335
|
-
setStorePathForTesting(null);
|
|
1336
|
-
_resetBackend();
|
|
1337
|
-
_resetRefreshBreakers();
|
|
1338
|
-
_resetInflightRefreshes();
|
|
1339
|
-
mockConnections.clear();
|
|
1340
|
-
mockApps.clear();
|
|
1341
|
-
mockProviders.clear();
|
|
1342
|
-
});
|
|
1343
|
-
|
|
1344
|
-
afterAll(() => {
|
|
1345
|
-
rmSync(TEST_DIR, { recursive: true, force: true });
|
|
1346
|
-
});
|
|
1347
|
-
|
|
1348
|
-
/**
|
|
1349
|
-
* Helper: set up a service with an access token, refresh token, and
|
|
1350
|
-
* mock DB data so that token refresh can proceed through doRefresh().
|
|
1351
|
-
*
|
|
1352
|
-
* OAuth-specific fields (tokenExchangeUrl, clientId, expiresAt) are now stored
|
|
1353
|
-
* in the SQLite oauth-store. The mock maps simulate the DB layer.
|
|
1354
|
-
*/
|
|
1355
|
-
async function setupService(
|
|
1356
|
-
service: string,
|
|
1357
|
-
opts?: { expired?: boolean; accessToken?: string },
|
|
1358
|
-
) {
|
|
1359
|
-
const accessToken = opts?.accessToken ?? "old-access-token";
|
|
1360
|
-
|
|
1361
|
-
// Seed mock oauth-store maps so token-manager can resolve refresh config
|
|
1362
|
-
const appId = `app-${service}`;
|
|
1363
|
-
const connId = `conn-${service}`;
|
|
1364
|
-
|
|
1365
|
-
// Store access token under the oauth_connection key path that
|
|
1366
|
-
// withValidToken reads (not the legacy credentialKey path).
|
|
1367
|
-
await setSecureKeyAsync(
|
|
1368
|
-
`oauth_connection/${connId}/access_token`,
|
|
1369
|
-
accessToken,
|
|
1370
|
-
);
|
|
1371
|
-
mockProviders.set(service, {
|
|
1372
|
-
key: service,
|
|
1373
|
-
tokenExchangeUrl: "https://oauth.example.com/token",
|
|
1374
|
-
refreshUrl: null,
|
|
1375
|
-
});
|
|
1376
|
-
mockApps.set(appId, {
|
|
1377
|
-
id: appId,
|
|
1378
|
-
provider: service,
|
|
1379
|
-
clientId: "test-client-id",
|
|
1380
|
-
clientSecretCredentialPath: `oauth_app/${appId}/client_secret`,
|
|
1381
|
-
});
|
|
1382
|
-
mockConnections.set(service, {
|
|
1383
|
-
id: connId,
|
|
1384
|
-
provider: service,
|
|
1385
|
-
oauthAppId: appId,
|
|
1386
|
-
expiresAt: opts?.expired
|
|
1387
|
-
? Date.now() - 60_000 // expired 1 minute ago
|
|
1388
|
-
: Date.now() + 3600_000, // expires in 1 hour
|
|
1389
|
-
});
|
|
1390
|
-
// Store refresh token and client_secret in secure keys (token-manager reads them)
|
|
1391
|
-
await setSecureKeyAsync(
|
|
1392
|
-
`oauth_connection/${connId}/refresh_token`,
|
|
1393
|
-
"valid-refresh-token",
|
|
1394
|
-
);
|
|
1395
|
-
await setSecureKeyAsync(
|
|
1396
|
-
`oauth_app/${appId}/client_secret`,
|
|
1397
|
-
"test-client-secret",
|
|
1398
|
-
);
|
|
1399
|
-
}
|
|
1400
|
-
|
|
1401
|
-
test("3 concurrent 401 refreshes for the same service call doRefresh exactly once", async () => {
|
|
1402
|
-
await setupService("google");
|
|
1403
|
-
|
|
1404
|
-
let resolveRefresh!: (value: {
|
|
1405
|
-
accessToken: string;
|
|
1406
|
-
expiresIn: number;
|
|
1407
|
-
}) => void;
|
|
1408
|
-
const refreshPromise = new Promise<{
|
|
1409
|
-
accessToken: string;
|
|
1410
|
-
expiresIn: number;
|
|
1411
|
-
}>((resolve) => {
|
|
1412
|
-
resolveRefresh = resolve;
|
|
1413
|
-
});
|
|
1414
|
-
|
|
1415
|
-
mockRefreshOAuth2Token.mockImplementation(() => refreshPromise);
|
|
1416
|
-
|
|
1417
|
-
const err401 = Object.assign(new Error("Unauthorized"), { status: 401 });
|
|
1418
|
-
|
|
1419
|
-
const callback = async (token: string) => {
|
|
1420
|
-
if (token === "old-access-token") throw err401;
|
|
1421
|
-
return `result-with-${token}`;
|
|
1422
|
-
};
|
|
1423
|
-
|
|
1424
|
-
// Launch 3 concurrent withValidToken calls — all will get a non-expired
|
|
1425
|
-
// token first, call the callback, get a 401, and then try to refresh.
|
|
1426
|
-
const p1 = withValidToken("google", callback);
|
|
1427
|
-
const p2 = withValidToken("google", callback);
|
|
1428
|
-
const p3 = withValidToken("google", callback);
|
|
1429
|
-
|
|
1430
|
-
// Let the event loop tick so all 3 calls enter the 401 retry path
|
|
1431
|
-
await new Promise((r) => setTimeout(r, 10));
|
|
1432
|
-
|
|
1433
|
-
// Resolve the single refresh attempt
|
|
1434
|
-
resolveRefresh({ accessToken: "new-token-123", expiresIn: 3600 });
|
|
1435
|
-
|
|
1436
|
-
const results = await Promise.all([p1, p2, p3]);
|
|
1437
|
-
|
|
1438
|
-
// All 3 should succeed with the refreshed token
|
|
1439
|
-
expect(results).toEqual([
|
|
1440
|
-
"result-with-new-token-123",
|
|
1441
|
-
"result-with-new-token-123",
|
|
1442
|
-
"result-with-new-token-123",
|
|
1443
|
-
]);
|
|
1444
|
-
|
|
1445
|
-
// refreshOAuth2Token should have been called exactly once
|
|
1446
|
-
expect(mockRefreshOAuth2Token).toHaveBeenCalledTimes(1);
|
|
1447
|
-
});
|
|
1448
|
-
|
|
1449
|
-
test("concurrent refreshes for different services proceed independently", async () => {
|
|
1450
|
-
await setupService("google");
|
|
1451
|
-
await setupService("slack");
|
|
1452
|
-
|
|
1453
|
-
let resolveGmail!: (value: {
|
|
1454
|
-
accessToken: string;
|
|
1455
|
-
expiresIn: number;
|
|
1456
|
-
}) => void;
|
|
1457
|
-
let resolveSlack!: (value: {
|
|
1458
|
-
accessToken: string;
|
|
1459
|
-
expiresIn: number;
|
|
1460
|
-
}) => void;
|
|
1461
|
-
|
|
1462
|
-
const gmailPromise = new Promise<{
|
|
1463
|
-
accessToken: string;
|
|
1464
|
-
expiresIn: number;
|
|
1465
|
-
}>((resolve) => {
|
|
1466
|
-
resolveGmail = resolve;
|
|
1467
|
-
});
|
|
1468
|
-
const slackPromise = new Promise<{
|
|
1469
|
-
accessToken: string;
|
|
1470
|
-
expiresIn: number;
|
|
1471
|
-
}>((resolve) => {
|
|
1472
|
-
resolveSlack = resolve;
|
|
1473
|
-
});
|
|
1474
|
-
|
|
1475
|
-
let refreshCallCount = 0;
|
|
1476
|
-
mockRefreshOAuth2Token.mockImplementation(() => {
|
|
1477
|
-
refreshCallCount++;
|
|
1478
|
-
// Both services use the same tokenExchangeUrl in this test, so we track by
|
|
1479
|
-
// call order to return the correct deferred promise.
|
|
1480
|
-
if (refreshCallCount === 1) return gmailPromise;
|
|
1481
|
-
return slackPromise;
|
|
1482
|
-
});
|
|
1483
|
-
|
|
1484
|
-
const err401 = Object.assign(new Error("Unauthorized"), { status: 401 });
|
|
1485
|
-
|
|
1486
|
-
const gmailCallback = async (token: string) => {
|
|
1487
|
-
if (token === "old-access-token") throw err401;
|
|
1488
|
-
return `gmail-${token}`;
|
|
1489
|
-
};
|
|
1490
|
-
const slackCallback = async (token: string) => {
|
|
1491
|
-
if (token === "old-access-token") throw err401;
|
|
1492
|
-
return `slack-${token}`;
|
|
1493
|
-
};
|
|
1494
|
-
|
|
1495
|
-
const p1 = withValidToken("google", gmailCallback);
|
|
1496
|
-
const p2 = withValidToken("slack", slackCallback);
|
|
1497
|
-
|
|
1498
|
-
await new Promise((r) => setTimeout(r, 10));
|
|
1499
|
-
|
|
1500
|
-
// Resolve both independently
|
|
1501
|
-
resolveGmail({ accessToken: "gmail-new-token", expiresIn: 3600 });
|
|
1502
|
-
resolveSlack({ accessToken: "slack-new-token", expiresIn: 3600 });
|
|
1503
|
-
|
|
1504
|
-
const [r1, r2] = await Promise.all([p1, p2]);
|
|
1505
|
-
|
|
1506
|
-
expect(r1).toBe("gmail-gmail-new-token");
|
|
1507
|
-
expect(r2).toBe("slack-slack-new-token");
|
|
1508
|
-
|
|
1509
|
-
// Both services should have triggered their own refresh
|
|
1510
|
-
expect(mockRefreshOAuth2Token).toHaveBeenCalledTimes(2);
|
|
1511
|
-
});
|
|
1512
|
-
|
|
1513
|
-
test("deduplication cleans up after refresh completes, allowing subsequent refreshes", async () => {
|
|
1514
|
-
await setupService("google");
|
|
1515
|
-
|
|
1516
|
-
let refreshCount = 0;
|
|
1517
|
-
mockRefreshOAuth2Token.mockImplementation(() => {
|
|
1518
|
-
refreshCount++;
|
|
1519
|
-
return Promise.resolve({
|
|
1520
|
-
accessToken: `token-${refreshCount}`,
|
|
1521
|
-
expiresIn: 3600,
|
|
1522
|
-
});
|
|
1523
|
-
});
|
|
1524
|
-
|
|
1525
|
-
const err401 = Object.assign(new Error("Unauthorized"), { status: 401 });
|
|
1526
|
-
|
|
1527
|
-
// First call triggers a refresh (old token → 401 → refresh → token-1)
|
|
1528
|
-
const r1 = await withValidToken("google", async (token: string) => {
|
|
1529
|
-
if (token !== "token-1") throw err401;
|
|
1530
|
-
return token;
|
|
1531
|
-
});
|
|
1532
|
-
expect(r1).toBe("token-1");
|
|
1533
|
-
expect(refreshCount).toBe(1);
|
|
1534
|
-
|
|
1535
|
-
// Second call also triggers a 401 to verify dedup state was cleaned up
|
|
1536
|
-
// and a new refresh is allowed (not deduplicated with the first).
|
|
1537
|
-
const r2 = await withValidToken("google", async (token: string) => {
|
|
1538
|
-
if (token !== "token-2") throw err401;
|
|
1539
|
-
return token;
|
|
1540
|
-
});
|
|
1541
|
-
expect(r2).toBe("token-2");
|
|
1542
|
-
// Second refresh should have happened (not deduplicated with the first,
|
|
1543
|
-
// since the first already completed)
|
|
1544
|
-
expect(refreshCount).toBe(2);
|
|
1545
|
-
});
|
|
1546
|
-
|
|
1547
|
-
test("deduplication propagates refresh errors to all waiting callers", async () => {
|
|
1548
|
-
await setupService("google");
|
|
1549
|
-
|
|
1550
|
-
mockRefreshOAuth2Token.mockImplementation(() =>
|
|
1551
|
-
Promise.reject(
|
|
1552
|
-
Object.assign(
|
|
1553
|
-
new Error("OAuth2 token refresh failed (HTTP 401: invalid_grant)"),
|
|
1554
|
-
),
|
|
1555
|
-
),
|
|
1556
|
-
);
|
|
1557
|
-
|
|
1558
|
-
const err401 = Object.assign(new Error("Unauthorized"), { status: 401 });
|
|
1559
|
-
|
|
1560
|
-
const callback = async (token: string) => {
|
|
1561
|
-
if (token === "old-access-token") throw err401;
|
|
1562
|
-
return "should-not-reach";
|
|
1563
|
-
};
|
|
1564
|
-
|
|
1565
|
-
// Launch 2 concurrent calls — both should fail with the same error
|
|
1566
|
-
const p1 = withValidToken("google", callback);
|
|
1567
|
-
const p2 = withValidToken("google", callback);
|
|
1568
|
-
|
|
1569
|
-
const results = await Promise.allSettled([p1, p2]);
|
|
1570
|
-
|
|
1571
|
-
expect(results[0].status).toBe("rejected");
|
|
1572
|
-
expect(results[1].status).toBe("rejected");
|
|
1573
|
-
|
|
1574
|
-
// Only one actual refresh attempt
|
|
1575
|
-
expect(mockRefreshOAuth2Token).toHaveBeenCalledTimes(1);
|
|
1576
|
-
});
|
|
1577
|
-
|
|
1578
|
-
// -----------------------------------------------------------------------
|
|
1579
|
-
// refreshUrl resolution — provider.refreshUrl with fallback to tokenExchangeUrl
|
|
1580
|
-
// -----------------------------------------------------------------------
|
|
1581
|
-
describe("refreshUrl resolution", () => {
|
|
1582
|
-
test("uses provider.refreshUrl when set", async () => {
|
|
1583
|
-
await setupService("google");
|
|
1584
|
-
mockProviders.get("google")!.refreshUrl =
|
|
1585
|
-
"https://refresh.example.com/token";
|
|
1586
|
-
|
|
1587
|
-
mockRefreshOAuth2Token.mockImplementation(() =>
|
|
1588
|
-
Promise.resolve({
|
|
1589
|
-
accessToken: "new-token-from-refresh-url",
|
|
1590
|
-
expiresIn: 3600,
|
|
1591
|
-
}),
|
|
1592
|
-
);
|
|
1593
|
-
|
|
1594
|
-
const err401 = Object.assign(new Error("Unauthorized"), { status: 401 });
|
|
1595
|
-
|
|
1596
|
-
const callback = async (token: string) => {
|
|
1597
|
-
if (token === "old-access-token") throw err401;
|
|
1598
|
-
return `result-with-${token}`;
|
|
1599
|
-
};
|
|
1600
|
-
|
|
1601
|
-
const result = await withValidToken("google", callback);
|
|
1602
|
-
|
|
1603
|
-
expect(result).toBe("result-with-new-token-from-refresh-url");
|
|
1604
|
-
expect(mockRefreshOAuth2Token).toHaveBeenCalledTimes(1);
|
|
1605
|
-
// Assert the refresh endpoint passed in is provider.refreshUrl, not
|
|
1606
|
-
// the tokenExchangeUrl fallback.
|
|
1607
|
-
expect(mockRefreshOAuth2Token.mock.calls[0]?.[0]).toBe(
|
|
1608
|
-
"https://refresh.example.com/token",
|
|
1609
|
-
);
|
|
1610
|
-
});
|
|
1611
|
-
|
|
1612
|
-
test("falls back to provider.tokenExchangeUrl when refreshUrl is null", async () => {
|
|
1613
|
-
// setupService sets refreshUrl: null by default — this exercises the
|
|
1614
|
-
// fallback path explicitly.
|
|
1615
|
-
await setupService("google");
|
|
1616
|
-
expect(mockProviders.get("google")!.refreshUrl).toBeNull();
|
|
1617
|
-
|
|
1618
|
-
mockRefreshOAuth2Token.mockImplementation(() =>
|
|
1619
|
-
Promise.resolve({
|
|
1620
|
-
accessToken: "new-token-from-token-exchange-url",
|
|
1621
|
-
expiresIn: 3600,
|
|
1622
|
-
}),
|
|
1623
|
-
);
|
|
1624
|
-
|
|
1625
|
-
const err401 = Object.assign(new Error("Unauthorized"), { status: 401 });
|
|
1626
|
-
|
|
1627
|
-
const callback = async (token: string) => {
|
|
1628
|
-
if (token === "old-access-token") throw err401;
|
|
1629
|
-
return `result-with-${token}`;
|
|
1630
|
-
};
|
|
1631
|
-
|
|
1632
|
-
const result = await withValidToken("google", callback);
|
|
1633
|
-
|
|
1634
|
-
expect(result).toBe("result-with-new-token-from-token-exchange-url");
|
|
1635
|
-
expect(mockRefreshOAuth2Token).toHaveBeenCalledTimes(1);
|
|
1636
|
-
// Assert the refresh endpoint falls back to tokenExchangeUrl.
|
|
1637
|
-
expect(mockRefreshOAuth2Token.mock.calls[0]?.[0]).toBe(
|
|
1638
|
-
"https://oauth.example.com/token",
|
|
1639
|
-
);
|
|
1640
|
-
});
|
|
1641
|
-
|
|
1642
|
-
test("falls back to provider.tokenExchangeUrl when refreshUrl is undefined", async () => {
|
|
1643
|
-
await setupService("google");
|
|
1644
|
-
// Delete the refreshUrl field entirely so the property is `undefined`
|
|
1645
|
-
// rather than `null`. Both representations of "not set" must produce
|
|
1646
|
-
// the fallback behavior.
|
|
1647
|
-
delete mockProviders.get("google")!.refreshUrl;
|
|
1648
|
-
expect(mockProviders.get("google")!.refreshUrl).toBeUndefined();
|
|
1649
|
-
|
|
1650
|
-
mockRefreshOAuth2Token.mockImplementation(() =>
|
|
1651
|
-
Promise.resolve({
|
|
1652
|
-
accessToken: "new-token-from-token-exchange-url",
|
|
1653
|
-
expiresIn: 3600,
|
|
1654
|
-
}),
|
|
1655
|
-
);
|
|
1656
|
-
|
|
1657
|
-
const err401 = Object.assign(new Error("Unauthorized"), { status: 401 });
|
|
1658
|
-
|
|
1659
|
-
const callback = async (token: string) => {
|
|
1660
|
-
if (token === "old-access-token") throw err401;
|
|
1661
|
-
return `result-with-${token}`;
|
|
1662
|
-
};
|
|
1663
|
-
|
|
1664
|
-
const result = await withValidToken("google", callback);
|
|
1665
|
-
|
|
1666
|
-
expect(result).toBe("result-with-new-token-from-token-exchange-url");
|
|
1667
|
-
expect(mockRefreshOAuth2Token).toHaveBeenCalledTimes(1);
|
|
1668
|
-
// Assert the refresh endpoint falls back to tokenExchangeUrl.
|
|
1669
|
-
expect(mockRefreshOAuth2Token.mock.calls[0]?.[0]).toBe(
|
|
1670
|
-
"https://oauth.example.com/token",
|
|
1671
|
-
);
|
|
1672
|
-
});
|
|
1673
|
-
|
|
1674
|
-
test("falls back to provider.tokenExchangeUrl when refreshUrl is empty string", async () => {
|
|
1675
|
-
// Platform's Python `oauth_app.refresh_url or oauth_app.token_exchange_url`
|
|
1676
|
-
// treats an empty string as unset. We use `||` (not `??`) so empty
|
|
1677
|
-
// strings follow the same fallback path and never resolve to an empty
|
|
1678
|
-
// endpoint.
|
|
1679
|
-
await setupService("google");
|
|
1680
|
-
mockProviders.get("google")!.refreshUrl = "";
|
|
1681
|
-
|
|
1682
|
-
mockRefreshOAuth2Token.mockImplementation(() =>
|
|
1683
|
-
Promise.resolve({
|
|
1684
|
-
accessToken: "new-token-from-token-exchange-url",
|
|
1685
|
-
expiresIn: 3600,
|
|
1686
|
-
}),
|
|
1687
|
-
);
|
|
1688
|
-
|
|
1689
|
-
const err401 = Object.assign(new Error("Unauthorized"), { status: 401 });
|
|
1690
|
-
|
|
1691
|
-
const callback = async (token: string) => {
|
|
1692
|
-
if (token === "old-access-token") throw err401;
|
|
1693
|
-
return `result-with-${token}`;
|
|
1694
|
-
};
|
|
1695
|
-
|
|
1696
|
-
const result = await withValidToken("google", callback);
|
|
1697
|
-
|
|
1698
|
-
expect(result).toBe("result-with-new-token-from-token-exchange-url");
|
|
1699
|
-
expect(mockRefreshOAuth2Token).toHaveBeenCalledTimes(1);
|
|
1700
|
-
// Assert the refresh endpoint falls back to tokenExchangeUrl — NOT "".
|
|
1701
|
-
expect(mockRefreshOAuth2Token.mock.calls[0]?.[0]).toBe(
|
|
1702
|
-
"https://oauth.example.com/token",
|
|
1703
|
-
);
|
|
1704
|
-
});
|
|
1705
|
-
});
|
|
1706
|
-
});
|