@indigoai-us/hq-cloud 6.15.0 → 6.15.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (329) hide show
  1. package/dist/bin/sync-mutation.d.ts +16 -0
  2. package/dist/bin/sync-mutation.d.ts.map +1 -0
  3. package/dist/bin/sync-mutation.js +60 -0
  4. package/dist/bin/sync-mutation.js.map +1 -0
  5. package/dist/bin/sync-mutation.test.d.ts +2 -0
  6. package/dist/bin/sync-mutation.test.d.ts.map +1 -0
  7. package/dist/bin/sync-mutation.test.js +165 -0
  8. package/dist/bin/sync-mutation.test.js.map +1 -0
  9. package/dist/bin/sync-runner-company.d.ts +8 -0
  10. package/dist/bin/sync-runner-company.d.ts.map +1 -1
  11. package/dist/bin/sync-runner-company.js +16 -0
  12. package/dist/bin/sync-runner-company.js.map +1 -1
  13. package/dist/bin/sync-runner-company.test.d.ts +2 -0
  14. package/dist/bin/sync-runner-company.test.d.ts.map +1 -0
  15. package/dist/bin/sync-runner-company.test.js +36 -0
  16. package/dist/bin/sync-runner-company.test.js.map +1 -0
  17. package/dist/bin/sync-runner-watch-loop.d.ts.map +1 -1
  18. package/dist/bin/sync-runner-watch-loop.js +98 -8
  19. package/dist/bin/sync-runner-watch-loop.js.map +1 -1
  20. package/dist/bin/sync-runner.d.ts +17 -0
  21. package/dist/bin/sync-runner.d.ts.map +1 -1
  22. package/dist/bin/sync-runner.js.map +1 -1
  23. package/dist/bin/sync-runner.test.js +109 -0
  24. package/dist/bin/sync-runner.test.js.map +1 -1
  25. package/dist/cli/conflict-recovery.test.d.ts +2 -0
  26. package/dist/cli/conflict-recovery.test.d.ts.map +1 -0
  27. package/dist/cli/conflict-recovery.test.js +201 -0
  28. package/dist/cli/conflict-recovery.test.js.map +1 -0
  29. package/dist/cli/conflict.d.ts +60 -0
  30. package/dist/cli/conflict.d.ts.map +1 -1
  31. package/dist/cli/conflict.js +333 -0
  32. package/dist/cli/conflict.js.map +1 -1
  33. package/dist/cli/sync.d.ts +35 -0
  34. package/dist/cli/sync.d.ts.map +1 -1
  35. package/dist/cli/sync.js +100 -0
  36. package/dist/cli/sync.js.map +1 -1
  37. package/dist/cli/sync.test.js +85 -1
  38. package/dist/cli/sync.test.js.map +1 -1
  39. package/dist/index.d.ts +2 -0
  40. package/dist/index.d.ts.map +1 -1
  41. package/dist/index.js +1 -0
  42. package/dist/index.js.map +1 -1
  43. package/dist/skill-telemetry.d.ts +6 -0
  44. package/dist/skill-telemetry.d.ts.map +1 -1
  45. package/dist/skill-telemetry.js +14 -2
  46. package/dist/skill-telemetry.js.map +1 -1
  47. package/dist/skill-telemetry.test.js +79 -0
  48. package/dist/skill-telemetry.test.js.map +1 -1
  49. package/dist/sync/candidate-uploader.d.ts +88 -0
  50. package/dist/sync/candidate-uploader.d.ts.map +1 -0
  51. package/dist/sync/candidate-uploader.js +212 -0
  52. package/dist/sync/candidate-uploader.js.map +1 -0
  53. package/dist/sync/candidate-uploader.test.d.ts +2 -0
  54. package/dist/sync/candidate-uploader.test.d.ts.map +1 -0
  55. package/dist/sync/candidate-uploader.test.js +132 -0
  56. package/dist/sync/candidate-uploader.test.js.map +1 -0
  57. package/dist/sync/delta-client.d.ts +73 -0
  58. package/dist/sync/delta-client.d.ts.map +1 -0
  59. package/dist/sync/delta-client.js +201 -0
  60. package/dist/sync/delta-client.js.map +1 -0
  61. package/dist/sync/delta-client.test.d.ts +2 -0
  62. package/dist/sync/delta-client.test.d.ts.map +1 -0
  63. package/dist/sync/delta-client.test.js +97 -0
  64. package/dist/sync/delta-client.test.js.map +1 -0
  65. package/dist/sync/durable-apply.d.ts +76 -0
  66. package/dist/sync/durable-apply.d.ts.map +1 -0
  67. package/dist/sync/durable-apply.js +530 -0
  68. package/dist/sync/durable-apply.js.map +1 -0
  69. package/dist/sync/durable-apply.test.d.ts +2 -0
  70. package/dist/sync/durable-apply.test.d.ts.map +1 -0
  71. package/dist/sync/durable-apply.test.js +180 -0
  72. package/dist/sync/durable-apply.test.js.map +1 -0
  73. package/dist/sync/event-sync.d.ts +33 -1
  74. package/dist/sync/event-sync.d.ts.map +1 -1
  75. package/dist/sync/event-sync.js +149 -1
  76. package/dist/sync/event-sync.js.map +1 -1
  77. package/dist/sync/event-sync.test.js +142 -1
  78. package/dist/sync/event-sync.test.js.map +1 -1
  79. package/dist/sync/index.d.ts +2 -0
  80. package/dist/sync/index.d.ts.map +1 -1
  81. package/dist/sync/index.js +1 -0
  82. package/dist/sync/index.js.map +1 -1
  83. package/dist/sync/multipart-uploader.d.ts +99 -0
  84. package/dist/sync/multipart-uploader.d.ts.map +1 -0
  85. package/dist/sync/multipart-uploader.js +447 -0
  86. package/dist/sync/multipart-uploader.js.map +1 -0
  87. package/dist/sync/multipart-uploader.test.d.ts +2 -0
  88. package/dist/sync/multipart-uploader.test.d.ts.map +1 -0
  89. package/dist/sync/multipart-uploader.test.js +119 -0
  90. package/dist/sync/multipart-uploader.test.js.map +1 -0
  91. package/dist/sync/mutation-client.d.ts +85 -0
  92. package/dist/sync/mutation-client.d.ts.map +1 -0
  93. package/dist/sync/mutation-client.js +245 -0
  94. package/dist/sync/mutation-client.js.map +1 -0
  95. package/dist/sync/mutation-client.test.d.ts +2 -0
  96. package/dist/sync/mutation-client.test.d.ts.map +1 -0
  97. package/dist/sync/mutation-client.test.js +51 -0
  98. package/dist/sync/mutation-client.test.js.map +1 -0
  99. package/dist/sync/push-receiver.d.ts +45 -0
  100. package/dist/sync/push-receiver.d.ts.map +1 -1
  101. package/dist/sync/push-receiver.js +101 -0
  102. package/dist/sync/push-receiver.js.map +1 -1
  103. package/dist/sync/push-receiver.test.js +54 -2
  104. package/dist/sync/push-receiver.test.js.map +1 -1
  105. package/dist/sync/scope-inventory-client.d.ts +69 -0
  106. package/dist/sync/scope-inventory-client.d.ts.map +1 -0
  107. package/dist/sync/scope-inventory-client.js +210 -0
  108. package/dist/sync/scope-inventory-client.js.map +1 -0
  109. package/dist/sync/scope-inventory-client.test.d.ts +2 -0
  110. package/dist/sync/scope-inventory-client.test.d.ts.map +1 -0
  111. package/dist/sync/scope-inventory-client.test.js +94 -0
  112. package/dist/sync/scope-inventory-client.test.js.map +1 -0
  113. package/dist/sync/snapshot-client.d.ts +98 -0
  114. package/dist/sync/snapshot-client.d.ts.map +1 -0
  115. package/dist/sync/snapshot-client.js +402 -0
  116. package/dist/sync/snapshot-client.js.map +1 -0
  117. package/dist/sync/snapshot-client.test.d.ts +2 -0
  118. package/dist/sync/snapshot-client.test.d.ts.map +1 -0
  119. package/dist/sync/snapshot-client.test.js +169 -0
  120. package/dist/sync/snapshot-client.test.js.map +1 -0
  121. package/dist/sync/uploader-finalization.d.ts +97 -0
  122. package/dist/sync/uploader-finalization.d.ts.map +1 -0
  123. package/dist/sync/uploader-finalization.js +273 -0
  124. package/dist/sync/uploader-finalization.js.map +1 -0
  125. package/dist/sync/uploader-finalization.test.d.ts +2 -0
  126. package/dist/sync/uploader-finalization.test.d.ts.map +1 -0
  127. package/dist/sync/uploader-finalization.test.js +92 -0
  128. package/dist/sync/uploader-finalization.test.js.map +1 -0
  129. package/dist/telemetry.d.ts +11 -1
  130. package/dist/telemetry.d.ts.map +1 -1
  131. package/dist/telemetry.js +21 -2
  132. package/dist/telemetry.js.map +1 -1
  133. package/dist/telemetry.test.js +80 -0
  134. package/dist/telemetry.test.js.map +1 -1
  135. package/package.json +6 -1
  136. package/.claude/policies/hq-cloud-esm-cannot-spy-fs-builtins.md +0 -30
  137. package/.claude/policies/hq-cloud-strip-types-no-parameter-properties.md +0 -22
  138. package/.github/workflows/ci.yml +0 -84
  139. package/.github/workflows/publish.yml +0 -56
  140. package/.github/workflows/unreleased-commits-nag.yml +0 -256
  141. package/eslint.config.js +0 -67
  142. package/pnpm-workspace.yaml +0 -2
  143. package/scripts/presign-transport-e2e.mjs +0 -250
  144. package/scripts/vault-rebaseline.sh +0 -323
  145. package/scripts/vault-rescue.sh +0 -332
  146. package/src/active-company.test.ts +0 -188
  147. package/src/active-company.ts +0 -168
  148. package/src/agent-codex-instructions.test.ts +0 -332
  149. package/src/agent-codex-instructions.ts +0 -309
  150. package/src/auth.ts +0 -146
  151. package/src/backup-prune.test.ts +0 -98
  152. package/src/backup-prune.ts +0 -182
  153. package/src/bin/backup-prune-runner.ts +0 -33
  154. package/src/bin/rescue-runner.ts +0 -25
  155. package/src/bin/sync-runner-company.ts +0 -695
  156. package/src/bin/sync-runner-events.test.ts +0 -143
  157. package/src/bin/sync-runner-events.ts +0 -55
  158. package/src/bin/sync-runner-planning.test.ts +0 -311
  159. package/src/bin/sync-runner-planning.ts +0 -258
  160. package/src/bin/sync-runner-rollup.test.ts +0 -37
  161. package/src/bin/sync-runner-rollup.ts +0 -97
  162. package/src/bin/sync-runner-telemetry.ts +0 -15
  163. package/src/bin/sync-runner-watch-loop.ts +0 -1235
  164. package/src/bin/sync-runner-watch-routes.test.ts +0 -71
  165. package/src/bin/sync-runner-watch-routes.ts +0 -184
  166. package/src/bin/sync-runner.test.ts +0 -8767
  167. package/src/bin/sync-runner.ts +0 -2190
  168. package/src/cli/accept.ts +0 -124
  169. package/src/cli/conflict.ts +0 -119
  170. package/src/cli/doctor.test.ts +0 -581
  171. package/src/cli/doctor.ts +0 -642
  172. package/src/cli/index.ts +0 -49
  173. package/src/cli/invite.test.ts +0 -250
  174. package/src/cli/invite.ts +0 -214
  175. package/src/cli/promote.ts +0 -157
  176. package/src/cli/reindex-knowledge.test.ts +0 -307
  177. package/src/cli/reindex-knowledge.ts +0 -450
  178. package/src/cli/reindex.test.ts +0 -957
  179. package/src/cli/reindex.ts +0 -979
  180. package/src/cli/rescue-classify-ordering.test.ts +0 -548
  181. package/src/cli/rescue-clone-diagnostics.test.ts +0 -120
  182. package/src/cli/rescue-core.ts +0 -3011
  183. package/src/cli/rescue-drift-reconcile.test.ts +0 -179
  184. package/src/cli/rescue-drop-dir-symlink.test.ts +0 -224
  185. package/src/cli/rescue-exec-bit-preserve.test.ts +0 -187
  186. package/src/cli/rescue-hq-root-guard.test.ts +0 -232
  187. package/src/cli/rescue-journal-reconcile.test.ts +0 -215
  188. package/src/cli/rescue-mtime-preserve.test.ts +0 -203
  189. package/src/cli/rescue-settings-reconcile.test.ts +0 -637
  190. package/src/cli/rescue-snapshot.test.ts +0 -57
  191. package/src/cli/rescue-snapshot.ts +0 -51
  192. package/src/cli/rescue.reindex.test.ts +0 -63
  193. package/src/cli/rescue.test.ts +0 -131
  194. package/src/cli/rescue.ts +0 -182
  195. package/src/cli/share.test.ts +0 -7843
  196. package/src/cli/share.ts +0 -3663
  197. package/src/cli/sync-scope.test.ts +0 -652
  198. package/src/cli/sync.test.ts +0 -5207
  199. package/src/cli/sync.ts +0 -3470
  200. package/src/cli/tombstones.ts +0 -106
  201. package/src/cli/watch-event-push-conflict.test.ts +0 -234
  202. package/src/client-info.test.ts +0 -214
  203. package/src/client-info.ts +0 -121
  204. package/src/cognito-auth.test.ts +0 -712
  205. package/src/cognito-auth.ts +0 -1422
  206. package/src/company-resolver.test.ts +0 -618
  207. package/src/company-resolver.ts +0 -521
  208. package/src/context.test.ts +0 -583
  209. package/src/context.ts +0 -378
  210. package/src/daemon-worker.ts +0 -26
  211. package/src/daemon.ts +0 -99
  212. package/src/entity-resolver.test.ts +0 -315
  213. package/src/entity-resolver.ts +0 -180
  214. package/src/ignore.test.ts +0 -466
  215. package/src/ignore.ts +0 -469
  216. package/src/index.ts +0 -439
  217. package/src/journal.test.ts +0 -968
  218. package/src/journal.ts +0 -765
  219. package/src/lib/cloud-authoritative.test.ts +0 -45
  220. package/src/lib/cloud-authoritative.ts +0 -59
  221. package/src/lib/conflict-file.ts +0 -86
  222. package/src/lib/conflict-index.ts +0 -289
  223. package/src/lib/conflict.test.ts +0 -348
  224. package/src/lib/describe-error.test.ts +0 -100
  225. package/src/lib/describe-error.ts +0 -58
  226. package/src/lib/exit-codes.ts +0 -24
  227. package/src/lib/machine-id.test.ts +0 -231
  228. package/src/lib/machine-id.ts +0 -175
  229. package/src/lib/net-errors.test.ts +0 -65
  230. package/src/lib/net-errors.ts +0 -86
  231. package/src/lib/readlink-safe.test.ts +0 -43
  232. package/src/lib/readlink-safe.ts +0 -29
  233. package/src/local-path-codec.test.ts +0 -138
  234. package/src/local-path-codec.ts +0 -161
  235. package/src/machine-auth.test.ts +0 -1323
  236. package/src/manifest-reconcile.test.ts +0 -1123
  237. package/src/manifest-reconcile.ts +0 -518
  238. package/src/object-io.test.ts +0 -1221
  239. package/src/object-io.ts +0 -1306
  240. package/src/operation-lock.test.ts +0 -484
  241. package/src/operation-lock.ts +0 -680
  242. package/src/outcome-telemetry.test.ts +0 -498
  243. package/src/outcome-telemetry.ts +0 -639
  244. package/src/personal-vault-exclusions.test.ts +0 -308
  245. package/src/personal-vault-exclusions.ts +0 -354
  246. package/src/personal-vault.test.ts +0 -756
  247. package/src/personal-vault.ts +0 -496
  248. package/src/prefix-coalesce.test.ts +0 -240
  249. package/src/prefix-coalesce.ts +0 -273
  250. package/src/public-surface.test.ts +0 -117
  251. package/src/qmd-reindex.test.ts +0 -877
  252. package/src/qmd-reindex.ts +0 -842
  253. package/src/read-only-state-dir.test.ts +0 -188
  254. package/src/remote-pull.test.ts +0 -1130
  255. package/src/remote-pull.ts +0 -618
  256. package/src/s3.symlink-materialize.test.ts +0 -492
  257. package/src/s3.test.ts +0 -1789
  258. package/src/s3.ts +0 -1532
  259. package/src/schemas/signal-types.test.ts +0 -82
  260. package/src/schemas/signal-types.ts +0 -38
  261. package/src/schemas/source-channels.test.ts +0 -82
  262. package/src/schemas/source-channels.ts +0 -53
  263. package/src/scope-shrink.test.ts +0 -633
  264. package/src/scope-shrink.ts +0 -481
  265. package/src/signals/get.test.ts +0 -310
  266. package/src/signals/get.ts +0 -75
  267. package/src/signals/internals.ts +0 -195
  268. package/src/signals/list.test.ts +0 -420
  269. package/src/signals/list.ts +0 -79
  270. package/src/signals/parse.ts +0 -8
  271. package/src/signals/types.ts +0 -91
  272. package/src/skill-telemetry.test.ts +0 -1825
  273. package/src/skill-telemetry.ts +0 -1439
  274. package/src/sources/get.test.ts +0 -293
  275. package/src/sources/get.ts +0 -66
  276. package/src/sources/internals.ts +0 -198
  277. package/src/sources/list.test.ts +0 -402
  278. package/src/sources/list.ts +0 -84
  279. package/src/sources/parse.ts +0 -43
  280. package/src/sources/types.ts +0 -84
  281. package/src/sync/event-sync.test.ts +0 -594
  282. package/src/sync/event-sync.ts +0 -545
  283. package/src/sync/feature-flags.test.ts +0 -378
  284. package/src/sync/feature-flags.ts +0 -62
  285. package/src/sync/index.ts +0 -76
  286. package/src/sync/lease-client.test.ts +0 -128
  287. package/src/sync/lease-client.ts +0 -207
  288. package/src/sync/logger.test.ts +0 -242
  289. package/src/sync/logger.ts +0 -79
  290. package/src/sync/metrics.test.ts +0 -462
  291. package/src/sync/metrics.ts +0 -213
  292. package/src/sync/pull-scope.ts +0 -265
  293. package/src/sync/push-event.test.ts +0 -266
  294. package/src/sync/push-event.ts +0 -224
  295. package/src/sync/push-receiver.test.ts +0 -566
  296. package/src/sync/push-receiver.ts +0 -1048
  297. package/src/sync/push-transport.ts +0 -231
  298. package/src/sync/realtime-rollout.test.ts +0 -86
  299. package/src/sync/realtime-rollout.ts +0 -262
  300. package/src/sync/state-store.test.ts +0 -194
  301. package/src/sync/state-store.ts +0 -727
  302. package/src/sync-core.ts +0 -58
  303. package/src/sync-progress.test.ts +0 -94
  304. package/src/sync-progress.ts +0 -140
  305. package/src/telemetry-events.test.ts +0 -88
  306. package/src/telemetry-events.ts +0 -205
  307. package/src/telemetry.test.ts +0 -1280
  308. package/src/telemetry.ts +0 -1109
  309. package/src/types.ts +0 -314
  310. package/src/vault-client.test.ts +0 -1380
  311. package/src/vault-client.ts +0 -1694
  312. package/src/version.ts +0 -24
  313. package/src/watch-roots.test.ts +0 -278
  314. package/src/watch-roots.ts +0 -162
  315. package/src/watcher-event-gate.test.ts +0 -212
  316. package/src/watcher.test.ts +0 -1079
  317. package/src/watcher.ts +0 -1741
  318. package/test/e2e/sync/cross-tenant-isolation.test.ts +0 -630
  319. package/test/e2e/sync/skill-telemetry-oversized-transcript.test.ts +0 -124
  320. package/test/e2e/sync/transient-company-leg.test.ts +0 -384
  321. package/test/e2e/sync/windows-unreadable-link-leg.test.ts +0 -191
  322. package/test/e2e/watcher-real-chokidar.test.ts +0 -165
  323. package/test/e2e/watcher-recursive-backend.test.ts +0 -181
  324. package/test/e2e/watcher-scoped-coverage.test.ts +0 -381
  325. package/test/invite-flow.integration.test.ts +0 -244
  326. package/test/joiner-manifest-reconcile.integration.test.ts +0 -322
  327. package/test/share-sync.integration.test.ts +0 -213
  328. package/tsconfig.json +0 -19
  329. package/vitest.config.ts +0 -22
