@vellumai/credential-executor 0.10.7 → 0.10.8-dev.202607102228.5945895

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.
Files changed (65) hide show
  1. package/Dockerfile +1 -1
  2. package/node_modules/@vellumai/service-contracts/package.json +1 -2
  3. package/node_modules/@vellumai/service-contracts/src/__tests__/attachment-naming.test.ts +104 -0
  4. package/node_modules/@vellumai/service-contracts/src/__tests__/contracts.test.ts +0 -2
  5. package/node_modules/@vellumai/service-contracts/src/attachment-naming.ts +118 -0
  6. package/node_modules/@vellumai/service-contracts/src/credential-rpc.ts +3 -5
  7. package/node_modules/@vellumai/service-contracts/src/index.ts +2 -4
  8. package/node_modules/@vellumai/service-contracts/src/rpc.ts +4 -447
  9. package/package.json +2 -3
  10. package/src/__tests__/bulk-set-credentials.test.ts +1 -1
  11. package/src/__tests__/local-standalone.test.ts +5 -36
  12. package/src/__tests__/managed-integration.test.ts +112 -91
  13. package/src/__tests__/managed-reconnect.test.ts +2 -2
  14. package/src/__tests__/transport.test.ts +23 -27
  15. package/src/cli.ts +1 -1
  16. package/src/index.ts +8 -88
  17. package/src/main.ts +228 -340
  18. package/src/paths.ts +4 -20
  19. package/src/server.ts +52 -469
  20. package/node_modules/@vellumai/service-contracts/src/__tests__/grants.test.ts +0 -686
  21. package/node_modules/@vellumai/service-contracts/src/grants.ts +0 -184
  22. package/node_modules/@vellumai/service-contracts/src/rendering.ts +0 -135
  23. package/src/__tests__/command-executor.test.ts +0 -1879
  24. package/src/__tests__/command-validator.test.ts +0 -1405
  25. package/src/__tests__/command-workspace.test.ts +0 -1050
  26. package/src/__tests__/grant-store.test.ts +0 -689
  27. package/src/__tests__/http-executor.test.ts +0 -1336
  28. package/src/__tests__/http-policy.test.ts +0 -1069
  29. package/src/__tests__/local-materializers.test.ts +0 -860
  30. package/src/__tests__/local-token-refresh.test.ts +0 -361
  31. package/src/__tests__/manage-secure-command-tool.test.ts +0 -134
  32. package/src/__tests__/managed-lazy-getters.test.ts +0 -359
  33. package/src/__tests__/managed-materializers.test.ts +0 -1028
  34. package/src/__tests__/managed-rejection.test.ts +0 -43
  35. package/src/__tests__/toolstore.test.ts +0 -773
  36. package/src/audit/store.ts +0 -188
  37. package/src/commands/auth-adapters.ts +0 -169
  38. package/src/commands/egress-hooks.ts +0 -203
  39. package/src/commands/executor.ts +0 -1155
  40. package/src/commands/output-scan.ts +0 -157
  41. package/src/commands/profiles.ts +0 -286
  42. package/src/commands/validator.ts +0 -702
  43. package/src/commands/workspace.ts +0 -550
  44. package/src/grants/index.ts +0 -17
  45. package/src/grants/persistent-store.ts +0 -309
  46. package/src/grants/rpc-handlers.ts +0 -293
  47. package/src/grants/temporary-store.ts +0 -289
  48. package/src/http/audit.ts +0 -84
  49. package/src/http/executor.ts +0 -684
  50. package/src/http/path-template.ts +0 -245
  51. package/src/http/policy.ts +0 -238
  52. package/src/http/response-filter.ts +0 -233
  53. package/src/managed-errors.ts +0 -9
  54. package/src/managed-lazy-getters.ts +0 -106
  55. package/src/managed-main.ts +0 -822
  56. package/src/materializers/local-oauth-lookup.ts +0 -98
  57. package/src/materializers/local-token-refresh.ts +0 -287
  58. package/src/materializers/local.ts +0 -316
  59. package/src/materializers/managed-platform.ts +0 -295
  60. package/src/subjects/local.ts +0 -177
  61. package/src/subjects/managed.ts +0 -311
  62. package/src/subjects/policy.ts +0 -79
  63. package/src/toolstore/integrity.ts +0 -94
  64. package/src/toolstore/manifest.ts +0 -154
  65. package/src/toolstore/publish.ts +0 -571
