mppx 0.4.7 → 0.4.9

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 (209) hide show
  1. package/CHANGELOG.md +15 -3
  2. package/README.md +13 -13
  3. package/dist/BodyDigest.d.ts.map +1 -1
  4. package/dist/BodyDigest.js.map +1 -1
  5. package/dist/Challenge.d.ts.map +1 -1
  6. package/dist/Challenge.js.map +1 -1
  7. package/dist/Credential.d.ts.map +1 -1
  8. package/dist/Credential.js.map +1 -1
  9. package/dist/Errors.js +64 -67
  10. package/dist/Errors.js.map +1 -1
  11. package/dist/PaymentRequest.d.ts.map +1 -1
  12. package/dist/PaymentRequest.js.map +1 -1
  13. package/dist/Receipt.d.ts.map +1 -1
  14. package/dist/Receipt.js.map +1 -1
  15. package/dist/Store.d.ts +14 -4
  16. package/dist/Store.d.ts.map +1 -1
  17. package/dist/Store.js +17 -0
  18. package/dist/Store.js.map +1 -1
  19. package/dist/cli/account.d.ts.map +1 -1
  20. package/dist/cli/account.js +40 -5
  21. package/dist/cli/account.js.map +1 -1
  22. package/dist/cli/cli.d.ts.map +1 -1
  23. package/dist/cli/cli.js +24 -8
  24. package/dist/cli/cli.js.map +1 -1
  25. package/dist/cli/internal.d.ts.map +1 -1
  26. package/dist/cli/internal.js.map +1 -1
  27. package/dist/cli/plugins/stripe.d.ts.map +1 -1
  28. package/dist/cli/plugins/stripe.js.map +1 -1
  29. package/dist/cli/plugins/tempo.d.ts.map +1 -1
  30. package/dist/cli/plugins/tempo.js +11 -23
  31. package/dist/cli/plugins/tempo.js.map +1 -1
  32. package/dist/cli/utils.d.ts.map +1 -1
  33. package/dist/cli/utils.js.map +1 -1
  34. package/dist/client/internal/Fetch.d.ts +2 -0
  35. package/dist/client/internal/Fetch.d.ts.map +1 -1
  36. package/dist/client/internal/Fetch.js +1 -1
  37. package/dist/client/internal/Fetch.js.map +1 -1
  38. package/dist/internal/types.d.ts.map +1 -1
  39. package/dist/mcp-sdk/client/McpClient.d.ts.map +1 -1
  40. package/dist/mcp-sdk/client/McpClient.js +1 -1
  41. package/dist/mcp-sdk/client/McpClient.js.map +1 -1
  42. package/dist/mcp-sdk/server/Transport.d.ts.map +1 -1
  43. package/dist/mcp-sdk/server/Transport.js.map +1 -1
  44. package/dist/middlewares/elysia.d.ts.map +1 -1
  45. package/dist/middlewares/elysia.js +5 -1
  46. package/dist/middlewares/elysia.js.map +1 -1
  47. package/dist/middlewares/express.d.ts.map +1 -1
  48. package/dist/middlewares/express.js +5 -2
  49. package/dist/middlewares/express.js.map +1 -1
  50. package/dist/middlewares/hono.d.ts.map +1 -1
  51. package/dist/middlewares/hono.js.map +1 -1
  52. package/dist/proxy/Proxy.d.ts.map +1 -1
  53. package/dist/proxy/Proxy.js +3 -1
  54. package/dist/proxy/Proxy.js.map +1 -1
  55. package/dist/proxy/Service.js +1 -1
  56. package/dist/proxy/Service.js.map +1 -1
  57. package/dist/proxy/internal/Route.d.ts +2 -2
  58. package/dist/proxy/internal/Route.d.ts.map +1 -1
  59. package/dist/proxy/internal/Route.js +4 -2
  60. package/dist/proxy/internal/Route.js.map +1 -1
  61. package/dist/server/Mppx.d.ts.map +1 -1
  62. package/dist/server/Mppx.js +47 -11
  63. package/dist/server/Mppx.js.map +1 -1
  64. package/dist/server/Request.d.ts.map +1 -1
  65. package/dist/server/Request.js.map +1 -1
  66. package/dist/stripe/Methods.d.ts.map +1 -1
  67. package/dist/stripe/Methods.js.map +1 -1
  68. package/dist/tempo/Methods.d.ts.map +1 -1
  69. package/dist/tempo/Methods.js.map +1 -1
  70. package/dist/tempo/client/ChannelOps.d.ts.map +1 -1
  71. package/dist/tempo/client/ChannelOps.js.map +1 -1
  72. package/dist/tempo/client/Charge.d.ts.map +1 -1
  73. package/dist/tempo/client/Charge.js.map +1 -1
  74. package/dist/tempo/client/Session.d.ts.map +1 -1
  75. package/dist/tempo/client/Session.js.map +1 -1
  76. package/dist/tempo/client/SessionManager.d.ts.map +1 -1
  77. package/dist/tempo/client/SessionManager.js +1 -1
  78. package/dist/tempo/client/SessionManager.js.map +1 -1
  79. package/dist/tempo/internal/address.d.ts +3 -0
  80. package/dist/tempo/internal/address.d.ts.map +1 -0
  81. package/dist/tempo/internal/address.js +4 -0
  82. package/dist/tempo/internal/address.js.map +1 -0
  83. package/dist/tempo/internal/auto-swap.d.ts.map +1 -1
  84. package/dist/tempo/internal/auto-swap.js +4 -4
  85. package/dist/tempo/internal/auto-swap.js.map +1 -1
  86. package/dist/tempo/internal/fee-payer.d.ts +4 -1
  87. package/dist/tempo/internal/fee-payer.d.ts.map +1 -1
  88. package/dist/tempo/internal/fee-payer.js +12 -4
  89. package/dist/tempo/internal/fee-payer.js.map +1 -1
  90. package/dist/tempo/server/Charge.d.ts +11 -0
  91. package/dist/tempo/server/Charge.d.ts.map +1 -1
  92. package/dist/tempo/server/Charge.js +110 -51
  93. package/dist/tempo/server/Charge.js.map +1 -1
  94. package/dist/tempo/server/Session.d.ts +1 -1
  95. package/dist/tempo/server/Session.d.ts.map +1 -1
  96. package/dist/tempo/server/Session.js +31 -23
  97. package/dist/tempo/server/Session.js.map +1 -1
  98. package/dist/tempo/server/internal/transport.d.ts +1 -1
  99. package/dist/tempo/server/internal/transport.d.ts.map +1 -1
  100. package/dist/tempo/server/internal/transport.js +41 -1
  101. package/dist/tempo/server/internal/transport.js.map +1 -1
  102. package/dist/tempo/session/Chain.d.ts.map +1 -1
  103. package/dist/tempo/session/Chain.js +51 -10
  104. package/dist/tempo/session/Chain.js.map +1 -1
  105. package/dist/tempo/session/ChannelStore.d.ts +2 -0
  106. package/dist/tempo/session/ChannelStore.d.ts.map +1 -1
  107. package/dist/tempo/session/ChannelStore.js +4 -2
  108. package/dist/tempo/session/ChannelStore.js.map +1 -1
  109. package/dist/tempo/session/Receipt.d.ts.map +1 -1
  110. package/dist/tempo/session/Receipt.js.map +1 -1
  111. package/dist/tempo/session/Sse.d.ts.map +1 -1
  112. package/dist/tempo/session/Sse.js.map +1 -1
  113. package/dist/tempo/session/Voucher.d.ts.map +1 -1
  114. package/dist/tempo/session/Voucher.js +3 -2
  115. package/dist/tempo/session/Voucher.js.map +1 -1
  116. package/dist/viem/Client.d.ts.map +1 -1
  117. package/dist/viem/Client.js.map +1 -1
  118. package/package.json +2 -2
  119. package/src/BodyDigest.ts +1 -0
  120. package/src/Challenge.test-d.ts +1 -0
  121. package/src/Challenge.ts +1 -0
  122. package/src/Credential.ts +1 -0
  123. package/src/Errors.test.ts +27 -39
  124. package/src/Expires.test.ts +1 -0
  125. package/src/PaymentRequest.ts +1 -0
  126. package/src/Receipt.ts +1 -0
  127. package/src/Store.test-d.ts +59 -0
  128. package/src/Store.test.ts +56 -6
  129. package/src/Store.ts +31 -4
  130. package/src/cli/account.ts +65 -30
  131. package/src/cli/cli.test.ts +127 -1
  132. package/src/cli/cli.ts +23 -8
  133. package/src/cli/config.test.ts +1 -0
  134. package/src/cli/internal.ts +1 -0
  135. package/src/cli/plugins/stripe.ts +1 -0
  136. package/src/cli/plugins/tempo.ts +21 -24
  137. package/src/cli/utils.ts +1 -0
  138. package/src/client/Mppx.test-d.ts +1 -0
  139. package/src/client/internal/Fetch.browser.test.ts +1 -0
  140. package/src/client/internal/Fetch.test-d.ts +1 -0
  141. package/src/client/internal/Fetch.test.ts +1 -0
  142. package/src/client/internal/Fetch.ts +1 -1
  143. package/src/internal/constantTimeEqual.test.ts +1 -0
  144. package/src/internal/types.ts +1 -3
  145. package/src/mcp-sdk/client/McpClient.test-d.ts +1 -0
  146. package/src/mcp-sdk/client/McpClient.test.ts +1 -0
  147. package/src/mcp-sdk/client/McpClient.ts +2 -0
  148. package/src/mcp-sdk/server/Transport.test.ts +1 -0
  149. package/src/mcp-sdk/server/Transport.ts +1 -0
  150. package/src/middlewares/elysia.test.ts +90 -0
  151. package/src/middlewares/elysia.ts +5 -1
  152. package/src/middlewares/express.test.ts +62 -2
  153. package/src/middlewares/express.ts +6 -2
  154. package/src/middlewares/hono.ts +1 -0
  155. package/src/middlewares/internal/mppx.test.ts +1 -0
  156. package/src/middlewares/nextjs.test.ts +1 -0
  157. package/src/proxy/Proxy.test.ts +57 -0
  158. package/src/proxy/Proxy.ts +8 -1
  159. package/src/proxy/Service.test.ts +1 -0
  160. package/src/proxy/Service.ts +8 -2
  161. package/src/proxy/internal/Headers.test.ts +1 -0
  162. package/src/proxy/internal/Route.test.ts +57 -0
  163. package/src/proxy/internal/Route.ts +3 -1
  164. package/src/proxy/services/openai.test.ts +1 -0
  165. package/src/server/Mppx.test.ts +438 -0
  166. package/src/server/Mppx.ts +51 -13
  167. package/src/server/Request.test.ts +1 -0
  168. package/src/server/Request.ts +1 -0
  169. package/src/server/Response.test.ts +1 -0
  170. package/src/server/Transport.test.ts +1 -0
  171. package/src/stripe/Methods.ts +1 -0
  172. package/src/stripe/client/Charge.test.ts +1 -0
  173. package/src/stripe/server/Charge.test.ts +1 -0
  174. package/src/tempo/Attribution.test.ts +1 -0
  175. package/src/tempo/Methods.ts +1 -0
  176. package/src/tempo/client/ChannelOps.test.ts +1 -0
  177. package/src/tempo/client/ChannelOps.ts +1 -0
  178. package/src/tempo/client/Charge.ts +1 -0
  179. package/src/tempo/client/Session.test.ts +1 -0
  180. package/src/tempo/client/Session.ts +1 -0
  181. package/src/tempo/client/SessionManager.test.ts +28 -0
  182. package/src/tempo/client/SessionManager.ts +2 -1
  183. package/src/tempo/internal/address.ts +6 -0
  184. package/src/tempo/internal/auto-swap.test.ts +1 -0
  185. package/src/tempo/internal/auto-swap.ts +4 -3
  186. package/src/tempo/internal/defaults.test.ts +1 -0
  187. package/src/tempo/internal/fee-payer.test.ts +1 -0
  188. package/src/tempo/internal/fee-payer.ts +19 -4
  189. package/src/tempo/server/Charge.test.ts +1081 -31
  190. package/src/tempo/server/Charge.ts +159 -63
  191. package/src/tempo/server/Session.test.ts +896 -107
  192. package/src/tempo/server/Session.ts +41 -23
  193. package/src/tempo/server/Sse.test.ts +2 -0
  194. package/src/tempo/server/internal/transport.test.ts +30 -0
  195. package/src/tempo/server/internal/transport.ts +41 -2
  196. package/src/tempo/session/Chain.test.ts +145 -0
  197. package/src/tempo/session/Chain.ts +59 -10
  198. package/src/tempo/session/Channel.test.ts +1 -0
  199. package/src/tempo/session/ChannelStore.test.ts +11 -0
  200. package/src/tempo/session/ChannelStore.ts +7 -3
  201. package/src/tempo/session/Receipt.test.ts +1 -0
  202. package/src/tempo/session/Receipt.ts +1 -0
  203. package/src/tempo/session/Sse.test.ts +2 -0
  204. package/src/tempo/session/Sse.ts +1 -0
  205. package/src/tempo/session/Voucher.test.ts +1 -0
  206. package/src/tempo/session/Voucher.ts +4 -2
  207. package/src/viem/Account.test.ts +1 -0
  208. package/src/viem/Client.test.ts +1 -0
  209. package/src/viem/Client.ts +1 -0
