mppx 0.8.8 → 0.8.11

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 (118) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/dist/cli/validate/challenge.d.ts +2 -0
  3. package/dist/cli/validate/challenge.d.ts.map +1 -1
  4. package/dist/cli/validate/challenge.js +21 -9
  5. package/dist/cli/validate/challenge.js.map +1 -1
  6. package/dist/cli/validate/discovery.d.ts.map +1 -1
  7. package/dist/cli/validate/discovery.js +50 -19
  8. package/dist/cli/validate/discovery.js.map +1 -1
  9. package/dist/cli/validate/helpers.d.ts +1 -0
  10. package/dist/cli/validate/helpers.d.ts.map +1 -1
  11. package/dist/cli/validate/helpers.js +11 -0
  12. package/dist/cli/validate/helpers.js.map +1 -1
  13. package/dist/cli/validate/index.d.ts +1 -0
  14. package/dist/cli/validate/index.d.ts.map +1 -1
  15. package/dist/cli/validate/index.js +4 -0
  16. package/dist/cli/validate/index.js.map +1 -1
  17. package/dist/cli/validate/payment.d.ts +1 -0
  18. package/dist/cli/validate/payment.d.ts.map +1 -1
  19. package/dist/cli/validate/payment.js +2 -2
  20. package/dist/cli/validate/payment.js.map +1 -1
  21. package/dist/tempo/internal/fee-payer.d.ts +39 -2
  22. package/dist/tempo/internal/fee-payer.d.ts.map +1 -1
  23. package/dist/tempo/internal/fee-payer.js +49 -2
  24. package/dist/tempo/internal/fee-payer.js.map +1 -1
  25. package/dist/tempo/internal/fee-token.d.ts +8 -0
  26. package/dist/tempo/internal/fee-token.d.ts.map +1 -1
  27. package/dist/tempo/internal/fee-token.js +19 -6
  28. package/dist/tempo/internal/fee-token.js.map +1 -1
  29. package/dist/tempo/legacy/session/Chain.d.ts.map +1 -1
  30. package/dist/tempo/legacy/session/Chain.js +53 -50
  31. package/dist/tempo/legacy/session/Chain.js.map +1 -1
  32. package/dist/tempo/server/Charge.d.ts +8 -0
  33. package/dist/tempo/server/Charge.d.ts.map +1 -1
  34. package/dist/tempo/server/Charge.js +49 -45
  35. package/dist/tempo/server/Charge.js.map +1 -1
  36. package/dist/tempo/server/Methods.d.ts +7 -0
  37. package/dist/tempo/server/Methods.d.ts.map +1 -1
  38. package/dist/tempo/server/Methods.js.map +1 -1
  39. package/dist/tempo/server/Subscription.d.ts.map +1 -1
  40. package/dist/tempo/server/Subscription.js +24 -16
  41. package/dist/tempo/server/Subscription.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/dist/tempo/session/client/SessionManager.d.ts +3 -0
  47. package/dist/tempo/session/client/SessionManager.d.ts.map +1 -1
  48. package/dist/tempo/session/client/SessionManager.js +1 -0
  49. package/dist/tempo/session/client/SessionManager.js.map +1 -1
  50. package/dist/tempo/session/client/Transports.d.ts +20 -0
  51. package/dist/tempo/session/client/Transports.d.ts.map +1 -1
  52. package/dist/tempo/session/client/Transports.js +23 -11
  53. package/dist/tempo/session/client/Transports.js.map +1 -1
  54. package/dist/tempo/session/precompile/Chain.d.ts +6 -6
  55. package/dist/tempo/session/precompile/Chain.d.ts.map +1 -1
  56. package/dist/tempo/session/precompile/Chain.js +29 -19
  57. package/dist/tempo/session/precompile/Chain.js.map +1 -1
  58. package/dist/tempo/session/server/CredentialVerification.d.ts +2 -2
  59. package/dist/tempo/session/server/CredentialVerification.d.ts.map +1 -1
  60. package/dist/tempo/session/server/CredentialVerification.js +1 -1
  61. package/dist/tempo/session/server/CredentialVerification.js.map +1 -1
  62. package/dist/tempo/session/server/RequestState.d.ts +4 -4
  63. package/dist/tempo/session/server/RequestState.d.ts.map +1 -1
  64. package/dist/tempo/session/server/RequestState.js.map +1 -1
  65. package/dist/tempo/session/server/Session.d.ts.map +1 -1
  66. package/dist/tempo/session/server/Session.js +6 -3
  67. package/dist/tempo/session/server/Session.js.map +1 -1
  68. package/dist/tempo/session/server/Settlement.d.ts +5 -3
  69. package/dist/tempo/session/server/Settlement.d.ts.map +1 -1
  70. package/dist/tempo/session/server/Settlement.js +9 -2
  71. package/dist/tempo/session/server/Settlement.js.map +1 -1
  72. package/dist/tempo/session/server/index.d.ts +2 -0
  73. package/dist/tempo/session/server/index.d.ts.map +1 -1
  74. package/dist/tempo/session/server/index.js +2 -0
  75. package/dist/tempo/session/server/index.js.map +1 -1
  76. package/dist/validation/core.d.ts +1 -0
  77. package/dist/validation/core.d.ts.map +1 -1
  78. package/dist/validation/core.js +5 -0
  79. package/dist/validation/core.js.map +1 -1
  80. package/dist/x402/Header.js +1 -1
  81. package/dist/x402/Header.js.map +1 -1
  82. package/package.json +1 -1
  83. package/src/cli/validate/challenge.ts +20 -8
  84. package/src/cli/validate/discovery.test.ts +128 -0
  85. package/src/cli/validate/discovery.ts +50 -15
  86. package/src/cli/validate/helpers.ts +10 -0
  87. package/src/cli/validate/index.ts +4 -0
  88. package/src/cli/validate/payment.ts +4 -1
  89. package/src/cli/validate.test.ts +23 -2
  90. package/src/client/Mppx.test-d.ts +3 -1
  91. package/src/tempo/PublicExports.test-d.ts +5 -0
  92. package/src/tempo/internal/fee-payer.test.ts +97 -1
  93. package/src/tempo/internal/fee-payer.ts +68 -3
  94. package/src/tempo/internal/fee-token.test.ts +37 -0
  95. package/src/tempo/internal/fee-token.ts +26 -5
  96. package/src/tempo/legacy/session/Chain.test.ts +78 -2
  97. package/src/tempo/legacy/session/Chain.ts +56 -52
  98. package/src/tempo/server/Charge.test.ts +121 -6
  99. package/src/tempo/server/Charge.ts +61 -48
  100. package/src/tempo/server/Methods.ts +6 -0
  101. package/src/tempo/server/Subscription.test.ts +79 -4
  102. package/src/tempo/server/Subscription.ts +24 -16
  103. package/src/tempo/server/internal/html.gen.ts +1 -1
  104. package/src/tempo/session/client/SessionManager.test.ts +12 -1
  105. package/src/tempo/session/client/SessionManager.ts +3 -0
  106. package/src/tempo/session/client/Transports.test.ts +34 -0
  107. package/src/tempo/session/client/Transports.ts +47 -20
  108. package/src/tempo/session/precompile/Chain.test.ts +51 -0
  109. package/src/tempo/session/precompile/Chain.ts +39 -26
  110. package/src/tempo/session/server/CredentialVerification.ts +3 -3
  111. package/src/tempo/session/server/RequestState.ts +4 -3
  112. package/src/tempo/session/server/Session.test.ts +23 -3
  113. package/src/tempo/session/server/Session.ts +6 -3
  114. package/src/tempo/session/server/Settlement.test.ts +16 -0
  115. package/src/tempo/session/server/Settlement.ts +16 -5
  116. package/src/tempo/session/server/index.ts +2 -0
  117. package/src/validation/core.ts +6 -0
  118. package/src/x402/Header.ts +1 -1
