@fased/fased 0.1.62 → 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 +32 -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/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-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-DdE_NIXo.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/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 +235 -67
- 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 +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/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-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-DzT4L4Ty.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-BrkmtFxB.js → config-schema-BOT7kHb6.js} +6779 -3696
- 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-BCFDaqJ-.js} +31 -31
- package/dist/plugin-sdk/{monitor-wYVncM9w.js → monitor-BgbjRiPM.js} +32 -32
- package/dist/plugin-sdk/{openresponses-http-B2GmeSt4.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-B3bHYGaw.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-CxE1mfzj.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 +6 -6
- package/dist/plugin-sdk/slack.js +9 -9
- package/dist/plugin-sdk/{sticker-cache-CGzGWu00.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-DaEcYQ8r.js → telegram-actions-CC_qN4v0.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-xOTGlzBE.d.ts +486 -0
- package/dist/plugin-sdk/{types-vcJrC_uv.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-DFAW-n1J.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-CInY-lCv.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 +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 +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 +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 +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 +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 +90 -36
- 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 +12 -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 +12 -0
- package/extensions/msteams/package.json +1 -1
- package/extensions/nextcloud-talk/package.json +1 -1
- package/extensions/nostr/CHANGELOG.md +12 -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 +12 -0
- package/extensions/twitch/package.json +1 -1
- package/extensions/voice-call/CHANGELOG.md +12 -0
- package/extensions/voice-call/package.json +1 -1
- package/extensions/zalo/CHANGELOG.md +12 -0
- package/extensions/zalo/package.json +1 -1
- package/extensions/zalouser/CHANGELOG.md +12 -0
- package/extensions/zalouser/package.json +1 -1
- package/install.sh +4955 -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 +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 +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 +169 -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
|
@@ -1,1836 +0,0 @@
|
|
|
1
|
-
import { ensureWalletStateDir, resolveWalletRuntimeConfig } from "./wallet-runtime-config.js";
|
|
2
|
-
import { readWalletProviderRegistry, resolveWalletUserRole } from "./wallet-provider-registry.js";
|
|
3
|
-
import { tryResolveSatRuntimeIds } from "../config/sat-runtime-ids.js";
|
|
4
|
-
import { applyWalletPolicyConfig, resolveWalletRolePolicyProfile } from "./wallet-policy.js";
|
|
5
|
-
import { walletDiagnosticErrorMessage } from "./wallet-redaction.js";
|
|
6
|
-
import { resolveWalletProviderId } from "./wallet-provider-resolver.js";
|
|
7
|
-
import { consumeWalletApprovalGrant, resolveWalletApprovalAuthMode } from "./wallet-approval-auth.js";
|
|
8
|
-
import { lockLocalSignerCustody, unlockLocalSignerCustody } from "./local-socket-signer-custody.js";
|
|
9
|
-
import syncFs from "node:fs";
|
|
10
|
-
import path from "node:path";
|
|
11
|
-
import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
|
|
12
|
-
|
|
13
|
-
//#region src/wallet/wallet-custody.ts
|
|
14
|
-
const DEFAULT_UNLOCK_TTL_SECONDS = 15 * 60;
|
|
15
|
-
const MAX_UNLOCK_TTL_SECONDS = 60 * 60;
|
|
16
|
-
const MANUAL_UNLOCK_EXPIRES_AT = "9999-12-31T23:59:59.000Z";
|
|
17
|
-
const CUSTODY_SECRET_BYTES = 32;
|
|
18
|
-
const SOLANA_TOKEN_PROGRAM_ID = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA";
|
|
19
|
-
const SOLANA_TOKEN_2022_PROGRAM_ID = "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb";
|
|
20
|
-
const SOLANA_ASSOCIATED_TOKEN_PROGRAM_ID = "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL";
|
|
21
|
-
const unlockMaterialBySession = new Map();
|
|
22
|
-
function normalizeHost(raw) {
|
|
23
|
-
const trimmed = raw.trim();
|
|
24
|
-
if (!trimmed) {
|
|
25
|
-
return "";
|
|
26
|
-
}
|
|
27
|
-
try {
|
|
28
|
-
const withScheme = trimmed.includes("://") ? trimmed : `https://${trimmed}`;
|
|
29
|
-
const parsed = new URL(withScheme);
|
|
30
|
-
const hostname = parsed.hostname.toLowerCase();
|
|
31
|
-
if (!parsed.port || parsed.port === "443") {
|
|
32
|
-
return hostname;
|
|
33
|
-
}
|
|
34
|
-
return `${hostname}:${parsed.port}`;
|
|
35
|
-
} catch {
|
|
36
|
-
return "";
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
function nowMs() {
|
|
40
|
-
return Date.now();
|
|
41
|
-
}
|
|
42
|
-
function isNonNull(value) {
|
|
43
|
-
return value !== null;
|
|
44
|
-
}
|
|
45
|
-
function sha256Hex(input) {
|
|
46
|
-
return createHash("sha256").update(input).digest("hex");
|
|
47
|
-
}
|
|
48
|
-
function normalizeWalletIdForState(value) {
|
|
49
|
-
const raw = String(value ?? "").trim().toLowerCase();
|
|
50
|
-
if (!raw) {
|
|
51
|
-
return "default";
|
|
52
|
-
}
|
|
53
|
-
const normalized = raw.replace(/[^a-z0-9_-]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
54
|
-
return normalized || "default";
|
|
55
|
-
}
|
|
56
|
-
function normalizeChainList(value) {
|
|
57
|
-
const seen = new Set();
|
|
58
|
-
for (const entry of value) {
|
|
59
|
-
if (entry === "solana") {
|
|
60
|
-
seen.add(entry);
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
return [...seen];
|
|
64
|
-
}
|
|
65
|
-
function normalizeAddressList(values) {
|
|
66
|
-
const seen = new Set();
|
|
67
|
-
for (const value of values) {
|
|
68
|
-
const normalized = String(value ?? "").trim().toLowerCase();
|
|
69
|
-
if (!normalized) {
|
|
70
|
-
continue;
|
|
71
|
-
}
|
|
72
|
-
seen.add(normalized);
|
|
73
|
-
}
|
|
74
|
-
return [...seen];
|
|
75
|
-
}
|
|
76
|
-
function normalizeWalletCustodyRole(value) {
|
|
77
|
-
return value === "mining" || value === "agent" || value === "vault" ? value : undefined;
|
|
78
|
-
}
|
|
79
|
-
function normalizeDeviceLabel(value) {
|
|
80
|
-
const normalized = String(value ?? "").trim().replace(/\s+/g, " ");
|
|
81
|
-
return normalized ? normalized.slice(0, 80) : undefined;
|
|
82
|
-
}
|
|
83
|
-
function createCustodyDeviceId() {
|
|
84
|
-
return randomBytes(8).toString("hex");
|
|
85
|
-
}
|
|
86
|
-
function normalizeDeviceShareX(value) {
|
|
87
|
-
return typeof value === "number" && Number.isInteger(value) && value >= 2 && value <= 255 && value !== 3 ? value : null;
|
|
88
|
-
}
|
|
89
|
-
function normalizeEnrolledDevice(raw) {
|
|
90
|
-
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
91
|
-
return null;
|
|
92
|
-
}
|
|
93
|
-
const value = raw;
|
|
94
|
-
const id = typeof value.id === "string" ? value.id.trim() : "";
|
|
95
|
-
const shareX = normalizeDeviceShareX(value.shareX);
|
|
96
|
-
const shareHash = typeof value.shareHash === "string" ? value.shareHash.trim() : "";
|
|
97
|
-
const createdAt = typeof value.createdAt === "string" ? value.createdAt : "";
|
|
98
|
-
if (!id || !shareX || !shareHash || !createdAt) {
|
|
99
|
-
return null;
|
|
100
|
-
}
|
|
101
|
-
return {
|
|
102
|
-
id,
|
|
103
|
-
label: normalizeDeviceLabel(typeof value.label === "string" ? value.label : undefined),
|
|
104
|
-
shareX,
|
|
105
|
-
shareHash,
|
|
106
|
-
createdAt,
|
|
107
|
-
revokedAt: typeof value.revokedAt === "string" ? value.revokedAt : undefined
|
|
108
|
-
};
|
|
109
|
-
}
|
|
110
|
-
function listActiveEnrolledDevices(shareState) {
|
|
111
|
-
if (!shareState) {
|
|
112
|
-
return [];
|
|
113
|
-
}
|
|
114
|
-
if (shareState.version === 3 && shareState.scheme === "shamir-2ofn-v1") {
|
|
115
|
-
return shareState.devices.filter((device) => !device.revokedAt);
|
|
116
|
-
}
|
|
117
|
-
if (shareState.version === 2 && shareState.scheme === "shamir-2of3-v1") {
|
|
118
|
-
return [{
|
|
119
|
-
id: "primary",
|
|
120
|
-
label: "Primary device",
|
|
121
|
-
shareX: 2,
|
|
122
|
-
shareHash: shareState.deviceShareHash,
|
|
123
|
-
createdAt: shareState.createdAt
|
|
124
|
-
}];
|
|
125
|
-
}
|
|
126
|
-
if (shareState.version === 1 && shareState.scheme === "xor-3of3-v1") {
|
|
127
|
-
return [{
|
|
128
|
-
id: "legacy-primary",
|
|
129
|
-
label: "Primary device",
|
|
130
|
-
shareX: 2,
|
|
131
|
-
shareHash: shareState.deviceShareHash,
|
|
132
|
-
createdAt: shareState.createdAt
|
|
133
|
-
}];
|
|
134
|
-
}
|
|
135
|
-
return [];
|
|
136
|
-
}
|
|
137
|
-
function allocateNextDeviceShareX(devices) {
|
|
138
|
-
const used = new Set(devices.map((device) => device.shareX));
|
|
139
|
-
for (let candidate = 2; candidate <= 255; candidate += 1) {
|
|
140
|
-
if (candidate === 3 || used.has(candidate)) {
|
|
141
|
-
continue;
|
|
142
|
-
}
|
|
143
|
-
return candidate;
|
|
144
|
-
}
|
|
145
|
-
throw new Error("no available device-share slots remain for this wallet");
|
|
146
|
-
}
|
|
147
|
-
function resolveLegacyCustodyStatePath(env = process.env) {
|
|
148
|
-
const walletPaths = ensureWalletStateDir(env);
|
|
149
|
-
return path.join(walletPaths.rootDir, "wallet-custody-state.json");
|
|
150
|
-
}
|
|
151
|
-
function resolveLegacyCustodyShareStatePath(env = process.env) {
|
|
152
|
-
const walletPaths = ensureWalletStateDir(env);
|
|
153
|
-
return path.join(walletPaths.rootDir, "wallet-custody-shares.v1.json");
|
|
154
|
-
}
|
|
155
|
-
function resolveCustodyWalletDir(walletId, env = process.env) {
|
|
156
|
-
const walletPaths = ensureWalletStateDir(env);
|
|
157
|
-
return path.join(walletPaths.rootDir, "custody", normalizeWalletIdForState(walletId));
|
|
158
|
-
}
|
|
159
|
-
function resolveCustodyStatePath(walletId, env = process.env) {
|
|
160
|
-
return path.join(resolveCustodyWalletDir(walletId, env), "state.json");
|
|
161
|
-
}
|
|
162
|
-
function resolveCustodyShareStatePath(walletId, env = process.env) {
|
|
163
|
-
return path.join(resolveCustodyWalletDir(walletId, env), "shares.v1.json");
|
|
164
|
-
}
|
|
165
|
-
function locateCustodyStatePath(walletId, env = process.env) {
|
|
166
|
-
const scopedPath = resolveCustodyStatePath(walletId, env);
|
|
167
|
-
if (syncFs.existsSync(scopedPath)) {
|
|
168
|
-
return scopedPath;
|
|
169
|
-
}
|
|
170
|
-
const legacyPath = resolveLegacyCustodyStatePath(env);
|
|
171
|
-
return syncFs.existsSync(legacyPath) ? legacyPath : scopedPath;
|
|
172
|
-
}
|
|
173
|
-
function locateCustodyShareStatePath(walletId, env = process.env) {
|
|
174
|
-
const scopedPath = resolveCustodyShareStatePath(walletId, env);
|
|
175
|
-
if (syncFs.existsSync(scopedPath)) {
|
|
176
|
-
return scopedPath;
|
|
177
|
-
}
|
|
178
|
-
const legacyPath = resolveLegacyCustodyShareStatePath(env);
|
|
179
|
-
return syncFs.existsSync(legacyPath) ? legacyPath : scopedPath;
|
|
180
|
-
}
|
|
181
|
-
function listCustodyStateTargets(env = process.env) {
|
|
182
|
-
const walletPaths = ensureWalletStateDir(env);
|
|
183
|
-
const custodyRoot = path.join(walletPaths.rootDir, "custody");
|
|
184
|
-
const targets = new Set();
|
|
185
|
-
if (syncFs.existsSync(custodyRoot)) {
|
|
186
|
-
for (const entry of syncFs.readdirSync(custodyRoot, { withFileTypes: true })) {
|
|
187
|
-
if (entry.isDirectory() && entry.name.trim()) {
|
|
188
|
-
targets.add(entry.name.trim());
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
const out = [...targets];
|
|
193
|
-
if (syncFs.existsSync(resolveLegacyCustodyStatePath(env)) || out.length === 0) {
|
|
194
|
-
out.push(undefined);
|
|
195
|
-
}
|
|
196
|
-
return out;
|
|
197
|
-
}
|
|
198
|
-
function readUnlockTtlSeconds(env = process.env, requestedTtlSeconds) {
|
|
199
|
-
const raw = typeof requestedTtlSeconds === "number" && Number.isFinite(requestedTtlSeconds) ? requestedTtlSeconds : Number.parseInt(String(env.FASED_WALLET_CUSTODY_UNLOCK_TTL_SECONDS ?? ""), 10);
|
|
200
|
-
if (!Number.isFinite(raw) || raw <= 0) {
|
|
201
|
-
return DEFAULT_UNLOCK_TTL_SECONDS;
|
|
202
|
-
}
|
|
203
|
-
return Math.min(MAX_UNLOCK_TTL_SECONDS, Math.max(30, Math.floor(raw)));
|
|
204
|
-
}
|
|
205
|
-
function resolveUnlockExpiresAt(env = process.env, requestedTtlSeconds) {
|
|
206
|
-
if (requestedTtlSeconds === 0) {
|
|
207
|
-
return MANUAL_UNLOCK_EXPIRES_AT;
|
|
208
|
-
}
|
|
209
|
-
return new Date(nowMs() + readUnlockTtlSeconds(env, requestedTtlSeconds) * 1e3).toISOString();
|
|
210
|
-
}
|
|
211
|
-
function readBooleanFlag(env, key) {
|
|
212
|
-
const value = String(env[key] ?? "").trim().toLowerCase();
|
|
213
|
-
return value === "1" || value === "true" || value === "yes" || value === "on";
|
|
214
|
-
}
|
|
215
|
-
function normalizeWalletIdForCustodyEnv(value) {
|
|
216
|
-
const raw = String(value ?? "").trim().toLowerCase();
|
|
217
|
-
if (!raw) {
|
|
218
|
-
return "default";
|
|
219
|
-
}
|
|
220
|
-
return raw.replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "") || "default";
|
|
221
|
-
}
|
|
222
|
-
function readCustodyWalletSet(env) {
|
|
223
|
-
const out = new Set();
|
|
224
|
-
for (const part of String(env.FASED_WALLET_CUSTODY_WALLETS ?? "").split(",")) {
|
|
225
|
-
if (!part.trim()) {
|
|
226
|
-
continue;
|
|
227
|
-
}
|
|
228
|
-
const normalized = normalizeWalletIdForCustodyEnv(part);
|
|
229
|
-
if (normalized) {
|
|
230
|
-
out.add(normalized);
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
return out;
|
|
234
|
-
}
|
|
235
|
-
function decodeBase64Share(raw) {
|
|
236
|
-
const value = raw.trim();
|
|
237
|
-
if (!value) {
|
|
238
|
-
return null;
|
|
239
|
-
}
|
|
240
|
-
for (const encoding of ["base64url", "base64"]) {
|
|
241
|
-
try {
|
|
242
|
-
const decoded = Buffer.from(value, encoding);
|
|
243
|
-
if (decoded.length > 0) {
|
|
244
|
-
return decoded;
|
|
245
|
-
}
|
|
246
|
-
} catch {}
|
|
247
|
-
}
|
|
248
|
-
return null;
|
|
249
|
-
}
|
|
250
|
-
function buffersEqualHex(leftHex, rightHex) {
|
|
251
|
-
try {
|
|
252
|
-
const left = Buffer.from(leftHex, "hex");
|
|
253
|
-
const right = Buffer.from(rightHex, "hex");
|
|
254
|
-
if (left.length === 0 || right.length === 0 || left.length !== right.length) {
|
|
255
|
-
return false;
|
|
256
|
-
}
|
|
257
|
-
return timingSafeEqual(left, right);
|
|
258
|
-
} catch {
|
|
259
|
-
return false;
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
function xorBuffers(buffers) {
|
|
263
|
-
if (buffers.length === 0) {
|
|
264
|
-
return Buffer.alloc(0);
|
|
265
|
-
}
|
|
266
|
-
const length = buffers[0].length;
|
|
267
|
-
const out = Buffer.alloc(length, 0);
|
|
268
|
-
for (const buffer of buffers) {
|
|
269
|
-
if (buffer.length !== length) {
|
|
270
|
-
throw new Error("buffer length mismatch");
|
|
271
|
-
}
|
|
272
|
-
for (let i = 0; i < length; i += 1) {
|
|
273
|
-
out[i] ^= buffer[i];
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
return out;
|
|
277
|
-
}
|
|
278
|
-
function gf256Multiply(left, right) {
|
|
279
|
-
let a = left & 255;
|
|
280
|
-
let b = right & 255;
|
|
281
|
-
let out = 0;
|
|
282
|
-
while (b > 0) {
|
|
283
|
-
if (b & 1) {
|
|
284
|
-
out ^= a;
|
|
285
|
-
}
|
|
286
|
-
const hiBit = a & 128;
|
|
287
|
-
a = a << 1 & 255;
|
|
288
|
-
if (hiBit) {
|
|
289
|
-
a ^= 27;
|
|
290
|
-
}
|
|
291
|
-
b >>= 1;
|
|
292
|
-
}
|
|
293
|
-
return out & 255;
|
|
294
|
-
}
|
|
295
|
-
function gf256Pow(value, exponent) {
|
|
296
|
-
let result = 1;
|
|
297
|
-
let base = value & 255;
|
|
298
|
-
let power = Math.max(0, Math.floor(exponent));
|
|
299
|
-
while (power > 0) {
|
|
300
|
-
if (power & 1) {
|
|
301
|
-
result = gf256Multiply(result, base);
|
|
302
|
-
}
|
|
303
|
-
base = gf256Multiply(base, base);
|
|
304
|
-
power >>= 1;
|
|
305
|
-
}
|
|
306
|
-
return result & 255;
|
|
307
|
-
}
|
|
308
|
-
function gf256Inverse(value) {
|
|
309
|
-
if (value === 0) {
|
|
310
|
-
throw new Error("cannot invert zero in GF(256)");
|
|
311
|
-
}
|
|
312
|
-
return gf256Pow(value, 254);
|
|
313
|
-
}
|
|
314
|
-
function gf256Divide(left, right) {
|
|
315
|
-
if (left === 0) {
|
|
316
|
-
return 0;
|
|
317
|
-
}
|
|
318
|
-
return gf256Multiply(left, gf256Inverse(right));
|
|
319
|
-
}
|
|
320
|
-
function encodeTypedCustodyShare(type, share) {
|
|
321
|
-
return `${type === "device" ? "d" : "r"}.${share.toString("base64url")}`;
|
|
322
|
-
}
|
|
323
|
-
function decodeTypedCustodyShare(raw) {
|
|
324
|
-
const value = raw.trim();
|
|
325
|
-
if (!value) {
|
|
326
|
-
return {
|
|
327
|
-
type: "unknown",
|
|
328
|
-
bytes: null
|
|
329
|
-
};
|
|
330
|
-
}
|
|
331
|
-
const match = /^([dr])\.(.+)$/i.exec(value);
|
|
332
|
-
if (!match) {
|
|
333
|
-
return {
|
|
334
|
-
type: "unknown",
|
|
335
|
-
bytes: decodeBase64Share(value)
|
|
336
|
-
};
|
|
337
|
-
}
|
|
338
|
-
return {
|
|
339
|
-
type: match[1]?.toLowerCase() === "r" ? "recovery" : "device",
|
|
340
|
-
bytes: decodeBase64Share(match[2] ?? "")
|
|
341
|
-
};
|
|
342
|
-
}
|
|
343
|
-
function splitSecretShamir2of3(secret) {
|
|
344
|
-
const slope = randomBytes(secret.length);
|
|
345
|
-
const hostShare = deriveSecretShamirShare(secret, slope, 1);
|
|
346
|
-
const deviceShare = deriveSecretShamirShare(secret, slope, 2);
|
|
347
|
-
const recoveryShare = deriveSecretShamirShare(secret, slope, 3);
|
|
348
|
-
slope.fill(0);
|
|
349
|
-
return {
|
|
350
|
-
hostShare,
|
|
351
|
-
deviceShare,
|
|
352
|
-
recoveryShare
|
|
353
|
-
};
|
|
354
|
-
}
|
|
355
|
-
function deriveSecretShamirShare(secret, slope, x) {
|
|
356
|
-
if (secret.length !== slope.length) {
|
|
357
|
-
throw new Error("share length mismatch");
|
|
358
|
-
}
|
|
359
|
-
const share = Buffer.alloc(secret.length, 0);
|
|
360
|
-
for (let i = 0; i < secret.length; i += 1) {
|
|
361
|
-
const s = secret[i] ?? 0;
|
|
362
|
-
const m = slope[i] ?? 0;
|
|
363
|
-
share[i] = s ^ gf256Multiply(m, x);
|
|
364
|
-
}
|
|
365
|
-
return share;
|
|
366
|
-
}
|
|
367
|
-
function splitSecretShamir2of3WithDeviceShare(secret, deviceShare) {
|
|
368
|
-
if (secret.length !== deviceShare.length) {
|
|
369
|
-
throw new Error("share length mismatch");
|
|
370
|
-
}
|
|
371
|
-
const slope = Buffer.alloc(secret.length, 0);
|
|
372
|
-
for (let i = 0; i < secret.length; i += 1) {
|
|
373
|
-
const s = secret[i] ?? 0;
|
|
374
|
-
const d = deviceShare[i] ?? 0;
|
|
375
|
-
slope[i] = gf256Divide(s ^ d, 2);
|
|
376
|
-
}
|
|
377
|
-
const hostShare = deriveSecretShamirShare(secret, slope, 1);
|
|
378
|
-
const recoveryShare = deriveSecretShamirShare(secret, slope, 3);
|
|
379
|
-
slope.fill(0);
|
|
380
|
-
return {
|
|
381
|
-
hostShare,
|
|
382
|
-
recoveryShare
|
|
383
|
-
};
|
|
384
|
-
}
|
|
385
|
-
function reconstructSecretShamir2ofN(params) {
|
|
386
|
-
const { hostShare, otherShare, otherX } = params;
|
|
387
|
-
if (hostShare.length !== otherShare.length) {
|
|
388
|
-
throw new Error("share length mismatch");
|
|
389
|
-
}
|
|
390
|
-
const out = Buffer.alloc(hostShare.length, 0);
|
|
391
|
-
const denominator = 1 ^ otherX;
|
|
392
|
-
const weightHost = gf256Divide(otherX, denominator);
|
|
393
|
-
const weightOther = gf256Divide(1, denominator);
|
|
394
|
-
for (let i = 0; i < hostShare.length; i += 1) {
|
|
395
|
-
const left = gf256Multiply(hostShare[i] ?? 0, weightHost);
|
|
396
|
-
const right = gf256Multiply(otherShare[i] ?? 0, weightOther);
|
|
397
|
-
out[i] = left ^ right;
|
|
398
|
-
}
|
|
399
|
-
return out;
|
|
400
|
-
}
|
|
401
|
-
function zeroizeBuffers(buffers) {
|
|
402
|
-
for (const buffer of buffers) {
|
|
403
|
-
if (!buffer || buffer.length === 0) {
|
|
404
|
-
continue;
|
|
405
|
-
}
|
|
406
|
-
buffer.fill(0);
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
function removeUnlockMaterial(sessionId) {
|
|
410
|
-
const existing = unlockMaterialBySession.get(sessionId);
|
|
411
|
-
if (!existing) {
|
|
412
|
-
return;
|
|
413
|
-
}
|
|
414
|
-
unlockMaterialBySession.delete(sessionId);
|
|
415
|
-
}
|
|
416
|
-
function pruneUnlockMaterialStore(now = nowMs()) {
|
|
417
|
-
for (const [sessionId, entry] of unlockMaterialBySession.entries()) {
|
|
418
|
-
if (entry.expiresAtMs <= now) {
|
|
419
|
-
removeUnlockMaterial(sessionId);
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
function setUnlockMaterial(params) {
|
|
424
|
-
removeUnlockMaterial(params.sessionId);
|
|
425
|
-
const expiresAtMs = Date.parse(params.expiresAt);
|
|
426
|
-
unlockMaterialBySession.set(params.sessionId, {
|
|
427
|
-
sessionId: params.sessionId,
|
|
428
|
-
host: params.host,
|
|
429
|
-
expiresAtMs: Number.isFinite(expiresAtMs) ? expiresAtMs : nowMs() + 3e4
|
|
430
|
-
});
|
|
431
|
-
}
|
|
432
|
-
function hasActiveUnlockMaterial(params) {
|
|
433
|
-
pruneUnlockMaterialStore();
|
|
434
|
-
const entry = unlockMaterialBySession.get(params.sessionId);
|
|
435
|
-
if (!entry) {
|
|
436
|
-
return false;
|
|
437
|
-
}
|
|
438
|
-
if (entry.host !== params.host || entry.expiresAtMs <= nowMs()) {
|
|
439
|
-
removeUnlockMaterial(params.sessionId);
|
|
440
|
-
return false;
|
|
441
|
-
}
|
|
442
|
-
return true;
|
|
443
|
-
}
|
|
444
|
-
async function withWalletCustodySigningMaterial(params) {
|
|
445
|
-
pruneUnlockMaterialStore();
|
|
446
|
-
const entry = unlockMaterialBySession.get(params.sessionId);
|
|
447
|
-
if (!entry || entry.host !== params.host || entry.expiresAtMs <= nowMs()) {
|
|
448
|
-
removeUnlockMaterial(params.sessionId);
|
|
449
|
-
return {
|
|
450
|
-
ok: false,
|
|
451
|
-
code: "custody_unlock_required",
|
|
452
|
-
message: "custody unlock signing material is unavailable; re-run passkey unlock"
|
|
453
|
-
};
|
|
454
|
-
}
|
|
455
|
-
try {
|
|
456
|
-
const value = await params.handler({
|
|
457
|
-
sessionId: entry.sessionId,
|
|
458
|
-
host: entry.host
|
|
459
|
-
});
|
|
460
|
-
return {
|
|
461
|
-
ok: true,
|
|
462
|
-
value
|
|
463
|
-
};
|
|
464
|
-
} finally {}
|
|
465
|
-
}
|
|
466
|
-
function inferWalletChains(params) {
|
|
467
|
-
try {
|
|
468
|
-
const registry = readWalletProviderRegistry(params.env);
|
|
469
|
-
const namedWallet = registry.wallets.find((entry) => entry.id === params.walletId);
|
|
470
|
-
const inferred = normalizeChainList([namedWallet?.addresses?.solana ? "solana" : ""]);
|
|
471
|
-
if (inferred.length > 0) {
|
|
472
|
-
return inferred;
|
|
473
|
-
}
|
|
474
|
-
} catch {}
|
|
475
|
-
return params.wallet.chains.length > 0 ? params.wallet.chains : ["solana"];
|
|
476
|
-
}
|
|
477
|
-
function resolveWalletCustodyScope(params) {
|
|
478
|
-
const env = params.env ?? process.env;
|
|
479
|
-
const wallet = params.wallet ?? resolveWalletRuntimeConfig(params.cfg ?? {}, env);
|
|
480
|
-
const registry = readWalletProviderRegistry(env);
|
|
481
|
-
const configuredMiningWalletId = typeof params.cfg?.plugins?.entries?.["sat-mining"]?.config?.walletId === "string" ? params.cfg.plugins.entries["sat-mining"]?.config?.walletId.trim() : "";
|
|
482
|
-
const explicitWalletId = params.walletId?.trim() || "";
|
|
483
|
-
const walletId = explicitWalletId || registry.defaultWalletId?.trim() || configuredMiningWalletId || registry.wallets.find((entry) => entry.providerId === "local-socket-signer")?.id || "default";
|
|
484
|
-
const registryWallet = registry.wallets.find((entry) => entry.id === walletId);
|
|
485
|
-
const walletPurpose = resolveWalletUserRole(registryWallet);
|
|
486
|
-
const role = walletId === configuredMiningWalletId || walletPurpose === "mining" ? "mining" : walletId === registry.defaultWalletId || walletPurpose === "agent" ? "agent" : "vault";
|
|
487
|
-
const effectiveWallet = applyWalletPolicyConfig({
|
|
488
|
-
config: wallet,
|
|
489
|
-
cfg: params.cfg,
|
|
490
|
-
env,
|
|
491
|
-
walletId
|
|
492
|
-
});
|
|
493
|
-
const roleProfile = resolveWalletRolePolicyProfile(role, env);
|
|
494
|
-
const chains = role === "mining" ? ["solana"] : inferWalletChains({
|
|
495
|
-
wallet: effectiveWallet,
|
|
496
|
-
env,
|
|
497
|
-
walletId
|
|
498
|
-
});
|
|
499
|
-
const allowPrograms = new Set(normalizeAddressList(effectiveWallet.policy.solana.allowPrograms));
|
|
500
|
-
if (role === "mining") {
|
|
501
|
-
const ids = tryResolveSatRuntimeIds(env);
|
|
502
|
-
if (ids?.programId) {
|
|
503
|
-
allowPrograms.add(ids.programId.toLowerCase());
|
|
504
|
-
}
|
|
505
|
-
} else if (chains.includes("solana")) {
|
|
506
|
-
allowPrograms.add(SOLANA_TOKEN_PROGRAM_ID.toLowerCase());
|
|
507
|
-
allowPrograms.add(SOLANA_TOKEN_2022_PROGRAM_ID.toLowerCase());
|
|
508
|
-
allowPrograms.add(SOLANA_ASSOCIATED_TOKEN_PROGRAM_ID.toLowerCase());
|
|
509
|
-
}
|
|
510
|
-
for (const program of roleProfile.defaults.solana.allowPrograms) {
|
|
511
|
-
allowPrograms.add(program.toLowerCase());
|
|
512
|
-
}
|
|
513
|
-
const solanaMaxPerTx = role === "mining" ? roleProfile.defaults.solana.maxPerTx : effectiveWallet.policy.solana.caps.maxPerTx.toString();
|
|
514
|
-
const solanaMaxDaily = role === "mining" ? roleProfile.defaults.solana.maxDaily : effectiveWallet.policy.solana.caps.maxDaily.toString();
|
|
515
|
-
return {
|
|
516
|
-
walletId,
|
|
517
|
-
role,
|
|
518
|
-
chains,
|
|
519
|
-
allowPrograms: [...allowPrograms],
|
|
520
|
-
solana: {
|
|
521
|
-
maxPerTx: solanaMaxPerTx,
|
|
522
|
-
maxDaily: solanaMaxDaily
|
|
523
|
-
}
|
|
524
|
-
};
|
|
525
|
-
}
|
|
526
|
-
function loadShareState(walletId, env = process.env) {
|
|
527
|
-
const filePath = locateCustodyShareStatePath(walletId, env);
|
|
528
|
-
if (!syncFs.existsSync(filePath)) {
|
|
529
|
-
return null;
|
|
530
|
-
}
|
|
531
|
-
try {
|
|
532
|
-
const parsed = JSON.parse(syncFs.readFileSync(filePath, "utf8"));
|
|
533
|
-
if (parsed.version === 1 && parsed.scheme === "xor-3of3-v1" && typeof parsed.secretBytes === "number" && parsed.secretBytes > 0 && typeof parsed.hotShare === "string" && typeof parsed.coldShare === "string" && typeof parsed.deviceShareHash === "string" && typeof parsed.secretChecksum === "string" && typeof parsed.createdAt === "string" && typeof parsed.updatedAt === "string") {
|
|
534
|
-
const hot = decodeBase64Share(parsed.hotShare);
|
|
535
|
-
const cold = decodeBase64Share(parsed.coldShare);
|
|
536
|
-
if (!hot || !cold || hot.length !== parsed.secretBytes || cold.length !== parsed.secretBytes) {
|
|
537
|
-
return null;
|
|
538
|
-
}
|
|
539
|
-
return {
|
|
540
|
-
version: 1,
|
|
541
|
-
scheme: "xor-3of3-v1",
|
|
542
|
-
secretBytes: parsed.secretBytes,
|
|
543
|
-
hotShare: parsed.hotShare,
|
|
544
|
-
coldShare: parsed.coldShare,
|
|
545
|
-
deviceShareHash: parsed.deviceShareHash,
|
|
546
|
-
secretChecksum: parsed.secretChecksum,
|
|
547
|
-
walletId: typeof parsed.walletId === "string" ? parsed.walletId.trim() || undefined : undefined,
|
|
548
|
-
role: normalizeWalletCustodyRole(parsed.role),
|
|
549
|
-
createdAt: parsed.createdAt,
|
|
550
|
-
updatedAt: parsed.updatedAt
|
|
551
|
-
};
|
|
552
|
-
}
|
|
553
|
-
if (parsed.version === 2 && parsed.scheme === "shamir-2of3-v1" && typeof parsed.secretBytes === "number" && parsed.secretBytes > 0 && typeof parsed.hostShare === "string" && typeof parsed.deviceShareHash === "string" && typeof parsed.recoveryShareHash === "string" && typeof parsed.secretChecksum === "string" && typeof parsed.createdAt === "string" && typeof parsed.updatedAt === "string") {
|
|
554
|
-
const host = decodeBase64Share(parsed.hostShare);
|
|
555
|
-
if (!host || host.length !== parsed.secretBytes) {
|
|
556
|
-
return null;
|
|
557
|
-
}
|
|
558
|
-
return {
|
|
559
|
-
version: 2,
|
|
560
|
-
scheme: "shamir-2of3-v1",
|
|
561
|
-
secretBytes: parsed.secretBytes,
|
|
562
|
-
hostShare: parsed.hostShare,
|
|
563
|
-
deviceShareHash: parsed.deviceShareHash,
|
|
564
|
-
recoveryShareHash: parsed.recoveryShareHash,
|
|
565
|
-
secretChecksum: parsed.secretChecksum,
|
|
566
|
-
walletId: typeof parsed.walletId === "string" ? parsed.walletId.trim() || undefined : undefined,
|
|
567
|
-
role: normalizeWalletCustodyRole(parsed.role),
|
|
568
|
-
createdAt: parsed.createdAt,
|
|
569
|
-
updatedAt: parsed.updatedAt
|
|
570
|
-
};
|
|
571
|
-
}
|
|
572
|
-
if (parsed.version === 3 && parsed.scheme === "shamir-2ofn-v1" && typeof parsed.secretBytes === "number" && parsed.secretBytes > 0 && typeof parsed.hostShare === "string" && typeof parsed.recoveryShareHash === "string" && typeof parsed.secretChecksum === "string" && typeof parsed.createdAt === "string" && typeof parsed.updatedAt === "string" && Array.isArray(parsed.devices)) {
|
|
573
|
-
const host = decodeBase64Share(parsed.hostShare);
|
|
574
|
-
if (!host || host.length !== parsed.secretBytes) {
|
|
575
|
-
return null;
|
|
576
|
-
}
|
|
577
|
-
const devices = (parsed.devices ?? []).map(normalizeEnrolledDevice).filter(isNonNull);
|
|
578
|
-
return {
|
|
579
|
-
version: 3,
|
|
580
|
-
scheme: "shamir-2ofn-v1",
|
|
581
|
-
secretBytes: parsed.secretBytes,
|
|
582
|
-
hostShare: parsed.hostShare,
|
|
583
|
-
recoveryShareHash: parsed.recoveryShareHash,
|
|
584
|
-
secretChecksum: parsed.secretChecksum,
|
|
585
|
-
devices,
|
|
586
|
-
walletId: typeof parsed.walletId === "string" ? parsed.walletId.trim() || undefined : undefined,
|
|
587
|
-
role: normalizeWalletCustodyRole(parsed.role),
|
|
588
|
-
createdAt: parsed.createdAt,
|
|
589
|
-
updatedAt: parsed.updatedAt
|
|
590
|
-
};
|
|
591
|
-
}
|
|
592
|
-
return null;
|
|
593
|
-
} catch {
|
|
594
|
-
return null;
|
|
595
|
-
}
|
|
596
|
-
}
|
|
597
|
-
function saveShareState(state, walletId, env = process.env) {
|
|
598
|
-
const filePath = resolveCustodyShareStatePath(walletId, env);
|
|
599
|
-
syncFs.mkdirSync(path.dirname(filePath), {
|
|
600
|
-
recursive: true,
|
|
601
|
-
mode: 448
|
|
602
|
-
});
|
|
603
|
-
syncFs.writeFileSync(filePath, `${JSON.stringify(state, null, 2)}\n`, {
|
|
604
|
-
encoding: "utf8",
|
|
605
|
-
mode: 384
|
|
606
|
-
});
|
|
607
|
-
try {
|
|
608
|
-
syncFs.chmodSync(filePath, 384);
|
|
609
|
-
} catch {}
|
|
610
|
-
}
|
|
611
|
-
function deleteWalletCustodyCeremony(params) {
|
|
612
|
-
const env = params?.env ?? process.env;
|
|
613
|
-
const scope = params?.wallet && params?.cfg ? resolveWalletCustodyScope({
|
|
614
|
-
wallet: params.wallet,
|
|
615
|
-
cfg: params.cfg,
|
|
616
|
-
env,
|
|
617
|
-
walletId: params.walletId
|
|
618
|
-
}) : {
|
|
619
|
-
walletId: params?.walletId?.trim() || "default",
|
|
620
|
-
role: "vault"
|
|
621
|
-
};
|
|
622
|
-
const statePath = locateCustodyShareStatePath(scope.walletId, env);
|
|
623
|
-
const existed = syncFs.existsSync(statePath);
|
|
624
|
-
if (existed) {
|
|
625
|
-
syncFs.rmSync(statePath, { force: true });
|
|
626
|
-
try {
|
|
627
|
-
syncFs.rmdirSync(path.dirname(statePath));
|
|
628
|
-
} catch {}
|
|
629
|
-
}
|
|
630
|
-
return {
|
|
631
|
-
ok: true,
|
|
632
|
-
walletId: scope.walletId,
|
|
633
|
-
removed: existed,
|
|
634
|
-
statePath
|
|
635
|
-
};
|
|
636
|
-
}
|
|
637
|
-
function encodeCustodyShareStateV3(params) {
|
|
638
|
-
const secretBytes = params.shareState?.secretBytes && params.shareState.secretBytes > 0 ? params.shareState.secretBytes : params.hostShare.length;
|
|
639
|
-
return {
|
|
640
|
-
version: 3,
|
|
641
|
-
scheme: "shamir-2ofn-v1",
|
|
642
|
-
secretBytes,
|
|
643
|
-
hostShare: params.hostShare.toString("base64url"),
|
|
644
|
-
recoveryShareHash: params.recoveryShareHash,
|
|
645
|
-
secretChecksum: params.secretChecksum,
|
|
646
|
-
devices: params.devices.map((device) => ({
|
|
647
|
-
id: device.id,
|
|
648
|
-
label: normalizeDeviceLabel(device.label),
|
|
649
|
-
shareX: device.shareX,
|
|
650
|
-
shareHash: device.shareHash,
|
|
651
|
-
createdAt: device.createdAt,
|
|
652
|
-
revokedAt: device.revokedAt
|
|
653
|
-
})),
|
|
654
|
-
walletId: params.walletId,
|
|
655
|
-
role: params.role,
|
|
656
|
-
createdAt: params.createdAt,
|
|
657
|
-
updatedAt: params.updatedAt
|
|
658
|
-
};
|
|
659
|
-
}
|
|
660
|
-
function initializeWalletCustodyCeremony(params) {
|
|
661
|
-
const env = params?.env ?? process.env;
|
|
662
|
-
const scope = params?.wallet && params?.cfg ? resolveWalletCustodyScope({
|
|
663
|
-
wallet: params.wallet,
|
|
664
|
-
cfg: params.cfg,
|
|
665
|
-
env,
|
|
666
|
-
walletId: params.walletId
|
|
667
|
-
}) : {
|
|
668
|
-
walletId: params?.walletId?.trim() || "default",
|
|
669
|
-
role: "vault"
|
|
670
|
-
};
|
|
671
|
-
const existing = loadShareState(scope.walletId, env);
|
|
672
|
-
if (existing && !params?.force) {
|
|
673
|
-
return {
|
|
674
|
-
ok: false,
|
|
675
|
-
code: "ceremony_exists",
|
|
676
|
-
message: "wallet custody ceremony is already initialized",
|
|
677
|
-
statePath: locateCustodyShareStatePath(scope.walletId, env)
|
|
678
|
-
};
|
|
679
|
-
}
|
|
680
|
-
const secret = randomBytes(CUSTODY_SECRET_BYTES);
|
|
681
|
-
let hostShare = null;
|
|
682
|
-
let deviceShare = null;
|
|
683
|
-
let recoveryShare = null;
|
|
684
|
-
try {
|
|
685
|
-
const requested = decodeTypedCustodyShare(params?.deviceShare ?? "");
|
|
686
|
-
const requestedDeviceShare = requested.bytes;
|
|
687
|
-
if (requestedDeviceShare && requestedDeviceShare.length !== CUSTODY_SECRET_BYTES) {
|
|
688
|
-
return {
|
|
689
|
-
ok: false,
|
|
690
|
-
code: "invalid_device_share",
|
|
691
|
-
message: `device share must be ${CUSTODY_SECRET_BYTES} bytes (base64/base64url)`
|
|
692
|
-
};
|
|
693
|
-
}
|
|
694
|
-
if (requestedDeviceShare) {
|
|
695
|
-
deviceShare = Buffer.from(requestedDeviceShare);
|
|
696
|
-
const split = splitSecretShamir2of3WithDeviceShare(secret, deviceShare);
|
|
697
|
-
hostShare = split.hostShare;
|
|
698
|
-
recoveryShare = split.recoveryShare;
|
|
699
|
-
} else {
|
|
700
|
-
const split = splitSecretShamir2of3(secret);
|
|
701
|
-
hostShare = split.hostShare;
|
|
702
|
-
deviceShare = split.deviceShare;
|
|
703
|
-
recoveryShare = split.recoveryShare;
|
|
704
|
-
}
|
|
705
|
-
const nowIso = new Date().toISOString();
|
|
706
|
-
const record = encodeCustodyShareStateV3({
|
|
707
|
-
shareState: existing,
|
|
708
|
-
walletId: scope.walletId,
|
|
709
|
-
role: scope.role,
|
|
710
|
-
hostShare,
|
|
711
|
-
recoveryShareHash: sha256Hex(recoveryShare),
|
|
712
|
-
secretChecksum: sha256Hex(secret),
|
|
713
|
-
createdAt: existing?.createdAt ?? nowIso,
|
|
714
|
-
updatedAt: nowIso,
|
|
715
|
-
devices: [{
|
|
716
|
-
id: createCustodyDeviceId(),
|
|
717
|
-
label: normalizeDeviceLabel(params?.deviceLabel) ?? "Primary device",
|
|
718
|
-
shareX: 2,
|
|
719
|
-
shareHash: sha256Hex(deviceShare),
|
|
720
|
-
createdAt: nowIso
|
|
721
|
-
}]
|
|
722
|
-
});
|
|
723
|
-
saveShareState(record, scope.walletId, env);
|
|
724
|
-
return {
|
|
725
|
-
ok: true,
|
|
726
|
-
statePath: resolveCustodyShareStatePath(scope.walletId, env),
|
|
727
|
-
scheme: record.scheme,
|
|
728
|
-
walletId: scope.walletId,
|
|
729
|
-
role: scope.role,
|
|
730
|
-
deviceShare: encodeTypedCustodyShare("device", deviceShare),
|
|
731
|
-
recoveryShare: encodeTypedCustodyShare("recovery", recoveryShare),
|
|
732
|
-
secretBytes: CUSTODY_SECRET_BYTES,
|
|
733
|
-
updatedAt: record.updatedAt
|
|
734
|
-
};
|
|
735
|
-
} finally {
|
|
736
|
-
zeroizeBuffers([
|
|
737
|
-
secret,
|
|
738
|
-
hostShare,
|
|
739
|
-
deviceShare,
|
|
740
|
-
recoveryShare
|
|
741
|
-
]);
|
|
742
|
-
}
|
|
743
|
-
}
|
|
744
|
-
function recoverWalletCustodyCeremony(params) {
|
|
745
|
-
const env = params?.env ?? process.env;
|
|
746
|
-
const scope = params?.wallet && params?.cfg ? resolveWalletCustodyScope({
|
|
747
|
-
wallet: params.wallet,
|
|
748
|
-
cfg: params.cfg,
|
|
749
|
-
env,
|
|
750
|
-
walletId: params.walletId
|
|
751
|
-
}) : {
|
|
752
|
-
walletId: params?.walletId?.trim() || "default",
|
|
753
|
-
role: "vault"
|
|
754
|
-
};
|
|
755
|
-
const existing = loadShareState(scope.walletId, env);
|
|
756
|
-
if (!existing) {
|
|
757
|
-
return {
|
|
758
|
-
ok: false,
|
|
759
|
-
code: "custody_ceremony_required",
|
|
760
|
-
message: "split-key custody ceremony is not initialized",
|
|
761
|
-
statePath: locateCustodyShareStatePath(scope.walletId, env)
|
|
762
|
-
};
|
|
763
|
-
}
|
|
764
|
-
const reconstructed = reconstructCustodySigningKey({
|
|
765
|
-
env,
|
|
766
|
-
recoveryShare: params?.recoveryShare,
|
|
767
|
-
walletId: scope.walletId
|
|
768
|
-
});
|
|
769
|
-
if (!reconstructed.ok) {
|
|
770
|
-
return reconstructed;
|
|
771
|
-
}
|
|
772
|
-
let deviceShare = null;
|
|
773
|
-
let hostShare = null;
|
|
774
|
-
let recoveryShare = null;
|
|
775
|
-
try {
|
|
776
|
-
const requested = decodeTypedCustodyShare(params?.deviceShare ?? "");
|
|
777
|
-
const requestedDeviceShare = requested.bytes;
|
|
778
|
-
if (requestedDeviceShare && requestedDeviceShare.length !== CUSTODY_SECRET_BYTES) {
|
|
779
|
-
return {
|
|
780
|
-
ok: false,
|
|
781
|
-
code: "invalid_device_share",
|
|
782
|
-
message: `device share must be ${CUSTODY_SECRET_BYTES} bytes (base64/base64url)`
|
|
783
|
-
};
|
|
784
|
-
}
|
|
785
|
-
if (requestedDeviceShare) {
|
|
786
|
-
deviceShare = Buffer.from(requestedDeviceShare);
|
|
787
|
-
const split = splitSecretShamir2of3WithDeviceShare(reconstructed.signingKey, deviceShare);
|
|
788
|
-
hostShare = split.hostShare;
|
|
789
|
-
recoveryShare = split.recoveryShare;
|
|
790
|
-
} else {
|
|
791
|
-
const split = splitSecretShamir2of3(reconstructed.signingKey);
|
|
792
|
-
hostShare = split.hostShare;
|
|
793
|
-
deviceShare = split.deviceShare;
|
|
794
|
-
recoveryShare = split.recoveryShare;
|
|
795
|
-
}
|
|
796
|
-
const updatedAt = new Date().toISOString();
|
|
797
|
-
const record = encodeCustodyShareStateV3({
|
|
798
|
-
shareState: existing,
|
|
799
|
-
walletId: scope.walletId,
|
|
800
|
-
role: scope.role,
|
|
801
|
-
hostShare,
|
|
802
|
-
recoveryShareHash: sha256Hex(recoveryShare),
|
|
803
|
-
secretChecksum: sha256Hex(reconstructed.signingKey),
|
|
804
|
-
createdAt: existing.createdAt,
|
|
805
|
-
updatedAt,
|
|
806
|
-
devices: [{
|
|
807
|
-
id: createCustodyDeviceId(),
|
|
808
|
-
label: normalizeDeviceLabel(params?.deviceLabel) ?? "Recovered device",
|
|
809
|
-
shareX: 2,
|
|
810
|
-
shareHash: sha256Hex(deviceShare),
|
|
811
|
-
createdAt: updatedAt
|
|
812
|
-
}]
|
|
813
|
-
});
|
|
814
|
-
saveShareState(record, scope.walletId, env);
|
|
815
|
-
return {
|
|
816
|
-
ok: true,
|
|
817
|
-
statePath: resolveCustodyShareStatePath(scope.walletId, env),
|
|
818
|
-
scheme: record.scheme,
|
|
819
|
-
walletId: scope.walletId,
|
|
820
|
-
role: scope.role,
|
|
821
|
-
deviceShare: encodeTypedCustodyShare("device", deviceShare),
|
|
822
|
-
recoveryShare: encodeTypedCustodyShare("recovery", recoveryShare),
|
|
823
|
-
secretBytes: CUSTODY_SECRET_BYTES,
|
|
824
|
-
updatedAt
|
|
825
|
-
};
|
|
826
|
-
} finally {
|
|
827
|
-
zeroizeBuffers([
|
|
828
|
-
reconstructed.signingKey,
|
|
829
|
-
deviceShare,
|
|
830
|
-
hostShare,
|
|
831
|
-
recoveryShare
|
|
832
|
-
]);
|
|
833
|
-
}
|
|
834
|
-
}
|
|
835
|
-
function enrollWalletCustodyDevice(params) {
|
|
836
|
-
const env = params?.env ?? process.env;
|
|
837
|
-
const scope = resolveWalletCustodyScope({
|
|
838
|
-
wallet: params?.wallet,
|
|
839
|
-
cfg: params?.cfg,
|
|
840
|
-
env,
|
|
841
|
-
walletId: params?.walletId
|
|
842
|
-
});
|
|
843
|
-
const shareState = loadShareState(scope.walletId, env);
|
|
844
|
-
if (!shareState) {
|
|
845
|
-
return {
|
|
846
|
-
ok: false,
|
|
847
|
-
code: "custody_ceremony_required",
|
|
848
|
-
message: "split-key custody ceremony is not initialized"
|
|
849
|
-
};
|
|
850
|
-
}
|
|
851
|
-
if (shareState.scheme === "xor-3of3-v1") {
|
|
852
|
-
return {
|
|
853
|
-
ok: false,
|
|
854
|
-
code: "custody_scheme_unsupported",
|
|
855
|
-
message: "legacy custody scheme does not support second-device enrollment; recover the wallet first"
|
|
856
|
-
};
|
|
857
|
-
}
|
|
858
|
-
const reconstructed = reconstructCustodySigningKey({
|
|
859
|
-
env,
|
|
860
|
-
walletId: scope.walletId,
|
|
861
|
-
deviceShare: params?.deviceShare,
|
|
862
|
-
recoveryShare: params?.recoveryShare
|
|
863
|
-
});
|
|
864
|
-
if (!reconstructed.ok) {
|
|
865
|
-
return reconstructed;
|
|
866
|
-
}
|
|
867
|
-
let hostShare = null;
|
|
868
|
-
let slope = null;
|
|
869
|
-
let newDeviceShare = null;
|
|
870
|
-
try {
|
|
871
|
-
hostShare = decodeBase64Share(shareState.hostShare);
|
|
872
|
-
if (!hostShare || hostShare.length !== CUSTODY_SECRET_BYTES) {
|
|
873
|
-
return {
|
|
874
|
-
ok: false,
|
|
875
|
-
code: "custody_shares_invalid",
|
|
876
|
-
message: "stored split-key share payload is invalid"
|
|
877
|
-
};
|
|
878
|
-
}
|
|
879
|
-
const activeDevices = listActiveEnrolledDevices(shareState);
|
|
880
|
-
const shareX = allocateNextDeviceShareX(activeDevices);
|
|
881
|
-
slope = Buffer.alloc(reconstructed.signingKey.length, 0);
|
|
882
|
-
for (let i = 0; i < reconstructed.signingKey.length; i += 1) {
|
|
883
|
-
slope[i] = (reconstructed.signingKey[i] ?? 0) ^ (hostShare[i] ?? 0);
|
|
884
|
-
}
|
|
885
|
-
newDeviceShare = deriveSecretShamirShare(reconstructed.signingKey, slope, shareX);
|
|
886
|
-
const updatedAt = new Date().toISOString();
|
|
887
|
-
const newDevice = {
|
|
888
|
-
id: createCustodyDeviceId(),
|
|
889
|
-
label: normalizeDeviceLabel(params?.label) ?? `Device ${activeDevices.length + 1}`,
|
|
890
|
-
shareX,
|
|
891
|
-
shareHash: sha256Hex(newDeviceShare),
|
|
892
|
-
createdAt: updatedAt
|
|
893
|
-
};
|
|
894
|
-
const record = encodeCustodyShareStateV3({
|
|
895
|
-
shareState,
|
|
896
|
-
walletId: scope.walletId,
|
|
897
|
-
role: scope.role,
|
|
898
|
-
hostShare,
|
|
899
|
-
recoveryShareHash: shareState.recoveryShareHash,
|
|
900
|
-
secretChecksum: shareState.secretChecksum,
|
|
901
|
-
createdAt: shareState.createdAt,
|
|
902
|
-
updatedAt,
|
|
903
|
-
devices: [...activeDevices, newDevice]
|
|
904
|
-
});
|
|
905
|
-
saveShareState(record, scope.walletId, env);
|
|
906
|
-
return {
|
|
907
|
-
ok: true,
|
|
908
|
-
walletId: scope.walletId,
|
|
909
|
-
role: scope.role,
|
|
910
|
-
deviceId: newDevice.id,
|
|
911
|
-
label: newDevice.label,
|
|
912
|
-
deviceShare: encodeTypedCustodyShare("device", newDeviceShare),
|
|
913
|
-
statePath: resolveCustodyShareStatePath(scope.walletId, env),
|
|
914
|
-
updatedAt
|
|
915
|
-
};
|
|
916
|
-
} finally {
|
|
917
|
-
zeroizeBuffers([
|
|
918
|
-
reconstructed.signingKey,
|
|
919
|
-
hostShare,
|
|
920
|
-
slope,
|
|
921
|
-
newDeviceShare
|
|
922
|
-
]);
|
|
923
|
-
}
|
|
924
|
-
}
|
|
925
|
-
function revokeWalletCustodyDevice(params) {
|
|
926
|
-
const env = params?.env ?? process.env;
|
|
927
|
-
const scope = resolveWalletCustodyScope({
|
|
928
|
-
wallet: params?.wallet,
|
|
929
|
-
cfg: params?.cfg,
|
|
930
|
-
env,
|
|
931
|
-
walletId: params?.walletId
|
|
932
|
-
});
|
|
933
|
-
const shareState = loadShareState(scope.walletId, env);
|
|
934
|
-
if (!shareState) {
|
|
935
|
-
return {
|
|
936
|
-
ok: false,
|
|
937
|
-
code: "custody_ceremony_required",
|
|
938
|
-
message: "split-key custody ceremony is not initialized"
|
|
939
|
-
};
|
|
940
|
-
}
|
|
941
|
-
if (shareState.scheme === "xor-3of3-v1") {
|
|
942
|
-
return {
|
|
943
|
-
ok: false,
|
|
944
|
-
code: "custody_scheme_unsupported",
|
|
945
|
-
message: "legacy custody scheme does not support per-device revocation; recover the wallet first"
|
|
946
|
-
};
|
|
947
|
-
}
|
|
948
|
-
const targetDeviceId = String(params?.deviceId ?? "").trim();
|
|
949
|
-
if (!targetDeviceId) {
|
|
950
|
-
return {
|
|
951
|
-
ok: false,
|
|
952
|
-
code: "custody_device_id_required",
|
|
953
|
-
message: "device id is required"
|
|
954
|
-
};
|
|
955
|
-
}
|
|
956
|
-
const reconstructed = reconstructCustodySigningKey({
|
|
957
|
-
env,
|
|
958
|
-
walletId: scope.walletId,
|
|
959
|
-
deviceShare: params?.deviceShare,
|
|
960
|
-
recoveryShare: params?.recoveryShare
|
|
961
|
-
});
|
|
962
|
-
if (!reconstructed.ok) {
|
|
963
|
-
return reconstructed;
|
|
964
|
-
}
|
|
965
|
-
let hostShare = null;
|
|
966
|
-
try {
|
|
967
|
-
hostShare = decodeBase64Share(shareState.hostShare);
|
|
968
|
-
if (!hostShare || hostShare.length !== CUSTODY_SECRET_BYTES) {
|
|
969
|
-
return {
|
|
970
|
-
ok: false,
|
|
971
|
-
code: "custody_shares_invalid",
|
|
972
|
-
message: "stored split-key share payload is invalid"
|
|
973
|
-
};
|
|
974
|
-
}
|
|
975
|
-
const activeDevices = listActiveEnrolledDevices(shareState);
|
|
976
|
-
const target = activeDevices.find((device) => device.id === targetDeviceId) ?? null;
|
|
977
|
-
if (!target) {
|
|
978
|
-
return {
|
|
979
|
-
ok: false,
|
|
980
|
-
code: "custody_device_not_found",
|
|
981
|
-
message: "device is not enrolled for this wallet"
|
|
982
|
-
};
|
|
983
|
-
}
|
|
984
|
-
if (activeDevices.length <= 1) {
|
|
985
|
-
return {
|
|
986
|
-
ok: false,
|
|
987
|
-
code: "custody_last_device_forbidden",
|
|
988
|
-
message: "cannot revoke the last enrolled device; recover the wallet instead"
|
|
989
|
-
};
|
|
990
|
-
}
|
|
991
|
-
const updatedAt = new Date().toISOString();
|
|
992
|
-
const record = encodeCustodyShareStateV3({
|
|
993
|
-
shareState,
|
|
994
|
-
walletId: scope.walletId,
|
|
995
|
-
role: scope.role,
|
|
996
|
-
hostShare,
|
|
997
|
-
recoveryShareHash: shareState.recoveryShareHash,
|
|
998
|
-
secretChecksum: shareState.secretChecksum,
|
|
999
|
-
createdAt: shareState.createdAt,
|
|
1000
|
-
updatedAt,
|
|
1001
|
-
devices: activeDevices.filter((device) => device.id !== targetDeviceId)
|
|
1002
|
-
});
|
|
1003
|
-
saveShareState(record, scope.walletId, env);
|
|
1004
|
-
return {
|
|
1005
|
-
ok: true,
|
|
1006
|
-
walletId: scope.walletId,
|
|
1007
|
-
role: scope.role,
|
|
1008
|
-
removedDeviceId: target.id,
|
|
1009
|
-
removedDeviceLabel: target.label,
|
|
1010
|
-
updatedAt
|
|
1011
|
-
};
|
|
1012
|
-
} finally {
|
|
1013
|
-
zeroizeBuffers([reconstructed.signingKey, hostShare]);
|
|
1014
|
-
}
|
|
1015
|
-
}
|
|
1016
|
-
function loadState(walletId, env = process.env) {
|
|
1017
|
-
const filePath = locateCustodyStatePath(walletId, env);
|
|
1018
|
-
if (!syncFs.existsSync(filePath)) {
|
|
1019
|
-
return {
|
|
1020
|
-
version: 1,
|
|
1021
|
-
unlockSessions: []
|
|
1022
|
-
};
|
|
1023
|
-
}
|
|
1024
|
-
try {
|
|
1025
|
-
const parsed = JSON.parse(syncFs.readFileSync(filePath, "utf8"));
|
|
1026
|
-
if (parsed.version !== 1 || !Array.isArray(parsed.unlockSessions)) {
|
|
1027
|
-
return {
|
|
1028
|
-
version: 1,
|
|
1029
|
-
unlockSessions: []
|
|
1030
|
-
};
|
|
1031
|
-
}
|
|
1032
|
-
return {
|
|
1033
|
-
version: 1,
|
|
1034
|
-
unlockSessions: parsed.unlockSessions.map((entry) => {
|
|
1035
|
-
if (!entry || typeof entry !== "object") {
|
|
1036
|
-
return null;
|
|
1037
|
-
}
|
|
1038
|
-
const value = entry;
|
|
1039
|
-
if (typeof value.id !== "string" || typeof value.host !== "string" || typeof value.walletId !== "string" || typeof value.createdAt !== "string" || typeof value.expiresAt !== "string") {
|
|
1040
|
-
return null;
|
|
1041
|
-
}
|
|
1042
|
-
const chains = normalizeChainList(Array.isArray(value.chains) ? (value.chains ?? []).map((entry) => String(entry)) : []);
|
|
1043
|
-
const allowPrograms = normalizeAddressList(Array.isArray(value.allowPrograms) ? (value.allowPrograms ?? []).map((entry) => String(entry)) : []);
|
|
1044
|
-
return {
|
|
1045
|
-
id: value.id,
|
|
1046
|
-
host: normalizeHost(value.host),
|
|
1047
|
-
walletId: value.walletId.trim() || "default",
|
|
1048
|
-
role: normalizeWalletCustodyRole(value.role) ?? "vault",
|
|
1049
|
-
chains,
|
|
1050
|
-
allowPrograms,
|
|
1051
|
-
solanaMaxPerTx: typeof value.solanaMaxPerTx === "string" ? value.solanaMaxPerTx : "0",
|
|
1052
|
-
solanaMaxDaily: typeof value.solanaMaxDaily === "string" ? value.solanaMaxDaily : "0",
|
|
1053
|
-
createdAt: value.createdAt,
|
|
1054
|
-
expiresAt: value.expiresAt,
|
|
1055
|
-
lastUsedAt: typeof value.lastUsedAt === "string" ? value.lastUsedAt : undefined
|
|
1056
|
-
};
|
|
1057
|
-
}).filter(isNonNull)
|
|
1058
|
-
};
|
|
1059
|
-
} catch {
|
|
1060
|
-
return {
|
|
1061
|
-
version: 1,
|
|
1062
|
-
unlockSessions: []
|
|
1063
|
-
};
|
|
1064
|
-
}
|
|
1065
|
-
}
|
|
1066
|
-
function saveState(state, walletId, env = process.env) {
|
|
1067
|
-
const filePath = resolveCustodyStatePath(walletId, env);
|
|
1068
|
-
syncFs.mkdirSync(path.dirname(filePath), {
|
|
1069
|
-
recursive: true,
|
|
1070
|
-
mode: 448
|
|
1071
|
-
});
|
|
1072
|
-
syncFs.writeFileSync(filePath, `${JSON.stringify(state, null, 2)}\n`, {
|
|
1073
|
-
encoding: "utf8",
|
|
1074
|
-
mode: 384
|
|
1075
|
-
});
|
|
1076
|
-
try {
|
|
1077
|
-
syncFs.chmodSync(filePath, 384);
|
|
1078
|
-
} catch {}
|
|
1079
|
-
}
|
|
1080
|
-
function pruneState(state) {
|
|
1081
|
-
const now = nowMs();
|
|
1082
|
-
const active = [];
|
|
1083
|
-
let changed = false;
|
|
1084
|
-
for (const entry of state.unlockSessions) {
|
|
1085
|
-
const expiresAt = Date.parse(entry.expiresAt);
|
|
1086
|
-
if (Number.isFinite(expiresAt) && expiresAt > now) {
|
|
1087
|
-
active.push(entry);
|
|
1088
|
-
continue;
|
|
1089
|
-
}
|
|
1090
|
-
removeUnlockMaterial(entry.id);
|
|
1091
|
-
changed = true;
|
|
1092
|
-
}
|
|
1093
|
-
if (changed) {
|
|
1094
|
-
state.unlockSessions = active;
|
|
1095
|
-
}
|
|
1096
|
-
return changed;
|
|
1097
|
-
}
|
|
1098
|
-
function getActiveUnlockSession(params) {
|
|
1099
|
-
const env = params.env ?? process.env;
|
|
1100
|
-
const host = normalizeHost(params.host);
|
|
1101
|
-
if (!host) {
|
|
1102
|
-
return null;
|
|
1103
|
-
}
|
|
1104
|
-
const state = loadState(params.walletId, env);
|
|
1105
|
-
const changed = pruneState(state);
|
|
1106
|
-
const active = state.unlockSessions.find((entry) => entry.host === host && entry.walletId === params.walletId) ?? null;
|
|
1107
|
-
if (changed) {
|
|
1108
|
-
saveState(state, params.walletId, env);
|
|
1109
|
-
}
|
|
1110
|
-
return active;
|
|
1111
|
-
}
|
|
1112
|
-
function touchUnlockSession(params) {
|
|
1113
|
-
const env = params.env ?? process.env;
|
|
1114
|
-
const state = loadState(params.walletId, env);
|
|
1115
|
-
const changed = pruneState(state);
|
|
1116
|
-
const session = state.unlockSessions.find((entry) => entry.id === params.sessionId) ?? null;
|
|
1117
|
-
if (!session) {
|
|
1118
|
-
if (changed) {
|
|
1119
|
-
saveState(state, params.walletId, env);
|
|
1120
|
-
}
|
|
1121
|
-
removeUnlockMaterial(params.sessionId);
|
|
1122
|
-
return null;
|
|
1123
|
-
}
|
|
1124
|
-
session.lastUsedAt = new Date().toISOString();
|
|
1125
|
-
saveState(state, params.walletId, env);
|
|
1126
|
-
return session;
|
|
1127
|
-
}
|
|
1128
|
-
function openUnlockSession(params) {
|
|
1129
|
-
const env = params.env ?? process.env;
|
|
1130
|
-
const state = loadState(params.scope.walletId, env);
|
|
1131
|
-
pruneState(state);
|
|
1132
|
-
const createdAt = new Date().toISOString();
|
|
1133
|
-
const expiresAt = resolveUnlockExpiresAt(env, params.ttlSeconds);
|
|
1134
|
-
const session = {
|
|
1135
|
-
id: randomBytes(12).toString("hex"),
|
|
1136
|
-
host: normalizeHost(params.host),
|
|
1137
|
-
walletId: params.scope.walletId,
|
|
1138
|
-
role: params.scope.role,
|
|
1139
|
-
chains: params.scope.chains,
|
|
1140
|
-
allowPrograms: params.scope.allowPrograms,
|
|
1141
|
-
solanaMaxPerTx: params.scope.solana.maxPerTx,
|
|
1142
|
-
solanaMaxDaily: params.scope.solana.maxDaily,
|
|
1143
|
-
createdAt,
|
|
1144
|
-
expiresAt,
|
|
1145
|
-
lastUsedAt: createdAt
|
|
1146
|
-
};
|
|
1147
|
-
for (const existing of state.unlockSessions) {
|
|
1148
|
-
if (existing.walletId === session.walletId) {
|
|
1149
|
-
removeUnlockMaterial(existing.id);
|
|
1150
|
-
}
|
|
1151
|
-
}
|
|
1152
|
-
state.unlockSessions = state.unlockSessions.filter((entry) => entry.walletId !== session.walletId);
|
|
1153
|
-
state.unlockSessions.push(session);
|
|
1154
|
-
saveState(state, params.scope.walletId, env);
|
|
1155
|
-
return session;
|
|
1156
|
-
}
|
|
1157
|
-
function clearLocalUnlockSessions(params) {
|
|
1158
|
-
const env = params?.env ?? process.env;
|
|
1159
|
-
const requestedHost = params?.host?.trim() ? normalizeHost(params.host) : "";
|
|
1160
|
-
const requestedWalletId = params?.walletId?.trim() || "";
|
|
1161
|
-
if (params?.host?.trim() && !requestedHost) {
|
|
1162
|
-
return {
|
|
1163
|
-
ok: false,
|
|
1164
|
-
code: "invalid_host",
|
|
1165
|
-
message: "host is invalid for custody lock"
|
|
1166
|
-
};
|
|
1167
|
-
}
|
|
1168
|
-
let removed = 0;
|
|
1169
|
-
let remaining = 0;
|
|
1170
|
-
const targets = requestedWalletId ? [requestedWalletId] : listCustodyStateTargets(env);
|
|
1171
|
-
for (const target of targets) {
|
|
1172
|
-
const state = loadState(target, env);
|
|
1173
|
-
const changedByPrune = pruneState(state);
|
|
1174
|
-
const before = state.unlockSessions.length;
|
|
1175
|
-
if (!requestedHost && !requestedWalletId) {
|
|
1176
|
-
for (const session of state.unlockSessions) {
|
|
1177
|
-
removeUnlockMaterial(session.id);
|
|
1178
|
-
}
|
|
1179
|
-
state.unlockSessions = [];
|
|
1180
|
-
} else {
|
|
1181
|
-
state.unlockSessions = state.unlockSessions.filter((session) => {
|
|
1182
|
-
if (requestedHost && session.host !== requestedHost) {
|
|
1183
|
-
return true;
|
|
1184
|
-
}
|
|
1185
|
-
if (requestedWalletId && session.walletId !== requestedWalletId) {
|
|
1186
|
-
return true;
|
|
1187
|
-
}
|
|
1188
|
-
removeUnlockMaterial(session.id);
|
|
1189
|
-
return false;
|
|
1190
|
-
});
|
|
1191
|
-
}
|
|
1192
|
-
removed += before - state.unlockSessions.length;
|
|
1193
|
-
remaining += state.unlockSessions.length;
|
|
1194
|
-
if (changedByPrune || before !== state.unlockSessions.length) {
|
|
1195
|
-
saveState(state, target, env);
|
|
1196
|
-
}
|
|
1197
|
-
}
|
|
1198
|
-
return {
|
|
1199
|
-
ok: true,
|
|
1200
|
-
host: requestedHost || undefined,
|
|
1201
|
-
walletId: requestedWalletId || undefined,
|
|
1202
|
-
removed,
|
|
1203
|
-
remaining
|
|
1204
|
-
};
|
|
1205
|
-
}
|
|
1206
|
-
function recoverWalletCustodyPassphrase(params) {
|
|
1207
|
-
const reconstructed = reconstructCustodySigningKey({
|
|
1208
|
-
env: params?.env ?? process.env,
|
|
1209
|
-
deviceShare: params?.deviceShare,
|
|
1210
|
-
recoveryShare: params?.recoveryShare,
|
|
1211
|
-
walletId: params?.walletId
|
|
1212
|
-
});
|
|
1213
|
-
if (!reconstructed.ok) {
|
|
1214
|
-
return reconstructed;
|
|
1215
|
-
}
|
|
1216
|
-
try {
|
|
1217
|
-
return {
|
|
1218
|
-
ok: true,
|
|
1219
|
-
passphrase: Buffer.from(reconstructed.signingKey).toString("base64url")
|
|
1220
|
-
};
|
|
1221
|
-
} finally {
|
|
1222
|
-
zeroizeBuffers([reconstructed.signingKey]);
|
|
1223
|
-
}
|
|
1224
|
-
}
|
|
1225
|
-
async function lockWalletCustodyUnlockSessions(params) {
|
|
1226
|
-
const env = params?.env ?? process.env;
|
|
1227
|
-
const localResult = clearLocalUnlockSessions({
|
|
1228
|
-
host: params?.host,
|
|
1229
|
-
walletId: params?.walletId,
|
|
1230
|
-
env
|
|
1231
|
-
});
|
|
1232
|
-
if (!localResult.ok) {
|
|
1233
|
-
return localResult;
|
|
1234
|
-
}
|
|
1235
|
-
const splitKeyActive = String(env.FASED_WALLET_CUSTODY_MODE ?? "").trim().toLowerCase() === "split-key" && readBooleanFlag(env, "FASED_WALLET_CUSTODY_PASSKEY_CEREMONY") && readBooleanFlag(env, "FASED_WALLET_CUSTODY_EPHEMERAL_RECONSTRUCTION") && readBooleanFlag(env, "FASED_WALLET_CUSTODY_PHASE2_COMPLETE");
|
|
1236
|
-
if (!splitKeyActive) {
|
|
1237
|
-
return localResult;
|
|
1238
|
-
}
|
|
1239
|
-
try {
|
|
1240
|
-
await lockLocalSignerCustody({
|
|
1241
|
-
host: localResult.host,
|
|
1242
|
-
walletId: localResult.walletId,
|
|
1243
|
-
env
|
|
1244
|
-
});
|
|
1245
|
-
return localResult;
|
|
1246
|
-
} catch (err) {
|
|
1247
|
-
return {
|
|
1248
|
-
ok: false,
|
|
1249
|
-
code: "custody_signer_lock_failed",
|
|
1250
|
-
message: walletDiagnosticErrorMessage(err),
|
|
1251
|
-
host: localResult.host,
|
|
1252
|
-
walletId: localResult.walletId,
|
|
1253
|
-
removed: localResult.removed,
|
|
1254
|
-
remaining: localResult.remaining
|
|
1255
|
-
};
|
|
1256
|
-
}
|
|
1257
|
-
}
|
|
1258
|
-
function reconstructCustodySigningKey(params) {
|
|
1259
|
-
const env = params.env ?? process.env;
|
|
1260
|
-
const shareState = loadShareState(params.walletId, env);
|
|
1261
|
-
if (!shareState) {
|
|
1262
|
-
return {
|
|
1263
|
-
ok: false,
|
|
1264
|
-
code: "custody_ceremony_required",
|
|
1265
|
-
message: "split-key custody ceremony is not initialized"
|
|
1266
|
-
};
|
|
1267
|
-
}
|
|
1268
|
-
let hot = null;
|
|
1269
|
-
let cold = null;
|
|
1270
|
-
let host = null;
|
|
1271
|
-
let providedShare = null;
|
|
1272
|
-
let secret = null;
|
|
1273
|
-
try {
|
|
1274
|
-
if (shareState.scheme === "xor-3of3-v1") {
|
|
1275
|
-
hot = decodeBase64Share(shareState.hotShare);
|
|
1276
|
-
cold = decodeBase64Share(shareState.coldShare);
|
|
1277
|
-
providedShare = decodeTypedCustodyShare(params.deviceShare?.trim() || String(env.FASED_WALLET_CUSTODY_DEVICE_SHARE ?? "").trim()).bytes;
|
|
1278
|
-
if (!hot || !cold || hot.length !== shareState.secretBytes || cold.length !== shareState.secretBytes) {
|
|
1279
|
-
return {
|
|
1280
|
-
ok: false,
|
|
1281
|
-
code: "custody_shares_invalid",
|
|
1282
|
-
message: "stored split-key share payload is invalid"
|
|
1283
|
-
};
|
|
1284
|
-
}
|
|
1285
|
-
if (!providedShare || providedShare.length !== shareState.secretBytes) {
|
|
1286
|
-
return {
|
|
1287
|
-
ok: false,
|
|
1288
|
-
code: "custody_device_share_required",
|
|
1289
|
-
message: "device share is required to unlock split-key custody"
|
|
1290
|
-
};
|
|
1291
|
-
}
|
|
1292
|
-
const deviceHash = sha256Hex(providedShare);
|
|
1293
|
-
if (!buffersEqualHex(deviceHash, shareState.deviceShareHash)) {
|
|
1294
|
-
return {
|
|
1295
|
-
ok: false,
|
|
1296
|
-
code: "custody_device_share_mismatch",
|
|
1297
|
-
message: "device share does not match split-key ceremony state"
|
|
1298
|
-
};
|
|
1299
|
-
}
|
|
1300
|
-
secret = xorBuffers([
|
|
1301
|
-
hot,
|
|
1302
|
-
cold,
|
|
1303
|
-
providedShare
|
|
1304
|
-
]);
|
|
1305
|
-
} else {
|
|
1306
|
-
host = decodeBase64Share(shareState.hostShare);
|
|
1307
|
-
const provided = decodeTypedCustodyShare(params.deviceShare?.trim() || params.recoveryShare?.trim() || String(env.FASED_WALLET_CUSTODY_DEVICE_SHARE ?? "").trim());
|
|
1308
|
-
providedShare = provided.bytes;
|
|
1309
|
-
if (!host || host.length !== shareState.secretBytes) {
|
|
1310
|
-
return {
|
|
1311
|
-
ok: false,
|
|
1312
|
-
code: "custody_shares_invalid",
|
|
1313
|
-
message: "stored split-key share payload is invalid"
|
|
1314
|
-
};
|
|
1315
|
-
}
|
|
1316
|
-
if (!providedShare || providedShare.length !== shareState.secretBytes) {
|
|
1317
|
-
return {
|
|
1318
|
-
ok: false,
|
|
1319
|
-
code: "custody_device_share_required",
|
|
1320
|
-
message: "device share or recovery share is required to unlock split-key custody"
|
|
1321
|
-
};
|
|
1322
|
-
}
|
|
1323
|
-
const providedHash = sha256Hex(providedShare);
|
|
1324
|
-
const activeDevices = listActiveEnrolledDevices(shareState);
|
|
1325
|
-
const matchingDevice = shareState.version === 3 ? activeDevices.find((device) => buffersEqualHex(providedHash, device.shareHash)) ?? null : buffersEqualHex(providedHash, shareState.deviceShareHash) ? activeDevices[0] ?? null : null;
|
|
1326
|
-
const matchesRecoveryShare = buffersEqualHex(providedHash, shareState.recoveryShareHash);
|
|
1327
|
-
const shareType = matchesRecoveryShare ? "recovery" : matchingDevice ? "device" : null;
|
|
1328
|
-
if (!shareType) {
|
|
1329
|
-
return {
|
|
1330
|
-
ok: false,
|
|
1331
|
-
code: "custody_device_share_mismatch",
|
|
1332
|
-
message: "provided custody share does not match device or recovery share state"
|
|
1333
|
-
};
|
|
1334
|
-
}
|
|
1335
|
-
if (provided.type !== "unknown" && provided.type !== shareType) {
|
|
1336
|
-
return {
|
|
1337
|
-
ok: false,
|
|
1338
|
-
code: "custody_device_share_mismatch",
|
|
1339
|
-
message: "provided custody share type does not match stored ceremony state"
|
|
1340
|
-
};
|
|
1341
|
-
}
|
|
1342
|
-
secret = reconstructSecretShamir2ofN({
|
|
1343
|
-
hostShare: host,
|
|
1344
|
-
otherShare: providedShare,
|
|
1345
|
-
otherX: shareType === "recovery" ? 3 : matchingDevice?.shareX ?? 2
|
|
1346
|
-
});
|
|
1347
|
-
}
|
|
1348
|
-
if (!buffersEqualHex(sha256Hex(secret), shareState.secretChecksum)) {
|
|
1349
|
-
return {
|
|
1350
|
-
ok: false,
|
|
1351
|
-
code: "custody_share_integrity_failed",
|
|
1352
|
-
message: "split-key reconstruction failed integrity check"
|
|
1353
|
-
};
|
|
1354
|
-
}
|
|
1355
|
-
return {
|
|
1356
|
-
ok: true,
|
|
1357
|
-
signingKey: Buffer.from(secret)
|
|
1358
|
-
};
|
|
1359
|
-
} finally {
|
|
1360
|
-
zeroizeBuffers([
|
|
1361
|
-
hot,
|
|
1362
|
-
cold,
|
|
1363
|
-
host,
|
|
1364
|
-
providedShare,
|
|
1365
|
-
secret
|
|
1366
|
-
]);
|
|
1367
|
-
}
|
|
1368
|
-
}
|
|
1369
|
-
async function activateWalletCustodyUnlockSession(params) {
|
|
1370
|
-
const env = params.env ?? process.env;
|
|
1371
|
-
const host = normalizeHost(params.host);
|
|
1372
|
-
if (!host) {
|
|
1373
|
-
return {
|
|
1374
|
-
ok: false,
|
|
1375
|
-
code: "invalid_host",
|
|
1376
|
-
message: "host is required for custody unlock"
|
|
1377
|
-
};
|
|
1378
|
-
}
|
|
1379
|
-
const consumed = consumeWalletApprovalGrant({
|
|
1380
|
-
host,
|
|
1381
|
-
operation: "wallet.custody-unlock",
|
|
1382
|
-
token: params.approvalToken,
|
|
1383
|
-
env,
|
|
1384
|
-
cfg: params.cfg
|
|
1385
|
-
});
|
|
1386
|
-
if (!consumed.ok) {
|
|
1387
|
-
return consumed;
|
|
1388
|
-
}
|
|
1389
|
-
const scope = params.wallet && params.cfg ? resolveWalletCustodyScope({
|
|
1390
|
-
wallet: params.wallet,
|
|
1391
|
-
cfg: params.cfg,
|
|
1392
|
-
env,
|
|
1393
|
-
walletId: params.walletId
|
|
1394
|
-
}) : {
|
|
1395
|
-
walletId: params.walletId?.trim() || "default",
|
|
1396
|
-
role: "vault",
|
|
1397
|
-
chains: ["solana"],
|
|
1398
|
-
allowPrograms: [],
|
|
1399
|
-
solana: {
|
|
1400
|
-
maxPerTx: "0",
|
|
1401
|
-
maxDaily: "0"
|
|
1402
|
-
}
|
|
1403
|
-
};
|
|
1404
|
-
const status = params.wallet ? readWalletCustodyStatus({
|
|
1405
|
-
wallet: params.wallet,
|
|
1406
|
-
env,
|
|
1407
|
-
cfg: params.cfg,
|
|
1408
|
-
walletId: scope.walletId
|
|
1409
|
-
}) : null;
|
|
1410
|
-
let custodyPassphrase = "";
|
|
1411
|
-
if (status?.mode === "split-key-active") {
|
|
1412
|
-
const reconstructed = recoverWalletCustodyPassphrase({
|
|
1413
|
-
env,
|
|
1414
|
-
deviceShare: params.deviceShare,
|
|
1415
|
-
walletId: scope.walletId
|
|
1416
|
-
});
|
|
1417
|
-
if (!reconstructed.ok) {
|
|
1418
|
-
return reconstructed;
|
|
1419
|
-
}
|
|
1420
|
-
custodyPassphrase = reconstructed.passphrase;
|
|
1421
|
-
}
|
|
1422
|
-
const session = openUnlockSession({
|
|
1423
|
-
host,
|
|
1424
|
-
env,
|
|
1425
|
-
scope,
|
|
1426
|
-
ttlSeconds: params.ttlSeconds
|
|
1427
|
-
});
|
|
1428
|
-
if (custodyPassphrase) {
|
|
1429
|
-
try {
|
|
1430
|
-
await unlockLocalSignerCustody({
|
|
1431
|
-
sessionId: session.id,
|
|
1432
|
-
host: session.host,
|
|
1433
|
-
walletId: session.walletId,
|
|
1434
|
-
role: session.role,
|
|
1435
|
-
chains: session.chains,
|
|
1436
|
-
allowPrograms: session.allowPrograms,
|
|
1437
|
-
expiresAt: session.expiresAt,
|
|
1438
|
-
passphrase: custodyPassphrase,
|
|
1439
|
-
solanaMaxPerTx: session.solanaMaxPerTx,
|
|
1440
|
-
solanaMaxDaily: session.solanaMaxDaily,
|
|
1441
|
-
env
|
|
1442
|
-
});
|
|
1443
|
-
} catch (err) {
|
|
1444
|
-
clearLocalUnlockSessions({
|
|
1445
|
-
host: session.host,
|
|
1446
|
-
walletId: session.walletId,
|
|
1447
|
-
env
|
|
1448
|
-
});
|
|
1449
|
-
return {
|
|
1450
|
-
ok: false,
|
|
1451
|
-
code: "custody_signer_unlock_failed",
|
|
1452
|
-
message: walletDiagnosticErrorMessage(err)
|
|
1453
|
-
};
|
|
1454
|
-
}
|
|
1455
|
-
}
|
|
1456
|
-
setUnlockMaterial({
|
|
1457
|
-
sessionId: session.id,
|
|
1458
|
-
host: session.host,
|
|
1459
|
-
expiresAt: session.expiresAt
|
|
1460
|
-
});
|
|
1461
|
-
return {
|
|
1462
|
-
ok: true,
|
|
1463
|
-
session
|
|
1464
|
-
};
|
|
1465
|
-
}
|
|
1466
|
-
async function refreshWalletCustodyUnlockSession(params) {
|
|
1467
|
-
const env = params.env ?? process.env;
|
|
1468
|
-
const host = normalizeHost(params.host);
|
|
1469
|
-
if (!host) {
|
|
1470
|
-
return {
|
|
1471
|
-
ok: false,
|
|
1472
|
-
code: "invalid_host",
|
|
1473
|
-
message: "host is required for custody refresh"
|
|
1474
|
-
};
|
|
1475
|
-
}
|
|
1476
|
-
const scope = params.wallet && params.cfg ? resolveWalletCustodyScope({
|
|
1477
|
-
wallet: params.wallet,
|
|
1478
|
-
cfg: params.cfg,
|
|
1479
|
-
env,
|
|
1480
|
-
walletId: params.walletId
|
|
1481
|
-
}) : {
|
|
1482
|
-
walletId: params.walletId?.trim() || "default",
|
|
1483
|
-
role: "vault",
|
|
1484
|
-
chains: ["solana"],
|
|
1485
|
-
allowPrograms: [],
|
|
1486
|
-
solana: {
|
|
1487
|
-
maxPerTx: "0",
|
|
1488
|
-
maxDaily: "0"
|
|
1489
|
-
}
|
|
1490
|
-
};
|
|
1491
|
-
const active = getActiveUnlockSession({
|
|
1492
|
-
host,
|
|
1493
|
-
walletId: scope.walletId,
|
|
1494
|
-
env
|
|
1495
|
-
});
|
|
1496
|
-
if (!active) {
|
|
1497
|
-
return {
|
|
1498
|
-
ok: false,
|
|
1499
|
-
code: "custody_unlock_required",
|
|
1500
|
-
message: `split-key custody for wallet ${scope.walletId} is locked`
|
|
1501
|
-
};
|
|
1502
|
-
}
|
|
1503
|
-
const status = params.wallet ? readWalletCustodyStatus({
|
|
1504
|
-
wallet: params.wallet,
|
|
1505
|
-
env,
|
|
1506
|
-
cfg: params.cfg,
|
|
1507
|
-
walletId: scope.walletId
|
|
1508
|
-
}) : null;
|
|
1509
|
-
let custodyPassphrase = "";
|
|
1510
|
-
if (status?.mode === "split-key-active") {
|
|
1511
|
-
const reconstructed = recoverWalletCustodyPassphrase({
|
|
1512
|
-
env,
|
|
1513
|
-
deviceShare: params.deviceShare,
|
|
1514
|
-
walletId: scope.walletId
|
|
1515
|
-
});
|
|
1516
|
-
if (!reconstructed.ok) {
|
|
1517
|
-
return reconstructed;
|
|
1518
|
-
}
|
|
1519
|
-
custodyPassphrase = reconstructed.passphrase;
|
|
1520
|
-
}
|
|
1521
|
-
const session = openUnlockSession({
|
|
1522
|
-
host,
|
|
1523
|
-
env,
|
|
1524
|
-
scope,
|
|
1525
|
-
ttlSeconds: params.ttlSeconds
|
|
1526
|
-
});
|
|
1527
|
-
if (custodyPassphrase) {
|
|
1528
|
-
try {
|
|
1529
|
-
await unlockLocalSignerCustody({
|
|
1530
|
-
sessionId: session.id,
|
|
1531
|
-
host: session.host,
|
|
1532
|
-
walletId: session.walletId,
|
|
1533
|
-
role: session.role,
|
|
1534
|
-
chains: session.chains,
|
|
1535
|
-
allowPrograms: session.allowPrograms,
|
|
1536
|
-
expiresAt: session.expiresAt,
|
|
1537
|
-
passphrase: custodyPassphrase,
|
|
1538
|
-
solanaMaxPerTx: session.solanaMaxPerTx,
|
|
1539
|
-
solanaMaxDaily: session.solanaMaxDaily,
|
|
1540
|
-
env
|
|
1541
|
-
});
|
|
1542
|
-
} catch (err) {
|
|
1543
|
-
clearLocalUnlockSessions({
|
|
1544
|
-
host: session.host,
|
|
1545
|
-
walletId: session.walletId,
|
|
1546
|
-
env
|
|
1547
|
-
});
|
|
1548
|
-
return {
|
|
1549
|
-
ok: false,
|
|
1550
|
-
code: "custody_signer_unlock_failed",
|
|
1551
|
-
message: walletDiagnosticErrorMessage(err)
|
|
1552
|
-
};
|
|
1553
|
-
}
|
|
1554
|
-
}
|
|
1555
|
-
setUnlockMaterial({
|
|
1556
|
-
sessionId: session.id,
|
|
1557
|
-
host: session.host,
|
|
1558
|
-
expiresAt: session.expiresAt
|
|
1559
|
-
});
|
|
1560
|
-
return {
|
|
1561
|
-
ok: true,
|
|
1562
|
-
session
|
|
1563
|
-
};
|
|
1564
|
-
}
|
|
1565
|
-
async function enforceWalletCustodyForAutonomousSend(params) {
|
|
1566
|
-
const env = params.env ?? process.env;
|
|
1567
|
-
const scope = resolveWalletCustodyScope({
|
|
1568
|
-
wallet: params.wallet,
|
|
1569
|
-
cfg: params.cfg,
|
|
1570
|
-
env,
|
|
1571
|
-
walletId: params.walletId
|
|
1572
|
-
});
|
|
1573
|
-
const status = readWalletCustodyStatus({
|
|
1574
|
-
wallet: params.wallet,
|
|
1575
|
-
env,
|
|
1576
|
-
cfg: params.cfg,
|
|
1577
|
-
walletId: scope.walletId
|
|
1578
|
-
});
|
|
1579
|
-
if (status.mode === "single-key") {
|
|
1580
|
-
return {
|
|
1581
|
-
ok: true,
|
|
1582
|
-
custodyMode: status.mode
|
|
1583
|
-
};
|
|
1584
|
-
}
|
|
1585
|
-
if (status.mode !== "split-key-active") {
|
|
1586
|
-
return {
|
|
1587
|
-
ok: false,
|
|
1588
|
-
code: "custody_phase2_incomplete",
|
|
1589
|
-
message: "split-key custody is configured but phase-2 controls are incomplete; autonomous signing is blocked"
|
|
1590
|
-
};
|
|
1591
|
-
}
|
|
1592
|
-
const runtimeGuard = enforceSplitKeyRuntimeGuard({
|
|
1593
|
-
wallet: params.wallet,
|
|
1594
|
-
cfg: params.cfg,
|
|
1595
|
-
env
|
|
1596
|
-
});
|
|
1597
|
-
if (!runtimeGuard.ok) {
|
|
1598
|
-
return runtimeGuard;
|
|
1599
|
-
}
|
|
1600
|
-
if (resolveWalletApprovalAuthMode(env, params.cfg) !== "webauthn") {
|
|
1601
|
-
return {
|
|
1602
|
-
ok: false,
|
|
1603
|
-
code: "custody_webauthn_required",
|
|
1604
|
-
message: "split-key custody requires wallet approval auth mode=webauthn"
|
|
1605
|
-
};
|
|
1606
|
-
}
|
|
1607
|
-
const host = normalizeHost(params.approvalHost?.trim() || "127.0.0.1");
|
|
1608
|
-
const active = getActiveUnlockSession({
|
|
1609
|
-
host,
|
|
1610
|
-
walletId: scope.walletId,
|
|
1611
|
-
env
|
|
1612
|
-
});
|
|
1613
|
-
if (active) {
|
|
1614
|
-
touchUnlockSession({
|
|
1615
|
-
sessionId: active.id,
|
|
1616
|
-
walletId: scope.walletId,
|
|
1617
|
-
env
|
|
1618
|
-
});
|
|
1619
|
-
if (!hasActiveUnlockMaterial({
|
|
1620
|
-
sessionId: active.id,
|
|
1621
|
-
host
|
|
1622
|
-
})) {
|
|
1623
|
-
return {
|
|
1624
|
-
ok: false,
|
|
1625
|
-
code: "custody_unlock_required",
|
|
1626
|
-
message: "split-key unlock session exists without ephemeral signing material; unlock custody again"
|
|
1627
|
-
};
|
|
1628
|
-
}
|
|
1629
|
-
return {
|
|
1630
|
-
ok: true,
|
|
1631
|
-
custodyMode: status.mode,
|
|
1632
|
-
session: active
|
|
1633
|
-
};
|
|
1634
|
-
}
|
|
1635
|
-
const token = params.approvalToken?.trim() ?? "";
|
|
1636
|
-
if (!token) {
|
|
1637
|
-
return {
|
|
1638
|
-
ok: false,
|
|
1639
|
-
code: "custody_unlock_required",
|
|
1640
|
-
message: "split-key custody requires an active unlock session; provide a passkey approval token for operation wallet.custody-unlock"
|
|
1641
|
-
};
|
|
1642
|
-
}
|
|
1643
|
-
const activated = await activateWalletCustodyUnlockSession({
|
|
1644
|
-
host,
|
|
1645
|
-
approvalToken: token,
|
|
1646
|
-
env,
|
|
1647
|
-
cfg: params.cfg,
|
|
1648
|
-
wallet: params.wallet,
|
|
1649
|
-
walletId: scope.walletId,
|
|
1650
|
-
deviceShare: params.deviceShare
|
|
1651
|
-
});
|
|
1652
|
-
if (!activated.ok) {
|
|
1653
|
-
return activated;
|
|
1654
|
-
}
|
|
1655
|
-
return {
|
|
1656
|
-
ok: true,
|
|
1657
|
-
custodyMode: status.mode,
|
|
1658
|
-
session: activated.session
|
|
1659
|
-
};
|
|
1660
|
-
}
|
|
1661
|
-
function enforceSplitKeyRuntimeGuard(params) {
|
|
1662
|
-
const env = params.env ?? process.env;
|
|
1663
|
-
if (!params.cfg) {
|
|
1664
|
-
return {
|
|
1665
|
-
ok: false,
|
|
1666
|
-
code: "custody_config_required",
|
|
1667
|
-
message: "split-key custody requires runtime config to verify signer backend"
|
|
1668
|
-
};
|
|
1669
|
-
}
|
|
1670
|
-
const providerId = resolveWalletProviderId(params.cfg, env);
|
|
1671
|
-
if (providerId !== "local-socket-signer") {
|
|
1672
|
-
return {
|
|
1673
|
-
ok: false,
|
|
1674
|
-
code: "custody_provider_unsupported",
|
|
1675
|
-
message: "split-key custody requires local-socket-signer"
|
|
1676
|
-
};
|
|
1677
|
-
}
|
|
1678
|
-
if (!params.wallet.enabled) {
|
|
1679
|
-
return {
|
|
1680
|
-
ok: false,
|
|
1681
|
-
code: "custody_stack_unconfigured",
|
|
1682
|
-
message: "split-key custody requires wallet runtime to be enabled"
|
|
1683
|
-
};
|
|
1684
|
-
}
|
|
1685
|
-
return { ok: true };
|
|
1686
|
-
}
|
|
1687
|
-
function readWalletCustodyStatus(params) {
|
|
1688
|
-
const env = params.env ?? process.env;
|
|
1689
|
-
const scope = resolveWalletCustodyScope({
|
|
1690
|
-
wallet: params.wallet,
|
|
1691
|
-
cfg: params.cfg,
|
|
1692
|
-
env,
|
|
1693
|
-
walletId: params.walletId
|
|
1694
|
-
});
|
|
1695
|
-
const requestedSplitKey = String(env.FASED_WALLET_CUSTODY_MODE ?? "").trim().toLowerCase() === "split-key";
|
|
1696
|
-
const passkeyCeremonyEnabled = readBooleanFlag(env, "FASED_WALLET_CUSTODY_PASSKEY_CEREMONY");
|
|
1697
|
-
const ephemeralReconstructionEnabled = readBooleanFlag(env, "FASED_WALLET_CUSTODY_EPHEMERAL_RECONSTRUCTION");
|
|
1698
|
-
const phase2CompleteFlag = readBooleanFlag(env, "FASED_WALLET_CUSTODY_PHASE2_COMPLETE");
|
|
1699
|
-
const shareState = loadShareState(scope.walletId, env);
|
|
1700
|
-
const ceremonyInitialized = Boolean(shareState);
|
|
1701
|
-
const custodyWallets = readCustodyWalletSet(env);
|
|
1702
|
-
const roleAllowsSplitKey = scope.role === "vault";
|
|
1703
|
-
const splitKeyEnabled = requestedSplitKey && roleAllowsSplitKey && (custodyWallets.size === 0 ? ceremonyInitialized : custodyWallets.has(normalizeWalletIdForCustodyEnv(scope.walletId)));
|
|
1704
|
-
const splitKeyActive = splitKeyEnabled && phase2CompleteFlag && passkeyCeremonyEnabled && ephemeralReconstructionEnabled && ceremonyInitialized;
|
|
1705
|
-
const complete = splitKeyActive;
|
|
1706
|
-
const activeUnlock = getActiveUnlockSession({
|
|
1707
|
-
host: String(params.approvalHost ?? env.FASED_WALLET_CUSTODY_ACTIVE_HOST ?? env.FASED_A2A_ORIGIN ?? "127.0.0.1"),
|
|
1708
|
-
walletId: scope.walletId,
|
|
1709
|
-
env
|
|
1710
|
-
});
|
|
1711
|
-
const notes = [];
|
|
1712
|
-
if (splitKeyEnabled && !splitKeyActive) {
|
|
1713
|
-
notes.push("split-key custody is configured but full runtime prerequisites are not complete.");
|
|
1714
|
-
}
|
|
1715
|
-
if (!splitKeyEnabled) {
|
|
1716
|
-
notes.push("single-key custody active (phase-2 split-key disabled).");
|
|
1717
|
-
}
|
|
1718
|
-
if (splitKeyEnabled && !passkeyCeremonyEnabled) {
|
|
1719
|
-
notes.push("split-key mode enabled without passkey ceremony flag.");
|
|
1720
|
-
}
|
|
1721
|
-
if (splitKeyEnabled && !ephemeralReconstructionEnabled) {
|
|
1722
|
-
notes.push("split-key mode enabled without ephemeral reconstruction flag.");
|
|
1723
|
-
}
|
|
1724
|
-
if (splitKeyEnabled && !phase2CompleteFlag) {
|
|
1725
|
-
notes.push("split-key mode enabled without phase-2 completion flag.");
|
|
1726
|
-
}
|
|
1727
|
-
if (splitKeyEnabled && !ceremonyInitialized) {
|
|
1728
|
-
notes.push(`split-key ceremony is not initialized for wallet ${scope.walletId}; run \`fased wallet custody-init --wallet ${scope.walletId}\` to generate host/device/recovery shares.`);
|
|
1729
|
-
}
|
|
1730
|
-
if (splitKeyActive && activeUnlock) {
|
|
1731
|
-
notes.push(`split-key custody unlock session is active for wallet ${scope.walletId}.`);
|
|
1732
|
-
if (!hasActiveUnlockMaterial({
|
|
1733
|
-
sessionId: activeUnlock.id,
|
|
1734
|
-
host: activeUnlock.host
|
|
1735
|
-
})) {
|
|
1736
|
-
notes.push("unlock session exists but ephemeral signing material is missing; unlock again.");
|
|
1737
|
-
}
|
|
1738
|
-
}
|
|
1739
|
-
return {
|
|
1740
|
-
mode: splitKeyActive ? "split-key-active" : splitKeyEnabled ? "split-key-scaffold" : "single-key",
|
|
1741
|
-
target: {
|
|
1742
|
-
walletId: scope.walletId,
|
|
1743
|
-
role: scope.role
|
|
1744
|
-
},
|
|
1745
|
-
scope: {
|
|
1746
|
-
chains: scope.chains,
|
|
1747
|
-
allowPrograms: scope.allowPrograms,
|
|
1748
|
-
solana: scope.solana
|
|
1749
|
-
},
|
|
1750
|
-
unlock: {
|
|
1751
|
-
active: Boolean(activeUnlock),
|
|
1752
|
-
sessionId: activeUnlock?.id,
|
|
1753
|
-
host: activeUnlock?.host,
|
|
1754
|
-
expiresAt: activeUnlock?.expiresAt
|
|
1755
|
-
},
|
|
1756
|
-
phase2: {
|
|
1757
|
-
complete,
|
|
1758
|
-
splitKeyEnabled,
|
|
1759
|
-
passkeyCeremonyEnabled,
|
|
1760
|
-
ephemeralReconstructionEnabled,
|
|
1761
|
-
notes
|
|
1762
|
-
},
|
|
1763
|
-
ceremony: {
|
|
1764
|
-
initialized: ceremonyInitialized,
|
|
1765
|
-
scheme: shareState?.scheme,
|
|
1766
|
-
secretBytes: shareState?.secretBytes,
|
|
1767
|
-
devices: listActiveEnrolledDevices(shareState).map((device) => ({
|
|
1768
|
-
id: device.id,
|
|
1769
|
-
label: device.label,
|
|
1770
|
-
createdAt: device.createdAt,
|
|
1771
|
-
revokedAt: device.revokedAt
|
|
1772
|
-
})),
|
|
1773
|
-
updatedAt: shareState?.updatedAt,
|
|
1774
|
-
path: locateCustodyShareStatePath(scope.walletId, env)
|
|
1775
|
-
}
|
|
1776
|
-
};
|
|
1777
|
-
}
|
|
1778
|
-
function listSplitKeyWalletCustodyStatuses(params) {
|
|
1779
|
-
const env = params.env ?? process.env;
|
|
1780
|
-
const walletIds = new Set();
|
|
1781
|
-
const registry = readWalletProviderRegistry(env);
|
|
1782
|
-
const registryWalletIds = new Set(registry.wallets.map((wallet) => wallet.id.trim()).filter(Boolean));
|
|
1783
|
-
const normalizedRegistryWalletIds = new Set([...registryWalletIds].map((walletId) => normalizeWalletIdForCustodyEnv(walletId)));
|
|
1784
|
-
const hasNamedWalletRegistry = registry.wallets.length > 0;
|
|
1785
|
-
const configuredMiningWalletId = typeof params.cfg?.plugins?.entries?.["sat-mining"]?.config?.walletId === "string" ? params.cfg.plugins.entries["sat-mining"]?.config?.walletId.trim() : "";
|
|
1786
|
-
const custodyWallets = readCustodyWalletSet(env);
|
|
1787
|
-
for (const wallet of registry.wallets) {
|
|
1788
|
-
if (wallet.id.trim()) {
|
|
1789
|
-
walletIds.add(wallet.id.trim());
|
|
1790
|
-
}
|
|
1791
|
-
}
|
|
1792
|
-
if (registry.defaultWalletId?.trim()) {
|
|
1793
|
-
walletIds.add(registry.defaultWalletId.trim());
|
|
1794
|
-
}
|
|
1795
|
-
for (const raw of String(env.FASED_WALLET_CUSTODY_WALLETS ?? "").split(",")) {
|
|
1796
|
-
if (raw.trim()) {
|
|
1797
|
-
walletIds.add(raw.trim());
|
|
1798
|
-
}
|
|
1799
|
-
}
|
|
1800
|
-
for (const target of listCustodyStateTargets(env)) {
|
|
1801
|
-
walletIds.add(target);
|
|
1802
|
-
}
|
|
1803
|
-
if (walletIds.size === 0) {
|
|
1804
|
-
walletIds.add(undefined);
|
|
1805
|
-
}
|
|
1806
|
-
const seen = new Set();
|
|
1807
|
-
const out = [];
|
|
1808
|
-
for (const walletId of walletIds) {
|
|
1809
|
-
const walletIdValue = walletId?.trim() ?? "";
|
|
1810
|
-
const normalizedTarget = normalizeWalletIdForCustodyEnv(walletId);
|
|
1811
|
-
const shareState = loadShareState(walletId, env);
|
|
1812
|
-
const isCurrentWallet = walletId === undefined || registryWalletIds.has(walletIdValue) || normalizedRegistryWalletIds.has(normalizedTarget) || registry.defaultWalletId?.trim() === walletIdValue || configuredMiningWalletId === walletIdValue || normalizeWalletIdForCustodyEnv(configuredMiningWalletId) === normalizedTarget || normalizedTarget === "default" || normalizedTarget === "vault" || !hasNamedWalletRegistry && (custodyWallets.has(normalizedTarget) || shareState?.role !== undefined);
|
|
1813
|
-
if (!isCurrentWallet) {
|
|
1814
|
-
continue;
|
|
1815
|
-
}
|
|
1816
|
-
const status = readWalletCustodyStatus({
|
|
1817
|
-
wallet: params.wallet,
|
|
1818
|
-
cfg: params.cfg,
|
|
1819
|
-
env,
|
|
1820
|
-
walletId
|
|
1821
|
-
});
|
|
1822
|
-
if (status.mode === "single-key") {
|
|
1823
|
-
continue;
|
|
1824
|
-
}
|
|
1825
|
-
const key = normalizeWalletIdForState(status.target.walletId);
|
|
1826
|
-
if (seen.has(key)) {
|
|
1827
|
-
continue;
|
|
1828
|
-
}
|
|
1829
|
-
seen.add(key);
|
|
1830
|
-
out.push(status);
|
|
1831
|
-
}
|
|
1832
|
-
return out;
|
|
1833
|
-
}
|
|
1834
|
-
|
|
1835
|
-
//#endregion
|
|
1836
|
-
export { activateWalletCustodyUnlockSession, deleteWalletCustodyCeremony, enforceWalletCustodyForAutonomousSend, enrollWalletCustodyDevice, initializeWalletCustodyCeremony, listSplitKeyWalletCustodyStatuses, lockWalletCustodyUnlockSessions, readWalletCustodyStatus, recoverWalletCustodyCeremony, recoverWalletCustodyPassphrase, refreshWalletCustodyUnlockSession, revokeWalletCustodyDevice, withWalletCustodySigningMaterial };
|