@fangorn-network/westmarch 0.0.20-dev → 2026.10.2-2.0-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 +33 -3
- package/docs/app-to-agent.md +117 -60
- package/package.json +10 -5
- package/plugin/scripts/fx.mjs +7 -9
- package/site/dist/assets/{AccountNotFoundScreen-C4njXc69-BOfmiSH1.js → AccountNotFoundScreen-C4njXc69-DyVH4CEz.js} +1 -1
- package/site/dist/assets/{AddFundsSelectionScreen-RAoCn7ej-D_EsIeJu.js → AddFundsSelectionScreen-RAoCn7ej-Dz1--SS9.js} +1 -1
- package/site/dist/assets/{Address-H8kD1d9O-C85sJ79B.js → Address-H8kD1d9O-BdFZmRwI.js} +1 -1
- package/site/dist/assets/{AffirmativeConsentScreen-BG0MJxku-DdEPFOww.js → AffirmativeConsentScreen-BG0MJxku-CxaCAisL.js} +1 -1
- package/site/dist/assets/{AllowlistRejectionScreen-Cae6e1Od-DhVy9iGN.js → AllowlistRejectionScreen-Cae6e1Od-JGk7vndN.js} +1 -1
- package/site/dist/assets/{ApiController-CYdiyu9B.js → ApiController-CTLbMCcH.js} +4 -4
- package/site/dist/assets/{ArrowsRightLeftIcon-cp60yobq.js → ArrowsRightLeftIcon-B8g9adpz.js} +1 -1
- package/site/dist/assets/{AuthenticateWithWalletScreen-CU6hIcFT-8rbYiYYE.js → AuthenticateWithWalletScreen-CU6hIcFT-CXD9_OP8.js} +1 -1
- package/site/dist/assets/{AwaitingEvmToSolBridgingScreen-D0e_sZMS-2pkNfCno.js → AwaitingEvmToSolBridgingScreen-D0e_sZMS-BtJCglvU.js} +1 -1
- package/site/dist/assets/{AwaitingExternalEthereumTransferScreen-ur5HL4R7-7AQt25_k.js → AwaitingExternalEthereumTransferScreen-ur5HL4R7-BgC2TsqH.js} +1 -1
- package/site/dist/assets/{AwaitingPasswordlessCodeScreen-DRHz76mM-BZY30TGs.js → AwaitingPasswordlessCodeScreen-DRHz76mM-B9OzLeEO.js} +1 -1
- package/site/dist/assets/{AwaitingSolToEvmBridgingScreen-CdLRrGD2-BILOOLi_.js → AwaitingSolToEvmBridgingScreen-CdLRrGD2-C7r5snUL.js} +1 -1
- package/site/dist/assets/BridgeNetworkSelectionView-CFGjB2nO-BTV0NRio.js +68 -0
- package/site/dist/assets/{CaptchaScreen-DbpvkIGx-D8eDcAkc.js → CaptchaScreen-DbpvkIGx-7rSZIuM-.js} +1 -1
- package/site/dist/assets/{CheckCircleIcon-NCOQc0Gs.js → CheckCircleIcon-B1l17ioc.js} +1 -1
- package/site/dist/assets/{CheckCircleIcon-BM409mGI.js → CheckCircleIcon-BGSI8nKf.js} +1 -1
- package/site/dist/assets/{Checkbox-D1EDeo41-rHlvsZzP.js → Checkbox-D1EDeo41-BUfq0w99.js} +1 -1
- package/site/dist/assets/{ChevronDownIcon-CW-4MSpE.js → ChevronDownIcon-B6uJvPHu.js} +1 -1
- package/site/dist/assets/{Chip-CZKIKt9K-CutJG010.js → Chip-CZKIKt9K-r9Nk__0-.js} +1 -1
- package/site/dist/assets/{CoinbaseOnrampStatusScreen-BbyCJDXL-DLs9zX-Q.js → CoinbaseOnrampStatusScreen-BbyCJDXL-DWWNHxmR.js} +1 -1
- package/site/dist/assets/{ConnectEmailForm-B-VRE0MN-N9J6T_YL.js → ConnectEmailForm-B-VRE0MN-CRQwS0Xu.js} +1 -1
- package/site/dist/assets/{ConnectLedgerScreen-DWHABGot-h0uYIUi6.js → ConnectLedgerScreen-DWHABGot-CZcCt31O.js} +1 -1
- package/site/dist/assets/{ConnectOnlyLandingScreen-CZYx9Bmp-LeuDMN1E.js → ConnectOnlyLandingScreen-CZYx9Bmp-C5IOf-SQ.js} +1 -1
- package/site/dist/assets/{ConnectOnlyStatusScreen-DCRzjxHx-DaRa59tZ.js → ConnectOnlyStatusScreen-DCRzjxHx-cwg74F3A.js} +1 -1
- package/site/dist/assets/ConnectOrCreateScreen-CUUYUO07-D28oeK6b.js +1 -0
- package/site/dist/assets/{ConnectPhoneForm-Dek5xn4C-BEA4FmhI.js → ConnectPhoneForm-Dek5xn4C-EEdkKeTp.js} +1 -1
- package/site/dist/assets/ConnectWalletView-DqI-PTvX-C2mEzukj.js +74 -0
- package/site/dist/assets/{ConnectionStatusScreen-fUfROm9m-BeWLchcf.js → ConnectionStatusScreen-fUfROm9m-CH_7r8iV.js} +1 -1
- package/site/dist/assets/ConnectionStatusScreen-fUfROm9m-Z33kis0B.js +1 -0
- package/site/dist/assets/{ConnectorUtil-Cq4lWnfe.js → ConnectorUtil-DH4NePk5.js} +1 -1
- package/site/dist/assets/{CopyToClipboard-i_OQSBJr-DujUHNzW.js → CopyToClipboard-i_OQSBJr-_G88JCVR.js} +1 -1
- package/site/dist/assets/{CopyableText-CQapvaMr-DMIdpDaU.js → CopyableText-CQapvaMr-CGGfc_zM.js} +1 -1
- package/site/dist/assets/{CrossAppAuthScreen-CLw7ymZq-Csju14vF.js → CrossAppAuthScreen-CLw7ymZq-C9Okoc0V.js} +1 -1
- package/site/dist/assets/{CustomLandingScreenView-Cm_MBSyX-CBOrbNLo.js → CustomLandingScreenView-Cm_MBSyX-D2l6hdSl.js} +1 -1
- package/site/dist/assets/{DelegatedActionsConsentScreen-DYF2U4y5-BL5hzNUI.js → DelegatedActionsConsentScreen-DYF2U4y5-Dda3KKqS.js} +1 -1
- package/site/dist/assets/{DelegatedActionsRevokeScreen-bPp6Uwyh-fcO51q5H.js → DelegatedActionsRevokeScreen-bPp6Uwyh-DG8kJPt1.js} +1 -1
- package/site/dist/assets/{DepositAddressAddressStepView-4vdkb55L-B3vY888G.js → DepositAddressAddressStepView-4vdkb55L-CcOUfn6f.js} +1 -1
- package/site/dist/assets/{DepositAddressScreen-DBLRs7aI-DmdQaFI2.js → DepositAddressScreen-DBLRs7aI-DeituDNh.js} +1 -1
- package/site/dist/assets/{DepositAddressV3Screen-CDqIEMQw-BoupRyfV.js → DepositAddressV3Screen-CDqIEMQw-FDg6K14-.js} +1 -1
- package/site/dist/assets/{EmailInputForm-Cqo1mda8-B2NG3K5V.js → EmailInputForm-Cqo1mda8-BmbxvPHC.js} +1 -1
- package/site/dist/assets/{EmbeddedWalletConnectingScreen-D4T7DUzs-D_AVgO4s.js → EmbeddedWalletConnectingScreen-D4T7DUzs-B05CQ9rx.js} +1 -1
- package/site/dist/assets/{EmbeddedWalletCreatedScreen-D7WhW5ve-CLsJbPYQ.js → EmbeddedWalletCreatedScreen-D7WhW5ve-Cxd1b9XH.js} +1 -1
- package/site/dist/assets/{EmbeddedWalletKeyExportScreen-CqV4NYQ3-C9sAExzE.js → EmbeddedWalletKeyExportScreen-CqV4NYQ3-CrJy0ViX.js} +1 -1
- package/site/dist/assets/{EmbeddedWalletOnAccountCreateScreen-BoKb3Q2S-DxaWAAKa.js → EmbeddedWalletOnAccountCreateScreen-BoKb3Q2S-Bakaa3zu.js} +1 -1
- package/site/dist/assets/{EmbeddedWalletPasswordUpdateScreen-D3rMlqS7-CxrfLr5q.js → EmbeddedWalletPasswordUpdateScreen-D3rMlqS7-j78vKrcj.js} +1 -1
- package/site/dist/assets/{EmbeddedWalletPasswordUpdateSplashScreen-BPFkWJeE-DHZRGoTN.js → EmbeddedWalletPasswordUpdateSplashScreen-BPFkWJeE-D3dr7W9_.js} +1 -1
- package/site/dist/assets/{EnrollTotp-Dibew4dp-SA2pvNEc.js → EnrollTotp-Dibew4dp-BXbYV8nI.js} +1 -1
- package/site/dist/assets/{EnvelopeIcon-Dh2vSVj4.js → EnvelopeIcon-DjPofnRI.js} +1 -1
- package/site/dist/assets/{ErrorBanner-BcpGRt0h-BKOYTyCi.js → ErrorBanner-BcpGRt0h-BhH6_V-9.js} +1 -1
- package/site/dist/assets/{ErrorMessage-D8VaAP5m-CrZOnUyg.js → ErrorMessage-D8VaAP5m-Bs7uX2a9.js} +1 -1
- package/site/dist/assets/{ErrorScreen-DipdTWzE-c8g8En21.js → ErrorScreen-DipdTWzE-BgPRgnOb.js} +1 -1
- package/site/dist/assets/ErrorScreen-DipdTWzE-CvxXC6pz.js +1 -0
- package/site/dist/assets/{ExclamationCircleIcon-DUA2wMfO.js → ExclamationCircleIcon-BE-WGrut.js} +1 -1
- package/site/dist/assets/{ExclamationTriangleIcon-eZfaqcFq.js → ExclamationTriangleIcon-Ca1Xlc_f.js} +1 -1
- package/site/dist/assets/{FarcasterConnectStatusScreen-51tjUxqq-s_s5M2CZ.js → FarcasterConnectStatusScreen-51tjUxqq-De2JvZdV.js} +1 -1
- package/site/dist/assets/{FarcasterSignerStatusScreen-6x-Uh95G-BHBR4k8H.js → FarcasterSignerStatusScreen-6x-Uh95G-bLp3hXoO.js} +1 -1
- package/site/dist/assets/FiatOnrampScreen-BAhYeiTI-Jr6D6JiZ.js +491 -0
- package/site/dist/assets/{FingerPrintIcon-Dm6kxmg7.js → FingerPrintIcon-DOCbS93y.js} +1 -1
- package/site/dist/assets/{FundSolWalletWithExternalSolanaWallet-WtLwYaOl-B8TRV36j.js → FundSolWalletWithExternalSolanaWallet-WtLwYaOl-A_G5kfa1.js} +1 -1
- package/site/dist/assets/{FundWalletMethodHeader-BWNkokCT-BQdbJ5Bb.js → FundWalletMethodHeader-BWNkokCT-LPv35wkD.js} +1 -1
- package/site/dist/assets/{FundWithBankDepositScreen-DajkHxBD-9rPtyA3d.js → FundWithBankDepositScreen-DajkHxBD-0hfGrfvf.js} +1 -1
- package/site/dist/assets/{FundingEditAmountScreen-CpvpiO4M-GaXigGI9.js → FundingEditAmountScreen-CpvpiO4M-Dz0s_NWR.js} +1 -1
- package/site/dist/assets/{FundingMethodSelectionScreen-Db7vhRlE-UsmtTqv4.js → FundingMethodSelectionScreen-Db7vhRlE-ZGm-3-Io.js} +1 -1
- package/site/dist/assets/{GlobeAltIcon-vqrVV5ho.js → GlobeAltIcon-BBRl2MMn.js} +1 -1
- package/site/dist/assets/{GooglePay-B53WnudL-BX5_WtCx.js → GooglePay-B53WnudL-BGiVpB8y.js} +1 -1
- package/site/dist/assets/{HCaptchaWrapper-CUi9LJsp-D-I8j7NO.js → HCaptchaWrapper-CUi9LJsp-CHdfIeD_.js} +3 -3
- package/site/dist/assets/{InAppBrowserLoginNotPossible-BMR9br1T-BY3j50El.js → InAppBrowserLoginNotPossible-BMR9br1T-Cgoiow3m.js} +1 -1
- package/site/dist/assets/{InfoBanner-Cb3p1z12-DLBl_Ywy.js → InfoBanner-Cb3p1z12-_YqQmRiu.js} +1 -1
- package/site/dist/assets/{InjectedWalletIcon-DLcYOGDj-DLSV_NjP.js → InjectedWalletIcon-DLcYOGDj-tuIrKQHH.js} +1 -1
- package/site/dist/assets/{InstallWalletScreen-DTA8dwLv-CxreyXoD.js → InstallWalletScreen-DTA8dwLv-BMU_wa61.js} +1 -1
- package/site/dist/assets/{JsonTree-BHzNC-ic-DcNcbSoM.js → JsonTree-BHzNC-ic-DcVr0YF0.js} +1 -1
- package/site/dist/assets/{LabelXs-oqZNqbm_-iafrj25N.js → LabelXs-oqZNqbm_-CNprGVQM.js} +1 -1
- package/site/dist/assets/LandingScreen-CGWUoSUG-fH6rsQen.js +1 -0
- package/site/dist/assets/{Layouts-BMRfo5hw-CeCOyCLK.js → Layouts-BMRfo5hw-e-sepi6F.js} +1 -1
- package/site/dist/assets/{Link-BdDilT2T-BEDEdHuS.js → Link-BdDilT2T-BdwlRJch.js} +1 -1
- package/site/dist/assets/{LinkConflictScreen-C3ZEPsgl-ClWoqvEL.js → LinkConflictScreen-C3ZEPsgl-BXKN2Qm5.js} +1 -1
- package/site/dist/assets/LinkEmailScreen-dKo9frwc-Cyn7Q8p1.js +1 -0
- package/site/dist/assets/{LinkPasskeyScreen-zbrl_AI_-D72Gb0LW.js → LinkPasskeyScreen-zbrl_AI_-CWiqnwLM.js} +1 -1
- package/site/dist/assets/LinkPasskeyScreen-zbrl_AI_-YmDevJzf.js +1 -0
- package/site/dist/assets/{LinkPhoneScreen-oIUpfj0w-C0DuD14j.js → LinkPhoneScreen-oIUpfj0w-h7shW-Ay.js} +1 -1
- package/site/dist/assets/{Loader-UAZqxsnw-Cy7hEE2f.js → Loader-UAZqxsnw-DJEVblm1.js} +1 -1
- package/site/dist/assets/{LoadingSkeleton-BMsgO5PV-CiEyS5PZ.js → LoadingSkeleton-BMsgO5PV-D2atjsP5.js} +1 -1
- package/site/dist/assets/{LockClosedIcon-BgY_5v_Z.js → LockClosedIcon-9EFzWK1C.js} +1 -1
- package/site/dist/assets/{LoginFailedScreen-CN0dJV6U-DhUMiJZJ.js → LoginFailedScreen-CN0dJV6U-By3Si77a.js} +1 -1
- package/site/dist/assets/{ManualTransferScreen-CHqcuqbs-tXiZDHmN.js → ManualTransferScreen-CHqcuqbs-BAHPY8rs.js} +1 -1
- package/site/dist/assets/{MfaAuthEnrollmentFlowScreen-Dj7gPTRC-CaW7xeCC.js → MfaAuthEnrollmentFlowScreen-Dj7gPTRC-CLjffFG1.js} +1 -1
- package/site/dist/assets/{MfaAuthVerifyFlowScreen-DbFpyWMy-CeRqvx8k.js → MfaAuthVerifyFlowScreen-DbFpyWMy-BmQg1Y61.js} +1 -1
- package/site/dist/assets/{MfaEnrollmentFlowScreen-DhLnGgnr-BkN1k4lh.js → MfaEnrollmentFlowScreen-DhLnGgnr-DmBtTtlw.js} +1 -1
- package/site/dist/assets/{MfaVerifyFlowScreen-C_XPjMRP-CXSqLVbE.js → MfaVerifyFlowScreen-C_XPjMRP-4a683Bmf.js} +1 -1
- package/site/dist/assets/{ModalFooter-BCuEK_nz-c-khvG4Y.js → ModalFooter-BCuEK_nz-C_71qsaz.js} +1 -1
- package/site/dist/assets/{MoonpayStatusScreen-CzTUtokc-BRFN2eXr.js → MoonpayStatusScreen-CzTUtokc-DEHHcEf8.js} +1 -1
- package/site/dist/assets/{OAuthStatusScreen-ufbOa-da-CeiGcSia.js → OAuthStatusScreen-ufbOa-da-BjAb_WsY.js} +1 -1
- package/site/dist/assets/{OpenLink-CUpJ1mOr-DfAvJRyK.js → OpenLink-CUpJ1mOr-BLWFsPOW.js} +1 -1
- package/site/dist/assets/{PasskeySelectSignupOrLogin-xh2ScC_l-CIf5I7wU.js → PasskeySelectSignupOrLogin-xh2ScC_l-2PJTJxX-.js} +1 -1
- package/site/dist/assets/{PasskeyStatusScreen-Bla8MaOk-CLoL7QC-.js → PasskeyStatusScreen-Bla8MaOk-DCEg761l.js} +1 -1
- package/site/dist/assets/{PasswordRecoveryScreen-awGXZkQk-B9F67IYV.js → PasswordRecoveryScreen-awGXZkQk-8nVjNc8k.js} +1 -1
- package/site/dist/assets/{PhoneIcon-CvIiPwYQ.js → PhoneIcon-CZ47md80.js} +1 -1
- package/site/dist/assets/{PinInput-DbZ0b1i1-CDXZBdQx.js → PinInput-DbZ0b1i1-BrTXu3Km.js} +1 -1
- package/site/dist/assets/{PrivyPluginContext-DA-yS5vO-CtQWBiV2.js → PrivyPluginContext-DA-yS5vO-CY60yubk.js} +1 -1
- package/site/dist/assets/{QrCode-DbnZwlkH-h6Bhoe5U.js → QrCode-DbnZwlkH-CcMrWV5D.js} +2 -2
- package/site/dist/assets/{RecoveryOAuthStatusScreen-CsgtTI2x-D-A5P3PX.js → RecoveryOAuthStatusScreen-CsgtTI2x-DgeDnHOK.js} +1 -1
- package/site/dist/assets/{RecoveryPasswordCreateScreen-Be-qJ-LU-C8lTSeSr.js → RecoveryPasswordCreateScreen-Be-qJ-LU-CvJJwLhK.js} +1 -1
- package/site/dist/assets/{RecoverySelectionScreen-D1iGtpmi-P5P3plkH.js → RecoverySelectionScreen-D1iGtpmi-CADtGtSV.js} +1 -1
- package/site/dist/assets/{SIWXUtil-TUCxa7aA.js → SIWXUtil-Cpiv91-M.js} +1 -1
- package/site/dist/assets/{Screen-D30meWsY-CCeVNuWs.js → Screen-D30meWsY-vEusJ7KX.js} +1 -1
- package/site/dist/assets/{ScreenHeader-CHmc4-Lu-DjyrH7IK.js → ScreenHeader-CHmc4-Lu-BvGTLg53.js} +1 -1
- package/site/dist/assets/{ScreenLayout-OwaQQcxH-CrQM6kvA.js → ScreenLayout-OwaQQcxH-cfOTmyl0.js} +1 -1
- package/site/dist/assets/{SelectSourceAsset-CWFog6gm-D7yTw87h.js → SelectSourceAsset-CWFog6gm-DxYklZYs.js} +1 -1
- package/site/dist/assets/{SetAutomaticRecoveryScreen-BO_OEqAZ-f0iDIGmw.js → SetAutomaticRecoveryScreen-BO_OEqAZ-oeqeo05x.js} +1 -1
- package/site/dist/assets/{SetWalletPasswordForm-DwCTVtAh-kq1zvA7d.js → SetWalletPasswordForm-DwCTVtAh-BxFee9UA.js} +1 -1
- package/site/dist/assets/{ShieldCheckIcon-o6RHYev1.js → ShieldCheckIcon-eFjsosCA.js} +1 -1
- package/site/dist/assets/{SignRequestScreen-BHKv32KR-DDpMYyJ1.js → SignRequestScreen-BHKv32KR-z8ixfIma.js} +1 -1
- package/site/dist/assets/{SolanaWalletConnector-C7Lqk-Wz-DPwhuzcI.js → SolanaWalletConnector-C7Lqk-Wz-Bc_lvdiF.js} +1 -1
- package/site/dist/assets/{StackedContainer-B2vaEl56-eQCM2cmv.js → StackedContainer-B2vaEl56-Do5dXB8-.js} +1 -1
- package/site/dist/assets/{StandardSignAndSendTransactionScreen-CfhIXVtd-BJ-onCjv.js → StandardSignAndSendTransactionScreen-CfhIXVtd-DudetZAc.js} +1 -1
- package/site/dist/assets/{Subtitle-CV-2yKE4-zxWw2m1Q.js → Subtitle-CV-2yKE4-DV0o1av3.js} +1 -1
- package/site/dist/assets/{TelegramAuthScreen-UL9zLfO8-NWlMgNRX.js → TelegramAuthScreen-UL9zLfO8-Bo3sONvf.js} +1 -1
- package/site/dist/assets/{Title-BnzYV3Is-B_JpTatT.js → Title-BnzYV3Is-C3qOPnO_.js} +1 -1
- package/site/dist/assets/{TodoList-DnyULl18-C8Eyy28X.js → TodoList-DnyULl18-BvxvubbF.js} +1 -1
- package/site/dist/assets/{TransactionDetails-e9nesl30-C4MpIHR-.js → TransactionDetails-e9nesl30-WWflkOIi.js} +1 -1
- package/site/dist/assets/{TransactionErrorView-BDBNqA_f-p1T9WVMx.js → TransactionErrorView-BDBNqA_f-CotYKcgf.js} +1 -1
- package/site/dist/assets/{TransferFromWalletScreen-Qvn1Yuno-88IQcRnY.js → TransferFromWalletScreen-Qvn1Yuno-sVFlWqa8.js} +1 -1
- package/site/dist/assets/{TransferOrBridgeLoadingScreen-Dl1I_iIz-1fDAIxAf.js → TransferOrBridgeLoadingScreen-Dl1I_iIz-CV2-mY19.js} +1 -1
- package/site/dist/assets/{TurnstileWrapper-B7sNddys-Jwy1tJtm.js → TurnstileWrapper-B7sNddys-oPbuMTQt.js} +1 -1
- package/site/dist/assets/{UpdateEmailScreen-DVOhPV0H-Jj2DQW8D.js → UpdateEmailScreen-DVOhPV0H-D_mjSpAy.js} +1 -1
- package/site/dist/assets/{UpdatePhoneScreen-CXRswTNZ-DwYIUBqi.js → UpdatePhoneScreen-CXRswTNZ-6RLG0ahv.js} +1 -1
- package/site/dist/assets/{UserLimitReachedScreen-5we2DHn8-D7m70C4b.js → UserLimitReachedScreen-5we2DHn8-CaVN0bti.js} +1 -1
- package/site/dist/assets/{Value-DTgR824E-BRuIGiec.js → Value-DTgR824E-Bp6WGZZm.js} +1 -1
- package/site/dist/assets/{WalletCards-DH1rqayz-Bgp9Fsgs.js → WalletCards-DH1rqayz-D3cOTb9x.js} +1 -1
- package/site/dist/assets/{WalletIcon-gKf0gTw6.js → WalletIcon-DOIZhb2l.js} +1 -1
- package/site/dist/assets/{WalletInfoCard-sq_2wgFg-DpNSAGte.js → WalletInfoCard-sq_2wgFg-bOqfl5MA.js} +1 -1
- package/site/dist/assets/{WalletInterstitialScreen-Sh7LfsAh-DG-RuO-F.js → WalletInterstitialScreen-Sh7LfsAh-DWaVkMCA.js} +1 -1
- package/site/dist/assets/{WalletLink-DK5LB5Ha-DHaL350n.js → WalletLink-DK5LB5Ha-DjUZvqs-.js} +1 -1
- package/site/dist/assets/WalletOverflowButton-bfCnp-9T-B6P7DQXA.js +1 -0
- package/site/dist/assets/{WarningBanner-ZZqCEtZK-2eU4Qo74.js → WarningBanner-ZZqCEtZK-Bc2YKZb7.js} +1 -1
- package/site/dist/assets/__vite-browser-external-NciY4Vu1.js +1 -0
- package/site/dist/assets/{_esm-BdeM6pxJ.js → _esm-BZP3cXTv.js} +1 -1
- package/site/dist/assets/_esm-ZmzBJpEs.js +6 -0
- package/site/dist/assets/{accounts-D96UKWkC.js → accounts-D4AXF17G.js} +1 -1
- package/site/dist/assets/accounts-IkdFx9nq.js +1 -0
- package/site/dist/assets/{baseSepolia-DX-qGsik.js → baseSepolia-Bn1j1Ysw.js} +1 -1
- package/site/dist/assets/{basic-oIwwSyu4.js → basic-Cz1YiCSk.js} +4 -4
- package/site/dist/assets/{ccip-BZvRTL6g.js → ccip-B1J3oTZx.js} +2 -2
- package/site/dist/assets/ccip-Bd7261zy.js +1 -0
- package/site/dist/assets/{ccip-65sIfSPR.js → ccip-D6EHKpjI.js} +1 -1
- package/site/dist/assets/{chain-DM3oH6Px.js → chain-scefkYIH.js} +3 -6
- package/site/dist/assets/check-DHnllI8N.js +1 -0
- package/site/dist/assets/{chevron-down-BSsbXkK-.js → chevron-down-1y-r4bGO.js} +1 -1
- package/site/dist/assets/{circle-alert-DWaeLZNj.js → circle-alert-OwxamXUX.js} +1 -1
- package/site/dist/assets/{circle-check-big-Chb1ivw9.js → circle-check-big-3sZGVC10.js} +1 -1
- package/site/dist/assets/{circle-x-AV9Cet0R.js → circle-x-DQjNUV9J.js} +1 -1
- package/site/dist/assets/client-B-BpRtwY.js +1 -0
- package/site/dist/assets/client-BfIR1FvH.js +9 -0
- package/site/dist/assets/coinbase-DCPC2L_R-YEuIn3jh.js +3 -0
- package/site/dist/assets/{copy-Ckr0AkL6.js → copy-DMOX_yn7.js} +1 -1
- package/site/dist/assets/core-GIEmxmAa.js +12 -0
- package/site/dist/assets/{createLucideIcon-IPW50ckY.js → createLucideIcon-BsERoFTA.js} +1 -1
- package/site/dist/assets/{defineChain-CiuCXUZW.js → defineChain-B-SQDLTG.js} +1 -1
- package/site/dist/assets/{dijkstra-Drfl93V4.js → dijkstra-CY95afk4.js} +1 -1
- package/site/dist/assets/dist-Cp6IXeJd.js +1 -0
- package/site/dist/assets/{dist-Cm7pNAtL.js → dist-CrNxmdZj.js} +1 -1
- package/site/dist/assets/{dist-DnGANz51.js → dist-DYGtf4F0.js} +4 -4
- package/site/dist/assets/embed.worker-Cnl4VGr3.js +1 -0
- package/site/dist/assets/{errors-BQ3N-RVt-BLqjTPNR.js → errors-BQ3N-RVt-CZPAWCgC.js} +1 -1
- package/site/dist/assets/{esm-B5cbIQaz.js → esm-Co94ENUV.js} +17 -17
- package/site/dist/assets/{esm-ByALO6mA.js → esm-bb8SANZh.js} +1 -1
- package/site/dist/assets/{ethers-zTjbodk5-wHhL_BQ1.js → ethers-zTjbodk5-Cs648CnD.js} +1 -1
- package/site/dist/assets/{eventemitter3-CL76jPsW.js → eventemitter3-Dzc_0Uq2.js} +1 -1
- package/site/dist/assets/{external-link-D-uehwM2.js → external-link-BBPG3rG7.js} +1 -1
- package/site/dist/assets/{farcaster-DPlSjvF5-BqTvDXBR.js → farcaster-DPlSjvF5-CzBCmgyL.js} +1 -1
- package/site/dist/assets/{features-DQ0UYy6K.js → features-CuiHbgEJ.js} +1 -1
- package/site/dist/assets/{fingerprint-pattern-D8ytA16Q.js → fingerprint-pattern-CjLtyIxB.js} +1 -1
- package/site/dist/assets/floating-ui.react-DmaCfayr.js +1 -0
- package/site/dist/assets/{floating-ui.react-dom-DKWOL9UG.js → floating-ui.react-dom-CaoMF8mP.js} +1 -1
- package/site/dist/assets/{get-entropy-details-for-user-dHRH5Gpi-Dukf7_kJ.js → get-entropy-details-for-user-dHRH5Gpi-BNiOFXbK.js} +1 -1
- package/site/dist/assets/{getErc20Balance-jXNpFf0N-Bb9WM31M.js → getErc20Balance-jXNpFf0N-q4bV8kQq.js} +1 -1
- package/site/dist/assets/{getErc20TokenInfo-BNTrzLMP-KPrdn04K.js → getErc20TokenInfo-BNTrzLMP-DcnTFrXN.js} +1 -1
- package/site/dist/assets/{guards-BIW-GiMJ-CHfXo8XI.js → guards-BIW-GiMJ-D3tWTQAR.js} +1 -1
- package/site/dist/assets/{hourglass-CPG1HZmf.js → hourglass-D_GfQdJr.js} +1 -1
- package/site/dist/assets/{id-UqEwL2BI-BkKm533U.js → id-UqEwL2BI-CTlvRMA5.js} +1 -1
- package/site/dist/assets/{index-BdEw5wyl-ChSJvPZk.js → index-BdEw5wyl-kLbhqLbv.js} +1 -1
- package/site/dist/assets/index-C2h3EneX.js +8 -0
- package/site/dist/assets/{index-CWARkn2w-Cezl_uBr.js → index-CWARkn2w-DxuckIFn.js} +1 -1
- package/site/dist/assets/{index-DvMddJcd-BatvgC-S.js → index-DvMddJcd-CNBb4VUn.js} +1 -1
- package/site/dist/assets/{index-YiCw0Y_x-CX5i3Z87.js → index-YiCw0Y_x-Czl0tWfF.js} +5 -6
- package/site/dist/assets/{index.es-BmqyoBEM.js → index.es-Crd0TyUS.js} +1 -1
- package/site/dist/assets/index.es-DQbup1WG.js +3 -0
- package/site/dist/assets/{isPaymentRequestAvailable-Bq1cemEn-By6r0dVB.js → isPaymentRequestAvailable-Bq1cemEn-Cjf3l05I.js} +1 -1
- package/site/dist/assets/{jsx-runtime-BbDfbRii.js → jsx-runtime-CndCZlyt.js} +1 -1
- package/site/dist/assets/{landmark-CH6LV6F0.js → landmark-BZPqMFEf.js} +1 -1
- package/site/dist/assets/lib-DGtzi2DK.js +1 -0
- package/site/dist/assets/{localBatchGatewayRequest-3kLrX_c7.js → localBatchGatewayRequest-B71J8sQ9.js} +7 -10
- package/site/dist/assets/{lock-DcskI4Bb.js → lock-DyN_c-ud.js} +1 -1
- package/site/dist/assets/{mail-Bs0r5yOi.js → mail-V_mcPb64.js} +1 -1
- package/site/dist/assets/{modal-context-CwmSeFcv-DruX9J1E.js → modal-context-CwmSeFcv-DPWNJrfK.js} +1 -1
- package/site/dist/assets/{ofetch.CWycOUEr-CHp-crOQ.js → ofetch.CWycOUEr-Duby3iHT.js} +1 -1
- package/site/dist/assets/parseEther-CHS0GNkM.js +1 -0
- package/site/dist/assets/{pkce-DXF0yKjP-ChQhE0st.js → pkce-DXF0yKjP-7_VY-d83.js} +1 -1
- package/site/dist/assets/preload-helper-CRzLMJPS.js +1 -0
- package/site/dist/assets/{privy-context-81KmsBw2-CjWGzyg7.js → privy-context-81KmsBw2-DiR6-Yq8.js} +1 -1
- package/site/dist/assets/{react-DrJ0MmMO.js → react-BfYA1q0G.js} +1 -1
- package/site/dist/assets/react-CK2HRI1t.js +1 -0
- package/site/dist/assets/{react-DQnMl2Xv.js → react-DxGeY8B8.js} +1 -1
- package/site/dist/assets/{react-dom-5dW7aKoP.js → react-dom-BI2Koe4o.js} +1 -1
- package/site/dist/assets/{reservoir-x-nGuZkT-BMX127i6.js → reservoir-x-nGuZkT-BguVFH3D.js} +1 -1
- package/site/dist/assets/roles-BDPDWrwo.js +1 -0
- package/site/dist/assets/rolldown-runtime-Bjp52BOI.js +1 -0
- package/site/dist/assets/secp256k1-BeYL8aZl.js +1 -0
- package/site/dist/assets/{secp256k1-_MleGpqL.js → secp256k1-Bh4MSkhz.js} +1 -1
- package/site/dist/assets/secp256k1-Ci4cqHl0.js +1 -0
- package/site/dist/assets/secp256k1-DsxXqbXv.js +1 -0
- package/site/dist/assets/secp256k1-N9RUmafO.js +1 -0
- package/site/dist/assets/{serializeAuthorizationList-CX8hGL56.js → serializeAuthorizationList-DGMik9Wx.js} +1 -1
- package/site/dist/assets/{serializeSignature-DfxkPnu-.js → serializeSignature-CWKpxoab.js} +1 -1
- package/site/dist/assets/sha2-C6s7zZmb.js +1 -0
- package/site/dist/assets/{shared-DRZUmnL5-xqAUARWo.js → shared-DRZUmnL5-BewE5wK8.js} +1 -1
- package/site/dist/assets/{shared-FM0rljBt--Avg6q2R.js → shared-FM0rljBt-CxLACri_.js} +1 -1
- package/site/dist/assets/shouldProceedtoEmbeddedWalletCreationFlow-B3vyavrh-BkAKsRNi.js +1 -0
- package/site/dist/assets/{simulateTransaction-CfuOrae4-64lWYJAL.js → simulateTransaction-CfuOrae4-PjA_tGjX.js} +1 -1
- package/site/dist/assets/{smartphone-CcEZ-XAy.js → smartphone-3p8nFSTE.js} +1 -1
- package/site/dist/assets/{storage-Ufl800Ne-D0bZBw-H.js → storage-Ufl800Ne-1qCR2JMO.js} +7 -8
- package/site/dist/assets/{stringify-BlDm7tuc.js → stringify-DqUj9VzO.js} +2 -3
- package/site/dist/assets/stripe.esm-DEi2D291.js +2 -0
- package/site/dist/assets/{styles-BsotlekN-CXEB4Ud9.js → styles-BsotlekN-Aq27nZGc.js} +1 -1
- package/site/dist/assets/{styles-Cm9H63WO-Br4QtzUL.js → styles-Cm9H63WO-p_ic_dxE.js} +1 -1
- package/site/dist/assets/{styles-DVyDvTdj-BTABn2K8.js → styles-DVyDvTdj-CjsWqAld.js} +1 -1
- package/site/dist/assets/{styles-hslXy6xG-D-49b1rk.js → styles-hslXy6xG-D5QHIrJU.js} +1 -1
- package/site/dist/assets/{telegram-B-JqnkqZ-RBoY3PLx.js → telegram-B-JqnkqZ-Dqkm4rDn.js} +1 -1
- package/site/dist/assets/{to-ui-error-DWe-qwS3-Cx2bk1kZ.js → to-ui-error-DWe-qwS3-Bi1HUQI9.js} +1 -1
- package/site/dist/assets/toViemAccount-B5iDGq1b-CdFZwv07.js +2 -0
- package/site/dist/assets/transformers.web-BTyOEJ0C.js +40 -0
- package/site/dist/assets/transformers.web-CUZUFjAK.js +49 -0
- package/site/dist/assets/{triangle-alert-CWjYH75H.js → triangle-alert-BG3Qw-Hq.js} +1 -1
- package/site/dist/assets/{twitch-5IOe4sIQ-lDtgk0os.js → twitch-5IOe4sIQ-CaXHFlAu.js} +1 -1
- package/site/dist/assets/{use-headless-crypto-deposit-Yn-QRLB3-LUIVFBSd.js → use-headless-crypto-deposit-Yn-QRLB3-CQqeyhGY.js} +1 -1
- package/site/dist/assets/{use-unlink-wallet-BKK4_5Ur-CC2pq27z.js → use-unlink-wallet-BKK4_5Ur-C0xQN_Lp.js} +1 -1
- package/site/dist/assets/{useActiveWallet-B-yZYGc6-D7Ik-LxJ.js → useActiveWallet-B-yZYGc6-BkK6rFBm.js} +1 -1
- package/site/dist/assets/{useGetSolPrice-x7gfUIHJ-D1hmQfpA.js → useGetSolPrice-x7gfUIHJ-CLVXqv6I.js} +1 -1
- package/site/dist/assets/{useGetTokenPrice-CaoUkb79-ZnbuJ-vP.js → useGetTokenPrice-CaoUkb79-DAXXs3rs.js} +1 -1
- package/site/dist/assets/{useI18n-C-5n_noF-BcqC76yj.js → useI18n-C-5n_noF-CGpkgIVy.js} +1 -1
- package/site/dist/assets/{useWalletBalance-CZnP5f_f-Dw2GIJ4g.js → useWalletBalance-CZnP5f_f-Cfd64m4n.js} +1 -1
- package/site/dist/assets/{useWallets-X1ndvJ_U-DRjz7uYD.js → useWallets-X1ndvJ_U-CAYqg7Za.js} +1 -1
- package/site/dist/assets/{w3m-modal-BeVrBOIf.js → w3m-modal-B4H_uiPH.js} +2 -2
- package/site/dist/assets/{wallet-Bn85Sg2W.js → wallet-BNI12IYk.js} +1 -1
- package/site/dist/assets/wallet-CITSaPlB.js +2 -0
- package/site/dist/assets/{wallet-connect-BvJtUsDt-Y03hhqFN.js → wallet-connect-BvJtUsDt-CVlSNIXP.js} +1 -1
- package/site/dist/assets/{walletconnect-registry-xxG8XLCb-D1SSFvMp.js → walletconnect-registry-xxG8XLCb-BpzpBq5s.js} +1 -1
- package/site/dist/assets/walletconnect-v2-3frQFwbg-CzZ6oybX.js +12 -0
- package/site/dist/assets/{wui-ux-by-reown-D34ILNLS.js → wui-ux-by-reown-XyHdtV1O.js} +1 -1
- package/site/dist/assets/x-gjDrY2rk.js +1 -0
- package/site/dist/index.html +5 -5
- package/site/feed.js +16 -1
- package/site/no-model.js +4 -0
- package/site/worker.js +586 -59
- package/src/agent/agent-card.js +20 -11
- package/src/agent/mcp.js +190 -324
- package/src/agent/view-tools.js +46 -13
- package/src/agent/x402.js +340 -44
- package/src/core/embed.js +51 -52
- package/src/core/embed.worker.js +1 -1
- package/src/core/shard.js +7 -3
- package/src/discover/apps.js +41 -5
- package/src/discover/explore.js +50 -30
- package/src/market/reactions.js +2 -2
- package/src/publish/devnet.js +95 -0
- package/src/publish/init.js +74 -0
- package/src/publish/lint.js +2 -1
- package/src/publish/ship.js +559 -267
- package/src/publish/view-cli.js +14 -10
- package/src/publish/view.js +355 -218
- package/src/taste/taste-doc.js +3 -2
- package/src/taste/taste.js +2 -2
- package/src/taste/vault.js +3 -1
- package/template/README.md +49 -0
- package/template/app.json +27 -0
- package/template/eval/goals.md +7 -0
- package/template/eval/golden.jsonl +0 -0
- package/template/gitignore +3 -0
- package/template/package.json +13 -0
- package/template/wrangler.json +12 -0
- package/site/dist/assets/BridgeNetworkSelectionView-CFGjB2nO-CvsbQcFv.js +0 -68
- package/site/dist/assets/ConnectOrCreateScreen-CUUYUO07-crHzh-TL.js +0 -1
- package/site/dist/assets/ConnectWalletView-DqI-PTvX-CMQIad63.js +0 -74
- package/site/dist/assets/ConnectionStatusScreen-fUfROm9m-wu3a3kw4.js +0 -1
- package/site/dist/assets/ErrorScreen-DipdTWzE-DcxVjMO4.js +0 -1
- package/site/dist/assets/FiatOnrampScreen-BAhYeiTI-BH_sU6EV.js +0 -491
- package/site/dist/assets/LandingScreen-CGWUoSUG-DK1EY-6v.js +0 -1
- package/site/dist/assets/LinkEmailScreen-dKo9frwc-C3v7glH0.js +0 -1
- package/site/dist/assets/LinkPasskeyScreen-zbrl_AI_-DYIl5Kr2.js +0 -1
- package/site/dist/assets/WalletOverflowButton-bfCnp-9T-CUyIJFzt.js +0 -1
- package/site/dist/assets/__vite-browser-external-CitKSmYk.js +0 -1
- package/site/dist/assets/_esm-B26FZg5Y.js +0 -7
- package/site/dist/assets/accounts-DiZ-0VmN.js +0 -1
- package/site/dist/assets/ccip-DA_zG341.js +0 -1
- package/site/dist/assets/check-BPgcwEPP.js +0 -1
- package/site/dist/assets/client-CZG0ndth.js +0 -2
- package/site/dist/assets/client-Dsa7_3V3.js +0 -9
- package/site/dist/assets/coinbase-DCPC2L_R-D3aIgpyd.js +0 -3
- package/site/dist/assets/core-snws5xwv.js +0 -12
- package/site/dist/assets/dist-Dl07uGq1.js +0 -1
- package/site/dist/assets/embed.worker-ugGhEqCV.js +0 -1
- package/site/dist/assets/floating-ui.react-xRkl9O3u.js +0 -1
- package/site/dist/assets/index-1HYFYA-W.js +0 -8
- package/site/dist/assets/index.es-C1v1or--.js +0 -3
- package/site/dist/assets/lib-BsS8z8Lj.js +0 -1
- package/site/dist/assets/parseEther-VjhVo3dd.js +0 -1
- package/site/dist/assets/preload-helper-uBIymjUX.js +0 -1
- package/site/dist/assets/react-DCXkXSrv.js +0 -1
- package/site/dist/assets/roles-32M1l9jI.js +0 -1
- package/site/dist/assets/rolldown-runtime-BcKkbAw3.js +0 -1
- package/site/dist/assets/secp256k1-C4nzcPo-.js +0 -1
- package/site/dist/assets/secp256k1-D92aGNnU.js +0 -1
- package/site/dist/assets/secp256k1-DmbwyFau.js +0 -1
- package/site/dist/assets/secp256k1-xl2og-wO.js +0 -1
- package/site/dist/assets/sha2-BU_QyRTx.js +0 -1
- package/site/dist/assets/shouldProceedtoEmbeddedWalletCreationFlow-B3vyavrh-BK_FBkaE.js +0 -1
- package/site/dist/assets/stripe.esm-CZRDpc-N.js +0 -2
- package/site/dist/assets/toViemAccount-B5iDGq1b-Ba8L7zed.js +0 -2
- package/site/dist/assets/transformers.web-CVP3pJOS.js +0 -40
- package/site/dist/assets/transformers.web-D8W1u_I2.js +0 -49
- package/site/dist/assets/wallet-Bnzeiqed.js +0 -2
- package/site/dist/assets/walletconnect-v2-3frQFwbg-CrtLXofr.js +0 -12
- package/site/dist/assets/x-rlSp9U7W.js +0 -1
package/site/worker.js
CHANGED
|
@@ -1,38 +1,219 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
1
|
+
// A Fangorn app on the free tier: one Cloudflare Worker that is the app's indexer, its view,
|
|
2
|
+
// its MCP server and its site. `westmarch-ship` deploys it with the app's CONFIG.
|
|
3
3
|
//
|
|
4
|
+
// cron the indexer. Follows the update log from where it left off, and runs
|
|
5
|
+
// the embedding lens (view.js, the Worker's AI binding as the encoder)
|
|
6
|
+
// over every member's repo whose head moved, into R2. A repo whose
|
|
7
|
+
// publisher leaves the app, or that the app excludes, loses its domain
|
|
8
|
+
// on the next run.
|
|
9
|
+
// GET /view/… the view, from R2: catalog, manifests, content-named shards
|
|
10
|
+
// POST /mcp the view as MCP tools (describe, search, get, similar, count, browse,
|
|
11
|
+
// threads) over streamable HTTP: no install, no browser
|
|
4
12
|
// GET /doc?u=<url> a record's source document, served from this origin so the page can
|
|
5
13
|
// show it inline. Source sites forbid framing and send no CORS, so the
|
|
6
14
|
// page cannot fetch or frame them itself. Only hosts the app's own
|
|
7
|
-
// records link to (
|
|
8
|
-
// GET /paid/<item id> with CONFIG.paid: 402 + the price; with a valid
|
|
9
|
-
// facilitator verifies and settles it, then
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
15
|
+
// records link to (each domain's `link_hosts`), only PDFs, cached a day.
|
|
16
|
+
// GET /paid/<item id> with CONFIG.paid: 402 + the price (x402 v2); with a valid
|
|
17
|
+
// PAYMENT-SIGNATURE header, the facilitator verifies and settles it, then
|
|
18
|
+
// the record's KEY is returned
|
|
19
|
+
// POST /paid {ids} many keys for one price, paid by one payment or several (vouchers).
|
|
20
|
+
// Its 402 carries the Bazaar extension, so the facilitator lists it on
|
|
21
|
+
// the first sale; /openapi.json and /.well-known/x402 name it for x402scan
|
|
22
|
+
// anything else the static site (the stock page, the card)
|
|
13
23
|
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
24
|
+
// The paid records are not here. Each is already in the app's committed rows on IPFS,
|
|
25
|
+
// encrypted (`paid_ct`, AES-256-GCM), with the sha256 of its plaintext beside it. This worker
|
|
26
|
+
// sells only the key, HMAC-SHA256(PAID_SECRET, id): the buyer holds the ciphertext before
|
|
27
|
+
// paying, so nobody pays for a record that does not exist, and decrypts and checks it after.
|
|
28
|
+
//
|
|
29
|
+
// Bindings: VIEW (R2), AI (Workers AI), ASSETS (the site), CONFIG (a JSON var: app, name,
|
|
30
|
+
// description, fromBlock, schema, repos, paid, deployment), and optionally RPC_URL and IPFS_GATEWAY
|
|
31
|
+
// (vars) and PAID_SECRET (a secret).
|
|
32
|
+
|
|
33
|
+
import { Fangorn, FangornConfig, SupportedNetworks, repoExcluded } from "@fangorn-network/sdk";
|
|
34
|
+
import { createPublicClient, http } from "viem";
|
|
35
|
+
import { generatePrivateKey } from "viem/accounts";
|
|
36
|
+
import { EMBED_MODEL, DOCUMENT, QUERY, matryoshka } from "../src/core/embed.js";
|
|
37
|
+
import { configure, resetShard } from "../src/core/shard.js";
|
|
38
|
+
import { publishView, r2Store } from "../src/publish/view.js";
|
|
39
|
+
import { viewTools } from "../src/agent/view-tools.js";
|
|
40
|
+
|
|
41
|
+
let CONFIG = { paid: null };
|
|
42
|
+
|
|
43
|
+
// ── the lens's encoder ────────────────────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
const AI_MODEL = "@cf/google/embeddinggemma-300m";
|
|
46
|
+
/** What a view built here records: the same model as embed.js, another runtime of it. */
|
|
47
|
+
export const WORKER_EMBEDDER = { runtime: "workers-ai", model: EMBED_MODEL, dtype: "workers-ai" };
|
|
48
|
+
// EMBED_URL: an encoder answering as the AI binding does ({text} → {data}), on a devnet, so
|
|
49
|
+
// nothing leaves the machine. Its views say so: {runtime, model, dtype} come back with it.
|
|
50
|
+
const ai = (env) => env.EMBED_URL
|
|
51
|
+
? async (body) => (await fetch(env.EMBED_URL, { method: "POST", body: JSON.stringify(body) })).json()
|
|
52
|
+
: (body) => env.AI.run(AI_MODEL, body);
|
|
53
|
+
const embedderOf = async (env) => env.EMBED_URL ? (await (await fetch(env.EMBED_URL)).json()) : WORKER_EMBEDDER;
|
|
54
|
+
const encode = (env, prompt) => async (texts) =>
|
|
55
|
+
(await ai(env)({ text: texts.map((t) => prompt + t) })).data.map((v) => matryoshka(v));
|
|
56
|
+
|
|
57
|
+
// ── the indexer ───────────────────────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
const STATE = "state.json", LOCK = "index.lock";
|
|
60
|
+
// A run past this is presumed dead and its lock taken over. Cron runs get 15 minutes.
|
|
61
|
+
const LOCK_MS = 14 * 60_000;
|
|
62
|
+
|
|
63
|
+
// CONFIG.network: the SDK's network name ("baseSepolia"); absent, its default.
|
|
64
|
+
const network = () => SupportedNetworks[CONFIG.network] ?? FangornConfig;
|
|
65
|
+
|
|
66
|
+
function reader(env) {
|
|
67
|
+
// CONFIG.deployment: another deployment's addresses (and appRegistryFromBlock as a string),
|
|
68
|
+
// for a testnet fork or a local chain. Absent, the network's.
|
|
69
|
+
const d = CONFIG.deployment ?? {};
|
|
70
|
+
const config = { ...network(), ...d, ...(d.appRegistryFromBlock ? { appRegistryFromBlock: BigInt(d.appRegistryFromBlock) } : {}),
|
|
71
|
+
...(env.RPC_URL ? { rpcUrl: env.RPC_URL } : {}) };
|
|
72
|
+
const fangorn = Fangorn.create({ privateKey: generatePrivateKey(), config, appId: CONFIG.app,
|
|
73
|
+
storage: { signedUrl: env.IPFS_GATEWAY ? { gateway: env.IPFS_GATEWAY } : {} } });
|
|
74
|
+
return { fangorn, chain: createPublicClient({ chain: config.chain, transport: http(config.rpcUrl) }) };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* What the app presents now, kept across runs in R2: each run reads only the updates since
|
|
79
|
+
* the last, then asks which repos the app presents: its members' (`presents`), less what it
|
|
80
|
+
* excludes. Neither membership nor exclusions move a head, so they are read, not followed.
|
|
81
|
+
* ponytail: reads every publisher's updates, and 4 contract reads per repo per run; follow
|
|
82
|
+
* the app's members and ExclusionChanged events when an app holds enough repos to show it.
|
|
83
|
+
*/
|
|
84
|
+
export async function presented({ fangorn, chain }, state) {
|
|
85
|
+
const to = await chain.getBlockNumber();
|
|
86
|
+
const from = state.block == null ? BigInt(CONFIG.fromBlock ?? network().appRegistryFromBlock) : BigInt(state.block) + 1n;
|
|
87
|
+
const app = fangorn.getAppId();
|
|
88
|
+
const registry = fangorn.getRegistry();
|
|
89
|
+
if (from <= to) for (const e of await registry.log({}, from, to))
|
|
90
|
+
state.repos[`${e.publisher.toLowerCase()}/${e.repo}`] = { owner: e.publisher, repo: e.repo, root: e.head };
|
|
91
|
+
state.block = String(to);
|
|
92
|
+
const all = Object.values(state.repos);
|
|
93
|
+
const kept = await Promise.all(all.map(async (r) => await registry.presents(app, r.owner)
|
|
94
|
+
&& !(await repoExcluded(registry, app, { publisher: r.owner, repo: r.repo }))));
|
|
95
|
+
return all.filter((_, i) => kept[i]);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export async function index(env, { log = console.log } = {}) {
|
|
99
|
+
const lock = await env.VIEW.get(LOCK);
|
|
100
|
+
if (lock && Date.now() - Number(await lock.text()) < LOCK_MS) return log("index: a run is in progress; skipped");
|
|
101
|
+
await env.VIEW.put(LOCK, String(Date.now()));
|
|
102
|
+
try {
|
|
103
|
+
const state = (await (await env.VIEW.get(STATE))?.json()) ?? { block: null, repos: {} };
|
|
104
|
+
const r = reader(env);
|
|
105
|
+
// Imported here: x402.js's self-check imports this module, and a cycle at load would deadlock it.
|
|
106
|
+
const { openRow } = await import("../src/agent/x402.js");
|
|
107
|
+
const report = await publishView({
|
|
108
|
+
fangorn: r.fangorn, app: CONFIG.app, schema: CONFIG.schema, repos: CONFIG.repos, store: r2Store(env.VIEW),
|
|
109
|
+
presented: await presented(r, state), embedBatch: encode(env, DOCUMENT), embedder: await embedderOf(env), log,
|
|
110
|
+
...(CONFIG.private ? { open: (p) => openRow(env.PAID_SECRET, p) } : {}),
|
|
111
|
+
});
|
|
112
|
+
await env.VIEW.put(STATE, JSON.stringify(state));
|
|
113
|
+
log(`index: through block ${state.block}, ${Object.keys(report).length} domain(s) changed`);
|
|
114
|
+
return report;
|
|
115
|
+
} finally {
|
|
116
|
+
await env.VIEW.delete(LOCK);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
16
119
|
|
|
17
|
-
|
|
120
|
+
// ── the view, from R2 ─────────────────────────────────────────────────────────
|
|
121
|
+
|
|
122
|
+
const CONTENT = { ".gz": "application/gzip" };
|
|
123
|
+
async function viewFile(env, path) {
|
|
124
|
+
const o = await env.VIEW.get(`view/${path}`);
|
|
125
|
+
if (!o) return new Response("not found", { status: 404, headers: CORS });
|
|
126
|
+
const type = CONTENT[path.slice(path.lastIndexOf("."))] ?? "application/json";
|
|
127
|
+
// Shards are content-named, so a reader's browser keeps one for good: a change or a
|
|
128
|
+
// retraction writes new names. `private`: kept out of shared caches, so a retracted
|
|
129
|
+
// record's shard stops being served the moment the indexer deletes it.
|
|
130
|
+
const cache = path.includes("/shards/") ? "private, max-age=31536000, immutable" : "no-cache";
|
|
131
|
+
return new Response(o.body, { headers: { "content-type": type, "cache-control": cache, ...CORS } });
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ── MCP over streamable HTTP ──────────────────────────────────────────────────
|
|
135
|
+
//
|
|
136
|
+
// Stateless: every POST is one JSON-RPC message, answered with one JSON body. The tools are
|
|
137
|
+
// view-tools.js's, reading the view straight from R2, ranking by a query embedded here by the
|
|
138
|
+
// same runtime that embedded the rows. They are rebuilt when the indexer writes a new catalog.
|
|
18
139
|
|
|
19
|
-
|
|
140
|
+
let mcp = null;
|
|
141
|
+
async function tools(env, origin) {
|
|
142
|
+
const catalog = await env.VIEW.get("view/cdn/catalog");
|
|
143
|
+
const at = catalog?.uploaded?.getTime() ?? 0;
|
|
144
|
+
if (mcp?.at !== at) {
|
|
145
|
+
resetShard();
|
|
146
|
+
configure({ fetch: (url) => viewFile(env, new URL(url).pathname.replace(/^\/view\//, "")) });
|
|
147
|
+
const qv = encode(env, QUERY);
|
|
148
|
+
mcp = { at, tools: viewTools({ name: CONFIG.name ?? CONFIG.app, desc: CONFIG.description ?? "", views: [`${origin}/view`] },
|
|
149
|
+
{ fetchCatalog: (url) => viewFile(env, new URL(url).pathname.replace(/^\/view\//, "")),
|
|
150
|
+
queryVector: async (q) => (await qv([q]).catch(() => [null]))[0] }) };
|
|
151
|
+
// viewTools starts its catalog survey unawaited. workerd cancels a request's I/O when
|
|
152
|
+
// it returns, so a survey begun under tools/list never settles and every later call
|
|
153
|
+
// waits on it forever. Finish it here, in the request that started it.
|
|
154
|
+
await mcp.tools.call("describe").catch(() => {});
|
|
155
|
+
}
|
|
156
|
+
return mcp.tools;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export async function mcpCall(env, origin, msg, request = null) {
|
|
160
|
+
const ok = (result) => ({ jsonrpc: "2.0", id: msg.id, result });
|
|
161
|
+
switch (msg.method) {
|
|
162
|
+
case "initialize": return ok({
|
|
163
|
+
protocolVersion: msg.params?.protocolVersion ?? "2025-06-18",
|
|
164
|
+
capabilities: { tools: { listChanged: false } },
|
|
165
|
+
serverInfo: { name: CONFIG.name ?? CONFIG.app ?? "fangorn-app", version: "0.1.0" },
|
|
166
|
+
instructions: `${CONFIG.description ?? ""} Call describe first, then search.`.trim(),
|
|
167
|
+
});
|
|
168
|
+
case "ping": return ok({});
|
|
169
|
+
case "tools/list": {
|
|
170
|
+
const t = await tools(env, origin), X = CONFIG.private;
|
|
171
|
+
const price = X && `${Number(X.call) / 10 ** (X.decimals ?? 6)} ${X.symbol ?? "USDC"}`;
|
|
172
|
+
return ok({ tools: (t ? t.tools : []).map((d) => !X || FREE_TOOLS.has(d.name) ? d
|
|
173
|
+
: { ...d, description: `${d.description} Paid: ${price} a call over x402, or a pass from POST ${origin}/pass.` }) });
|
|
174
|
+
}
|
|
175
|
+
case "tools/call": {
|
|
176
|
+
const t = await tools(env, origin), name = msg.params?.name;
|
|
177
|
+
const run = async () => {
|
|
178
|
+
try { return await t.call(name, msg.params?.arguments ?? {}); }
|
|
179
|
+
catch (e) { return { isError: true, content: [{ type: "text", text: e?.message ?? String(e) }] }; }
|
|
180
|
+
};
|
|
181
|
+
const tool = t.tools.find((d) => d.name === name);
|
|
182
|
+
if (!CONFIG.private || !tool || FREE_TOOLS.has(name) || (request && await passOf(env, request))) return ok(await run());
|
|
183
|
+
return ok(await paidCall(origin, tool, msg.params?._meta?.["x402/payment"], run));
|
|
184
|
+
}
|
|
185
|
+
default: return { jsonrpc: "2.0", id: msg.id, error: { code: -32601, message: `unknown method ${msg.method}` } };
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async function serveMcp(request, env, url) {
|
|
190
|
+
if (request.method !== "POST") return new Response("POST JSON-RPC here (MCP streamable HTTP)", { status: 405, headers: { allow: "POST", ...CORS } });
|
|
191
|
+
let msg;
|
|
192
|
+
try { msg = await request.json(); } catch { return json({ jsonrpc: "2.0", id: null, error: { code: -32700, message: "not JSON" } }, 400); }
|
|
193
|
+
if (Array.isArray(msg)) return json({ jsonrpc: "2.0", id: null, error: { code: -32600, message: "one message per request" } }, 400);
|
|
194
|
+
if (msg.id === undefined) return new Response(null, { status: 202, headers: CORS }); // a notification
|
|
195
|
+
return json(await mcpCall(env, url.origin, msg, request));
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const CORS = { "access-control-allow-origin": "*", "access-control-allow-headers": "PAYMENT-SIGNATURE, authorization, content-type",
|
|
199
|
+
"access-control-expose-headers": "PAYMENT-REQUIRED, PAYMENT-RESPONSE" };
|
|
20
200
|
const json = (o, status = 200, headers = {}) =>
|
|
21
201
|
new Response(JSON.stringify(o), { status, headers: { "content-type": "application/json", ...CORS, ...headers } });
|
|
22
202
|
const note = (msg, status) => new Response(`<!doctype html><meta charset="utf-8"><body style="font:15px system-ui;padding:24px;color:#555">${msg}</body>`,
|
|
23
203
|
{ status, headers: { "content-type": "text/html; charset=utf-8" } });
|
|
24
204
|
|
|
25
|
-
async function doc(request, url) {
|
|
205
|
+
async function doc(request, env, url) {
|
|
26
206
|
let src;
|
|
27
207
|
try { src = new URL(url.searchParams.get("u") ?? ""); } catch { return note("No document named.", 400); }
|
|
28
|
-
|
|
208
|
+
const hosts = new Set(((await (await env.VIEW.get("view/cdn/catalog"))?.json())?.domains ?? []).flatMap((d) => d.link_hosts ?? []));
|
|
209
|
+
if (src.protocol !== "https:" || !hosts.has(src.hostname)) return note("Not one of this app's sources.", 403);
|
|
29
210
|
const cache = caches.default, key = new Request(url.href);
|
|
30
211
|
const hit = await cache.match(key);
|
|
31
212
|
if (hit) return hit;
|
|
32
213
|
const up = await fetch(src.href, { headers: { "user-agent": "Mozilla/5.0 (compatible; fangorn-app/0.1; +https://fangorn.network)" }, redirect: "follow" });
|
|
33
214
|
// A redirect may not leave the allowed hosts, and only a PDF is shown: an HTML page from
|
|
34
215
|
// someone else's site, served from this origin, would run as this app.
|
|
35
|
-
if (!
|
|
216
|
+
if (!hosts.has(new URL(up.url).hostname)) return note("The source redirected elsewhere.", 403);
|
|
36
217
|
if (!up.ok || !(up.headers.get("content-type") ?? "").includes("pdf"))
|
|
37
218
|
return note(`This document is not a PDF. <a href="${src.href.replace(/"/g, """)}" target="_blank" rel="noopener">Open it on the source site</a>.`, 415);
|
|
38
219
|
const res = new Response(up.body, { headers: { "content-type": "application/pdf", "content-disposition": "inline",
|
|
@@ -41,65 +222,411 @@ async function doc(request, url) {
|
|
|
41
222
|
return res;
|
|
42
223
|
}
|
|
43
224
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
225
|
+
/** A record's key: HMAC-SHA256(PAID_SECRET, id), hex. The same derivation as the sources that
|
|
226
|
+
* encrypt (and x402.js's paidKey). */
|
|
227
|
+
async function keyFor(env, id) {
|
|
228
|
+
if (!env.PAID_SECRET) throw new Error("PAID_SECRET is not set (wrangler pages secret put PAID_SECRET)");
|
|
229
|
+
const k = await crypto.subtle.importKey("raw", new TextEncoder().encode(env.PAID_SECRET), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
|
|
230
|
+
const mac = new Uint8Array(await crypto.subtle.sign("HMAC", k, new TextEncoder().encode(id)));
|
|
231
|
+
return [...mac].map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
47
232
|
}
|
|
233
|
+
const keysFor = async (env, ids) => Object.fromEntries(await Promise.all(ids.map(async (id) => [id, await keyFor(env, id)])));
|
|
234
|
+
|
|
235
|
+
// ── x402 v2 ───────────────────────────────────────────────────────────────────
|
|
236
|
+
// Over HTTP the 402's PaymentRequired is in the PAYMENT-REQUIRED header (base64 JSON) and in the
|
|
237
|
+
// body, the buyer pays in PAYMENT-SIGNATURE, the settlement comes back in PAYMENT-RESPONSE. Over
|
|
238
|
+
// MCP (a private app's tools) the same objects ride in the tool result and `_meta`.
|
|
239
|
+
// Only a payment's `payload` (the signed EIP-3009 authorization) is taken from the buyer: what
|
|
240
|
+
// goes to the facilitator is rebuilt here around it, so `accepted`, `resource` and the Bazaar
|
|
241
|
+
// extension are always this worker's, including on vouchers signed weeks ago.
|
|
242
|
+
//
|
|
243
|
+
// A sale is what one route sells: its terms `P` (CONFIG.paid or CONFIG.private: network, asset,
|
|
244
|
+
// payTo, facilitator, the token's EIP-712 name and version), what a catalog says it is
|
|
245
|
+
// (`resource`), and its Bazaar entry, if the facilitator should list it.
|
|
246
|
+
|
|
247
|
+
const MAX_BATCH = 1000, MAX_PAYMENTS = 10;
|
|
248
|
+
const b64json = (o) => btoa(JSON.stringify(o));
|
|
249
|
+
const requirement = (P, amount) => ({ scheme: "exact", network: P.network, amount: String(amount), asset: P.asset,
|
|
250
|
+
payTo: P.payTo, maxTimeoutSeconds: 120, extra: { name: P.name, version: P.version } });
|
|
251
|
+
const resource = (url, description) => ({ url, mimeType: "application/json", serviceName: CONFIG.name, description });
|
|
252
|
+
const lead = (P) => (P.description ? `${P.description}. ` : "");
|
|
48
253
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
}
|
|
254
|
+
const SCHEMA = "https://json-schema.org/draft/2020-12/schema";
|
|
255
|
+
const OUTPUT = { type: "object", required: ["type"], properties: { type: { type: "string" }, example: { type: "object" } } };
|
|
256
|
+
/** A Bazaar entry for a POST route taking JSON. */
|
|
257
|
+
const httpBazaar = (body, bodySchema, example) => ({
|
|
258
|
+
info: { input: { type: "http", method: "POST", bodyType: "json", body }, output: { type: "json", example } },
|
|
259
|
+
schema: { $schema: SCHEMA, type: "object", required: ["input"], properties: {
|
|
260
|
+
input: { type: "object", required: ["type", "method", "bodyType", "body"], additionalProperties: false, properties: {
|
|
261
|
+
type: { type: "string", const: "http" }, method: { type: "string", enum: ["POST"] }, bodyType: { type: "string", enum: ["json"] }, body: bodySchema } },
|
|
262
|
+
output: OUTPUT } },
|
|
263
|
+
});
|
|
264
|
+
/** A Bazaar entry for one MCP tool: listed by (the server's URL, its name). */
|
|
265
|
+
const mcpBazaar = (tool) => ({
|
|
266
|
+
info: { input: { type: "mcp", toolName: tool.name, description: tool.description, inputSchema: tool.inputSchema, transport: "streamable-http" } },
|
|
267
|
+
schema: { $schema: SCHEMA, type: "object", required: ["input"], properties: {
|
|
268
|
+
input: { type: "object", required: ["type", "toolName", "inputSchema"], additionalProperties: false, properties: {
|
|
269
|
+
type: { type: "string", const: "mcp" }, toolName: { type: "string" }, description: { type: "string" },
|
|
270
|
+
transport: { type: "string", enum: ["streamable-http", "sse"] }, inputSchema: { type: "object" }, example: { type: "object" } } },
|
|
271
|
+
output: OUTPUT } },
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
// The keys to sealed records. Only POST /paid is listed: GET /paid/<id>'s URL differs per record,
|
|
275
|
+
// and each would be listed apart.
|
|
276
|
+
const KEYS_BAZAAR = httpBazaar({ ids: ["<record id>"] },
|
|
277
|
+
{ type: "object", required: ["ids"], properties: { ids: { type: "array", items: { type: "string" }, minItems: 1, maxItems: MAX_BATCH } } },
|
|
278
|
+
{ keys: { "<record id>": "<hex key>" }, missing: [], receipts: [], paid: [] });
|
|
279
|
+
const keySale = (url, listed) => ({ P: CONFIG.paid, bazaar: listed ? KEYS_BAZAAR : null,
|
|
280
|
+
resource: resource(url.href, `${lead(CONFIG.paid)}Sells the keys to ${CONFIG.name}'s sealed records (AES-256-GCM, in its rows as `
|
|
281
|
+
+ `paid_ct). Find records and their ids with the MCP server at ${url.origin}/mcp.`) });
|
|
282
|
+
|
|
283
|
+
// A private app's pass, and its tools one call at a time.
|
|
284
|
+
const PASS_BAZAAR = httpBazaar({}, { type: "object" }, { token: "<pass>", expires: 1900000000 });
|
|
285
|
+
const days = (s) => Math.round(s / 86400);
|
|
286
|
+
const passSale = (url) => ({ P: CONFIG.private, bazaar: PASS_BAZAAR,
|
|
287
|
+
resource: resource(url.href, `${lead(CONFIG.private)}A ${days(CONFIG.private.pass.seconds)}-day pass to ${CONFIG.name}: every tool of its MCP server `
|
|
288
|
+
+ `at ${url.origin}/mcp and its view, with Authorization: Bearer <token>.`) });
|
|
289
|
+
const callSale = (origin, tool) => ({ P: CONFIG.private, bazaar: mcpBazaar(tool),
|
|
290
|
+
resource: resource(`${origin}/mcp`, `${lead(CONFIG.private)}${CONFIG.name}'s ${tool.name} tool, one call. A pass (POST ${origin}/pass) covers every call.`) });
|
|
291
|
+
|
|
292
|
+
const paymentRequired = (sale, amount, error) => ({ x402Version: 2, error, resource: sale.resource, accepts: [requirement(sale.P, amount)],
|
|
293
|
+
...(sale.bazaar ? { extensions: { bazaar: sale.bazaar } } : {}) });
|
|
294
|
+
|
|
295
|
+
/** 402 with the price of `amount`; `more` rides in the body only. */
|
|
296
|
+
function quote(sale, amount, error, more = {}) {
|
|
297
|
+
const required = paymentRequired(sale, amount, error);
|
|
298
|
+
return json({ ...required, ...more }, 402, { "payment-required": b64json(required) });
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/** The PaymentPayload the facilitator sees: the buyer's signed `payload`, the rest ours. */
|
|
302
|
+
const payloadOf = (sale, p, amount) => ({ x402Version: 2, resource: sale.resource, accepted: requirement(sale.P, amount),
|
|
303
|
+
payload: p?.payload, ...(sale.bazaar ? { extensions: { bazaar: sale.bazaar } } : {}) });
|
|
304
|
+
|
|
305
|
+
async function facilitator(P, path, paymentPayload) {
|
|
306
|
+
// A network failure is an answer too: thrown, it would turn a half-settled batch into a 500.
|
|
307
|
+
let r;
|
|
308
|
+
try {
|
|
309
|
+
r = await fetch(`${P.facilitator}/${path}`, {
|
|
310
|
+
method: "POST", headers: { "content-type": "application/json" },
|
|
311
|
+
body: JSON.stringify({ x402Version: 2, paymentPayload, paymentRequirements: paymentPayload.accepted }),
|
|
312
|
+
});
|
|
313
|
+
} catch (e) { return { error: `facilitator ${path}: ${e.message}` }; }
|
|
54
314
|
const text = await r.text();
|
|
55
315
|
try { return JSON.parse(text); } catch { return { error: `facilitator ${path}: HTTP ${r.status} ${text.slice(0, 200)}` }; }
|
|
56
316
|
}
|
|
57
317
|
|
|
318
|
+
const signatures = (request) => {
|
|
319
|
+
const header = request.headers.get("PAYMENT-SIGNATURE");
|
|
320
|
+
if (!header) return { error: "PAYMENT-SIGNATURE header is required" };
|
|
321
|
+
try { return { payments: [JSON.parse(atob(header))].flat() }; } catch { return { error: "PAYMENT-SIGNATURE is not base64 JSON" }; }
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
/** One payment for `amount`, verified and settled: `{settled, payer}`, or `{refused}`, the 402 to answer with. */
|
|
325
|
+
async function payOnce(request, sale, amount) {
|
|
326
|
+
const { payments, error } = signatures(request);
|
|
327
|
+
if (error) return { refused: quote(sale, amount, error) };
|
|
328
|
+
if (payments.length !== 1) return { refused: quote(sale, amount, "one payment, not several") };
|
|
329
|
+
const payment = payloadOf(sale, payments[0], amount);
|
|
330
|
+
const verified = await facilitator(sale.P, "verify", payment);
|
|
331
|
+
if (!verified.isValid) return { refused: quote(sale, amount, verified.invalidReason ?? verified.error ?? "payment did not verify") };
|
|
332
|
+
const settled = await facilitator(sale.P, "settle", payment);
|
|
333
|
+
if (!settled.success) return { refused: quote(sale, amount, settled.errorReason ?? settled.error ?? "settlement failed") };
|
|
334
|
+
return { settled, payer: settled.payer ?? payment.payload?.authorization?.from };
|
|
335
|
+
}
|
|
336
|
+
|
|
58
337
|
async function sell(request, env, url) {
|
|
59
|
-
const P = CONFIG.paid;
|
|
60
338
|
const id = decodeURIComponent(url.pathname.slice("/paid/".length));
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
339
|
+
if (!id) return json({ error: "no id" }, 400);
|
|
340
|
+
const { settled, refused } = await payOnce(request, keySale(url, false), CONFIG.paid.price);
|
|
341
|
+
if (refused) return refused;
|
|
342
|
+
return json({ id, key: await keyFor(env, id) }, 200, { "payment-response": b64json(settled) });
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// POST /paid {ids}: many keys, one price. 402 with the total; then PAYMENT-SIGNATURE may be one
|
|
346
|
+
// payment or a JSON array of them (an agent's vouchers) that add up to it. The only subrequests
|
|
347
|
+
// are the facilitator's (2 per payment, 50 on the free plan), hence MAX_PAYMENTS; keys cost nothing.
|
|
348
|
+
async function sellBatch(request, env, url) {
|
|
349
|
+
const P = CONFIG.paid, sale = keySale(url, true);
|
|
350
|
+
const { payments, error } = signatures(request);
|
|
351
|
+
let ids;
|
|
352
|
+
try { ids = [...new Set((await request.json()).ids)].filter((x) => typeof x === "string"); } catch { ids = null; }
|
|
353
|
+
if (!ids?.length || ids.length > MAX_BATCH) {
|
|
354
|
+
// A probe (x402scan, a Bazaar crawler) posts no body: it gets the price of one record.
|
|
355
|
+
if (error) return quote(sale, P.price, `${error}; the body is {"ids": [...]}, ${P.price} per id`);
|
|
356
|
+
return json({ error: `body must be {"ids": [...]}, between 1 and ${MAX_BATCH} ids per purchase` }, 400);
|
|
357
|
+
}
|
|
358
|
+
const found = ids, missing = [];
|
|
359
|
+
const total = BigInt(P.price) * BigInt(found.length);
|
|
360
|
+
const refuse = (error, more = {}) => quote(sale, total, error, { found, missing, ...more });
|
|
361
|
+
if (error) return refuse(error);
|
|
362
|
+
if (payments.length > MAX_PAYMENTS) return refuse(`at most ${MAX_PAYMENTS} payments per purchase`);
|
|
363
|
+
const auths = payments.map((p) => p?.payload?.authorization ?? {});
|
|
364
|
+
if (new Set(auths.map((a) => a.nonce)).size !== auths.length) return refuse("a payment is repeated");
|
|
365
|
+
if (auths.some((a) => String(a.to).toLowerCase() !== P.payTo.toLowerCase())) return refuse("a payment is not to this app");
|
|
366
|
+
let sum = 0n;
|
|
367
|
+
try { for (const a of auths) sum += BigInt(a.value); } catch { return refuse("a payment has no amount"); }
|
|
368
|
+
if (sum !== total) return refuse(`payments add up to ${sum}, the price is ${total}`);
|
|
369
|
+
const signed = payments.map((p, i) => payloadOf(sale, p, auths[i].value));
|
|
370
|
+
for (const [i, p] of signed.entries()) {
|
|
371
|
+
const v = await facilitator(P, "verify", p);
|
|
372
|
+
if (!v.isValid) return refuse(`payment ${i + 1}: ${v.invalidReason ?? v.error ?? "did not verify"}`);
|
|
373
|
+
}
|
|
374
|
+
// Settle one by one. A spent voucher (or one the wallet can no longer cover) passes verify
|
|
375
|
+
// and fails here. If that happens after others settled, the buyer still gets what the
|
|
376
|
+
// settled money pays for, never more: a spent voucher riding along with a fresh one buys
|
|
377
|
+
// only what the fresh one pays for. `paid` and `rejected` name the vouchers (by nonce) so
|
|
378
|
+
// the buyer's stack can drop them either way.
|
|
379
|
+
const receipts = [];
|
|
380
|
+
let settled = 0n;
|
|
381
|
+
for (const [i, p] of signed.entries()) {
|
|
382
|
+
const s = await facilitator(P, "settle", p);
|
|
383
|
+
if (!s.success) {
|
|
384
|
+
const error = `payment ${i + 1}: ${s.errorReason ?? s.error ?? "settlement failed"}`;
|
|
385
|
+
if (!receipts.length) return refuse(error, { rejected: [auths[i].nonce] });
|
|
386
|
+
const n = Number(settled / BigInt(P.price));
|
|
387
|
+
return json({ keys: await keysFor(env, found.slice(0, n)), missing, undelivered: found.slice(n), receipts, error,
|
|
388
|
+
paid: auths.slice(0, i).map((a) => a.nonce), rejected: [auths[i].nonce] });
|
|
389
|
+
}
|
|
390
|
+
receipts.push(s);
|
|
391
|
+
settled += BigInt(auths[i].value);
|
|
392
|
+
}
|
|
393
|
+
return json({ keys: await keysFor(env, found), missing, receipts, paid: auths.map((a) => a.nonce) });
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// ── a private app ─────────────────────────────────────────────────────────────
|
|
397
|
+
// CONFIG.private: the rows are sealed on chain (x402.js's sealRow), opened by the cron with
|
|
398
|
+
// PAID_SECRET, and the view is served only with access: a pass bought at POST /pass (one x402
|
|
399
|
+
// payment, a token good for `pass.seconds`), or, on /mcp, one x402 payment per tool call. What a
|
|
400
|
+
// stranger needs to decide whether to pay stays free: the card, the catalog (views, counts) and
|
|
401
|
+
// the describe tool.
|
|
402
|
+
// ponytail: a pass is a signed token, not a record: it cannot be revoked before it expires, and
|
|
403
|
+
// changing PAID_SECRET voids every pass (and every key). A list of revoked payers in R2 if that matters.
|
|
404
|
+
|
|
405
|
+
const FREE_VIEW = new Set(["cdn/catalog"]), FREE_TOOLS = new Set(["describe"]);
|
|
406
|
+
const b64url = (bytes) => btoa(String.fromCharCode(...bytes)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
407
|
+
const unb64url = (s) => Uint8Array.from(atob(s.replace(/-/g, "+").replace(/_/g, "/")), (c) => c.charCodeAt(0));
|
|
408
|
+
const passKey = (env) => crypto.subtle.importKey("raw", new TextEncoder().encode(`${env.PAID_SECRET}:pass`), { name: "HMAC", hash: "SHA-256" }, false, ["sign", "verify"]);
|
|
409
|
+
|
|
410
|
+
async function issuePass(env, payer, exp) {
|
|
411
|
+
const claims = b64url(new TextEncoder().encode(JSON.stringify({ app: CONFIG.app, sub: payer, exp })));
|
|
412
|
+
return `${claims}.${b64url(new Uint8Array(await crypto.subtle.sign("HMAC", await passKey(env), new TextEncoder().encode(claims))))}`;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/** The pass a request carries (Authorization: Bearer), if it is this app's and unexpired. */
|
|
416
|
+
async function passOf(env, request) {
|
|
417
|
+
const m = /^Bearer ([\w-]+)\.([\w-]+)$/.exec(request.headers.get("authorization") ?? "");
|
|
418
|
+
if (!m || !env.PAID_SECRET) return null;
|
|
419
|
+
try {
|
|
420
|
+
if (!await crypto.subtle.verify("HMAC", await passKey(env), unb64url(m[2]), new TextEncoder().encode(m[1]))) return null;
|
|
421
|
+
const c = JSON.parse(new TextDecoder().decode(unb64url(m[1])));
|
|
422
|
+
return c.app === CONFIG.app && c.exp > Date.now() / 1000 ? c : null;
|
|
423
|
+
} catch { return null; }
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// POST /pass: one payment, a pass. A probe posts nothing and gets the price.
|
|
427
|
+
async function sellPass(request, env, url) {
|
|
428
|
+
const X = CONFIG.private;
|
|
429
|
+
const { settled, payer, refused } = await payOnce(request, passSale(url), X.pass.price);
|
|
430
|
+
if (refused) return refused;
|
|
431
|
+
const expires = Math.floor(Date.now() / 1000) + X.pass.seconds;
|
|
432
|
+
return json({ token: await issuePass(env, payer, expires), expires,
|
|
433
|
+
use: `Authorization: Bearer <token> on ${url.origin}/mcp and ${url.origin}/view/…` }, 200, { "payment-response": b64json(settled) });
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/** A paid tool call over x402's MCP transport: verify, run, settle. A call that fails is not
|
|
437
|
+
* charged (its payment is never settled), and a settlement that fails returns no content. */
|
|
438
|
+
async function paidCall(origin, tool, payment, run) {
|
|
439
|
+
const X = CONFIG.private, sale = callSale(origin, tool);
|
|
440
|
+
const required = (error) => {
|
|
441
|
+
const pr = paymentRequired(sale, X.call, error);
|
|
442
|
+
return { isError: true, structuredContent: pr, content: [{ type: "text", text: JSON.stringify(pr) }] };
|
|
69
443
|
};
|
|
70
|
-
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
if (
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
export
|
|
444
|
+
if (!payment) return required(`${tool.name} is paid: one x402 payment per call in _meta["x402/payment"], or a pass from POST ${origin}/pass`);
|
|
445
|
+
const p = payloadOf(sale, payment, X.call);
|
|
446
|
+
const verified = await facilitator(X, "verify", p);
|
|
447
|
+
if (!verified.isValid) return required(verified.invalidReason ?? verified.error ?? "payment did not verify");
|
|
448
|
+
const result = await run();
|
|
449
|
+
// view-tools answers a miss ("not in a loaded view") with {error} in the text, not isError.
|
|
450
|
+
let missed = false;
|
|
451
|
+
try { missed = !!JSON.parse(result.content?.[0]?.text ?? "{}").error; } catch { /* not JSON: an answer */ }
|
|
452
|
+
if (result.isError || missed) return result;
|
|
453
|
+
const settled = await facilitator(X, "settle", p);
|
|
454
|
+
if (!settled.success) return required(settled.errorReason ?? settled.error ?? "settlement failed");
|
|
455
|
+
return { ...result, _meta: { ...result._meta, "x402/payment-response": settled } };
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
export const ORIGIN = "https://origin.westmarch.invalid";
|
|
459
|
+
const BY_ORIGIN = new Set(["/.well-known/agent-card.json", "/openapi.json", "/.well-known/x402"]);
|
|
460
|
+
|
|
461
|
+
const worker = {
|
|
85
462
|
async fetch(request, env) {
|
|
463
|
+
CONFIG = env.CONFIG ?? CONFIG;
|
|
86
464
|
const url = new URL(request.url);
|
|
465
|
+
const p = url.pathname;
|
|
87
466
|
if (request.method === "OPTIONS") return new Response(null, { status: 204, headers: CORS });
|
|
88
|
-
if (
|
|
89
|
-
|
|
90
|
-
|
|
467
|
+
if (p.startsWith("/view/")) {
|
|
468
|
+
const path = p.slice("/view/".length);
|
|
469
|
+
if (CONFIG.private && !FREE_VIEW.has(path) && !await passOf(env, request))
|
|
470
|
+
return json({ error: `${CONFIG.name}'s data is private: send Authorization: Bearer <pass>, from POST ${url.origin}/pass (x402)` }, 401);
|
|
471
|
+
return viewFile(env, path);
|
|
472
|
+
}
|
|
473
|
+
if (p === "/pass" && request.method === "POST") return CONFIG.private ? sellPass(request, env, url) : json({ error: "nothing is sold here" }, 404);
|
|
474
|
+
if (p === "/mcp") return serveMcp(request, env, url);
|
|
475
|
+
if (p === "/doc") return doc(request, env, url);
|
|
476
|
+
if (p === "/paid" && request.method === "POST") return CONFIG.paid ? sellBatch(request, env, url) : json({ error: "nothing is sold here" }, 404);
|
|
477
|
+
if (p.startsWith("/paid/")) return CONFIG.paid ? sell(request, env, url) : json({ error: "nothing is sold here" }, 404);
|
|
91
478
|
|
|
92
|
-
|
|
479
|
+
let res = await env.ASSETS.fetch(request);
|
|
480
|
+
// A site built before its URL was known (`westmarch build`, under the deploy button) names
|
|
481
|
+
// ORIGIN wherever it means its own: it is wherever it is served from.
|
|
482
|
+
if (res.ok && BY_ORIGIN.has(p)) {
|
|
483
|
+
const body = (await res.text()).replaceAll(ORIGIN, url.origin);
|
|
484
|
+
res = new Response(body, { status: res.status, headers: res.headers });
|
|
485
|
+
res.headers.delete("content-length");
|
|
486
|
+
}
|
|
93
487
|
const out = new Response(res.body, res);
|
|
94
|
-
|
|
95
|
-
if (p === "/.well-known/agent-card.json" || p.startsWith("/view/")) out.headers.set("access-control-allow-origin", "*");
|
|
488
|
+
if (p === "/.well-known/agent-card.json") out.headers.set("access-control-allow-origin", "*");
|
|
96
489
|
if (p.startsWith("/assets/")) out.headers.set("cache-control", "public, max-age=31536000, immutable");
|
|
97
|
-
// Shards are content-named, so a reader's browser keeps one for good: a change or a
|
|
98
|
-
// retraction writes new names. `private`: pages.dev's edge cache cannot be purged, so it
|
|
99
|
-
// must never hold one, and a retracted record's shard stops being served when the deploy
|
|
100
|
-
// drops it. (This worker's header wins over _headers, which Pages skips for worker responses.)
|
|
101
|
-
else if (/^\/view\/cdn\/domains\/[^/]+\/shards\//.test(p))
|
|
102
|
-
out.headers.set("cache-control", (res.headers.get("content-type") ?? "").includes("html") ? "no-store" : "private, max-age=31536000, immutable");
|
|
103
490
|
return out;
|
|
104
491
|
},
|
|
492
|
+
async scheduled(_event, env, ctx) {
|
|
493
|
+
CONFIG = env.CONFIG ?? CONFIG;
|
|
494
|
+
ctx.waitUntil(index(env));
|
|
495
|
+
},
|
|
105
496
|
};
|
|
497
|
+
export default worker;
|
|
498
|
+
|
|
499
|
+
// ── self-check: `node site/worker.js` — a fake bucket, a fake AI binding, a fake registry ──
|
|
500
|
+
if (typeof process !== "undefined" && import.meta.url === `file://${process.argv[1]}` && process.argv[1]?.endsWith("/worker.js")) {
|
|
501
|
+
const { localFangorn } = await import("../src/publish/view.js");
|
|
502
|
+
const eq = (a, b, m) => { if (JSON.stringify(a) !== JSON.stringify(b)) throw new Error(`${m}: ${JSON.stringify(a)} != ${JSON.stringify(b)}`); };
|
|
503
|
+
|
|
504
|
+
const objs = new Map();
|
|
505
|
+
const object = (k) => { const b = objs.get(k).bytes; return { body: new Blob([b]).stream(), uploaded: objs.get(k).at,
|
|
506
|
+
arrayBuffer: async () => b, text: async () => new TextDecoder().decode(b), json: async () => JSON.parse(new TextDecoder().decode(b)) }; };
|
|
507
|
+
const VIEW = {
|
|
508
|
+
get: async (k) => (objs.has(k) ? object(k) : null),
|
|
509
|
+
head: async (k) => (objs.has(k) ? {} : null),
|
|
510
|
+
put: async (k, b) => { objs.set(k, { bytes: typeof b === "string" ? new TextEncoder().encode(b) : b, at: new Date(Date.now() + objs.size) }); },
|
|
511
|
+
delete: async (ks) => { for (const k of [ks].flat()) objs.delete(k); },
|
|
512
|
+
list: async ({ prefix, delimiter }) => {
|
|
513
|
+
const ks = [...objs.keys()].filter((k) => k.startsWith(prefix)).sort();
|
|
514
|
+
const rest = (k) => k.slice(prefix.length);
|
|
515
|
+
if (!delimiter) return { objects: ks.map((key) => ({ key })), truncated: false };
|
|
516
|
+
return { objects: ks.filter((k) => !rest(k).includes("/")).map((key) => ({ key })), truncated: false,
|
|
517
|
+
delimitedPrefixes: [...new Set(ks.filter((k) => rest(k).includes("/")).map((k) => `${prefix + rest(k).split("/")[0]}/`))] };
|
|
518
|
+
},
|
|
519
|
+
};
|
|
520
|
+
// Words → a 768-d vector, so a query shares dimensions with the rows that share its words.
|
|
521
|
+
const calls = [];
|
|
522
|
+
const AI = { run: async (model, { text }) => { calls.push({ model, text });
|
|
523
|
+
return { data: text.map((t) => { const v = new Array(768).fill(0); for (const w of t.toLowerCase().split(/\W+/)) if (w.length > 3) v[[...w].reduce((h, c) => (h * 31 + c.charCodeAt(0)) % 256, 7)] += 1; return v; }) }; } };
|
|
524
|
+
const env = { VIEW, AI, ASSETS: { fetch: async (r) => new Response(new URL(r.url).pathname.endsWith("agent-card.json") ? `{"url":"${ORIGIN}/"}` : "<!doctype html>site") },
|
|
525
|
+
CONFIG: { app: "shop", name: "Shop", description: "Things for a room.", repos: [] } };
|
|
526
|
+
CONFIG = env.CONFIG;
|
|
527
|
+
|
|
528
|
+
const APP = `0x${"ab".repeat(32)}`, OWNER = `0x${"11".repeat(20)}`, PUB = `0x${"22".repeat(20)}`, TAG = "shop.item.v1";
|
|
529
|
+
const rec = (cid, type, payload) => ({ id: cid, cid, type, payload });
|
|
530
|
+
CONFIG.schema = { types: { [TAG]: { role_map: { identity: "sku", title: "name", text: "about", media: "doc" } } } };
|
|
531
|
+
const chain = {
|
|
532
|
+
[`${PUB}/shop`]: { records: [
|
|
533
|
+
rec("c1", TAG, { sku: "a1", name: "Oak table", about: "a solid oak dining table", doc: "https://docs.example/oak.pdf" }),
|
|
534
|
+
rec("c2", TAG, { sku: "b2", name: "Brass lamp", about: "a brass reading lamp" })] },
|
|
535
|
+
};
|
|
536
|
+
const fangorn = localFangorn(chain, { appId: APP, owner: OWNER });
|
|
537
|
+
await publishView({ fangorn, app: APP, schema: CONFIG.schema, store: r2Store(VIEW), embedBatch: encode(env, DOCUMENT), embedder: WORKER_EMBEDDER, log: () => {} });
|
|
538
|
+
eq([calls[0].model, calls[0].text[0].startsWith(DOCUMENT)], [AI_MODEL, true], "documents go to the AI binding with the document prompt");
|
|
539
|
+
|
|
540
|
+
const call = async (method, params) => (await (await worker.fetch(new Request("https://shop.test/mcp",
|
|
541
|
+
{ method: "POST", body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, params }) }), env)).json());
|
|
542
|
+
eq((await call("initialize", {})).result.serverInfo.name, "Shop", "initialize names the app");
|
|
543
|
+
const names = (await call("tools/list")).result.tools.map((t) => t.name);
|
|
544
|
+
for (const t of ["describe", "search", "get", "similar", "count", "browse"]) if (!names.includes(t)) throw new Error(`missing tool ${t}: ${names}`);
|
|
545
|
+
const described = JSON.parse((await call("tools/call", { name: "describe", arguments: {} })).result.content[0].text);
|
|
546
|
+
eq(described.views.map((v) => v.rows), [2], "describe reads the catalog from R2");
|
|
547
|
+
const found = JSON.parse((await call("tools/call", { name: "search", arguments: { query: "reading lamp", limit: 1 } })).result.content[0].text);
|
|
548
|
+
eq([found.ranked_by, found.hits[0].title], ["meaning", "Brass lamp"], "search ranks by meaning, the query embedded by the same runtime");
|
|
549
|
+
if (!calls.at(-1).text[0].startsWith(QUERY)) throw new Error("the query goes to the AI binding with the query prompt");
|
|
550
|
+
eq((await (await worker.fetch(new Request("https://shop.test/mcp", { method: "POST", body: '{"jsonrpc":"2.0","method":"notifications/initialized"}' }), env)).status), 202, "a notification is accepted, not answered");
|
|
551
|
+
|
|
552
|
+
const cat = await worker.fetch(new Request("https://shop.test/view/cdn/catalog"), env);
|
|
553
|
+
const card = await worker.fetch(new Request("https://shop.example.workers.dev/.well-known/agent-card.json"), env);
|
|
554
|
+
eq([await card.json(), card.headers.get("access-control-allow-origin")], [{ url: "https://shop.example.workers.dev/" }, "*"], "a card built before its URL was known names the origin it is served from");
|
|
555
|
+
eq([cat.status, cat.headers.get("access-control-allow-origin"), (await cat.json()).domains[0].link_hosts], [200, "*", ["docs.example"]], "the view is served from R2, with the hosts its records link to");
|
|
556
|
+
eq((await worker.fetch(new Request("https://shop.test/doc?u=https://evil.example/x.pdf"), env)).status, 403, "/doc refuses a host no record links to");
|
|
557
|
+
eq((await worker.fetch(new Request("https://shop.test/view/cdn/nope"), env)).status, 404, "a missing file is a 404");
|
|
558
|
+
|
|
559
|
+
// A private app: its rows sealed on chain, opened by the indexer, served only with a pass or a paid call.
|
|
560
|
+
const { sealRow, openRow } = await import("../src/agent/x402.js");
|
|
561
|
+
const SECRET = "s3cret";
|
|
562
|
+
chain[`${PUB}/vault`] = { records: [rec("v1", TAG, await sealRow(SECRET, { sku: "v1", name: "Velvet chair", about: "a green velvet armchair" }))] };
|
|
563
|
+
const sealedAgain = await sealRow(SECRET, { sku: "v1", name: "Velvet chair", about: "a green velvet armchair" });
|
|
564
|
+
eq(sealedAgain, chain[`${PUB}/vault`].records[0].payload, "an unchanged row seals to the same bytes (the same CID: not embedded again)");
|
|
565
|
+
if (JSON.stringify(sealedAgain).includes("Velvet")) throw new Error("a sealed row must not carry its plaintext");
|
|
566
|
+
await publishView({ fangorn, app: APP, schema: CONFIG.schema, store: r2Store(VIEW), embedBatch: encode(env, DOCUMENT), embedder: WORKER_EMBEDDER, log: () => {},
|
|
567
|
+
open: (p) => openRow(SECRET, p) });
|
|
568
|
+
env.PAID_SECRET = SECRET;
|
|
569
|
+
CONFIG = env.CONFIG = { ...env.CONFIG, private: { network: "eip155:84532", asset: `0x${"aa".repeat(20)}`, payTo: `0x${"5e".repeat(20)}`,
|
|
570
|
+
facilitator: "https://fac", name: "USDC", version: "2", pass: { price: "1000000", seconds: 86400 }, call: "10000" } };
|
|
571
|
+
const settles = [];
|
|
572
|
+
const realFetch = globalThis.fetch;
|
|
573
|
+
globalThis.fetch = async (u, init) => {
|
|
574
|
+
const { paymentPayload: p } = JSON.parse(init.body);
|
|
575
|
+
if (u === "https://fac/verify") return new Response(JSON.stringify({ isValid: true, payer: p.payload.authorization.from }));
|
|
576
|
+
settles.push(p);
|
|
577
|
+
return new Response(JSON.stringify({ success: true, transaction: `0x${settles.length}`, network: "eip155:84532", payer: p.payload.authorization.from }));
|
|
578
|
+
};
|
|
579
|
+
try {
|
|
580
|
+
const payment = { x402Version: 2, payload: { signature: "0x", authorization: { from: "0xbuyer", to: CONFIG.private.payTo, value: "1", nonce: "0x1" } } };
|
|
581
|
+
const shard = [...objs.keys()].find((k) => k.startsWith("view/cdn/domains/") && k.endsWith("/manifest")).slice("view/".length);
|
|
582
|
+
const get = (path, headers = {}) => worker.fetch(new Request(`https://shop.test/${path}`, { headers }), env);
|
|
583
|
+
eq([(await get("view/cdn/catalog")).status, (await get(`view/${shard}`)).status], [200, 401], "the catalog is free, the rest of the view is not");
|
|
584
|
+
const asMcp = async (params, headers = {}) => (await (await worker.fetch(new Request("https://shop.test/mcp", { method: "POST", headers,
|
|
585
|
+
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/call", params }) }), env)).json()).result;
|
|
586
|
+
const search = { name: "search", arguments: { query: "velvet armchair", limit: 1 } };
|
|
587
|
+
const ask = await asMcp(search);
|
|
588
|
+
eq([ask.isError, ask.structuredContent.accepts[0].amount, ask.structuredContent.extensions.bazaar.info.input.toolName, JSON.parse(ask.content[0].text).x402Version],
|
|
589
|
+
[true, "10000", "search", 2], "a paid tool without payment answers with x402's PaymentRequired, listed as an MCP tool");
|
|
590
|
+
eq(JSON.parse((await asMcp({ name: "describe", arguments: {} })).content[0].text).views[0].rows, 3, "describe stays free, and counts the sealed row");
|
|
591
|
+
if (!(await call("tools/list")).result.tools.find((t) => t.name === "search").description.includes("Paid: 0.01 USDC")) throw new Error("tools/list names the price");
|
|
592
|
+
|
|
593
|
+
const quote402 = await worker.fetch(new Request("https://shop.test/pass", { method: "POST" }), env);
|
|
594
|
+
const pr = JSON.parse(atob(quote402.headers.get("payment-required")));
|
|
595
|
+
eq([quote402.status, pr.accepts[0].amount, pr.extensions.bazaar.info.input.method], [402, "1000000", "POST"], "a pass is quoted over x402, Bazaar entry and all");
|
|
596
|
+
const bought = await (await worker.fetch(new Request("https://shop.test/pass", { method: "POST", headers: { "PAYMENT-SIGNATURE": btoa(JSON.stringify(payment)) } }), env)).json();
|
|
597
|
+
const auth = { authorization: `Bearer ${bought.token}` };
|
|
598
|
+
eq([(await get(`view/${shard}`, auth)).status, settles.length], [200, 1], "a pass opens the view, paid once");
|
|
599
|
+
const withPass = JSON.parse((await asMcp(search, auth)).content[0].text);
|
|
600
|
+
eq([withPass.hits[0].title, settles.length], ["Velvet chair", 1], "a pass covers tool calls, and the sealed row was opened and embedded");
|
|
601
|
+
const forged = `${bought.token.split(".")[0]}.${"A".repeat(43)}`;
|
|
602
|
+
eq((await get(`view/${shard}`, { authorization: `Bearer ${forged}` })).status, 401, "a forged pass is refused");
|
|
603
|
+
|
|
604
|
+
const paidOnce = await asMcp({ ...search, _meta: { "x402/payment": payment } });
|
|
605
|
+
eq([JSON.parse(paidOnce.content[0].text).hits[0].title, paidOnce._meta["x402/payment-response"].transaction, settles.at(-1).resource.url],
|
|
606
|
+
["Velvet chair", "0x2", "https://shop.test/mcp"], "one call, one payment, settled after it ran");
|
|
607
|
+
const failed = await asMcp({ name: "get", arguments: { id: "no-such-row" }, _meta: { "x402/payment": payment } });
|
|
608
|
+
eq([!!JSON.parse(failed.content[0].text).error, !!failed._meta, settles.length], [true, false, 2], "a call that misses is not charged");
|
|
609
|
+
} finally { globalThis.fetch = realFetch; }
|
|
610
|
+
CONFIG = env.CONFIG = { ...env.CONFIG, private: null };
|
|
611
|
+
|
|
612
|
+
// The incremental scan: only updates since the last run, membership and exclusions applied every run.
|
|
613
|
+
const STRANGER = "0x" + "9".repeat(40);
|
|
614
|
+
const logs = [{ publisher: PUB, repo: "shop", head: "0x01", t: 10n }, { publisher: OWNER, repo: "notes", head: "0x02", t: 12n },
|
|
615
|
+
{ publisher: STRANGER, repo: "spam", head: "0x04", t: 14n }];
|
|
616
|
+
const asked = [];
|
|
617
|
+
let excluded = null, block = 20n;
|
|
618
|
+
const r = { chain: { getBlockNumber: async () => block }, fangorn: { getAppId: () => APP, getRegistry: () => ({
|
|
619
|
+
log: async (_f, from, to) => { asked.push([from, to]); return logs.filter((l) => l.t >= from && l.t <= to); },
|
|
620
|
+
presents: async (_app, p) => p !== STRANGER,
|
|
621
|
+
exclusionCount: async (_app, x) => (excluded && x.publisher === excluded && !x.repo ? 1 : 0),
|
|
622
|
+
}) } };
|
|
623
|
+
CONFIG = { ...CONFIG, fromBlock: "5" };
|
|
624
|
+
const state = { block: null, repos: {} };
|
|
625
|
+
eq((await presented(r, state)).map((t) => t.repo).sort(), ["notes", "shop"], "the first run scans from the app's fromBlock, and keeps members only");
|
|
626
|
+
logs.push({ publisher: PUB, repo: "shop", head: "0x03", t: 25n });
|
|
627
|
+
block = 30n;
|
|
628
|
+
excluded = OWNER;
|
|
629
|
+
eq((await presented(r, state)).map((t) => [t.repo, t.root]), [["shop", "0x03"]], "the next reads only what is new, and drops what the app excludes");
|
|
630
|
+
eq(asked.map((a) => a.map(String)), [["5", "20"], ["21", "30"]], "each run starts where the last ended");
|
|
631
|
+
console.log("worker.js self-check ok — the lens embeds through the AI binding, /mcp serves the view's tools from R2 with queries embedded by the same runtime, the view and /doc are served from R2, and the indexer scans only new updates and applies membership and exclusions every run");
|
|
632
|
+
}
|