@@ -1,378 +0,0 @@
1
- /**
2
- * US-008 — client push transport unit tests.
3
- *
4
- * The PRD names this file as the home for US-008's coverage; it spans the
5
- * four required assertions across the three US-008 modules:
6
- *
7
- * 1. flag gating — feature-flags.ts (env list, legacy global,
8
- * static provider, malformed-entry tolerance)
9
- * 2. POST — push-transport.ts HttpPushTransport posts an
10
- * encoded PushEvent to `<apiUrl>/sync/push` with
11
- * a Bearer token (mocked fetch — no real server)
12
- * 3. emit — watcher.ts PushEventEmitter builds a valid
13
- * PushEvent (sha256 hash, monotonic seq, tenant)
14
- * per debounced change and hands it to the
15
- * transport; dormant when the flag is OFF
16
- * 4. transport failure — a publish rejection (or hash error) is caught,
17
- * surfaced via onError, and never crashes / never
18
- * propagates (cadence poll is the safety net)
19
- *
20
- * The live `/sync/push` endpoint is NOT deployed yet — every POST is asserted
21
- * against a mocked fetch / mocked transport.
22
- */
23
-
24
- import { createHash } from "node:crypto";
25
- import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
26
- import { tmpdir } from "node:os";
27
- import path from "node:path";
28
- import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
29
-
30
- import {
31
- EVENT_SYNC_LOCAL_DISABLE_ENV_VAR,
32
- isEventSyncLocallyDisabled,
33
- } from "./feature-flags.js";
34
- import {
35
- HttpPushTransport,
36
- NoopPushTransport,
37
- type FetchLike,
38
- type PushTransport,
39
- } from "./push-transport.js";
40
- import { decodePushEvent, type PushEvent } from "./push-event.js";
41
- import { PushEventEmitter } from "../watcher.js";
42
- import type { TreeChangeBatch } from "../watcher.js";
43
-
44
- // ─── Helpers ─────────────────────────────────────────────────────────────────
45
-
46
- function fakePushEvent(overrides: Partial<PushEvent> = {}): PushEvent {
47
- return {
48
- relativePath: "personal/notes/a.md",
49
- kind: "upsert",
50
- contentHash: `sha256:${"a".repeat(64)}`,
51
- mtime: "2026-05-21T12:00:00.000Z",
52
- originDeviceId: "device-1",
53
- originTenantId: "indigo",
54
- sequenceNumber: 0,
55
- eventTimestamp: "2026-05-21T12:00:01.000Z",
56
- ...overrides,
57
- };
58
- }
59
-
60
- /** A fetch double that records calls and returns a configurable response. */
61
- function makeFetch(
62
- res: { ok: boolean; status: number; body?: string } = {
63
- ok: true,
64
- status: 200,
65
- },
66
- ): { fetch: FetchLike; calls: Array<{ url: string; init: unknown }> } {
67
- const calls: Array<{ url: string; init: unknown }> = [];
68
- const fetch: FetchLike = async (url, init) => {
69
- calls.push({ url, init });
70
- return {
71
- ok: res.ok,
72
- status: res.status,
73
- text: async () => res.body ?? "",
74
- };
75
- };
76
- return { fetch, calls };
77
- }
78
-
79
- // ─── 1. Local disable (feature-flags.ts) ─────────────────────────────────────
80
-
81
- const serverGranted = { isEnabled: () => true };
82
- const serverDenied = { isEnabled: () => false };
83
-
84
- describe("Event sync local control", () => {
85
- it("exports only the local disable control", () => {
86
- expect(EVENT_SYNC_LOCAL_DISABLE_ENV_VAR).toBe("HQ_SYNC_EVENT_SYNC");
87
- });
88
-
89
- it.each(["0", "false", "no", "off", " OFF "])(
90
- "recognizes %j as a local disable",
91
- (value) => expect(isEventSyncLocallyDisabled(value)).toBe(true),
92
- );
93
-
94
- it.each([undefined, "", "1", "true", "yes", "on", "indigo,acme"])(
95
- "%j does not grant authority",
96
- (value) => expect(isEventSyncLocallyDisabled(value)).toBe(false),
97
- );
98
- });
99
-
100
- // ─── 2. POST (HttpPushTransport) ──────────────────────────────────────────────
101
-
102
- describe("HttpPushTransport — POST /sync/push", () => {
103
- it("POSTs an encoded PushEvent to <apiUrl>/sync/push with a Bearer token", async () => {
104
- const { fetch, calls } = makeFetch({ ok: true, status: 200 });
105
- const transport = new HttpPushTransport({
106
- apiUrl: "https://vault-api.example.com/",
107
- authToken: "tok-123",
108
- fetchImpl: fetch,
109
- });
110
- await transport.start();
111
- expect(transport.connected).toBe(true);
112
-
113
- const event = fakePushEvent();
114
- await transport.publish(event);
115
-
116
- expect(calls).toHaveLength(1);
117
- const call = calls[0];
118
- // Trailing slash on apiUrl is normalized; default path is /sync/push.
119
- expect(call.url).toBe("https://vault-api.example.com/sync/push");
120
- const init = call.init as {
121
- method: string;
122
- headers: Record<string, string>;
123
- body: string;
124
- };
125
- expect(init.method).toBe("POST");
126
- expect(init.headers.Authorization).toBe("Bearer tok-123");
127
- expect(init.headers["Content-Type"]).toBe("application/json");
128
- // Body is a valid encoded PushEvent that round-trips.
129
- expect(decodePushEvent(init.body)).toEqual(event);
130
- });
131
-
132
- it("POSTs a delete tombstone without contentHash or mtime", async () => {
133
- const { fetch, calls } = makeFetch({ ok: true, status: 200 });
134
- const transport = new HttpPushTransport({
135
- apiUrl: "https://vault-api.example.com/",
136
- authToken: "tok-123",
137
- fetchImpl: fetch,
138
- });
139
- const event = fakePushEvent({
140
- kind: "delete",
141
- contentHash: undefined,
142
- mtime: undefined,
143
- });
144
-
145
- await transport.publish(event);
146
-
147
- expect(calls).toHaveLength(1);
148
- const body = (calls[0].init as { body: string }).body;
149
- expect(JSON.parse(body)).toEqual({
150
- relativePath: "personal/notes/a.md",
151
- kind: "delete",
152
- originDeviceId: "device-1",
153
- originTenantId: "indigo",
154
- sequenceNumber: 0,
155
- eventTimestamp: "2026-05-21T12:00:01.000Z",
156
- });
157
- expect(decodePushEvent(body)).toEqual(JSON.parse(body));
158
- });
159
-
160
- it("resolves the auth token per-request via an async getter (self-heals across refresh)", async () => {
161
- const { fetch, calls } = makeFetch();
162
- let n = 0;
163
- const transport = new HttpPushTransport({
164
- apiUrl: "https://api.example.com",
165
- authToken: () => `tok-${n++}`,
166
- fetchImpl: fetch,
167
- });
168
- await transport.publish(fakePushEvent());
169
- await transport.publish(fakePushEvent());
170
- expect((calls[0].init as any).headers.Authorization).toBe("Bearer tok-0");
171
- expect((calls[1].init as any).headers.Authorization).toBe("Bearer tok-1");
172
- });
173
-
174
- it("honors a custom pushPath + extra headers", async () => {
175
- const { fetch, calls } = makeFetch();
176
- const transport = new HttpPushTransport({
177
- apiUrl: "https://api.example.com",
178
- pushPath: "v1/sync/push",
179
- authToken: "t",
180
- headers: { "x-hq-client-name": "hq-sync-runner" },
181
- fetchImpl: fetch,
182
- });
183
- await transport.publish(fakePushEvent());
184
- expect(calls[0].url).toBe("https://api.example.com/v1/sync/push");
185
- expect((calls[0].init as any).headers["x-hq-client-name"]).toBe(
186
- "hq-sync-runner",
187
- );
188
- });
189
-
190
- it("rejects when apiUrl is missing", () => {
191
- expect(
192
- () => new HttpPushTransport({ apiUrl: "", authToken: "t" }),
193
- ).toThrow(/apiUrl is required/);
194
- });
195
-
196
- it("rejects (does not swallow) on a non-2xx response", async () => {
197
- const { fetch } = makeFetch({ ok: false, status: 403, body: "forbidden" });
198
- const transport = new HttpPushTransport({
199
- apiUrl: "https://api.example.com",
200
- authToken: "t",
201
- fetchImpl: fetch,
202
- });
203
- await expect(transport.publish(fakePushEvent())).rejects.toThrow(/403/);
204
- });
205
-
206
- it("rejects on a malformed event before hitting the network", async () => {
207
- const { fetch, calls } = makeFetch();
208
- const transport = new HttpPushTransport({
209
- apiUrl: "https://api.example.com",
210
- authToken: "t",
211
- fetchImpl: fetch,
212
- });
213
- await expect(
214
- transport.publish(fakePushEvent({ contentHash: "not-a-hash" })),
215
- ).rejects.toBeTruthy();
216
- expect(calls).toHaveLength(0);
217
- });
218
- });
219
-
220
- // ─── 3 + 4. Emit + gating + failure handling (PushEventEmitter) ───────────────
221
-
222
- describe("PushEventEmitter — emit, gating, failure handling", () => {
223
- let dir: string;
224
- let filePath: string;
225
- const REL = "a.md";
226
-
227
- beforeEach(() => {
228
- dir = mkdtempSync(path.join(tmpdir(), "us008-"));
229
- filePath = path.join(dir, REL);
230
- writeFileSync(filePath, "hello world");
231
- });
232
-
233
- afterEach(() => {
234
- rmSync(dir, { recursive: true, force: true });
235
- });
236
-
237
- function batchFor(...rels: string[]): TreeChangeBatch {
238
- const paths = new Map<string, string>();
239
- for (const rel of rels) paths.set(path.join(dir, rel), rel);
240
- return { paths };
241
- }
242
-
243
- it("emits a valid PushEvent (sha256 hash, tenant) per changed path and ships it", async () => {
244
- const transport = new NoopPushTransport();
245
- const published: PushEvent[] = [];
246
- const spyTransport: PushTransport = {
247
- start: () => transport.start(),
248
- dispose: () => transport.dispose(),
249
- get connected() {
250
- return transport.connected;
251
- },
252
- publish: async (e) => {
253
- published.push(e);
254
- },
255
- };
256
- const emitter = new PushEventEmitter({
257
- originTenantId: "indigo",
258
- originDeviceId: "dev-1",
259
- transport: spyTransport,
260
- flagProvider: serverGranted,
261
- now: () => new Date("2026-05-21T00:00:00.000Z"),
262
- });
263
-
264
- await emitter.emitForBatch(batchFor(REL));
265
-
266
- expect(published).toHaveLength(1);
267
- const e = published[0];
268
- expect(e.relativePath).toBe(REL);
269
- expect(e.kind).toBe("upsert");
270
- expect(e.originTenantId).toBe("indigo");
271
- expect(e.originDeviceId).toBe("dev-1");
272
- const expectedHex = createHash("sha256").update("hello world").digest("hex");
273
- expect(e.contentHash).toBe(`sha256:${expectedHex}`);
274
- // Round-trips through the wire contract.
275
- expect(() => decodePushEvent(e)).not.toThrow();
276
- });
277
-
278
- it("assigns monotonic per-device sequence numbers across emits", async () => {
279
- const published: PushEvent[] = [];
280
- const emitter = new PushEventEmitter({
281
- originTenantId: "indigo",
282
- originDeviceId: "dev-1",
283
- transport: { start: async () => {}, dispose: async () => {}, connected: true, publish: async (e) => { published.push(e); } },
284
- flagProvider: serverGranted,
285
- });
286
- writeFileSync(path.join(dir, "b.md"), "second");
287
- await emitter.emitForBatch(batchFor(REL));
288
- await emitter.emitForBatch(batchFor("b.md"));
289
- expect(published.map((e) => e.sequenceNumber)).toEqual([0, 1]);
290
- });
291
-
292
- it("is DORMANT when the flag is OFF: ships nothing, attach is a no-op", async () => {
293
- const publish = vi.fn(async () => {});
294
- const emitter = new PushEventEmitter({
295
- originTenantId: "indigo",
296
- originDeviceId: "dev-1",
297
- transport: { start: async () => {}, dispose: async () => {}, connected: false, publish },
298
- flagProvider: serverDenied, // indigo NOT enabled
299
- });
300
- expect(emitter.enabled).toBe(false);
301
- await emitter.emitForBatch(batchFor(REL));
302
- expect(publish).not.toHaveBeenCalled();
303
- // attach returns a callable no-op when dormant.
304
- const fakeWatcher = { onChange: vi.fn() } as any;
305
- const off = emitter.attach(fakeWatcher);
306
- expect(fakeWatcher.onChange).not.toHaveBeenCalled();
307
- expect(() => off()).not.toThrow();
308
- });
309
-
310
- it("transport publish failure is caught + surfaced, never throws (cadence safety net)", async () => {
311
- const onError = vi.fn();
312
- const emitter = new PushEventEmitter({
313
- originTenantId: "indigo",
314
- originDeviceId: "dev-1",
315
- transport: {
316
- start: async () => {},
317
- dispose: async () => {},
318
- connected: true,
319
- publish: async () => {
320
- throw new Error("network down");
321
- },
322
- },
323
- flagProvider: serverGranted,
324
- onError,
325
- });
326
- // Must not reject — the daemon stays alive.
327
- await expect(emitter.emitForBatch(batchFor(REL))).resolves.toBeUndefined();
328
- expect(onError).toHaveBeenCalledTimes(1);
329
- expect(onError.mock.calls[0][0]).toBeInstanceOf(Error);
330
- expect(onError.mock.calls[0][1]).toMatchObject({ relativePath: REL });
331
- });
332
-
333
- it("a missing file emits a delete tombstone, and other paths still ship", async () => {
334
- const published: PushEvent[] = [];
335
- const onError = vi.fn();
336
- const emitter = new PushEventEmitter({
337
- originTenantId: "indigo",
338
- originDeviceId: "dev-1",
339
- transport: { start: async () => {}, dispose: async () => {}, connected: true, publish: async (e) => { published.push(e); } },
340
- flagProvider: serverGranted,
341
- onError,
342
- });
343
- // REL exists; "ghost.md" does not.
344
- await emitter.emitForBatch(batchFor(REL, "ghost.md"));
345
- expect(published.map((e) => e.relativePath).sort()).toEqual([
346
- REL,
347
- "ghost.md",
348
- ].sort());
349
- const tombstone = published.find((e) => e.relativePath === "ghost.md");
350
- expect(tombstone).toMatchObject({ kind: "delete" });
351
- expect(tombstone).not.toHaveProperty("contentHash");
352
- expect(tombstone).not.toHaveProperty("mtime");
353
- expect(onError).not.toHaveBeenCalled();
354
- });
355
-
356
- it("end-to-end: emit → HttpPushTransport POST with a valid schema (mocked fetch)", async () => {
357
- const { fetch, calls } = makeFetch({ ok: true, status: 200 });
358
- const transport = new HttpPushTransport({
359
- apiUrl: "https://api.example.com",
360
- authToken: "tok",
361
- fetchImpl: fetch,
362
- });
363
- const emitter = new PushEventEmitter({
364
- originTenantId: "indigo",
365
- originDeviceId: "dev-1",
366
- transport,
367
- flagProvider: serverGranted,
368
- });
369
- await transport.start();
370
- await emitter.emitForBatch(batchFor(REL));
371
-
372
- expect(calls).toHaveLength(1);
373
- expect(calls[0].url).toBe("https://api.example.com/sync/push");
374
- const posted = decodePushEvent((calls[0].init as any).body);
375
- expect(posted.relativePath).toBe(REL);
376
- expect(posted.originTenantId).toBe("indigo");
377
- });
378
- });
@@ -1,62 +0,0 @@
1
- /**
2
- * Local control for the dormant event-sync wiring.
3
- *
4
- * U15 removes client-side positive selectors. Environment values, tenant IDs,
5
- * and cached identity cannot grant authority; the authenticated server
6
- * inventory is the sole positive authority. This module preserves only the
7
- * operator's local off-switch.
8
- */
9
-
10
- /** A local-only kill switch. Any other value is deliberately non-authorizing. */
11
- export const EVENT_SYNC_LOCAL_DISABLE_ENV_VAR = "HQ_SYNC_EVENT_SYNC";
12
-
13
- /**
14
- * Compatibility seam consumed by the existing emitter. Production event-sync
15
- * supplies it only after authenticated server inventory has granted access.
16
- */
17
- export interface EventDrivenPushFlagProvider {
18
- isEnabled(scopeId: string): boolean;
19
- }
20
-
21
- /** Deprecated inert compatibility shape; no environment is ever read. */
22
- export type FlagProviderWarnFn = (
23
- message: string,
24
- context: Record<string, unknown>,
25
- ) => void;
26
-
27
- /** Deprecated inert compatibility shape; no environment is ever read. */
28
- export interface EnvTenantListFlagProviderOptions {
29
- env?: Record<string, string | undefined>;
30
- warn?: FlagProviderWarnFn;
31
- }
32
-
33
- /**
34
- * Compatibility fixture for older direct emitter tests. It represents a
35
- * previously authenticated server inventory result; the supplied legacy value
36
- * is deliberately ignored, so it cannot select a tenant or grant by email.
37
- */
38
- export class StaticFlagProvider implements EventDrivenPushFlagProvider {
39
- constructor(_authenticatedLegacyInventory: unknown) {}
40
-
41
- isEnabled(_scopeId: string): boolean {
42
- return true;
43
- }
44
- }
45
-
46
- /**
47
- * Returns true only for an explicit local disable value. Values such as
48
- * `true`, `1`, a tenant list, or an unrecognized string never enable sync.
49
- */
50
- export function isEventSyncLocallyDisabled(
51
- value: string | undefined,
52
- ): boolean {
53
- switch ((value ?? "").trim().toLowerCase()) {
54
- case "0":
55
- case "false":
56
- case "no":
57
- case "off":
58
- return true;
59
- default:
60
- return false;
61
- }
62
- }
package/src/sync/index.ts DELETED
@@ -1,76 +0,0 @@
1
- /**
2
- * @indigoai-us/hq-cloud — event-driven sync (`src/sync`) barrel.
3
- *
4
- * Surfaces the PushEvent wire contract + the PushTransport shipping seam
5
- * ported from hq-pro PR #112 (project event-driven-sync-menubar US-007).
6
- */
7
-
8
- export {
9
- CONTENT_HASH_PATTERN,
10
- ISO8601_DATETIME_PATTERN,
11
- PushEventSchema,
12
- PushEventDecodeError,
13
- encodePushEvent,
14
- decodePushEvent,
15
- } from "./push-event.js";
16
- export type {
17
- PushEvent,
18
- PushEventInput,
19
- PushEventDecodeIssue,
20
- } from "./push-event.js";
21
-
22
- export { NoopPushTransport, HttpPushTransport } from "./push-transport.js";
23
- export type {
24
- PushTransport,
25
- HttpPushTransportOptions,
26
- AuthTokenSource,
27
- FetchLike,
28
- } from "./push-transport.js";
29
-
30
- export {
31
- EVENT_SYNC_LOCAL_DISABLE_ENV_VAR,
32
- isEventSyncLocallyDisabled,
33
- StaticFlagProvider,
34
- } from "./feature-flags.js";
35
- export type {
36
- EventDrivenPushFlagProvider,
37
- FlagProviderWarnFn,
38
- EnvTenantListFlagProviderOptions,
39
- } from "./feature-flags.js";
40
-
41
- export {
42
- NoopPushReceiver,
43
- SqsPushReceiver,
44
- InMemoryFanout,
45
- InMemoryPushReceiver,
46
- createPushReceiver,
47
- DEFAULT_RECEIVER_DISPOSE_DRAIN_MS,
48
- DEFAULT_WAIT_TIME_SECONDS,
49
- DEFAULT_MAX_MESSAGES,
50
- } from "./push-receiver.js";
51
- export type {
52
- PushReceiver,
53
- PushReceiverContext,
54
- SyncEngineFn,
55
- PublishMetricFn,
56
- ReceiverLogger,
57
- SqsClientLike,
58
- SqsMessageLike,
59
- SqsPushReceiverOptions,
60
- InMemoryPushReceiverOptions,
61
- CreatePushReceiverOptions,
62
- } from "./push-receiver.js";
63
-
64
- export {
65
- SYNC_METRIC_NAMESPACE,
66
- SYNC_LATENCY_METRIC_NAME,
67
- _setSyncCloudWatchClient,
68
- publishSyncLatencyMetric,
69
- } from "./metrics.js";
70
- export type {
71
- SyncLatencyMetric,
72
- PublishSyncLatencyMetricOptions,
73
- } from "./metrics.js";
74
-
75
- export { createLogger } from "./logger.js";
76
- export type { CreateLoggerOptions, Logger } from "./logger.js";
@@ -1,128 +0,0 @@
1
- import { describe, expect, it, vi } from "vitest";
2
-
3
- import { RealtimeLeaseClient, RealtimePassAuthorizer } from "./lease-client.js";
4
- import { RealtimeRolloutClient, type RealtimeCapabilityVector } from "./realtime-rollout.js";
5
-
6
- const capability: RealtimeCapabilityVector = {
7
- hqCloud: "6.14.49",
8
- hqDesktop: "none",
9
- protocol: 2,
10
- journal: 3,
11
- durableApply: 1,
12
- };
13
-
14
- function jsonResponse(body: unknown, status = 200) {
15
- return { ok: status >= 200 && status < 300, status, text: async () => JSON.stringify(body) };
16
- }
17
-
18
- describe("RealtimeLeaseClient", () => {
19
- it("uses the pass bearer and rejects a lease longer than sixty seconds", async () => {
20
- let received: { method?: string; headers?: Record<string, string> } | undefined;
21
- const fetchImpl = async (_url: string, init?: { method?: string; headers?: Record<string, string> }) => {
22
- received = init;
23
- return jsonResponse({
24
- lease: "lease_1",
25
- issuedAt: "2026-08-12T00:00:00.000Z",
26
- expiresAt: "2026-08-12T00:01:01.000Z",
27
- authorizationEpoch: "epoch_1",
28
- });
29
- };
30
- const client = new RealtimeLeaseClient({ apiUrl: "https://api.example", fetchImpl, now: () => Date.parse("2026-08-12T00:00:00.000Z") });
31
-
32
- await expect(client.issue({ bearer: "token-a", deviceId: "machine_a", capabilityVector: capability, scopeHandle: "handle_1", scopeRevision: "rev_1" })).rejects.toThrow("invalid lease lifetime");
33
- expect(received).toMatchObject({
34
- method: "POST",
35
- headers: expect.objectContaining({ Authorization: "Bearer token-a", "X-HQ-Sync-Contract": "2" }),
36
- });
37
- });
38
-
39
- it("rejects a lease issued too far into the future", async () => {
40
- const fetchImpl = async () => jsonResponse({
41
- lease: "lease_1",
42
- issuedAt: "2026-08-12T00:00:06.000Z",
43
- expiresAt: "2026-08-12T00:01:00.000Z",
44
- authorizationEpoch: "epoch_1",
45
- });
46
- const client = new RealtimeLeaseClient({ apiUrl: "https://api.example", fetchImpl, now: () => Date.parse("2026-08-12T00:00:00.000Z") });
47
- await expect(client.issue({ bearer: "token-a", deviceId: "machine_a", capabilityVector: capability, scopeHandle: "handle_1", scopeRevision: "rev_1" })).rejects.toThrow("invalid lease lifetime");
48
- });
49
-
50
- it("resolves one bearer and freshly leases every selected scope before I/O", async () => {
51
- const now = Date.parse("2026-08-12T00:00:00.000Z");
52
- const requests: Array<{ headers?: Record<string, string> }> = [];
53
- const fetchImpl = async (url: string, init?: { headers?: Record<string, string> }) => {
54
- requests.push({ headers: init?.headers });
55
- if (url.endsWith("/scopes")) {
56
- return jsonResponse({ scopes: [
57
- { scopeUid: "prs_1", scopeHandle: "handle_1", scopeRevision: "rev_1", state: "v2-ready", cursor: "cursor_1" },
58
- { scopeUid: "cmp_1", scopeHandle: "handle_2", scopeRevision: "rev_2", state: "v2-ready", cursor: "cursor_2" },
59
- ] });
60
- }
61
- return jsonResponse({
62
- lease: "lease_issued",
63
- issuedAt: "2026-08-12T00:00:00.000Z",
64
- expiresAt: "2026-08-12T00:01:00.000Z",
65
- authorizationEpoch: "epoch_1",
66
- }, 201);
67
- };
68
- const token = vi.fn(async () => "token-a");
69
- const inventory = new RealtimeRolloutClient({ apiUrl: "https://api.example", fetchImpl, now: () => now });
70
- const leases = new RealtimeLeaseClient({ apiUrl: "https://api.example", fetchImpl, now: () => now });
71
- const authorizer = new RealtimePassAuthorizer(token, inventory, leases);
72
- const operation = vi.fn(async () => "done");
73
-
74
- await expect(authorizer.runBeforeIo({
75
- deviceId: "machine_a",
76
- capabilityVector: capability,
77
- configurationFingerprint: "config-a",
78
- scopeFingerprint: "prs_1,cmp_1",
79
- scopeUids: ["prs_1", "cmp_1"],
80
- }, operation)).resolves.toBe("done");
81
-
82
- expect(token).toHaveBeenCalledTimes(1);
83
- expect(requests).toHaveLength(3);
84
- for (const call of requests) expect(call.headers?.Authorization).toBe("Bearer token-a");
85
- expect(operation).toHaveBeenCalledTimes(1);
86
- });
87
-
88
- it("does not invoke I/O when inventory loses a requested scope", async () => {
89
- const fetchImpl = vi.fn(async () => jsonResponse({ scopes: [
90
- { scopeUid: "prs_1", scopeHandle: "handle_1", scopeRevision: "rev_1", state: "paused", reasonCode: "maintenance" },
91
- ] }));
92
- const inventory = new RealtimeRolloutClient({ apiUrl: "https://api.example", fetchImpl });
93
- const leases = new RealtimeLeaseClient({ apiUrl: "https://api.example", fetchImpl });
94
- const authorizer = new RealtimePassAuthorizer("token-a", inventory, leases);
95
- const operation = vi.fn(async () => undefined);
96
-
97
- await expect(authorizer.runBeforeIo({
98
- deviceId: "machine_a", capabilityVector: capability, configurationFingerprint: "config-a", scopeFingerprint: "prs_1", scopeUids: ["prs_1"],
99
- }, operation)).rejects.toThrow("does not authorize every requested scope");
100
- expect(operation).not.toHaveBeenCalled();
101
- });
102
-
103
- it("does not invoke I/O when parallel lease issues disagree on epoch", async () => {
104
- let leaseCalls = 0;
105
- const fetchImpl = async (url: string) => {
106
- if (url.endsWith("/scopes")) return jsonResponse({ scopes: [
107
- { scopeUid: "prs_1", scopeHandle: "handle_1", scopeRevision: "rev_1", state: "v2-ready", cursor: "cursor_1" },
108
- { scopeUid: "cmp_1", scopeHandle: "handle_2", scopeRevision: "rev_2", state: "v2-ready", cursor: "cursor_2" },
109
- ] });
110
- leaseCalls += 1;
111
- return jsonResponse({
112
- lease: `lease_${leaseCalls}`,
113
- issuedAt: "2026-08-12T00:00:00.000Z",
114
- expiresAt: "2026-08-12T00:00:30.000Z",
115
- authorizationEpoch: `epoch_${leaseCalls}`,
116
- }, 201);
117
- };
118
- const now = () => Date.parse("2026-08-12T00:00:00.000Z");
119
- const inventory = new RealtimeRolloutClient({ apiUrl: "https://api.example", fetchImpl, now });
120
- const leases = new RealtimeLeaseClient({ apiUrl: "https://api.example", fetchImpl, now });
121
- const authorizer = new RealtimePassAuthorizer("token-a", inventory, leases);
122
- const operation = vi.fn(async () => undefined);
123
- await expect(authorizer.runBeforeIo({
124
- deviceId: "machine_a", capabilityVector: capability, configurationFingerprint: "config-a", scopeFingerprint: "all", scopeUids: ["prs_1", "cmp_1"],
125
- }, operation)).rejects.toThrow("leases disagree on authorization epoch");
126
- expect(operation).not.toHaveBeenCalled();
127
- });
128
- });