mppx 0.8.13 → 0.8.14

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 (123) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/Errors.d.ts +10 -0
  3. package/dist/Errors.d.ts.map +1 -1
  4. package/dist/Errors.js +11 -2
  5. package/dist/Errors.js.map +1 -1
  6. package/dist/Method.d.ts +105 -3
  7. package/dist/Method.d.ts.map +1 -1
  8. package/dist/Method.js +102 -2
  9. package/dist/Method.js.map +1 -1
  10. package/dist/client/internal/Fetch.d.ts.map +1 -1
  11. package/dist/client/internal/Fetch.js +12 -2
  12. package/dist/client/internal/Fetch.js.map +1 -1
  13. package/dist/client/internal/MethodChallenge.d.ts +17 -0
  14. package/dist/client/internal/MethodChallenge.d.ts.map +1 -0
  15. package/dist/client/internal/MethodChallenge.js +11 -0
  16. package/dist/client/internal/MethodChallenge.js.map +1 -0
  17. package/dist/server/Mppx.d.ts +23 -4
  18. package/dist/server/Mppx.d.ts.map +1 -1
  19. package/dist/server/Mppx.js +91 -82
  20. package/dist/server/Mppx.js.map +1 -1
  21. package/dist/tempo/client/Methods.d.ts +1 -0
  22. package/dist/tempo/client/Methods.d.ts.map +1 -1
  23. package/dist/tempo/internal/fee-payer.d.ts +1 -0
  24. package/dist/tempo/internal/fee-payer.d.ts.map +1 -1
  25. package/dist/tempo/internal/fee-payer.js +7 -2
  26. package/dist/tempo/internal/fee-payer.js.map +1 -1
  27. package/dist/tempo/legacy/client/ChannelOps.d.ts.map +1 -1
  28. package/dist/tempo/legacy/client/ChannelOps.js +2 -1
  29. package/dist/tempo/legacy/client/ChannelOps.js.map +1 -1
  30. package/dist/tempo/server/Charge.d.ts +38 -77
  31. package/dist/tempo/server/Charge.d.ts.map +1 -1
  32. package/dist/tempo/server/Charge.js +289 -209
  33. package/dist/tempo/server/Charge.js.map +1 -1
  34. package/dist/tempo/server/Methods.d.ts +12 -1
  35. package/dist/tempo/server/Methods.d.ts.map +1 -1
  36. package/dist/tempo/server/Methods.js +4 -0
  37. package/dist/tempo/server/Methods.js.map +1 -1
  38. package/dist/tempo/server/Relay.d.ts +48 -0
  39. package/dist/tempo/server/Relay.d.ts.map +1 -0
  40. package/dist/tempo/server/Relay.js +177 -0
  41. package/dist/tempo/server/Relay.js.map +1 -0
  42. package/dist/tempo/server/SponsorBudget.d.ts +59 -0
  43. package/dist/tempo/server/SponsorBudget.d.ts.map +1 -0
  44. package/dist/tempo/server/SponsorBudget.js +144 -0
  45. package/dist/tempo/server/SponsorBudget.js.map +1 -0
  46. package/dist/tempo/server/internal/html.gen.d.ts +1 -1
  47. package/dist/tempo/server/internal/html.gen.d.ts.map +1 -1
  48. package/dist/tempo/server/internal/html.gen.js +1 -1
  49. package/dist/tempo/server/internal/html.gen.js.map +1 -1
  50. package/dist/tempo/session/client/ChannelOps.d.ts +7 -1
  51. package/dist/tempo/session/client/ChannelOps.d.ts.map +1 -1
  52. package/dist/tempo/session/client/ChannelOps.js +9 -4
  53. package/dist/tempo/session/client/ChannelOps.js.map +1 -1
  54. package/dist/tempo/session/client/CredentialState.d.ts +5 -1
  55. package/dist/tempo/session/client/CredentialState.d.ts.map +1 -1
  56. package/dist/tempo/session/client/CredentialState.js +37 -19
  57. package/dist/tempo/session/client/CredentialState.js.map +1 -1
  58. package/dist/tempo/session/client/Runtime.d.ts +8 -0
  59. package/dist/tempo/session/client/Runtime.d.ts.map +1 -1
  60. package/dist/tempo/session/client/Runtime.js +17 -0
  61. package/dist/tempo/session/client/Runtime.js.map +1 -1
  62. package/dist/tempo/session/client/Session.d.ts +15 -5
  63. package/dist/tempo/session/client/Session.d.ts.map +1 -1
  64. package/dist/tempo/session/client/Session.js +93 -36
  65. package/dist/tempo/session/client/Session.js.map +1 -1
  66. package/dist/tempo/session/client/SessionManager.d.ts +8 -1
  67. package/dist/tempo/session/client/SessionManager.d.ts.map +1 -1
  68. package/dist/tempo/session/client/SessionManager.js +54 -26
  69. package/dist/tempo/session/client/SessionManager.js.map +1 -1
  70. package/dist/tempo/session/client/Transports.d.ts +7 -5
  71. package/dist/tempo/session/client/Transports.d.ts.map +1 -1
  72. package/dist/tempo/session/client/Transports.js +48 -24
  73. package/dist/tempo/session/client/Transports.js.map +1 -1
  74. package/dist/tempo/session/precompile/Chain.d.ts.map +1 -1
  75. package/dist/tempo/session/precompile/Chain.js +96 -8
  76. package/dist/tempo/session/precompile/Chain.js.map +1 -1
  77. package/dist/tempo/session/precompile/Channel.d.ts +2 -0
  78. package/dist/tempo/session/precompile/Channel.d.ts.map +1 -1
  79. package/dist/tempo/session/precompile/Channel.js +4 -0
  80. package/dist/tempo/session/precompile/Channel.js.map +1 -1
  81. package/dist/tempo/session/server/RequestState.js +3 -1
  82. package/dist/tempo/session/server/RequestState.js.map +1 -1
  83. package/package.json +1 -1
  84. package/src/Errors.test.ts +23 -0
  85. package/src/Errors.ts +21 -2
  86. package/src/Method.test.ts +102 -1
  87. package/src/Method.ts +241 -5
  88. package/src/client/Mppx.test-d.ts +3 -3
  89. package/src/client/internal/Fetch.ts +16 -7
  90. package/src/client/internal/MethodChallenge.ts +30 -0
  91. package/src/server/Mppx.test-d.ts +2 -0
  92. package/src/server/Mppx.test.ts +205 -0
  93. package/src/server/Mppx.ts +167 -103
  94. package/src/tempo/PublicExports.test-d.ts +20 -0
  95. package/src/tempo/internal/fee-payer.ts +7 -3
  96. package/src/tempo/legacy/client/ChannelOps.test.ts +7 -0
  97. package/src/tempo/legacy/client/ChannelOps.ts +2 -1
  98. package/src/tempo/server/Charge.test.ts +337 -63
  99. package/src/tempo/server/Charge.ts +435 -267
  100. package/src/tempo/server/Methods.ts +11 -0
  101. package/src/tempo/server/Relay.test.ts +523 -0
  102. package/src/tempo/server/Relay.ts +288 -0
  103. package/src/tempo/server/SponsorBudget.test.ts +125 -0
  104. package/src/tempo/server/SponsorBudget.ts +213 -0
  105. package/src/tempo/server/internal/html.gen.ts +1 -1
  106. package/src/tempo/session/client/ChannelOps.test.ts +17 -2
  107. package/src/tempo/session/client/ChannelOps.ts +12 -4
  108. package/src/tempo/session/client/CredentialState.ts +61 -20
  109. package/src/tempo/session/client/Runtime.test.ts +12 -0
  110. package/src/tempo/session/client/Runtime.ts +22 -0
  111. package/src/tempo/session/client/Session.test.ts +235 -2
  112. package/src/tempo/session/client/Session.ts +130 -41
  113. package/src/tempo/session/client/SessionManager.test.ts +102 -4
  114. package/src/tempo/session/client/SessionManager.ts +71 -27
  115. package/src/tempo/session/client/Transports.test.ts +32 -1
  116. package/src/tempo/session/client/Transports.ts +79 -35
  117. package/src/tempo/session/precompile/Chain.integration.test.ts +19 -1
  118. package/src/tempo/session/precompile/Chain.test.ts +197 -10
  119. package/src/tempo/session/precompile/Chain.ts +102 -8
  120. package/src/tempo/session/precompile/Channel.test.ts +9 -0
  121. package/src/tempo/session/precompile/Channel.ts +5 -0
  122. package/src/tempo/session/server/RequestState.test.ts +8 -1
  123. package/src/tempo/session/server/RequestState.ts +2 -2
