@fased/fased 0.1.62 → 0.1.65
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 +48 -0
- package/README.md +44 -29
- package/config/dependency-ownership.json +1 -1
- package/config/signer-policies/README.md +58 -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 +78 -0
- package/config/typescript-noemit-baseline.json +680 -0
- package/dist/agents/auth-profiles/store.js +267 -31
- package/dist/agents/bash-tools.exec.js +10 -4
- package/dist/agents/current-model-catalog.js +3 -3
- 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/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-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/skills/env-overrides.js +38 -28
- package/dist/agents/skills/trust.js +93 -0
- package/dist/agents/skills/workspace.js +128 -0
- package/dist/agents/skills-clawhub.js +36 -11
- package/dist/agents/skills-marketplace-scan.js +49 -0
- 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/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/session-reset-model.js +4 -4
- package/dist/brand.js +1 -1
- package/dist/build-info.json +3 -3
- package/dist/canvas-host/a2ui/.bundle.hash +1 -1
- package/dist/channel-web.js +2 -2
- package/dist/cli/gateway-cli/run.js +8 -0
- 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/update-cli/update-command.js +260 -11
- package/dist/commands/auth-choice.apply.api-providers.js +3 -3
- package/dist/commands/doctor-config-flow.js +1 -1
- package/dist/commands/doctor-legacy-config.js +2 -13
- 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 +32 -8
- 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/wallet.js +568 -1911
- package/dist/config/config.js +2 -2
- package/dist/config/io.js +64 -2
- package/dist/config/schema.help.js +8 -7
- package/dist/config/schema.labels.js +5 -4
- package/dist/config/zod-schema.agent-runtime.js +1 -0
- package/dist/config/zod-schema.js +5 -0
- package/dist/control-ui/assets/{app-DLil9VO1.js → app-MaQnPa5G.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-DdE_NIXo.js → index-D4YMr1fA.js} +3 -3
- package/dist/control-ui/assets/mining-commit-DFpB_4JN.js +2 -0
- package/dist/control-ui/assets/{mining-BdhMn18r.js → mining-qMYs4aD-.js} +70 -50
- 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/daemon/systemd-system.js +56 -50
- package/dist/extensions/sat-mining/src/protocol-contract.js +43 -3
- package/dist/extensions/sat-mining/src/rpc-read.js +291 -84
- package/dist/extensions/sat-mining/src/sat-bond-layout.js +5 -2
- package/dist/extensions/sat-mining/src/signer-codec-manifest.js +406 -0
- package/dist/extensions/sat-mining/src/solana-submit.js +1274 -390
- 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/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/mutating-admin-rpc-rate-limit.js +5 -0
- package/dist/gateway/security-path.js +1 -1
- package/dist/gateway/server-http.js +1084 -1205
- package/dist/gateway/server-methods/models.js +1 -1
- package/dist/gateway/server-methods/secrets.js +40 -0
- 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/sessions-patch.js +3 -5
- package/dist/index.js +4 -2
- package/dist/infra/fasedhub.js +64 -1
- package/dist/infra/local-source-paired-update.js +538 -0
- package/dist/infra/restart.js +41 -10
- package/dist/infra/update-runner.js +96 -0
- package/dist/light-update-status.js +11 -11
- package/dist/memory/backend-config.js +2 -1
- package/dist/memory/session-files.js +16 -2
- 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-Bwi2Oq03.js → audio-preflight-I4IngVlJ.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-DzT4L4Ty.js → chunk-items-DLQ6_lLK.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-BrkmtFxB.js → config-schema-D_8FVI1C.js} +6798 -3695
- package/dist/plugin-sdk/device-pair.d.ts +1 -1
- package/dist/plugin-sdk/discord.d.ts +6 -6
- 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 +21 -39
- package/dist/plugin-sdk/index.js +29 -29
- 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-M_ta2lMc.js → monitor-B4KF4Kpo.js} +31 -31
- package/dist/plugin-sdk/{monitor-wYVncM9w.js → monitor-Bb6x1KAG.js} +32 -32
- package/dist/plugin-sdk/{openresponses-http-B2GmeSt4.js → openresponses-http-DCTO8JkL.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-B3GC_7CD.js → pairing-message-D0CoLHRJ.js} +1 -1
- package/dist/plugin-sdk/{pairing-message-B3bHYGaw.d.ts → pairing-message-DHPDFZbC.d.ts} +2 -2
- 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 +565 -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-CxE1mfzj.js → slack-BHmvqgjH.js} +39 -39
- package/dist/plugin-sdk/{slack-actions-BcMkNv1Z.js → slack-actions-DhbMcp8M.js} +11 -11
- package/dist/plugin-sdk/slack.d.ts +6 -6
- package/dist/plugin-sdk/slack.js +9 -9
- package/dist/plugin-sdk/{sticker-cache-CGzGWu00.js → sticker-cache-By5AcorL.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-DaEcYQ8r.js → telegram-actions-DGnBz1YT.js} +30 -30
- package/dist/plugin-sdk/telegram.d.ts +6 -6
- 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-EJGTmL1Q.d.ts +486 -0
- package/dist/plugin-sdk/{types-vcJrC_uv.d.ts → types-BdwdFCD6.d.ts} +2 -3
- package/dist/plugin-sdk/{types-DFAW-n1J.d.ts → types-BrCpYMxn.d.ts} +2 -2
- package/dist/plugin-sdk/{types-DyhXnYUr.d.ts → types-D0kDG8l0.d.ts} +3 -2
- package/dist/plugin-sdk/{types.plugin-_fbu40Re.d.ts → types.plugin-BcSBq42x.d.ts} +8 -3
- package/dist/plugin-sdk/{web-CInY-lCv.js → web-DUJwrTYs.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 +13 -13
- package/dist/plugin-sdk/whatsapp.js +12 -12
- package/dist/plugin-sdk/{wsl-C4DGFkOd.js → wsl-J-HscWxS.js} +3 -3
- package/dist/plugin-sdk/{zod-schema.providers-core-BEbNizPe.d.ts → zod-schema.providers-core-CIzCjXH9.d.ts} +61 -61
- package/dist/providers/refresh.js +1 -1
- package/dist/providers/registry.js +1 -1
- package/dist/secrets/apply.js +1 -1
- package/dist/secrets/runtime.js +1 -1
- 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 +578 -117
- 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 +380 -105
- 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 +92 -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 +17 -28
- package/dist/wallet/wallet-atomic-state.js +53 -0
- package/dist/wallet/wallet-canary.js +0 -1
- package/dist/wallet/wallet-inbound-events.js +16 -11
- package/dist/wallet/wallet-policy-simulation.js +1 -1
- package/dist/wallet/wallet-policy.js +93 -37
- package/dist/wallet/wallet-provider-capabilities.js +28 -4
- package/dist/wallet/wallet-provider-registry.js +57 -44
- package/dist/wallet/wallet-provider-resolver.js +62 -63
- package/dist/wallet/wallet-runtime-config.js +28 -41
- package/dist/wallet/wallet-secrets-store.js +22 -0
- package/dist/wallet/wallet-send-approvals.js +1891 -589
- package/dist/wallet/wallet-send-execution-ledger.js +292 -0
- package/dist/wallet/wallet-settlement-links.js +14 -13
- 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/auto-reply/heartbeat-runner.js +1 -1
- package/dist/web/auto-reply/monitor.js +2 -2
- package/dist/wizard/host-security-capability.js +14 -23
- package/dist/wizard/onboarding.finalize.js +63 -184
- package/dist/wizard/onboarding.host-security.js +117 -1367
- package/dist/wizard/onboarding.js +172 -217
- package/dist/wizard/onboarding.wallet.js +232 -785
- 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 +587 -154
- 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 +91 -5
- package/extensions/sat-mining/src/claim-service.ts +85 -14
- package/extensions/sat-mining/src/client.ts +13 -0
- 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 +431 -106
- package/extensions/sat-mining/src/runtime.ts +16 -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 +1544 -436
- package/extensions/sat-mining/src/submission-ledger.ts +434 -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 +4951 -0
- package/package.json +23 -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 +1442 -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 +183 -108
- 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 +173 -360
- package/dist/control-ui/assets/mining-commit-R-6nu87p.js +0 -2
- package/dist/plugin-sdk/tmp-fased-dir-CS6zsYEE.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,1442 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { spawn } 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 os from "node:os";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import readline from "node:readline/promises";
|
|
10
|
+
import { pathToFileURL } from "node:url";
|
|
11
|
+
|
|
12
|
+
const MAX_POLICY_BYTES = 64 * 1024;
|
|
13
|
+
const MAX_ADMIN_OUTPUT_BYTES = 128 * 1024;
|
|
14
|
+
const ADMIN_TIMEOUT_MS = 20_000;
|
|
15
|
+
const FEDERATION_POLICY_DOMAIN = "domain:fased:federation-bond-challenge-v1";
|
|
16
|
+
const SYSTEM_PROGRAM = "11111111111111111111111111111111";
|
|
17
|
+
const TOKEN_PROGRAM = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA";
|
|
18
|
+
const TOKEN_2022_PROGRAM = "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb";
|
|
19
|
+
const ASSOCIATED_TOKEN_PROGRAM = "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL";
|
|
20
|
+
const ADDRESS_LOOKUP_TABLE_PROGRAM = "AddressLookupTab1e1111111111111111111111111";
|
|
21
|
+
const NATIVE_FEE_RESERVATION_LAMPORTS = 5_000_000n;
|
|
22
|
+
const UINT64_MAX = 18_446_744_073_709_551_615n;
|
|
23
|
+
const BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
|
24
|
+
const BASE58_INDEX = new Map(
|
|
25
|
+
Array.from(BASE58_ALPHABET).map((character, index) => [character, index]),
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
const POLICY_KEYS = ["walletId", "role", "operations", "programs", "assets"];
|
|
29
|
+
const ASSET_KEYS = ["asset", "destinations", "maxPerTx", "maxDaily"];
|
|
30
|
+
const STORED_POLICY_KEYS = [...POLICY_KEYS.slice(0, 2), "version", ...POLICY_KEYS.slice(2), "hash"];
|
|
31
|
+
const POLICY_ROLES = new Set(["agent", "mining", "vault"]);
|
|
32
|
+
const POLICY_OPERATIONS = new Set([
|
|
33
|
+
"solana.nativeTransfer",
|
|
34
|
+
"solana.splTransferChecked",
|
|
35
|
+
"federation.bondChallenge",
|
|
36
|
+
"solana.jupiter.swap",
|
|
37
|
+
"solana.jupiter.trigger.auth",
|
|
38
|
+
"solana.jupiter.trigger.create",
|
|
39
|
+
"solana.jupiter.trigger.deposit",
|
|
40
|
+
"solana.jupiter.trigger.cancel",
|
|
41
|
+
"solana.jupiter.trigger.withdraw",
|
|
42
|
+
]);
|
|
43
|
+
const SAT_MINING_ACTIONS = new Set([
|
|
44
|
+
"abortEmptyCycle",
|
|
45
|
+
"claimCycleRewards",
|
|
46
|
+
"claimCycleRewardsBatch",
|
|
47
|
+
"claimProtocolDistributorSat",
|
|
48
|
+
"claimProtocolTreasury",
|
|
49
|
+
"cleanupBatch",
|
|
50
|
+
"closeCommitPhase",
|
|
51
|
+
"closeResolvedCycleArtifacts",
|
|
52
|
+
"closeResolvedCycleRegistryPage",
|
|
53
|
+
"closeResolvedMinerCycleState",
|
|
54
|
+
"commitCycle",
|
|
55
|
+
"compactPendingCycleRange",
|
|
56
|
+
"depositMinerCapital",
|
|
57
|
+
"distributeCyclePage",
|
|
58
|
+
"finalizeCycleSettlement",
|
|
59
|
+
"initMinerCapital",
|
|
60
|
+
"initializeCycle",
|
|
61
|
+
"openCycle",
|
|
62
|
+
"openDispute",
|
|
63
|
+
"refillRegistryReserveFromTreasury",
|
|
64
|
+
"releaseUnrevealedCommit",
|
|
65
|
+
"republishEpochRoots",
|
|
66
|
+
"resolveDispute",
|
|
67
|
+
"retargetUnlock",
|
|
68
|
+
"revealCycle",
|
|
69
|
+
"scoreCyclePage",
|
|
70
|
+
"sealCycleEntropy",
|
|
71
|
+
"setActiveCommit",
|
|
72
|
+
"settleCyclePage",
|
|
73
|
+
"topUpRegistryReserve",
|
|
74
|
+
"validatorAttestation",
|
|
75
|
+
"withdrawMinerCapital",
|
|
76
|
+
]);
|
|
77
|
+
const SAT_LOOKUP_TABLE_ACTIONS = new Set(["create", "extend", "deactivate", "close"]);
|
|
78
|
+
const VAULT_BOND_ACTIONS = new Set([
|
|
79
|
+
"cancelBondUnlock",
|
|
80
|
+
"claimBondStakingRewards",
|
|
81
|
+
"claimUnallocatedStakingRewards",
|
|
82
|
+
"finalizeBondUnlock",
|
|
83
|
+
"increaseBondPosition",
|
|
84
|
+
"openBondPosition",
|
|
85
|
+
"requestBondUnlock",
|
|
86
|
+
"syncBondStakingPosition",
|
|
87
|
+
"syncBondStakingRewards",
|
|
88
|
+
"updateBondTierPolicy",
|
|
89
|
+
]);
|
|
90
|
+
const ASSOCIATED_TOKEN_PROGRAM_ACTIONS = new Set([
|
|
91
|
+
"claimBondStakingRewards",
|
|
92
|
+
"claimCycleRewards",
|
|
93
|
+
"claimCycleRewardsBatch",
|
|
94
|
+
"claimProtocolDistributorSat",
|
|
95
|
+
"claimProtocolTreasury",
|
|
96
|
+
"claimUnallocatedStakingRewards",
|
|
97
|
+
"finalizeBondUnlock",
|
|
98
|
+
"increaseBondPosition",
|
|
99
|
+
"openBondPosition",
|
|
100
|
+
]);
|
|
101
|
+
|
|
102
|
+
function compareCanonicalString(left, right) {
|
|
103
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const HOSTING_PATHS = Object.freeze({
|
|
107
|
+
binaryPath: "/opt/fased/signer/fased-signerd",
|
|
108
|
+
controlSocketPath: "/run/fased-signerd/control.sock",
|
|
109
|
+
runuserPath: "/usr/sbin/runuser",
|
|
110
|
+
signerUser: "fased-signer",
|
|
111
|
+
signerHome: "/var/lib/fased-signerd",
|
|
112
|
+
passwdPath: "/etc/passwd",
|
|
113
|
+
updateGateDirectory: "/var/lib/fased-signer-update-gate",
|
|
114
|
+
updateGatePath: "/var/lib/fased-signer-update-gate/active",
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
class StrictJsonParser {
|
|
118
|
+
constructor(text) {
|
|
119
|
+
this.text = text;
|
|
120
|
+
this.offset = 0;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
parse() {
|
|
124
|
+
const value = this.parseValue();
|
|
125
|
+
this.skipWhitespace();
|
|
126
|
+
if (this.offset !== this.text.length) {
|
|
127
|
+
throw new Error("policy file must contain exactly one JSON value");
|
|
128
|
+
}
|
|
129
|
+
return value;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
skipWhitespace() {
|
|
133
|
+
while (
|
|
134
|
+
this.offset < this.text.length &&
|
|
135
|
+
(this.text[this.offset] === " " ||
|
|
136
|
+
this.text[this.offset] === "\t" ||
|
|
137
|
+
this.text[this.offset] === "\n" ||
|
|
138
|
+
this.text[this.offset] === "\r")
|
|
139
|
+
) {
|
|
140
|
+
this.offset += 1;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
parseValue() {
|
|
145
|
+
this.skipWhitespace();
|
|
146
|
+
const character = this.text[this.offset];
|
|
147
|
+
if (character === "{") {
|
|
148
|
+
return this.parseObject();
|
|
149
|
+
}
|
|
150
|
+
if (character === "[") {
|
|
151
|
+
return this.parseArray();
|
|
152
|
+
}
|
|
153
|
+
if (character === '"') {
|
|
154
|
+
return this.parseString();
|
|
155
|
+
}
|
|
156
|
+
if (character === "t") {
|
|
157
|
+
return this.parseLiteral("true", true);
|
|
158
|
+
}
|
|
159
|
+
if (character === "f") {
|
|
160
|
+
return this.parseLiteral("false", false);
|
|
161
|
+
}
|
|
162
|
+
if (character === "n") {
|
|
163
|
+
return this.parseLiteral("null", null);
|
|
164
|
+
}
|
|
165
|
+
if (character === "-" || (character >= "0" && character <= "9")) {
|
|
166
|
+
return this.parseNumber();
|
|
167
|
+
}
|
|
168
|
+
throw new Error(`invalid JSON value at byte ${this.offset}`);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
parseObject() {
|
|
172
|
+
this.offset += 1;
|
|
173
|
+
const value = Object.create(null);
|
|
174
|
+
const keys = new Set();
|
|
175
|
+
this.skipWhitespace();
|
|
176
|
+
if (this.text[this.offset] === "}") {
|
|
177
|
+
this.offset += 1;
|
|
178
|
+
return value;
|
|
179
|
+
}
|
|
180
|
+
while (true) {
|
|
181
|
+
this.skipWhitespace();
|
|
182
|
+
if (this.text[this.offset] !== '"') {
|
|
183
|
+
throw new Error("JSON object keys must be strings");
|
|
184
|
+
}
|
|
185
|
+
const key = this.parseString();
|
|
186
|
+
if (keys.has(key)) {
|
|
187
|
+
throw new Error(`duplicate JSON field ${JSON.stringify(key)}`);
|
|
188
|
+
}
|
|
189
|
+
keys.add(key);
|
|
190
|
+
this.skipWhitespace();
|
|
191
|
+
if (this.text[this.offset] !== ":") {
|
|
192
|
+
throw new Error("JSON object field is missing ':'");
|
|
193
|
+
}
|
|
194
|
+
this.offset += 1;
|
|
195
|
+
value[key] = this.parseValue();
|
|
196
|
+
this.skipWhitespace();
|
|
197
|
+
const delimiter = this.text[this.offset];
|
|
198
|
+
if (delimiter === "}") {
|
|
199
|
+
this.offset += 1;
|
|
200
|
+
return value;
|
|
201
|
+
}
|
|
202
|
+
if (delimiter !== ",") {
|
|
203
|
+
throw new Error("JSON object fields must be comma separated");
|
|
204
|
+
}
|
|
205
|
+
this.offset += 1;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
parseArray() {
|
|
210
|
+
this.offset += 1;
|
|
211
|
+
const value = [];
|
|
212
|
+
this.skipWhitespace();
|
|
213
|
+
if (this.text[this.offset] === "]") {
|
|
214
|
+
this.offset += 1;
|
|
215
|
+
return value;
|
|
216
|
+
}
|
|
217
|
+
while (true) {
|
|
218
|
+
value.push(this.parseValue());
|
|
219
|
+
this.skipWhitespace();
|
|
220
|
+
const delimiter = this.text[this.offset];
|
|
221
|
+
if (delimiter === "]") {
|
|
222
|
+
this.offset += 1;
|
|
223
|
+
return value;
|
|
224
|
+
}
|
|
225
|
+
if (delimiter !== ",") {
|
|
226
|
+
throw new Error("JSON array values must be comma separated");
|
|
227
|
+
}
|
|
228
|
+
this.offset += 1;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
parseString() {
|
|
233
|
+
const start = this.offset;
|
|
234
|
+
this.offset += 1;
|
|
235
|
+
let escaped = false;
|
|
236
|
+
while (this.offset < this.text.length) {
|
|
237
|
+
const character = this.text[this.offset];
|
|
238
|
+
this.offset += 1;
|
|
239
|
+
if (escaped) {
|
|
240
|
+
escaped = false;
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
if (character === "\\") {
|
|
244
|
+
escaped = true;
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
if (character === '"') {
|
|
248
|
+
const token = this.text.slice(start, this.offset);
|
|
249
|
+
try {
|
|
250
|
+
return JSON.parse(token);
|
|
251
|
+
} catch {
|
|
252
|
+
throw new Error("invalid JSON string");
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
throw new Error("unterminated JSON string");
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
parseLiteral(token, value) {
|
|
260
|
+
if (this.text.slice(this.offset, this.offset + token.length) !== token) {
|
|
261
|
+
throw new Error(`invalid JSON literal at byte ${this.offset}`);
|
|
262
|
+
}
|
|
263
|
+
this.offset += token.length;
|
|
264
|
+
return value;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
parseNumber() {
|
|
268
|
+
const match = this.text
|
|
269
|
+
.slice(this.offset)
|
|
270
|
+
.match(/^-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?/u);
|
|
271
|
+
if (!match) {
|
|
272
|
+
throw new Error(`invalid JSON number at byte ${this.offset}`);
|
|
273
|
+
}
|
|
274
|
+
this.offset += match[0].length;
|
|
275
|
+
const value = Number(match[0]);
|
|
276
|
+
if (!Number.isFinite(value)) {
|
|
277
|
+
throw new Error("JSON number is outside the supported range");
|
|
278
|
+
}
|
|
279
|
+
return value;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function parseStrictJson(raw, description = "JSON") {
|
|
284
|
+
let text;
|
|
285
|
+
try {
|
|
286
|
+
text = new TextDecoder("utf-8", { fatal: true }).decode(raw);
|
|
287
|
+
} catch {
|
|
288
|
+
throw new Error(`${description} must be valid UTF-8`);
|
|
289
|
+
}
|
|
290
|
+
if (text.startsWith("\uFEFF")) {
|
|
291
|
+
throw new Error(`${description} must not contain a byte-order mark`);
|
|
292
|
+
}
|
|
293
|
+
try {
|
|
294
|
+
return new StrictJsonParser(text).parse();
|
|
295
|
+
} catch (error) {
|
|
296
|
+
throw new Error(`${description} is not strict JSON: ${error.message}`, { cause: error });
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function requirePlainObject(value, field) {
|
|
301
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
302
|
+
throw new Error(`${field} must be a JSON object`);
|
|
303
|
+
}
|
|
304
|
+
return value;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function requireExactKeys(value, keys, field) {
|
|
308
|
+
const actual = Object.keys(value).toSorted(compareCanonicalString);
|
|
309
|
+
const expected = [...keys].toSorted(compareCanonicalString);
|
|
310
|
+
if (actual.length !== expected.length || actual.some((key, index) => key !== expected[index])) {
|
|
311
|
+
const unknown = actual.filter((key) => !expected.includes(key));
|
|
312
|
+
const missing = expected.filter((key) => !actual.includes(key));
|
|
313
|
+
const details = [
|
|
314
|
+
unknown.length ? `unknown: ${unknown.join(", ")}` : "",
|
|
315
|
+
missing.length ? `missing: ${missing.join(", ")}` : "",
|
|
316
|
+
]
|
|
317
|
+
.filter(Boolean)
|
|
318
|
+
.join("; ");
|
|
319
|
+
throw new Error(`${field} must contain exactly ${keys.join(", ")} (${details})`);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function requireCanonicalWalletID(value) {
|
|
324
|
+
if (typeof value !== "string" || value.length < 1 || value.length > 64) {
|
|
325
|
+
throw new Error("walletId must contain 1 to 64 characters");
|
|
326
|
+
}
|
|
327
|
+
if (!/^[a-z0-9]+(?:_[a-z0-9]+)*$/u.test(value)) {
|
|
328
|
+
throw new Error(
|
|
329
|
+
"walletId must use normalized lowercase letters, numbers, and single underscores",
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
return value;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function decodeBase58(value) {
|
|
336
|
+
let number = 0n;
|
|
337
|
+
for (const character of value) {
|
|
338
|
+
const digit = BASE58_INDEX.get(character);
|
|
339
|
+
if (digit === undefined) {
|
|
340
|
+
return null;
|
|
341
|
+
}
|
|
342
|
+
number = number * 58n + BigInt(digit);
|
|
343
|
+
}
|
|
344
|
+
const bytes = [];
|
|
345
|
+
while (number > 0n) {
|
|
346
|
+
bytes.push(Number(number & 0xffn));
|
|
347
|
+
number >>= 8n;
|
|
348
|
+
}
|
|
349
|
+
bytes.reverse();
|
|
350
|
+
let leadingZeros = 0;
|
|
351
|
+
while (value[leadingZeros] === "1") {
|
|
352
|
+
leadingZeros += 1;
|
|
353
|
+
}
|
|
354
|
+
return Buffer.concat([Buffer.alloc(leadingZeros), Buffer.from(bytes)]);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function encodeBase58(bytes) {
|
|
358
|
+
let leadingZeros = 0;
|
|
359
|
+
while (leadingZeros < bytes.length && bytes[leadingZeros] === 0) {
|
|
360
|
+
leadingZeros += 1;
|
|
361
|
+
}
|
|
362
|
+
let number = 0n;
|
|
363
|
+
for (const byte of bytes) {
|
|
364
|
+
number = (number << 8n) | BigInt(byte);
|
|
365
|
+
}
|
|
366
|
+
let suffix = "";
|
|
367
|
+
while (number > 0n) {
|
|
368
|
+
suffix = BASE58_ALPHABET[Number(number % 58n)] + suffix;
|
|
369
|
+
number /= 58n;
|
|
370
|
+
}
|
|
371
|
+
return "1".repeat(leadingZeros) + suffix;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function requireSolanaPublicKey(value, field) {
|
|
375
|
+
if (
|
|
376
|
+
typeof value !== "string" ||
|
|
377
|
+
value.trim() !== value ||
|
|
378
|
+
value.length < 32 ||
|
|
379
|
+
value.length > 44
|
|
380
|
+
) {
|
|
381
|
+
throw new Error(`${field} must be a canonical Solana public key`);
|
|
382
|
+
}
|
|
383
|
+
const decoded = decodeBase58(value);
|
|
384
|
+
if (!decoded || decoded.length !== 32 || encodeBase58(decoded) !== value) {
|
|
385
|
+
throw new Error(`${field} must be a canonical Solana public key`);
|
|
386
|
+
}
|
|
387
|
+
return value;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function requireUniqueStrings(values, field, normalize, { allowEmpty = false, max = 128 } = {}) {
|
|
391
|
+
if (!Array.isArray(values) || (!allowEmpty && values.length === 0) || values.length > max) {
|
|
392
|
+
throw new Error(
|
|
393
|
+
`${field} must be ${allowEmpty ? "an" : "a non-empty"} array of at most ${max} values`,
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
const seen = new Set();
|
|
397
|
+
const normalized = values.map((value, index) => {
|
|
398
|
+
const result = normalize(value, `${field}[${index}]`);
|
|
399
|
+
if (seen.has(result)) {
|
|
400
|
+
throw new Error(`${field} contains duplicate value ${result}`);
|
|
401
|
+
}
|
|
402
|
+
seen.add(result);
|
|
403
|
+
return result;
|
|
404
|
+
});
|
|
405
|
+
return normalized.toSorted(compareCanonicalString);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function requireCanonicalPositiveCap(value, field) {
|
|
409
|
+
if (typeof value !== "string" || !/^[1-9][0-9]*$/u.test(value)) {
|
|
410
|
+
throw new Error(`${field} must be a canonical positive raw-unit integer string`);
|
|
411
|
+
}
|
|
412
|
+
const amount = BigInt(value);
|
|
413
|
+
if (amount > UINT64_MAX) {
|
|
414
|
+
throw new Error(`${field} exceeds uint64`);
|
|
415
|
+
}
|
|
416
|
+
return value;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function normalizeAssetName(value, field) {
|
|
420
|
+
if (typeof value !== "string" || value.trim() !== value) {
|
|
421
|
+
throw new Error(`${field} must be a canonical policy asset`);
|
|
422
|
+
}
|
|
423
|
+
if (
|
|
424
|
+
value === "solana:native" ||
|
|
425
|
+
value === "sat:action" ||
|
|
426
|
+
value === "sat:capital:lamports" ||
|
|
427
|
+
value === "federation:bond-challenge"
|
|
428
|
+
) {
|
|
429
|
+
return value;
|
|
430
|
+
}
|
|
431
|
+
for (const prefix of ["solana:spl:", "sat:mint:"]) {
|
|
432
|
+
if (value.startsWith(prefix)) {
|
|
433
|
+
return `${prefix}${requireSolanaPublicKey(value.slice(prefix.length), `${field} mint`)}`;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
throw new Error(`${field} is not a supported policy asset`);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function normalizePolicyOperation(value, role, field) {
|
|
440
|
+
if (typeof value !== "string" || value.trim() !== value) {
|
|
441
|
+
throw new Error(`${field} is not a supported typed signer operation`);
|
|
442
|
+
}
|
|
443
|
+
if (POLICY_OPERATIONS.has(value)) {
|
|
444
|
+
return value;
|
|
445
|
+
}
|
|
446
|
+
const match = /^(sat|satLookup|vaultBond)\.([A-Za-z][A-Za-z0-9]*)@(.+)$/u.exec(value);
|
|
447
|
+
if (!match) {
|
|
448
|
+
if (value === "solana.satAction" || value === "solana.vaultBondAction") {
|
|
449
|
+
throw new Error(`${field} must name an exact action bound to its SAT program`);
|
|
450
|
+
}
|
|
451
|
+
throw new Error(`${field} is not a supported typed signer operation`);
|
|
452
|
+
}
|
|
453
|
+
const [, family, action, rawProgram] = match;
|
|
454
|
+
const program = requireSolanaPublicKey(rawProgram, `${field} program`);
|
|
455
|
+
if (family === "sat") {
|
|
456
|
+
if (role !== "mining" || !SAT_MINING_ACTIONS.has(action)) {
|
|
457
|
+
throw new Error(`${field} is not an allowed program-bound Mining action`);
|
|
458
|
+
}
|
|
459
|
+
} else if (family === "satLookup") {
|
|
460
|
+
if (
|
|
461
|
+
role !== "mining" ||
|
|
462
|
+
!SAT_LOOKUP_TABLE_ACTIONS.has(action) ||
|
|
463
|
+
program !== ADDRESS_LOOKUP_TABLE_PROGRAM
|
|
464
|
+
) {
|
|
465
|
+
throw new Error(`${field} is not an allowed typed Mining lookup-table action`);
|
|
466
|
+
}
|
|
467
|
+
} else if (role !== "vault" || !VAULT_BOND_ACTIONS.has(action)) {
|
|
468
|
+
throw new Error(`${field} is not an allowed program-bound Vault bond action`);
|
|
469
|
+
}
|
|
470
|
+
return `${family}.${action}@${program}`;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function validatePolicyRelationships(policy) {
|
|
474
|
+
const operations = new Set(policy.operations);
|
|
475
|
+
const programs = new Set(policy.programs);
|
|
476
|
+
const assets = new Set(policy.assets.map((asset) => asset.asset));
|
|
477
|
+
const hasFederation = operations.has("federation.bondChallenge");
|
|
478
|
+
|
|
479
|
+
if (operations.has("federation.bondChallenge") && policy.role !== "vault") {
|
|
480
|
+
throw new Error("federation operations require the immutable vault role");
|
|
481
|
+
}
|
|
482
|
+
for (const operation of policy.operations) {
|
|
483
|
+
const separator = operation.lastIndexOf("@");
|
|
484
|
+
if (separator >= 0 && !programs.has(operation.slice(separator + 1))) {
|
|
485
|
+
throw new Error(`program-bound operation ${operation} requires the same program in programs`);
|
|
486
|
+
}
|
|
487
|
+
const action = separator >= 0 ? operation.slice(operation.indexOf(".") + 1, separator) : "";
|
|
488
|
+
if (ASSOCIATED_TOKEN_PROGRAM_ACTIONS.has(action) && !programs.has(ASSOCIATED_TOKEN_PROGRAM)) {
|
|
489
|
+
throw new Error(
|
|
490
|
+
`program-bound operation ${operation} requires the Associated Token program used by its exact native codec`,
|
|
491
|
+
);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
if (operations.has("solana.nativeTransfer")) {
|
|
495
|
+
if (!programs.has(SYSTEM_PROGRAM) || !assets.has("solana:native")) {
|
|
496
|
+
throw new Error("solana.nativeTransfer requires the System program and solana:native asset");
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
if (operations.has("solana.splTransferChecked")) {
|
|
500
|
+
const hasTokenProgram = programs.has(TOKEN_PROGRAM) || programs.has(TOKEN_2022_PROGRAM);
|
|
501
|
+
const hasSPLAsset = [...assets].some((asset) => asset.startsWith("solana:spl:"));
|
|
502
|
+
if (!hasTokenProgram || !hasSPLAsset) {
|
|
503
|
+
throw new Error(
|
|
504
|
+
"solana.splTransferChecked requires a token program and explicit SPL asset; signer-funded associated-token-account creation is not permitted",
|
|
505
|
+
);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
const hasOnChainOperation = policy.operations.some(
|
|
509
|
+
(operation) => operation !== "federation.bondChallenge",
|
|
510
|
+
);
|
|
511
|
+
if (hasOnChainOperation) {
|
|
512
|
+
const native = policy.assets.find((asset) => asset.asset === "solana:native");
|
|
513
|
+
if (
|
|
514
|
+
!native ||
|
|
515
|
+
BigInt(native.maxPerTx) < NATIVE_FEE_RESERVATION_LAMPORTS ||
|
|
516
|
+
BigInt(native.maxDaily) < NATIVE_FEE_RESERVATION_LAMPORTS
|
|
517
|
+
) {
|
|
518
|
+
throw new Error(
|
|
519
|
+
`on-chain signer policies require solana:native maxPerTx and maxDaily of at least ${NATIVE_FEE_RESERVATION_LAMPORTS} lamports for the signer-controlled fee/rent reservation`,
|
|
520
|
+
);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
if (hasFederation) {
|
|
524
|
+
if (!programs.has(FEDERATION_POLICY_DOMAIN) || !assets.has("federation:bond-challenge")) {
|
|
525
|
+
throw new Error(
|
|
526
|
+
`federation.bondChallenge requires ${FEDERATION_POLICY_DOMAIN} and federation:bond-challenge`,
|
|
527
|
+
);
|
|
528
|
+
}
|
|
529
|
+
} else if (programs.has(FEDERATION_POLICY_DOMAIN) || assets.has("federation:bond-challenge")) {
|
|
530
|
+
throw new Error("the federation policy domain and asset require federation.bondChallenge");
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
export function normalizeOwnerPolicy(value) {
|
|
535
|
+
const object = requirePlainObject(value, "policy");
|
|
536
|
+
requireExactKeys(object, POLICY_KEYS, "policy");
|
|
537
|
+
const walletId = requireCanonicalWalletID(object.walletId);
|
|
538
|
+
if (typeof object.role !== "string" || !POLICY_ROLES.has(object.role)) {
|
|
539
|
+
throw new Error("policy role must be agent, mining, or vault");
|
|
540
|
+
}
|
|
541
|
+
const operations = requireUniqueStrings(
|
|
542
|
+
object.operations,
|
|
543
|
+
"operations",
|
|
544
|
+
(operation, field) => normalizePolicyOperation(operation, object.role, field),
|
|
545
|
+
{ max: 128 },
|
|
546
|
+
);
|
|
547
|
+
const programs = requireUniqueStrings(
|
|
548
|
+
object.programs,
|
|
549
|
+
"programs",
|
|
550
|
+
(program, field) =>
|
|
551
|
+
program === FEDERATION_POLICY_DOMAIN ? program : requireSolanaPublicKey(program, field),
|
|
552
|
+
{ max: 64 },
|
|
553
|
+
);
|
|
554
|
+
if (!Array.isArray(object.assets) || object.assets.length < 1 || object.assets.length > 64) {
|
|
555
|
+
throw new Error("assets must be a non-empty array of at most 64 entries");
|
|
556
|
+
}
|
|
557
|
+
const seenAssets = new Set();
|
|
558
|
+
const assets = object.assets.map((entry, index) => {
|
|
559
|
+
const assetObject = requirePlainObject(entry, `assets[${index}]`);
|
|
560
|
+
requireExactKeys(assetObject, ASSET_KEYS, `assets[${index}]`);
|
|
561
|
+
const asset = normalizeAssetName(assetObject.asset, `assets[${index}].asset`);
|
|
562
|
+
if (seenAssets.has(asset)) {
|
|
563
|
+
throw new Error(`assets contains duplicate policy asset ${asset}`);
|
|
564
|
+
}
|
|
565
|
+
seenAssets.add(asset);
|
|
566
|
+
const destinations = requireUniqueStrings(
|
|
567
|
+
assetObject.destinations,
|
|
568
|
+
`assets[${index}].destinations`,
|
|
569
|
+
requireSolanaPublicKey,
|
|
570
|
+
{ max: 128 },
|
|
571
|
+
);
|
|
572
|
+
const maxPerTx = requireCanonicalPositiveCap(assetObject.maxPerTx, `assets[${index}].maxPerTx`);
|
|
573
|
+
const maxDaily = requireCanonicalPositiveCap(assetObject.maxDaily, `assets[${index}].maxDaily`);
|
|
574
|
+
if (BigInt(maxDaily) < BigInt(maxPerTx)) {
|
|
575
|
+
throw new Error(`assets[${index}].maxDaily must be at least maxPerTx`);
|
|
576
|
+
}
|
|
577
|
+
return { asset, destinations, maxPerTx, maxDaily };
|
|
578
|
+
});
|
|
579
|
+
const sortedAssets = assets.toSorted((left, right) =>
|
|
580
|
+
compareCanonicalString(left.asset, right.asset),
|
|
581
|
+
);
|
|
582
|
+
const policy = { walletId, role: object.role, operations, programs, assets: sortedAssets };
|
|
583
|
+
validatePolicyRelationships(policy);
|
|
584
|
+
return policy;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function sha256(value) {
|
|
588
|
+
return `sha256:${createHash("sha256").update(value).digest("hex")}`;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
function policyWithVersion(policy, version) {
|
|
592
|
+
const unhashed = {
|
|
593
|
+
walletId: policy.walletId,
|
|
594
|
+
role: policy.role,
|
|
595
|
+
version,
|
|
596
|
+
operations: policy.operations,
|
|
597
|
+
programs: policy.programs,
|
|
598
|
+
assets: policy.assets,
|
|
599
|
+
hash: "",
|
|
600
|
+
};
|
|
601
|
+
return { ...unhashed, hash: sha256(JSON.stringify(unhashed)) };
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
function normalizeStoredPolicy(raw) {
|
|
605
|
+
const object = requirePlainObject(raw, "signer policy response");
|
|
606
|
+
requireExactKeys(object, STORED_POLICY_KEYS, "signer policy response");
|
|
607
|
+
if (!Number.isSafeInteger(object.version) || object.version < 1) {
|
|
608
|
+
throw new Error("signer policy response has an invalid version");
|
|
609
|
+
}
|
|
610
|
+
if (typeof object.hash !== "string" || !/^sha256:[0-9a-f]{64}$/u.test(object.hash)) {
|
|
611
|
+
throw new Error("signer policy response has an invalid hash");
|
|
612
|
+
}
|
|
613
|
+
const policy = normalizeOwnerPolicy({
|
|
614
|
+
walletId: object.walletId,
|
|
615
|
+
role: object.role,
|
|
616
|
+
operations: object.operations,
|
|
617
|
+
programs: object.programs,
|
|
618
|
+
assets: object.assets,
|
|
619
|
+
});
|
|
620
|
+
const expected = policyWithVersion(policy, object.version);
|
|
621
|
+
if (expected.hash !== object.hash) {
|
|
622
|
+
throw new Error("signer policy response hash is not canonical");
|
|
623
|
+
}
|
|
624
|
+
return expected;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
function normalizeLockedStoredPolicy(raw) {
|
|
628
|
+
const object = requirePlainObject(raw, "current signer policy");
|
|
629
|
+
requireExactKeys(object, STORED_POLICY_KEYS, "current signer policy");
|
|
630
|
+
const walletId = requireCanonicalWalletID(object.walletId);
|
|
631
|
+
if (typeof object.role !== "string" || !POLICY_ROLES.has(object.role)) {
|
|
632
|
+
throw new Error("current signer policy has an invalid role");
|
|
633
|
+
}
|
|
634
|
+
if (object.version !== 1) {
|
|
635
|
+
throw new Error("initial policy setup requires current policy version 1");
|
|
636
|
+
}
|
|
637
|
+
for (const field of ["operations", "programs", "assets"]) {
|
|
638
|
+
if (!Array.isArray(object[field]) || object[field].length !== 0) {
|
|
639
|
+
throw new Error("initial policy setup requires a genuinely deny-all current policy");
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
const policy = { walletId, role: object.role, operations: [], programs: [], assets: [] };
|
|
643
|
+
const expected = policyWithVersion(policy, 1);
|
|
644
|
+
if (object.hash !== expected.hash) {
|
|
645
|
+
throw new Error("current deny-all policy hash is not canonical");
|
|
646
|
+
}
|
|
647
|
+
return expected;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
function samePolicy(left, right) {
|
|
651
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
function statsIdentity(stats) {
|
|
655
|
+
return [
|
|
656
|
+
stats.dev,
|
|
657
|
+
stats.ino,
|
|
658
|
+
stats.size,
|
|
659
|
+
stats.mode,
|
|
660
|
+
stats.uid,
|
|
661
|
+
stats.gid,
|
|
662
|
+
stats.nlink,
|
|
663
|
+
stats.mtimeNs,
|
|
664
|
+
stats.ctimeNs,
|
|
665
|
+
]
|
|
666
|
+
.map(String)
|
|
667
|
+
.join(":");
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
async function assertSecureAncestors(
|
|
671
|
+
targetPath,
|
|
672
|
+
allowedUIDs,
|
|
673
|
+
boundary = path.parse(targetPath).root,
|
|
674
|
+
) {
|
|
675
|
+
let current = path.dirname(targetPath);
|
|
676
|
+
const stop = path.resolve(boundary);
|
|
677
|
+
const stopPrefix = stop.endsWith(path.sep) ? stop : `${stop}${path.sep}`;
|
|
678
|
+
while (true) {
|
|
679
|
+
const stats = await fsp.lstat(current, { bigint: true });
|
|
680
|
+
if (!stats.isDirectory() || stats.isSymbolicLink()) {
|
|
681
|
+
throw new Error(`unsafe path parent: ${current} must be a non-symlink directory`);
|
|
682
|
+
}
|
|
683
|
+
const permissions = Number(stats.mode & 0o777n);
|
|
684
|
+
if ((permissions & 0o022) !== 0) {
|
|
685
|
+
throw new Error(`unsafe path parent: ${current} is group/world writable`);
|
|
686
|
+
}
|
|
687
|
+
if (!allowedUIDs.has(Number(stats.uid))) {
|
|
688
|
+
throw new Error(`unsafe path parent: ${current} has unexpected ownership`);
|
|
689
|
+
}
|
|
690
|
+
if (path.resolve(current) === stop) {
|
|
691
|
+
break;
|
|
692
|
+
}
|
|
693
|
+
const next = path.dirname(current);
|
|
694
|
+
if (next === current || !path.resolve(current).startsWith(stopPrefix)) {
|
|
695
|
+
throw new Error("secure path boundary was not reached");
|
|
696
|
+
}
|
|
697
|
+
current = next;
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
async function readBoundedFile(handle, maxBytes) {
|
|
702
|
+
const storage = Buffer.alloc(maxBytes + 1);
|
|
703
|
+
let offset = 0;
|
|
704
|
+
while (offset < storage.length) {
|
|
705
|
+
const { bytesRead } = await handle.read(storage, offset, storage.length - offset, null);
|
|
706
|
+
if (bytesRead === 0) {
|
|
707
|
+
break;
|
|
708
|
+
}
|
|
709
|
+
offset += bytesRead;
|
|
710
|
+
}
|
|
711
|
+
if (offset > maxBytes) {
|
|
712
|
+
storage.fill(0);
|
|
713
|
+
throw new Error(`policy file exceeds ${maxBytes} bytes`);
|
|
714
|
+
}
|
|
715
|
+
const result = Buffer.from(storage.subarray(0, offset));
|
|
716
|
+
storage.fill(0);
|
|
717
|
+
return result;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
export async function readOwnerPolicyFile(
|
|
721
|
+
policyPath,
|
|
722
|
+
expectedUID,
|
|
723
|
+
boundary,
|
|
724
|
+
allowedAncestorUIDs = new Set([0, expectedUID]),
|
|
725
|
+
) {
|
|
726
|
+
if (
|
|
727
|
+
typeof policyPath !== "string" ||
|
|
728
|
+
!path.isAbsolute(policyPath) ||
|
|
729
|
+
path.normalize(policyPath) !== policyPath
|
|
730
|
+
) {
|
|
731
|
+
throw new Error("--policy-file must be an absolute clean path");
|
|
732
|
+
}
|
|
733
|
+
await assertSecureAncestors(policyPath, allowedAncestorUIDs, boundary);
|
|
734
|
+
const before = await fsp.lstat(policyPath, { bigint: true });
|
|
735
|
+
const permissions = Number(before.mode & 0o777n);
|
|
736
|
+
if (!before.isFile() || before.isSymbolicLink() || before.nlink !== 1n) {
|
|
737
|
+
throw new Error("policy file must be a single-link regular non-symlink file");
|
|
738
|
+
}
|
|
739
|
+
if (Number(before.uid) !== expectedUID) {
|
|
740
|
+
throw new Error("policy file has the wrong owner");
|
|
741
|
+
}
|
|
742
|
+
if (
|
|
743
|
+
(permissions & 0o077) !== 0 ||
|
|
744
|
+
(permissions & 0o400) === 0 ||
|
|
745
|
+
(Number(before.mode) & 0o7000) !== 0
|
|
746
|
+
) {
|
|
747
|
+
throw new Error("policy file must be owner-readable and inaccessible to group/others");
|
|
748
|
+
}
|
|
749
|
+
if (before.size < 1n || before.size > BigInt(MAX_POLICY_BYTES)) {
|
|
750
|
+
throw new Error(`policy file must contain 1 to ${MAX_POLICY_BYTES} bytes`);
|
|
751
|
+
}
|
|
752
|
+
const flags = fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0);
|
|
753
|
+
const handle = await fsp.open(policyPath, flags);
|
|
754
|
+
try {
|
|
755
|
+
const opened = await handle.stat({ bigint: true });
|
|
756
|
+
if (statsIdentity(opened) !== statsIdentity(before) || !opened.isFile()) {
|
|
757
|
+
throw new Error("policy file changed while it was opened");
|
|
758
|
+
}
|
|
759
|
+
const raw = await readBoundedFile(handle, MAX_POLICY_BYTES);
|
|
760
|
+
const after = await handle.stat({ bigint: true });
|
|
761
|
+
if (statsIdentity(after) !== statsIdentity(opened)) {
|
|
762
|
+
raw.fill(0);
|
|
763
|
+
throw new Error("policy file changed while it was read");
|
|
764
|
+
}
|
|
765
|
+
const policy = normalizeOwnerPolicy(parseStrictJson(raw, "policy file"));
|
|
766
|
+
return { raw, policy, digest: sha256(raw) };
|
|
767
|
+
} finally {
|
|
768
|
+
await handle.close();
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
async function readPasswdSignerUID(passwdPath = HOSTING_PATHS.passwdPath) {
|
|
773
|
+
const stats = await fsp.lstat(passwdPath, { bigint: true });
|
|
774
|
+
if (
|
|
775
|
+
!stats.isFile() ||
|
|
776
|
+
stats.isSymbolicLink() ||
|
|
777
|
+
stats.uid !== 0n ||
|
|
778
|
+
(Number(stats.mode) & 0o022) !== 0
|
|
779
|
+
) {
|
|
780
|
+
throw new Error("/etc/passwd is not a trusted root-owned file");
|
|
781
|
+
}
|
|
782
|
+
if (stats.size < 1n || stats.size > 1024n * 1024n) {
|
|
783
|
+
throw new Error("/etc/passwd has an invalid size");
|
|
784
|
+
}
|
|
785
|
+
const text = await fsp.readFile(passwdPath, "utf8");
|
|
786
|
+
const matches = text
|
|
787
|
+
.split("\n")
|
|
788
|
+
.map((line) => line.split(":"))
|
|
789
|
+
.filter((fields) => fields[0] === HOSTING_PATHS.signerUser);
|
|
790
|
+
if (matches.length !== 1) {
|
|
791
|
+
throw new Error("the fixed fased-signer account is missing or duplicated");
|
|
792
|
+
}
|
|
793
|
+
const uid = Number(matches[0][2]);
|
|
794
|
+
if (!Number.isSafeInteger(uid) || uid <= 0 || matches[0][5] !== HOSTING_PATHS.signerHome) {
|
|
795
|
+
throw new Error("the fixed fased-signer account has an unsafe identity");
|
|
796
|
+
}
|
|
797
|
+
return uid;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
async function assertSafeExecutable(filePath, expectedUID, allowedParentUIDs, trustedHardlinkPath) {
|
|
801
|
+
await assertSecureAncestors(filePath, allowedParentUIDs);
|
|
802
|
+
const stats = await fsp.lstat(filePath, { bigint: true });
|
|
803
|
+
const permissions = Number(stats.mode & 0o777n);
|
|
804
|
+
if (!stats.isFile() || stats.isSymbolicLink() || Number(stats.uid) !== expectedUID) {
|
|
805
|
+
throw new Error(`${filePath} must be an executable owned by the trusted installer identity`);
|
|
806
|
+
}
|
|
807
|
+
if (
|
|
808
|
+
(permissions & 0o022) !== 0 ||
|
|
809
|
+
(permissions & 0o100) === 0 ||
|
|
810
|
+
(Number(stats.mode) & 0o6000) !== 0
|
|
811
|
+
) {
|
|
812
|
+
throw new Error(`${filePath} has unsafe executable permissions`);
|
|
813
|
+
}
|
|
814
|
+
if (stats.nlink === 1n) {
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
817
|
+
if (stats.nlink !== 2n || !trustedHardlinkPath) {
|
|
818
|
+
throw new Error(`${filePath} has an unexpected hardlink`);
|
|
819
|
+
}
|
|
820
|
+
await assertSecureAncestors(trustedHardlinkPath, allowedParentUIDs);
|
|
821
|
+
const launcher = await fsp.lstat(trustedHardlinkPath, { bigint: true });
|
|
822
|
+
if (
|
|
823
|
+
!launcher.isFile() ||
|
|
824
|
+
launcher.isSymbolicLink() ||
|
|
825
|
+
launcher.nlink !== 2n ||
|
|
826
|
+
launcher.dev !== stats.dev ||
|
|
827
|
+
launcher.ino !== stats.ino ||
|
|
828
|
+
Number(launcher.uid) !== expectedUID ||
|
|
829
|
+
Number(launcher.mode) !== Number(stats.mode)
|
|
830
|
+
) {
|
|
831
|
+
throw new Error(`${filePath} is not linked only to the attested enrollment launcher`);
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
async function assertControlSocket(socketPath, ownerUID, allowedParentUIDs) {
|
|
836
|
+
await assertSecureAncestors(socketPath, allowedParentUIDs);
|
|
837
|
+
const stats = await fsp.lstat(socketPath, { bigint: true });
|
|
838
|
+
if (
|
|
839
|
+
!stats.isSocket() ||
|
|
840
|
+
stats.isSymbolicLink() ||
|
|
841
|
+
stats.nlink !== 1n ||
|
|
842
|
+
Number(stats.uid) !== ownerUID
|
|
843
|
+
) {
|
|
844
|
+
throw new Error(
|
|
845
|
+
"signer control socket must be a single-link Unix socket owned by the signer identity",
|
|
846
|
+
);
|
|
847
|
+
}
|
|
848
|
+
if ((Number(stats.mode) & 0o077) !== 0) {
|
|
849
|
+
throw new Error("signer control socket must not be accessible to group/others");
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
export async function assertHostingUpdateGateInactive(paths = HOSTING_PATHS, trustedRootUID = 0) {
|
|
854
|
+
let directory;
|
|
855
|
+
try {
|
|
856
|
+
directory = await fsp.lstat(paths.updateGateDirectory, { bigint: true });
|
|
857
|
+
} catch (error) {
|
|
858
|
+
throw new Error(
|
|
859
|
+
"root signer update gate directory is missing or unreadable; refusing policy mutation",
|
|
860
|
+
{
|
|
861
|
+
cause: error,
|
|
862
|
+
},
|
|
863
|
+
);
|
|
864
|
+
}
|
|
865
|
+
if (
|
|
866
|
+
!directory.isDirectory() ||
|
|
867
|
+
directory.isSymbolicLink() ||
|
|
868
|
+
Number(directory.uid) !== trustedRootUID ||
|
|
869
|
+
(Number(directory.mode) & 0o022) !== 0
|
|
870
|
+
) {
|
|
871
|
+
throw new Error("root signer update gate directory is invalid; refusing policy mutation");
|
|
872
|
+
}
|
|
873
|
+
try {
|
|
874
|
+
const gate = await fsp.lstat(paths.updateGatePath, { bigint: true });
|
|
875
|
+
const valid =
|
|
876
|
+
gate.isFile() &&
|
|
877
|
+
!gate.isSymbolicLink() &&
|
|
878
|
+
Number(gate.uid) === trustedRootUID &&
|
|
879
|
+
gate.nlink === 1n &&
|
|
880
|
+
(Number(gate.mode) & 0o022) === 0;
|
|
881
|
+
throw new Error(
|
|
882
|
+
valid
|
|
883
|
+
? "root signer update gate is active; policy mutation is temporarily disabled"
|
|
884
|
+
: "root signer update gate is present but invalid; refusing policy mutation",
|
|
885
|
+
);
|
|
886
|
+
} catch (error) {
|
|
887
|
+
if (error?.code === "ENOENT") {
|
|
888
|
+
return;
|
|
889
|
+
}
|
|
890
|
+
throw error;
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
function localPaths(home) {
|
|
895
|
+
return {
|
|
896
|
+
binaryPath: path.join(home, ".fased", "bin", "fased-signerd"),
|
|
897
|
+
enrollmentLauncherPath: path.join(home, ".fased", "bin", "fased-signer-enroll"),
|
|
898
|
+
controlSocketPath: path.join(home, ".fased", "wallet", "local-signer-control.sock"),
|
|
899
|
+
signerHome: home,
|
|
900
|
+
};
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
export function createExecutionPlan(profile, identity = {}) {
|
|
904
|
+
const effectiveUID = identity.effectiveUID ?? process.geteuid?.();
|
|
905
|
+
if (!Number.isSafeInteger(effectiveUID) || effectiveUID < 0) {
|
|
906
|
+
throw new Error("could not determine the effective user identity");
|
|
907
|
+
}
|
|
908
|
+
if (profile === "local") {
|
|
909
|
+
if (effectiveUID === 0) {
|
|
910
|
+
throw new Error("Local signer policy setup must not run as root or through sudo");
|
|
911
|
+
}
|
|
912
|
+
const home = path.resolve(identity.home ?? os.homedir());
|
|
913
|
+
if (!path.isAbsolute(home)) {
|
|
914
|
+
throw new Error("Local signer home path is invalid");
|
|
915
|
+
}
|
|
916
|
+
const paths = localPaths(home);
|
|
917
|
+
return {
|
|
918
|
+
profile,
|
|
919
|
+
effectiveUID,
|
|
920
|
+
signerUID: effectiveUID,
|
|
921
|
+
...paths,
|
|
922
|
+
executablePath: paths.binaryPath,
|
|
923
|
+
executablePrefix: [],
|
|
924
|
+
childEnv: {
|
|
925
|
+
HOME: home,
|
|
926
|
+
LANG: "C",
|
|
927
|
+
LC_ALL: "C",
|
|
928
|
+
NO_COLOR: "1",
|
|
929
|
+
PATH: "/usr/bin:/bin:/usr/sbin:/sbin",
|
|
930
|
+
},
|
|
931
|
+
};
|
|
932
|
+
}
|
|
933
|
+
if (profile !== "hosting") {
|
|
934
|
+
throw new Error("--profile must be local or hosting");
|
|
935
|
+
}
|
|
936
|
+
if ((identity.platform ?? process.platform) !== "linux") {
|
|
937
|
+
throw new Error("Hosting signer policy setup is supported only on Linux");
|
|
938
|
+
}
|
|
939
|
+
if (effectiveUID !== 0) {
|
|
940
|
+
throw new Error("Hosting signer policy setup must run as root");
|
|
941
|
+
}
|
|
942
|
+
const signerUID = identity.signerUID;
|
|
943
|
+
if (!Number.isSafeInteger(signerUID) || signerUID <= 0) {
|
|
944
|
+
throw new Error(
|
|
945
|
+
"Hosting signer policy setup requires the dedicated non-root fased-signer identity",
|
|
946
|
+
);
|
|
947
|
+
}
|
|
948
|
+
return {
|
|
949
|
+
profile,
|
|
950
|
+
effectiveUID,
|
|
951
|
+
signerUID,
|
|
952
|
+
...HOSTING_PATHS,
|
|
953
|
+
executablePath: HOSTING_PATHS.runuserPath,
|
|
954
|
+
executablePrefix: ["-u", HOSTING_PATHS.signerUser, "--", HOSTING_PATHS.binaryPath],
|
|
955
|
+
childEnv: {
|
|
956
|
+
HOME: HOSTING_PATHS.signerHome,
|
|
957
|
+
LANG: "C",
|
|
958
|
+
LC_ALL: "C",
|
|
959
|
+
NO_COLOR: "1",
|
|
960
|
+
PATH: "/usr/sbin:/usr/bin:/sbin:/bin",
|
|
961
|
+
},
|
|
962
|
+
};
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
async function resolveAndValidateExecutionPlan(profile) {
|
|
966
|
+
if (profile !== "local" && profile !== "hosting") {
|
|
967
|
+
throw new Error("--profile must be local or hosting");
|
|
968
|
+
}
|
|
969
|
+
if (profile === "local") {
|
|
970
|
+
const plan = createExecutionPlan(profile);
|
|
971
|
+
await assertSafeExecutable(
|
|
972
|
+
plan.binaryPath,
|
|
973
|
+
plan.effectiveUID,
|
|
974
|
+
new Set([0, plan.effectiveUID]),
|
|
975
|
+
plan.enrollmentLauncherPath,
|
|
976
|
+
);
|
|
977
|
+
await assertControlSocket(
|
|
978
|
+
plan.controlSocketPath,
|
|
979
|
+
plan.effectiveUID,
|
|
980
|
+
new Set([0, plan.effectiveUID]),
|
|
981
|
+
);
|
|
982
|
+
return plan;
|
|
983
|
+
}
|
|
984
|
+
const signerUID = await readPasswdSignerUID();
|
|
985
|
+
const plan = createExecutionPlan(profile, { signerUID });
|
|
986
|
+
await assertSafeExecutable(plan.binaryPath, 0, new Set([0]));
|
|
987
|
+
await assertSafeExecutable(plan.runuserPath, 0, new Set([0]));
|
|
988
|
+
await assertControlSocket(plan.controlSocketPath, signerUID, new Set([0, signerUID]));
|
|
989
|
+
await assertHostingUpdateGateInactive(plan);
|
|
990
|
+
return plan;
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
export function buildAdminInvocation(plan, operation, walletId, stagedPolicyPath) {
|
|
994
|
+
if (operation !== "get" && operation !== "put") {
|
|
995
|
+
throw new Error("unsupported policy admin operation");
|
|
996
|
+
}
|
|
997
|
+
const args = [
|
|
998
|
+
...plan.executablePrefix,
|
|
999
|
+
"admin",
|
|
1000
|
+
"policy",
|
|
1001
|
+
operation,
|
|
1002
|
+
"--control-socket",
|
|
1003
|
+
plan.controlSocketPath,
|
|
1004
|
+
"--wallet-id",
|
|
1005
|
+
walletId,
|
|
1006
|
+
];
|
|
1007
|
+
if (operation === "put") {
|
|
1008
|
+
if (!stagedPolicyPath || !path.isAbsolute(stagedPolicyPath)) {
|
|
1009
|
+
throw new Error("an absolute signer-owned staged policy path is required");
|
|
1010
|
+
}
|
|
1011
|
+
args.push("--expected-version", "1", "--policy-file", stagedPolicyPath);
|
|
1012
|
+
}
|
|
1013
|
+
return { command: plan.executablePath, args, env: { ...plan.childEnv } };
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
async function runBoundedCommand(invocation) {
|
|
1017
|
+
return await new Promise((resolve, reject) => {
|
|
1018
|
+
const child = spawn(invocation.command, invocation.args, {
|
|
1019
|
+
env: invocation.env,
|
|
1020
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1021
|
+
shell: false,
|
|
1022
|
+
windowsHide: true,
|
|
1023
|
+
});
|
|
1024
|
+
const stdout = [];
|
|
1025
|
+
const stderr = [];
|
|
1026
|
+
let stdoutBytes = 0;
|
|
1027
|
+
let stderrBytes = 0;
|
|
1028
|
+
let settled = false;
|
|
1029
|
+
const finish = (error, result) => {
|
|
1030
|
+
if (settled) {
|
|
1031
|
+
return;
|
|
1032
|
+
}
|
|
1033
|
+
settled = true;
|
|
1034
|
+
clearTimeout(timer);
|
|
1035
|
+
if (error) {
|
|
1036
|
+
reject(error);
|
|
1037
|
+
} else {
|
|
1038
|
+
resolve(result);
|
|
1039
|
+
}
|
|
1040
|
+
};
|
|
1041
|
+
const collect = (target, chunk, kind) => {
|
|
1042
|
+
const buffer = Buffer.from(chunk);
|
|
1043
|
+
if (kind === "stdout") {
|
|
1044
|
+
stdoutBytes += buffer.length;
|
|
1045
|
+
} else {
|
|
1046
|
+
stderrBytes += buffer.length;
|
|
1047
|
+
}
|
|
1048
|
+
if (stdoutBytes > MAX_ADMIN_OUTPUT_BYTES || stderrBytes > MAX_ADMIN_OUTPUT_BYTES) {
|
|
1049
|
+
child.kill("SIGKILL");
|
|
1050
|
+
finish(new Error("native signer admin output exceeded the safe bound"));
|
|
1051
|
+
return;
|
|
1052
|
+
}
|
|
1053
|
+
target.push(buffer);
|
|
1054
|
+
};
|
|
1055
|
+
child.stdout.on("data", (chunk) => collect(stdout, chunk, "stdout"));
|
|
1056
|
+
child.stderr.on("data", (chunk) => collect(stderr, chunk, "stderr"));
|
|
1057
|
+
child.once("error", () =>
|
|
1058
|
+
finish(new Error("could not execute the fixed native signer admin client")),
|
|
1059
|
+
);
|
|
1060
|
+
child.once("close", (code, signal) => {
|
|
1061
|
+
const stderrText = Buffer.concat(stderr).toString("utf8").trim();
|
|
1062
|
+
if (code !== 0) {
|
|
1063
|
+
const reason = signal ? `signal ${signal}` : `exit ${code}`;
|
|
1064
|
+
const detail = stderrText ? `: ${stderrText.slice(0, 512)}` : "";
|
|
1065
|
+
finish(new Error(`native signer admin command failed (${reason})${detail}`));
|
|
1066
|
+
return;
|
|
1067
|
+
}
|
|
1068
|
+
finish(null, Buffer.concat(stdout));
|
|
1069
|
+
});
|
|
1070
|
+
const timer = setTimeout(() => {
|
|
1071
|
+
child.kill("SIGKILL");
|
|
1072
|
+
finish(
|
|
1073
|
+
new Error(
|
|
1074
|
+
"native signer admin command timed out; query durable policy state before retrying",
|
|
1075
|
+
),
|
|
1076
|
+
);
|
|
1077
|
+
}, ADMIN_TIMEOUT_MS);
|
|
1078
|
+
timer.unref?.();
|
|
1079
|
+
});
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
function parseAdminPolicyOutput(raw) {
|
|
1083
|
+
if (!Buffer.isBuffer(raw) || raw.length < 1 || raw.length > MAX_ADMIN_OUTPUT_BYTES) {
|
|
1084
|
+
throw new Error("native signer admin returned an invalid bounded response");
|
|
1085
|
+
}
|
|
1086
|
+
return normalizeStoredPolicy(parseStrictJson(raw, "native signer admin response"));
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
async function fsyncDirectory(directory) {
|
|
1090
|
+
const handle = await fsp.open(directory, fs.constants.O_RDONLY);
|
|
1091
|
+
try {
|
|
1092
|
+
await handle.sync();
|
|
1093
|
+
} finally {
|
|
1094
|
+
await handle.close();
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
async function removeStagedPolicyFile(parent, directory, stagedPath) {
|
|
1099
|
+
await fsp.rm(stagedPath, { force: true });
|
|
1100
|
+
await fsyncDirectory(directory);
|
|
1101
|
+
await fsp.rmdir(directory);
|
|
1102
|
+
await fsyncDirectory(parent);
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
async function stagePolicyFile(plan, raw) {
|
|
1106
|
+
const parent = path.dirname(plan.controlSocketPath);
|
|
1107
|
+
const directory = await fsp.mkdtemp(path.join(parent, ".owner-policy-"));
|
|
1108
|
+
const stagedPath = path.join(directory, "policy.json");
|
|
1109
|
+
let created = false;
|
|
1110
|
+
try {
|
|
1111
|
+
await fsp.chmod(directory, 0o700);
|
|
1112
|
+
if (plan.profile === "hosting") {
|
|
1113
|
+
await fsp.chown(directory, plan.signerUID, plan.signerUID);
|
|
1114
|
+
}
|
|
1115
|
+
const flags =
|
|
1116
|
+
fs.constants.O_CREAT |
|
|
1117
|
+
fs.constants.O_EXCL |
|
|
1118
|
+
fs.constants.O_WRONLY |
|
|
1119
|
+
(fs.constants.O_NOFOLLOW ?? 0);
|
|
1120
|
+
const handle = await fsp.open(stagedPath, flags, 0o600);
|
|
1121
|
+
created = true;
|
|
1122
|
+
try {
|
|
1123
|
+
let offset = 0;
|
|
1124
|
+
while (offset < raw.length) {
|
|
1125
|
+
const { bytesWritten } = await handle.write(raw, offset, raw.length - offset, null);
|
|
1126
|
+
if (bytesWritten < 1) {
|
|
1127
|
+
throw new Error("short write while staging signer policy");
|
|
1128
|
+
}
|
|
1129
|
+
offset += bytesWritten;
|
|
1130
|
+
}
|
|
1131
|
+
await handle.sync();
|
|
1132
|
+
if (plan.profile === "hosting") {
|
|
1133
|
+
await handle.chown(plan.signerUID, plan.signerUID);
|
|
1134
|
+
}
|
|
1135
|
+
await handle.chmod(0o600);
|
|
1136
|
+
await handle.sync();
|
|
1137
|
+
} finally {
|
|
1138
|
+
await handle.close();
|
|
1139
|
+
}
|
|
1140
|
+
await fsyncDirectory(directory);
|
|
1141
|
+
await fsyncDirectory(parent);
|
|
1142
|
+
} catch (error) {
|
|
1143
|
+
try {
|
|
1144
|
+
if (!created) {
|
|
1145
|
+
await fsyncDirectory(directory);
|
|
1146
|
+
}
|
|
1147
|
+
await removeStagedPolicyFile(parent, directory, stagedPath);
|
|
1148
|
+
} catch (cleanupError) {
|
|
1149
|
+
throw new Error(
|
|
1150
|
+
`signer policy staging failed (${error.message}) and its private temporary path could not be durably removed`,
|
|
1151
|
+
{ cause: cleanupError },
|
|
1152
|
+
);
|
|
1153
|
+
}
|
|
1154
|
+
throw error;
|
|
1155
|
+
}
|
|
1156
|
+
let cleaned = false;
|
|
1157
|
+
return {
|
|
1158
|
+
path: stagedPath,
|
|
1159
|
+
async cleanup() {
|
|
1160
|
+
if (cleaned) {
|
|
1161
|
+
return;
|
|
1162
|
+
}
|
|
1163
|
+
cleaned = true;
|
|
1164
|
+
await removeStagedPolicyFile(parent, directory, stagedPath);
|
|
1165
|
+
},
|
|
1166
|
+
};
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
function renderPolicySummary(policy, digest) {
|
|
1170
|
+
const lines = [
|
|
1171
|
+
"INITIAL SIGNER POLICY (one-time deny-all transition)",
|
|
1172
|
+
`Input SHA-256: ${digest}`,
|
|
1173
|
+
`Wallet: ${policy.walletId}`,
|
|
1174
|
+
`Role: ${policy.role}`,
|
|
1175
|
+
"Operations:",
|
|
1176
|
+
...policy.operations.map((operation) => ` - ${operation}`),
|
|
1177
|
+
"Programs / signer domains:",
|
|
1178
|
+
...policy.programs.map((program) => ` - ${program}`),
|
|
1179
|
+
"Assets and raw-unit caps:",
|
|
1180
|
+
];
|
|
1181
|
+
for (const asset of policy.assets) {
|
|
1182
|
+
lines.push(` - Asset: ${asset.asset}`);
|
|
1183
|
+
lines.push(" Destinations:");
|
|
1184
|
+
lines.push(...asset.destinations.map((destination) => ` - ${destination}`));
|
|
1185
|
+
lines.push(` Max per transaction (raw units): ${asset.maxPerTx}`);
|
|
1186
|
+
lines.push(` Max per UTC day (raw units): ${asset.maxDaily}`);
|
|
1187
|
+
}
|
|
1188
|
+
lines.push("The Gateway cannot repeat, widen, or replace this owner-authorized transition.");
|
|
1189
|
+
return `${lines.join("\n")}\n`;
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
async function defaultPromptConfirmation(expected, streams = {}) {
|
|
1193
|
+
const input = streams.stdin ?? process.stdin;
|
|
1194
|
+
const output = streams.stdout ?? process.stdout;
|
|
1195
|
+
if (!input.isTTY || !output.isTTY) {
|
|
1196
|
+
throw new Error(
|
|
1197
|
+
"interactive confirmation requires a terminal; use --non-interactive with --confirm-digest and --initial-install only in an owner-controlled installer",
|
|
1198
|
+
);
|
|
1199
|
+
}
|
|
1200
|
+
const prompt = readline.createInterface({ input, output, terminal: true });
|
|
1201
|
+
try {
|
|
1202
|
+
return await prompt.question(
|
|
1203
|
+
`Type exactly ${JSON.stringify(expected)} to install this initial policy: `,
|
|
1204
|
+
);
|
|
1205
|
+
} finally {
|
|
1206
|
+
prompt.close();
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
async function getCurrentPolicy(plan, walletId, runCommand) {
|
|
1211
|
+
const output = await runCommand(buildAdminInvocation(plan, "get", walletId));
|
|
1212
|
+
return parseAdminPolicyOutput(output);
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
export async function runInitialSignerPolicySetup(options, dependencies = {}) {
|
|
1216
|
+
if (!options?.initialInstall) {
|
|
1217
|
+
throw new Error("--initial-install is required for this one-time command");
|
|
1218
|
+
}
|
|
1219
|
+
const output = dependencies.output ?? process.stdout;
|
|
1220
|
+
const resolvePlan = dependencies.resolvePlan ?? resolveAndValidateExecutionPlan;
|
|
1221
|
+
const runCommand = dependencies.runCommand ?? runBoundedCommand;
|
|
1222
|
+
const stage = dependencies.stagePolicy ?? stagePolicyFile;
|
|
1223
|
+
const promptConfirmation = dependencies.promptConfirmation ?? defaultPromptConfirmation;
|
|
1224
|
+
const plan = await resolvePlan(options.profile);
|
|
1225
|
+
const expectedPolicyOwner = plan.profile === "hosting" ? 0 : plan.effectiveUID;
|
|
1226
|
+
const loaded = await readOwnerPolicyFile(
|
|
1227
|
+
options.policyFile,
|
|
1228
|
+
expectedPolicyOwner,
|
|
1229
|
+
dependencies.policyFileBoundary,
|
|
1230
|
+
);
|
|
1231
|
+
try {
|
|
1232
|
+
if (options.nonInteractive) {
|
|
1233
|
+
if (!/^sha256:[0-9a-f]{64}$/u.test(options.confirmDigest ?? "")) {
|
|
1234
|
+
throw new Error(
|
|
1235
|
+
"--non-interactive requires an exact lowercase sha256:<hex> --confirm-digest",
|
|
1236
|
+
);
|
|
1237
|
+
}
|
|
1238
|
+
if (options.confirmDigest !== loaded.digest) {
|
|
1239
|
+
throw new Error("owner confirmation digest does not match the exact policy file bytes");
|
|
1240
|
+
}
|
|
1241
|
+
} else if (options.confirmDigest) {
|
|
1242
|
+
throw new Error("--confirm-digest is accepted only with --non-interactive");
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
const current = normalizeLockedStoredPolicy(
|
|
1246
|
+
parseStrictJson(
|
|
1247
|
+
await runCommand(buildAdminInvocation(plan, "get", loaded.policy.walletId)),
|
|
1248
|
+
"native signer admin response",
|
|
1249
|
+
),
|
|
1250
|
+
);
|
|
1251
|
+
if (current.walletId !== loaded.policy.walletId) {
|
|
1252
|
+
throw new Error("current signer policy walletId does not match the owner policy file");
|
|
1253
|
+
}
|
|
1254
|
+
if (current.role !== loaded.policy.role) {
|
|
1255
|
+
throw new Error("owner policy role does not match the signer wallet's immutable role");
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
output.write(renderPolicySummary(loaded.policy, loaded.digest));
|
|
1259
|
+
if (!options.nonInteractive) {
|
|
1260
|
+
const expectedConfirmation = `${loaded.policy.walletId} ${loaded.digest}`;
|
|
1261
|
+
const confirmed = await promptConfirmation(expectedConfirmation, dependencies.streams);
|
|
1262
|
+
if (confirmed !== expectedConfirmation) {
|
|
1263
|
+
throw new Error("owner confirmation did not match exactly");
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
const expected = policyWithVersion(loaded.policy, 2);
|
|
1268
|
+
const staged = await stage(plan, loaded.raw);
|
|
1269
|
+
let putOutput;
|
|
1270
|
+
let putError;
|
|
1271
|
+
let cleanupError;
|
|
1272
|
+
try {
|
|
1273
|
+
putOutput = await runCommand(
|
|
1274
|
+
buildAdminInvocation(plan, "put", loaded.policy.walletId, staged.path),
|
|
1275
|
+
);
|
|
1276
|
+
} catch (error) {
|
|
1277
|
+
putError = error;
|
|
1278
|
+
} finally {
|
|
1279
|
+
try {
|
|
1280
|
+
await staged.cleanup();
|
|
1281
|
+
} catch (error) {
|
|
1282
|
+
cleanupError = error;
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
let durable;
|
|
1287
|
+
let readbackError;
|
|
1288
|
+
try {
|
|
1289
|
+
durable = await getCurrentPolicy(plan, loaded.policy.walletId, runCommand);
|
|
1290
|
+
} catch (error) {
|
|
1291
|
+
readbackError = error;
|
|
1292
|
+
}
|
|
1293
|
+
if (cleanupError) {
|
|
1294
|
+
throw new Error(
|
|
1295
|
+
"signer policy staging cleanup failed; policy state may have changed, so inspect the signer-only runtime directory and query policy before any retry",
|
|
1296
|
+
);
|
|
1297
|
+
}
|
|
1298
|
+
if (putError) {
|
|
1299
|
+
if (durable && samePolicy(durable, expected)) {
|
|
1300
|
+
throw new Error(
|
|
1301
|
+
"the signer durably installed the expected policy but the mutation acknowledgement failed; do not retry",
|
|
1302
|
+
);
|
|
1303
|
+
}
|
|
1304
|
+
if (readbackError) {
|
|
1305
|
+
throw new Error(
|
|
1306
|
+
"signer policy mutation and durable readback both failed; do not retry until a host administrator queries policy state",
|
|
1307
|
+
);
|
|
1308
|
+
}
|
|
1309
|
+
throw putError;
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
let acknowledged;
|
|
1313
|
+
try {
|
|
1314
|
+
acknowledged = parseAdminPolicyOutput(putOutput);
|
|
1315
|
+
} catch {
|
|
1316
|
+
throw new Error(
|
|
1317
|
+
durable && samePolicy(durable, expected)
|
|
1318
|
+
? "the signer installed the expected policy but returned an invalid acknowledgement; do not retry"
|
|
1319
|
+
: "the signer returned an invalid policy acknowledgement and durable state does not match; do not retry",
|
|
1320
|
+
);
|
|
1321
|
+
}
|
|
1322
|
+
if (!samePolicy(acknowledged, expected)) {
|
|
1323
|
+
throw new Error(
|
|
1324
|
+
durable && samePolicy(durable, expected)
|
|
1325
|
+
? "the signer installed the expected policy but acknowledgement content did not match; do not retry"
|
|
1326
|
+
: "signer policy acknowledgement did not match the exact owner policy",
|
|
1327
|
+
);
|
|
1328
|
+
}
|
|
1329
|
+
if (readbackError || !durable || !samePolicy(durable, expected)) {
|
|
1330
|
+
throw new Error("durable signer policy readback did not match the acknowledged owner policy");
|
|
1331
|
+
}
|
|
1332
|
+
const result = {
|
|
1333
|
+
walletId: durable.walletId,
|
|
1334
|
+
version: durable.version,
|
|
1335
|
+
hash: durable.hash,
|
|
1336
|
+
status: "acknowledged",
|
|
1337
|
+
};
|
|
1338
|
+
output.write(
|
|
1339
|
+
`${JSON.stringify({ version: result.version, hash: result.hash, status: result.status })}\n`,
|
|
1340
|
+
);
|
|
1341
|
+
return result;
|
|
1342
|
+
} finally {
|
|
1343
|
+
loaded.raw.fill(0);
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
function parseCLI(argv) {
|
|
1348
|
+
const values = Object.create(null);
|
|
1349
|
+
const booleans = new Set(["--initial-install", "--non-interactive"]);
|
|
1350
|
+
const strings = new Set(["--profile", "--policy-file", "--confirm-digest"]);
|
|
1351
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
1352
|
+
const flag = argv[index];
|
|
1353
|
+
if (flag === "--help") {
|
|
1354
|
+
if (argv.length !== 1) {
|
|
1355
|
+
throw new Error("--help must be used alone");
|
|
1356
|
+
}
|
|
1357
|
+
return { help: true };
|
|
1358
|
+
}
|
|
1359
|
+
if (!booleans.has(flag) && !strings.has(flag)) {
|
|
1360
|
+
throw new Error(`unknown argument: ${flag}`);
|
|
1361
|
+
}
|
|
1362
|
+
if (Object.hasOwn(values, flag)) {
|
|
1363
|
+
throw new Error(`duplicate argument: ${flag}`);
|
|
1364
|
+
}
|
|
1365
|
+
if (booleans.has(flag)) {
|
|
1366
|
+
values[flag] = true;
|
|
1367
|
+
continue;
|
|
1368
|
+
}
|
|
1369
|
+
const value = argv[index + 1];
|
|
1370
|
+
if (!value || value.startsWith("--")) {
|
|
1371
|
+
throw new Error(`${flag} requires a value`);
|
|
1372
|
+
}
|
|
1373
|
+
values[flag] = value;
|
|
1374
|
+
index += 1;
|
|
1375
|
+
}
|
|
1376
|
+
return {
|
|
1377
|
+
profile: values["--profile"],
|
|
1378
|
+
policyFile: values["--policy-file"],
|
|
1379
|
+
confirmDigest: values["--confirm-digest"],
|
|
1380
|
+
initialInstall: values["--initial-install"] === true,
|
|
1381
|
+
nonInteractive: values["--non-interactive"] === true,
|
|
1382
|
+
};
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
function usage() {
|
|
1386
|
+
return `Usage:
|
|
1387
|
+
# Installed profile-specific launcher (recommended)
|
|
1388
|
+
fased-signer-policy --initial-install --policy-file /secure/policy.json
|
|
1389
|
+
|
|
1390
|
+
# Direct packaged helper
|
|
1391
|
+
fased-signer-policy --profile local --initial-install --policy-file /secure/policy.json
|
|
1392
|
+
fased-signer-policy --profile hosting --initial-install --policy-file /root/policy.json
|
|
1393
|
+
|
|
1394
|
+
Owner-controlled noninteractive install:
|
|
1395
|
+
fased-signer-policy --profile <local|hosting> --initial-install --policy-file /secure/policy.json \\
|
|
1396
|
+
--non-interactive --confirm-digest sha256:<exact-file-digest>
|
|
1397
|
+
|
|
1398
|
+
There is intentionally no --yes flag. Hosting must run as root; Local must run as the
|
|
1399
|
+
same non-root user that owns the native signer control socket.
|
|
1400
|
+
`;
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
export async function main(argv = process.argv.slice(2)) {
|
|
1404
|
+
const options = parseCLI(argv);
|
|
1405
|
+
if (options.help) {
|
|
1406
|
+
process.stdout.write(usage());
|
|
1407
|
+
return;
|
|
1408
|
+
}
|
|
1409
|
+
await runInitialSignerPolicySetup(options);
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
const isMain = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
|
|
1413
|
+
if (isMain) {
|
|
1414
|
+
main().catch((error) => {
|
|
1415
|
+
process.stderr.write(`fased-signer-policy: ${error.message}\n`);
|
|
1416
|
+
process.exitCode = 1;
|
|
1417
|
+
});
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
export const __testing = Object.freeze({
|
|
1421
|
+
ADDRESS_LOOKUP_TABLE_PROGRAM,
|
|
1422
|
+
ASSOCIATED_TOKEN_PROGRAM,
|
|
1423
|
+
ASSOCIATED_TOKEN_PROGRAM_ACTIONS,
|
|
1424
|
+
FEDERATION_POLICY_DOMAIN,
|
|
1425
|
+
HOSTING_PATHS,
|
|
1426
|
+
SYSTEM_PROGRAM,
|
|
1427
|
+
NATIVE_FEE_RESERVATION_LAMPORTS,
|
|
1428
|
+
TOKEN_2022_PROGRAM,
|
|
1429
|
+
TOKEN_PROGRAM,
|
|
1430
|
+
SAT_MINING_ACTIONS,
|
|
1431
|
+
SAT_LOOKUP_TABLE_ACTIONS,
|
|
1432
|
+
VAULT_BOND_ACTIONS,
|
|
1433
|
+
assertSafeExecutable,
|
|
1434
|
+
encodeBase58,
|
|
1435
|
+
normalizeLockedStoredPolicy,
|
|
1436
|
+
parseAdminPolicyOutput,
|
|
1437
|
+
parseCLI,
|
|
1438
|
+
parseStrictJson,
|
|
1439
|
+
policyWithVersion,
|
|
1440
|
+
renderPolicySummary,
|
|
1441
|
+
stagePolicyFile,
|
|
1442
|
+
});
|