mppx 0.6.31 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +50 -0
- package/README.md +20 -11
- package/dist/Challenge.d.ts.map +1 -1
- package/dist/Challenge.js +27 -13
- package/dist/Challenge.js.map +1 -1
- package/dist/Constants.d.ts +46 -0
- package/dist/Constants.d.ts.map +1 -0
- package/dist/Constants.js +46 -0
- package/dist/Constants.js.map +1 -0
- package/dist/Credential.d.ts.map +1 -1
- package/dist/Credential.js +5 -4
- package/dist/Credential.js.map +1 -1
- package/dist/Mcp.d.ts +3 -0
- package/dist/Mcp.d.ts.map +1 -1
- package/dist/Mcp.js +2 -0
- package/dist/Mcp.js.map +1 -1
- package/dist/Method.d.ts +32 -4
- package/dist/Method.d.ts.map +1 -1
- package/dist/Method.js +5 -2
- package/dist/Method.js.map +1 -1
- package/dist/PaymentRequest.d.ts +10 -10
- package/dist/PaymentRequest.js +8 -8
- package/dist/Receipt.d.ts.map +1 -1
- package/dist/Receipt.js +3 -2
- package/dist/Receipt.js.map +1 -1
- package/dist/cli/cli.d.ts.map +1 -1
- package/dist/cli/cli.js +19 -11
- package/dist/cli/cli.js.map +1 -1
- package/dist/cli/plugins/tempo.d.ts.map +1 -1
- package/dist/cli/plugins/tempo.js +17 -6
- package/dist/cli/plugins/tempo.js.map +1 -1
- package/dist/cli/utils.d.ts +5 -0
- package/dist/cli/utils.d.ts.map +1 -1
- package/dist/cli/utils.js +10 -0
- package/dist/cli/utils.js.map +1 -1
- package/dist/client/Methods.d.ts +5 -2
- package/dist/client/Methods.d.ts.map +1 -1
- package/dist/client/Methods.js +5 -2
- package/dist/client/Methods.js.map +1 -1
- package/dist/client/Mppx.js +2 -2
- package/dist/client/Mppx.js.map +1 -1
- package/dist/client/Transport.d.ts +11 -16
- package/dist/client/Transport.d.ts.map +1 -1
- package/dist/client/Transport.js +55 -76
- package/dist/client/Transport.js.map +1 -1
- package/dist/client/index.d.ts +5 -1
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +3 -1
- package/dist/client/index.js.map +1 -1
- package/dist/client/internal/Fetch.d.ts.map +1 -1
- package/dist/client/internal/Fetch.js +60 -13
- package/dist/client/internal/Fetch.js.map +1 -1
- package/dist/client/internal/protocols/Mcp.d.ts +7 -0
- package/dist/client/internal/protocols/Mcp.d.ts.map +1 -0
- package/dist/client/internal/protocols/Mcp.js +159 -0
- package/dist/client/internal/protocols/Mcp.js.map +1 -0
- package/dist/client/internal/protocols/Mpp.d.ts +4 -0
- package/dist/client/internal/protocols/Mpp.d.ts.map +1 -0
- package/dist/client/internal/protocols/Mpp.js +18 -0
- package/dist/client/internal/protocols/Mpp.js.map +1 -0
- package/dist/client/internal/protocols/Protocol.d.ts +10 -0
- package/dist/client/internal/protocols/Protocol.d.ts.map +1 -0
- package/dist/client/internal/protocols/Protocol.js +2 -0
- package/dist/client/internal/protocols/Protocol.js.map +1 -0
- package/dist/client/internal/protocols/Shared.d.ts +5 -0
- package/dist/client/internal/protocols/Shared.d.ts.map +1 -0
- package/dist/client/internal/protocols/Shared.js +20 -0
- package/dist/client/internal/protocols/Shared.js.map +1 -0
- package/dist/client/internal/protocols/X402.d.ts +8 -0
- package/dist/client/internal/protocols/X402.d.ts.map +1 -0
- package/dist/client/internal/protocols/X402.js +39 -0
- package/dist/client/internal/protocols/X402.js.map +1 -0
- package/dist/evm/client/index.d.ts +1 -0
- package/dist/evm/client/index.d.ts.map +1 -1
- package/dist/evm/client/index.js +1 -0
- package/dist/evm/client/index.js.map +1 -1
- package/dist/evm/index.d.ts +2 -0
- package/dist/evm/index.d.ts.map +1 -1
- package/dist/evm/index.js +2 -0
- package/dist/evm/index.js.map +1 -1
- package/dist/evm/server/Methods.d.ts +1 -1
- package/dist/evm/server/Methods.d.ts.map +1 -1
- package/dist/evm/server/index.d.ts +1 -0
- package/dist/evm/server/index.d.ts.map +1 -1
- package/dist/evm/server/index.js +1 -0
- package/dist/evm/server/index.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/internal/AcceptPayment.d.ts +3 -0
- package/dist/internal/AcceptPayment.d.ts.map +1 -1
- package/dist/internal/AcceptPayment.js +15 -11
- package/dist/internal/AcceptPayment.js.map +1 -1
- package/dist/mcp/client/McpClient.d.ts +101 -0
- package/dist/mcp/client/McpClient.d.ts.map +1 -0
- package/dist/mcp/client/McpClient.js +162 -0
- package/dist/mcp/client/McpClient.js.map +1 -0
- package/dist/mcp/client/index.d.ts.map +1 -0
- package/dist/mcp/client/index.js.map +1 -0
- package/dist/mcp/server/Transport.d.ts.map +1 -0
- package/dist/mcp/server/Transport.js.map +1 -0
- package/dist/mcp/server/index.d.ts.map +1 -0
- package/dist/mcp/server/index.js.map +1 -0
- package/dist/server/Mppx.d.ts +12 -4
- package/dist/server/Mppx.d.ts.map +1 -1
- package/dist/server/Mppx.js +85 -27
- package/dist/server/Mppx.js.map +1 -1
- package/dist/server/Response.d.ts.map +1 -1
- package/dist/server/Response.js +2 -1
- package/dist/server/Response.js.map +1 -1
- package/dist/server/Transport.d.ts +1 -1
- package/dist/server/Transport.d.ts.map +1 -1
- package/dist/server/Transport.js +5 -4
- package/dist/server/Transport.js.map +1 -1
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +1 -0
- package/dist/server/index.js.map +1 -1
- package/dist/stripe/client/Charge.d.ts +1 -1
- package/dist/stripe/client/Charge.d.ts.map +1 -1
- package/dist/stripe/client/Charge.js +3 -1
- package/dist/stripe/client/Charge.js.map +1 -1
- package/dist/stripe/server/Charge.d.ts +1 -1
- package/dist/stripe/server/Charge.d.ts.map +1 -1
- package/dist/stripe/server/Charge.js +9 -2
- package/dist/stripe/server/Charge.js.map +1 -1
- package/dist/stripe/server/Methods.d.ts +1 -1
- package/dist/stripe/server/Methods.d.ts.map +1 -1
- package/dist/stripe/server/internal/html.gen.d.ts +1 -1
- package/dist/stripe/server/internal/html.gen.d.ts.map +1 -1
- package/dist/stripe/server/internal/html.gen.js +1 -1
- package/dist/stripe/server/internal/html.gen.js.map +1 -1
- package/dist/tempo/Methods.d.ts +18 -0
- package/dist/tempo/Methods.d.ts.map +1 -1
- package/dist/tempo/Methods.js +16 -1
- package/dist/tempo/Methods.js.map +1 -1
- package/dist/tempo/Proof.d.ts +85 -1
- package/dist/tempo/Proof.d.ts.map +1 -1
- package/dist/tempo/Proof.js +35 -0
- package/dist/tempo/Proof.js.map +1 -1
- package/dist/tempo/client/Charge.d.ts +19 -1
- package/dist/tempo/client/Charge.d.ts.map +1 -1
- package/dist/tempo/client/Charge.js +47 -27
- package/dist/tempo/client/Charge.js.map +1 -1
- package/dist/tempo/client/Methods.d.ts +41 -10
- package/dist/tempo/client/Methods.d.ts.map +1 -1
- package/dist/tempo/client/Methods.js +16 -7
- package/dist/tempo/client/Methods.js.map +1 -1
- package/dist/tempo/client/ResolveAccount.d.ts +40 -0
- package/dist/tempo/client/ResolveAccount.d.ts.map +1 -0
- package/dist/tempo/client/ResolveAccount.js +2 -0
- package/dist/tempo/client/ResolveAccount.js.map +1 -0
- package/dist/tempo/client/index.d.ts +7 -4
- package/dist/tempo/client/index.d.ts.map +1 -1
- package/dist/tempo/client/index.js +5 -3
- package/dist/tempo/client/index.js.map +1 -1
- package/dist/tempo/index.d.ts +1 -0
- package/dist/tempo/index.d.ts.map +1 -1
- package/dist/tempo/index.js +1 -0
- package/dist/tempo/index.js.map +1 -1
- package/dist/tempo/internal/fee-payer.d.ts +29 -1
- package/dist/tempo/internal/fee-payer.d.ts.map +1 -1
- package/dist/tempo/internal/fee-payer.js +138 -4
- package/dist/tempo/internal/fee-payer.js.map +1 -1
- package/dist/tempo/internal/proof.d.ts +71 -5
- package/dist/tempo/internal/proof.d.ts.map +1 -1
- package/dist/tempo/internal/proof.js +42 -6
- package/dist/tempo/internal/proof.js.map +1 -1
- package/dist/tempo/{client → legacy/client}/ChannelOps.d.ts +19 -6
- package/dist/tempo/legacy/client/ChannelOps.d.ts.map +1 -0
- package/dist/tempo/{client → legacy/client}/ChannelOps.js +9 -3
- package/dist/tempo/legacy/client/ChannelOps.js.map +1 -0
- package/dist/tempo/{client → legacy/client}/Session.d.ts +23 -4
- package/dist/tempo/legacy/client/Session.d.ts.map +1 -0
- package/dist/tempo/{client → legacy/client}/Session.js +14 -7
- package/dist/tempo/legacy/client/Session.js.map +1 -0
- package/dist/tempo/{client → legacy/client}/SessionManager.d.ts +20 -5
- package/dist/tempo/legacy/client/SessionManager.d.ts.map +1 -0
- package/dist/tempo/{client → legacy/client}/SessionManager.js +30 -19
- package/dist/tempo/legacy/client/SessionManager.js.map +1 -0
- package/dist/tempo/legacy/client/index.d.ts +7 -0
- package/dist/tempo/legacy/client/index.d.ts.map +1 -0
- package/dist/tempo/legacy/client/index.js +5 -0
- package/dist/tempo/legacy/client/index.js.map +1 -0
- package/dist/tempo/legacy/index.d.ts +7 -0
- package/dist/tempo/legacy/index.d.ts.map +1 -0
- package/dist/tempo/legacy/index.js +7 -0
- package/dist/tempo/legacy/index.js.map +1 -0
- package/dist/tempo/{server → legacy/server}/Session.d.ts +28 -11
- package/dist/tempo/legacy/server/Session.d.ts.map +1 -0
- package/dist/tempo/{server → legacy/server}/Session.js +12 -10
- package/dist/tempo/legacy/server/Session.js.map +1 -0
- package/dist/tempo/legacy/server/index.d.ts +5 -0
- package/dist/tempo/legacy/server/index.d.ts.map +1 -0
- package/dist/tempo/legacy/server/index.js +5 -0
- package/dist/tempo/legacy/server/index.js.map +1 -0
- package/dist/tempo/{session → legacy/session}/Chain.d.ts +30 -23
- package/dist/tempo/legacy/session/Chain.d.ts.map +1 -0
- package/dist/tempo/{session → legacy/session}/Chain.js +12 -11
- package/dist/tempo/legacy/session/Chain.js.map +1 -0
- package/dist/tempo/{session → legacy/session}/Channel.d.ts +1 -0
- package/dist/tempo/legacy/session/Channel.d.ts.map +1 -0
- package/dist/tempo/legacy/session/Channel.js.map +1 -0
- package/dist/tempo/legacy/session/ChannelStore.d.ts +22 -0
- package/dist/tempo/legacy/session/ChannelStore.d.ts.map +1 -0
- package/dist/tempo/legacy/session/ChannelStore.js +6 -0
- package/dist/tempo/legacy/session/ChannelStore.js.map +1 -0
- package/dist/tempo/legacy/session/Types.d.ts +73 -0
- package/dist/tempo/legacy/session/Types.d.ts.map +1 -0
- package/dist/tempo/legacy/session/Types.js.map +1 -0
- package/dist/tempo/{session → legacy/session}/Voucher.d.ts +4 -4
- package/dist/tempo/legacy/session/Voucher.d.ts.map +1 -0
- package/dist/tempo/{session → legacy/session}/Voucher.js +1 -1
- package/dist/tempo/legacy/session/Voucher.js.map +1 -0
- package/dist/tempo/{session → legacy/session}/escrow.abi.d.ts +1 -0
- package/dist/tempo/{session → legacy/session}/escrow.abi.d.ts.map +1 -1
- package/dist/tempo/{session → legacy/session}/escrow.abi.js +1 -0
- package/dist/tempo/legacy/session/escrow.abi.js.map +1 -0
- package/dist/tempo/legacy/session/index.d.ts +9 -0
- package/dist/tempo/legacy/session/index.d.ts.map +1 -0
- package/dist/tempo/legacy/session/index.js +9 -0
- package/dist/tempo/legacy/session/index.js.map +1 -0
- package/dist/tempo/server/Charge.d.ts +1 -1
- package/dist/tempo/server/Charge.d.ts.map +1 -1
- package/dist/tempo/server/Charge.js +51 -30
- package/dist/tempo/server/Charge.js.map +1 -1
- package/dist/tempo/server/Methods.d.ts +67 -8
- package/dist/tempo/server/Methods.d.ts.map +1 -1
- package/dist/tempo/server/Methods.js +40 -10
- package/dist/tempo/server/Methods.js.map +1 -1
- package/dist/tempo/server/Subscription.d.ts +11 -1
- package/dist/tempo/server/Subscription.d.ts.map +1 -1
- package/dist/tempo/server/Subscription.js +135 -23
- package/dist/tempo/server/Subscription.js.map +1 -1
- package/dist/tempo/server/index.d.ts +6 -5
- package/dist/tempo/server/index.d.ts.map +1 -1
- package/dist/tempo/server/index.js +5 -5
- package/dist/tempo/server/index.js.map +1 -1
- package/dist/tempo/server/internal/html.gen.d.ts +1 -1
- package/dist/tempo/server/internal/html.gen.d.ts.map +1 -1
- package/dist/tempo/server/internal/html.gen.js +1 -1
- package/dist/tempo/server/internal/html.gen.js.map +1 -1
- package/dist/tempo/server/internal/request-body.d.ts +7 -2
- package/dist/tempo/server/internal/request-body.d.ts.map +1 -1
- package/dist/tempo/server/internal/request-body.js +20 -3
- package/dist/tempo/server/internal/request-body.js.map +1 -1
- package/dist/tempo/server/internal/transport.d.ts +8 -4
- package/dist/tempo/server/internal/transport.d.ts.map +1 -1
- package/dist/tempo/server/internal/transport.js +8 -7
- package/dist/tempo/server/internal/transport.js.map +1 -1
- package/dist/tempo/session/Snapshot.d.ts +32 -0
- package/dist/tempo/session/Snapshot.d.ts.map +1 -0
- package/dist/tempo/session/Snapshot.js +37 -0
- package/dist/tempo/session/Snapshot.js.map +1 -0
- package/dist/tempo/session/client/ChannelOps.d.ts +81 -0
- package/dist/tempo/session/client/ChannelOps.d.ts.map +1 -0
- package/dist/tempo/session/client/ChannelOps.js +201 -0
- package/dist/tempo/session/client/ChannelOps.js.map +1 -0
- package/dist/tempo/session/client/ChannelStore.d.ts +51 -0
- package/dist/tempo/session/client/ChannelStore.d.ts.map +1 -0
- package/dist/tempo/session/client/ChannelStore.js +63 -0
- package/dist/tempo/session/client/ChannelStore.js.map +1 -0
- package/dist/tempo/session/client/CredentialState.d.ts +245 -0
- package/dist/tempo/session/client/CredentialState.d.ts.map +1 -0
- package/dist/tempo/session/client/CredentialState.js +419 -0
- package/dist/tempo/session/client/CredentialState.js.map +1 -0
- package/dist/tempo/session/client/ReceiptCoordinator.d.ts +26 -0
- package/dist/tempo/session/client/ReceiptCoordinator.d.ts.map +1 -0
- package/dist/tempo/session/client/ReceiptCoordinator.js +61 -0
- package/dist/tempo/session/client/ReceiptCoordinator.js.map +1 -0
- package/dist/tempo/session/client/Runtime.d.ts +464 -0
- package/dist/tempo/session/client/Runtime.d.ts.map +1 -0
- package/dist/tempo/session/client/Runtime.js +499 -0
- package/dist/tempo/session/client/Runtime.js.map +1 -0
- package/dist/tempo/session/client/Session.d.ts +138 -0
- package/dist/tempo/session/client/Session.d.ts.map +1 -0
- package/dist/tempo/session/client/Session.js +69 -0
- package/dist/tempo/session/client/Session.js.map +1 -0
- package/dist/tempo/session/client/SessionManager.d.ts +84 -0
- package/dist/tempo/session/client/SessionManager.d.ts.map +1 -0
- package/dist/tempo/session/client/SessionManager.js +577 -0
- package/dist/tempo/session/client/SessionManager.js.map +1 -0
- package/dist/tempo/session/client/Transports.d.ts +449 -0
- package/dist/tempo/session/client/Transports.d.ts.map +1 -0
- package/dist/tempo/session/client/Transports.js +721 -0
- package/dist/tempo/session/client/Transports.js.map +1 -0
- package/dist/tempo/session/client/index.d.ts +11 -0
- package/dist/tempo/session/client/index.d.ts.map +1 -0
- package/dist/tempo/session/client/index.js +6 -0
- package/dist/tempo/session/client/index.js.map +1 -0
- package/dist/tempo/session/index.d.ts +7 -8
- package/dist/tempo/session/index.d.ts.map +1 -1
- package/dist/tempo/session/index.js +7 -8
- package/dist/tempo/session/index.js.map +1 -1
- package/dist/tempo/session/precompile/Chain.d.ts +319 -0
- package/dist/tempo/session/precompile/Chain.d.ts.map +1 -0
- package/dist/tempo/session/precompile/Chain.js +492 -0
- package/dist/tempo/session/precompile/Chain.js.map +1 -0
- package/dist/tempo/session/precompile/Channel.d.ts +46 -0
- package/dist/tempo/session/precompile/Channel.d.ts.map +1 -0
- package/dist/tempo/session/precompile/Channel.js +56 -0
- package/dist/tempo/session/precompile/Channel.js.map +1 -0
- package/dist/tempo/session/precompile/Protocol.d.ts +308 -0
- package/dist/tempo/session/precompile/Protocol.d.ts.map +1 -0
- package/dist/tempo/session/precompile/Protocol.js +264 -0
- package/dist/tempo/session/precompile/Protocol.js.map +1 -0
- package/dist/tempo/session/precompile/Voucher.d.ts +40 -0
- package/dist/tempo/session/precompile/Voucher.d.ts.map +1 -0
- package/dist/tempo/session/precompile/Voucher.js +125 -0
- package/dist/tempo/session/precompile/Voucher.js.map +1 -0
- package/dist/tempo/session/precompile/escrow.abi.d.ts +522 -0
- package/dist/tempo/session/precompile/escrow.abi.d.ts.map +1 -0
- package/dist/tempo/session/precompile/escrow.abi.js +224 -0
- package/dist/tempo/session/precompile/escrow.abi.js.map +1 -0
- package/dist/tempo/session/precompile/index.d.ts +24 -0
- package/dist/tempo/session/precompile/index.d.ts.map +1 -0
- package/dist/tempo/session/precompile/index.js +22 -0
- package/dist/tempo/session/precompile/index.js.map +1 -0
- package/dist/tempo/session/server/ChannelOps.d.ts +56 -0
- package/dist/tempo/session/server/ChannelOps.d.ts.map +1 -0
- package/dist/tempo/session/server/ChannelOps.js +91 -0
- package/dist/tempo/session/server/ChannelOps.js.map +1 -0
- package/dist/tempo/session/server/ChannelStore.d.ts +347 -0
- package/dist/tempo/session/server/ChannelStore.d.ts.map +1 -0
- package/dist/tempo/session/server/ChannelStore.js +404 -0
- package/dist/tempo/session/server/ChannelStore.js.map +1 -0
- package/dist/tempo/session/server/CredentialVerification.d.ts +85 -0
- package/dist/tempo/session/server/CredentialVerification.d.ts.map +1 -0
- package/dist/tempo/session/server/CredentialVerification.js +494 -0
- package/dist/tempo/session/server/CredentialVerification.js.map +1 -0
- package/dist/tempo/session/server/MeteredStream.d.ts +40 -0
- package/dist/tempo/session/server/MeteredStream.d.ts.map +1 -0
- package/dist/tempo/session/server/MeteredStream.js +42 -0
- package/dist/tempo/session/server/MeteredStream.js.map +1 -0
- package/dist/tempo/session/server/RequestState.d.ts +208 -0
- package/dist/tempo/session/server/RequestState.d.ts.map +1 -0
- package/dist/tempo/session/server/RequestState.js +252 -0
- package/dist/tempo/session/server/RequestState.js.map +1 -0
- package/dist/tempo/session/server/Session.d.ts +169 -0
- package/dist/tempo/session/server/Session.d.ts.map +1 -0
- package/dist/tempo/session/server/Session.js +351 -0
- package/dist/tempo/session/server/Session.js.map +1 -0
- package/dist/tempo/session/server/Settlement.d.ts +185 -0
- package/dist/tempo/session/server/Settlement.d.ts.map +1 -0
- package/dist/tempo/session/server/Settlement.js +252 -0
- package/dist/tempo/session/server/Settlement.js.map +1 -0
- package/dist/tempo/session/{Sse.d.ts → server/Sse.d.ts} +9 -56
- package/dist/tempo/session/server/Sse.d.ts.map +1 -0
- package/dist/tempo/session/server/Sse.js +184 -0
- package/dist/tempo/session/server/Sse.js.map +1 -0
- package/dist/tempo/session/server/Transports.d.ts +89 -0
- package/dist/tempo/session/server/Transports.d.ts.map +1 -0
- package/dist/tempo/session/server/Transports.js +149 -0
- package/dist/tempo/session/server/Transports.js.map +1 -0
- package/dist/tempo/session/server/Ws.d.ts +48 -0
- package/dist/tempo/session/server/Ws.d.ts.map +1 -0
- package/dist/tempo/session/server/Ws.js +244 -0
- package/dist/tempo/session/server/Ws.js.map +1 -0
- package/dist/tempo/session/server/index.d.ts +4 -0
- package/dist/tempo/session/server/index.d.ts.map +1 -0
- package/dist/tempo/session/server/index.js +2 -0
- package/dist/tempo/session/server/index.js.map +1 -0
- package/dist/tempo/subscription/KeyAuthorization.d.ts +712 -1
- package/dist/tempo/subscription/KeyAuthorization.d.ts.map +1 -1
- package/dist/tempo/subscription/Store.d.ts +2 -0
- package/dist/tempo/subscription/Store.d.ts.map +1 -1
- package/dist/tempo/subscription/Store.js +16 -1
- package/dist/tempo/subscription/Store.js.map +1 -1
- package/dist/x402/index.d.ts +1 -0
- package/dist/x402/index.d.ts.map +1 -1
- package/dist/x402/index.js +1 -0
- package/dist/x402/index.js.map +1 -1
- package/package.json +25 -9
- package/src/Challenge.test.ts +40 -0
- package/src/Challenge.ts +28 -13
- package/src/Constants.ts +58 -0
- package/src/Credential.ts +5 -4
- package/src/Mcp.ts +4 -0
- package/src/Method.ts +46 -5
- package/src/PaymentRequest.ts +10 -10
- package/src/Receipt.ts +3 -2
- package/src/cli/cli.test.ts +38 -43
- package/src/cli/cli.ts +23 -10
- package/src/cli/mcp.test.ts +21 -7
- package/src/cli/plugins/tempo.ts +21 -8
- package/src/cli/utils.test.ts +25 -1
- package/src/cli/utils.ts +10 -0
- package/src/client/Methods.ts +5 -2
- package/src/client/Mppx.test-d.ts +31 -1
- package/src/client/Mppx.test.ts +76 -1
- package/src/client/Mppx.ts +2 -2
- package/src/client/Transport.test.ts +225 -178
- package/src/client/Transport.ts +77 -84
- package/src/client/index.ts +25 -1
- package/src/client/internal/Fetch.test.ts +236 -6
- package/src/client/internal/Fetch.ts +69 -11
- package/src/client/internal/protocols/Mcp.test.ts +220 -0
- package/src/client/internal/protocols/Mcp.ts +162 -0
- package/src/client/internal/protocols/Mpp.ts +21 -0
- package/src/client/internal/protocols/Protocol.ts +10 -0
- package/src/client/internal/protocols/Shared.ts +25 -0
- package/src/client/internal/protocols/X402.ts +42 -0
- package/src/discovery/OpenApi.test.ts +1 -1
- package/src/env.d.ts +1 -1
- package/src/evm/PublicInterface.test-d.ts +1 -1
- package/src/evm/client/index.ts +1 -0
- package/src/evm/index.ts +2 -0
- package/src/evm/server/Charge.test.ts +1 -1
- package/src/evm/server/index.ts +1 -0
- package/src/index.ts +1 -0
- package/src/internal/AcceptPayment.test.ts +61 -0
- package/src/internal/AcceptPayment.ts +21 -14
- package/src/{mcp-sdk → mcp}/client/McpClient.integration.test.ts +18 -11
- package/src/{mcp-sdk → mcp}/client/McpClient.test-d.ts +45 -11
- package/src/{mcp-sdk → mcp}/client/McpClient.test.ts +211 -5
- package/src/mcp/client/McpClient.ts +307 -0
- package/src/mcp/client/McpClient.unit.test.ts +135 -0
- package/src/middlewares/elysia.test.ts +9 -5
- package/src/middlewares/express.test.ts +9 -5
- package/src/middlewares/hono.test.ts +5 -5
- package/src/middlewares/internal/mppx.test.ts +1 -1
- package/src/middlewares/nextjs.test.ts +9 -5
- package/src/proxy/Proxy.test.ts +9 -9
- package/src/proxy/services/anthropic.test.ts +1 -1
- package/src/proxy/services/openai.test.ts +1 -1
- package/src/proxy/services/stripe.test.ts +1 -1
- package/src/server/Mppx.authorize.test.ts +1 -1
- package/src/server/Mppx.test-d.ts +55 -1
- package/src/server/Mppx.test.ts +220 -9
- package/src/server/Mppx.ts +501 -407
- package/src/server/Response.ts +2 -1
- package/src/server/Transport.test.ts +6 -6
- package/src/server/Transport.ts +5 -4
- package/src/server/index.ts +1 -0
- package/src/stripe/Charge.integration.test.ts +1 -1
- package/src/stripe/client/Charge.test.ts +21 -6
- package/src/stripe/client/Charge.ts +6 -2
- package/src/stripe/server/Charge.test.ts +115 -2
- package/src/stripe/server/Charge.ts +13 -2
- package/src/stripe/server/internal/html/package.json +1 -1
- package/src/stripe/server/internal/html.gen.ts +1 -1
- package/src/tempo/AccessKeyAuthorization.test.ts +4 -94
- package/src/tempo/Methods.test.ts +45 -17
- package/src/tempo/Methods.ts +22 -0
- package/src/tempo/Proof.conformance.test.ts +146 -0
- package/src/tempo/Proof.test-d.ts +15 -0
- package/src/tempo/Proof.ts +52 -1
- package/src/tempo/PublicExports.test-d.ts +105 -0
- package/src/tempo/Subscription.integration.test.ts +1 -1
- package/src/tempo/client/Charge.test.ts +258 -0
- package/src/tempo/client/Charge.ts +84 -38
- package/src/tempo/client/Methods.ts +22 -8
- package/src/tempo/client/ResolveAccount.ts +46 -0
- package/src/tempo/client/index.ts +15 -4
- package/src/tempo/index.ts +1 -0
- package/src/tempo/internal/fee-payer.test.ts +296 -17
- package/src/tempo/internal/fee-payer.ts +186 -4
- package/src/tempo/internal/fee-token.test.ts +14 -9
- package/src/tempo/internal/proof.test.ts +12 -4
- package/src/tempo/internal/proof.ts +55 -6
- package/src/tempo/legacy/AccessKeyAuthorization.test.ts +162 -0
- package/src/tempo/legacy/README.md +9 -0
- package/src/tempo/{client → legacy/client}/ChannelOps.test.ts +6 -7
- package/src/tempo/{client → legacy/client}/ChannelOps.ts +22 -9
- package/src/tempo/{client → legacy/client}/Session.test.ts +51 -9
- package/src/tempo/{client → legacy/client}/Session.ts +25 -11
- package/src/tempo/{client → legacy/client}/SessionManager.test.ts +81 -9
- package/src/tempo/{client → legacy/client}/SessionManager.ts +52 -23
- package/src/tempo/legacy/client/index.ts +6 -0
- package/src/tempo/legacy/index.ts +6 -0
- package/src/tempo/{server → legacy/server}/Session.test.ts +136 -71
- package/src/tempo/{server → legacy/server}/Session.ts +32 -23
- package/src/tempo/legacy/server/index.ts +4 -0
- package/src/tempo/{session → legacy/session}/Chain.test.ts +3 -4
- package/src/tempo/{session → legacy/session}/Chain.ts +94 -63
- package/src/tempo/{session → legacy/session}/Channel.ts +1 -0
- package/src/tempo/legacy/session/ChannelStore.test.ts +58 -0
- package/src/tempo/legacy/session/ChannelStore.ts +39 -0
- package/src/tempo/legacy/session/Types.ts +91 -0
- package/src/tempo/{session → legacy/session}/Voucher.ts +12 -8
- package/src/tempo/{session → legacy/session}/escrow.abi.ts +1 -0
- package/src/tempo/legacy/session/index.ts +8 -0
- package/src/tempo/server/AtomicStore.test-d.ts +16 -11
- package/src/tempo/server/Charge.test.ts +480 -31
- package/src/tempo/server/Charge.ts +54 -30
- package/src/tempo/server/Methods.ts +58 -10
- package/src/tempo/server/Sse.test.ts +2 -2
- package/src/tempo/server/Subscription.test.ts +465 -3
- package/src/tempo/server/Subscription.ts +174 -19
- package/src/tempo/server/index.ts +6 -5
- package/src/tempo/server/internal/html/package.json +2 -2
- package/src/tempo/server/internal/html.gen.ts +1 -1
- package/src/tempo/server/internal/request-body.test.ts +37 -4
- package/src/tempo/server/internal/request-body.ts +25 -6
- package/src/tempo/server/internal/transport.test.ts +4 -4
- package/src/tempo/server/internal/transport.ts +19 -10
- package/src/tempo/session/Snapshot.test.ts +41 -0
- package/src/tempo/session/Snapshot.ts +74 -0
- package/src/tempo/session/client/ChannelOps.test.ts +163 -0
- package/src/tempo/session/client/ChannelOps.ts +330 -0
- package/src/tempo/session/client/ChannelStore.ts +111 -0
- package/src/tempo/session/client/CredentialState.test.ts +789 -0
- package/src/tempo/session/client/CredentialState.ts +799 -0
- package/src/tempo/session/client/ReceiptCoordinator.ts +95 -0
- package/src/tempo/session/client/Runtime.test.ts +1092 -0
- package/src/tempo/session/client/Runtime.ts +986 -0
- package/src/tempo/session/client/Session.test.ts +774 -0
- package/src/tempo/session/client/Session.ts +123 -0
- package/src/tempo/session/client/SessionManager.test.ts +1397 -0
- package/src/tempo/session/client/SessionManager.ts +751 -0
- package/src/tempo/session/client/Transports.test.ts +837 -0
- package/src/tempo/session/client/Transports.ts +1292 -0
- package/src/tempo/session/client/index.ts +40 -0
- package/src/tempo/session/index.ts +7 -8
- package/src/tempo/session/precompile/Chain.integration.test.ts +321 -0
- package/src/tempo/session/precompile/Chain.test.ts +1258 -0
- package/src/tempo/session/precompile/Chain.ts +979 -0
- package/src/tempo/session/precompile/Channel.test.ts +138 -0
- package/src/tempo/session/precompile/Channel.ts +103 -0
- package/src/tempo/session/precompile/Protocol.test.ts +358 -0
- package/src/tempo/session/precompile/Protocol.ts +520 -0
- package/src/tempo/session/precompile/Voucher.test.ts +354 -0
- package/src/tempo/session/precompile/Voucher.ts +162 -0
- package/src/tempo/session/precompile/escrow.abi.ts +226 -0
- package/src/tempo/session/precompile/index.ts +33 -0
- package/src/tempo/session/server/ChannelOps.test.ts +129 -0
- package/src/tempo/session/server/ChannelOps.ts +157 -0
- package/src/tempo/session/{ChannelStore.test.ts → server/ChannelStore.test.ts} +536 -29
- package/src/tempo/session/server/ChannelStore.ts +835 -0
- package/src/tempo/session/server/CredentialVerification.test.ts +146 -0
- package/src/tempo/session/server/CredentialVerification.ts +710 -0
- package/src/tempo/session/server/MeteredStream.ts +88 -0
- package/src/tempo/session/server/RequestState.test.ts +531 -0
- package/src/tempo/session/server/RequestState.ts +499 -0
- package/src/tempo/session/server/Session.integration.test.ts +444 -0
- package/src/tempo/session/server/Session.test.ts +3253 -0
- package/src/tempo/session/server/Session.ts +543 -0
- package/src/tempo/session/server/Settlement.test.ts +329 -0
- package/src/tempo/session/server/Settlement.ts +471 -0
- package/src/tempo/session/{Sse.test.ts → server/Sse.test.ts} +37 -3
- package/src/tempo/session/server/Sse.ts +254 -0
- package/src/tempo/session/server/Transports.test.ts +346 -0
- package/src/tempo/session/server/Transports.ts +255 -0
- package/src/tempo/session/{Ws.test.ts → server/Ws.test.ts} +4 -4
- package/src/tempo/session/server/Ws.ts +380 -0
- package/src/tempo/session/server/index.ts +8 -0
- package/src/tempo/subscription/Store.ts +27 -9
- package/src/x402/Exact.e2e.test.ts +1 -1
- package/src/x402/PublicInterface.test-d.ts +1 -1
- package/src/x402/index.ts +1 -0
- package/dist/mcp-sdk/client/McpClient.d.ts +0 -78
- package/dist/mcp-sdk/client/McpClient.d.ts.map +0 -1
- package/dist/mcp-sdk/client/McpClient.js +0 -105
- package/dist/mcp-sdk/client/McpClient.js.map +0 -1
- package/dist/mcp-sdk/client/index.d.ts.map +0 -1
- package/dist/mcp-sdk/client/index.js.map +0 -1
- package/dist/mcp-sdk/server/Transport.d.ts.map +0 -1
- package/dist/mcp-sdk/server/Transport.js.map +0 -1
- package/dist/mcp-sdk/server/index.d.ts.map +0 -1
- package/dist/mcp-sdk/server/index.js.map +0 -1
- package/dist/tempo/client/ChannelOps.d.ts.map +0 -1
- package/dist/tempo/client/ChannelOps.js.map +0 -1
- package/dist/tempo/client/Session.d.ts.map +0 -1
- package/dist/tempo/client/Session.js.map +0 -1
- package/dist/tempo/client/SessionManager.d.ts.map +0 -1
- package/dist/tempo/client/SessionManager.js.map +0 -1
- package/dist/tempo/server/Session.d.ts.map +0 -1
- package/dist/tempo/server/Session.js.map +0 -1
- package/dist/tempo/session/Chain.d.ts.map +0 -1
- package/dist/tempo/session/Chain.js.map +0 -1
- package/dist/tempo/session/Channel.d.ts.map +0 -1
- package/dist/tempo/session/Channel.js.map +0 -1
- package/dist/tempo/session/ChannelStore.d.ts +0 -117
- package/dist/tempo/session/ChannelStore.d.ts.map +0 -1
- package/dist/tempo/session/ChannelStore.js +0 -172
- package/dist/tempo/session/ChannelStore.js.map +0 -1
- package/dist/tempo/session/Receipt.d.ts +0 -22
- package/dist/tempo/session/Receipt.d.ts.map +0 -1
- package/dist/tempo/session/Receipt.js +0 -34
- package/dist/tempo/session/Receipt.js.map +0 -1
- package/dist/tempo/session/Sse.d.ts.map +0 -1
- package/dist/tempo/session/Sse.js +0 -363
- package/dist/tempo/session/Sse.js.map +0 -1
- package/dist/tempo/session/Types.d.ts +0 -78
- package/dist/tempo/session/Types.d.ts.map +0 -1
- package/dist/tempo/session/Types.js.map +0 -1
- package/dist/tempo/session/Voucher.d.ts.map +0 -1
- package/dist/tempo/session/Voucher.js.map +0 -1
- package/dist/tempo/session/Ws.d.ts +0 -87
- package/dist/tempo/session/Ws.d.ts.map +0 -1
- package/dist/tempo/session/Ws.js +0 -443
- package/dist/tempo/session/Ws.js.map +0 -1
- package/dist/tempo/session/escrow.abi.js.map +0 -1
- package/src/mcp-sdk/client/McpClient.ts +0 -196
- package/src/tempo/session/ChannelStore.ts +0 -308
- package/src/tempo/session/Receipt.test.ts +0 -89
- package/src/tempo/session/Receipt.ts +0 -46
- package/src/tempo/session/Sse.ts +0 -462
- package/src/tempo/session/Types.ts +0 -86
- package/src/tempo/session/Ws.ts +0 -576
- /package/dist/{mcp-sdk → mcp}/client/index.d.ts +0 -0
- /package/dist/{mcp-sdk → mcp}/client/index.js +0 -0
- /package/dist/{mcp-sdk → mcp}/server/Transport.d.ts +0 -0
- /package/dist/{mcp-sdk → mcp}/server/Transport.js +0 -0
- /package/dist/{mcp-sdk → mcp}/server/index.d.ts +0 -0
- /package/dist/{mcp-sdk → mcp}/server/index.js +0 -0
- /package/dist/tempo/{session → legacy/session}/Channel.js +0 -0
- /package/dist/tempo/{session → legacy/session}/Types.js +0 -0
- /package/src/{mcp-sdk → mcp}/client/index.ts +0 -0
- /package/src/{mcp-sdk → mcp}/server/Transport.test.ts +0 -0
- /package/src/{mcp-sdk → mcp}/server/Transport.ts +0 -0
- /package/src/{mcp-sdk → mcp}/server/index.ts +0 -0
- /package/src/tempo/{session → legacy/session}/Channel.test.ts +0 -0
- /package/src/tempo/{session → legacy/session}/Voucher.test.ts +0 -0
- /package/src/tempo/session/{Sse.fuzz.test.ts → server/Sse.fuzz.test.ts} +0 -0
|
@@ -0,0 +1,1397 @@
|
|
|
1
|
+
import { createClient, custom, encodeFunctionResult, type Address, type Hex } from 'viem'
|
|
2
|
+
import { privateKeyToAccount } from 'viem/accounts'
|
|
3
|
+
import { describe, expect, test, vi } from 'vp/test'
|
|
4
|
+
|
|
5
|
+
import * as Challenge from '../../../Challenge.js'
|
|
6
|
+
import * as Constants from '../../../Constants.js'
|
|
7
|
+
import * as Credential from '../../../Credential.js'
|
|
8
|
+
import type { ChannelEntry } from '../client/ChannelOps.js'
|
|
9
|
+
import { createJsonChannelStore, entryKey, type ChannelStore } from '../client/ChannelStore.js'
|
|
10
|
+
import * as Channel from '../precompile/Channel.js'
|
|
11
|
+
import { escrowAbi } from '../precompile/escrow.abi.js'
|
|
12
|
+
import { tip20ChannelEscrow } from '../precompile/Protocol.js'
|
|
13
|
+
import { createSessionReceipt, serializeSessionReceipt } from '../precompile/Protocol.js'
|
|
14
|
+
import type { NeedVoucherEvent, SessionReceipt } from '../precompile/Protocol.js'
|
|
15
|
+
import { formatNeedVoucherEvent, parseEvent } from '../precompile/Protocol.js'
|
|
16
|
+
import type { SessionCredentialPayload } from '../precompile/Protocol.js'
|
|
17
|
+
import { computeFallbackCloseAmount, sessionManager } from './SessionManager.js'
|
|
18
|
+
|
|
19
|
+
const channelId = '0x0000000000000000000000000000000000000000000000000000000000000001' as Hex
|
|
20
|
+
const challengeId = 'test-challenge-1'
|
|
21
|
+
const realm = 'test.example.com'
|
|
22
|
+
const account = privateKeyToAccount(
|
|
23
|
+
'0xac0974bec39a17e36ba6a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80',
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
const client = createClient({
|
|
27
|
+
account,
|
|
28
|
+
chain: { id: 4217 } as never,
|
|
29
|
+
transport: custom({
|
|
30
|
+
async request(args) {
|
|
31
|
+
if (args.method === 'eth_chainId') return '0x1079'
|
|
32
|
+
if (args.method === 'eth_getTransactionCount') return '0x0'
|
|
33
|
+
if (args.method === 'eth_estimateGas') return '0x5208'
|
|
34
|
+
if (args.method === 'eth_maxPriorityFeePerGas') return '0x1'
|
|
35
|
+
if (args.method === 'eth_getBlockByNumber') return { baseFeePerGas: '0x1' }
|
|
36
|
+
if (args.method === 'eth_call')
|
|
37
|
+
return encodeFunctionResult({
|
|
38
|
+
abi: escrowAbi,
|
|
39
|
+
functionName: 'getChannelState',
|
|
40
|
+
result: { settled: 0n, deposit: 10_000_000n, closeRequestedAt: 0 },
|
|
41
|
+
})
|
|
42
|
+
throw new Error(`unexpected rpc request: ${args.method}`)
|
|
43
|
+
},
|
|
44
|
+
}),
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
const storedDescriptor = {
|
|
48
|
+
authorizedSigner: account.address,
|
|
49
|
+
expiringNonceHash: `0x${'11'.repeat(32)}` as Hex,
|
|
50
|
+
operator: '0x0000000000000000000000000000000000000000' as Address,
|
|
51
|
+
payee: '0x742d35cc6634c0532925a3b844bc9e7595f8fe00' as Address,
|
|
52
|
+
payer: account.address,
|
|
53
|
+
salt: `0x${'22'.repeat(32)}` as Hex,
|
|
54
|
+
token: '0x20c0000000000000000000000000000000000001' as Address,
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const storedChannelId = Channel.computeId({
|
|
58
|
+
...storedDescriptor,
|
|
59
|
+
chainId: 4217,
|
|
60
|
+
escrow: tip20ChannelEscrow,
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
function channelEntry(overrides: Partial<ChannelEntry> = {}): ChannelEntry {
|
|
64
|
+
return {
|
|
65
|
+
channelId: storedChannelId,
|
|
66
|
+
cumulativeAmount: 1_000_000n,
|
|
67
|
+
deposit: 10_000_000n,
|
|
68
|
+
descriptor: storedDescriptor,
|
|
69
|
+
escrow: tip20ChannelEscrow,
|
|
70
|
+
chainId: 4217,
|
|
71
|
+
opened: true,
|
|
72
|
+
...overrides,
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* In-memory {@link ChannelStore} with spied `set`/`delete`, optionally seeded.
|
|
78
|
+
* Seeded entries live in the entry index, so the plugin resumes them from
|
|
79
|
+
* `store.get(resolved.key)` after a 402 (there is no first-request hint).
|
|
80
|
+
*/
|
|
81
|
+
function makeChannelStore(seed: readonly ChannelEntry[] = []) {
|
|
82
|
+
const map = new Map<string, ChannelEntry>(seed.map((entry) => [entryKey(entry), entry]))
|
|
83
|
+
const set = vi.fn((entry: ChannelEntry) => {
|
|
84
|
+
map.set(entryKey(entry), entry)
|
|
85
|
+
})
|
|
86
|
+
const remove = vi.fn((key: string) => {
|
|
87
|
+
map.delete(key)
|
|
88
|
+
})
|
|
89
|
+
const store: ChannelStore = {
|
|
90
|
+
get: (key) => map.get(key),
|
|
91
|
+
set,
|
|
92
|
+
delete: remove,
|
|
93
|
+
}
|
|
94
|
+
return { store, set, delete: remove, map }
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function makeChallenge(overrides: Record<string, unknown> = {}): Challenge.Challenge {
|
|
98
|
+
return Challenge.from({
|
|
99
|
+
id: challengeId,
|
|
100
|
+
realm,
|
|
101
|
+
method: 'tempo',
|
|
102
|
+
intent: 'session',
|
|
103
|
+
request: {
|
|
104
|
+
amount: '1000000',
|
|
105
|
+
currency: '0x20c0000000000000000000000000000000000001',
|
|
106
|
+
recipient: '0x742d35cc6634c0532925a3b844bc9e7595f8fe00',
|
|
107
|
+
decimals: 6,
|
|
108
|
+
methodDetails: {
|
|
109
|
+
escrowContract: tip20ChannelEscrow,
|
|
110
|
+
chainId: 4217,
|
|
111
|
+
sessionProtocol: Constants.SessionProtocols.v2,
|
|
112
|
+
},
|
|
113
|
+
...overrides,
|
|
114
|
+
},
|
|
115
|
+
})
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function makeChargeChallenge(overrides: Record<string, unknown> = {}): Challenge.Challenge {
|
|
119
|
+
return Challenge.from({
|
|
120
|
+
id: 'charge-bootstrap',
|
|
121
|
+
realm,
|
|
122
|
+
method: 'tempo',
|
|
123
|
+
intent: 'charge',
|
|
124
|
+
request: {
|
|
125
|
+
amount: '0',
|
|
126
|
+
currency: '0x20c0000000000000000000000000000000000001',
|
|
127
|
+
recipient: '0x742d35cc6634c0532925a3b844bc9e7595f8fe00',
|
|
128
|
+
methodDetails: { chainId: 4217 },
|
|
129
|
+
...overrides,
|
|
130
|
+
},
|
|
131
|
+
})
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function make402Response(challenge?: Challenge.Challenge): Response {
|
|
135
|
+
const c = challenge ?? makeChallenge()
|
|
136
|
+
return new Response(null, {
|
|
137
|
+
status: 402,
|
|
138
|
+
headers: { 'WWW-Authenticate': Challenge.serialize(c) },
|
|
139
|
+
})
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function makeOkResponse(body?: string): Response {
|
|
143
|
+
return new Response(body ?? 'ok', { status: 200 })
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function makeSseResponse(events: string[]): Response {
|
|
147
|
+
const body = events.join('')
|
|
148
|
+
return new Response(body, {
|
|
149
|
+
status: 200,
|
|
150
|
+
headers: { 'Content-Type': 'text/event-stream' },
|
|
151
|
+
})
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
describe('Session', () => {
|
|
155
|
+
describe('computeFallbackCloseAmount', () => {
|
|
156
|
+
test('uses matching close-ready receipt spend first', () => {
|
|
157
|
+
const receipt = createSessionReceipt({
|
|
158
|
+
acceptedCumulative: 100n,
|
|
159
|
+
challengeId,
|
|
160
|
+
channelId,
|
|
161
|
+
spent: 80n,
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
expect(
|
|
165
|
+
computeFallbackCloseAmount({
|
|
166
|
+
challengeId,
|
|
167
|
+
channelId,
|
|
168
|
+
closeReadyReceipt: receipt,
|
|
169
|
+
cumulativeAmount: 100n,
|
|
170
|
+
deliveredChunks: 10n,
|
|
171
|
+
socketChallengeId: challengeId,
|
|
172
|
+
socketChannelId: channelId,
|
|
173
|
+
spent: 50n,
|
|
174
|
+
tickCost: 10n,
|
|
175
|
+
}),
|
|
176
|
+
).toBe(80n)
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
test('uses matching socket delivery estimate clamped to cumulative authorization', () => {
|
|
180
|
+
expect(
|
|
181
|
+
computeFallbackCloseAmount({
|
|
182
|
+
challengeId,
|
|
183
|
+
channelId,
|
|
184
|
+
cumulativeAmount: 90n,
|
|
185
|
+
deliveredChunks: 10n,
|
|
186
|
+
socketChallengeId: challengeId,
|
|
187
|
+
socketChannelId: channelId,
|
|
188
|
+
spent: 40n,
|
|
189
|
+
tickCost: 10n,
|
|
190
|
+
}),
|
|
191
|
+
).toBe(90n)
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
test('uses receipt-tracked spend when no socket estimate applies', () => {
|
|
195
|
+
expect(
|
|
196
|
+
computeFallbackCloseAmount({
|
|
197
|
+
challengeId,
|
|
198
|
+
channelId,
|
|
199
|
+
cumulativeAmount: 100n,
|
|
200
|
+
deliveredChunks: 10n,
|
|
201
|
+
socketChallengeId: 'other-challenge',
|
|
202
|
+
socketChannelId: channelId,
|
|
203
|
+
spent: 40n,
|
|
204
|
+
tickCost: 10n,
|
|
205
|
+
}),
|
|
206
|
+
).toBe(40n)
|
|
207
|
+
})
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
describe('parseEvent round-trip via SSE', () => {
|
|
211
|
+
test('parses message events from SSE stream', () => {
|
|
212
|
+
const raw = 'event: message\ndata: hello world\n\n'
|
|
213
|
+
const event = parseEvent(raw)
|
|
214
|
+
expect(event).toEqual({ type: 'message', data: 'hello world' })
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
test('parses payment-need-voucher events', () => {
|
|
218
|
+
const params: NeedVoucherEvent = {
|
|
219
|
+
channelId,
|
|
220
|
+
requiredCumulative: '6000000',
|
|
221
|
+
acceptedCumulative: '5000000',
|
|
222
|
+
deposit: '10000000',
|
|
223
|
+
}
|
|
224
|
+
const raw = formatNeedVoucherEvent(params)
|
|
225
|
+
const event = parseEvent(raw)
|
|
226
|
+
expect(event).toEqual({ type: 'payment-need-voucher', data: params })
|
|
227
|
+
})
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
describe('session creation', () => {
|
|
231
|
+
test('creates session with initial state', () => {
|
|
232
|
+
const s = sessionManager({
|
|
233
|
+
account: '0x0000000000000000000000000000000000000001',
|
|
234
|
+
maxDeposit: '10',
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
expect(s.channelId).toBeUndefined()
|
|
238
|
+
expect(s.cumulative).toBe(0n)
|
|
239
|
+
expect(s.opened).toBe(false)
|
|
240
|
+
})
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
describe('.fetch()', () => {
|
|
244
|
+
test('passes through non-402 responses', async () => {
|
|
245
|
+
const mockFetch = vi.fn().mockResolvedValue(makeOkResponse('hello'))
|
|
246
|
+
|
|
247
|
+
const s = sessionManager({
|
|
248
|
+
account: '0x0000000000000000000000000000000000000001',
|
|
249
|
+
fetch: mockFetch as typeof globalThis.fetch,
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
const res = await s.fetch('https://api.example.com/data')
|
|
253
|
+
expect(res.status).toBe(200)
|
|
254
|
+
expect(await res.text()).toBe('hello')
|
|
255
|
+
expect(mockFetch).toHaveBeenCalledOnce()
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
test('rejects a concurrent request while one is in flight', async () => {
|
|
259
|
+
let release!: () => void
|
|
260
|
+
const gate = new Promise<void>((resolve) => {
|
|
261
|
+
release = resolve
|
|
262
|
+
})
|
|
263
|
+
const mockFetch = vi.fn().mockImplementation(async () => {
|
|
264
|
+
await gate
|
|
265
|
+
return makeOkResponse('hello')
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
const s = sessionManager({
|
|
269
|
+
account: '0x0000000000000000000000000000000000000001',
|
|
270
|
+
fetch: mockFetch as typeof globalThis.fetch,
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
const first = s.fetch('https://api.example.com/data')
|
|
274
|
+
await expect(s.fetch('https://api.example.com/data')).rejects.toThrow(
|
|
275
|
+
'concurrent requests on one manager are not supported',
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
release()
|
|
279
|
+
expect((await first).status).toBe(200)
|
|
280
|
+
|
|
281
|
+
// The guard clears after the in-flight request settles.
|
|
282
|
+
expect((await s.fetch('https://api.example.com/data')).status).toBe(200)
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
test('binds the default global fetch for browser runtimes', async () => {
|
|
286
|
+
const originalFetch = globalThis.fetch
|
|
287
|
+
const mockFetch = vi.fn(function (this: unknown) {
|
|
288
|
+
expect(this).toBe(globalThis)
|
|
289
|
+
return Promise.resolve(makeOkResponse('hello'))
|
|
290
|
+
})
|
|
291
|
+
globalThis.fetch = mockFetch as typeof globalThis.fetch
|
|
292
|
+
try {
|
|
293
|
+
const s = sessionManager({
|
|
294
|
+
account: '0x0000000000000000000000000000000000000001',
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
const res = await s.fetch('https://api.example.com/data')
|
|
298
|
+
|
|
299
|
+
expect(res.status).toBe(200)
|
|
300
|
+
expect(mockFetch).toHaveBeenCalledOnce()
|
|
301
|
+
} finally {
|
|
302
|
+
globalThis.fetch = originalFetch
|
|
303
|
+
}
|
|
304
|
+
})
|
|
305
|
+
|
|
306
|
+
test('resumes a seeded channel after a 402 without a first-request hint', async () => {
|
|
307
|
+
const posted: SessionCredentialPayload[] = []
|
|
308
|
+
const mockFetch = vi.fn().mockImplementation((_input, init?: RequestInit) => {
|
|
309
|
+
const authorization = new Headers(init?.headers).get(Constants.Headers.authorization)
|
|
310
|
+
const payload = authorization
|
|
311
|
+
? Credential.deserialize<SessionCredentialPayload>(authorization).payload
|
|
312
|
+
: undefined
|
|
313
|
+
if (payload) posted.push(payload)
|
|
314
|
+
if (!payload) return Promise.resolve(make402Response())
|
|
315
|
+
return Promise.resolve(makeOkResponse())
|
|
316
|
+
})
|
|
317
|
+
const s = sessionManager({
|
|
318
|
+
account,
|
|
319
|
+
client,
|
|
320
|
+
fetch: mockFetch as typeof globalThis.fetch,
|
|
321
|
+
channelStore: makeChannelStore([channelEntry()]).store,
|
|
322
|
+
})
|
|
323
|
+
|
|
324
|
+
await s.fetch('https://api.example.com/data')
|
|
325
|
+
|
|
326
|
+
// No persisted hint: the first request carries no Payment-Session header,
|
|
327
|
+
// and the channel is resumed from the entry index with a voucher.
|
|
328
|
+
expect(new Headers(mockFetch.mock.calls[0]?.[1]?.headers).get('Payment-Session')).toBeNull()
|
|
329
|
+
expect(posted[0]).toMatchObject({ action: 'voucher', channelId: storedChannelId })
|
|
330
|
+
})
|
|
331
|
+
|
|
332
|
+
test('seeds a same-route HEAD snapshot into the entry index and resumes it', async () => {
|
|
333
|
+
const { store, set } = makeChannelStore()
|
|
334
|
+
const posted: SessionCredentialPayload[] = []
|
|
335
|
+
const mockFetch = vi.fn().mockImplementation((_input, init?: RequestInit) => {
|
|
336
|
+
const headers = new Headers(init?.headers)
|
|
337
|
+
if (init?.method === 'HEAD' && !headers.get(Constants.Headers.authorization)) {
|
|
338
|
+
expect(headers.get(Constants.Headers.acceptPayment)).toBe('tempo/charge')
|
|
339
|
+
return Promise.resolve(make402Response(makeChargeChallenge()))
|
|
340
|
+
}
|
|
341
|
+
if (init?.method === 'HEAD') {
|
|
342
|
+
const credential = Credential.deserialize(headers.get(Constants.Headers.authorization)!)
|
|
343
|
+
expect(credential.payload).toMatchObject({ type: 'proof' })
|
|
344
|
+
return Promise.resolve(
|
|
345
|
+
new Response(null, {
|
|
346
|
+
status: 204,
|
|
347
|
+
headers: {
|
|
348
|
+
[Constants.Headers.paymentSessionSnapshot]: sessionManager.serializeSnapshot({
|
|
349
|
+
acceptedCumulative: '1000000',
|
|
350
|
+
chainId: 4217,
|
|
351
|
+
channelId: storedChannelId,
|
|
352
|
+
deposit: '10000000',
|
|
353
|
+
descriptor: storedDescriptor,
|
|
354
|
+
escrow: tip20ChannelEscrow,
|
|
355
|
+
requiredCumulative: '1000000',
|
|
356
|
+
settled: '0',
|
|
357
|
+
spent: '0',
|
|
358
|
+
units: 0,
|
|
359
|
+
}),
|
|
360
|
+
},
|
|
361
|
+
}),
|
|
362
|
+
)
|
|
363
|
+
}
|
|
364
|
+
// The seeded snapshot is not sent as a first-request hint; the content
|
|
365
|
+
// request gets a 402 and resumes from the entry index with a voucher.
|
|
366
|
+
const authorization = headers.get(Constants.Headers.authorization)
|
|
367
|
+
const payload = authorization
|
|
368
|
+
? Credential.deserialize<SessionCredentialPayload>(authorization).payload
|
|
369
|
+
: undefined
|
|
370
|
+
if (payload) posted.push(payload)
|
|
371
|
+
if (!payload) return Promise.resolve(make402Response())
|
|
372
|
+
return Promise.resolve(makeOkResponse())
|
|
373
|
+
})
|
|
374
|
+
const s = sessionManager({
|
|
375
|
+
account,
|
|
376
|
+
bootstrap: true,
|
|
377
|
+
client,
|
|
378
|
+
fetch: mockFetch as typeof globalThis.fetch,
|
|
379
|
+
channelStore: store,
|
|
380
|
+
})
|
|
381
|
+
|
|
382
|
+
const response = await s.fetch('https://api.example.com/data')
|
|
383
|
+
|
|
384
|
+
expect(response.status).toBe(200)
|
|
385
|
+
expect(set).toHaveBeenCalledWith(expect.objectContaining({ channelId: storedChannelId }))
|
|
386
|
+
expect(posted[0]).toMatchObject({ action: 'voucher', channelId: storedChannelId })
|
|
387
|
+
const contentCall = mockFetch.mock.calls.find((call) => call[1]?.method !== 'HEAD')
|
|
388
|
+
expect(new Headers(contentCall?.[1]?.headers).get('Payment-Session')).toBeNull()
|
|
389
|
+
})
|
|
390
|
+
|
|
391
|
+
test('does not answer non-zero bootstrap charge challenges', async () => {
|
|
392
|
+
const mockFetch = vi.fn().mockImplementation((_input, init?: RequestInit) => {
|
|
393
|
+
const headers = new Headers(init?.headers)
|
|
394
|
+
if (init?.method === 'HEAD' && !headers.get(Constants.Headers.authorization)) {
|
|
395
|
+
return Promise.resolve(make402Response(makeChargeChallenge({ amount: '1' })))
|
|
396
|
+
}
|
|
397
|
+
if (init?.method === 'HEAD') throw new Error('unexpected bootstrap authorization')
|
|
398
|
+
expect(headers.get(Constants.Headers.paymentSession)).toBeNull()
|
|
399
|
+
return Promise.resolve(makeOkResponse())
|
|
400
|
+
})
|
|
401
|
+
const s = sessionManager({
|
|
402
|
+
account,
|
|
403
|
+
bootstrap: true,
|
|
404
|
+
client,
|
|
405
|
+
fetch: mockFetch as typeof globalThis.fetch,
|
|
406
|
+
})
|
|
407
|
+
|
|
408
|
+
const response = await s.fetch('https://api.example.com/data')
|
|
409
|
+
|
|
410
|
+
expect(response.status).toBe(200)
|
|
411
|
+
expect(mockFetch).toHaveBeenCalledTimes(2)
|
|
412
|
+
})
|
|
413
|
+
|
|
414
|
+
test('falls back to normal fetch when bootstrap is unsupported', async () => {
|
|
415
|
+
const mockFetch = vi
|
|
416
|
+
.fn()
|
|
417
|
+
.mockResolvedValueOnce(new Response(null, { status: 404 }))
|
|
418
|
+
.mockResolvedValueOnce(makeOkResponse())
|
|
419
|
+
const s = sessionManager({
|
|
420
|
+
account,
|
|
421
|
+
bootstrap: true,
|
|
422
|
+
client,
|
|
423
|
+
fetch: mockFetch as typeof globalThis.fetch,
|
|
424
|
+
})
|
|
425
|
+
|
|
426
|
+
const response = await s.fetch('https://api.example.com/data')
|
|
427
|
+
|
|
428
|
+
expect(response.status).toBe(200)
|
|
429
|
+
expect(s.channelId).toBeUndefined()
|
|
430
|
+
expect(mockFetch).toHaveBeenCalledTimes(2)
|
|
431
|
+
expect(mockFetch.mock.calls[0]?.[1]).toMatchObject({ method: 'HEAD' })
|
|
432
|
+
expect(new Headers(mockFetch.mock.calls[1]?.[1]?.headers).get('Payment-Session')).toBeNull()
|
|
433
|
+
})
|
|
434
|
+
|
|
435
|
+
test('drops a stale stored channel the server rejects and retries with a fresh one', async () => {
|
|
436
|
+
const { store, delete: remove } = makeChannelStore([channelEntry()])
|
|
437
|
+
const postedPayloads: SessionCredentialPayload[] = []
|
|
438
|
+
const mockFetch = vi.fn().mockImplementation((_input, init?: RequestInit) => {
|
|
439
|
+
const headers = new Headers(init?.headers)
|
|
440
|
+
const authorization = headers.get(Constants.Headers.authorization)
|
|
441
|
+
const payload = authorization
|
|
442
|
+
? Credential.deserialize<SessionCredentialPayload>(authorization).payload
|
|
443
|
+
: undefined
|
|
444
|
+
if (payload) postedPayloads.push(payload)
|
|
445
|
+
|
|
446
|
+
if (!payload) return Promise.resolve(make402Response())
|
|
447
|
+
// Reject any reuse of the stale stored channel; accept a freshly opened one.
|
|
448
|
+
if (payload.channelId === storedChannelId)
|
|
449
|
+
return Promise.resolve(new Response('gone', { status: 500 }))
|
|
450
|
+
return Promise.resolve(makeOkResponse())
|
|
451
|
+
})
|
|
452
|
+
const s = sessionManager({
|
|
453
|
+
account,
|
|
454
|
+
client,
|
|
455
|
+
fetch: mockFetch as typeof globalThis.fetch,
|
|
456
|
+
maxDeposit: '10',
|
|
457
|
+
channelStore: store,
|
|
458
|
+
})
|
|
459
|
+
|
|
460
|
+
const response = await s.fetch('https://api.example.com/data')
|
|
461
|
+
|
|
462
|
+
expect(response.status).toBe(200)
|
|
463
|
+
expect(remove).toHaveBeenCalledOnce()
|
|
464
|
+
expect(postedPayloads.map((payload) => payload.action)).toEqual(['voucher', 'open'])
|
|
465
|
+
expect(s.opened).toBe(true)
|
|
466
|
+
expect(s.channelId).not.toBe(storedChannelId)
|
|
467
|
+
})
|
|
468
|
+
|
|
469
|
+
test('does not bootstrap when disabled', async () => {
|
|
470
|
+
const mockFetch = vi.fn().mockResolvedValue(makeOkResponse())
|
|
471
|
+
const s = sessionManager({
|
|
472
|
+
account,
|
|
473
|
+
client,
|
|
474
|
+
fetch: mockFetch as typeof globalThis.fetch,
|
|
475
|
+
})
|
|
476
|
+
|
|
477
|
+
await s.fetch('https://api.example.com/data')
|
|
478
|
+
|
|
479
|
+
expect(mockFetch).toHaveBeenCalledOnce()
|
|
480
|
+
expect(new Headers(mockFetch.mock.calls[0]?.[1]?.headers).get('Payment-Session')).toBeNull()
|
|
481
|
+
})
|
|
482
|
+
|
|
483
|
+
test('uses stored channel details when the server does not return a snapshot', async () => {
|
|
484
|
+
const postedPayloads: SessionCredentialPayload[] = []
|
|
485
|
+
const mockFetch = vi.fn().mockImplementation((_input, init?: RequestInit) => {
|
|
486
|
+
const authorization = new Headers(init?.headers).get('Authorization')
|
|
487
|
+
const payload = authorization
|
|
488
|
+
? Credential.deserialize<SessionCredentialPayload>(authorization).payload
|
|
489
|
+
: undefined
|
|
490
|
+
if (payload) postedPayloads.push(payload)
|
|
491
|
+
|
|
492
|
+
if (!payload) return Promise.resolve(make402Response())
|
|
493
|
+
return Promise.resolve(makeOkResponse())
|
|
494
|
+
})
|
|
495
|
+
const s = sessionManager({
|
|
496
|
+
account,
|
|
497
|
+
client,
|
|
498
|
+
fetch: mockFetch as typeof globalThis.fetch,
|
|
499
|
+
channelStore: makeChannelStore([channelEntry()]).store,
|
|
500
|
+
})
|
|
501
|
+
|
|
502
|
+
await s.fetch('https://api.example.com/data')
|
|
503
|
+
|
|
504
|
+
expect(postedPayloads[0]).toMatchObject({
|
|
505
|
+
action: 'voucher',
|
|
506
|
+
channelId: storedChannelId,
|
|
507
|
+
})
|
|
508
|
+
})
|
|
509
|
+
|
|
510
|
+
test('resumes a persisted channel across a restart via the entry index', async () => {
|
|
511
|
+
// A shared durable KV backing two manager instances simulates a restart:
|
|
512
|
+
// the second manager shares only what survived to disk.
|
|
513
|
+
const backend = new Map<string, string>()
|
|
514
|
+
const durableStore = () =>
|
|
515
|
+
createJsonChannelStore({
|
|
516
|
+
get: (key) => backend.get(key),
|
|
517
|
+
set: (key, value) => {
|
|
518
|
+
backend.set(key, value)
|
|
519
|
+
},
|
|
520
|
+
delete: (key) => {
|
|
521
|
+
backend.delete(key)
|
|
522
|
+
},
|
|
523
|
+
})
|
|
524
|
+
|
|
525
|
+
const openFetch = vi.fn((_input: RequestInfo | URL, init?: RequestInit) => {
|
|
526
|
+
const authorization = new Headers(init?.headers).get(Constants.Headers.authorization)
|
|
527
|
+
if (!authorization) return Promise.resolve(make402Response())
|
|
528
|
+
return Promise.resolve(makeOkResponse())
|
|
529
|
+
})
|
|
530
|
+
const first = sessionManager({
|
|
531
|
+
account,
|
|
532
|
+
client,
|
|
533
|
+
fetch: openFetch as typeof globalThis.fetch,
|
|
534
|
+
maxDeposit: '10',
|
|
535
|
+
channelStore: durableStore(),
|
|
536
|
+
})
|
|
537
|
+
await first.fetch('https://api.example.com/data')
|
|
538
|
+
const channelId = first.channelId
|
|
539
|
+
expect(channelId).toBeDefined()
|
|
540
|
+
|
|
541
|
+
// The durable channel entry must have reached the KV.
|
|
542
|
+
expect([...backend.keys()].some((key) => key.startsWith('chan:'))).toBe(true)
|
|
543
|
+
// No persistent hints are written; only the durable channel entry survives.
|
|
544
|
+
expect([...backend.keys()].some((key) => key.startsWith('hint:'))).toBe(false)
|
|
545
|
+
|
|
546
|
+
const posted: SessionCredentialPayload[] = []
|
|
547
|
+
const resumeFetch = vi.fn((_input: RequestInfo | URL, init?: RequestInit) => {
|
|
548
|
+
const authorization = new Headers(init?.headers).get(Constants.Headers.authorization)
|
|
549
|
+
const payload = authorization
|
|
550
|
+
? Credential.deserialize<SessionCredentialPayload>(authorization).payload
|
|
551
|
+
: undefined
|
|
552
|
+
if (payload) posted.push(payload)
|
|
553
|
+
if (!payload) return Promise.resolve(make402Response())
|
|
554
|
+
return Promise.resolve(makeOkResponse())
|
|
555
|
+
})
|
|
556
|
+
const restarted = sessionManager({
|
|
557
|
+
account,
|
|
558
|
+
client,
|
|
559
|
+
fetch: resumeFetch as typeof globalThis.fetch,
|
|
560
|
+
maxDeposit: '10',
|
|
561
|
+
channelStore: durableStore(),
|
|
562
|
+
})
|
|
563
|
+
await restarted.fetch('https://api.example.com/data')
|
|
564
|
+
|
|
565
|
+
// The first request carries no hint header; after the 402 the restarted
|
|
566
|
+
// manager resumes the persisted channel from the entry index with a
|
|
567
|
+
// voucher rather than opening a new one.
|
|
568
|
+
expect(new Headers(resumeFetch.mock.calls[0]?.[1]?.headers).get('Payment-Session')).toBeNull()
|
|
569
|
+
expect(posted[0]).toMatchObject({ action: 'voucher', channelId })
|
|
570
|
+
})
|
|
571
|
+
|
|
572
|
+
test('persists opened channels and deletes closed channels when supported', async () => {
|
|
573
|
+
const { store, set, delete: remove } = makeChannelStore()
|
|
574
|
+
let callCount = 0
|
|
575
|
+
const mockFetch = vi.fn().mockImplementation((_input, init?: RequestInit) => {
|
|
576
|
+
const authorization = new Headers(init?.headers).get('Authorization')
|
|
577
|
+
const payload = authorization
|
|
578
|
+
? Credential.deserialize<SessionCredentialPayload>(authorization).payload
|
|
579
|
+
: undefined
|
|
580
|
+
callCount++
|
|
581
|
+
|
|
582
|
+
if (callCount === 1) return Promise.resolve(make402Response())
|
|
583
|
+
if (payload?.action === 'open') {
|
|
584
|
+
return Promise.resolve(
|
|
585
|
+
new Response('ok', {
|
|
586
|
+
headers: {
|
|
587
|
+
'Payment-Receipt': serializeSessionReceipt(
|
|
588
|
+
createSessionReceipt({
|
|
589
|
+
acceptedCumulative: BigInt(payload.cumulativeAmount),
|
|
590
|
+
challengeId,
|
|
591
|
+
channelId: payload.channelId,
|
|
592
|
+
spent: 0n,
|
|
593
|
+
}),
|
|
594
|
+
),
|
|
595
|
+
},
|
|
596
|
+
}),
|
|
597
|
+
)
|
|
598
|
+
}
|
|
599
|
+
if (payload?.action === 'close') {
|
|
600
|
+
return Promise.resolve(
|
|
601
|
+
new Response('ok', {
|
|
602
|
+
headers: {
|
|
603
|
+
'Payment-Receipt': serializeSessionReceipt(
|
|
604
|
+
createSessionReceipt({
|
|
605
|
+
acceptedCumulative: BigInt(payload.cumulativeAmount),
|
|
606
|
+
challengeId,
|
|
607
|
+
channelId: payload.channelId,
|
|
608
|
+
spent: BigInt(payload.cumulativeAmount),
|
|
609
|
+
txHash: `0x${'aa'.repeat(32)}`,
|
|
610
|
+
}),
|
|
611
|
+
),
|
|
612
|
+
},
|
|
613
|
+
}),
|
|
614
|
+
)
|
|
615
|
+
}
|
|
616
|
+
return Promise.resolve(makeOkResponse())
|
|
617
|
+
})
|
|
618
|
+
const s = sessionManager({
|
|
619
|
+
account,
|
|
620
|
+
client,
|
|
621
|
+
fetch: mockFetch as typeof globalThis.fetch,
|
|
622
|
+
maxDeposit: '10',
|
|
623
|
+
channelStore: store,
|
|
624
|
+
})
|
|
625
|
+
|
|
626
|
+
await s.fetch('https://api.example.com/data')
|
|
627
|
+
expect(set).toHaveBeenCalledWith(
|
|
628
|
+
expect.objectContaining({
|
|
629
|
+
channelId: s.channelId,
|
|
630
|
+
opened: true,
|
|
631
|
+
}),
|
|
632
|
+
)
|
|
633
|
+
|
|
634
|
+
await s.close()
|
|
635
|
+
expect(remove).toHaveBeenCalledOnce()
|
|
636
|
+
})
|
|
637
|
+
|
|
638
|
+
test('rolls back state when opening a channel throws', async () => {
|
|
639
|
+
const failingClient = createClient({
|
|
640
|
+
account,
|
|
641
|
+
chain: { id: 4217 } as never,
|
|
642
|
+
transport: custom({
|
|
643
|
+
async request(args) {
|
|
644
|
+
if (args.method === 'eth_chainId') return '0x1079'
|
|
645
|
+
if (args.method === 'eth_getTransactionCount') return '0x0'
|
|
646
|
+
if (args.method === 'eth_estimateGas') throw new Error('insufficient balance')
|
|
647
|
+
if (args.method === 'eth_maxPriorityFeePerGas') return '0x1'
|
|
648
|
+
if (args.method === 'eth_getBlockByNumber') return { baseFeePerGas: '0x1' }
|
|
649
|
+
throw new Error(`unexpected rpc request: ${args.method}`)
|
|
650
|
+
},
|
|
651
|
+
}),
|
|
652
|
+
})
|
|
653
|
+
const mockFetch = vi.fn().mockResolvedValue(make402Response())
|
|
654
|
+
const s = sessionManager({
|
|
655
|
+
account,
|
|
656
|
+
client: failingClient,
|
|
657
|
+
fetch: mockFetch as typeof globalThis.fetch,
|
|
658
|
+
maxDeposit: '10',
|
|
659
|
+
})
|
|
660
|
+
|
|
661
|
+
await expect(s.fetch('https://api.example.com/data')).rejects.toThrow(/insufficient balance/)
|
|
662
|
+
|
|
663
|
+
expect(s.state).toEqual({ status: 'idle' })
|
|
664
|
+
expect(s.opened).toBe(false)
|
|
665
|
+
expect(s.cumulative).toBe(0n)
|
|
666
|
+
})
|
|
667
|
+
|
|
668
|
+
test('automatically top-ups and retries when an HTTP session exceeds deposit', async () => {
|
|
669
|
+
const postedPayloads: SessionCredentialPayload[] = []
|
|
670
|
+
let callCount = 0
|
|
671
|
+
const mockFetch = vi.fn().mockImplementation((_input, init?: RequestInit) => {
|
|
672
|
+
callCount++
|
|
673
|
+
const authorization = new Headers(init?.headers).get('Authorization')
|
|
674
|
+
const payload = authorization
|
|
675
|
+
? Credential.deserialize<SessionCredentialPayload>(authorization).payload
|
|
676
|
+
: undefined
|
|
677
|
+
if (payload) postedPayloads.push(payload)
|
|
678
|
+
|
|
679
|
+
if (callCount === 1)
|
|
680
|
+
return Promise.resolve(make402Response(makeChallenge({ suggestedDeposit: '1000000' })))
|
|
681
|
+
|
|
682
|
+
if (payload?.action === 'open') {
|
|
683
|
+
return Promise.resolve(
|
|
684
|
+
make402Response(
|
|
685
|
+
makeChallenge({
|
|
686
|
+
methodDetails: {
|
|
687
|
+
escrowContract: tip20ChannelEscrow,
|
|
688
|
+
chainId: 4217,
|
|
689
|
+
sessionSnapshot: {
|
|
690
|
+
acceptedCumulative: '2000000',
|
|
691
|
+
chainId: 4217,
|
|
692
|
+
channelId: payload.channelId,
|
|
693
|
+
deposit: '1000000',
|
|
694
|
+
descriptor: payload.descriptor,
|
|
695
|
+
escrow: tip20ChannelEscrow,
|
|
696
|
+
requiredCumulative: '2000000',
|
|
697
|
+
settled: '0',
|
|
698
|
+
spent: '1000000',
|
|
699
|
+
units: 1,
|
|
700
|
+
},
|
|
701
|
+
},
|
|
702
|
+
}),
|
|
703
|
+
),
|
|
704
|
+
)
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
if (payload?.action === 'topUp') return Promise.resolve(makeOkResponse())
|
|
708
|
+
|
|
709
|
+
if (payload?.action === 'voucher') {
|
|
710
|
+
return Promise.resolve(
|
|
711
|
+
new Response('paid', {
|
|
712
|
+
status: 200,
|
|
713
|
+
headers: {
|
|
714
|
+
'Payment-Receipt': serializeSessionReceipt(
|
|
715
|
+
createSessionReceipt({
|
|
716
|
+
acceptedCumulative: 2_000_000n,
|
|
717
|
+
challengeId,
|
|
718
|
+
channelId: payload.channelId,
|
|
719
|
+
spent: 2_000_000n,
|
|
720
|
+
units: 2,
|
|
721
|
+
}),
|
|
722
|
+
),
|
|
723
|
+
},
|
|
724
|
+
}),
|
|
725
|
+
)
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
return Promise.resolve(makeOkResponse())
|
|
729
|
+
})
|
|
730
|
+
|
|
731
|
+
const s = sessionManager({
|
|
732
|
+
account,
|
|
733
|
+
client,
|
|
734
|
+
fetch: mockFetch as typeof globalThis.fetch,
|
|
735
|
+
maxDeposit: '10',
|
|
736
|
+
})
|
|
737
|
+
|
|
738
|
+
const response = await s.fetch('https://api.example.com/data')
|
|
739
|
+
|
|
740
|
+
expect(response.status).toBe(200)
|
|
741
|
+
expect(await response.text()).toBe('paid')
|
|
742
|
+
expect(postedPayloads.map((payload) => payload.action)).toEqual(['open', 'topUp', 'voucher'])
|
|
743
|
+
expect(s.state).toMatchObject({
|
|
744
|
+
status: 'active',
|
|
745
|
+
acceptedCumulative: '2000000',
|
|
746
|
+
deposit: '2000000',
|
|
747
|
+
spent: '2000000',
|
|
748
|
+
units: 2,
|
|
749
|
+
})
|
|
750
|
+
})
|
|
751
|
+
|
|
752
|
+
test('preemptively top-ups before signing an HTTP voucher that exceeds deposit', async () => {
|
|
753
|
+
const postedPayloads: SessionCredentialPayload[] = []
|
|
754
|
+
let challengeCount = 0
|
|
755
|
+
const receipt = (payload: Extract<SessionCredentialPayload, { channelId: Hex }>) =>
|
|
756
|
+
new Response('paid', {
|
|
757
|
+
status: 200,
|
|
758
|
+
headers: {
|
|
759
|
+
'Payment-Receipt': serializeSessionReceipt(
|
|
760
|
+
createSessionReceipt({
|
|
761
|
+
acceptedCumulative: BigInt(
|
|
762
|
+
'cumulativeAmount' in payload ? payload.cumulativeAmount : '1000000',
|
|
763
|
+
),
|
|
764
|
+
challengeId,
|
|
765
|
+
channelId: payload.channelId,
|
|
766
|
+
spent: BigInt('cumulativeAmount' in payload ? payload.cumulativeAmount : '1000000'),
|
|
767
|
+
units:
|
|
768
|
+
'cumulativeAmount' in payload && payload.cumulativeAmount === '2000000' ? 2 : 1,
|
|
769
|
+
}),
|
|
770
|
+
),
|
|
771
|
+
},
|
|
772
|
+
})
|
|
773
|
+
const mockFetch = vi.fn().mockImplementation((_input, init?: RequestInit) => {
|
|
774
|
+
const authorization = new Headers(init?.headers).get('Authorization')
|
|
775
|
+
const payload = authorization
|
|
776
|
+
? Credential.deserialize<SessionCredentialPayload>(authorization).payload
|
|
777
|
+
: undefined
|
|
778
|
+
if (payload) postedPayloads.push(payload)
|
|
779
|
+
|
|
780
|
+
if (!payload) {
|
|
781
|
+
challengeCount++
|
|
782
|
+
return Promise.resolve(
|
|
783
|
+
make402Response(
|
|
784
|
+
makeChallenge(challengeCount === 1 ? { suggestedDeposit: '1000000' } : {}),
|
|
785
|
+
),
|
|
786
|
+
)
|
|
787
|
+
}
|
|
788
|
+
if (payload.action === 'topUp') return Promise.resolve(makeOkResponse())
|
|
789
|
+
if (payload.action === 'open' || payload.action === 'voucher')
|
|
790
|
+
return Promise.resolve(receipt(payload))
|
|
791
|
+
return Promise.resolve(makeOkResponse())
|
|
792
|
+
})
|
|
793
|
+
|
|
794
|
+
const s = sessionManager({
|
|
795
|
+
account,
|
|
796
|
+
client,
|
|
797
|
+
fetch: mockFetch as typeof globalThis.fetch,
|
|
798
|
+
maxDeposit: '10',
|
|
799
|
+
})
|
|
800
|
+
|
|
801
|
+
await s.fetch('https://api.example.com/data')
|
|
802
|
+
const response = await s.fetch('https://api.example.com/data')
|
|
803
|
+
|
|
804
|
+
expect(response.status).toBe(200)
|
|
805
|
+
expect(postedPayloads.map((payload) => payload.action)).toEqual(['open', 'topUp', 'voucher'])
|
|
806
|
+
expect(s.state).toMatchObject({
|
|
807
|
+
status: 'active',
|
|
808
|
+
acceptedCumulative: '2000000',
|
|
809
|
+
deposit: '2000000',
|
|
810
|
+
spent: '2000000',
|
|
811
|
+
units: 2,
|
|
812
|
+
})
|
|
813
|
+
})
|
|
814
|
+
})
|
|
815
|
+
|
|
816
|
+
describe('.topUp()', () => {
|
|
817
|
+
test('posts a precompile top-up credential for the active channel', async () => {
|
|
818
|
+
const postedPayloads: SessionCredentialPayload[] = []
|
|
819
|
+
let callCount = 0
|
|
820
|
+
const mockFetch = vi.fn().mockImplementation((_input, init?: RequestInit) => {
|
|
821
|
+
callCount++
|
|
822
|
+
const authorization = new Headers(init?.headers).get('Authorization')
|
|
823
|
+
if (authorization) {
|
|
824
|
+
postedPayloads.push(
|
|
825
|
+
Credential.deserialize<SessionCredentialPayload>(authorization).payload,
|
|
826
|
+
)
|
|
827
|
+
}
|
|
828
|
+
if (callCount === 1)
|
|
829
|
+
return Promise.resolve(make402Response(makeChallenge({ suggestedDeposit: '5000000' })))
|
|
830
|
+
return Promise.resolve(makeOkResponse())
|
|
831
|
+
})
|
|
832
|
+
|
|
833
|
+
const s = sessionManager({
|
|
834
|
+
account,
|
|
835
|
+
client,
|
|
836
|
+
fetch: mockFetch as typeof globalThis.fetch,
|
|
837
|
+
maxDeposit: '10',
|
|
838
|
+
})
|
|
839
|
+
|
|
840
|
+
await s.fetch('https://api.example.com/data')
|
|
841
|
+
expect(s.state).toMatchObject({
|
|
842
|
+
status: 'active',
|
|
843
|
+
acceptedCumulative: '1000000',
|
|
844
|
+
deposit: '5000000',
|
|
845
|
+
})
|
|
846
|
+
const receipt = await s.topUp('1')
|
|
847
|
+
|
|
848
|
+
expect(receipt).toBeUndefined()
|
|
849
|
+
expect(s.state).toMatchObject({
|
|
850
|
+
status: 'active',
|
|
851
|
+
acceptedCumulative: '1000000',
|
|
852
|
+
deposit: '6000000',
|
|
853
|
+
})
|
|
854
|
+
expect(postedPayloads[0]?.action).toBe('open')
|
|
855
|
+
expect(postedPayloads[1]?.action).toBe('topUp')
|
|
856
|
+
const openPayload = postedPayloads[0]
|
|
857
|
+
const topUpPayload = postedPayloads[1]
|
|
858
|
+
if (openPayload?.action !== 'open' || topUpPayload?.action !== 'topUp') {
|
|
859
|
+
throw new Error('expected open then top-up payloads')
|
|
860
|
+
}
|
|
861
|
+
expect(topUpPayload.channelId).toBe(openPayload.channelId)
|
|
862
|
+
expect(topUpPayload.descriptor).toEqual(openPayload.descriptor)
|
|
863
|
+
expect(topUpPayload.additionalDeposit).toBe('1000000')
|
|
864
|
+
})
|
|
865
|
+
|
|
866
|
+
test('rejects top-up before a channel is open', async () => {
|
|
867
|
+
const s = sessionManager({
|
|
868
|
+
account,
|
|
869
|
+
client,
|
|
870
|
+
fetch: vi.fn() as typeof globalThis.fetch,
|
|
871
|
+
maxDeposit: '10',
|
|
872
|
+
})
|
|
873
|
+
|
|
874
|
+
await expect(s.topUp('1')).rejects.toThrow('Cannot top up session: no open channel.')
|
|
875
|
+
})
|
|
876
|
+
|
|
877
|
+
test('rolls back optimistic vouchers when the paid retry fails', async () => {
|
|
878
|
+
const postedPayloads: SessionCredentialPayload[] = []
|
|
879
|
+
let callCount = 0
|
|
880
|
+
const mockFetch = vi.fn().mockImplementation((_input, init?: RequestInit) => {
|
|
881
|
+
callCount++
|
|
882
|
+
const authorization = new Headers(init?.headers).get('Authorization')
|
|
883
|
+
if (authorization) {
|
|
884
|
+
postedPayloads.push(
|
|
885
|
+
Credential.deserialize<SessionCredentialPayload>(authorization).payload,
|
|
886
|
+
)
|
|
887
|
+
}
|
|
888
|
+
if (callCount === 1)
|
|
889
|
+
return Promise.resolve(make402Response(makeChallenge({ suggestedDeposit: '3000000' })))
|
|
890
|
+
if (callCount === 3) return Promise.resolve(make402Response())
|
|
891
|
+
if (callCount === 4) return Promise.resolve(make402Response())
|
|
892
|
+
return Promise.resolve(makeOkResponse())
|
|
893
|
+
})
|
|
894
|
+
|
|
895
|
+
const s = sessionManager({
|
|
896
|
+
account,
|
|
897
|
+
client,
|
|
898
|
+
fetch: mockFetch as typeof globalThis.fetch,
|
|
899
|
+
maxDeposit: '3',
|
|
900
|
+
})
|
|
901
|
+
|
|
902
|
+
await s.fetch('https://api.example.com/data')
|
|
903
|
+
expect(s.cumulative).toBe(1000000n)
|
|
904
|
+
|
|
905
|
+
const failed = await s.fetch('https://api.example.com/data')
|
|
906
|
+
expect(failed.status).toBe(402)
|
|
907
|
+
expect(s.cumulative).toBe(1000000n)
|
|
908
|
+
|
|
909
|
+
await s.topUp('1')
|
|
910
|
+
expect(postedPayloads.map((payload) => payload.action)).toEqual(['open', 'voucher', 'topUp'])
|
|
911
|
+
const topUpPayload = postedPayloads[2]
|
|
912
|
+
if (topUpPayload?.action !== 'topUp') throw new Error('expected top-up payload')
|
|
913
|
+
expect(topUpPayload.additionalDeposit).toBe('1000000')
|
|
914
|
+
})
|
|
915
|
+
})
|
|
916
|
+
|
|
917
|
+
describe('.sse() event parsing', () => {
|
|
918
|
+
test('yields only message data from SSE stream', async () => {
|
|
919
|
+
const events = [
|
|
920
|
+
'event: message\ndata: chunk1\n\n',
|
|
921
|
+
'event: message\ndata: chunk2\n\n',
|
|
922
|
+
`event: payment-receipt\ndata: ${JSON.stringify({
|
|
923
|
+
method: 'tempo',
|
|
924
|
+
intent: 'session',
|
|
925
|
+
status: 'success',
|
|
926
|
+
timestamp: '2025-01-01T00:00:00.000Z',
|
|
927
|
+
reference: channelId,
|
|
928
|
+
challengeId,
|
|
929
|
+
channelId,
|
|
930
|
+
acceptedCumulative: '2000000',
|
|
931
|
+
spent: '2000000',
|
|
932
|
+
units: 2,
|
|
933
|
+
} satisfies SessionReceipt)}\n\n`,
|
|
934
|
+
]
|
|
935
|
+
|
|
936
|
+
let callCount = 0
|
|
937
|
+
const mockFetch = vi.fn().mockImplementation(() => {
|
|
938
|
+
callCount++
|
|
939
|
+
if (callCount === 1) return Promise.resolve(makeSseResponse(events))
|
|
940
|
+
return Promise.resolve(makeOkResponse())
|
|
941
|
+
})
|
|
942
|
+
|
|
943
|
+
const s = sessionManager({
|
|
944
|
+
account: '0x0000000000000000000000000000000000000001',
|
|
945
|
+
fetch: mockFetch as typeof globalThis.fetch,
|
|
946
|
+
})
|
|
947
|
+
|
|
948
|
+
// Manually set channel state to skip auto-open flow
|
|
949
|
+
;(s as any).__test_setChannel?.()
|
|
950
|
+
|
|
951
|
+
const receiptCb = vi.fn()
|
|
952
|
+
const iterable = await s.sse('https://api.example.com/stream', {
|
|
953
|
+
onReceipt: receiptCb,
|
|
954
|
+
})
|
|
955
|
+
|
|
956
|
+
const messages: string[] = []
|
|
957
|
+
for await (const msg of iterable) {
|
|
958
|
+
messages.push(msg)
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
expect(messages).toEqual(['chunk1', 'chunk2'])
|
|
962
|
+
expect(receiptCb).toHaveBeenCalledOnce()
|
|
963
|
+
expect(receiptCb.mock.calls[0]![0].units).toBe(2)
|
|
964
|
+
})
|
|
965
|
+
|
|
966
|
+
test('posts precompile SSE top-up vouchers with the channel descriptor', async () => {
|
|
967
|
+
const requestedUrls: string[] = []
|
|
968
|
+
const postedPayloads: SessionCredentialPayload[] = []
|
|
969
|
+
let callCount = 0
|
|
970
|
+
const mockFetch = vi.fn().mockImplementation((input, init?: RequestInit) => {
|
|
971
|
+
callCount++
|
|
972
|
+
requestedUrls.push(input.toString())
|
|
973
|
+
const authorization = new Headers(init?.headers).get('Authorization')
|
|
974
|
+
if (authorization) {
|
|
975
|
+
postedPayloads.push(
|
|
976
|
+
Credential.deserialize<SessionCredentialPayload>(authorization).payload,
|
|
977
|
+
)
|
|
978
|
+
}
|
|
979
|
+
if (callCount === 1) return Promise.resolve(make402Response())
|
|
980
|
+
if (callCount === 2) {
|
|
981
|
+
const openPayload = postedPayloads[0]
|
|
982
|
+
if (openPayload?.action !== 'open') throw new Error('expected open payload')
|
|
983
|
+
const needVoucher: NeedVoucherEvent = {
|
|
984
|
+
channelId: openPayload.channelId,
|
|
985
|
+
requiredCumulative: '2000000',
|
|
986
|
+
acceptedCumulative: '1000000',
|
|
987
|
+
deposit: '10000000',
|
|
988
|
+
}
|
|
989
|
+
return Promise.resolve(
|
|
990
|
+
makeSseResponse([
|
|
991
|
+
'event: message\ndata: chunk1\n\n',
|
|
992
|
+
formatNeedVoucherEvent(needVoucher),
|
|
993
|
+
'event: message\ndata: chunk2\n\n',
|
|
994
|
+
]),
|
|
995
|
+
)
|
|
996
|
+
}
|
|
997
|
+
return Promise.resolve(makeOkResponse())
|
|
998
|
+
})
|
|
999
|
+
|
|
1000
|
+
const s = sessionManager({
|
|
1001
|
+
account,
|
|
1002
|
+
client,
|
|
1003
|
+
fetch: mockFetch as typeof globalThis.fetch,
|
|
1004
|
+
maxDeposit: '10',
|
|
1005
|
+
})
|
|
1006
|
+
|
|
1007
|
+
const iterable = await s.sse('https://api.example.com/stream?prompt=paid')
|
|
1008
|
+
const messages: string[] = []
|
|
1009
|
+
for await (const msg of iterable) messages.push(msg)
|
|
1010
|
+
|
|
1011
|
+
expect(messages).toEqual(['chunk1', 'chunk2'])
|
|
1012
|
+
expect(postedPayloads[0]?.action).toBe('open')
|
|
1013
|
+
expect(postedPayloads[1]?.action).toBe('voucher')
|
|
1014
|
+
const openPayload = postedPayloads[0]
|
|
1015
|
+
const voucherPayload = postedPayloads[1]
|
|
1016
|
+
if (openPayload?.action !== 'open' || voucherPayload?.action !== 'voucher')
|
|
1017
|
+
throw new Error('expected open then voucher payloads')
|
|
1018
|
+
expect(voucherPayload.channelId).toBe(openPayload.channelId)
|
|
1019
|
+
expect(voucherPayload.descriptor).toEqual(openPayload.descriptor)
|
|
1020
|
+
expect(voucherPayload.cumulativeAmount).toBe('2000000')
|
|
1021
|
+
expect(requestedUrls[2]).toBe('https://api.example.com/stream')
|
|
1022
|
+
})
|
|
1023
|
+
|
|
1024
|
+
test('ignores precompile SSE voucher requests for a different channel', async () => {
|
|
1025
|
+
const mismatchedChannelId = `0x${'ff'.repeat(32)}` as Hex
|
|
1026
|
+
const needVoucher: NeedVoucherEvent = {
|
|
1027
|
+
channelId: mismatchedChannelId,
|
|
1028
|
+
requiredCumulative: '2000000',
|
|
1029
|
+
acceptedCumulative: '1000000',
|
|
1030
|
+
deposit: '10000000',
|
|
1031
|
+
}
|
|
1032
|
+
const events = [
|
|
1033
|
+
'event: message\ndata: chunk1\n\n',
|
|
1034
|
+
formatNeedVoucherEvent(needVoucher),
|
|
1035
|
+
'event: message\ndata: chunk2\n\n',
|
|
1036
|
+
]
|
|
1037
|
+
const postedPayloads: SessionCredentialPayload[] = []
|
|
1038
|
+
let callCount = 0
|
|
1039
|
+
const mockFetch = vi.fn().mockImplementation((_input, init?: RequestInit) => {
|
|
1040
|
+
callCount++
|
|
1041
|
+
const authorization = new Headers(init?.headers).get('Authorization')
|
|
1042
|
+
if (authorization) {
|
|
1043
|
+
postedPayloads.push(
|
|
1044
|
+
Credential.deserialize<SessionCredentialPayload>(authorization).payload,
|
|
1045
|
+
)
|
|
1046
|
+
}
|
|
1047
|
+
if (callCount === 1) return Promise.resolve(make402Response())
|
|
1048
|
+
if (callCount === 2) return Promise.resolve(makeSseResponse(events))
|
|
1049
|
+
return Promise.resolve(makeOkResponse())
|
|
1050
|
+
})
|
|
1051
|
+
|
|
1052
|
+
const s = sessionManager({
|
|
1053
|
+
account,
|
|
1054
|
+
client,
|
|
1055
|
+
fetch: mockFetch as typeof globalThis.fetch,
|
|
1056
|
+
maxDeposit: '10',
|
|
1057
|
+
})
|
|
1058
|
+
|
|
1059
|
+
const iterable = await s.sse('https://api.example.com/stream')
|
|
1060
|
+
const messages: string[] = []
|
|
1061
|
+
for await (const msg of iterable) messages.push(msg)
|
|
1062
|
+
|
|
1063
|
+
expect(messages).toEqual(['chunk1', 'chunk2'])
|
|
1064
|
+
expect(postedPayloads.map((payload) => payload.action)).toEqual(['open'])
|
|
1065
|
+
expect(callCount).toBe(2)
|
|
1066
|
+
})
|
|
1067
|
+
|
|
1068
|
+
test('retries for the event stream after an open management response', async () => {
|
|
1069
|
+
const postedPayloads: SessionCredentialPayload[] = []
|
|
1070
|
+
let callCount = 0
|
|
1071
|
+
const mockFetch = vi.fn().mockImplementation((_input, init?: RequestInit) => {
|
|
1072
|
+
callCount++
|
|
1073
|
+
const authorization = new Headers(init?.headers).get('Authorization')
|
|
1074
|
+
const payload = authorization
|
|
1075
|
+
? Credential.deserialize<SessionCredentialPayload>(authorization).payload
|
|
1076
|
+
: undefined
|
|
1077
|
+
if (payload) postedPayloads.push(payload)
|
|
1078
|
+
|
|
1079
|
+
if (!payload) {
|
|
1080
|
+
return Promise.resolve(make402Response(makeChallenge({ suggestedDeposit: '5000000' })))
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
if (payload.action === 'open') {
|
|
1084
|
+
return Promise.resolve(
|
|
1085
|
+
new Response(null, {
|
|
1086
|
+
status: 200,
|
|
1087
|
+
headers: {
|
|
1088
|
+
'Payment-Receipt': serializeSessionReceipt(
|
|
1089
|
+
createSessionReceipt({
|
|
1090
|
+
acceptedCumulative: 1_000_000n,
|
|
1091
|
+
challengeId,
|
|
1092
|
+
channelId: payload.channelId,
|
|
1093
|
+
spent: 1_000_000n,
|
|
1094
|
+
units: 1,
|
|
1095
|
+
}),
|
|
1096
|
+
),
|
|
1097
|
+
},
|
|
1098
|
+
}),
|
|
1099
|
+
)
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
if (payload.action === 'voucher') {
|
|
1103
|
+
return Promise.resolve(
|
|
1104
|
+
makeSseResponse([
|
|
1105
|
+
'event: message\ndata: chunk1\n\n',
|
|
1106
|
+
'event: message\ndata: chunk2\n\n',
|
|
1107
|
+
`event: payment-receipt\ndata: ${JSON.stringify(
|
|
1108
|
+
createSessionReceipt({
|
|
1109
|
+
acceptedCumulative: BigInt(payload.cumulativeAmount),
|
|
1110
|
+
challengeId,
|
|
1111
|
+
channelId: payload.channelId,
|
|
1112
|
+
spent: BigInt(payload.cumulativeAmount),
|
|
1113
|
+
units: 2,
|
|
1114
|
+
}),
|
|
1115
|
+
)}\n\n`,
|
|
1116
|
+
]),
|
|
1117
|
+
)
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
return Promise.resolve(makeOkResponse())
|
|
1121
|
+
})
|
|
1122
|
+
|
|
1123
|
+
const s = sessionManager({
|
|
1124
|
+
account,
|
|
1125
|
+
client,
|
|
1126
|
+
fetch: mockFetch as typeof globalThis.fetch,
|
|
1127
|
+
maxDeposit: '10',
|
|
1128
|
+
})
|
|
1129
|
+
|
|
1130
|
+
const iterable = await s.sse('https://api.example.com/stream')
|
|
1131
|
+
const messages: string[] = []
|
|
1132
|
+
for await (const msg of iterable) messages.push(msg)
|
|
1133
|
+
|
|
1134
|
+
expect(messages).toEqual(['chunk1', 'chunk2'])
|
|
1135
|
+
expect(postedPayloads.map((payload) => payload.action)).toEqual(['open', 'voucher'])
|
|
1136
|
+
expect(callCount).toBe(4)
|
|
1137
|
+
expect(s.state).toMatchObject({
|
|
1138
|
+
status: 'active',
|
|
1139
|
+
acceptedCumulative: '2000000',
|
|
1140
|
+
deposit: '5000000',
|
|
1141
|
+
spent: '2000000',
|
|
1142
|
+
units: 2,
|
|
1143
|
+
})
|
|
1144
|
+
})
|
|
1145
|
+
})
|
|
1146
|
+
|
|
1147
|
+
describe('error handling', () => {
|
|
1148
|
+
test('.sse() silently skips payment-need-voucher when no channel open', async () => {
|
|
1149
|
+
const needVoucher: NeedVoucherEvent = {
|
|
1150
|
+
channelId,
|
|
1151
|
+
requiredCumulative: '2000000',
|
|
1152
|
+
acceptedCumulative: '1000000',
|
|
1153
|
+
deposit: '10000000',
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
const events = [
|
|
1157
|
+
'event: message\ndata: chunk1\n\n',
|
|
1158
|
+
formatNeedVoucherEvent(needVoucher),
|
|
1159
|
+
'event: message\ndata: chunk2\n\n',
|
|
1160
|
+
]
|
|
1161
|
+
|
|
1162
|
+
const mockFetch = vi.fn().mockResolvedValue(makeSseResponse(events))
|
|
1163
|
+
|
|
1164
|
+
const s = sessionManager({
|
|
1165
|
+
account: '0x0000000000000000000000000000000000000001',
|
|
1166
|
+
fetch: mockFetch as typeof globalThis.fetch,
|
|
1167
|
+
})
|
|
1168
|
+
|
|
1169
|
+
const iterable = await s.sse('https://api.example.com/stream')
|
|
1170
|
+
|
|
1171
|
+
const messages: string[] = []
|
|
1172
|
+
for await (const msg of iterable) {
|
|
1173
|
+
messages.push(msg)
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
expect(messages).toEqual(['chunk1', 'chunk2'])
|
|
1177
|
+
expect(mockFetch).toHaveBeenCalledOnce()
|
|
1178
|
+
})
|
|
1179
|
+
})
|
|
1180
|
+
|
|
1181
|
+
describe('.sse() headers normalization', () => {
|
|
1182
|
+
test('preserves Headers instance properties when passed as headers', async () => {
|
|
1183
|
+
const mockFetch = vi.fn().mockResolvedValue(makeSseResponse(['event: message\ndata: ok\n\n']))
|
|
1184
|
+
|
|
1185
|
+
const s = sessionManager({
|
|
1186
|
+
account: '0x0000000000000000000000000000000000000001',
|
|
1187
|
+
fetch: mockFetch as typeof globalThis.fetch,
|
|
1188
|
+
})
|
|
1189
|
+
|
|
1190
|
+
const iterable = await s.sse('https://api.example.com/stream', {
|
|
1191
|
+
headers: new Headers({ 'Content-Type': 'application/json', 'X-Custom': 'value' }),
|
|
1192
|
+
})
|
|
1193
|
+
|
|
1194
|
+
for await (const _ of iterable) {
|
|
1195
|
+
// drain
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
const calledHeaders = new Headers((mockFetch.mock.calls[0]![1] as RequestInit).headers)
|
|
1199
|
+
expect(calledHeaders.get('content-type')).toBe('application/json')
|
|
1200
|
+
expect(calledHeaders.get('x-custom')).toBe('value')
|
|
1201
|
+
expect(calledHeaders.get('accept')).toBe('text/event-stream')
|
|
1202
|
+
})
|
|
1203
|
+
})
|
|
1204
|
+
|
|
1205
|
+
describe('.close()', () => {
|
|
1206
|
+
test('is no-op when not opened', async () => {
|
|
1207
|
+
const mockFetch = vi.fn()
|
|
1208
|
+
|
|
1209
|
+
const s = sessionManager({
|
|
1210
|
+
account: '0x0000000000000000000000000000000000000001',
|
|
1211
|
+
fetch: mockFetch as typeof globalThis.fetch,
|
|
1212
|
+
})
|
|
1213
|
+
|
|
1214
|
+
await s.close()
|
|
1215
|
+
expect(mockFetch).not.toHaveBeenCalled()
|
|
1216
|
+
})
|
|
1217
|
+
|
|
1218
|
+
test('tracks spent from HTTP error receipts and closes at that amount', async () => {
|
|
1219
|
+
const postedPayloads: SessionCredentialPayload[] = []
|
|
1220
|
+
const mockFetch = vi.fn().mockImplementation((_input, init?: RequestInit) => {
|
|
1221
|
+
const authorization = new Headers(init?.headers).get('Authorization')
|
|
1222
|
+
const payload = authorization
|
|
1223
|
+
? Credential.deserialize<SessionCredentialPayload>(authorization).payload
|
|
1224
|
+
: undefined
|
|
1225
|
+
if (payload) postedPayloads.push(payload)
|
|
1226
|
+
|
|
1227
|
+
if (!payload)
|
|
1228
|
+
return Promise.resolve(
|
|
1229
|
+
make402Response(makeChallenge({ amount: '1', suggestedDeposit: '2' })),
|
|
1230
|
+
)
|
|
1231
|
+
if (payload.action === 'open') {
|
|
1232
|
+
return Promise.resolve(
|
|
1233
|
+
new Response('upstream failed', {
|
|
1234
|
+
status: 500,
|
|
1235
|
+
headers: {
|
|
1236
|
+
'Payment-Receipt': serializeSessionReceipt(
|
|
1237
|
+
createSessionReceipt({
|
|
1238
|
+
acceptedCumulative: BigInt(payload.cumulativeAmount),
|
|
1239
|
+
challengeId,
|
|
1240
|
+
channelId: payload.channelId,
|
|
1241
|
+
spent: 1n,
|
|
1242
|
+
units: 1,
|
|
1243
|
+
}),
|
|
1244
|
+
),
|
|
1245
|
+
},
|
|
1246
|
+
}),
|
|
1247
|
+
)
|
|
1248
|
+
}
|
|
1249
|
+
if (payload.action === 'close') {
|
|
1250
|
+
return Promise.resolve(
|
|
1251
|
+
new Response(null, {
|
|
1252
|
+
status: 200,
|
|
1253
|
+
headers: {
|
|
1254
|
+
'Payment-Receipt': serializeSessionReceipt(
|
|
1255
|
+
createSessionReceipt({
|
|
1256
|
+
acceptedCumulative: BigInt(payload.cumulativeAmount),
|
|
1257
|
+
challengeId,
|
|
1258
|
+
channelId: payload.channelId,
|
|
1259
|
+
spent: BigInt(payload.cumulativeAmount),
|
|
1260
|
+
units: 1,
|
|
1261
|
+
}),
|
|
1262
|
+
),
|
|
1263
|
+
},
|
|
1264
|
+
}),
|
|
1265
|
+
)
|
|
1266
|
+
}
|
|
1267
|
+
return Promise.resolve(makeOkResponse())
|
|
1268
|
+
})
|
|
1269
|
+
|
|
1270
|
+
const s = sessionManager({
|
|
1271
|
+
account,
|
|
1272
|
+
client,
|
|
1273
|
+
decimals: 0,
|
|
1274
|
+
fetch: mockFetch as typeof globalThis.fetch,
|
|
1275
|
+
maxDeposit: '2',
|
|
1276
|
+
})
|
|
1277
|
+
|
|
1278
|
+
const response = await s.fetch('https://api.example.com/resource')
|
|
1279
|
+
expect(response.status).toBe(500)
|
|
1280
|
+
expect(response.receipt?.spent).toBe('1')
|
|
1281
|
+
|
|
1282
|
+
const closeReceipt = await s.close()
|
|
1283
|
+
expect(closeReceipt?.status).toBe('success')
|
|
1284
|
+
expect(closeReceipt?.spent).toBe('1')
|
|
1285
|
+
expect(s.state).toMatchObject({
|
|
1286
|
+
status: 'closed',
|
|
1287
|
+
channelId: closeReceipt?.channelId,
|
|
1288
|
+
})
|
|
1289
|
+
const closePayload = postedPayloads.find((payload) => payload.action === 'close')
|
|
1290
|
+
expect(closePayload?.cumulativeAmount).toBe('1')
|
|
1291
|
+
})
|
|
1292
|
+
|
|
1293
|
+
test('rejects receipts that exceed the locally signed voucher', async () => {
|
|
1294
|
+
const mockFetch = vi.fn().mockImplementation((_input, init?: RequestInit) => {
|
|
1295
|
+
const authorization = new Headers(init?.headers).get('Authorization')
|
|
1296
|
+
const payload = authorization
|
|
1297
|
+
? Credential.deserialize<SessionCredentialPayload>(authorization).payload
|
|
1298
|
+
: undefined
|
|
1299
|
+
|
|
1300
|
+
if (!payload)
|
|
1301
|
+
return Promise.resolve(
|
|
1302
|
+
make402Response(makeChallenge({ amount: '1', suggestedDeposit: '3' })),
|
|
1303
|
+
)
|
|
1304
|
+
if (payload.action === 'open') {
|
|
1305
|
+
return Promise.resolve(
|
|
1306
|
+
new Response('ok', {
|
|
1307
|
+
status: 200,
|
|
1308
|
+
headers: {
|
|
1309
|
+
'Payment-Receipt': serializeSessionReceipt(
|
|
1310
|
+
createSessionReceipt({
|
|
1311
|
+
acceptedCumulative: 3n,
|
|
1312
|
+
challengeId,
|
|
1313
|
+
channelId: payload.channelId,
|
|
1314
|
+
spent: 3n,
|
|
1315
|
+
units: 1,
|
|
1316
|
+
}),
|
|
1317
|
+
),
|
|
1318
|
+
},
|
|
1319
|
+
}),
|
|
1320
|
+
)
|
|
1321
|
+
}
|
|
1322
|
+
return Promise.resolve(makeOkResponse())
|
|
1323
|
+
})
|
|
1324
|
+
|
|
1325
|
+
const s = sessionManager({
|
|
1326
|
+
account,
|
|
1327
|
+
client,
|
|
1328
|
+
decimals: 0,
|
|
1329
|
+
fetch: mockFetch as typeof globalThis.fetch,
|
|
1330
|
+
maxDeposit: '3',
|
|
1331
|
+
})
|
|
1332
|
+
|
|
1333
|
+
await expect(s.fetch('https://api.example.com/resource')).rejects.toThrow(
|
|
1334
|
+
'receipt accepted cumulative exceeds local voucher state',
|
|
1335
|
+
)
|
|
1336
|
+
})
|
|
1337
|
+
|
|
1338
|
+
test('surfaces close failure problem details', async () => {
|
|
1339
|
+
const mockFetch = vi.fn().mockImplementation((_input, init?: RequestInit) => {
|
|
1340
|
+
const authorization = new Headers(init?.headers).get('Authorization')
|
|
1341
|
+
const payload = authorization
|
|
1342
|
+
? Credential.deserialize<SessionCredentialPayload>(authorization).payload
|
|
1343
|
+
: undefined
|
|
1344
|
+
|
|
1345
|
+
if (!payload)
|
|
1346
|
+
return Promise.resolve(
|
|
1347
|
+
make402Response(makeChallenge({ amount: '1', suggestedDeposit: '2' })),
|
|
1348
|
+
)
|
|
1349
|
+
if (payload.action === 'open') {
|
|
1350
|
+
return Promise.resolve(
|
|
1351
|
+
new Response('ok', {
|
|
1352
|
+
status: 200,
|
|
1353
|
+
headers: {
|
|
1354
|
+
'Payment-Receipt': serializeSessionReceipt(
|
|
1355
|
+
createSessionReceipt({
|
|
1356
|
+
acceptedCumulative: BigInt(payload.cumulativeAmount),
|
|
1357
|
+
challengeId,
|
|
1358
|
+
channelId: payload.channelId,
|
|
1359
|
+
spent: 1n,
|
|
1360
|
+
units: 1,
|
|
1361
|
+
}),
|
|
1362
|
+
),
|
|
1363
|
+
},
|
|
1364
|
+
}),
|
|
1365
|
+
)
|
|
1366
|
+
}
|
|
1367
|
+
if (payload.action === 'close') {
|
|
1368
|
+
return Promise.resolve(
|
|
1369
|
+
new Response('close failed', {
|
|
1370
|
+
status: 500,
|
|
1371
|
+
headers: {
|
|
1372
|
+
'WWW-Authenticate': 'Payment error="close_failed"',
|
|
1373
|
+
},
|
|
1374
|
+
}),
|
|
1375
|
+
)
|
|
1376
|
+
}
|
|
1377
|
+
return Promise.resolve(makeOkResponse())
|
|
1378
|
+
})
|
|
1379
|
+
|
|
1380
|
+
const s = sessionManager({
|
|
1381
|
+
account,
|
|
1382
|
+
client,
|
|
1383
|
+
decimals: 0,
|
|
1384
|
+
fetch: mockFetch as typeof globalThis.fetch,
|
|
1385
|
+
maxDeposit: '2',
|
|
1386
|
+
})
|
|
1387
|
+
|
|
1388
|
+
const response = await s.fetch('https://api.example.com/resource')
|
|
1389
|
+
expect(response.status).toBe(200)
|
|
1390
|
+
expect(response.receipt?.spent).toBe('1')
|
|
1391
|
+
|
|
1392
|
+
await expect(s.close()).rejects.toThrow(
|
|
1393
|
+
'Close request failed with status 500: close failed [WWW-Authenticate: Payment error="close_failed"]',
|
|
1394
|
+
)
|
|
1395
|
+
})
|
|
1396
|
+
})
|
|
1397
|
+
})
|