mppx 0.5.0 → 0.5.3

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 +19 -0
  2. package/dist/Credential.d.ts +12 -0
  3. package/dist/Credential.d.ts.map +1 -1
  4. package/dist/Credential.js +22 -4
  5. package/dist/Credential.js.map +1 -1
  6. package/dist/Method.d.ts +4 -0
  7. package/dist/Method.d.ts.map +1 -1
  8. package/dist/Method.js +2 -1
  9. package/dist/Method.js.map +1 -1
  10. package/dist/cli/account.d.ts.map +1 -1
  11. package/dist/cli/account.js +12 -2
  12. package/dist/cli/account.js.map +1 -1
  13. package/dist/proxy/Proxy.d.ts.map +1 -1
  14. package/dist/proxy/Proxy.js +52 -8
  15. package/dist/proxy/Proxy.js.map +1 -1
  16. package/dist/proxy/internal/Route.d.ts.map +1 -1
  17. package/dist/proxy/internal/Route.js +7 -3
  18. package/dist/proxy/internal/Route.js.map +1 -1
  19. package/dist/server/Mppx.d.ts.map +1 -1
  20. package/dist/server/Mppx.js +90 -71
  21. package/dist/server/Mppx.js.map +1 -1
  22. package/dist/server/Transport.d.ts +5 -1
  23. package/dist/server/Transport.d.ts.map +1 -1
  24. package/dist/server/Transport.js +52 -7
  25. package/dist/server/Transport.js.map +1 -1
  26. package/dist/server/internal/html/config.d.ts +7 -0
  27. package/dist/server/internal/html/config.d.ts.map +1 -0
  28. package/dist/server/internal/html/config.js +3 -0
  29. package/dist/server/internal/html/config.js.map +1 -0
  30. package/dist/server/internal/html/serviceWorker.gen.d.ts +2 -0
  31. package/dist/server/internal/html/serviceWorker.gen.d.ts.map +1 -0
  32. package/dist/server/internal/html/serviceWorker.gen.js +3 -0
  33. package/dist/server/internal/html/serviceWorker.gen.js.map +1 -0
  34. package/dist/stripe/server/Charge.d.ts +5 -0
  35. package/dist/stripe/server/Charge.d.ts.map +1 -1
  36. package/dist/stripe/server/Charge.js +14 -6
  37. package/dist/stripe/server/Charge.js.map +1 -1
  38. package/dist/stripe/server/internal/html.gen.d.ts +2 -0
  39. package/dist/stripe/server/internal/html.gen.d.ts.map +1 -0
  40. package/dist/stripe/server/internal/html.gen.js +3 -0
  41. package/dist/stripe/server/internal/html.gen.js.map +1 -0
  42. package/dist/tempo/internal/proof.d.ts +6 -0
  43. package/dist/tempo/internal/proof.d.ts.map +1 -1
  44. package/dist/tempo/internal/proof.js +15 -0
  45. package/dist/tempo/internal/proof.js.map +1 -1
  46. package/dist/tempo/server/Charge.d.ts +10 -3
  47. package/dist/tempo/server/Charge.d.ts.map +1 -1
  48. package/dist/tempo/server/Charge.js +38 -10
  49. package/dist/tempo/server/Charge.js.map +1 -1
  50. package/dist/tempo/server/Session.d.ts.map +1 -1
  51. package/dist/tempo/server/Session.js +3 -2
  52. package/dist/tempo/server/Session.js.map +1 -1
  53. package/dist/tempo/server/internal/html.gen.d.ts +2 -0
  54. package/dist/tempo/server/internal/html.gen.d.ts.map +1 -0
  55. package/dist/tempo/server/internal/html.gen.js +3 -0
  56. package/dist/tempo/server/internal/html.gen.js.map +1 -0
  57. package/dist/tempo/server/internal/transport.d.ts +1 -1
  58. package/dist/tempo/server/internal/transport.d.ts.map +1 -1
  59. package/dist/tempo/server/internal/transport.js +45 -58
  60. package/dist/tempo/server/internal/transport.js.map +1 -1
  61. package/package.json +2 -2
  62. package/src/Credential.ts +28 -4
  63. package/src/Method.ts +6 -1
  64. package/src/cli/account.ts +13 -2
  65. package/src/env.d.ts +1 -0
  66. package/src/mcp-sdk/server/Transport.test.ts +6 -0
  67. package/src/middlewares/elysia.test.ts +3 -5
  68. package/src/middlewares/express.test.ts +3 -5
  69. package/src/middlewares/hono.test.ts +8 -5
  70. package/src/middlewares/nextjs.test.ts +3 -5
  71. package/src/proxy/Proxy.test.ts +188 -1
  72. package/src/proxy/Proxy.ts +58 -9
  73. package/src/proxy/internal/Route.test.ts +9 -0
  74. package/src/proxy/internal/Route.ts +5 -2
  75. package/src/server/Mppx.test.ts +171 -18
  76. package/src/server/Mppx.ts +120 -79
  77. package/src/server/Transport.test.ts +16 -2
  78. package/src/server/Transport.ts +61 -7
  79. package/src/server/internal/html/config.ts +8 -0
  80. package/src/server/internal/html/serviceWorker.client.ts +28 -0
  81. package/src/server/internal/html/serviceWorker.gen.ts +2 -0
  82. package/src/server/internal/html/serviceWorker.ts +27 -0
  83. package/src/server/internal/html/tsconfig.worker.client.json +8 -0
  84. package/src/server/internal/html/tsconfig.worker.json +8 -0
  85. package/src/stripe/server/Charge.ts +19 -5
  86. package/src/stripe/server/internal/html/main.ts +106 -0
  87. package/src/stripe/server/internal/html/node_modules/.bin/mppx.src +21 -0
  88. package/src/stripe/server/internal/html/package.json +9 -0
  89. package/src/stripe/server/internal/html/stripe-js-pure.d.ts +7 -0
  90. package/src/stripe/server/internal/html/tsconfig.json +8 -0
  91. package/src/stripe/server/internal/html.gen.ts +2 -0
  92. package/src/tempo/internal/proof.test.ts +47 -0
  93. package/src/tempo/internal/proof.ts +16 -0
  94. package/src/tempo/server/Charge.test.ts +298 -0
  95. package/src/tempo/server/Charge.ts +61 -12
  96. package/src/tempo/server/Session.ts +3 -2
  97. package/src/tempo/server/internal/html/main.ts +71 -0
  98. package/src/tempo/server/internal/html/node_modules/.bin/mppx.src +21 -0
  99. package/src/tempo/server/internal/html/package.json +10 -0
  100. package/src/tempo/server/internal/html/tsconfig.json +8 -0
  101. package/src/tempo/server/internal/html.gen.ts +2 -0
  102. package/src/tempo/server/internal/transport.test.ts +37 -31
  103. package/src/tempo/server/internal/transport.ts +44 -58
  104. package/src/tsconfig.json +1 -1
