@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,1323 +0,0 @@
1
- /**
2
- * Unit tests for the machine-identity auth mode in cognito-auth.ts.
3
- *
4
- * Machine identities (company agents) carry long-lived Cognito creds at
5
- * ~/.hq-agent/machine-creds.json (HQ_MACHINE_CREDS_FILE override) and mint
6
- * sessions via USER_PASSWORD_AUTH on demand — no browser, no refresh token.
7
- * The contract under test:
8
- * - detection: creds file present + well-formed → machine mode
9
- * - minting: BOTH tokens cached with correct field semantics
10
- * - re-mint on expiry; cache hit when valid
11
- * - getValidAccessToken short-circuits into machine mode (never browser)
12
- */
13
-
14
- import * as fs from "fs";
15
- import * as os from "os";
16
- import * as path from "path";
17
- import { spawn } from "child_process";
18
- import { pathToFileURL } from "url";
19
- import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
20
-
21
- let originalHome: string | undefined;
22
- let originalUserProfile: string | undefined;
23
- let originalCredsEnv: string | undefined;
24
- let tmpHome: string;
25
-
26
- beforeEach(() => {
27
- originalHome = process.env.HOME;
28
- originalUserProfile = process.env.USERPROFILE;
29
- originalCredsEnv = process.env.HQ_MACHINE_CREDS_FILE;
30
- tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), "hq-machine-auth-test-"));
31
- process.env.HOME = tmpHome;
32
- process.env.USERPROFILE = tmpHome;
33
- delete process.env.HQ_MACHINE_CREDS_FILE;
34
- vi.resetModules();
35
- });
36
-
37
- afterEach(() => {
38
- vi.useRealTimers();
39
- if (originalHome === undefined) delete process.env.HOME;
40
- else process.env.HOME = originalHome;
41
- if (originalUserProfile === undefined) delete process.env.USERPROFILE;
42
- else process.env.USERPROFILE = originalUserProfile;
43
- if (originalCredsEnv === undefined) delete process.env.HQ_MACHINE_CREDS_FILE;
44
- else process.env.HQ_MACHINE_CREDS_FILE = originalCredsEnv;
45
- fs.rmSync(tmpHome, { recursive: true, force: true });
46
- vi.unstubAllGlobals();
47
- vi.restoreAllMocks();
48
- });
49
-
50
- async function importModule() {
51
- return await import("./cognito-auth.js");
52
- }
53
-
54
- const CONFIG = {
55
- region: "us-east-1",
56
- userPoolDomain: "vault-indigo-hq-prod",
57
- clientId: "test-client-id",
58
- };
59
-
60
- function writeCreds(
61
- creds: unknown = { username: "machine-agt_01TEST", secret: "s3cret" },
62
- ): string {
63
- const dir = path.join(tmpHome, ".hq-agent");
64
- fs.mkdirSync(dir, { recursive: true });
65
- const file = path.join(dir, "machine-creds.json");
66
- fs.writeFileSync(file, JSON.stringify(creds));
67
- return file;
68
- }
69
-
70
- /** Build a fake JWT whose payload decodes to the given claims. */
71
- function fakeJwt(claims: Record<string, unknown>): string {
72
- const enc = (o: unknown) =>
73
- Buffer.from(JSON.stringify(o)).toString("base64url");
74
- return `${enc({ alg: "RS256", kid: "k" })}.${enc(claims)}.sig`;
75
- }
76
-
77
- function stubMintFetch(
78
- overrides: Partial<{
79
- AccessToken: string;
80
- IdToken: string;
81
- RefreshToken: string;
82
- ExpiresIn: number;
83
- }> = {},
84
- ) {
85
- const calls: Array<{ url: string; init: RequestInit }> = [];
86
- const fetchMock = vi.fn(async (url: string | URL, init?: RequestInit) => {
87
- calls.push({ url: String(url), init: init ?? {} });
88
- const body = JSON.parse(String(init?.body)) as {
89
- ClientId: string;
90
- AuthParameters: { USERNAME: string };
91
- };
92
- const credsFile =
93
- process.env.HQ_MACHINE_CREDS_FILE ??
94
- path.join(tmpHome, ".hq-agent", "machine-creds.json");
95
- const creds = JSON.parse(fs.readFileSync(credsFile, "utf-8")) as {
96
- entityType?: "agent" | "outpost";
97
- entityUid?: string;
98
- };
99
- const username = body.AuthParameters.USERNAME;
100
- const subject = `sub:${username}`;
101
- const entityType = creds.entityType ?? "agent";
102
- const entityUid = creds.entityUid ?? "agt_01TEST";
103
- return new Response(
104
- JSON.stringify({
105
- AuthenticationResult: {
106
- AccessToken:
107
- overrides.AccessToken ??
108
- fakeJwt({
109
- token_use: "access",
110
- client_id: body.ClientId,
111
- username,
112
- sub: subject,
113
- }),
114
- IdToken:
115
- overrides.IdToken ??
116
- fakeJwt({
117
- token_use: "id",
118
- aud: body.ClientId,
119
- "custom:entityType": entityType,
120
- "custom:entityUid": entityUid,
121
- ...(entityType === "outpost"
122
- ? {
123
- "custom:delegatedSub": "human-sub",
124
- "custom:delegatedEmail": "owner@example.com",
125
- }
126
- : {}),
127
- "cognito:username": username,
128
- sub: subject,
129
- }),
130
- RefreshToken: overrides.RefreshToken,
131
- ExpiresIn: overrides.ExpiresIn ?? 3600,
132
- },
133
- }),
134
- { status: 200 },
135
- );
136
- });
137
- vi.stubGlobal("fetch", fetchMock);
138
- return { fetchMock, calls };
139
- }
140
-
141
- // ---------------------------------------------------------------------------
142
- // Detection
143
- // ---------------------------------------------------------------------------
144
-
145
- describe("machine identity detection", () => {
146
- it("is off when no creds file exists", async () => {
147
- const { isMachineIdentity, loadMachineCreds } = await importModule();
148
- expect(loadMachineCreds()).toBeNull();
149
- expect(isMachineIdentity()).toBe(false);
150
- });
151
-
152
- it("detects creds at the default ~/.hq-agent path", async () => {
153
- writeCreds();
154
- const { isMachineIdentity, loadMachineCreds } = await importModule();
155
- expect(loadMachineCreds()).toEqual({
156
- username: "machine-agt_01TEST",
157
- secret: "s3cret",
158
- clientId: undefined,
159
- region: undefined,
160
- });
161
- expect(isMachineIdentity()).toBe(true);
162
- });
163
-
164
- it("detects provisioned-shape creds (agt-<ulid>@agents domain + clientId/region)", async () => {
165
- writeCreds({
166
- username: "agt-01ktt1rjj6kqrhnhhst0k4vrgt@agents.getindigo.ai",
167
- secret: "s3cret",
168
- clientId: "agent-client-1",
169
- region: "us-east-1",
170
- userPoolId: "us-east-1_pool",
171
- });
172
- const { isMachineIdentity, loadMachineCreds } = await importModule();
173
- expect(loadMachineCreds()).toEqual({
174
- username: "agt-01ktt1rjj6kqrhnhhst0k4vrgt@agents.getindigo.ai",
175
- secret: "s3cret",
176
- clientId: "agent-client-1",
177
- region: "us-east-1",
178
- });
179
- expect(isMachineIdentity()).toBe(true);
180
- });
181
-
182
- it("parses the expected machine entity type and uid from provisioned creds", async () => {
183
- writeCreds({
184
- username: "out-01test@outposts.getindigo.ai",
185
- secret: "s3cret",
186
- clientId: "outpost-client-1",
187
- region: "us-east-1",
188
- entityType: "outpost",
189
- entityUid: "out_01TEST",
190
- });
191
- const { loadMachineCreds } = await importModule();
192
-
193
- expect(loadMachineCreds()).toEqual({
194
- username: "out-01test@outposts.getindigo.ai",
195
- secret: "s3cret",
196
- clientId: "outpost-client-1",
197
- region: "us-east-1",
198
- entityType: "outpost",
199
- entityUid: "out_01TEST",
200
- });
201
- });
202
-
203
- it("honors HQ_MACHINE_CREDS_FILE override", async () => {
204
- const custom = path.join(tmpHome, "elsewhere.json");
205
- fs.writeFileSync(
206
- custom,
207
- JSON.stringify({ username: "machine-agt_X", secret: "y" }),
208
- );
209
- process.env.HQ_MACHINE_CREDS_FILE = custom;
210
- const { loadMachineCreds, machineCredsFilePath } = await importModule();
211
- expect(machineCredsFilePath()).toBe(custom);
212
- expect(loadMachineCreds()?.username).toBe("machine-agt_X");
213
- });
214
-
215
- it("rejects malformed creds (missing secret, empty username, bad JSON)", async () => {
216
- const { loadMachineCreds } = await importModule();
217
- writeCreds({ username: "", secret: "x" });
218
- expect(loadMachineCreds()).toBeNull();
219
- writeCreds({ username: "machine-agt_01TEST" });
220
- expect(loadMachineCreds()).toBeNull();
221
- fs.writeFileSync(path.join(tmpHome, ".hq-agent", "machine-creds.json"), "{nope");
222
- expect(loadMachineCreds()).toBeNull();
223
- });
224
-
225
- it.each([
226
- { entityType: "agent", entityUid: "out_WRONG" },
227
- { entityType: "outpost", entityUid: "agt_WRONG" },
228
- { entityType: "agent", entityUid: "garbage" },
229
- { entityType: "agent" },
230
- { entityType: "outpost" },
231
- { entityUid: "out_NO_TYPE" },
232
- { entityUid: "garbage" },
233
- { entityType: "agent", entityUid: "agt_" },
234
- { entityType: "outpost", entityUid: "out_" },
235
- ])("rejects malformed or cross-prefix entity metadata: %j", async (identity) => {
236
- writeCreds({ username: "machine", secret: "secret", ...identity });
237
- const { loadMachineCreds } = await importModule();
238
-
239
- expect(loadMachineCreds()).toBeNull();
240
- });
241
-
242
- it("accepts an agt_* uid without a type as the legacy agent inference", async () => {
243
- writeCreds({
244
- username: "machine-agt_01TEST",
245
- secret: "secret",
246
- entityUid: "agt_01TEST",
247
- });
248
- const { loadMachineCreds } = await importModule();
249
-
250
- expect(loadMachineCreds()).toMatchObject({ entityUid: "agt_01TEST" });
251
- expect(loadMachineCreds()?.entityType).toBeUndefined();
252
- });
253
- });
254
-
255
- // ---------------------------------------------------------------------------
256
- // Minting
257
- // ---------------------------------------------------------------------------
258
-
259
- describe("mintMachineTokens", () => {
260
- it("prefers the creds file's clientId/region over config (agent app client has USER_PASSWORD_AUTH)", async () => {
261
- writeCreds({
262
- username: "agt-01ktt1rjj6kqrhnhhst0k4vrgt@agents.getindigo.ai",
263
- secret: "s3cret",
264
- clientId: "agent-client-1",
265
- region: "eu-west-1",
266
- });
267
- const { calls } = stubMintFetch();
268
- const { mintMachineTokens } = await importModule();
269
- await mintMachineTokens(CONFIG);
270
- expect(calls[0].url).toBe("https://cognito-idp.eu-west-1.amazonaws.com/");
271
- const body = JSON.parse(String(calls[0].init.body));
272
- expect(body.ClientId).toBe("agent-client-1");
273
- });
274
-
275
- it("mints via USER_PASSWORD_AUTH and caches both tokens with correct fields", async () => {
276
- writeCreds();
277
- const { calls } = stubMintFetch({ RefreshToken: "rt" });
278
- const { mintMachineTokens, loadCachedTokens } = await importModule();
279
-
280
- const tokens = await mintMachineTokens(CONFIG);
281
-
282
- // Request shape: Cognito IDP InitiateAuth with the machine creds.
283
- expect(calls).toHaveLength(1);
284
- expect(calls[0].url).toBe("https://cognito-idp.us-east-1.amazonaws.com/");
285
- const body = JSON.parse(String(calls[0].init.body));
286
- expect(body).toMatchObject({
287
- AuthFlow: "USER_PASSWORD_AUTH",
288
- ClientId: CONFIG.clientId,
289
- AuthParameters: { USERNAME: "machine-agt_01TEST", PASSWORD: "s3cret" },
290
- });
291
-
292
- // Field semantics: access token in accessToken, id token in idToken.
293
- const accessClaims = JSON.parse(
294
- Buffer.from(tokens.accessToken.split(".")[1], "base64url").toString(),
295
- );
296
- const idClaims = JSON.parse(
297
- Buffer.from(tokens.idToken.split(".")[1], "base64url").toString(),
298
- );
299
- expect(accessClaims.token_use).toBe("access");
300
- expect(idClaims.token_use).toBe("id");
301
- expect(tokens.refreshToken).toBe("");
302
- expect(typeof tokens.expiresAt).toBe("number");
303
-
304
- // Persisted to the shared cache file.
305
- expect(loadCachedTokens()).toEqual(tokens);
306
- });
307
-
308
- it("throws CognitoAuthError on auth failure", async () => {
309
- writeCreds();
310
- vi.stubGlobal(
311
- "fetch",
312
- vi.fn(async () =>
313
- new Response(
314
- JSON.stringify({ __type: "NotAuthorizedException", message: "nope" }),
315
- { status: 400 },
316
- ),
317
- ),
318
- );
319
- const { mintMachineTokens, CognitoAuthError } = await importModule();
320
- await expect(mintMachineTokens(CONFIG)).rejects.toBeInstanceOf(
321
- CognitoAuthError,
322
- );
323
- expect(
324
- fs.existsSync(path.join(tmpHome, ".hq", "cognito-tokens.json.lock")),
325
- ).toBe(false);
326
-
327
- stubMintFetch();
328
- await expect(mintMachineTokens(CONFIG)).resolves.toBeDefined();
329
- });
330
-
331
- it("throws when no creds are present", async () => {
332
- const { mintMachineTokens, CognitoAuthError } = await importModule();
333
- await expect(mintMachineTokens(CONFIG)).rejects.toBeInstanceOf(
334
- CognitoAuthError,
335
- );
336
- });
337
-
338
- it.each([
339
- ["wrong entity type", { "custom:entityType": "agent", "custom:entityUid": "agt_OTHER" }],
340
- ["wrong entity uid", { "custom:entityUid": "out_OTHER" }],
341
- ["absent delegatedSub", { "custom:delegatedSub": undefined }],
342
- ["empty delegatedSub", { "custom:delegatedSub": "" }],
343
- ["non-string delegatedSub", { "custom:delegatedSub": 42 }],
344
- ["absent delegatedEmail", { "custom:delegatedEmail": undefined }],
345
- ["empty delegatedEmail", { "custom:delegatedEmail": "" }],
346
- ["non-string delegatedEmail", { "custom:delegatedEmail": 42 }],
347
- ] as const)(
348
- "rejects and does not cache a first mint with %s",
349
- async (_label, claimOverride) => {
350
- const username = "out-01test@outposts.getindigo.ai";
351
- writeCreds({
352
- username,
353
- secret: "outpost-secret",
354
- entityType: "outpost",
355
- entityUid: "out_01TEST",
356
- });
357
- const claims: Record<string, unknown> = {
358
- token_use: "id",
359
- aud: CONFIG.clientId,
360
- "custom:entityType": "outpost",
361
- "custom:entityUid": "out_01TEST",
362
- "custom:delegatedSub": "human-sub",
363
- "custom:delegatedEmail": "owner@example.com",
364
- "cognito:username": username,
365
- sub: "machine-sub",
366
- ...claimOverride,
367
- };
368
- for (const [key, value] of Object.entries(claims)) {
369
- if (value === undefined) delete claims[key];
370
- }
371
- stubMintFetch({
372
- AccessToken: fakeJwt({
373
- token_use: "access",
374
- client_id: CONFIG.clientId,
375
- username,
376
- sub: "machine-sub",
377
- }),
378
- IdToken: fakeJwt(claims),
379
- });
380
- const { mintMachineTokens, loadCachedTokens, CognitoAuthError } =
381
- await importModule();
382
-
383
- await expect(mintMachineTokens(CONFIG)).rejects.toBeInstanceOf(
384
- CognitoAuthError,
385
- );
386
- expect(loadCachedTokens()).toBeNull();
387
- },
388
- );
389
- });
390
-
391
- // ---------------------------------------------------------------------------
392
- // getValidMachineTokens — cache vs re-mint
393
- // ---------------------------------------------------------------------------
394
-
395
- describe("getValidMachineTokens", () => {
396
- it("returns the cache when valid without touching the network", async () => {
397
- writeCreds();
398
- const { fetchMock } = stubMintFetch();
399
- const { saveCachedTokens, getValidMachineTokens } = await importModule();
400
- // A valid machine cache must prove this exact agent identity (F06): the
401
- // cached tokens carry the agent claims that distinguish a machine token
402
- // from a same-client human token, so cache reuse is safe and skips the wire.
403
- const cached = {
404
- accessToken: fakeJwt({
405
- token_use: "access",
406
- client_id: CONFIG.clientId,
407
- username: "machine-agt_01TEST",
408
- }),
409
- idToken: fakeJwt({
410
- token_use: "id",
411
- aud: CONFIG.clientId,
412
- "custom:entityType": "agent",
413
- "custom:entityUid": "agt_01TEST",
414
- "cognito:username": "machine-agt_01TEST",
415
- }),
416
- refreshToken: "",
417
- expiresAt: Date.now() + 30 * 60 * 1000,
418
- tokenType: "Bearer" as const,
419
- };
420
- saveCachedTokens(cached);
421
-
422
- const tokens = await getValidMachineTokens(CONFIG);
423
- expect(tokens).toEqual(cached);
424
- expect(fetchMock).not.toHaveBeenCalled();
425
- });
426
-
427
- it("strips and persists an empty refresh token when reusing a machine cache", async () => {
428
- writeCreds();
429
- const { fetchMock } = stubMintFetch();
430
- const { saveCachedTokens, loadCachedTokens, getValidMachineTokens } =
431
- await importModule();
432
- saveCachedTokens({
433
- accessToken: fakeJwt({
434
- token_use: "access",
435
- client_id: CONFIG.clientId,
436
- username: "machine-agt_01TEST",
437
- sub: "machine-sub",
438
- }),
439
- idToken: fakeJwt({
440
- token_use: "id",
441
- aud: CONFIG.clientId,
442
- "custom:entityType": "agent",
443
- "custom:entityUid": "agt_01TEST",
444
- "cognito:username": "machine-agt_01TEST",
445
- sub: "machine-sub",
446
- }),
447
- refreshToken: "legacy-machine-refresh-token",
448
- expiresAt: Date.now() + 30 * 60 * 1000,
449
- tokenType: "Bearer",
450
- });
451
-
452
- const tokens = await getValidMachineTokens(CONFIG);
453
-
454
- expect(tokens.refreshToken).toBe("");
455
- expect(loadCachedTokens()?.refreshToken).toBe("");
456
- expect(fetchMock).not.toHaveBeenCalled();
457
- });
458
-
459
- it("re-mints when the cache is expiring", async () => {
460
- writeCreds();
461
- const { fetchMock } = stubMintFetch();
462
- const { saveCachedTokens, getValidMachineTokens } = await importModule();
463
- saveCachedTokens({
464
- accessToken: fakeJwt({ token_use: "access", client_id: CONFIG.clientId }),
465
- idToken: fakeJwt({ token_use: "id" }),
466
- refreshToken: "",
467
- expiresAt: Date.now() + 10 * 1000,
468
- tokenType: "Bearer",
469
- });
470
-
471
- await getValidMachineTokens(CONFIG);
472
- expect(fetchMock).toHaveBeenCalledTimes(1);
473
- });
474
-
475
- it("re-mints when the cached token targets a different app client", async () => {
476
- writeCreds();
477
- const { fetchMock } = stubMintFetch();
478
- const { saveCachedTokens, getValidMachineTokens } = await importModule();
479
- saveCachedTokens({
480
- accessToken: fakeJwt({ token_use: "access", client_id: "other-client" }),
481
- idToken: fakeJwt({ token_use: "id" }),
482
- refreshToken: "",
483
- expiresAt: Date.now() + 30 * 60 * 1000,
484
- tokenType: "Bearer",
485
- });
486
-
487
- await getValidMachineTokens(CONFIG);
488
- expect(fetchMock).toHaveBeenCalledTimes(1);
489
- });
490
-
491
- it("R-F06: re-mints when cached access and ID tokens belong to different agents", async () => {
492
- writeCreds();
493
- const freshAccess = fakeJwt({
494
- token_use: "access",
495
- client_id: CONFIG.clientId,
496
- username: "machine-agt_01TEST",
497
- sub: "sub-agent-1",
498
- });
499
- const freshId = fakeJwt({
500
- token_use: "id",
501
- aud: CONFIG.clientId,
502
- "custom:entityType": "agent",
503
- "custom:entityUid": "agt_01TEST",
504
- "cognito:username": "machine-agt_01TEST",
505
- sub: "sub-agent-1",
506
- });
507
- const { fetchMock } = stubMintFetch({
508
- AccessToken: freshAccess,
509
- IdToken: freshId,
510
- });
511
- const { saveCachedTokens, getValidAccessToken, loadCachedTokens } =
512
- await importModule();
513
- saveCachedTokens({
514
- accessToken: fakeJwt({
515
- token_use: "access",
516
- client_id: CONFIG.clientId,
517
- username: "machine-agt_01TEST",
518
- sub: "sub-agent-1",
519
- }),
520
- idToken: fakeJwt({
521
- token_use: "id",
522
- aud: CONFIG.clientId,
523
- "custom:entityType": "agent",
524
- "custom:entityUid": "agt_OTHER",
525
- "cognito:username": "machine-agt_OTHER",
526
- sub: "sub-agent-2",
527
- }),
528
- refreshToken: "",
529
- expiresAt: Date.now() + 30 * 60 * 1000,
530
- tokenType: "Bearer",
531
- });
532
-
533
- const token = await getValidAccessToken(CONFIG, { interactive: false });
534
-
535
- expect(fetchMock).toHaveBeenCalledTimes(1);
536
- expect(token).toBe(freshId);
537
- expect(loadCachedTokens()?.idToken).toBe(freshId);
538
- });
539
-
540
- it("accepts an Outpost machine cache with delegated claims, then re-mints after expiry without OAuth refresh", async () => {
541
- const username = "out-01test@outposts.getindigo.ai";
542
- const entityUid = "out_01TEST";
543
- writeCreds({
544
- username,
545
- secret: "outpost-secret",
546
- clientId: CONFIG.clientId,
547
- region: CONFIG.region,
548
- entityType: "outpost",
549
- entityUid,
550
- });
551
- const accessToken = fakeJwt({
552
- token_use: "access",
553
- client_id: CONFIG.clientId,
554
- username,
555
- sub: "machine-sub",
556
- });
557
- const idToken = fakeJwt({
558
- token_use: "id",
559
- aud: CONFIG.clientId,
560
- "custom:entityType": "outpost",
561
- "custom:entityUid": entityUid,
562
- "custom:delegatedSub": "human-sub",
563
- "custom:delegatedEmail": "owner@example.com",
564
- "cognito:username": username,
565
- sub: "machine-sub",
566
- });
567
- const { fetchMock, calls } = stubMintFetch({
568
- AccessToken: accessToken,
569
- IdToken: idToken,
570
- RefreshToken: "must-not-be-stored",
571
- });
572
-
573
- const firstModule = await importModule();
574
- const first = await firstModule.getValidMachineTokens(CONFIG);
575
- expect(first.idToken).toBe(idToken);
576
- expect(firstModule.loadCachedTokens()?.refreshToken).toBe("");
577
-
578
- // A new process accepts the shared Outpost cache without another mint.
579
- vi.resetModules();
580
- const cacheHitModule = await importModule();
581
- await expect(cacheHitModule.getValidMachineTokens(CONFIG)).resolves.toEqual(
582
- first,
583
- );
584
- expect(fetchMock).toHaveBeenCalledTimes(1);
585
-
586
- // Once expired, a new process re-mints with the permanent secret. It never
587
- // attempts the browser client's /oauth2/token refresh-token flow.
588
- cacheHitModule.saveCachedTokens({
589
- ...first,
590
- expiresAt: Date.now() - 1,
591
- });
592
- vi.resetModules();
593
- const expiredModule = await importModule();
594
- await expiredModule.getValidMachineTokens(CONFIG);
595
-
596
- expect(fetchMock).toHaveBeenCalledTimes(2);
597
- expect(calls.every(({ url }) => !url.includes("/oauth2/token"))).toBe(true);
598
- for (const { init } of calls) {
599
- const body = JSON.parse(String(init.body));
600
- expect(body).toMatchObject({
601
- AuthFlow: "USER_PASSWORD_AUTH",
602
- ClientId: CONFIG.clientId,
603
- AuthParameters: { USERNAME: username, PASSWORD: "outpost-secret" },
604
- });
605
- }
606
- expect(expiredModule.loadCachedTokens()?.refreshToken).toBe("");
607
- });
608
-
609
- it.each([
610
- [
611
- "entity type",
612
- { entityType: "outpost", entityUid: "out_01TEST" },
613
- "agent",
614
- "agt_01TEST",
615
- ],
616
- [
617
- "entity uid",
618
- { entityType: "outpost", entityUid: "out_01TEST" },
619
- "outpost",
620
- "out_OTHER",
621
- ],
622
- ] as const)(
623
- "re-mints when the cached machine %s does not match the creds",
624
- async (_label, expectedIdentity, cachedEntityType, cachedEntityUid) => {
625
- const username = "out-01test@outposts.getindigo.ai";
626
- writeCreds({
627
- username,
628
- secret: "outpost-secret",
629
- ...expectedIdentity,
630
- });
631
- const { fetchMock } = stubMintFetch();
632
- const { saveCachedTokens, getValidMachineTokens } = await importModule();
633
- saveCachedTokens({
634
- accessToken: fakeJwt({
635
- token_use: "access",
636
- client_id: CONFIG.clientId,
637
- username,
638
- sub: "machine-sub",
639
- }),
640
- idToken: fakeJwt({
641
- token_use: "id",
642
- aud: CONFIG.clientId,
643
- "custom:entityType": cachedEntityType,
644
- "custom:entityUid": cachedEntityUid,
645
- "custom:delegatedSub": "human-sub",
646
- "custom:delegatedEmail": "owner@example.com",
647
- "cognito:username": username,
648
- sub: "machine-sub",
649
- }),
650
- refreshToken: "",
651
- expiresAt: Date.now() + 30 * 60 * 1000,
652
- tokenType: "Bearer",
653
- });
654
-
655
- await getValidMachineTokens(CONFIG);
656
-
657
- expect(fetchMock).toHaveBeenCalledTimes(1);
658
- },
659
- );
660
-
661
- it.each(["custom:delegatedSub", "custom:delegatedEmail"])(
662
- "re-mints when an Outpost cache is missing %s",
663
- async (missingClaim) => {
664
- const username = "out-01test@outposts.getindigo.ai";
665
- const outpostClaims: Record<string, unknown> = {
666
- token_use: "id",
667
- aud: CONFIG.clientId,
668
- "custom:entityType": "outpost",
669
- "custom:entityUid": "out_01TEST",
670
- "custom:delegatedSub": "human-sub",
671
- "custom:delegatedEmail": "owner@example.com",
672
- "cognito:username": username,
673
- sub: "machine-sub",
674
- };
675
- delete outpostClaims[missingClaim];
676
- writeCreds({
677
- username,
678
- secret: "outpost-secret",
679
- entityType: "outpost",
680
- entityUid: "out_01TEST",
681
- });
682
- const { fetchMock } = stubMintFetch();
683
- const { saveCachedTokens, getValidMachineTokens } = await importModule();
684
- saveCachedTokens({
685
- accessToken: fakeJwt({
686
- token_use: "access",
687
- client_id: CONFIG.clientId,
688
- username,
689
- sub: "machine-sub",
690
- }),
691
- idToken: fakeJwt(outpostClaims),
692
- refreshToken: "",
693
- expiresAt: Date.now() + 30 * 60 * 1000,
694
- tokenType: "Bearer",
695
- });
696
-
697
- await getValidMachineTokens(CONFIG);
698
-
699
- expect(fetchMock).toHaveBeenCalledTimes(1);
700
- },
701
- );
702
-
703
- it("keeps accepting legacy agent creds without entity fields", async () => {
704
- writeCreds({ username: "machine-agt_01TEST", secret: "s3cret" });
705
- const { fetchMock } = stubMintFetch();
706
- const { saveCachedTokens, getValidMachineTokens } = await importModule();
707
- const cached = {
708
- accessToken: fakeJwt({
709
- token_use: "access",
710
- client_id: CONFIG.clientId,
711
- username: "machine-agt_01TEST",
712
- sub: "machine-sub",
713
- }),
714
- idToken: fakeJwt({
715
- token_use: "id",
716
- aud: CONFIG.clientId,
717
- "custom:entityType": "agent",
718
- "custom:entityUid": "agt_01TEST",
719
- "cognito:username": "machine-agt_01TEST",
720
- sub: "machine-sub",
721
- }),
722
- refreshToken: "",
723
- expiresAt: Date.now() + 30 * 60 * 1000,
724
- tokenType: "Bearer" as const,
725
- };
726
- saveCachedTokens(cached);
727
-
728
- await expect(getValidMachineTokens(CONFIG)).resolves.toEqual(cached);
729
- expect(fetchMock).not.toHaveBeenCalled();
730
- });
731
-
732
- it("invalidates the memo after the creds file is reprovisioned", async () => {
733
- writeCreds({
734
- username: "machine-a",
735
- secret: "secret-a",
736
- entityType: "agent",
737
- entityUid: "agt_A",
738
- });
739
- const { fetchMock } = stubMintFetch();
740
- const { getValidMachineTokens } = await importModule();
741
-
742
- const first = await getValidMachineTokens(CONFIG);
743
- writeCreds({
744
- username: "machine-b",
745
- secret: "secret-b",
746
- entityType: "outpost",
747
- entityUid: "out_B",
748
- });
749
- const second = await getValidMachineTokens(CONFIG);
750
-
751
- expect(fetchMock).toHaveBeenCalledTimes(2);
752
- expect(first.idToken).not.toBe(second.idToken);
753
- expect(
754
- JSON.parse(
755
- Buffer.from(second.idToken.split(".")[1], "base64url").toString(),
756
- )["custom:entityUid"],
757
- ).toBe("out_B");
758
- });
759
-
760
- it("invalidates the memo when legacy creds use a different config client", async () => {
761
- writeCreds();
762
- const { fetchMock } = stubMintFetch();
763
- const { getValidMachineTokens } = await importModule();
764
- const otherConfig = { ...CONFIG, clientId: "other-client-id" };
765
-
766
- const first = await getValidMachineTokens(CONFIG);
767
- const second = await getValidMachineTokens(otherConfig);
768
-
769
- expect(fetchMock).toHaveBeenCalledTimes(2);
770
- expect(first.idToken).not.toBe(second.idToken);
771
- expect(
772
- JSON.parse(
773
- Buffer.from(second.idToken.split(".")[1], "base64url").toString(),
774
- ).aud,
775
- ).toBe(otherConfig.clientId);
776
- });
777
-
778
- it("does not attach a reprovisioned identity to the previous identity's in-flight mint", async () => {
779
- writeCreds({
780
- username: "machine-a",
781
- secret: "secret-a",
782
- entityType: "agent",
783
- entityUid: "agt_A",
784
- });
785
- let releaseFirstMint: (() => void) | undefined;
786
- const firstMintGate = new Promise<void>((resolve) => {
787
- releaseFirstMint = resolve;
788
- });
789
- let signalFirstMint: (() => void) | undefined;
790
- const firstMintStarted = new Promise<void>((resolve) => {
791
- signalFirstMint = resolve;
792
- });
793
- let callCount = 0;
794
- const fetchMock = vi.fn(async (_url: string | URL, init?: RequestInit) => {
795
- callCount += 1;
796
- const body = JSON.parse(String(init?.body)) as {
797
- ClientId: string;
798
- AuthParameters: { USERNAME: string };
799
- };
800
- if (callCount === 1) {
801
- signalFirstMint?.();
802
- await firstMintGate;
803
- }
804
- const username = body.AuthParameters.USERNAME;
805
- const uid = username === "machine-a" ? "agt_A" : "agt_B";
806
- return new Response(
807
- JSON.stringify({
808
- AuthenticationResult: {
809
- AccessToken: fakeJwt({
810
- token_use: "access",
811
- client_id: body.ClientId,
812
- username,
813
- sub: `sub:${username}`,
814
- }),
815
- IdToken: fakeJwt({
816
- token_use: "id",
817
- aud: body.ClientId,
818
- "custom:entityType": "agent",
819
- "custom:entityUid": uid,
820
- "cognito:username": username,
821
- sub: `sub:${username}`,
822
- }),
823
- ExpiresIn: 3600,
824
- },
825
- }),
826
- { status: 200 },
827
- );
828
- });
829
- vi.stubGlobal("fetch", fetchMock);
830
- const { getValidMachineTokens } = await importModule();
831
-
832
- const first = getValidMachineTokens(CONFIG);
833
- await firstMintStarted;
834
- writeCreds({
835
- username: "machine-b",
836
- secret: "secret-b",
837
- entityType: "agent",
838
- entityUid: "agt_B",
839
- });
840
- const second = getValidMachineTokens(CONFIG);
841
- releaseFirstMint?.();
842
-
843
- const [firstTokens, secondTokens] = await Promise.all([first, second]);
844
- expect(fetchMock).toHaveBeenCalledTimes(2);
845
- expect(firstTokens.idToken).not.toBe(secondTokens.idToken);
846
- expect(
847
- JSON.parse(
848
- Buffer.from(secondTokens.idToken.split(".")[1], "base64url").toString(),
849
- )["custom:entityUid"],
850
- ).toBe("agt_B");
851
- });
852
-
853
- it("reuses only an empty-refresh matching cache after a lock timeout", async () => {
854
- writeCreds();
855
- const { fetchMock } = stubMintFetch();
856
- const { saveCachedTokens, getValidMachineTokens } = await importModule();
857
- const cached = {
858
- accessToken: fakeJwt({
859
- token_use: "access",
860
- client_id: CONFIG.clientId,
861
- username: "machine-agt_01TEST",
862
- sub: "machine-sub",
863
- }),
864
- idToken: fakeJwt({
865
- token_use: "id",
866
- aud: CONFIG.clientId,
867
- "custom:entityType": "agent",
868
- "custom:entityUid": "agt_01TEST",
869
- "cognito:username": "machine-agt_01TEST",
870
- sub: "machine-sub",
871
- }),
872
- refreshToken: "",
873
- expiresAt: Date.now() + 30 * 60 * 1000,
874
- tokenType: "Bearer" as const,
875
- };
876
- saveCachedTokens(cached);
877
- fs.writeFileSync(
878
- path.join(tmpHome, ".hq", "cognito-tokens.json.lock"),
879
- String(process.pid),
880
- );
881
- vi.useFakeTimers();
882
-
883
- const resolution = getValidMachineTokens(CONFIG);
884
- await vi.advanceTimersByTimeAsync(20_050);
885
-
886
- await expect(resolution).resolves.toEqual(cached);
887
- expect(fetchMock).not.toHaveBeenCalled();
888
- });
889
-
890
- it("rejects a legacy non-empty-refresh cache after a lock timeout without minting unlocked", async () => {
891
- writeCreds();
892
- const { fetchMock } = stubMintFetch();
893
- const {
894
- saveCachedTokens,
895
- getValidMachineTokens,
896
- CognitoAuthError,
897
- } = await importModule();
898
- saveCachedTokens({
899
- accessToken: fakeJwt({
900
- token_use: "access",
901
- client_id: CONFIG.clientId,
902
- username: "machine-agt_01TEST",
903
- sub: "machine-sub",
904
- }),
905
- idToken: fakeJwt({
906
- token_use: "id",
907
- aud: CONFIG.clientId,
908
- "custom:entityType": "agent",
909
- "custom:entityUid": "agt_01TEST",
910
- "cognito:username": "machine-agt_01TEST",
911
- sub: "machine-sub",
912
- }),
913
- refreshToken: "legacy-refresh",
914
- expiresAt: Date.now() + 30 * 60 * 1000,
915
- tokenType: "Bearer",
916
- });
917
- fs.writeFileSync(
918
- path.join(tmpHome, ".hq", "cognito-tokens.json.lock"),
919
- String(process.pid),
920
- );
921
- vi.useFakeTimers();
922
-
923
- const resolution = getValidMachineTokens(CONFIG);
924
- const rejection = expect(resolution).rejects.toBeInstanceOf(
925
- CognitoAuthError,
926
- );
927
- await vi.advanceTimersByTimeAsync(20_050);
928
-
929
- await rejection;
930
- expect(fetchMock).not.toHaveBeenCalled();
931
- });
932
- });
933
-
934
- // ---------------------------------------------------------------------------
935
- // Regression: constant token mints over N files (sync-runner mint storm)
936
- // ---------------------------------------------------------------------------
937
- //
938
- // `getValidAccessToken` (→ getValidMachineTokens) is invoked on EVERY vault
939
- // request a sync makes, including the per-file tombstone HEAD verifies. The
940
- // 2026-06-26 prod incident was 56 throttled USER_PASSWORD_AUTH mints over 314
941
- // files on one box: with no in-process memo + single-flight, mints scaled with
942
- // request count and tripped Cognito's per-user limit. These lock the invariant
943
- // that mints are O(1) per process, independent of N.
944
-
945
- describe("regression: constant mints regardless of file count", () => {
946
- it.each([10, 300])(
947
- "performs exactly ONE token mint across %i sequential file-verify requests",
948
- async (n) => {
949
- writeCreds();
950
- const { fetchMock } = stubMintFetch();
951
- const { getValidMachineTokens } = await importModule();
952
-
953
- for (let i = 0; i < n; i++) {
954
- await getValidMachineTokens(CONFIG);
955
- }
956
-
957
- // One mint for N requests — NOT proportional to N. The first call mints;
958
- // every later call is served by the in-process memo (no network).
959
- expect(fetchMock).toHaveBeenCalledTimes(1);
960
- },
961
- );
962
-
963
- it.each([10, 300])(
964
- "collapses a concurrent burst of %i requests onto a SINGLE mint (single-flight)",
965
- async (n) => {
966
- writeCreds();
967
- const { fetchMock } = stubMintFetch();
968
- const { getValidMachineTokens } = await importModule();
969
-
970
- // Fire all N at once, as the run-start fan-out does. Without single-flight
971
- // every concurrent miss would mint and burst the per-user limit.
972
- const results = await Promise.all(
973
- Array.from({ length: n }, () => getValidMachineTokens(CONFIG)),
974
- );
975
-
976
- expect(fetchMock).toHaveBeenCalledTimes(1);
977
- // All callers observe the same token.
978
- for (const r of results) expect(r).toEqual(results[0]);
979
-
980
- // Follow-up sequential requests stay on the memo — still one mint total.
981
- await getValidMachineTokens(CONFIG);
982
- expect(fetchMock).toHaveBeenCalledTimes(1);
983
- },
984
- );
985
-
986
- it("serializes independent module instances through the shared cache lock", async () => {
987
- const username = "machine-agt_01TEST";
988
- writeCreds({
989
- username,
990
- secret: "s3cret",
991
- entityType: "agent",
992
- entityUid: "agt_01TEST",
993
- });
994
- let releaseMint: (() => void) | undefined;
995
- const mintGate = new Promise<void>((resolve) => {
996
- releaseMint = resolve;
997
- });
998
- let signalMintStarted: (() => void) | undefined;
999
- const mintStarted = new Promise<void>((resolve) => {
1000
- signalMintStarted = resolve;
1001
- });
1002
- const accessToken = fakeJwt({
1003
- token_use: "access",
1004
- client_id: CONFIG.clientId,
1005
- username,
1006
- sub: "machine-sub",
1007
- });
1008
- const idToken = fakeJwt({
1009
- token_use: "id",
1010
- aud: CONFIG.clientId,
1011
- "custom:entityType": "agent",
1012
- "custom:entityUid": "agt_01TEST",
1013
- "cognito:username": username,
1014
- sub: "machine-sub",
1015
- });
1016
- const fetchMock = vi.fn(async () => {
1017
- signalMintStarted?.();
1018
- await mintGate;
1019
- return new Response(
1020
- JSON.stringify({
1021
- AuthenticationResult: {
1022
- AccessToken: accessToken,
1023
- IdToken: idToken,
1024
- RefreshToken: "must-not-be-stored",
1025
- ExpiresIn: 3600,
1026
- },
1027
- }),
1028
- { status: 200 },
1029
- );
1030
- });
1031
- vi.stubGlobal("fetch", fetchMock);
1032
-
1033
- const firstProcessModule = await importModule();
1034
- vi.resetModules();
1035
- const secondProcessModule = await importModule();
1036
-
1037
- const first = firstProcessModule.getValidMachineTokens(CONFIG);
1038
- await mintStarted;
1039
- const second = secondProcessModule.getValidMachineTokens(CONFIG);
1040
- releaseMint?.();
1041
-
1042
- const [firstTokens, secondTokens] = await Promise.all([first, second]);
1043
- expect(fetchMock).toHaveBeenCalledTimes(1);
1044
- expect(secondTokens).toEqual(firstTokens);
1045
- expect(secondProcessModule.loadCachedTokens()?.refreshToken).toBe("");
1046
- });
1047
-
1048
- it("serializes the exported direct mint API", async () => {
1049
- writeCreds();
1050
- let releaseFirstMint: (() => void) | undefined;
1051
- const firstMintGate = new Promise<void>((resolve) => {
1052
- releaseFirstMint = resolve;
1053
- });
1054
- let signalFirstMint: (() => void) | undefined;
1055
- const firstMintStarted = new Promise<void>((resolve) => {
1056
- signalFirstMint = resolve;
1057
- });
1058
- let activeMints = 0;
1059
- let maxActiveMints = 0;
1060
- let callCount = 0;
1061
- const accessToken = fakeJwt({
1062
- token_use: "access",
1063
- client_id: CONFIG.clientId,
1064
- username: "machine-agt_01TEST",
1065
- sub: "machine-sub",
1066
- });
1067
- const idToken = fakeJwt({
1068
- token_use: "id",
1069
- aud: CONFIG.clientId,
1070
- "custom:entityType": "agent",
1071
- "custom:entityUid": "agt_01TEST",
1072
- "cognito:username": "machine-agt_01TEST",
1073
- sub: "machine-sub",
1074
- });
1075
- const fetchMock = vi.fn(async () => {
1076
- callCount += 1;
1077
- activeMints += 1;
1078
- maxActiveMints = Math.max(maxActiveMints, activeMints);
1079
- if (callCount === 1) {
1080
- signalFirstMint?.();
1081
- await firstMintGate;
1082
- }
1083
- activeMints -= 1;
1084
- return new Response(
1085
- JSON.stringify({
1086
- AuthenticationResult: {
1087
- AccessToken: accessToken,
1088
- IdToken: idToken,
1089
- ExpiresIn: 3600,
1090
- },
1091
- }),
1092
- { status: 200 },
1093
- );
1094
- });
1095
- vi.stubGlobal("fetch", fetchMock);
1096
- const firstModule = await importModule();
1097
- vi.resetModules();
1098
- const secondModule = await importModule();
1099
-
1100
- const first = firstModule.mintMachineTokens(CONFIG);
1101
- await firstMintStarted;
1102
- const second = secondModule.mintMachineTokens(CONFIG);
1103
- releaseFirstMint?.();
1104
- await Promise.all([first, second]);
1105
-
1106
- expect(fetchMock).toHaveBeenCalledTimes(2);
1107
- expect(maxActiveMints).toBe(1);
1108
- });
1109
-
1110
- it("serializes direct mints in actual separate processes", async () => {
1111
- const moduleUrl = pathToFileURL(
1112
- path.join(process.cwd(), "src", "cognito-auth.ts"),
1113
- ).href;
1114
- const marker = path.join(tmpHome, "mint-active");
1115
- const overlap = path.join(tmpHome, "mint-overlap");
1116
- const start = path.join(tmpHome, "mint-start");
1117
- const childScript = String.raw`
1118
- import fs from "fs";
1119
- const pause = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
1120
- const fakeJwt = (claims) => {
1121
- const enc = (value) => Buffer.from(JSON.stringify(value)).toString("base64url");
1122
- return enc({ alg: "none" }) + "." + enc(claims) + ".sig";
1123
- };
1124
- const auth = await import(process.env.HQ_LOCK_TEST_MODULE_URL);
1125
- fs.writeFileSync(process.env.HQ_LOCK_TEST_READY, "ready");
1126
- while (!fs.existsSync(process.env.HQ_LOCK_TEST_START)) await pause(5);
1127
- globalThis.fetch = async () => {
1128
- let ownsMarker = false;
1129
- try {
1130
- const fd = fs.openSync(process.env.HQ_LOCK_TEST_MARKER, "wx");
1131
- fs.closeSync(fd);
1132
- ownsMarker = true;
1133
- } catch (error) {
1134
- if (error.code === "EEXIST") {
1135
- fs.writeFileSync(process.env.HQ_LOCK_TEST_OVERLAP, "overlap");
1136
- } else {
1137
- throw error;
1138
- }
1139
- }
1140
- await pause(125);
1141
- if (ownsMarker) fs.unlinkSync(process.env.HQ_LOCK_TEST_MARKER);
1142
- return new Response(JSON.stringify({ AuthenticationResult: {
1143
- AccessToken: fakeJwt({ token_use: "access", client_id: "test-client-id", username: "machine", sub: "sub" }),
1144
- IdToken: fakeJwt({ token_use: "id", aud: "test-client-id", "custom:entityType": "agent", "custom:entityUid": "agt_TEST", "cognito:username": "machine", sub: "sub" }),
1145
- ExpiresIn: 3600
1146
- }}), { status: 200 });
1147
- };
1148
- await auth.mintMachineTokens(
1149
- { region: "us-east-1", userPoolDomain: "unused", clientId: "test-client-id" },
1150
- { username: "machine", secret: "secret", entityType: "agent", entityUid: "agt_TEST" },
1151
- { baseDelayMs: 0 }
1152
- );
1153
- `;
1154
- const children = ["a", "b"].map((name) => {
1155
- const child = spawn(
1156
- process.execPath,
1157
- ["--experimental-strip-types", "--input-type=module", "-e", childScript],
1158
- {
1159
- cwd: process.cwd(),
1160
- env: {
1161
- ...process.env,
1162
- HOME: tmpHome,
1163
- HQ_LOCK_TEST_MODULE_URL: moduleUrl,
1164
- HQ_LOCK_TEST_READY: path.join(tmpHome, `ready-${name}`),
1165
- HQ_LOCK_TEST_START: start,
1166
- HQ_LOCK_TEST_MARKER: marker,
1167
- HQ_LOCK_TEST_OVERLAP: overlap,
1168
- },
1169
- },
1170
- );
1171
- let stderr = "";
1172
- child.stderr.on("data", (chunk) => {
1173
- stderr += String(chunk);
1174
- });
1175
- return { child, stderr: () => stderr };
1176
- });
1177
- const readyPaths = ["a", "b"].map((name) =>
1178
- path.join(tmpHome, `ready-${name}`),
1179
- );
1180
- const readyDeadline = Date.now() + 5_000;
1181
- while (
1182
- !readyPaths.every((readyPath) => fs.existsSync(readyPath)) &&
1183
- Date.now() < readyDeadline
1184
- ) {
1185
- await new Promise((resolve) => setTimeout(resolve, 10));
1186
- }
1187
- expect(readyPaths.every((readyPath) => fs.existsSync(readyPath))).toBe(true);
1188
- fs.writeFileSync(start, "start");
1189
-
1190
- const exits = await Promise.all(
1191
- children.map(
1192
- ({ child, stderr }) =>
1193
- new Promise<{ code: number | null; stderr: string }>((resolve) => {
1194
- child.on("exit", (code) => resolve({ code, stderr: stderr() }));
1195
- }),
1196
- ),
1197
- );
1198
-
1199
- expect(exits).toEqual([
1200
- { code: 0, stderr: "" },
1201
- { code: 0, stderr: "" },
1202
- ]);
1203
- expect(fs.existsSync(overlap)).toBe(false);
1204
- }, 10_000);
1205
- });
1206
-
1207
- // ---------------------------------------------------------------------------
1208
- // Throttle handling: retry TooManyRequestsException with backoff
1209
- // ---------------------------------------------------------------------------
1210
-
1211
- describe("mintMachineTokens throttle retry", () => {
1212
- /** Fetch mock that throttles (HTTP 400 + TooManyRequestsException, the shape
1213
- * Cognito actually returns) for the first `throttleCount` calls, then 200s. */
1214
- function stubThrottleThenSuccess(throttleCount: number) {
1215
- let call = 0;
1216
- const fetchMock = vi.fn(async () => {
1217
- call++;
1218
- if (call <= throttleCount) {
1219
- return new Response(
1220
- JSON.stringify({
1221
- __type: "TooManyRequestsException",
1222
- message: "Too many requests",
1223
- }),
1224
- { status: 400 },
1225
- );
1226
- }
1227
- return new Response(
1228
- JSON.stringify({
1229
- AuthenticationResult: {
1230
- AccessToken: fakeJwt({
1231
- token_use: "access",
1232
- client_id: CONFIG.clientId,
1233
- username: "machine-agt_01TEST",
1234
- sub: "machine-sub",
1235
- }),
1236
- IdToken: fakeJwt({
1237
- token_use: "id",
1238
- aud: CONFIG.clientId,
1239
- "custom:entityType": "agent",
1240
- "custom:entityUid": "agt_01TEST",
1241
- "cognito:username": "machine-agt_01TEST",
1242
- sub: "machine-sub",
1243
- }),
1244
- ExpiresIn: 3600,
1245
- },
1246
- }),
1247
- { status: 200 },
1248
- );
1249
- });
1250
- vi.stubGlobal("fetch", fetchMock);
1251
- return fetchMock;
1252
- }
1253
-
1254
- it("retries a throttled mint with backoff and eventually succeeds", async () => {
1255
- writeCreds();
1256
- const fetchMock = stubThrottleThenSuccess(2);
1257
- const { mintMachineTokens } = await importModule();
1258
-
1259
- // baseDelayMs: 0 keeps the test instant — jitter * 0 == 0.
1260
- const tokens = await mintMachineTokens(CONFIG, undefined, {
1261
- baseDelayMs: 0,
1262
- maxRetries: 5,
1263
- });
1264
-
1265
- expect(fetchMock).toHaveBeenCalledTimes(3); // 2 throttles + 1 success
1266
- const idClaims = JSON.parse(
1267
- Buffer.from(tokens.idToken.split(".")[1], "base64url").toString(),
1268
- );
1269
- expect(idClaims.token_use).toBe("id");
1270
- });
1271
-
1272
- it("gives up after maxRetries and throws when throttling never clears", async () => {
1273
- writeCreds();
1274
- const fetchMock = stubThrottleThenSuccess(Number.POSITIVE_INFINITY);
1275
- const { mintMachineTokens, CognitoAuthError } = await importModule();
1276
-
1277
- await expect(
1278
- mintMachineTokens(CONFIG, undefined, { baseDelayMs: 0, maxRetries: 2 }),
1279
- ).rejects.toBeInstanceOf(CognitoAuthError);
1280
-
1281
- // maxRetries: 2 → 3 total attempts (initial + 2 retries).
1282
- expect(fetchMock).toHaveBeenCalledTimes(3);
1283
- });
1284
-
1285
- it("does NOT retry a non-throttle auth failure (bad creds fail fast)", async () => {
1286
- writeCreds();
1287
- const fetchMock = vi.fn(async () =>
1288
- new Response(
1289
- JSON.stringify({ __type: "NotAuthorizedException", message: "nope" }),
1290
- { status: 400 },
1291
- ),
1292
- );
1293
- vi.stubGlobal("fetch", fetchMock);
1294
- const { mintMachineTokens, CognitoAuthError } = await importModule();
1295
-
1296
- await expect(
1297
- mintMachineTokens(CONFIG, undefined, { baseDelayMs: 0, maxRetries: 5 }),
1298
- ).rejects.toBeInstanceOf(CognitoAuthError);
1299
- expect(fetchMock).toHaveBeenCalledTimes(1); // no retries
1300
- });
1301
- });
1302
-
1303
- // ---------------------------------------------------------------------------
1304
- // getValidAccessToken — machine-mode short circuit
1305
- // ---------------------------------------------------------------------------
1306
-
1307
- describe("getValidAccessToken in machine mode", () => {
1308
- it("mints via machine creds and returns the ID token (vault-API identity)", async () => {
1309
- writeCreds();
1310
- const { calls } = stubMintFetch();
1311
- const { getValidAccessToken } = await importModule();
1312
-
1313
- const token = await getValidAccessToken(CONFIG, { interactive: false });
1314
-
1315
- expect(calls).toHaveLength(1);
1316
- const claims = JSON.parse(
1317
- Buffer.from(token.split(".")[1], "base64url").toString(),
1318
- );
1319
- // ID, NOT access: agent identity rides the ID token; the access-token
1320
- // return lobotomized first-sync on the first post-cutover box.
1321
- expect(claims.token_use).toBe("id");
1322
- });
1323
- });