mppx 0.15.0 → 0.17.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 +47 -0
- package/dist/Challenge.d.ts +6 -0
- package/dist/Challenge.js +12 -0
- package/dist/Method.d.ts +2 -0
- package/dist/cli/cli.js +143 -131
- package/dist/cli/plugins/index.js +11 -11
- package/dist/cli/validate/payment.d.ts +12 -0
- package/dist/cli/validate/payment.js +137 -14
- package/dist/client/Mppx.d.ts +13 -3
- package/dist/client/Mppx.js +47 -22
- package/dist/client/internal/Fetch.d.ts +11 -0
- package/dist/client/internal/Fetch.js +46 -8
- package/dist/client/internal/ProofRealm.d.ts +21 -0
- package/dist/client/internal/ProofRealm.js +55 -0
- package/dist/client/internal/protocols/Mpp.js +4 -1
- package/dist/{attestation → extensions/identity}/Constants.d.ts +2 -2
- package/dist/{attestation → extensions/identity}/Constants.js +2 -2
- package/dist/extensions/identity/Types.d.ts +21 -0
- package/dist/extensions/identity/index.d.ts +7 -0
- package/dist/extensions/identity/index.js +6 -0
- package/dist/{attestation → extensions/identity}/internal/HttpMessageSignature.d.ts +3 -3
- package/dist/{attestation → extensions/identity}/internal/HttpMessageSignature.js +10 -10
- package/dist/extensions/identity/internal/SigningContext.d.ts +6 -0
- package/dist/{attestation → extensions/identity}/internal/SigningContext.js +3 -3
- package/dist/extensions/identity/internal/Verification.d.ts +9 -0
- package/dist/extensions/identity/internal/Verification.js +10 -0
- package/dist/{tap → extensions/identity/tap}/Constants.js +1 -1
- package/dist/extensions/identity/tap/client/index.d.ts +32 -0
- package/dist/{tap → extensions/identity/tap}/client/index.js +13 -9
- package/dist/extensions/identity/tap/server/index.d.ts +27 -0
- package/dist/{tap → extensions/identity/tap}/server/index.js +12 -4
- package/dist/{web-bot-auth → extensions/identity/web-bot-auth}/Constants.js +1 -1
- package/dist/{web-bot-auth → extensions/identity/web-bot-auth}/client/index.d.ts +6 -5
- package/dist/{web-bot-auth → extensions/identity/web-bot-auth}/client/index.js +5 -5
- package/dist/{web-bot-auth → extensions/identity/web-bot-auth}/server/index.d.ts +6 -5
- package/dist/{web-bot-auth → extensions/identity/web-bot-auth}/server/index.js +6 -4
- package/dist/identity/Client.d.ts +34 -0
- package/dist/identity/Client.js +133 -0
- package/dist/identity/Server.d.ts +59 -0
- package/dist/identity/Server.js +194 -0
- package/dist/identity/Types.d.ts +112 -0
- package/dist/identity/Types.js +2 -0
- package/dist/{attestation → identity}/index.d.ts +2 -2
- package/dist/{attestation → identity}/index.js +1 -2
- package/dist/identity/internal/SigningContext.d.ts +6 -0
- package/dist/identity/internal/SigningContext.js +11 -0
- package/dist/integrations/x402/Evm.d.ts +1 -0
- package/dist/internal/version.d.ts +2 -2
- package/dist/internal/version.js +1 -1
- package/dist/mcp/client/McpClient.d.ts +5 -0
- package/dist/mcp/client/McpClient.js +2 -0
- package/dist/proxy/Proxy.js +10 -5
- package/dist/proxy/Service.d.ts +42 -4
- package/dist/proxy/Service.js +21 -5
- package/dist/server/Mppx.d.ts +27 -8
- package/dist/server/Mppx.js +164 -54
- package/dist/server/internal/challengeMeta.d.ts +6 -0
- package/dist/server/internal/challengeMeta.js +17 -1
- package/dist/stripe/server/internal/html.gen.js +1 -1
- package/dist/tempo/client/Charge.d.ts +11 -0
- package/dist/tempo/client/Charge.js +30 -1
- package/dist/tempo/internal/charge.js +2 -1
- package/dist/tempo/internal/defaults.d.ts +9 -0
- package/dist/tempo/internal/defaults.js +9 -0
- package/dist/tempo/internal/fee-payer.d.ts +0 -2
- package/dist/tempo/internal/fee-payer.js +2 -6
- package/dist/tempo/server/Charge.js +12 -1
- package/dist/tempo/server/Subscription.js +0 -1
- package/dist/tempo/server/internal/html.gen.js +1 -1
- package/dist/tempo/session/client/Runtime.d.ts +2 -0
- package/dist/tempo/session/client/Session.js +1 -1
- package/dist/tempo/session/client/SessionManager.d.ts +6 -0
- package/dist/tempo/session/client/SessionManager.js +14 -7
- package/dist/tempo/session/client/Transports.d.ts +12 -3
- package/dist/tempo/session/client/Transports.js +37 -11
- package/dist/tempo/session/precompile/Chain.js +0 -3
- package/dist/tempo/subscription/Store.js +10 -9
- package/package.json +15 -11
- package/dist/attestation/Client.d.ts +0 -17
- package/dist/attestation/Client.js +0 -28
- package/dist/attestation/Server.d.ts +0 -4
- package/dist/attestation/Server.js +0 -8
- package/dist/attestation/Types.d.ts +0 -76
- package/dist/attestation/internal/SigningContext.d.ts +0 -4
- package/dist/tap/client/index.d.ts +0 -26
- package/dist/tap/server/index.d.ts +0 -20
- /package/dist/{attestation → extensions/identity}/Types.js +0 -0
- /package/dist/{tap → extensions/identity/tap}/Constants.d.ts +0 -0
- /package/dist/{tap → extensions/identity/tap}/Types.d.ts +0 -0
- /package/dist/{tap → extensions/identity/tap}/Types.js +0 -0
- /package/dist/{tap → extensions/identity/tap}/index.d.ts +0 -0
- /package/dist/{tap → extensions/identity/tap}/index.js +0 -0
- /package/dist/{web-bot-auth → extensions/identity/web-bot-auth}/Constants.d.ts +0 -0
- /package/dist/{web-bot-auth → extensions/identity/web-bot-auth}/Types.d.ts +0 -0
- /package/dist/{web-bot-auth → extensions/identity/web-bot-auth}/Types.js +0 -0
- /package/dist/{web-bot-auth → extensions/identity/web-bot-auth}/index.d.ts +0 -0
- /package/dist/{web-bot-auth → extensions/identity/web-bot-auth}/index.js +0 -0
- /package/dist/{web-bot-auth → extensions/identity/web-bot-auth}/internal/JwkThumbprint.d.ts +0 -0
- /package/dist/{web-bot-auth → extensions/identity/web-bot-auth}/internal/JwkThumbprint.js +0 -0
- /package/dist/{web-bot-auth → extensions/identity/web-bot-auth}/internal/SignatureAgent.d.ts +0 -0
- /package/dist/{web-bot-auth → extensions/identity/web-bot-auth}/internal/SignatureAgent.js +0 -0
|
@@ -2,14 +2,18 @@ import * as Challenge from '../../Challenge.js';
|
|
|
2
2
|
import * as Constants from '../../Constants.js';
|
|
3
3
|
import { PaymentOutcomeUnknownError } from '../../Errors.js';
|
|
4
4
|
import * as Expires from '../../Expires.js';
|
|
5
|
+
import * as IdentityClient from '../../identity/Client.js';
|
|
5
6
|
import * as AcceptPayment from '../../internal/AcceptPayment.js';
|
|
6
7
|
import * as Transport from '../Transport.js';
|
|
7
8
|
import * as MethodChallenge from './MethodChallenge.js';
|
|
8
9
|
import * as MethodResponse from './MethodResponse.js';
|
|
10
|
+
import * as ProofRealm from './ProofRealm.js';
|
|
9
11
|
// We tag wrappers with a global symbol so we can recognize wrappers created by mppx,
|
|
10
12
|
// even across multiple module instances/bundles. This lets restore() avoid clobbering
|
|
11
13
|
// an unrelated fetch installed by user code or another library.
|
|
12
14
|
const MPPX_FETCH_WRAPPER = Symbol.for('mppx.fetch.wrapper');
|
|
15
|
+
/** Identity runner of an mppx fetch wrapper, inherited when that wrapper is wrapped again. */
|
|
16
|
+
const MPPX_FETCH_IDENTITY = Symbol.for('mppx.fetch.identity');
|
|
13
17
|
const defaultMaxPaymentRetries = 3;
|
|
14
18
|
const maxErrorBodyBytes = 64 * 1024;
|
|
15
19
|
const errorBodyTimeoutMs = 2_000;
|
|
@@ -101,19 +105,25 @@ async function settleAttempt(prepared, outcome) {
|
|
|
101
105
|
*
|
|
102
106
|
*/
|
|
103
107
|
export function from(config) {
|
|
104
|
-
const { acceptPayment, acceptPaymentPolicy = 'always', fetch = globalThis.fetch, maxPaymentRetries = defaultMaxPaymentRetries, methods, onChallenge, orderChallenges, transport = Transport.http(), } = config;
|
|
108
|
+
const { acceptPayment, acceptPaymentPolicy = 'always', fetch = globalThis.fetch, maxPaymentRetries = defaultMaxPaymentRetries, methods, onChallenge, orderChallenges, transport = Transport.http(), trustedTempoProofRealms, } = config;
|
|
109
|
+
// Rewrapping an mppx fetch keeps its identity provers unless new ones are configured.
|
|
110
|
+
// Only an explicitly passed mppx fetch hands down its provers; the global fetch never does.
|
|
111
|
+
const identity = config.identity ?? (config.fetch && identityOf(config.fetch));
|
|
105
112
|
const events = config.eventDispatcher ?? createEventDispatcher();
|
|
106
113
|
const resolvedAcceptPayment = acceptPayment ?? AcceptPayment.resolve(methods);
|
|
107
114
|
// Always operate on the true underlying fetch to avoid wrapper-on-wrapper stacking,
|
|
108
115
|
// which can duplicate retries and make restore semantics fragile.
|
|
109
116
|
const baseFetch = unwrapFetch(fetch);
|
|
117
|
+
const send = async (input, init, context) => identity
|
|
118
|
+
? (await identity.send(baseFetch, new Request(input, init), context)).response
|
|
119
|
+
: baseFetch(input, init);
|
|
110
120
|
const request = async (input, init, canRefetch) => {
|
|
111
121
|
const callerHeaders = getCallerHeaders(input, init?.headers);
|
|
112
122
|
const hasExplicitAcceptPayment = callerHeaders.has(Constants.Headers.acceptPayment);
|
|
113
123
|
const paymentPreferences = resolvePaymentPreferences(callerHeaders, resolvedAcceptPayment);
|
|
114
124
|
const capturedBody = captureRequestBody(input, init, callerHeaders);
|
|
115
125
|
const initialRequest = prepareInitialRequest(input, init, callerHeaders, paymentPreferences.header, hasExplicitAcceptPayment, acceptPaymentPolicy);
|
|
116
|
-
let response = await
|
|
126
|
+
let response = await send(cloneRequestInput(initialRequest.input), initialRequest.init);
|
|
117
127
|
const transportRequest = withCapturedBody(initialRequest.init, await capturedBody);
|
|
118
128
|
if (!(await transport.isPaymentRequired(response, transportRequest)))
|
|
119
129
|
return response;
|
|
@@ -150,7 +160,9 @@ export function from(config) {
|
|
|
150
160
|
mi = selected.method;
|
|
151
161
|
if (challenge.expires)
|
|
152
162
|
Expires.assert(challenge.expires, challenge.id);
|
|
163
|
+
await identity?.challenge(response, { challenge: selectedChallenge }, resolveRequestUrl(initialRequest.input).href);
|
|
153
164
|
const paymentInput = resolvePaymentRetryInput(response, initialRequest.input, initialRequest.input);
|
|
165
|
+
ProofRealm.assert(selectedChallenge, () => resolveRequestUrl(paymentInput), trustedTempoProofRealms);
|
|
154
166
|
const previousCredential = preparedCredential;
|
|
155
167
|
const key = Challenge.serialize(selectedChallenge);
|
|
156
168
|
const sameAttempt = previousCredential?.method === selected.method && previousCredential?.key === key;
|
|
@@ -179,7 +191,12 @@ export function from(config) {
|
|
|
179
191
|
prepare: () => MethodChallenge.handle(selected.method, {
|
|
180
192
|
challenge: selectedChallenge,
|
|
181
193
|
context: credentialContext,
|
|
182
|
-
|
|
194
|
+
// Management requests (e.g. session top-ups) carry the same identity proofs.
|
|
195
|
+
fetch: identity
|
|
196
|
+
? IdentityClient.wrapFetch(baseFetch, identity, {
|
|
197
|
+
challenge: selectedChallenge,
|
|
198
|
+
})
|
|
199
|
+
: baseFetch,
|
|
183
200
|
input: paymentInput,
|
|
184
201
|
signal: resolveRequestSignal(input, init),
|
|
185
202
|
}),
|
|
@@ -232,14 +249,14 @@ export function from(config) {
|
|
|
232
249
|
transactionHash: MethodResponse.getTransactionHash(selected.method, credential),
|
|
233
250
|
});
|
|
234
251
|
try {
|
|
235
|
-
response = await
|
|
252
|
+
response = await send(paymentInput, {
|
|
236
253
|
...transport.setCredential({
|
|
237
254
|
...fetchInit,
|
|
238
255
|
headers: initialRequest.headers,
|
|
239
256
|
}, credential, { challenge: selectedChallenge }),
|
|
240
257
|
// Runtimes do not strip custom payment headers on cross-origin redirects.
|
|
241
258
|
redirect: 'manual',
|
|
242
|
-
});
|
|
259
|
+
}, { challenge: selectedChallenge });
|
|
243
260
|
}
|
|
244
261
|
catch (cause) {
|
|
245
262
|
outcomeUnknown = unknownOutcome(cause);
|
|
@@ -347,6 +364,8 @@ export function from(config) {
|
|
|
347
364
|
};
|
|
348
365
|
const wrappedFetch = (input, init) => request(input, init, true);
|
|
349
366
|
wrappedFetch[MPPX_FETCH_WRAPPER] = baseFetch;
|
|
367
|
+
if (identity)
|
|
368
|
+
wrappedFetch[MPPX_FETCH_IDENTITY] = identity;
|
|
350
369
|
return wrappedFetch;
|
|
351
370
|
}
|
|
352
371
|
/**
|
|
@@ -380,7 +399,8 @@ export function polyfill(config) {
|
|
|
380
399
|
globalThis.fetch = from({
|
|
381
400
|
...config,
|
|
382
401
|
acceptPaymentPolicy: config.acceptPaymentPolicy ?? (isBrowser() ? 'same-origin' : 'always'),
|
|
383
|
-
|
|
402
|
+
// Unwrapped so a previous polyfill's identity provers are not inherited.
|
|
403
|
+
fetch: unwrapFetch(globalThis.fetch),
|
|
384
404
|
});
|
|
385
405
|
}
|
|
386
406
|
/**
|
|
@@ -586,8 +606,22 @@ function snapshotMethod(method) {
|
|
|
586
606
|
return freezeSnapshot(Object.assign({}, method));
|
|
587
607
|
}
|
|
588
608
|
function snapshotResponse(response) {
|
|
589
|
-
if (response instanceof Response)
|
|
590
|
-
|
|
609
|
+
if (response instanceof Response) {
|
|
610
|
+
try {
|
|
611
|
+
return response.clone();
|
|
612
|
+
}
|
|
613
|
+
catch {
|
|
614
|
+
// Consumed or locked bodies cannot be cloned; retain isolated status, headers and URL.
|
|
615
|
+
const snapshot = new Response(null, {
|
|
616
|
+
headers: response.headers,
|
|
617
|
+
status: response.status,
|
|
618
|
+
statusText: response.statusText,
|
|
619
|
+
});
|
|
620
|
+
for (const key of ['redirected', 'type', 'url'])
|
|
621
|
+
Object.defineProperty(snapshot, key, { value: response[key] });
|
|
622
|
+
return snapshot;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
591
625
|
return snapshotValue(response);
|
|
592
626
|
}
|
|
593
627
|
function snapshotValue(value) {
|
|
@@ -667,6 +701,10 @@ function getCallerHeaders(input, headers) {
|
|
|
667
701
|
return new Headers(input instanceof Request ? input.headers : undefined);
|
|
668
702
|
}
|
|
669
703
|
/** @internal */
|
|
704
|
+
/** Returns the identity runner of an mppx fetch wrapper, if it has one. @internal */
|
|
705
|
+
export function identityOf(fetch) {
|
|
706
|
+
return fetch[MPPX_FETCH_IDENTITY];
|
|
707
|
+
}
|
|
670
708
|
/** @internal */
|
|
671
709
|
export function unwrapFetch(fetch) {
|
|
672
710
|
let current = fetch;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type * as Challenge from '../../Challenge.js';
|
|
2
|
+
/**
|
|
3
|
+
* Pins that let a server origin request zero-amount Tempo proofs for a realm other than its
|
|
4
|
+
* hostname, keyed by origin (`scheme://host[:port]`), such as
|
|
5
|
+
* `{ 'https://api.example.com': 'MPP Payment' }`.
|
|
6
|
+
*/
|
|
7
|
+
export type TrustedRealms = Readonly<Record<string, string>>;
|
|
8
|
+
/**
|
|
9
|
+
* Refuses a zero-amount Tempo proof whose realm the HTTP server that sent the Challenge does not
|
|
10
|
+
* own. A proof authenticates the payer at the Challenge realm, so a foreign origin could otherwise
|
|
11
|
+
* relay another server's Challenge and replay the signed proof there. The server owns the realm
|
|
12
|
+
* when it is the request hostname, or when `trustedRealms` pins it to the request origin. Pass
|
|
13
|
+
* `url` as a function to resolve it only for proof Challenges.
|
|
14
|
+
*/
|
|
15
|
+
export declare function assert(challenge: Challenge.Challenge, url: URL | (() => URL), trustedRealms: TrustedRealms | undefined): void;
|
|
16
|
+
/**
|
|
17
|
+
* Refuses a zero-amount Tempo proof on a transport without an origin, such as MCP, unless
|
|
18
|
+
* `trustedRealms` lists its realm.
|
|
19
|
+
*/
|
|
20
|
+
export declare function assertWithoutOrigin(challenge: Challenge.Challenge, trustedRealms: readonly string[] | undefined): void;
|
|
21
|
+
//# sourceMappingURL=ProofRealm.d.ts.map
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Refuses a zero-amount Tempo proof whose realm the HTTP server that sent the Challenge does not
|
|
3
|
+
* own. A proof authenticates the payer at the Challenge realm, so a foreign origin could otherwise
|
|
4
|
+
* relay another server's Challenge and replay the signed proof there. The server owns the realm
|
|
5
|
+
* when it is the request hostname, or when `trustedRealms` pins it to the request origin. Pass
|
|
6
|
+
* `url` as a function to resolve it only for proof Challenges.
|
|
7
|
+
*/
|
|
8
|
+
export function assert(challenge, url, trustedRealms) {
|
|
9
|
+
if (!isProof(challenge))
|
|
10
|
+
return;
|
|
11
|
+
url = typeof url === 'function' ? url() : url;
|
|
12
|
+
const { realm } = challenge;
|
|
13
|
+
if (realm.toLowerCase() === url.hostname.toLowerCase())
|
|
14
|
+
return;
|
|
15
|
+
const origin = url.origin.toLowerCase();
|
|
16
|
+
for (const [trustedOrigin, trustedRealm] of Object.entries(trustedRealms ?? {}))
|
|
17
|
+
if (trustedRealm === realm && normalizeOrigin(trustedOrigin) === origin)
|
|
18
|
+
return;
|
|
19
|
+
throw new Error(`Refusing to sign Tempo proof for realm ${JSON.stringify(realm)} from ${url.origin}. Pin the realm to this origin with \`trustedTempoProofRealms\` if this server is expected to issue it.`);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Refuses a zero-amount Tempo proof on a transport without an origin, such as MCP, unless
|
|
23
|
+
* `trustedRealms` lists its realm.
|
|
24
|
+
*/
|
|
25
|
+
export function assertWithoutOrigin(challenge, trustedRealms) {
|
|
26
|
+
if (!isProof(challenge))
|
|
27
|
+
return;
|
|
28
|
+
if (trustedRealms?.includes(challenge.realm))
|
|
29
|
+
return;
|
|
30
|
+
throw new Error(`Refusing to sign Tempo proof for untrusted realm ${JSON.stringify(challenge.realm)}. Add it to \`trustedTempoProofRealms\` if this server is expected to issue it.`);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Whether `challenge` is a zero-amount `tempo/charge` Challenge, paid with a signed proof. Uses the
|
|
34
|
+
* same `BigInt` conversion as the Tempo charge client, so every encoding it signs a proof for
|
|
35
|
+
* (such as `"0x0"` or `" 0 "`) is checked.
|
|
36
|
+
*/
|
|
37
|
+
function isProof(challenge) {
|
|
38
|
+
if (challenge.method !== 'tempo' || challenge.intent !== 'charge')
|
|
39
|
+
return false;
|
|
40
|
+
try {
|
|
41
|
+
return BigInt(challenge.request.amount) === 0n;
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function normalizeOrigin(origin) {
|
|
48
|
+
try {
|
|
49
|
+
return new URL(origin).origin.toLowerCase();
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return origin.toLowerCase();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=ProofRealm.js.map
|
|
@@ -5,8 +5,11 @@ import { paymentRequiredStatus, setCredentialHeader } from './Shared.js';
|
|
|
5
5
|
export function mpp() {
|
|
6
6
|
return {
|
|
7
7
|
getChallenges(response) {
|
|
8
|
+
const header = response.headers.get(Constants.Headers.wwwAuthenticate);
|
|
9
|
+
// A 402 without a Payment challenge (such as `WWW-Authenticate: X402`) is left to other protocols.
|
|
8
10
|
if (response.status !== paymentRequiredStatus ||
|
|
9
|
-
!
|
|
11
|
+
!header ||
|
|
12
|
+
!Challenge.hasPaymentScheme(header))
|
|
10
13
|
return [];
|
|
11
14
|
return Challenge.fromResponseList(response);
|
|
12
15
|
},
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
/** RFC 9421 signature algorithms supported by
|
|
1
|
+
/** RFC 9421 signature algorithms supported by HTTP message signatures. */
|
|
2
2
|
export declare const Algorithms: {
|
|
3
3
|
/** EdDSA using the Ed25519 curve. */
|
|
4
4
|
readonly ed25519: 'ed25519';
|
|
5
5
|
/** RSASSA-PSS using SHA-512 and a 64-byte salt. */
|
|
6
6
|
readonly rsaPssSha512: 'rsa-pss-sha512';
|
|
7
7
|
};
|
|
8
|
-
/** HTTP headers defined by RFC 9421 for
|
|
8
|
+
/** HTTP headers defined by RFC 9421 for identity signatures. */
|
|
9
9
|
export declare const Headers: {
|
|
10
10
|
/** Carries the RFC 9421 signature bytes. */
|
|
11
11
|
readonly signature: 'Signature';
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
/** RFC 9421 signature algorithms supported by
|
|
1
|
+
/** RFC 9421 signature algorithms supported by HTTP message signatures. */
|
|
2
2
|
export const Algorithms = {
|
|
3
3
|
/** EdDSA using the Ed25519 curve. */
|
|
4
4
|
ed25519: 'ed25519',
|
|
5
5
|
/** RSASSA-PSS using SHA-512 and a 64-byte salt. */
|
|
6
6
|
rsaPssSha512: 'rsa-pss-sha512',
|
|
7
7
|
};
|
|
8
|
-
/** HTTP headers defined by RFC 9421 for
|
|
8
|
+
/** HTTP headers defined by RFC 9421 for identity signatures. */
|
|
9
9
|
export const Headers = {
|
|
10
10
|
/** Carries the RFC 9421 signature bytes. */
|
|
11
11
|
signature: 'Signature',
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { MaybePromise } from '../../internal/types.js';
|
|
2
|
+
import type { Algorithms } from './Constants.js';
|
|
3
|
+
/** RFC 9421 algorithms implemented by the HTTP message signature signer and verifier. */
|
|
4
|
+
export type SignatureAlgorithm = (typeof Algorithms)[keyof typeof Algorithms];
|
|
5
|
+
/** Resolves a trusted public key for a parsed request signature. */
|
|
6
|
+
export type KeyResolver = (parameters: {
|
|
7
|
+
/** RFC 9421 algorithm declared by the signature. */
|
|
8
|
+
algorithm: SignatureAlgorithm;
|
|
9
|
+
/** Key identifier declared by the signature. */
|
|
10
|
+
keyId: string;
|
|
11
|
+
/** Request carrying the signature. */
|
|
12
|
+
request: Request;
|
|
13
|
+
}) => MaybePromise<CryptoKey | undefined>;
|
|
14
|
+
/** Values shared by every signature on one HTTP attempt. */
|
|
15
|
+
export type SigningContext = {
|
|
16
|
+
/** Unix timestamp, in seconds, at which the attempt was created. */
|
|
17
|
+
readonly created: number;
|
|
18
|
+
/** Base64url-encoded random value identifying the attempt. */
|
|
19
|
+
readonly nonce: string;
|
|
20
|
+
};
|
|
21
|
+
//# sourceMappingURL=Types.d.ts.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Client and server adapters for the TAP agent-recognition signature profile. */
|
|
2
|
+
export * as Tap from './tap/index.js';
|
|
3
|
+
/** Client and server adapters for the Web Bot Auth HTTPS-directory profile. */
|
|
4
|
+
export * as WebBotAuth from './web-bot-auth/index.js';
|
|
5
|
+
export * from './Constants.js';
|
|
6
|
+
export type * from './Types.js';
|
|
7
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Client and server adapters for the TAP agent-recognition signature profile. */
|
|
2
|
+
export * as Tap from './tap/index.js';
|
|
3
|
+
/** Client and server adapters for the Web Bot Auth HTTPS-directory profile. */
|
|
4
|
+
export * as WebBotAuth from './web-bot-auth/index.js';
|
|
5
|
+
export * from './Constants.js';
|
|
6
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { BareItem } from 'structured-headers';
|
|
2
|
-
import type { MaybePromise } from '
|
|
3
|
-
import * as Store from '
|
|
2
|
+
import type { MaybePromise } from '../../../internal/types.js';
|
|
3
|
+
import * as Store from '../../../Store.js';
|
|
4
4
|
import type { KeyResolver, SignatureAlgorithm, SigningContext } from '../Types.js';
|
|
5
|
-
/** RFC 9421 constants used by
|
|
5
|
+
/** RFC 9421 constants used by identity signature profiles. */
|
|
6
6
|
export declare const Constants: {
|
|
7
7
|
/** Derived request components supported by this narrow request profile. */
|
|
8
8
|
readonly components: {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { isInnerList, parseDictionary, serializeItem, serializeDictionary, serializeInnerList, serializeParameters, } from 'structured-headers';
|
|
2
|
-
import * as Store from '
|
|
3
|
-
import { Algorithms, Headers as
|
|
2
|
+
import * as Store from '../../../Store.js';
|
|
3
|
+
import { Algorithms, Headers as SignatureHeaders } from '../Constants.js';
|
|
4
4
|
import * as SigningContextInternal from './SigningContext.js';
|
|
5
|
-
/** RFC 9421 constants used by
|
|
5
|
+
/** RFC 9421 constants used by identity signature profiles. */
|
|
6
6
|
export const Constants = {
|
|
7
7
|
/** Derived request components supported by this narrow request profile. */
|
|
8
8
|
components: {
|
|
@@ -55,16 +55,16 @@ export async function sign(request, options) {
|
|
|
55
55
|
};
|
|
56
56
|
const signature = await crypto.subtle.sign(webCryptoAlgorithm(algorithm), options.key, toArrayBuffer(encode(signatureBase(request, input))));
|
|
57
57
|
const headers = new Headers(request.headers);
|
|
58
|
-
const inputs = parseHeaderDictionary(headers,
|
|
59
|
-
const signatures = parseHeaderDictionary(headers,
|
|
58
|
+
const inputs = parseHeaderDictionary(headers, SignatureHeaders.signatureInput);
|
|
59
|
+
const signatures = parseHeaderDictionary(headers, SignatureHeaders.signature);
|
|
60
60
|
if (!haveSameKeys(inputs, signatures))
|
|
61
61
|
throw new Error('Signature-Input and Signature must contain the same labels.');
|
|
62
62
|
if (inputs.has(input.label) || signatures.has(input.label))
|
|
63
63
|
throw new Error(`HTTP message signature label "${input.label}" already exists.`);
|
|
64
64
|
inputs.set(input.label, toInnerList(input));
|
|
65
65
|
signatures.set(input.label, [signature, new Map()]);
|
|
66
|
-
headers.set(
|
|
67
|
-
headers.set(
|
|
66
|
+
headers.set(SignatureHeaders.signatureInput, serializeDictionary(inputs));
|
|
67
|
+
headers.set(SignatureHeaders.signature, serializeDictionary(signatures));
|
|
68
68
|
return new Request(request, { headers });
|
|
69
69
|
}
|
|
70
70
|
/** Verifies a request signature, its lifetime, and optionally its nonce. */
|
|
@@ -137,8 +137,8 @@ export async function verify(request, options) {
|
|
|
137
137
|
return { status: 'verified', input };
|
|
138
138
|
}
|
|
139
139
|
function parse(request, expectedTag) {
|
|
140
|
-
const inputHeader = request.headers.get(
|
|
141
|
-
const signatureHeader = request.headers.get(
|
|
140
|
+
const inputHeader = request.headers.get(SignatureHeaders.signatureInput);
|
|
141
|
+
const signatureHeader = request.headers.get(SignatureHeaders.signature);
|
|
142
142
|
if (!inputHeader && !signatureHeader)
|
|
143
143
|
return undefined;
|
|
144
144
|
if (!inputHeader || !signatureHeader)
|
|
@@ -219,7 +219,7 @@ function algorithmFromKey(key) {
|
|
|
219
219
|
for (const algorithm of Object.values(Algorithms))
|
|
220
220
|
if (keySupportsAlgorithm(key, algorithm))
|
|
221
221
|
return algorithm;
|
|
222
|
-
throw new TypeError('
|
|
222
|
+
throw new TypeError('HTTP message signatures require an Ed25519 or RSA-PSS SHA-512 signing key.');
|
|
223
223
|
}
|
|
224
224
|
function isInteger(value) {
|
|
225
225
|
return typeof value === 'number' && Number.isInteger(value);
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { Base64 } from 'ox';
|
|
2
|
-
/** Creates the
|
|
2
|
+
/** Creates the attempt values shared by every prover on one HTTP attempt. */
|
|
3
3
|
export function create() {
|
|
4
4
|
const bytes = new Uint8Array(64);
|
|
5
5
|
crypto.getRandomValues(bytes);
|
|
6
|
-
return
|
|
6
|
+
return {
|
|
7
7
|
created: Math.floor(Date.now() / 1_000),
|
|
8
8
|
nonce: Base64.fromBytes(bytes, { pad: false, url: true }),
|
|
9
|
-
}
|
|
9
|
+
};
|
|
10
10
|
}
|
|
11
11
|
//# sourceMappingURL=SigningContext.js.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type * as Identity from '../../../identity/Types.js';
|
|
2
|
+
import type { VerificationResult } from './HttpMessageSignature.js';
|
|
3
|
+
/** Maps a non-verified HTTP message signature result to an identity verification. */
|
|
4
|
+
export declare function fromSignature(result: Exclude<VerificationResult, {
|
|
5
|
+
status: 'verified';
|
|
6
|
+
}>): Exclude<Identity.Verification, {
|
|
7
|
+
status: 'valid';
|
|
8
|
+
}>;
|
|
9
|
+
//# sourceMappingURL=Verification.d.ts.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Maps a non-verified HTTP message signature result to an identity verification. */
|
|
2
|
+
export function fromSignature(result) {
|
|
3
|
+
if (result.status === 'absent')
|
|
4
|
+
return { status: 'missing' };
|
|
5
|
+
if (result.status === 'invalid')
|
|
6
|
+
return { status: 'invalid', reason: result.reason };
|
|
7
|
+
// The signature was presented but its key is not trusted or available.
|
|
8
|
+
return { status: 'invalid', code: 403, reason: result.reason };
|
|
9
|
+
}
|
|
10
|
+
//# sourceMappingURL=Verification.js.map
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Constants as HttpMessageSignature } from '../
|
|
1
|
+
import { Constants as HttpMessageSignature } from '../internal/HttpMessageSignature.js';
|
|
2
2
|
/** TAP values required for its RFC 9421 agent-recognition signature profile. */
|
|
3
3
|
export const Constants = {
|
|
4
4
|
/** Stable identifier for Trusted Agent Protocol signers and replay storage. */
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type * as Identity from '../../../../identity/Types.js';
|
|
2
|
+
import { Constants } from '../Constants.js';
|
|
3
|
+
/**
|
|
4
|
+
* Creates a TAP identity prover.
|
|
5
|
+
*
|
|
6
|
+
* The prover signs each in-scope attempt, including MPP payment retries, with
|
|
7
|
+
* an RFC 9421 HTTP message signature carrying TAP's `agent-browser-auth` or
|
|
8
|
+
* `agent-payer-auth` tag.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```ts
|
|
12
|
+
* Mppx.create({ identity: [Tap.Client.prover({ intent: 'payment', key, keyId, scope: ['https://shop.example'] })], methods })
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
export declare function prover(config: prover.Config): Identity.Prover;
|
|
16
|
+
export declare namespace prover {
|
|
17
|
+
type Config = {
|
|
18
|
+
/** TAP signature type for the request. */
|
|
19
|
+
intent: (typeof Constants.intents)[keyof typeof Constants.intents];
|
|
20
|
+
/** Ed25519 or RSA-PSS SHA-512 private key provisioned to the agent provider. */
|
|
21
|
+
key: CryptoKey;
|
|
22
|
+
/** Identifier the merchant uses to resolve the signing public key. */
|
|
23
|
+
keyId: string;
|
|
24
|
+
/** Origins that receive TAP signatures, or `'*'` for any origin. */
|
|
25
|
+
scope: Identity.Prover['scope'];
|
|
26
|
+
/** Signature lifetime in seconds. TAP permits at most eight minutes. @default 480 */
|
|
27
|
+
expiresIn?: number | undefined;
|
|
28
|
+
/** RFC 9421 dictionary label. @default 'tap' */
|
|
29
|
+
label?: string | undefined;
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1,14 +1,18 @@
|
|
|
1
|
-
import * as HttpMessageSignature from '../../
|
|
1
|
+
import * as HttpMessageSignature from '../../internal/HttpMessageSignature.js';
|
|
2
2
|
import { Constants } from '../Constants.js';
|
|
3
3
|
/**
|
|
4
|
-
* Creates a TAP
|
|
4
|
+
* Creates a TAP identity prover.
|
|
5
5
|
*
|
|
6
|
-
* The
|
|
7
|
-
* `agent-browser-auth` or
|
|
8
|
-
* `
|
|
9
|
-
*
|
|
6
|
+
* The prover signs each in-scope attempt, including MPP payment retries, with
|
|
7
|
+
* an RFC 9421 HTTP message signature carrying TAP's `agent-browser-auth` or
|
|
8
|
+
* `agent-payer-auth` tag.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```ts
|
|
12
|
+
* Mppx.create({ identity: [Tap.Client.prover({ intent: 'payment', key, keyId, scope: ['https://shop.example'] })], methods })
|
|
13
|
+
* ```
|
|
10
14
|
*/
|
|
11
|
-
export function
|
|
15
|
+
export function prover(config) {
|
|
12
16
|
const tag = config.intent === Constants.intents.payment ? Constants.tags.payment : Constants.tags.browse;
|
|
13
17
|
const expiresIn = config.expiresIn ?? Constants.defaultSignatureLifetime;
|
|
14
18
|
if (!Number.isInteger(expiresIn) ||
|
|
@@ -16,8 +20,8 @@ export function signer(config) {
|
|
|
16
20
|
expiresIn > Constants.maximumSignatureLifetime)
|
|
17
21
|
throw new RangeError(`TAP expiresIn must be an integer from 1 to ${Constants.maximumSignatureLifetime}.`);
|
|
18
22
|
return {
|
|
19
|
-
|
|
20
|
-
|
|
23
|
+
scope: config.scope,
|
|
24
|
+
prove(request, context) {
|
|
21
25
|
return HttpMessageSignature.sign(request, {
|
|
22
26
|
components: Constants.requiredComponents,
|
|
23
27
|
context,
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type * as Identity from '../../../../identity/Types.js';
|
|
2
|
+
import type * as Store from '../../../../Store.js';
|
|
3
|
+
import type * as SignatureTypes from '../../Types.js';
|
|
4
|
+
import type * as Types from '../Types.js';
|
|
5
|
+
/**
|
|
6
|
+
* Creates a request-stage identity verifier for TAP agent-recognition signatures.
|
|
7
|
+
*
|
|
8
|
+
* TAP verifies trusted agent participation and request integrity. Consumer
|
|
9
|
+
* recognition and payment-container verification stay protocol-specific and
|
|
10
|
+
* are intentionally not inferred from this header-only verifier.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts
|
|
14
|
+
* const tap = Tap.Server.verifier({ keyResolver, nonceStore })
|
|
15
|
+
* mppx.charge({ amount: '1', identity: [tap] })
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
export declare function verifier(config: verifier.Config): Identity.Verifier<Types.VerifiedRequest>;
|
|
19
|
+
export declare namespace verifier {
|
|
20
|
+
type Config = {
|
|
21
|
+
/** Resolves a TAP-approved public key by its `keyid` and signature algorithm. */
|
|
22
|
+
keyResolver: SignatureTypes.KeyResolver;
|
|
23
|
+
/** Atomically consumes each nonce in shared storage for multi-instance deployments. */
|
|
24
|
+
nonceStore: Store.AtomicStore;
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1,14 +1,22 @@
|
|
|
1
|
-
import * as HttpMessageSignature from '../../
|
|
1
|
+
import * as HttpMessageSignature from '../../internal/HttpMessageSignature.js';
|
|
2
|
+
import * as Verification from '../../internal/Verification.js';
|
|
2
3
|
import { Constants } from '../Constants.js';
|
|
3
4
|
/**
|
|
4
|
-
* Creates a verifier for TAP agent-recognition signatures.
|
|
5
|
+
* Creates a request-stage identity verifier for TAP agent-recognition signatures.
|
|
5
6
|
*
|
|
6
7
|
* TAP verifies trusted agent participation and request integrity. Consumer
|
|
7
8
|
* recognition and payment-container verification stay protocol-specific and
|
|
8
9
|
* are intentionally not inferred from this header-only verifier.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```ts
|
|
13
|
+
* const tap = Tap.Server.verifier({ keyResolver, nonceStore })
|
|
14
|
+
* mppx.charge({ amount: '1', identity: [tap] })
|
|
15
|
+
* ```
|
|
9
16
|
*/
|
|
10
17
|
export function verifier(config) {
|
|
11
18
|
return {
|
|
19
|
+
stage: 'request',
|
|
12
20
|
async verify(request) {
|
|
13
21
|
const result = await HttpMessageSignature.verify(request, {
|
|
14
22
|
keyResolver: config.keyResolver,
|
|
@@ -19,9 +27,9 @@ export function verifier(config) {
|
|
|
19
27
|
tag: [Constants.tags.browse, Constants.tags.payment],
|
|
20
28
|
});
|
|
21
29
|
if (result.status !== 'verified')
|
|
22
|
-
return result;
|
|
30
|
+
return Verification.fromSignature(result);
|
|
23
31
|
return {
|
|
24
|
-
status: '
|
|
32
|
+
status: 'valid',
|
|
25
33
|
value: {
|
|
26
34
|
keyId: result.input.keyId,
|
|
27
35
|
nonce: result.input.nonce,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Constants as HttpMessageSignature } from '../
|
|
1
|
+
import { Constants as HttpMessageSignature } from '../internal/HttpMessageSignature.js';
|
|
2
2
|
/** Web Bot Auth values for the supported HTTPS-directory signature profile. */
|
|
3
3
|
export const Constants = {
|
|
4
4
|
/** Stable identifier for Web Bot Auth signers and replay storage. */
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import type * as
|
|
2
|
-
import { Constants } from '../Constants.js';
|
|
1
|
+
import type * as Identity from '../../../../identity/Types.js';
|
|
3
2
|
/**
|
|
4
|
-
* Creates a Web Bot Auth HTTPS-directory profile
|
|
3
|
+
* Creates a Web Bot Auth HTTPS-directory profile identity prover.
|
|
5
4
|
*
|
|
6
5
|
* The signed `Signature-Agent` header links the request to the HTTPS origin
|
|
7
6
|
* of an HTTP Message Signatures Directory. This adapter does not support
|
|
@@ -9,13 +8,15 @@ import { Constants } from '../Constants.js';
|
|
|
9
8
|
* establishes bot identity only; it does not assert consumer identity or
|
|
10
9
|
* authority to make a payment.
|
|
11
10
|
*/
|
|
12
|
-
export declare function
|
|
13
|
-
export declare namespace
|
|
11
|
+
export declare function prover(config: prover.Config): Identity.Prover;
|
|
12
|
+
export declare namespace prover {
|
|
14
13
|
type Config = {
|
|
15
14
|
/** Ed25519 or RSA-PSS SHA-512 private key registered for the bot. */
|
|
16
15
|
key: CryptoKey;
|
|
17
16
|
/** RFC 7638 JWK thumbprint for the registered public key. */
|
|
18
17
|
keyId: string;
|
|
18
|
+
/** Origins that receive Web Bot Auth signatures, or `'*'` for any origin. */
|
|
19
|
+
scope: Identity.Prover['scope'];
|
|
19
20
|
/** Required HTTPS directory origin, sent with the default `directory` discovery type. */
|
|
20
21
|
signatureAgent: string;
|
|
21
22
|
/** Dictionary member name used for `Signature-Agent`. @default signature label */
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { parseDictionary, serializeDictionary } from 'structured-headers';
|
|
2
|
-
import * as HttpMessageSignature from '../../
|
|
2
|
+
import * as HttpMessageSignature from '../../internal/HttpMessageSignature.js';
|
|
3
3
|
import { Constants } from '../Constants.js';
|
|
4
4
|
import * as JwkThumbprint from '../internal/JwkThumbprint.js';
|
|
5
5
|
import * as SignatureAgent from '../internal/SignatureAgent.js';
|
|
6
6
|
/**
|
|
7
|
-
* Creates a Web Bot Auth HTTPS-directory profile
|
|
7
|
+
* Creates a Web Bot Auth HTTPS-directory profile identity prover.
|
|
8
8
|
*
|
|
9
9
|
* The signed `Signature-Agent` header links the request to the HTTPS origin
|
|
10
10
|
* of an HTTP Message Signatures Directory. This adapter does not support
|
|
@@ -12,7 +12,7 @@ import * as SignatureAgent from '../internal/SignatureAgent.js';
|
|
|
12
12
|
* establishes bot identity only; it does not assert consumer identity or
|
|
13
13
|
* authority to make a payment.
|
|
14
14
|
*/
|
|
15
|
-
export function
|
|
15
|
+
export function prover(config) {
|
|
16
16
|
const label = config.label ?? Constants.label;
|
|
17
17
|
const signatureAgentKey = config.signatureAgentKey ?? label;
|
|
18
18
|
const expiresIn = config.expiresIn ?? Constants.defaultSignatureLifetime;
|
|
@@ -24,8 +24,8 @@ export function signer(config) {
|
|
|
24
24
|
if (!Number.isInteger(expiresIn) || expiresIn <= 0)
|
|
25
25
|
throw new RangeError('Web Bot Auth expiresIn must be a positive integer.');
|
|
26
26
|
return {
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
scope: config.scope,
|
|
28
|
+
prove(request, context) {
|
|
29
29
|
const headers = new Headers(request.headers);
|
|
30
30
|
const signatureAgents = parseSignatureAgents(headers);
|
|
31
31
|
if (signatureAgents.has(signatureAgentKey))
|