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