@camada/hono 0.3.1 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,7 +6,11 @@ challenge page and beacon, records the outcomes your handlers know (`track()`),
6
6
  and ships wire events through `waitUntil` so nothing is on the response path.
7
7
  Fails open by design — a camada outage or bug never 5xxes your app.
8
8
 
9
- Not yet on npm — consumed via a `file:` dependency from a sibling checkout.
9
+ ```sh
10
+ npm install @camada/hono
11
+ ```
12
+
13
+ Needs `hono` 4 or later (a peer dependency) and Node 20 or later wherever Node runs it.
10
14
 
11
15
  ## Quickstart
12
16
 
@@ -76,7 +80,22 @@ unprovisioned environment behaves exactly as if camada were not installed.
76
80
  6. **Challenge** → a `403` proof-of-work page (see below).
77
81
  7. Otherwise your handler runs, and the settled response ships one batched, redacted event with
78
82
  its real status (Authorization and Cookie values never leave the isolate; credential-looking
79
- query values are scrubbed — see `@camada/core`).
83
+ query values are scrubbed — see `@camada/core`). The event's `ts` is the request start and
84
+ `dur` runs from there to the response: to its last byte for a `text/event-stream` body
85
+ (a client that leaves early ends it), and to the moment your handler returned for anything
86
+ else, which is the time to first byte (for a buffered body, effectively the whole response).
87
+ camada sends your handler's Response object itself. It only re-wraps an event-stream body,
88
+ with the same status and headers. A first visit is given the `_sfp` session cookie; when the
89
+ response's headers are immutable (a `fetch()` result, `Response.redirect()`), it goes out as
90
+ a faithful copy with the cookie added: same status, headers and body bytes (see
91
+ `@camada/core`'s `copyResponse`). A `HEAD` gets the cookie too, including on
92
+ `@hono/node-server` 1.x, whose `HEAD` copy would otherwise drop it. A WebSocket `101` is never
93
+ copied and gets no cookie. An accepted upgrade ships `st: 101` on every runtime; on Bun that
94
+ means `server.upgrade()` really took the socket, so a plain `GET` carrying `Upgrade: websocket`
95
+ that your handler answered ships the status it sent. One host quirk: once any middleware reads `c.res.headers`,
96
+ `@hono/node-server` sends its default `content-type: text/plain; charset=UTF-8` on a response
97
+ built with plain-object headers and no content type. camada reads them on every request, as
98
+ Hono's own cors, etag and secureHeaders middleware do.
80
99
 
81
100
  ## Custom rules
82
101
 
package/dist/index.cjs CHANGED
@@ -41,6 +41,7 @@ module.exports = __toCommonJS(index_exports);
41
41
  // src/camada.ts
42
42
  var import_iife_string = __toESM(require("@camada/browser/iife-string"), 1);
43
43
  var import_core3 = require("@camada/core");
44
+ var import_fetch = require("@camada/core/fetch");
44
45
 
45
46
  // src/env.ts
46
47
  var import_core = require("@camada/core");
@@ -87,7 +88,7 @@ function scriptTag(c) {
87
88
 
88
89
  // package.json
89
90
  var name = "@camada/hono";
90
- var version = "0.3.1";
91
+ var version = "0.3.2";
91
92
 
92
93
  // src/version.ts
93
94
  var SDK_ID = `${name}/${version}`;
@@ -220,28 +221,28 @@ function camada(opts = {}) {
220
221
  let newSession = false;
221
222
  const answer = await (0, import_core3.guardedAsync)(async () => {
222
223
  eng.snap.ensureFresh(waitUntil);
223
- const req = c.req.raw;
224
- const { url, path, cf, ip, sid } = describeRequest(eng, req);
224
+ const req2 = c.req.raw;
225
+ const { url, path, cf, ip, sid } = describeRequest(eng, req2);
225
226
  const v = eng.snap.verdict({
226
227
  ip,
227
228
  path,
228
- ua: req.headers.get("user-agent"),
229
+ ua: req2.headers.get("user-agent"),
229
230
  // §D3: without ua every `ua` rule is false
230
231
  // `header` conditions read the request through this getter; `Headers.get` is
231
232
  // case-insensitive, so the lower-cased name the matcher asks with finds whatever
232
233
  // spelling the client sent. Without it every `header` rule is false.
233
- header: (n) => req.headers.get(n),
234
+ header: (n) => req2.headers.get(n),
234
235
  asn: cf.asn ?? null,
235
236
  country: cf.country ?? null,
236
237
  tlsx: cf.tlsClientExtensionsSha1 ?? null
237
238
  });
238
239
  warnRule = v.warn ? v.rule ?? null : null;
239
240
  if (v.block) {
240
- const ev = buildEvent(req, path, url.search, ip, sid);
241
- ev.st = 403;
242
- ev.blk = v.reason;
243
- if (v.rule) ev.rl = v.rule;
244
- ship(eng, ev, waitUntil);
241
+ const ev2 = buildEvent(req2, path, url.search, ip, sid);
242
+ ev2.st = 403;
243
+ ev2.blk = v.reason;
244
+ if (v.rule) ev2.rl = v.rule;
245
+ ship(eng, ev2, waitUntil);
245
246
  const headers = {
246
247
  "content-type": "text/plain",
247
248
  "x-block-reason": String(v.reason ?? ""),
@@ -251,17 +252,17 @@ function camada(opts = {}) {
251
252
  return new Response("Forbidden", { status: 403, headers });
252
253
  }
253
254
  if (challengeOn && ip) {
254
- if (req.method === "POST" && path === challengePath) return verify(eng, req, ip, sid, challengePath, waitUntil);
255
- const token = cookieValue(req.headers.get("cookie") || "", import_core3.CHALLENGE_COOKIE);
255
+ if (req2.method === "POST" && path === challengePath) return verify(eng, req2, ip, sid, challengePath, waitUntil);
256
+ const token = cookieValue(req2.headers.get("cookie") || "", import_core3.CHALLENGE_COOKIE);
256
257
  if (v.challenge && !await eng.kit.tokenValid(ip, Date.now(), token)) {
257
- return serve(eng, req, ip, sid, challengePath, path + url.search, waitUntil);
258
+ return serve(eng, req2, ip, sid, challengePath, path + url.search, waitUntil);
258
259
  }
259
260
  }
260
261
  if (beaconEnabled(eng)) {
261
- if (req.method === "GET" && path === scriptPath) {
262
+ if (req2.method === "GET" && path === scriptPath) {
262
263
  return new Response(import_iife_string.default, { status: 200, headers: { "content-type": "application/javascript", "cache-control": "public, max-age=3600" } });
263
264
  }
264
- if (req.method === "POST" && path === fpPath) return relayBeacon(eng, req, ip, waitUntil);
265
+ if (req2.method === "POST" && path === fpPath) return relayBeacon(eng, req2, ip, waitUntil);
265
266
  }
266
267
  newSession = !sid;
267
268
  vars = { eng, rid: crypto.randomUUID(), sid: sid ?? crypto.randomUUID(), ip, waitUntil, scriptPath };
@@ -269,26 +270,68 @@ function camada(opts = {}) {
269
270
  return null;
270
271
  }, null);
271
272
  if (answer) return answer;
272
- await next();
273
- (0, import_core3.guarded)(() => {
274
- if (!vars || !newSession) return;
275
- const secure = new URL(c.req.raw.url).protocol === "https:" ? "; Secure" : "";
276
- c.res.headers.append("set-cookie", `${SESSION_COOKIE}=${vars.sid}; Path=/; Max-Age=${SESSION_MAX_AGE}; HttpOnly; SameSite=Lax${secure}`);
277
- }, void 0);
278
- void (0, import_core3.guardedAsync)(async () => {
279
- const req = c.req.raw;
273
+ const req = c.req.raw;
274
+ const method = req.method;
275
+ const wsRequest = (0, import_core3.guarded)(() => req.headers.get("upgrade")?.toLowerCase() === "websocket", false);
276
+ const bun = wsRequest ? (0, import_core3.guarded)(() => {
277
+ const e = c.env;
278
+ const s = e && "server" in e ? e.server : e;
279
+ return typeof s?.requestIP === "function" && s.requestIP(req) ? s : null;
280
+ }, null) : null;
281
+ const ev = (0, import_core3.guarded)(() => {
280
282
  const { url, path, ip } = describeRequest(eng, req);
283
+ return buildEvent(req, path, url.search, ip, vars?.sid ?? null, vars?.rid, newSession);
284
+ }, null);
285
+ await next();
286
+ const upgraded = wsRequest && c.res.status === 200 && !c.res.headers.has("content-type") && (bun ? (0, import_core3.guarded)(() => bun.requestIP(req) === null, false) : true);
287
+ const status = upgraded ? 101 : c.res.status;
288
+ const shipEvent = () => void (0, import_core3.guardedAsync)(async () => {
289
+ if (!ev) return;
281
290
  const cfg = eng.snap.config;
282
- if ((cfg?.exclude || []).some((x) => path.startsWith(x))) return;
291
+ if ((cfg?.exclude || []).some((x) => ev.p.startsWith(x))) return;
283
292
  if (Math.random() >= (cfg?.sample ?? 1)) return;
284
- const ev = buildEvent(req, path, url.search, ip, vars?.sid ?? null, vars?.rid, newSession);
285
- ev.st = c.res.status;
293
+ ev.ts = t0;
294
+ ev.st = status;
286
295
  ev.dur = Math.max(0, Date.now() - t0);
287
296
  if (warnRule) ev.wrn = warnRule;
288
297
  ship(eng, ev, waitUntil);
289
298
  }, void 0);
299
+ try {
300
+ replaceRes(c, (0, import_core3.onBodyDone)(c.res, shipEvent, { method, waitUntil, get signal() {
301
+ return c.req.raw.signal;
302
+ } }));
303
+ } catch (err) {
304
+ (0, import_core3.logRateLimited)(err);
305
+ shipEvent();
306
+ }
307
+ (0, import_core3.guarded)(() => {
308
+ if (c.res.status === 101) return;
309
+ const rid = ev?.rid;
310
+ let cookie;
311
+ if (vars && newSession) {
312
+ const secure = new URL(c.req.raw.url).protocol === "https:" ? "; Secure" : "";
313
+ cookie = `${SESSION_COOKIE}=${vars.sid}; Path=/; Max-Age=${SESSION_MAX_AGE}; HttpOnly; SameSite=Lax${secure}`;
314
+ }
315
+ if (!rid && !cookie) return;
316
+ if (method !== "HEAD") {
317
+ let out = c.res;
318
+ if (cookie) out = (0, import_fetch.withSetCookie)(out, cookie);
319
+ if (rid) out = (0, import_fetch.withRid)(out, { rid });
320
+ return replaceRes(c, out);
321
+ }
322
+ const head = new Response(null, c.res);
323
+ const headers = new Headers(head.headers);
324
+ if (cookie) headers.append("set-cookie", cookie);
325
+ if (rid) headers.set("x-rid", rid);
326
+ replaceRes(c, new Response(null, { status: head.status, statusText: head.statusText, headers }));
327
+ }, void 0);
290
328
  };
291
329
  }
330
+ function replaceRes(c, out) {
331
+ if (out === c.res) return;
332
+ c.res = void 0;
333
+ c.res = out;
334
+ }
292
335
  async function serve(eng, req, ip, sid, action, target, waitUntil) {
293
336
  const url = new URL(req.url);
294
337
  const ev = buildEvent(req, url.pathname, url.search, ip, sid);
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/camada.ts","../src/env.ts","../src/context.ts","../package.json","../src/version.ts"],"sourcesContent":["// @camada/hono — the one-line install for a Hono app on Cloudflare Workers:\n// app.use('*', camada()); // env: CAMADA_KEY (+ CAMADA_INGEST_URL / CAMADA_SNAPSHOT_URL in dev)\n// c.html(`<head>${scriptTag(c)}</head>…`) // the first-party beacon\n// track(c, 'login_failed', { user }) // an outcome the wire cannot show\nexport { camada, resetCamada } from './camada.js';\nexport { track, scriptTag } from './context.js';\nexport { resolveEnv, type CamadaHonoOptions, type ResolvedEnv } from './env.js';\n","// @camada/hono — the Hono middleware for Cloudflare Workers (SEC-07, D33).\n// app.use('*', camada());\n// Workers-native: `cf-connecting-ip` for the client address, `request.cf` for asn / country /\n// TLS fingerprint (so ASN and country rules really enforce at this tap), and\n// `c.executionCtx.waitUntil` so the snapshot poll and the event flush outlive the response.\n// Everything runs inside camada's fail-open envelope: a camada bug, a dead ingest or a corrupt\n// snapshot costs telemetry, never the app's response.\nimport type { Context, MiddlewareHandler, Next } from 'hono';\nimport iife from '@camada/browser/iife-string';\nimport {\n SnapshotClient, EventQueue, buildWireEvent, resolveClientIp, logRateLimited, guardedAsync,\n guarded, createChallengeAsync, challengePage, challengeCookie, safeReturnTo, wantsHtml, parseFormBody,\n CHALLENGE_COOKIE, DEFAULT_SNAPSHOT_VERSION, TAP_HONO,\n type AsyncChallengeKit, type TrustedProxyConfig, type WireEvent,\n} from '@camada/core';\nimport { resolveEnv, type CamadaHonoOptions, type ResolvedEnv } from './env.js';\nimport { VAR, beaconEnabled, type CamadaVars } from './context.js';\nimport { SDK_ID } from './version.js';\n\nconst DEFAULT_CHALLENGE_PATH = '/__camada/challenge';\nconst SCRIPT_PATH = '/_cam/b.js'; // the beacon IIFE; its auto-init posts to the sibling `fp` and reads the rid from ?r=\nconst FP_PATH = '/_cam/fp';\nconst FP_MAX = 32 * 1024; // matches the analyst's /fp cap: never accept what ingest will 413\nconst SESSION_COOKIE = '_sfp'; // the same session cookie as every other tap: sid/ns stay comparable\nconst SESSION_MAX_AGE = 2592000;\nconst BODY_MAX = 4 * 1024; // the verify form is ~120 bytes; anything larger is not ours\nconst encoder = new TextEncoder();\n\nexport interface Engine {\n env: ResolvedEnv;\n snap: SnapshotClient;\n queue: EventQueue;\n kit: AsyncChallengeKit;\n}\n\nexport type WaitUntil = (p: Promise<unknown>) => void;\n\n// One engine per resolved configuration, not per module: a Workers isolate can host several\n// Hono apps, and a shared singleton would enforce the first tenant's snapshot on the second\n// and sign its `_cch` cookies with the first tenant's secret. An unconfigured request is never\n// cached — a warm-up or a differently-keyed mount must not leave the isolate inert for good.\nconst engines = new Map<string, Engine>();\n\n/** Test/reset hook: drops every cached engine. */\nexport function resetCamada(): void {\n for (const e of engines.values()) { e.snap.stop(); e.queue.stop(); }\n engines.clear();\n}\n\nfunction ensure(opts: CamadaHonoOptions, ctxEnv: Record<string, string | undefined>): Engine | null {\n const env = resolveEnv(opts, ctxEnv);\n if (!env) {\n logRateLimited(new Error('CAMADA_KEY not set — camada is inactive'));\n return null;\n }\n const snapshotVersion = opts.snapshotVersion ?? DEFAULT_SNAPSHOT_VERSION;\n const id = `${env.ingestToken}|${env.ingestUrl}|${env.snapshotUrl}|${snapshotVersion}`;\n const cached = engines.get(id);\n if (cached) return cached;\n const injected = opts.fetchImpl ? { fetchImpl: opts.fetchImpl } : {}; // never pass an explicit undefined key\n const engine: Engine = {\n env,\n snap: new SnapshotClient({\n url: env.snapshotUrl, token: env.snapToken, mode: 'lazy', sdk: SDK_ID,\n snapshotVersion, ...injected,\n }),\n queue: new EventQueue({ url: env.ingestUrl, token: env.ingestToken, sdk: SDK_ID, ...injected }),\n kit: createChallengeAsync({ secret: env.secret }),\n };\n engines.set(id, engine);\n return engine;\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\ninterface CfProps { asn?: number; country?: string; tlsClientExtensionsSha1?: string; httpProtocol?: string }\nconst cfOf = (req: Request): CfProps => ((req as Request & { cf?: CfProps }).cf ?? {});\n\nconst trustedProxy = (e: Engine): TrustedProxyConfig | null =>\n e.env.trustedProxy ?? e.snap.config?.trusted_proxy ?? null;\n\n/** Cloudflare sets `cf-connecting-ip` itself and a client cannot forge it — but only on\n * Workers, which is also the only place `request.cf` exists. On any other Hono runtime\n * (node-server, Bun, Deno) that header is attacker-controlled, so it is ignored there and the\n * trusted-proxy rules decide, exactly as they do for X-Forwarded-For. */\nconst clientIp = (e: Engine, req: Request): string | null => {\n const cf = (req as Request & { cf?: unknown }).cf;\n const direct = cf ? req.headers.get('cf-connecting-ip') : null;\n return direct || resolveClientIp(null, req.headers.get('x-forwarded-for'), trustedProxy(e));\n};\n\ninterface RequestFacts {\n url: URL;\n path: string;\n cf: CfProps;\n ip: string | null;\n sid: string | null;\n}\n\nfunction describeRequest(e: Engine, req: Request): RequestFacts {\n const url = new URL(req.url);\n const cookies = req.headers.get('cookie') || '';\n return { url, path: url.pathname, cf: cfOf(req), ip: clientIp(e, req), sid: cookieValue(cookies, SESSION_COOKIE) };\n}\n\nfunction buildEvent(\n req: Request, path: string, query: string, ip: string | null, sid: string | null,\n rid: string = crypto.randomUUID(), newSession = false, // the capture path passes the ids it shared with the app; camada's own answers need none\n): WireEvent {\n const headers: Array<[string, string]> = [];\n req.headers.forEach((v, k) => headers.push([k, v])); // workerd normalises header order: no HEADER_ORDER signal here\n const cf = cfOf(req);\n const ev = buildWireEvent(\n {\n method: req.method, host: req.headers.get('host') ?? new URL(req.url).host, path, query, headers, ip,\n // The client's connection terminates at Cloudflare, so `cf.httpProtocol` really is the\n // visitor's protocol (ea's TAP_CAPS grants this tap TRUE_PROTO on that basis). Never read\n // a forwarded header for this — that would be the proxy's hop, not the client's.\n httpVersion: cf.httpProtocol ? cf.httpProtocol.replace(/^HTTP\\//i, '') : null,\n },\n { tap: TAP_HONO, rid, sid, newSession },\n );\n if (cf.asn) ev.asn = cf.asn;\n if (cf.country) ev.cc = cf.country;\n if (cf.tlsClientExtensionsSha1) ev.tlsx = cf.tlsClientExtensionsSha1;\n return ev;\n}\n\n/** Push then flush through `waitUntil` — the one rule every row (wire event, beacon, track()) follows. */\nexport function ship(e: Engine, ev: unknown, waitUntil: WaitUntil): void {\n e.queue.push(ev);\n e.queue.flush(waitUntil);\n}\n\n/** 204 and a `sig: 1` row on the event batch — one request per flush at the analyst, not one per page view. */\nasync function relayBeacon(eng: Engine, req: Request, ip: string | null, waitUntil: WaitUntil): Promise<Response> {\n const noContent = () => new Response(null, { status: 204, headers: { 'cache-control': 'no-store' } });\n if (Number(req.headers.get('content-length')) > FP_MAX) return new Response(null, { status: 413 });\n const body = await req.text();\n if (encoder.encode(body).byteLength > FP_MAX) return new Response(null, { status: 413 });\n let parsed: unknown;\n try { parsed = JSON.parse(body); } catch { return noContent(); } // not a beacon: drop it, never ship junk\n if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return noContent();\n // Spread first: the ip and the tap are the server's to say, whatever the body claimed.\n ship(eng, { ...(parsed as Record<string, unknown>), sig: 1, ip, tap: TAP_HONO }, waitUntil);\n return noContent();\n}\n\nexport function camada(opts: CamadaHonoOptions = {}): MiddlewareHandler {\n const challengePath = opts.challengePath ?? DEFAULT_CHALLENGE_PATH;\n const scriptPath = opts.scriptPath ?? SCRIPT_PATH;\n const fpPath = opts.fpPath ?? FP_PATH;\n\n return async function camadaHono(c: Context, next: Next): Promise<Response | void> {\n const t0 = Date.now(); // request start; the response event ships dur = settle - t0 (ms), @camada/node's semantics\n // Reading c.env and building the engine are inside the guard too: a Workers env carries\n // non-string bindings, and a throw here would 5xx the app on its very first request.\n const env = guarded(() => ({ ...(c.env as Record<string, string | undefined> | undefined), ...opts.env }), {} as Record<string, string | undefined>);\n const eng = guarded(() => (env.CAMADA_DISABLED === '1' ? null : ensure(opts, env)), null);\n if (!eng) return next();\n // Same opt-out as @camada/next: the code option, or CAMADA_CHALLENGE=0 in the Worker env.\n const challengeOn = opts.challenge !== false && env.CAMADA_CHALLENGE !== '0';\n\n const waitUntil: WaitUntil = (p) => {\n try { c.executionCtx?.waitUntil(p); } catch { /* no execution context outside a fetch handler */ }\n };\n\n // The id of the `warn` rule that let this request through, read again after next() when the\n // response event is built (§D3). Every other action settles before the app ever runs.\n let warnRule: string | null = null;\n // Set on the capture path only: the ids this request shares with the app (scriptTag, track)\n // and with its own post-response event. Null when camada answered the request itself.\n let vars: CamadaVars | null = null;\n let newSession = false;\n\n const answer = await guardedAsync(async (): Promise<Response | null> => {\n eng.snap.ensureFresh(waitUntil);\n\n const req = c.req.raw;\n const { url, path, cf, ip, sid } = describeRequest(eng, req);\n\n const v = eng.snap.verdict({\n ip, path, ua: req.headers.get('user-agent'), // §D3: without ua every `ua` rule is false\n // `header` conditions read the request through this getter; `Headers.get` is\n // case-insensitive, so the lower-cased name the matcher asks with finds whatever\n // spelling the client sent. Without it every `header` rule is false.\n header: (n) => req.headers.get(n),\n asn: cf.asn ?? null, country: cf.country ?? null, tlsx: cf.tlsClientExtensionsSha1 ?? null,\n });\n warnRule = v.warn ? v.rule ?? null : null;\n\n if (v.block) {\n const ev = buildEvent(req, path, url.search, ip, sid);\n ev.st = 403;\n ev.blk = v.reason; // SDK-01: the analyst counts SDK blocks apart from the app's own 403s ('rule' when a rule decided)\n if (v.rule) ev.rl = v.rule;\n ship(eng, ev, waitUntil);\n const headers: Record<string, string> = {\n 'content-type': 'text/plain', 'x-block-reason': String(v.reason ?? ''), 'x-block-version': v.version ?? '',\n };\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 return new Response('Forbidden', { status: 403, headers });\n }\n\n // A challenge needs a resolved client ip: the nonce and the `_cch` cookie are bound to it.\n // Without one, fail open rather than mint a cookie every unidentified client could use.\n if (challengeOn && ip) {\n // The verify endpoint answers first — a challenged client must be able to reach it.\n if (req.method === 'POST' && path === challengePath) return verify(eng, req, ip, sid, challengePath, waitUntil);\n const token = cookieValue(req.headers.get('cookie') || '', CHALLENGE_COOKIE);\n if (v.challenge && !(await eng.kit.tokenValid(ip, Date.now(), token))) {\n return serve(eng, req, ip, sid, challengePath, path + url.search, waitUntil);\n }\n }\n\n // The beacon endpoints come after enforcement (a blocked or challenged client gets neither\n // the script nor a relay) and before the app (they are camada's, not its) — @camada/node's order.\n if (beaconEnabled(eng)) {\n if (req.method === 'GET' && path === scriptPath) {\n return new Response(iife, { status: 200, headers: { 'content-type': 'application/javascript', 'cache-control': 'public, max-age=3600' } });\n }\n if (req.method === 'POST' && path === fpPath) return relayBeacon(eng, req, ip, waitUntil);\n }\n\n // Capture: mint the ids the app, the beacon and the post-response event all share. The\n // session is decided here rather than after next() so track() inside the handler and the\n // event both carry the sid a first visit is about to be given.\n newSession = !sid;\n vars = { eng, rid: crypto.randomUUID(), sid: sid ?? crypto.randomUUID(), ip, waitUntil, scriptPath };\n c.set(VAR, vars);\n return null;\n }, null);\n\n if (answer) return answer;\n\n await next();\n\n // Mint the shared session cookie the other taps use, so `sid`/`ns` are real here too\n // (ea's capability mask for sdk-hono claims SESSION). Never overwrite an existing one.\n guarded(() => {\n if (!vars || !newSession) return;\n const secure = new URL(c.req.raw.url).protocol === 'https:' ? '; Secure' : '';\n c.res.headers.append('set-cookie', `${SESSION_COOKIE}=${vars.sid}; Path=/; Max-Age=${SESSION_MAX_AGE}; HttpOnly; SameSite=Lax${secure}`);\n }, undefined);\n\n // The response has settled, so this tap ships the real status — unlike @camada/next's\n // middleware position, which can only report pre-response.\n void guardedAsync(async () => {\n const req = c.req.raw;\n const { url, path, ip } = describeRequest(eng, req);\n const cfg = eng.snap.config;\n if ((cfg?.exclude || []).some((x) => path.startsWith(x))) return;\n if (Math.random() >= (cfg?.sample ?? 1)) return;\n // `vars` is null only when the guard above threw before setting it — then the event still ships, with fresh ids.\n const ev = buildEvent(req, path, url.search, ip, vars?.sid ?? null, vars?.rid, newSession);\n ev.st = c.res.status;\n ev.dur = Math.max(0, Date.now() - t0);\n if (warnRule) ev.wrn = warnRule; // §D3: the warn rule that let this request through\n ship(eng, ev, waitUntil);\n }, undefined);\n };\n}\n\n/** 403 + the proof-of-work page (HTML navigations) or 403 JSON, plus the `blk: \"challenge\"` event. */\nasync function serve(eng: Engine, req: Request, ip: string, sid: string | null, action: string, target: string, waitUntil: WaitUntil): Promise<Response> {\n const url = new URL(req.url);\n const ev = buildEvent(req, url.pathname, url.search, ip, sid);\n ev.st = 403;\n ev.blk = 'challenge';\n ship(eng, ev, waitUntil);\n\n if (!wantsHtml(req.headers.get('accept'), req.headers.get('sec-fetch-dest'))) {\n return new Response('{\"error\":\"challenge_required\"}', {\n status: 403,\n headers: { 'content-type': 'application/json', 'cache-control': 'no-store', 'x-camada-challenge': '1' },\n });\n }\n const html = challengePage({ nonce: await eng.kit.nonce(ip, Date.now()), action, to: safeReturnTo(target) });\n return new Response(html, {\n status: 403,\n headers: { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-store', 'x-camada-challenge': '1' },\n });\n}\n\n/** POST from the page: validate, set `_cch`, 302 back, ship `{ st: 200, ch: 1 }`. */\nasync function verify(eng: Engine, req: Request, ip: string, sid: string | null, action: string, waitUntil: WaitUntil): Promise<Response> {\n const url = new URL(req.url);\n // camada answers this path before the app runs, so it must not become a place to post\n // hundreds of megabytes at an unauthenticated endpoint.\n if (Number(req.headers.get('content-length')) > BODY_MAX) return new Response(null, { status: 413 });\n const body = await req.text();\n if (body.length > BODY_MAX) return new Response(null, { status: 413 });\n const form = parseFormBody(body);\n const to = safeReturnTo(form.to);\n const now = Date.now();\n if (!(await eng.kit.verify(ip, now, form.nonce, form.solution))) {\n const html = challengePage({ nonce: await eng.kit.nonce(ip, now), action, to });\n return new Response(html, { status: 403, headers: { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-store' } });\n }\n const ev = buildEvent(req, url.pathname, '', ip, sid);\n ev.st = 200;\n ev.ch = 1; // challenge passed (contract §A3 ingest field)\n ship(eng, ev, waitUntil);\n return new Response(null, {\n status: 302,\n headers: {\n location: to,\n 'set-cookie': challengeCookie(await eng.kit.issue(ip, now), url.protocol === 'https:'),\n 'cache-control': 'no-store',\n },\n });\n}\n","// Workers hand env vars per request on `c.env`, not on a process — so the engine is built from\n// whatever the first request carries, merged with the options the app passed in code.\nimport { parseKey, parseTrustedProxyEnv, type SnapshotVersion, type TrustedProxyConfig } from '@camada/core';\n\nexport interface ResolvedEnv {\n ingestToken: string;\n snapToken: string;\n secret: string; // HMAC key for the challenge nonce/cookie — never leaves the isolate\n ingestUrl: string;\n snapshotUrl: string;\n trustedProxy: TrustedProxyConfig | null; // null = defer to server-delivered config\n}\n\nexport interface CamadaHonoOptions {\n key?: string;\n ingestUrl?: string;\n snapshotUrl?: string;\n trustedProxy?: TrustedProxyConfig | string | null;\n challenge?: boolean; // enforce `challenge` verdicts with the first-party page (default true)\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 scriptPath?: string; // where the first-party beacon script is served (default /_cam/b.js)\n fpPath?: string; // where that script posts the beacon (default /_cam/fp; must share scriptPath's directory — the script derives it)\n env?: Record<string, string | undefined>; // overrides c.env (tests, and apps that read config themselves)\n fetchImpl?: typeof fetch;\n}\n\nconst asProxy = (v: CamadaHonoOptions['trustedProxy']): TrustedProxyConfig | null =>\n typeof v === 'string' ? parseTrustedProxyEnv(v) : v ?? null;\n\n/** Returns null (the middleware stays inert, one log line) rather than throwing on bad config. */\nexport function resolveEnv(opts: CamadaHonoOptions, env: Record<string, string | undefined>): ResolvedEnv | null {\n const raw = opts.key || env.CAMADA_KEY;\n const key = parseKey(raw);\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 = (opts.ingestUrl || env.CAMADA_INGEST_URL || 'https://in.camada.app').replace(/\\/$/, ''); // PLACEHOLDER default — confirm the production ingest domain before any npm publish\n return {\n ingestToken, snapToken,\n secret: raw || `${ingestToken}.${snapToken}`,\n ingestUrl,\n snapshotUrl: opts.snapshotUrl || env.CAMADA_SNAPSHOT_URL || `${ingestUrl}/snapshot`,\n trustedProxy: asProxy(opts.trustedProxy) ?? parseTrustedProxyEnv(env.CAMADA_TRUSTED_PROXY),\n };\n}\n","// Per-request state the middleware leaves on the Hono context for the two helpers an app calls\n// from its own handlers: `scriptTag(c)` (the beacon tag) and `track(c, event, data)` (an\n// app-context outcome). The key is private — the helpers are the API, not `c.get`. When the\n// middleware did not run for this request (a path it is not mounted on, no key, CAMADA_DISABLED,\n// or a request it answered itself) there is nothing on the context and both helpers stand down.\nimport type { Context } from 'hono';\nimport { guarded, guardedAsync, hashUserId, TAP_HONO } from '@camada/core';\nimport { ship, type Engine, type WaitUntil } from './camada.js';\n\nexport const VAR = '__camada';\n\nexport interface CamadaVars {\n eng: Engine;\n rid: string; // the request id the page event carries; the beacon and track() join on it\n sid: string; // the `_sfp` session — the one just minted when the request arrived without it\n ip: string | null;\n waitUntil: WaitUntil;\n scriptPath: string;\n}\n\nexport const beaconEnabled = (e: Engine): boolean => e.snap.config?.beacon !== false; // tenant switch; a cold engine serves\n\nconst readVars = (c: Context): CamadaVars | undefined => c.get(VAR) as CamadaVars | undefined;\n\n/**\n * Records an outcome the app knows and the wire cannot show: `login_failed`, `login_succeeded`,\n * `signup`, `password_reset`, `mfa_failed`, `payment_failed`, `payment_succeeded`, `coupon_failed`\n * (free-form; that vocabulary is what the analyst's rules read). Joined to this request's event\n * through its rid and session. The user identifier is HMAC-hashed in-process with the ingest\n * token — the raw value never reaches the queue. Never throws, never rejects; awaiting it is\n * optional (the flush rides `waitUntil`), so a handler may fire and forget.\n */\nexport function track(c: Context, event: string, data?: { user?: string }): Promise<void> {\n const vars = guarded(() => readVars(c), undefined);\n if (!vars) return Promise.resolve();\n const p = guardedAsync(async () => {\n const uid = data?.user ? await hashUserId(data.user, vars.eng.env.ingestToken) : null;\n ship(vars.eng, { tap: TAP_HONO, et: event, uid, rid: vars.rid, sid: vars.sid, ip: vars.ip, ts: Date.now() }, vars.waitUntil);\n }, undefined);\n vars.waitUntil(p); // the isolate may freeze right after the response: hold it open for the flush\n return p;\n}\n\n/** The `<script>` tag for an HTML response — `''` when camada is off for this request or the tenant turned the beacon off. */\nexport function scriptTag(c: Context): string {\n return guarded(() => {\n const vars = readVars(c);\n if (!vars || !beaconEnabled(vars.eng)) return '';\n return `<script src=\"${vars.scriptPath}?r=${vars.rid}\" async></script>`;\n }, '');\n}\n","{\n \"name\": \"@camada/hono\",\n \"version\": \"0.3.1\",\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 \"description\": \"camada SDK for Hono on Cloudflare Workers: snapshot enforcement, first-party challenge and beacon, track() outcomes, waitUntil-flushed event shipping\",\n \"exports\": {\n \".\": {\n \"types\": \"./dist/index.d.ts\",\n \"import\": \"./dist/index.js\",\n \"require\": \"./dist/index.cjs\"\n }\n },\n \"main\": \"./dist/index.cjs\",\n \"module\": \"./dist/index.js\",\n \"types\": \"./dist/index.d.ts\",\n \"dependencies\": {\n \"@camada/core\": \"file:../camada-core\",\n \"@camada/browser\": \"file:../camada-browser\"\n },\n \"peerDependencies\": {\n \"hono\": \">=4\"\n },\n \"devDependencies\": {\n \"@cloudflare/workers-types\": \"^4.20250109.0\",\n \"@types/node\": \"^22.10.0\",\n \"hono\": \"^4.6.0\",\n \"tsup\": \"^8.3.5\",\n \"typescript\": \"^5.7.2\",\n \"vitest\": \"^3.0.0\"\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_core","import_core","name","iife"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/camada.ts","../src/env.ts","../src/context.ts","../package.json","../src/version.ts"],"sourcesContent":["// @camada/hono — the one-line install for a Hono app on Cloudflare Workers:\n// app.use('*', camada()); // env: CAMADA_KEY (+ CAMADA_INGEST_URL / CAMADA_SNAPSHOT_URL in dev)\n// c.html(`<head>${scriptTag(c)}</head>…`) // the first-party beacon\n// track(c, 'login_failed', { user }) // an outcome the wire cannot show\nexport { camada, resetCamada } from './camada.js';\nexport { track, scriptTag } from './context.js';\nexport { resolveEnv, type CamadaHonoOptions, type ResolvedEnv } from './env.js';\n","// @camada/hono — the Hono middleware for Cloudflare Workers (SEC-07, D33).\n// app.use('*', camada());\n// Workers-native: `cf-connecting-ip` for the client address, `request.cf` for asn / country /\n// TLS fingerprint (so ASN and country rules really enforce at this tap), and\n// `c.executionCtx.waitUntil` so the snapshot poll and the event flush outlive the response.\n// Everything runs inside camada's fail-open envelope: a camada bug, a dead ingest or a corrupt\n// snapshot costs telemetry, never the app's response.\nimport type { Context, MiddlewareHandler, Next } from 'hono';\nimport iife from '@camada/browser/iife-string';\nimport {\n SnapshotClient, EventQueue, buildWireEvent, resolveClientIp, logRateLimited, guardedAsync, onBodyDone,\n guarded, createChallengeAsync, challengePage, challengeCookie, safeReturnTo, wantsHtml, parseFormBody,\n CHALLENGE_COOKIE, DEFAULT_SNAPSHOT_VERSION, TAP_HONO,\n type AsyncChallengeKit, type TrustedProxyConfig, type WireEvent,\n} from '@camada/core';\nimport { withSetCookie, withRid } from '@camada/core/fetch';\nimport { resolveEnv, type CamadaHonoOptions, type ResolvedEnv } from './env.js';\nimport { VAR, beaconEnabled, type CamadaVars } from './context.js';\nimport { SDK_ID } from './version.js';\n\nconst DEFAULT_CHALLENGE_PATH = '/__camada/challenge';\nconst SCRIPT_PATH = '/_cam/b.js'; // the beacon IIFE; its auto-init posts to the sibling `fp` and reads the rid from ?r=\nconst FP_PATH = '/_cam/fp';\nconst FP_MAX = 32 * 1024; // matches the analyst's /fp cap: never accept what ingest will 413\nconst SESSION_COOKIE = '_sfp'; // the same session cookie as every other tap: sid/ns stay comparable\nconst SESSION_MAX_AGE = 2592000;\nconst BODY_MAX = 4 * 1024; // the verify form is ~120 bytes; anything larger is not ours\nconst encoder = new TextEncoder();\n\nexport interface Engine {\n env: ResolvedEnv;\n snap: SnapshotClient;\n queue: EventQueue;\n kit: AsyncChallengeKit;\n}\n\nexport type WaitUntil = (p: Promise<unknown>) => void;\n\n// One engine per resolved configuration, not per module: a Workers isolate can host several\n// Hono apps, and a shared singleton would enforce the first tenant's snapshot on the second\n// and sign its `_cch` cookies with the first tenant's secret. An unconfigured request is never\n// cached — a warm-up or a differently-keyed mount must not leave the isolate inert for good.\nconst engines = new Map<string, Engine>();\n\n/** Test/reset hook: drops every cached engine. */\nexport function resetCamada(): void {\n for (const e of engines.values()) { e.snap.stop(); e.queue.stop(); }\n engines.clear();\n}\n\nfunction ensure(opts: CamadaHonoOptions, ctxEnv: Record<string, string | undefined>): Engine | null {\n const env = resolveEnv(opts, ctxEnv);\n if (!env) {\n logRateLimited(new Error('CAMADA_KEY not set — camada is inactive'));\n return null;\n }\n const snapshotVersion = opts.snapshotVersion ?? DEFAULT_SNAPSHOT_VERSION;\n const id = `${env.ingestToken}|${env.ingestUrl}|${env.snapshotUrl}|${snapshotVersion}`;\n const cached = engines.get(id);\n if (cached) return cached;\n const injected = opts.fetchImpl ? { fetchImpl: opts.fetchImpl } : {}; // never pass an explicit undefined key\n const engine: Engine = {\n env,\n snap: new SnapshotClient({\n url: env.snapshotUrl, token: env.snapToken, mode: 'lazy', sdk: SDK_ID,\n snapshotVersion, ...injected,\n }),\n queue: new EventQueue({ url: env.ingestUrl, token: env.ingestToken, sdk: SDK_ID, ...injected }),\n kit: createChallengeAsync({ secret: env.secret }),\n };\n engines.set(id, engine);\n return engine;\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\ninterface BunServer { requestIP: (req: Request) => unknown }\n\ninterface CfProps { asn?: number; country?: string; tlsClientExtensionsSha1?: string; httpProtocol?: string }\nconst cfOf = (req: Request): CfProps => ((req as Request & { cf?: CfProps }).cf ?? {});\n\nconst trustedProxy = (e: Engine): TrustedProxyConfig | null =>\n e.env.trustedProxy ?? e.snap.config?.trusted_proxy ?? null;\n\n/** Cloudflare sets `cf-connecting-ip` itself and a client cannot forge it — but only on\n * Workers, which is also the only place `request.cf` exists. On any other Hono runtime\n * (node-server, Bun, Deno) that header is attacker-controlled, so it is ignored there and the\n * trusted-proxy rules decide, exactly as they do for X-Forwarded-For. */\nconst clientIp = (e: Engine, req: Request): string | null => {\n const cf = (req as Request & { cf?: unknown }).cf;\n const direct = cf ? req.headers.get('cf-connecting-ip') : null;\n return direct || resolveClientIp(null, req.headers.get('x-forwarded-for'), trustedProxy(e));\n};\n\ninterface RequestFacts {\n url: URL;\n path: string;\n cf: CfProps;\n ip: string | null;\n sid: string | null;\n}\n\nfunction describeRequest(e: Engine, req: Request): RequestFacts {\n const url = new URL(req.url);\n const cookies = req.headers.get('cookie') || '';\n return { url, path: url.pathname, cf: cfOf(req), ip: clientIp(e, req), sid: cookieValue(cookies, SESSION_COOKIE) };\n}\n\nfunction buildEvent(\n req: Request, path: string, query: string, ip: string | null, sid: string | null,\n rid: string = crypto.randomUUID(), newSession = false, // the capture path passes the ids it shared with the app; camada's own answers need none\n): WireEvent {\n const headers: Array<[string, string]> = [];\n req.headers.forEach((v, k) => headers.push([k, v])); // workerd normalises header order: no HEADER_ORDER signal here\n const cf = cfOf(req);\n const ev = buildWireEvent(\n {\n method: req.method, host: req.headers.get('host') ?? new URL(req.url).host, path, query, headers, ip,\n // The client's connection terminates at Cloudflare, so `cf.httpProtocol` really is the\n // visitor's protocol (ea's TAP_CAPS grants this tap TRUE_PROTO on that basis). Never read\n // a forwarded header for this — that would be the proxy's hop, not the client's.\n httpVersion: cf.httpProtocol ? cf.httpProtocol.replace(/^HTTP\\//i, '') : null,\n },\n { tap: TAP_HONO, rid, sid, newSession },\n );\n if (cf.asn) ev.asn = cf.asn;\n if (cf.country) ev.cc = cf.country;\n if (cf.tlsClientExtensionsSha1) ev.tlsx = cf.tlsClientExtensionsSha1;\n return ev;\n}\n\n/** Push then flush through `waitUntil` — the one rule every row (wire event, beacon, track()) follows. */\nexport function ship(e: Engine, ev: unknown, waitUntil: WaitUntil): void {\n e.queue.push(ev);\n e.queue.flush(waitUntil);\n}\n\n/** 204 and a `sig: 1` row on the event batch — one request per flush at the analyst, not one per page view. */\nasync function relayBeacon(eng: Engine, req: Request, ip: string | null, waitUntil: WaitUntil): Promise<Response> {\n const noContent = () => new Response(null, { status: 204, headers: { 'cache-control': 'no-store' } });\n if (Number(req.headers.get('content-length')) > FP_MAX) return new Response(null, { status: 413 });\n const body = await req.text();\n if (encoder.encode(body).byteLength > FP_MAX) return new Response(null, { status: 413 });\n let parsed: unknown;\n try { parsed = JSON.parse(body); } catch { return noContent(); } // not a beacon: drop it, never ship junk\n if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return noContent();\n // Spread first: the ip and the tap are the server's to say, whatever the body claimed.\n ship(eng, { ...(parsed as Record<string, unknown>), sig: 1, ip, tap: TAP_HONO }, waitUntil);\n return noContent();\n}\n\nexport function camada(opts: CamadaHonoOptions = {}): MiddlewareHandler {\n const challengePath = opts.challengePath ?? DEFAULT_CHALLENGE_PATH;\n const scriptPath = opts.scriptPath ?? SCRIPT_PATH;\n const fpPath = opts.fpPath ?? FP_PATH;\n\n return async function camadaHono(c: Context, next: Next): Promise<Response | void> {\n const t0 = Date.now(); // request start; the response event ships ts = t0 and dur = settle - t0 (ms), @camada/node's semantics\n // Reading c.env and building the engine are inside the guard too: a Workers env carries\n // non-string bindings, and a throw here would 5xx the app on its very first request.\n const env = guarded(() => ({ ...(c.env as Record<string, string | undefined> | undefined), ...opts.env }), {} as Record<string, string | undefined>);\n const eng = guarded(() => (env.CAMADA_DISABLED === '1' ? null : ensure(opts, env)), null);\n if (!eng) return next();\n // Same opt-out as @camada/next: the code option, or CAMADA_CHALLENGE=0 in the Worker env.\n const challengeOn = opts.challenge !== false && env.CAMADA_CHALLENGE !== '0';\n\n const waitUntil: WaitUntil = (p) => {\n try { c.executionCtx?.waitUntil(p); } catch { /* no execution context outside a fetch handler */ }\n };\n\n // The id of the `warn` rule that let this request through, read again after next() when the\n // response event is built (§D3). Every other action settles before the app ever runs.\n let warnRule: string | null = null;\n // Set on the capture path only: the ids this request shares with the app (scriptTag, track)\n // and with its own post-response event. Null when camada answered the request itself.\n let vars: CamadaVars | null = null;\n let newSession = false;\n\n const answer = await guardedAsync(async (): Promise<Response | null> => {\n eng.snap.ensureFresh(waitUntil);\n\n const req = c.req.raw;\n const { url, path, cf, ip, sid } = describeRequest(eng, req);\n\n const v = eng.snap.verdict({\n ip, path, ua: req.headers.get('user-agent'), // §D3: without ua every `ua` rule is false\n // `header` conditions read the request through this getter; `Headers.get` is\n // case-insensitive, so the lower-cased name the matcher asks with finds whatever\n // spelling the client sent. Without it every `header` rule is false.\n header: (n) => req.headers.get(n),\n asn: cf.asn ?? null, country: cf.country ?? null, tlsx: cf.tlsClientExtensionsSha1 ?? null,\n });\n warnRule = v.warn ? v.rule ?? null : null;\n\n if (v.block) {\n const ev = buildEvent(req, path, url.search, ip, sid);\n ev.st = 403;\n ev.blk = v.reason; // SDK-01: the analyst counts SDK blocks apart from the app's own 403s ('rule' when a rule decided)\n if (v.rule) ev.rl = v.rule;\n ship(eng, ev, waitUntil);\n const headers: Record<string, string> = {\n 'content-type': 'text/plain', 'x-block-reason': String(v.reason ?? ''), 'x-block-version': v.version ?? '',\n };\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 return new Response('Forbidden', { status: 403, headers });\n }\n\n // A challenge needs a resolved client ip: the nonce and the `_cch` cookie are bound to it.\n // Without one, fail open rather than mint a cookie every unidentified client could use.\n if (challengeOn && ip) {\n // The verify endpoint answers first — a challenged client must be able to reach it.\n if (req.method === 'POST' && path === challengePath) return verify(eng, req, ip, sid, challengePath, waitUntil);\n const token = cookieValue(req.headers.get('cookie') || '', CHALLENGE_COOKIE);\n if (v.challenge && !(await eng.kit.tokenValid(ip, Date.now(), token))) {\n return serve(eng, req, ip, sid, challengePath, path + url.search, waitUntil);\n }\n }\n\n // The beacon endpoints come after enforcement (a blocked or challenged client gets neither\n // the script nor a relay) and before the app (they are camada's, not its) — @camada/node's order.\n if (beaconEnabled(eng)) {\n if (req.method === 'GET' && path === scriptPath) {\n return new Response(iife, { status: 200, headers: { 'content-type': 'application/javascript', 'cache-control': 'public, max-age=3600' } });\n }\n if (req.method === 'POST' && path === fpPath) return relayBeacon(eng, req, ip, waitUntil);\n }\n\n // Capture: mint the ids the app, the beacon and the post-response event all share. The\n // session is decided here rather than after next() so track() inside the handler and the\n // event both carry the sid a first visit is about to be given.\n newSession = !sid;\n vars = { eng, rid: crypto.randomUUID(), sid: sid ?? crypto.randomUUID(), ip, waitUntil, scriptPath };\n c.set(VAR, vars);\n return null;\n }, null);\n\n if (answer) return answer;\n\n // Read the request now, before the app runs: after Deno.upgradeWebSocket it is closed and\n // reading its headers throws. `vars` is null only when the guard above threw before setting\n // it — then the event still ships, with fresh ids.\n const req = c.req.raw;\n const method = req.method;\n const wsRequest = guarded(() => req.headers.get('upgrade')?.toLowerCase() === 'websocket', false);\n // On Bun (hono/bun), c.env is the server, and server.upgrade() detaches the request from its\n // socket: server.requestIP() turns null. That is the upgrade test there, since Bun keeps a string or\n // Blob body's implicit Content-Type outside res.headers and a bare 200 alone is not proof.\n const bun = wsRequest ? guarded(() => {\n const e = c.env as { server?: unknown } | undefined;\n const s = (e && 'server' in e ? e.server : e) as BunServer | undefined; // hono/bun's getBunServer\n return typeof s?.requestIP === 'function' && s.requestIP(req) ? s : null;\n }, null) : null;\n const ev = guarded(() => {\n const { url, path, ip } = describeRequest(eng, req);\n return buildEvent(req, path, url.search, ip, vars?.sid ?? null, vars?.rid, newSession);\n }, null);\n\n await next();\n\n // The handler has answered, so this tap ships the real status — unlike @camada/next's\n // middleware position, which can only report pre-response. A server-sent-events body ships\n // once it has gone out (or the client left), so dur covers the stream, and waitUntil holds\n // the isolate until then (on workerd through a native pipe that sees the client leave). Any\n // other response ships now and is left exactly as the app returned it (see onBodyDone).\n // Bun (hono/bun) and @hono/node-ws answer an accepted WebSocket upgrade with a bare\n // `new Response()` (200, no Content-Type) that the server discards as it switches protocols;\n // Deno and workerd hand back the 101 itself.\n const upgraded = wsRequest && c.res.status === 200 && !c.res.headers.has('content-type')\n && (bun ? guarded(() => bun.requestIP(req) === null, false) : true);\n const status = upgraded ? 101 : c.res.status;\n const shipEvent = () => void guardedAsync(async () => {\n if (!ev) return;\n const cfg = eng.snap.config;\n if ((cfg?.exclude || []).some((x) => (ev.p as string).startsWith(x))) return;\n if (Math.random() >= (cfg?.sample ?? 1)) return;\n ev.ts = t0; // the request start: the timeline draws [ts, ts + dur]\n ev.st = status;\n ev.dur = Math.max(0, Date.now() - t0);\n if (warnRule) ev.wrn = warnRule; // §D3: the warn rule that let this request through\n ship(eng, ev, waitUntil);\n }, undefined);\n try {\n // onBodyDone hands back c.res itself unless it wrapped an SSE body: any copy loses a 101\n // upgrade on Deno < 2.6, Bun's implicit Content-Type and, on Bun < 1.2.10, an empty body's\n // status and headers.\n replaceRes(c, onBodyDone(c.res, shipEvent, { method, waitUntil, get signal() { return c.req.raw.signal; } })); // signal read on workerd only\n } catch (err) {\n logRateLimited(err); // a locked or foreign body: ship now (time to first byte), the response untouched\n shipEvent();\n }\n\n // Stamp x-rid (the rid of the shipped row) and mint the shared session cookie the other taps\n // use, so `sid`/`ns` are real here too (ea's capability mask for sdk-hono claims SESSION).\n // Never overwrite an existing cookie. After the wrap, on the response that goes out:\n // @hono/node-server 1.x rebuilds a wrapped (or HEAD) response from its original init, so a\n // header set before would be lost. An immutable response (a fetch() result, Response.redirect())\n // gets core's faithful copy; a 101 never does (core's withRid skips it too), since Deno < 2.6\n // stops Deno.serve over a copied upgrade.\n // Hono answers HEAD with `new Response(null, <the GET response>)`, rebuilt the same way. So a\n // HEAD gets that copy built here, exactly as Hono would build it, with the headers in its init.\n // Where the copy throws (node-server 1.x on an init-less response), the app's 500 goes out as is.\n guarded(() => {\n if (c.res.status === 101) return;\n const rid = ev?.rid as string | undefined;\n let cookie: string | undefined;\n if (vars && newSession) {\n const secure = new URL(c.req.raw.url).protocol === 'https:' ? '; Secure' : '';\n cookie = `${SESSION_COOKIE}=${vars.sid}; Path=/; Max-Age=${SESSION_MAX_AGE}; HttpOnly; SameSite=Lax${secure}`;\n }\n if (!rid && !cookie) return;\n if (method !== 'HEAD') {\n let out = c.res;\n if (cookie) out = withSetCookie(out, cookie);\n if (rid) out = withRid(out, { rid });\n return replaceRes(c, out);\n }\n const head = new Response(null, c.res);\n const headers = new Headers(head.headers);\n if (cookie) headers.append('set-cookie', cookie);\n if (rid) headers.set('x-rid', rid);\n replaceRes(c, new Response(null, { status: head.status, statusText: head.statusText, headers }));\n }, undefined);\n };\n}\n\n/** Sends `out` in place of c.res when it is a different object. Hono's c.res setter would copy it\n * again and copy the old response's headers back over it: the stale Content-Encoding/Length\n * copyResponse dropped from a Deno-decoded fetch() body, and a set-cookie list without ours.\n * `out` is already a faithful copy of c.res, so it goes in as is: the setter only merges into a\n * response it already holds. */\nfunction replaceRes(c: Context, out: Response): void {\n if (out === c.res) return;\n c.res = undefined;\n c.res = out;\n}\n\n/** 403 + the proof-of-work page (HTML navigations) or 403 JSON, plus the `blk: \"challenge\"` event. */\nasync function serve(eng: Engine, req: Request, ip: string, sid: string | null, action: string, target: string, waitUntil: WaitUntil): Promise<Response> {\n const url = new URL(req.url);\n const ev = buildEvent(req, url.pathname, url.search, ip, sid);\n ev.st = 403;\n ev.blk = 'challenge';\n ship(eng, ev, waitUntil);\n\n if (!wantsHtml(req.headers.get('accept'), req.headers.get('sec-fetch-dest'))) {\n return new Response('{\"error\":\"challenge_required\"}', {\n status: 403,\n headers: { 'content-type': 'application/json', 'cache-control': 'no-store', 'x-camada-challenge': '1' },\n });\n }\n const html = challengePage({ nonce: await eng.kit.nonce(ip, Date.now()), action, to: safeReturnTo(target) });\n return new Response(html, {\n status: 403,\n headers: { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-store', 'x-camada-challenge': '1' },\n });\n}\n\n/** POST from the page: validate, set `_cch`, 302 back, ship `{ st: 200, ch: 1 }`. */\nasync function verify(eng: Engine, req: Request, ip: string, sid: string | null, action: string, waitUntil: WaitUntil): Promise<Response> {\n const url = new URL(req.url);\n // camada answers this path before the app runs, so it must not become a place to post\n // hundreds of megabytes at an unauthenticated endpoint.\n if (Number(req.headers.get('content-length')) > BODY_MAX) return new Response(null, { status: 413 });\n const body = await req.text();\n if (body.length > BODY_MAX) return new Response(null, { status: 413 });\n const form = parseFormBody(body);\n const to = safeReturnTo(form.to);\n const now = Date.now();\n if (!(await eng.kit.verify(ip, now, form.nonce, form.solution))) {\n const html = challengePage({ nonce: await eng.kit.nonce(ip, now), action, to });\n return new Response(html, { status: 403, headers: { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-store' } });\n }\n const ev = buildEvent(req, url.pathname, '', ip, sid);\n ev.st = 200;\n ev.ch = 1; // challenge passed (contract §A3 ingest field)\n ship(eng, ev, waitUntil);\n return new Response(null, {\n status: 302,\n headers: {\n location: to,\n 'set-cookie': challengeCookie(await eng.kit.issue(ip, now), url.protocol === 'https:'),\n 'cache-control': 'no-store',\n },\n });\n}\n","// Workers hand env vars per request on `c.env`, not on a process — so the engine is built from\n// whatever the first request carries, merged with the options the app passed in code.\nimport { parseKey, parseTrustedProxyEnv, type SnapshotVersion, type TrustedProxyConfig } from '@camada/core';\n\nexport interface ResolvedEnv {\n ingestToken: string;\n snapToken: string;\n secret: string; // HMAC key for the challenge nonce/cookie — never leaves the isolate\n ingestUrl: string;\n snapshotUrl: string;\n trustedProxy: TrustedProxyConfig | null; // null = defer to server-delivered config\n}\n\nexport interface CamadaHonoOptions {\n key?: string;\n ingestUrl?: string;\n snapshotUrl?: string;\n trustedProxy?: TrustedProxyConfig | string | null;\n challenge?: boolean; // enforce `challenge` verdicts with the first-party page (default true)\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 scriptPath?: string; // where the first-party beacon script is served (default /_cam/b.js)\n fpPath?: string; // where that script posts the beacon (default /_cam/fp; must share scriptPath's directory — the script derives it)\n env?: Record<string, string | undefined>; // overrides c.env (tests, and apps that read config themselves)\n fetchImpl?: typeof fetch;\n}\n\nconst asProxy = (v: CamadaHonoOptions['trustedProxy']): TrustedProxyConfig | null =>\n typeof v === 'string' ? parseTrustedProxyEnv(v) : v ?? null;\n\n/** Returns null (the middleware stays inert, one log line) rather than throwing on bad config. */\nexport function resolveEnv(opts: CamadaHonoOptions, env: Record<string, string | undefined>): ResolvedEnv | null {\n const raw = opts.key || env.CAMADA_KEY;\n const key = parseKey(raw);\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 = (opts.ingestUrl || env.CAMADA_INGEST_URL || 'https://in.camada.app').replace(/\\/$/, '');\n return {\n ingestToken, snapToken,\n secret: raw || `${ingestToken}.${snapToken}`,\n ingestUrl,\n snapshotUrl: opts.snapshotUrl || env.CAMADA_SNAPSHOT_URL || `${ingestUrl}/snapshot`,\n trustedProxy: asProxy(opts.trustedProxy) ?? parseTrustedProxyEnv(env.CAMADA_TRUSTED_PROXY),\n };\n}\n","// Per-request state the middleware leaves on the Hono context for the two helpers an app calls\n// from its own handlers: `scriptTag(c)` (the beacon tag) and `track(c, event, data)` (an\n// app-context outcome). The key is private — the helpers are the API, not `c.get`. When the\n// middleware did not run for this request (a path it is not mounted on, no key, CAMADA_DISABLED,\n// or a request it answered itself) there is nothing on the context and both helpers stand down.\nimport type { Context } from 'hono';\nimport { guarded, guardedAsync, hashUserId, TAP_HONO } from '@camada/core';\nimport { ship, type Engine, type WaitUntil } from './camada.js';\n\nexport const VAR = '__camada';\n\nexport interface CamadaVars {\n eng: Engine;\n rid: string; // the request id the page event carries; the beacon and track() join on it\n sid: string; // the `_sfp` session — the one just minted when the request arrived without it\n ip: string | null;\n waitUntil: WaitUntil;\n scriptPath: string;\n}\n\nexport const beaconEnabled = (e: Engine): boolean => e.snap.config?.beacon !== false; // tenant switch; a cold engine serves\n\nconst readVars = (c: Context): CamadaVars | undefined => c.get(VAR) as CamadaVars | undefined;\n\n/**\n * Records an outcome the app knows and the wire cannot show: `login_failed`, `login_succeeded`,\n * `signup`, `password_reset`, `mfa_failed`, `payment_failed`, `payment_succeeded`, `coupon_failed`\n * (free-form; that vocabulary is what the analyst's rules read). Joined to this request's event\n * through its rid and session. The user identifier is HMAC-hashed in-process with the ingest\n * token — the raw value never reaches the queue. Never throws, never rejects; awaiting it is\n * optional (the flush rides `waitUntil`), so a handler may fire and forget.\n */\nexport function track(c: Context, event: string, data?: { user?: string }): Promise<void> {\n const vars = guarded(() => readVars(c), undefined);\n if (!vars) return Promise.resolve();\n const p = guardedAsync(async () => {\n const uid = data?.user ? await hashUserId(data.user, vars.eng.env.ingestToken) : null;\n ship(vars.eng, { tap: TAP_HONO, et: event, uid, rid: vars.rid, sid: vars.sid, ip: vars.ip, ts: Date.now() }, vars.waitUntil);\n }, undefined);\n vars.waitUntil(p); // the isolate may freeze right after the response: hold it open for the flush\n return p;\n}\n\n/** The `<script>` tag for an HTML response — `''` when camada is off for this request or the tenant turned the beacon off. */\nexport function scriptTag(c: Context): string {\n return guarded(() => {\n const vars = readVars(c);\n if (!vars || !beaconEnabled(vars.eng)) return '';\n return `<script src=\"${vars.scriptPath}?r=${vars.rid}\" async></script>`;\n }, '');\n}\n","{\n \"name\": \"@camada/hono\",\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 \"description\": \"camada SDK for Hono on Cloudflare Workers: snapshot enforcement, first-party challenge and beacon, track() outcomes, waitUntil-flushed event shipping\",\n \"exports\": {\n \".\": {\n \"types\": \"./dist/index.d.ts\",\n \"import\": \"./dist/index.js\",\n \"require\": \"./dist/index.cjs\"\n }\n },\n \"main\": \"./dist/index.cjs\",\n \"module\": \"./dist/index.js\",\n \"types\": \"./dist/index.d.ts\",\n \"dependencies\": {\n \"@camada/core\": \"file:../camada-core\",\n \"@camada/browser\": \"file:../camada-browser\"\n },\n \"peerDependencies\": {\n \"hono\": \">=4\"\n },\n \"devDependencies\": {\n \"@cloudflare/workers-types\": \"^4.20250109.0\",\n \"@types/node\": \"^22.10.0\",\n \"hono\": \"^4.6.0\",\n \"tsup\": \"^8.3.5\",\n \"typescript\": \"^5.7.2\",\n \"vitest\": \"^3.0.0\"\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_core","import_core","name","req","ev","iife"]}
package/dist/index.js CHANGED
@@ -7,6 +7,7 @@ import {
7
7
  resolveClientIp,
8
8
  logRateLimited,
9
9
  guardedAsync as guardedAsync2,
10
+ onBodyDone,
10
11
  guarded as guarded2,
11
12
  createChallengeAsync,
12
13
  challengePage,
@@ -18,6 +19,7 @@ import {
18
19
  DEFAULT_SNAPSHOT_VERSION,
19
20
  TAP_HONO as TAP_HONO2
20
21
  } from "@camada/core";
22
+ import { withSetCookie, withRid } from "@camada/core/fetch";
21
23
 
22
24
  // src/env.ts
23
25
  import { parseKey, parseTrustedProxyEnv } from "@camada/core";
@@ -64,7 +66,7 @@ function scriptTag(c) {
64
66
 
65
67
  // package.json
66
68
  var name = "@camada/hono";
67
- var version = "0.3.1";
69
+ var version = "0.3.2";
68
70
 
69
71
  // src/version.ts
70
72
  var SDK_ID = `${name}/${version}`;
@@ -197,28 +199,28 @@ function camada(opts = {}) {
197
199
  let newSession = false;
198
200
  const answer = await guardedAsync2(async () => {
199
201
  eng.snap.ensureFresh(waitUntil);
200
- const req = c.req.raw;
201
- const { url, path, cf, ip, sid } = describeRequest(eng, req);
202
+ const req2 = c.req.raw;
203
+ const { url, path, cf, ip, sid } = describeRequest(eng, req2);
202
204
  const v = eng.snap.verdict({
203
205
  ip,
204
206
  path,
205
- ua: req.headers.get("user-agent"),
207
+ ua: req2.headers.get("user-agent"),
206
208
  // §D3: without ua every `ua` rule is false
207
209
  // `header` conditions read the request through this getter; `Headers.get` is
208
210
  // case-insensitive, so the lower-cased name the matcher asks with finds whatever
209
211
  // spelling the client sent. Without it every `header` rule is false.
210
- header: (n) => req.headers.get(n),
212
+ header: (n) => req2.headers.get(n),
211
213
  asn: cf.asn ?? null,
212
214
  country: cf.country ?? null,
213
215
  tlsx: cf.tlsClientExtensionsSha1 ?? null
214
216
  });
215
217
  warnRule = v.warn ? v.rule ?? null : null;
216
218
  if (v.block) {
217
- const ev = buildEvent(req, path, url.search, ip, sid);
218
- ev.st = 403;
219
- ev.blk = v.reason;
220
- if (v.rule) ev.rl = v.rule;
221
- ship(eng, ev, waitUntil);
219
+ const ev2 = buildEvent(req2, path, url.search, ip, sid);
220
+ ev2.st = 403;
221
+ ev2.blk = v.reason;
222
+ if (v.rule) ev2.rl = v.rule;
223
+ ship(eng, ev2, waitUntil);
222
224
  const headers = {
223
225
  "content-type": "text/plain",
224
226
  "x-block-reason": String(v.reason ?? ""),
@@ -228,17 +230,17 @@ function camada(opts = {}) {
228
230
  return new Response("Forbidden", { status: 403, headers });
229
231
  }
230
232
  if (challengeOn && ip) {
231
- if (req.method === "POST" && path === challengePath) return verify(eng, req, ip, sid, challengePath, waitUntil);
232
- const token = cookieValue(req.headers.get("cookie") || "", CHALLENGE_COOKIE);
233
+ if (req2.method === "POST" && path === challengePath) return verify(eng, req2, ip, sid, challengePath, waitUntil);
234
+ const token = cookieValue(req2.headers.get("cookie") || "", CHALLENGE_COOKIE);
233
235
  if (v.challenge && !await eng.kit.tokenValid(ip, Date.now(), token)) {
234
- return serve(eng, req, ip, sid, challengePath, path + url.search, waitUntil);
236
+ return serve(eng, req2, ip, sid, challengePath, path + url.search, waitUntil);
235
237
  }
236
238
  }
237
239
  if (beaconEnabled(eng)) {
238
- if (req.method === "GET" && path === scriptPath) {
240
+ if (req2.method === "GET" && path === scriptPath) {
239
241
  return new Response(iife, { status: 200, headers: { "content-type": "application/javascript", "cache-control": "public, max-age=3600" } });
240
242
  }
241
- if (req.method === "POST" && path === fpPath) return relayBeacon(eng, req, ip, waitUntil);
243
+ if (req2.method === "POST" && path === fpPath) return relayBeacon(eng, req2, ip, waitUntil);
242
244
  }
243
245
  newSession = !sid;
244
246
  vars = { eng, rid: crypto.randomUUID(), sid: sid ?? crypto.randomUUID(), ip, waitUntil, scriptPath };
@@ -246,26 +248,68 @@ function camada(opts = {}) {
246
248
  return null;
247
249
  }, null);
248
250
  if (answer) return answer;
249
- await next();
250
- guarded2(() => {
251
- if (!vars || !newSession) return;
252
- const secure = new URL(c.req.raw.url).protocol === "https:" ? "; Secure" : "";
253
- c.res.headers.append("set-cookie", `${SESSION_COOKIE}=${vars.sid}; Path=/; Max-Age=${SESSION_MAX_AGE}; HttpOnly; SameSite=Lax${secure}`);
254
- }, void 0);
255
- void guardedAsync2(async () => {
256
- const req = c.req.raw;
251
+ const req = c.req.raw;
252
+ const method = req.method;
253
+ const wsRequest = guarded2(() => req.headers.get("upgrade")?.toLowerCase() === "websocket", false);
254
+ const bun = wsRequest ? guarded2(() => {
255
+ const e = c.env;
256
+ const s = e && "server" in e ? e.server : e;
257
+ return typeof s?.requestIP === "function" && s.requestIP(req) ? s : null;
258
+ }, null) : null;
259
+ const ev = guarded2(() => {
257
260
  const { url, path, ip } = describeRequest(eng, req);
261
+ return buildEvent(req, path, url.search, ip, vars?.sid ?? null, vars?.rid, newSession);
262
+ }, null);
263
+ await next();
264
+ const upgraded = wsRequest && c.res.status === 200 && !c.res.headers.has("content-type") && (bun ? guarded2(() => bun.requestIP(req) === null, false) : true);
265
+ const status = upgraded ? 101 : c.res.status;
266
+ const shipEvent = () => void guardedAsync2(async () => {
267
+ if (!ev) return;
258
268
  const cfg = eng.snap.config;
259
- if ((cfg?.exclude || []).some((x) => path.startsWith(x))) return;
269
+ if ((cfg?.exclude || []).some((x) => ev.p.startsWith(x))) return;
260
270
  if (Math.random() >= (cfg?.sample ?? 1)) return;
261
- const ev = buildEvent(req, path, url.search, ip, vars?.sid ?? null, vars?.rid, newSession);
262
- ev.st = c.res.status;
271
+ ev.ts = t0;
272
+ ev.st = status;
263
273
  ev.dur = Math.max(0, Date.now() - t0);
264
274
  if (warnRule) ev.wrn = warnRule;
265
275
  ship(eng, ev, waitUntil);
266
276
  }, void 0);
277
+ try {
278
+ replaceRes(c, onBodyDone(c.res, shipEvent, { method, waitUntil, get signal() {
279
+ return c.req.raw.signal;
280
+ } }));
281
+ } catch (err) {
282
+ logRateLimited(err);
283
+ shipEvent();
284
+ }
285
+ guarded2(() => {
286
+ if (c.res.status === 101) return;
287
+ const rid = ev?.rid;
288
+ let cookie;
289
+ if (vars && newSession) {
290
+ const secure = new URL(c.req.raw.url).protocol === "https:" ? "; Secure" : "";
291
+ cookie = `${SESSION_COOKIE}=${vars.sid}; Path=/; Max-Age=${SESSION_MAX_AGE}; HttpOnly; SameSite=Lax${secure}`;
292
+ }
293
+ if (!rid && !cookie) return;
294
+ if (method !== "HEAD") {
295
+ let out = c.res;
296
+ if (cookie) out = withSetCookie(out, cookie);
297
+ if (rid) out = withRid(out, { rid });
298
+ return replaceRes(c, out);
299
+ }
300
+ const head = new Response(null, c.res);
301
+ const headers = new Headers(head.headers);
302
+ if (cookie) headers.append("set-cookie", cookie);
303
+ if (rid) headers.set("x-rid", rid);
304
+ replaceRes(c, new Response(null, { status: head.status, statusText: head.statusText, headers }));
305
+ }, void 0);
267
306
  };
268
307
  }
308
+ function replaceRes(c, out) {
309
+ if (out === c.res) return;
310
+ c.res = void 0;
311
+ c.res = out;
312
+ }
269
313
  async function serve(eng, req, ip, sid, action, target, waitUntil) {
270
314
  const url = new URL(req.url);
271
315
  const ev = buildEvent(req, url.pathname, url.search, ip, sid);
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/camada.ts","../src/env.ts","../src/context.ts","../package.json","../src/version.ts"],"sourcesContent":["// @camada/hono — the Hono middleware for Cloudflare Workers (SEC-07, D33).\n// app.use('*', camada());\n// Workers-native: `cf-connecting-ip` for the client address, `request.cf` for asn / country /\n// TLS fingerprint (so ASN and country rules really enforce at this tap), and\n// `c.executionCtx.waitUntil` so the snapshot poll and the event flush outlive the response.\n// Everything runs inside camada's fail-open envelope: a camada bug, a dead ingest or a corrupt\n// snapshot costs telemetry, never the app's response.\nimport type { Context, MiddlewareHandler, Next } from 'hono';\nimport iife from '@camada/browser/iife-string';\nimport {\n SnapshotClient, EventQueue, buildWireEvent, resolveClientIp, logRateLimited, guardedAsync,\n guarded, createChallengeAsync, challengePage, challengeCookie, safeReturnTo, wantsHtml, parseFormBody,\n CHALLENGE_COOKIE, DEFAULT_SNAPSHOT_VERSION, TAP_HONO,\n type AsyncChallengeKit, type TrustedProxyConfig, type WireEvent,\n} from '@camada/core';\nimport { resolveEnv, type CamadaHonoOptions, type ResolvedEnv } from './env.js';\nimport { VAR, beaconEnabled, type CamadaVars } from './context.js';\nimport { SDK_ID } from './version.js';\n\nconst DEFAULT_CHALLENGE_PATH = '/__camada/challenge';\nconst SCRIPT_PATH = '/_cam/b.js'; // the beacon IIFE; its auto-init posts to the sibling `fp` and reads the rid from ?r=\nconst FP_PATH = '/_cam/fp';\nconst FP_MAX = 32 * 1024; // matches the analyst's /fp cap: never accept what ingest will 413\nconst SESSION_COOKIE = '_sfp'; // the same session cookie as every other tap: sid/ns stay comparable\nconst SESSION_MAX_AGE = 2592000;\nconst BODY_MAX = 4 * 1024; // the verify form is ~120 bytes; anything larger is not ours\nconst encoder = new TextEncoder();\n\nexport interface Engine {\n env: ResolvedEnv;\n snap: SnapshotClient;\n queue: EventQueue;\n kit: AsyncChallengeKit;\n}\n\nexport type WaitUntil = (p: Promise<unknown>) => void;\n\n// One engine per resolved configuration, not per module: a Workers isolate can host several\n// Hono apps, and a shared singleton would enforce the first tenant's snapshot on the second\n// and sign its `_cch` cookies with the first tenant's secret. An unconfigured request is never\n// cached — a warm-up or a differently-keyed mount must not leave the isolate inert for good.\nconst engines = new Map<string, Engine>();\n\n/** Test/reset hook: drops every cached engine. */\nexport function resetCamada(): void {\n for (const e of engines.values()) { e.snap.stop(); e.queue.stop(); }\n engines.clear();\n}\n\nfunction ensure(opts: CamadaHonoOptions, ctxEnv: Record<string, string | undefined>): Engine | null {\n const env = resolveEnv(opts, ctxEnv);\n if (!env) {\n logRateLimited(new Error('CAMADA_KEY not set — camada is inactive'));\n return null;\n }\n const snapshotVersion = opts.snapshotVersion ?? DEFAULT_SNAPSHOT_VERSION;\n const id = `${env.ingestToken}|${env.ingestUrl}|${env.snapshotUrl}|${snapshotVersion}`;\n const cached = engines.get(id);\n if (cached) return cached;\n const injected = opts.fetchImpl ? { fetchImpl: opts.fetchImpl } : {}; // never pass an explicit undefined key\n const engine: Engine = {\n env,\n snap: new SnapshotClient({\n url: env.snapshotUrl, token: env.snapToken, mode: 'lazy', sdk: SDK_ID,\n snapshotVersion, ...injected,\n }),\n queue: new EventQueue({ url: env.ingestUrl, token: env.ingestToken, sdk: SDK_ID, ...injected }),\n kit: createChallengeAsync({ secret: env.secret }),\n };\n engines.set(id, engine);\n return engine;\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\ninterface CfProps { asn?: number; country?: string; tlsClientExtensionsSha1?: string; httpProtocol?: string }\nconst cfOf = (req: Request): CfProps => ((req as Request & { cf?: CfProps }).cf ?? {});\n\nconst trustedProxy = (e: Engine): TrustedProxyConfig | null =>\n e.env.trustedProxy ?? e.snap.config?.trusted_proxy ?? null;\n\n/** Cloudflare sets `cf-connecting-ip` itself and a client cannot forge it — but only on\n * Workers, which is also the only place `request.cf` exists. On any other Hono runtime\n * (node-server, Bun, Deno) that header is attacker-controlled, so it is ignored there and the\n * trusted-proxy rules decide, exactly as they do for X-Forwarded-For. */\nconst clientIp = (e: Engine, req: Request): string | null => {\n const cf = (req as Request & { cf?: unknown }).cf;\n const direct = cf ? req.headers.get('cf-connecting-ip') : null;\n return direct || resolveClientIp(null, req.headers.get('x-forwarded-for'), trustedProxy(e));\n};\n\ninterface RequestFacts {\n url: URL;\n path: string;\n cf: CfProps;\n ip: string | null;\n sid: string | null;\n}\n\nfunction describeRequest(e: Engine, req: Request): RequestFacts {\n const url = new URL(req.url);\n const cookies = req.headers.get('cookie') || '';\n return { url, path: url.pathname, cf: cfOf(req), ip: clientIp(e, req), sid: cookieValue(cookies, SESSION_COOKIE) };\n}\n\nfunction buildEvent(\n req: Request, path: string, query: string, ip: string | null, sid: string | null,\n rid: string = crypto.randomUUID(), newSession = false, // the capture path passes the ids it shared with the app; camada's own answers need none\n): WireEvent {\n const headers: Array<[string, string]> = [];\n req.headers.forEach((v, k) => headers.push([k, v])); // workerd normalises header order: no HEADER_ORDER signal here\n const cf = cfOf(req);\n const ev = buildWireEvent(\n {\n method: req.method, host: req.headers.get('host') ?? new URL(req.url).host, path, query, headers, ip,\n // The client's connection terminates at Cloudflare, so `cf.httpProtocol` really is the\n // visitor's protocol (ea's TAP_CAPS grants this tap TRUE_PROTO on that basis). Never read\n // a forwarded header for this — that would be the proxy's hop, not the client's.\n httpVersion: cf.httpProtocol ? cf.httpProtocol.replace(/^HTTP\\//i, '') : null,\n },\n { tap: TAP_HONO, rid, sid, newSession },\n );\n if (cf.asn) ev.asn = cf.asn;\n if (cf.country) ev.cc = cf.country;\n if (cf.tlsClientExtensionsSha1) ev.tlsx = cf.tlsClientExtensionsSha1;\n return ev;\n}\n\n/** Push then flush through `waitUntil` — the one rule every row (wire event, beacon, track()) follows. */\nexport function ship(e: Engine, ev: unknown, waitUntil: WaitUntil): void {\n e.queue.push(ev);\n e.queue.flush(waitUntil);\n}\n\n/** 204 and a `sig: 1` row on the event batch — one request per flush at the analyst, not one per page view. */\nasync function relayBeacon(eng: Engine, req: Request, ip: string | null, waitUntil: WaitUntil): Promise<Response> {\n const noContent = () => new Response(null, { status: 204, headers: { 'cache-control': 'no-store' } });\n if (Number(req.headers.get('content-length')) > FP_MAX) return new Response(null, { status: 413 });\n const body = await req.text();\n if (encoder.encode(body).byteLength > FP_MAX) return new Response(null, { status: 413 });\n let parsed: unknown;\n try { parsed = JSON.parse(body); } catch { return noContent(); } // not a beacon: drop it, never ship junk\n if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return noContent();\n // Spread first: the ip and the tap are the server's to say, whatever the body claimed.\n ship(eng, { ...(parsed as Record<string, unknown>), sig: 1, ip, tap: TAP_HONO }, waitUntil);\n return noContent();\n}\n\nexport function camada(opts: CamadaHonoOptions = {}): MiddlewareHandler {\n const challengePath = opts.challengePath ?? DEFAULT_CHALLENGE_PATH;\n const scriptPath = opts.scriptPath ?? SCRIPT_PATH;\n const fpPath = opts.fpPath ?? FP_PATH;\n\n return async function camadaHono(c: Context, next: Next): Promise<Response | void> {\n const t0 = Date.now(); // request start; the response event ships dur = settle - t0 (ms), @camada/node's semantics\n // Reading c.env and building the engine are inside the guard too: a Workers env carries\n // non-string bindings, and a throw here would 5xx the app on its very first request.\n const env = guarded(() => ({ ...(c.env as Record<string, string | undefined> | undefined), ...opts.env }), {} as Record<string, string | undefined>);\n const eng = guarded(() => (env.CAMADA_DISABLED === '1' ? null : ensure(opts, env)), null);\n if (!eng) return next();\n // Same opt-out as @camada/next: the code option, or CAMADA_CHALLENGE=0 in the Worker env.\n const challengeOn = opts.challenge !== false && env.CAMADA_CHALLENGE !== '0';\n\n const waitUntil: WaitUntil = (p) => {\n try { c.executionCtx?.waitUntil(p); } catch { /* no execution context outside a fetch handler */ }\n };\n\n // The id of the `warn` rule that let this request through, read again after next() when the\n // response event is built (§D3). Every other action settles before the app ever runs.\n let warnRule: string | null = null;\n // Set on the capture path only: the ids this request shares with the app (scriptTag, track)\n // and with its own post-response event. Null when camada answered the request itself.\n let vars: CamadaVars | null = null;\n let newSession = false;\n\n const answer = await guardedAsync(async (): Promise<Response | null> => {\n eng.snap.ensureFresh(waitUntil);\n\n const req = c.req.raw;\n const { url, path, cf, ip, sid } = describeRequest(eng, req);\n\n const v = eng.snap.verdict({\n ip, path, ua: req.headers.get('user-agent'), // §D3: without ua every `ua` rule is false\n // `header` conditions read the request through this getter; `Headers.get` is\n // case-insensitive, so the lower-cased name the matcher asks with finds whatever\n // spelling the client sent. Without it every `header` rule is false.\n header: (n) => req.headers.get(n),\n asn: cf.asn ?? null, country: cf.country ?? null, tlsx: cf.tlsClientExtensionsSha1 ?? null,\n });\n warnRule = v.warn ? v.rule ?? null : null;\n\n if (v.block) {\n const ev = buildEvent(req, path, url.search, ip, sid);\n ev.st = 403;\n ev.blk = v.reason; // SDK-01: the analyst counts SDK blocks apart from the app's own 403s ('rule' when a rule decided)\n if (v.rule) ev.rl = v.rule;\n ship(eng, ev, waitUntil);\n const headers: Record<string, string> = {\n 'content-type': 'text/plain', 'x-block-reason': String(v.reason ?? ''), 'x-block-version': v.version ?? '',\n };\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 return new Response('Forbidden', { status: 403, headers });\n }\n\n // A challenge needs a resolved client ip: the nonce and the `_cch` cookie are bound to it.\n // Without one, fail open rather than mint a cookie every unidentified client could use.\n if (challengeOn && ip) {\n // The verify endpoint answers first — a challenged client must be able to reach it.\n if (req.method === 'POST' && path === challengePath) return verify(eng, req, ip, sid, challengePath, waitUntil);\n const token = cookieValue(req.headers.get('cookie') || '', CHALLENGE_COOKIE);\n if (v.challenge && !(await eng.kit.tokenValid(ip, Date.now(), token))) {\n return serve(eng, req, ip, sid, challengePath, path + url.search, waitUntil);\n }\n }\n\n // The beacon endpoints come after enforcement (a blocked or challenged client gets neither\n // the script nor a relay) and before the app (they are camada's, not its) — @camada/node's order.\n if (beaconEnabled(eng)) {\n if (req.method === 'GET' && path === scriptPath) {\n return new Response(iife, { status: 200, headers: { 'content-type': 'application/javascript', 'cache-control': 'public, max-age=3600' } });\n }\n if (req.method === 'POST' && path === fpPath) return relayBeacon(eng, req, ip, waitUntil);\n }\n\n // Capture: mint the ids the app, the beacon and the post-response event all share. The\n // session is decided here rather than after next() so track() inside the handler and the\n // event both carry the sid a first visit is about to be given.\n newSession = !sid;\n vars = { eng, rid: crypto.randomUUID(), sid: sid ?? crypto.randomUUID(), ip, waitUntil, scriptPath };\n c.set(VAR, vars);\n return null;\n }, null);\n\n if (answer) return answer;\n\n await next();\n\n // Mint the shared session cookie the other taps use, so `sid`/`ns` are real here too\n // (ea's capability mask for sdk-hono claims SESSION). Never overwrite an existing one.\n guarded(() => {\n if (!vars || !newSession) return;\n const secure = new URL(c.req.raw.url).protocol === 'https:' ? '; Secure' : '';\n c.res.headers.append('set-cookie', `${SESSION_COOKIE}=${vars.sid}; Path=/; Max-Age=${SESSION_MAX_AGE}; HttpOnly; SameSite=Lax${secure}`);\n }, undefined);\n\n // The response has settled, so this tap ships the real status — unlike @camada/next's\n // middleware position, which can only report pre-response.\n void guardedAsync(async () => {\n const req = c.req.raw;\n const { url, path, ip } = describeRequest(eng, req);\n const cfg = eng.snap.config;\n if ((cfg?.exclude || []).some((x) => path.startsWith(x))) return;\n if (Math.random() >= (cfg?.sample ?? 1)) return;\n // `vars` is null only when the guard above threw before setting it — then the event still ships, with fresh ids.\n const ev = buildEvent(req, path, url.search, ip, vars?.sid ?? null, vars?.rid, newSession);\n ev.st = c.res.status;\n ev.dur = Math.max(0, Date.now() - t0);\n if (warnRule) ev.wrn = warnRule; // §D3: the warn rule that let this request through\n ship(eng, ev, waitUntil);\n }, undefined);\n };\n}\n\n/** 403 + the proof-of-work page (HTML navigations) or 403 JSON, plus the `blk: \"challenge\"` event. */\nasync function serve(eng: Engine, req: Request, ip: string, sid: string | null, action: string, target: string, waitUntil: WaitUntil): Promise<Response> {\n const url = new URL(req.url);\n const ev = buildEvent(req, url.pathname, url.search, ip, sid);\n ev.st = 403;\n ev.blk = 'challenge';\n ship(eng, ev, waitUntil);\n\n if (!wantsHtml(req.headers.get('accept'), req.headers.get('sec-fetch-dest'))) {\n return new Response('{\"error\":\"challenge_required\"}', {\n status: 403,\n headers: { 'content-type': 'application/json', 'cache-control': 'no-store', 'x-camada-challenge': '1' },\n });\n }\n const html = challengePage({ nonce: await eng.kit.nonce(ip, Date.now()), action, to: safeReturnTo(target) });\n return new Response(html, {\n status: 403,\n headers: { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-store', 'x-camada-challenge': '1' },\n });\n}\n\n/** POST from the page: validate, set `_cch`, 302 back, ship `{ st: 200, ch: 1 }`. */\nasync function verify(eng: Engine, req: Request, ip: string, sid: string | null, action: string, waitUntil: WaitUntil): Promise<Response> {\n const url = new URL(req.url);\n // camada answers this path before the app runs, so it must not become a place to post\n // hundreds of megabytes at an unauthenticated endpoint.\n if (Number(req.headers.get('content-length')) > BODY_MAX) return new Response(null, { status: 413 });\n const body = await req.text();\n if (body.length > BODY_MAX) return new Response(null, { status: 413 });\n const form = parseFormBody(body);\n const to = safeReturnTo(form.to);\n const now = Date.now();\n if (!(await eng.kit.verify(ip, now, form.nonce, form.solution))) {\n const html = challengePage({ nonce: await eng.kit.nonce(ip, now), action, to });\n return new Response(html, { status: 403, headers: { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-store' } });\n }\n const ev = buildEvent(req, url.pathname, '', ip, sid);\n ev.st = 200;\n ev.ch = 1; // challenge passed (contract §A3 ingest field)\n ship(eng, ev, waitUntil);\n return new Response(null, {\n status: 302,\n headers: {\n location: to,\n 'set-cookie': challengeCookie(await eng.kit.issue(ip, now), url.protocol === 'https:'),\n 'cache-control': 'no-store',\n },\n });\n}\n","// Workers hand env vars per request on `c.env`, not on a process — so the engine is built from\n// whatever the first request carries, merged with the options the app passed in code.\nimport { parseKey, parseTrustedProxyEnv, type SnapshotVersion, type TrustedProxyConfig } from '@camada/core';\n\nexport interface ResolvedEnv {\n ingestToken: string;\n snapToken: string;\n secret: string; // HMAC key for the challenge nonce/cookie — never leaves the isolate\n ingestUrl: string;\n snapshotUrl: string;\n trustedProxy: TrustedProxyConfig | null; // null = defer to server-delivered config\n}\n\nexport interface CamadaHonoOptions {\n key?: string;\n ingestUrl?: string;\n snapshotUrl?: string;\n trustedProxy?: TrustedProxyConfig | string | null;\n challenge?: boolean; // enforce `challenge` verdicts with the first-party page (default true)\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 scriptPath?: string; // where the first-party beacon script is served (default /_cam/b.js)\n fpPath?: string; // where that script posts the beacon (default /_cam/fp; must share scriptPath's directory — the script derives it)\n env?: Record<string, string | undefined>; // overrides c.env (tests, and apps that read config themselves)\n fetchImpl?: typeof fetch;\n}\n\nconst asProxy = (v: CamadaHonoOptions['trustedProxy']): TrustedProxyConfig | null =>\n typeof v === 'string' ? parseTrustedProxyEnv(v) : v ?? null;\n\n/** Returns null (the middleware stays inert, one log line) rather than throwing on bad config. */\nexport function resolveEnv(opts: CamadaHonoOptions, env: Record<string, string | undefined>): ResolvedEnv | null {\n const raw = opts.key || env.CAMADA_KEY;\n const key = parseKey(raw);\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 = (opts.ingestUrl || env.CAMADA_INGEST_URL || 'https://in.camada.app').replace(/\\/$/, ''); // PLACEHOLDER default — confirm the production ingest domain before any npm publish\n return {\n ingestToken, snapToken,\n secret: raw || `${ingestToken}.${snapToken}`,\n ingestUrl,\n snapshotUrl: opts.snapshotUrl || env.CAMADA_SNAPSHOT_URL || `${ingestUrl}/snapshot`,\n trustedProxy: asProxy(opts.trustedProxy) ?? parseTrustedProxyEnv(env.CAMADA_TRUSTED_PROXY),\n };\n}\n","// Per-request state the middleware leaves on the Hono context for the two helpers an app calls\n// from its own handlers: `scriptTag(c)` (the beacon tag) and `track(c, event, data)` (an\n// app-context outcome). The key is private — the helpers are the API, not `c.get`. When the\n// middleware did not run for this request (a path it is not mounted on, no key, CAMADA_DISABLED,\n// or a request it answered itself) there is nothing on the context and both helpers stand down.\nimport type { Context } from 'hono';\nimport { guarded, guardedAsync, hashUserId, TAP_HONO } from '@camada/core';\nimport { ship, type Engine, type WaitUntil } from './camada.js';\n\nexport const VAR = '__camada';\n\nexport interface CamadaVars {\n eng: Engine;\n rid: string; // the request id the page event carries; the beacon and track() join on it\n sid: string; // the `_sfp` session — the one just minted when the request arrived without it\n ip: string | null;\n waitUntil: WaitUntil;\n scriptPath: string;\n}\n\nexport const beaconEnabled = (e: Engine): boolean => e.snap.config?.beacon !== false; // tenant switch; a cold engine serves\n\nconst readVars = (c: Context): CamadaVars | undefined => c.get(VAR) as CamadaVars | undefined;\n\n/**\n * Records an outcome the app knows and the wire cannot show: `login_failed`, `login_succeeded`,\n * `signup`, `password_reset`, `mfa_failed`, `payment_failed`, `payment_succeeded`, `coupon_failed`\n * (free-form; that vocabulary is what the analyst's rules read). Joined to this request's event\n * through its rid and session. The user identifier is HMAC-hashed in-process with the ingest\n * token — the raw value never reaches the queue. Never throws, never rejects; awaiting it is\n * optional (the flush rides `waitUntil`), so a handler may fire and forget.\n */\nexport function track(c: Context, event: string, data?: { user?: string }): Promise<void> {\n const vars = guarded(() => readVars(c), undefined);\n if (!vars) return Promise.resolve();\n const p = guardedAsync(async () => {\n const uid = data?.user ? await hashUserId(data.user, vars.eng.env.ingestToken) : null;\n ship(vars.eng, { tap: TAP_HONO, et: event, uid, rid: vars.rid, sid: vars.sid, ip: vars.ip, ts: Date.now() }, vars.waitUntil);\n }, undefined);\n vars.waitUntil(p); // the isolate may freeze right after the response: hold it open for the flush\n return p;\n}\n\n/** The `<script>` tag for an HTML response — `''` when camada is off for this request or the tenant turned the beacon off. */\nexport function scriptTag(c: Context): string {\n return guarded(() => {\n const vars = readVars(c);\n if (!vars || !beaconEnabled(vars.eng)) return '';\n return `<script src=\"${vars.scriptPath}?r=${vars.rid}\" async></script>`;\n }, '');\n}\n","{\n \"name\": \"@camada/hono\",\n \"version\": \"0.3.1\",\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 \"description\": \"camada SDK for Hono on Cloudflare Workers: snapshot enforcement, first-party challenge and beacon, track() outcomes, waitUntil-flushed event shipping\",\n \"exports\": {\n \".\": {\n \"types\": \"./dist/index.d.ts\",\n \"import\": \"./dist/index.js\",\n \"require\": \"./dist/index.cjs\"\n }\n },\n \"main\": \"./dist/index.cjs\",\n \"module\": \"./dist/index.js\",\n \"types\": \"./dist/index.d.ts\",\n \"dependencies\": {\n \"@camada/core\": \"file:../camada-core\",\n \"@camada/browser\": \"file:../camada-browser\"\n },\n \"peerDependencies\": {\n \"hono\": \">=4\"\n },\n \"devDependencies\": {\n \"@cloudflare/workers-types\": \"^4.20250109.0\",\n \"@types/node\": \"^22.10.0\",\n \"hono\": \"^4.6.0\",\n \"tsup\": \"^8.3.5\",\n \"typescript\": \"^5.7.2\",\n \"vitest\": \"^3.0.0\"\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":["guardedAsync","guarded","TAP_HONO","name","TAP_HONO","guarded","guardedAsync"]}
1
+ {"version":3,"sources":["../src/camada.ts","../src/env.ts","../src/context.ts","../package.json","../src/version.ts"],"sourcesContent":["// @camada/hono — the Hono middleware for Cloudflare Workers (SEC-07, D33).\n// app.use('*', camada());\n// Workers-native: `cf-connecting-ip` for the client address, `request.cf` for asn / country /\n// TLS fingerprint (so ASN and country rules really enforce at this tap), and\n// `c.executionCtx.waitUntil` so the snapshot poll and the event flush outlive the response.\n// Everything runs inside camada's fail-open envelope: a camada bug, a dead ingest or a corrupt\n// snapshot costs telemetry, never the app's response.\nimport type { Context, MiddlewareHandler, Next } from 'hono';\nimport iife from '@camada/browser/iife-string';\nimport {\n SnapshotClient, EventQueue, buildWireEvent, resolveClientIp, logRateLimited, guardedAsync, onBodyDone,\n guarded, createChallengeAsync, challengePage, challengeCookie, safeReturnTo, wantsHtml, parseFormBody,\n CHALLENGE_COOKIE, DEFAULT_SNAPSHOT_VERSION, TAP_HONO,\n type AsyncChallengeKit, type TrustedProxyConfig, type WireEvent,\n} from '@camada/core';\nimport { withSetCookie, withRid } from '@camada/core/fetch';\nimport { resolveEnv, type CamadaHonoOptions, type ResolvedEnv } from './env.js';\nimport { VAR, beaconEnabled, type CamadaVars } from './context.js';\nimport { SDK_ID } from './version.js';\n\nconst DEFAULT_CHALLENGE_PATH = '/__camada/challenge';\nconst SCRIPT_PATH = '/_cam/b.js'; // the beacon IIFE; its auto-init posts to the sibling `fp` and reads the rid from ?r=\nconst FP_PATH = '/_cam/fp';\nconst FP_MAX = 32 * 1024; // matches the analyst's /fp cap: never accept what ingest will 413\nconst SESSION_COOKIE = '_sfp'; // the same session cookie as every other tap: sid/ns stay comparable\nconst SESSION_MAX_AGE = 2592000;\nconst BODY_MAX = 4 * 1024; // the verify form is ~120 bytes; anything larger is not ours\nconst encoder = new TextEncoder();\n\nexport interface Engine {\n env: ResolvedEnv;\n snap: SnapshotClient;\n queue: EventQueue;\n kit: AsyncChallengeKit;\n}\n\nexport type WaitUntil = (p: Promise<unknown>) => void;\n\n// One engine per resolved configuration, not per module: a Workers isolate can host several\n// Hono apps, and a shared singleton would enforce the first tenant's snapshot on the second\n// and sign its `_cch` cookies with the first tenant's secret. An unconfigured request is never\n// cached — a warm-up or a differently-keyed mount must not leave the isolate inert for good.\nconst engines = new Map<string, Engine>();\n\n/** Test/reset hook: drops every cached engine. */\nexport function resetCamada(): void {\n for (const e of engines.values()) { e.snap.stop(); e.queue.stop(); }\n engines.clear();\n}\n\nfunction ensure(opts: CamadaHonoOptions, ctxEnv: Record<string, string | undefined>): Engine | null {\n const env = resolveEnv(opts, ctxEnv);\n if (!env) {\n logRateLimited(new Error('CAMADA_KEY not set — camada is inactive'));\n return null;\n }\n const snapshotVersion = opts.snapshotVersion ?? DEFAULT_SNAPSHOT_VERSION;\n const id = `${env.ingestToken}|${env.ingestUrl}|${env.snapshotUrl}|${snapshotVersion}`;\n const cached = engines.get(id);\n if (cached) return cached;\n const injected = opts.fetchImpl ? { fetchImpl: opts.fetchImpl } : {}; // never pass an explicit undefined key\n const engine: Engine = {\n env,\n snap: new SnapshotClient({\n url: env.snapshotUrl, token: env.snapToken, mode: 'lazy', sdk: SDK_ID,\n snapshotVersion, ...injected,\n }),\n queue: new EventQueue({ url: env.ingestUrl, token: env.ingestToken, sdk: SDK_ID, ...injected }),\n kit: createChallengeAsync({ secret: env.secret }),\n };\n engines.set(id, engine);\n return engine;\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\ninterface BunServer { requestIP: (req: Request) => unknown }\n\ninterface CfProps { asn?: number; country?: string; tlsClientExtensionsSha1?: string; httpProtocol?: string }\nconst cfOf = (req: Request): CfProps => ((req as Request & { cf?: CfProps }).cf ?? {});\n\nconst trustedProxy = (e: Engine): TrustedProxyConfig | null =>\n e.env.trustedProxy ?? e.snap.config?.trusted_proxy ?? null;\n\n/** Cloudflare sets `cf-connecting-ip` itself and a client cannot forge it — but only on\n * Workers, which is also the only place `request.cf` exists. On any other Hono runtime\n * (node-server, Bun, Deno) that header is attacker-controlled, so it is ignored there and the\n * trusted-proxy rules decide, exactly as they do for X-Forwarded-For. */\nconst clientIp = (e: Engine, req: Request): string | null => {\n const cf = (req as Request & { cf?: unknown }).cf;\n const direct = cf ? req.headers.get('cf-connecting-ip') : null;\n return direct || resolveClientIp(null, req.headers.get('x-forwarded-for'), trustedProxy(e));\n};\n\ninterface RequestFacts {\n url: URL;\n path: string;\n cf: CfProps;\n ip: string | null;\n sid: string | null;\n}\n\nfunction describeRequest(e: Engine, req: Request): RequestFacts {\n const url = new URL(req.url);\n const cookies = req.headers.get('cookie') || '';\n return { url, path: url.pathname, cf: cfOf(req), ip: clientIp(e, req), sid: cookieValue(cookies, SESSION_COOKIE) };\n}\n\nfunction buildEvent(\n req: Request, path: string, query: string, ip: string | null, sid: string | null,\n rid: string = crypto.randomUUID(), newSession = false, // the capture path passes the ids it shared with the app; camada's own answers need none\n): WireEvent {\n const headers: Array<[string, string]> = [];\n req.headers.forEach((v, k) => headers.push([k, v])); // workerd normalises header order: no HEADER_ORDER signal here\n const cf = cfOf(req);\n const ev = buildWireEvent(\n {\n method: req.method, host: req.headers.get('host') ?? new URL(req.url).host, path, query, headers, ip,\n // The client's connection terminates at Cloudflare, so `cf.httpProtocol` really is the\n // visitor's protocol (ea's TAP_CAPS grants this tap TRUE_PROTO on that basis). Never read\n // a forwarded header for this — that would be the proxy's hop, not the client's.\n httpVersion: cf.httpProtocol ? cf.httpProtocol.replace(/^HTTP\\//i, '') : null,\n },\n { tap: TAP_HONO, rid, sid, newSession },\n );\n if (cf.asn) ev.asn = cf.asn;\n if (cf.country) ev.cc = cf.country;\n if (cf.tlsClientExtensionsSha1) ev.tlsx = cf.tlsClientExtensionsSha1;\n return ev;\n}\n\n/** Push then flush through `waitUntil` — the one rule every row (wire event, beacon, track()) follows. */\nexport function ship(e: Engine, ev: unknown, waitUntil: WaitUntil): void {\n e.queue.push(ev);\n e.queue.flush(waitUntil);\n}\n\n/** 204 and a `sig: 1` row on the event batch — one request per flush at the analyst, not one per page view. */\nasync function relayBeacon(eng: Engine, req: Request, ip: string | null, waitUntil: WaitUntil): Promise<Response> {\n const noContent = () => new Response(null, { status: 204, headers: { 'cache-control': 'no-store' } });\n if (Number(req.headers.get('content-length')) > FP_MAX) return new Response(null, { status: 413 });\n const body = await req.text();\n if (encoder.encode(body).byteLength > FP_MAX) return new Response(null, { status: 413 });\n let parsed: unknown;\n try { parsed = JSON.parse(body); } catch { return noContent(); } // not a beacon: drop it, never ship junk\n if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return noContent();\n // Spread first: the ip and the tap are the server's to say, whatever the body claimed.\n ship(eng, { ...(parsed as Record<string, unknown>), sig: 1, ip, tap: TAP_HONO }, waitUntil);\n return noContent();\n}\n\nexport function camada(opts: CamadaHonoOptions = {}): MiddlewareHandler {\n const challengePath = opts.challengePath ?? DEFAULT_CHALLENGE_PATH;\n const scriptPath = opts.scriptPath ?? SCRIPT_PATH;\n const fpPath = opts.fpPath ?? FP_PATH;\n\n return async function camadaHono(c: Context, next: Next): Promise<Response | void> {\n const t0 = Date.now(); // request start; the response event ships ts = t0 and dur = settle - t0 (ms), @camada/node's semantics\n // Reading c.env and building the engine are inside the guard too: a Workers env carries\n // non-string bindings, and a throw here would 5xx the app on its very first request.\n const env = guarded(() => ({ ...(c.env as Record<string, string | undefined> | undefined), ...opts.env }), {} as Record<string, string | undefined>);\n const eng = guarded(() => (env.CAMADA_DISABLED === '1' ? null : ensure(opts, env)), null);\n if (!eng) return next();\n // Same opt-out as @camada/next: the code option, or CAMADA_CHALLENGE=0 in the Worker env.\n const challengeOn = opts.challenge !== false && env.CAMADA_CHALLENGE !== '0';\n\n const waitUntil: WaitUntil = (p) => {\n try { c.executionCtx?.waitUntil(p); } catch { /* no execution context outside a fetch handler */ }\n };\n\n // The id of the `warn` rule that let this request through, read again after next() when the\n // response event is built (§D3). Every other action settles before the app ever runs.\n let warnRule: string | null = null;\n // Set on the capture path only: the ids this request shares with the app (scriptTag, track)\n // and with its own post-response event. Null when camada answered the request itself.\n let vars: CamadaVars | null = null;\n let newSession = false;\n\n const answer = await guardedAsync(async (): Promise<Response | null> => {\n eng.snap.ensureFresh(waitUntil);\n\n const req = c.req.raw;\n const { url, path, cf, ip, sid } = describeRequest(eng, req);\n\n const v = eng.snap.verdict({\n ip, path, ua: req.headers.get('user-agent'), // §D3: without ua every `ua` rule is false\n // `header` conditions read the request through this getter; `Headers.get` is\n // case-insensitive, so the lower-cased name the matcher asks with finds whatever\n // spelling the client sent. Without it every `header` rule is false.\n header: (n) => req.headers.get(n),\n asn: cf.asn ?? null, country: cf.country ?? null, tlsx: cf.tlsClientExtensionsSha1 ?? null,\n });\n warnRule = v.warn ? v.rule ?? null : null;\n\n if (v.block) {\n const ev = buildEvent(req, path, url.search, ip, sid);\n ev.st = 403;\n ev.blk = v.reason; // SDK-01: the analyst counts SDK blocks apart from the app's own 403s ('rule' when a rule decided)\n if (v.rule) ev.rl = v.rule;\n ship(eng, ev, waitUntil);\n const headers: Record<string, string> = {\n 'content-type': 'text/plain', 'x-block-reason': String(v.reason ?? ''), 'x-block-version': v.version ?? '',\n };\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 return new Response('Forbidden', { status: 403, headers });\n }\n\n // A challenge needs a resolved client ip: the nonce and the `_cch` cookie are bound to it.\n // Without one, fail open rather than mint a cookie every unidentified client could use.\n if (challengeOn && ip) {\n // The verify endpoint answers first — a challenged client must be able to reach it.\n if (req.method === 'POST' && path === challengePath) return verify(eng, req, ip, sid, challengePath, waitUntil);\n const token = cookieValue(req.headers.get('cookie') || '', CHALLENGE_COOKIE);\n if (v.challenge && !(await eng.kit.tokenValid(ip, Date.now(), token))) {\n return serve(eng, req, ip, sid, challengePath, path + url.search, waitUntil);\n }\n }\n\n // The beacon endpoints come after enforcement (a blocked or challenged client gets neither\n // the script nor a relay) and before the app (they are camada's, not its) — @camada/node's order.\n if (beaconEnabled(eng)) {\n if (req.method === 'GET' && path === scriptPath) {\n return new Response(iife, { status: 200, headers: { 'content-type': 'application/javascript', 'cache-control': 'public, max-age=3600' } });\n }\n if (req.method === 'POST' && path === fpPath) return relayBeacon(eng, req, ip, waitUntil);\n }\n\n // Capture: mint the ids the app, the beacon and the post-response event all share. The\n // session is decided here rather than after next() so track() inside the handler and the\n // event both carry the sid a first visit is about to be given.\n newSession = !sid;\n vars = { eng, rid: crypto.randomUUID(), sid: sid ?? crypto.randomUUID(), ip, waitUntil, scriptPath };\n c.set(VAR, vars);\n return null;\n }, null);\n\n if (answer) return answer;\n\n // Read the request now, before the app runs: after Deno.upgradeWebSocket it is closed and\n // reading its headers throws. `vars` is null only when the guard above threw before setting\n // it — then the event still ships, with fresh ids.\n const req = c.req.raw;\n const method = req.method;\n const wsRequest = guarded(() => req.headers.get('upgrade')?.toLowerCase() === 'websocket', false);\n // On Bun (hono/bun), c.env is the server, and server.upgrade() detaches the request from its\n // socket: server.requestIP() turns null. That is the upgrade test there, since Bun keeps a string or\n // Blob body's implicit Content-Type outside res.headers and a bare 200 alone is not proof.\n const bun = wsRequest ? guarded(() => {\n const e = c.env as { server?: unknown } | undefined;\n const s = (e && 'server' in e ? e.server : e) as BunServer | undefined; // hono/bun's getBunServer\n return typeof s?.requestIP === 'function' && s.requestIP(req) ? s : null;\n }, null) : null;\n const ev = guarded(() => {\n const { url, path, ip } = describeRequest(eng, req);\n return buildEvent(req, path, url.search, ip, vars?.sid ?? null, vars?.rid, newSession);\n }, null);\n\n await next();\n\n // The handler has answered, so this tap ships the real status — unlike @camada/next's\n // middleware position, which can only report pre-response. A server-sent-events body ships\n // once it has gone out (or the client left), so dur covers the stream, and waitUntil holds\n // the isolate until then (on workerd through a native pipe that sees the client leave). Any\n // other response ships now and is left exactly as the app returned it (see onBodyDone).\n // Bun (hono/bun) and @hono/node-ws answer an accepted WebSocket upgrade with a bare\n // `new Response()` (200, no Content-Type) that the server discards as it switches protocols;\n // Deno and workerd hand back the 101 itself.\n const upgraded = wsRequest && c.res.status === 200 && !c.res.headers.has('content-type')\n && (bun ? guarded(() => bun.requestIP(req) === null, false) : true);\n const status = upgraded ? 101 : c.res.status;\n const shipEvent = () => void guardedAsync(async () => {\n if (!ev) return;\n const cfg = eng.snap.config;\n if ((cfg?.exclude || []).some((x) => (ev.p as string).startsWith(x))) return;\n if (Math.random() >= (cfg?.sample ?? 1)) return;\n ev.ts = t0; // the request start: the timeline draws [ts, ts + dur]\n ev.st = status;\n ev.dur = Math.max(0, Date.now() - t0);\n if (warnRule) ev.wrn = warnRule; // §D3: the warn rule that let this request through\n ship(eng, ev, waitUntil);\n }, undefined);\n try {\n // onBodyDone hands back c.res itself unless it wrapped an SSE body: any copy loses a 101\n // upgrade on Deno < 2.6, Bun's implicit Content-Type and, on Bun < 1.2.10, an empty body's\n // status and headers.\n replaceRes(c, onBodyDone(c.res, shipEvent, { method, waitUntil, get signal() { return c.req.raw.signal; } })); // signal read on workerd only\n } catch (err) {\n logRateLimited(err); // a locked or foreign body: ship now (time to first byte), the response untouched\n shipEvent();\n }\n\n // Stamp x-rid (the rid of the shipped row) and mint the shared session cookie the other taps\n // use, so `sid`/`ns` are real here too (ea's capability mask for sdk-hono claims SESSION).\n // Never overwrite an existing cookie. After the wrap, on the response that goes out:\n // @hono/node-server 1.x rebuilds a wrapped (or HEAD) response from its original init, so a\n // header set before would be lost. An immutable response (a fetch() result, Response.redirect())\n // gets core's faithful copy; a 101 never does (core's withRid skips it too), since Deno < 2.6\n // stops Deno.serve over a copied upgrade.\n // Hono answers HEAD with `new Response(null, <the GET response>)`, rebuilt the same way. So a\n // HEAD gets that copy built here, exactly as Hono would build it, with the headers in its init.\n // Where the copy throws (node-server 1.x on an init-less response), the app's 500 goes out as is.\n guarded(() => {\n if (c.res.status === 101) return;\n const rid = ev?.rid as string | undefined;\n let cookie: string | undefined;\n if (vars && newSession) {\n const secure = new URL(c.req.raw.url).protocol === 'https:' ? '; Secure' : '';\n cookie = `${SESSION_COOKIE}=${vars.sid}; Path=/; Max-Age=${SESSION_MAX_AGE}; HttpOnly; SameSite=Lax${secure}`;\n }\n if (!rid && !cookie) return;\n if (method !== 'HEAD') {\n let out = c.res;\n if (cookie) out = withSetCookie(out, cookie);\n if (rid) out = withRid(out, { rid });\n return replaceRes(c, out);\n }\n const head = new Response(null, c.res);\n const headers = new Headers(head.headers);\n if (cookie) headers.append('set-cookie', cookie);\n if (rid) headers.set('x-rid', rid);\n replaceRes(c, new Response(null, { status: head.status, statusText: head.statusText, headers }));\n }, undefined);\n };\n}\n\n/** Sends `out` in place of c.res when it is a different object. Hono's c.res setter would copy it\n * again and copy the old response's headers back over it: the stale Content-Encoding/Length\n * copyResponse dropped from a Deno-decoded fetch() body, and a set-cookie list without ours.\n * `out` is already a faithful copy of c.res, so it goes in as is: the setter only merges into a\n * response it already holds. */\nfunction replaceRes(c: Context, out: Response): void {\n if (out === c.res) return;\n c.res = undefined;\n c.res = out;\n}\n\n/** 403 + the proof-of-work page (HTML navigations) or 403 JSON, plus the `blk: \"challenge\"` event. */\nasync function serve(eng: Engine, req: Request, ip: string, sid: string | null, action: string, target: string, waitUntil: WaitUntil): Promise<Response> {\n const url = new URL(req.url);\n const ev = buildEvent(req, url.pathname, url.search, ip, sid);\n ev.st = 403;\n ev.blk = 'challenge';\n ship(eng, ev, waitUntil);\n\n if (!wantsHtml(req.headers.get('accept'), req.headers.get('sec-fetch-dest'))) {\n return new Response('{\"error\":\"challenge_required\"}', {\n status: 403,\n headers: { 'content-type': 'application/json', 'cache-control': 'no-store', 'x-camada-challenge': '1' },\n });\n }\n const html = challengePage({ nonce: await eng.kit.nonce(ip, Date.now()), action, to: safeReturnTo(target) });\n return new Response(html, {\n status: 403,\n headers: { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-store', 'x-camada-challenge': '1' },\n });\n}\n\n/** POST from the page: validate, set `_cch`, 302 back, ship `{ st: 200, ch: 1 }`. */\nasync function verify(eng: Engine, req: Request, ip: string, sid: string | null, action: string, waitUntil: WaitUntil): Promise<Response> {\n const url = new URL(req.url);\n // camada answers this path before the app runs, so it must not become a place to post\n // hundreds of megabytes at an unauthenticated endpoint.\n if (Number(req.headers.get('content-length')) > BODY_MAX) return new Response(null, { status: 413 });\n const body = await req.text();\n if (body.length > BODY_MAX) return new Response(null, { status: 413 });\n const form = parseFormBody(body);\n const to = safeReturnTo(form.to);\n const now = Date.now();\n if (!(await eng.kit.verify(ip, now, form.nonce, form.solution))) {\n const html = challengePage({ nonce: await eng.kit.nonce(ip, now), action, to });\n return new Response(html, { status: 403, headers: { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-store' } });\n }\n const ev = buildEvent(req, url.pathname, '', ip, sid);\n ev.st = 200;\n ev.ch = 1; // challenge passed (contract §A3 ingest field)\n ship(eng, ev, waitUntil);\n return new Response(null, {\n status: 302,\n headers: {\n location: to,\n 'set-cookie': challengeCookie(await eng.kit.issue(ip, now), url.protocol === 'https:'),\n 'cache-control': 'no-store',\n },\n });\n}\n","// Workers hand env vars per request on `c.env`, not on a process — so the engine is built from\n// whatever the first request carries, merged with the options the app passed in code.\nimport { parseKey, parseTrustedProxyEnv, type SnapshotVersion, type TrustedProxyConfig } from '@camada/core';\n\nexport interface ResolvedEnv {\n ingestToken: string;\n snapToken: string;\n secret: string; // HMAC key for the challenge nonce/cookie — never leaves the isolate\n ingestUrl: string;\n snapshotUrl: string;\n trustedProxy: TrustedProxyConfig | null; // null = defer to server-delivered config\n}\n\nexport interface CamadaHonoOptions {\n key?: string;\n ingestUrl?: string;\n snapshotUrl?: string;\n trustedProxy?: TrustedProxyConfig | string | null;\n challenge?: boolean; // enforce `challenge` verdicts with the first-party page (default true)\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 scriptPath?: string; // where the first-party beacon script is served (default /_cam/b.js)\n fpPath?: string; // where that script posts the beacon (default /_cam/fp; must share scriptPath's directory — the script derives it)\n env?: Record<string, string | undefined>; // overrides c.env (tests, and apps that read config themselves)\n fetchImpl?: typeof fetch;\n}\n\nconst asProxy = (v: CamadaHonoOptions['trustedProxy']): TrustedProxyConfig | null =>\n typeof v === 'string' ? parseTrustedProxyEnv(v) : v ?? null;\n\n/** Returns null (the middleware stays inert, one log line) rather than throwing on bad config. */\nexport function resolveEnv(opts: CamadaHonoOptions, env: Record<string, string | undefined>): ResolvedEnv | null {\n const raw = opts.key || env.CAMADA_KEY;\n const key = parseKey(raw);\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 = (opts.ingestUrl || env.CAMADA_INGEST_URL || 'https://in.camada.app').replace(/\\/$/, '');\n return {\n ingestToken, snapToken,\n secret: raw || `${ingestToken}.${snapToken}`,\n ingestUrl,\n snapshotUrl: opts.snapshotUrl || env.CAMADA_SNAPSHOT_URL || `${ingestUrl}/snapshot`,\n trustedProxy: asProxy(opts.trustedProxy) ?? parseTrustedProxyEnv(env.CAMADA_TRUSTED_PROXY),\n };\n}\n","// Per-request state the middleware leaves on the Hono context for the two helpers an app calls\n// from its own handlers: `scriptTag(c)` (the beacon tag) and `track(c, event, data)` (an\n// app-context outcome). The key is private — the helpers are the API, not `c.get`. When the\n// middleware did not run for this request (a path it is not mounted on, no key, CAMADA_DISABLED,\n// or a request it answered itself) there is nothing on the context and both helpers stand down.\nimport type { Context } from 'hono';\nimport { guarded, guardedAsync, hashUserId, TAP_HONO } from '@camada/core';\nimport { ship, type Engine, type WaitUntil } from './camada.js';\n\nexport const VAR = '__camada';\n\nexport interface CamadaVars {\n eng: Engine;\n rid: string; // the request id the page event carries; the beacon and track() join on it\n sid: string; // the `_sfp` session — the one just minted when the request arrived without it\n ip: string | null;\n waitUntil: WaitUntil;\n scriptPath: string;\n}\n\nexport const beaconEnabled = (e: Engine): boolean => e.snap.config?.beacon !== false; // tenant switch; a cold engine serves\n\nconst readVars = (c: Context): CamadaVars | undefined => c.get(VAR) as CamadaVars | undefined;\n\n/**\n * Records an outcome the app knows and the wire cannot show: `login_failed`, `login_succeeded`,\n * `signup`, `password_reset`, `mfa_failed`, `payment_failed`, `payment_succeeded`, `coupon_failed`\n * (free-form; that vocabulary is what the analyst's rules read). Joined to this request's event\n * through its rid and session. The user identifier is HMAC-hashed in-process with the ingest\n * token — the raw value never reaches the queue. Never throws, never rejects; awaiting it is\n * optional (the flush rides `waitUntil`), so a handler may fire and forget.\n */\nexport function track(c: Context, event: string, data?: { user?: string }): Promise<void> {\n const vars = guarded(() => readVars(c), undefined);\n if (!vars) return Promise.resolve();\n const p = guardedAsync(async () => {\n const uid = data?.user ? await hashUserId(data.user, vars.eng.env.ingestToken) : null;\n ship(vars.eng, { tap: TAP_HONO, et: event, uid, rid: vars.rid, sid: vars.sid, ip: vars.ip, ts: Date.now() }, vars.waitUntil);\n }, undefined);\n vars.waitUntil(p); // the isolate may freeze right after the response: hold it open for the flush\n return p;\n}\n\n/** The `<script>` tag for an HTML response — `''` when camada is off for this request or the tenant turned the beacon off. */\nexport function scriptTag(c: Context): string {\n return guarded(() => {\n const vars = readVars(c);\n if (!vars || !beaconEnabled(vars.eng)) return '';\n return `<script src=\"${vars.scriptPath}?r=${vars.rid}\" async></script>`;\n }, '');\n}\n","{\n \"name\": \"@camada/hono\",\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 \"description\": \"camada SDK for Hono on Cloudflare Workers: snapshot enforcement, first-party challenge and beacon, track() outcomes, waitUntil-flushed event shipping\",\n \"exports\": {\n \".\": {\n \"types\": \"./dist/index.d.ts\",\n \"import\": \"./dist/index.js\",\n \"require\": \"./dist/index.cjs\"\n }\n },\n \"main\": \"./dist/index.cjs\",\n \"module\": \"./dist/index.js\",\n \"types\": \"./dist/index.d.ts\",\n \"dependencies\": {\n \"@camada/core\": \"file:../camada-core\",\n \"@camada/browser\": \"file:../camada-browser\"\n },\n \"peerDependencies\": {\n \"hono\": \">=4\"\n },\n \"devDependencies\": {\n \"@cloudflare/workers-types\": \"^4.20250109.0\",\n \"@types/node\": \"^22.10.0\",\n \"hono\": \"^4.6.0\",\n \"tsup\": \"^8.3.5\",\n \"typescript\": \"^5.7.2\",\n \"vitest\": \"^3.0.0\"\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":["guardedAsync","guarded","TAP_HONO","name","TAP_HONO","guarded","guardedAsync","req","ev"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camada/hono",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "engines": {
@@ -23,8 +23,8 @@
23
23
  "module": "./dist/index.js",
24
24
  "types": "./dist/index.d.ts",
25
25
  "dependencies": {
26
- "@camada/core": "^0.4.0",
27
- "@camada/browser": "^0.2.0"
26
+ "@camada/core": "^0.5.0",
27
+ "@camada/browser": "^0.3.0"
28
28
  },
29
29
  "peerDependencies": {
30
30
  "hono": ">=4"