mppx 0.4.9 → 0.4.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -1
- 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/stripe/internal/types.d.ts +3 -0
- package/dist/stripe/internal/types.d.ts.map +1 -1
- package/dist/stripe/server/Charge.d.ts.map +1 -1
- package/dist/stripe/server/Charge.js +9 -2
- package/dist/stripe/server/Charge.js.map +1 -1
- package/dist/tempo/server/Session.d.ts.map +1 -1
- package/dist/tempo/server/Session.js +25 -8
- 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/internal/types.ts +5 -1
- package/src/stripe/server/Charge.test.ts +53 -2
- package/src/stripe/server/Charge.ts +12 -4
- 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 +116 -37
- package/src/tempo/server/Session.ts +32 -11
- 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
|
@@ -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
|
+
})
|