@vellumai/credential-executor 0.4.55

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