@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.
- package/CHANGELOG.md +43 -0
- package/README.md +38 -30
- package/config/dependency-ownership.json +1 -1
- package/config/signer-policies/README.md +49 -0
- package/config/signer-policies/agent.json.template +32 -0
- package/config/signer-policies/mining.json.template +69 -0
- package/config/signer-policies/network.json.template +5 -0
- package/config/signer-policies/vault.json.template +70 -0
- package/config/signer-protocol-v2.json +76 -0
- package/config/typescript-noemit-baseline.json +680 -0
- package/dist/acp/client.js +2 -2
- package/dist/acp/secret-file.js +2 -2
- package/dist/agents/agent-scope.js +2 -2
- package/dist/agents/apply-patch.js +8 -8
- package/dist/agents/auth-profiles/paths.js +2 -2
- package/dist/agents/auth-profiles/store.js +269 -33
- package/dist/agents/bash-tools.exec.js +10 -4
- package/dist/agents/bash-tools.shared.js +2 -2
- package/dist/agents/cli-credentials.js +4 -4
- package/dist/agents/current-model-catalog.js +3 -3
- package/dist/agents/docs-path.js +3 -3
- package/dist/agents/embedded-pi-mcp.js +4 -4
- package/dist/agents/identity-avatar.js +3 -3
- package/dist/agents/identity-file.js +2 -2
- package/dist/agents/memory-search.js +7 -1
- package/dist/agents/model-catalog-access.js +1 -2
- package/dist/agents/model-catalog-snapshot.js +1 -1
- package/dist/agents/model-catalog-source.js +1 -1
- package/dist/agents/model-metadata.js +1 -1
- package/dist/agents/model-selection.js +2 -28
- package/dist/agents/models-config.providers.js +6 -6
- package/dist/agents/openai-codex-app-server.js +5 -5
- package/dist/agents/pi-embedded-runner/compact.js +15 -7
- package/dist/agents/pi-embedded-runner/model.js +1 -1
- package/dist/agents/pi-embedded-runner/run/attempt.js +24 -13
- package/dist/agents/pi-embedded-runner/run.js +2 -0
- package/dist/agents/pi-embedded-runner/tool-split.js +40 -2
- package/dist/agents/pi-embedded-subscribe.raw-stream.js +3 -3
- package/dist/agents/pi-extensions/compaction-safeguard.js +3 -3
- package/dist/agents/pi-model-discovery.js +6 -6
- package/dist/agents/pi-project-settings.js +3 -3
- package/dist/agents/pi-tool-definition-adapter.js +16 -1
- package/dist/agents/pi-tools.js +2 -1
- package/dist/agents/sandbox/config.js +1 -1
- package/dist/agents/sandbox/context.js +27 -2
- package/dist/agents/sandbox/fs-bridge.js +2 -2
- package/dist/agents/session-dirs.js +4 -4
- package/dist/agents/session-write-lock.js +13 -13
- package/dist/agents/shell-utils.js +5 -5
- package/dist/agents/skills/bundled-dir.js +4 -4
- package/dist/agents/skills/env-overrides.js +38 -28
- package/dist/agents/skills/local-loader.js +6 -6
- package/dist/agents/skills/plugin-skills.js +2 -2
- package/dist/agents/skills/trust.js +93 -0
- package/dist/agents/skills/workspace.js +141 -13
- package/dist/agents/skills-clawhub.js +36 -11
- package/dist/agents/skills-install-download.js +7 -7
- package/dist/agents/skills-install-plan.js +2 -2
- package/dist/agents/skills-install.js +2 -2
- package/dist/agents/skills-marketplace-scan.js +49 -0
- package/dist/agents/skills-status.js +2 -2
- package/dist/agents/subagent-depth.js +2 -2
- package/dist/agents/system-prompt-params.js +2 -2
- package/dist/agents/tools/marketplace-offer-draft-tool.js +1 -1
- package/dist/agents/tools/mining-tool.js +1 -1
- package/dist/agents/tools/session-status-tool.js +1 -1
- package/dist/agents/tools/wallet-action-tool.js +172 -36
- package/dist/agents/tools/wallet-skill-policy.js +65 -23
- package/dist/agents/tools/wallet-tool.js +7 -11
- package/dist/agents/workspace.js +22 -22
- package/dist/auto-reply/reply/agent-runner-execution.js +2 -2
- package/dist/auto-reply/reply/agent-runner.js +2 -2
- package/dist/auto-reply/reply/commands-export-session.js +7 -7
- package/dist/auto-reply/reply/directive-handling.fast-lane.js +2 -1
- package/dist/auto-reply/reply/directive-handling.impl.js +2 -1
- package/dist/auto-reply/reply/directive-handling.model.js +2 -1
- package/dist/auto-reply/reply/directive-handling.persist.js +2 -2
- package/dist/auto-reply/reply/get-reply-directives-apply.js +2 -0
- package/dist/auto-reply/reply/get-reply-inline-actions.js +25 -1
- package/dist/auto-reply/reply/model-selection.js +7 -4
- package/dist/auto-reply/reply/post-compaction-audit.js +3 -3
- package/dist/auto-reply/reply/post-compaction-context.js +3 -3
- package/dist/auto-reply/reply/session-reset-model.js +4 -4
- package/dist/auto-reply/reply/session.js +3 -3
- package/dist/auto-reply/skill-commands.js +3 -3
- package/dist/auto-reply/status.js +6 -6
- package/dist/brand.js +1 -1
- package/dist/browser/chrome.executables.js +3 -3
- package/dist/browser/chrome.js +3 -3
- package/dist/browser/chrome.profile-decoration.js +6 -6
- package/dist/browser/profiles-service.js +2 -2
- package/dist/browser/server-context.js +2 -2
- package/dist/browser/trash.js +4 -4
- package/dist/build-info.json +3 -3
- package/dist/canvas-host/a2ui/.bundle.hash +1 -1
- package/dist/canvas-host/server.js +4 -4
- package/dist/capabilities/catalog.js +3 -3
- package/dist/channel-web.js +2 -2
- package/dist/channels/plugins/catalog.js +3 -3
- package/dist/cli/browser-cli-extension.js +6 -6
- package/dist/cli/command-format.js +2 -2
- package/dist/cli/cron-cli/register.cron-simple.js +2 -2
- package/dist/cli/dns-cli.js +9 -9
- package/dist/cli/gateway-cli/dev.js +5 -5
- package/dist/cli/gateway-cli/run.js +10 -2
- package/dist/cli/hooks-cli.js +5 -5
- package/dist/cli/lightweight/gateway-runtime-probe.js +2 -2
- package/dist/cli/memory-cli.js +7 -7
- package/dist/cli/nodes-media-utils.js +2 -2
- package/dist/cli/plugins-cli.js +2 -2
- package/dist/cli/program/command-registry.js +1 -1
- package/dist/cli/program/register.onboard.js +2 -3
- package/dist/cli/program/register.wallet.js +16 -123
- package/dist/cli/providers-cli.js +1 -1
- package/dist/cli/sat-cli.js +129 -2
- package/dist/cli/secrets-cli.js +3 -3
- package/dist/cli/update-cli/update-command.js +260 -11
- package/dist/cli/windows-argv.js +2 -2
- package/dist/commands/auth-choice.apply.api-providers.js +3 -3
- package/dist/commands/dashboard-link.js +3 -3
- package/dist/commands/doctor-config-flow.js +1 -1
- package/dist/commands/doctor-install.js +5 -5
- package/dist/commands/doctor-legacy-config.js +2 -13
- package/dist/commands/doctor-memory-search.js +2 -2
- package/dist/commands/doctor-platform-notes.js +2 -2
- package/dist/commands/doctor-sandbox.js +2 -2
- package/dist/commands/doctor-state-integrity.js +23 -23
- package/dist/commands/doctor-workspace.js +3 -3
- package/dist/commands/doctor.js +3 -3
- package/dist/commands/managed-up.js +2 -2
- package/dist/commands/model-picker.js +6 -6
- package/dist/commands/models/auth.js +1 -1
- package/dist/commands/onboard-auth.config-gateways.js +1 -1
- package/dist/commands/onboard-auth.credentials.js +35 -11
- package/dist/commands/onboard-non-interactive/local/wallet-config.js +28 -27
- package/dist/commands/onboard-non-interactive/local.js +5 -1
- package/dist/commands/onboarding/plugin-install.js +2 -2
- package/dist/commands/sessions-cleanup.js +2 -2
- package/dist/commands/signal-install.js +6 -6
- package/dist/commands/status-all/channels.js +2 -2
- package/dist/commands/wallet.js +570 -1913
- package/dist/config/cache-utils.js +2 -2
- package/dist/config/config.js +2 -2
- package/dist/config/includes.js +5 -5
- package/dist/config/io.js +66 -4
- package/dist/config/paths.js +3 -3
- package/dist/config/sat-runtime-ids.js +3 -3
- package/dist/config/schema.help.js +8 -7
- package/dist/config/schema.labels.js +5 -4
- package/dist/config/sessions/disk-budget.js +6 -6
- package/dist/config/sessions/paths.js +2 -2
- package/dist/config/sessions/store.js +17 -17
- package/dist/config/sessions/transcript.js +4 -4
- package/dist/config/talk.js +2 -2
- package/dist/config/zod-schema.agent-runtime.js +1 -0
- package/dist/config/zod-schema.js +5 -0
- package/dist/control-ui/assets/{app-BXA4gPvN.js → app-bR-fO5ty.js} +1511 -1555
- package/dist/control-ui/assets/{config-CU5Zi8Dh.js → config-LeImukCW.js} +10 -10
- package/dist/control-ui/assets/{federation-D10Q4eP_.js → federation-DgA9e-Pn.js} +364 -422
- package/dist/control-ui/assets/{index-D2QbJhVg.js → index-ckCxHwjy.js} +3 -3
- package/dist/control-ui/assets/{mining-BdhMn18r.js → mining-BgHFh0ei.js} +134 -114
- package/dist/control-ui/assets/mining-commit-DFpB_4JN.js +2 -0
- package/dist/control-ui/assets/{open-external-url-DwSiwMPl.js → open-external-url-ulF1B-sA.js} +3 -3
- package/dist/control-ui/assets/{providers-QMn4w2_f.js → providers-CIx9fxc6.js} +2 -2
- package/dist/control-ui/assets/{wallet-C2g294QT.js → wallet-DvStR_AT.js} +259 -377
- package/dist/control-ui/index.html +1 -1
- package/dist/control-ui/version.json +1 -1
- package/dist/cron/service/store.js +2 -2
- package/dist/cron/store.js +6 -6
- package/dist/daemon/systemd-system.js +60 -54
- package/dist/discord/monitor/thread-bindings.state.js +2 -2
- package/dist/extensions/sat-mining/src/protocol-contract.js +43 -3
- package/dist/extensions/sat-mining/src/rpc-read.js +235 -67
- package/dist/extensions/sat-mining/src/sat-bond-layout.js +14 -11
- package/dist/extensions/sat-mining/src/signer-codec-manifest.js +406 -0
- package/dist/extensions/sat-mining/src/solana-submit.js +1038 -397
- package/dist/extensions/sat-mining/src/submission-ledger.js +349 -0
- package/dist/federation/auto-connect.js +98 -8
- package/dist/federation/marketplace-capability-adapter.js +33 -7
- package/dist/federation/marketplace-delivery-outbox.js +196 -0
- package/dist/federation/marketplace-delivery.js +351 -215
- package/dist/federation/marketplace-direct-payment.js +272 -141
- package/dist/federation/marketplace-escrow.js +607 -249
- package/dist/federation/marketplace-settlement-ledger.js +352 -0
- package/dist/federation/offers.js +9 -7
- package/dist/federation/peer-auth-v2.js +529 -0
- package/dist/gateway/a2a-http.js +425 -96
- package/dist/gateway/a2a-settlement.js +259 -153
- package/dist/gateway/a2a-task-store.js +524 -0
- package/dist/gateway/auth.js +38 -2
- package/dist/gateway/config-reload.js +46 -33
- package/dist/gateway/control-ui-boot-check.js +6 -6
- package/dist/gateway/control-ui-login.js +7 -7
- package/dist/gateway/control-ui.js +9 -9
- package/dist/gateway/durable-capacity.js +31 -0
- package/dist/gateway/federation-http.js +540 -135
- package/dist/gateway/federation-marketplace-run-store.js +397 -0
- package/dist/gateway/federation-marketplace.js +535 -230
- package/dist/gateway/hooks-mapping.js +4 -4
- package/dist/gateway/mutating-admin-rpc-rate-limit.js +5 -0
- package/dist/gateway/security-path.js +1 -1
- package/dist/gateway/server-discovery.js +2 -2
- package/dist/gateway/server-http.js +1088 -1209
- package/dist/gateway/server-methods/chat.js +3 -3
- package/dist/gateway/server-methods/models.js +1 -1
- package/dist/gateway/server-methods/secrets.js +40 -0
- package/dist/gateway/server-methods/sessions.js +6 -6
- package/dist/gateway/server-methods/usage.js +2 -2
- package/dist/gateway/server-methods.js +1 -2
- package/dist/gateway/server-reload-handlers.js +196 -66
- package/dist/gateway/server-runtime-config.js +1 -1
- package/dist/gateway/server-secrets-runtime.js +288 -0
- package/dist/gateway/server.impl.js +73 -42
- package/dist/gateway/session-compaction-checkpoints.js +4 -4
- package/dist/gateway/session-utils.fs.js +24 -24
- package/dist/gateway/session-utils.js +6 -6
- package/dist/gateway/sessions-patch.js +3 -5
- package/dist/hooks/bundled-dir.js +4 -4
- package/dist/hooks/gmail-setup-utils.js +6 -6
- package/dist/hooks/import-url.js +2 -2
- package/dist/hooks/loader.js +4 -4
- package/dist/hooks/workspace.js +7 -7
- package/dist/index.js +4 -2
- package/dist/infra/archive.js +17 -17
- package/dist/infra/boundary-file-read.js +2 -2
- package/dist/infra/boundary-path.js +15 -15
- package/dist/infra/brew.js +2 -2
- package/dist/infra/control-ui-assets.js +14 -14
- package/dist/infra/device-auth-store.js +8 -8
- package/dist/infra/device-identity.js +8 -8
- package/dist/infra/dotenv.js +2 -2
- package/dist/infra/exec-approvals.js +8 -8
- package/dist/infra/exec-command-resolution.js +4 -4
- package/dist/infra/exec-safe-bin-trust.js +2 -2
- package/dist/infra/fased-root.js +5 -5
- package/dist/infra/fasedhub.js +64 -1
- package/dist/infra/fs-safe.js +14 -14
- package/dist/infra/gateway-lock.js +11 -11
- package/dist/infra/git-commit.js +5 -5
- package/dist/infra/git-root.js +4 -4
- package/dist/infra/hosted-runtime-artifact.js +3 -3
- package/dist/infra/is-main.js +2 -2
- package/dist/infra/json-file.js +7 -7
- package/dist/infra/local-source-paired-update.js +538 -0
- package/dist/infra/managed-runtime-bootstrap.js +6 -6
- package/dist/infra/outbound/delivery-queue.js +16 -16
- package/dist/infra/path-env.js +3 -3
- package/dist/infra/ports-lsof.js +4 -4
- package/dist/infra/provider-usage.auth.js +3 -3
- package/dist/infra/restart.js +41 -10
- package/dist/infra/safe-open-sync.js +2 -2
- package/dist/infra/session-cost-usage.js +9 -9
- package/dist/infra/shell-env.js +2 -2
- package/dist/infra/state-migrations.fs.js +6 -6
- package/dist/infra/state-migrations.js +23 -23
- package/dist/infra/tmp-fased-dir.js +6 -6
- package/dist/infra/update-runner.js +96 -0
- package/dist/infra/widearea-dns.js +3 -3
- package/dist/infra/wsl.js +2 -2
- package/dist/light-update-status.js +11 -11
- package/dist/line/accounts.js +2 -2
- package/dist/line/download.js +2 -2
- package/dist/logging/config.js +3 -3
- package/dist/logging/logger.js +7 -7
- package/dist/managed/federation.js +3 -3
- package/dist/managed/tunnel.js +3 -3
- package/dist/media/store.js +13 -13
- package/dist/media-understanding/runner.js +3 -3
- package/dist/memory/backend-config.js +2 -1
- package/dist/memory/embeddings.js +2 -2
- package/dist/memory/internal.js +10 -10
- package/dist/memory/manager-sync-ops.js +7 -7
- package/dist/memory/repair-executor.js +17 -17
- package/dist/memory/session-files.js +16 -2
- package/dist/node-host/invoke-system-run-plan.js +8 -8
- package/dist/node-host/invoke.js +2 -2
- package/dist/node-host/runner.js +3 -3
- package/dist/pairing/pairing-store.js +3 -3
- package/dist/plugin-sdk/{accounts-DxblobYM.js → accounts-Brn3-uWN.js} +1 -1
- package/dist/plugin-sdk/{actions-D4UNimWa.js → actions-DRu135h0.js} +2 -2
- package/dist/plugin-sdk/{active-listener-CFuy106s.js → active-listener-B_NMtgPL.js} +1 -1
- package/dist/plugin-sdk/{audio-preflight-Bv0hUOWL.js → audio-preflight-BYxAt775.js} +27 -27
- package/dist/plugin-sdk/{audit-Cs0K7LBp.js → audit-C5d2z5U1.js} +1 -1
- package/dist/plugin-sdk/{channel-access-D5aHoTY8.js → channel-access-CcFRdRPO.js} +1 -1
- package/dist/plugin-sdk/{channel-access-configure-Dvch9mMQ.js → channel-access-configure-DaI5wevh.js} +1 -1
- package/dist/plugin-sdk/channel-plugin-common.d.ts +3 -3
- package/dist/plugin-sdk/channel-plugin-common.js +4 -4
- package/dist/plugin-sdk/{chunk-B0FUGc5A.js → chunk-CyoyqpbG.js} +1 -1
- package/dist/plugin-sdk/{chunk-items-Bk--NU3E.js → chunk-items-DIRHG2gJ.js} +1 -1
- package/dist/plugin-sdk/command-status.d.ts +2 -2
- package/dist/plugin-sdk/command-status.js +27 -27
- package/dist/plugin-sdk/{commands-registry-DmDRhRAU.js → commands-registry-BfqdEWdF.js} +2 -2
- package/dist/plugin-sdk/{common-CfUOowOp.js → common-HCcXU-NR.js} +1 -1
- package/dist/plugin-sdk/{config-mxULv0fQ.js → config-DlWVeLFB.js} +7113 -6877
- package/dist/plugin-sdk/{config-schema-CGW6J3Pv.js → config-schema-BOT7kHb6.js} +6779 -3696
- package/dist/plugin-sdk/device-pair.d.ts +1 -1
- package/dist/plugin-sdk/discord.d.ts +5 -5
- package/dist/plugin-sdk/discord.js +19 -19
- package/dist/plugin-sdk/file-lock.js +35 -4
- package/dist/plugin-sdk/{filesystem-manager-ZoHU8oA7.js → filesystem-manager-DoI8IBNF.js} +1 -1
- package/dist/plugin-sdk/{group-mentions-BHRff6j8.d.ts → group-mentions-q3hUTCt7.d.ts} +1 -1
- package/dist/plugin-sdk/{handle-action-DbVCF9HZ.js → handle-action-hvwapMKg.js} +13 -13
- package/dist/plugin-sdk/{helpers-BXbdgs5H.js → helpers-DMBU4oTo.js} +1 -1
- package/dist/plugin-sdk/{image-ops-D7jPxaph.js → image-ops-DQi0n_Tt.js} +1 -1
- package/dist/plugin-sdk/index.d.ts +9 -27
- package/dist/plugin-sdk/index.js +29 -29
- package/dist/plugin-sdk/json-store.js +6 -6
- package/dist/plugin-sdk/{login-BRxww6nv.js → login-Ca583Okc.js} +3 -3
- package/dist/plugin-sdk/{login-qr-DJXsk9XH.js → login-qr-CGciBSNP.js} +3 -3
- package/dist/plugin-sdk/{manager-Y2S_5IlI.js → manager-BegWxNYX.js} +4 -4
- package/dist/plugin-sdk/{markdown-tables-DHLFQqaF.js → markdown-tables-DCOXEUfZ.js} +1 -1
- package/dist/plugin-sdk/{media-Czo4carg.js → media-CAdQF5TG.js} +47 -47
- package/dist/plugin-sdk/{memory-search-DTjIMw8J.js → memory-search-CrFPOeTH.js} +4 -1
- package/dist/plugin-sdk/{monitor-B9Cmt_Je.js → monitor-BCFDaqJ-.js} +31 -31
- package/dist/plugin-sdk/{monitor-B71iwAgC.js → monitor-BgbjRiPM.js} +32 -32
- package/dist/plugin-sdk/{openresponses-http-Du4GMmwo.js → openresponses-http-DFf87SMM.js} +27 -27
- package/dist/plugin-sdk/{optional-runtime-dependency-DdMbxS24.js → optional-runtime-dependency-0dznm3ZO.js} +1 -1
- package/dist/plugin-sdk/{outbound-Db8mRN6P.js → outbound-BgVDdHxx.js} +6 -6
- package/dist/plugin-sdk/{outbound-attachment-BqFjcypb.js → outbound-attachment-QqTnIhZL.js} +2 -2
- package/dist/plugin-sdk/{pairing-message-BMkkS038.d.ts → pairing-message-CS-9BHug.d.ts} +2 -2
- package/dist/plugin-sdk/{pairing-message-B3GC_7CD.js → pairing-message-D0CoLHRJ.js} +1 -1
- package/dist/plugin-sdk/{pairing-store-CQikgIbw.js → pairing-store-BChahgI2.js} +2 -2
- package/dist/plugin-sdk/{pi-model-discovery-CY3m5zJP.js → pi-model-discovery-BX_JLcEJ.js} +1 -1
- package/dist/plugin-sdk/{pi-model-discovery-runtime-DSh1T2pn.js → pi-model-discovery-runtime-DxTMz_XF.js} +2 -2
- package/dist/plugin-sdk/provider-web-search-config-contract.d.ts +2 -2
- package/dist/plugin-sdk/{pw-ai-AFUqn-tQ.js → pw-ai-natjh3rm.js} +3 -3
- package/dist/plugin-sdk/{qmd-manager-DSyx77KY.js → qmd-manager-CgRaTxUq.js} +1 -1
- package/dist/plugin-sdk/{query-expansion-CAI2kKLQ.js → query-expansion-BG3FgzF7.js} +14 -3
- package/dist/plugin-sdk/{replies-Fzh9PhXl.js → replies-ChYo5ZQM.js} +2 -2
- package/dist/plugin-sdk/{resolve-outbound-target-KpF3co6T.js → resolve-outbound-target-D-KGgHJZ.js} +1 -1
- package/dist/plugin-sdk/{resolve-route-CDl74O19.js → resolve-route-C8tvQm8Z.js} +1 -1
- package/dist/plugin-sdk/{retry-policy-D4uiqUOG.js → retry-policy-C6SjKjQA.js} +1 -1
- package/dist/plugin-sdk/{runtime-Bb6KRnCC.js → runtime-2B9_WKoI.js} +8 -3
- package/dist/plugin-sdk/{runtime-dependency-DLDmB94f.js → runtime-dependency-DczkvPxn.js} +2 -2
- package/dist/plugin-sdk/sat-runtime.d.ts +549 -309
- package/dist/plugin-sdk/sat-runtime.js +29 -29
- package/dist/plugin-sdk/{send-C4k4JuJN.js → send-B6KkGKZD.js} +6 -6
- package/dist/plugin-sdk/{send-D-uDKGIq.js → send-CFzGKJXo.js} +6 -6
- package/dist/plugin-sdk/{send-Du8ykP1k.js → send-DCO441U7.js} +8 -8
- package/dist/plugin-sdk/{send-5nFktR1n.js → send-gDoGA-7e.js} +5 -5
- package/dist/plugin-sdk/{send-Bfzf11VY.js → send-q9HXrSwK.js} +7 -7
- package/dist/plugin-sdk/{session-CS-Td9EM.js → session-DHD4ZKyG.js} +2 -2
- package/dist/plugin-sdk/{skill-commands-CFAlXWHk.js → skill-commands-FKEPLS7T.js} +201 -26
- package/dist/plugin-sdk/{slack-D_bbE6Yd.js → slack-BUTt8cxK.js} +39 -39
- package/dist/plugin-sdk/{slack-actions-BcMkNv1Z.js → slack-actions-DhbMcp8M.js} +11 -11
- package/dist/plugin-sdk/slack.d.ts +5 -5
- package/dist/plugin-sdk/slack.js +9 -9
- package/dist/plugin-sdk/{sticker-cache-Dqo9nRhi.js → sticker-cache-D0wHVqjt.js} +3 -3
- package/dist/plugin-sdk/{tables-bFXyRpkx.js → tables-B3jr0Pvq.js} +1 -1
- package/dist/plugin-sdk/{targets-DH7a9lKB.js → targets-CtSbTxYX.js} +1 -1
- package/dist/plugin-sdk/{telegram-actions-Cnag7JTH.js → telegram-actions-CC_qN4v0.js} +30 -30
- package/dist/plugin-sdk/telegram.d.ts +5 -5
- package/dist/plugin-sdk/telegram.js +8 -8
- package/dist/plugin-sdk/{thread-bindings.lifecycle-CgOvvdw8.js → thread-bindings.lifecycle-QbkNl-m6.js} +3 -3
- package/dist/plugin-sdk/{thread-bindings.state-CQpk44Hq.js → thread-bindings.state-D9hg_M1c.js} +2 -2
- package/dist/plugin-sdk/tmp-fased-dir-xOTGlzBE.d.ts +486 -0
- package/dist/plugin-sdk/{types-CLIvQaVg.d.ts → types-C3avNytw.d.ts} +3 -4
- package/dist/plugin-sdk/{types-DyhXnYUr.d.ts → types-D0kDG8l0.d.ts} +3 -2
- package/dist/plugin-sdk/{types-BYDL6dQj.d.ts → types-DWfbSAaM.d.ts} +2 -2
- package/dist/plugin-sdk/{types.plugin-_fbu40Re.d.ts → types.plugin-BcSBq42x.d.ts} +8 -3
- package/dist/plugin-sdk/{web-BInKQkc4.js → web-BXsJ1vqe.js} +32 -32
- package/dist/plugin-sdk/{whatsapp-actions-BtSc2CBv.js → whatsapp-actions-Bj_rOncr.js} +13 -13
- package/dist/plugin-sdk/{whatsapp-heartbeat-Bg9TuYzl.js → whatsapp-heartbeat-B1WHkJIg.js} +2 -2
- package/dist/plugin-sdk/whatsapp.d.ts +5 -5
- package/dist/plugin-sdk/whatsapp.js +12 -12
- package/dist/plugin-sdk/{wsl-C4DGFkOd.js → wsl-J-HscWxS.js} +3 -3
- package/dist/plugins/bundled-dir.js +3 -3
- package/dist/plugins/discovery.js +11 -11
- package/dist/plugins/loader.js +13 -13
- package/dist/plugins/manifest-registry.js +2 -2
- package/dist/plugins/manifest.js +4 -4
- package/dist/plugins/path-safety.js +3 -3
- package/dist/plugins/status-cache.js +9 -9
- package/dist/plugins/update.js +3 -3
- package/dist/plugins/web-search-install-catalog.js +3 -3
- package/dist/providers/refresh.js +1 -1
- package/dist/providers/registry.js +1 -1
- package/dist/secrets/apply.js +17 -17
- package/dist/secrets/audit.js +11 -11
- package/dist/secrets/runtime.js +1 -1
- package/dist/secrets/shared.js +9 -9
- package/dist/security/scan-paths.js +2 -2
- package/dist/shared/config-eval.js +2 -2
- package/dist/shared/pid-alive.js +2 -2
- package/dist/tasks/standing-orders.js +5 -5
- package/dist/tasks/task-flow-registry.js +5 -5
- package/dist/tasks/task-registry.js +5 -5
- package/dist/tasks/workflow-definitions.js +6 -6
- package/dist/telegram/token.js +5 -5
- package/dist/utils.js +5 -5
- package/dist/wallet/external-submission-ledger.js +376 -0
- package/dist/wallet/legacy-embedded-keystore.js +40 -0
- package/dist/wallet/local-socket-signer-archive.js +30 -0
- package/dist/wallet/local-socket-signer-lifecycle.js +97 -0
- package/dist/wallet/local-socket-signer-policy.js +166 -0
- package/dist/wallet/local-socket-signer-protocol.js +561 -118
- package/dist/wallet/native-signer-wallet-id.js +25 -0
- package/dist/wallet/providers/alchemy-adapter.js +3 -0
- package/dist/wallet/providers/local-socket-signer-adapter.js +382 -107
- package/dist/wallet/providers/turnkey-adapter.js +292 -34
- package/dist/wallet/providers/wallet-standard-adapter.js +100 -0
- package/dist/wallet/signer-network-admin.js +184 -0
- package/dist/wallet/signer-protocol-v2.generated.js +90 -0
- package/dist/wallet/solana-bond-signing.js +157 -69
- package/dist/wallet/solana-reviewed-transaction.js +375 -0
- package/dist/wallet/solana-swap.js +520 -64
- package/dist/wallet/solana-transaction-inspection.js +106 -4
- package/dist/wallet/solana-trigger.js +658 -320
- package/dist/wallet/turnkey-reviewed-state.js +370 -0
- package/dist/wallet/wallet-approval-auth.js +20 -31
- package/dist/wallet/wallet-atomic-state.js +53 -0
- package/dist/wallet/wallet-audit-log.js +5 -5
- package/dist/wallet/wallet-canary.js +0 -1
- package/dist/wallet/wallet-inbound-events.js +19 -14
- package/dist/wallet/wallet-observability.js +5 -5
- package/dist/wallet/wallet-policy-simulation.js +1 -1
- package/dist/wallet/wallet-policy.js +95 -41
- package/dist/wallet/wallet-provider-capabilities.js +28 -4
- package/dist/wallet/wallet-provider-registry.js +64 -51
- package/dist/wallet/wallet-provider-resolver.js +62 -63
- package/dist/wallet/wallet-runtime-config.js +31 -44
- package/dist/wallet/wallet-secrets-store.js +44 -22
- package/dist/wallet/wallet-send-approvals.js +1894 -592
- package/dist/wallet/wallet-send-execution-ledger.js +292 -0
- package/dist/wallet/wallet-settlement-links.js +17 -16
- package/dist/wallet/wallet-standard-review.js +353 -0
- package/dist/wallet/wallet-status.js +0 -9
- package/dist/wallet/wallet-task-ledger.js +16 -2
- package/dist/web/accounts.js +3 -3
- package/dist/web/auth-store.js +18 -18
- package/dist/web/auto-reply/heartbeat-runner.js +1 -1
- package/dist/web/auto-reply/monitor.js +2 -2
- package/dist/web/session.js +4 -4
- package/dist/wizard/host-security-capability.js +15 -24
- package/dist/wizard/onboarding.finalize.js +63 -184
- package/dist/wizard/onboarding.host-security.js +118 -1368
- package/dist/wizard/onboarding.js +180 -242
- package/dist/wizard/onboarding.wallet.js +323 -829
- package/extensions/acpx/package.json +1 -1
- package/extensions/bluebubbles/package.json +1 -1
- package/extensions/copilot-proxy/package.json +1 -1
- package/extensions/diagnostics-otel/package.json +1 -1
- package/extensions/fased-federation/package.json +1 -1
- package/extensions/google-gemini-cli-auth/package.json +1 -1
- package/extensions/imessage/package.json +1 -1
- package/extensions/irc/package.json +1 -1
- package/extensions/line/package.json +1 -1
- package/extensions/llm-task/package.json +1 -1
- package/extensions/lobster/package.json +1 -1
- package/extensions/matrix/CHANGELOG.md +18 -0
- package/extensions/matrix/package.json +1 -1
- package/extensions/mattermost/package.json +1 -1
- package/extensions/memory-core/package.json +2 -2
- package/extensions/memory-lancedb/package.json +1 -1
- package/extensions/minimax-portal-auth/package.json +1 -1
- package/extensions/msteams/CHANGELOG.md +18 -0
- package/extensions/msteams/package.json +1 -1
- package/extensions/nextcloud-talk/package.json +1 -1
- package/extensions/nostr/CHANGELOG.md +18 -0
- package/extensions/nostr/package.json +1 -1
- package/extensions/open-prose/package.json +1 -1
- package/extensions/sat-mining/index.ts +498 -153
- package/extensions/sat-mining/package.json +1 -1
- package/extensions/sat-mining/signer-codec-schema.v1.json +374 -0
- package/extensions/sat-mining/src/audit-store.ts +50 -5
- package/extensions/sat-mining/src/claim-service.ts +85 -14
- package/extensions/sat-mining/src/config.ts +9 -0
- package/extensions/sat-mining/src/cycle-progress.ts +16 -8
- package/extensions/sat-mining/src/epoch-service.ts +9 -5
- package/extensions/sat-mining/src/gateway-runner.ts +21 -1
- package/extensions/sat-mining/src/protocol-contract.ts +45 -1
- package/extensions/sat-mining/src/recovery-service.ts +312 -0
- package/extensions/sat-mining/src/round-watcher.ts +391 -81
- package/extensions/sat-mining/src/rpc-read.ts +330 -89
- package/extensions/sat-mining/src/runtime.ts +14 -0
- package/extensions/sat-mining/src/sat-bond-layout.ts +6 -0
- package/extensions/sat-mining/src/signer-codec-manifest.ts +429 -0
- package/extensions/sat-mining/src/solana-submit.ts +1199 -428
- package/extensions/sat-mining/src/submission-ledger.ts +433 -0
- package/extensions/signal/package.json +1 -1
- package/extensions/synology-chat/package.json +1 -1
- package/extensions/tlon/package.json +1 -1
- package/extensions/twitch/CHANGELOG.md +18 -0
- package/extensions/twitch/package.json +1 -1
- package/extensions/voice-call/CHANGELOG.md +18 -0
- package/extensions/voice-call/package.json +1 -1
- package/extensions/zalo/CHANGELOG.md +18 -0
- package/extensions/zalo/package.json +1 -1
- package/extensions/zalouser/CHANGELOG.md +18 -0
- package/extensions/zalouser/package.json +1 -1
- package/install.sh +4955 -0
- package/package.json +24 -5
- package/scripts/fased-host-updater.mjs +1421 -0
- package/scripts/fased-host-updaterctl.mjs +186 -0
- package/scripts/fased-managed-service.sh +36 -0
- package/scripts/fased-managed-updater.mjs +3065 -156
- package/scripts/fased-signer-enroll-hosting.sh +275 -0
- package/scripts/fased-signer-network-hosting.sh +143 -0
- package/scripts/fased-signer-owner-policy.mjs +1430 -0
- package/scripts/fased-signer-policy-hosting.sh +50 -0
- package/scripts/fased-signer-policy-local.sh +26 -0
- package/scripts/hosted-release-manifest.mjs +185 -0
- package/scripts/install-fased-signerd.sh +217 -0
- package/scripts/install-managed-runtime.mjs +212 -10
- package/scripts/managed-runtime-layout.mjs +75 -3
- package/scripts/migrate-hosted-signer-v2.mjs +58 -0
- package/scripts/start-managed.sh +169 -360
- package/dist/control-ui/assets/mining-commit-R-6nu87p.js +0 -2
- package/dist/plugin-sdk/tmp-fased-dir-BdFQvrvw.d.ts +0 -77
- package/dist/wallet/local-socket-signer-broker.js +0 -224
- package/dist/wallet/local-socket-signer-custody.js +0 -42
- package/dist/wallet/providers/embedded-keystore-adapter.js +0 -402
- package/dist/wallet/providers/privy-adapter.js +0 -80
- package/dist/wallet/wallet-custody.js +0 -1836
|
@@ -0,0 +1,1421 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { execFile } from "node:child_process";
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
import fs from "node:fs";
|
|
6
|
+
import fsp from "node:fs/promises";
|
|
7
|
+
import net from "node:net";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import { pipeline } from "node:stream/promises";
|
|
10
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
11
|
+
import { promisify } from "node:util";
|
|
12
|
+
|
|
13
|
+
const execFileAsync = promisify(execFile);
|
|
14
|
+
|
|
15
|
+
const RELEASE_BASE = "https://github.com/fased-ai/fased/releases/download";
|
|
16
|
+
const RELEASE_REPOSITORY = "fased-ai/fased";
|
|
17
|
+
const RELEASE_WORKFLOW = "fased-ai/fased/.github/workflows/hosted-runtime-release.yml";
|
|
18
|
+
const SOCKET_PATH = "/run/fased-host-updater/request.sock";
|
|
19
|
+
const STATE_DIR = "/var/lib/fased-host-updater";
|
|
20
|
+
const SIGNER_PATH = "/opt/fased/signer/fased-signerd";
|
|
21
|
+
const SIGNER_STATE_DB_PATH = "/var/lib/fased-signerd/state.db";
|
|
22
|
+
const SIGNER_UNIT_PATH = "/etc/systemd/system/fased-signerd.service";
|
|
23
|
+
const VERSION_PATH = path.join(STATE_DIR, "signer-version");
|
|
24
|
+
const CHANNEL_PATH = "/etc/fased/host-updater-channel";
|
|
25
|
+
const JOURNAL_PATH = path.join(STATE_DIR, "active-signer-transaction.json");
|
|
26
|
+
const ROLLBACK_FLOOR_PATH = path.join(STATE_DIR, "rollback-floor");
|
|
27
|
+
const GATEWAY_GATE_PATH = path.join(STATE_DIR, "gateway-update-gate");
|
|
28
|
+
const SIGNER_GATE_PATH = "/var/lib/fased-signer-update-gate/active";
|
|
29
|
+
const TRANSACTIONS_DIR = path.join(STATE_DIR, "transactions");
|
|
30
|
+
const MAX_REQUEST_BYTES = 4096;
|
|
31
|
+
const REQUEST_TIMEOUT_MS = 20 * 60_000;
|
|
32
|
+
const JOURNAL_SCHEMA_VERSION = 2;
|
|
33
|
+
const PROTOCOL_SCHEMA_VERSION = 2;
|
|
34
|
+
const TRANSACTION_ID_PATTERN =
|
|
35
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
36
|
+
const TRANSACTION_OPERATIONS = new Set([
|
|
37
|
+
"prepareRelease",
|
|
38
|
+
"activateRelease",
|
|
39
|
+
"authorizeGatewayRelease",
|
|
40
|
+
"gateGatewayRelease",
|
|
41
|
+
"commitRelease",
|
|
42
|
+
"rollbackRelease",
|
|
43
|
+
]);
|
|
44
|
+
const TRANSACTION_PHASES = new Set([
|
|
45
|
+
"prepared",
|
|
46
|
+
"snapshotting",
|
|
47
|
+
"activating",
|
|
48
|
+
"active",
|
|
49
|
+
"gateway-authorized",
|
|
50
|
+
"committing",
|
|
51
|
+
"rolling-back",
|
|
52
|
+
"restored",
|
|
53
|
+
]);
|
|
54
|
+
|
|
55
|
+
export const PRE_V2_HOSTING_MIGRATION_MESSAGE = [
|
|
56
|
+
"This hosted installation needs the one-time signer-v2 security migration before it can update.",
|
|
57
|
+
"From a VPS provider console or a root SSH session, follow the pre-execution verified tagged repair procedure at https://docs.fased.ai/install/vps.",
|
|
58
|
+
"After gh attestation verify succeeds, execute that verified install.sh asset with --repair-hosting --release vX.Y.Z.",
|
|
59
|
+
"Never run /home/app/fased/install.sh with sudo or as root.",
|
|
60
|
+
"The current Gateway, signer, wallets, and persistent state were left unchanged.",
|
|
61
|
+
].join(" ");
|
|
62
|
+
|
|
63
|
+
const DEFAULT_PATHS = Object.freeze({
|
|
64
|
+
socketPath: SOCKET_PATH,
|
|
65
|
+
stateDir: STATE_DIR,
|
|
66
|
+
signerPath: SIGNER_PATH,
|
|
67
|
+
signerStateDBPath: SIGNER_STATE_DB_PATH,
|
|
68
|
+
signerUnitPath: SIGNER_UNIT_PATH,
|
|
69
|
+
versionPath: VERSION_PATH,
|
|
70
|
+
channelPath: CHANNEL_PATH,
|
|
71
|
+
journalPath: JOURNAL_PATH,
|
|
72
|
+
rollbackFloorPath: ROLLBACK_FLOOR_PATH,
|
|
73
|
+
gatewayGatePath: GATEWAY_GATE_PATH,
|
|
74
|
+
signerGatePath: SIGNER_GATE_PATH,
|
|
75
|
+
transactionsDir: TRANSACTIONS_DIR,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
export function parseReleaseVersion(value) {
|
|
79
|
+
const version = String(value ?? "")
|
|
80
|
+
.trim()
|
|
81
|
+
.replace(/^v/, "");
|
|
82
|
+
if (!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z]+(?:[.-][0-9A-Za-z]+)*)?$/.test(version)) {
|
|
83
|
+
throw new Error("version must be an exact semantic release version");
|
|
84
|
+
}
|
|
85
|
+
return version;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function parseTransactionId(value) {
|
|
89
|
+
const transactionId = String(value ?? "").trim();
|
|
90
|
+
if (!TRANSACTION_ID_PATTERN.test(transactionId)) {
|
|
91
|
+
throw new Error("transactionId must be a UUIDv4");
|
|
92
|
+
}
|
|
93
|
+
return transactionId.toLowerCase();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function parseUpdateRequest(value) {
|
|
97
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
98
|
+
throw new Error("request must be an object");
|
|
99
|
+
}
|
|
100
|
+
if (value.schemaVersion === 1) {
|
|
101
|
+
throw new Error(PRE_V2_HOSTING_MIGRATION_MESSAGE);
|
|
102
|
+
}
|
|
103
|
+
const keys = Object.keys(value).toSorted();
|
|
104
|
+
if (keys.join(",") !== "op,schemaVersion,transactionId,version") {
|
|
105
|
+
throw new Error("request contains unsupported fields");
|
|
106
|
+
}
|
|
107
|
+
if (value.schemaVersion !== PROTOCOL_SCHEMA_VERSION || !TRANSACTION_OPERATIONS.has(value.op)) {
|
|
108
|
+
throw new Error("unsupported updater transaction request");
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
schemaVersion: PROTOCOL_SCHEMA_VERSION,
|
|
112
|
+
op: value.op,
|
|
113
|
+
transactionId: parseTransactionId(value.transactionId),
|
|
114
|
+
version: parseReleaseVersion(value.version),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function compareVersions(left, right) {
|
|
119
|
+
const parse = (value) => {
|
|
120
|
+
const match = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/.exec(value);
|
|
121
|
+
if (!match) {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
return { core: match.slice(1, 4).map(Number), prerelease: match[4]?.split(".") ?? [] };
|
|
125
|
+
};
|
|
126
|
+
const a = parse(left);
|
|
127
|
+
const b = parse(right);
|
|
128
|
+
if (!a || !b) {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
for (let index = 0; index < 3; index += 1) {
|
|
132
|
+
if (a.core[index] !== b.core[index]) {
|
|
133
|
+
return a.core[index] < b.core[index] ? -1 : 1;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (a.prerelease.length === 0 || b.prerelease.length === 0) {
|
|
137
|
+
return a.prerelease.length === b.prerelease.length ? 0 : a.prerelease.length === 0 ? 1 : -1;
|
|
138
|
+
}
|
|
139
|
+
const length = Math.max(a.prerelease.length, b.prerelease.length);
|
|
140
|
+
for (let index = 0; index < length; index += 1) {
|
|
141
|
+
const l = a.prerelease[index];
|
|
142
|
+
const r = b.prerelease[index];
|
|
143
|
+
if (l === r) {
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
if (l === undefined || r === undefined) {
|
|
147
|
+
return l === undefined ? -1 : 1;
|
|
148
|
+
}
|
|
149
|
+
const ln = /^\d+$/.test(l);
|
|
150
|
+
const rn = /^\d+$/.test(r);
|
|
151
|
+
if (ln && rn) {
|
|
152
|
+
return Number(l) < Number(r) ? -1 : 1;
|
|
153
|
+
}
|
|
154
|
+
if (ln !== rn) {
|
|
155
|
+
return ln ? -1 : 1;
|
|
156
|
+
}
|
|
157
|
+
return l < r ? -1 : 1;
|
|
158
|
+
}
|
|
159
|
+
return 0;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function releaseArchitecture() {
|
|
163
|
+
if (process.platform !== "linux") {
|
|
164
|
+
throw new Error("host updater supports Linux only");
|
|
165
|
+
}
|
|
166
|
+
if (process.arch === "x64") {
|
|
167
|
+
return "amd64";
|
|
168
|
+
}
|
|
169
|
+
if (process.arch === "arm64") {
|
|
170
|
+
return "arm64";
|
|
171
|
+
}
|
|
172
|
+
throw new Error(`unsupported host architecture: ${process.arch}`);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async function fixedExecutable(candidates, label) {
|
|
176
|
+
for (const candidate of candidates) {
|
|
177
|
+
try {
|
|
178
|
+
const resolved = await fsp.realpath(candidate);
|
|
179
|
+
const stat = await fsp.stat(resolved);
|
|
180
|
+
if (!stat.isFile() || stat.uid !== 0 || (stat.mode & 0o022) !== 0) {
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
await fsp.access(resolved, fs.constants.X_OK);
|
|
184
|
+
return resolved;
|
|
185
|
+
} catch {
|
|
186
|
+
// Try the next root-controlled system path.
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
throw new Error(`${label} is not installed in a root-controlled system path`);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async function fsyncDirectory(directory) {
|
|
193
|
+
const handle = await fsp.open(directory, "r");
|
|
194
|
+
try {
|
|
195
|
+
await handle.sync();
|
|
196
|
+
} finally {
|
|
197
|
+
await handle.close();
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async function atomicWriteFileDurable(targetPath, content, mode = 0o600) {
|
|
202
|
+
await fsp.mkdir(path.dirname(targetPath), { recursive: true, mode: 0o700 });
|
|
203
|
+
const temporaryPath = `${targetPath}.tmp-${process.pid}-${Date.now()}`;
|
|
204
|
+
const handle = await fsp.open(temporaryPath, "wx", mode);
|
|
205
|
+
try {
|
|
206
|
+
await handle.writeFile(content);
|
|
207
|
+
await handle.sync();
|
|
208
|
+
} finally {
|
|
209
|
+
await handle.close();
|
|
210
|
+
}
|
|
211
|
+
await fsp.chmod(temporaryPath, mode);
|
|
212
|
+
await fsp.rename(temporaryPath, targetPath);
|
|
213
|
+
await fsyncDirectory(path.dirname(targetPath));
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async function atomicCopyFileDurable(sourcePath, targetPath, metadata = {}) {
|
|
217
|
+
await fsp.mkdir(path.dirname(targetPath), { recursive: true, mode: 0o700 });
|
|
218
|
+
const temporaryPath = `${targetPath}.tmp-${process.pid}-${Date.now()}`;
|
|
219
|
+
await fsp.copyFile(sourcePath, temporaryPath, fs.constants.COPYFILE_EXCL);
|
|
220
|
+
await fsp.chmod(temporaryPath, metadata.mode ?? 0o600);
|
|
221
|
+
if (Number.isInteger(metadata.uid) && Number.isInteger(metadata.gid)) {
|
|
222
|
+
await fsp.chown(temporaryPath, metadata.uid, metadata.gid);
|
|
223
|
+
}
|
|
224
|
+
const handle = await fsp.open(temporaryPath, "r");
|
|
225
|
+
try {
|
|
226
|
+
await handle.sync();
|
|
227
|
+
} finally {
|
|
228
|
+
await handle.close();
|
|
229
|
+
}
|
|
230
|
+
await fsp.rename(temporaryPath, targetPath);
|
|
231
|
+
await fsyncDirectory(path.dirname(targetPath));
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async function download(url, destination) {
|
|
235
|
+
const response = await fetch(url, {
|
|
236
|
+
headers: { "cache-control": "no-cache" },
|
|
237
|
+
redirect: "follow",
|
|
238
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
239
|
+
});
|
|
240
|
+
if (!response.ok || !response.body) {
|
|
241
|
+
throw new Error(`official release download failed (${response.status})`);
|
|
242
|
+
}
|
|
243
|
+
await pipeline(response.body, fs.createWriteStream(destination, { mode: 0o600 }));
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async function sha256(filePath) {
|
|
247
|
+
const hash = createHash("sha256");
|
|
248
|
+
await pipeline(fs.createReadStream(filePath), hash);
|
|
249
|
+
return hash.digest("hex");
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async function verifyReleaseAsset(assetPath, version, stateDir) {
|
|
253
|
+
const gh = await fixedExecutable(["/usr/bin/gh", "/usr/local/bin/gh"], "GitHub CLI");
|
|
254
|
+
await execFileAsync(
|
|
255
|
+
gh,
|
|
256
|
+
[
|
|
257
|
+
"attestation",
|
|
258
|
+
"verify",
|
|
259
|
+
assetPath,
|
|
260
|
+
"--repo",
|
|
261
|
+
RELEASE_REPOSITORY,
|
|
262
|
+
"--signer-workflow",
|
|
263
|
+
RELEASE_WORKFLOW,
|
|
264
|
+
"--source-ref",
|
|
265
|
+
`refs/tags/v${version}`,
|
|
266
|
+
"--deny-self-hosted-runners",
|
|
267
|
+
],
|
|
268
|
+
{
|
|
269
|
+
env: {
|
|
270
|
+
HOME: stateDir,
|
|
271
|
+
PATH: "/usr/local/bin:/usr/bin:/bin",
|
|
272
|
+
GH_PROMPT_DISABLED: "1",
|
|
273
|
+
},
|
|
274
|
+
timeout: REQUEST_TIMEOUT_MS,
|
|
275
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
276
|
+
},
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
async function readVersionFile(filePath) {
|
|
281
|
+
try {
|
|
282
|
+
return parseReleaseVersion(await fsp.readFile(filePath, "utf8"));
|
|
283
|
+
} catch {
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function releaseAllowedForChannel(version, channel) {
|
|
289
|
+
return !version.includes("-") || channel.trim() === "beta";
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function parseSignerReleaseIdentity(value, expectedVersion) {
|
|
293
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
294
|
+
throw new Error("signer release identity is missing");
|
|
295
|
+
}
|
|
296
|
+
const keys = Object.keys(value).toSorted();
|
|
297
|
+
if (keys.join(",") !== "buildInputDigest,commit,development,version") {
|
|
298
|
+
throw new Error("signer release identity contains unsupported fields");
|
|
299
|
+
}
|
|
300
|
+
const version = parseReleaseVersion(value.version);
|
|
301
|
+
if (
|
|
302
|
+
(expectedVersion && version !== expectedVersion) ||
|
|
303
|
+
!/^[a-f0-9]{40}$/.test(value.commit || "") ||
|
|
304
|
+
!/^sha256:[a-f0-9]{64}$/.test(value.buildInputDigest || "") ||
|
|
305
|
+
value.development !== false
|
|
306
|
+
) {
|
|
307
|
+
throw new Error("signer release identity is development, malformed, or mismatched");
|
|
308
|
+
}
|
|
309
|
+
return Object.freeze({
|
|
310
|
+
version,
|
|
311
|
+
commit: value.commit,
|
|
312
|
+
buildInputDigest: value.buildInputDigest,
|
|
313
|
+
development: false,
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function canonicalJSON(value) {
|
|
318
|
+
if (Array.isArray(value)) {
|
|
319
|
+
return `[${value.map((entry) => canonicalJSON(entry)).join(",")}]`;
|
|
320
|
+
}
|
|
321
|
+
if (value && typeof value === "object") {
|
|
322
|
+
return `{${Object.keys(value)
|
|
323
|
+
.toSorted()
|
|
324
|
+
.map((key) => `${JSON.stringify(key)}:${canonicalJSON(value[key])}`)
|
|
325
|
+
.join(",")}}`;
|
|
326
|
+
}
|
|
327
|
+
return JSON.stringify(value);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function parseUnifiedHostedSignerRelease(value, expectedVersion, platform) {
|
|
331
|
+
if (
|
|
332
|
+
!value ||
|
|
333
|
+
typeof value !== "object" ||
|
|
334
|
+
Array.isArray(value) ||
|
|
335
|
+
Object.keys(value).toSorted().join(",") !== "application,release,schemaVersion,signer" ||
|
|
336
|
+
value.schemaVersion !== 2
|
|
337
|
+
) {
|
|
338
|
+
throw new Error("attested unified hosted release manifest schema is invalid");
|
|
339
|
+
}
|
|
340
|
+
const release = value.release;
|
|
341
|
+
if (
|
|
342
|
+
!release ||
|
|
343
|
+
Object.keys(release).toSorted().join(",") !== "commit,tag,version" ||
|
|
344
|
+
release.version !== expectedVersion ||
|
|
345
|
+
release.tag !== `v${expectedVersion}` ||
|
|
346
|
+
!/^[a-f0-9]{40}$/.test(release.commit || "")
|
|
347
|
+
) {
|
|
348
|
+
throw new Error("attested unified hosted release identity is malformed or mismatched");
|
|
349
|
+
}
|
|
350
|
+
const signer = value.signer;
|
|
351
|
+
if (
|
|
352
|
+
!signer ||
|
|
353
|
+
Object.keys(signer).toSorted().join(",") !==
|
|
354
|
+
"capabilities,capabilitiesDigest,platforms,release" ||
|
|
355
|
+
!/^sha256:[a-f0-9]{64}$/.test(signer.capabilitiesDigest || "") ||
|
|
356
|
+
`sha256:${createHash("sha256").update(canonicalJSON(signer.capabilities)).digest("hex")}` !==
|
|
357
|
+
signer.capabilitiesDigest ||
|
|
358
|
+
signer.capabilities?.protocol?.current !== 2 ||
|
|
359
|
+
signer.capabilities?.protocol?.min !== 2 ||
|
|
360
|
+
signer.capabilities?.protocol?.max !== 2
|
|
361
|
+
) {
|
|
362
|
+
throw new Error("attested unified signer capability contract is invalid");
|
|
363
|
+
}
|
|
364
|
+
const signerRelease = parseSignerReleaseIdentity(signer.release, expectedVersion);
|
|
365
|
+
if (signerRelease.commit !== release.commit) {
|
|
366
|
+
throw new Error("attested hosted app and signer commits do not match");
|
|
367
|
+
}
|
|
368
|
+
const artifact = signer.platforms?.[platform];
|
|
369
|
+
if (
|
|
370
|
+
!artifact ||
|
|
371
|
+
Object.keys(artifact).toSorted().join(",") !== "asset,sha256" ||
|
|
372
|
+
!/^[A-Za-z0-9][A-Za-z0-9._-]+$/.test(artifact.asset || "") ||
|
|
373
|
+
!/^[a-f0-9]{64}$/.test(artifact.sha256 || "")
|
|
374
|
+
) {
|
|
375
|
+
throw new Error(`attested unified signer release has no valid ${platform} artifact`);
|
|
376
|
+
}
|
|
377
|
+
return {
|
|
378
|
+
release: signerRelease,
|
|
379
|
+
artifact,
|
|
380
|
+
binding: {
|
|
381
|
+
releaseCommit: release.commit,
|
|
382
|
+
capabilitiesDigest: signer.capabilitiesDigest,
|
|
383
|
+
},
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function signerReleaseIdentitiesEqual(left, right) {
|
|
388
|
+
return (
|
|
389
|
+
left?.version === right?.version &&
|
|
390
|
+
left?.commit === right?.commit &&
|
|
391
|
+
left?.buildInputDigest === right?.buildInputDigest &&
|
|
392
|
+
left?.development === false &&
|
|
393
|
+
right?.development === false
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function signerStateInvariantFromHealth(result) {
|
|
398
|
+
const policies = result?.policies;
|
|
399
|
+
const networks = result?.network?.wallets;
|
|
400
|
+
const webAuthn = result?.webAuthn;
|
|
401
|
+
if (
|
|
402
|
+
!Array.isArray(policies) ||
|
|
403
|
+
!policies.every(
|
|
404
|
+
(policy) =>
|
|
405
|
+
typeof policy?.walletId === "string" &&
|
|
406
|
+
typeof policy?.role === "string" &&
|
|
407
|
+
Number.isSafeInteger(policy?.version) &&
|
|
408
|
+
policy.version > 0 &&
|
|
409
|
+
/^sha256:[a-f0-9]{64}$/.test(policy?.hash || ""),
|
|
410
|
+
) ||
|
|
411
|
+
typeof result?.network?.ready !== "boolean" ||
|
|
412
|
+
!Array.isArray(networks) ||
|
|
413
|
+
!networks.every(
|
|
414
|
+
(network) =>
|
|
415
|
+
typeof network?.walletId === "string" &&
|
|
416
|
+
typeof network?.configured === "boolean" &&
|
|
417
|
+
Number.isSafeInteger(network?.version) &&
|
|
418
|
+
network.version >= 0 &&
|
|
419
|
+
typeof network?.ready === "boolean" &&
|
|
420
|
+
(!network.configured || /^sha256:[a-f0-9]{64}$/.test(network?.hash || "")),
|
|
421
|
+
) ||
|
|
422
|
+
typeof webAuthn?.configured !== "boolean" ||
|
|
423
|
+
!Number.isSafeInteger(webAuthn?.credentialCount) ||
|
|
424
|
+
webAuthn.credentialCount < 0 ||
|
|
425
|
+
!Number.isSafeInteger(webAuthn?.credentialVersion) ||
|
|
426
|
+
webAuthn.credentialVersion < 0 ||
|
|
427
|
+
typeof webAuthn?.ready !== "boolean"
|
|
428
|
+
) {
|
|
429
|
+
throw new Error("signer health state invariants are malformed");
|
|
430
|
+
}
|
|
431
|
+
return canonicalJSON({
|
|
432
|
+
policies: [...policies].toSorted((left, right) => left.walletId.localeCompare(right.walletId)),
|
|
433
|
+
network: {
|
|
434
|
+
ready: result.network.ready,
|
|
435
|
+
wallets: [...networks].toSorted((left, right) => left.walletId.localeCompare(right.walletId)),
|
|
436
|
+
},
|
|
437
|
+
webAuthn: {
|
|
438
|
+
configured: webAuthn.configured,
|
|
439
|
+
rpId: webAuthn.rpId || "",
|
|
440
|
+
origins: [...(Array.isArray(webAuthn.origins) ? webAuthn.origins : [])].toSorted(
|
|
441
|
+
(left, right) => String(left).localeCompare(String(right)),
|
|
442
|
+
),
|
|
443
|
+
credentialCount: webAuthn.credentialCount,
|
|
444
|
+
credentialVersion: webAuthn.credentialVersion,
|
|
445
|
+
ready: webAuthn.ready,
|
|
446
|
+
},
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
async function assertReleaseChannelAllowed(version, channelPath) {
|
|
451
|
+
let channel = "stable";
|
|
452
|
+
try {
|
|
453
|
+
const stat = await fsp.lstat(channelPath);
|
|
454
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.uid !== 0 || (stat.mode & 0o022) !== 0) {
|
|
455
|
+
throw new Error(
|
|
456
|
+
"host updater channel file must be root-owned and not writable by group/others",
|
|
457
|
+
);
|
|
458
|
+
}
|
|
459
|
+
channel = await fsp.readFile(channelPath, "utf8");
|
|
460
|
+
} catch (error) {
|
|
461
|
+
if (error?.code !== "ENOENT") {
|
|
462
|
+
throw error;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
if (!releaseAllowedForChannel(version, channel)) {
|
|
466
|
+
throw new Error(
|
|
467
|
+
"prerelease signer updates require root to set /etc/fased/host-updater-channel to beta",
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function assertSignerV2Health(response, expectedRelease) {
|
|
473
|
+
const protocol = response?.result?.capabilities?.protocol;
|
|
474
|
+
if (
|
|
475
|
+
response?.ok !== true ||
|
|
476
|
+
response?.result?.ready !== true ||
|
|
477
|
+
response?.result?.keystoreType !== "signer-owned-v2" ||
|
|
478
|
+
protocol?.current !== 2 ||
|
|
479
|
+
protocol?.min > 2 ||
|
|
480
|
+
protocol?.max < 2 ||
|
|
481
|
+
!Array.isArray(response?.result?.policies)
|
|
482
|
+
) {
|
|
483
|
+
throw new Error("signer health did not acknowledge protocol v2 and signer-owned custody");
|
|
484
|
+
}
|
|
485
|
+
const release = parseSignerReleaseIdentity(response.result.release, expectedRelease?.version);
|
|
486
|
+
if (expectedRelease && !signerReleaseIdentitiesEqual(release, expectedRelease)) {
|
|
487
|
+
throw new Error("signer health release identity does not match the attested release manifest");
|
|
488
|
+
}
|
|
489
|
+
signerStateInvariantFromHealth(response.result);
|
|
490
|
+
return release;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
async function readSignerV2Health() {
|
|
494
|
+
return await new Promise((resolve, reject) => {
|
|
495
|
+
const socket = net.createConnection({ path: "/run/fased-signerd/app.sock" });
|
|
496
|
+
socket.setEncoding("utf8");
|
|
497
|
+
socket.setTimeout(3000);
|
|
498
|
+
let body = "";
|
|
499
|
+
socket.once("connect", () => socket.write(`${JSON.stringify({ op: "health" })}\n`));
|
|
500
|
+
socket.on("data", (chunk) => {
|
|
501
|
+
body += chunk;
|
|
502
|
+
const newline = body.indexOf("\n");
|
|
503
|
+
if (newline < 0) {
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
socket.destroy();
|
|
507
|
+
try {
|
|
508
|
+
resolve(JSON.parse(body.slice(0, newline)));
|
|
509
|
+
} catch {
|
|
510
|
+
reject(new Error("signer health response is invalid"));
|
|
511
|
+
}
|
|
512
|
+
});
|
|
513
|
+
socket.once("timeout", () => reject(new Error("signer health probe timed out")));
|
|
514
|
+
socket.once("error", reject);
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
export async function probeSignerV2(expectedRelease) {
|
|
519
|
+
const response = await readSignerV2Health();
|
|
520
|
+
return assertSignerV2Health(response, expectedRelease);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
async function probeSignerStateV2(expectedRelease) {
|
|
524
|
+
const response = await readSignerV2Health();
|
|
525
|
+
const release = assertSignerV2Health(response, expectedRelease);
|
|
526
|
+
return { release, invariant: signerStateInvariantFromHealth(response.result) };
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
async function systemctl(...args) {
|
|
530
|
+
const binary = await fixedExecutable(["/usr/bin/systemctl", "/bin/systemctl"], "systemctl");
|
|
531
|
+
return await execFileAsync(binary, args, {
|
|
532
|
+
env: { PATH: "/usr/bin:/bin" },
|
|
533
|
+
timeout: 60_000,
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
async function stopSignerService() {
|
|
538
|
+
await systemctl("stop", "fased-signerd.service");
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
async function startSignerService({ requireV2, expectedRelease }) {
|
|
542
|
+
await systemctl("start", "fased-signerd.service");
|
|
543
|
+
await systemctl("is-active", "--quiet", "fased-signerd.service");
|
|
544
|
+
if (!requireV2) {
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
let lastError;
|
|
548
|
+
for (let attempt = 0; attempt < 30; attempt += 1) {
|
|
549
|
+
try {
|
|
550
|
+
return await probeSignerStateV2(expectedRelease);
|
|
551
|
+
} catch (error) {
|
|
552
|
+
lastError = error;
|
|
553
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
throw new Error(`signer protocol v2 readiness failed: ${lastError?.message || "unknown error"}`);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function transactionPaths(paths, transactionId) {
|
|
560
|
+
const transactionDir = path.join(paths.transactionsDir, transactionId);
|
|
561
|
+
return {
|
|
562
|
+
transactionDir,
|
|
563
|
+
candidatePath: path.join(
|
|
564
|
+
path.dirname(paths.signerPath),
|
|
565
|
+
`.fased-signerd.candidate-${transactionId}`,
|
|
566
|
+
),
|
|
567
|
+
previousBinaryPath: path.join(transactionDir, "fased-signerd.previous"),
|
|
568
|
+
stateDBSnapshotPath: path.join(transactionDir, "state.db.previous"),
|
|
569
|
+
signerUnitSnapshotPath: path.join(transactionDir, "fased-signerd.service.previous"),
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
function validateJournal(value) {
|
|
574
|
+
if (
|
|
575
|
+
!value ||
|
|
576
|
+
typeof value !== "object" ||
|
|
577
|
+
!new Set([1, JOURNAL_SCHEMA_VERSION]).has(value.schemaVersion) ||
|
|
578
|
+
!TRANSACTION_PHASES.has(value.phase)
|
|
579
|
+
) {
|
|
580
|
+
throw new Error("host updater transaction journal is invalid");
|
|
581
|
+
}
|
|
582
|
+
if (
|
|
583
|
+
value.previousSignerInvariant != null &&
|
|
584
|
+
(typeof value.previousSignerInvariant !== "string" ||
|
|
585
|
+
value.previousSignerInvariant.length === 0 ||
|
|
586
|
+
value.previousSignerInvariant.length > 64 * 1024)
|
|
587
|
+
) {
|
|
588
|
+
throw new Error("host updater previous signer-state invariant is invalid");
|
|
589
|
+
}
|
|
590
|
+
const version = parseReleaseVersion(value.version);
|
|
591
|
+
const releaseBinding = value.releaseBinding == null ? null : value.releaseBinding;
|
|
592
|
+
if (
|
|
593
|
+
releaseBinding &&
|
|
594
|
+
(!/^sha256:[a-f0-9]{64}$/.test(releaseBinding.manifestDigest || "") ||
|
|
595
|
+
!/^sha256:[a-f0-9]{64}$/.test(releaseBinding.signerArtifactDigest || "") ||
|
|
596
|
+
!/^sha256:[a-f0-9]{64}$/.test(releaseBinding.capabilitiesDigest || "") ||
|
|
597
|
+
!/^[a-f0-9]{40}$/.test(releaseBinding.releaseCommit || ""))
|
|
598
|
+
) {
|
|
599
|
+
throw new Error("host updater release-manifest binding is invalid");
|
|
600
|
+
}
|
|
601
|
+
return {
|
|
602
|
+
...value,
|
|
603
|
+
transactionId: parseTransactionId(value.transactionId),
|
|
604
|
+
version,
|
|
605
|
+
previousVersion:
|
|
606
|
+
value.previousVersion == null ? null : parseReleaseVersion(value.previousVersion),
|
|
607
|
+
release: parseSignerReleaseIdentity(value.release, version),
|
|
608
|
+
releaseBinding,
|
|
609
|
+
changed: value.changed === true,
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
async function readJournal(context) {
|
|
614
|
+
try {
|
|
615
|
+
return validateJournal(JSON.parse(await fsp.readFile(context.paths.journalPath, "utf8")));
|
|
616
|
+
} catch (error) {
|
|
617
|
+
if (error?.code === "ENOENT") {
|
|
618
|
+
return null;
|
|
619
|
+
}
|
|
620
|
+
throw error;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
async function writeJournal(context, journal) {
|
|
625
|
+
const next = validateJournal({
|
|
626
|
+
...journal,
|
|
627
|
+
schemaVersion: JOURNAL_SCHEMA_VERSION,
|
|
628
|
+
updatedAt: new Date().toISOString(),
|
|
629
|
+
});
|
|
630
|
+
await atomicWriteFileDurable(
|
|
631
|
+
context.paths.journalPath,
|
|
632
|
+
`${JSON.stringify(next, null, 2)}\n`,
|
|
633
|
+
0o600,
|
|
634
|
+
);
|
|
635
|
+
return next;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
async function removeJournal(context) {
|
|
639
|
+
await fsp.rm(context.paths.journalPath, { force: true });
|
|
640
|
+
await fsyncDirectory(path.dirname(context.paths.journalPath));
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
async function fileMetadata(filePath) {
|
|
644
|
+
try {
|
|
645
|
+
const stat = await fsp.lstat(filePath);
|
|
646
|
+
if (!stat.isFile() || stat.isSymbolicLink()) {
|
|
647
|
+
throw new Error(`transaction source must be a regular non-symlink file: ${filePath}`);
|
|
648
|
+
}
|
|
649
|
+
return { existed: true, uid: stat.uid, gid: stat.gid, mode: stat.mode & 0o777 };
|
|
650
|
+
} catch (error) {
|
|
651
|
+
if (error?.code === "ENOENT") {
|
|
652
|
+
return { existed: false };
|
|
653
|
+
}
|
|
654
|
+
throw error;
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
async function cleanupTransactionFiles(context, transactionId) {
|
|
659
|
+
const txPaths = transactionPaths(context.paths, transactionId);
|
|
660
|
+
await fsp.rm(txPaths.candidatePath, { force: true });
|
|
661
|
+
await fsp.rm(txPaths.transactionDir, { recursive: true, force: true });
|
|
662
|
+
await fsyncDirectory(path.dirname(txPaths.candidatePath));
|
|
663
|
+
await fsyncDirectory(context.paths.transactionsDir).catch((error) => {
|
|
664
|
+
if (error?.code !== "ENOENT") {
|
|
665
|
+
throw error;
|
|
666
|
+
}
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
async function stageOfficialCandidate(version, candidatePath, context) {
|
|
671
|
+
const arch = releaseArchitecture();
|
|
672
|
+
const platform = `linux-${arch}`;
|
|
673
|
+
const releaseUrl = `${RELEASE_BASE}/v${version}`;
|
|
674
|
+
await fsp.mkdir(context.paths.stateDir, { recursive: true, mode: 0o700 });
|
|
675
|
+
const staging = await fsp.mkdtemp(path.join(context.paths.stateDir, `.download-${version}-`));
|
|
676
|
+
const releaseManifestName = "fased-hosted-release-v2.json";
|
|
677
|
+
const releaseManifestPath = path.join(staging, releaseManifestName);
|
|
678
|
+
try {
|
|
679
|
+
await download(`${releaseUrl}/${releaseManifestName}`, releaseManifestPath);
|
|
680
|
+
await verifyReleaseAsset(releaseManifestPath, version, context.paths.stateDir);
|
|
681
|
+
const manifestBytes = await fsp.readFile(releaseManifestPath);
|
|
682
|
+
const selected = parseUnifiedHostedSignerRelease(
|
|
683
|
+
JSON.parse(manifestBytes.toString("utf8")),
|
|
684
|
+
version,
|
|
685
|
+
platform,
|
|
686
|
+
);
|
|
687
|
+
const assetPath = path.join(staging, selected.artifact.asset);
|
|
688
|
+
await download(`${releaseUrl}/${selected.artifact.asset}`, assetPath);
|
|
689
|
+
if ((await sha256(assetPath)) !== selected.artifact.sha256) {
|
|
690
|
+
throw new Error("native signer does not match the attested unified release manifest");
|
|
691
|
+
}
|
|
692
|
+
await verifyReleaseAsset(assetPath, version, context.paths.stateDir);
|
|
693
|
+
await fsp.rm(candidatePath, { force: true });
|
|
694
|
+
await atomicCopyFileDurable(assetPath, candidatePath, { mode: 0o755 });
|
|
695
|
+
return {
|
|
696
|
+
release: selected.release,
|
|
697
|
+
binding: {
|
|
698
|
+
...selected.binding,
|
|
699
|
+
manifestDigest: `sha256:${createHash("sha256").update(manifestBytes).digest("hex")}`,
|
|
700
|
+
signerArtifactDigest: `sha256:${selected.artifact.sha256}`,
|
|
701
|
+
},
|
|
702
|
+
};
|
|
703
|
+
} finally {
|
|
704
|
+
await fsp.rm(staging, { recursive: true, force: true });
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
function createTransactionContext(overrides = {}) {
|
|
709
|
+
const paths = { ...DEFAULT_PATHS, ...overrides.paths };
|
|
710
|
+
return {
|
|
711
|
+
paths,
|
|
712
|
+
assertReleaseAllowed:
|
|
713
|
+
overrides.assertReleaseAllowed ??
|
|
714
|
+
(async (version) => await assertReleaseChannelAllowed(version, paths.channelPath)),
|
|
715
|
+
stageCandidate:
|
|
716
|
+
overrides.stageCandidate ??
|
|
717
|
+
(async (version, candidatePath, context) =>
|
|
718
|
+
await stageOfficialCandidate(version, candidatePath, context)),
|
|
719
|
+
stopSigner: overrides.stopSigner ?? stopSignerService,
|
|
720
|
+
startSignerV2:
|
|
721
|
+
overrides.startSignerV2 ??
|
|
722
|
+
(async ({ expectedRelease } = {}) =>
|
|
723
|
+
await startSignerService({ requireV2: true, expectedRelease })),
|
|
724
|
+
startPreviousSigner:
|
|
725
|
+
overrides.startPreviousSigner ??
|
|
726
|
+
(async ({ requireV2 = false } = {}) => await startSignerService({ requireV2 })),
|
|
727
|
+
reloadUnits: overrides.reloadUnits ?? (async () => await systemctl("daemon-reload")),
|
|
728
|
+
startGateway:
|
|
729
|
+
overrides.startGateway ?? (async () => await systemctl("start", "fased-gateway.service")),
|
|
730
|
+
probeSigner: overrides.probeSigner ?? probeSignerV2,
|
|
731
|
+
probeSignerState:
|
|
732
|
+
overrides.probeSignerState ??
|
|
733
|
+
(overrides.probeSigner
|
|
734
|
+
? async () => ({ release: await overrides.probeSigner(), invariant: null })
|
|
735
|
+
: probeSignerStateV2),
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
async function writeGatewayGate(context, journal) {
|
|
740
|
+
await atomicWriteFileDurable(
|
|
741
|
+
context.paths.gatewayGatePath,
|
|
742
|
+
`${JSON.stringify({ transactionId: journal.transactionId, version: journal.version })}\n`,
|
|
743
|
+
0o644,
|
|
744
|
+
);
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
async function writeSignerGate(context, journal) {
|
|
748
|
+
const directory = path.dirname(context.paths.signerGatePath);
|
|
749
|
+
await fsp.mkdir(directory, { recursive: true, mode: 0o755 });
|
|
750
|
+
const directoryStat = await fsp.lstat(directory);
|
|
751
|
+
if (
|
|
752
|
+
!directoryStat.isDirectory() ||
|
|
753
|
+
directoryStat.isSymbolicLink() ||
|
|
754
|
+
directoryStat.uid !== process.geteuid() ||
|
|
755
|
+
(directoryStat.mode & 0o022) !== 0
|
|
756
|
+
) {
|
|
757
|
+
throw new Error(
|
|
758
|
+
"signer update gate directory must be owned by the updater and not writable by group/others",
|
|
759
|
+
);
|
|
760
|
+
}
|
|
761
|
+
await fsp.chmod(directory, 0o755);
|
|
762
|
+
await atomicWriteFileDurable(
|
|
763
|
+
context.paths.signerGatePath,
|
|
764
|
+
`${JSON.stringify({ transactionId: journal.transactionId, version: journal.version })}\n`,
|
|
765
|
+
0o644,
|
|
766
|
+
);
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
async function writeUpdateGates(context, journal) {
|
|
770
|
+
await writeGatewayGate(context, journal);
|
|
771
|
+
await writeSignerGate(context, journal);
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
async function removeGatewayGate(context) {
|
|
775
|
+
await fsp.rm(context.paths.gatewayGatePath, { force: true });
|
|
776
|
+
await fsyncDirectory(path.dirname(context.paths.gatewayGatePath));
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
async function removeSignerGate(context) {
|
|
780
|
+
await fsp.rm(context.paths.signerGatePath, { force: true });
|
|
781
|
+
await fsyncDirectory(path.dirname(context.paths.signerGatePath));
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
async function removeUpdateGates(context) {
|
|
785
|
+
await removeGatewayGate(context);
|
|
786
|
+
await removeSignerGate(context);
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
async function readGatewayGate(context) {
|
|
790
|
+
try {
|
|
791
|
+
const value = JSON.parse(await fsp.readFile(context.paths.gatewayGatePath, "utf8"));
|
|
792
|
+
return {
|
|
793
|
+
transactionId: parseTransactionId(value.transactionId),
|
|
794
|
+
version: parseReleaseVersion(value.version),
|
|
795
|
+
};
|
|
796
|
+
} catch (error) {
|
|
797
|
+
if (error?.code === "ENOENT") {
|
|
798
|
+
return null;
|
|
799
|
+
}
|
|
800
|
+
throw new Error("hosted Gateway update gate is invalid", { cause: error });
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
function assertMatchingTransaction(journal, request) {
|
|
805
|
+
if (!journal) {
|
|
806
|
+
throw new Error("host updater transaction does not exist");
|
|
807
|
+
}
|
|
808
|
+
if (journal.transactionId !== request.transactionId || journal.version !== request.version) {
|
|
809
|
+
throw new Error(
|
|
810
|
+
`another hosted signer transaction is active (${journal.transactionId}, v${journal.version})`,
|
|
811
|
+
);
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
async function readRollbackFloor(context) {
|
|
816
|
+
try {
|
|
817
|
+
return parseReleaseVersion(await fsp.readFile(context.paths.rollbackFloorPath, "utf8"));
|
|
818
|
+
} catch (error) {
|
|
819
|
+
if (error?.code === "ENOENT") {
|
|
820
|
+
return null;
|
|
821
|
+
}
|
|
822
|
+
throw new Error("hosted custody rollback floor is invalid; repair it as root before updating", {
|
|
823
|
+
cause: error,
|
|
824
|
+
});
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
async function assertRollbackFloor(context, targetVersion) {
|
|
829
|
+
const floor = await readRollbackFloor(context);
|
|
830
|
+
if (floor && compareVersions(targetVersion, floor) === -1) {
|
|
831
|
+
throw new Error(
|
|
832
|
+
`refusing signer release v${targetVersion}: hosted custody rollback floor is v${floor}`,
|
|
833
|
+
);
|
|
834
|
+
}
|
|
835
|
+
return floor;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
async function writeInitialRollbackFloor(context, version) {
|
|
839
|
+
const existing = await readRollbackFloor(context);
|
|
840
|
+
if (existing) {
|
|
841
|
+
if (compareVersions(version, existing) === -1) {
|
|
842
|
+
throw new Error(`cannot commit below hosted custody rollback floor v${existing}`);
|
|
843
|
+
}
|
|
844
|
+
return existing;
|
|
845
|
+
}
|
|
846
|
+
await atomicWriteFileDurable(context.paths.rollbackFloorPath, `${version}\n`, 0o600);
|
|
847
|
+
return version;
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
async function prepareSignerRelease(request, context) {
|
|
851
|
+
await context.assertReleaseAllowed(request.version);
|
|
852
|
+
await assertRollbackFloor(context, request.version);
|
|
853
|
+
const active = await readJournal(context);
|
|
854
|
+
if (active) {
|
|
855
|
+
assertMatchingTransaction(active, request);
|
|
856
|
+
if (active.phase !== "gateway-authorized" && active.phase !== "committing") {
|
|
857
|
+
await writeUpdateGates(context, active);
|
|
858
|
+
} else if (active.phase === "gateway-authorized") {
|
|
859
|
+
await writeSignerGate(context, active);
|
|
860
|
+
}
|
|
861
|
+
return {
|
|
862
|
+
transactionId: active.transactionId,
|
|
863
|
+
version: active.version,
|
|
864
|
+
phase: active.phase,
|
|
865
|
+
changed: active.changed,
|
|
866
|
+
release: active.release,
|
|
867
|
+
};
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
const currentVersion = await readVersionFile(context.paths.versionPath);
|
|
871
|
+
if (currentVersion && compareVersions(currentVersion, request.version) === 1) {
|
|
872
|
+
throw new Error(`refusing signer downgrade from ${currentVersion} to ${request.version}`);
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
let changed = true;
|
|
876
|
+
let release = null;
|
|
877
|
+
let releaseBinding = null;
|
|
878
|
+
let previousSignerInvariant = null;
|
|
879
|
+
let previousSignerState = null;
|
|
880
|
+
if (currentVersion) {
|
|
881
|
+
previousSignerState = await context.probeSignerState();
|
|
882
|
+
parseSignerReleaseIdentity(previousSignerState.release, currentVersion);
|
|
883
|
+
previousSignerInvariant = previousSignerState.invariant;
|
|
884
|
+
}
|
|
885
|
+
if (currentVersion === request.version) {
|
|
886
|
+
try {
|
|
887
|
+
await fsp.access(context.paths.signerPath, fs.constants.X_OK);
|
|
888
|
+
release = parseSignerReleaseIdentity(
|
|
889
|
+
previousSignerState?.release ?? (await context.probeSigner()),
|
|
890
|
+
request.version,
|
|
891
|
+
);
|
|
892
|
+
changed = false;
|
|
893
|
+
} catch {
|
|
894
|
+
changed = true;
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
const txPaths = transactionPaths(context.paths, request.transactionId);
|
|
899
|
+
let journal;
|
|
900
|
+
try {
|
|
901
|
+
await fsp.mkdir(txPaths.transactionDir, { recursive: true, mode: 0o700 });
|
|
902
|
+
const signerUnit = await fileMetadata(context.paths.signerUnitPath);
|
|
903
|
+
if (signerUnit.existed) {
|
|
904
|
+
await atomicCopyFileDurable(context.paths.signerUnitPath, txPaths.signerUnitSnapshotPath, {
|
|
905
|
+
mode: signerUnit.mode,
|
|
906
|
+
});
|
|
907
|
+
}
|
|
908
|
+
if (changed) {
|
|
909
|
+
const staged = await context.stageCandidate(request.version, txPaths.candidatePath, context);
|
|
910
|
+
release = parseSignerReleaseIdentity(staged?.release || staged, request.version);
|
|
911
|
+
releaseBinding = staged?.binding || null;
|
|
912
|
+
if (!releaseBinding) {
|
|
913
|
+
throw new Error("signer candidate omitted its attested unified release binding");
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
journal = await writeJournal(context, {
|
|
917
|
+
schemaVersion: JOURNAL_SCHEMA_VERSION,
|
|
918
|
+
transactionId: request.transactionId,
|
|
919
|
+
version: request.version,
|
|
920
|
+
previousVersion: currentVersion,
|
|
921
|
+
release,
|
|
922
|
+
releaseBinding,
|
|
923
|
+
previousSignerInvariant,
|
|
924
|
+
phase: "prepared",
|
|
925
|
+
changed,
|
|
926
|
+
createdAt: new Date().toISOString(),
|
|
927
|
+
previousBinary: null,
|
|
928
|
+
stateDB: null,
|
|
929
|
+
signerUnit,
|
|
930
|
+
rollbackFromPhase: null,
|
|
931
|
+
});
|
|
932
|
+
await writeUpdateGates(context, journal);
|
|
933
|
+
} catch (error) {
|
|
934
|
+
await cleanupTransactionFiles(context, request.transactionId).catch(() => undefined);
|
|
935
|
+
if (journal) {
|
|
936
|
+
await removeJournal(context).catch(() => undefined);
|
|
937
|
+
}
|
|
938
|
+
await removeUpdateGates(context).catch(() => undefined);
|
|
939
|
+
throw error;
|
|
940
|
+
}
|
|
941
|
+
return {
|
|
942
|
+
transactionId: journal.transactionId,
|
|
943
|
+
version: journal.version,
|
|
944
|
+
phase: journal.phase,
|
|
945
|
+
changed: journal.changed,
|
|
946
|
+
release: journal.release,
|
|
947
|
+
};
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
async function restoreSignerUnit(context, journal, txPaths) {
|
|
951
|
+
if (journal.signerUnit?.existed) {
|
|
952
|
+
await fsp.access(txPaths.signerUnitSnapshotPath);
|
|
953
|
+
await atomicCopyFileDurable(txPaths.signerUnitSnapshotPath, context.paths.signerUnitPath, {
|
|
954
|
+
mode: journal.signerUnit.mode || 0o644,
|
|
955
|
+
uid: journal.signerUnit.uid,
|
|
956
|
+
gid: journal.signerUnit.gid,
|
|
957
|
+
});
|
|
958
|
+
} else {
|
|
959
|
+
await fsp.rm(context.paths.signerUnitPath, { force: true });
|
|
960
|
+
await fsyncDirectory(path.dirname(context.paths.signerUnitPath));
|
|
961
|
+
}
|
|
962
|
+
await context.reloadUnits();
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
async function restoreVersionFile(context, previousVersion) {
|
|
966
|
+
if (previousVersion) {
|
|
967
|
+
await atomicWriteFileDurable(context.paths.versionPath, `${previousVersion}\n`, 0o600);
|
|
968
|
+
return;
|
|
969
|
+
}
|
|
970
|
+
await fsp.rm(context.paths.versionPath, { force: true });
|
|
971
|
+
await fsyncDirectory(path.dirname(context.paths.versionPath));
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
async function restoreStateDB(context, journal, txPaths) {
|
|
975
|
+
if (!journal.stateDB || journal.rollbackFromPhase === "snapshotting") {
|
|
976
|
+
return;
|
|
977
|
+
}
|
|
978
|
+
if (journal.stateDB.existed) {
|
|
979
|
+
await fsp.access(txPaths.stateDBSnapshotPath);
|
|
980
|
+
await atomicCopyFileDurable(txPaths.stateDBSnapshotPath, context.paths.signerStateDBPath, {
|
|
981
|
+
mode: journal.stateDB.mode,
|
|
982
|
+
uid: journal.stateDB.uid,
|
|
983
|
+
gid: journal.stateDB.gid,
|
|
984
|
+
});
|
|
985
|
+
return;
|
|
986
|
+
}
|
|
987
|
+
await fsp.rm(context.paths.signerStateDBPath, { force: true });
|
|
988
|
+
await fsyncDirectory(path.dirname(context.paths.signerStateDBPath));
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
async function restorePreviousBinary(context, journal, txPaths) {
|
|
992
|
+
if (journal.previousBinary?.existed) {
|
|
993
|
+
await fsp.access(txPaths.previousBinaryPath);
|
|
994
|
+
await atomicCopyFileDurable(txPaths.previousBinaryPath, context.paths.signerPath, {
|
|
995
|
+
mode: journal.previousBinary.mode || 0o755,
|
|
996
|
+
uid: journal.previousBinary.uid,
|
|
997
|
+
gid: journal.previousBinary.gid,
|
|
998
|
+
});
|
|
999
|
+
return true;
|
|
1000
|
+
}
|
|
1001
|
+
await fsp.rm(context.paths.signerPath, { force: true });
|
|
1002
|
+
await fsyncDirectory(path.dirname(context.paths.signerPath));
|
|
1003
|
+
return false;
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
async function rollbackSignerRelease(request, context, { preserveGatewayGate = false } = {}) {
|
|
1007
|
+
let journal = await readJournal(context);
|
|
1008
|
+
if (!journal) {
|
|
1009
|
+
if (!preserveGatewayGate) {
|
|
1010
|
+
await removeUpdateGates(context);
|
|
1011
|
+
}
|
|
1012
|
+
return {
|
|
1013
|
+
transactionId: request.transactionId,
|
|
1014
|
+
version: request.version,
|
|
1015
|
+
phase: "rolled-back",
|
|
1016
|
+
changed: false,
|
|
1017
|
+
};
|
|
1018
|
+
}
|
|
1019
|
+
assertMatchingTransaction(journal, request);
|
|
1020
|
+
await writeUpdateGates(context, journal);
|
|
1021
|
+
if (journal.phase === "restored") {
|
|
1022
|
+
await cleanupTransactionFiles(context, journal.transactionId);
|
|
1023
|
+
await removeJournal(context);
|
|
1024
|
+
if (!preserveGatewayGate) {
|
|
1025
|
+
await removeUpdateGates(context);
|
|
1026
|
+
}
|
|
1027
|
+
return {
|
|
1028
|
+
transactionId: journal.transactionId,
|
|
1029
|
+
version: journal.version,
|
|
1030
|
+
phase: "rolled-back",
|
|
1031
|
+
changed: journal.changed,
|
|
1032
|
+
};
|
|
1033
|
+
}
|
|
1034
|
+
const rollbackFromPhase = journal.rollbackFromPhase || journal.phase;
|
|
1035
|
+
journal = await writeJournal(context, {
|
|
1036
|
+
...journal,
|
|
1037
|
+
phase: "rolling-back",
|
|
1038
|
+
rollbackFromPhase,
|
|
1039
|
+
});
|
|
1040
|
+
const txPaths = transactionPaths(context.paths, journal.transactionId);
|
|
1041
|
+
|
|
1042
|
+
// A host can reboot after prepare while the installer has already replaced
|
|
1043
|
+
// the unit. Always quiesce before restoring unit semantics, even though the
|
|
1044
|
+
// updater itself does not mutate the live signer during prepare.
|
|
1045
|
+
const signerMayNeedRestart = true;
|
|
1046
|
+
await context.stopSigner();
|
|
1047
|
+
await restoreSignerUnit(context, journal, txPaths);
|
|
1048
|
+
if (journal.changed && rollbackFromPhase !== "prepared") {
|
|
1049
|
+
const candidateMayHaveRun = new Set([
|
|
1050
|
+
"activating",
|
|
1051
|
+
"active",
|
|
1052
|
+
"gateway-authorized",
|
|
1053
|
+
"committing",
|
|
1054
|
+
"rolling-back",
|
|
1055
|
+
]).has(rollbackFromPhase);
|
|
1056
|
+
if (candidateMayHaveRun) {
|
|
1057
|
+
await restorePreviousBinary(context, journal, txPaths);
|
|
1058
|
+
await restoreStateDB(context, journal, txPaths);
|
|
1059
|
+
await restoreVersionFile(context, journal.previousVersion);
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
if (signerMayNeedRestart && (await fileMetadata(context.paths.signerPath)).existed) {
|
|
1063
|
+
await context.startPreviousSigner({ requireV2: Boolean(await readRollbackFloor(context)) });
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
journal = await writeJournal(context, { ...journal, phase: "restored" });
|
|
1067
|
+
await cleanupTransactionFiles(context, journal.transactionId);
|
|
1068
|
+
await removeJournal(context);
|
|
1069
|
+
if (!preserveGatewayGate) {
|
|
1070
|
+
await removeUpdateGates(context);
|
|
1071
|
+
}
|
|
1072
|
+
return {
|
|
1073
|
+
transactionId: journal.transactionId,
|
|
1074
|
+
version: journal.version,
|
|
1075
|
+
phase: "rolled-back",
|
|
1076
|
+
changed: journal.changed,
|
|
1077
|
+
};
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
async function authorizeGatewayRelease(request, context) {
|
|
1081
|
+
let journal = await readJournal(context);
|
|
1082
|
+
assertMatchingTransaction(journal, request);
|
|
1083
|
+
if (journal.phase !== "active" && journal.phase !== "gateway-authorized") {
|
|
1084
|
+
throw new Error(`signer transaction cannot authorize Gateway from phase ${journal.phase}`);
|
|
1085
|
+
}
|
|
1086
|
+
if (journal.phase !== "gateway-authorized") {
|
|
1087
|
+
journal = await writeJournal(context, { ...journal, phase: "gateway-authorized" });
|
|
1088
|
+
}
|
|
1089
|
+
try {
|
|
1090
|
+
await writeSignerGate(context, journal);
|
|
1091
|
+
await removeGatewayGate(context);
|
|
1092
|
+
try {
|
|
1093
|
+
await context.startGateway();
|
|
1094
|
+
} catch (error) {
|
|
1095
|
+
if (!/not found|not loaded|no such file/i.test(error?.message || "")) {
|
|
1096
|
+
throw error;
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
} catch (error) {
|
|
1100
|
+
await writeUpdateGates(context, journal).catch(() => undefined);
|
|
1101
|
+
throw error;
|
|
1102
|
+
}
|
|
1103
|
+
return {
|
|
1104
|
+
transactionId: journal.transactionId,
|
|
1105
|
+
version: journal.version,
|
|
1106
|
+
phase: journal.phase,
|
|
1107
|
+
changed: journal.changed,
|
|
1108
|
+
release: journal.release,
|
|
1109
|
+
};
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
async function gateGatewayRelease(request, context) {
|
|
1113
|
+
const journal = await readJournal(context);
|
|
1114
|
+
if (!journal) {
|
|
1115
|
+
const gate = await readGatewayGate(context);
|
|
1116
|
+
if (gate?.transactionId !== request.transactionId || gate?.version !== request.version) {
|
|
1117
|
+
throw new Error("host updater transaction does not exist");
|
|
1118
|
+
}
|
|
1119
|
+
return {
|
|
1120
|
+
transactionId: gate.transactionId,
|
|
1121
|
+
version: gate.version,
|
|
1122
|
+
phase: "rolled-back-gated",
|
|
1123
|
+
changed: false,
|
|
1124
|
+
};
|
|
1125
|
+
}
|
|
1126
|
+
assertMatchingTransaction(journal, request);
|
|
1127
|
+
if (journal.phase === "committing") {
|
|
1128
|
+
throw new Error("cannot gate a Gateway after the signer commit decision");
|
|
1129
|
+
}
|
|
1130
|
+
await writeUpdateGates(context, journal);
|
|
1131
|
+
return {
|
|
1132
|
+
transactionId: journal.transactionId,
|
|
1133
|
+
version: journal.version,
|
|
1134
|
+
phase: journal.phase,
|
|
1135
|
+
changed: journal.changed,
|
|
1136
|
+
release: journal.release,
|
|
1137
|
+
};
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
async function activateSignerRelease(request, context) {
|
|
1141
|
+
let journal = await readJournal(context);
|
|
1142
|
+
assertMatchingTransaction(journal, request);
|
|
1143
|
+
if (journal.phase === "active" || journal.phase === "gateway-authorized") {
|
|
1144
|
+
return {
|
|
1145
|
+
transactionId: journal.transactionId,
|
|
1146
|
+
version: journal.version,
|
|
1147
|
+
phase: journal.phase,
|
|
1148
|
+
changed: journal.changed,
|
|
1149
|
+
release: journal.release,
|
|
1150
|
+
};
|
|
1151
|
+
}
|
|
1152
|
+
if (journal.phase !== "prepared") {
|
|
1153
|
+
throw new Error(`signer transaction cannot activate from phase ${journal.phase}`);
|
|
1154
|
+
}
|
|
1155
|
+
if (!journal.changed) {
|
|
1156
|
+
journal = await writeJournal(context, { ...journal, phase: "active" });
|
|
1157
|
+
return {
|
|
1158
|
+
transactionId: journal.transactionId,
|
|
1159
|
+
version: journal.version,
|
|
1160
|
+
phase: journal.phase,
|
|
1161
|
+
changed: false,
|
|
1162
|
+
release: journal.release,
|
|
1163
|
+
};
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
const txPaths = transactionPaths(context.paths, journal.transactionId);
|
|
1167
|
+
const previousBinary = await fileMetadata(context.paths.signerPath);
|
|
1168
|
+
const stateDB = await fileMetadata(context.paths.signerStateDBPath);
|
|
1169
|
+
journal = await writeJournal(context, {
|
|
1170
|
+
...journal,
|
|
1171
|
+
phase: "snapshotting",
|
|
1172
|
+
previousBinary,
|
|
1173
|
+
stateDB,
|
|
1174
|
+
});
|
|
1175
|
+
try {
|
|
1176
|
+
await context.stopSigner();
|
|
1177
|
+
if (previousBinary.existed) {
|
|
1178
|
+
await atomicCopyFileDurable(context.paths.signerPath, txPaths.previousBinaryPath, {
|
|
1179
|
+
mode: previousBinary.mode,
|
|
1180
|
+
});
|
|
1181
|
+
}
|
|
1182
|
+
if (stateDB.existed) {
|
|
1183
|
+
await atomicCopyFileDurable(context.paths.signerStateDBPath, txPaths.stateDBSnapshotPath, {
|
|
1184
|
+
mode: 0o600,
|
|
1185
|
+
});
|
|
1186
|
+
}
|
|
1187
|
+
journal = await writeJournal(context, { ...journal, phase: "activating" });
|
|
1188
|
+
await fsp.rename(txPaths.candidatePath, context.paths.signerPath);
|
|
1189
|
+
await fsyncDirectory(path.dirname(context.paths.signerPath));
|
|
1190
|
+
const activatedState = await context.startSignerV2({ expectedRelease: journal.release });
|
|
1191
|
+
if (
|
|
1192
|
+
journal.previousSignerInvariant &&
|
|
1193
|
+
activatedState?.invariant !== journal.previousSignerInvariant
|
|
1194
|
+
) {
|
|
1195
|
+
throw new Error(
|
|
1196
|
+
"activated signer did not preserve exact wallet, policy, network, and WebAuthn state",
|
|
1197
|
+
);
|
|
1198
|
+
}
|
|
1199
|
+
await atomicWriteFileDurable(context.paths.versionPath, `${journal.version}\n`, 0o600);
|
|
1200
|
+
journal = await writeJournal(context, { ...journal, phase: "active" });
|
|
1201
|
+
return {
|
|
1202
|
+
transactionId: journal.transactionId,
|
|
1203
|
+
version: journal.version,
|
|
1204
|
+
phase: journal.phase,
|
|
1205
|
+
changed: true,
|
|
1206
|
+
release: journal.release,
|
|
1207
|
+
};
|
|
1208
|
+
} catch (error) {
|
|
1209
|
+
let rollbackError = null;
|
|
1210
|
+
try {
|
|
1211
|
+
await rollbackSignerRelease(request, context, { preserveGatewayGate: true });
|
|
1212
|
+
} catch (caught) {
|
|
1213
|
+
rollbackError = caught;
|
|
1214
|
+
}
|
|
1215
|
+
if (rollbackError) {
|
|
1216
|
+
throw new Error(
|
|
1217
|
+
`signer activation failed and rollback is incomplete: ${error.message}; rollback error: ${rollbackError.message}`,
|
|
1218
|
+
{ cause: error },
|
|
1219
|
+
);
|
|
1220
|
+
}
|
|
1221
|
+
throw new Error(`signer activation failed and was rolled back: ${error.message}`, {
|
|
1222
|
+
cause: error,
|
|
1223
|
+
});
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
async function finishCommit(context, journal) {
|
|
1228
|
+
await writeInitialRollbackFloor(context, journal.version);
|
|
1229
|
+
await removeUpdateGates(context);
|
|
1230
|
+
await cleanupTransactionFiles(context, journal.transactionId);
|
|
1231
|
+
await removeJournal(context);
|
|
1232
|
+
return {
|
|
1233
|
+
transactionId: journal.transactionId,
|
|
1234
|
+
version: journal.version,
|
|
1235
|
+
phase: "committed",
|
|
1236
|
+
changed: journal.changed,
|
|
1237
|
+
release: journal.release,
|
|
1238
|
+
};
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
async function commitSignerRelease(request, context) {
|
|
1242
|
+
let journal = await readJournal(context);
|
|
1243
|
+
if (!journal) {
|
|
1244
|
+
const installed = await readVersionFile(context.paths.versionPath);
|
|
1245
|
+
const floor = await readRollbackFloor(context);
|
|
1246
|
+
if (installed === request.version && floor && compareVersions(request.version, floor) >= 0) {
|
|
1247
|
+
return {
|
|
1248
|
+
transactionId: request.transactionId,
|
|
1249
|
+
version: request.version,
|
|
1250
|
+
phase: "committed",
|
|
1251
|
+
changed: false,
|
|
1252
|
+
};
|
|
1253
|
+
}
|
|
1254
|
+
throw new Error("host updater transaction does not exist");
|
|
1255
|
+
}
|
|
1256
|
+
assertMatchingTransaction(journal, request);
|
|
1257
|
+
if (journal.phase !== "gateway-authorized" && journal.phase !== "committing") {
|
|
1258
|
+
throw new Error(`signer transaction cannot commit from phase ${journal.phase}`);
|
|
1259
|
+
}
|
|
1260
|
+
if (journal.phase !== "committing") {
|
|
1261
|
+
journal = await writeJournal(context, { ...journal, phase: "committing" });
|
|
1262
|
+
}
|
|
1263
|
+
return await finishCommit(context, journal);
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
async function recoverInterruptedTransaction(context) {
|
|
1267
|
+
const journal = await readJournal(context);
|
|
1268
|
+
if (!journal) {
|
|
1269
|
+
return { recovered: false };
|
|
1270
|
+
}
|
|
1271
|
+
const request = {
|
|
1272
|
+
transactionId: journal.transactionId,
|
|
1273
|
+
version: journal.version,
|
|
1274
|
+
};
|
|
1275
|
+
if (journal.phase === "committing") {
|
|
1276
|
+
const result = await finishCommit(context, journal);
|
|
1277
|
+
return { recovered: true, action: "committed", result };
|
|
1278
|
+
}
|
|
1279
|
+
if (
|
|
1280
|
+
journal.phase === "prepared" ||
|
|
1281
|
+
journal.phase === "snapshotting" ||
|
|
1282
|
+
journal.phase === "activating" ||
|
|
1283
|
+
journal.phase === "rolling-back" ||
|
|
1284
|
+
journal.phase === "restored"
|
|
1285
|
+
) {
|
|
1286
|
+
const result = await rollbackSignerRelease(request, context, { preserveGatewayGate: true });
|
|
1287
|
+
return { recovered: true, action: "rolled-back", result };
|
|
1288
|
+
}
|
|
1289
|
+
// Active/authorized is a valid target signer awaiting the application coordinator's
|
|
1290
|
+
// durable commit/rollback decision. Keep signer mutations gated until that
|
|
1291
|
+
// decision; only a Gateway-authorized transaction may run the health probe.
|
|
1292
|
+
if (journal.phase === "gateway-authorized") {
|
|
1293
|
+
await writeSignerGate(context, journal);
|
|
1294
|
+
await removeGatewayGate(context);
|
|
1295
|
+
} else {
|
|
1296
|
+
await writeUpdateGates(context, journal);
|
|
1297
|
+
}
|
|
1298
|
+
return { recovered: true, action: "pending", phase: journal.phase };
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
async function dispatchUpdateRequest(request, context) {
|
|
1302
|
+
switch (request.op) {
|
|
1303
|
+
case "prepareRelease":
|
|
1304
|
+
return await prepareSignerRelease(request, context);
|
|
1305
|
+
case "activateRelease":
|
|
1306
|
+
return await activateSignerRelease(request, context);
|
|
1307
|
+
case "authorizeGatewayRelease":
|
|
1308
|
+
return await authorizeGatewayRelease(request, context);
|
|
1309
|
+
case "gateGatewayRelease":
|
|
1310
|
+
return await gateGatewayRelease(request, context);
|
|
1311
|
+
case "commitRelease":
|
|
1312
|
+
return await commitSignerRelease(request, context);
|
|
1313
|
+
case "rollbackRelease":
|
|
1314
|
+
return await rollbackSignerRelease(request, context);
|
|
1315
|
+
default:
|
|
1316
|
+
throw new Error("unsupported updater transaction request");
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
function writeResponse(socket, payload) {
|
|
1321
|
+
socket.end(`${JSON.stringify(payload)}\n`);
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
export async function startServer() {
|
|
1325
|
+
if (typeof process.getuid !== "function" || process.getuid() !== 0) {
|
|
1326
|
+
throw new Error("hosted signer updater must run as root");
|
|
1327
|
+
}
|
|
1328
|
+
const gidIndex = process.argv.indexOf("--socket-gid");
|
|
1329
|
+
const socketGid = gidIndex >= 0 ? Number(process.argv[gidIndex + 1]) : Number.NaN;
|
|
1330
|
+
if (!Number.isSafeInteger(socketGid) || socketGid <= 0) {
|
|
1331
|
+
throw new Error("--socket-gid must be a positive numeric group id");
|
|
1332
|
+
}
|
|
1333
|
+
const context = createTransactionContext();
|
|
1334
|
+
await recoverInterruptedTransaction(context);
|
|
1335
|
+
await fsp.mkdir(path.dirname(context.paths.socketPath), { recursive: true, mode: 0o750 });
|
|
1336
|
+
await fsp.rm(context.paths.socketPath, { force: true });
|
|
1337
|
+
process.umask(0o117);
|
|
1338
|
+
let queue = Promise.resolve();
|
|
1339
|
+
const server = net.createServer((socket) => {
|
|
1340
|
+
socket.setEncoding("utf8");
|
|
1341
|
+
socket.setTimeout(REQUEST_TIMEOUT_MS);
|
|
1342
|
+
let body = "";
|
|
1343
|
+
let handled = false;
|
|
1344
|
+
const fail = (message) => {
|
|
1345
|
+
if (!handled) {
|
|
1346
|
+
handled = true;
|
|
1347
|
+
writeResponse(socket, { ok: false, error: message });
|
|
1348
|
+
}
|
|
1349
|
+
};
|
|
1350
|
+
socket.on("timeout", () => fail("updater request timed out"));
|
|
1351
|
+
socket.on("error", () => socket.destroy());
|
|
1352
|
+
socket.on("data", (chunk) => {
|
|
1353
|
+
if (handled) {
|
|
1354
|
+
return;
|
|
1355
|
+
}
|
|
1356
|
+
body += chunk;
|
|
1357
|
+
if (body.length > MAX_REQUEST_BYTES) {
|
|
1358
|
+
fail("updater request is too large");
|
|
1359
|
+
return;
|
|
1360
|
+
}
|
|
1361
|
+
const newline = body.indexOf("\n");
|
|
1362
|
+
if (newline < 0) {
|
|
1363
|
+
return;
|
|
1364
|
+
}
|
|
1365
|
+
handled = true;
|
|
1366
|
+
let request;
|
|
1367
|
+
try {
|
|
1368
|
+
request = parseUpdateRequest(JSON.parse(body.slice(0, newline)));
|
|
1369
|
+
} catch (error) {
|
|
1370
|
+
writeResponse(socket, { ok: false, error: error.message });
|
|
1371
|
+
return;
|
|
1372
|
+
}
|
|
1373
|
+
const operation = queue.then(() => dispatchUpdateRequest(request, context));
|
|
1374
|
+
queue = operation.catch(() => undefined);
|
|
1375
|
+
void operation.then(
|
|
1376
|
+
(result) => writeResponse(socket, { ok: true, ...result }),
|
|
1377
|
+
(error) => writeResponse(socket, { ok: false, error: error.message }),
|
|
1378
|
+
);
|
|
1379
|
+
});
|
|
1380
|
+
});
|
|
1381
|
+
await new Promise((resolve, reject) => {
|
|
1382
|
+
server.once("error", reject);
|
|
1383
|
+
server.listen(context.paths.socketPath, resolve);
|
|
1384
|
+
});
|
|
1385
|
+
await fsp.chmod(context.paths.socketPath, 0o660);
|
|
1386
|
+
await fsp.chown(context.paths.socketPath, 0, socketGid);
|
|
1387
|
+
const close = async () => {
|
|
1388
|
+
await new Promise((resolve) => server.close(resolve));
|
|
1389
|
+
await fsp.rm(context.paths.socketPath, { force: true });
|
|
1390
|
+
};
|
|
1391
|
+
process.once("SIGTERM", () => void close().then(() => process.exit(0)));
|
|
1392
|
+
process.once("SIGINT", () => void close().then(() => process.exit(0)));
|
|
1393
|
+
return { server, close };
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
const isMain = process.argv[1]
|
|
1397
|
+
? pathToFileURL(path.resolve(process.argv[1])).href ===
|
|
1398
|
+
pathToFileURL(fileURLToPath(import.meta.url)).href
|
|
1399
|
+
: false;
|
|
1400
|
+
if (isMain) {
|
|
1401
|
+
await startServer();
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
export const __testing = {
|
|
1405
|
+
assertSignerV2Health,
|
|
1406
|
+
activateSignerRelease,
|
|
1407
|
+
authorizeGatewayRelease,
|
|
1408
|
+
commitSignerRelease,
|
|
1409
|
+
compareVersions,
|
|
1410
|
+
createTransactionContext,
|
|
1411
|
+
dispatchUpdateRequest,
|
|
1412
|
+
gateGatewayRelease,
|
|
1413
|
+
prepareSignerRelease,
|
|
1414
|
+
readJournal,
|
|
1415
|
+
recoverInterruptedTransaction,
|
|
1416
|
+
releaseAllowedForChannel,
|
|
1417
|
+
releaseArchitecture,
|
|
1418
|
+
rollbackSignerRelease,
|
|
1419
|
+
transactionPaths,
|
|
1420
|
+
writeJournal,
|
|
1421
|
+
};
|