mppx 0.8.9 → 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 (98) hide show
  1. package/CHANGELOG.md +17 -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 +45 -1
  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.map +1 -1
  55. package/dist/tempo/session/precompile/Chain.js +25 -19
  56. package/dist/tempo/session/precompile/Chain.js.map +1 -1
  57. package/dist/tempo/session/server/index.d.ts +2 -0
  58. package/dist/tempo/session/server/index.d.ts.map +1 -1
  59. package/dist/tempo/session/server/index.js +2 -0
  60. package/dist/tempo/session/server/index.js.map +1 -1
  61. package/dist/validation/core.d.ts +1 -0
  62. package/dist/validation/core.d.ts.map +1 -1
  63. package/dist/validation/core.js +5 -0
  64. package/dist/validation/core.js.map +1 -1
  65. package/dist/x402/Header.js +1 -1
  66. package/dist/x402/Header.js.map +1 -1
  67. package/package.json +1 -1
  68. package/src/cli/validate/challenge.ts +20 -8
  69. package/src/cli/validate/discovery.test.ts +128 -0
  70. package/src/cli/validate/discovery.ts +50 -15
  71. package/src/cli/validate/helpers.ts +10 -0
  72. package/src/cli/validate/index.ts +4 -0
  73. package/src/cli/validate/payment.ts +4 -1
  74. package/src/cli/validate.test.ts +23 -2
  75. package/src/client/Mppx.test-d.ts +3 -1
  76. package/src/tempo/PublicExports.test-d.ts +5 -0
  77. package/src/tempo/internal/fee-payer.test.ts +93 -0
  78. package/src/tempo/internal/fee-payer.ts +64 -2
  79. package/src/tempo/internal/fee-token.test.ts +37 -0
  80. package/src/tempo/internal/fee-token.ts +26 -5
  81. package/src/tempo/legacy/session/Chain.test.ts +78 -2
  82. package/src/tempo/legacy/session/Chain.ts +56 -52
  83. package/src/tempo/server/Charge.test.ts +121 -6
  84. package/src/tempo/server/Charge.ts +61 -48
  85. package/src/tempo/server/Methods.ts +6 -0
  86. package/src/tempo/server/Subscription.test.ts +79 -4
  87. package/src/tempo/server/Subscription.ts +24 -16
  88. package/src/tempo/server/internal/html.gen.ts +1 -1
  89. package/src/tempo/session/client/SessionManager.test.ts +12 -1
  90. package/src/tempo/session/client/SessionManager.ts +3 -0
  91. package/src/tempo/session/client/Transports.test.ts +34 -0
  92. package/src/tempo/session/client/Transports.ts +47 -20
  93. package/src/tempo/session/precompile/Chain.test.ts +9 -0
  94. package/src/tempo/session/precompile/Chain.ts +28 -20
  95. package/src/tempo/session/server/Session.test.ts +23 -3
  96. package/src/tempo/session/server/index.ts +2 -0
  97. package/src/validation/core.ts +6 -0
  98. 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,9 @@ 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)
614
617
  })
615
618
 
616
619
  test('hosted fee-payer relays a sender-signed open without local co-signing', async () => {
@@ -653,6 +656,9 @@ describe('precompile broadcastOpenTransaction', () => {
653
656
 
654
657
  expect(rpcMethods).toContain('eth_sendRawTransaction')
655
658
  expect(rpcMethods).not.toContain('eth_sendRawTransactionSync')
659
+ expect(rpcMethods.indexOf('eth_call')).toBeLessThan(
660
+ rpcMethods.indexOf('eth_sendRawTransaction'),
661
+ )
656
662
  })
657
663
 
658
664
  test('rejects expiring nonce hash mismatches before broadcasting', async () => {
@@ -971,6 +977,9 @@ describe('precompile broadcastTopUpTransaction', () => {
971
977
  expect(broadcastIndex).toBeGreaterThan(-1)
972
978
  expect(simulationIndex).toBeGreaterThan(-1)
973
979
  expect(simulationIndex).toBeLessThan(broadcastIndex)
980
+ expect(
981
+ rpcMethods.slice(0, broadcastIndex).filter((method) => method === 'eth_call'),
982
+ ).toHaveLength(2)
974
983
  })
975
984
 
976
985
  test('rejects top-up calldata amount mismatches before broadcasting', async () => {
@@ -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> {
@@ -705,25 +700,38 @@ export async function sendCredentialTransaction(parameters: SendCredentialTransa
705
700
  assertSenderSigned(transaction)
706
701
 
707
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
+ )
708
710
  const txHash = await sendTransaction(client, serializedTransaction)
709
711
  return waitForSuccessfulReceipt(client, txHash)
710
712
  }
711
713
 
712
- await simulateTempoTransaction(client, transaction)
713
-
714
- const sponsored = FeePayer.prepareSponsoredTransaction({
715
- account: feePayer,
716
- allowedFeeTokens,
717
- challengeExpires,
718
- chainId,
719
- details,
720
- policy: feePayerPolicy,
721
- transaction: {
722
- ...transaction,
723
- ...(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 }
724
732
  },
725
733
  })
726
- const serialized = await signTempoTransaction(client, sponsored)
734
+ const serialized = await signTempoTransaction(client, completed.transaction)
727
735
  const receipt = await sendRawTransactionSync(client, {
728
736
  serializedTransaction: serialized as Transaction.TransactionSerializedTempo,
729
737
  })
@@ -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)
@@ -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,