@@ -10,6 +10,7 @@ import {
10
10
  import { tempo as tempo_chain } from 'viem/chains'
11
11
  import { Abis, Transaction } from 'viem/tempo'
12
12
 
13
+ import { PaymentError, VerificationFailedError } from '../../Errors.js'
13
14
  import * as Expires from '../../Expires.js'
14
15
  import type { LooseOmit, NoExtraKeys } from '../../internal/types.js'
15
16
  import * as Method from '../../Method.js'
@@ -24,6 +25,7 @@ import * as Proof from '../internal/proof.js'
24
25
  import * as Selectors from '../internal/selectors.js'
25
26
  import type * as types from '../internal/types.js'
26
27
  import * as Methods from '../Methods.js'
28
+ import { html as htmlContent } from './internal/html.gen.js'
27
29
 
28
30
  /**
29
31
  * Creates a Tempo charge method intent for usage on the server.
@@ -47,10 +49,12 @@ export function charge<const parameters extends charge.Parameters>(
47
49
  decimals = defaults.decimals,
48
50
  description,
49
51
  externalId,
52
+ html,
50
53
  memo,
51
54
  waitForConfirmation = true,
52
55
  } = parameters
53
56
  const store = (parameters.store ?? Store.memory()) as Store.Store<charge.StoreItemMap>
57
+ const proofStore = parameters.store as Store.Store<charge.StoreItemMap> | undefined
54
58
 
55
59
  const { recipient, feePayer, feePayerUrl } = Account.resolve(parameters)
56
60
 
@@ -73,6 +77,8 @@ export function charge<const parameters extends charge.Parameters>(
73
77
  recipient,
74
78
  } as unknown as Defaults,
75
79
 
80
+ html: html ? { config: {}, content: htmlContent } : undefined,
81
+
76
82
  // TODO: dedupe `{charge,session}.request`
77
83
  async request({ credential, request }) {
78
84
  const chainId = await (async () => {
@@ -109,16 +115,17 @@ export function charge<const parameters extends charge.Parameters>(
109
115
 
110
116
  async verify({ credential, request }) {
111
117
  const { challenge } = credential
112
- const { chainId, feePayer } = request
118
+ const resolvedRequest = Methods.charge.schema.request.parse(request)
119
+ const chainId = resolvedRequest.methodDetails?.chainId ?? request.chainId
120
+ const feePayer = request.feePayer
113
121
 
114
122
  const client = await getClient({ chainId })
115
123
 
116
- const { request: challengeRequest } = challenge
117
- const { amount, methodDetails } = challengeRequest
124
+ const { amount, methodDetails } = resolvedRequest
118
125
  const expires = challenge.expires
119
126
 
120
- const currency = challengeRequest.currency as `0x${string}`
121
- const recipient = challengeRequest.recipient as `0x${string}`
127
+ const currency = resolvedRequest.currency as `0x${string}`
128
+ const recipient = resolvedRequest.recipient as `0x${string}`
122
129
 
123
130
  Expires.assert(expires, challenge.id)
124
131
 
@@ -159,11 +166,15 @@ export function charge<const parameters extends charge.Parameters>(
159
166
  if (!expectedSource)
160
167
  throw new MismatchError('Proof credential must include a source.', {})
161
168
 
162
- const sourceAddress = expectedSource.split(':').pop() as `0x${string}`
163
169
  const resolvedChainId = challenge.request.methodDetails?.chainId ?? chainId!
170
+ const source = Proof.parseProofSource(expectedSource)
171
+
172
+ if (!source || source.chainId !== resolvedChainId) {
173
+ throw new MismatchError('Proof credential source is invalid.', {})
174
+ }
164
175
 
165
176
  const valid = await verifyTypedData(client, {
166
- address: sourceAddress,
177
+ address: source.address,
167
178
  domain: Proof.domain(resolvedChainId),
168
179
  types: Proof.types,
169
180
  primaryType: 'Proof',
@@ -172,6 +183,11 @@ export function charge<const parameters extends charge.Parameters>(
172
183
  })
173
184
  if (!valid) throw new MismatchError('Proof signature does not match source.', {})
174
185
 
186
+ if (proofStore) {
187
+ await assertProofUnused(proofStore, challenge.id)
188
+ await markProofUsed(proofStore, challenge.id)
189
+ }
190
+
175
191
  return {
176
192
  method: 'tempo',
177
193
  status: 'success',
@@ -282,13 +298,20 @@ export declare namespace charge {
282
298
  type Defaults = LooseOmit<Method.RequestDefaults<typeof Methods.charge>, 'feePayer' | 'recipient'>
283
299
 
284
300
  type Parameters = {
301
+ /** Render payment page when Accept header is text/html (e.g. in browsers) */
302
+ html?: boolean | undefined
285
303
  /** Testnet mode. */
286
304
  testnet?: boolean | undefined
287
305
  /**
288
- * Store for transaction hash replay protection.
306
+ * Store for charge replay protection.
289
307
  *
290
- * Use a shared store in multi-instance deployments so consumed hashes are
291
- * visible across all server instances.
308
+ * Non-zero charge flows default to an in-memory store if omitted. For
309
+ * zero-dollar proof auth, replay prevention is enabled only when a store
310
+ * is explicitly provided; otherwise proofs remain reusable until the
311
+ * challenge expires.
312
+ *
313
+ * Use a shared store in multi-instance deployments so consumed hashes and
314
+ * proofs are visible across all server instances.
292
315
  */
293
316
  store?: Store.Store | undefined
294
317
  /**
@@ -504,13 +527,19 @@ function getHashStoreKey(hash: `0x${string}`): `mppx:charge:${string}` {
504
527
  return `mppx:charge:${hash.toLowerCase()}`
505
528
  }
506
529
 
530
+ /** @internal */
531
+ function getProofStoreKey(challengeId: string): `mppx:charge:${string}` {
532
+ return `mppx:charge:proof:${challengeId}`
533
+ }
534
+
507
535
  /** @internal */
508
536
  async function assertHashUnused(
509
537
  store: Store.Store<charge.StoreItemMap>,
510
538
  hash: `0x${string}`,
511
539
  ): Promise<void> {
512
540
  const seen = await store.get(getHashStoreKey(hash))
513
- if (seen !== null) throw new Error('Transaction hash has already been used.')
541
+ if (seen !== null)
542
+ throw new VerificationFailedError({ reason: 'Transaction hash has already been used' })
514
543
  }
515
544
 
516
545
  /** @internal */
@@ -521,6 +550,24 @@ async function markHashUsed(
521
550
  await store.put(getHashStoreKey(hash), Date.now())
522
551
  }
523
552
 
553
+ /** @internal */
554
+ async function assertProofUnused(
555
+ store: Store.Store<charge.StoreItemMap>,
556
+ challengeId: string,
557
+ ): Promise<void> {
558
+ const seen = await store.get(getProofStoreKey(challengeId))
559
+ if (seen !== null)
560
+ throw new VerificationFailedError({ reason: 'Proof credential has already been used' })
561
+ }
562
+
563
+ /** @internal */
564
+ async function markProofUsed(
565
+ store: Store.Store<charge.StoreItemMap>,
566
+ challengeId: string,
567
+ ): Promise<void> {
568
+ await store.put(getProofStoreKey(challengeId), Date.now())
569
+ }
570
+
524
571
  /** @internal */
525
572
  function toReceipt(receipt: TransactionReceipt) {
526
573
  const { status, transactionHash } = receipt
@@ -536,8 +583,10 @@ function toReceipt(receipt: TransactionReceipt) {
536
583
  }
537
584
 
538
585
  /** @internal */
539
- class MismatchError extends Error {
586
+ class MismatchError extends PaymentError {
540
587
  override readonly name = 'MismatchError'
588
+ readonly title = 'Verification Failed'
589
+ readonly type = 'https://paymentauth.org/problems/verification-failed'
541
590
 
542
591
  constructor(reason: string, details: Record<string, string>) {
543
592
  super([reason, ...Object.entries(details).map(([k, v]) => ` - ${k}: ${v}`)].join('\n'))
@@ -179,10 +179,11 @@ export function session<const parameters extends session.Parameters>(
179
179
  }
180
180
  },
181
181
 
182
- async verify({ credential }) {
182
+ async verify({ credential, request }) {
183
183
  const { challenge, payload } = credential as Credential.Credential<SessionCredentialPayload>
184
184
 
185
- const methodDetails = challenge.request.methodDetails as SessionMethodDetails
185
+ const resolvedRequest = Methods.session.schema.request.parse(request)
186
+ const methodDetails = resolvedRequest.methodDetails as SessionMethodDetails
186
187
  const client = await getClient({ chainId: methodDetails.chainId })
187
188
 
188
189
  const resolvedFeePayer = methodDetails.feePayer === true ? feePayer : undefined
@@ -0,0 +1,71 @@
1
+ import { local, Provider } from 'accounts'
2
+ import { Json } from 'ox'
3
+ import { createClient, custom, http } from 'viem'
4
+ import { tempoModerato, tempoLocalnet } from 'viem/chains'
5
+
6
+ import type * as Challenge from '../../../../Challenge.js'
7
+ import { tempo } from '../../../../client/index.js'
8
+ import * as Html from '../../../../server/internal/html/config.js'
9
+ import { submitCredential } from '../../../../server/internal/html/serviceWorker.client.js'
10
+ import type * as Methods from '../../../Methods.js'
11
+
12
+ const data = Json.parse(document.getElementById(Html.dataId)!.textContent) as {
13
+ challenge: Challenge.FromMethods<[typeof Methods.charge]>
14
+ }
15
+
16
+ const root = document.getElementById('root')!
17
+
18
+ const h2 = document.createElement('h2')
19
+ h2.textContent = 'tempo'
20
+ root.appendChild(h2)
21
+
22
+ const provider = Provider.create({
23
+ // Dead code eliminated from production bundle (including top-level imports)
24
+ ...(import.meta.env.MODE === 'test'
25
+ ? {
26
+ adapter: local({
27
+ async loadAccounts() {
28
+ const { generatePrivateKey } = await import('viem/accounts')
29
+ const { Account, Actions } = await import('viem/tempo')
30
+ const privateKey = generatePrivateKey()
31
+ const account = Account.fromSecp256k1(privateKey)
32
+ const client = createClient({
33
+ chain: [tempoModerato, tempoLocalnet].find(
34
+ (x) => x.id === data.challenge.request.methodDetails?.chainId,
35
+ ),
36
+ transport: http(),
37
+ })
38
+ await Actions.faucet.fundSync(client, { account })
39
+ return {
40
+ accounts: [account],
41
+ }
42
+ },
43
+ }),
44
+ }
45
+ : {}),
46
+ testnet:
47
+ data.challenge.request.methodDetails?.chainId === tempoModerato.id ||
48
+ data.challenge.request.methodDetails?.chainId === tempoLocalnet.id,
49
+ })
50
+
51
+ const button = document.createElement('button')
52
+ button.textContent = 'Continue with Tempo'
53
+ button.onclick = async () => {
54
+ try {
55
+ button.disabled = true
56
+
57
+ const chain = [...(provider?.chains ?? []), tempoModerato, tempoLocalnet].find(
58
+ (x) => x.id === data.challenge.request.methodDetails?.chainId,
59
+ )
60
+ const client = createClient({ chain, transport: custom(provider) })
61
+ const result = await provider.request({ method: 'wallet_connect' })
62
+ const account = result.accounts[0]?.address
63
+ const method = tempo({ account, getClient: () => client })[0]
64
+
65
+ const credential = await method.createCredential({ challenge: data.challenge, context: {} })
66
+ await submitCredential(credential)
67
+ } finally {
68
+ button.disabled = false
69
+ }
70
+ }
71
+ root.appendChild(button)
@@ -0,0 +1,21 @@
1
+ #!/bin/sh
2
+ basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
3
+
4
+ case `uname` in
5
+ *CYGWIN*|*MINGW*|*MSYS*)
6
+ if command -v cygpath > /dev/null 2>&1; then
7
+ basedir=`cygpath -w "$basedir"`
8
+ fi
9
+ ;;
10
+ esac
11
+
12
+ if [ -z "$NODE_PATH" ]; then
13
+ export NODE_PATH="/home/runner/work/mppx/mppx/src/node_modules:/home/runner/work/mppx/mppx/node_modules:/home/runner/work/mppx/node_modules:/home/runner/work/node_modules:/home/runner/node_modules:/home/node_modules:/node_modules:/home/runner/work/mppx/mppx/node_modules/.pnpm/node_modules"
14
+ else
15
+ export NODE_PATH="/home/runner/work/mppx/mppx/src/node_modules:/home/runner/work/mppx/mppx/node_modules:/home/runner/work/mppx/node_modules:/home/runner/work/node_modules:/home/runner/node_modules:/home/node_modules:/node_modules:/home/runner/work/mppx/mppx/node_modules/.pnpm/node_modules:$NODE_PATH"
16
+ fi
17
+ if [ -x "$basedir/node" ]; then
18
+ exec "$basedir/node" "$basedir/../mppx/src/bin.ts" "$@"
19
+ else
20
+ exec node "$basedir/../mppx/src/bin.ts" "$@"
21
+ fi
@@ -0,0 +1,10 @@
1
+ {
2
+ "name": "@mppx/tempo-html",
3
+ "private": true,
4
+ "type": "module",
5
+ "dependencies": {
6
+ "accounts": "https://pkg.pr.new/tempoxyz/accounts@c339a21",
7
+ "mppx": "workspace:*",
8
+ "viem": "2.47.5"
9
+ }
10
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "../../../../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "lib": ["es2022", "dom"],
5
+ "types": []
6
+ },
7
+ "include": ["./**/*.ts", "../../../../env.d.ts"]
8
+ }