@fased/fased 0.1.61 → 0.1.64

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 (514) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/README.md +38 -30
  3. package/config/dependency-ownership.json +1 -1
  4. package/config/signer-policies/README.md +49 -0
  5. package/config/signer-policies/agent.json.template +32 -0
  6. package/config/signer-policies/mining.json.template +69 -0
  7. package/config/signer-policies/network.json.template +5 -0
  8. package/config/signer-policies/vault.json.template +70 -0
  9. package/config/signer-protocol-v2.json +76 -0
  10. package/config/typescript-noemit-baseline.json +680 -0
  11. package/dist/acp/client.js +2 -2
  12. package/dist/acp/secret-file.js +2 -2
  13. package/dist/agents/agent-scope.js +2 -2
  14. package/dist/agents/apply-patch.js +8 -8
  15. package/dist/agents/auth-profiles/paths.js +2 -2
  16. package/dist/agents/auth-profiles/store.js +269 -33
  17. package/dist/agents/bash-tools.exec.js +10 -4
  18. package/dist/agents/bash-tools.shared.js +2 -2
  19. package/dist/agents/cli-credentials.js +4 -4
  20. package/dist/agents/current-model-catalog.js +3 -3
  21. package/dist/agents/docs-path.js +3 -3
  22. package/dist/agents/embedded-pi-mcp.js +4 -4
  23. package/dist/agents/identity-avatar.js +3 -3
  24. package/dist/agents/identity-file.js +2 -2
  25. package/dist/agents/memory-search.js +7 -1
  26. package/dist/agents/model-catalog-access.js +1 -2
  27. package/dist/agents/model-catalog-snapshot.js +1 -1
  28. package/dist/agents/model-catalog-source.js +1 -1
  29. package/dist/agents/model-metadata.js +1 -1
  30. package/dist/agents/model-selection.js +2 -28
  31. package/dist/agents/models-config.providers.js +6 -6
  32. package/dist/agents/openai-codex-app-server.js +5 -5
  33. package/dist/agents/pi-embedded-runner/compact.js +15 -7
  34. package/dist/agents/pi-embedded-runner/model.js +1 -1
  35. package/dist/agents/pi-embedded-runner/run/attempt.js +24 -13
  36. package/dist/agents/pi-embedded-runner/run.js +2 -0
  37. package/dist/agents/pi-embedded-runner/tool-split.js +40 -2
  38. package/dist/agents/pi-embedded-subscribe.raw-stream.js +3 -3
  39. package/dist/agents/pi-extensions/compaction-safeguard.js +3 -3
  40. package/dist/agents/pi-model-discovery.js +6 -6
  41. package/dist/agents/pi-project-settings.js +3 -3
  42. package/dist/agents/pi-tool-definition-adapter.js +16 -1
  43. package/dist/agents/pi-tools.js +2 -1
  44. package/dist/agents/sandbox/config.js +1 -1
  45. package/dist/agents/sandbox/context.js +27 -2
  46. package/dist/agents/sandbox/fs-bridge.js +2 -2
  47. package/dist/agents/session-dirs.js +4 -4
  48. package/dist/agents/session-write-lock.js +13 -13
  49. package/dist/agents/shell-utils.js +5 -5
  50. package/dist/agents/skills/bundled-dir.js +4 -4
  51. package/dist/agents/skills/env-overrides.js +38 -28
  52. package/dist/agents/skills/local-loader.js +6 -6
  53. package/dist/agents/skills/plugin-skills.js +2 -2
  54. package/dist/agents/skills/trust.js +93 -0
  55. package/dist/agents/skills/workspace.js +141 -13
  56. package/dist/agents/skills-clawhub.js +36 -11
  57. package/dist/agents/skills-install-download.js +7 -7
  58. package/dist/agents/skills-install-plan.js +2 -2
  59. package/dist/agents/skills-install.js +2 -2
  60. package/dist/agents/skills-marketplace-scan.js +49 -0
  61. package/dist/agents/skills-status.js +2 -2
  62. package/dist/agents/subagent-depth.js +2 -2
  63. package/dist/agents/system-prompt-params.js +2 -2
  64. package/dist/agents/tools/marketplace-offer-draft-tool.js +1 -1
  65. package/dist/agents/tools/mining-tool.js +1 -1
  66. package/dist/agents/tools/session-status-tool.js +1 -1
  67. package/dist/agents/tools/wallet-action-tool.js +172 -36
  68. package/dist/agents/tools/wallet-skill-policy.js +65 -23
  69. package/dist/agents/tools/wallet-tool.js +7 -11
  70. package/dist/agents/workspace.js +22 -22
  71. package/dist/auto-reply/reply/agent-runner-execution.js +2 -2
  72. package/dist/auto-reply/reply/agent-runner.js +2 -2
  73. package/dist/auto-reply/reply/commands-export-session.js +7 -7
  74. package/dist/auto-reply/reply/directive-handling.fast-lane.js +2 -1
  75. package/dist/auto-reply/reply/directive-handling.impl.js +2 -1
  76. package/dist/auto-reply/reply/directive-handling.model.js +2 -1
  77. package/dist/auto-reply/reply/directive-handling.persist.js +2 -2
  78. package/dist/auto-reply/reply/get-reply-directives-apply.js +2 -0
  79. package/dist/auto-reply/reply/get-reply-inline-actions.js +25 -1
  80. package/dist/auto-reply/reply/model-selection.js +7 -4
  81. package/dist/auto-reply/reply/post-compaction-audit.js +3 -3
  82. package/dist/auto-reply/reply/post-compaction-context.js +3 -3
  83. package/dist/auto-reply/reply/session-reset-model.js +4 -4
  84. package/dist/auto-reply/reply/session.js +3 -3
  85. package/dist/auto-reply/skill-commands.js +3 -3
  86. package/dist/auto-reply/status.js +6 -6
  87. package/dist/brand.js +1 -1
  88. package/dist/browser/chrome.executables.js +3 -3
  89. package/dist/browser/chrome.js +3 -3
  90. package/dist/browser/chrome.profile-decoration.js +6 -6
  91. package/dist/browser/profiles-service.js +2 -2
  92. package/dist/browser/server-context.js +2 -2
  93. package/dist/browser/trash.js +4 -4
  94. package/dist/build-info.json +3 -3
  95. package/dist/canvas-host/a2ui/.bundle.hash +1 -1
  96. package/dist/canvas-host/server.js +4 -4
  97. package/dist/capabilities/catalog.js +3 -3
  98. package/dist/channel-web.js +2 -2
  99. package/dist/channels/plugins/catalog.js +3 -3
  100. package/dist/cli/browser-cli-extension.js +6 -6
  101. package/dist/cli/command-format.js +2 -2
  102. package/dist/cli/cron-cli/register.cron-simple.js +2 -2
  103. package/dist/cli/dns-cli.js +9 -9
  104. package/dist/cli/gateway-cli/dev.js +5 -5
  105. package/dist/cli/gateway-cli/run.js +10 -2
  106. package/dist/cli/hooks-cli.js +5 -5
  107. package/dist/cli/lightweight/gateway-runtime-probe.js +2 -2
  108. package/dist/cli/memory-cli.js +7 -7
  109. package/dist/cli/nodes-media-utils.js +2 -2
  110. package/dist/cli/plugins-cli.js +2 -2
  111. package/dist/cli/program/command-registry.js +1 -1
  112. package/dist/cli/program/register.onboard.js +2 -3
  113. package/dist/cli/program/register.wallet.js +16 -123
  114. package/dist/cli/providers-cli.js +1 -1
  115. package/dist/cli/sat-cli.js +129 -2
  116. package/dist/cli/secrets-cli.js +3 -3
  117. package/dist/cli/update-cli/update-command.js +260 -11
  118. package/dist/cli/windows-argv.js +2 -2
  119. package/dist/commands/auth-choice.apply.api-providers.js +3 -3
  120. package/dist/commands/dashboard-link.js +3 -3
  121. package/dist/commands/doctor-config-flow.js +1 -1
  122. package/dist/commands/doctor-install.js +5 -5
  123. package/dist/commands/doctor-legacy-config.js +2 -13
  124. package/dist/commands/doctor-memory-search.js +2 -2
  125. package/dist/commands/doctor-platform-notes.js +2 -2
  126. package/dist/commands/doctor-sandbox.js +2 -2
  127. package/dist/commands/doctor-state-integrity.js +23 -23
  128. package/dist/commands/doctor-workspace.js +3 -3
  129. package/dist/commands/doctor.js +3 -3
  130. package/dist/commands/managed-up.js +2 -2
  131. package/dist/commands/model-picker.js +6 -6
  132. package/dist/commands/models/auth.js +1 -1
  133. package/dist/commands/onboard-auth.config-gateways.js +1 -1
  134. package/dist/commands/onboard-auth.credentials.js +35 -11
  135. package/dist/commands/onboard-non-interactive/local/wallet-config.js +28 -27
  136. package/dist/commands/onboard-non-interactive/local.js +5 -1
  137. package/dist/commands/onboarding/plugin-install.js +2 -2
  138. package/dist/commands/sessions-cleanup.js +2 -2
  139. package/dist/commands/signal-install.js +6 -6
  140. package/dist/commands/status-all/channels.js +2 -2
  141. package/dist/commands/wallet.js +570 -1913
  142. package/dist/config/cache-utils.js +2 -2
  143. package/dist/config/config.js +2 -2
  144. package/dist/config/includes.js +5 -5
  145. package/dist/config/io.js +66 -4
  146. package/dist/config/paths.js +3 -3
  147. package/dist/config/sat-runtime-ids.js +3 -3
  148. package/dist/config/schema.help.js +8 -7
  149. package/dist/config/schema.labels.js +5 -4
  150. package/dist/config/sessions/disk-budget.js +6 -6
  151. package/dist/config/sessions/paths.js +2 -2
  152. package/dist/config/sessions/store.js +17 -17
  153. package/dist/config/sessions/transcript.js +4 -4
  154. package/dist/config/talk.js +2 -2
  155. package/dist/config/zod-schema.agent-runtime.js +1 -0
  156. package/dist/config/zod-schema.js +5 -0
  157. package/dist/control-ui/assets/{app-BXA4gPvN.js → app-bR-fO5ty.js} +1511 -1555
  158. package/dist/control-ui/assets/{config-CU5Zi8Dh.js → config-LeImukCW.js} +10 -10
  159. package/dist/control-ui/assets/{federation-D10Q4eP_.js → federation-DgA9e-Pn.js} +364 -422
  160. package/dist/control-ui/assets/{index-D2QbJhVg.js → index-ckCxHwjy.js} +3 -3
  161. package/dist/control-ui/assets/{mining-BdhMn18r.js → mining-BgHFh0ei.js} +134 -114
  162. package/dist/control-ui/assets/mining-commit-DFpB_4JN.js +2 -0
  163. package/dist/control-ui/assets/{open-external-url-DwSiwMPl.js → open-external-url-ulF1B-sA.js} +3 -3
  164. package/dist/control-ui/assets/{providers-QMn4w2_f.js → providers-CIx9fxc6.js} +2 -2
  165. package/dist/control-ui/assets/{wallet-C2g294QT.js → wallet-DvStR_AT.js} +259 -377
  166. package/dist/control-ui/index.html +1 -1
  167. package/dist/control-ui/version.json +1 -1
  168. package/dist/cron/service/store.js +2 -2
  169. package/dist/cron/store.js +6 -6
  170. package/dist/daemon/systemd-system.js +60 -54
  171. package/dist/discord/monitor/thread-bindings.state.js +2 -2
  172. package/dist/extensions/sat-mining/src/protocol-contract.js +43 -3
  173. package/dist/extensions/sat-mining/src/rpc-read.js +235 -67
  174. package/dist/extensions/sat-mining/src/sat-bond-layout.js +14 -11
  175. package/dist/extensions/sat-mining/src/signer-codec-manifest.js +406 -0
  176. package/dist/extensions/sat-mining/src/solana-submit.js +1038 -397
  177. package/dist/extensions/sat-mining/src/submission-ledger.js +349 -0
  178. package/dist/federation/auto-connect.js +98 -8
  179. package/dist/federation/marketplace-capability-adapter.js +33 -7
  180. package/dist/federation/marketplace-delivery-outbox.js +196 -0
  181. package/dist/federation/marketplace-delivery.js +351 -215
  182. package/dist/federation/marketplace-direct-payment.js +272 -141
  183. package/dist/federation/marketplace-escrow.js +607 -249
  184. package/dist/federation/marketplace-settlement-ledger.js +352 -0
  185. package/dist/federation/offers.js +9 -7
  186. package/dist/federation/peer-auth-v2.js +529 -0
  187. package/dist/gateway/a2a-http.js +425 -96
  188. package/dist/gateway/a2a-settlement.js +259 -153
  189. package/dist/gateway/a2a-task-store.js +524 -0
  190. package/dist/gateway/auth.js +38 -2
  191. package/dist/gateway/config-reload.js +46 -33
  192. package/dist/gateway/control-ui-boot-check.js +6 -6
  193. package/dist/gateway/control-ui-login.js +7 -7
  194. package/dist/gateway/control-ui.js +9 -9
  195. package/dist/gateway/durable-capacity.js +31 -0
  196. package/dist/gateway/federation-http.js +540 -135
  197. package/dist/gateway/federation-marketplace-run-store.js +397 -0
  198. package/dist/gateway/federation-marketplace.js +535 -230
  199. package/dist/gateway/hooks-mapping.js +4 -4
  200. package/dist/gateway/mutating-admin-rpc-rate-limit.js +5 -0
  201. package/dist/gateway/security-path.js +1 -1
  202. package/dist/gateway/server-discovery.js +2 -2
  203. package/dist/gateway/server-http.js +1088 -1209
  204. package/dist/gateway/server-methods/chat.js +3 -3
  205. package/dist/gateway/server-methods/models.js +1 -1
  206. package/dist/gateway/server-methods/secrets.js +40 -0
  207. package/dist/gateway/server-methods/sessions.js +6 -6
  208. package/dist/gateway/server-methods/usage.js +2 -2
  209. package/dist/gateway/server-methods.js +1 -2
  210. package/dist/gateway/server-reload-handlers.js +196 -66
  211. package/dist/gateway/server-runtime-config.js +1 -1
  212. package/dist/gateway/server-secrets-runtime.js +288 -0
  213. package/dist/gateway/server.impl.js +73 -42
  214. package/dist/gateway/session-compaction-checkpoints.js +4 -4
  215. package/dist/gateway/session-utils.fs.js +24 -24
  216. package/dist/gateway/session-utils.js +6 -6
  217. package/dist/gateway/sessions-patch.js +3 -5
  218. package/dist/hooks/bundled-dir.js +4 -4
  219. package/dist/hooks/gmail-setup-utils.js +6 -6
  220. package/dist/hooks/import-url.js +2 -2
  221. package/dist/hooks/loader.js +4 -4
  222. package/dist/hooks/workspace.js +7 -7
  223. package/dist/index.js +4 -2
  224. package/dist/infra/archive.js +17 -17
  225. package/dist/infra/boundary-file-read.js +2 -2
  226. package/dist/infra/boundary-path.js +15 -15
  227. package/dist/infra/brew.js +2 -2
  228. package/dist/infra/control-ui-assets.js +14 -14
  229. package/dist/infra/device-auth-store.js +8 -8
  230. package/dist/infra/device-identity.js +8 -8
  231. package/dist/infra/dotenv.js +2 -2
  232. package/dist/infra/exec-approvals.js +8 -8
  233. package/dist/infra/exec-command-resolution.js +4 -4
  234. package/dist/infra/exec-safe-bin-trust.js +2 -2
  235. package/dist/infra/fased-root.js +5 -5
  236. package/dist/infra/fasedhub.js +64 -1
  237. package/dist/infra/fs-safe.js +14 -14
  238. package/dist/infra/gateway-lock.js +11 -11
  239. package/dist/infra/git-commit.js +5 -5
  240. package/dist/infra/git-root.js +4 -4
  241. package/dist/infra/hosted-runtime-artifact.js +3 -3
  242. package/dist/infra/is-main.js +2 -2
  243. package/dist/infra/json-file.js +7 -7
  244. package/dist/infra/local-source-paired-update.js +538 -0
  245. package/dist/infra/managed-runtime-bootstrap.js +6 -6
  246. package/dist/infra/outbound/delivery-queue.js +16 -16
  247. package/dist/infra/path-env.js +3 -3
  248. package/dist/infra/ports-lsof.js +4 -4
  249. package/dist/infra/provider-usage.auth.js +3 -3
  250. package/dist/infra/restart.js +41 -10
  251. package/dist/infra/safe-open-sync.js +2 -2
  252. package/dist/infra/session-cost-usage.js +9 -9
  253. package/dist/infra/shell-env.js +2 -2
  254. package/dist/infra/state-migrations.fs.js +6 -6
  255. package/dist/infra/state-migrations.js +23 -23
  256. package/dist/infra/tmp-fased-dir.js +6 -6
  257. package/dist/infra/update-runner.js +96 -0
  258. package/dist/infra/widearea-dns.js +3 -3
  259. package/dist/infra/wsl.js +2 -2
  260. package/dist/light-update-status.js +11 -11
  261. package/dist/line/accounts.js +2 -2
  262. package/dist/line/download.js +2 -2
  263. package/dist/logging/config.js +3 -3
  264. package/dist/logging/logger.js +7 -7
  265. package/dist/managed/federation.js +3 -3
  266. package/dist/managed/tunnel.js +3 -3
  267. package/dist/media/store.js +13 -13
  268. package/dist/media-understanding/runner.js +3 -3
  269. package/dist/memory/backend-config.js +2 -1
  270. package/dist/memory/embeddings.js +2 -2
  271. package/dist/memory/internal.js +10 -10
  272. package/dist/memory/manager-sync-ops.js +7 -7
  273. package/dist/memory/repair-executor.js +17 -17
  274. package/dist/memory/session-files.js +16 -2
  275. package/dist/node-host/invoke-system-run-plan.js +8 -8
  276. package/dist/node-host/invoke.js +2 -2
  277. package/dist/node-host/runner.js +3 -3
  278. package/dist/pairing/pairing-store.js +3 -3
  279. package/dist/plugin-sdk/{accounts-DxblobYM.js → accounts-Brn3-uWN.js} +1 -1
  280. package/dist/plugin-sdk/{actions-D4UNimWa.js → actions-DRu135h0.js} +2 -2
  281. package/dist/plugin-sdk/{active-listener-CFuy106s.js → active-listener-B_NMtgPL.js} +1 -1
  282. package/dist/plugin-sdk/{audio-preflight-Bv0hUOWL.js → audio-preflight-BYxAt775.js} +27 -27
  283. package/dist/plugin-sdk/{audit-Cs0K7LBp.js → audit-C5d2z5U1.js} +1 -1
  284. package/dist/plugin-sdk/{channel-access-D5aHoTY8.js → channel-access-CcFRdRPO.js} +1 -1
  285. package/dist/plugin-sdk/{channel-access-configure-Dvch9mMQ.js → channel-access-configure-DaI5wevh.js} +1 -1
  286. package/dist/plugin-sdk/channel-plugin-common.d.ts +3 -3
  287. package/dist/plugin-sdk/channel-plugin-common.js +4 -4
  288. package/dist/plugin-sdk/{chunk-B0FUGc5A.js → chunk-CyoyqpbG.js} +1 -1
  289. package/dist/plugin-sdk/{chunk-items-Bk--NU3E.js → chunk-items-DIRHG2gJ.js} +1 -1
  290. package/dist/plugin-sdk/command-status.d.ts +2 -2
  291. package/dist/plugin-sdk/command-status.js +27 -27
  292. package/dist/plugin-sdk/{commands-registry-DmDRhRAU.js → commands-registry-BfqdEWdF.js} +2 -2
  293. package/dist/plugin-sdk/{common-CfUOowOp.js → common-HCcXU-NR.js} +1 -1
  294. package/dist/plugin-sdk/{config-mxULv0fQ.js → config-DlWVeLFB.js} +7113 -6877
  295. package/dist/plugin-sdk/{config-schema-CGW6J3Pv.js → config-schema-BOT7kHb6.js} +6779 -3696
  296. package/dist/plugin-sdk/device-pair.d.ts +1 -1
  297. package/dist/plugin-sdk/discord.d.ts +5 -5
  298. package/dist/plugin-sdk/discord.js +19 -19
  299. package/dist/plugin-sdk/file-lock.js +35 -4
  300. package/dist/plugin-sdk/{filesystem-manager-ZoHU8oA7.js → filesystem-manager-DoI8IBNF.js} +1 -1
  301. package/dist/plugin-sdk/{group-mentions-BHRff6j8.d.ts → group-mentions-q3hUTCt7.d.ts} +1 -1
  302. package/dist/plugin-sdk/{handle-action-DbVCF9HZ.js → handle-action-hvwapMKg.js} +13 -13
  303. package/dist/plugin-sdk/{helpers-BXbdgs5H.js → helpers-DMBU4oTo.js} +1 -1
  304. package/dist/plugin-sdk/{image-ops-D7jPxaph.js → image-ops-DQi0n_Tt.js} +1 -1
  305. package/dist/plugin-sdk/index.d.ts +9 -27
  306. package/dist/plugin-sdk/index.js +29 -29
  307. package/dist/plugin-sdk/json-store.js +6 -6
  308. package/dist/plugin-sdk/{login-BRxww6nv.js → login-Ca583Okc.js} +3 -3
  309. package/dist/plugin-sdk/{login-qr-DJXsk9XH.js → login-qr-CGciBSNP.js} +3 -3
  310. package/dist/plugin-sdk/{manager-Y2S_5IlI.js → manager-BegWxNYX.js} +4 -4
  311. package/dist/plugin-sdk/{markdown-tables-DHLFQqaF.js → markdown-tables-DCOXEUfZ.js} +1 -1
  312. package/dist/plugin-sdk/{media-Czo4carg.js → media-CAdQF5TG.js} +47 -47
  313. package/dist/plugin-sdk/{memory-search-DTjIMw8J.js → memory-search-CrFPOeTH.js} +4 -1
  314. package/dist/plugin-sdk/{monitor-B9Cmt_Je.js → monitor-BCFDaqJ-.js} +31 -31
  315. package/dist/plugin-sdk/{monitor-B71iwAgC.js → monitor-BgbjRiPM.js} +32 -32
  316. package/dist/plugin-sdk/{openresponses-http-Du4GMmwo.js → openresponses-http-DFf87SMM.js} +27 -27
  317. package/dist/plugin-sdk/{optional-runtime-dependency-DdMbxS24.js → optional-runtime-dependency-0dznm3ZO.js} +1 -1
  318. package/dist/plugin-sdk/{outbound-Db8mRN6P.js → outbound-BgVDdHxx.js} +6 -6
  319. package/dist/plugin-sdk/{outbound-attachment-BqFjcypb.js → outbound-attachment-QqTnIhZL.js} +2 -2
  320. package/dist/plugin-sdk/{pairing-message-BMkkS038.d.ts → pairing-message-CS-9BHug.d.ts} +2 -2
  321. package/dist/plugin-sdk/{pairing-message-B3GC_7CD.js → pairing-message-D0CoLHRJ.js} +1 -1
  322. package/dist/plugin-sdk/{pairing-store-CQikgIbw.js → pairing-store-BChahgI2.js} +2 -2
  323. package/dist/plugin-sdk/{pi-model-discovery-CY3m5zJP.js → pi-model-discovery-BX_JLcEJ.js} +1 -1
  324. package/dist/plugin-sdk/{pi-model-discovery-runtime-DSh1T2pn.js → pi-model-discovery-runtime-DxTMz_XF.js} +2 -2
  325. package/dist/plugin-sdk/provider-web-search-config-contract.d.ts +2 -2
  326. package/dist/plugin-sdk/{pw-ai-AFUqn-tQ.js → pw-ai-natjh3rm.js} +3 -3
  327. package/dist/plugin-sdk/{qmd-manager-DSyx77KY.js → qmd-manager-CgRaTxUq.js} +1 -1
  328. package/dist/plugin-sdk/{query-expansion-CAI2kKLQ.js → query-expansion-BG3FgzF7.js} +14 -3
  329. package/dist/plugin-sdk/{replies-Fzh9PhXl.js → replies-ChYo5ZQM.js} +2 -2
  330. package/dist/plugin-sdk/{resolve-outbound-target-KpF3co6T.js → resolve-outbound-target-D-KGgHJZ.js} +1 -1
  331. package/dist/plugin-sdk/{resolve-route-CDl74O19.js → resolve-route-C8tvQm8Z.js} +1 -1
  332. package/dist/plugin-sdk/{retry-policy-D4uiqUOG.js → retry-policy-C6SjKjQA.js} +1 -1
  333. package/dist/plugin-sdk/{runtime-Bb6KRnCC.js → runtime-2B9_WKoI.js} +8 -3
  334. package/dist/plugin-sdk/{runtime-dependency-DLDmB94f.js → runtime-dependency-DczkvPxn.js} +2 -2
  335. package/dist/plugin-sdk/sat-runtime.d.ts +549 -309
  336. package/dist/plugin-sdk/sat-runtime.js +29 -29
  337. package/dist/plugin-sdk/{send-C4k4JuJN.js → send-B6KkGKZD.js} +6 -6
  338. package/dist/plugin-sdk/{send-D-uDKGIq.js → send-CFzGKJXo.js} +6 -6
  339. package/dist/plugin-sdk/{send-Du8ykP1k.js → send-DCO441U7.js} +8 -8
  340. package/dist/plugin-sdk/{send-5nFktR1n.js → send-gDoGA-7e.js} +5 -5
  341. package/dist/plugin-sdk/{send-Bfzf11VY.js → send-q9HXrSwK.js} +7 -7
  342. package/dist/plugin-sdk/{session-CS-Td9EM.js → session-DHD4ZKyG.js} +2 -2
  343. package/dist/plugin-sdk/{skill-commands-CFAlXWHk.js → skill-commands-FKEPLS7T.js} +201 -26
  344. package/dist/plugin-sdk/{slack-D_bbE6Yd.js → slack-BUTt8cxK.js} +39 -39
  345. package/dist/plugin-sdk/{slack-actions-BcMkNv1Z.js → slack-actions-DhbMcp8M.js} +11 -11
  346. package/dist/plugin-sdk/slack.d.ts +5 -5
  347. package/dist/plugin-sdk/slack.js +9 -9
  348. package/dist/plugin-sdk/{sticker-cache-Dqo9nRhi.js → sticker-cache-D0wHVqjt.js} +3 -3
  349. package/dist/plugin-sdk/{tables-bFXyRpkx.js → tables-B3jr0Pvq.js} +1 -1
  350. package/dist/plugin-sdk/{targets-DH7a9lKB.js → targets-CtSbTxYX.js} +1 -1
  351. package/dist/plugin-sdk/{telegram-actions-Cnag7JTH.js → telegram-actions-CC_qN4v0.js} +30 -30
  352. package/dist/plugin-sdk/telegram.d.ts +5 -5
  353. package/dist/plugin-sdk/telegram.js +8 -8
  354. package/dist/plugin-sdk/{thread-bindings.lifecycle-CgOvvdw8.js → thread-bindings.lifecycle-QbkNl-m6.js} +3 -3
  355. package/dist/plugin-sdk/{thread-bindings.state-CQpk44Hq.js → thread-bindings.state-D9hg_M1c.js} +2 -2
  356. package/dist/plugin-sdk/tmp-fased-dir-xOTGlzBE.d.ts +486 -0
  357. package/dist/plugin-sdk/{types-CLIvQaVg.d.ts → types-C3avNytw.d.ts} +3 -4
  358. package/dist/plugin-sdk/{types-DyhXnYUr.d.ts → types-D0kDG8l0.d.ts} +3 -2
  359. package/dist/plugin-sdk/{types-BYDL6dQj.d.ts → types-DWfbSAaM.d.ts} +2 -2
  360. package/dist/plugin-sdk/{types.plugin-_fbu40Re.d.ts → types.plugin-BcSBq42x.d.ts} +8 -3
  361. package/dist/plugin-sdk/{web-BInKQkc4.js → web-BXsJ1vqe.js} +32 -32
  362. package/dist/plugin-sdk/{whatsapp-actions-BtSc2CBv.js → whatsapp-actions-Bj_rOncr.js} +13 -13
  363. package/dist/plugin-sdk/{whatsapp-heartbeat-Bg9TuYzl.js → whatsapp-heartbeat-B1WHkJIg.js} +2 -2
  364. package/dist/plugin-sdk/whatsapp.d.ts +5 -5
  365. package/dist/plugin-sdk/whatsapp.js +12 -12
  366. package/dist/plugin-sdk/{wsl-C4DGFkOd.js → wsl-J-HscWxS.js} +3 -3
  367. package/dist/plugins/bundled-dir.js +3 -3
  368. package/dist/plugins/discovery.js +11 -11
  369. package/dist/plugins/loader.js +13 -13
  370. package/dist/plugins/manifest-registry.js +2 -2
  371. package/dist/plugins/manifest.js +4 -4
  372. package/dist/plugins/path-safety.js +3 -3
  373. package/dist/plugins/status-cache.js +9 -9
  374. package/dist/plugins/update.js +3 -3
  375. package/dist/plugins/web-search-install-catalog.js +3 -3
  376. package/dist/providers/refresh.js +1 -1
  377. package/dist/providers/registry.js +1 -1
  378. package/dist/secrets/apply.js +17 -17
  379. package/dist/secrets/audit.js +11 -11
  380. package/dist/secrets/runtime.js +1 -1
  381. package/dist/secrets/shared.js +9 -9
  382. package/dist/security/scan-paths.js +2 -2
  383. package/dist/shared/config-eval.js +2 -2
  384. package/dist/shared/pid-alive.js +2 -2
  385. package/dist/tasks/standing-orders.js +5 -5
  386. package/dist/tasks/task-flow-registry.js +5 -5
  387. package/dist/tasks/task-registry.js +5 -5
  388. package/dist/tasks/workflow-definitions.js +6 -6
  389. package/dist/telegram/token.js +5 -5
  390. package/dist/utils.js +5 -5
  391. package/dist/wallet/external-submission-ledger.js +376 -0
  392. package/dist/wallet/legacy-embedded-keystore.js +40 -0
  393. package/dist/wallet/local-socket-signer-archive.js +30 -0
  394. package/dist/wallet/local-socket-signer-lifecycle.js +97 -0
  395. package/dist/wallet/local-socket-signer-policy.js +166 -0
  396. package/dist/wallet/local-socket-signer-protocol.js +561 -118
  397. package/dist/wallet/native-signer-wallet-id.js +25 -0
  398. package/dist/wallet/providers/alchemy-adapter.js +3 -0
  399. package/dist/wallet/providers/local-socket-signer-adapter.js +382 -107
  400. package/dist/wallet/providers/turnkey-adapter.js +292 -34
  401. package/dist/wallet/providers/wallet-standard-adapter.js +100 -0
  402. package/dist/wallet/signer-network-admin.js +184 -0
  403. package/dist/wallet/signer-protocol-v2.generated.js +90 -0
  404. package/dist/wallet/solana-bond-signing.js +157 -69
  405. package/dist/wallet/solana-reviewed-transaction.js +375 -0
  406. package/dist/wallet/solana-swap.js +520 -64
  407. package/dist/wallet/solana-transaction-inspection.js +106 -4
  408. package/dist/wallet/solana-trigger.js +658 -320
  409. package/dist/wallet/turnkey-reviewed-state.js +370 -0
  410. package/dist/wallet/wallet-approval-auth.js +20 -31
  411. package/dist/wallet/wallet-atomic-state.js +53 -0
  412. package/dist/wallet/wallet-audit-log.js +5 -5
  413. package/dist/wallet/wallet-canary.js +0 -1
  414. package/dist/wallet/wallet-inbound-events.js +19 -14
  415. package/dist/wallet/wallet-observability.js +5 -5
  416. package/dist/wallet/wallet-policy-simulation.js +1 -1
  417. package/dist/wallet/wallet-policy.js +95 -41
  418. package/dist/wallet/wallet-provider-capabilities.js +28 -4
  419. package/dist/wallet/wallet-provider-registry.js +64 -51
  420. package/dist/wallet/wallet-provider-resolver.js +62 -63
  421. package/dist/wallet/wallet-runtime-config.js +31 -44
  422. package/dist/wallet/wallet-secrets-store.js +44 -22
  423. package/dist/wallet/wallet-send-approvals.js +1894 -592
  424. package/dist/wallet/wallet-send-execution-ledger.js +292 -0
  425. package/dist/wallet/wallet-settlement-links.js +17 -16
  426. package/dist/wallet/wallet-standard-review.js +353 -0
  427. package/dist/wallet/wallet-status.js +0 -9
  428. package/dist/wallet/wallet-task-ledger.js +16 -2
  429. package/dist/web/accounts.js +3 -3
  430. package/dist/web/auth-store.js +18 -18
  431. package/dist/web/auto-reply/heartbeat-runner.js +1 -1
  432. package/dist/web/auto-reply/monitor.js +2 -2
  433. package/dist/web/session.js +4 -4
  434. package/dist/wizard/host-security-capability.js +15 -24
  435. package/dist/wizard/onboarding.finalize.js +63 -184
  436. package/dist/wizard/onboarding.host-security.js +118 -1368
  437. package/dist/wizard/onboarding.js +180 -242
  438. package/dist/wizard/onboarding.wallet.js +323 -829
  439. package/extensions/acpx/package.json +1 -1
  440. package/extensions/bluebubbles/package.json +1 -1
  441. package/extensions/copilot-proxy/package.json +1 -1
  442. package/extensions/diagnostics-otel/package.json +1 -1
  443. package/extensions/fased-federation/package.json +1 -1
  444. package/extensions/google-gemini-cli-auth/package.json +1 -1
  445. package/extensions/imessage/package.json +1 -1
  446. package/extensions/irc/package.json +1 -1
  447. package/extensions/line/package.json +1 -1
  448. package/extensions/llm-task/package.json +1 -1
  449. package/extensions/lobster/package.json +1 -1
  450. package/extensions/matrix/CHANGELOG.md +18 -0
  451. package/extensions/matrix/package.json +1 -1
  452. package/extensions/mattermost/package.json +1 -1
  453. package/extensions/memory-core/package.json +2 -2
  454. package/extensions/memory-lancedb/package.json +1 -1
  455. package/extensions/minimax-portal-auth/package.json +1 -1
  456. package/extensions/msteams/CHANGELOG.md +18 -0
  457. package/extensions/msteams/package.json +1 -1
  458. package/extensions/nextcloud-talk/package.json +1 -1
  459. package/extensions/nostr/CHANGELOG.md +18 -0
  460. package/extensions/nostr/package.json +1 -1
  461. package/extensions/open-prose/package.json +1 -1
  462. package/extensions/sat-mining/index.ts +498 -153
  463. package/extensions/sat-mining/package.json +1 -1
  464. package/extensions/sat-mining/signer-codec-schema.v1.json +374 -0
  465. package/extensions/sat-mining/src/audit-store.ts +50 -5
  466. package/extensions/sat-mining/src/claim-service.ts +85 -14
  467. package/extensions/sat-mining/src/config.ts +9 -0
  468. package/extensions/sat-mining/src/cycle-progress.ts +16 -8
  469. package/extensions/sat-mining/src/epoch-service.ts +9 -5
  470. package/extensions/sat-mining/src/gateway-runner.ts +21 -1
  471. package/extensions/sat-mining/src/protocol-contract.ts +45 -1
  472. package/extensions/sat-mining/src/recovery-service.ts +312 -0
  473. package/extensions/sat-mining/src/round-watcher.ts +391 -81
  474. package/extensions/sat-mining/src/rpc-read.ts +330 -89
  475. package/extensions/sat-mining/src/runtime.ts +14 -0
  476. package/extensions/sat-mining/src/sat-bond-layout.ts +6 -0
  477. package/extensions/sat-mining/src/signer-codec-manifest.ts +429 -0
  478. package/extensions/sat-mining/src/solana-submit.ts +1199 -428
  479. package/extensions/sat-mining/src/submission-ledger.ts +433 -0
  480. package/extensions/signal/package.json +1 -1
  481. package/extensions/synology-chat/package.json +1 -1
  482. package/extensions/tlon/package.json +1 -1
  483. package/extensions/twitch/CHANGELOG.md +18 -0
  484. package/extensions/twitch/package.json +1 -1
  485. package/extensions/voice-call/CHANGELOG.md +18 -0
  486. package/extensions/voice-call/package.json +1 -1
  487. package/extensions/zalo/CHANGELOG.md +18 -0
  488. package/extensions/zalo/package.json +1 -1
  489. package/extensions/zalouser/CHANGELOG.md +18 -0
  490. package/extensions/zalouser/package.json +1 -1
  491. package/install.sh +4955 -0
  492. package/package.json +24 -5
  493. package/scripts/fased-host-updater.mjs +1421 -0
  494. package/scripts/fased-host-updaterctl.mjs +186 -0
  495. package/scripts/fased-managed-service.sh +36 -0
  496. package/scripts/fased-managed-updater.mjs +3065 -156
  497. package/scripts/fased-signer-enroll-hosting.sh +275 -0
  498. package/scripts/fased-signer-network-hosting.sh +143 -0
  499. package/scripts/fased-signer-owner-policy.mjs +1430 -0
  500. package/scripts/fased-signer-policy-hosting.sh +50 -0
  501. package/scripts/fased-signer-policy-local.sh +26 -0
  502. package/scripts/hosted-release-manifest.mjs +185 -0
  503. package/scripts/install-fased-signerd.sh +217 -0
  504. package/scripts/install-managed-runtime.mjs +212 -10
  505. package/scripts/managed-runtime-layout.mjs +75 -3
  506. package/scripts/migrate-hosted-signer-v2.mjs +58 -0
  507. package/scripts/start-managed.sh +169 -360
  508. package/dist/control-ui/assets/mining-commit-R-6nu87p.js +0 -2
  509. package/dist/plugin-sdk/tmp-fased-dir-BdFQvrvw.d.ts +0 -77
  510. package/dist/wallet/local-socket-signer-broker.js +0 -224
  511. package/dist/wallet/local-socket-signer-custody.js +0 -42
  512. package/dist/wallet/providers/embedded-keystore-adapter.js +0 -402
  513. package/dist/wallet/providers/privy-adapter.js +0 -80
  514. package/dist/wallet/wallet-custody.js +0 -1836
