machine-bridge-mcp 2.0.0 → 3.0.0-beta.102

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 (473) hide show
  1. package/CHANGELOG.md +1142 -0
  2. package/CONTRIBUTING.md +23 -25
  3. package/GOVERNANCE.md +2 -2
  4. package/README.md +58 -20
  5. package/SECURITY.md +161 -120
  6. package/SUPPORT.md +1 -1
  7. package/browser-extension/broker-auth.js +83 -0
  8. package/browser-extension/broker-liveness.js +35 -0
  9. package/browser-extension/browser-error-boundary.js +96 -0
  10. package/browser-extension/browser-operations.js +1456 -101
  11. package/browser-extension/devtools-input.js +195 -34
  12. package/browser-extension/devtools-observation.js +429 -0
  13. package/browser-extension/devtools-session.js +48 -0
  14. package/browser-extension/manifest.json +4 -3
  15. package/browser-extension/page-automation.js +578 -118
  16. package/browser-extension/pairing-bootstrap.js +42 -0
  17. package/browser-extension/pairing.js +28 -12
  18. package/browser-extension/service-worker.js +126 -106
  19. package/docs/AGENT_CONTEXT.md +13 -10
  20. package/docs/ARCHITECTURE.md +159 -67
  21. package/docs/AUDIT.md +1302 -4
  22. package/docs/CLIENTS.md +19 -6
  23. package/docs/COMPUTER_USE.md +297 -0
  24. package/docs/ENGINEERING.md +72 -13
  25. package/docs/GETTING_STARTED.md +18 -20
  26. package/docs/LOCAL_AUTHORIZATION.md +115 -72
  27. package/docs/LOCAL_AUTOMATION.md +27 -21
  28. package/docs/LOGGING.md +40 -19
  29. package/docs/MANAGED_JOBS.md +37 -34
  30. package/docs/MULTI_ACCOUNT.md +98 -37
  31. package/docs/OPERATIONS.md +145 -51
  32. package/docs/OVERVIEW.md +14 -11
  33. package/docs/POLICY_REFERENCE.md +71 -3
  34. package/docs/PRIVACY.md +41 -2
  35. package/docs/PROJECT_STANDARDS.md +29 -6
  36. package/docs/RELEASING.md +238 -64
  37. package/docs/TESTING.md +150 -39
  38. package/docs/THREAT_MODEL.md +164 -67
  39. package/docs/TOOL_REFERENCE.md +653 -60
  40. package/docs/UPGRADING.md +78 -18
  41. package/native/macos/MachineBridgeBackgroundInput.swift +1051 -0
  42. package/native/macos/MachineBridgeBackgroundInputSmokeFixture.swift +295 -0
  43. package/native/macos/MachineBridgeTrustBroker.swift +233 -0
  44. package/package.json +94 -16
  45. package/scripts/accepted-candidate-tarball.mjs +77 -0
  46. package/scripts/candidate-runtime-store.mjs +93 -0
  47. package/scripts/check-plan.mjs +73 -3
  48. package/scripts/check-runner.mjs +129 -17
  49. package/scripts/commit-message-check.mjs +5 -2
  50. package/scripts/consumer-package-security.mjs +293 -0
  51. package/scripts/coverage-check.mjs +251 -15
  52. package/scripts/coverage-generation.mjs +41 -0
  53. package/scripts/coverage-range-merge.mjs +41 -0
  54. package/scripts/foreground-daemon-recovery.mjs +88 -0
  55. package/scripts/generate-policy-reference.mjs +28 -5
  56. package/scripts/generate-tool-reference.mjs +1 -1
  57. package/scripts/generate-worker-types.mjs +21 -11
  58. package/scripts/github-backlog.mjs +11 -10
  59. package/scripts/github-push.mjs +62 -16
  60. package/scripts/github-release-asset.mjs +58 -0
  61. package/scripts/github-release.mjs +241 -132
  62. package/scripts/global-package-installation.mjs +68 -0
  63. package/scripts/hardened-npm-session.mjs +42 -0
  64. package/scripts/install-published-prerelease.mjs +198 -0
  65. package/scripts/local-release-acceptance.mjs +103 -45
  66. package/scripts/macos-background-input-smoke.mjs +515 -0
  67. package/scripts/network-retry.mjs +8 -1
  68. package/scripts/npm-global-prefix.mjs +27 -0
  69. package/scripts/npm-publication-policy.mjs +17 -0
  70. package/scripts/official-mcp-conformance.mjs +267 -0
  71. package/scripts/persistent-activation-process.mjs +101 -0
  72. package/scripts/prepare-pinned-npm.mjs +12 -69
  73. package/scripts/prerelease-activation.mjs +160 -0
  74. package/scripts/privacy-check.mjs +14 -6
  75. package/scripts/promotion-digest.mjs +128 -0
  76. package/scripts/publish-npm.mjs +260 -0
  77. package/scripts/published-release.mjs +132 -0
  78. package/scripts/release-acceptance.mjs +62 -54
  79. package/scripts/release-browser-extension-store.mjs +172 -0
  80. package/scripts/release-candidate-manifest.mjs +52 -0
  81. package/scripts/release-channel.mjs +106 -0
  82. package/scripts/release-diagnostic.mjs +41 -0
  83. package/scripts/release-impact-check.mjs +12 -22
  84. package/scripts/release-oauth-canary-core.mjs +325 -0
  85. package/scripts/release-oauth-canary-evidence.mjs +100 -0
  86. package/scripts/release-oauth-canary.mjs +127 -0
  87. package/scripts/release-publication-guard.mjs +67 -0
  88. package/scripts/release-soak.mjs +284 -0
  89. package/scripts/release-state.mjs +1 -1
  90. package/scripts/run-checks.mjs +44 -7
  91. package/scripts/run-worker-dry-run.mjs +21 -0
  92. package/scripts/sarif-security-gate.mjs +2 -1
  93. package/scripts/sbom-check.mjs +106 -0
  94. package/scripts/start-release-candidate.mjs +267 -44
  95. package/scripts/syntax-check.mjs +36 -14
  96. package/scripts/verification-generation-guard.mjs +15 -0
  97. package/scripts/verification-idle-sleep-guard.mjs +37 -0
  98. package/scripts/verification-state.mjs +108 -0
  99. package/scripts/wrangler-command-lifecycle.mjs +115 -0
  100. package/src/local/account-access.mjs +13 -1
  101. package/src/local/account-admin-response.mjs +92 -0
  102. package/src/local/account-admin.mjs +52 -19
  103. package/src/local/agent-context-projection.mjs +26 -7
  104. package/src/local/agent-context.mjs +88 -49
  105. package/src/local/agent-skill-discovery.mjs +16 -5
  106. package/src/local/app-automation-macos-jxa.mjs +350 -0
  107. package/src/local/app-automation.mjs +873 -163
  108. package/src/local/application-capability-projection.mjs +24 -0
  109. package/src/local/authority-context.mjs +106 -0
  110. package/src/local/autostart-log-maintenance.mjs +36 -0
  111. package/src/local/browser-bridge-http.mjs +5 -3
  112. package/src/local/browser-bridge.mjs +109 -31
  113. package/src/local/browser-broker-auth-http.mjs +67 -0
  114. package/src/local/browser-broker-auth.mjs +98 -0
  115. package/src/local/browser-broker-routes.mjs +63 -20
  116. package/src/local/browser-broker-server.mjs +17 -7
  117. package/src/local/browser-command.mjs +79 -30
  118. package/src/local/browser-computer-observation-service.mjs +266 -0
  119. package/src/local/browser-extension-identity.mjs +50 -0
  120. package/src/local/browser-extension-path.mjs +22 -0
  121. package/src/local/browser-extension-protocol.mjs +50 -11
  122. package/src/local/browser-operation-service.mjs +147 -117
  123. package/src/local/browser-pairing-grant.mjs +109 -0
  124. package/src/local/browser-pairing-http.mjs +36 -0
  125. package/src/local/browser-pairing-launch.mjs +58 -0
  126. package/src/local/browser-pairing-store.mjs +85 -48
  127. package/src/local/browser-request-registry.mjs +40 -14
  128. package/src/local/browser-request-settlement.mjs +78 -0
  129. package/src/local/browser-resource-input.mjs +74 -0
  130. package/src/local/browser-trusted-input-health.mjs +63 -0
  131. package/src/local/call-authority.mjs +21 -0
  132. package/src/local/call-capacity.mjs +45 -0
  133. package/src/local/call-registry-drain.mjs +54 -0
  134. package/src/local/call-registry.mjs +30 -15
  135. package/src/local/capability-observer.mjs +5 -0
  136. package/src/local/capability-ranking.mjs +3 -2
  137. package/src/local/child-process-settlement.mjs +132 -0
  138. package/src/local/cli-account-admin.mjs +40 -6
  139. package/src/local/cli-activate.mjs +162 -0
  140. package/src/local/cli-local-admin.mjs +20 -45
  141. package/src/local/cli-options.mjs +12 -13
  142. package/src/local/cli-ready-output.mjs +52 -0
  143. package/src/local/cli-service.mjs +91 -11
  144. package/src/local/cli.mjs +232 -150
  145. package/src/local/computer-use-application-observation.mjs +224 -0
  146. package/src/local/computer-use-arguments.mjs +270 -0
  147. package/src/local/computer-use-deadline.mjs +50 -0
  148. package/src/local/computer-use-dispatch-settlement.mjs +40 -0
  149. package/src/local/computer-use-expectation.mjs +171 -0
  150. package/src/local/computer-use-observation.mjs +769 -0
  151. package/src/local/computer-use-recovery.mjs +82 -0
  152. package/src/local/computer-use-result-budget.mjs +145 -0
  153. package/src/local/computer-use-snapshot-store.mjs +67 -0
  154. package/src/local/computer-use.mjs +2255 -0
  155. package/src/local/daemon-http-relay-auth.mjs +26 -0
  156. package/src/local/daemon-http-relay-connection.mjs +298 -0
  157. package/src/local/daemon-http-relay-request.mjs +64 -0
  158. package/src/local/daemon-http-relay-sequence.mjs +49 -0
  159. package/src/local/daemon-process.mjs +40 -7
  160. package/src/local/delegated-process-sandbox.mjs +148 -0
  161. package/src/local/device-identity.mjs +156 -49
  162. package/src/local/device-root-provider.mjs +81 -0
  163. package/src/local/directory-metadata.mjs +47 -0
  164. package/src/local/doctor-reporting.mjs +23 -0
  165. package/src/local/durable-process-spec.mjs +80 -0
  166. package/src/local/errors.mjs +25 -15
  167. package/src/local/exclusive-file.mjs +74 -26
  168. package/src/local/execution-limits.mjs +9 -0
  169. package/src/local/execution-routing.mjs +276 -0
  170. package/src/local/execution-surface.mjs +23 -0
  171. package/src/local/file-mutation-coordinator.mjs +47 -0
  172. package/src/local/file-snapshot-preservation.mjs +24 -0
  173. package/src/local/filesystem-identity.mjs +54 -0
  174. package/src/local/fixed-process-environment.mjs +21 -0
  175. package/src/local/full-access-test.mjs +31 -13
  176. package/src/local/git-commit.mjs +69 -0
  177. package/src/local/git-config-safety.mjs +55 -0
  178. package/src/local/git-log-parser.mjs +41 -0
  179. package/src/local/git-metadata-boundary.mjs +77 -0
  180. package/src/local/git-metadata-tree-safety.mjs +59 -0
  181. package/src/local/git-operation-state.mjs +19 -0
  182. package/src/local/git-service.mjs +165 -30
  183. package/src/local/hardened-npm-download-timeout.mjs +46 -0
  184. package/src/local/hardened-npm-download.mjs +109 -0
  185. package/src/local/hardened-npm-extract.mjs +20 -0
  186. package/src/local/hardened-npm-verification.mjs +104 -0
  187. package/src/local/hardened-npm.mjs +199 -0
  188. package/src/local/job-runner.mjs +244 -89
  189. package/src/local/lifecycle.mjs +6 -0
  190. package/src/local/log.mjs +19 -45
  191. package/src/local/macos-background-input.mjs +398 -0
  192. package/src/local/macos-idle-sleep-assertion.mjs +65 -0
  193. package/src/local/macos-trust-broker.mjs +404 -0
  194. package/src/local/managed-job-cancellation.mjs +42 -0
  195. package/src/local/managed-job-capacity.mjs +16 -0
  196. package/src/local/managed-job-directory-generation.mjs +60 -0
  197. package/src/local/managed-job-directory.mjs +75 -0
  198. package/src/local/managed-job-durable-process.mjs +49 -0
  199. package/src/local/managed-job-lock.mjs +46 -14
  200. package/src/local/managed-job-plan.mjs +38 -14
  201. package/src/local/managed-job-projection.mjs +23 -15
  202. package/src/local/managed-job-retention.mjs +128 -0
  203. package/src/local/managed-job-runner-claim.mjs +70 -0
  204. package/src/local/managed-job-runner.mjs +35 -10
  205. package/src/local/managed-job-storage.mjs +22 -6
  206. package/src/local/managed-job-terminal-maintenance.mjs +40 -0
  207. package/src/local/managed-job-terminal.mjs +150 -0
  208. package/src/local/managed-jobs.mjs +424 -287
  209. package/src/local/npm-cli.mjs +42 -0
  210. package/src/local/npm-environment.mjs +37 -0
  211. package/src/local/operation-authorization.mjs +68 -324
  212. package/src/local/operation-risk.mjs +51 -4
  213. package/src/local/owner-state-lock.mjs +118 -0
  214. package/src/local/package-identity.mjs +9 -0
  215. package/src/local/patch.mjs +20 -18
  216. package/src/local/path-inspection.mjs +23 -0
  217. package/src/local/policy.mjs +19 -0
  218. package/src/local/private-toolchain-integrity.mjs +54 -0
  219. package/src/local/process-execution.mjs +87 -75
  220. package/src/local/process-foreground-timeout.mjs +50 -0
  221. package/src/local/process-identity.mjs +41 -7
  222. package/src/local/process-nonreplayable-settlement.mjs +50 -0
  223. package/src/local/process-result-projection.mjs +22 -0
  224. package/src/local/process-session-events.mjs +47 -0
  225. package/src/local/process-session-remote-activity.mjs +10 -0
  226. package/src/local/process-session-termination.mjs +73 -0
  227. package/src/local/process-sessions.mjs +144 -112
  228. package/src/local/process-tracker.mjs +140 -13
  229. package/src/local/process-tree-ownership-types.d.ts +37 -0
  230. package/src/local/process-tree-ownership.mjs +57 -0
  231. package/src/local/process-tree-signal.mjs +34 -0
  232. package/src/local/process-tree-snapshot.mjs +77 -0
  233. package/src/local/process-tree-supervisor.mjs +69 -0
  234. package/src/local/process-tree.mjs +11 -53
  235. package/src/local/project-metadata.mjs +19 -6
  236. package/src/local/relay-call-recovery.mjs +58 -38
  237. package/src/local/relay-connection-classification.mjs +131 -0
  238. package/src/local/relay-connection-support.mjs +91 -0
  239. package/src/local/relay-connection.mjs +179 -272
  240. package/src/local/relay-diagnostics.mjs +91 -0
  241. package/src/local/relay-heartbeat.mjs +119 -0
  242. package/src/local/relay-liveness.mjs +109 -0
  243. package/src/local/relay-peer-diagnostics.mjs +29 -0
  244. package/src/local/release-runtime-lock.mjs +18 -0
  245. package/src/local/remote-activity-idle-sleep-guard.mjs +77 -0
  246. package/src/local/remote-configuration.mjs +48 -0
  247. package/src/local/resilient-relay-connection.mjs +178 -0
  248. package/src/local/resource-admission-diagnostics.mjs +52 -0
  249. package/src/local/resource-admission-policy.mjs +115 -0
  250. package/src/local/resource-admission.mjs +339 -0
  251. package/src/local/resource-build-root.mjs +83 -0
  252. package/src/local/resource-cargo-concurrency.mjs +38 -0
  253. package/src/local/resource-cmake-concurrency.mjs +42 -0
  254. package/src/local/resource-command-concurrency.mjs +37 -0
  255. package/src/local/resource-command-profile.mjs +176 -0
  256. package/src/local/resource-coordinator-accounting.mjs +21 -0
  257. package/src/local/resource-cpu-window.mjs +20 -0
  258. package/src/local/resource-disk-headroom.mjs +13 -0
  259. package/src/local/resource-elastic-memory.mjs +19 -0
  260. package/src/local/resource-elastic-request.mjs +36 -0
  261. package/src/local/resource-foreground-wait.mjs +29 -0
  262. package/src/local/resource-go-concurrency.mjs +43 -0
  263. package/src/local/resource-gradle-concurrency.mjs +57 -0
  264. package/src/local/resource-host-cache.mjs +39 -0
  265. package/src/local/resource-host-darwin.mjs +54 -0
  266. package/src/local/resource-host-linux.mjs +35 -0
  267. package/src/local/resource-host-sample-file.mjs +23 -0
  268. package/src/local/resource-host-snapshot.mjs +99 -0
  269. package/src/local/resource-lease-accounting.mjs +119 -0
  270. package/src/local/resource-light-command.mjs +6 -0
  271. package/src/local/resource-make-concurrency.mjs +42 -0
  272. package/src/local/resource-maven-concurrency.mjs +38 -0
  273. package/src/local/resource-ninja-command-concurrency.mjs +20 -0
  274. package/src/local/resource-ninja-concurrency.mjs +28 -0
  275. package/src/local/resource-operations.mjs +26 -10
  276. package/src/local/resource-pressure.mjs +67 -0
  277. package/src/local/resource-probe-command.mjs +35 -0
  278. package/src/local/resource-process-admission.mjs +59 -0
  279. package/src/local/resource-process-ancestry-cache.mjs +43 -0
  280. package/src/local/resource-process-ancestry.mjs +32 -0
  281. package/src/local/resource-process-priority.mjs +15 -0
  282. package/src/local/resource-project-key.mjs +53 -0
  283. package/src/local/resource-pytest-concurrency.mjs +33 -0
  284. package/src/local/resource-release-control-classification.mjs +14 -0
  285. package/src/local/resource-release-control-executable.mjs +70 -0
  286. package/src/local/resource-release-control-workspace.mjs +41 -0
  287. package/src/local/resource-request-contract.mjs +30 -0
  288. package/src/local/resource-script-classification.mjs +44 -0
  289. package/src/local/resource-shell-analysis.mjs +58 -0
  290. package/src/local/resource-staging-recovery.mjs +97 -0
  291. package/src/local/resource-swift-concurrency.mjs +31 -0
  292. package/src/local/resource-transaction-lock.mjs +189 -0
  293. package/src/local/resource-wait.mjs +48 -0
  294. package/src/local/resource-waiters.mjs +139 -0
  295. package/src/local/resource-xcode-command.mjs +7 -0
  296. package/src/local/resource-xcode-concurrency.mjs +34 -0
  297. package/src/local/resource-xcode-non-build.mjs +19 -0
  298. package/src/local/runtime-activation.mjs +700 -0
  299. package/src/local/runtime-activity-projection.mjs +48 -0
  300. package/src/local/runtime-capabilities.mjs +48 -15
  301. package/src/local/runtime-diagnostic-state.mjs +14 -0
  302. package/src/local/runtime-diagnostics.mjs +40 -13
  303. package/src/local/runtime-info-projection.mjs +60 -0
  304. package/src/local/runtime-info-relay-projection.mjs +21 -0
  305. package/src/local/runtime-paths.mjs +2 -1
  306. package/src/local/runtime-process-routing.mjs +33 -0
  307. package/src/local/runtime-relay-connection-options.mjs +44 -0
  308. package/src/local/runtime-relay-control.mjs +98 -0
  309. package/src/local/runtime-relay.mjs +26 -50
  310. package/src/local/runtime-reporting.mjs +37 -21
  311. package/src/local/runtime-resource-service.mjs +77 -0
  312. package/src/local/runtime-tool-handlers.mjs +13 -10
  313. package/src/local/runtime.mjs +244 -208
  314. package/src/local/secure-file.mjs +73 -20
  315. package/src/local/security-audit-dispatch.mjs +35 -0
  316. package/src/local/security-audit-log.mjs +215 -0
  317. package/src/local/security-audit-state.mjs +139 -0
  318. package/src/local/security-audit-storage.mjs +135 -0
  319. package/src/local/security-audit-warning.mjs +31 -0
  320. package/src/local/security-audit-worker.mjs +87 -0
  321. package/src/local/service-convergence.mjs +42 -1
  322. package/src/local/service-definition.mjs +24 -0
  323. package/src/local/service-environment.mjs +18 -10
  324. package/src/local/service-owner.mjs +146 -0
  325. package/src/local/service-ownership.mjs +17 -0
  326. package/src/local/service-restart-handoff.mjs +64 -0
  327. package/src/local/service-restart-scheduler.mjs +49 -0
  328. package/src/local/service-runtime-convergence.mjs +52 -0
  329. package/src/local/service-runtime.mjs +105 -0
  330. package/src/local/service-status.mjs +60 -0
  331. package/src/local/service.mjs +385 -92
  332. package/src/local/shell.mjs +39 -19
  333. package/src/local/ssh-key.mjs +158 -42
  334. package/src/local/state-inventory.mjs +50 -24
  335. package/src/local/state-owner-lock-inventory.mjs +23 -0
  336. package/src/local/state-root-owned-namespaces.mjs +83 -0
  337. package/src/local/state-root-retirement.mjs +89 -0
  338. package/src/local/state.mjs +386 -96
  339. package/src/local/stdio.mjs +96 -66
  340. package/src/local/support-state-projection.mjs +30 -0
  341. package/src/local/system-network-route.mjs +76 -0
  342. package/src/local/systemd-removal.mjs +20 -0
  343. package/src/local/tool-executor.mjs +85 -20
  344. package/src/local/tool-result-boundary.mjs +51 -0
  345. package/src/local/toolchain-operation-lock.mjs +41 -0
  346. package/src/local/tools.mjs +7 -5
  347. package/src/local/trusted-executable.mjs +53 -0
  348. package/src/local/trusted-git-executable.mjs +34 -0
  349. package/src/local/trusted-github-cli.mjs +24 -0
  350. package/src/local/windows-service-convergence.mjs +49 -0
  351. package/src/local/windows-service.mjs +52 -45
  352. package/src/local/worker-deployment-fingerprint.mjs +102 -0
  353. package/src/local/worker-deployment.mjs +48 -60
  354. package/src/local/worker-health.mjs +12 -4
  355. package/src/local/worker-secret-file.mjs +37 -17
  356. package/src/local/workspace-file-service.mjs +156 -239
  357. package/src/local/workspace-file-transaction.mjs +189 -0
  358. package/src/local/workspace-search.mjs +55 -0
  359. package/src/local/wrangler-toolchain/package-lock.json +1505 -0
  360. package/src/local/wrangler-toolchain/package.json +23 -0
  361. package/src/local/wrangler-toolchain-verification.mjs +135 -0
  362. package/src/local/wrangler-toolchain.mjs +188 -0
  363. package/src/shared/access-contract.json +7 -1
  364. package/src/shared/activation-recovery.mjs +49 -0
  365. package/src/shared/admin-auth.d.mts +2 -1
  366. package/src/shared/admin-auth.mjs +3 -3
  367. package/src/shared/authority-revocation.d.mts +9 -0
  368. package/src/shared/authority-revocation.mjs +28 -0
  369. package/src/shared/daemon-auth.d.mts +11 -0
  370. package/src/shared/daemon-auth.mjs +17 -0
  371. package/src/shared/device-session-auth.d.mts +20 -0
  372. package/src/shared/device-session-auth.mjs +51 -0
  373. package/src/shared/foreground-timeout.d.mts +15 -0
  374. package/src/shared/foreground-timeout.mjs +70 -0
  375. package/src/shared/log-redaction.d.mts +9 -0
  376. package/src/shared/log-redaction.mjs +51 -0
  377. package/src/shared/mcp-protocol.d.mts +27 -0
  378. package/src/shared/mcp-protocol.mjs +275 -0
  379. package/src/shared/project-overview-projection.d.mts +4 -0
  380. package/src/shared/project-overview-projection.mjs +96 -0
  381. package/src/shared/relay-contract.json +28 -0
  382. package/src/shared/result-projection.d.mts +2 -1
  383. package/src/shared/result-projection.mjs +13 -2
  384. package/src/shared/server-metadata.json +15 -9
  385. package/src/shared/tool-argument-validation.d.mts +17 -0
  386. package/src/shared/tool-argument-validation.mjs +325 -0
  387. package/src/shared/tool-call-capacity.d.mts +41 -0
  388. package/src/shared/tool-call-capacity.mjs +59 -0
  389. package/src/shared/tool-catalog.json +626 -57
  390. package/src/worker/access.ts +12 -0
  391. package/src/worker/account-admin.ts +55 -33
  392. package/src/worker/authority-revocations.ts +174 -0
  393. package/src/worker/authority.ts +52 -6
  394. package/src/worker/daemon-auth.ts +49 -68
  395. package/src/worker/daemon-channel.ts +36 -0
  396. package/src/worker/daemon-http-auth.ts +63 -0
  397. package/src/worker/daemon-http-channel.ts +120 -0
  398. package/src/worker/daemon-http-controller.ts +163 -0
  399. package/src/worker/daemon-http-protocol.ts +62 -0
  400. package/src/worker/daemon-http-registry.ts +82 -0
  401. package/src/worker/daemon-last-observation.ts +30 -0
  402. package/src/worker/daemon-liveness.ts +8 -6
  403. package/src/worker/daemon-ready-dispatch.ts +17 -0
  404. package/src/worker/daemon-ready-messages.ts +73 -0
  405. package/src/worker/daemon-ready-waiters.ts +79 -0
  406. package/src/worker/daemon-recovery-budget.ts +13 -0
  407. package/src/worker/daemon-registry.ts +58 -0
  408. package/src/worker/daemon-relay-diagnostics.ts +127 -0
  409. package/src/worker/daemon-socket-attachment.ts +55 -0
  410. package/src/worker/daemon-sockets.ts +72 -62
  411. package/src/worker/daemon-status.ts +38 -0
  412. package/src/worker/device-session-verifier.ts +129 -0
  413. package/src/worker/dpop.ts +157 -0
  414. package/src/worker/durable-process-timeout.ts +29 -0
  415. package/src/worker/errors.ts +57 -0
  416. package/src/worker/http.ts +112 -26
  417. package/src/worker/index.ts +446 -440
  418. package/src/worker/mcp-access.ts +57 -0
  419. package/src/worker/mcp-controller.ts +188 -0
  420. package/src/worker/mcp-http-accept.ts +11 -0
  421. package/src/worker/mcp-http-contract.ts +322 -0
  422. package/src/worker/mcp-initialization-compat.ts +141 -0
  423. package/src/worker/mcp-jsonrpc.ts +11 -29
  424. package/src/worker/mcp-removed-protocol.ts +17 -0
  425. package/src/worker/mcp-response-proxy.ts +135 -0
  426. package/src/worker/mcp-response-stream.ts +65 -0
  427. package/src/worker/mcp-stale-schema-compat.ts +57 -0
  428. package/src/worker/mcp-stream-proxy-contract.ts +48 -0
  429. package/src/worker/mcp-tool-call-input.ts +23 -0
  430. package/src/worker/nonce-store.ts +33 -35
  431. package/src/worker/oauth-authorization-page.ts +21 -1
  432. package/src/worker/oauth-client-admin.ts +52 -0
  433. package/src/worker/oauth-client-contract.ts +38 -0
  434. package/src/worker/oauth-controller.ts +107 -47
  435. package/src/worker/oauth-field-contract.ts +31 -0
  436. package/src/worker/oauth-record-contract.ts +7 -0
  437. package/src/worker/oauth-refresh-authority.ts +27 -0
  438. package/src/worker/oauth-refresh-exchange.ts +162 -0
  439. package/src/worker/oauth-refresh-families.ts +130 -27
  440. package/src/worker/oauth-refresh-persistence.ts +116 -0
  441. package/src/worker/oauth-state.ts +33 -18
  442. package/src/worker/oauth-store-validation.ts +131 -0
  443. package/src/worker/oauth-token-derivation.ts +33 -0
  444. package/src/worker/oauth-token-issuance.ts +119 -0
  445. package/src/worker/oauth-tokens.ts +56 -190
  446. package/src/worker/observability.ts +38 -5
  447. package/src/worker/pending-admission.ts +15 -0
  448. package/src/worker/pending-call-capacity.ts +108 -0
  449. package/src/worker/pending-call-contract.ts +42 -5
  450. package/src/worker/pending-call-deadlines.ts +75 -0
  451. package/src/worker/pending-calls.ts +131 -125
  452. package/src/worker/runtime-alarm-storage.ts +37 -0
  453. package/src/worker/runtime-alarm.ts +136 -0
  454. package/src/worker/server-info-activity.ts +36 -0
  455. package/src/worker/server-info-tool-delivery.ts +15 -0
  456. package/src/worker/server-info.ts +168 -0
  457. package/src/worker/tool-catalog.ts +47 -1
  458. package/src/worker/tool-timeout.ts +75 -14
  459. package/src/worker/websocket-protocol.ts +26 -2
  460. package/src/worker/worker-edge-guard.ts +90 -0
  461. package/src/worker/worker-edge-log.ts +65 -0
  462. package/src/worker/worker-entry.ts +52 -0
  463. package/src/worker/worker-mcp-config.ts +23 -0
  464. package/src/worker/worker-metadata.ts +48 -0
  465. package/src/worker/worker-rate-limit-key.ts +30 -0
  466. package/src/worker/worker-runtime-config.ts +20 -0
  467. package/src/worker/worker-static-routes.ts +54 -0
  468. package/src/worker/worker-task-lifetime.ts +18 -0
  469. package/tsconfig.local.json +17 -1
  470. package/wrangler.jsonc +13 -0
  471. package/src/local/cli-approval.mjs +0 -117
  472. package/src/local/operation-state-lock.mjs +0 -92
  473. package/src/worker/mcp-session.ts +0 -72
