@camada/node 0.3.2 → 0.4.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.
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/camada.ts","../src/env.ts","../src/challenge.ts","../package.json","../src/version.ts"],"sourcesContent":["// Quickstart (Express / Connect / Nest-on-Express):\n// import camada from '@camada/node';\n// app.use(camada.express()); // env: CAMADA_KEY (+ CAMADA_INGEST_URL for dev)\n// The default export is a lazy singleton wired from the environment on first use.\nimport type { IncomingMessage, Server, ServerResponse } from 'node:http';\nimport { Camada, createCamada, type CamadaOptions } from './camada.js';\n\nexport { Camada, createCamada, type CamadaOptions } from './camada.js';\nexport { resolveEnv, parseTrustedProxyEnv, type ResolvedEnv } from './env.js';\n\nlet singleton: Camada | null = null;\nexport function getDefault(): Camada {\n return (singleton ??= createCamada());\n}\n/** Test/reset hook: replaces the singleton (stops the old one). */\nexport function configure(opts: CamadaOptions): Camada {\n singleton?.stop();\n singleton = createCamada(opts);\n return singleton;\n}\n\nexport default {\n express: () => (req: IncomingMessage, res: ServerResponse, next: (err?: unknown) => void) => { if (!getDefault().handle(req, res)) next(); },\n handle: (req: IncomingMessage, res: ServerResponse) => getDefault().handle(req, res),\n scriptTag: (req: IncomingMessage) => getDefault().scriptTag(req),\n serveChallenge: (req: IncomingMessage, res: ServerResponse) => getDefault().serveChallenge(req, res),\n track: (req: IncomingMessage, event: string, data?: { user?: string }) => getDefault().track(req, event, data),\n attach: <S extends Server>(server: S): S => getDefault().attach(server),\n configure,\n};\n","// The engine: node:http-level request handling every adapter delegates to.\n// camada.handle(req, res) -> true when camada fully answered the request (block or beacon\n// endpoint), false when the app should proceed. Everything runs inside the fail-open\n// envelope: a camada bug must never 5xx the customer (plan.md INT-2), and CAMADA_DISABLED=1\n// bypasses the SDK entirely.\nimport type { IncomingMessage, Server, ServerResponse } from 'node:http';\nimport { randomUUID, webcrypto } from 'node:crypto'; // webcrypto handed to hashUserId explicitly, so the hash never depends on which global the host exposes\nimport {\n SnapshotClient, EventQueue, buildWireEvent, resolveClientIp, hashUserId, guarded, logRateLimited,\n challengeCookie, safeReturnTo, wantsHtml, CHALLENGE_COOKIE,\n TAP_NODE, type ChallengeKit, type SnapshotVersion, type TrustedProxyConfig, type WireEvent,\n} from '@camada/core';\nimport iife from '@camada/browser/iife-string';\nimport { resolveEnv, type ResolvedEnv } from './env.js';\nimport {\n CHALLENGE_PATH, nodeChallengeKit, readForm, parsedBody, isHttps, writeChallengePage, writeChallengeJson,\n} from './challenge.js';\nimport { SDK_ID } from './version.js';\n\nconst SESSION_COOKIE = '_sfp'; // same cookie as the edge collector: sid/ns comparable across taps\nconst SCRIPT_PATH = '/_cam/b.js';\nconst FP_PATH = '/_cam/fp';\nconst FP_MAX = 32 * 1024; // matches the server's /fp cap: never accept what ingest will 413\nconst ATTACHED = Symbol.for('camada.attached'); // one wrap per server, however often attach() is called\n\nexport interface CamadaOptions {\n env?: Record<string, string | undefined>;\n fetchImpl?: typeof fetch; // threaded into snapshot client, event queue, and the fp relay (tests)\n refreshMs?: number;\n scriptPath?: string;\n fpPath?: string;\n challenge?: boolean; // enforce `challenge` verdicts with the first-party page (default true; CAMADA_CHALLENGE=0 also switches it off)\n challengePath?: string; // where that page posts its solution (default /__camada/challenge)\n snapshotVersion?: SnapshotVersion; // 5 (default) also carries the tenant's ordered custom rules; 4 the allow/challenge sides only; 3 opts out of both\n /** With CAMADA_SERVERLESS=1: handed every per-request event flush and snapshot refresh, so the\n * platform keeps the invocation alive until they finish (e.g. `waitUntil` from `@vercel/functions`). */\n waitUntil?: (p: Promise<unknown>) => void;\n}\n\ninterface CamadaRequest extends IncomingMessage {\n originalUrl?: string; // Express/Nest: the full URL; `url` loses the mount path inside app.use('/x', …)\n body?: unknown; // what a body parser mounted before camada left behind\n camada?: { rid: string; ip: string | null; sid: string };\n route?: { path?: string }; // Express fills this after routing\n camadaChallenged?: boolean; // serveChallenge() already shipped this request's event\n camadaOpen?: boolean; // its event ships on response finish: a track() row until then rides that flush\n camadaTracks?: Promise<void>[]; // track() rows still hashing (serverless: the finish flush waits for them)\n camadaPrefix?: string; // the mount prefix seen when camada ran (scriptTag() runs later, where req.url is whole again)\n}\n\n/** The getter `header` conditions read (§D3). node:http lower-cases every incoming name and\n * the matcher always asks with a lower-cased one, so nothing has to be normalised here; a\n * header the client repeated arrives as an array and is joined the way the wire carried it. */\nconst headerReader = (req: IncomingMessage) => (name: string): string | null => {\n const v = req.headers[name];\n return v == null ? null : Array.isArray(v) ? v.join(', ') : v;\n};\n\nconst splitUrl = (raw: string): { path: string; query: string } => {\n const qi = raw.indexOf('?');\n return qi === -1 ? { path: raw, query: '' } : { path: raw.slice(0, qi), query: raw.slice(qi) };\n};\n\n/** Path and query as the client sent them: what rules, the challenge list and events see. Express\n * strips the mount path from `req.url` inside `app.use(path, mw)` (Nest mounts module middleware\n * that way, at '{*splat}' or '*'), and koa-mount does the same (the Koa adapter copies Koa's\n * `originalUrl` over), so read `originalUrl` first; plain node:http and Fastify only have `url`. */\nfunction pathAndQuery(req: CamadaRequest): { path: string; query: string } {\n return splitUrl(req.originalUrl || req.url || '/');\n}\n\n/** Whether the request is for one of camada's own endpoints (beacon script, beacon POST,\n * challenge verify). Those answer at the mount-relative path (`req.url`), so `app.use('/api',\n * camada)` serves `/api/_cam/b.js`, and at the full path, so a wildcard mount (Nest, whose\n * `req.url` is stripped to '/') still serves `/_cam/b.js`. */\nconst atEndpoint = (req: CamadaRequest, path: string, endpoint: string): boolean =>\n path === endpoint || splitUrl(req.url || '/').path === endpoint;\n\n/** The mount prefix camada runs under: the full path minus the mount-relative one, when the full\n * path ends with it (`app.use('/api', …)`, koa-mount). A relative path of `/` says nothing: it is\n * both the mount's own root and what a wildcard mount (Nest) strips every path to, so no prefix. */\nfunction mountPrefix(req: CamadaRequest): string {\n if (!req.originalUrl) return '';\n const full = splitUrl(req.originalUrl).path, rel = splitUrl(req.url || '/').path;\n return rel !== '/' && full !== rel && full.endsWith(rel) ? full.slice(0, full.length - rel.length) : '';\n}\n\n/** A unix-socket (pipe) listener: Node never has a peer address there, on any request. The\n * listening server reports its address as the socket path (a string); `_pipeName` keeps the\n * answer after server.close() has dropped the handle while connections drain. */\nfunction onUnixListener(socket: unknown): boolean {\n const srv = (socket as { server?: { address?: () => unknown; _pipeName?: unknown } } | null)?.server;\n if (!srv) return false;\n try { if (typeof srv.address?.() === 'string') return true; } catch { /* not a net.Server */ }\n return typeof srv._pipeName === 'string';\n}\n\nconst cookieValue = (cookie: string, name: string): string | null => {\n const src = '; ' + cookie;\n const i = src.indexOf('; ' + name + '=');\n if (i === -1) return null;\n const start = i + name.length + 3;\n const j = src.indexOf(';', start);\n return src.slice(start, j === -1 ? undefined : j);\n};\n\n/** Sets a first visit's `_sfp` and keeps it there: an app that replaces Set-Cookie through\n * setHeader (or writeHead, setHeaders, which route through it once any header is set) gets its\n * own cookies plus the session's, unless it set `_sfp` itself. appendHeader already keeps it. */\nfunction keepSessionCookie(res: ServerResponse, cookie: string): void {\n const set = res.setHeader;\n res.setHeader = function (this: ServerResponse, name: string, value: number | string | readonly string[]) {\n if (typeof name === 'string' && name.toLowerCase() === 'set-cookie') {\n const list = Array.isArray(value) ? value : [String(value)];\n if (!list.some((c) => String(c).trimStart().startsWith(SESSION_COOKIE + '='))) value = [...list, cookie];\n }\n return set.call(this, name, value);\n } as ServerResponse['setHeader'];\n res.setHeader('set-cookie', cookie);\n}\n\nexport class Camada {\n readonly env: ResolvedEnv | null;\n readonly snap: SnapshotClient | null = null;\n readonly queue: EventQueue | null = null;\n private readonly fetchImpl: typeof fetch;\n private readonly scriptPath: string;\n private readonly fpPath: string;\n private readonly challengeOn: boolean;\n private readonly challengePath: string;\n private readonly challengePathSet: boolean; // configured explicitly: used as given, never prefixed\n private readonly scriptPathSet: boolean;\n private readonly kit: ChallengeKit | null = null;\n private readonly envSource: Record<string, string | undefined>;\n private readonly waitUntil?: (p: Promise<unknown>) => void;\n\n constructor(opts: CamadaOptions = {}) {\n this.envSource = opts.env ?? process.env;\n this.fetchImpl = opts.fetchImpl ?? fetch;\n this.waitUntil = opts.waitUntil;\n this.scriptPath = opts.scriptPath ?? SCRIPT_PATH;\n this.fpPath = opts.fpPath ?? FP_PATH;\n this.challengeOn = opts.challenge !== false && this.envSource.CAMADA_CHALLENGE !== '0';\n this.challengePath = opts.challengePath ?? CHALLENGE_PATH;\n this.challengePathSet = opts.challengePath != null;\n this.scriptPathSet = opts.scriptPath != null;\n this.env = resolveEnv(this.envSource);\n if (!this.env) return; // unconfigured: every entry point no-ops\n if (this.envSource.CAMADA_DISABLED === '1') return; // killed at boot: no poll timer, no exit hooks, truly silent\n this.snap = new SnapshotClient({\n url: this.env.snapshotUrl, token: this.env.snapToken,\n mode: this.env.serverless ? 'lazy' : 'timer',\n refreshMs: opts.refreshMs, snapshotVersion: opts.snapshotVersion, fetchImpl: this.fetchImpl, sdk: SDK_ID,\n });\n this.queue = new EventQueue({ url: this.env.ingestUrl, token: this.env.ingestToken, fetchImpl: this.fetchImpl, sdk: SDK_ID });\n this.kit = nodeChallengeKit(this.env.secret);\n this.snap.start();\n this.queue.installNodeExitFlush();\n }\n\n get disabled(): boolean { return this.envSource.CAMADA_DISABLED === '1' || !this.env; }\n\n private trustedProxy(): TrustedProxyConfig | null {\n return this.env?.trustedProxy ?? this.snap?.config?.trusted_proxy ?? null; // explicit local override wins\n }\n\n /** The client IP per the trusted-proxy config (team contract C6 v3). No socket at all, or a\n * unix-socket listener (nginx/Passenger in front, where Node never has a peer): core's no-peer\n * X-Forwarded-For walk, which trusts the local proxy. A TCP socket with no readable peer (the\n * client reset the connection before camada ran, e.g. behind an async middleware) is\n * unidentifiable: the walk would believe whatever the client wrote. So: null, the stance every\n * ip-bound feature already takes (no ip rules, no challenge). */\n private clientIp(req: IncomingMessage): string | null {\n const peer = req.socket?.remoteAddress;\n if (!peer && req.socket && !onUnixListener(req.socket)) return null;\n return resolveClientIp(peer ?? null, req.headers['x-forwarded-for'] as string | undefined, this.trustedProxy());\n }\n\n /** Where the challenge form posts: under the mount prefix, so it reaches camada, unless the\n * path was configured explicitly. */\n private challengeAction(req: CamadaRequest): string {\n return this.challengePathSet ? this.challengePath : (req.camadaPrefix ?? mountPrefix(req)) + this.challengePath;\n }\n\n private beaconEnabled(): boolean { return this.snap?.config?.beacon !== false; }\n\n /** Queues a row. With CAMADA_SERVERLESS=1 it also starts the POST now: the platform freezes the\n * process once the response is out, so the 15 s interval would never run. Never throws. */\n private enqueue(row: unknown, req?: CamadaRequest): void {\n const q = this.queue!;\n q.push(row);\n if (!this.env?.serverless) return;\n if (req?.camadaOpen) return; // a track() row while the response is open: the finish flush carries it\n this.flushFor(req);\n }\n\n /** Serverless: start the POST now and hand it to waitUntil. Never throws. */\n private flushFor(req?: CamadaRequest): void {\n const q = this.queue!;\n guarded(() => {\n // One POST per request: the response-finish flush first waits for that request's track()\n // rows still hashing. flush() is single-in-flight, so a row queued while a POST was\n // finishing rides a second one.\n const tracks = req?.camadaTracks ?? [];\n const p = Promise.allSettled(tracks)\n .then(() => q.flush())\n .then(() => (q.size > 0 ? q.flush() : undefined));\n this.waitUntil?.(p);\n }, undefined);\n }\n\n /** True when camada fully handled the request. Synchronous; never throws. */\n handle(req: IncomingMessage, res: ServerResponse): boolean {\n return guarded(() => this.handleInner(req as CamadaRequest, res), false);\n }\n\n private handleInner(req: CamadaRequest, res: ServerResponse): boolean {\n if (this.disabled || !this.snap || !this.queue || !this.env) return false;\n const t0 = Date.now();\n this.snap.ensureFresh(this.env.serverless ? this.waitUntil : undefined);\n\n const { path, query } = pathAndQuery(req);\n const ip = this.clientIp(req);\n req.camadaPrefix = mountPrefix(req);\n\n // enforce before anything else, beacon endpoints included — fail open while cold. The custom\n // rules read the user agent and the request headers (§D3); without them every `ua` and\n // `header` condition is false.\n const v = this.snap.verdict({ ip, path, ua: req.headers['user-agent'], header: headerReader(req) });\n if (v.block) {\n const headers: Record<string, string> = { 'x-block-reason': v.reason ?? '', 'x-block-version': v.version ?? '', 'content-type': 'text/plain', 'cache-control': 'no-store' };\n if (v.rule) headers['x-block-rule'] = v.rule; // a custom rule blocked: name it, so the customer knows which row to edit\n res.writeHead(403, headers);\n res.end('Forbidden');\n const ev = this.buildEvent(req, path, query, ip, { rid: randomUUID(), sid: null, newSession: false });\n ev.st = 403; // blocked requests always ship: silent expiry makes blocks oscillate\n ev.blk = v.reason; // SDK-01: the reason rides the event so the analyst counts SDK blocks, not the app's own 403s ('rule' when a rule decided)\n if (v.rule) ev.rl = v.rule;\n this.enqueue(ev);\n return true;\n }\n // `warn` passes the request and only marks its event (below, on response-finish); a skip\n // passes with nothing stamped at all — it is the absence of enforcement.\n\n // A challenge needs a resolved client IP: the nonce and the `_cch` cookie are bound to it,\n // so without one a single solve would mint a cookie every unidentified client could\n // present. No ip -> no challenge (fail open), the same stance ip rules take.\n if (this.challengeOn && this.kit && ip) {\n // The verify endpoint answers first: a challenged client must be able to reach it.\n if (req.method === 'POST' && atEndpoint(req, path, this.challengePath)) { this.verifyChallenge(req, res, ip); return true; }\n if (v.challenge && !this.challengePassed(req, ip)) { this.serveChallengeInner(req, res, ip, path + query); return true; }\n }\n\n if (this.beaconEnabled()) {\n if (req.method === 'GET' && atEndpoint(req, path, this.scriptPath)) {\n res.writeHead(200, { 'content-type': 'application/javascript', 'cache-control': 'public, max-age=3600' });\n res.end(iife);\n return true;\n }\n if (req.method === 'POST' && atEndpoint(req, path, this.fpPath)) {\n this.relayBeacon(req, res, ip);\n return true;\n }\n }\n\n const rid = randomUUID();\n let sid = cookieValue((req.headers.cookie as string) || '', SESSION_COOKIE);\n const newSession = !sid;\n if (!sid) {\n sid = randomUUID();\n const https = (req.socket as { encrypted?: boolean }).encrypted || req.headers['x-forwarded-proto'] === 'https';\n keepSessionCookie(res, `${SESSION_COOKIE}=${sid}; Path=/; Max-Age=2592000; HttpOnly; SameSite=Lax${https ? '; Secure' : ''}`);\n }\n req.camada = { rid, ip, sid };\n res.setHeader('x-rid', rid);\n\n const cfg = this.snap.config;\n const excluded = (cfg?.exclude || []).some((x) => path.startsWith(x));\n if (!excluded && Math.random() < (cfg?.sample ?? 1)) {\n // 'finish' when the response went out whole; 'close' alone when the client left first (an\n // aborted SSE or download), which ships the status set so far and dur up to the abort — the\n // fetch adapters' cancel semantics. 'close' also follows every 'finish': one event either way.\n let shipped = false;\n req.camadaOpen = true;\n const ship = () => guarded(() => {\n req.camadaOpen = false;\n if (shipped) return;\n shipped = true;\n // serveChallenge() may have answered from inside the app, and it already shipped the\n // `blk: \"challenge\"` row — one request, one event. A track() row queued while the\n // response was open still needs this finish flush (serverless).\n if (req.camadaChallenged) { if (this.env?.serverless) this.flushFor(req); return; }\n const ev = this.buildEvent(req, path, query, ip, { rid, sid, newSession });\n ev.ts = t0; // the request start: the timeline draws [ts, ts + dur]\n ev.st = res.statusCode;\n ev.dur = Date.now() - t0;\n if (req.route?.path) ev.rt = String(req.route.path);\n if (v.warn && v.rule) ev.wrn = v.rule; // §D3: the warn rule that let this request through\n this.enqueue(ev, req);\n }, undefined);\n res.once('finish', ship);\n res.once('close', ship);\n }\n return false;\n }\n\n private buildEvent(req: CamadaRequest, path: string, query: string, ip: string | null, o: { rid: string; sid: string | null; newSession: boolean }): WireEvent {\n const raw = req.rawHeaders || [];\n const headers: Array<[string, string]> = [];\n for (let i = 0; i + 1 < raw.length; i += 2) headers.push([raw[i], raw[i + 1]]);\n return buildWireEvent(\n { method: req.method || 'GET', host: (req.headers.host as string) || '', path, query, headers, ip, httpVersion: req.httpVersion },\n { tap: TAP_NODE, rid: o.rid, sid: o.sid, newSession: o.newSession },\n );\n }\n\n /** Reads the beacon POST (≤32 KB), answers 204 immediately, and queues the beacon as a `sig: 1`\n * row with the trusted-proxy-resolved client IP: it rides the next event batch, so the analyst\n * sees one request per flush instead of one per page view. Junk bodies are dropped, never shipped.\n * A body parser mounted before camada (express.json(), always on Nest) has already drained the\n * stream: its `req.body` is used instead, and the request is answered either way. */\n private relayBeacon(req: CamadaRequest, res: ServerResponse, ip: string | null): void {\n const accept = (parsed: unknown) => {\n res.writeHead(204, { 'cache-control': 'no-store' });\n res.end();\n if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed) || Buffer.isBuffer(parsed)) return;\n this.enqueue({ ...(parsed as Record<string, unknown>), sig: 1, ip, tap: TAP_NODE });\n };\n const fromText = (text: string) => { try { return JSON.parse(text) as unknown; } catch { return null; } };\n if (req.readableEnded) {\n const b = parsedBody(req);\n if ((typeof b === 'string' ? b : b ? JSON.stringify(b) : '').length > FP_MAX) { res.writeHead(413).end(); return; }\n accept(typeof b === 'string' ? fromText(b) : b);\n return;\n }\n const chunks: Buffer[] = [];\n let size = 0, dead = false;\n req.on('data', (c: Buffer) => {\n size += c.length;\n if (size > FP_MAX) { dead = true; req.removeAllListeners('data'); res.writeHead(413).end(); return; }\n chunks.push(c);\n });\n req.on('end', () => guarded(() => {\n if (dead) return;\n accept(fromText(Buffer.concat(chunks).toString('utf8')));\n }, undefined));\n req.on('error', () => { try { res.destroy(); } catch { /* already gone */ } });\n }\n\n private challengePassed(req: IncomingMessage, ip: string | null): boolean {\n return !!this.kit?.tokenValid(ip, Date.now(), cookieValue((req.headers.cookie as string) || '', CHALLENGE_COOKIE));\n }\n\n /** 403 + the proof-of-work page (HTML navigations) or 403 JSON (everything else), plus the\n * `blk: \"challenge\"` event — a served challenge is reported like a block (contract §D2). */\n private serveChallengeInner(req: CamadaRequest, res: ServerResponse, ip: string, target: string): void {\n const to = safeReturnTo(target);\n if (wantsHtml((req.headers.accept as string) ?? null, (req.headers['sec-fetch-dest'] as string) ?? null)) {\n writeChallengePage(res, { nonce: this.kit!.nonce(ip, Date.now()), action: this.challengeAction(req), to });\n } else {\n writeChallengeJson(res);\n }\n // The response is out; telemetry must never be able to undo that (a throw here would make\n // guarded() report the request as unhandled and let the app write to an ended response).\n guarded(() => {\n const qi = target.indexOf('?');\n const ev = this.buildEvent(req, qi === -1 ? target : target.slice(0, qi), qi === -1 ? '' : target.slice(qi), ip,\n { rid: randomUUID(), sid: this.sessionOf(req), newSession: false });\n ev.st = 403;\n ev.blk = 'challenge';\n this.enqueue(ev);\n }, undefined);\n }\n\n /** The request's existing session, so challenge rows join the session that produced them. */\n private sessionOf(req: IncomingMessage): string | null {\n return cookieValue((req.headers.cookie as string) || '', SESSION_COOKIE);\n }\n\n /** POST from the challenge page: validate the nonce and the proof of work, set `_cch`, 302\n * back to the (sanitised, same-site) original URL, and ship `{ st: 200, ch: 1 }`. */\n private verifyChallenge(req: CamadaRequest, res: ServerResponse, ip: string): void {\n readForm(req, (form) => guarded(() => {\n const to = safeReturnTo(form.to);\n const now = Date.now();\n if (!this.kit!.verify(ip, now, form.nonce, form.solution)) {\n writeChallengePage(res, { nonce: this.kit!.nonce(ip, now), action: this.challengeAction(req), to });\n return;\n }\n res.writeHead(302, {\n location: to,\n 'set-cookie': challengeCookie(this.kit!.issue(ip, now), isHttps(req)),\n 'cache-control': 'no-store',\n });\n res.end();\n const ev = this.buildEvent(req, this.challengeAction(req), '', ip, { rid: randomUUID(), sid: this.sessionOf(req), newSession: false });\n ev.st = 200;\n ev.ch = 1; // challenge passed (contract §A3 ingest field)\n this.enqueue(ev);\n }, undefined));\n }\n\n /**\n * Records the upgrades (WebSocket handshakes) a node:http or node:https server hands to its\n * 'upgrade' listeners: Node never routes those through the request handler the middleware runs\n * in. Each one ships one event with `st: 101` once the app's listeners have run. Observe only:\n * the request is never blocked or challenged, nothing is written to the socket, no session\n * cookie is minted, and a server with no 'upgrade' listener of its own behaves exactly as before\n * (this wraps `server.emit` rather than adding a listener, which would claim every upgrade).\n * Idempotent; returns the server. A handshake the WebSocket library then rejects still ships 101.\n */\n attach<S extends Server>(server: S): S {\n guarded(() => {\n const s = server as S & { [ATTACHED]?: true };\n if (this.disabled || !this.queue || s[ATTACHED]) return;\n s[ATTACHED] = true;\n const emit = s.emit.bind(s) as (event: string | symbol, ...args: unknown[]) => boolean;\n s.emit = ((event: string | symbol, ...args: unknown[]) => {\n if (event !== 'upgrade') return emit(event, ...args);\n const t0 = Date.now();\n try {\n return emit(event, ...args);\n } finally {\n guarded(() => this.shipUpgrade(args[0] as CamadaRequest, t0), undefined);\n }\n }) as S['emit'];\n }, undefined);\n return server;\n }\n\n private shipUpgrade(req: CamadaRequest, t0: number): void {\n if (this.disabled || !this.queue) return;\n const { path, query } = pathAndQuery(req);\n const cfg = this.snap?.config;\n if ((cfg?.exclude || []).some((x) => path.startsWith(x)) || Math.random() >= (cfg?.sample ?? 1)) return;\n const ip = this.clientIp(req);\n const ev = this.buildEvent(req, path, query, ip, { rid: randomUUID(), sid: this.sessionOf(req), newSession: false });\n ev.ts = t0;\n ev.st = 101;\n ev.dur = Date.now() - t0;\n this.enqueue(ev);\n }\n\n /** Serve the challenge for this request on demand — for a route the app wants to gate itself\n * (the example's /challenge-me). Returns false when the client already holds a valid `_cch`,\n * so the caller renders its own page. */\n serveChallenge(req: IncomingMessage, res: ServerResponse): boolean {\n return guarded(() => {\n if (this.disabled || !this.kit || !this.queue) return false;\n const ip = this.clientIp(req);\n if (!ip || this.challengePassed(req, ip)) return false; // unidentifiable client: fail open\n (req as CamadaRequest).camadaChallenged = true;\n const { path, query } = pathAndQuery(req as CamadaRequest);\n this.serveChallengeInner(req as CamadaRequest, res, ip, path + query);\n return true;\n }, false);\n }\n\n /** For HTML templates: the first-party beacon tag with the request's rid. */\n scriptTag(req: IncomingMessage): string {\n if (this.disabled || !this.beaconEnabled()) return '';\n const rid = (req as CamadaRequest).camada?.rid;\n const r = req as CamadaRequest;\n const src = this.scriptPathSet ? this.scriptPath : (r.camadaPrefix ?? mountPrefix(r)) + this.scriptPath;\n return `<script src=\"${src}${rid ? `?r=${rid}` : ''}\" async></script>`;\n }\n\n /** App-context outcome events (login failed, signup, …). The identifier is HMAC-hashed\n * in-process; the raw value never reaches the queue. */\n track(req: IncomingMessage, event: string, data?: { user?: string }): void {\n guarded(() => {\n if (this.disabled || !this.queue || !this.env) return;\n const ctx = (req as CamadaRequest).camada;\n const r = req as CamadaRequest;\n const row = (async () => {\n const uid = data?.user ? await hashUserId(data.user, this.env!.ingestToken, (globalThis.crypto ?? webcrypto).subtle) : null;\n this.enqueue({ tap: TAP_NODE, et: event, uid, rid: ctx?.rid ?? null, sid: ctx?.sid ?? null, ip: ctx?.ip ?? null, ts: Date.now() }, r);\n })().catch(logRateLimited);\n if (r.camadaOpen) (r.camadaTracks ??= []).push(row);\n }, undefined);\n }\n\n /** Express/Connect middleware (also what NestJS on the default platform consumes). */\n express() {\n return (req: IncomingMessage, res: ServerResponse, next: (err?: unknown) => void): void => {\n if (!this.handle(req, res)) next();\n };\n }\n\n /** Stops polling and the event queue. The promise settles once rows still queued behind a drain\n * in flight or a 429 hold are sent (or counted dropped); it never rejects. */\n stop(): Promise<void> {\n this.snap?.stop();\n return this.queue?.stop() ?? Promise.resolve();\n }\n}\n\nexport function createCamada(opts: CamadaOptions = {}): Camada {\n const c = new Camada(opts);\n if (!c.env) logRateLimited(new Error('CAMADA_KEY (or CAMADA_TOKEN + CAMADA_SNAPSHOT_TOKEN) not set — camada is inactive'));\n return c;\n}\n","// Environment wiring. The two-line quickstart depends on this file doing the right thing:\n// CAMADA_KEY=<ingest_token>.<snap_token> (printed by `reconcile instructions` and seed)\n// CAMADA_INGEST_URL / CAMADA_SNAPSHOT_URL (dev: http://localhost:8787[/snapshot])\n// CAMADA_DISABLED=1 kill switch, checked per request\n// CAMADA_SERVERLESS=1 lazy snapshot mode (no interval timer), flush per request\n// CAMADA_TRUSTED_PROXY local override: none | vercel | hops:N | cidrs:a,b\nimport { parseKey, parseTrustedProxyEnv, type TrustedProxyConfig } from '@camada/core';\n\nexport { parseTrustedProxyEnv }; // re-export: this was @camada/node's public surface before it moved to core\n\nexport interface ResolvedEnv {\n ingestToken: string;\n snapToken: string;\n secret: string; // HMAC key for the challenge nonce/cookie — never leaves the process\n ingestUrl: string;\n snapshotUrl: string;\n serverless: boolean;\n trustedProxy: TrustedProxyConfig | null; // null = defer to server-delivered config\n}\n\n/** Returns null (SDK stays disabled, one log line) rather than throwing on bad config. */\nexport function resolveEnv(env: Record<string, string | undefined> = process.env): ResolvedEnv | null {\n const key = parseKey(env.CAMADA_KEY);\n const ingestToken = key?.ingestToken ?? env.CAMADA_TOKEN;\n const snapToken = key?.snapToken ?? env.CAMADA_SNAPSHOT_TOKEN;\n if (!ingestToken || !snapToken) return null;\n const ingestUrl = (env.CAMADA_INGEST_URL || 'https://in.camada.app').replace(/\\/$/, '');\n return {\n ingestToken, snapToken, ingestUrl,\n secret: env.CAMADA_KEY || `${ingestToken}.${snapToken}`,\n snapshotUrl: env.CAMADA_SNAPSHOT_URL || `${ingestUrl}/snapshot`,\n serverless: env.CAMADA_SERVERLESS === '1',\n trustedProxy: parseTrustedProxyEnv(env.CAMADA_TRUSTED_PROXY),\n };\n}\n","// The challenge for @camada/node (SDK-04). node:crypto gives us a synchronous HMAC, so the\n// cookie check costs nothing on the hot path and Camada.handle() stays a synchronous boolean.\nimport type { IncomingMessage, ServerResponse } from 'node:http';\nimport { createHash, createHmac } from 'node:crypto';\nimport { createChallenge, challengePage, challengePageCsp, parseFormBody, type ChallengeKit, type ChallengePageOptions } from '@camada/core';\n\nexport const CHALLENGE_PATH = '/__camada/challenge';\nconst BODY_MAX = 4 * 1024; // the verify form is ~120 bytes; anything larger is not ours\n\nexport function nodeChallengeKit(secret: string): ChallengeKit {\n return createChallenge({\n secret,\n hmac: (s, m) => createHmac('sha256', s).update(m).digest('hex'),\n sha: (m) => createHash('sha256').update(m).digest('hex'),\n });\n}\n\n/** Reads at most BODY_MAX bytes; hands back '' when the client sends more (or errors). */\nexport function readBody(req: IncomingMessage, done: (body: string) => void): void {\n const chunks: Buffer[] = [];\n let size = 0, dead = false;\n req.on('data', (c: Buffer) => {\n if (dead) return;\n size += c.length;\n if (size > BODY_MAX) { dead = true; chunks.length = 0; return; }\n chunks.push(c);\n });\n req.on('end', () => done(dead ? '' : Buffer.concat(chunks).toString('utf8')));\n req.on('error', () => done(''));\n}\n\nconst FORM_FIELDS = ['nonce', 'solution', 'to'] as const;\n\n/** What a body parser mounted before camada left on `req.body` once it drained the stream: the\n * text of a string/Buffer body (text/raw parsers), the object of a parsed one (json, urlencoded,\n * Koa's parsers via the adapter), or null when nothing usable is left. */\nexport function parsedBody(req: IncomingMessage & { body?: unknown }): string | Record<string, unknown> | null {\n const b = req.body;\n if (typeof b === 'string') return b;\n if (Buffer.isBuffer(b)) return b.toString('utf8');\n return b && typeof b === 'object' ? (b as Record<string, unknown>) : null;\n}\n\n/** The verify POST's form fields, always delivered (never leaves the request hanging). A body\n * parser mounted before camada (express.urlencoded(), always on Nest) has already drained the\n * stream, so its `'end'` will never fire again: use what it left behind (parsedBody). Only\n * string fields count. A drained stream with nothing usable, or an oversized one, yields {},\n * which fails verification and re-serves the page at once. */\nexport function readForm(req: IncomingMessage & { body?: unknown }, done: (form: Record<string, string>) => void): void {\n if (!req.readableEnded) { readBody(req, (body) => done(parseFormBody(body))); return; }\n const b = parsedBody(req);\n if (typeof b === 'string') { done(b.length > BODY_MAX ? {} : parseFormBody(b)); return; }\n const form: Record<string, string> = Object.create(null);\n for (const k of FORM_FIELDS) {\n const v = b?.[k];\n if (typeof v === 'string') form[k] = v;\n }\n done(form);\n}\n\nexport const isHttps = (req: IncomingMessage): boolean =>\n !!(req.socket as { encrypted?: boolean } | undefined)?.encrypted || req.headers['x-forwarded-proto'] === 'https';\n\n/** Writes `challengePage(o)` with its own CSP for the same `bits` (only its inline solver and\n * style, by hash): a policy the app set earlier (helmet before camada) would otherwise ride along\n * on this response and block the solver. */\nexport function writeChallengePage(res: ServerResponse, o: ChallengePageOptions): void {\n res.removeHeader('content-security-policy');\n res.removeHeader('content-security-policy-report-only');\n res.writeHead(403, {\n 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-store', 'x-camada-challenge': '1',\n 'content-security-policy': challengePageCsp(o.bits),\n });\n res.end(challengePage(o));\n}\n\nexport function writeChallengeJson(res: ServerResponse): void {\n res.writeHead(403, { 'content-type': 'application/json', 'cache-control': 'no-store', 'x-camada-challenge': '1' });\n res.end('{\"error\":\"challenge_required\"}');\n}\n","{\n \"version\": \"0.3.2\",\n \"license\": \"MIT\",\n \"type\": \"module\",\n \"engines\": {\n \"node\": \">=20\"\n },\n \"files\": [\n \"dist\",\n \"README.md\",\n \"LICENSE\"\n ],\n \"scripts\": {\n \"build\": \"tsup\",\n \"dev\": \"tsup --watch\",\n \"test\": \"vitest run\",\n \"check\": \"tsc --noEmit\"\n },\n \"name\": \"@camada/node\",\n \"description\": \"camada backend SDK for Node: request capture, inline blocking, first-party beacon serving; Express/Fastify/Nest/Koa adapters\",\n \"exports\": {\n \".\": {\n \"import\": {\n \"types\": \"./dist/index.d.ts\",\n \"default\": \"./dist/index.js\"\n },\n \"require\": {\n \"types\": \"./dist/index.d.cts\",\n \"default\": \"./dist/index.cjs\"\n }\n },\n \"./fastify\": {\n \"import\": {\n \"types\": \"./dist/fastify.d.ts\",\n \"default\": \"./dist/fastify.js\"\n },\n \"require\": {\n \"types\": \"./dist/fastify.d.cts\",\n \"default\": \"./dist/fastify.cjs\"\n }\n },\n \"./nest\": {\n \"import\": {\n \"types\": \"./dist/nest.d.ts\",\n \"default\": \"./dist/nest.js\"\n },\n \"require\": {\n \"types\": \"./dist/nest.d.cts\",\n \"default\": \"./dist/nest.cjs\"\n }\n },\n \"./koa\": {\n \"import\": {\n \"types\": \"./dist/koa.d.ts\",\n \"default\": \"./dist/koa.js\"\n },\n \"require\": {\n \"types\": \"./dist/koa.d.cts\",\n \"default\": \"./dist/koa.cjs\"\n }\n }\n },\n \"main\": \"./dist/index.cjs\",\n \"module\": \"./dist/index.js\",\n \"types\": \"./dist/index.d.ts\",\n \"typesVersions\": {\n \"*\": {\n \"fastify\": [\n \"./dist/fastify.d.ts\"\n ],\n \"nest\": [\n \"./dist/nest.d.ts\"\n ],\n \"koa\": [\n \"./dist/koa.d.ts\"\n ]\n }\n },\n \"dependencies\": {\n \"@camada/browser\": \"file:../camada-browser\",\n \"@camada/core\": \"file:../camada-core\"\n },\n \"devDependencies\": {\n \"@koa/bodyparser\": \"^6.1.0\",\n \"@nestjs/common\": \"^11.0.0\",\n \"@nestjs/core\": \"^11.0.0\",\n \"@nestjs/platform-express\": \"^11.0.0\",\n \"@types/express\": \"^4.17.25\",\n \"@types/koa\": \"^2.15.2\",\n \"@types/koa-mount\": \"^4.0.5\",\n \"@types/node\": \"^22.10.0\",\n \"express\": \"^4.21.0\",\n \"fastify\": \"^5.2.0\",\n \"koa\": \"^2.15.0\",\n \"koa-mount\": \"^4.2.0\",\n \"reflect-metadata\": \"^0.2.2\",\n \"rxjs\": \"^7.8.1\",\n \"tsup\": \"^8.3.5\",\n \"typescript\": \"^5.7.2\",\n \"vitest\": \"^3.0.0\"\n },\n \"peerDependencies\": {\n \"@nestjs/common\": \">=10\"\n },\n \"peerDependenciesMeta\": {\n \"@nestjs/common\": {\n \"optional\": true\n }\n }\n}\n","// The SDK's wire identity (SDK-03): x-camada-sdk: <package>/<version>. Named JSON imports are\n// inlined and tree-shaken by tsup at build, so dist carries two literals — no runtime fs read.\nimport { name, version } from '../package.json';\nexport const SDK_ID = `${name}/${version}`;\n"],"mappings":"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","names":["import_node_crypto","import_core","import_core","name","iife"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/camada.ts","../src/env.ts","../src/challenge.ts","../package.json","../src/version.ts"],"sourcesContent":["// Quickstart (Express / Connect / Nest-on-Express):\n// import camada from '@camada/node';\n// app.use(camada.express()); // env: CAMADA_KEY (+ CAMADA_INGEST_URL for dev)\n// The default export is a lazy singleton wired from the environment on first use.\nimport type { IncomingMessage, Server, ServerResponse } from 'node:http';\nimport { Camada, createCamada, type CamadaOptions } from './camada.js';\n\nexport { Camada, createCamada, type CamadaOptions } from './camada.js';\nexport { resolveEnv, parseTrustedProxyEnv, type ResolvedEnv } from './env.js';\n\nlet singleton: Camada | null = null;\nexport function getDefault(): Camada {\n return (singleton ??= createCamada());\n}\n/** Test/reset hook: replaces the singleton (stops the old one). */\nexport function configure(opts: CamadaOptions): Camada {\n singleton?.stop();\n singleton = createCamada(opts);\n return singleton;\n}\n\nexport default {\n express: () => (req: IncomingMessage, res: ServerResponse, next: (err?: unknown) => void) => { if (!getDefault().handle(req, res)) next(); },\n handle: (req: IncomingMessage, res: ServerResponse) => getDefault().handle(req, res),\n scriptTag: (req: IncomingMessage) => getDefault().scriptTag(req),\n serveChallenge: (req: IncomingMessage, res: ServerResponse) => getDefault().serveChallenge(req, res),\n track: (req: IncomingMessage, event: string, data?: { user?: string }) => getDefault().track(req, event, data),\n attach: <S extends Server>(server: S): S => getDefault().attach(server),\n configure,\n};\n","// The engine: node:http-level request handling every adapter delegates to.\n// camada.handle(req, res) -> true when camada fully answered the request (block or beacon\n// endpoint), false when the app should proceed. Everything runs inside the fail-open\n// envelope: a camada bug must never 5xx the customer (plan.md INT-2), and CAMADA_DISABLED=1\n// bypasses the SDK entirely.\nimport type { IncomingMessage, Server, ServerResponse } from 'node:http';\nimport { randomUUID, webcrypto } from 'node:crypto'; // webcrypto handed to hashUserId explicitly, so the hash never depends on which global the host exposes\nimport {\n SnapshotClient, EventQueue, buildWireEvent, resolveClientIp, isTrustedPeer, hashUserId, guarded, logRateLimited,\n challengeCookie, safeReturnTo, wantsHtml, CHALLENGE_COOKIE,\n TAP_NODE, REQUEST_ROW_KEYS, type ChallengeKit, type SnapshotVersion, type TrustedProxyConfig, type WireEvent,\n} from '@camada/core';\nimport { isIP } from 'node:net';\nimport iife from '@camada/browser/iife-string';\nimport { resolveEnv, parseTlsxHeader, type ResolvedEnv } from './env.js';\nimport {\n CHALLENGE_PATH, nodeChallengeKit, readForm, parsedBody, isHttps, writeChallengePage,\n} from './challenge.js';\nimport { SDK_ID } from './version.js';\n\nconst SESSION_COOKIE = '_sfp'; // same cookie as the edge collector: sid/ns comparable across taps\nconst SCRIPT_PATH = '/_cam/b.js';\nconst FP_PATH = '/_cam/fp';\nconst FP_MAX = 32 * 1024; // matches the server's /fp cap: never accept what ingest will 413\n/** True for a peer address that is loopback (127.0.0.0/8, ::1, ::ffff:127.x) or not an IP at all. Node\n * normalises req.socket.remoteAddress (dotted v4, compressed lower-case v6), and the peer is not\n * attacker-spelled, so a prefix check on that string is enough; this is not a general IP parser. */\nfunction isLoopbackPeer(peer: string): boolean {\n const k = isIP(peer);\n if (k === 4) return peer.startsWith('127.');\n return k !== 6 || peer === '::1' || /^::ffff:127\\./i.test(peer);\n}\nconst TLSX_RE = /^[A-Za-z0-9+/=_-]+$/;\nconst ATTACHED = Symbol.for('camada.attached'); // one wrap per server, however often attach() is called\n\nexport interface CamadaOptions {\n env?: Record<string, string | undefined>;\n fetchImpl?: typeof fetch; // threaded into snapshot client, event queue, and the fp relay (tests)\n refreshMs?: number;\n scriptPath?: string;\n fpPath?: string;\n challenge?: boolean; // enforce `challenge` verdicts with the first-party page (default true; CAMADA_CHALLENGE=0 also switches it off)\n tlsxHeader?: string; // header carrying the TLS fingerprint (CAMADA_TLSX_HEADER); read only from a trusted-proxy peer in cidrs mode\n challengePath?: string; // where that page posts its solution (default /__camada/challenge)\n snapshotVersion?: SnapshotVersion; // 5 (default) also carries the tenant's ordered custom rules; 4 the allow/challenge sides only; 3 opts out of both\n /** With CAMADA_SERVERLESS=1: handed every per-request event flush and snapshot refresh, so the\n * platform keeps the invocation alive until they finish (e.g. `waitUntil` from `@vercel/functions`). */\n waitUntil?: (p: Promise<unknown>) => void;\n}\n\ninterface CamadaRequest extends IncomingMessage {\n originalUrl?: string; // Express/Nest: the full URL; `url` loses the mount path inside app.use('/x', …)\n body?: unknown; // what a body parser mounted before camada left behind\n camada?: { rid: string; ip: string | null; sid: string; ns: boolean; t0: number; wrn?: string }; // ns: first visit (no session cookie came in); t0: request start\n route?: { path?: string }; // Express fills this after routing\n camadaChallenged?: boolean; // serveChallenge() already shipped this request's event\n camadaSignalled?: boolean; // a non-HTML request passed under a challenge: x-camada-challenge header + `chs: 1` on its event\n camadaOpen?: boolean; // its event ships on response finish: a track() row until then rides that flush\n camadaTracks?: Promise<void>[]; // track() rows still hashing (serverless: the finish flush waits for them)\n camadaPrefix?: string; // the mount prefix seen when camada ran (scriptTag() runs later, where req.url is whole again)\n}\n\n/** The getter `header` conditions read (§D3). node:http lower-cases every incoming name and\n * the matcher always asks with a lower-cased one, so nothing has to be normalised here; a\n * header the client repeated arrives as an array and is joined the way the wire carried it. */\nconst headerReader = (req: IncomingMessage) => (name: string): string | null => {\n const v = req.headers[name];\n return v == null ? null : Array.isArray(v) ? v.join(', ') : v;\n};\n\nconst splitUrl = (raw: string): { path: string; query: string } => {\n const qi = raw.indexOf('?');\n return qi === -1 ? { path: raw, query: '' } : { path: raw.slice(0, qi), query: raw.slice(qi) };\n};\n\nconst isPage = (req: IncomingMessage): boolean =>\n wantsHtml((req.headers.accept as string) ?? null, (req.headers['sec-fetch-dest'] as string) ?? null);\n\n/** Path and query as the client sent them: what rules, the challenge list and events see. Express\n * strips the mount path from `req.url` inside `app.use(path, mw)` (Nest mounts module middleware\n * that way, at '{*splat}' or '*'), and koa-mount does the same (the Koa adapter copies Koa's\n * `originalUrl` over), so read `originalUrl` first; plain node:http and Fastify only have `url`. */\nfunction pathAndQuery(req: CamadaRequest): { path: string; query: string } {\n return splitUrl(req.originalUrl || req.url || '/');\n}\n\n/** Whether the request is for one of camada's own endpoints (beacon script, beacon POST,\n * challenge verify). Those answer at the mount-relative path (`req.url`), so `app.use('/api',\n * camada)` serves `/api/_cam/b.js`, and at the full path, so a wildcard mount (Nest, whose\n * `req.url` is stripped to '/') still serves `/_cam/b.js`. */\nconst atEndpoint = (req: CamadaRequest, path: string, endpoint: string): boolean =>\n path === endpoint || splitUrl(req.url || '/').path === endpoint;\n\n/** The mount prefix camada runs under: the full path minus the mount-relative one, when the full\n * path ends with it (`app.use('/api', …)`, koa-mount). A relative path of `/` says nothing: it is\n * both the mount's own root and what a wildcard mount (Nest) strips every path to, so no prefix. */\nfunction mountPrefix(req: CamadaRequest): string {\n if (!req.originalUrl) return '';\n const full = splitUrl(req.originalUrl).path, rel = splitUrl(req.url || '/').path;\n return rel !== '/' && full !== rel && full.endsWith(rel) ? full.slice(0, full.length - rel.length) : '';\n}\n\n/** A unix-socket (pipe) listener: Node never has a peer address there, on any request. The\n * listening server reports its address as the socket path (a string); `_pipeName` keeps the\n * answer after server.close() has dropped the handle while connections drain. */\nfunction onUnixListener(socket: unknown): boolean {\n const srv = (socket as { server?: { address?: () => unknown; _pipeName?: unknown } } | null)?.server;\n if (!srv) return false;\n try { if (typeof srv.address?.() === 'string') return true; } catch { /* not a net.Server */ }\n return typeof srv._pipeName === 'string';\n}\n\nconst cookieValue = (cookie: string, name: string): string | null => {\n const src = '; ' + cookie;\n const i = src.indexOf('; ' + name + '=');\n if (i === -1) return null;\n const start = i + name.length + 3;\n const j = src.indexOf(';', start);\n return src.slice(start, j === -1 ? undefined : j);\n};\n\n/** Sets a first visit's `_sfp` and keeps it there: an app that replaces Set-Cookie through\n * setHeader (or writeHead, setHeaders, which route through it once any header is set) gets its\n * own cookies plus the session's, unless it set `_sfp` itself. appendHeader already keeps it. */\nfunction keepSessionCookie(res: ServerResponse, cookie: string): void {\n const set = res.setHeader;\n res.setHeader = function (this: ServerResponse, name: string, value: number | string | readonly string[]) {\n if (typeof name === 'string' && name.toLowerCase() === 'set-cookie') {\n const list = Array.isArray(value) ? value : [String(value)];\n if (!list.some((c) => String(c).trimStart().startsWith(SESSION_COOKIE + '='))) value = [...list, cookie];\n }\n return set.call(this, name, value);\n } as ServerResponse['setHeader'];\n res.setHeader('set-cookie', cookie);\n}\n\n// CDNs read their own cache header ahead of cache-control, so all of these end up no-store.\nconst NO_STORE_HEADERS = ['cache-control', 'cdn-cache-control', 'surrogate-control', 'cloudflare-cdn-cache-control', 'vercel-cdn-cache-control', 'netlify-cdn-cache-control'];\n\n/** Marks a signalled response: `x-camada-challenge: 1` and no-store on every NO_STORE_HEADERS name. The header is\n * per client, so no shared cache may keep the copy, whatever the app sets. Every flush (an explicit\n * writeHead, http1's `_implicitHeader`, http2 compat's end/write/flushHeaders) goes through\n * `this.writeHead`, so that is the one choke point: it drops those names from the headers it is\n * given (object, flat array, tuple array) and re-sets no-store with the original setHeader. */\nfunction signal(res: ServerResponse): void {\n const set = res.setHeader, writeHead = res.writeHead;\n const isNoStore = (n: unknown) => typeof n === 'string' && NO_STORE_HEADERS.includes(n.toLowerCase());\n const strip = (h: unknown): unknown => {\n if (Array.isArray(h)) {\n if (h.length && Array.isArray(h[0])) return h.filter((p) => !isNoStore(p[0]));\n const out: unknown[] = [];\n for (let i = 0; i < h.length; i += 2) if (!isNoStore(h[i])) out.push(h[i], h[i + 1]);\n return out;\n }\n if (h && typeof h === 'object') return Object.fromEntries(Object.entries(h).filter(([k]) => !isNoStore(k)));\n return h;\n };\n res.writeHead = function (this: ServerResponse, status: number, ...rest: unknown[]) {\n if (!this.headersSent) for (const h of NO_STORE_HEADERS) set.call(this, h, 'no-store');\n return (writeHead as (...a: unknown[]) => ServerResponse).call(this, status, ...rest.map(strip));\n } as ServerResponse['writeHead'];\n res.setHeader('x-camada-challenge', '1');\n for (const h of NO_STORE_HEADERS) res.setHeader(h, 'no-store');\n}\n\nexport class Camada {\n readonly env: ResolvedEnv | null;\n readonly snap: SnapshotClient | null = null;\n readonly queue: EventQueue | null = null;\n private readonly fetchImpl: typeof fetch;\n private readonly scriptPath: string;\n private readonly fpPath: string;\n private readonly challengeOn: boolean;\n private readonly challengePath: string;\n private readonly challengePathSet: boolean; // configured explicitly: used as given, never prefixed\n private readonly scriptPathSet: boolean;\n private readonly kit: ChallengeKit | null = null;\n private readonly envSource: Record<string, string | undefined>;\n private readonly tlsxHeader: string | null;\n private readonly waitUntil?: (p: Promise<unknown>) => void;\n\n constructor(opts: CamadaOptions = {}) {\n this.envSource = opts.env ?? process.env;\n this.fetchImpl = opts.fetchImpl ?? fetch;\n this.waitUntil = opts.waitUntil;\n this.scriptPath = opts.scriptPath ?? SCRIPT_PATH;\n this.fpPath = opts.fpPath ?? FP_PATH;\n this.challengeOn = opts.challenge !== false && this.envSource.CAMADA_CHALLENGE !== '0';\n this.challengePath = opts.challengePath ?? CHALLENGE_PATH;\n this.challengePathSet = opts.challengePath != null;\n this.scriptPathSet = opts.scriptPath != null;\n this.env = resolveEnv(this.envSource);\n this.tlsxHeader = parseTlsxHeader(opts.tlsxHeader) ?? this.env?.tlsxHeader ?? null;\n if (!this.env) return; // unconfigured: every entry point no-ops\n if (this.envSource.CAMADA_DISABLED === '1') return; // killed at boot: no poll timer, no exit hooks, truly silent\n this.snap = new SnapshotClient({\n url: this.env.snapshotUrl, token: this.env.snapToken,\n mode: this.env.serverless ? 'lazy' : 'timer',\n refreshMs: opts.refreshMs, snapshotVersion: opts.snapshotVersion, fetchImpl: this.fetchImpl, sdk: SDK_ID,\n });\n this.queue = new EventQueue({ url: this.env.ingestUrl, token: this.env.ingestToken, fetchImpl: this.fetchImpl, sdk: SDK_ID });\n this.kit = nodeChallengeKit(this.env.secret);\n this.snap.start();\n this.queue.installNodeExitFlush();\n }\n\n get disabled(): boolean { return this.envSource.CAMADA_DISABLED === '1' || !this.env; }\n\n private trustedProxy(): TrustedProxyConfig | null {\n return this.env?.trustedProxy ?? this.snap?.config?.trusted_proxy ?? null; // explicit local override wins\n }\n\n /** The client IP per the trusted-proxy config (team contract C6 v3). No socket at all, or a\n * unix-socket listener (nginx/Passenger in front, where Node never has a peer): core's no-peer\n * X-Forwarded-For walk, which trusts the local proxy. A TCP socket with no readable peer (the\n * client reset the connection before camada ran, e.g. behind an async middleware) is\n * unidentifiable: the walk would believe whatever the client wrote. So: null, the stance every\n * ip-bound feature already takes (no ip rules, no challenge). */\n private clientIp(req: IncomingMessage): string | null {\n const peer = req.socket?.remoteAddress;\n if (!peer && req.socket && !onUnixListener(req.socket)) return null;\n return resolveClientIp(peer ?? null, req.headers['x-forwarded-for'] as string | undefined, this.trustedProxy());\n }\n\n /** The TLS fingerprint Cloudflare (a Transform Rule) put in the opt-in header, or null. Trusted only\n * in `cidrs` mode and only when the socket peer itself is a NON-loopback address core's cidrs walk\n * trusts, i.e. Cloudflare connects to the app directly. A loopback peer (a local reverse proxy or\n * cloudflared) is never trusted and X-Forwarded-For is never consulted, so the header is not read\n * and tlsx conditions fail open. Trust is core's isTrustedPeer, the same first-hop test the cidrs\n * walk uses. No readable peer (unix listener included) -> null, since core's no-peer walk would\n * trust everyone. */\n private tlsxOf(req: IncomingMessage): string | null {\n const header = this.tlsxHeader;\n if (!header) return null;\n const v = headerReader(req)(header); // a repeated header joins with ', ', which the charset rejects\n if (v == null || v.length > 128 || !TLSX_RE.test(v)) return null;\n const cfg = this.trustedProxy();\n const peer = req.socket?.remoteAddress;\n if (cfg?.mode !== 'cidrs' || !peer) return null;\n if (isLoopbackPeer(peer)) return null;\n return isTrustedPeer(peer, cfg) ? v : null;\n }\n\n /** Where the challenge form posts: under the mount prefix, so it reaches camada, unless the\n * path was configured explicitly. */\n private challengeAction(req: CamadaRequest): string {\n return this.challengePathSet ? this.challengePath : (req.camadaPrefix ?? mountPrefix(req)) + this.challengePath;\n }\n\n private beaconEnabled(): boolean { return this.snap?.config?.beacon !== false; }\n\n /** Queues a row. With CAMADA_SERVERLESS=1 it also starts the POST now: the platform freezes the\n * process once the response is out, so the 15 s interval would never run. Never throws. */\n private enqueue(row: unknown, req?: CamadaRequest): void {\n const q = this.queue!;\n q.push(row);\n if (!this.env?.serverless) return;\n if (req?.camadaOpen) return; // a track() row while the response is open: the finish flush carries it\n this.flushFor(req);\n }\n\n /** Serverless: start the POST now and hand it to waitUntil. Never throws. */\n private flushFor(req?: CamadaRequest): void {\n const q = this.queue!;\n guarded(() => {\n // One POST per request: the response-finish flush first waits for that request's track()\n // rows still hashing. flush() is single-in-flight, so a row queued while a POST was\n // finishing rides a second one.\n const tracks = req?.camadaTracks ?? [];\n const p = Promise.allSettled(tracks)\n .then(() => q.flush())\n .then(() => (q.size > 0 ? q.flush() : undefined));\n this.waitUntil?.(p);\n }, undefined);\n }\n\n /** True when camada fully handled the request. Synchronous; never throws. */\n handle(req: IncomingMessage, res: ServerResponse): boolean {\n return guarded(() => this.handleInner(req as CamadaRequest, res), false);\n }\n\n private handleInner(req: CamadaRequest, res: ServerResponse): boolean {\n if (this.disabled || !this.snap || !this.queue || !this.env) return false;\n const t0 = Date.now();\n this.snap.ensureFresh(this.env.serverless ? this.waitUntil : undefined);\n\n const { path, query } = pathAndQuery(req);\n const ip = this.clientIp(req);\n req.camadaPrefix = mountPrefix(req);\n\n // enforce before anything else, beacon endpoints included — fail open while cold. The custom\n // rules read the user agent and the request headers (§D3); without them every `ua` and\n // `header` condition is false.\n const v = this.snap.verdict({ ip, path, ua: req.headers['user-agent'], header: headerReader(req), tlsx: this.tlsxOf(req) });\n if (v.block) {\n const headers: Record<string, string> = { 'x-block-reason': v.reason ?? '', 'x-block-version': v.version ?? '', 'content-type': 'text/plain', 'cache-control': 'no-store' };\n if (v.rule) headers['x-block-rule'] = v.rule; // a custom rule blocked: name it, so the customer knows which row to edit\n res.writeHead(403, headers);\n res.end('Forbidden');\n const ev = this.buildEvent(req, path, query, ip, { rid: randomUUID(), sid: null, newSession: false });\n ev.st = 403; // blocked requests always ship: silent expiry makes blocks oscillate\n ev.blk = v.reason; // SDK-01: the reason rides the event so the analyst counts SDK blocks, not the app's own 403s ('rule' when a rule decided)\n if (v.rule) ev.rl = v.rule;\n this.enqueue(ev);\n return true;\n }\n // `warn` passes the request and only marks its event (below, on response-finish); a skip\n // passes with nothing stamped at all — it is the absence of enforcement.\n\n // A challenge needs a resolved client IP: the nonce and the `_cch` cookie are bound to it,\n // so without one a single solve would mint a cookie every unidentified client could\n // present. No ip -> no challenge (fail open), the same stance ip rules take.\n if (this.challengeOn && this.kit && ip) {\n // The verify endpoint answers first: a challenged client must be able to reach it.\n if (req.method === 'POST' && atEndpoint(req, path, this.challengePath)) { this.verifyChallenge(req, res, ip); return true; }\n if (v.challenge && !this.challengePassed(req, ip)) {\n // A page load gets the challenge; anything else (fetch/XHR/API) passes to the app, flagged.\n if (isPage(req)) { this.serveChallengeInner(req, res, ip, path + query); return true; }\n req.camadaSignalled = true;\n }\n }\n\n if (this.beaconEnabled()) {\n if (req.method === 'GET' && atEndpoint(req, path, this.scriptPath)) {\n res.writeHead(200, { 'content-type': 'application/javascript', 'cache-control': 'public, max-age=3600' });\n res.end(iife);\n return true;\n }\n if (req.method === 'POST' && atEndpoint(req, path, this.fpPath)) {\n this.relayBeacon(req, res, ip);\n return true;\n }\n }\n\n const { rid, sid, ns: newSession } = this.begin(req, res, ip, t0, v.warn ? v.rule : undefined);\n if (req.camadaSignalled) signal(res);\n\n const cfg = this.snap.config;\n const excluded = (cfg?.exclude || []).some((x) => path.startsWith(x));\n // A signalled request ships regardless of sample and exclude, like the 403 row it replaces.\n if (req.camadaSignalled || (!excluded && Math.random() < (cfg?.sample ?? 1))) {\n this.hookCapture(req, res, path, query, ip, { rid, sid, newSession, t0 });\n }\n return false;\n }\n\n /** Mints the request's rid and session (setting the cookie on a first visit), records them on\n * `req.camada` and sets `x-rid`. handle() and serveChallenge() (when handle() did not run) share it. */\n private begin(req: CamadaRequest, res: ServerResponse, ip: string | null, t0: number, wrn?: string): NonNullable<CamadaRequest['camada']> {\n const rid = randomUUID();\n let sid = cookieValue((req.headers.cookie as string) || '', SESSION_COOKIE);\n const ns = !sid;\n if (!sid) {\n sid = randomUUID();\n const https = (req.socket as { encrypted?: boolean }).encrypted || req.headers['x-forwarded-proto'] === 'https';\n keepSessionCookie(res, `${SESSION_COOKIE}=${sid}; Path=/; Max-Age=2592000; HttpOnly; SameSite=Lax${https ? '; Secure' : ''}`);\n }\n res.setHeader('x-rid', rid);\n return (req.camada = { rid, ip, sid, ns, t0, wrn });\n }\n\n /** Installs the response-finish hook that ships the request's normal capture event. */\n private hookCapture(req: CamadaRequest, res: ServerResponse, path: string, query: string, ip: string | null,\n o: { rid: string; sid: string | null; newSession: boolean; t0: number }): void {\n // 'finish' when the response went out whole; 'close' alone when the client left first (an\n // aborted SSE or download), which ships the status set so far and dur up to the abort — the\n // fetch adapters' cancel semantics. 'close' also follows every 'finish': one event either way.\n let shipped = false;\n req.camadaOpen = true;\n const ship = () => guarded(() => {\n req.camadaOpen = false;\n if (shipped) return;\n shipped = true;\n // serveChallenge() may have answered from inside the app, and it already shipped the\n // `blk: \"challenge\"` row — one request, one event. A track() row queued while the\n // response was open still needs this finish flush (serverless).\n if (req.camadaChallenged) { if (this.env?.serverless) this.flushFor(req); return; }\n const ev = this.buildEvent(req, path, query, ip, o);\n ev.ts = o.t0; // the request start: the timeline draws [ts, ts + dur]\n ev.st = res.statusCode;\n ev.dur = Date.now() - o.t0;\n if (req.route?.path) ev.rt = String(req.route.path);\n if (req.camada?.wrn) ev.wrn = req.camada.wrn; // §D3: the warn rule that let this request through\n if (req.camadaSignalled) ev.chs = 1; // challenge signalled (camada-all-82ar)\n this.enqueue(ev, req);\n }, undefined);\n res.once('finish', ship);\n res.once('close', ship);\n }\n\n private buildEvent(req: CamadaRequest, path: string, query: string, ip: string | null, o: { rid: string; sid: string | null; newSession: boolean }): WireEvent {\n const raw = req.rawHeaders || [];\n const headers: Array<[string, string]> = [];\n for (let i = 0; i + 1 < raw.length; i += 2) headers.push([raw[i], raw[i + 1]]);\n const ev = buildWireEvent(\n { method: req.method || 'GET', host: (req.headers.host as string) || '', path, query, headers, ip, httpVersion: req.httpVersion },\n { tap: TAP_NODE, rid: o.rid, sid: o.sid, newSession: o.newSession },\n );\n const tlsx = this.tlsxOf(req); // same trusted-header gate as matching (camada-all-bcsf)\n if (tlsx) ev.tlsx = tlsx;\n return ev;\n }\n\n /** Reads the beacon POST (≤32 KB), answers 204 immediately, and queues the beacon as a `sig: 1`\n * row with the trusted-proxy-resolved client IP: it rides the next event batch, so the analyst\n * sees one request per flush instead of one per page view. Junk bodies are dropped, never shipped.\n * A body parser mounted before camada (express.json(), always on Nest) has already drained the\n * stream: its `req.body` is used instead, and the request is answered either way. */\n private relayBeacon(req: CamadaRequest, res: ServerResponse, ip: string | null): void {\n const accept = (parsed: unknown) => {\n res.writeHead(204, { 'cache-control': 'no-store' });\n res.end();\n if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed) || Buffer.isBuffer(parsed)) return;\n const row: Record<string, unknown> = { ...(parsed as Record<string, unknown>), sig: 1, ip, tap: TAP_NODE, rts: Date.now() };\n for (const k of REQUEST_ROW_KEYS) delete row[k];\n this.enqueue(row);\n };\n const fromText = (text: string) => { try { return JSON.parse(text) as unknown; } catch { return null; } };\n if (req.readableEnded) {\n const b = parsedBody(req);\n if ((typeof b === 'string' ? b : b ? JSON.stringify(b) : '').length > FP_MAX) { res.writeHead(413).end(); return; }\n accept(typeof b === 'string' ? fromText(b) : b);\n return;\n }\n const chunks: Buffer[] = [];\n let size = 0, dead = false;\n req.on('data', (c: Buffer) => {\n size += c.length;\n if (size > FP_MAX) { dead = true; req.removeAllListeners('data'); res.writeHead(413).end(); return; }\n chunks.push(c);\n });\n req.on('end', () => guarded(() => {\n if (dead) return;\n accept(fromText(Buffer.concat(chunks).toString('utf8')));\n }, undefined));\n req.on('error', () => { try { res.destroy(); } catch { /* already gone */ } });\n }\n\n private challengePassed(req: IncomingMessage, ip: string | null): boolean {\n return !!this.kit?.tokenValid(ip, Date.now(), cookieValue((req.headers.cookie as string) || '', CHALLENGE_COOKIE));\n }\n\n /** 403 + the proof-of-work page (HTML navigations only), plus the\n * `blk: \"challenge\"` event — a served challenge is reported like a block (contract §D2). */\n private serveChallengeInner(req: CamadaRequest, res: ServerResponse, ip: string, target: string): void {\n const to = safeReturnTo(target);\n writeChallengePage(res, { nonce: this.kit!.nonce(ip, Date.now()), action: this.challengeAction(req), to });\n // The response is out; telemetry must never be able to undo that (a throw here would make\n // guarded() report the request as unhandled and let the app write to an ended response).\n guarded(() => {\n const qi = target.indexOf('?');\n const ev = this.buildEvent(req, qi === -1 ? target : target.slice(0, qi), qi === -1 ? '' : target.slice(qi), ip,\n { rid: randomUUID(), sid: this.sessionOf(req), newSession: false });\n ev.st = 403;\n ev.blk = 'challenge';\n this.enqueue(ev);\n }, undefined);\n }\n\n /** The request's existing session, so challenge rows join the session that produced them. */\n private sessionOf(req: IncomingMessage): string | null {\n return cookieValue((req.headers.cookie as string) || '', SESSION_COOKIE);\n }\n\n /** POST from the challenge page: validate the nonce and the proof of work, set `_cch`, 302\n * back to the (sanitised, same-site) original URL, and ship `{ st: 200, ch: 1 }`. */\n private verifyChallenge(req: CamadaRequest, res: ServerResponse, ip: string): void {\n readForm(req, (form) => guarded(() => {\n const to = safeReturnTo(form.to);\n const now = Date.now();\n if (!this.kit!.verify(ip, now, form.nonce, form.solution)) {\n writeChallengePage(res, { nonce: this.kit!.nonce(ip, now), action: this.challengeAction(req), to });\n return;\n }\n res.writeHead(302, {\n location: to,\n 'set-cookie': challengeCookie(this.kit!.issue(ip, now), isHttps(req)),\n 'cache-control': 'no-store',\n });\n res.end();\n const ev = this.buildEvent(req, this.challengeAction(req), '', ip, { rid: randomUUID(), sid: this.sessionOf(req), newSession: false });\n ev.st = 200;\n ev.ch = 1; // challenge passed (contract §A3 ingest field)\n this.enqueue(ev);\n }, undefined));\n }\n\n /**\n * Records the upgrades (WebSocket handshakes) a node:http or node:https server hands to its\n * 'upgrade' listeners: Node never routes those through the request handler the middleware runs\n * in. Each one ships one event with `st: 101` once the app's listeners have run. Observe only:\n * the request is never blocked or challenged, nothing is written to the socket, no session\n * cookie is minted, and a server with no 'upgrade' listener of its own behaves exactly as before\n * (this wraps `server.emit` rather than adding a listener, which would claim every upgrade).\n * Idempotent; returns the server. A handshake the WebSocket library then rejects still ships 101.\n */\n attach<S extends Server>(server: S): S {\n guarded(() => {\n const s = server as S & { [ATTACHED]?: true };\n if (this.disabled || !this.queue || s[ATTACHED]) return;\n s[ATTACHED] = true;\n const emit = s.emit.bind(s) as (event: string | symbol, ...args: unknown[]) => boolean;\n s.emit = ((event: string | symbol, ...args: unknown[]) => {\n if (event !== 'upgrade') return emit(event, ...args);\n const t0 = Date.now();\n try {\n return emit(event, ...args);\n } finally {\n guarded(() => this.shipUpgrade(args[0] as CamadaRequest, t0), undefined);\n }\n }) as S['emit'];\n }, undefined);\n return server;\n }\n\n private shipUpgrade(req: CamadaRequest, t0: number): void {\n if (this.disabled || !this.queue) return;\n const { path, query } = pathAndQuery(req);\n const cfg = this.snap?.config;\n if ((cfg?.exclude || []).some((x) => path.startsWith(x)) || Math.random() >= (cfg?.sample ?? 1)) return;\n const ip = this.clientIp(req);\n const ev = this.buildEvent(req, path, query, ip, { rid: randomUUID(), sid: this.sessionOf(req), newSession: false });\n ev.ts = t0;\n ev.st = 101;\n ev.dur = Date.now() - t0;\n this.enqueue(ev);\n }\n\n /** Serve the challenge for this request on demand — for a route the app wants to gate itself\n * (the example's /challenge-me). Returns false when the client already holds a valid `_cch`,\n * so the caller renders its own page. A non-HTML request is never challenged: it gets the\n * `x-camada-challenge: 1` header and a `chs: 1` event, and false, so the app answers. */\n serveChallenge(req: IncomingMessage, res: ServerResponse): boolean {\n return guarded(() => {\n if (this.disabled || !this.kit || !this.queue) return false;\n const ip = this.clientIp(req);\n if (!ip || this.challengePassed(req, ip)) return false; // unidentifiable client: fail open\n const r = req as CamadaRequest;\n const { path, query } = pathAndQuery(r);\n if (!isPage(r)) {\n // Not a page: the app renders, flagged. Its normal event ships with chs: 1.\n if (!r.camadaSignalled) signal(res); // handle() may have done it already\n r.camadaSignalled = true;\n if (r.camadaOpen === undefined) {\n const c = r.camada ?? this.begin(r, res, ip, Date.now()); // handle() ran (sampled out): its ids; else mint them as it would\n this.hookCapture(r, res, path, query, ip, { rid: c.rid, sid: c.sid, newSession: c.ns, t0: c.t0 });\n }\n return false;\n }\n r.camadaChallenged = true;\n this.serveChallengeInner(req as CamadaRequest, res, ip, path + query);\n return true;\n }, false);\n }\n\n /** For HTML templates: the first-party beacon tag with the request's rid. */\n scriptTag(req: IncomingMessage): string {\n if (this.disabled || !this.beaconEnabled()) return '';\n const rid = (req as CamadaRequest).camada?.rid;\n const r = req as CamadaRequest;\n const src = this.scriptPathSet ? this.scriptPath : (r.camadaPrefix ?? mountPrefix(r)) + this.scriptPath;\n return `<script src=\"${src}${rid ? `?r=${rid}` : ''}\" async></script>`;\n }\n\n /** App-context outcome events (login failed, signup, …). The identifier is HMAC-hashed\n * in-process; the raw value never reaches the queue. */\n track(req: IncomingMessage, event: string, data?: { user?: string }): void {\n guarded(() => {\n if (this.disabled || !this.queue || !this.env) return;\n const ctx = (req as CamadaRequest).camada;\n const r = req as CamadaRequest;\n const row = (async () => {\n const uid = data?.user ? await hashUserId(data.user, this.env!.ingestToken, (globalThis.crypto ?? webcrypto).subtle) : null;\n this.enqueue({ tap: TAP_NODE, et: event, uid, rid: ctx?.rid ?? null, sid: ctx?.sid ?? null, ip: ctx?.ip ?? null, ts: Date.now() }, r);\n })().catch(logRateLimited);\n if (r.camadaOpen) (r.camadaTracks ??= []).push(row);\n }, undefined);\n }\n\n /** Express/Connect middleware (also what NestJS on the default platform consumes). */\n express() {\n return (req: IncomingMessage, res: ServerResponse, next: (err?: unknown) => void): void => {\n if (!this.handle(req, res)) next();\n };\n }\n\n /** Stops polling and the event queue. The promise settles once rows still queued behind a drain\n * in flight or a 429 hold are sent (or counted dropped); it never rejects. */\n stop(): Promise<void> {\n this.snap?.stop();\n return this.queue?.stop() ?? Promise.resolve();\n }\n}\n\nexport function createCamada(opts: CamadaOptions = {}): Camada {\n const c = new Camada(opts);\n if (!c.env) logRateLimited(new Error('CAMADA_KEY (or CAMADA_TOKEN + CAMADA_SNAPSHOT_TOKEN) not set — camada is inactive'));\n return c;\n}\n","// Environment wiring. The two-line quickstart depends on this file doing the right thing:\n// CAMADA_KEY=<ingest_token>.<snap_token> (printed by `reconcile instructions` and seed)\n// CAMADA_INGEST_URL / CAMADA_SNAPSHOT_URL (dev: http://localhost:8787[/snapshot])\n// CAMADA_DISABLED=1 kill switch, checked per request\n// CAMADA_SERVERLESS=1 lazy snapshot mode (no interval timer), flush per request\n// CAMADA_TLSX_HEADER opt-in: header carrying the TLS fingerprint (trusted-proxy cidrs mode only)\n// CAMADA_TRUSTED_PROXY local override: none | vercel | hops:N | cidrs:a,b\nimport { parseKey, parseTrustedProxyEnv, type TrustedProxyConfig } from '@camada/core';\n\nexport { parseTrustedProxyEnv }; // re-export: this was @camada/node's public surface before it moved to core\n\nexport interface ResolvedEnv {\n ingestToken: string;\n snapToken: string;\n secret: string; // HMAC key for the challenge nonce/cookie — never leaves the process\n ingestUrl: string;\n snapshotUrl: string;\n serverless: boolean;\n trustedProxy: TrustedProxyConfig | null; // null = defer to server-delivered config\n tlsxHeader: string | null; // lower-cased header name, null = tlsx never matches here\n}\n\n/** A usable header name (RFC 9110 token), lower-cased; anything else is \"off\". */\nexport function parseTlsxHeader(raw: string | undefined | null): string | null {\n const n = (raw ?? '').trim().toLowerCase();\n return /^[a-z0-9!#$%&'*+.^_`|~-]+$/.test(n) ? n : null;\n}\n\n/** Returns null (SDK stays disabled, one log line) rather than throwing on bad config. */\nexport function resolveEnv(env: Record<string, string | undefined> = process.env): ResolvedEnv | null {\n const key = parseKey(env.CAMADA_KEY);\n const ingestToken = key?.ingestToken ?? env.CAMADA_TOKEN;\n const snapToken = key?.snapToken ?? env.CAMADA_SNAPSHOT_TOKEN;\n if (!ingestToken || !snapToken) return null;\n const ingestUrl = (env.CAMADA_INGEST_URL || 'https://in.camada.app').replace(/\\/$/, '');\n return {\n ingestToken, snapToken, ingestUrl,\n secret: env.CAMADA_KEY || `${ingestToken}.${snapToken}`,\n snapshotUrl: env.CAMADA_SNAPSHOT_URL || `${ingestUrl}/snapshot`,\n serverless: env.CAMADA_SERVERLESS === '1',\n trustedProxy: parseTrustedProxyEnv(env.CAMADA_TRUSTED_PROXY),\n tlsxHeader: parseTlsxHeader(env.CAMADA_TLSX_HEADER),\n };\n}\n","// The challenge for @camada/node (SDK-04). node:crypto gives us a synchronous HMAC, so the\n// cookie check costs nothing on the hot path and Camada.handle() stays a synchronous boolean.\nimport type { IncomingMessage, ServerResponse } from 'node:http';\nimport { createHash, createHmac } from 'node:crypto';\nimport { createChallenge, challengePage, challengePageCsp, parseFormBody, type ChallengeKit, type ChallengePageOptions } from '@camada/core';\n\nexport const CHALLENGE_PATH = '/__camada/challenge';\nconst BODY_MAX = 4 * 1024; // the verify form is ~120 bytes; anything larger is not ours\n\nexport function nodeChallengeKit(secret: string): ChallengeKit {\n return createChallenge({\n secret,\n hmac: (s, m) => createHmac('sha256', s).update(m).digest('hex'),\n sha: (m) => createHash('sha256').update(m).digest('hex'),\n });\n}\n\n/** Reads at most BODY_MAX bytes; hands back '' when the client sends more (or errors). */\nexport function readBody(req: IncomingMessage, done: (body: string) => void): void {\n const chunks: Buffer[] = [];\n let size = 0, dead = false;\n req.on('data', (c: Buffer) => {\n if (dead) return;\n size += c.length;\n if (size > BODY_MAX) { dead = true; chunks.length = 0; return; }\n chunks.push(c);\n });\n req.on('end', () => done(dead ? '' : Buffer.concat(chunks).toString('utf8')));\n req.on('error', () => done(''));\n}\n\nconst FORM_FIELDS = ['nonce', 'solution', 'to'] as const;\n\n/** What a body parser mounted before camada left on `req.body` once it drained the stream: the\n * text of a string/Buffer body (text/raw parsers), the object of a parsed one (json, urlencoded,\n * Koa's parsers via the adapter), or null when nothing usable is left. */\nexport function parsedBody(req: IncomingMessage & { body?: unknown }): string | Record<string, unknown> | null {\n const b = req.body;\n if (typeof b === 'string') return b;\n if (Buffer.isBuffer(b)) return b.toString('utf8');\n return b && typeof b === 'object' ? (b as Record<string, unknown>) : null;\n}\n\n/** The verify POST's form fields, always delivered (never leaves the request hanging). A body\n * parser mounted before camada (express.urlencoded(), always on Nest) has already drained the\n * stream, so its `'end'` will never fire again: use what it left behind (parsedBody). Only\n * string fields count. A drained stream with nothing usable, or an oversized one, yields {},\n * which fails verification and re-serves the page at once. */\nexport function readForm(req: IncomingMessage & { body?: unknown }, done: (form: Record<string, string>) => void): void {\n if (!req.readableEnded) { readBody(req, (body) => done(parseFormBody(body))); return; }\n const b = parsedBody(req);\n if (typeof b === 'string') { done(b.length > BODY_MAX ? {} : parseFormBody(b)); return; }\n const form: Record<string, string> = Object.create(null);\n for (const k of FORM_FIELDS) {\n const v = b?.[k];\n if (typeof v === 'string') form[k] = v;\n }\n done(form);\n}\n\nexport const isHttps = (req: IncomingMessage): boolean =>\n !!(req.socket as { encrypted?: boolean } | undefined)?.encrypted || req.headers['x-forwarded-proto'] === 'https';\n\n/** Writes `challengePage(o)` with its own CSP for the same `bits` (only its inline solver and\n * style, by hash): a policy the app set earlier (helmet before camada) would otherwise ride along\n * on this response and block the solver. */\nexport function writeChallengePage(res: ServerResponse, o: ChallengePageOptions): void {\n res.removeHeader('content-security-policy');\n res.removeHeader('content-security-policy-report-only');\n res.writeHead(403, {\n 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-store', 'x-camada-challenge': '1',\n 'content-security-policy': challengePageCsp(o.bits),\n });\n res.end(challengePage(o));\n}\n","{\n \"version\": \"0.4.0\",\n \"license\": \"MIT\",\n \"type\": \"module\",\n \"engines\": {\n \"node\": \">=20\"\n },\n \"files\": [\n \"dist\",\n \"README.md\",\n \"LICENSE\"\n ],\n \"scripts\": {\n \"build\": \"tsup\",\n \"dev\": \"tsup --watch\",\n \"test\": \"vitest run\",\n \"check\": \"tsc --noEmit\"\n },\n \"name\": \"@camada/node\",\n \"description\": \"camada backend SDK for Node: request capture, inline blocking, first-party beacon serving; Express/Fastify/Nest/Koa adapters\",\n \"exports\": {\n \".\": {\n \"import\": {\n \"types\": \"./dist/index.d.ts\",\n \"default\": \"./dist/index.js\"\n },\n \"require\": {\n \"types\": \"./dist/index.d.cts\",\n \"default\": \"./dist/index.cjs\"\n }\n },\n \"./fastify\": {\n \"import\": {\n \"types\": \"./dist/fastify.d.ts\",\n \"default\": \"./dist/fastify.js\"\n },\n \"require\": {\n \"types\": \"./dist/fastify.d.cts\",\n \"default\": \"./dist/fastify.cjs\"\n }\n },\n \"./nest\": {\n \"import\": {\n \"types\": \"./dist/nest.d.ts\",\n \"default\": \"./dist/nest.js\"\n },\n \"require\": {\n \"types\": \"./dist/nest.d.cts\",\n \"default\": \"./dist/nest.cjs\"\n }\n },\n \"./koa\": {\n \"import\": {\n \"types\": \"./dist/koa.d.ts\",\n \"default\": \"./dist/koa.js\"\n },\n \"require\": {\n \"types\": \"./dist/koa.d.cts\",\n \"default\": \"./dist/koa.cjs\"\n }\n }\n },\n \"main\": \"./dist/index.cjs\",\n \"module\": \"./dist/index.js\",\n \"types\": \"./dist/index.d.ts\",\n \"typesVersions\": {\n \"*\": {\n \"fastify\": [\n \"./dist/fastify.d.ts\"\n ],\n \"nest\": [\n \"./dist/nest.d.ts\"\n ],\n \"koa\": [\n \"./dist/koa.d.ts\"\n ]\n }\n },\n \"dependencies\": {\n \"@camada/browser\": \"file:../camada-browser\",\n \"@camada/core\": \"file:../camada-core\"\n },\n \"devDependencies\": {\n \"@koa/bodyparser\": \"^6.1.0\",\n \"@nestjs/common\": \"^11.0.0\",\n \"@nestjs/core\": \"^11.0.0\",\n \"@nestjs/platform-express\": \"^11.0.0\",\n \"@types/express\": \"^4.17.25\",\n \"@types/koa\": \"^2.15.2\",\n \"@types/koa-mount\": \"^4.0.5\",\n \"@types/node\": \"^22.10.0\",\n \"express\": \"^4.21.0\",\n \"fastify\": \"^5.2.0\",\n \"koa\": \"^2.15.0\",\n \"koa-mount\": \"^4.2.0\",\n \"reflect-metadata\": \"^0.2.2\",\n \"rxjs\": \"^7.8.1\",\n \"tsup\": \"^8.3.5\",\n \"typescript\": \"^5.7.2\",\n \"vitest\": \"^3.0.0\"\n },\n \"peerDependencies\": {\n \"@nestjs/common\": \">=10\"\n },\n \"peerDependenciesMeta\": {\n \"@nestjs/common\": {\n \"optional\": true\n }\n }\n}\n","// The SDK's wire identity (SDK-03): x-camada-sdk: <package>/<version>. Named JSON imports are\n// inlined and tree-shaken by tsup at build, so dist carries two literals — no runtime fs read.\nimport { name, version } from '../package.json';\nexport const SDK_ID = `${name}/${version}`;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACMA,IAAAA,sBAAsC;AACtC,IAAAC,eAIO;AACP,sBAAqB;AACrB,yBAAiB;;;ACNjB,kBAAwE;AAgBjE,SAAS,gBAAgB,KAA+C;AAC7E,QAAM,KAAK,OAAO,IAAI,KAAK,EAAE,YAAY;AACzC,SAAO,6BAA6B,KAAK,CAAC,IAAI,IAAI;AACpD;AAGO,SAAS,WAAW,MAA0C,QAAQ,KAAyB;AACpG,QAAM,UAAM,sBAAS,IAAI,UAAU;AACnC,QAAM,cAAc,KAAK,eAAe,IAAI;AAC5C,QAAM,YAAY,KAAK,aAAa,IAAI;AACxC,MAAI,CAAC,eAAe,CAAC,UAAW,QAAO;AACvC,QAAM,aAAa,IAAI,qBAAqB,yBAAyB,QAAQ,OAAO,EAAE;AACtF,SAAO;AAAA,IACL;AAAA,IAAa;AAAA,IAAW;AAAA,IACxB,QAAQ,IAAI,cAAc,GAAG,WAAW,IAAI,SAAS;AAAA,IACrD,aAAa,IAAI,uBAAuB,GAAG,SAAS;AAAA,IACpD,YAAY,IAAI,sBAAsB;AAAA,IACtC,kBAAc,kCAAqB,IAAI,oBAAoB;AAAA,IAC3D,YAAY,gBAAgB,IAAI,kBAAkB;AAAA,EACpD;AACF;;;ACxCA,yBAAuC;AACvC,IAAAC,eAA8H;AAEvH,IAAM,iBAAiB;AAC9B,IAAM,WAAW,IAAI;AAEd,SAAS,iBAAiB,QAA8B;AAC7D,aAAO,8BAAgB;AAAA,IACrB;AAAA,IACA,MAAM,CAAC,GAAG,UAAM,+BAAW,UAAU,CAAC,EAAE,OAAO,CAAC,EAAE,OAAO,KAAK;AAAA,IAC9D,KAAK,CAAC,UAAM,+BAAW,QAAQ,EAAE,OAAO,CAAC,EAAE,OAAO,KAAK;AAAA,EACzD,CAAC;AACH;AAGO,SAAS,SAAS,KAAsB,MAAoC;AACjF,QAAM,SAAmB,CAAC;AAC1B,MAAI,OAAO,GAAG,OAAO;AACrB,MAAI,GAAG,QAAQ,CAAC,MAAc;AAC5B,QAAI,KAAM;AACV,YAAQ,EAAE;AACV,QAAI,OAAO,UAAU;AAAE,aAAO;AAAM,aAAO,SAAS;AAAG;AAAA,IAAQ;AAC/D,WAAO,KAAK,CAAC;AAAA,EACf,CAAC;AACD,MAAI,GAAG,OAAO,MAAM,KAAK,OAAO,KAAK,OAAO,OAAO,MAAM,EAAE,SAAS,MAAM,CAAC,CAAC;AAC5E,MAAI,GAAG,SAAS,MAAM,KAAK,EAAE,CAAC;AAChC;AAEA,IAAM,cAAc,CAAC,SAAS,YAAY,IAAI;AAKvC,SAAS,WAAW,KAAoF;AAC7G,QAAM,IAAI,IAAI;AACd,MAAI,OAAO,MAAM,SAAU,QAAO;AAClC,MAAI,OAAO,SAAS,CAAC,EAAG,QAAO,EAAE,SAAS,MAAM;AAChD,SAAO,KAAK,OAAO,MAAM,WAAY,IAAgC;AACvE;AAOO,SAAS,SAAS,KAA2C,MAAoD;AACtH,MAAI,CAAC,IAAI,eAAe;AAAE,aAAS,KAAK,CAAC,SAAS,SAAK,4BAAc,IAAI,CAAC,CAAC;AAAG;AAAA,EAAQ;AACtF,QAAM,IAAI,WAAW,GAAG;AACxB,MAAI,OAAO,MAAM,UAAU;AAAE,SAAK,EAAE,SAAS,WAAW,CAAC,QAAI,4BAAc,CAAC,CAAC;AAAG;AAAA,EAAQ;AACxF,QAAM,OAA+B,uBAAO,OAAO,IAAI;AACvD,aAAW,KAAK,aAAa;AAC3B,UAAM,IAAI,IAAI,CAAC;AACf,QAAI,OAAO,MAAM,SAAU,MAAK,CAAC,IAAI;AAAA,EACvC;AACA,OAAK,IAAI;AACX;AAEO,IAAM,UAAU,CAAC,QACtB,CAAC,CAAE,IAAI,QAAgD,aAAa,IAAI,QAAQ,mBAAmB,MAAM;AAKpG,SAAS,mBAAmB,KAAqB,GAA+B;AACrF,MAAI,aAAa,yBAAyB;AAC1C,MAAI,aAAa,qCAAqC;AACtD,MAAI,UAAU,KAAK;AAAA,IACjB,gBAAgB;AAAA,IAA4B,iBAAiB;AAAA,IAAY,sBAAsB;AAAA,IAC/F,+BAA2B,+BAAiB,EAAE,IAAI;AAAA,EACpD,CAAC;AACD,MAAI,QAAI,4BAAc,CAAC,CAAC;AAC1B;;;ACzEE,cAAW;AAiBX,WAAQ;;;ACfH,IAAM,SAAS,GAAG,IAAI,IAAI,OAAO;;;AJiBxC,IAAM,iBAAiB;AACvB,IAAM,cAAc;AACpB,IAAM,UAAU;AAChB,IAAM,SAAS,KAAK;AAIpB,SAAS,eAAe,MAAuB;AAC7C,QAAM,QAAI,sBAAK,IAAI;AACnB,MAAI,MAAM,EAAG,QAAO,KAAK,WAAW,MAAM;AAC1C,SAAO,MAAM,KAAK,SAAS,SAAS,iBAAiB,KAAK,IAAI;AAChE;AACA,IAAM,UAAU;AAChB,IAAM,WAAW,uBAAO,IAAI,iBAAiB;AAgC7C,IAAM,eAAe,CAAC,QAAyB,CAACC,UAAgC;AAC9E,QAAM,IAAI,IAAI,QAAQA,KAAI;AAC1B,SAAO,KAAK,OAAO,OAAO,MAAM,QAAQ,CAAC,IAAI,EAAE,KAAK,IAAI,IAAI;AAC9D;AAEA,IAAM,WAAW,CAAC,QAAiD;AACjE,QAAM,KAAK,IAAI,QAAQ,GAAG;AAC1B,SAAO,OAAO,KAAK,EAAE,MAAM,KAAK,OAAO,GAAG,IAAI,EAAE,MAAM,IAAI,MAAM,GAAG,EAAE,GAAG,OAAO,IAAI,MAAM,EAAE,EAAE;AAC/F;AAEA,IAAM,SAAS,CAAC,YACd,wBAAW,IAAI,QAAQ,UAAqB,MAAO,IAAI,QAAQ,gBAAgB,KAAgB,IAAI;AAMrG,SAAS,aAAa,KAAqD;AACzE,SAAO,SAAS,IAAI,eAAe,IAAI,OAAO,GAAG;AACnD;AAMA,IAAM,aAAa,CAAC,KAAoB,MAAc,aACpD,SAAS,YAAY,SAAS,IAAI,OAAO,GAAG,EAAE,SAAS;AAKzD,SAAS,YAAY,KAA4B;AAC/C,MAAI,CAAC,IAAI,YAAa,QAAO;AAC7B,QAAM,OAAO,SAAS,IAAI,WAAW,EAAE,MAAM,MAAM,SAAS,IAAI,OAAO,GAAG,EAAE;AAC5E,SAAO,QAAQ,OAAO,SAAS,OAAO,KAAK,SAAS,GAAG,IAAI,KAAK,MAAM,GAAG,KAAK,SAAS,IAAI,MAAM,IAAI;AACvG;AAKA,SAAS,eAAe,QAA0B;AAChD,QAAM,MAAO,QAAiF;AAC9F,MAAI,CAAC,IAAK,QAAO;AACjB,MAAI;AAAE,QAAI,OAAO,IAAI,UAAU,MAAM,SAAU,QAAO;AAAA,EAAM,QAAQ;AAAA,EAAyB;AAC7F,SAAO,OAAO,IAAI,cAAc;AAClC;AAEA,IAAM,cAAc,CAAC,QAAgBA,UAAgC;AACnE,QAAM,MAAM,OAAO;AACnB,QAAM,IAAI,IAAI,QAAQ,OAAOA,QAAO,GAAG;AACvC,MAAI,MAAM,GAAI,QAAO;AACrB,QAAM,QAAQ,IAAIA,MAAK,SAAS;AAChC,QAAM,IAAI,IAAI,QAAQ,KAAK,KAAK;AAChC,SAAO,IAAI,MAAM,OAAO,MAAM,KAAK,SAAY,CAAC;AAClD;AAKA,SAAS,kBAAkB,KAAqB,QAAsB;AACpE,QAAM,MAAM,IAAI;AAChB,MAAI,YAAY,SAAgCA,OAAc,OAA4C;AACxG,QAAI,OAAOA,UAAS,YAAYA,MAAK,YAAY,MAAM,cAAc;AACnE,YAAM,OAAO,MAAM,QAAQ,KAAK,IAAI,QAAQ,CAAC,OAAO,KAAK,CAAC;AAC1D,UAAI,CAAC,KAAK,KAAK,CAAC,MAAM,OAAO,CAAC,EAAE,UAAU,EAAE,WAAW,iBAAiB,GAAG,CAAC,EAAG,SAAQ,CAAC,GAAG,MAAM,MAAM;AAAA,IACzG;AACA,WAAO,IAAI,KAAK,MAAMA,OAAM,KAAK;AAAA,EACnC;AACA,MAAI,UAAU,cAAc,MAAM;AACpC;AAGA,IAAM,mBAAmB,CAAC,iBAAiB,qBAAqB,qBAAqB,gCAAgC,4BAA4B,2BAA2B;AAO5K,SAAS,OAAO,KAA2B;AACzC,QAAM,MAAM,IAAI,WAAW,YAAY,IAAI;AAC3C,QAAM,YAAY,CAAC,MAAe,OAAO,MAAM,YAAY,iBAAiB,SAAS,EAAE,YAAY,CAAC;AACpG,QAAM,QAAQ,CAAC,MAAwB;AACrC,QAAI,MAAM,QAAQ,CAAC,GAAG;AACpB,UAAI,EAAE,UAAU,MAAM,QAAQ,EAAE,CAAC,CAAC,EAAG,QAAO,EAAE,OAAO,CAAC,MAAM,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC;AAC5E,YAAM,MAAiB,CAAC;AACxB,eAAS,IAAI,GAAG,IAAI,EAAE,QAAQ,KAAK,EAAG,KAAI,CAAC,UAAU,EAAE,CAAC,CAAC,EAAG,KAAI,KAAK,EAAE,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;AACnF,aAAO;AAAA,IACT;AACA,QAAI,KAAK,OAAO,MAAM,SAAU,QAAO,OAAO,YAAY,OAAO,QAAQ,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC;AAC1G,WAAO;AAAA,EACT;AACA,MAAI,YAAY,SAAgC,WAAmB,MAAiB;AAClF,QAAI,CAAC,KAAK,YAAa,YAAW,KAAK,iBAAkB,KAAI,KAAK,MAAM,GAAG,UAAU;AACrF,WAAQ,UAAkD,KAAK,MAAM,QAAQ,GAAG,KAAK,IAAI,KAAK,CAAC;AAAA,EACjG;AACA,MAAI,UAAU,sBAAsB,GAAG;AACvC,aAAW,KAAK,iBAAkB,KAAI,UAAU,GAAG,UAAU;AAC/D;AAEO,IAAM,SAAN,MAAa;AAAA,EACT;AAAA,EACA,OAA8B;AAAA,EAC9B,QAA2B;AAAA,EACnB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA;AAAA,EACA;AAAA,EACA,MAA2B;AAAA,EAC3B;AAAA,EACA;AAAA,EACA;AAAA,EAEjB,YAAY,OAAsB,CAAC,GAAG;AACpC,SAAK,YAAY,KAAK,OAAO,QAAQ;AACrC,SAAK,YAAY,KAAK,aAAa;AACnC,SAAK,YAAY,KAAK;AACtB,SAAK,aAAa,KAAK,cAAc;AACrC,SAAK,SAAS,KAAK,UAAU;AAC7B,SAAK,cAAc,KAAK,cAAc,SAAS,KAAK,UAAU,qBAAqB;AACnF,SAAK,gBAAgB,KAAK,iBAAiB;AAC3C,SAAK,mBAAmB,KAAK,iBAAiB;AAC9C,SAAK,gBAAgB,KAAK,cAAc;AACxC,SAAK,MAAM,WAAW,KAAK,SAAS;AACpC,SAAK,aAAa,gBAAgB,KAAK,UAAU,KAAK,KAAK,KAAK,cAAc;AAC9E,QAAI,CAAC,KAAK,IAAK;AACf,QAAI,KAAK,UAAU,oBAAoB,IAAK;AAC5C,SAAK,OAAO,IAAI,4BAAe;AAAA,MAC7B,KAAK,KAAK,IAAI;AAAA,MAAa,OAAO,KAAK,IAAI;AAAA,MAC3C,MAAM,KAAK,IAAI,aAAa,SAAS;AAAA,MACrC,WAAW,KAAK;AAAA,MAAW,iBAAiB,KAAK;AAAA,MAAiB,WAAW,KAAK;AAAA,MAAW,KAAK;AAAA,IACpG,CAAC;AACD,SAAK,QAAQ,IAAI,wBAAW,EAAE,KAAK,KAAK,IAAI,WAAW,OAAO,KAAK,IAAI,aAAa,WAAW,KAAK,WAAW,KAAK,OAAO,CAAC;AAC5H,SAAK,MAAM,iBAAiB,KAAK,IAAI,MAAM;AAC3C,SAAK,KAAK,MAAM;AAChB,SAAK,MAAM,qBAAqB;AAAA,EAClC;AAAA,EAEA,IAAI,WAAoB;AAAE,WAAO,KAAK,UAAU,oBAAoB,OAAO,CAAC,KAAK;AAAA,EAAK;AAAA,EAE9E,eAA0C;AAChD,WAAO,KAAK,KAAK,gBAAgB,KAAK,MAAM,QAAQ,iBAAiB;AAAA,EACvE;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQQ,SAAS,KAAqC;AACpD,UAAM,OAAO,IAAI,QAAQ;AACzB,QAAI,CAAC,QAAQ,IAAI,UAAU,CAAC,eAAe,IAAI,MAAM,EAAG,QAAO;AAC/D,eAAO,8BAAgB,QAAQ,MAAM,IAAI,QAAQ,iBAAiB,GAAyB,KAAK,aAAa,CAAC;AAAA,EAChH;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASQ,OAAO,KAAqC;AAClD,UAAM,SAAS,KAAK;AACpB,QAAI,CAAC,OAAQ,QAAO;AACpB,UAAM,IAAI,aAAa,GAAG,EAAE,MAAM;AAClC,QAAI,KAAK,QAAQ,EAAE,SAAS,OAAO,CAAC,QAAQ,KAAK,CAAC,EAAG,QAAO;AAC5D,UAAM,MAAM,KAAK,aAAa;AAC9B,UAAM,OAAO,IAAI,QAAQ;AACzB,QAAI,KAAK,SAAS,WAAW,CAAC,KAAM,QAAO;AAC3C,QAAI,eAAe,IAAI,EAAG,QAAO;AACjC,eAAO,4BAAc,MAAM,GAAG,IAAI,IAAI;AAAA,EACxC;AAAA;AAAA;AAAA,EAIQ,gBAAgB,KAA4B;AAClD,WAAO,KAAK,mBAAmB,KAAK,iBAAiB,IAAI,gBAAgB,YAAY,GAAG,KAAK,KAAK;AAAA,EACpG;AAAA,EAEQ,gBAAyB;AAAE,WAAO,KAAK,MAAM,QAAQ,WAAW;AAAA,EAAO;AAAA;AAAA;AAAA,EAIvE,QAAQ,KAAc,KAA2B;AACvD,UAAM,IAAI,KAAK;AACf,MAAE,KAAK,GAAG;AACV,QAAI,CAAC,KAAK,KAAK,WAAY;AAC3B,QAAI,KAAK,WAAY;AACrB,SAAK,SAAS,GAAG;AAAA,EACnB;AAAA;AAAA,EAGQ,SAAS,KAA2B;AAC1C,UAAM,IAAI,KAAK;AACf,8BAAQ,MAAM;AAIZ,YAAM,SAAS,KAAK,gBAAgB,CAAC;AACrC,YAAM,IAAI,QAAQ,WAAW,MAAM,EAChC,KAAK,MAAM,EAAE,MAAM,CAAC,EACpB,KAAK,MAAO,EAAE,OAAO,IAAI,EAAE,MAAM,IAAI,MAAU;AAClD,WAAK,YAAY,CAAC;AAAA,IACpB,GAAG,MAAS;AAAA,EACd;AAAA;AAAA,EAGA,OAAO,KAAsB,KAA8B;AACzD,eAAO,sBAAQ,MAAM,KAAK,YAAY,KAAsB,GAAG,GAAG,KAAK;AAAA,EACzE;AAAA,EAEQ,YAAY,KAAoB,KAA8B;AACpE,QAAI,KAAK,YAAY,CAAC,KAAK,QAAQ,CAAC,KAAK,SAAS,CAAC,KAAK,IAAK,QAAO;AACpE,UAAM,KAAK,KAAK,IAAI;AACpB,SAAK,KAAK,YAAY,KAAK,IAAI,aAAa,KAAK,YAAY,MAAS;AAEtE,UAAM,EAAE,MAAM,MAAM,IAAI,aAAa,GAAG;AACxC,UAAM,KAAK,KAAK,SAAS,GAAG;AAC5B,QAAI,eAAe,YAAY,GAAG;AAKlC,UAAM,IAAI,KAAK,KAAK,QAAQ,EAAE,IAAI,MAAM,IAAI,IAAI,QAAQ,YAAY,GAAG,QAAQ,aAAa,GAAG,GAAG,MAAM,KAAK,OAAO,GAAG,EAAE,CAAC;AAC1H,QAAI,EAAE,OAAO;AACX,YAAM,UAAkC,EAAE,kBAAkB,EAAE,UAAU,IAAI,mBAAmB,EAAE,WAAW,IAAI,gBAAgB,cAAc,iBAAiB,WAAW;AAC1K,UAAI,EAAE,KAAM,SAAQ,cAAc,IAAI,EAAE;AACxC,UAAI,UAAU,KAAK,OAAO;AAC1B,UAAI,IAAI,WAAW;AACnB,YAAM,KAAK,KAAK,WAAW,KAAK,MAAM,OAAO,IAAI,EAAE,SAAK,gCAAW,GAAG,KAAK,MAAM,YAAY,MAAM,CAAC;AACpG,SAAG,KAAK;AACR,SAAG,MAAM,EAAE;AACX,UAAI,EAAE,KAAM,IAAG,KAAK,EAAE;AACtB,WAAK,QAAQ,EAAE;AACf,aAAO;AAAA,IACT;AAOA,QAAI,KAAK,eAAe,KAAK,OAAO,IAAI;AAEtC,UAAI,IAAI,WAAW,UAAU,WAAW,KAAK,MAAM,KAAK,aAAa,GAAG;AAAE,aAAK,gBAAgB,KAAK,KAAK,EAAE;AAAG,eAAO;AAAA,MAAM;AAC3H,UAAI,EAAE,aAAa,CAAC,KAAK,gBAAgB,KAAK,EAAE,GAAG;AAEjD,YAAI,OAAO,GAAG,GAAG;AAAE,eAAK,oBAAoB,KAAK,KAAK,IAAI,OAAO,KAAK;AAAG,iBAAO;AAAA,QAAM;AACtF,YAAI,kBAAkB;AAAA,MACxB;AAAA,IACF;AAEA,QAAI,KAAK,cAAc,GAAG;AACxB,UAAI,IAAI,WAAW,SAAS,WAAW,KAAK,MAAM,KAAK,UAAU,GAAG;AAClE,YAAI,UAAU,KAAK,EAAE,gBAAgB,0BAA0B,iBAAiB,uBAAuB,CAAC;AACxG,YAAI,IAAI,mBAAAC,OAAI;AACZ,eAAO;AAAA,MACT;AACA,UAAI,IAAI,WAAW,UAAU,WAAW,KAAK,MAAM,KAAK,MAAM,GAAG;AAC/D,aAAK,YAAY,KAAK,KAAK,EAAE;AAC7B,eAAO;AAAA,MACT;AAAA,IACF;AAEA,UAAM,EAAE,KAAK,KAAK,IAAI,WAAW,IAAI,KAAK,MAAM,KAAK,KAAK,IAAI,IAAI,EAAE,OAAO,EAAE,OAAO,MAAS;AAC7F,QAAI,IAAI,gBAAiB,QAAO,GAAG;AAEnC,UAAM,MAAM,KAAK,KAAK;AACtB,UAAM,YAAY,KAAK,WAAW,CAAC,GAAG,KAAK,CAAC,MAAM,KAAK,WAAW,CAAC,CAAC;AAEpE,QAAI,IAAI,mBAAoB,CAAC,YAAY,KAAK,OAAO,KAAK,KAAK,UAAU,IAAK;AAC5E,WAAK,YAAY,KAAK,KAAK,MAAM,OAAO,IAAI,EAAE,KAAK,KAAK,YAAY,GAAG,CAAC;AAAA,IAC1E;AACA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA,EAIQ,MAAM,KAAoB,KAAqB,IAAmB,IAAY,KAAoD;AACxI,UAAM,UAAM,gCAAW;AACvB,QAAI,MAAM,YAAa,IAAI,QAAQ,UAAqB,IAAI,cAAc;AAC1E,UAAM,KAAK,CAAC;AACZ,QAAI,CAAC,KAAK;AACR,gBAAM,gCAAW;AACjB,YAAM,QAAS,IAAI,OAAmC,aAAa,IAAI,QAAQ,mBAAmB,MAAM;AACxG,wBAAkB,KAAK,GAAG,cAAc,IAAI,GAAG,oDAAoD,QAAQ,aAAa,EAAE,EAAE;AAAA,IAC9H;AACA,QAAI,UAAU,SAAS,GAAG;AAC1B,WAAQ,IAAI,SAAS,EAAE,KAAK,IAAI,KAAK,IAAI,IAAI,IAAI;AAAA,EACnD;AAAA;AAAA,EAGQ,YAAY,KAAoB,KAAqB,MAAc,OAAe,IACxF,GAA+E;AAI/E,QAAI,UAAU;AACd,QAAI,aAAa;AACjB,UAAM,OAAO,UAAM,sBAAQ,MAAM;AAC/B,UAAI,aAAa;AACjB,UAAI,QAAS;AACb,gBAAU;AAIV,UAAI,IAAI,kBAAkB;AAAE,YAAI,KAAK,KAAK,WAAY,MAAK,SAAS,GAAG;AAAG;AAAA,MAAQ;AAClF,YAAM,KAAK,KAAK,WAAW,KAAK,MAAM,OAAO,IAAI,CAAC;AAClD,SAAG,KAAK,EAAE;AACV,SAAG,KAAK,IAAI;AACZ,SAAG,MAAM,KAAK,IAAI,IAAI,EAAE;AACxB,UAAI,IAAI,OAAO,KAAM,IAAG,KAAK,OAAO,IAAI,MAAM,IAAI;AAClD,UAAI,IAAI,QAAQ,IAAK,IAAG,MAAM,IAAI,OAAO;AACzC,UAAI,IAAI,gBAAiB,IAAG,MAAM;AAClC,WAAK,QAAQ,IAAI,GAAG;AAAA,IACtB,GAAG,MAAS;AACZ,QAAI,KAAK,UAAU,IAAI;AACvB,QAAI,KAAK,SAAS,IAAI;AAAA,EACxB;AAAA,EAEQ,WAAW,KAAoB,MAAc,OAAe,IAAmB,GAAwE;AAC7J,UAAM,MAAM,IAAI,cAAc,CAAC;AAC/B,UAAM,UAAmC,CAAC;AAC1C,aAAS,IAAI,GAAG,IAAI,IAAI,IAAI,QAAQ,KAAK,EAAG,SAAQ,KAAK,CAAC,IAAI,CAAC,GAAG,IAAI,IAAI,CAAC,CAAC,CAAC;AAC7E,UAAM,SAAK;AAAA,MACT,EAAE,QAAQ,IAAI,UAAU,OAAO,MAAO,IAAI,QAAQ,QAAmB,IAAI,MAAM,OAAO,SAAS,IAAI,aAAa,IAAI,YAAY;AAAA,MAChI,EAAE,KAAK,uBAAU,KAAK,EAAE,KAAK,KAAK,EAAE,KAAK,YAAY,EAAE,WAAW;AAAA,IACpE;AACA,UAAM,OAAO,KAAK,OAAO,GAAG;AAC5B,QAAI,KAAM,IAAG,OAAO;AACpB,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOQ,YAAY,KAAoB,KAAqB,IAAyB;AACpF,UAAM,SAAS,CAAC,WAAoB;AAClC,UAAI,UAAU,KAAK,EAAE,iBAAiB,WAAW,CAAC;AAClD,UAAI,IAAI;AACR,UAAI,CAAC,UAAU,OAAO,WAAW,YAAY,MAAM,QAAQ,MAAM,KAAK,OAAO,SAAS,MAAM,EAAG;AAC/F,YAAM,MAA+B,EAAE,GAAI,QAAoC,KAAK,GAAG,IAAI,KAAK,uBAAU,KAAK,KAAK,IAAI,EAAE;AAC1H,iBAAW,KAAK,8BAAkB,QAAO,IAAI,CAAC;AAC9C,WAAK,QAAQ,GAAG;AAAA,IAClB;AACA,UAAM,WAAW,CAAC,SAAiB;AAAE,UAAI;AAAE,eAAO,KAAK,MAAM,IAAI;AAAA,MAAc,QAAQ;AAAE,eAAO;AAAA,MAAM;AAAA,IAAE;AACxG,QAAI,IAAI,eAAe;AACrB,YAAM,IAAI,WAAW,GAAG;AACxB,WAAK,OAAO,MAAM,WAAW,IAAI,IAAI,KAAK,UAAU,CAAC,IAAI,IAAI,SAAS,QAAQ;AAAE,YAAI,UAAU,GAAG,EAAE,IAAI;AAAG;AAAA,MAAQ;AAClH,aAAO,OAAO,MAAM,WAAW,SAAS,CAAC,IAAI,CAAC;AAC9C;AAAA,IACF;AACA,UAAM,SAAmB,CAAC;AAC1B,QAAI,OAAO,GAAG,OAAO;AACrB,QAAI,GAAG,QAAQ,CAAC,MAAc;AAC5B,cAAQ,EAAE;AACV,UAAI,OAAO,QAAQ;AAAE,eAAO;AAAM,YAAI,mBAAmB,MAAM;AAAG,YAAI,UAAU,GAAG,EAAE,IAAI;AAAG;AAAA,MAAQ;AACpG,aAAO,KAAK,CAAC;AAAA,IACf,CAAC;AACD,QAAI,GAAG,OAAO,UAAM,sBAAQ,MAAM;AAChC,UAAI,KAAM;AACV,aAAO,SAAS,OAAO,OAAO,MAAM,EAAE,SAAS,MAAM,CAAC,CAAC;AAAA,IACzD,GAAG,MAAS,CAAC;AACb,QAAI,GAAG,SAAS,MAAM;AAAE,UAAI;AAAE,YAAI,QAAQ;AAAA,MAAG,QAAQ;AAAA,MAAqB;AAAA,IAAE,CAAC;AAAA,EAC/E;AAAA,EAEQ,gBAAgB,KAAsB,IAA4B;AACxE,WAAO,CAAC,CAAC,KAAK,KAAK,WAAW,IAAI,KAAK,IAAI,GAAG,YAAa,IAAI,QAAQ,UAAqB,IAAI,6BAAgB,CAAC;AAAA,EACnH;AAAA;AAAA;AAAA,EAIQ,oBAAoB,KAAoB,KAAqB,IAAY,QAAsB;AACrG,UAAM,SAAK,2BAAa,MAAM;AAC9B,uBAAmB,KAAK,EAAE,OAAO,KAAK,IAAK,MAAM,IAAI,KAAK,IAAI,CAAC,GAAG,QAAQ,KAAK,gBAAgB,GAAG,GAAG,GAAG,CAAC;AAGzG,8BAAQ,MAAM;AACZ,YAAM,KAAK,OAAO,QAAQ,GAAG;AAC7B,YAAM,KAAK,KAAK;AAAA,QAAW;AAAA,QAAK,OAAO,KAAK,SAAS,OAAO,MAAM,GAAG,EAAE;AAAA,QAAG,OAAO,KAAK,KAAK,OAAO,MAAM,EAAE;AAAA,QAAG;AAAA,QAC3G,EAAE,SAAK,gCAAW,GAAG,KAAK,KAAK,UAAU,GAAG,GAAG,YAAY,MAAM;AAAA,MAAC;AACpE,SAAG,KAAK;AACR,SAAG,MAAM;AACT,WAAK,QAAQ,EAAE;AAAA,IACjB,GAAG,MAAS;AAAA,EACd;AAAA;AAAA,EAGQ,UAAU,KAAqC;AACrD,WAAO,YAAa,IAAI,QAAQ,UAAqB,IAAI,cAAc;AAAA,EACzE;AAAA;AAAA;AAAA,EAIQ,gBAAgB,KAAoB,KAAqB,IAAkB;AACjF,aAAS,KAAK,CAAC,aAAS,sBAAQ,MAAM;AACpC,YAAM,SAAK,2BAAa,KAAK,EAAE;AAC/B,YAAM,MAAM,KAAK,IAAI;AACrB,UAAI,CAAC,KAAK,IAAK,OAAO,IAAI,KAAK,KAAK,OAAO,KAAK,QAAQ,GAAG;AACzD,2BAAmB,KAAK,EAAE,OAAO,KAAK,IAAK,MAAM,IAAI,GAAG,GAAG,QAAQ,KAAK,gBAAgB,GAAG,GAAG,GAAG,CAAC;AAClG;AAAA,MACF;AACA,UAAI,UAAU,KAAK;AAAA,QACjB,UAAU;AAAA,QACV,kBAAc,8BAAgB,KAAK,IAAK,MAAM,IAAI,GAAG,GAAG,QAAQ,GAAG,CAAC;AAAA,QACpE,iBAAiB;AAAA,MACnB,CAAC;AACD,UAAI,IAAI;AACR,YAAM,KAAK,KAAK,WAAW,KAAK,KAAK,gBAAgB,GAAG,GAAG,IAAI,IAAI,EAAE,SAAK,gCAAW,GAAG,KAAK,KAAK,UAAU,GAAG,GAAG,YAAY,MAAM,CAAC;AACrI,SAAG,KAAK;AACR,SAAG,KAAK;AACR,WAAK,QAAQ,EAAE;AAAA,IACjB,GAAG,MAAS,CAAC;AAAA,EACf;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWA,OAAyB,QAAc;AACrC,8BAAQ,MAAM;AACZ,YAAM,IAAI;AACV,UAAI,KAAK,YAAY,CAAC,KAAK,SAAS,EAAE,QAAQ,EAAG;AACjD,QAAE,QAAQ,IAAI;AACd,YAAM,OAAO,EAAE,KAAK,KAAK,CAAC;AAC1B,QAAE,QAAQ,CAAC,UAA2B,SAAoB;AACxD,YAAI,UAAU,UAAW,QAAO,KAAK,OAAO,GAAG,IAAI;AACnD,cAAM,KAAK,KAAK,IAAI;AACpB,YAAI;AACF,iBAAO,KAAK,OAAO,GAAG,IAAI;AAAA,QAC5B,UAAE;AACA,oCAAQ,MAAM,KAAK,YAAY,KAAK,CAAC,GAAoB,EAAE,GAAG,MAAS;AAAA,QACzE;AAAA,MACF;AAAA,IACF,GAAG,MAAS;AACZ,WAAO;AAAA,EACT;AAAA,EAEQ,YAAY,KAAoB,IAAkB;AACxD,QAAI,KAAK,YAAY,CAAC,KAAK,MAAO;AAClC,UAAM,EAAE,MAAM,MAAM,IAAI,aAAa,GAAG;AACxC,UAAM,MAAM,KAAK,MAAM;AACvB,SAAK,KAAK,WAAW,CAAC,GAAG,KAAK,CAAC,MAAM,KAAK,WAAW,CAAC,CAAC,KAAK,KAAK,OAAO,MAAM,KAAK,UAAU,GAAI;AACjG,UAAM,KAAK,KAAK,SAAS,GAAG;AAC5B,UAAM,KAAK,KAAK,WAAW,KAAK,MAAM,OAAO,IAAI,EAAE,SAAK,gCAAW,GAAG,KAAK,KAAK,UAAU,GAAG,GAAG,YAAY,MAAM,CAAC;AACnH,OAAG,KAAK;AACR,OAAG,KAAK;AACR,OAAG,MAAM,KAAK,IAAI,IAAI;AACtB,SAAK,QAAQ,EAAE;AAAA,EACjB;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,eAAe,KAAsB,KAA8B;AACjE,eAAO,sBAAQ,MAAM;AACnB,UAAI,KAAK,YAAY,CAAC,KAAK,OAAO,CAAC,KAAK,MAAO,QAAO;AACtD,YAAM,KAAK,KAAK,SAAS,GAAG;AAC5B,UAAI,CAAC,MAAM,KAAK,gBAAgB,KAAK,EAAE,EAAG,QAAO;AACjD,YAAM,IAAI;AACV,YAAM,EAAE,MAAM,MAAM,IAAI,aAAa,CAAC;AACtC,UAAI,CAAC,OAAO,CAAC,GAAG;AAEd,YAAI,CAAC,EAAE,gBAAiB,QAAO,GAAG;AAClC,UAAE,kBAAkB;AACpB,YAAI,EAAE,eAAe,QAAW;AAC9B,gBAAM,IAAI,EAAE,UAAU,KAAK,MAAM,GAAG,KAAK,IAAI,KAAK,IAAI,CAAC;AACvD,eAAK,YAAY,GAAG,KAAK,MAAM,OAAO,IAAI,EAAE,KAAK,EAAE,KAAK,KAAK,EAAE,KAAK,YAAY,EAAE,IAAI,IAAI,EAAE,GAAG,CAAC;AAAA,QAClG;AACA,eAAO;AAAA,MACT;AACA,QAAE,mBAAmB;AACrB,WAAK,oBAAoB,KAAsB,KAAK,IAAI,OAAO,KAAK;AACpE,aAAO;AAAA,IACT,GAAG,KAAK;AAAA,EACV;AAAA;AAAA,EAGA,UAAU,KAA8B;AACtC,QAAI,KAAK,YAAY,CAAC,KAAK,cAAc,EAAG,QAAO;AACnD,UAAM,MAAO,IAAsB,QAAQ;AAC3C,UAAM,IAAI;AACV,UAAM,MAAM,KAAK,gBAAgB,KAAK,cAAc,EAAE,gBAAgB,YAAY,CAAC,KAAK,KAAK;AAC7F,WAAO,gBAAgB,GAAG,GAAG,MAAM,MAAM,GAAG,KAAK,EAAE;AAAA,EACrD;AAAA;AAAA;AAAA,EAIA,MAAM,KAAsB,OAAe,MAAgC;AACzE,8BAAQ,MAAM;AACZ,UAAI,KAAK,YAAY,CAAC,KAAK,SAAS,CAAC,KAAK,IAAK;AAC/C,YAAM,MAAO,IAAsB;AACnC,YAAM,IAAI;AACV,YAAM,OAAO,YAAY;AACvB,cAAM,MAAM,MAAM,OAAO,UAAM,yBAAW,KAAK,MAAM,KAAK,IAAK,cAAc,WAAW,UAAU,+BAAW,MAAM,IAAI;AACvH,aAAK,QAAQ,EAAE,KAAK,uBAAU,IAAI,OAAO,KAAK,KAAK,KAAK,OAAO,MAAM,KAAK,KAAK,OAAO,MAAM,IAAI,KAAK,MAAM,MAAM,IAAI,KAAK,IAAI,EAAE,GAAG,CAAC;AAAA,MACtI,GAAG,EAAE,MAAM,2BAAc;AACzB,UAAI,EAAE,WAAY,EAAC,EAAE,iBAAiB,CAAC,GAAG,KAAK,GAAG;AAAA,IACpD,GAAG,MAAS;AAAA,EACd;AAAA;AAAA,EAGA,UAAU;AACR,WAAO,CAAC,KAAsB,KAAqB,SAAwC;AACzF,UAAI,CAAC,KAAK,OAAO,KAAK,GAAG,EAAG,MAAK;AAAA,IACnC;AAAA,EACF;AAAA;AAAA;AAAA,EAIA,OAAsB;AACpB,SAAK,MAAM,KAAK;AAChB,WAAO,KAAK,OAAO,KAAK,KAAK,QAAQ,QAAQ;AAAA,EAC/C;AACF;AAEO,SAAS,aAAa,OAAsB,CAAC,GAAW;AAC7D,QAAM,IAAI,IAAI,OAAO,IAAI;AACzB,MAAI,CAAC,EAAE,IAAK,kCAAe,IAAI,MAAM,wFAAmF,CAAC;AACzH,SAAO;AACT;;;AD5kBA,IAAI,YAA2B;AACxB,SAAS,aAAqB;AACnC,SAAQ,cAAc,aAAa;AACrC;AAEO,SAAS,UAAU,MAA6B;AACrD,aAAW,KAAK;AAChB,cAAY,aAAa,IAAI;AAC7B,SAAO;AACT;AAEA,IAAO,gBAAQ;AAAA,EACb,SAAS,MAAM,CAAC,KAAsB,KAAqB,SAAkC;AAAE,QAAI,CAAC,WAAW,EAAE,OAAO,KAAK,GAAG,EAAG,MAAK;AAAA,EAAG;AAAA,EAC3I,QAAQ,CAAC,KAAsB,QAAwB,WAAW,EAAE,OAAO,KAAK,GAAG;AAAA,EACnF,WAAW,CAAC,QAAyB,WAAW,EAAE,UAAU,GAAG;AAAA,EAC/D,gBAAgB,CAAC,KAAsB,QAAwB,WAAW,EAAE,eAAe,KAAK,GAAG;AAAA,EACnG,OAAO,CAAC,KAAsB,OAAe,SAA6B,WAAW,EAAE,MAAM,KAAK,OAAO,IAAI;AAAA,EAC7G,QAAQ,CAAmB,WAAiB,WAAW,EAAE,OAAO,MAAM;AAAA,EACtE;AACF;","names":["import_node_crypto","import_core","import_core","name","iife"]}
package/dist/index.d.cts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { IncomingMessage, ServerResponse, Server } from 'node:http';
2
- import { a as CamadaOptions, C as Camada } from './camada-CFmJTruP.cjs';
3
- export { R as ResolvedEnv, c as createCamada, r as resolveEnv } from './camada-CFmJTruP.cjs';
2
+ import { a as CamadaOptions, C as Camada } from './camada-DuHQ4sLn.cjs';
3
+ export { R as ResolvedEnv, c as createCamada, r as resolveEnv } from './camada-DuHQ4sLn.cjs';
4
4
  export { parseTrustedProxyEnv } from '@camada/core';
