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.
- package/CHANGELOG.md +15 -3
- package/README.md +13 -13
- package/dist/BodyDigest.d.ts.map +1 -1
- package/dist/BodyDigest.js.map +1 -1
- package/dist/Challenge.d.ts.map +1 -1
- package/dist/Challenge.js.map +1 -1
- package/dist/Credential.d.ts.map +1 -1
- package/dist/Credential.js.map +1 -1
- package/dist/Errors.js +64 -67
- package/dist/Errors.js.map +1 -1
- package/dist/PaymentRequest.d.ts.map +1 -1
- package/dist/PaymentRequest.js.map +1 -1
- package/dist/Receipt.d.ts.map +1 -1
- package/dist/Receipt.js.map +1 -1
- package/dist/Store.d.ts +14 -4
- package/dist/Store.d.ts.map +1 -1
- package/dist/Store.js +17 -0
- package/dist/Store.js.map +1 -1
- package/dist/cli/account.d.ts.map +1 -1
- package/dist/cli/account.js +40 -5
- package/dist/cli/account.js.map +1 -1
- package/dist/cli/cli.d.ts.map +1 -1
- package/dist/cli/cli.js +24 -8
- package/dist/cli/cli.js.map +1 -1
- package/dist/cli/internal.d.ts.map +1 -1
- package/dist/cli/internal.js.map +1 -1
- package/dist/cli/plugins/stripe.d.ts.map +1 -1
- package/dist/cli/plugins/stripe.js.map +1 -1
- package/dist/cli/plugins/tempo.d.ts.map +1 -1
- package/dist/cli/plugins/tempo.js +11 -23
- package/dist/cli/plugins/tempo.js.map +1 -1
- package/dist/cli/utils.d.ts.map +1 -1
- package/dist/cli/utils.js.map +1 -1
- package/dist/client/internal/Fetch.d.ts +2 -0
- package/dist/client/internal/Fetch.d.ts.map +1 -1
- package/dist/client/internal/Fetch.js +1 -1
- package/dist/client/internal/Fetch.js.map +1 -1
- package/dist/internal/types.d.ts.map +1 -1
- package/dist/mcp-sdk/client/McpClient.d.ts.map +1 -1
- package/dist/mcp-sdk/client/McpClient.js +1 -1
- package/dist/mcp-sdk/client/McpClient.js.map +1 -1
- package/dist/mcp-sdk/server/Transport.d.ts.map +1 -1
- package/dist/mcp-sdk/server/Transport.js.map +1 -1
- package/dist/middlewares/elysia.d.ts.map +1 -1
- package/dist/middlewares/elysia.js +5 -1
- package/dist/middlewares/elysia.js.map +1 -1
- package/dist/middlewares/express.d.ts.map +1 -1
- package/dist/middlewares/express.js +5 -2
- package/dist/middlewares/express.js.map +1 -1
- package/dist/middlewares/hono.d.ts.map +1 -1
- package/dist/middlewares/hono.js.map +1 -1
- package/dist/proxy/Proxy.d.ts.map +1 -1
- package/dist/proxy/Proxy.js +3 -1
- package/dist/proxy/Proxy.js.map +1 -1
- package/dist/proxy/Service.js +1 -1
- package/dist/proxy/Service.js.map +1 -1
- package/dist/proxy/internal/Route.d.ts +2 -2
- package/dist/proxy/internal/Route.d.ts.map +1 -1
- package/dist/proxy/internal/Route.js +4 -2
- package/dist/proxy/internal/Route.js.map +1 -1
- package/dist/server/Mppx.d.ts.map +1 -1
- package/dist/server/Mppx.js +47 -11
- package/dist/server/Mppx.js.map +1 -1
- package/dist/server/Request.d.ts.map +1 -1
- package/dist/server/Request.js.map +1 -1
- package/dist/stripe/Methods.d.ts.map +1 -1
- package/dist/stripe/Methods.js.map +1 -1
- package/dist/tempo/Methods.d.ts.map +1 -1
- package/dist/tempo/Methods.js.map +1 -1
- package/dist/tempo/client/ChannelOps.d.ts.map +1 -1
- package/dist/tempo/client/ChannelOps.js.map +1 -1
- package/dist/tempo/client/Charge.d.ts.map +1 -1
- package/dist/tempo/client/Charge.js.map +1 -1
- package/dist/tempo/client/Session.d.ts.map +1 -1
- package/dist/tempo/client/Session.js.map +1 -1
- package/dist/tempo/client/SessionManager.d.ts.map +1 -1
- package/dist/tempo/client/SessionManager.js +1 -1
- package/dist/tempo/client/SessionManager.js.map +1 -1
- package/dist/tempo/internal/address.d.ts +3 -0
- package/dist/tempo/internal/address.d.ts.map +1 -0
- package/dist/tempo/internal/address.js +4 -0
- package/dist/tempo/internal/address.js.map +1 -0
- package/dist/tempo/internal/auto-swap.d.ts.map +1 -1
- package/dist/tempo/internal/auto-swap.js +4 -4
- package/dist/tempo/internal/auto-swap.js.map +1 -1
- package/dist/tempo/internal/fee-payer.d.ts +4 -1
- package/dist/tempo/internal/fee-payer.d.ts.map +1 -1
- package/dist/tempo/internal/fee-payer.js +12 -4
- package/dist/tempo/internal/fee-payer.js.map +1 -1
- package/dist/tempo/server/Charge.d.ts +11 -0
- package/dist/tempo/server/Charge.d.ts.map +1 -1
- package/dist/tempo/server/Charge.js +110 -51
- package/dist/tempo/server/Charge.js.map +1 -1
- package/dist/tempo/server/Session.d.ts +1 -1
- package/dist/tempo/server/Session.d.ts.map +1 -1
- package/dist/tempo/server/Session.js +31 -23
- package/dist/tempo/server/Session.js.map +1 -1
- package/dist/tempo/server/internal/transport.d.ts +1 -1
- package/dist/tempo/server/internal/transport.d.ts.map +1 -1
- package/dist/tempo/server/internal/transport.js +41 -1
- package/dist/tempo/server/internal/transport.js.map +1 -1
- package/dist/tempo/session/Chain.d.ts.map +1 -1
- package/dist/tempo/session/Chain.js +51 -10
- package/dist/tempo/session/Chain.js.map +1 -1
- package/dist/tempo/session/ChannelStore.d.ts +2 -0
- package/dist/tempo/session/ChannelStore.d.ts.map +1 -1
- package/dist/tempo/session/ChannelStore.js +4 -2
- package/dist/tempo/session/ChannelStore.js.map +1 -1
- package/dist/tempo/session/Receipt.d.ts.map +1 -1
- package/dist/tempo/session/Receipt.js.map +1 -1
- package/dist/tempo/session/Sse.d.ts.map +1 -1
- package/dist/tempo/session/Sse.js.map +1 -1
- package/dist/tempo/session/Voucher.d.ts.map +1 -1
- package/dist/tempo/session/Voucher.js +3 -2
- package/dist/tempo/session/Voucher.js.map +1 -1
- package/dist/viem/Client.d.ts.map +1 -1
- package/dist/viem/Client.js.map +1 -1
- package/package.json +2 -2
- package/src/BodyDigest.ts +1 -0
- package/src/Challenge.test-d.ts +1 -0
- package/src/Challenge.ts +1 -0
- package/src/Credential.ts +1 -0
- package/src/Errors.test.ts +27 -39
- package/src/Expires.test.ts +1 -0
- package/src/PaymentRequest.ts +1 -0
- package/src/Receipt.ts +1 -0
- package/src/Store.test-d.ts +59 -0
- package/src/Store.test.ts +56 -6
- package/src/Store.ts +31 -4
- package/src/cli/account.ts +65 -30
- package/src/cli/cli.test.ts +127 -1
- package/src/cli/cli.ts +23 -8
- package/src/cli/config.test.ts +1 -0
- package/src/cli/internal.ts +1 -0
- package/src/cli/plugins/stripe.ts +1 -0
- package/src/cli/plugins/tempo.ts +21 -24
- package/src/cli/utils.ts +1 -0
- package/src/client/Mppx.test-d.ts +1 -0
- package/src/client/internal/Fetch.browser.test.ts +1 -0
- package/src/client/internal/Fetch.test-d.ts +1 -0
- package/src/client/internal/Fetch.test.ts +1 -0
- package/src/client/internal/Fetch.ts +1 -1
- package/src/internal/constantTimeEqual.test.ts +1 -0
- package/src/internal/types.ts +1 -3
- package/src/mcp-sdk/client/McpClient.test-d.ts +1 -0
- package/src/mcp-sdk/client/McpClient.test.ts +1 -0
- package/src/mcp-sdk/client/McpClient.ts +2 -0
- package/src/mcp-sdk/server/Transport.test.ts +1 -0
- package/src/mcp-sdk/server/Transport.ts +1 -0
- package/src/middlewares/elysia.test.ts +90 -0
- package/src/middlewares/elysia.ts +5 -1
- package/src/middlewares/express.test.ts +62 -2
- package/src/middlewares/express.ts +6 -2
- package/src/middlewares/hono.ts +1 -0
- package/src/middlewares/internal/mppx.test.ts +1 -0
- package/src/middlewares/nextjs.test.ts +1 -0
- package/src/proxy/Proxy.test.ts +57 -0
- package/src/proxy/Proxy.ts +8 -1
- package/src/proxy/Service.test.ts +1 -0
- package/src/proxy/Service.ts +8 -2
- package/src/proxy/internal/Headers.test.ts +1 -0
- package/src/proxy/internal/Route.test.ts +57 -0
- package/src/proxy/internal/Route.ts +3 -1
- package/src/proxy/services/openai.test.ts +1 -0
- package/src/server/Mppx.test.ts +438 -0
- package/src/server/Mppx.ts +51 -13
- package/src/server/Request.test.ts +1 -0
- package/src/server/Request.ts +1 -0
- package/src/server/Response.test.ts +1 -0
- package/src/server/Transport.test.ts +1 -0
- package/src/stripe/Methods.ts +1 -0
- package/src/stripe/client/Charge.test.ts +1 -0
- package/src/stripe/server/Charge.test.ts +1 -0
- package/src/tempo/Attribution.test.ts +1 -0
- package/src/tempo/Methods.ts +1 -0
- package/src/tempo/client/ChannelOps.test.ts +1 -0
- package/src/tempo/client/ChannelOps.ts +1 -0
- package/src/tempo/client/Charge.ts +1 -0
- package/src/tempo/client/Session.test.ts +1 -0
- package/src/tempo/client/Session.ts +1 -0
- package/src/tempo/client/SessionManager.test.ts +28 -0
- package/src/tempo/client/SessionManager.ts +2 -1
- package/src/tempo/internal/address.ts +6 -0
- package/src/tempo/internal/auto-swap.test.ts +1 -0
- package/src/tempo/internal/auto-swap.ts +4 -3
- package/src/tempo/internal/defaults.test.ts +1 -0
- package/src/tempo/internal/fee-payer.test.ts +1 -0
- package/src/tempo/internal/fee-payer.ts +19 -4
- package/src/tempo/server/Charge.test.ts +1081 -31
- package/src/tempo/server/Charge.ts +159 -63
- package/src/tempo/server/Session.test.ts +896 -107
- package/src/tempo/server/Session.ts +41 -23
- package/src/tempo/server/Sse.test.ts +2 -0
- package/src/tempo/server/internal/transport.test.ts +30 -0
- package/src/tempo/server/internal/transport.ts +41 -2
- package/src/tempo/session/Chain.test.ts +145 -0
- package/src/tempo/session/Chain.ts +59 -10
- package/src/tempo/session/Channel.test.ts +1 -0
- package/src/tempo/session/ChannelStore.test.ts +11 -0
- package/src/tempo/session/ChannelStore.ts +7 -3
- package/src/tempo/session/Receipt.test.ts +1 -0
- package/src/tempo/session/Receipt.ts +1 -0
- package/src/tempo/session/Sse.test.ts +2 -0
- package/src/tempo/session/Sse.ts +1 -0
- package/src/tempo/session/Voucher.test.ts +1 -0
- package/src/tempo/session/Voucher.ts +4 -2
- package/src/viem/Account.test.ts +1 -0
- package/src/viem/Client.test.ts +1 -0
- package/src/viem/Client.ts +1 -0
package/src/Errors.test.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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",
|
package/src/Expires.test.ts
CHANGED
package/src/PaymentRequest.ts
CHANGED
package/src/Receipt.ts
CHANGED
|
@@ -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()
|
|
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('
|
|
75
|
-
const store = Store.
|
|
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
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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({
|
package/src/cli/account.ts
CHANGED
|
@@ -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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
84
|
-
|
|
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
|
-
|
|
88
|
-
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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', [
|
|
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
|
-
|
|
118
|
-
|
|
152
|
+
await execCommand('security', ['delete-generic-password', '-s', service, '-a', account])
|
|
153
|
+
return
|
|
119
154
|
}
|
|
120
155
|
if (platform === 'linux') {
|
|
121
|
-
|
|
122
|
-
|
|
156
|
+
await execCommand('secret-tool', ['clear', 'service', service, 'account', account])
|
|
157
|
+
return
|
|
123
158
|
}
|
|
124
159
|
throw new Error(`Unsupported platform: ${platform}`)
|
|
125
160
|
},
|
package/src/cli/cli.test.ts
CHANGED
|
@@ -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
|
-
|
|
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)
|