@@ -1,22 +1,25 @@
1
1
  import { createHash, randomBytes } from "node:crypto";
2
- import { existsSync, lstatSync, mkdirSync, realpathSync, rmSync, unlinkSync, readdirSync, statSync } from "node:fs";
2
+ import { existsSync, lstatSync, mkdirSync, realpathSync, unlinkSync, readdirSync, statSync } from "node:fs";
3
3
  import os from "node:os";
4
4
  import path from "node:path";
5
- import { fileURLToPath } from "node:url";
6
- import serverMetadata from "../shared/server-metadata.json" with { type: "json" };
7
- import { replaceFileSync } from "./atomic-fs.mjs";
8
5
  import { createExclusiveFileSync, replaceFileAtomicallySync } from "./exclusive-file.mjs";
6
+ import { preserveFileSnapshotSync } from "./file-snapshot-preservation.mjs";
9
7
  import { createMonotonicDeadline } from "./monotonic-deadline.mjs";
10
- import { createDeviceIdentity, validateDeviceIdentity } from "./device-identity.mjs";
8
+ import { createDeviceIdentity } from "./device-identity.mjs";
9
+ import { validateDeviceRootIdentity } from "./device-root-provider.mjs";
11
10
  import { currentProcessStartTimeMs, inspectProcessInstance } from "./process-identity.mjs";
