@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,689 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for CES grant stores.
|
|
3
|
-
*
|
|
4
|
-
* Covers:
|
|
5
|
-
* - Persistent store: initialization, duplicate prevention by canonical grant
|
|
6
|
-
* hash, fail-closed behavior on corrupt/unreadable files.
|
|
7
|
-
* - Temporary store: allow_once consumption, allow_10m expiry, allow_conversation
|
|
8
|
-
* scoping by conversation ID, clearConversation cleanup.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { describe, expect, test, beforeEach, afterEach } from "bun:test";
|
|
12
|
-
import {
|
|
13
|
-
existsSync,
|
|
14
|
-
mkdirSync,
|
|
15
|
-
readFileSync,
|
|
16
|
-
rmSync,
|
|
17
|
-
writeFileSync,
|
|
18
|
-
} from "node:fs";
|
|
19
|
-
import { join } from "node:path";
|
|
20
|
-
import { tmpdir } from "node:os";
|
|
21
|
-
import { randomUUID } from "node:crypto";
|
|
22
|
-
|
|
23
|
-
import { PersistentGrantStore, type PersistentGrant } from "../grants/persistent-store.js";
|
|
24
|
-
import { TemporaryGrantStore } from "../grants/temporary-store.js";
|
|
25
|
-
|
|
26
|
-
// ---------------------------------------------------------------------------
|
|
27
|
-
// Test helpers
|
|
28
|
-
// ---------------------------------------------------------------------------
|
|
29
|
-
|
|
30
|
-
function makeTmpDir(): string {
|
|
31
|
-
const dir = join(tmpdir(), `ces-grant-test-${randomUUID()}`);
|
|
32
|
-
mkdirSync(dir, { recursive: true });
|
|
33
|
-
return dir;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function makeGrant(overrides?: Partial<PersistentGrant>): PersistentGrant {
|
|
37
|
-
return {
|
|
38
|
-
id: overrides?.id ?? randomUUID(),
|
|
39
|
-
tool: overrides?.tool ?? "host_bash",
|
|
40
|
-
pattern: overrides?.pattern ?? "npm install",
|
|
41
|
-
scope: overrides?.scope ?? "/project",
|
|
42
|
-
createdAt: overrides?.createdAt ?? Date.now(),
|
|
43
|
-
sessionId: overrides?.sessionId ?? "test-session",
|
|
44
|
-
revokedAt: overrides?.revokedAt,
|
|
45
|
-
revokedReason: overrides?.revokedReason,
|
|
46
|
-
};
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// ---------------------------------------------------------------------------
|
|
50
|
-
// Persistent store
|
|
51
|
-
// ---------------------------------------------------------------------------
|
|
52
|
-
|
|
53
|
-
describe("PersistentGrantStore", () => {
|
|
54
|
-
let tmpDir: string;
|
|
55
|
-
|
|
56
|
-
beforeEach(() => {
|
|
57
|
-
tmpDir = makeTmpDir();
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
afterEach(() => {
|
|
61
|
-
rmSync(tmpDir, { recursive: true, force: true });
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
describe("initialization", () => {
|
|
65
|
-
test("creates grants.json when file does not exist", () => {
|
|
66
|
-
const store = new PersistentGrantStore(tmpDir);
|
|
67
|
-
store.init();
|
|
68
|
-
|
|
69
|
-
const filePath = join(tmpDir, "grants.json");
|
|
70
|
-
expect(existsSync(filePath)).toBe(true);
|
|
71
|
-
|
|
72
|
-
const data = JSON.parse(readFileSync(filePath, "utf-8"));
|
|
73
|
-
expect(data.version).toBe(1);
|
|
74
|
-
expect(data.grants).toEqual([]);
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
test("creates parent directory if missing", () => {
|
|
78
|
-
const nestedDir = join(tmpDir, "nested", "grants");
|
|
79
|
-
const store = new PersistentGrantStore(nestedDir);
|
|
80
|
-
store.init();
|
|
81
|
-
|
|
82
|
-
expect(existsSync(join(nestedDir, "grants.json"))).toBe(true);
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
test("loads existing valid grants file", () => {
|
|
86
|
-
const grant = makeGrant();
|
|
87
|
-
writeFileSync(
|
|
88
|
-
join(tmpDir, "grants.json"),
|
|
89
|
-
JSON.stringify({ version: 1, grants: [grant] }, null, 2),
|
|
90
|
-
);
|
|
91
|
-
|
|
92
|
-
const store = new PersistentGrantStore(tmpDir);
|
|
93
|
-
store.init();
|
|
94
|
-
|
|
95
|
-
const grants = store.getAll();
|
|
96
|
-
expect(grants).toHaveLength(1);
|
|
97
|
-
expect(grants[0].id).toBe(grant.id);
|
|
98
|
-
});
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
describe("add and deduplication", () => {
|
|
102
|
-
test("adds a new grant and persists it", () => {
|
|
103
|
-
const store = new PersistentGrantStore(tmpDir);
|
|
104
|
-
store.init();
|
|
105
|
-
|
|
106
|
-
const grant = makeGrant();
|
|
107
|
-
const added = store.add(grant);
|
|
108
|
-
expect(added).toBe(true);
|
|
109
|
-
|
|
110
|
-
// Verify persisted by creating a new store instance
|
|
111
|
-
const store2 = new PersistentGrantStore(tmpDir);
|
|
112
|
-
const grants = store2.getAll();
|
|
113
|
-
expect(grants).toHaveLength(1);
|
|
114
|
-
expect(grants[0].id).toBe(grant.id);
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
test("deduplicates by canonical grant id", () => {
|
|
118
|
-
const store = new PersistentGrantStore(tmpDir);
|
|
119
|
-
store.init();
|
|
120
|
-
|
|
121
|
-
const grant = makeGrant({ id: "canonical-hash-123" });
|
|
122
|
-
expect(store.add(grant)).toBe(true);
|
|
123
|
-
expect(store.add(grant)).toBe(false); // duplicate
|
|
124
|
-
|
|
125
|
-
expect(store.getAll()).toHaveLength(1);
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
test("allows grants with different IDs", () => {
|
|
129
|
-
const store = new PersistentGrantStore(tmpDir);
|
|
130
|
-
store.init();
|
|
131
|
-
|
|
132
|
-
store.add(makeGrant({ id: "grant-a" }));
|
|
133
|
-
store.add(makeGrant({ id: "grant-b" }));
|
|
134
|
-
|
|
135
|
-
expect(store.getAll()).toHaveLength(2);
|
|
136
|
-
});
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
describe("reactivation of revoked grants", () => {
|
|
140
|
-
test("adding a grant with same ID as revoked returns true and reactivates", () => {
|
|
141
|
-
const store = new PersistentGrantStore(tmpDir);
|
|
142
|
-
store.init();
|
|
143
|
-
|
|
144
|
-
const grant = makeGrant({ id: "revoke-reactivate" });
|
|
145
|
-
store.add(grant);
|
|
146
|
-
store.markRevoked("revoke-reactivate", "test revocation");
|
|
147
|
-
|
|
148
|
-
// Revoked grant should not appear in getAll
|
|
149
|
-
expect(store.getAll()).toHaveLength(0);
|
|
150
|
-
|
|
151
|
-
// Re-add with same ID — should reactivate
|
|
152
|
-
const reactivated = store.add(
|
|
153
|
-
makeGrant({ id: "revoke-reactivate", pattern: "bun install" }),
|
|
154
|
-
);
|
|
155
|
-
expect(reactivated).toBe(true);
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
test("reactivated grant appears in getAll()", () => {
|
|
159
|
-
const store = new PersistentGrantStore(tmpDir);
|
|
160
|
-
store.init();
|
|
161
|
-
|
|
162
|
-
store.add(makeGrant({ id: "revoke-visible" }));
|
|
163
|
-
store.markRevoked("revoke-visible");
|
|
164
|
-
store.add(makeGrant({ id: "revoke-visible" }));
|
|
165
|
-
|
|
166
|
-
const all = store.getAll();
|
|
167
|
-
expect(all).toHaveLength(1);
|
|
168
|
-
expect(all[0].id).toBe("revoke-visible");
|
|
169
|
-
expect(all[0].revokedAt).toBeUndefined();
|
|
170
|
-
expect(all[0].revokedReason).toBeUndefined();
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
test("reactivated grant fields are updated from the new grant", () => {
|
|
174
|
-
const store = new PersistentGrantStore(tmpDir);
|
|
175
|
-
store.init();
|
|
176
|
-
|
|
177
|
-
store.add(
|
|
178
|
-
makeGrant({
|
|
179
|
-
id: "revoke-update",
|
|
180
|
-
tool: "host_bash",
|
|
181
|
-
pattern: "npm install",
|
|
182
|
-
scope: "/old-project",
|
|
183
|
-
sessionId: "session-1",
|
|
184
|
-
}),
|
|
185
|
-
);
|
|
186
|
-
store.markRevoked("revoke-update", "no longer needed");
|
|
187
|
-
|
|
188
|
-
const newGrant = makeGrant({
|
|
189
|
-
id: "revoke-update",
|
|
190
|
-
tool: "command",
|
|
191
|
-
pattern: "bun install",
|
|
192
|
-
scope: "/new-project",
|
|
193
|
-
sessionId: "session-2",
|
|
194
|
-
});
|
|
195
|
-
store.add(newGrant);
|
|
196
|
-
|
|
197
|
-
const result = store.getById("revoke-update");
|
|
198
|
-
expect(result).toBeDefined();
|
|
199
|
-
expect(result!.tool).toBe("command");
|
|
200
|
-
expect(result!.pattern).toBe("bun install");
|
|
201
|
-
expect(result!.scope).toBe("/new-project");
|
|
202
|
-
expect(result!.sessionId).toBe("session-2");
|
|
203
|
-
expect(result!.revokedAt).toBeUndefined();
|
|
204
|
-
expect(result!.revokedReason).toBeUndefined();
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
test("reactivated grant round-trips through serialization", () => {
|
|
208
|
-
const store = new PersistentGrantStore(tmpDir);
|
|
209
|
-
store.init();
|
|
210
|
-
|
|
211
|
-
store.add(makeGrant({ id: "revoke-persist" }));
|
|
212
|
-
store.markRevoked("revoke-persist", "reason");
|
|
213
|
-
store.add(makeGrant({ id: "revoke-persist", pattern: "bun test" }));
|
|
214
|
-
|
|
215
|
-
// Load from a fresh store instance to verify serialization
|
|
216
|
-
const store2 = new PersistentGrantStore(tmpDir);
|
|
217
|
-
const grants = store2.getAll();
|
|
218
|
-
expect(grants).toHaveLength(1);
|
|
219
|
-
expect(grants[0].id).toBe("revoke-persist");
|
|
220
|
-
expect(grants[0].pattern).toBe("bun test");
|
|
221
|
-
expect(grants[0].revokedAt).toBeUndefined();
|
|
222
|
-
expect(grants[0].revokedReason).toBeUndefined();
|
|
223
|
-
});
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
describe("legacy sessionId migration", () => {
|
|
227
|
-
test("backfills sessionId on legacy grants during init", () => {
|
|
228
|
-
// Write a grants file with a grant missing sessionId (legacy format)
|
|
229
|
-
const legacyGrant = {
|
|
230
|
-
id: "legacy-grant",
|
|
231
|
-
tool: "host_bash",
|
|
232
|
-
pattern: "npm install",
|
|
233
|
-
scope: "/project",
|
|
234
|
-
createdAt: Date.now(),
|
|
235
|
-
};
|
|
236
|
-
writeFileSync(
|
|
237
|
-
join(tmpDir, "grants.json"),
|
|
238
|
-
JSON.stringify({ version: 1, grants: [legacyGrant] }, null, 2),
|
|
239
|
-
);
|
|
240
|
-
|
|
241
|
-
const store = new PersistentGrantStore(tmpDir);
|
|
242
|
-
store.init();
|
|
243
|
-
|
|
244
|
-
const grants = store.getAll();
|
|
245
|
-
expect(grants).toHaveLength(1);
|
|
246
|
-
expect(grants[0].sessionId).toBe("unknown");
|
|
247
|
-
|
|
248
|
-
// Verify it was persisted
|
|
249
|
-
const raw = JSON.parse(readFileSync(join(tmpDir, "grants.json"), "utf-8"));
|
|
250
|
-
expect(raw.grants[0].sessionId).toBe("unknown");
|
|
251
|
-
});
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
describe("getById and has", () => {
|
|
255
|
-
test("returns grant by ID", () => {
|
|
256
|
-
const store = new PersistentGrantStore(tmpDir);
|
|
257
|
-
store.init();
|
|
258
|
-
|
|
259
|
-
const grant = makeGrant({ id: "lookup-id" });
|
|
260
|
-
store.add(grant);
|
|
261
|
-
|
|
262
|
-
expect(store.getById("lookup-id")).toBeDefined();
|
|
263
|
-
expect(store.getById("lookup-id")!.tool).toBe(grant.tool);
|
|
264
|
-
});
|
|
265
|
-
|
|
266
|
-
test("returns undefined for missing ID", () => {
|
|
267
|
-
const store = new PersistentGrantStore(tmpDir);
|
|
268
|
-
store.init();
|
|
269
|
-
|
|
270
|
-
expect(store.getById("nonexistent")).toBeUndefined();
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
test("has() returns true for existing grant", () => {
|
|
274
|
-
const store = new PersistentGrantStore(tmpDir);
|
|
275
|
-
store.init();
|
|
276
|
-
|
|
277
|
-
store.add(makeGrant({ id: "exists" }));
|
|
278
|
-
expect(store.has("exists")).toBe(true);
|
|
279
|
-
expect(store.has("missing")).toBe(false);
|
|
280
|
-
});
|
|
281
|
-
});
|
|
282
|
-
|
|
283
|
-
describe("remove", () => {
|
|
284
|
-
test("removes an existing grant", () => {
|
|
285
|
-
const store = new PersistentGrantStore(tmpDir);
|
|
286
|
-
store.init();
|
|
287
|
-
|
|
288
|
-
store.add(makeGrant({ id: "to-remove" }));
|
|
289
|
-
expect(store.remove("to-remove")).toBe(true);
|
|
290
|
-
expect(store.has("to-remove")).toBe(false);
|
|
291
|
-
});
|
|
292
|
-
|
|
293
|
-
test("returns false for non-existent grant", () => {
|
|
294
|
-
const store = new PersistentGrantStore(tmpDir);
|
|
295
|
-
store.init();
|
|
296
|
-
|
|
297
|
-
expect(store.remove("nonexistent")).toBe(false);
|
|
298
|
-
});
|
|
299
|
-
});
|
|
300
|
-
|
|
301
|
-
describe("clear", () => {
|
|
302
|
-
test("removes all grants", () => {
|
|
303
|
-
const store = new PersistentGrantStore(tmpDir);
|
|
304
|
-
store.init();
|
|
305
|
-
|
|
306
|
-
store.add(makeGrant({ id: "a" }));
|
|
307
|
-
store.add(makeGrant({ id: "b" }));
|
|
308
|
-
store.clear();
|
|
309
|
-
|
|
310
|
-
expect(store.getAll()).toHaveLength(0);
|
|
311
|
-
});
|
|
312
|
-
});
|
|
313
|
-
|
|
314
|
-
describe("fail-closed behavior", () => {
|
|
315
|
-
test("throws on malformed JSON", () => {
|
|
316
|
-
writeFileSync(join(tmpDir, "grants.json"), "not json at all");
|
|
317
|
-
|
|
318
|
-
const store = new PersistentGrantStore(tmpDir);
|
|
319
|
-
expect(() => store.init()).toThrow();
|
|
320
|
-
});
|
|
321
|
-
|
|
322
|
-
test("throws on missing version field", () => {
|
|
323
|
-
writeFileSync(
|
|
324
|
-
join(tmpDir, "grants.json"),
|
|
325
|
-
JSON.stringify({ grants: [] }),
|
|
326
|
-
);
|
|
327
|
-
|
|
328
|
-
const store = new PersistentGrantStore(tmpDir);
|
|
329
|
-
expect(() => store.init()).toThrow(/malformed/i);
|
|
330
|
-
});
|
|
331
|
-
|
|
332
|
-
test("throws on missing grants field", () => {
|
|
333
|
-
writeFileSync(
|
|
334
|
-
join(tmpDir, "grants.json"),
|
|
335
|
-
JSON.stringify({ version: 1 }),
|
|
336
|
-
);
|
|
337
|
-
|
|
338
|
-
const store = new PersistentGrantStore(tmpDir);
|
|
339
|
-
expect(() => store.init()).toThrow(/malformed/i);
|
|
340
|
-
});
|
|
341
|
-
|
|
342
|
-
test("throws on unsupported version", () => {
|
|
343
|
-
writeFileSync(
|
|
344
|
-
join(tmpDir, "grants.json"),
|
|
345
|
-
JSON.stringify({ version: 999, grants: [] }),
|
|
346
|
-
);
|
|
347
|
-
|
|
348
|
-
const store = new PersistentGrantStore(tmpDir);
|
|
349
|
-
expect(() => store.init()).toThrow(/unsupported version/i);
|
|
350
|
-
});
|
|
351
|
-
|
|
352
|
-
test("blocks all operations after corruption is detected", () => {
|
|
353
|
-
writeFileSync(join(tmpDir, "grants.json"), "corrupt");
|
|
354
|
-
|
|
355
|
-
const store = new PersistentGrantStore(tmpDir);
|
|
356
|
-
expect(() => store.init()).toThrow();
|
|
357
|
-
|
|
358
|
-
// Subsequent operations should also fail
|
|
359
|
-
expect(() => store.getAll()).toThrow(/corrupt/i);
|
|
360
|
-
expect(() => store.add(makeGrant())).toThrow(/corrupt/i);
|
|
361
|
-
expect(() => store.remove("any")).toThrow(/corrupt/i);
|
|
362
|
-
expect(() => store.clear()).toThrow(/corrupt/i);
|
|
363
|
-
});
|
|
364
|
-
|
|
365
|
-
test("throws on grants field being non-array", () => {
|
|
366
|
-
writeFileSync(
|
|
367
|
-
join(tmpDir, "grants.json"),
|
|
368
|
-
JSON.stringify({ version: 1, grants: "not-an-array" }),
|
|
369
|
-
);
|
|
370
|
-
|
|
371
|
-
const store = new PersistentGrantStore(tmpDir);
|
|
372
|
-
expect(() => store.init()).toThrow(/not an array/i);
|
|
373
|
-
});
|
|
374
|
-
});
|
|
375
|
-
|
|
376
|
-
describe("atomic writes", () => {
|
|
377
|
-
test("grants.json is valid after write", () => {
|
|
378
|
-
const store = new PersistentGrantStore(tmpDir);
|
|
379
|
-
store.init();
|
|
380
|
-
|
|
381
|
-
store.add(makeGrant({ id: "atom-1" }));
|
|
382
|
-
store.add(makeGrant({ id: "atom-2" }));
|
|
383
|
-
|
|
384
|
-
const raw = readFileSync(join(tmpDir, "grants.json"), "utf-8");
|
|
385
|
-
const data = JSON.parse(raw);
|
|
386
|
-
expect(data.version).toBe(1);
|
|
387
|
-
expect(data.grants).toHaveLength(2);
|
|
388
|
-
});
|
|
389
|
-
});
|
|
390
|
-
});
|
|
391
|
-
|
|
392
|
-
// ---------------------------------------------------------------------------
|
|
393
|
-
// Temporary store
|
|
394
|
-
// ---------------------------------------------------------------------------
|
|
395
|
-
|
|
396
|
-
describe("TemporaryGrantStore", () => {
|
|
397
|
-
let store: TemporaryGrantStore;
|
|
398
|
-
|
|
399
|
-
beforeEach(() => {
|
|
400
|
-
store = new TemporaryGrantStore();
|
|
401
|
-
});
|
|
402
|
-
|
|
403
|
-
describe("allow_once", () => {
|
|
404
|
-
test("grant is consumed on first check", () => {
|
|
405
|
-
store.add("allow_once", "hash-abc");
|
|
406
|
-
|
|
407
|
-
expect(store.check("allow_once", "hash-abc")).toBe(true);
|
|
408
|
-
// Second check should fail — consumed
|
|
409
|
-
expect(store.check("allow_once", "hash-abc")).toBe(false);
|
|
410
|
-
});
|
|
411
|
-
|
|
412
|
-
test("different proposal hashes are independent", () => {
|
|
413
|
-
store.add("allow_once", "hash-1");
|
|
414
|
-
store.add("allow_once", "hash-2");
|
|
415
|
-
|
|
416
|
-
expect(store.check("allow_once", "hash-1")).toBe(true);
|
|
417
|
-
expect(store.check("allow_once", "hash-2")).toBe(true);
|
|
418
|
-
// Both consumed
|
|
419
|
-
expect(store.check("allow_once", "hash-1")).toBe(false);
|
|
420
|
-
expect(store.check("allow_once", "hash-2")).toBe(false);
|
|
421
|
-
});
|
|
422
|
-
|
|
423
|
-
test("remains usable immediately (default TTL is not instantaneous)", () => {
|
|
424
|
-
// The default `allow_once` TTL must still allow a prompt retry of the
|
|
425
|
-
// just-approved operation.
|
|
426
|
-
store.add("allow_once", "hash-default-ttl");
|
|
427
|
-
expect(store.check("allow_once", "hash-default-ttl")).toBe(true);
|
|
428
|
-
});
|
|
429
|
-
|
|
430
|
-
test("expires after its TTL even if never consumed (ATL-935)", () => {
|
|
431
|
-
// An unconsumed single-use approval must not live forever.
|
|
432
|
-
store.add("allow_once", "hash-once-expire", { durationMs: 1 });
|
|
433
|
-
|
|
434
|
-
const start = Date.now();
|
|
435
|
-
while (Date.now() - start < 5) {
|
|
436
|
-
// spin
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
expect(store.check("allow_once", "hash-once-expire")).toBe(false);
|
|
440
|
-
});
|
|
441
|
-
});
|
|
442
|
-
|
|
443
|
-
// -------------------------------------------------------------------------
|
|
444
|
-
// ATL-935: ephemeral approvals are bounded by TTLs, not by connection
|
|
445
|
-
// teardown.
|
|
446
|
-
//
|
|
447
|
-
// In managed mode the store instance — contents included — is deliberately
|
|
448
|
-
// shared across assistant reconnects and, in the multi-process daemon model,
|
|
449
|
-
// across the connections that each talk to CES, so one guardian approval can
|
|
450
|
-
// be used by any connection entitled to it. The security boundary is the
|
|
451
|
-
// per-grant TTL: an approval that is recorded but never consumed must expire
|
|
452
|
-
// on its own rather than survive indefinitely and be replayed by a much later
|
|
453
|
-
// connection. These tests encode both halves of that contract — an approval
|
|
454
|
-
// stays usable across a reconnect (sharing preserved) but does not outlive
|
|
455
|
-
// its TTL (replay window bounded).
|
|
456
|
-
// -------------------------------------------------------------------------
|
|
457
|
-
describe("ATL-935: ephemeral approvals are bounded by TTLs, not teardown", () => {
|
|
458
|
-
test("every grant kind is bounded by a TTL (no unbounded approvals)", () => {
|
|
459
|
-
// The finding was that allow_once and allow_conversation had no time
|
|
460
|
-
// bound at all. Every kind must now expire on its own.
|
|
461
|
-
store.add("allow_once", "exp-once", { durationMs: 1 });
|
|
462
|
-
store.add("allow_10m", "exp-10m", { durationMs: 1 });
|
|
463
|
-
store.add("allow_conversation", "exp-conv", {
|
|
464
|
-
conversationId: "c",
|
|
465
|
-
durationMs: 1,
|
|
466
|
-
});
|
|
467
|
-
|
|
468
|
-
const start = Date.now();
|
|
469
|
-
while (Date.now() - start < 5) {
|
|
470
|
-
// spin until all three TTLs lapse
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
expect(store.check("allow_once", "exp-once")).toBe(false);
|
|
474
|
-
expect(store.check("allow_10m", "exp-10m")).toBe(false);
|
|
475
|
-
expect(store.check("allow_conversation", "exp-conv", "c")).toBe(false);
|
|
476
|
-
});
|
|
477
|
-
|
|
478
|
-
test("allow_once that is never consumed expires instead of lingering", () => {
|
|
479
|
-
// The headline scenario: an approval recorded but not consumed before a
|
|
480
|
-
// connection drops must not be replayable by a later connection.
|
|
481
|
-
store.add("allow_once", "hash-stale", { durationMs: 1 });
|
|
482
|
-
|
|
483
|
-
const start = Date.now();
|
|
484
|
-
while (Date.now() - start < 5) {
|
|
485
|
-
// spin
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
expect(store.checkAny("hash-stale")).toBeUndefined();
|
|
489
|
-
});
|
|
490
|
-
|
|
491
|
-
test("allow_conversation is bounded by an absolute TTL backstop", () => {
|
|
492
|
-
store.add("allow_conversation", "hash-conv-ttl", {
|
|
493
|
-
conversationId: "conv-1",
|
|
494
|
-
durationMs: 1,
|
|
495
|
-
});
|
|
496
|
-
|
|
497
|
-
const start = Date.now();
|
|
498
|
-
while (Date.now() - start < 5) {
|
|
499
|
-
// spin
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
expect(store.checkAny("hash-conv-ttl", "conv-1")).toBeUndefined();
|
|
503
|
-
});
|
|
504
|
-
|
|
505
|
-
test("an unexpired approval is still shared across a reconnect (no teardown)", () => {
|
|
506
|
-
// The multi-process daemon model depends on this: the store is NOT
|
|
507
|
-
// cleared on disconnect, so an approval granted while one connection was
|
|
508
|
-
// live remains usable by a later or sibling connection within its TTL.
|
|
509
|
-
store.add("allow_conversation", "hash-shared", {
|
|
510
|
-
conversationId: "conv-1",
|
|
511
|
-
});
|
|
512
|
-
|
|
513
|
-
// Simulate a reconnect — no clear() happens between connections.
|
|
514
|
-
expect(store.checkAny("hash-shared", "conv-1")).toBe(
|
|
515
|
-
"allow_conversation",
|
|
516
|
-
);
|
|
517
|
-
// Still usable again (conversation grants are not consumed on use).
|
|
518
|
-
expect(store.checkAny("hash-shared", "conv-1")).toBe(
|
|
519
|
-
"allow_conversation",
|
|
520
|
-
);
|
|
521
|
-
|
|
522
|
-
// An allow_10m approval likewise survives a reconnect within its window.
|
|
523
|
-
store.add("allow_10m", "hash-shared-10m");
|
|
524
|
-
expect(store.checkAny("hash-shared-10m")).toBe("allow_10m");
|
|
525
|
-
});
|
|
526
|
-
});
|
|
527
|
-
|
|
528
|
-
describe("allow_10m", () => {
|
|
529
|
-
test("grant is active before expiry", () => {
|
|
530
|
-
store.add("allow_10m", "hash-timed");
|
|
531
|
-
|
|
532
|
-
expect(store.check("allow_10m", "hash-timed")).toBe(true);
|
|
533
|
-
// Can be checked multiple times (not consumed)
|
|
534
|
-
expect(store.check("allow_10m", "hash-timed")).toBe(true);
|
|
535
|
-
});
|
|
536
|
-
|
|
537
|
-
test("grant expires after TTL", () => {
|
|
538
|
-
// Add with a very short duration (1ms)
|
|
539
|
-
store.add("allow_10m", "hash-expire", { durationMs: 1 });
|
|
540
|
-
|
|
541
|
-
// Wait for expiry — use a synchronous busy-wait to avoid flakes
|
|
542
|
-
const start = Date.now();
|
|
543
|
-
while (Date.now() - start < 5) {
|
|
544
|
-
// spin
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
expect(store.check("allow_10m", "hash-expire")).toBe(false);
|
|
548
|
-
});
|
|
549
|
-
|
|
550
|
-
test("expired grant is lazily purged", () => {
|
|
551
|
-
store.add("allow_10m", "hash-lazy", { durationMs: 1 });
|
|
552
|
-
|
|
553
|
-
const start = Date.now();
|
|
554
|
-
while (Date.now() - start < 5) {
|
|
555
|
-
// spin
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
// Check triggers lazy purge
|
|
559
|
-
store.check("allow_10m", "hash-lazy");
|
|
560
|
-
expect(store.size).toBe(0);
|
|
561
|
-
});
|
|
562
|
-
});
|
|
563
|
-
|
|
564
|
-
describe("allow_conversation", () => {
|
|
565
|
-
test("requires conversationId", () => {
|
|
566
|
-
expect(() => store.add("allow_conversation", "hash-t", {})).toThrow(
|
|
567
|
-
/conversationId/,
|
|
568
|
-
);
|
|
569
|
-
});
|
|
570
|
-
|
|
571
|
-
test("scoped to conversation ID", () => {
|
|
572
|
-
store.add("allow_conversation", "hash-t", { conversationId: "conv-1" });
|
|
573
|
-
|
|
574
|
-
expect(store.check("allow_conversation", "hash-t", "conv-1")).toBe(true);
|
|
575
|
-
// Different conversation should not match
|
|
576
|
-
expect(store.check("allow_conversation", "hash-t", "conv-2")).toBe(false);
|
|
577
|
-
});
|
|
578
|
-
|
|
579
|
-
test("same proposal hash in different conversations are independent", () => {
|
|
580
|
-
store.add("allow_conversation", "hash-t", { conversationId: "conv-a" });
|
|
581
|
-
store.add("allow_conversation", "hash-t", { conversationId: "conv-b" });
|
|
582
|
-
|
|
583
|
-
expect(store.check("allow_conversation", "hash-t", "conv-a")).toBe(true);
|
|
584
|
-
expect(store.check("allow_conversation", "hash-t", "conv-b")).toBe(true);
|
|
585
|
-
});
|
|
586
|
-
|
|
587
|
-
test("not consumed on check (persists within conversation)", () => {
|
|
588
|
-
store.add("allow_conversation", "hash-t", { conversationId: "conv-1" });
|
|
589
|
-
|
|
590
|
-
expect(store.check("allow_conversation", "hash-t", "conv-1")).toBe(true);
|
|
591
|
-
expect(store.check("allow_conversation", "hash-t", "conv-1")).toBe(true);
|
|
592
|
-
expect(store.check("allow_conversation", "hash-t", "conv-1")).toBe(true);
|
|
593
|
-
});
|
|
594
|
-
});
|
|
595
|
-
|
|
596
|
-
describe("checkAny", () => {
|
|
597
|
-
test("finds allow_once grant", () => {
|
|
598
|
-
store.add("allow_once", "hash-any");
|
|
599
|
-
expect(store.checkAny("hash-any")).toBe("allow_once");
|
|
600
|
-
// Consumed — should not match again
|
|
601
|
-
expect(store.checkAny("hash-any")).toBeUndefined();
|
|
602
|
-
});
|
|
603
|
-
|
|
604
|
-
test("finds allow_10m grant", () => {
|
|
605
|
-
store.add("allow_10m", "hash-any-t");
|
|
606
|
-
expect(store.checkAny("hash-any-t")).toBe("allow_10m");
|
|
607
|
-
});
|
|
608
|
-
|
|
609
|
-
test("finds allow_conversation grant with conversationId", () => {
|
|
610
|
-
store.add("allow_conversation", "hash-any-th", { conversationId: "conv-x" });
|
|
611
|
-
expect(store.checkAny("hash-any-th", "conv-x")).toBe("allow_conversation");
|
|
612
|
-
});
|
|
613
|
-
|
|
614
|
-
test("returns undefined when no grants match", () => {
|
|
615
|
-
expect(store.checkAny("no-such-hash")).toBeUndefined();
|
|
616
|
-
});
|
|
617
|
-
|
|
618
|
-
test("prefers allow_once over allow_10m", () => {
|
|
619
|
-
store.add("allow_once", "hash-prio");
|
|
620
|
-
store.add("allow_10m", "hash-prio");
|
|
621
|
-
|
|
622
|
-
expect(store.checkAny("hash-prio")).toBe("allow_once");
|
|
623
|
-
});
|
|
624
|
-
});
|
|
625
|
-
|
|
626
|
-
describe("remove", () => {
|
|
627
|
-
test("removes a specific grant", () => {
|
|
628
|
-
store.add("allow_10m", "hash-rm");
|
|
629
|
-
expect(store.remove("allow_10m", "hash-rm")).toBe(true);
|
|
630
|
-
expect(store.check("allow_10m", "hash-rm")).toBe(false);
|
|
631
|
-
});
|
|
632
|
-
|
|
633
|
-
test("returns false for non-existent grant", () => {
|
|
634
|
-
expect(store.remove("allow_once", "nope")).toBe(false);
|
|
635
|
-
});
|
|
636
|
-
});
|
|
637
|
-
|
|
638
|
-
describe("clearConversation", () => {
|
|
639
|
-
test("removes all conversation grants for a conversation", () => {
|
|
640
|
-
store.add("allow_conversation", "hash-1", { conversationId: "conv-clear" });
|
|
641
|
-
store.add("allow_conversation", "hash-2", { conversationId: "conv-clear" });
|
|
642
|
-
store.add("allow_conversation", "hash-3", { conversationId: "conv-keep" });
|
|
643
|
-
|
|
644
|
-
store.clearConversation("conv-clear");
|
|
645
|
-
|
|
646
|
-
expect(store.check("allow_conversation", "hash-1", "conv-clear")).toBe(false);
|
|
647
|
-
expect(store.check("allow_conversation", "hash-2", "conv-clear")).toBe(false);
|
|
648
|
-
// Other conversation unaffected
|
|
649
|
-
expect(store.check("allow_conversation", "hash-3", "conv-keep")).toBe(true);
|
|
650
|
-
});
|
|
651
|
-
|
|
652
|
-
test("does not affect non-conversation grants", () => {
|
|
653
|
-
store.add("allow_once", "hash-non-conv");
|
|
654
|
-
store.add("allow_10m", "hash-non-conv-t");
|
|
655
|
-
|
|
656
|
-
store.clearConversation("any-conv");
|
|
657
|
-
|
|
658
|
-
// Non-conversation grants unaffected
|
|
659
|
-
expect(store.check("allow_once", "hash-non-conv")).toBe(true);
|
|
660
|
-
expect(store.check("allow_10m", "hash-non-conv-t")).toBe(true);
|
|
661
|
-
});
|
|
662
|
-
});
|
|
663
|
-
|
|
664
|
-
describe("clear", () => {
|
|
665
|
-
test("removes all grants", () => {
|
|
666
|
-
store.add("allow_once", "h1");
|
|
667
|
-
store.add("allow_10m", "h2");
|
|
668
|
-
store.add("allow_conversation", "h3", { conversationId: "c1" });
|
|
669
|
-
|
|
670
|
-
store.clear();
|
|
671
|
-
expect(store.size).toBe(0);
|
|
672
|
-
});
|
|
673
|
-
});
|
|
674
|
-
|
|
675
|
-
describe("process restart simulation", () => {
|
|
676
|
-
test("grants do not survive new store instance", () => {
|
|
677
|
-
store.add("allow_once", "hash-restart");
|
|
678
|
-
store.add("allow_10m", "hash-restart-t");
|
|
679
|
-
store.add("allow_conversation", "hash-restart-th", {
|
|
680
|
-
conversationId: "conv-r",
|
|
681
|
-
});
|
|
682
|
-
|
|
683
|
-
// "Restart" — new instance
|
|
684
|
-
const newStore = new TemporaryGrantStore();
|
|
685
|
-
expect(newStore.size).toBe(0);
|
|
686
|
-
expect(newStore.checkAny("hash-restart")).toBeUndefined();
|
|
687
|
-
});
|
|
688
|
-
});
|
|
689
|
-
});
|