@indigoai-us/hq-cloud 6.14.13 → 6.14.15
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/dist/bin/sync-runner-watch-loop.d.ts +9 -0
- package/dist/bin/sync-runner-watch-loop.d.ts.map +1 -1
- package/dist/bin/sync-runner-watch-loop.js +35 -2
- package/dist/bin/sync-runner-watch-loop.js.map +1 -1
- package/dist/bin/sync-runner.d.ts +7 -0
- package/dist/bin/sync-runner.d.ts.map +1 -1
- package/dist/bin/sync-runner.js.map +1 -1
- package/dist/bin/sync-runner.test.js +100 -0
- package/dist/bin/sync-runner.test.js.map +1 -1
- package/dist/cognito-auth.d.ts.map +1 -1
- package/dist/cognito-auth.js +16 -1
- package/dist/cognito-auth.js.map +1 -1
- package/dist/cognito-auth.test.js +107 -0
- package/dist/cognito-auth.test.js.map +1 -1
- package/package.json +1 -1
- package/src/bin/sync-runner-watch-loop.ts +44 -3
- package/src/bin/sync-runner.test.ts +122 -0
- package/src/bin/sync-runner.ts +7 -0
- package/src/cognito-auth.test.ts +140 -0
- package/src/cognito-auth.ts +16 -1
package/src/cognito-auth.test.ts
CHANGED
|
@@ -565,3 +565,143 @@ describe("refresh expiry hardening", () => {
|
|
|
565
565
|
expect(loadCachedTokens()?.accessToken).toBe("new-access");
|
|
566
566
|
});
|
|
567
567
|
});
|
|
568
|
+
|
|
569
|
+
// ---------------------------------------------------------------------------
|
|
570
|
+
// Machine mint identity binding under EMAIL-ALIAS pools (regression, 2026-07-20)
|
|
571
|
+
//
|
|
572
|
+
// The prod pool aliases email-format usernames: Cognito's canonical username
|
|
573
|
+
// (`cognito:username` / access `username`) is a generated UUID equal to `sub`,
|
|
574
|
+
// NEVER the email-format username the machine creds carry. The 6.14.13
|
|
575
|
+
// predicate bound the ID token to creds via username equality only, so every
|
|
576
|
+
// valid outpost/agent mint was rejected with "Machine token mint returned
|
|
577
|
+
// tokens that do not match the requested machine identity" — a fleet-wide
|
|
578
|
+
// sync outage on boxes. Claim shapes below mirror a real prod outpost token
|
|
579
|
+
// (email claim ABSENT: the machine app client cannot read `email`, so Cognito
|
|
580
|
+
// omits it from the ID token entirely).
|
|
581
|
+
// ---------------------------------------------------------------------------
|
|
582
|
+
|
|
583
|
+
describe("machine mint under email-alias pools (username is a UUID)", () => {
|
|
584
|
+
const MACHINE_CLIENT = "4q3bmth9ad4v3be6booc5td193";
|
|
585
|
+
const OUTPOST_UID = "out_01KXVRESJMP7TD263P7S0YHPSD";
|
|
586
|
+
const COGNITO_INTERNAL_SUB = "f4981438-a0a1-704c-542a-dfac148f2380";
|
|
587
|
+
const CREDS_USERNAME = "out-01kxvresjmp7td263p7s0yhpsd@outposts.hq.computer";
|
|
588
|
+
|
|
589
|
+
function writeOutpostCreds(overrides: Record<string, unknown> = {}) {
|
|
590
|
+
const dir = path.join(tmpHome, ".hq-agent");
|
|
591
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
592
|
+
fs.writeFileSync(
|
|
593
|
+
path.join(dir, "machine-creds.json"),
|
|
594
|
+
JSON.stringify({
|
|
595
|
+
username: CREDS_USERNAME,
|
|
596
|
+
secret: "machine-secret",
|
|
597
|
+
clientId: MACHINE_CLIENT,
|
|
598
|
+
region: "us-east-1",
|
|
599
|
+
entityType: "outpost",
|
|
600
|
+
entityUid: OUTPOST_UID,
|
|
601
|
+
...overrides,
|
|
602
|
+
}),
|
|
603
|
+
);
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
function mintResponse(accessClaims: Record<string, unknown>, idClaims: Record<string, unknown>) {
|
|
607
|
+
return vi.fn(async () =>
|
|
608
|
+
new Response(
|
|
609
|
+
JSON.stringify({
|
|
610
|
+
AuthenticationResult: {
|
|
611
|
+
AccessToken: makeAccessToken(accessClaims),
|
|
612
|
+
IdToken: makeAccessToken(idClaims),
|
|
613
|
+
ExpiresIn: 3600,
|
|
614
|
+
},
|
|
615
|
+
}),
|
|
616
|
+
{ status: 200, headers: { "Content-Type": "application/json" } },
|
|
617
|
+
),
|
|
618
|
+
);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
it("accepts a valid outpost mint whose cognito:username is the internal UUID (alias pool)", async () => {
|
|
622
|
+
const { getValidAccessToken } = await importModule();
|
|
623
|
+
writeOutpostCreds();
|
|
624
|
+
const fetchMock = mintResponse(
|
|
625
|
+
{
|
|
626
|
+
token_use: "access",
|
|
627
|
+
client_id: MACHINE_CLIENT,
|
|
628
|
+
// Alias pool: access username is the internal UUID, not the email-format name.
|
|
629
|
+
username: COGNITO_INTERNAL_SUB,
|
|
630
|
+
sub: COGNITO_INTERNAL_SUB,
|
|
631
|
+
},
|
|
632
|
+
{
|
|
633
|
+
token_use: "id",
|
|
634
|
+
aud: MACHINE_CLIENT,
|
|
635
|
+
"cognito:username": COGNITO_INTERNAL_SUB,
|
|
636
|
+
sub: COGNITO_INTERNAL_SUB,
|
|
637
|
+
// email claim intentionally ABSENT (client cannot read `email`).
|
|
638
|
+
"custom:entityType": "outpost",
|
|
639
|
+
"custom:entityUid": OUTPOST_UID,
|
|
640
|
+
"custom:delegatedSub": "24785418-c071-7077-b7e9-0b1b583cfba1",
|
|
641
|
+
"custom:delegatedEmail": "hassaan@getindigo.ai",
|
|
642
|
+
},
|
|
643
|
+
);
|
|
644
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
645
|
+
|
|
646
|
+
await expect(
|
|
647
|
+
getValidAccessToken(baseConfig, { interactive: false }),
|
|
648
|
+
).resolves.toBeTypeOf("string");
|
|
649
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
it("still rejects a mint whose entityUid does not match the requested creds", async () => {
|
|
653
|
+
const { getValidAccessToken } = await importModule();
|
|
654
|
+
writeOutpostCreds();
|
|
655
|
+
const fetchMock = mintResponse(
|
|
656
|
+
{
|
|
657
|
+
token_use: "access",
|
|
658
|
+
client_id: MACHINE_CLIENT,
|
|
659
|
+
username: COGNITO_INTERNAL_SUB,
|
|
660
|
+
sub: COGNITO_INTERNAL_SUB,
|
|
661
|
+
},
|
|
662
|
+
{
|
|
663
|
+
token_use: "id",
|
|
664
|
+
aud: MACHINE_CLIENT,
|
|
665
|
+
"cognito:username": COGNITO_INTERNAL_SUB,
|
|
666
|
+
sub: COGNITO_INTERNAL_SUB,
|
|
667
|
+
"custom:entityType": "outpost",
|
|
668
|
+
// A DIFFERENT machine's uid — identity binding must refuse it.
|
|
669
|
+
"custom:entityUid": "out_01KX0000000000000000000000",
|
|
670
|
+
"custom:delegatedSub": "24785418-c071-7077-b7e9-0b1b583cfba1",
|
|
671
|
+
"custom:delegatedEmail": "hassaan@getindigo.ai",
|
|
672
|
+
},
|
|
673
|
+
);
|
|
674
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
675
|
+
|
|
676
|
+
await expect(
|
|
677
|
+
getValidAccessToken(baseConfig, { interactive: false }),
|
|
678
|
+
).rejects.toThrow(/do not match the requested machine identity/);
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
it("still rejects legacy creds (no entityUid) when the username binding fails", async () => {
|
|
682
|
+
const { getValidAccessToken } = await importModule();
|
|
683
|
+
// Legacy agent creds: no entityType/entityUid persisted.
|
|
684
|
+
writeOutpostCreds({ entityType: undefined, entityUid: undefined, username: "agt-legacy@agents.getindigo.ai" });
|
|
685
|
+
const fetchMock = mintResponse(
|
|
686
|
+
{
|
|
687
|
+
token_use: "access",
|
|
688
|
+
client_id: MACHINE_CLIENT,
|
|
689
|
+
username: "someone-else",
|
|
690
|
+
sub: "someone-else",
|
|
691
|
+
},
|
|
692
|
+
{
|
|
693
|
+
token_use: "id",
|
|
694
|
+
aud: MACHINE_CLIENT,
|
|
695
|
+
"cognito:username": "someone-else",
|
|
696
|
+
sub: "someone-else",
|
|
697
|
+
"custom:entityType": "agent",
|
|
698
|
+
"custom:entityUid": "agt_01JZ9999999999999999999999",
|
|
699
|
+
},
|
|
700
|
+
);
|
|
701
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
702
|
+
|
|
703
|
+
await expect(
|
|
704
|
+
getValidAccessToken(baseConfig, { interactive: false }),
|
|
705
|
+
).rejects.toThrow(/do not match the requested machine identity/);
|
|
706
|
+
});
|
|
707
|
+
});
|
package/src/cognito-auth.ts
CHANGED
|
@@ -399,8 +399,23 @@ function machineTokenPairMatchesIdentity(
|
|
|
399
399
|
const entityMatchesCreds =
|
|
400
400
|
entityType === expectedEntityType &&
|
|
401
401
|
(creds.entityUid === undefined || entityUid === creds.entityUid);
|
|
402
|
+
// Bind the ID token to the REQUESTED credentials. Username equality only
|
|
403
|
+
// holds in pools without email aliasing; the prod pool aliases email-format
|
|
404
|
+
// usernames, so Cognito's canonical username (`cognito:username`, access
|
|
405
|
+
// `username`, `sub`) is a generated UUID that NEVER equals the email-format
|
|
406
|
+
// creds.username — and the machine app client cannot read `email`, so no
|
|
407
|
+
// email claim rides the ID token either. For creds that persist their
|
|
408
|
+
// entityUid (every ADR-0009 outpost and modern agent), the unique
|
|
409
|
+
// custom:entityUid claim IS the identity binding: exactly one Cognito user
|
|
410
|
+
// carries that attribute value, stamped server-side at provisioning.
|
|
411
|
+
// Regression: 6.14.13 shipped username-only binding and rejected every
|
|
412
|
+
// valid machine mint in the alias pool ("Machine token mint returned tokens
|
|
413
|
+
// that do not match the requested machine identity"), logging out the
|
|
414
|
+
// outpost fleet. Legacy creds without entityUid keep the username binding.
|
|
402
415
|
const idTokenMatchesCreds =
|
|
403
|
-
idUsername === creds.username ||
|
|
416
|
+
idUsername === creds.username ||
|
|
417
|
+
idSub === creds.username ||
|
|
418
|
+
(creds.entityUid !== undefined && entityUid === creds.entityUid);
|
|
404
419
|
const subjectBindings: boolean[] = [];
|
|
405
420
|
if (accessUsername !== null && idUsername !== null) {
|
|
406
421
|
subjectBindings.push(accessUsername === idUsername);
|