@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.
- package/README.md +172 -15
- package/dist/index.d.ts +204 -27
- package/dist/index.js +966 -112
- package/package.json +16 -5
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/mpp-fetch.ts
|
|
2
|
-
import { AsyncLocalStorage as
|
|
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 ??
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
|
751
|
+
let gateDecision;
|
|
156
752
|
try {
|
|
157
|
-
|
|
753
|
+
gateDecision = await gatePending;
|
|
158
754
|
} catch (err) {
|
|
159
|
-
if (
|
|
755
|
+
if (gateOnce.get(key) === gatePending) gateOnce.delete(key);
|
|
160
756
|
throw err;
|
|
161
757
|
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
|
|
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 ??=
|
|
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
|
|
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
|
-
|
|
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
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
spend
|
|
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
|
-
|
|
312
|
-
|
|
1162
|
+
creditPaidNoCompletion,
|
|
1163
|
+
fulfillPaidProof,
|
|
1164
|
+
quoteFromChallenge,
|
|
1165
|
+
readCredits,
|
|
1166
|
+
readPaidCallEvidence
|
|
313
1167
|
};
|