mppx 0.5.4 → 0.5.6

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 (66) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/Html.d.ts +10 -0
  3. package/dist/Html.d.ts.map +1 -0
  4. package/dist/Html.js +41 -0
  5. package/dist/Html.js.map +1 -0
  6. package/dist/server/Mppx.d.ts +1 -28
  7. package/dist/server/Mppx.d.ts.map +1 -1
  8. package/dist/server/Mppx.js +101 -30
  9. package/dist/server/Mppx.js.map +1 -1
  10. package/dist/server/Transport.d.ts.map +1 -1
  11. package/dist/server/Transport.js +18 -46
  12. package/dist/server/Transport.js.map +1 -1
  13. package/dist/server/internal/html/compose.main.gen.d.ts +2 -0
  14. package/dist/server/internal/html/compose.main.gen.d.ts.map +1 -0
  15. package/dist/server/internal/html/compose.main.gen.js +3 -0
  16. package/dist/server/internal/html/compose.main.gen.js.map +1 -0
  17. package/dist/server/internal/html/config.d.ts +46 -49
  18. package/dist/server/internal/html/config.d.ts.map +1 -1
  19. package/dist/server/internal/html/config.js +323 -117
  20. package/dist/server/internal/html/config.js.map +1 -1
  21. package/dist/server/internal/html/constants.d.ts +26 -0
  22. package/dist/server/internal/html/constants.d.ts.map +1 -0
  23. package/dist/server/internal/html/constants.js +26 -0
  24. package/dist/server/internal/html/constants.js.map +1 -0
  25. package/dist/server/internal/html/serviceWorker.client.d.ts +2 -0
  26. package/dist/server/internal/html/serviceWorker.client.d.ts.map +1 -0
  27. package/dist/server/internal/html/serviceWorker.client.js +26 -0
  28. package/dist/server/internal/html/serviceWorker.client.js.map +1 -0
  29. package/dist/stripe/server/internal/html.gen.d.ts +1 -1
  30. package/dist/stripe/server/internal/html.gen.d.ts.map +1 -1
  31. package/dist/stripe/server/internal/html.gen.js +1 -1
  32. package/dist/stripe/server/internal/html.gen.js.map +1 -1
  33. package/dist/tempo/Attribution.d.ts +24 -7
  34. package/dist/tempo/Attribution.d.ts.map +1 -1
  35. package/dist/tempo/Attribution.js +33 -7
  36. package/dist/tempo/Attribution.js.map +1 -1
  37. package/dist/tempo/client/Charge.js +1 -1
  38. package/dist/tempo/client/Charge.js.map +1 -1
  39. package/dist/tempo/server/Charge.d.ts.map +1 -1
  40. package/dist/tempo/server/Charge.js +36 -2
  41. package/dist/tempo/server/Charge.js.map +1 -1
  42. package/dist/tempo/server/internal/html.gen.d.ts +1 -1
  43. package/dist/tempo/server/internal/html.gen.d.ts.map +1 -1
  44. package/dist/tempo/server/internal/html.gen.js +1 -1
  45. package/dist/tempo/server/internal/html.gen.js.map +1 -1
  46. package/package.json +6 -1
  47. package/src/Html.ts +57 -0
  48. package/src/server/Mppx.test.ts +203 -0
  49. package/src/server/Mppx.ts +118 -3
  50. package/src/server/Transport.test.ts +5 -2
  51. package/src/server/Transport.ts +21 -54
  52. package/src/server/internal/html/compose.main.gen.ts +2 -0
  53. package/src/server/internal/html/compose.main.ts +88 -0
  54. package/src/server/internal/html/config.ts +422 -177
  55. package/src/server/internal/html/constants.ts +28 -0
  56. package/src/server/internal/html/serviceWorker.client.ts +2 -2
  57. package/src/server/internal/html/tsconfig.compose.json +8 -0
  58. package/src/stripe/server/internal/html/main.ts +44 -53
  59. package/src/stripe/server/internal/html.gen.ts +1 -1
  60. package/src/tempo/Attribution.test.ts +129 -23
  61. package/src/tempo/Attribution.ts +39 -10
  62. package/src/tempo/client/Charge.ts +1 -1
  63. package/src/tempo/server/Charge.test.ts +205 -5
  64. package/src/tempo/server/Charge.ts +54 -3
  65. package/src/tempo/server/internal/html/main.ts +26 -28
  66. package/src/tempo/server/internal/html.gen.ts +1 -1
@@ -24,6 +24,7 @@ import type * as Html from '../../server/internal/html/config.ts'
24
24
  import * as Store from '../../Store.js'
25
25
  import * as Client from '../../viem/Client.js'
