mppx 0.4.9 → 0.4.10

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 (158) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/dist/cli/cli.d.ts.map +1 -1
  3. package/dist/cli/cli.js +155 -0
  4. package/dist/cli/cli.js.map +1 -1
  5. package/dist/discovery/Discovery.d.ts +146 -0
  6. package/dist/discovery/Discovery.d.ts.map +1 -0
  7. package/dist/discovery/Discovery.js +60 -0
  8. package/dist/discovery/Discovery.js.map +1 -0
  9. package/dist/discovery/OpenApi.d.ts +61 -0
  10. package/dist/discovery/OpenApi.d.ts.map +1 -0
  11. package/dist/discovery/OpenApi.js +139 -0
  12. package/dist/discovery/OpenApi.js.map +1 -0
  13. package/dist/discovery/Validate.d.ts +10 -0
  14. package/dist/discovery/Validate.d.ts.map +1 -0
  15. package/dist/discovery/Validate.js +63 -0
  16. package/dist/discovery/Validate.js.map +1 -0
  17. package/dist/discovery/index.d.ts +4 -0
  18. package/dist/discovery/index.d.ts.map +1 -0
  19. package/dist/discovery/index.js +4 -0
  20. package/dist/discovery/index.js.map +1 -0
  21. package/dist/middlewares/elysia.d.ts +52 -1
  22. package/dist/middlewares/elysia.d.ts.map +1 -1
  23. package/dist/middlewares/elysia.js +17 -0
  24. package/dist/middlewares/elysia.js.map +1 -1
  25. package/dist/middlewares/express.d.ts +13 -1
  26. package/dist/middlewares/express.d.ts.map +1 -1
  27. package/dist/middlewares/express.js +18 -0
  28. package/dist/middlewares/express.js.map +1 -1
  29. package/dist/middlewares/hono.d.ts +19 -1
  30. package/dist/middlewares/hono.d.ts.map +1 -1
  31. package/dist/middlewares/hono.js +51 -0
  32. package/dist/middlewares/hono.js.map +1 -1
  33. package/dist/middlewares/internal/mppx.d.ts +4 -2
  34. package/dist/middlewares/internal/mppx.d.ts.map +1 -1
  35. package/dist/middlewares/internal/mppx.js +10 -3
  36. package/dist/middlewares/internal/mppx.js.map +1 -1
  37. package/dist/middlewares/nextjs.d.ts +11 -0
  38. package/dist/middlewares/nextjs.d.ts.map +1 -1
  39. package/dist/middlewares/nextjs.js +15 -0
  40. package/dist/middlewares/nextjs.js.map +1 -1
  41. package/dist/proxy/Proxy.d.ts +6 -0
  42. package/dist/proxy/Proxy.d.ts.map +1 -1
  43. package/dist/proxy/Proxy.js +56 -80
  44. package/dist/proxy/Proxy.js.map +1 -1
  45. package/dist/proxy/Service.d.ts +16 -23
  46. package/dist/proxy/Service.d.ts.map +1 -1
  47. package/dist/proxy/Service.js +19 -83
  48. package/dist/proxy/Service.js.map +1 -1
  49. package/dist/proxy/internal/Route.js +1 -1
  50. package/dist/proxy/internal/Route.js.map +1 -1
  51. package/dist/proxy/services/anthropic.d.ts.map +1 -1
  52. package/dist/proxy/services/anthropic.js +5 -0
  53. package/dist/proxy/services/anthropic.js.map +1 -1
  54. package/dist/proxy/services/openai.d.ts.map +1 -1
  55. package/dist/proxy/services/openai.js +6 -3
  56. package/dist/proxy/services/openai.js.map +1 -1
  57. package/dist/proxy/services/stripe.d.ts.map +1 -1
  58. package/dist/proxy/services/stripe.js +6 -3
  59. package/dist/proxy/services/stripe.js.map +1 -1
  60. package/dist/tempo/server/Session.d.ts.map +1 -1
  61. package/dist/tempo/server/Session.js +18 -5
  62. package/dist/tempo/server/Session.js.map +1 -1
  63. package/dist/tempo/server/internal/transport.d.ts.map +1 -1
  64. package/dist/tempo/server/internal/transport.js +8 -0
  65. package/dist/tempo/server/internal/transport.js.map +1 -1
  66. package/dist/tempo/session/Chain.js +1 -1
  67. package/dist/tempo/session/Chain.js.map +1 -1
  68. package/package.json +6 -1
  69. package/src/BodyDigest.test.ts +1 -1
  70. package/src/Challenge.fuzz.test.ts +121 -0
  71. package/src/Challenge.test-d.ts +1 -1
  72. package/src/Challenge.test.ts +1 -1
  73. package/src/Credential.fuzz.test.ts +62 -0
  74. package/src/Credential.test.ts +1 -1
  75. package/src/Errors.test.ts +1 -1
  76. package/src/Expires.test.ts +1 -1
  77. package/src/Method.test.ts +1 -1
  78. package/src/PaymentRequest.test.ts +1 -1
  79. package/src/Receipt.test.ts +1 -1
  80. package/src/Store.test-d.ts +1 -1
  81. package/src/Store.test.ts +1 -1
  82. package/src/cli/cli.test.ts +212 -1
  83. package/src/cli/cli.ts +162 -0
  84. package/src/client/Mppx.test-d.ts +1 -1
  85. package/src/client/Mppx.test.ts +1 -1
  86. package/src/client/Transport.test.ts +1 -1
  87. package/src/client/internal/Fetch.browser.test.ts +1 -1
  88. package/src/client/internal/Fetch.test-d.ts +1 -1
  89. package/src/client/internal/Fetch.test.ts +2 -1
  90. package/src/discovery/Discovery.test.ts +152 -0
  91. package/src/discovery/Discovery.ts +72 -0
  92. package/src/discovery/OpenApi.test.ts +425 -0
  93. package/src/discovery/OpenApi.ts +224 -0
  94. package/src/discovery/Validate.test.ts +188 -0
  95. package/src/discovery/Validate.ts +76 -0
  96. package/src/discovery/index.ts +3 -0
  97. package/src/internal/constantTimeEqual.test.ts +1 -1
  98. package/src/mcp-sdk/client/McpClient.test-d.ts +1 -1
  99. package/src/mcp-sdk/client/McpClient.test.ts +1 -1
  100. package/src/mcp-sdk/server/Transport.test.ts +1 -1
  101. package/src/middlewares/elysia.test.ts +27 -2
  102. package/src/middlewares/elysia.ts +35 -1
  103. package/src/middlewares/express.test.ts +35 -7
  104. package/src/middlewares/express.ts +34 -0
  105. package/src/middlewares/hono.test.ts +28 -6
  106. package/src/middlewares/hono.ts +73 -1
  107. package/src/middlewares/internal/mppx.test.ts +1 -1
  108. package/src/middlewares/internal/mppx.ts +14 -6
  109. package/src/middlewares/nextjs.test.ts +31 -6
  110. package/src/middlewares/nextjs.ts +28 -0
  111. package/src/proxy/Proxy.test.ts +54 -270
  112. package/src/proxy/Proxy.ts +71 -93
  113. package/src/proxy/Service.test.ts +23 -1
  114. package/src/proxy/Service.ts +40 -86
  115. package/src/proxy/internal/Headers.test.ts +1 -1
  116. package/src/proxy/internal/Route.test.ts +9 -1
  117. package/src/proxy/internal/Route.ts +1 -1
  118. package/src/proxy/services/anthropic.test.ts +132 -0
  119. package/src/proxy/services/anthropic.ts +5 -0
  120. package/src/proxy/services/openai.test.ts +1 -1
  121. package/src/proxy/services/openai.ts +6 -4
  122. package/src/proxy/services/stripe.test.ts +132 -0
  123. package/src/proxy/services/stripe.ts +6 -4
  124. package/src/server/Mppx.test-d.ts +1 -1
  125. package/src/server/Mppx.test.ts +2 -1
  126. package/src/server/NodeListener.test.ts +1 -1
  127. package/src/server/Request.test.ts +1 -1
  128. package/src/server/Response.test.ts +1 -1
  129. package/src/server/Transport.test.ts +1 -1
  130. package/src/stripe/Charge.integration.test.ts +1 -1
  131. package/src/stripe/Methods.test.ts +1 -1
  132. package/src/stripe/client/Charge.test.ts +1 -1
  133. package/src/stripe/server/Charge.test.ts +1 -1
  134. package/src/tempo/Attribution.test.ts +1 -1
  135. package/src/tempo/Methods.test.ts +1 -1
  136. package/src/tempo/client/ChannelOps.test.ts +6 -3
  137. package/src/tempo/client/Session.test.ts +5 -2
  138. package/src/tempo/client/SessionManager.test.ts +1 -1
  139. package/src/tempo/internal/auto-swap.test.ts +1 -1
  140. package/src/tempo/internal/defaults.test.ts +1 -1
  141. package/src/tempo/internal/fee-payer.test.ts +1 -1
  142. package/src/tempo/server/Charge.test.ts +1 -1
  143. package/src/tempo/server/Session.test.ts +87 -37
  144. package/src/tempo/server/Session.ts +25 -8
  145. package/src/tempo/server/Sse.test.ts +1 -1
  146. package/src/tempo/server/internal/transport.test.ts +24 -1
  147. package/src/tempo/server/internal/transport.ts +11 -0
  148. package/src/tempo/session/Chain.test.ts +5 -2
  149. package/src/tempo/session/Chain.ts +1 -1
  150. package/src/tempo/session/Channel.test.ts +1 -1
  151. package/src/tempo/session/ChannelStore.test.ts +1 -1
  152. package/src/tempo/session/Receipt.test.ts +1 -1
  153. package/src/tempo/session/Sse.fuzz.test.ts +138 -0
  154. package/src/tempo/session/Sse.test.ts +1 -1
  155. package/src/tempo/session/Voucher.test.ts +1 -1
  156. package/src/viem/Account.test.ts +1 -1
  157. package/src/viem/Client.test.ts +1 -1
  158. package/src/zod.test.ts +147 -0
