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.
- package/CHANGELOG.md +13 -0
- package/dist/Credential.d.ts +12 -0
- package/dist/Credential.d.ts.map +1 -1
- package/dist/Credential.js +22 -4
- package/dist/Credential.js.map +1 -1
- package/dist/Method.d.ts +4 -0
- package/dist/Method.d.ts.map +1 -1
- package/dist/Method.js +2 -1
- package/dist/Method.js.map +1 -1
- package/dist/proxy/Proxy.d.ts.map +1 -1
- package/dist/proxy/Proxy.js +52 -8
- package/dist/proxy/Proxy.js.map +1 -1
- package/dist/proxy/internal/Route.d.ts.map +1 -1
- package/dist/proxy/internal/Route.js +7 -3
- package/dist/proxy/internal/Route.js.map +1 -1
- package/dist/server/Mppx.d.ts.map +1 -1
- package/dist/server/Mppx.js +90 -71
- package/dist/server/Mppx.js.map +1 -1
- package/dist/server/Transport.d.ts +5 -1
- package/dist/server/Transport.d.ts.map +1 -1
- package/dist/server/Transport.js +52 -7
- package/dist/server/Transport.js.map +1 -1
- package/dist/server/internal/html/config.d.ts +7 -0
- package/dist/server/internal/html/config.d.ts.map +1 -0
- package/dist/server/internal/html/config.js +3 -0
- package/dist/server/internal/html/config.js.map +1 -0
- package/dist/server/internal/html/serviceWorker.gen.d.ts +2 -0
- package/dist/server/internal/html/serviceWorker.gen.d.ts.map +1 -0
- package/dist/server/internal/html/serviceWorker.gen.js +3 -0
- package/dist/server/internal/html/serviceWorker.gen.js.map +1 -0
- package/dist/stripe/server/Charge.d.ts +5 -0
- package/dist/stripe/server/Charge.d.ts.map +1 -1
- package/dist/stripe/server/Charge.js +14 -6
- package/dist/stripe/server/Charge.js.map +1 -1
- package/dist/stripe/server/internal/html.gen.d.ts +2 -0
- package/dist/stripe/server/internal/html.gen.d.ts.map +1 -0
- package/dist/stripe/server/internal/html.gen.js +3 -0
- package/dist/stripe/server/internal/html.gen.js.map +1 -0
- package/dist/tempo/server/Charge.d.ts +2 -0
- package/dist/tempo/server/Charge.d.ts.map +1 -1
- package/dist/tempo/server/Charge.js +15 -9
- package/dist/tempo/server/Charge.js.map +1 -1
- package/dist/tempo/server/Session.d.ts.map +1 -1
- package/dist/tempo/server/Session.js +3 -2
- package/dist/tempo/server/Session.js.map +1 -1
- package/dist/tempo/server/internal/html.gen.d.ts +2 -0
- package/dist/tempo/server/internal/html.gen.d.ts.map +1 -0
- package/dist/tempo/server/internal/html.gen.js +3 -0
- package/dist/tempo/server/internal/html.gen.js.map +1 -0
- 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 +45 -58
- package/dist/tempo/server/internal/transport.js.map +1 -1
- package/package.json +2 -2
- package/src/Credential.ts +28 -4
- package/src/Method.ts +6 -1
- package/src/env.d.ts +1 -0
- package/src/mcp-sdk/server/Transport.test.ts +6 -0
- package/src/proxy/Proxy.test.ts +188 -1
- package/src/proxy/Proxy.ts +58 -9
- package/src/proxy/internal/Route.test.ts +9 -0
- package/src/proxy/internal/Route.ts +5 -2
- package/src/server/Mppx.test.ts +171 -18
- package/src/server/Mppx.ts +120 -79
- package/src/server/Transport.test.ts +16 -2
- package/src/server/Transport.ts +61 -7
- package/src/server/internal/html/config.ts +8 -0
- package/src/server/internal/html/serviceWorker.client.ts +28 -0
- package/src/server/internal/html/serviceWorker.gen.ts +2 -0
- package/src/server/internal/html/serviceWorker.ts +27 -0
- package/src/server/internal/html/tsconfig.worker.client.json +8 -0
- package/src/server/internal/html/tsconfig.worker.json +8 -0
- package/src/stripe/server/Charge.ts +19 -5
- package/src/stripe/server/internal/html/main.ts +106 -0
- package/src/stripe/server/internal/html/node_modules/.bin/mppx.src +21 -0
- package/src/stripe/server/internal/html/package.json +9 -0
- package/src/stripe/server/internal/html/stripe-js-pure.d.ts +7 -0
- package/src/stripe/server/internal/html/tsconfig.json +8 -0
- package/src/stripe/server/internal/html.gen.ts +2 -0
- package/src/tempo/server/Charge.ts +20 -8
- package/src/tempo/server/Session.ts +3 -2
- package/src/tempo/server/internal/html/main.ts +71 -0
- package/src/tempo/server/internal/html/node_modules/.bin/mppx.src +21 -0
- package/src/tempo/server/internal/html/package.json +10 -0
- package/src/tempo/server/internal/html/tsconfig.json +8 -0
- package/src/tempo/server/internal/html.gen.ts +2 -0
- package/src/tempo/server/internal/transport.test.ts +37 -31
- package/src/tempo/server/internal/transport.ts +44 -58
- 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 +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;
|
|
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
|
-
|
|
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
|
|
62
|
+
channelId,
|
|
73
63
|
challengeId,
|
|
74
|
-
tickCost
|
|
64
|
+
tickCost,
|
|
75
65
|
pollIntervalMs: pollingInterval,
|
|
76
66
|
generate,
|
|
77
|
-
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
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":"AAAA;;;;;;GAMG;AACH,OAAO,KAAK,SAAS,MAAM,8BAA8B,CAAA;AACzD,OAAO,KAAK,YAAY,MAAM,+BAA+B,CAAA;AAC7D,OAAO,KAAK,QAAQ,MAAM,sBAAsB,CAAA;
|
|
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.
|
|
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": "
|
|
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
|
|
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
|
|
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
|
|
135
|
+
if (!header) throw new MissingAuthorizationHeaderError()
|
|
112
136
|
const payment = extractPaymentScheme(header)
|
|
113
|
-
if (!payment) throw new
|
|
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
|
@@ -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
|
})
|
package/src/proxy/Proxy.test.ts
CHANGED
|
@@ -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({
|
package/src/proxy/Proxy.ts
CHANGED
|
@@ -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
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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,
|