mppx 0.5.17 → 0.6.1
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 +22 -0
- package/dist/Method.d.ts +2 -0
- package/dist/Method.d.ts.map +1 -1
- package/dist/Method.js.map +1 -1
- package/dist/client/Mppx.d.ts +2 -0
- package/dist/client/Mppx.d.ts.map +1 -1
- package/dist/client/Mppx.js +4 -1
- package/dist/client/Mppx.js.map +1 -1
- package/dist/client/index.d.ts +1 -0
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +1 -0
- package/dist/client/index.js.map +1 -1
- package/dist/client/internal/Fetch.d.ts +4 -0
- package/dist/client/internal/Fetch.d.ts.map +1 -1
- package/dist/client/internal/Fetch.js +43 -5
- package/dist/client/internal/Fetch.js.map +1 -1
- package/dist/server/Mppx.d.ts +45 -1
- package/dist/server/Mppx.d.ts.map +1 -1
- package/dist/server/Mppx.js +139 -16
- package/dist/server/Mppx.js.map +1 -1
- package/dist/stripe/server/Charge.d.ts.map +1 -1
- package/dist/stripe/server/Charge.js +8 -1
- package/dist/stripe/server/Charge.js.map +1 -1
- package/dist/tempo/server/Charge.d.ts.map +1 -1
- package/dist/tempo/server/Charge.js +15 -4
- package/dist/tempo/server/Charge.js.map +1 -1
- package/dist/tempo/server/Session.d.ts +39 -38
- package/dist/tempo/server/Session.d.ts.map +1 -1
- package/dist/tempo/server/Session.js +14 -24
- package/dist/tempo/server/Session.js.map +1 -1
- package/dist/tempo/server/internal/html.gen.d.ts +1 -1
- package/dist/tempo/server/internal/html.gen.d.ts.map +1 -1
- package/dist/tempo/server/internal/html.gen.js +1 -1
- package/dist/tempo/server/internal/html.gen.js.map +1 -1
- package/dist/tempo/server/internal/request-body.d.ts +8 -0
- package/dist/tempo/server/internal/request-body.d.ts.map +1 -0
- package/dist/tempo/server/internal/request-body.js +27 -0
- package/dist/tempo/server/internal/request-body.js.map +1 -0
- package/dist/tempo/server/internal/transport.d.ts.map +1 -1
- package/dist/tempo/server/internal/transport.js +6 -17
- package/dist/tempo/server/internal/transport.js.map +1 -1
- package/package.json +1 -1
- package/src/Method.ts +2 -0
- package/src/cli/cli.test.ts +15 -7
- package/src/client/Mppx.ts +11 -2
- package/src/client/index.ts +1 -0
- package/src/client/internal/Fetch.browser.test.ts +58 -0
- package/src/client/internal/Fetch.test.ts +173 -0
- package/src/client/internal/Fetch.ts +62 -3
- package/src/server/Mppx.test-d.ts +36 -0
- package/src/server/Mppx.test.ts +1073 -1
- package/src/server/Mppx.ts +241 -22
- package/src/server/Transport.test.ts +2 -1
- package/src/stripe/server/Charge.ts +7 -1
- package/src/tempo/server/Charge.ts +15 -4
- package/src/tempo/server/Session.test.ts +68 -0
- package/src/tempo/server/Session.ts +15 -35
- package/src/tempo/server/internal/html.gen.ts +1 -1
- package/src/tempo/server/internal/request-body.test.ts +142 -0
- package/src/tempo/server/internal/request-body.ts +37 -0
- package/src/tempo/server/internal/transport.test.ts +126 -2
- package/src/tempo/server/internal/transport.ts +7 -19
|
@@ -1 +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,
|
|
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,sokcAAsokc,CAAA"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type * as Method from '../../../Method.js';
|
|
2
|
+
import type { SessionCredentialPayload } from '../../session/Types.js';
|
|
3
|
+
export type RequestBodyProbe = Pick<Method.CapturedRequest, 'headers' | 'hasBody' | 'method'>;
|
|
4
|
+
export declare function captureRequestBodyProbe(input: Request): RequestBodyProbe;
|
|
5
|
+
export declare function hasCapturedRequestBody(input: Pick<RequestBodyProbe, 'headers' | 'hasBody'>): boolean;
|
|
6
|
+
export declare function isSessionContentRequest(input: RequestBodyProbe): boolean;
|
|
7
|
+
export declare function shouldChargePlainResponse(input: RequestBodyProbe, payload: Partial<SessionCredentialPayload>): boolean;
|
|
8
|
+
//# sourceMappingURL=request-body.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"request-body.d.ts","sourceRoot":"","sources":["../../../../src/tempo/server/internal/request-body.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,MAAM,oBAAoB,CAAA;AACjD,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,wBAAwB,CAAA;AAEtE,MAAM,MAAM,gBAAgB,GAAG,IAAI,CAAC,MAAM,CAAC,eAAe,EAAE,SAAS,GAAG,SAAS,GAAG,QAAQ,CAAC,CAAA;AAE7F,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,OAAO,GAAG,gBAAgB,CAMxE;AAED,wBAAgB,sBAAsB,CACpC,KAAK,EAAE,IAAI,CAAC,gBAAgB,EAAE,SAAS,GAAG,SAAS,CAAC,GACnD,OAAO,CAOT;AAED,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,gBAAgB,GAAG,OAAO,CAIxE;AAED,wBAAgB,yBAAyB,CACvC,KAAK,EAAE,gBAAgB,EACvB,OAAO,EAAE,OAAO,CAAC,wBAAwB,CAAC,GACzC,OAAO,CAGT"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export function captureRequestBodyProbe(input) {
|
|
2
|
+
return {
|
|
3
|
+
headers: input.headers,
|
|
4
|
+
hasBody: input.body !== null,
|
|
5
|
+
method: input.method,
|
|
6
|
+
};
|
|
7
|
+
}
|
|
8
|
+
export function hasCapturedRequestBody(input) {
|
|
9
|
+
const contentLength = input.headers.get('content-length');
|
|
10
|
+
const headerIndicatesBody = (contentLength !== null && contentLength !== '0') || input.headers.has('transfer-encoding');
|
|
11
|
+
if (input.hasBody === true)
|
|
12
|
+
return true;
|
|
13
|
+
return headerIndicatesBody;
|
|
14
|
+
}
|
|
15
|
+
export function isSessionContentRequest(input) {
|
|
16
|
+
if (input.method === 'HEAD')
|
|
17
|
+
return false;
|
|
18
|
+
if (input.method !== 'POST')
|
|
19
|
+
return true;
|
|
20
|
+
return hasCapturedRequestBody(input);
|
|
21
|
+
}
|
|
22
|
+
export function shouldChargePlainResponse(input, payload) {
|
|
23
|
+
if (payload.action === 'close' || payload.action === 'topUp')
|
|
24
|
+
return false;
|
|
25
|
+
return isSessionContentRequest(input);
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=request-body.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"request-body.js","sourceRoot":"","sources":["../../../../src/tempo/server/internal/request-body.ts"],"names":[],"mappings":"AAKA,MAAM,UAAU,uBAAuB,CAAC,KAAc;IACpD,OAAO;QACL,OAAO,EAAE,KAAK,CAAC,OAAO;QACtB,OAAO,EAAE,KAAK,CAAC,IAAI,KAAK,IAAI;QAC5B,MAAM,EAAE,KAAK,CAAC,MAAM;KACrB,CAAA;AACH,CAAC;AAED,MAAM,UAAU,sBAAsB,CACpC,KAAoD;IAEpD,MAAM,aAAa,GAAG,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAA;IACzD,MAAM,mBAAmB,GACvB,CAAC,aAAa,KAAK,IAAI,IAAI,aAAa,KAAK,GAAG,CAAC,IAAI,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,mBAAmB,CAAC,CAAA;IAE7F,IAAI,KAAK,CAAC,OAAO,KAAK,IAAI;QAAE,OAAO,IAAI,CAAA;IACvC,OAAO,mBAAmB,CAAA;AAC5B,CAAC;AAED,MAAM,UAAU,uBAAuB,CAAC,KAAuB;IAC7D,IAAI,KAAK,CAAC,MAAM,KAAK,MAAM;QAAE,OAAO,KAAK,CAAA;IACzC,IAAI,KAAK,CAAC,MAAM,KAAK,MAAM;QAAE,OAAO,IAAI,CAAA;IACxC,OAAO,sBAAsB,CAAC,KAAK,CAAC,CAAA;AACtC,CAAC;AAED,MAAM,UAAU,yBAAyB,CACvC,KAAuB,EACvB,OAA0C;IAE1C,IAAI,OAAO,CAAC,MAAM,KAAK,OAAO,IAAI,OAAO,CAAC,MAAM,KAAK,OAAO;QAAE,OAAO,KAAK,CAAA;IAC1E,OAAO,uBAAuB,CAAC,KAAK,CAAC,CAAA;AACvC,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"transport.d.ts","sourceRoot":"","sources":["../../../../src/tempo/server/internal/transport.ts"],"names":[],"mappings":"AASA,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":"AASA,OAAO,KAAK,SAAS,MAAM,8BAA8B,CAAA;AACzD,OAAO,KAAK,YAAY,MAAM,+BAA+B,CAAA;AAC7D,OAAO,KAAK,QAAQ,MAAM,sBAAsB,CAAA;AAIhD,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,CAuKpF;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"}
|
|
@@ -10,6 +10,7 @@ import * as Errors from '../../../Errors.js';
|
|
|
10
10
|
import * as Transport from '../../../server/Transport.js';
|
|
11
11
|
import * as ChannelStore from '../../session/ChannelStore.js';
|
|
12
12
|
import * as Sse_core from '../../session/Sse.js';
|
|
13
|
+
import { captureRequestBodyProbe, shouldChargePlainResponse } from './request-body.js';
|
|
13
14
|
/**
|
|
14
15
|
* Creates a Tempo-metered SSE transport.
|
|
15
16
|
*
|
|
@@ -35,8 +36,8 @@ export function sse(options) {
|
|
|
35
36
|
name: 'sse',
|
|
36
37
|
captureRequest(request) {
|
|
37
38
|
return (base.captureRequest?.(request) ?? {
|
|
38
|
-
hasBody: request.body !== null,
|
|
39
39
|
headers: new Headers(request.headers),
|
|
40
|
+
hasBody: request.body !== null,
|
|
40
41
|
method: request.method,
|
|
41
42
|
url: new URL(request.url),
|
|
42
43
|
});
|
|
@@ -50,14 +51,13 @@ export function sse(options) {
|
|
|
50
51
|
respondReceipt({ credential, envelope, receipt, response, challengeId, input }) {
|
|
51
52
|
const verifiedCredential = envelope?.credential ?? credential;
|
|
52
53
|
const verifiedChallengeId = envelope?.challenge.id ?? challengeId;
|
|
54
|
+
const verifiedRequest = envelope?.request ?? verifiedCredential.challenge.request;
|
|
53
55
|
const payload = verifiedCredential.payload;
|
|
54
56
|
if (!payload.channelId)
|
|
55
57
|
throw new Error('No SSE context available');
|
|
56
58
|
const channelId = payload.channelId;
|
|
57
59
|
const tickCost = BigInt(verifiedCredential.challenge.request.amount);
|
|
58
|
-
const unitType = typeof
|
|
59
|
-
? verifiedCredential.challenge.request.unitType
|
|
60
|
-
: undefined;
|
|
60
|
+
const unitType = typeof verifiedRequest.unitType === 'string' ? verifiedRequest.unitType : undefined;
|
|
61
61
|
// Auto-detect upstream SSE responses and parse them into an
|
|
62
62
|
// AsyncIterable so they flow through the metered pipeline.
|
|
63
63
|
// This lets proxy consumers simply pass `result.withReceipt(upstreamRes)`
|
|
@@ -89,7 +89,8 @@ export function sse(options) {
|
|
|
89
89
|
response: response,
|
|
90
90
|
challengeId: verifiedChallengeId,
|
|
91
91
|
});
|
|
92
|
-
|
|
92
|
+
const request = envelope?.capturedRequest ?? captureRequestBodyProbe(input);
|
|
93
|
+
if (!shouldChargePlainResponse(request, payload)) {
|
|
93
94
|
return baseResponse;
|
|
94
95
|
}
|
|
95
96
|
const currentReceipt = receipt;
|
|
@@ -220,16 +221,4 @@ function resolveMeteredGenerate(value, unitType) {
|
|
|
220
221
|
function isNullBodyStatus(status) {
|
|
221
222
|
return [101, 204, 205, 304].includes(status);
|
|
222
223
|
}
|
|
223
|
-
function shouldChargePlainResponse(input, payload) {
|
|
224
|
-
if (payload.action === 'close' || payload.action === 'topUp')
|
|
225
|
-
return false;
|
|
226
|
-
if (input.method !== 'POST')
|
|
227
|
-
return true;
|
|
228
|
-
const contentLength = input.headers.get('content-length');
|
|
229
|
-
if (contentLength !== null && contentLength !== '0')
|
|
230
|
-
return true;
|
|
231
|
-
if (input.headers.has('transfer-encoding'))
|
|
232
|
-
return true;
|
|
233
|
-
return false;
|
|
234
|
-
}
|
|
235
224
|
//# sourceMappingURL=transport.js.map
|
|
@@ -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,uBAAuB,CAAA;AAClD,OAAO,KAAK,MAAM,MAAM,oBAAoB,CAAA;AAC5C,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
package/src/Method.ts
CHANGED
|
@@ -88,6 +88,8 @@ export type VerifiedChallengeEnvelope<
|
|
|
88
88
|
payload,
|
|
89
89
|
Challenge.Challenge<request, intent, MethodName>
|
|
90
90
|
>
|
|
91
|
+
/** The authoritative route request after defaults and request-hook transforms. */
|
|
92
|
+
readonly request: request
|
|
91
93
|
}
|
|
92
94
|
|
|
93
95
|
/** Request hook parameters for a single method. */
|
package/src/cli/cli.test.ts
CHANGED
|
@@ -28,18 +28,22 @@ import cli from './cli.js'
|
|
|
28
28
|
|
|
29
29
|
const testPrivateKey = generatePrivateKey()
|
|
30
30
|
const testAccount = privateKeyToAccount(testPrivateKey)
|
|
31
|
+
const cliTestXdgDataHome = fs.mkdtempSync(path.join(os.tmpdir(), 'mppx-cli-xdg-'))
|
|
32
|
+
|
|
33
|
+
afterAll(() => {
|
|
34
|
+
fs.rmSync(cliTestXdgDataHome, { recursive: true, force: true })
|
|
35
|
+
})
|
|
31
36
|
|
|
32
37
|
async function serve(argv: string[], options?: { env?: Record<string, string | undefined> }) {
|
|
33
38
|
let output = ''
|
|
34
39
|
let stderr = ''
|
|
35
40
|
let exitCode: number | undefined
|
|
36
41
|
const saved: Record<string, string | undefined> = {}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
}
|
|
42
|
+
const env = { XDG_DATA_HOME: cliTestXdgDataHome, ...options?.env }
|
|
43
|
+
for (const [key, value] of Object.entries(env)) {
|
|
44
|
+
saved[key] = process.env[key]
|
|
45
|
+
if (value === undefined) delete process.env[key]
|
|
46
|
+
else process.env[key] = value
|
|
43
47
|
}
|
|
44
48
|
const origStdoutWrite = process.stdout.write
|
|
45
49
|
const origStderrWrite = process.stderr.write
|
|
@@ -1053,7 +1057,11 @@ describe('stripe charge', () => {
|
|
|
1053
1057
|
describe.skipIf(!!process.env.CI)('account', () => {
|
|
1054
1058
|
const binPath = path.resolve(import.meta.dirname, '../bin.ts')
|
|
1055
1059
|
const cwd = path.resolve(import.meta.dirname, '../..')
|
|
1056
|
-
const accountEnv = {
|
|
1060
|
+
const accountEnv = {
|
|
1061
|
+
...process.env,
|
|
1062
|
+
NODE_NO_WARNINGS: '1',
|
|
1063
|
+
XDG_DATA_HOME: cliTestXdgDataHome,
|
|
1064
|
+
}
|
|
1057
1065
|
const prefix = `__mppx_test_${Date.now()}`
|
|
1058
1066
|
const createdAccounts: string[] = []
|
|
1059
1067
|
|
package/src/client/Mppx.ts
CHANGED
|
@@ -58,10 +58,16 @@ export function create<
|
|
|
58
58
|
Response
|
|
59
59
|
>,
|
|
60
60
|
>(config: create.Config<methods, transport>): Mppx<methods, transport> {
|
|
61
|
-
const {
|
|
61
|
+
const {
|
|
62
|
+
onChallenge,
|
|
63
|
+
polyfill = true,
|
|
64
|
+
acceptPaymentPolicy = polyfill && typeof globalThis.location !== 'undefined'
|
|
65
|
+
? 'same-origin'
|
|
66
|
+
: 'always',
|
|
67
|
+
transport = Transport.http() as transport,
|
|
68
|
+
} = config
|
|
62
69
|
|
|
63
70
|
const rawFetch = config.fetch ?? globalThis.fetch
|
|
64
|
-
|
|
65
71
|
const methods = config.methods.flat() as unknown as FlattenMethods<methods>
|
|
66
72
|
const acceptPayment = AcceptPayment.resolve(methods, config.paymentPreferences)
|
|
67
73
|
|
|
@@ -70,6 +76,7 @@ export function create<
|
|
|
70
76
|
>['onChallenge']
|
|
71
77
|
const config_fetch = {
|
|
72
78
|
acceptPayment,
|
|
79
|
+
acceptPaymentPolicy,
|
|
73
80
|
...(config.fetch && { fetch: config.fetch }),
|
|
74
81
|
...(resolvedOnChallenge && { onChallenge: resolvedOnChallenge }),
|
|
75
82
|
methods,
|
|
@@ -142,6 +149,8 @@ export declare namespace create {
|
|
|
142
149
|
methods extends Methods = Methods,
|
|
143
150
|
transport extends Transport.Transport = Transport.Transport,
|
|
144
151
|
> = {
|
|
152
|
+
/** Controls when `Accept-Payment` is injected. */
|
|
153
|
+
acceptPaymentPolicy?: Fetch.from.Config['acceptPaymentPolicy'] | undefined
|
|
145
154
|
/** Custom fetch function to wrap. Defaults to `globalThis.fetch`. */
|
|
146
155
|
fetch?: typeof globalThis.fetch
|
|
147
156
|
/** Called when a 402 challenge is received, before credential creation. */
|
package/src/client/index.ts
CHANGED
|
@@ -94,6 +94,64 @@ describe('Fetch.from: browser header normalization', () => {
|
|
|
94
94
|
})
|
|
95
95
|
})
|
|
96
96
|
|
|
97
|
+
describe('Fetch.from: acceptPaymentPolicy (browser)', () => {
|
|
98
|
+
test('policy: "same-origin" injects for same-origin requests', async () => {
|
|
99
|
+
const receivedInits: (RequestInit | undefined)[] = []
|
|
100
|
+
const mockFetch: typeof globalThis.fetch = async (_input, init) => {
|
|
101
|
+
receivedInits.push(init)
|
|
102
|
+
return new Response('OK', { status: 200 })
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const fetch = Fetch.from({
|
|
106
|
+
fetch: mockFetch,
|
|
107
|
+
methods: [noopMethod],
|
|
108
|
+
acceptPaymentPolicy: 'same-origin',
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
await fetch(`${globalThis.location.origin}/api`)
|
|
112
|
+
expect(toHeaders(receivedInits[0]?.headers).get('Accept-Payment')).toBe('test/test')
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
test('policy: "same-origin" skips for cross-origin requests', async () => {
|
|
116
|
+
const receivedInits: (RequestInit | undefined)[] = []
|
|
117
|
+
const mockFetch: typeof globalThis.fetch = async (_input, init) => {
|
|
118
|
+
receivedInits.push(init)
|
|
119
|
+
return new Response('OK', { status: 200 })
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const fetch = Fetch.from({
|
|
123
|
+
fetch: mockFetch,
|
|
124
|
+
methods: [noopMethod],
|
|
125
|
+
acceptPaymentPolicy: 'same-origin',
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
await fetch('https://cross-origin.example.com/api')
|
|
129
|
+
expect(toHeaders(receivedInits[0]?.headers).get('Accept-Payment')).toBeNull()
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
test('policy: "same-origin" still handles 402 on cross-origin', async () => {
|
|
133
|
+
let callCount = 0
|
|
134
|
+
const mockFetch: typeof globalThis.fetch = async (_input, init) => {
|
|
135
|
+
callCount++
|
|
136
|
+
if (callCount === 1) {
|
|
137
|
+
expect(toHeaders(init?.headers).get('Accept-Payment')).toBeNull()
|
|
138
|
+
return make402()
|
|
139
|
+
}
|
|
140
|
+
expect(toHeaders(init?.headers).get('Authorization')).toBe('credential')
|
|
141
|
+
return new Response('OK', { status: 200 })
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const fetch = Fetch.from({
|
|
145
|
+
fetch: mockFetch,
|
|
146
|
+
methods: [noopMethod],
|
|
147
|
+
acceptPaymentPolicy: 'same-origin',
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
const response = await fetch('https://cross-origin.example.com/api')
|
|
151
|
+
expect(response.status).toBe(200)
|
|
152
|
+
})
|
|
153
|
+
})
|
|
154
|
+
|
|
97
155
|
describe('Fetch.polyfill / restore: browser', () => {
|
|
98
156
|
test('restore is a no-op when polyfill was never called', () => {
|
|
99
157
|
const before = globalThis.fetch
|
|
@@ -907,6 +907,179 @@ describe('Fetch.from: 402 retry path', () => {
|
|
|
907
907
|
})
|
|
908
908
|
})
|
|
909
909
|
|
|
910
|
+
describe('Fetch.from: acceptPaymentPolicy', () => {
|
|
911
|
+
test('policy: "always" injects Accept-Payment on all requests', async () => {
|
|
912
|
+
const receivedInits: (RequestInit | undefined)[] = []
|
|
913
|
+
const mockFetch: typeof globalThis.fetch = async (_input, init) => {
|
|
914
|
+
receivedInits.push(init)
|
|
915
|
+
return new Response('OK', { status: 200 })
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
const fetch = Fetch.from({
|
|
919
|
+
fetch: mockFetch,
|
|
920
|
+
methods: [noopMethod],
|
|
921
|
+
acceptPaymentPolicy: 'always',
|
|
922
|
+
})
|
|
923
|
+
|
|
924
|
+
await fetch('https://cross-origin.com/api')
|
|
925
|
+
expect(new Headers(receivedInits[0]?.headers).get('Accept-Payment')).toBe('test/test')
|
|
926
|
+
})
|
|
927
|
+
|
|
928
|
+
test('policy: "never" skips Accept-Payment on all requests', async () => {
|
|
929
|
+
const receivedInits: (RequestInit | undefined)[] = []
|
|
930
|
+
const mockFetch: typeof globalThis.fetch = async (_input, init) => {
|
|
931
|
+
receivedInits.push(init)
|
|
932
|
+
return new Response('OK', { status: 200 })
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
const fetch = Fetch.from({
|
|
936
|
+
fetch: mockFetch,
|
|
937
|
+
methods: [noopMethod],
|
|
938
|
+
acceptPaymentPolicy: 'never',
|
|
939
|
+
})
|
|
940
|
+
|
|
941
|
+
await fetch('https://example.com/api')
|
|
942
|
+
expect(new Headers(receivedInits[0]?.headers).get('Accept-Payment')).toBeNull()
|
|
943
|
+
})
|
|
944
|
+
|
|
945
|
+
test('policy: "same-origin" injects when no globalThis.location (server-side)', async () => {
|
|
946
|
+
const receivedInits: (RequestInit | undefined)[] = []
|
|
947
|
+
const mockFetch: typeof globalThis.fetch = async (_input, init) => {
|
|
948
|
+
receivedInits.push(init)
|
|
949
|
+
return new Response('OK', { status: 200 })
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
const fetch = Fetch.from({
|
|
953
|
+
fetch: mockFetch,
|
|
954
|
+
methods: [noopMethod],
|
|
955
|
+
acceptPaymentPolicy: 'same-origin',
|
|
956
|
+
})
|
|
957
|
+
|
|
958
|
+
await fetch('https://example.com/api')
|
|
959
|
+
expect(new Headers(receivedInits[0]?.headers).get('Accept-Payment')).toBe('test/test')
|
|
960
|
+
})
|
|
961
|
+
|
|
962
|
+
test('policy: { origins } injects only for matching origins', async () => {
|
|
963
|
+
const receivedInits: (RequestInit | undefined)[] = []
|
|
964
|
+
const mockFetch: typeof globalThis.fetch = async (_input, init) => {
|
|
965
|
+
receivedInits.push(init)
|
|
966
|
+
return new Response('OK', { status: 200 })
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
const fetch = Fetch.from({
|
|
970
|
+
fetch: mockFetch,
|
|
971
|
+
methods: [noopMethod],
|
|
972
|
+
acceptPaymentPolicy: { origins: ['https://pay.example.com'] },
|
|
973
|
+
})
|
|
974
|
+
|
|
975
|
+
await fetch('https://pay.example.com/resource')
|
|
976
|
+
expect(new Headers(receivedInits[0]?.headers).get('Accept-Payment')).toBe('test/test')
|
|
977
|
+
|
|
978
|
+
await fetch('https://other.example.com/resource')
|
|
979
|
+
expect(new Headers(receivedInits[1]?.headers).get('Accept-Payment')).toBeNull()
|
|
980
|
+
})
|
|
981
|
+
|
|
982
|
+
test('policy: { origins } matches origin regardless of path', async () => {
|
|
983
|
+
const receivedInits: (RequestInit | undefined)[] = []
|
|
984
|
+
const mockFetch: typeof globalThis.fetch = async (_input, init) => {
|
|
985
|
+
receivedInits.push(init)
|
|
986
|
+
return new Response('OK', { status: 200 })
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
const fetch = Fetch.from({
|
|
990
|
+
fetch: mockFetch,
|
|
991
|
+
methods: [noopMethod],
|
|
992
|
+
acceptPaymentPolicy: { origins: ['https://pay.example.com/some/path'] },
|
|
993
|
+
})
|
|
994
|
+
|
|
995
|
+
await fetch('https://pay.example.com/different/path')
|
|
996
|
+
expect(new Headers(receivedInits[0]?.headers).get('Accept-Payment')).toBe('test/test')
|
|
997
|
+
})
|
|
998
|
+
|
|
999
|
+
test('policy: { origins } supports wildcard subdomains', async () => {
|
|
1000
|
+
const receivedInits: (RequestInit | undefined)[] = []
|
|
1001
|
+
const mockFetch: typeof globalThis.fetch = async (_input, init) => {
|
|
1002
|
+
receivedInits.push(init)
|
|
1003
|
+
return new Response('OK', { status: 200 })
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
const fetch = Fetch.from({
|
|
1007
|
+
fetch: mockFetch,
|
|
1008
|
+
methods: [noopMethod],
|
|
1009
|
+
acceptPaymentPolicy: { origins: ['*.example.com'] },
|
|
1010
|
+
})
|
|
1011
|
+
|
|
1012
|
+
await fetch('https://pay.example.com/resource')
|
|
1013
|
+
expect(new Headers(receivedInits[0]?.headers).get('Accept-Payment')).toBe('test/test')
|
|
1014
|
+
|
|
1015
|
+
await fetch('https://api.pay.example.com/resource')
|
|
1016
|
+
expect(new Headers(receivedInits[1]?.headers).get('Accept-Payment')).toBe('test/test')
|
|
1017
|
+
|
|
1018
|
+
await fetch('https://example.com/resource')
|
|
1019
|
+
expect(new Headers(receivedInits[2]?.headers).get('Accept-Payment')).toBe('test/test')
|
|
1020
|
+
|
|
1021
|
+
await fetch('https://notexample.com/resource')
|
|
1022
|
+
expect(new Headers(receivedInits[3]?.headers).get('Accept-Payment')).toBeNull()
|
|
1023
|
+
})
|
|
1024
|
+
|
|
1025
|
+
test('policy: "never" still handles 402 responses', async () => {
|
|
1026
|
+
let callCount = 0
|
|
1027
|
+
const mockFetch: typeof globalThis.fetch = async (_input, init) => {
|
|
1028
|
+
callCount++
|
|
1029
|
+
if (callCount === 1) {
|
|
1030
|
+
expect(new Headers(init?.headers).get('Accept-Payment')).toBeNull()
|
|
1031
|
+
return make402()
|
|
1032
|
+
}
|
|
1033
|
+
expect(new Headers(init?.headers).get('Authorization')).toBe('credential')
|
|
1034
|
+
return new Response('OK', { status: 200 })
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
const fetch = Fetch.from({
|
|
1038
|
+
fetch: mockFetch,
|
|
1039
|
+
methods: [noopMethod],
|
|
1040
|
+
acceptPaymentPolicy: 'never',
|
|
1041
|
+
})
|
|
1042
|
+
|
|
1043
|
+
const response = await fetch('https://example.com/api')
|
|
1044
|
+
expect(response.status).toBe(200)
|
|
1045
|
+
})
|
|
1046
|
+
|
|
1047
|
+
test('policy: explicit Accept-Payment header always takes precedence over "never"', async () => {
|
|
1048
|
+
const receivedInits: (RequestInit | undefined)[] = []
|
|
1049
|
+
const mockFetch: typeof globalThis.fetch = async (_input, init) => {
|
|
1050
|
+
receivedInits.push(init)
|
|
1051
|
+
return new Response('OK', { status: 200 })
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
const fetch = Fetch.from({
|
|
1055
|
+
fetch: mockFetch,
|
|
1056
|
+
methods: [noopMethod],
|
|
1057
|
+
acceptPaymentPolicy: 'never',
|
|
1058
|
+
})
|
|
1059
|
+
|
|
1060
|
+
await fetch('https://example.com/api', {
|
|
1061
|
+
headers: { 'Accept-Payment': 'custom/charge' },
|
|
1062
|
+
})
|
|
1063
|
+
expect(new Headers(receivedInits[0]?.headers).get('Accept-Payment')).toBe('custom/charge')
|
|
1064
|
+
})
|
|
1065
|
+
|
|
1066
|
+
test('defaults to "always" for Fetch.from', async () => {
|
|
1067
|
+
const receivedInits: (RequestInit | undefined)[] = []
|
|
1068
|
+
const mockFetch: typeof globalThis.fetch = async (_input, init) => {
|
|
1069
|
+
receivedInits.push(init)
|
|
1070
|
+
return new Response('OK', { status: 200 })
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
const fetch = Fetch.from({
|
|
1074
|
+
fetch: mockFetch,
|
|
1075
|
+
methods: [noopMethod],
|
|
1076
|
+
})
|
|
1077
|
+
|
|
1078
|
+
await fetch('https://cross-origin.com/api')
|
|
1079
|
+
expect(new Headers(receivedInits[0]?.headers).get('Accept-Payment')).toBe('test/test')
|
|
1080
|
+
})
|
|
1081
|
+
})
|
|
1082
|
+
|
|
910
1083
|
describe('Fetch.from: input passthrough', () => {
|
|
911
1084
|
test('passes URL input through on both initial and retry calls', async () => {
|
|
912
1085
|
let callCount = 0
|
|
@@ -38,7 +38,13 @@ let originalFetch: typeof globalThis.fetch | undefined
|
|
|
38
38
|
export function from<const methods extends readonly Method.AnyClient[]>(
|
|
39
39
|
config: from.Config<methods>,
|
|
40
40
|
): from.Fetch<methods> {
|
|
41
|
-
const {
|
|
41
|
+
const {
|
|
42
|
+
acceptPayment,
|
|
43
|
+
acceptPaymentPolicy = 'always',
|
|
44
|
+
fetch = globalThis.fetch,
|
|
45
|
+
methods,
|
|
46
|
+
onChallenge,
|
|
47
|
+
} = config
|
|
42
48
|
const resolvedAcceptPayment = acceptPayment ?? AcceptPayment.resolve(methods)
|
|
43
49
|
// Always operate on the true underlying fetch to avoid wrapper-on-wrapper stacking,
|
|
44
50
|
// which can duplicate retries and make restore semantics fragile.
|
|
@@ -54,6 +60,7 @@ export function from<const methods extends readonly Method.AnyClient[]>(
|
|
|
54
60
|
callerHeaders,
|
|
55
61
|
paymentPreferences.header,
|
|
56
62
|
hasExplicitAcceptPayment,
|
|
63
|
+
acceptPaymentPolicy,
|
|
57
64
|
)
|
|
58
65
|
const response = await baseFetch(initialRequest.input, initialRequest.init)
|
|
59
66
|
|
|
@@ -108,6 +115,13 @@ export declare namespace from {
|
|
|
108
115
|
type Config<methods extends readonly Method.AnyClient[] = readonly Method.AnyClient[]> = {
|
|
109
116
|
/** Resolved `Accept-Payment` header and selection preferences. */
|
|
110
117
|
acceptPayment?: AcceptPayment.Resolved<methods> | undefined
|
|
118
|
+
/** Controls when `Accept-Payment` is injected. @default 'always' */
|
|
119
|
+
acceptPaymentPolicy?:
|
|
120
|
+
| 'always'
|
|
121
|
+
| 'same-origin'
|
|
122
|
+
| 'never'
|
|
123
|
+
| { origins: readonly string[] }
|
|
124
|
+
| undefined
|
|
111
125
|
/** Custom fetch function to wrap. Defaults to `globalThis.fetch`. */
|
|
112
126
|
fetch?: typeof globalThis.fetch
|
|
113
127
|
/** Array of methods to use. */
|
|
@@ -165,7 +179,11 @@ export function polyfill<const methods extends readonly Method.AnyClient[]>(
|
|
|
165
179
|
}
|
|
166
180
|
|
|
167
181
|
if (!originalFetch) originalFetch = globalThis.fetch
|
|
168
|
-
globalThis.fetch = from({
|
|
182
|
+
globalThis.fetch = from({
|
|
183
|
+
...config,
|
|
184
|
+
acceptPaymentPolicy: config.acceptPaymentPolicy ?? (isBrowser() ? 'same-origin' : 'always'),
|
|
185
|
+
fetch: globalThis.fetch,
|
|
186
|
+
}) as typeof globalThis.fetch
|
|
169
187
|
}
|
|
170
188
|
|
|
171
189
|
export declare namespace polyfill {
|
|
@@ -229,8 +247,10 @@ function prepareInitialRequest<methods extends readonly Method.AnyClient[]>(
|
|
|
229
247
|
callerHeaders: Headers,
|
|
230
248
|
header: string,
|
|
231
249
|
hasExplicitAcceptPayment: boolean,
|
|
250
|
+
policy: NonNullable<from.Config['acceptPaymentPolicy']>,
|
|
232
251
|
): { headers: Headers; init: from.RequestInit<methods> | undefined; input: RequestInfo | URL } {
|
|
233
|
-
const shouldInjectAcceptPayment =
|
|
252
|
+
const shouldInjectAcceptPayment =
|
|
253
|
+
Boolean(header) && !hasExplicitAcceptPayment && shouldInjectForPolicy(input, policy)
|
|
234
254
|
if (!shouldInjectAcceptPayment) return { headers: callerHeaders, init, input }
|
|
235
255
|
|
|
236
256
|
const headers = new Headers(input instanceof Request ? input.headers : undefined)
|
|
@@ -317,3 +337,42 @@ function resolvePaymentPreferences<methods extends readonly Method.AnyClient[]>(
|
|
|
317
337
|
return acceptPayment
|
|
318
338
|
}
|
|
319
339
|
}
|
|
340
|
+
|
|
341
|
+
/** @internal */
|
|
342
|
+
function shouldInjectForPolicy(
|
|
343
|
+
input: RequestInfo | URL,
|
|
344
|
+
policy: NonNullable<from.Config['acceptPaymentPolicy']>,
|
|
345
|
+
): boolean {
|
|
346
|
+
if (policy === 'always') return true
|
|
347
|
+
if (policy === 'never') return false
|
|
348
|
+
|
|
349
|
+
const url = resolveRequestUrl(input)
|
|
350
|
+
|
|
351
|
+
if (policy === 'same-origin') {
|
|
352
|
+
if (!isBrowser()) return true
|
|
353
|
+
return url.origin === globalThis.location.origin
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
return policy.origins.some((origin) => matchesOrigin(url, origin))
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/** @internal Matches an origin pattern, supporting `*.` prefix for subdomain wildcards. */
|
|
360
|
+
function matchesOrigin(url: URL, pattern: string): boolean {
|
|
361
|
+
if (pattern.startsWith('*.')) {
|
|
362
|
+
const suffix = pattern.slice(1) // e.g. ".example.com"
|
|
363
|
+
return url.hostname.endsWith(suffix) || url.hostname === pattern.slice(2)
|
|
364
|
+
}
|
|
365
|
+
return url.origin === new URL(pattern).origin
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/** @internal */
|
|
369
|
+
function isBrowser(): boolean {
|
|
370
|
+
return typeof globalThis.location !== 'undefined'
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/** @internal */
|
|
374
|
+
function resolveRequestUrl(input: RequestInfo | URL): URL {
|
|
375
|
+
if (input instanceof URL) return input
|
|
376
|
+
if (input instanceof Request) return new URL(input.url)
|
|
377
|
+
return new URL(input, isBrowser() ? globalThis.location.href : undefined)
|
|
378
|
+
}
|
|
@@ -133,4 +133,40 @@ describe('Mppx type tests', () => {
|
|
|
133
133
|
test('static Mppx.compose accepts configured handlers', () => {
|
|
134
134
|
expectTypeOf(Mppx.compose).toBeFunction()
|
|
135
135
|
})
|
|
136
|
+
|
|
137
|
+
test('challenge namespace has nested accessors matching methods', () => {
|
|
138
|
+
const mppx = Mppx.create({ methods: [alphaMethod, betaMethod], realm, secretKey })
|
|
139
|
+
|
|
140
|
+
expectTypeOf(mppx.challenge).toBeObject()
|
|
141
|
+
expectTypeOf(mppx.challenge.alpha).toBeObject()
|
|
142
|
+
expectTypeOf(mppx.challenge.alpha.charge).toBeFunction()
|
|
143
|
+
expectTypeOf(mppx.challenge.beta).toBeObject()
|
|
144
|
+
expectTypeOf(mppx.challenge.beta.charge).toBeFunction()
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
test('challenge functions return Promise<Challenge>', () => {
|
|
148
|
+
const mppx = Mppx.create({ methods: [alphaMethod], realm, secretKey })
|
|
149
|
+
|
|
150
|
+
const challenge = mppx.challenge.alpha.charge({
|
|
151
|
+
amount: '100',
|
|
152
|
+
currency: '0x01',
|
|
153
|
+
decimals: 6,
|
|
154
|
+
recipient: '0x02',
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
expectTypeOf(challenge).toMatchTypeOf<Promise<unknown>>()
|
|
158
|
+
|
|
159
|
+
type AwaitedChallenge = Awaited<typeof challenge>
|
|
160
|
+
expectTypeOf<AwaitedChallenge>().toHaveProperty('id')
|
|
161
|
+
expectTypeOf<AwaitedChallenge>().toHaveProperty('realm')
|
|
162
|
+
expectTypeOf<AwaitedChallenge>().toHaveProperty('method')
|
|
163
|
+
expectTypeOf<AwaitedChallenge>().toHaveProperty('intent')
|
|
164
|
+
expectTypeOf<AwaitedChallenge>().toHaveProperty('request')
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
test('verifyCredential exists and returns Promise<Receipt>', () => {
|
|
168
|
+
const mppx = Mppx.create({ methods: [alphaMethod], realm, secretKey })
|
|
169
|
+
|
|
170
|
+
expectTypeOf(mppx.verifyCredential).toBeFunction()
|
|
171
|
+
})
|
|
136
172
|
})
|