@@ -1,1422 +1,172 @@
1
- import { redactSensitiveText } from "../logging/redact.js";
2
- import { redactSensitiveUrlLikeString } from "../shared/net/redact-sensitive-url.js";
3
- import { noteCommands, noteHeading, noteStep, noteWarn } from "./onboarding-note-format.js";
4
1
  import { isHostingProfile } from "./onboarding.types.js";
5
- import fs from "node:fs";
2
+ import syncFs from "node:fs";
6
3
  import os from "node:os";
7
4
  import path from "node:path";
8
- import { spawn, spawnSync } from "node:child_process";
9
- import { Buffer } from "node:buffer";
5
+ import { spawnSync } from "node:child_process";
10
6
 
11
7
  //#region src/wizard/onboarding.host-security.ts
8
+ const HOSTING_PREREQUISITES_MARKER = "/etc/fased/hosting-prerequisites";
9
+ const EXPECTED_MARKER_KEYS = new Set([
10
+ "schemaVersion",
11
+ "release",
12
+ "gatewayPort",
13
+ "tailscaleDns",
14
+ "tailnetSshConfirmed",
15
+ "tailscaleServeReady",
16
+ "firewallReady",
17
+ "sshHardened",
18
+ "fail2banReady",
19
+ "automaticUpdatesReady",
20
+ "signerReady",
21
+ "appSudoDisabled",
22
+ "preparedBy"
23
+ ]);
12
24
  function resolveHostSecurityLogPath() {
13
25
  const home = process.env.HOME?.trim() || os.homedir();
14
- const logDir = path.join(home, ".fased", "logs");
15
- fs.mkdirSync(logDir, {
16
- recursive: true,
17
- mode: 448
18
- });
19
- return path.join(logDir, "onboarding-host-security.log");
20
- }
21
- function appendHostSecurityLog(logPath, title, detail) {
22
- const sanitizedTitle = sanitizeHostSecurityLogText(title);
23
- const sanitizedDetail = detail ? sanitizeHostSecurityLogText(detail) : undefined;
24
- const body = [
25
- `\n=== ${new Date().toISOString()} ${sanitizedTitle} ===`,
26
- sanitizedDetail?.trim() || "(no output)",
27
- ""
28
- ].join("\n");
29
- fs.appendFileSync(logPath, body, "utf8");
30
- }
31
- function resolveTailnetSshConfirmationPath() {
32
- const stateDir = process.env.FASED_STATE_DIR?.trim() || process.env.FASED_CONFIG_DIR?.trim() || path.join(process.env.HOME?.trim() || os.homedir(), ".fased");
33
- return path.join(stateDir, "hosting-tailnet-ssh-confirmed.json");
34
- }
35
- function hasStoredTailnetSshConfirmation(target) {
36
- try {
37
- const raw = fs.readFileSync(resolveTailnetSshConfirmationPath(), "utf8");
38
- const parsed = JSON.parse(raw);
39
- return parsed.user === target.user && parsed.repoDir === target.repoDir;
40
- } catch {
41
- return false;
42
- }
26
+ return path.join(home, ".fased", "logs", "onboarding-host-security.log");
43
27
  }
