@quolu/lattice 0.1.0 → 0.12.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 (370) hide show
  1. package/README.md +87 -4
  2. package/bin/lattice-bridge.mjs +87 -0
  3. package/bin/lattice-dashboard.mjs +70 -0
  4. package/bin/lattice-mcp.mjs +9 -9
  5. package/bin/lattice.mjs +61 -8
  6. package/docs/bridge-setup.md +87 -0
  7. package/docs/schemas/lattice.plan_create_input.v1.schema.json +56 -0
  8. package/docs/schemas/lattice.plan_create_input.v2.schema.json +72 -0
  9. package/docs/schemas/lattice.plan_create_input.v3.schema.json +81 -0
  10. package/package.json +19 -7
  11. package/sensor/dist/bin/command-supervision.d.ts +1 -1
  12. package/sensor/dist/bin/command-supervision.js +4 -4
  13. package/sensor/dist/bin/command-supervision.js.map +1 -1
  14. package/sensor/dist/bin/fatal-handler.js +2 -2
  15. package/sensor/dist/bin/fatal-handler.js.map +1 -1
  16. package/sensor/dist/bin/{codegraph.js → lattice-sensor.js} +207 -328
  17. package/sensor/dist/bin/node-version-check.d.ts +7 -5
  18. package/sensor/dist/bin/node-version-check.d.ts.map +1 -1
  19. package/sensor/dist/bin/node-version-check.js +19 -14
  20. package/sensor/dist/bin/node-version-check.js.map +1 -1
  21. package/sensor/dist/context/index.d.ts +1 -1
  22. package/sensor/dist/context/index.js +6 -6
  23. package/sensor/dist/context/index.js.map +1 -1
  24. package/sensor/dist/db/index.d.ts +5 -5
  25. package/sensor/dist/db/index.d.ts.map +1 -1
  26. package/sensor/dist/db/index.js +7 -7
  27. package/sensor/dist/db/index.js.map +1 -1
  28. package/sensor/dist/db/migrations.js +1 -1
  29. package/sensor/dist/db/migrations.js.map +1 -1
  30. package/sensor/dist/db/queries.d.ts +1 -1
  31. package/sensor/dist/db/queries.js +5 -5
  32. package/sensor/dist/db/queries.js.map +1 -1
  33. package/sensor/dist/db/schema.sql +2 -2
  34. package/sensor/dist/db/sqlite-adapter.d.ts +2 -2
  35. package/sensor/dist/db/sqlite-adapter.js +3 -3
  36. package/sensor/dist/db/sqlite-adapter.js.map +1 -1
  37. package/sensor/dist/db/wal-valve.d.ts +1 -1
  38. package/sensor/dist/db/wal-valve.d.ts.map +1 -1
  39. package/sensor/dist/db/wal-valve.js +2 -2
  40. package/sensor/dist/db/wal-valve.js.map +1 -1
  41. package/sensor/dist/directory.d.ts +27 -63
  42. package/sensor/dist/directory.d.ts.map +1 -1
  43. package/sensor/dist/directory.js +78 -134
  44. package/sensor/dist/directory.js.map +1 -1
  45. package/sensor/dist/errors.d.ts +15 -15
  46. package/sensor/dist/errors.d.ts.map +1 -1
  47. package/sensor/dist/errors.js +21 -21
  48. package/sensor/dist/errors.js.map +1 -1
  49. package/sensor/dist/extraction/dynamic-import.d.ts +1 -1
  50. package/sensor/dist/extraction/dynamic-import.js +1 -1
  51. package/sensor/dist/extraction/extraction-version.d.ts +1 -1
  52. package/sensor/dist/extraction/extraction-version.js +1 -1
  53. package/sensor/dist/extraction/generated-detection.d.ts +1 -1
  54. package/sensor/dist/extraction/generated-detection.js +1 -1
  55. package/sensor/dist/extraction/grammars.d.ts +3 -3
  56. package/sensor/dist/extraction/grammars.js +5 -5
  57. package/sensor/dist/extraction/grammars.js.map +1 -1
  58. package/sensor/dist/extraction/index.d.ts +6 -6
  59. package/sensor/dist/extraction/index.js +50 -50
  60. package/sensor/dist/extraction/index.js.map +1 -1
  61. package/sensor/dist/extraction/kernel/decode.d.ts +1 -1
  62. package/sensor/dist/extraction/kernel/decode.js +1 -1
  63. package/sensor/dist/extraction/kernel/index.d.ts +3 -3
  64. package/sensor/dist/extraction/kernel/index.js +6 -6
  65. package/sensor/dist/extraction/kernel/index.js.map +1 -1
  66. package/sensor/dist/extraction/kernel/layout.d.ts +1 -1
  67. package/sensor/dist/extraction/kernel/layout.js +1 -1
  68. package/sensor/dist/extraction/kernel/loader.d.ts +6 -6
  69. package/sensor/dist/extraction/kernel/loader.js +13 -13
  70. package/sensor/dist/extraction/kernel/loader.js.map +1 -1
  71. package/sensor/dist/extraction/languages/cfquery.d.ts +1 -1
  72. package/sensor/dist/extraction/languages/cfquery.js +1 -1
  73. package/sensor/dist/extraction/languages/lua.d.ts +11 -0
  74. package/sensor/dist/extraction/languages/lua.d.ts.map +1 -1
  75. package/sensor/dist/extraction/languages/lua.js +1 -0
  76. package/sensor/dist/extraction/languages/lua.js.map +1 -1
  77. package/sensor/dist/extraction/parse-pool.d.ts +5 -5
  78. package/sensor/dist/extraction/parse-pool.d.ts.map +1 -1
  79. package/sensor/dist/extraction/parse-pool.js +4 -4
  80. package/sensor/dist/extraction/parse-worker.js +1 -1
  81. package/sensor/dist/extraction/parse-worker.js.map +1 -1
  82. package/sensor/dist/extraction/store-writer.d.ts +1 -1
  83. package/sensor/dist/extraction/store-writer.js +1 -1
  84. package/sensor/dist/extraction/svelte-extractor.d.ts +1 -1
  85. package/sensor/dist/extraction/svelte-extractor.js +1 -1
  86. package/sensor/dist/extraction/tree-sitter-helpers.js +1 -1
  87. package/sensor/dist/extraction/tree-sitter.d.ts +2 -2
  88. package/sensor/dist/extraction/tree-sitter.d.ts.map +1 -1
  89. package/sensor/dist/extraction/tree-sitter.js +71 -4
  90. package/sensor/dist/extraction/tree-sitter.js.map +1 -1
  91. package/sensor/dist/extraction/wasm-runtime-flags.d.ts +2 -2
  92. package/sensor/dist/extraction/wasm-runtime-flags.d.ts.map +1 -1
  93. package/sensor/dist/extraction/wasm-runtime-flags.js +5 -5
  94. package/sensor/dist/extraction/wasm-runtime-flags.js.map +1 -1
  95. package/sensor/dist/index.d.ts +36 -36
  96. package/sensor/dist/index.d.ts.map +1 -1
  97. package/sensor/dist/index.js +71 -71
  98. package/sensor/dist/index.js.map +1 -1
  99. package/sensor/dist/mcp/daemon-manager.js +1 -1
  100. package/sensor/dist/mcp/daemon-paths.d.ts +3 -3
  101. package/sensor/dist/mcp/daemon-paths.js +7 -7
  102. package/sensor/dist/mcp/daemon-paths.js.map +1 -1
  103. package/sensor/dist/mcp/daemon-registry.d.ts +2 -2
  104. package/sensor/dist/mcp/daemon-registry.js +5 -5
  105. package/sensor/dist/mcp/daemon.d.ts +7 -7
  106. package/sensor/dist/mcp/daemon.d.ts.map +1 -1
  107. package/sensor/dist/mcp/daemon.js +21 -21
  108. package/sensor/dist/mcp/daemon.js.map +1 -1
  109. package/sensor/dist/mcp/dynamic-boundaries.js +1 -1
  110. package/sensor/dist/mcp/engine.d.ts +9 -9
  111. package/sensor/dist/mcp/engine.d.ts.map +1 -1
  112. package/sensor/dist/mcp/engine.js +44 -44
  113. package/sensor/dist/mcp/engine.js.map +1 -1
  114. package/sensor/dist/mcp/index.d.ts +14 -14
  115. package/sensor/dist/mcp/index.d.ts.map +1 -1
  116. package/sensor/dist/mcp/index.js +41 -41
  117. package/sensor/dist/mcp/index.js.map +1 -1
  118. package/sensor/dist/mcp/liveness-watchdog.js +7 -7
  119. package/sensor/dist/mcp/liveness-watchdog.js.map +1 -1
  120. package/sensor/dist/mcp/ppid-watchdog.d.ts +3 -3
  121. package/sensor/dist/mcp/ppid-watchdog.js +2 -2
  122. package/sensor/dist/mcp/proxy.d.ts.map +1 -1
  123. package/sensor/dist/mcp/proxy.js +37 -37
  124. package/sensor/dist/mcp/proxy.js.map +1 -1
  125. package/sensor/dist/mcp/query-pool.d.ts +3 -3
  126. package/sensor/dist/mcp/query-pool.js +7 -7
  127. package/sensor/dist/mcp/query-pool.js.map +1 -1
  128. package/sensor/dist/mcp/query-worker.d.ts +2 -2
  129. package/sensor/dist/mcp/query-worker.js +7 -7
  130. package/sensor/dist/mcp/query-worker.js.map +1 -1
  131. package/sensor/dist/mcp/server-instructions.d.ts +10 -12
  132. package/sensor/dist/mcp/server-instructions.d.ts.map +1 -1
  133. package/sensor/dist/mcp/server-instructions.js +39 -37
  134. package/sensor/dist/mcp/server-instructions.js.map +1 -1
  135. package/sensor/dist/mcp/session.d.ts +2 -2
  136. package/sensor/dist/mcp/session.js +18 -18
  137. package/sensor/dist/mcp/session.js.map +1 -1
  138. package/sensor/dist/mcp/startup-handshake.d.ts +3 -3
  139. package/sensor/dist/mcp/startup-handshake.d.ts.map +1 -1
  140. package/sensor/dist/mcp/startup-handshake.js +3 -3
  141. package/sensor/dist/mcp/startup-handshake.js.map +1 -1
  142. package/sensor/dist/mcp/tools.d.ts +53 -53
  143. package/sensor/dist/mcp/tools.d.ts.map +1 -1
  144. package/sensor/dist/mcp/tools.js +180 -216
  145. package/sensor/dist/mcp/tools.js.map +1 -1
  146. package/sensor/dist/mcp/transport.d.ts +1 -1
  147. package/sensor/dist/mcp/transport.js +6 -6
  148. package/sensor/dist/mcp/transport.js.map +1 -1
  149. package/sensor/dist/mcp/version.d.ts +2 -2
  150. package/sensor/dist/mcp/version.d.ts.map +1 -1
  151. package/sensor/dist/mcp/version.js +3 -3
  152. package/sensor/dist/mcp/version.js.map +1 -1
  153. package/sensor/dist/project-config.d.ts +6 -6
  154. package/sensor/dist/project-config.d.ts.map +1 -1
  155. package/sensor/dist/project-config.js +10 -10
  156. package/sensor/dist/project-config.js.map +1 -1
  157. package/sensor/dist/resolution/callback-synthesizer.js +4 -4
  158. package/sensor/dist/resolution/callback-synthesizer.js.map +1 -1
  159. package/sensor/dist/resolution/frameworks/drupal.d.ts +3 -3
  160. package/sensor/dist/resolution/frameworks/drupal.js +3 -3
  161. package/sensor/dist/resolution/frameworks/fabric.js +1 -1
  162. package/sensor/dist/resolution/frameworks/fabric.js.map +1 -1
  163. package/sensor/dist/resolution/frameworks/java.js +1 -1
  164. package/sensor/dist/resolution/frameworks/java.js.map +1 -1
  165. package/sensor/dist/resolution/frameworks/svelte.js +1 -1
  166. package/sensor/dist/resolution/frameworks/svelte.js.map +1 -1
  167. package/sensor/dist/resolution/goframe-synthesizer.d.ts +1 -1
  168. package/sensor/dist/resolution/goframe-synthesizer.js +1 -1
  169. package/sensor/dist/resolution/index.d.ts +1 -1
  170. package/sensor/dist/resolution/index.js +9 -9
  171. package/sensor/dist/resolution/index.js.map +1 -1
  172. package/sensor/dist/resolution/name-matcher.js +2 -2
  173. package/sensor/dist/resolution/name-matcher.js.map +1 -1
  174. package/sensor/dist/resolution/resolver-pool.d.ts +2 -2
  175. package/sensor/dist/resolution/resolver-pool.js +4 -4
  176. package/sensor/dist/resolution/resolver-pool.js.map +1 -1
  177. package/sensor/dist/resolution/resolver-worker.js +2 -2
  178. package/sensor/dist/resolution/resolver-worker.js.map +1 -1
  179. package/sensor/dist/resolution/workspace-packages.js +1 -1
  180. package/sensor/dist/resolution/workspace-packages.js.map +1 -1
  181. package/sensor/dist/search/identifier-segments.d.ts +1 -1
  182. package/sensor/dist/search/identifier-segments.js +1 -1
  183. package/sensor/dist/search/query-utils.d.ts +1 -0
  184. package/sensor/dist/search/query-utils.d.ts.map +1 -1
  185. package/sensor/dist/search/query-utils.js +10 -3
  186. package/sensor/dist/search/query-utils.js.map +1 -1
  187. package/sensor/dist/sync/git-hooks.d.ts +9 -9
  188. package/sensor/dist/sync/git-hooks.d.ts.map +1 -1
  189. package/sensor/dist/sync/git-hooks.js +15 -15
  190. package/sensor/dist/sync/git-hooks.js.map +1 -1
  191. package/sensor/dist/sync/watch-policy.d.ts +2 -2
  192. package/sensor/dist/sync/watch-policy.js +5 -5
  193. package/sensor/dist/sync/watch-policy.js.map +1 -1
  194. package/sensor/dist/sync/watcher.d.ts +2 -2
  195. package/sensor/dist/sync/watcher.d.ts.map +1 -1
  196. package/sensor/dist/sync/watcher.js +16 -16
  197. package/sensor/dist/sync/watcher.js.map +1 -1
  198. package/sensor/dist/sync/worktree.d.ts +5 -5
  199. package/sensor/dist/sync/worktree.d.ts.map +1 -1
  200. package/sensor/dist/sync/worktree.js +9 -9
  201. package/sensor/dist/sync/worktree.js.map +1 -1
  202. package/sensor/dist/telemetry/index.d.ts +8 -8
  203. package/sensor/dist/telemetry/index.d.ts.map +1 -1
  204. package/sensor/dist/telemetry/index.js +16 -16
  205. package/sensor/dist/telemetry/index.js.map +1 -1
  206. package/sensor/dist/types.d.ts +2 -2
  207. package/sensor/dist/types.js +1 -1
  208. package/sensor/dist/ui/color.d.ts +2 -2
  209. package/sensor/dist/ui/color.js +2 -2
  210. package/sensor/dist/ui/glyphs.d.ts +3 -3
  211. package/sensor/dist/ui/glyphs.js +7 -7
  212. package/sensor/dist/ui/glyphs.js.map +1 -1
  213. package/sensor/dist/utils.d.ts +4 -4
  214. package/sensor/dist/utils.js +8 -8
  215. package/sensor/dist/utils.js.map +1 -1
  216. package/sensor/package.json +8 -13
  217. package/src/artifact-contracts.mjs +2 -2
  218. package/src/boundary-compiler.mjs +28 -28
  219. package/src/boundary-observation-compiler-v2.mjs +10 -10
  220. package/src/bounded-seam.mjs +144 -0
  221. package/src/bridge-cli.mjs +227 -0
  222. package/src/bridge-config.mjs +346 -0
  223. package/src/bridge-daemon.mjs +328 -0
  224. package/src/bridge-launch-agent.mjs +311 -0
  225. package/src/bridge-server.mjs +309 -0
  226. package/src/cli-help.mjs +163 -0
  227. package/src/control-compiler.mjs +30 -30
  228. package/src/dag-chain.mjs +261 -0
  229. package/src/factory-diagnostics.mjs +8 -7
  230. package/src/hash-chain.mjs +76 -0
  231. package/src/node-version-guard.mjs +5 -4
  232. package/src/project-cli.mjs +392 -0
  233. package/src/project-identity.mjs +71 -0
  234. package/src/rc1-evidence-bundle.mjs +16 -16
  235. package/src/rc1-v4-campaign.mjs +15 -14
  236. package/src/rc1-v5-artifact-set.mjs +2 -2
  237. package/src/rc1-v5-campaign.mjs +15 -14
  238. package/src/rc1-v6-artifact-set.mjs +18 -18
  239. package/src/rc1-v6-campaign.mjs +16 -16
  240. package/src/rc1-v6-causal-binding.mjs +16 -14
  241. package/src/rc1-v6-measurement.mjs +20 -38
  242. package/src/rc2-artifact-set.mjs +45 -40
  243. package/src/rc2-campaign.mjs +48 -66
  244. package/src/rc2-delivery-policy-front-end.mjs +30 -30
  245. package/src/rc2-rc1-transfer-front-end.mjs +5 -5
  246. package/src/rc3-actual-dogfood.mjs +6 -6
  247. package/src/rc3-dogfood-scaffold.mjs +12 -7
  248. package/src/rc3-scripted-campaign.mjs +75 -21
  249. package/src/rc4-stage1-dogfood.mjs +24 -7
  250. package/src/runtime-cli.mjs +2424 -45
  251. package/src/runtime-contracts.mjs +7 -7
  252. package/src/runtime-control-store.mjs +547 -0
  253. package/src/runtime-controller-protocol.mjs +517 -0
  254. package/src/runtime-direct-os-observer.mjs +284 -0
  255. package/src/runtime-engine.mjs +2 -2
  256. package/src/runtime-errors.mjs +1 -1
  257. package/src/runtime-event-store.mjs +15 -43
  258. package/src/runtime-front-end.mjs +26 -26
  259. package/src/runtime-gate-store.mjs +477 -0
  260. package/src/runtime-hold-recompile.mjs +214 -23
  261. package/src/runtime-lifecycle-lock.mjs +290 -0
  262. package/src/runtime-managed-supervisor.mjs +1276 -0
  263. package/src/runtime-multi-epoch-store.mjs +761 -0
  264. package/src/runtime-projection.mjs +62 -0
  265. package/src/seam-transform.mjs +38 -38
  266. package/src/{codegraph-adapter.mjs → sensor-adapter.mjs} +7 -7
  267. package/src/sensor-cli.mjs +107 -0
  268. package/src/sensor-runtime.mjs +48 -0
  269. package/src/timestamp-contract.mjs +8 -0
  270. package/src/todo-chain.mjs +178 -0
  271. package/src/todo-cli.mjs +1032 -0
  272. package/src/todo-contracts.mjs +501 -0
  273. package/src/todo-dashboard-registry.mjs +318 -0
  274. package/src/todo-gantt-html.mjs +416 -0
  275. package/src/todo-gantt-layout.mjs +603 -0
  276. package/src/todo-gantt-live.mjs +269 -0
  277. package/src/todo-gantt-presentation.mjs +217 -0
  278. package/src/todo-gantt-svg.mjs +249 -0
  279. package/src/todo-markdown-renderer.mjs +260 -0
  280. package/src/todo-migration.mjs +260 -0
  281. package/src/todo-narrative-anchor.mjs +130 -0
  282. package/src/todo-revision.mjs +425 -0
  283. package/src/todo-status.mjs +340 -0
  284. package/src/todo-store.mjs +3451 -0
  285. package/src/treatment-compiler.mjs +28 -28
  286. package/src/treatment-runner.mjs +42 -44
  287. package/sensor/dist/bin/codegraph.d.ts +0 -26
  288. package/sensor/dist/bin/codegraph.d.ts.map +0 -1
  289. package/sensor/dist/bin/codegraph.js.map +0 -1
  290. package/sensor/dist/bin/uninstall.d.ts +0 -14
  291. package/sensor/dist/bin/uninstall.d.ts.map +0 -1
  292. package/sensor/dist/bin/uninstall.js +0 -36
  293. package/sensor/dist/bin/uninstall.js.map +0 -1
  294. package/sensor/dist/installer/beta-signup.d.ts +0 -54
  295. package/sensor/dist/installer/beta-signup.d.ts.map +0 -1
  296. package/sensor/dist/installer/beta-signup.js +0 -178
  297. package/sensor/dist/installer/beta-signup.js.map +0 -1
  298. package/sensor/dist/installer/config-writer.d.ts +0 -28
  299. package/sensor/dist/installer/config-writer.d.ts.map +0 -1
  300. package/sensor/dist/installer/config-writer.js +0 -91
  301. package/sensor/dist/installer/config-writer.js.map +0 -1
  302. package/sensor/dist/installer/index.d.ts +0 -142
  303. package/sensor/dist/installer/index.d.ts.map +0 -1
  304. package/sensor/dist/installer/index.js +0 -622
  305. package/sensor/dist/installer/index.js.map +0 -1
  306. package/sensor/dist/installer/instructions-template.d.ts +0 -41
  307. package/sensor/dist/installer/instructions-template.d.ts.map +0 -1
  308. package/sensor/dist/installer/instructions-template.js +0 -53
  309. package/sensor/dist/installer/instructions-template.js.map +0 -1
  310. package/sensor/dist/installer/targets/antigravity.d.ts +0 -57
  311. package/sensor/dist/installer/targets/antigravity.d.ts.map +0 -1
  312. package/sensor/dist/installer/targets/antigravity.js +0 -308
  313. package/sensor/dist/installer/targets/antigravity.js.map +0 -1
  314. package/sensor/dist/installer/targets/claude.d.ts +0 -62
  315. package/sensor/dist/installer/targets/claude.d.ts.map +0 -1
  316. package/sensor/dist/installer/targets/claude.js +0 -454
  317. package/sensor/dist/installer/targets/claude.js.map +0 -1
  318. package/sensor/dist/installer/targets/codex.d.ts +0 -18
  319. package/sensor/dist/installer/targets/codex.d.ts.map +0 -1
  320. package/sensor/dist/installer/targets/codex.js +0 -185
  321. package/sensor/dist/installer/targets/codex.js.map +0 -1
  322. package/sensor/dist/installer/targets/cursor.d.ts +0 -35
  323. package/sensor/dist/installer/targets/cursor.d.ts.map +0 -1
  324. package/sensor/dist/installer/targets/cursor.js +0 -254
  325. package/sensor/dist/installer/targets/cursor.js.map +0 -1
  326. package/sensor/dist/installer/targets/gemini.d.ts +0 -26
  327. package/sensor/dist/installer/targets/gemini.d.ts.map +0 -1
  328. package/sensor/dist/installer/targets/gemini.js +0 -165
  329. package/sensor/dist/installer/targets/gemini.js.map +0 -1
  330. package/sensor/dist/installer/targets/hermes.d.ts +0 -18
  331. package/sensor/dist/installer/targets/hermes.d.ts.map +0 -1
  332. package/sensor/dist/installer/targets/hermes.js +0 -359
  333. package/sensor/dist/installer/targets/hermes.js.map +0 -1
  334. package/sensor/dist/installer/targets/kiro.d.ts +0 -27
  335. package/sensor/dist/installer/targets/kiro.d.ts.map +0 -1
  336. package/sensor/dist/installer/targets/kiro.js +0 -178
  337. package/sensor/dist/installer/targets/kiro.js.map +0 -1
  338. package/sensor/dist/installer/targets/opencode.d.ts +0 -38
  339. package/sensor/dist/installer/targets/opencode.d.ts.map +0 -1
  340. package/sensor/dist/installer/targets/opencode.js +0 -288
  341. package/sensor/dist/installer/targets/opencode.js.map +0 -1
  342. package/sensor/dist/installer/targets/registry.d.ts +0 -35
  343. package/sensor/dist/installer/targets/registry.d.ts.map +0 -1
  344. package/sensor/dist/installer/targets/registry.js +0 -91
  345. package/sensor/dist/installer/targets/registry.js.map +0 -1
  346. package/sensor/dist/installer/targets/shared.d.ts +0 -101
  347. package/sensor/dist/installer/targets/shared.d.ts.map +0 -1
  348. package/sensor/dist/installer/targets/shared.js +0 -264
  349. package/sensor/dist/installer/targets/shared.js.map +0 -1
  350. package/sensor/dist/installer/targets/toml.d.ts +0 -52
  351. package/sensor/dist/installer/targets/toml.d.ts.map +0 -1
  352. package/sensor/dist/installer/targets/toml.js +0 -147
  353. package/sensor/dist/installer/targets/toml.js.map +0 -1
  354. package/sensor/dist/installer/targets/types.d.ts +0 -108
  355. package/sensor/dist/installer/targets/types.d.ts.map +0 -1
  356. package/sensor/dist/installer/targets/types.js +0 -16
  357. package/sensor/dist/installer/targets/types.js.map +0 -1
  358. package/sensor/dist/upgrade/index.d.ts +0 -171
  359. package/sensor/dist/upgrade/index.d.ts.map +0 -1
  360. package/sensor/dist/upgrade/index.js +0 -659
  361. package/sensor/dist/upgrade/index.js.map +0 -1
  362. package/sensor/dist/upgrade/remove-binary.d.ts +0 -87
  363. package/sensor/dist/upgrade/remove-binary.d.ts.map +0 -1
  364. package/sensor/dist/upgrade/remove-binary.js +0 -289
  365. package/sensor/dist/upgrade/remove-binary.js.map +0 -1
  366. package/sensor/dist/upgrade/update-check.d.ts +0 -92
  367. package/sensor/dist/upgrade/update-check.d.ts.map +0 -1
  368. package/sensor/dist/upgrade/update-check.js +0 -258
  369. package/sensor/dist/upgrade/update-check.js.map +0 -1
  370. package/src/bootstrap.mjs +0 -88