@@ -9,7 +9,7 @@ import * as Expires from '../Expires.js'
9
9
  import * as AcceptPayment from '../internal/AcceptPayment.js'
10
10
  import * as Env from '../internal/env.js'
11
11
  import type { MaybePromise } from '../internal/types.js'
12
- import type * as Method from '../Method.js'
12
+ import * as Method from '../Method.js'
13
13
  import * as PaymentRequest from '../PaymentRequest.js'
14
14
  import type * as Receipt from '../Receipt.js'
15
15
  import * as x402_Header from '../x402/Header.js'
@@ -248,14 +248,18 @@ export type Mppx<
248
248
  /**
249
249
  * Verify a credential string or object end-to-end: deserialize,
250
250
  * HMAC-check, match to a registered method, validate payload schema,
251
- * check expiry, and call the method's verify function.
251
+ * check expiry, and call the method's broadcast or legacy verify function.
252
252
  *
253
- * Method verification can settle payments and persist state. For example,
253
+ * Method verification can broadcast payments and persist state. For example,
254
254
  * subscription credentials may activate or renew a subscription. Failed
255
255
  * standalone verification emits `payment.failed` once a credential challenge
256
256
  * can be parsed; strings that cannot be deserialized have no challenge
257
257
  * context to report.
258
258
  *
259
+ * @deprecated Use `broadcastCredential()` instead. This legacy name maps
260
+ * to the same mutating operation and does not provide validation-only
261
+ * semantics.
262
+ *
259
263
  * @example
260
264
  * ```ts
261
265
  * const receipt = await mppx.verifyCredential('eyJjaGFsbGVuZ2...')
@@ -267,6 +271,27 @@ export type Mppx<
267
271
  credential: string | Credential.Credential,
268
272
  options?: VerifyCredentialOptions | undefined,
269
273
  ): Promise<Receipt.Receipt>
274
+ /**
275
+ * Validate a credential without consuming or broadcasting it.
276
+ *
277
+ * This is an advisory pre-check. Methods that support it must not write
278
+ * replay state, sign fee-payer transactions, broadcast, or otherwise
279
+ * mutate payment state. Use `broadcastCredential()` when accepting payment.
280
+ */
281
+ validateCredential(
282
+ credential: string | Credential.Credential,
283
+ options?: VerifyCredentialOptions | undefined,
284
+ ): Promise<Method.Validation>
285
+ /**
286
+ * Re-validates and broadcasts a credential, returning a receipt.
287
+ *
288
+ * `verifyCredential()` is retained as a backwards-compatible alias for
289
+ * this mutating path.
290
+ */
291
+ broadcastCredential(
292
+ credential: string | Credential.Credential,
293
+ options?: VerifyCredentialOptions | undefined,
294
+ ): Promise<Receipt.Receipt>
270
295
  }
