mppx 0.4.9 → 0.4.10
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/cli.d.ts.map +1 -1
- package/dist/cli/cli.js +155 -0
- package/dist/cli/cli.js.map +1 -1
- package/dist/discovery/Discovery.d.ts +146 -0
- package/dist/discovery/Discovery.d.ts.map +1 -0
- package/dist/discovery/Discovery.js +60 -0
- package/dist/discovery/Discovery.js.map +1 -0
- package/dist/discovery/OpenApi.d.ts +61 -0
- package/dist/discovery/OpenApi.d.ts.map +1 -0
- package/dist/discovery/OpenApi.js +139 -0
- package/dist/discovery/OpenApi.js.map +1 -0
- package/dist/discovery/Validate.d.ts +10 -0
- package/dist/discovery/Validate.d.ts.map +1 -0
- package/dist/discovery/Validate.js +63 -0
- package/dist/discovery/Validate.js.map +1 -0
- package/dist/discovery/index.d.ts +4 -0
- package/dist/discovery/index.d.ts.map +1 -0
- package/dist/discovery/index.js +4 -0
- package/dist/discovery/index.js.map +1 -0
- package/dist/middlewares/elysia.d.ts +52 -1
- package/dist/middlewares/elysia.d.ts.map +1 -1
- package/dist/middlewares/elysia.js +17 -0
- package/dist/middlewares/elysia.js.map +1 -1
- package/dist/middlewares/express.d.ts +13 -1
- package/dist/middlewares/express.d.ts.map +1 -1
- package/dist/middlewares/express.js +18 -0
- package/dist/middlewares/express.js.map +1 -1
- package/dist/middlewares/hono.d.ts +19 -1
- package/dist/middlewares/hono.d.ts.map +1 -1
- package/dist/middlewares/hono.js +51 -0
- package/dist/middlewares/hono.js.map +1 -1
- package/dist/middlewares/internal/mppx.d.ts +4 -2
- package/dist/middlewares/internal/mppx.d.ts.map +1 -1
- package/dist/middlewares/internal/mppx.js +10 -3
- package/dist/middlewares/internal/mppx.js.map +1 -1
- package/dist/middlewares/nextjs.d.ts +11 -0
- package/dist/middlewares/nextjs.d.ts.map +1 -1
- package/dist/middlewares/nextjs.js +15 -0
- package/dist/middlewares/nextjs.js.map +1 -1
- package/dist/proxy/Proxy.d.ts +6 -0
- package/dist/proxy/Proxy.d.ts.map +1 -1
- package/dist/proxy/Proxy.js +56 -80
- package/dist/proxy/Proxy.js.map +1 -1
- package/dist/proxy/Service.d.ts +16 -23
- package/dist/proxy/Service.d.ts.map +1 -1
- package/dist/proxy/Service.js +19 -83
- package/dist/proxy/Service.js.map +1 -1
- package/dist/proxy/internal/Route.js +1 -1
- package/dist/proxy/internal/Route.js.map +1 -1
- package/dist/proxy/services/anthropic.d.ts.map +1 -1
- package/dist/proxy/services/anthropic.js +5 -0
- package/dist/proxy/services/anthropic.js.map +1 -1
- package/dist/proxy/services/openai.d.ts.map +1 -1
- package/dist/proxy/services/openai.js +6 -3
- package/dist/proxy/services/openai.js.map +1 -1
- package/dist/proxy/services/stripe.d.ts.map +1 -1
- package/dist/proxy/services/stripe.js +6 -3
- package/dist/proxy/services/stripe.js.map +1 -1
- package/dist/tempo/server/Session.d.ts.map +1 -1
- package/dist/tempo/server/Session.js +18 -5
- package/dist/tempo/server/Session.js.map +1 -1
- package/dist/tempo/server/internal/transport.d.ts.map +1 -1
- package/dist/tempo/server/internal/transport.js +8 -0
- package/dist/tempo/server/internal/transport.js.map +1 -1
- package/dist/tempo/session/Chain.js +1 -1
- package/dist/tempo/session/Chain.js.map +1 -1
- package/package.json +6 -1
- package/src/BodyDigest.test.ts +1 -1
- package/src/Challenge.fuzz.test.ts +121 -0
- package/src/Challenge.test-d.ts +1 -1
- package/src/Challenge.test.ts +1 -1
- package/src/Credential.fuzz.test.ts +62 -0
- package/src/Credential.test.ts +1 -1
- package/src/Errors.test.ts +1 -1
- package/src/Expires.test.ts +1 -1
- package/src/Method.test.ts +1 -1
- package/src/PaymentRequest.test.ts +1 -1
- package/src/Receipt.test.ts +1 -1
- package/src/Store.test-d.ts +1 -1
- package/src/Store.test.ts +1 -1
- package/src/cli/cli.test.ts +212 -1
- package/src/cli/cli.ts +162 -0
- package/src/client/Mppx.test-d.ts +1 -1
- package/src/client/Mppx.test.ts +1 -1
- package/src/client/Transport.test.ts +1 -1
- package/src/client/internal/Fetch.browser.test.ts +1 -1
- package/src/client/internal/Fetch.test-d.ts +1 -1
- package/src/client/internal/Fetch.test.ts +2 -1
- package/src/discovery/Discovery.test.ts +152 -0
- package/src/discovery/Discovery.ts +72 -0
- package/src/discovery/OpenApi.test.ts +425 -0
- package/src/discovery/OpenApi.ts +224 -0
- package/src/discovery/Validate.test.ts +188 -0
- package/src/discovery/Validate.ts +76 -0
- package/src/discovery/index.ts +3 -0
- package/src/internal/constantTimeEqual.test.ts +1 -1
- package/src/mcp-sdk/client/McpClient.test-d.ts +1 -1
- package/src/mcp-sdk/client/McpClient.test.ts +1 -1
- package/src/mcp-sdk/server/Transport.test.ts +1 -1
- package/src/middlewares/elysia.test.ts +27 -2
- package/src/middlewares/elysia.ts +35 -1
- package/src/middlewares/express.test.ts +35 -7
- package/src/middlewares/express.ts +34 -0
- package/src/middlewares/hono.test.ts +28 -6
- package/src/middlewares/hono.ts +73 -1
- package/src/middlewares/internal/mppx.test.ts +1 -1
- package/src/middlewares/internal/mppx.ts +14 -6
- package/src/middlewares/nextjs.test.ts +31 -6
- package/src/middlewares/nextjs.ts +28 -0
- package/src/proxy/Proxy.test.ts +54 -270
- package/src/proxy/Proxy.ts +71 -93
- package/src/proxy/Service.test.ts +23 -1
- package/src/proxy/Service.ts +40 -86
- package/src/proxy/internal/Headers.test.ts +1 -1
- package/src/proxy/internal/Route.test.ts +9 -1
- package/src/proxy/internal/Route.ts +1 -1
- package/src/proxy/services/anthropic.test.ts +132 -0
- package/src/proxy/services/anthropic.ts +5 -0
- package/src/proxy/services/openai.test.ts +1 -1
- package/src/proxy/services/openai.ts +6 -4
- package/src/proxy/services/stripe.test.ts +132 -0
- package/src/proxy/services/stripe.ts +6 -4
- package/src/server/Mppx.test-d.ts +1 -1
- package/src/server/Mppx.test.ts +2 -1
- package/src/server/NodeListener.test.ts +1 -1
- package/src/server/Request.test.ts +1 -1
- package/src/server/Response.test.ts +1 -1
- package/src/server/Transport.test.ts +1 -1
- package/src/stripe/Charge.integration.test.ts +1 -1
- package/src/stripe/Methods.test.ts +1 -1
- package/src/stripe/client/Charge.test.ts +1 -1
- package/src/stripe/server/Charge.test.ts +1 -1
- package/src/tempo/Attribution.test.ts +1 -1
- package/src/tempo/Methods.test.ts +1 -1
- package/src/tempo/client/ChannelOps.test.ts +6 -3
- package/src/tempo/client/Session.test.ts +5 -2
- package/src/tempo/client/SessionManager.test.ts +1 -1
- package/src/tempo/internal/auto-swap.test.ts +1 -1
- package/src/tempo/internal/defaults.test.ts +1 -1
- package/src/tempo/internal/fee-payer.test.ts +1 -1
- package/src/tempo/server/Charge.test.ts +1 -1
- package/src/tempo/server/Session.test.ts +87 -37
- package/src/tempo/server/Session.ts +25 -8
- package/src/tempo/server/Sse.test.ts +1 -1
- package/src/tempo/server/internal/transport.test.ts +24 -1
- package/src/tempo/server/internal/transport.ts +11 -0
- package/src/tempo/session/Chain.test.ts +5 -2
- package/src/tempo/session/Chain.ts +1 -1
- package/src/tempo/session/Channel.test.ts +1 -1
- package/src/tempo/session/ChannelStore.test.ts +1 -1
- package/src/tempo/session/Receipt.test.ts +1 -1
- package/src/tempo/session/Sse.fuzz.test.ts +138 -0
- package/src/tempo/session/Sse.test.ts +1 -1
- package/src/tempo/session/Voucher.test.ts +1 -1
- package/src/viem/Account.test.ts +1 -1
- package/src/viem/Client.test.ts +1 -1
- package/src/zod.test.ts +147 -0
|
@@ -101,6 +101,11 @@ export function session<const parameters extends session.Parameters>(p?: paramet
|
|
|
101
101
|
|
|
102
102
|
const { account, recipient, feePayer, feePayerUrl } = Account.resolve(parameters)
|
|
103
103
|
|
|
104
|
+
if (!account)
|
|
105
|
+
throw new Error(
|
|
106
|
+
'tempo.session() requires an `account` (viem Account, e.g. privateKeyToAccount("0x...")). An address string is not sufficient — the server needs a signing account for on-chain channel close and settlement.',
|
|
107
|
+
)
|
|
108
|
+
|
|
104
109
|
const getClient = Client.getResolver({
|
|
105
110
|
chain: tempo_chain,
|
|
106
111
|
feePayerUrl,
|
|
@@ -462,19 +467,12 @@ async function verifyAndAcceptVoucher(parameters: {
|
|
|
462
467
|
throw new AmountExceedsDepositError({ reason: 'voucher amount exceeds on-chain deposit' })
|
|
463
468
|
}
|
|
464
469
|
|
|
465
|
-
if (voucher.cumulativeAmount
|
|
470
|
+
if (voucher.cumulativeAmount < channel.highestVoucherAmount) {
|
|
466
471
|
throw new VerificationFailedError({
|
|
467
472
|
reason: 'voucher cumulativeAmount must be strictly greater than highest accepted voucher',
|
|
468
473
|
})
|
|
469
474
|
}
|
|
470
475
|
|
|
471
|
-
const delta = voucher.cumulativeAmount - channel.highestVoucherAmount
|
|
472
|
-
if (delta < minVoucherDelta) {
|
|
473
|
-
throw new DeltaTooSmallError({
|
|
474
|
-
reason: `voucher delta ${delta} below minimum ${minVoucherDelta}`,
|
|
475
|
-
})
|
|
476
|
-
}
|
|
477
|
-
|
|
478
476
|
const isValid = await verifyVoucher(
|
|
479
477
|
methodDetails.escrowContract,
|
|
480
478
|
methodDetails.chainId,
|
|
@@ -486,6 +484,25 @@ async function verifyAndAcceptVoucher(parameters: {
|
|
|
486
484
|
throw new InvalidSignatureError({ reason: 'invalid voucher signature' })
|
|
487
485
|
}
|
|
488
486
|
|
|
487
|
+
// Idempotent replay: equal cumulative voucher is accepted without
|
|
488
|
+
// advancing channel state or charging additional value.
|
|
489
|
+
if (voucher.cumulativeAmount === channel.highestVoucherAmount) {
|
|
490
|
+
return createSessionReceipt({
|
|
491
|
+
challengeId: challenge.id,
|
|
492
|
+
channelId,
|
|
493
|
+
acceptedCumulative: channel.highestVoucherAmount,
|
|
494
|
+
spent: channel.spent,
|
|
495
|
+
units: channel.units,
|
|
496
|
+
})
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
const delta = voucher.cumulativeAmount - channel.highestVoucherAmount
|
|
500
|
+
if (delta < minVoucherDelta) {
|
|
501
|
+
throw new DeltaTooSmallError({
|
|
502
|
+
reason: `voucher delta ${delta} below minimum ${minVoucherDelta}`,
|
|
503
|
+
})
|
|
504
|
+
}
|
|
505
|
+
|
|
489
506
|
const updated = await store.updateChannel(channelId, (current) => {
|
|
490
507
|
if (!current) throw new ChannelNotFoundError({ reason: 'channel not found' })
|
|
491
508
|
if (voucher.cumulativeAmount > current.highestVoucherAmount) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Address, Hex } from 'viem'
|
|
2
|
-
import { describe, expect, test } from '
|
|
2
|
+
import { describe, expect, test } from 'vp/test'
|
|
3
3
|
|
|
4
4
|
import { chainId, escrowContract as escrowContractDefaults } from '../internal/defaults.js'
|
|
5
5
|
import type * as ChannelStore from '../session/ChannelStore.js'
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Challenge, Credential } from 'mppx'
|
|
2
2
|
import type { Address, Hex } from 'viem'
|
|
3
|
-
import { describe, expect, test } from '
|
|
3
|
+
import { describe, expect, test } from 'vp/test'
|
|
4
4
|
|
|
5
5
|
import * as Store from '../../../Store.js'
|
|
6
6
|
import { chainId, escrowContract as escrowContractDefaults } from '../../internal/defaults.js'
|
|
@@ -292,6 +292,29 @@ describe('sse transport', () => {
|
|
|
292
292
|
expect(response.headers.get('Payment-Receipt')).toBeTruthy()
|
|
293
293
|
})
|
|
294
294
|
|
|
295
|
+
test('respondReceipt with 204 management response keeps null body and receipt', async () => {
|
|
296
|
+
const store = memoryStore()
|
|
297
|
+
await seedChannel(store, 10000000n)
|
|
298
|
+
const transport = sse({ store })
|
|
299
|
+
|
|
300
|
+
transport.getCredential(makeAuthorizedRequest())
|
|
301
|
+
|
|
302
|
+
const managementResponse = new Response(null, { status: 204 })
|
|
303
|
+
const response = transport.respondReceipt({
|
|
304
|
+
receipt: makeReceipt(),
|
|
305
|
+
response: managementResponse,
|
|
306
|
+
challengeId,
|
|
307
|
+
})
|
|
308
|
+
|
|
309
|
+
expect(response.status).toBe(204)
|
|
310
|
+
expect(await response.text()).toBe('')
|
|
311
|
+
expect(response.headers.get('Payment-Receipt')).toBeTruthy()
|
|
312
|
+
|
|
313
|
+
const channel = await store.getChannel(channelId)
|
|
314
|
+
expect(channel!.spent).toBe(0n)
|
|
315
|
+
expect(channel!.units).toBe(0)
|
|
316
|
+
})
|
|
317
|
+
|
|
295
318
|
test('poll: true strips waitForUpdate from store', async () => {
|
|
296
319
|
const store = memoryStore()
|
|
297
320
|
;(store as any).waitForUpdate = async () => {}
|
|
@@ -100,6 +100,13 @@ export function sse(options: sse.Options & { store: ChannelStore.ChannelStore })
|
|
|
100
100
|
const ctx = contextMap.get(challengeId)
|
|
101
101
|
if (ctx) {
|
|
102
102
|
contextMap.delete(challengeId)
|
|
103
|
+
|
|
104
|
+
// Null-body statuses (e.g. 204 from management actions) cannot carry a
|
|
105
|
+
// response body per Fetch/HTTP semantics.
|
|
106
|
+
if (isNullBodyStatus(baseResponse.status)) {
|
|
107
|
+
return baseResponse
|
|
108
|
+
}
|
|
109
|
+
|
|
103
110
|
const stream = new ReadableStream<Uint8Array>({
|
|
104
111
|
async start(controller) {
|
|
105
112
|
// deduction completes before consumer reads
|
|
@@ -191,3 +198,7 @@ function isAsyncGeneratorFunction(
|
|
|
191
198
|
function isAsyncIterable(value: unknown): value is AsyncIterable<string> {
|
|
192
199
|
return value !== null && typeof value === 'object' && Symbol.asyncIterator in (value as object)
|
|
193
200
|
}
|
|
201
|
+
|
|
202
|
+
function isNullBodyStatus(status: number): boolean {
|
|
203
|
+
return [101, 204, 205, 304].includes(status)
|
|
204
|
+
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { type Address, encodeFunctionData, erc20Abi, type Hex } from 'viem'
|
|
2
2
|
import { waitForTransactionReceipt } from 'viem/actions'
|
|
3
3
|
import { Addresses, Transaction } from 'viem/tempo'
|
|
4
|
-
import { beforeAll, describe, expect, test } from '
|
|
4
|
+
import { beforeAll, describe, expect, test } from 'vp/test'
|
|
5
|
+
import { nodeEnv } from '~test/config.js'
|
|
5
6
|
import {
|
|
6
7
|
closeChannelOnChain,
|
|
7
8
|
deployEscrow,
|
|
@@ -22,6 +23,8 @@ import {
|
|
|
22
23
|
} from './Chain.js'
|
|
23
24
|
import { signVoucher } from './Voucher.js'
|
|
24
25
|
|
|
26
|
+
const isLocalnet = nodeEnv === 'localnet'
|
|
27
|
+
|
|
25
28
|
const UINT128_MAX = 2n ** 128n - 1n
|
|
26
29
|
|
|
27
30
|
describe('assertUint128 (via settleOnChain / closeOnChain)', () => {
|
|
@@ -70,7 +73,7 @@ describe('assertUint128 (via settleOnChain / closeOnChain)', () => {
|
|
|
70
73
|
})
|
|
71
74
|
})
|
|
72
75
|
|
|
73
|
-
describe('on-chain', () => {
|
|
76
|
+
describe.runIf(isLocalnet)('on-chain', () => {
|
|
74
77
|
const payer = accounts[2]
|
|
75
78
|
const recipient = accounts[0].address
|
|
76
79
|
const currency = asset
|
|
@@ -132,7 +132,7 @@ export async function closeOnChain(
|
|
|
132
132
|
const resolved = account ?? client.account
|
|
133
133
|
if (!resolved)
|
|
134
134
|
throw new Error(
|
|
135
|
-
'Cannot close channel: no account available.
|
|
135
|
+
'Cannot close channel: no account available. Pass an `account` (viem Account, e.g. privateKeyToAccount("0x...")) to tempo.session(), or provide a `getClient` that returns an account-bearing client.',
|
|
136
136
|
)
|
|
137
137
|
const args = [voucher.channelId, voucher.cumulativeAmount, voucher.signature] as const
|
|
138
138
|
if (feePayer) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Address, Hex } from 'viem'
|
|
2
|
-
import { describe, expect, test } from '
|
|
2
|
+
import { describe, expect, test } from 'vp/test'
|
|
3
3
|
|
|
4
4
|
import * as Store from '../../Store.js'
|
|
5
5
|
import { chainId, escrowContract as escrowContractDefaults } from '../internal/defaults.js'
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import * as fc from 'fast-check'
|
|
2
|
+
import { describe, expect, test } from 'vp/test'
|
|
3
|
+
|
|
4
|
+
import * as Sse from './Sse.js'
|
|
5
|
+
|
|
6
|
+
function createChunkedResponse(chunks: Uint8Array[]): Response {
|
|
7
|
+
let index = 0
|
|
8
|
+
const stream = new ReadableStream<Uint8Array>({
|
|
9
|
+
pull(controller) {
|
|
10
|
+
if (index < chunks.length) {
|
|
11
|
+
controller.enqueue(chunks[index]!)
|
|
12
|
+
index++
|
|
13
|
+
} else {
|
|
14
|
+
controller.close()
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
})
|
|
18
|
+
return new Response(stream, {
|
|
19
|
+
headers: { 'Content-Type': 'text/event-stream' },
|
|
20
|
+
})
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function splitAtPositions(str: string, positions: number[]): Uint8Array[] {
|
|
24
|
+
const encoder = new TextEncoder()
|
|
25
|
+
const sorted = [...new Set([0, ...positions, str.length])]
|
|
26
|
+
.filter((p) => p >= 0 && p <= str.length)
|
|
27
|
+
.sort((a, b) => a - b)
|
|
28
|
+
const chunks: Uint8Array[] = []
|
|
29
|
+
for (let i = 0; i < sorted.length - 1; i++) {
|
|
30
|
+
const chunk = str.slice(sorted[i], sorted[i + 1])
|
|
31
|
+
if (chunk.length > 0) chunks.push(encoder.encode(chunk))
|
|
32
|
+
}
|
|
33
|
+
return chunks
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function collectData(response: Response): Promise<string[]> {
|
|
37
|
+
const results: string[] = []
|
|
38
|
+
for await (const data of Sse.iterateData(response)) {
|
|
39
|
+
results.push(data)
|
|
40
|
+
}
|
|
41
|
+
return results
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
describe('parseEvent', () => {
|
|
45
|
+
test('never throws on arbitrary message-type input', () => {
|
|
46
|
+
fc.assert(
|
|
47
|
+
fc.property(fc.string(), (input) => {
|
|
48
|
+
const result = Sse.parseEvent(input)
|
|
49
|
+
if (result !== null) {
|
|
50
|
+
expect(result.type).toBe('message')
|
|
51
|
+
expect(typeof result.data).toBe('string')
|
|
52
|
+
}
|
|
53
|
+
}),
|
|
54
|
+
{ numRuns: 10_000 },
|
|
55
|
+
)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
test('parseEvent with valid SSE format', () => {
|
|
59
|
+
const sseMessageArb = fc
|
|
60
|
+
.array(
|
|
61
|
+
fc.string().filter((s) => !s.includes('\n')),
|
|
62
|
+
{ minLength: 1, maxLength: 5 },
|
|
63
|
+
)
|
|
64
|
+
.map((lines) => lines.map((l) => `data: ${l}`).join('\n'))
|
|
65
|
+
|
|
66
|
+
fc.assert(
|
|
67
|
+
fc.property(sseMessageArb, (raw) => {
|
|
68
|
+
const result = Sse.parseEvent(raw)
|
|
69
|
+
expect(result).not.toBeNull()
|
|
70
|
+
expect(result!.type).toBe('message')
|
|
71
|
+
const expectedData = raw
|
|
72
|
+
.split('\n')
|
|
73
|
+
.map((l) => l.slice(6))
|
|
74
|
+
.join('\n')
|
|
75
|
+
expect(result!.data).toBe(expectedData)
|
|
76
|
+
}),
|
|
77
|
+
{ numRuns: 5_000 },
|
|
78
|
+
)
|
|
79
|
+
})
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
describe('iterateData', () => {
|
|
83
|
+
const sseEventArb = fc
|
|
84
|
+
.array(
|
|
85
|
+
fc.string().filter((s) => !s.includes('\n\n') && !s.includes('\n')),
|
|
86
|
+
{ minLength: 1, maxLength: 3 },
|
|
87
|
+
)
|
|
88
|
+
.map((lines) => lines.map((l) => `data: ${l}`).join('\n'))
|
|
89
|
+
|
|
90
|
+
const sseStreamArb = fc
|
|
91
|
+
.array(sseEventArb, { minLength: 1, maxLength: 5 })
|
|
92
|
+
.map((events) => events.join('\n\n') + '\n\n')
|
|
93
|
+
|
|
94
|
+
test('chunk boundary invariance', async () => {
|
|
95
|
+
await fc.assert(
|
|
96
|
+
fc.asyncProperty(
|
|
97
|
+
sseStreamArb,
|
|
98
|
+
fc.array(fc.nat(), { minLength: 1, maxLength: 10 }),
|
|
99
|
+
async (stream, positions) => {
|
|
100
|
+
const encoder = new TextEncoder()
|
|
101
|
+
|
|
102
|
+
const singleChunk = createChunkedResponse([encoder.encode(stream)])
|
|
103
|
+
const singleResult = await collectData(singleChunk)
|
|
104
|
+
|
|
105
|
+
const boundedPositions = positions.map((p) => p % (stream.length + 1))
|
|
106
|
+
const chunks = splitAtPositions(stream, boundedPositions)
|
|
107
|
+
const multiChunk = createChunkedResponse(chunks)
|
|
108
|
+
const multiResult = await collectData(multiChunk)
|
|
109
|
+
|
|
110
|
+
expect(multiResult).toEqual(singleResult)
|
|
111
|
+
},
|
|
112
|
+
),
|
|
113
|
+
{ numRuns: 1_000 },
|
|
114
|
+
)
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
test('iterateData never throws on arbitrary chunked input', async () => {
|
|
118
|
+
await fc.assert(
|
|
119
|
+
fc.asyncProperty(
|
|
120
|
+
fc.array(fc.uint8Array({ minLength: 1, maxLength: 100 }), {
|
|
121
|
+
minLength: 1,
|
|
122
|
+
maxLength: 5,
|
|
123
|
+
}),
|
|
124
|
+
async (chunks) => {
|
|
125
|
+
const response = createChunkedResponse(chunks)
|
|
126
|
+
const results: string[] = []
|
|
127
|
+
for await (const data of Sse.iterateData(response)) {
|
|
128
|
+
results.push(data)
|
|
129
|
+
}
|
|
130
|
+
for (const item of results) {
|
|
131
|
+
expect(typeof item).toBe('string')
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
),
|
|
135
|
+
{ numRuns: 5_000 },
|
|
136
|
+
)
|
|
137
|
+
})
|
|
138
|
+
})
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Address, Hex } from 'viem'
|
|
2
|
-
import { describe, expect, test } from '
|
|
2
|
+
import { describe, expect, test } from 'vp/test'
|
|
3
3
|
|
|
4
4
|
import { chainId, escrowContract as escrowContractDefaults } from '../internal/defaults.js'
|
|
5
5
|
import type * as ChannelStore from './ChannelStore.js'
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createClient, http } from 'viem'
|
|
2
2
|
import { privateKeyToAccount } from 'viem/accounts'
|
|
3
|
-
import { describe, expect, test } from '
|
|
3
|
+
import { describe, expect, test } from 'vp/test'
|
|
4
4
|
|
|
5
5
|
import { parseVoucherFromPayload, signVoucher, verifyVoucher } from './Voucher.js'
|
|
6
6
|
|
package/src/viem/Account.test.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createClient, http } from 'viem'
|
|
2
2
|
import { privateKeyToAccount } from 'viem/accounts'
|
|
3
3
|
import { mainnet } from 'viem/chains'
|
|
4
|
-
import { describe, expect, test } from '
|
|
4
|
+
import { describe, expect, test } from 'vp/test'
|
|
5
5
|
|
|
6
6
|
import * as Account from './Account.js'
|
|
7
7
|
|
package/src/viem/Client.test.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { privateKeyToAccount } from 'viem/accounts'
|
|
|
3
3
|
import { signTransaction } from 'viem/actions'
|
|
4
4
|
import { tempoLocalnet } from 'viem/chains'
|
|
5
5
|
import { Transaction } from 'viem/tempo'
|
|
6
|
-
import { describe, expect, test } from '
|
|
6
|
+
import { describe, expect, test } from 'vp/test'
|
|
7
7
|
|
|
8
8
|
import * as Client from './Client.js'
|
|
9
9
|
|
package/src/zod.test.ts
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { describe, expect, test } from 'vp/test'
|
|
2
|
+
|
|
3
|
+
import { address, amount, datetime, hash, period, signature, unwrapOptional, z } from './zod.js'
|
|
4
|
+
|
|
5
|
+
describe('amount', () => {
|
|
6
|
+
test.each([
|
|
7
|
+
{ input: '1', expected: true, desc: 'integer' },
|
|
8
|
+
{ input: '1000000', expected: true, desc: 'large integer' },
|
|
9
|
+
{ input: '1.5', expected: true, desc: 'decimal' },
|
|
10
|
+
{ input: '0', expected: true, desc: 'zero' },
|
|
11
|
+
{ input: '007', expected: true, desc: 'leading zeros' },
|
|
12
|
+
{ input: '-1', expected: false, desc: 'negative number' },
|
|
13
|
+
{ input: '', expected: false, desc: 'empty string' },
|
|
14
|
+
{ input: 'abc', expected: false, desc: 'alphabetic string' },
|
|
15
|
+
{ input: '1.', expected: false, desc: 'trailing decimal point' },
|
|
16
|
+
{ input: 123 as unknown as string, expected: false, desc: 'non-string type' },
|
|
17
|
+
])('$desc ($input) → $expected', ({ input, expected }) => {
|
|
18
|
+
expect(amount().safeParse(input).success).toBe(expected)
|
|
19
|
+
})
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
describe('datetime', () => {
|
|
23
|
+
test.each([
|
|
24
|
+
{ input: '2025-01-06T12:00:00Z', expected: true, desc: 'UTC with Z suffix' },
|
|
25
|
+
{ input: '2025-01-06T12:00:00.123Z', expected: true, desc: 'fractional seconds' },
|
|
26
|
+
{ input: '2025-01-06T12:00:00+05:30', expected: true, desc: 'positive UTC offset' },
|
|
27
|
+
{ input: '2025-01-06T12:00:00-08:00', expected: true, desc: 'negative UTC offset' },
|
|
28
|
+
{ input: '2025-01-06T12:00:00', expected: false, desc: 'missing timezone' },
|
|
29
|
+
{ input: '2025-01-06', expected: false, desc: 'date only, no time' },
|
|
30
|
+
{ input: '', expected: false, desc: 'empty string' },
|
|
31
|
+
{ input: 'not-a-date', expected: false, desc: 'non-date string' },
|
|
32
|
+
])('$desc ($input) → $expected', ({ input, expected }) => {
|
|
33
|
+
expect(datetime().safeParse(input).success).toBe(expected)
|
|
34
|
+
})
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
describe('address', () => {
|
|
38
|
+
test.each([
|
|
39
|
+
{ input: '0x1234567890abcdef1234567890abcdef12345678', expected: true, desc: 'lowercase hex' },
|
|
40
|
+
{ input: '0x1234567890ABCDEF1234567890ABCDEF12345678', expected: true, desc: 'uppercase hex' },
|
|
41
|
+
{ input: '0xAbCdEf0123456789AbCdEf0123456789AbCdEf01', expected: true, desc: 'mixed case hex' },
|
|
42
|
+
{ input: '0x1234', expected: false, desc: 'too short' },
|
|
43
|
+
{
|
|
44
|
+
input: '0x1234567890abcdef1234567890abcdef1234567890',
|
|
45
|
+
expected: false,
|
|
46
|
+
desc: 'too long (42 hex chars)',
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
input: '1234567890abcdef1234567890abcdef12345678',
|
|
50
|
+
expected: false,
|
|
51
|
+
desc: 'missing 0x prefix',
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
input: '0xGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG',
|
|
55
|
+
expected: false,
|
|
56
|
+
desc: 'non-hex characters',
|
|
57
|
+
},
|
|
58
|
+
{ input: '', expected: false, desc: 'empty string' },
|
|
59
|
+
])('$desc → $expected', ({ input, expected }) => {
|
|
60
|
+
expect(address().safeParse(input).success).toBe(expected)
|
|
61
|
+
})
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
describe('hash', () => {
|
|
65
|
+
test.each([
|
|
66
|
+
{
|
|
67
|
+
input: '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef',
|
|
68
|
+
expected: true,
|
|
69
|
+
desc: 'valid 64 hex chars',
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
input: '0xABCDEF1234567890ABCDEF1234567890ABCDEF1234567890ABCDEF1234567890',
|
|
73
|
+
expected: true,
|
|
74
|
+
desc: 'uppercase hex',
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
input: '0x1234567890abcdef1234567890abcdef12345678',
|
|
78
|
+
expected: false,
|
|
79
|
+
desc: 'too short (address length)',
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
input: '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef00',
|
|
83
|
+
expected: false,
|
|
84
|
+
desc: 'too long (66 hex chars)',
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
input: '1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef',
|
|
88
|
+
expected: false,
|
|
89
|
+
desc: 'missing 0x prefix',
|
|
90
|
+
},
|
|
91
|
+
{ input: '', expected: false, desc: 'empty string' },
|
|
92
|
+
])('$desc → $expected', ({ input, expected }) => {
|
|
93
|
+
expect(hash().safeParse(input).success).toBe(expected)
|
|
94
|
+
})
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
describe('period', () => {
|
|
98
|
+
test.each([
|
|
99
|
+
{ input: 'day', expected: true, desc: 'day keyword' },
|
|
100
|
+
{ input: 'week', expected: true, desc: 'week keyword' },
|
|
101
|
+
{ input: 'month', expected: true, desc: 'month keyword' },
|
|
102
|
+
{ input: 'year', expected: true, desc: 'year keyword' },
|
|
103
|
+
{ input: '3600', expected: true, desc: 'numeric seconds' },
|
|
104
|
+
{ input: '', expected: false, desc: 'empty string' },
|
|
105
|
+
{ input: 'hourly', expected: false, desc: 'unsupported keyword' },
|
|
106
|
+
{ input: 'day1', expected: false, desc: 'keyword with trailing digits' },
|
|
107
|
+
])('$desc ($input) → $expected', ({ input, expected }) => {
|
|
108
|
+
expect(period().safeParse(input).success).toBe(expected)
|
|
109
|
+
})
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
describe('signature', () => {
|
|
113
|
+
test.each([
|
|
114
|
+
{ input: '0xabcdef', expected: true, desc: 'short hex' },
|
|
115
|
+
{
|
|
116
|
+
input:
|
|
117
|
+
'0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1b',
|
|
118
|
+
expected: true,
|
|
119
|
+
desc: 'full 65-byte secp256k1 signature',
|
|
120
|
+
},
|
|
121
|
+
{ input: '0x', expected: false, desc: 'bare 0x prefix with no data' },
|
|
122
|
+
{ input: 'abcdef', expected: false, desc: 'missing 0x prefix' },
|
|
123
|
+
{ input: '0xZZZZ', expected: false, desc: 'non-hex characters after 0x' },
|
|
124
|
+
{ input: '', expected: false, desc: 'empty string' },
|
|
125
|
+
])('$desc → $expected', ({ input, expected }) => {
|
|
126
|
+
expect(signature().safeParse(input).success).toBe(expected)
|
|
127
|
+
})
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
describe('unwrapOptional', () => {
|
|
131
|
+
test('unwraps optional string schema to inner type', () => {
|
|
132
|
+
const inner = z.string()
|
|
133
|
+
const result = unwrapOptional(z.optional(inner))
|
|
134
|
+
expect(result).toBe(inner)
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
test('returns non-optional schema unchanged', () => {
|
|
138
|
+
const schema = z.string()
|
|
139
|
+
expect(unwrapOptional(schema)).toBe(schema)
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
test('unwraps optional number schema to inner type', () => {
|
|
143
|
+
const inner = z.number()
|
|
144
|
+
const result = unwrapOptional(z.optional(inner))
|
|
145
|
+
expect(result).toBe(inner)
|
|
146
|
+
})
|
|
147
|
+
})
|