mppx 0.3.4 → 0.3.6
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/README.md +0 -52
- package/dist/Challenge.d.ts +8 -0
- package/dist/Challenge.d.ts.map +1 -1
- package/dist/Challenge.js +20 -4
- package/dist/Challenge.js.map +1 -1
- package/dist/cli.js +193 -66
- package/dist/cli.js.map +1 -1
- package/dist/internal/types.d.ts +10 -0
- package/dist/internal/types.d.ts.map +1 -1
- package/dist/proxy/internal/Headers.d.ts +2 -0
- package/dist/proxy/internal/Headers.d.ts.map +1 -1
- package/dist/proxy/internal/Headers.js +2 -0
- package/dist/proxy/internal/Headers.js.map +1 -1
- package/dist/proxy/internal/Route.d.ts +4 -0
- package/dist/proxy/internal/Route.d.ts.map +1 -1
- package/dist/proxy/internal/Route.js +4 -0
- package/dist/proxy/internal/Route.js.map +1 -1
- package/dist/server/Mppx.d.ts +2 -0
- package/dist/server/Mppx.d.ts.map +1 -1
- package/dist/server/Mppx.js +4 -3
- package/dist/server/Mppx.js.map +1 -1
- package/dist/server/NodeListener.d.ts +6 -0
- package/dist/server/NodeListener.d.ts.map +1 -1
- package/dist/server/NodeListener.js +6 -0
- package/dist/server/NodeListener.js.map +1 -1
- package/dist/server/Response.d.ts +17 -0
- package/dist/server/Response.d.ts.map +1 -1
- package/dist/server/Response.js +17 -0
- package/dist/server/Response.js.map +1 -1
- package/dist/tempo/client/ChannelOps.js.map +1 -1
- package/dist/tempo/internal/defaults.d.ts +34 -8
- package/dist/tempo/internal/defaults.d.ts.map +1 -1
- package/dist/tempo/internal/defaults.js +30 -8
- package/dist/tempo/internal/defaults.js.map +1 -1
- package/dist/tempo/server/Charge.js +2 -2
- package/dist/tempo/server/Charge.js.map +1 -1
- package/dist/tempo/server/Session.d.ts.map +1 -1
- package/dist/tempo/server/Session.js +8 -3
- package/dist/tempo/server/Session.js.map +1 -1
- package/package.json +1 -1
- package/src/Challenge.test.ts +201 -11
- package/src/Challenge.ts +34 -4
- package/src/Store.test.ts +93 -0
- package/src/cli.test.ts +233 -37
- package/src/cli.ts +229 -79
- package/src/client/Transport.test.ts +4 -4
- package/src/internal/env.test.ts +42 -0
- package/src/internal/types.ts +11 -0
- package/src/proxy/internal/Headers.ts +2 -0
- package/src/proxy/internal/Route.ts +4 -0
- package/src/server/Mppx.test.ts +173 -0
- package/src/server/Mppx.ts +6 -3
- package/src/server/NodeListener.ts +6 -0
- package/src/server/Response.ts +17 -0
- package/src/server/Transport.test.ts +5 -5
- package/src/tempo/client/ChannelOps.ts +1 -1
- package/src/tempo/internal/defaults.test.ts +94 -0
- package/src/tempo/internal/defaults.ts +41 -8
- package/src/tempo/server/Charge.test.ts +150 -0
- package/src/tempo/server/Charge.ts +2 -2
- package/src/tempo/server/Session.test.ts +241 -1
- package/src/tempo/server/Session.ts +8 -3
- package/src/tempo/server/internal/transport.test.ts +285 -0
- package/src/tempo/session/Voucher.test.ts +46 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { z } from 'mppx'
|
|
2
|
+
import { Challenge } from 'mppx'
|
|
2
3
|
import { Mppx as Mppx_server, tempo as tempo_server } from 'mppx/server'
|
|
3
4
|
import { type Address, createClient, type Hex } from 'viem'
|
|
4
5
|
import { Addresses } from 'viem/tempo'
|
|
@@ -1099,6 +1100,58 @@ describe('session', () => {
|
|
|
1099
1100
|
} as any)
|
|
1100
1101
|
expect(result).toBeUndefined()
|
|
1101
1102
|
})
|
|
1103
|
+
|
|
1104
|
+
test('returns undefined for voucher POST with content-length > 0 (content request)', () => {
|
|
1105
|
+
const server = createServer()
|
|
1106
|
+
const result = server.respond!({
|
|
1107
|
+
credential: {
|
|
1108
|
+
challenge: makeChallenge({
|
|
1109
|
+
channelId: '0x0000000000000000000000000000000000000000000000000000000000000001' as Hex,
|
|
1110
|
+
}),
|
|
1111
|
+
payload: { action: 'voucher' },
|
|
1112
|
+
},
|
|
1113
|
+
input: new Request('http://localhost', {
|
|
1114
|
+
method: 'POST',
|
|
1115
|
+
headers: { 'content-length': '42' },
|
|
1116
|
+
}),
|
|
1117
|
+
} as any)
|
|
1118
|
+
expect(result).toBeUndefined()
|
|
1119
|
+
})
|
|
1120
|
+
|
|
1121
|
+
test('returns undefined for voucher POST with transfer-encoding header (content request)', () => {
|
|
1122
|
+
const server = createServer()
|
|
1123
|
+
const result = server.respond!({
|
|
1124
|
+
credential: {
|
|
1125
|
+
challenge: makeChallenge({
|
|
1126
|
+
channelId: '0x0000000000000000000000000000000000000000000000000000000000000001' as Hex,
|
|
1127
|
+
}),
|
|
1128
|
+
payload: { action: 'voucher' },
|
|
1129
|
+
},
|
|
1130
|
+
input: new Request('http://localhost', {
|
|
1131
|
+
method: 'POST',
|
|
1132
|
+
headers: { 'transfer-encoding': 'chunked' },
|
|
1133
|
+
}),
|
|
1134
|
+
} as any)
|
|
1135
|
+
expect(result).toBeUndefined()
|
|
1136
|
+
})
|
|
1137
|
+
|
|
1138
|
+
test('returns 204 for voucher POST with content-length: 0', () => {
|
|
1139
|
+
const server = createServer()
|
|
1140
|
+
const result = server.respond!({
|
|
1141
|
+
credential: {
|
|
1142
|
+
challenge: makeChallenge({
|
|
1143
|
+
channelId: '0x0000000000000000000000000000000000000000000000000000000000000001' as Hex,
|
|
1144
|
+
}),
|
|
1145
|
+
payload: { action: 'voucher' },
|
|
1146
|
+
},
|
|
1147
|
+
input: new Request('http://localhost', {
|
|
1148
|
+
method: 'POST',
|
|
1149
|
+
headers: { 'content-length': '0' },
|
|
1150
|
+
}),
|
|
1151
|
+
} as any)
|
|
1152
|
+
expect(result).toBeInstanceOf(Response)
|
|
1153
|
+
expect((result as Response).status).toBe(204)
|
|
1154
|
+
})
|
|
1102
1155
|
})
|
|
1103
1156
|
|
|
1104
1157
|
describe('SSE', () => {
|
|
@@ -1282,6 +1335,193 @@ describe('monotonicity and TOCTOU (unit tests)', () => {
|
|
|
1282
1335
|
})
|
|
1283
1336
|
})
|
|
1284
1337
|
|
|
1338
|
+
describe('session default currency resolution', () => {
|
|
1339
|
+
const mockClient = createClient({ transport: http('http://localhost:1') })
|
|
1340
|
+
const mockMainnetClient = createClient({
|
|
1341
|
+
chain: {
|
|
1342
|
+
id: 4217,
|
|
1343
|
+
name: 'Tempo',
|
|
1344
|
+
nativeCurrency: { name: 'ETH', symbol: 'ETH', decimals: 18 },
|
|
1345
|
+
rpcUrls: { default: { http: ['http://localhost:1'] } },
|
|
1346
|
+
},
|
|
1347
|
+
transport: http('http://localhost:1'),
|
|
1348
|
+
})
|
|
1349
|
+
const mockTestnetClient = createClient({
|
|
1350
|
+
chain: {
|
|
1351
|
+
id: 42431,
|
|
1352
|
+
name: 'Tempo Testnet',
|
|
1353
|
+
nativeCurrency: { name: 'ETH', symbol: 'ETH', decimals: 18 },
|
|
1354
|
+
rpcUrls: { default: { http: ['http://localhost:1'] } },
|
|
1355
|
+
},
|
|
1356
|
+
transport: http('http://localhost:1'),
|
|
1357
|
+
})
|
|
1358
|
+
|
|
1359
|
+
test('mainnet (default) resolves to USDC', () => {
|
|
1360
|
+
const server = session({
|
|
1361
|
+
store: Store.memory(),
|
|
1362
|
+
getClient: () => mockClient,
|
|
1363
|
+
account: '0x0000000000000000000000000000000000000001',
|
|
1364
|
+
escrowContract: '0x0000000000000000000000000000000000000002',
|
|
1365
|
+
} as session.Parameters)
|
|
1366
|
+
expect(server.defaults?.currency).toBe('0x20C000000000000000000000b9537d11c60E8b50')
|
|
1367
|
+
})
|
|
1368
|
+
|
|
1369
|
+
test('testnet: true defaults to pathUSD', () => {
|
|
1370
|
+
const server = session({
|
|
1371
|
+
store: Store.memory(),
|
|
1372
|
+
getClient: () => mockClient,
|
|
1373
|
+
account: '0x0000000000000000000000000000000000000001',
|
|
1374
|
+
escrowContract: '0x0000000000000000000000000000000000000002',
|
|
1375
|
+
testnet: true,
|
|
1376
|
+
} as session.Parameters)
|
|
1377
|
+
expect(server.defaults?.currency).toBe('0x20c0000000000000000000000000000000000000')
|
|
1378
|
+
})
|
|
1379
|
+
|
|
1380
|
+
test('unknown chain defaults to pathUSD', () => {
|
|
1381
|
+
const server = session({
|
|
1382
|
+
store: Store.memory(),
|
|
1383
|
+
getClient: () => mockClient,
|
|
1384
|
+
account: '0x0000000000000000000000000000000000000001',
|
|
1385
|
+
escrowContract: '0x0000000000000000000000000000000000000002',
|
|
1386
|
+
chainId: 69420,
|
|
1387
|
+
} as session.Parameters)
|
|
1388
|
+
expect(server.defaults?.currency).toBe('0x20c0000000000000000000000000000000000000')
|
|
1389
|
+
})
|
|
1390
|
+
|
|
1391
|
+
test('explicit currency overrides default', () => {
|
|
1392
|
+
const server = session({
|
|
1393
|
+
store: Store.memory(),
|
|
1394
|
+
getClient: () => mockClient,
|
|
1395
|
+
account: '0x0000000000000000000000000000000000000001',
|
|
1396
|
+
currency: '0xcustom',
|
|
1397
|
+
escrowContract: '0x0000000000000000000000000000000000000002',
|
|
1398
|
+
chainId: 4217,
|
|
1399
|
+
testnet: false,
|
|
1400
|
+
} as session.Parameters)
|
|
1401
|
+
expect(server.defaults?.currency).toBe('0xcustom')
|
|
1402
|
+
})
|
|
1403
|
+
|
|
1404
|
+
test('decimals defaults to 6', () => {
|
|
1405
|
+
const server = session({
|
|
1406
|
+
store: Store.memory(),
|
|
1407
|
+
getClient: () => mockClient,
|
|
1408
|
+
account: '0x0000000000000000000000000000000000000001',
|
|
1409
|
+
escrowContract: '0x0000000000000000000000000000000000000002',
|
|
1410
|
+
chainId: 42431,
|
|
1411
|
+
} as session.Parameters)
|
|
1412
|
+
expect(server.defaults?.decimals).toBe(6)
|
|
1413
|
+
})
|
|
1414
|
+
|
|
1415
|
+
test('challenge contains USDC currency (mainnet default)', async () => {
|
|
1416
|
+
const handler = Mppx_server.create({
|
|
1417
|
+
methods: [
|
|
1418
|
+
tempo_server.session({
|
|
1419
|
+
store: Store.memory(),
|
|
1420
|
+
getClient: () => mockMainnetClient,
|
|
1421
|
+
account: '0x0000000000000000000000000000000000000001',
|
|
1422
|
+
escrowContract: '0x0000000000000000000000000000000000000002',
|
|
1423
|
+
chainId: 4217,
|
|
1424
|
+
testnet: false,
|
|
1425
|
+
}),
|
|
1426
|
+
],
|
|
1427
|
+
realm: 'api.example.com',
|
|
1428
|
+
secretKey: 'secret',
|
|
1429
|
+
})
|
|
1430
|
+
|
|
1431
|
+
const result = await (handler.session as Function)({
|
|
1432
|
+
amount: '1',
|
|
1433
|
+
decimals: 6,
|
|
1434
|
+
unitType: 'token',
|
|
1435
|
+
})(new Request('https://example.com'))
|
|
1436
|
+
expect(result.status).toBe(402)
|
|
1437
|
+
|
|
1438
|
+
const challenge = Challenge.fromResponse(result.challenge)
|
|
1439
|
+
expect(challenge.request.currency).toBe('0x20C000000000000000000000b9537d11c60E8b50')
|
|
1440
|
+
})
|
|
1441
|
+
|
|
1442
|
+
test('challenge contains pathUSD currency when testnet: true', async () => {
|
|
1443
|
+
const handler = Mppx_server.create({
|
|
1444
|
+
methods: [
|
|
1445
|
+
tempo_server.session({
|
|
1446
|
+
store: Store.memory(),
|
|
1447
|
+
getClient: () => mockTestnetClient,
|
|
1448
|
+
account: '0x0000000000000000000000000000000000000001',
|
|
1449
|
+
escrowContract: '0x0000000000000000000000000000000000000002',
|
|
1450
|
+
testnet: true,
|
|
1451
|
+
}),
|
|
1452
|
+
],
|
|
1453
|
+
realm: 'api.example.com',
|
|
1454
|
+
secretKey: 'secret',
|
|
1455
|
+
})
|
|
1456
|
+
|
|
1457
|
+
const result = await (handler.session as Function)({
|
|
1458
|
+
amount: '1',
|
|
1459
|
+
decimals: 6,
|
|
1460
|
+
unitType: 'token',
|
|
1461
|
+
chainId: 42431,
|
|
1462
|
+
})(new Request('https://example.com'))
|
|
1463
|
+
expect(result.status).toBe(402)
|
|
1464
|
+
|
|
1465
|
+
const challenge = Challenge.fromResponse(result.challenge)
|
|
1466
|
+
expect(challenge.request.currency).toBe('0x20c0000000000000000000000000000000000000')
|
|
1467
|
+
})
|
|
1468
|
+
|
|
1469
|
+
test('challenge contains pathUSD currency (unknown chain)', async () => {
|
|
1470
|
+
const handler = Mppx_server.create({
|
|
1471
|
+
methods: [
|
|
1472
|
+
tempo_server.session({
|
|
1473
|
+
store: Store.memory(),
|
|
1474
|
+
getClient: () => mockTestnetClient,
|
|
1475
|
+
account: '0x0000000000000000000000000000000000000001',
|
|
1476
|
+
escrowContract: '0x0000000000000000000000000000000000000002',
|
|
1477
|
+
chainId: 69420,
|
|
1478
|
+
}),
|
|
1479
|
+
],
|
|
1480
|
+
realm: 'api.example.com',
|
|
1481
|
+
secretKey: 'secret',
|
|
1482
|
+
})
|
|
1483
|
+
|
|
1484
|
+
const result = await (handler.session as Function)({
|
|
1485
|
+
amount: '1',
|
|
1486
|
+
decimals: 6,
|
|
1487
|
+
unitType: 'token',
|
|
1488
|
+
})(new Request('https://example.com'))
|
|
1489
|
+
expect(result.status).toBe(402)
|
|
1490
|
+
|
|
1491
|
+
const challenge = Challenge.fromResponse(result.challenge)
|
|
1492
|
+
expect(challenge.request.currency).toBe('0x20c0000000000000000000000000000000000000')
|
|
1493
|
+
})
|
|
1494
|
+
|
|
1495
|
+
test('explicit currency in challenge overrides testnet default', async () => {
|
|
1496
|
+
const handler = Mppx_server.create({
|
|
1497
|
+
methods: [
|
|
1498
|
+
tempo_server.session({
|
|
1499
|
+
store: Store.memory(),
|
|
1500
|
+
getClient: () => mockClient,
|
|
1501
|
+
account: '0x0000000000000000000000000000000000000001',
|
|
1502
|
+
currency: '0xcustom',
|
|
1503
|
+
escrowContract: '0x0000000000000000000000000000000000000002',
|
|
1504
|
+
chainId: 4217,
|
|
1505
|
+
testnet: false,
|
|
1506
|
+
}),
|
|
1507
|
+
],
|
|
1508
|
+
realm: 'api.example.com',
|
|
1509
|
+
secretKey: 'secret',
|
|
1510
|
+
})
|
|
1511
|
+
|
|
1512
|
+
const result = await handler.session({
|
|
1513
|
+
amount: '1',
|
|
1514
|
+
decimals: 6,
|
|
1515
|
+
unitType: 'token',
|
|
1516
|
+
})(new Request('https://example.com'))
|
|
1517
|
+
expect(result.status).toBe(402)
|
|
1518
|
+
if (result.status !== 402) throw new Error()
|
|
1519
|
+
|
|
1520
|
+
const challenge = Challenge.fromResponse(result.challenge)
|
|
1521
|
+
expect(challenge.request.currency).toBe('0xcustom')
|
|
1522
|
+
})
|
|
1523
|
+
})
|
|
1524
|
+
|
|
1285
1525
|
function nextSalt(): Hex {
|
|
1286
1526
|
saltCounter++
|
|
1287
1527
|
return `0x${saltCounter.toString(16).padStart(64, '0')}` as Hex
|
|
@@ -85,7 +85,7 @@ export function session<const parameters extends session.Parameters>(p?: paramet
|
|
|
85
85
|
const parameters = p as parameters
|
|
86
86
|
const {
|
|
87
87
|
amount,
|
|
88
|
-
currency,
|
|
88
|
+
currency = defaults.resolveCurrency(parameters),
|
|
89
89
|
decimals = defaults.decimals,
|
|
90
90
|
store: rawStore = Store.memory(),
|
|
91
91
|
suggestedDeposit,
|
|
@@ -127,7 +127,7 @@ export function session<const parameters extends session.Parameters>(p?: paramet
|
|
|
127
127
|
// Extract chainId from request or default.
|
|
128
128
|
const chainId = await (async () => {
|
|
129
129
|
if (request.chainId) return request.chainId
|
|
130
|
-
if (parameters.testnet) return defaults.
|
|
130
|
+
if (parameters.testnet) return defaults.chainId.testnet
|
|
131
131
|
return (await getClient({})).chain?.id
|
|
132
132
|
})()
|
|
133
133
|
|
|
@@ -156,7 +156,12 @@ export function session<const parameters extends session.Parameters>(p?: paramet
|
|
|
156
156
|
return undefined
|
|
157
157
|
})()
|
|
158
158
|
|
|
159
|
-
return {
|
|
159
|
+
return {
|
|
160
|
+
...request,
|
|
161
|
+
chainId,
|
|
162
|
+
escrowContract: resolvedEscrow,
|
|
163
|
+
feePayer: resolvedFeePayer,
|
|
164
|
+
}
|
|
160
165
|
},
|
|
161
166
|
|
|
162
167
|
async verify({ credential }) {
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import { Challenge, Credential } from 'mppx'
|
|
2
|
+
import type { Address, Hex } from 'viem'
|
|
3
|
+
import { describe, expect, test } from 'vitest'
|
|
4
|
+
import * as Store from '../../../Store.js'
|
|
5
|
+
import * as ChannelStore from '../../session/ChannelStore.js'
|
|
6
|
+
import { sse } from './transport.js'
|
|
7
|
+
|
|
8
|
+
const channelId = '0x0000000000000000000000000000000000000000000000000000000000000001' as Hex
|
|
9
|
+
const challengeId = 'challenge-1'
|
|
10
|
+
|
|
11
|
+
function memoryStore() {
|
|
12
|
+
return ChannelStore.fromStore(Store.memory())
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function seedChannel(
|
|
16
|
+
storage: ChannelStore.ChannelStore,
|
|
17
|
+
balance: bigint,
|
|
18
|
+
): Promise<ChannelStore.State | null> {
|
|
19
|
+
return storage.updateChannel(channelId, () => ({
|
|
20
|
+
channelId,
|
|
21
|
+
payer: '0x0000000000000000000000000000000000000001' as Address,
|
|
22
|
+
payee: '0x0000000000000000000000000000000000000002' as Address,
|
|
23
|
+
token: '0x0000000000000000000000000000000000000003' as Address,
|
|
24
|
+
authorizedSigner: '0x0000000000000000000000000000000000000004' as Address,
|
|
25
|
+
chainId: 42431,
|
|
26
|
+
escrowContract: '0x542831e3E4Ace07559b7C8787395f4Fb99F70787' as Address,
|
|
27
|
+
deposit: balance,
|
|
28
|
+
settledOnChain: 0n,
|
|
29
|
+
highestVoucherAmount: balance,
|
|
30
|
+
highestVoucher: null,
|
|
31
|
+
spent: 0n,
|
|
32
|
+
units: 0,
|
|
33
|
+
finalized: false,
|
|
34
|
+
createdAt: new Date().toISOString(),
|
|
35
|
+
}))
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function makeChallenge() {
|
|
39
|
+
return Challenge.from({
|
|
40
|
+
id: challengeId,
|
|
41
|
+
realm: 'test.example.com',
|
|
42
|
+
method: 'tempo',
|
|
43
|
+
intent: 'session',
|
|
44
|
+
request: {
|
|
45
|
+
amount: '1000000',
|
|
46
|
+
currency: '0x20c0000000000000000000000000000000000001',
|
|
47
|
+
recipient: '0x0000000000000000000000000000000000000002',
|
|
48
|
+
},
|
|
49
|
+
})
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function makeCredential() {
|
|
53
|
+
const challenge = makeChallenge()
|
|
54
|
+
return Credential.from({
|
|
55
|
+
challenge,
|
|
56
|
+
payload: {
|
|
57
|
+
action: 'voucher',
|
|
58
|
+
channelId,
|
|
59
|
+
cumulativeAmount: '1000000',
|
|
60
|
+
signature: '0xdeadbeef',
|
|
61
|
+
},
|
|
62
|
+
})
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function makeAuthorizedRequest(): Request {
|
|
66
|
+
const credential = makeCredential()
|
|
67
|
+
const header = Credential.serialize(credential)
|
|
68
|
+
return new Request('https://test.example.com/session', {
|
|
69
|
+
headers: { Authorization: header },
|
|
70
|
+
})
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function makeReceipt() {
|
|
74
|
+
return {
|
|
75
|
+
method: 'tempo',
|
|
76
|
+
status: 'success' as const,
|
|
77
|
+
timestamp: new Date().toISOString(),
|
|
78
|
+
reference: channelId,
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
describe('sse transport', () => {
|
|
83
|
+
test('getCredential returns null when no Authorization header', () => {
|
|
84
|
+
const store = memoryStore()
|
|
85
|
+
const transport = sse({ store })
|
|
86
|
+
const request = new Request('https://test.example.com/session')
|
|
87
|
+
expect(transport.getCredential(request)).toBeNull()
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
test('getCredential returns credential from Authorization header', () => {
|
|
91
|
+
const store = memoryStore()
|
|
92
|
+
const transport = sse({ store })
|
|
93
|
+
const request = makeAuthorizedRequest()
|
|
94
|
+
const credential = transport.getCredential(request)
|
|
95
|
+
expect(credential).not.toBeNull()
|
|
96
|
+
expect(credential!.challenge.id).toBe(challengeId)
|
|
97
|
+
expect((credential!.payload as any).channelId).toBe(channelId)
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
test('getCredential captures SSE context in contextMap', async () => {
|
|
101
|
+
const store = memoryStore()
|
|
102
|
+
await seedChannel(store, 10000000n)
|
|
103
|
+
const transport = sse({ store })
|
|
104
|
+
|
|
105
|
+
const request = makeAuthorizedRequest()
|
|
106
|
+
transport.getCredential(request)
|
|
107
|
+
|
|
108
|
+
async function* gen() {
|
|
109
|
+
yield 'test'
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const response = transport.respondReceipt({
|
|
113
|
+
receipt: makeReceipt(),
|
|
114
|
+
response: gen(),
|
|
115
|
+
challengeId,
|
|
116
|
+
})
|
|
117
|
+
expect(response.headers.get('Content-Type')).toContain('text/event-stream')
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
test('respondChallenge delegates to base http transport', async () => {
|
|
121
|
+
const store = memoryStore()
|
|
122
|
+
const transport = sse({ store })
|
|
123
|
+
const challenge = makeChallenge()
|
|
124
|
+
|
|
125
|
+
const response = await transport.respondChallenge({
|
|
126
|
+
challenge,
|
|
127
|
+
input: new Request('https://test.example.com/session'),
|
|
128
|
+
})
|
|
129
|
+
expect(response).toBeInstanceOf(Response)
|
|
130
|
+
expect(response.status).toBe(402)
|
|
131
|
+
expect(response.headers.get('WWW-Authenticate')).toContain('Payment')
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
test('respondReceipt with AsyncIterable produces SSE response', async () => {
|
|
135
|
+
const store = memoryStore()
|
|
136
|
+
await seedChannel(store, 10000000n)
|
|
137
|
+
const transport = sse({ store })
|
|
138
|
+
|
|
139
|
+
transport.getCredential(makeAuthorizedRequest())
|
|
140
|
+
|
|
141
|
+
async function* gen() {
|
|
142
|
+
yield 'hello'
|
|
143
|
+
yield 'world'
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const response = transport.respondReceipt({
|
|
147
|
+
receipt: makeReceipt(),
|
|
148
|
+
response: gen(),
|
|
149
|
+
challengeId,
|
|
150
|
+
})
|
|
151
|
+
expect(response.headers.get('Content-Type')).toContain('text/event-stream')
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
test('respondReceipt with AsyncGeneratorFunction passes stream controller', async () => {
|
|
155
|
+
const store = memoryStore()
|
|
156
|
+
await seedChannel(store, 10000000n)
|
|
157
|
+
const transport = sse({ store })
|
|
158
|
+
|
|
159
|
+
transport.getCredential(makeAuthorizedRequest())
|
|
160
|
+
|
|
161
|
+
const response = transport.respondReceipt({
|
|
162
|
+
receipt: makeReceipt(),
|
|
163
|
+
response: async function* (stream) {
|
|
164
|
+
await stream.charge()
|
|
165
|
+
yield 'hello'
|
|
166
|
+
},
|
|
167
|
+
challengeId,
|
|
168
|
+
})
|
|
169
|
+
expect(response.headers.get('Content-Type')).toContain('text/event-stream')
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
test('respondReceipt with upstream SSE Response auto-detects and iterates', async () => {
|
|
173
|
+
const store = memoryStore()
|
|
174
|
+
await seedChannel(store, 10000000n)
|
|
175
|
+
const transport = sse({ store })
|
|
176
|
+
|
|
177
|
+
transport.getCredential(makeAuthorizedRequest())
|
|
178
|
+
|
|
179
|
+
const encoder = new TextEncoder()
|
|
180
|
+
const upstream = new Response(
|
|
181
|
+
new ReadableStream({
|
|
182
|
+
start(controller) {
|
|
183
|
+
controller.enqueue(encoder.encode('event: message\ndata: chunk1\n\n'))
|
|
184
|
+
controller.enqueue(encoder.encode('event: message\ndata: chunk2\n\n'))
|
|
185
|
+
controller.close()
|
|
186
|
+
},
|
|
187
|
+
}),
|
|
188
|
+
{ headers: { 'Content-Type': 'text/event-stream; charset=utf-8' } },
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
const response = transport.respondReceipt({
|
|
192
|
+
receipt: makeReceipt(),
|
|
193
|
+
response: upstream,
|
|
194
|
+
challengeId,
|
|
195
|
+
})
|
|
196
|
+
expect(response.headers.get('Content-Type')).toContain('text/event-stream')
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
test('respondReceipt with plain Response delegates to base http transport', () => {
|
|
200
|
+
const store = memoryStore()
|
|
201
|
+
const transport = sse({ store })
|
|
202
|
+
const receipt = makeReceipt()
|
|
203
|
+
|
|
204
|
+
const plainResponse = new Response('ok', {
|
|
205
|
+
headers: { 'Content-Type': 'application/json' },
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
const response = transport.respondReceipt({
|
|
209
|
+
receipt,
|
|
210
|
+
response: plainResponse,
|
|
211
|
+
challengeId,
|
|
212
|
+
})
|
|
213
|
+
expect(response).toBeInstanceOf(Response)
|
|
214
|
+
expect(response.headers.get('Payment-Receipt')).toBeTruthy()
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
test('respondReceipt cleans up contextMap after use', async () => {
|
|
218
|
+
const store = memoryStore()
|
|
219
|
+
await seedChannel(store, 10000000n)
|
|
220
|
+
const transport = sse({ store })
|
|
221
|
+
|
|
222
|
+
transport.getCredential(makeAuthorizedRequest())
|
|
223
|
+
|
|
224
|
+
async function* gen() {
|
|
225
|
+
yield 'first'
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
transport.respondReceipt({
|
|
229
|
+
receipt: makeReceipt(),
|
|
230
|
+
response: gen(),
|
|
231
|
+
challengeId,
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
async function* gen2() {
|
|
235
|
+
yield 'second'
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
expect(() =>
|
|
239
|
+
transport.respondReceipt({
|
|
240
|
+
receipt: makeReceipt(),
|
|
241
|
+
response: gen2(),
|
|
242
|
+
challengeId,
|
|
243
|
+
}),
|
|
244
|
+
).toThrow('No SSE context available')
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
test('respondReceipt throws when no SSE context available', () => {
|
|
248
|
+
const store = memoryStore()
|
|
249
|
+
const transport = sse({ store })
|
|
250
|
+
|
|
251
|
+
async function* gen() {
|
|
252
|
+
yield 'hello'
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
expect(() =>
|
|
256
|
+
transport.respondReceipt({
|
|
257
|
+
receipt: makeReceipt(),
|
|
258
|
+
response: gen(),
|
|
259
|
+
challengeId,
|
|
260
|
+
}),
|
|
261
|
+
).toThrow('No SSE context available')
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
test('poll: true strips waitForUpdate from store', async () => {
|
|
265
|
+
const store = memoryStore()
|
|
266
|
+
;(store as any).waitForUpdate = async () => {}
|
|
267
|
+
await seedChannel(store, 10000000n)
|
|
268
|
+
|
|
269
|
+
const transport = sse({ store, poll: true })
|
|
270
|
+
|
|
271
|
+
transport.getCredential(makeAuthorizedRequest())
|
|
272
|
+
|
|
273
|
+
async function* gen() {
|
|
274
|
+
yield 'test'
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const response = transport.respondReceipt({
|
|
278
|
+
receipt: makeReceipt(),
|
|
279
|
+
response: gen(),
|
|
280
|
+
challengeId,
|
|
281
|
+
})
|
|
282
|
+
expect(response.headers.get('Content-Type')).toContain('text/event-stream')
|
|
283
|
+
expect(transport.name).toBe('sse')
|
|
284
|
+
})
|
|
285
|
+
})
|
|
@@ -131,4 +131,50 @@ describe('Voucher', () => {
|
|
|
131
131
|
expect(voucher.cumulativeAmount).toBe(5000000n)
|
|
132
132
|
expect(voucher.signature).toBe(sig)
|
|
133
133
|
})
|
|
134
|
+
|
|
135
|
+
test('parseVoucherFromPayload with zero amount', () => {
|
|
136
|
+
const sig =
|
|
137
|
+
'0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890ab' as const
|
|
138
|
+
const voucher = parseVoucherFromPayload(channelId, '0', sig)
|
|
139
|
+
expect(voucher.cumulativeAmount).toBe(0n)
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
test('verifyVoucher rejects wrong escrow contract', async () => {
|
|
143
|
+
const signature = await signVoucher(
|
|
144
|
+
client,
|
|
145
|
+
account,
|
|
146
|
+
{ channelId, cumulativeAmount },
|
|
147
|
+
escrowContract,
|
|
148
|
+
chainId,
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
const wrongEscrow = '0xabcdefabcdefabcdefabcdefabcdefabcdefabcd' as const
|
|
152
|
+
const isValid = await verifyVoucher(
|
|
153
|
+
wrongEscrow,
|
|
154
|
+
chainId,
|
|
155
|
+
{ channelId, cumulativeAmount, signature },
|
|
156
|
+
account.address,
|
|
157
|
+
)
|
|
158
|
+
expect(isValid).toBe(false)
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
test('signVoucher and verifyVoucher round-trip with zero amount', async () => {
|
|
162
|
+
const zeroAmount = 0n
|
|
163
|
+
const signature = await signVoucher(
|
|
164
|
+
client,
|
|
165
|
+
account,
|
|
166
|
+
{ channelId, cumulativeAmount: zeroAmount },
|
|
167
|
+
escrowContract,
|
|
168
|
+
chainId,
|
|
169
|
+
)
|
|
170
|
+
expect(signature).toMatch(/^0x/)
|
|
171
|
+
|
|
172
|
+
const isValid = await verifyVoucher(
|
|
173
|
+
escrowContract,
|
|
174
|
+
chainId,
|
|
175
|
+
{ channelId, cumulativeAmount: zeroAmount, signature },
|
|
176
|
+
account.address,
|
|
177
|
+
)
|
|
178
|
+
expect(isValid).toBe(true)
|
|
179
|
+
})
|
|
134
180
|
})
|