@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,1028 +0,0 @@
1
- /**
2
- * Tests for managed OAuth subject resolution and materialization.
3
- *
4
- * Covers:
5
- * 1. Successful subject resolution from platform catalog
6
- * 2. Platform HTTP error handling (401, 403, 404, 5xx)
7
- * 3. Successful token materialization
8
- * 4. Expired token refresh via platform re-materialization
9
- * 5. Missing assistant API key behavior
10
- * 6. Fail-closed behavior when platform is unreachable
11
- * 7. Uniform subject shape between local and managed handles
12
- * 8. Materialized tokens are never persisted
13
- */
14
-
15
- import { describe, expect, test } from "bun:test";
16
-
17
- import { platformOAuthHandle } from "@vellumai/service-contracts/credential-rpc";
18
-
19
- import {
20
- type ManagedSubject,
21
- resolveManagedSubject,
22
- type PlatformCatalogEntry,
23
- type ResolvedSubject,
24
- } from "../subjects/managed.js";
25
- import {
26
- materializeManagedToken,
27
- type ManagedMaterializerOptions,
28
- } from "../materializers/managed-platform.js";
29
-
30
- // ---------------------------------------------------------------------------
31
- // Test helpers
32
- // ---------------------------------------------------------------------------
33
-
34
- const TEST_PLATFORM_URL = "https://api.test-platform.vellum.ai";
35
- const TEST_API_KEY = "test-api-key-abc123";
36
- const TEST_ASSISTANT_ID = "a1b2c3d4-e5f6-7890-abcd-ef1234567890";
37
-
38
- /**
39
- * Build a mock catalog response matching the platform's flat-array format.
40
- * The platform serializes with many=True, returning a JSON array directly.
41
- */
42
- function buildCatalogResponse(
43
- entries: PlatformCatalogEntry[],
44
- ): PlatformCatalogEntry[] {
45
- return entries;
46
- }
47
-
48
- /**
49
- * Build a mock platform token response matching
50
- * ManagedTokenMaterializeResponseSerializer.
51
- */
52
- function buildTokenResponse(overrides: {
53
- access_token?: string;
54
- token_type?: string;
55
- expires_at?: string | null;
56
- provider?: string;
57
- handle?: string;
58
- } = {}) {
59
- return {
60
- access_token: overrides.access_token ?? "mat_token_abc123",
61
- token_type: overrides.token_type ?? "Bearer",
62
- expires_at: overrides.expires_at ?? new Date(Date.now() + 3600_000).toISOString(),
63
- provider: overrides.provider ?? "google",
64
- handle: overrides.handle ?? "platform_oauth:conn_test123",
65
- };
66
- }
67
-
68
- /**
69
- * Create a mock fetch that responds based on URL path.
70
- */
71
- function createMockFetch(handlers: {
72
- catalog?: {
73
- status: number;
74
- body?: unknown;
75
- error?: Error;
76
- };
77
- materialize?: {
78
- status: number;
79
- body?: unknown;
80
- error?: Error;
81
- };
82
- }): typeof globalThis.fetch {
83
- return (async (input: RequestInfo | URL, _init?: RequestInit) => {
84
- const url = typeof input === "string" ? input : input.toString();
85
-
86
- if (url.includes("/oauth/managed/catalog")) {
87
- if (handlers.catalog?.error) {
88
- throw handlers.catalog.error;
89
- }
90
- return new Response(
91
- JSON.stringify(handlers.catalog?.body ?? []),
92
- {
93
- status: handlers.catalog?.status ?? 200,
94
- headers: { "Content-Type": "application/json" },
95
- },
96
- );
97
- }
98
-
99
- if (url.includes("/oauth/managed/materialize")) {
100
- if (handlers.materialize?.error) {
101
- throw handlers.materialize.error;
102
- }
103
- return new Response(
104
- JSON.stringify(handlers.materialize?.body ?? buildTokenResponse()),
105
- {
106
- status: handlers.materialize?.status ?? 200,
107
- headers: { "Content-Type": "application/json" },
108
- },
109
- );
110
- }
111
-
112
- return new Response("Not Found", { status: 404 });
113
- }) as typeof globalThis.fetch;
114
- }
115
-
116
- /**
117
- * Build a managed subject for materialization tests.
118
- */
119
- function buildManagedSubject(
120
- overrides: Partial<ManagedSubject> = {},
121
- ): ManagedSubject {
122
- return {
123
- source: "managed",
124
- handle: platformOAuthHandle("conn_test123"),
125
- provider: "google",
126
- connectionId: "conn_test123",
127
- accountInfo: "user@example.com",
128
- grantedScopes: ["email", "calendar"],
129
- status: "active",
130
- ...overrides,
131
- };
132
- }
133
-
134
- // ---------------------------------------------------------------------------
135
- // 1. Successful subject resolution
136
- // ---------------------------------------------------------------------------
137
-
138
- describe("resolveManagedSubject", () => {
139
- test("resolves a valid platform_oauth handle from the catalog", async () => {
140
- const handle = platformOAuthHandle("conn_abc123");
141
- const mockFetch = createMockFetch({
142
- catalog: {
143
- status: 200,
144
- body: buildCatalogResponse([
145
- {
146
- handle: "platform_oauth:conn_abc123",
147
- connection_id: "conn_abc123",
148
- provider: "google",
149
- account_label: "user@example.com",
150
- scopes_granted: ["email", "calendar"],
151
- status: "active",
152
- },
153
- ]),
154
- },
155
- });
156
-
157
- const result = await resolveManagedSubject(handle, {
158
- platformBaseUrl: TEST_PLATFORM_URL,
159
- assistantApiKey: TEST_API_KEY,
160
- assistantId: TEST_ASSISTANT_ID,
161
- fetch: mockFetch,
162
- });
163
-
164
- expect(result.ok).toBe(true);
165
- if (!result.ok) return;
166
-
167
- expect(result.subject.source).toBe("managed");
168
- expect(result.subject.handle).toBe(handle);
169
- expect(result.subject.provider).toBe("google");
170
- expect(result.subject.connectionId).toBe("conn_abc123");
171
- expect(result.subject.accountInfo).toBe("user@example.com");
172
- expect(result.subject.grantedScopes).toEqual(["email", "calendar"]);
173
- expect(result.subject.status).toBe("active");
174
- });
175
-
176
- test("resolves a handle when catalog has multiple connections", async () => {
177
- const handle = platformOAuthHandle("conn_second");
178
- const mockFetch = createMockFetch({
179
- catalog: {
180
- status: 200,
181
- body: buildCatalogResponse([
182
- { handle: "platform_oauth:conn_first", connection_id: "conn_first", provider: "slack" },
183
- {
184
- handle: "platform_oauth:conn_second",
185
- connection_id: "conn_second",
186
- provider: "github",
187
- account_label: "dev@github.com",
188
- scopes_granted: ["repo", "read:org"],
189
- status: "active",
190
- },
191
- { handle: "platform_oauth:conn_third", connection_id: "conn_third", provider: "google" },
192
- ]),
193
- },
194
- });
195
-
196
- const result = await resolveManagedSubject(handle, {
197
- platformBaseUrl: TEST_PLATFORM_URL,
198
- assistantApiKey: TEST_API_KEY,
199
- assistantId: TEST_ASSISTANT_ID,
200
- fetch: mockFetch,
201
- });
202
-
203
- expect(result.ok).toBe(true);
204
- if (!result.ok) return;
205
- expect(result.subject.provider).toBe("github");
206
- expect(result.subject.connectionId).toBe("conn_second");
207
- });
208
-
209
- test("defaults account_info to null and granted_scopes to empty array", async () => {
210
- const handle = platformOAuthHandle("conn_minimal");
211
- const mockFetch = createMockFetch({
212
- catalog: {
213
- status: 200,
214
- body: buildCatalogResponse([
215
- { handle: "platform_oauth:conn_minimal", connection_id: "conn_minimal", provider: "slack" },
216
- ]),
217
- },
218
- });
219
-
220
- const result = await resolveManagedSubject(handle, {
221
- platformBaseUrl: TEST_PLATFORM_URL,
222
- assistantApiKey: TEST_API_KEY,
223
- assistantId: TEST_ASSISTANT_ID,
224
- fetch: mockFetch,
225
- });
226
-
227
- expect(result.ok).toBe(true);
228
- if (!result.ok) return;
229
- expect(result.subject.accountInfo).toBeNull();
230
- expect(result.subject.grantedScopes).toEqual([]);
231
- expect(result.subject.status).toBe("unknown");
232
- });
233
-
234
- // -------------------------------------------------------------------------
235
- // Handle validation
236
- // -------------------------------------------------------------------------
237
-
238
- test("rejects an invalid handle format", async () => {
239
- const result = await resolveManagedSubject("not-a-valid-handle", {
240
- platformBaseUrl: TEST_PLATFORM_URL,
241
- assistantApiKey: TEST_API_KEY,
242
- assistantId: TEST_ASSISTANT_ID,
243
- });
244
-
245
- expect(result.ok).toBe(false);
246
- if (result.ok) return;
247
- expect(result.error.code).toBe("INVALID_HANDLE");
248
- });
249
-
250
- test("rejects a local_static handle", async () => {
251
- const result = await resolveManagedSubject("local_static:github/api_key", {
252
- platformBaseUrl: TEST_PLATFORM_URL,
253
- assistantApiKey: TEST_API_KEY,
254
- assistantId: TEST_ASSISTANT_ID,
255
- });
256
-
257
- expect(result.ok).toBe(false);
258
- if (result.ok) return;
259
- expect(result.error.code).toBe("WRONG_HANDLE_TYPE");
260
- expect(result.error.message).toContain("local_static");
261
- });
262
-
263
- test("rejects a local_oauth handle", async () => {
264
- const result = await resolveManagedSubject(
265
- "local_oauth:google/conn_local1",
266
- {
267
- platformBaseUrl: TEST_PLATFORM_URL,
268
- assistantApiKey: TEST_API_KEY,
269
- assistantId: TEST_ASSISTANT_ID,
270
- },
271
- );
272
-
273
- expect(result.ok).toBe(false);
274
- if (result.ok) return;
275
- expect(result.error.code).toBe("WRONG_HANDLE_TYPE");
276
- expect(result.error.message).toContain("local_oauth");
277
- });
278
-
279
- // -------------------------------------------------------------------------
280
- // Connection not found in catalog
281
- // -------------------------------------------------------------------------
282
-
283
- test("returns error when connection is not in the catalog", async () => {
284
- const handle = platformOAuthHandle("conn_nonexistent");
285
- const mockFetch = createMockFetch({
286
- catalog: {
287
- status: 200,
288
- body: buildCatalogResponse([
289
- { handle: "platform_oauth:conn_other", connection_id: "conn_other", provider: "slack" },
290
- ]),
291
- },
292
- });
293
-
294
- const result = await resolveManagedSubject(handle, {
295
- platformBaseUrl: TEST_PLATFORM_URL,
296
- assistantApiKey: TEST_API_KEY,
297
- assistantId: TEST_ASSISTANT_ID,
298
- fetch: mockFetch,
299
- });
300
-
301
- expect(result.ok).toBe(false);
302
- if (result.ok) return;
303
- expect(result.error.code).toBe("CONNECTION_NOT_FOUND");
304
- expect(result.error.message).toContain("conn_nonexistent");
305
- });
306
-
307
- // -------------------------------------------------------------------------
308
- // Platform HTTP errors
309
- // -------------------------------------------------------------------------
310
-
311
- test("handles platform 401 (invalid API key)", async () => {
312
- const handle = platformOAuthHandle("conn_test");
313
- const mockFetch = createMockFetch({
314
- catalog: { status: 401 },
315
- });
316
-
317
- const result = await resolveManagedSubject(handle, {
318
- platformBaseUrl: TEST_PLATFORM_URL,
319
- assistantApiKey: TEST_API_KEY,
320
- assistantId: TEST_ASSISTANT_ID,
321
- fetch: mockFetch,
322
- });
323
-
324
- expect(result.ok).toBe(false);
325
- if (result.ok) return;
326
- expect(result.error.code).toBe("PLATFORM_HTTP_401");
327
- expect(result.error.message).toContain("401");
328
- });
329
-
330
- test("handles platform 403 (forbidden)", async () => {
331
- const handle = platformOAuthHandle("conn_test");
332
- const mockFetch = createMockFetch({
333
- catalog: { status: 403 },
334
- });
335
-
336
- const result = await resolveManagedSubject(handle, {
337
- platformBaseUrl: TEST_PLATFORM_URL,
338
- assistantApiKey: TEST_API_KEY,
339
- assistantId: TEST_ASSISTANT_ID,
340
- fetch: mockFetch,
341
- });
342
-
343
- expect(result.ok).toBe(false);
344
- if (result.ok) return;
345
- expect(result.error.code).toBe("PLATFORM_HTTP_403");
346
- });
347
-
348
- test("handles platform 404 (catalog not found)", async () => {
349
- const handle = platformOAuthHandle("conn_test");
350
- const mockFetch = createMockFetch({
351
- catalog: { status: 404 },
352
- });
353
-
354
- const result = await resolveManagedSubject(handle, {
355
- platformBaseUrl: TEST_PLATFORM_URL,
356
- assistantApiKey: TEST_API_KEY,
357
- assistantId: TEST_ASSISTANT_ID,
358
- fetch: mockFetch,
359
- });
360
-
361
- expect(result.ok).toBe(false);
362
- if (result.ok) return;
363
- expect(result.error.code).toBe("PLATFORM_HTTP_404");
364
- });
365
-
366
- test("handles platform 500 (server error)", async () => {
367
- const handle = platformOAuthHandle("conn_test");
368
- const mockFetch = createMockFetch({
369
- catalog: { status: 500 },
370
- });
371
-
372
- const result = await resolveManagedSubject(handle, {
373
- platformBaseUrl: TEST_PLATFORM_URL,
374
- assistantApiKey: TEST_API_KEY,
375
- assistantId: TEST_ASSISTANT_ID,
376
- fetch: mockFetch,
377
- });
378
-
379
- expect(result.ok).toBe(false);
380
- if (result.ok) return;
381
- expect(result.error.code).toBe("PLATFORM_HTTP_500");
382
- });
383
-
384
- // -------------------------------------------------------------------------
385
- // Missing prerequisites
386
- // -------------------------------------------------------------------------
387
-
388
- test("fails when assistant API key is missing", async () => {
389
- const handle = platformOAuthHandle("conn_test");
390
-
391
- const result = await resolveManagedSubject(handle, {
392
- platformBaseUrl: TEST_PLATFORM_URL,
393
- assistantApiKey: "",
394
- assistantId: TEST_ASSISTANT_ID,
395
- });
396
-
397
- expect(result.ok).toBe(false);
398
- if (result.ok) return;
399
- expect(result.error.code).toBe("MISSING_API_KEY");
400
- });
401
-
402
- test("fails when platform base URL is missing", async () => {
403
- const handle = platformOAuthHandle("conn_test");
404
-
405
- const result = await resolveManagedSubject(handle, {
406
- platformBaseUrl: "",
407
- assistantApiKey: TEST_API_KEY,
408
- assistantId: TEST_ASSISTANT_ID,
409
- });
410
-
411
- expect(result.ok).toBe(false);
412
- if (result.ok) return;
413
- expect(result.error.code).toBe("MISSING_PLATFORM_URL");
414
- });
415
-
416
- test("fails when assistant ID is missing", async () => {
417
- const handle = platformOAuthHandle("conn_test");
418
-
419
- const result = await resolveManagedSubject(handle, {
420
- platformBaseUrl: TEST_PLATFORM_URL,
421
- assistantApiKey: TEST_API_KEY,
422
- assistantId: "",
423
- });
424
-
425
- expect(result.ok).toBe(false);
426
- if (result.ok) return;
427
- expect(result.error.code).toBe("MISSING_ASSISTANT_ID");
428
- });
429
-
430
- // -------------------------------------------------------------------------
431
- // Fail-closed behavior (network errors)
432
- // -------------------------------------------------------------------------
433
-
434
- test("fails closed when platform is unreachable", async () => {
435
- const handle = platformOAuthHandle("conn_test");
436
- const mockFetch = createMockFetch({
437
- catalog: {
438
- status: 0,
439
- error: new Error("ECONNREFUSED: Connection refused"),
440
- },
441
- });
442
-
443
- const result = await resolveManagedSubject(handle, {
444
- platformBaseUrl: TEST_PLATFORM_URL,
445
- assistantApiKey: TEST_API_KEY,
446
- assistantId: TEST_ASSISTANT_ID,
447
- fetch: mockFetch,
448
- });
449
-
450
- expect(result.ok).toBe(false);
451
- if (result.ok) return;
452
- expect(result.error.code).toBe("PLATFORM_UNREACHABLE");
453
- expect(result.error.message).toContain("ECONNREFUSED");
454
- });
455
-
456
- test("sanitizes API key from network error messages", async () => {
457
- const handle = platformOAuthHandle("conn_test");
458
- const mockFetch = createMockFetch({
459
- catalog: {
460
- status: 0,
461
- error: new Error(
462
- `Failed to connect with Api-Key ${TEST_API_KEY} header`,
463
- ),
464
- },
465
- });
466
-
467
- const result = await resolveManagedSubject(handle, {
468
- platformBaseUrl: TEST_PLATFORM_URL,
469
- assistantApiKey: TEST_API_KEY,
470
- assistantId: TEST_ASSISTANT_ID,
471
- fetch: mockFetch,
472
- });
473
-
474
- expect(result.ok).toBe(false);
475
- if (result.ok) return;
476
- expect(result.error.message).not.toContain(TEST_API_KEY);
477
- expect(result.error.message).toContain("[REDACTED]");
478
- });
479
-
480
- // -------------------------------------------------------------------------
481
- // Invalid catalog response format
482
- // -------------------------------------------------------------------------
483
-
484
- test("handles catalog response with missing connections field", async () => {
485
- const handle = platformOAuthHandle("conn_test");
486
- const mockFetch = createMockFetch({
487
- catalog: {
488
- status: 200,
489
- body: { data: [] }, // wrong shape
490
- },
491
- });
492
-
493
- const result = await resolveManagedSubject(handle, {
494
- platformBaseUrl: TEST_PLATFORM_URL,
495
- assistantApiKey: TEST_API_KEY,
496
- assistantId: TEST_ASSISTANT_ID,
497
- fetch: mockFetch,
498
- });
499
-
500
- expect(result.ok).toBe(false);
501
- if (result.ok) return;
502
- expect(result.error.code).toBe("INVALID_CATALOG_RESPONSE");
503
- });
504
- });
505
-
506
- // ---------------------------------------------------------------------------
507
- // 2. Managed token materialization
508
- // ---------------------------------------------------------------------------
509
-
510
- describe("materializeManagedToken", () => {
511
- test("materializes a token successfully", async () => {
512
- const subject = buildManagedSubject();
513
- const futureExpiry = new Date(Date.now() + 1_800_000).toISOString();
514
- const mockFetch = createMockFetch({
515
- materialize: {
516
- status: 200,
517
- body: buildTokenResponse({
518
- access_token: "fresh_token_xyz",
519
- expires_at: futureExpiry,
520
- }),
521
- },
522
- });
523
-
524
- const result = await materializeManagedToken(subject, {
525
- platformBaseUrl: TEST_PLATFORM_URL,
526
- assistantApiKey: TEST_API_KEY,
527
- assistantId: TEST_ASSISTANT_ID,
528
- fetch: mockFetch,
529
- });
530
-
531
- expect(result.ok).toBe(true);
532
- if (!result.ok) return;
533
-
534
- expect(result.token.accessToken).toBe("fresh_token_xyz");
535
- expect(result.token.tokenType).toBe("Bearer");
536
- expect(result.token.provider).toBe("google");
537
- expect(result.token.connectionId).toBe("conn_test123");
538
- // expires_at is parsed from ISO datetime
539
- expect(result.token.expiresAt).not.toBeNull();
540
- const expectedMs = new Date(futureExpiry).getTime();
541
- expect(result.token.expiresAt!).toBe(expectedMs);
542
- });
543
-
544
- test("defaults token_type to Bearer when not provided", async () => {
545
- const subject = buildManagedSubject();
546
- const mockFetch = createMockFetch({
547
- materialize: {
548
- status: 200,
549
- body: { access_token: "tok_no_type", expires_at: null },
550
- },
551
- });
552
-
553
- const result = await materializeManagedToken(subject, {
554
- platformBaseUrl: TEST_PLATFORM_URL,
555
- assistantApiKey: TEST_API_KEY,
556
- assistantId: TEST_ASSISTANT_ID,
557
- fetch: mockFetch,
558
- });
559
-
560
- expect(result.ok).toBe(true);
561
- if (!result.ok) return;
562
- expect(result.token.tokenType).toBe("Bearer");
563
- expect(result.token.expiresAt).toBeNull();
564
- });
565
-
566
- // -------------------------------------------------------------------------
567
- // Platform error responses during materialization
568
- // -------------------------------------------------------------------------
569
-
570
- test("handles platform 401 during materialization", async () => {
571
- const subject = buildManagedSubject();
572
- const mockFetch = createMockFetch({
573
- materialize: { status: 401 },
574
- });
575
-
576
- const result = await materializeManagedToken(subject, {
577
- platformBaseUrl: TEST_PLATFORM_URL,
578
- assistantApiKey: TEST_API_KEY,
579
- assistantId: TEST_ASSISTANT_ID,
580
- fetch: mockFetch,
581
- });
582
-
583
- expect(result.ok).toBe(false);
584
- if (result.ok) return;
585
- expect(result.error.code).toBe("PLATFORM_AUTH_FAILED");
586
- expect(result.error.message).toContain("401");
587
- });
588
-
589
- test("handles platform 403 during materialization", async () => {
590
- const subject = buildManagedSubject();
591
- const mockFetch = createMockFetch({
592
- materialize: { status: 403 },
593
- });
594
-
595
- const result = await materializeManagedToken(subject, {
596
- platformBaseUrl: TEST_PLATFORM_URL,
597
- assistantApiKey: TEST_API_KEY,
598
- assistantId: TEST_ASSISTANT_ID,
599
- fetch: mockFetch,
600
- });
601
-
602
- expect(result.ok).toBe(false);
603
- if (result.ok) return;
604
- expect(result.error.code).toBe("PLATFORM_FORBIDDEN");
605
- });
606
-
607
- test("handles platform 404 during materialization", async () => {
608
- const subject = buildManagedSubject();
609
- const mockFetch = createMockFetch({
610
- materialize: { status: 404 },
611
- });
612
-
613
- const result = await materializeManagedToken(subject, {
614
- platformBaseUrl: TEST_PLATFORM_URL,
615
- assistantApiKey: TEST_API_KEY,
616
- assistantId: TEST_ASSISTANT_ID,
617
- fetch: mockFetch,
618
- });
619
-
620
- expect(result.ok).toBe(false);
621
- if (result.ok) return;
622
- expect(result.error.code).toBe("CONNECTION_NOT_FOUND");
623
- expect(result.error.message).toContain("conn_test123");
624
- });
625
-
626
- test("handles platform 500 during materialization", async () => {
627
- const subject = buildManagedSubject();
628
- const mockFetch = createMockFetch({
629
- materialize: { status: 500 },
630
- });
631
-
632
- const result = await materializeManagedToken(subject, {
633
- platformBaseUrl: TEST_PLATFORM_URL,
634
- assistantApiKey: TEST_API_KEY,
635
- assistantId: TEST_ASSISTANT_ID,
636
- fetch: mockFetch,
637
- });
638
-
639
- expect(result.ok).toBe(false);
640
- if (result.ok) return;
641
- expect(result.error.code).toBe("PLATFORM_HTTP_500");
642
- });
643
-
644
- // -------------------------------------------------------------------------
645
- // Missing prerequisites
646
- // -------------------------------------------------------------------------
647
-
648
- test("fails when assistant API key is missing", async () => {
649
- const subject = buildManagedSubject();
650
-
651
- const result = await materializeManagedToken(subject, {
652
- platformBaseUrl: TEST_PLATFORM_URL,
653
- assistantApiKey: "",
654
- assistantId: TEST_ASSISTANT_ID,
655
- });
656
-
657
- expect(result.ok).toBe(false);
658
- if (result.ok) return;
659
- expect(result.error.code).toBe("MISSING_API_KEY");
660
- });
661
-
662
- test("fails when platform base URL is missing", async () => {
663
- const subject = buildManagedSubject();
664
-
665
- const result = await materializeManagedToken(subject, {
666
- platformBaseUrl: "",
667
- assistantApiKey: TEST_API_KEY,
668
- assistantId: TEST_ASSISTANT_ID,
669
- });
670
-
671
- expect(result.ok).toBe(false);
672
- if (result.ok) return;
673
- expect(result.error.code).toBe("MISSING_PLATFORM_URL");
674
- });
675
-
676
- test("fails when assistant ID is missing", async () => {
677
- const subject = buildManagedSubject();
678
-
679
- const result = await materializeManagedToken(subject, {
680
- platformBaseUrl: TEST_PLATFORM_URL,
681
- assistantApiKey: TEST_API_KEY,
682
- assistantId: "",
683
- });
684
-
685
- expect(result.ok).toBe(false);
686
- if (result.ok) return;
687
- expect(result.error.code).toBe("MISSING_ASSISTANT_ID");
688
- });
689
-
690
- // -------------------------------------------------------------------------
691
- // Fail-closed (network errors)
692
- // -------------------------------------------------------------------------
693
-
694
- test("fails closed when platform is unreachable during materialization", async () => {
695
- const subject = buildManagedSubject();
696
- const mockFetch = createMockFetch({
697
- materialize: {
698
- status: 0,
699
- error: new Error("ETIMEDOUT: Connection timed out"),
700
- },
701
- });
702
-
703
- const result = await materializeManagedToken(subject, {
704
- platformBaseUrl: TEST_PLATFORM_URL,
705
- assistantApiKey: TEST_API_KEY,
706
- assistantId: TEST_ASSISTANT_ID,
707
- fetch: mockFetch,
708
- });
709
-
710
- expect(result.ok).toBe(false);
711
- if (result.ok) return;
712
- expect(result.error.code).toBe("PLATFORM_UNREACHABLE");
713
- expect(result.error.message).toContain("ETIMEDOUT");
714
- });
715
-
716
- test("sanitizes API key from materialization error messages", async () => {
717
- const subject = buildManagedSubject();
718
- const mockFetch = createMockFetch({
719
- materialize: {
720
- status: 0,
721
- error: new Error(
722
- `Request failed with Api-Key ${TEST_API_KEY}`,
723
- ),
724
- },
725
- });
726
-
727
- const result = await materializeManagedToken(subject, {
728
- platformBaseUrl: TEST_PLATFORM_URL,
729
- assistantApiKey: TEST_API_KEY,
730
- assistantId: TEST_ASSISTANT_ID,
731
- fetch: mockFetch,
732
- });
733
-
734
- expect(result.ok).toBe(false);
735
- if (result.ok) return;
736
- expect(result.error.message).not.toContain(TEST_API_KEY);
737
- expect(result.error.message).toContain("[REDACTED]");
738
- });
739
-
740
- // -------------------------------------------------------------------------
741
- // Invalid token response
742
- // -------------------------------------------------------------------------
743
-
744
- test("handles response with missing access_token", async () => {
745
- const subject = buildManagedSubject();
746
- const mockFetch = createMockFetch({
747
- materialize: {
748
- status: 200,
749
- body: { token_type: "Bearer", expires_at: new Date(Date.now() + 3600_000).toISOString() },
750
- },
751
- });
752
-
753
- const result = await materializeManagedToken(subject, {
754
- platformBaseUrl: TEST_PLATFORM_URL,
755
- assistantApiKey: TEST_API_KEY,
756
- assistantId: TEST_ASSISTANT_ID,
757
- fetch: mockFetch,
758
- });
759
-
760
- expect(result.ok).toBe(false);
761
- if (result.ok) return;
762
- expect(result.error.code).toBe("INVALID_TOKEN_RESPONSE");
763
- expect(result.error.message).toContain("access_token");
764
- });
765
-
766
- // -------------------------------------------------------------------------
767
- // Expired token refresh via re-materialization
768
- // -------------------------------------------------------------------------
769
-
770
- test("re-materialization after expiry returns a fresh token", async () => {
771
- const subject = buildManagedSubject();
772
- let callCount = 0;
773
-
774
- const mockFetch = ((
775
- input: RequestInfo | URL,
776
- _init?: RequestInit,
777
- ) => {
778
- callCount++;
779
- const url = typeof input === "string" ? input : input.toString();
780
- if (url.includes("/oauth/managed/materialize")) {
781
- // Each call returns a different token to prove we got a fresh one
782
- return Promise.resolve(
783
- new Response(
784
- JSON.stringify(
785
- buildTokenResponse({
786
- access_token: `fresh_token_${callCount}`,
787
- expires_at: new Date(Date.now() + 3600_000).toISOString(),
788
- }),
789
- ),
790
- {
791
- status: 200,
792
- headers: { "Content-Type": "application/json" },
793
- },
794
- ),
795
- );
796
- }
797
- return Promise.resolve(new Response("Not Found", { status: 404 }));
798
- }) as typeof globalThis.fetch;
799
-
800
- const opts: ManagedMaterializerOptions = {
801
- platformBaseUrl: TEST_PLATFORM_URL,
802
- assistantApiKey: TEST_API_KEY,
803
- assistantId: TEST_ASSISTANT_ID,
804
- fetch: mockFetch,
805
- };
806
-
807
- // First materialization
808
- const result1 = await materializeManagedToken(subject, opts);
809
- expect(result1.ok).toBe(true);
810
- if (!result1.ok) return;
811
- expect(result1.token.accessToken).toBe("fresh_token_1");
812
-
813
- // Simulate expiry by re-materializing (in the real system, CES would
814
- // check expiresAt and call materialize again)
815
- const result2 = await materializeManagedToken(subject, opts);
816
- expect(result2.ok).toBe(true);
817
- if (!result2.ok) return;
818
- expect(result2.token.accessToken).toBe("fresh_token_2");
819
- expect(callCount).toBe(2);
820
- });
821
- });
822
-
823
- // ---------------------------------------------------------------------------
824
- // 3. Uniform subject interface
825
- // ---------------------------------------------------------------------------
826
-
827
- describe("uniform subject interface", () => {
828
- test("managed subject conforms to ResolvedSubject interface", async () => {
829
- const handle = platformOAuthHandle("conn_uniform");
830
- const mockFetch = createMockFetch({
831
- catalog: {
832
- status: 200,
833
- body: buildCatalogResponse([
834
- {
835
- handle: "platform_oauth:conn_uniform",
836
- connection_id: "conn_uniform",
837
- provider: "github",
838
- account_label: "dev@example.com",
839
- scopes_granted: ["repo"],
840
- status: "active",
841
- },
842
- ]),
843
- },
844
- });
845
-
846
- const result = await resolveManagedSubject(handle, {
847
- platformBaseUrl: TEST_PLATFORM_URL,
848
- assistantApiKey: TEST_API_KEY,
849
- assistantId: TEST_ASSISTANT_ID,
850
- fetch: mockFetch,
851
- });
852
-
853
- expect(result.ok).toBe(true);
854
- if (!result.ok) return;
855
-
856
- // Verify the managed subject can be treated as a ResolvedSubject
857
- const subject: ResolvedSubject = result.subject;
858
- expect(subject.source).toBe("managed");
859
- expect(subject.handle).toBe(handle);
860
- expect(subject.provider).toBe("github");
861
- expect(subject.connectionId).toBe("conn_uniform");
862
- });
863
-
864
- test("managed subject has 'managed' source for branching", async () => {
865
- const handle = platformOAuthHandle("conn_branch");
866
- const mockFetch = createMockFetch({
867
- catalog: {
868
- status: 200,
869
- body: buildCatalogResponse([
870
- { handle: "platform_oauth:conn_branch", connection_id: "conn_branch", provider: "slack" },
871
- ]),
872
- },
873
- });
874
-
875
- const result = await resolveManagedSubject(handle, {
876
- platformBaseUrl: TEST_PLATFORM_URL,
877
- assistantApiKey: TEST_API_KEY,
878
- assistantId: TEST_ASSISTANT_ID,
879
- fetch: mockFetch,
880
- });
881
-
882
- expect(result.ok).toBe(true);
883
- if (!result.ok) return;
884
-
885
- // CES execution paths can branch on source without casting
886
- const subject: ResolvedSubject = result.subject;
887
- switch (subject.source) {
888
- case "managed":
889
- // Managed path — materialize via platform
890
- expect(true).toBe(true);
891
- break;
892
- case "local":
893
- // Local path — should not reach here
894
- expect(true).toBe(false);
895
- break;
896
- }
897
- });
898
- });
899
-
900
- // ---------------------------------------------------------------------------
901
- // 4. Token non-persistence invariant
902
- // ---------------------------------------------------------------------------
903
-
904
- describe("token non-persistence invariant", () => {
905
- test("materialized token is returned in-memory only — no disk writes", async () => {
906
- const subject = buildManagedSubject();
907
- const mockFetch = createMockFetch({
908
- materialize: {
909
- status: 200,
910
- body: buildTokenResponse({ access_token: "ephemeral_token" }),
911
- },
912
- });
913
-
914
- const result = await materializeManagedToken(subject, {
915
- platformBaseUrl: TEST_PLATFORM_URL,
916
- assistantApiKey: TEST_API_KEY,
917
- assistantId: TEST_ASSISTANT_ID,
918
- fetch: mockFetch,
919
- });
920
-
921
- expect(result.ok).toBe(true);
922
- if (!result.ok) return;
923
-
924
- // The MaterializedToken type has no persist/save methods —
925
- // it is a plain data object. Verify it is a simple object.
926
- expect(typeof result.token.accessToken).toBe("string");
927
- expect(typeof result.token.tokenType).toBe("string");
928
- expect(typeof result.token.provider).toBe("string");
929
- expect(typeof result.token.connectionId).toBe("string");
930
-
931
- // Verify no reference to storage backends, file paths, or persist calls
932
- const tokenKeys = Object.keys(result.token);
933
- expect(tokenKeys).toEqual([
934
- "accessToken",
935
- "tokenType",
936
- "expiresAt",
937
- "provider",
938
- "connectionId",
939
- ]);
940
- });
941
-
942
- test("materializer source code does not import credential-storage", async () => {
943
- // Static verification: the materializer must not import from
944
- // @vellumai/credential-storage, which would indicate it might
945
- // persist tokens locally.
946
- const { readFileSync } = await import("node:fs");
947
- const { resolve } = await import("node:path");
948
-
949
- const src = readFileSync(
950
- resolve(__dirname, "..", "materializers", "managed-platform.ts"),
951
- "utf-8",
952
- );
953
-
954
- expect(src).not.toContain("credential-storage");
955
- expect(src).not.toContain("writeFile");
956
- expect(src).not.toContain("SecureKeyBackend");
957
- expect(src).not.toContain("persistOAuthTokens");
958
- });
959
-
960
- test("subject resolver source code does not import credential-storage", async () => {
961
- const { readFileSync } = await import("node:fs");
962
- const { resolve } = await import("node:path");
963
-
964
- const src = readFileSync(
965
- resolve(__dirname, "..", "subjects", "managed.ts"),
966
- "utf-8",
967
- );
968
-
969
- expect(src).not.toContain("credential-storage");
970
- expect(src).not.toContain("SecureKeyBackend");
971
- });
972
- });
973
-
974
- // ---------------------------------------------------------------------------
975
- // 5. End-to-end resolve + materialize
976
- // ---------------------------------------------------------------------------
977
-
978
- describe("end-to-end resolve and materialize", () => {
979
- test("resolves a handle and then materializes a token", async () => {
980
- const handle = platformOAuthHandle("conn_e2e");
981
- const mockFetch = createMockFetch({
982
- catalog: {
983
- status: 200,
984
- body: buildCatalogResponse([
985
- {
986
- handle: "platform_oauth:conn_e2e",
987
- connection_id: "conn_e2e",
988
- provider: "google",
989
- account_label: "e2e@example.com",
990
- scopes_granted: ["drive"],
991
- status: "active",
992
- },
993
- ]),
994
- },
995
- materialize: {
996
- status: 200,
997
- body: buildTokenResponse({
998
- access_token: "e2e_access_token",
999
- expires_at: new Date(Date.now() + 7200_000).toISOString(),
1000
- }),
1001
- },
1002
- });
1003
-
1004
- const opts = {
1005
- platformBaseUrl: TEST_PLATFORM_URL,
1006
- assistantApiKey: TEST_API_KEY,
1007
- assistantId: TEST_ASSISTANT_ID,
1008
- fetch: mockFetch,
1009
- };
1010
-
1011
- // Phase 1: Resolve
1012
- const resolveResult = await resolveManagedSubject(handle, opts);
1013
- expect(resolveResult.ok).toBe(true);
1014
- if (!resolveResult.ok) return;
1015
-
1016
- // Phase 2: Materialize using the resolved subject
1017
- const matResult = await materializeManagedToken(
1018
- resolveResult.subject,
1019
- opts,
1020
- );
1021
- expect(matResult.ok).toBe(true);
1022
- if (!matResult.ok) return;
1023
-
1024
- expect(matResult.token.accessToken).toBe("e2e_access_token");
1025
- expect(matResult.token.provider).toBe("google");
1026
- expect(matResult.token.connectionId).toBe("conn_e2e");
1027
- });
1028
- });