@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,207 +0,0 @@
1
- import type { AuthTokenSource, FetchLike } from "./push-transport.js";
2
- import {
3
- DEFAULT_REALTIME_AUTHORIZATION_TIMEOUT_MS,
4
- REALTIME_SYNC_CONTRACT_VERSION,
5
- type RealtimeCapabilityVector,
6
- type RealtimeInventoryGrant,
7
- type RealtimeInventoryRequest,
8
- type RealtimeRolloutClient,
9
- resolveRealtimeBearer,
10
- } from "./realtime-rollout.js";
11
-
12
- export const MAX_REALTIME_LEASE_MS = 60_000;
13
- export const MAX_REALTIME_LEASE_CLOCK_SKEW_MS = 5_000;
14
-
15
- export interface RealtimeLease {
16
- lease: string;
17
- issuedAt: string;
18
- expiresAt: string;
19
- authorizationEpoch: string;
20
- }
21
-
22
- export interface RealtimeLeaseRequest {
23
- bearer: string;
24
- deviceId: string;
25
- capabilityVector: RealtimeCapabilityVector;
26
- scopeHandle: string;
27
- scopeRevision: string;
28
- }
29
-
30
- export interface RealtimeLeaseClientOptions {
31
- apiUrl: string;
32
- fetchImpl?: FetchLike;
33
- now?: () => number;
34
- authorizationTimeoutMs?: number;
35
- }
36
-
37
- function asNonEmptyString(value: unknown, field: string): string {
38
- if (typeof value !== "string" || value.trim() === "") throw new Error(`realtime lease: ${field} must be a non-empty string`);
39
- return value;
40
- }
41
-
42
- function parseLease(value: unknown, now: number): RealtimeLease {
43
- if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("realtime lease: response is not an object");
44
- const record = value as Record<string, unknown>;
45
- if (Object.keys(record).some((key) => !["lease", "issuedAt", "expiresAt", "authorizationEpoch"].includes(key))) {
46
- throw new Error("realtime lease: response contains an unknown field");
47
- }
48
- const lease: RealtimeLease = {
49
- lease: asNonEmptyString(record.lease, "lease"),
50
- issuedAt: asNonEmptyString(record.issuedAt, "issuedAt"),
51
- expiresAt: asNonEmptyString(record.expiresAt, "expiresAt"),
52
- authorizationEpoch: asNonEmptyString(record.authorizationEpoch, "authorizationEpoch"),
53
- };
54
- const issuedAtMs = Date.parse(lease.issuedAt);
55
- const expiresAtMs = Date.parse(lease.expiresAt);
56
- if (
57
- !Number.isFinite(issuedAtMs) ||
58
- !Number.isFinite(expiresAtMs) ||
59
- issuedAtMs > now + MAX_REALTIME_LEASE_CLOCK_SKEW_MS ||
60
- expiresAtMs <= now ||
61
- expiresAtMs <= issuedAtMs ||
62
- expiresAtMs - issuedAtMs > MAX_REALTIME_LEASE_MS ||
63
- expiresAtMs > now + MAX_REALTIME_LEASE_MS
64
- ) {
65
- throw new Error("realtime lease: invalid lease lifetime");
66
- }
67
- return lease;
68
- }
69
-
70
- /** Issues a non-cacheable, <=60 second lease for one exact inventory grant. */
71
- export class RealtimeLeaseClient {
72
- private readonly apiUrl: string;
73
- private readonly fetchImpl: FetchLike;
74
- private readonly now: () => number;
75
- private readonly authorizationTimeoutMs: number;
76
-
77
- constructor(options: RealtimeLeaseClientOptions) {
78
- if (!options.apiUrl.trim()) throw new Error("realtime lease: apiUrl is required");
79
- this.apiUrl = options.apiUrl.replace(/\/+$/, "");
80
- this.fetchImpl = options.fetchImpl ?? ((input, init) => (globalThis.fetch as unknown as FetchLike)(input, init));
81
- this.now = options.now ?? Date.now;
82
- this.authorizationTimeoutMs = options.authorizationTimeoutMs ?? DEFAULT_REALTIME_AUTHORIZATION_TIMEOUT_MS;
83
- if (!Number.isSafeInteger(this.authorizationTimeoutMs) || this.authorizationTimeoutMs <= 0) {
84
- throw new Error("realtime lease: authorizationTimeoutMs must be a positive integer");
85
- }
86
- }
87
-
88
- assertUsable(lease: RealtimeLease): void {
89
- const expiresAtMs = Date.parse(lease.expiresAt);
90
- if (!Number.isFinite(expiresAtMs) || expiresAtMs <= this.now()) {
91
- throw new Error("realtime lease: expired before guarded I/O");
92
- }
93
- }
94
-
95
- async issue(request: RealtimeLeaseRequest): Promise<RealtimeLease> {
96
- const controller = new AbortController();
97
- const timeout = setTimeout(() => controller.abort(), this.authorizationTimeoutMs);
98
- let response: Awaited<ReturnType<FetchLike>>;
99
- try {
100
- response = await this.fetchImpl(`${this.apiUrl}/v2/sync/leases`, {
101
- method: "POST",
102
- headers: {
103
- Authorization: `Bearer ${request.bearer}`,
104
- Accept: "application/json",
105
- "Content-Type": "application/json",
106
- "X-HQ-Sync-Contract": String(REALTIME_SYNC_CONTRACT_VERSION),
107
- "X-HQ-Device-ID": request.deviceId,
108
- "X-HQ-Cloud-Version": request.capabilityVector.hqCloud,
109
- "X-HQ-Desktop-Version": request.capabilityVector.hqDesktop,
110
- },
111
- body: JSON.stringify({
112
- scopeHandle: request.scopeHandle,
113
- scopeRevision: request.scopeRevision,
114
- capabilityVector: request.capabilityVector,
115
- }),
116
- signal: controller.signal,
117
- });
118
- } catch (error) {
119
- throw new Error(`realtime lease: POST /v2/sync/leases failed: ${error instanceof Error ? error.message : String(error)}`);
120
- } finally {
121
- clearTimeout(timeout);
122
- }
123
- const text = await response.text().catch((error: unknown) => {
124
- throw new Error(`realtime lease: unable to read response: ${error instanceof Error ? error.message : String(error)}`);
125
- });
126
- if (!response.ok) throw new Error(`realtime lease: POST /v2/sync/leases failed (${response.status}): ${text.slice(0, 300)}`);
127
- let parsed: unknown;
128
- try {
129
- parsed = JSON.parse(text);
130
- } catch (error) {
131
- throw new Error(`realtime lease: response is not JSON: ${error instanceof Error ? error.message : String(error)}`);
132
- }
133
- return parseLease(parsed, this.now());
134
- }
135
- }
136
-
137
- export interface RealtimeScopeAuthorization {
138
- grant: RealtimeInventoryGrant & { state: "v2-ready" };
139
- lease: RealtimeLease;
140
- }
141
-
142
- export interface RealtimePassAuthorization {
143
- /** One resolved bearer is reused by every inventory and lease request in this pass. */
144
- bearer: string;
145
- scopes: readonly RealtimeScopeAuthorization[];
146
- }
147
-
148
- export interface RealtimePassRequest {
149
- deviceId: string;
150
- capabilityVector: RealtimeCapabilityVector;
151
- configurationFingerprint: string;
152
- scopeFingerprint: string;
153
- scopeUids: readonly string[];
154
- }
155
-
156
- /**
157
- * The only pre-I/O admission seam for later V2 work. It intentionally has no
158
- * V1 fallback: malformed, stale, revoked, or non-ready inventory never gives
159
- * an operation a lease. It resolves its bearer once, then reissues leases on
160
- * every guarded pass so cached positives cannot cross server config/scope
161
- * changes.
162
- */
163
- export class RealtimePassAuthorizer {
164
- constructor(
165
- private readonly authToken: AuthTokenSource,
166
- private readonly inventoryClient: RealtimeRolloutClient,
167
- private readonly leaseClient: RealtimeLeaseClient,
168
- ) {}
169
-
170
- async authorize(request: RealtimePassRequest): Promise<RealtimePassAuthorization> {
171
- const bearer = await resolveRealtimeBearer(this.authToken);
172
- const inventoryRequest: RealtimeInventoryRequest = {
173
- bearer,
174
- deviceId: request.deviceId,
175
- capabilityVector: request.capabilityVector,
176
- configurationFingerprint: request.configurationFingerprint,
177
- scopeFingerprint: request.scopeFingerprint,
178
- };
179
- const inventory = await this.inventoryClient.inventory(inventoryRequest);
180
- const requested = new Set(request.scopeUids);
181
- if (requested.size === 0) throw new Error("realtime authorization: guarded pass has no scopes");
182
- const grants = inventory.scopes.filter((grant) => requested.has(grant.scopeUid));
183
- if (grants.length !== requested.size || grants.some((grant) => grant.state !== "v2-ready")) {
184
- throw new Error("realtime authorization: inventory does not authorize every requested scope");
185
- }
186
- const scopes = await Promise.all(grants.map(async (grant) => ({
187
- grant: grant as RealtimeInventoryGrant & { state: "v2-ready" },
188
- lease: await this.leaseClient.issue({
189
- bearer,
190
- deviceId: request.deviceId,
191
- capabilityVector: request.capabilityVector,
192
- scopeHandle: grant.scopeHandle,
193
- scopeRevision: grant.scopeRevision,
194
- }),
195
- })));
196
- const authorizationEpoch = scopes[0]?.lease.authorizationEpoch;
197
- if (!authorizationEpoch || scopes.some((scope) => scope.lease.authorizationEpoch !== authorizationEpoch)) {
198
- throw new Error("realtime authorization: leases disagree on authorization epoch");
199
- }
200
- for (const scope of scopes) this.leaseClient.assertUsable(scope.lease);
201
- return { bearer, scopes };
202
- }
203
-
204
- async runBeforeIo<T>(request: RealtimePassRequest, operation: (authorization: RealtimePassAuthorization) => Promise<T>): Promise<T> {
205
- return operation(await this.authorize(request));
206
- }
207
- }
@@ -1,242 +0,0 @@
1
- /**
2
- * US-011 — unit tests for the pino logger factory + the 3-log diagnostic
3
- * chain's CLIENT-side correlated fields.
4
- *
5
- * The PRD requires three correlated logs sharing the same `sequenceNumber`:
6
- * 1. `watcher.emit` — client push (US-008 PushEventEmitter) [client]
7
- * 2. `push.receive` — server (hq-pro) [server]
8
- * 3. `fanout.receive` — client fanout-receive (US-009 receiver) [client]
9
- *
10
- * This package owns the two CLIENT-side links. These tests prove:
11
- * - `createLogger` stamps the `component` tag on every line and respects
12
- * level + injected destination.
13
- * - the watcher emits `event=watcher.emit` carrying `sequenceNumber`.
14
- * - the receiver emits `event=fanout.receive` carrying the SAME
15
- * `sequenceNumber` → an operator can join the two client links (and the
16
- * server `push.receive` line, which carries the same key) end-to-end.
17
- */
18
-
19
- import { mkdtemp, rm, writeFile } from "node:fs/promises";
20
- import { tmpdir } from "node:os";
21
- import { join } from "node:path";
22
- import { Writable } from "node:stream";
23
-
24
- import { afterEach, describe, expect, it } from "vitest";
25
-
26
- import { createLogger } from "./logger.js";
27
- import { encodePushEvent, type PushEvent } from "./push-event.js";
28
- import { NoopPushTransport } from "./push-transport.js";
29
- import { StaticFlagProvider } from "./feature-flags.js";
30
- import {
31
- SqsPushReceiver,
32
- type SqsClientLike,
33
- type SqsMessageLike,
34
- } from "./push-receiver.js";
35
- import { PushEventEmitter, type TreeChangeBatch } from "../watcher.js";
36
-
37
- const TENANT = "tenant-indigo";
38
-
39
- // ── Capture stream + JSON line reader ─────────────────────────────────────────
40
-
41
- function captureStream(): { stream: Writable; lines: () => Array<Record<string, unknown>> } {
42
- const chunks: string[] = [];
43
- const stream = new Writable({
44
- write(chunk, _enc, cb) {
45
- chunks.push(
46
- typeof chunk === "string" ? chunk : (chunk as Buffer).toString("utf8"),
47
- );
48
- cb();
49
- },
50
- });
51
- return {
52
- stream,
53
- lines: () =>
54
- chunks
55
- .join("")
56
- .split("\n")
57
- .filter((s) => s.length > 0)
58
- .map((s) => JSON.parse(s) as Record<string, unknown>),
59
- };
60
- }
61
-
62
- async function until(predicate: () => boolean, timeoutMs = 1000): Promise<void> {
63
- const start = Date.now();
64
- while (!predicate()) {
65
- if (Date.now() - start > timeoutMs) throw new Error("until() timed out");
66
- await new Promise((r) => setTimeout(r, 5));
67
- }
68
- }
69
-
70
- function makeEvent(overrides: Partial<PushEvent> = {}): PushEvent {
71
- return {
72
- relativePath: "companies/indigo/notes.md",
73
- kind: "upsert",
74
- contentHash:
75
- "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
76
- mtime: "2026-05-21T12:34:56.000Z",
77
- originDeviceId: "device-A",
78
- originTenantId: TENANT,
79
- sequenceNumber: 1,
80
- eventTimestamp: "2026-05-21T12:34:56.000Z",
81
- ...overrides,
82
- };
83
- }
84
-
85
- class OneBatchSqs implements SqsClientLike {
86
- private batch: SqsMessageLike[] | null;
87
- constructor(messages: SqsMessageLike[]) {
88
- this.batch = messages;
89
- }
90
- async receiveMessage(args: {
91
- queueUrl: string;
92
- maxMessages: number;
93
- waitTimeSeconds: number;
94
- signal: AbortSignal;
95
- }): Promise<{ messages: SqsMessageLike[] }> {
96
- if (this.batch) {
97
- const b = this.batch;
98
- this.batch = null;
99
- return { messages: b };
100
- }
101
- return new Promise((resolve) => {
102
- if (args.signal.aborted) return resolve({ messages: [] });
103
- const t = setTimeout(() => resolve({ messages: [] }), 5);
104
- (t as { unref?: () => void }).unref?.();
105
- args.signal.addEventListener(
106
- "abort",
107
- () => {
108
- clearTimeout(t);
109
- resolve({ messages: [] });
110
- },
111
- { once: true },
112
- );
113
- });
114
- }
115
- async deleteMessage(): Promise<void> {
116
- /* no-op */
117
- }
118
- }
119
-
120
- // ── createLogger ──────────────────────────────────────────────────────────────
121
-
122
- describe("createLogger", () => {
123
- it("stamps the `component` tag on every line", () => {
124
- const { stream, lines } = captureStream();
125
- const log = createLogger({
126
- component: "sync-watcher",
127
- destination: stream,
128
- level: "info",
129
- });
130
- log.info({ event: "x" }, "hello");
131
- log.warn({ event: "y" }, "world");
132
- const out = lines();
133
- expect(out).toHaveLength(2);
134
- expect(out.every((l) => l.component === "sync-watcher")).toBe(true);
135
- });
136
-
137
- it("respects the injected level (debug below info is dropped at info)", () => {
138
- const { stream, lines } = captureStream();
139
- const log = createLogger({
140
- component: "c",
141
- destination: stream,
142
- level: "info",
143
- });
144
- log.debug({ event: "d" }, "dropped");
145
- log.info({ event: "i" }, "kept");
146
- const out = lines();
147
- expect(out).toHaveLength(1);
148
- expect(out[0]!.event).toBe("i");
149
- });
150
- });
151
-
152
- // ── 3-log chain: client-side correlated fields share sequenceNumber ───────────
153
-
154
- describe("3-log diagnostic chain (client links share sequenceNumber)", () => {
155
- let tmp: string | undefined;
156
- let receiver: SqsPushReceiver | undefined;
157
-
158
- afterEach(async () => {
159
- if (receiver) {
160
- await receiver.dispose();
161
- receiver = undefined;
162
- }
163
- if (tmp) {
164
- await rm(tmp, { recursive: true, force: true });
165
- tmp = undefined;
166
- }
167
- });
168
-
169
- it("watcher.emit and fanout.receive carry the SAME sequenceNumber", async () => {
170
- const SEQ = 4242;
171
-
172
- // ── Link 1: watcher.emit (client push side) ──────────────────────────
173
- tmp = await mkdtemp(join(tmpdir(), "us011-logger-"));
174
- const absPath = join(tmp, "notes.md");
175
- await writeFile(absPath, "hello world", "utf8");
176
-
177
- const watcherCap = captureStream();
178
- const watcherLog = createLogger({
179
- component: "sync-watcher",
180
- destination: watcherCap.stream,
181
- level: "info",
182
- });
183
-
184
- const emitter = new PushEventEmitter({
185
- originTenantId: TENANT,
186
- originDeviceId: "device-A",
187
- transport: new NoopPushTransport(),
188
- flagProvider: new StaticFlagProvider([TENANT]),
189
- getSequenceNumber: () => SEQ,
190
- logger: watcherLog,
191
- });
192
-
193
- const batch: TreeChangeBatch = {
194
- paths: new Map([[absPath, "companies/indigo/notes.md"]]),
195
- };
196
- await emitter.emitForBatch(batch);
197
-
198
- const emitLine = watcherCap
199
- .lines()
200
- .find((l) => l.event === "watcher.emit");
201
- expect(emitLine).toBeDefined();
202
- expect(emitLine!.sequenceNumber).toBe(SEQ);
203
- expect(emitLine!.component).toBe("sync-watcher");
204
-
205
- // ── Link 3: fanout.receive (client receive side) ─────────────────────
206
- const receiverCap = captureStream();
207
- const receiverLog = createLogger({
208
- component: "sync-receiver",
209
- destination: receiverCap.stream,
210
- level: "info",
211
- });
212
- const event = makeEvent({ sequenceNumber: SEQ });
213
-
214
- receiver = new SqsPushReceiver({
215
- tenantId: TENANT,
216
- queueUrl: "https://sqs.local/q",
217
- sqs: new OneBatchSqs([{ Body: encodePushEvent(event), ReceiptHandle: "rh" }]),
218
- syncFn: async () => {
219
- /* success */
220
- },
221
- enabled: true,
222
- logger: receiverLog,
223
- // Inject a no-op metric so no real AWS is touched.
224
- publishMetric: async () => {},
225
- });
226
-
227
- await receiver.start();
228
- await until(() =>
229
- receiverCap.lines().some((l) => l.event === "fanout.receive"),
230
- );
231
-
232
- const recvLine = receiverCap
233
- .lines()
234
- .find((l) => l.event === "fanout.receive");
235
- expect(recvLine).toBeDefined();
236
- expect(recvLine!.sequenceNumber).toBe(SEQ);
237
- expect(recvLine!.component).toBe("sync-receiver");
238
-
239
- // ── The join key matches across both client links ────────────────────
240
- expect(emitLine!.sequenceNumber).toBe(recvLine!.sequenceNumber);
241
- });
242
- });
@@ -1,79 +0,0 @@
1
- /**
2
- * Component-tagged logger factory for the hq-cloud sync subsystem
3
- * (project event-driven-sync-menubar US-011).
4
- *
5
- * Every log line emitted from a sync module SHOULD carry a `component` field
6
- * so operators can grep / route by subsystem in the aggregated JSON stream.
7
- * `createLogger({ component: "sync-watcher" })` is the canonical entry point —
8
- * modules call this once at module-load time and use the returned logger for
9
- * the lifetime of the process.
10
- *
11
- * The 3-log diagnostic chain (US-011)
12
- * ───────────────────────────────────
13
- * Three correlated log lines share a single `sequenceNumber` join key so an
14
- * operator can walk one event end-to-end:
15
- * 1. `event=watcher.emit` — client push side (US-008 PushEventEmitter)
16
- * 2. `event=push.receive` — server side (hq-pro; context only here)
17
- * 3. `event=fanout.receive` — client fanout-receive side (US-009 receiver)
18
- * The watcher.emit + fanout.receive halves live in this client package; the
19
- * push.receive half lives server-side in hq-pro. All three stamp the same
20
- * `sequenceNumber` for log-chain correlation.
21
- *
22
- * Output format: pino's default newline-delimited JSON. No transports, no
23
- * pretty-printing — the daemon consumes the raw JSON stream. (Operators who
24
- * want pretty output pipe through `pino-pretty` themselves.)
25
- *
26
- * Destination injection: tests pass a `destination` stream so they can capture
27
- * log lines and assert on them. Production callers omit it and pino defaults
28
- * to `process.stdout`.
29
- *
30
- * Adapted from indigoai-us/hq-pro PR #112 (src/sync/logger.ts) into
31
- * @indigoai-us/hq-cloud (Path B).
32
- */
33
-
34
- import {
35
- pino,
36
- type DestinationStream,
37
- type Level,
38
- type Logger,
39
- type LoggerOptions,
40
- } from "pino";
41
-
42
- export interface CreateLoggerOptions {
43
- /**
44
- * Component tag stamped on every log line as `"component": <value>`.
45
- * Required so daemon / watcher / receiver lines never appear in the stream
46
- * untagged.
47
- */
48
- component: string;
49
- /**
50
- * Optional pino level. Default: pino's own default (`info`). Set via
51
- * `LOG_LEVEL` env var, command-line flag, or test injection.
52
- */
53
- level?: Level;
54
- /**
55
- * Optional pino destination. Default: `process.stdout`. Tests inject a
56
- * memory stream here to capture lines for assertion.
57
- */
58
- destination?: DestinationStream;
59
- }
60
-
61
- /**
62
- * Build a pino logger pre-bound to a `component` tag.
63
- *
64
- * Use this — not a bare `pino()` call — so every sync module's log lines carry
65
- * the tag uniformly. Adding more bound fields (e.g. `deviceId`, `tenantId`) is
66
- * a `logger.child({ ... })` call away.
67
- */
68
- export function createLogger(opts: CreateLoggerOptions): Logger {
69
- const { component, level, destination } = opts;
70
- const pinoOpts: LoggerOptions = {
71
- base: { component },
72
- ...(level === undefined ? {} : { level }),
73
- };
74
- return destination === undefined
75
- ? pino(pinoOpts)
76
- : pino(pinoOpts, destination);
77
- }
78
-
79
- export type { Logger } from "pino";