mppx 0.3.3 → 0.3.5
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/Errors.d.ts +7 -7
- package/dist/Errors.d.ts.map +1 -1
- package/dist/Errors.js +7 -7
- package/dist/Errors.js.map +1 -1
- package/dist/cli.js +280 -119
- package/dist/cli.js.map +1 -1
- package/dist/internal/env.js +2 -2
- package/dist/internal/env.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/tempo/client/ChannelOps.d.ts +5 -5
- package/dist/tempo/client/ChannelOps.d.ts.map +1 -1
- package/dist/tempo/client/ChannelOps.js +3 -3
- package/dist/tempo/client/ChannelOps.js.map +1 -1
- package/dist/tempo/client/Session.d.ts +2 -2
- package/dist/tempo/client/Session.d.ts.map +1 -1
- package/dist/tempo/client/Session.js +3 -3
- package/dist/tempo/client/Session.js.map +1 -1
- package/dist/tempo/client/SessionManager.d.ts +4 -4
- package/dist/tempo/client/SessionManager.d.ts.map +1 -1
- package/dist/tempo/client/SessionManager.js +4 -4
- package/dist/tempo/client/SessionManager.js.map +1 -1
- package/dist/tempo/index.d.ts +1 -1
- package/dist/tempo/index.d.ts.map +1 -1
- package/dist/tempo/index.js +1 -1
- package/dist/tempo/index.js.map +1 -1
- package/dist/tempo/server/Charge.js +1 -1
- package/dist/tempo/server/Charge.js.map +1 -1
- package/dist/tempo/server/Methods.d.ts +1 -1
- package/dist/tempo/server/Methods.d.ts.map +1 -1
- package/dist/tempo/server/Session.d.ts +8 -8
- package/dist/tempo/server/Session.d.ts.map +1 -1
- package/dist/tempo/server/Session.js +24 -24
- package/dist/tempo/server/Session.js.map +1 -1
- package/dist/tempo/server/index.d.ts +2 -2
- package/dist/tempo/server/index.d.ts.map +1 -1
- package/dist/tempo/server/index.js +2 -2
- package/dist/tempo/server/index.js.map +1 -1
- package/dist/tempo/server/internal/transport.d.ts +4 -4
- package/dist/tempo/server/internal/transport.d.ts.map +1 -1
- package/dist/tempo/server/internal/transport.js +3 -3
- package/dist/tempo/server/internal/transport.js.map +1 -1
- package/dist/tempo/session/Chain.d.ts.map +1 -0
- package/dist/tempo/session/Chain.js.map +1 -0
- package/dist/tempo/session/Channel.d.ts.map +1 -0
- package/dist/tempo/session/Channel.js.map +1 -0
- package/dist/tempo/session/ChannelStore.d.ts.map +1 -0
- package/dist/tempo/session/ChannelStore.js.map +1 -0
- package/dist/tempo/session/Receipt.d.ts +22 -0
- package/dist/tempo/session/Receipt.d.ts.map +1 -0
- package/dist/tempo/{stream → session}/Receipt.js +6 -6
- package/dist/tempo/session/Receipt.js.map +1 -0
- package/dist/tempo/{stream → session}/Sse.d.ts +7 -7
- package/dist/tempo/session/Sse.d.ts.map +1 -0
- package/dist/tempo/{stream → session}/Sse.js +4 -4
- package/dist/tempo/session/Sse.js.map +1 -0
- package/dist/tempo/{stream → session}/Types.d.ts +4 -4
- package/dist/tempo/session/Types.d.ts.map +1 -0
- package/dist/tempo/{stream → session}/Types.js.map +1 -1
- package/dist/tempo/session/Voucher.d.ts.map +1 -0
- package/dist/tempo/session/Voucher.js.map +1 -0
- package/dist/tempo/{stream → session}/escrow.abi.d.ts.map +1 -1
- package/dist/tempo/session/escrow.abi.js.map +1 -0
- package/dist/tempo/session/index.d.ts.map +1 -0
- package/dist/tempo/session/index.js.map +1 -0
- package/package.json +1 -1
- package/src/Challenge.test.ts +201 -11
- package/src/Challenge.ts +34 -4
- package/src/Errors.test.ts +10 -10
- package/src/Errors.ts +7 -7
- package/src/Store.test.ts +93 -0
- package/src/cli.test.ts +234 -38
- package/src/cli.ts +340 -135
- package/src/client/Transport.test.ts +4 -4
- package/src/internal/env.test.ts +42 -0
- package/src/internal/env.ts +2 -2
- package/src/middlewares/express.test.ts +1 -1
- package/src/middlewares/hono.test.ts +1 -1
- package/src/middlewares/nextjs.test.ts +1 -1
- package/src/server/Mppx.test.ts +173 -0
- package/src/server/Mppx.ts +6 -3
- package/src/server/Transport.test.ts +6 -6
- package/src/tempo/client/ChannelOps.test.ts +2 -2
- package/src/tempo/client/ChannelOps.ts +8 -8
- package/src/tempo/client/Session.test.ts +3 -3
- package/src/tempo/client/Session.ts +9 -9
- package/src/tempo/client/SessionManager.test.ts +3 -3
- package/src/tempo/client/SessionManager.ts +9 -9
- package/src/tempo/index.ts +1 -1
- package/src/tempo/server/Charge.ts +1 -1
- package/src/tempo/server/Session.test.ts +61 -9
- package/src/tempo/server/Session.ts +47 -47
- package/src/tempo/server/Sse.test.ts +3 -3
- package/src/tempo/server/index.ts +2 -2
- package/src/tempo/server/internal/transport.test.ts +285 -0
- package/src/tempo/server/internal/transport.ts +6 -6
- package/src/tempo/{stream → session}/Chain.test.ts +1 -1
- package/src/tempo/{stream → session}/Receipt.test.ts +16 -12
- package/src/tempo/{stream → session}/Receipt.ts +9 -9
- package/src/tempo/{stream → session}/Sse.test.ts +5 -5
- package/src/tempo/{stream → session}/Sse.ts +11 -11
- package/src/tempo/{stream → session}/Types.ts +4 -4
- package/dist/tempo/stream/Chain.d.ts.map +0 -1
- package/dist/tempo/stream/Chain.js.map +0 -1
- package/dist/tempo/stream/Channel.d.ts.map +0 -1
- package/dist/tempo/stream/Channel.js.map +0 -1
- package/dist/tempo/stream/ChannelStore.d.ts.map +0 -1
- package/dist/tempo/stream/ChannelStore.js.map +0 -1
- package/dist/tempo/stream/Receipt.d.ts +0 -22
- package/dist/tempo/stream/Receipt.d.ts.map +0 -1
- package/dist/tempo/stream/Receipt.js.map +0 -1
- package/dist/tempo/stream/Sse.d.ts.map +0 -1
- package/dist/tempo/stream/Sse.js.map +0 -1
- package/dist/tempo/stream/Types.d.ts.map +0 -1
- package/dist/tempo/stream/Voucher.d.ts.map +0 -1
- package/dist/tempo/stream/Voucher.js.map +0 -1
- package/dist/tempo/stream/escrow.abi.js.map +0 -1
- package/dist/tempo/stream/index.d.ts.map +0 -1
- package/dist/tempo/stream/index.js.map +0 -1
- /package/dist/tempo/{stream → session}/Chain.d.ts +0 -0
- /package/dist/tempo/{stream → session}/Chain.js +0 -0
- /package/dist/tempo/{stream → session}/Channel.d.ts +0 -0
- /package/dist/tempo/{stream → session}/Channel.js +0 -0
- /package/dist/tempo/{stream → session}/ChannelStore.d.ts +0 -0
- /package/dist/tempo/{stream → session}/ChannelStore.js +0 -0
- /package/dist/tempo/{stream → session}/Types.js +0 -0
- /package/dist/tempo/{stream → session}/Voucher.d.ts +0 -0
- /package/dist/tempo/{stream → session}/Voucher.js +0 -0
- /package/dist/tempo/{stream → session}/escrow.abi.d.ts +0 -0
- /package/dist/tempo/{stream → session}/escrow.abi.js +0 -0
- /package/dist/tempo/{stream → session}/index.d.ts +0 -0
- /package/dist/tempo/{stream → session}/index.js +0 -0
- /package/src/tempo/{stream → session}/Chain.ts +0 -0
- /package/src/tempo/{stream → session}/Channel.test.ts +0 -0
- /package/src/tempo/{stream → session}/Channel.ts +0 -0
- /package/src/tempo/{stream → session}/ChannelStore.test.ts +0 -0
- /package/src/tempo/{stream → session}/ChannelStore.ts +0 -0
- /package/src/tempo/{stream → session}/Voucher.test.ts +0 -0
- /package/src/tempo/{stream → session}/Voucher.ts +0 -0
- /package/src/tempo/{stream → session}/escrow.abi.ts +0 -0
- /package/src/tempo/{stream → session}/index.ts +0 -0
|
@@ -60,7 +60,7 @@ describe('http', () => {
|
|
|
60
60
|
expect(transport.getChallenge(response)).toMatchInlineSnapshot(`
|
|
61
61
|
{
|
|
62
62
|
"expires": "2025-01-01T00:00:00.000Z",
|
|
63
|
-
"id": "
|
|
63
|
+
"id": "z8dUi61lViOj6cwh_ISb_5X8nBJF2OjTydcEap8wX0o",
|
|
64
64
|
"intent": "charge",
|
|
65
65
|
"method": "tempo",
|
|
66
66
|
"realm": "api.example.com",
|
|
@@ -91,7 +91,7 @@ describe('http', () => {
|
|
|
91
91
|
const headers = result.headers as Headers
|
|
92
92
|
|
|
93
93
|
expect(headers.get('Authorization')).toMatchInlineSnapshot(
|
|
94
|
-
`"Payment
|
|
94
|
+
`"Payment eyJjaGFsbGVuZ2UiOnsiZXhwaXJlcyI6IjIwMjUtMDEtMDFUMDA6MDA6MDAuMDAwWiIsImlkIjoiejhkVWk2MWxWaU9qNmN3aF9JU2JfNVg4bkJKRjJPalR5ZGNFYXA4d1gwbyIsImludGVudCI6ImNoYXJnZSIsIm1ldGhvZCI6InRlbXBvIiwicmVhbG0iOiJhcGkuZXhhbXBsZS5jb20iLCJyZXF1ZXN0IjoiZXlKaGJXOTFiblFpT2lJeE1EQXdJaXdpWTNWeWNtVnVZM2tpT2lJd2VESXdZekF3TURBd01EQXdNREF3TURBd01EQXdNREF3TURBd01EQXdNREF3TURBd01EQXdNREVpTENKbGVIQnBjbVZ6SWpvaU1qQXlOUzB3TVMwd01WUXdNRG93TURvd01DNHdNREJhSWl3aWNtVmphWEJwWlc1MElqb2lNSGczTkRKa016VkRZelkyTXpSRE1EVXpNamt5TldFellqZzBORUpqT1dVM05UazFaamhtUlRBd0luMCJ9LCJwYXlsb2FkIjp7InNpZ25hdHVyZSI6IjB4YWJjMTIzIiwidHlwZSI6InRyYW5zYWN0aW9uIn19"`,
|
|
95
95
|
)
|
|
96
96
|
})
|
|
97
97
|
|
|
@@ -182,7 +182,7 @@ describe('mcp', () => {
|
|
|
182
182
|
expect(transport.getChallenge(response)).toMatchInlineSnapshot(`
|
|
183
183
|
{
|
|
184
184
|
"expires": "2025-01-01T00:00:00.000Z",
|
|
185
|
-
"id": "
|
|
185
|
+
"id": "z8dUi61lViOj6cwh_ISb_5X8nBJF2OjTydcEap8wX0o",
|
|
186
186
|
"intent": "charge",
|
|
187
187
|
"method": "tempo",
|
|
188
188
|
"realm": "api.example.com",
|
|
@@ -239,7 +239,7 @@ describe('mcp', () => {
|
|
|
239
239
|
"org.paymentauth/credential": {
|
|
240
240
|
"challenge": {
|
|
241
241
|
"expires": "2025-01-01T00:00:00.000Z",
|
|
242
|
-
"id": "
|
|
242
|
+
"id": "z8dUi61lViOj6cwh_ISb_5X8nBJF2OjTydcEap8wX0o",
|
|
243
243
|
"intent": "charge",
|
|
244
244
|
"method": "tempo",
|
|
245
245
|
"realm": "api.example.com",
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import * as Env from './env.js'
|
|
2
|
+
|
|
3
|
+
afterEach(() => {
|
|
4
|
+
vi.unstubAllEnvs()
|
|
5
|
+
})
|
|
6
|
+
|
|
7
|
+
describe('Env.get', () => {
|
|
8
|
+
test('returns default realm when no env vars are set', () => {
|
|
9
|
+
expect(Env.get('realm')).toBe('MPP Payment')
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
test('returns default secretKey when MPP_SECRET_KEY is not set', () => {
|
|
13
|
+
expect(Env.get('secretKey')).toBe('tmp')
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
test('returns MPP_SECRET_KEY when set', () => {
|
|
17
|
+
vi.stubEnv('MPP_SECRET_KEY', 'sk_live_abc123')
|
|
18
|
+
expect(Env.get('secretKey')).toBe('sk_live_abc123')
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
test('returns FLY_APP_NAME when set', () => {
|
|
22
|
+
vi.stubEnv('FLY_APP_NAME', 'my-fly-app')
|
|
23
|
+
expect(Env.get('realm')).toBe('my-fly-app')
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
test('FLY_APP_NAME takes precedence over HOST', () => {
|
|
27
|
+
vi.stubEnv('FLY_APP_NAME', 'fly-app')
|
|
28
|
+
vi.stubEnv('HOST', 'my-host')
|
|
29
|
+
expect(Env.get('realm')).toBe('fly-app')
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
test('HOST takes precedence over MPP_REALM', () => {
|
|
33
|
+
vi.stubEnv('HOST', 'my-host')
|
|
34
|
+
vi.stubEnv('MPP_REALM', 'custom-realm')
|
|
35
|
+
expect(Env.get('realm')).toBe('my-host')
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
test('falls through to later vars when earlier ones are unset', () => {
|
|
39
|
+
vi.stubEnv('MPP_REALM', 'fallback-realm')
|
|
40
|
+
expect(Env.get('realm')).toBe('fallback-realm')
|
|
41
|
+
})
|
|
42
|
+
})
|
package/src/internal/env.ts
CHANGED
|
@@ -42,12 +42,12 @@ export function get(key: keyof typeof variables): string {
|
|
|
42
42
|
/** Reads a single environment variable, probing available runtime APIs. */
|
|
43
43
|
function read(name: string): string | undefined {
|
|
44
44
|
try {
|
|
45
|
-
if (typeof process !== 'undefined' && process?.env) return process.env[name]
|
|
45
|
+
if (typeof process !== 'undefined' && process?.env) return process.env[name] || undefined
|
|
46
46
|
} catch {}
|
|
47
47
|
|
|
48
48
|
try {
|
|
49
49
|
const deno = (globalThis as any).Deno
|
|
50
|
-
if (deno?.env?.get) return deno.env.get(name)
|
|
50
|
+
if (deno?.env?.get) return deno.env.get(name) || undefined
|
|
51
51
|
} catch {}
|
|
52
52
|
|
|
53
53
|
return undefined
|
|
@@ -6,7 +6,7 @@ import { tempo as tempo_server } from 'mppx/server'
|
|
|
6
6
|
import type { Address } from 'viem'
|
|
7
7
|
import { Addresses } from 'viem/tempo'
|
|
8
8
|
import { beforeAll, describe, expect, test } from 'vitest'
|
|
9
|
-
import { deployEscrow } from '~test/tempo/
|
|
9
|
+
import { deployEscrow } from '~test/tempo/session.js'
|
|
10
10
|
import { accounts, asset, client, fundAccount } from '~test/tempo/viem.js'
|
|
11
11
|
|
|
12
12
|
function createServer(app: express.Express) {
|
|
@@ -7,7 +7,7 @@ import { tempo as tempo_server } from 'mppx/server'
|
|
|
7
7
|
import type { Address } from 'viem'
|
|
8
8
|
import { Addresses } from 'viem/tempo'
|
|
9
9
|
import { beforeAll, describe, expect, test } from 'vitest'
|
|
10
|
-
import { deployEscrow } from '~test/tempo/
|
|
10
|
+
import { deployEscrow } from '~test/tempo/session.js'
|
|
11
11
|
import { accounts, asset, client, fundAccount } from '~test/tempo/viem.js'
|
|
12
12
|
|
|
13
13
|
function createServer(app: Hono) {
|
|
@@ -6,7 +6,7 @@ import { tempo as tempo_server } from 'mppx/server'
|
|
|
6
6
|
import type { Address } from 'viem'
|
|
7
7
|
import { Addresses } from 'viem/tempo'
|
|
8
8
|
import { beforeAll, describe, expect, test } from 'vitest'
|
|
9
|
-
import { deployEscrow } from '~test/tempo/
|
|
9
|
+
import { deployEscrow } from '~test/tempo/session.js'
|
|
10
10
|
import { accounts, asset, client, fundAccount } from '~test/tempo/viem.js'
|
|
11
11
|
|
|
12
12
|
function createServer(handler: (request: Request) => Promise<Response> | Response) {
|
package/src/server/Mppx.test.ts
CHANGED
|
@@ -344,3 +344,176 @@ describe('receipt handling', () => {
|
|
|
344
344
|
expect(result.status).toBe(200)
|
|
345
345
|
})
|
|
346
346
|
})
|
|
347
|
+
|
|
348
|
+
describe('withReceipt', () => {
|
|
349
|
+
const mockCharge = Method.from({
|
|
350
|
+
name: 'mock',
|
|
351
|
+
intent: 'charge',
|
|
352
|
+
schema: {
|
|
353
|
+
credential: {
|
|
354
|
+
payload: z.object({ token: z.string() }),
|
|
355
|
+
},
|
|
356
|
+
request: z.object({
|
|
357
|
+
amount: z.string(),
|
|
358
|
+
currency: z.string(),
|
|
359
|
+
decimals: z.number(),
|
|
360
|
+
recipient: z.string(),
|
|
361
|
+
}),
|
|
362
|
+
},
|
|
363
|
+
})
|
|
364
|
+
|
|
365
|
+
function mockReceipt() {
|
|
366
|
+
return {
|
|
367
|
+
method: 'mock',
|
|
368
|
+
reference: 'tx-ref',
|
|
369
|
+
status: 'success' as const,
|
|
370
|
+
timestamp: new Date().toISOString(),
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
test('attaches Payment-Receipt header to response', async () => {
|
|
375
|
+
const mockMethod = Method.toServer(mockCharge, {
|
|
376
|
+
async verify() {
|
|
377
|
+
return mockReceipt()
|
|
378
|
+
},
|
|
379
|
+
})
|
|
380
|
+
|
|
381
|
+
const handler = Mppx.create({ methods: [mockMethod], realm, secretKey })
|
|
382
|
+
const handle = handler.charge({
|
|
383
|
+
amount: '1000',
|
|
384
|
+
currency: '0x0000000000000000000000000000000000000001',
|
|
385
|
+
decimals: 6,
|
|
386
|
+
expires: new Date(Date.now() + 60_000).toISOString(),
|
|
387
|
+
recipient: '0x0000000000000000000000000000000000000002',
|
|
388
|
+
})
|
|
389
|
+
|
|
390
|
+
const firstResult = await handle(new Request('https://example.com/resource'))
|
|
391
|
+
expect(firstResult.status).toBe(402)
|
|
392
|
+
if (firstResult.status !== 402) throw new Error()
|
|
393
|
+
|
|
394
|
+
const challenge = Challenge.fromResponse(firstResult.challenge)
|
|
395
|
+
const credential = Credential.from({ challenge, payload: { token: 'valid' } })
|
|
396
|
+
|
|
397
|
+
const result = await handle(
|
|
398
|
+
new Request('https://example.com/resource', {
|
|
399
|
+
headers: { Authorization: Credential.serialize(credential) },
|
|
400
|
+
}),
|
|
401
|
+
)
|
|
402
|
+
expect(result.status).toBe(200)
|
|
403
|
+
if (result.status !== 200) throw new Error()
|
|
404
|
+
|
|
405
|
+
const response = result.withReceipt(Response.json({ data: 'ok' }))
|
|
406
|
+
expect(response.headers.get('Payment-Receipt')).toBeTruthy()
|
|
407
|
+
const body = await response.json()
|
|
408
|
+
expect(body).toEqual({ data: 'ok' })
|
|
409
|
+
})
|
|
410
|
+
|
|
411
|
+
test('throws when called without response arg and no management response', async () => {
|
|
412
|
+
const mockMethod = Method.toServer(mockCharge, {
|
|
413
|
+
async verify() {
|
|
414
|
+
return mockReceipt()
|
|
415
|
+
},
|
|
416
|
+
})
|
|
417
|
+
|
|
418
|
+
const handler = Mppx.create({ methods: [mockMethod], realm, secretKey })
|
|
419
|
+
const handle = handler.charge({
|
|
420
|
+
amount: '1000',
|
|
421
|
+
currency: '0x0000000000000000000000000000000000000001',
|
|
422
|
+
decimals: 6,
|
|
423
|
+
expires: new Date(Date.now() + 60_000).toISOString(),
|
|
424
|
+
recipient: '0x0000000000000000000000000000000000000002',
|
|
425
|
+
})
|
|
426
|
+
|
|
427
|
+
const firstResult = await handle(new Request('https://example.com/resource'))
|
|
428
|
+
if (firstResult.status !== 402) throw new Error()
|
|
429
|
+
|
|
430
|
+
const challenge = Challenge.fromResponse(firstResult.challenge)
|
|
431
|
+
const credential = Credential.from({ challenge, payload: { token: 'valid' } })
|
|
432
|
+
|
|
433
|
+
const result = await handle(
|
|
434
|
+
new Request('https://example.com/resource', {
|
|
435
|
+
headers: { Authorization: Credential.serialize(credential) },
|
|
436
|
+
}),
|
|
437
|
+
)
|
|
438
|
+
expect(result.status).toBe(200)
|
|
439
|
+
if (result.status !== 200) throw new Error()
|
|
440
|
+
|
|
441
|
+
expect(() => result.withReceipt()).toThrow('withReceipt() requires a response argument')
|
|
442
|
+
})
|
|
443
|
+
|
|
444
|
+
test('returns management response when respond hook returns Response', async () => {
|
|
445
|
+
const mockMethodWithRespond = Method.toServer(mockCharge, {
|
|
446
|
+
async verify() {
|
|
447
|
+
return mockReceipt()
|
|
448
|
+
},
|
|
449
|
+
respond() {
|
|
450
|
+
return new Response(null, { status: 204 })
|
|
451
|
+
},
|
|
452
|
+
})
|
|
453
|
+
|
|
454
|
+
const handler = Mppx.create({ methods: [mockMethodWithRespond], realm, secretKey })
|
|
455
|
+
const handle = handler.charge({
|
|
456
|
+
amount: '1000',
|
|
457
|
+
currency: '0x0000000000000000000000000000000000000001',
|
|
458
|
+
decimals: 6,
|
|
459
|
+
expires: new Date(Date.now() + 60_000).toISOString(),
|
|
460
|
+
recipient: '0x0000000000000000000000000000000000000002',
|
|
461
|
+
})
|
|
462
|
+
|
|
463
|
+
const firstResult = await handle(new Request('https://example.com/resource'))
|
|
464
|
+
if (firstResult.status !== 402) throw new Error()
|
|
465
|
+
|
|
466
|
+
const challenge = Challenge.fromResponse(firstResult.challenge)
|
|
467
|
+
const credential = Credential.from({ challenge, payload: { token: 'valid' } })
|
|
468
|
+
|
|
469
|
+
const result = await handle(
|
|
470
|
+
new Request('https://example.com/resource', {
|
|
471
|
+
headers: { Authorization: Credential.serialize(credential) },
|
|
472
|
+
}),
|
|
473
|
+
)
|
|
474
|
+
expect(result.status).toBe(200)
|
|
475
|
+
if (result.status !== 200) throw new Error()
|
|
476
|
+
|
|
477
|
+
const response = result.withReceipt()
|
|
478
|
+
expect(response.status).toBe(204)
|
|
479
|
+
expect(response.headers.get('Payment-Receipt')).toBeTruthy()
|
|
480
|
+
})
|
|
481
|
+
|
|
482
|
+
test('toNodeListener sets Payment-Receipt header on 200', async () => {
|
|
483
|
+
const mockMethod = Method.toServer(mockCharge, {
|
|
484
|
+
async verify() {
|
|
485
|
+
return mockReceipt()
|
|
486
|
+
},
|
|
487
|
+
})
|
|
488
|
+
|
|
489
|
+
const handler = Mppx.create({ methods: [mockMethod], realm, secretKey })
|
|
490
|
+
|
|
491
|
+
const server = await Http.createServer(async (req, res) => {
|
|
492
|
+
const result = await Mppx.toNodeListener(
|
|
493
|
+
handler.charge({
|
|
494
|
+
amount: '1000',
|
|
495
|
+
currency: '0x0000000000000000000000000000000000000001',
|
|
496
|
+
decimals: 6,
|
|
497
|
+
expires: new Date(Date.now() + 60_000).toISOString(),
|
|
498
|
+
recipient: '0x0000000000000000000000000000000000000002',
|
|
499
|
+
}),
|
|
500
|
+
)(req, res)
|
|
501
|
+
if (result.status === 402) return
|
|
502
|
+
res.end('OK')
|
|
503
|
+
})
|
|
504
|
+
|
|
505
|
+
const firstResponse = await fetch(server.url)
|
|
506
|
+
expect(firstResponse.status).toBe(402)
|
|
507
|
+
|
|
508
|
+
const challenge = Challenge.fromResponse(firstResponse)
|
|
509
|
+
const credential = Credential.from({ challenge, payload: { token: 'valid' } })
|
|
510
|
+
|
|
511
|
+
const response = await fetch(server.url, {
|
|
512
|
+
headers: { Authorization: Credential.serialize(credential) },
|
|
513
|
+
})
|
|
514
|
+
expect(response.status).toBe(200)
|
|
515
|
+
expect(response.headers.get('Payment-Receipt')).toBeTruthy()
|
|
516
|
+
|
|
517
|
+
server.close()
|
|
518
|
+
})
|
|
519
|
+
})
|
package/src/server/Mppx.ts
CHANGED
|
@@ -126,14 +126,14 @@ function createMethodFn(parameters: createMethodFn.Parameters): createMethodFn.R
|
|
|
126
126
|
const { defaults, method, realm, respond, secretKey, transport, verify } = parameters
|
|
127
127
|
|
|
128
128
|
return (options) => {
|
|
129
|
-
const
|
|
129
|
+
const methodMeta = {
|
|
130
130
|
...method,
|
|
131
131
|
...defaults,
|
|
132
132
|
...options,
|
|
133
133
|
}
|
|
134
134
|
return Object.assign(
|
|
135
135
|
async (input: Transport.InputOf): Promise<MethodFn.Response> => {
|
|
136
|
-
const { description, ...rest } = options
|
|
136
|
+
const { description, meta, ...rest } = options
|
|
137
137
|
const expires = 'expires' in options ? (options.expires as string | undefined) : undefined
|
|
138
138
|
|
|
139
139
|
// Merge defaults with per-request options
|
|
@@ -164,6 +164,7 @@ function createMethodFn(parameters: createMethodFn.Parameters): createMethodFn.R
|
|
|
164
164
|
const challenge = Challenge.fromMethod(method, {
|
|
165
165
|
description,
|
|
166
166
|
expires,
|
|
167
|
+
meta,
|
|
167
168
|
realm,
|
|
168
169
|
request,
|
|
169
170
|
secretKey,
|
|
@@ -261,7 +262,7 @@ function createMethodFn(parameters: createMethodFn.Parameters): createMethodFn.R
|
|
|
261
262
|
},
|
|
262
263
|
}
|
|
263
264
|
},
|
|
264
|
-
{ _internal:
|
|
265
|
+
{ _internal: methodMeta },
|
|
265
266
|
)
|
|
266
267
|
}
|
|
267
268
|
}
|
|
@@ -309,6 +310,8 @@ declare namespace MethodFn {
|
|
|
309
310
|
description?: string | undefined
|
|
310
311
|
/** Optional challenge expiration timestamp (ISO 8601). */
|
|
311
312
|
expires?: string | undefined
|
|
313
|
+
/** Optional server-defined correlation data (serialized as `opaque` in the request). Flat string-to-string map; clients MUST NOT modify. */
|
|
314
|
+
meta?: Record<string, string> | undefined
|
|
312
315
|
} & Method.WithDefaults<z.input<method['schema']['request']>, defaults>
|
|
313
316
|
|
|
314
317
|
export type Response<transport extends Transport.AnyTransport = Transport.Http> =
|
|
@@ -43,7 +43,7 @@ describe('http', () => {
|
|
|
43
43
|
{
|
|
44
44
|
"challenge": {
|
|
45
45
|
"expires": "2025-01-01T00:00:00.000Z",
|
|
46
|
-
"id": "
|
|
46
|
+
"id": "4XKyFaMO73Ypu-wOofzu3F8pRIt8vb7zxmWB2GgHAsE",
|
|
47
47
|
"intent": "charge",
|
|
48
48
|
"method": "tempo",
|
|
49
49
|
"realm": "api.example.com",
|
|
@@ -93,7 +93,7 @@ describe('http', () => {
|
|
|
93
93
|
{
|
|
94
94
|
"headers": {
|
|
95
95
|
"cache-control": "no-store",
|
|
96
|
-
"www-authenticate": "Payment id="
|
|
96
|
+
"www-authenticate": "Payment id="4XKyFaMO73Ypu-wOofzu3F8pRIt8vb7zxmWB2GgHAsE", realm="api.example.com", method="tempo", intent="charge", request="eyJhbW91bnQiOiIxMDAwMDAwMDAwIiwiY3VycmVuY3kiOiIweDIwYzAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDEiLCJleHBpcmVzIjoiMjAyNS0wMS0wMVQwMDowMDowMC4wMDBaIiwicmVjaXBpZW50IjoiMHg3NDJkMzVDYzY2MzRDMDUzMjkyNWEzYjg0NEJjOWU3NTk1ZjhmRTAwIn0", expires="2025-01-01T00:00:00.000Z"",
|
|
97
97
|
},
|
|
98
98
|
"status": 402,
|
|
99
99
|
}
|
|
@@ -124,7 +124,7 @@ describe('http', () => {
|
|
|
124
124
|
|
|
125
125
|
expect(response.status).toBe(410)
|
|
126
126
|
const body = await response.json()
|
|
127
|
-
expect(body.type).toBe('https://paymentauth.org/problems/
|
|
127
|
+
expect(body.type).toBe('https://paymentauth.org/problems/session/channel-finalized')
|
|
128
128
|
expect(body.status).toBe(410)
|
|
129
129
|
})
|
|
130
130
|
})
|
|
@@ -183,7 +183,7 @@ describe('mcp', () => {
|
|
|
183
183
|
{
|
|
184
184
|
"challenge": {
|
|
185
185
|
"expires": "2025-01-01T00:00:00.000Z",
|
|
186
|
-
"id": "
|
|
186
|
+
"id": "4XKyFaMO73Ypu-wOofzu3F8pRIt8vb7zxmWB2GgHAsE",
|
|
187
187
|
"intent": "charge",
|
|
188
188
|
"method": "tempo",
|
|
189
189
|
"realm": "api.example.com",
|
|
@@ -221,7 +221,7 @@ describe('mcp', () => {
|
|
|
221
221
|
"challenges": [
|
|
222
222
|
{
|
|
223
223
|
"expires": "2025-01-01T00:00:00.000Z",
|
|
224
|
-
"id": "
|
|
224
|
+
"id": "4XKyFaMO73Ypu-wOofzu3F8pRIt8vb7zxmWB2GgHAsE",
|
|
225
225
|
"intent": "charge",
|
|
226
226
|
"method": "tempo",
|
|
227
227
|
"realm": "api.example.com",
|
|
@@ -262,7 +262,7 @@ describe('mcp', () => {
|
|
|
262
262
|
"result": {
|
|
263
263
|
"_meta": {
|
|
264
264
|
"org.paymentauth/receipt": {
|
|
265
|
-
"challengeId": "
|
|
265
|
+
"challengeId": "4XKyFaMO73Ypu-wOofzu3F8pRIt8vb7zxmWB2GgHAsE",
|
|
266
266
|
"method": "tempo",
|
|
267
267
|
"reference": "0xtxhash",
|
|
268
268
|
"status": "success",
|
|
@@ -3,11 +3,11 @@ import { type Address, createClient } from 'viem'
|
|
|
3
3
|
import { privateKeyToAccount } from 'viem/accounts'
|
|
4
4
|
import { Addresses } from 'viem/tempo'
|
|
5
5
|
import { beforeAll, describe, expect, test } from 'vitest'
|
|
6
|
-
import { deployEscrow, openChannel } from '~test/tempo/
|
|
6
|
+
import { deployEscrow, openChannel } from '~test/tempo/session.js'
|
|
7
7
|
import { accounts, asset, chain, client, fundAccount, http } from '~test/tempo/viem.js'
|
|
8
8
|
import type { Challenge } from '../../Challenge.js'
|
|
9
9
|
import * as Credential from '../../Credential.js'
|
|
10
|
-
import { verifyVoucher } from '../
|
|
10
|
+
import { verifyVoucher } from '../session/Voucher.js'
|
|
11
11
|
import {
|
|
12
12
|
createClosePayload,
|
|
13
13
|
createOpenPayload,
|
|
@@ -18,10 +18,10 @@ import { Abis } from 'viem/tempo'
|
|
|
18
18
|
import type { Challenge } from '../../Challenge.js'
|
|
19
19
|
import * as Credential from '../../Credential.js'
|
|
20
20
|
import * as defaults from '../internal/defaults.js'
|
|
21
|
-
import { escrowAbi, getOnChainChannel } from '../
|
|
22
|
-
import * as Channel from '../
|
|
23
|
-
import type {
|
|
24
|
-
import { signVoucher } from '../
|
|
21
|
+
import { escrowAbi, getOnChainChannel } from '../session/Chain.js'
|
|
22
|
+
import * as Channel from '../session/Channel.js'
|
|
23
|
+
import type { SessionCredentialPayload } from '../session/Types.js'
|
|
24
|
+
import { signVoucher } from '../session/Voucher.js'
|
|
25
25
|
|
|
26
26
|
export type ChannelEntry = {
|
|
27
27
|
channelId: Hex.Hex
|
|
@@ -57,7 +57,7 @@ export function resolveEscrow(
|
|
|
57
57
|
|
|
58
58
|
export function serializeCredential(
|
|
59
59
|
challenge: Challenge,
|
|
60
|
-
payload:
|
|
60
|
+
payload: SessionCredentialPayload,
|
|
61
61
|
chainId: number,
|
|
62
62
|
account: viem_Account,
|
|
63
63
|
): string {
|
|
@@ -76,7 +76,7 @@ export async function createVoucherPayload(
|
|
|
76
76
|
escrowContract: Address,
|
|
77
77
|
chainId: number,
|
|
78
78
|
authorizedSigner?: Address | undefined,
|
|
79
|
-
): Promise<
|
|
79
|
+
): Promise<SessionCredentialPayload> {
|
|
80
80
|
const signature = await signVoucher(
|
|
81
81
|
client,
|
|
82
82
|
account,
|
|
@@ -101,7 +101,7 @@ export async function createClosePayload(
|
|
|
101
101
|
escrowContract: Address,
|
|
102
102
|
chainId: number,
|
|
103
103
|
authorizedSigner?: Address | undefined,
|
|
104
|
-
): Promise<
|
|
104
|
+
): Promise<SessionCredentialPayload> {
|
|
105
105
|
const signature = await signVoucher(
|
|
106
106
|
client,
|
|
107
107
|
account,
|
|
@@ -131,7 +131,7 @@ export async function createOpenPayload(
|
|
|
131
131
|
chainId: number
|
|
132
132
|
feePayer?: boolean | undefined
|
|
133
133
|
},
|
|
134
|
-
): Promise<{ entry: ChannelEntry; payload:
|
|
134
|
+
): Promise<{ entry: ChannelEntry; payload: SessionCredentialPayload }> {
|
|
135
135
|
const { escrowContract, payee, currency, deposit, initialAmount, chainId, feePayer } = options
|
|
136
136
|
const authorizedSigner = options.authorizedSigner ?? account.address
|
|
137
137
|
|
|
@@ -2,15 +2,15 @@ import { type Address, createClient, type Hex, http } from 'viem'
|
|
|
2
2
|
import { privateKeyToAccount } from 'viem/accounts'
|
|
3
3
|
import { Addresses } from 'viem/tempo'
|
|
4
4
|
import { beforeAll, describe, expect, test } from 'vitest'
|
|
5
|
-
import { deployEscrow, openChannel } from '~test/tempo/
|
|
5
|
+
import { deployEscrow, openChannel } from '~test/tempo/session.js'
|
|
6
6
|
import { accounts, asset, chain, client, fundAccount } from '~test/tempo/viem.js'
|
|
7
7
|
import * as Challenge from '../../Challenge.js'
|
|
8
8
|
import * as Credential from '../../Credential.js'
|
|
9
|
-
import type {
|
|
9
|
+
import type { SessionCredentialPayload } from '../session/Types.js'
|
|
10
10
|
import { session } from './Session.js'
|
|
11
11
|
|
|
12
12
|
function deserializePayload(result: string) {
|
|
13
|
-
const cred = Credential.deserialize<
|
|
13
|
+
const cred = Credential.deserialize<SessionCredentialPayload>(result)
|
|
14
14
|
return cred
|
|
15
15
|
}
|
|
16
16
|
|
|
@@ -8,8 +8,8 @@ import * as Client from '../../viem/Client.js'
|
|
|
8
8
|
import * as z from '../../zod.js'
|
|
9
9
|
import * as defaults from '../internal/defaults.js'
|
|
10
10
|
import * as Methods from '../Methods.js'
|
|
11
|
-
import type {
|
|
12
|
-
import { signVoucher } from '../
|
|
11
|
+
import type { SessionCredentialPayload } from '../session/Types.js'
|
|
12
|
+
import { signVoucher } from '../session/Voucher.js'
|
|
13
13
|
import {
|
|
14
14
|
type ChannelEntry,
|
|
15
15
|
createOpenPayload,
|
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
tryRecoverChannel,
|
|
20
20
|
} from './ChannelOps.js'
|
|
21
21
|
|
|
22
|
-
export const
|
|
22
|
+
export const sessionContextSchema = z.object({
|
|
23
23
|
account: z.optional(z.custom<Account.getResolver.Parameters['account']>()),
|
|
24
24
|
action: z.optional(z.enum(['open', 'topUp', 'voucher', 'close'])),
|
|
25
25
|
channelId: z.optional(z.string()),
|
|
@@ -32,7 +32,7 @@ export const streamContextSchema = z.object({
|
|
|
32
32
|
depositRaw: z.optional(z.string()),
|
|
33
33
|
})
|
|
34
34
|
|
|
35
|
-
export type
|
|
35
|
+
export type SessionContext = z.infer<typeof sessionContextSchema>
|
|
36
36
|
|
|
37
37
|
/**
|
|
38
38
|
* Creates a session payment method for use with `Mppx.create()`.
|
|
@@ -112,7 +112,7 @@ export function session(parameters: session.Parameters = {}) {
|
|
|
112
112
|
async function autoManageCredential(
|
|
113
113
|
challenge: Challenge.Challenge,
|
|
114
114
|
account: viem_Account,
|
|
115
|
-
context?:
|
|
115
|
+
context?: SessionContext,
|
|
116
116
|
): Promise<string> {
|
|
117
117
|
const md = challenge.request.methodDetails as
|
|
118
118
|
| { chainId?: number; escrowContract?: string; channelId?: string; feePayer?: boolean }
|
|
@@ -174,7 +174,7 @@ export function session(parameters: session.Parameters = {}) {
|
|
|
174
174
|
}
|
|
175
175
|
}
|
|
176
176
|
|
|
177
|
-
let payload:
|
|
177
|
+
let payload: SessionCredentialPayload
|
|
178
178
|
|
|
179
179
|
if (entry?.opened) {
|
|
180
180
|
entry.cumulativeAmount += amount
|
|
@@ -212,7 +212,7 @@ export function session(parameters: session.Parameters = {}) {
|
|
|
212
212
|
async function manualCredential(
|
|
213
213
|
challenge: Challenge.Challenge,
|
|
214
214
|
account: viem_Account,
|
|
215
|
-
context:
|
|
215
|
+
context: SessionContext,
|
|
216
216
|
): Promise<string> {
|
|
217
217
|
const md = challenge.request.methodDetails as
|
|
218
218
|
| { chainId?: number; escrowContract?: string; channelId?: string }
|
|
@@ -242,7 +242,7 @@ export function session(parameters: session.Parameters = {}) {
|
|
|
242
242
|
const escrowContract = resolveEscrowCached(challenge, chainId, channelId)
|
|
243
243
|
escrowContractMap.set(channelId, escrowContract)
|
|
244
244
|
|
|
245
|
-
let payload:
|
|
245
|
+
let payload: SessionCredentialPayload
|
|
246
246
|
|
|
247
247
|
switch (action) {
|
|
248
248
|
case 'open': {
|
|
@@ -341,7 +341,7 @@ export function session(parameters: session.Parameters = {}) {
|
|
|
341
341
|
}
|
|
342
342
|
|
|
343
343
|
return Method.toClient(Methods.session, {
|
|
344
|
-
context:
|
|
344
|
+
context: sessionContextSchema,
|
|
345
345
|
|
|
346
346
|
async createCredential({ challenge, context }) {
|
|
347
347
|
const chainId = challenge.request.methodDetails?.chainId ?? 0
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { Hex } from 'viem'
|
|
2
2
|
import { describe, expect, test, vi } from 'vitest'
|
|
3
3
|
import * as Challenge from '../../Challenge.js'
|
|
4
|
-
import { formatNeedVoucherEvent, parseEvent } from '../
|
|
5
|
-
import type { NeedVoucherEvent,
|
|
4
|
+
import { formatNeedVoucherEvent, parseEvent } from '../session/Sse.js'
|
|
5
|
+
import type { NeedVoucherEvent, SessionReceipt } from '../session/Types.js'
|
|
6
6
|
import { sessionManager } from './SessionManager.js'
|
|
7
7
|
|
|
8
8
|
const channelId = '0x0000000000000000000000000000000000000000000000000000000000000001' as Hex
|
|
@@ -139,7 +139,7 @@ describe('Session', () => {
|
|
|
139
139
|
acceptedCumulative: '2000000',
|
|
140
140
|
spent: '2000000',
|
|
141
141
|
units: 2,
|
|
142
|
-
} satisfies
|
|
142
|
+
} satisfies SessionReceipt)}\n\n`,
|
|
143
143
|
]
|
|
144
144
|
|
|
145
145
|
let callCount = 0
|
|
@@ -4,9 +4,9 @@ import type * as Challenge from '../../Challenge.js'
|
|
|
4
4
|
import * as Fetch from '../../client/internal/Fetch.js'
|
|
5
5
|
import type * as Account from '../../viem/Account.js'
|
|
6
6
|
import type * as Client from '../../viem/Client.js'
|
|
7
|
-
import {
|
|
8
|
-
import { parseEvent } from '../
|
|
9
|
-
import type {
|
|
7
|
+
import { deserializeSessionReceipt } from '../session/Receipt.js'
|
|
8
|
+
import { parseEvent } from '../session/Sse.js'
|
|
9
|
+
import type { SessionReceipt } from '../session/Types.js'
|
|
10
10
|
import type { ChannelEntry } from './ChannelOps.js'
|
|
11
11
|
import { session as sessionPlugin } from './Session.js'
|
|
12
12
|
|
|
@@ -20,15 +20,15 @@ export type SessionManager = {
|
|
|
20
20
|
sse(
|
|
21
21
|
input: RequestInfo | URL,
|
|
22
22
|
init?: RequestInit & {
|
|
23
|
-
onReceipt?: ((receipt:
|
|
23
|
+
onReceipt?: ((receipt: SessionReceipt) => void) | undefined
|
|
24
24
|
signal?: AbortSignal | undefined
|
|
25
25
|
},
|
|
26
26
|
): Promise<AsyncIterable<string>>
|
|
27
|
-
close(): Promise<
|
|
27
|
+
close(): Promise<SessionReceipt | undefined>
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
export type PaymentResponse = Response & {
|
|
31
|
-
receipt:
|
|
31
|
+
receipt: SessionReceipt | null
|
|
32
32
|
challenge: Challenge.Challenge | null
|
|
33
33
|
channelId: Hex.Hex | null
|
|
34
34
|
cumulative: bigint
|
|
@@ -83,7 +83,7 @@ export function sessionManager(parameters: sessionManager.Parameters): SessionMa
|
|
|
83
83
|
|
|
84
84
|
function toPaymentResponse(response: Response): PaymentResponse {
|
|
85
85
|
const receiptHeader = response.headers.get('Payment-Receipt')
|
|
86
|
-
const receipt = receiptHeader ?
|
|
86
|
+
const receipt = receiptHeader ? deserializeSessionReceipt(receiptHeader) : null
|
|
87
87
|
return Object.assign(response, {
|
|
88
88
|
receipt,
|
|
89
89
|
challenge: lastChallenge,
|
|
@@ -241,14 +241,14 @@ export function sessionManager(parameters: sessionManager.Parameters): SessionMa
|
|
|
241
241
|
},
|
|
242
242
|
})
|
|
243
243
|
|
|
244
|
-
let receipt:
|
|
244
|
+
let receipt: SessionReceipt | undefined
|
|
245
245
|
if (lastUrl) {
|
|
246
246
|
const response = await fetchFn(lastUrl, {
|
|
247
247
|
method: 'POST',
|
|
248
248
|
headers: { Authorization: credential },
|
|
249
249
|
})
|
|
250
250
|
const receiptHeader = response.headers.get('Payment-Receipt')
|
|
251
|
-
if (receiptHeader) receipt =
|
|
251
|
+
if (receiptHeader) receipt = deserializeSessionReceipt(receiptHeader)
|
|
252
252
|
}
|
|
253
253
|
|
|
254
254
|
return receipt
|
package/src/tempo/index.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export * as Methods from './Methods.js'
|
|
2
|
-
export * as
|
|
2
|
+
export * as Session from './session/index.js'
|
|
@@ -67,7 +67,7 @@ export function charge<const parameters extends charge.Parameters>(
|
|
|
67
67
|
recipient,
|
|
68
68
|
} as unknown as Defaults,
|
|
69
69
|
|
|
70
|
-
// TODO: dedupe `{charge,
|
|
70
|
+
// TODO: dedupe `{charge,session}.request`
|
|
71
71
|
async request({ credential, request }) {
|
|
72
72
|
const chainId = await (async () => {
|
|
73
73
|
if (request.chainId) return request.chainId
|