mppx 0.5.1 → 0.5.3

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 (89) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/dist/Credential.d.ts +12 -0
  3. package/dist/Credential.d.ts.map +1 -1
  4. package/dist/Credential.js +22 -4
  5. package/dist/Credential.js.map +1 -1
  6. package/dist/Method.d.ts +4 -0
  7. package/dist/Method.d.ts.map +1 -1
  8. package/dist/Method.js +2 -1
  9. package/dist/Method.js.map +1 -1
  10. package/dist/proxy/Proxy.d.ts.map +1 -1
  11. package/dist/proxy/Proxy.js +52 -8
  12. package/dist/proxy/Proxy.js.map +1 -1
  13. package/dist/proxy/internal/Route.d.ts.map +1 -1
  14. package/dist/proxy/internal/Route.js +7 -3
  15. package/dist/proxy/internal/Route.js.map +1 -1
  16. package/dist/server/Mppx.d.ts.map +1 -1
  17. package/dist/server/Mppx.js +90 -71
  18. package/dist/server/Mppx.js.map +1 -1
  19. package/dist/server/Transport.d.ts +5 -1
  20. package/dist/server/Transport.d.ts.map +1 -1
  21. package/dist/server/Transport.js +52 -7
  22. package/dist/server/Transport.js.map +1 -1
  23. package/dist/server/internal/html/config.d.ts +7 -0
  24. package/dist/server/internal/html/config.d.ts.map +1 -0
  25. package/dist/server/internal/html/config.js +3 -0
  26. package/dist/server/internal/html/config.js.map +1 -0
  27. package/dist/server/internal/html/serviceWorker.gen.d.ts +2 -0
  28. package/dist/server/internal/html/serviceWorker.gen.d.ts.map +1 -0
  29. package/dist/server/internal/html/serviceWorker.gen.js +3 -0
  30. package/dist/server/internal/html/serviceWorker.gen.js.map +1 -0
  31. package/dist/stripe/server/Charge.d.ts +5 -0
  32. package/dist/stripe/server/Charge.d.ts.map +1 -1
  33. package/dist/stripe/server/Charge.js +14 -6
  34. package/dist/stripe/server/Charge.js.map +1 -1
  35. package/dist/stripe/server/internal/html.gen.d.ts +2 -0
  36. package/dist/stripe/server/internal/html.gen.d.ts.map +1 -0
  37. package/dist/stripe/server/internal/html.gen.js +3 -0
  38. package/dist/stripe/server/internal/html.gen.js.map +1 -0
  39. package/dist/tempo/server/Charge.d.ts +2 -0
  40. package/dist/tempo/server/Charge.d.ts.map +1 -1
  41. package/dist/tempo/server/Charge.js +15 -9
  42. package/dist/tempo/server/Charge.js.map +1 -1
  43. package/dist/tempo/server/Session.d.ts.map +1 -1
  44. package/dist/tempo/server/Session.js +3 -2
  45. package/dist/tempo/server/Session.js.map +1 -1
  46. package/dist/tempo/server/internal/html.gen.d.ts +2 -0
  47. package/dist/tempo/server/internal/html.gen.d.ts.map +1 -0
  48. package/dist/tempo/server/internal/html.gen.js +3 -0
  49. package/dist/tempo/server/internal/html.gen.js.map +1 -0
  50. package/dist/tempo/server/internal/transport.d.ts +1 -1
  51. package/dist/tempo/server/internal/transport.d.ts.map +1 -1
  52. package/dist/tempo/server/internal/transport.js +45 -58
  53. package/dist/tempo/server/internal/transport.js.map +1 -1
  54. package/package.json +2 -2
  55. package/src/Credential.ts +28 -4
  56. package/src/Method.ts +6 -1
  57. package/src/env.d.ts +1 -0
  58. package/src/mcp-sdk/server/Transport.test.ts +6 -0
  59. package/src/proxy/Proxy.test.ts +188 -1
  60. package/src/proxy/Proxy.ts +58 -9
  61. package/src/proxy/internal/Route.test.ts +9 -0
  62. package/src/proxy/internal/Route.ts +5 -2
  63. package/src/server/Mppx.test.ts +171 -18
  64. package/src/server/Mppx.ts +120 -79
  65. package/src/server/Transport.test.ts +16 -2
  66. package/src/server/Transport.ts +61 -7
  67. package/src/server/internal/html/config.ts +8 -0
  68. package/src/server/internal/html/serviceWorker.client.ts +28 -0
  69. package/src/server/internal/html/serviceWorker.gen.ts +2 -0
  70. package/src/server/internal/html/serviceWorker.ts +27 -0
  71. package/src/server/internal/html/tsconfig.worker.client.json +8 -0
  72. package/src/server/internal/html/tsconfig.worker.json +8 -0
  73. package/src/stripe/server/Charge.ts +19 -5
  74. package/src/stripe/server/internal/html/main.ts +106 -0
  75. package/src/stripe/server/internal/html/node_modules/.bin/mppx.src +21 -0
  76. package/src/stripe/server/internal/html/package.json +9 -0
  77. package/src/stripe/server/internal/html/stripe-js-pure.d.ts +7 -0
  78. package/src/stripe/server/internal/html/tsconfig.json +8 -0
  79. package/src/stripe/server/internal/html.gen.ts +2 -0
  80. package/src/tempo/server/Charge.ts +20 -8
  81. package/src/tempo/server/Session.ts +3 -2
  82. package/src/tempo/server/internal/html/main.ts +71 -0
  83. package/src/tempo/server/internal/html/node_modules/.bin/mppx.src +21 -0
  84. package/src/tempo/server/internal/html/package.json +10 -0
  85. package/src/tempo/server/internal/html/tsconfig.json +8 -0
  86. package/src/tempo/server/internal/html.gen.ts +2 -0
  87. package/src/tempo/server/internal/transport.test.ts +37 -31
  88. package/src/tempo/server/internal/transport.ts +44 -58
  89. package/src/tsconfig.json +1 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"html.gen.js","sourceRoot":"","sources":["../../../../src/tempo/server/internal/html.gen.ts"],"names":[],"mappings":"AAAA,2BAA2B;AAC3B,MAAM,CAAC,MAAM,IAAI,GAAG,8wwaAA8wwa,CAAA"}
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Tempo-specific SSE transport that wraps the base HTTP transport
3
- * with metering logic (context capture from credentials, per-token
3
+ * with metering logic (context capture from verified credentials, per-token
4
4
  * charging via Sse.serve).
5
5
  *
6
6
  * @internal
@@ -1 +1 @@
1
- {"version":3,"file":"transport.d.ts","sourceRoot":"","sources":["../../../../src/tempo/server/internal/transport.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,KAAK,SAAS,MAAM,8BAA8B,CAAA;AACzD,OAAO,KAAK,YAAY,MAAM,+BAA+B,CAAA;AAC7D,OAAO,KAAK,QAAQ,MAAM,sBAAsB,CAAA;AAEhD,mDAAmD;AACnD,MAAM,MAAM,GAAG,GAAG,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC,iBAAiB,CAAC,CAAA;AAE3D;;;;;;;;GAQG;AACH,wBAAgB,GAAG,CAAC,OAAO,EAAE,GAAG,CAAC,OAAO,GAAG;IAAE,KAAK,EAAE,YAAY,CAAC,YAAY,CAAA;CAAE,GAAG,GAAG,CAqHpF;AAED,MAAM,CAAC,OAAO,WAAW,GAAG,CAAC;IAC3B,KAAK,OAAO,GAAG;QACb;;;;;;;;;WASG;QACH,IAAI,CAAC,EAAE,OAAO,GAAG,SAAS,CAAA;QAC1B,sDAAsD;QACtD,eAAe,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;KACrC,CAAA;CACF;AAED,+EAA+E;AAC/E,wBAAgB,YAAY,CAAC,OAAO,EAAE;IACpC,QAAQ,EAAE,aAAa,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,aAAa,CAAC,MAAM,CAAC,CAAC,CAAA;IAC7E,WAAW,EAAE,MAAM,CAAA;CACpB,GAAG,QAAQ,CAwBX"}
1
+ {"version":3,"file":"transport.d.ts","sourceRoot":"","sources":["../../../../src/tempo/server/internal/transport.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,KAAK,SAAS,MAAM,8BAA8B,CAAA;AACzD,OAAO,KAAK,YAAY,MAAM,+BAA+B,CAAA;AAC7D,OAAO,KAAK,QAAQ,MAAM,sBAAsB,CAAA;AAGhD,mDAAmD;AACnD,MAAM,MAAM,GAAG,GAAG,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC,iBAAiB,CAAC,CAAA;AAE3D;;;;;;;;GAQG;AACH,wBAAgB,GAAG,CAAC,OAAO,EAAE,GAAG,CAAC,OAAO,GAAG;IAAE,KAAK,EAAE,YAAY,CAAC,YAAY,CAAA;CAAE,GAAG,GAAG,CAsGpF;AAED,MAAM,CAAC,OAAO,WAAW,GAAG,CAAC;IAC3B,KAAK,OAAO,GAAG;QACb;;;;;;;;;WASG;QACH,IAAI,CAAC,EAAE,OAAO,GAAG,SAAS,CAAA;QAC1B,sDAAsD;QACtD,eAAe,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;KACrC,CAAA;CACF;AAED,+EAA+E;AAC/E,wBAAgB,YAAY,CAAC,OAAO,EAAE;IACpC,QAAQ,EAAE,aAAa,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,aAAa,CAAC,MAAM,CAAC,CAAC,CAAA;IAC7E,WAAW,EAAE,MAAM,CAAA;CACpB,GAAG,QAAQ,CAwBX"}
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Tempo-specific SSE transport that wraps the base HTTP transport
3
- * with metering logic (context capture from credentials, per-token
3
+ * with metering logic (context capture from verified credentials, per-token
4
4
  * charging via Sse.serve).
5
5
  *
6
6
  * @internal
@@ -28,27 +28,21 @@ export function sse(options) {
28
28
  const { waitForUpdate: _, ...store } = options.store;
29
29
  return store;
30
30
  })();
31
- const contextMap = new Map();
32
31
  const base = Transport.http();
33
32
  return Transport.from({
34
33
  name: 'sse',
35
34
  getCredential(request) {
36
- const credential = base.getCredential(request);
37
- if (credential) {
38
- try {
39
- const ctx = Sse_core.fromRequest(request);
40
- contextMap.set(ctx.challengeId, { ...ctx, signal: request.signal });
41
- }
42
- catch {
43
- // ignore — non-SSE credentials won't have session context
44
- }
45
- }
46
- return credential;
35
+ return base.getCredential(request);
47
36
  },
48
37
  respondChallenge(options) {
49
38
  return base.respondChallenge(options);
50
39
  },
51
- respondReceipt({ receipt, response, challengeId }) {
40
+ respondReceipt({ credential, receipt, response, challengeId, input }) {
41
+ const payload = credential.payload;
42
+ if (!payload.channelId)
43
+ throw new Error('No SSE context available');
44
+ const channelId = payload.channelId;
45
+ const tickCost = BigInt(credential.challenge.request.amount);
52
46
  // Auto-detect upstream SSE responses and parse them into an
53
47
  // AsyncIterable so they flow through the metered pipeline.
54
48
  // This lets proxy consumers simply pass `result.withReceipt(upstreamRes)`
@@ -57,10 +51,6 @@ export function sse(options) {
57
51
  ? Sse_core.iterateData(response, { skip: (d) => d === '[DONE]' })
58
52
  : response;
59
53
  if (isAsyncGeneratorFunction(resolved) || isAsyncIterable(resolved)) {
60
- const ctx = contextMap.get(challengeId);
61
- if (!ctx)
62
- throw new Error('No SSE context available — credential was not parsed');
63
- contextMap.delete(challengeId);
64
54
  // Pass async generator functions directly so Sse.serve gives them
65
55
  // a SessionController for manual charge(). Pass raw AsyncIterables
66
56
  // as-is so Sse.serve auto-charges per yielded value.
@@ -69,60 +59,57 @@ export function sse(options) {
69
59
  : resolved;
70
60
  const stream = Sse_core.serve({
71
61
  store,
72
- channelId: ctx.channelId,
62
+ channelId,
73
63
  challengeId,
74
- tickCost: ctx.tickCost,
64
+ tickCost,
75
65
  pollIntervalMs: pollingInterval,
76
66
  generate,
77
- signal: ctx.signal,
67
+ signal: input.signal,
78
68
  });
79
69
  return Sse_core.toResponse(stream);
80
70
  }
81
71
  const baseResponse = base.respondReceipt({
72
+ credential,
73
+ input,
82
74
  receipt,
83
75
  response: response,
84
76
  challengeId,
85
77
  });
86
78
  // Non-SSE response (e.g. upstream returned JSON instead of event-stream).
87
79
  // Need to deduct tickCost so request isn't free.
88
- const ctx = contextMap.get(challengeId);
89
- if (ctx) {
90
- contextMap.delete(challengeId);
91
- // Null-body statuses (e.g. 204 from management actions) cannot carry a
92
- // response body per Fetch/HTTP semantics.
93
- if (isNullBodyStatus(baseResponse.status)) {
94
- return baseResponse;
95
- }
96
- const stream = new ReadableStream({
97
- async start(controller) {
98
- // deduction completes before consumer reads
99
- await ChannelStore.deductFromChannel(store, ctx.channelId, ctx.tickCost);
100
- if (!baseResponse.body) {
101
- controller.close();
102
- return;
103
- }
104
- const reader = baseResponse.body.getReader();
105
- try {
106
- while (true) {
107
- const { done, value } = await reader.read();
108
- if (done)
109
- break;
110
- controller.enqueue(value);
111
- }
112
- }
113
- finally {
114
- reader.releaseLock();
115
- controller.close();
116
- }
117
- },
118
- });
119
- return new Response(stream, {
120
- status: baseResponse.status,
121
- statusText: baseResponse.statusText,
122
- headers: baseResponse.headers,
123
- });
80
+ // Null-body statuses (e.g. 204 from management actions) cannot carry a
81
+ // response body per Fetch/HTTP semantics.
82
+ if (isNullBodyStatus(baseResponse.status)) {
83
+ return baseResponse;
124
84
  }
125
- return baseResponse;
85
+ const stream = new ReadableStream({
86
+ async start(controller) {
87
+ // deduction completes before consumer reads
88
+ await ChannelStore.deductFromChannel(store, channelId, tickCost);
89
+ if (!baseResponse.body) {
90
+ controller.close();
91
+ return;
92
+ }
93
+ const reader = baseResponse.body.getReader();
94
+ try {
95
+ while (true) {
96
+ const { done, value } = await reader.read();
97
+ if (done)
98
+ break;
99
+ controller.enqueue(value);
100
+ }
101
+ }
102
+ finally {
103
+ reader.releaseLock();
104
+ controller.close();
105
+ }
106
+ },
107
+ });
108
+ return new Response(stream, {
109
+ status: baseResponse.status,
110
+ statusText: baseResponse.statusText,
111
+ headers: baseResponse.headers,
112
+ });
126
113
  },
127
114
  });
128
115
  }
@@ -1 +1 @@
1
- {"version":3,"file":"transport.js","sourceRoot":"","sources":["../../../../src/tempo/server/internal/transport.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"transport.js","sourceRoot":"","sources":["../../../../src/tempo/server/internal/transport.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "mppx",
3
3
  "type": "module",
4
- "version": "0.5.1",
4
+ "version": "0.5.3",
5
5
  "main": "./dist/index.js",
6
6
  "license": "MIT",
7
7
  "files": [
@@ -121,7 +121,7 @@
121
121
  "@remix-run/fetch-proxy": "^0.7.1",
122
122
  "@remix-run/node-fetch-server": "^0.13.0",
123
123
  "incur": "^0.3.1",
124
- "ox": "^0.14.1",
124
+ "ox": "0.14.7",
125
125
  "zod": "^4.3.6"
126
126
  },
127
127
  "repository": {
package/src/Credential.ts CHANGED
@@ -18,6 +18,30 @@ export type Credential<
18
18
  source?: string
19
19
  }
20
20
 
21
+ export class MissingAuthorizationHeaderError extends Error {
22
+ override readonly name = 'MissingAuthorizationHeaderError'
23
+
24
+ constructor() {
25
+ super('Missing Authorization header.')
26
+ }
27
+ }
28
+
29
+ export class MissingPaymentSchemeError extends Error {
30
+ override readonly name = 'MissingPaymentSchemeError'
31
+
32
+ constructor() {
33
+ super('Missing Payment scheme.')
34
+ }
35
+ }
36
+
37
+ export class InvalidCredentialEncodingError extends Error {
38
+ override readonly name = 'InvalidCredentialEncodingError'
39
+
40
+ constructor() {
41
+ super('Invalid base64url or JSON.')
42
+ }
43
+ }
44
+
21
45
  /**
22
46
  * Deserializes an Authorization header value to a credential.
23
47
  *
@@ -33,7 +57,7 @@ export type Credential<
33
57
  */
34
58
  export function deserialize<payload = unknown>(value: string): Credential<payload> {
35
59
  const prefixMatch = value.match(/^Payment\s+(.+)$/i)
36
- if (!prefixMatch?.[1]) throw new Error('Missing Payment scheme.')
60
+ if (!prefixMatch?.[1]) throw new MissingPaymentSchemeError()
37
61
  try {
38
62
  const json = Base64.toString(prefixMatch[1])
39
63
  const parsed = JSON.parse(json) as {
@@ -51,7 +75,7 @@ export function deserialize<payload = unknown>(value: string): Credential<payloa
51
75
  ...(parsed.source && { source: parsed.source }),
52
76
  } as Credential<payload>
53
77
  } catch {
54
- throw new Error('Invalid base64url or JSON.')
78
+ throw new InvalidCredentialEncodingError()
55
79
  }
56
80
  }
57
81
 
@@ -108,9 +132,9 @@ export declare namespace from {
108
132
  */
109
133
  export function fromRequest<payload = unknown>(request: Request): Credential<payload> {
110
134
  const header = request.headers.get('Authorization')
111
- if (!header) throw new Error('Missing Authorization header.')
135
+ if (!header) throw new MissingAuthorizationHeaderError()
112
136
  const payment = extractPaymentScheme(header)
113
- if (!payment) throw new Error('Missing Payment scheme.')
137
+ if (!payment) throw new MissingPaymentSchemeError()
114
138
  return deserialize<payload>(payment)
115
139
  }
116
140
 
package/src/Method.ts CHANGED
@@ -2,6 +2,7 @@ import type * as Challenge from './Challenge.js'
2
2
  import type * as Credential from './Credential.js'
3
3
  import type { ExactPartial, LooseOmit, MaybePromise } from './internal/types.js'
4
4
  import type * as Receipt from './Receipt.js'
5
+ import type * as Html from './server/internal/html/config.js'
5
6
  import type * as Transport from './server/Transport.js'
6
7
  import type * as z from './zod.js'
7
8
 
@@ -10,6 +11,7 @@ import type * as z from './zod.js'
10
11
  */
11
12
  export type Method = {
12
13
  name: string
14
+ html?: Html.Options | undefined
13
15
  intent: string
14
16
  schema: {
15
17
  credential: {
@@ -74,6 +76,7 @@ export type Server<
74
76
  transportOverride = undefined,
75
77
  > = method & {
76
78
  defaults?: defaults | undefined
79
+ html?: Html.Options | undefined
77
80
  request?: RequestFn<method> | undefined
78
81
  respond?: RespondFn<method> | undefined
79
82
  transport?: transportOverride | undefined
@@ -202,10 +205,11 @@ export function toServer<
202
205
  method: method,
203
206
  options: toServer.Options<method, defaults, transportOverride>,
204
207
  ): Server<method, defaults, transportOverride> {
205
- const { defaults, request, respond, transport, verify } = options
208
+ const { defaults, html, request, respond, transport, verify } = options
206
209
  return {
207
210
  ...method,
208
211
  defaults,
212
+ html,
209
213
  request,
210
214
  respond,
211
215
  transport,
@@ -220,6 +224,7 @@ export declare namespace toServer {
220
224
  transportOverride extends Transport.AnyTransport | undefined = undefined,
221
225
  > = {
222
226
  defaults?: defaults | undefined
227
+ html?: Html.Options | undefined
223
228
  request?: RequestFn<method> | undefined
224
229
  respond?: RespondFn<method> | undefined
225
230
  transport?: transportOverride | undefined
package/src/env.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  /// <reference types="vite/client" />
2
2
 
3
3
  interface ImportMetaEnv {
4
+ readonly MODE: 'test' | 'production'
4
5
  readonly VITE_NODE_ENV: 'localnet' | 'testnet' | 'mainnet'
5
6
  readonly VITE_HTTP_LOG: 'true' | 'false'
6
7
  readonly VITE_RPC_CREDENTIALS: string
@@ -113,6 +113,8 @@ describe('mcpSdk', () => {
113
113
 
114
114
  const result = transport.respondReceipt({
115
115
  challengeId: 'test-challenge-id',
116
+ credential,
117
+ input: {} as Extra,
116
118
  receipt,
117
119
  response,
118
120
  })
@@ -139,6 +141,8 @@ describe('mcpSdk', () => {
139
141
 
140
142
  const result = transport.respondReceipt({
141
143
  challengeId: 'cid',
144
+ credential,
145
+ input: {} as Extra,
142
146
  receipt,
143
147
  response,
144
148
  })
@@ -162,6 +166,8 @@ describe('mcpSdk', () => {
162
166
 
163
167
  const result = transport.respondReceipt({
164
168
  challengeId: 'cid',
169
+ credential,
170
+ input: {} as Extra,
165
171
  receipt,
166
172
  response,
167
173
  })
@@ -1,4 +1,4 @@
1
- import { Receipt } from 'mppx'
1
+ import { Challenge, Credential, Method, Receipt, z } from 'mppx'
2
2
  import { Mppx as Mppx_client, tempo as tempo_client } from 'mppx/client'
3
3
  import { Mppx as Mppx_server, tempo as tempo_server } from 'mppx/server'
4
4
  import { afterEach, describe, expect, test } from 'vp/test'
@@ -439,6 +439,193 @@ describe('create', () => {
439
439
  expect(res.status).toBe(402)
440
440
  })
441
441
 
442
+ test('behavior: management POST uses credential method binding to disambiguate same-path paid routes', async () => {
443
+ const alpha = Method.from({
444
+ name: 'alpha',
445
+ intent: 'charge',
446
+ schema: {
447
+ credential: { payload: z.object({ token: z.string() }) },
448
+ request: z.object({ amount: z.string() }),
449
+ },
450
+ })
451
+ const beta = Method.from({
452
+ name: 'beta',
453
+ intent: 'charge',
454
+ schema: {
455
+ credential: { payload: z.object({ token: z.string() }) },
456
+ request: z.object({ amount: z.string() }),
457
+ },
458
+ })
459
+
460
+ const handler = Mppx_server.create({
461
+ methods: [
462
+ Method.toServer(alpha, {
463
+ async verify() {
464
+ return Receipt.from({
465
+ method: 'alpha',
466
+ status: 'success',
467
+ timestamp: new Date().toISOString(),
468
+ reference: 'alpha-reference',
469
+ })
470
+ },
471
+ respond() {
472
+ return new Response(null, { status: 204 })
473
+ },
474
+ }),
475
+ Method.toServer(beta, {
476
+ async verify() {
477
+ return Receipt.from({
478
+ method: 'beta',
479
+ status: 'success',
480
+ timestamp: new Date().toISOString(),
481
+ reference: 'beta-reference',
482
+ })
483
+ },
484
+ respond() {
485
+ return new Response(null, { status: 205 })
486
+ },
487
+ }),
488
+ ],
489
+ secretKey,
490
+ })
491
+
492
+ const proxy = ApiProxy.create({
493
+ services: [
494
+ Service.from('api', {
495
+ baseUrl: 'https://example.com',
496
+ routes: {
497
+ 'GET /v1/stream': handler['alpha/charge']({ amount: '1' }),
498
+ 'PATCH /v1/stream': handler['beta/charge']({ amount: '1' }),
499
+ },
500
+ }),
501
+ ],
502
+ })
503
+ proxyServer = await Http.createServer(proxy.listener)
504
+
505
+ const challengeResponse = await fetch(`${proxyServer.url}/api/v1/stream`)
506
+ expect(challengeResponse.status).toBe(402)
507
+
508
+ const challenge = Challenge.fromResponse(challengeResponse)
509
+ const authorization = Credential.serialize(
510
+ Credential.from({
511
+ challenge,
512
+ payload: { token: 'ok' },
513
+ }),
514
+ )
515
+
516
+ const res = await fetch(`${proxyServer.url}/api/v1/stream`, {
517
+ method: 'POST',
518
+ headers: { Authorization: authorization },
519
+ })
520
+
521
+ expect(res.status).toBe(204)
522
+ })
523
+
524
+ test('behavior: exact-match management POST does not forward upstream', async () => {
525
+ let upstreamRequests = 0
526
+ upstream = await createUpstream(() => {
527
+ upstreamRequests += 1
528
+ return Response.json({ ok: true })
529
+ })
530
+
531
+ const method = Method.from({
532
+ name: 'mock',
533
+ intent: 'charge',
534
+ schema: {
535
+ credential: { payload: z.object({ token: z.string() }) },
536
+ request: z.object({ amount: z.string() }),
537
+ },
538
+ })
539
+ const handler = Mppx_server.create({
540
+ methods: [
541
+ Method.toServer(method, {
542
+ async verify() {
543
+ return Receipt.from({
544
+ method: 'mock',
545
+ status: 'success',
546
+ timestamp: new Date().toISOString(),
547
+ reference: 'mock-reference',
548
+ })
549
+ },
550
+ respond() {
551
+ return new Response(null, { status: 204 })
552
+ },
553
+ }),
554
+ ],
555
+ secretKey,
556
+ })
557
+
558
+ const proxy = ApiProxy.create({
559
+ services: [
560
+ Service.from('api', {
561
+ baseUrl: upstream.url,
562
+ routes: {
563
+ 'POST /v1/stream': handler['mock/charge']({ amount: '1' }),
564
+ },
565
+ }),
566
+ ],
567
+ })
568
+ proxyServer = await Http.createServer(proxy.listener)
569
+
570
+ const challengeResponse = await fetch(`${proxyServer.url}/api/v1/stream`, { method: 'POST' })
571
+ expect(challengeResponse.status).toBe(402)
572
+
573
+ const challenge = Challenge.fromResponse(challengeResponse)
574
+ const authorization = Credential.serialize(
575
+ Credential.from({
576
+ challenge,
577
+ payload: { token: 'ok' },
578
+ }),
579
+ )
580
+ const res = await fetch(`${proxyServer.url}/api/v1/stream`, {
581
+ method: 'POST',
582
+ headers: { Authorization: authorization },
583
+ })
584
+
585
+ expect(res.status).toBe(204)
586
+ expect(upstreamRequests).toBe(0)
587
+ })
588
+
589
+ test('behavior: paid GET fallback does not forward POST upstream', async () => {
590
+ let upstreamRequests = 0
591
+ upstream = await createUpstream(async (req) => {
592
+ upstreamRequests += 1
593
+ return Response.json({
594
+ body: await req.json(),
595
+ method: req.method,
596
+ })
597
+ })
598
+
599
+ const proxy = ApiProxy.create({
600
+ services: [
601
+ Service.from('api', {
602
+ baseUrl: upstream.url,
603
+ bearer: 'sk-upstream-key',
604
+ routes: { 'GET /v1/messages': mppx_server.charge({ amount: '1', decimals: 6 }) },
605
+ }),
606
+ ],
607
+ })
608
+ proxyServer = await Http.createServer(proxy.listener)
609
+
610
+ const challengeResponse = await fetch(`${proxyServer.url}/api/v1/messages`)
611
+ expect(challengeResponse.status).toBe(402)
612
+
613
+ const authorization = await mppx_client.createCredential(challengeResponse)
614
+ expect(Credential.extractPaymentScheme(authorization)).toBeTruthy()
615
+
616
+ const res = await fetch(`${proxyServer.url}/api/v1/messages`, {
617
+ method: 'POST',
618
+ headers: {
619
+ Authorization: authorization,
620
+ 'Content-Type': 'application/json',
621
+ },
622
+ body: JSON.stringify({ prompt: 'hello' }),
623
+ })
624
+
625
+ expect(res.status).toBe(405)
626
+ expect(upstreamRequests).toBe(0)
627
+ })
628
+
442
629
  test('behavior: POST to unregistered method does not fall back to free GET route', async () => {
443
630
  upstream = await createUpstream(() => Response.json({ ok: true }))
444
631
  const proxy = ApiProxy.create({
@@ -2,6 +2,7 @@ import type * as http from 'node:http'
2
2
 
3
3
  import { createFetchProxy } from '@remix-run/fetch-proxy'
4
4
 
5
+ import * as Credential from '../Credential.js'
5
6
  import { generateProxy } from '../discovery/OpenApi.js'
6
7
  import * as Request from '../server/Request.js'
7
8
  import * as Headers from './internal/Headers.js'
@@ -106,19 +107,26 @@ export function create(config: create.Config): Proxy {
106
107
 
107
108
  const { service, proxy } = entry
108
109
 
109
- const matched =
110
- Route.match(service.routes, request.method, upstreamPath) ??
111
- // Management POSTs (e.g. session close) may target a path whose route
112
- // is registered for a different HTTP method (e.g. GET). Fall back to
113
- // path-only matching so the payment handler can process the action.
114
- (request.method === 'POST' && request.headers.has('authorization')
115
- ? Route.matchPath(
110
+ const exactMatch = Route.match(service.routes, request.method, upstreamPath)
111
+ const fallbackBinding =
112
+ !exactMatch && request.method === 'POST' && request.headers.has('authorization')
113
+ ? getPaymentBinding(request)
114
+ : null
115
+ const fallbackMatch =
116
+ !exactMatch && request.method === 'POST' && request.headers.has('authorization')
117
+ ? // Management POSTs (e.g. session close) may target a path whose route
118
+ // is registered for a different HTTP method (e.g. GET). Fall back to
119
+ // path-only matching so the payment handler can process the action.
120
+ // When the credential parses cleanly, also bind on payment method+intent
121
+ // so same-path paid routes can coexist without sharing credentials.
122
+ Route.matchPath(
116
123
  service.routes,
117
124
  upstreamPath,
118
125
  // skip free routes (e.g. `'GET /foo/bar': true`)
119
- (endpoint) => endpoint !== true,
126
+ (endpoint) => endpoint !== true && matchesPaymentBinding(endpoint, fallbackBinding),
120
127
  )
121
- : null)
128
+ : null
129
+ const matched = exactMatch ?? fallbackMatch
122
130
  if (!matched) return new Response('Not Found', { status: 404 })
123
131
 
124
132
  const endpoint = matched.value as Service.Endpoint
@@ -130,6 +138,22 @@ export function create(config: create.Config): Proxy {
130
138
  const result = await handler(request)
131
139
  if (result.status === 402) return result.challenge
132
140
 
141
+ const managementResponse = (() => {
142
+ try {
143
+ return (result.withReceipt as () => Response)()
144
+ } catch (error) {
145
+ if (
146
+ error instanceof Error &&
147
+ error.message === 'withReceipt() requires a response argument'
148
+ )
149
+ return null
150
+ throw error
151
+ }
152
+ })()
153
+
154
+ if (managementResponse) return managementResponse
155
+ if (fallbackMatch) return new Response('Method Not Allowed', { status: 405 })
156
+
133
157
  const options = Service.getOptions(endpoint)
134
158
  const upstreamRes = await proxyUpstream({
135
159
  request,
@@ -246,3 +270,28 @@ function withBasePath(basePath: string | undefined, path: string) {
246
270
  const trimmed = normalized.endsWith('/') ? normalized.slice(0, -1) : normalized
247
271
  return `${trimmed}${path}`
248
272
  }
273
+
274
+ type PaymentBinding = {
275
+ intent: string
276
+ method: string
277
+ }
278
+
279
+ function getPaymentBinding(request: Request): PaymentBinding | null {
280
+ try {
281
+ const credential = Credential.fromRequest(request)
282
+ return {
283
+ intent: credential.challenge.intent,
284
+ method: credential.challenge.method,
285
+ }
286
+ } catch {
287
+ return null
288
+ }
289
+ }
290
+
291
+ function matchesPaymentBinding(endpoint: unknown, binding: PaymentBinding | null): boolean {
292
+ if (endpoint === true) return false
293
+ if (!binding) return true
294
+ const payment = Service.paymentOf(endpoint as Exclude<Service.Endpoint, true>)
295
+ if (!payment) return true
296
+ return payment.method === binding.method && payment.intent === binding.intent
297
+ }
@@ -193,6 +193,15 @@ describe('matchPath', () => {
193
193
  expect(result).toMatchObject({ key: 'POST /v1/*' })
194
194
  })
195
195
 
196
+ // TODO (brendanryan): Relax this if `matchPath()` gains method-aware disambiguation.
197
+ test('error: returns null when multiple paid routes share the same path', () => {
198
+ const routes = {
199
+ 'GET /v1/stream': { pay: () => {} },
200
+ 'POST /v1/stream': { pay: () => {} },
201
+ }
202
+ expect(Route.matchPath(routes, '/v1/stream', paidOnly)).toBeNull()
203
+ })
204
+
196
205
  test('error: returns null when all routes are free', () => {
197
206
  const routes = {
198
207
  'GET /v1/models': true,