mppx 0.8.13 → 0.8.15
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 +37 -0
- package/dist/Errors.d.ts +10 -0
- package/dist/Errors.d.ts.map +1 -1
- package/dist/Errors.js +11 -2
- package/dist/Errors.js.map +1 -1
- package/dist/Method.d.ts +105 -3
- package/dist/Method.d.ts.map +1 -1
- package/dist/Method.js +102 -2
- package/dist/Method.js.map +1 -1
- package/dist/cli/validate/payment.d.ts.map +1 -1
- package/dist/cli/validate/payment.js +18 -4
- package/dist/cli/validate/payment.js.map +1 -1
- package/dist/client/internal/Fetch.d.ts.map +1 -1
- package/dist/client/internal/Fetch.js +98 -5
- package/dist/client/internal/Fetch.js.map +1 -1
- package/dist/client/internal/MethodChallenge.d.ts +19 -0
- package/dist/client/internal/MethodChallenge.d.ts.map +1 -0
- package/dist/client/internal/MethodChallenge.js +15 -0
- package/dist/client/internal/MethodChallenge.js.map +1 -0
- package/dist/client/internal/MethodResponse.d.ts +15 -0
- package/dist/client/internal/MethodResponse.d.ts.map +1 -1
- package/dist/client/internal/MethodResponse.js +10 -0
- package/dist/client/internal/MethodResponse.js.map +1 -1
- package/dist/server/Mppx.d.ts +23 -4
- package/dist/server/Mppx.d.ts.map +1 -1
- package/dist/server/Mppx.js +91 -82
- package/dist/server/Mppx.js.map +1 -1
- package/dist/stripe/server/internal/html.gen.d.ts +1 -1
- package/dist/stripe/server/internal/html.gen.d.ts.map +1 -1
- package/dist/stripe/server/internal/html.gen.js +1 -1
- package/dist/stripe/server/internal/html.gen.js.map +1 -1
- package/dist/tempo/client/Methods.d.ts +1 -0
- package/dist/tempo/client/Methods.d.ts.map +1 -1
- package/dist/tempo/internal/fee-payer.d.ts +1 -0
- package/dist/tempo/internal/fee-payer.d.ts.map +1 -1
- package/dist/tempo/internal/fee-payer.js +7 -2
- package/dist/tempo/internal/fee-payer.js.map +1 -1
- package/dist/tempo/legacy/client/ChannelOps.d.ts.map +1 -1
- package/dist/tempo/legacy/client/ChannelOps.js +2 -1
- package/dist/tempo/legacy/client/ChannelOps.js.map +1 -1
- package/dist/tempo/server/Charge.d.ts +46 -77
- package/dist/tempo/server/Charge.d.ts.map +1 -1
- package/dist/tempo/server/Charge.js +289 -209
- package/dist/tempo/server/Charge.js.map +1 -1
- package/dist/tempo/server/Methods.d.ts +12 -1
- package/dist/tempo/server/Methods.d.ts.map +1 -1
- package/dist/tempo/server/Methods.js +4 -0
- package/dist/tempo/server/Methods.js.map +1 -1
- package/dist/tempo/server/Relay.d.ts +57 -0
- package/dist/tempo/server/Relay.d.ts.map +1 -0
- package/dist/tempo/server/Relay.js +179 -0
- package/dist/tempo/server/Relay.js.map +1 -0
- package/dist/tempo/server/SponsorBudget.d.ts +59 -0
- package/dist/tempo/server/SponsorBudget.d.ts.map +1 -0
- package/dist/tempo/server/SponsorBudget.js +150 -0
- package/dist/tempo/server/SponsorBudget.js.map +1 -0
- 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/ChannelOps.d.ts +7 -1
- package/dist/tempo/session/client/ChannelOps.d.ts.map +1 -1
- package/dist/tempo/session/client/ChannelOps.js +9 -4
- package/dist/tempo/session/client/ChannelOps.js.map +1 -1
- package/dist/tempo/session/client/CredentialState.d.ts +5 -1
- package/dist/tempo/session/client/CredentialState.d.ts.map +1 -1
- package/dist/tempo/session/client/CredentialState.js +37 -19
- package/dist/tempo/session/client/CredentialState.js.map +1 -1
- package/dist/tempo/session/client/Runtime.d.ts +8 -0
- package/dist/tempo/session/client/Runtime.d.ts.map +1 -1
- package/dist/tempo/session/client/Runtime.js +17 -0
- package/dist/tempo/session/client/Runtime.js.map +1 -1
- package/dist/tempo/session/client/Session.d.ts +15 -5
- package/dist/tempo/session/client/Session.d.ts.map +1 -1
- package/dist/tempo/session/client/Session.js +216 -45
- package/dist/tempo/session/client/Session.js.map +1 -1
- package/dist/tempo/session/client/SessionManager.d.ts +8 -1
- package/dist/tempo/session/client/SessionManager.d.ts.map +1 -1
- package/dist/tempo/session/client/SessionManager.js +114 -44
- package/dist/tempo/session/client/SessionManager.js.map +1 -1
- package/dist/tempo/session/client/Transports.d.ts +16 -6
- package/dist/tempo/session/client/Transports.d.ts.map +1 -1
- package/dist/tempo/session/client/Transports.js +84 -37
- 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 +96 -8
- package/dist/tempo/session/precompile/Chain.js.map +1 -1
- package/dist/tempo/session/precompile/Channel.d.ts +2 -0
- package/dist/tempo/session/precompile/Channel.d.ts.map +1 -1
- package/dist/tempo/session/precompile/Channel.js +4 -0
- package/dist/tempo/session/precompile/Channel.js.map +1 -1
- package/dist/tempo/session/server/RequestState.js +3 -1
- package/dist/tempo/session/server/RequestState.js.map +1 -1
- package/dist/tempo/session/server/Ws.d.ts.map +1 -1
- package/dist/tempo/session/server/Ws.js +3 -0
- package/dist/tempo/session/server/Ws.js.map +1 -1
- package/dist/tempo/subscription/KeyAuthorization.d.ts +1 -829
- package/dist/tempo/subscription/KeyAuthorization.d.ts.map +1 -1
- package/dist/tempo/subscription/KeyAuthorization.js +14 -3
- package/dist/tempo/subscription/KeyAuthorization.js.map +1 -1
- package/dist/viem/Client.d.ts.map +1 -1
- package/dist/viem/Client.js +6 -1
- package/dist/viem/Client.js.map +1 -1
- package/package.json +2 -2
- package/src/Errors.test.ts +23 -0
- package/src/Errors.ts +21 -2
- package/src/Method.test.ts +102 -1
- package/src/Method.ts +241 -5
- package/src/cli/validate/payment.ts +18 -4
- package/src/cli/validate.test.ts +16 -0
- package/src/client/Mppx.test-d.ts +3 -3
- package/src/client/internal/Fetch.test.ts +77 -0
- package/src/client/internal/Fetch.ts +122 -11
- package/src/client/internal/MethodChallenge.ts +35 -0
- package/src/client/internal/MethodResponse.ts +28 -0
- package/src/server/Mppx.test-d.ts +2 -0
- package/src/server/Mppx.test.ts +205 -0
- package/src/server/Mppx.ts +167 -103
- package/src/stripe/server/internal/html.gen.ts +1 -1
- package/src/tempo/PublicExports.test-d.ts +20 -0
- package/src/tempo/internal/fee-payer.ts +7 -3
- package/src/tempo/legacy/client/ChannelOps.test.ts +7 -0
- package/src/tempo/legacy/client/ChannelOps.ts +2 -1
- package/src/tempo/server/Charge.test.ts +337 -63
- package/src/tempo/server/Charge.ts +443 -267
- package/src/tempo/server/Methods.ts +11 -0
- package/src/tempo/server/Relay.test.ts +677 -0
- package/src/tempo/server/Relay.ts +297 -0
- package/src/tempo/server/SponsorBudget.test.ts +154 -0
- package/src/tempo/server/SponsorBudget.ts +219 -0
- package/src/tempo/server/internal/html.gen.ts +1 -1
- package/src/tempo/session/README.md +7 -0
- package/src/tempo/session/client/ChannelOps.test.ts +17 -2
- package/src/tempo/session/client/ChannelOps.ts +12 -4
- package/src/tempo/session/client/CredentialState.ts +61 -20
- package/src/tempo/session/client/Runtime.test.ts +12 -0
- package/src/tempo/session/client/Runtime.ts +22 -0
- package/src/tempo/session/client/Session.test.ts +506 -7
- package/src/tempo/session/client/Session.ts +279 -54
- package/src/tempo/session/client/SessionManager.test.ts +318 -13
- package/src/tempo/session/client/SessionManager.ts +132 -42
- package/src/tempo/session/client/Transports.test.ts +54 -1
- package/src/tempo/session/client/Transports.ts +137 -57
- package/src/tempo/session/precompile/Chain.integration.test.ts +19 -1
- package/src/tempo/session/precompile/Chain.test.ts +197 -10
- package/src/tempo/session/precompile/Chain.ts +102 -8
- package/src/tempo/session/precompile/Channel.test.ts +9 -0
- package/src/tempo/session/precompile/Channel.ts +5 -0
- package/src/tempo/session/server/RequestState.test.ts +8 -1
- package/src/tempo/session/server/RequestState.ts +2 -2
- package/src/tempo/session/server/Session.test.ts +66 -4
- package/src/tempo/session/server/Ws.ts +3 -0
- package/src/tempo/subscription/KeyAuthorization.test.ts +70 -1
- package/src/tempo/subscription/KeyAuthorization.ts +14 -3
- package/src/viem/Client.test.ts +18 -1
- package/src/viem/Client.ts +7 -1
|
@@ -113,6 +113,20 @@ function makeChannelStore(seed: readonly ChannelEntry[] = []) {
|
|
|
113
113
|
return { store, set, delete: remove, map }
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
+
function makeDurableChannelStore() {
|
|
117
|
+
const backend = new Map<string, string>()
|
|
118
|
+
const store = createJsonChannelStore({
|
|
119
|
+
get: (key) => backend.get(key),
|
|
120
|
+
set: (key, value) => {
|
|
121
|
+
backend.set(key, value)
|
|
122
|
+
},
|
|
123
|
+
delete: (key) => {
|
|
124
|
+
backend.delete(key)
|
|
125
|
+
},
|
|
126
|
+
})
|
|
127
|
+
return { backend, store }
|
|
128
|
+
}
|
|
129
|
+
|
|
116
130
|
function makeChallenge(overrides: Record<string, unknown> = {}): Challenge.Challenge {
|
|
117
131
|
return Challenge.from({
|
|
118
132
|
id: challengeId,
|
|
@@ -609,7 +623,55 @@ describe('Session', () => {
|
|
|
609
623
|
expect(s.channelId).not.toBe(storedChannelId)
|
|
610
624
|
})
|
|
611
625
|
|
|
612
|
-
test('
|
|
626
|
+
test('keeps a persisted channel after its committed top-up when the paid retry fails', async () => {
|
|
627
|
+
const seeded = channelEntry({ cumulativeAmount: 10_000_000n, deposit: 10_000_000n })
|
|
628
|
+
const { store, delete: remove, map } = makeChannelStore([seeded])
|
|
629
|
+
const postedPayloads: SessionCredentialPayload[] = []
|
|
630
|
+
let probes = 0
|
|
631
|
+
const mockFetch = vi.fn().mockImplementation((_input, init?: RequestInit) => {
|
|
632
|
+
const authorization = new Headers(init?.headers).get(Constants.Headers.authorization)
|
|
633
|
+
const payload = authorization
|
|
634
|
+
? Credential.deserialize<SessionCredentialPayload>(authorization).payload
|
|
635
|
+
: undefined
|
|
636
|
+
if (!payload) {
|
|
637
|
+
probes++
|
|
638
|
+
if (probes > 1) throw new Error('unexpected fresh-channel retry')
|
|
639
|
+
return Promise.resolve(make402Response())
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
postedPayloads.push(payload)
|
|
643
|
+
if (payload.action === 'topUp') return Promise.resolve(new Response(null, { status: 204 }))
|
|
644
|
+
if (payload.action === 'voucher')
|
|
645
|
+
return Promise.resolve(new Response('upstream failed', { status: 500 }))
|
|
646
|
+
throw new Error(`unexpected ${payload.action} credential`)
|
|
647
|
+
})
|
|
648
|
+
const s = sessionManager({
|
|
649
|
+
account,
|
|
650
|
+
client,
|
|
651
|
+
fetch: mockFetch as typeof globalThis.fetch,
|
|
652
|
+
maxDeposit: '20',
|
|
653
|
+
channelStore: store,
|
|
654
|
+
})
|
|
655
|
+
|
|
656
|
+
const response = await s.fetch('https://api.example.com/data')
|
|
657
|
+
|
|
658
|
+
expect(response.status).toBe(500)
|
|
659
|
+
expect(postedPayloads.map((payload) => payload.action)).toEqual(['topUp', 'voucher'])
|
|
660
|
+
expect(remove).not.toHaveBeenCalled()
|
|
661
|
+
expect(map.get(entryKey(seeded))).toMatchObject({
|
|
662
|
+
channelId: storedChannelId,
|
|
663
|
+
cumulativeAmount: 10_000_000n,
|
|
664
|
+
deposit: 11_000_000n,
|
|
665
|
+
})
|
|
666
|
+
expect(s.channelId).toBe(storedChannelId)
|
|
667
|
+
expect(s.state).toMatchObject({
|
|
668
|
+
status: 'active',
|
|
669
|
+
channelId: storedChannelId,
|
|
670
|
+
deposit: '11000000',
|
|
671
|
+
})
|
|
672
|
+
})
|
|
673
|
+
|
|
674
|
+
test('rolls back optimistic opens across replacement challenges', async () => {
|
|
613
675
|
const { store, delete: remove } = makeChannelStore()
|
|
614
676
|
const postedPayloads: SessionCredentialPayload[] = []
|
|
615
677
|
const challenge = (feePayer: boolean) =>
|
|
@@ -621,6 +683,8 @@ describe('Session', () => {
|
|
|
621
683
|
feePayer,
|
|
622
684
|
},
|
|
623
685
|
})
|
|
686
|
+
const original = challenge(true)
|
|
687
|
+
const replacement = challenge(false)
|
|
624
688
|
let openCount = 0
|
|
625
689
|
const mockFetch = vi.fn().mockImplementation((_input, init?: RequestInit) => {
|
|
626
690
|
const authorization = new Headers(init?.headers).get(Constants.Headers.authorization)
|
|
@@ -629,10 +693,11 @@ describe('Session', () => {
|
|
|
629
693
|
: undefined
|
|
630
694
|
if (payload) postedPayloads.push(payload)
|
|
631
695
|
|
|
632
|
-
if (!payload) return Promise.resolve(make402Response(
|
|
696
|
+
if (!payload) return Promise.resolve(make402Response(original))
|
|
633
697
|
if (payload.action === 'open') {
|
|
634
698
|
openCount++
|
|
635
|
-
if (openCount === 1) return Promise.resolve(make402Response(
|
|
699
|
+
if (openCount === 1) return Promise.resolve(make402Response(replacement))
|
|
700
|
+
if (openCount === 2) return Promise.resolve(make402Response(original))
|
|
636
701
|
return Promise.resolve(makeOkResponse())
|
|
637
702
|
}
|
|
638
703
|
return Promise.resolve(new Response('unexpected voucher', { status: 500 }))
|
|
@@ -648,12 +713,33 @@ describe('Session', () => {
|
|
|
648
713
|
const response = await s.fetch('https://api.example.com/data')
|
|
649
714
|
|
|
650
715
|
expect(response.status).toBe(200)
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
expect(
|
|
654
|
-
expect(
|
|
655
|
-
|
|
656
|
-
|
|
716
|
+
expect(postedPayloads.map((payload) => payload.action)).toEqual(['open', 'open', 'open'])
|
|
717
|
+
expect(new Set(postedPayloads.map((payload) => payload.channelId)).size).toBe(3)
|
|
718
|
+
expect(remove).toHaveBeenCalledTimes(2)
|
|
719
|
+
expect(s.channelId).toBe(postedPayloads[2]?.channelId)
|
|
720
|
+
})
|
|
721
|
+
|
|
722
|
+
test('reuses prepared voucher state when an identical challenge succeeds', async () => {
|
|
723
|
+
const vouchers: Extract<SessionCredentialPayload, { action: 'voucher' }>[] = []
|
|
724
|
+
const mockFetch = vi.fn().mockImplementation((_input, init?: RequestInit) => {
|
|
725
|
+
const authorization = new Headers(init?.headers).get(Constants.Headers.authorization)
|
|
726
|
+
if (!authorization) return Promise.resolve(make402Response())
|
|
727
|
+
const payload = Credential.deserialize<SessionCredentialPayload>(authorization).payload
|
|
728
|
+
if (payload.action !== 'voucher') throw new Error('expected voucher')
|
|
729
|
+
vouchers.push(payload)
|
|
730
|
+
return Promise.resolve(vouchers.length === 1 ? make402Response() : makeOkResponse())
|
|
731
|
+
})
|
|
732
|
+
const s = sessionManager({
|
|
733
|
+
account,
|
|
734
|
+
client,
|
|
735
|
+
fetch: mockFetch as typeof globalThis.fetch,
|
|
736
|
+
channelStore: makeChannelStore([channelEntry()]).store,
|
|
737
|
+
})
|
|
738
|
+
|
|
739
|
+
expect((await s.fetch('https://api.example.com/data')).status).toBe(200)
|
|
740
|
+
expect(vouchers.map((voucher) => voucher.cumulativeAmount)).toEqual(['2000000', '2000000'])
|
|
741
|
+
expect(s.cumulative).toBe(2_000_000n)
|
|
742
|
+
expect(s.state.status).toBe('active')
|
|
657
743
|
})
|
|
658
744
|
|
|
659
745
|
test('does not bootstrap when disabled', async () => {
|
|
@@ -753,10 +839,11 @@ describe('Session', () => {
|
|
|
753
839
|
await restarted.fetch('https://api.example.com/data')
|
|
754
840
|
|
|
755
841
|
// The first request carries no hint header; after the 402 the restarted
|
|
756
|
-
// manager resumes the persisted channel
|
|
757
|
-
//
|
|
842
|
+
// manager tops up and resumes the full persisted channel rather than
|
|
843
|
+
// opening a new one.
|
|
758
844
|
expect(new Headers(resumeFetch.mock.calls[0]?.[1]?.headers).get('Payment-Session')).toBeNull()
|
|
759
|
-
expect(posted
|
|
845
|
+
expect(posted.map((payload) => payload.action)).toEqual(['topUp', 'voucher'])
|
|
846
|
+
expect(posted[1]).toMatchObject({ channelId })
|
|
760
847
|
})
|
|
761
848
|
|
|
762
849
|
test('persists opened channels and deletes closed channels when supported', async () => {
|
|
@@ -940,6 +1027,60 @@ describe('Session', () => {
|
|
|
940
1027
|
})
|
|
941
1028
|
})
|
|
942
1029
|
|
|
1030
|
+
test('preserves a snapshot-acknowledged open when its voucher fails', async () => {
|
|
1031
|
+
const { backend, store } = makeDurableChannelStore()
|
|
1032
|
+
const payloads: SessionCredentialPayload[] = []
|
|
1033
|
+
const mockFetch = vi.fn().mockImplementation((_input, init?: RequestInit) => {
|
|
1034
|
+
const authorization = new Headers(init?.headers).get(Constants.Headers.authorization)
|
|
1035
|
+
if (!authorization)
|
|
1036
|
+
return Promise.resolve(make402Response(makeChallenge({ suggestedDeposit: '10000000' })))
|
|
1037
|
+
const payload = Credential.deserialize<SessionCredentialPayload>(authorization).payload
|
|
1038
|
+
payloads.push(payload)
|
|
1039
|
+
if (payload.action !== 'open')
|
|
1040
|
+
return Promise.resolve(new Response('failed', { status: 500 }))
|
|
1041
|
+
return Promise.resolve(
|
|
1042
|
+
make402Response(
|
|
1043
|
+
makeChallenge({
|
|
1044
|
+
methodDetails: {
|
|
1045
|
+
escrowContract: tip20ChannelEscrow,
|
|
1046
|
+
chainId: 4217,
|
|
1047
|
+
sessionProtocol: Constants.SessionProtocols.v2,
|
|
1048
|
+
sessionSnapshot: {
|
|
1049
|
+
acceptedCumulative: '1000000',
|
|
1050
|
+
chainId: 4217,
|
|
1051
|
+
channelId: payload.channelId,
|
|
1052
|
+
deposit: '10000000',
|
|
1053
|
+
descriptor: payload.descriptor,
|
|
1054
|
+
escrow: tip20ChannelEscrow,
|
|
1055
|
+
requiredCumulative: '2000000',
|
|
1056
|
+
settled: '0',
|
|
1057
|
+
spent: '1000000',
|
|
1058
|
+
units: 1,
|
|
1059
|
+
},
|
|
1060
|
+
},
|
|
1061
|
+
}),
|
|
1062
|
+
),
|
|
1063
|
+
)
|
|
1064
|
+
})
|
|
1065
|
+
const s = sessionManager({
|
|
1066
|
+
account,
|
|
1067
|
+
client,
|
|
1068
|
+
fetch: mockFetch as typeof globalThis.fetch,
|
|
1069
|
+
maxDeposit: '10',
|
|
1070
|
+
channelStore: store,
|
|
1071
|
+
})
|
|
1072
|
+
|
|
1073
|
+
expect((await s.fetch('https://api.example.com/data')).status).toBe(500)
|
|
1074
|
+
expect(payloads.map((payload) => payload.action)).toEqual(['open', 'voucher'])
|
|
1075
|
+
expect(s.channelId).toBe(payloads[0]?.channelId)
|
|
1076
|
+
expect(s.cumulative).toBe(1_000_000n)
|
|
1077
|
+
expect(JSON.parse([...backend.values()][0]!)).toMatchObject({
|
|
1078
|
+
channelId: payloads[0]?.channelId,
|
|
1079
|
+
cumulativeAmount: '1000000',
|
|
1080
|
+
opened: true,
|
|
1081
|
+
})
|
|
1082
|
+
})
|
|
1083
|
+
|
|
943
1084
|
test('preemptively top-ups before signing an HTTP voucher that exceeds deposit', async () => {
|
|
944
1085
|
const postedPayloads: SessionCredentialPayload[] = []
|
|
945
1086
|
let challengeCount = 0
|
|
@@ -987,6 +1128,7 @@ describe('Session', () => {
|
|
|
987
1128
|
client,
|
|
988
1129
|
fetch: mockFetch as typeof globalThis.fetch,
|
|
989
1130
|
maxDeposit: '10',
|
|
1131
|
+
topUpAmount: '5',
|
|
990
1132
|
})
|
|
991
1133
|
|
|
992
1134
|
await s.fetch('https://api.example.com/data')
|
|
@@ -994,14 +1136,132 @@ describe('Session', () => {
|
|
|
994
1136
|
|
|
995
1137
|
expect(response.status).toBe(200)
|
|
996
1138
|
expect(postedPayloads.map((payload) => payload.action)).toEqual(['open', 'topUp', 'voucher'])
|
|
1139
|
+
const topUp = postedPayloads[1]
|
|
1140
|
+
if (topUp?.action !== 'topUp') throw new Error('expected top-up payload')
|
|
1141
|
+
expect(topUp.additionalDeposit).toBe('5000000')
|
|
997
1142
|
expect(s.state).toMatchObject({
|
|
998
1143
|
status: 'active',
|
|
999
1144
|
acceptedCumulative: '2000000',
|
|
1000
|
-
deposit: '
|
|
1145
|
+
deposit: '6000000',
|
|
1001
1146
|
spent: '2000000',
|
|
1002
1147
|
units: 2,
|
|
1003
1148
|
})
|
|
1004
1149
|
})
|
|
1150
|
+
|
|
1151
|
+
test('preserves a successful automatic top-up when the paid retry fails', async () => {
|
|
1152
|
+
const { map, store } = makeChannelStore()
|
|
1153
|
+
const payloads: SessionCredentialPayload[] = []
|
|
1154
|
+
const mockFetch = vi.fn().mockImplementation((_input, init?: RequestInit) => {
|
|
1155
|
+
const authorization = new Headers(init?.headers).get(Constants.Headers.authorization)
|
|
1156
|
+
if (!authorization)
|
|
1157
|
+
return Promise.resolve(make402Response(makeChallenge({ suggestedDeposit: '1000000' })))
|
|
1158
|
+
const payload = Credential.deserialize<SessionCredentialPayload>(authorization).payload
|
|
1159
|
+
payloads.push(payload)
|
|
1160
|
+
if (payload.action === 'open')
|
|
1161
|
+
return Promise.resolve(
|
|
1162
|
+
new Response('ok', {
|
|
1163
|
+
headers: {
|
|
1164
|
+
[Constants.Headers.paymentReceipt]: serializeSessionReceipt(
|
|
1165
|
+
createSessionReceipt({
|
|
1166
|
+
acceptedCumulative: 1_000_000n,
|
|
1167
|
+
challengeId,
|
|
1168
|
+
channelId: payload.channelId,
|
|
1169
|
+
spent: 1_000_000n,
|
|
1170
|
+
units: 3,
|
|
1171
|
+
}),
|
|
1172
|
+
),
|
|
1173
|
+
},
|
|
1174
|
+
}),
|
|
1175
|
+
)
|
|
1176
|
+
if (payload.action === 'topUp') return Promise.resolve(new Response(null, { status: 204 }))
|
|
1177
|
+
return Promise.resolve(make402Response(makeChallenge({ suggestedDeposit: '1000000' })))
|
|
1178
|
+
})
|
|
1179
|
+
const s = sessionManager({
|
|
1180
|
+
account,
|
|
1181
|
+
client,
|
|
1182
|
+
fetch: mockFetch as typeof globalThis.fetch,
|
|
1183
|
+
maxDeposit: '3',
|
|
1184
|
+
channelStore: store,
|
|
1185
|
+
})
|
|
1186
|
+
|
|
1187
|
+
await s.fetch('https://api.example.com/data')
|
|
1188
|
+
expect((await s.fetch('https://api.example.com/data')).status).toBe(402)
|
|
1189
|
+
|
|
1190
|
+
expect(payloads.map((payload) => payload.action)).toEqual([
|
|
1191
|
+
'open',
|
|
1192
|
+
'topUp',
|
|
1193
|
+
'voucher',
|
|
1194
|
+
'voucher',
|
|
1195
|
+
'voucher',
|
|
1196
|
+
])
|
|
1197
|
+
expect(s.cumulative).toBe(1_000_000n)
|
|
1198
|
+
expect(s.state).toMatchObject({ status: 'active', deposit: '2000000', units: 3 })
|
|
1199
|
+
expect([...map.values()][0]).toMatchObject({
|
|
1200
|
+
cumulativeAmount: 1_000_000n,
|
|
1201
|
+
deposit: 2_000_000n,
|
|
1202
|
+
})
|
|
1203
|
+
})
|
|
1204
|
+
|
|
1205
|
+
test('preserves a topped-up resumed channel in durable storage after payment fails', async () => {
|
|
1206
|
+
const { store } = makeDurableChannelStore()
|
|
1207
|
+
const seeded = channelEntry({ cumulativeAmount: 1_000_000n, deposit: 1_000_000n })
|
|
1208
|
+
await store.set(seeded)
|
|
1209
|
+
const payloads: SessionCredentialPayload[] = []
|
|
1210
|
+
const mockFetch = vi.fn().mockImplementation((_input, init?: RequestInit) => {
|
|
1211
|
+
const authorization = new Headers(init?.headers).get(Constants.Headers.authorization)
|
|
1212
|
+
if (!authorization)
|
|
1213
|
+
return Promise.resolve(make402Response(makeChallenge({ suggestedDeposit: '1000000' })))
|
|
1214
|
+
const payload = Credential.deserialize<SessionCredentialPayload>(authorization).payload
|
|
1215
|
+
payloads.push(payload)
|
|
1216
|
+
return Promise.resolve(
|
|
1217
|
+
payload.action === 'topUp'
|
|
1218
|
+
? new Response(null, { status: 204 })
|
|
1219
|
+
: new Response('failed', { status: 500 }),
|
|
1220
|
+
)
|
|
1221
|
+
})
|
|
1222
|
+
const s = sessionManager({
|
|
1223
|
+
account,
|
|
1224
|
+
client,
|
|
1225
|
+
fetch: mockFetch as typeof globalThis.fetch,
|
|
1226
|
+
maxDeposit: '3',
|
|
1227
|
+
channelStore: store,
|
|
1228
|
+
})
|
|
1229
|
+
|
|
1230
|
+
expect((await s.fetch('https://api.example.com/data')).status).toBe(500)
|
|
1231
|
+
|
|
1232
|
+
expect(payloads.map((payload) => payload.action)).toEqual(['topUp', 'voucher'])
|
|
1233
|
+
expect(s.cumulative).toBe(1_000_000n)
|
|
1234
|
+
expect(s.state).toMatchObject({ status: 'active', deposit: '2000000' })
|
|
1235
|
+
await expect(store.get(entryKey(seeded))).resolves.toMatchObject({
|
|
1236
|
+
cumulativeAmount: 1_000_000n,
|
|
1237
|
+
deposit: 2_000_000n,
|
|
1238
|
+
opened: true,
|
|
1239
|
+
})
|
|
1240
|
+
})
|
|
1241
|
+
|
|
1242
|
+
test.each([
|
|
1243
|
+
['new', false],
|
|
1244
|
+
['replacement', true],
|
|
1245
|
+
] as const)('removes a failed %s durable channel', async (_kind, seeded) => {
|
|
1246
|
+
const { backend, store } = makeDurableChannelStore()
|
|
1247
|
+
if (seeded) await store.set(channelEntry())
|
|
1248
|
+
const mockFetch = vi.fn().mockImplementation((_input, init?: RequestInit) => {
|
|
1249
|
+
const authorization = new Headers(init?.headers).get(Constants.Headers.authorization)
|
|
1250
|
+
return Promise.resolve(
|
|
1251
|
+
authorization ? new Response('failed', { status: 500 }) : make402Response(),
|
|
1252
|
+
)
|
|
1253
|
+
})
|
|
1254
|
+
const s = sessionManager({
|
|
1255
|
+
account,
|
|
1256
|
+
client,
|
|
1257
|
+
fetch: mockFetch as typeof globalThis.fetch,
|
|
1258
|
+
channelStore: store,
|
|
1259
|
+
})
|
|
1260
|
+
|
|
1261
|
+
expect((await s.fetch('https://api.example.com/data')).status).toBe(500)
|
|
1262
|
+
expect(s.channelId).toBeUndefined()
|
|
1263
|
+
expect(backend.size).toBe(0)
|
|
1264
|
+
})
|
|
1005
1265
|
})
|
|
1006
1266
|
|
|
1007
1267
|
describe('.topUp()', () => {
|
|
@@ -1398,6 +1658,51 @@ describe('Session', () => {
|
|
|
1398
1658
|
})
|
|
1399
1659
|
})
|
|
1400
1660
|
|
|
1661
|
+
describe('.ws()', () => {
|
|
1662
|
+
test('tops up a full persisted channel before constructing the socket', async () => {
|
|
1663
|
+
const events: string[] = []
|
|
1664
|
+
const { map, set, store } = makeChannelStore([
|
|
1665
|
+
channelEntry({ cumulativeAmount: 1_000_000n, deposit: 1_000_000n }),
|
|
1666
|
+
])
|
|
1667
|
+
set.mockImplementation((entry) => {
|
|
1668
|
+
map.set(entryKey(entry), entry)
|
|
1669
|
+
events.push(`set:${entry.deposit}:${entry.cumulativeAmount}`)
|
|
1670
|
+
})
|
|
1671
|
+
const fetch = vi.fn(async (_input, init?: RequestInit) => {
|
|
1672
|
+
const authorization = new Headers(init?.headers).get(Constants.Headers.authorization)
|
|
1673
|
+
if (!authorization) {
|
|
1674
|
+
events.push('probe')
|
|
1675
|
+
return make402Response(makeChallenge({ suggestedDeposit: '1000000' }))
|
|
1676
|
+
}
|
|
1677
|
+
const payload = Credential.deserialize<SessionCredentialPayload>(authorization).payload
|
|
1678
|
+
if (payload.action !== 'topUp') throw new Error(`unexpected ${payload.action} fetch`)
|
|
1679
|
+
events.push('topUp')
|
|
1680
|
+
return new Response(null, { status: 204 })
|
|
1681
|
+
})
|
|
1682
|
+
function WebSocket() {
|
|
1683
|
+
events.push('socket')
|
|
1684
|
+
throw new Error('stop after preparation')
|
|
1685
|
+
}
|
|
1686
|
+
const s = sessionManager({
|
|
1687
|
+
account,
|
|
1688
|
+
client,
|
|
1689
|
+
fetch: fetch as typeof globalThis.fetch,
|
|
1690
|
+
maxDeposit: '3',
|
|
1691
|
+
channelStore: store,
|
|
1692
|
+
webSocket: WebSocket as never,
|
|
1693
|
+
})
|
|
1694
|
+
|
|
1695
|
+
await expect(s.ws('wss://api.example.com/socket')).rejects.toThrow('stop after preparation')
|
|
1696
|
+
expect(events).toEqual([
|
|
1697
|
+
'probe',
|
|
1698
|
+
'topUp',
|
|
1699
|
+
'set:2000000:1000000',
|
|
1700
|
+
'set:2000000:2000000',
|
|
1701
|
+
'socket',
|
|
1702
|
+
])
|
|
1703
|
+
})
|
|
1704
|
+
})
|
|
1705
|
+
|
|
1401
1706
|
describe('.close()', () => {
|
|
1402
1707
|
test('is no-op when not opened', async () => {
|
|
1403
1708
|
const mockFetch = vi.fn()
|