mppx 0.4.12 → 0.5.0

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 (52) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/Expires.d.ts +7 -0
  3. package/dist/Expires.d.ts.map +1 -1
  4. package/dist/Expires.js +21 -0
  5. package/dist/Expires.js.map +1 -1
  6. package/dist/server/Mppx.js +6 -5
  7. package/dist/server/Mppx.js.map +1 -1
  8. package/dist/stripe/server/Charge.d.ts.map +1 -1
  9. package/dist/stripe/server/Charge.js +3 -3
  10. package/dist/stripe/server/Charge.js.map +1 -1
  11. package/dist/tempo/Methods.d.ts +3 -0
  12. package/dist/tempo/Methods.d.ts.map +1 -1
  13. package/dist/tempo/Methods.js +1 -0
  14. package/dist/tempo/Methods.js.map +1 -1
  15. package/dist/tempo/client/Charge.d.ts +3 -0
  16. package/dist/tempo/client/Charge.d.ts.map +1 -1
  17. package/dist/tempo/client/Charge.js +18 -2
  18. package/dist/tempo/client/Charge.js.map +1 -1
  19. package/dist/tempo/client/Methods.d.ts +3 -0
  20. package/dist/tempo/client/Methods.d.ts.map +1 -1
  21. package/dist/tempo/internal/proof.d.ts +23 -0
  22. package/dist/tempo/internal/proof.d.ts.map +1 -0
  23. package/dist/tempo/internal/proof.js +17 -0
  24. package/dist/tempo/internal/proof.js.map +1 -0
  25. package/dist/tempo/server/Charge.d.ts +3 -0
  26. package/dist/tempo/server/Charge.d.ts.map +1 -1
  27. package/dist/tempo/server/Charge.js +32 -4
  28. package/dist/tempo/server/Charge.js.map +1 -1
  29. package/dist/tempo/server/Methods.d.ts +3 -0
  30. package/dist/tempo/server/Methods.d.ts.map +1 -1
  31. package/package.json +1 -1
  32. package/src/Expires.ts +25 -0
  33. package/src/cli/cli.test.ts +230 -1
  34. package/src/middlewares/elysia.test.ts +127 -4
  35. package/src/middlewares/express.test.ts +120 -54
  36. package/src/middlewares/hono.test.ts +73 -34
  37. package/src/middlewares/nextjs.test.ts +159 -36
  38. package/src/server/Mppx.test.ts +86 -0
  39. package/src/server/Mppx.ts +5 -5
  40. package/src/stripe/server/Charge.ts +3 -7
  41. package/src/tempo/Methods.test.ts +26 -0
  42. package/src/tempo/Methods.ts +1 -0
  43. package/src/tempo/client/Charge.ts +26 -3
  44. package/src/tempo/internal/charge.test.ts +66 -0
  45. package/src/tempo/internal/proof.test.ts +36 -0
  46. package/src/tempo/internal/proof.ts +19 -0
  47. package/src/tempo/server/Charge.test.ts +362 -1
  48. package/src/tempo/server/Charge.ts +40 -2
  49. package/src/tempo/server/Session.test.ts +1123 -53
  50. package/src/tempo/server/internal/transport.test.ts +32 -0
  51. package/src/tempo/session/Chain.test.ts +35 -0
  52. package/src/tempo/session/Sse.test.ts +31 -0
