mppx 0.8.5 → 0.8.7
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 -0
- package/dist/Receipt.d.ts +20 -2
- package/dist/Receipt.d.ts.map +1 -1
- package/dist/Receipt.js +19 -12
- package/dist/Receipt.js.map +1 -1
- package/dist/cli/cli.d.ts.map +1 -1
- package/dist/cli/cli.js +6 -4
- package/dist/cli/cli.js.map +1 -1
- 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/tempo.d.ts.map +1 -1
- package/dist/cli/plugins/tempo.js +4 -2
- package/dist/cli/plugins/tempo.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 +184 -78
- 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 +98 -151
- package/dist/cli/validate/index.js.map +1 -1
- package/dist/cli/validate/payment.d.ts +1 -0
- package/dist/cli/validate/payment.d.ts.map +1 -1
- package/dist/cli/validate/payment.js +215 -144
- 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 +6 -1
- package/dist/client/internal/Fetch.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/client/Charge.d.ts.map +1 -1
- package/dist/tempo/client/Charge.js +6 -2
- package/dist/tempo/client/Charge.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/precompile/Chain.d.ts +26 -2
- package/dist/tempo/session/precompile/Chain.d.ts.map +1 -1
- package/dist/tempo/session/precompile/Chain.js +38 -5
- package/dist/tempo/session/precompile/Chain.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 +79 -0
- package/dist/validation/core.d.ts.map +1 -0
- package/dist/validation/core.js +256 -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/Receipt.test.ts +53 -0
- package/src/Receipt.ts +26 -13
- package/src/cli/cli.test.ts +41 -5
- package/src/cli/cli.ts +7 -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/tempo.ts +6 -2
- package/src/cli/validate/challenge.ts +334 -121
- 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 +117 -240
- package/src/cli/validate/payment.ts +261 -178
- package/src/cli/validate.test.ts +143 -3
- package/src/client/internal/Fetch.test.ts +186 -2
- package/src/client/internal/Fetch.ts +5 -1
- 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/client/Charge.test.ts +61 -0
- package/src/tempo/client/Charge.ts +6 -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/precompile/Chain.test.ts +121 -2
- package/src/tempo/session/precompile/Chain.ts +56 -6
- 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 +348 -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
|
@@ -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,
|
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
import { validateChallenge, validateErrorHandling } from '../cli/validate/challenge.js'
|
|
2
|
+
import {
|
|
3
|
+
buildUrl,
|
|
4
|
+
extractEndpointsFromDiscovery,
|
|
5
|
+
extractRequestBodyFromDiscovery,
|
|
6
|
+
fetchDiscoveryDoc,
|
|
7
|
+
} from '../cli/validate/discovery.js'
|
|
8
|
+
import type { CheckResult, EndpointSpec, PathParameter } from '../cli/validate/helpers.js'
|
|
9
|
+
import {
|
|
10
|
+
isValidIntegerAmount,
|
|
11
|
+
parseEndpointArg,
|
|
12
|
+
resolveBodyForEndpoint,
|
|
13
|
+
} from '../cli/validate/helpers.js'
|
|
14
|
+
import { validatePaymentFlow } from '../cli/validate/payment.js'
|
|
15
|
+
import { validate as validateDiscoveryDoc } from '../discovery/Validate.js'
|
|
16
|
+
|
|
17
|
+
export { buildUrl }
|
|
18
|
+
export type { CheckResult, EndpointSpec, PathParameter }
|
|
19
|
+
|
|
20
|
+
export type ValidateOptions = {
|
|
21
|
+
url: string
|
|
22
|
+
endpoint?: string | undefined
|
|
23
|
+
body?: string | undefined
|
|
24
|
+
query?: string[] | undefined
|
|
25
|
+
verbose?: boolean | undefined
|
|
26
|
+
yes?: boolean | undefined
|
|
27
|
+
skipPayment?: boolean | undefined
|
|
28
|
+
discoveryPath?: string | undefined
|
|
29
|
+
onPaymentResults?: (results: CheckResult[]) => void
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type DiscoveryResult = {
|
|
33
|
+
found: boolean
|
|
34
|
+
valid: boolean
|
|
35
|
+
endpoints: EndpointSpec[]
|
|
36
|
+
checks: CheckResult[]
|
|
37
|
+
doc: Record<string, unknown> | null
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export type EndpointValidationResult = {
|
|
41
|
+
method: string
|
|
42
|
+
path: string
|
|
43
|
+
challenge: CheckResult[]
|
|
44
|
+
errorHandling: CheckResult[]
|
|
45
|
+
payment: CheckResult[]
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export type ValidateResult = {
|
|
49
|
+
url: string
|
|
50
|
+
discovery: DiscoveryResult
|
|
51
|
+
endpoints: EndpointValidationResult[]
|
|
52
|
+
summary: { passed: number; failed: number; warnings: number; skipped: number }
|
|
53
|
+
flags: {
|
|
54
|
+
sawMppEndpoint: boolean
|
|
55
|
+
sawNonMppPaymentEndpoint: boolean
|
|
56
|
+
sawMalformedChallenge: boolean
|
|
57
|
+
sawTestnet: boolean
|
|
58
|
+
sawMainnet: boolean
|
|
59
|
+
paymentSucceeded: boolean
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export type ValidateEvent =
|
|
64
|
+
| { phase: 'discovery'; discovery: DiscoveryResult; results: CheckResult[] }
|
|
65
|
+
| { phase: 'endpoint'; endpoint: EndpointSpec }
|
|
66
|
+
| {
|
|
67
|
+
phase: 'challenge'
|
|
68
|
+
endpoint: EndpointSpec
|
|
69
|
+
results: CheckResult[]
|
|
70
|
+
isMpp: boolean
|
|
71
|
+
isTestnet: boolean
|
|
72
|
+
isNonMppPayment: boolean
|
|
73
|
+
isMalformedChallenge: boolean
|
|
74
|
+
}
|
|
75
|
+
| { phase: 'errorHandling'; endpoint: EndpointSpec; results: CheckResult[] }
|
|
76
|
+
| {
|
|
77
|
+
phase: 'payment'
|
|
78
|
+
endpoint: EndpointSpec
|
|
79
|
+
results: CheckResult[]
|
|
80
|
+
succeeded: boolean
|
|
81
|
+
body?: string | undefined
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Streams validation results as each phase completes. */
|
|
85
|
+
export async function* validateStream(options: ValidateOptions): AsyncGenerator<ValidateEvent> {
|
|
86
|
+
const baseUrl = options.url.replace(/\/$/, '').replace(/\/openapi\.json$/i, '')
|
|
87
|
+
const verbose = options.verbose ?? false
|
|
88
|
+
|
|
89
|
+
const discovery = await runDiscovery(baseUrl, options)
|
|
90
|
+
yield { phase: 'discovery', discovery, results: discovery.checks }
|
|
91
|
+
|
|
92
|
+
for (const endpoint of discovery.endpoints) {
|
|
93
|
+
yield { phase: 'endpoint', endpoint }
|
|
94
|
+
|
|
95
|
+
let body: string | undefined
|
|
96
|
+
if (options.endpoint) {
|
|
97
|
+
body = options.body
|
|
98
|
+
} else {
|
|
99
|
+
body = resolveBodyForEndpoint(options.body, endpoint.path)
|
|
100
|
+
if (!body && discovery.doc) {
|
|
101
|
+
body = extractRequestBodyFromDiscovery(discovery.doc, endpoint)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const { results: challengeResults, resolvedBody } = await validateChallenge(
|
|
106
|
+
baseUrl,
|
|
107
|
+
endpoint,
|
|
108
|
+
verbose,
|
|
109
|
+
{
|
|
110
|
+
body,
|
|
111
|
+
query: options.query,
|
|
112
|
+
discoveryDoc: discovery.doc ?? undefined,
|
|
113
|
+
},
|
|
114
|
+
)
|
|
115
|
+
const effectiveBody = resolvedBody ?? body
|
|
116
|
+
const isMpp = challengeResults.some(
|
|
117
|
+
(r) => r.severity === 'pass' && r.label === 'Challenge parseable',
|
|
118
|
+
)
|
|
119
|
+
const isTestnet = challengeResults.some(
|
|
120
|
+
(r) =>
|
|
121
|
+
r.severity === 'pass' &&
|
|
122
|
+
r.label.endsWith('Valid currency address') &&
|
|
123
|
+
r.detail === 'testnet',
|
|
124
|
+
)
|
|
125
|
+
const isNonMppPayment =
|
|
126
|
+
!isMpp && challengeResults.some((r) => r.label === 'Not an MPP endpoint')
|
|
127
|
+
const hasPaymentScheme = challengeResults.some(
|
|
128
|
+
(r) => r.severity === 'pass' && r.label === 'WWW-Authenticate header present',
|
|
129
|
+
)
|
|
130
|
+
const challengeFailed = challengeResults.some(
|
|
131
|
+
(r) => r.severity === 'fail' && r.label === 'Challenge parseable',
|
|
132
|
+
)
|
|
133
|
+
const isMalformedChallenge = !isMpp && hasPaymentScheme && challengeFailed
|
|
134
|
+
|
|
135
|
+
yield {
|
|
136
|
+
phase: 'challenge',
|
|
137
|
+
endpoint,
|
|
138
|
+
results: challengeResults,
|
|
139
|
+
isMpp,
|
|
140
|
+
isTestnet,
|
|
141
|
+
isNonMppPayment,
|
|
142
|
+
isMalformedChallenge,
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (isMpp) {
|
|
146
|
+
const errorResults = await validateErrorHandling(baseUrl, endpoint, {
|
|
147
|
+
body: effectiveBody,
|
|
148
|
+
query: options.query,
|
|
149
|
+
})
|
|
150
|
+
yield { phase: 'errorHandling', endpoint, results: errorResults }
|
|
151
|
+
|
|
152
|
+
if (!options.skipPayment) {
|
|
153
|
+
const onResults = options.onPaymentResults
|
|
154
|
+
const paymentResults = await validatePaymentFlow(baseUrl, endpoint, verbose, {
|
|
155
|
+
body: effectiveBody,
|
|
156
|
+
query: options.query,
|
|
157
|
+
yes: options.yes,
|
|
158
|
+
...(onResults && { onResults }),
|
|
159
|
+
})
|
|
160
|
+
const succeeded = paymentResults.some(
|
|
161
|
+
(r) => r.severity === 'pass' && r.label === 'Payment: successful',
|
|
162
|
+
)
|
|
163
|
+
yield { phase: 'payment', endpoint, results: paymentResults, succeeded }
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Runs the full validation suite and returns all results as a batch. */
|
|
170
|
+
export async function validate(options: ValidateOptions): Promise<ValidateResult> {
|
|
171
|
+
const baseUrl = options.url.replace(/\/$/, '').replace(/\/openapi\.json$/i, '')
|
|
172
|
+
let discovery: DiscoveryResult | undefined
|
|
173
|
+
const endpointResults: EndpointValidationResult[] = []
|
|
174
|
+
let current: EndpointValidationResult | undefined
|
|
175
|
+
|
|
176
|
+
let sawTestnet = false
|
|
177
|
+
let sawMainnet = false
|
|
178
|
+
let paymentSucceeded = false
|
|
179
|
+
let sawMppEndpoint = false
|
|
180
|
+
let sawNonMppPaymentEndpoint = false
|
|
181
|
+
let sawMalformedChallenge = false
|
|
182
|
+
|
|
183
|
+
for await (const event of validateStream(options)) {
|
|
184
|
+
switch (event.phase) {
|
|
185
|
+
case 'discovery':
|
|
186
|
+
discovery = event.discovery
|
|
187
|
+
break
|
|
188
|
+
case 'endpoint':
|
|
189
|
+
current = {
|
|
190
|
+
method: event.endpoint.method,
|
|
191
|
+
path: event.endpoint.path,
|
|
192
|
+
challenge: [],
|
|
193
|
+
errorHandling: [],
|
|
194
|
+
payment: [],
|
|
195
|
+
}
|
|
196
|
+
endpointResults.push(current)
|
|
197
|
+
break
|
|
198
|
+
case 'challenge':
|
|
199
|
+
if (current) current.challenge = event.results
|
|
200
|
+
if (event.isMpp) {
|
|
201
|
+
sawMppEndpoint = true
|
|
202
|
+
if (event.isTestnet) sawTestnet = true
|
|
203
|
+
else sawMainnet = true
|
|
204
|
+
}
|
|
205
|
+
if (event.isNonMppPayment) sawNonMppPaymentEndpoint = true
|
|
206
|
+
if (event.isMalformedChallenge) sawMalformedChallenge = true
|
|
207
|
+
break
|
|
208
|
+
case 'errorHandling':
|
|
209
|
+
if (current) current.errorHandling = event.results
|
|
210
|
+
break
|
|
211
|
+
case 'payment':
|
|
212
|
+
if (current) current.payment = event.results
|
|
213
|
+
if (event.succeeded) paymentSucceeded = true
|
|
214
|
+
break
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const summary = { passed: 0, failed: 0, warnings: 0, skipped: 0 }
|
|
219
|
+
if (discovery) {
|
|
220
|
+
for (const r of discovery.checks) {
|
|
221
|
+
if (r.severity === 'pass') summary.passed++
|
|
222
|
+
else if (r.severity === 'fail') summary.failed++
|
|
223
|
+
else if (r.severity === 'warn') summary.warnings++
|
|
224
|
+
else if (r.severity === 'skip') summary.skipped++
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
for (const ep of endpointResults) {
|
|
228
|
+
for (const r of [...ep.challenge, ...ep.errorHandling, ...ep.payment]) {
|
|
229
|
+
if (r.severity === 'pass') summary.passed++
|
|
230
|
+
else if (r.severity === 'fail') summary.failed++
|
|
231
|
+
else if (r.severity === 'warn') summary.warnings++
|
|
232
|
+
else if (r.severity === 'skip') summary.skipped++
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (!discovery) throw new Error('Discovery phase did not complete')
|
|
237
|
+
|
|
238
|
+
return {
|
|
239
|
+
url: baseUrl,
|
|
240
|
+
discovery,
|
|
241
|
+
endpoints: endpointResults,
|
|
242
|
+
summary,
|
|
243
|
+
flags: {
|
|
244
|
+
sawMppEndpoint,
|
|
245
|
+
sawNonMppPaymentEndpoint,
|
|
246
|
+
sawMalformedChallenge,
|
|
247
|
+
sawTestnet,
|
|
248
|
+
sawMainnet,
|
|
249
|
+
paymentSucceeded,
|
|
250
|
+
},
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
async function runDiscovery(baseUrl: string, options: ValidateOptions): Promise<DiscoveryResult> {
|
|
255
|
+
const checks: CheckResult[] = []
|
|
256
|
+
let endpoints: EndpointSpec[] = []
|
|
257
|
+
let discoveryDoc: Record<string, unknown> | null = null
|
|
258
|
+
let found = false
|
|
259
|
+
let valid = false
|
|
260
|
+
|
|
261
|
+
// Try the user's path first, then root, then /api.
|
|
262
|
+
const origin = new URL(baseUrl).origin
|
|
263
|
+
let candidates: string[]
|
|
264
|
+
if (options.discoveryPath) {
|
|
265
|
+
candidates = [
|
|
266
|
+
new URL(options.discoveryPath, baseUrl + '/').href.replace(/\/openapi\.json$/i, ''),
|
|
267
|
+
]
|
|
268
|
+
} else {
|
|
269
|
+
candidates = [baseUrl, origin, `${origin}/api`]
|
|
270
|
+
candidates = [...new Set(candidates)]
|
|
271
|
+
}
|
|
272
|
+
let discoveryResult: Awaited<ReturnType<typeof fetchDiscoveryDoc>> | undefined
|
|
273
|
+
const attemptedErrors: string[] = []
|
|
274
|
+
for (const candidate of candidates) {
|
|
275
|
+
discoveryResult = await fetchDiscoveryDoc(candidate)
|
|
276
|
+
if ('error' in discoveryResult) {
|
|
277
|
+
attemptedErrors.push(`${candidate}/openapi.json: ${discoveryResult.error}`)
|
|
278
|
+
} else {
|
|
279
|
+
break
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (!discoveryResult || 'error' in discoveryResult) {
|
|
284
|
+
checks.push({
|
|
285
|
+
label: 'Document found',
|
|
286
|
+
detail: attemptedErrors.join('; '),
|
|
287
|
+
hint: 'MPP servers must serve an OpenAPI document at /openapi.json with x-payment-info extensions.',
|
|
288
|
+
severity: 'fail',
|
|
289
|
+
})
|
|
290
|
+
} else {
|
|
291
|
+
found = true
|
|
292
|
+
checks.push({ label: 'Document found and parseable', severity: 'pass' })
|
|
293
|
+
|
|
294
|
+
const issues = validateDiscoveryDoc(discoveryResult.doc)
|
|
295
|
+
const errors = issues.filter((i) => i.severity === 'error')
|
|
296
|
+
const warnings = issues.filter((i) => i.severity === 'warning')
|
|
297
|
+
|
|
298
|
+
if (errors.length > 0) {
|
|
299
|
+
checks.push({
|
|
300
|
+
label: 'Valid OpenAPI structure',
|
|
301
|
+
detail: `${errors.length} error(s)`,
|
|
302
|
+
severity: 'fail',
|
|
303
|
+
})
|
|
304
|
+
for (const e of errors) {
|
|
305
|
+
checks.push({ label: e.message, detail: e.path, severity: 'fail' })
|
|
306
|
+
}
|
|
307
|
+
} else {
|
|
308
|
+
valid = true
|
|
309
|
+
checks.push({ label: 'Valid OpenAPI structure', severity: 'pass' })
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
for (const w of warnings) {
|
|
313
|
+
checks.push({ label: w.message, detail: w.path, severity: 'warn' })
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
discoveryDoc = discoveryResult.doc as Record<string, unknown>
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (options.endpoint) {
|
|
320
|
+
const parsed = parseEndpointArg(options.endpoint)
|
|
321
|
+
if (parsed) endpoints.push(parsed)
|
|
322
|
+
} else if (discoveryDoc) {
|
|
323
|
+
endpoints = extractEndpointsFromDiscovery(discoveryDoc)
|
|
324
|
+
|
|
325
|
+
const NO_AMOUNT = BigInt('999999999999999999')
|
|
326
|
+
endpoints.sort((a, b) => {
|
|
327
|
+
const aAmt = isValidIntegerAmount(a.amount) ? BigInt(a.amount!) : NO_AMOUNT
|
|
328
|
+
const bAmt = isValidIntegerAmount(b.amount) ? BigInt(b.amount!) : NO_AMOUNT
|
|
329
|
+
return aAmt < bAmt ? -1 : aAmt > bAmt ? 1 : 0
|
|
330
|
+
})
|
|
331
|
+
|
|
332
|
+
if (endpoints.length > 0) {
|
|
333
|
+
checks.push({
|
|
334
|
+
label: 'Paid endpoints found',
|
|
335
|
+
detail: `${endpoints.length} endpoint(s)`,
|
|
336
|
+
severity: 'pass',
|
|
337
|
+
})
|
|
338
|
+
} else {
|
|
339
|
+
checks.push({
|
|
340
|
+
label: 'Paid endpoints found',
|
|
341
|
+
detail: 'No endpoints with x-payment-info',
|
|
342
|
+
severity: 'warn',
|
|
343
|
+
})
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
return { found, valid, endpoints, checks, doc: discoveryDoc }
|
|
348
|
+
}
|