@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.
Files changed (329) hide show
  1. package/dist/bin/sync-mutation.d.ts +16 -0
  2. package/dist/bin/sync-mutation.d.ts.map +1 -0
  3. package/dist/bin/sync-mutation.js +60 -0
  4. package/dist/bin/sync-mutation.js.map +1 -0
  5. package/dist/bin/sync-mutation.test.d.ts +2 -0
  6. package/dist/bin/sync-mutation.test.d.ts.map +1 -0
  7. package/dist/bin/sync-mutation.test.js +165 -0
  8. package/dist/bin/sync-mutation.test.js.map +1 -0
  9. package/dist/bin/sync-runner-company.d.ts +8 -0
  10. package/dist/bin/sync-runner-company.d.ts.map +1 -1
  11. package/dist/bin/sync-runner-company.js +16 -0
  12. package/dist/bin/sync-runner-company.js.map +1 -1
  13. package/dist/bin/sync-runner-company.test.d.ts +2 -0
  14. package/dist/bin/sync-runner-company.test.d.ts.map +1 -0
  15. package/dist/bin/sync-runner-company.test.js +36 -0
  16. package/dist/bin/sync-runner-company.test.js.map +1 -0
  17. package/dist/bin/sync-runner-watch-loop.d.ts.map +1 -1
  18. package/dist/bin/sync-runner-watch-loop.js +98 -8
  19. package/dist/bin/sync-runner-watch-loop.js.map +1 -1
  20. package/dist/bin/sync-runner.d.ts +17 -0
  21. package/dist/bin/sync-runner.d.ts.map +1 -1
  22. package/dist/bin/sync-runner.js.map +1 -1
  23. package/dist/bin/sync-runner.test.js +109 -0
  24. package/dist/bin/sync-runner.test.js.map +1 -1
  25. package/dist/cli/conflict-recovery.test.d.ts +2 -0
  26. package/dist/cli/conflict-recovery.test.d.ts.map +1 -0
  27. package/dist/cli/conflict-recovery.test.js +201 -0
  28. package/dist/cli/conflict-recovery.test.js.map +1 -0
  29. package/dist/cli/conflict.d.ts +60 -0
  30. package/dist/cli/conflict.d.ts.map +1 -1
  31. package/dist/cli/conflict.js +333 -0
  32. package/dist/cli/conflict.js.map +1 -1
  33. package/dist/cli/sync.d.ts +35 -0
  34. package/dist/cli/sync.d.ts.map +1 -1
  35. package/dist/cli/sync.js +100 -0
  36. package/dist/cli/sync.js.map +1 -1
  37. package/dist/cli/sync.test.js +85 -1
  38. package/dist/cli/sync.test.js.map +1 -1
  39. package/dist/index.d.ts +2 -0
  40. package/dist/index.d.ts.map +1 -1
  41. package/dist/index.js +1 -0
  42. package/dist/index.js.map +1 -1
  43. package/dist/skill-telemetry.d.ts +6 -0
  44. package/dist/skill-telemetry.d.ts.map +1 -1
  45. package/dist/skill-telemetry.js +14 -2
  46. package/dist/skill-telemetry.js.map +1 -1
  47. package/dist/skill-telemetry.test.js +79 -0
  48. package/dist/skill-telemetry.test.js.map +1 -1
  49. package/dist/sync/candidate-uploader.d.ts +88 -0
  50. package/dist/sync/candidate-uploader.d.ts.map +1 -0
  51. package/dist/sync/candidate-uploader.js +212 -0
  52. package/dist/sync/candidate-uploader.js.map +1 -0
  53. package/dist/sync/candidate-uploader.test.d.ts +2 -0
  54. package/dist/sync/candidate-uploader.test.d.ts.map +1 -0
  55. package/dist/sync/candidate-uploader.test.js +132 -0
  56. package/dist/sync/candidate-uploader.test.js.map +1 -0
  57. package/dist/sync/delta-client.d.ts +73 -0
  58. package/dist/sync/delta-client.d.ts.map +1 -0
  59. package/dist/sync/delta-client.js +201 -0
  60. package/dist/sync/delta-client.js.map +1 -0
  61. package/dist/sync/delta-client.test.d.ts +2 -0
  62. package/dist/sync/delta-client.test.d.ts.map +1 -0
  63. package/dist/sync/delta-client.test.js +97 -0
  64. package/dist/sync/delta-client.test.js.map +1 -0
  65. package/dist/sync/durable-apply.d.ts +76 -0
  66. package/dist/sync/durable-apply.d.ts.map +1 -0
  67. package/dist/sync/durable-apply.js +530 -0
  68. package/dist/sync/durable-apply.js.map +1 -0
  69. package/dist/sync/durable-apply.test.d.ts +2 -0
  70. package/dist/sync/durable-apply.test.d.ts.map +1 -0
  71. package/dist/sync/durable-apply.test.js +180 -0
  72. package/dist/sync/durable-apply.test.js.map +1 -0
  73. package/dist/sync/event-sync.d.ts +33 -1
  74. package/dist/sync/event-sync.d.ts.map +1 -1
  75. package/dist/sync/event-sync.js +149 -1
  76. package/dist/sync/event-sync.js.map +1 -1
  77. package/dist/sync/event-sync.test.js +142 -1
  78. package/dist/sync/event-sync.test.js.map +1 -1
  79. package/dist/sync/index.d.ts +2 -0
  80. package/dist/sync/index.d.ts.map +1 -1
  81. package/dist/sync/index.js +1 -0
  82. package/dist/sync/index.js.map +1 -1
  83. package/dist/sync/multipart-uploader.d.ts +99 -0
  84. package/dist/sync/multipart-uploader.d.ts.map +1 -0
  85. package/dist/sync/multipart-uploader.js +447 -0
  86. package/dist/sync/multipart-uploader.js.map +1 -0
  87. package/dist/sync/multipart-uploader.test.d.ts +2 -0
  88. package/dist/sync/multipart-uploader.test.d.ts.map +1 -0
  89. package/dist/sync/multipart-uploader.test.js +119 -0
  90. package/dist/sync/multipart-uploader.test.js.map +1 -0
  91. package/dist/sync/mutation-client.d.ts +85 -0
  92. package/dist/sync/mutation-client.d.ts.map +1 -0
  93. package/dist/sync/mutation-client.js +245 -0
  94. package/dist/sync/mutation-client.js.map +1 -0
  95. package/dist/sync/mutation-client.test.d.ts +2 -0
  96. package/dist/sync/mutation-client.test.d.ts.map +1 -0
  97. package/dist/sync/mutation-client.test.js +51 -0
  98. package/dist/sync/mutation-client.test.js.map +1 -0
  99. package/dist/sync/push-receiver.d.ts +45 -0
  100. package/dist/sync/push-receiver.d.ts.map +1 -1
  101. package/dist/sync/push-receiver.js +101 -0
  102. package/dist/sync/push-receiver.js.map +1 -1
  103. package/dist/sync/push-receiver.test.js +54 -2
  104. package/dist/sync/push-receiver.test.js.map +1 -1
  105. package/dist/sync/scope-inventory-client.d.ts +69 -0
  106. package/dist/sync/scope-inventory-client.d.ts.map +1 -0
  107. package/dist/sync/scope-inventory-client.js +210 -0
  108. package/dist/sync/scope-inventory-client.js.map +1 -0
  109. package/dist/sync/scope-inventory-client.test.d.ts +2 -0
  110. package/dist/sync/scope-inventory-client.test.d.ts.map +1 -0
  111. package/dist/sync/scope-inventory-client.test.js +94 -0
  112. package/dist/sync/scope-inventory-client.test.js.map +1 -0
  113. package/dist/sync/snapshot-client.d.ts +98 -0
  114. package/dist/sync/snapshot-client.d.ts.map +1 -0
  115. package/dist/sync/snapshot-client.js +402 -0
  116. package/dist/sync/snapshot-client.js.map +1 -0
  117. package/dist/sync/snapshot-client.test.d.ts +2 -0
  118. package/dist/sync/snapshot-client.test.d.ts.map +1 -0
  119. package/dist/sync/snapshot-client.test.js +169 -0
  120. package/dist/sync/snapshot-client.test.js.map +1 -0
  121. package/dist/sync/uploader-finalization.d.ts +97 -0
  122. package/dist/sync/uploader-finalization.d.ts.map +1 -0
  123. package/dist/sync/uploader-finalization.js +273 -0
  124. package/dist/sync/uploader-finalization.js.map +1 -0
  125. package/dist/sync/uploader-finalization.test.d.ts +2 -0
  126. package/dist/sync/uploader-finalization.test.d.ts.map +1 -0
  127. package/dist/sync/uploader-finalization.test.js +92 -0
  128. package/dist/sync/uploader-finalization.test.js.map +1 -0
  129. package/dist/telemetry.d.ts +11 -1
  130. package/dist/telemetry.d.ts.map +1 -1
  131. package/dist/telemetry.js +21 -2
  132. package/dist/telemetry.js.map +1 -1
  133. package/dist/telemetry.test.js +80 -0
  134. package/dist/telemetry.test.js.map +1 -1
  135. package/package.json +6 -1
  136. package/.claude/policies/hq-cloud-esm-cannot-spy-fs-builtins.md +0 -30
  137. package/.claude/policies/hq-cloud-strip-types-no-parameter-properties.md +0 -22
  138. package/.github/workflows/ci.yml +0 -84
  139. package/.github/workflows/publish.yml +0 -56
  140. package/.github/workflows/unreleased-commits-nag.yml +0 -256
  141. package/eslint.config.js +0 -67
  142. package/pnpm-workspace.yaml +0 -2
  143. package/scripts/presign-transport-e2e.mjs +0 -250
  144. package/scripts/vault-rebaseline.sh +0 -323
  145. package/scripts/vault-rescue.sh +0 -332
  146. package/src/active-company.test.ts +0 -188
  147. package/src/active-company.ts +0 -168
  148. package/src/agent-codex-instructions.test.ts +0 -332
  149. package/src/agent-codex-instructions.ts +0 -309
  150. package/src/auth.ts +0 -146
  151. package/src/backup-prune.test.ts +0 -98
  152. package/src/backup-prune.ts +0 -182
  153. package/src/bin/backup-prune-runner.ts +0 -33
  154. package/src/bin/rescue-runner.ts +0 -25
  155. package/src/bin/sync-runner-company.ts +0 -695
  156. package/src/bin/sync-runner-events.test.ts +0 -143
  157. package/src/bin/sync-runner-events.ts +0 -55
  158. package/src/bin/sync-runner-planning.test.ts +0 -311
  159. package/src/bin/sync-runner-planning.ts +0 -258
  160. package/src/bin/sync-runner-rollup.test.ts +0 -37
  161. package/src/bin/sync-runner-rollup.ts +0 -97
  162. package/src/bin/sync-runner-telemetry.ts +0 -15
  163. package/src/bin/sync-runner-watch-loop.ts +0 -1235
  164. package/src/bin/sync-runner-watch-routes.test.ts +0 -71
  165. package/src/bin/sync-runner-watch-routes.ts +0 -184
  166. package/src/bin/sync-runner.test.ts +0 -8767
  167. package/src/bin/sync-runner.ts +0 -2190
  168. package/src/cli/accept.ts +0 -124
  169. package/src/cli/conflict.ts +0 -119
  170. package/src/cli/doctor.test.ts +0 -581
  171. package/src/cli/doctor.ts +0 -642
  172. package/src/cli/index.ts +0 -49
  173. package/src/cli/invite.test.ts +0 -250
  174. package/src/cli/invite.ts +0 -214
  175. package/src/cli/promote.ts +0 -157
  176. package/src/cli/reindex-knowledge.test.ts +0 -307
  177. package/src/cli/reindex-knowledge.ts +0 -450
  178. package/src/cli/reindex.test.ts +0 -957
  179. package/src/cli/reindex.ts +0 -979
  180. package/src/cli/rescue-classify-ordering.test.ts +0 -548
  181. package/src/cli/rescue-clone-diagnostics.test.ts +0 -120
  182. package/src/cli/rescue-core.ts +0 -3011
  183. package/src/cli/rescue-drift-reconcile.test.ts +0 -179
  184. package/src/cli/rescue-drop-dir-symlink.test.ts +0 -224
  185. package/src/cli/rescue-exec-bit-preserve.test.ts +0 -187
  186. package/src/cli/rescue-hq-root-guard.test.ts +0 -232
  187. package/src/cli/rescue-journal-reconcile.test.ts +0 -215
  188. package/src/cli/rescue-mtime-preserve.test.ts +0 -203
  189. package/src/cli/rescue-settings-reconcile.test.ts +0 -637
  190. package/src/cli/rescue-snapshot.test.ts +0 -57
  191. package/src/cli/rescue-snapshot.ts +0 -51
  192. package/src/cli/rescue.reindex.test.ts +0 -63
  193. package/src/cli/rescue.test.ts +0 -131
  194. package/src/cli/rescue.ts +0 -182
  195. package/src/cli/share.test.ts +0 -7843
  196. package/src/cli/share.ts +0 -3663
  197. package/src/cli/sync-scope.test.ts +0 -652
  198. package/src/cli/sync.test.ts +0 -5207
  199. package/src/cli/sync.ts +0 -3470
  200. package/src/cli/tombstones.ts +0 -106
  201. package/src/cli/watch-event-push-conflict.test.ts +0 -234
  202. package/src/client-info.test.ts +0 -214
  203. package/src/client-info.ts +0 -121
  204. package/src/cognito-auth.test.ts +0 -712
  205. package/src/cognito-auth.ts +0 -1422
  206. package/src/company-resolver.test.ts +0 -618
  207. package/src/company-resolver.ts +0 -521
  208. package/src/context.test.ts +0 -583
  209. package/src/context.ts +0 -378
  210. package/src/daemon-worker.ts +0 -26
  211. package/src/daemon.ts +0 -99
  212. package/src/entity-resolver.test.ts +0 -315
  213. package/src/entity-resolver.ts +0 -180
  214. package/src/ignore.test.ts +0 -466
  215. package/src/ignore.ts +0 -469
  216. package/src/index.ts +0 -439
  217. package/src/journal.test.ts +0 -968
  218. package/src/journal.ts +0 -765
  219. package/src/lib/cloud-authoritative.test.ts +0 -45
  220. package/src/lib/cloud-authoritative.ts +0 -59
  221. package/src/lib/conflict-file.ts +0 -86
  222. package/src/lib/conflict-index.ts +0 -289
  223. package/src/lib/conflict.test.ts +0 -348
  224. package/src/lib/describe-error.test.ts +0 -100
  225. package/src/lib/describe-error.ts +0 -58
  226. package/src/lib/exit-codes.ts +0 -24
  227. package/src/lib/machine-id.test.ts +0 -231
  228. package/src/lib/machine-id.ts +0 -175
  229. package/src/lib/net-errors.test.ts +0 -65
  230. package/src/lib/net-errors.ts +0 -86
  231. package/src/lib/readlink-safe.test.ts +0 -43
  232. package/src/lib/readlink-safe.ts +0 -29
  233. package/src/local-path-codec.test.ts +0 -138
  234. package/src/local-path-codec.ts +0 -161
  235. package/src/machine-auth.test.ts +0 -1323
  236. package/src/manifest-reconcile.test.ts +0 -1123
  237. package/src/manifest-reconcile.ts +0 -518
  238. package/src/object-io.test.ts +0 -1221
  239. package/src/object-io.ts +0 -1306
  240. package/src/operation-lock.test.ts +0 -484
  241. package/src/operation-lock.ts +0 -680
  242. package/src/outcome-telemetry.test.ts +0 -498
  243. package/src/outcome-telemetry.ts +0 -639
  244. package/src/personal-vault-exclusions.test.ts +0 -308
  245. package/src/personal-vault-exclusions.ts +0 -354
  246. package/src/personal-vault.test.ts +0 -756
  247. package/src/personal-vault.ts +0 -496
  248. package/src/prefix-coalesce.test.ts +0 -240
  249. package/src/prefix-coalesce.ts +0 -273
  250. package/src/public-surface.test.ts +0 -117
  251. package/src/qmd-reindex.test.ts +0 -877
  252. package/src/qmd-reindex.ts +0 -842
  253. package/src/read-only-state-dir.test.ts +0 -188
  254. package/src/remote-pull.test.ts +0 -1130
  255. package/src/remote-pull.ts +0 -618
  256. package/src/s3.symlink-materialize.test.ts +0 -492
  257. package/src/s3.test.ts +0 -1789
  258. package/src/s3.ts +0 -1532
  259. package/src/schemas/signal-types.test.ts +0 -82
  260. package/src/schemas/signal-types.ts +0 -38
  261. package/src/schemas/source-channels.test.ts +0 -82
  262. package/src/schemas/source-channels.ts +0 -53
  263. package/src/scope-shrink.test.ts +0 -633
  264. package/src/scope-shrink.ts +0 -481
  265. package/src/signals/get.test.ts +0 -310
  266. package/src/signals/get.ts +0 -75
  267. package/src/signals/internals.ts +0 -195
  268. package/src/signals/list.test.ts +0 -420
  269. package/src/signals/list.ts +0 -79
  270. package/src/signals/parse.ts +0 -8
  271. package/src/signals/types.ts +0 -91
  272. package/src/skill-telemetry.test.ts +0 -1825
  273. package/src/skill-telemetry.ts +0 -1439
  274. package/src/sources/get.test.ts +0 -293
  275. package/src/sources/get.ts +0 -66
  276. package/src/sources/internals.ts +0 -198
  277. package/src/sources/list.test.ts +0 -402
  278. package/src/sources/list.ts +0 -84
  279. package/src/sources/parse.ts +0 -43
  280. package/src/sources/types.ts +0 -84
  281. package/src/sync/event-sync.test.ts +0 -594
  282. package/src/sync/event-sync.ts +0 -545
  283. package/src/sync/feature-flags.test.ts +0 -378
  284. package/src/sync/feature-flags.ts +0 -62
  285. package/src/sync/index.ts +0 -76
  286. package/src/sync/lease-client.test.ts +0 -128
  287. package/src/sync/lease-client.ts +0 -207
  288. package/src/sync/logger.test.ts +0 -242
  289. package/src/sync/logger.ts +0 -79
  290. package/src/sync/metrics.test.ts +0 -462
  291. package/src/sync/metrics.ts +0 -213
  292. package/src/sync/pull-scope.ts +0 -265
  293. package/src/sync/push-event.test.ts +0 -266
  294. package/src/sync/push-event.ts +0 -224
  295. package/src/sync/push-receiver.test.ts +0 -566
  296. package/src/sync/push-receiver.ts +0 -1048
  297. package/src/sync/push-transport.ts +0 -231
  298. package/src/sync/realtime-rollout.test.ts +0 -86
  299. package/src/sync/realtime-rollout.ts +0 -262
  300. package/src/sync/state-store.test.ts +0 -194
  301. package/src/sync/state-store.ts +0 -727
  302. package/src/sync-core.ts +0 -58
  303. package/src/sync-progress.test.ts +0 -94
  304. package/src/sync-progress.ts +0 -140
  305. package/src/telemetry-events.test.ts +0 -88
  306. package/src/telemetry-events.ts +0 -205
  307. package/src/telemetry.test.ts +0 -1280
  308. package/src/telemetry.ts +0 -1109
  309. package/src/types.ts +0 -314
  310. package/src/vault-client.test.ts +0 -1380
  311. package/src/vault-client.ts +0 -1694
  312. package/src/version.ts +0 -24
  313. package/src/watch-roots.test.ts +0 -278
  314. package/src/watch-roots.ts +0 -162
  315. package/src/watcher-event-gate.test.ts +0 -212
  316. package/src/watcher.test.ts +0 -1079
  317. package/src/watcher.ts +0 -1741
  318. package/test/e2e/sync/cross-tenant-isolation.test.ts +0 -630
  319. package/test/e2e/sync/skill-telemetry-oversized-transcript.test.ts +0 -124
  320. package/test/e2e/sync/transient-company-leg.test.ts +0 -384
  321. package/test/e2e/sync/windows-unreadable-link-leg.test.ts +0 -191
  322. package/test/e2e/watcher-real-chokidar.test.ts +0 -165
  323. package/test/e2e/watcher-recursive-backend.test.ts +0 -181
  324. package/test/e2e/watcher-scoped-coverage.test.ts +0 -381
  325. package/test/invite-flow.integration.test.ts +0 -244
  326. package/test/joiner-manifest-reconcile.integration.test.ts +0 -322
  327. package/test/share-sync.integration.test.ts +0 -213
  328. package/tsconfig.json +0 -19
  329. package/vitest.config.ts +0 -22
@@ -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
- });