@@ -23,13 +23,14 @@ function createServer(app: express.Express) {
23
23
 
24
24
  const secretKey = 'test-secret-key'
25
25
 
26
- describe('charge', () => {
26
+ function createChargeHarness(feePayer: boolean) {
27
27
  const mppx = Mppx.create({
28
28
  methods: [
29
29
  tempo_server({
30
30
  getClient: () => client,
31
31
  currency: asset,
32
32
  account: accounts[0],
33
+ ...(feePayer ? { feePayer: true } : {}),
33
34
  }),
34
35
  ],
35
36
  secretKey,
@@ -45,7 +46,39 @@ describe('charge', () => {
45
46
  ],
46
47
  })
47
48
 
49
+ return { fetch, mppx }
50
+ }
51
+
52
+ function createCoreChargeHarness(feePayer: boolean) {
53
+ const mppx = Mppx_server.create({
54
+ methods: [
55
+ tempo_server({
56
+ getClient: () => client,
57
+ currency: asset,
58
+ account: accounts[0],
59
+ ...(feePayer ? { feePayer: true } : {}),
60
+ }),
61
+ ],
62
+ secretKey,
63
+ })
64
+
65
+ const { fetch } = Mppx_client.create({
66
+ polyfill: false,
67
+ methods: [
68
+ tempo_client({
69
+ account: accounts[1],
70
+ getClient: () => client,
71
+ }),
72
+ ],
73
+ })
74
+
75
+ return { fetch, mppx }
76
+ }
77
+
78
+ describe('charge', () => {
48
79
  test('returns 402 when no credential', async () => {
80
+ const { mppx } = createChargeHarness(false)
81
+
49
82
  const app = express()
50
83
  app.get('/', mppx.charge({ amount: '1' }), (_req, res) => {
51
84
  res.json({ fortune: 'You will be rich' })
@@ -60,6 +93,8 @@ describe('charge', () => {
60
93
  })
61
94
 
62
95
  test('returns 200 with receipt on valid payment', async () => {
96
+ const { fetch, mppx } = createChargeHarness(false)
97
+
63
98
  const app = express()
64
99
  app.get('/', mppx.charge({ amount: '1' }), (_req, res) => {
65
100
  res.json({ fortune: 'You will be rich' })
@@ -82,7 +117,25 @@ describe('charge', () => {
82
117
  server.close()
83
118
  })
84
119
 
120
+ test('fee payer: returns 200 with receipt on valid payment', async () => {
121
+ const { fetch, mppx } = createChargeHarness(true)
122
+
123
+ const app = express()
124
+ app.get('/', mppx.charge({ amount: '1' }), (_req, res) => {
125
+ res.json({ fortune: 'You will be rich' })
126
+ })
127
+
128
+ const server = await createServer(app)
129
+ const response = await fetch(server.url)
130
+ expect(response.status).toBe(200)
131
+ expect(Receipt.fromResponse(response).status).toBe('success')
132
+
133
+ server.close()
134
+ })
135
+
85
136
  test('serves /openapi.json from a handler-derived route config', async () => {
137
+ const { mppx } = createChargeHarness(false)
138
+
86
139
  const app = express()
87
140
  const pay = mppx.charge({ amount: '1' })
88
141
  app.get('/', pay, (_req, res) => {
@@ -114,13 +167,7 @@ describe('charge', () => {
114
167
  describe('session', () => {
115
168
  let escrowContract: Address
116
169
 
117
- beforeAll(async () => {
118
- escrowContract = await deployEscrow()
119
- await fundAccount({ address: accounts[2].address, token: Addresses.pathUsd })
120
- await fundAccount({ address: accounts[2].address, token: asset })
121
- })
122
-
123
- test('returns 402 when no credential', async () => {
170
+ function createSessionHarness(feePayer: boolean) {
124
171
  const mppx = Mppx.create({
125
172
  methods: [
126
173
  tempo_server.session({
@@ -128,13 +175,39 @@ describe('session', () => {
128
175
  account: accounts[0],
129
176
  currency: asset,
130
177
  escrowContract,
131
- }),
178
+ ...(feePayer ? { feePayer: accounts[1] } : {}),
179
+ } as any),
132
180
  ],
133
181
  secretKey,
134
182
  })
135
183
 
184
+ const { fetch } = Mppx_client.create({
185
+ polyfill: false,
186
+ methods: [
187
+ sessionIntent({
188
+ account: accounts[2],
189
+ deposit: '10',
190
+ getClient: () => client,
191
+ }),
192
+ ],
193
+ })
194
+
195
+ return { fetch, mppx }
196
+ }
197
+
198
+ beforeAll(async () => {
199
+ escrowContract = await deployEscrow()
200
+ await fundAccount({ address: accounts[1].address, token: Addresses.pathUsd })
201
+ await fundAccount({ address: accounts[1].address, token: asset })
202
+ await fundAccount({ address: accounts[2].address, token: Addresses.pathUsd })
203
+ await fundAccount({ address: accounts[2].address, token: asset })
204
+ })
205
+
206
+ test('returns 402 when no credential', async () => {
207
+ const { mppx } = createSessionHarness(false)
208
+
136
209
  const app = express()
137
- app.get('/', mppx.session({ amount: '1', unitType: 'token' }), (_req, res) => {
210
+ app.get('/', mppx.session({ amount: '1', currency: asset, unitType: 'token' }), (_req, res) => {
138
211
  res.json({ data: 'streamed' })
139
212
  })
140
213
 
@@ -147,32 +220,10 @@ describe('session', () => {
147
220
  })
148
221
 
149
222
  test('returns 200 with receipt on valid payment', async () => {
150
- const mppx = Mppx.create({
151
- methods: [
152
- tempo_server.session({
153
- getClient: () => client,
154
- account: accounts[0],
155
- currency: asset,
156
- escrowContract,
157
- feePayer: true,
158
- }),
159
- ],
160
- secretKey,
161
- })
162
-
163
- const { fetch } = Mppx_client.create({
164
- polyfill: false,
165
- methods: [
166
- sessionIntent({
167
- account: accounts[2],
168
- deposit: '10',
169
- getClient: () => client,
170
- }),
171
- ],
172
- })
223
+ const { fetch, mppx } = createSessionHarness(false)
173
224
 
174
225
  const app = express()
175
- app.get('/', mppx.session({ amount: '1', unitType: 'token' }), (_req, res) => {
226
+ app.get('/', mppx.session({ amount: '1', currency: asset, unitType: 'token' }), (_req, res) => {
176
227
  res.json({ data: 'streamed' })
177
228
  })
178
229
 
@@ -185,31 +236,28 @@ describe('session', () => {
185
236
 
186
237
  server.close()
187
238
  })
188
- })
189
239
 
190
- describe('payment', () => {
191
- const mppx = Mppx_server.create({
192
- methods: [
193
- tempo_server({
194
- getClient: () => client,
195
- currency: asset,
196
- account: accounts[0],
197
- }),
198
- ],
199
- secretKey,
200
- })
240
+ test('fee payer: returns 200 with receipt on valid payment', async () => {
241
+ const { fetch, mppx } = createSessionHarness(true)
201
242
 
202
- const { fetch } = Mppx_client.create({
203
- polyfill: false,
204
- methods: [
205
- tempo_client({
206
- account: accounts[1],
207
- getClient: () => client,
208
- }),
209
- ],
243
+ const app = express()
244
+ app.get('/', mppx.session({ amount: '1', currency: asset, unitType: 'token' }), (_req, res) => {
245
+ res.json({ data: 'streamed' })
246
+ })
247
+
248
+ const server = await createServer(app)
249
+ const response = await fetch(server.url)
250
+ expect(response.status).toBe(200)
251
+ expect(Receipt.fromResponse(response).status).toBe('success')
252
+
253
+ server.close()
210
254
  })
255
+ })
211
256
 
257
+ describe('payment', () => {
212
258
  test('returns 402 when no credential', async () => {
259
+ const { mppx } = createCoreChargeHarness(false)
260
+
213
261
  const app = express()
214
262
  app.get('/', payment(mppx.charge, { amount: '1' }), (_req, res) => {
215
263
  res.json({ fortune: 'You will be rich' })
@@ -224,6 +272,8 @@ describe('payment', () => {
224
272
  })
225
273
 
226
274
  test('returns 200 with receipt on valid payment', async () => {
275
+ const { fetch, mppx } = createCoreChargeHarness(false)
276
+
227
277
  const app = express()
228
278
  app.get('/', payment(mppx.charge, { amount: '1' }), (_req, res) => {
229
279
  res.json({ fortune: 'You will be rich' })
@@ -245,4 +295,20 @@ describe('payment', () => {
245
295
 
246
296
  server.close()
247
297
  })
298
+
299
+ test('fee payer: returns 200 with receipt on valid payment', async () => {
300
+ const { fetch, mppx } = createCoreChargeHarness(true)
301
+
302
+ const app = express()
303
+ app.get('/', payment(mppx.charge, { amount: '1' }), (_req, res) => {
304
+ res.json({ fortune: 'You will be rich' })
305
+ })
306
+
307
+ const server = await createServer(app)
308
+ const response = await fetch(server.url)
309
+ expect(response.status).toBe(200)
310
+ expect(Receipt.fromResponse(response).status).toBe('success')
311
+
312
+ server.close()
313
+ })
248
314
  })
@@ -23,13 +23,14 @@ function createServer(app: Hono) {
23
23
 
24
24
  const secretKey = 'test-secret-key'
25
25
 
26
- describe('charge', () => {
26
+ function createChargeHarness(feePayer: boolean) {
27
27
  const mppx = Mppx.create({
28
28
  methods: [
29
29
  tempo_server.charge({
30
30
  getClient: () => client,
31
31
  currency: asset,
32
32
  account: accounts[0],
33
+ ...(feePayer ? { feePayer: true } : {}),
33
34
  }),
34
35
  ],
35
36
  secretKey,
@@ -45,7 +46,13 @@ describe('charge', () => {
45
46
  ],
46
47
  })
47
48
 
49
+ return { fetch, mppx }
50
+ }
51
+
52
+ describe('charge', () => {
48
53
  test('returns 402 when no credential', async () => {
54
+ const { mppx } = createChargeHarness(false)
55
+
49
56
  const app = new Hono()
50
57
  app.get('/', mppx.charge({ amount: '1' }), (c) => c.json({ fortune: 'You will be rich' }))
51
58
 
@@ -58,6 +65,8 @@ describe('charge', () => {
58
65
  })
59
66
 
60
67
  test('returns 200 with receipt on valid payment', async () => {
68
+ const { fetch, mppx } = createChargeHarness(false)
69
+
61
70
  const app = new Hono()
62
71
  app.get('/', mppx.charge({ amount: '1' }), (c) => c.json({ fortune: 'You will be rich' }))
63
72
 
@@ -75,7 +84,23 @@ describe('charge', () => {
75
84
  server.close()
76
85
  })
77
86
 
87
+ test('fee payer: returns 200 with receipt on valid payment', async () => {
88
+ const { fetch, mppx } = createChargeHarness(true)
89
+
90
+ const app = new Hono()
91
+ app.get('/', mppx.charge({ amount: '1' }), (c) => c.json({ fortune: 'You will be rich' }))
92
+
93
+ const server = await createServer(app)
94
+ const response = await fetch(server.url)
95
+ expect(response.status).toBe(200)
96
+ expect(Receipt.fromResponse(response).status).toBe('success')
97
+
98
+ server.close()
99
+ })
100
+
78
101
  test('serves /openapi.json via auto discovery', async () => {
102
+ const { mppx } = createChargeHarness(false)
103
+
79
104
  const app = new Hono()
80
105
  app.get('/', mppx.charge({ amount: '1' }), (c) => c.json({ fortune: 'You will be rich' }))
81
106
  discovery(app, mppx, { auto: true, info: { title: 'Auto API', version: '2.0.0' } })
@@ -101,13 +126,7 @@ describe('charge', () => {
101
126
  describe('session', () => {
102
127
  let escrowContract: Address
103
128
 
104
- beforeAll(async () => {
105
- escrowContract = await deployEscrow()
106
- await fundAccount({ address: accounts[2].address, token: Addresses.pathUsd })
107
- await fundAccount({ address: accounts[2].address, token: asset })
108
- })
109
-
110
- test('returns 402 when no credential', async () => {
129
+ function createSessionHarness(feePayer: boolean) {
111
130
  const mppx = Mppx.create({
112
131
  methods: [
113
132
  tempo_server.session({
@@ -115,13 +134,39 @@ describe('session', () => {
115
134
  account: accounts[0],
116
135
  currency: asset,
117
136
  escrowContract,
118
- }),
137
+ ...(feePayer ? { feePayer: accounts[1] } : {}),
138
+ } as any),
119
139
  ],
120
140
  secretKey,
121
141
  })
122
142
 
143
+ const { fetch } = Mppx_client.create({
144
+ polyfill: false,
145
+ methods: [
146
+ sessionIntent({
147
+ account: accounts[2],
148
+ deposit: '10',
149
+ getClient: () => client,
150
+ }),
151
+ ],
152
+ })
153
+
154
+ return { fetch, mppx }
155
+ }
156
+
157
+ beforeAll(async () => {
158
+ escrowContract = await deployEscrow()
159
+ await fundAccount({ address: accounts[1].address, token: Addresses.pathUsd })
160
+ await fundAccount({ address: accounts[1].address, token: asset })
161
+ await fundAccount({ address: accounts[2].address, token: Addresses.pathUsd })
162
+ await fundAccount({ address: accounts[2].address, token: asset })
163
+ })
164
+
165
+ test('returns 402 when no credential', async () => {
166
+ const { mppx } = createSessionHarness(false)
167
+
123
168
  const app = new Hono()
124
- app.get('/', mppx.session({ amount: '1', unitType: 'token' }), (c) =>
169
+ app.get('/', mppx.session({ amount: '1', currency: asset, unitType: 'token' }), (c) =>
125
170
  c.json({ data: 'streamed' }),
126
171
  )
127
172
 
@@ -134,32 +179,10 @@ describe('session', () => {
134
179
  })
135
180
 
136
181
  test('returns 200 with receipt on valid payment', async () => {
137
- const mppx = Mppx.create({
138
- methods: [
139
- tempo_server.session({
140
- getClient: () => client,
141
- account: accounts[0],
142
- currency: asset,
143
- escrowContract,
144
- feePayer: true,
145
- }),
146
- ],
147
- secretKey,
148
- })
149
-
150
- const { fetch } = Mppx_client.create({
151
- polyfill: false,
152
- methods: [
153
- sessionIntent({
154
- account: accounts[2],
155
- deposit: '10',
156
- getClient: () => client,
157
- }),
158
- ],
159
- })
182
+ const { fetch, mppx } = createSessionHarness(false)
160
183
 
161
184
  const app = new Hono()
162
- app.get('/', mppx.session({ amount: '1', unitType: 'token' }), (c) =>
185
+ app.get('/', mppx.session({ amount: '1', currency: asset, unitType: 'token' }), (c) =>
163
186
  c.json({ data: 'streamed' }),
164
187
  )
165
188
 
@@ -172,4 +195,20 @@ describe('session', () => {
172
195
 
173
196
  server.close()
174
197
  })
198
+
199
+ test('fee payer: returns 200 with receipt on valid payment', async () => {
200
+ const { fetch, mppx } = createSessionHarness(true)
201
+
202
+ const app = new Hono()
203
+ app.get('/', mppx.session({ amount: '1', currency: asset, unitType: 'token' }), (c) =>
204
+ c.json({ data: 'streamed' }),
205
+ )
206
+
207
+ const server = await createServer(app)
208
+ const response = await fetch(server.url)
209
+ expect(response.status).toBe(200)
210
+ expect(Receipt.fromResponse(response).status).toBe('success')
211
+
212
+ server.close()
213
+ })
175
214
  })
@@ -1,6 +1,6 @@
1
1
  import * as http from 'node:http'
2
2
 
3
- import { Receipt } from 'mppx'
3
+ import { Challenge, Credential, Receipt } from 'mppx'
4
4
  import { Mppx as Mppx_client, session as sessionIntent, tempo as tempo_client } from 'mppx/client'
5
5
  import { Mppx, discovery } from 'mppx/nextjs'
6
6
  import { tempo as tempo_server } from 'mppx/server'
@@ -8,7 +8,7 @@ import type { Address } from 'viem'
8
8
  import { Addresses } from 'viem/tempo'
9
9
  import { beforeAll, describe, expect, test } from 'vp/test'
10
10
  import { deployEscrow } from '~test/tempo/session.js'
11
- import { accounts, asset, client, fundAccount } from '~test/tempo/viem.js'
11
+ import { accounts, asset, chain, client, fundAccount } from '~test/tempo/viem.js'
12
12
 
13
13
  function createServer(handler: (request: Request) => Promise<Response> | Response) {
14
14
  return new Promise<{ url: string; close: () => void }>((resolve) => {
@@ -36,13 +36,14 @@ function createServer(handler: (request: Request) => Promise<Response> | Respons
36
36
 
37
37
  const secretKey = 'test-secret-key'
38
38
 
39
- describe('charge', () => {
39
+ function createChargeHarness(feePayer: boolean) {
40
40
  const mppx = Mppx.create({
41
41
  methods: [
42
42
  tempo_server.charge({
43
43
  getClient: () => client,
44
44
  currency: asset,
45
45
  account: accounts[0],
46
+ ...(feePayer ? { feePayer: true } : {}),
46
47
  }),
47
48
  ],
48
49
  secretKey,
@@ -58,7 +59,13 @@ describe('charge', () => {
58
59
  ],
59
60
  })
60
61
 
62
+ return { fetch, mppx }
63
+ }
64
+
65
+ describe('charge', () => {
61
66
  test('returns 402 when no credential', async () => {
67
+ const { mppx } = createChargeHarness(false)
68
+
62
69
  const handler = mppx.charge({ amount: '1' })(() =>
63
70
  Response.json({ fortune: 'You will be rich' }),
64
71
  )
@@ -72,6 +79,8 @@ describe('charge', () => {
72
79
  })
73
80
 
74
81
  test('returns 200 with receipt on valid payment', async () => {
82
+ const { fetch, mppx } = createChargeHarness(false)
83
+
75
84
  const handler = mppx.charge({ amount: '1' })(() =>
76
85
  Response.json({ fortune: 'You will be rich' }),
77
86
  )
@@ -90,7 +99,108 @@ describe('charge', () => {
90
99
  server.close()
91
100
  })
92
101
 
102
+ test('fee payer: returns 200 with receipt on valid payment', async () => {
103
+ const { fetch, mppx } = createChargeHarness(true)
104
+
105
+ const handler = mppx.charge({ amount: '1' })(() =>
106
+ Response.json({ fortune: 'You will be rich' }),
107
+ )
108
+
109
+ const server = await createServer(handler)
110
+ const response = await fetch(server.url)
111
+ expect(response.status).toBe(200)
112
+ expect(Receipt.fromResponse(response).status).toBe('success')
113
+
114
+ server.close()
115
+ })
116
+
117
+ test('zero-amount charge creates a proof credential and receipt', async () => {
118
+ const { fetch, mppx } = createChargeHarness(false)
119
+
120
+ const handler = mppx.charge({ amount: '0' })((request) =>
121
+ Response.json({ payer: request.headers.get('Authorization') }),
122
+ )
123
+
124
+ const server = await createServer(handler)
125
+
126
+ const challengeResponse = await globalThis.fetch(server.url)
127
+ expect(challengeResponse.status).toBe(402)
128
+
129
+ const response = await fetch(server.url)
130
+ expect(response.status).toBe(200)
131
+
132
+ const body = (await response.json()) as { payer: string }
133
+ const credential = Credential.deserialize<{ signature: string; type: 'proof' }>(body.payer)
134
+ expect(credential.challenge.request.amount).toBe('0')
135
+ expect(credential.payload.type).toBe('proof')
136
+ expect(credential.source).toBe(`did:pkh:eip155:${chain.id}:${accounts[1].address}`)
137
+
138
+ const receipt = Receipt.fromResponse(response)
139
+ expect(receipt.reference).toBe(credential.challenge.id)
140
+
141
+ server.close()
142
+ })
143
+
144
+ test('zero-amount charge with testnet currency omission creates a proof credential', async () => {
145
+ const isTestnet = true
146
+ const mainnetCurrency = '0x20C000000000000000000000b9537d11c60E8b50' as const
147
+
148
+ const mppx = Mppx.create({
149
+ methods: [
150
+ tempo_server.charge({
151
+ account: accounts[0],
152
+ getClient: () => client,
153
+ ...(isTestnet ? {} : { currency: mainnetCurrency }),
154
+ recipient: accounts[0].address,
155
+ testnet: isTestnet,
156
+ }),
157
+ ],
158
+ secretKey,
159
+ })
160
+
161
+ const { fetch } = Mppx_client.create({
162
+ polyfill: false,
163
+ methods: [
164
+ tempo_client.charge({
165
+ account: accounts[1],
166
+ getClient: () => client,
167
+ }),
168
+ ],
169
+ })
170
+
171
+ const handler = mppx.charge({ amount: '0', chainId: chain.id })((request) =>
172
+ Response.json({ payer: request.headers.get('Authorization') }),
173
+ )
174
+
175
+ const server = await createServer(handler)
176
+
177
+ const challengeResponse = await globalThis.fetch(server.url)
178
+ expect(challengeResponse.status).toBe(402)
179
+
180
+ const challenge = Challenge.fromResponse(challengeResponse, {
181
+ methods: [tempo_client.charge()],
182
+ })
183
+ expect(challenge.request.currency).toBe('0x20c0000000000000000000000000000000000000')
184
+
185
+ const response = await fetch(server.url)
186
+ expect(response.status).toBe(200)
187
+
188
+ const body = (await response.json()) as { payer: string }
189
+ const credential = Credential.deserialize<{ signature: string; type: 'proof' }>(body.payer)
190
+ expect(credential.challenge.request.amount).toBe('0')
191
+ expect(credential.challenge.request.currency).toBe('0x20c0000000000000000000000000000000000000')
192
+ expect(credential.payload.type).toBe('proof')
193
+ expect(credential.source).toBe(`did:pkh:eip155:${chain.id}:${accounts[1].address}`)
194
+
195
+ const receipt = Receipt.fromResponse(response)
196
+ expect(receipt.reference).toBe(credential.challenge.id)
197
+
198
+ server.close()
199
+ })
200
+
93
201
  test('serves /openapi.json from a handler-derived route config', async () => {
202
+ const { mppx } = createChargeHarness(false)
203
+
94
204
  const pay = mppx.charge({ amount: '1' })
95
205
  const server = await createServer(
96
206
  discovery(mppx, {
@@ -119,13 +229,7 @@ describe('charge', () => {
119
229
  describe('session', () => {
120
230
  let escrowContract: Address
121
231
 
122
- beforeAll(async () => {
123
- escrowContract = await deployEscrow()
124
- await fundAccount({ address: accounts[2].address, token: Addresses.pathUsd })
125
- await fundAccount({ address: accounts[2].address, token: asset })
126
- })
127
-
128
- test('returns 402 when no credential', async () => {
232
+ function createSessionHarness(feePayer: boolean) {
129
233
  const mppx = Mppx.create({
130
234
  methods: [
131
235
  tempo_server.session({
@@ -133,12 +237,38 @@ describe('session', () => {
133
237
  account: accounts[0],
134
238
  currency: asset,
135
239
  escrowContract,
136
- }),
240
+ ...(feePayer ? { feePayer: accounts[1] } : {}),
241
+ } as any),
137
242
  ],
138
243
  secretKey,
139
244
  })
140
245
 
141
- const handler = mppx.session({ amount: '1', unitType: 'token' })(() =>
246
+ const { fetch } = Mppx_client.create({
247
+ polyfill: false,
248
+ methods: [
249
+ sessionIntent({
250
+ account: accounts[2],
251
+ deposit: '10',
252
+ getClient: () => client,
253
+ }),
254
+ ],
255
+ })
256
+
257
+ return { fetch, mppx }
258
+ }
259
+
260
+ beforeAll(async () => {
261
+ escrowContract = await deployEscrow()
262
+ await fundAccount({ address: accounts[1].address, token: Addresses.pathUsd })
263
+ await fundAccount({ address: accounts[1].address, token: asset })
264
+ await fundAccount({ address: accounts[2].address, token: Addresses.pathUsd })
265
+ await fundAccount({ address: accounts[2].address, token: asset })
266
+ })
267
+
268
+ test('returns 402 when no credential', async () => {
269
+ const { mppx } = createSessionHarness(false)
270
+
271
+ const handler = mppx.session({ amount: '1', currency: asset, unitType: 'token' })(() =>
142
272
  Response.json({ data: 'streamed' }),
143
273
  )
144
274
 
@@ -151,31 +281,9 @@ describe('session', () => {
151
281
  })
152
282
 
153
283
  test('returns 200 with receipt on valid payment', async () => {
154
- const mppx = Mppx.create({
155
- methods: [
156
- tempo_server.session({
157
- getClient: () => client,
158
- account: accounts[0],
159
- currency: asset,
160
- escrowContract,
161
- feePayer: true,
162
- }),
163
- ],
164
- secretKey,
165
- })
166
-
167
- const { fetch } = Mppx_client.create({
168
- polyfill: false,
169
- methods: [
170
- sessionIntent({
171
- account: accounts[2],
172
- deposit: '10',
173
- getClient: () => client,
174
- }),
175
- ],
176
- })
284
+ const { fetch, mppx } = createSessionHarness(false)
177
285
 
178
- const handler = mppx.session({ amount: '1', unitType: 'token' })(() =>
286
+ const handler = mppx.session({ amount: '1', currency: asset, unitType: 'token' })(() =>
179
287
  Response.json({ data: 'streamed' }),
180
288
  )
181
289
 
@@ -192,4 +300,19 @@ describe('session', () => {
192
300
 
193
301
  server.close()
194
302
  })
303
+
304
+ test('fee payer: returns 200 with receipt on valid payment', async () => {
305
+ const { fetch, mppx } = createSessionHarness(true)
306
+
307
+ const handler = mppx.session({ amount: '1', currency: asset, unitType: 'token' })(() =>
308
+ Response.json({ data: 'streamed' }),
309
+ )
310
+
311
+ const server = await createServer(handler)
312
+ const response = await fetch(server.url)
313
+ expect(response.status).toBe(200)
314
+ expect(Receipt.fromResponse(response).status).toBe('success')
315
+
316
+ server.close()
317
+ })
195
318
  })