5
5
 
6
6
  declare function getDefault(): Camada;
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { IncomingMessage, ServerResponse, Server } from 'node:http';
2
- import { a as CamadaOptions, C as Camada } from './camada-CFmJTruP.js';
3
- export { R as ResolvedEnv, c as createCamada, r as resolveEnv } from './camada-CFmJTruP.js';
2
+ import { a as CamadaOptions, C as Camada } from './camada-DuHQ4sLn.js';
3
+ export { R as ResolvedEnv, c as createCamada, r as resolveEnv } from './camada-DuHQ4sLn.js';
4
4
  export { parseTrustedProxyEnv } from '@camada/core';
5
5
 
6
6
  declare function getDefault(): Camada;
package/dist/index.js CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  index_default,
7
7
  parseTrustedProxyEnv,
8
8
  resolveEnv
9
- } from "./chunk-V3UWO5OT.js";
9
+ } from "./chunk-44FSZJ5F.js";
10
10
  export {
11
11
  Camada,
12
12
  configure,
package/dist/koa.cjs CHANGED
@@ -37,10 +37,15 @@ module.exports = __toCommonJS(koa_exports);
37
37
  // src/camada.ts
38
38
  var import_node_crypto2 = require("crypto");
39
39
  var import_core3 = require("@camada/core");
40
+ var import_node_net = require("net");
40
41
  var import_iife_string = __toESM(require("@camada/browser/iife-string"), 1);
41
42
 
42
43
  // src/env.ts
43
44
  var import_core = require("@camada/core");
45
+ function parseTlsxHeader(raw) {
46
+ const n = (raw ?? "").trim().toLowerCase();
47
+ return /^[a-z0-9!#$%&'*+.^_`|~-]+$/.test(n) ? n : null;
48
+ }
44
49
  function resolveEnv(env = process.env) {
45
50
  const key = (0, import_core.parseKey)(env.CAMADA_KEY);
46
51
  const ingestToken = key?.ingestToken ?? env.CAMADA_TOKEN;
@@ -54,7 +59,8 @@ function resolveEnv(env = process.env) {
54
59
  secret: env.CAMADA_KEY || `${ingestToken}.${snapToken}`,
55
60
  snapshotUrl: env.CAMADA_SNAPSHOT_URL || `${ingestUrl}/snapshot`,
56
61
  serverless: env.CAMADA_SERVERLESS === "1",
57
- trustedProxy: (0, import_core.parseTrustedProxyEnv)(env.CAMADA_TRUSTED_PROXY)
62
+ trustedProxy: (0, import_core.parseTrustedProxyEnv)(env.CAMADA_TRUSTED_PROXY),
63
+ tlsxHeader: parseTlsxHeader(env.CAMADA_TLSX_HEADER)
58
64
  };
59
65
  }
60
66
 
@@ -122,13 +128,9 @@ function writeChallengePage(res, o) {
122
128
  });
123
129
  res.end((0, import_core2.challengePage)(o));
124
130
  }