12
- import { chmodRegularFileSync, ensureOwnerOnlyDirectorySync, readBoundedRegularFileSync } from "./secure-file.mjs";
11
+ import { ensureOwnerOnlyDir, inspectPathIfPresentSync, ownerOnlyFile, readBoundedRegularFileSync, readBoundedRegularFileWithInfoSync, retryTransientMultipleLinksSync, unlinkRegularFileIfIdentitySync } from "./secure-file.mjs";
12
+ import { isPlainRecord } from "./records.mjs";
13
+ import { exactFilesystemInteger, filesystemIdentity, filesystemTimeMs, sameFilesystemIdentity } from "./filesystem-identity.mjs";
14
+ import { appName, packageRoot } from "./package-identity.mjs";
15
+ import { inspectStateRootGeneration, pruneRetiredStateRootDirectories, removeStateRootGenerationIfCurrent } from "./state-root-retirement.mjs";
16
+ import { validateOwnedStateNamespaces } from "./state-root-owned-namespaces.mjs";
13
17
 
14
- export const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
15
- export const appName = String(serverMetadata.name);
16
18
  const STATE_MARKER = ".machine-bridge-mcp-state";
17
19
  const STATE_MARKER_SCHEMA = 2;
18
20
  export const STATE_SCHEMA_VERSION = 6;