44
- function writeTailnetSshConfirmation(target) {
45
- const markerPath = resolveTailnetSshConfirmationPath();
46
- fs.mkdirSync(path.dirname(markerPath), {
47
- recursive: true,
48
- mode: 448
49
- });
50
- fs.writeFileSync(markerPath, `${JSON.stringify({
51
- confirmedAt: new Date().toISOString(),
52
- user: target.user,
53
- host: target.host,
54
- dns: target.dns,
55
- ipv4: target.ipv4,
56
- repoDir: target.repoDir
57
- }, null, 2)}\n`, {
28
+ function runReadOnlyProbe(command, args) {
29
+ const result = spawnSync(command, args, {
58
30
  encoding: "utf8",
59
- mode: 384
60
- });
61
- }
62
- function sanitizeHostSecurityLogText(value) {
63
- return redactSensitiveText(redactSensitiveUrlLikeString(value), { mode: "tools" }).replace(/--authkey(?:=|\s+)(["']?)([^\s"']+)\1/gi, "--authkey ***").replace(/\btskey-auth-[A-Za-z0-9_-]+/g, "tskey-auth-***");
64
- }
65
- function hasCommand(name) {
66
- const probe = spawnSync("bash", ["-lc", `command -v ${name}`], { stdio: "ignore" });
67
- return probe.status === 0;
68
- }
69
- const LOW_MEMORY_SWAP_THRESHOLD_MB = 2304;
70
- const LOW_MEMORY_HOSTING_SWAP_GB = 4;
71
- const HOSTING_SWAP_GB = 2;
72
- const TAILSCALE_INTERACTIVE_TIMEOUT_MS = 2 * 60 * 1e3;
73
- const TAILSCALE_LOGIN_URL_RE = /https:\/\/login\.tailscale\.com\/a\/[A-Za-z0-9_-]+/i;
74
- const HOST_MAINTENANCE_HELPER = "/usr/local/sbin/fased-host-maintenance";
75
- function detectTotalMemoryMb() {
76
- if (process.platform === "linux") {
77
- try {
78
- const meminfo = fs.readFileSync("/proc/meminfo", "utf8");
79
- const match = meminfo.match(/^MemTotal:\s+(\d+)\s+kB/m);
80
- if (match?.[1]) {
81
- return Math.floor(Number.parseInt(match[1], 10) / 1024);
82
- }
83
- } catch {}
84
- }
85
- return Math.floor(os.totalmem() / 1024 / 1024);
86
- }
87
- function resolveHostingSwapGb(explicitSwapGb, totalMemMb = detectTotalMemoryMb()) {
88
- if (typeof explicitSwapGb === "number" && Number.isFinite(explicitSwapGb)) {
89
- return Math.max(0, explicitSwapGb);
90
- }
91
- if (!Number.isFinite(totalMemMb) || totalMemMb <= 0 || totalMemMb <= LOW_MEMORY_SWAP_THRESHOLD_MB) {
92
- return LOW_MEMORY_HOSTING_SWAP_GB;
93
- }
94
- return HOSTING_SWAP_GB;
95
- }
96
- function run(command, logPath) {
97
- const proc = spawnSync("bash", ["-lc", command], {
98
- stdio: [
99
- "ignore",
100
- "pipe",
101
- "pipe"
102
- ],
103
- encoding: "utf8"
104
- });
105
- const detail = `${proc.stdout ?? ""}\n${proc.stderr ?? ""}`.trim();
106
- if (logPath) {
107
- appendHostSecurityLog(logPath, command, detail);
108
- }
109
- if (proc.status === 0) {
110
- return {
111
- ok: true,
112
- detail: detail || undefined
113
- };
114
- }
115
- return {
116
- ok: false,
117
- detail: detail || `${command} (exit=${proc.status ?? "unknown"})`
118
- };
119
- }
120
- async function runInteractive(command, logPath, options) {
121
- if (logPath) {
122
- appendHostSecurityLog(logPath, command, "interactive command started; output captured for framed wizard display");
123
- }
124
- return await new Promise((resolve) => {
125
- const proc = spawn("bash", ["-lc", command], { stdio: [
126
- "ignore",
127
- "pipe",
128
- "pipe"
129
- ] });
130
- let output = "";
131
- let loginUrlShown = false;
132
- let timedOut = false;
133
- let noteQueue = Promise.resolve();
134
- const timer = options?.timeoutMs && options.timeoutMs > 0 ? setTimeout(() => {
135
- timedOut = true;
136
- proc.kill("SIGINT");
137
- }, options.timeoutMs) : null;
138
- const maybeShowLoginUrl = (chunk) => {
139
- output += chunk;
140
- if (loginUrlShown) {
141
- return;
142
- }
143
- const loginUrl = output.match(TAILSCALE_LOGIN_URL_RE)?.[0];
144
- if (!loginUrl) {
145
- return;
146
- }
147
- loginUrlShown = true;
148
- if (options?.prompter) {
149
- const message = [
150
- noteHeading("Open login URL"),
151
- "Open this URL in your local browser:",
152
- ...noteCommands([loginUrl]),
153
- "Return here after approving this VPS."
154
- ].join("\n");
155
- noteQueue = noteQueue.then(() => options.prompter?.note(message, options.noteTitle ?? "Tailscale login URL"));
156
- }
157
- };
158
- proc.stdout?.on("data", (chunk) => {
159
- maybeShowLoginUrl(String(chunk));
160
- });
161
- proc.stderr?.on("data", (chunk) => {
162
- maybeShowLoginUrl(String(chunk));
163
- });
164
- proc.on("error", (error) => {
165
- output += `\n${error instanceof Error ? error.message : String(error)}`;
166
- });
167
- proc.on("close", (code) => {
168
- if (timer) {
169
- clearTimeout(timer);
170
- }
171
- const detail = timedOut ? `${command} timed out after ${Math.round((options?.timeoutMs ?? 0) / 1e3)}s\n${output}`.trim() : `${command} (exit=${code ?? "unknown"})\n${output}`.trim();
172
- if (logPath) {
173
- appendHostSecurityLog(logPath, command, detail);
174
- }
175
- void noteQueue.then(() => {
176
- if (code === 0) {
177
- resolve({
178
- ok: true,
179
- detail
180
- });
181
- return;
182
- }
183
- resolve({
184
- ok: false,
185
- detail,
186
- timedOut
187
- });
188
- });
189
- });
190
- });
191
- }
192
- function buildTailscaleLoginWaitCommand(command) {
193
- const quotedCommand = shellQuote(command);
194
- return [
195
- "set +e",
196
- `bash -lc ${quotedCommand} &`,
197
- "ts_pid=$!",
198
- "for _ in $(seq 1 55); do",
199
- ` if tailscale ip -4 >/dev/null 2>&1 || ${hostMaintenanceCommand("tailscale-ip4")} >/dev/null 2>&1 || sudo -n tailscale ip -4 >/dev/null 2>&1; then`,
200
- " kill -INT \"$ts_pid\" >/dev/null 2>&1 || true",
201
- " wait \"$ts_pid\" >/dev/null 2>&1 || true",
202
- " exit 0",
203
- " fi",
204
- " if ! kill -0 \"$ts_pid\" >/dev/null 2>&1; then",
205
- " wait \"$ts_pid\"",
206
- " exit $?",
207
- " fi",
208
- " sleep 2",
209
- "done",
210
- "kill -INT \"$ts_pid\" >/dev/null 2>&1 || true",
211
- "wait \"$ts_pid\" >/dev/null 2>&1 || true",
212
- `if tailscale ip -4 >/dev/null 2>&1 || ${hostMaintenanceCommand("tailscale-ip4")} >/dev/null 2>&1 || sudo -n tailscale ip -4 >/dev/null 2>&1; then`,
213
- " exit 0",
214
- "fi",
215
- "exit 124"
216
- ].join("\n");
217
- }
218
- async function runInteractiveTailscaleLogin(command, logPath, prompter) {
219
- if (logPath) {
220
- appendHostSecurityLog(logPath, command, "interactive Tailscale login started; login URL shown in framed wizard note");
221
- }
222
- const result = await runInteractive(buildTailscaleLoginWaitCommand(command), undefined, {
223
- timeoutMs: TAILSCALE_INTERACTIVE_TIMEOUT_MS,
224
- prompter,
225
- noteTitle: "Tailscale login URL"
226
- });
227
- if (logPath) {
228
- appendHostSecurityLog(logPath, "tailscale login result", result.ok ? "tailnet IP appeared or tailscale up completed" : result.detail ?? "tailscale login failed");
229
- }
230
- if (result.ok) {
231
- return {
232
- ok: true,
233
- detail: "tailnet IP appeared or tailscale up completed"
234
- };
235
- }
236
- return result;
237
- }
238
- function formatTailscaleBrowserLoginNote() {
239
- return [
240
- noteStep(1, "Open login URL"),
241
- "Use the Tailscale login URL printed below.",
242
- "",
243
- noteStep(2, "Approve VPS"),
244
- "Approve this VPS in your local browser.",
245
- "",
246
- noteHeading("If it waits"),
247
- noteWarn("Setup continues after about two minutes when a tailnet IP is already present.")
248
- ].join("\n");
249
- }
250
- function formatTailscaleAccountBrowserLoginNote() {
251
- return [
252
- noteStep(1, "Open login URL"),
253
- "Use the Tailscale login URL printed below.",
254
- "",
255
- noteStep(2, "Use same account"),
256
- "Use the account you want for dashboard and SSH access.",
257
- "",
258
- noteHeading("If it waits"),
259
- noteWarn("Setup continues after about two minutes when a tailnet IP is already present.")
260
- ].join("\n");
261
- }
262
- function tailscaleTimedOutButReady(result, logPath, runner = run) {
263
- if (!result.timedOut) {
264
- return false;
265
- }
266
- const ready = hasTailscaleIp(logPath, runner);
267
- if (ready && logPath) {
268
- appendHostSecurityLog(logPath, "tailscale login timeout accepted", "tailscale up timed out, but a tailnet IPv4 is present; continuing");
269
- }
270
- return ready;
271
- }
272
- function shellQuote(value) {
273
- return `'${value.replace(/'/g, "'\\''")}'`;
274
- }
275
- function hostMaintenanceCommand(action) {
276
- return `sudo -n ${HOST_MAINTENANCE_HELPER} ${action}`;
277
- }
278
- function hostMaintenanceCommandWithInput(action, input) {
279
- return `printf '%s\\n' ${shellQuote(input)} | ${hostMaintenanceCommand(action)}`;
280
- }
281
- function isTailscaleLoggedIn(logPath, runner = run) {
282
- return runner("tailscale status >/dev/null 2>&1", logPath).ok || runner(`${hostMaintenanceCommand("tailscale-status")} >/dev/null 2>&1`, logPath).ok || runner("sudo -n tailscale status >/dev/null 2>&1", logPath).ok;
283
- }
284
- function hasTailscaleIp(logPath, runner = run) {
285
- return runner("tailscale ip -4 >/dev/null 2>&1", logPath).ok || runner(`${hostMaintenanceCommand("tailscale-ip4")} >/dev/null 2>&1`, logPath).ok || runner("sudo -n tailscale ip -4 >/dev/null 2>&1", logPath).ok;
286
- }
287
- function readTailscaleIp(logPath, runner = run) {
288
- const probe = runner("tailscale ip -4", logPath);
289
- const helperProbe = probe.ok ? probe : runner(hostMaintenanceCommand("tailscale-ip4"), logPath);
290
- const sudoProbe = helperProbe.ok ? helperProbe : runner("sudo -n tailscale ip -4", logPath);
291
- if (!sudoProbe.ok || !sudoProbe.detail) {
292
- return undefined;
293
- }
294
- return sudoProbe.detail.match(/\b\d{1,3}(?:\.\d{1,3}){3}\b/)?.[0];
295
- }
296
- function readTailscaleSelf(logPath, runner = run) {
297
- const probe = runner("tailscale status --json", logPath);
298
- const helperProbe = probe.ok ? probe : runner(hostMaintenanceCommand("tailscale-status-json"), logPath);
299
- const sudoProbe = helperProbe.ok ? helperProbe : runner("sudo -n tailscale status --json", logPath);
300
- if (!sudoProbe.ok || !sudoProbe.detail) {
301
- return { ipv4: readTailscaleIp(logPath, runner) };
302
- }
303
- try {
304
- const parsed = JSON.parse(sudoProbe.detail);
305
- const dns = String(parsed.Self?.DNSName ?? "").trim().replace(/\.$/, "");
306
- const ipv4 = parsed.Self?.TailscaleIPs?.find((ip) => typeof ip === "string" && ip.includes(".")) ?? undefined;
307
- return {
308
- dns: dns || undefined,
309
- ipv4: ipv4 ?? readTailscaleIp(logPath, runner)
310
- };
311
- } catch {
312
- return { ipv4: readTailscaleIp(logPath, runner) };
313
- }
314
- }
315
- function resolveTailnetSshTarget(params) {
316
- const self = readTailscaleSelf(params.logPath, params.runner ?? run);
317
- const user = params.user?.trim() || "app";
318
- return {
319
- user,
320
- dns: self.dns,
321
- ipv4: self.ipv4,
322
- host: self.dns || self.ipv4 || "YOUR_VPS_TAILSCALE_NAME",
323
- repoDir: params.repoDir?.trim() || `/home/${user}/fased`
324
- };
325
- }
326
- function formatLocalDeviceTailnetRequirementNote() {
327
- return [
328
- noteStep(1, "Prepare local computer"),
329
- "Do this on your own computer, not inside the VPS.",
330
- "",
331
- "Turn off any non-Tailscale VPN.",
332
- "Sign into the same Tailscale account used on this VPS.",
333
- "Leave this VPS installer open.",
334
- "",
335
- noteStep(2, "Check Tailscale"),
336
- "Run these on your own computer:",
337
- ...noteCommands(["tailscale status", "tailscale ip -4"]),
338
- "Continue only after `tailscale ip -4` prints a `100.x.x.x` address.",
339
- "",
340
- noteStep(3, "Install if needed"),
341
- noteHeading("Windows and macOS"),
342
- "Install the Tailscale app. Sign in. Then run step 2.",
343
- "",
344
- noteHeading("Fedora"),
345
- ...noteCommands([
346
- "sudo dnf install -y tailscale",
347
- "sudo systemctl enable --now tailscaled",
348
- "sudo tailscale up"
349
- ]),
350
- noteHeading("Ubuntu Debian Kali"),
351
- ...noteCommands([
352
- "curl -fsSL https://tailscale.com/install.sh | sh",
353
- "sudo systemctl enable --now tailscaled",
354
- "sudo tailscale up"
355
- ]),
356
- noteHeading("Common blockers"),
357
- noteWarn("`tailscale` not found means Tailscale is not installed locally."),
358
- noteWarn("Another VPN can break MagicDNS."),
359
- noteWarn("Use the `100.x.x.x` IP fallback when hostname lookup fails."),
360
- ""
361
- ].join("\n");
362
- }
363
- function formatTailnetSshVerificationNote(target, method = "ssh-key") {
364
- const pingTargets = [target.host, target.ipv4 && target.ipv4 !== target.host ? target.ipv4 : undefined].filter((value) => Boolean(value));
365
- const sshTargets = pingTargets;
366
- const sshCommands = method === "tailscale-ssh" ? sshTargets.map((host) => `tailscale ssh ${target.user}@${host}`) : sshTargets.map((host) => `ssh ${target.user}@${host}`);
367
- return [
368
- noteStep(1, "Check visibility"),
369
- "Run on your own computer:",
370
- ...noteCommands([
371
- "tailscale status",
372
- "tailscale ip -4",
373
- ...pingTargets.map((host) => `tailscale ping ${host}`)
374
- ]),
375
- noteWarn("\"no matching peer\" means this computer and VPS are not in the same tailnet."),
376
- "",
377
- noteStep(2, "SSH into VPS"),
378
- "Run one command:",
379
- ...method === "tailscale-ssh" ? ["No app SSH key was found on this VPS.", "Use Tailscale SSH from your own Tailscale-connected computer:"] : [],
380
- ...noteCommands(sshCommands),
381
- `Continue only after SSH opens in ${target.repoDir}.`,
382
- "",
383
- noteHeading("Fallback"),
384
- noteWarn(method === "tailscale-ssh" ? "If Tailscale SSH is unavailable in your tailnet, choose the SSH public key fallback." : "If hostname lookup fails, keep the other VPN off and use the `100.x.x.x` command.")
385
- ].join("\n");
386
- }
387
- function formatTailnetSshPublicKeyNote(target) {
388
- return [
389
- noteStep(1, "Find your public key"),
390
- "Use this fallback only when Tailscale SSH is unavailable in your tailnet.",
391
- "Do this on your own computer while this VPS installer stays open.",
392
- ...noteCommands(["cat ~/.ssh/id_ed25519.pub", "type $env:USERPROFILE\\.ssh\\id_ed25519.pub"]),
393
- noteWarn("Paste a `.pub` key only. Never paste a private key."),
394
- "",
395
- noteStep(2, "Create one if needed"),
396
- "Run this on your own computer, then paste the `.pub` line here:",
397
- ...noteCommands(["ssh-keygen -t ed25519 -C \"fased-vps\" -f ~/.ssh/id_ed25519"]),
398
- "",
399
- noteStep(3, "What Fased will do"),
400
- `Install that public key for ${target.user} at /home/${target.user}/.ssh/authorized_keys.`,
401
- "Then you will test regular SSH over Tailscale before root/password access is locked down."
402
- ].join("\n");
403
- }
404
- function normalizeSshPublicKeys(input) {
405
- const acceptedTypes = new Set([
406
- "ecdsa-sha2-nistp256",
407
- "ecdsa-sha2-nistp384",
408
- "ecdsa-sha2-nistp521",
409
- "sk-ecdsa-sha2-nistp256@openssh.com",
410
- "sk-ssh-ed25519@openssh.com",
411
- "ssh-ed25519",
412
- "ssh-rsa"
413
- ]);
414
- const lines = input.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
415
- if (lines.length === 0) {
416
- return {
417
- ok: false,
418
- keys: [],
419
- detail: "Paste one SSH public key line."
420
- };
421
- }
422
- if (lines.some((line) => /^-{5}BEGIN\b/i.test(line) || /PRIVATE KEY/i.test(line))) {
423
- return {
424
- ok: false,
425
- keys: [],
426
- detail: "That looks like a private key. Paste the `.pub` key only."
427
- };
428
- }
429
- const normalized = [];
430
- for (const line of lines) {
431
- const parts = line.split(/\s+/);
432
- const keyType = parts[0] ?? "";
433
- const keyBody = parts[1] ?? "";
434
- if (!acceptedTypes.has(keyType) || !/^[A-Za-z0-9+/]+={0,3}$/.test(keyBody)) {
435
- return {
436
- ok: false,
437
- keys: [],
438
- detail: "Paste a valid OpenSSH public key, for example `ssh-ed25519 AAAA... user@computer`."
439
- };
440
- }
441
- normalized.push(parts.join(" "));
442
- }
443
- return {
444
- ok: true,
445
- keys: Array.from(new Set(normalized))
446
- };
447
- }
448
- function buildInstallTailnetSshAuthorizedKeysCommand(target, keys) {
449
- const sshDir = `/home/${target.user}/.ssh`;
450
- const authorizedKeys = `${sshDir}/authorized_keys`;
451
- const encodedKeys = Buffer.from(`${keys.join("\n")}\n`, "utf8").toString("base64");
452
- return [
453
- "set -e",
454
- "umask 077",
455
- `mkdir -p ${shellQuote(sshDir)}`,
456
- "tmp_keys=\"$(mktemp)\"",
457
- "tmp_merged=\"$(mktemp)\"",
458
- `printf '%s' ${shellQuote(encodedKeys)} | base64 -d > "$tmp_keys"`,
459
- `{ cat ${shellQuote(authorizedKeys)} 2>/dev/null || true; cat "$tmp_keys"; } | awk 'NF { print }' | sort -u > "$tmp_merged"`,
460
- `install -m 600 "$tmp_merged" ${shellQuote(authorizedKeys)}`,
461
- `chmod 700 ${shellQuote(sshDir)}`,
462
- `chmod 600 ${shellQuote(authorizedKeys)}`,
463
- "rm -f \"$tmp_keys\" \"$tmp_merged\""
464
- ].join("\n");
465
- }
466
- async function ensureTailnetSshAuthorizedKeys(params) {
467
- const { target, logPath, runtime, prompter, runner } = params;
468
- const authorizedKeys = `/home/${target.user}/.ssh/authorized_keys`;
469
- const existing = runner(`test -s ${shellQuote(authorizedKeys)}`, logPath);
470
- if (existing.ok) {
471
- return true;
472
- }
473
- await prompter.note(formatTailnetSshPublicKeyNote(target), "SSH key fallback");
474
- const pasted = await prompter.text({
475
- message: `Paste the SSH public key to install for ${target.user}`,
476
- placeholder: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA... user@computer",
477
- validate: (value) => {
478
- const parsed = normalizeSshPublicKeys(value);
479
- return parsed.ok ? undefined : parsed.detail;
480
- }
31
+ timeout: 5e3,
32
+ env: process.env
481
33
  });
482
- const parsed = normalizeSshPublicKeys(pasted);
483
- if (!parsed.ok) {
484
- runtime.error("Hosting setup stopped before SSH/firewall lock-down.");
485
- runtime.error(parsed.detail);
486
- runtime.error(`Host hardening log: ${logPath}`);
487
- runtime.exit(1);
488
- return false;
489
- }
490
- const installResult = runner(buildInstallTailnetSshAuthorizedKeysCommand(target, parsed.keys), logPath);
491
- if (!installResult.ok) {
492
- runtime.error("Hosting setup stopped before SSH/firewall lock-down.");
493
- runtime.error(`Could not install SSH public key for ${target.user}: ${authorizedKeys}`);
494
- runtime.error(installResult.detail ?? "unknown SSH key install error");
495
- runtime.error(`Host hardening log: ${logPath}`);
496
- runtime.exit(1);
497
- return false;
498
- }
499
- const verify = runner(`test -s ${shellQuote(authorizedKeys)}`, logPath);
500
- if (!verify.ok) {
501
- runtime.error("Hosting setup stopped before SSH/firewall lock-down.");
502
- runtime.error(`SSH public key was not written for ${target.user}: ${authorizedKeys}`);
503
- runtime.error(`Host hardening log: ${logPath}`);
504
- runtime.exit(1);
505
- return false;
506
- }
507
- appendHostSecurityLog(logPath, "tailnet ssh public key installed", `installed ${parsed.keys.length} public key(s) for ${target.user}: ${authorizedKeys}`);
508
- return true;
509
- }
510
- function verifyTailnetSshServerPrerequisites(params) {
511
- const runner = params.runner ?? run;
512
- const target = params.target;
513
- const checks = [];
514
- const failures = [];
515
- let method = "tailscale-ssh";
516
- const repo = runner(`test -d ${shellQuote(target.repoDir)}`, params.logPath);
517
- if (repo.ok) {
518
- checks.push(`repo directory ready: ${target.repoDir}`);
519
- } else {
520
- failures.push(`missing app repo directory: ${target.repoDir}`);
521
- }
522
- const authorizedKeys = `/home/${target.user}/.ssh/authorized_keys`;
523
- const keys = runner(`test -s ${shellQuote(authorizedKeys)}`, params.logPath);
524
- if (keys.ok) {
525
- method = "ssh-key";
526
- checks.push(`SSH keys ready: ${authorizedKeys}`);
527
- const sshService = runner("systemctl is-active --quiet ssh || systemctl is-active --quiet sshd || " + "sudo -n systemctl is-active --quiet ssh || sudo -n systemctl is-active --quiet sshd", params.logPath);
528
- if (sshService.ok) {
529
- checks.push("OS SSH service active");
530
- } else {
531
- failures.push("OS SSH service is not active");
532
- }
533
- } else {
534
- checks.push(`no app SSH keys at ${authorizedKeys}; using Tailscale SSH`);
535
- }
536
- if (failures.length > 0) {
537
- return {
538
- ok: false,
539
- detail: failures.join("\n"),
540
- method
541
- };
542
- }
543
34
  return {
544
- ok: true,
545
- detail: checks.join("\n"),
546
- method
35
+ ok: result.status === 0,
36
+ stdout: result.stdout || "",
37
+ detail: result.error?.message || result.stderr || undefined
547
38
  };
548
39
  }
549
- function ensureTailnetSshIngressForVerification(params) {
550
- const runner = params.runner ?? run;
551
- const command = [
552
- "if command -v ufw >/dev/null 2>&1; then",
553
- " if sudo -n ufw status | grep -qi '^Status: active'; then",
554
- " sudo -n ufw insert 1 allow in on tailscale0 to any port 22 proto tcp || sudo -n ufw allow in on tailscale0 to any port 22 proto tcp",
555
- " else",
556
- " sudo -n ufw allow in on tailscale0 to any port 22 proto tcp >/dev/null 2>&1 || true",
557
- " fi",
558
- "elif command -v firewall-cmd >/dev/null 2>&1 && sudo -n systemctl is-active --quiet firewalld; then",
559
- " sudo -n firewall-cmd --permanent --zone=trusted --add-interface=tailscale0 >/dev/null 2>&1 || true",
560
- " sudo -n firewall-cmd --reload >/dev/null 2>&1 || true",
561
- "fi"
562
- ].join("\n");
563
- const helperResult = runner(hostMaintenanceCommand("tailnet-ssh-ingress"), params.logPath);
564
- const result = helperResult.ok ? helperResult : runner(command, params.logPath);
565
- if (!result.ok) {
566
- return {
567
- ok: false,
568
- detail: result.detail ?? "could not prepare tailnet SSH firewall rule before verification"
569
- };
40
+ function readRootPreparedMarker(markerPath, requiredUid = 0) {
41
+ const stat = syncFs.lstatSync(markerPath);
42
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.uid !== requiredUid || stat.nlink !== 1 || (stat.mode & 18) !== 0 || stat.size <= 0 || stat.size > 4096) {
43
+ throw new Error("marker is not a safe root-owned, non-writable regular file");
570
44
  }
571
- return {
572
- ok: true,
573
- detail: result.detail ?? "tailnet SSH firewall rule ready"
574
- };
575
- }
576
- function hasExplicitTailnetSshConfirmation(env = process.env) {
577
- const raw = String(env.FASED_HOSTING_TAILNET_SSH_CONFIRMED ?? "").trim().toLowerCase();
578
- return raw === "1" || raw === "true" || raw === "yes";
579
- }
580
- function failTailnetSshConfirmation(params) {
581
- const { runtime, logPath, target } = params;
582
- const commandPrefix = params.method === "tailscale-ssh" ? "tailscale ssh" : "ssh";
583
- runtime.error("Hosting setup stopped before SSH/firewall lock-down.");
584
- runtime.error(`Confirm SSH over Tailscale first: ${commandPrefix} ${target.user}@${target.host}`);
585
- runtime.error(`Expected app repo directory after login: ${target.repoDir}`);
586
- runtime.error(`Host hardening log: ${logPath}`);
587
- runtime.exit(1);
588
- }
589
- async function confirmTailnetSshBeforeLockdown(params) {
590
- const { opts, runtime, prompter, logPath } = params;
591
- const runner = params.runner ?? run;
592
- const interactiveRunner = params.interactiveRunner ?? runInteractiveTailscaleLogin;
593
- let target = params.target;
594
- if (hasExplicitTailnetSshConfirmation()) {
595
- appendHostSecurityLog(logPath, "tailnet ssh confirmation", "confirmed by env");
596
- return true;
597
- }
598
- if (hasStoredTailnetSshConfirmation(target)) {
599
- appendHostSecurityLog(logPath, "tailnet ssh confirmation", "confirmed by previous run");
600
- return true;
601
- }
602
- if (!prompter || opts.nonInteractive === true) {
603
- const serverPrereqs = verifyTailnetSshServerPrerequisites({
604
- target,
605
- logPath,
606
- runner
607
- });
608
- failTailnetSshConfirmation({
609
- runtime,
610
- logPath,
611
- target,
612
- method: serverPrereqs.method
613
- });
614
- return false;
615
- }
616
- for (let attempt = 0; attempt < 2; attempt += 1) {
617
- const serverPrereqs = verifyTailnetSshServerPrerequisites({
618
- target,
619
- logPath,
620
- runner
621
- });
622
- if (!serverPrereqs.ok) {
623
- runtime.error("Hosting setup stopped before SSH/firewall lock-down.");
624
- runtime.error(serverPrereqs.detail);
625
- runtime.error(`Host hardening log: ${logPath}`);
626
- runtime.exit(1);
627
- return false;
628
- }
629
- appendHostSecurityLog(logPath, "tailnet ssh server prerequisites", serverPrereqs.detail);
630
- if (serverPrereqs.method === "ssh-key") {
631
- const ingress = ensureTailnetSshIngressForVerification({
632
- logPath,
633
- runner
634
- });
635
- if (!ingress.ok) {
636
- runtime.error("Hosting setup stopped before SSH/firewall lock-down.");
637
- runtime.error("Could not prepare the tailnet-only SSH firewall rule required for verification.");
638
- runtime.error(ingress.detail ?? "unknown firewall error");
639
- runtime.error(`Host hardening log: ${logPath}`);
640
- runtime.exit(1);
641
- return false;
642
- }
643
- appendHostSecurityLog(logPath, "tailnet ssh ingress prepared for verification", ingress.detail);
644
- } else {
645
- appendHostSecurityLog(logPath, "tailnet ssh verification mode", "using Tailscale SSH because app authorized_keys is empty");
646
- }
647
- await prompter.note(formatTailnetSshVerificationNote(target, serverPrereqs.method), "Verify SSH over Tailscale");
648
- const pingConfirmed = await prompter.confirm({
649
- message: "Did tailscale ping find this VPS from your own computer?",
650
- initialValue: false
651
- });
652
- if (!pingConfirmed) {
653
- const reauth = await prompter.confirm({
654
- message: "Re-authenticate Tailscale on this VPS now?",
655
- initialValue: true
656
- });
657
- if (!reauth) {
658
- failTailnetSshConfirmation({
659
- runtime,
660
- logPath,
661
- target,
662
- method: serverPrereqs.method
663
- });
664
- return false;
665
- }
666
- await prompter.note(formatTailscaleAccountBrowserLoginNote(), "Tailscale login");
667
- const tsReset = await interactiveRunner(hostMaintenanceCommand("tailscale-up-reset-ssh"), logPath, prompter);
668
- const acceptedTimedOutLogin = tailscaleTimedOutButReady(tsReset, logPath, runner);
669
- if (!tsReset.ok && !acceptedTimedOutLogin || !hasTailscaleIp(logPath, runner)) {
670
- runtime.error("Hosting setup stopped before SSH/firewall lock-down.");
671
- runtime.error(tsReset.detail ?? "tailscale re-authentication failed");
672
- runtime.error(`Host hardening log: ${logPath}`);
673
- runtime.exit(1);
674
- return false;
675
- }
676
- target = resolveTailnetSshTarget({
677
- user: target.user,
678
- repoDir: target.repoDir,
679
- logPath,
680
- runner
681
- });
45
+ const values = new Map();
46
+ for (const line of syncFs.readFileSync(markerPath, "utf8").split("\n")) {
47
+ if (!line) {
682
48
  continue;
683
49
  }
684
- const confirmed = await prompter.confirm({
685
- message: serverPrereqs.method === "tailscale-ssh" ? `Did Tailscale SSH connect as ${target.user} and open ${target.repoDir}?` : `Did SSH over Tailscale connect as ${target.user} and open ${target.repoDir}?`,
686
- initialValue: false
687
- });
688
- if (!confirmed) {
689
- if (serverPrereqs.method === "tailscale-ssh") {
690
- const useKeyFallback = await prompter.confirm({
691
- message: "Use SSH public key fallback instead?",
692
- initialValue: false
693
- });
694
- if (useKeyFallback) {
695
- const keysReady = await ensureTailnetSshAuthorizedKeys({
696
- target,
697
- logPath,
698
- runtime,
699
- prompter,
700
- runner
701
- });
702
- if (!keysReady) {
703
- return false;
704
- }
705
- continue;
706
- }
707
- }
708
- failTailnetSshConfirmation({
709
- runtime,
710
- logPath,
711
- target,
712
- method: serverPrereqs.method
713
- });
714
- return false;
50
+ const separator = line.indexOf("=");
51
+ if (separator <= 0) {
52
+ throw new Error("marker contains an invalid field");
715
53
  }
716
- writeTailnetSshConfirmation(target);
717
- appendHostSecurityLog(logPath, "tailnet ssh confirmation", "confirmed interactively");
718
- return true;
719
- }
720
- failTailnetSshConfirmation({
721
- runtime,
722
- logPath,
723
- target
724
- });
725
- return false;
726
- }
727
- function ensureTailscaleServe(port, logPath) {
728
- const modern = run(`tailscale serve --bg http://127.0.0.1:${port}`, logPath);
729
- if (modern.ok) {
730
- return {
731
- ok: true,
732
- detail: `tailscale serve --bg -> 127.0.0.1:${port}`
733
- };
734
- }
735
- const modernSudo = run(hostMaintenanceCommandWithInput("tailscale-serve", String(port)), logPath);
736
- if (modernSudo.ok) {
737
- return {
738
- ok: true,
739
- detail: `host maintenance tailscale serve -> 127.0.0.1:${port}`
740
- };
741
- }
742
- const legacy = run(`tailscale serve https / http://127.0.0.1:${port}`, logPath);
743
- if (legacy.ok) {
744
- return {
745
- ok: true,
746
- detail: `tailscale serve https / -> 127.0.0.1:${port}`
747
- };
748
- }
749
- const legacySudo = run(`sudo -n tailscale serve https / http://127.0.0.1:${port}`, logPath);
750
- if (legacySudo.ok) {
751
- return {
752
- ok: true,
753
- detail: `sudo tailscale serve https / -> 127.0.0.1:${port}`
754
- };
755
- }
756
- return {
757
- ok: false,
758
- detail: "serve setup failed (may require manual: sudo tailscale serve --bg http://127.0.0.1:" + port + ")"
759
- };
760
- }
761
- function isTailscaleServeReady(port) {
762
- let probe = spawnSync("bash", ["-lc", "tailscale serve status"], {
763
- stdio: [
764
- "ignore",
765
- "pipe",
766
- "pipe"
767
- ],
768
- encoding: "utf8"
769
- });
770
- if ((probe.status ?? 1) !== 0) {
771
- probe = spawnSync("bash", ["-lc", hostMaintenanceCommand("tailscale-serve-status")], {
772
- stdio: [
773
- "ignore",
774
- "pipe",
775
- "pipe"
776
- ],
777
- encoding: "utf8"
778
- });
779
- }
780
- if ((probe.status ?? 1) !== 0) {
781
- return false;
54
+ const key = line.slice(0, separator);
55
+ const value = line.slice(separator + 1);
56
+ if (!EXPECTED_MARKER_KEYS.has(key) || values.has(key)) {
57
+ throw new Error("marker contains an unknown or duplicate field");
58
+ }
59
+ values.set(key, value);
782
60
  }
783
- const out = `${probe.stdout ?? ""}\n${probe.stderr ?? ""}`;
784
- return out.includes(`127.0.0.1:${port}`);
785
- }
786
- function runHostSetupCommand(command) {
787
- return run(`export DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a PYTHONWARNINGS=ignore::SyntaxWarning; ${command}`, resolveHostSecurityLogPath());
61
+ return values;
788
62
  }
789
- function packageInstallCommand(packages) {
790
- const packageList = packages.map(shellQuote).join(" ");
791
- return [
792
- "if command -v apt-get >/dev/null 2>&1; then",
793
- " sudo -n apt-get update && sudo -n apt-get install -y " + packageList,
794
- "elif command -v dnf >/dev/null 2>&1; then",
795
- " sudo -n dnf install -y " + packageList,
796
- "elif command -v dnf5 >/dev/null 2>&1; then",
797
- " sudo -n dnf5 install -y " + packageList,
798
- "elif command -v yum >/dev/null 2>&1; then",
799
- " sudo -n yum install -y " + packageList,
800
- "else",
801
- " echo 'unsupported package manager: need apt-get, dnf, dnf5, or yum' >&2",
802
- " exit 1",
803
- "fi"
804
- ].join("\n");
63
+ function markerHasExpectedRootState(values) {
64
+ return values.size === EXPECTED_MARKER_KEYS.size && values.get("schemaVersion") === "2" && /^\d+\.\d+\.\d+$/.test(values.get("release") || "") && values.get("gatewayPort") === "18789" && /^[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?$/.test(values.get("tailscaleDns") || "") && values.get("tailnetSshConfirmed") === "true" && values.get("tailscaleServeReady") === "true" && values.get("firewallReady") === "true" && values.get("sshHardened") === "true" && values.get("fail2banReady") === "true" && values.get("automaticUpdatesReady") === "true" && values.get("signerReady") === "true" && values.get("appSudoDisabled") === "true" && values.get("preparedBy") === "root";
805
65
  }
806
- function tailscaleInstallCommand() {
807
- return [
808
- `${hostMaintenanceCommand("tailscale-install-start")} && exit 0`,
809
- "if command -v tailscale >/dev/null 2>&1; then",
810
- " :",
811
- "elif command -v apt-get >/dev/null 2>&1; then",
812
- " curl -fsSL https://tailscale.com/install.sh | sudo -E env DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a sh",
813
- "elif command -v dnf >/dev/null 2>&1; then",
814
- " sudo -n dnf install -y tailscale || curl -fsSL https://tailscale.com/install.sh | sudo -E sh",
815
- "elif command -v dnf5 >/dev/null 2>&1; then",
816
- " sudo -n dnf5 install -y tailscale || curl -fsSL https://tailscale.com/install.sh | sudo -E sh",
817
- "elif command -v yum >/dev/null 2>&1; then",
818
- " sudo -n yum install -y tailscale || curl -fsSL https://tailscale.com/install.sh | sudo -E sh",
819
- "elif command -v pacman >/dev/null 2>&1; then",
820
- " sudo -n pacman -Sy --needed --noconfirm tailscale",
821
- "elif command -v apk >/dev/null 2>&1; then",
822
- " sudo -n apk add --no-cache tailscale",
823
- "else",
824
- " curl -fsSL https://tailscale.com/install.sh | sudo -E sh",
825
- "fi",
826
- "if command -v systemctl >/dev/null 2>&1; then",
827
- " sudo -n systemctl enable --now tailscaled >/dev/null 2>&1 || true",
828
- "fi",
829
- "command -v tailscale >/dev/null 2>&1"
830
- ].join("\n");
831
- }
832
- function firewallBaselineCommand() {
833
- return [
834
- `${hostMaintenanceCommand("firewall-baseline")} && exit 0`,
835
- "if command -v ufw >/dev/null 2>&1 || command -v apt-get >/dev/null 2>&1; then",
836
- " command -v ufw >/dev/null 2>&1 || { " + packageInstallCommand(["ufw"]) + "; }",
837
- " sudo -n ufw default deny incoming",
838
- " sudo -n ufw default allow outgoing",
839
- " sudo -n ufw insert 1 allow in on tailscale0 to any port 22 proto tcp || sudo -n ufw allow in on tailscale0 to any port 22 proto tcp",
840
- " sudo -n ufw insert 2 allow in on tailscale0 to any port 443 proto tcp || sudo -n ufw allow in on tailscale0 to any port 443 proto tcp",
841
- " sudo -n ufw deny 22/tcp || true",
842
- " sudo -n ufw --force enable",
843
- "elif command -v firewall-cmd >/dev/null 2>&1 || command -v dnf >/dev/null 2>&1 || command -v dnf5 >/dev/null 2>&1 || command -v yum >/dev/null 2>&1; then",
844
- " command -v firewall-cmd >/dev/null 2>&1 || { " + packageInstallCommand(["firewalld"]) + "; }",
845
- " sudo -n systemctl enable --now firewalld",
846
- " sudo -n firewall-cmd --permanent --zone=trusted --add-interface=tailscale0 >/dev/null 2>&1 || true",
847
- " sudo -n firewall-cmd --permanent --zone=public --remove-service=ssh >/dev/null 2>&1 || true",
848
- " sudo -n firewall-cmd --permanent --zone=public --remove-port=22/tcp >/dev/null 2>&1 || true",
849
- " sudo -n firewall-cmd --reload",
850
- "else",
851
- " echo 'no supported firewall manager found: need ufw or firewalld' >&2",
852
- " exit 1",
853
- "fi"
854
- ].join("\n");
855
- }
856
- function automaticUpdatesCommand() {
66
+ function verifyRootPreparedHostingPrerequisites(params) {
67
+ const markerPath = params?.markerPath ?? HOSTING_PREREQUISITES_MARKER;
68
+ const probe = params?.probe ?? runReadOnlyProbe;
69
+ let markerReady = false;
70
+ try {
71
+ markerReady = markerHasExpectedRootState(readRootPreparedMarker(markerPath, params?.requiredMarkerUid ?? 0));
72
+ } catch {
73
+ markerReady = false;
74
+ }
75
+ const tailscaleIp = probe("tailscale", ["ip", "-4"]);
76
+ const tailnetReady = tailscaleIp.ok && /^100\.(?:6[4-9]|[789]\d|1[01]\d|12[0-7])\./m.test(tailscaleIp.stdout || "");
77
+ const serve = probe("tailscale", ["serve", "status"]);
78
+ const serveReady = serve.ok && (serve.stdout || "").includes("127.0.0.1:18789");
79
+ const signerReady = probe("systemctl", [
80
+ "is-active",
81
+ "--quiet",
82
+ "fased-signerd.service"
83
+ ]).ok;
84
+ const fail2banReady = probe("systemctl", [
85
+ "is-active",
86
+ "--quiet",
87
+ "fail2ban.service"
88
+ ]).ok;
89
+ const groups = probe("id", ["-nG"]);
90
+ const adminGroupPresent = /(?:^|\s)(?:sudo|wheel)(?:\s|$)/.test(groups.stdout || "");
91
+ const appCanElevate = probe("sudo", ["-n", "true"]).ok;
857
92
  return [
858
- `${hostMaintenanceCommand("automatic-updates")} && exit 0`,
859
- "if command -v apt-get >/dev/null 2>&1; then",
860
- " " + packageInstallCommand(["unattended-upgrades"]),
861
- " sudo -n systemctl enable --now unattended-upgrades >/dev/null 2>&1 || true",
862
- " sudo -n systemctl enable --now apt-daily.timer apt-daily-upgrade.timer",
863
- "elif command -v dnf >/dev/null 2>&1 || command -v dnf5 >/dev/null 2>&1; then",
864
- " (" + packageInstallCommand(["dnf5-plugin-automatic"]) + ") || (" + packageInstallCommand(["dnf-automatic"]) + ")",
865
- " sudo -n /usr/local/sbin/fased-host-maintenance enable-dnf-automatic >/dev/null 2>&1 || sudo -n sed -i 's/^apply_updates[[:space:]]*=.*/apply_updates = yes/' /etc/dnf/automatic.conf >/dev/null 2>&1 || true",
866
- " sudo -n systemctl enable --now dnf5-automatic.timer >/dev/null 2>&1 || sudo -n systemctl enable --now dnf-automatic.timer",
867
- "elif command -v yum >/dev/null 2>&1; then",
868
- " (" + packageInstallCommand(["dnf-automatic"]) + ") || (" + packageInstallCommand(["yum-cron"]) + ")",
869
- " sudo -n systemctl enable --now dnf-automatic.timer >/dev/null 2>&1 || sudo -n systemctl enable --now yum-cron",
870
- "else",
871
- " echo 'unsupported package manager for automatic updates' >&2",
872
- " exit 1",
873
- "fi"
874
- ].join("\n");
875
- }
876
- function sshHardeningCommand() {
877
- return hostMaintenanceCommand("harden-ssh");
878
- }
879
- function failOrContinue(params) {
880
- const { opts, runtime, step, detail } = params;
881
- const body = detail ? `${step}: ${detail}` : step;
882
- const logPath = resolveHostSecurityLogPath();
883
- if (opts.allowInsecure === true) {
884
- runtime.error(`WARN insecure-continue: ${body}`);
885
- runtime.error(`Host hardening log: ${logPath}`);
886
- return;
887
- }
888
- runtime.error(`Hosting setup failed: ${body}`);
889
- runtime.error(`Host hardening log: ${logPath}`);
890
- runtime.error("Re-run with --allow-insecure to bypass (not recommended).");
891
- runtime.exit(1);
93
+ {
94
+ name: "tailscale",
95
+ ok: markerReady && tailnetReady && serveReady,
96
+ detail: markerReady && tailnetReady && serveReady ? "root-prepared tailnet identity, private Serve route, and SSH confirmation verified" : "root-prepared Tailscale identity or private Serve route is unavailable"
97
+ },
98
+ {
99
+ name: "firewall",
100
+ ok: markerReady && groups.ok && !adminGroupPresent && !appCanElevate,
101
+ detail: markerReady && groups.ok && !adminGroupPresent && !appCanElevate ? "root prepared host lock-down; app account has no sudo or admin-group access" : "root preparation is invalid or the app account can elevate"
102
+ },
103
+ {
104
+ name: "ssh",
105
+ ok: markerReady,
106
+ detail: markerReady ? "tailnet SSH was confirmed before root applied SSH hardening" : "root SSH hardening marker is invalid"
107
+ },
108
+ {
109
+ name: "fail2ban",
110
+ ok: markerReady && fail2banReady,
111
+ detail: markerReady && fail2banReady ? "root-managed fail2ban service is active" : "root-managed fail2ban service is inactive"
112
+ },
113
+ {
114
+ name: "updates",
115
+ ok: markerReady && signerReady,
116
+ detail: markerReady && signerReady ? "root-managed signer and automatic-update prerequisites are active" : "root-managed signer service or prerequisite marker is unavailable"
117
+ }
118
+ ];
892
119
  }
893
120
  async function applyHostingSecurity(params) {
894
- const { opts, runtime, prompter } = params;
895
- const checks = [];
121
+ const { opts, runtime } = params;
122
+ const profile = isHostingProfile(opts.hostProfile) ? "hosting" : "local";
896
123
  const logPath = resolveHostSecurityLogPath();
897
- const hostingProfile = isHostingProfile(opts.hostProfile);
898
- const profile = hostingProfile ? "hosting" : "local";
899
- const maybeReauthenticateTailscaleInMaintenance = async () => {
900
- if (!prompter || opts.nonInteractive === true) {
901
- return {
902
- ok: true,
903
- detail: "post-bootstrap hosted maintenance session; skipping host hardening bootstrap"
904
- };
905
- }
906
- const switchAccount = await prompter.confirm({
907
- message: "Switch or re-authenticate Tailscale account on this VPS now?",
908
- initialValue: false
909
- });
910
- if (!switchAccount) {
911
- return {
912
- ok: true,
913
- detail: "post-bootstrap hosted maintenance session; skipping host hardening bootstrap"
914
- };
915
- }
916
- let tsAuthkey = opts.tsAuthkey?.trim() ?? "";
917
- if (!tsAuthkey) {
918
- const useBrowserLogin = await prompter.confirm({
919
- message: "Use browser login for Tailscale? (recommended)",
920
- initialValue: true
921
- });
922
- if (!useBrowserLogin) {
923
- const keyValue = typeof prompter.secret === "function" ? await prompter.secret({
924
- message: "Paste Tailscale auth key (tskey-auth-...)",
925
- validate: (value) => value.trim() ? undefined : "Required"
926
- }) : await prompter.text({
927
- message: "Paste Tailscale auth key (tskey-auth-...)",
928
- validate: (value) => value.trim() ? undefined : "Required"
929
- });
930
- tsAuthkey = keyValue.trim();
931
- } else {
932
- await prompter.note(formatTailscaleBrowserLoginNote(), "Tailscale login");
933
- }
934
- }
935
- const resetCommand = tsAuthkey ? hostMaintenanceCommandWithInput("tailscale-up-reset-authkey-ssh", tsAuthkey) : hostMaintenanceCommand("tailscale-up-reset-ssh");
936
- const tsReset = tsAuthkey ? run(resetCommand, logPath) : await runInteractiveTailscaleLogin(resetCommand, logPath, prompter);
937
- if (!tsReset.ok && !tailscaleTimedOutButReady(tsReset, logPath)) {
938
- return {
939
- ok: false,
940
- detail: tsReset.detail ?? "tailscale account switch failed"
941
- };
942
- }
943
- return {
944
- ok: true,
945
- detail: "tailscale re-authenticated in hosted maintenance session"
946
- };
947
- };
948
- if (!hostingProfile) {
949
- return {
950
- profile,
951
- checks,
952
- enforced: false,
953
- logPath
954
- };
955
- }
956
- if (opts.hostMaintenanceSession === true && opts.hostSecurityCapable !== true) {
957
- const maintenanceTailscale = await maybeReauthenticateTailscaleInMaintenance();
124
+ if (profile !== "hosting") {
958
125
  return {
959
126
  profile,
960
- checks: [{
961
- name: "tailscale",
962
- ok: maintenanceTailscale.ok,
963
- detail: maintenanceTailscale.detail
964
- }],
127
+ checks: [],
965
128
  enforced: false,
966
129
  logPath
967
130
  };
968
131
  }
969
132
  if (process.platform !== "linux") {
970
- checks.push({
971
- name: "tailscale",
972
- ok: false,
973
- detail: "hosting profile currently supports Linux only"
974
- });
975
- failOrContinue({
976
- opts,
977
- runtime,
978
- step: "hosting profile currently supports Linux only"
979
- });
980
- return {
981
- profile,
982
- checks,
983
- enforced: false,
984
- logPath
985
- };
986
- }
987
- if (!hasCommand("sudo")) {
988
- checks.push({
989
- name: "firewall",
990
- ok: false,
991
- detail: "sudo is required for hosting setup"
992
- });
993
- failOrContinue({
994
- opts,
995
- runtime,
996
- step: "sudo is required for hosting setup"
997
- });
998
- return {
999
- profile,
1000
- checks,
1001
- enforced: false,
1002
- logPath
1003
- };
1004
- }
1005
- const swapGb = resolveHostingSwapGb(opts.swapGb);
1006
- if (swapGb > 0) {
1007
- const hasSwap = run("swapon --show | tail -n +2 | grep -q .", logPath).ok;
1008
- if (!hasSwap) {
1009
- const swapCmd = `sudo fallocate -l ${swapGb}G /swapfile || sudo dd if=/dev/zero of=/swapfile bs=1M count=$((` + `${swapGb}*1024)) status=none; ` + "sudo chmod 600 /swapfile; sudo mkswap /swapfile >/dev/null; sudo swapon /swapfile; " + "grep -q '^/swapfile ' /etc/fstab || echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab >/dev/null";
1010
- const swapRes = run(swapCmd, logPath);
1011
- if (!swapRes.ok) {
1012
- checks.push({
1013
- name: "swap",
1014
- ok: false,
1015
- detail: swapRes.detail ?? "swap setup failed"
1016
- });
1017
- failOrContinue({
1018
- opts,
1019
- runtime,
1020
- step: "swap setup failed",
1021
- detail: swapRes.detail
1022
- });
1023
- return {
1024
- profile,
1025
- checks,
1026
- enforced: false,
1027
- logPath
1028
- };
1029
- }
1030
- checks.push({
1031
- name: "swap",
1032
- ok: true,
1033
- detail: `configured ${swapGb}G swapfile`
1034
- });
1035
- } else {
1036
- checks.push({
1037
- name: "swap",
1038
- ok: true,
1039
- detail: "existing swap detected"
1040
- });
1041
- }
1042
- } else {
1043
- checks.push({
1044
- name: "swap",
1045
- ok: true,
1046
- detail: "disabled (swap-gb=0)"
1047
- });
1048
- }
1049
- if (!hasCommand("tailscale")) {
1050
- const installTs = run(tailscaleInstallCommand(), logPath);
1051
- if (!installTs.ok) {
1052
- checks.push({
1053
- name: "tailscale",
1054
- ok: false,
1055
- detail: installTs.detail ?? "tailscale install failed"
1056
- });
1057
- failOrContinue({
1058
- opts,
1059
- runtime,
1060
- step: "tailscale install failed",
1061
- detail: installTs.detail
1062
- });
1063
- return {
1064
- profile,
1065
- checks,
1066
- enforced: false,
1067
- logPath
1068
- };
1069
- }
1070
- } else if (hasCommand("systemctl")) {
1071
- run(`${hostMaintenanceCommand("tailscale-install-start")} >/dev/null 2>&1 || sudo -n systemctl enable --now tailscaled >/dev/null 2>&1 || true`, logPath);
1072
- }
1073
- if (prompter && opts.nonInteractive !== true) {
1074
- await prompter.note(formatLocalDeviceTailnetRequirementNote(), "Local device requirement");
1075
- }
1076
- let tsHealthy = isTailscaleLoggedIn(logPath);
1077
- if (tsHealthy && prompter && opts.nonInteractive !== true) {
1078
- const switchAccount = await prompter.confirm({
1079
- message: "Switch or re-authenticate Tailscale account on this VPS now?",
1080
- initialValue: false
1081
- });
1082
- if (switchAccount) {
1083
- if (prompter) {
1084
- await prompter.note(formatTailscaleBrowserLoginNote(), "Tailscale login");
1085
- }
1086
- const tsReset = await runInteractiveTailscaleLogin(hostMaintenanceCommand("tailscale-up-reset-ssh"), logPath, prompter);
1087
- if (!tsReset.ok && !tailscaleTimedOutButReady(tsReset, logPath)) {
1088
- checks.push({
1089
- name: "tailscale",
1090
- ok: false,
1091
- detail: tsReset.detail ?? "tailscale account switch failed"
1092
- });
1093
- failOrContinue({
1094
- opts,
1095
- runtime,
1096
- step: "tailscale account switch failed",
1097
- detail: tsReset.detail
1098
- });
1099
- return {
1100
- profile,
1101
- checks,
1102
- enforced: false,
1103
- logPath
1104
- };
1105
- }
1106
- checks.push({
1107
- name: "tailscale",
1108
- ok: true,
1109
- detail: "tailscale re-authenticated via account switch/reset"
1110
- });
1111
- tsHealthy = isTailscaleLoggedIn(logPath);
1112
- }
1113
- }
1114
- if (!tsHealthy) {
1115
- let tsAuthkey = opts.tsAuthkey?.trim() ?? "";
1116
- if (!tsAuthkey && opts.nonInteractive !== true && prompter) {
1117
- const useBrowserLogin = await prompter.confirm({
1118
- message: "Use browser login for Tailscale? (recommended)",
1119
- initialValue: true
1120
- });
1121
- if (!useBrowserLogin) {
1122
- const keyValue = typeof prompter.secret === "function" ? await prompter.secret({
1123
- message: "Paste Tailscale auth key (tskey-auth-...)",
1124
- validate: (value) => value.trim() ? undefined : "Required"
1125
- }) : await prompter.text({
1126
- message: "Paste Tailscale auth key (tskey-auth-...)",
1127
- validate: (value) => value.trim() ? undefined : "Required"
1128
- });
1129
- tsAuthkey = keyValue.trim();
1130
- }
1131
- }
1132
- if (tsAuthkey) {
1133
- const tsUp = run(hostMaintenanceCommandWithInput("tailscale-up-authkey-ssh", tsAuthkey), logPath);
1134
- if (!tsUp.ok) {
1135
- checks.push({
1136
- name: "tailscale",
1137
- ok: false,
1138
- detail: tsUp.detail ?? "tailscale up with auth key failed"
1139
- });
1140
- failOrContinue({
1141
- opts,
1142
- runtime,
1143
- step: "tailscale up with auth key failed",
1144
- detail: tsUp.detail
1145
- });
1146
- return {
1147
- profile,
1148
- checks,
1149
- enforced: false,
1150
- logPath
1151
- };
1152
- }
1153
- checks.push({
1154
- name: "tailscale",
1155
- ok: true,
1156
- detail: "tailscale up via auth key"
1157
- });
1158
- } else if (opts.nonInteractive === true) {
1159
- checks.push({
1160
- name: "tailscale",
1161
- ok: false,
1162
- detail: "non-interactive hosting setup requires --ts-authkey"
1163
- });
1164
- failOrContinue({
1165
- opts,
1166
- runtime,
1167
- step: "non-interactive hosting setup requires --ts-authkey (or --allow-insecure)"
1168
- });
1169
- return {
1170
- profile,
1171
- checks,
1172
- enforced: false,
1173
- logPath
1174
- };
1175
- } else {
1176
- if (prompter) {
1177
- await prompter.note(formatTailscaleBrowserLoginNote(), "Tailscale login");
1178
- }
1179
- const tsUp = await runInteractiveTailscaleLogin(hostMaintenanceCommand("tailscale-up-ssh"), logPath, prompter);
1180
- if (!tsUp.ok && !tailscaleTimedOutButReady(tsUp, logPath)) {
1181
- checks.push({
1182
- name: "tailscale",
1183
- ok: false,
1184
- detail: tsUp.detail ?? "tailscale up failed"
1185
- });
1186
- failOrContinue({
1187
- opts,
1188
- runtime,
1189
- step: "tailscale up failed",
1190
- detail: tsUp.detail
1191
- });
1192
- return {
1193
- profile,
1194
- checks,
1195
- enforced: false,
1196
- logPath
1197
- };
1198
- }
1199
- checks.push({
1200
- name: "tailscale",
1201
- ok: true,
1202
- detail: "tailscale up via interactive login"
1203
- });
1204
- }
1205
- } else {
1206
- checks.push({
1207
- name: "tailscale",
1208
- ok: true,
1209
- detail: "tailscale already healthy"
1210
- });
1211
- }
1212
- if (!hasTailscaleIp(logPath)) {
1213
- checks.push({
1214
- name: "tailscale",
1215
- ok: false,
1216
- detail: "tailscale network is not ready (no tailnet IP)"
1217
- });
1218
- failOrContinue({
1219
- opts,
1220
- runtime,
1221
- step: "tailscale not ready; refusing to apply ssh/firewall lock-down"
1222
- });
1223
- return {
1224
- profile,
1225
- checks,
1226
- enforced: false,
1227
- logPath
1228
- };
1229
- }
1230
- checks.push({
1231
- name: "tailscale",
1232
- ok: true,
1233
- detail: "tailnet IP present; verifying app SSH before lock-down"
1234
- });
1235
- const operatorUser = run("id -u app >/dev/null 2>&1", logPath).ok ? "app" : process.env.SUDO_USER?.trim() || process.env.USER?.trim() || process.env.LOGNAME?.trim();
1236
- if (operatorUser) {
1237
- run(`${hostMaintenanceCommand("tailscale-set-operator-self")} >/dev/null 2>&1 || true`, logPath);
1238
- }
1239
- const tailnetSshTarget = resolveTailnetSshTarget({
1240
- user: operatorUser || "app",
1241
- repoDir: operatorUser ? `/home/${operatorUser}/fased` : "/home/app/fased",
1242
- logPath
1243
- });
1244
- const tailnetSshConfirmed = await confirmTailnetSshBeforeLockdown({
1245
- opts,
1246
- runtime,
1247
- prompter,
1248
- logPath,
1249
- target: tailnetSshTarget
1250
- });
1251
- if (!tailnetSshConfirmed) {
1252
- return {
1253
- profile,
1254
- checks,
1255
- enforced: false,
1256
- logPath
1257
- };
1258
- }
1259
- checks.push({
1260
- name: "ssh",
1261
- ok: true,
1262
- detail: "operator confirmed app SSH over Tailscale before lock-down"
1263
- });
1264
- const servePort = Math.max(1, opts.gatewayPort ?? 18789);
1265
- const serveRes = ensureTailscaleServe(servePort, logPath);
1266
- if (!serveRes.ok || !isTailscaleServeReady(servePort)) {
1267
- checks.push({
1268
- name: "tailscale",
1269
- ok: false,
1270
- detail: serveRes.detail ?? `tailscale serve not ready for 127.0.0.1:${servePort}`
1271
- });
1272
- failOrContinue({
1273
- opts,
1274
- runtime,
1275
- step: "tailscale serve setup failed; refusing to apply ssh/firewall lock-down",
1276
- detail: serveRes.detail
1277
- });
1278
- return {
1279
- profile,
1280
- checks,
1281
- enforced: false,
1282
- logPath
1283
- };
1284
- }
1285
- checks.push({
1286
- name: "tailscale",
1287
- ok: true,
1288
- detail: `tailscale serve is active for 127.0.0.1:${servePort}`
1289
- });
1290
- const firewallRes = runHostSetupCommand(firewallBaselineCommand());
1291
- if (!firewallRes.ok) {
1292
- checks.push({
1293
- name: "firewall",
1294
- ok: false,
1295
- detail: firewallRes.detail ?? "firewall baseline failed"
1296
- });
1297
- failOrContinue({
1298
- opts,
1299
- runtime,
1300
- step: "firewall baseline failed",
1301
- detail: firewallRes.detail
1302
- });
1303
- return {
1304
- profile,
1305
- checks,
1306
- enforced: false
1307
- };
1308
- }
1309
- checks.push({
1310
- name: "firewall",
1311
- ok: true,
1312
- detail: "default-deny public ingress with tailnet SSH/HTTPS access"
1313
- });
1314
- const sshRes = run(sshHardeningCommand(), logPath);
1315
- if (!sshRes.ok) {
1316
- checks.push({
1317
- name: "ssh",
1318
- ok: false,
1319
- detail: sshRes.detail ?? "ssh hardening failed"
1320
- });
1321
- failOrContinue({
1322
- opts,
1323
- runtime,
1324
- step: "ssh hardening failed",
1325
- detail: sshRes.detail
1326
- });
133
+ runtime.error("Hosting setup failed: the maintained Hosting profile requires Linux.");
134
+ runtime.exit(1);
1327
135
  return {
1328
136
  profile,
1329
- checks,
137
+ checks: [],
1330
138
  enforced: false,
1331
139
  logPath
1332
140
  };
1333
141
  }
1334
- checks.push({
1335
- name: "ssh",
1336
- ok: true,
1337
- detail: "password auth/root login disabled and ssh restarted"
1338
- });
1339
- const f2bRes = runHostSetupCommand(`${hostMaintenanceCommand("fail2ban-enable")} || (${packageInstallCommand(["fail2ban"])}\nsudo -n systemctl enable --now fail2ban)`);
1340
- if (!f2bRes.ok) {
1341
- checks.push({
1342
- name: "fail2ban",
1343
- ok: false,
1344
- detail: f2bRes.detail ?? "fail2ban setup failed"
1345
- });
1346
- failOrContinue({
1347
- opts,
1348
- runtime,
1349
- step: "fail2ban setup failed",
1350
- detail: f2bRes.detail
1351
- });
142
+ if (process.env.FASED_HOST_ROOT_PREPARED?.trim() !== "1") {
143
+ runtime.error("Hosting setup failed: start from the provider root console with an exact tagged, attested release.");
144
+ runtime.error("The app account cannot repair host prerequisites and is never given sudo access.");
145
+ runtime.exit(1);
1352
146
  return {
1353
147
  profile,
1354
- checks,
148
+ checks: [],
1355
149
  enforced: false,
1356
150
  logPath
1357
151
  };
1358
152
  }
1359
- checks.push({
1360
- name: "fail2ban",
1361
- ok: true,
1362
- detail: "installed and enabled"
1363
- });
1364
- const updatesRes = runHostSetupCommand(automaticUpdatesCommand());
1365
- if (!updatesRes.ok) {
1366
- checks.push({
1367
- name: "updates",
1368
- ok: false,
1369
- detail: updatesRes.detail ?? "automatic security updates setup failed"
1370
- });
1371
- failOrContinue({
1372
- opts,
1373
- runtime,
1374
- step: "automatic security updates setup failed",
1375
- detail: updatesRes.detail
1376
- });
1377
- return {
1378
- profile,
1379
- checks,
1380
- enforced: false,
1381
- logPath
1382
- };
153
+ const checks = verifyRootPreparedHostingPrerequisites();
154
+ const enforced = checks.every((check) => check.ok);
155
+ if (!enforced) {
156
+ runtime.error("Hosting setup failed: root-prepared prerequisites could not be verified. Run the exact tagged repair from the provider root console.");
157
+ runtime.exit(1);
1383
158
  }
1384
- checks.push({
1385
- name: "updates",
1386
- ok: true,
1387
- detail: "automatic security updates enabled"
1388
- });
1389
- appendHostSecurityLog(logPath, "hosting host hardening complete", "enforced");
1390
159
  return {
1391
160
  profile,
1392
161
  checks,
1393
- enforced: true,
162
+ enforced,
1394
163
  logPath
1395
164
  };
1396
165
  }
1397
166
  const __testing = {
1398
- formatLocalDeviceTailnetRequirementNote,
1399
- formatTailscaleBrowserLoginNote,
1400
- formatTailnetSshVerificationNote,
1401
- hasTailscaleIp,
1402
- hasExplicitTailnetSshConfirmation,
1403
- isTailscaleLoggedIn,
1404
- readTailscaleIp,
1405
- resolveHostingSwapGb,
1406
- resolveTailnetSshTarget,
1407
- sanitizeHostSecurityLogText,
1408
- confirmTailnetSshBeforeLockdown,
1409
- buildInstallTailnetSshAuthorizedKeysCommand,
1410
- formatTailnetSshPublicKeyNote,
1411
- normalizeSshPublicKeys,
1412
- ensureTailnetSshIngressForVerification,
1413
- firewallBaselineCommand,
1414
- packageInstallCommand,
1415
- tailscaleInstallCommand,
1416
- automaticUpdatesCommand,
1417
- sshHardeningCommand,
1418
- buildTailscaleLoginWaitCommand,
1419
- verifyTailnetSshServerPrerequisites
167
+ markerHasExpectedRootState,
168
+ readRootPreparedMarker,
169
+ verifyRootPreparedHostingPrerequisites
1420
170
  };
1421
171
 
1422
172
  //#endregion