mppx 0.8.6 → 0.8.8
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 +19 -0
- package/dist/cli/internal.js +2 -2
- package/dist/cli/internal.js.map +1 -1
- package/dist/cli/plugins/evm.d.ts +2 -0
- package/dist/cli/plugins/evm.d.ts.map +1 -0
- package/dist/cli/plugins/evm.js +51 -0
- package/dist/cli/plugins/evm.js.map +1 -0
- package/dist/cli/plugins/index.d.ts +1 -0
- package/dist/cli/plugins/index.d.ts.map +1 -1
- package/dist/cli/plugins/index.js +1 -0
- package/dist/cli/plugins/index.js.map +1 -1
- package/dist/cli/plugins/stripe.d.ts.map +1 -1
- package/dist/cli/plugins/stripe.js +3 -2
- package/dist/cli/plugins/stripe.js.map +1 -1
- 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 +191 -84
- package/dist/cli/validate/challenge.js.map +1 -1
- package/dist/cli/validate/discovery.d.ts +1 -0
- package/dist/cli/validate/discovery.d.ts.map +1 -1
- package/dist/cli/validate/discovery.js +99 -35
- package/dist/cli/validate/discovery.js.map +1 -1
- package/dist/cli/validate/helpers.d.ts +14 -1
- package/dist/cli/validate/helpers.d.ts.map +1 -1
- package/dist/cli/validate/helpers.js +0 -13
- 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 +102 -153
- package/dist/cli/validate/index.js.map +1 -1
- package/dist/cli/validate/payment.d.ts +4 -1
- package/dist/cli/validate/payment.d.ts.map +1 -1
- package/dist/cli/validate/payment.js +312 -106
- package/dist/cli/validate/payment.js.map +1 -1
- package/dist/evm/Chains.d.ts +4 -0
- package/dist/evm/Chains.d.ts.map +1 -1
- package/dist/evm/Chains.js +4 -0
- package/dist/evm/Chains.js.map +1 -1
- package/dist/internal/AcceptPayment.d.ts.map +1 -1
- package/dist/internal/AcceptPayment.js +7 -1
- package/dist/internal/AcceptPayment.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/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/server/internal/request-body.d.ts.map +1 -1
- package/dist/tempo/server/internal/request-body.js +2 -0
- package/dist/tempo/server/internal/request-body.js.map +1 -1
- package/dist/tempo/session/server/ChannelStore.js +4 -4
- package/dist/tempo/session/server/ChannelStore.js.map +1 -1
- package/dist/tempo/session/server/CredentialVerification.d.ts +8 -0
- package/dist/tempo/session/server/CredentialVerification.d.ts.map +1 -1
- package/dist/tempo/session/server/CredentialVerification.js +21 -1
- package/dist/tempo/session/server/CredentialVerification.js.map +1 -1
- package/dist/tempo/session/server/Session.d.ts.map +1 -1
- package/dist/tempo/session/server/Session.js +1 -0
- package/dist/tempo/session/server/Session.js.map +1 -1
- package/dist/validation/core.d.ts +75 -0
- package/dist/validation/core.d.ts.map +1 -0
- package/dist/validation/core.js +269 -0
- package/dist/validation/core.js.map +1 -0
- package/dist/validation/index.d.ts +3 -0
- package/dist/validation/index.d.ts.map +1 -0
- package/dist/validation/index.js +2 -0
- package/dist/validation/index.js.map +1 -0
- package/dist/x402/Assets.d.ts +9 -0
- package/dist/x402/Assets.d.ts.map +1 -1
- package/dist/x402/Assets.js +38 -0
- package/dist/x402/Assets.js.map +1 -1
- package/package.json +9 -4
- package/src/cli/internal.ts +2 -2
- package/src/cli/plugins/evm.ts +55 -0
- package/src/cli/plugins/index.ts +1 -0
- package/src/cli/plugins/stripe.ts +3 -2
- package/src/cli/validate/challenge.ts +345 -127
- package/src/cli/validate/discovery.test.ts +307 -0
- package/src/cli/validate/discovery.ts +104 -38
- package/src/cli/validate/helpers.ts +15 -13
- package/src/cli/validate/index.ts +121 -242
- package/src/cli/validate/payment.ts +413 -144
- package/src/cli/validate.test.ts +237 -3
- package/src/client/internal/Fetch.test.ts +142 -2
- package/src/evm/Chains.ts +6 -0
- package/src/evm/PublicInterface.test-d.ts +7 -0
- package/src/internal/AcceptPayment.test.ts +45 -0
- package/src/internal/AcceptPayment.ts +11 -1
- package/src/stripe/server/internal/html/package.json +1 -1
- package/src/stripe/server/internal/html.gen.ts +1 -1
- package/src/tempo/server/internal/html/package.json +1 -1
- package/src/tempo/server/internal/html.gen.ts +1 -1
- package/src/tempo/server/internal/request-body.test.ts +9 -0
- package/src/tempo/server/internal/request-body.ts +2 -0
- package/src/tempo/session/server/ChannelStore.test.ts +17 -0
- package/src/tempo/session/server/ChannelStore.ts +4 -4
- package/src/tempo/session/server/CredentialVerification.test.ts +118 -1
- package/src/tempo/session/server/CredentialVerification.ts +30 -0
- package/src/tempo/session/server/Session.integration.test.ts +5 -0
- package/src/tempo/session/server/Session.test.ts +119 -53
- package/src/tempo/session/server/Session.ts +1 -0
- package/src/validation/core.ts +374 -0
- package/src/validation/index.ts +11 -0
- package/src/x402/Assets.test.ts +42 -0
- package/src/x402/Assets.ts +40 -0
- package/src/x402/Exact.localnet.test.ts +475 -0
|
@@ -27,6 +27,15 @@ describe('request-body', () => {
|
|
|
27
27
|
).toBe(true)
|
|
28
28
|
})
|
|
29
29
|
|
|
30
|
+
test('returns false for empty body streams with content-length zero', () => {
|
|
31
|
+
expect(
|
|
32
|
+
hasCapturedRequestBody({
|
|
33
|
+
hasBody: true,
|
|
34
|
+
headers: new Headers({ 'content-length': '0' }),
|
|
35
|
+
}),
|
|
36
|
+
).toBe(false)
|
|
37
|
+
})
|
|
38
|
+
|
|
30
39
|
test('returns true when content-length is non-zero', () => {
|
|
31
40
|
expect(
|
|
32
41
|
hasCapturedRequestBody({
|
|
@@ -19,6 +19,8 @@ export function captureRequestBodyProbe(input: Request): RequestBodyProbe {
|
|
|
19
19
|
export function hasCapturedRequestBody(
|
|
20
20
|
input: Pick<RequestBodyProbe, 'headers' | 'hasBody'>,
|
|
21
21
|
): boolean {
|
|
22
|
+
if (input.headers.get('content-length') === '0' && !input.headers.has('transfer-encoding'))
|
|
23
|
+
return false
|
|
22
24
|
if (hasBodyFramingHeaders(input)) return true
|
|
23
25
|
if (input.hasBody === true) return true
|
|
24
26
|
return false
|
|
@@ -559,6 +559,23 @@ describe('ChannelStore voucher acceptance', () => {
|
|
|
559
559
|
expect((await store.getChannel(stateUpdateChannelId))?.highestVoucherAmount).toBe(50n)
|
|
560
560
|
})
|
|
561
561
|
|
|
562
|
+
test('rejects an already accepted voucher when it has settled on-chain', async () => {
|
|
563
|
+
vi.spyOn(Voucher, 'verifyVoucher').mockReturnValue(true)
|
|
564
|
+
const stored = voucherAcceptanceChannel({ highestVoucherAmount: 50n })
|
|
565
|
+
|
|
566
|
+
await expect(
|
|
567
|
+
ChannelStore.verifyAndAcceptVoucher({
|
|
568
|
+
challenge: voucherAcceptanceChallenge,
|
|
569
|
+
channel: stored,
|
|
570
|
+
channelState: { deposit: 100n, settled: 50n, closeRequestedAt: 0 },
|
|
571
|
+
methodDetails: { chainId: 4217, escrowContract: voucherAcceptanceEscrow },
|
|
572
|
+
minVoucherDelta: 5n,
|
|
573
|
+
store: memoryChannelStore(stored),
|
|
574
|
+
voucher: voucherAcceptanceVoucher(50n),
|
|
575
|
+
}),
|
|
576
|
+
).rejects.toThrow(VerificationFailedError)
|
|
577
|
+
})
|
|
578
|
+
|
|
562
579
|
test('rejects vouchers beyond deposit or below minimum delta', async () => {
|
|
563
580
|
vi.spyOn(Voucher, 'verifyVoucher').mockReturnValue(true)
|
|
564
581
|
const stored = voucherAcceptanceChannel({ highestVoucherAmount: 50n })
|
|
@@ -542,6 +542,10 @@ export async function verifyAndAcceptVoucher(
|
|
|
542
542
|
)
|
|
543
543
|
if (!valid) throw new InvalidSignatureError({ reason: 'invalid voucher signature' })
|
|
544
544
|
|
|
545
|
+
if (voucher.cumulativeAmount <= channelState.settled)
|
|
546
|
+
throw new VerificationFailedError({
|
|
547
|
+
reason: 'voucher cumulativeAmount is below on-chain settled amount',
|
|
548
|
+
})
|
|
545
549
|
if (voucher.cumulativeAmount === channel.highestVoucherAmount)
|
|
546
550
|
return createSessionReceipt({
|
|
547
551
|
challengeId: challenge.id,
|
|
@@ -550,10 +554,6 @@ export async function verifyAndAcceptVoucher(
|
|
|
550
554
|
spent: channel.spent,
|
|
551
555
|
units: channel.units,
|
|
552
556
|
})
|
|
553
|
-
if (voucher.cumulativeAmount <= channelState.settled)
|
|
554
|
-
throw new VerificationFailedError({
|
|
555
|
-
reason: 'voucher cumulativeAmount is below on-chain settled amount',
|
|
556
|
-
})
|
|
557
557
|
const delta = voucher.cumulativeAmount - channel.highestVoucherAmount
|
|
558
558
|
if (delta < minVoucherDelta)
|
|
559
559
|
throw new DeltaTooSmallError({
|
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
import type { Address, Hex } from 'viem'
|
|
2
|
-
import { describe, expect, test } from 'vp/test'
|
|
2
|
+
import { afterEach, describe, expect, test, vi } from 'vp/test'
|
|
3
3
|
|
|
4
|
+
import * as Challenge from '../../../Challenge.js'
|
|
5
|
+
import * as Store from '../../../Store.js'
|
|
6
|
+
import { chainId as chainIds } from '../../internal/defaults.js'
|
|
4
7
|
import * as Channel from '../precompile/Channel.js'
|
|
8
|
+
import * as Voucher from '../precompile/Voucher.js'
|
|
9
|
+
import * as ChannelStore from './ChannelStore.js'
|
|
5
10
|
import {
|
|
6
11
|
assertOpenCredentialCoversRequest,
|
|
7
12
|
requireSessionCredentialAction,
|
|
8
13
|
requireSessionCredentialPayload,
|
|
9
14
|
requireSessionCredentialPayloadHeader,
|
|
10
15
|
validateChannelDescriptor,
|
|
16
|
+
verifyCredentialPayload,
|
|
11
17
|
} from './CredentialVerification.js'
|
|
12
18
|
|
|
13
19
|
describe('SessionCredentialGuards', () => {
|
|
@@ -24,6 +30,10 @@ describe('SessionCredentialGuards', () => {
|
|
|
24
30
|
const signature = `0x${'ab'.repeat(65)}` as Hex
|
|
25
31
|
const transaction = `0x${'cd'.repeat(32)}` as Hex
|
|
26
32
|
|
|
33
|
+
afterEach(() => {
|
|
34
|
+
vi.restoreAllMocks()
|
|
35
|
+
})
|
|
36
|
+
|
|
27
37
|
describe('SessionCredentialGuards', () => {
|
|
28
38
|
test('reads valid action discriminators', () => {
|
|
29
39
|
expect(requireSessionCredentialAction({ action: 'open' })).toBe('open')
|
|
@@ -179,4 +189,111 @@ describe('SessionCredentialGuards', () => {
|
|
|
179
189
|
).toThrow(case_.expected)
|
|
180
190
|
})
|
|
181
191
|
})
|
|
192
|
+
|
|
193
|
+
describe('verifyCredentialPayload', () => {
|
|
194
|
+
const escrow = '0x4D50500000000000000000000000000000000000' as Address
|
|
195
|
+
const testChainId = chainIds.testnet
|
|
196
|
+
const computedChannelId = Channel.computeId({ ...descriptor, chainId: testChainId, escrow })
|
|
197
|
+
const challenge = Challenge.from({
|
|
198
|
+
id: 'credential-source-test',
|
|
199
|
+
realm: 'example.test',
|
|
200
|
+
method: 'tempo',
|
|
201
|
+
intent: 'session',
|
|
202
|
+
request: {
|
|
203
|
+
amount: '1',
|
|
204
|
+
currency: descriptor.token,
|
|
205
|
+
recipient: descriptor.payee,
|
|
206
|
+
unitType: 'request',
|
|
207
|
+
},
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
function channelStore(): ChannelStore.ChannelStore {
|
|
211
|
+
return ChannelStore.fromStore(Store.memory())
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async function seedChannel(store: ChannelStore.ChannelStore) {
|
|
215
|
+
await store.updateChannel(computedChannelId, () => ({
|
|
216
|
+
backend: 'precompile',
|
|
217
|
+
authorizedSigner: descriptor.authorizedSigner,
|
|
218
|
+
chainId: testChainId,
|
|
219
|
+
channelId: computedChannelId,
|
|
220
|
+
closeRequestedAt: 0n,
|
|
221
|
+
createdAt: '2026-01-01T00:00:00.000Z',
|
|
222
|
+
deposit: 100n,
|
|
223
|
+
descriptor,
|
|
224
|
+
escrowContract: escrow,
|
|
225
|
+
expiringNonceHash: descriptor.expiringNonceHash,
|
|
226
|
+
finalized: false,
|
|
227
|
+
highestVoucher: null,
|
|
228
|
+
highestVoucherAmount: 50n,
|
|
229
|
+
operator: descriptor.operator,
|
|
230
|
+
payee: descriptor.payee,
|
|
231
|
+
payer: descriptor.payer,
|
|
232
|
+
salt: descriptor.salt,
|
|
233
|
+
settledOnChain: 0n,
|
|
234
|
+
spent: 0n,
|
|
235
|
+
token: descriptor.token,
|
|
236
|
+
units: 0,
|
|
237
|
+
}))
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
test.each([
|
|
241
|
+
{ label: 'payer', sourceAddress: descriptor.payer },
|
|
242
|
+
{ label: 'authorized signer', sourceAddress: descriptor.authorizedSigner },
|
|
243
|
+
])('accepts voucher credentials from the channel $label', async ({ sourceAddress }) => {
|
|
244
|
+
const store = channelStore()
|
|
245
|
+
await seedChannel(store)
|
|
246
|
+
const verifyVoucher = vi.spyOn(Voucher, 'verifyVoucher').mockResolvedValue(true)
|
|
247
|
+
|
|
248
|
+
await expect(
|
|
249
|
+
verifyCredentialPayload({
|
|
250
|
+
challenge,
|
|
251
|
+
channelStateTtl: 60_000,
|
|
252
|
+
chainId: testChainId,
|
|
253
|
+
client: {} as never,
|
|
254
|
+
credentialSource: `did:pkh:eip155:${testChainId}:${sourceAddress}`,
|
|
255
|
+
escrow,
|
|
256
|
+
lastOnChainVerified: new Map([[computedChannelId, Date.now()]]),
|
|
257
|
+
minVoucherDelta: 1n,
|
|
258
|
+
payload: {
|
|
259
|
+
action: 'voucher',
|
|
260
|
+
channelId: computedChannelId,
|
|
261
|
+
cumulativeAmount: '60',
|
|
262
|
+
descriptor,
|
|
263
|
+
signature,
|
|
264
|
+
},
|
|
265
|
+
store,
|
|
266
|
+
}),
|
|
267
|
+
).resolves.toMatchObject({ acceptedCumulative: '60' })
|
|
268
|
+
expect(verifyVoucher).toHaveBeenCalledOnce()
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
test('rejects voucher credentials from a different source', async () => {
|
|
272
|
+
const store = channelStore()
|
|
273
|
+
await seedChannel(store)
|
|
274
|
+
const verifyVoucher = vi.spyOn(Voucher, 'verifyVoucher').mockResolvedValue(true)
|
|
275
|
+
|
|
276
|
+
await expect(
|
|
277
|
+
verifyCredentialPayload({
|
|
278
|
+
challenge,
|
|
279
|
+
channelStateTtl: 60_000,
|
|
280
|
+
chainId: testChainId,
|
|
281
|
+
client: {} as never,
|
|
282
|
+
credentialSource: `did:pkh:eip155:${testChainId}:0x0000000000000000000000000000000000000099`,
|
|
283
|
+
escrow,
|
|
284
|
+
lastOnChainVerified: new Map([[computedChannelId, Date.now()]]),
|
|
285
|
+
minVoucherDelta: 1n,
|
|
286
|
+
payload: {
|
|
287
|
+
action: 'voucher',
|
|
288
|
+
channelId: computedChannelId,
|
|
289
|
+
cumulativeAmount: '60',
|
|
290
|
+
descriptor,
|
|
291
|
+
signature,
|
|
292
|
+
},
|
|
293
|
+
store,
|
|
294
|
+
}),
|
|
295
|
+
).rejects.toThrow('credential source does not match channel payer or authorized signer')
|
|
296
|
+
expect(verifyVoucher).not.toHaveBeenCalled()
|
|
297
|
+
})
|
|
298
|
+
})
|
|
182
299
|
})
|
|
@@ -126,6 +126,32 @@ export function assertOpenCredentialCoversRequest(parameters: {
|
|
|
126
126
|
throw new VerificationFailedError({ reason: 'voucher amount is less than request amount' })
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
+
/** Verifies that the credential source is authorized to spend from the channel. */
|
|
130
|
+
export function assertCredentialSourceCanSpend(parameters: {
|
|
131
|
+
chainId: number
|
|
132
|
+
channel: Pick<ChannelStore.State, 'authorizedSigner' | 'payer'>
|
|
133
|
+
source?: string | undefined
|
|
134
|
+
}): void {
|
|
135
|
+
const sourceAddress = readCredentialSourceAddress(parameters.source, parameters.chainId)
|
|
136
|
+
if (
|
|
137
|
+
isAddressEqual(sourceAddress, parameters.channel.payer) ||
|
|
138
|
+
isAddressEqual(sourceAddress, parameters.channel.authorizedSigner)
|
|
139
|
+
)
|
|
140
|
+
return
|
|
141
|
+
throw new VerificationFailedError({
|
|
142
|
+
reason: 'credential source does not match channel payer or authorized signer',
|
|
143
|
+
})
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function readCredentialSourceAddress(source: string | undefined, chainId: number): Address {
|
|
147
|
+
const prefix = `did:pkh:eip155:${chainId}:`
|
|
148
|
+
if (!source?.startsWith(prefix))
|
|
149
|
+
throw new VerificationFailedError({ reason: 'credential source does not match channel' })
|
|
150
|
+
const address = source.slice(prefix.length)
|
|
151
|
+
if (isAddress(address, { strict: false })) return address
|
|
152
|
+
throw new VerificationFailedError({ reason: 'invalid credential source' })
|
|
153
|
+
}
|
|
154
|
+
|
|
129
155
|
const sessionCredentialActions = [
|
|
130
156
|
'open',
|
|
131
157
|
'topUp',
|
|
@@ -302,6 +328,8 @@ export type VerifyCredentialPayloadParameters = {
|
|
|
302
328
|
chainId: number
|
|
303
329
|
/** viem client used for precompile reads and transaction broadcasts. */
|
|
304
330
|
client: Chain.TransactionClient
|
|
331
|
+
/** Optional payer identifier from the HTTP credential source field. */
|
|
332
|
+
credentialSource?: string | undefined
|
|
305
333
|
/** TIP20EscrowChannel precompile address for this session method. */
|
|
306
334
|
escrow: Address
|
|
307
335
|
/** Operator address advertised in the HMAC-bound challenge details. */
|
|
@@ -531,6 +559,7 @@ async function handleVoucherCredential(
|
|
|
531
559
|
store,
|
|
532
560
|
client,
|
|
533
561
|
challenge,
|
|
562
|
+
credentialSource,
|
|
534
563
|
payload,
|
|
535
564
|
chainId,
|
|
536
565
|
escrow,
|
|
@@ -564,6 +593,7 @@ async function handleVoucherCredential(
|
|
|
564
593
|
store,
|
|
565
594
|
validateDescriptor: true,
|
|
566
595
|
})
|
|
596
|
+
assertCredentialSourceCanSpend({ chainId, channel, source: credentialSource })
|
|
567
597
|
if (channel.finalized) throw new ChannelClosedError({ reason: 'channel is finalized' })
|
|
568
598
|
const isStale = Date.now() - (lastOnChainVerified.get(channelId) ?? 0) > channelStateTtl
|
|
569
599
|
const state = isStale ? await Chain.getChannelState(client, channelId, escrow) : undefined
|
|
@@ -84,6 +84,10 @@ function sessionRequest(channelId: Hex.Hex, amount = '100') {
|
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
function sourceFor(account = payer): string {
|
|
88
|
+
return `did:pkh:eip155:${chain.id}:${account.address}`
|
|
89
|
+
}
|
|
90
|
+
|
|
87
91
|
describe.runIf(isPrecompileTestnet)('precompile server session chain integration', () => {
|
|
88
92
|
test('broadcasts and verifies a real precompile open credential', async () => {
|
|
89
93
|
const rawStore = Store.memory()
|
|
@@ -247,6 +251,7 @@ describe.runIf(isPrecompileTestnet)('precompile server session chain integration
|
|
|
247
251
|
request: sessionRequest(channelId),
|
|
248
252
|
} as never,
|
|
249
253
|
payload,
|
|
254
|
+
source: sourceFor(),
|
|
250
255
|
},
|
|
251
256
|
request: sessionRequest(channelId) as never,
|
|
252
257
|
})
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import * as node_http from 'node:http'
|
|
2
2
|
|
|
3
|
+
import { serve } from '@hono/node-server'
|
|
4
|
+
import { Hono } from 'hono'
|
|
3
5
|
import { Challenge, Constants, Credential } from 'mppx'
|
|
4
6
|
import { Mppx as Mppx_server, tempo as tempo_server } from 'mppx/server'
|
|
5
7
|
import {
|
|
@@ -235,6 +237,18 @@ function verifyRequestWithFeePayer(channelId: Hex, feePayer: typeof payer): Veri
|
|
|
235
237
|
} as unknown as VerifyRequest
|
|
236
238
|
}
|
|
237
239
|
|
|
240
|
+
function sourceFor(account = payer): string {
|
|
241
|
+
return `did:pkh:eip155:${chainId}:${account.address}`
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function voucherCredential(payload: SessionCredentialPayload, channelId: Hex) {
|
|
245
|
+
return {
|
|
246
|
+
challenge: makeChallenge(channelId),
|
|
247
|
+
payload,
|
|
248
|
+
source: sourceFor(),
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
238
252
|
let saltCounter = 0
|
|
239
253
|
|
|
240
254
|
async function createOpenPayload(
|
|
@@ -1434,10 +1448,7 @@ describe('precompile server session unit guardrails', () => {
|
|
|
1434
1448
|
)
|
|
1435
1449
|
|
|
1436
1450
|
const receipt = (await method.verify({
|
|
1437
|
-
credential:
|
|
1438
|
-
challenge: makeChallenge(openPayload.channelId),
|
|
1439
|
-
payload: voucher,
|
|
1440
|
-
},
|
|
1451
|
+
credential: voucherCredential(voucher, openPayload.channelId),
|
|
1441
1452
|
request: verifyRequest(openPayload.channelId),
|
|
1442
1453
|
})) as SessionReceipt
|
|
1443
1454
|
|
|
@@ -1473,10 +1484,7 @@ describe('precompile server session unit guardrails', () => {
|
|
|
1473
1484
|
})
|
|
1474
1485
|
|
|
1475
1486
|
const receipt = (await method.verify({
|
|
1476
|
-
credential:
|
|
1477
|
-
challenge: makeChallenge(openPayload.channelId),
|
|
1478
|
-
payload: voucher,
|
|
1479
|
-
},
|
|
1487
|
+
credential: voucherCredential(voucher, openPayload.channelId),
|
|
1480
1488
|
request: verifyRequest(openPayload.channelId),
|
|
1481
1489
|
})) as SessionReceipt
|
|
1482
1490
|
|
|
@@ -1506,10 +1514,7 @@ describe('precompile server session unit guardrails', () => {
|
|
|
1506
1514
|
|
|
1507
1515
|
await expect(
|
|
1508
1516
|
method.verify({
|
|
1509
|
-
credential:
|
|
1510
|
-
challenge: makeChallenge(openPayload.channelId),
|
|
1511
|
-
payload: voucher,
|
|
1512
|
-
},
|
|
1517
|
+
credential: voucherCredential(voucher, openPayload.channelId),
|
|
1513
1518
|
request: verifyRequest(openPayload.channelId),
|
|
1514
1519
|
}),
|
|
1515
1520
|
).rejects.toThrow(
|
|
@@ -1534,16 +1539,13 @@ describe('precompile server session unit guardrails', () => {
|
|
|
1534
1539
|
|
|
1535
1540
|
await expect(
|
|
1536
1541
|
method.verify({
|
|
1537
|
-
credential:
|
|
1538
|
-
challenge: makeChallenge(openPayload.channelId),
|
|
1539
|
-
payload: voucher,
|
|
1540
|
-
},
|
|
1542
|
+
credential: voucherCredential(voucher, openPayload.channelId),
|
|
1541
1543
|
request: verifyRequest(openPayload.channelId),
|
|
1542
1544
|
}),
|
|
1543
1545
|
).rejects.toThrow(/voucher delta 150 below minimum 200/)
|
|
1544
1546
|
})
|
|
1545
1547
|
|
|
1546
|
-
test('
|
|
1548
|
+
test('rejects idempotent precompile voucher replay after on-chain settlement catches up', async () => {
|
|
1547
1549
|
const rawStore = Store.memory()
|
|
1548
1550
|
const store = channelStore(rawStore)
|
|
1549
1551
|
const openPayload = await createOpenPayload({ initialAmount: 100n })
|
|
@@ -1572,17 +1574,12 @@ describe('precompile server session unit guardrails', () => {
|
|
|
1572
1574
|
createStateClient(payer, { settled: 500n, deposit: 1_000n, closeRequestedAt: 0 }),
|
|
1573
1575
|
})
|
|
1574
1576
|
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
},
|
|
1580
|
-
|
|
1581
|
-
})) as SessionReceipt
|
|
1582
|
-
|
|
1583
|
-
expect(receipt.acceptedCumulative).toBe('500')
|
|
1584
|
-
expect(receipt.spent).toBe('500')
|
|
1585
|
-
expect(receipt.units).toBe(10)
|
|
1577
|
+
await expect(
|
|
1578
|
+
method.verify({
|
|
1579
|
+
credential: voucherCredential(voucher, openPayload.channelId),
|
|
1580
|
+
request: verifyRequest(openPayload.channelId),
|
|
1581
|
+
}),
|
|
1582
|
+
).rejects.toThrow(/below on-chain settled amount/)
|
|
1586
1583
|
})
|
|
1587
1584
|
|
|
1588
1585
|
test('rejects stale or hijacked precompile voucher signatures', async () => {
|
|
@@ -1599,16 +1596,16 @@ describe('precompile server session unit guardrails', () => {
|
|
|
1599
1596
|
|
|
1600
1597
|
await expect(
|
|
1601
1598
|
method.verify({
|
|
1602
|
-
credential:
|
|
1603
|
-
|
|
1604
|
-
payload: {
|
|
1599
|
+
credential: voucherCredential(
|
|
1600
|
+
{
|
|
1605
1601
|
action: 'voucher',
|
|
1606
1602
|
channelId: openPayload.channelId,
|
|
1607
1603
|
cumulativeAmount: '250',
|
|
1608
1604
|
descriptor: openPayload.descriptor,
|
|
1609
1605
|
signature,
|
|
1610
1606
|
},
|
|
1611
|
-
|
|
1607
|
+
openPayload.channelId,
|
|
1608
|
+
),
|
|
1612
1609
|
request: verifyRequest(openPayload.channelId),
|
|
1613
1610
|
}),
|
|
1614
1611
|
).rejects.toThrow(/invalid voucher signature/)
|
|
@@ -1628,10 +1625,7 @@ describe('precompile server session unit guardrails', () => {
|
|
|
1628
1625
|
|
|
1629
1626
|
await expect(
|
|
1630
1627
|
method.verify({
|
|
1631
|
-
credential:
|
|
1632
|
-
challenge: makeChallenge(openPayload.channelId),
|
|
1633
|
-
payload: voucher,
|
|
1634
|
-
},
|
|
1628
|
+
credential: voucherCredential(voucher, openPayload.channelId),
|
|
1635
1629
|
request: verifyRequest(openPayload.channelId),
|
|
1636
1630
|
}),
|
|
1637
1631
|
).rejects.toThrow(/exceeds.*deposit|insufficient channel deposit/)
|
|
@@ -1664,10 +1658,7 @@ describe('precompile server session unit guardrails', () => {
|
|
|
1664
1658
|
|
|
1665
1659
|
await expect(
|
|
1666
1660
|
method.verify({
|
|
1667
|
-
credential:
|
|
1668
|
-
challenge: makeChallenge(openPayload.channelId),
|
|
1669
|
-
payload: voucher,
|
|
1670
|
-
},
|
|
1661
|
+
credential: voucherCredential(voucher, openPayload.channelId),
|
|
1671
1662
|
request: verifyRequest(openPayload.channelId),
|
|
1672
1663
|
}),
|
|
1673
1664
|
).rejects.toThrow(/pending close request/)
|
|
@@ -1699,10 +1690,7 @@ describe('precompile server session unit guardrails', () => {
|
|
|
1699
1690
|
|
|
1700
1691
|
await expect(
|
|
1701
1692
|
method.verify({
|
|
1702
|
-
credential:
|
|
1703
|
-
challenge: makeChallenge(openPayload.channelId),
|
|
1704
|
-
payload: voucher,
|
|
1705
|
-
},
|
|
1693
|
+
credential: voucherCredential(voucher, openPayload.channelId),
|
|
1706
1694
|
request: verifyRequest(openPayload.channelId),
|
|
1707
1695
|
}),
|
|
1708
1696
|
).rejects.toThrow(/deposit is zero|channel deposit is zero|not found/)
|
|
@@ -1853,6 +1841,7 @@ describe('precompile server session unit guardrails', () => {
|
|
|
1853
1841
|
Authorization: Credential.serialize({
|
|
1854
1842
|
challenge: Challenge.fromResponse(first.challenge),
|
|
1855
1843
|
payload: voucher,
|
|
1844
|
+
source: sourceFor(),
|
|
1856
1845
|
}),
|
|
1857
1846
|
},
|
|
1858
1847
|
}),
|
|
@@ -1874,6 +1863,7 @@ describe('precompile server session unit guardrails', () => {
|
|
|
1874
1863
|
Authorization: Credential.serialize({
|
|
1875
1864
|
challenge: Challenge.fromResponse(replayChallenge.challenge),
|
|
1876
1865
|
payload: voucher,
|
|
1866
|
+
source: sourceFor(),
|
|
1877
1867
|
}),
|
|
1878
1868
|
},
|
|
1879
1869
|
}),
|
|
@@ -1915,6 +1905,7 @@ describe('precompile server session unit guardrails', () => {
|
|
|
1915
1905
|
Credential.serialize({
|
|
1916
1906
|
challenge: Challenge.fromResponse(first.challenge),
|
|
1917
1907
|
payload: voucher,
|
|
1908
|
+
source: sourceFor(),
|
|
1918
1909
|
}),
|
|
1919
1910
|
),
|
|
1920
1911
|
)
|
|
@@ -1933,6 +1924,7 @@ describe('precompile server session unit guardrails', () => {
|
|
|
1933
1924
|
Credential.serialize({
|
|
1934
1925
|
challenge: Challenge.fromResponse(replayChallenge.challenge),
|
|
1935
1926
|
payload: voucher,
|
|
1927
|
+
source: sourceFor(),
|
|
1936
1928
|
}),
|
|
1937
1929
|
),
|
|
1938
1930
|
)
|
|
@@ -1963,6 +1955,7 @@ describe('precompile server session unit guardrails', () => {
|
|
|
1963
1955
|
descriptor: openPayload.descriptor,
|
|
1964
1956
|
signature: (await createOpenPayload({ account: wrongPayer })).signature,
|
|
1965
1957
|
},
|
|
1958
|
+
source: sourceFor(),
|
|
1966
1959
|
}),
|
|
1967
1960
|
},
|
|
1968
1961
|
}),
|
|
@@ -2125,6 +2118,7 @@ describe('precompile server session unit guardrails', () => {
|
|
|
2125
2118
|
let voucherPosts = 0
|
|
2126
2119
|
const amount = options.amount ?? '1'
|
|
2127
2120
|
const maxDeposit = options.maxDeposit ?? 3n
|
|
2121
|
+
let deposit = maxDeposit
|
|
2128
2122
|
const unitType = options.unitType ?? 'token'
|
|
2129
2123
|
const route = Mppx_server.create({
|
|
2130
2124
|
methods: [
|
|
@@ -2151,12 +2145,19 @@ describe('precompile server session unit guardrails', () => {
|
|
|
2151
2145
|
receipt: transactionReceipt([
|
|
2152
2146
|
closedLog(payload.channelId, BigInt(payload.cumulativeAmount), 0n),
|
|
2153
2147
|
]),
|
|
2154
|
-
state: { settled: 0n, deposit
|
|
2148
|
+
state: { settled: 0n, deposit, closeRequestedAt: 0 },
|
|
2149
|
+
})
|
|
2150
|
+
}
|
|
2151
|
+
if (payload?.action === 'topUp') {
|
|
2152
|
+
deposit += BigInt(payload.additionalDeposit)
|
|
2153
|
+
return createServerClient([], payer, payload.channelId, {
|
|
2154
|
+
receipt: transactionReceipt([topUpLog(payload, deposit)]),
|
|
2155
|
+
state: { settled: 0n, deposit, closeRequestedAt: 0 },
|
|
2155
2156
|
})
|
|
2156
2157
|
}
|
|
2157
2158
|
return createStateClient(payer, {
|
|
2158
2159
|
settled: 0n,
|
|
2159
|
-
deposit
|
|
2160
|
+
deposit,
|
|
2160
2161
|
closeRequestedAt: 0,
|
|
2161
2162
|
})
|
|
2162
2163
|
},
|
|
@@ -2166,8 +2167,7 @@ describe('precompile server session unit guardrails', () => {
|
|
|
2166
2167
|
secretKey: 'test-secret-key-test-secret-key-32',
|
|
2167
2168
|
}).session({ amount, decimals: 0, suggestedDeposit: maxDeposit.toString(), unitType })
|
|
2168
2169
|
|
|
2169
|
-
const
|
|
2170
|
-
const request = new Request(input, init)
|
|
2170
|
+
const handle = async (request: Request) => {
|
|
2171
2171
|
currentPayload = undefined
|
|
2172
2172
|
if (request.headers.has('Authorization')) {
|
|
2173
2173
|
try {
|
|
@@ -2178,7 +2178,7 @@ describe('precompile server session unit guardrails', () => {
|
|
|
2178
2178
|
|
|
2179
2179
|
const result = await route(request)
|
|
2180
2180
|
if (result.status === 402) return result.challenge
|
|
2181
|
-
if (
|
|
2181
|
+
if (request.method === 'POST') return result.withReceipt()
|
|
2182
2182
|
|
|
2183
2183
|
if (request.headers.get('Accept')?.includes('text/event-stream')) {
|
|
2184
2184
|
if (unitType === 'request') {
|
|
@@ -2211,8 +2211,12 @@ describe('precompile server session unit guardrails', () => {
|
|
|
2211
2211
|
return result.withReceipt(new Response('ok'))
|
|
2212
2212
|
}
|
|
2213
2213
|
|
|
2214
|
+
const fetch = async (input: RequestInfo | URL, init?: RequestInit) =>
|
|
2215
|
+
handle(new Request(input, init))
|
|
2216
|
+
|
|
2214
2217
|
return {
|
|
2215
2218
|
fetch,
|
|
2219
|
+
handle,
|
|
2216
2220
|
rawStore,
|
|
2217
2221
|
get voucherPosts() {
|
|
2218
2222
|
return voucherPosts
|
|
@@ -2247,6 +2251,71 @@ describe('precompile server session unit guardrails', () => {
|
|
|
2247
2251
|
expect(persisted?.finalized).toBe(true)
|
|
2248
2252
|
})
|
|
2249
2253
|
|
|
2254
|
+
test('handles empty management POST streams through a Node adapter', async () => {
|
|
2255
|
+
const harness = createManagedSseFetch({ maxDeposit: 1n })
|
|
2256
|
+
const managementPosts: Array<{
|
|
2257
|
+
action: SessionCredentialPayload['action']
|
|
2258
|
+
contentLength: string | null
|
|
2259
|
+
hasBody: boolean
|
|
2260
|
+
status: number
|
|
2261
|
+
}> = []
|
|
2262
|
+
const app = new Hono()
|
|
2263
|
+
app.all('*', async (c) => {
|
|
2264
|
+
const request = c.req.raw
|
|
2265
|
+
const payload = request.headers.has('Authorization')
|
|
2266
|
+
? Credential.fromRequest<SessionCredentialPayload>(request).payload
|
|
2267
|
+
: undefined
|
|
2268
|
+
const response = await harness.handle(request)
|
|
2269
|
+
if (request.method === 'POST' && payload)
|
|
2270
|
+
managementPosts.push({
|
|
2271
|
+
action: payload.action,
|
|
2272
|
+
contentLength: request.headers.get('content-length'),
|
|
2273
|
+
hasBody: request.body !== null,
|
|
2274
|
+
status: response.status,
|
|
2275
|
+
})
|
|
2276
|
+
return response
|
|
2277
|
+
})
|
|
2278
|
+
const server = serve({ fetch: app.fetch, port: 0 })
|
|
2279
|
+
await new Promise<void>((resolve) => server.once('listening', resolve))
|
|
2280
|
+
const address = server.address()
|
|
2281
|
+
if (!address || typeof address === 'string') throw new Error('expected TCP address')
|
|
2282
|
+
|
|
2283
|
+
try {
|
|
2284
|
+
const fetch: typeof globalThis.fetch = async (input, init) => {
|
|
2285
|
+
const response = await globalThis.fetch(input, init)
|
|
2286
|
+
return new Response(response.body, response)
|
|
2287
|
+
}
|
|
2288
|
+
const manager = precompileSessionManager({
|
|
2289
|
+
account: payer,
|
|
2290
|
+
client: createSigningClient(),
|
|
2291
|
+
decimals: 0,
|
|
2292
|
+
fetch,
|
|
2293
|
+
maxDeposit: '3',
|
|
2294
|
+
})
|
|
2295
|
+
|
|
2296
|
+
const chunks: string[] = []
|
|
2297
|
+
const stream = await manager.sse(`http://localhost:${address.port}/stream`)
|
|
2298
|
+
for await (const chunk of stream) chunks.push(chunk)
|
|
2299
|
+
|
|
2300
|
+
expect(chunks).toEqual(['chunk-1', 'chunk-2', 'chunk-3'])
|
|
2301
|
+
const closeReceipt = await manager.close()
|
|
2302
|
+
expect(closeReceipt?.spent).toBe('3')
|
|
2303
|
+
expect(managementPosts).toEqual(
|
|
2304
|
+
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 },
|
|
2308
|
+
]),
|
|
2309
|
+
)
|
|
2310
|
+
expect(managementPosts.every(({ status }) => status === 204)).toBe(true)
|
|
2311
|
+
|
|
2312
|
+
const persisted = await channelStore(harness.rawStore).getChannel(manager.channelId!)
|
|
2313
|
+
expect(persisted?.finalized).toBe(true)
|
|
2314
|
+
} finally {
|
|
2315
|
+
server.close()
|
|
2316
|
+
}
|
|
2317
|
+
})
|
|
2318
|
+
|
|
2250
2319
|
test('unitType=request auto-metered SSE responses charge once across the stream', async () => {
|
|
2251
2320
|
const harness = createManagedSseFetch({ maxDeposit: 1n, unitType: 'request' })
|
|
2252
2321
|
const manager = precompileSessionManager({
|
|
@@ -2848,10 +2917,7 @@ describe('precompile server session unit guardrails', () => {
|
|
|
2848
2917
|
})
|
|
2849
2918
|
|
|
2850
2919
|
const receipt = await method.verify({
|
|
2851
|
-
credential:
|
|
2852
|
-
challenge: makeChallenge(openPayload.channelId),
|
|
2853
|
-
payload: lowerVoucher,
|
|
2854
|
-
},
|
|
2920
|
+
credential: voucherCredential(lowerVoucher, openPayload.channelId),
|
|
2855
2921
|
request: verifyRequest(openPayload.channelId),
|
|
2856
2922
|
})
|
|
2857
2923
|
|
|
@@ -413,6 +413,7 @@ export function session<const parameters extends session.Parameters>(
|
|
|
413
413
|
channelStateTtl,
|
|
414
414
|
chainId: context.chainId,
|
|
415
415
|
client: context.client,
|
|
416
|
+
credentialSource: credential.source,
|
|
416
417
|
escrow: context.escrow,
|
|
417
418
|
expectedOperator: context.methodDetails.operator,
|
|
418
419
|
feePayer: context.feePayer,
|