271
296
 
272
297
  /** Extracts the transport override from a method, if any. */
@@ -292,7 +317,9 @@ const reservedMppxKeyValues = [
292
317
  'onPaymentFailed',
293
318
  'onPaymentSuccess',
294
319
  'realm',
320
+ 'broadcastCredential',
295
321
  'transport',
322
+ 'validateCredential',
296
323
  'verifyCredential',
297
324
  ] as const
298
325
 
@@ -457,9 +484,11 @@ export function create<
457
484
  preflight: mi.preflight as never,
458
485
  request: mi.request as never,
459
486
  respond: mi.respond as never,
487
+ broadcast: mi.broadcast as never,
460
488
  secretKey,
461
489
  stableBinding: mi.stableBinding as never,
462
490
  transport: (mi.transport ?? transport) as never,
491
+ validate: mi.validate as never,
463
492
  verify: mi.verify as never,
464
493
  })
465
494
  const wireKey = `${mi.name}/${mi.intent}`
@@ -496,125 +525,118 @@ export function create<
496
525
  })
497
526
  }
498
527
 
499
- // verifyCredential: single-call end-to-end verification
500
- async function verifyCredentialFn(
528
+ async function prepareStandaloneCredential(
501
529
  input: string | Credential.Credential,
502
- options?: VerifyCredentialOptions,
503
- ): Promise<Receipt.Receipt> {
530
+ options: VerifyCredentialOptions | undefined,
531
+ parameters: { emitFailures?: boolean | undefined; requireValidate?: boolean | undefined } = {},
532
+ ) {
504
533
  const credential = hydrateCredentialMeta(
505
534
  typeof input === 'string' ? Credential.deserialize(input) : input,
506
535
  )
507
536
 
508
- // Find matching method by name + intent, then use method-details markers
509
- // when multiple handlers intentionally share the same wire identity.
537
+ const emitFailures = parameters.emitFailures === true
510
538
  const { method: credMethod, intent: credIntent } = credential.challenge
511
539
  const methodCandidates = (methods as readonly Method.AnyServer[]).filter(
512
540
  (m) => m.name === credMethod && m.intent === credIntent,
513
541
  )
514
- const mi = selectVerificationMethod(methodCandidates, credential.challenge)
542
+ const mi = Method.selectServerMethod(methodCandidates, credential.challenge)
515
543
  const eventMethod =
516
544
  mi ?? ({ intent: credIntent, name: credMethod } satisfies ServerMethodDescriptor)
517
545
 
518
- const emitStandalonePaymentFailed = async (parameters: {
546
+ const emitStandalonePaymentFailed = async (failure: {
519
547
  challenge: Challenge.Challenge
520
548
  credential: Credential.Credential | null
521
549
  error: Errors.PaymentError
522
550
  request: Record<string, unknown>
523
551
  submittedChallenge?: Challenge.Challenge | undefined
524
552
  }) => {
553
+ if (!emitFailures) return
525
554
  await serverEvents.emit(
526
555
  'payment.failed',
527
556
  createPaymentFailedContext({
528
557
  capturedRequest: options?.capturedRequest,
529
- challenge: parameters.challenge,
530
- credential: parameters.credential,
531
- error: parameters.error,
558
+ challenge: failure.challenge,
559
+ credential: failure.credential,
560
+ error: failure.error,
532
561
  method: eventMethod,
533
- request: parameters.request,
534
- submittedChallenge: parameters.submittedChallenge,
562
+ request: failure.request,
563
+ submittedChallenge: failure.submittedChallenge,
535
564
  }) as never,
536
565
  )
537
566
  }
538
567
 
539
- if (!mi) {
540
- const error = new Errors.InvalidChallengeError({
541
- id: credential.challenge.id,
542
- reason: `no registered method for ${credMethod}/${credIntent}`,
543
- })
568
+ const fail = async (
569
+ error: Errors.PaymentError,
570
+ failure: {
571
+ credential?: Credential.Credential | null | undefined
572
+ request?: Record<string, unknown> | undefined
573
+ submittedChallenge?: Challenge.Challenge | undefined
574
+ } = {},
575
+ thrown: unknown = error,
576
+ ): Promise<never> => {
544
577
  await emitStandalonePaymentFailed({
545
578
  challenge: credential.challenge,
546
- credential,
579
+ credential: failure.credential ?? credential,
547
580
  error,
548
- request: credential.challenge.request as Record<string, unknown>,
549
- submittedChallenge: credential.challenge,
581
+ request: failure.request ?? (credential.challenge.request as Record<string, unknown>),
582
+ submittedChallenge: failure.submittedChallenge ?? credential.challenge,
550
583
  })
551
- throw error
584
+ throw thrown
585
+ }
586
+
587
+ if (!mi) {
588
+ await fail(
589
+ new Errors.InvalidChallengeError({
590
+ id: credential.challenge.id,
591
+ reason: `no registered method for ${credMethod}/${credIntent}`,
592
+ }),
593
+ )
552
594
  }
553
595
 
554
- // HMAC provenance check (secretKey is guaranteed non-null by the guard at the top of create())
596
+ if (parameters.requireValidate && !mi.validate)
597
+ await fail(
598
+ new Errors.VerificationFailedError({
599
+ details: { intent: credIntent, method: credMethod },
600
+ reason: `${credMethod}/${credIntent} does not support non-mutating credential validation`,
601
+ }),
602
+ )
603
+
555
604
  if (!Challenge.verify(credential.challenge, { secretKey: secretKey! })) {
556
- const error = new Errors.InvalidChallengeError({
557
- id: credential.challenge.id,
558
- reason: 'challenge was not issued by this server',
559
- })
560
- await emitStandalonePaymentFailed({
561
- challenge: credential.challenge,
562
- credential,
563
- error,
564
- request: credential.challenge.request as Record<string, unknown>,
565
- submittedChallenge: credential.challenge,
566
- })
567
- throw error
605
+ await fail(
606
+ new Errors.InvalidChallengeError({
607
+ id: credential.challenge.id,
608
+ reason: 'challenge was not issued by this server',
609
+ }),
610
+ )
568
611
  }
569
612
 
570
- // Expiry check
571
613
  try {
572
614
  Expires.assert(credential.challenge.expires, credential.challenge.id)
573
615
  } catch (e) {
574
- if (e instanceof Errors.PaymentError)
575
- await emitStandalonePaymentFailed({
576
- challenge: credential.challenge,
577
- credential,
578
- error: e,
579
- request: credential.challenge.request as Record<string, unknown>,
580
- submittedChallenge: credential.challenge,
581
- })
616
+ if (e instanceof Errors.PaymentError) await fail(e)
582
617
  throw e
583
618
  }
584
619
 
585
- // Validate payload against method schema
586
- let parsedCredential: Credential.Credential
620
+ let parsedCredential!: Credential.Credential
587
621
  try {
588
622
  parsedCredential = withParsedCredentialPayload(
589
623
  credential,
590
624
  mi.schema.credential.payload.parse(credential.payload),
591
625
  )
592
626
  } catch (e) {
593
- await emitStandalonePaymentFailed({
594
- challenge: credential.challenge,
595
- credential,
596
- error: new Errors.InvalidPayloadError(),
597
- request: credential.challenge.request as Record<string, unknown>,
598
- submittedChallenge: credential.challenge,
599
- })
600
- throw e
627
+ await fail(new Errors.InvalidPayloadError(), {}, e)
601
628
  }
602
629
 
603
630
  const expectedMeta = Scope.merge({ meta: options?.meta, scope: options?.scope })
604
631
 
605
632
  if (options?.scope !== undefined && Scope.read(credential.challenge.meta) !== options.scope) {
606
- const error = new Errors.InvalidChallengeError({
607
- id: credential.challenge.id,
608
- reason: "credential scope does not match this route's requirements",
609
- })
610
- await emitStandalonePaymentFailed({
611
- challenge: credential.challenge,
612
- credential: parsedCredential,
613
- error,
614
- request: credential.challenge.request as Record<string, unknown>,
615
- submittedChallenge: credential.challenge,
616
- })
617
- throw error
633
+ await fail(
634
+ new Errors.InvalidChallengeError({
635
+ id: credential.challenge.id,
636
+ reason: "credential scope does not match this route's requirements",
637
+ }),
638
+ { credential: parsedCredential },
639
+ )
618
640
  }
619
641
 
620
642
  const shouldValidateRoute =
@@ -660,12 +682,9 @@ export function create<
660
682
  : (credential.challenge.request as z.input<typeof mi.schema.request>)
661
683
  } catch (e) {
662
684
  if (e instanceof Errors.PaymentError)
663
- await emitStandalonePaymentFailed({
664
- challenge: credential.challenge,
685
+ await fail(e, {
665
686
  credential: parsedCredential,
666
- error: e,
667
687
  request: credential.challenge.request as Record<string, unknown>,
668
- submittedChallenge: credential.challenge,
669
688
  })
670
689
  throw e
671
690
  }
@@ -679,17 +698,72 @@ export function create<
679
698
  } as Method.VerifiedChallengeEnvelope)
680
699
  : undefined
681
700
 
701
+ return {
702
+ credential,
703
+ envelope,
704
+ eventMethod,
705
+ method: mi,
706
+ parsedCredential,
707
+ parsedRequest,
708
+ request,
709
+ }
710
+ }
711
+
712
+ async function validateCredentialFn(
713
+ input: string | Credential.Credential,
714
+ options?: VerifyCredentialOptions,
715
+ ): Promise<Method.Validation> {
716
+ const prepared = await prepareStandaloneCredential(input, options, { requireValidate: true })
717
+ return prepared.method.validate!({
718
+ credential: prepared.parsedCredential,
719
+ envelope: prepared.envelope,
720
+ request: prepared.request,
721
+ } as never)
722
+ }
723
+
724
+ // broadcastCredential: single-call end-to-end validation and broadcast
725
+ async function broadcastCredentialFn(
726
+ input: string | Credential.Credential,
727
+ options?: VerifyCredentialOptions,
728
+ ): Promise<Receipt.Receipt> {
729
+ const prepared = await prepareStandaloneCredential(input, options, { emitFailures: true })
730
+ const { method: mi, parsedCredential, parsedRequest, request, envelope } = prepared
731
+
732
+ const emitStandalonePaymentFailed = async (parameters: {
733
+ challenge: Challenge.Challenge
734
+ credential: Credential.Credential | null
735
+ error: Errors.PaymentError
736
+ request: Record<string, unknown>
737
+ submittedChallenge?: Challenge.Challenge | undefined
738
+ }) => {
739
+ await serverEvents.emit(
740
+ 'payment.failed',
741
+ createPaymentFailedContext({
742
+ capturedRequest: options?.capturedRequest,
743
+ challenge: parameters.challenge,
744
+ credential: parameters.credential,
745
+ error: parameters.error,
746
+ method: prepared.eventMethod,
747
+ request: parameters.request,
748
+ submittedChallenge: parameters.submittedChallenge,
749
+ }) as never,
750
+ )
751
+ }
752
+
682
753
  let receipt: Receipt.Receipt
683
754
  try {
684
- receipt = await mi.verify({ credential: parsedCredential, envelope, request } as never)
755
+ if (mi.broadcast && mi.validate)
756
+ await mi.validate({ credential: parsedCredential, envelope, request } as never)
757
+ const broadcast = mi.broadcast ?? mi.verify
758
+ receipt = await broadcast({ credential: parsedCredential, envelope, request } as never)
685
759
  } catch (e) {
686
760
  const error = e instanceof Errors.PaymentError ? e : new Errors.VerificationFailedError()
687
761
  await emitStandalonePaymentFailed({
688
- challenge: credential.challenge,
762
+ challenge: prepared.credential.challenge,
689
763
  credential: parsedCredential,
690
764
  error,
691
765
  request: parsedRequest,
692
- submittedChallenge: credential.challenge,
766
+ submittedChallenge: prepared.credential.challenge,
693
767
  })
694
768
  throw e
695
769
  }
@@ -698,7 +772,7 @@ export function create<
698
772
  'payment.success',
699
773
  createPaymentSuccessContext({
700
774
  capturedRequest: options?.capturedRequest,
701
- challenge: credential.challenge,
775
+ challenge: prepared.credential.challenge,
702
776
  credential: parsedCredential,
703
777
  envelope,
704
778
  method: mi,
@@ -710,6 +784,8 @@ export function create<
710
784
  return receipt
711
785
  }
712
786
 
787
+ const verifyCredentialFn = broadcastCredentialFn
788
+
713
789
  function composeFn(
714
790
  ...entries: readonly [
715
791
  Method.AnyServer | AnyMethodFnWithMethod | string,
@@ -760,7 +836,9 @@ export function create<
760
836
  onPaymentFailed,
761
837
  onPaymentSuccess,
762
838
  realm: realm as string | undefined,
839
+ broadcastCredential: broadcastCredentialFn,
763
840
  transport,
841
+ validateCredential: validateCredentialFn,
764
842
  verifyCredential: verifyCredentialFn,
765
843
  ...handlers,
766
844
  } as never
@@ -809,8 +887,10 @@ function createMethodFn(parameters: createMethodFn.Parameters): createMethodFn.R
809
887
  realm,
810
888
  respond,
811
889
  secretKey,
890
+ broadcast,
812
891
  stableBinding,
813
892
  transport,
893
+ validate,
814
894
  verify,
815
895
  } = parameters
816
896
 
@@ -1256,7 +1336,14 @@ function createMethodFn(parameters: createMethodFn.Parameters): createMethodFn.R
1256
1336
  // If verification fails, re-issue the challenge so the client can retry.
1257
1337
  let receiptData: Receipt.Receipt
1258
1338
  try {
1259
- receiptData = await verify({ credential: parsedCredential, envelope, request } as never)
1339
+ if (broadcast && validate)
1340
+ await validate({ credential: parsedCredential, envelope, request } as never)
1341
+ const broadcastCredential = broadcast ?? verify
1342
+ receiptData = await broadcastCredential({
1343
+ credential: parsedCredential,
1344
+ envelope,
1345
+ request,
1346
+ } as never)
1260
1347
  } catch (e) {
1261
1348
  if (!(e instanceof Errors.PaymentError))
1262
1349
  console.error('mppx: internal verification error', e)
@@ -1375,9 +1462,11 @@ declare namespace createMethodFn {
1375
1462
  realm: string | undefined
1376
1463
  request?: Method.RequestFn<method>
1377
1464
  respond?: Method.RespondFn<method>
1465
+ broadcast?: Method.BroadcastFn<method>
1378
1466
  secretKey: string
1379
1467
  stableBinding?: Method.StableBindingFn<method>
1380
1468
  transport: transport
1469
+ validate?: Method.ValidateFn<method>
1381
1470
  verify: Method.VerifyFn<method>
1382
1471
  }
1383
1472
 
@@ -1843,31 +1932,6 @@ type PinnedRequestBindingField = (typeof pinnedRequestBindingFields)[number]
1843
1932
  type PinnedChallengeField = 'method' | 'intent' | 'realm' | 'opaque' | PinnedRequestBindingField
1844
1933
  type StableBinding = Record<string, unknown>
1845
1934
 
1846
- function selectVerificationMethod(
1847
- methods: readonly Method.AnyServer[],
1848
- challenge: Challenge.Challenge,
1849
- ): Method.AnyServer | undefined {
1850
- if (methods.length <= 1) return methods[0]
1851
- if (
1852
- challenge.method !== Constants.Methods.tempo ||
1853
- challenge.intent !== Constants.Intents.session
1854
- )
1855
- return methods[0]
1856
-
1857
- const sessionProtocolMarker = Constants.getMethodDetail(
1858
- challenge.request.methodDetails,
1859
- Constants.MethodDetailKeys.sessionProtocol,
1860
- )
1861
- if (
1862
- sessionProtocolMarker === undefined ||
1863
- sessionProtocolMarker === Constants.SessionProtocols.v1
1864
- )
1865
- return methods.find((method) => method.alias === 'sessionLegacy') ?? methods[0]
1866
- if (sessionProtocolMarker === Constants.SessionProtocols.v2)
1867
- return methods.find((method) => method.alias === undefined) ?? methods[0]
1868
- return undefined
1869
- }
1870
-
1871
1935
  function getChallengeBindingMismatch(
1872
1936
  expectedChallenge: Challenge.Challenge,
1873
1937
  actualChallenge: Challenge.Challenge,
@@ -8,6 +8,7 @@ import type {
8
8
  SessionManagerWebSocketOptions as ClientSessionManagerWebSocketOptions,
9
9
  } from './client/index.js'
10
10
  import * as Tempo from './index.js'
11
+ import type { charge as ServerCharge } from './server/Charge.js'
11
12
  import type { SettlementSchedule as ServerSettlementSchedule } from './server/index.js'
12
13
  import type {
13
14
  ActiveSessionState,
@@ -31,6 +32,25 @@ test('tempo session public barrels expose manager and schedule interfaces', () =
31
32
  expectTypeOf(Tempo.Session.Precompile).toBeObject()
32
33
  expectTypeOf(Tempo.Session.Server).toBeObject()
33
34
  expectTypeOf<typeof Tempo>().not.toHaveProperty('Precompile')
35
+ expectTypeOf<serverTempo.RelayOptions>().toEqualTypeOf<ServerCharge.RelayOptions>()
36
+ expectTypeOf<serverTempo.RelayErrorCode>().toEqualTypeOf<
37
+ | 'already_used'
38
+ | 'broadcast_failed'
39
+ | 'expired'
40
+ | 'invalid_payment'
41
+ | 'insufficient_funds'
42
+ | 'policy_denied'
43
+ | 'screen_rejected'
44
+ | 'simulation_failed'
45
+ | 'temporarily_unavailable'
46
+ | 'unsupported'
47
+ | 'unknown'
48
+ >()
49
+ expectTypeOf<serverTempo.RelayErrorDetails>().toEqualTypeOf<
50
+ | { code: 'already_used' | 'broadcast_failed' | 'insufficient_funds' | 'invalid_payment' }
51
+ | { code: 'simulation_failed' | 'unsupported' }
52
+ | { code: 'temporarily_unavailable'; retry: 'same_credential' }
53
+ >()
34
54
 
35
55
  expectTypeOf<ClientPaymentResponse>().toEqualTypeOf<SessionPaymentResponse>()
36
56
  expectTypeOf<ClientSessionManager>().toEqualTypeOf<SessionManager>()
@@ -636,14 +636,13 @@ export function assertTransactionPolicy(parameters: {
636
636
  maxFeePerGas: maxFeePerGasValue.toString(),
637
637
  })
638
638
 
639
- const maxTotalFee = gasLimit * maxFeePerGasValue
639
+ const maxTotalFee = BigInt(gasLimit) * BigInt(maxFeePerGasValue)
640
640
  if (maxTotalFee > policy.maxTotalFee)
641
641
  fail('fee-sponsored transaction total fee budget exceeds sponsor policy', {
642
642
  gas: gasLimit.toString(),
643
643
  maxFeePerGas: maxFeePerGasValue.toString(),
644
644
  totalFee: maxTotalFee.toString(),
645
645
  })
646
-
647
646
  if (maxPriorityFeePerGas !== undefined && maxPriorityFeePerGas > maxFeePerGasValue)
648
647
  fail('fee-sponsored transaction maxPriorityFeePerGas exceeds maxFeePerGas', {
649
648
  maxFeePerGas: maxFeePerGasValue.toString(),
@@ -677,7 +676,12 @@ export function assertTransactionPolicy(parameters: {
677
676
  validBefore: String(validBeforeValue),
678
677
  })
679
678
 
680
- return { gasLimit, maxFeePerGasValue, validBeforeValue }
679
+ return {
680
+ gasLimit,
681
+ maxFeePerGasValue,
682
+ totalFee: maxTotalFee,
683
+ validBeforeValue,
684
+ }
681
685
  }
682
686
 
683
687
  export function prepareSponsoredTransaction(parameters: {
@@ -245,6 +245,13 @@ describe.runIf(isLocalnet)('createOpenPayload', () => {
245
245
  expect(result.payload).toHaveProperty('transaction')
246
246
  expect(result.payload).toHaveProperty('signature')
247
247
  expect(result.payload.channelId).toBe(result.entry.channelId)
248
+
249
+ if (result.payload.action !== 'open') throw new Error('unexpected action')
250
+ const transaction = Transaction.deserialize(result.payload.transaction)
251
+ if (!('nonceKey' in transaction)) throw new Error('unexpected transaction type')
252
+ expect(transaction.nonce).toBe(0)
253
+ expect(transaction.nonceKey).toBe((1n << 256n) - 1n)
254
+ expect((transaction as { validBefore?: bigint }).validBefore).toBeDefined()
248
255
  })
249
256
 
250
257
  test('defaults authorizedSigner to account.address', async () => {
@@ -192,7 +192,8 @@ export async function createOpenPayload(
192
192
  { to: escrowContract, data: openData },
193
193
  ],
194
194
  feeToken: currency,
195
- ...(feePayer ? { nonceKey: 'expiring', validBefore } : {}),
195
+ nonceKey: 'expiring',
196
+ validBefore,
196
197
  } as never)
197
198
  // Estimate before enabling fee-payer mode so Tempo includes sender
198
199
  // signature and access-key verification costs in the gas budget.