mtok-relay 0.2.3 → 0.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/mtok-relay.mjs +183 -85
- package/package.json +1 -1
package/dist/mtok-relay.mjs
CHANGED
|
@@ -2157,7 +2157,7 @@ var secp256k1 = createCurve({
|
|
|
2157
2157
|
}, sha256);
|
|
2158
2158
|
|
|
2159
2159
|
// node_modules/viem/_esm/errors/version.js
|
|
2160
|
-
var version = "2.56.
|
|
2160
|
+
var version = "2.56.8";
|
|
2161
2161
|
|
|
2162
2162
|
// node_modules/viem/_esm/errors/base.js
|
|
2163
2163
|
var errorConfig = {
|
|
@@ -2423,7 +2423,7 @@ function hexToBigInt(hex, opts = {}) {
|
|
|
2423
2423
|
const value = BigInt(hex);
|
|
2424
2424
|
if (!signed)
|
|
2425
2425
|
return value;
|
|
2426
|
-
const size2 = (hex.length - 2) / 2;
|
|
2426
|
+
const size2 = Math.ceil((hex.length - 2) / 2);
|
|
2427
2427
|
const max = (1n << BigInt(size2) * 8n - 1n) - 1n;
|
|
2428
2428
|
if (value <= max)
|
|
2429
2429
|
return value;
|
|
@@ -4662,6 +4662,7 @@ function privateKeyToAccount(privateKey, options = {}) {
|
|
|
4662
4662
|
}
|
|
4663
4663
|
|
|
4664
4664
|
// src/config.mjs
|
|
4665
|
+
import { isIP } from "node:net";
|
|
4665
4666
|
var flag = (args, name) => {
|
|
4666
4667
|
const i = args.indexOf(name);
|
|
4667
4668
|
return i !== -1 ? args[i + 1] : null;
|
|
@@ -4687,6 +4688,15 @@ function readRelayConfig({ argv = process.argv.slice(2), env = process.env } = {
|
|
|
4687
4688
|
if (maxOutputTokens != null && (!Number.isFinite(maxOutputTokens) || maxOutputTokens < 1)) {
|
|
4688
4689
|
throw new Error("--max-output-tokens must be a positive integer");
|
|
4689
4690
|
}
|
|
4691
|
+
const trustedProxies = String(env.RELAY_TRUSTED_PROXIES ?? "").split(",").map((value) => value.trim()).filter(Boolean);
|
|
4692
|
+
if (trustedProxies.some((value) => !isIP(value) || value.includes("%"))) throw new Error("RELAY_TRUSTED_PROXIES must contain exact IP addresses");
|
|
4693
|
+
const clientIpHeader = String(env.RELAY_CLIENT_IP_HEADER ?? "cf-connecting-ip").toLowerCase();
|
|
4694
|
+
if (!/^[a-z0-9-]+$/.test(clientIpHeader)) throw new Error("RELAY_CLIENT_IP_HEADER must be a header name");
|
|
4695
|
+
const maxConcurrentRequests = Number(env.RELAY_MAX_CONCURRENT_REQUESTS ?? 64);
|
|
4696
|
+
const upstreamTimeoutMs = Number(env.RELAY_UPSTREAM_TIMEOUT_MS ?? 12e4);
|
|
4697
|
+
for (const [name, value] of Object.entries({ RELAY_MAX_CONCURRENT_REQUESTS: maxConcurrentRequests, RELAY_UPSTREAM_TIMEOUT_MS: upstreamTimeoutMs })) {
|
|
4698
|
+
if (!Number.isSafeInteger(value) || value < 1 || value > 2147483647) throw new Error(`${name} must be a positive finite integer`);
|
|
4699
|
+
}
|
|
4690
4700
|
if (!offerId) throw new Error("--offer <id> is required");
|
|
4691
4701
|
if (!model) throw new Error("--model <id> is required (the offer model you serve)");
|
|
4692
4702
|
if (!upstream) throw new Error("--upstream <url> is required");
|
|
@@ -4726,23 +4736,72 @@ function readRelayConfig({ argv = process.argv.slice(2), env = process.env } = {
|
|
|
4726
4736
|
upstreamKey,
|
|
4727
4737
|
settlementAddr,
|
|
4728
4738
|
payerDenylist,
|
|
4729
|
-
maxOutputTokens
|
|
4739
|
+
maxOutputTokens,
|
|
4740
|
+
trustedProxies,
|
|
4741
|
+
clientIpHeader,
|
|
4742
|
+
maxConcurrentRequests,
|
|
4743
|
+
upstreamTimeoutMs
|
|
4730
4744
|
};
|
|
4731
4745
|
}
|
|
4732
4746
|
|
|
4733
4747
|
// src/http.mjs
|
|
4734
4748
|
import http from "node:http";
|
|
4749
|
+
import { isIP as isIP2 } from "node:net";
|
|
4750
|
+
|
|
4751
|
+
// core/errors.js
|
|
4752
|
+
function apiError(status, code, message, details) {
|
|
4753
|
+
const err = new Error(message);
|
|
4754
|
+
err.status = status;
|
|
4755
|
+
err.code = code;
|
|
4756
|
+
if (details !== void 0) err.details = details;
|
|
4757
|
+
return err;
|
|
4758
|
+
}
|
|
4759
|
+
|
|
4760
|
+
// core/limiter.js
|
|
4761
|
+
function createLimiter({ windowMs = 6e4, max = 120, maxKeys = 1e4, now = () => Date.now() } = {}) {
|
|
4762
|
+
const windows = /* @__PURE__ */ new Map();
|
|
4763
|
+
return {
|
|
4764
|
+
check(key, scope = "api") {
|
|
4765
|
+
const id = `${scope}:${key}`;
|
|
4766
|
+
const t = now();
|
|
4767
|
+
let w = windows.get(id);
|
|
4768
|
+
if (!w || t - w.start >= windowMs) {
|
|
4769
|
+
w = { start: t, count: 0 };
|
|
4770
|
+
windows.delete(id);
|
|
4771
|
+
if (windows.size >= maxKeys) windows.delete(windows.keys().next().value);
|
|
4772
|
+
windows.set(id, w);
|
|
4773
|
+
}
|
|
4774
|
+
w.count += 1;
|
|
4775
|
+
if (w.count > max) {
|
|
4776
|
+
const retryInSeconds = Math.ceil((w.start + windowMs - t) / 1e3);
|
|
4777
|
+
throw apiError(429, "rate_limited", `Rate limit exceeded (${max}/${windowMs / 1e3}s). Retry in ~${retryInSeconds}s.`, { retryAfterSeconds: retryInSeconds });
|
|
4778
|
+
}
|
|
4779
|
+
},
|
|
4780
|
+
get size() {
|
|
4781
|
+
return windows.size;
|
|
4782
|
+
}
|
|
4783
|
+
};
|
|
4784
|
+
}
|
|
4785
|
+
|
|
4786
|
+
// src/http.mjs
|
|
4735
4787
|
var MAX_BODY_BYTES = 256e3;
|
|
4736
|
-
function readBody(req, { maxBytes = MAX_BODY_BYTES } = {}) {
|
|
4788
|
+
function readBody(req, { maxBytes = MAX_BODY_BYTES, timeoutMs = 1e4 } = {}) {
|
|
4737
4789
|
return new Promise((resolve, reject) => {
|
|
4738
4790
|
const chunks = [];
|
|
4739
4791
|
let bytes = 0;
|
|
4740
4792
|
let settled = false;
|
|
4793
|
+
const fail = (error) => {
|
|
4794
|
+
if (settled) return;
|
|
4795
|
+
settled = true;
|
|
4796
|
+
clearTimeout(timer);
|
|
4797
|
+
chunks.length = 0;
|
|
4798
|
+
reject(error);
|
|
4799
|
+
};
|
|
4800
|
+
const timer = setTimeout(() => fail(Object.assign(new Error("body_timeout"), { code: "body_timeout" })), timeoutMs);
|
|
4741
4801
|
req.on("data", (d) => {
|
|
4742
4802
|
bytes += d.length;
|
|
4743
4803
|
if (bytes > maxBytes && !settled) {
|
|
4744
|
-
|
|
4745
|
-
reject(Object.assign(new Error("body_too_large"), { code: "body_too_large" }));
|
|
4804
|
+
fail(Object.assign(new Error("body_too_large"), { code: "body_too_large" }));
|
|
4746
4805
|
req.resume();
|
|
4747
4806
|
return;
|
|
4748
4807
|
}
|
|
@@ -4752,6 +4811,7 @@ function readBody(req, { maxBytes = MAX_BODY_BYTES } = {}) {
|
|
|
4752
4811
|
req.on("end", () => {
|
|
4753
4812
|
if (settled) return;
|
|
4754
4813
|
settled = true;
|
|
4814
|
+
clearTimeout(timer);
|
|
4755
4815
|
const raw = Buffer.concat(chunks).toString("utf8");
|
|
4756
4816
|
try {
|
|
4757
4817
|
resolve(JSON.parse(raw || "{}"));
|
|
@@ -4759,54 +4819,82 @@ function readBody(req, { maxBytes = MAX_BODY_BYTES } = {}) {
|
|
|
4759
4819
|
resolve({});
|
|
4760
4820
|
}
|
|
4761
4821
|
});
|
|
4762
|
-
req.on("error",
|
|
4763
|
-
|
|
4764
|
-
settled = true;
|
|
4765
|
-
reject(e);
|
|
4766
|
-
});
|
|
4822
|
+
req.on("error", fail);
|
|
4823
|
+
req.on("aborted", () => fail(new Error("body_aborted")));
|
|
4767
4824
|
});
|
|
4768
4825
|
}
|
|
4769
4826
|
function send(res, status, body) {
|
|
4827
|
+
if (res.destroyed || res.writableEnded) return;
|
|
4770
4828
|
const payload = JSON.stringify(body);
|
|
4771
4829
|
res.writeHead(status, { "content-type": "application/json", "content-length": Buffer.byteLength(payload) });
|
|
4772
4830
|
res.end(payload);
|
|
4773
4831
|
}
|
|
4832
|
+
function canonicalIp(value) {
|
|
4833
|
+
if (typeof value !== "string" || value.includes("%") || !isIP2(value)) return null;
|
|
4834
|
+
if (isIP2(value) === 4) return value;
|
|
4835
|
+
const canonical = new URL(`http://[${value}]/`).hostname.slice(1, -1);
|
|
4836
|
+
const mapped = /^::ffff:([\da-f]+):([\da-f]+)$/.exec(canonical);
|
|
4837
|
+
if (mapped) return mapped.slice(1).flatMap((word) => [parseInt(word, 16) >> 8, parseInt(word, 16) & 255]).join(".");
|
|
4838
|
+
return canonical;
|
|
4839
|
+
}
|
|
4840
|
+
function clientRateKey(req, trustedProxies = /* @__PURE__ */ new Set(), header = "cf-connecting-ip") {
|
|
4841
|
+
const peer = canonicalIp(req.socket.remoteAddress) ?? "unknown";
|
|
4842
|
+
const forwarded = trustedProxies.has(peer) ? canonicalIp(req.headers[header]) : null;
|
|
4843
|
+
const address = forwarded ?? peer;
|
|
4844
|
+
if (!address.includes(":")) return address;
|
|
4845
|
+
const [left, right] = address.split("::");
|
|
4846
|
+
const start = left ? left.split(":") : [];
|
|
4847
|
+
const end = right ? right.split(":") : [];
|
|
4848
|
+
const words = right === void 0 ? start : [...start, ...Array(8 - start.length - end.length).fill("0"), ...end];
|
|
4849
|
+
return words.slice(0, 4).map((word) => word.padStart(4, "0")).join(":") + "::/64";
|
|
4850
|
+
}
|
|
4774
4851
|
function startRelayServer({ config, handleDraw }) {
|
|
4775
|
-
const
|
|
4776
|
-
const
|
|
4777
|
-
const
|
|
4778
|
-
const
|
|
4779
|
-
|
|
4780
|
-
|
|
4781
|
-
const
|
|
4782
|
-
|
|
4783
|
-
|
|
4784
|
-
|
|
4785
|
-
|
|
4786
|
-
|
|
4787
|
-
for (const [k, v] of windows) if (now - v.start >= windowMs) windows.delete(k);
|
|
4788
|
-
}
|
|
4789
|
-
}
|
|
4790
|
-
w.count += 1;
|
|
4791
|
-
return w.count <= maxPerMinute;
|
|
4792
|
-
};
|
|
4793
|
-
const server = http.createServer(async (req, res) => {
|
|
4852
|
+
const callers = createLimiter({ max: config.maxRequestsPerMinute ?? 120 });
|
|
4853
|
+
const aggregate = createLimiter({ max: config.maxTotalRequestsPerMinute ?? 1200 });
|
|
4854
|
+
const trustedProxies = new Set((config.trustedProxies ?? []).map(canonicalIp).filter(Boolean));
|
|
4855
|
+
const maxConcurrent = config.maxConcurrentRequests ?? 64;
|
|
4856
|
+
let active = 0;
|
|
4857
|
+
const server = http.createServer({ headersTimeout: 1e4, requestTimeout: 15e3, connectionsCheckingInterval: 1e3 }, async (req, res) => {
|
|
4858
|
+
const refuse = (status, error) => {
|
|
4859
|
+
if (res.headersSent || res.destroyed) return res.destroy();
|
|
4860
|
+
res.setHeader("connection", "close");
|
|
4861
|
+
res.once("finish", () => req.destroy());
|
|
4862
|
+
send(res, status, { error });
|
|
4863
|
+
};
|
|
4794
4864
|
if (req.method !== "POST" || req.url !== "/chunk") {
|
|
4795
|
-
return
|
|
4865
|
+
return refuse(404, "not found");
|
|
4796
4866
|
}
|
|
4797
|
-
if (!checkRate(req)) return send(res, 429, { error: "rate_limited" });
|
|
4798
|
-
let body;
|
|
4799
4867
|
try {
|
|
4800
|
-
|
|
4801
|
-
|
|
4802
|
-
|
|
4803
|
-
|
|
4868
|
+
callers.check(clientRateKey(req, trustedProxies, config.clientIpHeader ?? "cf-connecting-ip"));
|
|
4869
|
+
aggregate.check("all");
|
|
4870
|
+
} catch {
|
|
4871
|
+
res.setHeader("retry-after", "60");
|
|
4872
|
+
return refuse(429, "rate_limited");
|
|
4804
4873
|
}
|
|
4805
|
-
if (
|
|
4806
|
-
|
|
4874
|
+
if (active >= maxConcurrent) return refuse(503, "relay_busy");
|
|
4875
|
+
active++;
|
|
4876
|
+
try {
|
|
4877
|
+
let body;
|
|
4878
|
+
try {
|
|
4879
|
+
body = await readBody(req, { timeoutMs: config.bodyTimeoutMs ?? 1e4 });
|
|
4880
|
+
} catch (e) {
|
|
4881
|
+
if (e?.code === "body_too_large") return refuse(413, "body_too_large");
|
|
4882
|
+
if (e?.code === "body_timeout") return refuse(408, "relay_timeout");
|
|
4883
|
+
return refuse(400, "bad body");
|
|
4884
|
+
}
|
|
4885
|
+
if (body?.request == null) {
|
|
4886
|
+
return send(res, 400, { error: "bad_request", detail: "need a DRAW (request); the legacy FUND lane is retired, pay per draw on-chain" });
|
|
4887
|
+
}
|
|
4888
|
+
await handleDraw(body, res);
|
|
4889
|
+
} catch (error) {
|
|
4890
|
+
(config.log ?? console).error?.(`mtok relay: request failed (${error.message})`);
|
|
4891
|
+
refuse(503, "relay_unavailable");
|
|
4892
|
+
} finally {
|
|
4893
|
+
active--;
|
|
4807
4894
|
}
|
|
4808
|
-
return handleDraw(body, res);
|
|
4809
4895
|
});
|
|
4896
|
+
server.maxConnections = maxConcurrent * 2;
|
|
4897
|
+
server.maxRequestsPerSocket = 100;
|
|
4810
4898
|
server.listen(config.port, () => {
|
|
4811
4899
|
console.log(`mtok-relay: listening on port ${config.port} offer=${config.offerId} model=${config.model} upstream=${config.upstream} api=${config.apiBase} settlement=${config.settlementAddr}`);
|
|
4812
4900
|
});
|
|
@@ -4977,15 +5065,6 @@ function createRedemptionStore({ file = null, retentionMs = DEFAULT_RETENTION_MS
|
|
|
4977
5065
|
};
|
|
4978
5066
|
}
|
|
4979
5067
|
|
|
4980
|
-
// core/errors.js
|
|
4981
|
-
function apiError(status, code, message, details) {
|
|
4982
|
-
const err = new Error(message);
|
|
4983
|
-
err.status = status;
|
|
4984
|
-
err.code = code;
|
|
4985
|
-
if (details !== void 0) err.details = details;
|
|
4986
|
-
return err;
|
|
4987
|
-
}
|
|
4988
|
-
|
|
4989
5068
|
// core/onchain.js
|
|
4990
5069
|
var TRANSFER_TOPIC = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
|
|
4991
5070
|
var DRAW_PAID_TOPIC = "0xb0243f80521d0dccd159389597aba96047e60ba5d7a9df12b67e5cb75230ac41";
|
|
@@ -5056,8 +5135,9 @@ function createOnchainVerifier({
|
|
|
5056
5135
|
receiptRetries = 3,
|
|
5057
5136
|
receiptRetryMs = 700,
|
|
5058
5137
|
sleepImpl = (ms) => new Promise((r) => setTimeout(r, ms)),
|
|
5059
|
-
nowMs = () => Date.now()
|
|
5138
|
+
nowMs = () => Date.now(),
|
|
5060
5139
|
// injectable clock for the optional draw-age guard (verifyDrawPaid maxPaidAgeMs).
|
|
5140
|
+
rpcTimeoutMs = 5e3
|
|
5061
5141
|
} = {}) {
|
|
5062
5142
|
const urls = (rpcUrls?.length ? rpcUrls : String(rpcUrl || "").split(",")).map((s) => String(s).trim()).filter(Boolean);
|
|
5063
5143
|
const configured = Boolean(urls.length && usdcAddress);
|
|
@@ -5066,7 +5146,8 @@ function createOnchainVerifier({
|
|
|
5066
5146
|
const res = await fetchImpl(url, {
|
|
5067
5147
|
method: "POST",
|
|
5068
5148
|
headers: { "content-type": "application/json" },
|
|
5069
|
-
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, params })
|
|
5149
|
+
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, params }),
|
|
5150
|
+
signal: AbortSignal.timeout(rpcTimeoutMs)
|
|
5070
5151
|
});
|
|
5071
5152
|
if (!res.ok) throw apiError(502, "rpc_error", `chain RPC returned ${res.status}`);
|
|
5072
5153
|
const body = await res.json();
|
|
@@ -5091,13 +5172,16 @@ function createOnchainVerifier({
|
|
|
5091
5172
|
async function assertChain() {
|
|
5092
5173
|
if (!chainPinned) return true;
|
|
5093
5174
|
if (chainOkUrls.size) return true;
|
|
5175
|
+
let timeout;
|
|
5094
5176
|
for (const url of urls) {
|
|
5095
5177
|
try {
|
|
5096
5178
|
const hex = await rpcOn(url, "eth_chainId", []);
|
|
5097
5179
|
if (typeof hex === "string" && parseInt(hex, 16) === expChain) chainOkUrls.add(url);
|
|
5098
|
-
} catch {
|
|
5180
|
+
} catch (error) {
|
|
5181
|
+
if (error.name === "TimeoutError") timeout = error;
|
|
5099
5182
|
}
|
|
5100
5183
|
}
|
|
5184
|
+
if (!chainOkUrls.size && timeout) throw timeout;
|
|
5101
5185
|
return chainOkUrls.size > 0;
|
|
5102
5186
|
}
|
|
5103
5187
|
async function fetchReceipt(txHash) {
|
|
@@ -5212,23 +5296,27 @@ function createOnchainVerifier({
|
|
|
5212
5296
|
if (BigInt(event.sellerUsdAtomic) < BigInt(minSellerAtomic)) return { ok: false, reason: "amount_too_low" };
|
|
5213
5297
|
const maxAge = Number(maxPaidAgeMs);
|
|
5214
5298
|
if (Number.isFinite(maxAge) && maxAge > 0) {
|
|
5215
|
-
const bn = matchedLog?.blockNumber;
|
|
5299
|
+
const bn = matchedLog?.blockNumber ?? got.receipt.blockNumber;
|
|
5216
5300
|
let paidAtMs = null;
|
|
5217
5301
|
if (bn != null) {
|
|
5218
|
-
const blockTag = typeof bn === "string" && bn.startsWith("0x") ? bn : "0x" + BigInt(bn).toString(16);
|
|
5219
5302
|
for (let i = 0; i <= receiptRetries; i++) {
|
|
5220
5303
|
try {
|
|
5304
|
+
const blockTag = "0x" + BigInt(bn).toString(16);
|
|
5221
5305
|
const block = await rpc("eth_getBlockByNumber", [blockTag, false]);
|
|
5222
5306
|
if (block?.timestamp != null) {
|
|
5223
|
-
|
|
5224
|
-
|
|
5307
|
+
const timestamp = Number(BigInt(block.timestamp)) * 1e3;
|
|
5308
|
+
if (Number.isSafeInteger(timestamp) && timestamp >= 0 && timestamp <= nowMs() + 6e4) {
|
|
5309
|
+
paidAtMs = timestamp;
|
|
5310
|
+
break;
|
|
5311
|
+
}
|
|
5225
5312
|
}
|
|
5226
5313
|
} catch {
|
|
5227
5314
|
}
|
|
5228
5315
|
if (i < receiptRetries) await sleepImpl(receiptRetryMs);
|
|
5229
5316
|
}
|
|
5230
5317
|
}
|
|
5231
|
-
if (paidAtMs
|
|
5318
|
+
if (paidAtMs == null) return { ok: false, reason: "payment_age_unavailable" };
|
|
5319
|
+
if (nowMs() - paidAtMs > maxAge) return { ok: false, reason: "payment_too_old" };
|
|
5232
5320
|
}
|
|
5233
5321
|
const consumed = /* @__PURE__ */ new Set();
|
|
5234
5322
|
let sellerTransfer = null;
|
|
@@ -5423,6 +5511,17 @@ function createServeCore({
|
|
|
5423
5511
|
return { status: 402, body: { error: "contract_mode_required", detail: "this relay only serves contract-mode draws; the platform is not reporting a dripContractAddress" } };
|
|
5424
5512
|
}
|
|
5425
5513
|
if (!drawPaidTxHash) return { status: 402, body: { error: "draw_payment_required", detail: "contract mode requires drawPaidTxHash before upstream delivery" } };
|
|
5514
|
+
let storedKey = cacheKey;
|
|
5515
|
+
let redemptionState = await redemption.state(storedKey);
|
|
5516
|
+
if (!redemptionState && oldLegacyKey) {
|
|
5517
|
+
const oldLegacyState = await redemption.state(oldLegacyKey);
|
|
5518
|
+
if (oldLegacyState) {
|
|
5519
|
+
storedKey = oldLegacyKey;
|
|
5520
|
+
redemptionState = oldLegacyState;
|
|
5521
|
+
} else {
|
|
5522
|
+
redemptionState = await redemption.state(cacheKey);
|
|
5523
|
+
}
|
|
5524
|
+
}
|
|
5426
5525
|
let paid;
|
|
5427
5526
|
try {
|
|
5428
5527
|
paid = await verifier.verifyDrawPaid(drawPaidTxHash, {
|
|
@@ -5437,16 +5536,15 @@ function createServeCore({
|
|
|
5437
5536
|
requestHash,
|
|
5438
5537
|
sellerWallet,
|
|
5439
5538
|
feeRecipient: currentFeeRecipient,
|
|
5440
|
-
//
|
|
5441
|
-
//
|
|
5442
|
-
|
|
5443
|
-
// skip-on-unreadable-block residual is named in redemption.mjs). An
|
|
5444
|
-
// honest retry is seconds-to-minutes old, never days.
|
|
5445
|
-
maxPaidAgeMs: redemption.retentionMs
|
|
5539
|
+
// A known completion spends no new inference. New claims need a verified
|
|
5540
|
+
// age because both payload and claim markers expire after retention.
|
|
5541
|
+
maxPaidAgeMs: redemptionState === "complete" ? void 0 : redemption.retentionMs
|
|
5446
5542
|
});
|
|
5447
5543
|
} catch (e) {
|
|
5544
|
+
if (e.name === "TimeoutError") return { status: 503, body: { error: "relay_timeout", _bookingId: bookingId } };
|
|
5448
5545
|
return { status: 402, body: { error: "payment_unverified", detail: e.message } };
|
|
5449
5546
|
}
|
|
5547
|
+
if (paid?.reason === "payment_age_unavailable") return { status: 503, body: { error: "payment_age_unavailable", detail: "payment age could not be verified; retry this same paid draw", _bookingId: bookingId } };
|
|
5450
5548
|
if (!paid?.ok) return { status: 402, body: { error: "payment_unverified", detail: paid?.reason || "unknown" } };
|
|
5451
5549
|
const expectedFee = configuredFeeAtomic({
|
|
5452
5550
|
sellerUsdAtomic: paid.event.sellerUsdAtomic,
|
|
@@ -5465,17 +5563,6 @@ function createServeCore({
|
|
|
5465
5563
|
return { status: 403, body: { error: "payer_denied", detail: "payer screening failed: " + e.message } };
|
|
5466
5564
|
}
|
|
5467
5565
|
}
|
|
5468
|
-
let storedKey = cacheKey;
|
|
5469
|
-
let redemptionState = await redemption.state(storedKey);
|
|
5470
|
-
if (!redemptionState && oldLegacyKey) {
|
|
5471
|
-
const oldLegacyState = await redemption.state(oldLegacyKey);
|
|
5472
|
-
if (oldLegacyState) {
|
|
5473
|
-
storedKey = oldLegacyKey;
|
|
5474
|
-
redemptionState = oldLegacyState;
|
|
5475
|
-
} else {
|
|
5476
|
-
redemptionState = await redemption.state(cacheKey);
|
|
5477
|
-
}
|
|
5478
|
-
}
|
|
5479
5566
|
if (redemptionState === "complete") return { status: 200, body: await redemption.get(storedKey) };
|
|
5480
5567
|
if (redemptionState === "pending") {
|
|
5481
5568
|
return { status: 409, body: { error: "draw_pending", detail: "this paid draw was already claimed; refusing to run upstream again", _bookingId: bookingId } };
|
|
@@ -5506,6 +5593,7 @@ function createServeCore({
|
|
|
5506
5593
|
try {
|
|
5507
5594
|
completion = await upstream(safeRequest);
|
|
5508
5595
|
} catch (e) {
|
|
5596
|
+
if (e.name === "TimeoutError") return { status: 503, body: { error: "relay_timeout", _bookingId: bookingId } };
|
|
5509
5597
|
return { status: 502, body: { error: "upstream_error", detail: e.message } };
|
|
5510
5598
|
}
|
|
5511
5599
|
try {
|
|
@@ -5530,7 +5618,7 @@ function createServeCore({
|
|
|
5530
5618
|
}
|
|
5531
5619
|
|
|
5532
5620
|
// bridge/bridge.mjs
|
|
5533
|
-
function httpUpstream({ baseUrl, key }) {
|
|
5621
|
+
function httpUpstream({ baseUrl, key, timeoutMs }) {
|
|
5534
5622
|
const url = String(baseUrl || "").replace(/\/$/, "") + "/chat/completions";
|
|
5535
5623
|
return async (payload) => {
|
|
5536
5624
|
const res = await fetch(url, {
|
|
@@ -5539,7 +5627,8 @@ function httpUpstream({ baseUrl, key }) {
|
|
|
5539
5627
|
"content-type": "application/json",
|
|
5540
5628
|
...key ? { authorization: `Bearer ${key}` } : {}
|
|
5541
5629
|
},
|
|
5542
|
-
body: JSON.stringify(payload)
|
|
5630
|
+
body: JSON.stringify(payload),
|
|
5631
|
+
...timeoutMs == null ? {} : { signal: AbortSignal.timeout(timeoutMs) }
|
|
5543
5632
|
});
|
|
5544
5633
|
const text = await res.text();
|
|
5545
5634
|
let json;
|
|
@@ -5593,7 +5682,7 @@ async function createRelayRuntime(config) {
|
|
|
5593
5682
|
if (!verifier.configured) throw new Error("onchain verifier not configured (missing usdcAddress in /api/config)");
|
|
5594
5683
|
const payerDenylist = new Set((config.payerDenylist ?? []).map((a) => String(a).trim().toLowerCase()).filter(Boolean));
|
|
5595
5684
|
const screenPayer = typeof config.screenPayer === "function" ? config.screenPayer : null;
|
|
5596
|
-
const upstream = httpUpstream({ baseUrl: config.upstream + "/v1", key: config.upstreamKey });
|
|
5685
|
+
const upstream = httpUpstream({ baseUrl: config.upstream + "/v1", key: config.upstreamKey, timeoutMs: config.upstreamTimeoutMs ?? 12e4 });
|
|
5597
5686
|
const payerDenied = async (payer) => {
|
|
5598
5687
|
if (!payer) return false;
|
|
5599
5688
|
if (payerDenylist.has(payer)) return true;
|
|
@@ -5639,18 +5728,27 @@ async function createRelayRuntime(config) {
|
|
|
5639
5728
|
if (drawLocks.get(bookingId) === tail) drawLocks.delete(bookingId);
|
|
5640
5729
|
}
|
|
5641
5730
|
};
|
|
5642
|
-
|
|
5643
|
-
|
|
5644
|
-
|
|
5645
|
-
|
|
5646
|
-
|
|
5731
|
+
let active = 0;
|
|
5732
|
+
const handleDraw = async (body, res) => {
|
|
5733
|
+
if (active >= (config.maxConcurrentRequests ?? 64)) return send(res, 503, { error: "relay_busy" });
|
|
5734
|
+
active++;
|
|
5735
|
+
try {
|
|
5736
|
+
return await withBookingLock(String(body?.bookingId ?? ""), async () => {
|
|
5737
|
+
await refreshPlatformFeeIfStale();
|
|
5738
|
+
let out = await core.serve(body);
|
|
5739
|
+
if (out.status === 402 && out.body?.detail === "fee_amount_too_low" && await refreshPlatformFee()) {
|
|
5740
|
+
out = await core.serve(body);
|
|
5741
|
+
}
|
|
5742
|
+
return send(res, out.status, out.body);
|
|
5743
|
+
});
|
|
5744
|
+
} finally {
|
|
5745
|
+
active--;
|
|
5647
5746
|
}
|
|
5648
|
-
|
|
5649
|
-
});
|
|
5747
|
+
};
|
|
5650
5748
|
return { handleDraw };
|
|
5651
5749
|
}
|
|
5652
5750
|
async function fetchPlatformConfig(config) {
|
|
5653
|
-
const r = await fetch(config.apiBase + "/api/config");
|
|
5751
|
+
const r = await fetch(config.apiBase + "/api/config", { signal: AbortSignal.timeout(5e3) });
|
|
5654
5752
|
if (!r.ok) throw new Error("config fetch failed: " + r.status);
|
|
5655
5753
|
const body = await r.json();
|
|
5656
5754
|
return {
|
package/package.json
CHANGED