@@ -1,4 +1,5 @@
1
1
  import { describe, expect, test } from 'vitest'
2
+
2
3
  import {
3
4
  AmountExceedsDepositError,
4
5
  BadRequestError,
@@ -41,9 +42,8 @@ describe('MalformedCredentialError', () => {
41
42
  })
42
43
 
43
44
  test('with reason', () => {
44
- expect(
45
- errorSnapshot(new MalformedCredentialError({ reason: 'invalid base64url' })),
46
- ).toMatchInlineSnapshot(`
45
+ expect(errorSnapshot(new MalformedCredentialError({ reason: 'invalid base64url' })))
46
+ .toMatchInlineSnapshot(`
47
47
  {
48
48
  "message": "Credential is malformed: invalid base64url.",
49
49
  "name": "MalformedCredentialError",
@@ -89,9 +89,8 @@ describe('InvalidChallengeError', () => {
89
89
  })
90
90
 
91
91
  test('with id and reason', () => {
92
- expect(
93
- errorSnapshot(new InvalidChallengeError({ id: 'abc123', reason: 'already used' })),
94
- ).toMatchInlineSnapshot(`
92
+ expect(errorSnapshot(new InvalidChallengeError({ id: 'abc123', reason: 'already used' })))
93
+ .toMatchInlineSnapshot(`
95
94
  {
96
95
  "message": "Challenge "abc123" is invalid: already used.",
97
96
  "name": "InvalidChallengeError",
@@ -115,9 +114,8 @@ describe('VerificationFailedError', () => {
115
114
  })
116
115
 
117
116
  test('with reason', () => {
118
- expect(
119
- errorSnapshot(new VerificationFailedError({ reason: 'invalid signature' })),
120
- ).toMatchInlineSnapshot(`
117
+ expect(errorSnapshot(new VerificationFailedError({ reason: 'invalid signature' })))
118
+ .toMatchInlineSnapshot(`
121
119
  {
122
120
  "message": "Payment verification failed: invalid signature.",
123
121
  "name": "VerificationFailedError",
@@ -141,9 +139,8 @@ describe('PaymentExpiredError', () => {
141
139
  })
142
140
 
143
141
  test('with expires', () => {
144
- expect(
145
- errorSnapshot(new PaymentExpiredError({ expires: '2025-01-26T12:00:00Z' })),
146
- ).toMatchInlineSnapshot(`
142
+ expect(errorSnapshot(new PaymentExpiredError({ expires: '2025-01-26T12:00:00Z' })))
143
+ .toMatchInlineSnapshot(`
147
144
  {
148
145
  "message": "Payment expired at 2025-01-26T12:00:00Z.",
149
146
  "name": "PaymentExpiredError",
@@ -167,9 +164,8 @@ describe('PaymentRequiredError', () => {
167
164
  })
168
165
 
169
166
  test('with description', () => {
170
- expect(
171
- errorSnapshot(new PaymentRequiredError({ description: 'API access fee' })),
172
- ).toMatchInlineSnapshot(`
167
+ expect(errorSnapshot(new PaymentRequiredError({ description: 'API access fee' })))
168
+ .toMatchInlineSnapshot(`
173
169
  {
174
170
  "message": "Payment is required (API access fee).",
175
171
  "name": "PaymentRequiredError",
@@ -193,9 +189,8 @@ describe('InvalidPayloadError', () => {
193
189
  })
194
190
 
195
191
  test('with reason', () => {
196
- expect(
197
- errorSnapshot(new InvalidPayloadError({ reason: 'missing signature field' })),
198
- ).toMatchInlineSnapshot(`
192
+ expect(errorSnapshot(new InvalidPayloadError({ reason: 'missing signature field' })))
193
+ .toMatchInlineSnapshot(`
199
194
  {
200
195
  "message": "Credential payload is invalid: missing signature field.",
201
196
  "name": "InvalidPayloadError",
@@ -219,9 +214,8 @@ describe('BadRequestError', () => {
219
214
  })
220
215
 
221
216
  test('with reason', () => {
222
- expect(
223
- errorSnapshot(new BadRequestError({ reason: 'cannot combine hash type with feePayer' })),
224
- ).toMatchInlineSnapshot(`
217
+ expect(errorSnapshot(new BadRequestError({ reason: 'cannot combine hash type with feePayer' })))
218
+ .toMatchInlineSnapshot(`
225
219
  {
226
220
  "message": "Bad request: cannot combine hash type with feePayer.",
227
221
  "name": "BadRequestError",
@@ -245,9 +239,8 @@ describe('PaymentInsufficientError', () => {
245
239
  })
246
240
 
247
241
  test('with reason', () => {
248
- expect(
249
- errorSnapshot(new PaymentInsufficientError({ reason: 'expected 1000, received 500' })),
250
- ).toMatchInlineSnapshot(`
242
+ expect(errorSnapshot(new PaymentInsufficientError({ reason: 'expected 1000, received 500' })))
243
+ .toMatchInlineSnapshot(`
251
244
  {
252
245
  "message": "Payment insufficient: expected 1000, received 500.",
253
246
  "name": "PaymentInsufficientError",
@@ -271,9 +264,8 @@ describe('PaymentMethodUnsupportedError', () => {
271
264
  })
272
265
 
273
266
  test('with method', () => {
274
- expect(
275
- errorSnapshot(new PaymentMethodUnsupportedError({ method: 'bitcoin' })),
276
- ).toMatchInlineSnapshot(`
267
+ expect(errorSnapshot(new PaymentMethodUnsupportedError({ method: 'bitcoin' })))
268
+ .toMatchInlineSnapshot(`
277
269
  {
278
270
  "message": "Payment method "bitcoin" is not supported.",
279
271
  "name": "PaymentMethodUnsupportedError",
@@ -297,9 +289,8 @@ describe('InsufficientBalanceError', () => {
297
289
  })
298
290
 
299
291
  test('with reason', () => {
300
- expect(
301
- errorSnapshot(new InsufficientBalanceError({ reason: 'requested 500, available 100' })),
302
- ).toMatchInlineSnapshot(`
292
+ expect(errorSnapshot(new InsufficientBalanceError({ reason: 'requested 500, available 100' })))
293
+ .toMatchInlineSnapshot(`
303
294
  {
304
295
  "message": "Insufficient balance: requested 500, available 100.",
305
296
  "name": "InsufficientBalanceError",
@@ -323,9 +314,8 @@ describe('InvalidSignatureError', () => {
323
314
  })
324
315
 
325
316
  test('with reason', () => {
326
- expect(
327
- errorSnapshot(new InvalidSignatureError({ reason: 'ECDSA recovery failed' })),
328
- ).toMatchInlineSnapshot(`
317
+ expect(errorSnapshot(new InvalidSignatureError({ reason: 'ECDSA recovery failed' })))
318
+ .toMatchInlineSnapshot(`
329
319
  {
330
320
  "message": "Invalid signature: ECDSA recovery failed.",
331
321
  "name": "InvalidSignatureError",
@@ -401,9 +391,8 @@ describe('ChannelClosedError', () => {
401
391
  })
402
392
 
403
393
  test('with reason', () => {
404
- expect(
405
- errorSnapshot(new ChannelClosedError({ reason: 'channel is finalized on-chain' })),
406
- ).toMatchInlineSnapshot(`
394
+ expect(errorSnapshot(new ChannelClosedError({ reason: 'channel is finalized on-chain' })))
395
+ .toMatchInlineSnapshot(`
407
396
  {
408
397
  "message": "Channel closed: channel is finalized on-chain.",
409
398
  "name": "ChannelClosedError",
@@ -427,9 +416,8 @@ describe('PaymentActionRequiredError', () => {
427
416
  })
428
417
 
429
418
  test('with reason', () => {
430
- expect(
431
- errorSnapshot(new PaymentActionRequiredError({ reason: 'requires_action' })),
432
- ).toMatchInlineSnapshot(`
419
+ expect(errorSnapshot(new PaymentActionRequiredError({ reason: 'requires_action' })))
420
+ .toMatchInlineSnapshot(`
433
421
  {
434
422
  "message": "Payment requires action: requires_action.",
435
423
  "name": "PaymentActionRequiredError",
@@ -1,4 +1,5 @@
1
1
  import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'
2
+
2
3
  import * as Expires from './Expires.js'
3
4
 
4
5
  const FIXED_NOW = new Date('2025-06-15T12:00:00.000Z').getTime()
@@ -1,4 +1,5 @@
1
1
  import { Base64, Json } from 'ox'
2
+
2
3
  import type { Compute } from './internal/types.js'
3
4
  import type * as Method from './Method.js'
4
5
  import type * as z from './zod.js'
package/src/Receipt.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { Base64 } from 'ox'
2
+
2
3
  import * as z from './zod.js'
3
4
 
4
5
  /**
@@ -0,0 +1,59 @@
1
+ import { expectTypeOf, test } from 'vitest'
2
+
3
+ import * as Store from './Store.js'
4
+
5
+ test('default Store accepts any string key', () => {
6
+ const store = Store.memory()
7
+ expectTypeOf(store.get).parameter(0).toBeString()
8
+ expectTypeOf(store.put).parameter(0).toBeString()
9
+ expectTypeOf(store.delete).parameter(0).toBeString()
10
+ })
11
+
12
+ test('default Store get returns unknown', async () => {
13
+ const store = Store.memory()
14
+ const value = await store.get('anything')
15
+ expectTypeOf(value).toEqualTypeOf<unknown>()
16
+ })
17
+
18
+ test('typed Store constrains keys', () => {
19
+ type ItemMap = { [key: `mppx:charge:${string}`]: number }
20
+ const store = {} as Store.Store<ItemMap>
21
+
22
+ expectTypeOf(store.get).parameter(0).toEqualTypeOf<`mppx:charge:${string}`>()
23
+ expectTypeOf(store.put).parameter(0).toEqualTypeOf<`mppx:charge:${string}`>()
24
+ expectTypeOf(store.delete).parameter(0).toEqualTypeOf<`mppx:charge:${string}`>()
25
+ })
26
+
27
+ test('typed Store infers value from key', async () => {
28
+ type ItemMap = { [key: `mppx:charge:${string}`]: number }
29
+ const store = {} as Store.Store<ItemMap>
30
+
31
+ const value = await store.get('mppx:charge:0x123')
32
+ expectTypeOf(value).toEqualTypeOf<number | null>()
33
+ })
34
+
35
+ test('typed Store enforces value type on put', () => {
36
+ type ItemMap = { [key: `mppx:charge:${string}`]: number }
37
+ const store = {} as Store.Store<ItemMap>
38
+
39
+ // @ts-expect-error — value must be number, not string
40
+ store.put('mppx:charge:0x123', 'wrong')
41
+ })
42
+
43
+ test('cloudflare returns generic Store', () => {
44
+ const store = Store.cloudflare({
45
+ get: async () => null,
46
+ put: async () => {},
47
+ delete: async () => {},
48
+ })
49
+ expectTypeOf(store).toEqualTypeOf<Store.Store>()
50
+ })
51
+
52
+ test('upstash returns generic Store', () => {
53
+ const store = Store.upstash({
54
+ get: async () => null,
55
+ set: async () => null,
56
+ del: async () => null,
57
+ })
58
+ expectTypeOf(store).toEqualTypeOf<Store.Store>()
59
+ })
package/src/Store.test.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { describe, expect, test } from 'vitest'
2
+
2
3
  import * as Store from './Store.js'
3
4
 
4
5
  const nested = {
@@ -7,16 +8,16 @@ const nested = {
7
8
  meta: { active: true, tags: ['a', 'b'] },
8
9
  }
9
10
 
10
- function fakeKv(): Store.cloudflare.Parameters {
11
+ function fakeKv() {
11
12
  const map = new Map<string, string>()
12
13
  return {
13
- async get(key) {
14
+ async get(key: string) {
14
15
  return map.get(key) ?? null
15
16
  },
16
- async put(key, value) {
17
+ async put(key: string, value: string) {
17
18
  map.set(key, value)
18
19
  },
19
- async delete(key) {
20
+ async delete(key: string) {
20
21
  map.delete(key)
21
22
  },
22
23
  }
@@ -25,6 +26,17 @@ function fakeKv(): Store.cloudflare.Parameters {
25
26
  describe.each([
26
27
  { label: 'memory', create: () => Store.memory() },
27
28
  { label: 'cloudflare', create: () => Store.cloudflare(fakeKv()) },
29
+ {
30
+ label: 'redis',
31
+ create: () => {
32
+ const kv = fakeKv()
33
+ return Store.redis({
34
+ get: kv.get,
35
+ set: kv.put,
36
+ del: (key) => kv.delete(key),
37
+ })
38
+ },
39
+ },
28
40
  {
29
41
  label: 'upstash',
30
42
  create: () => {
@@ -64,6 +76,20 @@ describe.each([
64
76
  })
65
77
 
66
78
  describe('json roundtrip behavior', () => {
79
+ test('cloudflare json-roundtrips nested objects', async () => {
80
+ const store = Store.cloudflare(fakeKv())
81
+ const value = { a: [1, { b: 'c' }], d: null }
82
+ await store.put('k', value)
83
+ expect(await store.get('k')).toEqual(value)
84
+ })
85
+
86
+ test('cloudflare roundtrips BigInt values', async () => {
87
+ const store = Store.cloudflare(fakeKv())
88
+ const value = { amount: 1000000000000000000n, nested: { big: 42n } }
89
+ await store.put('k', value)
90
+ expect(await store.get('k')).toEqual(value)
91
+ })
92
+
67
93
  test('memory json-roundtrips nested objects', async () => {
68
94
  const store = Store.memory()
69
95
  const value = { a: [1, { b: 'c' }], d: null }
@@ -71,13 +97,37 @@ describe('json roundtrip behavior', () => {
71
97
  expect(await store.get('k')).toEqual(value)
72
98
  })
73
99
 
74
- test('cloudflare json-roundtrips nested objects', async () => {
75
- const store = Store.cloudflare(fakeKv())
100
+ test('memory roundtrips BigInt values', async () => {
101
+ const store = Store.memory()
102
+ const value = { amount: 1000000000000000000n, nested: { big: 42n } }
103
+ await store.put('k', value)
104
+ expect(await store.get('k')).toEqual(value)
105
+ })
106
+
107
+ test('redis json-roundtrips nested objects', async () => {
108
+ const kv = fakeKv()
109
+ const store = Store.redis({
110
+ get: kv.get,
111
+ set: kv.put,
112
+ del: (key) => kv.delete(key),
113
+ })
76
114
  const value = { a: [1, { b: 'c' }], d: null }
77
115
  await store.put('k', value)
78
116
  expect(await store.get('k')).toEqual(value)
79
117
  })
80
118
 
119
+ test('redis roundtrips BigInt values', async () => {
120
+ const kv = fakeKv()
121
+ const store = Store.redis({
122
+ get: kv.get,
123
+ set: kv.put,
124
+ del: (key) => kv.delete(key),
125
+ })
126
+ const value = { amount: 1000000000000000000n, nested: { big: 42n } }
127
+ await store.put('k', value)
128
+ expect(await store.get('k')).toEqual(value)
129
+ })
130
+
81
131
  test('upstash passes values through without json serialization', async () => {
82
132
  const kv = fakeKv()
83
133
  const store = Store.upstash({
package/src/Store.ts CHANGED
@@ -6,10 +6,12 @@
6
6
  */
7
7
  import { Json } from 'ox'
8
8
 
9
- export type Store = {
10
- get: <value = unknown>(key: string) => Promise<value | null>
11
- put: (key: string, value: unknown) => Promise<void>
12
- delete: (key: string) => Promise<void>
9
+ export type StoreItemMap = Record<string, unknown>
10
+
11
+ export type Store<itemMap extends StoreItemMap = StoreItemMap> = {
12
+ get: <key extends keyof itemMap & string>(key: key) => Promise<itemMap[key] | null>
13
+ put: <key extends keyof itemMap & string>(key: key, value: itemMap[key]) => Promise<void>
14
+ delete: <key extends keyof itemMap & string>(key: key) => Promise<void>
13
15
  }
14
16
 
15
17
  /** Creates a {@link Store} from an existing implementation. */
@@ -60,6 +62,31 @@ export function memory(): Store {
60
62
  })
61
63
  }
62
64
 
65
+ /** Wraps a standard Redis client (ioredis, node-redis, Valkey). */
66
+ export function redis(client: redis.Parameters): Store {
67
+ return from({
68
+ async get(key) {
69
+ const raw = await client.get(key)
70
+ if (raw == null) return null as any
71
+ return Json.parse(raw)
72
+ },
73
+ async put(key, value) {
74
+ await client.set(key, Json.stringify(value))
75
+ },
76
+ async delete(key) {
77
+ await client.del(key)
78
+ },
79
+ })
80
+ }
81
+
82
+ export declare namespace redis {
83
+ export type Parameters = {
84
+ get: (key: string) => Promise<string | null>
85
+ set: (key: string, value: string) => Promise<unknown>
86
+ del: (key: string) => Promise<unknown>
87
+ }
88
+ }
89
+
63
90
  /** Wraps an Upstash Redis instance (e.g. Vercel KV). */
64
91
  export function upstash(redis: upstash.Parameters): Store {
65
92
  return from({
@@ -55,50 +55,85 @@ export function createKeychain(account = 'main') {
55
55
  async list(): Promise<string[]> {
56
56
  const platform = os.platform()
57
57
  if (platform === 'darwin') {
58
- const { stdout, error } = await execCommand('security', ['dump-keychain'])
59
- if (error) return []
60
- const accounts: string[] = []
61
- const blocks = stdout.split('keychain:')
62
- for (const block of blocks) {
63
- const serviceMatch = block.match(/"svce"<blob>="([^"]*)"/)
64
- const accountMatch = block.match(/"acct"<blob>="([^"]*)"/)
65
- if (serviceMatch?.[1] === service && accountMatch?.[1]) accounts.push(accountMatch[1])
66
- }
67
- return accounts
58
+ const { stdout, error } = await execCommand('security', ['dump-keychain'])
59
+ if (error) return []
60
+ const accounts: string[] = []
61
+ const blocks = stdout.split('keychain:')
62
+ for (const block of blocks) {
63
+ const serviceMatch = block.match(/"svce"<blob>="([^"]*)"/)
64
+ const accountMatch = block.match(/"acct"<blob>="([^"]*)"/)
65
+ if (serviceMatch?.[1] === service && accountMatch?.[1]) accounts.push(accountMatch[1])
66
+ }
67
+ return accounts
68
68
  }
69
69
  if (platform === 'linux') {
70
- const { stdout, stderr, error } = await execCommand('secret-tool', ['search', '--all', '--unlock', 'service', service])
71
- if (error) return []
72
- const combined = `${stdout}\n${stderr}`
73
- const accounts: string[] = []
74
- const matches = combined.matchAll(/\baccount = (.+)/g)
75
- for (const match of matches) if (match[1]) accounts.push(match[1])
76
- return accounts
70
+ const { stdout, stderr, error } = await execCommand('secret-tool', [
71
+ 'search',
72
+ '--all',
73
+ '--unlock',
74
+ 'service',
75
+ service,
76
+ ])
77
+ if (error) return []
78
+ const combined = `${stdout}\n${stderr}`
79
+ const accounts: string[] = []
80
+ const matches = combined.matchAll(/\baccount = (.+)/g)
81
+ for (const match of matches) if (match[1]) accounts.push(match[1])
82
+ return accounts
77
83
  }
78
84
  throw new Error(`Unsupported platform: ${platform}`)
79
85
  },
80
86
  async get(): Promise<string | undefined> {
81
87
  const platform = os.platform()
82
88
  if (platform === 'darwin') {
83
- const { stdout, error } = await execCommand('security', ['find-generic-password', '-s', service, '-a', account, '-w'])
84
- return error ? undefined : stdout
89
+ const { stdout, error } = await execCommand('security', [
90
+ 'find-generic-password',
91
+ '-s',
92
+ service,
93
+ '-a',
94
+ account,
95
+ '-w',
96
+ ])
97
+ return error ? undefined : stdout
85
98
  }
86
99
  if (platform === 'linux') {
87
- const { stdout, error } = await execCommand('secret-tool', ['lookup', 'service', service, 'account', account])
88
- return error ? undefined : stdout || undefined
100
+ const { stdout, error } = await execCommand('secret-tool', [
101
+ 'lookup',
102
+ 'service',
103
+ service,
104
+ 'account',
105
+ account,
106
+ ])
107
+ return error ? undefined : stdout || undefined
89
108
  }
90
109
  throw new Error(`Unsupported platform: ${platform}`)
91
110
  },
92
111
  async set(value: string): Promise<void> {
93
112
  const platform = os.platform()
94
113
  if (platform === 'darwin') {
95
- await execCommand('security', ['delete-generic-password', '-s', service, '-a', account])
96
- const { error } = await execCommand('security', ['add-generic-password', '-s', service, '-a', account, '-w', value])
97
- if (error) throw error
98
- return
114
+ await execCommand('security', ['delete-generic-password', '-s', service, '-a', account])
115
+ const { error } = await execCommand('security', [
116
+ 'add-generic-password',
117
+ '-s',
118
+ service,
119
+ '-a',
120
+ account,
121
+ '-w',
122
+ value,
123
+ ])
124
+ if (error) throw error
125
+ return
99
126
  }
100
127
  if (platform === 'linux') {
101
- const proc = child.execFile('secret-tool', ['store', '--label', `${service} ${account}`, 'service', service, 'account', account])
128
+ const proc = child.execFile('secret-tool', [
129
+ 'store',
130
+ '--label',
131
+ `${service} ${account}`,
132
+ 'service',
133
+ service,
134
+ 'account',
135
+ account,
136
+ ])
102
137
  proc.stdin?.write(value)
103
138
  proc.stdin?.end()
104
139
  return new Promise((resolve, reject) => {
@@ -114,12 +149,12 @@ export function createKeychain(account = 'main') {
114
149
  async delete(): Promise<void> {
115
150
  const platform = os.platform()
116
151
  if (platform === 'darwin') {
117
- await execCommand('security', ['delete-generic-password', '-s', service, '-a', account])
118
- return
152
+ await execCommand('security', ['delete-generic-password', '-s', service, '-a', account])
153
+ return
119
154
  }
120
155
  if (platform === 'linux') {
121
- await execCommand('secret-tool', ['clear', 'service', service, 'account', account])
122
- return
156
+ await execCommand('secret-tool', ['clear', 'service', service, 'account', account])
157
+ return
123
158
  }
124
159
  throw new Error(`Unsupported platform: ${platform}`)
125
160
  },
@@ -2,6 +2,7 @@ import { spawnSync } from 'node:child_process'
2
2
  import * as fs from 'node:fs'
3
3
  import * as os from 'node:os'
4
4
  import * as path from 'node:path'
5
+
5
6
  import { parseUnits } from 'viem'
6
7
  import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts'
7
8
  import { Addresses } from 'viem/tempo'
@@ -10,11 +11,14 @@ import * as Http from '~test/Http.js'
10
11
  import { rpcUrl } from '~test/tempo/prool.js'
11
12
  import { deployEscrow } from '~test/tempo/session.js'
12
13
  import { accounts, asset, client, fundAccount } from '~test/tempo/viem.js'
13
- import * as Store from '../Store.js'
14
+
15
+ import * as Credential from '../Credential.js'
14
16
  import * as Mppx_server from '../server/Mppx.js'
15
17
  import { toNodeListener } from '../server/Mppx.js'
18
+ import * as Store from '../Store.js'
16
19
  import { stripe as stripe_server } from '../stripe/server/Methods.js'
17
20
  import { tempo } from '../tempo/server/Methods.js'
21
+ import type { SessionCredentialPayload } from '../tempo/session/Types.js'
18
22
  import cli from './cli.js'
19
23
 
20
24
  const testPrivateKey = generatePrivateKey()
@@ -192,6 +196,128 @@ describe('session multi-fetch (examples/session/multi-fetch)', () => {
192
196
  }
193
197
  })
194
198
 
199
+ test('bug: non-SSE open should not double-charge tick amount', { timeout: 120_000 }, async () => {
200
+ await fundAccount({ address: testAccount.address, token: Addresses.pathUsd })
201
+ await fundAccount({ address: testAccount.address, token: asset })
202
+
203
+ const escrow = await deployEscrow()
204
+ const store = Store.memory()
205
+ const tickAmount = '0.001'
206
+ const server = Mppx_server.create({
207
+ methods: [
208
+ tempo.session({
209
+ account: accounts[0],
210
+ store,
211
+ getClient: () => client,
212
+ currency: asset,
213
+ escrowContract: escrow,
214
+ chainId: client.chain.id,
215
+ feePayer: true,
216
+ }),
217
+ ],
218
+ realm: 'cli-test-double-charge',
219
+ secretKey: 'cli-test-secret',
220
+ })
221
+
222
+ // Track voucher cumulative amounts from credential payloads
223
+ const voucherAmounts: string[] = []
224
+
225
+ const httpServer = await Http.createServer(async (req, res) => {
226
+ const authHeader = req.headers.authorization
227
+ if (authHeader) {
228
+ try {
229
+ const cred = Credential.deserialize<SessionCredentialPayload>(authHeader)
230
+ if (cred.payload.action === 'voucher' && 'cumulativeAmount' in cred.payload) {
231
+ voucherAmounts.push(cred.payload.cumulativeAmount)
232
+ }
233
+ } catch {}
234
+ }
235
+
236
+ const result = await toNodeListener(
237
+ server.session({
238
+ amount: tickAmount,
239
+ recipient: accounts[0].address,
240
+ unitType: 'page',
241
+ }),
242
+ )(req, res)
243
+ if (result.status === 402) return
244
+ // Non-SSE: plain text response (not text/event-stream)
245
+ res.end('scraped-content')
246
+ })
247
+
248
+ try {
249
+ await serve([httpServer.url, '--rpc-url', rpcUrl, '-s', '-M', 'deposit=10'], {
250
+ env: { MPPX_PRIVATE_KEY: testPrivateKey },
251
+ })
252
+
253
+ // No follow-up voucher should be sent after a non-SSE open.
254
+ // The open credential already paid for this unit, so the CLI
255
+ // should NOT send a redundant voucher that would double-charge.
256
+ expect(voucherAmounts.length).toBe(0)
257
+ } finally {
258
+ httpServer.close()
259
+ }
260
+ })
261
+
262
+ test('bug: closeChannel sends action "close" not "voucher"', { timeout: 120_000 }, async () => {
263
+ await fundAccount({ address: testAccount.address, token: Addresses.pathUsd })
264
+ await fundAccount({ address: testAccount.address, token: asset })
265
+
266
+ const escrow = await deployEscrow()
267
+ const store = Store.memory()
268
+ const server = Mppx_server.create({
269
+ methods: [
270
+ tempo.session({
271
+ account: accounts[0],
272
+ store,
273
+ getClient: () => client,
274
+ currency: asset,
275
+ escrowContract: escrow,
276
+ chainId: client.chain.id,
277
+ feePayer: true,
278
+ }),
279
+ ],
280
+ realm: 'cli-test-close-action',
281
+ secretKey: 'cli-test-secret',
282
+ })
283
+
284
+ // Track the credential payload action from the close request
285
+ const credentialActions: string[] = []
286
+
287
+ const httpServer = await Http.createServer(async (req, res) => {
288
+ // Capture credential action from every request with Authorization header
289
+ const authHeader = req.headers.authorization
290
+ if (authHeader) {
291
+ try {
292
+ const cred = Credential.deserialize<SessionCredentialPayload>(authHeader)
293
+ credentialActions.push(cred.payload.action)
294
+ } catch {}
295
+ }
296
+
297
+ const result = await toNodeListener(
298
+ server.session({
299
+ amount: '0.001',
300
+ recipient: accounts[0].address,
301
+ unitType: 'page',
302
+ }),
303
+ )(req, res)
304
+ if (result.status === 402) return
305
+ res.end('scraped-content')
306
+ })
307
+
308
+ try {
309
+ await serve([httpServer.url, '--rpc-url', rpcUrl, '-s', '-M', 'deposit=10'], {
310
+ env: { MPPX_PRIVATE_KEY: testPrivateKey },
311
+ })
312
+
313
+ // The last credential sent should be the close request with action: 'close'
314
+ const lastAction = credentialActions[credentialActions.length - 1]
315
+ expect(lastAction).toBe('close')
316
+ } finally {
317
+ httpServer.close()
318
+ }
319
+ })
320
+
195
321
  test('error: --fail exits on server error', { timeout: 60_000 }, async () => {
196
322
  const httpServer = await Http.createServer(async (_req, res) => {
197
323
  res.writeHead(500)