@nevermined-io/payments 1.9.0 → 1.11.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/dist/api/base-payments.d.ts +11 -0
- package/dist/api/base-payments.d.ts.map +1 -1
- package/dist/api/base-payments.js +28 -3
- package/dist/api/base-payments.js.map +1 -1
- package/dist/api/nvm-api.d.ts +4 -0
- package/dist/api/nvm-api.d.ts.map +1 -1
- package/dist/api/nvm-api.js +7 -0
- package/dist/api/nvm-api.js.map +1 -1
- package/dist/api/organizations-api/organizations-api.d.ts +19 -1
- package/dist/api/organizations-api/organizations-api.d.ts.map +1 -1
- package/dist/api/organizations-api/organizations-api.js +49 -0
- package/dist/api/organizations-api/organizations-api.js.map +1 -1
- package/dist/api/organizations-api/types.d.ts +27 -0
- package/dist/api/organizations-api/types.d.ts.map +1 -1
- package/dist/api/organizations-api/types.js.map +1 -1
- package/dist/common/api-version.d.ts +1 -1
- package/dist/common/api-version.js +1 -1
- package/dist/common/api-version.js.map +1 -1
- package/dist/common/types.d.ts +9 -4
- package/dist/common/types.d.ts.map +1 -1
- package/dist/common/types.js.map +1 -1
- package/dist/environments.d.ts +12 -0
- package/dist/environments.d.ts.map +1 -1
- package/dist/environments.js +31 -0
- package/dist/environments.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/mcp/core/server-manager.d.ts.map +1 -1
- package/dist/mcp/core/server-manager.js +3 -0
- package/dist/mcp/core/server-manager.js.map +1 -1
- package/dist/mcp/http/mcp-handler.d.ts +7 -0
- package/dist/mcp/http/mcp-handler.d.ts.map +1 -1
- package/dist/mcp/http/mcp-handler.js +8 -1
- package/dist/mcp/http/mcp-handler.js.map +1 -1
- package/dist/mcp/http/oauth-metadata.d.ts.map +1 -1
- package/dist/mcp/http/oauth-metadata.js +31 -6
- package/dist/mcp/http/oauth-metadata.js.map +1 -1
- package/dist/mcp/http/oauth-router.d.ts +17 -16
- package/dist/mcp/http/oauth-router.d.ts.map +1 -1
- package/dist/mcp/http/oauth-router.js +56 -28
- package/dist/mcp/http/oauth-router.js.map +1 -1
- package/dist/mpp/codec.d.ts +92 -0
- package/dist/mpp/codec.d.ts.map +1 -0
- package/dist/mpp/codec.js +405 -0
- package/dist/mpp/codec.js.map +1 -0
- package/dist/mpp/errors.d.ts +165 -0
- package/dist/mpp/errors.d.ts.map +1 -0
- package/dist/mpp/errors.js +177 -0
- package/dist/mpp/errors.js.map +1 -0
- package/dist/mpp/fetch.d.ts +196 -0
- package/dist/mpp/fetch.d.ts.map +1 -0
- package/dist/mpp/fetch.js +493 -0
- package/dist/mpp/fetch.js.map +1 -0
- package/dist/mpp/index.d.ts +11 -0
- package/dist/mpp/index.d.ts.map +1 -0
- package/dist/mpp/index.js +20 -0
- package/dist/mpp/index.js.map +1 -0
- package/dist/mpp/mpp-api.d.ts +153 -0
- package/dist/mpp/mpp-api.d.ts.map +1 -0
- package/dist/mpp/mpp-api.js +298 -0
- package/dist/mpp/mpp-api.js.map +1 -0
- package/dist/mpp/types.d.ts +43 -0
- package/dist/mpp/types.d.ts.map +1 -0
- package/dist/mpp/types.js +9 -0
- package/dist/mpp/types.js.map +1 -0
- package/dist/payments.d.ts +13 -0
- package/dist/payments.d.ts.map +1 -1
- package/dist/payments.js +3 -0
- package/dist/payments.js.map +1 -1
- package/dist/x402/delegation-api.d.ts +3 -1
- package/dist/x402/delegation-api.d.ts.map +1 -1
- package/dist/x402/delegation-api.js.map +1 -1
- package/dist/x402/express/index.d.ts +2 -0
- package/dist/x402/express/index.d.ts.map +1 -1
- package/dist/x402/express/index.js +2 -0
- package/dist/x402/express/index.js.map +1 -1
- package/dist/x402/express/middleware.d.ts +33 -1
- package/dist/x402/express/middleware.d.ts.map +1 -1
- package/dist/x402/express/middleware.js +831 -1
- package/dist/x402/express/middleware.js.map +1 -1
- package/dist/x402/express/mpp-support.d.ts +58 -0
- package/dist/x402/express/mpp-support.d.ts.map +1 -0
- package/dist/x402/express/mpp-support.js +67 -0
- package/dist/x402/express/mpp-support.js.map +1 -0
- package/dist/x402/express/raw-body.d.ts +24 -0
- package/dist/x402/express/raw-body.d.ts.map +1 -0
- package/dist/x402/express/raw-body.js +34 -0
- package/dist/x402/express/raw-body.js.map +1 -0
- package/dist/x402/token-request.d.ts +15 -0
- package/dist/x402/token-request.d.ts.map +1 -0
- package/dist/x402/token-request.js +66 -0
- package/dist/x402/token-request.js.map +1 -0
- package/dist/x402/token.d.ts.map +1 -1
- package/dist/x402/token.js +7 -51
- package/dist/x402/token.js.map +1 -1
- package/package.json +3 -1
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The MPP wire format, hand-written so the SDK takes no protocol dependency.
|
|
3
|
+
*
|
|
4
|
+
* The one rule that matters: the backend re-derives the challenge id as
|
|
5
|
+
* `HMAC(secret, realm|method|intent|canonicalize(request)|expires|digest|opaque)`
|
|
6
|
+
* from the fields carried inside the credential. `request` and `opaque` are
|
|
7
|
+
* therefore passed through as the exact base64url strings received — this file
|
|
8
|
+
* never re-encodes them.
|
|
9
|
+
*/
|
|
10
|
+
import { MppError } from './errors.js';
|
|
11
|
+
/** A bare token (RFC 9110 §5.6.2, used for auth-scheme names and auth-param keys). */
|
|
12
|
+
const TOKEN = "[!#$%&'*+\\-.^_`|~0-9A-Za-z]+";
|
|
13
|
+
/** Matches the start of a `key=` auth-param, i.e. a Payment challenge continuing. */
|
|
14
|
+
const AUTH_PARAM_START = new RegExp(`^${TOKEN}\\s*=`);
|
|
15
|
+
/**
|
|
16
|
+
* Finds where a structured challenge's auth-param list ends within `rest`
|
|
17
|
+
* (the content right after "Payment "), scanning quote- and escape-aware so
|
|
18
|
+
* neither a comma nor a `\"` inside a `"…"` value is mistaken for a
|
|
19
|
+
* boundary. mppx serializes quoted values with
|
|
20
|
+
* `value.replace(/\\/g,'\\\\').replace(/"/g,'\\"')` (`Challenge.ts:316-319`),
|
|
21
|
+
* so a seller-supplied `description` like `5" screen replacement plan` wire-
|
|
22
|
+
* encodes as `description="5\" screen replacement plan"`. A scanner that
|
|
23
|
+
* toggles quote state on every literal `"` — escaped or not — desyncs on
|
|
24
|
+
* that `\"`, stays stuck "inside" a quote for the rest of the header, and
|
|
25
|
+
* swallows a genuine trailing scheme. This mirrors mppx's own reference
|
|
26
|
+
* parser's `escaped` flag (`Challenge.ts:362-379`) rather than inventing new
|
|
27
|
+
* semantics.
|
|
28
|
+
*
|
|
29
|
+
* A top-level (non-quoted, non-escaped) comma only ends the scheme if what
|
|
30
|
+
* follows it does NOT itself look like a continuing `key=value` auth-param —
|
|
31
|
+
* which is also how a following literal "Payment " (the merged-challenge
|
|
32
|
+
* case) is recognized as a new scheme: "Payment" followed by whitespace
|
|
33
|
+
* never matches `AUTH_PARAM_START`, since that requires "=" immediately
|
|
34
|
+
* (modulo optional whitespace) after the leading token.
|
|
35
|
+
*
|
|
36
|
+
* An unterminated quote is not an error here: the loop simply runs out of
|
|
37
|
+
* input without finding a boundary and falls back to treating the whole
|
|
38
|
+
* remainder as this scheme — a safe, total (never-throwing) O(n) fallback.
|
|
39
|
+
*/
|
|
40
|
+
function findStructuredChallengeEnd(rest) {
|
|
41
|
+
let inQuotes = false;
|
|
42
|
+
let escaped = false;
|
|
43
|
+
for (let i = 0; i < rest.length; i++) {
|
|
44
|
+
const ch = rest[i];
|
|
45
|
+
if (inQuotes) {
|
|
46
|
+
if (escaped) {
|
|
47
|
+
escaped = false;
|
|
48
|
+
}
|
|
49
|
+
else if (ch === '\\') {
|
|
50
|
+
escaped = true;
|
|
51
|
+
}
|
|
52
|
+
else if (ch === '"') {
|
|
53
|
+
inQuotes = false;
|
|
54
|
+
}
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (ch === '"') {
|
|
58
|
+
inQuotes = true;
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (ch === ',' && !AUTH_PARAM_START.test(rest.slice(i + 1).trimStart())) {
|
|
62
|
+
return i;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return rest.length;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Matches the `Payment` scheme name at a genuine scheme boundary: start of
|
|
69
|
+
* the header value, or right after a top-level comma (RFC 9110 §11.6.1
|
|
70
|
+
* separates comma-separated challenges/credentials that way). Unanchored,
|
|
71
|
+
* `/Payment\s+/i` matches mid-token — `XPayment abc`, `NotPayment abc`,
|
|
72
|
+
* `Bearer prepayment xyz` — and also matches "Payment" text embedded inside
|
|
73
|
+
* a *different*, preceding scheme's quoted value (e.g.
|
|
74
|
+
* `Digest username="my payment plan", …`), since plain whitespace alone is
|
|
75
|
+
* not a scheme boundary. Anchoring on comma-or-start (not bare whitespace)
|
|
76
|
+
* closes both: a scheme name is never preceded by an arbitrary space, only
|
|
77
|
+
* by the start of the header or the comma that separates it from what came
|
|
78
|
+
* before.
|
|
79
|
+
*
|
|
80
|
+
* This decides which protocol handles a request:
|
|
81
|
+
* `extractCredential` → `extractPaymentScheme` feeds the MPP-vs-x402 routing
|
|
82
|
+
* predicate in `middleware.ts`, so an unanchored match would divert an
|
|
83
|
+
* x402 buyer carrying a perfectly valid `payment-signature` token, plus an
|
|
84
|
+
* unrelated `Authorization` header that happens to contain "payment" text,
|
|
85
|
+
* onto the MPP path and challenge them instead of serving the request.
|
|
86
|
+
*/
|
|
87
|
+
const PAYMENT_SCHEME_BOUNDARY = /(?:^|,)\s*(Payment\s+)/i;
|
|
88
|
+
/**
|
|
89
|
+
* Extracts the `Payment` scheme from a header value that may carry several
|
|
90
|
+
* schemes comma-separated (RFC 9110 §11.6.1).
|
|
91
|
+
*
|
|
92
|
+
* The `Payment` scheme takes one of two shapes on our wire: a bare token68
|
|
93
|
+
* credential (`Payment <base64url>`, which cannot itself contain a comma or
|
|
94
|
+
* a quote) or a structured challenge (`Payment id="...", realm="...", ...`,
|
|
95
|
+
* comma-separated `key="value"` auth-params, where a value MAY contain a
|
|
96
|
+
* comma). Bounding the match at the next literal "Payment " occurrence — or
|
|
97
|
+
* at end-of-string otherwise — corrupts the first shape whenever a
|
|
98
|
+
* *different* trailing scheme follows: e.g. `Payment <token>, Bearer <jwt>`
|
|
99
|
+
* used to extract the whole remainder including the trailing scheme.
|
|
100
|
+
* Bounding a structured challenge with a naive, quote-unaware comma split
|
|
101
|
+
* corrupts the second shape whenever a value contains a comma: it truncates
|
|
102
|
+
* mid-quote and silently drops every param after it. Instead: a bare
|
|
103
|
+
* token68 is bounded by its first top-level comma; a structured challenge is
|
|
104
|
+
* bounded by {@link findStructuredChallengeEnd}'s quote-aware scan.
|
|
105
|
+
*/
|
|
106
|
+
export function extractPaymentScheme(headerValue) {
|
|
107
|
+
const schemeMatch = PAYMENT_SCHEME_BOUNDARY.exec(headerValue);
|
|
108
|
+
if (!schemeMatch || schemeMatch.index === undefined)
|
|
109
|
+
return null;
|
|
110
|
+
// Group 1 ("Payment\s+") is the tail of the whole match, so the full
|
|
111
|
+
// match's end position is also where the captured keyword ends; its start
|
|
112
|
+
// is offset back by the keyword's own length, which skips the leading
|
|
113
|
+
// comma/whitespace the boundary alternation consumed.
|
|
114
|
+
const keyword = schemeMatch[1];
|
|
115
|
+
const contentStart = schemeMatch.index + schemeMatch[0].length;
|
|
116
|
+
const start = contentStart - keyword.length;
|
|
117
|
+
const rest = headerValue.slice(contentStart);
|
|
118
|
+
if (!AUTH_PARAM_START.test(rest)) {
|
|
119
|
+
// Bare token68 credential: bounded by its first top-level comma, since a
|
|
120
|
+
// token68 cannot itself contain one (or a quote, so no quote-tracking
|
|
121
|
+
// is needed here).
|
|
122
|
+
const commaIndex = rest.indexOf(',');
|
|
123
|
+
const end = commaIndex === -1 ? headerValue.length : contentStart + commaIndex;
|
|
124
|
+
return headerValue.slice(start, end).trim();
|
|
125
|
+
}
|
|
126
|
+
const end = contentStart + findStructuredChallengeEnd(rest);
|
|
127
|
+
return headerValue.slice(start, end).replace(/,\s*$/, '').trim();
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* The stable identity of a credential: the challenge id it carries.
|
|
131
|
+
*
|
|
132
|
+
* The header bytes are NOT that identity. `PAYMENT_SCHEME_BOUNDARY` is
|
|
133
|
+
* case-insensitive and {@link extractPaymentScheme} returns the matched slice
|
|
134
|
+
* verbatim, so `Payment x`, `payment x` and `Payment x` are three different
|
|
135
|
+
* strings for one credential — and the body is base64url of JSON the BUYER
|
|
136
|
+
* assembles, so re-ordering its keys yields more. The backend collapses every
|
|
137
|
+
* one of them onto a single burn, because its own idempotency key is the
|
|
138
|
+
* decoded challenge id. Anything at the seller edge enforcing single-use has
|
|
139
|
+
* to key on the same thing, or a buyer flips one byte of case and buys the
|
|
140
|
+
* response again.
|
|
141
|
+
*
|
|
142
|
+
* @returns the `challenge.id` a token68 credential decodes to, or `null` when
|
|
143
|
+
* the credential cannot be decoded into one — a structured `key="value"`
|
|
144
|
+
* scheme (that shape is a challenge, never a credential), undecodable
|
|
145
|
+
* base64url JSON, or JSON without a non-empty string `challenge.id`. The
|
|
146
|
+
* backend rejects all of those anyway; returning `null` lets the caller refuse
|
|
147
|
+
* them without a round-trip rather than fall back to a key it cannot trust.
|
|
148
|
+
*/
|
|
149
|
+
export function extractCredentialChallengeId(credential) {
|
|
150
|
+
const token68 = credential.replace(/^Payment\s+/i, '').trim();
|
|
151
|
+
if (!token68 || AUTH_PARAM_START.test(token68))
|
|
152
|
+
return null;
|
|
153
|
+
let decoded;
|
|
154
|
+
try {
|
|
155
|
+
decoded = JSON.parse(Buffer.from(token68, 'base64url').toString('utf8'));
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
if (typeof decoded !== 'object' || decoded === null || Array.isArray(decoded))
|
|
161
|
+
return null;
|
|
162
|
+
const { challenge } = decoded;
|
|
163
|
+
if (typeof challenge !== 'object' || challenge === null || Array.isArray(challenge))
|
|
164
|
+
return null;
|
|
165
|
+
const { id } = challenge;
|
|
166
|
+
return typeof id === 'string' && id !== '' ? id : null;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Reads a quoted-string value starting right after its opening `"`,
|
|
170
|
+
* unescaping `\"` to a literal `"` and `\\` to a literal `\` (mirrors
|
|
171
|
+
* mppx's `readQuotedAuthParamValue`, `Challenge.ts:461-465`).
|
|
172
|
+
*
|
|
173
|
+
* An unterminated quote is not an error here (unlike mppx's reference
|
|
174
|
+
* parser, which throws): the loop runs out of input and returns whatever was
|
|
175
|
+
* accumulated, matching {@link findStructuredChallengeEnd}'s equally
|
|
176
|
+
* permissive fallback for the same input shape.
|
|
177
|
+
*/
|
|
178
|
+
function readQuotedValue(input, start) {
|
|
179
|
+
let i = start;
|
|
180
|
+
let value = '';
|
|
181
|
+
let escaped = false;
|
|
182
|
+
while (i < input.length) {
|
|
183
|
+
const ch = input[i];
|
|
184
|
+
i++;
|
|
185
|
+
if (escaped) {
|
|
186
|
+
value += ch;
|
|
187
|
+
escaped = false;
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
if (ch === '\\') {
|
|
191
|
+
escaped = true;
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
if (ch === '"')
|
|
195
|
+
return [value, i];
|
|
196
|
+
value += ch;
|
|
197
|
+
}
|
|
198
|
+
return [value, i];
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Splits `key="value", key2="value2"` into a map. Quoted values are read
|
|
202
|
+
* escape-aware and unescaped ({@link readQuotedValue}), so a value may
|
|
203
|
+
* itself contain a comma, a quote (`\"`) or a backslash (`\\`) without
|
|
204
|
+
* corrupting the param that follows it. Unquoted values are tolerated too.
|
|
205
|
+
*/
|
|
206
|
+
function parseAuthParams(input) {
|
|
207
|
+
const params = {};
|
|
208
|
+
let i = 0;
|
|
209
|
+
while (i < input.length) {
|
|
210
|
+
while (i < input.length && /[\s,]/.test(input[i]))
|
|
211
|
+
i++;
|
|
212
|
+
if (i >= input.length)
|
|
213
|
+
break;
|
|
214
|
+
const keyStart = i;
|
|
215
|
+
while (i < input.length && /[a-zA-Z0-9_-]/.test(input[i]))
|
|
216
|
+
i++;
|
|
217
|
+
const key = input.slice(keyStart, i);
|
|
218
|
+
if (!key)
|
|
219
|
+
break;
|
|
220
|
+
while (i < input.length && /\s/.test(input[i]))
|
|
221
|
+
i++;
|
|
222
|
+
if (input[i] !== '=')
|
|
223
|
+
break;
|
|
224
|
+
i++;
|
|
225
|
+
while (i < input.length && /\s/.test(input[i]))
|
|
226
|
+
i++;
|
|
227
|
+
let value;
|
|
228
|
+
if (input[i] === '"') {
|
|
229
|
+
const [decoded, nextIndex] = readQuotedValue(input, i + 1);
|
|
230
|
+
value = decoded;
|
|
231
|
+
i = nextIndex;
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
const valueStart = i;
|
|
235
|
+
while (i < input.length && input[i] !== ',')
|
|
236
|
+
i++;
|
|
237
|
+
value = input.slice(valueStart, i).trim();
|
|
238
|
+
}
|
|
239
|
+
params[key] = value;
|
|
240
|
+
}
|
|
241
|
+
return params;
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Decodes a base64url string as JSON, raising a typed {@link MppError}
|
|
245
|
+
* rather than a raw `SyntaxError` on malformed input.
|
|
246
|
+
*
|
|
247
|
+
* `Buffer.from(x, 'base64url')` never throws — it silently drops invalid
|
|
248
|
+
* characters — so garbage would otherwise reach `JSON.parse` and escape as a
|
|
249
|
+
* bare `SyntaxError` that names neither MPP nor payment, invisible to a
|
|
250
|
+
* caller writing `catch (e) { if (e instanceof MppError) … }` exactly as our
|
|
251
|
+
* own docs tell them to.
|
|
252
|
+
*/
|
|
253
|
+
function decodeBase64UrlJson(encoded, context) {
|
|
254
|
+
try {
|
|
255
|
+
return JSON.parse(Buffer.from(encoded, 'base64url').toString('utf8'));
|
|
256
|
+
}
|
|
257
|
+
catch (error) {
|
|
258
|
+
throw new MppError(`Could not decode the ${context}: ${error instanceof Error ? error.message : String(error)}`);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Validates a decoded `request` param's shape before it is trusted.
|
|
263
|
+
*
|
|
264
|
+
* The raw decode only guarantees valid JSON, not the right shape: a remote
|
|
265
|
+
* challenge's `request=` can decode to `null`, an array, or `{}`, all of
|
|
266
|
+
* which would otherwise sail through a bare type cast and reach
|
|
267
|
+
* `payments.mpp.fetch` with an unusable or `undefined` `planId`.
|
|
268
|
+
*
|
|
269
|
+
* `planId` is the field with the documented failure mode (an unusable or
|
|
270
|
+
* `undefined` value reaching the mint) and is checked strictly: a non-empty
|
|
271
|
+
* string, full stop. `agentId` is unchecked structurally but IS load-bearing
|
|
272
|
+
* once present — the buyer helper forwards it straight into the token mint
|
|
273
|
+
* (`options.agentId ?? challenge.request.agentId`) — so a wrong-typed value
|
|
274
|
+
* is rejected here rather than reaching that spend path. `credits`, by
|
|
275
|
+
* contrast, is not what anything spends: the amount the backend re-derives
|
|
276
|
+
* comes from `requestEncoded`, forwarded byte-verbatim, so rejecting a
|
|
277
|
+
* perfectly reasonable JSON-number encoding of "credits" would make a valid
|
|
278
|
+
* third-party seller wholly unpayable over a field this SDK does not itself
|
|
279
|
+
* act on — it is coerced by {@link toChallengeRequest} instead.
|
|
280
|
+
*/
|
|
281
|
+
function isValidChallengeRequestShape(value) {
|
|
282
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
283
|
+
return false;
|
|
284
|
+
const { planId, credits, agentId } = value;
|
|
285
|
+
if (typeof planId !== 'string' || planId === '')
|
|
286
|
+
return false;
|
|
287
|
+
if (typeof credits !== 'string' && typeof credits !== 'number')
|
|
288
|
+
return false;
|
|
289
|
+
if (agentId !== undefined && typeof agentId !== 'string')
|
|
290
|
+
return false;
|
|
291
|
+
return true;
|
|
292
|
+
}
|
|
293
|
+
/** Normalizes a validated decoded request into {@link MppChallengeRequest}'s
|
|
294
|
+
* declared shape, coercing a numeric `credits` to the decimal string the
|
|
295
|
+
* type promises. */
|
|
296
|
+
function toChallengeRequest(shape) {
|
|
297
|
+
return {
|
|
298
|
+
planId: shape.planId,
|
|
299
|
+
credits: String(shape.credits),
|
|
300
|
+
...(shape.agentId !== undefined && { agentId: shape.agentId }),
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Parses a `WWW-Authenticate` header into a challenge.
|
|
305
|
+
*
|
|
306
|
+
* @returns `null` when there is no usable `Payment` challenge to parse: the
|
|
307
|
+
* header carries no `Payment` scheme at all, OR the scheme is present but
|
|
308
|
+
* missing one of the structural auth-params (`id`/`realm`/`method`/`intent`/
|
|
309
|
+
* `request`) needed to even attempt decoding it. Both cases mean the same
|
|
310
|
+
* thing to a caller deciding whether to retry: there is nothing here to pay.
|
|
311
|
+
*
|
|
312
|
+
* A `Payment` scheme that has all of those structural params present, but
|
|
313
|
+
* whose `request` value fails to decode as JSON or decodes to something
|
|
314
|
+
* that is not a usable `{ planId, credits }` object, raises a typed
|
|
315
|
+
* {@link MppError} instead — that is a seller who tried to speak MPP and
|
|
316
|
+
* sent something broken, a distinct failure from "there is no challenge
|
|
317
|
+
* here to parse".
|
|
318
|
+
*/
|
|
319
|
+
export function parseChallengeHeader(headerValue) {
|
|
320
|
+
const scheme = extractPaymentScheme(headerValue);
|
|
321
|
+
if (!scheme)
|
|
322
|
+
return null;
|
|
323
|
+
const params = parseAuthParams(scheme.replace(/^Payment\s+/i, ''));
|
|
324
|
+
const { id, realm, method, intent, request, expires, digest, opaque, description } = params;
|
|
325
|
+
if (!id || !realm || !method || !intent || !request)
|
|
326
|
+
return null;
|
|
327
|
+
const decodedRequest = decodeBase64UrlJson(request, 'MPP challenge request parameter');
|
|
328
|
+
if (!isValidChallengeRequestShape(decodedRequest)) {
|
|
329
|
+
throw new MppError('The MPP challenge names a request parameter that is not a valid ' +
|
|
330
|
+
'{ planId: string, credits: string | number, agentId?: string } object.');
|
|
331
|
+
}
|
|
332
|
+
return {
|
|
333
|
+
id,
|
|
334
|
+
realm,
|
|
335
|
+
method,
|
|
336
|
+
intent,
|
|
337
|
+
request: toChallengeRequest(decodedRequest),
|
|
338
|
+
requestEncoded: request,
|
|
339
|
+
...(expires && { expires }),
|
|
340
|
+
...(digest && { digest }),
|
|
341
|
+
...(opaque && { opaque }),
|
|
342
|
+
...(description && { description }),
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Builds the `Authorization: Payment …` value for a challenge.
|
|
347
|
+
*
|
|
348
|
+
* Key order in the JSON is irrelevant — the server parses it — but the two
|
|
349
|
+
* base64url strings are emitted untouched, which is what keeps the HMAC valid.
|
|
350
|
+
*/
|
|
351
|
+
export function buildCredentialHeader(challenge, payload) {
|
|
352
|
+
const wire = {
|
|
353
|
+
challenge: {
|
|
354
|
+
id: challenge.id,
|
|
355
|
+
realm: challenge.realm,
|
|
356
|
+
method: challenge.method,
|
|
357
|
+
intent: challenge.intent,
|
|
358
|
+
...(challenge.expires && { expires: challenge.expires }),
|
|
359
|
+
...(challenge.digest && { digest: challenge.digest }),
|
|
360
|
+
...(challenge.description && { description: challenge.description }),
|
|
361
|
+
...(challenge.opaque && { opaque: challenge.opaque }),
|
|
362
|
+
request: challenge.requestEncoded,
|
|
363
|
+
},
|
|
364
|
+
payload,
|
|
365
|
+
};
|
|
366
|
+
return `Payment ${Buffer.from(JSON.stringify(wire), 'utf8').toString('base64url')}`;
|
|
367
|
+
}
|
|
368
|
+
/** Validates a decoded `Payment-Receipt` body against {@link MppReceipt}'s
|
|
369
|
+
* shape — mirrors {@link isValidChallengeRequestShape}'s strictness for the
|
|
370
|
+
* same reason: without it, `null`, an array, or `{}` all sail through a
|
|
371
|
+
* bare type cast and surface later as an untyped `TypeError` on a field
|
|
372
|
+
* access, with nothing pointing back at the header that caused it. */
|
|
373
|
+
function isValidReceipt(value) {
|
|
374
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
375
|
+
return false;
|
|
376
|
+
const { method, reference, status, timestamp } = value;
|
|
377
|
+
return (typeof method === 'string' &&
|
|
378
|
+
typeof reference === 'string' &&
|
|
379
|
+
reference !== '' &&
|
|
380
|
+
typeof status === 'string' &&
|
|
381
|
+
typeof timestamp === 'string');
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Decodes a `Payment-Receipt` header value.
|
|
385
|
+
*
|
|
386
|
+
* Raises a typed {@link MppError} on malformed input — undecodable base64url
|
|
387
|
+
* JSON, or JSON that decodes to something that is not a usable
|
|
388
|
+
* `{ method, reference, status, timestamp }` object — rather than a raw
|
|
389
|
+
* `SyntaxError` or an untyped receipt. This function does not decide whether
|
|
390
|
+
* that failure is fatal for its caller (the receipt is "unsigned by design,
|
|
391
|
+
* and carries no balance", so a caller may reasonably treat a decode
|
|
392
|
+
* failure as non-fatal and simply omit the receipt); it only guarantees the
|
|
393
|
+
* failure is typed and raised at the boundary, not as a later `TypeError`
|
|
394
|
+
* on a field access with nothing pointing back at the header that caused it
|
|
395
|
+
* — the same asymmetry {@link parseChallengeHeader} closes for `request=`.
|
|
396
|
+
*/
|
|
397
|
+
export function parseReceiptHeader(headerValue) {
|
|
398
|
+
const decoded = decodeBase64UrlJson(headerValue.trim(), 'MPP receipt');
|
|
399
|
+
if (!isValidReceipt(decoded)) {
|
|
400
|
+
throw new MppError('The Payment-Receipt header is not a valid ' +
|
|
401
|
+
'{ method: string, reference: string, status: string, timestamp: string } object.');
|
|
402
|
+
}
|
|
403
|
+
return decoded;
|
|
404
|
+
}
|
|
405
|
+
//# sourceMappingURL=codec.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"codec.js","sourceRoot":"","sources":["../../src/mpp/codec.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The MPP wire format, hand-written so the SDK takes no protocol dependency.\n *\n * The one rule that matters: the backend re-derives the challenge id as\n * `HMAC(secret, realm|method|intent|canonicalize(request)|expires|digest|opaque)`\n * from the fields carried inside the credential. `request` and `opaque` are\n * therefore passed through as the exact base64url strings received — this file\n * never re-encodes them.\n */\n\nimport { MppError } from './errors.js'\nimport type { MppChallenge, MppChallengeRequest, MppReceipt } from './types.js'\n\n/** A bare token (RFC 9110 §5.6.2, used for auth-scheme names and auth-param keys). */\nconst TOKEN = \"[!#$%&'*+\\\\-.^_`|~0-9A-Za-z]+\"\n/** Matches the start of a `key=` auth-param, i.e. a Payment challenge continuing. */\nconst AUTH_PARAM_START = new RegExp(`^${TOKEN}\\\\s*=`)\n\n/**\n * Finds where a structured challenge's auth-param list ends within `rest`\n * (the content right after \"Payment \"), scanning quote- and escape-aware so\n * neither a comma nor a `\\\"` inside a `\"…\"` value is mistaken for a\n * boundary. mppx serializes quoted values with\n * `value.replace(/\\\\/g,'\\\\\\\\').replace(/\"/g,'\\\\\"')` (`Challenge.ts:316-319`),\n * so a seller-supplied `description` like `5\" screen replacement plan` wire-\n * encodes as `description=\"5\\\" screen replacement plan\"`. A scanner that\n * toggles quote state on every literal `\"` — escaped or not — desyncs on\n * that `\\\"`, stays stuck \"inside\" a quote for the rest of the header, and\n * swallows a genuine trailing scheme. This mirrors mppx's own reference\n * parser's `escaped` flag (`Challenge.ts:362-379`) rather than inventing new\n * semantics.\n *\n * A top-level (non-quoted, non-escaped) comma only ends the scheme if what\n * follows it does NOT itself look like a continuing `key=value` auth-param —\n * which is also how a following literal \"Payment \" (the merged-challenge\n * case) is recognized as a new scheme: \"Payment\" followed by whitespace\n * never matches `AUTH_PARAM_START`, since that requires \"=\" immediately\n * (modulo optional whitespace) after the leading token.\n *\n * An unterminated quote is not an error here: the loop simply runs out of\n * input without finding a boundary and falls back to treating the whole\n * remainder as this scheme — a safe, total (never-throwing) O(n) fallback.\n */\nfunction findStructuredChallengeEnd(rest: string): number {\n let inQuotes = false\n let escaped = false\n for (let i = 0; i < rest.length; i++) {\n const ch = rest[i]\n if (inQuotes) {\n if (escaped) {\n escaped = false\n } else if (ch === '\\\\') {\n escaped = true\n } else if (ch === '\"') {\n inQuotes = false\n }\n continue\n }\n if (ch === '\"') {\n inQuotes = true\n continue\n }\n if (ch === ',' && !AUTH_PARAM_START.test(rest.slice(i + 1).trimStart())) {\n return i\n }\n }\n return rest.length\n}\n\n/**\n * Matches the `Payment` scheme name at a genuine scheme boundary: start of\n * the header value, or right after a top-level comma (RFC 9110 §11.6.1\n * separates comma-separated challenges/credentials that way). Unanchored,\n * `/Payment\\s+/i` matches mid-token — `XPayment abc`, `NotPayment abc`,\n * `Bearer prepayment xyz` — and also matches \"Payment\" text embedded inside\n * a *different*, preceding scheme's quoted value (e.g.\n * `Digest username=\"my payment plan\", …`), since plain whitespace alone is\n * not a scheme boundary. Anchoring on comma-or-start (not bare whitespace)\n * closes both: a scheme name is never preceded by an arbitrary space, only\n * by the start of the header or the comma that separates it from what came\n * before.\n *\n * This decides which protocol handles a request:\n * `extractCredential` → `extractPaymentScheme` feeds the MPP-vs-x402 routing\n * predicate in `middleware.ts`, so an unanchored match would divert an\n * x402 buyer carrying a perfectly valid `payment-signature` token, plus an\n * unrelated `Authorization` header that happens to contain \"payment\" text,\n * onto the MPP path and challenge them instead of serving the request.\n */\nconst PAYMENT_SCHEME_BOUNDARY = /(?:^|,)\\s*(Payment\\s+)/i\n\n/**\n * Extracts the `Payment` scheme from a header value that may carry several\n * schemes comma-separated (RFC 9110 §11.6.1).\n *\n * The `Payment` scheme takes one of two shapes on our wire: a bare token68\n * credential (`Payment <base64url>`, which cannot itself contain a comma or\n * a quote) or a structured challenge (`Payment id=\"...\", realm=\"...\", ...`,\n * comma-separated `key=\"value\"` auth-params, where a value MAY contain a\n * comma). Bounding the match at the next literal \"Payment \" occurrence — or\n * at end-of-string otherwise — corrupts the first shape whenever a\n * *different* trailing scheme follows: e.g. `Payment <token>, Bearer <jwt>`\n * used to extract the whole remainder including the trailing scheme.\n * Bounding a structured challenge with a naive, quote-unaware comma split\n * corrupts the second shape whenever a value contains a comma: it truncates\n * mid-quote and silently drops every param after it. Instead: a bare\n * token68 is bounded by its first top-level comma; a structured challenge is\n * bounded by {@link findStructuredChallengeEnd}'s quote-aware scan.\n */\nexport function extractPaymentScheme(headerValue: string): string | null {\n const schemeMatch = PAYMENT_SCHEME_BOUNDARY.exec(headerValue)\n if (!schemeMatch || schemeMatch.index === undefined) return null\n\n // Group 1 (\"Payment\\s+\") is the tail of the whole match, so the full\n // match's end position is also where the captured keyword ends; its start\n // is offset back by the keyword's own length, which skips the leading\n // comma/whitespace the boundary alternation consumed.\n const keyword = schemeMatch[1]\n const contentStart = schemeMatch.index + schemeMatch[0].length\n const start = contentStart - keyword.length\n const rest = headerValue.slice(contentStart)\n\n if (!AUTH_PARAM_START.test(rest)) {\n // Bare token68 credential: bounded by its first top-level comma, since a\n // token68 cannot itself contain one (or a quote, so no quote-tracking\n // is needed here).\n const commaIndex = rest.indexOf(',')\n const end = commaIndex === -1 ? headerValue.length : contentStart + commaIndex\n return headerValue.slice(start, end).trim()\n }\n\n const end = contentStart + findStructuredChallengeEnd(rest)\n return headerValue.slice(start, end).replace(/,\\s*$/, '').trim()\n}\n\n/**\n * The stable identity of a credential: the challenge id it carries.\n *\n * The header bytes are NOT that identity. `PAYMENT_SCHEME_BOUNDARY` is\n * case-insensitive and {@link extractPaymentScheme} returns the matched slice\n * verbatim, so `Payment x`, `payment x` and `Payment x` are three different\n * strings for one credential — and the body is base64url of JSON the BUYER\n * assembles, so re-ordering its keys yields more. The backend collapses every\n * one of them onto a single burn, because its own idempotency key is the\n * decoded challenge id. Anything at the seller edge enforcing single-use has\n * to key on the same thing, or a buyer flips one byte of case and buys the\n * response again.\n *\n * @returns the `challenge.id` a token68 credential decodes to, or `null` when\n * the credential cannot be decoded into one — a structured `key=\"value\"`\n * scheme (that shape is a challenge, never a credential), undecodable\n * base64url JSON, or JSON without a non-empty string `challenge.id`. The\n * backend rejects all of those anyway; returning `null` lets the caller refuse\n * them without a round-trip rather than fall back to a key it cannot trust.\n */\nexport function extractCredentialChallengeId(credential: string): string | null {\n const token68 = credential.replace(/^Payment\\s+/i, '').trim()\n if (!token68 || AUTH_PARAM_START.test(token68)) return null\n\n let decoded: unknown\n try {\n decoded = JSON.parse(Buffer.from(token68, 'base64url').toString('utf8'))\n } catch {\n return null\n }\n if (typeof decoded !== 'object' || decoded === null || Array.isArray(decoded)) return null\n\n const { challenge } = decoded as { challenge?: unknown }\n if (typeof challenge !== 'object' || challenge === null || Array.isArray(challenge)) return null\n\n const { id } = challenge as { id?: unknown }\n return typeof id === 'string' && id !== '' ? id : null\n}\n\n/**\n * Reads a quoted-string value starting right after its opening `\"`,\n * unescaping `\\\"` to a literal `\"` and `\\\\` to a literal `\\` (mirrors\n * mppx's `readQuotedAuthParamValue`, `Challenge.ts:461-465`).\n *\n * An unterminated quote is not an error here (unlike mppx's reference\n * parser, which throws): the loop runs out of input and returns whatever was\n * accumulated, matching {@link findStructuredChallengeEnd}'s equally\n * permissive fallback for the same input shape.\n */\nfunction readQuotedValue(input: string, start: number): [value: string, nextIndex: number] {\n let i = start\n let value = ''\n let escaped = false\n while (i < input.length) {\n const ch = input[i]\n i++\n if (escaped) {\n value += ch\n escaped = false\n continue\n }\n if (ch === '\\\\') {\n escaped = true\n continue\n }\n if (ch === '\"') return [value, i]\n value += ch\n }\n return [value, i]\n}\n\n/**\n * Splits `key=\"value\", key2=\"value2\"` into a map. Quoted values are read\n * escape-aware and unescaped ({@link readQuotedValue}), so a value may\n * itself contain a comma, a quote (`\\\"`) or a backslash (`\\\\`) without\n * corrupting the param that follows it. Unquoted values are tolerated too.\n */\nfunction parseAuthParams(input: string): Record<string, string> {\n const params: Record<string, string> = {}\n let i = 0\n\n while (i < input.length) {\n while (i < input.length && /[\\s,]/.test(input[i])) i++\n if (i >= input.length) break\n\n const keyStart = i\n while (i < input.length && /[a-zA-Z0-9_-]/.test(input[i])) i++\n const key = input.slice(keyStart, i)\n if (!key) break\n\n while (i < input.length && /\\s/.test(input[i])) i++\n if (input[i] !== '=') break\n i++\n while (i < input.length && /\\s/.test(input[i])) i++\n\n let value: string\n if (input[i] === '\"') {\n const [decoded, nextIndex] = readQuotedValue(input, i + 1)\n value = decoded\n i = nextIndex\n } else {\n const valueStart = i\n while (i < input.length && input[i] !== ',') i++\n value = input.slice(valueStart, i).trim()\n }\n\n params[key] = value\n }\n\n return params\n}\n\n/**\n * Decodes a base64url string as JSON, raising a typed {@link MppError}\n * rather than a raw `SyntaxError` on malformed input.\n *\n * `Buffer.from(x, 'base64url')` never throws — it silently drops invalid\n * characters — so garbage would otherwise reach `JSON.parse` and escape as a\n * bare `SyntaxError` that names neither MPP nor payment, invisible to a\n * caller writing `catch (e) { if (e instanceof MppError) … }` exactly as our\n * own docs tell them to.\n */\nfunction decodeBase64UrlJson<T>(encoded: string, context: string): T {\n try {\n return JSON.parse(Buffer.from(encoded, 'base64url').toString('utf8')) as T\n } catch (error) {\n throw new MppError(\n `Could not decode the ${context}: ${error instanceof Error ? error.message : String(error)}`,\n )\n }\n}\n\n/** The shape a decoded `request` param must have before it is trusted — see\n * {@link isValidChallengeRequestShape}. `credits` is intentionally looser\n * than {@link MppChallengeRequest}'s declared `string`: it is coerced, not\n * rejected, by {@link toChallengeRequest}. */\ninterface DecodedChallengeRequestShape {\n planId: string\n credits: string | number\n agentId?: string\n}\n\n/**\n * Validates a decoded `request` param's shape before it is trusted.\n *\n * The raw decode only guarantees valid JSON, not the right shape: a remote\n * challenge's `request=` can decode to `null`, an array, or `{}`, all of\n * which would otherwise sail through a bare type cast and reach\n * `payments.mpp.fetch` with an unusable or `undefined` `planId`.\n *\n * `planId` is the field with the documented failure mode (an unusable or\n * `undefined` value reaching the mint) and is checked strictly: a non-empty\n * string, full stop. `agentId` is unchecked structurally but IS load-bearing\n * once present — the buyer helper forwards it straight into the token mint\n * (`options.agentId ?? challenge.request.agentId`) — so a wrong-typed value\n * is rejected here rather than reaching that spend path. `credits`, by\n * contrast, is not what anything spends: the amount the backend re-derives\n * comes from `requestEncoded`, forwarded byte-verbatim, so rejecting a\n * perfectly reasonable JSON-number encoding of \"credits\" would make a valid\n * third-party seller wholly unpayable over a field this SDK does not itself\n * act on — it is coerced by {@link toChallengeRequest} instead.\n */\nfunction isValidChallengeRequestShape(value: unknown): value is DecodedChallengeRequestShape {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) return false\n const { planId, credits, agentId } = value as Record<string, unknown>\n if (typeof planId !== 'string' || planId === '') return false\n if (typeof credits !== 'string' && typeof credits !== 'number') return false\n if (agentId !== undefined && typeof agentId !== 'string') return false\n return true\n}\n\n/** Normalizes a validated decoded request into {@link MppChallengeRequest}'s\n * declared shape, coercing a numeric `credits` to the decimal string the\n * type promises. */\nfunction toChallengeRequest(shape: DecodedChallengeRequestShape): MppChallengeRequest {\n return {\n planId: shape.planId,\n credits: String(shape.credits),\n ...(shape.agentId !== undefined && { agentId: shape.agentId }),\n }\n}\n\n/**\n * Parses a `WWW-Authenticate` header into a challenge.\n *\n * @returns `null` when there is no usable `Payment` challenge to parse: the\n * header carries no `Payment` scheme at all, OR the scheme is present but\n * missing one of the structural auth-params (`id`/`realm`/`method`/`intent`/\n * `request`) needed to even attempt decoding it. Both cases mean the same\n * thing to a caller deciding whether to retry: there is nothing here to pay.\n *\n * A `Payment` scheme that has all of those structural params present, but\n * whose `request` value fails to decode as JSON or decodes to something\n * that is not a usable `{ planId, credits }` object, raises a typed\n * {@link MppError} instead — that is a seller who tried to speak MPP and\n * sent something broken, a distinct failure from \"there is no challenge\n * here to parse\".\n */\nexport function parseChallengeHeader(headerValue: string): MppChallenge | null {\n const scheme = extractPaymentScheme(headerValue)\n if (!scheme) return null\n\n const params = parseAuthParams(scheme.replace(/^Payment\\s+/i, ''))\n const { id, realm, method, intent, request, expires, digest, opaque, description } = params\n if (!id || !realm || !method || !intent || !request) return null\n\n const decodedRequest = decodeBase64UrlJson<unknown>(request, 'MPP challenge request parameter')\n if (!isValidChallengeRequestShape(decodedRequest)) {\n throw new MppError(\n 'The MPP challenge names a request parameter that is not a valid ' +\n '{ planId: string, credits: string | number, agentId?: string } object.',\n )\n }\n\n return {\n id,\n realm,\n method,\n intent,\n request: toChallengeRequest(decodedRequest),\n requestEncoded: request,\n ...(expires && { expires }),\n ...(digest && { digest }),\n ...(opaque && { opaque }),\n ...(description && { description }),\n }\n}\n\n/**\n * Builds the `Authorization: Payment …` value for a challenge.\n *\n * Key order in the JSON is irrelevant — the server parses it — but the two\n * base64url strings are emitted untouched, which is what keeps the HMAC valid.\n */\nexport function buildCredentialHeader(\n challenge: MppChallenge,\n payload: { accessToken: string },\n): string {\n const wire = {\n challenge: {\n id: challenge.id,\n realm: challenge.realm,\n method: challenge.method,\n intent: challenge.intent,\n ...(challenge.expires && { expires: challenge.expires }),\n ...(challenge.digest && { digest: challenge.digest }),\n ...(challenge.description && { description: challenge.description }),\n ...(challenge.opaque && { opaque: challenge.opaque }),\n request: challenge.requestEncoded,\n },\n payload,\n }\n return `Payment ${Buffer.from(JSON.stringify(wire), 'utf8').toString('base64url')}`\n}\n\n/** Validates a decoded `Payment-Receipt` body against {@link MppReceipt}'s\n * shape — mirrors {@link isValidChallengeRequestShape}'s strictness for the\n * same reason: without it, `null`, an array, or `{}` all sail through a\n * bare type cast and surface later as an untyped `TypeError` on a field\n * access, with nothing pointing back at the header that caused it. */\nfunction isValidReceipt(value: unknown): value is MppReceipt {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) return false\n const { method, reference, status, timestamp } = value as Record<string, unknown>\n return (\n typeof method === 'string' &&\n typeof reference === 'string' &&\n reference !== '' &&\n typeof status === 'string' &&\n typeof timestamp === 'string'\n )\n}\n\n/**\n * Decodes a `Payment-Receipt` header value.\n *\n * Raises a typed {@link MppError} on malformed input — undecodable base64url\n * JSON, or JSON that decodes to something that is not a usable\n * `{ method, reference, status, timestamp }` object — rather than a raw\n * `SyntaxError` or an untyped receipt. This function does not decide whether\n * that failure is fatal for its caller (the receipt is \"unsigned by design,\n * and carries no balance\", so a caller may reasonably treat a decode\n * failure as non-fatal and simply omit the receipt); it only guarantees the\n * failure is typed and raised at the boundary, not as a later `TypeError`\n * on a field access with nothing pointing back at the header that caused it\n * — the same asymmetry {@link parseChallengeHeader} closes for `request=`.\n */\nexport function parseReceiptHeader(headerValue: string): MppReceipt {\n const decoded = decodeBase64UrlJson<unknown>(headerValue.trim(), 'MPP receipt')\n if (!isValidReceipt(decoded)) {\n throw new MppError(\n 'The Payment-Receipt header is not a valid ' +\n '{ method: string, reference: string, status: string, timestamp: string } object.',\n )\n }\n return decoded\n}\n"]}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed errors for the MPP surface.
|
|
3
|
+
*
|
|
4
|
+
* The backend deliberately collapses every rejection reason into one code so
|
|
5
|
+
* the endpoint cannot be used as a forgery oracle. The SDK mirrors that: it
|
|
6
|
+
* does not try to reconstruct why a credential was refused.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* What a buyer-side failure reports about money already committed.
|
|
10
|
+
*
|
|
11
|
+
* `payments.mpp.fetch` returns this accounting on its success path
|
|
12
|
+
* (`credentialsPresented` / `creditsPresented` on `MppFetchResult`). It is
|
|
13
|
+
* repeated on the ERROR path because that is where a caller needs it most: the
|
|
14
|
+
* credential and its access token are function-local and gone once the helper
|
|
15
|
+
* throws, so an error without these numbers leaves the caller unable to tell
|
|
16
|
+
* whether money left — the one outcome a buyer helper must never produce.
|
|
17
|
+
*
|
|
18
|
+
* Read it with {@link mppSpendOf} rather than casting: it rides on
|
|
19
|
+
* `PaymentsError` too (a `maxCredits` or `planId` guard can fire on the
|
|
20
|
+
* re-challenge turn, after a credential has already been presented).
|
|
21
|
+
*/
|
|
22
|
+
export interface MppSpendReport {
|
|
23
|
+
/** How many credentials were minted and sent before the failure (0, 1 or 2). */
|
|
24
|
+
credentialsPresented: number;
|
|
25
|
+
/**
|
|
26
|
+
* Total credits named by the challenges those credentials were minted
|
|
27
|
+
* against, as a decimal string. Present whenever `credentialsPresented > 0`.
|
|
28
|
+
*/
|
|
29
|
+
creditsPresented?: string;
|
|
30
|
+
/** `id` of the challenge the last credential was minted against, so the caller can correlate it with the seller's side. */
|
|
31
|
+
challengeId?: string;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Reads the spend accounting off any error raised by `payments.mpp.fetch`.
|
|
35
|
+
*
|
|
36
|
+
* Exported so a caller never has to hand-cast: the field is declared on
|
|
37
|
+
* {@link MppError} but is also attached to the {@link PaymentsError} a
|
|
38
|
+
* caller-constraint guard throws on the re-challenge turn, and those two do
|
|
39
|
+
* not share a base class.
|
|
40
|
+
*
|
|
41
|
+
* A report is attached **only** when at least one credential was presented, so
|
|
42
|
+
* a truthy result always means money may have left, and `undefined` always
|
|
43
|
+
* means nothing was spent. That is what makes `if (mppSpendOf(err))` a usable
|
|
44
|
+
* test rather than one that fires on plain argument validation too.
|
|
45
|
+
*/
|
|
46
|
+
export declare function mppSpendOf(error: unknown): MppSpendReport | undefined;
|
|
47
|
+
export declare class MppError extends Error {
|
|
48
|
+
readonly code?: string | undefined;
|
|
49
|
+
/**
|
|
50
|
+
* Spend accounting, set when this error was raised after a credential had
|
|
51
|
+
* been minted and presented. Absent means nothing was spent. Prefer
|
|
52
|
+
* {@link mppSpendOf}, which also reads it off a `PaymentsError`.
|
|
53
|
+
*/
|
|
54
|
+
spend?: MppSpendReport;
|
|
55
|
+
constructor(message: string, code?: string | undefined);
|
|
56
|
+
}
|
|
57
|
+
/** `BCK.MPP.0002` — the deployment has no MPP secret, so MPP routes are off. */
|
|
58
|
+
export declare class MppNotConfiguredError extends MppError {
|
|
59
|
+
constructor(message?: string);
|
|
60
|
+
}
|
|
61
|
+
/** `BCK.MPP.0003` — the credential was refused (replay, forgery, plan, balance). */
|
|
62
|
+
export declare class MppCredentialRejectedError extends MppError {
|
|
63
|
+
constructor(message?: string);
|
|
64
|
+
}
|
|
65
|
+
/** `BCK.MPP.0004` — the challenge expired. Fetch a fresh one; do not retry blindly. */
|
|
66
|
+
export declare class MppChallengeExpiredError extends MppError {
|
|
67
|
+
constructor(message?: string);
|
|
68
|
+
}
|
|
69
|
+
/** `BCK.MPP.0005` — the body sent does not match the digest sealed in the challenge. */
|
|
70
|
+
export declare class MppBodyDigestMismatchError extends MppError {
|
|
71
|
+
constructor(message?: string);
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Stable `code` on {@link MppSettlementOutcomeUnknownError}. Not a backend
|
|
75
|
+
* code — no `BCK.MPP.*` prefix, so it can never collide with one.
|
|
76
|
+
*/
|
|
77
|
+
export declare const MPP_SETTLEMENT_OUTCOME_UNKNOWN_CODE = "settlement_outcome_unknown";
|
|
78
|
+
/**
|
|
79
|
+
* Raised when settlement's outcome cannot be determined: `settleCredential`'s
|
|
80
|
+
* own outbound deadline firing before any response, or a 2xx whose body could
|
|
81
|
+
* not be read. Settlement is the one MPP call that burns, so in both cases the
|
|
82
|
+
* burn may have already happened even though the caller received nothing
|
|
83
|
+
* usable. Collapsing that into the same `network_error` `MppError` used for
|
|
84
|
+
* "nothing happened" failures (connection refused, DNS failure, a hung
|
|
85
|
+
* challenge/verify call) would let a real burn get logged and treated exactly
|
|
86
|
+
* like one that never occurred — silently corrupting the seller's own
|
|
87
|
+
* accounting on the call that is not safe to shrug off.
|
|
88
|
+
*/
|
|
89
|
+
export declare class MppSettlementOutcomeUnknownError extends MppError {
|
|
90
|
+
constructor(message?: string);
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Stable `code` on {@link MppSpendOutcomeUnknownError}. Not a backend code, for
|
|
94
|
+
* the same reason as {@link MPP_SETTLEMENT_OUTCOME_UNKNOWN_CODE}.
|
|
95
|
+
*/
|
|
96
|
+
export declare const MPP_SPEND_OUTCOME_UNKNOWN_CODE = "spend_outcome_unknown";
|
|
97
|
+
/**
|
|
98
|
+
* The buyer-side mirror of {@link MppSettlementOutcomeUnknownError}: raised
|
|
99
|
+
* when the retry that carries a credential fails at the transport level — a
|
|
100
|
+
* disconnect, a DNS failure, or the caller's own `AbortSignal` firing between
|
|
101
|
+
* the mint and the retry (`init.signal` is re-sent with it).
|
|
102
|
+
*
|
|
103
|
+
* The credential is on the wire by then, so the seller may already have
|
|
104
|
+
* verified and burned it. Left as the raw `TypeError` that `fetch` rejects
|
|
105
|
+
* with, the failure would escape the `catch (e) { if (e instanceof MppError) }`
|
|
106
|
+
* pattern this module and `markdown/mpp-integration.md` both prescribe, and
|
|
107
|
+
* carry no accounting at all: spent, invisible, unrecoverable. Wrapped, it
|
|
108
|
+
* reaches the documented handler with {@link MppSpendReport} attached and the
|
|
109
|
+
* original error preserved on `cause`.
|
|
110
|
+
*
|
|
111
|
+
* A transport failure BEFORE any credential exists is not this error — it stays
|
|
112
|
+
* exactly as `fetch` threw it, because nothing was spent and wrapping it would
|
|
113
|
+
* only obscure a plain network fault.
|
|
114
|
+
*/
|
|
115
|
+
export declare class MppSpendOutcomeUnknownError extends MppError {
|
|
116
|
+
/** The original error `fetch` (or the mint) rejected with. */
|
|
117
|
+
readonly cause?: unknown;
|
|
118
|
+
constructor(message: string, cause?: unknown, spend?: MppSpendReport);
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* What `paymentMiddleware`'s `onAfterSettle` hook receives as its third
|
|
122
|
+
* argument when settlement raised {@link MppSettlementOutcomeUnknownError}.
|
|
123
|
+
* That parameter's declared type is `unknown` (shared with the x402 hook of
|
|
124
|
+
* the same name — out of scope to narrow here), so nothing stops a consumer
|
|
125
|
+
* from casting straight to `MppSettleResult` and silently reading
|
|
126
|
+
* `undefined` for `creditsRedeemed` on this branch. Exporting this shape
|
|
127
|
+
* gives a consumer something concrete to check for instead — e.g.
|
|
128
|
+
* `if ((result as MppSettlementOutcomeUnknown).outcome === 'unknown')` —
|
|
129
|
+
* and documents, at the type level, that the branch exists at all.
|
|
130
|
+
*/
|
|
131
|
+
export interface MppSettlementOutcomeUnknown {
|
|
132
|
+
outcome: 'unknown';
|
|
133
|
+
reason: string;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* `BCK.MPP.*` codes a buyer can retry by minting a fresh credential against
|
|
137
|
+
* the NEW challenge the same 402 carries alongside them — as opposed to
|
|
138
|
+
* `BCK.MPP.0003`, which means the credential itself was refused and paying
|
|
139
|
+
* again cannot help.
|
|
140
|
+
*
|
|
141
|
+
* `0004` (expired) is obviously retryable: fetch a fresh challenge, pay it.
|
|
142
|
+
* `0005` (body digest mismatch) is less obvious but equally retryable: the
|
|
143
|
+
* fresh challenge is sealed to the digest of the request that just arrived,
|
|
144
|
+
* so a credential minted against it — presented with the SAME body — would
|
|
145
|
+
* succeed. A buyer gate that only excepts `0004` from a bare
|
|
146
|
+
* `code.startsWith('BCK.MPP.')` check wrongly treats `0005` as terminal and
|
|
147
|
+
* gives up on a request that would have worked on the next attempt.
|
|
148
|
+
*
|
|
149
|
+
* Grouped here, once, so a buyer checks {@link isRetryableMppCode} instead of
|
|
150
|
+
* hardcoding the exception list — and so a future retryable code only needs
|
|
151
|
+
* to be added to this one set.
|
|
152
|
+
*
|
|
153
|
+
* Exported so the buyer's own tests derive their table from this set instead of
|
|
154
|
+
* repeating its members, which is what makes "a code added here is honoured by
|
|
155
|
+
* `payments.mpp.fetch`" an enforced property rather than a convention. It is
|
|
156
|
+
* deliberately NOT re-exported from `./index.js`: a mutable-looking membership
|
|
157
|
+
* list is not something to make public API.
|
|
158
|
+
*/
|
|
159
|
+
export declare const RETRYABLE_BCK_MPP_CODES: ReadonlySet<string>;
|
|
160
|
+
/** Whether a `BCK.MPP.*` code means "mint a fresh credential and try again"
|
|
161
|
+
* rather than "this credential was refused; a new one changes nothing". */
|
|
162
|
+
export declare function isRetryableMppCode(code: string | undefined): boolean;
|
|
163
|
+
/** Maps a backend error payload onto the typed error hierarchy. */
|
|
164
|
+
export declare function toMppError(code: string | undefined, message: string): MppError;
|
|
165
|
+
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/mpp/errors.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH;;;;;;;;;;;;;GAaG;AACH,MAAM,WAAW,cAAc;IAC7B,gFAAgF;IAChF,oBAAoB,EAAE,MAAM,CAAA;IAC5B;;;OAGG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,2HAA2H;IAC3H,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,OAAO,GAAG,cAAc,GAAG,SAAS,CAOrE;AAED,qBAAa,QAAS,SAAQ,KAAK;IAU/B,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM;IATxB;;;;OAIG;IACH,KAAK,CAAC,EAAE,cAAc,CAAA;gBAGpB,OAAO,EAAE,MAAM,EACN,IAAI,CAAC,EAAE,MAAM,YAAA;CAKzB;AAED,gFAAgF;AAChF,qBAAa,qBAAsB,SAAQ,QAAQ;gBACrC,OAAO,SAA8C;CAIlE;AAED,oFAAoF;AACpF,qBAAa,0BAA2B,SAAQ,QAAQ;gBAC1C,OAAO,SAA4B;CAIhD;AAED,uFAAuF;AACvF,qBAAa,wBAAyB,SAAQ,QAAQ;gBACxC,OAAO,SAA0B;CAI9C;AAED,wFAAwF;AACxF,qBAAa,0BAA2B,SAAQ,QAAQ;gBAC1C,OAAO,SAA6B;CAIjD;AAED;;;GAGG;AACH,eAAO,MAAM,mCAAmC,+BAA+B,CAAA;AAE/E;;;;;;;;;;GAUG;AACH,qBAAa,gCAAiC,SAAQ,QAAQ;gBAE1D,OAAO,SAAuI;CAejJ;AAED;;;GAGG;AACH,eAAO,MAAM,8BAA8B,0BAA0B,CAAA;AAErE;;;;;;;;;;;;;;;;;GAiBG;AACH,qBAAa,2BAA4B,SAAQ,QAAQ;IACvD,8DAA8D;IAC9D,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAA;gBAEZ,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO,EAAE,KAAK,CAAC,EAAE,cAAc;CAMrE;AAED;;;;;;;;;;GAUG;AACH,MAAM,WAAW,2BAA2B;IAC1C,OAAO,EAAE,SAAS,CAAA;IAClB,MAAM,EAAE,MAAM,CAAA;CACf;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,eAAO,MAAM,uBAAuB,EAAE,WAAW,CAAC,MAAM,CAGtD,CAAA;AAEF;4EAC4E;AAC5E,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAEpE;AAED,mEAAmE;AACnE,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,EAAE,OAAO,EAAE,MAAM,GAAG,QAAQ,CAa9E"}
|