@@ -1,1336 +0,0 @@
1
- /**
2
- * Integration tests for the CES HTTP executor.
3
- *
4
- * Covers:
5
- * 1. Local static secret flow — resolve, grant-check, materialise, execute, filter
6
- * 2. Local OAuth flow — resolve, grant-check, materialise (with token), execute, filter
7
- * 3. platform_oauth:<connection_id> flow — resolve, grant-check, materialise via
8
- * platform, execute, filter
9
- * 4. Approval-required short-circuit — off-grant requests return a proposal
10
- * without making any network call
11
- * 5. Forbidden header rejection — caller-supplied auth headers are blocked
12
- * 6. Redirect denial — redirect hops that violate grant policy are blocked
13
- * 7. Filtered response behaviour — secret scrubbing, header stripping, body clamping
14
- * 8. Audit summaries are token-free
15
- */
16
-
17
- import { describe, expect, test, beforeEach, afterEach } from "bun:test";
18
- import { randomUUID } from "node:crypto";
19
- import { mkdirSync, rmSync } from "node:fs";
20
- import { join } from "node:path";
21
- import { tmpdir } from "node:os";
22
-
23
- import {
24
- localStaticHandle,
25
- localOAuthHandle,
26
- platformOAuthHandle,
27
- } from "@vellumai/service-contracts/credential-rpc";
28
- import {
29
- type OAuthConnectionRecord,
30
- type SecureKeyBackend,
31
- type SecureKeyDeleteResult,
32
- type StaticCredentialRecord,
33
- StaticCredentialMetadataStore,
34
- oauthConnectionAccessTokenPath,
35
- credentialKey,
36
- } from "@vellumai/credential-storage";
37
-
38
- import {
39
- executeAuthenticatedHttpRequest,
40
- type HttpExecutorDeps,
41
- } from "../http/executor.js";
42
- import { PersistentGrantStore } from "../grants/persistent-store.js";
43
- import { TemporaryGrantStore } from "../grants/temporary-store.js";
44
- import { LocalMaterialiser } from "../materializers/local.js";
45
- import type { OAuthConnectionLookup } from "../subjects/local.js";
46
- import { AuditStore } from "../audit/store.js";
47
-
48
- // ---------------------------------------------------------------------------
49
- // Test helpers
50
- // ---------------------------------------------------------------------------
51
-
52
- function makeTmpDir(): string {
53
- const dir = join(tmpdir(), `ces-http-executor-test-${randomUUID()}`);
54
- mkdirSync(dir, { recursive: true });
55
- return dir;
56
- }
57
-
58
- /**
59
- * In-memory secure-key backend for testing.
60
- */
61
- function createMemoryBackend(
62
- initial: Record<string, string> = {},
63
- ): SecureKeyBackend {
64
- const store = new Map<string, string>(Object.entries(initial));
65
- return {
66
- async get(key: string): Promise<string | undefined> {
67
- return store.get(key);
68
- },
69
- async set(key: string, value: string): Promise<boolean> {
70
- store.set(key, value);
71
- return true;
72
- },
73
- async delete(key: string): Promise<SecureKeyDeleteResult> {
74
- if (store.has(key)) {
75
- store.delete(key);
76
- return "deleted";
77
- }
78
- return "not-found";
79
- },
80
- async list(): Promise<string[]> {
81
- return Array.from(store.keys());
82
- },
83
- };
84
- }
85
-
86
- function buildStaticRecord(
87
- overrides: Partial<StaticCredentialRecord> = {},
88
- ): StaticCredentialRecord {
89
- return {
90
- credentialId: overrides.credentialId ?? "cred-uuid-1",
91
- service: overrides.service ?? "github",
92
- field: overrides.field ?? "api_key",
93
- allowedTools: overrides.allowedTools ?? ["make_authenticated_request"],
94
- allowedDomains: overrides.allowedDomains ?? [],
95
- createdAt: overrides.createdAt ?? Date.now(),
96
- updatedAt: overrides.updatedAt ?? Date.now(),
97
- };
98
- }
99
-
100
- function buildOAuthConnection(
101
- overrides: Partial<OAuthConnectionRecord> = {},
102
- ): OAuthConnectionRecord {
103
- return {
104
- id: overrides.id ?? "conn-uuid-1",
105
- providerKey: overrides.providerKey ?? "google",
106
- accountInfo: overrides.accountInfo ?? "user@example.com",
107
- grantedScopes: overrides.grantedScopes ?? ["openid", "email"],
108
- accessTokenPath: overrides.accessTokenPath ??
109
- oauthConnectionAccessTokenPath(overrides.id ?? "conn-uuid-1"),
110
- hasRefreshToken: overrides.hasRefreshToken ?? true,
111
- expiresAt: overrides.expiresAt ?? null,
112
- createdAt: overrides.createdAt ?? Date.now(),
113
- updatedAt: overrides.updatedAt ?? Date.now(),
114
- };
115
- }
116
-
117
- function createOAuthLookup(
118
- connections: OAuthConnectionRecord[] = [],
119
- ): OAuthConnectionLookup {
120
- const byId = new Map(connections.map((c) => [c.id, c]));
121
- return {
122
- getById(id: string) {
123
- return byId.get(id);
124
- },
125
- };
126
- }
127
-
128
- /**
129
- * Build a mock fetch that returns a predetermined response.
130
- */
131
- function mockFetch(
132
- statusCode: number,
133
- body: string,
134
- headers?: Record<string, string>,
135
- ): typeof globalThis.fetch {
136
- return asFetch(async (_url: string | URL | Request, _init?: RequestInit) => {
137
- const responseHeaders = new Headers(headers ?? {});
138
- if (!responseHeaders.has("content-type")) {
139
- responseHeaders.set("content-type", "application/json");
140
- }
141
- return new Response(body, {
142
- status: statusCode,
143
- headers: responseHeaders,
144
- });
145
- });
146
- }
147
-
148
- /**
149
- * Build a mock fetch that records requests for inspection.
150
- */
151
- function mockFetchRecorder(
152
- statusCode: number,
153
- body: string,
154
- headers?: Record<string, string>,
155
- ): {
156
- fetch: typeof globalThis.fetch;
157
- requests: Array<{ url: string; init?: RequestInit }>;
158
- } {
159
- const requests: Array<{ url: string; init?: RequestInit }> = [];
160
- const fetchFn = asFetch(async (
161
- url: string | URL | Request,
162
- init?: RequestInit,
163
- ) => {
164
- requests.push({ url: url.toString(), init });
165
- const responseHeaders = new Headers(headers ?? {});
166
- if (!responseHeaders.has("content-type")) {
167
- responseHeaders.set("content-type", "application/json");
168
- }
169
- return new Response(body, {
170
- status: statusCode,
171
- headers: responseHeaders,
172
- });
173
- });
174
- return { fetch: fetchFn, requests };
175
- }
176
-
177
- /**
178
- * Build a mock fetch that returns a redirect on first call, then a real
179
- * response on subsequent calls.
180
- */
181
- function mockFetchRedirect(
182
- redirectUrl: string,
183
- redirectStatus: number,
184
- finalStatusCode: number,
185
- finalBody: string,
186
- ): typeof globalThis.fetch {
187
- let callCount = 0;
188
- return asFetch(async (_url: string | URL | Request, _init?: RequestInit) => {
189
- callCount++;
190
- if (callCount === 1) {
191
- return new Response(null, {
192
- status: redirectStatus,
193
- headers: { Location: redirectUrl },
194
- });
195
- }
196
- return new Response(finalBody, {
197
- status: finalStatusCode,
198
- headers: { "Content-Type": "application/json" },
199
- });
200
- });
201
- }
202
-
203
- /**
204
- * Attach a no-op `preconnect` so a plain async function satisfies
205
- * Bun's `typeof globalThis.fetch` (which includes `preconnect`).
206
- */
207
- function asFetch(
208
- fn: (url: string | URL | Request, init?: RequestInit) => Promise<Response>,
209
- ): typeof globalThis.fetch {
210
- return Object.assign(fn, {
211
- preconnect: (_url: string | URL) => {},
212
- }) as unknown as typeof globalThis.fetch;
213
- }
214
-
215
- /** Silent logger that suppresses output during tests. */
216
- const silentLogger: Pick<Console, "log" | "warn" | "error"> = {
217
- log: () => {},
218
- warn: () => {},
219
- error: () => {},
220
- };
221
-
222
- // ---------------------------------------------------------------------------
223
- // Test fixtures
224
- // ---------------------------------------------------------------------------
225
-
226
- interface TestFixture {
227
- tmpDir: string;
228
- persistentStore: PersistentGrantStore;
229
- temporaryStore: TemporaryGrantStore;
230
- backend: SecureKeyBackend;
231
- metadataStore: StaticCredentialMetadataStore;
232
- localMaterialiser: LocalMaterialiser;
233
- }
234
-
235
- function createFixture(
236
- secretEntries: Record<string, string> = {},
237
- staticRecords: StaticCredentialRecord[] = [],
238
- _oauthConnections: OAuthConnectionRecord[] = [],
239
- ): TestFixture {
240
- const tmpDir = makeTmpDir();
241
- const persistentStore = new PersistentGrantStore(tmpDir);
242
- persistentStore.init();
243
- const temporaryStore = new TemporaryGrantStore();
244
-
245
- const backend = createMemoryBackend(secretEntries);
246
- const metadataStore = new StaticCredentialMetadataStore(
247
- join(tmpDir, "credentials.json"),
248
- );
249
- for (const record of staticRecords) {
250
- metadataStore.upsert(record.service, record.field, {
251
- allowedTools: record.allowedTools,
252
- allowedDomains: record.allowedDomains,
253
- });
254
- }
255
-
256
- const localMaterialiser = new LocalMaterialiser({
257
- secureKeyBackend: backend,
258
- });
259
-
260
- return {
261
- tmpDir,
262
- persistentStore,
263
- temporaryStore,
264
- backend,
265
- metadataStore,
266
- localMaterialiser,
267
- };
268
- }
269
-
270
- function buildDeps(
271
- fixture: TestFixture,
272
- oauthConnections: OAuthConnectionRecord[] = [],
273
- overrides: Partial<HttpExecutorDeps> = {},
274
- ): HttpExecutorDeps {
275
- return {
276
- persistentGrantStore: fixture.persistentStore,
277
- temporaryGrantStore: fixture.temporaryStore,
278
- localMaterialiser: fixture.localMaterialiser,
279
- localSubjectDeps: {
280
- metadataStore: fixture.metadataStore,
281
- oauthConnections: createOAuthLookup(oauthConnections),
282
- },
283
- sessionId: "test-session",
284
- logger: silentLogger,
285
- ...overrides,
286
- auditStore: overrides.auditStore ?? new AuditStore(fixture.tmpDir),
287
- };
288
- }
289
-
290
- // ---------------------------------------------------------------------------
291
- // Tests: Local static secrets
292
- // ---------------------------------------------------------------------------
293
-
294
- describe("HTTP executor: local static secrets", () => {
295
- let fixture: TestFixture;
296
-
297
- beforeEach(() => {
298
- const storageKey = credentialKey("github", "api_key");
299
- fixture = createFixture(
300
- { [storageKey]: "ghp_testtoken_12345678" },
301
- [buildStaticRecord({ service: "github", field: "api_key" })],
302
- );
303
- });
304
-
305
- afterEach(() => {
306
- rmSync(fixture.tmpDir, { recursive: true, force: true });
307
- });
308
-
309
- test("successful request with matching grant", async () => {
310
- const handle = localStaticHandle("github", "api_key");
311
-
312
- // Add a matching grant
313
- fixture.persistentStore.add({
314
- id: "grant-github-repos",
315
- tool: "http",
316
- pattern: "GET https://api.github.com/repos/owner/repo",
317
- scope: handle,
318
- createdAt: Date.now(),
319
- sessionId: "test-session",
320
- });
321
-
322
- const { fetch: fetchFn, requests } = mockFetchRecorder(
323
- 200,
324
- '{"name": "repo", "full_name": "owner/repo"}',
325
- );
326
-
327
- const deps = buildDeps(fixture, [], { fetch: fetchFn });
328
- const result = await executeAuthenticatedHttpRequest(
329
- {
330
- credentialHandle: handle,
331
- method: "GET",
332
- url: "https://api.github.com/repos/owner/repo",
333
- purpose: "Get repo info",
334
- },
335
- deps,
336
- );
337
-
338
- expect(result.success).toBe(true);
339
- expect(result.statusCode).toBe(200);
340
- expect(result.responseBody).toContain("owner/repo");
341
- expect(result.auditId).toBeDefined();
342
-
343
- // Verify the outbound request had auth injected
344
- expect(requests).toHaveLength(1);
345
- const outboundHeaders = requests[0].init?.headers as Record<string, string>;
346
- expect(outboundHeaders?.["Authorization"]).toContain("Bearer");
347
- expect(outboundHeaders?.["Authorization"]).toContain("ghp_testtoken_12345678");
348
- });
349
-
350
- test("response body is scrubbed of secret values", async () => {
351
- const handle = localStaticHandle("github", "api_key");
352
-
353
- fixture.persistentStore.add({
354
- id: "grant-github-user",
355
- tool: "http",
356
- pattern: "GET https://api.github.com/user",
357
- scope: handle,
358
- createdAt: Date.now(),
359
- sessionId: "test-session",
360
- });
361
-
362
- // Simulate API echoing back the token in response
363
- const fetchFn = mockFetch(
364
- 200,
365
- '{"token_echo": "ghp_testtoken_12345678", "name": "octocat"}',
366
- );
367
-
368
- const deps = buildDeps(fixture, [], { fetch: fetchFn });
369
- const result = await executeAuthenticatedHttpRequest(
370
- {
371
- credentialHandle: handle,
372
- method: "GET",
373
- url: "https://api.github.com/user",
374
- purpose: "Get user info",
375
- },
376
- deps,
377
- );
378
-
379
- expect(result.success).toBe(true);
380
- expect(result.responseBody).not.toContain("ghp_testtoken_12345678");
381
- expect(result.responseBody).toContain("[CES:REDACTED]");
382
- expect(result.responseBody).toContain("octocat");
383
- });
384
-
385
- test("response headers are filtered (set-cookie stripped)", async () => {
386
- const handle = localStaticHandle("github", "api_key");
387
-
388
- fixture.persistentStore.add({
389
- id: "grant-github-data",
390
- tool: "http",
391
- pattern: "GET https://api.github.com/data",
392
- scope: handle,
393
- createdAt: Date.now(),
394
- sessionId: "test-session",
395
- });
396
-
397
- const fetchFn = mockFetch(200, '{"ok": true}', {
398
- "content-type": "application/json",
399
- "set-cookie": "session=secret; HttpOnly",
400
- "x-request-id": "req-123",
401
- });
402
-
403
- const deps = buildDeps(fixture, [], { fetch: fetchFn });
404
- const result = await executeAuthenticatedHttpRequest(
405
- {
406
- credentialHandle: handle,
407
- method: "GET",
408
- url: "https://api.github.com/data",
409
- purpose: "Get data",
410
- },
411
- deps,
412
- );
413
-
414
- expect(result.success).toBe(true);
415
- expect(result.responseHeaders).toBeDefined();
416
- expect(result.responseHeaders!["content-type"]).toBe("application/json");
417
- expect(result.responseHeaders!["x-request-id"]).toBe("req-123");
418
- // set-cookie must be stripped
419
- expect(result.responseHeaders!["set-cookie"]).toBeUndefined();
420
- });
421
- });
422
-
423
- // ---------------------------------------------------------------------------
424
- // Tests: Local OAuth
425
- // ---------------------------------------------------------------------------
426
-
427
- describe("HTTP executor: local OAuth", () => {
428
- let fixture: TestFixture;
429
- let connection: OAuthConnectionRecord;
430
-
431
- beforeEach(() => {
432
- connection = buildOAuthConnection({
433
- id: "conn-google-1",
434
- providerKey: "google",
435
- expiresAt: Date.now() + 3600000, // valid for 1 hour
436
- });
437
-
438
- const accessTokenPath = oauthConnectionAccessTokenPath("conn-google-1");
439
- fixture = createFixture(
440
- { [accessTokenPath]: "ya29.test-google-token-abc123" },
441
- [],
442
- [connection],
443
- );
444
- });
445
-
446
- afterEach(() => {
447
- rmSync(fixture.tmpDir, { recursive: true, force: true });
448
- });
449
-
450
- test("successful OAuth request with matching grant", async () => {
451
- const handle = localOAuthHandle("google", "conn-google-1");
452
-
453
- fixture.persistentStore.add({
454
- id: "grant-google-calendar",
455
- tool: "http",
456
- pattern: "GET https://www.googleapis.com/calendar/v3/calendars/primary/events",
457
- scope: handle,
458
- createdAt: Date.now(),
459
- sessionId: "test-session",
460
- });
461
-
462
- const { fetch: fetchFn, requests } = mockFetchRecorder(
463
- 200,
464
- '{"items": [{"summary": "Meeting"}]}',
465
- );
466
-
467
- const deps = buildDeps(fixture, [connection], { fetch: fetchFn });
468
- const result = await executeAuthenticatedHttpRequest(
469
- {
470
- credentialHandle: handle,
471
- method: "GET",
472
- url: "https://www.googleapis.com/calendar/v3/calendars/primary/events",
473
- purpose: "List calendar events",
474
- },
475
- deps,
476
- );
477
-
478
- expect(result.success).toBe(true);
479
- expect(result.statusCode).toBe(200);
480
- expect(result.responseBody).toContain("Meeting");
481
- expect(result.auditId).toBeDefined();
482
-
483
- // Verify Bearer token was injected
484
- expect(requests).toHaveLength(1);
485
- const outboundHeaders = requests[0].init?.headers as Record<string, string>;
486
- expect(outboundHeaders?.["Authorization"]).toBe(
487
- "Bearer ya29.test-google-token-abc123",
488
- );
489
- });
490
-
491
- test("OAuth token scrubbed from response body", async () => {
492
- const handle = localOAuthHandle("google", "conn-google-1");
493
-
494
- fixture.persistentStore.add({
495
- id: "grant-google-debug",
496
- tool: "http",
497
- pattern: "GET https://www.googleapis.com/oauth2/v1/tokeninfo",
498
- scope: handle,
499
- createdAt: Date.now(),
500
- sessionId: "test-session",
501
- });
502
-
503
- // Simulate tokeninfo endpoint echoing the token
504
- const fetchFn = mockFetch(
505
- 200,
506
- '{"access_token": "ya29.test-google-token-abc123", "expires_in": 3600}',
507
- );
508
-
509
- const deps = buildDeps(fixture, [connection], { fetch: fetchFn });
510
- const result = await executeAuthenticatedHttpRequest(
511
- {
512
- credentialHandle: handle,
513
- method: "GET",
514
- url: "https://www.googleapis.com/oauth2/v1/tokeninfo",
515
- purpose: "Check token info",
516
- },
517
- deps,
518
- );
519
-
520
- expect(result.success).toBe(true);
521
- expect(result.responseBody).not.toContain("ya29.test-google-token-abc123");
522
- expect(result.responseBody).toContain("[CES:REDACTED]");
523
- });
524
- });
525
-
526
- // ---------------------------------------------------------------------------
527
- // Tests: Managed (platform_oauth) handles
528
- // ---------------------------------------------------------------------------
529
-
530
- describe("HTTP executor: platform_oauth handles", () => {
531
- let fixture: TestFixture;
532
- let tmpDir: string;
533
-
534
- beforeEach(() => {
535
- fixture = createFixture();
536
- tmpDir = fixture.tmpDir;
537
- });
538
-
539
- afterEach(() => {
540
- rmSync(tmpDir, { recursive: true, force: true });
541
- });
542
-
543
- test("successful platform OAuth request", async () => {
544
- const handle = platformOAuthHandle("conn-platform-1");
545
-
546
- fixture.persistentStore.add({
547
- id: "grant-platform-api",
548
- tool: "http",
549
- pattern: "GET https://api.slack.com/api/conversations.list",
550
- scope: handle,
551
- createdAt: Date.now(),
552
- sessionId: "test-session",
553
- });
554
-
555
- // Mock the platform catalog response
556
- const platformCatalogFetch = asFetch(async (
557
- url: string | URL | Request,
558
- _init?: RequestInit,
559
- ) => {
560
- const urlStr = url.toString();
561
-
562
- // Platform catalog
563
- if (urlStr.includes("/oauth/managed/catalog")) {
564
- return new Response(
565
- JSON.stringify([
566
- {
567
- handle: "platform_oauth:conn-platform-1",
568
- connection_id: "conn-platform-1",
569
- provider: "slack",
570
- account_label: "workspace@slack.com",
571
- scopes_granted: ["channels:read"],
572
- status: "active",
573
- },
574
- ]),
575
- { status: 200, headers: { "Content-Type": "application/json" } },
576
- );
577
- }
578
-
579
- // Platform token materialization
580
- if (urlStr.includes("/oauth/managed/materialize")) {
581
- return new Response(
582
- JSON.stringify({
583
- access_token: "xoxp-platform-token-12345678",
584
- token_type: "Bearer",
585
- expires_at: new Date(Date.now() + 3600_000).toISOString(),
586
- provider: "slack",
587
- handle: "platform_oauth:conn-platform-1",
588
- }),
589
- { status: 200, headers: { "Content-Type": "application/json" } },
590
- );
591
- }
592
-
593
- // The actual outbound Slack API call
594
- if (urlStr.includes("api.slack.com")) {
595
- return new Response(
596
- JSON.stringify({
597
- ok: true,
598
- channels: [{ name: "general" }],
599
- }),
600
- { status: 200, headers: { "Content-Type": "application/json" } },
601
- );
602
- }
603
-
604
- return new Response("Not found", { status: 404 });
605
- });
606
-
607
- const deps = buildDeps(fixture, [], {
608
- fetch: platformCatalogFetch,
609
- managedSubjectOptions: {
610
- platformBaseUrl: "https://api.vellum.ai",
611
- assistantApiKey: "test-api-key",
612
- assistantId: "test-assistant-id",
613
- fetch: platformCatalogFetch,
614
- },
615
- managedMaterializerOptions: {
616
- platformBaseUrl: "https://api.vellum.ai",
617
- assistantApiKey: "test-api-key",
618
- assistantId: "test-assistant-id",
619
- fetch: platformCatalogFetch,
620
- },
621
- });
622
-
623
- const result = await executeAuthenticatedHttpRequest(
624
- {
625
- credentialHandle: handle,
626
- method: "GET",
627
- url: "https://api.slack.com/api/conversations.list",
628
- purpose: "List Slack channels",
629
- },
630
- deps,
631
- );
632
-
633
- expect(result.success).toBe(true);
634
- expect(result.statusCode).toBe(200);
635
- expect(result.responseBody).toContain("general");
636
- expect(result.auditId).toBeDefined();
637
-
638
- // Token should be scrubbed from body
639
- expect(result.responseBody).not.toContain("xoxp-platform-token-12345678");
640
- });
641
-
642
- test("fails when managed mode is not configured", async () => {
643
- const handle = platformOAuthHandle("conn-platform-1");
644
-
645
- fixture.persistentStore.add({
646
- id: "grant-platform-unconfigured",
647
- tool: "http",
648
- pattern: "GET https://api.slack.com/data",
649
- scope: handle,
650
- createdAt: Date.now(),
651
- sessionId: "test-session",
652
- });
653
-
654
- // No managedSubjectOptions or managedMaterializerOptions
655
- const deps = buildDeps(fixture, []);
656
-
657
- const result = await executeAuthenticatedHttpRequest(
658
- {
659
- credentialHandle: handle,
660
- method: "GET",
661
- url: "https://api.slack.com/data",
662
- purpose: "Get data",
663
- },
664
- deps,
665
- );
666
-
667
- expect(result.success).toBe(false);
668
- expect(result.error).toBeDefined();
669
- expect(result.error!.code).toBe("MATERIALISATION_FAILED");
670
- expect(result.error!.message).toContain("not configured");
671
- });
672
- });
673
-
674
- // ---------------------------------------------------------------------------
675
- // Tests: Approval required short-circuit
676
- // ---------------------------------------------------------------------------
677
-
678
- describe("HTTP executor: approval-required short-circuit", () => {
679
- let fixture: TestFixture;
680
-
681
- beforeEach(() => {
682
- const storageKey = credentialKey("stripe", "api_key");
683
- fixture = createFixture(
684
- { [storageKey]: "sk_test_abcdefghijklmnop" },
685
- [buildStaticRecord({ service: "stripe", field: "api_key" })],
686
- );
687
- });
688
-
689
- afterEach(() => {
690
- rmSync(fixture.tmpDir, { recursive: true, force: true });
691
- });
692
-
693
- test("off-grant request returns approval_required without network call", async () => {
694
- const handle = localStaticHandle("stripe", "api_key");
695
- // No grant added — should be blocked
696
-
697
- const { fetch: fetchFn, requests } = mockFetchRecorder(200, '{"ok": true}');
698
- const deps = buildDeps(fixture, [], { fetch: fetchFn });
699
-
700
- const result = await executeAuthenticatedHttpRequest(
701
- {
702
- credentialHandle: handle,
703
- method: "POST",
704
- url: "https://api.stripe.com/v1/charges",
705
- purpose: "Create a charge",
706
- },
707
- deps,
708
- );
709
-
710
- // Request must be blocked
711
- expect(result.success).toBe(false);
712
- expect(result.error).toBeDefined();
713
- expect(result.error!.code).toBe("APPROVAL_REQUIRED");
714
- expect(result.error!.details).toBeDefined();
715
- expect((result.error!.details as Record<string, unknown>).proposal).toBeDefined();
716
- expect((result.error!.details as Record<string, unknown>).proposalHash).toBeDefined();
717
-
718
- // No network call should have been made
719
- expect(requests).toHaveLength(0);
720
- });
721
-
722
- test("proposal contains specific URL pattern (no wildcards)", async () => {
723
- const handle = localStaticHandle("stripe", "api_key");
724
-
725
- const deps = buildDeps(fixture, []);
726
-
727
- const result = await executeAuthenticatedHttpRequest(
728
- {
729
- credentialHandle: handle,
730
- method: "GET",
731
- url: "https://api.stripe.com/v1/charges/123",
732
- purpose: "Get charge",
733
- },
734
- deps,
735
- );
736
-
737
- expect(result.success).toBe(false);
738
- expect(result.error!.code).toBe("APPROVAL_REQUIRED");
739
-
740
- const proposal = (result.error!.details as Record<string, unknown>)
741
- .proposal as Record<string, unknown>;
742
- const allowedPatterns = proposal.allowedUrlPatterns as string[];
743
- expect(allowedPatterns).toBeDefined();
744
- expect(allowedPatterns.length).toBeGreaterThan(0);
745
-
746
- for (const pattern of allowedPatterns) {
747
- expect(pattern).not.toContain("/*");
748
- expect(pattern).not.toContain("*.");
749
- // Should have {:num} for the charge ID
750
- expect(pattern).toContain("{:num}");
751
- }
752
- });
753
- });
754
-
755
- // ---------------------------------------------------------------------------
756
- // Tests: Forbidden header rejection
757
- // ---------------------------------------------------------------------------
758
-
759
- describe("HTTP executor: forbidden header rejection", () => {
760
- let fixture: TestFixture;
761
-
762
- beforeEach(() => {
763
- const storageKey = credentialKey("github", "api_key");
764
- fixture = createFixture(
765
- { [storageKey]: "ghp_testtoken_12345678" },
766
- [buildStaticRecord({ service: "github", field: "api_key" })],
767
- );
768
- });
769
-
770
- afterEach(() => {
771
- rmSync(fixture.tmpDir, { recursive: true, force: true });
772
- });
773
-
774
- test("rejects request with Authorization header", async () => {
775
- const handle = localStaticHandle("github", "api_key");
776
-
777
- // Even with a matching grant, should be rejected
778
- fixture.persistentStore.add({
779
- id: "grant-with-auth",
780
- tool: "http",
781
- pattern: "GET https://api.github.com/user",
782
- scope: handle,
783
- createdAt: Date.now(),
784
- sessionId: "test-session",
785
- });
786
-
787
- const { fetch: fetchFn, requests } = mockFetchRecorder(200, '{"ok": true}');
788
- const deps = buildDeps(fixture, [], { fetch: fetchFn });
789
-
790
- const result = await executeAuthenticatedHttpRequest(
791
- {
792
- credentialHandle: handle,
793
- method: "GET",
794
- url: "https://api.github.com/user",
795
- headers: { Authorization: "Bearer smuggled-token" },
796
- purpose: "Get user info",
797
- },
798
- deps,
799
- );
800
-
801
- expect(result.success).toBe(false);
802
- expect(result.error!.code).toBe("FORBIDDEN_HEADERS");
803
- expect(result.error!.message).toContain("Authorization");
804
- expect(requests).toHaveLength(0);
805
- });
806
-
807
- test("rejects request with Cookie header", async () => {
808
- const handle = localStaticHandle("github", "api_key");
809
-
810
- fixture.persistentStore.add({
811
- id: "grant-with-cookie",
812
- tool: "http",
813
- pattern: "GET https://api.github.com/user",
814
- scope: handle,
815
- createdAt: Date.now(),
816
- sessionId: "test-session",
817
- });
818
-
819
- const { fetch: fetchFn, requests } = mockFetchRecorder(200, '{"ok": true}');
820
- const deps = buildDeps(fixture, [], { fetch: fetchFn });
821
-
822
- const result = await executeAuthenticatedHttpRequest(
823
- {
824
- credentialHandle: handle,
825
- method: "GET",
826
- url: "https://api.github.com/user",
827
- headers: { Cookie: "session=hijacked" },
828
- purpose: "Get user info",
829
- },
830
- deps,
831
- );
832
-
833
- expect(result.success).toBe(false);
834
- expect(result.error!.code).toBe("FORBIDDEN_HEADERS");
835
- expect(requests).toHaveLength(0);
836
- });
837
-
838
- test("rejects request with X-Api-Key header", async () => {
839
- const handle = localStaticHandle("github", "api_key");
840
-
841
- fixture.persistentStore.add({
842
- id: "grant-with-xapikey",
843
- tool: "http",
844
- pattern: "GET https://api.github.com/user",
845
- scope: handle,
846
- createdAt: Date.now(),
847
- sessionId: "test-session",
848
- });
849
-
850
- const deps = buildDeps(fixture, []);
851
-
852
- const result = await executeAuthenticatedHttpRequest(
853
- {
854
- credentialHandle: handle,
855
- method: "GET",
856
- url: "https://api.github.com/user",
857
- headers: { "X-Api-Key": "smuggled-key" },
858
- purpose: "Get user info",
859
- },
860
- deps,
861
- );
862
-
863
- expect(result.success).toBe(false);
864
- expect(result.error!.code).toBe("FORBIDDEN_HEADERS");
865
- });
866
- });
867
-
868
- // ---------------------------------------------------------------------------
869
- // Tests: Redirect denial
870
- // ---------------------------------------------------------------------------
871
-
872
- describe("HTTP executor: redirect denial", () => {
873
- let fixture: TestFixture;
874
-
875
- beforeEach(() => {
876
- const storageKey = credentialKey("github", "api_key");
877
- fixture = createFixture(
878
- { [storageKey]: "ghp_testtoken_12345678" },
879
- [buildStaticRecord({ service: "github", field: "api_key" })],
880
- );
881
- });
882
-
883
- afterEach(() => {
884
- rmSync(fixture.tmpDir, { recursive: true, force: true });
885
- });
886
-
887
- test("blocks redirect to domain not covered by grant", async () => {
888
- const handle = localStaticHandle("github", "api_key");
889
-
890
- // Grant only covers api.github.com
891
- fixture.persistentStore.add({
892
- id: "grant-github-only",
893
- tool: "http",
894
- pattern: "GET https://api.github.com/repos/owner/repo",
895
- scope: handle,
896
- createdAt: Date.now(),
897
- sessionId: "test-session",
898
- });
899
-
900
- // Redirect to an evil domain
901
- const fetchFn = mockFetchRedirect(
902
- "https://evil.example.com/steal-token",
903
- 302,
904
- 200,
905
- '{"stolen": true}',
906
- );
907
-
908
- const deps = buildDeps(fixture, [], { fetch: fetchFn });
909
-
910
- const result = await executeAuthenticatedHttpRequest(
911
- {
912
- credentialHandle: handle,
913
- method: "GET",
914
- url: "https://api.github.com/repos/owner/repo",
915
- purpose: "Get repo info",
916
- },
917
- deps,
918
- );
919
-
920
- expect(result.success).toBe(false);
921
- expect(result.error!.code).toBe("HTTP_REQUEST_FAILED");
922
- expect(result.error!.message).toContain("redirect");
923
- expect(result.error!.message).toContain("denied");
924
- });
925
-
926
- test("303 redirect evaluates policy against GET, not original method", async () => {
927
- const handle = localStaticHandle("github", "api_key");
928
-
929
- // Grant only covers GET — no POST grant exists
930
- fixture.persistentStore.add({
931
- id: "grant-github-get-only",
932
- tool: "http",
933
- pattern: "GET https://api.github.com/repos/owner/repo/result",
934
- scope: handle,
935
- createdAt: Date.now(),
936
- sessionId: "test-session",
937
- });
938
-
939
- // Also add a grant for the original POST endpoint
940
- fixture.persistentStore.add({
941
- id: "grant-github-post",
942
- tool: "http",
943
- pattern: "POST https://api.github.com/repos/owner/repo/action",
944
- scope: handle,
945
- createdAt: Date.now(),
946
- sessionId: "test-session",
947
- });
948
-
949
- // POST -> 303 redirect to a GET-only granted endpoint
950
- let callCount = 0;
951
- const requests: Array<{ url: string; method: string }> = [];
952
- const fetchFn = asFetch(async (
953
- url: string | URL | Request,
954
- init?: RequestInit,
955
- ) => {
956
- callCount++;
957
- requests.push({ url: url.toString(), method: init?.method ?? "GET" });
958
- if (callCount === 1) {
959
- return new Response(null, {
960
- status: 303,
961
- headers: { Location: "https://api.github.com/repos/owner/repo/result" },
962
- });
963
- }
964
- return new Response('{"status": "done"}', {
965
- status: 200,
966
- headers: { "Content-Type": "application/json" },
967
- });
968
- });
969
-
970
- const deps = buildDeps(fixture, [], { fetch: fetchFn });
971
-
972
- const result = await executeAuthenticatedHttpRequest(
973
- {
974
- credentialHandle: handle,
975
- method: "POST",
976
- url: "https://api.github.com/repos/owner/repo/action",
977
- body: '{"trigger": true}',
978
- purpose: "Trigger action",
979
- },
980
- deps,
981
- );
982
-
983
- // Should succeed — policy evaluates GET (post-303 method) against the grant
984
- expect(result.success).toBe(true);
985
- expect(result.statusCode).toBe(200);
986
-
987
- // Verify the redirect hop actually used GET
988
- expect(requests).toHaveLength(2);
989
- expect(requests[0].method).toBe("POST");
990
- expect(requests[1].method).toBe("GET");
991
- });
992
-
993
- test("allows redirect to path covered by same grant", async () => {
994
- const handle = localStaticHandle("github", "api_key");
995
-
996
- // Grant covers a template that matches both source and redirect target
997
- fixture.persistentStore.add({
998
- id: "grant-github-repos-template",
999
- tool: "http",
1000
- pattern: "GET https://api.github.com/repos/owner/repo",
1001
- scope: handle,
1002
- createdAt: Date.now(),
1003
- sessionId: "test-session",
1004
- });
1005
-
1006
- // Redirect to the same domain/path that matches the grant
1007
- let callCount = 0;
1008
- const fetchFn = asFetch(async (
1009
- _url: string | URL | Request,
1010
- _init?: RequestInit,
1011
- ) => {
1012
- callCount++;
1013
- if (callCount === 1) {
1014
- return new Response(null, {
1015
- status: 301,
1016
- headers: { Location: "https://api.github.com/repos/owner/repo" },
1017
- });
1018
- }
1019
- return new Response('{"name": "repo"}', {
1020
- status: 200,
1021
- headers: { "Content-Type": "application/json" },
1022
- });
1023
- });
1024
-
1025
- const deps = buildDeps(fixture, [], { fetch: fetchFn });
1026
-
1027
- const result = await executeAuthenticatedHttpRequest(
1028
- {
1029
- credentialHandle: handle,
1030
- method: "GET",
1031
- url: "https://api.github.com/repos/owner/repo",
1032
- purpose: "Get repo info",
1033
- },
1034
- deps,
1035
- );
1036
-
1037
- expect(result.success).toBe(true);
1038
- expect(result.statusCode).toBe(200);
1039
- });
1040
- });
1041
-
1042
- // ---------------------------------------------------------------------------
1043
- // Tests: Filtered response behaviour
1044
- // ---------------------------------------------------------------------------
1045
-
1046
- describe("HTTP executor: filtered response behaviour", () => {
1047
- let fixture: TestFixture;
1048
-
1049
- beforeEach(() => {
1050
- const storageKey = credentialKey("example", "key");
1051
- fixture = createFixture(
1052
- { [storageKey]: "secret-key-value-12345678" },
1053
- [buildStaticRecord({ service: "example", field: "key" })],
1054
- );
1055
- });
1056
-
1057
- afterEach(() => {
1058
- rmSync(fixture.tmpDir, { recursive: true, force: true });
1059
- });
1060
-
1061
- test("strips www-authenticate from response headers", async () => {
1062
- const handle = localStaticHandle("example", "key");
1063
-
1064
- fixture.persistentStore.add({
1065
- id: "grant-example",
1066
- tool: "http",
1067
- pattern: "GET https://api.example.com/protected",
1068
- scope: handle,
1069
- createdAt: Date.now(),
1070
- sessionId: "test-session",
1071
- });
1072
-
1073
- const fetchFn = mockFetch(401, '{"error": "unauthorized"}', {
1074
- "www-authenticate": "Bearer realm=api",
1075
- "content-type": "application/json",
1076
- });
1077
-
1078
- const deps = buildDeps(fixture, [], { fetch: fetchFn });
1079
-
1080
- const result = await executeAuthenticatedHttpRequest(
1081
- {
1082
- credentialHandle: handle,
1083
- method: "GET",
1084
- url: "https://api.example.com/protected",
1085
- purpose: "Access protected resource",
1086
- },
1087
- deps,
1088
- );
1089
-
1090
- expect(result.success).toBe(true);
1091
- expect(result.statusCode).toBe(401);
1092
- expect(result.responseHeaders!["www-authenticate"]).toBeUndefined();
1093
- expect(result.responseHeaders!["content-type"]).toBe("application/json");
1094
- });
1095
-
1096
- test("passes through safe response headers", async () => {
1097
- const handle = localStaticHandle("example", "key");
1098
-
1099
- fixture.persistentStore.add({
1100
- id: "grant-example-safe",
1101
- tool: "http",
1102
- pattern: "GET https://api.example.com/data",
1103
- scope: handle,
1104
- createdAt: Date.now(),
1105
- sessionId: "test-session",
1106
- });
1107
-
1108
- const fetchFn = mockFetch(200, '{"ok": true}', {
1109
- "content-type": "application/json",
1110
- "x-ratelimit-remaining": "42",
1111
- "x-request-id": "abc-def",
1112
- "etag": '"v1"',
1113
- });
1114
-
1115
- const deps = buildDeps(fixture, [], { fetch: fetchFn });
1116
-
1117
- const result = await executeAuthenticatedHttpRequest(
1118
- {
1119
- credentialHandle: handle,
1120
- method: "GET",
1121
- url: "https://api.example.com/data",
1122
- purpose: "Get data",
1123
- },
1124
- deps,
1125
- );
1126
-
1127
- expect(result.success).toBe(true);
1128
- expect(result.responseHeaders!["content-type"]).toBe("application/json");
1129
- expect(result.responseHeaders!["x-ratelimit-remaining"]).toBe("42");
1130
- expect(result.responseHeaders!["x-request-id"]).toBe("abc-def");
1131
- expect(result.responseHeaders!["etag"]).toBe('"v1"');
1132
- });
1133
- });
1134
-
1135
- // ---------------------------------------------------------------------------
1136
- // Tests: Audit summaries are token-free
1137
- // ---------------------------------------------------------------------------
1138
-
1139
- describe("HTTP executor: audit summary integrity", () => {
1140
- let fixture: TestFixture;
1141
-
1142
- beforeEach(() => {
1143
- const storageKey = credentialKey("github", "api_key");
1144
- fixture = createFixture(
1145
- { [storageKey]: "ghp_secrettoken_12345678" },
1146
- [buildStaticRecord({ service: "github", field: "api_key" })],
1147
- );
1148
- });
1149
-
1150
- afterEach(() => {
1151
- rmSync(fixture.tmpDir, { recursive: true, force: true });
1152
- });
1153
-
1154
- test("audit ID is returned on successful request", async () => {
1155
- const handle = localStaticHandle("github", "api_key");
1156
-
1157
- fixture.persistentStore.add({
1158
- id: "grant-audit-test",
1159
- tool: "http",
1160
- pattern: "GET https://api.github.com/user",
1161
- scope: handle,
1162
- createdAt: Date.now(),
1163
- sessionId: "test-session",
1164
- });
1165
-
1166
- const fetchFn = mockFetch(200, '{"login": "octocat"}');
1167
- const deps = buildDeps(fixture, [], { fetch: fetchFn });
1168
-
1169
- const result = await executeAuthenticatedHttpRequest(
1170
- {
1171
- credentialHandle: handle,
1172
- method: "GET",
1173
- url: "https://api.github.com/user",
1174
- purpose: "Get user info",
1175
- },
1176
- deps,
1177
- );
1178
-
1179
- expect(result.success).toBe(true);
1180
- expect(result.auditId).toBeDefined();
1181
- expect(typeof result.auditId).toBe("string");
1182
- expect(result.auditId!.length).toBeGreaterThan(0);
1183
- });
1184
-
1185
- test("audit ID is returned on materialisation failure", async () => {
1186
- // Grant exists but we'll break materialisation by using a handle that
1187
- // won't resolve (wrong service)
1188
- const badHandle = localStaticHandle("nonexistent", "key");
1189
- fixture.persistentStore.add({
1190
- id: "grant-bad-cred",
1191
- tool: "http",
1192
- pattern: "GET https://api.github.com/user",
1193
- scope: badHandle,
1194
- createdAt: Date.now(),
1195
- sessionId: "test-session",
1196
- });
1197
-
1198
- const deps = buildDeps(fixture, []);
1199
-
1200
- const result = await executeAuthenticatedHttpRequest(
1201
- {
1202
- credentialHandle: badHandle,
1203
- method: "GET",
1204
- url: "https://api.github.com/user",
1205
- purpose: "Get user info",
1206
- },
1207
- deps,
1208
- );
1209
-
1210
- expect(result.success).toBe(false);
1211
- expect(result.error!.code).toBe("MATERIALISATION_FAILED");
1212
- expect(result.auditId).toBeDefined();
1213
- });
1214
- });
1215
-
1216
- // ---------------------------------------------------------------------------
1217
- // Tests: Invalid handle
1218
- // ---------------------------------------------------------------------------
1219
-
1220
- describe("HTTP executor: invalid handle", () => {
1221
- let fixture: TestFixture;
1222
-
1223
- beforeEach(() => {
1224
- fixture = createFixture();
1225
- });
1226
-
1227
- afterEach(() => {
1228
- rmSync(fixture.tmpDir, { recursive: true, force: true });
1229
- });
1230
-
1231
- test("rejects completely invalid handle format", async () => {
1232
- const deps = buildDeps(fixture, []);
1233
-
1234
- const result = await executeAuthenticatedHttpRequest(
1235
- {
1236
- credentialHandle: "not-a-valid-handle",
1237
- method: "GET",
1238
- url: "https://api.example.com/data",
1239
- purpose: "Get data",
1240
- },
1241
- deps,
1242
- );
1243
-
1244
- expect(result.success).toBe(false);
1245
- expect(result.error!.code).toBe("INVALID_HANDLE");
1246
- });
1247
- });
1248
-
1249
- // ---------------------------------------------------------------------------
1250
- // Tests: Network error handling
1251
- // ---------------------------------------------------------------------------
1252
-
1253
- describe("HTTP executor: network error handling", () => {
1254
- let fixture: TestFixture;
1255
-
1256
- beforeEach(() => {
1257
- const storageKey = credentialKey("example", "key");
1258
- fixture = createFixture(
1259
- { [storageKey]: "secret-key-value-12345678" },
1260
- [buildStaticRecord({ service: "example", field: "key" })],
1261
- );
1262
- });
1263
-
1264
- afterEach(() => {
1265
- rmSync(fixture.tmpDir, { recursive: true, force: true });
1266
- });
1267
-
1268
- test("returns error when fetch fails", async () => {
1269
- const handle = localStaticHandle("example", "key");
1270
-
1271
- fixture.persistentStore.add({
1272
- id: "grant-network-fail",
1273
- tool: "http",
1274
- pattern: "GET https://api.example.com/data",
1275
- scope: handle,
1276
- createdAt: Date.now(),
1277
- sessionId: "test-session",
1278
- });
1279
-
1280
- const fetchFn = asFetch(async () => {
1281
- throw new Error("Connection refused");
1282
- });
1283
-
1284
- const deps = buildDeps(fixture, [], { fetch: fetchFn });
1285
-
1286
- const result = await executeAuthenticatedHttpRequest(
1287
- {
1288
- credentialHandle: handle,
1289
- method: "GET",
1290
- url: "https://api.example.com/data",
1291
- purpose: "Get data",
1292
- },
1293
- deps,
1294
- );
1295
-
1296
- expect(result.success).toBe(false);
1297
- expect(result.error!.code).toBe("HTTP_REQUEST_FAILED");
1298
- expect(result.error!.message).toContain("Connection refused");
1299
- expect(result.auditId).toBeDefined();
1300
- });
1301
-
1302
- test("error message is scrubbed of secret values", async () => {
1303
- const handle = localStaticHandle("example", "key");
1304
-
1305
- fixture.persistentStore.add({
1306
- id: "grant-error-scrub",
1307
- tool: "http",
1308
- pattern: "GET https://api.example.com/data",
1309
- scope: handle,
1310
- createdAt: Date.now(),
1311
- sessionId: "test-session",
1312
- });
1313
-
1314
- const fetchFn = asFetch(async () => {
1315
- throw new Error(
1316
- "Failed to connect with token secret-key-value-12345678 to api.example.com",
1317
- );
1318
- });
1319
-
1320
- const deps = buildDeps(fixture, [], { fetch: fetchFn });
1321
-
1322
- const result = await executeAuthenticatedHttpRequest(
1323
- {
1324
- credentialHandle: handle,
1325
- method: "GET",
1326
- url: "https://api.example.com/data",
1327
- purpose: "Get data",
1328
- },
1329
- deps,
1330
- );
1331
-
1332
- expect(result.success).toBe(false);
1333
- expect(result.error!.message).not.toContain("secret-key-value-12345678");
1334
- expect(result.error!.message).toContain("[CES:REDACTED]");
1335
- });
1336
- });