@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.
- package/Dockerfile +55 -0
- package/bun.lock +37 -0
- package/package.json +32 -0
- package/src/__tests__/command-executor.test.ts +1333 -0
- package/src/__tests__/command-validator.test.ts +708 -0
- package/src/__tests__/command-workspace.test.ts +997 -0
- package/src/__tests__/grant-store.test.ts +467 -0
- package/src/__tests__/http-executor.test.ts +1251 -0
- package/src/__tests__/http-policy.test.ts +970 -0
- package/src/__tests__/local-materializers.test.ts +826 -0
- package/src/__tests__/managed-materializers.test.ts +961 -0
- package/src/__tests__/toolstore.test.ts +539 -0
- package/src/__tests__/transport.test.ts +388 -0
- package/src/audit/store.ts +188 -0
- package/src/commands/auth-adapters.ts +169 -0
- package/src/commands/executor.ts +840 -0
- package/src/commands/output-scan.ts +157 -0
- package/src/commands/profiles.ts +282 -0
- package/src/commands/validator.ts +438 -0
- package/src/commands/workspace.ts +512 -0
- package/src/grants/index.ts +17 -0
- package/src/grants/persistent-store.ts +247 -0
- package/src/grants/rpc-handlers.ts +269 -0
- package/src/grants/temporary-store.ts +219 -0
- package/src/http/audit.ts +84 -0
- package/src/http/executor.ts +540 -0
- package/src/http/path-template.ts +179 -0
- package/src/http/policy.ts +256 -0
- package/src/http/response-filter.ts +233 -0
- package/src/index.ts +106 -0
- package/src/main.ts +263 -0
- package/src/managed-main.ts +420 -0
- package/src/materializers/local.ts +300 -0
- package/src/materializers/managed-platform.ts +270 -0
- package/src/paths.ts +137 -0
- package/src/server.ts +636 -0
- package/src/subjects/local.ts +177 -0
- package/src/subjects/managed.ts +290 -0
- package/src/toolstore/integrity.ts +94 -0
- package/src/toolstore/manifest.ts +154 -0
- package/src/toolstore/publish.ts +342 -0
- 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
|
+
});
|