@nevermined-io/payments 1.10.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/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 +1 -1
- package/dist/common/types.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 +1 -1
|
@@ -0,0 +1,493 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The buyer half of MPP: pay a challenged endpoint with an existing Nevermined
|
|
3
|
+
* delegation.
|
|
4
|
+
*
|
|
5
|
+
* The buyer learns nothing about MPP beyond calling this instead of `fetch`.
|
|
6
|
+
* The plan comes out of the challenge, the credential is built from the
|
|
7
|
+
* challenge plus an MPP-domain access token, and the request is retried once.
|
|
8
|
+
*
|
|
9
|
+
* Two error families are used deliberately:
|
|
10
|
+
* - {@link PaymentsError} (`code: 'validation'`) for guards this call refuses
|
|
11
|
+
* to even attempt — a bad argument, a challenge that violates a
|
|
12
|
+
* caller-supplied constraint (`planId`, `maxCredits`), or a body that
|
|
13
|
+
* cannot be replayed. None of these mean a payment failed; nothing was ever
|
|
14
|
+
* attempted.
|
|
15
|
+
* - {@link MppError} (and its typed subclasses) for what the wire actually
|
|
16
|
+
* said: a rejected credential, a malformed challenge, an MPP-disabled
|
|
17
|
+
* environment. A caller branching on `instanceof MppError` to mean
|
|
18
|
+
* "the payment failed" gets exactly that, and no more.
|
|
19
|
+
*/
|
|
20
|
+
import { PaymentsError } from '../common/payments.error.js';
|
|
21
|
+
import { buildCredentialHeader, parseChallengeHeader, parseReceiptHeader } from './codec.js';
|
|
22
|
+
import { MppError, MppSpendOutcomeUnknownError, isRetryableMppCode, toMppError } from './errors.js';
|
|
23
|
+
/**
|
|
24
|
+
* Whether `body` is a `ReadableStream`.
|
|
25
|
+
*
|
|
26
|
+
* A stream is single-read: once a `fetch()` starts consuming it, the stream
|
|
27
|
+
* is locked/disturbed, and a *second* `fetch()` with the same `init.body`
|
|
28
|
+
* throws an opaque runtime `TypeError` rather than a typed error. Every other
|
|
29
|
+
* `BodyInit` — `string`, `Buffer`/`ArrayBuffer`/typed arrays,
|
|
30
|
+
* `URLSearchParams`, `FormData`, `Blob`, or no body at all — can be read more
|
|
31
|
+
* than once, so a retry is safe.
|
|
32
|
+
*/
|
|
33
|
+
function isNonReplayableBody(body) {
|
|
34
|
+
return typeof ReadableStream !== 'undefined' && body instanceof ReadableStream;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Cap on how much of a 402's error body is buffered before parsing it.
|
|
38
|
+
*
|
|
39
|
+
* The body is seller-controlled and read purely to look for a `code`, so a
|
|
40
|
+
* hostile or broken endpoint answering a 402 with an unbounded stream must not
|
|
41
|
+
* be able to exhaust the buyer's memory.
|
|
42
|
+
*
|
|
43
|
+
* A truncated read of a body large enough to matter will usually not parse; if
|
|
44
|
+
* it does, the prefix necessarily contains the complete `code` object — a
|
|
45
|
+
* truncated prefix of one JSON object can only parse once its closing brace is
|
|
46
|
+
* inside the cap — so the classification is unchanged either way. The branch
|
|
47
|
+
* keys on `JSON.parse` succeeding, never on "truncated implies unparseable",
|
|
48
|
+
* which is false: `<complete JSON><whitespace><garbage>` parses after
|
|
49
|
+
* truncation and throws before it.
|
|
50
|
+
*/
|
|
51
|
+
const MAX_ERROR_BODY_BYTES = 64 * 1024;
|
|
52
|
+
/**
|
|
53
|
+
* Receipt `status` values that state failure outright, lower-cased.
|
|
54
|
+
*
|
|
55
|
+
* `MppReceipt.status` is a seller-set string and this SDK deliberately does not
|
|
56
|
+
* try to recognize *success*: `'success'` is the only value with any agreement
|
|
57
|
+
* behind it, so treating an unrecognized `'ok'`/`'completed'` as failure would
|
|
58
|
+
* report an unpaid call that was in fact paid. The asymmetry is the point — an
|
|
59
|
+
* explicit negative is not an unknown vocabulary, and reporting `paid: true`
|
|
60
|
+
* for a receipt that says the settlement failed is wrong in the one direction
|
|
61
|
+
* this field must never be wrong.
|
|
62
|
+
*
|
|
63
|
+
* Only unambiguous negatives belong here; anything genuinely ambiguous stays
|
|
64
|
+
* unrecognized and is reported as settled, with `receipt` on the result for a
|
|
65
|
+
* caller that wants to judge for itself.
|
|
66
|
+
*/
|
|
67
|
+
const EXPLICIT_FAILURE_RECEIPT_STATUSES = new Set([
|
|
68
|
+
'failed',
|
|
69
|
+
'failure',
|
|
70
|
+
'declined',
|
|
71
|
+
'error',
|
|
72
|
+
]);
|
|
73
|
+
/** Whether a decoded receipt states outright that the settlement did not happen. */
|
|
74
|
+
function statesFailure(receipt) {
|
|
75
|
+
return (receipt !== undefined &&
|
|
76
|
+
typeof receipt.status === 'string' &&
|
|
77
|
+
EXPLICIT_FAILURE_RECEIPT_STATUSES.has(receipt.status.trim().toLowerCase()));
|
|
78
|
+
}
|
|
79
|
+
/** The origin of `input`, or the raw value when it does not parse as a URL — used only to label a remote error. */
|
|
80
|
+
function originOf(input) {
|
|
81
|
+
try {
|
|
82
|
+
return new URL(String(input)).origin;
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return String(input);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Parses a challenge header, swallowing any decode failure into `null`.
|
|
90
|
+
*
|
|
91
|
+
* Used only to peek at a re-challenge's freshness inside the retry gate — a
|
|
92
|
+
* garbled re-challenge is simply "not a fresh challenge", which the gate
|
|
93
|
+
* already treats as terminal. The FIRST challenge of a 402 is never parsed
|
|
94
|
+
* this leniently: see the `try`/`catch` around {@link parseChallengeHeader}
|
|
95
|
+
* in {@link mppFetch}, which throws a typed error instead, since minting
|
|
96
|
+
* against a challenge this function could not even parse would be worse.
|
|
97
|
+
*/
|
|
98
|
+
function tryParseChallenge(headerValue) {
|
|
99
|
+
try {
|
|
100
|
+
return parseChallengeHeader(headerValue);
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Refuses a challenge whose `credits` is not a decimal string before it is
|
|
108
|
+
* ever minted against.
|
|
109
|
+
*
|
|
110
|
+
* `parseChallengeHeader` guarantees `credits` is a string OR a number and
|
|
111
|
+
* coerces the number (`isValidChallengeRequestShape` / `toChallengeRequest` in
|
|
112
|
+
* `codec.ts`), so what survives to here and still needs refusing is a string
|
|
113
|
+
* that is not a non-negative integer — `'2.5'`, `'-1'`, `'1e3'`, `'abc'`. Each
|
|
114
|
+
* of those would otherwise reach `BigInt()` in the cap comparison as a raw
|
|
115
|
+
* `SyntaxError`, or the mint as a price nothing can account for.
|
|
116
|
+
*
|
|
117
|
+
* `planId` is NOT re-checked here: the codec already rejects a non-string or
|
|
118
|
+
* empty `planId` outright, so a second guard for it was unreachable code that
|
|
119
|
+
* read as coverage.
|
|
120
|
+
*/
|
|
121
|
+
function assertValidChallengeRequest(challenge, input) {
|
|
122
|
+
const credits = challenge.request?.credits;
|
|
123
|
+
if (typeof credits !== 'string' || !/^\d+$/.test(credits)) {
|
|
124
|
+
throw new MppError(`The MPP challenge from ${originOf(input)} names a non-decimal-string credits value ` +
|
|
125
|
+
`(${JSON.stringify(credits)}); refusing to mint.`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Validates `options.maxCredits` at entry and normalizes it to a `bigint`.
|
|
130
|
+
*
|
|
131
|
+
* At entry, not at the comparison: `maxCredits` is a caller argument, and a
|
|
132
|
+
* caller mistake must surface before the first request rather than mid-flight
|
|
133
|
+
* on whatever 402 happens to arrive — and as a {@link PaymentsError}, which is
|
|
134
|
+
* what this module documents for bad arguments, rather than the raw
|
|
135
|
+
* `SyntaxError`/`RangeError` that `BigInt('abc')` or `BigInt(1.5)` throws.
|
|
136
|
+
*/
|
|
137
|
+
function parseMaxCredits(value) {
|
|
138
|
+
if (value === undefined)
|
|
139
|
+
return undefined;
|
|
140
|
+
const refuse = () => PaymentsError.validation(`maxCredits must be a non-negative integer (decimal string, safe integer or bigint), ` +
|
|
141
|
+
`got ${JSON.stringify(typeof value === 'bigint' ? String(value) : value)}`);
|
|
142
|
+
if (typeof value === 'bigint') {
|
|
143
|
+
if (value < 0n)
|
|
144
|
+
throw refuse();
|
|
145
|
+
return value;
|
|
146
|
+
}
|
|
147
|
+
if (typeof value === 'number') {
|
|
148
|
+
if (!Number.isSafeInteger(value) || value < 0)
|
|
149
|
+
throw refuse();
|
|
150
|
+
return BigInt(value);
|
|
151
|
+
}
|
|
152
|
+
if (typeof value === 'string' && /^\d+$/.test(value.trim()))
|
|
153
|
+
return BigInt(value.trim());
|
|
154
|
+
throw refuse();
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Reads at most {@link MAX_ERROR_BODY_BYTES} of a response body as text.
|
|
158
|
+
*
|
|
159
|
+
* Streams the clone and stops at the cap instead of `text()`-ing it, so an
|
|
160
|
+
* endpoint answering a 402 with an endless body cannot be used to exhaust the
|
|
161
|
+
* buyer. A runtime whose `Response` exposes no readable `body` (or a test
|
|
162
|
+
* double) falls back to `text()` with the same cap applied afterwards — the
|
|
163
|
+
* bound is then advisory, which is why the streaming path is preferred.
|
|
164
|
+
*/
|
|
165
|
+
async function readBoundedText(response) {
|
|
166
|
+
const body = response.body;
|
|
167
|
+
if (!body || typeof body.getReader !== 'function') {
|
|
168
|
+
return (await response.text()).slice(0, MAX_ERROR_BODY_BYTES);
|
|
169
|
+
}
|
|
170
|
+
const reader = body.getReader();
|
|
171
|
+
const chunks = [];
|
|
172
|
+
let size = 0;
|
|
173
|
+
try {
|
|
174
|
+
for (;;) {
|
|
175
|
+
const { done, value } = await reader.read();
|
|
176
|
+
if (done)
|
|
177
|
+
break;
|
|
178
|
+
if (!value)
|
|
179
|
+
continue;
|
|
180
|
+
// Truncate the chunk that crosses the cap rather than retaining it whole:
|
|
181
|
+
// the transport chooses the chunk size, so "stop after the chunk that
|
|
182
|
+
// crossed" would let one huge chunk defeat the bound entirely.
|
|
183
|
+
const room = MAX_ERROR_BODY_BYTES - size;
|
|
184
|
+
if (value.byteLength >= room) {
|
|
185
|
+
chunks.push(value.subarray(0, room));
|
|
186
|
+
size = MAX_ERROR_BODY_BYTES;
|
|
187
|
+
break;
|
|
188
|
+
}
|
|
189
|
+
chunks.push(value);
|
|
190
|
+
size += value.byteLength;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
finally {
|
|
194
|
+
// NOT awaited, deliberately: this reader is one branch of the tee that
|
|
195
|
+
// `clone()` creates, and awaiting its cancel deadlocks while the sibling
|
|
196
|
+
// branch (the response the caller still holds) is unread — verified, it
|
|
197
|
+
// hangs rather than resolving. Signalling the cancel is enough.
|
|
198
|
+
reader.cancel().catch(() => undefined);
|
|
199
|
+
}
|
|
200
|
+
const joined = new Uint8Array(size);
|
|
201
|
+
let offset = 0;
|
|
202
|
+
for (const chunk of chunks) {
|
|
203
|
+
joined.set(chunk, offset);
|
|
204
|
+
offset += chunk.byteLength;
|
|
205
|
+
}
|
|
206
|
+
return new TextDecoder().decode(joined);
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Reads the error body of a challenged retry.
|
|
210
|
+
*
|
|
211
|
+
* A retry that comes back 402 is either "your credential was refused"
|
|
212
|
+
* (terminal) or "here is a fresh challenge" (retryable once). An unreadable
|
|
213
|
+
* or non-JSON body — an HTML WAF/CDN page, a truncated response, a
|
|
214
|
+
* disturbed-body clone failure — is neither: it is treated as terminal by
|
|
215
|
+
* the caller, since it is not evidence of anything retryable.
|
|
216
|
+
*
|
|
217
|
+
* A body that is EMPTY or whitespace-only is not in that class. It is an
|
|
218
|
+
* ordinary HTTP shape — the one the opening 402 is free to use — and carries no
|
|
219
|
+
* code, so it is reported as "no code" rather than as unreadable. Reported as
|
|
220
|
+
* unreadable it would suppress the documented fresh-challenge retry against a
|
|
221
|
+
* seller that did nothing wrong, and blame a WAF that is not there. Truncated
|
|
222
|
+
* bodies are non-empty by construction, so this cannot loosen that path.
|
|
223
|
+
*/
|
|
224
|
+
async function readMppErrorCode(response) {
|
|
225
|
+
let raw;
|
|
226
|
+
try {
|
|
227
|
+
raw = await readBoundedText(response.clone());
|
|
228
|
+
}
|
|
229
|
+
catch (err) {
|
|
230
|
+
return {
|
|
231
|
+
message: `MPP 402 body could not be read: ${err instanceof Error ? err.message : String(err)}`,
|
|
232
|
+
bodyUnreadable: true,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
if (raw.trim() === '')
|
|
236
|
+
return { message: 'MPP 402 carried no body' };
|
|
237
|
+
try {
|
|
238
|
+
const body = JSON.parse(raw);
|
|
239
|
+
// A non-compliant seller can send a body shaped `{ error: { reason: '...' } }`
|
|
240
|
+
// — no `message`, and `error` itself an object rather than a string.
|
|
241
|
+
// Coerced to a string here, once, so every caller of this function (in
|
|
242
|
+
// particular the terminal-throw's `message.slice(0, 200)`) can treat
|
|
243
|
+
// `message` as always a string instead of risking a raw TypeError.
|
|
244
|
+
const rawMessage = body?.message ?? body?.error;
|
|
245
|
+
const message = typeof rawMessage === 'string' ? rawMessage : 'MPP request failed';
|
|
246
|
+
// `code` is only a code when it is a non-empty string. `{"code": null}` is
|
|
247
|
+
// a routine way to serialize "no code", and taking it literally would both
|
|
248
|
+
// store a `null` in a field typed `code?: string` and skip the
|
|
249
|
+
// fresh-challenge fallback — which is gated on `code === undefined` — so a
|
|
250
|
+
// legitimate re-challenge from such a seller would read as terminal.
|
|
251
|
+
const code = typeof body?.code === 'string' && body.code !== '' ? body.code : undefined;
|
|
252
|
+
return { code, message };
|
|
253
|
+
}
|
|
254
|
+
catch {
|
|
255
|
+
return {
|
|
256
|
+
message: `MPP 402 was not JSON (likely a proxy or WAF page): ${raw.slice(0, 200)}`,
|
|
257
|
+
bodyUnreadable: true,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Performs the request, paying an MPP challenge if one comes back.
|
|
263
|
+
*
|
|
264
|
+
* At most one re-challenge cycle is followed: a seller that keeps challenging
|
|
265
|
+
* a freshly paid credential is not going to be satisfied by looping, and a
|
|
266
|
+
* loop would burn a credential per turn.
|
|
267
|
+
*
|
|
268
|
+
* The default on a retry-turn 402 is to STOP, not to pay again. **A code, when
|
|
269
|
+
* present, decides alone**: one {@link isRetryableMppCode} accepts
|
|
270
|
+
* (`BCK.MPP.0004` expired, `BCK.MPP.0005` body-digest mismatch) is retried,
|
|
271
|
+
* every other code is terminal — including a non-`BCK.MPP.*` one, e.g. the
|
|
272
|
+
* `network_error`/`http_500`-shaped code `MppAPI.post` can synthesize and this
|
|
273
|
+
* repo's own seller forwards. The challenge id is not consulted on that path,
|
|
274
|
+
* so a retryable code replaying the identical id does re-mint; `maxCredits`,
|
|
275
|
+
* not id-freshness, is what bounds what that can cost.
|
|
276
|
+
*
|
|
277
|
+
* **With no code**, freshness is the whole signal: a challenge whose `id`
|
|
278
|
+
* differs from the one just presented is a real re-challenge and is retried
|
|
279
|
+
* once, while the identical id replayed, an unparseable challenge or an
|
|
280
|
+
* unreadable body are terminal — a credential already proven invalid is never
|
|
281
|
+
* paid for twice.
|
|
282
|
+
*
|
|
283
|
+
* ### What throws and what comes back as a 402
|
|
284
|
+
*
|
|
285
|
+
* Only a rejection the remote NAMED throws. Three dead ends RETURN the 402
|
|
286
|
+
* instead, because the response is evidence the caller may need and throwing
|
|
287
|
+
* would discard it:
|
|
288
|
+
*
|
|
289
|
+
* 1. A 402 with no USABLE `Payment` challenge (either turn): no
|
|
290
|
+
* `WWW-Authenticate`, another scheme, or a `Payment` challenge missing a
|
|
291
|
+
* required param — {@link parseChallengeHeader} yields nothing for all
|
|
292
|
+
* three, so a seller that announced `Payment` and then sent it malformed
|
|
293
|
+
* lands here too, not only one that does not speak MPP.
|
|
294
|
+
* `credentialsPresented: 0`.
|
|
295
|
+
* 2. A retry-turn 402 that IS retryable but carries no challenge to retry
|
|
296
|
+
* against, so the next turn finds nothing to mint for.
|
|
297
|
+
* 3. The one re-challenge cycle spent: two credentials presented and the
|
|
298
|
+
* seller still answering 402.
|
|
299
|
+
*
|
|
300
|
+
* In cases 2 and 3 a credential WAS presented and may have been burned, so the
|
|
301
|
+
* result is `paid: false` with a non-zero `credentialsPresented` and the last
|
|
302
|
+
* 402 as `response`. Checking `response.ok` is therefore not optional — a
|
|
303
|
+
* resolved promise does not mean the request was paid for.
|
|
304
|
+
*
|
|
305
|
+
* Every throw raised after a credential was presented carries the same
|
|
306
|
+
* accounting as {@link MppSpendReport}, readable with `mppSpendOf(error)`,
|
|
307
|
+
* including a transport failure on the credential-bearing retry (wrapped as
|
|
308
|
+
* {@link MppSpendOutcomeUnknownError} so `instanceof MppError` catches it).
|
|
309
|
+
*/
|
|
310
|
+
export async function mppFetch(mintToken, input, init, options) {
|
|
311
|
+
const maxCredits = parseMaxCredits(options.maxCredits);
|
|
312
|
+
const maxChallenges = 2;
|
|
313
|
+
let response = await fetch(input, init);
|
|
314
|
+
let credentialsPresented = 0;
|
|
315
|
+
let creditsPresentedTotal = 0n;
|
|
316
|
+
let lastChallengeId;
|
|
317
|
+
/** The accounting as it stands right now — reported identically on the return and the throw paths. */
|
|
318
|
+
const spend = () => ({
|
|
319
|
+
credentialsPresented,
|
|
320
|
+
...(credentialsPresented > 0 && { creditsPresented: creditsPresentedTotal.toString() }),
|
|
321
|
+
...(lastChallengeId !== undefined && { challengeId: lastChallengeId }),
|
|
322
|
+
});
|
|
323
|
+
for (let attempt = 0; attempt < maxChallenges; attempt++) {
|
|
324
|
+
if (response.status !== 402)
|
|
325
|
+
break;
|
|
326
|
+
const challengeHeader = response.headers.get('www-authenticate');
|
|
327
|
+
if (!challengeHeader)
|
|
328
|
+
break;
|
|
329
|
+
// Everything from here to the end of the turn can throw AFTER a credential
|
|
330
|
+
// has been presented on an earlier turn — and, past the mint, after one has
|
|
331
|
+
// been presented on this one. A single boundary attaches the accounting to
|
|
332
|
+
// whatever comes out, so no exit from this function is silent about money.
|
|
333
|
+
try {
|
|
334
|
+
let challenge;
|
|
335
|
+
try {
|
|
336
|
+
challenge = parseChallengeHeader(challengeHeader);
|
|
337
|
+
}
|
|
338
|
+
catch (err) {
|
|
339
|
+
throw new MppError(`The 402 from ${originOf(input)} carried a malformed MPP challenge ` +
|
|
340
|
+
`(${err instanceof Error ? err.message : String(err)}). No payment was attempted.`);
|
|
341
|
+
}
|
|
342
|
+
if (!challenge)
|
|
343
|
+
break;
|
|
344
|
+
assertValidChallengeRequest(challenge, input);
|
|
345
|
+
const planId = challenge.request.planId;
|
|
346
|
+
if (options.planId && options.planId !== planId) {
|
|
347
|
+
throw PaymentsError.validation(`MPP challenge names plan ${planId}, but plan ${options.planId} was pinned by the caller`);
|
|
348
|
+
}
|
|
349
|
+
// The cap bounds the CALL: what this challenge asks for is added to what
|
|
350
|
+
// earlier turns already committed. Bounding each turn separately would
|
|
351
|
+
// let a seller collect `maxCredits` twice by re-challenging once.
|
|
352
|
+
const credits = BigInt(challenge.request.credits);
|
|
353
|
+
if (maxCredits !== undefined && creditsPresentedTotal + credits > maxCredits) {
|
|
354
|
+
throw PaymentsError.validation(`MPP challenge asks for ${challenge.request.credits} credits, which would take this call to ` +
|
|
355
|
+
`${creditsPresentedTotal + credits}, above the caller's cap of ${maxCredits}` +
|
|
356
|
+
(credentialsPresented > 0
|
|
357
|
+
? ` (${creditsPresentedTotal} already presented on ${credentialsPresented} credential(s))`
|
|
358
|
+
: ''));
|
|
359
|
+
}
|
|
360
|
+
// A retry resends init.body verbatim. A ReadableStream is single-read —
|
|
361
|
+
// the first fetch() above already consumed it once — so replaying it now
|
|
362
|
+
// would throw an opaque runtime TypeError. Checked here, at the point a
|
|
363
|
+
// retry is actually about to happen, not before the (harmless) first
|
|
364
|
+
// attempt: whether this endpoint ever challenges is not known ahead of
|
|
365
|
+
// time, and a stream body against a non-challenging endpoint is fine.
|
|
366
|
+
if (isNonReplayableBody(init?.body)) {
|
|
367
|
+
throw PaymentsError.validation('payments.mpp.fetch cannot retry a ReadableStream request body: streams are single-read, ' +
|
|
368
|
+
'so the 402 challenge from this endpoint cannot be replayed. Pass a replayable body ' +
|
|
369
|
+
'instead — a string, Buffer/ArrayBuffer/typed array, URLSearchParams, FormData or Blob.');
|
|
370
|
+
}
|
|
371
|
+
const { accessToken } = await mintToken(planId, options.agentId ?? challenge.request.agentId, { delegationConfig: options.delegationConfig });
|
|
372
|
+
const headers = new Headers(init?.headers ?? {});
|
|
373
|
+
const existingAuth = headers.get('authorization');
|
|
374
|
+
const credential = buildCredentialHeader(challenge, { accessToken });
|
|
375
|
+
// Append, not replace: a caller authenticating to the resource server
|
|
376
|
+
// with its own Authorization (the normal shape for a metered API) must
|
|
377
|
+
// not have that credential stripped on the request that costs money.
|
|
378
|
+
// Our own seller's extractPaymentScheme was hardened for exactly this
|
|
379
|
+
// multi-scheme shape.
|
|
380
|
+
headers.set('authorization', existingAuth ? `${credential}, ${existingAuth}` : credential);
|
|
381
|
+
// Counted BEFORE the request, not after: the credential is on the wire as
|
|
382
|
+
// soon as fetch() is called, so a rejection here (disconnect, DNS, the
|
|
383
|
+
// caller's AbortSignal) must not report zero credentials presented. The
|
|
384
|
+
// seller may already have verified and burned it.
|
|
385
|
+
credentialsPresented += 1;
|
|
386
|
+
creditsPresentedTotal += credits;
|
|
387
|
+
lastChallengeId = challenge.id;
|
|
388
|
+
response = await fetch(input, { ...init, headers });
|
|
389
|
+
if (response.status === 402) {
|
|
390
|
+
const { code, message, bodyUnreadable } = await readMppErrorCode(response);
|
|
391
|
+
let isFreshChallenge = false;
|
|
392
|
+
if (code === undefined && !bodyUnreadable) {
|
|
393
|
+
const nextChallengeHeader = response.headers.get('www-authenticate');
|
|
394
|
+
const nextChallenge = nextChallengeHeader ? tryParseChallenge(nextChallengeHeader) : null;
|
|
395
|
+
isFreshChallenge = !!nextChallenge && nextChallenge.id !== challenge.id;
|
|
396
|
+
}
|
|
397
|
+
if (!isRetryableMppCode(code) && !isFreshChallenge) {
|
|
398
|
+
throw toMppError(code, `${originOf(input)} rejected the credential: ${message.slice(0, 200)}`);
|
|
399
|
+
}
|
|
400
|
+
// Otherwise the seller genuinely re-challenged (expired, a digest
|
|
401
|
+
// mismatch, or — for a seller that sends no code, including THIS repo's
|
|
402
|
+
// middleware when verification fails for infrastructure reasons — a
|
|
403
|
+
// fresh id); the loop takes one more turn and mints a NEW credential
|
|
404
|
+
// against the fresh challenge — the old one is not re-presented.
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
catch (err) {
|
|
409
|
+
throw asSpendAwareError(err, spend(), input);
|
|
410
|
+
}
|
|
411
|
+
const receiptHeader = response.headers.get('payment-receipt');
|
|
412
|
+
let receipt;
|
|
413
|
+
if (receiptHeader) {
|
|
414
|
+
try {
|
|
415
|
+
receipt = parseReceiptHeader(receiptHeader);
|
|
416
|
+
}
|
|
417
|
+
catch (err) {
|
|
418
|
+
// The receipt is decorative ("unsigned by design, and carries no
|
|
419
|
+
// balance" — see MppReceipt) and receipt? is optional precisely so
|
|
420
|
+
// it can be absent. A failed decode must not destroy the response
|
|
421
|
+
// the caller already paid for.
|
|
422
|
+
console.warn('[payments.mpp.fetch] payment may have succeeded but the Payment-Receipt could not be ' +
|
|
423
|
+
`decoded: ${err instanceof Error ? err.message : String(err)}`);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
const settled = receipt !== undefined && !statesFailure(receipt);
|
|
427
|
+
return {
|
|
428
|
+
response,
|
|
429
|
+
settled,
|
|
430
|
+
paid: response.ok && settled,
|
|
431
|
+
credentialsPresented,
|
|
432
|
+
...(credentialsPresented > 0 && { creditsPresented: creditsPresentedTotal.toString() }),
|
|
433
|
+
...(receipt && { receipt }),
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
// Every dead end documented on this function lands here: no challenge to pay,
|
|
437
|
+
// a retryable 402 with no challenge to retry against, or the re-challenge
|
|
438
|
+
// budget spent. `spend()` is what tells those apart — the last two carry a
|
|
439
|
+
// non-zero `credentialsPresented`.
|
|
440
|
+
return {
|
|
441
|
+
response,
|
|
442
|
+
settled: false,
|
|
443
|
+
paid: false,
|
|
444
|
+
credentialsPresented,
|
|
445
|
+
...(credentialsPresented > 0 && { creditsPresented: creditsPresentedTotal.toString() }),
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
/**
|
|
449
|
+
* Attaches spend accounting to whatever is escaping, and wraps a raw
|
|
450
|
+
* transport failure that happened with a credential already on the wire.
|
|
451
|
+
*
|
|
452
|
+
* **Nothing is attached when nothing was presented.** `{ credentialsPresented: 0 }`
|
|
453
|
+
* is a truthy object, so annotating a first-turn argument failure would send a
|
|
454
|
+
* caller following the documented `if (mppSpendOf(err))` pattern into the
|
|
455
|
+
* "credits may already be burned, do not retry" branch on a plain validation
|
|
456
|
+
* error — making the field useless for the one decision it exists to inform.
|
|
457
|
+
*
|
|
458
|
+
* With that, three cases:
|
|
459
|
+
* - An {@link MppError} (or the {@link PaymentsError} a caller-constraint guard
|
|
460
|
+
* throws on the re-challenge turn) is annotated and rethrown AS-IS: the type
|
|
461
|
+
* a caller branches on must not change just because money is now reported.
|
|
462
|
+
* - Anything else with a credential already presented becomes an
|
|
463
|
+
* {@link MppSpendOutcomeUnknownError}, so it reaches the `instanceof MppError`
|
|
464
|
+
* handler this module documents instead of escaping as a raw `TypeError`.
|
|
465
|
+
* - Anything else with nothing presented is rethrown untouched — nothing was
|
|
466
|
+
* spent, and dressing up a plain network fault would only obscure it.
|
|
467
|
+
*/
|
|
468
|
+
function asSpendAwareError(err, spend, input) {
|
|
469
|
+
if (spend.credentialsPresented === 0)
|
|
470
|
+
return err;
|
|
471
|
+
if (err instanceof MppError || err instanceof PaymentsError) {
|
|
472
|
+
try {
|
|
473
|
+
const annotated = err;
|
|
474
|
+
annotated.spend = spend;
|
|
475
|
+
return err;
|
|
476
|
+
}
|
|
477
|
+
catch {
|
|
478
|
+
// A frozen or otherwise non-extensible error cannot carry the report, and
|
|
479
|
+
// silently returning it would recreate exactly the invisible-spend failure
|
|
480
|
+
// this boundary exists to close. Fall through to the wrapper below, which
|
|
481
|
+
// holds the report in its own field.
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
485
|
+
return new MppSpendOutcomeUnknownError(err instanceof MppError || err instanceof PaymentsError
|
|
486
|
+
? `The MPP credential was sent to ${originOf(input)} and the call then failed with an error that ` +
|
|
487
|
+
`could not carry its own spend report (${detail}), so ${spend.creditsPresented} credits may or ` +
|
|
488
|
+
'may not have been burned. Do not blindly retry.'
|
|
489
|
+
: `The MPP credential was sent to ${originOf(input)} but the request failed before any response ` +
|
|
490
|
+
`was read (${detail}), so ${spend.creditsPresented} credits may or may not have been burned. ` +
|
|
491
|
+
'Do not blindly retry.', err, spend);
|
|
492
|
+
}
|
|
493
|
+
//# sourceMappingURL=fetch.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fetch.js","sourceRoot":"","sources":["../../src/mpp/fetch.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The buyer half of MPP: pay a challenged endpoint with an existing Nevermined\n * delegation.\n *\n * The buyer learns nothing about MPP beyond calling this instead of `fetch`.\n * The plan comes out of the challenge, the credential is built from the\n * challenge plus an MPP-domain access token, and the request is retried once.\n *\n * Two error families are used deliberately:\n * - {@link PaymentsError} (`code: 'validation'`) for guards this call refuses\n * to even attempt — a bad argument, a challenge that violates a\n * caller-supplied constraint (`planId`, `maxCredits`), or a body that\n * cannot be replayed. None of these mean a payment failed; nothing was ever\n * attempted.\n * - {@link MppError} (and its typed subclasses) for what the wire actually\n * said: a rejected credential, a malformed challenge, an MPP-disabled\n * environment. A caller branching on `instanceof MppError` to mean\n * \"the payment failed\" gets exactly that, and no more.\n */\n\nimport { PaymentsError } from '../common/payments.error.js'\nimport type { DelegationConfig, X402TokenOptions } from '../common/types.js'\nimport { buildCredentialHeader, parseChallengeHeader, parseReceiptHeader } from './codec.js'\nimport { MppError, MppSpendOutcomeUnknownError, isRetryableMppCode, toMppError } from './errors.js'\nimport type { MppSpendReport } from './errors.js'\nimport type { MppChallenge, MppReceipt } from './types.js'\n\n/**\n * Options for {@link mppFetch} / `MppAPI.fetch`.\n *\n * @experimental The MPP buyer surface may change in a minor release.\n *\n * The request's `init.body`, if any, must be replayable **if the endpoint may\n * challenge the request**: a `ReadableStream` body throws a typed\n * {@link PaymentsError} once a 402 challenge actually requires a retry, since\n * the stream cannot be resent. A request that is never challenged sends a\n * stream body exactly once, exactly like plain `fetch`.\n *\n * This helper mints `nvm:erc4337` access tokens only in this release — a\n * buyer holding an `nvm:card-delegation` delegation cannot use it yet.\n */\nexport interface MppFetchOptions {\n /** The delegation that backs the payment — the same one x402 uses. */\n delegationConfig: DelegationConfig\n /**\n * Overrides the agent id the seller's challenge names — the minted token is\n * addressed to this agent id instead of whatever the challenge carries.\n * Unlike `planId`, this is not a guard: a mismatch is not checked or\n * refused, it simply replaces what the seller asked for. Leave unset to\n * honor the challenge as issued.\n */\n agentId?: string\n /** Fail before minting if the challenge names a different plan than this. */\n planId?: string\n /**\n * Budget for the WHOLE call, not for one challenge: the helper refuses to\n * mint whenever the credits named so far plus the credits this challenge\n * asks for would exceed it. A seller unilaterally names the price, and a\n * re-challenge names it again — a per-turn cap would therefore bound each\n * turn at `maxCredits` and the call at twice it, which is not what \"cap\"\n * reads as. `creditsPresented` on the result is the same running total, so\n * the two always speak about the same number.\n *\n * Must be a non-negative integer (a decimal string, a safe integer, or a\n * `bigint`); anything else is refused with a {@link PaymentsError} at entry,\n * before the first request, rather than mid-flight on the first 402.\n */\n maxCredits?: string | number | bigint\n}\n\n/**\n * What {@link mppFetch} / `MppAPI.fetch` resolves to.\n *\n * @experimental Fields may be added or change meaning in a minor release —\n * `settled` and `creditsPresented` both did during review.\n */\nexport interface MppFetchResult {\n /** The final response — the paid one when a payment happened. */\n response: Response\n /**\n * The decoded `Payment-Receipt`, when the server returned one and it\n * decoded cleanly. A malformed receipt never throws — it leaves this\n * absent (with a console warning) rather than destroying a response the\n * caller already paid for.\n */\n receipt?: MppReceipt\n /**\n * Whether the endpoint returned settlement evidence: a `Payment-Receipt` that\n * decoded cleanly and does not state failure outright. Never derived from the\n * HTTP status.\n *\n * `receipt.status` is read asymmetrically, on purpose. Success is NOT\n * recognized — `'success'` is the only value with agreement behind it, so\n * treating an unrecognized `'ok'`/`'completed'` as failure would report an\n * unpaid call that was in fact paid. An explicit negative (`'failed'`,\n * `'failure'`, `'declined'`, `'error'`) is a different thing from an unknown\n * vocabulary, and is excluded: `paid: true` on a receipt that says the\n * settlement failed is wrong in the one direction this field must never be\n * wrong. Anything else stays `settled: true` with `receipt` on the result, so\n * a caller paying third parties can judge the value itself.\n *\n * This SDK's own seller attaches a receipt only when settlement succeeded, so\n * the distinction is third-party exposure rather than in-ecosystem.\n */\n settled: boolean\n /**\n * How many credentials were minted and presented to the endpoint during\n * this call (0, 1 or 2). This is NOT the same as `settled`:\n * `credentialsPresented > 0` with `settled: false` means the seller may\n * already have burned credits for a credential whose fate is unknown to\n * the caller — treat that as \"do not blindly retry\", not as \"nothing\n * happened\".\n */\n credentialsPresented: number\n /**\n * TOTAL credits named by every challenge a credential was minted against\n * during this call, as a decimal string — summed, not the last turn's\n * amount, since a re-challenge is free to name a different price and the\n * caller is accounting for the call. Present whenever\n * `credentialsPresented > 0`.\n *\n * It is an **upper bound** on what may have burned, never a lower one. A\n * seller that answers a retryable code while replaying the identical\n * challenge id gets a second credential minted against that same challenge\n * (a code decides alone — see {@link mppFetch}), and against a seller that\n * keys single-use on the challenge id, as this SDK's middleware does, that\n * second credential is refused as a replay and burns nothing. The count is\n * deliberately not lowered for it: this field answers \"what could have left\",\n * and guessing which of a remote's credentials it honoured would answer a\n * question the buyer cannot see.\n */\n creditsPresented?: string\n /**\n * Whether a credential was presented AND the final response looks\n * successful: `response.ok && settled`. A 2xx with no receipt (a\n * settlement that silently failed) and a non-2xx with a receipt (settle-\n * then-error) are both `paid: false` — check `settled` and\n * `credentialsPresented` for the honest picture in either case.\n *\n * `ok: true, paid: false, credentialsPresented: 1` is a ROUTINE outcome, not\n * an exotic one: a seller whose handler streams or flushes has already sent\n * its headers when settlement runs, so `Payment-Receipt` cannot be attached\n * to a response that is already on the wire (this SDK's own middleware says\n * so where it settles detached). The credits were burned. Never read this\n * combination as \"the payment did not happen\" and retry.\n */\n paid: boolean\n}\n\n/** Mints an MPP access token for a plan. Supplied by `MppAPI`. */\nexport type MppTokenMinter = (\n planId: string,\n agentId?: string,\n tokenOptions?: X402TokenOptions,\n) => Promise<{ accessToken: string }>\n\n/**\n * Whether `body` is a `ReadableStream`.\n *\n * A stream is single-read: once a `fetch()` starts consuming it, the stream\n * is locked/disturbed, and a *second* `fetch()` with the same `init.body`\n * throws an opaque runtime `TypeError` rather than a typed error. Every other\n * `BodyInit` — `string`, `Buffer`/`ArrayBuffer`/typed arrays,\n * `URLSearchParams`, `FormData`, `Blob`, or no body at all — can be read more\n * than once, so a retry is safe.\n */\nfunction isNonReplayableBody(body: BodyInit | null | undefined): boolean {\n return typeof ReadableStream !== 'undefined' && body instanceof ReadableStream\n}\n\n/**\n * Cap on how much of a 402's error body is buffered before parsing it.\n *\n * The body is seller-controlled and read purely to look for a `code`, so a\n * hostile or broken endpoint answering a 402 with an unbounded stream must not\n * be able to exhaust the buyer's memory.\n *\n * A truncated read of a body large enough to matter will usually not parse; if\n * it does, the prefix necessarily contains the complete `code` object — a\n * truncated prefix of one JSON object can only parse once its closing brace is\n * inside the cap — so the classification is unchanged either way. The branch\n * keys on `JSON.parse` succeeding, never on \"truncated implies unparseable\",\n * which is false: `<complete JSON><whitespace><garbage>` parses after\n * truncation and throws before it.\n */\nconst MAX_ERROR_BODY_BYTES = 64 * 1024\n\n/**\n * Receipt `status` values that state failure outright, lower-cased.\n *\n * `MppReceipt.status` is a seller-set string and this SDK deliberately does not\n * try to recognize *success*: `'success'` is the only value with any agreement\n * behind it, so treating an unrecognized `'ok'`/`'completed'` as failure would\n * report an unpaid call that was in fact paid. The asymmetry is the point — an\n * explicit negative is not an unknown vocabulary, and reporting `paid: true`\n * for a receipt that says the settlement failed is wrong in the one direction\n * this field must never be wrong.\n *\n * Only unambiguous negatives belong here; anything genuinely ambiguous stays\n * unrecognized and is reported as settled, with `receipt` on the result for a\n * caller that wants to judge for itself.\n */\nconst EXPLICIT_FAILURE_RECEIPT_STATUSES: ReadonlySet<string> = new Set([\n 'failed',\n 'failure',\n 'declined',\n 'error',\n])\n\n/** Whether a decoded receipt states outright that the settlement did not happen. */\nfunction statesFailure(receipt: MppReceipt | undefined): boolean {\n return (\n receipt !== undefined &&\n typeof receipt.status === 'string' &&\n EXPLICIT_FAILURE_RECEIPT_STATUSES.has(receipt.status.trim().toLowerCase())\n )\n}\n\n/** The origin of `input`, or the raw value when it does not parse as a URL — used only to label a remote error. */\nfunction originOf(input: string | URL): string {\n try {\n return new URL(String(input)).origin\n } catch {\n return String(input)\n }\n}\n\n/**\n * Parses a challenge header, swallowing any decode failure into `null`.\n *\n * Used only to peek at a re-challenge's freshness inside the retry gate — a\n * garbled re-challenge is simply \"not a fresh challenge\", which the gate\n * already treats as terminal. The FIRST challenge of a 402 is never parsed\n * this leniently: see the `try`/`catch` around {@link parseChallengeHeader}\n * in {@link mppFetch}, which throws a typed error instead, since minting\n * against a challenge this function could not even parse would be worse.\n */\nfunction tryParseChallenge(headerValue: string): MppChallenge | null {\n try {\n return parseChallengeHeader(headerValue)\n } catch {\n return null\n }\n}\n\n/**\n * Refuses a challenge whose `credits` is not a decimal string before it is\n * ever minted against.\n *\n * `parseChallengeHeader` guarantees `credits` is a string OR a number and\n * coerces the number (`isValidChallengeRequestShape` / `toChallengeRequest` in\n * `codec.ts`), so what survives to here and still needs refusing is a string\n * that is not a non-negative integer — `'2.5'`, `'-1'`, `'1e3'`, `'abc'`. Each\n * of those would otherwise reach `BigInt()` in the cap comparison as a raw\n * `SyntaxError`, or the mint as a price nothing can account for.\n *\n * `planId` is NOT re-checked here: the codec already rejects a non-string or\n * empty `planId` outright, so a second guard for it was unreachable code that\n * read as coverage.\n */\nfunction assertValidChallengeRequest(challenge: MppChallenge, input: string | URL): void {\n const credits: unknown = challenge.request?.credits\n if (typeof credits !== 'string' || !/^\\d+$/.test(credits)) {\n throw new MppError(\n `The MPP challenge from ${originOf(input)} names a non-decimal-string credits value ` +\n `(${JSON.stringify(credits)}); refusing to mint.`,\n )\n }\n}\n\n/**\n * Validates `options.maxCredits` at entry and normalizes it to a `bigint`.\n *\n * At entry, not at the comparison: `maxCredits` is a caller argument, and a\n * caller mistake must surface before the first request rather than mid-flight\n * on whatever 402 happens to arrive — and as a {@link PaymentsError}, which is\n * what this module documents for bad arguments, rather than the raw\n * `SyntaxError`/`RangeError` that `BigInt('abc')` or `BigInt(1.5)` throws.\n */\nfunction parseMaxCredits(value: string | number | bigint | undefined): bigint | undefined {\n if (value === undefined) return undefined\n const refuse = () =>\n PaymentsError.validation(\n `maxCredits must be a non-negative integer (decimal string, safe integer or bigint), ` +\n `got ${JSON.stringify(typeof value === 'bigint' ? String(value) : value)}`,\n )\n if (typeof value === 'bigint') {\n if (value < 0n) throw refuse()\n return value\n }\n if (typeof value === 'number') {\n if (!Number.isSafeInteger(value) || value < 0) throw refuse()\n return BigInt(value)\n }\n if (typeof value === 'string' && /^\\d+$/.test(value.trim())) return BigInt(value.trim())\n throw refuse()\n}\n\n/**\n * Reads at most {@link MAX_ERROR_BODY_BYTES} of a response body as text.\n *\n * Streams the clone and stops at the cap instead of `text()`-ing it, so an\n * endpoint answering a 402 with an endless body cannot be used to exhaust the\n * buyer. A runtime whose `Response` exposes no readable `body` (or a test\n * double) falls back to `text()` with the same cap applied afterwards — the\n * bound is then advisory, which is why the streaming path is preferred.\n */\nasync function readBoundedText(response: Response): Promise<string> {\n const body = response.body\n if (!body || typeof body.getReader !== 'function') {\n return (await response.text()).slice(0, MAX_ERROR_BODY_BYTES)\n }\n const reader = body.getReader()\n const chunks: Uint8Array[] = []\n let size = 0\n try {\n for (;;) {\n const { done, value } = await reader.read()\n if (done) break\n if (!value) continue\n // Truncate the chunk that crosses the cap rather than retaining it whole:\n // the transport chooses the chunk size, so \"stop after the chunk that\n // crossed\" would let one huge chunk defeat the bound entirely.\n const room = MAX_ERROR_BODY_BYTES - size\n if (value.byteLength >= room) {\n chunks.push(value.subarray(0, room))\n size = MAX_ERROR_BODY_BYTES\n break\n }\n chunks.push(value)\n size += value.byteLength\n }\n } finally {\n // NOT awaited, deliberately: this reader is one branch of the tee that\n // `clone()` creates, and awaiting its cancel deadlocks while the sibling\n // branch (the response the caller still holds) is unread — verified, it\n // hangs rather than resolving. Signalling the cancel is enough.\n reader.cancel().catch(() => undefined)\n }\n const joined = new Uint8Array(size)\n let offset = 0\n for (const chunk of chunks) {\n joined.set(chunk, offset)\n offset += chunk.byteLength\n }\n return new TextDecoder().decode(joined)\n}\n\n/**\n * Reads the error body of a challenged retry.\n *\n * A retry that comes back 402 is either \"your credential was refused\"\n * (terminal) or \"here is a fresh challenge\" (retryable once). An unreadable\n * or non-JSON body — an HTML WAF/CDN page, a truncated response, a\n * disturbed-body clone failure — is neither: it is treated as terminal by\n * the caller, since it is not evidence of anything retryable.\n *\n * A body that is EMPTY or whitespace-only is not in that class. It is an\n * ordinary HTTP shape — the one the opening 402 is free to use — and carries no\n * code, so it is reported as \"no code\" rather than as unreadable. Reported as\n * unreadable it would suppress the documented fresh-challenge retry against a\n * seller that did nothing wrong, and blame a WAF that is not there. Truncated\n * bodies are non-empty by construction, so this cannot loosen that path.\n */\nasync function readMppErrorCode(\n response: Response,\n): Promise<{ code?: string; message: string; bodyUnreadable?: boolean }> {\n let raw: string\n try {\n raw = await readBoundedText(response.clone())\n } catch (err) {\n return {\n message: `MPP 402 body could not be read: ${err instanceof Error ? err.message : String(err)}`,\n bodyUnreadable: true,\n }\n }\n if (raw.trim() === '') return { message: 'MPP 402 carried no body' }\n try {\n const body = JSON.parse(raw)\n // A non-compliant seller can send a body shaped `{ error: { reason: '...' } }`\n // — no `message`, and `error` itself an object rather than a string.\n // Coerced to a string here, once, so every caller of this function (in\n // particular the terminal-throw's `message.slice(0, 200)`) can treat\n // `message` as always a string instead of risking a raw TypeError.\n const rawMessage = body?.message ?? body?.error\n const message = typeof rawMessage === 'string' ? rawMessage : 'MPP request failed'\n // `code` is only a code when it is a non-empty string. `{\"code\": null}` is\n // a routine way to serialize \"no code\", and taking it literally would both\n // store a `null` in a field typed `code?: string` and skip the\n // fresh-challenge fallback — which is gated on `code === undefined` — so a\n // legitimate re-challenge from such a seller would read as terminal.\n const code = typeof body?.code === 'string' && body.code !== '' ? body.code : undefined\n return { code, message }\n } catch {\n return {\n message: `MPP 402 was not JSON (likely a proxy or WAF page): ${raw.slice(0, 200)}`,\n bodyUnreadable: true,\n }\n }\n}\n\n/**\n * Performs the request, paying an MPP challenge if one comes back.\n *\n * At most one re-challenge cycle is followed: a seller that keeps challenging\n * a freshly paid credential is not going to be satisfied by looping, and a\n * loop would burn a credential per turn.\n *\n * The default on a retry-turn 402 is to STOP, not to pay again. **A code, when\n * present, decides alone**: one {@link isRetryableMppCode} accepts\n * (`BCK.MPP.0004` expired, `BCK.MPP.0005` body-digest mismatch) is retried,\n * every other code is terminal — including a non-`BCK.MPP.*` one, e.g. the\n * `network_error`/`http_500`-shaped code `MppAPI.post` can synthesize and this\n * repo's own seller forwards. The challenge id is not consulted on that path,\n * so a retryable code replaying the identical id does re-mint; `maxCredits`,\n * not id-freshness, is what bounds what that can cost.\n *\n * **With no code**, freshness is the whole signal: a challenge whose `id`\n * differs from the one just presented is a real re-challenge and is retried\n * once, while the identical id replayed, an unparseable challenge or an\n * unreadable body are terminal — a credential already proven invalid is never\n * paid for twice.\n *\n * ### What throws and what comes back as a 402\n *\n * Only a rejection the remote NAMED throws. Three dead ends RETURN the 402\n * instead, because the response is evidence the caller may need and throwing\n * would discard it:\n *\n * 1. A 402 with no USABLE `Payment` challenge (either turn): no\n * `WWW-Authenticate`, another scheme, or a `Payment` challenge missing a\n * required param — {@link parseChallengeHeader} yields nothing for all\n * three, so a seller that announced `Payment` and then sent it malformed\n * lands here too, not only one that does not speak MPP.\n * `credentialsPresented: 0`.\n * 2. A retry-turn 402 that IS retryable but carries no challenge to retry\n * against, so the next turn finds nothing to mint for.\n * 3. The one re-challenge cycle spent: two credentials presented and the\n * seller still answering 402.\n *\n * In cases 2 and 3 a credential WAS presented and may have been burned, so the\n * result is `paid: false` with a non-zero `credentialsPresented` and the last\n * 402 as `response`. Checking `response.ok` is therefore not optional — a\n * resolved promise does not mean the request was paid for.\n *\n * Every throw raised after a credential was presented carries the same\n * accounting as {@link MppSpendReport}, readable with `mppSpendOf(error)`,\n * including a transport failure on the credential-bearing retry (wrapped as\n * {@link MppSpendOutcomeUnknownError} so `instanceof MppError` catches it).\n */\nexport async function mppFetch(\n mintToken: MppTokenMinter,\n input: string | URL,\n init: RequestInit | undefined,\n options: MppFetchOptions,\n): Promise<MppFetchResult> {\n const maxCredits = parseMaxCredits(options.maxCredits)\n const maxChallenges = 2\n let response = await fetch(input, init)\n let credentialsPresented = 0\n let creditsPresentedTotal = 0n\n let lastChallengeId: string | undefined\n\n /** The accounting as it stands right now — reported identically on the return and the throw paths. */\n const spend = (): MppSpendReport => ({\n credentialsPresented,\n ...(credentialsPresented > 0 && { creditsPresented: creditsPresentedTotal.toString() }),\n ...(lastChallengeId !== undefined && { challengeId: lastChallengeId }),\n })\n\n for (let attempt = 0; attempt < maxChallenges; attempt++) {\n if (response.status !== 402) break\n\n const challengeHeader = response.headers.get('www-authenticate')\n if (!challengeHeader) break\n\n // Everything from here to the end of the turn can throw AFTER a credential\n // has been presented on an earlier turn — and, past the mint, after one has\n // been presented on this one. A single boundary attaches the accounting to\n // whatever comes out, so no exit from this function is silent about money.\n try {\n let challenge: MppChallenge | null\n try {\n challenge = parseChallengeHeader(challengeHeader)\n } catch (err) {\n throw new MppError(\n `The 402 from ${originOf(input)} carried a malformed MPP challenge ` +\n `(${err instanceof Error ? err.message : String(err)}). No payment was attempted.`,\n )\n }\n if (!challenge) break\n assertValidChallengeRequest(challenge, input)\n\n const planId = challenge.request.planId\n if (options.planId && options.planId !== planId) {\n throw PaymentsError.validation(\n `MPP challenge names plan ${planId}, but plan ${options.planId} was pinned by the caller`,\n )\n }\n\n // The cap bounds the CALL: what this challenge asks for is added to what\n // earlier turns already committed. Bounding each turn separately would\n // let a seller collect `maxCredits` twice by re-challenging once.\n const credits = BigInt(challenge.request.credits)\n if (maxCredits !== undefined && creditsPresentedTotal + credits > maxCredits) {\n throw PaymentsError.validation(\n `MPP challenge asks for ${challenge.request.credits} credits, which would take this call to ` +\n `${creditsPresentedTotal + credits}, above the caller's cap of ${maxCredits}` +\n (credentialsPresented > 0\n ? ` (${creditsPresentedTotal} already presented on ${credentialsPresented} credential(s))`\n : ''),\n )\n }\n\n // A retry resends init.body verbatim. A ReadableStream is single-read —\n // the first fetch() above already consumed it once — so replaying it now\n // would throw an opaque runtime TypeError. Checked here, at the point a\n // retry is actually about to happen, not before the (harmless) first\n // attempt: whether this endpoint ever challenges is not known ahead of\n // time, and a stream body against a non-challenging endpoint is fine.\n if (isNonReplayableBody(init?.body)) {\n throw PaymentsError.validation(\n 'payments.mpp.fetch cannot retry a ReadableStream request body: streams are single-read, ' +\n 'so the 402 challenge from this endpoint cannot be replayed. Pass a replayable body ' +\n 'instead — a string, Buffer/ArrayBuffer/typed array, URLSearchParams, FormData or Blob.',\n )\n }\n\n const { accessToken } = await mintToken(\n planId,\n options.agentId ?? challenge.request.agentId,\n { delegationConfig: options.delegationConfig },\n )\n\n const headers = new Headers(init?.headers ?? {})\n const existingAuth = headers.get('authorization')\n const credential = buildCredentialHeader(challenge, { accessToken })\n // Append, not replace: a caller authenticating to the resource server\n // with its own Authorization (the normal shape for a metered API) must\n // not have that credential stripped on the request that costs money.\n // Our own seller's extractPaymentScheme was hardened for exactly this\n // multi-scheme shape.\n headers.set('authorization', existingAuth ? `${credential}, ${existingAuth}` : credential)\n\n // Counted BEFORE the request, not after: the credential is on the wire as\n // soon as fetch() is called, so a rejection here (disconnect, DNS, the\n // caller's AbortSignal) must not report zero credentials presented. The\n // seller may already have verified and burned it.\n credentialsPresented += 1\n creditsPresentedTotal += credits\n lastChallengeId = challenge.id\n response = await fetch(input, { ...init, headers })\n\n if (response.status === 402) {\n const { code, message, bodyUnreadable } = await readMppErrorCode(response)\n\n let isFreshChallenge = false\n if (code === undefined && !bodyUnreadable) {\n const nextChallengeHeader = response.headers.get('www-authenticate')\n const nextChallenge = nextChallengeHeader ? tryParseChallenge(nextChallengeHeader) : null\n isFreshChallenge = !!nextChallenge && nextChallenge.id !== challenge.id\n }\n\n if (!isRetryableMppCode(code) && !isFreshChallenge) {\n throw toMppError(\n code,\n `${originOf(input)} rejected the credential: ${message.slice(0, 200)}`,\n )\n }\n // Otherwise the seller genuinely re-challenged (expired, a digest\n // mismatch, or — for a seller that sends no code, including THIS repo's\n // middleware when verification fails for infrastructure reasons — a\n // fresh id); the loop takes one more turn and mints a NEW credential\n // against the fresh challenge — the old one is not re-presented.\n continue\n }\n } catch (err) {\n throw asSpendAwareError(err, spend(), input)\n }\n\n const receiptHeader = response.headers.get('payment-receipt')\n let receipt: MppReceipt | undefined\n if (receiptHeader) {\n try {\n receipt = parseReceiptHeader(receiptHeader)\n } catch (err) {\n // The receipt is decorative (\"unsigned by design, and carries no\n // balance\" — see MppReceipt) and receipt? is optional precisely so\n // it can be absent. A failed decode must not destroy the response\n // the caller already paid for.\n console.warn(\n '[payments.mpp.fetch] payment may have succeeded but the Payment-Receipt could not be ' +\n `decoded: ${err instanceof Error ? err.message : String(err)}`,\n )\n }\n }\n\n const settled = receipt !== undefined && !statesFailure(receipt)\n return {\n response,\n settled,\n paid: response.ok && settled,\n credentialsPresented,\n ...(credentialsPresented > 0 && { creditsPresented: creditsPresentedTotal.toString() }),\n ...(receipt && { receipt }),\n }\n }\n\n // Every dead end documented on this function lands here: no challenge to pay,\n // a retryable 402 with no challenge to retry against, or the re-challenge\n // budget spent. `spend()` is what tells those apart — the last two carry a\n // non-zero `credentialsPresented`.\n return {\n response,\n settled: false,\n paid: false,\n credentialsPresented,\n ...(credentialsPresented > 0 && { creditsPresented: creditsPresentedTotal.toString() }),\n }\n}\n\n/**\n * Attaches spend accounting to whatever is escaping, and wraps a raw\n * transport failure that happened with a credential already on the wire.\n *\n * **Nothing is attached when nothing was presented.** `{ credentialsPresented: 0 }`\n * is a truthy object, so annotating a first-turn argument failure would send a\n * caller following the documented `if (mppSpendOf(err))` pattern into the\n * \"credits may already be burned, do not retry\" branch on a plain validation\n * error — making the field useless for the one decision it exists to inform.\n *\n * With that, three cases:\n * - An {@link MppError} (or the {@link PaymentsError} a caller-constraint guard\n * throws on the re-challenge turn) is annotated and rethrown AS-IS: the type\n * a caller branches on must not change just because money is now reported.\n * - Anything else with a credential already presented becomes an\n * {@link MppSpendOutcomeUnknownError}, so it reaches the `instanceof MppError`\n * handler this module documents instead of escaping as a raw `TypeError`.\n * - Anything else with nothing presented is rethrown untouched — nothing was\n * spent, and dressing up a plain network fault would only obscure it.\n */\nfunction asSpendAwareError(err: unknown, spend: MppSpendReport, input: string | URL): unknown {\n if (spend.credentialsPresented === 0) return err\n if (err instanceof MppError || err instanceof PaymentsError) {\n try {\n const annotated = err as { spend?: MppSpendReport }\n annotated.spend = spend\n return err\n } catch {\n // A frozen or otherwise non-extensible error cannot carry the report, and\n // silently returning it would recreate exactly the invisible-spend failure\n // this boundary exists to close. Fall through to the wrapper below, which\n // holds the report in its own field.\n }\n }\n const detail = err instanceof Error ? err.message : String(err)\n return new MppSpendOutcomeUnknownError(\n err instanceof MppError || err instanceof PaymentsError\n ? `The MPP credential was sent to ${originOf(input)} and the call then failed with an error that ` +\n `could not carry its own spend report (${detail}), so ${spend.creditsPresented} credits may or ` +\n 'may not have been burned. Do not blindly retry.'\n : `The MPP credential was sent to ${originOf(input)} but the request failed before any response ` +\n `was read (${detail}), so ${spend.creditsPresented} credits may or may not have been burned. ` +\n 'Do not blindly retry.',\n err,\n spend,\n )\n}\n"]}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Machine Payments Protocol (MPP) module.
|
|
3
|
+
*/
|
|
4
|
+
export { MppAPI, normalizeCredits } from './mpp-api.js';
|
|
5
|
+
export type { IssueMppChallengeParams, RedeemMppParams, MppSettleResult } from './mpp-api.js';
|
|
6
|
+
export { parseChallengeHeader, buildCredentialHeader, parseReceiptHeader, extractPaymentScheme, extractCredentialChallengeId, } from './codec.js';
|
|
7
|
+
export type { MppChallenge, MppChallengeRequest, MppReceipt } from './types.js';
|
|
8
|
+
export { MppError, MppNotConfiguredError, MppCredentialRejectedError, MppChallengeExpiredError, MppBodyDigestMismatchError, MppSettlementOutcomeUnknownError, isRetryableMppCode, MPP_SETTLEMENT_OUTCOME_UNKNOWN_CODE, MppSpendOutcomeUnknownError, MPP_SPEND_OUTCOME_UNKNOWN_CODE, mppSpendOf, } from './errors.js';
|
|
9
|
+
export type { MppSettlementOutcomeUnknown, MppSpendReport } from './errors.js';
|
|
10
|
+
export type { MppFetchOptions, MppFetchResult } from './fetch.js';
|
|
11
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/mpp/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,MAAM,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAA;AACvD,YAAY,EAAE,uBAAuB,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,cAAc,CAAA;AAC7F,OAAO,EACL,oBAAoB,EACpB,qBAAqB,EACrB,kBAAkB,EAClB,oBAAoB,EACpB,4BAA4B,GAC7B,MAAM,YAAY,CAAA;AACnB,YAAY,EAAE,YAAY,EAAE,mBAAmB,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAC/E,OAAO,EACL,QAAQ,EACR,qBAAqB,EACrB,0BAA0B,EAC1B,wBAAwB,EACxB,0BAA0B,EAC1B,gCAAgC,EAQhC,kBAAkB,EAClB,mCAAmC,EAKnC,2BAA2B,EAC3B,8BAA8B,EAC9B,UAAU,GACX,MAAM,aAAa,CAAA;AACpB,YAAY,EAAE,2BAA2B,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAY9E,YAAY,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Machine Payments Protocol (MPP) module.
|
|
3
|
+
*/
|
|
4
|
+
export { MppAPI, normalizeCredits } from './mpp-api.js';
|
|
5
|
+
export { parseChallengeHeader, buildCredentialHeader, parseReceiptHeader, extractPaymentScheme, extractCredentialChallengeId, } from './codec.js';
|
|
6
|
+
export { MppError, MppNotConfiguredError, MppCredentialRejectedError, MppChallengeExpiredError, MppBodyDigestMismatchError, MppSettlementOutcomeUnknownError,
|
|
7
|
+
// The docstring on isRetryableMppCode exists to stop a buyer hardcoding
|
|
8
|
+
// ['BCK.MPP.0004','BCK.MPP.0005'] at their call site, where it goes stale
|
|
9
|
+
// the moment a code joins the set — so the function has to be reachable
|
|
10
|
+
// from the package, not just from inside it. The wire-level `retryable`
|
|
11
|
+
// flag the middleware puts on its 402s only helps buyers of THIS
|
|
12
|
+
// middleware; a buyer calling payments.mpp.* directly and catching
|
|
13
|
+
// MppError has nothing but `error.code`.
|
|
14
|
+
isRetryableMppCode, MPP_SETTLEMENT_OUTCOME_UNKNOWN_CODE,
|
|
15
|
+
// Same argument as isRetryableMppCode: the accounting a buyer needs after a
|
|
16
|
+
// failed payment rides on two error classes with no common base (MppError and
|
|
17
|
+
// PaymentsError), so `mppSpendOf` has to be reachable from the package or
|
|
18
|
+
// every consumer hand-casts `(err as any).spend`.
|
|
19
|
+
MppSpendOutcomeUnknownError, MPP_SPEND_OUTCOME_UNKNOWN_CODE, mppSpendOf, } from './errors.js';
|
|
20
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/mpp/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,MAAM,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAA;AAEvD,OAAO,EACL,oBAAoB,EACpB,qBAAqB,EACrB,kBAAkB,EAClB,oBAAoB,EACpB,4BAA4B,GAC7B,MAAM,YAAY,CAAA;AAEnB,OAAO,EACL,QAAQ,EACR,qBAAqB,EACrB,0BAA0B,EAC1B,wBAAwB,EACxB,0BAA0B,EAC1B,gCAAgC;AAChC,wEAAwE;AACxE,0EAA0E;AAC1E,wEAAwE;AACxE,wEAAwE;AACxE,iEAAiE;AACjE,mEAAmE;AACnE,yCAAyC;AACzC,kBAAkB,EAClB,mCAAmC;AACnC,4EAA4E;AAC5E,8EAA8E;AAC9E,0EAA0E;AAC1E,kDAAkD;AAClD,2BAA2B,EAC3B,8BAA8B,EAC9B,UAAU,GACX,MAAM,aAAa,CAAA","sourcesContent":["/**\n * Machine Payments Protocol (MPP) module.\n */\n\nexport { MppAPI, normalizeCredits } from './mpp-api.js'\nexport type { IssueMppChallengeParams, RedeemMppParams, MppSettleResult } from './mpp-api.js'\nexport {\n parseChallengeHeader,\n buildCredentialHeader,\n parseReceiptHeader,\n extractPaymentScheme,\n extractCredentialChallengeId,\n} from './codec.js'\nexport type { MppChallenge, MppChallengeRequest, MppReceipt } from './types.js'\nexport {\n MppError,\n MppNotConfiguredError,\n MppCredentialRejectedError,\n MppChallengeExpiredError,\n MppBodyDigestMismatchError,\n MppSettlementOutcomeUnknownError,\n // The docstring on isRetryableMppCode exists to stop a buyer hardcoding\n // ['BCK.MPP.0004','BCK.MPP.0005'] at their call site, where it goes stale\n // the moment a code joins the set — so the function has to be reachable\n // from the package, not just from inside it. The wire-level `retryable`\n // flag the middleware puts on its 402s only helps buyers of THIS\n // middleware; a buyer calling payments.mpp.* directly and catching\n // MppError has nothing but `error.code`.\n isRetryableMppCode,\n MPP_SETTLEMENT_OUTCOME_UNKNOWN_CODE,\n // Same argument as isRetryableMppCode: the accounting a buyer needs after a\n // failed payment rides on two error classes with no common base (MppError and\n // PaymentsError), so `mppSpendOf` has to be reachable from the package or\n // every consumer hand-casts `(err as any).spend`.\n MppSpendOutcomeUnknownError,\n MPP_SPEND_OUTCOME_UNKNOWN_CODE,\n mppSpendOf,\n} from './errors.js'\nexport type { MppSettlementOutcomeUnknown, MppSpendReport } from './errors.js'\n// Marked `@experimental` on the types themselves: `MppFetchResult` gained\n// `settled` and `creditsPresented` between two rounds of a single review, and\n// epic #2024 lists a `payments.mpp.*` surface as an MVP non-goal. The tag is\n// what buys the freedom to keep changing it in a minor release — the\n// alternative (withdrawing the surface) costs more and helps nobody.\n// `mppFetch` and `MppTokenMinter` are deliberately NOT re-exported here.\n// `payments.mpp.fetch` (via `MppAPI`, exported above) is the intended public\n// surface — it routes through `MppAPI.post`'s error translation and the\n// `Nevermined-Version` pinning. Exporting the free function would hand\n// consumers a supported way to bypass both, which could not be withdrawn\n// later without a major bump. Tests import `./fetch.js` directly.\nexport type { MppFetchOptions, MppFetchResult } from './fetch.js'\n"]}
|