@ordspv/gateway 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +8 -2
- package/dist/index.js +39 -8
- package/dist/index.js.map +1 -1
- package/dist/ratelimit.d.ts +6 -3
- package/package.json +3 -3
package/dist/index.d.ts
CHANGED
|
@@ -36,8 +36,14 @@ export interface GatewayOptions {
|
|
|
36
36
|
rateLimitPerSec?: number;
|
|
37
37
|
/** burst size per IP (default 40) */
|
|
38
38
|
rateBurst?: number;
|
|
39
|
-
/**
|
|
40
|
-
|
|
39
|
+
/**
|
|
40
|
+
* Behind a load balancer / CDN: number of TRUSTED proxy hops in front of
|
|
41
|
+
* this gateway (true = 1). The client IP is taken from the right of
|
|
42
|
+
* X-Forwarded-For — the entries appended by your own proxies — never from
|
|
43
|
+
* the client-controlled left end, and must parse as an IP address
|
|
44
|
+
* (otherwise the socket address is used).
|
|
45
|
+
*/
|
|
46
|
+
trustProxy?: boolean | number;
|
|
41
47
|
/** structured log sink; false silences (default). CLI wires console.log. */
|
|
42
48
|
log?: ((line: Record<string, unknown>) => void) | false;
|
|
43
49
|
}
|
package/dist/index.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
// packages/gateway/src/index.ts
|
|
2
2
|
import { createServer } from "http";
|
|
3
|
+
import { isIP } from "net";
|
|
3
4
|
import { Readable } from "stream";
|
|
4
5
|
import { isInscriptionId, parseInscriptionId } from "@ordspv/core";
|
|
5
6
|
import {
|
|
6
7
|
buildProofBundle,
|
|
7
8
|
EsploraBackend,
|
|
8
9
|
OrdResolver,
|
|
10
|
+
readBodyCapped,
|
|
9
11
|
toResponse
|
|
10
12
|
} from "@ordspv/fetch";
|
|
11
13
|
|
|
@@ -154,16 +156,19 @@ ${g.name} ${g.read()}`);
|
|
|
154
156
|
};
|
|
155
157
|
|
|
156
158
|
// packages/gateway/src/ratelimit.ts
|
|
159
|
+
var DEFAULT_MAX_TRACKED_KEYS = 5e4;
|
|
157
160
|
var TokenBucketLimiter = class {
|
|
158
|
-
constructor(ratePerSec, burst, now = () => Date.now()) {
|
|
161
|
+
constructor(ratePerSec, burst, now = () => Date.now(), maxTrackedKeys = DEFAULT_MAX_TRACKED_KEYS) {
|
|
159
162
|
this.ratePerSec = ratePerSec;
|
|
160
163
|
this.burst = burst;
|
|
161
164
|
this.now = now;
|
|
165
|
+
this.maxTrackedKeys = maxTrackedKeys;
|
|
162
166
|
this.lastSweep = this.now();
|
|
163
167
|
}
|
|
164
168
|
ratePerSec;
|
|
165
169
|
burst;
|
|
166
170
|
now;
|
|
171
|
+
maxTrackedKeys;
|
|
167
172
|
buckets = /* @__PURE__ */ new Map();
|
|
168
173
|
lastSweep;
|
|
169
174
|
/** true = allowed (a token was taken); false = rate limited */
|
|
@@ -172,6 +177,10 @@ var TokenBucketLimiter = class {
|
|
|
172
177
|
this.maybeSweep(t);
|
|
173
178
|
let bucket = this.buckets.get(key);
|
|
174
179
|
if (!bucket) {
|
|
180
|
+
if (this.buckets.size >= this.maxTrackedKeys) {
|
|
181
|
+
const oldest = this.buckets.keys().next().value;
|
|
182
|
+
if (oldest !== void 0) this.buckets.delete(oldest);
|
|
183
|
+
}
|
|
175
184
|
bucket = { tokens: this.burst, lastRefill: t };
|
|
176
185
|
this.buckets.set(key, bucket);
|
|
177
186
|
} else {
|
|
@@ -218,6 +227,14 @@ function send(res, status, body, headers = {}) {
|
|
|
218
227
|
function sendJson(res, status, value, headers = {}) {
|
|
219
228
|
send(res, status, JSON.stringify(value, null, 2), { "content-type": "application/json", ...headers });
|
|
220
229
|
}
|
|
230
|
+
function stripPort(entry) {
|
|
231
|
+
if (entry.startsWith("[")) {
|
|
232
|
+
const end = entry.indexOf("]");
|
|
233
|
+
return end === -1 ? entry : entry.slice(1, end);
|
|
234
|
+
}
|
|
235
|
+
const parts = entry.split(":");
|
|
236
|
+
return parts.length === 2 ? parts[0] : entry;
|
|
237
|
+
}
|
|
221
238
|
function routeLabel(path) {
|
|
222
239
|
if (path === "/healthz") return "healthz";
|
|
223
240
|
if (path === "/metrics") return "metrics";
|
|
@@ -259,11 +276,17 @@ function createGateway(options = {}) {
|
|
|
259
276
|
registry.gauge("gateway_cache_bytes", "bytes held by the LRU", () => cache.usedBytes);
|
|
260
277
|
registry.gauge("gateway_cache_entries", "entries in the LRU", () => cache.size);
|
|
261
278
|
registry.gauge("gateway_ratelimit_tracked_ips", "token buckets currently tracked", () => limiter.trackedKeys);
|
|
279
|
+
const trustedHops = options.trustProxy === true ? 1 : typeof options.trustProxy === "number" ? options.trustProxy : 0;
|
|
262
280
|
function clientIp(req) {
|
|
263
|
-
if (
|
|
281
|
+
if (trustedHops > 0) {
|
|
264
282
|
const xff = req.headers["x-forwarded-for"];
|
|
265
|
-
const
|
|
266
|
-
|
|
283
|
+
const joined = Array.isArray(xff) ? xff.join(",") : xff;
|
|
284
|
+
const entries = (joined ?? "").split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
285
|
+
const candidate = entries[Math.max(0, entries.length - trustedHops)];
|
|
286
|
+
if (candidate !== void 0) {
|
|
287
|
+
const ip = stripPort(candidate);
|
|
288
|
+
if (isIP(ip) !== 0) return ip;
|
|
289
|
+
}
|
|
267
290
|
}
|
|
268
291
|
return req.socket.remoteAddress ?? "unknown";
|
|
269
292
|
}
|
|
@@ -288,7 +311,7 @@ function createGateway(options = {}) {
|
|
|
288
311
|
const length = Number(upstreamRes.headers.get("content-length") ?? NaN);
|
|
289
312
|
const cacheable = upstreamRes.status === 200 && !Number.isNaN(length) && length <= cacheMaxEntry;
|
|
290
313
|
if (cacheable || !upstreamRes.body) {
|
|
291
|
-
const body =
|
|
314
|
+
const body = await readBodyCapped(upstreamRes, cacheMaxEntry, url);
|
|
292
315
|
if (upstreamRes.status === 200) {
|
|
293
316
|
if (cacheMaxBytes > 0) cache.set(cacheKey, { status: 200, headers: { ...headers, ...flat(extra) }, body });
|
|
294
317
|
return send(res, 200, body, { ...headers, ...extra, "x-cache": "MISS" });
|
|
@@ -304,6 +327,13 @@ function createGateway(options = {}) {
|
|
|
304
327
|
for (const [k, v] of Object.entries(h)) out[k] = Array.isArray(v) ? v.join(", ") : v;
|
|
305
328
|
return out;
|
|
306
329
|
}
|
|
330
|
+
function cacheKeyFor(path, url) {
|
|
331
|
+
if (/^\/ord\/v1\/proof\//.test(path)) {
|
|
332
|
+
const level2 = (url.searchParams.get("level") ?? "l2").toUpperCase() === "L3" ? "l3" : "l2";
|
|
333
|
+
return `${path}?level=${level2}`;
|
|
334
|
+
}
|
|
335
|
+
return path;
|
|
336
|
+
}
|
|
307
337
|
async function handle(req, res) {
|
|
308
338
|
const url = new URL(req.url ?? "/", "http://gateway.local");
|
|
309
339
|
const path = url.pathname;
|
|
@@ -323,7 +353,7 @@ function createGateway(options = {}) {
|
|
|
323
353
|
return sendJson(res, 429, { error: "rate limited" }, { "retry-after": String(limiter.retryAfterSeconds(ip)) });
|
|
324
354
|
}
|
|
325
355
|
}
|
|
326
|
-
const cacheKey = path
|
|
356
|
+
const cacheKey = cacheKeyFor(path, url);
|
|
327
357
|
if (cacheMaxBytes > 0) {
|
|
328
358
|
const hit = cache.get(cacheKey);
|
|
329
359
|
if (hit) {
|
|
@@ -367,7 +397,7 @@ function createGateway(options = {}) {
|
|
|
367
397
|
}
|
|
368
398
|
if (contentMatch || path.startsWith("/r/") || path.startsWith("/preview/") || path === "/blockheight" || path === "/blocktime" || path.startsWith("/blockhash")) {
|
|
369
399
|
try {
|
|
370
|
-
return await proxy(req, res, path
|
|
400
|
+
return await proxy(req, res, path, cacheKey);
|
|
371
401
|
} catch (e) {
|
|
372
402
|
mUpstreamErrors.inc();
|
|
373
403
|
return sendJson(res, 502, { error: e.message });
|
|
@@ -447,7 +477,8 @@ if (import.meta.url === `file://${process.argv[1]}`) {
|
|
|
447
477
|
cacheMaxEntryBytes: process.env.CACHE_MAX_ENTRY_BYTES ? Number(process.env.CACHE_MAX_ENTRY_BYTES) : void 0,
|
|
448
478
|
rateLimitPerSec: process.env.RATE_LIMIT ? Number(process.env.RATE_LIMIT) : void 0,
|
|
449
479
|
rateBurst: process.env.RATE_BURST ? Number(process.env.RATE_BURST) : void 0,
|
|
450
|
-
|
|
480
|
+
// TRUST_PROXY = number of trusted proxy hops (1 for a single LB/CDN)
|
|
481
|
+
trustProxy: process.env.TRUST_PROXY ? Number(process.env.TRUST_PROXY) || 0 : void 0,
|
|
451
482
|
log
|
|
452
483
|
});
|
|
453
484
|
installShutdown(gateway, void 0, log);
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/lru.ts","../src/metrics.ts","../src/ratelimit.ts"],"sourcesContent":["import { createServer, type IncomingMessage, type Server, type ServerResponse } from 'node:http';\nimport { Readable } from 'node:stream';\nimport { isInscriptionId, parseInscriptionId } from '@ordspv/core';\nimport {\n buildProofBundle,\n EsploraBackend,\n OrdResolver,\n toResponse,\n type FetchFn,\n} from '@ordspv/fetch';\nimport { ByteLru } from './lru.js';\nimport { Registry } from './metrics.js';\nimport { TokenBucketLimiter } from './ratelimit.js';\n\nexport { ByteLru } from './lru.js';\nexport { Counter, Histogram, Registry } from './metrics.js';\nexport { TokenBucketLimiter } from './ratelimit.js';\n\n/**\n * Reference ord gateway (SPEC-GATEWAY.md).\n *\n * Personalities:\n * - proxy : replicate an upstream ord server's /content and /r/* surface\n * with ord-parity headers. Availability play only; adds no trust.\n * - verify : serve /content only after locally verifying the bytes against\n * Bitcoin (L2 by default) via esplora backends.\n *\n * Both serve: GET /ord/v1/proof/<id>?level=l2|l3, GET /ord/v1/verified/<id>,\n * GET /healthz, GET /metrics (prometheus text)\n *\n * Operational features (SPEC-GATEWAY §7): bounded byte-budget LRU on\n * immutable 200s (x-cache header), per-IP token-bucket rate limiting,\n * streaming proxy for oversized bodies, structured JSON request logs,\n * graceful shutdown. Verified responses are buffered, because a merkle proof\n * cannot be verified over bytes that have not all been read.\n */\n\nexport interface GatewayOptions {\n port?: number;\n upstream?: string;\n esplora?: string[];\n mode?: 'proxy' | 'verify';\n verification?: 'L2' | 'L3';\n fetchFn?: FetchFn;\n /** LRU budget across cached bodies (default 256 MiB; 0 disables) */\n cacheMaxBytes?: number;\n /** largest single cacheable body (default 8 MiB) */\n cacheMaxEntryBytes?: number;\n /** sustained requests/second per IP (default 10; 0 disables) */\n rateLimitPerSec?: number;\n /** burst size per IP (default 40) */\n rateBurst?: number;\n /** trust the first X-Forwarded-For hop for the client IP (behind a LB/CDN) */\n trustProxy?: boolean;\n /** structured log sink; false silences (default). CLI wires console.log. */\n log?: ((line: Record<string, unknown>) => void) | false;\n}\n\n// ord-parity security headers for /content (see research: ord server.rs)\nconst CONTENT_CSP = [\n \"default-src 'self' 'unsafe-eval' 'unsafe-inline' data: blob:\",\n \"default-src *:*/content/ *:*/blockheight *:*/blockhash *:*/blockhash/ *:*/blocktime *:*/r/ 'unsafe-eval' 'unsafe-inline' data: blob:\",\n];\nconst IMMUTABLE = 'public, max-age=1209600, immutable';\n\nfunction send(\n res: ServerResponse,\n status: number,\n body: string | Uint8Array,\n headers: Record<string, string | string[]> = {},\n): void {\n res.writeHead(status, { 'access-control-allow-origin': '*', ...headers });\n res.end(typeof body === 'string' ? body : Buffer.from(body));\n}\n\nfunction sendJson(res: ServerResponse, status: number, value: unknown, headers: Record<string, string> = {}): void {\n send(res, status, JSON.stringify(value, null, 2), { 'content-type': 'application/json', ...headers });\n}\n\n/** low-cardinality route label for metrics/logs */\nexport function routeLabel(path: string): string {\n if (path === '/healthz') return 'healthz';\n if (path === '/metrics') return 'metrics';\n if (path.startsWith('/content/')) return '/content/:id';\n if (path.startsWith('/preview/')) return '/preview/:id';\n if (/^\\/ord\\/v1\\/proof\\//.test(path)) return '/ord/v1/proof/:id';\n if (/^\\/ord\\/v1\\/verified\\//.test(path)) return '/ord/v1/verified/:id';\n if (path.startsWith('/r/')) return '/r/*';\n if (path === '/blockheight' || path === '/blocktime' || path.startsWith('/blockhash')) return '/chain';\n return 'other';\n}\n\nexport function createGateway(options: GatewayOptions = {}): Server {\n const upstream = (options.upstream ?? 'https://ordinals.com').replace(/\\/+$/, '');\n const mode = options.mode ?? 'proxy';\n const level = options.verification ?? 'L2';\n const fetchFn: FetchFn = options.fetchFn ?? ((u, i) => fetch(u, i));\n const esploras = (options.esplora ?? ['https://mempool.space/api', 'https://blockstream.info/api']).map(\n (u) => new EsploraBackend(u, fetchFn),\n );\n const resolver = new OrdResolver({\n esplora: esploras.map((e) => e.baseUrl),\n ordGateways: [upstream],\n fetchFn,\n verification: level,\n });\n\n const cacheMaxBytes = options.cacheMaxBytes ?? 256 * 1024 * 1024;\n const cacheMaxEntry = options.cacheMaxEntryBytes ?? 8 * 1024 * 1024;\n const cache = new ByteLru(cacheMaxBytes, cacheMaxEntry);\n const ratePerSec = options.rateLimitPerSec ?? 10;\n const limiter = new TokenBucketLimiter(ratePerSec, options.rateBurst ?? 40);\n const log = options.log || undefined;\n\n const registry = new Registry();\n const mRequests = registry.counter('gateway_http_requests_total', 'HTTP requests by route/method/status');\n const mDuration = registry.histogram('gateway_http_request_duration_seconds', 'request latency');\n const mCacheHits = registry.counter('gateway_cache_hits_total', 'LRU cache hits');\n const mCacheMisses = registry.counter('gateway_cache_misses_total', 'LRU cache misses (cacheable routes only)');\n const mRateLimited = registry.counter('gateway_rate_limited_total', 'requests rejected by the per-IP token bucket');\n const mUpstreamErrors = registry.counter('gateway_upstream_errors_total', 'errors talking to upstream/esplora');\n registry.gauge('gateway_cache_bytes', 'bytes held by the LRU', () => cache.usedBytes);\n registry.gauge('gateway_cache_entries', 'entries in the LRU', () => cache.size);\n registry.gauge('gateway_ratelimit_tracked_ips', 'token buckets currently tracked', () => limiter.trackedKeys);\n\n function clientIp(req: IncomingMessage): string {\n if (options.trustProxy) {\n const xff = req.headers['x-forwarded-for'];\n const first = (Array.isArray(xff) ? xff[0] : xff)?.split(',')[0]?.trim();\n if (first) return first;\n }\n return req.socket.remoteAddress ?? 'unknown';\n }\n\n /** cache + send an immutable 200 */\n function sendCached(\n res: ServerResponse,\n cacheKey: string,\n body: Uint8Array,\n headers: Record<string, string>,\n ): void {\n if (cacheMaxBytes > 0) cache.set(cacheKey, { status: 200, headers, body });\n send(res, 200, body, { ...headers, 'x-cache': 'MISS' });\n }\n\n async function proxy(req: IncomingMessage, res: ServerResponse, path: string, cacheKey: string): Promise<void> {\n const url = `${upstream}${path}`;\n const upstreamRes = await fetchFn(url, {\n headers: { 'accept-encoding': req.headers['accept-encoding'] ?? 'identity' },\n });\n const headers: Record<string, string> = {};\n for (const name of ['content-type', 'content-encoding', 'cache-control']) {\n const v = upstreamRes.headers.get(name);\n if (v) headers[name] = v;\n }\n if (path.startsWith('/content/')) {\n headers['cache-control'] = IMMUTABLE;\n }\n const extra: Record<string, string | string[]> = path.startsWith('/content/')\n ? { 'content-security-policy': CONTENT_CSP }\n : {};\n\n const length = Number(upstreamRes.headers.get('content-length') ?? NaN);\n const cacheable = upstreamRes.status === 200 && !Number.isNaN(length) && length <= cacheMaxEntry;\n if (cacheable || !upstreamRes.body) {\n const body = new Uint8Array(await upstreamRes.arrayBuffer());\n if (upstreamRes.status === 200) {\n if (cacheMaxBytes > 0) cache.set(cacheKey, { status: 200, headers: { ...headers, ...flat(extra) }, body });\n return send(res, 200, body, { ...headers, ...extra, 'x-cache': 'MISS' });\n }\n return send(res, upstreamRes.status, body, { ...headers, ...extra });\n }\n // large or unknown-length body: stream through, uncached\n res.writeHead(upstreamRes.status, { 'access-control-allow-origin': '*', ...headers, ...extra });\n Readable.fromWeb(upstreamRes.body as import('node:stream/web').ReadableStream).pipe(res);\n await new Promise<void>((resolve) => res.on('close', resolve));\n }\n\n function flat(h: Record<string, string | string[]>): Record<string, string> {\n const out: Record<string, string> = {};\n for (const [k, v] of Object.entries(h)) out[k] = Array.isArray(v) ? v.join(', ') : v;\n return out;\n }\n\n async function handle(req: IncomingMessage, res: ServerResponse): Promise<void> {\n const url = new URL(req.url ?? '/', 'http://gateway.local');\n const path = url.pathname;\n\n if (req.method !== 'GET' && req.method !== 'HEAD') {\n return sendJson(res, 405, { error: 'method not allowed' });\n }\n\n if (path === '/healthz') {\n return sendJson(res, 200, { ok: true, mode, upstream, esplora: esploras.map((e) => e.baseUrl) });\n }\n if (path === '/metrics') {\n return send(res, 200, registry.render(), { 'content-type': 'text/plain; version=0.0.4' });\n }\n\n // rate limit everything else\n if (ratePerSec > 0) {\n const ip = clientIp(req);\n if (!limiter.take(ip)) {\n mRateLimited.inc();\n return sendJson(res, 429, { error: 'rate limited' }, { 'retry-after': String(limiter.retryAfterSeconds(ip)) });\n }\n }\n\n // immutable-response cache\n const cacheKey = path + url.search;\n if (cacheMaxBytes > 0) {\n const hit = cache.get(cacheKey);\n if (hit) {\n mCacheHits.inc();\n return send(res, hit.status, hit.body, { ...hit.headers, 'x-cache': 'HIT' });\n }\n mCacheMisses.inc();\n }\n\n // proof bundles\n const proofMatch = path.match(/^\\/ord\\/v1\\/proof\\/([^/]+)$/);\n if (proofMatch) {\n const id = proofMatch[1];\n if (!isInscriptionId(id)) return sendJson(res, 400, { error: `invalid inscription id: ${id}` });\n const wanted = (url.searchParams.get('level') ?? 'l2').toUpperCase() === 'L3' ? 'L3' : 'L2';\n try {\n const bundle = await tryBackends(esploras, (e) => buildProofBundle(e, parseInscriptionId(id), wanted));\n return sendCached(res, cacheKey, new TextEncoder().encode(JSON.stringify(bundle)), {\n 'content-type': 'application/vnd.ord.proof+json; version=1',\n 'cache-control': IMMUTABLE,\n });\n } catch (e) {\n mUpstreamErrors.inc();\n return sendJson(res, 502, { error: (e as Error).message });\n }\n }\n\n // verified content (also the verify-mode /content handler)\n const verifiedMatch = path.match(/^\\/ord\\/v1\\/verified\\/([^/]+)$/);\n const contentMatch = path.match(/^\\/content\\/([^/]+)$/);\n if (verifiedMatch || (contentMatch && mode === 'verify')) {\n const id = (verifiedMatch ?? contentMatch)![1];\n if (!isInscriptionId(id)) return sendJson(res, 400, { error: `invalid inscription id: ${id}` });\n try {\n const result = await resolver.resolve(`ord:${id}/content`);\n const response = toResponse(result);\n const headers: Record<string, string> = {};\n response.headers.forEach((v, k) => (headers[k] = v));\n headers['content-security-policy'] = CONTENT_CSP.join(', ');\n return sendCached(res, cacheKey, new Uint8Array(await response.arrayBuffer()), headers);\n } catch (e) {\n mUpstreamErrors.inc();\n return sendJson(res, 502, { error: (e as Error).message });\n }\n }\n\n // ord-server surface passthrough (recursion compatibility)\n if (\n contentMatch ||\n path.startsWith('/r/') ||\n path.startsWith('/preview/') ||\n path === '/blockheight' ||\n path === '/blocktime' ||\n path.startsWith('/blockhash')\n ) {\n try {\n return await proxy(req, res, path + url.search, cacheKey);\n } catch (e) {\n mUpstreamErrors.inc();\n return sendJson(res, 502, { error: (e as Error).message });\n }\n }\n\n return sendJson(res, 404, {\n error: 'not found',\n routes: [\n '/content/<id>',\n '/r/*',\n '/ord/v1/proof/<id>?level=l2|l3',\n '/ord/v1/verified/<id>',\n '/healthz',\n '/metrics',\n ],\n });\n }\n\n return createServer((req, res) => {\n const started = performance.now();\n res.on('finish', () => {\n const seconds = (performance.now() - started) / 1000;\n const route = routeLabel(new URL(req.url ?? '/', 'http://x').pathname);\n mRequests.inc({ route, method: req.method ?? 'GET', status: String(res.statusCode) });\n mDuration.observe({ route }, seconds);\n log?.({\n t: new Date().toISOString(),\n msg: 'req',\n ip: clientIp(req),\n method: req.method,\n path: req.url,\n status: res.statusCode,\n ms: Math.round(seconds * 1000),\n cache: res.getHeader('x-cache') ?? undefined,\n bytes: Number(res.getHeader('content-length') ?? 0) || undefined,\n });\n });\n handle(req, res).catch((e) => sendJson(res, 500, { error: (e as Error).message }));\n });\n}\n\nasync function tryBackends<T>(backends: EsploraBackend[], fn: (e: EsploraBackend) => Promise<T>): Promise<T> {\n const errors: string[] = [];\n for (const b of backends) {\n try {\n return await fn(b);\n } catch (e) {\n errors.push(`${b.baseUrl}: ${(e as Error).message}`);\n }\n }\n throw new Error(`all backends failed: ${errors.join('; ')}`);\n}\n\n/** graceful shutdown: stop accepting, drain in-flight, force-close after grace */\nexport function installShutdown(server: Server, graceMs = 10_000, log?: (l: Record<string, unknown>) => void): void {\n let closing = false;\n const close = (signal: string) => {\n if (closing) return;\n closing = true;\n log?.({ t: new Date().toISOString(), msg: 'shutdown', signal, graceMs });\n server.close(() => process.exit(0));\n server.closeIdleConnections();\n setTimeout(() => {\n server.closeAllConnections();\n process.exit(0);\n }, graceMs).unref();\n };\n process.on('SIGTERM', () => close('SIGTERM'));\n process.on('SIGINT', () => close('SIGINT'));\n}\n\n/** CLI entry: npx tsx packages/gateway/src/index.ts */\nif (import.meta.url === `file://${process.argv[1]}`) {\n const port = Number(process.env.PORT ?? 8317);\n const log = (line: Record<string, unknown>) => console.log(JSON.stringify(line));\n const gateway = createGateway({\n port,\n upstream: process.env.ORD_UPSTREAM,\n esplora: process.env.ESPLORA?.split(','),\n mode: (process.env.GATEWAY_MODE as 'proxy' | 'verify') ?? 'proxy',\n verification: process.env.GATEWAY_LEVEL === 'L3' ? 'L3' : 'L2',\n cacheMaxBytes: process.env.CACHE_MAX_BYTES ? Number(process.env.CACHE_MAX_BYTES) : undefined,\n cacheMaxEntryBytes: process.env.CACHE_MAX_ENTRY_BYTES ? Number(process.env.CACHE_MAX_ENTRY_BYTES) : undefined,\n rateLimitPerSec: process.env.RATE_LIMIT ? Number(process.env.RATE_LIMIT) : undefined,\n rateBurst: process.env.RATE_BURST ? Number(process.env.RATE_BURST) : undefined,\n trustProxy: process.env.TRUST_PROXY === '1',\n log,\n });\n installShutdown(gateway, undefined, log);\n gateway.listen(port, () => {\n log({ t: new Date().toISOString(), msg: 'listening', port, mode: process.env.GATEWAY_MODE ?? 'proxy' });\n });\n}\n","/**\n * Byte-budgeted LRU for immutable responses. Everything a gateway serves with\n * `immutable` cache semantics is safe to cache forever; the only pressure is\n * memory, so the budget is BYTES, not entries. Map iteration order gives us\n * recency for free (delete + re-set on hit).\n */\n\nexport interface CachedResponse {\n status: number;\n headers: Record<string, string>;\n body: Uint8Array;\n}\n\nexport class ByteLru {\n private readonly entries = new Map<string, CachedResponse>();\n private bytes = 0;\n hits = 0;\n misses = 0;\n\n constructor(\n /** total budget across all bodies */\n private readonly maxBytes: number,\n /** refuse to cache any single body larger than this */\n private readonly maxEntryBytes: number,\n ) {}\n\n get(key: string): CachedResponse | undefined {\n const hit = this.entries.get(key);\n if (!hit) {\n this.misses++;\n return undefined;\n }\n // refresh recency\n this.entries.delete(key);\n this.entries.set(key, hit);\n this.hits++;\n return hit;\n }\n\n set(key: string, value: CachedResponse): void {\n if (value.body.length > this.maxEntryBytes) return;\n const existing = this.entries.get(key);\n if (existing) {\n this.bytes -= existing.body.length;\n this.entries.delete(key);\n }\n this.entries.set(key, value);\n this.bytes += value.body.length;\n // evict least-recently-used until inside budget\n for (const [oldest, entry] of this.entries) {\n if (this.bytes <= this.maxBytes) break;\n if (oldest === key && this.entries.size === 1) break;\n this.entries.delete(oldest);\n this.bytes -= entry.body.length;\n }\n }\n\n get size(): number {\n return this.entries.size;\n }\n\n get usedBytes(): number {\n return this.bytes;\n }\n}\n","/**\n * Hand-rolled Prometheus text-format (0.0.4) registry: counters and\n * histograms with a small fixed label space. No dependency, no magic;\n * exactly what /metrics needs and nothing more.\n */\n\ntype Labels = Record<string, string>;\n\nfunction labelKey(labels: Labels): string {\n return Object.keys(labels)\n .sort()\n .map((k) => `${k}=${JSON.stringify(labels[k])}`)\n .join(',');\n}\n\nfunction renderLabels(labels: Labels, extra?: string): string {\n const parts = Object.keys(labels)\n .sort()\n .map((k) => `${k}=${JSON.stringify(labels[k])}`);\n if (extra) parts.push(extra);\n return parts.length ? `{${parts.join(',')}}` : '';\n}\n\nexport class Counter {\n private readonly series = new Map<string, { labels: Labels; value: number }>();\n constructor(\n readonly name: string,\n readonly help: string,\n ) {}\n\n inc(labels: Labels = {}, by = 1): void {\n const key = labelKey(labels);\n const entry = this.series.get(key) ?? { labels, value: 0 };\n entry.value += by;\n this.series.set(key, entry);\n }\n\n render(): string {\n const lines = [`# HELP ${this.name} ${this.help}`, `# TYPE ${this.name} counter`];\n if (this.series.size === 0) lines.push(`${this.name} 0`);\n for (const { labels, value } of this.series.values()) {\n lines.push(`${this.name}${renderLabels(labels)} ${value}`);\n }\n return lines.join('\\n');\n }\n}\n\nexport class Histogram {\n private readonly series = new Map<string, { labels: Labels; buckets: number[]; sum: number; count: number }>();\n constructor(\n readonly name: string,\n readonly help: string,\n readonly bounds: number[] = [0.005, 0.025, 0.1, 0.25, 1, 2.5, 10, 30],\n ) {}\n\n observe(labels: Labels, value: number): void {\n const key = labelKey(labels);\n let entry = this.series.get(key);\n if (!entry) {\n entry = { labels, buckets: this.bounds.map(() => 0), sum: 0, count: 0 };\n this.series.set(key, entry);\n }\n for (let i = 0; i < this.bounds.length; i++) {\n if (value <= this.bounds[i]) entry.buckets[i]++;\n }\n entry.sum += value;\n entry.count++;\n }\n\n render(): string {\n const lines = [`# HELP ${this.name} ${this.help}`, `# TYPE ${this.name} histogram`];\n for (const { labels, buckets, sum, count } of this.series.values()) {\n this.bounds.forEach((bound, i) => {\n lines.push(`${this.name}_bucket${renderLabels(labels, `le=\"${bound}\"`)} ${buckets[i]}`);\n });\n lines.push(`${this.name}_bucket${renderLabels(labels, 'le=\"+Inf\"')} ${count}`);\n lines.push(`${this.name}_sum${renderLabels(labels)} ${sum}`);\n lines.push(`${this.name}_count${renderLabels(labels)} ${count}`);\n }\n return lines.join('\\n');\n }\n}\n\nexport class Registry {\n private readonly metrics: Array<Counter | Histogram> = [];\n private readonly gauges: Array<{ name: string; help: string; read: () => number }> = [];\n\n counter(name: string, help: string): Counter {\n const c = new Counter(name, help);\n this.metrics.push(c);\n return c;\n }\n\n histogram(name: string, help: string, bounds?: number[]): Histogram {\n const h = new Histogram(name, help, bounds);\n this.metrics.push(h);\n return h;\n }\n\n /** callback gauge, sampled at render time (cache bytes, tracked IPs, …) */\n gauge(name: string, help: string, read: () => number): void {\n this.gauges.push({ name, help, read });\n }\n\n render(): string {\n const parts = this.metrics.map((m) => m.render());\n for (const g of this.gauges) {\n parts.push(`# HELP ${g.name} ${g.help}\\n# TYPE ${g.name} gauge\\n${g.name} ${g.read()}`);\n }\n return `${parts.join('\\n')}\\n`;\n }\n}\n","/**\n * Per-key token bucket: `ratePerSec` sustained, `burst` peak. Injectable\n * clock for tests. Idle buckets are swept so hostile IP churn cannot grow the\n * map without bound.\n */\n\ninterface Bucket {\n tokens: number;\n lastRefill: number;\n}\n\nexport class TokenBucketLimiter {\n private readonly buckets = new Map<string, Bucket>();\n private lastSweep: number;\n\n constructor(\n private readonly ratePerSec: number,\n private readonly burst: number,\n private readonly now: () => number = () => Date.now(),\n ) {\n this.lastSweep = this.now();\n }\n\n /** true = allowed (a token was taken); false = rate limited */\n take(key: string): boolean {\n const t = this.now();\n this.maybeSweep(t);\n let bucket = this.buckets.get(key);\n if (!bucket) {\n bucket = { tokens: this.burst, lastRefill: t };\n this.buckets.set(key, bucket);\n } else {\n const elapsed = (t - bucket.lastRefill) / 1000;\n bucket.tokens = Math.min(this.burst, bucket.tokens + elapsed * this.ratePerSec);\n bucket.lastRefill = t;\n }\n if (bucket.tokens < 1) return false;\n bucket.tokens -= 1;\n return true;\n }\n\n /** seconds until one token is available (for retry-after) */\n retryAfterSeconds(key: string): number {\n const bucket = this.buckets.get(key);\n if (!bucket || bucket.tokens >= 1) return 0;\n return Math.ceil((1 - bucket.tokens) / this.ratePerSec);\n }\n\n private maybeSweep(t: number): void {\n if (t - this.lastSweep < 60_000) return;\n this.lastSweep = t;\n const idleCutoff = t - 120_000;\n for (const [key, bucket] of this.buckets) {\n // effective tokens: what the bucket WOULD hold if refilled right now;\n // an idle bucket is safe to drop once it is logically full again\n const effective = bucket.tokens + ((t - bucket.lastRefill) / 1000) * this.ratePerSec;\n if (bucket.lastRefill < idleCutoff && effective >= this.burst) {\n this.buckets.delete(key);\n }\n }\n }\n\n get trackedKeys(): number {\n return this.buckets.size;\n }\n}\n"],"mappings":";AAAA,SAAS,oBAA4E;AACrF,SAAS,gBAAgB;AACzB,SAAS,iBAAiB,0BAA0B;AACpD;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAEK;;;ACIA,IAAM,UAAN,MAAc;AAAA,EAMnB,YAEmB,UAEA,eACjB;AAHiB;AAEA;AAAA,EAChB;AAAA,EAHgB;AAAA,EAEA;AAAA,EATF,UAAU,oBAAI,IAA4B;AAAA,EACnD,QAAQ;AAAA,EAChB,OAAO;AAAA,EACP,SAAS;AAAA,EAST,IAAI,KAAyC;AAC3C,UAAM,MAAM,KAAK,QAAQ,IAAI,GAAG;AAChC,QAAI,CAAC,KAAK;AACR,WAAK;AACL,aAAO;AAAA,IACT;AAEA,SAAK,QAAQ,OAAO,GAAG;AACvB,SAAK,QAAQ,IAAI,KAAK,GAAG;AACzB,SAAK;AACL,WAAO;AAAA,EACT;AAAA,EAEA,IAAI,KAAa,OAA6B;AAC5C,QAAI,MAAM,KAAK,SAAS,KAAK,cAAe;AAC5C,UAAM,WAAW,KAAK,QAAQ,IAAI,GAAG;AACrC,QAAI,UAAU;AACZ,WAAK,SAAS,SAAS,KAAK;AAC5B,WAAK,QAAQ,OAAO,GAAG;AAAA,IACzB;AACA,SAAK,QAAQ,IAAI,KAAK,KAAK;AAC3B,SAAK,SAAS,MAAM,KAAK;AAEzB,eAAW,CAAC,QAAQ,KAAK,KAAK,KAAK,SAAS;AAC1C,UAAI,KAAK,SAAS,KAAK,SAAU;AACjC,UAAI,WAAW,OAAO,KAAK,QAAQ,SAAS,EAAG;AAC/C,WAAK,QAAQ,OAAO,MAAM;AAC1B,WAAK,SAAS,MAAM,KAAK;AAAA,IAC3B;AAAA,EACF;AAAA,EAEA,IAAI,OAAe;AACjB,WAAO,KAAK,QAAQ;AAAA,EACtB;AAAA,EAEA,IAAI,YAAoB;AACtB,WAAO,KAAK;AAAA,EACd;AACF;;;ACxDA,SAAS,SAAS,QAAwB;AACxC,SAAO,OAAO,KAAK,MAAM,EACtB,KAAK,EACL,IAAI,CAAC,MAAM,GAAG,CAAC,IAAI,KAAK,UAAU,OAAO,CAAC,CAAC,CAAC,EAAE,EAC9C,KAAK,GAAG;AACb;AAEA,SAAS,aAAa,QAAgB,OAAwB;AAC5D,QAAM,QAAQ,OAAO,KAAK,MAAM,EAC7B,KAAK,EACL,IAAI,CAAC,MAAM,GAAG,CAAC,IAAI,KAAK,UAAU,OAAO,CAAC,CAAC,CAAC,EAAE;AACjD,MAAI,MAAO,OAAM,KAAK,KAAK;AAC3B,SAAO,MAAM,SAAS,IAAI,MAAM,KAAK,GAAG,CAAC,MAAM;AACjD;AAEO,IAAM,UAAN,MAAc;AAAA,EAEnB,YACW,MACA,MACT;AAFS;AACA;AAAA,EACR;AAAA,EAFQ;AAAA,EACA;AAAA,EAHM,SAAS,oBAAI,IAA+C;AAAA,EAM7E,IAAI,SAAiB,CAAC,GAAG,KAAK,GAAS;AACrC,UAAM,MAAM,SAAS,MAAM;AAC3B,UAAM,QAAQ,KAAK,OAAO,IAAI,GAAG,KAAK,EAAE,QAAQ,OAAO,EAAE;AACzD,UAAM,SAAS;AACf,SAAK,OAAO,IAAI,KAAK,KAAK;AAAA,EAC5B;AAAA,EAEA,SAAiB;AACf,UAAM,QAAQ,CAAC,UAAU,KAAK,IAAI,IAAI,KAAK,IAAI,IAAI,UAAU,KAAK,IAAI,UAAU;AAChF,QAAI,KAAK,OAAO,SAAS,EAAG,OAAM,KAAK,GAAG,KAAK,IAAI,IAAI;AACvD,eAAW,EAAE,QAAQ,MAAM,KAAK,KAAK,OAAO,OAAO,GAAG;AACpD,YAAM,KAAK,GAAG,KAAK,IAAI,GAAG,aAAa,MAAM,CAAC,IAAI,KAAK,EAAE;AAAA,IAC3D;AACA,WAAO,MAAM,KAAK,IAAI;AAAA,EACxB;AACF;AAEO,IAAM,YAAN,MAAgB;AAAA,EAErB,YACW,MACA,MACA,SAAmB,CAAC,MAAO,OAAO,KAAK,MAAM,GAAG,KAAK,IAAI,EAAE,GACpE;AAHS;AACA;AACA;AAAA,EACR;AAAA,EAHQ;AAAA,EACA;AAAA,EACA;AAAA,EAJM,SAAS,oBAAI,IAA+E;AAAA,EAO7G,QAAQ,QAAgB,OAAqB;AAC3C,UAAM,MAAM,SAAS,MAAM;AAC3B,QAAI,QAAQ,KAAK,OAAO,IAAI,GAAG;AAC/B,QAAI,CAAC,OAAO;AACV,cAAQ,EAAE,QAAQ,SAAS,KAAK,OAAO,IAAI,MAAM,CAAC,GAAG,KAAK,GAAG,OAAO,EAAE;AACtE,WAAK,OAAO,IAAI,KAAK,KAAK;AAAA,IAC5B;AACA,aAAS,IAAI,GAAG,IAAI,KAAK,OAAO,QAAQ,KAAK;AAC3C,UAAI,SAAS,KAAK,OAAO,CAAC,EAAG,OAAM,QAAQ,CAAC;AAAA,IAC9C;AACA,UAAM,OAAO;AACb,UAAM;AAAA,EACR;AAAA,EAEA,SAAiB;AACf,UAAM,QAAQ,CAAC,UAAU,KAAK,IAAI,IAAI,KAAK,IAAI,IAAI,UAAU,KAAK,IAAI,YAAY;AAClF,eAAW,EAAE,QAAQ,SAAS,KAAK,MAAM,KAAK,KAAK,OAAO,OAAO,GAAG;AAClE,WAAK,OAAO,QAAQ,CAAC,OAAO,MAAM;AAChC,cAAM,KAAK,GAAG,KAAK,IAAI,UAAU,aAAa,QAAQ,OAAO,KAAK,GAAG,CAAC,IAAI,QAAQ,CAAC,CAAC,EAAE;AAAA,MACxF,CAAC;AACD,YAAM,KAAK,GAAG,KAAK,IAAI,UAAU,aAAa,QAAQ,WAAW,CAAC,IAAI,KAAK,EAAE;AAC7E,YAAM,KAAK,GAAG,KAAK,IAAI,OAAO,aAAa,MAAM,CAAC,IAAI,GAAG,EAAE;AAC3D,YAAM,KAAK,GAAG,KAAK,IAAI,SAAS,aAAa,MAAM,CAAC,IAAI,KAAK,EAAE;AAAA,IACjE;AACA,WAAO,MAAM,KAAK,IAAI;AAAA,EACxB;AACF;AAEO,IAAM,WAAN,MAAe;AAAA,EACH,UAAsC,CAAC;AAAA,EACvC,SAAoE,CAAC;AAAA,EAEtF,QAAQ,MAAc,MAAuB;AAC3C,UAAM,IAAI,IAAI,QAAQ,MAAM,IAAI;AAChC,SAAK,QAAQ,KAAK,CAAC;AACnB,WAAO;AAAA,EACT;AAAA,EAEA,UAAU,MAAc,MAAc,QAA8B;AAClE,UAAM,IAAI,IAAI,UAAU,MAAM,MAAM,MAAM;AAC1C,SAAK,QAAQ,KAAK,CAAC;AACnB,WAAO;AAAA,EACT;AAAA;AAAA,EAGA,MAAM,MAAc,MAAc,MAA0B;AAC1D,SAAK,OAAO,KAAK,EAAE,MAAM,MAAM,KAAK,CAAC;AAAA,EACvC;AAAA,EAEA,SAAiB;AACf,UAAM,QAAQ,KAAK,QAAQ,IAAI,CAAC,MAAM,EAAE,OAAO,CAAC;AAChD,eAAW,KAAK,KAAK,QAAQ;AAC3B,YAAM,KAAK,UAAU,EAAE,IAAI,IAAI,EAAE,IAAI;AAAA,SAAY,EAAE,IAAI;AAAA,EAAW,EAAE,IAAI,IAAI,EAAE,KAAK,CAAC,EAAE;AAAA,IACxF;AACA,WAAO,GAAG,MAAM,KAAK,IAAI,CAAC;AAAA;AAAA,EAC5B;AACF;;;ACpGO,IAAM,qBAAN,MAAyB;AAAA,EAI9B,YACmB,YACA,OACA,MAAoB,MAAM,KAAK,IAAI,GACpD;AAHiB;AACA;AACA;AAEjB,SAAK,YAAY,KAAK,IAAI;AAAA,EAC5B;AAAA,EALmB;AAAA,EACA;AAAA,EACA;AAAA,EANF,UAAU,oBAAI,IAAoB;AAAA,EAC3C;AAAA;AAAA,EAWR,KAAK,KAAsB;AACzB,UAAM,IAAI,KAAK,IAAI;AACnB,SAAK,WAAW,CAAC;AACjB,QAAI,SAAS,KAAK,QAAQ,IAAI,GAAG;AACjC,QAAI,CAAC,QAAQ;AACX,eAAS,EAAE,QAAQ,KAAK,OAAO,YAAY,EAAE;AAC7C,WAAK,QAAQ,IAAI,KAAK,MAAM;AAAA,IAC9B,OAAO;AACL,YAAM,WAAW,IAAI,OAAO,cAAc;AAC1C,aAAO,SAAS,KAAK,IAAI,KAAK,OAAO,OAAO,SAAS,UAAU,KAAK,UAAU;AAC9E,aAAO,aAAa;AAAA,IACtB;AACA,QAAI,OAAO,SAAS,EAAG,QAAO;AAC9B,WAAO,UAAU;AACjB,WAAO;AAAA,EACT;AAAA;AAAA,EAGA,kBAAkB,KAAqB;AACrC,UAAM,SAAS,KAAK,QAAQ,IAAI,GAAG;AACnC,QAAI,CAAC,UAAU,OAAO,UAAU,EAAG,QAAO;AAC1C,WAAO,KAAK,MAAM,IAAI,OAAO,UAAU,KAAK,UAAU;AAAA,EACxD;AAAA,EAEQ,WAAW,GAAiB;AAClC,QAAI,IAAI,KAAK,YAAY,IAAQ;AACjC,SAAK,YAAY;AACjB,UAAM,aAAa,IAAI;AACvB,eAAW,CAAC,KAAK,MAAM,KAAK,KAAK,SAAS;AAGxC,YAAM,YAAY,OAAO,UAAW,IAAI,OAAO,cAAc,MAAQ,KAAK;AAC1E,UAAI,OAAO,aAAa,cAAc,aAAa,KAAK,OAAO;AAC7D,aAAK,QAAQ,OAAO,GAAG;AAAA,MACzB;AAAA,IACF;AAAA,EACF;AAAA,EAEA,IAAI,cAAsB;AACxB,WAAO,KAAK,QAAQ;AAAA,EACtB;AACF;;;AHNA,IAAM,cAAc;AAAA,EAClB;AAAA,EACA;AACF;AACA,IAAM,YAAY;AAElB,SAAS,KACP,KACA,QACA,MACA,UAA6C,CAAC,GACxC;AACN,MAAI,UAAU,QAAQ,EAAE,+BAA+B,KAAK,GAAG,QAAQ,CAAC;AACxE,MAAI,IAAI,OAAO,SAAS,WAAW,OAAO,OAAO,KAAK,IAAI,CAAC;AAC7D;AAEA,SAAS,SAAS,KAAqB,QAAgB,OAAgB,UAAkC,CAAC,GAAS;AACjH,OAAK,KAAK,QAAQ,KAAK,UAAU,OAAO,MAAM,CAAC,GAAG,EAAE,gBAAgB,oBAAoB,GAAG,QAAQ,CAAC;AACtG;AAGO,SAAS,WAAW,MAAsB;AAC/C,MAAI,SAAS,WAAY,QAAO;AAChC,MAAI,SAAS,WAAY,QAAO;AAChC,MAAI,KAAK,WAAW,WAAW,EAAG,QAAO;AACzC,MAAI,KAAK,WAAW,WAAW,EAAG,QAAO;AACzC,MAAI,sBAAsB,KAAK,IAAI,EAAG,QAAO;AAC7C,MAAI,yBAAyB,KAAK,IAAI,EAAG,QAAO;AAChD,MAAI,KAAK,WAAW,KAAK,EAAG,QAAO;AACnC,MAAI,SAAS,kBAAkB,SAAS,gBAAgB,KAAK,WAAW,YAAY,EAAG,QAAO;AAC9F,SAAO;AACT;AAEO,SAAS,cAAc,UAA0B,CAAC,GAAW;AAClE,QAAM,YAAY,QAAQ,YAAY,wBAAwB,QAAQ,QAAQ,EAAE;AAChF,QAAM,OAAO,QAAQ,QAAQ;AAC7B,QAAM,QAAQ,QAAQ,gBAAgB;AACtC,QAAM,UAAmB,QAAQ,YAAY,CAAC,GAAG,MAAM,MAAM,GAAG,CAAC;AACjE,QAAM,YAAY,QAAQ,WAAW,CAAC,6BAA6B,8BAA8B,GAAG;AAAA,IAClG,CAAC,MAAM,IAAI,eAAe,GAAG,OAAO;AAAA,EACtC;AACA,QAAM,WAAW,IAAI,YAAY;AAAA,IAC/B,SAAS,SAAS,IAAI,CAAC,MAAM,EAAE,OAAO;AAAA,IACtC,aAAa,CAAC,QAAQ;AAAA,IACtB;AAAA,IACA,cAAc;AAAA,EAChB,CAAC;AAED,QAAM,gBAAgB,QAAQ,iBAAiB,MAAM,OAAO;AAC5D,QAAM,gBAAgB,QAAQ,sBAAsB,IAAI,OAAO;AAC/D,QAAM,QAAQ,IAAI,QAAQ,eAAe,aAAa;AACtD,QAAM,aAAa,QAAQ,mBAAmB;AAC9C,QAAM,UAAU,IAAI,mBAAmB,YAAY,QAAQ,aAAa,EAAE;AAC1E,QAAM,MAAM,QAAQ,OAAO;AAE3B,QAAM,WAAW,IAAI,SAAS;AAC9B,QAAM,YAAY,SAAS,QAAQ,+BAA+B,sCAAsC;AACxG,QAAM,YAAY,SAAS,UAAU,yCAAyC,iBAAiB;AAC/F,QAAM,aAAa,SAAS,QAAQ,4BAA4B,gBAAgB;AAChF,QAAM,eAAe,SAAS,QAAQ,8BAA8B,0CAA0C;AAC9G,QAAM,eAAe,SAAS,QAAQ,8BAA8B,8CAA8C;AAClH,QAAM,kBAAkB,SAAS,QAAQ,iCAAiC,oCAAoC;AAC9G,WAAS,MAAM,uBAAuB,yBAAyB,MAAM,MAAM,SAAS;AACpF,WAAS,MAAM,yBAAyB,sBAAsB,MAAM,MAAM,IAAI;AAC9E,WAAS,MAAM,iCAAiC,mCAAmC,MAAM,QAAQ,WAAW;AAE5G,WAAS,SAAS,KAA8B;AAC9C,QAAI,QAAQ,YAAY;AACtB,YAAM,MAAM,IAAI,QAAQ,iBAAiB;AACzC,YAAM,SAAS,MAAM,QAAQ,GAAG,IAAI,IAAI,CAAC,IAAI,MAAM,MAAM,GAAG,EAAE,CAAC,GAAG,KAAK;AACvE,UAAI,MAAO,QAAO;AAAA,IACpB;AACA,WAAO,IAAI,OAAO,iBAAiB;AAAA,EACrC;AAGA,WAAS,WACP,KACA,UACA,MACA,SACM;AACN,QAAI,gBAAgB,EAAG,OAAM,IAAI,UAAU,EAAE,QAAQ,KAAK,SAAS,KAAK,CAAC;AACzE,SAAK,KAAK,KAAK,MAAM,EAAE,GAAG,SAAS,WAAW,OAAO,CAAC;AAAA,EACxD;AAEA,iBAAe,MAAM,KAAsB,KAAqB,MAAc,UAAiC;AAC7G,UAAM,MAAM,GAAG,QAAQ,GAAG,IAAI;AAC9B,UAAM,cAAc,MAAM,QAAQ,KAAK;AAAA,MACrC,SAAS,EAAE,mBAAmB,IAAI,QAAQ,iBAAiB,KAAK,WAAW;AAAA,IAC7E,CAAC;AACD,UAAM,UAAkC,CAAC;AACzC,eAAW,QAAQ,CAAC,gBAAgB,oBAAoB,eAAe,GAAG;AACxE,YAAM,IAAI,YAAY,QAAQ,IAAI,IAAI;AACtC,UAAI,EAAG,SAAQ,IAAI,IAAI;AAAA,IACzB;AACA,QAAI,KAAK,WAAW,WAAW,GAAG;AAChC,cAAQ,eAAe,IAAI;AAAA,IAC7B;AACA,UAAM,QAA2C,KAAK,WAAW,WAAW,IACxE,EAAE,2BAA2B,YAAY,IACzC,CAAC;AAEL,UAAM,SAAS,OAAO,YAAY,QAAQ,IAAI,gBAAgB,KAAK,GAAG;AACtE,UAAM,YAAY,YAAY,WAAW,OAAO,CAAC,OAAO,MAAM,MAAM,KAAK,UAAU;AACnF,QAAI,aAAa,CAAC,YAAY,MAAM;AAClC,YAAM,OAAO,IAAI,WAAW,MAAM,YAAY,YAAY,CAAC;AAC3D,UAAI,YAAY,WAAW,KAAK;AAC9B,YAAI,gBAAgB,EAAG,OAAM,IAAI,UAAU,EAAE,QAAQ,KAAK,SAAS,EAAE,GAAG,SAAS,GAAG,KAAK,KAAK,EAAE,GAAG,KAAK,CAAC;AACzG,eAAO,KAAK,KAAK,KAAK,MAAM,EAAE,GAAG,SAAS,GAAG,OAAO,WAAW,OAAO,CAAC;AAAA,MACzE;AACA,aAAO,KAAK,KAAK,YAAY,QAAQ,MAAM,EAAE,GAAG,SAAS,GAAG,MAAM,CAAC;AAAA,IACrE;AAEA,QAAI,UAAU,YAAY,QAAQ,EAAE,+BAA+B,KAAK,GAAG,SAAS,GAAG,MAAM,CAAC;AAC9F,aAAS,QAAQ,YAAY,IAAgD,EAAE,KAAK,GAAG;AACvF,UAAM,IAAI,QAAc,CAAC,YAAY,IAAI,GAAG,SAAS,OAAO,CAAC;AAAA,EAC/D;AAEA,WAAS,KAAK,GAA8D;AAC1E,UAAM,MAA8B,CAAC;AACrC,eAAW,CAAC,GAAG,CAAC,KAAK,OAAO,QAAQ,CAAC,EAAG,KAAI,CAAC,IAAI,MAAM,QAAQ,CAAC,IAAI,EAAE,KAAK,IAAI,IAAI;AACnF,WAAO;AAAA,EACT;AAEA,iBAAe,OAAO,KAAsB,KAAoC;AAC9E,UAAM,MAAM,IAAI,IAAI,IAAI,OAAO,KAAK,sBAAsB;AAC1D,UAAM,OAAO,IAAI;AAEjB,QAAI,IAAI,WAAW,SAAS,IAAI,WAAW,QAAQ;AACjD,aAAO,SAAS,KAAK,KAAK,EAAE,OAAO,qBAAqB,CAAC;AAAA,IAC3D;AAEA,QAAI,SAAS,YAAY;AACvB,aAAO,SAAS,KAAK,KAAK,EAAE,IAAI,MAAM,MAAM,UAAU,SAAS,SAAS,IAAI,CAAC,MAAM,EAAE,OAAO,EAAE,CAAC;AAAA,IACjG;AACA,QAAI,SAAS,YAAY;AACvB,aAAO,KAAK,KAAK,KAAK,SAAS,OAAO,GAAG,EAAE,gBAAgB,4BAA4B,CAAC;AAAA,IAC1F;AAGA,QAAI,aAAa,GAAG;AAClB,YAAM,KAAK,SAAS,GAAG;AACvB,UAAI,CAAC,QAAQ,KAAK,EAAE,GAAG;AACrB,qBAAa,IAAI;AACjB,eAAO,SAAS,KAAK,KAAK,EAAE,OAAO,eAAe,GAAG,EAAE,eAAe,OAAO,QAAQ,kBAAkB,EAAE,CAAC,EAAE,CAAC;AAAA,MAC/G;AAAA,IACF;AAGA,UAAM,WAAW,OAAO,IAAI;AAC5B,QAAI,gBAAgB,GAAG;AACrB,YAAM,MAAM,MAAM,IAAI,QAAQ;AAC9B,UAAI,KAAK;AACP,mBAAW,IAAI;AACf,eAAO,KAAK,KAAK,IAAI,QAAQ,IAAI,MAAM,EAAE,GAAG,IAAI,SAAS,WAAW,MAAM,CAAC;AAAA,MAC7E;AACA,mBAAa,IAAI;AAAA,IACnB;AAGA,UAAM,aAAa,KAAK,MAAM,6BAA6B;AAC3D,QAAI,YAAY;AACd,YAAM,KAAK,WAAW,CAAC;AACvB,UAAI,CAAC,gBAAgB,EAAE,EAAG,QAAO,SAAS,KAAK,KAAK,EAAE,OAAO,2BAA2B,EAAE,GAAG,CAAC;AAC9F,YAAM,UAAU,IAAI,aAAa,IAAI,OAAO,KAAK,MAAM,YAAY,MAAM,OAAO,OAAO;AACvF,UAAI;AACF,cAAM,SAAS,MAAM,YAAY,UAAU,CAAC,MAAM,iBAAiB,GAAG,mBAAmB,EAAE,GAAG,MAAM,CAAC;AACrG,eAAO,WAAW,KAAK,UAAU,IAAI,YAAY,EAAE,OAAO,KAAK,UAAU,MAAM,CAAC,GAAG;AAAA,UACjF,gBAAgB;AAAA,UAChB,iBAAiB;AAAA,QACnB,CAAC;AAAA,MACH,SAAS,GAAG;AACV,wBAAgB,IAAI;AACpB,eAAO,SAAS,KAAK,KAAK,EAAE,OAAQ,EAAY,QAAQ,CAAC;AAAA,MAC3D;AAAA,IACF;AAGA,UAAM,gBAAgB,KAAK,MAAM,gCAAgC;AACjE,UAAM,eAAe,KAAK,MAAM,sBAAsB;AACtD,QAAI,iBAAkB,gBAAgB,SAAS,UAAW;AACxD,YAAM,MAAM,iBAAiB,cAAe,CAAC;AAC7C,UAAI,CAAC,gBAAgB,EAAE,EAAG,QAAO,SAAS,KAAK,KAAK,EAAE,OAAO,2BAA2B,EAAE,GAAG,CAAC;AAC9F,UAAI;AACF,cAAM,SAAS,MAAM,SAAS,QAAQ,OAAO,EAAE,UAAU;AACzD,cAAM,WAAW,WAAW,MAAM;AAClC,cAAM,UAAkC,CAAC;AACzC,iBAAS,QAAQ,QAAQ,CAAC,GAAG,MAAO,QAAQ,CAAC,IAAI,CAAE;AACnD,gBAAQ,yBAAyB,IAAI,YAAY,KAAK,IAAI;AAC1D,eAAO,WAAW,KAAK,UAAU,IAAI,WAAW,MAAM,SAAS,YAAY,CAAC,GAAG,OAAO;AAAA,MACxF,SAAS,GAAG;AACV,wBAAgB,IAAI;AACpB,eAAO,SAAS,KAAK,KAAK,EAAE,OAAQ,EAAY,QAAQ,CAAC;AAAA,MAC3D;AAAA,IACF;AAGA,QACE,gBACA,KAAK,WAAW,KAAK,KACrB,KAAK,WAAW,WAAW,KAC3B,SAAS,kBACT,SAAS,gBACT,KAAK,WAAW,YAAY,GAC5B;AACA,UAAI;AACF,eAAO,MAAM,MAAM,KAAK,KAAK,OAAO,IAAI,QAAQ,QAAQ;AAAA,MAC1D,SAAS,GAAG;AACV,wBAAgB,IAAI;AACpB,eAAO,SAAS,KAAK,KAAK,EAAE,OAAQ,EAAY,QAAQ,CAAC;AAAA,MAC3D;AAAA,IACF;AAEA,WAAO,SAAS,KAAK,KAAK;AAAA,MACxB,OAAO;AAAA,MACP,QAAQ;AAAA,QACN;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MACF;AAAA,IACF,CAAC;AAAA,EACH;AAEA,SAAO,aAAa,CAAC,KAAK,QAAQ;AAChC,UAAM,UAAU,YAAY,IAAI;AAChC,QAAI,GAAG,UAAU,MAAM;AACrB,YAAM,WAAW,YAAY,IAAI,IAAI,WAAW;AAChD,YAAM,QAAQ,WAAW,IAAI,IAAI,IAAI,OAAO,KAAK,UAAU,EAAE,QAAQ;AACrE,gBAAU,IAAI,EAAE,OAAO,QAAQ,IAAI,UAAU,OAAO,QAAQ,OAAO,IAAI,UAAU,EAAE,CAAC;AACpF,gBAAU,QAAQ,EAAE,MAAM,GAAG,OAAO;AACpC,YAAM;AAAA,QACJ,IAAG,oBAAI,KAAK,GAAE,YAAY;AAAA,QAC1B,KAAK;AAAA,QACL,IAAI,SAAS,GAAG;AAAA,QAChB,QAAQ,IAAI;AAAA,QACZ,MAAM,IAAI;AAAA,QACV,QAAQ,IAAI;AAAA,QACZ,IAAI,KAAK,MAAM,UAAU,GAAI;AAAA,QAC7B,OAAO,IAAI,UAAU,SAAS,KAAK;AAAA,QACnC,OAAO,OAAO,IAAI,UAAU,gBAAgB,KAAK,CAAC,KAAK;AAAA,MACzD,CAAC;AAAA,IACH,CAAC;AACD,WAAO,KAAK,GAAG,EAAE,MAAM,CAAC,MAAM,SAAS,KAAK,KAAK,EAAE,OAAQ,EAAY,QAAQ,CAAC,CAAC;AAAA,EACnF,CAAC;AACH;AAEA,eAAe,YAAe,UAA4B,IAAmD;AAC3G,QAAM,SAAmB,CAAC;AAC1B,aAAW,KAAK,UAAU;AACxB,QAAI;AACF,aAAO,MAAM,GAAG,CAAC;AAAA,IACnB,SAAS,GAAG;AACV,aAAO,KAAK,GAAG,EAAE,OAAO,KAAM,EAAY,OAAO,EAAE;AAAA,IACrD;AAAA,EACF;AACA,QAAM,IAAI,MAAM,wBAAwB,OAAO,KAAK,IAAI,CAAC,EAAE;AAC7D;AAGO,SAAS,gBAAgB,QAAgB,UAAU,KAAQ,KAAkD;AAClH,MAAI,UAAU;AACd,QAAM,QAAQ,CAAC,WAAmB;AAChC,QAAI,QAAS;AACb,cAAU;AACV,UAAM,EAAE,IAAG,oBAAI,KAAK,GAAE,YAAY,GAAG,KAAK,YAAY,QAAQ,QAAQ,CAAC;AACvE,WAAO,MAAM,MAAM,QAAQ,KAAK,CAAC,CAAC;AAClC,WAAO,qBAAqB;AAC5B,eAAW,MAAM;AACf,aAAO,oBAAoB;AAC3B,cAAQ,KAAK,CAAC;AAAA,IAChB,GAAG,OAAO,EAAE,MAAM;AAAA,EACpB;AACA,UAAQ,GAAG,WAAW,MAAM,MAAM,SAAS,CAAC;AAC5C,UAAQ,GAAG,UAAU,MAAM,MAAM,QAAQ,CAAC;AAC5C;AAGA,IAAI,YAAY,QAAQ,UAAU,QAAQ,KAAK,CAAC,CAAC,IAAI;AACnD,QAAM,OAAO,OAAO,QAAQ,IAAI,QAAQ,IAAI;AAC5C,QAAM,MAAM,CAAC,SAAkC,QAAQ,IAAI,KAAK,UAAU,IAAI,CAAC;AAC/E,QAAM,UAAU,cAAc;AAAA,IAC5B;AAAA,IACA,UAAU,QAAQ,IAAI;AAAA,IACtB,SAAS,QAAQ,IAAI,SAAS,MAAM,GAAG;AAAA,IACvC,MAAO,QAAQ,IAAI,gBAAuC;AAAA,IAC1D,cAAc,QAAQ,IAAI,kBAAkB,OAAO,OAAO;AAAA,IAC1D,eAAe,QAAQ,IAAI,kBAAkB,OAAO,QAAQ,IAAI,eAAe,IAAI;AAAA,IACnF,oBAAoB,QAAQ,IAAI,wBAAwB,OAAO,QAAQ,IAAI,qBAAqB,IAAI;AAAA,IACpG,iBAAiB,QAAQ,IAAI,aAAa,OAAO,QAAQ,IAAI,UAAU,IAAI;AAAA,IAC3E,WAAW,QAAQ,IAAI,aAAa,OAAO,QAAQ,IAAI,UAAU,IAAI;AAAA,IACrE,YAAY,QAAQ,IAAI,gBAAgB;AAAA,IACxC;AAAA,EACF,CAAC;AACD,kBAAgB,SAAS,QAAW,GAAG;AACvC,UAAQ,OAAO,MAAM,MAAM;AACzB,QAAI,EAAE,IAAG,oBAAI,KAAK,GAAE,YAAY,GAAG,KAAK,aAAa,MAAM,MAAM,QAAQ,IAAI,gBAAgB,QAAQ,CAAC;AAAA,EACxG,CAAC;AACH;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/lru.ts","../src/metrics.ts","../src/ratelimit.ts"],"sourcesContent":["import { createServer, type IncomingMessage, type Server, type ServerResponse } from 'node:http';\nimport { isIP } from 'node:net';\nimport { Readable } from 'node:stream';\nimport { isInscriptionId, parseInscriptionId } from '@ordspv/core';\nimport {\n buildProofBundle,\n EsploraBackend,\n OrdResolver,\n readBodyCapped,\n toResponse,\n type FetchFn,\n} from '@ordspv/fetch';\nimport { ByteLru } from './lru.js';\nimport { Registry } from './metrics.js';\nimport { TokenBucketLimiter } from './ratelimit.js';\n\nexport { ByteLru } from './lru.js';\nexport { Counter, Histogram, Registry } from './metrics.js';\nexport { TokenBucketLimiter } from './ratelimit.js';\n\n/**\n * Reference ord gateway (SPEC-GATEWAY.md).\n *\n * Personalities:\n * - proxy : replicate an upstream ord server's /content and /r/* surface\n * with ord-parity headers. Availability play only; adds no trust.\n * - verify : serve /content only after locally verifying the bytes against\n * Bitcoin (L2 by default) via esplora backends.\n *\n * Both serve: GET /ord/v1/proof/<id>?level=l2|l3, GET /ord/v1/verified/<id>,\n * GET /healthz, GET /metrics (prometheus text)\n *\n * Operational features (SPEC-GATEWAY §7): bounded byte-budget LRU on\n * immutable 200s (x-cache header), per-IP token-bucket rate limiting,\n * streaming proxy for oversized bodies, structured JSON request logs,\n * graceful shutdown. Verified responses are buffered, because a merkle proof\n * cannot be verified over bytes that have not all been read.\n */\n\nexport interface GatewayOptions {\n port?: number;\n upstream?: string;\n esplora?: string[];\n mode?: 'proxy' | 'verify';\n verification?: 'L2' | 'L3';\n fetchFn?: FetchFn;\n /** LRU budget across cached bodies (default 256 MiB; 0 disables) */\n cacheMaxBytes?: number;\n /** largest single cacheable body (default 8 MiB) */\n cacheMaxEntryBytes?: number;\n /** sustained requests/second per IP (default 10; 0 disables) */\n rateLimitPerSec?: number;\n /** burst size per IP (default 40) */\n rateBurst?: number;\n /**\n * Behind a load balancer / CDN: number of TRUSTED proxy hops in front of\n * this gateway (true = 1). The client IP is taken from the right of\n * X-Forwarded-For — the entries appended by your own proxies — never from\n * the client-controlled left end, and must parse as an IP address\n * (otherwise the socket address is used).\n */\n trustProxy?: boolean | number;\n /** structured log sink; false silences (default). CLI wires console.log. */\n log?: ((line: Record<string, unknown>) => void) | false;\n}\n\n// ord-parity security headers for /content (see research: ord server.rs)\nconst CONTENT_CSP = [\n \"default-src 'self' 'unsafe-eval' 'unsafe-inline' data: blob:\",\n \"default-src *:*/content/ *:*/blockheight *:*/blockhash *:*/blockhash/ *:*/blocktime *:*/r/ 'unsafe-eval' 'unsafe-inline' data: blob:\",\n];\nconst IMMUTABLE = 'public, max-age=1209600, immutable';\n\nfunction send(\n res: ServerResponse,\n status: number,\n body: string | Uint8Array,\n headers: Record<string, string | string[]> = {},\n): void {\n res.writeHead(status, { 'access-control-allow-origin': '*', ...headers });\n res.end(typeof body === 'string' ? body : Buffer.from(body));\n}\n\nfunction sendJson(res: ServerResponse, status: number, value: unknown, headers: Record<string, string> = {}): void {\n send(res, status, JSON.stringify(value, null, 2), { 'content-type': 'application/json', ...headers });\n}\n\n/** drop a :port suffix from an X-Forwarded-For entry (IPv4:port / [IPv6]:port) */\nfunction stripPort(entry: string): string {\n if (entry.startsWith('[')) {\n const end = entry.indexOf(']');\n return end === -1 ? entry : entry.slice(1, end);\n }\n const parts = entry.split(':');\n return parts.length === 2 ? parts[0] : entry;\n}\n\n/** low-cardinality route label for metrics/logs */\nexport function routeLabel(path: string): string {\n if (path === '/healthz') return 'healthz';\n if (path === '/metrics') return 'metrics';\n if (path.startsWith('/content/')) return '/content/:id';\n if (path.startsWith('/preview/')) return '/preview/:id';\n if (/^\\/ord\\/v1\\/proof\\//.test(path)) return '/ord/v1/proof/:id';\n if (/^\\/ord\\/v1\\/verified\\//.test(path)) return '/ord/v1/verified/:id';\n if (path.startsWith('/r/')) return '/r/*';\n if (path === '/blockheight' || path === '/blocktime' || path.startsWith('/blockhash')) return '/chain';\n return 'other';\n}\n\nexport function createGateway(options: GatewayOptions = {}): Server {\n const upstream = (options.upstream ?? 'https://ordinals.com').replace(/\\/+$/, '');\n const mode = options.mode ?? 'proxy';\n const level = options.verification ?? 'L2';\n const fetchFn: FetchFn = options.fetchFn ?? ((u, i) => fetch(u, i));\n const esploras = (options.esplora ?? ['https://mempool.space/api', 'https://blockstream.info/api']).map(\n (u) => new EsploraBackend(u, fetchFn),\n );\n const resolver = new OrdResolver({\n esplora: esploras.map((e) => e.baseUrl),\n ordGateways: [upstream],\n fetchFn,\n verification: level,\n });\n\n const cacheMaxBytes = options.cacheMaxBytes ?? 256 * 1024 * 1024;\n const cacheMaxEntry = options.cacheMaxEntryBytes ?? 8 * 1024 * 1024;\n const cache = new ByteLru(cacheMaxBytes, cacheMaxEntry);\n const ratePerSec = options.rateLimitPerSec ?? 10;\n const limiter = new TokenBucketLimiter(ratePerSec, options.rateBurst ?? 40);\n const log = options.log || undefined;\n\n const registry = new Registry();\n const mRequests = registry.counter('gateway_http_requests_total', 'HTTP requests by route/method/status');\n const mDuration = registry.histogram('gateway_http_request_duration_seconds', 'request latency');\n const mCacheHits = registry.counter('gateway_cache_hits_total', 'LRU cache hits');\n const mCacheMisses = registry.counter('gateway_cache_misses_total', 'LRU cache misses (cacheable routes only)');\n const mRateLimited = registry.counter('gateway_rate_limited_total', 'requests rejected by the per-IP token bucket');\n const mUpstreamErrors = registry.counter('gateway_upstream_errors_total', 'errors talking to upstream/esplora');\n registry.gauge('gateway_cache_bytes', 'bytes held by the LRU', () => cache.usedBytes);\n registry.gauge('gateway_cache_entries', 'entries in the LRU', () => cache.size);\n registry.gauge('gateway_ratelimit_tracked_ips', 'token buckets currently tracked', () => limiter.trackedKeys);\n\n const trustedHops =\n options.trustProxy === true ? 1 : typeof options.trustProxy === 'number' ? options.trustProxy : 0;\n\n function clientIp(req: IncomingMessage): string {\n if (trustedHops > 0) {\n const xff = req.headers['x-forwarded-for'];\n const joined = Array.isArray(xff) ? xff.join(',') : xff;\n const entries = (joined ?? '')\n .split(',')\n .map((s) => s.trim())\n .filter((s) => s.length > 0);\n // the rightmost `trustedHops` entries were appended by our own proxies;\n // the client is the entry those hops vouch for. Leftmost entries are\n // attacker-controlled and never used.\n const candidate = entries[Math.max(0, entries.length - trustedHops)];\n if (candidate !== undefined) {\n const ip = stripPort(candidate);\n if (isIP(ip) !== 0) return ip;\n }\n }\n return req.socket.remoteAddress ?? 'unknown';\n }\n\n /** cache + send an immutable 200 */\n function sendCached(\n res: ServerResponse,\n cacheKey: string,\n body: Uint8Array,\n headers: Record<string, string>,\n ): void {\n if (cacheMaxBytes > 0) cache.set(cacheKey, { status: 200, headers, body });\n send(res, 200, body, { ...headers, 'x-cache': 'MISS' });\n }\n\n async function proxy(req: IncomingMessage, res: ServerResponse, path: string, cacheKey: string): Promise<void> {\n const url = `${upstream}${path}`;\n const upstreamRes = await fetchFn(url, {\n headers: { 'accept-encoding': req.headers['accept-encoding'] ?? 'identity' },\n });\n const headers: Record<string, string> = {};\n for (const name of ['content-type', 'content-encoding', 'cache-control']) {\n const v = upstreamRes.headers.get(name);\n if (v) headers[name] = v;\n }\n if (path.startsWith('/content/')) {\n headers['cache-control'] = IMMUTABLE;\n }\n const extra: Record<string, string | string[]> = path.startsWith('/content/')\n ? { 'content-security-policy': CONTENT_CSP }\n : {};\n\n const length = Number(upstreamRes.headers.get('content-length') ?? NaN);\n const cacheable = upstreamRes.status === 200 && !Number.isNaN(length) && length <= cacheMaxEntry;\n if (cacheable || !upstreamRes.body) {\n // Content-Length may lie: the buffered read is capped regardless\n const body = await readBodyCapped(upstreamRes, cacheMaxEntry, url);\n if (upstreamRes.status === 200) {\n if (cacheMaxBytes > 0) cache.set(cacheKey, { status: 200, headers: { ...headers, ...flat(extra) }, body });\n return send(res, 200, body, { ...headers, ...extra, 'x-cache': 'MISS' });\n }\n return send(res, upstreamRes.status, body, { ...headers, ...extra });\n }\n // large or unknown-length body: stream through, uncached\n res.writeHead(upstreamRes.status, { 'access-control-allow-origin': '*', ...headers, ...extra });\n Readable.fromWeb(upstreamRes.body as import('node:stream/web').ReadableStream).pipe(res);\n await new Promise<void>((resolve) => res.on('close', resolve));\n }\n\n function flat(h: Record<string, string | string[]>): Record<string, string> {\n const out: Record<string, string> = {};\n for (const [k, v] of Object.entries(h)) out[k] = Array.isArray(v) ? v.join(', ') : v;\n return out;\n }\n\n /**\n * Cache keys are derived from canonicalized route inputs, never from the\n * raw query string: unknown parameters would otherwise mint unlimited\n * distinct entries for the same immutable response (cache-busting).\n */\n function cacheKeyFor(path: string, url: URL): string {\n if (/^\\/ord\\/v1\\/proof\\//.test(path)) {\n const level = (url.searchParams.get('level') ?? 'l2').toUpperCase() === 'L3' ? 'l3' : 'l2';\n return `${path}?level=${level}`;\n }\n return path;\n }\n\n async function handle(req: IncomingMessage, res: ServerResponse): Promise<void> {\n const url = new URL(req.url ?? '/', 'http://gateway.local');\n const path = url.pathname;\n\n if (req.method !== 'GET' && req.method !== 'HEAD') {\n return sendJson(res, 405, { error: 'method not allowed' });\n }\n\n if (path === '/healthz') {\n return sendJson(res, 200, { ok: true, mode, upstream, esplora: esploras.map((e) => e.baseUrl) });\n }\n if (path === '/metrics') {\n return send(res, 200, registry.render(), { 'content-type': 'text/plain; version=0.0.4' });\n }\n\n // rate limit everything else\n if (ratePerSec > 0) {\n const ip = clientIp(req);\n if (!limiter.take(ip)) {\n mRateLimited.inc();\n return sendJson(res, 429, { error: 'rate limited' }, { 'retry-after': String(limiter.retryAfterSeconds(ip)) });\n }\n }\n\n // immutable-response cache (canonicalized key; see cacheKeyFor)\n const cacheKey = cacheKeyFor(path, url);\n if (cacheMaxBytes > 0) {\n const hit = cache.get(cacheKey);\n if (hit) {\n mCacheHits.inc();\n return send(res, hit.status, hit.body, { ...hit.headers, 'x-cache': 'HIT' });\n }\n mCacheMisses.inc();\n }\n\n // proof bundles\n const proofMatch = path.match(/^\\/ord\\/v1\\/proof\\/([^/]+)$/);\n if (proofMatch) {\n const id = proofMatch[1];\n if (!isInscriptionId(id)) return sendJson(res, 400, { error: `invalid inscription id: ${id}` });\n const wanted = (url.searchParams.get('level') ?? 'l2').toUpperCase() === 'L3' ? 'L3' : 'L2';\n try {\n const bundle = await tryBackends(esploras, (e) => buildProofBundle(e, parseInscriptionId(id), wanted));\n return sendCached(res, cacheKey, new TextEncoder().encode(JSON.stringify(bundle)), {\n 'content-type': 'application/vnd.ord.proof+json; version=1',\n 'cache-control': IMMUTABLE,\n });\n } catch (e) {\n mUpstreamErrors.inc();\n return sendJson(res, 502, { error: (e as Error).message });\n }\n }\n\n // verified content (also the verify-mode /content handler)\n const verifiedMatch = path.match(/^\\/ord\\/v1\\/verified\\/([^/]+)$/);\n const contentMatch = path.match(/^\\/content\\/([^/]+)$/);\n if (verifiedMatch || (contentMatch && mode === 'verify')) {\n const id = (verifiedMatch ?? contentMatch)![1];\n if (!isInscriptionId(id)) return sendJson(res, 400, { error: `invalid inscription id: ${id}` });\n try {\n const result = await resolver.resolve(`ord:${id}/content`);\n const response = toResponse(result);\n const headers: Record<string, string> = {};\n response.headers.forEach((v, k) => (headers[k] = v));\n headers['content-security-policy'] = CONTENT_CSP.join(', ');\n return sendCached(res, cacheKey, new Uint8Array(await response.arrayBuffer()), headers);\n } catch (e) {\n mUpstreamErrors.inc();\n return sendJson(res, 502, { error: (e as Error).message });\n }\n }\n\n // ord-server surface passthrough (recursion compatibility)\n if (\n contentMatch ||\n path.startsWith('/r/') ||\n path.startsWith('/preview/') ||\n path === '/blockheight' ||\n path === '/blocktime' ||\n path.startsWith('/blockhash')\n ) {\n try {\n // the ord passthrough surface takes no query parameters: forward the\n // pathname only, so the cached body always matches the canonical\n // upstream response for that path\n return await proxy(req, res, path, cacheKey);\n } catch (e) {\n mUpstreamErrors.inc();\n return sendJson(res, 502, { error: (e as Error).message });\n }\n }\n\n return sendJson(res, 404, {\n error: 'not found',\n routes: [\n '/content/<id>',\n '/r/*',\n '/ord/v1/proof/<id>?level=l2|l3',\n '/ord/v1/verified/<id>',\n '/healthz',\n '/metrics',\n ],\n });\n }\n\n return createServer((req, res) => {\n const started = performance.now();\n res.on('finish', () => {\n const seconds = (performance.now() - started) / 1000;\n const route = routeLabel(new URL(req.url ?? '/', 'http://x').pathname);\n mRequests.inc({ route, method: req.method ?? 'GET', status: String(res.statusCode) });\n mDuration.observe({ route }, seconds);\n log?.({\n t: new Date().toISOString(),\n msg: 'req',\n ip: clientIp(req),\n method: req.method,\n path: req.url,\n status: res.statusCode,\n ms: Math.round(seconds * 1000),\n cache: res.getHeader('x-cache') ?? undefined,\n bytes: Number(res.getHeader('content-length') ?? 0) || undefined,\n });\n });\n handle(req, res).catch((e) => sendJson(res, 500, { error: (e as Error).message }));\n });\n}\n\nasync function tryBackends<T>(backends: EsploraBackend[], fn: (e: EsploraBackend) => Promise<T>): Promise<T> {\n const errors: string[] = [];\n for (const b of backends) {\n try {\n return await fn(b);\n } catch (e) {\n errors.push(`${b.baseUrl}: ${(e as Error).message}`);\n }\n }\n throw new Error(`all backends failed: ${errors.join('; ')}`);\n}\n\n/** graceful shutdown: stop accepting, drain in-flight, force-close after grace */\nexport function installShutdown(server: Server, graceMs = 10_000, log?: (l: Record<string, unknown>) => void): void {\n let closing = false;\n const close = (signal: string) => {\n if (closing) return;\n closing = true;\n log?.({ t: new Date().toISOString(), msg: 'shutdown', signal, graceMs });\n server.close(() => process.exit(0));\n server.closeIdleConnections();\n setTimeout(() => {\n server.closeAllConnections();\n process.exit(0);\n }, graceMs).unref();\n };\n process.on('SIGTERM', () => close('SIGTERM'));\n process.on('SIGINT', () => close('SIGINT'));\n}\n\n/** CLI entry: npx tsx packages/gateway/src/index.ts */\nif (import.meta.url === `file://${process.argv[1]}`) {\n const port = Number(process.env.PORT ?? 8317);\n const log = (line: Record<string, unknown>) => console.log(JSON.stringify(line));\n const gateway = createGateway({\n port,\n upstream: process.env.ORD_UPSTREAM,\n esplora: process.env.ESPLORA?.split(','),\n mode: (process.env.GATEWAY_MODE as 'proxy' | 'verify') ?? 'proxy',\n verification: process.env.GATEWAY_LEVEL === 'L3' ? 'L3' : 'L2',\n cacheMaxBytes: process.env.CACHE_MAX_BYTES ? Number(process.env.CACHE_MAX_BYTES) : undefined,\n cacheMaxEntryBytes: process.env.CACHE_MAX_ENTRY_BYTES ? Number(process.env.CACHE_MAX_ENTRY_BYTES) : undefined,\n rateLimitPerSec: process.env.RATE_LIMIT ? Number(process.env.RATE_LIMIT) : undefined,\n rateBurst: process.env.RATE_BURST ? Number(process.env.RATE_BURST) : undefined,\n // TRUST_PROXY = number of trusted proxy hops (1 for a single LB/CDN)\n trustProxy: process.env.TRUST_PROXY ? Number(process.env.TRUST_PROXY) || 0 : undefined,\n log,\n });\n installShutdown(gateway, undefined, log);\n gateway.listen(port, () => {\n log({ t: new Date().toISOString(), msg: 'listening', port, mode: process.env.GATEWAY_MODE ?? 'proxy' });\n });\n}\n","/**\n * Byte-budgeted LRU for immutable responses. Everything a gateway serves with\n * `immutable` cache semantics is safe to cache forever; the only pressure is\n * memory, so the budget is BYTES, not entries. Map iteration order gives us\n * recency for free (delete + re-set on hit).\n */\n\nexport interface CachedResponse {\n status: number;\n headers: Record<string, string>;\n body: Uint8Array;\n}\n\nexport class ByteLru {\n private readonly entries = new Map<string, CachedResponse>();\n private bytes = 0;\n hits = 0;\n misses = 0;\n\n constructor(\n /** total budget across all bodies */\n private readonly maxBytes: number,\n /** refuse to cache any single body larger than this */\n private readonly maxEntryBytes: number,\n ) {}\n\n get(key: string): CachedResponse | undefined {\n const hit = this.entries.get(key);\n if (!hit) {\n this.misses++;\n return undefined;\n }\n // refresh recency\n this.entries.delete(key);\n this.entries.set(key, hit);\n this.hits++;\n return hit;\n }\n\n set(key: string, value: CachedResponse): void {\n if (value.body.length > this.maxEntryBytes) return;\n const existing = this.entries.get(key);\n if (existing) {\n this.bytes -= existing.body.length;\n this.entries.delete(key);\n }\n this.entries.set(key, value);\n this.bytes += value.body.length;\n // evict least-recently-used until inside budget\n for (const [oldest, entry] of this.entries) {\n if (this.bytes <= this.maxBytes) break;\n if (oldest === key && this.entries.size === 1) break;\n this.entries.delete(oldest);\n this.bytes -= entry.body.length;\n }\n }\n\n get size(): number {\n return this.entries.size;\n }\n\n get usedBytes(): number {\n return this.bytes;\n }\n}\n","/**\n * Hand-rolled Prometheus text-format (0.0.4) registry: counters and\n * histograms with a small fixed label space. No dependency, no magic;\n * exactly what /metrics needs and nothing more.\n */\n\ntype Labels = Record<string, string>;\n\nfunction labelKey(labels: Labels): string {\n return Object.keys(labels)\n .sort()\n .map((k) => `${k}=${JSON.stringify(labels[k])}`)\n .join(',');\n}\n\nfunction renderLabels(labels: Labels, extra?: string): string {\n const parts = Object.keys(labels)\n .sort()\n .map((k) => `${k}=${JSON.stringify(labels[k])}`);\n if (extra) parts.push(extra);\n return parts.length ? `{${parts.join(',')}}` : '';\n}\n\nexport class Counter {\n private readonly series = new Map<string, { labels: Labels; value: number }>();\n constructor(\n readonly name: string,\n readonly help: string,\n ) {}\n\n inc(labels: Labels = {}, by = 1): void {\n const key = labelKey(labels);\n const entry = this.series.get(key) ?? { labels, value: 0 };\n entry.value += by;\n this.series.set(key, entry);\n }\n\n render(): string {\n const lines = [`# HELP ${this.name} ${this.help}`, `# TYPE ${this.name} counter`];\n if (this.series.size === 0) lines.push(`${this.name} 0`);\n for (const { labels, value } of this.series.values()) {\n lines.push(`${this.name}${renderLabels(labels)} ${value}`);\n }\n return lines.join('\\n');\n }\n}\n\nexport class Histogram {\n private readonly series = new Map<string, { labels: Labels; buckets: number[]; sum: number; count: number }>();\n constructor(\n readonly name: string,\n readonly help: string,\n readonly bounds: number[] = [0.005, 0.025, 0.1, 0.25, 1, 2.5, 10, 30],\n ) {}\n\n observe(labels: Labels, value: number): void {\n const key = labelKey(labels);\n let entry = this.series.get(key);\n if (!entry) {\n entry = { labels, buckets: this.bounds.map(() => 0), sum: 0, count: 0 };\n this.series.set(key, entry);\n }\n for (let i = 0; i < this.bounds.length; i++) {\n if (value <= this.bounds[i]) entry.buckets[i]++;\n }\n entry.sum += value;\n entry.count++;\n }\n\n render(): string {\n const lines = [`# HELP ${this.name} ${this.help}`, `# TYPE ${this.name} histogram`];\n for (const { labels, buckets, sum, count } of this.series.values()) {\n this.bounds.forEach((bound, i) => {\n lines.push(`${this.name}_bucket${renderLabels(labels, `le=\"${bound}\"`)} ${buckets[i]}`);\n });\n lines.push(`${this.name}_bucket${renderLabels(labels, 'le=\"+Inf\"')} ${count}`);\n lines.push(`${this.name}_sum${renderLabels(labels)} ${sum}`);\n lines.push(`${this.name}_count${renderLabels(labels)} ${count}`);\n }\n return lines.join('\\n');\n }\n}\n\nexport class Registry {\n private readonly metrics: Array<Counter | Histogram> = [];\n private readonly gauges: Array<{ name: string; help: string; read: () => number }> = [];\n\n counter(name: string, help: string): Counter {\n const c = new Counter(name, help);\n this.metrics.push(c);\n return c;\n }\n\n histogram(name: string, help: string, bounds?: number[]): Histogram {\n const h = new Histogram(name, help, bounds);\n this.metrics.push(h);\n return h;\n }\n\n /** callback gauge, sampled at render time (cache bytes, tracked IPs, …) */\n gauge(name: string, help: string, read: () => number): void {\n this.gauges.push({ name, help, read });\n }\n\n render(): string {\n const parts = this.metrics.map((m) => m.render());\n for (const g of this.gauges) {\n parts.push(`# HELP ${g.name} ${g.help}\\n# TYPE ${g.name} gauge\\n${g.name} ${g.read()}`);\n }\n return `${parts.join('\\n')}\\n`;\n }\n}\n","/**\n * Per-key token bucket: `ratePerSec` sustained, `burst` peak. Injectable\n * clock for tests. Idle buckets are swept, and the tracked-key count is\n * hard-capped (oldest evicted first), so hostile key churn cannot grow the\n * map without bound between sweeps.\n */\n\ninterface Bucket {\n tokens: number;\n lastRefill: number;\n}\n\nexport const DEFAULT_MAX_TRACKED_KEYS = 50_000;\n\nexport class TokenBucketLimiter {\n private readonly buckets = new Map<string, Bucket>();\n private lastSweep: number;\n\n constructor(\n private readonly ratePerSec: number,\n private readonly burst: number,\n private readonly now: () => number = () => Date.now(),\n private readonly maxTrackedKeys: number = DEFAULT_MAX_TRACKED_KEYS,\n ) {\n this.lastSweep = this.now();\n }\n\n /** true = allowed (a token was taken); false = rate limited */\n take(key: string): boolean {\n const t = this.now();\n this.maybeSweep(t);\n let bucket = this.buckets.get(key);\n if (!bucket) {\n // hard cap: evict the oldest-tracked bucket rather than grow unbounded\n if (this.buckets.size >= this.maxTrackedKeys) {\n const oldest = this.buckets.keys().next().value;\n if (oldest !== undefined) this.buckets.delete(oldest);\n }\n bucket = { tokens: this.burst, lastRefill: t };\n this.buckets.set(key, bucket);\n } else {\n const elapsed = (t - bucket.lastRefill) / 1000;\n bucket.tokens = Math.min(this.burst, bucket.tokens + elapsed * this.ratePerSec);\n bucket.lastRefill = t;\n }\n if (bucket.tokens < 1) return false;\n bucket.tokens -= 1;\n return true;\n }\n\n /** seconds until one token is available (for retry-after) */\n retryAfterSeconds(key: string): number {\n const bucket = this.buckets.get(key);\n if (!bucket || bucket.tokens >= 1) return 0;\n return Math.ceil((1 - bucket.tokens) / this.ratePerSec);\n }\n\n private maybeSweep(t: number): void {\n if (t - this.lastSweep < 60_000) return;\n this.lastSweep = t;\n const idleCutoff = t - 120_000;\n for (const [key, bucket] of this.buckets) {\n // effective tokens: what the bucket WOULD hold if refilled right now;\n // an idle bucket is safe to drop once it is logically full again\n const effective = bucket.tokens + ((t - bucket.lastRefill) / 1000) * this.ratePerSec;\n if (bucket.lastRefill < idleCutoff && effective >= this.burst) {\n this.buckets.delete(key);\n }\n }\n }\n\n get trackedKeys(): number {\n return this.buckets.size;\n }\n}\n"],"mappings":"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","names":["level"]}
|
package/dist/ratelimit.d.ts
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Per-key token bucket: `ratePerSec` sustained, `burst` peak. Injectable
|
|
3
|
-
* clock for tests. Idle buckets are swept
|
|
4
|
-
*
|
|
3
|
+
* clock for tests. Idle buckets are swept, and the tracked-key count is
|
|
4
|
+
* hard-capped (oldest evicted first), so hostile key churn cannot grow the
|
|
5
|
+
* map without bound between sweeps.
|
|
5
6
|
*/
|
|
7
|
+
export declare const DEFAULT_MAX_TRACKED_KEYS = 50000;
|
|
6
8
|
export declare class TokenBucketLimiter {
|
|
7
9
|
private readonly ratePerSec;
|
|
8
10
|
private readonly burst;
|
|
9
11
|
private readonly now;
|
|
12
|
+
private readonly maxTrackedKeys;
|
|
10
13
|
private readonly buckets;
|
|
11
14
|
private lastSweep;
|
|
12
|
-
constructor(ratePerSec: number, burst: number, now?: () => number);
|
|
15
|
+
constructor(ratePerSec: number, burst: number, now?: () => number, maxTrackedKeys?: number);
|
|
13
16
|
/** true = allowed (a token was taken); false = rate limited */
|
|
14
17
|
take(key: string): boolean;
|
|
15
18
|
/** seconds until one token is available (for retry-after) */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ordspv/gateway",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "HTTP gateway serving ord:// content and SPV proof bundles.",
|
|
6
6
|
"license": "ISC",
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"main": "./dist/index.js",
|
|
18
18
|
"types": "./dist/index.d.ts",
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@ordspv/core": "0.
|
|
21
|
-
"@ordspv/fetch": "0.
|
|
20
|
+
"@ordspv/core": "0.2.0",
|
|
21
|
+
"@ordspv/fetch": "0.2.0"
|
|
22
22
|
}
|
|
23
23
|
}
|