@@ -0,0 +1,132 @@
1
+ import { Receipt } from 'mppx'
2
+ import { Mppx as Mppx_client, tempo as tempo_client } from 'mppx/client'
3
+ import { Mppx as Mppx_server, tempo as tempo_server } from 'mppx/server'
4
+ import { afterEach, describe, expect, test } from 'vp/test'
5
+ import * as Http from '~test/Http.js'
6
+ import { accounts, asset, client } from '~test/tempo/viem.js'
7
+
8
+ import * as ApiProxy from '../Proxy.js'
9
+ import { stripe } from './stripe.js'
10
+
11
+ const apiKey = 'sk_test_fake_stripe_key'
12
+ const secretKey = 'test-secret-key'
13
+
14
+ const mppx_server = Mppx_server.create({
15
+ methods: [
16
+ tempo_server({
17
+ account: accounts[0],
18
+ currency: asset,
19
+ getClient: () => client,
20
+ }),
21
+ ],
22
+ secretKey,
23
+ })
24
+
25
+ const mppx_client = Mppx_client.create({
26
+ polyfill: false,
27
+ methods: [
28
+ tempo_client({
29
+ account: accounts[1],
30
+ getClient: () => client,
31
+ }),
32
+ ],
33
+ })
34
+
35
+ let proxyServer: Awaited<ReturnType<typeof Http.createServer>> | undefined
36
+ let upstreamServer: Awaited<ReturnType<typeof Http.createServer>> | undefined
37
+
38
+ afterEach(() => {
39
+ proxyServer?.close()
40
+ upstreamServer?.close()
41
+ })
42
+
43
+ describe('stripe', () => {
44
+ test('behavior: proxies POST /v1/charges with charge and injects Basic auth', async () => {
45
+ upstreamServer = await Http.createServer((req, res) => {
46
+ res.writeHead(200, { 'Content-Type': 'application/json' })
47
+ res.end(
48
+ JSON.stringify({
49
+ headers: {
50
+ authorization: req.headers.authorization,
51
+ },
52
+ }),
53
+ )
54
+ })
55
+
56
+ const proxy = ApiProxy.create({
57
+ services: [
58
+ stripe({
59
+ apiKey,
60
+ baseUrl: upstreamServer.url,
61
+ routes: {
62
+ 'POST /v1/charges': mppx_server.charge({ amount: '1', decimals: 6 }),
63
+ },
64
+ }),
65
+ ],
66
+ })
67
+ proxyServer = await Http.createServer(proxy.listener)
68
+
69
+ const res = await mppx_client.fetch(`${proxyServer.url}/stripe/v1/charges`, {
70
+ method: 'POST',
71
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
72
+ body: 'amount=100&currency=usd',
73
+ })
74
+ expect(res.status).toBe(200)
75
+
76
+ const body = (await res.json()) as { headers: { authorization: string } }
77
+ expect(body.headers.authorization).toBe(`Basic ${btoa(`${apiKey}:`)}`)
78
+
79
+ const receipt = Receipt.fromResponse(res)
80
+ expect(receipt.status).toBe('success')
81
+ expect(receipt.method).toBe('tempo')
82
+ })
83
+
84
+ test('behavior: returns 402 without credential', async () => {
85
+ upstreamServer = await Http.createServer((_req, res) => {
86
+ res.writeHead(200, { 'Content-Type': 'application/json' })
87
+ res.end('{}')
88
+ })
89
+
90
+ const proxy = ApiProxy.create({
91
+ services: [
92
+ stripe({
93
+ apiKey,
94
+ baseUrl: upstreamServer.url,
95
+ routes: {
96
+ 'POST /v1/charges': mppx_server.charge({ amount: '1', decimals: 6 }),
97
+ },
98
+ }),
99
+ ],
100
+ })
101
+ proxyServer = await Http.createServer(proxy.listener)
102
+
103
+ const res = await fetch(`${proxyServer.url}/stripe/v1/charges`, {
104
+ method: 'POST',
105
+ })
106
+ expect(res.status).toBe(402)
107
+ expect(res.headers.get('WWW-Authenticate')).toContain('Payment')
108
+ })
109
+
110
+ test('behavior: returns 404 for unmatched route', async () => {
111
+ upstreamServer = await Http.createServer((_req, res) => {
112
+ res.writeHead(200, { 'Content-Type': 'application/json' })
113
+ res.end('{}')
114
+ })
115
+
116
+ const proxy = ApiProxy.create({
117
+ services: [
118
+ stripe({
119
+ apiKey,
120
+ baseUrl: upstreamServer.url,
121
+ routes: {
122
+ 'POST /v1/charges': mppx_server.charge({ amount: '1', decimals: 6 }),
123
+ },
124
+ }),
125
+ ],
126
+ })
127
+ proxyServer = await Http.createServer(proxy.listener)
128
+
129
+ const res = await fetch(`${proxyServer.url}/stripe/v1/unknown`)
130
+ expect(res.status).toBe(404)
131
+ })
132
+ })
@@ -20,11 +20,13 @@ import * as Service from '../Service.js'
20
20
  export function stripe(config: stripe.Config) {
21
21
  return Service.from<stripe.Config>('stripe', {
22
22
  baseUrl: config.baseUrl ?? 'https://api.stripe.com',
23
+ categories: ['payments'],
23
24
  description: 'Payment processing, customers, subscriptions, and invoices.',
24
- docsLlmsUrl: ({ route }) =>
25
- route
26
- ? `https://context7.com/websites/stripe/llms.txt?topic=${encodeURIComponent(route)}`
27
- : 'https://docs.stripe.com/llms.txt',
25
+ docs: {
26
+ apiReference: 'https://docs.stripe.com/api',
27
+ homepage: 'https://docs.stripe.com',
28
+ llms: 'https://docs.stripe.com/llms.txt',
29
+ },
28
30
  rewriteRequest(request, ctx) {
29
31
  const apiKey = ctx.apiKey ?? config.apiKey
30
32
  request.headers.set('Authorization', `Basic ${btoa(`${apiKey}:`)}`)
@@ -1,6 +1,6 @@
1
1
  import { Method, z } from 'mppx'
2
2
  import { Mppx } from 'mppx/server'
3
- import { assertType, describe, expectTypeOf, test } from 'vitest'
3
+ import { assertType, describe, expectTypeOf, test } from 'vp/test'
4
4
 
5
5
  const mockChargeA = Method.from({
6
6
  name: 'alpha',
@@ -1,6 +1,6 @@
1
1
  import { Challenge, Credential, Method, z } from 'mppx'
2
2
  import { Mppx, Transport, tempo } from 'mppx/server'
3
- import { describe, expect, test } from 'vitest'
3
+ import { describe, expect, test } from 'vp/test'
4
4
  import * as Http from '~test/Http.js'
5
5
  import { accounts, asset, client } from '~test/tempo/viem.js'
6
6
 
@@ -9,6 +9,7 @@ const secretKey = 'test-secret-key'
9
9
 
10
10
  const method = tempo({
11
11
  getClient: () => client,
12
+ account: accounts[0],
12
13
  })
13
14
 
14
15
  describe('create', () => {
@@ -1,5 +1,5 @@
1
1
  import { NodeListener, Request } from 'mppx/server'
2
- import { afterEach, describe, expect, test } from 'vitest'
2
+ import { afterEach, describe, expect, test } from 'vp/test'
3
3
  import * as Http from '~test/Http.js'
4
4
 
5
5
  let server: Awaited<ReturnType<typeof Http.createServer>> | undefined
@@ -2,7 +2,7 @@ import { EventEmitter } from 'node:events'
2
2
  import type { IncomingMessage, ServerResponse } from 'node:http'
3
3
 
4
4
  import { Request } from 'mppx/server'
5
- import { describe, expect, test } from 'vitest'
5
+ import { describe, expect, test } from 'vp/test'
6
6
 
7
7
  function createMockRequest(options: {
8
8
  method?: string
@@ -1,6 +1,6 @@
1
1
  import { Challenge } from 'mppx'
2
2
  import { Response } from 'mppx/server'
3
- import { describe, expect, test } from 'vitest'
3
+ import { describe, expect, test } from 'vp/test'
4
4
 
5
5
  import * as Errors from '../Errors.js'
6
6
 
@@ -1,7 +1,7 @@
1
1
  import { Challenge, Credential, Mcp, Receipt } from 'mppx'
2
2
  import { Transport } from 'mppx/server'
3
3
  import { Methods } from 'mppx/tempo'
4
- import { describe, expect, test } from 'vitest'
4
+ import { describe, expect, test } from 'vp/test'
5
5
 
6
6
  import { BadRequestError, ChannelClosedError } from '../Errors.js'
7
7
 
@@ -1,7 +1,7 @@
1
1
  import { Challenge, Credential, Receipt } from 'mppx'
2
2
  import { Mppx as Mppx_client, stripe as stripe_client } from 'mppx/client'
3
3
  import { Mppx as Mppx_server, stripe as stripe_server } from 'mppx/server'
4
- import { afterEach, describe, expect, test } from 'vitest'
4
+ import { afterEach, describe, expect, test } from 'vp/test'
5
5
  import * as Http from '~test/Http.js'
6
6
 
7
7
  const stripeSecretKey = process.env.VITE_STRIPE_SECRET_KEY
@@ -1,5 +1,5 @@
1
1
  import { Methods } from 'mppx/stripe'
2
- import { describe, expect, expectTypeOf, test } from 'vitest'
2
+ import { describe, expect, expectTypeOf, test } from 'vp/test'
3
3
 
4
4
  describe('charge', () => {
5
5
  test('has correct name and intent', () => {
@@ -1,7 +1,7 @@
1
1
  import { Challenge, Credential } from 'mppx'
2
2
  import { Mppx, stripe } from 'mppx/client'
3
3
  import { Mppx as Mppx_server, stripe as stripe_server } from 'mppx/server'
4
- import { describe, expect, test, vi } from 'vitest'
4
+ import { describe, expect, test, vi } from 'vp/test'
5
5
 
6
6
  import type { StripeJs } from '../internal/types.js'
7
7
  import { charge as clientCharge_ } from './Charge.js'
@@ -1,6 +1,6 @@
1
1
  import { Challenge, Credential } from 'mppx'
2
2
  import { Mppx, stripe } from 'mppx/server'
3
- import { afterEach, describe, expect, test, vi } from 'vitest'
3
+ import { afterEach, describe, expect, test, vi } from 'vp/test'
4
4
  import * as Http from '~test/Http.js'
5
5
 
6
6
  import type { StripeClient } from '../internal/types.js'
@@ -1,5 +1,5 @@
1
1
  import { Bytes, Hash, Hex } from 'ox'
2
- import { describe, expect, test } from 'vitest'
2
+ import { describe, expect, test } from 'vp/test'
3
3
 
4
4
  import * as Attribution from './Attribution.js'
5
5
 
@@ -1,5 +1,5 @@
1
1
  import { Methods } from 'mppx/tempo'
2
- import { describe, expect, expectTypeOf, test } from 'vitest'
2
+ import { describe, expect, expectTypeOf, test } from 'vp/test'
3
3
 
4
4
  describe('charge', () => {
5
5
  test('has correct name and intent', () => {
@@ -2,10 +2,13 @@ import { Hex } from 'ox'
2
2
  import { type Address, createClient } from 'viem'
3
3
  import { privateKeyToAccount } from 'viem/accounts'
4
4
  import { Addresses } from 'viem/tempo'
5
- import { beforeAll, describe, expect, test } from 'vitest'
5
+ import { beforeAll, describe, expect, test } from 'vp/test'
6
+ import { nodeEnv } from '~test/config.js'
6
7
  import { deployEscrow, openChannel } from '~test/tempo/session.js'
7
8
  import { accounts, asset, chain, client, fundAccount, http } from '~test/tempo/viem.js'
8
9
 
10
+ const isLocalnet = nodeEnv === 'localnet'
11
+
9
12
  import type { Challenge } from '../../Challenge.js'
10
13
  import * as Credential from '../../Credential.js'
11
14
  import {
@@ -166,7 +169,7 @@ describe('createClosePayload', () => {
166
169
  })
167
170
  })
168
171
 
169
- describe('createOpenPayload', () => {
172
+ describe.runIf(isLocalnet)('createOpenPayload', () => {
170
173
  const payer = accounts[2]
171
174
  const payee = accounts[1].address
172
175
  const currency = asset
@@ -250,7 +253,7 @@ describe('createOpenPayload', () => {
250
253
  })
251
254
  })
252
255
 
253
- describe('tryRecoverChannel', () => {
256
+ describe.runIf(isLocalnet)('tryRecoverChannel', () => {
254
257
  const payer = accounts[3]
255
258
  const payee = accounts[1].address
256
259
  const currency = asset
@@ -1,10 +1,13 @@
1
1
  import { type Address, createClient, type Hex, http } from 'viem'
2
2
  import { privateKeyToAccount } from 'viem/accounts'
3
3
  import { Addresses } from 'viem/tempo'
4
- import { beforeAll, describe, expect, test } from 'vitest'
4
+ import { beforeAll, describe, expect, test } from 'vp/test'
5
+ import { nodeEnv } from '~test/config.js'
5
6
  import { deployEscrow, openChannel } from '~test/tempo/session.js'
6
7
  import { accounts, asset, chain, client, fundAccount } from '~test/tempo/viem.js'
7
8
 
9
+ const isLocalnet = nodeEnv === 'localnet'
10
+
8
11
  import * as Challenge from '../../Challenge.js'
9
12
  import * as Credential from '../../Credential.js'
10
13
  import { chainId, escrowContract as escrowContractDefaults } from '../internal/defaults.js'
@@ -201,7 +204,7 @@ describe('session (pure)', () => {
201
204
  })
202
205
  })
203
206
 
204
- describe('session (on-chain)', () => {
207
+ describe.runIf(isLocalnet)('session (on-chain)', () => {
205
208
  const payer = accounts[2]
206
209
  const payee = accounts[1].address
207
210
  let escrowContract: Address
@@ -1,5 +1,5 @@
1
1
  import type { Hex } from 'viem'
2
- import { describe, expect, test, vi } from 'vitest'
2
+ import { describe, expect, test, vi } from 'vp/test'
3
3
 
4
4
  import * as Challenge from '../../Challenge.js'
5
5
  import { formatNeedVoucherEvent, parseEvent } from '../session/Sse.js'
@@ -1,5 +1,5 @@
1
1
  import type { Address } from 'viem'
2
- import { describe, expect, test } from 'vitest'
2
+ import { describe, expect, test } from 'vp/test'
3
3
 
4
4
  import { defaultCurrencies, InsufficientFundsError, resolve } from './auto-swap.js'
5
5
 
@@ -1,4 +1,4 @@
1
- import { describe, expect, test } from 'vitest'
1
+ import { describe, expect, test } from 'vp/test'
2
2
 
3
3
  import {
4
4
  chainId,
@@ -1,6 +1,6 @@
1
1
  import { encodeFunctionData } from 'viem'
2
2
  import { Abis, Addresses } from 'viem/tempo'
3
- import { describe, expect, test } from 'vitest'
3
+ import { describe, expect, test } from 'vp/test'
4
4
 
5
5
  import { callScopes, FeePayerValidationError, validateCalls } from './fee-payer.js'
6
6
  import * as Selectors from './selectors.js'
@@ -7,7 +7,7 @@ import { Handler } from 'tempo.ts/server'
7
7
  import { createClient, custom, encodeFunctionData, parseUnits } from 'viem'
8
8
  import { getTransactionReceipt, prepareTransactionRequest, signTransaction } from 'viem/actions'
9
9
  import { Abis, Account, Actions, Addresses, Secp256k1, Tick, Transaction } from 'viem/tempo'
10
- import { beforeAll, describe, expect, test } from 'vitest'
10
+ import { beforeAll, describe, expect, test } from 'vp/test'
11
11
  import * as Http from '~test/Http.js'
12
12
  import { closeChannelOnChain, deployEscrow, openChannel } from '~test/tempo/session.js'
13
13
  import { accounts, asset, chain, client, fundAccount } from '~test/tempo/viem.js'
@@ -4,7 +4,10 @@ import { Mppx as Mppx_server, tempo as tempo_server } from 'mppx/server'
4
4
  import { type Address, createClient, type Hex } from 'viem'
5
5
  import { waitForTransactionReceipt } from 'viem/actions'
6
6
  import { Addresses } from 'viem/tempo'
7
- import { beforeAll, beforeEach, describe, expect, test } from 'vitest'
7
+ import { beforeAll, beforeEach, describe, expect, expectTypeOf, test } from 'vp/test'
8
+ import { nodeEnv } from '~test/config.js'
9
+
10
+ const isLocalnet = nodeEnv === 'localnet'
8
11
  import {
9
12
  deployEscrow,
10
13
  requestCloseChannel,
@@ -19,7 +22,6 @@ import {
19
22
  ChannelNotFoundError,
20
23
  InsufficientBalanceError,
21
24
  InvalidSignatureError,
22
- VerificationFailedError,
23
25
  } from '../../Errors.js'
24
26
  import * as Store from '../../Store.js'
25
27
  import {
@@ -33,6 +35,7 @@ import { signVoucher } from '../session/Voucher.js'
33
35
  import { charge, session, settle } from './Session.js'
34
36
 
35
37
  const payer = accounts[2]
38
+ const recipientAccount = accounts[0]
36
39
  const recipient = accounts[0].address
37
40
  const currency = asset
38
41
 
@@ -40,12 +43,13 @@ let escrowContract: Address
40
43
  let saltCounter = 0
41
44
 
42
45
  beforeAll(async () => {
46
+ if (!isLocalnet) return
43
47
  escrowContract = await deployEscrow()
44
48
  await fundAccount({ address: payer.address, token: Addresses.pathUsd })
45
49
  await fundAccount({ address: payer.address, token: currency })
46
50
  })
47
51
 
48
- describe('session', () => {
52
+ describe.runIf(isLocalnet)('session', () => {
49
53
  let rawStore: Store.Store
50
54
  let store: ChannelStore.ChannelStore
51
55
 
@@ -58,7 +62,7 @@ describe('session', () => {
58
62
  return session({
59
63
  store: rawStore,
60
64
  getClient: () => client,
61
- account: recipient,
65
+ account: recipientAccount,
62
66
  currency,
63
67
  escrowContract,
64
68
  chainId: chain.id,
@@ -618,7 +622,47 @@ describe('session', () => {
618
622
  ).rejects.toThrow(InvalidSignatureError)
619
623
  })
620
624
 
621
- test('rejects exact replay of already-verified voucher (non-increasing)', async () => {
625
+ test('accepts exact replay of already-verified voucher as idempotent', async () => {
626
+ const { channelId, serializedTransaction } = await createSignedOpenTransaction(10000000n)
627
+ const server = createServer()
628
+ await openServerChannel(server, channelId, serializedTransaction)
629
+
630
+ const payload = {
631
+ action: 'voucher' as const,
632
+ channelId,
633
+ cumulativeAmount: '2000000',
634
+ signature: await signTestVoucher(channelId, 2000000n),
635
+ }
636
+
637
+ await server.verify({
638
+ credential: {
639
+ challenge: makeChallenge({ id: 'challenge-2', channelId }),
640
+ payload,
641
+ },
642
+ request: makeRequest(),
643
+ })
644
+
645
+ const channelAfterFirstAccept = await store.getChannel(channelId)
646
+
647
+ const replayReceipt = (await server.verify({
648
+ credential: {
649
+ challenge: makeChallenge({ id: 'challenge-3', channelId }),
650
+ payload,
651
+ },
652
+ request: makeRequest(),
653
+ })) as SessionReceipt
654
+
655
+ expect(replayReceipt.status).toBe('success')
656
+ expect(replayReceipt.acceptedCumulative).toBe('2000000')
657
+ expect(replayReceipt.spent).toBe(channelAfterFirstAccept!.spent.toString())
658
+ expect(replayReceipt.units).toBe(channelAfterFirstAccept!.units)
659
+
660
+ const channelAfterReplay = await store.getChannel(channelId)
661
+ expect(channelAfterReplay).toEqual(channelAfterFirstAccept)
662
+ expect(channelAfterReplay!.highestVoucherAmount).toBe(2000000n)
663
+ })
664
+
665
+ test('rejects exact replay with invalid signature', async () => {
622
666
  const { channelId, serializedTransaction } = await createSignedOpenTransaction(10000000n)
623
667
  const server = createServer()
624
668
  await openServerChannel(server, channelId, serializedTransaction)
@@ -642,11 +686,14 @@ describe('session', () => {
642
686
  server.verify({
643
687
  credential: {
644
688
  challenge: makeChallenge({ id: 'challenge-3', channelId }),
645
- payload,
689
+ payload: {
690
+ ...payload,
691
+ signature: `0x${'ab'.repeat(65)}` as Hex,
692
+ },
646
693
  },
647
694
  request: makeRequest(),
648
695
  }),
649
- ).rejects.toThrow(VerificationFailedError)
696
+ ).rejects.toThrow(InvalidSignatureError)
650
697
  })
651
698
 
652
699
  test('rejects replayed voucher at settled amount after on-chain settlement', async () => {
@@ -1191,27 +1238,29 @@ describe('session', () => {
1191
1238
  expect(ch!.finalized).toBe(true)
1192
1239
  })
1193
1240
 
1194
- test('close throws when client has no account', async () => {
1195
- const { channelId, serializedTransaction } = await createSignedOpenTransaction(10000000n)
1196
- const server = createServer({
1197
- getClient: () => createClient({ chain, transport: http() }),
1198
- })
1199
- await openServerChannel(server, channelId, serializedTransaction)
1200
-
1201
- await expect(
1202
- server.verify({
1203
- credential: {
1204
- challenge: makeChallenge({ id: 'challenge-2', channelId }),
1205
- payload: {
1206
- action: 'close' as const,
1207
- channelId,
1208
- cumulativeAmount: '1000000',
1209
- signature: await signTestVoucher(channelId, 1000000n),
1210
- },
1211
- },
1212
- request: makeRequest(),
1213
- }),
1214
- ).rejects.toThrow('Cannot close channel: no account available')
1241
+ test('session() throws at initialization when no account provided', () => {
1242
+ expect(() =>
1243
+ session({
1244
+ store: rawStore,
1245
+ getClient: () => client,
1246
+ account: recipient as Address,
1247
+ currency,
1248
+ escrowContract,
1249
+ chainId: chain.id,
1250
+ } as session.Parameters),
1251
+ ).toThrow('tempo.session() requires an `account`')
1252
+ })
1253
+
1254
+ test('session() throws at initialization with no account at all', () => {
1255
+ expect(() =>
1256
+ session({
1257
+ store: rawStore,
1258
+ getClient: () => client,
1259
+ currency,
1260
+ escrowContract,
1261
+ chainId: chain.id,
1262
+ } as session.Parameters),
1263
+ ).toThrow('tempo.session() requires an `account`')
1215
1264
  })
1216
1265
  })
1217
1266
 
@@ -2208,6 +2257,7 @@ describe('monotonicity and TOCTOU (unit tests)', () => {
2208
2257
  })
2209
2258
 
2210
2259
  describe('session default currency resolution', () => {
2260
+ const mockAccount = accounts[0]
2211
2261
  const mockClient = createClient({ transport: http('http://localhost:1') })
2212
2262
  const mockMainnetClient = createClient({
2213
2263
  chain: {
@@ -2232,7 +2282,7 @@ describe('session default currency resolution', () => {
2232
2282
  const server = session({
2233
2283
  store: Store.memory(),
2234
2284
  getClient: () => mockClient,
2235
- account: '0x0000000000000000000000000000000000000001',
2285
+ account: mockAccount,
2236
2286
  escrowContract: '0x0000000000000000000000000000000000000002',
2237
2287
  } as session.Parameters)
2238
2288
  expect(server.defaults?.currency).toBe('0x20C000000000000000000000b9537d11c60E8b50')
@@ -2242,7 +2292,7 @@ describe('session default currency resolution', () => {
2242
2292
  const server = session({
2243
2293
  store: Store.memory(),
2244
2294
  getClient: () => mockClient,
2245
- account: '0x0000000000000000000000000000000000000001',
2295
+ account: mockAccount,
2246
2296
  escrowContract: '0x0000000000000000000000000000000000000002',
2247
2297
  testnet: true,
2248
2298
  } as session.Parameters)
@@ -2253,7 +2303,7 @@ describe('session default currency resolution', () => {
2253
2303
  const server = session({
2254
2304
  store: Store.memory(),
2255
2305
  getClient: () => mockClient,
2256
- account: '0x0000000000000000000000000000000000000001',
2306
+ account: mockAccount,
2257
2307
  escrowContract: '0x0000000000000000000000000000000000000002',
2258
2308
  chainId: 69420,
2259
2309
  } as session.Parameters)
@@ -2264,7 +2314,7 @@ describe('session default currency resolution', () => {
2264
2314
  const server = session({
2265
2315
  store: Store.memory(),
2266
2316
  getClient: () => mockClient,
2267
- account: '0x0000000000000000000000000000000000000001',
2317
+ account: mockAccount,
2268
2318
  currency: '0xcustom',
2269
2319
  escrowContract: '0x0000000000000000000000000000000000000002',
2270
2320
  chainId: 4217,
@@ -2277,7 +2327,7 @@ describe('session default currency resolution', () => {
2277
2327
  const server = session({
2278
2328
  store: Store.memory(),
2279
2329
  getClient: () => mockClient,
2280
- account: '0x0000000000000000000000000000000000000001',
2330
+ account: mockAccount,
2281
2331
  escrowContract: '0x0000000000000000000000000000000000000002',
2282
2332
  chainId: 42431,
2283
2333
  } as session.Parameters)
@@ -2290,7 +2340,7 @@ describe('session default currency resolution', () => {
2290
2340
  tempo_server.session({
2291
2341
  store: Store.memory(),
2292
2342
  getClient: () => mockMainnetClient,
2293
- account: '0x0000000000000000000000000000000000000001',
2343
+ account: mockAccount,
2294
2344
  escrowContract: '0x0000000000000000000000000000000000000002',
2295
2345
  chainId: 4217,
2296
2346
  testnet: false,
@@ -2317,7 +2367,7 @@ describe('session default currency resolution', () => {
2317
2367
  tempo_server.session({
2318
2368
  store: Store.memory(),
2319
2369
  getClient: () => mockTestnetClient,
2320
- account: '0x0000000000000000000000000000000000000001',
2370
+ account: mockAccount,
2321
2371
  escrowContract: '0x0000000000000000000000000000000000000002',
2322
2372
  testnet: true,
2323
2373
  }),
@@ -2344,7 +2394,7 @@ describe('session default currency resolution', () => {
2344
2394
  tempo_server.session({
2345
2395
  store: Store.memory(),
2346
2396
  getClient: () => mockTestnetClient,
2347
- account: '0x0000000000000000000000000000000000000001',
2397
+ account: mockAccount,
2348
2398
  escrowContract: '0x0000000000000000000000000000000000000002',
2349
2399
  chainId: 69420,
2350
2400
  }),
@@ -2370,7 +2420,7 @@ describe('session default currency resolution', () => {
2370
2420
  tempo_server.session({
2371
2421
  store: Store.memory(),
2372
2422
  getClient: () => mockClient,
2373
- account: '0x0000000000000000000000000000000000000001',
2423
+ account: mockAccount,
2374
2424
  currency: '0xcustom',
2375
2425
  escrowContract: '0x0000000000000000000000000000000000000002',
2376
2426
  chainId: 4217,