26
26
  import type * as z from '../../zod.js'
27
+ import * as Attribution from '../Attribution.js'
27
28
  import * as Account from '../internal/account.js'
28
29
  import * as TempoAddress from '../internal/address.js'
29
30
  import * as Charge_internal from '../internal/charge.js'
@@ -180,12 +181,22 @@ export function charge<const parameters extends charge.Parameters>(
180
181
 
181
182
  const expectedTransfers = getExpectedTransfers({ amount, memo, methodDetails, recipient })
182
183
  const receipt = await getTransactionReceipt(client, { hash })
183
- assertTransferLogs(receipt, {
184
+ const matchedLogs = assertTransferLogs(receipt, {
184
185
  currency,
185
186
  sender: receipt.from,
186
187
  transfers: expectedTransfers,
187
188
  })
188
189
 
190
+ // Only verify challenge binding when using auto-generated attribution memos.
191
+ // Explicit memos (set by the server) are strictly matched by assertTransferLogs
192
+ // but are NOT challenge-bound — callers that set explicit memos are responsible
193
+ // for ensuring memo uniqueness per challenge to prevent cross-challenge hash reuse.
194
+ if (!memo)
195
+ assertChallengeBoundMemo(matchedLogs, {
196
+ challengeId: challenge.id,
197
+ realm: challenge.realm,
198
+ })
199
+
189
200
  await markHashUsed(store, hash)
190
201
 
191
202
  return toReceipt(receipt)
@@ -507,6 +518,18 @@ function decodeTransferCall(
507
518
  return null
508
519
  }
509
520
 
521
+ type TransferLog =
522
+ | {
523
+ kind: 'transfer'
524
+ args: { from: `0x${string}`; to: `0x${string}`; amount: bigint }
525
+ address: `0x${string}`
526
+ }
527
+ | {
528
+ kind: 'memo'
529
+ args: { from: `0x${string}`; to: `0x${string}`; amount: bigint; memo: `0x${string}` }
530
+ address: `0x${string}`
531
+ }
532
+
510
533
  function assertTransferLogs(
511
534
  receipt: TransactionReceipt,
512
535
  parameters: {
@@ -514,7 +537,7 @@ function assertTransferLogs(
514
537
  sender: `0x${string}`
515
538
  transfers: readonly ExpectedTransfer[]
516
539
  },
517
- ) {
540
+ ): TransferLog[] {
518
541
  const transferLogs = parseEventLogs({
519
542
  abi: Abis.tip20,
520
543
  eventName: 'Transfer',
@@ -527,8 +550,11 @@ function assertTransferLogs(
527
550
  logs: receipt.logs,
528
551
  }).map((log) => ({ ...log, kind: 'memo' as const }))
529
552
 
530
- const logs = [...transferLogs, ...memoLogs]
553
+ // Prefer memo logs so allowAnyMemo matches TransferWithMemo before Transfer,
554
+ // preserving the memo for challenge binding verification.
555
+ const logs = [...memoLogs, ...transferLogs]
531
556
  const used = new Set<number>()
557
+ const matched: TransferLog[] = []
532
558
 
533
559
  // Match memo-specific transfers before wildcards to avoid greedy
534
560
  // consumption of memo-bearing logs by allowAnyMemo entries.
@@ -561,7 +587,10 @@ function assertTransferLogs(
561
587
  }
562
588
 
563
589
  used.add(matchIndex)
590
+ matched.push(logs[matchIndex]! as TransferLog)
564
591
  }
592
+
593
+ return matched
565
594
  }
566
595
 
567
596
  /** @internal */
@@ -624,6 +653,28 @@ function toReceipt(receipt: TransactionReceipt) {
624
653
  } as const
625
654
  }
626
655
 
656
+ /**
657
+ * Asserts that at least one of the matched payment logs carries a
658
+ * challenge-bound memo nonce (keccak256(challengeId)[0..6] in bytes 25–31).
659
+ * Only checks logs that were matched by `assertTransferLogs`, not the
660
+ * entire receipt — preventing unrelated dust transfers from satisfying
661
+ * the binding.
662
+ * @internal
663
+ */
664
+ function assertChallengeBoundMemo(
665
+ matchedLogs: readonly TransferLog[],
666
+ parameters: { challengeId: string; realm: string },
667
+ ) {
668
+ const bound = matchedLogs.some((log) => {
669
+ if (log.kind !== 'memo') return false
670
+ if (!Attribution.verifyServer(log.args.memo, parameters.realm)) return false
671
+ return Attribution.verifyChallengeBinding(log.args.memo, parameters.challengeId)
672
+ })
673
+
674
+ if (!bound)
675
+ throw new MismatchError('Payment verification failed: memo is not bound to this challenge.', {})
676
+ }
677
+
627
678
  /** @internal */
628
679
  class MismatchError extends PaymentError {
629
680
  override readonly name = 'MismatchError'
@@ -1,17 +1,12 @@
1
1
  import { local, Provider } from 'accounts'
2
- import { Json } from 'ox'
3
2
  import { createClient, custom, http } from 'viem'
4
3
  import { tempoModerato, tempoLocalnet } from 'viem/chains'
5
4
 
6
5
  import { tempo } from '../../../../client/index.js'
7
- import * as Html from '../../../../server/internal/html/config.js'
8
- import { submitCredential } from '../../../../server/internal/html/serviceWorker.client.js'
6
+ import * as Html from '../../../../Html.js'
9
7
  import type * as Methods from '../../../Methods.js'
10
8
 
11
- const dataElement = document.getElementById(Html.dataId)!
12
- const data = Json.parse(dataElement.textContent) as Html.Data<typeof Methods.charge>
13
-
14
- const root = document.getElementById(Html.rootId)!
9
+ const c = Html.init<typeof Methods.charge>('tempo')
15
10
 
16
11
  const css = String.raw
17
12
  const style = document.createElement('style')
@@ -19,15 +14,15 @@ style.textContent = css`
19
14
  form {
20
15
  display: flex;
21
16
  flex-direction: column;
22
- gap: calc(${Html.vars.spacingUnit} * 8);
17
+ gap: calc(${c.vars.spacingUnit} * 8);
23
18
  }
24
19
  button {
25
- background: ${Html.vars.accent};
26
- border-radius: ${Html.vars.radius};
27
- color: ${Html.vars.background};
20
+ background: ${c.vars.accent};
21
+ border-radius: ${c.vars.radius};
22
+ color: ${c.vars.background};
28
23
  cursor: pointer;
29
24
  font-weight: 500;
30
- padding: calc(${Html.vars.spacingUnit} * 4) calc(${Html.vars.spacingUnit} * 8);
25
+ padding: calc(${c.vars.spacingUnit} * 4) calc(${c.vars.spacingUnit} * 8);
31
26
  width: 100%;
32
27
  }
33
28
  button:hover:not(:disabled) {
@@ -46,7 +41,7 @@ style.textContent = css`
46
41
  width: auto;
47
42
  }
48
43
  `
49
- root.append(style)
44
+ c.root.append(style)
50
45
 
51
46
  const provider = Provider.create({
52
47
  // Dead code eliminated from production bundle (including top-level imports)
@@ -60,7 +55,7 @@ const provider = Provider.create({
60
55
  const account = Account.fromSecp256k1(privateKey)
61
56
  const client = createClient({
62
57
  chain: [tempoModerato, tempoLocalnet].find(
63
- (x) => x.id === data.challenge.request.methodDetails?.chainId,
58
+ (x) => x.id === c.challenge.request.methodDetails?.chainId,
64
59
  ),
65
60
  transport: http(),
66
61
  })
@@ -73,8 +68,8 @@ const provider = Provider.create({
73
68
  }
74
69
  : {}),
75
70
  testnet:
76
- data.challenge.request.methodDetails?.chainId === tempoModerato.id ||
77
- data.challenge.request.methodDetails?.chainId === tempoLocalnet.id,
71
+ c.challenge.request.methodDetails?.chainId === tempoModerato.id ||
72
+ c.challenge.request.methodDetails?.chainId === tempoLocalnet.id,
78
73
  })
79
74
 
80
75
  const button = document.createElement('button')
@@ -82,30 +77,33 @@ button.innerHTML =
82
77
  'Continue with <svg aria-label="Tempo" viewBox="0 0 107 25" role="img"><path d="M8.10464 23.7163H1.82475L7.64513 5.79356H0.201172L1.82475 0.540352H22.5637L20.9401 5.79356H13.8944L8.10464 23.7163Z"></path><path d="M31.474 23.7163H16.5861L24.0607 0.540352H38.8873L37.4782 4.95923H28.8701L27.3078 9.93433H35.6402L34.231 14.2914H25.8681L24.3057 19.2974H32.8525L31.474 23.7163Z"></path><path d="M38.2124 23.7163H33.2192L40.7244 0.540352H49.0567L48.781 13.0245L56.8989 0.540352H66.0277L58.5531 23.7163H52.3039L57.3584 7.86395L46.9736 23.7163H43.267L43.4201 7.80214L38.2124 23.7163Z"></path><path d="M73.057 4.83563L70.6369 12.3137H71.3108C72.8425 12.3137 74.1189 11.9532 75.14 11.2322C76.1612 10.4906 76.8249 9.43991 77.1312 8.08025C77.3967 6.90601 77.2538 6.07167 76.7023 5.57725C76.1509 5.08284 75.2319 4.83563 73.9453 4.83563H73.057ZM66.9915 23.7163H60.7116L68.1862 0.540352H75.814C77.5703 0.540352 79.0816 0.828764 80.3478 1.40559C81.6344 1.96181 82.5738 2.76524 83.166 3.81588C83.7787 4.84592 83.9829 6.05107 83.7787 7.43133C83.5132 9.2442 82.8189 10.8408 81.6956 12.221C80.5724 13.6013 79.1122 14.6725 77.315 15.4347C75.5383 16.1764 73.5471 16.5472 71.3415 16.5472H69.289L66.9915 23.7163Z"></path><path d="M98.747 22.233C96.664 23.4691 94.4481 24.0871 92.0996 24.0871H92.0383C89.9552 24.0871 88.1989 23.6236 86.7693 22.6965C85.3602 21.7489 84.3493 20.4717 83.7366 18.8648C83.1443 17.2579 83.0014 15.4966 83.3077 13.5807C83.6957 11.1704 84.5841 8.94549 85.9728 6.90601C87.3616 4.86653 89.0975 3.23906 91.1805 2.02361C93.2636 0.808164 95.4897 0.200439 97.8587 0.200439H97.9199C100.085 0.200439 101.872 0.663958 103.281 1.591C104.71 2.51803 105.701 3.78498 106.252 5.39185C106.824 6.97811 106.947 8.76008 106.62 10.7378C106.232 13.0657 105.343 15.2596 103.955 17.3197C102.566 19.3592 100.83 20.997 98.747 22.233ZM90.0777 18.2468C90.6292 19.2974 91.589 19.8227 92.9573 19.8227H93.0186C94.1418 19.8227 95.1833 19.4004 96.1432 18.5558C97.1235 17.6905 97.9506 16.5369 98.6245 15.0948C99.3189 13.6528 99.8294 12.0459 100.156 10.2742C100.463 8.54377 100.34 7.15322 99.7886 6.10257C99.2372 5.03133 98.2875 4.49571 96.9397 4.49571H96.8784C95.8369 4.49571 94.826 4.92833 93.8457 5.79356C92.8858 6.6588 92.0485 7.82274 91.3337 9.2854C90.6189 10.7481 90.0982 12.3343 89.7714 14.0442C89.4446 15.7747 89.5468 17.1755 90.0777 18.2468Z"></path></svg>'
83
78
  button.onclick = async () => {
84
79
  try {
85
- document.getElementById(Html.errorId)?.remove()
80
+ c.error()
86
81
  button.disabled = true
87
82
 
88
- const chain = [...(provider?.chains ?? []), tempoModerato, tempoLocalnet].find(
89
- (x) => x.id === data.challenge.request.methodDetails?.chainId,
90
- )
91
- const client = createClient({ chain, transport: custom(provider) })
92
83
  const account = await (async () => {
93
84
  const accounts = await provider.request({ method: 'eth_accounts' })
94
85
  if (accounts.length > 0) return accounts.at(0)
95
86
  const result = await provider.request({ method: 'wallet_connect' })
96
87
  return result.accounts[0]?.address
97
88
  })()
98
- const method = tempo({ account, getClient: () => client })[0]
89
+ const method = tempo({
90
+ account,
91
+ getClient(opts) {
92
+ const chainId = opts.chainId ?? c.challenge.request.methodDetails?.chainId
93
+ const chain = [...(provider?.chains ?? []), tempoModerato, tempoLocalnet].find(
94
+ (x) => x.id === chainId,
95
+ )
96
+ return createClient({ chain, transport: custom(provider) })
97
+ },
98
+ })[0]
99
99
 
100
- const credential = await method.createCredential({ challenge: data.challenge, context: {} })
101
- await submitCredential(credential)
100
+ const credential = await method.createCredential({ challenge: c.challenge, context: {} })
101
+ await c.submit(credential)
102
102
  } catch (e) {
103
103
  const message = e instanceof Error && 'shortMessage' in e ? (e as any).shortMessage : undefined
104
- Html.showError(message ?? (e instanceof Error ? e.message : 'Payment failed'))
104
+ c.error(message ?? (e instanceof Error ? e.message : 'Payment failed'))
105
105
  } finally {
106
106
  button.disabled = false
107
107
  }
108
108
  }
109
- root.appendChild(button)
110
-
111
- dataElement.remove()
109
+ c.root.appendChild(button)