mppx 0.6.29 → 0.6.31
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 +14 -0
- package/dist/evm/index.d.ts +1 -0
- package/dist/evm/index.d.ts.map +1 -1
- package/dist/evm/index.js +1 -0
- package/dist/evm/index.js.map +1 -1
- package/dist/server/Request.js +24 -10
- package/dist/server/Request.js.map +1 -1
- package/dist/stripe/server/internal/html.gen.d.ts +1 -1
- package/dist/stripe/server/internal/html.gen.js +1 -1
- package/dist/tempo/server/Session.d.ts.map +1 -1
- package/dist/tempo/server/Session.js +16 -13
- package/dist/tempo/server/Session.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/package.json +3 -3
- package/src/Challenge.test.ts +30 -4
- package/src/evm/PublicInterface.test-d.ts +5 -0
- package/src/evm/index.ts +1 -0
- package/src/server/Mppx.test.ts +74 -0
- package/src/server/Request.test.ts +81 -0
- package/src/server/Request.ts +23 -9
- package/src/stripe/server/internal/html/package.json +1 -1
- package/src/stripe/server/internal/html.gen.ts +1 -1
- package/src/tempo/server/Session.test.ts +117 -18
- package/src/tempo/server/Session.ts +17 -14
- package/src/tempo/server/internal/html/package.json +1 -1
- package/src/tempo/server/internal/html.gen.ts +1 -1
|
@@ -486,7 +486,7 @@ describe.runIf(isLocalnet)('session', () => {
|
|
|
486
486
|
},
|
|
487
487
|
request: makeRequest(),
|
|
488
488
|
}),
|
|
489
|
-
).rejects.toThrow('voucher amount is below settled on-chain amount')
|
|
489
|
+
).rejects.toThrow('voucher amount is at or below settled on-chain amount')
|
|
490
490
|
})
|
|
491
491
|
|
|
492
492
|
test('zero signer fallback uses payer', async () => {
|
|
@@ -698,27 +698,128 @@ describe.runIf(isLocalnet)('session', () => {
|
|
|
698
698
|
).rejects.toThrow(InvalidSignatureError)
|
|
699
699
|
})
|
|
700
700
|
|
|
701
|
-
test('
|
|
701
|
+
test('accepts lower voucher replay idempotently', async () => {
|
|
702
702
|
const { channelId, serializedTransaction } = await createSignedOpenTransaction(10000000n)
|
|
703
703
|
const server = createServer()
|
|
704
704
|
await openServerChannel(server, channelId, serializedTransaction)
|
|
705
705
|
|
|
706
|
+
const before = await store.getChannel(channelId)
|
|
707
|
+
|
|
708
|
+
// A non-advancing voucher (below highest, above settled) is accepted
|
|
709
|
+
// idempotently with the current highest amount, per the session spec.
|
|
710
|
+
const receipt = (await server.verify({
|
|
711
|
+
credential: {
|
|
712
|
+
challenge: makeChallenge({ id: 'challenge-2', channelId }),
|
|
713
|
+
payload: {
|
|
714
|
+
action: 'voucher' as const,
|
|
715
|
+
channelId,
|
|
716
|
+
cumulativeAmount: '500000',
|
|
717
|
+
signature: await signTestVoucher(channelId, 500000n),
|
|
718
|
+
},
|
|
719
|
+
},
|
|
720
|
+
request: makeRequest(),
|
|
721
|
+
})) as SessionReceipt
|
|
722
|
+
|
|
723
|
+
expect(receipt.status).toBe('success')
|
|
724
|
+
expect(receipt.acceptedCumulative).toBe('1000000')
|
|
725
|
+
|
|
726
|
+
// Channel state is unchanged - no advance, no additional charge.
|
|
727
|
+
const after = await store.getChannel(channelId)
|
|
728
|
+
expect(after).toEqual(before)
|
|
729
|
+
})
|
|
730
|
+
|
|
731
|
+
test('accepts lower replay after a higher voucher has advanced the channel', async () => {
|
|
732
|
+
const { channelId, serializedTransaction } = await createSignedOpenTransaction(10000000n)
|
|
733
|
+
const server = createServer()
|
|
734
|
+
await openServerChannel(server, channelId, serializedTransaction)
|
|
735
|
+
|
|
736
|
+
// Advance the channel from the opening 1000000 to 5000000.
|
|
737
|
+
await server.verify({
|
|
738
|
+
credential: {
|
|
739
|
+
challenge: makeChallenge({ id: 'challenge-advance', channelId }),
|
|
740
|
+
payload: {
|
|
741
|
+
action: 'voucher' as const,
|
|
742
|
+
channelId,
|
|
743
|
+
cumulativeAmount: '5000000',
|
|
744
|
+
signature: await signTestVoucher(channelId, 5000000n),
|
|
745
|
+
},
|
|
746
|
+
},
|
|
747
|
+
request: makeRequest(),
|
|
748
|
+
})
|
|
749
|
+
|
|
750
|
+
const before = await store.getChannel(channelId)
|
|
751
|
+
|
|
752
|
+
// A lower (but above-settled) voucher returns the current highest
|
|
753
|
+
// without rewinding state.
|
|
754
|
+
const receipt = (await server.verify({
|
|
755
|
+
credential: {
|
|
756
|
+
challenge: makeChallenge({ id: 'challenge-lower', channelId }),
|
|
757
|
+
payload: {
|
|
758
|
+
action: 'voucher' as const,
|
|
759
|
+
channelId,
|
|
760
|
+
cumulativeAmount: '3000000',
|
|
761
|
+
signature: await signTestVoucher(channelId, 3000000n),
|
|
762
|
+
},
|
|
763
|
+
},
|
|
764
|
+
request: makeRequest(),
|
|
765
|
+
})) as SessionReceipt
|
|
766
|
+
|
|
767
|
+
expect(receipt.status).toBe('success')
|
|
768
|
+
expect(receipt.acceptedCumulative).toBe('5000000')
|
|
769
|
+
|
|
770
|
+
const after = await store.getChannel(channelId)
|
|
771
|
+
expect(after).toEqual(before)
|
|
772
|
+
})
|
|
773
|
+
|
|
774
|
+
test('accepts lower voucher replay even when below minVoucherDelta', async () => {
|
|
775
|
+
const { channelId, serializedTransaction } = await createSignedOpenTransaction(10000000n)
|
|
776
|
+
// Non-advancing replay must short-circuit before the delta check.
|
|
777
|
+
const server = createServer({ minVoucherDelta: '2' })
|
|
778
|
+
await openServerChannel(server, channelId, serializedTransaction)
|
|
779
|
+
|
|
780
|
+
const before = await store.getChannel(channelId)
|
|
781
|
+
|
|
782
|
+
const receipt = (await server.verify({
|
|
783
|
+
credential: {
|
|
784
|
+
challenge: makeChallenge({ id: 'challenge-lower-delta', channelId }),
|
|
785
|
+
payload: {
|
|
786
|
+
action: 'voucher' as const,
|
|
787
|
+
channelId,
|
|
788
|
+
cumulativeAmount: '500000',
|
|
789
|
+
signature: await signTestVoucher(channelId, 500000n),
|
|
790
|
+
},
|
|
791
|
+
},
|
|
792
|
+
request: makeRequest(),
|
|
793
|
+
})) as SessionReceipt
|
|
794
|
+
|
|
795
|
+
expect(receipt.status).toBe('success')
|
|
796
|
+
expect(receipt.acceptedCumulative).toBe('1000000')
|
|
797
|
+
|
|
798
|
+
const after = await store.getChannel(channelId)
|
|
799
|
+
expect(after).toEqual(before)
|
|
800
|
+
})
|
|
801
|
+
|
|
802
|
+
test('rejects lower voucher replay signed by the wrong signer', async () => {
|
|
803
|
+
const { channelId, serializedTransaction } = await createSignedOpenTransaction(10000000n)
|
|
804
|
+
const server = createServer()
|
|
805
|
+
await openServerChannel(server, channelId, serializedTransaction)
|
|
806
|
+
|
|
807
|
+
// A valid signature from a different account must NOT be treated as an
|
|
808
|
+
// idempotent replay.
|
|
706
809
|
await expect(
|
|
707
810
|
server.verify({
|
|
708
811
|
credential: {
|
|
709
|
-
challenge: makeChallenge({ id: 'challenge-
|
|
812
|
+
challenge: makeChallenge({ id: 'challenge-wrong-signer', channelId }),
|
|
710
813
|
payload: {
|
|
711
814
|
action: 'voucher' as const,
|
|
712
815
|
channelId,
|
|
713
816
|
cumulativeAmount: '500000',
|
|
714
|
-
signature: await signTestVoucher(channelId, 500000n),
|
|
817
|
+
signature: await signTestVoucher(channelId, 500000n, accounts[3]),
|
|
715
818
|
},
|
|
716
819
|
},
|
|
717
820
|
request: makeRequest(),
|
|
718
821
|
}),
|
|
719
|
-
).rejects.toThrow(
|
|
720
|
-
'voucher cumulativeAmount must be strictly greater than highest accepted voucher',
|
|
721
|
-
)
|
|
822
|
+
).rejects.toThrow(InvalidSignatureError)
|
|
722
823
|
})
|
|
723
824
|
|
|
724
825
|
test('rejects replay of settled voucher', async () => {
|
|
@@ -743,7 +844,7 @@ describe.runIf(isLocalnet)('session', () => {
|
|
|
743
844
|
},
|
|
744
845
|
request: makeRequest(),
|
|
745
846
|
}),
|
|
746
|
-
).rejects.toThrow('voucher cumulativeAmount is below on-chain settled amount')
|
|
847
|
+
).rejects.toThrow('voucher cumulativeAmount is at or below on-chain settled amount')
|
|
747
848
|
})
|
|
748
849
|
|
|
749
850
|
test('rejects voucher exceeding deposit', async () => {
|
|
@@ -820,17 +921,15 @@ describe.runIf(isLocalnet)('session', () => {
|
|
|
820
921
|
payload: {
|
|
821
922
|
action: 'voucher' as const,
|
|
822
923
|
channelId,
|
|
823
|
-
//
|
|
824
|
-
//
|
|
924
|
+
// Forged signature for a non-advancing amount (500000 <= highest
|
|
925
|
+
// 1000000, > settled 0). Rejected by signature verification.
|
|
825
926
|
cumulativeAmount: '500000',
|
|
826
927
|
signature: `0x${'ab'.repeat(65)}` as Hex,
|
|
827
928
|
},
|
|
828
929
|
},
|
|
829
930
|
request: makeRequest(),
|
|
830
931
|
}),
|
|
831
|
-
).rejects.toThrow(
|
|
832
|
-
'voucher cumulativeAmount must be strictly greater than highest accepted voucher',
|
|
833
|
-
)
|
|
932
|
+
).rejects.toThrow('invalid voucher signature')
|
|
834
933
|
})
|
|
835
934
|
|
|
836
935
|
test('rejects forged voucher with valid amount but invalid signature', async () => {
|
|
@@ -967,7 +1066,7 @@ describe.runIf(isLocalnet)('session', () => {
|
|
|
967
1066
|
},
|
|
968
1067
|
request: makeRequest(),
|
|
969
1068
|
}),
|
|
970
|
-
).rejects.toThrow('voucher cumulativeAmount is below on-chain settled amount')
|
|
1069
|
+
).rejects.toThrow('voucher cumulativeAmount is at or below on-chain settled amount')
|
|
971
1070
|
})
|
|
972
1071
|
|
|
973
1072
|
test('rejects leaked voucher used in open action with mismatched channel', async () => {
|
|
@@ -2510,7 +2609,7 @@ describe.runIf(isLocalnet)('session', () => {
|
|
|
2510
2609
|
},
|
|
2511
2610
|
request: makeRequest(),
|
|
2512
2611
|
}),
|
|
2513
|
-
).rejects.toThrow('voucher cumulativeAmount is below on-chain settled amount')
|
|
2612
|
+
).rejects.toThrow('voucher cumulativeAmount is at or below on-chain settled amount')
|
|
2514
2613
|
})
|
|
2515
2614
|
|
|
2516
2615
|
test('close after recovery respects on-chain settled as minimum', async () => {
|
|
@@ -4385,7 +4484,7 @@ describe.runIf(isLocalnet)('session', () => {
|
|
|
4385
4484
|
if (result.status === 402) return result.challenge
|
|
4386
4485
|
|
|
4387
4486
|
if (action === 'voucher') {
|
|
4388
|
-
return
|
|
4487
|
+
return result.withReceipt()
|
|
4389
4488
|
}
|
|
4390
4489
|
|
|
4391
4490
|
if (request.headers.get('Accept')?.includes('text/event-stream')) {
|
|
@@ -4463,7 +4562,7 @@ describe.runIf(isLocalnet)('session', () => {
|
|
|
4463
4562
|
if (result.status === 402) return result.challenge
|
|
4464
4563
|
|
|
4465
4564
|
if (action === 'voucher') {
|
|
4466
|
-
return
|
|
4565
|
+
return result.withReceipt()
|
|
4467
4566
|
}
|
|
4468
4567
|
|
|
4469
4568
|
if (request.headers.get('Accept')?.includes('text/event-stream')) {
|
|
@@ -4631,7 +4730,7 @@ describe.runIf(isLocalnet)('session', () => {
|
|
|
4631
4730
|
if (result.status === 402) return result.challenge
|
|
4632
4731
|
|
|
4633
4732
|
if (action === 'voucher') {
|
|
4634
|
-
return
|
|
4733
|
+
return result.withReceipt()
|
|
4635
4734
|
}
|
|
4636
4735
|
|
|
4637
4736
|
if (request.headers.get('Accept')?.includes('text/event-stream')) {
|
|
@@ -285,7 +285,8 @@ export function session<const parameters extends session.Parameters>(
|
|
|
285
285
|
if (
|
|
286
286
|
envelope &&
|
|
287
287
|
isSessionContentRequest(envelope.capturedRequest) &&
|
|
288
|
-
(payload.action === 'open' ||
|
|
288
|
+
(payload.action === 'open' ||
|
|
289
|
+
(payload.action === 'voucher' && !isSseNegotiationRequest(envelope.capturedRequest)))
|
|
289
290
|
) {
|
|
290
291
|
const charged = await charge(
|
|
291
292
|
store,
|
|
@@ -315,17 +316,23 @@ export function session<const parameters extends session.Parameters>(
|
|
|
315
316
|
// updates; billable requests fall through to the application handler.
|
|
316
317
|
respond({ credential, envelope, input }) {
|
|
317
318
|
const { payload } = credential as Credential.Credential<SessionCredentialPayload>
|
|
319
|
+
const request = envelope?.capturedRequest ?? captureRequestBodyProbe(input)
|
|
318
320
|
|
|
319
321
|
if (payload.action === 'close') return new Response(null, { status: 204 })
|
|
320
322
|
if (payload.action === 'topUp') return new Response(null, { status: 204 })
|
|
323
|
+
if (parameters.sse && payload.action === 'voucher' && isSseNegotiationRequest(request))
|
|
324
|
+
return new Response(null, { status: 204 })
|
|
321
325
|
|
|
322
|
-
const request = envelope?.capturedRequest ?? captureRequestBodyProbe(input)
|
|
323
326
|
if (isSessionContentRequest(request)) return undefined
|
|
324
327
|
return new Response(null, { status: 204 })
|
|
325
328
|
},
|
|
326
329
|
})
|
|
327
330
|
}
|
|
328
331
|
|
|
332
|
+
function isSseNegotiationRequest(input: Pick<Method.CapturedRequest, 'headers'>): boolean {
|
|
333
|
+
return input.headers.get('Accept')?.includes('text/event-stream') ?? false
|
|
334
|
+
}
|
|
335
|
+
|
|
329
336
|
export declare namespace session {
|
|
330
337
|
type Defaults = LooseOmit<
|
|
331
338
|
Method.RequestDefaults<typeof Methods.session>,
|
|
@@ -553,7 +560,7 @@ async function verifyAndAcceptVoucher(parameters: {
|
|
|
553
560
|
|
|
554
561
|
if (voucher.cumulativeAmount <= onChain.settled) {
|
|
555
562
|
throw new VerificationFailedError({
|
|
556
|
-
reason: 'voucher cumulativeAmount is below on-chain settled amount',
|
|
563
|
+
reason: 'voucher cumulativeAmount is at or below on-chain settled amount',
|
|
557
564
|
})
|
|
558
565
|
}
|
|
559
566
|
|
|
@@ -561,12 +568,6 @@ async function verifyAndAcceptVoucher(parameters: {
|
|
|
561
568
|
throw new AmountExceedsDepositError({ reason: 'voucher amount exceeds on-chain deposit' })
|
|
562
569
|
}
|
|
563
570
|
|
|
564
|
-
if (voucher.cumulativeAmount < channel.highestVoucherAmount) {
|
|
565
|
-
throw new VerificationFailedError({
|
|
566
|
-
reason: 'voucher cumulativeAmount must be strictly greater than highest accepted voucher',
|
|
567
|
-
})
|
|
568
|
-
}
|
|
569
|
-
|
|
570
571
|
const isValid = await verifyVoucher(
|
|
571
572
|
channel.escrowContract,
|
|
572
573
|
channel.chainId,
|
|
@@ -578,9 +579,11 @@ async function verifyAndAcceptVoucher(parameters: {
|
|
|
578
579
|
throw new InvalidSignatureError({ reason: 'invalid voucher signature' })
|
|
579
580
|
}
|
|
580
581
|
|
|
581
|
-
// Idempotent replay:
|
|
582
|
-
//
|
|
583
|
-
|
|
582
|
+
// Idempotent replay: a non-advancing voucher (at or below the highest
|
|
583
|
+
// accepted amount, but above the on-chain settled amount checked above)
|
|
584
|
+
// returns 200 OK with the current highest amount without advancing state,
|
|
585
|
+
// per the session spec's idempotency requirement.
|
|
586
|
+
if (voucher.cumulativeAmount <= channel.highestVoucherAmount) {
|
|
584
587
|
return createSessionReceipt({
|
|
585
588
|
challengeId: challenge.id,
|
|
586
589
|
channelId,
|
|
@@ -660,7 +663,7 @@ async function handleOpen(
|
|
|
660
663
|
|
|
661
664
|
if (voucher.cumulativeAmount <= onChain.settled) {
|
|
662
665
|
throw new VerificationFailedError({
|
|
663
|
-
reason: 'voucher cumulativeAmount is below on-chain settled amount',
|
|
666
|
+
reason: 'voucher cumulativeAmount is at or below on-chain settled amount',
|
|
664
667
|
})
|
|
665
668
|
}
|
|
666
669
|
|
|
@@ -701,7 +704,7 @@ async function handleOpen(
|
|
|
701
704
|
if (existing) {
|
|
702
705
|
if (voucher.cumulativeAmount <= existing.settledOnChain) {
|
|
703
706
|
throw new VerificationFailedError({
|
|
704
|
-
reason: 'voucher amount is below settled on-chain amount',
|
|
707
|
+
reason: 'voucher amount is at or below settled on-chain amount',
|
|
705
708
|
})
|
|
706
709
|
}
|
|
707
710
|
|