mppx 0.3.15 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/README.md +1 -0
  2. package/dist/Challenge.d.ts +38 -0
  3. package/dist/Challenge.d.ts.map +1 -1
  4. package/dist/Challenge.js +62 -0
  5. package/dist/Challenge.js.map +1 -1
  6. package/dist/client/internal/Fetch.d.ts.map +1 -1
  7. package/dist/client/internal/Fetch.js +16 -4
  8. package/dist/client/internal/Fetch.js.map +1 -1
  9. package/dist/middlewares/internal/mppx.d.ts +6 -1
  10. package/dist/middlewares/internal/mppx.d.ts.map +1 -1
  11. package/dist/middlewares/internal/mppx.js +6 -1
  12. package/dist/middlewares/internal/mppx.js.map +1 -1
  13. package/dist/proxy/Service.js +1 -1
  14. package/dist/proxy/Service.js.map +1 -1
  15. package/dist/server/Mppx.d.ts +92 -2
  16. package/dist/server/Mppx.d.ts.map +1 -1
  17. package/dist/server/Mppx.js +155 -10
  18. package/dist/server/Mppx.js.map +1 -1
  19. package/dist/tempo/client/ChannelOps.d.ts.map +1 -1
  20. package/dist/tempo/client/ChannelOps.js +1 -0
  21. package/dist/tempo/client/ChannelOps.js.map +1 -1
  22. package/dist/tempo/server/Charge.d.ts.map +1 -1
  23. package/dist/tempo/server/Charge.js +6 -4
  24. package/dist/tempo/server/Charge.js.map +1 -1
  25. package/dist/tempo/session/Chain.d.ts.map +1 -1
  26. package/dist/tempo/session/Chain.js +13 -6
  27. package/dist/tempo/session/Chain.js.map +1 -1
  28. package/package.json +1 -1
  29. package/src/Challenge.ts +72 -0
  30. package/src/client/internal/Fetch.test.ts +1 -1
  31. package/src/client/internal/Fetch.ts +18 -6
  32. package/src/middlewares/internal/mppx.test.ts +152 -0
  33. package/src/middlewares/internal/mppx.ts +27 -4
  34. package/src/proxy/Service.ts +2 -1
  35. package/src/server/Mppx.test-d.ts +94 -299
  36. package/src/server/Mppx.test.ts +694 -0
  37. package/src/server/Mppx.ts +256 -14
  38. package/src/tempo/client/ChannelOps.ts +1 -0
  39. package/src/tempo/server/Charge.ts +9 -4
  40. package/src/tempo/session/Chain.ts +15 -5
  41. package/dist/tempo/internal/simulate.d.ts +0 -21
  42. package/dist/tempo/internal/simulate.d.ts.map +0 -1
  43. package/dist/tempo/internal/simulate.js +0 -31
  44. package/dist/tempo/internal/simulate.js.map +0 -1
  45. package/src/tempo/internal/simulate.ts +0 -49
