@indigoai-us/hq-cloud 6.14.12 → 6.14.14
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/.claude/policies/hq-cloud-strip-types-no-parameter-properties.md +22 -0
- package/dist/bin/sync-runner-company.d.ts.map +1 -1
- package/dist/bin/sync-runner-company.js +6 -0
- package/dist/bin/sync-runner-company.js.map +1 -1
- package/dist/bin/sync-runner-planning.d.ts.map +1 -1
- package/dist/bin/sync-runner-planning.js +3 -1
- package/dist/bin/sync-runner-planning.js.map +1 -1
- package/dist/bin/sync-runner-watch-loop.d.ts +10 -0
- package/dist/bin/sync-runner-watch-loop.d.ts.map +1 -1
- package/dist/bin/sync-runner-watch-loop.js +42 -3
- package/dist/bin/sync-runner-watch-loop.js.map +1 -1
- package/dist/bin/sync-runner-watch-routes.d.ts +1 -0
- package/dist/bin/sync-runner-watch-routes.d.ts.map +1 -1
- package/dist/bin/sync-runner-watch-routes.js +3 -0
- package/dist/bin/sync-runner-watch-routes.js.map +1 -1
- package/dist/bin/sync-runner.d.ts +17 -0
- package/dist/bin/sync-runner.d.ts.map +1 -1
- package/dist/bin/sync-runner.js +83 -33
- package/dist/bin/sync-runner.js.map +1 -1
- package/dist/bin/sync-runner.test.js +193 -5
- package/dist/bin/sync-runner.test.js.map +1 -1
- package/dist/cli/reindex.d.ts.map +1 -1
- package/dist/cli/reindex.js +23 -60
- package/dist/cli/reindex.js.map +1 -1
- package/dist/cli/reindex.test.d.ts +2 -2
- package/dist/cli/reindex.test.js +60 -109
- package/dist/cli/reindex.test.js.map +1 -1
- package/dist/cli/rescue.reindex.test.js +3 -2
- package/dist/cli/rescue.reindex.test.js.map +1 -1
- package/dist/cli/share.d.ts.map +1 -1
- package/dist/cli/share.js +12 -1
- package/dist/cli/share.js.map +1 -1
- package/dist/cli/share.test.js +14 -0
- package/dist/cli/share.test.js.map +1 -1
- package/dist/cli/sync.d.ts.map +1 -1
- package/dist/cli/sync.js +9 -1
- package/dist/cli/sync.js.map +1 -1
- package/dist/cli/sync.test.js +8 -0
- package/dist/cli/sync.test.js.map +1 -1
- package/dist/cognito-auth.d.ts +44 -12
- package/dist/cognito-auth.d.ts.map +1 -1
- package/dist/cognito-auth.js +440 -76
- package/dist/cognito-auth.js.map +1 -1
- package/dist/cognito-auth.test.js +174 -0
- package/dist/cognito-auth.test.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/machine-auth.test.js +683 -5
- package/dist/machine-auth.test.js.map +1 -1
- package/dist/vault-client.d.ts +2 -1
- package/dist/vault-client.d.ts.map +1 -1
- package/dist/vault-client.js +45 -9
- package/dist/vault-client.js.map +1 -1
- package/dist/vault-client.test.js +77 -7
- package/dist/vault-client.test.js.map +1 -1
- package/package.json +2 -1
- package/src/bin/sync-runner-company.ts +5 -0
- package/src/bin/sync-runner-planning.ts +2 -1
- package/src/bin/sync-runner-watch-loop.ts +51 -4
- package/src/bin/sync-runner-watch-routes.ts +2 -0
- package/src/bin/sync-runner.test.ts +244 -4
- package/src/bin/sync-runner.ts +99 -32
- package/src/cli/reindex.test.ts +63 -120
- package/src/cli/reindex.ts +23 -62
- package/src/cli/rescue.reindex.test.ts +3 -2
- package/src/cli/share.test.ts +20 -0
- package/src/cli/share.ts +7 -1
- package/src/cli/sync.test.ts +14 -0
- package/src/cli/sync.ts +5 -1
- package/src/cognito-auth.test.ts +227 -0
- package/src/cognito-auth.ts +533 -71
- package/src/index.ts +5 -1
- package/src/machine-auth.test.ts +808 -5
- package/src/vault-client.test.ts +100 -11
- package/src/vault-client.ts +55 -10
- package/test/share-sync.integration.test.ts +12 -9
package/src/machine-auth.test.ts
CHANGED
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
import * as fs from "fs";
|
|
15
15
|
import * as os from "os";
|
|
16
16
|
import * as path from "path";
|
|
17
|
+
import { spawn } from "child_process";
|
|
18
|
+
import { pathToFileURL } from "url";
|
|
17
19
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
18
20
|
|
|
19
21
|
let originalHome: string | undefined;
|
|
@@ -30,6 +32,7 @@ beforeEach(() => {
|
|
|
30
32
|
});
|
|
31
33
|
|
|
32
34
|
afterEach(() => {
|
|
35
|
+
vi.useRealTimers();
|
|
33
36
|
if (originalHome === undefined) delete process.env.HOME;
|
|
34
37
|
else process.env.HOME = originalHome;
|
|
35
38
|
if (originalCredsEnv === undefined) delete process.env.HQ_MACHINE_CREDS_FILE;
|
|
@@ -77,13 +80,48 @@ function stubMintFetch(
|
|
|
77
80
|
const calls: Array<{ url: string; init: RequestInit }> = [];
|
|
78
81
|
const fetchMock = vi.fn(async (url: string | URL, init?: RequestInit) => {
|
|
79
82
|
calls.push({ url: String(url), init: init ?? {} });
|
|
83
|
+
const body = JSON.parse(String(init?.body)) as {
|
|
84
|
+
ClientId: string;
|
|
85
|
+
AuthParameters: { USERNAME: string };
|
|
86
|
+
};
|
|
87
|
+
const credsFile =
|
|
88
|
+
process.env.HQ_MACHINE_CREDS_FILE ??
|
|
89
|
+
path.join(tmpHome, ".hq-agent", "machine-creds.json");
|
|
90
|
+
const creds = JSON.parse(fs.readFileSync(credsFile, "utf-8")) as {
|
|
91
|
+
entityType?: "agent" | "outpost";
|
|
92
|
+
entityUid?: string;
|
|
93
|
+
};
|
|
94
|
+
const username = body.AuthParameters.USERNAME;
|
|
95
|
+
const subject = `sub:${username}`;
|
|
96
|
+
const entityType = creds.entityType ?? "agent";
|
|
97
|
+
const entityUid = creds.entityUid ?? "agt_01TEST";
|
|
80
98
|
return new Response(
|
|
81
99
|
JSON.stringify({
|
|
82
100
|
AuthenticationResult: {
|
|
83
101
|
AccessToken:
|
|
84
102
|
overrides.AccessToken ??
|
|
85
|
-
fakeJwt({
|
|
86
|
-
|
|
103
|
+
fakeJwt({
|
|
104
|
+
token_use: "access",
|
|
105
|
+
client_id: body.ClientId,
|
|
106
|
+
username,
|
|
107
|
+
sub: subject,
|
|
108
|
+
}),
|
|
109
|
+
IdToken:
|
|
110
|
+
overrides.IdToken ??
|
|
111
|
+
fakeJwt({
|
|
112
|
+
token_use: "id",
|
|
113
|
+
aud: body.ClientId,
|
|
114
|
+
"custom:entityType": entityType,
|
|
115
|
+
"custom:entityUid": entityUid,
|
|
116
|
+
...(entityType === "outpost"
|
|
117
|
+
? {
|
|
118
|
+
"custom:delegatedSub": "human-sub",
|
|
119
|
+
"custom:delegatedEmail": "owner@example.com",
|
|
120
|
+
}
|
|
121
|
+
: {}),
|
|
122
|
+
"cognito:username": username,
|
|
123
|
+
sub: subject,
|
|
124
|
+
}),
|
|
87
125
|
RefreshToken: overrides.RefreshToken,
|
|
88
126
|
ExpiresIn: overrides.ExpiresIn ?? 3600,
|
|
89
127
|
},
|
|
@@ -136,6 +174,27 @@ describe("machine identity detection", () => {
|
|
|
136
174
|
expect(isMachineIdentity()).toBe(true);
|
|
137
175
|
});
|
|
138
176
|
|
|
177
|
+
it("parses the expected machine entity type and uid from provisioned creds", async () => {
|
|
178
|
+
writeCreds({
|
|
179
|
+
username: "out-01test@outposts.getindigo.ai",
|
|
180
|
+
secret: "s3cret",
|
|
181
|
+
clientId: "outpost-client-1",
|
|
182
|
+
region: "us-east-1",
|
|
183
|
+
entityType: "outpost",
|
|
184
|
+
entityUid: "out_01TEST",
|
|
185
|
+
});
|
|
186
|
+
const { loadMachineCreds } = await importModule();
|
|
187
|
+
|
|
188
|
+
expect(loadMachineCreds()).toEqual({
|
|
189
|
+
username: "out-01test@outposts.getindigo.ai",
|
|
190
|
+
secret: "s3cret",
|
|
191
|
+
clientId: "outpost-client-1",
|
|
192
|
+
region: "us-east-1",
|
|
193
|
+
entityType: "outpost",
|
|
194
|
+
entityUid: "out_01TEST",
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
139
198
|
it("honors HQ_MACHINE_CREDS_FILE override", async () => {
|
|
140
199
|
const custom = path.join(tmpHome, "elsewhere.json");
|
|
141
200
|
fs.writeFileSync(
|
|
@@ -157,6 +216,35 @@ describe("machine identity detection", () => {
|
|
|
157
216
|
fs.writeFileSync(path.join(tmpHome, ".hq-agent", "machine-creds.json"), "{nope");
|
|
158
217
|
expect(loadMachineCreds()).toBeNull();
|
|
159
218
|
});
|
|
219
|
+
|
|
220
|
+
it.each([
|
|
221
|
+
{ entityType: "agent", entityUid: "out_WRONG" },
|
|
222
|
+
{ entityType: "outpost", entityUid: "agt_WRONG" },
|
|
223
|
+
{ entityType: "agent", entityUid: "garbage" },
|
|
224
|
+
{ entityType: "agent" },
|
|
225
|
+
{ entityType: "outpost" },
|
|
226
|
+
{ entityUid: "out_NO_TYPE" },
|
|
227
|
+
{ entityUid: "garbage" },
|
|
228
|
+
{ entityType: "agent", entityUid: "agt_" },
|
|
229
|
+
{ entityType: "outpost", entityUid: "out_" },
|
|
230
|
+
])("rejects malformed or cross-prefix entity metadata: %j", async (identity) => {
|
|
231
|
+
writeCreds({ username: "machine", secret: "secret", ...identity });
|
|
232
|
+
const { loadMachineCreds } = await importModule();
|
|
233
|
+
|
|
234
|
+
expect(loadMachineCreds()).toBeNull();
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it("accepts an agt_* uid without a type as the legacy agent inference", async () => {
|
|
238
|
+
writeCreds({
|
|
239
|
+
username: "machine-agt_01TEST",
|
|
240
|
+
secret: "secret",
|
|
241
|
+
entityUid: "agt_01TEST",
|
|
242
|
+
});
|
|
243
|
+
const { loadMachineCreds } = await importModule();
|
|
244
|
+
|
|
245
|
+
expect(loadMachineCreds()).toMatchObject({ entityUid: "agt_01TEST" });
|
|
246
|
+
expect(loadMachineCreds()?.entityType).toBeUndefined();
|
|
247
|
+
});
|
|
160
248
|
});
|
|
161
249
|
|
|
162
250
|
// ---------------------------------------------------------------------------
|
|
@@ -205,7 +293,7 @@ describe("mintMachineTokens", () => {
|
|
|
205
293
|
);
|
|
206
294
|
expect(accessClaims.token_use).toBe("access");
|
|
207
295
|
expect(idClaims.token_use).toBe("id");
|
|
208
|
-
expect(tokens.refreshToken).toBe("
|
|
296
|
+
expect(tokens.refreshToken).toBe("");
|
|
209
297
|
expect(typeof tokens.expiresAt).toBe("number");
|
|
210
298
|
|
|
211
299
|
// Persisted to the shared cache file.
|
|
@@ -227,6 +315,12 @@ describe("mintMachineTokens", () => {
|
|
|
227
315
|
await expect(mintMachineTokens(CONFIG)).rejects.toBeInstanceOf(
|
|
228
316
|
CognitoAuthError,
|
|
229
317
|
);
|
|
318
|
+
expect(
|
|
319
|
+
fs.existsSync(path.join(tmpHome, ".hq", "cognito-tokens.json.lock")),
|
|
320
|
+
).toBe(false);
|
|
321
|
+
|
|
322
|
+
stubMintFetch();
|
|
323
|
+
await expect(mintMachineTokens(CONFIG)).resolves.toBeDefined();
|
|
230
324
|
});
|
|
231
325
|
|
|
232
326
|
it("throws when no creds are present", async () => {
|
|
@@ -235,6 +329,58 @@ describe("mintMachineTokens", () => {
|
|
|
235
329
|
CognitoAuthError,
|
|
236
330
|
);
|
|
237
331
|
});
|
|
332
|
+
|
|
333
|
+
it.each([
|
|
334
|
+
["wrong entity type", { "custom:entityType": "agent", "custom:entityUid": "agt_OTHER" }],
|
|
335
|
+
["wrong entity uid", { "custom:entityUid": "out_OTHER" }],
|
|
336
|
+
["absent delegatedSub", { "custom:delegatedSub": undefined }],
|
|
337
|
+
["empty delegatedSub", { "custom:delegatedSub": "" }],
|
|
338
|
+
["non-string delegatedSub", { "custom:delegatedSub": 42 }],
|
|
339
|
+
["absent delegatedEmail", { "custom:delegatedEmail": undefined }],
|
|
340
|
+
["empty delegatedEmail", { "custom:delegatedEmail": "" }],
|
|
341
|
+
["non-string delegatedEmail", { "custom:delegatedEmail": 42 }],
|
|
342
|
+
] as const)(
|
|
343
|
+
"rejects and does not cache a first mint with %s",
|
|
344
|
+
async (_label, claimOverride) => {
|
|
345
|
+
const username = "out-01test@outposts.getindigo.ai";
|
|
346
|
+
writeCreds({
|
|
347
|
+
username,
|
|
348
|
+
secret: "outpost-secret",
|
|
349
|
+
entityType: "outpost",
|
|
350
|
+
entityUid: "out_01TEST",
|
|
351
|
+
});
|
|
352
|
+
const claims: Record<string, unknown> = {
|
|
353
|
+
token_use: "id",
|
|
354
|
+
aud: CONFIG.clientId,
|
|
355
|
+
"custom:entityType": "outpost",
|
|
356
|
+
"custom:entityUid": "out_01TEST",
|
|
357
|
+
"custom:delegatedSub": "human-sub",
|
|
358
|
+
"custom:delegatedEmail": "owner@example.com",
|
|
359
|
+
"cognito:username": username,
|
|
360
|
+
sub: "machine-sub",
|
|
361
|
+
...claimOverride,
|
|
362
|
+
};
|
|
363
|
+
for (const [key, value] of Object.entries(claims)) {
|
|
364
|
+
if (value === undefined) delete claims[key];
|
|
365
|
+
}
|
|
366
|
+
stubMintFetch({
|
|
367
|
+
AccessToken: fakeJwt({
|
|
368
|
+
token_use: "access",
|
|
369
|
+
client_id: CONFIG.clientId,
|
|
370
|
+
username,
|
|
371
|
+
sub: "machine-sub",
|
|
372
|
+
}),
|
|
373
|
+
IdToken: fakeJwt(claims),
|
|
374
|
+
});
|
|
375
|
+
const { mintMachineTokens, loadCachedTokens, CognitoAuthError } =
|
|
376
|
+
await importModule();
|
|
377
|
+
|
|
378
|
+
await expect(mintMachineTokens(CONFIG)).rejects.toBeInstanceOf(
|
|
379
|
+
CognitoAuthError,
|
|
380
|
+
);
|
|
381
|
+
expect(loadCachedTokens()).toBeNull();
|
|
382
|
+
},
|
|
383
|
+
);
|
|
238
384
|
});
|
|
239
385
|
|
|
240
386
|
// ---------------------------------------------------------------------------
|
|
@@ -273,6 +419,38 @@ describe("getValidMachineTokens", () => {
|
|
|
273
419
|
expect(fetchMock).not.toHaveBeenCalled();
|
|
274
420
|
});
|
|
275
421
|
|
|
422
|
+
it("strips and persists an empty refresh token when reusing a machine cache", async () => {
|
|
423
|
+
writeCreds();
|
|
424
|
+
const { fetchMock } = stubMintFetch();
|
|
425
|
+
const { saveCachedTokens, loadCachedTokens, getValidMachineTokens } =
|
|
426
|
+
await importModule();
|
|
427
|
+
saveCachedTokens({
|
|
428
|
+
accessToken: fakeJwt({
|
|
429
|
+
token_use: "access",
|
|
430
|
+
client_id: CONFIG.clientId,
|
|
431
|
+
username: "machine-agt_01TEST",
|
|
432
|
+
sub: "machine-sub",
|
|
433
|
+
}),
|
|
434
|
+
idToken: fakeJwt({
|
|
435
|
+
token_use: "id",
|
|
436
|
+
aud: CONFIG.clientId,
|
|
437
|
+
"custom:entityType": "agent",
|
|
438
|
+
"custom:entityUid": "agt_01TEST",
|
|
439
|
+
"cognito:username": "machine-agt_01TEST",
|
|
440
|
+
sub: "machine-sub",
|
|
441
|
+
}),
|
|
442
|
+
refreshToken: "legacy-machine-refresh-token",
|
|
443
|
+
expiresAt: Date.now() + 30 * 60 * 1000,
|
|
444
|
+
tokenType: "Bearer",
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
const tokens = await getValidMachineTokens(CONFIG);
|
|
448
|
+
|
|
449
|
+
expect(tokens.refreshToken).toBe("");
|
|
450
|
+
expect(loadCachedTokens()?.refreshToken).toBe("");
|
|
451
|
+
expect(fetchMock).not.toHaveBeenCalled();
|
|
452
|
+
});
|
|
453
|
+
|
|
276
454
|
it("re-mints when the cache is expiring", async () => {
|
|
277
455
|
writeCreds();
|
|
278
456
|
const { fetchMock } = stubMintFetch();
|
|
@@ -353,6 +531,399 @@ describe("getValidMachineTokens", () => {
|
|
|
353
531
|
expect(token).toBe(freshId);
|
|
354
532
|
expect(loadCachedTokens()?.idToken).toBe(freshId);
|
|
355
533
|
});
|
|
534
|
+
|
|
535
|
+
it("accepts an Outpost machine cache with delegated claims, then re-mints after expiry without OAuth refresh", async () => {
|
|
536
|
+
const username = "out-01test@outposts.getindigo.ai";
|
|
537
|
+
const entityUid = "out_01TEST";
|
|
538
|
+
writeCreds({
|
|
539
|
+
username,
|
|
540
|
+
secret: "outpost-secret",
|
|
541
|
+
clientId: CONFIG.clientId,
|
|
542
|
+
region: CONFIG.region,
|
|
543
|
+
entityType: "outpost",
|
|
544
|
+
entityUid,
|
|
545
|
+
});
|
|
546
|
+
const accessToken = fakeJwt({
|
|
547
|
+
token_use: "access",
|
|
548
|
+
client_id: CONFIG.clientId,
|
|
549
|
+
username,
|
|
550
|
+
sub: "machine-sub",
|
|
551
|
+
});
|
|
552
|
+
const idToken = fakeJwt({
|
|
553
|
+
token_use: "id",
|
|
554
|
+
aud: CONFIG.clientId,
|
|
555
|
+
"custom:entityType": "outpost",
|
|
556
|
+
"custom:entityUid": entityUid,
|
|
557
|
+
"custom:delegatedSub": "human-sub",
|
|
558
|
+
"custom:delegatedEmail": "owner@example.com",
|
|
559
|
+
"cognito:username": username,
|
|
560
|
+
sub: "machine-sub",
|
|
561
|
+
});
|
|
562
|
+
const { fetchMock, calls } = stubMintFetch({
|
|
563
|
+
AccessToken: accessToken,
|
|
564
|
+
IdToken: idToken,
|
|
565
|
+
RefreshToken: "must-not-be-stored",
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
const firstModule = await importModule();
|
|
569
|
+
const first = await firstModule.getValidMachineTokens(CONFIG);
|
|
570
|
+
expect(first.idToken).toBe(idToken);
|
|
571
|
+
expect(firstModule.loadCachedTokens()?.refreshToken).toBe("");
|
|
572
|
+
|
|
573
|
+
// A new process accepts the shared Outpost cache without another mint.
|
|
574
|
+
vi.resetModules();
|
|
575
|
+
const cacheHitModule = await importModule();
|
|
576
|
+
await expect(cacheHitModule.getValidMachineTokens(CONFIG)).resolves.toEqual(
|
|
577
|
+
first,
|
|
578
|
+
);
|
|
579
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
580
|
+
|
|
581
|
+
// Once expired, a new process re-mints with the permanent secret. It never
|
|
582
|
+
// attempts the browser client's /oauth2/token refresh-token flow.
|
|
583
|
+
cacheHitModule.saveCachedTokens({
|
|
584
|
+
...first,
|
|
585
|
+
expiresAt: Date.now() - 1,
|
|
586
|
+
});
|
|
587
|
+
vi.resetModules();
|
|
588
|
+
const expiredModule = await importModule();
|
|
589
|
+
await expiredModule.getValidMachineTokens(CONFIG);
|
|
590
|
+
|
|
591
|
+
expect(fetchMock).toHaveBeenCalledTimes(2);
|
|
592
|
+
expect(calls.every(({ url }) => !url.includes("/oauth2/token"))).toBe(true);
|
|
593
|
+
for (const { init } of calls) {
|
|
594
|
+
const body = JSON.parse(String(init.body));
|
|
595
|
+
expect(body).toMatchObject({
|
|
596
|
+
AuthFlow: "USER_PASSWORD_AUTH",
|
|
597
|
+
ClientId: CONFIG.clientId,
|
|
598
|
+
AuthParameters: { USERNAME: username, PASSWORD: "outpost-secret" },
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
expect(expiredModule.loadCachedTokens()?.refreshToken).toBe("");
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
it.each([
|
|
605
|
+
[
|
|
606
|
+
"entity type",
|
|
607
|
+
{ entityType: "outpost", entityUid: "out_01TEST" },
|
|
608
|
+
"agent",
|
|
609
|
+
"agt_01TEST",
|
|
610
|
+
],
|
|
611
|
+
[
|
|
612
|
+
"entity uid",
|
|
613
|
+
{ entityType: "outpost", entityUid: "out_01TEST" },
|
|
614
|
+
"outpost",
|
|
615
|
+
"out_OTHER",
|
|
616
|
+
],
|
|
617
|
+
] as const)(
|
|
618
|
+
"re-mints when the cached machine %s does not match the creds",
|
|
619
|
+
async (_label, expectedIdentity, cachedEntityType, cachedEntityUid) => {
|
|
620
|
+
const username = "out-01test@outposts.getindigo.ai";
|
|
621
|
+
writeCreds({
|
|
622
|
+
username,
|
|
623
|
+
secret: "outpost-secret",
|
|
624
|
+
...expectedIdentity,
|
|
625
|
+
});
|
|
626
|
+
const { fetchMock } = stubMintFetch();
|
|
627
|
+
const { saveCachedTokens, getValidMachineTokens } = await importModule();
|
|
628
|
+
saveCachedTokens({
|
|
629
|
+
accessToken: fakeJwt({
|
|
630
|
+
token_use: "access",
|
|
631
|
+
client_id: CONFIG.clientId,
|
|
632
|
+
username,
|
|
633
|
+
sub: "machine-sub",
|
|
634
|
+
}),
|
|
635
|
+
idToken: fakeJwt({
|
|
636
|
+
token_use: "id",
|
|
637
|
+
aud: CONFIG.clientId,
|
|
638
|
+
"custom:entityType": cachedEntityType,
|
|
639
|
+
"custom:entityUid": cachedEntityUid,
|
|
640
|
+
"custom:delegatedSub": "human-sub",
|
|
641
|
+
"custom:delegatedEmail": "owner@example.com",
|
|
642
|
+
"cognito:username": username,
|
|
643
|
+
sub: "machine-sub",
|
|
644
|
+
}),
|
|
645
|
+
refreshToken: "",
|
|
646
|
+
expiresAt: Date.now() + 30 * 60 * 1000,
|
|
647
|
+
tokenType: "Bearer",
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
await getValidMachineTokens(CONFIG);
|
|
651
|
+
|
|
652
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
653
|
+
},
|
|
654
|
+
);
|
|
655
|
+
|
|
656
|
+
it.each(["custom:delegatedSub", "custom:delegatedEmail"])(
|
|
657
|
+
"re-mints when an Outpost cache is missing %s",
|
|
658
|
+
async (missingClaim) => {
|
|
659
|
+
const username = "out-01test@outposts.getindigo.ai";
|
|
660
|
+
const outpostClaims: Record<string, unknown> = {
|
|
661
|
+
token_use: "id",
|
|
662
|
+
aud: CONFIG.clientId,
|
|
663
|
+
"custom:entityType": "outpost",
|
|
664
|
+
"custom:entityUid": "out_01TEST",
|
|
665
|
+
"custom:delegatedSub": "human-sub",
|
|
666
|
+
"custom:delegatedEmail": "owner@example.com",
|
|
667
|
+
"cognito:username": username,
|
|
668
|
+
sub: "machine-sub",
|
|
669
|
+
};
|
|
670
|
+
delete outpostClaims[missingClaim];
|
|
671
|
+
writeCreds({
|
|
672
|
+
username,
|
|
673
|
+
secret: "outpost-secret",
|
|
674
|
+
entityType: "outpost",
|
|
675
|
+
entityUid: "out_01TEST",
|
|
676
|
+
});
|
|
677
|
+
const { fetchMock } = stubMintFetch();
|
|
678
|
+
const { saveCachedTokens, getValidMachineTokens } = await importModule();
|
|
679
|
+
saveCachedTokens({
|
|
680
|
+
accessToken: fakeJwt({
|
|
681
|
+
token_use: "access",
|
|
682
|
+
client_id: CONFIG.clientId,
|
|
683
|
+
username,
|
|
684
|
+
sub: "machine-sub",
|
|
685
|
+
}),
|
|
686
|
+
idToken: fakeJwt(outpostClaims),
|
|
687
|
+
refreshToken: "",
|
|
688
|
+
expiresAt: Date.now() + 30 * 60 * 1000,
|
|
689
|
+
tokenType: "Bearer",
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
await getValidMachineTokens(CONFIG);
|
|
693
|
+
|
|
694
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
695
|
+
},
|
|
696
|
+
);
|
|
697
|
+
|
|
698
|
+
it("keeps accepting legacy agent creds without entity fields", async () => {
|
|
699
|
+
writeCreds({ username: "machine-agt_01TEST", secret: "s3cret" });
|
|
700
|
+
const { fetchMock } = stubMintFetch();
|
|
701
|
+
const { saveCachedTokens, getValidMachineTokens } = await importModule();
|
|
702
|
+
const cached = {
|
|
703
|
+
accessToken: fakeJwt({
|
|
704
|
+
token_use: "access",
|
|
705
|
+
client_id: CONFIG.clientId,
|
|
706
|
+
username: "machine-agt_01TEST",
|
|
707
|
+
sub: "machine-sub",
|
|
708
|
+
}),
|
|
709
|
+
idToken: fakeJwt({
|
|
710
|
+
token_use: "id",
|
|
711
|
+
aud: CONFIG.clientId,
|
|
712
|
+
"custom:entityType": "agent",
|
|
713
|
+
"custom:entityUid": "agt_01TEST",
|
|
714
|
+
"cognito:username": "machine-agt_01TEST",
|
|
715
|
+
sub: "machine-sub",
|
|
716
|
+
}),
|
|
717
|
+
refreshToken: "",
|
|
718
|
+
expiresAt: Date.now() + 30 * 60 * 1000,
|
|
719
|
+
tokenType: "Bearer" as const,
|
|
720
|
+
};
|
|
721
|
+
saveCachedTokens(cached);
|
|
722
|
+
|
|
723
|
+
await expect(getValidMachineTokens(CONFIG)).resolves.toEqual(cached);
|
|
724
|
+
expect(fetchMock).not.toHaveBeenCalled();
|
|
725
|
+
});
|
|
726
|
+
|
|
727
|
+
it("invalidates the memo after the creds file is reprovisioned", async () => {
|
|
728
|
+
writeCreds({
|
|
729
|
+
username: "machine-a",
|
|
730
|
+
secret: "secret-a",
|
|
731
|
+
entityType: "agent",
|
|
732
|
+
entityUid: "agt_A",
|
|
733
|
+
});
|
|
734
|
+
const { fetchMock } = stubMintFetch();
|
|
735
|
+
const { getValidMachineTokens } = await importModule();
|
|
736
|
+
|
|
737
|
+
const first = await getValidMachineTokens(CONFIG);
|
|
738
|
+
writeCreds({
|
|
739
|
+
username: "machine-b",
|
|
740
|
+
secret: "secret-b",
|
|
741
|
+
entityType: "outpost",
|
|
742
|
+
entityUid: "out_B",
|
|
743
|
+
});
|
|
744
|
+
const second = await getValidMachineTokens(CONFIG);
|
|
745
|
+
|
|
746
|
+
expect(fetchMock).toHaveBeenCalledTimes(2);
|
|
747
|
+
expect(first.idToken).not.toBe(second.idToken);
|
|
748
|
+
expect(
|
|
749
|
+
JSON.parse(
|
|
750
|
+
Buffer.from(second.idToken.split(".")[1], "base64url").toString(),
|
|
751
|
+
)["custom:entityUid"],
|
|
752
|
+
).toBe("out_B");
|
|
753
|
+
});
|
|
754
|
+
|
|
755
|
+
it("invalidates the memo when legacy creds use a different config client", async () => {
|
|
756
|
+
writeCreds();
|
|
757
|
+
const { fetchMock } = stubMintFetch();
|
|
758
|
+
const { getValidMachineTokens } = await importModule();
|
|
759
|
+
const otherConfig = { ...CONFIG, clientId: "other-client-id" };
|
|
760
|
+
|
|
761
|
+
const first = await getValidMachineTokens(CONFIG);
|
|
762
|
+
const second = await getValidMachineTokens(otherConfig);
|
|
763
|
+
|
|
764
|
+
expect(fetchMock).toHaveBeenCalledTimes(2);
|
|
765
|
+
expect(first.idToken).not.toBe(second.idToken);
|
|
766
|
+
expect(
|
|
767
|
+
JSON.parse(
|
|
768
|
+
Buffer.from(second.idToken.split(".")[1], "base64url").toString(),
|
|
769
|
+
).aud,
|
|
770
|
+
).toBe(otherConfig.clientId);
|
|
771
|
+
});
|
|
772
|
+
|
|
773
|
+
it("does not attach a reprovisioned identity to the previous identity's in-flight mint", async () => {
|
|
774
|
+
writeCreds({
|
|
775
|
+
username: "machine-a",
|
|
776
|
+
secret: "secret-a",
|
|
777
|
+
entityType: "agent",
|
|
778
|
+
entityUid: "agt_A",
|
|
779
|
+
});
|
|
780
|
+
let releaseFirstMint: (() => void) | undefined;
|
|
781
|
+
const firstMintGate = new Promise<void>((resolve) => {
|
|
782
|
+
releaseFirstMint = resolve;
|
|
783
|
+
});
|
|
784
|
+
let signalFirstMint: (() => void) | undefined;
|
|
785
|
+
const firstMintStarted = new Promise<void>((resolve) => {
|
|
786
|
+
signalFirstMint = resolve;
|
|
787
|
+
});
|
|
788
|
+
let callCount = 0;
|
|
789
|
+
const fetchMock = vi.fn(async (_url: string | URL, init?: RequestInit) => {
|
|
790
|
+
callCount += 1;
|
|
791
|
+
const body = JSON.parse(String(init?.body)) as {
|
|
792
|
+
ClientId: string;
|
|
793
|
+
AuthParameters: { USERNAME: string };
|
|
794
|
+
};
|
|
795
|
+
if (callCount === 1) {
|
|
796
|
+
signalFirstMint?.();
|
|
797
|
+
await firstMintGate;
|
|
798
|
+
}
|
|
799
|
+
const username = body.AuthParameters.USERNAME;
|
|
800
|
+
const uid = username === "machine-a" ? "agt_A" : "agt_B";
|
|
801
|
+
return new Response(
|
|
802
|
+
JSON.stringify({
|
|
803
|
+
AuthenticationResult: {
|
|
804
|
+
AccessToken: fakeJwt({
|
|
805
|
+
token_use: "access",
|
|
806
|
+
client_id: body.ClientId,
|
|
807
|
+
username,
|
|
808
|
+
sub: `sub:${username}`,
|
|
809
|
+
}),
|
|
810
|
+
IdToken: fakeJwt({
|
|
811
|
+
token_use: "id",
|
|
812
|
+
aud: body.ClientId,
|
|
813
|
+
"custom:entityType": "agent",
|
|
814
|
+
"custom:entityUid": uid,
|
|
815
|
+
"cognito:username": username,
|
|
816
|
+
sub: `sub:${username}`,
|
|
817
|
+
}),
|
|
818
|
+
ExpiresIn: 3600,
|
|
819
|
+
},
|
|
820
|
+
}),
|
|
821
|
+
{ status: 200 },
|
|
822
|
+
);
|
|
823
|
+
});
|
|
824
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
825
|
+
const { getValidMachineTokens } = await importModule();
|
|
826
|
+
|
|
827
|
+
const first = getValidMachineTokens(CONFIG);
|
|
828
|
+
await firstMintStarted;
|
|
829
|
+
writeCreds({
|
|
830
|
+
username: "machine-b",
|
|
831
|
+
secret: "secret-b",
|
|
832
|
+
entityType: "agent",
|
|
833
|
+
entityUid: "agt_B",
|
|
834
|
+
});
|
|
835
|
+
const second = getValidMachineTokens(CONFIG);
|
|
836
|
+
releaseFirstMint?.();
|
|
837
|
+
|
|
838
|
+
const [firstTokens, secondTokens] = await Promise.all([first, second]);
|
|
839
|
+
expect(fetchMock).toHaveBeenCalledTimes(2);
|
|
840
|
+
expect(firstTokens.idToken).not.toBe(secondTokens.idToken);
|
|
841
|
+
expect(
|
|
842
|
+
JSON.parse(
|
|
843
|
+
Buffer.from(secondTokens.idToken.split(".")[1], "base64url").toString(),
|
|
844
|
+
)["custom:entityUid"],
|
|
845
|
+
).toBe("agt_B");
|
|
846
|
+
});
|
|
847
|
+
|
|
848
|
+
it("reuses only an empty-refresh matching cache after a lock timeout", async () => {
|
|
849
|
+
writeCreds();
|
|
850
|
+
const { fetchMock } = stubMintFetch();
|
|
851
|
+
const { saveCachedTokens, getValidMachineTokens } = await importModule();
|
|
852
|
+
const cached = {
|
|
853
|
+
accessToken: fakeJwt({
|
|
854
|
+
token_use: "access",
|
|
855
|
+
client_id: CONFIG.clientId,
|
|
856
|
+
username: "machine-agt_01TEST",
|
|
857
|
+
sub: "machine-sub",
|
|
858
|
+
}),
|
|
859
|
+
idToken: fakeJwt({
|
|
860
|
+
token_use: "id",
|
|
861
|
+
aud: CONFIG.clientId,
|
|
862
|
+
"custom:entityType": "agent",
|
|
863
|
+
"custom:entityUid": "agt_01TEST",
|
|
864
|
+
"cognito:username": "machine-agt_01TEST",
|
|
865
|
+
sub: "machine-sub",
|
|
866
|
+
}),
|
|
867
|
+
refreshToken: "",
|
|
868
|
+
expiresAt: Date.now() + 30 * 60 * 1000,
|
|
869
|
+
tokenType: "Bearer" as const,
|
|
870
|
+
};
|
|
871
|
+
saveCachedTokens(cached);
|
|
872
|
+
fs.writeFileSync(
|
|
873
|
+
path.join(tmpHome, ".hq", "cognito-tokens.json.lock"),
|
|
874
|
+
String(process.pid),
|
|
875
|
+
);
|
|
876
|
+
vi.useFakeTimers();
|
|
877
|
+
|
|
878
|
+
const resolution = getValidMachineTokens(CONFIG);
|
|
879
|
+
await vi.advanceTimersByTimeAsync(20_050);
|
|
880
|
+
|
|
881
|
+
await expect(resolution).resolves.toEqual(cached);
|
|
882
|
+
expect(fetchMock).not.toHaveBeenCalled();
|
|
883
|
+
});
|
|
884
|
+
|
|
885
|
+
it("rejects a legacy non-empty-refresh cache after a lock timeout without minting unlocked", async () => {
|
|
886
|
+
writeCreds();
|
|
887
|
+
const { fetchMock } = stubMintFetch();
|
|
888
|
+
const {
|
|
889
|
+
saveCachedTokens,
|
|
890
|
+
getValidMachineTokens,
|
|
891
|
+
CognitoAuthError,
|
|
892
|
+
} = await importModule();
|
|
893
|
+
saveCachedTokens({
|
|
894
|
+
accessToken: fakeJwt({
|
|
895
|
+
token_use: "access",
|
|
896
|
+
client_id: CONFIG.clientId,
|
|
897
|
+
username: "machine-agt_01TEST",
|
|
898
|
+
sub: "machine-sub",
|
|
899
|
+
}),
|
|
900
|
+
idToken: fakeJwt({
|
|
901
|
+
token_use: "id",
|
|
902
|
+
aud: CONFIG.clientId,
|
|
903
|
+
"custom:entityType": "agent",
|
|
904
|
+
"custom:entityUid": "agt_01TEST",
|
|
905
|
+
"cognito:username": "machine-agt_01TEST",
|
|
906
|
+
sub: "machine-sub",
|
|
907
|
+
}),
|
|
908
|
+
refreshToken: "legacy-refresh",
|
|
909
|
+
expiresAt: Date.now() + 30 * 60 * 1000,
|
|
910
|
+
tokenType: "Bearer",
|
|
911
|
+
});
|
|
912
|
+
fs.writeFileSync(
|
|
913
|
+
path.join(tmpHome, ".hq", "cognito-tokens.json.lock"),
|
|
914
|
+
String(process.pid),
|
|
915
|
+
);
|
|
916
|
+
vi.useFakeTimers();
|
|
917
|
+
|
|
918
|
+
const resolution = getValidMachineTokens(CONFIG);
|
|
919
|
+
const rejection = expect(resolution).rejects.toBeInstanceOf(
|
|
920
|
+
CognitoAuthError,
|
|
921
|
+
);
|
|
922
|
+
await vi.advanceTimersByTimeAsync(20_050);
|
|
923
|
+
|
|
924
|
+
await rejection;
|
|
925
|
+
expect(fetchMock).not.toHaveBeenCalled();
|
|
926
|
+
});
|
|
356
927
|
});
|
|
357
928
|
|
|
358
929
|
// ---------------------------------------------------------------------------
|
|
@@ -406,6 +977,226 @@ describe("regression: constant mints regardless of file count", () => {
|
|
|
406
977
|
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
407
978
|
},
|
|
408
979
|
);
|
|
980
|
+
|
|
981
|
+
it("serializes independent module instances through the shared cache lock", async () => {
|
|
982
|
+
const username = "machine-agt_01TEST";
|
|
983
|
+
writeCreds({
|
|
984
|
+
username,
|
|
985
|
+
secret: "s3cret",
|
|
986
|
+
entityType: "agent",
|
|
987
|
+
entityUid: "agt_01TEST",
|
|
988
|
+
});
|
|
989
|
+
let releaseMint: (() => void) | undefined;
|
|
990
|
+
const mintGate = new Promise<void>((resolve) => {
|
|
991
|
+
releaseMint = resolve;
|
|
992
|
+
});
|
|
993
|
+
let signalMintStarted: (() => void) | undefined;
|
|
994
|
+
const mintStarted = new Promise<void>((resolve) => {
|
|
995
|
+
signalMintStarted = resolve;
|
|
996
|
+
});
|
|
997
|
+
const accessToken = fakeJwt({
|
|
998
|
+
token_use: "access",
|
|
999
|
+
client_id: CONFIG.clientId,
|
|
1000
|
+
username,
|
|
1001
|
+
sub: "machine-sub",
|
|
1002
|
+
});
|
|
1003
|
+
const idToken = fakeJwt({
|
|
1004
|
+
token_use: "id",
|
|
1005
|
+
aud: CONFIG.clientId,
|
|
1006
|
+
"custom:entityType": "agent",
|
|
1007
|
+
"custom:entityUid": "agt_01TEST",
|
|
1008
|
+
"cognito:username": username,
|
|
1009
|
+
sub: "machine-sub",
|
|
1010
|
+
});
|
|
1011
|
+
const fetchMock = vi.fn(async () => {
|
|
1012
|
+
signalMintStarted?.();
|
|
1013
|
+
await mintGate;
|
|
1014
|
+
return new Response(
|
|
1015
|
+
JSON.stringify({
|
|
1016
|
+
AuthenticationResult: {
|
|
1017
|
+
AccessToken: accessToken,
|
|
1018
|
+
IdToken: idToken,
|
|
1019
|
+
RefreshToken: "must-not-be-stored",
|
|
1020
|
+
ExpiresIn: 3600,
|
|
1021
|
+
},
|
|
1022
|
+
}),
|
|
1023
|
+
{ status: 200 },
|
|
1024
|
+
);
|
|
1025
|
+
});
|
|
1026
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
1027
|
+
|
|
1028
|
+
const firstProcessModule = await importModule();
|
|
1029
|
+
vi.resetModules();
|
|
1030
|
+
const secondProcessModule = await importModule();
|
|
1031
|
+
|
|
1032
|
+
const first = firstProcessModule.getValidMachineTokens(CONFIG);
|
|
1033
|
+
await mintStarted;
|
|
1034
|
+
const second = secondProcessModule.getValidMachineTokens(CONFIG);
|
|
1035
|
+
releaseMint?.();
|
|
1036
|
+
|
|
1037
|
+
const [firstTokens, secondTokens] = await Promise.all([first, second]);
|
|
1038
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
1039
|
+
expect(secondTokens).toEqual(firstTokens);
|
|
1040
|
+
expect(secondProcessModule.loadCachedTokens()?.refreshToken).toBe("");
|
|
1041
|
+
});
|
|
1042
|
+
|
|
1043
|
+
it("serializes the exported direct mint API", async () => {
|
|
1044
|
+
writeCreds();
|
|
1045
|
+
let releaseFirstMint: (() => void) | undefined;
|
|
1046
|
+
const firstMintGate = new Promise<void>((resolve) => {
|
|
1047
|
+
releaseFirstMint = resolve;
|
|
1048
|
+
});
|
|
1049
|
+
let signalFirstMint: (() => void) | undefined;
|
|
1050
|
+
const firstMintStarted = new Promise<void>((resolve) => {
|
|
1051
|
+
signalFirstMint = resolve;
|
|
1052
|
+
});
|
|
1053
|
+
let activeMints = 0;
|
|
1054
|
+
let maxActiveMints = 0;
|
|
1055
|
+
let callCount = 0;
|
|
1056
|
+
const accessToken = fakeJwt({
|
|
1057
|
+
token_use: "access",
|
|
1058
|
+
client_id: CONFIG.clientId,
|
|
1059
|
+
username: "machine-agt_01TEST",
|
|
1060
|
+
sub: "machine-sub",
|
|
1061
|
+
});
|
|
1062
|
+
const idToken = fakeJwt({
|
|
1063
|
+
token_use: "id",
|
|
1064
|
+
aud: CONFIG.clientId,
|
|
1065
|
+
"custom:entityType": "agent",
|
|
1066
|
+
"custom:entityUid": "agt_01TEST",
|
|
1067
|
+
"cognito:username": "machine-agt_01TEST",
|
|
1068
|
+
sub: "machine-sub",
|
|
1069
|
+
});
|
|
1070
|
+
const fetchMock = vi.fn(async () => {
|
|
1071
|
+
callCount += 1;
|
|
1072
|
+
activeMints += 1;
|
|
1073
|
+
maxActiveMints = Math.max(maxActiveMints, activeMints);
|
|
1074
|
+
if (callCount === 1) {
|
|
1075
|
+
signalFirstMint?.();
|
|
1076
|
+
await firstMintGate;
|
|
1077
|
+
}
|
|
1078
|
+
activeMints -= 1;
|
|
1079
|
+
return new Response(
|
|
1080
|
+
JSON.stringify({
|
|
1081
|
+
AuthenticationResult: {
|
|
1082
|
+
AccessToken: accessToken,
|
|
1083
|
+
IdToken: idToken,
|
|
1084
|
+
ExpiresIn: 3600,
|
|
1085
|
+
},
|
|
1086
|
+
}),
|
|
1087
|
+
{ status: 200 },
|
|
1088
|
+
);
|
|
1089
|
+
});
|
|
1090
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
1091
|
+
const firstModule = await importModule();
|
|
1092
|
+
vi.resetModules();
|
|
1093
|
+
const secondModule = await importModule();
|
|
1094
|
+
|
|
1095
|
+
const first = firstModule.mintMachineTokens(CONFIG);
|
|
1096
|
+
await firstMintStarted;
|
|
1097
|
+
const second = secondModule.mintMachineTokens(CONFIG);
|
|
1098
|
+
releaseFirstMint?.();
|
|
1099
|
+
await Promise.all([first, second]);
|
|
1100
|
+
|
|
1101
|
+
expect(fetchMock).toHaveBeenCalledTimes(2);
|
|
1102
|
+
expect(maxActiveMints).toBe(1);
|
|
1103
|
+
});
|
|
1104
|
+
|
|
1105
|
+
it("serializes direct mints in actual separate processes", async () => {
|
|
1106
|
+
const moduleUrl = pathToFileURL(
|
|
1107
|
+
path.join(process.cwd(), "src", "cognito-auth.ts"),
|
|
1108
|
+
).href;
|
|
1109
|
+
const marker = path.join(tmpHome, "mint-active");
|
|
1110
|
+
const overlap = path.join(tmpHome, "mint-overlap");
|
|
1111
|
+
const start = path.join(tmpHome, "mint-start");
|
|
1112
|
+
const childScript = String.raw`
|
|
1113
|
+
import fs from "fs";
|
|
1114
|
+
const pause = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
1115
|
+
const fakeJwt = (claims) => {
|
|
1116
|
+
const enc = (value) => Buffer.from(JSON.stringify(value)).toString("base64url");
|
|
1117
|
+
return enc({ alg: "none" }) + "." + enc(claims) + ".sig";
|
|
1118
|
+
};
|
|
1119
|
+
const auth = await import(process.env.HQ_LOCK_TEST_MODULE_URL);
|
|
1120
|
+
fs.writeFileSync(process.env.HQ_LOCK_TEST_READY, "ready");
|
|
1121
|
+
while (!fs.existsSync(process.env.HQ_LOCK_TEST_START)) await pause(5);
|
|
1122
|
+
globalThis.fetch = async () => {
|
|
1123
|
+
let ownsMarker = false;
|
|
1124
|
+
try {
|
|
1125
|
+
const fd = fs.openSync(process.env.HQ_LOCK_TEST_MARKER, "wx");
|
|
1126
|
+
fs.closeSync(fd);
|
|
1127
|
+
ownsMarker = true;
|
|
1128
|
+
} catch (error) {
|
|
1129
|
+
if (error.code === "EEXIST") {
|
|
1130
|
+
fs.writeFileSync(process.env.HQ_LOCK_TEST_OVERLAP, "overlap");
|
|
1131
|
+
} else {
|
|
1132
|
+
throw error;
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
await pause(125);
|
|
1136
|
+
if (ownsMarker) fs.unlinkSync(process.env.HQ_LOCK_TEST_MARKER);
|
|
1137
|
+
return new Response(JSON.stringify({ AuthenticationResult: {
|
|
1138
|
+
AccessToken: fakeJwt({ token_use: "access", client_id: "test-client-id", username: "machine", sub: "sub" }),
|
|
1139
|
+
IdToken: fakeJwt({ token_use: "id", aud: "test-client-id", "custom:entityType": "agent", "custom:entityUid": "agt_TEST", "cognito:username": "machine", sub: "sub" }),
|
|
1140
|
+
ExpiresIn: 3600
|
|
1141
|
+
}}), { status: 200 });
|
|
1142
|
+
};
|
|
1143
|
+
await auth.mintMachineTokens(
|
|
1144
|
+
{ region: "us-east-1", userPoolDomain: "unused", clientId: "test-client-id" },
|
|
1145
|
+
{ username: "machine", secret: "secret", entityType: "agent", entityUid: "agt_TEST" },
|
|
1146
|
+
{ baseDelayMs: 0 }
|
|
1147
|
+
);
|
|
1148
|
+
`;
|
|
1149
|
+
const children = ["a", "b"].map((name) => {
|
|
1150
|
+
const child = spawn(
|
|
1151
|
+
process.execPath,
|
|
1152
|
+
["--experimental-strip-types", "--input-type=module", "-e", childScript],
|
|
1153
|
+
{
|
|
1154
|
+
cwd: process.cwd(),
|
|
1155
|
+
env: {
|
|
1156
|
+
...process.env,
|
|
1157
|
+
HOME: tmpHome,
|
|
1158
|
+
HQ_LOCK_TEST_MODULE_URL: moduleUrl,
|
|
1159
|
+
HQ_LOCK_TEST_READY: path.join(tmpHome, `ready-${name}`),
|
|
1160
|
+
HQ_LOCK_TEST_START: start,
|
|
1161
|
+
HQ_LOCK_TEST_MARKER: marker,
|
|
1162
|
+
HQ_LOCK_TEST_OVERLAP: overlap,
|
|
1163
|
+
},
|
|
1164
|
+
},
|
|
1165
|
+
);
|
|
1166
|
+
let stderr = "";
|
|
1167
|
+
child.stderr.on("data", (chunk) => {
|
|
1168
|
+
stderr += String(chunk);
|
|
1169
|
+
});
|
|
1170
|
+
return { child, stderr: () => stderr };
|
|
1171
|
+
});
|
|
1172
|
+
const readyPaths = ["a", "b"].map((name) =>
|
|
1173
|
+
path.join(tmpHome, `ready-${name}`),
|
|
1174
|
+
);
|
|
1175
|
+
const readyDeadline = Date.now() + 5_000;
|
|
1176
|
+
while (
|
|
1177
|
+
!readyPaths.every((readyPath) => fs.existsSync(readyPath)) &&
|
|
1178
|
+
Date.now() < readyDeadline
|
|
1179
|
+
) {
|
|
1180
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
1181
|
+
}
|
|
1182
|
+
expect(readyPaths.every((readyPath) => fs.existsSync(readyPath))).toBe(true);
|
|
1183
|
+
fs.writeFileSync(start, "start");
|
|
1184
|
+
|
|
1185
|
+
const exits = await Promise.all(
|
|
1186
|
+
children.map(
|
|
1187
|
+
({ child, stderr }) =>
|
|
1188
|
+
new Promise<{ code: number | null; stderr: string }>((resolve) => {
|
|
1189
|
+
child.on("exit", (code) => resolve({ code, stderr: stderr() }));
|
|
1190
|
+
}),
|
|
1191
|
+
),
|
|
1192
|
+
);
|
|
1193
|
+
|
|
1194
|
+
expect(exits).toEqual([
|
|
1195
|
+
{ code: 0, stderr: "" },
|
|
1196
|
+
{ code: 0, stderr: "" },
|
|
1197
|
+
]);
|
|
1198
|
+
expect(fs.existsSync(overlap)).toBe(false);
|
|
1199
|
+
}, 10_000);
|
|
409
1200
|
});
|
|
410
1201
|
|
|
411
1202
|
// ---------------------------------------------------------------------------
|
|
@@ -431,8 +1222,20 @@ describe("mintMachineTokens throttle retry", () => {
|
|
|
431
1222
|
return new Response(
|
|
432
1223
|
JSON.stringify({
|
|
433
1224
|
AuthenticationResult: {
|
|
434
|
-
AccessToken: fakeJwt({
|
|
435
|
-
|
|
1225
|
+
AccessToken: fakeJwt({
|
|
1226
|
+
token_use: "access",
|
|
1227
|
+
client_id: CONFIG.clientId,
|
|
1228
|
+
username: "machine-agt_01TEST",
|
|
1229
|
+
sub: "machine-sub",
|
|
1230
|
+
}),
|
|
1231
|
+
IdToken: fakeJwt({
|
|
1232
|
+
token_use: "id",
|
|
1233
|
+
aud: CONFIG.clientId,
|
|
1234
|
+
"custom:entityType": "agent",
|
|
1235
|
+
"custom:entityUid": "agt_01TEST",
|
|
1236
|
+
"cognito:username": "machine-agt_01TEST",
|
|
1237
|
+
sub: "machine-sub",
|
|
1238
|
+
}),
|
|
436
1239
|
ExpiresIn: 3600,
|
|
437
1240
|
},
|
|
438
1241
|
}),
|