@@ -329,9 +329,13 @@ describe('Session', () => {
329
329
  expect(posted[0]).toMatchObject({ action: 'voucher', channelId: storedChannelId })
330
330
  })
331
331
 
332
- test('seeds a same-route HEAD snapshot into the entry index and resumes it', async () => {
332
+ test('seeds a same-route HEAD snapshot and resolves the account when resuming it', async () => {
333
333
  const { store, set } = makeChannelStore()
334
334
  const posted: SessionCredentialPayload[] = []
335
+ const resolveAccount = vi.fn(
336
+ (info: Parameters<NonNullable<sessionManager.Parameters['resolveAccount']>>[0]) =>
337
+ info.account,
338
+ )
335
339
  const mockFetch = vi.fn().mockImplementation((_input, init?: RequestInit) => {
336
340
  const headers = new Headers(init?.headers)
337
341
  if (init?.method === 'HEAD' && !headers.get(Constants.Headers.authorization)) {
@@ -377,6 +381,7 @@ describe('Session', () => {
377
381
  client,
378
382
  fetch: mockFetch as typeof globalThis.fetch,
379
383
  channelStore: store,
384
+ resolveAccount,
380
385
  })
381
386
 
382
387
  const response = await s.fetch('https://api.example.com/data')
@@ -384,6 +389,12 @@ describe('Session', () => {
384
389
  expect(response.status).toBe(200)
385
390
  expect(set).toHaveBeenCalledWith(expect.objectContaining({ channelId: storedChannelId }))
386
391
  expect(posted[0]).toMatchObject({ action: 'voucher', channelId: storedChannelId })
392
+ expect(resolveAccount).toHaveBeenCalledOnce()
393
+ expect(resolveAccount).toHaveBeenCalledWith({
394
+ account,
395
+ chainId: 4217,
396
+ operation: { authority: account.address, kind: 'authorizePaymentChannel' },
397
+ })
387
398
  const contentCall = mockFetch.mock.calls.find((call) => call[1]?.method !== 'HEAD')
388
399
  expect(new Headers(contentCall?.[1]?.headers).get('Payment-Session')).toBeNull()
389
400
  })
@@ -274,6 +274,7 @@ export function sessionManager(parameters: sessionManager.Parameters): SessionMa
274
274
  const method = sessionPlugin({
275
275
  account: parameters.account,
276
276
  getClient: parameters.client ? () => parameters.client! : parameters.getClient,
277
+ resolveAccount: parameters.resolveAccount,
277
278
  escrow: parameters.escrow,
278
279
  decimals: config.decimals,
279
280
  maxDeposit: parameters.maxDeposit,
@@ -787,6 +788,8 @@ export namespace sessionManager {
787
788
  fetch?: typeof globalThis.fetch | undefined
788
789
  /** Maximum deposit in human-readable units (e.g. `'10'` for 10 tokens). Converted to raw units via `decimals`. */
789
790
  maxDeposit?: string | undefined
791
+ /** Selects the account that signs session credentials. */
792
+ resolveAccount?: sessionPlugin.ResolveAccount | undefined
790
793
  /** Store for reusable session channels. Defaults to in-memory. */
791
794
  channelStore?: ChannelStore | undefined
792
795
  /** Optional websocket constructor for runtimes without a global WebSocket. */
@@ -19,6 +19,7 @@ import {
19
19
  applyTopUpResult,
20
20
  closeHttpSession,
21
21
  createActiveSocketSession,
22
+ driveSseResponse,
22
23
  isExpectedSocketReceipt,
23
24
  managementInput,
24
25
  postTopUp,
@@ -640,6 +641,39 @@ describe('VoucherManagement', () => {
640
641
  })
641
642
  })
642
643
 
644
+ describe('SseDriver', () => {
645
+ test('preserves application frames and consumes payment receipts', async () => {
646
+ const receipt = createSessionReceipt({
647
+ acceptedCumulative: 2n,
648
+ challengeId: 'challenge-1',
649
+ channelId: `0x${'01'.repeat(32)}`,
650
+ spent: 2n,
651
+ })
652
+ const acceptReceipt = vi.fn()
653
+ const response = new Response(
654
+ [
655
+ ': keepalive\n\n',
656
+ 'event: custom\ndata: hello\n\n',
657
+ `event: payment-receipt\ndata: ${JSON.stringify(receipt)}\n\n`,
658
+ ].join(''),
659
+ )
660
+
661
+ const frames = []
662
+ for await (const frame of driveSseResponse({
663
+ async onNeedVoucher() {},
664
+ onReceipt: acceptReceipt,
665
+ response,
666
+ }))
667
+ frames.push(frame)
668
+
669
+ expect(frames).toEqual([
670
+ { raw: ': keepalive\n\n' },
671
+ { data: 'hello', raw: 'event: custom\ndata: hello\n\n' },
672
+ ])
673
+ expect(acceptReceipt).toHaveBeenCalledWith(receipt)
674
+ })
675
+ })
676
+
643
677
  describe('WsDriver', () => {
644
678
  const channelId = '0x0000000000000000000000000000000000000000000000000000000000000001' as Hex.Hex
645
679
  const challenge: TempoSessionChallenge = {
@@ -777,29 +777,49 @@ export async function openSseSession(
777
777
  if (!isEventStream(response)) throw new Error('SSE response is not an event stream.')
778
778
  if (!response.body) throw new Error('Response has no body.')
779
779
 
780
- return iterateSseResponse({
781
- challenge,
782
- driver,
783
- input,
784
- onReceipt,
780
+ return iterateSseMessages({
781
+ onNeedVoucher: (event) => handleSseNeedVoucher({ challenge, driver, input }, event),
782
+ onReceipt(receipt) {
783
+ driver.acceptReceipt(receipt)
784
+ onReceipt?.(receipt)
785
+ },
785
786
  response,
786
787
  signal,
787
788
  })
788
789
  }
789
790
 
790
- type IterateSseResponseParameters = {
791
- challenge: TempoSessionChallenge | null
792
- driver: OpenSseSessionParameters
793
- input: RequestInfo | URL
794
- onReceipt?: ((receipt: SessionReceipt) => void) | undefined
791
+ /** Application SSE frame emitted after session payment events are handled. */
792
+ export type SseResponseFrame = {
793
+ /** Parsed application data, when present. */
794
+ data?: string | undefined
795
+ /** Original frame text, including its separator. */
796
+ raw: string
797
+ }
798
+
799
+ /** Inputs for driving an open paid SSE response. */
800
+ export type DriveSseResponseParameters = {
801
+ /** Handles a request for the stream's next cumulative voucher. */
802
+ onNeedVoucher(event: NeedVoucherEvent): Promise<void>
803
+ /** Handles a payment receipt emitted by the stream. */
804
+ onReceipt(receipt: SessionReceipt): void
805
+ /** Open SSE response to drive. */
795
806
  response: Response
807
+ /** Abort signal used to stop reading the response. */
796
808
  signal?: AbortSignal | undefined
797
809
  }
798
810
 
799
- async function* iterateSseResponse(
800
- parameters: IterateSseResponseParameters,
801
- ): AsyncGenerator<string> {
802
- const reader = parameters.response.body!.getReader()
811
+ async function* iterateSseMessages(parameters: DriveSseResponseParameters): AsyncGenerator<string> {
812
+ for await (const frame of driveSseResponse(parameters)) {
813
+ if (frame.data !== undefined) yield frame.data
814
+ }
815
+ }
816
+
817
+ /** Handles session payment events and yields application SSE frames unchanged. */
818
+ export async function* driveSseResponse(
819
+ parameters: DriveSseResponseParameters,
820
+ ): AsyncGenerator<SseResponseFrame> {
821
+ if (!parameters.response.body) throw new Error('Response has no body.')
822
+ const reader = parameters.response.body.getReader()
803
823
  const decoder = new TextDecoder()
804
824
  let buffer = ''
805
825
 
@@ -817,18 +837,21 @@ async function* iterateSseResponse(
817
837
  for (const part of parts) {
818
838
  if (!part.trim()) continue
819
839
  const event = parseEvent(part)
820
- if (!event) continue
840
+ const raw = `${part}\n\n`
841
+ if (!event) {
842
+ yield { raw }
843
+ continue
844
+ }
821
845
 
822
846
  switch (event.type) {
823
847
  case 'message':
824
- yield event.data
848
+ yield { data: event.data, raw }
825
849
  break
826
850
  case 'payment-need-voucher':
827
- await handleSseNeedVoucher(parameters, event.data)
851
+ await parameters.onNeedVoucher(event.data)
828
852
  break
829
853
  case 'payment-receipt':
830
- parameters.driver.acceptReceipt(event.data)
831
- parameters.onReceipt?.(event.data)
854
+ parameters.onReceipt(event.data)
832
855
  break
833
856
  }
834
857
  }
@@ -839,7 +862,11 @@ async function* iterateSseResponse(
839
862
  }
840
863
 
841
864
  async function handleSseNeedVoucher(
842
- parameters: IterateSseResponseParameters,
865
+ parameters: {
866
+ challenge: TempoSessionChallenge | null
867
+ driver: OpenSseSessionParameters
868
+ input: RequestInfo | URL
869
+ },
843
870
  event: NeedVoucherEvent,
844
871
  ) {
845
872
  const channel = parameters.driver.getChannel()
@@ -611,6 +611,54 @@ describe('precompile broadcastOpenTransaction', () => {
611
611
  expect(broadcastIndex).toBeGreaterThan(-1)
612
612
  expect(simulationIndex).toBeGreaterThan(-1)
613
613
  expect(simulationIndex).toBeLessThan(broadcastIndex)
614
+ expect(
615
+ rpcMethods.slice(0, broadcastIndex).filter((method) => method === 'eth_call'),
616
+ ).toHaveLength(2)
617
+ })
618
+
619
+ test('hosted fee-payer relays a sender-signed open without local co-signing', async () => {
620
+ const rpcMethods: string[] = []
621
+ const serializedTransaction = await createOpenTransaction({ signed: true })
622
+ const transaction = Transaction.deserialize(
623
+ serializedTransaction as Transaction.TransactionSerializedTempo,
624
+ )
625
+ const payer = transaction.from!
626
+ const expiringNonceHash = Channel.computeExpiringNonceHash(
627
+ Channel.transactionForExpiringNonceHash({ feePayer: true, transaction }),
628
+ { sender: payer },
629
+ )
630
+ const expectedDescriptor = { ...descriptor, payer, expiringNonceHash }
631
+ const channelId = Channel.computeId({
632
+ ...expectedDescriptor,
633
+ chainId,
634
+ escrow: tip20ChannelEscrow,
635
+ })
636
+ const state = { settled: 0n, deposit, closeRequestedAt: 0 }
637
+
638
+ await Chain.broadcastOpenTransaction({
639
+ chainId,
640
+ client: createMockClient({
641
+ channel: { descriptor: expectedDescriptor, state },
642
+ receipt: receipt([openedLog({ channelId, expiringNonceHash })]),
643
+ rpcMethods,
644
+ }),
645
+ escrowContract: tip20ChannelEscrow,
646
+ expectedAuthorizedSigner: descriptor.authorizedSigner,
647
+ expectedChannelId: channelId,
648
+ expectedCurrency: descriptor.token,
649
+ expectedExpiringNonceHash: expiringNonceHash,
650
+ expectedOperator: descriptor.operator,
651
+ expectedPayee: descriptor.payee,
652
+ expectedPayer: payer,
653
+ feePayer: true,
654
+ serializedTransaction,
655
+ })
656
+
657
+ expect(rpcMethods).toContain('eth_sendRawTransaction')
658
+ expect(rpcMethods).not.toContain('eth_sendRawTransactionSync')
659
+ expect(rpcMethods.indexOf('eth_call')).toBeLessThan(
660
+ rpcMethods.indexOf('eth_sendRawTransaction'),
661
+ )
614
662
  })
615
663
 
616
664
  test('rejects expiring nonce hash mismatches before broadcasting', async () => {
@@ -929,6 +977,9 @@ describe('precompile broadcastTopUpTransaction', () => {
929
977
  expect(broadcastIndex).toBeGreaterThan(-1)
930
978
  expect(simulationIndex).toBeGreaterThan(-1)
931
979
  expect(simulationIndex).toBeLessThan(broadcastIndex)
980
+ expect(
981
+ rpcMethods.slice(0, broadcastIndex).filter((method) => method === 'eth_call'),
982
+ ).toHaveLength(2)
932
983
  })
933
984
 
934
985
  test('rejects top-up calldata amount mismatches before broadcasting', async () => {
@@ -406,7 +406,7 @@ type ParsedPrecompileCredentialTransaction = {
406
406
 
407
407
  function parsePrecompileCredentialTransaction(parameters: {
408
408
  escrowContract: Address
409
- feePayer?: Account | undefined
409
+ feePayer?: Account | true | undefined
410
410
  label: 'open' | 'topUp'
411
411
  serializedTransaction: Hex
412
412
  }): ParsedPrecompileCredentialTransaction {
@@ -434,15 +434,10 @@ function parsePrecompileCredentialTransaction(parameters: {
434
434
  return { transaction, call: { ...call, data: call.data, to: call.to } }
435
435
  }
436
436
 
437
- async function simulateTempoTransaction(client: Client, transaction: Transaction.TransactionTempo) {
437
+ async function simulateTempoTransaction(client: Client, request: unknown) {
438
438
  // viem's public `call` type does not yet model Tempo's multi-call and
439
439
  // fee-payer fields together. Keep that compatibility cast in one place.
440
- await call(client, {
441
- ...transaction,
442
- account: transaction.from,
443
- calls: transaction.calls ?? [],
444
- feePayerSignature: undefined,
445
- } as never)
440
+ await call(client, request as never)
446
441
  }
447
442
 
448
443
  async function signTempoTransaction(client: Client, transaction: unknown): Promise<Hex> {
@@ -668,8 +663,8 @@ export type SendCredentialTransactionParameters = {
668
663
  allowedFeeTokens?: readonly Address[] | undefined
669
664
  /** Human-readable transaction details used by fee-payer policy hooks. */
670
665
  details: Record<string, string>
671
- /** Fee-payer account used to co-sign Tempo transactions. */
672
- feePayer?: Account | undefined
666
+ /** Fee-payer account, or `true` when the client transport delegates co-signing to a hosted relay. */
667
+ feePayer?: Account | true | undefined
673
668
  /** Optional fee-payer policy enforced before co-signing. */
674
669
  feePayerPolicy?: Partial<FeePayer.Policy> | undefined
675
670
  /** Management transaction kind, used for validation errors. */
@@ -704,21 +699,39 @@ export async function sendCredentialTransaction(parameters: SendCredentialTransa
704
699
  throw new BadRequestError({ reason: 'Only Tempo (0x76/0x78) transactions are supported' })
705
700
  assertSenderSigned(transaction)
706
701
 
707
- await simulateTempoTransaction(client, transaction)
702
+ if (feePayer === true) {
703
+ // The transport owns hosted completion, so mppx can only preflight call
704
+ // execution as the sender. It intentionally omits fee fields here; the
705
+ // transport is responsible for validating its final sponsored envelope.
706
+ await simulateTempoTransaction(
707
+ client,
708
+ FeePayer.simulationTransaction(transaction, { feePayer: true }),
709
+ )
710
+ const txHash = await sendTransaction(client, serializedTransaction)
711
+ return waitForSuccessfulReceipt(client, txHash)
712
+ }
708
713
 
709
- const sponsored = FeePayer.prepareSponsoredTransaction({
710
- account: feePayer,
711
- allowedFeeTokens,
712
- challengeExpires,
713
- chainId,
714
- details,
715
- policy: feePayerPolicy,
716
- transaction: {
717
- ...transaction,
718
- ...(allowedFeeTokens?.[0] ? { feeToken: transaction.feeToken ?? allowedFeeTokens[0] } : {}),
714
+ const sponsorshipTransaction = {
715
+ ...transaction,
716
+ ...(allowedFeeTokens?.[0] ? { feeToken: transaction.feeToken ?? allowedFeeTokens[0] } : {}),
717
+ }
718
+ const completed = await FeePayer.preflightSponsorship({
719
+ transaction: sponsorshipTransaction,
720
+ simulate: (request) => simulateTempoTransaction(client, request),
721
+ async complete() {
722
+ const sponsored = FeePayer.prepareSponsoredTransaction({
723
+ account: feePayer,
724
+ allowedFeeTokens,
725
+ challengeExpires,
726
+ chainId,
727
+ details,
728
+ policy: feePayerPolicy,
729
+ transaction: sponsorshipTransaction,
730
+ })
731
+ return { feePayer: feePayer.address, transaction: sponsored }
719
732
  },
720
733
  })
721
- const serialized = await signTempoTransaction(client, sponsored)
734
+ const serialized = await signTempoTransaction(client, completed.transaction)
722
735
  const receipt = await sendRawTransactionSync(client, {
723
736
  serializedTransaction: serialized as Transaction.TransactionSerializedTempo,
724
737
  })
@@ -771,8 +784,8 @@ export type BroadcastOpenTransactionParameters = {
771
784
  expectedPayee: Address
772
785
  /** Payer expected to have signed the open transaction. */
773
786
  expectedPayer: Address
774
- /** Fee-payer account used to co-sign sponsored open transactions. */
775
- feePayer?: Account | undefined
787
+ /** Fee-payer account, or `true` when the client transport delegates co-signing to a hosted relay. */
788
+ feePayer?: Account | true | undefined
776
789
  /** Optional fee-payer policy enforced before co-signing. */
777
790
  feePayerPolicy?: Partial<FeePayer.Policy> | undefined
778
791
  /** Client-signed serialized open transaction. */
@@ -895,8 +908,8 @@ export type BroadcastTopUpTransactionParameters = {
895
908
  expectedChannelId: Hex
896
909
  /** Payment token expected for sponsored transaction fee token checks. */
897
910
  expectedCurrency: Address
898
- /** Fee-payer account used to co-sign sponsored top-up transactions. */
899
- feePayer?: Account | undefined
911
+ /** Fee-payer account, or `true` when the client transport delegates co-signing to a hosted relay. */
912
+ feePayer?: Account | true | undefined
900
913
  /** Optional fee-payer policy enforced before co-signing. */
901
914
  feePayerPolicy?: Partial<FeePayer.Policy> | undefined
902
915
  /** Client-signed serialized top-up transaction. */
@@ -334,8 +334,8 @@ export type VerifyCredentialPayloadParameters = {
334
334
  escrow: Address
335
335
  /** Operator address advertised in the HMAC-bound challenge details. */
336
336
  expectedOperator?: Address | undefined
337
- /** Optional fee-payer account for fee-sponsored management transactions. */
338
- feePayer?: viem_Account | undefined
337
+ /** Fee-payer account, or `true` when the client transport delegates co-signing to a hosted relay. */
338
+ feePayer?: viem_Account | true | undefined
339
339
  /** Optional policy for fee-sponsored close/open/top-up transactions. */
340
340
  feePayerPolicy?: Partial<FeePayer.Policy> | undefined
341
341
  /** Optional fee token override for close transactions. */
@@ -714,7 +714,7 @@ async function handleCloseCredential(
714
714
  account
715
715
  ? {
716
716
  account,
717
- ...(parameters.feePayer ? { feePayer: parameters.feePayer } : {}),
717
+ ...(typeof parameters.feePayer === 'object' ? { feePayer: parameters.feePayer } : {}),
718
718
  ...(parameters.feePayerPolicy ? { feePayerPolicy: parameters.feePayerPolicy } : {}),
719
719
  ...(parameters.feeToken ? { feeToken: parameters.feeToken } : {}),
720
720
  candidateFeeTokens: [channel.token],
@@ -27,6 +27,7 @@ import {
27
27
  resolveCredentialFeePayer,
28
28
  resolveRequestFeePayer,
29
29
  type ParameterFeePayer,
30
+ type ResolvedFeePayer,
30
31
  } from './Settlement.js'
31
32
 
32
33
  /** Inputs used to build server bootstrap hints for a reusable session channel. */
@@ -267,8 +268,8 @@ export type ResolveSessionPaymentRequestParameters = {
267
268
 
268
269
  /** Inputs used to resolve shared context for credential verification. */
269
270
  export type ResolveCredentialVerificationContextParameters = {
270
- /** Default configured fee-payer account, when enabled. */
271
- feePayer?: viem_Account | undefined
271
+ /** Configured local fee payer, hosted relay URL, or sponsorship flag. */
272
+ feePayer?: ParameterFeePayer
272
273
  /** Resolves a viem client for the challenge chain ID. */
273
274
  getClient(parameters: {
274
275
  chainId: number
@@ -294,7 +295,7 @@ export type CredentialVerificationContext = {
294
295
  /** Client for precompile reads and transaction broadcasts. */
295
296
  client: PrecompileChain.TransactionClient
296
297
  /** Fee payer authorized for this credential, when allowed. */
297
- feePayer?: viem_Account | undefined
298
+ feePayer?: ResolvedFeePayer
298
299
  /** Minimum allowed voucher delta in raw token units. */
299
300
  minVoucherDelta: bigint
300
301
  }
@@ -2257,6 +2257,7 @@ describe('precompile server session unit guardrails', () => {
2257
2257
  action: SessionCredentialPayload['action']
2258
2258
  contentLength: string | null
2259
2259
  hasBody: boolean
2260
+ hasReceipt: boolean
2260
2261
  status: number
2261
2262
  }> = []
2262
2263
  const app = new Hono()
@@ -2271,6 +2272,7 @@ describe('precompile server session unit guardrails', () => {
2271
2272
  action: payload.action,
2272
2273
  contentLength: request.headers.get('content-length'),
2273
2274
  hasBody: request.body !== null,
2275
+ hasReceipt: response.headers.has(Constants.Headers.paymentReceipt),
2274
2276
  status: response.status,
2275
2277
  })
2276
2278
  return response
@@ -2302,9 +2304,27 @@ describe('precompile server session unit guardrails', () => {
2302
2304
  expect(closeReceipt?.spent).toBe('3')
2303
2305
  expect(managementPosts).toEqual(
2304
2306
  expect.arrayContaining([
2305
- { action: 'topUp', contentLength: '0', hasBody: true, status: 204 },
2306
- { action: 'voucher', contentLength: '0', hasBody: true, status: 204 },
2307
- { action: 'close', contentLength: '0', hasBody: true, status: 204 },
2307
+ {
2308
+ action: 'topUp',
2309
+ contentLength: '0',
2310
+ hasBody: true,
2311
+ hasReceipt: true,
2312
+ status: 204,
2313
+ },
2314
+ {
2315
+ action: 'voucher',
2316
+ contentLength: '0',
2317
+ hasBody: true,
2318
+ hasReceipt: true,
2319
+ status: 204,
2320
+ },
2321
+ {
2322
+ action: 'close',
2323
+ contentLength: '0',
2324
+ hasBody: true,
2325
+ hasReceipt: true,
2326
+ status: 204,
2327
+ },
2308
2328
  ]),
2309
2329
  )
2310
2330
  expect(managementPosts.every(({ status }) => status === 204)).toBe(true)
@@ -315,9 +315,11 @@ export function session<const parameters extends session.Parameters>(
315
315
 
316
316
  const store = ChannelStore.fromStore(rawStore)
317
317
  const lastOnChainVerified = new Map<Hex, number>()
318
- const { account, feePayer, recipient } = Account.resolve(parameters)
318
+ const { account, feePayer, feePayerUrl, recipient } = Account.resolve(parameters)
319
+ const configuredFeePayer = feePayer ?? feePayerUrl
319
320
  const getClient = Client.getResolver({
320
321
  chain: tempo_chain,
322
+ feePayerUrl,
321
323
  getClient: parameters.getClient,
322
324
  rpcUrl: defaults.rpcUrl,
323
325
  })
@@ -326,6 +328,7 @@ export function session<const parameters extends session.Parameters>(
326
328
  chainId: parameters.chainId,
327
329
  currency,
328
330
  decimals,
331
+ feePayer: parameters.feePayer,
329
332
  getClient: parameters.getClient,
330
333
  recipient,
331
334
  store: rawStore,
@@ -401,7 +404,7 @@ export function session<const parameters extends session.Parameters>(
401
404
  const payload = requireSessionCredentialPayload(credential.payload)
402
405
  const context = await resolveCredentialVerificationContext({
403
406
  decimals,
404
- feePayer,
407
+ feePayer: configuredFeePayer,
405
408
  getClient,
406
409
  minVoucherDelta: parameters.minVoucherDelta,
407
410
  request,
@@ -438,7 +441,7 @@ export function session<const parameters extends session.Parameters>(
438
441
  maybeSettleScheduled({
439
442
  account,
440
443
  client: context.client,
441
- feePayer: context.feePayer,
444
+ ...(typeof context.feePayer === 'object' ? { feePayer: context.feePayer } : {}),
442
445
  feePayerPolicy: parameters.feePayerPolicy,
443
446
  feeToken: parameters.feeToken,
444
447
  schedule: settlementSchedule,
@@ -23,6 +23,7 @@ describe('FeePayerResolution', () => {
23
23
  const requestFeePayer = privateKeyToAccount(
24
24
  '0x5de4111a56d1f1ad3c74c8a3be6fba32114d0f9f8e9e4b0d4d4f5a7833f1b6c9',
25
25
  )
26
+ const feePayerUrl = 'https://fee-payer.example/relay'
26
27
 
27
28
  function credential(): Credential.Credential {
28
29
  return {
@@ -60,6 +61,13 @@ describe('FeePayerResolution', () => {
60
61
  parameterFeePayer: true,
61
62
  }),
62
63
  ).toBe(true)
64
+
65
+ expect(
66
+ resolveRequestFeePayer({
67
+ credential: null,
68
+ parameterFeePayer: feePayerUrl,
69
+ }),
70
+ ).toBe(true)
63
71
  })
64
72
 
65
73
  test('does not advertise fee-payer support when request disables it', () => {
@@ -106,6 +114,14 @@ describe('FeePayerResolution', () => {
106
114
  }),
107
115
  ).toBe(defaultFeePayer)
108
116
 
117
+ expect(
118
+ resolveCredentialFeePayer({
119
+ feePayer: feePayerUrl,
120
+ methodDetails: { feePayer: true },
121
+ request: { feePayer: true },
122
+ }),
123
+ ).toBe(true)
124
+
109
125
  expect(
110
126
  resolveCredentialFeePayer({
111
127
  feePayer: defaultFeePayer,
@@ -32,6 +32,9 @@ import * as ChannelStore from './ChannelStore.js'
32
32
  /** Fee-payer parameter accepted by the server session method. */
33
33
  export type ParameterFeePayer = viem_Account | string | true | undefined
34
34
 
35
+ /** Resolved fee-payer mode for credential-time transaction submission. */
36
+ export type ResolvedFeePayer = viem_Account | true | undefined
37
+
35
38
  /** Minimum method details needed to decide credential-time fee sponsorship. */
36
39
  export type CredentialFeePayerMethodDetails = {
37
40
  /** Whether the challenge advertised fee-payer support. */
@@ -56,8 +59,8 @@ export type ResolveCredentialFeePayerParameters = {
56
59
  request: unknown
57
60
  /** Challenge method details echoed by the credential. */
58
61
  methodDetails: CredentialFeePayerMethodDetails
59
- /** Default fee-payer account resolved from server parameters. */
60
- feePayer?: viem_Account | undefined
62
+ /** Configured local fee payer, hosted relay URL, or sponsorship flag. */
63
+ feePayer?: ParameterFeePayer
61
64
  }
62
65
 
63
66
  /** Fee-payer value read from an untrusted credential challenge request. */
@@ -89,20 +92,28 @@ export function resolveRequestFeePayer(
89
92
 
90
93
  const account = typeof requestFeePayer === 'object' ? requestFeePayer : defaultFeePayer
91
94
  if (credential) return account ?? undefined
92
- if (account || defaultFeePayer || parameterFeePayer === true) return true
95
+ if (
96
+ account ||
97
+ defaultFeePayer ||
98
+ parameterFeePayer === true ||
99
+ typeof parameterFeePayer === 'string'
100
+ )
101
+ return true
93
102
  return undefined
94
103
  }
95
104
 
96
105
  /** Resolves the fee-payer account allowed for an incoming credential. */
97
106
  export function resolveCredentialFeePayer(
98
107
  parameters: ResolveCredentialFeePayerParameters,
99
- ): viem_Account | undefined {
108
+ ): ResolvedFeePayer {
100
109
  const { feePayer, methodDetails, request } = parameters
101
110
  const requestFeePayer = readRequestFeePayer(request)
102
111
  const requestAllowsFeePayer =
103
112
  requestFeePayer === undefined || requestFeePayer === true || typeof requestFeePayer === 'object'
104
113
  if (methodDetails.feePayer !== true || !requestAllowsFeePayer) return undefined
105
- return typeof requestFeePayer === 'object' ? requestFeePayer : feePayer
114
+ if (typeof requestFeePayer === 'object') return requestFeePayer
115
+ if (typeof feePayer === 'object') return feePayer
116
+ return typeof feePayer === 'string' ? true : undefined
106
117
  }
107
118
 
108
119
  /** Declarative server-side settlement cadence for automatic session settlement. */
@@ -1,4 +1,6 @@
1
1
  export { charge, session, settle, settleBatch } from './Session.js'
2
+ /** SSE helpers and types for Tempo session streams. */
3
+ export * as Sse from './Sse.js'
2
4
  /** Server-side automatic settlement schedule. */
3
5
  export type {
4
6
  ResolveSessionChannelId,
@@ -25,6 +25,7 @@ export type ValidateOptions = {
25
25
  endpoint?: string | undefined
26
26
  body?: string | undefined
27
27
  query?: string[] | undefined
28
+ headers?: string[] | undefined
28
29
  verbose?: boolean | undefined
29
30
  yes?: boolean | undefined
30
31
  skipPayment?: boolean | undefined
@@ -92,12 +93,14 @@ export async function* validateStream(options: ValidateOptions): AsyncGenerator<
92
93
  yield { phase: 'endpoint', endpoint }
93
94
 
94
95
  let body: string | undefined
96
+ let bodyIsAutogenerated = false
95
97
  if (options.endpoint) {
96
98
  body = options.body
97
99
  } else {
98
100
  body = resolveBodyForEndpoint(options.body, endpoint.path)
99
101
  if (!body && discovery.doc) {
100
102
  body = extractRequestBodyFromDiscovery(discovery.doc, endpoint)
103
+ bodyIsAutogenerated = !!body
101
104
  }
102
105
  }
103
106
 
@@ -107,7 +110,9 @@ export async function* validateStream(options: ValidateOptions): AsyncGenerator<
107
110
  challenges: parsedChallenges,
108
111
  } = await validateChallenge(baseUrl, endpoint, verbose, {
109
112
  body,
113
+ bodyIsAutogenerated,
110
114
  query: options.query,
115
+ extraHeaders: options.headers,
111
116
  discoveryDoc: discovery.doc ?? undefined,
112
117
  })
113
118
  const effectiveBody = resolvedBody ?? body
@@ -156,6 +161,7 @@ export async function* validateStream(options: ValidateOptions): AsyncGenerator<
156
161
  const paymentResults = await validatePaymentFlow(baseUrl, endpoint, verbose, {
157
162
  body: effectiveBody,
158
163
  query: options.query,
164
+ extraHeaders: options.headers,
159
165
  yes: options.yes,
160
166
  silent: !onResults,
161
167
  interactive: options.interactive,
@@ -68,7 +68,7 @@ const parsePaymentRequiredEnvelope = (value: unknown): PaymentRequiredEnvelope =
68
68
 
69
69
  const resource = ResourceInfoSchema.parse(record.resource)
70
70
  const extensions =
71
- record.extensions === undefined ? undefined : ExtensionsSchema.parse(record.extensions)
71
+ record.extensions !== undefined ? ExtensionsSchema.safeParse(record.extensions).data : undefined
72
72
 
73
73
  return {
74
74
  accepts: record.accepts,