@fangorn-network/westmarch 2026.10.29 → 2026.11.4-dev
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/README.md +5 -5
- package/docs/app-to-agent.md +61 -625
- package/package.json +12 -17
- package/site/dist/assets/__vite-browser-external-D2U8RZpp.js +1 -0
- package/site/dist/assets/embed.worker-CmS57Zti.js +1 -0
- package/site/dist/assets/index-Djs8f08m.js +8 -0
- package/site/dist/assets/rolldown-runtime-COiEfhsA.js +1 -0
- package/site/dist/assets/{transformers.web-BTyOEJ0C.js → transformers.web-CwR_IYQV.js} +1 -1
- package/site/dist/index.html +3 -4
- package/site/worker.js +115 -338
- package/src/agent/agent-card.js +33 -45
- package/src/agent/mcp.js +168 -371
- package/src/agent/tools.js +2 -1
- package/src/agent/view-tools.js +4 -56
- package/src/agent/x402.js +182 -369
- package/src/core/roles.js +6 -6
- package/src/core/shard.js +4 -4
- package/src/discover/apps.js +67 -81
- package/src/discover/directory.js +2 -2
- package/src/market/reactions.js +21 -15
- package/src/publish/devnet.js +69 -18
- package/src/publish/init.js +1 -1
- package/src/publish/ship.js +74 -275
- package/src/publish/view.js +52 -53
- package/template/README.md +9 -11
- package/template/package.json +1 -1
- package/plugin/scripts/fx.mjs +0 -266
- package/site/dist/assets/AccountNotFoundScreen-C4njXc69-DyVH4CEz.js +0 -1
- package/site/dist/assets/AddFundsSelectionScreen-RAoCn7ej-Dz1--SS9.js +0 -31
- package/site/dist/assets/Address-H8kD1d9O-BdFZmRwI.js +0 -17
- package/site/dist/assets/AffirmativeConsentScreen-BG0MJxku-CxaCAisL.js +0 -6
- package/site/dist/assets/AllowlistRejectionScreen-Cae6e1Od-JGk7vndN.js +0 -1
- package/site/dist/assets/ApiController-CTLbMCcH.js +0 -5
- package/site/dist/assets/ArrowsRightLeftIcon-B8g9adpz.js +0 -1
- package/site/dist/assets/AuthenticateWithWalletScreen-CU6hIcFT-CXD9_OP8.js +0 -1
- package/site/dist/assets/AwaitingEvmToSolBridgingScreen-D0e_sZMS-BtJCglvU.js +0 -1
- package/site/dist/assets/AwaitingExternalEthereumTransferScreen-ur5HL4R7-BgC2TsqH.js +0 -1
- package/site/dist/assets/AwaitingPasswordlessCodeScreen-DRHz76mM-B9OzLeEO.js +0 -103
- package/site/dist/assets/AwaitingSolToEvmBridgingScreen-CdLRrGD2-C7r5snUL.js +0 -1
- package/site/dist/assets/BridgeNetworkSelectionView-CFGjB2nO-BTV0NRio.js +0 -68
- package/site/dist/assets/CaptchaScreen-DbpvkIGx-7rSZIuM-.js +0 -1
- package/site/dist/assets/CheckCircleIcon-B1l17ioc.js +0 -1
- package/site/dist/assets/CheckCircleIcon-BGSI8nKf.js +0 -1
- package/site/dist/assets/Checkbox-D1EDeo41-BUfq0w99.js +0 -52
- package/site/dist/assets/ChevronDownIcon-B6uJvPHu.js +0 -1
- package/site/dist/assets/Chip-CZKIKt9K-r9Nk__0-.js +0 -26
- package/site/dist/assets/CoinbaseOnrampStatusScreen-BbyCJDXL-DWWNHxmR.js +0 -16
- package/site/dist/assets/ConnectEmailForm-B-VRE0MN-CRQwS0Xu.js +0 -3
- package/site/dist/assets/ConnectLedgerScreen-DWHABGot-CZcCt31O.js +0 -10
- package/site/dist/assets/ConnectOnlyLandingScreen-CZYx9Bmp-C5IOf-SQ.js +0 -1
- package/site/dist/assets/ConnectOnlyStatusScreen-DCRzjxHx-cwg74F3A.js +0 -1
- package/site/dist/assets/ConnectOrCreateScreen-CUUYUO07-D28oeK6b.js +0 -1
- package/site/dist/assets/ConnectPhoneForm-Dek5xn4C-EEdkKeTp.js +0 -95
- package/site/dist/assets/ConnectWalletView-DqI-PTvX-C2mEzukj.js +0 -74
- package/site/dist/assets/ConnectionStatusScreen-fUfROm9m-CH_7r8iV.js +0 -7
- package/site/dist/assets/ConnectionStatusScreen-fUfROm9m-Z33kis0B.js +0 -1
- package/site/dist/assets/ConnectorUtil-DH4NePk5.js +0 -21
- package/site/dist/assets/ConstantsUtil-DmLsRHAl.js +0 -198
- package/site/dist/assets/CopyToClipboard-i_OQSBJr-_G88JCVR.js +0 -39
- package/site/dist/assets/CopyableText-CQapvaMr-CGGfc_zM.js +0 -28
- package/site/dist/assets/CrossAppAuthScreen-CLw7ymZq-C9Okoc0V.js +0 -1
- package/site/dist/assets/CustomLandingScreenView-Cm_MBSyX-D2l6hdSl.js +0 -22
- package/site/dist/assets/DelegatedActionsConsentScreen-DYF2U4y5-Dda3KKqS.js +0 -1
- package/site/dist/assets/DelegatedActionsRevokeScreen-bPp6Uwyh-DG8kJPt1.js +0 -1
- package/site/dist/assets/DepositAddressAddressStepView-4vdkb55L-CcOUfn6f.js +0 -187
- package/site/dist/assets/DepositAddressScreen-DBLRs7aI-DeituDNh.js +0 -8
- package/site/dist/assets/DepositAddressV3Screen-CDqIEMQw-FDg6K14-.js +0 -1
- package/site/dist/assets/EmailInputForm-Cqo1mda8-BmbxvPHC.js +0 -124
- package/site/dist/assets/EmbeddedWalletConnectingScreen-D4T7DUzs-B05CQ9rx.js +0 -1
- package/site/dist/assets/EmbeddedWalletCreatedScreen-D7WhW5ve-Cxd1b9XH.js +0 -1
- package/site/dist/assets/EmbeddedWalletKeyExportScreen-CqV4NYQ3-CrJy0ViX.js +0 -23
- package/site/dist/assets/EmbeddedWalletOnAccountCreateScreen-BoKb3Q2S-Bakaa3zu.js +0 -1
- package/site/dist/assets/EmbeddedWalletPasswordUpdateScreen-D3rMlqS7-j78vKrcj.js +0 -1
- package/site/dist/assets/EmbeddedWalletPasswordUpdateSplashScreen-BPFkWJeE-D3dr7W9_.js +0 -1
- package/site/dist/assets/EnrollTotp-Dibew4dp-BXbYV8nI.js +0 -66
- package/site/dist/assets/EnvelopeIcon-DjPofnRI.js +0 -1
- package/site/dist/assets/ErrorBanner-BcpGRt0h-BhH6_V-9.js +0 -21
- package/site/dist/assets/ErrorMessage-D8VaAP5m-Bs7uX2a9.js +0 -8
- package/site/dist/assets/ErrorScreen-DipdTWzE-BgPRgnOb.js +0 -4
- package/site/dist/assets/ErrorScreen-DipdTWzE-CvxXC6pz.js +0 -1
- package/site/dist/assets/ExclamationCircleIcon-BE-WGrut.js +0 -1
- package/site/dist/assets/ExclamationTriangleIcon-Ca1Xlc_f.js +0 -1
- package/site/dist/assets/FarcasterConnectStatusScreen-51tjUxqq-De2JvZdV.js +0 -75
- package/site/dist/assets/FarcasterSignerStatusScreen-6x-Uh95G-bLp3hXoO.js +0 -28
- package/site/dist/assets/FiatOnrampScreen-BAhYeiTI-Jr6D6JiZ.js +0 -491
- package/site/dist/assets/FingerPrintIcon-DOCbS93y.js +0 -1
- package/site/dist/assets/FundSolWalletWithExternalSolanaWallet-WtLwYaOl-A_G5kfa1.js +0 -1
- package/site/dist/assets/FundWalletMethodHeader-BWNkokCT-LPv35wkD.js +0 -1
- package/site/dist/assets/FundWithBankDepositScreen-DajkHxBD-0hfGrfvf.js +0 -58
- package/site/dist/assets/FundingEditAmountScreen-CpvpiO4M-Dz0s_NWR.js +0 -1
- package/site/dist/assets/FundingMethodSelectionScreen-Db7vhRlE-ZGm-3-Io.js +0 -1
- package/site/dist/assets/GlobeAltIcon-BBRl2MMn.js +0 -1
- package/site/dist/assets/GooglePay-B53WnudL-BGiVpB8y.js +0 -1
- package/site/dist/assets/HCaptchaWrapper-CUi9LJsp-CHdfIeD_.js +0 -4
- package/site/dist/assets/InAppBrowserLoginNotPossible-BMR9br1T-Cgoiow3m.js +0 -1
- package/site/dist/assets/InfoBanner-Cb3p1z12-_YqQmRiu.js +0 -22
- package/site/dist/assets/InjectedWalletIcon-DLcYOGDj-tuIrKQHH.js +0 -1
- package/site/dist/assets/InstallWalletScreen-DTA8dwLv-BMU_wa61.js +0 -1
- package/site/dist/assets/JsonTree-BHzNC-ic-DcVr0YF0.js +0 -34
- package/site/dist/assets/LabelXs-oqZNqbm_-CNprGVQM.js +0 -6
- package/site/dist/assets/LandingScreen-CGWUoSUG-fH6rsQen.js +0 -1
- package/site/dist/assets/Layouts-BMRfo5hw-e-sepi6F.js +0 -77
- package/site/dist/assets/Link-BdDilT2T-BdwlRJch.js +0 -40
- package/site/dist/assets/LinkConflictScreen-C3ZEPsgl-BXKN2Qm5.js +0 -77
- package/site/dist/assets/LinkEmailScreen-dKo9frwc-Cyn7Q8p1.js +0 -1
- package/site/dist/assets/LinkPasskeyScreen-zbrl_AI_-CWiqnwLM.js +0 -117
- package/site/dist/assets/LinkPasskeyScreen-zbrl_AI_-YmDevJzf.js +0 -1
- package/site/dist/assets/LinkPhoneScreen-oIUpfj0w-h7shW-Ay.js +0 -1
- package/site/dist/assets/Loader-UAZqxsnw-DJEVblm1.js +0 -62
- package/site/dist/assets/LoadingSkeleton-BMsgO5PV-D2atjsP5.js +0 -17
- package/site/dist/assets/LockClosedIcon-9EFzWK1C.js +0 -1
- package/site/dist/assets/LoginFailedScreen-CN0dJV6U-By3Si77a.js +0 -1
- package/site/dist/assets/ManualTransferScreen-CHqcuqbs-BAHPY8rs.js +0 -1
- package/site/dist/assets/MfaAuthEnrollmentFlowScreen-Dj7gPTRC-CLjffFG1.js +0 -1
- package/site/dist/assets/MfaAuthVerifyFlowScreen-DbFpyWMy-BmQg1Y61.js +0 -1
- package/site/dist/assets/MfaEnrollmentFlowScreen-DhLnGgnr-DmBtTtlw.js +0 -1
- package/site/dist/assets/MfaVerifyFlowScreen-C_XPjMRP-4a683Bmf.js +0 -1
- package/site/dist/assets/ModalFooter-BCuEK_nz-C_71qsaz.js +0 -403
- package/site/dist/assets/MoonpayStatusScreen-CzTUtokc-DEHHcEf8.js +0 -16
- package/site/dist/assets/OAuthStatusScreen-ufbOa-da-BjAb_WsY.js +0 -1
- package/site/dist/assets/OpenLink-CUpJ1mOr-BLWFsPOW.js +0 -39
- package/site/dist/assets/PasskeySelectSignupOrLogin-xh2ScC_l-2PJTJxX-.js +0 -1
- package/site/dist/assets/PasskeyStatusScreen-Bla8MaOk-DCEg761l.js +0 -6
- package/site/dist/assets/PasswordRecoveryScreen-awGXZkQk-8nVjNc8k.js +0 -20
- package/site/dist/assets/PhArrowCircleDown-D97G-BOA.js +0 -14
- package/site/dist/assets/PhArrowClockwise-DVgyFuez.js +0 -14
- package/site/dist/assets/PhArrowDown-87b1BizM.js +0 -14
- package/site/dist/assets/PhArrowLeft-Da0MfnbK.js +0 -14
- package/site/dist/assets/PhArrowRight-AdrIL4DW.js +0 -14
- package/site/dist/assets/PhArrowSquareOut-Bk2CMG3I.js +0 -14
- package/site/dist/assets/PhArrowUp-4sgyKPGx.js +0 -14
- package/site/dist/assets/PhArrowUpRight-BNghD2zq.js +0 -14
- package/site/dist/assets/PhArrowsClockwise-BXRn1GEl.js +0 -14
- package/site/dist/assets/PhArrowsDownUp-DM09Jo_3.js +0 -14
- package/site/dist/assets/PhArrowsLeftRight-MuKBLUoF.js +0 -14
- package/site/dist/assets/PhBank-fHbgCqqZ.js +0 -14
- package/site/dist/assets/PhBrowser-2-9m-AkM.js +0 -14
- package/site/dist/assets/PhCaretDown-BDNabFuL.js +0 -14
- package/site/dist/assets/PhCaretLeft-CI460R-A.js +0 -14
- package/site/dist/assets/PhCaretRight-D7ZS3Ls8.js +0 -14
- package/site/dist/assets/PhCaretUp-C-iIfyvO.js +0 -14
- package/site/dist/assets/PhCheck-DxQXn57r.js +0 -14
- package/site/dist/assets/PhCircleHalf-MJZeRBGF.js +0 -14
- package/site/dist/assets/PhClock-x3MS3Mwi.js +0 -14
- package/site/dist/assets/PhCompass-hYn3HhGN.js +0 -14
- package/site/dist/assets/PhCopy-CWNYcABI.js +0 -14
- package/site/dist/assets/PhCreditCard-Fy4pzwtt.js +0 -14
- package/site/dist/assets/PhCurrencyDollar-BgiaenCi.js +0 -14
- package/site/dist/assets/PhDesktop-CxrzCSv5.js +0 -14
- package/site/dist/assets/PhDeviceMobile-CsyA5Goi.js +0 -14
- package/site/dist/assets/PhDotsThree-D-k1BiYg.js +0 -14
- package/site/dist/assets/PhEnvelope-D8kzeNox.js +0 -14
- package/site/dist/assets/PhFunnelSimple-CfQs5DAW.js +0 -14
- package/site/dist/assets/PhGlobe-DvmSc9_A.js +0 -14
- package/site/dist/assets/PhIdentificationCard-CfkZm-5K.js +0 -14
- package/site/dist/assets/PhImage-Cf0J2ZfJ.js +0 -14
- package/site/dist/assets/PhInfo-Bwi51-2X.js +0 -14
- package/site/dist/assets/PhLightbulb-C4FZV7Es.js +0 -14
- package/site/dist/assets/PhMagnifyingGlass-C9u8jSCJ.js +0 -14
- package/site/dist/assets/PhPaperPlaneRight-zxQVvPLX.js +0 -14
- package/site/dist/assets/PhPlus-C4UoW-2n.js +0 -14
- package/site/dist/assets/PhPower-CFyWDmgD.js +0 -14
- package/site/dist/assets/PhPuzzlePiece-BjoNVfST.js +0 -14
- package/site/dist/assets/PhQrCode-CP3tFauY.js +0 -14
- package/site/dist/assets/PhQuestion-wOWJBd3G.js +0 -14
- package/site/dist/assets/PhQuestionMark-CV0k9C0Y.js +0 -14
- package/site/dist/assets/PhSealCheck-vIKzREuN.js +0 -14
- package/site/dist/assets/PhSignOut-CxELV-i0.js +0 -14
- package/site/dist/assets/PhSpinner-CGDmbzUd.js +0 -14
- package/site/dist/assets/PhTrash-C9PD0SAt.js +0 -14
- package/site/dist/assets/PhUser-0sWNXQeZ.js +0 -14
- package/site/dist/assets/PhVault-6JTskUcC.js +0 -14
- package/site/dist/assets/PhWarning-CoKs3gz6.js +0 -14
- package/site/dist/assets/PhWarningCircle-BXHw1fCt.js +0 -14
- package/site/dist/assets/PhX-DX0SFIqC.js +0 -14
- package/site/dist/assets/PhoneIcon-CZ47md80.js +0 -1
- package/site/dist/assets/PinInput-DbZ0b1i1-BrTXu3Km.js +0 -247
- package/site/dist/assets/PrivyPluginContext-DA-yS5vO-CY60yubk.js +0 -1
- package/site/dist/assets/QrCode-DbnZwlkH-CcMrWV5D.js +0 -22
- package/site/dist/assets/RecoveryOAuthStatusScreen-CsgtTI2x-DgeDnHOK.js +0 -1
- package/site/dist/assets/RecoveryPasswordCreateScreen-Be-qJ-LU-CvJJwLhK.js +0 -1
- package/site/dist/assets/RecoverySelectionScreen-D1iGtpmi-CADtGtSV.js +0 -7
- package/site/dist/assets/SIWXUtil-Cpiv91-M.js +0 -1
- package/site/dist/assets/Screen-D30meWsY-vEusJ7KX.js +0 -175
- package/site/dist/assets/ScreenHeader-CHmc4-Lu-BvGTLg53.js +0 -31
- package/site/dist/assets/ScreenLayout-OwaQQcxH-cfOTmyl0.js +0 -1
- package/site/dist/assets/SelectSourceAsset-CWFog6gm-DxYklZYs.js +0 -140
- package/site/dist/assets/SetAutomaticRecoveryScreen-BO_OEqAZ-oeqeo05x.js +0 -14
- package/site/dist/assets/SetWalletPasswordForm-DwCTVtAh-BxFee9UA.js +0 -11
- package/site/dist/assets/ShieldCheckIcon-eFjsosCA.js +0 -1
- package/site/dist/assets/SignRequestScreen-BHKv32KR-z8ixfIma.js +0 -26
- package/site/dist/assets/SolanaWalletConnector-C7Lqk-Wz-Bc_lvdiF.js +0 -1
- package/site/dist/assets/StackedContainer-B2vaEl56-Do5dXB8-.js +0 -24
- package/site/dist/assets/StandardSignAndSendTransactionScreen-CfhIXVtd-DudetZAc.js +0 -20
- package/site/dist/assets/Subtitle-CV-2yKE4-DV0o1av3.js +0 -13
- package/site/dist/assets/TelegramAuthScreen-UL9zLfO8-Bo3sONvf.js +0 -1
- package/site/dist/assets/Title-BnzYV3Is-C3qOPnO_.js +0 -7
- package/site/dist/assets/TodoList-DnyULl18-BvxvubbF.js +0 -36
- package/site/dist/assets/TransactionDetails-e9nesl30-WWflkOIi.js +0 -109
- package/site/dist/assets/TransactionErrorView-BDBNqA_f-CotYKcgf.js +0 -178
- package/site/dist/assets/TransferFromWalletScreen-Qvn1Yuno-sVFlWqa8.js +0 -31
- package/site/dist/assets/TransferOrBridgeLoadingScreen-Dl1I_iIz-CV2-mY19.js +0 -6
- package/site/dist/assets/TurnstileWrapper-B7sNddys-oPbuMTQt.js +0 -1
- package/site/dist/assets/UpdateEmailScreen-DVOhPV0H-D_mjSpAy.js +0 -1
- package/site/dist/assets/UpdatePhoneScreen-CXRswTNZ-6RLG0ahv.js +0 -1
- package/site/dist/assets/UserLimitReachedScreen-5we2DHn8-CaVN0bti.js +0 -1
- package/site/dist/assets/Value-DTgR824E-Bp6WGZZm.js +0 -27
- package/site/dist/assets/WalletCards-DH1rqayz-D3cOTb9x.js +0 -1
- package/site/dist/assets/WalletIcon-DOIZhb2l.js +0 -1
- package/site/dist/assets/WalletInfoCard-sq_2wgFg-bOqfl5MA.js +0 -29
- package/site/dist/assets/WalletInterstitialScreen-Sh7LfsAh-DWaVkMCA.js +0 -1
- package/site/dist/assets/WalletLink-DK5LB5Ha-DjUZvqs-.js +0 -20
- package/site/dist/assets/WalletOverflowButton-bfCnp-9T-B6P7DQXA.js +0 -1
- package/site/dist/assets/WarningBanner-ZZqCEtZK-Bc2YKZb7.js +0 -21
- package/site/dist/assets/__vite-browser-external-NciY4Vu1.js +0 -1
- package/site/dist/assets/_esm-BZP3cXTv.js +0 -1
- package/site/dist/assets/_esm-ZmzBJpEs.js +0 -6
- package/site/dist/assets/accounts-D4AXF17G.js +0 -1
- package/site/dist/assets/accounts-IkdFx9nq.js +0 -1
- package/site/dist/assets/analytics-mkkvFRju-BRBcadka.js +0 -1
- package/site/dist/assets/baseSepolia-Bn1j1Ysw.js +0 -1
- package/site/dist/assets/basic-Cz1YiCSk.js +0 -1575
- package/site/dist/assets/capitalizeFirstLetter-DmLYqXsO-DtOXx-2n.js +0 -1
- package/site/dist/assets/ccip-B1J3oTZx.js +0 -7
- package/site/dist/assets/ccip-Bd7261zy.js +0 -1
- package/site/dist/assets/ccip-D6EHKpjI.js +0 -1
- package/site/dist/assets/chain-scefkYIH.js +0 -5
- package/site/dist/assets/check-DHnllI8N.js +0 -1
- package/site/dist/assets/chevron-down-1y-r4bGO.js +0 -1
- package/site/dist/assets/circle-alert-OwxamXUX.js +0 -1
- package/site/dist/assets/circle-check-big-3sZGVC10.js +0 -1
- package/site/dist/assets/circle-x-DQjNUV9J.js +0 -1
- package/site/dist/assets/client-B-BpRtwY.js +0 -1
- package/site/dist/assets/client-BfIR1FvH.js +0 -9
- package/site/dist/assets/coinbase-DCPC2L_R-YEuIn3jh.js +0 -3
- package/site/dist/assets/copy-DMOX_yn7.js +0 -1
- package/site/dist/assets/core-GIEmxmAa.js +0 -12
- package/site/dist/assets/createLucideIcon-BsERoFTA.js +0 -1
- package/site/dist/assets/defineChain-B-SQDLTG.js +0 -1
- package/site/dist/assets/dijkstra-CY95afk4.js +0 -1
- package/site/dist/assets/dist-CoS6E-3x.js +0 -1
- package/site/dist/assets/dist-Cp6IXeJd.js +0 -1
- package/site/dist/assets/dist-CrNxmdZj.js +0 -1
- package/site/dist/assets/dist-DYGtf4F0.js +0 -16
- package/site/dist/assets/embed.worker-Cnl4VGr3.js +0 -1
- package/site/dist/assets/errors-BQ3N-RVt-CZPAWCgC.js +0 -1
- package/site/dist/assets/esm-C3DslVJx.js +0 -1
- package/site/dist/assets/esm-Ci1ogKuK.js +0 -1
- package/site/dist/assets/esm-Co94ENUV.js +0 -37
- package/site/dist/assets/esm-bb8SANZh.js +0 -1
- package/site/dist/assets/esm-n2lWo7kz.js +0 -1
- package/site/dist/assets/ethers-zTjbodk5-Cs648CnD.js +0 -1
- package/site/dist/assets/eventemitter3-Dzc_0Uq2.js +0 -1
- package/site/dist/assets/external-link-BBPG3rG7.js +0 -1
- package/site/dist/assets/farcaster-DPlSjvF5-CzBCmgyL.js +0 -1
- package/site/dist/assets/features-CuiHbgEJ.js +0 -2
- package/site/dist/assets/fingerprint-pattern-CjLtyIxB.js +0 -1
- package/site/dist/assets/floating-ui.react-DmaCfayr.js +0 -1
- package/site/dist/assets/floating-ui.react-dom-CaoMF8mP.js +0 -1
- package/site/dist/assets/formatErc20TokenAmount-BuPk9xcy-BVqOKQif.js +0 -1
- package/site/dist/assets/formatter-CVbp4SlJ.js +0 -1
- package/site/dist/assets/get-entropy-details-for-user-dHRH5Gpi-BNiOFXbK.js +0 -1
- package/site/dist/assets/getChainName-DjpPdUSc-DUyKDrnp.js +0 -1
- package/site/dist/assets/getErc20Balance-jXNpFf0N-q4bV8kQq.js +0 -1
- package/site/dist/assets/getErc20TokenInfo-BNTrzLMP-DcnTFrXN.js +0 -1
- package/site/dist/assets/getFormattedUsdFromLamports-De3U9GlO-Gx4_wxON.js +0 -1
- package/site/dist/assets/getUsdcMintAddress-BVgm9GRo-r2UnDeEy.js +0 -1
- package/site/dist/assets/guards-BIW-GiMJ-D3tWTQAR.js +0 -1
- package/site/dist/assets/hooks.module-BYZGfwDl.js +0 -1
- package/site/dist/assets/hourglass-D_GfQdJr.js +0 -1
- package/site/dist/assets/id-UqEwL2BI-CTlvRMA5.js +0 -1
- package/site/dist/assets/index-BdEw5wyl-kLbhqLbv.js +0 -1
- package/site/dist/assets/index-C2h3EneX.js +0 -8
- package/site/dist/assets/index-CWARkn2w-DxuckIFn.js +0 -61
- package/site/dist/assets/index-DvMddJcd-CNBb4VUn.js +0 -12
- package/site/dist/assets/index-YiCw0Y_x-Czl0tWfF.js +0 -436
- package/site/dist/assets/index.browser-B-i-Lj3J.js +0 -1
- package/site/dist/assets/index.browser-BMmUcykv.js +0 -1
- package/site/dist/assets/index.es-Crd0TyUS.js +0 -2
- package/site/dist/assets/index.es-DQbup1WG.js +0 -3
- package/site/dist/assets/isPaymentRequestAvailable-Bq1cemEn-Cjf3l05I.js +0 -1
- package/site/dist/assets/jsx-runtime-CndCZlyt.js +0 -1
- package/site/dist/assets/landmark-BZPqMFEf.js +0 -1
- package/site/dist/assets/lib-DGtzi2DK.js +0 -1
- package/site/dist/assets/localBatchGatewayRequest-B71J8sQ9.js +0 -29
- package/site/dist/assets/lock-DyN_c-ud.js +0 -1
- package/site/dist/assets/logger-CsXm3oIv-B0UYpNFI.js +0 -1
- package/site/dist/assets/mail-V_mcPb64.js +0 -1
- package/site/dist/assets/modal-context-CwmSeFcv-DPWNJrfK.js +0 -1
- package/site/dist/assets/native-BKj27wfc.js +0 -1
- package/site/dist/assets/ofetch.CWycOUEr-Duby3iHT.js +0 -1
- package/site/dist/assets/parseAbi-Csg-8t59.js +0 -2
- package/site/dist/assets/parseAbiParameters-BWbIMYe4.js +0 -1
- package/site/dist/assets/parseEther-CHS0GNkM.js +0 -1
- package/site/dist/assets/pkce-DXF0yKjP-7_VY-d83.js +0 -1
- package/site/dist/assets/preload-helper-CRzLMJPS.js +0 -1
- package/site/dist/assets/privy-context-81KmsBw2-DiR6-Yq8.js +0 -2
- package/site/dist/assets/property-DuZ9DLP4.js +0 -3
- package/site/dist/assets/react-BfYA1q0G.js +0 -1
- package/site/dist/assets/react-CK2HRI1t.js +0 -1
- package/site/dist/assets/react-DxGeY8B8.js +0 -1
- package/site/dist/assets/react-auth-sHnipzbS.js +0 -1
- package/site/dist/assets/react-dom-BI2Koe4o.js +0 -1
- package/site/dist/assets/reservoir-x-nGuZkT-BguVFH3D.js +0 -1
- package/site/dist/assets/rolldown-runtime-Bjp52BOI.js +0 -1
- package/site/dist/assets/safe-url-D7SRPu33-CkMedGro.js +0 -1
- package/site/dist/assets/secp256k1-BeYL8aZl.js +0 -1
- package/site/dist/assets/secp256k1-Bh4MSkhz.js +0 -1
- package/site/dist/assets/secp256k1-Ci4cqHl0.js +0 -1
- package/site/dist/assets/secp256k1-DsxXqbXv.js +0 -1
- package/site/dist/assets/secp256k1-N9RUmafO.js +0 -1
- package/site/dist/assets/serializeAuthorizationList-DGMik9Wx.js +0 -1
- package/site/dist/assets/serializeSignature-CWKpxoab.js +0 -2
- package/site/dist/assets/sha2-C6s7zZmb.js +0 -1
- package/site/dist/assets/shared-DRZUmnL5-BewE5wK8.js +0 -193
- package/site/dist/assets/shared-FM0rljBt-CxLACri_.js +0 -21
- package/site/dist/assets/shouldProceedtoEmbeddedWalletCreationFlow-B3vyavrh-BkAKsRNi.js +0 -1
- package/site/dist/assets/simulateTransaction-CfuOrae4-PjA_tGjX.js +0 -1
- package/site/dist/assets/smartphone-3p8nFSTE.js +0 -1
- package/site/dist/assets/src-BBjkfi-Q.js +0 -1
- package/site/dist/assets/storage-Ufl800Ne-1qCR2JMO.js +0 -8
- package/site/dist/assets/stringify-DqUj9VzO.js +0 -20
- package/site/dist/assets/stripe.esm-DEi2D291.js +0 -2
- package/site/dist/assets/styles-BsotlekN-Aq27nZGc.js +0 -9
- package/site/dist/assets/styles-Cm9H63WO-p_ic_dxE.js +0 -92
- package/site/dist/assets/styles-DVyDvTdj-CjsWqAld.js +0 -74
- package/site/dist/assets/styles-hslXy6xG-D5QHIrJU.js +0 -164
- package/site/dist/assets/telegram-B-JqnkqZ-Dqkm4rDn.js +0 -1
- package/site/dist/assets/to-ui-error-DWe-qwS3-Bi1HUQI9.js +0 -4
- package/site/dist/assets/toViemAccount-B5iDGq1b-CdFZwv07.js +0 -2
- package/site/dist/assets/transaction-BNTP-bFm-M5tJx8vY.js +0 -1
- package/site/dist/assets/transfer-6YztDh-t-BVQkTvG-.js +0 -1
- package/site/dist/assets/triangle-alert-BG3Qw-Hq.js +0 -1
- package/site/dist/assets/twitch-5IOe4sIQ-CaXHFlAu.js +0 -1
- package/site/dist/assets/types-4CuOD23i.js +0 -1
- package/site/dist/assets/use-headless-crypto-deposit-Yn-QRLB3-CQqeyhGY.js +0 -1
- package/site/dist/assets/use-unlink-wallet-BKK4_5Ur-C0xQN_Lp.js +0 -1
- package/site/dist/assets/useActiveWallet-B-yZYGc6-BkK6rFBm.js +0 -123
- package/site/dist/assets/useGetSolPrice-x7gfUIHJ-CLVXqv6I.js +0 -1
- package/site/dist/assets/useGetTokenPrice-CaoUkb79-DAXXs3rs.js +0 -1
- package/site/dist/assets/useI18n-C-5n_noF-CGpkgIVy.js +0 -1
- package/site/dist/assets/useWalletBalance-CZnP5f_f-Cfd64m4n.js +0 -1
- package/site/dist/assets/useWallets-X1ndvJ_U-CAYqg7Za.js +0 -1
- package/site/dist/assets/utils-BkR7EQjg.js +0 -1
- package/site/dist/assets/utils-DJhBNOZA.js +0 -1
- package/site/dist/assets/utils-DdckVfQr.js +0 -1
- package/site/dist/assets/w3m-modal-B4H_uiPH.js +0 -1174
- package/site/dist/assets/wallet-BNI12IYk.js +0 -1
- package/site/dist/assets/wallet-CITSaPlB.js +0 -2
- package/site/dist/assets/wallet-connect-BvJtUsDt-CVlSNIXP.js +0 -1
- package/site/dist/assets/walletconnect-registry-xxG8XLCb-BpzpBq5s.js +0 -1
- package/site/dist/assets/walletconnect-v2-3frQFwbg-CzZ6oybX.js +0 -12
- package/site/dist/assets/wui-ux-by-reown-XyHdtV1O.js +0 -1147
- package/site/dist/assets/x-gjDrY2rk.js +0 -1
- package/src/discover/explore.js +0 -291
- package/src/market/settle.js +0 -165
- package/src/market/terms.js +0 -200
- package/src/publish/cli.js +0 -336
- package/src/publish/envelope.js +0 -185
- package/src/publish/manifest.js +0 -142
package/src/agent/x402.js
CHANGED
|
@@ -1,31 +1,33 @@
|
|
|
1
|
-
//
|
|
1
|
+
// Buying a record over x402 (v2, scheme "fangorn"): the buyer's half.
|
|
2
2
|
//
|
|
3
|
-
// const {
|
|
3
|
+
// const { fields, price, tx } = await buyRecord(quoteUrl, { privateKey, fangorn, maxPrice: 50000n });
|
|
4
4
|
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
5
|
+
// `quoteUrl` is an app's GET /paid/<publisher>/<repo>/<id> (site/worker.js). The publisher signed
|
|
6
|
+
// the record's price (its terms); paying is SettlementRegistry.register, sent by the facilitator.
|
|
7
|
+
// 1. GET → 402: the requirement (the terms, paid to the registry in USDC), the sealed fields'
|
|
8
|
+
// handles, the repo's deposit, and the relay
|
|
9
|
+
// 2. a Semaphore identity from one wallet signature, and an ERC-3009 receiveWithAuthorization to
|
|
10
|
+
// the registry for exactly the price. Its nonce binds the subject, the identity's commitment,
|
|
11
|
+
// the referrer and a salt, so nobody can reuse it for anything else.
|
|
12
|
+
// 3. GET again with PAYMENT-SIGNATURE: the app has the facilitator verify it and send `register`
|
|
13
|
+
// 4. a Semaphore proof that some identity that paid names `reader` (message = reader, scope =
|
|
14
|
+
// subject), sent to the relay's /relay/settle. `reader` is derived from the identity, so the
|
|
15
|
+
// chain doesn't link it to the paying wallet (the relay could, by timing: PLAN.md, Known gaps).
|
|
16
|
+
// 5. each field's key from the deposit's key server, asked as `reader`; each field decrypted (JSON)
|
|
17
|
+
// A retry picks up where the last one stopped: a registered identity doesn't pay again, and a
|
|
18
|
+
// reader that can already read doesn't settle again.
|
|
12
19
|
//
|
|
13
|
-
// `maxPrice` (
|
|
14
|
-
|
|
20
|
+
// `maxPrice` (USDC base units) is checked before anything is signed.
|
|
21
|
+
|
|
22
|
+
import { concat, createPublicClient, http, keccak256, parseSignature, toHex } from "viem";
|
|
23
|
+
import { privateKeyToAccount } from "viem/accounts";
|
|
24
|
+
import { openField, recordKey, registerNonce, repoId, subjectOf } from "@fangorn-network/sdk";
|
|
15
25
|
|
|
16
26
|
const b64 = (s) => (typeof btoa === "function" ? btoa(s) : Buffer.from(s, "utf8").toString("base64"));
|
|
17
27
|
const unb64 = (s) => (typeof atob === "function" ? atob(s) : Buffer.from(s, "base64").toString("utf8"));
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
export function choose(accepts = [], { maxPrice } = {}) {
|
|
22
|
-
const ok = accepts.filter((a) => a.scheme === "exact" && EVM.test(a.network));
|
|
23
|
-
if (!ok.length) throw new Error(`no payable option: ${JSON.stringify(accepts.map((a) => `${a.scheme}/${a.network}`))}`);
|
|
24
|
-
const cheapest = ok.sort((a, b) => (BigInt(a.amount) < BigInt(b.amount) ? -1 : 1))[0];
|
|
25
|
-
if (maxPrice != null && BigInt(cheapest.amount) > BigInt(maxPrice))
|
|
26
|
-
throw new Error(`price ${cheapest.amount} exceeds the cap ${maxPrice} (token base units)`);
|
|
27
|
-
return cheapest;
|
|
28
|
-
}
|
|
28
|
+
const semaphore = () => import("@semaphore-protocol/core").catch(() => {
|
|
29
|
+
throw new Error("buying needs @semaphore-protocol/core: npm i @semaphore-protocol/core");
|
|
30
|
+
});
|
|
29
31
|
|
|
30
32
|
/** A 402's PaymentRequired, from its PAYMENT-REQUIRED header. */
|
|
31
33
|
export function paymentRequired(res) {
|
|
@@ -34,368 +36,179 @@ export function paymentRequired(res) {
|
|
|
34
36
|
return JSON.parse(unb64(h));
|
|
35
37
|
}
|
|
36
38
|
|
|
37
|
-
/** The
|
|
38
|
-
export
|
|
39
|
-
|
|
39
|
+
/** The requirement we can pay: scheme "fangorn" on an EVM chain, within `maxPrice`. */
|
|
40
|
+
export function choose(accepts = [], { maxPrice } = {}) {
|
|
41
|
+
const r = accepts.find((a) => a.scheme === "fangorn" && /^eip155:\d+$/.test(a.network));
|
|
42
|
+
if (!r) throw new Error(`no payable option: ${JSON.stringify(accepts.map((a) => `${a.scheme}/${a.network}`))}`);
|
|
43
|
+
if (maxPrice != null && BigInt(r.amount) > BigInt(maxPrice)) throw new Error(`price ${r.amount} exceeds the cap ${maxPrice} (USDC base units)`);
|
|
44
|
+
return r;
|
|
40
45
|
}
|
|
41
46
|
|
|
42
|
-
/**
|
|
43
|
-
export async function
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
if (!req.extra?.name || !req.extra?.version) throw new Error(`${req.network}: the seller names no EIP-712 domain for ${req.asset} (extra.name, extra.version)`);
|
|
47
|
+
/** The Semaphore identity and reading account a wallet derives: the same on any machine, from one signature. */
|
|
48
|
+
export async function identityOf(account) {
|
|
49
|
+
const sig = await account.signMessage({ message: "fangorn:identity:v1" });
|
|
50
|
+
const { Identity } = await semaphore();
|
|
51
|
+
return { identity: new Identity(sig), reader: privateKeyToAccount(keccak256(concat([toHex("fangorn:reader:v1"), sig]))) };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** An ERC-3009 receiveWithAuthorization from `account` to `req.payTo` for exactly `req.amount`, bound to this purchase. */
|
|
55
|
+
export async function signPayment(req, account, { subject, commitment, salt, now = Math.floor(Date.now() / 1000) }) {
|
|
56
|
+
const message = { from: account.address, to: req.payTo, value: BigInt(req.amount), validAfter: 0n,
|
|
57
|
+
validBefore: BigInt(now + (req.maxTimeoutSeconds ?? 300)), nonce: registerNonce(subject, commitment, req.extra.referrer, salt) };
|
|
54
58
|
const signature = await account.signTypedData({
|
|
55
|
-
domain: { name: req.extra.name, version: req.extra.version, chainId, verifyingContract: req.asset },
|
|
56
|
-
types: {
|
|
57
|
-
{ name: "from", type: "address" }, { name: "to", type: "address" }, { name: "value", type: "uint256" },
|
|
59
|
+
domain: { name: req.extra.name, version: req.extra.version, chainId: Number(req.network.split(":")[1]), verifyingContract: req.asset },
|
|
60
|
+
types: { ReceiveWithAuthorization: [{ name: "from", type: "address" }, { name: "to", type: "address" }, { name: "value", type: "uint256" },
|
|
58
61
|
{ name: "validAfter", type: "uint256" }, { name: "validBefore", type: "uint256" }, { name: "nonce", type: "bytes32" }] },
|
|
59
|
-
primaryType: "
|
|
60
|
-
message: { ...authorization, value: BigInt(authorization.value), validAfter: BigInt(authorization.validAfter), validBefore: BigInt(authorization.validBefore) },
|
|
62
|
+
primaryType: "ReceiveWithAuthorization", message,
|
|
61
63
|
});
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
//
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
const
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
const toB64 = (u8) => (typeof btoa === "function" ? btoa(String.fromCharCode(...u8)) : Buffer.from(u8).toString("base64"));
|
|
94
|
-
const sha256 = async (bytes) => hex(new Uint8Array(await crypto.subtle.digest("SHA-256", bytes)));
|
|
95
|
-
|
|
96
|
-
/** A record's key, hex: HMAC-SHA256(secret, id). The worker's keyFor; here for sources and tests. */
|
|
97
|
-
export async function paidKey(secret, id) {
|
|
98
|
-
const k = await crypto.subtle.importKey("raw", new TextEncoder().encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
|
|
99
|
-
return hex(new Uint8Array(await crypto.subtle.sign("HMAC", k, new TextEncoder().encode(id))));
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/** Seal `text` under `keyHex`: what a source writes as `paid_ct`. */
|
|
103
|
-
export async function sealPaid(keyHex, text, nonce = crypto.getRandomValues(new Uint8Array(12))) {
|
|
104
|
-
const k = await crypto.subtle.importKey("raw", unhex(keyHex), "AES-GCM", false, ["encrypt"]);
|
|
105
|
-
const ct = new Uint8Array(await crypto.subtle.encrypt({ name: "AES-GCM", iv: nonce }, k, new TextEncoder().encode(text)));
|
|
106
|
-
const out = new Uint8Array(12 + ct.length);
|
|
107
|
-
out.set(nonce); out.set(ct, 12);
|
|
108
|
-
return toB64(out);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/** Open a sealed record and check it against the committed sha256 (`want`, if known). */
|
|
112
|
-
export async function openPaid(ct, keyHex, want) {
|
|
113
|
-
const bytes = fromB64(ct);
|
|
114
|
-
const k = await crypto.subtle.importKey("raw", unhex(keyHex), "AES-GCM", false, ["decrypt"]);
|
|
115
|
-
const plain = new Uint8Array(await crypto.subtle.decrypt({ name: "AES-GCM", iv: bytes.slice(0, 12) }, k, bytes.slice(12)));
|
|
116
|
-
const sha = await sha256(plain);
|
|
117
|
-
return { sha256: sha, verified: want ? want === sha : null, record: JSON.parse(new TextDecoder().decode(plain)) };
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/** A private app's pass (its card's `access`): one payment at POST `pass_url`, a bearer token back. */
|
|
121
|
-
export async function buyPass(access, { account, privateKey, maxPrice = access.pass, fetchImpl } = {}) {
|
|
122
|
-
const r = await payAndFetch(access.pass_url, { method: "POST", account, privateKey, maxPrice, fetchImpl });
|
|
123
|
-
if (r.status !== 200) throw new Error(`no pass: HTTP ${r.status} ${r.body.slice(0, 200)}`);
|
|
124
|
-
return { ...JSON.parse(r.body), price: r.price, receipt: r.receipt };
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// ── Private apps ──────────────────────────────────────────────────────────────────────────
|
|
128
|
-
// A private app's rows are sealed whole before they reach IPFS: the payload on chain is
|
|
129
|
-
// `{sealed}`, base64(nonce[12] ‖ AES-256-GCM(rowKey, JSON(payload))), with rowKey =
|
|
130
|
-
// HMAC-SHA256(PAID_SECRET, "fangorn.row.v1"). The app's Worker holds the secret, opens the rows
|
|
131
|
-
// in its cron and serves the view only to those who pay. The nonce is the first 12 bytes of
|
|
132
|
-
// HMAC(rowKey, plaintext): an unchanged row seals to the same bytes, so the same CID, and is not
|
|
133
|
-
// embedded again; two different rows never share a nonce. (Equal rows look equal: that is all.)
|
|
134
|
-
|
|
135
|
-
const ROW = "fangorn.row.v1";
|
|
136
|
-
async function rowNonce(keyHex, text) {
|
|
137
|
-
const k = await crypto.subtle.importKey("raw", unhex(keyHex), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
|
|
138
|
-
return new Uint8Array(await crypto.subtle.sign("HMAC", k, new TextEncoder().encode(text))).slice(0, 12);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/** The payload a private app commits for `payload`. */
|
|
142
|
-
export async function sealRow(secret, payload) {
|
|
143
|
-
const key = await paidKey(secret, ROW), text = JSON.stringify(payload);
|
|
144
|
-
return { sealed: await sealPaid(key, text, await rowNonce(key, text)) };
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/** A row as committed, opened; a payload that is not sealed comes back as it is. */
|
|
148
|
-
export async function openRow(secret, payload) {
|
|
149
|
-
if (typeof payload?.sealed !== "string") return payload;
|
|
150
|
-
return (await openPaid(payload.sealed, await paidKey(secret, ROW))).record;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/** Open every record a seller sent a key for. `ct[id]` is the row's paid_ct, `want[id]` its paid_sha256. */
|
|
154
|
-
async function openAll(keys, ct, want) {
|
|
155
|
-
const out = [];
|
|
156
|
-
for (const [id, key] of Object.entries(keys ?? {})) {
|
|
157
|
-
if (!ct[id]) { out.push({ id, key, error: "no sealed record for this id in the rows read" }); continue; }
|
|
158
|
-
try { out.push({ id, ...(await openPaid(ct[id], key, want[id])) }); }
|
|
159
|
-
catch (e) { out.push({ id, key, error: `could not open: ${e.message}` }); }
|
|
64
|
+
const { v, r, s, yParity } = parseSignature(signature);
|
|
65
|
+
return { from: message.from, value: req.amount, validAfter: "0", validBefore: String(message.validBefore), nonce: message.nonce,
|
|
66
|
+
v: Number(v ?? 27n + BigInt(yParity)), r, s };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** A Semaphore proof that `identity` is in the group, naming `reader`, scoped to `subject`, as `settle` checks it. */
|
|
70
|
+
async function prove({ identity, members, reader, subject }) {
|
|
71
|
+
const { Group, generateProof } = await semaphore();
|
|
72
|
+
const p = await generateProof(identity, new Group(members), BigInt(reader), BigInt(subject));
|
|
73
|
+
return { merkleTreeDepth: String(p.merkleTreeDepth), merkleTreeRoot: String(p.merkleTreeRoot), nullifier: String(p.nullifier), points: p.points.map(String) };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Buy one record and open its sealed fields. `fangorn` is an SDK client on the record's chain:
|
|
78
|
+
* it reads the registry and the ciphertext. `deps` replaces the chain reads and the proof in tests.
|
|
79
|
+
*/
|
|
80
|
+
export async function buyRecord(quoteUrl, { privateKey, account = privateKeyToAccount(privateKey), fangorn, maxPrice, fetchImpl = fetch, deps = {} } = {}) {
|
|
81
|
+
const first = await fetchImpl(quoteUrl);
|
|
82
|
+
if (first.status !== 402) throw new Error(`${quoteUrl}: expected a 402 quote, got HTTP ${first.status} ${(await first.text()).slice(0, 200)}`);
|
|
83
|
+
const req = choose(paymentRequired(first).accepts, { maxPrice });
|
|
84
|
+
const { record, fields, deposit, relay } = await first.json();
|
|
85
|
+
const terms = req.extra.terms;
|
|
86
|
+
// The quote is the app's word; the terms are the publisher's, and must be this record's.
|
|
87
|
+
if (terms.publisher.toLowerCase() !== record.publisher.toLowerCase() || terms.repo !== repoId(record.repo) || terms.key !== recordKey(record.id))
|
|
88
|
+
throw new Error("the quote's terms are for another record");
|
|
89
|
+
const subject = subjectOf(terms.publisher, terms.repo, terms.key);
|
|
90
|
+
// Every sealed field must be readable before anything is paid: a sale we can't open is money lost.
|
|
91
|
+
const storage = fangorn.getStorage(), held = new Map();
|
|
92
|
+
for (const h of Object.values(fields)) {
|
|
93
|
+
try { held.set(h.cid, await storage.getFile(h.cid)); }
|
|
94
|
+
catch (e) { throw new Error(`not buying: the record's sealed data (${h.cid}) can't be read: ${e.message}`); }
|
|
160
95
|
}
|
|
161
|
-
return out;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
/** Buy one record an app sells (its agent card's `paid`): pay for its key, open `ct` (the
|
|
165
|
-
* row's paid_ct) and check it against `want` (its paid_sha256). Shared by fangorn-mcp and the page. */
|
|
166
|
-
export async function buyRecord(paid, key, { ct, want, account, privateKey, maxPrice = paid.price, fetchImpl } = {}) {
|
|
167
|
-
if (!ct) return { id: key, error: "this row carries no sealed record (paid_ct): nothing to buy" };
|
|
168
|
-
const r = await payAndFetch(paid.url.replace("{id}", encodeURIComponent(key)), { account, privateKey, maxPrice, fetchImpl });
|
|
169
|
-
if (r.status !== 200) return { id: key, error: `HTTP ${r.status}: ${r.body.slice(0, 200)}` };
|
|
170
|
-
const opened = await openPaid(ct, JSON.parse(r.body).key, want);
|
|
171
|
-
return { id: key, price: r.price, receipt: r.receipt, digest: opened.sha256, verified: opened.verified, record: opened.record };
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
// ── Batches and vouchers ──────────────────────────────────────────────────────────────────
|
|
175
|
-
// `POST <app>/paid {ids}` answers 402 with one price for every record it has; PAYMENT-SIGNATURE may then
|
|
176
|
-
// carry several payments (a JSON array) that add up to exactly that price. A payment signs
|
|
177
|
-
// payee, amount and expiry but not the resource (checked against the PayAI facilitator,
|
|
178
|
-
// 2026-10-07), so a person's wallet can sign a stack of them ahead of time ("vouchers") and an
|
|
179
|
-
// agent spends them later with no key: each pays only the app, for a fixed amount, until it
|
|
180
|
-
// expires. Holding them is holding the right to that much of the person's money, nothing more.
|
|
181
96
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
if (
|
|
190
|
-
const
|
|
191
|
-
|
|
97
|
+
const config = fangorn.getConfig(), settlement = fangorn.getSettlementRegistry();
|
|
98
|
+
const pub = deps.pub ?? createPublicClient({ transport: http(config.rpcUrl) });
|
|
99
|
+
const members = deps.members ?? ((s) => settlement.membersOf(s).then((m) => [...m]));
|
|
100
|
+
const { identity, reader } = await identityOf(account);
|
|
101
|
+
|
|
102
|
+
let tx = null;
|
|
103
|
+
if (!(await settlement.canRead(reader.address, terms.publisher, terms.repo, terms.key))) {
|
|
104
|
+
if (!(await settlement.isRegistered(subject, identity.commitment))) {
|
|
105
|
+
const salt = toHex(crypto.getRandomValues(new Uint8Array(32)));
|
|
106
|
+
const payment = await signPayment(req, account, { subject, commitment: identity.commitment, salt });
|
|
107
|
+
const payload = { commitment: identity.commitment.toString(), salt, payment };
|
|
108
|
+
const paid = await fetchImpl(quoteUrl, { headers: { "PAYMENT-SIGNATURE": b64(JSON.stringify({ x402Version: 2, accepted: req, payload })) } });
|
|
109
|
+
if (!paid.ok) throw new Error(`payment refused: ${(await paid.json().catch(() => ({}))).error ?? `HTTP ${paid.status}`}`);
|
|
110
|
+
tx = JSON.parse(unb64(paid.headers.get("PAYMENT-RESPONSE") ?? b64("{}"))).transaction || null;
|
|
111
|
+
if (tx) await pub.waitForTransactionReceipt({ hash: tx });
|
|
192
112
|
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
/** Vouchers adding up to exactly `amount`, largest first; null if they cannot. */
|
|
201
|
-
export function pick(vouchers, amount) {
|
|
202
|
-
let left = BigInt(amount);
|
|
203
|
-
const out = [];
|
|
204
|
-
for (const v of [...vouchers].sort((a, b) => { const x = BigInt(a.payload.authorization.value), y = BigInt(b.payload.authorization.value); return x > y ? -1 : x < y ? 1 : 0; })) { // largest first; equal ones keep their order
|
|
205
|
-
const x = BigInt(v.payload.authorization.value);
|
|
206
|
-
if (x <= left) { out.push(v); left -= x; }
|
|
207
|
-
}
|
|
208
|
-
return left === 0n ? out : null; // ponytail: greedy; exact for denominations that divide each other
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
/** Run `fn` holding `<path>.lock` (Node only): two fangorn-mcp processes sharing one allowance
|
|
212
|
-
* file would otherwise pick the same vouchers and race. A lock older than `staleMs` belongs to
|
|
213
|
-
* a crashed purchase and is taken over.
|
|
214
|
-
* ponytail: a takeover can race a third process for the same stale lock; add a pid check if
|
|
215
|
-
* more than two servers ever share a file. */
|
|
216
|
-
export async function withLock(path, fn, { staleMs = 120_000, waitMs = 30_000 } = {}) {
|
|
217
|
-
const fs = await import("node:fs"), lock = `${path}.lock`, start = Date.now();
|
|
218
|
-
for (;;) {
|
|
219
|
-
try { fs.closeSync(fs.openSync(lock, "wx")); break; } catch (e) {
|
|
220
|
-
if (e.code !== "EEXIST") throw e;
|
|
221
|
-
let age = 0;
|
|
222
|
-
try { age = Date.now() - fs.statSync(lock).mtimeMs; } catch { continue; } // released meanwhile
|
|
223
|
-
if (age > staleMs) { fs.rmSync(lock, { force: true }); continue; }
|
|
224
|
-
if (Date.now() - start > waitMs) throw new Error(`the allowance is in use by another purchase (${lock})`);
|
|
225
|
-
await new Promise((ok) => setTimeout(ok, 100));
|
|
113
|
+
// A load-balanced RPC can answer from a node behind the one that mined `register`:
|
|
114
|
+
// our commitment is registered, so read until it shows up.
|
|
115
|
+
const has = (g) => !!deps.members || g.some((c) => BigInt(c) === identity.commitment);
|
|
116
|
+
let group = await members(subject);
|
|
117
|
+
for (let i = 0; i < 10 && !has(group); i++) {
|
|
118
|
+
await new Promise((r) => setTimeout(r, 3000));
|
|
119
|
+
group = await members(subject);
|
|
226
120
|
}
|
|
121
|
+
if (!has(group)) throw new Error("our commitment is registered but the RPC doesn't show it yet; retry the purchase");
|
|
122
|
+
const proof = await (deps.prove ?? prove)({ identity, members: group, reader: reader.address, subject });
|
|
123
|
+
const res = await fetchImpl(`${relay}/relay/settle`, { method: "POST", headers: { "content-type": "application/json" },
|
|
124
|
+
body: JSON.stringify({ subject, reader: reader.address, ...proof }) });
|
|
125
|
+
const out = await res.json().catch(() => ({}));
|
|
126
|
+
if (!res.ok) throw new Error(`settle refused: ${out.error ?? `HTTP ${res.status}`}`);
|
|
127
|
+
await pub.waitForTransactionReceipt({ hash: out.tx });
|
|
227
128
|
}
|
|
228
|
-
try { return await fn(); } finally { fs.rmSync(lock, { force: true }); }
|
|
229
|
-
}
|
|
230
129
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
export async function buyBatch(paid, allIds, { ct = {}, want = {}, vouchers, account, privateKey, maxPrice, fetchImpl = fetch } = {}) {
|
|
236
|
-
const url = paid.url.replace(/\/\{id\}$/, "");
|
|
237
|
-
// Only ids whose sealed record is in hand are bought: anything else would pay for nothing.
|
|
238
|
-
const ids = allIds.filter((id) => ct[id]), unsealed = allIds.filter((id) => !ct[id]);
|
|
239
|
-
if (!ids.length) throw new Error(`none of these rows carries a sealed record (paid_ct): ${unsealed.join(", ")}`);
|
|
240
|
-
const post = (headers = {}) => fetchImpl(url, { method: "POST", headers: { "content-type": "application/json", ...headers }, body: JSON.stringify({ ids }) });
|
|
241
|
-
const first = await post();
|
|
242
|
-
const body0 = await first.json();
|
|
243
|
-
if (first.status !== 402) throw new Error(`expected a price, got HTTP ${first.status}: ${JSON.stringify(body0).slice(0, 200)}`);
|
|
244
|
-
const offer = { ...body0, ...paymentRequired(first) };
|
|
245
|
-
const req = choose(offer.accepts, { maxPrice });
|
|
246
|
-
let payments;
|
|
247
|
-
if (vouchers) {
|
|
248
|
-
payments = pick(vouchers, req.amount);
|
|
249
|
-
if (!payments) throw new Error(`the allowance cannot pay exactly ${req.amount} (base units); add funds or buy fewer records`);
|
|
250
|
-
} else {
|
|
251
|
-
account ??= privateKey ? (await import("viem/accounts")).privateKeyToAccount(privateKey) : null;
|
|
252
|
-
if (!account) throw new Error(`${offer.found.length} record(s) cost ${req.amount} (base units); no wallet or allowance to pay`);
|
|
253
|
-
payments = [await signPayment(req, account, { resource: offer.resource })];
|
|
130
|
+
const opened = {};
|
|
131
|
+
for (const [field, handle] of Object.entries(fields)) {
|
|
132
|
+
const bytes = await openField({ handle, deposit, key: terms.key, field, signer: reader, storage: { getFile: async (cid) => held.get(cid) ?? storage.getFile(cid) }, fetch: fetchImpl });
|
|
133
|
+
opened[field] = JSON.parse(new TextDecoder().decode(bytes));
|
|
254
134
|
}
|
|
255
|
-
|
|
256
|
-
const body = await res.json();
|
|
257
|
-
// Drop every voucher the seller says it settled (`paid`) or could not (`rejected`: spent, or
|
|
258
|
-
// the wallet cannot cover it), on success, partial delivery and refusal alike, so a dead
|
|
259
|
-
// voucher is never offered twice. A seller that names none: all of them on a 200.
|
|
260
|
-
const nonce = (p) => p.payload.authorization.nonce;
|
|
261
|
-
const paidNonces = new Set(body.paid ?? (res.status === 200 ? payments.map(nonce) : []));
|
|
262
|
-
if (vouchers) for (const n of [...paidNonces, ...(body.rejected ?? [])]) {
|
|
263
|
-
const k = vouchers.findIndex((v) => nonce(v) === n);
|
|
264
|
-
if (k >= 0) vouchers.splice(k, 1);
|
|
265
|
-
}
|
|
266
|
-
if (res.status !== 200) throw new Error(`payment refused: ${body.error ?? JSON.stringify(body).slice(0, 200)}`);
|
|
267
|
-
const spent = payments.filter((p) => paidNonces.has(nonce(p))).reduce((s, p) => s + BigInt(p.payload.authorization.value), 0n);
|
|
268
|
-
const records = await openAll(body.keys, ct, want);
|
|
269
|
-
// `undelivered` + `error`: a payment failed after others settled; these ids were not paid for.
|
|
270
|
-
return { records, missing: [...unsealed, ...(body.missing ?? [])], undelivered: body.undelivered ?? [], error: body.error ?? null,
|
|
271
|
-
manifest: { app: url, bought: new Date().toISOString(), price: String(spent),
|
|
272
|
-
asset: req.asset, network: req.network, transactions: body.receipts.map((r) => r.transaction),
|
|
273
|
-
records: records.map(({ id, sha256 }) => ({ id, sha256 })) } };
|
|
135
|
+
return { record, fields: opened, price: req.amount, tx };
|
|
274
136
|
}
|
|
275
137
|
|
|
276
|
-
// ── self-check: `node src/agent/x402.js` — a stand-in
|
|
138
|
+
// ── self-check: `node src/agent/x402.js` — a stand-in app, relay and key server; no chain ──
|
|
277
139
|
if (typeof process !== "undefined" && import.meta.url === `file://${process.argv[1]}` && process.argv[1].endsWith("/x402.js")) {
|
|
278
|
-
const {
|
|
279
|
-
const {
|
|
280
|
-
const
|
|
281
|
-
|
|
282
|
-
const
|
|
283
|
-
const
|
|
284
|
-
|
|
285
|
-
const
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
const
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
const WORKER = "../../site/worker.js";
|
|
313
|
-
const worker = (await import(/* @vite-ignore */ WORKER)).default;
|
|
314
|
-
// The rows as a buyer reads them: each record sealed under its key, with the plaintext's sha256.
|
|
315
|
-
const SECRET = "s3cret", ct = {}, want = {};
|
|
316
|
-
for (const id of ["a", "b", "c"]) {
|
|
317
|
-
const plain = JSON.stringify({ id, amount: 1 });
|
|
318
|
-
ct[id] = await sealPaid(await paidKey(SECRET, id), plain);
|
|
319
|
-
want[id] = await sha256(new TextEncoder().encode(plain));
|
|
320
|
-
}
|
|
321
|
-
const env = { PAID_SECRET: SECRET, CONFIG: { name: "Test", paid: sale } };
|
|
322
|
-
const spent = new Set(), facilitated = [];
|
|
323
|
-
const realFetch = globalThis.fetch;
|
|
324
|
-
globalThis.fetch = async (u, init) => {
|
|
325
|
-
const { x402Version, paymentPayload: p, paymentRequirements: q } = JSON.parse(init.body), a = p.payload.authorization;
|
|
326
|
-
if (x402Version !== 2 || p.x402Version !== 2) throw new Error("the facilitator is spoken to in v2");
|
|
327
|
-
facilitated.push(p);
|
|
328
|
-
if (u.endsWith("/verify")) {
|
|
329
|
-
const ok = await verifyTypedData({ address: a.from, signature: p.payload.signature,
|
|
330
|
-
domain: { ...DOMAIN, chainId: CHAIN, verifyingContract: USDC },
|
|
331
|
-
types: { TransferWithAuthorization: [{ name: "from", type: "address" }, { name: "to", type: "address" }, { name: "value", type: "uint256" },
|
|
332
|
-
{ name: "validAfter", type: "uint256" }, { name: "validBefore", type: "uint256" }, { name: "nonce", type: "bytes32" }] },
|
|
333
|
-
primaryType: "TransferWithAuthorization",
|
|
334
|
-
message: { ...a, value: BigInt(a.value), validAfter: BigInt(a.validAfter), validBefore: BigInt(a.validBefore) } });
|
|
335
|
-
return new Response(JSON.stringify({ isValid: ok && a.value === q.amount, invalidReason: "bad" }));
|
|
140
|
+
const { x25519 } = await import("@noble/curves/ed25519");
|
|
141
|
+
const { generatePrivateKey } = await import("viem/accounts");
|
|
142
|
+
const { recoverTypedDataAddress } = await import("viem");
|
|
143
|
+
const sdk = await import("@fangorn-network/sdk");
|
|
144
|
+
const { testKeyServer } = await import("@fangorn-network/sdk/lib/test/key-server.js");
|
|
145
|
+
const eq = (a, b, m) => { if (JSON.stringify(a) !== JSON.stringify(b)) throw new Error(`${m}: ${JSON.stringify(a)} != ${JSON.stringify(b)}`); };
|
|
146
|
+
|
|
147
|
+
const publisher = privateKeyToAccount(generatePrivateKey()), buyer = privateKeyToAccount(generatePrivateKey());
|
|
148
|
+
const REGISTRY = `0x${"e0".repeat(20)}`, USDC = `0x${"aa".repeat(20)}`, REFERRER = `0x${"fe".repeat(20)}`;
|
|
149
|
+
const repo = sdk.repoId("notes"), key = sdk.recordKey("a");
|
|
150
|
+
const scope = { chainId: 84532, registry: REGISTRY, publisher: publisher.address, repo, generation: 0n };
|
|
151
|
+
const ksSecret = x25519.utils.randomSecretKey(), kRepo = sdk.newRepoKey();
|
|
152
|
+
const deposit = sdk.makeDeposit({ kRepo, keyServerPubkey: x25519.getPublicKey(ksSecret), keyServer: "https://keys.test", scope });
|
|
153
|
+
const files = new Map();
|
|
154
|
+
const storage = { putFile: async (b) => { const cid = `bafy${files.size}`; files.set(cid, b); return cid; }, getFile: async (cid) => files.get(cid) };
|
|
155
|
+
const body = await sdk.sealField({ plaintext: new TextEncoder().encode(JSON.stringify({ text: "the body" })), kRepo, scope, key, field: "body", storage });
|
|
156
|
+
const terms = { publisher: publisher.address, repo, key, price: "10000", shareBps: 500, generation: "0", deadline: "2000000000" };
|
|
157
|
+
const req = { scheme: "fangorn", network: "eip155:84532", amount: "10000", asset: USDC, payTo: REGISTRY, maxTimeoutSeconds: 300,
|
|
158
|
+
extra: { terms, signature: "0x12", referrer: REFERRER, name: "USDC", version: "2" } };
|
|
159
|
+
|
|
160
|
+
// The chain as the buyer sees it: settled once the relay has the proof.
|
|
161
|
+
let settled = false, registered = false, signed = null, proven = null;
|
|
162
|
+
const fangorn = { getConfig: () => ({ rpcUrl: "http://unused", fromBlock: 0n }), getStorage: () => storage,
|
|
163
|
+
getSettlementRegistry: () => ({ getAddress: () => REGISTRY, canRead: async () => settled, isRegistered: async () => registered }) };
|
|
164
|
+
const pub = { waitForTransactionReceipt: async () => ({}) };
|
|
165
|
+
const quoteUrl = `https://app.test/paid/${publisher.address}/notes/a`;
|
|
166
|
+
const fetchImpl = async (url, init = {}) => {
|
|
167
|
+
if (url === quoteUrl) {
|
|
168
|
+
const pay = init.headers?.["PAYMENT-SIGNATURE"];
|
|
169
|
+
if (!pay) return new Response(JSON.stringify({ record: { publisher: publisher.address, repo: "notes", id: "a" }, fields: { body }, deposit, relay: "https://relay.test" }),
|
|
170
|
+
{ status: 402, headers: { "PAYMENT-REQUIRED": b64(JSON.stringify({ x402Version: 2, accepts: [req] })) } });
|
|
171
|
+
signed = JSON.parse(unb64(pay)).payload;
|
|
172
|
+
registered = true;
|
|
173
|
+
return new Response("{}", { headers: { "PAYMENT-RESPONSE": b64(JSON.stringify({ success: true, transaction: "0xabc" })) } });
|
|
336
174
|
}
|
|
337
|
-
if (
|
|
338
|
-
|
|
339
|
-
|
|
175
|
+
if (url === "https://relay.test/relay/settle") { proven = JSON.parse(init.body); settled = true; return new Response(JSON.stringify({ tx: "0xdef" })); }
|
|
176
|
+
if (url === "https://keys.test/access") return testKeyServer(JSON.parse(init.body), { secret: ksSecret, chainId: 84532, registry: REGISTRY,
|
|
177
|
+
canRead: async (r) => settled && r === proven.reader });
|
|
178
|
+
throw new Error(`unexpected fetch ${url}`);
|
|
340
179
|
};
|
|
341
|
-
const
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
if (short.status !== 402) throw new Error("payments short of the price must be refused");
|
|
376
|
-
// A spent voucher still in the stack, behind a fresh one (a crash before the file was
|
|
377
|
-
// saved): the fresh one settles and buys one record, the spent one is refused, and both
|
|
378
|
-
// leave the stack so neither is offered again.
|
|
379
|
-
const fresh2 = vs.find((v) => v !== fresh && v.payload.authorization.value === "100000");
|
|
380
|
-
const stack = [fresh2, usedOne];
|
|
381
|
-
const half = await buyBatch(paidCard, ["a", "b"], { vouchers: stack, ct, want, fetchImpl: viaWorker });
|
|
382
|
-
if (half.records.length !== 1 || half.undelivered[0] !== "b" || !half.error || half.manifest.price !== "100000" || stack.length !== 0)
|
|
383
|
-
throw new Error(`partial settle: ${JSON.stringify({ ...half, stack: stack.length })}`);
|
|
384
|
-
// The spent voucher first: nothing settles, nothing is sent, and it is dropped.
|
|
385
|
-
const stack2 = [usedOne, fresh];
|
|
386
|
-
let threw = false;
|
|
387
|
-
try { await buyBatch(paidCard, ["a", "b"], { vouchers: stack2, ct, want, fetchImpl: viaWorker }); } catch { threw = true; }
|
|
388
|
-
if (!threw || stack2.length !== 1 || stack2[0] !== fresh) throw new Error(`refused batch must drop only the spent voucher: ${stack2.length}`);
|
|
389
|
-
// The allowance lock: two purchases at once take turns; a crashed one's lock is taken over.
|
|
390
|
-
const vfile = `${mkdtempSync(`${tmpdir()}/wm-`)}/vouchers.json`, order = [];
|
|
391
|
-
const turn = (name) => withLock(vfile, async () => { order.push(`${name}+`); await new Promise((ok) => setTimeout(ok, 50)); order.push(`${name}-`); });
|
|
392
|
-
await Promise.all([turn("a"), turn("b")]);
|
|
393
|
-
if (order.join() !== "a+,a-,b+,b-" && order.join() !== "b+,b-,a+,a-") throw new Error(`lock interleaved: ${order}`);
|
|
394
|
-
writeFileSync(`${vfile}.lock`, "");
|
|
395
|
-
const { utimesSync, existsSync } = await import("node:fs");
|
|
396
|
-
utimesSync(`${vfile}.lock`, new Date(Date.now() - 300_000), new Date(Date.now() - 300_000));
|
|
397
|
-
if (await withLock(vfile, async () => 7) !== 7 || existsSync(`${vfile}.lock`)) throw new Error("a stale lock must be taken over and released");
|
|
398
|
-
} finally { globalThis.fetch = realFetch; }
|
|
399
|
-
console.log("x402.js batch self-check ok — keys, not records: each opens the row's sealed record and matches its hash; one price for the ids sealed in the rows, paid by one signature or by vouchers signed ahead, spent vouchers removed and refused if replayed, short payment refused, a half-settled batch delivers what was paid and drops spent vouchers, the allowance file is locked per purchase");
|
|
400
|
-
console.log("x402.js self-check ok — a signed EIP-3009 authorization the seller can verify, exactly the asked amount to the asked payee, the receipt read back, and a price over the cap refused before signing");
|
|
180
|
+
const deps = { pub, members: async () => [1n], prove: async ({ reader, subject }) => ({ merkleTreeDepth: "1", merkleTreeRoot: "1", nullifier: "1", points: Array(8).fill("0"), reader, subject }) };
|
|
181
|
+
|
|
182
|
+
await buyRecord(quoteUrl, { account: buyer, fangorn, fetchImpl, deps, maxPrice: 9999n }).then(() => { throw new Error("over the cap must refuse"); },
|
|
183
|
+
(e) => { if (!/exceeds the cap/.test(e.message)) throw e; });
|
|
184
|
+
eq(signed, null, "nothing is signed over the cap");
|
|
185
|
+
const gone = { ...fangorn, getStorage: () => ({ getFile: async () => { throw new Error("HTTP 404"); } }) };
|
|
186
|
+
await buyRecord(quoteUrl, { account: buyer, fangorn: gone, fetchImpl, deps }).then(() => { throw new Error("unreadable data must refuse"); },
|
|
187
|
+
(e) => { if (!/can't be read/.test(e.message)) throw e; });
|
|
188
|
+
eq(signed, null, "nothing is paid for data that can't be read");
|
|
189
|
+
|
|
190
|
+
const got = await buyRecord(quoteUrl, { account: buyer, fangorn, fetchImpl, deps, maxPrice: 10000n });
|
|
191
|
+
eq([got.fields, got.price, got.tx], [{ body: { text: "the body" } }, "10000", "0xabc"], "paid, settled, and the field opens as JSON");
|
|
192
|
+
const { identity, reader } = await identityOf(buyer);
|
|
193
|
+
eq([signed.commitment, proven.reader, proven.subject], [identity.commitment.toString(), reader.address, subjectOf(publisher.address, repo, key)], "the payment carries the identity; the proof names the reader");
|
|
194
|
+
if (reader.address === buyer.address) throw new Error("the reader must not be the paying wallet");
|
|
195
|
+
const p = signed.payment;
|
|
196
|
+
const from = await recoverTypedDataAddress({ domain: { name: "USDC", version: "2", chainId: 84532, verifyingContract: USDC },
|
|
197
|
+
types: { ReceiveWithAuthorization: [{ name: "from", type: "address" }, { name: "to", type: "address" }, { name: "value", type: "uint256" },
|
|
198
|
+
{ name: "validAfter", type: "uint256" }, { name: "validBefore", type: "uint256" }, { name: "nonce", type: "bytes32" }] },
|
|
199
|
+
primaryType: "ReceiveWithAuthorization", message: { from: p.from, to: p.payTo ?? REGISTRY, value: BigInt(p.value), validAfter: 0n, validBefore: BigInt(p.validBefore), nonce: p.nonce },
|
|
200
|
+
signature: concat([p.r, p.s, toHex(p.v, { size: 1 })]) });
|
|
201
|
+
eq([from, p.value, p.nonce], [buyer.address, "10000", registerNonce(subjectOf(publisher.address, repo, key), identity.commitment, REFERRER, signed.salt)],
|
|
202
|
+
"an ERC-3009 authorization from the buyer for exactly the price, its nonce bound to this purchase and referrer");
|
|
203
|
+
|
|
204
|
+
signed = null;
|
|
205
|
+
const again = await buyRecord(quoteUrl, { account: buyer, fangorn, fetchImpl, deps });
|
|
206
|
+
eq([again.fields.body.text, signed, again.tx], ["the body", null, null], "a reader that can already read pays nothing again");
|
|
207
|
+
|
|
208
|
+
const other = { ...req, extra: { ...req.extra, terms: { ...terms, key: sdk.recordKey("b") } } };
|
|
209
|
+
const lying = async (url, init) => url === quoteUrl && !init?.headers ? new Response(JSON.stringify({ record: { publisher: publisher.address, repo: "notes", id: "a" }, fields: { body }, deposit, relay: "https://relay.test" }),
|
|
210
|
+
{ status: 402, headers: { "PAYMENT-REQUIRED": b64(JSON.stringify({ x402Version: 2, accepts: [other] })) } }) : fetchImpl(url, init);
|
|
211
|
+
await buyRecord(quoteUrl, { account: privateKeyToAccount(generatePrivateKey()), fangorn, fetchImpl: lying, deps }).then(() => { throw new Error("must refuse"); },
|
|
212
|
+
(e) => { if (!/another record/.test(e.message)) throw e; });
|
|
213
|
+
console.log("x402.js self-check ok — the cap holds before signing, the payment is an ERC-3009 authorization bound to this purchase, the proof names a reader that isn't the wallet, a retry pays nothing, a quote for another record is refused, nothing is paid when the sealed data can't be read, and the field opens");
|
|
401
214
|
}
|