@indigoai-us/hq-cloud 6.14.50 → 6.15.1

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