borgmcp 1.1.15 → 2.0.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 (466) hide show
  1. package/CONTRIBUTING.md +15 -0
  2. package/LICENSE +201 -33
  3. package/NOTICE +2 -0
  4. package/README.md +85 -36
  5. package/SECURITY.md +7 -0
  6. package/dist/agent-runtime.d.ts.map +1 -0
  7. package/dist/agent-runtime.js +63 -1
  8. package/dist/agent-runtime.js.map +1 -0
  9. package/dist/assimilate-cmd.d.ts +96 -17
  10. package/dist/assimilate-cmd.d.ts.map +1 -0
  11. package/dist/assimilate-cmd.js +1396 -48
  12. package/dist/assimilate-cmd.js.map +1 -0
  13. package/dist/assimilate-deps.d.ts.map +1 -0
  14. package/dist/assimilate-deps.js +384 -3
  15. package/dist/assimilate-deps.js.map +1 -0
  16. package/dist/assimilate-guard.d.ts +5 -7
  17. package/dist/assimilate-guard.d.ts.map +1 -0
  18. package/dist/assimilate-guard.js +60 -2
  19. package/dist/assimilate-guard.js.map +1 -0
  20. package/dist/assimilate-welcome.d.ts +1 -1
  21. package/dist/assimilate-welcome.d.ts.map +1 -0
  22. package/dist/assimilate-welcome.js +32 -2
  23. package/dist/assimilate-welcome.js.map +1 -0
  24. package/dist/auth-env.d.ts +7 -50
  25. package/dist/auth-env.d.ts.map +1 -0
  26. package/dist/auth-env.js +22 -1
  27. package/dist/auth-env.js.map +1 -0
  28. package/dist/backends/launch-all-pastelist.d.ts.map +1 -0
  29. package/dist/backends/launch-all-pastelist.js +11 -4
  30. package/dist/backends/launch-all-pastelist.js.map +1 -0
  31. package/dist/backends/launch-all-tmux.d.ts.map +1 -0
  32. package/dist/backends/launch-all-tmux.js +49 -1
  33. package/dist/backends/launch-all-tmux.js.map +1 -0
  34. package/dist/backends/launch-all-windows.d.ts.map +1 -0
  35. package/dist/backends/launch-all-windows.js +86 -12
  36. package/dist/backends/launch-all-windows.js.map +1 -0
  37. package/dist/bare-launch-menu.d.ts.map +1 -0
  38. package/dist/bare-launch-menu.js +83 -6
  39. package/dist/bare-launch-menu.js.map +1 -0
  40. package/dist/claude-launch-args.d.ts.map +1 -0
  41. package/dist/claude-launch-args.js +34 -1
  42. package/dist/claude-launch-args.js.map +1 -0
  43. package/dist/claude.d.ts.map +1 -0
  44. package/dist/claude.js +486 -14
  45. package/dist/claude.js.map +1 -0
  46. package/dist/cleanup-cmd.d.ts +5 -5
  47. package/dist/cleanup-cmd.d.ts.map +1 -0
  48. package/dist/cleanup-cmd.js +453 -14
  49. package/dist/cleanup-cmd.js.map +1 -0
  50. package/dist/clear-rewake-core.d.ts.map +1 -0
  51. package/dist/clear-rewake-core.js +23 -2
  52. package/dist/clear-rewake-core.js.map +1 -0
  53. package/dist/clear-rewake.d.ts.map +1 -0
  54. package/dist/clear-rewake.js +19 -1
  55. package/dist/clear-rewake.js.map +1 -0
  56. package/dist/cli-help.d.ts +5 -0
  57. package/dist/cli-help.d.ts.map +1 -0
  58. package/dist/cli-help.js +104 -56
  59. package/dist/cli-help.js.map +1 -0
  60. package/dist/cli-platform.d.ts.map +1 -0
  61. package/dist/cli-platform.js +106 -4
  62. package/dist/cli-platform.js.map +1 -0
  63. package/dist/cli-tool-approval.d.ts +84 -0
  64. package/dist/cli-tool-approval.d.ts.map +1 -0
  65. package/dist/cli-tool-approval.js +583 -0
  66. package/dist/cli-tool-approval.js.map +1 -0
  67. package/dist/codex-app-server.d.ts.map +1 -0
  68. package/dist/codex-app-server.js +228 -4
  69. package/dist/codex-app-server.js.map +1 -0
  70. package/dist/codex-app-wake.d.ts +9 -11
  71. package/dist/codex-app-wake.d.ts.map +1 -0
  72. package/dist/codex-app-wake.js +436 -2
  73. package/dist/codex-app-wake.js.map +1 -0
  74. package/dist/codex-launch.d.ts.map +1 -0
  75. package/dist/codex-launch.js +102 -2
  76. package/dist/codex-launch.js.map +1 -0
  77. package/dist/codex-remote.d.ts +6 -1
  78. package/dist/codex-remote.d.ts.map +1 -0
  79. package/dist/codex-remote.js +394 -1
  80. package/dist/codex-remote.js.map +1 -0
  81. package/dist/codex-wake-resolve.d.ts +4 -4
  82. package/dist/codex-wake-resolve.d.ts.map +1 -0
  83. package/dist/codex-wake-resolve.js +127 -1
  84. package/dist/codex-wake-resolve.js.map +1 -0
  85. package/dist/config-utils.d.ts +2 -2
  86. package/dist/config-utils.d.ts.map +1 -0
  87. package/dist/config-utils.js +766 -3
  88. package/dist/config-utils.js.map +1 -0
  89. package/dist/config.d.ts +98 -61
  90. package/dist/config.d.ts.map +1 -0
  91. package/dist/config.js +426 -1
  92. package/dist/config.js.map +1 -0
  93. package/dist/console-prefix.d.ts.map +1 -0
  94. package/dist/console-prefix.js +96 -1
  95. package/dist/console-prefix.js.map +1 -0
  96. package/dist/cube-name.d.ts.map +1 -0
  97. package/dist/cube-name.js +67 -1
  98. package/dist/cube-name.js.map +1 -0
  99. package/dist/cubes.d.ts +126 -18
  100. package/dist/cubes.d.ts.map +1 -0
  101. package/dist/cubes.js +412 -4
  102. package/dist/cubes.js.map +1 -0
  103. package/dist/debug.d.ts.map +1 -0
  104. package/dist/debug.js +71 -1
  105. package/dist/debug.js.map +1 -0
  106. package/dist/direct-log.d.ts.map +1 -0
  107. package/dist/direct-log.js +11 -1
  108. package/dist/direct-log.js.map +1 -0
  109. package/dist/docs-sections.d.ts +11 -10
  110. package/dist/docs-sections.d.ts.map +1 -0
  111. package/dist/docs-sections.js +113 -3
  112. package/dist/docs-sections.js.map +1 -0
  113. package/dist/drone-lifecycle.d.ts +5 -21
  114. package/dist/drone-lifecycle.d.ts.map +1 -0
  115. package/dist/drone-lifecycle.js +57 -8
  116. package/dist/drone-lifecycle.js.map +1 -0
  117. package/dist/drone-management.d.ts +29 -0
  118. package/dist/drone-management.d.ts.map +1 -0
  119. package/dist/drone-management.js +109 -0
  120. package/dist/drone-management.js.map +1 -0
  121. package/dist/ensure-mcp-config.d.ts.map +1 -0
  122. package/dist/ensure-mcp-config.js +36 -1
  123. package/dist/ensure-mcp-config.js.map +1 -0
  124. package/dist/evict-drone.d.ts +10 -2
  125. package/dist/evict-drone.d.ts.map +1 -0
  126. package/dist/evict-drone.js +60 -1
  127. package/dist/evict-drone.js.map +1 -0
  128. package/dist/gc-orphan-inboxes.d.ts.map +1 -0
  129. package/dist/gc-orphan-inboxes.js +177 -1
  130. package/dist/gc-orphan-inboxes.js.map +1 -0
  131. package/dist/inbox-monitor.d.ts +1 -1
  132. package/dist/inbox-monitor.d.ts.map +1 -0
  133. package/dist/inbox-monitor.js +856 -3
  134. package/dist/inbox-monitor.js.map +1 -0
  135. package/dist/index.d.ts +6 -4
  136. package/dist/index.d.ts.map +1 -0
  137. package/dist/index.js +1034 -37
  138. package/dist/index.js.map +1 -0
  139. package/dist/launch-all-cmd.d.ts.map +1 -0
  140. package/dist/launch-all-cmd.js +377 -37
  141. package/dist/launch-all-cmd.js.map +1 -0
  142. package/dist/launch-all-command.d.ts.map +1 -0
  143. package/dist/launch-all-command.js +24 -1
  144. package/dist/launch-all-command.js.map +1 -0
  145. package/dist/launch-all-deps.d.ts +3 -8
  146. package/dist/launch-all-deps.d.ts.map +1 -0
  147. package/dist/launch-all-deps.js +99 -1
  148. package/dist/launch-all-deps.js.map +1 -0
  149. package/dist/launch-all-discovery.d.ts +1 -0
  150. package/dist/launch-all-discovery.d.ts.map +1 -0
  151. package/dist/launch-all-discovery.js +90 -4
  152. package/dist/launch-all-discovery.js.map +1 -0
  153. package/dist/launch-all-locks.d.ts.map +1 -0
  154. package/dist/launch-all-locks.js +54 -1
  155. package/dist/launch-all-locks.js.map +1 -0
  156. package/dist/launch-gate.d.ts +2 -2
  157. package/dist/launch-gate.d.ts.map +1 -0
  158. package/dist/launch-gate.js +73 -1
  159. package/dist/launch-gate.js.map +1 -0
  160. package/dist/lifecycle-log-guard.d.ts.map +1 -0
  161. package/dist/lifecycle-log-guard.js +93 -2
  162. package/dist/lifecycle-log-guard.js.map +1 -0
  163. package/dist/list-roles-render.d.ts.map +1 -0
  164. package/dist/list-roles-render.js +40 -6
  165. package/dist/list-roles-render.js.map +1 -0
  166. package/dist/local-log-routing.d.ts +19 -0
  167. package/dist/local-log-routing.d.ts.map +1 -0
  168. package/dist/local-log-routing.js +67 -0
  169. package/dist/local-log-routing.js.map +1 -0
  170. package/dist/local-manage-tool-result.d.ts +8 -0
  171. package/dist/local-manage-tool-result.d.ts.map +1 -0
  172. package/dist/local-manage-tool-result.js +10 -0
  173. package/dist/local-manage-tool-result.js.map +1 -0
  174. package/dist/local-server-cursor.d.ts +34 -0
  175. package/dist/local-server-cursor.d.ts.map +1 -0
  176. package/dist/local-server-cursor.js +151 -0
  177. package/dist/local-server-cursor.js.map +1 -0
  178. package/dist/log-audit.d.ts.map +1 -0
  179. package/dist/log-audit.js +194 -3
  180. package/dist/log-audit.js.map +1 -0
  181. package/dist/log-stream.d.ts +35 -18
  182. package/dist/log-stream.d.ts.map +1 -0
  183. package/dist/log-stream.js +1270 -12
  184. package/dist/log-stream.js.map +1 -0
  185. package/dist/model-presets.d.ts.map +1 -0
  186. package/dist/model-presets.js +19 -1
  187. package/dist/model-presets.js.map +1 -0
  188. package/dist/name-validator.d.ts.map +1 -0
  189. package/dist/name-validator.js +22 -1
  190. package/dist/name-validator.js.map +1 -0
  191. package/dist/opencode-drone.d.ts.map +1 -0
  192. package/dist/opencode-drone.js +438 -4
  193. package/dist/opencode-drone.js.map +1 -0
  194. package/dist/opencode-plugin.d.ts +1 -1
  195. package/dist/opencode-plugin.d.ts.map +1 -0
  196. package/dist/opencode-plugin.js +29 -4
  197. package/dist/opencode-plugin.js.map +1 -0
  198. package/dist/parse-assimilate-args.d.ts +1 -1
  199. package/dist/parse-assimilate-args.d.ts.map +1 -0
  200. package/dist/parse-assimilate-args.js +125 -1
  201. package/dist/parse-assimilate-args.js.map +1 -0
  202. package/dist/parse-launch-all-args.d.ts.map +1 -0
  203. package/dist/parse-launch-all-args.js +70 -1
  204. package/dist/parse-launch-all-args.js.map +1 -0
  205. package/dist/postinstall-banner.d.ts.map +1 -0
  206. package/dist/postinstall-banner.js +27 -2
  207. package/dist/postinstall-banner.js.map +1 -0
  208. package/dist/postinstall.d.ts.map +1 -0
  209. package/dist/postinstall.js +32 -5
  210. package/dist/postinstall.js.map +1 -0
  211. package/dist/readiness-probe.d.ts.map +1 -0
  212. package/dist/readiness-probe.js +9 -1
  213. package/dist/readiness-probe.js.map +1 -0
  214. package/dist/regen-format.d.ts.map +1 -0
  215. package/dist/regen-format.js +545 -13
  216. package/dist/regen-format.js.map +1 -0
  217. package/dist/regen.d.ts.map +1 -0
  218. package/dist/regen.js +141 -5
  219. package/dist/regen.js.map +1 -0
  220. package/dist/remote-client.d.ts +88 -173
  221. package/dist/remote-client.d.ts.map +1 -0
  222. package/dist/remote-client.js +997 -1
  223. package/dist/remote-client.js.map +1 -0
  224. package/dist/reset-local-seat-cmd.d.ts +50 -0
  225. package/dist/reset-local-seat-cmd.d.ts.map +1 -0
  226. package/dist/reset-local-seat-cmd.js +169 -0
  227. package/dist/reset-local-seat-cmd.js.map +1 -0
  228. package/dist/role-match.d.ts.map +1 -0
  229. package/dist/role-match.js +23 -1
  230. package/dist/role-match.js.map +1 -0
  231. package/dist/role-resolver.d.ts.map +1 -0
  232. package/dist/role-resolver.js +60 -1
  233. package/dist/role-resolver.js.map +1 -0
  234. package/dist/roster-render.d.ts.map +1 -0
  235. package/dist/roster-render.js +121 -3
  236. package/dist/roster-render.js.map +1 -0
  237. package/dist/seat-probe.d.ts +32 -13
  238. package/dist/seat-probe.d.ts.map +1 -0
  239. package/dist/seat-probe.js +78 -1
  240. package/dist/seat-probe.js.map +1 -0
  241. package/dist/seat-store.d.ts +76 -0
  242. package/dist/seat-store.d.ts.map +1 -0
  243. package/dist/seat-store.js +309 -0
  244. package/dist/seat-store.js.map +1 -0
  245. package/dist/seats.d.ts +375 -0
  246. package/dist/seats.d.ts.map +1 -0
  247. package/dist/seats.js +670 -0
  248. package/dist/seats.js.map +1 -0
  249. package/dist/self-path.d.ts +27 -0
  250. package/dist/self-path.d.ts.map +1 -0
  251. package/dist/self-path.js +43 -0
  252. package/dist/self-path.js.map +1 -0
  253. package/dist/server-endpoint.d.ts +9 -0
  254. package/dist/server-endpoint.d.ts.map +1 -0
  255. package/dist/server-endpoint.js +38 -0
  256. package/dist/server-endpoint.js.map +1 -0
  257. package/dist/server-errors.d.ts +50 -0
  258. package/dist/server-errors.d.ts.map +1 -0
  259. package/dist/server-errors.js +79 -0
  260. package/dist/server-errors.js.map +1 -0
  261. package/dist/server-handshake.d.ts +197 -0
  262. package/dist/server-handshake.d.ts.map +1 -0
  263. package/dist/server-handshake.js +533 -0
  264. package/dist/server-handshake.js.map +1 -0
  265. package/dist/server-response.d.ts +3 -0
  266. package/dist/server-response.d.ts.map +1 -0
  267. package/dist/server-response.js +67 -0
  268. package/dist/server-response.js.map +1 -0
  269. package/dist/server-trust.d.ts +14 -0
  270. package/dist/server-trust.d.ts.map +1 -0
  271. package/dist/server-trust.js +225 -0
  272. package/dist/server-trust.js.map +1 -0
  273. package/dist/session-continuity.d.ts +33 -0
  274. package/dist/session-continuity.d.ts.map +1 -0
  275. package/dist/session-continuity.js +220 -0
  276. package/dist/session-continuity.js.map +1 -0
  277. package/dist/setup-confirm.d.ts +3 -3
  278. package/dist/setup-confirm.d.ts.map +1 -0
  279. package/dist/setup-confirm.js +126 -2
  280. package/dist/setup-confirm.js.map +1 -0
  281. package/dist/setup.d.ts +5 -3
  282. package/dist/setup.d.ts.map +1 -0
  283. package/dist/setup.js +169 -46
  284. package/dist/setup.js.map +1 -0
  285. package/dist/shell-escape.d.ts.map +1 -0
  286. package/dist/shell-escape.js +22 -1
  287. package/dist/shell-escape.js.map +1 -0
  288. package/dist/spawn.d.ts +2 -2
  289. package/dist/spawn.d.ts.map +1 -0
  290. package/dist/spawn.js +29 -10
  291. package/dist/spawn.js.map +1 -0
  292. package/dist/startup-services.d.ts +0 -1
  293. package/dist/startup-services.d.ts.map +1 -0
  294. package/dist/startup-services.js +26 -1
  295. package/dist/startup-services.js.map +1 -0
  296. package/dist/stream-owner.d.ts +6 -2
  297. package/dist/stream-owner.d.ts.map +1 -0
  298. package/dist/stream-owner.js +547 -2
  299. package/dist/stream-owner.js.map +1 -0
  300. package/dist/stream-status.d.ts +1 -1
  301. package/dist/stream-status.d.ts.map +1 -0
  302. package/dist/stream-status.js +268 -3
  303. package/dist/stream-status.js.map +1 -0
  304. package/dist/sync-roles-render.d.ts.map +1 -0
  305. package/dist/sync-roles-render.js +118 -3
  306. package/dist/sync-roles-render.js.map +1 -0
  307. package/dist/sync.d.ts +1 -1
  308. package/dist/sync.d.ts.map +1 -0
  309. package/dist/sync.js +286 -22
  310. package/dist/sync.js.map +1 -0
  311. package/dist/terminal-title.d.ts.map +1 -0
  312. package/dist/terminal-title.js +68 -1
  313. package/dist/terminal-title.js.map +1 -0
  314. package/dist/token-store.d.ts +17 -111
  315. package/dist/token-store.d.ts.map +1 -0
  316. package/dist/token-store.js +81 -1
  317. package/dist/token-store.js.map +1 -0
  318. package/dist/tool-manifest.d.ts +4 -8
  319. package/dist/tool-manifest.d.ts.map +1 -0
  320. package/dist/tool-manifest.js +554 -1
  321. package/dist/tool-manifest.js.map +1 -0
  322. package/dist/tool-scope.d.ts +8 -12
  323. package/dist/tool-scope.d.ts.map +1 -0
  324. package/dist/tool-scope.js +97 -1
  325. package/dist/tool-scope.js.map +1 -0
  326. package/dist/types.d.ts +1 -5
  327. package/dist/types.d.ts.map +1 -0
  328. package/dist/types.js +5 -0
  329. package/dist/types.js.map +1 -0
  330. package/dist/unknown-subcommand.d.ts +1 -1
  331. package/dist/unknown-subcommand.d.ts.map +1 -0
  332. package/dist/unknown-subcommand.js +37 -1
  333. package/dist/unknown-subcommand.js.map +1 -0
  334. package/dist/version.d.ts +1 -1
  335. package/dist/version.d.ts.map +1 -0
  336. package/dist/version.js +78 -2
  337. package/dist/version.js.map +1 -0
  338. package/dist/working-repo.d.ts.map +1 -0
  339. package/dist/working-repo.js +81 -1
  340. package/dist/working-repo.js.map +1 -0
  341. package/dist/worktree-lifecycle.d.ts.map +1 -0
  342. package/dist/worktree-lifecycle.js +205 -2
  343. package/dist/worktree-lifecycle.js.map +1 -0
  344. package/docs/EXTRACTION_PROVENANCE.md +42 -0
  345. package/docs/LOCAL_SERVER.md +103 -0
  346. package/docs/RELEASING.md +129 -0
  347. package/package.json +52 -34
  348. package/src/agent-runtime.ts +69 -0
  349. package/src/assimilate-cmd.ts +1864 -0
  350. package/src/assimilate-deps.ts +444 -0
  351. package/src/assimilate-guard.ts +84 -0
  352. package/src/assimilate-welcome.ts +39 -0
  353. package/src/auth-env.ts +21 -0
  354. package/src/backends/launch-all-pastelist.ts +18 -0
  355. package/src/backends/launch-all-tmux.ts +75 -0
  356. package/src/backends/launch-all-windows.ts +110 -0
  357. package/src/bare-launch-menu.ts +148 -0
  358. package/src/claude-launch-args.ts +35 -0
  359. package/src/claude.ts +561 -0
  360. package/src/cleanup-cmd.ts +581 -0
  361. package/src/clear-rewake-core.ts +34 -0
  362. package/src/clear-rewake.ts +18 -0
  363. package/src/cli-help.ts +116 -0
  364. package/src/cli-platform.ts +127 -0
  365. package/src/cli-tool-approval.ts +716 -0
  366. package/src/codex-app-server.ts +238 -0
  367. package/src/codex-app-wake.ts +524 -0
  368. package/src/codex-launch.ts +140 -0
  369. package/src/codex-remote.ts +500 -0
  370. package/src/codex-wake-resolve.ts +155 -0
  371. package/src/config-utils.ts +811 -0
  372. package/src/config.ts +595 -0
  373. package/src/console-prefix.ts +99 -0
  374. package/src/cube-name.ts +69 -0
  375. package/src/cubes.ts +583 -0
  376. package/src/debug.ts +73 -0
  377. package/src/direct-log.ts +9 -0
  378. package/src/docs-sections.ts +129 -0
  379. package/src/drone-lifecycle.ts +62 -0
  380. package/src/drone-management.ts +162 -0
  381. package/src/ensure-mcp-config.ts +54 -0
  382. package/src/evict-drone.ts +82 -0
  383. package/src/gc-orphan-inboxes.ts +229 -0
  384. package/src/inbox-monitor.ts +1005 -0
  385. package/src/index.ts +1201 -0
  386. package/src/launch-all-cmd.ts +450 -0
  387. package/src/launch-all-command.ts +30 -0
  388. package/src/launch-all-deps.ts +188 -0
  389. package/src/launch-all-discovery.ts +118 -0
  390. package/src/launch-all-locks.ts +74 -0
  391. package/src/launch-gate.ts +80 -0
  392. package/src/lifecycle-log-guard.ts +144 -0
  393. package/src/list-roles-render.ts +52 -0
  394. package/src/local-log-routing.ts +106 -0
  395. package/src/local-manage-tool-result.ts +10 -0
  396. package/src/local-server-cursor.ts +188 -0
  397. package/src/log-audit.ts +207 -0
  398. package/src/log-stream.ts +1567 -0
  399. package/src/model-presets.ts +22 -0
  400. package/src/name-validator.ts +23 -0
  401. package/src/opencode-drone.ts +528 -0
  402. package/src/opencode-plugin.ts +38 -0
  403. package/src/parse-assimilate-args.ts +125 -0
  404. package/src/parse-launch-all-args.ts +100 -0
  405. package/src/postinstall-banner.ts +37 -0
  406. package/src/postinstall.ts +36 -0
  407. package/src/readiness-probe.ts +12 -0
  408. package/src/regen-format.ts +651 -0
  409. package/src/regen.ts +150 -0
  410. package/src/remote-client.ts +1457 -0
  411. package/src/reset-local-seat-cmd.ts +227 -0
  412. package/src/role-match.ts +29 -0
  413. package/src/role-resolver.ts +93 -0
  414. package/src/roster-render.ts +196 -0
  415. package/src/seat-probe.ts +115 -0
  416. package/src/seat-store.ts +333 -0
  417. package/src/seats.ts +911 -0
  418. package/src/self-path.ts +51 -0
  419. package/src/server-endpoint.ts +38 -0
  420. package/src/server-errors.ts +95 -0
  421. package/src/server-handshake.ts +816 -0
  422. package/src/server-response.ts +76 -0
  423. package/src/server-trust.ts +245 -0
  424. package/src/session-continuity.ts +280 -0
  425. package/src/setup-confirm.ts +171 -0
  426. package/src/setup.ts +186 -0
  427. package/src/shell-escape.ts +22 -0
  428. package/src/spawn.ts +31 -0
  429. package/src/startup-services.ts +36 -0
  430. package/src/stream-owner.ts +686 -0
  431. package/src/stream-status.ts +337 -0
  432. package/src/sync-roles-render.ts +163 -0
  433. package/src/sync.ts +363 -0
  434. package/src/terminal-title.ts +76 -0
  435. package/src/token-store.ts +102 -0
  436. package/src/tool-manifest.ts +610 -0
  437. package/src/tool-scope.ts +110 -0
  438. package/src/types.ts +5 -0
  439. package/src/unknown-subcommand.ts +35 -0
  440. package/src/version.ts +82 -0
  441. package/src/working-repo.ts +99 -0
  442. package/src/worktree-lifecycle.ts +254 -0
  443. package/dist/auth-recovery.d.ts +0 -33
  444. package/dist/auth-recovery.js +0 -1
  445. package/dist/auth.d.ts +0 -103
  446. package/dist/auth.js +0 -39
  447. package/dist/device-auth.d.ts +0 -75
  448. package/dist/device-auth.js +0 -1
  449. package/dist/drone-address.d.ts +0 -26
  450. package/dist/drone-address.js +0 -1
  451. package/dist/get-started.d.ts +0 -20
  452. package/dist/get-started.js +0 -2
  453. package/dist/health-beat.d.ts +0 -134
  454. package/dist/health-beat.js +0 -1
  455. package/dist/role-section.d.ts +0 -110
  456. package/dist/role-section.js +0 -8
  457. package/dist/setup-action.d.ts +0 -20
  458. package/dist/setup-action.js +0 -1
  459. package/dist/stale-version-check.d.ts +0 -40
  460. package/dist/stale-version-check.js +0 -1
  461. package/dist/subscription-retry.d.ts +0 -40
  462. package/dist/subscription-retry.js +0 -1
  463. package/dist/templates.d.ts +0 -107
  464. package/dist/templates.js +0 -359
  465. package/dist/token-crypto.d.ts +0 -50
  466. package/dist/token-crypto.js +0 -1
