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.
Files changed (148) hide show
  1. package/README.md +0 -52
  2. package/dist/Challenge.d.ts +8 -0
  3. package/dist/Challenge.d.ts.map +1 -1
  4. package/dist/Challenge.js +20 -4
  5. package/dist/Challenge.js.map +1 -1
  6. package/dist/Errors.d.ts +7 -7
  7. package/dist/Errors.d.ts.map +1 -1
  8. package/dist/Errors.js +7 -7
  9. package/dist/Errors.js.map +1 -1
  10. package/dist/cli.js +280 -119
  11. package/dist/cli.js.map +1 -1
  12. package/dist/internal/env.js +2 -2
  13. package/dist/internal/env.js.map +1 -1
  14. package/dist/server/Mppx.d.ts +2 -0
  15. package/dist/server/Mppx.d.ts.map +1 -1
  16. package/dist/server/Mppx.js +4 -3
  17. package/dist/server/Mppx.js.map +1 -1
  18. package/dist/tempo/client/ChannelOps.d.ts +5 -5
  19. package/dist/tempo/client/ChannelOps.d.ts.map +1 -1
  20. package/dist/tempo/client/ChannelOps.js +3 -3
  21. package/dist/tempo/client/ChannelOps.js.map +1 -1
  22. package/dist/tempo/client/Session.d.ts +2 -2
  23. package/dist/tempo/client/Session.d.ts.map +1 -1
  24. package/dist/tempo/client/Session.js +3 -3
  25. package/dist/tempo/client/Session.js.map +1 -1
  26. package/dist/tempo/client/SessionManager.d.ts +4 -4
  27. package/dist/tempo/client/SessionManager.d.ts.map +1 -1
  28. package/dist/tempo/client/SessionManager.js +4 -4
  29. package/dist/tempo/client/SessionManager.js.map +1 -1
  30. package/dist/tempo/index.d.ts +1 -1
  31. package/dist/tempo/index.d.ts.map +1 -1
  32. package/dist/tempo/index.js +1 -1
  33. package/dist/tempo/index.js.map +1 -1
  34. package/dist/tempo/server/Charge.js +1 -1
  35. package/dist/tempo/server/Charge.js.map +1 -1
  36. package/dist/tempo/server/Methods.d.ts +1 -1
  37. package/dist/tempo/server/Methods.d.ts.map +1 -1
  38. package/dist/tempo/server/Session.d.ts +8 -8
  39. package/dist/tempo/server/Session.d.ts.map +1 -1
  40. package/dist/tempo/server/Session.js +24 -24
  41. package/dist/tempo/server/Session.js.map +1 -1
  42. package/dist/tempo/server/index.d.ts +2 -2
  43. package/dist/tempo/server/index.d.ts.map +1 -1
  44. package/dist/tempo/server/index.js +2 -2
  45. package/dist/tempo/server/index.js.map +1 -1
  46. package/dist/tempo/server/internal/transport.d.ts +4 -4
  47. package/dist/tempo/server/internal/transport.d.ts.map +1 -1
  48. package/dist/tempo/server/internal/transport.js +3 -3
  49. package/dist/tempo/server/internal/transport.js.map +1 -1
  50. package/dist/tempo/session/Chain.d.ts.map +1 -0
  51. package/dist/tempo/session/Chain.js.map +1 -0
  52. package/dist/tempo/session/Channel.d.ts.map +1 -0
  53. package/dist/tempo/session/Channel.js.map +1 -0
  54. package/dist/tempo/session/ChannelStore.d.ts.map +1 -0
  55. package/dist/tempo/session/ChannelStore.js.map +1 -0
  56. package/dist/tempo/session/Receipt.d.ts +22 -0
  57. package/dist/tempo/session/Receipt.d.ts.map +1 -0
  58. package/dist/tempo/{stream → session}/Receipt.js +6 -6
  59. package/dist/tempo/session/Receipt.js.map +1 -0
  60. package/dist/tempo/{stream → session}/Sse.d.ts +7 -7
  61. package/dist/tempo/session/Sse.d.ts.map +1 -0
  62. package/dist/tempo/{stream → session}/Sse.js +4 -4
  63. package/dist/tempo/session/Sse.js.map +1 -0
  64. package/dist/tempo/{stream → session}/Types.d.ts +4 -4
  65. package/dist/tempo/session/Types.d.ts.map +1 -0
  66. package/dist/tempo/{stream → session}/Types.js.map +1 -1
  67. package/dist/tempo/session/Voucher.d.ts.map +1 -0
  68. package/dist/tempo/session/Voucher.js.map +1 -0
  69. package/dist/tempo/{stream → session}/escrow.abi.d.ts.map +1 -1
  70. package/dist/tempo/session/escrow.abi.js.map +1 -0
  71. package/dist/tempo/session/index.d.ts.map +1 -0
  72. package/dist/tempo/session/index.js.map +1 -0
  73. package/package.json +1 -1
  74. package/src/Challenge.test.ts +201 -11
  75. package/src/Challenge.ts +34 -4
  76. package/src/Errors.test.ts +10 -10
  77. package/src/Errors.ts +7 -7
  78. package/src/Store.test.ts +93 -0
  79. package/src/cli.test.ts +234 -38
  80. package/src/cli.ts +340 -135
  81. package/src/client/Transport.test.ts +4 -4
  82. package/src/internal/env.test.ts +42 -0
  83. package/src/internal/env.ts +2 -2
  84. package/src/middlewares/express.test.ts +1 -1
  85. package/src/middlewares/hono.test.ts +1 -1
  86. package/src/middlewares/nextjs.test.ts +1 -1
  87. package/src/server/Mppx.test.ts +173 -0
  88. package/src/server/Mppx.ts +6 -3
  89. package/src/server/Transport.test.ts +6 -6
  90. package/src/tempo/client/ChannelOps.test.ts +2 -2
  91. package/src/tempo/client/ChannelOps.ts +8 -8
  92. package/src/tempo/client/Session.test.ts +3 -3
  93. package/src/tempo/client/Session.ts +9 -9
  94. package/src/tempo/client/SessionManager.test.ts +3 -3
  95. package/src/tempo/client/SessionManager.ts +9 -9
  96. package/src/tempo/index.ts +1 -1
  97. package/src/tempo/server/Charge.ts +1 -1
  98. package/src/tempo/server/Session.test.ts +61 -9
  99. package/src/tempo/server/Session.ts +47 -47
  100. package/src/tempo/server/Sse.test.ts +3 -3
  101. package/src/tempo/server/index.ts +2 -2
  102. package/src/tempo/server/internal/transport.test.ts +285 -0
  103. package/src/tempo/server/internal/transport.ts +6 -6
  104. package/src/tempo/{stream → session}/Chain.test.ts +1 -1
  105. package/src/tempo/{stream → session}/Receipt.test.ts +16 -12
  106. package/src/tempo/{stream → session}/Receipt.ts +9 -9
  107. package/src/tempo/{stream → session}/Sse.test.ts +5 -5
  108. package/src/tempo/{stream → session}/Sse.ts +11 -11
  109. package/src/tempo/{stream → session}/Types.ts +4 -4
  110. package/dist/tempo/stream/Chain.d.ts.map +0 -1
  111. package/dist/tempo/stream/Chain.js.map +0 -1
  112. package/dist/tempo/stream/Channel.d.ts.map +0 -1
  113. package/dist/tempo/stream/Channel.js.map +0 -1
  114. package/dist/tempo/stream/ChannelStore.d.ts.map +0 -1
  115. package/dist/tempo/stream/ChannelStore.js.map +0 -1
  116. package/dist/tempo/stream/Receipt.d.ts +0 -22
  117. package/dist/tempo/stream/Receipt.d.ts.map +0 -1
  118. package/dist/tempo/stream/Receipt.js.map +0 -1
  119. package/dist/tempo/stream/Sse.d.ts.map +0 -1
  120. package/dist/tempo/stream/Sse.js.map +0 -1
  121. package/dist/tempo/stream/Types.d.ts.map +0 -1
  122. package/dist/tempo/stream/Voucher.d.ts.map +0 -1
  123. package/dist/tempo/stream/Voucher.js.map +0 -1
  124. package/dist/tempo/stream/escrow.abi.js.map +0 -1
  125. package/dist/tempo/stream/index.d.ts.map +0 -1
  126. package/dist/tempo/stream/index.js.map +0 -1
  127. /package/dist/tempo/{stream → session}/Chain.d.ts +0 -0
  128. /package/dist/tempo/{stream → session}/Chain.js +0 -0
  129. /package/dist/tempo/{stream → session}/Channel.d.ts +0 -0
  130. /package/dist/tempo/{stream → session}/Channel.js +0 -0
  131. /package/dist/tempo/{stream → session}/ChannelStore.d.ts +0 -0
  132. /package/dist/tempo/{stream → session}/ChannelStore.js +0 -0
  133. /package/dist/tempo/{stream → session}/Types.js +0 -0
  134. /package/dist/tempo/{stream → session}/Voucher.d.ts +0 -0
  135. /package/dist/tempo/{stream → session}/Voucher.js +0 -0
  136. /package/dist/tempo/{stream → session}/escrow.abi.d.ts +0 -0
  137. /package/dist/tempo/{stream → session}/escrow.abi.js +0 -0
  138. /package/dist/tempo/{stream → session}/index.d.ts +0 -0
  139. /package/dist/tempo/{stream → session}/index.js +0 -0
  140. /package/src/tempo/{stream → session}/Chain.ts +0 -0
  141. /package/src/tempo/{stream → session}/Channel.test.ts +0 -0
  142. /package/src/tempo/{stream → session}/Channel.ts +0 -0
  143. /package/src/tempo/{stream → session}/ChannelStore.test.ts +0 -0
  144. /package/src/tempo/{stream → session}/ChannelStore.ts +0 -0
  145. /package/src/tempo/{stream → session}/Voucher.test.ts +0 -0
  146. /package/src/tempo/{stream → session}/Voucher.ts +0 -0
  147. /package/src/tempo/{stream → session}/escrow.abi.ts +0 -0
  148. /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": "i1474pQ7BtfAx76cLch6u8_AQkcp3akMkerEYrL5Rwo",
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 eyJjaGFsbGVuZ2UiOnsiZXhwaXJlcyI6IjIwMjUtMDEtMDFUMDA6MDA6MDAuMDAwWiIsImlkIjoiaTE0NzRwUTdCdGZBeDc2Y0xjaDZ1OF9BUWtjcDNha01rZXJFWXJMNVJ3byIsImludGVudCI6ImNoYXJnZSIsIm1ldGhvZCI6InRlbXBvIiwicmVhbG0iOiJhcGkuZXhhbXBsZS5jb20iLCJyZXF1ZXN0IjoiZXlKaGJXOTFiblFpT2lJeE1EQXdJaXdpWTNWeWNtVnVZM2tpT2lJd2VESXdZekF3TURBd01EQXdNREF3TURBd01EQXdNREF3TURBd01EQXdNREF3TURBd01EQXdNREVpTENKbGVIQnBjbVZ6SWpvaU1qQXlOUzB3TVMwd01WUXdNRG93TURvd01DNHdNREJhSWl3aWNtVmphWEJwWlc1MElqb2lNSGczTkRKa016VkRZelkyTXpSRE1EVXpNamt5TldFellqZzBORUpqT1dVM05UazFaamhtUlRBd0luMCJ9LCJwYXlsb2FkIjp7InNpZ25hdHVyZSI6IjB4YWJjMTIzIiwidHlwZSI6InRyYW5zYWN0aW9uIn19"`,
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": "i1474pQ7BtfAx76cLch6u8_AQkcp3akMkerEYrL5Rwo",
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": "i1474pQ7BtfAx76cLch6u8_AQkcp3akMkerEYrL5Rwo",
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
+ })
@@ -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/stream.js'
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/stream.js'
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/stream.js'
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) {
@@ -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
+ })
@@ -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 meta = {
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: meta },
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": "N_Q_IM9V5tO3JMcOTniz7anX81m7MdEp4aLW9q5KNK0",
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="N_Q_IM9V5tO3JMcOTniz7anX81m7MdEp4aLW9q5KNK0", realm="api.example.com", method="tempo", intent="charge", request="eyJhbW91bnQiOiIxMDAwMDAwMDAwIiwiY3VycmVuY3kiOiIweDIwYzAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDEiLCJleHBpcmVzIjoiMjAyNS0wMS0wMVQwMDowMDowMC4wMDBaIiwicmVjaXBpZW50IjoiMHg3NDJkMzVDYzY2MzRDMDUzMjkyNWEzYjg0NEJjOWU3NTk1ZjhmRTAwIn0", expires="2025-01-01T00:00:00.000Z"",
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/stream/channel-finalized')
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": "N_Q_IM9V5tO3JMcOTniz7anX81m7MdEp4aLW9q5KNK0",
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": "N_Q_IM9V5tO3JMcOTniz7anX81m7MdEp4aLW9q5KNK0",
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": "N_Q_IM9V5tO3JMcOTniz7anX81m7MdEp4aLW9q5KNK0",
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/stream.js'
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 '../stream/Voucher.js'
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 '../stream/Chain.js'
22
- import * as Channel from '../stream/Channel.js'
23
- import type { StreamCredentialPayload } from '../stream/Types.js'
24
- import { signVoucher } from '../stream/Voucher.js'
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: StreamCredentialPayload,
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<StreamCredentialPayload> {
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<StreamCredentialPayload> {
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: StreamCredentialPayload }> {
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/stream.js'
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 { StreamCredentialPayload } from '../stream/Types.js'
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<StreamCredentialPayload>(result)
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 { StreamCredentialPayload } from '../stream/Types.js'
12
- import { signVoucher } from '../stream/Voucher.js'
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 streamContextSchema = z.object({
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 StreamContext = z.infer<typeof streamContextSchema>
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?: StreamContext,
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: StreamCredentialPayload
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: StreamContext,
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: StreamCredentialPayload
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: streamContextSchema,
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 '../stream/Sse.js'
5
- import type { NeedVoucherEvent, StreamReceipt } from '../stream/Types.js'
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 StreamReceipt)}\n\n`,
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 { deserializeStreamReceipt } from '../stream/Receipt.js'
8
- import { parseEvent } from '../stream/Sse.js'
9
- import type { StreamReceipt } from '../stream/Types.js'
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: StreamReceipt) => void) | undefined
23
+ onReceipt?: ((receipt: SessionReceipt) => void) | undefined
24
24
  signal?: AbortSignal | undefined
25
25
  },
26
26
  ): Promise<AsyncIterable<string>>
27
- close(): Promise<StreamReceipt | undefined>
27
+ close(): Promise<SessionReceipt | undefined>
28
28
  }
29
29
 
30
30
  export type PaymentResponse = Response & {
31
- receipt: StreamReceipt | null
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 ? deserializeStreamReceipt(receiptHeader) : null
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: StreamReceipt | undefined
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 = deserializeStreamReceipt(receiptHeader)
251
+ if (receiptHeader) receipt = deserializeSessionReceipt(receiptHeader)
252
252
  }
253
253
 
254
254
  return receipt
@@ -1,2 +1,2 @@
1
1
  export * as Methods from './Methods.js'
2
- export * as Stream from './stream/index.js'
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,stream}.request`
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