vellum 0.2.13 → 0.2.14
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/README.md +32 -0
- package/bun.lock +2 -2
- package/docs/skills.md +4 -4
- package/package.json +2 -2
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +213 -3
- package/src/__tests__/app-git-history.test.ts +176 -0
- package/src/__tests__/app-git-service.test.ts +169 -0
- package/src/__tests__/assistant-events-sse-hardening.test.ts +315 -0
- package/src/__tests__/browser-skill-baseline-tool-payload.test.ts +8 -8
- package/src/__tests__/browser-skill-endstate.test.ts +6 -6
- package/src/__tests__/call-bridge.test.ts +105 -13
- package/src/__tests__/call-domain.test.ts +163 -0
- package/src/__tests__/call-orchestrator.test.ts +113 -0
- package/src/__tests__/call-routes-http.test.ts +246 -6
- package/src/__tests__/channel-approval-routes.test.ts +438 -0
- package/src/__tests__/channel-approval.test.ts +266 -0
- package/src/__tests__/channel-approvals.test.ts +393 -0
- package/src/__tests__/channel-delivery-store.test.ts +447 -0
- package/src/__tests__/checker.test.ts +607 -1048
- package/src/__tests__/cli.test.ts +1 -56
- package/src/__tests__/config-schema.test.ts +137 -18
- package/src/__tests__/conflict-intent-tokenization.test.ts +141 -0
- package/src/__tests__/conflict-policy.test.ts +121 -0
- package/src/__tests__/conflict-store.test.ts +2 -0
- package/src/__tests__/contacts-tools.test.ts +3 -3
- package/src/__tests__/contradiction-checker.test.ts +99 -1
- package/src/__tests__/credential-security-invariants.test.ts +22 -6
- package/src/__tests__/credential-vault-unit.test.ts +780 -0
- package/src/__tests__/elevenlabs-client.test.ts +62 -0
- package/src/__tests__/ephemeral-permissions.test.ts +73 -23
- package/src/__tests__/filesystem-tools.test.ts +579 -0
- package/src/__tests__/gateway-only-enforcement.test.ts +114 -4
- package/src/__tests__/handlers-add-trust-rule-metadata.test.ts +202 -0
- package/src/__tests__/handlers-cu-observation-blob.test.ts +2 -1
- package/src/__tests__/handlers-ipc-blob-probe.test.ts +2 -1
- package/src/__tests__/handlers-slack-config.test.ts +2 -1
- package/src/__tests__/handlers-telegram-config.test.ts +855 -0
- package/src/__tests__/handlers-twitter-config.test.ts +141 -1
- package/src/__tests__/hooks-runner.test.ts +6 -2
- package/src/__tests__/host-file-edit-tool.test.ts +124 -0
- package/src/__tests__/host-file-read-tool.test.ts +62 -0
- package/src/__tests__/host-file-write-tool.test.ts +59 -0
- package/src/__tests__/host-shell-tool.test.ts +251 -0
- package/src/__tests__/ingress-reconcile.test.ts +581 -0
- package/src/__tests__/ipc-snapshot.test.ts +100 -41
- package/src/__tests__/ipc-validate.test.ts +50 -0
- package/src/__tests__/key-migration.test.ts +23 -0
- package/src/__tests__/memory-regressions.test.ts +99 -0
- package/src/__tests__/memory-retrieval.benchmark.test.ts +1 -1
- package/src/__tests__/oauth-callback-registry.test.ts +11 -4
- package/src/__tests__/playbook-execution.test.ts +502 -0
- package/src/__tests__/playbook-tools.test.ts +4 -6
- package/src/__tests__/public-ingress-urls.test.ts +34 -0
- package/src/__tests__/qdrant-manager.test.ts +267 -0
- package/src/__tests__/recurrence-engine-rruleset.test.ts +97 -0
- package/src/__tests__/recurrence-engine.test.ts +9 -0
- package/src/__tests__/recurrence-types.test.ts +8 -0
- package/src/__tests__/registry.test.ts +1 -1
- package/src/__tests__/runtime-runs.test.ts +1 -25
- package/src/__tests__/schedule-store.test.ts +16 -14
- package/src/__tests__/schedule-tools.test.ts +83 -0
- package/src/__tests__/scheduler-recurrence.test.ts +111 -10
- package/src/__tests__/secret-allowlist.test.ts +18 -17
- package/src/__tests__/secret-ingress-handler.test.ts +11 -0
- package/src/__tests__/secret-scanner.test.ts +43 -0
- package/src/__tests__/session-conflict-gate.test.ts +442 -6
- package/src/__tests__/session-init.benchmark.test.ts +3 -0
- package/src/__tests__/session-process-bridge.test.ts +242 -0
- package/src/__tests__/session-skill-tools.test.ts +1 -1
- package/src/__tests__/shell-identity.test.ts +256 -0
- package/src/__tests__/skill-projection.benchmark.test.ts +11 -1
- package/src/__tests__/subagent-tools.test.ts +637 -54
- package/src/__tests__/task-management-tools.test.ts +936 -0
- package/src/__tests__/task-runner.test.ts +2 -2
- package/src/__tests__/terminal-tools.test.ts +840 -0
- package/src/__tests__/tool-executor-shell-integration.test.ts +301 -0
- package/src/__tests__/tool-executor.test.ts +85 -151
- package/src/__tests__/tool-permission-simulate-handler.test.ts +336 -0
- package/src/__tests__/trust-store.test.ts +27 -453
- package/src/__tests__/twilio-provider.test.ts +153 -3
- package/src/__tests__/twilio-routes-elevenlabs.test.ts +375 -0
- package/src/__tests__/twilio-routes-twiml.test.ts +4 -4
- package/src/__tests__/twilio-routes.test.ts +17 -262
- package/src/__tests__/twitter-auth-handler.test.ts +2 -1
- package/src/__tests__/twitter-cli-error-shaping.test.ts +208 -0
- package/src/__tests__/twitter-cli-routing.test.ts +252 -0
- package/src/__tests__/twitter-oauth-client.test.ts +209 -0
- package/src/__tests__/workspace-policy.test.ts +213 -0
- package/src/calls/call-bridge.ts +92 -19
- package/src/calls/call-domain.ts +157 -5
- package/src/calls/call-orchestrator.ts +93 -7
- package/src/calls/call-store.ts +6 -0
- package/src/calls/elevenlabs-client.ts +8 -0
- package/src/calls/elevenlabs-config.ts +7 -5
- package/src/calls/twilio-provider.ts +91 -0
- package/src/calls/twilio-routes.ts +32 -37
- package/src/calls/types.ts +3 -1
- package/src/calls/voice-quality.ts +29 -7
- package/src/cli/twitter.ts +200 -21
- package/src/cli.ts +1 -20
- package/src/config/bundled-skills/contacts/tools/contact-merge.ts +52 -4
- package/src/config/bundled-skills/contacts/tools/contact-search.ts +55 -4
- package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +61 -4
- package/src/config/bundled-skills/messaging/SKILL.md +17 -2
- package/src/config/bundled-skills/messaging/tools/messaging-reply.ts +4 -1
- package/src/config/bundled-skills/messaging/tools/messaging-send.ts +5 -1
- package/src/config/bundled-skills/messaging/tools/shared.ts +5 -0
- package/src/config/bundled-skills/phone-calls/SKILL.md +142 -34
- package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +95 -6
- package/src/config/bundled-skills/playbooks/tools/playbook-delete.ts +51 -6
- package/src/config/bundled-skills/playbooks/tools/playbook-list.ts +73 -6
- package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +110 -6
- package/src/config/bundled-skills/public-ingress/SKILL.md +22 -5
- package/src/config/bundled-skills/twitter/SKILL.md +103 -17
- package/src/config/defaults.ts +10 -4
- package/src/config/schema.ts +80 -21
- package/src/config/types.ts +1 -0
- package/src/config/vellum-skills/telegram-setup/SKILL.md +56 -61
- package/src/daemon/assistant-attachments.ts +4 -2
- package/src/daemon/handlers/apps.ts +69 -0
- package/src/daemon/handlers/config.ts +543 -24
- package/src/daemon/handlers/index.ts +1 -0
- package/src/daemon/handlers/sessions.ts +22 -6
- package/src/daemon/handlers/shared.ts +2 -1
- package/src/daemon/handlers/skills.ts +5 -20
- package/src/daemon/ipc-contract-inventory.json +28 -0
- package/src/daemon/ipc-contract.ts +168 -10
- package/src/daemon/ipc-validate.ts +17 -0
- package/src/daemon/lifecycle.ts +2 -0
- package/src/daemon/server.ts +78 -72
- package/src/daemon/session-attachments.ts +1 -1
- package/src/daemon/session-conflict-gate.ts +62 -6
- package/src/daemon/session-notifiers.ts +1 -1
- package/src/daemon/session-process.ts +62 -3
- package/src/daemon/session-tool-setup.ts +1 -2
- package/src/daemon/tls-certs.ts +189 -0
- package/src/daemon/video-thumbnail.ts +5 -3
- package/src/hooks/manager.ts +5 -9
- package/src/memory/app-git-service.ts +295 -0
- package/src/memory/app-store.ts +21 -0
- package/src/memory/conflict-intent.ts +47 -4
- package/src/memory/conflict-policy.ts +73 -0
- package/src/memory/conflict-store.ts +9 -1
- package/src/memory/contradiction-checker.ts +28 -0
- package/src/memory/conversation-key-store.ts +15 -0
- package/src/memory/db.ts +81 -0
- package/src/memory/embedding-local.ts +3 -13
- package/src/memory/external-conversation-store.ts +234 -0
- package/src/memory/job-handlers/conflict.ts +22 -2
- package/src/memory/jobs-worker.ts +67 -28
- package/src/memory/runs-store.ts +54 -7
- package/src/memory/schema.ts +20 -0
- package/src/messaging/provider.ts +9 -0
- package/src/messaging/providers/telegram-bot/adapter.ts +162 -0
- package/src/messaging/providers/telegram-bot/client.ts +104 -0
- package/src/messaging/providers/telegram-bot/types.ts +15 -0
- package/src/messaging/registry.ts +1 -0
- package/src/permissions/checker.ts +48 -44
- package/src/permissions/prompter.ts +0 -4
- package/src/permissions/shell-identity.ts +227 -0
- package/src/permissions/trust-store.ts +76 -53
- package/src/permissions/types.ts +0 -19
- package/src/permissions/workspace-policy.ts +114 -0
- package/src/providers/retry.ts +12 -37
- package/src/runtime/assistant-event-hub.ts +41 -4
- package/src/runtime/channel-approval-parser.ts +60 -0
- package/src/runtime/channel-approval-types.ts +71 -0
- package/src/runtime/channel-approvals.ts +145 -0
- package/src/runtime/gateway-client.ts +16 -0
- package/src/runtime/http-server.ts +29 -9
- package/src/runtime/routes/call-routes.ts +52 -2
- package/src/runtime/routes/channel-routes.ts +296 -16
- package/src/runtime/routes/events-routes.ts +97 -28
- package/src/runtime/routes/run-routes.ts +2 -7
- package/src/runtime/run-orchestrator.ts +0 -3
- package/src/schedule/recurrence-engine.ts +26 -2
- package/src/schedule/recurrence-types.ts +1 -1
- package/src/schedule/schedule-store.ts +12 -3
- package/src/security/secret-scanner.ts +7 -0
- package/src/tasks/ephemeral-permissions.ts +0 -2
- package/src/tasks/task-scheduler.ts +2 -1
- package/src/tools/calls/call-start.ts +8 -0
- package/src/tools/execution-target.ts +21 -0
- package/src/tools/execution-timeout.ts +49 -0
- package/src/tools/executor.ts +6 -135
- package/src/tools/network/web-search.ts +9 -32
- package/src/tools/policy-context.ts +29 -0
- package/src/tools/schedule/update.ts +8 -1
- package/src/tools/terminal/parser.ts +16 -18
- package/src/tools/types.ts +4 -11
- package/src/twitter/oauth-client.ts +102 -0
- package/src/twitter/router.ts +101 -0
- package/src/util/debounce.ts +88 -0
- package/src/util/network-info.ts +47 -0
- package/src/util/platform.ts +29 -4
- package/src/util/promise-guard.ts +37 -0
- package/src/util/retry.ts +98 -0
- package/src/util/truncate.ts +1 -1
- package/src/workspace/git-service.ts +129 -112
- package/src/tools/contacts/contact-merge.ts +0 -55
- package/src/tools/contacts/contact-search.ts +0 -58
- package/src/tools/contacts/contact-upsert.ts +0 -64
- package/src/tools/playbooks/index.ts +0 -4
- package/src/tools/playbooks/playbook-create.ts +0 -96
- package/src/tools/playbooks/playbook-delete.ts +0 -52
- package/src/tools/playbooks/playbook-list.ts +0 -74
- package/src/tools/playbooks/playbook-update.ts +0 -111
|
@@ -0,0 +1,780 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach, afterEach, afterAll, mock } from 'bun:test';
|
|
2
|
+
import { mkdirSync, rmSync, existsSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { randomBytes } from 'node:crypto';
|
|
6
|
+
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
// Mock logger
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
|
|
11
|
+
mock.module('../util/logger.js', () => ({
|
|
12
|
+
getLogger: () => new Proxy({} as Record<string, unknown>, {
|
|
13
|
+
get: () => () => {},
|
|
14
|
+
}),
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// Use encrypted backend (no keychain) with a temp store path
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
import { _overrideDeps, _resetDeps } from '../security/keychain.js';
|
|
22
|
+
|
|
23
|
+
_overrideDeps({
|
|
24
|
+
isMacOS: () => false,
|
|
25
|
+
isLinux: () => false,
|
|
26
|
+
execFileSync: (() => '') as unknown as typeof import('node:child_process').execFileSync,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
import { _resetBackend } from '../security/secure-keys.js';
|
|
30
|
+
import { _setStorePath } from '../security/encrypted-store.js';
|
|
31
|
+
|
|
32
|
+
const TEST_DIR = join(tmpdir(), `vellum-credvault-unit-${randomBytes(4).toString('hex')}`);
|
|
33
|
+
const STORE_PATH = join(TEST_DIR, 'keys.enc');
|
|
34
|
+
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
// Mock registry to avoid double-registration
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
mock.module('../tools/registry.js', () => ({
|
|
40
|
+
registerTool: () => {},
|
|
41
|
+
}));
|
|
42
|
+
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
// Imports under test
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
import { CredentialBroker } from '../tools/credentials/broker.js';
|
|
48
|
+
import { upsertCredentialMetadata, _setMetadataPath } from '../tools/credentials/metadata-store.js';
|
|
49
|
+
import { setSecureKey, getSecureKey } from '../security/secure-keys.js';
|
|
50
|
+
import { credentialStoreTool } from '../tools/credentials/vault.js';
|
|
51
|
+
import type { ToolContext } from '../tools/types.js';
|
|
52
|
+
|
|
53
|
+
const _ctx: ToolContext = {
|
|
54
|
+
workingDir: '/tmp',
|
|
55
|
+
sessionId: 'test-session',
|
|
56
|
+
conversationId: 'test-conv',
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
afterAll(() => {
|
|
60
|
+
_resetDeps();
|
|
61
|
+
mock.restore();
|
|
62
|
+
if (existsSync(TEST_DIR)) {
|
|
63
|
+
rmSync(TEST_DIR, { recursive: true });
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
// 1. Broker — Transient (one-time) credential injection and consumption
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
describe('CredentialBroker transient credentials', () => {
|
|
72
|
+
let broker: CredentialBroker;
|
|
73
|
+
|
|
74
|
+
beforeEach(() => {
|
|
75
|
+
if (existsSync(TEST_DIR)) rmSync(TEST_DIR, { recursive: true });
|
|
76
|
+
mkdirSync(TEST_DIR, { recursive: true });
|
|
77
|
+
_setStorePath(STORE_PATH);
|
|
78
|
+
_resetBackend();
|
|
79
|
+
_setMetadataPath(join(TEST_DIR, 'metadata.json'));
|
|
80
|
+
broker = new CredentialBroker();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
afterEach(() => {
|
|
84
|
+
_setMetadataPath(null);
|
|
85
|
+
_setStorePath(null);
|
|
86
|
+
_resetBackend();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('consume returns transient value and deletes it', () => {
|
|
90
|
+
upsertCredentialMetadata('svc', 'key', { allowedTools: ['tool1'] });
|
|
91
|
+
broker.injectTransient('svc', 'key', 'one-time-secret');
|
|
92
|
+
|
|
93
|
+
const auth = broker.authorize({ service: 'svc', field: 'key', toolName: 'tool1' });
|
|
94
|
+
expect(auth.authorized).toBe(true);
|
|
95
|
+
if (!auth.authorized) return;
|
|
96
|
+
|
|
97
|
+
const result = broker.consume(auth.token.tokenId);
|
|
98
|
+
expect(result.success).toBe(true);
|
|
99
|
+
expect(result.value).toBe('one-time-secret');
|
|
100
|
+
expect(result.storageKey).toBe('credential:svc:key');
|
|
101
|
+
|
|
102
|
+
// Second authorize + consume should NOT have the transient value
|
|
103
|
+
const auth2 = broker.authorize({ service: 'svc', field: 'key', toolName: 'tool1' });
|
|
104
|
+
expect(auth2.authorized).toBe(true);
|
|
105
|
+
if (!auth2.authorized) return;
|
|
106
|
+
const result2 = broker.consume(auth2.token.tokenId);
|
|
107
|
+
expect(result2.success).toBe(true);
|
|
108
|
+
// No transient value — falls back to storage key only
|
|
109
|
+
expect(result2.value).toBeUndefined();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('browserFill uses transient value when available', async () => {
|
|
113
|
+
upsertCredentialMetadata('github', 'token', { allowedTools: ['browser_fill_credential'] });
|
|
114
|
+
broker.injectTransient('github', 'token', 'transient-ghp-123');
|
|
115
|
+
|
|
116
|
+
let filledValue: string | undefined;
|
|
117
|
+
const result = await broker.browserFill({
|
|
118
|
+
service: 'github',
|
|
119
|
+
field: 'token',
|
|
120
|
+
toolName: 'browser_fill_credential',
|
|
121
|
+
fill: async (v) => { filledValue = v; },
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
expect(result.success).toBe(true);
|
|
125
|
+
expect(filledValue).toBe('transient-ghp-123');
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test('browserFill consumes transient value — second fill falls back to stored', async () => {
|
|
129
|
+
upsertCredentialMetadata('github', 'token', { allowedTools: ['browser_fill_credential'] });
|
|
130
|
+
setSecureKey('credential:github:token', 'stored-value');
|
|
131
|
+
broker.injectTransient('github', 'token', 'transient-value');
|
|
132
|
+
|
|
133
|
+
// First fill uses transient
|
|
134
|
+
let filled1: string | undefined;
|
|
135
|
+
await broker.browserFill({
|
|
136
|
+
service: 'github',
|
|
137
|
+
field: 'token',
|
|
138
|
+
toolName: 'browser_fill_credential',
|
|
139
|
+
fill: async (v) => { filled1 = v; },
|
|
140
|
+
});
|
|
141
|
+
expect(filled1).toBe('transient-value');
|
|
142
|
+
|
|
143
|
+
// Second fill falls back to stored value
|
|
144
|
+
let filled2: string | undefined;
|
|
145
|
+
await broker.browserFill({
|
|
146
|
+
service: 'github',
|
|
147
|
+
field: 'token',
|
|
148
|
+
toolName: 'browser_fill_credential',
|
|
149
|
+
fill: async (v) => { filled2 = v; },
|
|
150
|
+
});
|
|
151
|
+
expect(filled2).toBe('stored-value');
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test('browserFill preserves transient value on fill failure', async () => {
|
|
155
|
+
upsertCredentialMetadata('github', 'token', { allowedTools: ['browser_fill_credential'] });
|
|
156
|
+
broker.injectTransient('github', 'token', 'transient-preserved');
|
|
157
|
+
|
|
158
|
+
// First fill fails
|
|
159
|
+
const result1 = await broker.browserFill({
|
|
160
|
+
service: 'github',
|
|
161
|
+
field: 'token',
|
|
162
|
+
toolName: 'browser_fill_credential',
|
|
163
|
+
fill: async () => { throw new Error('Playwright timeout'); },
|
|
164
|
+
});
|
|
165
|
+
expect(result1.success).toBe(false);
|
|
166
|
+
|
|
167
|
+
// Second fill should still have the transient value
|
|
168
|
+
let filled: string | undefined;
|
|
169
|
+
const result2 = await broker.browserFill({
|
|
170
|
+
service: 'github',
|
|
171
|
+
field: 'token',
|
|
172
|
+
toolName: 'browser_fill_credential',
|
|
173
|
+
fill: async (v) => { filled = v; },
|
|
174
|
+
});
|
|
175
|
+
expect(result2.success).toBe(true);
|
|
176
|
+
expect(filled).toBe('transient-preserved');
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test('serverUse uses transient value when available', async () => {
|
|
180
|
+
upsertCredentialMetadata('vercel', 'api_token', { allowedTools: ['deploy'] });
|
|
181
|
+
broker.injectTransient('vercel', 'api_token', 'transient-vercel-tok');
|
|
182
|
+
|
|
183
|
+
const result = await broker.serverUse({
|
|
184
|
+
service: 'vercel',
|
|
185
|
+
field: 'api_token',
|
|
186
|
+
toolName: 'deploy',
|
|
187
|
+
execute: async (v) => {
|
|
188
|
+
expect(v).toBe('transient-vercel-tok');
|
|
189
|
+
return 'deployed';
|
|
190
|
+
},
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
expect(result.success).toBe(true);
|
|
194
|
+
expect(result.result).toBe('deployed');
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test('serverUse consumes transient — subsequent call has no value without stored key', async () => {
|
|
198
|
+
upsertCredentialMetadata('vercel', 'api_token', { allowedTools: ['deploy'] });
|
|
199
|
+
// Only transient, no stored value
|
|
200
|
+
broker.injectTransient('vercel', 'api_token', 'transient-only');
|
|
201
|
+
|
|
202
|
+
await broker.serverUse({
|
|
203
|
+
service: 'vercel',
|
|
204
|
+
field: 'api_token',
|
|
205
|
+
toolName: 'deploy',
|
|
206
|
+
execute: async () => 'ok',
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// Second call: no transient, no stored value
|
|
210
|
+
const result = await broker.serverUse({
|
|
211
|
+
service: 'vercel',
|
|
212
|
+
field: 'api_token',
|
|
213
|
+
toolName: 'deploy',
|
|
214
|
+
execute: async () => { throw new Error('should not be called'); },
|
|
215
|
+
});
|
|
216
|
+
expect(result.success).toBe(false);
|
|
217
|
+
expect(result.reason).toContain('no stored value');
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
test('injectTransient replaces previous transient for same key', () => {
|
|
221
|
+
upsertCredentialMetadata('svc', 'key', { allowedTools: ['t'] });
|
|
222
|
+
broker.injectTransient('svc', 'key', 'first');
|
|
223
|
+
broker.injectTransient('svc', 'key', 'second');
|
|
224
|
+
|
|
225
|
+
const auth = broker.authorize({ service: 'svc', field: 'key', toolName: 't' });
|
|
226
|
+
if (!auth.authorized) return;
|
|
227
|
+
const result = broker.consume(auth.token.tokenId);
|
|
228
|
+
expect(result.value).toBe('second');
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
test('transient value for one credential does not affect another', () => {
|
|
232
|
+
upsertCredentialMetadata('svcA', 'key', { allowedTools: ['t'] });
|
|
233
|
+
upsertCredentialMetadata('svcB', 'key', { allowedTools: ['t'] });
|
|
234
|
+
broker.injectTransient('svcA', 'key', 'val-a');
|
|
235
|
+
|
|
236
|
+
// svcB should not have a transient value — consume returns storageKey only
|
|
237
|
+
const authB = broker.authorize({ service: 'svcB', field: 'key', toolName: 't' });
|
|
238
|
+
if (!authB.authorized) return;
|
|
239
|
+
const resultB = broker.consume(authB.token.tokenId);
|
|
240
|
+
expect(resultB.success).toBe(true);
|
|
241
|
+
expect(resultB.value).toBeUndefined();
|
|
242
|
+
|
|
243
|
+
// svcA should have the transient
|
|
244
|
+
const authA = broker.authorize({ service: 'svcA', field: 'key', toolName: 't' });
|
|
245
|
+
if (!authA.authorized) return;
|
|
246
|
+
const resultA = broker.consume(authA.token.tokenId);
|
|
247
|
+
expect(resultA.value).toBe('val-a');
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
// ---------------------------------------------------------------------------
|
|
252
|
+
// 2. Vault — unknown action handling
|
|
253
|
+
// ---------------------------------------------------------------------------
|
|
254
|
+
|
|
255
|
+
describe('credential_store tool — unknown action', () => {
|
|
256
|
+
beforeEach(() => {
|
|
257
|
+
if (existsSync(TEST_DIR)) rmSync(TEST_DIR, { recursive: true });
|
|
258
|
+
mkdirSync(TEST_DIR, { recursive: true });
|
|
259
|
+
_setStorePath(STORE_PATH);
|
|
260
|
+
_resetBackend();
|
|
261
|
+
_setMetadataPath(join(TEST_DIR, 'metadata.json'));
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
afterEach(() => {
|
|
265
|
+
_setMetadataPath(null);
|
|
266
|
+
_setStorePath(null);
|
|
267
|
+
_resetBackend();
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
test('returns error for unknown action', async () => {
|
|
271
|
+
const result = await credentialStoreTool.execute({ action: 'unknown_action' }, _ctx);
|
|
272
|
+
expect(result.isError).toBe(true);
|
|
273
|
+
expect(result.content).toContain('unknown action');
|
|
274
|
+
expect(result.content).toContain('unknown_action');
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
// ---------------------------------------------------------------------------
|
|
279
|
+
// 3. Vault — prompt action edge cases
|
|
280
|
+
// ---------------------------------------------------------------------------
|
|
281
|
+
|
|
282
|
+
describe('credential_store tool — prompt action', () => {
|
|
283
|
+
beforeEach(() => {
|
|
284
|
+
if (existsSync(TEST_DIR)) rmSync(TEST_DIR, { recursive: true });
|
|
285
|
+
mkdirSync(TEST_DIR, { recursive: true });
|
|
286
|
+
_setStorePath(STORE_PATH);
|
|
287
|
+
_resetBackend();
|
|
288
|
+
_setMetadataPath(join(TEST_DIR, 'metadata.json'));
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
afterEach(() => {
|
|
292
|
+
_setMetadataPath(null);
|
|
293
|
+
_setStorePath(null);
|
|
294
|
+
_resetBackend();
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
test('returns error when requestSecret is not available', async () => {
|
|
298
|
+
const result = await credentialStoreTool.execute(
|
|
299
|
+
{ action: 'prompt', service: 'svc', field: 'key', label: 'API Key' },
|
|
300
|
+
_ctx, // no requestSecret
|
|
301
|
+
);
|
|
302
|
+
expect(result.isError).toBe(true);
|
|
303
|
+
expect(result.content).toContain('not available');
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
test('returns error when service is missing for prompt', async () => {
|
|
307
|
+
const result = await credentialStoreTool.execute(
|
|
308
|
+
{ action: 'prompt', field: 'key' },
|
|
309
|
+
_ctx,
|
|
310
|
+
);
|
|
311
|
+
expect(result.isError).toBe(true);
|
|
312
|
+
expect(result.content).toContain('service is required');
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
test('returns error when field is missing for prompt', async () => {
|
|
316
|
+
const result = await credentialStoreTool.execute(
|
|
317
|
+
{ action: 'prompt', service: 'svc' },
|
|
318
|
+
_ctx,
|
|
319
|
+
);
|
|
320
|
+
expect(result.isError).toBe(true);
|
|
321
|
+
expect(result.content).toContain('field is required');
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
test('handles user cancellation (null value)', async () => {
|
|
325
|
+
const ctxWithPrompt: ToolContext = {
|
|
326
|
+
..._ctx,
|
|
327
|
+
requestSecret: async () => ({ value: null as unknown as string, delivery: 'store' as const }),
|
|
328
|
+
};
|
|
329
|
+
const result = await credentialStoreTool.execute(
|
|
330
|
+
{ action: 'prompt', service: 'svc', field: 'key', label: 'Test' },
|
|
331
|
+
ctxWithPrompt,
|
|
332
|
+
);
|
|
333
|
+
expect(result.isError).toBe(false);
|
|
334
|
+
expect(result.content).toContain('cancelled');
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
test('stores credential when user provides value via prompt', async () => {
|
|
338
|
+
const ctxWithPrompt: ToolContext = {
|
|
339
|
+
..._ctx,
|
|
340
|
+
requestSecret: async () => ({ value: 'prompt-secret-val', delivery: 'store' as const }),
|
|
341
|
+
};
|
|
342
|
+
const result = await credentialStoreTool.execute(
|
|
343
|
+
{ action: 'prompt', service: 'test-prompt', field: 'api_key', label: 'API Key' },
|
|
344
|
+
ctxWithPrompt,
|
|
345
|
+
);
|
|
346
|
+
expect(result.isError).toBe(false);
|
|
347
|
+
expect(result.content).toContain('test-prompt/api_key');
|
|
348
|
+
expect(result.content).not.toContain('prompt-secret-val');
|
|
349
|
+
|
|
350
|
+
// Verify stored
|
|
351
|
+
expect(getSecureKey('credential:test-prompt:api_key')).toBe('prompt-secret-val');
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
test('prompt with policy fields persists metadata', async () => {
|
|
355
|
+
const ctxWithPrompt: ToolContext = {
|
|
356
|
+
..._ctx,
|
|
357
|
+
requestSecret: async () => ({ value: 'prompt-val', delivery: 'store' as const }),
|
|
358
|
+
};
|
|
359
|
+
const result = await credentialStoreTool.execute({
|
|
360
|
+
action: 'prompt',
|
|
361
|
+
service: 'github',
|
|
362
|
+
field: 'token',
|
|
363
|
+
label: 'GitHub Token',
|
|
364
|
+
allowed_tools: ['browser_fill_credential'],
|
|
365
|
+
allowed_domains: ['github.com'],
|
|
366
|
+
usage_description: 'GitHub login',
|
|
367
|
+
}, ctxWithPrompt);
|
|
368
|
+
expect(result.isError).toBe(false);
|
|
369
|
+
|
|
370
|
+
const { getCredentialMetadata } = await import('../tools/credentials/metadata-store.js');
|
|
371
|
+
const meta = getCredentialMetadata('github', 'token');
|
|
372
|
+
expect(meta).toBeDefined();
|
|
373
|
+
expect(meta!.allowedTools).toEqual(['browser_fill_credential']);
|
|
374
|
+
expect(meta!.allowedDomains).toEqual(['github.com']);
|
|
375
|
+
expect(meta!.usageDescription).toBe('GitHub login');
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
test('prompt rejects invalid policy input', async () => {
|
|
379
|
+
const ctxWithPrompt: ToolContext = {
|
|
380
|
+
..._ctx,
|
|
381
|
+
requestSecret: async () => ({ value: 'val', delivery: 'store' as const }),
|
|
382
|
+
};
|
|
383
|
+
const result = await credentialStoreTool.execute({
|
|
384
|
+
action: 'prompt',
|
|
385
|
+
service: 'svc',
|
|
386
|
+
field: 'key',
|
|
387
|
+
label: 'Test',
|
|
388
|
+
allowed_tools: 'not-an-array',
|
|
389
|
+
}, ctxWithPrompt);
|
|
390
|
+
expect(result.isError).toBe(true);
|
|
391
|
+
expect(result.content).toContain('allowed_tools must be an array');
|
|
392
|
+
});
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
// ---------------------------------------------------------------------------
|
|
396
|
+
// 4. Vault — oauth2_connect error paths
|
|
397
|
+
// ---------------------------------------------------------------------------
|
|
398
|
+
|
|
399
|
+
describe('credential_store tool — oauth2_connect error paths', () => {
|
|
400
|
+
beforeEach(() => {
|
|
401
|
+
if (existsSync(TEST_DIR)) rmSync(TEST_DIR, { recursive: true });
|
|
402
|
+
mkdirSync(TEST_DIR, { recursive: true });
|
|
403
|
+
_setStorePath(STORE_PATH);
|
|
404
|
+
_resetBackend();
|
|
405
|
+
_setMetadataPath(join(TEST_DIR, 'metadata.json'));
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
afterEach(() => {
|
|
409
|
+
_setMetadataPath(null);
|
|
410
|
+
_setStorePath(null);
|
|
411
|
+
_resetBackend();
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
test('requires service parameter', async () => {
|
|
415
|
+
const result = await credentialStoreTool.execute(
|
|
416
|
+
{ action: 'oauth2_connect' },
|
|
417
|
+
_ctx,
|
|
418
|
+
);
|
|
419
|
+
expect(result.isError).toBe(true);
|
|
420
|
+
expect(result.content).toContain('service is required');
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
test('requires auth_url for unknown service', async () => {
|
|
424
|
+
const result = await credentialStoreTool.execute(
|
|
425
|
+
{ action: 'oauth2_connect', service: 'custom-svc', token_url: 'https://t', scopes: ['read'] },
|
|
426
|
+
_ctx,
|
|
427
|
+
);
|
|
428
|
+
expect(result.isError).toBe(true);
|
|
429
|
+
expect(result.content).toContain('auth_url is required');
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
test('requires token_url for unknown service', async () => {
|
|
433
|
+
const result = await credentialStoreTool.execute(
|
|
434
|
+
{ action: 'oauth2_connect', service: 'custom-svc', auth_url: 'https://a', scopes: ['read'] },
|
|
435
|
+
_ctx,
|
|
436
|
+
);
|
|
437
|
+
expect(result.isError).toBe(true);
|
|
438
|
+
expect(result.content).toContain('token_url is required');
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
test('requires scopes for unknown service', async () => {
|
|
442
|
+
const result = await credentialStoreTool.execute(
|
|
443
|
+
{ action: 'oauth2_connect', service: 'custom-svc', auth_url: 'https://a', token_url: 'https://t' },
|
|
444
|
+
_ctx,
|
|
445
|
+
);
|
|
446
|
+
expect(result.isError).toBe(true);
|
|
447
|
+
expect(result.content).toContain('scopes is required');
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
test('requires client_id', async () => {
|
|
451
|
+
const result = await credentialStoreTool.execute({
|
|
452
|
+
action: 'oauth2_connect',
|
|
453
|
+
service: 'custom-svc',
|
|
454
|
+
auth_url: 'https://auth.example.com',
|
|
455
|
+
token_url: 'https://token.example.com',
|
|
456
|
+
scopes: ['read'],
|
|
457
|
+
}, _ctx);
|
|
458
|
+
expect(result.isError).toBe(true);
|
|
459
|
+
expect(result.content).toContain('client_id is required');
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
test('requires interactive context', async () => {
|
|
463
|
+
const result = await credentialStoreTool.execute({
|
|
464
|
+
action: 'oauth2_connect',
|
|
465
|
+
service: 'custom-svc',
|
|
466
|
+
auth_url: 'https://auth.example.com',
|
|
467
|
+
token_url: 'https://token.example.com',
|
|
468
|
+
scopes: ['read'],
|
|
469
|
+
client_id: 'test-client-id',
|
|
470
|
+
}, { ..._ctx, isInteractive: false });
|
|
471
|
+
expect(result.isError).toBe(true);
|
|
472
|
+
expect(result.content).toContain('interactive client session');
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
test('resolves gmail alias to integration:gmail', async () => {
|
|
476
|
+
// Even with alias resolution, missing client_id should still fail
|
|
477
|
+
const result = await credentialStoreTool.execute({
|
|
478
|
+
action: 'oauth2_connect',
|
|
479
|
+
service: 'gmail',
|
|
480
|
+
}, _ctx);
|
|
481
|
+
expect(result.isError).toBe(true);
|
|
482
|
+
// Should NOT require auth_url/token_url/scopes — those are well-known for gmail
|
|
483
|
+
// Should fail on client_id since none is stored
|
|
484
|
+
expect(result.content).toContain('client_id is required');
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
test('resolves slack alias to integration:slack', async () => {
|
|
488
|
+
const result = await credentialStoreTool.execute({
|
|
489
|
+
action: 'oauth2_connect',
|
|
490
|
+
service: 'slack',
|
|
491
|
+
}, _ctx);
|
|
492
|
+
expect(result.isError).toBe(true);
|
|
493
|
+
expect(result.content).toContain('client_id is required');
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
test('uses stored client_id from secure storage', async () => {
|
|
497
|
+
// Store a client_id for the service
|
|
498
|
+
setSecureKey('credential:integration:gmail:client_id', 'stored-client-id-123');
|
|
499
|
+
|
|
500
|
+
const result = await credentialStoreTool.execute({
|
|
501
|
+
action: 'oauth2_connect',
|
|
502
|
+
service: 'gmail',
|
|
503
|
+
}, { ..._ctx, isInteractive: false });
|
|
504
|
+
|
|
505
|
+
// Should pass client_id check but fail on interactive check
|
|
506
|
+
expect(result.isError).toBe(true);
|
|
507
|
+
expect(result.content).toContain('interactive client session');
|
|
508
|
+
// Does NOT contain client_id error
|
|
509
|
+
expect(result.content).not.toContain('client_id is required');
|
|
510
|
+
});
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
// ---------------------------------------------------------------------------
|
|
514
|
+
// 5. Vault — store action validation edge cases
|
|
515
|
+
// ---------------------------------------------------------------------------
|
|
516
|
+
|
|
517
|
+
describe('credential_store tool — store validation edge cases', () => {
|
|
518
|
+
beforeEach(() => {
|
|
519
|
+
if (existsSync(TEST_DIR)) rmSync(TEST_DIR, { recursive: true });
|
|
520
|
+
mkdirSync(TEST_DIR, { recursive: true });
|
|
521
|
+
_setStorePath(STORE_PATH);
|
|
522
|
+
_resetBackend();
|
|
523
|
+
_setMetadataPath(join(TEST_DIR, 'metadata.json'));
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
afterEach(() => {
|
|
527
|
+
_setMetadataPath(null);
|
|
528
|
+
_setStorePath(null);
|
|
529
|
+
_resetBackend();
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
test('rejects alias that is not a string', async () => {
|
|
533
|
+
const result = await credentialStoreTool.execute({
|
|
534
|
+
action: 'store',
|
|
535
|
+
service: 'svc',
|
|
536
|
+
field: 'key',
|
|
537
|
+
value: 'val',
|
|
538
|
+
alias: 42,
|
|
539
|
+
}, _ctx);
|
|
540
|
+
expect(result.isError).toBe(true);
|
|
541
|
+
expect(result.content).toContain('alias must be a string');
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
test('rejects injection_templates that is not an array', async () => {
|
|
545
|
+
const result = await credentialStoreTool.execute({
|
|
546
|
+
action: 'store',
|
|
547
|
+
service: 'svc',
|
|
548
|
+
field: 'key',
|
|
549
|
+
value: 'val',
|
|
550
|
+
injection_templates: 'not-an-array',
|
|
551
|
+
}, _ctx);
|
|
552
|
+
expect(result.isError).toBe(true);
|
|
553
|
+
expect(result.content).toContain('injection_templates must be an array');
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
test('rejects template with invalid injectionType', async () => {
|
|
557
|
+
const result = await credentialStoreTool.execute({
|
|
558
|
+
action: 'store',
|
|
559
|
+
service: 'svc',
|
|
560
|
+
field: 'key',
|
|
561
|
+
value: 'val',
|
|
562
|
+
injection_templates: [
|
|
563
|
+
{ hostPattern: '*.example.com', injectionType: 'cookie' },
|
|
564
|
+
],
|
|
565
|
+
}, _ctx);
|
|
566
|
+
expect(result.isError).toBe(true);
|
|
567
|
+
expect(result.content).toContain("injectionType must be 'header' or 'query'");
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
test('rejects template with empty hostPattern', async () => {
|
|
571
|
+
const result = await credentialStoreTool.execute({
|
|
572
|
+
action: 'store',
|
|
573
|
+
service: 'svc',
|
|
574
|
+
field: 'key',
|
|
575
|
+
value: 'val',
|
|
576
|
+
injection_templates: [
|
|
577
|
+
{ hostPattern: ' ', injectionType: 'header', headerName: 'Authorization' },
|
|
578
|
+
],
|
|
579
|
+
}, _ctx);
|
|
580
|
+
expect(result.isError).toBe(true);
|
|
581
|
+
expect(result.content).toContain('hostPattern must be a non-empty string');
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
test('rejects template with non-string valuePrefix', async () => {
|
|
585
|
+
const result = await credentialStoreTool.execute({
|
|
586
|
+
action: 'store',
|
|
587
|
+
service: 'svc',
|
|
588
|
+
field: 'key',
|
|
589
|
+
value: 'val',
|
|
590
|
+
injection_templates: [
|
|
591
|
+
{ hostPattern: '*.example.com', injectionType: 'header', headerName: 'Auth', valuePrefix: 42 },
|
|
592
|
+
],
|
|
593
|
+
}, _ctx);
|
|
594
|
+
expect(result.isError).toBe(true);
|
|
595
|
+
expect(result.content).toContain('valuePrefix must be a string');
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
test('reports multiple template errors at once', async () => {
|
|
599
|
+
const result = await credentialStoreTool.execute({
|
|
600
|
+
action: 'store',
|
|
601
|
+
service: 'svc',
|
|
602
|
+
field: 'key',
|
|
603
|
+
value: 'val',
|
|
604
|
+
injection_templates: [
|
|
605
|
+
{ hostPattern: '', injectionType: 'header', headerName: 'X-Key' },
|
|
606
|
+
{ hostPattern: '*.example.com', injectionType: 'query' }, // missing queryParamName
|
|
607
|
+
],
|
|
608
|
+
}, _ctx);
|
|
609
|
+
expect(result.isError).toBe(true);
|
|
610
|
+
expect(result.content).toContain('hostPattern');
|
|
611
|
+
expect(result.content).toContain('queryParamName');
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
test('delete removes both secret and metadata', async () => {
|
|
615
|
+
await credentialStoreTool.execute({
|
|
616
|
+
action: 'store', service: 'del-test', field: 'key', value: 'secret',
|
|
617
|
+
}, _ctx);
|
|
618
|
+
|
|
619
|
+
// Verify stored
|
|
620
|
+
expect(getSecureKey('credential:del-test:key')).toBe('secret');
|
|
621
|
+
const { getCredentialMetadata } = await import('../tools/credentials/metadata-store.js');
|
|
622
|
+
expect(getCredentialMetadata('del-test', 'key')).toBeDefined();
|
|
623
|
+
|
|
624
|
+
// Delete
|
|
625
|
+
const result = await credentialStoreTool.execute({
|
|
626
|
+
action: 'delete', service: 'del-test', field: 'key',
|
|
627
|
+
}, _ctx);
|
|
628
|
+
expect(result.isError).toBe(false);
|
|
629
|
+
|
|
630
|
+
// Both should be gone
|
|
631
|
+
expect(getSecureKey('credential:del-test:key')).toBeUndefined();
|
|
632
|
+
expect(getCredentialMetadata('del-test', 'key')).toBeUndefined();
|
|
633
|
+
});
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
// ---------------------------------------------------------------------------
|
|
637
|
+
// 6. Vault — tool definition schema
|
|
638
|
+
// ---------------------------------------------------------------------------
|
|
639
|
+
|
|
640
|
+
describe('credential_store tool — tool definition', () => {
|
|
641
|
+
test('tool name and category are correct', () => {
|
|
642
|
+
expect(credentialStoreTool.name).toBe('credential_store');
|
|
643
|
+
expect(credentialStoreTool.category).toBe('credentials');
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
test('getDefinition returns valid schema with required action', () => {
|
|
647
|
+
const def = credentialStoreTool.getDefinition();
|
|
648
|
+
expect(def.name).toBe('credential_store');
|
|
649
|
+
const schema = def.input_schema as Record<string, unknown>;
|
|
650
|
+
expect(schema.type).toBe('object');
|
|
651
|
+
expect(schema.required).toContain('action');
|
|
652
|
+
const props = schema.properties as Record<string, Record<string, unknown>>;
|
|
653
|
+
expect(props.action.enum).toEqual(
|
|
654
|
+
['store', 'list', 'delete', 'prompt', 'oauth2_connect'],
|
|
655
|
+
);
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
test('getDefinition includes injection_templates schema', () => {
|
|
659
|
+
const def = credentialStoreTool.getDefinition();
|
|
660
|
+
const schemaProps = (def.input_schema as Record<string, unknown>).properties as Record<string, Record<string, unknown>>;
|
|
661
|
+
const templates = schemaProps.injection_templates as Record<string, unknown>;
|
|
662
|
+
expect(templates).toBeDefined();
|
|
663
|
+
expect(templates.type).toBe('array');
|
|
664
|
+
const items = (templates.items as Record<string, unknown>).properties as Record<string, Record<string, unknown>>;
|
|
665
|
+
expect(items.hostPattern).toBeDefined();
|
|
666
|
+
expect(items.injectionType.enum).toEqual(['header', 'query']);
|
|
667
|
+
});
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
// ---------------------------------------------------------------------------
|
|
671
|
+
// 7. Broker — serverUseById with transient not supported
|
|
672
|
+
// (transient is scoped to authorize+consume and browserFill/serverUse)
|
|
673
|
+
// ---------------------------------------------------------------------------
|
|
674
|
+
|
|
675
|
+
describe('CredentialBroker — serverUseById edge cases', () => {
|
|
676
|
+
let broker: CredentialBroker;
|
|
677
|
+
|
|
678
|
+
beforeEach(() => {
|
|
679
|
+
if (existsSync(TEST_DIR)) rmSync(TEST_DIR, { recursive: true });
|
|
680
|
+
mkdirSync(TEST_DIR, { recursive: true });
|
|
681
|
+
_setStorePath(STORE_PATH);
|
|
682
|
+
_resetBackend();
|
|
683
|
+
_setMetadataPath(join(TEST_DIR, 'metadata.json'));
|
|
684
|
+
broker = new CredentialBroker();
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
afterEach(() => {
|
|
688
|
+
_setMetadataPath(null);
|
|
689
|
+
_setStorePath(null);
|
|
690
|
+
_resetBackend();
|
|
691
|
+
});
|
|
692
|
+
|
|
693
|
+
test('serverUseById with multiple injection templates returns all', () => {
|
|
694
|
+
const meta = upsertCredentialMetadata('multi', 'api_key', {
|
|
695
|
+
allowedTools: ['proxy'],
|
|
696
|
+
injectionTemplates: [
|
|
697
|
+
{ hostPattern: '*.fal.ai', injectionType: 'header', headerName: 'Authorization', valuePrefix: 'Key ' },
|
|
698
|
+
{ hostPattern: 'gateway.fal.ai', injectionType: 'header', headerName: 'X-Fal-Key' },
|
|
699
|
+
],
|
|
700
|
+
});
|
|
701
|
+
setSecureKey('credential:multi:api_key', 'multi-secret');
|
|
702
|
+
|
|
703
|
+
const result = broker.serverUseById({
|
|
704
|
+
credentialId: meta.credentialId,
|
|
705
|
+
requestingTool: 'proxy',
|
|
706
|
+
});
|
|
707
|
+
|
|
708
|
+
expect(result.success).toBe(true);
|
|
709
|
+
if (!result.success) return;
|
|
710
|
+
expect(result.injectionTemplates).toHaveLength(2);
|
|
711
|
+
expect(result.injectionTemplates[0].hostPattern).toBe('*.fal.ai');
|
|
712
|
+
expect(result.injectionTemplates[1].hostPattern).toBe('gateway.fal.ai');
|
|
713
|
+
// No secret value in result
|
|
714
|
+
expect(JSON.stringify(result)).not.toContain('multi-secret');
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
test('serverUseById verifies secret exists in storage (fail-closed)', () => {
|
|
718
|
+
const meta = upsertCredentialMetadata('fal', 'api_key', {
|
|
719
|
+
allowedTools: ['proxy'],
|
|
720
|
+
});
|
|
721
|
+
// No setSecureKey — metadata exists but value doesn't
|
|
722
|
+
|
|
723
|
+
const result = broker.serverUseById({
|
|
724
|
+
credentialId: meta.credentialId,
|
|
725
|
+
requestingTool: 'proxy',
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
expect(result.success).toBe(false);
|
|
729
|
+
if (result.success) return;
|
|
730
|
+
expect(result.reason).toContain('no stored value');
|
|
731
|
+
});
|
|
732
|
+
});
|
|
733
|
+
|
|
734
|
+
// ---------------------------------------------------------------------------
|
|
735
|
+
// 8. Broker — revokeAll clears transient values indirectly via token cleanup
|
|
736
|
+
// ---------------------------------------------------------------------------
|
|
737
|
+
|
|
738
|
+
describe('CredentialBroker — revokeAll', () => {
|
|
739
|
+
let broker: CredentialBroker;
|
|
740
|
+
|
|
741
|
+
beforeEach(() => {
|
|
742
|
+
if (existsSync(TEST_DIR)) rmSync(TEST_DIR, { recursive: true });
|
|
743
|
+
mkdirSync(TEST_DIR, { recursive: true });
|
|
744
|
+
_setStorePath(STORE_PATH);
|
|
745
|
+
_resetBackend();
|
|
746
|
+
_setMetadataPath(join(TEST_DIR, 'metadata.json'));
|
|
747
|
+
broker = new CredentialBroker();
|
|
748
|
+
});
|
|
749
|
+
|
|
750
|
+
afterEach(() => {
|
|
751
|
+
_setMetadataPath(null);
|
|
752
|
+
_setStorePath(null);
|
|
753
|
+
_resetBackend();
|
|
754
|
+
});
|
|
755
|
+
|
|
756
|
+
test('revokeAll clears all tokens and subsequent consume fails', () => {
|
|
757
|
+
upsertCredentialMetadata('svc', 'key', { allowedTools: ['t1', 't2'] });
|
|
758
|
+
const a1 = broker.authorize({ service: 'svc', field: 'key', toolName: 't1' });
|
|
759
|
+
const a2 = broker.authorize({ service: 'svc', field: 'key', toolName: 't2' });
|
|
760
|
+
expect(broker.activeTokenCount).toBe(2);
|
|
761
|
+
|
|
762
|
+
broker.revokeAll();
|
|
763
|
+
expect(broker.activeTokenCount).toBe(0);
|
|
764
|
+
|
|
765
|
+
if (a1.authorized) {
|
|
766
|
+
const r = broker.consume(a1.token.tokenId);
|
|
767
|
+
expect(r.success).toBe(false);
|
|
768
|
+
}
|
|
769
|
+
if (a2.authorized) {
|
|
770
|
+
const r = broker.consume(a2.token.tokenId);
|
|
771
|
+
expect(r.success).toBe(false);
|
|
772
|
+
}
|
|
773
|
+
});
|
|
774
|
+
|
|
775
|
+
test('revokeAll on empty broker is a no-op', () => {
|
|
776
|
+
expect(broker.activeTokenCount).toBe(0);
|
|
777
|
+
broker.revokeAll();
|
|
778
|
+
expect(broker.activeTokenCount).toBe(0);
|
|
779
|
+
});
|
|
780
|
+
});
|