@@ -0,0 +1,76 @@
1
+ function abortError(signal: AbortSignal): Error {
2
+ return signal.reason instanceof Error
3
+ ? signal.reason
4
+ : new Error('Borg server request was aborted');
5
+ }
6
+
7
+ type ByteReadResult = Awaited<
8
+ ReturnType<ReadableStreamDefaultReader<Uint8Array>['read']>
9
+ >;
10
+
11
+ async function readWithAbort(
12
+ reader: ReadableStreamDefaultReader<Uint8Array>,
13
+ signal?: AbortSignal,
14
+ ): Promise<ByteReadResult> {
15
+ if (!signal) return reader.read();
16
+ if (signal.aborted) throw abortError(signal);
17
+
18
+ return new Promise<ByteReadResult>((resolve, reject) => {
19
+ const onAbort = () => reject(abortError(signal));
20
+ signal.addEventListener('abort', onAbort, { once: true });
21
+ reader.read().then(resolve, reject).finally(() => {
22
+ signal.removeEventListener('abort', onAbort);
23
+ });
24
+ });
25
+ }
26
+
27
+ /** Read one server response without trusting Content-Length or stream EOF. */
28
+ export async function readBoundedResponseBody(
29
+ response: Response,
30
+ maxBytes: number,
31
+ limitMessage: string,
32
+ signal?: AbortSignal,
33
+ ): Promise<string> {
34
+ const declaredLength = response.headers.get('content-length');
35
+ if (declaredLength !== null) {
36
+ const parsed = Number(declaredLength);
37
+ if (!Number.isSafeInteger(parsed) || parsed < 0 || parsed > maxBytes) {
38
+ await response.body?.cancel().catch(() => {});
39
+ throw new Error(limitMessage);
40
+ }
41
+ }
42
+ if (!response.body) return '';
43
+
44
+ const reader = response.body.getReader();
45
+ const chunks: Uint8Array[] = [];
46
+ let total = 0;
47
+ try {
48
+ while (true) {
49
+ const { done, value } = await readWithAbort(reader, signal);
50
+ if (done) break;
51
+ total += value.byteLength;
52
+ if (total > maxBytes) {
53
+ await reader.cancel().catch(() => {});
54
+ throw new Error(limitMessage);
55
+ }
56
+ chunks.push(value);
57
+ }
58
+ } catch (error) {
59
+ await reader.cancel(error).catch(() => {});
60
+ throw error;
61
+ } finally {
62
+ try {
63
+ reader.releaseLock();
64
+ } catch {
65
+ // The stream may already be canceled.
66
+ }
67
+ }
68
+
69
+ const body = new Uint8Array(total);
70
+ let offset = 0;
71
+ for (const chunk of chunks) {
72
+ body.set(chunk, offset);
73
+ offset += chunk.byteLength;
74
+ }
75
+ return new TextDecoder('utf-8', { fatal: true }).decode(body);
76
+ }
@@ -0,0 +1,245 @@
1
+ import { createHash, timingSafeEqual, X509Certificate } from 'node:crypto';
2
+ import { constants } from 'node:fs';
3
+ import { open } from 'node:fs/promises';
4
+ import { request as httpsRequest } from 'node:https';
5
+ import { homedir } from 'node:os';
6
+ import { join, resolve } from 'node:path';
7
+ import { Readable } from 'node:stream';
8
+ import { BorgServerTrustError } from './server-errors.js';
9
+
10
+ // CR5 TLS LATTICE: OpenSSL/Node TLS certificate-verification error codes. A raw
11
+ // CA / cert-chain / SAN failure from the pinned transport is a potential MITM and
12
+ // MUST be a TERMINAL trust-mismatch verdict — never a transient 'restart' blip.
13
+ // Connection refusal / reset / timeout are NOT in here: those stay raw transport
14
+ // errors so the seat probe classifies them as `unreachable` (genuinely transient).
15
+ const TLS_TRUST_ERROR_CODES = new Set([
16
+ 'DEPTH_ZERO_SELF_SIGNED_CERT',
17
+ 'SELF_SIGNED_CERT_IN_CHAIN',
18
+ 'UNABLE_TO_GET_ISSUER_CERT',
19
+ 'UNABLE_TO_GET_ISSUER_CERT_LOCALLY',
20
+ 'UNABLE_TO_VERIFY_LEAF_SIGNATURE',
21
+ 'CERT_UNTRUSTED',
22
+ 'CERT_CHAIN_TOO_LONG',
23
+ 'HOSTNAME_MISMATCH',
24
+ 'ERR_TLS_CERT_ALTNAME_INVALID',
25
+ ]);
26
+
27
+ /**
28
+ * True iff `code` is a PINNED-TRANSPORT certificate-verification failure (bad CA,
29
+ * unverifiable chain, self-signed leaf, expired/not-yet-valid cert, or a SAN /
30
+ * hostname mismatch). These are terminal trust-mismatch — a restart never fixes a
31
+ * wrong cert. `CERT_*` covers the OpenSSL verify family (CERT_HAS_EXPIRED,
32
+ * CERT_NOT_YET_VALID, CERT_REVOKED, …); `ERR_TLS_CERT*` covers Node's SAN check.
33
+ */
34
+ function isPinnedTransportTrustFailure(code: string | undefined): boolean {
35
+ if (!code) return false;
36
+ return (
37
+ code.startsWith('CERT_') ||
38
+ code.startsWith('ERR_TLS_CERT') ||
39
+ TLS_TRUST_ERROR_CODES.has(code)
40
+ );
41
+ }
42
+
43
+ export type ServerFetch = typeof fetch;
44
+
45
+ export interface BorgServerTrust {
46
+ identity: string;
47
+ fetchImpl: ServerFetch;
48
+ }
49
+
50
+ interface ServerTrustConfig {
51
+ ca_spki_sha256: string;
52
+ }
53
+
54
+ const trustCache = new Map<string, Promise<BorgServerTrust>>();
55
+
56
+ function serverDataDirectory(): string {
57
+ return resolve(process.env.BORG_SERVER_DATA_DIR ?? join(homedir(), '.borg', 'server'));
58
+ }
59
+
60
+ async function readTrustFile(path: string): Promise<string> {
61
+ let handle;
62
+ try {
63
+ handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
64
+ } catch (error) {
65
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
66
+ throw new Error('Borg server trust files were not found');
67
+ }
68
+ throw new Error('Borg server trust files could not be opened safely');
69
+ }
70
+ try {
71
+ const metadata = await handle.stat();
72
+ if (!metadata.isFile() || (metadata.mode & 0o077) !== 0) {
73
+ throw new Error('Borg server trust files must be private regular files');
74
+ }
75
+ if (typeof process.getuid === 'function' && metadata.uid !== process.getuid()) {
76
+ throw new Error('Borg server trust files must be owned by the current user');
77
+ }
78
+ return await handle.readFile('utf8');
79
+ } finally {
80
+ await handle.close();
81
+ }
82
+ }
83
+
84
+ function decodeTrustConfig(value: string): ServerTrustConfig {
85
+ let parsed: unknown;
86
+ try {
87
+ parsed = JSON.parse(value);
88
+ } catch {
89
+ throw new Error('Borg server trust metadata is invalid');
90
+ }
91
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
92
+ throw new Error('Borg server trust metadata is invalid');
93
+ }
94
+ const fingerprint = (parsed as Record<string, unknown>).ca_spki_sha256;
95
+ if (typeof fingerprint !== 'string' || !/^[a-f0-9]{64}$/i.test(fingerprint)) {
96
+ throw new Error('Borg server trust metadata is missing a valid CA identity');
97
+ }
98
+ return { ca_spki_sha256: fingerprint.toLowerCase() };
99
+ }
100
+
101
+ function verifyCaIdentity(certificate: string, expected: string): string {
102
+ let parsed: X509Certificate;
103
+ try {
104
+ parsed = new X509Certificate(certificate);
105
+ } catch {
106
+ throw new Error('Borg server CA certificate is invalid');
107
+ }
108
+ if (!parsed.ca) throw new Error('Borg server trust anchor is not a CA certificate');
109
+ const actual = createHash('sha256')
110
+ .update(parsed.publicKey.export({ type: 'spki', format: 'der' }))
111
+ .digest();
112
+ const expectedBytes = Buffer.from(expected, 'hex');
113
+ if (expectedBytes.length !== actual.length || !timingSafeEqual(actual, expectedBytes)) {
114
+ throw new Error('Borg server CA certificate does not match its pinned identity');
115
+ }
116
+ return `spki-sha256:${actual.toString('hex')}`;
117
+ }
118
+
119
+ function responseHeaders(rawHeaders: string[]): Headers {
120
+ const headers = new Headers();
121
+ for (let index = 0; index < rawHeaders.length; index += 2) {
122
+ const name = rawHeaders[index];
123
+ const value = rawHeaders[index + 1];
124
+ if (name !== undefined && value !== undefined) headers.append(name, value);
125
+ }
126
+ return headers;
127
+ }
128
+
129
+ function requestBody(value: unknown): string | Uint8Array | undefined {
130
+ if (value === null || value === undefined) return undefined;
131
+ if (typeof value === 'string') return value;
132
+ if (value instanceof Uint8Array) return value;
133
+ if (value instanceof ArrayBuffer) return new Uint8Array(value);
134
+ throw new Error('Borg server transport received an unsupported request body');
135
+ }
136
+
137
+ /**
138
+ * Minimal fetch-compatible HTTPS transport bound to one origin and one
139
+ * explicit local CA. Node's global fetch cannot consume the server-owned CA,
140
+ * and disabling certificate validation would collapse the authority boundary.
141
+ */
142
+ export function createPinnedServerFetch(origin: string, caCertificate: string): ServerFetch {
143
+ const authority = new URL(origin);
144
+ if (authority.protocol !== 'https:' || authority.origin !== origin) {
145
+ throw new Error('Borg server trust requires a canonical HTTPS origin');
146
+ }
147
+
148
+ return (async (input: string | URL | Request, init: RequestInit = {}): Promise<Response> => {
149
+ const url = input instanceof Request ? new URL(input.url) : new URL(input.toString());
150
+ if (url.origin !== authority.origin || url.protocol !== 'https:') {
151
+ throw new Error('Borg server transport refused a cross-authority request');
152
+ }
153
+ if (input instanceof Request) {
154
+ throw new Error('Borg server transport requires an explicit URL and request options');
155
+ }
156
+ if (init.signal?.aborted) throw new DOMException('This operation was aborted', 'AbortError');
157
+
158
+ const body = requestBody(init.body);
159
+ const headers = new Headers(init.headers);
160
+ return await new Promise<Response>((resolvePromise, rejectPromise) => {
161
+ const request = httpsRequest({
162
+ protocol: 'https:',
163
+ hostname: url.hostname.replace(/^\[(.*)\]$/, '$1'),
164
+ port: url.port || 443,
165
+ path: `${url.pathname}${url.search}`,
166
+ method: init.method ?? 'GET',
167
+ headers: Object.fromEntries(headers.entries()),
168
+ ca: caCertificate,
169
+ rejectUnauthorized: true,
170
+ minVersion: 'TLSv1.3',
171
+ }, (incoming) => {
172
+ const status = incoming.statusCode ?? 500;
173
+ if (init.redirect === 'error' && status >= 300 && status < 400) {
174
+ incoming.resume();
175
+ rejectPromise(new Error('Borg server redirect refused'));
176
+ return;
177
+ }
178
+ const noBody = init.method === 'HEAD' || status === 204 || status === 304;
179
+ const stream = noBody
180
+ ? null
181
+ : Readable.toWeb(incoming) as ReadableStream<Uint8Array>;
182
+ if (noBody) incoming.resume();
183
+ resolvePromise(new Response(stream, {
184
+ status,
185
+ statusText: incoming.statusMessage,
186
+ headers: responseHeaders(incoming.rawHeaders),
187
+ }));
188
+ });
189
+
190
+ const abort = (): void => {
191
+ request.destroy(new DOMException('This operation was aborted', 'AbortError'));
192
+ };
193
+ init.signal?.addEventListener('abort', abort, { once: true });
194
+ request.once('close', () => init.signal?.removeEventListener('abort', abort));
195
+ request.once('error', (error) => {
196
+ // CR5: a pinned-transport CERT/CA/SAN verification failure is TERMINAL trust
197
+ // — type it as BorgServerTrustError so the seat probe returns `trust-mismatch`
198
+ // (never `indeterminate` → "restart"). Connection refusal/reset/timeout carry
199
+ // a transport errno (or an AbortError) and are rethrown RAW so the probe
200
+ // classifies them as `unreachable`.
201
+ const code = (error as NodeJS.ErrnoException).code;
202
+ if (isPinnedTransportTrustFailure(code)) {
203
+ rejectPromise(
204
+ new BorgServerTrustError(
205
+ `Borg server presented a certificate that failed pinned verification (${code})`,
206
+ ),
207
+ );
208
+ return;
209
+ }
210
+ rejectPromise(error);
211
+ });
212
+ if (body !== undefined) request.write(body);
213
+ request.end();
214
+ });
215
+ }) as ServerFetch;
216
+ }
217
+
218
+ export async function loadBorgServerTrust(
219
+ origin: string,
220
+ dataDirectory = serverDataDirectory(),
221
+ ): Promise<BorgServerTrust> {
222
+ const key = `${dataDirectory}\0${origin}`;
223
+ let pending = trustCache.get(key);
224
+ if (!pending) {
225
+ pending = (async () => {
226
+ const [certificate, configText] = await Promise.all([
227
+ readTrustFile(join(dataDirectory, 'ca.crt')),
228
+ readTrustFile(join(dataDirectory, 'server.json')),
229
+ ]);
230
+ const config = decodeTrustConfig(configText);
231
+ const identity = verifyCaIdentity(certificate, config.ca_spki_sha256);
232
+ return {
233
+ identity,
234
+ fetchImpl: createPinnedServerFetch(origin, certificate),
235
+ };
236
+ })();
237
+ trustCache.set(key, pending);
238
+ pending.catch(() => trustCache.delete(key));
239
+ }
240
+ return pending;
241
+ }
242
+
243
+ export function __clearServerTrustCacheForTest(): void {
244
+ trustCache.clear();
245
+ }
@@ -0,0 +1,280 @@
1
+ import { createHash, randomBytes } from 'node:crypto';
2
+ import { getServerCredential } from './config.js';
3
+ import { getActiveCube, type ActiveCube } from './cubes.js';
4
+ import { DroneEvictedError } from './drone-lifecycle.js';
5
+ import { sendBorgServerAttach } from './server-handshake.js';
6
+ import { BorgServerError, BorgServerTrustError } from './server-errors.js';
7
+ import { loadBorgServerTrust } from './server-trust.js';
8
+ import {
9
+ getActiveSeat,
10
+ prepareSeatReplacement,
11
+ promoteSeatReplacement,
12
+ refreshActiveSeatSession,
13
+ scrubSeatReplacement,
14
+ seatRef,
15
+ type SeatRecord,
16
+ } from './seats.js';
17
+
18
+ export const SESSION_RENEWAL_LEAD_MS = 5 * 60_000;
19
+
20
+ interface AttachOutcome {
21
+ result: 'created' | 'reused';
22
+ droneId: string;
23
+ sessionId: string;
24
+ expiresAt: string;
25
+ }
26
+
27
+ interface SessionContinuityDeps {
28
+ now(): number;
29
+ randomCredential(): string;
30
+ getSeat(ref: string, binding: { origin: string; trustIdentity: string; cubeId: string }): Promise<SeatRecord | null>;
31
+ getActive(): Promise<ActiveCube | null>;
32
+ getParentCredential(origin: string, trustIdentity: string): Promise<string | null>;
33
+ sendAttach(record: SeatRecord, bearer: string, parentCredential: string): Promise<AttachOutcome>;
34
+ prepareReplacement: typeof prepareSeatReplacement;
35
+ refreshSession: typeof refreshActiveSeatSession;
36
+ promoteReplacement: typeof promoteSeatReplacement;
37
+ scrubReplacement: typeof scrubSeatReplacement;
38
+ }
39
+
40
+ const defaultDeps: SessionContinuityDeps = {
41
+ now: Date.now,
42
+ randomCredential: () => randomBytes(32).toString('base64url'),
43
+ getSeat: getActiveSeat,
44
+ getActive: getActiveCube,
45
+ getParentCredential: getServerCredential,
46
+ async sendAttach(record, bearer, parentCredential) {
47
+ const trust = await loadBorgServerTrust(record.origin);
48
+ if (trust.identity !== record.trustIdentity) {
49
+ throw new BorgServerTrustError('Borg server trust identity changed; refusing session renewal');
50
+ }
51
+ const attached = await sendBorgServerAttach(
52
+ record.origin,
53
+ record.trustIdentity,
54
+ parentCredential,
55
+ {
56
+ cubeId: record.cubeId,
57
+ roleId: record.roleId,
58
+ operation: record.operation,
59
+ priorDroneId: record.droneId,
60
+ },
61
+ bearer,
62
+ { fetchImpl: trust.fetchImpl },
63
+ );
64
+ return {
65
+ result: attached.result,
66
+ droneId: attached.drone.id,
67
+ sessionId: attached.session.sessionId,
68
+ expiresAt: attached.session.expiresAt,
69
+ };
70
+ },
71
+ prepareReplacement: prepareSeatReplacement,
72
+ refreshSession: refreshActiveSeatSession,
73
+ promoteReplacement: promoteSeatReplacement,
74
+ scrubReplacement: scrubSeatReplacement,
75
+ };
76
+
77
+ const inflight = new Map<string, Promise<ActiveCube>>();
78
+ const digestOf = (value: string) => createHash('sha256').update(value).digest('hex');
79
+
80
+ function sameSeat(active: ActiveCube, candidate: ActiveCube | null): candidate is ActiveCube {
81
+ return candidate !== null &&
82
+ candidate.apiUrl === active.apiUrl &&
83
+ candidate.serverTrustIdentity === active.serverTrustIdentity &&
84
+ candidate.cubeId === active.cubeId &&
85
+ candidate.droneId === active.droneId &&
86
+ candidate.localSessionCredentialRef === active.localSessionCredentialRef;
87
+ }
88
+
89
+ function seatBinding(record: SeatRecord) {
90
+ return {
91
+ origin: record.origin,
92
+ trustIdentity: record.trustIdentity,
93
+ cubeId: record.cubeId,
94
+ };
95
+ }
96
+
97
+ function validActiveRecord(active: ActiveCube, record: SeatRecord | null): record is SeatRecord {
98
+ return record !== null &&
99
+ record.state === 'active' &&
100
+ record.origin === active.apiUrl &&
101
+ record.trustIdentity === active.serverTrustIdentity &&
102
+ record.cubeId === active.cubeId &&
103
+ record.droneId === active.droneId &&
104
+ seatRef(record) === active.localSessionCredentialRef;
105
+ }
106
+
107
+ function deterministicAttachFailure(error: unknown): boolean {
108
+ return error instanceof BorgServerError ||
109
+ error instanceof BorgServerTrustError ||
110
+ error instanceof DroneEvictedError;
111
+ }
112
+
113
+ async function reloadSameSeat(
114
+ active: ActiveCube,
115
+ deps: SessionContinuityDeps,
116
+ requireFreshCredential = false,
117
+ ): Promise<ActiveCube> {
118
+ const current = await deps.getActive();
119
+ if (!sameSeat(active, current) || (requireFreshCredential && current.sessionToken === active.sessionToken)) {
120
+ throw new Error('Session renewal no longer targets the same saved seat');
121
+ }
122
+ return current;
123
+ }
124
+
125
+ async function performRenewal(
126
+ active: ActiveCube,
127
+ mode: 'proactive' | 'expired',
128
+ deps: SessionContinuityDeps,
129
+ ): Promise<ActiveCube> {
130
+ if (!active.serverTrustIdentity || !active.localSessionCredentialRef) {
131
+ throw new Error('Selected Borg server authority state is missing or unreadable');
132
+ }
133
+ const binding = {
134
+ origin: active.apiUrl,
135
+ trustIdentity: active.serverTrustIdentity,
136
+ cubeId: active.cubeId,
137
+ };
138
+ const record = await deps.getSeat(active.localSessionCredentialRef, binding);
139
+ if (!validActiveRecord(active, record)) {
140
+ throw new Error('Session renewal no longer targets the same saved seat');
141
+ }
142
+ const activeDigest = digestOf(active.sessionToken);
143
+ if (digestOf(record.credential) !== activeDigest) {
144
+ return reloadSameSeat(active, deps, true);
145
+ }
146
+ const expiresIn = Date.parse(record.expiresAt!) - deps.now();
147
+ if (mode === 'proactive' && expiresIn > SESSION_RENEWAL_LEAD_MS && record.replacement === undefined) {
148
+ return active;
149
+ }
150
+ const parentCredential = await deps.getParentCredential(record.origin, record.trustIdentity);
151
+ if (!parentCredential) throw new Error('Selected Borg server parent credential is missing or unreadable');
152
+ const recordBinding = seatBinding(record);
153
+
154
+ let restoreRequired = mode === 'expired' || expiresIn <= 0 || record.replacement !== undefined;
155
+ if (!restoreRequired) {
156
+ let attached: AttachOutcome | null = null;
157
+ try {
158
+ attached = await deps.sendAttach(record, record.credential, parentCredential);
159
+ } catch (error) {
160
+ if (error instanceof BorgServerError && error.code === 'AUTH_EXPIRED') {
161
+ restoreRequired = true;
162
+ } else {
163
+ throw error;
164
+ }
165
+ }
166
+ if (!restoreRequired) {
167
+ if (!attached) throw new Error('Session renewal returned no result');
168
+ const renewedExpiry = Date.parse(attached.expiresAt);
169
+ if (
170
+ attached.result !== 'reused' ||
171
+ attached.droneId !== record.droneId ||
172
+ attached.sessionId !== record.sessionId ||
173
+ !Number.isFinite(renewedExpiry) ||
174
+ renewedExpiry <= Date.parse(record.expiresAt!)
175
+ ) {
176
+ throw new Error('Session renewal did not preserve the same saved seat');
177
+ }
178
+ const refreshed = await deps.refreshSession({
179
+ ref: active.localSessionCredentialRef,
180
+ binding: recordBinding,
181
+ expectedDroneId: record.droneId!,
182
+ expectedActiveDigest: activeDigest,
183
+ expectedSessionId: record.sessionId!,
184
+ expiresAt: attached.expiresAt,
185
+ });
186
+ if (!refreshed) return reloadSameSeat(active, deps);
187
+ return reloadSameSeat(active, deps);
188
+ }
189
+ }
190
+
191
+ const replacement = await deps.prepareReplacement({
192
+ ref: active.localSessionCredentialRef,
193
+ binding: recordBinding,
194
+ expectedDroneId: record.droneId!,
195
+ expectedActiveDigest: activeDigest,
196
+ replacementCredential: deps.randomCredential(),
197
+ });
198
+ if (!replacement.ok) return reloadSameSeat(active, deps, true);
199
+
200
+ let attached: AttachOutcome;
201
+ try {
202
+ attached = await deps.sendAttach(record, replacement.credential, parentCredential);
203
+ } catch (error) {
204
+ if (deterministicAttachFailure(error)) {
205
+ await deps.scrubReplacement({
206
+ ref: active.localSessionCredentialRef,
207
+ binding: recordBinding,
208
+ expectedActiveDigest: activeDigest,
209
+ expectedReplacementDigest: replacement.digest,
210
+ });
211
+ }
212
+ throw error;
213
+ }
214
+ const restoredExpiry = Date.parse(attached.expiresAt);
215
+ if (
216
+ (attached.result !== 'created' && attached.result !== 'reused') ||
217
+ attached.droneId !== record.droneId ||
218
+ attached.sessionId === record.sessionId ||
219
+ !Number.isFinite(restoredExpiry) ||
220
+ restoredExpiry <= deps.now()
221
+ ) {
222
+ await deps.scrubReplacement({
223
+ ref: active.localSessionCredentialRef,
224
+ binding: recordBinding,
225
+ expectedActiveDigest: activeDigest,
226
+ expectedReplacementDigest: replacement.digest,
227
+ });
228
+ throw new Error('Session renewal did not preserve the same saved seat');
229
+ }
230
+ const promoted = await deps.promoteReplacement({
231
+ ref: active.localSessionCredentialRef,
232
+ binding: recordBinding,
233
+ expectedDroneId: record.droneId!,
234
+ expectedActiveDigest: activeDigest,
235
+ expectedReplacementDigest: replacement.digest,
236
+ sessionId: attached.sessionId,
237
+ expiresAt: attached.expiresAt,
238
+ });
239
+ if (promoted !== 'promoted') return reloadSameSeat(active, deps, true);
240
+ return reloadSameSeat(active, deps, true);
241
+ }
242
+
243
+ function renewLocalSession(
244
+ active: ActiveCube,
245
+ mode: 'proactive' | 'expired',
246
+ deps: SessionContinuityDeps,
247
+ ): Promise<ActiveCube> {
248
+ const key = active.localSessionCredentialRef;
249
+ if (!key) return Promise.reject(new Error('Selected Borg server authority state is missing or unreadable'));
250
+ const existing = inflight.get(key);
251
+ if (existing) return existing;
252
+ const renewal = performRenewal(active, mode, deps);
253
+ inflight.set(key, renewal);
254
+ void renewal.finally(() => {
255
+ if (inflight.get(key) === renewal) inflight.delete(key);
256
+ }).catch(() => {});
257
+ return renewal;
258
+ }
259
+
260
+ export function ensureLocalSessionFresh(active: ActiveCube): Promise<ActiveCube> {
261
+ return renewLocalSession(active, 'proactive', defaultDeps);
262
+ }
263
+
264
+ export function recoverExpiredLocalSession(active: ActiveCube): Promise<ActiveCube> {
265
+ return renewLocalSession(active, 'expired', defaultDeps);
266
+ }
267
+
268
+ /** @internal */
269
+ export function __renewLocalSessionForTest(
270
+ active: ActiveCube,
271
+ mode: 'proactive' | 'expired',
272
+ deps: SessionContinuityDeps,
273
+ ): Promise<ActiveCube> {
274
+ return renewLocalSession(active, mode, deps);
275
+ }
276
+
277
+ /** @internal */
278
+ export function __resetSessionContinuityForTest(): void {
279
+ inflight.clear();
280
+ }