@indigoai-us/hq-cloud 6.15.0 → 6.15.2
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-mutation.d.ts +16 -0
- package/dist/bin/sync-mutation.d.ts.map +1 -0
- package/dist/bin/sync-mutation.js +60 -0
- package/dist/bin/sync-mutation.js.map +1 -0
- package/dist/bin/sync-mutation.test.d.ts +2 -0
- package/dist/bin/sync-mutation.test.d.ts.map +1 -0
- package/dist/bin/sync-mutation.test.js +165 -0
- package/dist/bin/sync-mutation.test.js.map +1 -0
- package/dist/bin/sync-runner-company.d.ts +8 -0
- package/dist/bin/sync-runner-company.d.ts.map +1 -1
- package/dist/bin/sync-runner-company.js +16 -0
- package/dist/bin/sync-runner-company.js.map +1 -1
- package/dist/bin/sync-runner-company.test.d.ts +2 -0
- package/dist/bin/sync-runner-company.test.d.ts.map +1 -0
- package/dist/bin/sync-runner-company.test.js +36 -0
- package/dist/bin/sync-runner-company.test.js.map +1 -0
- package/dist/bin/sync-runner-watch-loop.d.ts.map +1 -1
- package/dist/bin/sync-runner-watch-loop.js +98 -8
- package/dist/bin/sync-runner-watch-loop.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.map +1 -1
- package/dist/bin/sync-runner.test.js +109 -0
- package/dist/bin/sync-runner.test.js.map +1 -1
- package/dist/cli/conflict-recovery.test.d.ts +2 -0
- package/dist/cli/conflict-recovery.test.d.ts.map +1 -0
- package/dist/cli/conflict-recovery.test.js +201 -0
- package/dist/cli/conflict-recovery.test.js.map +1 -0
- package/dist/cli/conflict.d.ts +60 -0
- package/dist/cli/conflict.d.ts.map +1 -1
- package/dist/cli/conflict.js +333 -0
- package/dist/cli/conflict.js.map +1 -1
- package/dist/cli/sync.d.ts +35 -0
- package/dist/cli/sync.d.ts.map +1 -1
- package/dist/cli/sync.js +100 -0
- package/dist/cli/sync.js.map +1 -1
- package/dist/cli/sync.test.js +85 -1
- package/dist/cli/sync.test.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/skill-telemetry.d.ts +6 -0
- package/dist/skill-telemetry.d.ts.map +1 -1
- package/dist/skill-telemetry.js +14 -2
- package/dist/skill-telemetry.js.map +1 -1
- package/dist/skill-telemetry.test.js +79 -0
- package/dist/skill-telemetry.test.js.map +1 -1
- package/dist/sync/candidate-uploader.d.ts +88 -0
- package/dist/sync/candidate-uploader.d.ts.map +1 -0
- package/dist/sync/candidate-uploader.js +212 -0
- package/dist/sync/candidate-uploader.js.map +1 -0
- package/dist/sync/candidate-uploader.test.d.ts +2 -0
- package/dist/sync/candidate-uploader.test.d.ts.map +1 -0
- package/dist/sync/candidate-uploader.test.js +132 -0
- package/dist/sync/candidate-uploader.test.js.map +1 -0
- package/dist/sync/delta-client.d.ts +73 -0
- package/dist/sync/delta-client.d.ts.map +1 -0
- package/dist/sync/delta-client.js +201 -0
- package/dist/sync/delta-client.js.map +1 -0
- package/dist/sync/delta-client.test.d.ts +2 -0
- package/dist/sync/delta-client.test.d.ts.map +1 -0
- package/dist/sync/delta-client.test.js +97 -0
- package/dist/sync/delta-client.test.js.map +1 -0
- package/dist/sync/durable-apply.d.ts +76 -0
- package/dist/sync/durable-apply.d.ts.map +1 -0
- package/dist/sync/durable-apply.js +530 -0
- package/dist/sync/durable-apply.js.map +1 -0
- package/dist/sync/durable-apply.test.d.ts +2 -0
- package/dist/sync/durable-apply.test.d.ts.map +1 -0
- package/dist/sync/durable-apply.test.js +180 -0
- package/dist/sync/durable-apply.test.js.map +1 -0
- package/dist/sync/event-sync.d.ts +33 -1
- package/dist/sync/event-sync.d.ts.map +1 -1
- package/dist/sync/event-sync.js +149 -1
- package/dist/sync/event-sync.js.map +1 -1
- package/dist/sync/event-sync.test.js +142 -1
- package/dist/sync/event-sync.test.js.map +1 -1
- package/dist/sync/index.d.ts +2 -0
- package/dist/sync/index.d.ts.map +1 -1
- package/dist/sync/index.js +1 -0
- package/dist/sync/index.js.map +1 -1
- package/dist/sync/multipart-uploader.d.ts +99 -0
- package/dist/sync/multipart-uploader.d.ts.map +1 -0
- package/dist/sync/multipart-uploader.js +447 -0
- package/dist/sync/multipart-uploader.js.map +1 -0
- package/dist/sync/multipart-uploader.test.d.ts +2 -0
- package/dist/sync/multipart-uploader.test.d.ts.map +1 -0
- package/dist/sync/multipart-uploader.test.js +119 -0
- package/dist/sync/multipart-uploader.test.js.map +1 -0
- package/dist/sync/mutation-client.d.ts +85 -0
- package/dist/sync/mutation-client.d.ts.map +1 -0
- package/dist/sync/mutation-client.js +245 -0
- package/dist/sync/mutation-client.js.map +1 -0
- package/dist/sync/mutation-client.test.d.ts +2 -0
- package/dist/sync/mutation-client.test.d.ts.map +1 -0
- package/dist/sync/mutation-client.test.js +51 -0
- package/dist/sync/mutation-client.test.js.map +1 -0
- package/dist/sync/push-receiver.d.ts +45 -0
- package/dist/sync/push-receiver.d.ts.map +1 -1
- package/dist/sync/push-receiver.js +101 -0
- package/dist/sync/push-receiver.js.map +1 -1
- package/dist/sync/push-receiver.test.js +54 -2
- package/dist/sync/push-receiver.test.js.map +1 -1
- package/dist/sync/scope-inventory-client.d.ts +69 -0
- package/dist/sync/scope-inventory-client.d.ts.map +1 -0
- package/dist/sync/scope-inventory-client.js +210 -0
- package/dist/sync/scope-inventory-client.js.map +1 -0
- package/dist/sync/scope-inventory-client.test.d.ts +2 -0
- package/dist/sync/scope-inventory-client.test.d.ts.map +1 -0
- package/dist/sync/scope-inventory-client.test.js +94 -0
- package/dist/sync/scope-inventory-client.test.js.map +1 -0
- package/dist/sync/snapshot-client.d.ts +98 -0
- package/dist/sync/snapshot-client.d.ts.map +1 -0
- package/dist/sync/snapshot-client.js +402 -0
- package/dist/sync/snapshot-client.js.map +1 -0
- package/dist/sync/snapshot-client.test.d.ts +2 -0
- package/dist/sync/snapshot-client.test.d.ts.map +1 -0
- package/dist/sync/snapshot-client.test.js +169 -0
- package/dist/sync/snapshot-client.test.js.map +1 -0
- package/dist/sync/uploader-finalization.d.ts +97 -0
- package/dist/sync/uploader-finalization.d.ts.map +1 -0
- package/dist/sync/uploader-finalization.js +273 -0
- package/dist/sync/uploader-finalization.js.map +1 -0
- package/dist/sync/uploader-finalization.test.d.ts +2 -0
- package/dist/sync/uploader-finalization.test.d.ts.map +1 -0
- package/dist/sync/uploader-finalization.test.js +92 -0
- package/dist/sync/uploader-finalization.test.js.map +1 -0
- package/dist/telemetry.d.ts +11 -1
- package/dist/telemetry.d.ts.map +1 -1
- package/dist/telemetry.js +21 -2
- package/dist/telemetry.js.map +1 -1
- package/dist/telemetry.test.js +80 -0
- package/dist/telemetry.test.js.map +1 -1
- package/package.json +6 -1
- package/.claude/policies/hq-cloud-esm-cannot-spy-fs-builtins.md +0 -30
- package/.claude/policies/hq-cloud-strip-types-no-parameter-properties.md +0 -22
- package/.github/workflows/ci.yml +0 -84
- package/.github/workflows/publish.yml +0 -56
- package/.github/workflows/unreleased-commits-nag.yml +0 -256
- package/eslint.config.js +0 -67
- package/pnpm-workspace.yaml +0 -2
- package/scripts/presign-transport-e2e.mjs +0 -250
- package/scripts/vault-rebaseline.sh +0 -323
- package/scripts/vault-rescue.sh +0 -332
- package/src/active-company.test.ts +0 -188
- package/src/active-company.ts +0 -168
- package/src/agent-codex-instructions.test.ts +0 -332
- package/src/agent-codex-instructions.ts +0 -309
- package/src/auth.ts +0 -146
- package/src/backup-prune.test.ts +0 -98
- package/src/backup-prune.ts +0 -182
- package/src/bin/backup-prune-runner.ts +0 -33
- package/src/bin/rescue-runner.ts +0 -25
- package/src/bin/sync-runner-company.ts +0 -695
- package/src/bin/sync-runner-events.test.ts +0 -143
- package/src/bin/sync-runner-events.ts +0 -55
- package/src/bin/sync-runner-planning.test.ts +0 -311
- package/src/bin/sync-runner-planning.ts +0 -258
- package/src/bin/sync-runner-rollup.test.ts +0 -37
- package/src/bin/sync-runner-rollup.ts +0 -97
- package/src/bin/sync-runner-telemetry.ts +0 -15
- package/src/bin/sync-runner-watch-loop.ts +0 -1235
- package/src/bin/sync-runner-watch-routes.test.ts +0 -71
- package/src/bin/sync-runner-watch-routes.ts +0 -184
- package/src/bin/sync-runner.test.ts +0 -8767
- package/src/bin/sync-runner.ts +0 -2190
- package/src/cli/accept.ts +0 -124
- package/src/cli/conflict.ts +0 -119
- package/src/cli/doctor.test.ts +0 -581
- package/src/cli/doctor.ts +0 -642
- package/src/cli/index.ts +0 -49
- package/src/cli/invite.test.ts +0 -250
- package/src/cli/invite.ts +0 -214
- package/src/cli/promote.ts +0 -157
- package/src/cli/reindex-knowledge.test.ts +0 -307
- package/src/cli/reindex-knowledge.ts +0 -450
- package/src/cli/reindex.test.ts +0 -957
- package/src/cli/reindex.ts +0 -979
- package/src/cli/rescue-classify-ordering.test.ts +0 -548
- package/src/cli/rescue-clone-diagnostics.test.ts +0 -120
- package/src/cli/rescue-core.ts +0 -3011
- package/src/cli/rescue-drift-reconcile.test.ts +0 -179
- package/src/cli/rescue-drop-dir-symlink.test.ts +0 -224
- package/src/cli/rescue-exec-bit-preserve.test.ts +0 -187
- package/src/cli/rescue-hq-root-guard.test.ts +0 -232
- package/src/cli/rescue-journal-reconcile.test.ts +0 -215
- package/src/cli/rescue-mtime-preserve.test.ts +0 -203
- package/src/cli/rescue-settings-reconcile.test.ts +0 -637
- package/src/cli/rescue-snapshot.test.ts +0 -57
- package/src/cli/rescue-snapshot.ts +0 -51
- package/src/cli/rescue.reindex.test.ts +0 -63
- package/src/cli/rescue.test.ts +0 -131
- package/src/cli/rescue.ts +0 -182
- package/src/cli/share.test.ts +0 -7843
- package/src/cli/share.ts +0 -3663
- package/src/cli/sync-scope.test.ts +0 -652
- package/src/cli/sync.test.ts +0 -5207
- package/src/cli/sync.ts +0 -3470
- package/src/cli/tombstones.ts +0 -106
- package/src/cli/watch-event-push-conflict.test.ts +0 -234
- package/src/client-info.test.ts +0 -214
- package/src/client-info.ts +0 -121
- package/src/cognito-auth.test.ts +0 -712
- package/src/cognito-auth.ts +0 -1422
- package/src/company-resolver.test.ts +0 -618
- package/src/company-resolver.ts +0 -521
- package/src/context.test.ts +0 -583
- package/src/context.ts +0 -378
- package/src/daemon-worker.ts +0 -26
- package/src/daemon.ts +0 -99
- package/src/entity-resolver.test.ts +0 -315
- package/src/entity-resolver.ts +0 -180
- package/src/ignore.test.ts +0 -466
- package/src/ignore.ts +0 -469
- package/src/index.ts +0 -439
- package/src/journal.test.ts +0 -968
- package/src/journal.ts +0 -765
- package/src/lib/cloud-authoritative.test.ts +0 -45
- package/src/lib/cloud-authoritative.ts +0 -59
- package/src/lib/conflict-file.ts +0 -86
- package/src/lib/conflict-index.ts +0 -289
- package/src/lib/conflict.test.ts +0 -348
- package/src/lib/describe-error.test.ts +0 -100
- package/src/lib/describe-error.ts +0 -58
- package/src/lib/exit-codes.ts +0 -24
- package/src/lib/machine-id.test.ts +0 -231
- package/src/lib/machine-id.ts +0 -175
- package/src/lib/net-errors.test.ts +0 -65
- package/src/lib/net-errors.ts +0 -86
- package/src/lib/readlink-safe.test.ts +0 -43
- package/src/lib/readlink-safe.ts +0 -29
- package/src/local-path-codec.test.ts +0 -138
- package/src/local-path-codec.ts +0 -161
- package/src/machine-auth.test.ts +0 -1323
- package/src/manifest-reconcile.test.ts +0 -1123
- package/src/manifest-reconcile.ts +0 -518
- package/src/object-io.test.ts +0 -1221
- package/src/object-io.ts +0 -1306
- package/src/operation-lock.test.ts +0 -484
- package/src/operation-lock.ts +0 -680
- package/src/outcome-telemetry.test.ts +0 -498
- package/src/outcome-telemetry.ts +0 -639
- package/src/personal-vault-exclusions.test.ts +0 -308
- package/src/personal-vault-exclusions.ts +0 -354
- package/src/personal-vault.test.ts +0 -756
- package/src/personal-vault.ts +0 -496
- package/src/prefix-coalesce.test.ts +0 -240
- package/src/prefix-coalesce.ts +0 -273
- package/src/public-surface.test.ts +0 -117
- package/src/qmd-reindex.test.ts +0 -877
- package/src/qmd-reindex.ts +0 -842
- package/src/read-only-state-dir.test.ts +0 -188
- package/src/remote-pull.test.ts +0 -1130
- package/src/remote-pull.ts +0 -618
- package/src/s3.symlink-materialize.test.ts +0 -492
- package/src/s3.test.ts +0 -1789
- package/src/s3.ts +0 -1532
- package/src/schemas/signal-types.test.ts +0 -82
- package/src/schemas/signal-types.ts +0 -38
- package/src/schemas/source-channels.test.ts +0 -82
- package/src/schemas/source-channels.ts +0 -53
- package/src/scope-shrink.test.ts +0 -633
- package/src/scope-shrink.ts +0 -481
- package/src/signals/get.test.ts +0 -310
- package/src/signals/get.ts +0 -75
- package/src/signals/internals.ts +0 -195
- package/src/signals/list.test.ts +0 -420
- package/src/signals/list.ts +0 -79
- package/src/signals/parse.ts +0 -8
- package/src/signals/types.ts +0 -91
- package/src/skill-telemetry.test.ts +0 -1825
- package/src/skill-telemetry.ts +0 -1439
- package/src/sources/get.test.ts +0 -293
- package/src/sources/get.ts +0 -66
- package/src/sources/internals.ts +0 -198
- package/src/sources/list.test.ts +0 -402
- package/src/sources/list.ts +0 -84
- package/src/sources/parse.ts +0 -43
- package/src/sources/types.ts +0 -84
- package/src/sync/event-sync.test.ts +0 -594
- package/src/sync/event-sync.ts +0 -545
- package/src/sync/feature-flags.test.ts +0 -378
- package/src/sync/feature-flags.ts +0 -62
- package/src/sync/index.ts +0 -76
- package/src/sync/lease-client.test.ts +0 -128
- package/src/sync/lease-client.ts +0 -207
- package/src/sync/logger.test.ts +0 -242
- package/src/sync/logger.ts +0 -79
- package/src/sync/metrics.test.ts +0 -462
- package/src/sync/metrics.ts +0 -213
- package/src/sync/pull-scope.ts +0 -265
- package/src/sync/push-event.test.ts +0 -266
- package/src/sync/push-event.ts +0 -224
- package/src/sync/push-receiver.test.ts +0 -566
- package/src/sync/push-receiver.ts +0 -1048
- package/src/sync/push-transport.ts +0 -231
- package/src/sync/realtime-rollout.test.ts +0 -86
- package/src/sync/realtime-rollout.ts +0 -262
- package/src/sync/state-store.test.ts +0 -194
- package/src/sync/state-store.ts +0 -727
- package/src/sync-core.ts +0 -58
- package/src/sync-progress.test.ts +0 -94
- package/src/sync-progress.ts +0 -140
- package/src/telemetry-events.test.ts +0 -88
- package/src/telemetry-events.ts +0 -205
- package/src/telemetry.test.ts +0 -1280
- package/src/telemetry.ts +0 -1109
- package/src/types.ts +0 -314
- package/src/vault-client.test.ts +0 -1380
- package/src/vault-client.ts +0 -1694
- package/src/version.ts +0 -24
- package/src/watch-roots.test.ts +0 -278
- package/src/watch-roots.ts +0 -162
- package/src/watcher-event-gate.test.ts +0 -212
- package/src/watcher.test.ts +0 -1079
- package/src/watcher.ts +0 -1741
- package/test/e2e/sync/cross-tenant-isolation.test.ts +0 -630
- package/test/e2e/sync/skill-telemetry-oversized-transcript.test.ts +0 -124
- package/test/e2e/sync/transient-company-leg.test.ts +0 -384
- package/test/e2e/sync/windows-unreadable-link-leg.test.ts +0 -191
- package/test/e2e/watcher-real-chokidar.test.ts +0 -165
- package/test/e2e/watcher-recursive-backend.test.ts +0 -181
- package/test/e2e/watcher-scoped-coverage.test.ts +0 -381
- package/test/invite-flow.integration.test.ts +0 -244
- package/test/joiner-manifest-reconcile.integration.test.ts +0 -322
- package/test/share-sync.integration.test.ts +0 -213
- package/tsconfig.json +0 -19
- package/vitest.config.ts +0 -22
package/src/cognito-auth.test.ts
DELETED
|
@@ -1,712 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Unit tests for cognito-auth.ts — focus on the `expiresAt` shape contract.
|
|
3
|
-
*
|
|
4
|
-
* Canonical on-disk shape is epoch milliseconds (number). The reader also
|
|
5
|
-
* tolerates ISO 8601 strings for backward compatibility with pre-migration
|
|
6
|
-
* token files, and fails safe on anything unparseable.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import * as fs from "fs";
|
|
10
|
-
import * as os from "os";
|
|
11
|
-
import * as path from "path";
|
|
12
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
13
|
-
|
|
14
|
-
// Sandbox HOME *before* importing the module — it reads os.homedir() at load
|
|
15
|
-
// time to compute the cache file path.
|
|
16
|
-
let originalHome: string | undefined;
|
|
17
|
-
let originalUserProfile: string | undefined;
|
|
18
|
-
let tmpHome: string;
|
|
19
|
-
|
|
20
|
-
beforeEach(() => {
|
|
21
|
-
originalHome = process.env.HOME;
|
|
22
|
-
originalUserProfile = process.env.USERPROFILE;
|
|
23
|
-
tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), "hq-cognito-auth-test-"));
|
|
24
|
-
process.env.HOME = tmpHome;
|
|
25
|
-
process.env.USERPROFILE = tmpHome;
|
|
26
|
-
vi.resetModules();
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
afterEach(() => {
|
|
30
|
-
if (originalHome === undefined) delete process.env.HOME;
|
|
31
|
-
else process.env.HOME = originalHome;
|
|
32
|
-
if (originalUserProfile === undefined) delete process.env.USERPROFILE;
|
|
33
|
-
else process.env.USERPROFILE = originalUserProfile;
|
|
34
|
-
fs.rmSync(tmpHome, { recursive: true, force: true });
|
|
35
|
-
vi.unstubAllGlobals();
|
|
36
|
-
vi.restoreAllMocks();
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
async function importModule() {
|
|
40
|
-
return await import("./cognito-auth.js");
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const baseTokens = {
|
|
44
|
-
accessToken: "access",
|
|
45
|
-
idToken: "id",
|
|
46
|
-
refreshToken: "refresh",
|
|
47
|
-
tokenType: "Bearer" as const,
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
// ---------------------------------------------------------------------------
|
|
51
|
-
// Reader: isExpiring accepts both shapes and fails safe
|
|
52
|
-
// ---------------------------------------------------------------------------
|
|
53
|
-
|
|
54
|
-
describe("isExpiring — expiresAt shape tolerance", () => {
|
|
55
|
-
it("returns false for ISO string far in the future", async () => {
|
|
56
|
-
const { isExpiring } = await importModule();
|
|
57
|
-
const future = new Date(Date.now() + 60 * 60 * 1000).toISOString();
|
|
58
|
-
expect(isExpiring({ ...baseTokens, expiresAt: future })).toBe(false);
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it("returns true for ISO string within the buffer window", async () => {
|
|
62
|
-
const { isExpiring } = await importModule();
|
|
63
|
-
const soon = new Date(Date.now() + 10 * 1000).toISOString();
|
|
64
|
-
expect(isExpiring({ ...baseTokens, expiresAt: soon }, 60)).toBe(true);
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
it("returns false for raw number (ms) far in the future", async () => {
|
|
68
|
-
const { isExpiring } = await importModule();
|
|
69
|
-
const future = Date.now() + 60 * 60 * 1000;
|
|
70
|
-
// Cast because the type says string; the point is runtime tolerance.
|
|
71
|
-
expect(
|
|
72
|
-
isExpiring({ ...baseTokens, expiresAt: future as unknown as string }),
|
|
73
|
-
).toBe(false);
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
it("returns true for raw number (ms) within the buffer window", async () => {
|
|
77
|
-
const { isExpiring } = await importModule();
|
|
78
|
-
const soon = Date.now() + 10 * 1000;
|
|
79
|
-
expect(
|
|
80
|
-
isExpiring(
|
|
81
|
-
{ ...baseTokens, expiresAt: soon as unknown as string },
|
|
82
|
-
60,
|
|
83
|
-
),
|
|
84
|
-
).toBe(true);
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
it("fails safe (returns true) for malformed expiresAt", async () => {
|
|
88
|
-
const { isExpiring } = await importModule();
|
|
89
|
-
expect(
|
|
90
|
-
isExpiring({ ...baseTokens, expiresAt: "not a date" }),
|
|
91
|
-
).toBe(true);
|
|
92
|
-
expect(
|
|
93
|
-
isExpiring({
|
|
94
|
-
...baseTokens,
|
|
95
|
-
expiresAt: undefined as unknown as string,
|
|
96
|
-
}),
|
|
97
|
-
).toBe(true);
|
|
98
|
-
expect(
|
|
99
|
-
isExpiring({
|
|
100
|
-
...baseTokens,
|
|
101
|
-
expiresAt: Number.NaN as unknown as string,
|
|
102
|
-
}),
|
|
103
|
-
).toBe(true);
|
|
104
|
-
});
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
// ---------------------------------------------------------------------------
|
|
108
|
-
// Stale-pool detection — decodeAccessTokenClientId + getValidAccessToken
|
|
109
|
-
// self-evicts cached tokens minted by a different App Client (e.g. dev pool
|
|
110
|
-
// tokens left over from before the 2026-04-25 cutover).
|
|
111
|
-
// ---------------------------------------------------------------------------
|
|
112
|
-
|
|
113
|
-
/** Build a minimal unsigned JWT carrying the given claims. Cognito's real */
|
|
114
|
-
/** tokens are RS256-signed; we don't verify here so the signature can be */
|
|
115
|
-
/** anything — only the base64url-encoded payload matters. */
|
|
116
|
-
function makeAccessToken(claims: Record<string, unknown>): string {
|
|
117
|
-
const header = Buffer.from(JSON.stringify({ alg: "RS256", typ: "JWT" }))
|
|
118
|
-
.toString("base64")
|
|
119
|
-
.replace(/=+$/, "");
|
|
120
|
-
const payload = Buffer.from(JSON.stringify(claims))
|
|
121
|
-
.toString("base64")
|
|
122
|
-
.replace(/=+$/, "");
|
|
123
|
-
return `${header}.${payload}.signature`;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
const DEV_CLIENT = "4mmujmjq3srakdueg656b9m0mp";
|
|
127
|
-
const PROD_CLIENT = "7acei2c8v870enheptb1j5foln";
|
|
128
|
-
|
|
129
|
-
const baseConfig = {
|
|
130
|
-
region: "us-east-1",
|
|
131
|
-
userPoolDomain: "vault-indigo-hq-prod",
|
|
132
|
-
clientId: PROD_CLIENT,
|
|
133
|
-
};
|
|
134
|
-
|
|
135
|
-
describe("decodeAccessTokenClientId", () => {
|
|
136
|
-
it("returns the client_id claim from a well-formed JWT", async () => {
|
|
137
|
-
const { decodeAccessTokenClientId } = await importModule();
|
|
138
|
-
const token = makeAccessToken({ client_id: DEV_CLIENT, sub: "abc" });
|
|
139
|
-
expect(decodeAccessTokenClientId(token)).toBe(DEV_CLIENT);
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
it("returns null when client_id is absent", async () => {
|
|
143
|
-
const { decodeAccessTokenClientId } = await importModule();
|
|
144
|
-
const token = makeAccessToken({ sub: "abc" });
|
|
145
|
-
expect(decodeAccessTokenClientId(token)).toBeNull();
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
it("returns null when the token has fewer than two segments", async () => {
|
|
149
|
-
const { decodeAccessTokenClientId } = await importModule();
|
|
150
|
-
expect(decodeAccessTokenClientId("not-a-jwt")).toBeNull();
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
it("returns null when the payload isn't valid JSON", async () => {
|
|
154
|
-
const { decodeAccessTokenClientId } = await importModule();
|
|
155
|
-
expect(decodeAccessTokenClientId("aaa.bbb.ccc")).toBeNull();
|
|
156
|
-
});
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
describe("getValidAccessToken stale-pool detection", () => {
|
|
160
|
-
it("evicts a cached token whose client_id mismatches the current config", async () => {
|
|
161
|
-
const { saveCachedTokens, loadCachedTokens, getValidAccessToken } =
|
|
162
|
-
await importModule();
|
|
163
|
-
const devToken = makeAccessToken({ client_id: DEV_CLIENT, sub: "abc" });
|
|
164
|
-
saveCachedTokens({
|
|
165
|
-
...baseTokens,
|
|
166
|
-
accessToken: devToken,
|
|
167
|
-
expiresAt: Date.now() + 60 * 60 * 1000,
|
|
168
|
-
});
|
|
169
|
-
expect(loadCachedTokens()).not.toBeNull();
|
|
170
|
-
|
|
171
|
-
await expect(
|
|
172
|
-
getValidAccessToken(baseConfig, { interactive: false }),
|
|
173
|
-
).rejects.toThrow(/No valid HQ session/);
|
|
174
|
-
|
|
175
|
-
expect(loadCachedTokens()).toBeNull();
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
it("keeps a cached token whose client_id matches", async () => {
|
|
179
|
-
const { saveCachedTokens, getValidAccessToken } = await importModule();
|
|
180
|
-
const prodToken = makeAccessToken({ client_id: PROD_CLIENT, sub: "abc" });
|
|
181
|
-
saveCachedTokens({
|
|
182
|
-
...baseTokens,
|
|
183
|
-
accessToken: prodToken,
|
|
184
|
-
expiresAt: Date.now() + 60 * 60 * 1000,
|
|
185
|
-
});
|
|
186
|
-
const access = await getValidAccessToken(baseConfig, {
|
|
187
|
-
interactive: false,
|
|
188
|
-
});
|
|
189
|
-
expect(access).toBe(prodToken);
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
it("keeps a cached token when client_id can't be decoded (back-compat)", async () => {
|
|
193
|
-
const { saveCachedTokens, getValidAccessToken } = await importModule();
|
|
194
|
-
saveCachedTokens({
|
|
195
|
-
...baseTokens,
|
|
196
|
-
accessToken: "opaque-non-jwt",
|
|
197
|
-
expiresAt: Date.now() + 60 * 60 * 1000,
|
|
198
|
-
});
|
|
199
|
-
const access = await getValidAccessToken(baseConfig, {
|
|
200
|
-
interactive: false,
|
|
201
|
-
});
|
|
202
|
-
expect(access).toBe("opaque-non-jwt");
|
|
203
|
-
});
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
// ---------------------------------------------------------------------------
|
|
207
|
-
// Machine identity cache isolation — machine mode must not reuse human tokens
|
|
208
|
-
// from the shared Cognito cache even when the app client matches.
|
|
209
|
-
// ---------------------------------------------------------------------------
|
|
210
|
-
|
|
211
|
-
describe("machine identity cache isolation", () => {
|
|
212
|
-
it("F06: machine mode re-mints instead of reusing a cached human token", async () => {
|
|
213
|
-
const { saveCachedTokens, getValidAccessToken } = await importModule();
|
|
214
|
-
|
|
215
|
-
const cachedHumanAccessToken = makeAccessToken({
|
|
216
|
-
token_use: "access",
|
|
217
|
-
client_id: PROD_CLIENT,
|
|
218
|
-
username: "human@example.com",
|
|
219
|
-
sub: "human-sub",
|
|
220
|
-
});
|
|
221
|
-
const cachedHumanIdToken = makeAccessToken({
|
|
222
|
-
token_use: "id",
|
|
223
|
-
aud: PROD_CLIENT,
|
|
224
|
-
email: "human@example.com",
|
|
225
|
-
"cognito:username": "human@example.com",
|
|
226
|
-
sub: "human-sub",
|
|
227
|
-
});
|
|
228
|
-
saveCachedTokens({
|
|
229
|
-
...baseTokens,
|
|
230
|
-
accessToken: cachedHumanAccessToken,
|
|
231
|
-
idToken: cachedHumanIdToken,
|
|
232
|
-
expiresAt: Date.now() + 60 * 60 * 1000,
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
const machineUid = "agt_01JZ0000000000000000000000";
|
|
236
|
-
const machineCredsDir = path.join(tmpHome, ".hq-agent");
|
|
237
|
-
fs.mkdirSync(machineCredsDir, { recursive: true });
|
|
238
|
-
fs.writeFileSync(
|
|
239
|
-
path.join(machineCredsDir, "machine-creds.json"),
|
|
240
|
-
JSON.stringify({
|
|
241
|
-
username: "agt-01jz0000000000000000000000@agents.getindigo.ai",
|
|
242
|
-
secret: "machine-secret",
|
|
243
|
-
clientId: PROD_CLIENT,
|
|
244
|
-
region: "us-east-1",
|
|
245
|
-
}),
|
|
246
|
-
);
|
|
247
|
-
|
|
248
|
-
const mintedMachineAccessToken = makeAccessToken({
|
|
249
|
-
token_use: "access",
|
|
250
|
-
client_id: PROD_CLIENT,
|
|
251
|
-
username: "agt-01jz0000000000000000000000@agents.getindigo.ai",
|
|
252
|
-
sub: machineUid,
|
|
253
|
-
});
|
|
254
|
-
const mintedMachineIdToken = makeAccessToken({
|
|
255
|
-
token_use: "id",
|
|
256
|
-
aud: PROD_CLIENT,
|
|
257
|
-
"custom:entityType": "agent",
|
|
258
|
-
"custom:entityUid": machineUid,
|
|
259
|
-
"cognito:username": "agt-01jz0000000000000000000000@agents.getindigo.ai",
|
|
260
|
-
sub: machineUid,
|
|
261
|
-
});
|
|
262
|
-
const fetchMock = vi.fn(async () =>
|
|
263
|
-
new Response(
|
|
264
|
-
JSON.stringify({
|
|
265
|
-
AuthenticationResult: {
|
|
266
|
-
AccessToken: mintedMachineAccessToken,
|
|
267
|
-
IdToken: mintedMachineIdToken,
|
|
268
|
-
ExpiresIn: 3600,
|
|
269
|
-
},
|
|
270
|
-
}),
|
|
271
|
-
{ status: 200, headers: { "Content-Type": "application/json" } },
|
|
272
|
-
),
|
|
273
|
-
);
|
|
274
|
-
vi.stubGlobal("fetch", fetchMock);
|
|
275
|
-
|
|
276
|
-
await expect(
|
|
277
|
-
getValidAccessToken(baseConfig, { interactive: false }),
|
|
278
|
-
).resolves.toBe(mintedMachineIdToken);
|
|
279
|
-
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
280
|
-
});
|
|
281
|
-
});
|
|
282
|
-
|
|
283
|
-
// ---------------------------------------------------------------------------
|
|
284
|
-
// Round-trip: writers emit epoch-ms, readers read epoch-ms
|
|
285
|
-
// ---------------------------------------------------------------------------
|
|
286
|
-
|
|
287
|
-
describe("expiresAt shape round-trip", () => {
|
|
288
|
-
it("saveCachedTokens + loadCachedTokens preserves epoch-ms number shape", async () => {
|
|
289
|
-
const { saveCachedTokens, loadCachedTokens } = await importModule();
|
|
290
|
-
const epochMs = Date.now() + 3600 * 1000;
|
|
291
|
-
saveCachedTokens({ ...baseTokens, expiresAt: epochMs });
|
|
292
|
-
const loaded = loadCachedTokens();
|
|
293
|
-
expect(loaded).not.toBeNull();
|
|
294
|
-
expect(typeof loaded?.expiresAt).toBe("number");
|
|
295
|
-
expect(loaded?.expiresAt).toBe(epochMs);
|
|
296
|
-
});
|
|
297
|
-
|
|
298
|
-
it("saveCachedTokens + loadCachedTokens tolerates legacy ISO string", async () => {
|
|
299
|
-
const { saveCachedTokens, loadCachedTokens } = await importModule();
|
|
300
|
-
const iso = new Date(Date.now() + 3600 * 1000).toISOString();
|
|
301
|
-
saveCachedTokens({ ...baseTokens, expiresAt: iso });
|
|
302
|
-
const loaded = loadCachedTokens();
|
|
303
|
-
expect(loaded).not.toBeNull();
|
|
304
|
-
expect(typeof loaded?.expiresAt).toBe("string");
|
|
305
|
-
expect(loaded?.expiresAt).toBe(iso);
|
|
306
|
-
});
|
|
307
|
-
|
|
308
|
-
it("refreshTokens writes epoch milliseconds to cache", async () => {
|
|
309
|
-
vi.stubGlobal(
|
|
310
|
-
"fetch",
|
|
311
|
-
vi.fn(async () =>
|
|
312
|
-
new Response(
|
|
313
|
-
JSON.stringify({
|
|
314
|
-
access_token: "new-access",
|
|
315
|
-
id_token: "new-id",
|
|
316
|
-
refresh_token: "new-refresh",
|
|
317
|
-
expires_in: 3600,
|
|
318
|
-
token_type: "Bearer",
|
|
319
|
-
}),
|
|
320
|
-
{ status: 200, headers: { "Content-Type": "application/json" } },
|
|
321
|
-
),
|
|
322
|
-
),
|
|
323
|
-
);
|
|
324
|
-
|
|
325
|
-
const { refreshTokens, loadCachedTokens } = await importModule();
|
|
326
|
-
const before = Date.now();
|
|
327
|
-
const result = await refreshTokens(
|
|
328
|
-
{
|
|
329
|
-
region: "us-east-1",
|
|
330
|
-
userPoolDomain: "vault-indigo-hq-dev",
|
|
331
|
-
clientId: "test-client",
|
|
332
|
-
},
|
|
333
|
-
"prior-refresh-token",
|
|
334
|
-
);
|
|
335
|
-
const after = Date.now();
|
|
336
|
-
|
|
337
|
-
expect(typeof result.expiresAt).toBe("number");
|
|
338
|
-
expect(result.expiresAt).toBeGreaterThanOrEqual(before + 3600 * 1000);
|
|
339
|
-
expect(result.expiresAt).toBeLessThanOrEqual(after + 3600 * 1000);
|
|
340
|
-
|
|
341
|
-
const onDisk = loadCachedTokens();
|
|
342
|
-
expect(onDisk?.expiresAt).toBe(result.expiresAt);
|
|
343
|
-
expect(typeof onDisk?.expiresAt).toBe("number");
|
|
344
|
-
});
|
|
345
|
-
});
|
|
346
|
-
|
|
347
|
-
// ---------------------------------------------------------------------------
|
|
348
|
-
// Concurrency-safe token refresh (cross-process lock)
|
|
349
|
-
//
|
|
350
|
-
// Boxes run several hq processes (`hq auth refresh` on a timer, the sync runner,
|
|
351
|
-
// replica-sync) that refresh the SAME cached session at once. Without
|
|
352
|
-
// coordination they race on the shared cache; and if the app client issues
|
|
353
|
-
// single-use (rotated) tokens, concurrent refreshers invalidate each other —
|
|
354
|
-
// the first consumes the token, the second gets `invalid_grant`. These pin the
|
|
355
|
-
// fix: a cross-process lock + a re-read of the freshest token inside the lock,
|
|
356
|
-
// so refreshes serialize and a consumed token is never reused. (Rotation does
|
|
357
|
-
// NOT reset the 30-day lifetime — see ADR-0009; durable never-log-out is per-box
|
|
358
|
-
// machine identity, which reuses this same lock for its mint sequence.)
|
|
359
|
-
// ---------------------------------------------------------------------------
|
|
360
|
-
describe("refreshTokens — concurrency-safe refresh (cross-process lock)", () => {
|
|
361
|
-
const config = {
|
|
362
|
-
region: "us-east-1",
|
|
363
|
-
userPoolDomain: "vault-indigo-hq-dev",
|
|
364
|
-
clientId: "test-client",
|
|
365
|
-
};
|
|
366
|
-
|
|
367
|
-
// A minimal unsigned JWT carrying a client_id claim, so the cache's
|
|
368
|
-
// client-ownership check (decodeAccessTokenClientId) resolves in tests.
|
|
369
|
-
const jwtFor = (clientId: string, extra: Record<string, unknown> = {}) =>
|
|
370
|
-
"eyJhbGciOiJub25lIn0." +
|
|
371
|
-
Buffer.from(JSON.stringify({ client_id: clientId, ...extra })).toString(
|
|
372
|
-
"base64url",
|
|
373
|
-
) +
|
|
374
|
-
".sig";
|
|
375
|
-
|
|
376
|
-
// Models Cognito refresh-token ROTATION with single-use enforcement: a valid
|
|
377
|
-
// refresh mints a new token and consumes the old one; reusing a consumed
|
|
378
|
-
// token returns 400 invalid_grant (exactly what took the outposts offline).
|
|
379
|
-
function rotatingCognitoFetch(initialToken: string) {
|
|
380
|
-
let current = initialToken;
|
|
381
|
-
const consumed = new Set<string>();
|
|
382
|
-
let counter = 0;
|
|
383
|
-
return vi.fn(async (_url: string, init?: { body?: string }) => {
|
|
384
|
-
const sent = new URLSearchParams(init?.body ?? "").get("refresh_token") ?? "";
|
|
385
|
-
if (sent !== current || consumed.has(sent)) {
|
|
386
|
-
return new Response(JSON.stringify({ error: "invalid_grant" }), {
|
|
387
|
-
status: 400,
|
|
388
|
-
headers: { "Content-Type": "application/json" },
|
|
389
|
-
});
|
|
390
|
-
}
|
|
391
|
-
consumed.add(sent);
|
|
392
|
-
counter += 1;
|
|
393
|
-
current = `refresh-${counter}`;
|
|
394
|
-
return new Response(
|
|
395
|
-
JSON.stringify({
|
|
396
|
-
access_token: jwtFor("test-client", { n: counter }),
|
|
397
|
-
id_token: jwtFor("test-client", { n: counter }),
|
|
398
|
-
refresh_token: current,
|
|
399
|
-
expires_in: 3600,
|
|
400
|
-
token_type: "Bearer",
|
|
401
|
-
}),
|
|
402
|
-
{ status: 200, headers: { "Content-Type": "application/json" } },
|
|
403
|
-
);
|
|
404
|
-
});
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
it("persists the rotated refresh token when Cognito returns one", async () => {
|
|
408
|
-
vi.stubGlobal("fetch", rotatingCognitoFetch("R0"));
|
|
409
|
-
const { refreshTokens, loadCachedTokens } = await importModule();
|
|
410
|
-
const result = await refreshTokens(config, "R0");
|
|
411
|
-
expect(result.refreshToken).toBe("refresh-1");
|
|
412
|
-
expect(loadCachedTokens()?.refreshToken).toBe("refresh-1");
|
|
413
|
-
});
|
|
414
|
-
|
|
415
|
-
it("serializes concurrent refreshers: ONE network refresh, neither gets invalid_grant", async () => {
|
|
416
|
-
const fetchMock = rotatingCognitoFetch("R0");
|
|
417
|
-
vi.stubGlobal("fetch", fetchMock);
|
|
418
|
-
const { refreshTokens, saveCachedTokens, loadCachedTokens } =
|
|
419
|
-
await importModule();
|
|
420
|
-
// Seed an EXPIRED session (for THIS client) so both callers attempt refresh.
|
|
421
|
-
saveCachedTokens({
|
|
422
|
-
...baseTokens,
|
|
423
|
-
accessToken: jwtFor("test-client"),
|
|
424
|
-
refreshToken: "R0",
|
|
425
|
-
expiresAt: Date.now() - 1000,
|
|
426
|
-
});
|
|
427
|
-
|
|
428
|
-
// Two processes refresh the same expiring session at once. Without the lock
|
|
429
|
-
// both send "R0"; the second would hit the mock's invalid_grant and throw.
|
|
430
|
-
const [a, b] = await Promise.all([
|
|
431
|
-
refreshTokens(config, "R0"),
|
|
432
|
-
refreshTokens(config, "R0"),
|
|
433
|
-
]);
|
|
434
|
-
|
|
435
|
-
// Neither threw; the network was hit exactly once (the waiter reused the
|
|
436
|
-
// winner's fresh token under the lock); both hold that same token.
|
|
437
|
-
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
438
|
-
expect(a.refreshToken).toBe("refresh-1");
|
|
439
|
-
expect(b.refreshToken).toBe("refresh-1");
|
|
440
|
-
expect(a.accessToken).toBe(b.accessToken);
|
|
441
|
-
expect(loadCachedTokens()?.refreshToken).toBe("refresh-1");
|
|
442
|
-
});
|
|
443
|
-
|
|
444
|
-
it("reclaims a lock whose holder process has died (no deadlock)", async () => {
|
|
445
|
-
vi.stubGlobal("fetch", rotatingCognitoFetch("R0"));
|
|
446
|
-
const { refreshTokens } = await importModule();
|
|
447
|
-
// A crashed refresher left its lock behind, tagged with a pid that is not
|
|
448
|
-
// running — reclaimed by pid-liveness, not by an mtime heuristic.
|
|
449
|
-
const hqDir = path.join(tmpHome, ".hq");
|
|
450
|
-
fs.mkdirSync(hqDir, { recursive: true });
|
|
451
|
-
const lockPath = path.join(hqDir, "cognito-tokens.json.lock");
|
|
452
|
-
fs.writeFileSync(lockPath, "2147483646");
|
|
453
|
-
|
|
454
|
-
const result = await refreshTokens(config, "R0");
|
|
455
|
-
expect(result.refreshToken).toBe("refresh-1");
|
|
456
|
-
// Our own lock is released after the refresh completes (no leak).
|
|
457
|
-
expect(fs.existsSync(lockPath)).toBe(false);
|
|
458
|
-
});
|
|
459
|
-
|
|
460
|
-
it("does NOT reuse a cached session minted by a different app client", async () => {
|
|
461
|
-
const fetchMock = rotatingCognitoFetch("R-B");
|
|
462
|
-
vi.stubGlobal("fetch", fetchMock);
|
|
463
|
-
const { refreshTokens, saveCachedTokens } = await importModule();
|
|
464
|
-
// Cache holds a FRESH token, but for a DIFFERENT client.
|
|
465
|
-
saveCachedTokens({
|
|
466
|
-
...baseTokens,
|
|
467
|
-
accessToken: jwtFor("other-client"),
|
|
468
|
-
refreshToken: "R-other",
|
|
469
|
-
expiresAt: Date.now() + 3600 * 1000,
|
|
470
|
-
});
|
|
471
|
-
|
|
472
|
-
// A caller for "test-client" must NOT be handed the other client's cached
|
|
473
|
-
// token; it must exchange the token it supplied.
|
|
474
|
-
const result = await refreshTokens(config, "R-B");
|
|
475
|
-
expect(fetchMock).toHaveBeenCalledTimes(1); // did not short-circuit on the wrong-client cache
|
|
476
|
-
expect(result.refreshToken).toBe("refresh-1");
|
|
477
|
-
});
|
|
478
|
-
});
|
|
479
|
-
|
|
480
|
-
describe("refresh expiry hardening", () => {
|
|
481
|
-
it("retries one transient refresh failure and preserves the refresh token", async () => {
|
|
482
|
-
const fetchMock = vi
|
|
483
|
-
.fn()
|
|
484
|
-
.mockResolvedValueOnce(
|
|
485
|
-
new Response(JSON.stringify({ message: "temporarily unavailable" }), {
|
|
486
|
-
status: 503,
|
|
487
|
-
}),
|
|
488
|
-
)
|
|
489
|
-
.mockResolvedValueOnce(
|
|
490
|
-
new Response(
|
|
491
|
-
JSON.stringify({
|
|
492
|
-
access_token: "fresh-access",
|
|
493
|
-
id_token: "fresh-id",
|
|
494
|
-
expires_in: 3600,
|
|
495
|
-
token_type: "Bearer",
|
|
496
|
-
}),
|
|
497
|
-
{ status: 200, headers: { "Content-Type": "application/json" } },
|
|
498
|
-
),
|
|
499
|
-
);
|
|
500
|
-
vi.stubGlobal("fetch", fetchMock);
|
|
501
|
-
|
|
502
|
-
const { refreshTokens } = await importModule();
|
|
503
|
-
const result = await refreshTokens(baseConfig, "preserved-refresh");
|
|
504
|
-
|
|
505
|
-
expect(fetchMock).toHaveBeenCalledTimes(2);
|
|
506
|
-
expect(result.accessToken).toBe("fresh-access");
|
|
507
|
-
expect(result.refreshToken).toBe("preserved-refresh");
|
|
508
|
-
});
|
|
509
|
-
|
|
510
|
-
it("clears an expired refresh session after one 401 retry", async () => {
|
|
511
|
-
const fetchMock = vi.fn(async () =>
|
|
512
|
-
new Response(JSON.stringify({ message: "refresh token expired" }), {
|
|
513
|
-
status: 401,
|
|
514
|
-
}),
|
|
515
|
-
);
|
|
516
|
-
vi.stubGlobal("fetch", fetchMock);
|
|
517
|
-
|
|
518
|
-
const { saveCachedTokens, loadCachedTokens, getValidAccessToken } =
|
|
519
|
-
await importModule();
|
|
520
|
-
saveCachedTokens({ ...baseTokens, expiresAt: 0 });
|
|
521
|
-
|
|
522
|
-
await expect(
|
|
523
|
-
getValidAccessToken(baseConfig, { interactive: false }),
|
|
524
|
-
).rejects.toThrow(/quick refresh/i);
|
|
525
|
-
|
|
526
|
-
expect(fetchMock).toHaveBeenCalledTimes(2);
|
|
527
|
-
expect(loadCachedTokens()).toBeNull();
|
|
528
|
-
});
|
|
529
|
-
|
|
530
|
-
it("keeps the stored session after two transient network failures", async () => {
|
|
531
|
-
const fetchMock = vi.fn(async () => {
|
|
532
|
-
throw new Error("ECONNRESET");
|
|
533
|
-
});
|
|
534
|
-
vi.stubGlobal("fetch", fetchMock);
|
|
535
|
-
|
|
536
|
-
const { saveCachedTokens, loadCachedTokens, getValidAccessToken } =
|
|
537
|
-
await importModule();
|
|
538
|
-
saveCachedTokens({ ...baseTokens, expiresAt: 0 });
|
|
539
|
-
|
|
540
|
-
await expect(
|
|
541
|
-
getValidAccessToken(baseConfig, { interactive: false }),
|
|
542
|
-
).rejects.toThrow(/ECONNRESET/);
|
|
543
|
-
|
|
544
|
-
expect(fetchMock).toHaveBeenCalledTimes(2);
|
|
545
|
-
expect(loadCachedTokens()?.refreshToken).toBe("refresh");
|
|
546
|
-
});
|
|
547
|
-
|
|
548
|
-
it("does not invalidate a newer login when an older token fails late", async () => {
|
|
549
|
-
const {
|
|
550
|
-
accessTokenFingerprint,
|
|
551
|
-
invalidateCachedTokensByFingerprint,
|
|
552
|
-
loadCachedTokens,
|
|
553
|
-
saveCachedTokens,
|
|
554
|
-
} = await importModule();
|
|
555
|
-
|
|
556
|
-
saveCachedTokens({
|
|
557
|
-
...baseTokens,
|
|
558
|
-
accessToken: "old-access",
|
|
559
|
-
expiresAt: Date.now() + 60_000,
|
|
560
|
-
});
|
|
561
|
-
const oldFingerprint = accessTokenFingerprint("old-access");
|
|
562
|
-
|
|
563
|
-
saveCachedTokens({
|
|
564
|
-
...baseTokens,
|
|
565
|
-
accessToken: "new-access",
|
|
566
|
-
expiresAt: Date.now() + 60_000,
|
|
567
|
-
});
|
|
568
|
-
invalidateCachedTokensByFingerprint(oldFingerprint);
|
|
569
|
-
|
|
570
|
-
expect(loadCachedTokens()?.accessToken).toBe("new-access");
|
|
571
|
-
});
|
|
572
|
-
});
|
|
573
|
-
|
|
574
|
-
// ---------------------------------------------------------------------------
|
|
575
|
-
// Machine mint identity binding under EMAIL-ALIAS pools (regression, 2026-07-20)
|
|
576
|
-
//
|
|
577
|
-
// The prod pool aliases email-format usernames: Cognito's canonical username
|
|
578
|
-
// (`cognito:username` / access `username`) is a generated UUID equal to `sub`,
|
|
579
|
-
// NEVER the email-format username the machine creds carry. The 6.14.13
|
|
580
|
-
// predicate bound the ID token to creds via username equality only, so every
|
|
581
|
-
// valid outpost/agent mint was rejected with "Machine token mint returned
|
|
582
|
-
// tokens that do not match the requested machine identity" — a fleet-wide
|
|
583
|
-
// sync outage on boxes. Claim shapes below mirror a real prod outpost token
|
|
584
|
-
// (email claim ABSENT: the machine app client cannot read `email`, so Cognito
|
|
585
|
-
// omits it from the ID token entirely).
|
|
586
|
-
// ---------------------------------------------------------------------------
|
|
587
|
-
|
|
588
|
-
describe("machine mint under email-alias pools (username is a UUID)", () => {
|
|
589
|
-
const MACHINE_CLIENT = "4q3bmth9ad4v3be6booc5td193";
|
|
590
|
-
const OUTPOST_UID = "out_01KXVRESJMP7TD263P7S0YHPSD";
|
|
591
|
-
const COGNITO_INTERNAL_SUB = "f4981438-a0a1-704c-542a-dfac148f2380";
|
|
592
|
-
const CREDS_USERNAME = "out-01kxvresjmp7td263p7s0yhpsd@outposts.hq.computer";
|
|
593
|
-
|
|
594
|
-
function writeOutpostCreds(overrides: Record<string, unknown> = {}) {
|
|
595
|
-
const dir = path.join(tmpHome, ".hq-agent");
|
|
596
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
597
|
-
fs.writeFileSync(
|
|
598
|
-
path.join(dir, "machine-creds.json"),
|
|
599
|
-
JSON.stringify({
|
|
600
|
-
username: CREDS_USERNAME,
|
|
601
|
-
secret: "machine-secret",
|
|
602
|
-
clientId: MACHINE_CLIENT,
|
|
603
|
-
region: "us-east-1",
|
|
604
|
-
entityType: "outpost",
|
|
605
|
-
entityUid: OUTPOST_UID,
|
|
606
|
-
...overrides,
|
|
607
|
-
}),
|
|
608
|
-
);
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
function mintResponse(accessClaims: Record<string, unknown>, idClaims: Record<string, unknown>) {
|
|
612
|
-
return vi.fn(async () =>
|
|
613
|
-
new Response(
|
|
614
|
-
JSON.stringify({
|
|
615
|
-
AuthenticationResult: {
|
|
616
|
-
AccessToken: makeAccessToken(accessClaims),
|
|
617
|
-
IdToken: makeAccessToken(idClaims),
|
|
618
|
-
ExpiresIn: 3600,
|
|
619
|
-
},
|
|
620
|
-
}),
|
|
621
|
-
{ status: 200, headers: { "Content-Type": "application/json" } },
|
|
622
|
-
),
|
|
623
|
-
);
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
it("accepts a valid outpost mint whose cognito:username is the internal UUID (alias pool)", async () => {
|
|
627
|
-
const { getValidAccessToken } = await importModule();
|
|
628
|
-
writeOutpostCreds();
|
|
629
|
-
const fetchMock = mintResponse(
|
|
630
|
-
{
|
|
631
|
-
token_use: "access",
|
|
632
|
-
client_id: MACHINE_CLIENT,
|
|
633
|
-
// Alias pool: access username is the internal UUID, not the email-format name.
|
|
634
|
-
username: COGNITO_INTERNAL_SUB,
|
|
635
|
-
sub: COGNITO_INTERNAL_SUB,
|
|
636
|
-
},
|
|
637
|
-
{
|
|
638
|
-
token_use: "id",
|
|
639
|
-
aud: MACHINE_CLIENT,
|
|
640
|
-
"cognito:username": COGNITO_INTERNAL_SUB,
|
|
641
|
-
sub: COGNITO_INTERNAL_SUB,
|
|
642
|
-
// email claim intentionally ABSENT (client cannot read `email`).
|
|
643
|
-
"custom:entityType": "outpost",
|
|
644
|
-
"custom:entityUid": OUTPOST_UID,
|
|
645
|
-
"custom:delegatedSub": "24785418-c071-7077-b7e9-0b1b583cfba1",
|
|
646
|
-
"custom:delegatedEmail": "hassaan@getindigo.ai",
|
|
647
|
-
},
|
|
648
|
-
);
|
|
649
|
-
vi.stubGlobal("fetch", fetchMock);
|
|
650
|
-
|
|
651
|
-
await expect(
|
|
652
|
-
getValidAccessToken(baseConfig, { interactive: false }),
|
|
653
|
-
).resolves.toBeTypeOf("string");
|
|
654
|
-
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
655
|
-
});
|
|
656
|
-
|
|
657
|
-
it("still rejects a mint whose entityUid does not match the requested creds", async () => {
|
|
658
|
-
const { getValidAccessToken } = await importModule();
|
|
659
|
-
writeOutpostCreds();
|
|
660
|
-
const fetchMock = mintResponse(
|
|
661
|
-
{
|
|
662
|
-
token_use: "access",
|
|
663
|
-
client_id: MACHINE_CLIENT,
|
|
664
|
-
username: COGNITO_INTERNAL_SUB,
|
|
665
|
-
sub: COGNITO_INTERNAL_SUB,
|
|
666
|
-
},
|
|
667
|
-
{
|
|
668
|
-
token_use: "id",
|
|
669
|
-
aud: MACHINE_CLIENT,
|
|
670
|
-
"cognito:username": COGNITO_INTERNAL_SUB,
|
|
671
|
-
sub: COGNITO_INTERNAL_SUB,
|
|
672
|
-
"custom:entityType": "outpost",
|
|
673
|
-
// A DIFFERENT machine's uid — identity binding must refuse it.
|
|
674
|
-
"custom:entityUid": "out_01KX0000000000000000000000",
|
|
675
|
-
"custom:delegatedSub": "24785418-c071-7077-b7e9-0b1b583cfba1",
|
|
676
|
-
"custom:delegatedEmail": "hassaan@getindigo.ai",
|
|
677
|
-
},
|
|
678
|
-
);
|
|
679
|
-
vi.stubGlobal("fetch", fetchMock);
|
|
680
|
-
|
|
681
|
-
await expect(
|
|
682
|
-
getValidAccessToken(baseConfig, { interactive: false }),
|
|
683
|
-
).rejects.toThrow(/do not match the requested machine identity/);
|
|
684
|
-
});
|
|
685
|
-
|
|
686
|
-
it("still rejects legacy creds (no entityUid) when the username binding fails", async () => {
|
|
687
|
-
const { getValidAccessToken } = await importModule();
|
|
688
|
-
// Legacy agent creds: no entityType/entityUid persisted.
|
|
689
|
-
writeOutpostCreds({ entityType: undefined, entityUid: undefined, username: "agt-legacy@agents.getindigo.ai" });
|
|
690
|
-
const fetchMock = mintResponse(
|
|
691
|
-
{
|
|
692
|
-
token_use: "access",
|
|
693
|
-
client_id: MACHINE_CLIENT,
|
|
694
|
-
username: "someone-else",
|
|
695
|
-
sub: "someone-else",
|
|
696
|
-
},
|
|
697
|
-
{
|
|
698
|
-
token_use: "id",
|
|
699
|
-
aud: MACHINE_CLIENT,
|
|
700
|
-
"cognito:username": "someone-else",
|
|
701
|
-
sub: "someone-else",
|
|
702
|
-
"custom:entityType": "agent",
|
|
703
|
-
"custom:entityUid": "agt_01JZ9999999999999999999999",
|
|
704
|
-
},
|
|
705
|
-
);
|
|
706
|
-
vi.stubGlobal("fetch", fetchMock);
|
|
707
|
-
|
|
708
|
-
await expect(
|
|
709
|
-
getValidAccessToken(baseConfig, { interactive: false }),
|
|
710
|
-
).rejects.toThrow(/do not match the requested machine identity/);
|
|
711
|
-
});
|
|
712
|
-
});
|