mppx 0.4.12 → 0.5.1

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 (56) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/Expires.d.ts +7 -0
  3. package/dist/Expires.d.ts.map +1 -1
  4. package/dist/Expires.js +21 -0
  5. package/dist/Expires.js.map +1 -1
  6. package/dist/cli/account.d.ts.map +1 -1
  7. package/dist/cli/account.js +12 -2
  8. package/dist/cli/account.js.map +1 -1
  9. package/dist/server/Mppx.js +6 -5
  10. package/dist/server/Mppx.js.map +1 -1
  11. package/dist/stripe/server/Charge.d.ts.map +1 -1
  12. package/dist/stripe/server/Charge.js +3 -3
  13. package/dist/stripe/server/Charge.js.map +1 -1
  14. package/dist/tempo/Methods.d.ts +3 -0
  15. package/dist/tempo/Methods.d.ts.map +1 -1
  16. package/dist/tempo/Methods.js +1 -0
  17. package/dist/tempo/Methods.js.map +1 -1
  18. package/dist/tempo/client/Charge.d.ts +3 -0
  19. package/dist/tempo/client/Charge.d.ts.map +1 -1
  20. package/dist/tempo/client/Charge.js +18 -2
  21. package/dist/tempo/client/Charge.js.map +1 -1
  22. package/dist/tempo/client/Methods.d.ts +3 -0
  23. package/dist/tempo/client/Methods.d.ts.map +1 -1
  24. package/dist/tempo/internal/proof.d.ts +29 -0
  25. package/dist/tempo/internal/proof.d.ts.map +1 -0
  26. package/dist/tempo/internal/proof.js +32 -0
  27. package/dist/tempo/internal/proof.js.map +1 -0
  28. package/dist/tempo/server/Charge.d.ts +11 -3
  29. package/dist/tempo/server/Charge.d.ts.map +1 -1
  30. package/dist/tempo/server/Charge.js +54 -4
  31. package/dist/tempo/server/Charge.js.map +1 -1
  32. package/dist/tempo/server/Methods.d.ts +3 -0
  33. package/dist/tempo/server/Methods.d.ts.map +1 -1
  34. package/package.json +1 -1
  35. package/src/Expires.ts +25 -0
  36. package/src/cli/account.ts +13 -2
  37. package/src/cli/cli.test.ts +230 -1
  38. package/src/middlewares/elysia.test.ts +130 -9
  39. package/src/middlewares/express.test.ts +123 -59
  40. package/src/middlewares/hono.test.ts +81 -39
  41. package/src/middlewares/nextjs.test.ts +162 -41
  42. package/src/server/Mppx.test.ts +86 -0
  43. package/src/server/Mppx.ts +5 -5
  44. package/src/stripe/server/Charge.ts +3 -7
  45. package/src/tempo/Methods.test.ts +26 -0
  46. package/src/tempo/Methods.ts +1 -0
  47. package/src/tempo/client/Charge.ts +26 -3
  48. package/src/tempo/internal/charge.test.ts +66 -0
  49. package/src/tempo/internal/proof.test.ts +83 -0
  50. package/src/tempo/internal/proof.ts +35 -0
  51. package/src/tempo/server/Charge.test.ts +660 -1
  52. package/src/tempo/server/Charge.ts +80 -5
  53. package/src/tempo/server/Session.test.ts +1123 -53
  54. package/src/tempo/server/internal/transport.test.ts +32 -0
  55. package/src/tempo/session/Chain.test.ts +35 -0
  56. package/src/tempo/session/Sse.test.ts +31 -0
@@ -4,12 +4,13 @@ import {
4
4
  sendRawTransaction,
5
5
  sendRawTransactionSync,
6
6
  signTransaction,
7
+ verifyTypedData,
7
8
  call as viem_call,
8
9
  } from 'viem/actions'
9
10
  import { tempo as tempo_chain } from 'viem/chains'
10
11
  import { Abis, Transaction } from 'viem/tempo'
11
12
 
12
- import { PaymentExpiredError } from '../../Errors.js'
13
+ import * as Expires from '../../Expires.js'
13
14
  import type { LooseOmit, NoExtraKeys } from '../../internal/types.js'
14
15
  import * as Method from '../../Method.js'
15
16
  import * as Store from '../../Store.js'
@@ -19,6 +20,7 @@ import * as TempoAddress from '../internal/address.js'
19
20
  import * as Charge_internal from '../internal/charge.js'
20
21
  import * as defaults from '../internal/defaults.js'
21
22
  import * as FeePayer from '../internal/fee-payer.js'
23
+ import * as Proof from '../internal/proof.js'
22
24
  import * as Selectors from '../internal/selectors.js'
23
25
  import type * as types from '../internal/types.js'
24
26
  import * as Methods from '../Methods.js'
@@ -49,6 +51,7 @@ export function charge<const parameters extends charge.Parameters>(
49
51
  waitForConfirmation = true,
50
52
  } = parameters
51
53
  const store = (parameters.store ?? Store.memory()) as Store.Store<charge.StoreItemMap>
54
+ const proofStore = parameters.store as Store.Store<charge.StoreItemMap> | undefined
52
55
 
