mppx 0.5.10 → 0.5.12

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.
Files changed (104) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/cli/cli.d.ts.map +1 -1
  3. package/dist/cli/cli.js +41 -16
  4. package/dist/cli/cli.js.map +1 -1
  5. package/dist/cli/config.d.ts +6 -4
  6. package/dist/cli/config.d.ts.map +1 -1
  7. package/dist/cli/config.js.map +1 -1
  8. package/dist/cli/internal.d.ts +8 -0
  9. package/dist/cli/internal.d.ts.map +1 -1
  10. package/dist/cli/internal.js +33 -3
  11. package/dist/cli/internal.js.map +1 -1
  12. package/dist/cli/plugins/plugin.d.ts +2 -0
  13. package/dist/cli/plugins/plugin.d.ts.map +1 -1
  14. package/dist/cli/plugins/stripe.d.ts.map +1 -1
  15. package/dist/cli/plugins/stripe.js +3 -0
  16. package/dist/cli/plugins/stripe.js.map +1 -1
  17. package/dist/cli/plugins/tempo.d.ts.map +1 -1
  18. package/dist/cli/plugins/tempo.js +3 -0
  19. package/dist/cli/plugins/tempo.js.map +1 -1
  20. package/dist/client/Mppx.d.ts +10 -1
  21. package/dist/client/Mppx.d.ts.map +1 -1
  22. package/dist/client/Mppx.js +17 -5
  23. package/dist/client/Mppx.js.map +1 -1
  24. package/dist/client/Transport.d.ts +2 -0
  25. package/dist/client/Transport.d.ts.map +1 -1
  26. package/dist/client/Transport.js +11 -0
  27. package/dist/client/Transport.js.map +1 -1
  28. package/dist/client/internal/Fetch.d.ts +3 -0
  29. package/dist/client/internal/Fetch.d.ts.map +1 -1
  30. package/dist/client/internal/Fetch.js +65 -19
  31. package/dist/client/internal/Fetch.js.map +1 -1
  32. package/dist/internal/AcceptPayment.d.ts +72 -0
  33. package/dist/internal/AcceptPayment.d.ts.map +1 -0
  34. package/dist/internal/AcceptPayment.js +185 -0
  35. package/dist/internal/AcceptPayment.js.map +1 -0
  36. package/dist/mcp-sdk/client/McpClient.d.ts.map +1 -1
  37. package/dist/mcp-sdk/client/McpClient.js +8 -4
  38. package/dist/mcp-sdk/client/McpClient.js.map +1 -1
  39. package/dist/server/Mppx.d.ts +1 -1
  40. package/dist/server/Mppx.d.ts.map +1 -1
  41. package/dist/server/Mppx.js +33 -24
  42. package/dist/server/Mppx.js.map +1 -1
  43. package/dist/server/Request.js +1 -1
  44. package/dist/server/Request.js.map +1 -1
  45. package/dist/stripe/internal/constants.d.ts +8 -0
  46. package/dist/stripe/internal/constants.d.ts.map +1 -0
  47. package/dist/stripe/internal/constants.js +8 -0
  48. package/dist/stripe/internal/constants.js.map +1 -0
  49. package/dist/stripe/server/Charge.d.ts.map +1 -1
  50. package/dist/stripe/server/Charge.js +23 -5
  51. package/dist/stripe/server/Charge.js.map +1 -1
  52. package/dist/tempo/Proof.d.ts +12 -0
  53. package/dist/tempo/Proof.d.ts.map +1 -0
  54. package/dist/tempo/Proof.js +10 -0
  55. package/dist/tempo/Proof.js.map +1 -0
  56. package/dist/tempo/index.d.ts +1 -0
  57. package/dist/tempo/index.d.ts.map +1 -1
  58. package/dist/tempo/index.js +1 -0
  59. package/dist/tempo/index.js.map +1 -1
  60. package/dist/tempo/internal/fee-payer.d.ts.map +1 -1
  61. package/dist/tempo/internal/fee-payer.js +5 -1
  62. package/dist/tempo/internal/fee-payer.js.map +1 -1
  63. package/dist/tempo/server/Charge.d.ts.map +1 -1
  64. package/dist/tempo/server/Charge.js +62 -3
  65. package/dist/tempo/server/Charge.js.map +1 -1
  66. package/dist/tempo/server/internal/html.gen.d.ts +1 -1
  67. package/dist/tempo/server/internal/html.gen.d.ts.map +1 -1
  68. package/dist/tempo/server/internal/html.gen.js +1 -1
  69. package/dist/tempo/server/internal/html.gen.js.map +1 -1
  70. package/package.json +3 -3
  71. package/src/cli/cli.test.ts +278 -0
  72. package/src/cli/cli.ts +47 -16
  73. package/src/cli/config.ts +10 -4
  74. package/src/cli/internal.ts +59 -3
  75. package/src/cli/plugins/plugin.ts +3 -0
  76. package/src/cli/plugins/stripe.ts +3 -0
  77. package/src/cli/plugins/tempo.ts +3 -0
  78. package/src/client/Mppx.test-d.ts +33 -0
  79. package/src/client/Mppx.test.ts +130 -1
  80. package/src/client/Mppx.ts +35 -5
  81. package/src/client/Transport.test.ts +88 -55
  82. package/src/client/Transport.ts +13 -0
  83. package/src/client/internal/Fetch.browser.test.ts +16 -13
  84. package/src/client/internal/Fetch.test.ts +307 -10
  85. package/src/client/internal/Fetch.ts +85 -19
  86. package/src/internal/AcceptPayment.test.ts +211 -0
  87. package/src/internal/AcceptPayment.ts +304 -0
  88. package/src/mcp-sdk/client/McpClient.ts +11 -5
  89. package/src/server/Mppx.test.ts +141 -44
  90. package/src/server/Mppx.ts +43 -23
  91. package/src/server/NodeListener.test.ts +78 -0
  92. package/src/server/Request.ts +1 -1
  93. package/src/stripe/internal/constants.ts +7 -0
  94. package/src/stripe/server/Charge.ts +22 -4
  95. package/src/tempo/Proof.test-d.ts +13 -0
  96. package/src/tempo/Proof.test.ts +31 -0
  97. package/src/tempo/Proof.ts +13 -0
  98. package/src/tempo/client/SessionManager.test.ts +4 -7
  99. package/src/tempo/index.ts +1 -0
  100. package/src/tempo/internal/fee-payer.test.ts +1 -1
  101. package/src/tempo/internal/fee-payer.ts +5 -1
  102. package/src/tempo/server/Charge.test.ts +123 -0
  103. package/src/tempo/server/Charge.ts +74 -1
  104. package/src/tempo/server/internal/html.gen.ts +1 -1
