@camada/hono 0.3.3 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -3
- package/dist/index.cjs +118 -34
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +11 -1
- package/dist/index.d.ts +11 -1
- package/dist/index.js +120 -34
- package/dist/index.js.map +1 -1
- package/package.json +11 -6
package/README.md
CHANGED
|
@@ -4,6 +4,12 @@ camada for [Hono](https://hono.dev) on Cloudflare Workers: enforces the tenant s
|
|
|
4
4
|
(your ordered custom rules, then block, allow, challenge), serves a first-party proof-of-work
|
|
5
5
|
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
|
+
It also runs on `@hono/node-server`, Bun and Deno: there it takes the client from the socket
|
|
8
|
+
peer, and batches events on a timer (one `POST /e` per flush, drained on exit) instead of one
|
|
9
|
+
per request. On a serverless host without an execution context (`hono/vercel`, `hono/netlify`,
|
|
10
|
+
`hono/aws-lambda`, `hono/lambda-edge`, or any of these with `VERCEL`, `AWS_LAMBDA_FUNCTION_NAME`,
|
|
11
|
+
`NETLIFY` or `DENO_DEPLOYMENT_ID` set) every event still flushes as it is shipped, since the
|
|
12
|
+
function freezes after the response; see the `mode` option.
|
|
7
13
|
Fails open by design — a camada outage or bug never 5xxes your app.
|
|
8
14
|
|
|
9
15
|
```sh
|
|
@@ -71,7 +77,13 @@ unprovisioned environment behaves exactly as if camada were not installed.
|
|
|
71
77
|
counts only on Workers (where Cloudflare sets it and a client cannot forge it, and where
|
|
72
78
|
`request.cf` proves it), and `X-Forwarded-For` counts only under a trusted-proxy config. On
|
|
73
79
|
`@hono/node-server`, Bun or Deno the CF header is ignored — there it is just another header
|
|
74
|
-
the caller controls.
|
|
80
|
+
the caller controls. There the client is the socket peer, read from the object the runtime
|
|
81
|
+
hands Hono as `c.env`: `incoming.socket.remoteAddress` on `@hono/node-server`,
|
|
82
|
+
`server.requestIP(req)` on `Bun.serve({ fetch: app.fetch })`, `info.remoteAddr` on
|
|
83
|
+
`Deno.serve(app.fetch)`. With a trusted-proxy config, `X-Forwarded-For` is walked from that
|
|
84
|
+
peer, as on every other adapter. Anywhere else (a host that hands Hono no such object), pass
|
|
85
|
+
the `peer` option; without a peer and a trusted proxy the client stays unknown, so IP rules do
|
|
86
|
+
not match and no challenge is served.
|
|
75
87
|
3. Runs your ordered custom rules (see below), then the allow, block and challenge lists.
|
|
76
88
|
4. **Block** → `403` with `x-block-reason` before your handler; the event still ships, with
|
|
77
89
|
`st: 403` and `blk: <reason>` so the analyst counts SDK blocks apart from your own 403s.
|
|
@@ -139,12 +151,19 @@ work, sets `_cch` (an HMAC token, 1 h, `HttpOnly; SameSite=Lax`) and 302s back t
|
|
|
139
151
|
URL. Events: `{ st: 403, blk: "challenge" }` when served, `{ st: 200, ch: 1 }` when passed.
|
|
140
152
|
|
|
141
153
|
Requests that are not HTML navigations (no `text/html` in `Accept`, or a `sec-fetch-dest` other
|
|
142
|
-
than `document`)
|
|
143
|
-
|
|
154
|
+
than `document`) are not challenged: they pass to your app, the response carries
|
|
155
|
+
`x-camada-challenge: 1` beside `x-rid`, and the event ships with the real status and `chs: 1`
|
|
156
|
+
(unsampled, never excluded). Your frontend may act on the header.
|
|
144
157
|
|
|
145
158
|
The nonce and the cookie are bound to the client IP, so a request camada cannot identify is
|
|
146
159
|
never challenged (it would mint a cookie any other unidentified client could present).
|
|
147
160
|
|
|
161
|
+
The page sends its own `Content-Security-Policy`, allowing only its inline script and style by
|
|
162
|
+
`sha256` hash, and drops any `Content-Security-Policy(-Report-Only)` a middleware in front of
|
|
163
|
+
camada put on `c.res` or `c.header()`. A middleware that sets its policy *after* `await next()`
|
|
164
|
+
(`secureHeaders({ contentSecurityPolicy })`) overwrites the page's and blocks the solver, so
|
|
165
|
+
register `camada()` before it: a challenged request then never reaches it.
|
|
166
|
+
|
|
148
167
|
## Options
|
|
149
168
|
|
|
150
169
|
| option | default | meaning |
|
|
@@ -159,6 +178,27 @@ never challenged (it would mint a cookie any other unidentified client could pre
|
|
|
159
178
|
| `scriptPath` | `/_cam/b.js` | where the first-party beacon script is served |
|
|
160
179
|
| `fpPath` | `/_cam/fp` | where that script posts the beacon; keep it in `scriptPath`'s directory |
|
|
161
180
|
| `env` | `c.env` | overrides the Worker env (tests) |
|
|
181
|
+
| `mode` | detected | off Workers only: `'timer'` batches events on a 15 s timer and drains them on exit (a long-lived server); `'lazy'` flushes every event as it is shipped (a function that freezes after the response). Unset: `'timer'` on a recognised `@hono/node-server` / `Bun.serve` / `Deno.serve` server with none of `VERCEL`, `AWS_LAMBDA_FUNCTION_NAME`, `NETLIFY`, `DENO_DEPLOYMENT_ID` set, else `'lazy'`. `CAMADA_SERVERLESS=1` forces `'lazy'`. With an execution context (Workers) every event flushes through `waitUntil` |
|
|
182
|
+
| `peer` | the runtime's socket peer | off Workers: `(c) => string \| null`, the socket address for this request, e.g. `(c) => getConnInfo(c).remote.address` with your runtime's `getConnInfo`. Wins over what camada reads from `c.env`; never consulted on Workers |
|
|
183
|
+
|
|
184
|
+
`CAMADA_TRUSTED_PROXY` sets `trustedProxy` from the env. With neither set, it defaults to `vercel`
|
|
185
|
+
where `VERCEL` is set (Vercel rewrites `X-Forwarded-For`, so its rightmost entry is the client),
|
|
186
|
+
else to the tenant's server config. Under `VERCEL` that default also wins over the trusted-proxy
|
|
187
|
+
setting in your dashboard: only the `trustedProxy` option or `CAMADA_TRUSTED_PROXY` overrides it.
|
|
188
|
+
Behind any other proxy or load balancer off Workers, set it: without it the client is the proxy's
|
|
189
|
+
address.
|
|
190
|
+
|
|
191
|
+
On a Deno edge host that sets none of the variables above (Supabase Edge Functions and the like),
|
|
192
|
+
`Deno.serve` looks like a long-lived server and events would wait for the 15 s timer in an isolate
|
|
193
|
+
that may not live that long. Set `mode: 'lazy'` or `CAMADA_SERVERLESS=1` there. Deno Deploy is
|
|
194
|
+
detected through `DENO_DEPLOYMENT_ID`, read with `Deno.env.get` where the permission API is absent.
|
|
195
|
+
On Vercel without an execution context (`hono/vercel`), each flush is handed to Vercel's
|
|
196
|
+
request-context `waitUntil` when the platform provides one, so the function stays up until it is
|
|
197
|
+
sent.
|
|
198
|
+
|
|
199
|
+
A body over the cap at `/_cam/fp` (32 KB) or the challenge path (4 KB) is refused with `413`; on
|
|
200
|
+
`@hono/node-server` the client may see a connection reset instead, since the server stops reading
|
|
201
|
+
the upload.
|
|
162
202
|
|
|
163
203
|
`CAMADA_CHALLENGE=0` in the Worker env switches the challenge off without a code change.
|
|
164
204
|
|
package/dist/index.cjs
CHANGED
|
@@ -45,6 +45,22 @@ var import_fetch = require("@camada/core/fetch");
|
|
|
45
45
|
|
|
46
46
|
// src/env.ts
|
|
47
47
|
var import_core = require("@camada/core");
|
|
48
|
+
function hostVar(name2, env) {
|
|
49
|
+
if (env[name2] !== void 0) return env[name2];
|
|
50
|
+
const g = globalThis;
|
|
51
|
+
try {
|
|
52
|
+
if (g.Deno) {
|
|
53
|
+
const perms = g.Deno.permissions;
|
|
54
|
+
if (perms) {
|
|
55
|
+
if (typeof perms.querySync !== "function" || perms.querySync({ name: "env", variable: name2 }).state !== "granted") return void 0;
|
|
56
|
+
}
|
|
57
|
+
return g.Deno.env?.get?.(name2);
|
|
58
|
+
}
|
|
59
|
+
return g.process?.env?.[name2];
|
|
60
|
+
} catch {
|
|
61
|
+
return void 0;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
48
64
|
var asProxy = (v) => typeof v === "string" ? (0, import_core.parseTrustedProxyEnv)(v) : v ?? null;
|
|
49
65
|
function resolveEnv(opts, env) {
|
|
50
66
|
const raw = opts.key || env.CAMADA_KEY;
|
|
@@ -59,7 +75,8 @@ function resolveEnv(opts, env) {
|
|
|
59
75
|
secret: raw || `${ingestToken}.${snapToken}`,
|
|
60
76
|
ingestUrl,
|
|
61
77
|
snapshotUrl: opts.snapshotUrl || env.CAMADA_SNAPSHOT_URL || `${ingestUrl}/snapshot`,
|
|
62
|
-
|
|
78
|
+
// On Vercel the platform rewrites X-Forwarded-For, so its rightmost entry is the client (as @camada/next).
|
|
79
|
+
trustedProxy: asProxy(opts.trustedProxy) ?? (0, import_core.parseTrustedProxyEnv)(env.CAMADA_TRUSTED_PROXY) ?? (hostVar("VERCEL", env) ? { mode: "vercel" } : null)
|
|
63
80
|
};
|
|
64
81
|
}
|
|
65
82
|
|
|
@@ -88,7 +105,7 @@ function scriptTag(c) {
|
|
|
88
105
|
|
|
89
106
|
// package.json
|
|
90
107
|
var name = "@camada/hono";
|
|
91
|
-
var version = "0.
|
|
108
|
+
var version = "0.4.0";
|
|
92
109
|
|
|
93
110
|
// src/version.ts
|
|
94
111
|
var SDK_ID = `${name}/${version}`;
|
|
@@ -101,8 +118,25 @@ var FP_MAX = 32 * 1024;
|
|
|
101
118
|
var SESSION_COOKIE = "_sfp";
|
|
102
119
|
var SESSION_MAX_AGE = 2592e3;
|
|
103
120
|
var BODY_MAX = 4 * 1024;
|
|
104
|
-
var
|
|
121
|
+
var noWait = () => {
|
|
122
|
+
};
|
|
123
|
+
var flushNow = () => {
|
|
124
|
+
};
|
|
125
|
+
function requestContextWaitUntil() {
|
|
126
|
+
return (0, import_core3.guarded)(() => {
|
|
127
|
+
const g = globalThis;
|
|
128
|
+
const ctx = g[/* @__PURE__ */ Symbol.for("@vercel/request-context")]?.get?.();
|
|
129
|
+
if (typeof ctx?.waitUntil !== "function") return null;
|
|
130
|
+
return (p) => {
|
|
131
|
+
try {
|
|
132
|
+
ctx.waitUntil(p);
|
|
133
|
+
} catch {
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
}, null);
|
|
137
|
+
}
|
|
105
138
|
var engines = /* @__PURE__ */ new Map();
|
|
139
|
+
var NO_STORE_HEADERS = ["cache-control", "cdn-cache-control", "surrogate-control", "cloudflare-cdn-cache-control", "vercel-cdn-cache-control", "netlify-cdn-cache-control"];
|
|
106
140
|
function resetCamada() {
|
|
107
141
|
for (const e of engines.values()) {
|
|
108
142
|
e.snap.stop();
|
|
@@ -147,15 +181,41 @@ var cookieValue = (cookie, name2) => {
|
|
|
147
181
|
};
|
|
148
182
|
var cfOf = (req) => req.cf ?? {};
|
|
149
183
|
var trustedProxy = (e) => e.env.trustedProxy ?? e.snap.config?.trusted_proxy ?? null;
|
|
150
|
-
var
|
|
184
|
+
var runtime = globalThis;
|
|
185
|
+
var onWorkers = () => runtime.navigator?.userAgent === "Cloudflare-Workers";
|
|
186
|
+
function serverShape(env) {
|
|
187
|
+
if (!env || typeof env !== "object" || onWorkers()) return null;
|
|
188
|
+
const e = env;
|
|
189
|
+
const str = (v) => typeof v === "string" && v ? v : null;
|
|
190
|
+
if (runtime.process?.versions?.node && e.incoming && typeof e.incoming === "object") return () => str(e.incoming?.socket?.remoteAddress);
|
|
191
|
+
if (runtime.Bun !== void 0) {
|
|
192
|
+
const bun = "server" in e ? e.server : e;
|
|
193
|
+
if (typeof bun?.requestIP === "function") return (req) => str(bun.requestIP(req)?.address);
|
|
194
|
+
}
|
|
195
|
+
if (runtime.Deno !== void 0 && e.remoteAddr && typeof e.remoteAddr === "object") return () => str(e.remoteAddr?.hostname);
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
var SERVERLESS_HOSTS = ["VERCEL", "AWS_LAMBDA_FUNCTION_NAME", "NETLIFY", "DENO_DEPLOYMENT_ID"];
|
|
199
|
+
function batches(opts, env, runtimeEnv) {
|
|
200
|
+
if (hostVar("CAMADA_SERVERLESS", env) === "1") return false;
|
|
201
|
+
if (opts.mode) return opts.mode === "timer";
|
|
202
|
+
if (SERVERLESS_HOSTS.some((k) => hostVar(k, env))) return false;
|
|
203
|
+
return serverShape(runtimeEnv) !== null;
|
|
204
|
+
}
|
|
205
|
+
var clientIp = (e, req, peer) => {
|
|
151
206
|
const cf = req.cf;
|
|
152
|
-
|
|
153
|
-
return
|
|
207
|
+
if (cf) return req.headers.get("cf-connecting-ip") || (0, import_core3.resolveClientIp)(null, req.headers.get("x-forwarded-for"), trustedProxy(e));
|
|
208
|
+
return (0, import_core3.resolveClientIp)(peer, req.headers.get("x-forwarded-for"), trustedProxy(e));
|
|
154
209
|
};
|
|
155
|
-
|
|
210
|
+
var peerOf = (opts, c, hasCtx) => (0, import_core3.guarded)(() => {
|
|
211
|
+
if (hasCtx || c.req.raw.cf) return null;
|
|
212
|
+
if (opts.peer) return opts.peer(c) || null;
|
|
213
|
+
return serverShape(c.env)?.(c.req.raw) ?? null;
|
|
214
|
+
}, null);
|
|
215
|
+
function describeRequest(e, req, peer) {
|
|
156
216
|
const url = new URL(req.url);
|
|
157
217
|
const cookies = req.headers.get("cookie") || "";
|
|
158
|
-
return { url, path: url.pathname, cf: cfOf(req), ip: clientIp(e, req), sid: cookieValue(cookies, SESSION_COOKIE) };
|
|
218
|
+
return { url, path: url.pathname, cf: cfOf(req), ip: clientIp(e, req, peer), sid: cookieValue(cookies, SESSION_COOKIE) };
|
|
159
219
|
}
|
|
160
220
|
function buildEvent(req, path, query, ip, sid, rid = crypto.randomUUID(), newSession = false) {
|
|
161
221
|
const headers = [];
|
|
@@ -183,13 +243,12 @@ function buildEvent(req, path, query, ip, sid, rid = crypto.randomUUID(), newSes
|
|
|
183
243
|
}
|
|
184
244
|
function ship(e, ev, waitUntil) {
|
|
185
245
|
e.queue.push(ev);
|
|
186
|
-
e.queue.flush(waitUntil);
|
|
246
|
+
if (waitUntil !== noWait) e.queue.flush(waitUntil);
|
|
187
247
|
}
|
|
188
248
|
async function relayBeacon(eng, req, ip, waitUntil) {
|
|
189
249
|
const noContent = () => new Response(null, { status: 204, headers: { "cache-control": "no-store" } });
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
if (encoder.encode(body).byteLength > FP_MAX) return new Response(null, { status: 413 });
|
|
250
|
+
const body = await (0, import_core3.readBodyCapped)(req, FP_MAX);
|
|
251
|
+
if (body === null) return new Response(null, { status: 413 });
|
|
193
252
|
let parsed;
|
|
194
253
|
try {
|
|
195
254
|
parsed = JSON.parse(body);
|
|
@@ -197,7 +256,7 @@ async function relayBeacon(eng, req, ip, waitUntil) {
|
|
|
197
256
|
return noContent();
|
|
198
257
|
}
|
|
199
258
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return noContent();
|
|
200
|
-
ship(eng, { ...parsed, sig: 1, ip, tap: import_core3.TAP_HONO }, waitUntil);
|
|
259
|
+
ship(eng, { ...parsed, sig: 1, ip, tap: import_core3.TAP_HONO, rts: Date.now() }, waitUntil);
|
|
201
260
|
return noContent();
|
|
202
261
|
}
|
|
203
262
|
function camada(opts = {}) {
|
|
@@ -210,19 +269,28 @@ function camada(opts = {}) {
|
|
|
210
269
|
const eng = (0, import_core3.guarded)(() => env.CAMADA_DISABLED === "1" ? null : ensure(opts, env), null);
|
|
211
270
|
if (!eng) return next();
|
|
212
271
|
const challengeOn = opts.challenge !== false && env.CAMADA_CHALLENGE !== "0";
|
|
213
|
-
|
|
272
|
+
let ctx = null;
|
|
273
|
+
try {
|
|
274
|
+
ctx = typeof c.executionCtx?.waitUntil === "function" ? c.executionCtx : null;
|
|
275
|
+
} catch {
|
|
276
|
+
}
|
|
277
|
+
const batch = !ctx && (0, import_core3.guarded)(() => batches(opts, env, c.env), false);
|
|
278
|
+
const waitUntil = ctx ? (p) => {
|
|
214
279
|
try {
|
|
215
|
-
|
|
280
|
+
ctx.waitUntil(p);
|
|
216
281
|
} catch {
|
|
217
282
|
}
|
|
218
|
-
};
|
|
283
|
+
} : batch ? noWait : requestContextWaitUntil() ?? flushNow;
|
|
284
|
+
if (batch) (0, import_core3.guarded)(() => eng.queue.installNodeExitFlush(), void 0);
|
|
285
|
+
const peer = peerOf(opts, c, ctx !== null);
|
|
219
286
|
let warnRule = null;
|
|
220
287
|
let vars = null;
|
|
221
288
|
let newSession = false;
|
|
289
|
+
let signalled = false;
|
|
222
290
|
const answer = await (0, import_core3.guardedAsync)(async () => {
|
|
223
291
|
eng.snap.ensureFresh(waitUntil);
|
|
224
292
|
const req2 = c.req.raw;
|
|
225
|
-
const { url, path, cf, ip, sid } = describeRequest(eng, req2);
|
|
293
|
+
const { url, path, cf, ip, sid } = describeRequest(eng, req2, peer);
|
|
226
294
|
const v = eng.snap.verdict({
|
|
227
295
|
ip,
|
|
228
296
|
path,
|
|
@@ -245,6 +313,7 @@ function camada(opts = {}) {
|
|
|
245
313
|
ship(eng, ev2, waitUntil);
|
|
246
314
|
const headers = {
|
|
247
315
|
"content-type": "text/plain",
|
|
316
|
+
"cache-control": "no-store",
|
|
248
317
|
"x-block-reason": String(v.reason ?? ""),
|
|
249
318
|
"x-block-version": v.version ?? ""
|
|
250
319
|
};
|
|
@@ -255,7 +324,8 @@ function camada(opts = {}) {
|
|
|
255
324
|
if (req2.method === "POST" && path === challengePath) return verify(eng, req2, ip, sid, challengePath, waitUntil);
|
|
256
325
|
const token = cookieValue(req2.headers.get("cookie") || "", import_core3.CHALLENGE_COOKIE);
|
|
257
326
|
if (v.challenge && !await eng.kit.tokenValid(ip, Date.now(), token)) {
|
|
258
|
-
return serve(eng, req2, ip, sid, challengePath, path + url.search, waitUntil);
|
|
327
|
+
if ((0, import_core3.wantsHtml)(req2.headers.get("accept"), req2.headers.get("sec-fetch-dest"))) return serve(eng, req2, ip, sid, challengePath, path + url.search, waitUntil);
|
|
328
|
+
signalled = true;
|
|
259
329
|
}
|
|
260
330
|
}
|
|
261
331
|
if (beaconEnabled(eng)) {
|
|
@@ -269,7 +339,24 @@ function camada(opts = {}) {
|
|
|
269
339
|
c.set(VAR, vars);
|
|
270
340
|
return null;
|
|
271
341
|
}, null);
|
|
272
|
-
if (answer)
|
|
342
|
+
if (answer) {
|
|
343
|
+
return (0, import_core3.guarded)(() => {
|
|
344
|
+
c.res = answer;
|
|
345
|
+
const h = c.res.headers;
|
|
346
|
+
if (h === answer.headers) return answer;
|
|
347
|
+
for (const [k, v] of answer.headers) if (k !== "set-cookie") h.set(k, v);
|
|
348
|
+
const cookies = answer.headers.getSetCookie();
|
|
349
|
+
if (cookies.length > 0) {
|
|
350
|
+
const name2 = (ck) => ck.slice(0, Math.max(ck.indexOf("="), 0)).trim();
|
|
351
|
+
const ours = new Set(cookies.map(name2));
|
|
352
|
+
const kept = h.getSetCookie().filter((ck) => !ours.has(name2(ck)));
|
|
353
|
+
h.delete("set-cookie");
|
|
354
|
+
for (const ck of [...kept, ...cookies]) h.append("set-cookie", ck);
|
|
355
|
+
}
|
|
356
|
+
h.delete("content-security-policy-report-only");
|
|
357
|
+
return c.res;
|
|
358
|
+
}, answer);
|
|
359
|
+
}
|
|
273
360
|
const req = c.req.raw;
|
|
274
361
|
const method = req.method;
|
|
275
362
|
const wsRequest = (0, import_core3.guarded)(() => req.headers.get("upgrade")?.toLowerCase() === "websocket", false);
|
|
@@ -279,7 +366,7 @@ function camada(opts = {}) {
|
|
|
279
366
|
return typeof s?.requestIP === "function" && s.requestIP(req) ? s : null;
|
|
280
367
|
}, null) : null;
|
|
281
368
|
const ev = (0, import_core3.guarded)(() => {
|
|
282
|
-
const { url, path, ip } = describeRequest(eng, req);
|
|
369
|
+
const { url, path, ip } = describeRequest(eng, req, peer);
|
|
283
370
|
return buildEvent(req, path, url.search, ip, vars?.sid ?? null, vars?.rid, newSession);
|
|
284
371
|
}, null);
|
|
285
372
|
await next();
|
|
@@ -288,8 +375,8 @@ function camada(opts = {}) {
|
|
|
288
375
|
const shipEvent = () => void (0, import_core3.guardedAsync)(async () => {
|
|
289
376
|
if (!ev) return;
|
|
290
377
|
const cfg = eng.snap.config;
|
|
291
|
-
if (
|
|
292
|
-
if (Math.random() >= (cfg?.sample ?? 1)) return;
|
|
378
|
+
if (signalled) ev.chs = 1;
|
|
379
|
+
else if ((cfg?.exclude || []).some((x) => ev.p.startsWith(x)) || Math.random() >= (cfg?.sample ?? 1)) return;
|
|
293
380
|
ev.ts = t0;
|
|
294
381
|
ev.st = status;
|
|
295
382
|
ev.dur = Math.max(0, Date.now() - t0);
|
|
@@ -316,13 +403,17 @@ function camada(opts = {}) {
|
|
|
316
403
|
if (method !== "HEAD") {
|
|
317
404
|
let out = c.res;
|
|
318
405
|
if (cookie) out = (0, import_fetch.withSetCookie)(out, cookie);
|
|
319
|
-
if (rid) out = (0, import_fetch.withRid)(out, { rid });
|
|
406
|
+
if (rid) out = (0, import_fetch.withRid)(out, { rid, signalled });
|
|
320
407
|
return replaceRes(c, out);
|
|
321
408
|
}
|
|
322
409
|
const head = new Response(null, c.res);
|
|
323
410
|
const headers = new Headers(head.headers);
|
|
324
411
|
if (cookie) headers.append("set-cookie", cookie);
|
|
325
412
|
if (rid) headers.set("x-rid", rid);
|
|
413
|
+
if (signalled) {
|
|
414
|
+
headers.set("x-camada-challenge", "1");
|
|
415
|
+
for (const n of NO_STORE_HEADERS) headers.set(n, "no-store");
|
|
416
|
+
}
|
|
326
417
|
replaceRes(c, new Response(null, { status: head.status, statusText: head.statusText, headers }));
|
|
327
418
|
}, void 0);
|
|
328
419
|
};
|
|
@@ -338,29 +429,22 @@ async function serve(eng, req, ip, sid, action, target, waitUntil) {
|
|
|
338
429
|
ev.st = 403;
|
|
339
430
|
ev.blk = "challenge";
|
|
340
431
|
ship(eng, ev, waitUntil);
|
|
341
|
-
if (!(0, import_core3.wantsHtml)(req.headers.get("accept"), req.headers.get("sec-fetch-dest"))) {
|
|
342
|
-
return new Response('{"error":"challenge_required"}', {
|
|
343
|
-
status: 403,
|
|
344
|
-
headers: { "content-type": "application/json", "cache-control": "no-store", "x-camada-challenge": "1" }
|
|
345
|
-
});
|
|
346
|
-
}
|
|
347
432
|
const html = (0, import_core3.challengePage)({ nonce: await eng.kit.nonce(ip, Date.now()), action, to: (0, import_core3.safeReturnTo)(target) });
|
|
348
433
|
return new Response(html, {
|
|
349
434
|
status: 403,
|
|
350
|
-
headers: { "content-type": "text/html; charset=utf-8", "cache-control": "no-store", "x-camada-challenge": "1" }
|
|
435
|
+
headers: { "content-type": "text/html; charset=utf-8", "cache-control": "no-store", "x-camada-challenge": "1", "content-security-policy": (0, import_core3.challengePageCsp)() }
|
|
351
436
|
});
|
|
352
437
|
}
|
|
353
438
|
async function verify(eng, req, ip, sid, action, waitUntil) {
|
|
354
439
|
const url = new URL(req.url);
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
if (body.length > BODY_MAX) return new Response(null, { status: 413 });
|
|
440
|
+
const body = await (0, import_core3.readBodyCapped)(req, BODY_MAX);
|
|
441
|
+
if (body === null) return new Response(null, { status: 413 });
|
|
358
442
|
const form = (0, import_core3.parseFormBody)(body);
|
|
359
443
|
const to = (0, import_core3.safeReturnTo)(form.to);
|
|
360
444
|
const now = Date.now();
|
|
361
445
|
if (!await eng.kit.verify(ip, now, form.nonce, form.solution)) {
|
|
362
446
|
const html = (0, import_core3.challengePage)({ nonce: await eng.kit.nonce(ip, now), action, to });
|
|
363
|
-
return new Response(html, { status: 403, headers: { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" } });
|
|
447
|
+
return new Response(html, { status: 403, headers: { "content-type": "text/html; charset=utf-8", "cache-control": "no-store", "content-security-policy": (0, import_core3.challengePageCsp)() } });
|
|
364
448
|
}
|
|
365
449
|
const ev = buildEvent(req, url.pathname, "", ip, sid);
|
|
366
450
|
ev.st = 200;
|
package/dist/index.cjs.map
CHANGED
|
@@ -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, 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.3\",\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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACQA,yBAAiB;AACjB,IAAAA,eAKO;AACP,mBAAuC;;;ACbvC,kBAA8F;AAyB9F,IAAM,UAAU,CAAC,MACf,OAAO,MAAM,eAAW,kCAAqB,CAAC,IAAI,KAAK;AAGlD,SAAS,WAAW,MAAyB,KAA6D;AAC/G,QAAM,MAAM,KAAK,OAAO,IAAI;AAC5B,QAAM,UAAM,sBAAS,GAAG;AACxB,QAAM,cAAc,KAAK,eAAe,IAAI;AAC5C,QAAM,YAAY,KAAK,aAAa,IAAI;AACxC,MAAI,CAAC,eAAe,CAAC,UAAW,QAAO;AACvC,QAAM,aAAa,KAAK,aAAa,IAAI,qBAAqB,yBAAyB,QAAQ,OAAO,EAAE;AACxG,SAAO;AAAA,IACL;AAAA,IAAa;AAAA,IACb,QAAQ,OAAO,GAAG,WAAW,IAAI,SAAS;AAAA,IAC1C;AAAA,IACA,aAAa,KAAK,eAAe,IAAI,uBAAuB,GAAG,SAAS;AAAA,IACxE,cAAc,QAAQ,KAAK,YAAY,SAAK,kCAAqB,IAAI,oBAAoB;AAAA,EAC3F;AACF;;;ACvCA,IAAAC,eAA4D;AAGrD,IAAM,MAAM;AAWZ,IAAM,gBAAgB,CAAC,MAAuB,EAAE,KAAK,QAAQ,WAAW;AAE/E,IAAM,WAAW,CAAC,MAAuC,EAAE,IAAI,GAAG;AAU3D,SAAS,MAAM,GAAY,OAAe,MAAyC;AACxF,QAAM,WAAO,sBAAQ,MAAM,SAAS,CAAC,GAAG,MAAS;AACjD,MAAI,CAAC,KAAM,QAAO,QAAQ,QAAQ;AAClC,QAAM,QAAI,2BAAa,YAAY;AACjC,UAAM,MAAM,MAAM,OAAO,UAAM,yBAAW,KAAK,MAAM,KAAK,IAAI,IAAI,WAAW,IAAI;AACjF,SAAK,KAAK,KAAK,EAAE,KAAK,uBAAU,IAAI,OAAO,KAAK,KAAK,KAAK,KAAK,KAAK,KAAK,KAAK,IAAI,KAAK,IAAI,IAAI,KAAK,IAAI,EAAE,GAAG,KAAK,SAAS;AAAA,EAC7H,GAAG,MAAS;AACZ,OAAK,UAAU,CAAC;AAChB,SAAO;AACT;AAGO,SAAS,UAAU,GAAoB;AAC5C,aAAO,sBAAQ,MAAM;AACnB,UAAM,OAAO,SAAS,CAAC;AACvB,QAAI,CAAC,QAAQ,CAAC,cAAc,KAAK,GAAG,EAAG,QAAO;AAC9C,WAAO,gBAAgB,KAAK,UAAU,MAAM,KAAK,GAAG;AAAA,EACtD,GAAG,EAAE;AACP;;;ACjDE,WAAQ;AACR,cAAW;;;ACCN,IAAM,SAAS,GAAG,IAAI,IAAI,OAAO;;;AJiBxC,IAAM,yBAAyB;AAC/B,IAAM,cAAc;AACpB,IAAM,UAAU;AAChB,IAAM,SAAS,KAAK;AACpB,IAAM,iBAAiB;AACvB,IAAM,kBAAkB;AACxB,IAAM,WAAW,IAAI;AACrB,IAAM,UAAU,IAAI,YAAY;AAehC,IAAM,UAAU,oBAAI,IAAoB;AAGjC,SAAS,cAAoB;AAClC,aAAW,KAAK,QAAQ,OAAO,GAAG;AAAE,MAAE,KAAK,KAAK;AAAG,MAAE,MAAM,KAAK;AAAA,EAAG;AACnE,UAAQ,MAAM;AAChB;AAEA,SAAS,OAAO,MAAyB,QAA2D;AAClG,QAAM,MAAM,WAAW,MAAM,MAAM;AACnC,MAAI,CAAC,KAAK;AACR,qCAAe,IAAI,MAAM,8CAAyC,CAAC;AACnE,WAAO;AAAA,EACT;AACA,QAAM,kBAAkB,KAAK,mBAAmB;AAChD,QAAM,KAAK,GAAG,IAAI,WAAW,IAAI,IAAI,SAAS,IAAI,IAAI,WAAW,IAAI,eAAe;AACpF,QAAM,SAAS,QAAQ,IAAI,EAAE;AAC7B,MAAI,OAAQ,QAAO;AACnB,QAAM,WAAW,KAAK,YAAY,EAAE,WAAW,KAAK,UAAU,IAAI,CAAC;AACnE,QAAM,SAAiB;AAAA,IACrB;AAAA,IACA,MAAM,IAAI,4BAAe;AAAA,MACvB,KAAK,IAAI;AAAA,MAAa,OAAO,IAAI;AAAA,MAAW,MAAM;AAAA,MAAQ,KAAK;AAAA,MAC/D;AAAA,MAAiB,GAAG;AAAA,IACtB,CAAC;AAAA,IACD,OAAO,IAAI,wBAAW,EAAE,KAAK,IAAI,WAAW,OAAO,IAAI,aAAa,KAAK,QAAQ,GAAG,SAAS,CAAC;AAAA,IAC9F,SAAK,mCAAqB,EAAE,QAAQ,IAAI,OAAO,CAAC;AAAA,EAClD;AACA,UAAQ,IAAI,IAAI,MAAM;AACtB,SAAO;AACT;AAEA,IAAM,cAAc,CAAC,QAAgBC,UAAgC;AACnE,QAAM,MAAM,OAAO;AACnB,QAAM,IAAI,IAAI,QAAQ,OAAOA,QAAO,GAAG;AACvC,MAAI,MAAM,GAAI,QAAO;AACrB,QAAM,QAAQ,IAAIA,MAAK,SAAS;AAChC,QAAM,IAAI,IAAI,QAAQ,KAAK,KAAK;AAChC,SAAO,IAAI,MAAM,OAAO,MAAM,KAAK,SAAY,CAAC;AAClD;AAKA,IAAM,OAAO,CAAC,QAA4B,IAAmC,MAAM,CAAC;AAEpF,IAAM,eAAe,CAAC,MACpB,EAAE,IAAI,gBAAgB,EAAE,KAAK,QAAQ,iBAAiB;AAMxD,IAAM,WAAW,CAAC,GAAW,QAAgC;AAC3D,QAAM,KAAM,IAAmC;AAC/C,QAAM,SAAS,KAAK,IAAI,QAAQ,IAAI,kBAAkB,IAAI;AAC1D,SAAO,cAAU,8BAAgB,MAAM,IAAI,QAAQ,IAAI,iBAAiB,GAAG,aAAa,CAAC,CAAC;AAC5F;AAUA,SAAS,gBAAgB,GAAW,KAA4B;AAC9D,QAAM,MAAM,IAAI,IAAI,IAAI,GAAG;AAC3B,QAAM,UAAU,IAAI,QAAQ,IAAI,QAAQ,KAAK;AAC7C,SAAO,EAAE,KAAK,MAAM,IAAI,UAAU,IAAI,KAAK,GAAG,GAAG,IAAI,SAAS,GAAG,GAAG,GAAG,KAAK,YAAY,SAAS,cAAc,EAAE;AACnH;AAEA,SAAS,WACP,KAAc,MAAc,OAAe,IAAmB,KAC9D,MAAc,OAAO,WAAW,GAAG,aAAa,OACrC;AACX,QAAM,UAAmC,CAAC;AAC1C,MAAI,QAAQ,QAAQ,CAAC,GAAG,MAAM,QAAQ,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC;AAClD,QAAM,KAAK,KAAK,GAAG;AACnB,QAAM,SAAK;AAAA,IACT;AAAA,MACE,QAAQ,IAAI;AAAA,MAAQ,MAAM,IAAI,QAAQ,IAAI,MAAM,KAAK,IAAI,IAAI,IAAI,GAAG,EAAE;AAAA,MAAM;AAAA,MAAM;AAAA,MAAO;AAAA,MAAS;AAAA;AAAA;AAAA;AAAA,MAIlG,aAAa,GAAG,eAAe,GAAG,aAAa,QAAQ,YAAY,EAAE,IAAI;AAAA,IAC3E;AAAA,IACA,EAAE,KAAK,uBAAU,KAAK,KAAK,WAAW;AAAA,EACxC;AACA,MAAI,GAAG,IAAK,IAAG,MAAM,GAAG;AACxB,MAAI,GAAG,QAAS,IAAG,KAAK,GAAG;AAC3B,MAAI,GAAG,wBAAyB,IAAG,OAAO,GAAG;AAC7C,SAAO;AACT;AAGO,SAAS,KAAK,GAAW,IAAa,WAA4B;AACvE,IAAE,MAAM,KAAK,EAAE;AACf,IAAE,MAAM,MAAM,SAAS;AACzB;AAGA,eAAe,YAAY,KAAa,KAAc,IAAmB,WAAyC;AAChH,QAAM,YAAY,MAAM,IAAI,SAAS,MAAM,EAAE,QAAQ,KAAK,SAAS,EAAE,iBAAiB,WAAW,EAAE,CAAC;AACpG,MAAI,OAAO,IAAI,QAAQ,IAAI,gBAAgB,CAAC,IAAI,OAAQ,QAAO,IAAI,SAAS,MAAM,EAAE,QAAQ,IAAI,CAAC;AACjG,QAAM,OAAO,MAAM,IAAI,KAAK;AAC5B,MAAI,QAAQ,OAAO,IAAI,EAAE,aAAa,OAAQ,QAAO,IAAI,SAAS,MAAM,EAAE,QAAQ,IAAI,CAAC;AACvF,MAAI;AACJ,MAAI;AAAE,aAAS,KAAK,MAAM,IAAI;AAAA,EAAG,QAAQ;AAAE,WAAO,UAAU;AAAA,EAAG;AAC/D,MAAI,CAAC,UAAU,OAAO,WAAW,YAAY,MAAM,QAAQ,MAAM,EAAG,QAAO,UAAU;AAErF,OAAK,KAAK,EAAE,GAAI,QAAoC,KAAK,GAAG,IAAI,KAAK,sBAAS,GAAG,SAAS;AAC1F,SAAO,UAAU;AACnB;AAEO,SAAS,OAAO,OAA0B,CAAC,GAAsB;AACtE,QAAM,gBAAgB,KAAK,iBAAiB;AAC5C,QAAM,aAAa,KAAK,cAAc;AACtC,QAAM,SAAS,KAAK,UAAU;AAE9B,SAAO,eAAe,WAAW,GAAY,MAAsC;AACjF,UAAM,KAAK,KAAK,IAAI;AAGpB,UAAM,UAAM,sBAAQ,OAAO,EAAE,GAAI,EAAE,KAAwD,GAAG,KAAK,IAAI,IAAI,CAAC,CAAuC;AACnJ,UAAM,UAAM,sBAAQ,MAAO,IAAI,oBAAoB,MAAM,OAAO,OAAO,MAAM,GAAG,GAAI,IAAI;AACxF,QAAI,CAAC,IAAK,QAAO,KAAK;AAEtB,UAAM,cAAc,KAAK,cAAc,SAAS,IAAI,qBAAqB;AAEzE,UAAM,YAAuB,CAAC,MAAM;AAClC,UAAI;AAAE,UAAE,cAAc,UAAU,CAAC;AAAA,MAAG,QAAQ;AAAA,MAAqD;AAAA,IACnG;AAIA,QAAI,WAA0B;AAG9B,QAAI,OAA0B;AAC9B,QAAI,aAAa;AAEjB,UAAM,SAAS,UAAM,2BAAa,YAAsC;AACtE,UAAI,KAAK,YAAY,SAAS;AAE9B,YAAMC,OAAM,EAAE,IAAI;AAClB,YAAM,EAAE,KAAK,MAAM,IAAI,IAAI,IAAI,IAAI,gBAAgB,KAAKA,IAAG;AAE3D,YAAM,IAAI,IAAI,KAAK,QAAQ;AAAA,QACzB;AAAA,QAAI;AAAA,QAAM,IAAIA,KAAI,QAAQ,IAAI,YAAY;AAAA;AAAA;AAAA;AAAA;AAAA,QAI1C,QAAQ,CAAC,MAAMA,KAAI,QAAQ,IAAI,CAAC;AAAA,QAChC,KAAK,GAAG,OAAO;AAAA,QAAM,SAAS,GAAG,WAAW;AAAA,QAAM,MAAM,GAAG,2BAA2B;AAAA,MACxF,CAAC;AACD,iBAAW,EAAE,OAAO,EAAE,QAAQ,OAAO;AAErC,UAAI,EAAE,OAAO;AACX,cAAMC,MAAK,WAAWD,MAAK,MAAM,IAAI,QAAQ,IAAI,GAAG;AACpD,QAAAC,IAAG,KAAK;AACR,QAAAA,IAAG,MAAM,EAAE;AACX,YAAI,EAAE,KAAM,CAAAA,IAAG,KAAK,EAAE;AACtB,aAAK,KAAKA,KAAI,SAAS;AACvB,cAAM,UAAkC;AAAA,UACtC,gBAAgB;AAAA,UAAc,kBAAkB,OAAO,EAAE,UAAU,EAAE;AAAA,UAAG,mBAAmB,EAAE,WAAW;AAAA,QAC1G;AACA,YAAI,EAAE,KAAM,SAAQ,cAAc,IAAI,EAAE;AACxC,eAAO,IAAI,SAAS,aAAa,EAAE,QAAQ,KAAK,QAAQ,CAAC;AAAA,MAC3D;AAIA,UAAI,eAAe,IAAI;AAErB,YAAID,KAAI,WAAW,UAAU,SAAS,cAAe,QAAO,OAAO,KAAKA,MAAK,IAAI,KAAK,eAAe,SAAS;AAC9G,cAAM,QAAQ,YAAYA,KAAI,QAAQ,IAAI,QAAQ,KAAK,IAAI,6BAAgB;AAC3E,YAAI,EAAE,aAAa,CAAE,MAAM,IAAI,IAAI,WAAW,IAAI,KAAK,IAAI,GAAG,KAAK,GAAI;AACrE,iBAAO,MAAM,KAAKA,MAAK,IAAI,KAAK,eAAe,OAAO,IAAI,QAAQ,SAAS;AAAA,QAC7E;AAAA,MACF;AAIA,UAAI,cAAc,GAAG,GAAG;AACtB,YAAIA,KAAI,WAAW,SAAS,SAAS,YAAY;AAC/C,iBAAO,IAAI,SAAS,mBAAAE,SAAM,EAAE,QAAQ,KAAK,SAAS,EAAE,gBAAgB,0BAA0B,iBAAiB,uBAAuB,EAAE,CAAC;AAAA,QAC3I;AACA,YAAIF,KAAI,WAAW,UAAU,SAAS,OAAQ,QAAO,YAAY,KAAKA,MAAK,IAAI,SAAS;AAAA,MAC1F;AAKA,mBAAa,CAAC;AACd,aAAO,EAAE,KAAK,KAAK,OAAO,WAAW,GAAG,KAAK,OAAO,OAAO,WAAW,GAAG,IAAI,WAAW,WAAW;AACnG,QAAE,IAAI,KAAK,IAAI;AACf,aAAO;AAAA,IACT,GAAG,IAAI;AAEP,QAAI,OAAQ,QAAO;AAKnB,UAAM,MAAM,EAAE,IAAI;AAClB,UAAM,SAAS,IAAI;AACnB,UAAM,gBAAY,sBAAQ,MAAM,IAAI,QAAQ,IAAI,SAAS,GAAG,YAAY,MAAM,aAAa,KAAK;AAIhG,UAAM,MAAM,gBAAY,sBAAQ,MAAM;AACpC,YAAM,IAAI,EAAE;AACZ,YAAM,IAAK,KAAK,YAAY,IAAI,EAAE,SAAS;AAC3C,aAAO,OAAO,GAAG,cAAc,cAAc,EAAE,UAAU,GAAG,IAAI,IAAI;AAAA,IACtE,GAAG,IAAI,IAAI;AACX,UAAM,SAAK,sBAAQ,MAAM;AACvB,YAAM,EAAE,KAAK,MAAM,GAAG,IAAI,gBAAgB,KAAK,GAAG;AAClD,aAAO,WAAW,KAAK,MAAM,IAAI,QAAQ,IAAI,MAAM,OAAO,MAAM,MAAM,KAAK,UAAU;AAAA,IACvF,GAAG,IAAI;AAEP,UAAM,KAAK;AAUX,UAAM,WAAW,aAAa,EAAE,IAAI,WAAW,OAAO,CAAC,EAAE,IAAI,QAAQ,IAAI,cAAc,MACjF,UAAM,sBAAQ,MAAM,IAAI,UAAU,GAAG,MAAM,MAAM,KAAK,IAAI;AAChE,UAAM,SAAS,WAAW,MAAM,EAAE,IAAI;AACtC,UAAM,YAAY,MAAM,SAAK,2BAAa,YAAY;AACpD,UAAI,CAAC,GAAI;AACT,YAAM,MAAM,IAAI,KAAK;AACrB,WAAK,KAAK,WAAW,CAAC,GAAG,KAAK,CAAC,MAAO,GAAG,EAAa,WAAW,CAAC,CAAC,EAAG;AACtE,UAAI,KAAK,OAAO,MAAM,KAAK,UAAU,GAAI;AACzC,SAAG,KAAK;AACR,SAAG,KAAK;AACR,SAAG,MAAM,KAAK,IAAI,GAAG,KAAK,IAAI,IAAI,EAAE;AACpC,UAAI,SAAU,IAAG,MAAM;AACvB,WAAK,KAAK,IAAI,SAAS;AAAA,IACzB,GAAG,MAAS;AACZ,QAAI;AAIF,iBAAW,OAAG,yBAAW,EAAE,KAAK,WAAW,EAAE,QAAQ,WAAW,IAAI,SAAS;AAAE,eAAO,EAAE,IAAI,IAAI;AAAA,MAAQ,EAAE,CAAC,CAAC;AAAA,IAC9G,SAAS,KAAK;AACZ,uCAAe,GAAG;AAClB,gBAAU;AAAA,IACZ;AAYA,8BAAQ,MAAM;AACZ,UAAI,EAAE,IAAI,WAAW,IAAK;AAC1B,YAAM,MAAM,IAAI;AAChB,UAAI;AACJ,UAAI,QAAQ,YAAY;AACtB,cAAM,SAAS,IAAI,IAAI,EAAE,IAAI,IAAI,GAAG,EAAE,aAAa,WAAW,aAAa;AAC3E,iBAAS,GAAG,cAAc,IAAI,KAAK,GAAG,qBAAqB,eAAe,2BAA2B,MAAM;AAAA,MAC7G;AACA,UAAI,CAAC,OAAO,CAAC,OAAQ;AACrB,UAAI,WAAW,QAAQ;AACrB,YAAI,MAAM,EAAE;AACZ,YAAI,OAAQ,WAAM,4BAAc,KAAK,MAAM;AAC3C,YAAI,IAAK,WAAM,sBAAQ,KAAK,EAAE,IAAI,CAAC;AACnC,eAAO,WAAW,GAAG,GAAG;AAAA,MAC1B;AACA,YAAM,OAAO,IAAI,SAAS,MAAM,EAAE,GAAG;AACrC,YAAM,UAAU,IAAI,QAAQ,KAAK,OAAO;AACxC,UAAI,OAAQ,SAAQ,OAAO,cAAc,MAAM;AAC/C,UAAI,IAAK,SAAQ,IAAI,SAAS,GAAG;AACjC,iBAAW,GAAG,IAAI,SAAS,MAAM,EAAE,QAAQ,KAAK,QAAQ,YAAY,KAAK,YAAY,QAAQ,CAAC,CAAC;AAAA,IACjG,GAAG,MAAS;AAAA,EACd;AACF;AAOA,SAAS,WAAW,GAAY,KAAqB;AACnD,MAAI,QAAQ,EAAE,IAAK;AACnB,IAAE,MAAM;AACR,IAAE,MAAM;AACV;AAGA,eAAe,MAAM,KAAa,KAAc,IAAY,KAAoB,QAAgB,QAAgB,WAAyC;AACvJ,QAAM,MAAM,IAAI,IAAI,IAAI,GAAG;AAC3B,QAAM,KAAK,WAAW,KAAK,IAAI,UAAU,IAAI,QAAQ,IAAI,GAAG;AAC5D,KAAG,KAAK;AACR,KAAG,MAAM;AACT,OAAK,KAAK,IAAI,SAAS;AAEvB,MAAI,KAAC,wBAAU,IAAI,QAAQ,IAAI,QAAQ,GAAG,IAAI,QAAQ,IAAI,gBAAgB,CAAC,GAAG;AAC5E,WAAO,IAAI,SAAS,kCAAkC;AAAA,MACpD,QAAQ;AAAA,MACR,SAAS,EAAE,gBAAgB,oBAAoB,iBAAiB,YAAY,sBAAsB,IAAI;AAAA,IACxG,CAAC;AAAA,EACH;AACA,QAAM,WAAO,4BAAc,EAAE,OAAO,MAAM,IAAI,IAAI,MAAM,IAAI,KAAK,IAAI,CAAC,GAAG,QAAQ,QAAI,2BAAa,MAAM,EAAE,CAAC;AAC3G,SAAO,IAAI,SAAS,MAAM;AAAA,IACxB,QAAQ;AAAA,IACR,SAAS,EAAE,gBAAgB,4BAA4B,iBAAiB,YAAY,sBAAsB,IAAI;AAAA,EAChH,CAAC;AACH;AAGA,eAAe,OAAO,KAAa,KAAc,IAAY,KAAoB,QAAgB,WAAyC;AACxI,QAAM,MAAM,IAAI,IAAI,IAAI,GAAG;AAG3B,MAAI,OAAO,IAAI,QAAQ,IAAI,gBAAgB,CAAC,IAAI,SAAU,QAAO,IAAI,SAAS,MAAM,EAAE,QAAQ,IAAI,CAAC;AACnG,QAAM,OAAO,MAAM,IAAI,KAAK;AAC5B,MAAI,KAAK,SAAS,SAAU,QAAO,IAAI,SAAS,MAAM,EAAE,QAAQ,IAAI,CAAC;AACrE,QAAM,WAAO,4BAAc,IAAI;AAC/B,QAAM,SAAK,2BAAa,KAAK,EAAE;AAC/B,QAAM,MAAM,KAAK,IAAI;AACrB,MAAI,CAAE,MAAM,IAAI,IAAI,OAAO,IAAI,KAAK,KAAK,OAAO,KAAK,QAAQ,GAAI;AAC/D,UAAM,WAAO,4BAAc,EAAE,OAAO,MAAM,IAAI,IAAI,MAAM,IAAI,GAAG,GAAG,QAAQ,GAAG,CAAC;AAC9E,WAAO,IAAI,SAAS,MAAM,EAAE,QAAQ,KAAK,SAAS,EAAE,gBAAgB,4BAA4B,iBAAiB,WAAW,EAAE,CAAC;AAAA,EACjI;AACA,QAAM,KAAK,WAAW,KAAK,IAAI,UAAU,IAAI,IAAI,GAAG;AACpD,KAAG,KAAK;AACR,KAAG,KAAK;AACR,OAAK,KAAK,IAAI,SAAS;AACvB,SAAO,IAAI,SAAS,MAAM;AAAA,IACxB,QAAQ;AAAA,IACR,SAAS;AAAA,MACP,UAAU;AAAA,MACV,kBAAc,8BAAgB,MAAM,IAAI,IAAI,MAAM,IAAI,GAAG,GAAG,IAAI,aAAa,QAAQ;AAAA,MACrF,iBAAiB;AAAA,IACnB;AAAA,EACF,CAAC;AACH;","names":["import_core","import_core","name","req","ev","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, challengePageCsp, challengeCookie, safeReturnTo, wantsHtml, parseFormBody, readBodyCapped,\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, hostVar, 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\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/** Off Workers in a long-lived process: nothing to hold open, and the queue timer batches the rows. */\nconst noWait: WaitUntil = () => {};\n/** Off Workers in a function that freezes after the response: nothing to hold open either, but\n * every row flushes as it is shipped — a row left for the timer would never go out. */\nconst flushNow: WaitUntil = () => {};\n\n/** The host's request-scoped waitUntil where Vercel provides one (`@vercel/functions` reads the same\n * global; @camada/next's wait-until.ts too): hono/vercel passes no execution context, so without it\n * the function may freeze before a flush started at the response is done. Null elsewhere. */\nfunction requestContextWaitUntil(): WaitUntil | null {\n return guarded(() => {\n const g = globalThis as Record<symbol, { get?: () => { waitUntil?: WaitUntil } | undefined } | undefined>;\n const ctx = g[Symbol.for('@vercel/request-context')]?.get?.();\n if (typeof ctx?.waitUntil !== 'function') return null;\n return (p: Promise<unknown>) => { try { ctx.waitUntil!(p); } catch { /* a context that refuses costs only the hold */ } };\n }, null);\n}\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/** Per-client signal: no shared cache may keep the copy. CDNs read their own header ahead of cache-control. */\nconst NO_STORE_HEADERS = ['cache-control', 'cdn-cache-control', 'surrogate-control', 'cloudflare-cdn-cache-control', 'vercel-cdn-cache-control', 'netlify-cdn-cache-control'];\n\n/** 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\nconst runtime = globalThis as { Bun?: unknown; Deno?: unknown; navigator?: { userAgent?: string }; process?: { versions?: { node?: string } } };\nconst onWorkers = (): boolean => runtime.navigator?.userAgent === 'Cloudflare-Workers';\n\n/** The long-lived server Hono runs under, recognised from the object it hands the app as `c.env`\n * (no static import of hono/bun etc., so a Workers bundle pulls none of them), and a reader for\n * the socket peer it vouches for. Each shape is read only on its own runtime, so a Workers\n * binding that happens to be named `server` or `incoming` (a JS-RPC stub answers any property\n * with a callable) is never called:\n * @hono/node-server `{ incoming }` → `incoming.socket.remoteAddress` (Node or Bun, not Workers);\n * Bun `Bun.serve({ fetch: app.fetch })` → the server (or hono/bun's `{ server }`) →\n * `requestIP(req).address`; Deno `Deno.serve(app.fetch)` → `info.remoteAddr.hostname`.\n * Null for anything else: Workers bindings, hono/vercel, netlify, aws-lambda, lambda-edge. */\nfunction serverShape(env: unknown): ((req: Request) => string | null) | null {\n if (!env || typeof env !== 'object' || onWorkers()) return null;\n const e = env as { incoming?: { socket?: { remoteAddress?: unknown } }; server?: unknown; remoteAddr?: { hostname?: unknown } };\n const str = (v: unknown): string | null => (typeof v === 'string' && v ? v : null);\n if (runtime.process?.versions?.node && e.incoming && typeof e.incoming === 'object') return () => str(e.incoming?.socket?.remoteAddress);\n if (runtime.Bun !== undefined) {\n const bun = ('server' in e ? e.server : e) as BunServer | undefined; // hono/bun's getBunServer\n if (typeof bun?.requestIP === 'function') return (req) => str((bun.requestIP(req) as { address?: unknown } | null)?.address);\n }\n if (runtime.Deno !== undefined && e.remoteAddr && typeof e.remoteAddr === 'object') return () => str(e.remoteAddr?.hostname);\n return null;\n}\n\n/** Serverless hosts whose functions freeze after the response, whatever object they hand `c.env`. */\nconst SERVERLESS_HOSTS = ['VERCEL', 'AWS_LAMBDA_FUNCTION_NAME', 'NETLIFY', 'DENO_DEPLOYMENT_ID'];\n\n/** Whether rows wait for the queue timer (a process that outlives the response) rather than\n * each flushing as it is shipped. Only where there is no execution context to hold open. */\nfunction batches(opts: CamadaHonoOptions, env: Record<string, string | undefined>, runtimeEnv: unknown): boolean {\n if (hostVar('CAMADA_SERVERLESS', env) === '1') return false;\n if (opts.mode) return opts.mode === 'timer';\n if (SERVERLESS_HOSTS.some((k) => hostVar(k, env))) return false;\n return serverShape(runtimeEnv) !== null;\n}\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: the\n * socket peer (the app's `peer(c)`, else the runtime's own) and X-Forwarded-For go through the\n * trusted-proxy rules instead, as on every other adapter. */\nconst clientIp = (e: Engine, req: Request, peer: string | null): string | null => {\n const cf = (req as Request & { cf?: unknown }).cf;\n if (cf) return req.headers.get('cf-connecting-ip') || resolveClientIp(null, req.headers.get('x-forwarded-for'), trustedProxy(e));\n return resolveClientIp(peer, req.headers.get('x-forwarded-for'), trustedProxy(e));\n};\n\n/** The peer for a request off Workers; never consulted where `request.cf` or an execution\n * context exists (a Workers env holds bindings, not a server). Never throws. */\nconst peerOf = (opts: CamadaHonoOptions, c: Context, hasCtx: boolean): string | null => guarded(() => {\n if (hasCtx || (c.req.raw as Request & { cf?: unknown }).cf) return null;\n if (opts.peer) return opts.peer(c) || null;\n return serverShape(c.env)?.(c.req.raw) ?? null;\n}, null);\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, peer: string | null): 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, peer), 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` (Workers, or Vercel's request context) or at once (`flushNow`, serverless without an\n * execution context): the function may freeze right after the response, so each row goes out\n * with it. In a long-lived process (`noWait`) the queue's timer batches the rows — one POST /e per\n * flush, not one per request (core's 7u3r rule). The one rule every row (wire event, beacon,\n * track()) follows. */\nexport function ship(e: Engine, ev: unknown, waitUntil: WaitUntil): void {\n e.queue.push(ev);\n if (waitUntil !== noWait) 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 const body = await readBodyCapped(req, FP_MAX); // streamed: a chunked body cannot buffer past the cap\n if (body === null) 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, rts: Date.now() }, 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 // Hono's executionCtx getter throws where the runtime passed none (node-server, Bun, Deno).\n let ctx: Context['executionCtx'] | null = null;\n try { ctx = typeof c.executionCtx?.waitUntil === 'function' ? c.executionCtx : null; } catch { /* expected off Workers: not an error to log */ }\n const batch = !ctx && guarded(() => batches(opts, env, c.env), false);\n const waitUntil: WaitUntil = ctx\n ? (p) => { try { ctx.waitUntil(p); } catch { /* a context that refuses: the row ships with the next flush */ } }\n : batch ? noWait : requestContextWaitUntil() ?? flushNow;\n // A batching process waits on the queue timer, so drain what is queued on exit (core's timer mode does the same).\n if (batch) guarded(() => eng.queue.installNodeExitFlush(), undefined);\n const peer = peerOf(opts, c, ctx !== null);\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 // A non-HTML request that passed under a challenge: x-camada-challenge: 1 and chs: 1 (82ar).\n let signalled = 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, peer);\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', 'cache-control': 'no-store', '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 // Only a page can be challenged; any other request passes to the app, signalled.\n if (wantsHtml(req.headers.get('accept'), req.headers.get('sec-fetch-dest'))) return serve(eng, req, ip, sid, challengePath, path + url.search, waitUntil);\n signalled = true;\n }\n }\n\n // The beacon endpoints come after enforcement (a blocked client, or one served the challenge\n // page, gets neither the script nor a relay; a challenged non-HTML client is only signalled\n // and still reaches them) 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) {\n // Hono merges the headers of a c.res an app middleware in front of camada touched (c.res.headers,\n // or c.header() once anything read c.res) over the response returned here: an upstream\n // `cache-control: public` would make the IP-bound challenge cacheable, its CSP would block the\n // solver, its Set-Cookie of the same name would replace the `_cch` one. So set c.res to the answer, then put every\n // header of the answer back on top and drop a report-only policy (camada-all-mwpo, R7-1).\n return guarded(() => {\n c.res = answer;\n const h = c.res.headers;\n if (h === answer.headers) return answer; // nothing was merged in\n for (const [k, v] of answer.headers) if (k !== 'set-cookie') h.set(k, v);\n const cookies = answer.headers.getSetCookie();\n if (cookies.length > 0) {\n // The answer's cookie wins only on a name clash; the app's own (a session, say) are kept (RF-2).\n const name = (ck: string) => ck.slice(0, Math.max(ck.indexOf('='), 0)).trim();\n const ours = new Set(cookies.map(name));\n const kept = h.getSetCookie().filter((ck) => !ours.has(name(ck)));\n h.delete('set-cookie');\n for (const ck of [...kept, ...cookies]) h.append('set-cookie', ck);\n }\n h.delete('content-security-policy-report-only');\n return c.res;\n }, answer);\n }\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, peer);\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 // a signalled row ships whatever sample and exclude say, like the 403 row it replaces\n if (signalled) ev.chs = 1;\n else if ((cfg?.exclude || []).some((x) => (ev.p as string).startsWith(x)) || 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; // signalled implies a captured request, so rid is set\n if (method !== 'HEAD') {\n let out = c.res;\n if (cookie) out = withSetCookie(out, cookie);\n if (rid) out = withRid(out, { rid, signalled });\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 if (signalled) { headers.set('x-camada-challenge', '1'); for (const n of NO_STORE_HEADERS) headers.set(n, 'no-store'); }\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), 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 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', 'content-security-policy': challengePageCsp() },\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 const body = await readBodyCapped(req, BODY_MAX);\n if (body === null) 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', 'content-security-policy': challengePageCsp() } });\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 type { Context } from 'hono';\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 /** Off Workers: the socket peer for this request, when the runtime object on `c.env` is not\n * one camada reads itself (node-server, Bun, Deno) — e.g. `(c) => getConnInfo(c).remote.address`.\n * Goes through the trusted-proxy rules with X-Forwarded-For. Never consulted on Workers. */\n peer?: (c: Context) => string | null | undefined;\n /** How rows reach ingest where there is no execution context (always per row through waitUntil\n * where there is one). 'timer': batch on the queue's 15 s timer and drain on exit — for a\n * long-lived server. 'lazy': flush every row as it is shipped — for a function that freezes after\n * the response. Unset: 'timer' only on a recognised node-server / Bun / Deno server outside\n * Vercel, AWS Lambda, Netlify and Deno Deploy, else 'lazy'. CAMADA_SERVERLESS=1 forces 'lazy'. */\n mode?: 'timer' | 'lazy';\n}\n\n/** A host variable: the merged env first (c.env + opts.env), then the process's own env\n * (process.env, or Deno.env where the permission is already granted — never a prompt; a Deno whose\n * permission API has no querySync is not read). A Deno without the permission API (Deno Deploy)\n * has no prompt either, so its env is read directly:\n * that is where DENO_DEPLOYMENT_ID tells a short-lived isolate apart from a long-lived server. */\nexport function hostVar(name: string, env: Record<string, string | undefined>): string | undefined {\n if (env[name] !== undefined) return env[name];\n const g = globalThis as {\n Deno?: { env?: { get?: (k: string) => string | undefined }; permissions?: { querySync?: (d: { name: string; variable: string }) => { state: string } } };\n process?: { env?: Record<string, string | undefined> };\n };\n try {\n if (g.Deno) {\n const perms = g.Deno.permissions;\n if (perms) { // a permission API without querySync (Deno <1.37) cannot be asked silently: don't read (R7-3)\n if (typeof perms.querySync !== 'function' || perms.querySync({ name: 'env', variable: name }).state !== 'granted') return undefined;\n }\n return g.Deno.env?.get?.(name); // a throw (NotCapable) reads as unset, below\n }\n return g.process?.env?.[name];\n } catch { return undefined; }\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 // On Vercel the platform rewrites X-Forwarded-For, so its rightmost entry is the client (as @camada/next).\n trustedProxy: asProxy(opts.trustedProxy) ?? parseTrustedProxyEnv(env.CAMADA_TRUSTED_PROXY)\n ?? (hostVar('VERCEL', env) ? { mode: 'vercel' } : null),\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.4.0\",\n \"license\": \"MIT\",\n \"type\": \"module\",\n \"engines\": {\n \"node\": \">=20\"\n },\n \"files\": [\n \"dist\",\n \"README.md\",\n \"LICENSE\"\n ],\n \"scripts\": {\n \"build\": \"tsup\",\n \"dev\": \"tsup --watch\",\n \"test\": \"vitest run\",\n \"check\": \"tsc --noEmit\"\n },\n \"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 \"import\": {\n \"types\": \"./dist/index.d.ts\",\n \"default\": \"./dist/index.js\"\n },\n \"require\": {\n \"types\": \"./dist/index.d.cts\",\n \"default\": \"./dist/index.cjs\"\n }\n }\n },\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","name","import_core","name","req","ev","iife"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Context, MiddlewareHandler } from 'hono';
|
|
2
2
|
import { TrustedProxyConfig, SnapshotVersion } from '@camada/core';
|
|
3
3
|
|
|
4
4
|
interface ResolvedEnv {
|
|
@@ -21,6 +21,16 @@ interface CamadaHonoOptions {
|
|
|
21
21
|
fpPath?: string;
|
|
22
22
|
env?: Record<string, string | undefined>;
|
|
23
23
|
fetchImpl?: typeof fetch;
|
|
24
|
+
/** Off Workers: the socket peer for this request, when the runtime object on `c.env` is not
|
|
25
|
+
* one camada reads itself (node-server, Bun, Deno) — e.g. `(c) => getConnInfo(c).remote.address`.
|
|
26
|
+
* Goes through the trusted-proxy rules with X-Forwarded-For. Never consulted on Workers. */
|
|
27
|
+
peer?: (c: Context) => string | null | undefined;
|
|
28
|
+
/** How rows reach ingest where there is no execution context (always per row through waitUntil
|
|
29
|
+
* where there is one). 'timer': batch on the queue's 15 s timer and drain on exit — for a
|
|
30
|
+
* long-lived server. 'lazy': flush every row as it is shipped — for a function that freezes after
|
|
31
|
+
* the response. Unset: 'timer' only on a recognised node-server / Bun / Deno server outside
|
|
32
|
+
* Vercel, AWS Lambda, Netlify and Deno Deploy, else 'lazy'. CAMADA_SERVERLESS=1 forces 'lazy'. */
|
|
33
|
+
mode?: 'timer' | 'lazy';
|
|
24
34
|
}
|
|
25
35
|
/** Returns null (the middleware stays inert, one log line) rather than throwing on bad config. */
|
|
26
36
|
declare function resolveEnv(opts: CamadaHonoOptions, env: Record<string, string | undefined>): ResolvedEnv | null;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Context, MiddlewareHandler } from 'hono';
|
|
2
2
|
import { TrustedProxyConfig, SnapshotVersion } from '@camada/core';
|
|
3
3
|
|
|
4
4
|
interface ResolvedEnv {
|
|
@@ -21,6 +21,16 @@ interface CamadaHonoOptions {
|
|
|
21
21
|
fpPath?: string;
|
|
22
22
|
env?: Record<string, string | undefined>;
|
|
23
23
|
fetchImpl?: typeof fetch;
|
|
24
|
+
/** Off Workers: the socket peer for this request, when the runtime object on `c.env` is not
|
|
25
|
+
* one camada reads itself (node-server, Bun, Deno) — e.g. `(c) => getConnInfo(c).remote.address`.
|
|
26
|
+
* Goes through the trusted-proxy rules with X-Forwarded-For. Never consulted on Workers. */
|
|
27
|
+
peer?: (c: Context) => string | null | undefined;
|
|
28
|
+
/** How rows reach ingest where there is no execution context (always per row through waitUntil
|
|
29
|
+
* where there is one). 'timer': batch on the queue's 15 s timer and drain on exit — for a
|
|
30
|
+
* long-lived server. 'lazy': flush every row as it is shipped — for a function that freezes after
|
|
31
|
+
* the response. Unset: 'timer' only on a recognised node-server / Bun / Deno server outside
|
|
32
|
+
* Vercel, AWS Lambda, Netlify and Deno Deploy, else 'lazy'. CAMADA_SERVERLESS=1 forces 'lazy'. */
|
|
33
|
+
mode?: 'timer' | 'lazy';
|
|
24
34
|
}
|
|
25
35
|
/** Returns null (the middleware stays inert, one log line) rather than throwing on bad config. */
|
|
26
36
|
declare function resolveEnv(opts: CamadaHonoOptions, env: Record<string, string | undefined>): ResolvedEnv | null;
|
package/dist/index.js
CHANGED
|
@@ -11,10 +11,12 @@ import {
|
|
|
11
11
|
guarded as guarded2,
|
|
12
12
|
createChallengeAsync,
|
|
13
13
|
challengePage,
|
|
14
|
+
challengePageCsp,
|
|
14
15
|
challengeCookie,
|
|
15
16
|
safeReturnTo,
|
|
16
17
|
wantsHtml,
|
|
17
18
|
parseFormBody,
|
|
19
|
+
readBodyCapped,
|
|
18
20
|
CHALLENGE_COOKIE,
|
|
19
21
|
DEFAULT_SNAPSHOT_VERSION,
|
|
20
22
|
TAP_HONO as TAP_HONO2
|
|
@@ -23,6 +25,22 @@ import { withSetCookie, withRid } from "@camada/core/fetch";
|
|
|
23
25
|
|
|
24
26
|
// src/env.ts
|
|
25
27
|
import { parseKey, parseTrustedProxyEnv } from "@camada/core";
|
|
28
|
+
function hostVar(name2, env) {
|
|
29
|
+
if (env[name2] !== void 0) return env[name2];
|
|
30
|
+
const g = globalThis;
|
|
31
|
+
try {
|
|
32
|
+
if (g.Deno) {
|
|
33
|
+
const perms = g.Deno.permissions;
|
|
34
|
+
if (perms) {
|
|
35
|
+
if (typeof perms.querySync !== "function" || perms.querySync({ name: "env", variable: name2 }).state !== "granted") return void 0;
|
|
36
|
+
}
|
|
37
|
+
return g.Deno.env?.get?.(name2);
|
|
38
|
+
}
|
|
39
|
+
return g.process?.env?.[name2];
|
|
40
|
+
} catch {
|
|
41
|
+
return void 0;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
26
44
|
var asProxy = (v) => typeof v === "string" ? parseTrustedProxyEnv(v) : v ?? null;
|
|
27
45
|
function resolveEnv(opts, env) {
|
|
28
46
|
const raw = opts.key || env.CAMADA_KEY;
|
|
@@ -37,7 +55,8 @@ function resolveEnv(opts, env) {
|
|
|
37
55
|
secret: raw || `${ingestToken}.${snapToken}`,
|
|
38
56
|
ingestUrl,
|
|
39
57
|
snapshotUrl: opts.snapshotUrl || env.CAMADA_SNAPSHOT_URL || `${ingestUrl}/snapshot`,
|
|
40
|
-
|
|
58
|
+
// On Vercel the platform rewrites X-Forwarded-For, so its rightmost entry is the client (as @camada/next).
|
|
59
|
+
trustedProxy: asProxy(opts.trustedProxy) ?? parseTrustedProxyEnv(env.CAMADA_TRUSTED_PROXY) ?? (hostVar("VERCEL", env) ? { mode: "vercel" } : null)
|
|
41
60
|
};
|
|
42
61
|
}
|
|
43
62
|
|
|
@@ -66,7 +85,7 @@ function scriptTag(c) {
|
|
|
66
85
|
|
|
67
86
|
// package.json
|
|
68
87
|
var name = "@camada/hono";
|
|
69
|
-
var version = "0.
|
|
88
|
+
var version = "0.4.0";
|
|
70
89
|
|
|
71
90
|
// src/version.ts
|
|
72
91
|
var SDK_ID = `${name}/${version}`;
|
|
@@ -79,8 +98,25 @@ var FP_MAX = 32 * 1024;
|
|
|
79
98
|
var SESSION_COOKIE = "_sfp";
|
|
80
99
|
var SESSION_MAX_AGE = 2592e3;
|
|
81
100
|
var BODY_MAX = 4 * 1024;
|
|
82
|
-
var
|
|
101
|
+
var noWait = () => {
|
|
102
|
+
};
|
|
103
|
+
var flushNow = () => {
|
|
104
|
+
};
|
|
105
|
+
function requestContextWaitUntil() {
|
|
106
|
+
return guarded2(() => {
|
|
107
|
+
const g = globalThis;
|
|
108
|
+
const ctx = g[/* @__PURE__ */ Symbol.for("@vercel/request-context")]?.get?.();
|
|
109
|
+
if (typeof ctx?.waitUntil !== "function") return null;
|
|
110
|
+
return (p) => {
|
|
111
|
+
try {
|
|
112
|
+
ctx.waitUntil(p);
|
|
113
|
+
} catch {
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
}, null);
|
|
117
|
+
}
|
|
83
118
|
var engines = /* @__PURE__ */ new Map();
|
|
119
|
+
var NO_STORE_HEADERS = ["cache-control", "cdn-cache-control", "surrogate-control", "cloudflare-cdn-cache-control", "vercel-cdn-cache-control", "netlify-cdn-cache-control"];
|
|
84
120
|
function resetCamada() {
|
|
85
121
|
for (const e of engines.values()) {
|
|
86
122
|
e.snap.stop();
|
|
@@ -125,15 +161,41 @@ var cookieValue = (cookie, name2) => {
|
|
|
125
161
|
};
|
|
126
162
|
var cfOf = (req) => req.cf ?? {};
|
|
127
163
|
var trustedProxy = (e) => e.env.trustedProxy ?? e.snap.config?.trusted_proxy ?? null;
|
|
128
|
-
var
|
|
164
|
+
var runtime = globalThis;
|
|
165
|
+
var onWorkers = () => runtime.navigator?.userAgent === "Cloudflare-Workers";
|
|
166
|
+
function serverShape(env) {
|
|
167
|
+
if (!env || typeof env !== "object" || onWorkers()) return null;
|
|
168
|
+
const e = env;
|
|
169
|
+
const str = (v) => typeof v === "string" && v ? v : null;
|
|
170
|
+
if (runtime.process?.versions?.node && e.incoming && typeof e.incoming === "object") return () => str(e.incoming?.socket?.remoteAddress);
|
|
171
|
+
if (runtime.Bun !== void 0) {
|
|
172
|
+
const bun = "server" in e ? e.server : e;
|
|
173
|
+
if (typeof bun?.requestIP === "function") return (req) => str(bun.requestIP(req)?.address);
|
|
174
|
+
}
|
|
175
|
+
if (runtime.Deno !== void 0 && e.remoteAddr && typeof e.remoteAddr === "object") return () => str(e.remoteAddr?.hostname);
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
var SERVERLESS_HOSTS = ["VERCEL", "AWS_LAMBDA_FUNCTION_NAME", "NETLIFY", "DENO_DEPLOYMENT_ID"];
|
|
179
|
+
function batches(opts, env, runtimeEnv) {
|
|
180
|
+
if (hostVar("CAMADA_SERVERLESS", env) === "1") return false;
|
|
181
|
+
if (opts.mode) return opts.mode === "timer";
|
|
182
|
+
if (SERVERLESS_HOSTS.some((k) => hostVar(k, env))) return false;
|
|
183
|
+
return serverShape(runtimeEnv) !== null;
|
|
184
|
+
}
|
|
185
|
+
var clientIp = (e, req, peer) => {
|
|
129
186
|
const cf = req.cf;
|
|
130
|
-
|
|
131
|
-
return
|
|
187
|
+
if (cf) return req.headers.get("cf-connecting-ip") || resolveClientIp(null, req.headers.get("x-forwarded-for"), trustedProxy(e));
|
|
188
|
+
return resolveClientIp(peer, req.headers.get("x-forwarded-for"), trustedProxy(e));
|
|
132
189
|
};
|
|
133
|
-
|
|
190
|
+
var peerOf = (opts, c, hasCtx) => guarded2(() => {
|
|
191
|
+
if (hasCtx || c.req.raw.cf) return null;
|
|
192
|
+
if (opts.peer) return opts.peer(c) || null;
|
|
193
|
+
return serverShape(c.env)?.(c.req.raw) ?? null;
|
|
194
|
+
}, null);
|
|
195
|
+
function describeRequest(e, req, peer) {
|
|
134
196
|
const url = new URL(req.url);
|
|
135
197
|
const cookies = req.headers.get("cookie") || "";
|
|
136
|
-
return { url, path: url.pathname, cf: cfOf(req), ip: clientIp(e, req), sid: cookieValue(cookies, SESSION_COOKIE) };
|
|
198
|
+
return { url, path: url.pathname, cf: cfOf(req), ip: clientIp(e, req, peer), sid: cookieValue(cookies, SESSION_COOKIE) };
|
|
137
199
|
}
|
|
138
200
|
function buildEvent(req, path, query, ip, sid, rid = crypto.randomUUID(), newSession = false) {
|
|
139
201
|
const headers = [];
|
|
@@ -161,13 +223,12 @@ function buildEvent(req, path, query, ip, sid, rid = crypto.randomUUID(), newSes
|
|
|
161
223
|
}
|
|
162
224
|
function ship(e, ev, waitUntil) {
|
|
163
225
|
e.queue.push(ev);
|
|
164
|
-
e.queue.flush(waitUntil);
|
|
226
|
+
if (waitUntil !== noWait) e.queue.flush(waitUntil);
|
|
165
227
|
}
|
|
166
228
|
async function relayBeacon(eng, req, ip, waitUntil) {
|
|
167
229
|
const noContent = () => new Response(null, { status: 204, headers: { "cache-control": "no-store" } });
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
if (encoder.encode(body).byteLength > FP_MAX) return new Response(null, { status: 413 });
|
|
230
|
+
const body = await readBodyCapped(req, FP_MAX);
|
|
231
|
+
if (body === null) return new Response(null, { status: 413 });
|
|
171
232
|
let parsed;
|
|
172
233
|
try {
|
|
173
234
|
parsed = JSON.parse(body);
|
|
@@ -175,7 +236,7 @@ async function relayBeacon(eng, req, ip, waitUntil) {
|
|
|
175
236
|
return noContent();
|
|
176
237
|
}
|
|
177
238
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return noContent();
|
|
178
|
-
ship(eng, { ...parsed, sig: 1, ip, tap: TAP_HONO2 }, waitUntil);
|
|
239
|
+
ship(eng, { ...parsed, sig: 1, ip, tap: TAP_HONO2, rts: Date.now() }, waitUntil);
|
|
179
240
|
return noContent();
|
|
180
241
|
}
|
|
181
242
|
function camada(opts = {}) {
|
|
@@ -188,19 +249,28 @@ function camada(opts = {}) {
|
|
|
188
249
|
const eng = guarded2(() => env.CAMADA_DISABLED === "1" ? null : ensure(opts, env), null);
|
|
189
250
|
if (!eng) return next();
|
|
190
251
|
const challengeOn = opts.challenge !== false && env.CAMADA_CHALLENGE !== "0";
|
|
191
|
-
|
|
252
|
+
let ctx = null;
|
|
253
|
+
try {
|
|
254
|
+
ctx = typeof c.executionCtx?.waitUntil === "function" ? c.executionCtx : null;
|
|
255
|
+
} catch {
|
|
256
|
+
}
|
|
257
|
+
const batch = !ctx && guarded2(() => batches(opts, env, c.env), false);
|
|
258
|
+
const waitUntil = ctx ? (p) => {
|
|
192
259
|
try {
|
|
193
|
-
|
|
260
|
+
ctx.waitUntil(p);
|
|
194
261
|
} catch {
|
|
195
262
|
}
|
|
196
|
-
};
|
|
263
|
+
} : batch ? noWait : requestContextWaitUntil() ?? flushNow;
|
|
264
|
+
if (batch) guarded2(() => eng.queue.installNodeExitFlush(), void 0);
|
|
265
|
+
const peer = peerOf(opts, c, ctx !== null);
|
|
197
266
|
let warnRule = null;
|
|
198
267
|
let vars = null;
|
|
199
268
|
let newSession = false;
|
|
269
|
+
let signalled = false;
|
|
200
270
|
const answer = await guardedAsync2(async () => {
|
|
201
271
|
eng.snap.ensureFresh(waitUntil);
|
|
202
272
|
const req2 = c.req.raw;
|
|
203
|
-
const { url, path, cf, ip, sid } = describeRequest(eng, req2);
|
|
273
|
+
const { url, path, cf, ip, sid } = describeRequest(eng, req2, peer);
|
|
204
274
|
const v = eng.snap.verdict({
|
|
205
275
|
ip,
|
|
206
276
|
path,
|
|
@@ -223,6 +293,7 @@ function camada(opts = {}) {
|
|
|
223
293
|
ship(eng, ev2, waitUntil);
|
|
224
294
|
const headers = {
|
|
225
295
|
"content-type": "text/plain",
|
|
296
|
+
"cache-control": "no-store",
|
|
226
297
|
"x-block-reason": String(v.reason ?? ""),
|
|
227
298
|
"x-block-version": v.version ?? ""
|
|
228
299
|
};
|
|
@@ -233,7 +304,8 @@ function camada(opts = {}) {
|
|
|
233
304
|
if (req2.method === "POST" && path === challengePath) return verify(eng, req2, ip, sid, challengePath, waitUntil);
|
|
234
305
|
const token = cookieValue(req2.headers.get("cookie") || "", CHALLENGE_COOKIE);
|
|
235
306
|
if (v.challenge && !await eng.kit.tokenValid(ip, Date.now(), token)) {
|
|
236
|
-
return serve(eng, req2, ip, sid, challengePath, path + url.search, waitUntil);
|
|
307
|
+
if (wantsHtml(req2.headers.get("accept"), req2.headers.get("sec-fetch-dest"))) return serve(eng, req2, ip, sid, challengePath, path + url.search, waitUntil);
|
|
308
|
+
signalled = true;
|
|
237
309
|
}
|
|
238
310
|
}
|
|
239
311
|
if (beaconEnabled(eng)) {
|
|
@@ -247,7 +319,24 @@ function camada(opts = {}) {
|
|
|
247
319
|
c.set(VAR, vars);
|
|
248
320
|
return null;
|
|
249
321
|
}, null);
|
|
250
|
-
if (answer)
|
|
322
|
+
if (answer) {
|
|
323
|
+
return guarded2(() => {
|
|
324
|
+
c.res = answer;
|
|
325
|
+
const h = c.res.headers;
|
|
326
|
+
if (h === answer.headers) return answer;
|
|
327
|
+
for (const [k, v] of answer.headers) if (k !== "set-cookie") h.set(k, v);
|
|
328
|
+
const cookies = answer.headers.getSetCookie();
|
|
329
|
+
if (cookies.length > 0) {
|
|
330
|
+
const name2 = (ck) => ck.slice(0, Math.max(ck.indexOf("="), 0)).trim();
|
|
331
|
+
const ours = new Set(cookies.map(name2));
|
|
332
|
+
const kept = h.getSetCookie().filter((ck) => !ours.has(name2(ck)));
|
|
333
|
+
h.delete("set-cookie");
|
|
334
|
+
for (const ck of [...kept, ...cookies]) h.append("set-cookie", ck);
|
|
335
|
+
}
|
|
336
|
+
h.delete("content-security-policy-report-only");
|
|
337
|
+
return c.res;
|
|
338
|
+
}, answer);
|
|
339
|
+
}
|
|
251
340
|
const req = c.req.raw;
|
|
252
341
|
const method = req.method;
|
|
253
342
|
const wsRequest = guarded2(() => req.headers.get("upgrade")?.toLowerCase() === "websocket", false);
|
|
@@ -257,7 +346,7 @@ function camada(opts = {}) {
|
|
|
257
346
|
return typeof s?.requestIP === "function" && s.requestIP(req) ? s : null;
|
|
258
347
|
}, null) : null;
|
|
259
348
|
const ev = guarded2(() => {
|
|
260
|
-
const { url, path, ip } = describeRequest(eng, req);
|
|
349
|
+
const { url, path, ip } = describeRequest(eng, req, peer);
|
|
261
350
|
return buildEvent(req, path, url.search, ip, vars?.sid ?? null, vars?.rid, newSession);
|
|
262
351
|
}, null);
|
|
263
352
|
await next();
|
|
@@ -266,8 +355,8 @@ function camada(opts = {}) {
|
|
|
266
355
|
const shipEvent = () => void guardedAsync2(async () => {
|
|
267
356
|
if (!ev) return;
|
|
268
357
|
const cfg = eng.snap.config;
|
|
269
|
-
if (
|
|
270
|
-
if (Math.random() >= (cfg?.sample ?? 1)) return;
|
|
358
|
+
if (signalled) ev.chs = 1;
|
|
359
|
+
else if ((cfg?.exclude || []).some((x) => ev.p.startsWith(x)) || Math.random() >= (cfg?.sample ?? 1)) return;
|
|
271
360
|
ev.ts = t0;
|
|
272
361
|
ev.st = status;
|
|
273
362
|
ev.dur = Math.max(0, Date.now() - t0);
|
|
@@ -294,13 +383,17 @@ function camada(opts = {}) {
|
|
|
294
383
|
if (method !== "HEAD") {
|
|
295
384
|
let out = c.res;
|
|
296
385
|
if (cookie) out = withSetCookie(out, cookie);
|
|
297
|
-
if (rid) out = withRid(out, { rid });
|
|
386
|
+
if (rid) out = withRid(out, { rid, signalled });
|
|
298
387
|
return replaceRes(c, out);
|
|
299
388
|
}
|
|
300
389
|
const head = new Response(null, c.res);
|
|
301
390
|
const headers = new Headers(head.headers);
|
|
302
391
|
if (cookie) headers.append("set-cookie", cookie);
|
|
303
392
|
if (rid) headers.set("x-rid", rid);
|
|
393
|
+
if (signalled) {
|
|
394
|
+
headers.set("x-camada-challenge", "1");
|
|
395
|
+
for (const n of NO_STORE_HEADERS) headers.set(n, "no-store");
|
|
396
|
+
}
|
|
304
397
|
replaceRes(c, new Response(null, { status: head.status, statusText: head.statusText, headers }));
|
|
305
398
|
}, void 0);
|
|
306
399
|
};
|
|
@@ -316,29 +409,22 @@ async function serve(eng, req, ip, sid, action, target, waitUntil) {
|
|
|
316
409
|
ev.st = 403;
|
|
317
410
|
ev.blk = "challenge";
|
|
318
411
|
ship(eng, ev, waitUntil);
|
|
319
|
-
if (!wantsHtml(req.headers.get("accept"), req.headers.get("sec-fetch-dest"))) {
|
|
320
|
-
return new Response('{"error":"challenge_required"}', {
|
|
321
|
-
status: 403,
|
|
322
|
-
headers: { "content-type": "application/json", "cache-control": "no-store", "x-camada-challenge": "1" }
|
|
323
|
-
});
|
|
324
|
-
}
|
|
325
412
|
const html = challengePage({ nonce: await eng.kit.nonce(ip, Date.now()), action, to: safeReturnTo(target) });
|
|
326
413
|
return new Response(html, {
|
|
327
414
|
status: 403,
|
|
328
|
-
headers: { "content-type": "text/html; charset=utf-8", "cache-control": "no-store", "x-camada-challenge": "1" }
|
|
415
|
+
headers: { "content-type": "text/html; charset=utf-8", "cache-control": "no-store", "x-camada-challenge": "1", "content-security-policy": challengePageCsp() }
|
|
329
416
|
});
|
|
330
417
|
}
|
|
331
418
|
async function verify(eng, req, ip, sid, action, waitUntil) {
|
|
332
419
|
const url = new URL(req.url);
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
if (body.length > BODY_MAX) return new Response(null, { status: 413 });
|
|
420
|
+
const body = await readBodyCapped(req, BODY_MAX);
|
|
421
|
+
if (body === null) return new Response(null, { status: 413 });
|
|
336
422
|
const form = parseFormBody(body);
|
|
337
423
|
const to = safeReturnTo(form.to);
|
|
338
424
|
const now = Date.now();
|
|
339
425
|
if (!await eng.kit.verify(ip, now, form.nonce, form.solution)) {
|
|
340
426
|
const html = challengePage({ nonce: await eng.kit.nonce(ip, now), action, to });
|
|
341
|
-
return new Response(html, { status: 403, headers: { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" } });
|
|
427
|
+
return new Response(html, { status: 403, headers: { "content-type": "text/html; charset=utf-8", "cache-control": "no-store", "content-security-policy": challengePageCsp() } });
|
|
342
428
|
}
|
|
343
429
|
const ev = buildEvent(req, url.pathname, "", ip, sid);
|
|
344
430
|
ev.st = 200;
|
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, 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.3\",\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"]}
|
|
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, challengePageCsp, challengeCookie, safeReturnTo, wantsHtml, parseFormBody, readBodyCapped,\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, hostVar, 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\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/** Off Workers in a long-lived process: nothing to hold open, and the queue timer batches the rows. */\nconst noWait: WaitUntil = () => {};\n/** Off Workers in a function that freezes after the response: nothing to hold open either, but\n * every row flushes as it is shipped — a row left for the timer would never go out. */\nconst flushNow: WaitUntil = () => {};\n\n/** The host's request-scoped waitUntil where Vercel provides one (`@vercel/functions` reads the same\n * global; @camada/next's wait-until.ts too): hono/vercel passes no execution context, so without it\n * the function may freeze before a flush started at the response is done. Null elsewhere. */\nfunction requestContextWaitUntil(): WaitUntil | null {\n return guarded(() => {\n const g = globalThis as Record<symbol, { get?: () => { waitUntil?: WaitUntil } | undefined } | undefined>;\n const ctx = g[Symbol.for('@vercel/request-context')]?.get?.();\n if (typeof ctx?.waitUntil !== 'function') return null;\n return (p: Promise<unknown>) => { try { ctx.waitUntil!(p); } catch { /* a context that refuses costs only the hold */ } };\n }, null);\n}\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/** Per-client signal: no shared cache may keep the copy. CDNs read their own header ahead of cache-control. */\nconst NO_STORE_HEADERS = ['cache-control', 'cdn-cache-control', 'surrogate-control', 'cloudflare-cdn-cache-control', 'vercel-cdn-cache-control', 'netlify-cdn-cache-control'];\n\n/** 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\nconst runtime = globalThis as { Bun?: unknown; Deno?: unknown; navigator?: { userAgent?: string }; process?: { versions?: { node?: string } } };\nconst onWorkers = (): boolean => runtime.navigator?.userAgent === 'Cloudflare-Workers';\n\n/** The long-lived server Hono runs under, recognised from the object it hands the app as `c.env`\n * (no static import of hono/bun etc., so a Workers bundle pulls none of them), and a reader for\n * the socket peer it vouches for. Each shape is read only on its own runtime, so a Workers\n * binding that happens to be named `server` or `incoming` (a JS-RPC stub answers any property\n * with a callable) is never called:\n * @hono/node-server `{ incoming }` → `incoming.socket.remoteAddress` (Node or Bun, not Workers);\n * Bun `Bun.serve({ fetch: app.fetch })` → the server (or hono/bun's `{ server }`) →\n * `requestIP(req).address`; Deno `Deno.serve(app.fetch)` → `info.remoteAddr.hostname`.\n * Null for anything else: Workers bindings, hono/vercel, netlify, aws-lambda, lambda-edge. */\nfunction serverShape(env: unknown): ((req: Request) => string | null) | null {\n if (!env || typeof env !== 'object' || onWorkers()) return null;\n const e = env as { incoming?: { socket?: { remoteAddress?: unknown } }; server?: unknown; remoteAddr?: { hostname?: unknown } };\n const str = (v: unknown): string | null => (typeof v === 'string' && v ? v : null);\n if (runtime.process?.versions?.node && e.incoming && typeof e.incoming === 'object') return () => str(e.incoming?.socket?.remoteAddress);\n if (runtime.Bun !== undefined) {\n const bun = ('server' in e ? e.server : e) as BunServer | undefined; // hono/bun's getBunServer\n if (typeof bun?.requestIP === 'function') return (req) => str((bun.requestIP(req) as { address?: unknown } | null)?.address);\n }\n if (runtime.Deno !== undefined && e.remoteAddr && typeof e.remoteAddr === 'object') return () => str(e.remoteAddr?.hostname);\n return null;\n}\n\n/** Serverless hosts whose functions freeze after the response, whatever object they hand `c.env`. */\nconst SERVERLESS_HOSTS = ['VERCEL', 'AWS_LAMBDA_FUNCTION_NAME', 'NETLIFY', 'DENO_DEPLOYMENT_ID'];\n\n/** Whether rows wait for the queue timer (a process that outlives the response) rather than\n * each flushing as it is shipped. Only where there is no execution context to hold open. */\nfunction batches(opts: CamadaHonoOptions, env: Record<string, string | undefined>, runtimeEnv: unknown): boolean {\n if (hostVar('CAMADA_SERVERLESS', env) === '1') return false;\n if (opts.mode) return opts.mode === 'timer';\n if (SERVERLESS_HOSTS.some((k) => hostVar(k, env))) return false;\n return serverShape(runtimeEnv) !== null;\n}\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: the\n * socket peer (the app's `peer(c)`, else the runtime's own) and X-Forwarded-For go through the\n * trusted-proxy rules instead, as on every other adapter. */\nconst clientIp = (e: Engine, req: Request, peer: string | null): string | null => {\n const cf = (req as Request & { cf?: unknown }).cf;\n if (cf) return req.headers.get('cf-connecting-ip') || resolveClientIp(null, req.headers.get('x-forwarded-for'), trustedProxy(e));\n return resolveClientIp(peer, req.headers.get('x-forwarded-for'), trustedProxy(e));\n};\n\n/** The peer for a request off Workers; never consulted where `request.cf` or an execution\n * context exists (a Workers env holds bindings, not a server). Never throws. */\nconst peerOf = (opts: CamadaHonoOptions, c: Context, hasCtx: boolean): string | null => guarded(() => {\n if (hasCtx || (c.req.raw as Request & { cf?: unknown }).cf) return null;\n if (opts.peer) return opts.peer(c) || null;\n return serverShape(c.env)?.(c.req.raw) ?? null;\n}, null);\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, peer: string | null): 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, peer), 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` (Workers, or Vercel's request context) or at once (`flushNow`, serverless without an\n * execution context): the function may freeze right after the response, so each row goes out\n * with it. In a long-lived process (`noWait`) the queue's timer batches the rows — one POST /e per\n * flush, not one per request (core's 7u3r rule). The one rule every row (wire event, beacon,\n * track()) follows. */\nexport function ship(e: Engine, ev: unknown, waitUntil: WaitUntil): void {\n e.queue.push(ev);\n if (waitUntil !== noWait) 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 const body = await readBodyCapped(req, FP_MAX); // streamed: a chunked body cannot buffer past the cap\n if (body === null) 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, rts: Date.now() }, 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 // Hono's executionCtx getter throws where the runtime passed none (node-server, Bun, Deno).\n let ctx: Context['executionCtx'] | null = null;\n try { ctx = typeof c.executionCtx?.waitUntil === 'function' ? c.executionCtx : null; } catch { /* expected off Workers: not an error to log */ }\n const batch = !ctx && guarded(() => batches(opts, env, c.env), false);\n const waitUntil: WaitUntil = ctx\n ? (p) => { try { ctx.waitUntil(p); } catch { /* a context that refuses: the row ships with the next flush */ } }\n : batch ? noWait : requestContextWaitUntil() ?? flushNow;\n // A batching process waits on the queue timer, so drain what is queued on exit (core's timer mode does the same).\n if (batch) guarded(() => eng.queue.installNodeExitFlush(), undefined);\n const peer = peerOf(opts, c, ctx !== null);\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 // A non-HTML request that passed under a challenge: x-camada-challenge: 1 and chs: 1 (82ar).\n let signalled = 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, peer);\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', 'cache-control': 'no-store', '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 // Only a page can be challenged; any other request passes to the app, signalled.\n if (wantsHtml(req.headers.get('accept'), req.headers.get('sec-fetch-dest'))) return serve(eng, req, ip, sid, challengePath, path + url.search, waitUntil);\n signalled = true;\n }\n }\n\n // The beacon endpoints come after enforcement (a blocked client, or one served the challenge\n // page, gets neither the script nor a relay; a challenged non-HTML client is only signalled\n // and still reaches them) 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) {\n // Hono merges the headers of a c.res an app middleware in front of camada touched (c.res.headers,\n // or c.header() once anything read c.res) over the response returned here: an upstream\n // `cache-control: public` would make the IP-bound challenge cacheable, its CSP would block the\n // solver, its Set-Cookie of the same name would replace the `_cch` one. So set c.res to the answer, then put every\n // header of the answer back on top and drop a report-only policy (camada-all-mwpo, R7-1).\n return guarded(() => {\n c.res = answer;\n const h = c.res.headers;\n if (h === answer.headers) return answer; // nothing was merged in\n for (const [k, v] of answer.headers) if (k !== 'set-cookie') h.set(k, v);\n const cookies = answer.headers.getSetCookie();\n if (cookies.length > 0) {\n // The answer's cookie wins only on a name clash; the app's own (a session, say) are kept (RF-2).\n const name = (ck: string) => ck.slice(0, Math.max(ck.indexOf('='), 0)).trim();\n const ours = new Set(cookies.map(name));\n const kept = h.getSetCookie().filter((ck) => !ours.has(name(ck)));\n h.delete('set-cookie');\n for (const ck of [...kept, ...cookies]) h.append('set-cookie', ck);\n }\n h.delete('content-security-policy-report-only');\n return c.res;\n }, answer);\n }\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, peer);\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 // a signalled row ships whatever sample and exclude say, like the 403 row it replaces\n if (signalled) ev.chs = 1;\n else if ((cfg?.exclude || []).some((x) => (ev.p as string).startsWith(x)) || 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; // signalled implies a captured request, so rid is set\n if (method !== 'HEAD') {\n let out = c.res;\n if (cookie) out = withSetCookie(out, cookie);\n if (rid) out = withRid(out, { rid, signalled });\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 if (signalled) { headers.set('x-camada-challenge', '1'); for (const n of NO_STORE_HEADERS) headers.set(n, 'no-store'); }\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), 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 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', 'content-security-policy': challengePageCsp() },\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 const body = await readBodyCapped(req, BODY_MAX);\n if (body === null) 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', 'content-security-policy': challengePageCsp() } });\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 type { Context } from 'hono';\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 /** Off Workers: the socket peer for this request, when the runtime object on `c.env` is not\n * one camada reads itself (node-server, Bun, Deno) — e.g. `(c) => getConnInfo(c).remote.address`.\n * Goes through the trusted-proxy rules with X-Forwarded-For. Never consulted on Workers. */\n peer?: (c: Context) => string | null | undefined;\n /** How rows reach ingest where there is no execution context (always per row through waitUntil\n * where there is one). 'timer': batch on the queue's 15 s timer and drain on exit — for a\n * long-lived server. 'lazy': flush every row as it is shipped — for a function that freezes after\n * the response. Unset: 'timer' only on a recognised node-server / Bun / Deno server outside\n * Vercel, AWS Lambda, Netlify and Deno Deploy, else 'lazy'. CAMADA_SERVERLESS=1 forces 'lazy'. */\n mode?: 'timer' | 'lazy';\n}\n\n/** A host variable: the merged env first (c.env + opts.env), then the process's own env\n * (process.env, or Deno.env where the permission is already granted — never a prompt; a Deno whose\n * permission API has no querySync is not read). A Deno without the permission API (Deno Deploy)\n * has no prompt either, so its env is read directly:\n * that is where DENO_DEPLOYMENT_ID tells a short-lived isolate apart from a long-lived server. */\nexport function hostVar(name: string, env: Record<string, string | undefined>): string | undefined {\n if (env[name] !== undefined) return env[name];\n const g = globalThis as {\n Deno?: { env?: { get?: (k: string) => string | undefined }; permissions?: { querySync?: (d: { name: string; variable: string }) => { state: string } } };\n process?: { env?: Record<string, string | undefined> };\n };\n try {\n if (g.Deno) {\n const perms = g.Deno.permissions;\n if (perms) { // a permission API without querySync (Deno <1.37) cannot be asked silently: don't read (R7-3)\n if (typeof perms.querySync !== 'function' || perms.querySync({ name: 'env', variable: name }).state !== 'granted') return undefined;\n }\n return g.Deno.env?.get?.(name); // a throw (NotCapable) reads as unset, below\n }\n return g.process?.env?.[name];\n } catch { return undefined; }\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 // On Vercel the platform rewrites X-Forwarded-For, so its rightmost entry is the client (as @camada/next).\n trustedProxy: asProxy(opts.trustedProxy) ?? parseTrustedProxyEnv(env.CAMADA_TRUSTED_PROXY)\n ?? (hostVar('VERCEL', env) ? { mode: 'vercel' } : null),\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.4.0\",\n \"license\": \"MIT\",\n \"type\": \"module\",\n \"engines\": {\n \"node\": \">=20\"\n },\n \"files\": [\n \"dist\",\n \"README.md\",\n \"LICENSE\"\n ],\n \"scripts\": {\n \"build\": \"tsup\",\n \"dev\": \"tsup --watch\",\n \"test\": \"vitest run\",\n \"check\": \"tsc --noEmit\"\n },\n \"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 \"import\": {\n \"types\": \"./dist/index.d.ts\",\n \"default\": \"./dist/index.js\"\n },\n \"require\": {\n \"types\": \"./dist/index.d.cts\",\n \"default\": \"./dist/index.cjs\"\n }\n }\n },\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":";AAQA,OAAO,UAAU;AACjB;AAAA,EACE;AAAA,EAAgB;AAAA,EAAY;AAAA,EAAgB;AAAA,EAAiB;AAAA,EAAgB,gBAAAA;AAAA,EAAc;AAAA,EAC3F,WAAAC;AAAA,EAAS;AAAA,EAAsB;AAAA,EAAe;AAAA,EAAkB;AAAA,EAAiB;AAAA,EAAc;AAAA,EAAW;AAAA,EAAe;AAAA,EACzH;AAAA,EAAkB;AAAA,EAA0B,YAAAC;AAAA,OAEvC;AACP,SAAS,eAAe,eAAe;;;ACZvC,SAAS,UAAU,4BAA2E;AAwCvF,SAAS,QAAQC,OAAc,KAA6D;AACjG,MAAI,IAAIA,KAAI,MAAM,OAAW,QAAO,IAAIA,KAAI;AAC5C,QAAM,IAAI;AAIV,MAAI;AACF,QAAI,EAAE,MAAM;AACV,YAAM,QAAQ,EAAE,KAAK;AACrB,UAAI,OAAO;AACT,YAAI,OAAO,MAAM,cAAc,cAAc,MAAM,UAAU,EAAE,MAAM,OAAO,UAAUA,MAAK,CAAC,EAAE,UAAU,UAAW,QAAO;AAAA,MAC5H;AACA,aAAO,EAAE,KAAK,KAAK,MAAMA,KAAI;AAAA,IAC/B;AACA,WAAO,EAAE,SAAS,MAAMA,KAAI;AAAA,EAC9B,QAAQ;AAAE,WAAO;AAAA,EAAW;AAC9B;AAEA,IAAM,UAAU,CAAC,MACf,OAAO,MAAM,WAAW,qBAAqB,CAAC,IAAI,KAAK;AAGlD,SAAS,WAAW,MAAyB,KAA6D;AAC/G,QAAM,MAAM,KAAK,OAAO,IAAI;AAC5B,QAAM,MAAM,SAAS,GAAG;AACxB,QAAM,cAAc,KAAK,eAAe,IAAI;AAC5C,QAAM,YAAY,KAAK,aAAa,IAAI;AACxC,MAAI,CAAC,eAAe,CAAC,UAAW,QAAO;AACvC,QAAM,aAAa,KAAK,aAAa,IAAI,qBAAqB,yBAAyB,QAAQ,OAAO,EAAE;AACxG,SAAO;AAAA,IACL;AAAA,IAAa;AAAA,IACb,QAAQ,OAAO,GAAG,WAAW,IAAI,SAAS;AAAA,IAC1C;AAAA,IACA,aAAa,KAAK,eAAe,IAAI,uBAAuB,GAAG,SAAS;AAAA;AAAA,IAExE,cAAc,QAAQ,KAAK,YAAY,KAAK,qBAAqB,IAAI,oBAAoB,MACnF,QAAQ,UAAU,GAAG,IAAI,EAAE,MAAM,SAAS,IAAI;AAAA,EACtD;AACF;;;AC3EA,SAAS,SAAS,cAAc,YAAY,gBAAgB;AAGrD,IAAM,MAAM;AAWZ,IAAM,gBAAgB,CAAC,MAAuB,EAAE,KAAK,QAAQ,WAAW;AAE/E,IAAM,WAAW,CAAC,MAAuC,EAAE,IAAI,GAAG;AAU3D,SAAS,MAAM,GAAY,OAAe,MAAyC;AACxF,QAAM,OAAO,QAAQ,MAAM,SAAS,CAAC,GAAG,MAAS;AACjD,MAAI,CAAC,KAAM,QAAO,QAAQ,QAAQ;AAClC,QAAM,IAAI,aAAa,YAAY;AACjC,UAAM,MAAM,MAAM,OAAO,MAAM,WAAW,KAAK,MAAM,KAAK,IAAI,IAAI,WAAW,IAAI;AACjF,SAAK,KAAK,KAAK,EAAE,KAAK,UAAU,IAAI,OAAO,KAAK,KAAK,KAAK,KAAK,KAAK,KAAK,KAAK,IAAI,KAAK,IAAI,IAAI,KAAK,IAAI,EAAE,GAAG,KAAK,SAAS;AAAA,EAC7H,GAAG,MAAS;AACZ,OAAK,UAAU,CAAC;AAChB,SAAO;AACT;AAGO,SAAS,UAAU,GAAoB;AAC5C,SAAO,QAAQ,MAAM;AACnB,UAAM,OAAO,SAAS,CAAC;AACvB,QAAI,CAAC,QAAQ,CAAC,cAAc,KAAK,GAAG,EAAG,QAAO;AAC9C,WAAO,gBAAgB,KAAK,UAAU,MAAM,KAAK,GAAG;AAAA,EACtD,GAAG,EAAE;AACP;;;ACjDE,WAAQ;AACR,cAAW;;;ACCN,IAAM,SAAS,GAAG,IAAI,IAAI,OAAO;;;AJiBxC,IAAM,yBAAyB;AAC/B,IAAM,cAAc;AACpB,IAAM,UAAU;AAChB,IAAM,SAAS,KAAK;AACpB,IAAM,iBAAiB;AACvB,IAAM,kBAAkB;AACxB,IAAM,WAAW,IAAI;AAWrB,IAAM,SAAoB,MAAM;AAAC;AAGjC,IAAM,WAAsB,MAAM;AAAC;AAKnC,SAAS,0BAA4C;AACnD,SAAOC,SAAQ,MAAM;AACnB,UAAM,IAAI;AACV,UAAM,MAAM,EAAE,uBAAO,IAAI,yBAAyB,CAAC,GAAG,MAAM;AAC5D,QAAI,OAAO,KAAK,cAAc,WAAY,QAAO;AACjD,WAAO,CAAC,MAAwB;AAAE,UAAI;AAAE,YAAI,UAAW,CAAC;AAAA,MAAG,QAAQ;AAAA,MAAmD;AAAA,IAAE;AAAA,EAC1H,GAAG,IAAI;AACT;AAMA,IAAM,UAAU,oBAAI,IAAoB;AAGxC,IAAM,mBAAmB,CAAC,iBAAiB,qBAAqB,qBAAqB,gCAAgC,4BAA4B,2BAA2B;AAGrK,SAAS,cAAoB;AAClC,aAAW,KAAK,QAAQ,OAAO,GAAG;AAAE,MAAE,KAAK,KAAK;AAAG,MAAE,MAAM,KAAK;AAAA,EAAG;AACnE,UAAQ,MAAM;AAChB;AAEA,SAAS,OAAO,MAAyB,QAA2D;AAClG,QAAM,MAAM,WAAW,MAAM,MAAM;AACnC,MAAI,CAAC,KAAK;AACR,mBAAe,IAAI,MAAM,8CAAyC,CAAC;AACnE,WAAO;AAAA,EACT;AACA,QAAM,kBAAkB,KAAK,mBAAmB;AAChD,QAAM,KAAK,GAAG,IAAI,WAAW,IAAI,IAAI,SAAS,IAAI,IAAI,WAAW,IAAI,eAAe;AACpF,QAAM,SAAS,QAAQ,IAAI,EAAE;AAC7B,MAAI,OAAQ,QAAO;AACnB,QAAM,WAAW,KAAK,YAAY,EAAE,WAAW,KAAK,UAAU,IAAI,CAAC;AACnE,QAAM,SAAiB;AAAA,IACrB;AAAA,IACA,MAAM,IAAI,eAAe;AAAA,MACvB,KAAK,IAAI;AAAA,MAAa,OAAO,IAAI;AAAA,MAAW,MAAM;AAAA,MAAQ,KAAK;AAAA,MAC/D;AAAA,MAAiB,GAAG;AAAA,IACtB,CAAC;AAAA,IACD,OAAO,IAAI,WAAW,EAAE,KAAK,IAAI,WAAW,OAAO,IAAI,aAAa,KAAK,QAAQ,GAAG,SAAS,CAAC;AAAA,IAC9F,KAAK,qBAAqB,EAAE,QAAQ,IAAI,OAAO,CAAC;AAAA,EAClD;AACA,UAAQ,IAAI,IAAI,MAAM;AACtB,SAAO;AACT;AAEA,IAAM,cAAc,CAAC,QAAgBC,UAAgC;AACnE,QAAM,MAAM,OAAO;AACnB,QAAM,IAAI,IAAI,QAAQ,OAAOA,QAAO,GAAG;AACvC,MAAI,MAAM,GAAI,QAAO;AACrB,QAAM,QAAQ,IAAIA,MAAK,SAAS;AAChC,QAAM,IAAI,IAAI,QAAQ,KAAK,KAAK;AAChC,SAAO,IAAI,MAAM,OAAO,MAAM,KAAK,SAAY,CAAC;AAClD;AAKA,IAAM,OAAO,CAAC,QAA4B,IAAmC,MAAM,CAAC;AAEpF,IAAM,eAAe,CAAC,MACpB,EAAE,IAAI,gBAAgB,EAAE,KAAK,QAAQ,iBAAiB;AAExD,IAAM,UAAU;AAChB,IAAM,YAAY,MAAe,QAAQ,WAAW,cAAc;AAWlE,SAAS,YAAY,KAAwD;AAC3E,MAAI,CAAC,OAAO,OAAO,QAAQ,YAAY,UAAU,EAAG,QAAO;AAC3D,QAAM,IAAI;AACV,QAAM,MAAM,CAAC,MAA+B,OAAO,MAAM,YAAY,IAAI,IAAI;AAC7E,MAAI,QAAQ,SAAS,UAAU,QAAQ,EAAE,YAAY,OAAO,EAAE,aAAa,SAAU,QAAO,MAAM,IAAI,EAAE,UAAU,QAAQ,aAAa;AACvI,MAAI,QAAQ,QAAQ,QAAW;AAC7B,UAAM,MAAO,YAAY,IAAI,EAAE,SAAS;AACxC,QAAI,OAAO,KAAK,cAAc,WAAY,QAAO,CAAC,QAAQ,IAAK,IAAI,UAAU,GAAG,GAAoC,OAAO;AAAA,EAC7H;AACA,MAAI,QAAQ,SAAS,UAAa,EAAE,cAAc,OAAO,EAAE,eAAe,SAAU,QAAO,MAAM,IAAI,EAAE,YAAY,QAAQ;AAC3H,SAAO;AACT;AAGA,IAAM,mBAAmB,CAAC,UAAU,4BAA4B,WAAW,oBAAoB;AAI/F,SAAS,QAAQ,MAAyB,KAAyC,YAA8B;AAC/G,MAAI,QAAQ,qBAAqB,GAAG,MAAM,IAAK,QAAO;AACtD,MAAI,KAAK,KAAM,QAAO,KAAK,SAAS;AACpC,MAAI,iBAAiB,KAAK,CAAC,MAAM,QAAQ,GAAG,GAAG,CAAC,EAAG,QAAO;AAC1D,SAAO,YAAY,UAAU,MAAM;AACrC;AAOA,IAAM,WAAW,CAAC,GAAW,KAAc,SAAuC;AAChF,QAAM,KAAM,IAAmC;AAC/C,MAAI,GAAI,QAAO,IAAI,QAAQ,IAAI,kBAAkB,KAAK,gBAAgB,MAAM,IAAI,QAAQ,IAAI,iBAAiB,GAAG,aAAa,CAAC,CAAC;AAC/H,SAAO,gBAAgB,MAAM,IAAI,QAAQ,IAAI,iBAAiB,GAAG,aAAa,CAAC,CAAC;AAClF;AAIA,IAAM,SAAS,CAAC,MAAyB,GAAY,WAAmCD,SAAQ,MAAM;AACpG,MAAI,UAAW,EAAE,IAAI,IAAmC,GAAI,QAAO;AACnE,MAAI,KAAK,KAAM,QAAO,KAAK,KAAK,CAAC,KAAK;AACtC,SAAO,YAAY,EAAE,GAAG,IAAI,EAAE,IAAI,GAAG,KAAK;AAC5C,GAAG,IAAI;AAUP,SAAS,gBAAgB,GAAW,KAAc,MAAmC;AACnF,QAAM,MAAM,IAAI,IAAI,IAAI,GAAG;AAC3B,QAAM,UAAU,IAAI,QAAQ,IAAI,QAAQ,KAAK;AAC7C,SAAO,EAAE,KAAK,MAAM,IAAI,UAAU,IAAI,KAAK,GAAG,GAAG,IAAI,SAAS,GAAG,KAAK,IAAI,GAAG,KAAK,YAAY,SAAS,cAAc,EAAE;AACzH;AAEA,SAAS,WACP,KAAc,MAAc,OAAe,IAAmB,KAC9D,MAAc,OAAO,WAAW,GAAG,aAAa,OACrC;AACX,QAAM,UAAmC,CAAC;AAC1C,MAAI,QAAQ,QAAQ,CAAC,GAAG,MAAM,QAAQ,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC;AAClD,QAAM,KAAK,KAAK,GAAG;AACnB,QAAM,KAAK;AAAA,IACT;AAAA,MACE,QAAQ,IAAI;AAAA,MAAQ,MAAM,IAAI,QAAQ,IAAI,MAAM,KAAK,IAAI,IAAI,IAAI,GAAG,EAAE;AAAA,MAAM;AAAA,MAAM;AAAA,MAAO;AAAA,MAAS;AAAA;AAAA;AAAA;AAAA,MAIlG,aAAa,GAAG,eAAe,GAAG,aAAa,QAAQ,YAAY,EAAE,IAAI;AAAA,IAC3E;AAAA,IACA,EAAE,KAAKE,WAAU,KAAK,KAAK,WAAW;AAAA,EACxC;AACA,MAAI,GAAG,IAAK,IAAG,MAAM,GAAG;AACxB,MAAI,GAAG,QAAS,IAAG,KAAK,GAAG;AAC3B,MAAI,GAAG,wBAAyB,IAAG,OAAO,GAAG;AAC7C,SAAO;AACT;AAOO,SAAS,KAAK,GAAW,IAAa,WAA4B;AACvE,IAAE,MAAM,KAAK,EAAE;AACf,MAAI,cAAc,OAAQ,GAAE,MAAM,MAAM,SAAS;AACnD;AAGA,eAAe,YAAY,KAAa,KAAc,IAAmB,WAAyC;AAChH,QAAM,YAAY,MAAM,IAAI,SAAS,MAAM,EAAE,QAAQ,KAAK,SAAS,EAAE,iBAAiB,WAAW,EAAE,CAAC;AACpG,QAAM,OAAO,MAAM,eAAe,KAAK,MAAM;AAC7C,MAAI,SAAS,KAAM,QAAO,IAAI,SAAS,MAAM,EAAE,QAAQ,IAAI,CAAC;AAC5D,MAAI;AACJ,MAAI;AAAE,aAAS,KAAK,MAAM,IAAI;AAAA,EAAG,QAAQ;AAAE,WAAO,UAAU;AAAA,EAAG;AAC/D,MAAI,CAAC,UAAU,OAAO,WAAW,YAAY,MAAM,QAAQ,MAAM,EAAG,QAAO,UAAU;AAErF,OAAK,KAAK,EAAE,GAAI,QAAoC,KAAK,GAAG,IAAI,KAAKA,WAAU,KAAK,KAAK,IAAI,EAAE,GAAG,SAAS;AAC3G,SAAO,UAAU;AACnB;AAEO,SAAS,OAAO,OAA0B,CAAC,GAAsB;AACtE,QAAM,gBAAgB,KAAK,iBAAiB;AAC5C,QAAM,aAAa,KAAK,cAAc;AACtC,QAAM,SAAS,KAAK,UAAU;AAE9B,SAAO,eAAe,WAAW,GAAY,MAAsC;AACjF,UAAM,KAAK,KAAK,IAAI;AAGpB,UAAM,MAAMF,SAAQ,OAAO,EAAE,GAAI,EAAE,KAAwD,GAAG,KAAK,IAAI,IAAI,CAAC,CAAuC;AACnJ,UAAM,MAAMA,SAAQ,MAAO,IAAI,oBAAoB,MAAM,OAAO,OAAO,MAAM,GAAG,GAAI,IAAI;AACxF,QAAI,CAAC,IAAK,QAAO,KAAK;AAEtB,UAAM,cAAc,KAAK,cAAc,SAAS,IAAI,qBAAqB;AAGzE,QAAI,MAAsC;AAC1C,QAAI;AAAE,YAAM,OAAO,EAAE,cAAc,cAAc,aAAa,EAAE,eAAe;AAAA,IAAM,QAAQ;AAAA,IAAkD;AAC/I,UAAM,QAAQ,CAAC,OAAOA,SAAQ,MAAM,QAAQ,MAAM,KAAK,EAAE,GAAG,GAAG,KAAK;AACpE,UAAM,YAAuB,MACzB,CAAC,MAAM;AAAE,UAAI;AAAE,YAAI,UAAU,CAAC;AAAA,MAAG,QAAQ;AAAA,MAAkE;AAAA,IAAE,IAC7G,QAAQ,SAAS,wBAAwB,KAAK;AAElD,QAAI,MAAO,CAAAA,SAAQ,MAAM,IAAI,MAAM,qBAAqB,GAAG,MAAS;AACpE,UAAM,OAAO,OAAO,MAAM,GAAG,QAAQ,IAAI;AAIzC,QAAI,WAA0B;AAG9B,QAAI,OAA0B;AAC9B,QAAI,aAAa;AAEjB,QAAI,YAAY;AAEhB,UAAM,SAAS,MAAMG,cAAa,YAAsC;AACtE,UAAI,KAAK,YAAY,SAAS;AAE9B,YAAMC,OAAM,EAAE,IAAI;AAClB,YAAM,EAAE,KAAK,MAAM,IAAI,IAAI,IAAI,IAAI,gBAAgB,KAAKA,MAAK,IAAI;AAEjE,YAAM,IAAI,IAAI,KAAK,QAAQ;AAAA,QACzB;AAAA,QAAI;AAAA,QAAM,IAAIA,KAAI,QAAQ,IAAI,YAAY;AAAA;AAAA;AAAA;AAAA;AAAA,QAI1C,QAAQ,CAAC,MAAMA,KAAI,QAAQ,IAAI,CAAC;AAAA,QAChC,KAAK,GAAG,OAAO;AAAA,QAAM,SAAS,GAAG,WAAW;AAAA,QAAM,MAAM,GAAG,2BAA2B;AAAA,MACxF,CAAC;AACD,iBAAW,EAAE,OAAO,EAAE,QAAQ,OAAO;AAErC,UAAI,EAAE,OAAO;AACX,cAAMC,MAAK,WAAWD,MAAK,MAAM,IAAI,QAAQ,IAAI,GAAG;AACpD,QAAAC,IAAG,KAAK;AACR,QAAAA,IAAG,MAAM,EAAE;AACX,YAAI,EAAE,KAAM,CAAAA,IAAG,KAAK,EAAE;AACtB,aAAK,KAAKA,KAAI,SAAS;AACvB,cAAM,UAAkC;AAAA,UACtC,gBAAgB;AAAA,UAAc,iBAAiB;AAAA,UAAY,kBAAkB,OAAO,EAAE,UAAU,EAAE;AAAA,UAAG,mBAAmB,EAAE,WAAW;AAAA,QACvI;AACA,YAAI,EAAE,KAAM,SAAQ,cAAc,IAAI,EAAE;AACxC,eAAO,IAAI,SAAS,aAAa,EAAE,QAAQ,KAAK,QAAQ,CAAC;AAAA,MAC3D;AAIA,UAAI,eAAe,IAAI;AAErB,YAAID,KAAI,WAAW,UAAU,SAAS,cAAe,QAAO,OAAO,KAAKA,MAAK,IAAI,KAAK,eAAe,SAAS;AAC9G,cAAM,QAAQ,YAAYA,KAAI,QAAQ,IAAI,QAAQ,KAAK,IAAI,gBAAgB;AAC3E,YAAI,EAAE,aAAa,CAAE,MAAM,IAAI,IAAI,WAAW,IAAI,KAAK,IAAI,GAAG,KAAK,GAAI;AAErE,cAAI,UAAUA,KAAI,QAAQ,IAAI,QAAQ,GAAGA,KAAI,QAAQ,IAAI,gBAAgB,CAAC,EAAG,QAAO,MAAM,KAAKA,MAAK,IAAI,KAAK,eAAe,OAAO,IAAI,QAAQ,SAAS;AACxJ,sBAAY;AAAA,QACd;AAAA,MACF;AAKA,UAAI,cAAc,GAAG,GAAG;AACtB,YAAIA,KAAI,WAAW,SAAS,SAAS,YAAY;AAC/C,iBAAO,IAAI,SAAS,MAAM,EAAE,QAAQ,KAAK,SAAS,EAAE,gBAAgB,0BAA0B,iBAAiB,uBAAuB,EAAE,CAAC;AAAA,QAC3I;AACA,YAAIA,KAAI,WAAW,UAAU,SAAS,OAAQ,QAAO,YAAY,KAAKA,MAAK,IAAI,SAAS;AAAA,MAC1F;AAKA,mBAAa,CAAC;AACd,aAAO,EAAE,KAAK,KAAK,OAAO,WAAW,GAAG,KAAK,OAAO,OAAO,WAAW,GAAG,IAAI,WAAW,WAAW;AACnG,QAAE,IAAI,KAAK,IAAI;AACf,aAAO;AAAA,IACT,GAAG,IAAI;AAEP,QAAI,QAAQ;AAMV,aAAOJ,SAAQ,MAAM;AACnB,UAAE,MAAM;AACR,cAAM,IAAI,EAAE,IAAI;AAChB,YAAI,MAAM,OAAO,QAAS,QAAO;AACjC,mBAAW,CAAC,GAAG,CAAC,KAAK,OAAO,QAAS,KAAI,MAAM,aAAc,GAAE,IAAI,GAAG,CAAC;AACvE,cAAM,UAAU,OAAO,QAAQ,aAAa;AAC5C,YAAI,QAAQ,SAAS,GAAG;AAEtB,gBAAMC,QAAO,CAAC,OAAe,GAAG,MAAM,GAAG,KAAK,IAAI,GAAG,QAAQ,GAAG,GAAG,CAAC,CAAC,EAAE,KAAK;AAC5E,gBAAM,OAAO,IAAI,IAAI,QAAQ,IAAIA,KAAI,CAAC;AACtC,gBAAM,OAAO,EAAE,aAAa,EAAE,OAAO,CAAC,OAAO,CAAC,KAAK,IAAIA,MAAK,EAAE,CAAC,CAAC;AAChE,YAAE,OAAO,YAAY;AACrB,qBAAW,MAAM,CAAC,GAAG,MAAM,GAAG,OAAO,EAAG,GAAE,OAAO,cAAc,EAAE;AAAA,QACnE;AACA,UAAE,OAAO,qCAAqC;AAC9C,eAAO,EAAE;AAAA,MACX,GAAG,MAAM;AAAA,IACX;AAKA,UAAM,MAAM,EAAE,IAAI;AAClB,UAAM,SAAS,IAAI;AACnB,UAAM,YAAYD,SAAQ,MAAM,IAAI,QAAQ,IAAI,SAAS,GAAG,YAAY,MAAM,aAAa,KAAK;AAIhG,UAAM,MAAM,YAAYA,SAAQ,MAAM;AACpC,YAAM,IAAI,EAAE;AACZ,YAAM,IAAK,KAAK,YAAY,IAAI,EAAE,SAAS;AAC3C,aAAO,OAAO,GAAG,cAAc,cAAc,EAAE,UAAU,GAAG,IAAI,IAAI;AAAA,IACtE,GAAG,IAAI,IAAI;AACX,UAAM,KAAKA,SAAQ,MAAM;AACvB,YAAM,EAAE,KAAK,MAAM,GAAG,IAAI,gBAAgB,KAAK,KAAK,IAAI;AACxD,aAAO,WAAW,KAAK,MAAM,IAAI,QAAQ,IAAI,MAAM,OAAO,MAAM,MAAM,KAAK,UAAU;AAAA,IACvF,GAAG,IAAI;AAEP,UAAM,KAAK;AAUX,UAAM,WAAW,aAAa,EAAE,IAAI,WAAW,OAAO,CAAC,EAAE,IAAI,QAAQ,IAAI,cAAc,MACjF,MAAMA,SAAQ,MAAM,IAAI,UAAU,GAAG,MAAM,MAAM,KAAK,IAAI;AAChE,UAAM,SAAS,WAAW,MAAM,EAAE,IAAI;AACtC,UAAM,YAAY,MAAM,KAAKG,cAAa,YAAY;AACpD,UAAI,CAAC,GAAI;AACT,YAAM,MAAM,IAAI,KAAK;AAErB,UAAI,UAAW,IAAG,MAAM;AAAA,gBACd,KAAK,WAAW,CAAC,GAAG,KAAK,CAAC,MAAO,GAAG,EAAa,WAAW,CAAC,CAAC,KAAK,KAAK,OAAO,MAAM,KAAK,UAAU,GAAI;AAClH,SAAG,KAAK;AACR,SAAG,KAAK;AACR,SAAG,MAAM,KAAK,IAAI,GAAG,KAAK,IAAI,IAAI,EAAE;AACpC,UAAI,SAAU,IAAG,MAAM;AACvB,WAAK,KAAK,IAAI,SAAS;AAAA,IACzB,GAAG,MAAS;AACZ,QAAI;AAIF,iBAAW,GAAG,WAAW,EAAE,KAAK,WAAW,EAAE,QAAQ,WAAW,IAAI,SAAS;AAAE,eAAO,EAAE,IAAI,IAAI;AAAA,MAAQ,EAAE,CAAC,CAAC;AAAA,IAC9G,SAAS,KAAK;AACZ,qBAAe,GAAG;AAClB,gBAAU;AAAA,IACZ;AAYA,IAAAH,SAAQ,MAAM;AACZ,UAAI,EAAE,IAAI,WAAW,IAAK;AAC1B,YAAM,MAAM,IAAI;AAChB,UAAI;AACJ,UAAI,QAAQ,YAAY;AACtB,cAAM,SAAS,IAAI,IAAI,EAAE,IAAI,IAAI,GAAG,EAAE,aAAa,WAAW,aAAa;AAC3E,iBAAS,GAAG,cAAc,IAAI,KAAK,GAAG,qBAAqB,eAAe,2BAA2B,MAAM;AAAA,MAC7G;AACA,UAAI,CAAC,OAAO,CAAC,OAAQ;AACrB,UAAI,WAAW,QAAQ;AACrB,YAAI,MAAM,EAAE;AACZ,YAAI,OAAQ,OAAM,cAAc,KAAK,MAAM;AAC3C,YAAI,IAAK,OAAM,QAAQ,KAAK,EAAE,KAAK,UAAU,CAAC;AAC9C,eAAO,WAAW,GAAG,GAAG;AAAA,MAC1B;AACA,YAAM,OAAO,IAAI,SAAS,MAAM,EAAE,GAAG;AACrC,YAAM,UAAU,IAAI,QAAQ,KAAK,OAAO;AACxC,UAAI,OAAQ,SAAQ,OAAO,cAAc,MAAM;AAC/C,UAAI,IAAK,SAAQ,IAAI,SAAS,GAAG;AACjC,UAAI,WAAW;AAAE,gBAAQ,IAAI,sBAAsB,GAAG;AAAG,mBAAW,KAAK,iBAAkB,SAAQ,IAAI,GAAG,UAAU;AAAA,MAAG;AACvH,iBAAW,GAAG,IAAI,SAAS,MAAM,EAAE,QAAQ,KAAK,QAAQ,YAAY,KAAK,YAAY,QAAQ,CAAC,CAAC;AAAA,IACjG,GAAG,MAAS;AAAA,EACd;AACF;AAOA,SAAS,WAAW,GAAY,KAAqB;AACnD,MAAI,QAAQ,EAAE,IAAK;AACnB,IAAE,MAAM;AACR,IAAE,MAAM;AACV;AAGA,eAAe,MAAM,KAAa,KAAc,IAAY,KAAoB,QAAgB,QAAgB,WAAyC;AACvJ,QAAM,MAAM,IAAI,IAAI,IAAI,GAAG;AAC3B,QAAM,KAAK,WAAW,KAAK,IAAI,UAAU,IAAI,QAAQ,IAAI,GAAG;AAC5D,KAAG,KAAK;AACR,KAAG,MAAM;AACT,OAAK,KAAK,IAAI,SAAS;AAEvB,QAAM,OAAO,cAAc,EAAE,OAAO,MAAM,IAAI,IAAI,MAAM,IAAI,KAAK,IAAI,CAAC,GAAG,QAAQ,IAAI,aAAa,MAAM,EAAE,CAAC;AAC3G,SAAO,IAAI,SAAS,MAAM;AAAA,IACxB,QAAQ;AAAA,IACR,SAAS,EAAE,gBAAgB,4BAA4B,iBAAiB,YAAY,sBAAsB,KAAK,2BAA2B,iBAAiB,EAAE;AAAA,EAC/J,CAAC;AACH;AAGA,eAAe,OAAO,KAAa,KAAc,IAAY,KAAoB,QAAgB,WAAyC;AACxI,QAAM,MAAM,IAAI,IAAI,IAAI,GAAG;AAG3B,QAAM,OAAO,MAAM,eAAe,KAAK,QAAQ;AAC/C,MAAI,SAAS,KAAM,QAAO,IAAI,SAAS,MAAM,EAAE,QAAQ,IAAI,CAAC;AAC5D,QAAM,OAAO,cAAc,IAAI;AAC/B,QAAM,KAAK,aAAa,KAAK,EAAE;AAC/B,QAAM,MAAM,KAAK,IAAI;AACrB,MAAI,CAAE,MAAM,IAAI,IAAI,OAAO,IAAI,KAAK,KAAK,OAAO,KAAK,QAAQ,GAAI;AAC/D,UAAM,OAAO,cAAc,EAAE,OAAO,MAAM,IAAI,IAAI,MAAM,IAAI,GAAG,GAAG,QAAQ,GAAG,CAAC;AAC9E,WAAO,IAAI,SAAS,MAAM,EAAE,QAAQ,KAAK,SAAS,EAAE,gBAAgB,4BAA4B,iBAAiB,YAAY,2BAA2B,iBAAiB,EAAE,EAAE,CAAC;AAAA,EAChL;AACA,QAAM,KAAK,WAAW,KAAK,IAAI,UAAU,IAAI,IAAI,GAAG;AACpD,KAAG,KAAK;AACR,KAAG,KAAK;AACR,OAAK,KAAK,IAAI,SAAS;AACvB,SAAO,IAAI,SAAS,MAAM;AAAA,IACxB,QAAQ;AAAA,IACR,SAAS;AAAA,MACP,UAAU;AAAA,MACV,cAAc,gBAAgB,MAAM,IAAI,IAAI,MAAM,IAAI,GAAG,GAAG,IAAI,aAAa,QAAQ;AAAA,MACrF,iBAAiB;AAAA,IACnB;AAAA,EACF,CAAC;AACH;","names":["guardedAsync","guarded","TAP_HONO","name","guarded","name","TAP_HONO","guardedAsync","req","ev"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@camada/hono",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -14,17 +14,22 @@
|
|
|
14
14
|
"description": "camada SDK for Hono on Cloudflare Workers: snapshot enforcement, first-party challenge and beacon, track() outcomes, waitUntil-flushed event shipping",
|
|
15
15
|
"exports": {
|
|
16
16
|
".": {
|
|
17
|
-
"
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
"import": {
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"default": "./dist/index.js"
|
|
20
|
+
},
|
|
21
|
+
"require": {
|
|
22
|
+
"types": "./dist/index.d.cts",
|
|
23
|
+
"default": "./dist/index.cjs"
|
|
24
|
+
}
|
|
20
25
|
}
|
|
21
26
|
},
|
|
22
27
|
"main": "./dist/index.cjs",
|
|
23
28
|
"module": "./dist/index.js",
|
|
24
29
|
"types": "./dist/index.d.ts",
|
|
25
30
|
"dependencies": {
|
|
26
|
-
"@camada/core": "^0.
|
|
27
|
-
"@camada/browser": "^0.
|
|
31
|
+
"@camada/core": "^0.6.0",
|
|
32
|
+
"@camada/browser": "^0.4.0"
|
|
28
33
|
},
|
|
29
34
|
"peerDependencies": {
|
|
30
35
|
"hono": ">=4"
|