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.
- package/CHANGELOG.md +17 -0
- package/dist/cli/validate/challenge.d.ts +2 -0
- package/dist/cli/validate/challenge.d.ts.map +1 -1
- package/dist/cli/validate/challenge.js +21 -9
- package/dist/cli/validate/challenge.js.map +1 -1
- package/dist/cli/validate/discovery.d.ts.map +1 -1
- package/dist/cli/validate/discovery.js +50 -19
- package/dist/cli/validate/discovery.js.map +1 -1
- package/dist/cli/validate/helpers.d.ts +1 -0
- package/dist/cli/validate/helpers.d.ts.map +1 -1
- package/dist/cli/validate/helpers.js +11 -0
- package/dist/cli/validate/helpers.js.map +1 -1
- package/dist/cli/validate/index.d.ts +1 -0
- package/dist/cli/validate/index.d.ts.map +1 -1
- package/dist/cli/validate/index.js +4 -0
- package/dist/cli/validate/index.js.map +1 -1
- package/dist/cli/validate/payment.d.ts +1 -0
- package/dist/cli/validate/payment.d.ts.map +1 -1
- package/dist/cli/validate/payment.js +2 -2
- package/dist/cli/validate/payment.js.map +1 -1
- package/dist/tempo/internal/fee-payer.d.ts +39 -2
- package/dist/tempo/internal/fee-payer.d.ts.map +1 -1
- package/dist/tempo/internal/fee-payer.js +45 -1
- package/dist/tempo/internal/fee-payer.js.map +1 -1
- package/dist/tempo/internal/fee-token.d.ts +8 -0
- package/dist/tempo/internal/fee-token.d.ts.map +1 -1
- package/dist/tempo/internal/fee-token.js +19 -6
- package/dist/tempo/internal/fee-token.js.map +1 -1
- package/dist/tempo/legacy/session/Chain.d.ts.map +1 -1
- package/dist/tempo/legacy/session/Chain.js +53 -50
- package/dist/tempo/legacy/session/Chain.js.map +1 -1
- package/dist/tempo/server/Charge.d.ts +8 -0
- package/dist/tempo/server/Charge.d.ts.map +1 -1
- package/dist/tempo/server/Charge.js +49 -45
- package/dist/tempo/server/Charge.js.map +1 -1
- package/dist/tempo/server/Methods.d.ts +7 -0
- package/dist/tempo/server/Methods.d.ts.map +1 -1
- package/dist/tempo/server/Methods.js.map +1 -1
- package/dist/tempo/server/Subscription.d.ts.map +1 -1
- package/dist/tempo/server/Subscription.js +24 -16
- package/dist/tempo/server/Subscription.js.map +1 -1
- package/dist/tempo/server/internal/html.gen.d.ts +1 -1
- package/dist/tempo/server/internal/html.gen.d.ts.map +1 -1
- package/dist/tempo/server/internal/html.gen.js +1 -1
- package/dist/tempo/server/internal/html.gen.js.map +1 -1
- package/dist/tempo/session/client/SessionManager.d.ts +3 -0
- package/dist/tempo/session/client/SessionManager.d.ts.map +1 -1
- package/dist/tempo/session/client/SessionManager.js +1 -0
- package/dist/tempo/session/client/SessionManager.js.map +1 -1
- package/dist/tempo/session/client/Transports.d.ts +20 -0
- package/dist/tempo/session/client/Transports.d.ts.map +1 -1
- package/dist/tempo/session/client/Transports.js +23 -11
- package/dist/tempo/session/client/Transports.js.map +1 -1
- package/dist/tempo/session/precompile/Chain.d.ts.map +1 -1
- package/dist/tempo/session/precompile/Chain.js +25 -19
- package/dist/tempo/session/precompile/Chain.js.map +1 -1
- package/dist/tempo/session/server/index.d.ts +2 -0
- package/dist/tempo/session/server/index.d.ts.map +1 -1
- package/dist/tempo/session/server/index.js +2 -0
- package/dist/tempo/session/server/index.js.map +1 -1
- package/dist/validation/core.d.ts +1 -0
- package/dist/validation/core.d.ts.map +1 -1
- package/dist/validation/core.js +5 -0
- package/dist/validation/core.js.map +1 -1
- package/dist/x402/Header.js +1 -1
- package/dist/x402/Header.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/validate/challenge.ts +20 -8
- package/src/cli/validate/discovery.test.ts +128 -0
- package/src/cli/validate/discovery.ts +50 -15
- package/src/cli/validate/helpers.ts +10 -0
- package/src/cli/validate/index.ts +4 -0
- package/src/cli/validate/payment.ts +4 -1
- package/src/cli/validate.test.ts +23 -2
- package/src/client/Mppx.test-d.ts +3 -1
- package/src/tempo/PublicExports.test-d.ts +5 -0
- package/src/tempo/internal/fee-payer.test.ts +93 -0
- package/src/tempo/internal/fee-payer.ts +64 -2
- package/src/tempo/internal/fee-token.test.ts +37 -0
- package/src/tempo/internal/fee-token.ts +26 -5
- package/src/tempo/legacy/session/Chain.test.ts +78 -2
- package/src/tempo/legacy/session/Chain.ts +56 -52
- package/src/tempo/server/Charge.test.ts +121 -6
- package/src/tempo/server/Charge.ts +61 -48
- package/src/tempo/server/Methods.ts +6 -0
- package/src/tempo/server/Subscription.test.ts +79 -4
- package/src/tempo/server/Subscription.ts +24 -16
- package/src/tempo/server/internal/html.gen.ts +1 -1
- package/src/tempo/session/client/SessionManager.test.ts +12 -1
- package/src/tempo/session/client/SessionManager.ts +3 -0
- package/src/tempo/session/client/Transports.test.ts +34 -0
- package/src/tempo/session/client/Transports.ts +47 -20
- package/src/tempo/session/precompile/Chain.test.ts +9 -0
- package/src/tempo/session/precompile/Chain.ts +28 -20
- package/src/tempo/session/server/Session.test.ts +23 -3
- package/src/tempo/session/server/index.ts +2 -0
- package/src/validation/core.ts +6 -0
- 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
|
|
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
|
|
781
|
-
challenge,
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
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
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
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*
|
|
800
|
-
parameters
|
|
801
|
-
)
|
|
802
|
-
|
|
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
|
-
|
|
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
|
|
851
|
+
await parameters.onNeedVoucher(event.data)
|
|
828
852
|
break
|
|
829
853
|
case 'payment-receipt':
|
|
830
|
-
parameters.
|
|
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:
|
|
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,
|
|
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
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
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,
|
|
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
|
-
{
|
|
2306
|
-
|
|
2307
|
-
|
|
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)
|
package/src/validation/core.ts
CHANGED
|
@@ -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,
|
package/src/x402/Header.ts
CHANGED
|
@@ -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
|
|
71
|
+
record.extensions !== undefined ? ExtensionsSchema.safeParse(record.extensions).data : undefined
|
|
72
72
|
|
|
73
73
|
return {
|
|
74
74
|
accepts: record.accepts,
|