19
21
  const GLOBAL_CONFIG_SCHEMA = 1;
22
+ const RECOVERY_MARKER_SCHEMA = 1;
20
23
  const CORRUPT_RECOVERY = Symbol("corrupt-recovery");
21
24
  const MAX_STATE_JSON_BYTES = 2 * 1024 * 1024;
22
25
  const MAX_LOCK_BYTES = 64 * 1024;
@@ -24,6 +27,7 @@ const MAX_MARKER_BYTES = 4096;
24
27
  const STARTUP_LOCK_MAX_AGE_MS = 2 * 60 * 60 * 1000;
25
28
  const MALFORMED_LOCK_GRACE_MS = 60_000;
26
29
  const MAINTENANCE_LOCK_MAX_AGE_MS = 2 * 60 * 60 * 1000;
30
+ const MACHINE_SERVICE_LOCK_MAX_AGE_MS = 2 * 60 * 60 * 1000;
27
31
 
28
32
  export function expandHome(input = "") {
29
33
  if (!input || input === "~") return os.homedir();
@@ -31,6 +35,12 @@ export function expandHome(input = "") {
31
35
  return input;
32
36
  }
33
37
 
38
+ function expandHomeFrom(input, home) {
39
+ if (!input || input === "~") return home;
40
+ if (input.startsWith("~/")) return path.join(home, input.slice(2));
41
+ return input;
42
+ }
43
+
34
44
  export function resolveWorkspace(input = process.cwd()) {
35
45
  const resolved = path.resolve(expandHome(input));
36
46
  return realpathSync.native ? realpathSync.native(resolved) : realpathSync(resolved);
@@ -46,13 +56,16 @@ export function ensureWorkspaceDirectory(input) {
46
56
  return resolveWorkspace(requested);
47
57
  }
48
58
 
49
- export function defaultStateRoot() {
50
- if (process.platform === "win32") {
51
- const base = process.env.APPDATA || path.join(os.homedir(), "AppData", "Roaming");
59
+ export function defaultStateRoot(options = {}) {
60
+ const platform = String(options.platform || process.platform);
61
+ const home = path.resolve(String(options.home || os.homedir()));
62
+ const environment = options.environment && typeof options.environment === "object" ? options.environment : process.env;
63
+ if (platform === "win32") {
64
+ const base = environment.APPDATA ? expandHomeFrom(String(environment.APPDATA), home) : path.join(home, "AppData", "Roaming");
52
65
  return path.join(base, appName);
53
66
  }
54
- if (process.env.XDG_STATE_HOME) return path.join(expandHome(process.env.XDG_STATE_HOME), appName);
55
- return path.join(os.homedir(), ".local", "state", appName);
67
+ if (environment.XDG_STATE_HOME) return path.join(expandHomeFrom(String(environment.XDG_STATE_HOME), home), appName);
68
+ return path.join(home, ".local", "state", appName);
56
69
  }
57
70
 
58
71
 
@@ -60,13 +73,16 @@ function configPath(stateRoot = defaultStateRoot()) {
60
73
  return path.join(expandHome(stateRoot), "config.json");
61
74
  }
62
75
 
63
- export function loadGlobalConfig(stateRoot = defaultStateRoot()) {
76
+ export function loadGlobalConfig(stateRoot = defaultStateRoot(), options = {}) {
64
77
  const root = path.resolve(expandHome(stateRoot));
65
78
  assertNoForeignMaintenance(root);
66
79
  const file = configPath(root);
67
- if (!existsSync(file)) return { schemaVersion: GLOBAL_CONFIG_SCHEMA };
80
+ const inspect = options.inspectPathIfPresentSync || inspectPathIfPresentSync;
81
+ const info = inspect(file, "global configuration file");
82
+ if (!info) return { schemaVersion: GLOBAL_CONFIG_SCHEMA };
83
+ if (info.isSymbolicLink() || !info.isFile()) throw new Error("global configuration file must be a regular file and not a symbolic link");
68
84
  ownerOnlyFile(file);
69
- const config = readJsonObjectOrBackup(file);
85
+ const config = readJsonObjectOrBackup(file, { allowEmptyRecovery: true });
70
86
  if (config[CORRUPT_RECOVERY]) return { schemaVersion: GLOBAL_CONFIG_SCHEMA };
71
87
  if (config.schemaVersion !== GLOBAL_CONFIG_SCHEMA) {
72
88
  throw new Error("global configuration schema is obsolete; remove the state root and initialize the current version");
@@ -100,16 +116,21 @@ export function selectedWorkspace(stateRoot = defaultStateRoot()) {
100
116
 
101
117
  export function validateStateRootForRemoval(stateRoot = defaultStateRoot()) {
102
118
  const root = path.resolve(expandHome(stateRoot));
103
- if (!existsSync(root)) return { exists: false, root };
119
+ if (!inspectPathIfPresentSync(root, "state root")) return { exists: false, root };
104
120
  const canonical = assertSafeStateRootForRemoval(root);
105
121
  return { exists: true, root: canonical };
106
122
  }
107
123
 
108
124
  export function removeStateRoot(stateRoot = defaultStateRoot()) {
109
- const validation = validateStateRootForRemoval(stateRoot);
125
+ const requested = path.resolve(expandHome(stateRoot));
126
+ pruneRetiredStateRootDirectories(requested, assertSafeStateRootForRemoval);
127
+ const validation = validateStateRootForRemoval(requested);
110
128
  if (!validation.exists) return false;
111
- rmSync(validation.root, { recursive: true, force: true });
112
- return true;
129
+ const generation = inspectStateRootGeneration(validation.root);
130
+ if (removeStateRootGenerationIfCurrent(validation.root, generation, assertSafeStateRootForRemoval)) return true;
131
+ pruneRetiredStateRootDirectories(requested, assertSafeStateRootForRemoval);
132
+ if (!inspectPathIfPresentSync(validation.root, "state root")) return false;
133
+ throw new Error("state root changed during generation-bound removal; state requires inspection");
113
134
  }
114
135
 
115
136
  function workspaceHash(workspace) {
@@ -127,7 +148,7 @@ function profileDirForWorkspace(workspace, stateRoot = defaultStateRoot()) {
127
148
  function canonicalizePotentialPath(input) {
128
149
  let existing = path.resolve(input);
129
150
  const suffix = [];
130
- while (!existsSync(existing)) {
151
+ while (!inspectPathIfPresentSync(existing, "path identity")) {
131
152
  const parent = path.dirname(existing);
132
153
  if (parent === existing) break;
133
154
  suffix.unshift(path.basename(existing));
@@ -149,6 +170,16 @@ function assertStateRootSeparatedFromWorkspace(stateRoot, workspace) {
149
170
  }
150
171
  }
151
172
 
173
+ function samePotentialPathIdentity(left, right) {
174
+ try {
175
+ const a = canonicalizePotentialPath(String(left));
176
+ const b = canonicalizePotentialPath(String(right));
177
+ return process.platform === "win32" ? a.toLowerCase() === b.toLowerCase() : a === b;
178
+ } catch {
179
+ return false;
180
+ }
181
+ }
182
+
152
183
  function sameWorkspaceIdentity(left, right) {
153
184
  try {
154
185
  const a = resolveWorkspace(left);
@@ -169,13 +200,24 @@ export function loadState(workspace, options = {}) {
169
200
  const profileDir = profileDirForWorkspace(canonicalWorkspace, stateRoot);
170
201
  const statePath = path.join(profileDir, "state.json");
171
202
  ensureOwnerOnlyDir(profileDir);
203
+ removeObsoleteOperationLeaseState(profileDir);
204
+ const recoveryPath = recoveryMarkerPath(statePath);
205
+ const recoveryPending = Boolean(inspectPathIfPresentSync(recoveryPath, "state recovery marker"));
206
+ const recoverySnapshot = recoveryPending ? readRecoveryMarker(recoveryPath, statePath) : null;
207
+ const recoveryMarker = recoverySnapshot?.marker || null;
172
208
  let state = {};
173
- if (existsSync(statePath)) {
209
+ if (inspectPathIfPresentSync(statePath, "workspace state")) {
174
210
  ownerOnlyFile(statePath);
175
- state = readJsonObjectOrBackup(statePath);
176
- if (!state[CORRUPT_RECOVERY] && state.schemaVersion !== STATE_SCHEMA_VERSION) {
211
+ state = readJsonObjectOrBackup(statePath, { recoveryPath });
212
+ if (state.schemaVersion !== STATE_SCHEMA_VERSION) {
177
213
  throw new Error("workspace state schema is obsolete; remove the state root and initialize the current version");
178
214
  }
215
+ assertWorkspaceStateEnvelope(state, { canonicalWorkspace, stateRoot, profileDir, statePath });
216
+ if (recoverySnapshot && !unlinkRegularFileIfIdentitySync(recoveryPath, recoverySnapshot.identity, "state recovery marker")) {
217
+ throw new Error("workspace state recovery marker changed before removal");
218
+ }
219
+ } else if (recoveryPending) {
220
+ throw recoveryRequiredError(recoveryMarker);
179
221
  }
180
222
  state.schemaVersion = STATE_SCHEMA_VERSION;
181
223
  state.workspace = {
@@ -185,17 +227,78 @@ export function loadState(workspace, options = {}) {
185
227
  };
186
228
  state.paths = { stateRoot, profileDir, statePath };
187
229
  state.worker ||= {};
230
+ delete state.worker.accountAdminSecret;
188
231
  state.policy ||= {};
189
232
  state.resources ||= {};
190
233
  return state;
191
234
  }
192
235
 
236
+ function removeObsoleteOperationLeaseState(profileDir) {
237
+ const file = path.join(profileDir, "operation-leases.json");
238
+ const info = inspectPathIfPresentSync(file, "obsolete operation lease state", {
239
+ lstatSync: (target) => lstatSync(target, { bigint: true }),
240
+ });
241
+ if (!info) return false;
242
+ if (info.isSymbolicLink() || !info.isFile() || info.nlink !== 1n) {
243
+ throw new Error("obsolete operation lease state must be a single-link regular file before migration cleanup");
244
+ }
245
+ const identity = filesystemIdentity(info, "obsolete operation lease state");
246
+ if (!unlinkRegularFileIfIdentitySync(file, identity, "obsolete operation lease state")) {
247
+ throw new Error("obsolete operation lease state changed before migration cleanup");
248
+ }
249
+ return true;
250
+ }
251
+
193
252
  export function saveState(state) {
194
- assertNoForeignMaintenance(state?.paths?.stateRoot);
195
- const statePath = state?.paths?.statePath;
196
- if (!statePath) throw new Error("state path is missing");
197
- ensureOwnerOnlyDir(path.dirname(statePath));
198
- atomicWriteJson(statePath, { ...state });
253
+ const envelope = assertWorkspaceStateEnvelope(state);
254
+ assertNoForeignMaintenance(envelope.stateRoot);
255
+ assertValidStateMarker(path.join(envelope.stateRoot, STATE_MARKER));
256
+ ensureOwnerOnlyDir(envelope.profileDir);
257
+ atomicWriteJson(envelope.statePath, { ...state });
258
+ const recoveryPath = recoveryMarkerPath(envelope.statePath);
259
+ if (inspectPathIfPresentSync(recoveryPath, "state recovery marker")) {
260
+ const recoverySnapshot = readRecoveryMarker(recoveryPath, envelope.statePath);
261
+ if (!unlinkRegularFileIfIdentitySync(recoveryPath, recoverySnapshot.identity, "state recovery marker")) {
262
+ throw new Error("workspace state recovery marker changed before removal");
263
+ }
264
+ }
265
+ }
266
+
267
+ function assertWorkspaceStateEnvelope(state, expected = {}) {
268
+ if (!isPlainRecord(state) || state.schemaVersion !== STATE_SCHEMA_VERSION) {
269
+ throw new Error("workspace state envelope is incomplete or uses the wrong schema");
270
+ }
271
+ if (!isPlainRecord(state.workspace) || typeof state.workspace.path !== "string" || typeof state.workspace.hash !== "string") {
272
+ throw new Error("workspace state envelope is missing workspace identity");
273
+ }
274
+ if (!isPlainRecord(state.paths) || !isPlainRecord(state.worker) || !isPlainRecord(state.policy) || !isPlainRecord(state.resources)) {
275
+ throw new Error("workspace state envelope is missing required object fields");
276
+ }
277
+ const canonicalWorkspace = resolveWorkspace(expected.canonicalWorkspace || state.workspace.path);
278
+ const expectedHash = workspaceHash(canonicalWorkspace);
279
+ if (!sameWorkspaceIdentity(state.workspace.path, canonicalWorkspace) || state.workspace.hash !== expectedHash) {
280
+ throw new Error("workspace state envelope does not match the selected workspace");
281
+ }
282
+ if (typeof state.paths.stateRoot !== "string" || !state.paths.stateRoot
283
+ || typeof state.paths.profileDir !== "string" || !state.paths.profileDir
284
+ || typeof state.paths.statePath !== "string" || !state.paths.statePath) {
285
+ throw new Error("workspace state envelope is missing canonical state paths");
286
+ }
287
+ const stateRoot = canonicalizePotentialPath(expected.stateRoot || state.paths.stateRoot);
288
+ const profileDir = canonicalizePotentialPath(expected.profileDir || state.paths.profileDir);
289
+ const statePath = canonicalizePotentialPath(expected.statePath || state.paths.statePath);
290
+ const expectedProfileDir = canonicalizePotentialPath(profileDirForWorkspace(canonicalWorkspace, stateRoot));
291
+ const expectedStatePath = canonicalizePotentialPath(path.join(expectedProfileDir, "state.json"));
292
+ if (!samePotentialPathIdentity(profileDir, expectedProfileDir) || !samePotentialPathIdentity(statePath, expectedStatePath)) {
293
+ throw new Error("workspace state envelope contains inconsistent state paths");
294
+ }
295
+ if (!samePotentialPathIdentity(state.paths.stateRoot, stateRoot)
296
+ || !samePotentialPathIdentity(state.paths.profileDir, profileDir)
297
+ || !samePotentialPathIdentity(state.paths.statePath, statePath)) {
298
+ throw new Error("workspace state envelope does not match the active state location");
299
+ }
300
+ assertStateRootSeparatedFromWorkspace(stateRoot, canonicalWorkspace);
301
+ return { canonicalWorkspace, stateRoot, profileDir, statePath };
199
302
  }
200
303
 
201
304
  export function daemonLockPathForState(state) {
@@ -212,9 +315,59 @@ function lockPathForState(state, name) {
212
315
  return path.join(profileDir, name);
213
316
  }
214
317
 
318
+ export function machineServiceControlRoot(options = {}) {
319
+ const home = path.resolve(String(options.home || os.homedir()));
320
+ if (options.controlRoot) return path.resolve(expandHomeFrom(String(options.controlRoot), home));
321
+ if (String(options.platform || process.platform) === "win32") {
322
+ const environment = options.environment && typeof options.environment === "object" ? options.environment : process.env;
323
+ const base = environment.APPDATA ? expandHomeFrom(String(environment.APPDATA), home) : path.join(home, "AppData", "Roaming");
324
+ return path.join(base, `${appName}-control`);
325
+ }
326
+ return path.join(home, ".local", "state", `${appName}-control`);
327
+ }
328
+
329
+ export function machineServiceLockPath(options = {}) {
330
+ return path.join(machineServiceControlRoot(options), "service-operation.lock");
331
+ }
332
+
333
+ export function acquireMachineServiceLock(metadata = {}, options = {}) {
334
+ const root = machineServiceControlRoot(options);
335
+ ensureOwnerOnlyDir(root);
336
+ assertNoForeignMaintenance(root);
337
+ const operation = typeof metadata.operation === "string" && /^[a-z][a-z0-9._-]{0,63}$/.test(metadata.operation)
338
+ ? metadata.operation
339
+ : "service-operation";
340
+ return acquireProcessLock(machineServiceLockPath({ controlRoot: root }), {
341
+ workspace: { path: "" },
342
+ paths: { profileDir: root, stateRoot: root },
343
+ }, "machine-service", { operation }, { maxAgeMs: MACHINE_SERVICE_LOCK_MAX_AGE_MS });
344
+ }
345
+
346
+ export async function acquireMachineServiceLockWithWait(options = {}) {
347
+ const timeoutMs = boundedPositiveInteger(options.timeoutMs, 30_000);
348
+ const pollMs = boundedPositiveInteger(options.pollMs, 100);
349
+ const logger = options.logger || { info() {} };
350
+ const metadata = { operation: options.operation };
351
+ let lock = acquireMachineServiceLock(metadata, options);
352
+ if (lock.acquired) return lock;
353
+ logger.info?.("waiting for another process to finish the current machine-service operation");
354
+ const deadline = createMonotonicDeadline(timeoutMs);
355
+ while (!deadline.expired()) {
356
+ await new Promise((resolvePromise) => { setTimeout(resolvePromise, Math.min(pollMs, Math.max(1, deadline.remainingMs()))); });
357
+ lock = acquireMachineServiceLock(metadata, options);
358
+ if (lock.acquired) {
359
+ logger.info?.("the previous machine-service operation finished; continuing");
360
+ return lock;
361
+ }
362
+ }
363
+ const pid = lock.owner?.pid ? `pid ${lock.owner.pid}` : "unknown pid";
364
+ const operation = typeof lock.owner?.operation === "string" ? ` (${lock.owner.operation})` : "";
365
+ throw new Error(`another machine-service operation did not finish within ${Math.ceil(timeoutMs / 1000)} seconds (${pid}${operation}); inspect the process before retrying`);
366
+ }
367
+
215
368
  export function acquireMaintenanceLock(stateRoot, metadata = {}) {
216
369
  const root = path.resolve(expandHome(stateRoot));
217
- if (!existsSync(root)) throw new Error("cannot acquire maintenance lock for a missing state root");
370
+ if (!inspectPathIfPresentSync(root, "state root")) throw new Error("cannot acquire maintenance lock for a missing state root");
218
371
  const operation = typeof metadata.operation === "string" && /^[a-z][a-z0-9._-]{0,63}$/.test(metadata.operation)
219
372
  ? metadata.operation
220
373
  : "maintenance";
@@ -231,7 +384,6 @@ export function assertStateMaintenanceAvailable(stateRoot) {
231
384
  function assertNoForeignMaintenance(stateRoot) {
232
385
  if (!stateRoot) return;
233
386
  const file = path.join(path.resolve(expandHome(stateRoot)), "maintenance.lock");
234
- if (!existsSync(file)) return;
235
387
  const snapshot = readProcessLockSnapshot(file);
236
388
  if (!snapshot) return;
237
389
  if (!snapshot.owner) {
@@ -256,7 +408,7 @@ function assertNoForeignMaintenance(stateRoot) {
256
408
 
257
409
  export function acquireDaemonLock(state, metadata = {}) {
258
410
  assertNoForeignMaintenance(state?.paths?.stateRoot);
259
- const details = {};
411
+ const details = { startupReady: false, startupReadyAt: null, nodeVersion: process.versions.node };
260
412
  if (metadata?.mode === "foreground" || metadata?.mode === "service") details.mode = metadata.mode;
261
413
  if (typeof metadata?.version === "string" && /^[0-9A-Za-z.+_-]{1,64}$/.test(metadata.version)) details.version = metadata.version;
262
414
  return acquireProcessLock(daemonLockPathForState(state), state, "daemon", details, { maxAgeMs: Number.POSITIVE_INFINITY });
@@ -276,8 +428,7 @@ export async function acquireStartupLockWithWait(state, options = {}) {
276
428
  const metadata = { operation: options.operation };
277
429
  let lock = acquireStartupLock(state, metadata);
278
430
  if (lock.acquired) return lock;
279
- const ownerPid = lock.owner?.pid ? `pid ${lock.owner.pid}` : "another process";
280
- logger.info?.(`waiting for ${ownerPid} to finish the current startup/state operation`);
431
+ logger.info?.("waiting for another process to finish the current startup/state operation");
281
432
  const deadline = createMonotonicDeadline(timeoutMs);
282
433
  while (!deadline.expired()) {
283
434
  await new Promise((resolvePromise) => { setTimeout(resolvePromise, Math.min(pollMs, Math.max(1, deadline.remainingMs()))); });
@@ -311,12 +462,17 @@ function acquireProcessLock(lockPath, state, purpose, details = {}, options = {}
311
462
  createExclusiveFileSync(lockPath, `${JSON.stringify(payload, null, 2)}
312
463
  `, { mode: 0o600 });
313
464
  ownerOnlyFile(lockPath);
314
- return {
465
+ const lock = {
315
466
  acquired: true,
316
467
  path: lockPath,
317
468
  owner: payload,
469
+ update(patch) {
470
+ lock.owner = updateProcessLock(lockPath, token, patch);
471
+ return lock.owner;
472
+ },
318
473
  release() { releaseProcessLock(lockPath, token); },
319
474
  };
475
+ return lock;
320
476
  } catch (error) {
321
477
  if (error?.code !== "EEXIST") throw error;
322
478
  const snapshot = readProcessLockSnapshot(lockPath);
@@ -345,26 +501,31 @@ function acquireProcessLock(lockPath, state, purpose, details = {}, options = {}
345
501
  }
346
502
 
347
503
  function readProcessLockSnapshot(lockPath) {
348
- let info;
349
- try { info = lstatSync(lockPath); } catch (error) {
350
- if (error?.code === "ENOENT") return null;
504
+ let opened;
505
+ try {
506
+ opened = retryTransientMultipleLinksSync(() => readBoundedRegularFileWithInfoSync(lockPath, MAX_LOCK_BYTES, "process lock", {
507
+ verifyPathIdentity: true,
508
+ rejectMultipleLinks: true,
509
+ }));
510
+ } catch (error) {
511
+ if (error?.code === "ENOENT" || error?.cause?.code === "ENOENT") return null;
351
512
  throw error;
352
513
  }
353
- if (info.isSymbolicLink()) throw new Error(`process lock must not be a symbolic link: ${lockPath}`);
354
- if (!info.isFile()) throw new Error(`process lock is not a regular file: ${lockPath}`);
514
+ const info = lockIdentity(opened.identityInfo, opened.identity);
515
+ let text;
516
+ try { text = new TextDecoder("utf-8", { fatal: true }).decode(opened.buffer); }
517
+ catch { return { owner: null, info }; }
355
518
  let owner = null;
356
519
  try {
357
- const parsed = JSON.parse(readBoundedUtf8(lockPath, MAX_LOCK_BYTES, "lock file"));
520
+ const parsed = JSON.parse(text);
358
521
  if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) owner = parsed;
359
- } catch (error) {
360
- if (error?.code === "ENOENT") return null;
361
- }
362
- return { owner, info: lockIdentity(info) };
522
+ } catch { /* Successfully read malformed JSON remains eligible for bounded stale recovery. */ }
523
+ return { owner, info };
363
524
  }
364
525
 
365
526
  function removeLockSnapshot(lockPath, snapshot) {
366
527
  let current;
367
- try { current = lstatSync(lockPath); } catch (error) { return error?.code === "ENOENT"; }
528
+ try { current = lstatSync(lockPath, { bigint: true }); } catch (error) { return error?.code === "ENOENT"; }
368
529
  if (current.isSymbolicLink() || !current.isFile()) return false;
369
530
  if (!sameLockIdentity(snapshot.info, lockIdentity(current))) return false;
370
531
  if (snapshot.owner?.token) {
@@ -379,17 +540,48 @@ function removeLockSnapshot(lockPath, snapshot) {
379
540
  }
380
541
  }
381
542
 
382
- function lockIdentity(info) {
543
+ function lockIdentity(info, identity = filesystemIdentity(info, "process lock")) {
383
544
  return {
384
- dev: Number(info.dev),
385
- ino: Number(info.ino),
386
- size: Number(info.size),
387
- mtimeMs: Number(info.mtimeMs),
545
+ ...identity,
546
+ size: exactFilesystemInteger(info.size, "process lock size"),
547
+ nlink: exactFilesystemInteger(info.nlink, "process lock link count"),
548
+ mtimeMs: filesystemTimeMs(info.mtimeMs, "process lock modification time"),
388
549
  };
389
550
  }
390
551
 
391
552
  function sameLockIdentity(left, right) {
392
- return left.dev === right.dev && left.ino === right.ino && left.size === right.size && left.mtimeMs === right.mtimeMs;
553
+ return sameFilesystemIdentity(left, right) && left.size === right.size && left.mtimeMs === right.mtimeMs;
554
+ }
555
+
556
+ function updateProcessLock(lockPath, token, patch) {
557
+ if (!isPlainRecord(patch)) throw new TypeError("process lock update must be a plain record");
558
+ const allowed = new Set(["startupReady", "startupReadyAt"]);
559
+ for (const key of Object.keys(patch)) {
560
+ if (!allowed.has(key)) throw new Error(`process lock field is immutable: ${key}`);
561
+ }
562
+ if (patch.startupReady !== true) {
563
+ throw new TypeError("process lock startup readiness can only be published as true");
564
+ }
565
+ if (typeof patch.startupReadyAt !== "string" || !Number.isFinite(Date.parse(patch.startupReadyAt))) {
566
+ throw new TypeError("process lock startupReadyAt update must be an ISO timestamp");
567
+ }
568
+ const snapshot = readProcessLockSnapshot(lockPath);
569
+ if (!snapshot?.owner || snapshot.owner.token !== token || snapshot.owner.pid !== process.pid) {
570
+ throw new Error("process lock ownership changed before update");
571
+ }
572
+ if (snapshot.owner.purpose !== "daemon") {
573
+ throw new Error("only daemon locks can publish startup readiness");
574
+ }
575
+ if (snapshot.owner.startupReady === true || snapshot.owner.startupReadyAt !== null) {
576
+ throw new Error("process lock startup readiness was already published");
577
+ }
578
+ const updated = { ...snapshot.owner, startupReady: true, startupReadyAt: patch.startupReadyAt };
579
+ const content = `${JSON.stringify(updated, null, 2)}
580
+ `;
581
+ if (Buffer.byteLength(content) > MAX_LOCK_BYTES) throw new Error("process lock update exceeds the size limit");
582
+ replaceFileAtomicallySync(lockPath, content, { mode: 0o600 });
583
+ ownerOnlyFile(lockPath);
584
+ return updated;
393
585
  }
394
586
 
395
587
  function releaseProcessLock(lockPath, token) {
@@ -400,12 +592,7 @@ function releaseProcessLock(lockPath, token) {
400
592
  }
401
593
 
402
594
  export function readDaemonLockOwner(lockPath) {
403
- try {
404
- const parsed = JSON.parse(readBoundedUtf8(lockPath, MAX_LOCK_BYTES, "lock file"));
405
- return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
406
- } catch {
407
- return null;
408
- }
595
+ return readProcessLockSnapshot(lockPath)?.owner || null;
409
596
  }
410
597
 
411
598
  function boundedPositiveInteger(value, fallback) {
@@ -413,17 +600,23 @@ function boundedPositiveInteger(value, fallback) {
413
600
  return Number.isFinite(parsed) ? Math.max(1, Math.floor(parsed)) : fallback;
414
601
  }
415
602
 
416
- function readBoundedUtf8(filePath, maxBytes, label) {
417
- const buffer = readBoundedRegularFileSync(filePath, maxBytes, label);
603
+ function readBoundedUtf8(filePath, maxBytes, label, options = {}) {
604
+ return decodeUtf8(readBoundedRegularFileSync(filePath, maxBytes, label, options), label);
605
+ }
606
+
607
+ function decodeUtf8(buffer, label) {
418
608
  try {
419
609
  return new TextDecoder("utf-8", { fatal: true }).decode(buffer);
420
- } catch {
421
- throw new Error(`${label} is not valid UTF-8`);
610
+ } catch (cause) {
611
+ throw Object.assign(new Error(`${label} is not valid UTF-8`, { cause }), { code: "ERR_INVALID_UTF8" });
422
612
  }
423
613
  }
424
614
 
425
- function readJsonObjectOrBackup(filePath) {
426
- const text = readBoundedUtf8(filePath, MAX_STATE_JSON_BYTES, "state JSON");
615
+ function readJsonObjectOrBackup(filePath, options = {}) {
616
+ const opened = readBoundedRegularFileWithInfoSync(filePath, MAX_STATE_JSON_BYTES, "state JSON", {
617
+ verifyPathIdentity: true, rejectMultipleLinks: true,
618
+ });
619
+ const text = decodeUtf8(opened.buffer, "state JSON");
427
620
  let parsed;
428
621
  try {
429
622
  parsed = JSON.parse(text);
@@ -431,15 +624,52 @@ function readJsonObjectOrBackup(filePath) {
431
624
  return parsed;
432
625
  } catch {
433
626
  const backupPath = `${filePath}.corrupt-${Date.now()}-${randomBytes(4).toString("hex")}`;
434
- replaceFileSync(filePath, backupPath);
627
+ preserveFileSnapshotSync(filePath, backupPath, opened.buffer, opened.identity, { label: "state JSON", mode: 0o600 });
435
628
  ownerOnlyFile(backupPath);
436
629
  pruneBackups(filePath, 3);
437
- const recovered = {};
438
- Object.defineProperty(recovered, CORRUPT_RECOVERY, { value: true });
439
- return recovered;
630
+ if (options.allowEmptyRecovery === true) {
631
+ const recovered = {};
632
+ Object.defineProperty(recovered, CORRUPT_RECOVERY, { value: true });
633
+ return recovered;
634
+ }
635
+ const recoveryPath = options.recoveryPath || recoveryMarkerPath(filePath);
636
+ const marker = { schemaVersion: RECOVERY_MARKER_SCHEMA, backup: path.basename(backupPath), detectedAt: new Date().toISOString() };
637
+ replaceFileAtomicallySync(recoveryPath, `${JSON.stringify(marker, null, 2)}\n`, { mode: 0o600 });
638
+ ownerOnlyFile(recoveryPath);
639
+ throw recoveryRequiredError(marker);
440
640
  }
441
641
  }
442
642
 
643
+ function recoveryMarkerPath(statePath) { return `${statePath}.recovery-required`; }
644
+
645
+ function readRecoveryMarker(markerPath, statePath) {
646
+ ownerOnlyFile(markerPath);
647
+ let opened;
648
+ let marker;
649
+ try {
650
+ opened = readBoundedRegularFileWithInfoSync(markerPath, MAX_MARKER_BYTES, "state recovery marker", {
651
+ verifyPathIdentity: true, rejectMultipleLinks: true,
652
+ });
653
+ marker = JSON.parse(decodeUtf8(opened.buffer, "state recovery marker"));
654
+ } catch { throw new Error("workspace state recovery marker is invalid; inspect the profile manually before continuing"); }
655
+ if (marker?.schemaVersion !== RECOVERY_MARKER_SCHEMA || typeof marker.backup !== "string"
656
+ || !marker.backup.startsWith(`${path.basename(statePath)}.corrupt-`) || path.basename(marker.backup) !== marker.backup
657
+ || !Number.isFinite(Date.parse(String(marker.detectedAt || "")))) {
658
+ throw new Error("workspace state recovery marker is invalid; inspect the profile manually before continuing");
659
+ }
660
+ return {
661
+ marker: Object.freeze({ schemaVersion: marker.schemaVersion, backup: marker.backup, detectedAt: marker.detectedAt }),
662
+ identity: opened.identity,
663
+ };
664
+ }
665
+
666
+ function recoveryRequiredError(marker) {
667
+ const backup = marker?.backup || "the bounded corrupt backup";
668
+ const error = new Error(`workspace state recovery is required; invalid JSON was preserved as ${backup}. Restore a valid current-schema state.json or intentionally remove this profile before restarting`);
669
+ error.code = "state_recovery_required";
670
+ return error;
671
+ }
672
+
443
673
  function ensureStateRoot(inputRoot) {
444
674
  const root = path.resolve(expandHome(inputRoot));
445
675
  if (existsSync(root)) {
@@ -473,6 +703,9 @@ function assertSafeStateRootForRemoval(root) {
473
703
  path.resolve(packageRoot),
474
704
  ]);
475
705
  if (forbidden.has(canonical)) throw new Error(`refusing to remove unsafe state root: ${canonical}`);
706
+ if (currentEntrypointInsideStateRoot(canonical)) {
707
+ throw new Error("refusing to remove a state root that contains the currently executing Machine Bridge CLI; run uninstall from an installation outside that state root");
708
+ }
476
709
  if (looksLikeSourceTree(canonical) || stateRootMatchesRecordedWorkspace(canonical)) {
477
710
  throw new Error(`refusing to remove state root that appears to be a workspace: ${canonical}`);
478
711
  }
@@ -480,6 +713,7 @@ function assertSafeStateRootForRemoval(root) {
480
713
  if (!hasOnlyStateEntries(entries)) {
481
714
  throw new Error(`refusing to remove state root containing unrelated entries: ${canonical}`);
482
715
  }
716
+ validateOwnedStateNamespaces(canonical);
483
717
  const marker = path.join(canonical, STATE_MARKER);
484
718
  if (existsSync(marker)) {
485
719
  assertValidStateMarker(marker);
@@ -489,7 +723,7 @@ function assertSafeStateRootForRemoval(root) {
489
723
  }
490
724
 
491
725
  function assertValidStateMarker(marker) {
492
- const content = readBoundedUtf8(marker, MAX_MARKER_BYTES, "state marker");
726
+ const content = readBoundedUtf8(marker, MAX_MARKER_BYTES, "state marker", { verifyPathIdentity: true, rejectMultipleLinks: true });
493
727
  let value;
494
728
  try { value = JSON.parse(content); } catch { throw new Error(`invalid state root marker: ${marker}`); }
495
729
  if (value?.app !== appName || value?.schema !== STATE_MARKER_SCHEMA) {
@@ -498,10 +732,26 @@ function assertValidStateMarker(marker) {
498
732
  }
499
733
 
500
734
  function hasOnlyStateEntries(entries) {
501
- const allowed = new Set([STATE_MARKER, "config.json", "browser-bridge.json", "maintenance.lock", "profiles", "logs", "service-environment.json", "service-launcher.cmd"]);
735
+ const allowed = new Set([STATE_MARKER, "config.json", "browser-bridge.json", "maintenance.lock", "profiles", "logs", "service-environment.json", "service-launcher.cmd", "toolchains", "release-channels", "release-tasks"]);
502
736
  return entries.every((entry) => allowed.has(entry) || /^config\.json\.corrupt-\d+(?:-[a-f0-9]{8})?$/.test(entry));
503
737
  }
504
738
 
739
+ function currentEntrypointInsideStateRoot(root) {
740
+ const entry = String(process.argv[1] || "").trim();
741
+ if (!entry) return false;
742
+ let canonicalRoot;
743
+ let resolved;
744
+ try {
745
+ canonicalRoot = canonicalizePotentialPath(root);
746
+ resolved = canonicalizePotentialPath(entry);
747
+ } catch {
748
+ canonicalRoot = path.resolve(root);
749
+ resolved = path.resolve(entry);
750
+ }
751
+ const relative = path.relative(canonicalRoot, resolved);
752
+ return Boolean(relative) && relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
753
+ }
754
+
505
755
  function looksLikeSourceTree(root) {
506
756
  return [".git", ".hg", ".svn", "package.json", "pyproject.toml", "Cargo.toml", "go.mod"].some((name) => existsSync(path.join(root, name)));
507
757
  }
@@ -511,13 +761,12 @@ function stateRootMatchesRecordedWorkspace(root) {
511
761
  if (typeof config?.selectedWorkspace === "string" && sameWorkspaceIdentity(config.selectedWorkspace, root)) return true;
512
762
 
513
763
  const profiles = path.join(root, "profiles");
514
- let entries;
515
- try {
516
- entries = readdirSync(profiles, { withFileTypes: true });
517
- } catch (error) {
518
- if (error?.code === "ENOENT") return false;
519
- throw new Error("could not inspect state-root profiles before removal", { cause: error });
764
+ const profilesInfo = inspectPathIfPresentSync(profiles, "state-root profiles");
765
+ if (!profilesInfo) return false;
766
+ if (profilesInfo.isSymbolicLink() || !profilesInfo.isDirectory()) {
767
+ throw new Error("state-root profiles must be a real directory before removal");
520
768
  }
769
+ const entries = readdirSync(profiles, { withFileTypes: true });
521
770
  for (const entry of entries) {
522
771
  if (!entry.isDirectory()) continue;
523
772
  const profileDir = path.join(profiles, entry.name);
@@ -532,7 +781,7 @@ function stateRootMatchesRecordedWorkspace(root) {
532
781
  function readOptionalRemovalJson(file, label) {
533
782
  let text;
534
783
  try {
535
- text = readBoundedUtf8(file, MAX_STATE_JSON_BYTES, label);
784
+ text = readBoundedUtf8(file, MAX_STATE_JSON_BYTES, label, { verifyPathIdentity: true, rejectMultipleLinks: true });
536
785
  } catch (error) {
537
786
  if (error?.code === "ENOENT" || error?.cause?.code === "ENOENT") return null;
538
787
  throw new Error(`${label} could not be verified before state removal`, { cause: error });
@@ -569,7 +818,7 @@ function cleanupStaleAtomicTemps(dir, baseName = "") {
569
818
  const file = path.join(dir, name);
570
819
  try {
571
820
  if (now - statSync(file).mtimeMs > 60 * 60 * 1000) unlinkSync(file);
572
- } catch {}
821
+ } catch { /* Stale-temp cleanup is opportunistic and must not block the authoritative state write. */ }
573
822
  }
574
823
  }
575
824
 
@@ -586,16 +835,18 @@ function pruneBackups(filePath, keep) {
586
835
  return;
587
836
  }
588
837
  for (const backup of backups.slice(keep)) {
589
- try { unlinkSync(backup.path); } catch {}
838
+ try { unlinkSync(backup.path); }
839
+ catch { /* Backup pruning is bounded housekeeping; retained backups are safer than failing state recovery. */ }
590
840
  }
591
841
  }
592
842
 
593
843
  export function ensureWorkerSecrets(state, options = {}) {
594
844
  state.worker ||= {};
845
+ delete state.worker.accountAdminSecret;
595
846
  const enrollingDeviceIdentity = !state.worker.deviceIdentity;
596
- if (!state.worker.accountAdminSecret || options.rotateSecrets) state.worker.accountAdminSecret = randomToken("account_admin");
597
- if (enrollingDeviceIdentity || options.rotateSecrets) state.worker.deviceIdentity = createDeviceIdentity();
598
- else validateDeviceIdentity(state.worker.deviceIdentity);
847
+ if (enrollingDeviceIdentity || (options.rotateSecrets && !options.deferDeviceRotation)) state.worker.deviceIdentity = createDeviceIdentity();
848
+ else validateDeviceRootIdentity(state.worker.deviceIdentity);
849
+ if (state.worker.pendingDeviceIdentity) validateDeviceRootIdentity(state.worker.pendingDeviceIdentity);
599
850
  delete state.worker.daemonSecret;
600
851
  if (!state.worker.oauthTokenVersion || enrollingDeviceIdentity || options.rotateSecrets) {
601
852
  state.worker.oauthTokenVersion = randomToken("token_version");
@@ -622,26 +873,53 @@ export function ensureWorkerSecrets(state, options = {}) {
622
873
  delete state.worker.updatedAt;
623
874
  }
624
875
 
625
- function defaultWorkerName(hash) {
626
- return `mbm-${String(hash || "default").slice(0, 12)}`;
876
+ export function deploymentDeviceIdentity(state) {
877
+ const identity = state?.worker?.pendingDeviceIdentity || state?.worker?.deviceIdentity;
878
+ return validateDeviceRootIdentity(identity);
627
879
  }
628
880
 
629
- function randomToken(prefix) {
630
- return `${prefix}_${randomBytes(32).toString("base64url")}`;
881
+ export function promotePendingDeviceIdentity(state) {
882
+ const pending = state?.worker?.pendingDeviceIdentity;
883
+ if (!pending) return false;
884
+ validateDeviceRootIdentity(pending);
885
+ const current = state.worker.deviceIdentity;
886
+ if (current) {
887
+ const previous = Array.isArray(state.worker.previousDeviceIdentities) ? state.worker.previousDeviceIdentities : [];
888
+ state.worker.previousDeviceIdentities = [...previous, publicDeviceRootRecord(current)].slice(-2);
889
+ }
890
+ state.worker.deviceIdentity = pending;
891
+ delete state.worker.pendingDeviceIdentity;
892
+ return true;
631
893
  }
632
894
 
633
- export function ensureOwnerOnlyDir(dir, options = {}) {
634
- return ensureOwnerOnlyDirectorySync(dir, options);
895
+ function publicDeviceRootRecord(identity) {
896
+ validateDeviceRootIdentity(identity);
897
+ return {
898
+ scheme: identity.scheme,
899
+ provider: identity.provider || "portable-jwk-v1",
900
+ brokerProtocol: identity.brokerProtocol || undefined,
901
+ brokerPath: identity.brokerPath || undefined,
902
+ brokerIdentifier: identity.brokerIdentifier || undefined,
903
+ brokerTeamIdentifier: identity.brokerTeamIdentifier || undefined,
904
+ keyTag: identity.keyTag || undefined,
905
+ publicJwk: identity.publicJwk,
906
+ keyId: identity.keyId,
907
+ createdAt: identity.createdAt,
908
+ retiredAt: new Date().toISOString(),
909
+ };
635
910
  }
636
911
 
637
- export function ownerOnlyFile(filePath) {
638
- chmodRegularFileSync(filePath, 0o600, "owner-only path");
912
+ function defaultWorkerName(hash) {
913
+ return `mbm-${String(hash || "default").slice(0, 12)}`;
914
+ }
915
+
916
+ function randomToken(prefix) {
917
+ return `${prefix}_${randomBytes(32).toString("base64url")}`;
639
918
  }
640
919
 
641
920
  export function redactState(state) {
642
921
  const clone = redactHomeInValue(JSON.parse(JSON.stringify(state)));
643
- if (clone.worker?.accountAdminSecret) clone.worker.accountAdminSecret = "<redacted>";
644
- if (clone.worker?.deviceIdentity?.privateJwk?.d) clone.worker.deviceIdentity.privateJwk.d = "<redacted>";
922
+ redactDeviceIdentityState(clone.worker);
645
923
  if (clone.worker?.oauthTokenVersion) clone.worker.oauthTokenVersion = "<redacted>";
646
924
  if (clone.resources && typeof clone.resources === "object") {
647
925
  for (const value of Object.values(clone.resources)) {
@@ -653,6 +931,22 @@ export function redactState(state) {
653
931
  return clone;
654
932
  }
655
933
 
934
+
935
+ function redactDeviceIdentityState(worker) {
936
+ if (!worker || typeof worker !== "object") return;
937
+ for (const identity of [
938
+ worker.deviceIdentity,
939
+ worker.pendingDeviceIdentity,
940
+ ...(Array.isArray(worker.previousDeviceIdentities) ? worker.previousDeviceIdentities : []),
941
+ ]) {
942
+ if (!identity || typeof identity !== "object") continue;
943
+ delete identity.privateJwk;
944
+ delete identity.publicJwk;
945
+ if (identity.brokerPath) identity.brokerPath = "<local-broker-path>";
946
+ if (identity.keyTag) identity.keyTag = "<local-key-tag>";
947
+ }
948
+ }
949
+
656
950
  function redactHomeInValue(value) {
657
951
  const home = os.homedir();
658
952
  if (typeof value === "string") {
@@ -665,7 +959,3 @@ function redactHomeInValue(value) {
665
959
  }
666
960
  return value;
667
961
  }
668
-
669
- export function previewSecret(_value) {
670
- return "<redacted>";
671
- }