@@ -25,6 +25,11 @@ import { signVoucher } from '../session/Voucher.js'
25
25
  const realm = 'api.example.com'
26
26
  const secretKey = 'test-secret-key'
27
27
 
28
+ type ProofAccessKeyContext = {
29
+ accessKey: ReturnType<typeof Account.fromSecp256k1>
30
+ rootAccount: (typeof accounts)[number]
31
+ }
32
+
28
33
  const server = Mppx_server.create({
29
34
  methods: [
30
35
  tempo_server.charge({
@@ -2114,6 +2119,124 @@ describe('tempo', () => {
2114
2119
  httpServer.close()
2115
2120
  })
2116
2121
 
2122
+ for (const testCase of [
2123
+ {
2124
+ name: 'accepts proof signed by an authorized access key for the root source',
2125
+ expectedStatus: 200,
2126
+ async prepare({ accessKey, rootAccount }: ProofAccessKeyContext) {
2127
+ await Actions.accessKey.authorizeSync(client, {
2128
+ account: rootAccount,
2129
+ accessKey,
2130
+ feeToken: asset,
2131
+ })
2132
+ },
2133
+ },
2134
+ {
2135
+ name: 'rejects proof signed by an unauthorized access key for the root source',
2136
+ expectedDetail: 'Proof signature does not match source.',
2137
+ expectedStatus: 402,
2138
+ },
2139
+ {
2140
+ name: 'rejects proof signed by a revoked access key for the root source',
2141
+ expectedDetail: 'Proof signature does not match source.',
2142
+ expectedStatus: 402,
2143
+ async prepare({ accessKey, rootAccount }: ProofAccessKeyContext) {
2144
+ await Actions.accessKey.authorizeSync(client, {
2145
+ account: rootAccount,
2146
+ accessKey,
2147
+ feeToken: asset,
2148
+ })
2149
+ await fundAccount({ address: rootAccount.address, token: asset })
2150
+ await Actions.accessKey.revokeSync(client, {
2151
+ account: rootAccount,
2152
+ accessKey,
2153
+ feeToken: asset,
2154
+ })
2155
+ },
2156
+ },
2157
+ {
2158
+ name: 'rejects proof signed by an expired access key for the root source',
2159
+ expectedDetail: 'Proof signature does not match source.',
2160
+ expectedStatus: 402,
2161
+ async prepare({ accessKey, rootAccount }: ProofAccessKeyContext) {
2162
+ await Actions.accessKey.authorizeSync(client, {
2163
+ account: rootAccount,
2164
+ accessKey,
2165
+ expiry: Math.floor(Date.now() / 1000) + 10,
2166
+ feeToken: asset,
2167
+ })
2168
+
2169
+ const metadata = await Actions.accessKey.getMetadata(client, {
2170
+ account: rootAccount.address,
2171
+ accessKey,
2172
+ })
2173
+ const originalNow = Date.now
2174
+ Date.now = () => (Number(metadata.expiry) + 5) * 1000
2175
+
2176
+ return () => {
2177
+ Date.now = originalNow
2178
+ }
2179
+ },
2180
+ },
2181
+ ] as const) {
2182
+ test(`behavior: ${testCase.name}`, async () => {
2183
+ const rootAccount = accounts[1]
2184
+ const accessKey = Account.fromSecp256k1(Secp256k1.randomPrivateKey(), {
2185
+ access: rootAccount,
2186
+ })
2187
+
2188
+ let cleanup: (() => void) | undefined
2189
+ let httpServer: Awaited<ReturnType<typeof Http.createServer>> | undefined
2190
+
2191
+ try {
2192
+ const maybeCleanup = await testCase.prepare?.({ accessKey, rootAccount })
2193
+ cleanup = typeof maybeCleanup === 'function' ? maybeCleanup : undefined
2194
+
2195
+ httpServer = await Http.createServer(async (req, res) => {
2196
+ const result = await Mppx_server.toNodeListener(
2197
+ server.charge({ amount: '0', decimals: 6 }),
2198
+ )(req, res)
2199
+ if (result.status === 402) return
2200
+ res.end('OK')
2201
+ })
2202
+
2203
+ const response1 = await fetch(httpServer.url)
2204
+ expect(response1.status).toBe(402)
2205
+
2206
+ const challenge = Challenge.fromResponse(response1, {
2207
+ methods: [tempo_client.charge()],
2208
+ })
2209
+
2210
+ const signature = await signTypedData(client, {
2211
+ account: accessKey,
2212
+ domain: Proof.domain(chain.id),
2213
+ types: Proof.types,
2214
+ primaryType: 'Proof',
2215
+ message: Proof.message(challenge.id),
2216
+ })
2217
+
2218
+ const credential = Credential.from({
2219
+ challenge,
2220
+ payload: { signature, type: 'proof' as const },
2221
+ source: `did:pkh:eip155:${chain.id}:${rootAccount.address}`,
2222
+ })
2223
+
2224
+ const response2 = await fetch(httpServer.url, {
2225
+ headers: { Authorization: Credential.serialize(credential) },
2226
+ })
2227
+ expect(response2.status).toBe(testCase.expectedStatus)
2228
+
2229
+ if (testCase.expectedDetail) {
2230
+ const body = (await response2.json()) as { detail: string }
2231
+ expect(body.detail).toContain(testCase.expectedDetail)
2232
+ }
2233
+ } finally {
2234
+ cleanup?.()
2235
+ httpServer?.close()
2236
+ }
2237
+ })
2238
+ }
2239
+
2117
2240
  test('behavior: rejects replayed proof credential when store is configured', async () => {
2118
2241
  const replayStore = Store.memory()
2119
2242
  const server_ = Mppx_server.create({
@@ -1,6 +1,8 @@
1
+ import * as SignatureEnvelope from 'ox/tempo/SignatureEnvelope'
1
2
  import {
2
3
  decodeFunctionData,
3
4
  formatUnits,
5
+ hashTypedData,
4
6
  keccak256,
5
7
  parseEventLogs,
6
8
  type TransactionReceipt,
@@ -227,7 +229,21 @@ export function charge<const parameters extends charge.Parameters>(
227
229
  message: Proof.message(challenge.id),
228
230
  signature: payload.signature as `0x${string}`,
229
231
  })
230
- if (!valid) throw new MismatchError('Proof signature does not match source.', {})
232
+ if (!valid) {
233
+ const proofSigner = recoverAuthorizedProofSigner({
234
+ chainId: resolvedChainId,
235
+ challengeId: challenge.id,
236
+ signature: payload.signature as `0x${string}`,
237
+ sourceAddress: source.address,
238
+ })
239
+ const authorized = proofSigner
240
+ ? await isActiveAccessKey(client, {
241
+ accessKey: proofSigner,
242
+ account: source.address,
243
+ })
244
+ : false
245
+ if (!authorized) throw new MismatchError('Proof signature does not match source.', {})
246
+ }
231
247
 
232
248
  if (proofStore && !(await markProofUsed(proofStore, challenge.id))) {
233
249
  throw new VerificationFailedError({ reason: 'Proof credential has already been used' })
@@ -651,6 +667,63 @@ async function markProofUsed(
651
667
  })
652
668
  }
653
669
 
670
+ function recoverAuthorizedProofSigner(parameters: {
671
+ chainId: number
672
+ challengeId: string
673
+ signature: `0x${string}`
674
+ sourceAddress: `0x${string}`
675
+ }): `0x${string}` | null {
676
+ const { chainId, challengeId, signature, sourceAddress } = parameters
677
+
678
+ try {
679
+ const envelope = SignatureEnvelope.from(signature)
680
+ const proofHash = hashTypedData({
681
+ domain: Proof.domain(chainId),
682
+ types: Proof.types,
683
+ primaryType: 'Proof',
684
+ message: Proof.message(challengeId),
685
+ })
686
+
687
+ if (envelope.type === 'keychain') {
688
+ if (!TempoAddress.isEqual(envelope.userAddress, sourceAddress)) return null
689
+
690
+ const keychainPayload =
691
+ envelope.version === 'v2'
692
+ ? keccak256(`0x04${proofHash.slice(2)}${sourceAddress.slice(2)}` as `0x${string}`)
693
+ : proofHash
694
+
695
+ const signer = SignatureEnvelope.extractAddress({
696
+ payload: keychainPayload,
697
+ signature: envelope.inner,
698
+ })
699
+ const valid = SignatureEnvelope.verify(envelope.inner, {
700
+ address: signer,
701
+ payload: keychainPayload,
702
+ })
703
+ if (!valid) return null
704
+
705
+ return signer
706
+ }
707
+
708
+ return SignatureEnvelope.extractAddress({ payload: proofHash, signature: envelope })
709
+ } catch {
710
+ return null
711
+ }
712
+ }
713
+
714
+ async function isActiveAccessKey(
715
+ client: Awaited<ReturnType<ReturnType<typeof Client.getResolver>>>,
716
+ parameters: { account: `0x${string}`; accessKey: `0x${string}` },
717
+ ): Promise<boolean> {
718
+ try {
719
+ const metadata = await Actions.accessKey.getMetadata(client, parameters)
720
+ const nowSeconds = BigInt(Math.floor(Date.now() / 1000))
721
+ return !metadata.isRevoked && metadata.expiry > nowSeconds
722
+ } catch {
723
+ return false
724
+ }
725
+ }
726
+
654
727
  /** @internal */
655
728
  function toReceipt(receipt: TransactionReceipt) {
656
729
  const { status, transactionHash } = receipt