53
56
  const { recipient, feePayer, feePayerUrl } = Account.resolve(parameters)
54
57
 
@@ -118,11 +121,15 @@ export function charge<const parameters extends charge.Parameters>(
118
121
  const currency = challengeRequest.currency as `0x${string}`
119
122
  const recipient = challengeRequest.recipient as `0x${string}`
120
123
 
121
- if (expires && new Date(expires) < new Date()) throw new PaymentExpiredError({ expires })
124
+ Expires.assert(expires, challenge.id)
122
125
 
123
126
  const memo = methodDetails?.memo as `0x${string}` | undefined
124
127
 
125
128
  const payload = credential.payload
129
+ const isZeroAmount = BigInt(amount) === 0n
130
+
131
+ if (isZeroAmount && payload.type !== 'proof')
132
+ throw new MismatchError('Zero-amount challenges require a proof credential.', {})
126
133
 
127
134
  switch (payload.type) {
128
135
  case 'hash': {
@@ -142,6 +149,47 @@ export function charge<const parameters extends charge.Parameters>(
142
149
  return toReceipt(receipt)
143
150
  }
144
151
 
152
+ case 'proof': {
153
+ if (!isZeroAmount)
154
+ throw new MismatchError(
155
+ 'Proof credentials are only valid for zero-amount challenges.',
156
+ {},
157
+ )
158
+
159
+ const expectedSource = credential.source
160
+ if (!expectedSource)
161
+ throw new MismatchError('Proof credential must include a source.', {})
162
+
163
+ const resolvedChainId = challenge.request.methodDetails?.chainId ?? chainId!
164
+ const source = Proof.parseProofSource(expectedSource)
165
+
166
+ if (!source || source.chainId !== resolvedChainId) {
167
+ throw new MismatchError('Proof credential source is invalid.', {})
168
+ }
169
+
170
+ const valid = await verifyTypedData(client, {
171
+ address: source.address,
172
+ domain: Proof.domain(resolvedChainId),
173
+ types: Proof.types,
174
+ primaryType: 'Proof',
175
+ message: Proof.message(challenge.id),
176
+ signature: payload.signature as `0x${string}`,
177
+ })
178
+ if (!valid) throw new MismatchError('Proof signature does not match source.', {})
179
+
180
+ if (proofStore) {
181
+ await assertProofUnused(proofStore, challenge.id)
182
+ await markProofUsed(proofStore, challenge.id)
183
+ }
184
+
185
+ return {
186
+ method: 'tempo',
187
+ status: 'success',
188
+ timestamp: new Date().toISOString(),
189
+ reference: challenge.id,
190
+ } as const
191
+ }
192
+
145
193
  case 'transaction': {
146
194
  const serializedTransaction = payload.signature as Transaction.TransactionSerializedTempo
147
195
 
@@ -247,10 +295,15 @@ export declare namespace charge {
247
295
  /** Testnet mode. */
248
296
  testnet?: boolean | undefined
249
297
  /**
250
- * Store for transaction hash replay protection.
298
+ * Store for charge replay protection.
251
299
  *
252
- * Use a shared store in multi-instance deployments so consumed hashes are
253
- * visible across all server instances.
300
+ * Non-zero charge flows default to an in-memory store if omitted. For
301
+ * zero-dollar proof auth, replay prevention is enabled only when a store
302
+ * is explicitly provided; otherwise proofs remain reusable until the
303
+ * challenge expires.
304
+ *
305
+ * Use a shared store in multi-instance deployments so consumed hashes and
306
+ * proofs are visible across all server instances.
254
307
  */
255
308
  store?: Store.Store | undefined
256
309
  /**
@@ -466,6 +519,11 @@ function getHashStoreKey(hash: `0x${string}`): `mppx:charge:${string}` {
466
519
  return `mppx:charge:${hash.toLowerCase()}`
467
520
  }
468
521
 
522
+ /** @internal */
523
+ function getProofStoreKey(challengeId: string): `mppx:charge:${string}` {
524
+ return `mppx:charge:proof:${challengeId}`
525
+ }
526
+
469
527
  /** @internal */
470
528
  async function assertHashUnused(
471
529
  store: Store.Store<charge.StoreItemMap>,
@@ -483,6 +541,23 @@ async function markHashUsed(
483
541
  await store.put(getHashStoreKey(hash), Date.now())
484
542
  }
485
543
 
544
+ /** @internal */
545
+ async function assertProofUnused(
546
+ store: Store.Store<charge.StoreItemMap>,
547
+ challengeId: string,
548
+ ): Promise<void> {
549
+ const seen = await store.get(getProofStoreKey(challengeId))
550
+ if (seen !== null) throw new Error('Proof credential has already been used.')
551
+ }
552
+
553
+ /** @internal */
554
+ async function markProofUsed(
555
+ store: Store.Store<charge.StoreItemMap>,
556
+ challengeId: string,
557
+ ): Promise<void> {
558
+ await store.put(getProofStoreKey(challengeId), Date.now())
559
+ }
560
+
486
561
  /** @internal */
487
562
  function toReceipt(receipt: TransactionReceipt) {
488
563
  const { status, transactionHash } = receipt