@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.
- package/Dockerfile +1 -1
- package/node_modules/@vellumai/service-contracts/package.json +1 -2
- package/node_modules/@vellumai/service-contracts/src/__tests__/attachment-naming.test.ts +104 -0
- package/node_modules/@vellumai/service-contracts/src/__tests__/contracts.test.ts +0 -2
- package/node_modules/@vellumai/service-contracts/src/attachment-naming.ts +118 -0
- package/node_modules/@vellumai/service-contracts/src/credential-rpc.ts +3 -5
- package/node_modules/@vellumai/service-contracts/src/index.ts +2 -4
- package/node_modules/@vellumai/service-contracts/src/rpc.ts +4 -447
- package/package.json +2 -3
- package/src/__tests__/bulk-set-credentials.test.ts +1 -1
- package/src/__tests__/local-standalone.test.ts +5 -36
- package/src/__tests__/managed-integration.test.ts +112 -91
- package/src/__tests__/managed-reconnect.test.ts +2 -2
- package/src/__tests__/transport.test.ts +23 -27
- package/src/cli.ts +1 -1
- package/src/index.ts +8 -88
- package/src/main.ts +228 -340
- package/src/paths.ts +4 -20
- package/src/server.ts +52 -469
- package/node_modules/@vellumai/service-contracts/src/__tests__/grants.test.ts +0 -686
- package/node_modules/@vellumai/service-contracts/src/grants.ts +0 -184
- package/node_modules/@vellumai/service-contracts/src/rendering.ts +0 -135
- package/src/__tests__/command-executor.test.ts +0 -1879
- package/src/__tests__/command-validator.test.ts +0 -1405
- package/src/__tests__/command-workspace.test.ts +0 -1050
- package/src/__tests__/grant-store.test.ts +0 -689
- package/src/__tests__/http-executor.test.ts +0 -1336
- package/src/__tests__/http-policy.test.ts +0 -1069
- package/src/__tests__/local-materializers.test.ts +0 -860
- package/src/__tests__/local-token-refresh.test.ts +0 -361
- package/src/__tests__/manage-secure-command-tool.test.ts +0 -134
- package/src/__tests__/managed-lazy-getters.test.ts +0 -359
- package/src/__tests__/managed-materializers.test.ts +0 -1028
- package/src/__tests__/managed-rejection.test.ts +0 -43
- package/src/__tests__/toolstore.test.ts +0 -773
- package/src/audit/store.ts +0 -188
- package/src/commands/auth-adapters.ts +0 -169
- package/src/commands/egress-hooks.ts +0 -203
- package/src/commands/executor.ts +0 -1155
- package/src/commands/output-scan.ts +0 -157
- package/src/commands/profiles.ts +0 -286
- package/src/commands/validator.ts +0 -702
- package/src/commands/workspace.ts +0 -550
- package/src/grants/index.ts +0 -17
- package/src/grants/persistent-store.ts +0 -309
- package/src/grants/rpc-handlers.ts +0 -293
- package/src/grants/temporary-store.ts +0 -289
- package/src/http/audit.ts +0 -84
- package/src/http/executor.ts +0 -684
- package/src/http/path-template.ts +0 -245
- package/src/http/policy.ts +0 -238
- package/src/http/response-filter.ts +0 -233
- package/src/managed-errors.ts +0 -9
- package/src/managed-lazy-getters.ts +0 -106
- package/src/managed-main.ts +0 -822
- package/src/materializers/local-oauth-lookup.ts +0 -98
- package/src/materializers/local-token-refresh.ts +0 -287
- package/src/materializers/local.ts +0 -316
- package/src/materializers/managed-platform.ts +0 -295
- package/src/subjects/local.ts +0 -177
- package/src/subjects/managed.ts +0 -311
- package/src/subjects/policy.ts +0 -79
- package/src/toolstore/integrity.ts +0 -94
- package/src/toolstore/manifest.ts +0 -154
- package/src/toolstore/publish.ts +0 -571
|
@@ -1,361 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for CES local-token-refresh `refresh_url` support.
|
|
3
|
-
*
|
|
4
|
-
* Verifies that `createLocalTokenRefreshFn`:
|
|
5
|
-
* 1. Uses `refresh_url` when it is set on the provider.
|
|
6
|
-
* 2. Falls back to `token_url` when `refresh_url` is null or empty.
|
|
7
|
-
* 3. Preserves existing `token_exchange_body_format` and `token_endpoint_auth_method` behaviour.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import Database from "bun:sqlite";
|
|
11
|
-
import { mkdirSync, rmSync } from "node:fs";
|
|
12
|
-
import { join } from "node:path";
|
|
13
|
-
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
14
|
-
|
|
15
|
-
import type {
|
|
16
|
-
SecureKeyBackend,
|
|
17
|
-
SecureKeyDeleteResult,
|
|
18
|
-
} from "@vellumai/credential-storage";
|
|
19
|
-
|
|
20
|
-
import { createLocalTokenRefreshFn } from "../materializers/local-token-refresh.js";
|
|
21
|
-
|
|
22
|
-
// ---------------------------------------------------------------------------
|
|
23
|
-
// Test helpers
|
|
24
|
-
// ---------------------------------------------------------------------------
|
|
25
|
-
|
|
26
|
-
function createMemoryBackend(
|
|
27
|
-
initial: Record<string, string> = {},
|
|
28
|
-
): SecureKeyBackend {
|
|
29
|
-
const store = new Map<string, string>(Object.entries(initial));
|
|
30
|
-
return {
|
|
31
|
-
async get(key: string): Promise<string | undefined> {
|
|
32
|
-
return store.get(key);
|
|
33
|
-
},
|
|
34
|
-
async set(key: string, value: string): Promise<boolean> {
|
|
35
|
-
store.set(key, value);
|
|
36
|
-
return true;
|
|
37
|
-
},
|
|
38
|
-
async delete(key: string): Promise<SecureKeyDeleteResult> {
|
|
39
|
-
if (store.has(key)) {
|
|
40
|
-
store.delete(key);
|
|
41
|
-
return "deleted";
|
|
42
|
-
}
|
|
43
|
-
return "not-found";
|
|
44
|
-
},
|
|
45
|
-
async list(): Promise<string[]> {
|
|
46
|
-
return Array.from(store.keys());
|
|
47
|
-
},
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/** Unique temp root for each test run. */
|
|
52
|
-
let tmpRoot: string;
|
|
53
|
-
|
|
54
|
-
function setupTestDb(opts: {
|
|
55
|
-
providerKey?: string;
|
|
56
|
-
tokenUrl?: string;
|
|
57
|
-
refreshUrl?: string | null;
|
|
58
|
-
tokenEndpointAuthMethod?: string | null;
|
|
59
|
-
tokenExchangeBodyFormat?: string | null;
|
|
60
|
-
}): string {
|
|
61
|
-
const providerKey = opts.providerKey ?? "test-provider";
|
|
62
|
-
const tokenUrl = opts.tokenUrl ?? "https://provider.example.com/token";
|
|
63
|
-
const refreshUrl = opts.refreshUrl ?? null;
|
|
64
|
-
const authMethod = opts.tokenEndpointAuthMethod ?? "client_secret_post";
|
|
65
|
-
const bodyFormat = opts.tokenExchangeBodyFormat ?? "form";
|
|
66
|
-
|
|
67
|
-
tmpRoot = join(
|
|
68
|
-
"/tmp",
|
|
69
|
-
`ces-token-refresh-test-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
70
|
-
);
|
|
71
|
-
const dbDir = join(tmpRoot, "workspace", "data", "db");
|
|
72
|
-
mkdirSync(dbDir, { recursive: true });
|
|
73
|
-
|
|
74
|
-
const dbPath = join(dbDir, "assistant.db");
|
|
75
|
-
const db = new Database(dbPath);
|
|
76
|
-
|
|
77
|
-
// Create minimal schema matching the assistant's tables
|
|
78
|
-
db.exec(/*sql*/ `
|
|
79
|
-
CREATE TABLE oauth_providers (
|
|
80
|
-
provider_key TEXT PRIMARY KEY,
|
|
81
|
-
auth_url TEXT NOT NULL,
|
|
82
|
-
token_url TEXT NOT NULL,
|
|
83
|
-
refresh_url TEXT,
|
|
84
|
-
token_endpoint_auth_method TEXT,
|
|
85
|
-
token_exchange_body_format TEXT,
|
|
86
|
-
created_at INTEGER NOT NULL,
|
|
87
|
-
updated_at INTEGER NOT NULL
|
|
88
|
-
)
|
|
89
|
-
`);
|
|
90
|
-
|
|
91
|
-
db.exec(/*sql*/ `
|
|
92
|
-
CREATE TABLE oauth_apps (
|
|
93
|
-
id TEXT PRIMARY KEY,
|
|
94
|
-
provider_key TEXT NOT NULL REFERENCES oauth_providers(provider_key),
|
|
95
|
-
client_id TEXT NOT NULL,
|
|
96
|
-
client_secret_credential_path TEXT NOT NULL,
|
|
97
|
-
created_at INTEGER NOT NULL,
|
|
98
|
-
updated_at INTEGER NOT NULL
|
|
99
|
-
)
|
|
100
|
-
`);
|
|
101
|
-
|
|
102
|
-
db.exec(/*sql*/ `
|
|
103
|
-
CREATE TABLE oauth_connections (
|
|
104
|
-
id TEXT PRIMARY KEY,
|
|
105
|
-
oauth_app_id TEXT NOT NULL REFERENCES oauth_apps(id),
|
|
106
|
-
provider_key TEXT NOT NULL,
|
|
107
|
-
status TEXT NOT NULL DEFAULT 'active',
|
|
108
|
-
created_at INTEGER NOT NULL,
|
|
109
|
-
updated_at INTEGER NOT NULL
|
|
110
|
-
)
|
|
111
|
-
`);
|
|
112
|
-
|
|
113
|
-
const now = Date.now();
|
|
114
|
-
|
|
115
|
-
db.exec(/*sql*/ `
|
|
116
|
-
INSERT INTO oauth_providers (provider_key, auth_url, token_url, refresh_url, token_endpoint_auth_method, token_exchange_body_format, created_at, updated_at)
|
|
117
|
-
VALUES ('${providerKey}', 'https://provider.example.com/authorize', '${tokenUrl}', ${refreshUrl === null ? "NULL" : `'${refreshUrl}'`}, ${authMethod === null ? "NULL" : `'${authMethod}'`}, ${bodyFormat === null ? "NULL" : `'${bodyFormat}'`}, ${now}, ${now})
|
|
118
|
-
`);
|
|
119
|
-
|
|
120
|
-
db.exec(/*sql*/ `
|
|
121
|
-
INSERT INTO oauth_apps (id, provider_key, client_id, client_secret_credential_path, created_at, updated_at)
|
|
122
|
-
VALUES ('app-1', '${providerKey}', 'test-client-id', 'oauth_app/app-1/client_secret', ${now}, ${now})
|
|
123
|
-
`);
|
|
124
|
-
|
|
125
|
-
db.exec(/*sql*/ `
|
|
126
|
-
INSERT INTO oauth_connections (id, oauth_app_id, provider_key, status, created_at, updated_at)
|
|
127
|
-
VALUES ('conn-1', 'app-1', '${providerKey}', 'active', ${now}, ${now})
|
|
128
|
-
`);
|
|
129
|
-
|
|
130
|
-
db.close();
|
|
131
|
-
// Return the workspace directory — callers pass this to
|
|
132
|
-
// createLocalTokenRefreshFn(workspaceDir, ...).
|
|
133
|
-
return join(tmpRoot, "workspace");
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// ---------------------------------------------------------------------------
|
|
137
|
-
// Mock fetch
|
|
138
|
-
// ---------------------------------------------------------------------------
|
|
139
|
-
|
|
140
|
-
const originalFetch = globalThis.fetch;
|
|
141
|
-
|
|
142
|
-
function mockFetch(capturedUrls: string[]): void {
|
|
143
|
-
globalThis.fetch = mock(async (input: string | URL | Request) => {
|
|
144
|
-
const url =
|
|
145
|
-
typeof input === "string"
|
|
146
|
-
? input
|
|
147
|
-
: input instanceof URL
|
|
148
|
-
? input.toString()
|
|
149
|
-
: input.url;
|
|
150
|
-
capturedUrls.push(url);
|
|
151
|
-
return new Response(
|
|
152
|
-
JSON.stringify({
|
|
153
|
-
access_token: "new-access-token",
|
|
154
|
-
refresh_token: "new-refresh-token",
|
|
155
|
-
expires_in: 3600,
|
|
156
|
-
}),
|
|
157
|
-
{ status: 200, headers: { "Content-Type": "application/json" } },
|
|
158
|
-
);
|
|
159
|
-
}) as unknown as typeof globalThis.fetch;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
// ---------------------------------------------------------------------------
|
|
163
|
-
// Tests
|
|
164
|
-
// ---------------------------------------------------------------------------
|
|
165
|
-
|
|
166
|
-
describe("createLocalTokenRefreshFn – refresh_url support", () => {
|
|
167
|
-
const capturedUrls: string[] = [];
|
|
168
|
-
|
|
169
|
-
beforeEach(() => {
|
|
170
|
-
capturedUrls.length = 0;
|
|
171
|
-
mockFetch(capturedUrls);
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
afterEach(() => {
|
|
175
|
-
globalThis.fetch = originalFetch;
|
|
176
|
-
if (tmpRoot) {
|
|
177
|
-
rmSync(tmpRoot, { recursive: true, force: true });
|
|
178
|
-
}
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
test("uses refresh_url when set on the provider", async () => {
|
|
182
|
-
const root = setupTestDb({
|
|
183
|
-
tokenUrl: "https://provider.example.com/token",
|
|
184
|
-
refreshUrl: "https://provider.example.com/refresh",
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
const backend = createMemoryBackend({
|
|
188
|
-
"oauth_app/app-1/client_secret": "test-secret",
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
const refreshFn = createLocalTokenRefreshFn(root, backend);
|
|
192
|
-
const result = await refreshFn("conn-1", "old-refresh-token");
|
|
193
|
-
|
|
194
|
-
expect(result.success).toBe(true);
|
|
195
|
-
expect(capturedUrls).toHaveLength(1);
|
|
196
|
-
expect(capturedUrls[0]).toBe("https://provider.example.com/refresh");
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
test("falls back to token_url when refresh_url is null", async () => {
|
|
200
|
-
const root = setupTestDb({
|
|
201
|
-
tokenUrl: "https://provider.example.com/token",
|
|
202
|
-
refreshUrl: null,
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
const backend = createMemoryBackend({
|
|
206
|
-
"oauth_app/app-1/client_secret": "test-secret",
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
const refreshFn = createLocalTokenRefreshFn(root, backend);
|
|
210
|
-
const result = await refreshFn("conn-1", "old-refresh-token");
|
|
211
|
-
|
|
212
|
-
expect(result.success).toBe(true);
|
|
213
|
-
expect(capturedUrls).toHaveLength(1);
|
|
214
|
-
expect(capturedUrls[0]).toBe("https://provider.example.com/token");
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
test("falls back to token_url when refresh_url is an empty string", async () => {
|
|
218
|
-
const root = setupTestDb({
|
|
219
|
-
tokenUrl: "https://provider.example.com/token",
|
|
220
|
-
refreshUrl: "",
|
|
221
|
-
});
|
|
222
|
-
|
|
223
|
-
const backend = createMemoryBackend({
|
|
224
|
-
"oauth_app/app-1/client_secret": "test-secret",
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
const refreshFn = createLocalTokenRefreshFn(root, backend);
|
|
228
|
-
const result = await refreshFn("conn-1", "old-refresh-token");
|
|
229
|
-
|
|
230
|
-
expect(result.success).toBe(true);
|
|
231
|
-
expect(capturedUrls).toHaveLength(1);
|
|
232
|
-
expect(capturedUrls[0]).toBe("https://provider.example.com/token");
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
test("preserves token_endpoint_auth_method=client_secret_basic behaviour", async () => {
|
|
236
|
-
const root = setupTestDb({
|
|
237
|
-
refreshUrl: "https://provider.example.com/refresh",
|
|
238
|
-
tokenEndpointAuthMethod: "client_secret_basic",
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
const backend = createMemoryBackend({
|
|
242
|
-
"oauth_app/app-1/client_secret": "test-secret",
|
|
243
|
-
});
|
|
244
|
-
|
|
245
|
-
// Capture the fetch call to verify Authorization header
|
|
246
|
-
const capturedHeaders: Record<string, string>[] = [];
|
|
247
|
-
globalThis.fetch = mock(
|
|
248
|
-
async (input: string | URL | Request, init?: RequestInit) => {
|
|
249
|
-
const url =
|
|
250
|
-
typeof input === "string"
|
|
251
|
-
? input
|
|
252
|
-
: input instanceof URL
|
|
253
|
-
? input.toString()
|
|
254
|
-
: input.url;
|
|
255
|
-
capturedUrls.push(url);
|
|
256
|
-
if (init?.headers) {
|
|
257
|
-
capturedHeaders.push(init.headers as Record<string, string>);
|
|
258
|
-
}
|
|
259
|
-
return new Response(
|
|
260
|
-
JSON.stringify({
|
|
261
|
-
access_token: "new-access-token",
|
|
262
|
-
refresh_token: "new-refresh-token",
|
|
263
|
-
expires_in: 3600,
|
|
264
|
-
}),
|
|
265
|
-
{ status: 200, headers: { "Content-Type": "application/json" } },
|
|
266
|
-
);
|
|
267
|
-
},
|
|
268
|
-
) as unknown as typeof globalThis.fetch;
|
|
269
|
-
|
|
270
|
-
const refreshFn = createLocalTokenRefreshFn(root, backend);
|
|
271
|
-
const result = await refreshFn("conn-1", "old-refresh-token");
|
|
272
|
-
|
|
273
|
-
expect(result.success).toBe(true);
|
|
274
|
-
expect(capturedUrls).toHaveLength(1);
|
|
275
|
-
expect(capturedUrls[0]).toBe("https://provider.example.com/refresh");
|
|
276
|
-
|
|
277
|
-
// Verify Basic auth header was sent
|
|
278
|
-
expect(capturedHeaders).toHaveLength(1);
|
|
279
|
-
const expectedCredentials = Buffer.from(
|
|
280
|
-
"test-client-id:test-secret",
|
|
281
|
-
).toString("base64");
|
|
282
|
-
expect(capturedHeaders[0]["Authorization"]).toBe(
|
|
283
|
-
`Basic ${expectedCredentials}`,
|
|
284
|
-
);
|
|
285
|
-
});
|
|
286
|
-
|
|
287
|
-
test("preserves token_exchange_body_format=json behaviour", async () => {
|
|
288
|
-
const root = setupTestDb({
|
|
289
|
-
refreshUrl: "https://provider.example.com/refresh",
|
|
290
|
-
tokenExchangeBodyFormat: "json",
|
|
291
|
-
});
|
|
292
|
-
|
|
293
|
-
const backend = createMemoryBackend({
|
|
294
|
-
"oauth_app/app-1/client_secret": "test-secret",
|
|
295
|
-
});
|
|
296
|
-
|
|
297
|
-
const capturedContentTypes: string[] = [];
|
|
298
|
-
const capturedBodies: string[] = [];
|
|
299
|
-
globalThis.fetch = mock(
|
|
300
|
-
async (input: string | URL | Request, init?: RequestInit) => {
|
|
301
|
-
const url =
|
|
302
|
-
typeof input === "string"
|
|
303
|
-
? input
|
|
304
|
-
: input instanceof URL
|
|
305
|
-
? input.toString()
|
|
306
|
-
: input.url;
|
|
307
|
-
capturedUrls.push(url);
|
|
308
|
-
if (init?.headers) {
|
|
309
|
-
const headers = init.headers as Record<string, string>;
|
|
310
|
-
capturedContentTypes.push(headers["Content-Type"] ?? "");
|
|
311
|
-
}
|
|
312
|
-
if (init?.body) {
|
|
313
|
-
capturedBodies.push(
|
|
314
|
-
typeof init.body === "string" ? init.body : String(init.body),
|
|
315
|
-
);
|
|
316
|
-
}
|
|
317
|
-
return new Response(
|
|
318
|
-
JSON.stringify({
|
|
319
|
-
access_token: "new-access-token",
|
|
320
|
-
refresh_token: "new-refresh-token",
|
|
321
|
-
expires_in: 3600,
|
|
322
|
-
}),
|
|
323
|
-
{ status: 200, headers: { "Content-Type": "application/json" } },
|
|
324
|
-
);
|
|
325
|
-
},
|
|
326
|
-
) as unknown as typeof globalThis.fetch;
|
|
327
|
-
|
|
328
|
-
const refreshFn = createLocalTokenRefreshFn(root, backend);
|
|
329
|
-
const result = await refreshFn("conn-1", "old-refresh-token");
|
|
330
|
-
|
|
331
|
-
expect(result.success).toBe(true);
|
|
332
|
-
expect(capturedContentTypes).toHaveLength(1);
|
|
333
|
-
expect(capturedContentTypes[0]).toBe("application/json");
|
|
334
|
-
|
|
335
|
-
// Verify the body was sent as JSON
|
|
336
|
-
expect(capturedBodies).toHaveLength(1);
|
|
337
|
-
const parsed = JSON.parse(capturedBodies[0]);
|
|
338
|
-
expect(parsed.grant_type).toBe("refresh_token");
|
|
339
|
-
expect(parsed.refresh_token).toBe("old-refresh-token");
|
|
340
|
-
});
|
|
341
|
-
|
|
342
|
-
test("returns successful token refresh result", async () => {
|
|
343
|
-
const root = setupTestDb({
|
|
344
|
-
refreshUrl: "https://provider.example.com/refresh",
|
|
345
|
-
});
|
|
346
|
-
|
|
347
|
-
const backend = createMemoryBackend({
|
|
348
|
-
"oauth_app/app-1/client_secret": "test-secret",
|
|
349
|
-
});
|
|
350
|
-
|
|
351
|
-
const refreshFn = createLocalTokenRefreshFn(root, backend);
|
|
352
|
-
const result = await refreshFn("conn-1", "old-refresh-token");
|
|
353
|
-
|
|
354
|
-
expect(result.success).toBe(true);
|
|
355
|
-
if (result.success) {
|
|
356
|
-
expect(result.accessToken).toBe("new-access-token");
|
|
357
|
-
expect(result.refreshToken).toBe("new-refresh-token");
|
|
358
|
-
expect(result.expiresAt).toBeTypeOf("number");
|
|
359
|
-
}
|
|
360
|
-
});
|
|
361
|
-
});
|
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for the manage_secure_command_tool handler's operation serialization.
|
|
3
|
-
*
|
|
4
|
-
* The register path awaits a bundle download mid-handler. Without
|
|
5
|
-
* serialization, a concurrent unregister could run its "still in use?" check
|
|
6
|
-
* and bundle delete during that await — against a registry that doesn't yet
|
|
7
|
-
* reflect the in-flight registration — transiently deleting a bundle another
|
|
8
|
-
* caller is publishing or executing. The handler runs operations one-at-a-time
|
|
9
|
-
* to close that window.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import { describe, expect, test } from "bun:test";
|
|
13
|
-
|
|
14
|
-
import type { ManageSecureCommandTool } from "@vellumai/service-contracts/credential-rpc";
|
|
15
|
-
|
|
16
|
-
import {
|
|
17
|
-
createManageSecureCommandToolHandler,
|
|
18
|
-
type ManageSecureCommandToolHandlerDeps,
|
|
19
|
-
} from "../server.js";
|
|
20
|
-
|
|
21
|
-
const CTX = { sessionId: "test-session" };
|
|
22
|
-
|
|
23
|
-
function registerRequest(toolName: string): ManageSecureCommandTool {
|
|
24
|
-
return {
|
|
25
|
-
action: "register",
|
|
26
|
-
toolName,
|
|
27
|
-
bundleId: "bundle-1",
|
|
28
|
-
version: "1.0.0",
|
|
29
|
-
sourceUrl: "https://example.com/bundle.tgz",
|
|
30
|
-
sha256: "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef0",
|
|
31
|
-
credentialHandle: "local_static:svc/key",
|
|
32
|
-
description: "a tool",
|
|
33
|
-
// publishBundle is mocked in these tests, so the manifest contents are
|
|
34
|
-
// irrelevant — only its presence matters for the required-field check.
|
|
35
|
-
secureCommandManifest: {} as unknown as ManageSecureCommandTool["secureCommandManifest"],
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function unregisterRequest(toolName: string): ManageSecureCommandTool {
|
|
40
|
-
return { action: "unregister", toolName };
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
describe("manage_secure_command_tool serialization", () => {
|
|
44
|
-
test("a slow register does not let a concurrent unregister interleave", async () => {
|
|
45
|
-
const events: string[] = [];
|
|
46
|
-
|
|
47
|
-
let releaseDownload!: () => void;
|
|
48
|
-
const downloadGate = new Promise<void>((resolve) => {
|
|
49
|
-
releaseDownload = resolve;
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
const deps: ManageSecureCommandToolHandlerDeps = {
|
|
53
|
-
downloadBundle: async () => {
|
|
54
|
-
events.push("download:start");
|
|
55
|
-
await downloadGate;
|
|
56
|
-
events.push("download:end");
|
|
57
|
-
return Buffer.from("bundle-bytes");
|
|
58
|
-
},
|
|
59
|
-
publishBundle: () => {
|
|
60
|
-
events.push("publish");
|
|
61
|
-
return { success: true, deduplicated: false, bundlePath: "/tmp/bundle" };
|
|
62
|
-
},
|
|
63
|
-
registerTool: () => {
|
|
64
|
-
events.push("register");
|
|
65
|
-
},
|
|
66
|
-
unregisterTool: (toolName: string) => {
|
|
67
|
-
events.push(`unregister:${toolName}`);
|
|
68
|
-
return true;
|
|
69
|
-
},
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
const handler = createManageSecureCommandToolHandler(deps);
|
|
73
|
-
|
|
74
|
-
// Fire a register (which blocks in downloadBundle) then an unregister.
|
|
75
|
-
const registerPromise = handler(registerRequest("tool-a"), CTX);
|
|
76
|
-
const unregisterPromise = handler(unregisterRequest("tool-b"), CTX);
|
|
77
|
-
|
|
78
|
-
// Let the event loop run: the register has reached its download await, and
|
|
79
|
-
// the unregister must be queued behind it — its delete must not have run.
|
|
80
|
-
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
81
|
-
expect(events).toEqual(["download:start"]);
|
|
82
|
-
|
|
83
|
-
// Release the download; both operations complete in order.
|
|
84
|
-
releaseDownload();
|
|
85
|
-
const [registerResult, unregisterResult] = await Promise.all([
|
|
86
|
-
registerPromise,
|
|
87
|
-
unregisterPromise,
|
|
88
|
-
]);
|
|
89
|
-
|
|
90
|
-
expect(registerResult.success).toBe(true);
|
|
91
|
-
expect(unregisterResult.success).toBe(true);
|
|
92
|
-
|
|
93
|
-
// The unregister ran only after the register fully completed.
|
|
94
|
-
expect(events).toEqual([
|
|
95
|
-
"download:start",
|
|
96
|
-
"download:end",
|
|
97
|
-
"publish",
|
|
98
|
-
"register",
|
|
99
|
-
"unregister:tool-b",
|
|
100
|
-
]);
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
test("a rejected operation does not break serialization for later ones", async () => {
|
|
104
|
-
const events: string[] = [];
|
|
105
|
-
|
|
106
|
-
const deps: ManageSecureCommandToolHandlerDeps = {
|
|
107
|
-
downloadBundle: async () => {
|
|
108
|
-
throw new Error("network down");
|
|
109
|
-
},
|
|
110
|
-
publishBundle: () => ({
|
|
111
|
-
success: true,
|
|
112
|
-
deduplicated: false,
|
|
113
|
-
bundlePath: "/tmp/bundle",
|
|
114
|
-
}),
|
|
115
|
-
registerTool: () => {},
|
|
116
|
-
unregisterTool: (toolName: string) => {
|
|
117
|
-
events.push(`unregister:${toolName}`);
|
|
118
|
-
return true;
|
|
119
|
-
},
|
|
120
|
-
};
|
|
121
|
-
|
|
122
|
-
const handler = createManageSecureCommandToolHandler(deps);
|
|
123
|
-
|
|
124
|
-
// First op fails inside the handler (download error → structured failure),
|
|
125
|
-
// second op must still run.
|
|
126
|
-
const first = await handler(registerRequest("tool-a"), CTX);
|
|
127
|
-
const second = await handler(unregisterRequest("tool-b"), CTX);
|
|
128
|
-
|
|
129
|
-
expect(first.success).toBe(false);
|
|
130
|
-
expect(first.error?.code).toBe("DOWNLOAD_FAILED");
|
|
131
|
-
expect(second.success).toBe(true);
|
|
132
|
-
expect(events).toEqual(["unregister:tool-b"]);
|
|
133
|
-
});
|
|
134
|
-
});
|