@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 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
- /** trust the first X-Forwarded-For hop for the client IP (behind a LB/CDN) */
40
- trustProxy?: boolean;
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 (options.trustProxy) {
281
+ if (trustedHops > 0) {
264
282
  const xff = req.headers["x-forwarded-for"];
265
- const first = (Array.isArray(xff) ? xff[0] : xff)?.split(",")[0]?.trim();
266
- if (first) return first;
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 = new Uint8Array(await upstreamRes.arrayBuffer());
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 + url.search;
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 + url.search, cacheKey);
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
- trustProxy: process.env.TRUST_PROXY === "1",
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":"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","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"]}
@@ -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 so hostile IP churn cannot grow the
4
- * map without bound.
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.1.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.1.0",
21
- "@ordspv/fetch": "0.1.0"
20
+ "@ordspv/core": "0.2.0",
21
+ "@ordspv/fetch": "0.2.0"
22
22
  }
23
23
  }