@catena/sdk 0.0.0-bootstrap.0 → 0.1.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/mpp.mjs ADDED
@@ -0,0 +1,564 @@
1
+ import { T as paymentCredentialFromIntentData, n as IntentSubmitError, t as ApiError } from "./client-CHZMO00P.mjs";
2
+ import { a as assertNever, i as isReplayableFetchRequest, n as reportSettlementHint, r as fetchRetryHeaders, t as EVM_TRANSACTION_HASH_PATTERN } from "./settlement-report-CT3EbtLL.mjs";
3
+ import { Credential, Method, PaymentRequest, Receipt, z } from "mppx";
4
+ import { Mppx } from "mppx/client";
5
+ import { isAddress } from "viem";
6
+ //#region src/lib/mpp-resource-url.ts
7
+ const MPP_MAX_RESOURCE_URL_LENGTH = 2048;
8
+ var MppResourceUrlError = class extends Error {
9
+ name = "MppResourceUrlError";
10
+ };
11
+ function canonicalizeMppResourceUrl(input) {
12
+ if (input.length > MPP_MAX_RESOURCE_URL_LENGTH) throw new MppResourceUrlError("MPP resource URL is too long");
13
+ if (Array.from(input).some((character) => {
14
+ const code = character.charCodeAt(0);
15
+ return character === "\\" || code <= 32 || code === 127;
16
+ })) throw new MppResourceUrlError("MPP resource URL is invalid");
17
+ const components = input.match(/^([A-Za-z][A-Za-z0-9+.-]*):\/\/([^/?#]*)([^?#]*)(\?[^#]*)?(?:#.*)?$/);
18
+ if (!components) throw new MppResourceUrlError("MPP resource URL must be absolute");
19
+ let parsed;
20
+ try {
21
+ parsed = new URL(input);
22
+ } catch {
23
+ throw new MppResourceUrlError("MPP resource URL is invalid");
24
+ }
25
+ if (parsed.username || parsed.password) throw new MppResourceUrlError("MPP resource URL must not contain credentials");
26
+ const protocol = parsed.protocol.toLowerCase();
27
+ if (protocol !== "https:" && !(protocol === "http:" && isLoopbackHostname(parsed.hostname))) throw new MppResourceUrlError("MPP resource URL must use HTTPS or loopback HTTP");
28
+ const rawPath = components[3] || "/";
29
+ const rawQuery = components[4] || "";
30
+ const canonical = `${protocol}//${parsed.hostname.toLowerCase()}${parsed.port ? `:${parsed.port}` : ""}${rawPath}${rawQuery}`;
31
+ if (canonical.length > MPP_MAX_RESOURCE_URL_LENGTH) throw new MppResourceUrlError("MPP resource URL is too long");
32
+ return canonical;
33
+ }
34
+ function isLoopbackHostname(hostname) {
35
+ const normalized = hostname.toLowerCase();
36
+ if (normalized === "localhost" || normalized === "[::1]") return true;
37
+ const octets = normalized.split(".");
38
+ return octets.length === 4 && octets[0] === "127" && octets.every((octet) => /^\d{1,3}$/.test(octet) && Number(octet) <= 255);
39
+ }
40
+ //#endregion
41
+ //#region src/mpp.ts
42
+ const BASE_USDC = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
43
+ const BASE_SEPOLIA_USDC = "0x036CbD53842c5426634e7929541eC2318f3dCF7e";
44
+ const MAX_REQUEST_LENGTH = 16384;
45
+ const MAX_CHALLENGE_BYTES = 32768;
46
+ const MIN_REMAINING_MS = 5e3;
47
+ const MAX_VALIDITY_MS = 36e5;
48
+ const BASE64URL_PATTERN = /^[A-Za-z0-9_-]+$/;
49
+ const ADDRESS_PATTERN = /^0x[0-9a-fA-F]{40}$/;
50
+ const AMOUNT_PATTERN = /^(?:0|[1-9]\d*)$/;
51
+ const RFC3339_UTC_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?Z$/;
52
+ const RFC9530_DIGEST_PATTERN = /^sha-256=:([A-Za-z0-9+/]{43}=):$/;
53
+ const MPPX_0_9_DIGEST_PATTERN = /^sha-256=([A-Za-z0-9+/]{43}=)$/;
54
+ const MAX_CANDIDATE_CHALLENGES = 5;
55
+ const FALLBACK_ERROR_CODES = new Set([
56
+ "mpp_challenge_not_payable",
57
+ "mpp_network_mismatch",
58
+ "mpp_counterparty_rail_not_found"
59
+ ]);
60
+ const COUNTERPARTY_RAIL_NOT_FOUND_CODE = "mpp_counterparty_rail_not_found";
61
+ const NETWORK_MISMATCH_CODE = "mpp_network_mismatch";
62
+ const authorizationPayloadSchema = z.object({
63
+ type: z.literal("authorization"),
64
+ from: z.string(),
65
+ to: z.string(),
66
+ value: z.string(),
67
+ validAfter: z.string(),
68
+ validBefore: z.string(),
69
+ nonce: z.string(),
70
+ signature: z.string()
71
+ });
72
+ const commonRequestFields = {
73
+ amount: z.string(),
74
+ currency: z.string(),
75
+ description: z.optional(z.string()),
76
+ externalId: z.optional(z.string()),
77
+ recipient: z.string()
78
+ };
79
+ const evmRequestSchema = z.object({
80
+ ...commonRequestFields,
81
+ methodDetails: z.object({
82
+ chainId: z.number(),
83
+ credentialTypes: z.array(z.literal("authorization")),
84
+ decimals: z.literal(6),
85
+ permit2Address: z.optional(z.string()),
86
+ splits: z.optional(z.never())
87
+ })
88
+ });
89
+ const usdcRequestSchema = z.object({
90
+ ...commonRequestFields,
91
+ methodDetails: z.object({
92
+ type: z.literal("evm"),
93
+ evm: z.object({
94
+ chainId: z.number(),
95
+ credentialTypes: z.optional(z.array(z.literal("authorization"))),
96
+ decimals: z.literal(6),
97
+ permit2Address: z.optional(z.string()),
98
+ splits: z.optional(z.never())
99
+ })
100
+ })
101
+ });
102
+ const evmCharge = Method.from({
103
+ name: "evm",
104
+ intent: "charge",
105
+ schema: {
106
+ credential: { payload: authorizationPayloadSchema },
107
+ request: evmRequestSchema
108
+ }
109
+ });
110
+ const usdcCharge = Method.from({
111
+ name: "usdc",
112
+ intent: "charge",
113
+ schema: {
114
+ credential: { payload: authorizationPayloadSchema },
115
+ request: usdcRequestSchema
116
+ }
117
+ });
118
+ var MppPaymentError = class extends Error {
119
+ intentId;
120
+ status;
121
+ reasons;
122
+ expiresAt;
123
+ constructor(message, details) {
124
+ super(message);
125
+ this.name = "MppPaymentError";
126
+ this.intentId = details.intentId;
127
+ this.status = details.status;
128
+ this.reasons = details.reasons;
129
+ this.expiresAt = details.expiresAt;
130
+ }
131
+ };
132
+ var MppFetchPaymentError = class extends Error {
133
+ intentId;
134
+ reasons;
135
+ constructor(message, details = {}) {
136
+ super(message, details.cause === void 0 ? void 0 : { cause: details.cause });
137
+ this.name = "MppFetchPaymentError";
138
+ this.intentId = details.intentId;
139
+ this.reasons = details.reasons ?? [];
140
+ }
141
+ };
142
+ var MppNetworkMismatchError = class extends MppFetchPaymentError {
143
+ accountId;
144
+ requiredNetworks;
145
+ constructor(accountId, requiredNetworks, reasons) {
146
+ super(`Account ${accountId} cannot pay on the MPP challenge's required network${requiredNetworks.length === 1 ? "" : "s"}: ${requiredNetworks.join(", ")}`, { reasons });
147
+ this.name = "MppNetworkMismatchError";
148
+ this.accountId = accountId;
149
+ this.requiredNetworks = requiredNetworks;
150
+ }
151
+ };
152
+ var MppCounterpartyNotFoundError = class extends MppFetchPaymentError {
153
+ recipient;
154
+ network;
155
+ constructor(recipient, network, reasons) {
156
+ super(`No saved Catena counterparty rail can pay ${recipient} on ${network}`, { reasons });
157
+ this.name = "MppCounterpartyNotFoundError";
158
+ this.recipient = recipient;
159
+ this.network = network;
160
+ }
161
+ };
162
+ var MppApprovalPendingError = class extends MppFetchPaymentError {
163
+ expiresAt;
164
+ constructor(intentId, reasons, expiresAt) {
165
+ super(`The MPP payment is awaiting human approval (intent ${intentId}); retry the request after approval`, {
166
+ intentId,
167
+ reasons
168
+ });
169
+ this.name = "MppApprovalPendingError";
170
+ this.expiresAt = expiresAt;
171
+ }
172
+ };
173
+ var MppPaymentDeclinedError = class extends MppFetchPaymentError {
174
+ status;
175
+ constructor(intentId, status, reasons) {
176
+ super(`Catena ${status} the MPP payment (intent ${intentId})`, {
177
+ intentId,
178
+ reasons
179
+ });
180
+ this.name = "MppPaymentDeclinedError";
181
+ this.status = status;
182
+ }
183
+ };
184
+ var MppSubmitInterruptedError = class extends MppFetchPaymentError {
185
+ method;
186
+ atomicAmount;
187
+ network;
188
+ recipient;
189
+ resourceUrl;
190
+ constructor(intentId, terms, resourceUrl, cause) {
191
+ super(`The MPP payment could not be confirmed (intent ${intentId}); reconcile it with getIntent("${intentId}") before paying again`, {
192
+ intentId,
193
+ cause
194
+ });
195
+ this.name = "MppSubmitInterruptedError";
196
+ this.method = terms.method;
197
+ this.atomicAmount = terms.atomicAmount;
198
+ this.network = terms.network;
199
+ this.recipient = terms.recipient;
200
+ this.resourceUrl = resourceUrl;
201
+ }
202
+ };
203
+ var MppRetryFailedError = class extends MppFetchPaymentError {
204
+ receipt;
205
+ constructor(receipt, cause) {
206
+ super(`The MPP payment completed (intent ${receipt.intentId}) but the post-payment continuation failed; inspect getIntent("${receipt.intentId}") before retrying manually`, {
207
+ intentId: receipt.intentId,
208
+ cause
209
+ });
210
+ this.name = "MppRetryFailedError";
211
+ this.receipt = receipt;
212
+ }
213
+ };
214
+ function charge(parameters) {
215
+ const createCredential = async ({ challenge }) => {
216
+ return (await submitCatenaMppPayment(parameters, prepareCatenaMppPayment(challenge, parameters.maxAtomicAmount))).credential;
217
+ };
218
+ return [Method.toClient(evmCharge, {
219
+ canHandleChallenge: ({ challenge }) => supportedChallenge("evm", challenge, parameters.maxAtomicAmount) !== void 0,
220
+ createCredential
221
+ }), Method.toClient(usdcCharge, {
222
+ canHandleChallenge: ({ challenge }) => supportedChallenge("usdc", challenge, parameters.maxAtomicAmount) !== void 0,
223
+ createCredential
224
+ })];
225
+ }
226
+ const catena = Object.assign((parameters) => charge(parameters), { charge });
227
+ function wrapFetchWithMppPayment(client, options) {
228
+ const baseFetch = (options.baseFetch ?? globalThis.fetch).bind(globalThis);
229
+ const parameters = {
230
+ client,
231
+ accountId: options.accountId,
232
+ ...options.maxAtomicAmount !== void 0 && { maxAtomicAmount: options.maxAtomicAmount }
233
+ };
234
+ const mppx = Mppx.create({
235
+ methods: catena(parameters),
236
+ fetch: baseFetch,
237
+ polyfill: false
238
+ });
239
+ return async (input, init) => {
240
+ const response = await baseFetch(input, init);
241
+ if (response.status !== 402 || !response.headers.has("WWW-Authenticate")) return response;
242
+ let challenges;
243
+ try {
244
+ challenges = await mppx.transport.getChallenges(response, init);
245
+ } catch {
246
+ return response;
247
+ }
248
+ if (challenges.length === 0) return response;
249
+ if (!isReplayableFetchRequest(input, init)) throw new MppFetchPaymentError("The request body cannot be replayed after MPP payment; buffer it as a string, URLSearchParams, Blob, ArrayBuffer, or typed array");
250
+ const resourceUrl = paymentResourceUrl(input, response);
251
+ const { payment, prepared } = await paySupportedMppChallenge(mppx, response, challenges, init, parameters, resourceUrl);
252
+ const receipt = {
253
+ intentId: payment.intentId,
254
+ ...payment.terms,
255
+ resourceUrl
256
+ };
257
+ await preserveMppPaymentReceipt(receipt, async () => {
258
+ await options.onPayment?.(receipt);
259
+ });
260
+ const headers = fetchRetryHeaders(input, init);
261
+ const paidInit = prepared.setCredential({
262
+ ...init,
263
+ headers
264
+ }, payment.credential);
265
+ const paidResponse = await preserveMppPaymentReceipt(receipt, () => baseFetch(input, paidInit));
266
+ if (paidResponse.status === 402) throw new MppRetryFailedError(receipt, new Error("The paid MPP retry still requires payment"));
267
+ reportMppSettlement(client, receipt.intentId, paidResponse);
268
+ return paidResponse;
269
+ };
270
+ }
271
+ async function paySupportedMppChallenge(mppx, response, challenges, request, parameters, resourceUrl) {
272
+ let candidateIndexes;
273
+ const skips = [];
274
+ for (let attempt = 0; attempt < MAX_CANDIDATE_CHALLENGES; attempt += 1) {
275
+ const selection = { missing: false };
276
+ let prepared;
277
+ try {
278
+ prepared = await mppx.preparePayment(response, {
279
+ request,
280
+ orderChallenges: (candidates) => {
281
+ candidateIndexes ??= candidates.slice(0, MAX_CANDIDATE_CHALLENGES).map((candidate) => candidate.index);
282
+ const candidateIndex = candidateIndexes[attempt];
283
+ const candidate = candidates.find((current) => current.index === candidateIndex);
284
+ selection.missing = candidate === void 0;
285
+ return candidate === void 0 ? [] : [candidate];
286
+ }
287
+ });
288
+ } catch (cause) {
289
+ const currentCandidateIndexes = candidateIndexes ?? [];
290
+ if (currentCandidateIndexes.length === 0) {
291
+ if (challenges.some((challenge) => exceedsMaximumAtomicAmount(challenge, parameters.maxAtomicAmount))) throw new MppFetchPaymentError("Every supported charge in the MPP challenge costs more than the caller's maximum amount", { cause });
292
+ throw new MppFetchPaymentError("The MPP challenge offers no Catena-supported native USDC charge", { cause });
293
+ }
294
+ if (selection.missing && skips.length > 0) {
295
+ if (attempt + 1 < currentCandidateIndexes.length) continue;
296
+ break;
297
+ }
298
+ throw new MppFetchPaymentError("The selected MPP challenge could not be prepared", { cause });
299
+ }
300
+ const selected = prepareCatenaMppPayment(prepared.challenge, parameters.maxAtomicAmount);
301
+ try {
302
+ return {
303
+ payment: await submitCatenaMppPayment(parameters, selected, resourceUrl),
304
+ prepared
305
+ };
306
+ } catch (error) {
307
+ if (error instanceof ApiError && error.code !== void 0 && FALLBACK_ERROR_CODES.has(error.code)) skips.push({
308
+ error,
309
+ terms: selected.terms
310
+ });
311
+ else throw managedMppPaymentError(error, selected.terms, resourceUrl);
312
+ }
313
+ if (attempt + 1 >= (candidateIndexes?.length ?? 0)) break;
314
+ }
315
+ throw terminalMppCandidateError(parameters.accountId, skips);
316
+ }
317
+ function managedMppPaymentError(error, terms, resourceUrl) {
318
+ if (error instanceof IntentSubmitError && error.outcome === "unknown") return new MppSubmitInterruptedError(error.intentId, terms, resourceUrl, error);
319
+ if (!(error instanceof MppPaymentError)) return error;
320
+ switch (error.status) {
321
+ case "pending": return new MppApprovalPendingError(error.intentId, error.reasons, error.expiresAt);
322
+ case "blocked":
323
+ case "failed": return new MppPaymentDeclinedError(error.intentId, error.status, error.reasons);
324
+ case "processing":
325
+ case "completed": return new MppFetchPaymentError(error.message, {
326
+ intentId: error.intentId,
327
+ reasons: error.reasons,
328
+ cause: error
329
+ });
330
+ }
331
+ return assertNever(error.status);
332
+ }
333
+ function terminalMppCandidateError(accountId, skips) {
334
+ const counterpartySkips = skips.filter((skip) => skip.error.code === COUNTERPARTY_RAIL_NOT_FOUND_CODE);
335
+ const mismatchSkips = skips.filter((skip) => skip.error.code === NETWORK_MISMATCH_CODE);
336
+ const otherSkips = skips.filter((skip) => skip.error.code !== COUNTERPARTY_RAIL_NOT_FOUND_CODE && skip.error.code !== NETWORK_MISMATCH_CODE);
337
+ const reasons = [...new Set([
338
+ ...counterpartySkips,
339
+ ...mismatchSkips,
340
+ ...otherSkips
341
+ ].map((skip) => skip.error.message))];
342
+ const unsaved = counterpartySkips.find((skip) => skip.error.status === 404);
343
+ if (unsaved) return new MppCounterpartyNotFoundError(unsaved.terms.recipient, unsaved.terms.network, reasons);
344
+ if (mismatchSkips.length > 0 && counterpartySkips.length === 0) return new MppNetworkMismatchError(accountId, [...new Set(mismatchSkips.map((skip) => skip.terms.network))], reasons);
345
+ return new MppFetchPaymentError("No supported candidate in the MPP challenge is payable from this account", { reasons });
346
+ }
347
+ function paymentResourceUrl(input, response) {
348
+ let requested;
349
+ try {
350
+ requested = typeof input === "string" ? absoluteOrBrowserUrl(input) : input instanceof URL ? input : new URL(input.url);
351
+ } catch (cause) {
352
+ throw new MppFetchPaymentError("The MPP resource request URL must be absolute", { cause });
353
+ }
354
+ let finalUrl;
355
+ try {
356
+ finalUrl = new URL(response.url || requested.href);
357
+ } catch (cause) {
358
+ throw new MppFetchPaymentError("The final MPP resource URL is invalid", { cause });
359
+ }
360
+ if (finalUrl.origin !== requested.origin) throw new MppFetchPaymentError(`Refusing to pay after a cross-origin redirect from ${requested.origin} to ${finalUrl.origin}`);
361
+ try {
362
+ return canonicalizeMppResourceUrl(response.url || requested.href);
363
+ } catch (cause) {
364
+ throw new MppFetchPaymentError("The final MPP resource URL cannot be bound to a payment intent", { cause });
365
+ }
366
+ }
367
+ function absoluteOrBrowserUrl(input) {
368
+ try {
369
+ return new URL(input);
370
+ } catch {
371
+ return new URL(input, globalThis.location.href);
372
+ }
373
+ }
374
+ async function preserveMppPaymentReceipt(receipt, operation) {
375
+ try {
376
+ return await operation();
377
+ } catch (cause) {
378
+ throw new MppRetryFailedError(receipt, cause);
379
+ }
380
+ }
381
+ function reportMppSettlement(client, intentId, response) {
382
+ try {
383
+ const receipt = Receipt.fromResponse(response);
384
+ if (!EVM_TRANSACTION_HASH_PATTERN.test(receipt.reference)) return;
385
+ reportSettlementHint(client, intentId, receipt.reference);
386
+ } catch {
387
+ return;
388
+ }
389
+ }
390
+ function prepareCatenaMppPayment(challenge, maxAtomicAmount) {
391
+ const method = challenge.method;
392
+ if (method !== "evm" && method !== "usdc") throw new Error(`Unsupported Catena MPP challenge ${challenge.method}/${challenge.intent}`);
393
+ const supported = supportedChallenge(method, challenge, maxAtomicAmount);
394
+ if (supported === void 0) throw new Error("Catena MPP charge challenge failed validation");
395
+ return {
396
+ challenge: {
397
+ id: challenge.id,
398
+ realm: challenge.realm,
399
+ method,
400
+ intent: "charge",
401
+ request: supported.request,
402
+ ...challenge.description !== void 0 && { description: challenge.description },
403
+ ...challenge.digest !== void 0 && { digest: challenge.digest },
404
+ expires: supported.expires,
405
+ ...supported.header !== void 0 && { header: supported.header },
406
+ ...challenge.opaque !== void 0 && { opaque: challenge.opaque }
407
+ },
408
+ terms: supported.terms
409
+ };
410
+ }
411
+ async function submitCatenaMppPayment(parameters, prepared, resourceUrl) {
412
+ const result = await parameters.client.submitIntent({ action: {
413
+ type: "mpp",
414
+ accountId: parameters.accountId,
415
+ challenge: prepared.challenge,
416
+ ...resourceUrl !== void 0 && { resource: { url: resourceUrl } }
417
+ } });
418
+ return {
419
+ ...prepared,
420
+ credential: credentialFromResult(result, prepared.challenge),
421
+ intentId: result.id
422
+ };
423
+ }
424
+ function supportedChallenge(method, challenge, maxAtomicAmount) {
425
+ if (challenge.method !== method || challenge.intent !== "charge") return;
426
+ const envelope = supportedEnvelope(challenge);
427
+ if (envelope === void 0) return;
428
+ const terms = method === "evm" ? supportedEvmRequest(challenge.request, maxAtomicAmount) : supportedUsdcRequest(challenge.request, maxAtomicAmount);
429
+ return terms === void 0 ? void 0 : {
430
+ ...envelope,
431
+ terms
432
+ };
433
+ }
434
+ function exceedsMaximumAtomicAmount(challenge, maxAtomicAmount) {
435
+ if (maxAtomicAmount === void 0 || challenge.method !== "evm" && challenge.method !== "usdc") return false;
436
+ const supported = supportedChallenge(challenge.method, challenge, void 0);
437
+ return supported !== void 0 && BigInt(supported.terms.atomicAmount) > maxAtomicAmount;
438
+ }
439
+ function hasSupportedCommonRequest(request, maxAtomicAmount) {
440
+ return isCanonicalPositiveAmount(request.amount) && (maxAtomicAmount === void 0 || BigInt(request.amount) <= maxAtomicAmount) && isAddress$1(request.currency) && isAddress$1(request.recipient) && (request.description === void 0 || isCanonicalizableText(request.description, 1024)) && (request.externalId === void 0 || isCanonicalizableText(request.externalId, 256));
441
+ }
442
+ function isCanonicalizableText(value, maximumLength) {
443
+ if (value.length > maximumLength) return false;
444
+ for (const character of value) {
445
+ const codePoint = character.codePointAt(0);
446
+ if (codePoint !== void 0 && codePoint >= 55296 && codePoint <= 57343) return false;
447
+ }
448
+ return true;
449
+ }
450
+ function supportedEvmRequest(request, maxAtomicAmount) {
451
+ const result = evmRequestSchema.safeParse(request);
452
+ if (!result.success || !hasSupportedCommonRequest(result.data, maxAtomicAmount)) return;
453
+ const { amount, currency, methodDetails, recipient } = result.data;
454
+ const network = nativeUsdcNetwork(methodDetails.chainId, currency);
455
+ return isSupportedMethodDetails(methodDetails) && methodDetails.credentialTypes.length === 1 && network !== void 0 ? {
456
+ method: "evm",
457
+ atomicAmount: amount,
458
+ network,
459
+ recipient
460
+ } : void 0;
461
+ }
462
+ function supportedUsdcRequest(request, maxAtomicAmount) {
463
+ const result = usdcRequestSchema.safeParse(request);
464
+ if (!result.success || !hasSupportedCommonRequest(result.data, maxAtomicAmount)) return;
465
+ const { amount, currency, methodDetails, recipient } = result.data;
466
+ const details = methodDetails.evm;
467
+ const network = nativeUsdcNetwork(details.chainId, currency);
468
+ return isSupportedMethodDetails(details) && (details.credentialTypes === void 0 || details.credentialTypes.length === 1) && network !== void 0 ? {
469
+ method: "usdc",
470
+ atomicAmount: amount,
471
+ network,
472
+ recipient
473
+ } : void 0;
474
+ }
475
+ function isSupportedMethodDetails(details) {
476
+ return Number.isSafeInteger(details.chainId) && details.chainId >= 1 && (details.permit2Address === void 0 || isAddress$1(details.permit2Address));
477
+ }
478
+ function nativeUsdcNetwork(chainId, currency) {
479
+ if (chainId === 8453) return currency.toLowerCase() === BASE_USDC.toLowerCase() ? "eip155:8453" : void 0;
480
+ if (chainId === 84532) return currency.toLowerCase() === BASE_SEPOLIA_USDC.toLowerCase() ? "eip155:84532" : void 0;
481
+ }
482
+ function supportedEnvelope(challenge) {
483
+ if (!hasSupportedChallengeStrings(challenge)) return;
484
+ const expiresAt = Date.parse(challenge.expires);
485
+ const remaining = expiresAt - Date.now();
486
+ if (!Number.isFinite(expiresAt) || remaining < MIN_REMAINING_MS || remaining > MAX_VALIDITY_MS) return;
487
+ const request = PaymentRequest.serialize(challenge.request);
488
+ const serializedBytes = new TextEncoder().encode(JSON.stringify({
489
+ ...challenge,
490
+ request
491
+ })).byteLength;
492
+ return request.length > 0 && request.length <= MAX_REQUEST_LENGTH && serializedBytes <= MAX_CHALLENGE_BYTES ? {
493
+ request,
494
+ expires: challenge.expires,
495
+ header: challenge.header
496
+ } : void 0;
497
+ }
498
+ function hasSupportedChallengeStrings(challenge) {
499
+ return !(challenge.id.length < 1 || challenge.id.length > 256 || challenge.realm.length < 1 || challenge.realm.length > 256 || challenge.expires === void 0 || !RFC3339_UTC_PATTERN.test(challenge.expires) || challenge.description !== void 0 && challenge.description.length > 1024 || challenge.header !== void 0 && challenge.header !== "Payment-Authorization" || challenge.opaque !== void 0 && (challenge.opaque.length > 8192 || !BASE64URL_PATTERN.test(challenge.opaque)) || challenge.digest !== void 0 && !isSupportedDigest(challenge.digest));
500
+ }
501
+ function isSupportedDigest(value) {
502
+ if (value.length > 128) return false;
503
+ const match = RFC9530_DIGEST_PATTERN.exec(value) ?? MPPX_0_9_DIGEST_PATTERN.exec(value);
504
+ if (!match) return false;
505
+ try {
506
+ const decoded = atob(match[1]);
507
+ return decoded.length === 32 && btoa(decoded) === match[1];
508
+ } catch {
509
+ return false;
510
+ }
511
+ }
512
+ function isCanonicalPositiveAmount(value) {
513
+ return value.length <= 78 && AMOUNT_PATTERN.test(value) && BigInt(value) > 0n;
514
+ }
515
+ function isAddress$1(value) {
516
+ return ADDRESS_PATTERN.test(value) && isAddress(value);
517
+ }
518
+ function credentialFromResult(result, selectedChallenge) {
519
+ switch (result.status) {
520
+ case "completed": {
521
+ const credential = paymentCredentialFromIntentData(result.data, "mpp");
522
+ if (credential === void 0) throw paymentError(`Catena MPP payment ${result.id} completed without an MPP credential`, result);
523
+ if (!isCredentialForChallenge(credential, selectedChallenge)) throw paymentError(`Catena MPP payment ${result.id} completed with a credential that does not match the selected challenge; reconcile with client.getIntent("${result.id}") before another payment attempt`, result);
524
+ return credential;
525
+ }
526
+ case "pending": throw paymentError(`Catena MPP payment ${result.id} is pending${formatReasons(result.reasons)}`, result);
527
+ case "processing": throw paymentError(`Catena MPP payment ${result.id} is processing; if it was approved, re-run the paid request so a fresh challenge can consume the grant (polling never advances an approved payment); otherwise poll client.getIntent("${result.id}"), because an executing payment completes on its own`, result);
528
+ case "blocked": throw paymentError(`Catena MPP payment ${result.id} was blocked${formatReasons(result.reasons)}`, result);
529
+ case "failed": throw paymentError(`Catena MPP payment ${result.id} failed${formatReasons(result.reasons)}`, result);
530
+ }
531
+ return assertNever(result.status);
532
+ }
533
+ function isCredentialForChallenge(value, expected) {
534
+ if (value.trim().length === 0 || value.includes("\r") || value.includes("\n")) return false;
535
+ let credential;
536
+ try {
537
+ credential = Credential.deserialize(value);
538
+ } catch {
539
+ return false;
540
+ }
541
+ const challenge = credential.challenge;
542
+ if (challenge.id !== expected.id || challenge.realm !== expected.realm || challenge.method !== expected.method || challenge.intent !== expected.intent || challenge.description !== expected.description || challenge.digest !== expected.digest || challenge.expires !== expected.expires || challenge.header !== expected.header || challenge.opaque !== expected.opaque || PaymentRequest.serialize(challenge.request) !== expected.request) return false;
543
+ const payload = authorizationPayloadSchema.safeParse(credential.payload);
544
+ const request = expected.method === "evm" ? evmRequestSchema.safeParse(challenge.request) : usdcRequestSchema.safeParse(challenge.request);
545
+ if (!payload.success || !request.success) return false;
546
+ const recipient = request.data.recipient;
547
+ return isAddress$1(payload.data.from) && isAddress$1(payload.data.to) && payload.data.to.toLowerCase() === recipient.toLowerCase() && payload.data.value === request.data.amount && isCanonicalAtomicValue(payload.data.validAfter) && isCanonicalAtomicValue(payload.data.validBefore) && BigInt(payload.data.validBefore) > BigInt(payload.data.validAfter) && /^0x[0-9a-fA-F]{64}$/.test(payload.data.nonce) && /^0x[0-9a-fA-F]{130}$/.test(payload.data.signature);
548
+ }
549
+ function isCanonicalAtomicValue(value) {
550
+ return value.length <= 78 && AMOUNT_PATTERN.test(value);
551
+ }
552
+ function paymentError(message, result) {
553
+ return new MppPaymentError(message, {
554
+ intentId: result.id,
555
+ status: result.status,
556
+ reasons: result.reasons,
557
+ expiresAt: result.expiresAt
558
+ });
559
+ }
560
+ function formatReasons(reasons) {
561
+ return reasons.length > 0 ? `: ${reasons.join("; ")}` : "";
562
+ }
563
+ //#endregion
564
+ export { MppApprovalPendingError, MppCounterpartyNotFoundError, MppFetchPaymentError, MppNetworkMismatchError, MppPaymentDeclinedError, MppPaymentError, MppRetryFailedError, MppSubmitInterruptedError, catena, wrapFetchWithMppPayment };
@@ -0,0 +1,36 @@
1
+ //#region src/lib/assert-never.ts
2
+ function assertNever(value) {
3
+ throw new Error(`Unhandled case: ${String(value)}`);
4
+ }
5
+ //#endregion
6
+ //#region src/lib/fetch-replay.ts
7
+ function isReplayableFetchRequest(input, init) {
8
+ if (init?.body !== void 0 && init.body !== null) {
9
+ if (typeof init.body !== "string" && !(init.body instanceof URLSearchParams) && !(init.body instanceof Blob) && !(init.body instanceof ArrayBuffer) && !ArrayBuffer.isView(init.body)) return false;
10
+ }
11
+ if (typeof Request !== "undefined" && input instanceof Request) return input.body === null && !input.bodyUsed;
12
+ return true;
13
+ }
14
+ function fetchRetryHeaders(input, init) {
15
+ const headers = new Headers(typeof Request !== "undefined" && input instanceof Request ? input.headers : void 0);
16
+ new Headers(init?.headers).forEach((value, key) => {
17
+ headers.set(key, value);
18
+ });
19
+ return headers;
20
+ }
21
+ //#endregion
22
+ //#region src/lib/settlement-report.ts
23
+ const EVM_TRANSACTION_HASH_PATTERN = /^0x[0-9a-fA-F]{64}$/;
24
+ async function reportSettlementHint(client, intentId, txHash, receipt) {
25
+ try {
26
+ await client.reportSettlement({
27
+ intentId,
28
+ txHash,
29
+ ...receipt !== void 0 && { receipt }
30
+ });
31
+ } catch {
32
+ return;
33
+ }
34
+ }
35
+ //#endregion
36
+ export { assertNever as a, isReplayableFetchRequest as i, reportSettlementHint as n, fetchRetryHeaders as r, EVM_TRANSACTION_HASH_PATTERN as t };