mppx 0.5.16 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/dist/cli/cli.d.ts.map +1 -1
- package/dist/cli/cli.js +30 -1
- package/dist/cli/cli.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 +38 -1
- package/dist/server/Mppx.d.ts.map +1 -1
- package/dist/server/Mppx.js +70 -1
- 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/stripe/server/internal/html.gen.d.ts +1 -1
- package/dist/stripe/server/internal/html.gen.js +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 +4 -14
- package/dist/tempo/server/internal/transport.js.map +1 -1
- package/package.json +3 -3
- package/src/cli/cli.test.ts +36 -7
- package/src/cli/cli.ts +33 -1
- 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 +926 -1
- package/src/server/Mppx.ts +141 -2
- package/src/server/Transport.test.ts +2 -1
- package/src/stripe/server/Charge.ts +7 -1
- package/src/stripe/server/internal/html/package.json +1 -1
- package/src/stripe/server/internal/html.gen.ts +1 -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/package.json +1 -1
- 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 +42 -2
- package/src/tempo/server/internal/transport.ts +4 -16
|
@@ -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
|
});
|
|
@@ -89,7 +90,8 @@ export function sse(options) {
|
|
|
89
90
|
response: response,
|
|
90
91
|
challengeId: verifiedChallengeId,
|
|
91
92
|
});
|
|
92
|
-
|
|
93
|
+
const request = envelope?.capturedRequest ?? captureRequestBodyProbe(input);
|
|
94
|
+
if (!shouldChargePlainResponse(request, payload)) {
|
|
93
95
|
return baseResponse;
|
|
94
96
|
}
|
|
95
97
|
const currentReceipt = receipt;
|
|
@@ -220,16 +222,4 @@ function resolveMeteredGenerate(value, unitType) {
|
|
|
220
222
|
function isNullBodyStatus(status) {
|
|
221
223
|
return [101, 204, 205, 304].includes(status);
|
|
222
224
|
}
|
|
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
225
|
//# 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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mppx",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.6.0",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"files": [
|
|
@@ -123,8 +123,8 @@
|
|
|
123
123
|
}
|
|
124
124
|
},
|
|
125
125
|
"dependencies": {
|
|
126
|
-
"incur": "^0.3.
|
|
127
|
-
"ox": "0.14.
|
|
126
|
+
"incur": "^0.3.25",
|
|
127
|
+
"ox": "0.14.15",
|
|
128
128
|
"zod": "^4.3.6"
|
|
129
129
|
},
|
|
130
130
|
"repository": {
|
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
|
|
|
@@ -1118,6 +1126,27 @@ describe.skipIf(!!process.env.CI)('account', () => {
|
|
|
1118
1126
|
expect(result.stdout).toContain('not found')
|
|
1119
1127
|
})
|
|
1120
1128
|
|
|
1129
|
+
// --- account export ---
|
|
1130
|
+
|
|
1131
|
+
test('export: prints private key for existing account', () => {
|
|
1132
|
+
const name = `${prefix}_export`
|
|
1133
|
+
createAccount(name)
|
|
1134
|
+
const result = accountRun(['account', 'export', '--account', name])
|
|
1135
|
+
expect(result.status).toBe(0)
|
|
1136
|
+
|
|
1137
|
+
const privateKey = result.stdout.match(/0x[0-9a-fA-F]{64}/)?.[0]
|
|
1138
|
+
expect(privateKey).toBeDefined()
|
|
1139
|
+
|
|
1140
|
+
const view = accountRun(['account', 'view', '--account', name])
|
|
1141
|
+
expect(view.stdout).toContain(privateKeyToAccount(privateKey as `0x${string}`).address)
|
|
1142
|
+
})
|
|
1143
|
+
|
|
1144
|
+
test('export: missing account exits non-zero', () => {
|
|
1145
|
+
const result = accountRun(['account', 'export', '--account', `${prefix}_missing_export`])
|
|
1146
|
+
expect(result.status).not.toBe(0)
|
|
1147
|
+
expect(result.stdout).toContain('not found')
|
|
1148
|
+
})
|
|
1149
|
+
|
|
1121
1150
|
// --- account list ---
|
|
1122
1151
|
|
|
1123
1152
|
test('list: includes created accounts', () => {
|
package/src/cli/cli.ts
CHANGED
|
@@ -497,7 +497,7 @@ const cli = Cli.create('mppx', {
|
|
|
497
497
|
})
|
|
498
498
|
|
|
499
499
|
const account = Cli.create('account', {
|
|
500
|
-
description: 'Manage accounts (create, default, delete, fund, list, view)',
|
|
500
|
+
description: 'Manage accounts (create, default, delete, export, fund, list, view)',
|
|
501
501
|
})
|
|
502
502
|
.command('create', {
|
|
503
503
|
description: 'Create new account',
|
|
@@ -713,6 +713,38 @@ const account = Cli.create('account', {
|
|
|
713
713
|
}
|
|
714
714
|
},
|
|
715
715
|
})
|
|
716
|
+
.command('export', {
|
|
717
|
+
description: 'Export the private key for a local account',
|
|
718
|
+
options: z.object({
|
|
719
|
+
account: z.string().optional().describe('Account name (env: MPPX_ACCOUNT)'),
|
|
720
|
+
}),
|
|
721
|
+
alias: { account: 'a' },
|
|
722
|
+
async run(c) {
|
|
723
|
+
const accountName = resolveAccountName(c.options.account)
|
|
724
|
+
|
|
725
|
+
if (isTempoAccount(accountName)) {
|
|
726
|
+
return c.error({
|
|
727
|
+
code: 'UNSUPPORTED_ACCOUNT',
|
|
728
|
+
message: `Account "${accountName}" is managed by Tempo wallet and does not expose a private key via mppx.`,
|
|
729
|
+
exitCode: 2,
|
|
730
|
+
})
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
const key = await createKeychain(accountName).get()
|
|
734
|
+
if (!key) {
|
|
735
|
+
if (c.options.account)
|
|
736
|
+
return c.error({
|
|
737
|
+
code: 'ACCOUNT_NOT_FOUND',
|
|
738
|
+
message: `Account "${accountName}" not found.`,
|
|
739
|
+
exitCode: 69,
|
|
740
|
+
})
|
|
741
|
+
else
|
|
742
|
+
return c.error({ code: 'ACCOUNT_NOT_FOUND', message: 'No account found.', exitCode: 69 })
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
console.log(key)
|
|
746
|
+
},
|
|
747
|
+
})
|
|
716
748
|
.command('view', {
|
|
717
749
|
description: 'View account address',
|
|
718
750
|
options: z.object({
|
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
|
+
}
|