@twzrd1111/clawrouter-mpp 0.0.0-stage → 0.2.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/index.js ADDED
@@ -0,0 +1,1055 @@
1
+ // src/mpp-fetch.ts
2
+ import { AsyncLocalStorage as AsyncLocalStorage3 } from "async_hooks";
3
+ import {
4
+ assertSpendPolicyAllows,
5
+ SpendPolicyError as SpendPolicyError2
6
+ } from "@twzrd1111/clawrouter";
7
+
8
+ // src/mpp-paid-failure.ts
9
+ import { createHash } from "crypto";
10
+ import { AsyncLocalStorage } from "async_hooks";
11
+ import {
12
+ PAYMENT_SETTLED_NO_COMPLETION,
13
+ SpendPolicyError
14
+ } from "@twzrd1111/clawrouter";
15
+
16
+ // src/credit-record.ts
17
+ var CreditLedger = class {
18
+ rows = [];
19
+ /** One row per payer and challenge. A retry returns the first row. */
20
+ recordOnce(entry) {
21
+ const prior = this.rows.find(
22
+ (row) => row.payer === entry.payer && row.challengeId === entry.challengeId
23
+ );
24
+ if (prior) return prior;
25
+ this.rows.push(Object.freeze({ ...entry }));
26
+ return this.rows[this.rows.length - 1];
27
+ }
28
+ read() {
29
+ return Object.freeze([...this.rows]);
30
+ }
31
+ };
32
+
33
+ // src/mpp-paid-failure.ts
34
+ var MppPaidNoCompletionError = class extends Error {
35
+ reference;
36
+ status;
37
+ constructor(message, reference, status, options) {
38
+ super(message, options);
39
+ this.name = PAYMENT_SETTLED_NO_COMPLETION;
40
+ this.reference = reference;
41
+ this.status = status;
42
+ }
43
+ };
44
+ function creditPaidNoCompletion(ledger, err, fields, hooks) {
45
+ if (!(err instanceof MppPaidNoCompletionError)) {
46
+ throw new TypeError("credit requires MppPaidNoCompletionError");
47
+ }
48
+ void hooks;
49
+ return ledger.recordOnce(fields);
50
+ }
51
+ var credits = new CreditLedger();
52
+ function creditFromChallenge(challenge, payer) {
53
+ const request = challenge.request;
54
+ const amount = request && "amount" in request ? request.amount : void 0;
55
+ if (typeof amount !== "string" || !/^\d+$/.test(amount) || !payer || !challenge.id)
56
+ return void 0;
57
+ return { payer, amount, challengeId: challenge.id };
58
+ }
59
+ function readCredits(ledger = credits) {
60
+ return ledger.read();
61
+ }
62
+ var storage = new AsyncLocalStorage();
63
+ var paidResponses = /* @__PURE__ */ new WeakMap();
64
+ function rejectPaidNoCompletion(response, reason) {
65
+ const paid = paidResponses.get(response);
66
+ if (paid) throw fail(paid.spend, paid.call, response.status, reason);
67
+ }
68
+ function isPaidResponse(response) {
69
+ return paidResponses.has(response);
70
+ }
71
+ function readPaidCallEvidence(response) {
72
+ const paid = paidResponses.get(response);
73
+ if (!paid) return void 0;
74
+ const { payer, reference, credit, gate, payment } = paid.call;
75
+ return {
76
+ payer,
77
+ ...reference ? { reference } : {},
78
+ ...credit ? { credit } : {},
79
+ ...gate ? { gate } : {},
80
+ ...payment ? { payment } : {}
81
+ };
82
+ }
83
+ function currentPaidCall() {
84
+ return storage.getStore();
85
+ }
86
+ function markGateEvaluated(decision) {
87
+ const call = storage.getStore();
88
+ if (call && decision) call.gate = decision;
89
+ }
90
+ function markPaymentChallenge(payment) {
91
+ const call = storage.getStore();
92
+ if (call) call.payment = payment;
93
+ }
94
+ function paymentReference(payload) {
95
+ return `mpp-${createHash("sha256").update(payload).digest("hex").slice(0, 16)}`;
96
+ }
97
+ function markSigned(payload, facts, record) {
98
+ const call = storage.getStore();
99
+ if (!call || call.signed) return;
100
+ call.signed = true;
101
+ call.reference = paymentReference(payload);
102
+ call.record = record;
103
+ call.credit = facts;
104
+ }
105
+ async function guardPaid(spend, run, payer) {
106
+ const call = { signed: false, payer };
107
+ let response;
108
+ try {
109
+ response = await storage.run(call, run);
110
+ } catch (err) {
111
+ if (!call.signed || err instanceof SpendPolicyError) throw err;
112
+ const detail = err instanceof Error ? err.message : String(err);
113
+ throw fail(spend, call, void 0, detail, err);
114
+ }
115
+ if (call.signed && response.status >= 400) {
116
+ throw fail(spend, call, response.status, await boundedSnippet(response));
117
+ }
118
+ if (call.signed) paidResponses.set(response, { spend, call });
119
+ return response;
120
+ }
121
+ var SNIPPET_CHARS = 200;
122
+ var SNIPPET_MAX_BYTES = 1024;
123
+ var SNIPPET_TIMEOUT_MS = 1e3;
124
+ async function boundedSnippet(response, timeoutMs = SNIPPET_TIMEOUT_MS) {
125
+ let reader;
126
+ let timer;
127
+ try {
128
+ const body = response.clone().body;
129
+ if (!body) return "";
130
+ reader = body.getReader();
131
+ const r = reader;
132
+ const chunks = [];
133
+ let total = 0;
134
+ const read = (async () => {
135
+ while (total < SNIPPET_MAX_BYTES) {
136
+ const { done, value } = await r.read();
137
+ if (done || !value) break;
138
+ chunks.push(value);
139
+ total += value.byteLength;
140
+ }
141
+ })();
142
+ const timeout = new Promise((resolve) => {
143
+ timer = setTimeout(resolve, timeoutMs);
144
+ });
145
+ await Promise.race([read, timeout]);
146
+ const bytes = new Uint8Array(Math.min(total, SNIPPET_MAX_BYTES));
147
+ let at = 0;
148
+ for (const c of chunks) {
149
+ const n = Math.min(c.byteLength, bytes.byteLength - at);
150
+ bytes.set(c.subarray(0, n), at);
151
+ at += n;
152
+ }
153
+ return new TextDecoder().decode(bytes).slice(0, SNIPPET_CHARS);
154
+ } catch {
155
+ return "";
156
+ } finally {
157
+ if (timer) clearTimeout(timer);
158
+ reader?.cancel().catch(() => {
159
+ });
160
+ }
161
+ }
162
+ function fail(spend, call, status, detail, cause) {
163
+ const reference = call.reference ?? "mpp-unknown";
164
+ const why = status !== void 0 ? `HTTP ${status}` : "request failed";
165
+ if (spend && call.record) {
166
+ spend.annotateRecord(call.record, `signed, no completion (${why}) ref ${reference}`);
167
+ }
168
+ const error = new MppPaidNoCompletionError(
169
+ `MPP payment was signed and sent but no completion came back (${why}${detail ? `: ${detail}` : ""}). Not retrying, to avoid paying twice. Payment reference: ${reference}`,
170
+ reference,
171
+ status,
172
+ { cause }
173
+ );
174
+ if (call.credit) {
175
+ creditPaidNoCompletion(credits, error, { ...call.credit, reason: why });
176
+ }
177
+ return error;
178
+ }
179
+
180
+ // src/mpp-optional.ts
181
+ async function loadOptional(name, range, importer) {
182
+ try {
183
+ return await importer();
184
+ } catch (err) {
185
+ const code = err?.code;
186
+ const message = err instanceof Error ? err.message : String(err);
187
+ const missing = code === "ERR_MODULE_NOT_FOUND" || code === "MODULE_NOT_FOUND" || /Cannot find (package|module)/.test(message);
188
+ if (!missing || !message.includes(name)) throw err;
189
+ throw new Error(
190
+ `ClawRouter's MPP payment rail needs the optional package "${name}", which is not installed. Install it next to ClawRouter: npm install ${name}@${range}`,
191
+ { cause: err }
192
+ );
193
+ }
194
+ }
195
+ var MPPX_RANGE = "~0.17.0";
196
+ var SOLANA_MPP_RANGE = "~0.7.0";
197
+
198
+ // src/mpp-gate.ts
199
+ function paymentFactsFromMppChallenge(challenge) {
200
+ const request = challenge.request;
201
+ const details = request.methodDetails && typeof request.methodDetails === "object" ? request.methodDetails : {};
202
+ const rawNetwork = details.chainId ?? request.chainId ?? details.network ?? request.network;
203
+ return {
204
+ challengeId: challenge.id,
205
+ method: challenge.method,
206
+ intent: challenge.intent,
207
+ payTo: typeof request.recipient === "string" ? request.recipient : null,
208
+ network: typeof rawNetwork === "number" ? `eip155:${rawNetwork}` : typeof rawNetwork === "string" ? rawNetwork : null,
209
+ asset: typeof request.currency === "string" ? request.currency : null,
210
+ amountAtomic: typeof request.amount === "string" ? request.amount : null
211
+ };
212
+ }
213
+ function assertMppIntegration(integration, rail) {
214
+ if (integration?.twzrdMppIntegration !== true) {
215
+ throw new TypeError(`${rail} MPP requires the MPP integration marker; refusing unmarked hooks`);
216
+ }
217
+ if (typeof integration.gate !== "function") {
218
+ throw new TypeError(`${rail} MPP requires a pre-payment gate; refusing an ungated rail`);
219
+ }
220
+ if (typeof integration.onFulfillment !== "function" || typeof integration.flush !== "function") {
221
+ throw new TypeError(`${rail} MPP requires fulfillment and flush callbacks`);
222
+ }
223
+ }
224
+ function freezeRecursively(value) {
225
+ if (value !== null && typeof value === "object" && !Object.isFrozen(value)) {
226
+ Object.freeze(value);
227
+ for (const child of Object.values(value)) freezeRecursively(child);
228
+ }
229
+ return value;
230
+ }
231
+ var MppGateError = class extends Error {
232
+ code;
233
+ challengeId;
234
+ decisionId;
235
+ reasonCodes;
236
+ constructor(code, challengeId, message, decision) {
237
+ super(message);
238
+ this.name = "MppGateError";
239
+ this.code = code;
240
+ this.challengeId = challengeId;
241
+ this.decisionId = decision?.decisionId;
242
+ this.reasonCodes = decision?.reasonCodes;
243
+ }
244
+ };
245
+ async function evaluateMppGate(evaluator, challenge) {
246
+ if (!evaluator) return void 0;
247
+ const gateChallenge = freezeRecursively(structuredClone(challenge));
248
+ const raw = await evaluator(gateChallenge);
249
+ const disposition = raw && typeof raw === "object" ? raw.disposition : null;
250
+ if (!raw || typeof raw !== "object" || typeof disposition !== "string" || !["allow", "warn", "block"].includes(disposition)) {
251
+ throw new MppGateError(
252
+ "MPP_GATE_INVALID_RESULT",
253
+ challenge.id,
254
+ "MPP gate returned an invalid result; refusing payment"
255
+ );
256
+ }
257
+ const result = raw;
258
+ const decision = {
259
+ disposition: result.disposition,
260
+ ...typeof result.decisionId === "string" ? { decisionId: result.decisionId } : {},
261
+ ...typeof result.intentHash === "string" ? { intentHash: result.intentHash } : {},
262
+ ...Array.isArray(result.reasonCodes) && result.reasonCodes.every((x) => typeof x === "string") ? { reasonCodes: [...result.reasonCodes] } : {}
263
+ };
264
+ if (decision.disposition === "block") {
265
+ throw new MppGateError(
266
+ "MPP_GATE_BLOCKED",
267
+ challenge.id,
268
+ `MPP gate blocked challenge ${challenge.id}${decision.reasonCodes?.length ? `: ${decision.reasonCodes.join(",")}` : ""}`,
269
+ decision
270
+ );
271
+ }
272
+ return decision;
273
+ }
274
+
275
+ // src/mpp-fulfillment-record.ts
276
+ var MPP_FULFILLMENT_RECORD_SCHEMA = "twzrd.mpp.fulfillment.v1";
277
+ var MPP_FULFILLMENT_MAX_BODY_BYTES = 1024 * 1024;
278
+ function asRecord(value) {
279
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
280
+ }
281
+ function string(value) {
282
+ return typeof value === "string" && value.trim() ? value.trim() : null;
283
+ }
284
+ function isTimestamp(value) {
285
+ return typeof value === "string" && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/.test(value) && Number.isFinite(Date.parse(value));
286
+ }
287
+ function originOf(input) {
288
+ const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
289
+ try {
290
+ return new URL(url).origin;
291
+ } catch {
292
+ return null;
293
+ }
294
+ }
295
+ function decodeReceiptObject(value, challengeId, expectedIntent, expectedMethod) {
296
+ const receipt = asRecord(value);
297
+ if (!receipt || string(receipt.method) !== expectedMethod || !string(receipt.reference) || receipt.status !== "success" || !isTimestamp(receipt.timestamp)) {
298
+ return { state: "invalid_format", value: null };
299
+ }
300
+ if (typeof receipt.intent === "string" && receipt.intent !== expectedIntent || expectedIntent !== "charge" && receipt.intent !== expectedIntent || expectedIntent === "session" && (receipt.method !== "tempo" || receipt.challengeId !== challengeId || typeof receipt.channelId !== "string" || typeof receipt.acceptedCumulative !== "string" || typeof receipt.spent !== "string")) {
301
+ return { state: "invalid_format", value: null };
302
+ }
303
+ return { state: "valid_format", value: receipt };
304
+ }
305
+ function decodeReceipt(encoded, challengeId, expectedIntent, expectedMethod) {
306
+ if (!encoded) return { state: "missing", value: null };
307
+ try {
308
+ return decodeReceiptObject(
309
+ JSON.parse(Buffer.from(encoded, "base64url").toString("utf8")),
310
+ challengeId,
311
+ expectedIntent,
312
+ expectedMethod
313
+ );
314
+ } catch {
315
+ return { state: "invalid_format", value: null };
316
+ }
317
+ }
318
+ function createSseReceiptParser(challengeId, expectedIntent, expectedMethod) {
319
+ const decoder = new TextDecoder();
320
+ let buffer = "";
321
+ let eventName = "";
322
+ let data = [];
323
+ let totalBytes = 0;
324
+ let unavailable = false;
325
+ let receipt;
326
+ let sellerReportedModel = null;
327
+ const finishEvent = () => {
328
+ if (data.length && eventName === "payment-receipt" && !receipt) {
329
+ try {
330
+ receipt = decodeReceiptObject(
331
+ JSON.parse(data.join("\n")),
332
+ challengeId,
333
+ expectedIntent,
334
+ expectedMethod
335
+ );
336
+ } catch {
337
+ receipt = { state: "invalid_format", value: null };
338
+ }
339
+ } else if (data.length && (!eventName || eventName === "message") && !sellerReportedModel) {
340
+ try {
341
+ sellerReportedModel = string(asRecord(JSON.parse(data.join("\n")))?.model);
342
+ } catch {
343
+ }
344
+ }
345
+ eventName = "";
346
+ data = [];
347
+ };
348
+ const line = (raw) => {
349
+ const value = raw.endsWith("\r") ? raw.slice(0, -1) : raw;
350
+ if (!value) return finishEvent();
351
+ if (value.startsWith(":")) return;
352
+ const separator = value.indexOf(":");
353
+ const field = separator < 0 ? value : value.slice(0, separator);
354
+ const fieldValue = separator < 0 ? "" : value.slice(separator + 1).replace(/^ /, "");
355
+ if (field === "event") eventName = fieldValue;
356
+ if (field === "data" && data.join("\n").length + fieldValue.length <= 16 * 1024)
357
+ data.push(fieldValue);
358
+ };
359
+ return {
360
+ push(chunk) {
361
+ totalBytes += chunk.byteLength;
362
+ if (totalBytes > MPP_FULFILLMENT_MAX_BODY_BYTES) {
363
+ unavailable = true;
364
+ buffer = "";
365
+ data = [];
366
+ return;
367
+ }
368
+ if (unavailable) return;
369
+ buffer += decoder.decode(chunk, { stream: true });
370
+ for (; ; ) {
371
+ const newline = buffer.indexOf("\n");
372
+ if (newline < 0) break;
373
+ line(buffer.slice(0, newline));
374
+ buffer = buffer.slice(newline + 1);
375
+ }
376
+ if (buffer.length > 16 * 1024) {
377
+ unavailable = true;
378
+ buffer = "";
379
+ }
380
+ },
381
+ finish() {
382
+ if (!unavailable && !receipt) {
383
+ buffer += decoder.decode();
384
+ if (buffer) line(buffer);
385
+ finishEvent();
386
+ }
387
+ return {
388
+ receipt: receipt ?? { state: unavailable ? "unavailable" : "missing", value: null },
389
+ sellerReportedModel
390
+ };
391
+ }
392
+ };
393
+ }
394
+ async function readJsonCapped(body) {
395
+ if (!body) return void 0;
396
+ const reader = body.getReader();
397
+ const chunks = [];
398
+ let size = 0;
399
+ try {
400
+ for (; ; ) {
401
+ const { done, value } = await reader.read();
402
+ if (done) break;
403
+ size += value.byteLength;
404
+ if (size > MPP_FULFILLMENT_MAX_BODY_BYTES) {
405
+ await reader.cancel().catch(() => void 0);
406
+ return void 0;
407
+ }
408
+ chunks.push(value);
409
+ }
410
+ return JSON.parse(Buffer.concat(chunks).toString("utf8"));
411
+ } catch {
412
+ return void 0;
413
+ }
414
+ }
415
+ function bodyIsJson(headers) {
416
+ return (headers.get("content-type") ?? "").toLowerCase().includes("json");
417
+ }
418
+ async function readRequestBody(input, init) {
419
+ const headers = new Headers(input instanceof Request ? input.headers : void 0);
420
+ new Headers(init?.headers).forEach((value, key) => headers.set(key, value));
421
+ if (!bodyIsJson(headers)) return void 0;
422
+ if (typeof init?.body === "string") {
423
+ if (Buffer.byteLength(init.body, "utf8") > MPP_FULFILLMENT_MAX_BODY_BYTES) return void 0;
424
+ try {
425
+ return JSON.parse(init.body);
426
+ } catch {
427
+ return void 0;
428
+ }
429
+ }
430
+ if (input instanceof Request && init?.body === void 0) {
431
+ try {
432
+ return await readJsonCapped(input.clone().body);
433
+ } catch {
434
+ return void 0;
435
+ }
436
+ }
437
+ return void 0;
438
+ }
439
+ function wrappedResponse(response, body) {
440
+ const wrapped = new Response(body, {
441
+ status: response.status,
442
+ statusText: response.statusText,
443
+ headers: response.headers
444
+ });
445
+ Object.defineProperties(wrapped, {
446
+ url: { value: response.url },
447
+ redirected: { value: response.redirected },
448
+ type: { value: response.type }
449
+ });
450
+ return wrapped;
451
+ }
452
+ async function emitRecord(input, receipt, requestValue, responseValue, sink, sellerModelOverride) {
453
+ const req = asRecord(requestValue);
454
+ const res = asRecord(responseValue);
455
+ const requestedModel = string(req?.model);
456
+ const sellerReportedModel = string(sellerModelOverride) ?? string(res?.model);
457
+ const row = {
458
+ schema: MPP_FULFILLMENT_RECORD_SCHEMA,
459
+ at_unix_ms: Date.now(),
460
+ resource_origin: originOf(input.input),
461
+ http_status: input.response.status,
462
+ payment_method: input.payment.method,
463
+ payment_intent: input.payment.intent,
464
+ challenge_id: input.payment.challengeId,
465
+ paid_to: input.payment.payTo,
466
+ network: input.payment.network,
467
+ asset: input.payment.asset,
468
+ amount_atomic: input.payment.amountAtomic,
469
+ receipt_state: receipt.state,
470
+ receipt_status: string(receipt.value?.status),
471
+ receipt_method: string(receipt.value?.method),
472
+ receipt_reference: string(receipt.value?.reference),
473
+ receipt_timestamp: string(receipt.value?.timestamp),
474
+ requested_model: requestedModel,
475
+ seller_reported_model: sellerReportedModel,
476
+ seller_reported_model_matches_request: requestedModel && sellerReportedModel ? requestedModel.toLowerCase() === sellerReportedModel.toLowerCase() : null,
477
+ gate: input.gate ?? null
478
+ };
479
+ await sink(row);
480
+ }
481
+ function recordMppFulfillment(input, sink, onPending) {
482
+ const track = (pending) => {
483
+ onPending?.(pending);
484
+ void pending.catch(() => {
485
+ });
486
+ };
487
+ const receipt = decodeReceipt(
488
+ input.response.headers.get("payment-receipt"),
489
+ input.payment.challengeId,
490
+ input.payment.intent,
491
+ input.payment.method
492
+ );
493
+ const requestBody = readRequestBody(input.input, input.init);
494
+ const isSse = (input.response.headers.get("content-type") ?? "").toLowerCase().startsWith("text/event-stream");
495
+ if (isSse && receipt.state === "missing" && input.response.body && input.response.status !== 204 && input.response.status !== 205) {
496
+ const parser = createSseReceiptParser(
497
+ input.payment.challengeId,
498
+ input.payment.intent,
499
+ input.payment.method
500
+ );
501
+ const reader = input.response.body.getReader();
502
+ let resolveRecording;
503
+ let rejectRecording;
504
+ const recording2 = new Promise((resolve, reject) => {
505
+ resolveRecording = resolve;
506
+ rejectRecording = reject;
507
+ });
508
+ track(recording2);
509
+ let recordingStarted = false;
510
+ const finishRecord = (receipt2, sellerReportedModel) => {
511
+ if (recordingStarted) return;
512
+ recordingStarted = true;
513
+ const write = requestBody.then(
514
+ (request) => emitRecord(input, receipt2, request, void 0, sink, sellerReportedModel)
515
+ );
516
+ void write.then(resolveRecording, rejectRecording);
517
+ void write.catch(() => {
518
+ process.emitWarning("MPP fulfillment record failed; call payFetch.flush() to inspect", {
519
+ code: "TWZRD_MPP_FULFILLMENT_RECORD_FAILED"
520
+ });
521
+ });
522
+ };
523
+ const tapped = new ReadableStream({
524
+ async pull(controller) {
525
+ try {
526
+ const { done, value } = await reader.read();
527
+ if (done) {
528
+ const parsed = parser.finish();
529
+ finishRecord(parsed.receipt, parsed.sellerReportedModel);
530
+ controller.close();
531
+ reader.releaseLock();
532
+ return;
533
+ }
534
+ parser.push(value);
535
+ controller.enqueue(value);
536
+ } catch (error) {
537
+ finishRecord({ state: "unavailable", value: null }, null);
538
+ controller.error(error);
539
+ }
540
+ },
541
+ async cancel(reason) {
542
+ await reader.cancel(reason).catch(() => void 0);
543
+ const parsed = parser.finish();
544
+ const receipt2 = parsed.receipt.state === "missing" ? { state: "unavailable", value: null } : parsed.receipt;
545
+ finishRecord(receipt2, parsed.sellerReportedModel);
546
+ }
547
+ });
548
+ return wrappedResponse(input.response, tapped);
549
+ }
550
+ const responseBody = bodyIsJson(input.response.headers) ? readJsonCapped(input.response.clone().body) : Promise.resolve(void 0);
551
+ const recording = Promise.all([requestBody, responseBody]).then(
552
+ ([request, response]) => emitRecord(input, receipt, request, response, sink)
553
+ );
554
+ track(recording);
555
+ void recording.catch(() => {
556
+ process.emitWarning("MPP fulfillment record failed; call payFetch.flush() to inspect", {
557
+ code: "TWZRD_MPP_FULFILLMENT_RECORD_FAILED"
558
+ });
559
+ });
560
+ return input.response;
561
+ }
562
+
563
+ // src/mpp-payment-budget.ts
564
+ import { AsyncLocalStorage as AsyncLocalStorage2 } from "async_hooks";
565
+ var PAY_FETCH_BUDGET_CONTEXT = /* @__PURE__ */ Symbol.for("twzrd.clawrouter.pay-fetch-budget.v1");
566
+ var contextStorage = new AsyncLocalStorage2();
567
+ function paymentBudgetFromInit(init) {
568
+ const context = init?.[PAY_FETCH_BUDGET_CONTEXT];
569
+ if (!context || typeof context !== "object") return void 0;
570
+ const value = context;
571
+ return typeof value.assertPaymentAllowed === "function" && typeof value.recordSigned === "function" ? value : void 0;
572
+ }
573
+ function stripPaymentBudgetFromInit(init) {
574
+ if (!init) return void 0;
575
+ const clean = { ...init };
576
+ delete clean[PAY_FETCH_BUDGET_CONTEXT];
577
+ return clean;
578
+ }
579
+ function currentMppPaymentBudget() {
580
+ return contextStorage.getStore();
581
+ }
582
+ function runWithMppPaymentBudget(context, run) {
583
+ return contextStorage.run(context, run);
584
+ }
585
+
586
+ // src/mpp-fetch.ts
587
+ var mppxClientDeps = {
588
+ load: () => loadOptional("mppx", MPPX_RANGE, () => import("mppx/client"))
589
+ };
590
+ var DEFAULT_TEMPO_USD_ASSETS = /* @__PURE__ */ new Set([
591
+ "0x20c0000000000000000000000000000000000000",
592
+ "0x20c000000000000000000000b9537d11c60e8b50",
593
+ "0x20c000000000000000000000f37de3740adec032"
594
+ ]);
595
+ function quoteFromChallenge(challenge, usdAssets = DEFAULT_TEMPO_USD_ASSETS) {
596
+ const req = challenge.request ?? {};
597
+ const asset = typeof req.currency === "string" ? req.currency : void 0;
598
+ const chainId = req.methodDetails?.chainId;
599
+ const priceable = asset !== void 0 && usdAssets.has(asset.toLowerCase()) && typeof req.amount === "string";
600
+ return {
601
+ payTo: typeof req.recipient === "string" ? req.recipient : void 0,
602
+ network: typeof chainId === "number" ? `eip155:${chainId}` : void 0,
603
+ asset,
604
+ amount: priceable ? req.amount : void 0,
605
+ priceable
606
+ };
607
+ }
608
+ function guardMethod(method, control, usdAssets = DEFAULT_TEMPO_USD_ASSETS, gate) {
609
+ const signedOnce = /* @__PURE__ */ new Map();
610
+ const gateOnce = /* @__PURE__ */ new Map();
611
+ const reuseKey = (challenge) => JSON.stringify(challenge);
612
+ const guarded = {
613
+ createCredential: async (options) => {
614
+ const challenge = options.challenge;
615
+ markPaymentChallenge(paymentFactsFromMppChallenge(challenge));
616
+ const key = reuseKey(challenge);
617
+ let gatePending = gateOnce.get(key);
618
+ if (!gatePending) {
619
+ gatePending = evaluateMppGate(gate, challenge);
620
+ gateOnce.set(key, gatePending);
621
+ }
622
+ let gateDecision;
623
+ try {
624
+ gateDecision = await gatePending;
625
+ } catch (err) {
626
+ if (gateOnce.get(key) === gatePending) gateOnce.delete(key);
627
+ throw err;
628
+ }
629
+ if (gateOnce.get(key) === gatePending) gateOnce.delete(key);
630
+ markGateEvaluated(gateDecision);
631
+ const quote = quoteFromChallenge(challenge, usdAssets);
632
+ const budget = currentMppPaymentBudget();
633
+ const quoteMicros = quote.priceable && quote.amount && /^\d+$/.test(quote.amount) ? BigInt(quote.amount) : void 0;
634
+ budget?.assertPaymentAllowed(quoteMicros);
635
+ const call = currentPaidCall();
636
+ const credit = creditFromChallenge(challenge, call?.payer ?? "");
637
+ if (call && !credit)
638
+ throw new Error("challenge is missing amount or id, or payer is missing");
639
+ let attempt = signedOnce.get(key);
640
+ if (!attempt) {
641
+ const state = {
642
+ promise: Promise.resolve({ credential: "" }),
643
+ signingStarted: false
644
+ };
645
+ state.promise = (async () => {
646
+ let reservationId;
647
+ if (control) {
648
+ if (!quote.priceable && control.hasAmountLimits()) {
649
+ throw new SpendPolicyError2(
650
+ `MPP quote is in an asset or shape ClawRouter cannot price (${quote.asset ?? "none"}); refusing to pay against a configured spend limit`
651
+ );
652
+ }
653
+ reservationId = assertSpendPolicyAllows(control, quote);
654
+ }
655
+ let credential;
656
+ try {
657
+ state.signingStarted = true;
658
+ credential = await method.createCredential(options);
659
+ } catch (err) {
660
+ if (control && reservationId !== void 0) control.releaseReservation(reservationId);
661
+ throw err;
662
+ }
663
+ const record = control && reservationId !== void 0 ? control.settleReservation(reservationId, { action: "mpp tempo charge" }) : void 0;
664
+ budget?.recordSigned(quoteMicros);
665
+ return { credential, record };
666
+ })();
667
+ signedOnce.set(key, state);
668
+ attempt = state;
669
+ }
670
+ let signed;
671
+ try {
672
+ signed = await attempt.promise;
673
+ } catch (err) {
674
+ if (!attempt.signingStarted && signedOnce.get(key) === attempt) signedOnce.delete(key);
675
+ throw err;
676
+ }
677
+ markSigned(signed.credential, credit, signed.record);
678
+ return signed.credential;
679
+ }
680
+ };
681
+ return { ...method, ...guarded };
682
+ }
683
+ function createMppFetch(account, base = fetch, opts) {
684
+ assertMppIntegration(opts?.integration, "Tempo");
685
+ const integration = Object.freeze({ ...opts.integration });
686
+ const calls = new AsyncLocalStorage3();
687
+ const pendingFulfillments = /* @__PURE__ */ new Set();
688
+ const fulfillmentFailures = [];
689
+ const trackFulfillment = (task) => {
690
+ let tracked;
691
+ tracked = task.then(
692
+ () => {
693
+ pendingFulfillments.delete(tracked);
694
+ },
695
+ (error) => {
696
+ fulfillmentFailures.push(error);
697
+ pendingFulfillments.delete(tracked);
698
+ }
699
+ );
700
+ pendingFulfillments.add(tracked);
701
+ };
702
+ const recording = async (input, init) => {
703
+ const res = await base(input, init);
704
+ const call = calls.getStore();
705
+ if (call) call.last402 = res.status === 402 ? res.clone() : void 0;
706
+ const paid = currentPaidCall();
707
+ if (res.status !== 402 && paid?.signed && paid.payment) {
708
+ return recordMppFulfillment(
709
+ { input, init, response: res, payment: paid.payment, gate: paid.gate },
710
+ integration.onFulfillment,
711
+ trackFulfillment
712
+ );
713
+ }
714
+ return res;
715
+ };
716
+ const { spend, usdAssets } = opts;
717
+ let mppxPromise;
718
+ const getMppx = () => mppxPromise ??= mppxClientDeps.load().then(
719
+ ({ Mppx, tempo }) => Mppx.create({
720
+ methods: tempo({ account }).map((m) => guardMethod(m, spend, usdAssets, integration.gate)),
721
+ polyfill: false,
722
+ fetch: recording
723
+ })
724
+ );
725
+ const payFetch = async (input, init) => {
726
+ const call = {};
727
+ const mppx = await getMppx();
728
+ const budget = paymentBudgetFromInit(init);
729
+ const cleanInit = stripPaymentBudgetFromInit(init);
730
+ try {
731
+ return await runWithMppPaymentBudget(
732
+ budget,
733
+ () => calls.run(
734
+ call,
735
+ () => guardPaid(spend, () => mppx.fetch(input, cleanInit), account.address)
736
+ )
737
+ );
738
+ } catch (e) {
739
+ if (call.last402 && /No method found for challenges/.test(e.message)) {
740
+ return call.last402;
741
+ }
742
+ throw e;
743
+ }
744
+ };
745
+ payFetch.enforcesSpend = opts.spend !== void 0;
746
+ payFetch.deferStreamingHeaders = true;
747
+ payFetch.supportsNativeStreaming = true;
748
+ payFetch.onNoCompletion = rejectPaidNoCompletion;
749
+ payFetch.isPaidResponse = isPaidResponse;
750
+ payFetch.flush = async () => {
751
+ await Promise.all([...pendingFulfillments]);
752
+ const errors = fulfillmentFailures.splice(0);
753
+ try {
754
+ await integration.flush();
755
+ } catch (error) {
756
+ errors.push(error);
757
+ }
758
+ if (errors.length) throw new AggregateError(errors, "MPP fulfillment records did not flush");
759
+ };
760
+ return payFetch;
761
+ }
762
+
763
+ // src/mpp-solana-fetch.ts
764
+ import {
765
+ assertSpendPolicyAllows as assertSpendPolicyAllows2,
766
+ SpendPolicyError as SpendPolicyError3
767
+ } from "@twzrd1111/clawrouter";
768
+ var DEFAULT_SOLANA_USD_MINTS = /* @__PURE__ */ new Set([
769
+ "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
770
+ "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU",
771
+ "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB"
772
+ ]);
773
+ var CAIP2 = {
774
+ mainnet: "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp",
775
+ devnet: "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1",
776
+ localnet: "solana:localnet"
777
+ };
778
+ var solanaChargeDeps = {
779
+ load: () => loadOptional("@solana/mpp", SOLANA_MPP_RANGE, () => import("@solana/mpp/client"))
780
+ };
781
+ async function createSolanaMppFetch(opts) {
782
+ assertMppIntegration(opts?.integration, "Solana");
783
+ const integration = Object.freeze({ ...opts.integration });
784
+ const { charge, selectSolanaChargeChallengeFromResponse, resolveStablecoinMint } = await solanaChargeDeps.load();
785
+ const base = opts.fetch ?? fetch;
786
+ const usd = /* @__PURE__ */ new Set([...DEFAULT_SOLANA_USD_MINTS, ...opts.usdMints ?? []]);
787
+ const network = opts.network === "mainnet" ? "mainnet-beta" : opts.network;
788
+ const signedOnce = /* @__PURE__ */ new Map();
789
+ const gateOnce = /* @__PURE__ */ new Map();
790
+ const pendingFulfillments = /* @__PURE__ */ new Set();
791
+ const fulfillmentFailures = [];
792
+ const trackFulfillment = (task) => {
793
+ let tracked;
794
+ tracked = task.then(
795
+ () => {
796
+ pendingFulfillments.delete(tracked);
797
+ },
798
+ (error) => {
799
+ fulfillmentFailures.push(error);
800
+ pendingFulfillments.delete(tracked);
801
+ }
802
+ );
803
+ pendingFulfillments.add(tracked);
804
+ };
805
+ const payFetch = async (input, init) => {
806
+ const budget = paymentBudgetFromInit(init);
807
+ const cleanInit = stripPaymentBudgetFromInit(init);
808
+ return runWithMppPaymentBudget(
809
+ budget,
810
+ () => guardPaid(
811
+ opts.spend,
812
+ async () => {
813
+ const request = new Request(input, cleanInit);
814
+ const requestForRecord = request.clone();
815
+ const probe = await base(request.clone());
816
+ if (probe.status !== 402) return probe;
817
+ if (probe.redirected)
818
+ throw new SpendPolicyError3("Refusing a payment challenge reached through a redirect");
819
+ const challenge = selectSolanaChargeChallengeFromResponse(probe);
820
+ if (!challenge) return probe;
821
+ const challengeNetwork = challenge.request.methodDetails.network ?? "mainnet";
822
+ if (solanaNetworkId(challengeNetwork) !== CAIP2[opts.network]) {
823
+ throw new SpendPolicyError3(
824
+ "Solana challenge network does not match the configured network"
825
+ );
826
+ }
827
+ const credit = creditFromChallenge(challenge, opts.signer.address);
828
+ if (!credit) throw new Error("challenge is missing amount or id");
829
+ const quote = quoteFromSolanaChallenge(
830
+ {
831
+ request: {
832
+ ...challenge.request,
833
+ methodDetails: { ...challenge.request.methodDetails, network: challengeNetwork },
834
+ currency: resolveStablecoinMint(challenge.request.currency, network) ?? challenge.request.currency
835
+ }
836
+ },
837
+ usd
838
+ );
839
+ const payment = paymentFactsFromMppChallenge(challenge);
840
+ markPaymentChallenge(payment);
841
+ const key = JSON.stringify([
842
+ challenge.id,
843
+ challenge.realm,
844
+ challenge.method,
845
+ challenge.intent,
846
+ challenge.expires,
847
+ challenge.request
848
+ ]);
849
+ let gatePending = gateOnce.get(key);
850
+ if (!gatePending) {
851
+ gatePending = evaluateMppGate(integration.gate, challenge);
852
+ gateOnce.set(key, gatePending);
853
+ }
854
+ let gateDecision;
855
+ try {
856
+ gateDecision = await gatePending;
857
+ } catch (err) {
858
+ if (gateOnce.get(key) === gatePending) gateOnce.delete(key);
859
+ throw err;
860
+ }
861
+ if (gateOnce.get(key) === gatePending) gateOnce.delete(key);
862
+ markGateEvaluated(gateDecision);
863
+ const budgetContext = currentMppPaymentBudget();
864
+ const budgetQuoteMicros = quote.priceable && quote.amount && /^\d+$/.test(quote.amount) ? BigInt(quote.amount) : void 0;
865
+ budgetContext?.assertPaymentAllowed(budgetQuoteMicros);
866
+ let pending = signedOnce.get(key);
867
+ if (!pending) {
868
+ pending = (async () => {
869
+ const spend = opts.spend;
870
+ if (spend && !quote.priceable && spend.hasAmountLimits()) {
871
+ throw new SpendPolicyError3(
872
+ "MPP quote is in a mint ClawRouter cannot price; refusing to pay against a configured spend limit"
873
+ );
874
+ }
875
+ const reservationId = spend ? assertSpendPolicyAllows2(spend, quote) : void 0;
876
+ let signed = false;
877
+ let sessionSpendRecorded = false;
878
+ let record;
879
+ let payload = "";
880
+ try {
881
+ const method = charge({
882
+ signer: opts.signer,
883
+ rpcUrl: opts.rpcUrl,
884
+ expectedNetwork: challengeNetwork,
885
+ broadcast: false,
886
+ onProgress(event) {
887
+ if (event.type !== "signed") return;
888
+ signed = true;
889
+ payload = event.transaction;
890
+ if (!sessionSpendRecorded) {
891
+ budgetContext?.recordSigned(budgetQuoteMicros);
892
+ sessionSpendRecorded = true;
893
+ }
894
+ if (spend && reservationId !== void 0) {
895
+ record = spend.settleReservation(reservationId, {
896
+ action: "mpp solana charge"
897
+ });
898
+ }
899
+ markSigned(payload, credit, record);
900
+ }
901
+ });
902
+ const credential = await method.createCredential({ challenge });
903
+ if (!signed) {
904
+ payload = credential;
905
+ if (!sessionSpendRecorded) {
906
+ budgetContext?.recordSigned(budgetQuoteMicros);
907
+ sessionSpendRecorded = true;
908
+ }
909
+ if (spend && reservationId !== void 0) {
910
+ record = spend.settleReservation(reservationId, {
911
+ action: "mpp solana charge"
912
+ });
913
+ }
914
+ }
915
+ return {
916
+ credential,
917
+ payload,
918
+ record,
919
+ gate: gateDecision,
920
+ payment
921
+ };
922
+ } catch (err) {
923
+ if (signed) {
924
+ throw new MppPaidNoCompletionError(
925
+ "Solana payment was signed but credential construction failed; refusing to sign again",
926
+ paymentReference(payload),
927
+ void 0,
928
+ { cause: err }
929
+ );
930
+ }
931
+ if (!signed && spend && reservationId !== void 0)
932
+ spend.releaseReservation(reservationId);
933
+ throw err;
934
+ }
935
+ })();
936
+ signedOnce.set(key, pending);
937
+ }
938
+ let result;
939
+ try {
940
+ result = await pending;
941
+ } catch (err) {
942
+ if (!(err instanceof MppPaidNoCompletionError) && signedOnce.get(key) === pending)
943
+ signedOnce.delete(key);
944
+ throw err;
945
+ }
946
+ markGateEvaluated(result.gate);
947
+ if (result.payment) markPaymentChallenge(result.payment);
948
+ markSigned(result.payload, credit, result.record);
949
+ const headers = new Headers(request.headers);
950
+ headers.set("authorization", result.credential);
951
+ const response = await base(new Request(request, { headers }));
952
+ const paid = currentPaidCall();
953
+ if (response.status !== 402 && paid?.signed && paid.payment) {
954
+ return recordMppFulfillment(
955
+ { input: requestForRecord, response, payment: paid.payment, gate: paid.gate },
956
+ integration.onFulfillment,
957
+ trackFulfillment
958
+ );
959
+ }
960
+ return response;
961
+ },
962
+ opts.signer.address
963
+ )
964
+ );
965
+ };
966
+ payFetch.enforcesSpend = opts.spend !== void 0;
967
+ payFetch.deferStreamingHeaders = true;
968
+ payFetch.supportsNativeStreaming = true;
969
+ payFetch.onNoCompletion = rejectPaidNoCompletion;
970
+ payFetch.isPaidResponse = isPaidResponse;
971
+ payFetch.flush = async () => {
972
+ await Promise.all([...pendingFulfillments]);
973
+ const errors = fulfillmentFailures.splice(0);
974
+ try {
975
+ await integration.flush();
976
+ } catch (error) {
977
+ errors.push(error);
978
+ }
979
+ if (errors.length) throw new AggregateError(errors, "MPP fulfillment records did not flush");
980
+ };
981
+ return payFetch;
982
+ }
983
+ function solanaNetworkId(value) {
984
+ if (value === "devnet" || value === CAIP2.devnet) return CAIP2.devnet;
985
+ if (value === "mainnet" || value === "mainnet-beta" || value === CAIP2.mainnet)
986
+ return CAIP2.mainnet;
987
+ if (value === "localnet" || value === CAIP2.localnet) return CAIP2.localnet;
988
+ return value;
989
+ }
990
+ function quoteFromSolanaChallenge(challenge, usdMints = DEFAULT_SOLANA_USD_MINTS) {
991
+ const req = challenge.request ?? {};
992
+ const asset = typeof req.currency === "string" ? req.currency : void 0;
993
+ const rawNetwork = req.methodDetails?.network;
994
+ const priceable = asset !== void 0 && usdMints.has(asset) && typeof req.amount === "string";
995
+ return {
996
+ payTo: typeof req.recipient === "string" ? req.recipient : void 0,
997
+ network: typeof rawNetwork === "string" ? solanaNetworkId(rawNetwork) : void 0,
998
+ asset,
999
+ amount: priceable ? req.amount : void 0,
1000
+ priceable
1001
+ };
1002
+ }
1003
+
1004
+ // src/spent-proof.ts
1005
+ var ReplayRejectedError = class extends Error {
1006
+ constructor(networkId, txSignature, challengeId) {
1007
+ super("payment transaction or challenge already used on this network");
1008
+ this.networkId = networkId;
1009
+ this.txSignature = txSignature;
1010
+ this.challengeId = challengeId;
1011
+ this.name = "ReplayRejectedError";
1012
+ }
1013
+ networkId;
1014
+ txSignature;
1015
+ challengeId;
1016
+ status = 409;
1017
+ code = "proof_replayed";
1018
+ };
1019
+ var SpentProofLedger = class {
1020
+ transactions = /* @__PURE__ */ new Map();
1021
+ challenges = /* @__PURE__ */ new Map();
1022
+ claim(networkId, txSignature, challengeId) {
1023
+ if (!networkId || !txSignature || !challengeId)
1024
+ throw new TypeError("network id, transaction signature, and challenge id are required");
1025
+ const transactionKey = JSON.stringify([networkId, txSignature]);
1026
+ const challengeKey = JSON.stringify([networkId, challengeId]);
1027
+ if (this.transactions.has(transactionKey) || this.challenges.has(challengeKey)) return false;
1028
+ this.transactions.set(transactionKey, challengeId);
1029
+ this.challenges.set(challengeKey, txSignature);
1030
+ return true;
1031
+ }
1032
+ };
1033
+ async function fulfillPaidProof(opts) {
1034
+ const won = opts.ledger.claim(opts.networkId, opts.txSignature, opts.challengeId);
1035
+ if (!won) throw new ReplayRejectedError(opts.networkId, opts.txSignature, opts.challengeId);
1036
+ return opts.upstream();
1037
+ }
1038
+ export {
1039
+ CreditLedger,
1040
+ DEFAULT_SOLANA_USD_MINTS,
1041
+ DEFAULT_TEMPO_USD_ASSETS,
1042
+ MPP_FULFILLMENT_MAX_BODY_BYTES,
1043
+ MPP_FULFILLMENT_RECORD_SCHEMA,
1044
+ MppGateError,
1045
+ MppPaidNoCompletionError,
1046
+ ReplayRejectedError,
1047
+ SpentProofLedger,
1048
+ createMppFetch,
1049
+ createSolanaMppFetch,
1050
+ creditPaidNoCompletion,
1051
+ fulfillPaidProof,
1052
+ quoteFromChallenge,
1053
+ readCredits,
1054
+ readPaidCallEvidence
1055
+ };