@@ -0,0 +1,346 @@
1
+ import { createServer } from 'node:http';
2
+ import { randomBytes, randomInt } from 'node:crypto';
3
+ import { homedir } from 'node:os';
4
+ import { isIP } from 'node:net';
5
+ import {
6
+ chmod, lstat, mkdir, open, readFile, rename, rm, writeFile,
7
+ } from 'node:fs/promises';
8
+ import path from 'node:path';
9
+ import { domainToASCII } from 'node:url';
10
+ import { parseTree } from 'jsonc-parser';
11
+
12
+ export const BRIDGE_CONFIG_SCHEMA = 'lattice.bridge_config.v1';
13
+ export const BRIDGE_PORT_MIN = 49_152;
14
+ export const BRIDGE_PORT_MAX = 65_535;
15
+ const CONFIG_NAME = 'bridge.json';
16
+ const LOCK_NAME = 'bridge.lock';
17
+ const OPERATION_LOCK_NAME = 'bridge-operation.lock';
18
+ const LOCK_ATTEMPTS = 240;
19
+ const LOCK_WAIT_MS = 25;
20
+ const LOCK_STALE_MS = 30_000;
21
+ const AUTO_PORT_ATTEMPTS = 256;
22
+
23
+ export class BridgeConfigError extends Error {
24
+ constructor(code, message, detail = undefined, cause = undefined) {
25
+ super(message, { cause });
26
+ this.name = 'BridgeConfigError';
27
+ this.code = code;
28
+ if (detail !== undefined) this.detail = detail;
29
+ }
30
+ }
31
+
32
+ function exact(value, keys) {
33
+ return value !== null && typeof value === 'object' && !Array.isArray(value)
34
+ && Object.keys(value).sort().join(',') === [...keys].sort().join(',');
35
+ }
36
+
37
+ function configRoot(env) {
38
+ const configured = env.LATTICE_CONFIG_DIR;
39
+ if (configured !== undefined) {
40
+ if (typeof configured !== 'string' || !path.isAbsolute(configured)) {
41
+ throw new BridgeConfigError('BRIDGE_CONFIG_DIR_INVALID', 'LATTICE_CONFIG_DIR must be absolute');
42
+ }
43
+ return configured;
44
+ }
45
+ return path.join(homedir(), '.lattice');
46
+ }
47
+
48
+ export function bridgeConfigPaths(env = process.env) {
49
+ const root = configRoot(env);
50
+ return Object.freeze({ root, config: path.join(root, CONFIG_NAME), lock: path.join(root, LOCK_NAME),
51
+ operationLock: path.join(root, OPERATION_LOCK_NAME) });
52
+ }
53
+
54
+ function validTimestamp(value) {
55
+ return typeof value === 'string' && Number.isFinite(Date.parse(value))
56
+ && new Date(value).toISOString() === value;
57
+ }
58
+
59
+ function validateListen(listen) {
60
+ if (!exact(listen, ['address', 'port']) || isIP(listen.address) === 0
61
+ || !Number.isSafeInteger(listen.port) || listen.port < BRIDGE_PORT_MIN
62
+ || listen.port > BRIDGE_PORT_MAX) {
63
+ throw new BridgeConfigError('BRIDGE_CONFIG_INVALID', 'bridge listen configuration is invalid');
64
+ }
65
+ }
66
+
67
+ export function normalizeBridgeAllowedHost(value) {
68
+ if (typeof value !== 'string' || value.length === 0 || value !== value.trim()) {
69
+ throw new BridgeConfigError('BRIDGE_ALLOWED_HOST_INVALID', 'allowed host is invalid');
70
+ }
71
+ if (isIP(value) !== 0) return value.toLowerCase();
72
+ const withoutDot = value.endsWith('.') ? value.slice(0, -1) : value;
73
+ const ascii = domainToASCII(withoutDot).toLowerCase();
74
+ if (ascii.length === 0 || ascii.length > 253
75
+ || !/^[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?$/u.test(ascii)
76
+ || ascii.split('.').some((label) => label.length === 0 || label.length > 63
77
+ || label.startsWith('-') || label.endsWith('-'))) {
78
+ throw new BridgeConfigError('BRIDGE_ALLOWED_HOST_INVALID', 'allowed host is invalid');
79
+ }
80
+ return ascii;
81
+ }
82
+
83
+ function normalizeAllowedHosts(address, values) {
84
+ if (!Array.isArray(values) || values.length > 32) {
85
+ throw new BridgeConfigError('BRIDGE_CONFIG_INVALID', 'allowed hosts must be an array');
86
+ }
87
+ const hosts = new Set([normalizeBridgeAllowedHost(address)]);
88
+ if (address === '0.0.0.0') hosts.add('127.0.0.1');
89
+ if (address === '::') hosts.add('::1');
90
+ for (const value of values) hosts.add(normalizeBridgeAllowedHost(value));
91
+ if (hosts.size > 32) throw new BridgeConfigError('BRIDGE_CONFIG_INVALID', 'too many allowed hosts');
92
+ return Object.freeze([...hosts].sort((left, right) => left.localeCompare(right, 'en')));
93
+ }
94
+
95
+ export function normalizeBridgeUpstream(upstream) {
96
+ if (upstream?.mode === 'dashboard_descriptor' && exact(upstream, ['mode'])) {
97
+ return Object.freeze({ mode: 'dashboard_descriptor' });
98
+ }
99
+ if (upstream?.mode !== 'url' || !exact(upstream, ['mode', 'url']) || typeof upstream.url !== 'string') {
100
+ throw new BridgeConfigError('BRIDGE_UPSTREAM_INVALID', 'bridge upstream configuration is invalid');
101
+ }
102
+ let parsed;
103
+ try { parsed = new URL(upstream.url); } catch {
104
+ throw new BridgeConfigError('BRIDGE_UPSTREAM_INVALID', 'bridge upstream URL is invalid');
105
+ }
106
+ if (!['http:', 'https:'].includes(parsed.protocol) || parsed.username !== '' || parsed.password !== ''
107
+ || parsed.hash !== '' || parsed.search !== '') {
108
+ throw new BridgeConfigError('BRIDGE_UPSTREAM_INVALID', 'bridge upstream URL must be an HTTP origin or base path');
109
+ }
110
+ parsed.pathname = parsed.pathname.endsWith('/') ? parsed.pathname : `${parsed.pathname}/`;
111
+ return Object.freeze({ mode: 'url', url: parsed.href });
112
+ }
113
+
114
+ export function validateBridgeConfig(value) {
115
+ if (!exact(value, ['schema', 'enabled', 'listen', 'allowed_hosts', 'upstream', 'updated_at'])
116
+ || value.schema !== BRIDGE_CONFIG_SCHEMA || typeof value.enabled !== 'boolean'
117
+ || !validTimestamp(value.updated_at)) {
118
+ throw new BridgeConfigError('BRIDGE_CONFIG_INVALID', 'bridge configuration schema is invalid');
119
+ }
120
+ validateListen(value.listen);
121
+ const allowedHosts = normalizeAllowedHosts(value.listen.address, value.allowed_hosts);
122
+ if (allowedHosts.length !== value.allowed_hosts.length
123
+ || allowedHosts.some((host, index) => host !== value.allowed_hosts[index])) {
124
+ throw new BridgeConfigError('BRIDGE_CONFIG_INVALID', 'allowed hosts are not canonical');
125
+ }
126
+ const upstream = normalizeBridgeUpstream(value.upstream);
127
+ return Object.freeze({
128
+ schema: BRIDGE_CONFIG_SCHEMA,
129
+ enabled: value.enabled,
130
+ listen: Object.freeze({ address: value.listen.address, port: value.listen.port }),
131
+ allowed_hosts: allowedHosts,
132
+ upstream,
133
+ updated_at: value.updated_at,
134
+ });
135
+ }
136
+
137
+ async function prepareRoot(root) {
138
+ await mkdir(root, { recursive: true, mode: 0o700 });
139
+ const stats = await lstat(root);
140
+ if (!stats.isDirectory() || stats.isSymbolicLink()) {
141
+ throw new BridgeConfigError('BRIDGE_CONFIG_DIR_INVALID', 'bridge config directory is unsafe');
142
+ }
143
+ if ((stats.mode & 0o077) !== 0) await chmod(root, 0o700);
144
+ }
145
+
146
+ async function readDocument(ref) {
147
+ let stats;
148
+ try { stats = await lstat(ref); } catch (error) {
149
+ if (error?.code === 'ENOENT') return null;
150
+ throw new BridgeConfigError('BRIDGE_CONFIG_UNREADABLE', 'bridge config cannot be read', undefined, error);
151
+ }
152
+ if (!stats.isFile() || stats.isSymbolicLink() || (stats.mode & 0o777) !== 0o600) {
153
+ throw new BridgeConfigError('BRIDGE_CONFIG_MODE_INVALID', 'bridge config must be a regular 0600 file');
154
+ }
155
+ let value;
156
+ try {
157
+ const text = await readFile(ref, 'utf8');
158
+ const errors = [];
159
+ const tree = parseTree(text, errors, { allowTrailingComma: false, disallowComments: true });
160
+ const duplicate = (node) => {
161
+ if (node?.type === 'object') {
162
+ const keys = new Set();
163
+ for (const property of node.children ?? []) {
164
+ const [key, child] = property.children ?? [];
165
+ if (keys.has(key?.value) || duplicate(child)) return true;
166
+ keys.add(key?.value);
167
+ }
168
+ } else if (node?.type === 'array') return (node.children ?? []).some(duplicate);
169
+ return false;
170
+ };
171
+ if (errors.length > 0 || tree === undefined || duplicate(tree)) {
172
+ throw new Error('duplicate or invalid JSON');
173
+ }
174
+ value = JSON.parse(text);
175
+ } catch (error) {
176
+ throw new BridgeConfigError('BRIDGE_CONFIG_INVALID', 'bridge config JSON is invalid', undefined, error);
177
+ }
178
+ return validateBridgeConfig(value);
179
+ }
180
+
181
+ export async function readBridgeConfig({ env = process.env } = {}) {
182
+ return readDocument(bridgeConfigPaths(env).config);
183
+ }
184
+
185
+ async function atomicWrite(ref, value) {
186
+ const temporary = `${ref}.${process.pid}.${randomBytes(8).toString('hex')}.tmp`;
187
+ try {
188
+ await writeFile(temporary, `${JSON.stringify(value)}\n`, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
189
+ await rename(temporary, ref);
190
+ await chmod(ref, 0o600);
191
+ } finally {
192
+ await rm(temporary, { force: true });
193
+ }
194
+ }
195
+
196
+ async function withLock(ref, action) {
197
+ for (let attempt = 0; attempt < LOCK_ATTEMPTS; attempt += 1) {
198
+ let handle;
199
+ try {
200
+ handle = await open(ref, 'wx', 0o600);
201
+ await handle.writeFile(`${JSON.stringify({ pid: process.pid, created_at: new Date().toISOString() })}\n`);
202
+ try { return await action(); } finally {
203
+ await handle.close();
204
+ await rm(ref, { force: true });
205
+ }
206
+ } catch (error) {
207
+ if (error?.code !== 'EEXIST') throw error;
208
+ try {
209
+ const lock = JSON.parse(await readFile(ref, 'utf8'));
210
+ let alive = Number.isSafeInteger(lock.pid) && lock.pid > 0;
211
+ if (alive) try { process.kill(lock.pid, 0); } catch { alive = false; }
212
+ if (!alive || Date.now() - Date.parse(lock.created_at) > LOCK_STALE_MS) {
213
+ await rm(ref, { force: true });
214
+ continue;
215
+ }
216
+ } catch (lockError) {
217
+ if (lockError?.code === 'ENOENT') continue;
218
+ }
219
+ await new Promise((resolve) => setTimeout(resolve, LOCK_WAIT_MS));
220
+ }
221
+ }
222
+ throw new BridgeConfigError('BRIDGE_CONFIG_BUSY', 'bridge config lock timed out');
223
+ }
224
+
225
+ export async function withBridgeOperationLock({ env = process.env } = {}, action) {
226
+ if (typeof action !== 'function') throw new TypeError('bridge operation action required');
227
+ const refs = bridgeConfigPaths(env);
228
+ await prepareRoot(refs.root);
229
+ return withLock(refs.operationLock, action);
230
+ }
231
+
232
+ function closeServer(server) {
233
+ return new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
234
+ }
235
+
236
+ async function bindCandidate(address, port, createCandidateServer) {
237
+ const server = createCandidateServer();
238
+ try {
239
+ await new Promise((resolve, reject) => {
240
+ server.once('error', reject);
241
+ server.listen({ host: address, port, exclusive: true }, resolve);
242
+ });
243
+ return server;
244
+ } catch (error) {
245
+ try { server.close(); } catch {}
246
+ if (['EADDRINUSE', 'EACCES', 'EADDRNOTAVAIL'].includes(error?.code)) return null;
247
+ throw new BridgeConfigError('BRIDGE_BIND_FAILED', 'bridge candidate bind failed', { address, port }, error);
248
+ }
249
+ }
250
+
251
+ async function reservePort({ address, requestedPort, createCandidateServer, choosePort }) {
252
+ if (requestedPort !== null) {
253
+ const server = await bindCandidate(address, requestedPort, createCandidateServer);
254
+ if (server === null) {
255
+ throw new BridgeConfigError('BRIDGE_PORT_UNAVAILABLE', 'requested bridge port is unavailable',
256
+ { address, port: requestedPort });
257
+ }
258
+ return { port: requestedPort, server };
259
+ }
260
+ const attempted = new Set();
261
+ while (attempted.size < AUTO_PORT_ATTEMPTS) {
262
+ let port = choosePort();
263
+ if (!Number.isSafeInteger(port) || port < BRIDGE_PORT_MIN || port > BRIDGE_PORT_MAX) {
264
+ port = randomInt(BRIDGE_PORT_MIN, BRIDGE_PORT_MAX + 1);
265
+ }
266
+ while (attempted.has(port)) {
267
+ port = port === BRIDGE_PORT_MAX ? BRIDGE_PORT_MIN : port + 1;
268
+ }
269
+ attempted.add(port);
270
+ const server = await bindCandidate(address, port, createCandidateServer);
271
+ if (server !== null) return { port, server };
272
+ }
273
+ throw new BridgeConfigError('BRIDGE_PORT_EXHAUSTED', 'no exclusive high bridge port could be bound',
274
+ { address, attempts: attempted.size });
275
+ }
276
+
277
+ function validateMutation({ address, port, upstream, allowedHosts }) {
278
+ if (typeof address !== 'string' || isIP(address) === 0) {
279
+ throw new BridgeConfigError('BRIDGE_LISTEN_INVALID', 'bridge listen address must be an IP literal');
280
+ }
281
+ if (port !== null && (!Number.isSafeInteger(port) || port < BRIDGE_PORT_MIN || port > BRIDGE_PORT_MAX)) {
282
+ throw new BridgeConfigError('BRIDGE_PORT_INVALID', `bridge port must be ${BRIDGE_PORT_MIN}..${BRIDGE_PORT_MAX}`);
283
+ }
284
+ return { upstream: normalizeBridgeUpstream(upstream), allowedHosts: normalizeAllowedHosts(address, allowedHosts) };
285
+ }
286
+
287
+ export async function configureBridge({
288
+ address, port = null, upstream = { mode: 'dashboard_descriptor' }, allowedHosts = [],
289
+ env = process.env, now = () => new Date(),
290
+ createCandidateServer = () => createServer(),
291
+ choosePort = () => randomInt(BRIDGE_PORT_MIN, BRIDGE_PORT_MAX + 1),
292
+ reuseCurrentPort = true,
293
+ } = {}) {
294
+ const normalized = validateMutation({ address, port, upstream, allowedHosts });
295
+ const refs = bridgeConfigPaths(env);
296
+ await prepareRoot(refs.root);
297
+ return withLock(refs.lock, async () => {
298
+ const current = await readDocument(refs.config);
299
+ const selectedPort = port ?? (reuseCurrentPort && current?.enabled && current.listen.address === address
300
+ ? current.listen.port : null);
301
+ const bindingUnchanged = current?.enabled === true && current.listen.address === address
302
+ && current.listen.port === selectedPort;
303
+ let reservation = null;
304
+ try {
305
+ if (!bindingUnchanged) {
306
+ reservation = await reservePort({ address, requestedPort: selectedPort,
307
+ createCandidateServer, choosePort });
308
+ }
309
+ const config = validateBridgeConfig({
310
+ schema: BRIDGE_CONFIG_SCHEMA,
311
+ enabled: true,
312
+ listen: { address, port: bindingUnchanged ? selectedPort : reservation.port },
313
+ allowed_hosts: normalized.allowedHosts,
314
+ upstream: normalized.upstream,
315
+ updated_at: now().toISOString(),
316
+ });
317
+ await atomicWrite(refs.config, config);
318
+ return config;
319
+ } finally {
320
+ if (reservation !== null) await closeServer(reservation.server);
321
+ }
322
+ });
323
+ }
324
+
325
+ export async function disableBridge({ env = process.env, now = () => new Date() } = {}) {
326
+ const refs = bridgeConfigPaths(env);
327
+ await prepareRoot(refs.root);
328
+ return withLock(refs.lock, async () => {
329
+ const current = await readDocument(refs.config);
330
+ if (current === null) return null;
331
+ const config = validateBridgeConfig({ ...current, enabled: false, updated_at: now().toISOString() });
332
+ await atomicWrite(refs.config, config);
333
+ return config;
334
+ });
335
+ }
336
+
337
+ export async function restoreBridgeConfig(config, { env = process.env } = {}) {
338
+ if (config !== null) validateBridgeConfig(config);
339
+ const refs = bridgeConfigPaths(env);
340
+ await prepareRoot(refs.root);
341
+ return withLock(refs.lock, async () => {
342
+ if (config === null) await rm(refs.config, { force: true });
343
+ else await atomicWrite(refs.config, config);
344
+ return config;
345
+ });
346
+ }
@@ -0,0 +1,328 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { randomBytes } from 'node:crypto';
3
+ import { constants as fsConstants } from 'node:fs';
4
+ import { chmod, lstat, open, rename, rm, writeFile } from 'node:fs/promises';
5
+ import { isIP } from 'node:net';
6
+ import path from 'node:path';
7
+ import { parseTree } from 'jsonc-parser';
8
+
9
+ import {
10
+ BRIDGE_PORT_MAX, BRIDGE_PORT_MIN, BridgeConfigError, bridgeConfigPaths, readBridgeConfig,
11
+ } from './bridge-config.mjs';
12
+
13
+ const DESCRIPTOR_SCHEMA = 'lattice.bridge_daemon.v1';
14
+ const START_TIMEOUT_MS = 5_000;
15
+ const STOP_TIMEOUT_MS = 3_000;
16
+ const STOP_REQUEST_SCHEMA = 'lattice.bridge_stop_request.v1';
17
+ const STOP_RECEIPT_SCHEMA = 'lattice.bridge_stop_receipt.v1';
18
+ const ACTIVE_MARKER_SCHEMA = 'lattice.bridge_daemon_active.v1';
19
+ const CONTROL_MAX_BYTES = 65_536;
20
+
21
+ export function bridgeDaemonDescriptorPath(env = process.env) {
22
+ return path.join(bridgeConfigPaths(env).root, 'bridge-daemon.json');
23
+ }
24
+
25
+ export function bridgeDaemonActiveMarkerPath(env = process.env) {
26
+ return path.join(bridgeConfigPaths(env).root, 'bridge-daemon-active.json');
27
+ }
28
+
29
+ function bridgeStopPaths(env) {
30
+ const root = bridgeConfigPaths(env).root;
31
+ return { request: path.join(root, 'bridge-stop-request.json'),
32
+ receipt: path.join(root, 'bridge-stop-receipt.json') };
33
+ }
34
+
35
+ export async function removeBridgeDaemonDescriptor({ env = process.env } = {}) {
36
+ await rm(bridgeDaemonDescriptorPath(env), { force: true });
37
+ }
38
+
39
+ export async function removeBridgeDaemonActiveMarker({ env = process.env } = {}) {
40
+ await rm(bridgeDaemonActiveMarkerPath(env), { force: true });
41
+ }
42
+
43
+ async function atomicDescriptor(ref, value) {
44
+ const temporary = `${ref}.${process.pid}.${randomBytes(8).toString('hex')}.tmp`;
45
+ try {
46
+ await writeFile(temporary, `${JSON.stringify(value)}\n`, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
47
+ await rename(temporary, ref);
48
+ await chmod(ref, 0o600);
49
+ } finally { await rm(temporary, { force: true }); }
50
+ }
51
+
52
+ function duplicateJsonKey(node) {
53
+ if (node?.type === 'object') {
54
+ const keys = new Set();
55
+ for (const property of node.children ?? []) {
56
+ const [key, child] = property.children ?? [];
57
+ if (keys.has(key?.value) || duplicateJsonKey(child)) return true;
58
+ keys.add(key?.value);
59
+ }
60
+ } else if (node?.type === 'array') return (node.children ?? []).some(duplicateJsonKey);
61
+ return false;
62
+ }
63
+
64
+ function isIsoTimestamp(value) {
65
+ if (typeof value !== 'string') return false;
66
+ try { return new Date(value).toISOString() === value; } catch { return false; }
67
+ }
68
+
69
+ async function readStrictJson(ref, code, label) {
70
+ let before;
71
+ let handle;
72
+ try {
73
+ before = await lstat(ref);
74
+ if (!before.isFile() || before.isSymbolicLink() || (before.mode & 0o777) !== 0o600
75
+ || before.size > CONTROL_MAX_BYTES) throw new Error(`${label} unsafe`);
76
+ handle = await open(ref, fsConstants.O_RDONLY | (fsConstants.O_NOFOLLOW ?? 0));
77
+ const opened = await handle.stat();
78
+ if (!opened.isFile() || opened.dev !== before.dev || opened.ino !== before.ino
79
+ || opened.size !== before.size || opened.size > CONTROL_MAX_BYTES) {
80
+ throw new Error(`${label} changed during validation`);
81
+ }
82
+ const text = await handle.readFile('utf8');
83
+ const after = await lstat(ref);
84
+ if (after.dev !== opened.dev || after.ino !== opened.ino || after.size !== opened.size) {
85
+ throw new Error(`${label} changed during read`);
86
+ }
87
+ const errors = [];
88
+ const tree = parseTree(text, errors, { allowTrailingComma: false, disallowComments: true });
89
+ if (errors.length > 0 || tree === undefined || duplicateJsonKey(tree)) {
90
+ throw new Error(`${label} JSON invalid`);
91
+ }
92
+ return JSON.parse(text);
93
+ } catch (error) {
94
+ if (error?.code === 'ENOENT' && before === undefined) return null;
95
+ throw new BridgeConfigError(code, `${label} invalid`, undefined, error);
96
+ } finally { await handle?.close(); }
97
+ }
98
+
99
+ async function readStrictControl(ref, schema, keys) {
100
+ const value = await readStrictJson(ref, 'BRIDGE_STOP_CONTROL_INVALID', 'bridge stop control');
101
+ if (value === null) return null;
102
+ const timestampKey = keys.find((key) => key.endsWith('_at'));
103
+ if (value?.schema !== schema || Object.keys(value).sort().join(',') !== [...keys].sort().join(',')
104
+ || keys.includes('nonce')
105
+ && (typeof value.nonce !== 'string' || !/^[0-9a-f]{64}$/u.test(value.nonce))
106
+ || keys.includes('address') && isIP(value.address) === 0
107
+ || keys.includes('port')
108
+ && (!Number.isSafeInteger(value.port) || value.port < BRIDGE_PORT_MIN || value.port > BRIDGE_PORT_MAX)
109
+ || timestampKey === undefined || !isIsoTimestamp(value[timestampKey])) {
110
+ throw new BridgeConfigError('BRIDGE_STOP_CONTROL_INVALID', 'bridge stop control schema invalid');
111
+ }
112
+ return value;
113
+ }
114
+
115
+ async function readBridgeDaemonActiveMarker(env) {
116
+ return (await readStrictControl(bridgeDaemonActiveMarkerPath(env), ACTIVE_MARKER_SCHEMA,
117
+ ['schema', 'address', 'port', 'started_at']));
118
+ }
119
+
120
+ export async function readBridgeStopRequest({ env = process.env } = {}) {
121
+ return readStrictControl(bridgeStopPaths(env).request, STOP_REQUEST_SCHEMA,
122
+ ['schema', 'nonce', 'requested_at']);
123
+ }
124
+
125
+ export async function writeBridgeStopReceipt({ nonce, env = process.env }) {
126
+ if (typeof nonce !== 'string' || !/^[0-9a-f]{64}$/u.test(nonce)) {
127
+ throw new BridgeConfigError('BRIDGE_STOP_CONTROL_INVALID', 'bridge stop nonce invalid');
128
+ }
129
+ await atomicDescriptor(bridgeStopPaths(env).receipt, {
130
+ schema: STOP_RECEIPT_SCHEMA, nonce, stopped_at: new Date().toISOString(),
131
+ });
132
+ }
133
+
134
+ export async function clearBridgeStopControl({ env = process.env } = {}) {
135
+ const refs = bridgeStopPaths(env);
136
+ await rm(refs.request, { force: true });
137
+ await rm(refs.receipt, { force: true });
138
+ }
139
+
140
+ export async function requestBridgeDaemonStop({ env = process.env, listen = null } = {}) {
141
+ let descriptor = null;
142
+ try { descriptor = await readBridgeDaemonDescriptor({ env }); } catch {}
143
+ if (descriptor !== null && await attest(descriptor) === null
144
+ && !await bridgeEndpointAvailable({ address: descriptor.address, port: descriptor.port })) {
145
+ return { state: 'not_running', nonce: null };
146
+ }
147
+ if (descriptor === null) {
148
+ const refs = bridgeStopPaths(env);
149
+ let descriptorExists = true;
150
+ try { await lstat(bridgeDaemonDescriptorPath(env)); } catch (error) {
151
+ if (error?.code === 'ENOENT') descriptorExists = false;
152
+ }
153
+ let activeMarker;
154
+ let activeMarkerInvalid = false;
155
+ try { activeMarker = await readBridgeDaemonActiveMarker(env); } catch (error) {
156
+ if (error?.code !== 'BRIDGE_STOP_CONTROL_INVALID') throw error;
157
+ activeMarkerInvalid = true;
158
+ activeMarker = null;
159
+ }
160
+ if (!descriptorExists) {
161
+ const witnessedListen = activeMarker === null ? listen : activeMarker;
162
+ if (!activeMarkerInvalid
163
+ && (witnessedListen === null || !await bridgeEndpointAvailable(witnessedListen))) {
164
+ return { state: 'not_running', nonce: null };
165
+ }
166
+ }
167
+ }
168
+ const refs = bridgeStopPaths(env);
169
+ const nonce = randomBytes(32).toString('hex');
170
+ await rm(refs.receipt, { force: true });
171
+ await atomicDescriptor(refs.request, {
172
+ schema: STOP_REQUEST_SCHEMA, nonce, requested_at: new Date().toISOString(),
173
+ });
174
+ const deadline = Date.now() + STOP_TIMEOUT_MS;
175
+ while (Date.now() < deadline) {
176
+ await new Promise((resolve) => setTimeout(resolve, 50));
177
+ const receipt = await readStrictControl(refs.receipt, STOP_RECEIPT_SCHEMA,
178
+ ['schema', 'nonce', 'stopped_at']);
179
+ if (receipt?.nonce === nonce) return { state: 'stopped', nonce };
180
+ }
181
+ throw new BridgeConfigError('BRIDGE_DAEMON_STOP_FAILED', 'bridge daemon stop receipt timed out');
182
+ }
183
+
184
+ export async function writeBridgeDaemonDescriptor({ config, env = process.env }) {
185
+ const instanceToken = env.LATTICE_BRIDGE_INSTANCE_TOKEN;
186
+ if (typeof instanceToken !== 'string' || !/^[0-9a-f]{64}$/u.test(instanceToken)) {
187
+ throw new BridgeConfigError('BRIDGE_INSTANCE_TOKEN_INVALID', 'bridge instance token is invalid');
188
+ }
189
+ const descriptor = { schema: DESCRIPTOR_SCHEMA, pid: process.pid,
190
+ address: config.listen.address, port: config.listen.port,
191
+ config_updated_at: config.updated_at, instance_token: instanceToken,
192
+ started_at: new Date().toISOString() };
193
+ await atomicDescriptor(bridgeDaemonDescriptorPath(env), descriptor);
194
+ await atomicDescriptor(bridgeDaemonActiveMarkerPath(env), {
195
+ schema: ACTIVE_MARKER_SCHEMA, address: descriptor.address, port: descriptor.port,
196
+ started_at: descriptor.started_at,
197
+ });
198
+ return descriptor;
199
+ }
200
+
201
+ export async function readBridgeDaemonDescriptor({ env = process.env } = {}) {
202
+ const value = await readStrictJson(bridgeDaemonDescriptorPath(env),
203
+ 'BRIDGE_DAEMON_DESCRIPTOR_INVALID', 'bridge daemon descriptor');
204
+ if (value === null) return null;
205
+ if (value?.schema !== DESCRIPTOR_SCHEMA || !Number.isSafeInteger(value.pid) || value.pid <= 0
206
+ || typeof value.address !== 'string' || isIP(value.address) === 0
207
+ || !Number.isSafeInteger(value.port) || value.port < BRIDGE_PORT_MIN || value.port > BRIDGE_PORT_MAX
208
+ || !isIsoTimestamp(value.config_updated_at) || !isIsoTimestamp(value.started_at)
209
+ || typeof value.instance_token !== 'string' || !/^[0-9a-f]{64}$/u.test(value.instance_token)
210
+ || Object.keys(value).sort().join(',')
211
+ !== 'address,config_updated_at,instance_token,pid,port,schema,started_at') {
212
+ throw new BridgeConfigError('BRIDGE_DAEMON_DESCRIPTOR_INVALID', 'bridge daemon descriptor schema invalid');
213
+ }
214
+ return value;
215
+ }
216
+
217
+ function healthHost(address) {
218
+ if (address === '0.0.0.0') return '127.0.0.1';
219
+ if (address === '::') return '[::1]';
220
+ return address.includes(':') ? `[${address}]` : address;
221
+ }
222
+
223
+ async function bridgeEndpointAvailable(listen) {
224
+ try {
225
+ const response = await fetch(`http://${healthHost(listen.address)}:${listen.port}/__lattice/bridge-health`,
226
+ { signal: AbortSignal.timeout(300) });
227
+ const body = response.status === 200 ? await response.json() : null;
228
+ return body?.schema === 'lattice.bridge_health.v1';
229
+ } catch { return false; }
230
+ }
231
+
232
+ export async function waitForBridgeSocketClose({ listen, timeoutMs = STOP_TIMEOUT_MS } = {}) {
233
+ if (listen === null || typeof listen !== 'object') throw new TypeError('bridge listen required');
234
+ const deadline = Date.now() + timeoutMs;
235
+ while (Date.now() < deadline) {
236
+ if (!await bridgeEndpointAvailable(listen)) return true;
237
+ await new Promise((resolve) => setTimeout(resolve, 50));
238
+ }
239
+ return !await bridgeEndpointAvailable(listen);
240
+ }
241
+
242
+ async function attest(descriptor) {
243
+ if (descriptor === null) return null;
244
+ try {
245
+ const response = await fetch(`http://${healthHost(descriptor.address)}:${descriptor.port}/__lattice/bridge-health`, {
246
+ headers: { 'x-lattice-bridge-instance-token': descriptor.instance_token },
247
+ signal: AbortSignal.timeout(400),
248
+ });
249
+ if (response.status !== 200) return null;
250
+ const body = await response.json();
251
+ return body?.schema === 'lattice.bridge_health.v1' && body.pid === descriptor.pid ? body : null;
252
+ } catch { return null; }
253
+ }
254
+
255
+ async function healthy(descriptor, config) {
256
+ if (descriptor === null || descriptor.address !== config.listen.address
257
+ || descriptor.port !== config.listen.port) return false;
258
+ const body = await attest(descriptor);
259
+ return body !== null && body.updated_at === config.updated_at;
260
+ }
261
+
262
+ export async function ensureBridgeDaemon({ env = process.env } = {}) {
263
+ const config = await readBridgeConfig({ env });
264
+ if (config === null || !config.enabled) throw new BridgeConfigError('BRIDGE_DISABLED', 'bridge is disabled');
265
+ const previous = await readBridgeDaemonDescriptor({ env });
266
+ if (await healthy(previous, config)) return previous;
267
+ if (previous !== null && previous.address === config.listen.address
268
+ && previous.port === config.listen.port && await attest(previous) !== null) {
269
+ const deadline = Date.now() + START_TIMEOUT_MS;
270
+ while (Date.now() < deadline) {
271
+ await new Promise((resolve) => setTimeout(resolve, 50));
272
+ const reconciled = await readBridgeDaemonDescriptor({ env });
273
+ if (reconciled?.pid === previous.pid && await healthy(reconciled, config)) return reconciled;
274
+ }
275
+ throw new BridgeConfigError('BRIDGE_DAEMON_UNAVAILABLE',
276
+ 'existing bridge daemon did not reconcile the updated config');
277
+ }
278
+ const instanceToken = randomBytes(32).toString('hex');
279
+ const child = spawn(process.execPath, [path.resolve(import.meta.dirname, '../bin/lattice-bridge.mjs')], {
280
+ detached: true, stdio: 'ignore', env: { ...env, LATTICE_BRIDGE_INSTANCE_TOKEN: instanceToken },
281
+ });
282
+ child.unref();
283
+ const deadline = Date.now() + START_TIMEOUT_MS;
284
+ while (Date.now() < deadline) {
285
+ await new Promise((resolve) => setTimeout(resolve, 50));
286
+ const descriptor = await readBridgeDaemonDescriptor({ env });
287
+ if (await healthy(descriptor, config)) {
288
+ if (previous !== null && previous.pid !== descriptor.pid) {
289
+ if (await attest(previous) !== null) {
290
+ try { process.kill(previous.pid, 'SIGTERM'); } catch {}
291
+ }
292
+ }
293
+ return descriptor;
294
+ }
295
+ }
296
+ throw new BridgeConfigError('BRIDGE_DAEMON_UNAVAILABLE', 'bridge daemon did not bind and become healthy');
297
+ }
298
+
299
+ export async function stopBridgeDaemon({ env = process.env } = {}) {
300
+ const descriptor = await readBridgeDaemonDescriptor({ env });
301
+ if (descriptor === null) return null;
302
+ if (await attest(descriptor) === null) {
303
+ let alive = true;
304
+ try { process.kill(descriptor.pid, 0); } catch { alive = false; }
305
+ if (!alive) {
306
+ await rm(bridgeDaemonDescriptorPath(env), { force: true });
307
+ await removeBridgeDaemonActiveMarker({ env });
308
+ return descriptor;
309
+ }
310
+ throw new BridgeConfigError('BRIDGE_DAEMON_ATTESTATION_FAILED',
311
+ 'bridge daemon identity could not be attested; refusing to signal descriptor PID');
312
+ }
313
+ try { process.kill(descriptor.pid, 'SIGTERM'); } catch (error) {
314
+ if (error?.code !== 'ESRCH') throw new BridgeConfigError('BRIDGE_DAEMON_STOP_FAILED', 'bridge daemon signal failed');
315
+ }
316
+ const deadline = Date.now() + STOP_TIMEOUT_MS;
317
+ while (Date.now() < deadline) {
318
+ let alive = true;
319
+ try { process.kill(descriptor.pid, 0); } catch { alive = false; }
320
+ if (!alive) {
321
+ await rm(bridgeDaemonDescriptorPath(env), { force: true });
322
+ await removeBridgeDaemonActiveMarker({ env });
323
+ return descriptor;
324
+ }
325
+ await new Promise((resolve) => setTimeout(resolve, 50));
326
+ }
327
+ throw new BridgeConfigError('BRIDGE_DAEMON_STOP_FAILED', 'bridge daemon did not stop');
328
+ }