125
- function writeChallengeJson(res) {
126
- res.writeHead(403, { "content-type": "application/json", "cache-control": "no-store", "x-camada-challenge": "1" });
127
- res.end('{"error":"challenge_required"}');
128
- }
129
131
 
130
132
  // package.json
131
- var version = "0.3.2";
133
+ var version = "0.4.0";
132
134
  var name = "@camada/node";
133
135
 
134
136
  // src/version.ts
@@ -139,6 +141,12 @@ var SESSION_COOKIE = "_sfp";
139
141
  var SCRIPT_PATH = "/_cam/b.js";
140
142
  var FP_PATH = "/_cam/fp";
141
143
  var FP_MAX = 32 * 1024;
144
+ function isLoopbackPeer(peer) {
145
+ const k = (0, import_node_net.isIP)(peer);
146
+ if (k === 4) return peer.startsWith("127.");
147
+ return k !== 6 || peer === "::1" || /^::ffff:127\./i.test(peer);
148
+ }
149
+ var TLSX_RE = /^[A-Za-z0-9+/=_-]+$/;
142
150
  var ATTACHED = /* @__PURE__ */ Symbol.for("camada.attached");
143
151
  var headerReader = (req) => (name2) => {
144
152
  const v = req.headers[name2];
@@ -148,6 +156,7 @@ var splitUrl = (raw) => {
148
156
  const qi = raw.indexOf("?");
149
157
  return qi === -1 ? { path: raw, query: "" } : { path: raw.slice(0, qi), query: raw.slice(qi) };
150
158
  };
159
+ var isPage = (req) => (0, import_core3.wantsHtml)(req.headers.accept ?? null, req.headers["sec-fetch-dest"] ?? null);
151
160
  function pathAndQuery(req) {
152
161
  return splitUrl(req.originalUrl || req.url || "/");
153
162
  }
@@ -185,6 +194,27 @@ function keepSessionCookie(res, cookie) {
185
194
  };
186
195
  res.setHeader("set-cookie", cookie);
187
196
  }