@@ -0,0 +1,152 @@
1
+ import { Challenge, Credential, Method, z } from 'mppx'
2
+ import { Mppx } from 'mppx/server'
3
+ import { describe, expect, test } from 'vitest'
4
+ import { wrap } from './mppx.js'
5
+
6
+ const realm = 'api.example.com'
7
+ const secretKey = 'test-secret-key'
8
+
9
+ const mockChargeA = Method.from({
10
+ name: 'alpha',
11
+ intent: 'charge',
12
+ schema: {
13
+ credential: {
14
+ payload: z.object({ token: z.string() }),
15
+ },
16
+ request: z.object({
17
+ amount: z.string(),
18
+ currency: z.string(),
19
+ decimals: z.number(),
20
+ recipient: z.string(),
21
+ }),
22
+ },
23
+ })
24
+
25
+ const mockChargeB = Method.from({
26
+ name: 'beta',
27
+ intent: 'charge',
28
+ schema: {
29
+ credential: {
30
+ payload: z.object({ token: z.string() }),
31
+ },
32
+ request: z.object({
33
+ amount: z.string(),
34
+ currency: z.string(),
35
+ decimals: z.number(),
36
+ recipient: z.string(),
37
+ }),
38
+ },
39
+ })
40
+
41
+ function mockReceipt(name: string) {
42
+ return {
43
+ method: name,
44
+ reference: `tx-${name}`,
45
+ status: 'success' as const,
46
+ timestamp: new Date().toISOString(),
47
+ }
48
+ }
49
+
50
+ const alphaMethod = Method.toServer(mockChargeA, {
51
+ async verify() {
52
+ return mockReceipt('alpha')
53
+ },
54
+ })
55
+
56
+ const betaMethod = Method.toServer(mockChargeB, {
57
+ async verify() {
58
+ return mockReceipt('beta')
59
+ },
60
+ })
61
+
62
+ const challengeOpts = {
63
+ amount: '1000',
64
+ currency: '0x0000000000000000000000000000000000000001',
65
+ decimals: 6,
66
+ expires: new Date(Date.now() + 60_000).toISOString(),
67
+ recipient: '0x0000000000000000000000000000000000000002',
68
+ }
69
+
70
+ describe('wrap: nested handlers', () => {
71
+ test('wrapped.alpha.charge produces a wrapped handler', () => {
72
+ const mppx = Mppx.create({ methods: [alphaMethod, betaMethod], realm, secretKey }) as any
73
+
74
+ const wrapped = wrap(mppx, (methodFn, options) => {
75
+ return { type: 'wrapped' as const, handler: methodFn(options) }
76
+ })
77
+
78
+ const result = wrapped.alpha.charge(challengeOpts)
79
+ expect(result.type).toBe('wrapped')
80
+ expect(typeof result.handler).toBe('function')
81
+ })
82
+
83
+ test('wrapped.beta.charge produces a wrapped handler', () => {
84
+ const mppx = Mppx.create({ methods: [alphaMethod, betaMethod], realm, secretKey }) as any
85
+
86
+ const wrapped = wrap(mppx, (methodFn, options) => {
87
+ return { type: 'wrapped' as const, handler: methodFn(options) }
88
+ })
89
+
90
+ const result = wrapped.beta.charge(challengeOpts)
91
+ expect(result.type).toBe('wrapped')
92
+ })
93
+
94
+ test('nested wrapped handler works end-to-end (402 then 200)', async () => {
95
+ const mppx = Mppx.create({ methods: [alphaMethod], realm, secretKey }) as any
96
+
97
+ const wrapped = wrap(mppx, (methodFn, options) => methodFn(options))
98
+
99
+ const handle = wrapped.alpha.charge(challengeOpts)
100
+
101
+ const firstResult = await handle(new Request('https://example.com/resource'))
102
+ expect(firstResult.status).toBe(402)
103
+ if (firstResult.status !== 402) throw new Error()
104
+
105
+ const challenge = Challenge.fromResponse(firstResult.challenge)
106
+ const credential = Credential.from({ challenge, payload: { token: 'valid' } })
107
+
108
+ const result = await handle(
109
+ new Request('https://example.com/resource', {
110
+ headers: { Authorization: Credential.serialize(credential) },
111
+ }),
112
+ )
113
+ expect(result.status).toBe(200)
114
+ })
115
+
116
+ test('slash key and nested key produce equivalent wrapped handlers', () => {
117
+ const mppx = Mppx.create({ methods: [alphaMethod], realm, secretKey }) as any
118
+
119
+ const wrapped = wrap(mppx, (methodFn, options) => {
120
+ return { methodFn, options }
121
+ })
122
+
123
+ const nestedResult = wrapped.alpha.charge(challengeOpts) as {
124
+ methodFn: unknown
125
+ options: unknown
126
+ }
127
+ const slashResult = wrapped['alpha/charge'](challengeOpts) as {
128
+ methodFn: unknown
129
+ options: unknown
130
+ }
131
+
132
+ expect(nestedResult.methodFn).toBe(slashResult.methodFn)
133
+ expect(nestedResult.options).toEqual(slashResult.options)
134
+ })
135
+
136
+ test('compose is omitted from wrapped object', () => {
137
+ const mppx = Mppx.create({ methods: [alphaMethod], realm, secretKey }) as any
138
+
139
+ const wrapped = wrap(mppx, (_methodFn, _options) => 'wrapped')
140
+
141
+ expect(wrapped.compose).toBeUndefined()
142
+ })
143
+
144
+ test('realm and transport are passed through', () => {
145
+ const mppx = Mppx.create({ methods: [alphaMethod], realm, secretKey }) as any
146
+
147
+ const wrapped = wrap(mppx, (_methodFn, _options) => 'wrapped')
148
+
149
+ expect(wrapped.realm).toBe(realm)
150
+ expect(wrapped.transport).toBe(mppx.transport)
151
+ })
152
+ })
@@ -4,10 +4,27 @@ import type * as Mppx from '../../server/Mppx.js'
4
4
  export type AnyMethodFn = Mppx.AnyMethodFn
