@vellumai/assistant 0.4.48 → 0.4.49
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +2 -2
- package/README.md +2 -23
- package/docs/architecture/integrations.md +45 -41
- package/docs/architecture/keychain-broker.md +3 -3
- package/docs/runbook-trusted-contacts.md +3 -8
- package/hook-templates/debug-prompt-logger/hook.json +1 -1
- package/hook-templates/debug-prompt-logger/run.sh +1 -3
- package/package.json +1 -1
- package/src/__tests__/actor-token-service.test.ts +0 -1
- package/src/__tests__/anthropic-provider.test.ts +156 -0
- package/src/__tests__/approval-cascade.test.ts +810 -0
- package/src/__tests__/approval-primitive.test.ts +0 -1
- package/src/__tests__/approval-routes-http.test.ts +2 -0
- package/src/__tests__/assistant-attachments.test.ts +12 -34
- package/src/__tests__/assistant-feature-flag-guardrails.test.ts +76 -0
- package/src/__tests__/assistant-feature-flags-integration.test.ts +0 -1
- package/src/__tests__/browser-skill-baseline-tool-payload.test.ts +2 -2
- package/src/__tests__/channel-guardian.test.ts +0 -2
- package/src/__tests__/channel-readiness-routes.test.ts +15 -6
- package/src/__tests__/channel-readiness-service.test.ts +10 -9
- package/src/__tests__/checker.test.ts +9 -29
- package/src/__tests__/computer-use-skill-manifest-regression.test.ts +1 -1
- package/src/__tests__/computer-use-tools.test.ts +2 -19
- package/src/__tests__/config-watcher.test.ts +0 -1
- package/src/__tests__/confirmation-request-guardian-bridge.test.ts +0 -1
- package/src/__tests__/context-image-dimensions.test.ts +332 -0
- package/src/__tests__/context-token-estimator.test.ts +196 -13
- package/src/__tests__/conversation-attention-store.test.ts +0 -1
- package/src/__tests__/conversation-attention-telegram.test.ts +0 -1
- package/src/__tests__/conversation-routes-guardian-reply.test.ts +144 -0
- package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -0
- package/src/__tests__/credential-metadata-store.test.ts +64 -73
- package/src/__tests__/credential-security-invariants.test.ts +13 -7
- package/src/__tests__/credential-vault-unit.test.ts +280 -49
- package/src/__tests__/credential-vault.test.ts +138 -16
- package/src/__tests__/credentials-cli.test.ts +71 -0
- package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +0 -1
- package/src/__tests__/ephemeral-permissions.test.ts +3 -3
- package/src/__tests__/gateway-only-guard.test.ts +0 -1
- package/src/__tests__/guardian-action-grant-mint-consume.test.ts +0 -1
- package/src/__tests__/guardian-decision-primitive-canonical.test.ts +0 -1
- package/src/__tests__/guardian-routing-invariants.test.ts +0 -1
- package/src/__tests__/guardian-verification-voice-binding.test.ts +0 -1
- package/src/__tests__/handlers-user-message-approval-consumption.test.ts +0 -39
- package/src/__tests__/heartbeat-service.test.ts +0 -1
- package/src/__tests__/host-cu-proxy.test.ts +629 -0
- package/src/__tests__/host-shell-tool.test.ts +27 -15
- package/src/__tests__/http-user-message-parity.test.ts +1 -0
- package/src/__tests__/ingress-url-consistency.test.ts +14 -21
- package/src/__tests__/integration-status.test.ts +32 -51
- package/src/__tests__/intent-routing.test.ts +0 -1
- package/src/__tests__/invite-routes-http.test.ts +10 -9
- package/src/__tests__/keychain-broker-client.test.ts +11 -43
- package/src/__tests__/notification-routing-intent.test.ts +0 -1
- package/src/__tests__/oauth-cli.test.ts +373 -14
- package/src/__tests__/oauth-provider-profiles.test.ts +9 -9
- package/src/__tests__/oauth-scope-policy.test.ts +4 -6
- package/src/__tests__/oauth-store.test.ts +756 -0
- package/src/__tests__/onboarding-starter-tasks.test.ts +0 -1
- package/src/__tests__/provider-error-scenarios.test.ts +0 -1
- package/src/__tests__/provider-streaming.benchmark.test.ts +0 -1
- package/src/__tests__/public-ingress-urls.test.ts +15 -21
- package/src/__tests__/recording-handler.test.ts +3 -4
- package/src/__tests__/registry.test.ts +2 -2
- package/src/__tests__/runtime-events-sse.test.ts +55 -7
- package/src/__tests__/schedule-store.test.ts +0 -1
- package/src/__tests__/scheduler-recurrence.test.ts +0 -1
- package/src/__tests__/scoped-approval-grants.test.ts +0 -1
- package/src/__tests__/scoped-grant-security-matrix.test.ts +0 -1
- package/src/__tests__/secret-ingress-handler.test.ts +0 -1
- package/src/__tests__/send-endpoint-busy.test.ts +21 -6
- package/src/__tests__/sequence-store.test.ts +0 -1
- package/src/__tests__/session-init.benchmark.test.ts +4 -5
- package/src/__tests__/skill-include-graph.test.ts +66 -0
- package/src/__tests__/skill-load-feature-flag.test.ts +0 -1
- package/src/__tests__/skill-load-tool.test.ts +149 -1
- package/src/__tests__/skill-projection-feature-flag.test.ts +0 -1
- package/src/__tests__/skills-uninstall.test.ts +1 -1
- package/src/__tests__/skills.test.ts +3 -3
- package/src/__tests__/slack-channel-config.test.ts +67 -3
- package/src/__tests__/slack-share-routes.test.ts +17 -19
- package/src/__tests__/system-prompt.test.ts +0 -1
- package/src/__tests__/telegram-invite-adapter.test.ts +18 -22
- package/src/__tests__/terminal-tools.test.ts +4 -3
- package/src/__tests__/test-support/computer-use-skill-harness.ts +3 -2
- package/src/__tests__/tool-approval-handler.test.ts +0 -1
- package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +0 -1
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +0 -1
- package/src/__tests__/tool-executor-shell-integration.test.ts +0 -1
- package/src/__tests__/tool-executor.test.ts +0 -1
- package/src/__tests__/tool-grant-request-escalation.test.ts +0 -1
- package/src/__tests__/trust-store-pattern-matches.test.ts +29 -0
- package/src/__tests__/trust-store.test.ts +1 -22
- package/src/__tests__/trusted-contact-approval-notifier.test.ts +0 -1
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +0 -1
- package/src/__tests__/twilio-routes.test.ts +0 -16
- package/src/__tests__/verification-control-plane-policy.test.ts +0 -1
- package/src/__tests__/voice-scoped-grant-consumer.test.ts +0 -1
- package/src/agent/ax-tree-compaction.test.ts +235 -0
- package/src/agent/loop.ts +76 -130
- package/src/calls/call-domain.ts +1 -6
- package/src/calls/relay-server.ts +9 -13
- package/src/calls/twilio-config.ts +2 -7
- package/src/calls/twilio-routes.ts +1 -2
- package/src/calls/voice-ingress-preflight.ts +1 -1
- package/src/cli/commands/browser-relay.ts +18 -12
- package/src/cli/commands/completions.ts +0 -3
- package/src/cli/commands/credentials.ts +101 -15
- package/src/cli/commands/oauth/apps.ts +255 -0
- package/src/cli/commands/oauth/connections.ts +299 -0
- package/src/cli/commands/oauth/index.ts +52 -0
- package/src/cli/commands/oauth/providers.ts +242 -0
- package/src/cli/commands/skills.ts +4 -338
- package/src/cli/program.ts +1 -5
- package/src/cli/reference.ts +1 -3
- package/src/config/assistant-feature-flags.ts +0 -3
- package/src/config/bundled-skills/_shared/CLI_RETRIEVAL_PATTERN.md +1 -1
- package/src/config/bundled-skills/computer-use/SKILL.md +3 -6
- package/src/config/bundled-skills/computer-use/TOOLS.json +22 -4
- package/src/config/bundled-skills/google-calendar/calendar-client.ts +21 -16
- package/src/config/bundled-skills/messaging/tools/shared.ts +1 -4
- package/src/config/bundled-skills/settings/SKILL.md +1 -1
- package/src/config/bundled-skills/settings/TOOLS.json +2 -8
- package/src/config/bundled-skills/settings/tools/voice-config-update.ts +5 -33
- package/src/config/env-registry.ts +14 -83
- package/src/config/env.ts +11 -50
- package/src/config/feature-flag-registry.json +16 -16
- package/src/config/loader.ts +0 -6
- package/src/config/schema.ts +3 -1
- package/src/config/skills.ts +21 -2
- package/src/context/image-dimensions.ts +229 -0
- package/src/context/token-estimator.ts +75 -12
- package/src/context/window-manager.ts +49 -10
- package/src/daemon/assistant-attachments.ts +1 -13
- package/src/daemon/handlers/config-ingress.ts +8 -33
- package/src/daemon/handlers/config-slack-channel.ts +49 -46
- package/src/daemon/handlers/config-telegram.ts +32 -16
- package/src/daemon/handlers/sessions.ts +10 -24
- package/src/daemon/handlers/shared.ts +0 -130
- package/src/daemon/host-cu-proxy.ts +401 -0
- package/src/daemon/lifecycle.ts +36 -68
- package/src/daemon/message-protocol.ts +3 -0
- package/src/daemon/message-types/computer-use.ts +2 -119
- package/src/daemon/message-types/host-cu.ts +19 -0
- package/src/daemon/message-types/messages.ts +3 -0
- package/src/daemon/server.ts +14 -21
- package/src/daemon/session-agent-loop-handlers.ts +2 -0
- package/src/daemon/session-attachments.ts +1 -2
- package/src/daemon/session-slash.ts +1 -1
- package/src/daemon/session-surfaces.ts +40 -28
- package/src/daemon/session-tool-setup.ts +2 -9
- package/src/daemon/session.ts +138 -15
- package/src/daemon/tool-side-effects.ts +2 -8
- package/src/daemon/watch-handler.ts +2 -2
- package/src/events/tool-metrics-listener.ts +2 -2
- package/src/hooks/manager.ts +1 -4
- package/src/inbound/public-ingress-urls.ts +7 -7
- package/src/logfire.ts +16 -5
- package/src/memory/conversation-key-store.ts +21 -0
- package/src/memory/db-init.ts +4 -0
- package/src/memory/migrations/149-oauth-tables.ts +60 -0
- package/src/memory/migrations/index.ts +1 -0
- package/src/memory/schema/index.ts +1 -0
- package/src/memory/schema/oauth.ts +65 -0
- package/src/messaging/provider.ts +4 -4
- package/src/messaging/providers/gmail/client.ts +82 -2
- package/src/messaging/providers/gmail/people-client.ts +10 -10
- package/src/messaging/providers/telegram-bot/adapter.ts +17 -17
- package/src/messaging/providers/whatsapp/adapter.ts +11 -8
- package/src/messaging/registry.ts +2 -32
- package/src/notifications/copy-composer.ts +0 -5
- package/src/notifications/signal.ts +4 -5
- package/src/oauth/byo-connection.test.ts +126 -25
- package/src/oauth/byo-connection.ts +22 -6
- package/src/oauth/connect-orchestrator.ts +113 -57
- package/src/oauth/connect-types.ts +17 -23
- package/src/oauth/connection-resolver.ts +35 -11
- package/src/oauth/connection.ts +1 -1
- package/src/oauth/manual-token-connection.ts +104 -0
- package/src/oauth/oauth-store.ts +496 -0
- package/src/oauth/platform-connection.test.ts +29 -0
- package/src/oauth/platform-connection.ts +6 -5
- package/src/oauth/provider-behaviors.ts +124 -0
- package/src/oauth/scope-policy.ts +9 -2
- package/src/oauth/seed-providers.ts +161 -0
- package/src/oauth/token-persistence.ts +74 -78
- package/src/permissions/checker.ts +3 -3
- package/src/permissions/defaults.ts +0 -1
- package/src/permissions/prompter.ts +10 -1
- package/src/permissions/trust-store.ts +13 -0
- package/src/prompts/__tests__/build-cli-reference-section.test.ts +3 -1
- package/src/prompts/system-prompt.ts +28 -40
- package/src/providers/anthropic/client.ts +133 -24
- package/src/providers/retry.ts +1 -27
- package/src/runtime/auth/route-policy.ts +0 -3
- package/src/runtime/channel-reply-delivery.ts +0 -40
- package/src/runtime/gateway-client.ts +0 -7
- package/src/runtime/http-server.ts +8 -6
- package/src/runtime/http-types.ts +2 -2
- package/src/runtime/middleware/twilio-validation.ts +1 -11
- package/src/runtime/pending-interactions.ts +14 -12
- package/src/runtime/routes/channel-delivery-routes.ts +0 -1
- package/src/runtime/routes/conversation-routes.ts +73 -19
- package/src/runtime/routes/events-routes.ts +21 -11
- package/src/runtime/routes/host-cu-routes.ts +97 -0
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +12 -111
- package/src/runtime/routes/integrations/slack/share.ts +6 -7
- package/src/runtime/routes/log-export-routes.ts +126 -8
- package/src/runtime/routes/settings-routes.ts +55 -48
- package/src/runtime/routes/surface-action-routes.ts +1 -1
- package/src/runtime/routes/watch-routes.ts +128 -0
- package/src/schedule/integration-status.ts +10 -9
- package/src/security/credential-key.ts +0 -156
- package/src/security/keychain-broker-client.ts +5 -6
- package/src/security/oauth2.ts +1 -1
- package/src/security/token-manager.ts +119 -46
- package/src/skills/catalog-install.ts +358 -0
- package/src/skills/include-graph.ts +32 -0
- package/src/telegram/bot-username.ts +2 -3
- package/src/tools/browser/network-recorder.ts +1 -1
- package/src/tools/browser/network-recording-types.ts +1 -1
- package/src/tools/computer-use/definitions.ts +46 -11
- package/src/tools/computer-use/registry.ts +4 -5
- package/src/tools/credentials/broker.ts +1 -2
- package/src/tools/credentials/metadata-store.ts +17 -121
- package/src/tools/credentials/vault.ts +94 -167
- package/src/tools/registry.ts +2 -7
- package/src/tools/skills/load.ts +62 -3
- package/src/tools/watch/watch-state.ts +0 -12
- package/src/util/logger.ts +7 -41
- package/src/util/platform.ts +9 -28
- package/src/watcher/providers/google-calendar.ts +2 -1
- package/src/__tests__/computer-use-session-compaction.test.ts +0 -143
- package/src/__tests__/computer-use-session-lifecycle.test.ts +0 -322
- package/src/__tests__/computer-use-session-working-dir.test.ts +0 -166
- package/src/__tests__/computer-use-skill-baseline.test.ts +0 -78
- package/src/__tests__/computer-use-skill-endstate.test.ts +0 -105
- package/src/__tests__/computer-use-skill-lifecycle-cleanup.test.ts +0 -249
- package/src/__tests__/ride-shotgun-handler.test.ts +0 -452
- package/src/cli/commands/dev.ts +0 -129
- package/src/cli/commands/map.ts +0 -391
- package/src/cli/commands/oauth.ts +0 -77
- package/src/config/bundled-skills/computer-use/tools/computer-use-request-control.ts +0 -16
- package/src/daemon/computer-use-session.ts +0 -1026
- package/src/daemon/ride-shotgun-handler.ts +0 -569
- package/src/oauth/provider-base-urls.ts +0 -21
- package/src/oauth/provider-profiles.ts +0 -192
- package/src/prompts/computer-use-prompt.ts +0 -98
- package/src/runtime/routes/computer-use-routes.ts +0 -641
- package/src/runtime/telegram-streaming-delivery.test.ts +0 -729
- package/src/runtime/telegram-streaming-delivery.ts +0 -393
- package/src/tools/computer-use/request-computer-control.ts +0 -56
|
@@ -44,6 +44,62 @@ mock.module("../tools/registry.js", () => ({
|
|
|
44
44
|
registerTool: () => {},
|
|
45
45
|
}));
|
|
46
46
|
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
// Mock oauth-store to avoid SQLite dependency in unit tests
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
|
|
51
|
+
let mockGetMostRecentAppByProvider: ReturnType<
|
|
52
|
+
typeof mock<(key: string) => unknown>
|
|
53
|
+
>;
|
|
54
|
+
let mockGetAppByProviderAndClientId: ReturnType<
|
|
55
|
+
typeof mock<(key: string, clientId: string) => unknown>
|
|
56
|
+
>;
|
|
57
|
+
let mockGetProvider: ReturnType<typeof mock<(key: string) => unknown>>;
|
|
58
|
+
|
|
59
|
+
mock.module("../oauth/oauth-store.js", () => {
|
|
60
|
+
mockGetMostRecentAppByProvider = mock(() => undefined);
|
|
61
|
+
mockGetAppByProviderAndClientId = mock(() => undefined);
|
|
62
|
+
mockGetProvider = mock(() => undefined);
|
|
63
|
+
return {
|
|
64
|
+
getMostRecentAppByProvider: mockGetMostRecentAppByProvider,
|
|
65
|
+
getAppByProviderAndClientId: mockGetAppByProviderAndClientId,
|
|
66
|
+
getProvider: mockGetProvider,
|
|
67
|
+
listConnections: mock(() => []),
|
|
68
|
+
seedProviders: mock(() => {}),
|
|
69
|
+
disconnectOAuthProvider: mock(async () => "not-found" as const),
|
|
70
|
+
};
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
// Mock public ingress URL — not available in unit tests. The connect
|
|
75
|
+
// orchestrator dynamically imports this for non-interactive flows.
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
|
|
78
|
+
mock.module("../inbound/public-ingress-urls.js", () => ({
|
|
79
|
+
getPublicBaseUrl: () => {
|
|
80
|
+
throw new Error("No public ingress URL configured");
|
|
81
|
+
},
|
|
82
|
+
}));
|
|
83
|
+
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
// Mock prepareOAuth2Flow — unit tests should not start real loopback HTTP
|
|
86
|
+
// servers. The connect orchestrator still runs its own validation logic
|
|
87
|
+
// (scope policy, non-interactive ingress checks, etc.) but the actual
|
|
88
|
+
// OAuth flow setup is stubbed.
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
|
|
91
|
+
mock.module("../security/oauth2.js", () => ({
|
|
92
|
+
prepareOAuth2Flow: mock(async () => ({
|
|
93
|
+
authUrl: "https://mock-auth-url.example.com/authorize",
|
|
94
|
+
state: "mock-state",
|
|
95
|
+
completion: new Promise(() => {}),
|
|
96
|
+
})),
|
|
97
|
+
startOAuth2Flow: mock(async () => ({
|
|
98
|
+
grantedScopes: [],
|
|
99
|
+
tokens: { access_token: "mock-token" },
|
|
100
|
+
})),
|
|
101
|
+
}));
|
|
102
|
+
|
|
47
103
|
// ---------------------------------------------------------------------------
|
|
48
104
|
// Imports under test
|
|
49
105
|
// ---------------------------------------------------------------------------
|
|
@@ -473,18 +529,50 @@ describe("credential_store tool — prompt action", () => {
|
|
|
473
529
|
// ---------------------------------------------------------------------------
|
|
474
530
|
|
|
475
531
|
describe("credential_store tool — oauth2_connect error paths", () => {
|
|
532
|
+
/** Well-known provider rows returned by the mocked getProvider */
|
|
533
|
+
const wellKnownProviders: Record<string, object> = {
|
|
534
|
+
"integration:gmail": {
|
|
535
|
+
key: "integration:gmail",
|
|
536
|
+
authUrl: "https://accounts.google.com/o/oauth2/v2/auth",
|
|
537
|
+
tokenUrl: "https://oauth2.googleapis.com/token",
|
|
538
|
+
defaultScopes: JSON.stringify(["https://mail.google.com/"]),
|
|
539
|
+
scopePolicy: JSON.stringify({}),
|
|
540
|
+
callbackTransport: "loopback",
|
|
541
|
+
loopbackPort: 8756,
|
|
542
|
+
},
|
|
543
|
+
"integration:slack": {
|
|
544
|
+
key: "integration:slack",
|
|
545
|
+
authUrl: "https://slack.com/oauth/v2/authorize",
|
|
546
|
+
tokenUrl: "https://slack.com/api/oauth.v2.access",
|
|
547
|
+
defaultScopes: JSON.stringify(["channels:read"]),
|
|
548
|
+
scopePolicy: JSON.stringify({}),
|
|
549
|
+
},
|
|
550
|
+
};
|
|
551
|
+
|
|
476
552
|
beforeEach(() => {
|
|
477
553
|
if (existsSync(TEST_DIR)) rmSync(TEST_DIR, { recursive: true });
|
|
478
554
|
mkdirSync(TEST_DIR, { recursive: true });
|
|
479
555
|
_setStorePath(STORE_PATH);
|
|
480
556
|
_resetBackend();
|
|
481
557
|
_setMetadataPath(join(TEST_DIR, "metadata.json"));
|
|
558
|
+
// Return well-known provider rows so vault.ts knows gmail/slack are
|
|
559
|
+
// registered, and custom providers return undefined.
|
|
560
|
+
mockGetProvider.mockImplementation(
|
|
561
|
+
(key: string) => wellKnownProviders[key] ?? undefined,
|
|
562
|
+
);
|
|
563
|
+
mockGetMostRecentAppByProvider.mockClear();
|
|
564
|
+
mockGetMostRecentAppByProvider.mockImplementation(() => undefined);
|
|
565
|
+
mockGetAppByProviderAndClientId.mockClear();
|
|
566
|
+
mockGetAppByProviderAndClientId.mockImplementation(() => undefined);
|
|
482
567
|
});
|
|
483
568
|
|
|
484
569
|
afterEach(() => {
|
|
485
570
|
_setMetadataPath(null);
|
|
486
571
|
_setStorePath(null);
|
|
487
572
|
_resetBackend();
|
|
573
|
+
mockGetProvider.mockImplementation(() => undefined);
|
|
574
|
+
mockGetMostRecentAppByProvider.mockImplementation(() => undefined);
|
|
575
|
+
mockGetAppByProviderAndClientId.mockImplementation(() => undefined);
|
|
488
576
|
});
|
|
489
577
|
|
|
490
578
|
test("requires service parameter", async () => {
|
|
@@ -496,55 +584,38 @@ describe("credential_store tool — oauth2_connect error paths", () => {
|
|
|
496
584
|
expect(result.content).toContain("service is required");
|
|
497
585
|
});
|
|
498
586
|
|
|
499
|
-
test("
|
|
500
|
-
const result = await credentialStoreTool.execute(
|
|
501
|
-
{
|
|
502
|
-
action: "oauth2_connect",
|
|
503
|
-
service: "custom-svc",
|
|
504
|
-
token_url: "https://t",
|
|
505
|
-
scopes: ["read"],
|
|
506
|
-
},
|
|
507
|
-
_ctx,
|
|
508
|
-
);
|
|
509
|
-
expect(result.isError).toBe(true);
|
|
510
|
-
expect(result.content).toContain("auth_url is required");
|
|
511
|
-
});
|
|
512
|
-
|
|
513
|
-
test("requires token_url for unknown service", async () => {
|
|
514
|
-
const result = await credentialStoreTool.execute(
|
|
515
|
-
{
|
|
516
|
-
action: "oauth2_connect",
|
|
517
|
-
service: "custom-svc",
|
|
518
|
-
auth_url: "https://a",
|
|
519
|
-
scopes: ["read"],
|
|
520
|
-
},
|
|
521
|
-
_ctx,
|
|
522
|
-
);
|
|
523
|
-
expect(result.isError).toBe(true);
|
|
524
|
-
expect(result.content).toContain("token_url is required");
|
|
525
|
-
});
|
|
526
|
-
|
|
527
|
-
test("requires scopes for unknown service", async () => {
|
|
587
|
+
test("rejects unknown service without registered provider", async () => {
|
|
528
588
|
const result = await credentialStoreTool.execute(
|
|
529
589
|
{
|
|
530
590
|
action: "oauth2_connect",
|
|
531
591
|
service: "custom-svc",
|
|
532
592
|
auth_url: "https://a",
|
|
533
593
|
token_url: "https://t",
|
|
594
|
+
scopes: ["read"],
|
|
534
595
|
},
|
|
535
596
|
_ctx,
|
|
536
597
|
);
|
|
537
598
|
expect(result.isError).toBe(true);
|
|
538
|
-
expect(result.content).toContain("
|
|
599
|
+
expect(result.content).toContain("no OAuth provider registered");
|
|
539
600
|
});
|
|
540
601
|
|
|
541
602
|
test("requires client_id", async () => {
|
|
603
|
+
mockGetProvider.mockImplementation((key: string) => {
|
|
604
|
+
if (key === "custom-svc") {
|
|
605
|
+
return {
|
|
606
|
+
key: "custom-svc",
|
|
607
|
+
authUrl: "https://auth.example.com",
|
|
608
|
+
tokenUrl: "https://token.example.com",
|
|
609
|
+
defaultScopes: JSON.stringify(["read"]),
|
|
610
|
+
scopePolicy: JSON.stringify({}),
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
return wellKnownProviders[key] ?? undefined;
|
|
614
|
+
});
|
|
542
615
|
const result = await credentialStoreTool.execute(
|
|
543
616
|
{
|
|
544
617
|
action: "oauth2_connect",
|
|
545
618
|
service: "custom-svc",
|
|
546
|
-
auth_url: "https://auth.example.com",
|
|
547
|
-
token_url: "https://token.example.com",
|
|
548
619
|
scopes: ["read"],
|
|
549
620
|
},
|
|
550
621
|
_ctx,
|
|
@@ -554,6 +625,21 @@ describe("credential_store tool — oauth2_connect error paths", () => {
|
|
|
554
625
|
});
|
|
555
626
|
|
|
556
627
|
test("requires interactive context", async () => {
|
|
628
|
+
// Register custom-svc as a provider so the orchestrator finds it
|
|
629
|
+
// and reaches the non-interactive check (gateway transport).
|
|
630
|
+
mockGetProvider.mockImplementation((key: string) => {
|
|
631
|
+
if (key === "custom-svc") {
|
|
632
|
+
return {
|
|
633
|
+
key: "custom-svc",
|
|
634
|
+
authUrl: "https://auth.example.com",
|
|
635
|
+
tokenUrl: "https://token.example.com",
|
|
636
|
+
defaultScopes: JSON.stringify(["read"]),
|
|
637
|
+
scopePolicy: JSON.stringify({}),
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
return wellKnownProviders[key] ?? undefined;
|
|
641
|
+
});
|
|
642
|
+
|
|
557
643
|
const result = await credentialStoreTool.execute(
|
|
558
644
|
{
|
|
559
645
|
action: "oauth2_connect",
|
|
@@ -596,18 +682,25 @@ describe("credential_store tool — oauth2_connect error paths", () => {
|
|
|
596
682
|
expect(result.content).toContain("client_id is required");
|
|
597
683
|
});
|
|
598
684
|
|
|
599
|
-
test("uses stored client_id from
|
|
600
|
-
//
|
|
601
|
-
// in the secure store
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
"
|
|
610
|
-
|
|
685
|
+
test("uses stored client_id from oauth-store DB", async () => {
|
|
686
|
+
// Mock getMostRecentAppByProvider to return an app with a client_id
|
|
687
|
+
// and store client_secret in the secure store.
|
|
688
|
+
mockGetMostRecentAppByProvider.mockImplementation(() => ({
|
|
689
|
+
id: "test-app-id",
|
|
690
|
+
providerKey: "integration:gmail",
|
|
691
|
+
clientId: "stored-client-id-123",
|
|
692
|
+
createdAt: Date.now(),
|
|
693
|
+
}));
|
|
694
|
+
mockGetProvider.mockImplementation(() => ({
|
|
695
|
+
key: "integration:gmail",
|
|
696
|
+
authUrl: "https://accounts.google.com/o/oauth2/v2/auth",
|
|
697
|
+
tokenUrl: "https://oauth2.googleapis.com/token",
|
|
698
|
+
defaultScopes: JSON.stringify(["https://mail.google.com/"]),
|
|
699
|
+
scopePolicy: JSON.stringify({}),
|
|
700
|
+
callbackTransport: "loopback",
|
|
701
|
+
loopbackPort: 8756,
|
|
702
|
+
}));
|
|
703
|
+
setSecureKey("oauth_app/test-app-id/client_secret", "test-secret");
|
|
611
704
|
|
|
612
705
|
const result = await credentialStoreTool.execute(
|
|
613
706
|
{
|
|
@@ -624,14 +717,148 @@ describe("credential_store tool — oauth2_connect error paths", () => {
|
|
|
624
717
|
expect(result.content).toContain("To connect gmail, open this link");
|
|
625
718
|
expect(result.content).not.toContain("client_id is required");
|
|
626
719
|
expect(result.content).not.toContain("client_secret is required");
|
|
720
|
+
|
|
721
|
+
// Reset mocks
|
|
722
|
+
mockGetMostRecentAppByProvider.mockImplementation(() => undefined);
|
|
723
|
+
mockGetProvider.mockImplementation(() => undefined);
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
test("uses getAppByProviderAndClientId when client_id is provided without client_secret", async () => {
|
|
727
|
+
// When client_id is supplied but client_secret is not, the vault should
|
|
728
|
+
// look up the matching app via getAppByProviderAndClientId (not the
|
|
729
|
+
// most-recent-app heuristic) so the secret comes from the correct app.
|
|
730
|
+
mockGetAppByProviderAndClientId.mockImplementation(
|
|
731
|
+
(providerKey: string, cId: string) => {
|
|
732
|
+
if (
|
|
733
|
+
providerKey === "integration:gmail" &&
|
|
734
|
+
cId === "caller-supplied-client-id"
|
|
735
|
+
) {
|
|
736
|
+
return {
|
|
737
|
+
id: "matched-app-id",
|
|
738
|
+
providerKey: "integration:gmail",
|
|
739
|
+
clientId: "caller-supplied-client-id",
|
|
740
|
+
createdAt: Date.now(),
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
return undefined;
|
|
744
|
+
},
|
|
745
|
+
);
|
|
746
|
+
mockGetProvider.mockImplementation(() => ({
|
|
747
|
+
key: "integration:gmail",
|
|
748
|
+
authUrl: "https://accounts.google.com/o/oauth2/v2/auth",
|
|
749
|
+
tokenUrl: "https://oauth2.googleapis.com/token",
|
|
750
|
+
defaultScopes: JSON.stringify(["https://mail.google.com/"]),
|
|
751
|
+
scopePolicy: JSON.stringify({}),
|
|
752
|
+
callbackTransport: "loopback",
|
|
753
|
+
loopbackPort: 8756,
|
|
754
|
+
}));
|
|
755
|
+
setSecureKey("oauth_app/matched-app-id/client_secret", "matched-secret");
|
|
756
|
+
|
|
757
|
+
const result = await credentialStoreTool.execute(
|
|
758
|
+
{
|
|
759
|
+
action: "oauth2_connect",
|
|
760
|
+
service: "gmail",
|
|
761
|
+
client_id: "caller-supplied-client-id",
|
|
762
|
+
},
|
|
763
|
+
{ ..._ctx, isInteractive: false },
|
|
764
|
+
);
|
|
765
|
+
|
|
766
|
+
// Should succeed — client_secret resolved from the matched app
|
|
767
|
+
expect(result.isError).toBe(false);
|
|
768
|
+
expect(result.content).toContain("To connect gmail, open this link");
|
|
769
|
+
// getMostRecentAppByProvider should NOT have been called since client_id was known
|
|
770
|
+
expect(mockGetMostRecentAppByProvider).not.toHaveBeenCalled();
|
|
771
|
+
|
|
772
|
+
// Reset mocks
|
|
773
|
+
mockGetAppByProviderAndClientId.mockImplementation(() => undefined);
|
|
774
|
+
mockGetProvider.mockImplementation(() => undefined);
|
|
775
|
+
});
|
|
776
|
+
|
|
777
|
+
test("falls back to getMostRecentAppByProvider when client_id is not provided", async () => {
|
|
778
|
+
// When neither client_id nor client_secret is provided, the vault should
|
|
779
|
+
// use getMostRecentAppByProvider (the fallback heuristic).
|
|
780
|
+
mockGetMostRecentAppByProvider.mockImplementation(() => ({
|
|
781
|
+
id: "recent-app-id",
|
|
782
|
+
providerKey: "integration:gmail",
|
|
783
|
+
clientId: "recent-client-id",
|
|
784
|
+
createdAt: Date.now(),
|
|
785
|
+
}));
|
|
786
|
+
mockGetProvider.mockImplementation(() => ({
|
|
787
|
+
key: "integration:gmail",
|
|
788
|
+
authUrl: "https://accounts.google.com/o/oauth2/v2/auth",
|
|
789
|
+
tokenUrl: "https://oauth2.googleapis.com/token",
|
|
790
|
+
defaultScopes: JSON.stringify(["https://mail.google.com/"]),
|
|
791
|
+
scopePolicy: JSON.stringify({}),
|
|
792
|
+
callbackTransport: "loopback",
|
|
793
|
+
loopbackPort: 8756,
|
|
794
|
+
}));
|
|
795
|
+
setSecureKey("oauth_app/recent-app-id/client_secret", "recent-secret");
|
|
796
|
+
|
|
797
|
+
const result = await credentialStoreTool.execute(
|
|
798
|
+
{
|
|
799
|
+
action: "oauth2_connect",
|
|
800
|
+
service: "gmail",
|
|
801
|
+
},
|
|
802
|
+
{ ..._ctx, isInteractive: false },
|
|
803
|
+
);
|
|
804
|
+
|
|
805
|
+
expect(result.isError).toBe(false);
|
|
806
|
+
expect(result.content).toContain("To connect gmail, open this link");
|
|
807
|
+
// getAppByProviderAndClientId should NOT have been called since client_id was unknown
|
|
808
|
+
expect(mockGetAppByProviderAndClientId).not.toHaveBeenCalled();
|
|
809
|
+
|
|
810
|
+
// Reset mocks
|
|
811
|
+
mockGetMostRecentAppByProvider.mockImplementation(() => undefined);
|
|
812
|
+
mockGetProvider.mockImplementation(() => undefined);
|
|
813
|
+
});
|
|
814
|
+
|
|
815
|
+
test("getAppByProviderAndClientId returning undefined leaves client_secret unresolved", async () => {
|
|
816
|
+
// When client_id is provided but getAppByProviderAndClientId returns no
|
|
817
|
+
// matching app, client_secret remains unresolved and the vault should
|
|
818
|
+
// report the missing secret error.
|
|
819
|
+
mockGetAppByProviderAndClientId.mockImplementation(() => undefined);
|
|
820
|
+
mockGetProvider.mockImplementation(() => ({
|
|
821
|
+
key: "integration:gmail",
|
|
822
|
+
authUrl: "https://accounts.google.com/o/oauth2/v2/auth",
|
|
823
|
+
tokenUrl: "https://oauth2.googleapis.com/token",
|
|
824
|
+
defaultScopes: JSON.stringify(["https://mail.google.com/"]),
|
|
825
|
+
}));
|
|
826
|
+
|
|
827
|
+
const result = await credentialStoreTool.execute(
|
|
828
|
+
{
|
|
829
|
+
action: "oauth2_connect",
|
|
830
|
+
service: "gmail",
|
|
831
|
+
client_id: "unknown-client-id",
|
|
832
|
+
},
|
|
833
|
+
_ctx,
|
|
834
|
+
);
|
|
835
|
+
|
|
836
|
+
expect(result.isError).toBe(true);
|
|
837
|
+
expect(result.content).toContain("client_secret is required for gmail");
|
|
838
|
+
// getMostRecentAppByProvider should NOT have been called
|
|
839
|
+
expect(mockGetMostRecentAppByProvider).not.toHaveBeenCalled();
|
|
840
|
+
|
|
841
|
+
// Reset mocks
|
|
842
|
+
mockGetAppByProviderAndClientId.mockImplementation(() => undefined);
|
|
843
|
+
mockGetProvider.mockImplementation(() => undefined);
|
|
627
844
|
});
|
|
628
845
|
|
|
629
846
|
test("rejects when client_secret is missing for service that requires it", async () => {
|
|
630
|
-
//
|
|
631
|
-
//
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
847
|
+
// Mock getMostRecentAppByProvider to return an app with client_id but
|
|
848
|
+
// no client_secret in secure storage — validates the requiresClientSecret
|
|
849
|
+
// guardrail.
|
|
850
|
+
mockGetMostRecentAppByProvider.mockImplementation(() => ({
|
|
851
|
+
id: "test-app-id-no-secret",
|
|
852
|
+
providerKey: "integration:gmail",
|
|
853
|
+
clientId: "stored-client-id-456",
|
|
854
|
+
createdAt: Date.now(),
|
|
855
|
+
}));
|
|
856
|
+
mockGetProvider.mockImplementation(() => ({
|
|
857
|
+
key: "integration:gmail",
|
|
858
|
+
authUrl: "https://accounts.google.com/o/oauth2/v2/auth",
|
|
859
|
+
tokenUrl: "https://oauth2.googleapis.com/token",
|
|
860
|
+
defaultScopes: JSON.stringify(["https://mail.google.com/"]),
|
|
861
|
+
}));
|
|
635
862
|
|
|
636
863
|
const result = await credentialStoreTool.execute(
|
|
637
864
|
{
|
|
@@ -643,6 +870,10 @@ describe("credential_store tool — oauth2_connect error paths", () => {
|
|
|
643
870
|
|
|
644
871
|
expect(result.isError).toBe(true);
|
|
645
872
|
expect(result.content).toContain("client_secret is required for gmail");
|
|
873
|
+
|
|
874
|
+
// Reset mocks
|
|
875
|
+
mockGetMostRecentAppByProvider.mockImplementation(() => undefined);
|
|
876
|
+
mockGetProvider.mockImplementation(() => undefined);
|
|
646
877
|
});
|
|
647
878
|
});
|
|
648
879
|
|
|
@@ -65,6 +65,58 @@ mock.module("../security/oauth2.js", () => {
|
|
|
65
65
|
};
|
|
66
66
|
});
|
|
67
67
|
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
// Mock oauth-store — token-manager reads refresh config from SQLite
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
|
|
72
|
+
/** Mutable per-test map of provider connections for getConnectionByProvider */
|
|
73
|
+
const mockConnections = new Map<
|
|
74
|
+
string,
|
|
75
|
+
{
|
|
76
|
+
id: string;
|
|
77
|
+
providerKey: string;
|
|
78
|
+
oauthAppId: string;
|
|
79
|
+
expiresAt: number | null;
|
|
80
|
+
}
|
|
81
|
+
>();
|
|
82
|
+
const mockApps = new Map<
|
|
83
|
+
string,
|
|
84
|
+
{ id: string; providerKey: string; clientId: string }
|
|
85
|
+
>();
|
|
86
|
+
const mockProviders = new Map<
|
|
87
|
+
string,
|
|
88
|
+
{
|
|
89
|
+
key: string;
|
|
90
|
+
tokenUrl: string;
|
|
91
|
+
tokenEndpointAuthMethod?: string;
|
|
92
|
+
}
|
|
93
|
+
>();
|
|
94
|
+
|
|
95
|
+
let mockDisconnectOAuthProvider: ReturnType<
|
|
96
|
+
typeof mock<
|
|
97
|
+
(providerKey: string) => Promise<"disconnected" | "not-found" | "error">
|
|
98
|
+
>
|
|
99
|
+
>;
|
|
100
|
+
|
|
101
|
+
mock.module("../oauth/oauth-store.js", () => {
|
|
102
|
+
mockDisconnectOAuthProvider = mock((providerKey: string) =>
|
|
103
|
+
Promise.resolve(
|
|
104
|
+
mockConnections.has(providerKey)
|
|
105
|
+
? ("disconnected" as const)
|
|
106
|
+
: ("not-found" as const),
|
|
107
|
+
),
|
|
108
|
+
);
|
|
109
|
+
return {
|
|
110
|
+
disconnectOAuthProvider: mockDisconnectOAuthProvider,
|
|
111
|
+
getConnectionByProvider: (service: string) => mockConnections.get(service),
|
|
112
|
+
getApp: (id: string) => mockApps.get(id),
|
|
113
|
+
getProvider: (key: string) => mockProviders.get(key),
|
|
114
|
+
updateConnection: () => {},
|
|
115
|
+
getMostRecentAppByProvider: () => undefined,
|
|
116
|
+
listConnections: () => [],
|
|
117
|
+
};
|
|
118
|
+
});
|
|
119
|
+
|
|
68
120
|
// ---------------------------------------------------------------------------
|
|
69
121
|
// Import the module under test
|
|
70
122
|
// ---------------------------------------------------------------------------
|
|
@@ -85,7 +137,6 @@ import {
|
|
|
85
137
|
import {
|
|
86
138
|
_setMetadataPath,
|
|
87
139
|
getCredentialMetadata,
|
|
88
|
-
upsertCredentialMetadata,
|
|
89
140
|
} from "../tools/credentials/metadata-store.js";
|
|
90
141
|
import { credentialStoreTool } from "../tools/credentials/vault.js";
|
|
91
142
|
import type { ToolContext } from "../tools/types.js";
|
|
@@ -214,12 +265,15 @@ describe("credential_store tool", () => {
|
|
|
214
265
|
}
|
|
215
266
|
_setStorePath(STORE_PATH);
|
|
216
267
|
_setMetadataPath(join(TEST_DIR, "metadata.json"));
|
|
268
|
+
mockDisconnectOAuthProvider.mockClear();
|
|
269
|
+
mockConnections.clear();
|
|
217
270
|
});
|
|
218
271
|
|
|
219
272
|
afterEach(() => {
|
|
220
273
|
_setMetadataPath(null);
|
|
221
274
|
_setStorePath(null);
|
|
222
275
|
_resetBackend();
|
|
276
|
+
mockConnections.clear();
|
|
223
277
|
});
|
|
224
278
|
|
|
225
279
|
afterAll(() => {
|
|
@@ -664,6 +718,44 @@ describe("credential_store tool", () => {
|
|
|
664
718
|
expect(result.isError).toBe(true);
|
|
665
719
|
expect(result.content).toContain("field is required");
|
|
666
720
|
});
|
|
721
|
+
|
|
722
|
+
test("delete also disconnects OAuth connection for the service", async () => {
|
|
723
|
+
// Store a credential via the real tool so metadata exists
|
|
724
|
+
await credentialStoreTool.execute(
|
|
725
|
+
{
|
|
726
|
+
action: "store",
|
|
727
|
+
service: "integration:gmail",
|
|
728
|
+
field: "api_key",
|
|
729
|
+
value: "test-value",
|
|
730
|
+
},
|
|
731
|
+
_ctx,
|
|
732
|
+
);
|
|
733
|
+
|
|
734
|
+
// Simulate an active OAuth connection for this service
|
|
735
|
+
mockConnections.set("integration:gmail", {
|
|
736
|
+
id: "conn-gmail",
|
|
737
|
+
providerKey: "integration:gmail",
|
|
738
|
+
oauthAppId: "app-gmail",
|
|
739
|
+
expiresAt: Date.now() + 3600_000,
|
|
740
|
+
});
|
|
741
|
+
|
|
742
|
+
const result = await credentialStoreTool.execute(
|
|
743
|
+
{
|
|
744
|
+
action: "delete",
|
|
745
|
+
service: "integration:gmail",
|
|
746
|
+
field: "api_key",
|
|
747
|
+
},
|
|
748
|
+
_ctx,
|
|
749
|
+
);
|
|
750
|
+
|
|
751
|
+
expect(result.isError).toBe(false);
|
|
752
|
+
expect(result.content).toContain("Deleted credential");
|
|
753
|
+
// Verify disconnectOAuthProvider was called with the service name
|
|
754
|
+
expect(mockDisconnectOAuthProvider).toHaveBeenCalledTimes(1);
|
|
755
|
+
expect(mockDisconnectOAuthProvider).toHaveBeenCalledWith(
|
|
756
|
+
"integration:gmail",
|
|
757
|
+
);
|
|
758
|
+
});
|
|
667
759
|
});
|
|
668
760
|
|
|
669
761
|
// -----------------------------------------------------------------------
|
|
@@ -1172,6 +1264,10 @@ describe("withValidToken refresh deduplication", () => {
|
|
|
1172
1264
|
_resetRefreshBreakers();
|
|
1173
1265
|
_resetInflightRefreshes();
|
|
1174
1266
|
mockRefreshOAuth2Token.mockClear();
|
|
1267
|
+
// Clear mock oauth-store maps
|
|
1268
|
+
mockConnections.clear();
|
|
1269
|
+
mockApps.clear();
|
|
1270
|
+
mockProviders.clear();
|
|
1175
1271
|
});
|
|
1176
1272
|
|
|
1177
1273
|
afterEach(() => {
|
|
@@ -1180,6 +1276,9 @@ describe("withValidToken refresh deduplication", () => {
|
|
|
1180
1276
|
_resetBackend();
|
|
1181
1277
|
_resetRefreshBreakers();
|
|
1182
1278
|
_resetInflightRefreshes();
|
|
1279
|
+
mockConnections.clear();
|
|
1280
|
+
mockApps.clear();
|
|
1281
|
+
mockProviders.clear();
|
|
1183
1282
|
});
|
|
1184
1283
|
|
|
1185
1284
|
afterAll(() => {
|
|
@@ -1187,26 +1286,48 @@ describe("withValidToken refresh deduplication", () => {
|
|
|
1187
1286
|
});
|
|
1188
1287
|
|
|
1189
1288
|
/**
|
|
1190
|
-
* Helper: set up a service with an access token, refresh token, and
|
|
1191
|
-
*
|
|
1289
|
+
* Helper: set up a service with an access token, refresh token, and
|
|
1290
|
+
* mock DB data so that token refresh can proceed through doRefresh().
|
|
1291
|
+
*
|
|
1292
|
+
* OAuth-specific fields (tokenUrl, clientId, expiresAt) are now stored
|
|
1293
|
+
* in the SQLite oauth-store. The mock maps simulate the DB layer.
|
|
1192
1294
|
*/
|
|
1193
1295
|
function setupService(
|
|
1194
1296
|
service: string,
|
|
1195
1297
|
opts?: { expired?: boolean; accessToken?: string },
|
|
1196
1298
|
) {
|
|
1197
1299
|
const accessToken = opts?.accessToken ?? "old-access-token";
|
|
1198
|
-
|
|
1300
|
+
|
|
1301
|
+
// Seed mock oauth-store maps so token-manager can resolve refresh config
|
|
1302
|
+
const appId = `app-${service}`;
|
|
1303
|
+
const connId = `conn-${service}`;
|
|
1304
|
+
|
|
1305
|
+
// Store access token under the oauth_connection key path that
|
|
1306
|
+
// withValidToken reads (not the legacy credentialKey path).
|
|
1307
|
+
setSecureKey(`oauth_connection/${connId}/access_token`, accessToken);
|
|
1308
|
+
mockProviders.set(service, {
|
|
1309
|
+
key: service,
|
|
1310
|
+
tokenUrl: "https://oauth.example.com/token",
|
|
1311
|
+
});
|
|
1312
|
+
mockApps.set(appId, {
|
|
1313
|
+
id: appId,
|
|
1314
|
+
providerKey: service,
|
|
1315
|
+
clientId: "test-client-id",
|
|
1316
|
+
});
|
|
1317
|
+
mockConnections.set(service, {
|
|
1318
|
+
id: connId,
|
|
1319
|
+
providerKey: service,
|
|
1320
|
+
oauthAppId: appId,
|
|
1321
|
+
expiresAt: opts?.expired
|
|
1322
|
+
? Date.now() - 60_000 // expired 1 minute ago
|
|
1323
|
+
: Date.now() + 3600_000, // expires in 1 hour
|
|
1324
|
+
});
|
|
1325
|
+
// Store refresh token and client_secret in secure keys (token-manager reads them)
|
|
1199
1326
|
setSecureKey(
|
|
1200
|
-
|
|
1327
|
+
`oauth_connection/${connId}/refresh_token`,
|
|
1201
1328
|
"valid-refresh-token",
|
|
1202
1329
|
);
|
|
1203
|
-
|
|
1204
|
-
oauth2TokenUrl: "https://oauth.example.com/token",
|
|
1205
|
-
oauth2ClientId: "test-client-id",
|
|
1206
|
-
...(opts?.expired
|
|
1207
|
-
? { expiresAt: Date.now() - 60_000 } // expired 1 minute ago
|
|
1208
|
-
: { expiresAt: Date.now() + 3600_000 }), // expires in 1 hour
|
|
1209
|
-
});
|
|
1330
|
+
setSecureKey(`oauth_app/${appId}/client_secret`, "test-client-secret");
|
|
1210
1331
|
}
|
|
1211
1332
|
|
|
1212
1333
|
test("3 concurrent 401 refreshes for the same service call doRefresh exactly once", async () => {
|
|
@@ -1335,22 +1456,23 @@ describe("withValidToken refresh deduplication", () => {
|
|
|
1335
1456
|
|
|
1336
1457
|
const err401 = Object.assign(new Error("Unauthorized"), { status: 401 });
|
|
1337
1458
|
|
|
1338
|
-
// First call triggers a refresh
|
|
1459
|
+
// First call triggers a refresh (old token → 401 → refresh → token-1)
|
|
1339
1460
|
const r1 = await withValidToken(
|
|
1340
1461
|
"integration:gmail",
|
|
1341
1462
|
async (token: string) => {
|
|
1342
|
-
if (token
|
|
1463
|
+
if (token !== "token-1") throw err401;
|
|
1343
1464
|
return token;
|
|
1344
1465
|
},
|
|
1345
1466
|
);
|
|
1346
1467
|
expect(r1).toBe("token-1");
|
|
1347
1468
|
expect(refreshCount).toBe(1);
|
|
1348
1469
|
|
|
1349
|
-
//
|
|
1470
|
+
// Second call also triggers a 401 to verify dedup state was cleaned up
|
|
1471
|
+
// and a new refresh is allowed (not deduplicated with the first).
|
|
1350
1472
|
const r2 = await withValidToken(
|
|
1351
1473
|
"integration:gmail",
|
|
1352
1474
|
async (token: string) => {
|
|
1353
|
-
if (token
|
|
1475
|
+
if (token !== "token-2") throw err401;
|
|
1354
1476
|
return token;
|
|
1355
1477
|
},
|
|
1356
1478
|
);
|