197
+ var NO_STORE_HEADERS = ["cache-control", "cdn-cache-control", "surrogate-control", "cloudflare-cdn-cache-control", "vercel-cdn-cache-control", "netlify-cdn-cache-control"];
198
+ function signal(res) {
199
+ const set = res.setHeader, writeHead = res.writeHead;
200
+ const isNoStore = (n) => typeof n === "string" && NO_STORE_HEADERS.includes(n.toLowerCase());
201
+ const strip = (h) => {
202
+ if (Array.isArray(h)) {
203
+ if (h.length && Array.isArray(h[0])) return h.filter((p) => !isNoStore(p[0]));
204
+ const out = [];
205
+ for (let i = 0; i < h.length; i += 2) if (!isNoStore(h[i])) out.push(h[i], h[i + 1]);
206
+ return out;
207
+ }
208
+ if (h && typeof h === "object") return Object.fromEntries(Object.entries(h).filter(([k]) => !isNoStore(k)));
209
+ return h;
210
+ };
211
+ res.writeHead = function(status, ...rest) {
212
+ if (!this.headersSent) for (const h of NO_STORE_HEADERS) set.call(this, h, "no-store");
213
+ return writeHead.call(this, status, ...rest.map(strip));
214
+ };
215
+ res.setHeader("x-camada-challenge", "1");
216
+ for (const h of NO_STORE_HEADERS) res.setHeader(h, "no-store");
217
+ }
188
218
  var Camada = class {
189
219
  env;
190
220
  snap = null;
@@ -199,6 +229,7 @@ var Camada = class {
199
229
  scriptPathSet;
200
230
  kit = null;
201
231
  envSource;
232
+ tlsxHeader;
202
233
  waitUntil;
203
234
  constructor(opts = {}) {
204
235
  this.envSource = opts.env ?? process.env;
@@ -211,6 +242,7 @@ var Camada = class {
211
242
  this.challengePathSet = opts.challengePath != null;
212
243
  this.scriptPathSet = opts.scriptPath != null;
213
244
  this.env = resolveEnv(this.envSource);
245
+ this.tlsxHeader = parseTlsxHeader(opts.tlsxHeader) ?? this.env?.tlsxHeader ?? null;
214
246
  if (!this.env) return;
215
247
  if (this.envSource.CAMADA_DISABLED === "1") return;
216
248
  this.snap = new import_core3.SnapshotClient({
@@ -244,6 +276,24 @@ var Camada = class {
244
276
  if (!peer && req.socket && !onUnixListener(req.socket)) return null;
245
277
  return (0, import_core3.resolveClientIp)(peer ?? null, req.headers["x-forwarded-for"], this.trustedProxy());
246
278
  }
279
+ /** The TLS fingerprint Cloudflare (a Transform Rule) put in the opt-in header, or null. Trusted only
280
+ * in `cidrs` mode and only when the socket peer itself is a NON-loopback address core's cidrs walk
281
+ * trusts, i.e. Cloudflare connects to the app directly. A loopback peer (a local reverse proxy or
282
+ * cloudflared) is never trusted and X-Forwarded-For is never consulted, so the header is not read
283
+ * and tlsx conditions fail open. Trust is core's isTrustedPeer, the same first-hop test the cidrs
284
+ * walk uses. No readable peer (unix listener included) -> null, since core's no-peer walk would
285
+ * trust everyone. */
286
+ tlsxOf(req) {
287
+ const header = this.tlsxHeader;
288
+ if (!header) return null;
289
+ const v = headerReader(req)(header);
290
+ if (v == null || v.length > 128 || !TLSX_RE.test(v)) return null;
291
+ const cfg = this.trustedProxy();
292
+ const peer = req.socket?.remoteAddress;
293
+ if (cfg?.mode !== "cidrs" || !peer) return null;
294
+ if (isLoopbackPeer(peer)) return null;
295
+ return (0, import_core3.isTrustedPeer)(peer, cfg) ? v : null;
296
+ }
247
297
  /** Where the challenge form posts: under the mount prefix, so it reaches camada, unless the
248
298
  * path was configured explicitly. */
249
299
  challengeAction(req) {
@@ -281,7 +331,7 @@ var Camada = class {
281
331
  const { path, query } = pathAndQuery(req);
282
332
  const ip = this.clientIp(req);
283
333
  req.camadaPrefix = mountPrefix(req);
284
- const v = this.snap.verdict({ ip, path, ua: req.headers["user-agent"], header: headerReader(req) });
334
+ const v = this.snap.verdict({ ip, path, ua: req.headers["user-agent"], header: headerReader(req), tlsx: this.tlsxOf(req) });
285
335
  if (v.block) {
286
336
  const headers = { "x-block-reason": v.reason ?? "", "x-block-version": v.version ?? "", "content-type": "text/plain", "cache-control": "no-store" };
287
337
  if (v.rule) headers["x-block-rule"] = v.rule;
@@ -300,8 +350,11 @@ var Camada = class {
300
350
  return true;
301
351
  }
302
352
  if (v.challenge && !this.challengePassed(req, ip)) {
303
- this.serveChallengeInner(req, res, ip, path + query);
304
- return true;
353
+ if (isPage(req)) {
354
+ this.serveChallengeInner(req, res, ip, path + query);
355
+ return true;
356
+ }
357
+ req.camadaSignalled = true;
305
358
  }
306
359
  }
307
360
  if (this.beaconEnabled()) {
@@ -315,50 +368,64 @@ var Camada = class {
315
368
  return true;
316
369
  }
317
370
  }
371
+ const { rid, sid, ns: newSession } = this.begin(req, res, ip, t0, v.warn ? v.rule : void 0);
372
+ if (req.camadaSignalled) signal(res);
373
+ const cfg = this.snap.config;
374
+ const excluded = (cfg?.exclude || []).some((x) => path.startsWith(x));
375
+ if (req.camadaSignalled || !excluded && Math.random() < (cfg?.sample ?? 1)) {
376
+ this.hookCapture(req, res, path, query, ip, { rid, sid, newSession, t0 });
377
+ }
378
+ return false;
379
+ }
380
+ /** Mints the request's rid and session (setting the cookie on a first visit), records them on
381
+ * `req.camada` and sets `x-rid`. handle() and serveChallenge() (when handle() did not run) share it. */
382
+ begin(req, res, ip, t0, wrn) {
318
383
  const rid = (0, import_node_crypto2.randomUUID)();
319
384
  let sid = cookieValue(req.headers.cookie || "", SESSION_COOKIE);
320
- const newSession = !sid;
385
+ const ns = !sid;
321
386
  if (!sid) {
322
387
  sid = (0, import_node_crypto2.randomUUID)();
323
388
  const https = req.socket.encrypted || req.headers["x-forwarded-proto"] === "https";
324
389
  keepSessionCookie(res, `${SESSION_COOKIE}=${sid}; Path=/; Max-Age=2592000; HttpOnly; SameSite=Lax${https ? "; Secure" : ""}`);
325
390
  }
326
- req.camada = { rid, ip, sid };
327
391
  res.setHeader("x-rid", rid);
328
- const cfg = this.snap.config;
329
- const excluded = (cfg?.exclude || []).some((x) => path.startsWith(x));
330
- if (!excluded && Math.random() < (cfg?.sample ?? 1)) {
331
- let shipped = false;
332
- req.camadaOpen = true;
333
- const ship = () => (0, import_core3.guarded)(() => {
334
- req.camadaOpen = false;
335
- if (shipped) return;
336
- shipped = true;
337
- if (req.camadaChallenged) {
338
- if (this.env?.serverless) this.flushFor(req);
339
- return;
340
- }
341
- const ev = this.buildEvent(req, path, query, ip, { rid, sid, newSession });
342
- ev.ts = t0;
343
- ev.st = res.statusCode;
344
- ev.dur = Date.now() - t0;
345
- if (req.route?.path) ev.rt = String(req.route.path);
346
- if (v.warn && v.rule) ev.wrn = v.rule;
347
- this.enqueue(ev, req);
348
- }, void 0);
349
- res.once("finish", ship);
350
- res.once("close", ship);
351
- }
352
- return false;
392
+ return req.camada = { rid, ip, sid, ns, t0, wrn };
393
+ }
394
+ /** Installs the response-finish hook that ships the request's normal capture event. */
395
+ hookCapture(req, res, path, query, ip, o) {
396
+ let shipped = false;
397
+ req.camadaOpen = true;
398
+ const ship = () => (0, import_core3.guarded)(() => {
399
+ req.camadaOpen = false;
400
+ if (shipped) return;
401
+ shipped = true;
402
+ if (req.camadaChallenged) {
403
+ if (this.env?.serverless) this.flushFor(req);
404
+ return;
405
+ }
406
+ const ev = this.buildEvent(req, path, query, ip, o);
407
+ ev.ts = o.t0;
408
+ ev.st = res.statusCode;
409
+ ev.dur = Date.now() - o.t0;
410
+ if (req.route?.path) ev.rt = String(req.route.path);
411
+ if (req.camada?.wrn) ev.wrn = req.camada.wrn;
412
+ if (req.camadaSignalled) ev.chs = 1;
413
+ this.enqueue(ev, req);
414
+ }, void 0);
415
+ res.once("finish", ship);
416
+ res.once("close", ship);
353
417
  }
354
418
  buildEvent(req, path, query, ip, o) {
355
419
  const raw = req.rawHeaders || [];
356
420
  const headers = [];
357
421
  for (let i = 0; i + 1 < raw.length; i += 2) headers.push([raw[i], raw[i + 1]]);
358
- return (0, import_core3.buildWireEvent)(
422
+ const ev = (0, import_core3.buildWireEvent)(
359
423
  { method: req.method || "GET", host: req.headers.host || "", path, query, headers, ip, httpVersion: req.httpVersion },
360
424
  { tap: import_core3.TAP_NODE, rid: o.rid, sid: o.sid, newSession: o.newSession }
361
425
  );
426
+ const tlsx = this.tlsxOf(req);
427
+ if (tlsx) ev.tlsx = tlsx;
428
+ return ev;
362
429
  }
363
430
  /** Reads the beacon POST (≤32 KB), answers 204 immediately, and queues the beacon as a `sig: 1`
364
431
  * row with the trusted-proxy-resolved client IP: it rides the next event batch, so the analyst
@@ -370,7 +437,9 @@ var Camada = class {
370
437
  res.writeHead(204, { "cache-control": "no-store" });
371
438
  res.end();
372
439
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed) || Buffer.isBuffer(parsed)) return;
373
- this.enqueue({ ...parsed, sig: 1, ip, tap: import_core3.TAP_NODE });
440
+ const row = { ...parsed, sig: 1, ip, tap: import_core3.TAP_NODE, rts: Date.now() };
441
+ for (const k of import_core3.REQUEST_ROW_KEYS) delete row[k];
442
+ this.enqueue(row);
374
443
  };
375
444
  const fromText = (text) => {
376
445
  try {
@@ -414,15 +483,11 @@ var Camada = class {
414
483
  challengePassed(req, ip) {
415
484
  return !!this.kit?.tokenValid(ip, Date.now(), cookieValue(req.headers.cookie || "", import_core3.CHALLENGE_COOKIE));
416
485
  }
417
- /** 403 + the proof-of-work page (HTML navigations) or 403 JSON (everything else), plus the
486
+ /** 403 + the proof-of-work page (HTML navigations only), plus the
418
487
  * `blk: "challenge"` event — a served challenge is reported like a block (contract §D2). */
419
488
  serveChallengeInner(req, res, ip, target) {
420
489
  const to = (0, import_core3.safeReturnTo)(target);
421
- if ((0, import_core3.wantsHtml)(req.headers.accept ?? null, req.headers["sec-fetch-dest"] ?? null)) {
422
- writeChallengePage(res, { nonce: this.kit.nonce(ip, Date.now()), action: this.challengeAction(req), to });
423
- } else {
424
- writeChallengeJson(res);
425
- }
490
+ writeChallengePage(res, { nonce: this.kit.nonce(ip, Date.now()), action: this.challengeAction(req), to });
426
491
  (0, import_core3.guarded)(() => {
427
492
  const qi = target.indexOf("?");
428
493
  const ev = this.buildEvent(
@@ -504,14 +569,25 @@ var Camada = class {
504
569
  }
505
570
  /** Serve the challenge for this request on demand — for a route the app wants to gate itself
506
571
  * (the example's /challenge-me). Returns false when the client already holds a valid `_cch`,
507
- * so the caller renders its own page. */
572
+ * so the caller renders its own page. A non-HTML request is never challenged: it gets the
573
+ * `x-camada-challenge: 1` header and a `chs: 1` event, and false, so the app answers. */
508
574
  serveChallenge(req, res) {
509
575
  return (0, import_core3.guarded)(() => {
510
576
  if (this.disabled || !this.kit || !this.queue) return false;
511
577
  const ip = this.clientIp(req);
512
578
  if (!ip || this.challengePassed(req, ip)) return false;
513
- req.camadaChallenged = true;
514
- const { path, query } = pathAndQuery(req);
579
+ const r = req;
580
+ const { path, query } = pathAndQuery(r);
581
+ if (!isPage(r)) {
582
+ if (!r.camadaSignalled) signal(res);
583
+ r.camadaSignalled = true;
584
+ if (r.camadaOpen === void 0) {
585
+ const c = r.camada ?? this.begin(r, res, ip, Date.now());
586
+ this.hookCapture(r, res, path, query, ip, { rid: c.rid, sid: c.sid, newSession: c.ns, t0: c.t0 });
587
+ }
588
+ return false;
589
+ }
590
+ r.camadaChallenged = true;
515
591
  this.serveChallengeInner(req, res, ip, path + query);
516
592
  return true;
517
593
  }, false);