5
5
  export type AnyServer = Method.AnyServer
6
6
 
7
- export type Wrap<mppx, handler> = {
8
- [key in keyof mppx]: mppx[key] extends (options: infer options) => any
7
+ /** Recursively wraps nested handler objects one level deep. */
8
+ type WrapNested<obj, handler> = {
9
+ [key in keyof obj]: obj[key] extends (options: infer options) => any
9
10
  ? (o: options) => handler
10
- : mppx[key]
11
+ : obj[key]
12
+ }
13
+
14
+ export type Wrap<mppx, handler> = {
15
+ // `compose` is omitted — it returns a raw HTTP handler, not a
16
+ // middleware-shaped result. Use `Mppx.compose()` static instead.
17
+ // `methods`, `realm`, `transport` are data properties — not handlers.
18
+ [key in keyof mppx as key extends 'compose' ? never : key]: key extends
19
+ | 'methods'
20
+ | 'realm'
21
+ | 'transport'
22
+ ? mppx[key]
23
+ : mppx[key] extends (options: infer options) => any
24
+ ? (o: options) => handler
25
+ : mppx[key] extends Record<string, (options: any) => any>
26
+ ? WrapNested<mppx[key], handler>
27
+ : mppx[key]
11
28
  }
12
29
 
13
30
  /**
@@ -21,13 +38,19 @@ export function wrap<mppx extends Mppx.Mppx<any, any>, handler>(
21
38
  mppx: mppx,
22
39
  wrapper: (method: AnyMethodFn, options: any) => handler,
23
40
  ): Wrap<mppx, handler> {
24
- const result: Record<string, unknown> = { ...mppx }
41
+ const { compose: _, ...rest } = mppx as any
42
+ const result: Record<string, unknown> = { ...rest }
25
43
  for (const mi of mppx.methods as readonly Method.AnyServer[]) {
26
44
  const key = `${mi.name}/${mi.intent}`
27
45
  const methodFn = (mppx as any)[key]
28
46
  result[key] = (options: any) => wrapper(methodFn, options)
29
47
  // Also set shorthand intent key if Mppx registered it (no collision)
30
48
  if ((mppx as any)[mi.intent]) result[mi.intent] = (options: any) => wrapper(methodFn, options)
49
+ // Build nested handlers: wrapped.tempo.charge(...)
50
+ if (!result[mi.name] || typeof result[mi.name] !== 'object')
51
+ result[mi.name] = {} as Record<string, unknown>
52
+ ;(result[mi.name] as Record<string, unknown>)[mi.intent] = (options: any) =>
53
+ wrapper(methodFn, options)
31
54
  }
32
55
  return result as never
33
56
  }
@@ -267,7 +267,8 @@ function resolvePayment(endpoint: Endpoint): Record<string, unknown> | null {
267
267
  if (endpoint === true) return null
268
268
  const handler = typeof endpoint === 'function' ? endpoint : endpoint.pay
269
269
  if (!('_internal' in handler)) return {}
270
- const { name, intent, defaults, schema, ...rest } = handler._internal as Record<string, unknown>
270
+ const { name, intent, defaults, schema, _canonicalRequest, ...rest } =
271
+ handler._internal as Record<string, unknown>
271
272
  const amount = (() => {
272
273
  if (typeof rest.amount === 'string' && typeof rest.decimals === 'number')
273
274
  return String(Value.from(rest.amount, rest.decimals))
@@ -1,20 +1,13 @@
1
- import { tempo } from 'mppx/server'
2
- import { createClient, http } from 'viem'
3
- import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts'
4
- import { describe, expectTypeOf, test } from 'vitest'
5
- import * as Method from '../Method.js'
6
- import * as z from '../zod.js'
7
- import * as Mppx from './Mppx.js'
1
+ import { Method, z } from 'mppx'
2
+ import { Mppx } from 'mppx/server'
3
+ import { assertType, describe, expectTypeOf, test } from 'vitest'
8
4
 
9
- const account = privateKeyToAccount(generatePrivateKey())
10
- const getClient = () => createClient({ account, transport: http() })
11
-
12
- const fooCharge = Method.from({
13
- name: 'test',
5
+ const mockChargeA = Method.from({
6
+ name: 'alpha',
14
7
  intent: 'charge',
15
8
  schema: {
16
9
  credential: {
17
- payload: z.object({ signature: z.string() }),
10
+ payload: z.object({ token: z.string() }),
18
11
  },
19
12
  request: z.object({
20
13
  amount: z.string(),
@@ -25,317 +18,119 @@ const fooCharge = Method.from({
25
18
  },
26
19
  })
27
20
 
28
- describe('Mppx', () => {
29
- test('has methods and realm properties', () => {
30
- const method = Method.toServer(fooCharge, {
31
- async verify() {
32
- return {
33
- method: 'test',
34
- reference: '0x123',
35
- status: 'success' as const,
36
- timestamp: new Date().toISOString(),
37
- }
38
- },
39
- })
40
-
41
- const handler = Mppx.create({
42
- methods: [method],
43
- realm: 'api.example.com',
44
- secretKey: 'secret',
45
- })
46
-
47
- expectTypeOf(handler.methods).toEqualTypeOf([method] as const)
48
- expectTypeOf(handler.realm).toBeString()
49
- })
50
-
51
- test('has method functions matching methods', () => {
52
- const method = Method.toServer(fooCharge, {
53
- async verify() {
54
- return {
55
- method: 'test',
56
- reference: '0x123',
57
- status: 'success' as const,
58
- timestamp: new Date().toISOString(),
59
- }
60
- },
61
- })
21
+ const mockChargeB = Method.from({
22
+ name: 'beta',
23
+ intent: 'charge',
24
+ schema: {
25
+ credential: {
26
+ payload: z.object({ token: z.string() }),
27
+ },
28
+ request: z.object({
29
+ amount: z.string(),
30
+ currency: z.string(),
31
+ decimals: z.number(),
32
+ recipient: z.string(),
33
+ }),
34
+ },
35
+ })
62
36
 
63
- const handler = Mppx.create({
64
- methods: [method],
65
- realm: 'api.example.com',
66
- secretKey: 'secret',
67
- })
37
+ const alphaMethod = Method.toServer(mockChargeA, {
38
+ async verify() {
39
+ return {
40
+ method: 'alpha',
41
+ reference: 'tx',
42
+ status: 'success' as const,
43
+ timestamp: new Date().toISOString(),
44
+ }
45
+ },
46
+ })
68
47
 
69
- expectTypeOf(handler.charge).toBeFunction()
70
- })
48
+ const betaMethod = Method.toServer(mockChargeB, {
49
+ async verify() {
50
+ return {
51
+ method: 'beta',
52
+ reference: 'tx',
53
+ status: 'success' as const,
54
+ timestamp: new Date().toISOString(),
55
+ }
56
+ },
57
+ })
71
58
 
72
- test('method function options include request', () => {
73
- const method = Method.toServer(fooCharge, {
74
- async verify() {
75
- return {
76
- method: 'test',
77
- reference: '0x123',
78
- status: 'success' as const,
79
- timestamp: new Date().toISOString(),
80
- }
81
- },
82
- })
59
+ const secretKey = 'test-secret'
60
+ const realm = 'api.example.com'
83
61
 
84
- const handler = Mppx.create({
85
- methods: [method],
86
- realm: 'api.example.com',
87
- secretKey: 'secret',
88
- })
62
+ describe('Mppx type tests', () => {
63
+ test('compose exists on the instance and returns a handler', () => {
64
+ const mppx = Mppx.create({ methods: [alphaMethod, betaMethod], realm, secretKey })
89
65
 
90
- handler.charge({
91
- amount: '1000',
92
- currency: '0x1234',
93
- decimals: 6,
94
- expires: '2025-01-01T00:00:00Z',
95
- recipient: '0xabc',
96
- })
66
+ expectTypeOf(mppx.compose).toBeFunction()
97
67
  })
98
68
 
99
- test('method function returns handler that accepts Request', async () => {
100
- const method = Method.toServer(fooCharge, {
101
- async verify() {
102
- return {
103
- method: 'test',
104
- reference: '0x123',
105
- status: 'success' as const,
106
- timestamp: new Date().toISOString(),
107
- }
108
- },
109
- })
110
-
111
- const handler = Mppx.create({
112
- methods: [method],
113
- realm: 'api.example.com',
114
- secretKey: 'secret',
115
- })
69
+ test('compose accepts method reference tuples', () => {
70
+ const mppx = Mppx.create({ methods: [alphaMethod, betaMethod], realm, secretKey })
116
71
 
117
- const chargeHandler = handler.charge({
118
- amount: '1000',
119
- currency: '0x1234',
72
+ const opts = {
73
+ amount: '100',
74
+ currency: '0x01',
120
75
  decimals: 6,
121
- expires: '2025-01-01T00:00:00Z',
122
- recipient: '0xabc',
123
- })
124
-
125
- const result = await chargeHandler(new Request('https://example.com'))
126
-
127
- if (result.status === 402) {
128
- expectTypeOf(result.challenge).toEqualTypeOf<Response>()
129
- } else {
130
- expectTypeOf(result.withReceipt).toBeFunction()
76
+ recipient: '0x02',
131
77
  }
132
- })
133
-
134
- test('multiple methods', () => {
135
- const fooAuthorize = Method.from({
136
- name: 'test',
137
- intent: 'authorize',
138
- schema: {
139
- credential: {
140
- payload: z.object({ token: z.string() }),
141
- },
142
- request: z.object({
143
- scope: z.string(),
144
- duration: z.number(),
145
- }),
146
- },
147
- })
148
78
 
149
- const chargeMethod = Method.toServer(fooCharge, {
150
- defaults: {
151
- currency: '0x1234',
152
- recipient: '0xabc',
153
- },
154
- async verify() {
155
- return {
156
- method: 'test',
157
- reference: '0x123',
158
- status: 'success' as const,
159
- timestamp: new Date().toISOString(),
160
- }
161
- },
162
- })
163
-
164
- const authorizeMethod = Method.toServer(fooAuthorize, {
165
- async verify() {
166
- return {
167
- method: 'test',
168
- reference: '0x456',
169
- status: 'success' as const,
170
- timestamp: new Date().toISOString(),
171
- }
172
- },
173
- })
79
+ // Should compile method reference entries
80
+ const handler = mppx.compose([alphaMethod, opts], [betaMethod, opts])
81
+ expectTypeOf(handler).toBeFunction()
82
+ })
174
83
 
175
- const handler = Mppx.create({
176
- methods: [chargeMethod, authorizeMethod],
177
- realm: 'api.example.com',
178
- secretKey: 'secret',
179
- })
84
+ test('compose accepts string key tuples', () => {
85
+ const mppx = Mppx.create({ methods: [alphaMethod, betaMethod], realm, secretKey })
180
86
 
181
- handler.charge({
182
- amount: '1000',
183
- currency: '0x1234',
87
+ const opts = {
88
+ amount: '100',
89
+ currency: '0x01',
184
90
  decimals: 6,
185
- recipient: '0xabc',
186
- })
91
+ recipient: '0x02',
92
+ }
187
93
 
188
- handler.authorize({
189
- scope: 'read',
190
- duration: 3600,
191
- })
94
+ // Should compile — string key entries
95
+ const handler = mppx.compose(['alpha/charge', opts], ['beta/charge', opts])
96
+ expectTypeOf(handler).toBeFunction()
192
97
  })
193
98
 
194
- describe('defaults', () => {
195
- test('defaulted fields are optional in method options', () => {
196
- const handler = Mppx.create({
197
- methods: [tempo({ currency: '0x1234', recipient: '0xabc', getClient })],
198
- realm: 'api.example.com',
199
- secretKey: 'secret',
200
- })
201
-
202
- // currency and recipient should be optional since they're in defaults
203
- handler.charge({
204
- amount: '1000',
205
- decimals: 6,
206
- })
207
-
208
- // But can still be overridden
209
- handler.charge({
210
- amount: '1000',
211
- currency: '0x5678',
212
- decimals: 6,
213
- recipient: '0xdef',
214
- })
215
- })
216
-
217
- test('non-defaulted fields remain required', () => {
218
- const handler = Mppx.create({
219
- methods: [tempo({ currency: '0x1234', getClient })],
220
- realm: 'api.example.com',
221
- secretKey: 'secret',
222
- })
223
-
224
- // recipient is still required since it's not in defaults
225
- handler.charge({
226
- amount: '1000',
227
- decimals: 6,
228
- recipient: '0xabc',
229
- })
230
- })
231
-
232
- test('no defaults means all fields required', () => {
233
- const handler = Mppx.create({
234
- methods: [tempo({ getClient })],
235
- realm: 'api.example.com',
236
- secretKey: 'secret',
237
- })
238
-
239
- // All required fields must be provided
240
- handler.charge({
241
- amount: '1000',
242
- currency: '0x1234',
243
- decimals: 6,
244
- recipient: '0xabc',
245
- })
246
- })
99
+ test('nested handlers are accessible', () => {
100
+ const mppx = Mppx.create({ methods: [alphaMethod, betaMethod], realm, secretKey })
247
101
 
248
- test('type: defaulted fields are optional in options type', () => {
249
- const handler = Mppx.create({
250
- methods: [tempo({ currency: '0x1234', recipient: '0xabc', getClient })],
251
- realm: 'api.example.com',
252
- secretKey: 'secret',
253
- })
254
-
255
- type ChargeOptions = Parameters<typeof handler.charge>[0]
256
-
257
- // currency and recipient should be optional (include undefined)
258
- expectTypeOf<ChargeOptions['currency']>().toEqualTypeOf<string | undefined>()
259
- expectTypeOf<ChargeOptions['recipient']>().toEqualTypeOf<string | undefined>()
260
-
261
- // amount should still be required (no undefined)
262
- expectTypeOf<ChargeOptions['amount']>().toEqualTypeOf<string>()
263
- })
264
-
265
- test('account as Account defaults recipient', () => {
266
- const handler = Mppx.create({
267
- methods: [tempo.charge({ currency: '0x1234', account })],
268
- realm: 'api.example.com',
269
- secretKey: 'secret',
270
- })
271
-
272
- // recipient is defaulted via account, so it should be optional
273
- handler.charge({
274
- amount: '1000',
275
- decimals: 6,
276
- })
277
-
278
- // can still override recipient
279
- handler.charge({
280
- amount: '1000',
281
- decimals: 6,
282
- recipient: '0xdef',
283
- })
284
- })
102
+ expectTypeOf(mppx.alpha).toBeObject()
103
+ expectTypeOf(mppx.alpha.charge).toBeFunction()
104
+ expectTypeOf(mppx.beta).toBeObject()
105
+ expectTypeOf(mppx.beta.charge).toBeFunction()
106
+ })
285
107
 
286
- test('account as Account with feePayer: true', () => {
287
- const handler = Mppx.create({
288
- methods: [tempo.charge({ currency: '0x1234', account, feePayer: true })],
289
- realm: 'api.example.com',
290
- secretKey: 'secret',
291
- })
108
+ test('slash key handlers are accessible', () => {
109
+ const mppx = Mppx.create({ methods: [alphaMethod, betaMethod], realm, secretKey })
292
110
 
293
- handler.charge({
294
- amount: '1000',
295
- decimals: 6,
296
- })
297
- })
111
+ expectTypeOf(mppx['alpha/charge']).toBeFunction()
112
+ expectTypeOf(mppx['beta/charge']).toBeFunction()
298
113
  })
299
114
 
300
- describe('session getClient', () => {
301
- test('tempo.session requires getClient', () => {
302
- Mppx.create({
303
- methods: [
304
- tempo.session({
305
- currency: '0x1234',
306
- recipient: '0xabc',
307
- getClient,
308
- }),
309
- ],
310
- realm: 'api.example.com',
311
- secretKey: 'secret',
312
- })
313
- })
115
+ test('compose return type is a request handler returning the response union', () => {
116
+ const mppx = Mppx.create({ methods: [alphaMethod], realm, secretKey })
117
+
118
+ const opts = {
119
+ amount: '100',
120
+ currency: '0x01',
121
+ decimals: 6,
122
+ recipient: '0x02',
123
+ }
314
124
 
315
- test('combined tempo() requires getClient', () => {
316
- Mppx.create({
317
- methods: [tempo({ currency: '0x1234', recipient: '0xabc', getClient })],
318
- realm: 'api.example.com',
319
- secretKey: 'secret',
320
- })
321
- })
125
+ const handler = mppx.compose([alphaMethod, opts])
126
+ type HandlerReturn = ReturnType<typeof handler>
322
127
 
323
- test('tempo.charge does not require getClient', () => {
324
- Mppx.create({
325
- methods: [tempo.charge({ currency: '0x1234', recipient: '0xabc' })],
326
- realm: 'api.example.com',
327
- secretKey: 'secret',
328
- })
329
- })
128
+ assertType<Promise<{ status: 402; challenge: Response } | { status: 200; withReceipt: any }>>(
129
+ {} as Awaited<HandlerReturn> as any,
130
+ )
330
131
  })
331
- })
332
-
333
- describe('create.Config', () => {
334
- test('requires methods, realm, and secretKey', () => {
335
- type Config = Mppx.create.Config
336
132
 
337
- expectTypeOf<Config>().toHaveProperty('methods')
338
- expectTypeOf<Config>().toHaveProperty('realm')
339
- expectTypeOf<Config>().toHaveProperty('secretKey')
133
+ test('static Mppx.compose accepts configured handlers', () => {
134
+ expectTypeOf(Mppx.compose).toBeFunction()
340
135
  })
341
136
  })