@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,583 +0,0 @@
1
- /**
2
- * Unit tests for context.ts — entity context resolution (VLT-5 US-001).
3
- *
4
- * Uses a mock fetch to simulate vault-service API responses.
5
- */
6
-
7
- import { describe, it, expect, beforeEach, vi } from "vitest";
8
- import {
9
- resolveEntityContext,
10
- refreshEntityContext,
11
- clearContextCache,
12
- isExpiringSoon,
13
- } from "./context.js";
14
- import type { VaultServiceConfig } from "./types.js";
15
-
16
- const mockConfig: VaultServiceConfig = {
17
- apiUrl: "https://vault-api.test",
18
- authToken: "test-jwt-token",
19
- region: "us-east-1",
20
- };
21
-
22
- const mockEntity = {
23
- uid: "cmp_01ABCDEF",
24
- slug: "acme",
25
- bucketName: "hq-vault-acme-123",
26
- status: "active",
27
- };
28
-
29
- const mockVendResponse = {
30
- credentials: {
31
- accessKeyId: "ASIA_TEST_KEY",
32
- secretAccessKey: "test-secret",
33
- sessionToken: "test-session-token",
34
- expiration: new Date(Date.now() + 15 * 60 * 1000).toISOString(),
35
- },
36
- expiresAt: new Date(Date.now() + 15 * 60 * 1000).toISOString(),
37
- };
38
-
39
- function setupFetchMock(overrides?: {
40
- entityStatus?: number;
41
- entityBody?: unknown;
42
- vendStatus?: number;
43
- vendBody?: unknown;
44
- }) {
45
- const fetchMock = vi.fn();
46
-
47
- fetchMock.mockImplementation(async (url: string) => {
48
- const urlStr = String(url);
49
-
50
- // New per-user-namespace slug resolver (hq-pro PR 67). Maps slug
51
- // lookups to `{available: false, conflictingCompanyUid}` so the
52
- // caller follows up with `/entity/{uid}`, which lands in the
53
- // `/entity/cmp_` branch below — that's where `entityBody` and
54
- // `entityStatus` overrides apply. The check-slug branch only
55
- // honors `entityStatus` (so tests can simulate a 404/500 on the
56
- // namespace lookup itself); its response shape stays fixed.
57
- if (urlStr.includes("/entity/check-slug/me")) {
58
- return {
59
- ok: (overrides?.entityStatus ?? 200) < 400,
60
- status: overrides?.entityStatus ?? 200,
61
- json: async () => ({
62
- available: false,
63
- conflictingCompanyUid: mockEntity.uid,
64
- }),
65
- text: async () =>
66
- JSON.stringify({
67
- available: false,
68
- conflictingCompanyUid: mockEntity.uid,
69
- }),
70
- };
71
- }
72
-
73
- // Kept for any tests that still mock the legacy global endpoint
74
- // directly (none should, post-PR 67 — but harmless if invoked).
75
- if (urlStr.includes("/entity/by-slug/")) {
76
- return {
77
- ok: (overrides?.entityStatus ?? 200) < 400,
78
- status: overrides?.entityStatus ?? 200,
79
- json: async () => overrides?.entityBody ?? { entity: mockEntity },
80
- text: async () => JSON.stringify(overrides?.entityBody ?? { entity: mockEntity }),
81
- };
82
- }
83
-
84
- if (urlStr.includes("/entity/cmp_")) {
85
- return {
86
- ok: (overrides?.entityStatus ?? 200) < 400,
87
- status: overrides?.entityStatus ?? 200,
88
- json: async () => overrides?.entityBody ?? { entity: mockEntity },
89
- text: async () => JSON.stringify(overrides?.entityBody ?? { entity: mockEntity }),
90
- };
91
- }
92
-
93
- if (urlStr.includes("/sts/vend")) {
94
- return {
95
- ok: (overrides?.vendStatus ?? 200) < 400,
96
- status: overrides?.vendStatus ?? 200,
97
- json: async () => overrides?.vendBody ?? mockVendResponse,
98
- text: async () => JSON.stringify(overrides?.vendBody ?? mockVendResponse),
99
- };
100
- }
101
-
102
- return { ok: false, status: 404, text: async () => "Not found" };
103
- });
104
-
105
- vi.stubGlobal("fetch", fetchMock);
106
- return fetchMock;
107
- }
108
-
109
- describe("resolveEntityContext", () => {
110
- beforeEach(() => {
111
- clearContextCache();
112
- vi.restoreAllMocks();
113
- });
114
-
115
- it("resolves context by slug", async () => {
116
- const fetchMock = setupFetchMock();
117
-
118
- const ctx = await resolveEntityContext("acme", mockConfig);
119
-
120
- expect(ctx.uid).toBe("cmp_01ABCDEF");
121
- expect(ctx.bucketName).toBe("hq-vault-acme-123");
122
- expect(ctx.credentials?.accessKeyId).toBe("ASIA_TEST_KEY");
123
- expect(ctx.region).toBe("us-east-1");
124
-
125
- // Verify entity lookup used the new per-user-namespace endpoint
126
- // (PR 67) + a follow-up `/entity/{uid}` materialization + STS.
127
- expect(fetchMock).toHaveBeenCalledTimes(3);
128
- expect(String(fetchMock.mock.calls[0][0])).toContain(
129
- "/entity/check-slug/me?type=company&slug=acme",
130
- );
131
- expect(String(fetchMock.mock.calls[1][0])).toContain(
132
- `/entity/${mockEntity.uid}`,
133
- );
134
- expect(String(fetchMock.mock.calls[2][0])).toContain("/sts/vend");
135
- });
136
-
137
- it("resolves context by UID directly", async () => {
138
- const fetchMock = setupFetchMock();
139
-
140
- const ctx = await resolveEntityContext("cmp_01ABCDEF", mockConfig);
141
-
142
- expect(ctx.uid).toBe("cmp_01ABCDEF");
143
- // Verify entity lookup used direct UID endpoint
144
- expect(String(fetchMock.mock.calls[0][0])).toContain("/entity/cmp_01ABCDEF");
145
- });
146
-
147
- it("returns cached context when credentials are fresh", async () => {
148
- const fetchMock = setupFetchMock();
149
-
150
- const ctx1 = await resolveEntityContext("acme", mockConfig);
151
- const ctx2 = await resolveEntityContext("acme", mockConfig);
152
-
153
- expect(ctx1).toBe(ctx2); // Same reference
154
- // 3 fetches per resolve under the new model: check-slug + entity.get + sts/vend.
155
- // 1 resolve here (second call hits cache, no new fetches).
156
- expect(fetchMock).toHaveBeenCalledTimes(3);
157
- });
158
-
159
- it("auto-refreshes when credentials expire soon", async () => {
160
- const almostExpired = new Date(Date.now() + 60 * 1000).toISOString(); // 1 min left
161
- const fetchMock = setupFetchMock({
162
- vendBody: {
163
- credentials: mockVendResponse.credentials,
164
- expiresAt: almostExpired,
165
- },
166
- });
167
-
168
- const ctx1 = await resolveEntityContext("acme", mockConfig);
169
-
170
- // Second call should refresh because <2 min remaining
171
- const ctx2 = await resolveEntityContext("acme", mockConfig);
172
- expect(ctx2).not.toBe(ctx1);
173
- // 2 resolves × 3 fetches each = 6 under the new model.
174
- expect(fetchMock).toHaveBeenCalledTimes(6);
175
- });
176
-
177
- it("throws when entity has no bucket", async () => {
178
- setupFetchMock({
179
- entityBody: { entity: { ...mockEntity, bucketName: undefined } },
180
- });
181
-
182
- await expect(resolveEntityContext("acme", mockConfig)).rejects.toThrow(
183
- /no bucket provisioned/,
184
- );
185
- });
186
-
187
- it("throws on entity lookup failure", async () => {
188
- setupFetchMock({ entityStatus: 404 });
189
-
190
- // The namespace lookup fails first under the new model — error
191
- // message now reflects "Failed to check slug" before the
192
- // entity.get(uid) step is reached.
193
- await expect(resolveEntityContext("nonexistent", mockConfig)).rejects.toThrow(
194
- /Failed to check slug/,
195
- );
196
- });
197
-
198
- it("throws on STS vend failure", async () => {
199
- setupFetchMock({ vendStatus: 403 });
200
-
201
- await expect(resolveEntityContext("acme", mockConfig)).rejects.toThrow(
202
- /STS.*vend.*failed/,
203
- );
204
- });
205
- });
206
-
207
- describe("routing by UID prefix and vend-self dispatch", () => {
208
- beforeEach(() => {
209
- clearContextCache();
210
- vi.restoreAllMocks();
211
- });
212
-
213
- it("prs_* UID: entity resolved via /entity/{uid} and credentials via /sts/vend-self", async () => {
214
- const prsEntity = {
215
- uid: "prs_01PERSON",
216
- slug: "test-person",
217
- bucketName: "hq-vault-prs-01person",
218
- status: "active",
219
- };
220
- const calls: string[] = [];
221
- vi.stubGlobal("fetch", vi.fn().mockImplementation(async (url: string) => {
222
- const u = String(url);
223
- calls.push(u);
224
- if (u.includes("/entity/prs_")) {
225
- return { ok: true, status: 200, json: async () => ({ entity: prsEntity }), text: async () => "" };
226
- }
227
- if (u.includes("/sts/vend-self")) {
228
- return { ok: true, status: 200, json: async () => mockVendResponse, text: async () => "" };
229
- }
230
- return { ok: false, status: 404, text: async () => "Not found" };
231
- }));
232
-
233
- await resolveEntityContext("prs_01PERSON", mockConfig);
234
-
235
- expect(calls.some((u) => u.includes("/entity/prs_01PERSON"))).toBe(true);
236
- const vendCalls = calls.filter((u) => u.includes("/sts/vend"));
237
- expect(vendCalls).toHaveLength(1);
238
- expect(vendCalls[0]).toContain("/sts/vend-self");
239
- });
240
-
241
- it("foo_bar slug: entity resolved via /entity/check-slug/me + /entity/<uid> and credentials via /sts/vend", async () => {
242
- const calls: string[] = [];
243
- vi.stubGlobal("fetch", vi.fn().mockImplementation(async (url: string) => {
244
- const u = String(url);
245
- calls.push(u);
246
- if (u.includes("/entity/check-slug/me")) {
247
- return {
248
- ok: true,
249
- status: 200,
250
- json: async () => ({ available: false, conflictingCompanyUid: mockEntity.uid }),
251
- text: async () => "",
252
- };
253
- }
254
- if (u.includes(`/entity/${mockEntity.uid}`)) {
255
- return { ok: true, status: 200, json: async () => ({ entity: mockEntity }), text: async () => "" };
256
- }
257
- if (u.includes("/sts/vend")) {
258
- return { ok: true, status: 200, json: async () => mockVendResponse, text: async () => "" };
259
- }
260
- return { ok: false, status: 404, text: async () => "Not found" };
261
- }));
262
-
263
- await resolveEntityContext("foo_bar", mockConfig);
264
-
265
- expect(
266
- calls.some((u) => u.includes("/entity/check-slug/me?type=company&slug=foo_bar")),
267
- ).toBe(true);
268
- const vendCalls = calls.filter((u) => u.includes("/sts/vend"));
269
- expect(vendCalls).toHaveLength(1);
270
- expect(vendCalls[0]).not.toContain("/sts/vend-self");
271
- expect(vendCalls[0]).toContain("/sts/vend");
272
- });
273
-
274
- it("team_alpha slug: entity resolved via /entity/check-slug/me + /entity/<uid> and credentials via /sts/vend", async () => {
275
- const calls: string[] = [];
276
- vi.stubGlobal("fetch", vi.fn().mockImplementation(async (url: string) => {
277
- const u = String(url);
278
- calls.push(u);
279
- if (u.includes("/entity/check-slug/me")) {
280
- return {
281
- ok: true,
282
- status: 200,
283
- json: async () => ({ available: false, conflictingCompanyUid: mockEntity.uid }),
284
- text: async () => "",
285
- };
286
- }
287
- if (u.includes(`/entity/${mockEntity.uid}`)) {
288
- return { ok: true, status: 200, json: async () => ({ entity: mockEntity }), text: async () => "" };
289
- }
290
- if (u.includes("/sts/vend")) {
291
- return { ok: true, status: 200, json: async () => mockVendResponse, text: async () => "" };
292
- }
293
- return { ok: false, status: 404, text: async () => "Not found" };
294
- }));
295
-
296
- await resolveEntityContext("team_alpha", mockConfig);
297
-
298
- expect(
299
- calls.some((u) => u.includes("/entity/check-slug/me?type=company&slug=team_alpha")),
300
- ).toBe(true);
301
- const vendCalls = calls.filter((u) => u.includes("/sts/vend"));
302
- expect(vendCalls).toHaveLength(1);
303
- expect(vendCalls[0]).not.toContain("/sts/vend-self");
304
- });
305
-
306
- it("cmp_* UID: entity resolved via /entity/{uid} and credentials via /sts/vend", async () => {
307
- const calls: string[] = [];
308
- vi.stubGlobal("fetch", vi.fn().mockImplementation(async (url: string) => {
309
- const u = String(url);
310
- calls.push(u);
311
- if (u.includes("/entity/cmp_")) {
312
- return { ok: true, status: 200, json: async () => ({ entity: mockEntity }), text: async () => "" };
313
- }
314
- if (u.includes("/sts/vend")) {
315
- return { ok: true, status: 200, json: async () => mockVendResponse, text: async () => "" };
316
- }
317
- return { ok: false, status: 404, text: async () => "Not found" };
318
- }));
319
-
320
- await resolveEntityContext("cmp_01ABCDEF", mockConfig);
321
-
322
- expect(calls.some((u) => u.includes("/entity/cmp_01ABCDEF"))).toBe(true);
323
- const vendCalls = calls.filter((u) => u.includes("/sts/vend"));
324
- expect(vendCalls).toHaveLength(1);
325
- expect(vendCalls[0]).not.toContain("/sts/vend-self");
326
- expect(vendCalls[0]).toContain("/sts/vend");
327
- });
328
- });
329
-
330
- // ---------------------------------------------------------------------------
331
- // HQ-59 company vend-skip (presign transport)
332
- //
333
- // When the run uses the presigned-URL transport for company vaults
334
- // (`companyVaultUsesPresign`), resolveEntityContext must NOT call the company
335
- // `/sts/vend` route — the presign transport needs no STS creds, and the mere
336
- // presence of a cmp_ vend call is the pre-6.11.6 signal the hq-pro min-version
337
- // gate denies on. Personal vaults must still vend self.
338
- // ---------------------------------------------------------------------------
339
- describe("HQ-59 company vend-skip (companyVaultUsesPresign)", () => {
340
- const presignConfig: VaultServiceConfig = {
341
- ...mockConfig,
342
- companyVaultUsesPresign: true,
343
- };
344
-
345
- beforeEach(() => {
346
- clearContextCache();
347
- vi.restoreAllMocks();
348
- });
349
-
350
- it("cmp_* UID: SKIPS /sts/vend, returns no credentials + a far-future expiry", async () => {
351
- const calls: string[] = [];
352
- vi.stubGlobal(
353
- "fetch",
354
- vi.fn().mockImplementation(async (url: string) => {
355
- const u = String(url);
356
- calls.push(u);
357
- if (u.includes("/entity/cmp_")) {
358
- return { ok: true, status: 200, json: async () => ({ entity: mockEntity }), text: async () => "" };
359
- }
360
- // If a vend ever fires, surface it as a hard failure so the test fails
361
- // LOUDLY rather than silently allowing the route the gate watches.
362
- if (u.includes("/sts/vend")) {
363
- return { ok: true, status: 200, json: async () => mockVendResponse, text: async () => "" };
364
- }
365
- return { ok: false, status: 404, text: async () => "Not found" };
366
- }),
367
- );
368
-
369
- const ctx = await resolveEntityContext("cmp_01ABCDEF", presignConfig);
370
-
371
- // The load-bearing assertion: NO company vend route was hit.
372
- expect(calls.some((u) => u.includes("/sts/vend"))).toBe(false);
373
- expect(calls.some((u) => u.includes("/entity/cmp_01ABCDEF"))).toBe(true);
374
- // Credential-less context with a sentinel far-future expiry → never re-vends.
375
- expect(ctx.credentials).toBeUndefined();
376
- expect(ctx.bucketName).toBe(mockEntity.bucketName);
377
- expect(isExpiringSoon(ctx.expiresAt)).toBe(false);
378
- });
379
-
380
- it("company slug: SKIPS /sts/vend after the namespace + entity lookup", async () => {
381
- const calls: string[] = [];
382
- vi.stubGlobal(
383
- "fetch",
384
- vi.fn().mockImplementation(async (url: string) => {
385
- const u = String(url);
386
- calls.push(u);
387
- if (u.includes("/entity/check-slug/me")) {
388
- return {
389
- ok: true,
390
- status: 200,
391
- json: async () => ({ available: false, conflictingCompanyUid: mockEntity.uid }),
392
- text: async () => "",
393
- };
394
- }
395
- if (u.includes(`/entity/${mockEntity.uid}`)) {
396
- return { ok: true, status: 200, json: async () => ({ entity: mockEntity }), text: async () => "" };
397
- }
398
- if (u.includes("/sts/vend")) {
399
- return { ok: true, status: 200, json: async () => mockVendResponse, text: async () => "" };
400
- }
401
- return { ok: false, status: 404, text: async () => "Not found" };
402
- }),
403
- );
404
-
405
- const ctx = await resolveEntityContext("acme", presignConfig);
406
-
407
- expect(calls.some((u) => u.includes("/sts/vend"))).toBe(false);
408
- expect(ctx.credentials).toBeUndefined();
409
- });
410
-
411
- it("prs_* UID: STILL vends self even with the company flag set", async () => {
412
- const prsEntity = {
413
- uid: "prs_01PERSON",
414
- slug: "test-person",
415
- bucketName: "hq-vault-prs-01person",
416
- status: "active",
417
- };
418
- const calls: string[] = [];
419
- vi.stubGlobal(
420
- "fetch",
421
- vi.fn().mockImplementation(async (url: string) => {
422
- const u = String(url);
423
- calls.push(u);
424
- if (u.includes("/entity/prs_")) {
425
- return { ok: true, status: 200, json: async () => ({ entity: prsEntity }), text: async () => "" };
426
- }
427
- if (u.includes("/sts/vend-self")) {
428
- return { ok: true, status: 200, json: async () => mockVendResponse, text: async () => "" };
429
- }
430
- return { ok: false, status: 404, text: async () => "Not found" };
431
- }),
432
- );
433
-
434
- const ctx = await resolveEntityContext("prs_01PERSON", presignConfig);
435
-
436
- const vendCalls = calls.filter((u) => u.includes("/sts/vend"));
437
- expect(vendCalls).toHaveLength(1);
438
- expect(vendCalls[0]).toContain("/sts/vend-self");
439
- expect(ctx.credentials?.accessKeyId).toBe(mockVendResponse.credentials.accessKeyId);
440
- });
441
-
442
- it("flag OFF (default): cmp_* still vends — no behavior change without the flag", async () => {
443
- const calls: string[] = [];
444
- vi.stubGlobal(
445
- "fetch",
446
- vi.fn().mockImplementation(async (url: string) => {
447
- const u = String(url);
448
- calls.push(u);
449
- if (u.includes("/entity/cmp_")) {
450
- return { ok: true, status: 200, json: async () => ({ entity: mockEntity }), text: async () => "" };
451
- }
452
- if (u.includes("/sts/vend")) {
453
- return { ok: true, status: 200, json: async () => mockVendResponse, text: async () => "" };
454
- }
455
- return { ok: false, status: 404, text: async () => "Not found" };
456
- }),
457
- );
458
-
459
- const ctx = await resolveEntityContext("cmp_01ABCDEF", mockConfig);
460
-
461
- const vendCalls = calls.filter((u) => u.includes("/sts/vend"));
462
- expect(vendCalls).toHaveLength(1);
463
- expect(vendCalls[0]).toContain("/sts/vend");
464
- expect(ctx.credentials?.accessKeyId).toBe(mockVendResponse.credentials.accessKeyId);
465
- });
466
- });
467
-
468
- describe("refreshEntityContext", () => {
469
- beforeEach(() => {
470
- clearContextCache();
471
- vi.restoreAllMocks();
472
- });
473
-
474
- it("evicts cache and fetches fresh credentials", async () => {
475
- const fetchMock = setupFetchMock();
476
-
477
- const ctx1 = await resolveEntityContext("acme", mockConfig);
478
- const ctx2 = await refreshEntityContext("acme", mockConfig);
479
-
480
- expect(ctx2).not.toBe(ctx1);
481
- // 2 resolves × 3 fetches each = 6 under the new model
482
- // (check-slug + entity.get + sts/vend each).
483
- expect(fetchMock).toHaveBeenCalledTimes(6);
484
- });
485
- });
486
-
487
- describe("isExpiringSoon", () => {
488
- it("returns false when well within TTL", () => {
489
- const future = new Date(Date.now() + 10 * 60 * 1000).toISOString();
490
- expect(isExpiringSoon(future)).toBe(false);
491
- });
492
-
493
- it("returns true when within 2 minutes", () => {
494
- const soon = new Date(Date.now() + 90 * 1000).toISOString();
495
- expect(isExpiringSoon(soon)).toBe(true);
496
- });
497
-
498
- it("returns true when already expired", () => {
499
- const past = new Date(Date.now() - 1000).toISOString();
500
- expect(isExpiringSoon(past)).toBe(true);
501
- });
502
- });
503
-
504
- // ---------------------------------------------------------------------------
505
- // Refreshable authToken getter
506
- //
507
- // Regression for the personal-sync 401: a long-running `hq-sync-runner` run
508
- // captures vaultConfig.authToken as a string at startup, then `refreshEntityContext`
509
- // fires ~13 min into the personal-company sync (STS expiry) and uses that
510
- // stale string against API Gateway's JWT authorizer → 401. The getter form
511
- // lets every fetchEntity / postVend call resolve the latest token from disk.
512
- // ---------------------------------------------------------------------------
513
-
514
- describe("authToken getter is invoked per-request (regression: personal-sync 401)", () => {
515
- beforeEach(() => {
516
- clearContextCache();
517
- vi.restoreAllMocks();
518
- });
519
-
520
- it("calls the getter on entity fetch AND on vend (every request)", async () => {
521
- setupFetchMock();
522
- const getter = vi.fn(async () => "fresh-token");
523
-
524
- await resolveEntityContext("cmp_01ABCDEF", {
525
- apiUrl: "https://vault-api.test",
526
- authToken: getter,
527
- });
528
-
529
- // Two upstream requests: fetchEntity (UID looks like a UID) + postVend.
530
- expect(getter).toHaveBeenCalledTimes(2);
531
- });
532
-
533
- it("picks up a rotated token between resolveEntityContext and refreshEntityContext", async () => {
534
- const fetchMock = setupFetchMock();
535
- let current = "stale-token";
536
- const cfg = {
537
- apiUrl: "https://vault-api.test",
538
- authToken: async () => current,
539
- };
540
-
541
- await resolveEntityContext("cmp_01ABCDEF", cfg);
542
-
543
- // fetchEntity + vend during initial resolution — both used "stale-token"
544
- const initialAuth = (fetchMock.mock.calls as [string, RequestInit][]).map(
545
- ([, init]) =>
546
- (init.headers as Record<string, string>).Authorization,
547
- );
548
- expect(initialAuth.every((a) => a === "Bearer stale-token")).toBe(true);
549
-
550
- // Simulate the on-disk token rotating mid-flight.
551
- current = "fresh-token";
552
-
553
- await refreshEntityContext("cmp_01ABCDEF", cfg);
554
-
555
- // The post-refresh calls must use the new token. Without the per-request
556
- // getter, refreshEntityContext would still send "Bearer stale-token" and
557
- // 401 against the gateway.
558
- const allCalls = fetchMock.mock.calls as [string, RequestInit][];
559
- const postRefreshCalls = allCalls.slice(initialAuth.length);
560
- expect(postRefreshCalls.length).toBeGreaterThan(0);
561
- for (const [, init] of postRefreshCalls) {
562
- expect((init.headers as Record<string, string>).Authorization).toBe(
563
- "Bearer fresh-token",
564
- );
565
- }
566
- });
567
-
568
- it("static-string authToken still works (back-compat)", async () => {
569
- const fetchMock = setupFetchMock();
570
- await resolveEntityContext("cmp_01ABCDEF", {
571
- apiUrl: "https://vault-api.test",
572
- authToken: "static-token",
573
- });
574
-
575
- const calls = fetchMock.mock.calls as [string, RequestInit][];
576
- expect(calls.length).toBeGreaterThan(0);
577
- for (const [, init] of calls) {
578
- expect((init.headers as Record<string, string>).Authorization).toBe(
579
- "Bearer static-token",
580
- );
581
- }
582
- });
583
- });