@camada/next 0.2.0 → 0.3.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 CHANGED
@@ -3,8 +3,13 @@
3
3
  camada backend SDK for Next.js (app router): edge-safe middleware/proxy enforcement, the
4
4
  `/api/camada` route handlers that serve the fingerprint beacon first-party, and a
5
5
  `<CamadaBeacon/>` server component carrying the request's `rid`. Implements plan.md
6
- INT-2/INT-3 for the Next.js position. Not yet published to npm; consumed via `file:`
7
- dependencies from sibling checkouts.
6
+ INT-2/INT-3 for the Next.js position.
7
+
8
+ ```sh
9
+ npm install @camada/next
10
+ ```
11
+
12
+ Requires Node 20 or later.
8
13
 
9
14
  ## Install
10
15
 
@@ -33,7 +38,7 @@ import { CamadaBeacon } from '@camada/next';
33
38
  CAMADA_KEY=<ingest_token>.<snap_token>
34
39
  ```
35
40
 
36
- Optional env: `CAMADA_INGEST_URL` (default `https://in.camada.dev`), `CAMADA_SNAPSHOT_URL`
41
+ Optional env: `CAMADA_INGEST_URL` (default `https://in.camada.app`), `CAMADA_SNAPSHOT_URL`
37
42
  (default ingest + `/snapshot`), `CAMADA_TRUSTED_PROXY` (`none | vercel | hops:N |
38
43
  cidrs:a,b`), `CAMADA_DISABLED=1` (kill switch, checked per request). Unconfigured, the SDK
39
44
  is inert: the middleware returns `undefined`, the route handlers answer 404/204, and one
@@ -44,7 +49,7 @@ rate-limited `console.error` says why.
44
49
  - **`camada()`** — the middleware: refreshes the blocklist snapshot off-path
45
50
  (`event.waitUntil`), enforces it inline (403 with
46
51
  `x-block-reason`/`x-block-version`/`x-block-rule`; blocked requests always ship,
47
- unsampled), stamps `x-camada-rid` onto the forwarded request
52
+ unsampled), stamps `x-camada-rid` onto the forwarded request (dropping any the client sent)
48
53
  and `x-rid` onto the response, and ships the wire event fire-and-forget with tap
49
54
  `sdk-next`. On Vercel, `x-vercel-ja4-digest` — a real TLS fingerprint — rides along as
50
55
  `ja4`.
@@ -56,6 +61,64 @@ rate-limited `console.error` says why.
56
61
  `next/headers` and renders `<script src="/api/camada/b.js?r=<rid>" async>` so the beacon
57
62
  POST joins the server-side wire event.
58
63
 
64
+ ## Status and duration: `withCamada()`
65
+
66
+ The middleware runs before your route, so its event ships pre-response with `st` and `dur`
67
+ null, and that traffic shows on the IP timeline as points without a duration. To report both
68
+ for a route handler, wrap it:
69
+
70
+ ```ts
71
+ // app/api/data/route.ts
72
+ import { withCamada } from '@camada/next';
73
+ export const GET = withCamada(async (req) => Response.json(await load()));
74
+ ```
75
+
76
+ The response carries `x-rid` and the `_sfp` session cookie, as the middleware's does.
77
+
78
+ ```ts
79
+ // proxy.ts: leave wrapped routes out of the matcher, so each request is recorded once
80
+ export const config = { matcher: ['/((?!_next/|favicon.ico|api/data$).*)'] };
81
+ ```
82
+
83
+ End each excluded route with `$`. Without it the exclusion is a prefix match: `api/data` also
84
+ takes `/api/database`, `/api/data-export` and `/api/data/anything` out of the middleware, and
85
+ a route left out of the matcher is protected only by its own `withCamada()`. If it isn't
86
+ wrapped, it gets no blocks, no challenges and no events. To exclude a wrapped route and its
87
+ sub-routes, use `api/data(?:/|$)`, and wrap all of them.
88
+
89
+ The wrapper does what the middleware does (verdict, block, challenge, the `_sfp` session) and
90
+ ships one event with the real status and `dur`: request start to the response, and to the last
91
+ byte for a streamed `text/event-stream` body. A thrown `redirect()` or `notFound()` reports the status
92
+ Next answers with. The event flushes once the body has gone out. On a serverless host the
93
+ wrapper holds the function open for it with Next's `after()` (Next 15.1+), or with the request
94
+ context's `waitUntil` on older versions. `track()` does the same.
95
+
96
+ If the middleware also matched the request, it has already shipped an event, and the wrapper
97
+ skips its own (it logs a rate-limited hint, and that request has no `dur`).
98
+
99
+ **Threat model.** Every request header can come from the client. A route left out of the
100
+ matcher never sees the middleware, and Next has no server-only channel from the middleware to
101
+ a route handler (on Vercel they run in different processes). So:
102
+
103
+ - The wrapper enforces on every request. Verdict, block and challenge never depend on a header.
104
+ - The one thing a header decides is whether to skip the second event. The middleware stamps
105
+ `x-camada-mw` on the request it forwards: a timestamp and an HMAC, under your server-only
106
+ `CAMADA_KEY`, of that timestamp, the method, the path and query, the rid and
107
+ `X-Forwarded-For`. The wrapper accepts it for 30 seconds and compares in constant time.
108
+ - The middleware deletes any `x-camada-mw` the client sent before it forwards a request.
109
+ - A forged mark doesn't verify. A leaked one (the rid is in the `x-rid` response header, and
110
+ the mark rides the forwarded request headers into anything that logs or proxies them)
111
+ doesn't verify on another path, query or method, from another client chain, or after 30 s.
112
+ Within those bounds, the most a replay can do is hide its own event. It still gets blocked
113
+ or challenged, and that ships an event.
114
+ - A middleware rewrite, or a proxy that changes `X-Forwarded-For` between the middleware and
115
+ the function, makes the mark fail. That costs a second event, never enforcement.
116
+
117
+ Pages and server components have no handler to wrap and keep the middleware's pre-response
118
+ event. `track()` inside a wrapped route joins on the wrapper's own rid and session, never on an
119
+ `x-camada-rid` the client sent (nothing strips it outside the matcher). The Vercel `ja4` header
120
+ is not sent from this position.
121
+
59
122
  ## Custom rules
60
123
 
61
124
  Your Rules page holds one ordered list per project, and the middleware walks it before the
@@ -110,8 +173,9 @@ a real cold → blocked flow against the golden snapshot fixture.
110
173
  - **Header order is alphabetical.** The edge runtime sorts request headers, so `hord` is
111
174
  sorted at this tap (still shipped; the scorer's capability mask knows `sdk-next` lacks the
112
175
  raw-wire-order signal `@camada/node` has).
113
- - **Events ship pre-response** (`st: null`, like the edge collector's tap) — middleware
114
- cannot see the final status. Blocked requests ship with `st: 403` and `blk: <reason>`
176
+ - **Middleware events ship pre-response** (`st` and `dur` null, like the edge collector's
177
+ tap): middleware cannot see the final status. Wrap a route handler in `withCamada()` to
178
+ report both (above). Blocked requests ship with `st: 403` and `blk: <reason>`
115
179
  (ip4|ip6|path|rule); the beacon route handlers ship the same event when they deny. Every
116
180
  snapshot poll and event batch carries `x-camada-sdk: @camada/next/<version>`.
117
181
  - **Serverless cold start fails open**: the first request on a cold instance sees no
@@ -139,4 +203,5 @@ The middleware/proxy matcher decides where enforcement runs. The beacon route ha
139
203
  (`/api/camada/*`) enforce the blocklist themselves, so blocked clients can never fetch the
140
204
  beacon even when your matcher excludes `/api/` — but every OTHER route your matcher excludes
141
205
  is invisible to camada: no blocking, no events. Keep the matcher as broad as the example's
142
- (`/((?!_next/|favicon.ico).*)`) unless you have a specific reason not to.
206
+ (`/((?!_next/|favicon.ico).*)`) unless you have a specific reason not to. Anchor every exclusion
207
+ with `$` (see `withCamada()` above): an unanchored lookahead entry is a prefix match.
package/dist/index.cjs CHANGED
@@ -34,7 +34,8 @@ __export(index_exports, {
34
34
  camada: () => camada,
35
35
  camadaRoute: () => camadaRoute,
36
36
  challengeGate: () => challengeGate,
37
- track: () => track
37
+ track: () => track,
38
+ withCamada: () => withCamada
38
39
  });
39
40
  module.exports = __toCommonJS(index_exports);
40
41
 
@@ -46,7 +47,7 @@ var import_core4 = require("@camada/core");
46
47
  var import_core = require("@camada/core");
47
48
 
48
49
  // package.json
49
- var version = "0.2.0";
50
+ var version = "0.3.0";
50
51
  var name = "@camada/next";
51
52
 
52
53
  // src/version.ts
@@ -58,7 +59,7 @@ function resolveEnv(env) {
58
59
  const ingestToken = key?.ingestToken ?? env.CAMADA_TOKEN;
59
60
  const snapToken = key?.snapToken ?? env.CAMADA_SNAPSHOT_TOKEN;
60
61
  if (!ingestToken || !snapToken) return null;
61
- const ingestUrl = (env.CAMADA_INGEST_URL || "https://in.camada.dev").replace(/\/$/, "");
62
+ const ingestUrl = (env.CAMADA_INGEST_URL || "https://in.camada.app").replace(/\/$/, "");
62
63
  return {
63
64
  ingestToken,
64
65
  snapToken,
@@ -126,6 +127,24 @@ function buildEvent(req, path, ip, rid, sid, newSession) {
126
127
  { tap: import_core2.TAP_NEXT, rid, sid, newSession, ja4: req.headers.get("x-vercel-ja4-digest") }
127
128
  );
128
129
  }
130
+ var MW_HEADER = "x-camada-mw";
131
+ var MW_WINDOW_MS = 3e4;
132
+ var MW_SKEW_MS = 5e3;
133
+ var markInput = (ts, req, rid) => {
134
+ const url = new URL(req.url);
135
+ return "mw:" + JSON.stringify([ts, req.method, url.pathname + url.search, rid, req.headers.get("x-forwarded-for") ?? ""]);
136
+ };
137
+ async function middlewareMark(secret, req, rid, now = Date.now()) {
138
+ return `${now}.${await (0, import_core2.hmacHex)(secret, markInput(now, req, rid))}`;
139
+ }
140
+ async function middlewareMarkValid(secret, req, now = Date.now()) {
141
+ const m = /^(\d{13})\.([0-9a-f]{64})$/.exec(req.headers.get(MW_HEADER) ?? "");
142
+ const rid = req.headers.get("x-camada-rid");
143
+ if (!m || !rid) return false;
144
+ const ts = Number(m[1]);
145
+ if (now - ts > MW_WINDOW_MS || ts - now > MW_SKEW_MS) return false;
146
+ return (0, import_core2.safeEqual)(m[2], await (0, import_core2.hmacHex)(secret, markInput(ts, req, rid)));
147
+ }
129
148
  var SESSION_COOKIE = "_sfp";
130
149
  var cookieValue = (cookie, name2) => {
131
150
  const src = "; " + cookie;
@@ -208,11 +227,13 @@ async function challengeGate(req) {
208
227
  const ip = clientIp(engine2, req);
209
228
  if (!ip || await challengePassed(engine2, req, ip)) return null;
210
229
  const url = new URL(req.url);
211
- return serveChallenge(engine2, req, ip, url.pathname + url.search, void 0, !req.headers.has("x-camada-rid"));
230
+ const shipped = await middlewareMarkValid(engine2.env.secret, req).catch(() => false);
231
+ return serveChallenge(engine2, req, ip, url.pathname + url.search, void 0, !shipped);
212
232
  }
213
233
 
214
234
  // src/middleware.ts
215
- function capture(engine2, req, path, ip, existingSid, warnRule, waitUntil) {
235
+ var isBeaconRoute = (method, path) => method === "GET" && path === "/api/camada/b.js" || method === "POST" && path === "/api/camada/fp";
236
+ async function capture(engine2, req, path, ip, existingSid, warnRule, waitUntil) {
216
237
  const rid = crypto.randomUUID();
217
238
  const sid = existingSid ?? crypto.randomUUID();
218
239
  const cfg = engine2.snap.config;
@@ -225,6 +246,9 @@ function capture(engine2, req, path, ip, existingSid, warnRule, waitUntil) {
225
246
  }
226
247
  const headers = new Headers(req.headers);
227
248
  headers.set("x-camada-rid", rid);
249
+ headers.delete(MW_HEADER);
250
+ const mark = await middlewareMark(engine2.env.secret, req, rid).catch(() => null);
251
+ if (mark) headers.set(MW_HEADER, mark);
228
252
  const res = import_server.NextResponse.next({ request: { headers } });
229
253
  res.headers.set("x-rid", rid);
230
254
  if (!existingSid) {
@@ -233,12 +257,19 @@ function capture(engine2, req, path, ip, existingSid, warnRule, waitUntil) {
233
257
  }
234
258
  return res;
235
259
  }
260
+ function passThrough(req) {
261
+ if (!req.headers.has(MW_HEADER) && !req.headers.has("x-camada-rid")) return void 0;
262
+ const headers = new Headers(req.headers);
263
+ headers.delete(MW_HEADER);
264
+ headers.delete("x-camada-rid");
265
+ return import_server.NextResponse.next({ request: { headers } });
266
+ }
236
267
  function camada(_options) {
237
268
  return function camadaMiddleware(req, event) {
238
269
  try {
239
- if (isDisabled()) return void 0;
270
+ if (isDisabled()) return passThrough(req);
240
271
  const engine2 = getEngine();
241
- if (!engine2) return void 0;
272
+ if (!engine2) return passThrough(req);
242
273
  const waitUntil = event?.waitUntil ? (p) => event.waitUntil(p) : void 0;
243
274
  engine2.snap.ensureFresh(waitUntil);
244
275
  const ip = (0, import_core4.resolveClientIp)(null, req.headers.get("x-forwarded-for"), trustedProxy(engine2));
@@ -267,12 +298,13 @@ function camada(_options) {
267
298
  }
268
299
  const warnRule = v.warn ? v.rule ?? null : null;
269
300
  if (v.challenge && ip && challengeEnabled() && !isChallengeRoute(path)) {
270
- return (0, import_core4.guardedAsync)(async () => await challengePassed(engine2, req, ip) ? capture(engine2, req, path, ip, existingSid, warnRule, waitUntil) : serveChallenge(engine2, req, ip, path + new URL(req.url).search, waitUntil), void 0);
301
+ return (0, import_core4.guardedAsync)(async () => await challengePassed(engine2, req, ip) ? capture(engine2, req, path, ip, existingSid, warnRule, waitUntil) : serveChallenge(engine2, req, ip, path + new URL(req.url).search, waitUntil), passThrough(req));
271
302
  }
272
- return capture(engine2, req, path, ip, existingSid, warnRule, waitUntil);
303
+ if (isBeaconRoute(req.method, path)) return passThrough(req);
304
+ return (0, import_core4.guardedAsync)(() => capture(engine2, req, path, ip, existingSid, warnRule, waitUntil), passThrough(req));
273
305
  } catch (err) {
274
306
  (0, import_core4.logRateLimited)(err);
275
- return void 0;
307
+ return (0, import_core4.guarded)(() => passThrough(req), void 0);
276
308
  }
277
309
  };
278
310
  }
@@ -372,7 +404,63 @@ async function CamadaBeacon({ src = "/api/camada/b.js" } = {}) {
372
404
 
373
405
  // src/track.ts
374
406
  var import_core6 = require("@camada/core");
407
+
408
+ // src/wait-until.ts
409
+ var nextServer = __toESM(require("next/server"), 1);
410
+ function contextWaitUntil() {
411
+ const g = globalThis;
412
+ for (const name2 of ["@next/request-context", "@vercel/request-context"]) {
413
+ const ctx = g[Symbol.for(name2)]?.get?.();
414
+ if (typeof ctx?.waitUntil === "function") return (p) => ctx.waitUntil(p);
415
+ }
416
+ return void 0;
417
+ }
418
+ function requestWaitUntil() {
419
+ const pending = [];
420
+ let hold;
421
+ const drain = async () => {
422
+ for (let i = 0; i < pending.length; i++) await pending[i].catch(() => {
423
+ });
424
+ };
425
+ return (p) => {
426
+ if (hold === void 0) {
427
+ hold = null;
428
+ try {
429
+ const after2 = nextServer.after;
430
+ if (typeof after2 !== "function") throw new Error("no after()");
431
+ after2(drain);
432
+ hold = (q) => {
433
+ pending.push(q);
434
+ };
435
+ } catch {
436
+ try {
437
+ hold = contextWaitUntil() ?? null;
438
+ } catch {
439
+ hold = null;
440
+ }
441
+ }
442
+ }
443
+ try {
444
+ hold?.(p);
445
+ } catch {
446
+ }
447
+ };
448
+ }
449
+
450
+ // src/track.ts
375
451
  var SESSION_COOKIE2 = "_sfp";
452
+ var als;
453
+ var scopes = () => {
454
+ if (als === void 0) {
455
+ const Ctor = globalThis.AsyncLocalStorage;
456
+ als = Ctor ? new Ctor() : null;
457
+ }
458
+ return als;
459
+ };
460
+ var withTrackScope = (scope, fn) => {
461
+ const s = scopes();
462
+ return s ? s.run(scope, fn) : fn();
463
+ };
376
464
  async function track(event, data) {
377
465
  try {
378
466
  if (isDisabled()) return;
@@ -380,27 +468,82 @@ async function track(event, data) {
380
468
  if (!engine2) return;
381
469
  const { headers, cookies } = await import("next/headers");
382
470
  const [h, c] = await Promise.all([headers(), cookies()]);
471
+ const scope = scopes()?.getStore();
383
472
  const uid = data?.user ? await (0, import_core6.hashUserId)(data.user, engine2.env.ingestToken) : null;
384
473
  engine2.queue.push({
385
474
  tap: import_core6.TAP_NEXT,
386
475
  et: event,
387
476
  uid,
388
- rid: h.get("x-camada-rid"),
389
- sid: c.get(SESSION_COOKIE2)?.value ?? null,
477
+ rid: scope ? scope.rid : h.get("x-camada-rid"),
478
+ sid: scope?.sid ?? c.get(SESSION_COOKIE2)?.value ?? null,
390
479
  ip: (0, import_core6.resolveClientIp)(null, h.get("x-forwarded-for"), trustedProxy(engine2)),
391
480
  ts: Date.now()
392
481
  });
393
- void engine2.queue.flush();
482
+ void engine2.queue.flush(requestWaitUntil());
394
483
  } catch (err) {
395
484
  (0, import_core6.logRateLimited)(err);
396
485
  }
397
486
  }
487
+
488
+ // src/with-camada.ts
489
+ var import_iife_string2 = __toESM(require("@camada/browser/iife-string"), 1);
490
+ var import_core7 = require("@camada/core");
491
+ var import_fetch = require("@camada/core/fetch");
492
+ var cam = (0, import_fetch.createFetchCamada)({ tap: import_core7.TAP_NEXT, sdk: SDK_ID, iife: import_iife_string2.default }, { challengePath: VERIFY_PATH });
493
+ async function middlewareShipped(req) {
494
+ const secret = getEngine()?.env.secret;
495
+ return !!secret && await middlewareMarkValid(secret, req);
496
+ }
497
+ var thrownStatus = (err) => Number(/;(\d{3});?$/.exec(String(err?.digest ?? ""))?.[1] ?? 500);
498
+ function withCamada(handler) {
499
+ return async (req, ...rest) => {
500
+ const env = envSource();
501
+ const r = await cam.before(req, {
502
+ env: { ...env, CAMADA_TRUSTED_PROXY: env.CAMADA_TRUSTED_PROXY || (env.VERCEL ? "vercel" : void 0) },
503
+ waitUntil: requestWaitUntil()
504
+ // the event flushes after the body: hold a serverless function open for it
505
+ });
506
+ if (!r) return handler(req, ...rest);
507
+ if (r.response) return r.response;
508
+ let shipped = false;
509
+ try {
510
+ shipped = await middlewareShipped(req);
511
+ } catch (err) {
512
+ (0, import_core7.logRateLimited)(err);
513
+ }
514
+ if (shipped) {
515
+ (0, import_core7.logRateLimited)(new Error(`withCamada: the middleware also matched ${new URL(req.url).pathname}, so its pre-response event stands and dur is not reported; exclude wrapped routes from the matcher`));
516
+ return handler(req, ...rest);
517
+ }
518
+ let res;
519
+ try {
520
+ res = await withTrackScope({ rid: r.vars.rid, sid: r.vars.sid }, () => handler(req, ...rest));
521
+ } catch (err) {
522
+ cam.after(req, r.vars, thrownStatus(err));
523
+ throw err;
524
+ }
525
+ res = withRid(res, r.vars.rid);
526
+ return cam.finish(req, r.vars, r.vars.sessionCookie ? (0, import_fetch.withSetCookie)(res, r.vars.sessionCookie) : res);
527
+ };
528
+ }
529
+ function withRid(res, rid) {
530
+ if (res.status === 101) return res;
531
+ try {
532
+ res.headers.set("x-rid", rid);
533
+ return res;
534
+ } catch {
535
+ const out = new Response(res.body, res);
536
+ out.headers.set("x-rid", rid);
537
+ return out;
538
+ }
539
+ }
398
540
  // Annotate the CommonJS export names for ESM import in node:
399
541
  0 && (module.exports = {
400
542
  CamadaBeacon,
401
543
  camada,
402
544
  camadaRoute,
403
545
  challengeGate,
404
- track
546
+ track,
547
+ withCamada
405
548
  });
406
549
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/middleware.ts","../src/engine.ts","../package.json","../src/version.ts","../src/challenge.ts","../src/event.ts","../src/route.ts","../src/beacon.tsx","../src/track.ts"],"sourcesContent":["// @camada/next — the three-line install:\n// middleware.ts / proxy.ts: export default camada();\n// app/api/camada/[...camada]/route.ts: export const { GET, POST } = camadaRoute();\n// app/layout.tsx <head>: <CamadaBeacon/>\n// Plus track() for app-context outcomes from server actions / route handlers.\nexport { camada, type CamadaMiddlewareOptions, type MiddlewareResult } from './middleware';\nexport { camadaRoute } from './route';\nexport { challengeGate } from './challenge';\nexport { CamadaBeacon, type CamadaBeaconProps } from './beacon';\nexport { track } from './track';\n","// The Next.js middleware/proxy entry: enforce the blocklist inline, stamp x-camada-rid onto\n// the forwarded request (the <CamadaBeacon/> server component reads it back), ship the wire\n// event fire-and-forget. Everything runs inside a try/catch fail-open envelope — a camada\n// bug must never break the customer's app (plan.md INT-2).\n//\n// MUST stay edge-runtime-safe: Web APIs only (fetch/Headers/Request/Response/URL/crypto),\n// no node: imports anywhere in this entry's import graph. Proven by test/edge-safety.test.ts,\n// which bundles this file for a bare edge runtime and executes it inside @edge-runtime/vm.\n// NextRequest/NextFetchEvent are imported as TYPES only; NextResponse comes from\n// 'next/server', which is itself edge-safe.\nimport type { NextFetchEvent, NextRequest } from 'next/server';\nimport { NextResponse } from 'next/server';\nimport { resolveClientIp, logRateLimited, guardedAsync } from '@camada/core';\nimport { getEngine, isDisabled, challengeEnabled, trustedProxy, type Engine } from './engine';\nimport { isChallengeRoute, challengePassed, serveChallenge } from './challenge';\nimport { buildEvent, cookieValue, SESSION_COOKIE } from './event';\n\nexport interface CamadaMiddlewareOptions {\n // Reserved. The engine is configured via CAMADA_* environment variables.\n}\n\n/** Next accepts a promise here; only the challenge branch returns one (it awaits WebCrypto). */\nexport type MiddlewareResult = Response | undefined | Promise<Response | undefined>;\n\n/** The ordinary path: ship one pre-response event, stamp the rid, mint the session cookie.\n * `warnRule` is the id of the `warn` rule that let this request through, if one did (§D3). */\nfunction capture(engine: Engine, req: NextRequest, path: string, ip: string | null, existingSid: string | null, warnRule: string | null, waitUntil?: (p: Promise<unknown>) => void): Response {\n const rid = crypto.randomUUID();\n const sid = existingSid ?? crypto.randomUUID();\n const cfg = engine.snap.config;\n const excluded = (cfg?.exclude || []).some((x) => path.startsWith(x));\n if (!excluded && Math.random() < (cfg?.sample ?? 1)) {\n // st stays null: middleware ships pre-response, like the edge collector's tap.\n const ev = buildEvent(req, path, ip, rid, sid, !existingSid);\n if (warnRule) ev.wrn = warnRule;\n engine.queue.push(ev);\n engine.queue.flush(waitUntil);\n }\n\n const headers = new Headers(req.headers);\n headers.set('x-camada-rid', rid);\n const res = NextResponse.next({ request: { headers } });\n res.headers.set('x-rid', rid);\n if (!existingSid) {\n const secure = new URL(req.url).protocol === 'https:' ? '; Secure' : '';\n res.headers.append('set-cookie', `${SESSION_COOKIE}=${sid}; Path=/; Max-Age=2592000; HttpOnly; SameSite=Lax${secure}`);\n }\n return res;\n}\n\n/**\n * `export default camada();` from middleware.ts (Next ≤15) / proxy.ts (Next 16).\n * Returns undefined (Next continues) whenever camada is disabled, unconfigured, or broken.\n */\nexport function camada(_options?: CamadaMiddlewareOptions): (req: NextRequest, event: NextFetchEvent) => MiddlewareResult {\n return function camadaMiddleware(req: NextRequest, event: NextFetchEvent): MiddlewareResult {\n try {\n if (isDisabled()) return undefined;\n const engine = getEngine();\n if (!engine) return undefined;\n const waitUntil = event?.waitUntil ? (p: Promise<unknown>) => event.waitUntil(p) : undefined;\n engine.snap.ensureFresh(waitUntil);\n\n // There is no socket peer at the edge: with no trusted-proxy config (or no XFF) the\n // ip resolves to null and ip rules simply don't enforce — a spoofed X-Forwarded-For\n // can never reach the blocklist. On Vercel the engine defaults to {mode:'vercel'}.\n const ip = resolveClientIp(null, req.headers.get('x-forwarded-for'), trustedProxy(engine));\n const path = new URL(req.url).pathname;\n\n // the same _sfp session as the collector and @camada/node: sid/ns comparable across taps\n const existingSid = cookieValue(req.headers.get('cookie') || '', SESSION_COOKIE);\n\n // The custom rules read the user agent and the request headers (§D3); without them every\n // `ua` and `header` condition is false. `Headers.get` is case-insensitive, so the\n // lower-cased name the matcher asks with finds whatever spelling the client sent.\n const v = engine.snap.verdict({\n ip, path, ua: req.headers.get('user-agent'), header: (n) => req.headers.get(n),\n }); // cold start fails open ('cold')\n if (v.block) {\n const ev = buildEvent(req, path, ip, crypto.randomUUID(), existingSid, false);\n ev.st = 403; // blocked requests always ship, unsampled\n ev.blk = v.reason; // SDK-01: the reason rides the event so the analyst counts SDK blocks, not the app's own 403s ('rule' when a rule decided)\n if (v.rule) ev.rl = v.rule;\n engine.queue.push(ev);\n engine.queue.flush(waitUntil);\n const headers: Record<string, string> = {\n 'content-type': 'text/plain',\n 'x-block-reason': String(v.reason ?? ''),\n '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 // `warn` passes the request and only marks its event; a skip passes with nothing\n // stamped — it is the absence of enforcement.\n const warnRule = v.warn ? v.rule ?? null : null;\n\n // A challenge verdict: serve the proof-of-work page unless this client already passed.\n // The verify route answers its own endpoint, so never challenge that path. A challenge\n // needs a resolved ip (the nonce and `_cch` are bound to it) — without one, fail open,\n // the same stance ip rules take at this position.\n // A client that HAS passed falls through to the normal capture path: it keeps its rid,\n // its session cookie and its event, so an hour of `_cch` is not an hour of blindness.\n // guardedAsync covers the whole branch — the synchronous catch below cannot see a\n // rejection from these awaits, and WebCrypto is not guaranteed to exist.\n if (v.challenge && ip && challengeEnabled() && !isChallengeRoute(path)) {\n return guardedAsync(async () => (\n (await challengePassed(engine, req, ip))\n ? capture(engine, req, path, ip, existingSid, warnRule, waitUntil)\n : serveChallenge(engine, req, ip, path + new URL(req.url).search, waitUntil)\n ), undefined);\n }\n\n return capture(engine, req, path, ip, existingSid, warnRule, waitUntil);\n } catch (err) {\n logRateLimited(err); // fail open: the app proceeds as if camada were not installed\n return undefined;\n }\n };\n}\n","// The lazy singleton engine behind every @camada/next entry point: one SnapshotClient in\n// 'lazy' mode (edge runtime — ensureFresh per request, no interval timers) plus one\n// EventQueue, wired from the environment on first use. Unconfigured -> engine is null, every\n// entry point stays inert, and one rate-limited console.error says why. CAMADA_DISABLED=1 is\n// checked per request (isDisabled), never memoized here.\n//\n// This module is part of the middleware's import graph and therefore MUST stay\n// edge-runtime-safe: Web APIs and process.env reads only, no node: imports.\nimport {\n SnapshotClient, EventQueue, parseKey, parseTrustedProxyEnv, logRateLimited,\n type SnapshotVersion, type TrustedProxyConfig,\n} from '@camada/core';\n\nimport { SDK_ID } from './version';\n\nexport { parseTrustedProxyEnv }; // re-export for callers that had reached into this module\n\nexport interface ResolvedEnv {\n ingestToken: string;\n snapToken: string;\n secret: string; // HMAC key for the challenge nonce/cookie — never leaves the process\n ingestUrl: string;\n snapshotUrl: string;\n trustedProxy: TrustedProxyConfig | null; // null = defer to server-delivered config\n}\n\n/** Returns null (SDK stays inert, one log line) rather than throwing on bad config. */\nexport function resolveEnv(env: Record<string, string | undefined>): ResolvedEnv | null {\n const key = parseKey(env.CAMADA_KEY);\n const ingestToken = key?.ingestToken ?? env.CAMADA_TOKEN;\n const snapToken = key?.snapToken ?? env.CAMADA_SNAPSHOT_TOKEN;\n if (!ingestToken || !snapToken) return null;\n const ingestUrl = (env.CAMADA_INGEST_URL || 'https://in.camada.dev').replace(/\\/$/, ''); // PLACEHOLDER default — confirm the production ingest domain before any npm publish\n return {\n ingestToken, snapToken, ingestUrl,\n secret: env.CAMADA_KEY || `${ingestToken}.${snapToken}`,\n snapshotUrl: env.CAMADA_SNAPSHOT_URL || `${ingestUrl}/snapshot`,\n // On Vercel the platform overwrites X-Forwarded-For, so its rightmost entry is\n // trustworthy: default to vercel mode there unless explicitly overridden.\n trustedProxy: parseTrustedProxyEnv(env.CAMADA_TRUSTED_PROXY) ?? (env.VERCEL ? { mode: 'vercel' } : null),\n };\n}\n\nexport interface Engine {\n env: ResolvedEnv;\n snap: SnapshotClient;\n queue: EventQueue;\n fetchImpl: typeof fetch; // for the beacon relay; snapshot/queue carry their own copy\n}\n\nexport interface ConfigureOptions {\n env?: Record<string, string | undefined>;\n fetchImpl?: typeof fetch;\n challenge?: boolean; // enforce `challenge` verdicts with the first-party page (default true)\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}\n\nlet engine: Engine | null | undefined; // undefined = not built yet; null = unconfigured\nlet overrides: ConfigureOptions = {};\n\n/** Internal test/reset hook (mirrors @camada/node's configure): replaces the singleton. */\nexport function configure(opts: ConfigureOptions = {}): void {\n engine?.snap.stop();\n engine?.queue.stop();\n overrides = opts;\n engine = undefined;\n}\n\nfunction envSource(): Record<string, string | undefined> {\n return overrides.env ?? (typeof process !== 'undefined' && process.env ? process.env : {});\n}\n\n/** Kill switch, checked per request so an env flip disables camada without a code change. */\nexport function isDisabled(): boolean {\n return envSource().CAMADA_DISABLED === '1';\n}\n\n/** SDK-04 is on unless the app opts out in code or with CAMADA_CHALLENGE=0. */\nexport function challengeEnabled(): boolean {\n return overrides.challenge !== false && envSource().CAMADA_CHALLENGE !== '0';\n}\n\nexport function getEngine(): Engine | null {\n if (engine !== undefined) return engine;\n const env = resolveEnv(envSource());\n if (!env) {\n logRateLimited(new Error('CAMADA_KEY (or CAMADA_TOKEN + CAMADA_SNAPSHOT_TOKEN) not set — camada is inactive'));\n return (engine = null);\n }\n const fetchImpl = overrides.fetchImpl;\n const injected = fetchImpl ? { fetchImpl } : {}; // never pass an explicit undefined key\n engine = {\n env,\n fetchImpl: fetchImpl ?? fetch,\n // snapshotVersion left undefined takes core's default (5); the client drops undefined keys.\n snap: new SnapshotClient({ url: env.snapshotUrl, token: env.snapToken, mode: 'lazy', sdk: SDK_ID, snapshotVersion: overrides.snapshotVersion, ...injected }),\n queue: new EventQueue({ url: env.ingestUrl, token: env.ingestToken, sdk: SDK_ID, ...injected }),\n };\n return engine;\n}\n\n/** The trusted-proxy config in force: explicit local/env override wins, then the\n * server-delivered tenant config, then none (socket-less edge -> ip stays null). */\nexport function trustedProxy(e: Engine): TrustedProxyConfig | null {\n return e.env.trustedProxy ?? e.snap.config?.trusted_proxy ?? null;\n}\n","{\n \"version\": \"0.2.0\",\n \"license\": \"MIT\",\n \"type\": \"module\",\n \"engines\": {\n \"node\": \">=18\"\n },\n \"files\": [\n \"dist\",\n \"README.md\",\n \"LICENSE\"\n ],\n \"scripts\": {\n \"build\": \"tsup\",\n \"dev\": \"tsup --watch\",\n \"test\": \"vitest run\",\n \"check\": \"tsc --noEmit\"\n },\n \"name\": \"@camada/next\",\n \"description\": \"camada backend SDK for Next.js: edge-safe middleware/proxy, /api/camada route handlers, <CamadaBeacon/> with the request rid\",\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 \"@camada/react\": \"file:../camada-react\"\n },\n \"peerDependencies\": {\n \"next\": \">=15\",\n \"react\": \">=18\"\n },\n \"devDependencies\": {\n \"typescript\": \"^5.7.2\",\n \"tsup\": \"^8.3.5\",\n \"vitest\": \"^3.0.0\",\n \"next\": \"^16.0.0\",\n \"react\": \"^19.0.0\",\n \"@types/react\": \"^19.0.0\",\n \"@edge-runtime/vm\": \"^5.0.0\",\n \"esbuild\": \"^0.24.0\",\n \"@types/node\": \"^22.10.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","// The challenge for @camada/next (SDK-04). Edge-runtime-safe: Web APIs and WebCrypto only, no\n// node: imports — this module sits in the middleware's import graph, which\n// test/edge-safety.test.ts bundles for a bare edge runtime.\n//\n// The page is served by the middleware (or by challengeGate() from a route the app gates\n// itself) and posts a form to POST /api/camada/challenge, handled by camadaRoute().\nimport {\n createChallengeAsync, challengePage, challengeCookie, safeReturnTo, wantsHtml, parseFormBody,\n resolveClientIp, CHALLENGE_COOKIE, type AsyncChallengeKit,\n} from '@camada/core';\nimport { getEngine, isDisabled, challengeEnabled, trustedProxy, type Engine } from './engine';\nimport { buildEvent, cookieValue, SESSION_COOKIE } from './event';\n\n/** Where the page posts. The catch-all route handler must be mounted at /api/camada/[...camada]\n * (the documented install) — the served page hard-codes this action. */\nexport const VERIFY_PATH = '/api/camada/challenge';\n\nconst BODY_MAX = 4 * 1024; // the verify form is ~120 bytes; anything larger is not ours\n\n/** The app may mount the catch-all elsewhere, so the middleware matches on the tail. */\nexport const isChallengeRoute = (path: string): boolean => /\\/camada\\/challenge\\/?$/.test(path);\n\n// One kit per engine; the engine is replaced wholesale by configure(), so the map never grows.\nconst kits = new WeakMap<Engine, AsyncChallengeKit>();\nexport function kitFor(engine: Engine): AsyncChallengeKit {\n let k = kits.get(engine);\n if (!k) { k = createChallengeAsync({ secret: engine.env.secret }); kits.set(engine, k); }\n return k;\n}\n\nexport const clientIp = (engine: Engine, req: Request): string | null =>\n resolveClientIp(null, req.headers.get('x-forwarded-for'), trustedProxy(engine));\n\nexport function challengePassed(engine: Engine, req: Request, ip: string): Promise<boolean> {\n return kitFor(engine).tokenValid(ip, Date.now(), cookieValue(req.headers.get('cookie') || '', CHALLENGE_COOKIE));\n}\n\n/** 403 + the proof-of-work page (HTML navigations) or 403 JSON (everything else), plus the\n * `blk: \"challenge\"` event — a served challenge is reported like a block (contract §D2). */\nexport async function serveChallenge(engine: Engine, req: Request, ip: string, target: string, waitUntil?: (p: Promise<unknown>) => void, ship = true): Promise<Response> {\n if (ship) {\n const ev = buildEvent(req, new URL(req.url).pathname, ip, crypto.randomUUID(), cookieValue(req.headers.get('cookie') || '', SESSION_COOKIE), false);\n ev.st = 403;\n ev.blk = 'challenge';\n engine.queue.push(ev);\n engine.queue.flush(waitUntil);\n }\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 kitFor(engine).nonce(ip, Date.now()), action: VERIFY_PATH, 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 handler for `…/camada/challenge`: validate, set `_cch`, 302 back, ship `{ st: 200, ch: 1 }`. */\nexport async function verifyChallenge(engine: Engine, req: Request): Promise<Response> {\n const url = new URL(req.url);\n const ip = clientIp(engine, req);\n const kit = kitFor(engine);\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 (!ip || !(await kit.verify(ip, now, form.nonce, form.solution))) {\n const html = ip ? challengePage({ nonce: await kit.nonce(ip, now), action: url.pathname, to }) : 'Forbidden';\n return new Response(html, { status: 403, headers: { 'content-type': ip ? 'text/html; charset=utf-8' : 'text/plain', 'cache-control': 'no-store' } });\n }\n const ev = buildEvent(req, url.pathname, ip, crypto.randomUUID(), cookieValue(req.headers.get('cookie') || '', SESSION_COOKIE), false);\n ev.st = 200;\n ev.ch = 1; // challenge passed (contract §A3 ingest field)\n engine.queue.push(ev);\n void engine.queue.flush();\n return new Response(null, {\n status: 302,\n headers: {\n location: to,\n 'set-cookie': challengeCookie(await kit.issue(ip, now), url.protocol === 'https:'),\n 'cache-control': 'no-store',\n },\n });\n}\n\n/** For a route that wants to gate itself (the example's /challenge-me): the challenge Response,\n * or null when this client already holds a valid `_cch` and the route should render normally.\n *\n * One request, one event. The middleware stamps `x-camada-rid` on the request it forwards, so\n * its presence means the middleware already shipped this request's row; the gate then serves\n * the page WITHOUT a second row, which would double-count the request at the analyst. Exclude\n * the gated route from the middleware matcher and the gate ships the `blk: \"challenge\"` row\n * itself — that is the configuration to use when you want the challenge counted. */\nexport async function challengeGate(req: Request): Promise<Response | null> {\n if (isDisabled() || !challengeEnabled()) return null;\n const engine = getEngine();\n if (!engine) return null;\n const ip = clientIp(engine, req);\n if (!ip || await challengePassed(engine, req, ip)) return null; // unidentifiable client: fail open\n const url = new URL(req.url);\n return serveChallenge(engine, req, ip, url.pathname + url.search, undefined, !req.headers.has('x-camada-rid'));\n}\n","// One wire-event builder for every @camada/next entry (middleware + beacon route). Web APIs\n// only: this sits in the middleware's edge-safe import graph.\nimport { buildWireEvent, TAP_NEXT, type WireEvent } from '@camada/core';\n\nexport function buildEvent(req: Request, path: string, ip: string | null, rid: string, sid: string | null, newSession: boolean): WireEvent {\n const url = new URL(req.url);\n return buildWireEvent(\n {\n method: req.method,\n host: req.headers.get('host') ?? url.host,\n path,\n query: url.search,\n // The edge runtime sorts header names, so hord is alphabetical at this tap — still\n // shipped; the scorer knows sdk-next lacks the raw-wire-order signal (capability mask).\n headers: [...req.headers.entries()],\n ip,\n httpVersion: null, // not observable at this position\n },\n { tap: TAP_NEXT, rid, sid, newSession, ja4: req.headers.get('x-vercel-ja4-digest') },\n );\n}\n\nexport const SESSION_COOKIE = '_sfp'; // the same session cookie as the edge collector and @camada/node: sid/ns comparable across taps\n\nexport const 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","// Route handlers for app/api/camada/[...camada]/route.ts:\n// GET …/b.js -> the first-party beacon IIFE (its auto-init derives the fp endpoint from\n// the script URL's final path segment and the rid from ?r=)\n// POST …/fp -> queue the beacon as a sig:1 row with the trusted-proxy-resolved client IP\n// and tap 'sdk-next': it rides the event batch (one request per flush at the\n// analyst, not one per page view) — the mirror of @camada/node's /_cam/*.\n// Works on BOTH runtimes (edge and node): Web APIs only, no node: imports. Unconfigured or\n// disabled: GET 404s, POST answers 204 and drops — inert, never an error.\nimport iife from '@camada/browser/iife-string';\nimport { guardedAsync, resolveClientIp, TAP_NEXT } from '@camada/core';\nimport { getEngine, isDisabled, challengeEnabled, trustedProxy, type Engine } from './engine';\nimport { verifyChallenge } from './challenge';\nimport { buildEvent, cookieValue, SESSION_COOKIE } from './event';\n\nconst FP_MAX = 32 * 1024; // matches the server's /fp cap: never accept what ingest will 413\nconst encoder = new TextEncoder();\n\nconst notFound = () => new Response(null, { status: 404 });\nconst noContent = () => new Response(null, { status: 204, headers: { 'cache-control': 'no-store' } });\n\n// The route handlers enforce too: a middleware matcher that excludes /api/ (a common pattern)\n// must not leave the beacon endpoints serving blocked clients.\nfunction blocked(engine: Engine, req: Request): Response | null {\n const ip = resolveClientIp(null, req.headers.get('x-forwarded-for'), trustedProxy(engine));\n const path = new URL(req.url).pathname;\n // §D3: `ua` and `header` rules need these; `Headers.get` already matches names case-insensitively\n const v = engine.snap.verdict({ ip, path, ua: req.headers.get('user-agent'), header: (n) => req.headers.get(n) });\n if (!v.block) return null;\n const ev = buildEvent(req, path, ip, crypto.randomUUID(), cookieValue(req.headers.get('cookie') || '', SESSION_COOKIE), false);\n ev.st = 403; ev.blk = v.reason; // SDK-01: a beacon-route deny is a block like any other — it ships, unsampled\n if (v.rule) ev.rl = v.rule;\n engine.queue.push(ev);\n void engine.queue.flush();\n const headers: Record<string, string> = { 'x-block-reason': String(v.reason ?? ''), 'x-block-version': v.version ?? '' };\n if (v.rule) headers['x-block-rule'] = v.rule;\n return new Response('Forbidden', { status: 403, headers });\n}\n\nfunction lastSegment(req: Request): string {\n const parts = new URL(req.url).pathname.split('/');\n return parts[parts.length - 1];\n}\n\nfunction activeEngine(): Engine | null {\n return isDisabled() ? null : getEngine();\n}\n\n// tenant switch: both beacon endpoints stand down together (b.js and fp), as node's and hono's do\nconst beaconOff = (engine: Engine): boolean => engine.snap.config?.beacon === false;\n\nexport function camadaRoute(): {\n GET: (req: Request) => Promise<Response>;\n POST: (req: Request) => Promise<Response>;\n} {\n return {\n GET: (req) => guardedAsync(async () => {\n if (lastSegment(req) !== 'b.js') return notFound();\n const engine = activeEngine();\n if (!engine) return notFound();\n engine.snap.ensureFresh();\n const deny = blocked(engine, req);\n if (deny) return deny;\n if (beaconOff(engine)) return notFound();\n return new Response(iife, {\n status: 200,\n headers: { 'content-type': 'application/javascript', 'cache-control': 'public, max-age=3600' },\n });\n }, notFound()),\n\n POST: (req) => guardedAsync(async () => {\n if (lastSegment(req) === 'challenge') {\n const engine = activeEngine();\n if (!engine || !challengeEnabled()) return notFound();\n engine.snap.ensureFresh();\n const deny = blocked(engine, req); // a blocked client cannot buy its way past with a solution\n if (deny) return deny;\n return verifyChallenge(engine, req);\n }\n if (lastSegment(req) !== 'fp') return notFound();\n const body = await req.text();\n if (encoder.encode(body).byteLength > FP_MAX) return new Response(null, { status: 413 });\n const engine = activeEngine();\n if (engine) {\n engine.snap.ensureFresh();\n const deny = blocked(engine, req);\n if (deny) return deny;\n if (beaconOff(engine)) return notFound();\n const ip = resolveClientIp(null, req.headers.get('x-forwarded-for'), trustedProxy(engine));\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)) {\n engine.queue.push({ ...(parsed as Record<string, unknown>), sig: 1, ip, tap: TAP_NEXT });\n void engine.queue.flush(); // fire-and-forget, as the relay was: a serverless runtime may freeze right after the response\n }\n }\n return noContent();\n }, noContent()),\n };\n}\n","// <CamadaBeacon/> — an async React Server Component for the app-router layout <head>: reads\n// the middleware-stamped x-camada-rid request header (so the beacon POST joins the server's\n// wire event) and renders the @camada/react beacon tag pointing at the camada route.\n// 'next/headers' is imported lazily inside the component so this module only touches it\n// server-side, in a request scope.\nimport { CamadaBeacon as Beacon } from '@camada/react';\n\nexport interface CamadaBeaconProps {\n /** Where camadaRoute() serves the beacon script. Default: '/api/camada/b.js'. */\n src?: string;\n}\n\nexport async function CamadaBeacon({ src = '/api/camada/b.js' }: CamadaBeaconProps = {}) {\n let rid: string | null = null;\n try {\n const { headers } = await import('next/headers');\n rid = (await headers()).get('x-camada-rid');\n } catch { /* outside a request scope (static render, tests): beacon ships without a rid */ }\n return <Beacon src={src} rid={rid} />;\n}\n","// track() — app-context outcome events recorded from a server action, route handler, or server\n// component, joined to the middleware's wire event. The name is free-form; the analyst's rules read\n// this vocabulary: login_failed, login_succeeded, signup, password_reset, mfa_failed, payment_failed,\n// payment_succeeded, coupon_failed (@camada/node README has the table). Events are joined\n// through the x-camada-rid request header and the _sfp session cookie. The identifier is\n// HMAC-hashed in-process with the ingest token: the raw value never reaches the queue, and\n// the analyst drops anything that is not hash-shaped anyway.\n// next/headers is imported lazily inside the call (the beacon.tsx pattern) so this module\n// stays importable everywhere; calling track() outside a request scope is a no-op.\nimport { hashUserId, logRateLimited, resolveClientIp, TAP_NEXT } from '@camada/core';\nimport { getEngine, isDisabled, trustedProxy } from './engine';\n\nconst SESSION_COOKIE = '_sfp';\n\n/** Never throws, never blocks the response: a camada problem must not break a login. */\nexport async function track(event: string, data?: { user?: string }): Promise<void> {\n try {\n if (isDisabled()) return;\n const engine = getEngine();\n if (!engine) return;\n const { headers, cookies } = await import('next/headers');\n const [h, c] = await Promise.all([headers(), cookies()]);\n const uid = data?.user ? await hashUserId(data.user, engine.env.ingestToken) : null;\n engine.queue.push({\n tap: TAP_NEXT,\n et: event,\n uid,\n rid: h.get('x-camada-rid'),\n sid: c.get(SESSION_COOKIE)?.value ?? null,\n ip: resolveClientIp(null, h.get('x-forwarded-for'), trustedProxy(engine)),\n ts: Date.now(),\n });\n void engine.queue.flush(); // a serverless runtime may freeze right after the response; don't wait for the interval\n } catch (err) {\n logRateLimited(err); // outside a request scope, unconfigured, or a camada bug: swallow\n }\n}\n"],"mappings":"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","names":["import_core","import_core","import_core","name","engine","engine","import_core","engine","iife","Beacon","import_core","SESSION_COOKIE","engine"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/middleware.ts","../src/engine.ts","../package.json","../src/version.ts","../src/challenge.ts","../src/event.ts","../src/route.ts","../src/beacon.tsx","../src/track.ts","../src/wait-until.ts","../src/with-camada.ts"],"sourcesContent":["// @camada/next — the three-line install:\n// middleware.ts / proxy.ts: export default camada();\n// app/api/camada/[...camada]/route.ts: export const { GET, POST } = camadaRoute();\n// app/layout.tsx <head>: <CamadaBeacon/>\n// Plus track() for app-context outcomes from server actions / route handlers, and withCamada(handler)\n// for a route handler whose event should carry the real status and dur.\nexport { camada, type CamadaMiddlewareOptions, type MiddlewareResult } from './middleware';\nexport { camadaRoute } from './route';\nexport { challengeGate } from './challenge';\nexport { CamadaBeacon, type CamadaBeaconProps } from './beacon';\nexport { track } from './track';\nexport { withCamada } from './with-camada';\n","// The Next.js middleware/proxy entry: enforce the blocklist inline, stamp x-camada-rid onto\n// the forwarded request (the <CamadaBeacon/> server component reads it back), ship the wire\n// event fire-and-forget. Everything runs inside a try/catch fail-open envelope — a camada\n// bug must never break the customer's app (plan.md INT-2).\n//\n// MUST stay edge-runtime-safe: Web APIs only (fetch/Headers/Request/Response/URL/crypto),\n// no node: imports anywhere in this entry's import graph. Proven by test/edge-safety.test.ts,\n// which bundles this file for a bare edge runtime and executes it inside @edge-runtime/vm.\n// NextRequest/NextFetchEvent are imported as TYPES only; NextResponse comes from\n// 'next/server', which is itself edge-safe.\nimport type { NextFetchEvent, NextRequest } from 'next/server';\nimport { NextResponse } from 'next/server';\nimport { resolveClientIp, logRateLimited, guarded, guardedAsync } from '@camada/core';\nimport { getEngine, isDisabled, challengeEnabled, trustedProxy, type Engine } from './engine';\nimport { isChallengeRoute, challengePassed, serveChallenge } from './challenge';\nimport { buildEvent, cookieValue, middlewareMark, MW_HEADER, SESSION_COOKIE } from './event';\n\n/** camadaRoute()'s beacon endpoints (mounted at /api/camada/[...camada]). Camada's own, like core's scriptPath/fpPath:\n * after enforcement they are neither captured nor stamped with x-rid. */\nconst isBeaconRoute = (method: string, path: string): boolean =>\n (method === 'GET' && path === '/api/camada/b.js') || (method === 'POST' && path === '/api/camada/fp');\n\nexport interface CamadaMiddlewareOptions {\n // Reserved. The engine is configured via CAMADA_* environment variables.\n}\n\n/** Next accepts a promise here; the capture and challenge branches return one (they await WebCrypto). */\nexport type MiddlewareResult = Response | undefined | Promise<Response | undefined>;\n\n/** The ordinary path: ship one pre-response event, stamp the rid (and its proof for withCamada()),\n * mint the session cookie. `warnRule` is the id of the `warn` rule that let this request through, if one did (§D3). */\nasync function capture(engine: Engine, req: NextRequest, path: string, ip: string | null, existingSid: string | null, warnRule: string | null, waitUntil?: (p: Promise<unknown>) => void): Promise<Response> {\n const rid = crypto.randomUUID();\n const sid = existingSid ?? crypto.randomUUID();\n const cfg = engine.snap.config;\n const excluded = (cfg?.exclude || []).some((x) => path.startsWith(x));\n if (!excluded && Math.random() < (cfg?.sample ?? 1)) {\n // st stays null: middleware ships pre-response, like the edge collector's tap.\n const ev = buildEvent(req, path, ip, rid, sid, !existingSid);\n if (warnRule) ev.wrn = warnRule;\n engine.queue.push(ev);\n engine.queue.flush(waitUntil);\n }\n\n const headers = new Headers(req.headers);\n headers.set('x-camada-rid', rid);\n // WebCrypto, the one await on this path; without it the request still goes through, just unproven (withCamada then ships too).\n // The headers are a copy of the client's: drop any mark it sent before stamping ours.\n headers.delete(MW_HEADER);\n const mark = await middlewareMark(engine.env.secret, req, rid).catch(() => null);\n if (mark) headers.set(MW_HEADER, mark);\n const res = NextResponse.next({ request: { headers } });\n res.headers.set('x-rid', rid);\n if (!existingSid) {\n const secure = new URL(req.url).protocol === 'https:' ? '; Secure' : '';\n res.headers.append('set-cookie', `${SESSION_COOKIE}=${sid}; Path=/; Max-Age=2592000; HttpOnly; SameSite=Lax${secure}`);\n }\n return res;\n}\n\n/** Where camada lets a request through without capturing it: forward it without any x-camada-mw or x-camada-rid the\n * client sent, so the only mark and rid a route handler ever sees are ones this middleware made. Undefined (Next\n * continues untouched) when there is neither. */\nfunction passThrough(req: Request): Response | undefined {\n if (!req.headers.has(MW_HEADER) && !req.headers.has('x-camada-rid')) return undefined;\n const headers = new Headers(req.headers);\n headers.delete(MW_HEADER);\n headers.delete('x-camada-rid');\n return NextResponse.next({ request: { headers } });\n}\n\n/**\n * `export default camada();` from middleware.ts (Next ≤15) / proxy.ts (Next 16).\n * Returns undefined (Next continues) whenever camada is disabled, unconfigured, or broken; a client-sent\n * x-camada-mw or x-camada-rid is stripped on every path that forwards the request.\n */\nexport function camada(_options?: CamadaMiddlewareOptions): (req: NextRequest, event: NextFetchEvent) => MiddlewareResult {\n return function camadaMiddleware(req: NextRequest, event: NextFetchEvent): MiddlewareResult {\n try {\n if (isDisabled()) return passThrough(req);\n const engine = getEngine();\n if (!engine) return passThrough(req);\n const waitUntil = event?.waitUntil ? (p: Promise<unknown>) => event.waitUntil(p) : undefined;\n engine.snap.ensureFresh(waitUntil);\n\n // There is no socket peer at the edge: with no trusted-proxy config (or no XFF) the\n // ip resolves to null and ip rules simply don't enforce — a spoofed X-Forwarded-For\n // can never reach the blocklist. On Vercel the engine defaults to {mode:'vercel'}.\n const ip = resolveClientIp(null, req.headers.get('x-forwarded-for'), trustedProxy(engine));\n const path = new URL(req.url).pathname;\n\n // the same _sfp session as the collector and @camada/node: sid/ns comparable across taps\n const existingSid = cookieValue(req.headers.get('cookie') || '', SESSION_COOKIE);\n\n // The custom rules read the user agent and the request headers (§D3); without them every\n // `ua` and `header` condition is false. `Headers.get` is case-insensitive, so the\n // lower-cased name the matcher asks with finds whatever spelling the client sent.\n const v = engine.snap.verdict({\n ip, path, ua: req.headers.get('user-agent'), header: (n) => req.headers.get(n),\n }); // cold start fails open ('cold')\n if (v.block) {\n const ev = buildEvent(req, path, ip, crypto.randomUUID(), existingSid, false);\n ev.st = 403; // blocked requests always ship, unsampled\n ev.blk = v.reason; // SDK-01: the reason rides the event so the analyst counts SDK blocks, not the app's own 403s ('rule' when a rule decided)\n if (v.rule) ev.rl = v.rule;\n engine.queue.push(ev);\n engine.queue.flush(waitUntil);\n const headers: Record<string, string> = {\n 'content-type': 'text/plain',\n 'x-block-reason': String(v.reason ?? ''),\n '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 // `warn` passes the request and only marks its event; a skip passes with nothing\n // stamped — it is the absence of enforcement.\n const warnRule = v.warn ? v.rule ?? null : null;\n\n // A challenge verdict: serve the proof-of-work page unless this client already passed.\n // The verify route answers its own endpoint, so never challenge that path. A challenge\n // needs a resolved ip (the nonce and `_cch` are bound to it) — without one, fail open,\n // the same stance ip rules take at this position.\n // A client that HAS passed falls through to the normal capture path: it keeps its rid,\n // its session cookie and its event, so an hour of `_cch` is not an hour of blindness.\n // guardedAsync covers the whole branch — the synchronous catch below cannot see a\n // rejection from these awaits, and WebCrypto is not guaranteed to exist.\n if (v.challenge && ip && challengeEnabled() && !isChallengeRoute(path)) {\n return guardedAsync(async () => (\n (await challengePassed(engine, req, ip))\n ? capture(engine, req, path, ip, existingSid, warnRule, waitUntil)\n : serveChallenge(engine, req, ip, path + new URL(req.url).search, waitUntil)\n ), passThrough(req));\n }\n\n if (isBeaconRoute(req.method, path)) return passThrough(req); // no row, no x-rid: the route handler answers it\n\n return guardedAsync(() => capture(engine, req, path, ip, existingSid, warnRule, waitUntil), passThrough(req));\n } catch (err) {\n logRateLimited(err); // fail open: the app proceeds as if camada were not installed\n return guarded(() => passThrough(req), undefined);\n }\n };\n}\n","// The lazy singleton engine behind every @camada/next entry point: one SnapshotClient in\n// 'lazy' mode (edge runtime — ensureFresh per request, no interval timers) plus one\n// EventQueue, wired from the environment on first use. Unconfigured -> engine is null, every\n// entry point stays inert, and one rate-limited console.error says why. CAMADA_DISABLED=1 is\n// checked per request (isDisabled), never memoized here.\n//\n// This module is part of the middleware's import graph and therefore MUST stay\n// edge-runtime-safe: Web APIs and process.env reads only, no node: imports.\nimport {\n SnapshotClient, EventQueue, parseKey, parseTrustedProxyEnv, logRateLimited,\n type SnapshotVersion, type TrustedProxyConfig,\n} from '@camada/core';\n\nimport { SDK_ID } from './version';\n\nexport { parseTrustedProxyEnv }; // re-export for callers that had reached into this module\n\nexport interface ResolvedEnv {\n ingestToken: string;\n snapToken: string;\n secret: string; // HMAC key for the challenge nonce/cookie — never leaves the process\n ingestUrl: string;\n snapshotUrl: string;\n trustedProxy: TrustedProxyConfig | null; // null = defer to server-delivered config\n}\n\n/** Returns null (SDK stays inert, one log line) rather than throwing on bad config. */\nexport function resolveEnv(env: Record<string, string | undefined>): ResolvedEnv | null {\n const key = parseKey(env.CAMADA_KEY);\n const ingestToken = key?.ingestToken ?? env.CAMADA_TOKEN;\n const snapToken = key?.snapToken ?? env.CAMADA_SNAPSHOT_TOKEN;\n if (!ingestToken || !snapToken) return null;\n const ingestUrl = (env.CAMADA_INGEST_URL || 'https://in.camada.app').replace(/\\/$/, '');\n return {\n ingestToken, snapToken, ingestUrl,\n secret: env.CAMADA_KEY || `${ingestToken}.${snapToken}`,\n snapshotUrl: env.CAMADA_SNAPSHOT_URL || `${ingestUrl}/snapshot`,\n // On Vercel the platform overwrites X-Forwarded-For, so its rightmost entry is\n // trustworthy: default to vercel mode there unless explicitly overridden.\n trustedProxy: parseTrustedProxyEnv(env.CAMADA_TRUSTED_PROXY) ?? (env.VERCEL ? { mode: 'vercel' } : null),\n };\n}\n\nexport interface Engine {\n env: ResolvedEnv;\n snap: SnapshotClient;\n queue: EventQueue;\n fetchImpl: typeof fetch; // for the beacon relay; snapshot/queue carry their own copy\n}\n\nexport interface ConfigureOptions {\n env?: Record<string, string | undefined>;\n fetchImpl?: typeof fetch;\n challenge?: boolean; // enforce `challenge` verdicts with the first-party page (default true)\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}\n\nlet engine: Engine | null | undefined; // undefined = not built yet; null = unconfigured\nlet overrides: ConfigureOptions = {};\n\n/** Internal test/reset hook (mirrors @camada/node's configure): replaces the singleton. */\nexport function configure(opts: ConfigureOptions = {}): void {\n engine?.snap.stop();\n engine?.queue.stop();\n overrides = opts;\n engine = undefined;\n}\n\nexport function envSource(): Record<string, string | undefined> {\n return overrides.env ?? (typeof process !== 'undefined' && process.env ? process.env : {});\n}\n\n/** Kill switch, checked per request so an env flip disables camada without a code change. */\nexport function isDisabled(): boolean {\n return envSource().CAMADA_DISABLED === '1';\n}\n\n/** SDK-04 is on unless the app opts out in code or with CAMADA_CHALLENGE=0. */\nexport function challengeEnabled(): boolean {\n return overrides.challenge !== false && envSource().CAMADA_CHALLENGE !== '0';\n}\n\nexport function getEngine(): Engine | null {\n if (engine !== undefined) return engine;\n const env = resolveEnv(envSource());\n if (!env) {\n logRateLimited(new Error('CAMADA_KEY (or CAMADA_TOKEN + CAMADA_SNAPSHOT_TOKEN) not set — camada is inactive'));\n return (engine = null);\n }\n const fetchImpl = overrides.fetchImpl;\n const injected = fetchImpl ? { fetchImpl } : {}; // never pass an explicit undefined key\n engine = {\n env,\n fetchImpl: fetchImpl ?? fetch,\n // snapshotVersion left undefined takes core's default (5); the client drops undefined keys.\n snap: new SnapshotClient({ url: env.snapshotUrl, token: env.snapToken, mode: 'lazy', sdk: SDK_ID, snapshotVersion: overrides.snapshotVersion, ...injected }),\n queue: new EventQueue({ url: env.ingestUrl, token: env.ingestToken, sdk: SDK_ID, ...injected }),\n };\n return engine;\n}\n\n/** The trusted-proxy config in force: explicit local/env override wins, then the\n * server-delivered tenant config, then none (socket-less edge -> ip stays null). */\nexport function trustedProxy(e: Engine): TrustedProxyConfig | null {\n return e.env.trustedProxy ?? e.snap.config?.trusted_proxy ?? null;\n}\n","{\n \"version\": \"0.3.0\",\n \"license\": \"MIT\",\n \"type\": \"module\",\n \"engines\": {\n \"node\": \">=20\"\n },\n \"files\": [\n \"dist\",\n \"README.md\",\n \"LICENSE\"\n ],\n \"scripts\": {\n \"build\": \"tsup\",\n \"dev\": \"tsup --watch\",\n \"test\": \"vitest run\",\n \"check\": \"tsc --noEmit\"\n },\n \"name\": \"@camada/next\",\n \"description\": \"camada backend SDK for Next.js: edge-safe middleware/proxy, /api/camada route handlers, <CamadaBeacon/> with the request rid\",\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 \"@camada/react\": \"file:../camada-react\"\n },\n \"peerDependencies\": {\n \"next\": \">=15\",\n \"react\": \">=18\"\n },\n \"devDependencies\": {\n \"typescript\": \"^5.7.2\",\n \"tsup\": \"^8.3.5\",\n \"vitest\": \"^3.0.0\",\n \"next\": \"^16.0.0\",\n \"react\": \"^19.0.0\",\n \"@types/react\": \"^19.0.0\",\n \"@edge-runtime/vm\": \"^5.0.0\",\n \"esbuild\": \"^0.24.0\",\n \"@types/node\": \"^22.10.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","// The challenge for @camada/next (SDK-04). Edge-runtime-safe: Web APIs and WebCrypto only, no\n// node: imports — this module sits in the middleware's import graph, which\n// test/edge-safety.test.ts bundles for a bare edge runtime.\n//\n// The page is served by the middleware (or by challengeGate() from a route the app gates\n// itself) and posts a form to POST /api/camada/challenge, handled by camadaRoute().\nimport {\n createChallengeAsync, challengePage, challengeCookie, safeReturnTo, wantsHtml, parseFormBody,\n resolveClientIp, CHALLENGE_COOKIE, type AsyncChallengeKit,\n} from '@camada/core';\nimport { getEngine, isDisabled, challengeEnabled, trustedProxy, type Engine } from './engine';\nimport { buildEvent, cookieValue, middlewareMarkValid, SESSION_COOKIE } from './event';\n\n/** Where the page posts. The catch-all route handler must be mounted at /api/camada/[...camada]\n * (the documented install) — the served page hard-codes this action. */\nexport const VERIFY_PATH = '/api/camada/challenge';\n\nconst BODY_MAX = 4 * 1024; // the verify form is ~120 bytes; anything larger is not ours\n\n/** The app may mount the catch-all elsewhere, so the middleware matches on the tail. */\nexport const isChallengeRoute = (path: string): boolean => /\\/camada\\/challenge\\/?$/.test(path);\n\n// One kit per engine; the engine is replaced wholesale by configure(), so the map never grows.\nconst kits = new WeakMap<Engine, AsyncChallengeKit>();\nexport function kitFor(engine: Engine): AsyncChallengeKit {\n let k = kits.get(engine);\n if (!k) { k = createChallengeAsync({ secret: engine.env.secret }); kits.set(engine, k); }\n return k;\n}\n\nexport const clientIp = (engine: Engine, req: Request): string | null =>\n resolveClientIp(null, req.headers.get('x-forwarded-for'), trustedProxy(engine));\n\nexport function challengePassed(engine: Engine, req: Request, ip: string): Promise<boolean> {\n return kitFor(engine).tokenValid(ip, Date.now(), cookieValue(req.headers.get('cookie') || '', CHALLENGE_COOKIE));\n}\n\n/** 403 + the proof-of-work page (HTML navigations) or 403 JSON (everything else), plus the\n * `blk: \"challenge\"` event — a served challenge is reported like a block (contract §D2). */\nexport async function serveChallenge(engine: Engine, req: Request, ip: string, target: string, waitUntil?: (p: Promise<unknown>) => void, ship = true): Promise<Response> {\n if (ship) {\n const ev = buildEvent(req, new URL(req.url).pathname, ip, crypto.randomUUID(), cookieValue(req.headers.get('cookie') || '', SESSION_COOKIE), false);\n ev.st = 403;\n ev.blk = 'challenge';\n engine.queue.push(ev);\n engine.queue.flush(waitUntil);\n }\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 kitFor(engine).nonce(ip, Date.now()), action: VERIFY_PATH, 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 handler for `…/camada/challenge`: validate, set `_cch`, 302 back, ship `{ st: 200, ch: 1 }`. */\nexport async function verifyChallenge(engine: Engine, req: Request): Promise<Response> {\n const url = new URL(req.url);\n const ip = clientIp(engine, req);\n const kit = kitFor(engine);\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 (!ip || !(await kit.verify(ip, now, form.nonce, form.solution))) {\n const html = ip ? challengePage({ nonce: await kit.nonce(ip, now), action: url.pathname, to }) : 'Forbidden';\n return new Response(html, { status: 403, headers: { 'content-type': ip ? 'text/html; charset=utf-8' : 'text/plain', 'cache-control': 'no-store' } });\n }\n const ev = buildEvent(req, url.pathname, ip, crypto.randomUUID(), cookieValue(req.headers.get('cookie') || '', SESSION_COOKIE), false);\n ev.st = 200;\n ev.ch = 1; // challenge passed (contract §A3 ingest field)\n engine.queue.push(ev);\n void engine.queue.flush();\n return new Response(null, {\n status: 302,\n headers: {\n location: to,\n 'set-cookie': challengeCookie(await kit.issue(ip, now), url.protocol === 'https:'),\n 'cache-control': 'no-store',\n },\n });\n}\n\n/** For a route that wants to gate itself (the example's /challenge-me): the challenge Response,\n * or null when this client already holds a valid `_cch` and the route should render normally.\n *\n * One request, one event. The middleware stamps `x-camada-rid` and its `x-camada-mw` proof on\n * the request it forwards, so a mark that verifies means the middleware already shipped this\n * request's row (a client-sent rid alone proves nothing); the gate then serves\n * the page WITHOUT a second row, which would double-count the request at the analyst. Exclude\n * the gated route from the middleware matcher and the gate ships the `blk: \"challenge\"` row\n * itself — that is the configuration to use when you want the challenge counted. */\nexport async function challengeGate(req: Request): Promise<Response | null> {\n if (isDisabled() || !challengeEnabled()) return null;\n const engine = getEngine();\n if (!engine) return null;\n const ip = clientIp(engine, req);\n if (!ip || await challengePassed(engine, req, ip)) return null; // unidentifiable client: fail open\n const url = new URL(req.url);\n const shipped = await middlewareMarkValid(engine.env.secret, req).catch(() => false);\n return serveChallenge(engine, req, ip, url.pathname + url.search, undefined, !shipped);\n}\n","// One wire-event builder for every @camada/next entry (middleware + beacon route). Web APIs\n// only: this sits in the middleware's edge-safe import graph.\nimport { buildWireEvent, hmacHex, safeEqual, TAP_NEXT, type WireEvent } from '@camada/core';\n\nexport function buildEvent(req: Request, path: string, ip: string | null, rid: string, sid: string | null, newSession: boolean): WireEvent {\n const url = new URL(req.url);\n return buildWireEvent(\n {\n method: req.method,\n host: req.headers.get('host') ?? url.host,\n path,\n query: url.search,\n // The edge runtime sorts header names, so hord is alphabetical at this tap — still\n // shipped; the scorer knows sdk-next lacks the raw-wire-order signal (capability mask).\n headers: [...req.headers.entries()],\n ip,\n httpVersion: null, // not observable at this position\n },\n { tap: TAP_NEXT, rid, sid, newSession, ja4: req.headers.get('x-vercel-ja4-digest') },\n );\n}\n\n/** Stamped by the middleware next to x-camada-rid on the request it forwards: proof, for withCamada(), that this request's\n * event already shipped. It only ever spares the wrapper a second event; enforcement never depends on it. */\nexport const MW_HEADER = 'x-camada-mw';\nexport const MW_WINDOW_MS = 30_000; // middleware → route handler takes milliseconds; 30 s covers a cold function start\nconst MW_SKEW_MS = 5_000; // the middleware's clock may run ahead of the function's (edge vs. Node on Vercel)\n\n/** What the mark binds: when it was made, the method, the path and query, the rid, and the client chain as forwarded.\n * JSON keeps the fields apart, since the rid and X-Forwarded-For are whatever the client sent where the middleware did not run. */\nconst markInput = (ts: number, req: Request, rid: string): string => {\n const url = new URL(req.url);\n return 'mw:' + JSON.stringify([ts, req.method, url.pathname + url.search, rid, req.headers.get('x-forwarded-for') ?? '']);\n};\n\n/** `<ms timestamp>.<HMAC-SHA256 hex>` under the server-only secret. */\nexport async function middlewareMark(secret: string, req: Request, rid: string, now = Date.now()): Promise<string> {\n return `${now}.${await hmacHex(secret, markInput(now, req, rid))}`;\n}\n\n/** True only for a mark this deployment's middleware made for this very request line and client, within the window.\n * A copied pair fails on another path, query, method or client, and on any request once the window has passed. */\nexport async function middlewareMarkValid(secret: string, req: Request, now = Date.now()): Promise<boolean> {\n const m = /^(\\d{13})\\.([0-9a-f]{64})$/.exec(req.headers.get(MW_HEADER) ?? '');\n const rid = req.headers.get('x-camada-rid');\n if (!m || !rid) return false;\n const ts = Number(m[1]);\n if (now - ts > MW_WINDOW_MS || ts - now > MW_SKEW_MS) return false;\n return safeEqual(m[2], await hmacHex(secret, markInput(ts, req, rid)));\n}\n\nexport const SESSION_COOKIE = '_sfp'; // the same session cookie as the edge collector and @camada/node: sid/ns comparable across taps\n\nexport const 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","// Route handlers for app/api/camada/[...camada]/route.ts:\n// GET …/b.js -> the first-party beacon IIFE (its auto-init derives the fp endpoint from\n// the script URL's final path segment and the rid from ?r=)\n// POST …/fp -> queue the beacon as a sig:1 row with the trusted-proxy-resolved client IP\n// and tap 'sdk-next': it rides the event batch (one request per flush at the\n// analyst, not one per page view) — the mirror of @camada/node's /_cam/*.\n// Works on BOTH runtimes (edge and node): Web APIs only, no node: imports. Unconfigured or\n// disabled: GET 404s, POST answers 204 and drops — inert, never an error.\nimport iife from '@camada/browser/iife-string';\nimport { guardedAsync, resolveClientIp, TAP_NEXT } from '@camada/core';\nimport { getEngine, isDisabled, challengeEnabled, trustedProxy, type Engine } from './engine';\nimport { verifyChallenge } from './challenge';\nimport { buildEvent, cookieValue, SESSION_COOKIE } from './event';\n\nconst FP_MAX = 32 * 1024; // matches the server's /fp cap: never accept what ingest will 413\nconst encoder = new TextEncoder();\n\nconst notFound = () => new Response(null, { status: 404 });\nconst noContent = () => new Response(null, { status: 204, headers: { 'cache-control': 'no-store' } });\n\n// The route handlers enforce too: a middleware matcher that excludes /api/ (a common pattern)\n// must not leave the beacon endpoints serving blocked clients.\nfunction blocked(engine: Engine, req: Request): Response | null {\n const ip = resolveClientIp(null, req.headers.get('x-forwarded-for'), trustedProxy(engine));\n const path = new URL(req.url).pathname;\n // §D3: `ua` and `header` rules need these; `Headers.get` already matches names case-insensitively\n const v = engine.snap.verdict({ ip, path, ua: req.headers.get('user-agent'), header: (n) => req.headers.get(n) });\n if (!v.block) return null;\n const ev = buildEvent(req, path, ip, crypto.randomUUID(), cookieValue(req.headers.get('cookie') || '', SESSION_COOKIE), false);\n ev.st = 403; ev.blk = v.reason; // SDK-01: a beacon-route deny is a block like any other — it ships, unsampled\n if (v.rule) ev.rl = v.rule;\n engine.queue.push(ev);\n void engine.queue.flush();\n const headers: Record<string, string> = { 'x-block-reason': String(v.reason ?? ''), 'x-block-version': v.version ?? '' };\n if (v.rule) headers['x-block-rule'] = v.rule;\n return new Response('Forbidden', { status: 403, headers });\n}\n\nfunction lastSegment(req: Request): string {\n const parts = new URL(req.url).pathname.split('/');\n return parts[parts.length - 1];\n}\n\nfunction activeEngine(): Engine | null {\n return isDisabled() ? null : getEngine();\n}\n\n// tenant switch: both beacon endpoints stand down together (b.js and fp), as node's and hono's do\nconst beaconOff = (engine: Engine): boolean => engine.snap.config?.beacon === false;\n\nexport function camadaRoute(): {\n GET: (req: Request) => Promise<Response>;\n POST: (req: Request) => Promise<Response>;\n} {\n return {\n GET: (req) => guardedAsync(async () => {\n if (lastSegment(req) !== 'b.js') return notFound();\n const engine = activeEngine();\n if (!engine) return notFound();\n engine.snap.ensureFresh();\n const deny = blocked(engine, req);\n if (deny) return deny;\n if (beaconOff(engine)) return notFound();\n return new Response(iife, {\n status: 200,\n headers: { 'content-type': 'application/javascript', 'cache-control': 'public, max-age=3600' },\n });\n }, notFound()),\n\n POST: (req) => guardedAsync(async () => {\n if (lastSegment(req) === 'challenge') {\n const engine = activeEngine();\n if (!engine || !challengeEnabled()) return notFound();\n engine.snap.ensureFresh();\n const deny = blocked(engine, req); // a blocked client cannot buy its way past with a solution\n if (deny) return deny;\n return verifyChallenge(engine, req);\n }\n if (lastSegment(req) !== 'fp') return notFound();\n const body = await req.text();\n if (encoder.encode(body).byteLength > FP_MAX) return new Response(null, { status: 413 });\n const engine = activeEngine();\n if (engine) {\n engine.snap.ensureFresh();\n const deny = blocked(engine, req);\n if (deny) return deny;\n if (beaconOff(engine)) return notFound();\n const ip = resolveClientIp(null, req.headers.get('x-forwarded-for'), trustedProxy(engine));\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)) {\n engine.queue.push({ ...(parsed as Record<string, unknown>), sig: 1, ip, tap: TAP_NEXT });\n void engine.queue.flush(); // fire-and-forget, as the relay was: a serverless runtime may freeze right after the response\n }\n }\n return noContent();\n }, noContent()),\n };\n}\n","// <CamadaBeacon/> — an async React Server Component for the app-router layout <head>: reads\n// the middleware-stamped x-camada-rid request header (so the beacon POST joins the server's\n// wire event) and renders the @camada/react beacon tag pointing at the camada route.\n// 'next/headers' is imported lazily inside the component so this module only touches it\n// server-side, in a request scope.\nimport { CamadaBeacon as Beacon } from '@camada/react';\n\nexport interface CamadaBeaconProps {\n /** Where camadaRoute() serves the beacon script. Default: '/api/camada/b.js'. */\n src?: string;\n}\n\nexport async function CamadaBeacon({ src = '/api/camada/b.js' }: CamadaBeaconProps = {}) {\n let rid: string | null = null;\n try {\n const { headers } = await import('next/headers');\n rid = (await headers()).get('x-camada-rid');\n } catch { /* outside a request scope (static render, tests): beacon ships without a rid */ }\n return <Beacon src={src} rid={rid} />;\n}\n","// track() — app-context outcome events recorded from a server action, route handler, or server\n// component, joined to the middleware's wire event. The name is free-form; the analyst's rules read\n// this vocabulary: login_failed, login_succeeded, signup, password_reset, mfa_failed, payment_failed,\n// payment_succeeded, coupon_failed (@camada/node README has the table). Inside withCamada() an\n// event joins on the wrapper's own rid and session; elsewhere on the x-camada-rid the middleware\n// stamps and the _sfp session cookie. The identifier is\n// HMAC-hashed in-process with the ingest token: the raw value never reaches the queue, and\n// the analyst drops anything that is not hash-shaped anyway.\n// next/headers is imported lazily inside the call (the beacon.tsx pattern) so this module\n// stays importable everywhere; calling track() outside a request scope is a no-op.\nimport { hashUserId, logRateLimited, resolveClientIp, TAP_NEXT } from '@camada/core';\nimport { getEngine, isDisabled, trustedProxy } from './engine';\nimport { requestWaitUntil } from './wait-until';\n\nconst SESSION_COOKIE = '_sfp';\n\ninterface Scope { rid: string; sid: string | null }\ninterface Als { run<R>(store: Scope, fn: () => R): R; getStore(): Scope | undefined }\nlet als: Als | null | undefined;\n/** Next puts AsyncLocalStorage on globalThis in both runtimes (node-environment-baseline, the edge sandbox). */\nconst scopes = (): Als | null => {\n if (als === undefined) {\n const Ctor = (globalThis as { AsyncLocalStorage?: new () => Als }).AsyncLocalStorage;\n als = Ctor ? new Ctor() : null; // ponytail: without it (never under Next) a wrapped route's track() falls back to the header\n }\n return als;\n};\n\n/** withCamada() runs its handler here, so track() inside it takes the wrapper's rid and never a\n * client-sent x-camada-rid: a wrapped route sits outside the matcher, where nothing strips it. */\nexport const withTrackScope = <R>(scope: Scope, fn: () => R): R => { const s = scopes(); return s ? s.run(scope, fn) : fn(); };\n\n/** Never throws, never blocks the response: a camada problem must not break a login. */\nexport async function track(event: string, data?: { user?: string }): Promise<void> {\n try {\n if (isDisabled()) return;\n const engine = getEngine();\n if (!engine) return;\n const { headers, cookies } = await import('next/headers');\n const [h, c] = await Promise.all([headers(), cookies()]);\n const scope = scopes()?.getStore();\n const uid = data?.user ? await hashUserId(data.user, engine.env.ingestToken) : null;\n engine.queue.push({\n tap: TAP_NEXT,\n et: event,\n uid,\n rid: scope ? scope.rid : h.get('x-camada-rid'),\n sid: scope?.sid ?? c.get(SESSION_COOKIE)?.value ?? null,\n ip: resolveClientIp(null, h.get('x-forwarded-for'), trustedProxy(engine)),\n ts: Date.now(),\n });\n void engine.queue.flush(requestWaitUntil()); // a serverless runtime may freeze right after the response: hold it open for the flush\n } catch (err) {\n logRateLimited(err); // outside a request scope, unconfigured, or a camada bug: swallow\n }\n}\n","// A waitUntil for code that runs inside a route handler or server action (withCamada, track()). A\n// serverless host (Vercel) may freeze the function once the response has gone out, and the event's\n// flush only starts then, when the body is done. Next's after() (15.1+) holds the function open\n// until its callback settles, but it must be called inside the request scope, which the body-done\n// callback is not. So the first promise handed in registers ONE after() callback; it runs once the\n// response has closed and drains every promise handed in, including any that arrive while it waits.\n// Without after() (Next 15.0, or outside a request scope) the host's request context waitUntil is\n// used, the one @vercel/functions reads; with neither, the promise is left to run, which a\n// long-lived `next start` process does anyway.\nimport * as nextServer from 'next/server';\n\ntype WaitUntil = (p: Promise<unknown>) => void;\ntype After = (task: () => Promise<unknown>) => void;\ntype RequestContext = { get?: () => { waitUntil?: WaitUntil } | undefined };\n\nfunction contextWaitUntil(): WaitUntil | undefined {\n const g = globalThis as Record<symbol, RequestContext | undefined>;\n for (const name of ['@next/request-context', '@vercel/request-context']) {\n const ctx = g[Symbol.for(name)]?.get?.();\n if (typeof ctx?.waitUntil === 'function') return (p) => ctx.waitUntil!(p);\n }\n return undefined;\n}\n\n/** Call once per request, inside its scope; hand the result to everything that flushes for it. Never throws. */\nexport function requestWaitUntil(): WaitUntil {\n const pending: Promise<unknown>[] = [];\n let hold: WaitUntil | null | undefined; // undefined = not decided yet; null = nothing to hold the host open with\n const drain = async () => {\n for (let i = 0; i < pending.length; i++) await pending[i].catch(() => {});\n };\n return (p) => {\n if (hold === undefined) {\n hold = null;\n try {\n const after = (nextServer as { after?: After }).after;\n if (typeof after !== 'function') throw new Error('no after()');\n after(drain); // throws outside a request scope\n hold = (q) => { pending.push(q); };\n } catch {\n try { hold = contextWaitUntil() ?? null; } catch { hold = null; }\n }\n }\n try { hold?.(p); } catch { /* a host that refuses the promise costs only the hold */ }\n };\n}\n","// withCamada(handler): the route-handler position, where the response is visible. The middleware\n// runs before the route and ships its event pre-response (st and dur null); a wrapped route\n// handler runs @camada/core's fetch pipeline itself (verdict, block, challenge, the session) and\n// ships one event with the real status and a dur up to the response (a text/event-stream body to its last byte).\n//\n// Threat model. Every request header can come from the client: the middleware does not run for a\n// route left out of its matcher, and Next offers no server-only channel from the middleware to the\n// route (on Vercel they run in different processes, often different regions). So:\n// - Enforcement here never depends on a header. Verdict, block and challenge run on every request.\n// - The one thing a header may decide is whether this request's event already shipped. Wrapped\n// routes belong outside the matcher; if the middleware ran anyway it stamps `x-camada-mw`, an\n// HMAC under the server-only CAMADA_KEY of a timestamp, the method, the path and query, the rid\n// and X-Forwarded-For. A mark that verifies within 30 s skips the second event and the second\n// session cookie, nothing else. A copied mark fails on another path, query, method or client\n// and after the window; within it, it can at most hide a replay's event, never pass a block.\n// - The middleware strips any x-camada-mw the client sent before forwarding.\n// - track() inside the handler takes this wrapper's rid and session (withTrackScope), never a\n// client-sent x-camada-rid; only a request the middleware already shipped keeps its stamped one.\nimport iife from '@camada/browser/iife-string';\nimport { logRateLimited, TAP_NEXT } from '@camada/core';\nimport { createFetchCamada, withSetCookie } from '@camada/core/fetch';\nimport { envSource, getEngine } from './engine';\nimport { VERIFY_PATH } from './challenge';\nimport { middlewareMarkValid } from './event';\nimport { requestWaitUntil } from './wait-until';\nimport { withTrackScope } from './track';\nimport { SDK_ID } from './version';\n\nconst cam = createFetchCamada({ tap: TAP_NEXT, sdk: SDK_ID, iife }, { challengePath: VERIFY_PATH }); // lazy mode: edge and serverless\n\nasync function middlewareShipped(req: Request): Promise<boolean> {\n const secret = getEngine()?.env.secret;\n return !!secret && (await middlewareMarkValid(secret, req));\n}\n\n/** Next's redirect() / notFound() throw an error whose digest ends in the status they answer with. */\nconst thrownStatus = (err: unknown): number =>\n Number(/;(\\d{3});?$/.exec(String((err as { digest?: unknown } | null)?.digest ?? ''))?.[1] ?? 500);\n\n/**\n * `export const GET = withCamada(async (req) => Response.json(...))` in a route.ts. Same\n * CAMADA_* env as the middleware; inert without a key or with CAMADA_DISABLED=1. Never breaks\n * the handler: a camada failure costs the event, not the response.\n */\nexport function withCamada<R extends Request, A extends unknown[]>(\n handler: (req: R, ...rest: A) => Response | Promise<Response>,\n): (req: R, ...rest: A) => Promise<Response> {\n return async (req, ...rest) => {\n const env = envSource();\n // On Vercel the platform overwrites X-Forwarded-For, so its rightmost entry is trustworthy: the middleware's default too.\n const r = await cam.before(req, {\n env: { ...env, CAMADA_TRUSTED_PROXY: env.CAMADA_TRUSTED_PROXY || (env.VERCEL ? 'vercel' : undefined) },\n waitUntil: requestWaitUntil(), // the event flushes after the body: hold a serverless function open for it\n });\n if (!r) return handler(req, ...rest);\n if (r.response) return r.response; // block, challenge: whatever headers the request carries\n let shipped = false;\n try { shipped = await middlewareShipped(req); } catch (err) { logRateLimited(err); }\n if (shipped) {\n logRateLimited(new Error(`withCamada: the middleware also matched ${new URL(req.url).pathname}, so its pre-response event stands and dur is not reported; exclude wrapped routes from the matcher`));\n return handler(req, ...rest); // and its _sfp cookie: no second session\n }\n let res: Response;\n try {\n res = await withTrackScope({ rid: r.vars.rid, sid: r.vars.sid }, () => handler(req, ...rest));\n } catch (err) {\n cam.after(req, r.vars, thrownStatus(err));\n throw err;\n }\n res = withRid(res, r.vars.rid);\n return cam.finish(req, r.vars, r.vars.sessionCookie ? withSetCookie(res, r.vars.sessionCookie) : res);\n };\n}\n\n/** x-rid, the rid of this request's row, the same header the middleware stamps. Set in place, or on a copy when the\n * headers are immutable (`Response.redirect()`, a `fetch()` result), as withSetCookie does: same status and body.\n * A 101 upgrade is left alone. Not on a request the middleware shipped: its own x-rid, that row's, reaches the client. */\nfunction withRid(res: Response, rid: string): Response {\n if (res.status === 101) return res;\n try {\n res.headers.set('x-rid', rid);\n return res;\n } catch {\n // ponytail: core's copyResponse is not exported; its Deno and @hono/node-server cases never reach a Next route.\n const out = new Response(res.body, res);\n out.headers.set('x-rid', rid);\n return out;\n }\n}\n"],"mappings":"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","names":["import_core","import_core","import_core","name","engine","engine","import_core","engine","iife","Beacon","import_core","name","after","SESSION_COOKIE","engine","import_iife_string","import_core","iife"]}
package/dist/index.d.cts CHANGED
@@ -3,11 +3,12 @@ import * as react from 'react';
3
3
 
4
4
  interface CamadaMiddlewareOptions {
5
5
  }
6
- /** Next accepts a promise here; only the challenge branch returns one (it awaits WebCrypto). */
6
+ /** Next accepts a promise here; the capture and challenge branches return one (they await WebCrypto). */
7
7
  type MiddlewareResult = Response | undefined | Promise<Response | undefined>;
8
8
  /**
9
9
  * `export default camada();` from middleware.ts (Next ≤15) / proxy.ts (Next 16).
10
- * Returns undefined (Next continues) whenever camada is disabled, unconfigured, or broken.
10
+ * Returns undefined (Next continues) whenever camada is disabled, unconfigured, or broken; a client-sent
11
+ * x-camada-mw or x-camada-rid is stripped on every path that forwards the request.
11
12
  */
12
13
  declare function camada(_options?: CamadaMiddlewareOptions): (req: NextRequest, event: NextFetchEvent) => MiddlewareResult;
13
14
 
@@ -19,8 +20,9 @@ declare function camadaRoute(): {
19
20
  /** For a route that wants to gate itself (the example's /challenge-me): the challenge Response,
20
21
  * or null when this client already holds a valid `_cch` and the route should render normally.
21
22
  *
22
- * One request, one event. The middleware stamps `x-camada-rid` on the request it forwards, so
23
- * its presence means the middleware already shipped this request's row; the gate then serves
23
+ * One request, one event. The middleware stamps `x-camada-rid` and its `x-camada-mw` proof on
24
+ * the request it forwards, so a mark that verifies means the middleware already shipped this
25
+ * request's row (a client-sent rid alone proves nothing); the gate then serves
24
26
  * the page WITHOUT a second row, which would double-count the request at the analyst. Exclude
25
27
  * the gated route from the middleware matcher and the gate ships the `blk: "challenge"` row
26
28
  * itself — that is the configuration to use when you want the challenge counted. */
@@ -37,4 +39,11 @@ declare function track(event: string, data?: {
37
39
  user?: string;
38
40
  }): Promise<void>;
39
41
 
40
- export { CamadaBeacon, type CamadaBeaconProps, type CamadaMiddlewareOptions, type MiddlewareResult, camada, camadaRoute, challengeGate, track };
42
+ /**
43
+ * `export const GET = withCamada(async (req) => Response.json(...))` in a route.ts. Same
44
+ * CAMADA_* env as the middleware; inert without a key or with CAMADA_DISABLED=1. Never breaks
45
+ * the handler: a camada failure costs the event, not the response.
46
+ */
47
+ declare function withCamada<R extends Request, A extends unknown[]>(handler: (req: R, ...rest: A) => Response | Promise<Response>): (req: R, ...rest: A) => Promise<Response>;
48
+
49
+ export { CamadaBeacon, type CamadaBeaconProps, type CamadaMiddlewareOptions, type MiddlewareResult, camada, camadaRoute, challengeGate, track, withCamada };
package/dist/index.d.ts CHANGED
@@ -3,11 +3,12 @@ import * as react from 'react';
3
3
 
4
4
  interface CamadaMiddlewareOptions {
5
5
  }
6
- /** Next accepts a promise here; only the challenge branch returns one (it awaits WebCrypto). */
6
+ /** Next accepts a promise here; the capture and challenge branches return one (they await WebCrypto). */
7
7
  type MiddlewareResult = Response | undefined | Promise<Response | undefined>;
8
8
  /**
9
9
  * `export default camada();` from middleware.ts (Next ≤15) / proxy.ts (Next 16).
10
- * Returns undefined (Next continues) whenever camada is disabled, unconfigured, or broken.
10
+ * Returns undefined (Next continues) whenever camada is disabled, unconfigured, or broken; a client-sent
11
+ * x-camada-mw or x-camada-rid is stripped on every path that forwards the request.
11
12
  */
12
13
  declare function camada(_options?: CamadaMiddlewareOptions): (req: NextRequest, event: NextFetchEvent) => MiddlewareResult;
13
14
 
@@ -19,8 +20,9 @@ declare function camadaRoute(): {
19
20
  /** For a route that wants to gate itself (the example's /challenge-me): the challenge Response,
20
21
  * or null when this client already holds a valid `_cch` and the route should render normally.
21
22
  *
22
- * One request, one event. The middleware stamps `x-camada-rid` on the request it forwards, so
23
- * its presence means the middleware already shipped this request's row; the gate then serves
23
+ * One request, one event. The middleware stamps `x-camada-rid` and its `x-camada-mw` proof on
24
+ * the request it forwards, so a mark that verifies means the middleware already shipped this
25
+ * request's row (a client-sent rid alone proves nothing); the gate then serves
24
26
  * the page WITHOUT a second row, which would double-count the request at the analyst. Exclude
25
27
  * the gated route from the middleware matcher and the gate ships the `blk: "challenge"` row
26
28
  * itself — that is the configuration to use when you want the challenge counted. */
@@ -37,4 +39,11 @@ declare function track(event: string, data?: {
37
39
  user?: string;
38
40
  }): Promise<void>;
39
41
 
40
- export { CamadaBeacon, type CamadaBeaconProps, type CamadaMiddlewareOptions, type MiddlewareResult, camada, camadaRoute, challengeGate, track };
42
+ /**
43
+ * `export const GET = withCamada(async (req) => Response.json(...))` in a route.ts. Same
44
+ * CAMADA_* env as the middleware; inert without a key or with CAMADA_DISABLED=1. Never breaks
45
+ * the handler: a camada failure costs the event, not the response.
46
+ */
47
+ declare function withCamada<R extends Request, A extends unknown[]>(handler: (req: R, ...rest: A) => Response | Promise<Response>): (req: R, ...rest: A) => Promise<Response>;
48
+
49
+ export { CamadaBeacon, type CamadaBeaconProps, type CamadaMiddlewareOptions, type MiddlewareResult, camada, camadaRoute, challengeGate, track, withCamada };
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // src/middleware.ts
2
2
  import { NextResponse } from "next/server";
3
- import { resolveClientIp as resolveClientIp2, logRateLimited as logRateLimited2, guardedAsync } from "@camada/core";
3
+ import { resolveClientIp as resolveClientIp2, logRateLimited as logRateLimited2, guarded, guardedAsync } from "@camada/core";
4
4
 
5
5
  // src/engine.ts
6
6
  import {
@@ -12,7 +12,7 @@ import {
12
12
  } from "@camada/core";
13
13
 
14
14
  // package.json
15
- var version = "0.2.0";
15
+ var version = "0.3.0";
16
16
  var name = "@camada/next";
17
17
 
18
18
  // src/version.ts
@@ -24,7 +24,7 @@ function resolveEnv(env) {
24
24
  const ingestToken = key?.ingestToken ?? env.CAMADA_TOKEN;
25
25
  const snapToken = key?.snapToken ?? env.CAMADA_SNAPSHOT_TOKEN;
26
26
  if (!ingestToken || !snapToken) return null;
27
- const ingestUrl = (env.CAMADA_INGEST_URL || "https://in.camada.dev").replace(/\/$/, "");
27
+ const ingestUrl = (env.CAMADA_INGEST_URL || "https://in.camada.app").replace(/\/$/, "");
28
28
  return {
29
29
  ingestToken,
30
30
  snapToken,
@@ -82,7 +82,7 @@ import {
82
82
  } from "@camada/core";
83
83
 
84
84
  // src/event.ts
85
- import { buildWireEvent, TAP_NEXT } from "@camada/core";
85
+ import { buildWireEvent, hmacHex, safeEqual, TAP_NEXT } from "@camada/core";
86
86
  function buildEvent(req, path, ip, rid, sid, newSession) {
87
87
  const url = new URL(req.url);
88
88
  return buildWireEvent(
@@ -101,6 +101,24 @@ function buildEvent(req, path, ip, rid, sid, newSession) {
101
101
  { tap: TAP_NEXT, rid, sid, newSession, ja4: req.headers.get("x-vercel-ja4-digest") }
102
102
  );
103
103
  }
104
+ var MW_HEADER = "x-camada-mw";
105
+ var MW_WINDOW_MS = 3e4;
106
+ var MW_SKEW_MS = 5e3;
107
+ var markInput = (ts, req, rid) => {
108
+ const url = new URL(req.url);
109
+ return "mw:" + JSON.stringify([ts, req.method, url.pathname + url.search, rid, req.headers.get("x-forwarded-for") ?? ""]);
110
+ };
111
+ async function middlewareMark(secret, req, rid, now = Date.now()) {
112
+ return `${now}.${await hmacHex(secret, markInput(now, req, rid))}`;
113
+ }
114
+ async function middlewareMarkValid(secret, req, now = Date.now()) {
115
+ const m = /^(\d{13})\.([0-9a-f]{64})$/.exec(req.headers.get(MW_HEADER) ?? "");
116
+ const rid = req.headers.get("x-camada-rid");
117
+ if (!m || !rid) return false;
118
+ const ts = Number(m[1]);
119
+ if (now - ts > MW_WINDOW_MS || ts - now > MW_SKEW_MS) return false;
120
+ return safeEqual(m[2], await hmacHex(secret, markInput(ts, req, rid)));
121
+ }
104
122
  var SESSION_COOKIE = "_sfp";
105
123
  var cookieValue = (cookie, name2) => {
106
124
  const src = "; " + cookie;
@@ -183,11 +201,13 @@ async function challengeGate(req) {
183
201
  const ip = clientIp(engine2, req);
184
202
  if (!ip || await challengePassed(engine2, req, ip)) return null;
185
203
  const url = new URL(req.url);
186
- return serveChallenge(engine2, req, ip, url.pathname + url.search, void 0, !req.headers.has("x-camada-rid"));
204
+ const shipped = await middlewareMarkValid(engine2.env.secret, req).catch(() => false);
205
+ return serveChallenge(engine2, req, ip, url.pathname + url.search, void 0, !shipped);
187
206
  }
188
207
 
189
208
  // src/middleware.ts
190
- function capture(engine2, req, path, ip, existingSid, warnRule, waitUntil) {
209
+ var isBeaconRoute = (method, path) => method === "GET" && path === "/api/camada/b.js" || method === "POST" && path === "/api/camada/fp";
210
+ async function capture(engine2, req, path, ip, existingSid, warnRule, waitUntil) {
191
211
  const rid = crypto.randomUUID();
192
212
  const sid = existingSid ?? crypto.randomUUID();
193
213
  const cfg = engine2.snap.config;
@@ -200,6 +220,9 @@ function capture(engine2, req, path, ip, existingSid, warnRule, waitUntil) {
200
220
  }
201
221
  const headers = new Headers(req.headers);
202
222
  headers.set("x-camada-rid", rid);
223
+ headers.delete(MW_HEADER);
224
+ const mark = await middlewareMark(engine2.env.secret, req, rid).catch(() => null);
225
+ if (mark) headers.set(MW_HEADER, mark);
203
226
  const res = NextResponse.next({ request: { headers } });
204
227
  res.headers.set("x-rid", rid);
205
228
  if (!existingSid) {
@@ -208,12 +231,19 @@ function capture(engine2, req, path, ip, existingSid, warnRule, waitUntil) {
208
231
  }
209
232
  return res;
210
233
  }
234
+ function passThrough(req) {
235
+ if (!req.headers.has(MW_HEADER) && !req.headers.has("x-camada-rid")) return void 0;
236
+ const headers = new Headers(req.headers);
237
+ headers.delete(MW_HEADER);
238
+ headers.delete("x-camada-rid");
239
+ return NextResponse.next({ request: { headers } });
240
+ }
211
241
  function camada(_options) {
212
242
  return function camadaMiddleware(req, event) {
213
243
  try {
214
- if (isDisabled()) return void 0;
244
+ if (isDisabled()) return passThrough(req);
215
245
  const engine2 = getEngine();
216
- if (!engine2) return void 0;
246
+ if (!engine2) return passThrough(req);
217
247
  const waitUntil = event?.waitUntil ? (p) => event.waitUntil(p) : void 0;
218
248
  engine2.snap.ensureFresh(waitUntil);
219
249
  const ip = resolveClientIp2(null, req.headers.get("x-forwarded-for"), trustedProxy(engine2));
@@ -242,12 +272,13 @@ function camada(_options) {
242
272
  }
243
273
  const warnRule = v.warn ? v.rule ?? null : null;
244
274
  if (v.challenge && ip && challengeEnabled() && !isChallengeRoute(path)) {
245
- return guardedAsync(async () => await challengePassed(engine2, req, ip) ? capture(engine2, req, path, ip, existingSid, warnRule, waitUntil) : serveChallenge(engine2, req, ip, path + new URL(req.url).search, waitUntil), void 0);
275
+ return guardedAsync(async () => await challengePassed(engine2, req, ip) ? capture(engine2, req, path, ip, existingSid, warnRule, waitUntil) : serveChallenge(engine2, req, ip, path + new URL(req.url).search, waitUntil), passThrough(req));
246
276
  }
247
- return capture(engine2, req, path, ip, existingSid, warnRule, waitUntil);
277
+ if (isBeaconRoute(req.method, path)) return passThrough(req);
278
+ return guardedAsync(() => capture(engine2, req, path, ip, existingSid, warnRule, waitUntil), passThrough(req));
248
279
  } catch (err) {
249
280
  logRateLimited2(err);
250
- return void 0;
281
+ return guarded(() => passThrough(req), void 0);
251
282
  }
252
283
  };
253
284
  }
@@ -347,7 +378,63 @@ async function CamadaBeacon({ src = "/api/camada/b.js" } = {}) {
347
378
 
348
379
  // src/track.ts
349
380
  import { hashUserId, logRateLimited as logRateLimited3, resolveClientIp as resolveClientIp4, TAP_NEXT as TAP_NEXT3 } from "@camada/core";
381
+
382
+ // src/wait-until.ts
383
+ import * as nextServer from "next/server";
384
+ function contextWaitUntil() {
385
+ const g = globalThis;
386
+ for (const name2 of ["@next/request-context", "@vercel/request-context"]) {
387
+ const ctx = g[Symbol.for(name2)]?.get?.();
388
+ if (typeof ctx?.waitUntil === "function") return (p) => ctx.waitUntil(p);
389
+ }
390
+ return void 0;
391
+ }
392
+ function requestWaitUntil() {
393
+ const pending = [];
394
+ let hold;
395
+ const drain = async () => {
396
+ for (let i = 0; i < pending.length; i++) await pending[i].catch(() => {
397
+ });
398
+ };
399
+ return (p) => {
400
+ if (hold === void 0) {
401
+ hold = null;
402
+ try {
403
+ const after2 = nextServer.after;
404
+ if (typeof after2 !== "function") throw new Error("no after()");
405
+ after2(drain);
406
+ hold = (q) => {
407
+ pending.push(q);
408
+ };
409
+ } catch {
410
+ try {
411
+ hold = contextWaitUntil() ?? null;
412
+ } catch {
413
+ hold = null;
414
+ }
415
+ }
416
+ }
417
+ try {
418
+ hold?.(p);
419
+ } catch {
420
+ }
421
+ };
422
+ }
423
+
424
+ // src/track.ts
350
425
  var SESSION_COOKIE2 = "_sfp";
426
+ var als;
427
+ var scopes = () => {
428
+ if (als === void 0) {
429
+ const Ctor = globalThis.AsyncLocalStorage;
430
+ als = Ctor ? new Ctor() : null;
431
+ }
432
+ return als;
433
+ };
434
+ var withTrackScope = (scope, fn) => {
435
+ const s = scopes();
436
+ return s ? s.run(scope, fn) : fn();
437
+ };
351
438
  async function track(event, data) {
352
439
  try {
353
440
  if (isDisabled()) return;
@@ -355,26 +442,81 @@ async function track(event, data) {
355
442
  if (!engine2) return;
356
443
  const { headers, cookies } = await import("next/headers");
357
444
  const [h, c] = await Promise.all([headers(), cookies()]);
445
+ const scope = scopes()?.getStore();
358
446
  const uid = data?.user ? await hashUserId(data.user, engine2.env.ingestToken) : null;
359
447
  engine2.queue.push({
360
448
  tap: TAP_NEXT3,
361
449
  et: event,
362
450
  uid,
363
- rid: h.get("x-camada-rid"),
364
- sid: c.get(SESSION_COOKIE2)?.value ?? null,
451
+ rid: scope ? scope.rid : h.get("x-camada-rid"),
452
+ sid: scope?.sid ?? c.get(SESSION_COOKIE2)?.value ?? null,
365
453
  ip: resolveClientIp4(null, h.get("x-forwarded-for"), trustedProxy(engine2)),
366
454
  ts: Date.now()
367
455
  });
368
- void engine2.queue.flush();
456
+ void engine2.queue.flush(requestWaitUntil());
369
457
  } catch (err) {
370
458
  logRateLimited3(err);
371
459
  }
372
460
  }
461
+
462
+ // src/with-camada.ts
463
+ import iife2 from "@camada/browser/iife-string";
464
+ import { logRateLimited as logRateLimited4, TAP_NEXT as TAP_NEXT4 } from "@camada/core";
465
+ import { createFetchCamada, withSetCookie } from "@camada/core/fetch";
466
+ var cam = createFetchCamada({ tap: TAP_NEXT4, sdk: SDK_ID, iife: iife2 }, { challengePath: VERIFY_PATH });
467
+ async function middlewareShipped(req) {
468
+ const secret = getEngine()?.env.secret;
469
+ return !!secret && await middlewareMarkValid(secret, req);
470
+ }
471
+ var thrownStatus = (err) => Number(/;(\d{3});?$/.exec(String(err?.digest ?? ""))?.[1] ?? 500);
472
+ function withCamada(handler) {
473
+ return async (req, ...rest) => {
474
+ const env = envSource();
475
+ const r = await cam.before(req, {
476
+ env: { ...env, CAMADA_TRUSTED_PROXY: env.CAMADA_TRUSTED_PROXY || (env.VERCEL ? "vercel" : void 0) },
477
+ waitUntil: requestWaitUntil()
478
+ // the event flushes after the body: hold a serverless function open for it
479
+ });
480
+ if (!r) return handler(req, ...rest);
481
+ if (r.response) return r.response;
482
+ let shipped = false;
483
+ try {
484
+ shipped = await middlewareShipped(req);
485
+ } catch (err) {
486
+ logRateLimited4(err);
487
+ }
488
+ if (shipped) {
489
+ logRateLimited4(new Error(`withCamada: the middleware also matched ${new URL(req.url).pathname}, so its pre-response event stands and dur is not reported; exclude wrapped routes from the matcher`));
490
+ return handler(req, ...rest);
491
+ }
492
+ let res;
493
+ try {
494
+ res = await withTrackScope({ rid: r.vars.rid, sid: r.vars.sid }, () => handler(req, ...rest));
495
+ } catch (err) {
496
+ cam.after(req, r.vars, thrownStatus(err));
497
+ throw err;
498
+ }
499
+ res = withRid(res, r.vars.rid);
500
+ return cam.finish(req, r.vars, r.vars.sessionCookie ? withSetCookie(res, r.vars.sessionCookie) : res);
501
+ };
502
+ }
503
+ function withRid(res, rid) {
504
+ if (res.status === 101) return res;
505
+ try {
506
+ res.headers.set("x-rid", rid);
507
+ return res;
508
+ } catch {
509
+ const out = new Response(res.body, res);
510
+ out.headers.set("x-rid", rid);
511
+ return out;
512
+ }
513
+ }
373
514
  export {
374
515
  CamadaBeacon,
375
516
  camada,
376
517
  camadaRoute,
377
518
  challengeGate,
378
- track
519
+ track,
520
+ withCamada
379
521
  };
380
522
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/middleware.ts","../src/engine.ts","../package.json","../src/version.ts","../src/challenge.ts","../src/event.ts","../src/route.ts","../src/beacon.tsx","../src/track.ts"],"sourcesContent":["// The Next.js middleware/proxy entry: enforce the blocklist inline, stamp x-camada-rid onto\n// the forwarded request (the <CamadaBeacon/> server component reads it back), ship the wire\n// event fire-and-forget. Everything runs inside a try/catch fail-open envelope — a camada\n// bug must never break the customer's app (plan.md INT-2).\n//\n// MUST stay edge-runtime-safe: Web APIs only (fetch/Headers/Request/Response/URL/crypto),\n// no node: imports anywhere in this entry's import graph. Proven by test/edge-safety.test.ts,\n// which bundles this file for a bare edge runtime and executes it inside @edge-runtime/vm.\n// NextRequest/NextFetchEvent are imported as TYPES only; NextResponse comes from\n// 'next/server', which is itself edge-safe.\nimport type { NextFetchEvent, NextRequest } from 'next/server';\nimport { NextResponse } from 'next/server';\nimport { resolveClientIp, logRateLimited, guardedAsync } from '@camada/core';\nimport { getEngine, isDisabled, challengeEnabled, trustedProxy, type Engine } from './engine';\nimport { isChallengeRoute, challengePassed, serveChallenge } from './challenge';\nimport { buildEvent, cookieValue, SESSION_COOKIE } from './event';\n\nexport interface CamadaMiddlewareOptions {\n // Reserved. The engine is configured via CAMADA_* environment variables.\n}\n\n/** Next accepts a promise here; only the challenge branch returns one (it awaits WebCrypto). */\nexport type MiddlewareResult = Response | undefined | Promise<Response | undefined>;\n\n/** The ordinary path: ship one pre-response event, stamp the rid, mint the session cookie.\n * `warnRule` is the id of the `warn` rule that let this request through, if one did (§D3). */\nfunction capture(engine: Engine, req: NextRequest, path: string, ip: string | null, existingSid: string | null, warnRule: string | null, waitUntil?: (p: Promise<unknown>) => void): Response {\n const rid = crypto.randomUUID();\n const sid = existingSid ?? crypto.randomUUID();\n const cfg = engine.snap.config;\n const excluded = (cfg?.exclude || []).some((x) => path.startsWith(x));\n if (!excluded && Math.random() < (cfg?.sample ?? 1)) {\n // st stays null: middleware ships pre-response, like the edge collector's tap.\n const ev = buildEvent(req, path, ip, rid, sid, !existingSid);\n if (warnRule) ev.wrn = warnRule;\n engine.queue.push(ev);\n engine.queue.flush(waitUntil);\n }\n\n const headers = new Headers(req.headers);\n headers.set('x-camada-rid', rid);\n const res = NextResponse.next({ request: { headers } });\n res.headers.set('x-rid', rid);\n if (!existingSid) {\n const secure = new URL(req.url).protocol === 'https:' ? '; Secure' : '';\n res.headers.append('set-cookie', `${SESSION_COOKIE}=${sid}; Path=/; Max-Age=2592000; HttpOnly; SameSite=Lax${secure}`);\n }\n return res;\n}\n\n/**\n * `export default camada();` from middleware.ts (Next ≤15) / proxy.ts (Next 16).\n * Returns undefined (Next continues) whenever camada is disabled, unconfigured, or broken.\n */\nexport function camada(_options?: CamadaMiddlewareOptions): (req: NextRequest, event: NextFetchEvent) => MiddlewareResult {\n return function camadaMiddleware(req: NextRequest, event: NextFetchEvent): MiddlewareResult {\n try {\n if (isDisabled()) return undefined;\n const engine = getEngine();\n if (!engine) return undefined;\n const waitUntil = event?.waitUntil ? (p: Promise<unknown>) => event.waitUntil(p) : undefined;\n engine.snap.ensureFresh(waitUntil);\n\n // There is no socket peer at the edge: with no trusted-proxy config (or no XFF) the\n // ip resolves to null and ip rules simply don't enforce — a spoofed X-Forwarded-For\n // can never reach the blocklist. On Vercel the engine defaults to {mode:'vercel'}.\n const ip = resolveClientIp(null, req.headers.get('x-forwarded-for'), trustedProxy(engine));\n const path = new URL(req.url).pathname;\n\n // the same _sfp session as the collector and @camada/node: sid/ns comparable across taps\n const existingSid = cookieValue(req.headers.get('cookie') || '', SESSION_COOKIE);\n\n // The custom rules read the user agent and the request headers (§D3); without them every\n // `ua` and `header` condition is false. `Headers.get` is case-insensitive, so the\n // lower-cased name the matcher asks with finds whatever spelling the client sent.\n const v = engine.snap.verdict({\n ip, path, ua: req.headers.get('user-agent'), header: (n) => req.headers.get(n),\n }); // cold start fails open ('cold')\n if (v.block) {\n const ev = buildEvent(req, path, ip, crypto.randomUUID(), existingSid, false);\n ev.st = 403; // blocked requests always ship, unsampled\n ev.blk = v.reason; // SDK-01: the reason rides the event so the analyst counts SDK blocks, not the app's own 403s ('rule' when a rule decided)\n if (v.rule) ev.rl = v.rule;\n engine.queue.push(ev);\n engine.queue.flush(waitUntil);\n const headers: Record<string, string> = {\n 'content-type': 'text/plain',\n 'x-block-reason': String(v.reason ?? ''),\n '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 // `warn` passes the request and only marks its event; a skip passes with nothing\n // stamped — it is the absence of enforcement.\n const warnRule = v.warn ? v.rule ?? null : null;\n\n // A challenge verdict: serve the proof-of-work page unless this client already passed.\n // The verify route answers its own endpoint, so never challenge that path. A challenge\n // needs a resolved ip (the nonce and `_cch` are bound to it) — without one, fail open,\n // the same stance ip rules take at this position.\n // A client that HAS passed falls through to the normal capture path: it keeps its rid,\n // its session cookie and its event, so an hour of `_cch` is not an hour of blindness.\n // guardedAsync covers the whole branch — the synchronous catch below cannot see a\n // rejection from these awaits, and WebCrypto is not guaranteed to exist.\n if (v.challenge && ip && challengeEnabled() && !isChallengeRoute(path)) {\n return guardedAsync(async () => (\n (await challengePassed(engine, req, ip))\n ? capture(engine, req, path, ip, existingSid, warnRule, waitUntil)\n : serveChallenge(engine, req, ip, path + new URL(req.url).search, waitUntil)\n ), undefined);\n }\n\n return capture(engine, req, path, ip, existingSid, warnRule, waitUntil);\n } catch (err) {\n logRateLimited(err); // fail open: the app proceeds as if camada were not installed\n return undefined;\n }\n };\n}\n","// The lazy singleton engine behind every @camada/next entry point: one SnapshotClient in\n// 'lazy' mode (edge runtime — ensureFresh per request, no interval timers) plus one\n// EventQueue, wired from the environment on first use. Unconfigured -> engine is null, every\n// entry point stays inert, and one rate-limited console.error says why. CAMADA_DISABLED=1 is\n// checked per request (isDisabled), never memoized here.\n//\n// This module is part of the middleware's import graph and therefore MUST stay\n// edge-runtime-safe: Web APIs and process.env reads only, no node: imports.\nimport {\n SnapshotClient, EventQueue, parseKey, parseTrustedProxyEnv, logRateLimited,\n type SnapshotVersion, type TrustedProxyConfig,\n} from '@camada/core';\n\nimport { SDK_ID } from './version';\n\nexport { parseTrustedProxyEnv }; // re-export for callers that had reached into this module\n\nexport interface ResolvedEnv {\n ingestToken: string;\n snapToken: string;\n secret: string; // HMAC key for the challenge nonce/cookie — never leaves the process\n ingestUrl: string;\n snapshotUrl: string;\n trustedProxy: TrustedProxyConfig | null; // null = defer to server-delivered config\n}\n\n/** Returns null (SDK stays inert, one log line) rather than throwing on bad config. */\nexport function resolveEnv(env: Record<string, string | undefined>): ResolvedEnv | null {\n const key = parseKey(env.CAMADA_KEY);\n const ingestToken = key?.ingestToken ?? env.CAMADA_TOKEN;\n const snapToken = key?.snapToken ?? env.CAMADA_SNAPSHOT_TOKEN;\n if (!ingestToken || !snapToken) return null;\n const ingestUrl = (env.CAMADA_INGEST_URL || 'https://in.camada.dev').replace(/\\/$/, ''); // PLACEHOLDER default — confirm the production ingest domain before any npm publish\n return {\n ingestToken, snapToken, ingestUrl,\n secret: env.CAMADA_KEY || `${ingestToken}.${snapToken}`,\n snapshotUrl: env.CAMADA_SNAPSHOT_URL || `${ingestUrl}/snapshot`,\n // On Vercel the platform overwrites X-Forwarded-For, so its rightmost entry is\n // trustworthy: default to vercel mode there unless explicitly overridden.\n trustedProxy: parseTrustedProxyEnv(env.CAMADA_TRUSTED_PROXY) ?? (env.VERCEL ? { mode: 'vercel' } : null),\n };\n}\n\nexport interface Engine {\n env: ResolvedEnv;\n snap: SnapshotClient;\n queue: EventQueue;\n fetchImpl: typeof fetch; // for the beacon relay; snapshot/queue carry their own copy\n}\n\nexport interface ConfigureOptions {\n env?: Record<string, string | undefined>;\n fetchImpl?: typeof fetch;\n challenge?: boolean; // enforce `challenge` verdicts with the first-party page (default true)\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}\n\nlet engine: Engine | null | undefined; // undefined = not built yet; null = unconfigured\nlet overrides: ConfigureOptions = {};\n\n/** Internal test/reset hook (mirrors @camada/node's configure): replaces the singleton. */\nexport function configure(opts: ConfigureOptions = {}): void {\n engine?.snap.stop();\n engine?.queue.stop();\n overrides = opts;\n engine = undefined;\n}\n\nfunction envSource(): Record<string, string | undefined> {\n return overrides.env ?? (typeof process !== 'undefined' && process.env ? process.env : {});\n}\n\n/** Kill switch, checked per request so an env flip disables camada without a code change. */\nexport function isDisabled(): boolean {\n return envSource().CAMADA_DISABLED === '1';\n}\n\n/** SDK-04 is on unless the app opts out in code or with CAMADA_CHALLENGE=0. */\nexport function challengeEnabled(): boolean {\n return overrides.challenge !== false && envSource().CAMADA_CHALLENGE !== '0';\n}\n\nexport function getEngine(): Engine | null {\n if (engine !== undefined) return engine;\n const env = resolveEnv(envSource());\n if (!env) {\n logRateLimited(new Error('CAMADA_KEY (or CAMADA_TOKEN + CAMADA_SNAPSHOT_TOKEN) not set — camada is inactive'));\n return (engine = null);\n }\n const fetchImpl = overrides.fetchImpl;\n const injected = fetchImpl ? { fetchImpl } : {}; // never pass an explicit undefined key\n engine = {\n env,\n fetchImpl: fetchImpl ?? fetch,\n // snapshotVersion left undefined takes core's default (5); the client drops undefined keys.\n snap: new SnapshotClient({ url: env.snapshotUrl, token: env.snapToken, mode: 'lazy', sdk: SDK_ID, snapshotVersion: overrides.snapshotVersion, ...injected }),\n queue: new EventQueue({ url: env.ingestUrl, token: env.ingestToken, sdk: SDK_ID, ...injected }),\n };\n return engine;\n}\n\n/** The trusted-proxy config in force: explicit local/env override wins, then the\n * server-delivered tenant config, then none (socket-less edge -> ip stays null). */\nexport function trustedProxy(e: Engine): TrustedProxyConfig | null {\n return e.env.trustedProxy ?? e.snap.config?.trusted_proxy ?? null;\n}\n","{\n \"version\": \"0.2.0\",\n \"license\": \"MIT\",\n \"type\": \"module\",\n \"engines\": {\n \"node\": \">=18\"\n },\n \"files\": [\n \"dist\",\n \"README.md\",\n \"LICENSE\"\n ],\n \"scripts\": {\n \"build\": \"tsup\",\n \"dev\": \"tsup --watch\",\n \"test\": \"vitest run\",\n \"check\": \"tsc --noEmit\"\n },\n \"name\": \"@camada/next\",\n \"description\": \"camada backend SDK for Next.js: edge-safe middleware/proxy, /api/camada route handlers, <CamadaBeacon/> with the request rid\",\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 \"@camada/react\": \"file:../camada-react\"\n },\n \"peerDependencies\": {\n \"next\": \">=15\",\n \"react\": \">=18\"\n },\n \"devDependencies\": {\n \"typescript\": \"^5.7.2\",\n \"tsup\": \"^8.3.5\",\n \"vitest\": \"^3.0.0\",\n \"next\": \"^16.0.0\",\n \"react\": \"^19.0.0\",\n \"@types/react\": \"^19.0.0\",\n \"@edge-runtime/vm\": \"^5.0.0\",\n \"esbuild\": \"^0.24.0\",\n \"@types/node\": \"^22.10.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","// The challenge for @camada/next (SDK-04). Edge-runtime-safe: Web APIs and WebCrypto only, no\n// node: imports — this module sits in the middleware's import graph, which\n// test/edge-safety.test.ts bundles for a bare edge runtime.\n//\n// The page is served by the middleware (or by challengeGate() from a route the app gates\n// itself) and posts a form to POST /api/camada/challenge, handled by camadaRoute().\nimport {\n createChallengeAsync, challengePage, challengeCookie, safeReturnTo, wantsHtml, parseFormBody,\n resolveClientIp, CHALLENGE_COOKIE, type AsyncChallengeKit,\n} from '@camada/core';\nimport { getEngine, isDisabled, challengeEnabled, trustedProxy, type Engine } from './engine';\nimport { buildEvent, cookieValue, SESSION_COOKIE } from './event';\n\n/** Where the page posts. The catch-all route handler must be mounted at /api/camada/[...camada]\n * (the documented install) — the served page hard-codes this action. */\nexport const VERIFY_PATH = '/api/camada/challenge';\n\nconst BODY_MAX = 4 * 1024; // the verify form is ~120 bytes; anything larger is not ours\n\n/** The app may mount the catch-all elsewhere, so the middleware matches on the tail. */\nexport const isChallengeRoute = (path: string): boolean => /\\/camada\\/challenge\\/?$/.test(path);\n\n// One kit per engine; the engine is replaced wholesale by configure(), so the map never grows.\nconst kits = new WeakMap<Engine, AsyncChallengeKit>();\nexport function kitFor(engine: Engine): AsyncChallengeKit {\n let k = kits.get(engine);\n if (!k) { k = createChallengeAsync({ secret: engine.env.secret }); kits.set(engine, k); }\n return k;\n}\n\nexport const clientIp = (engine: Engine, req: Request): string | null =>\n resolveClientIp(null, req.headers.get('x-forwarded-for'), trustedProxy(engine));\n\nexport function challengePassed(engine: Engine, req: Request, ip: string): Promise<boolean> {\n return kitFor(engine).tokenValid(ip, Date.now(), cookieValue(req.headers.get('cookie') || '', CHALLENGE_COOKIE));\n}\n\n/** 403 + the proof-of-work page (HTML navigations) or 403 JSON (everything else), plus the\n * `blk: \"challenge\"` event — a served challenge is reported like a block (contract §D2). */\nexport async function serveChallenge(engine: Engine, req: Request, ip: string, target: string, waitUntil?: (p: Promise<unknown>) => void, ship = true): Promise<Response> {\n if (ship) {\n const ev = buildEvent(req, new URL(req.url).pathname, ip, crypto.randomUUID(), cookieValue(req.headers.get('cookie') || '', SESSION_COOKIE), false);\n ev.st = 403;\n ev.blk = 'challenge';\n engine.queue.push(ev);\n engine.queue.flush(waitUntil);\n }\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 kitFor(engine).nonce(ip, Date.now()), action: VERIFY_PATH, 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 handler for `…/camada/challenge`: validate, set `_cch`, 302 back, ship `{ st: 200, ch: 1 }`. */\nexport async function verifyChallenge(engine: Engine, req: Request): Promise<Response> {\n const url = new URL(req.url);\n const ip = clientIp(engine, req);\n const kit = kitFor(engine);\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 (!ip || !(await kit.verify(ip, now, form.nonce, form.solution))) {\n const html = ip ? challengePage({ nonce: await kit.nonce(ip, now), action: url.pathname, to }) : 'Forbidden';\n return new Response(html, { status: 403, headers: { 'content-type': ip ? 'text/html; charset=utf-8' : 'text/plain', 'cache-control': 'no-store' } });\n }\n const ev = buildEvent(req, url.pathname, ip, crypto.randomUUID(), cookieValue(req.headers.get('cookie') || '', SESSION_COOKIE), false);\n ev.st = 200;\n ev.ch = 1; // challenge passed (contract §A3 ingest field)\n engine.queue.push(ev);\n void engine.queue.flush();\n return new Response(null, {\n status: 302,\n headers: {\n location: to,\n 'set-cookie': challengeCookie(await kit.issue(ip, now), url.protocol === 'https:'),\n 'cache-control': 'no-store',\n },\n });\n}\n\n/** For a route that wants to gate itself (the example's /challenge-me): the challenge Response,\n * or null when this client already holds a valid `_cch` and the route should render normally.\n *\n * One request, one event. The middleware stamps `x-camada-rid` on the request it forwards, so\n * its presence means the middleware already shipped this request's row; the gate then serves\n * the page WITHOUT a second row, which would double-count the request at the analyst. Exclude\n * the gated route from the middleware matcher and the gate ships the `blk: \"challenge\"` row\n * itself — that is the configuration to use when you want the challenge counted. */\nexport async function challengeGate(req: Request): Promise<Response | null> {\n if (isDisabled() || !challengeEnabled()) return null;\n const engine = getEngine();\n if (!engine) return null;\n const ip = clientIp(engine, req);\n if (!ip || await challengePassed(engine, req, ip)) return null; // unidentifiable client: fail open\n const url = new URL(req.url);\n return serveChallenge(engine, req, ip, url.pathname + url.search, undefined, !req.headers.has('x-camada-rid'));\n}\n","// One wire-event builder for every @camada/next entry (middleware + beacon route). Web APIs\n// only: this sits in the middleware's edge-safe import graph.\nimport { buildWireEvent, TAP_NEXT, type WireEvent } from '@camada/core';\n\nexport function buildEvent(req: Request, path: string, ip: string | null, rid: string, sid: string | null, newSession: boolean): WireEvent {\n const url = new URL(req.url);\n return buildWireEvent(\n {\n method: req.method,\n host: req.headers.get('host') ?? url.host,\n path,\n query: url.search,\n // The edge runtime sorts header names, so hord is alphabetical at this tap — still\n // shipped; the scorer knows sdk-next lacks the raw-wire-order signal (capability mask).\n headers: [...req.headers.entries()],\n ip,\n httpVersion: null, // not observable at this position\n },\n { tap: TAP_NEXT, rid, sid, newSession, ja4: req.headers.get('x-vercel-ja4-digest') },\n );\n}\n\nexport const SESSION_COOKIE = '_sfp'; // the same session cookie as the edge collector and @camada/node: sid/ns comparable across taps\n\nexport const 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","// Route handlers for app/api/camada/[...camada]/route.ts:\n// GET …/b.js -> the first-party beacon IIFE (its auto-init derives the fp endpoint from\n// the script URL's final path segment and the rid from ?r=)\n// POST …/fp -> queue the beacon as a sig:1 row with the trusted-proxy-resolved client IP\n// and tap 'sdk-next': it rides the event batch (one request per flush at the\n// analyst, not one per page view) — the mirror of @camada/node's /_cam/*.\n// Works on BOTH runtimes (edge and node): Web APIs only, no node: imports. Unconfigured or\n// disabled: GET 404s, POST answers 204 and drops — inert, never an error.\nimport iife from '@camada/browser/iife-string';\nimport { guardedAsync, resolveClientIp, TAP_NEXT } from '@camada/core';\nimport { getEngine, isDisabled, challengeEnabled, trustedProxy, type Engine } from './engine';\nimport { verifyChallenge } from './challenge';\nimport { buildEvent, cookieValue, SESSION_COOKIE } from './event';\n\nconst FP_MAX = 32 * 1024; // matches the server's /fp cap: never accept what ingest will 413\nconst encoder = new TextEncoder();\n\nconst notFound = () => new Response(null, { status: 404 });\nconst noContent = () => new Response(null, { status: 204, headers: { 'cache-control': 'no-store' } });\n\n// The route handlers enforce too: a middleware matcher that excludes /api/ (a common pattern)\n// must not leave the beacon endpoints serving blocked clients.\nfunction blocked(engine: Engine, req: Request): Response | null {\n const ip = resolveClientIp(null, req.headers.get('x-forwarded-for'), trustedProxy(engine));\n const path = new URL(req.url).pathname;\n // §D3: `ua` and `header` rules need these; `Headers.get` already matches names case-insensitively\n const v = engine.snap.verdict({ ip, path, ua: req.headers.get('user-agent'), header: (n) => req.headers.get(n) });\n if (!v.block) return null;\n const ev = buildEvent(req, path, ip, crypto.randomUUID(), cookieValue(req.headers.get('cookie') || '', SESSION_COOKIE), false);\n ev.st = 403; ev.blk = v.reason; // SDK-01: a beacon-route deny is a block like any other — it ships, unsampled\n if (v.rule) ev.rl = v.rule;\n engine.queue.push(ev);\n void engine.queue.flush();\n const headers: Record<string, string> = { 'x-block-reason': String(v.reason ?? ''), 'x-block-version': v.version ?? '' };\n if (v.rule) headers['x-block-rule'] = v.rule;\n return new Response('Forbidden', { status: 403, headers });\n}\n\nfunction lastSegment(req: Request): string {\n const parts = new URL(req.url).pathname.split('/');\n return parts[parts.length - 1];\n}\n\nfunction activeEngine(): Engine | null {\n return isDisabled() ? null : getEngine();\n}\n\n// tenant switch: both beacon endpoints stand down together (b.js and fp), as node's and hono's do\nconst beaconOff = (engine: Engine): boolean => engine.snap.config?.beacon === false;\n\nexport function camadaRoute(): {\n GET: (req: Request) => Promise<Response>;\n POST: (req: Request) => Promise<Response>;\n} {\n return {\n GET: (req) => guardedAsync(async () => {\n if (lastSegment(req) !== 'b.js') return notFound();\n const engine = activeEngine();\n if (!engine) return notFound();\n engine.snap.ensureFresh();\n const deny = blocked(engine, req);\n if (deny) return deny;\n if (beaconOff(engine)) return notFound();\n return new Response(iife, {\n status: 200,\n headers: { 'content-type': 'application/javascript', 'cache-control': 'public, max-age=3600' },\n });\n }, notFound()),\n\n POST: (req) => guardedAsync(async () => {\n if (lastSegment(req) === 'challenge') {\n const engine = activeEngine();\n if (!engine || !challengeEnabled()) return notFound();\n engine.snap.ensureFresh();\n const deny = blocked(engine, req); // a blocked client cannot buy its way past with a solution\n if (deny) return deny;\n return verifyChallenge(engine, req);\n }\n if (lastSegment(req) !== 'fp') return notFound();\n const body = await req.text();\n if (encoder.encode(body).byteLength > FP_MAX) return new Response(null, { status: 413 });\n const engine = activeEngine();\n if (engine) {\n engine.snap.ensureFresh();\n const deny = blocked(engine, req);\n if (deny) return deny;\n if (beaconOff(engine)) return notFound();\n const ip = resolveClientIp(null, req.headers.get('x-forwarded-for'), trustedProxy(engine));\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)) {\n engine.queue.push({ ...(parsed as Record<string, unknown>), sig: 1, ip, tap: TAP_NEXT });\n void engine.queue.flush(); // fire-and-forget, as the relay was: a serverless runtime may freeze right after the response\n }\n }\n return noContent();\n }, noContent()),\n };\n}\n","// <CamadaBeacon/> — an async React Server Component for the app-router layout <head>: reads\n// the middleware-stamped x-camada-rid request header (so the beacon POST joins the server's\n// wire event) and renders the @camada/react beacon tag pointing at the camada route.\n// 'next/headers' is imported lazily inside the component so this module only touches it\n// server-side, in a request scope.\nimport { CamadaBeacon as Beacon } from '@camada/react';\n\nexport interface CamadaBeaconProps {\n /** Where camadaRoute() serves the beacon script. Default: '/api/camada/b.js'. */\n src?: string;\n}\n\nexport async function CamadaBeacon({ src = '/api/camada/b.js' }: CamadaBeaconProps = {}) {\n let rid: string | null = null;\n try {\n const { headers } = await import('next/headers');\n rid = (await headers()).get('x-camada-rid');\n } catch { /* outside a request scope (static render, tests): beacon ships without a rid */ }\n return <Beacon src={src} rid={rid} />;\n}\n","// track() — app-context outcome events recorded from a server action, route handler, or server\n// component, joined to the middleware's wire event. The name is free-form; the analyst's rules read\n// this vocabulary: login_failed, login_succeeded, signup, password_reset, mfa_failed, payment_failed,\n// payment_succeeded, coupon_failed (@camada/node README has the table). Events are joined\n// through the x-camada-rid request header and the _sfp session cookie. The identifier is\n// HMAC-hashed in-process with the ingest token: the raw value never reaches the queue, and\n// the analyst drops anything that is not hash-shaped anyway.\n// next/headers is imported lazily inside the call (the beacon.tsx pattern) so this module\n// stays importable everywhere; calling track() outside a request scope is a no-op.\nimport { hashUserId, logRateLimited, resolveClientIp, TAP_NEXT } from '@camada/core';\nimport { getEngine, isDisabled, trustedProxy } from './engine';\n\nconst SESSION_COOKIE = '_sfp';\n\n/** Never throws, never blocks the response: a camada problem must not break a login. */\nexport async function track(event: string, data?: { user?: string }): Promise<void> {\n try {\n if (isDisabled()) return;\n const engine = getEngine();\n if (!engine) return;\n const { headers, cookies } = await import('next/headers');\n const [h, c] = await Promise.all([headers(), cookies()]);\n const uid = data?.user ? await hashUserId(data.user, engine.env.ingestToken) : null;\n engine.queue.push({\n tap: TAP_NEXT,\n et: event,\n uid,\n rid: h.get('x-camada-rid'),\n sid: c.get(SESSION_COOKIE)?.value ?? null,\n ip: resolveClientIp(null, h.get('x-forwarded-for'), trustedProxy(engine)),\n ts: Date.now(),\n });\n void engine.queue.flush(); // a serverless runtime may freeze right after the response; don't wait for the interval\n } catch (err) {\n logRateLimited(err); // outside a request scope, unconfigured, or a camada bug: swallow\n }\n}\n"],"mappings":"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","names":["resolveClientIp","logRateLimited","name","engine","engine","resolveClientIp","logRateLimited","guardedAsync","resolveClientIp","TAP_NEXT","engine","resolveClientIp","guardedAsync","TAP_NEXT","logRateLimited","resolveClientIp","TAP_NEXT","SESSION_COOKIE","engine","TAP_NEXT","resolveClientIp","logRateLimited"]}
1
+ {"version":3,"sources":["../src/middleware.ts","../src/engine.ts","../package.json","../src/version.ts","../src/challenge.ts","../src/event.ts","../src/route.ts","../src/beacon.tsx","../src/track.ts","../src/wait-until.ts","../src/with-camada.ts"],"sourcesContent":["// The Next.js middleware/proxy entry: enforce the blocklist inline, stamp x-camada-rid onto\n// the forwarded request (the <CamadaBeacon/> server component reads it back), ship the wire\n// event fire-and-forget. Everything runs inside a try/catch fail-open envelope — a camada\n// bug must never break the customer's app (plan.md INT-2).\n//\n// MUST stay edge-runtime-safe: Web APIs only (fetch/Headers/Request/Response/URL/crypto),\n// no node: imports anywhere in this entry's import graph. Proven by test/edge-safety.test.ts,\n// which bundles this file for a bare edge runtime and executes it inside @edge-runtime/vm.\n// NextRequest/NextFetchEvent are imported as TYPES only; NextResponse comes from\n// 'next/server', which is itself edge-safe.\nimport type { NextFetchEvent, NextRequest } from 'next/server';\nimport { NextResponse } from 'next/server';\nimport { resolveClientIp, logRateLimited, guarded, guardedAsync } from '@camada/core';\nimport { getEngine, isDisabled, challengeEnabled, trustedProxy, type Engine } from './engine';\nimport { isChallengeRoute, challengePassed, serveChallenge } from './challenge';\nimport { buildEvent, cookieValue, middlewareMark, MW_HEADER, SESSION_COOKIE } from './event';\n\n/** camadaRoute()'s beacon endpoints (mounted at /api/camada/[...camada]). Camada's own, like core's scriptPath/fpPath:\n * after enforcement they are neither captured nor stamped with x-rid. */\nconst isBeaconRoute = (method: string, path: string): boolean =>\n (method === 'GET' && path === '/api/camada/b.js') || (method === 'POST' && path === '/api/camada/fp');\n\nexport interface CamadaMiddlewareOptions {\n // Reserved. The engine is configured via CAMADA_* environment variables.\n}\n\n/** Next accepts a promise here; the capture and challenge branches return one (they await WebCrypto). */\nexport type MiddlewareResult = Response | undefined | Promise<Response | undefined>;\n\n/** The ordinary path: ship one pre-response event, stamp the rid (and its proof for withCamada()),\n * mint the session cookie. `warnRule` is the id of the `warn` rule that let this request through, if one did (§D3). */\nasync function capture(engine: Engine, req: NextRequest, path: string, ip: string | null, existingSid: string | null, warnRule: string | null, waitUntil?: (p: Promise<unknown>) => void): Promise<Response> {\n const rid = crypto.randomUUID();\n const sid = existingSid ?? crypto.randomUUID();\n const cfg = engine.snap.config;\n const excluded = (cfg?.exclude || []).some((x) => path.startsWith(x));\n if (!excluded && Math.random() < (cfg?.sample ?? 1)) {\n // st stays null: middleware ships pre-response, like the edge collector's tap.\n const ev = buildEvent(req, path, ip, rid, sid, !existingSid);\n if (warnRule) ev.wrn = warnRule;\n engine.queue.push(ev);\n engine.queue.flush(waitUntil);\n }\n\n const headers = new Headers(req.headers);\n headers.set('x-camada-rid', rid);\n // WebCrypto, the one await on this path; without it the request still goes through, just unproven (withCamada then ships too).\n // The headers are a copy of the client's: drop any mark it sent before stamping ours.\n headers.delete(MW_HEADER);\n const mark = await middlewareMark(engine.env.secret, req, rid).catch(() => null);\n if (mark) headers.set(MW_HEADER, mark);\n const res = NextResponse.next({ request: { headers } });\n res.headers.set('x-rid', rid);\n if (!existingSid) {\n const secure = new URL(req.url).protocol === 'https:' ? '; Secure' : '';\n res.headers.append('set-cookie', `${SESSION_COOKIE}=${sid}; Path=/; Max-Age=2592000; HttpOnly; SameSite=Lax${secure}`);\n }\n return res;\n}\n\n/** Where camada lets a request through without capturing it: forward it without any x-camada-mw or x-camada-rid the\n * client sent, so the only mark and rid a route handler ever sees are ones this middleware made. Undefined (Next\n * continues untouched) when there is neither. */\nfunction passThrough(req: Request): Response | undefined {\n if (!req.headers.has(MW_HEADER) && !req.headers.has('x-camada-rid')) return undefined;\n const headers = new Headers(req.headers);\n headers.delete(MW_HEADER);\n headers.delete('x-camada-rid');\n return NextResponse.next({ request: { headers } });\n}\n\n/**\n * `export default camada();` from middleware.ts (Next ≤15) / proxy.ts (Next 16).\n * Returns undefined (Next continues) whenever camada is disabled, unconfigured, or broken; a client-sent\n * x-camada-mw or x-camada-rid is stripped on every path that forwards the request.\n */\nexport function camada(_options?: CamadaMiddlewareOptions): (req: NextRequest, event: NextFetchEvent) => MiddlewareResult {\n return function camadaMiddleware(req: NextRequest, event: NextFetchEvent): MiddlewareResult {\n try {\n if (isDisabled()) return passThrough(req);\n const engine = getEngine();\n if (!engine) return passThrough(req);\n const waitUntil = event?.waitUntil ? (p: Promise<unknown>) => event.waitUntil(p) : undefined;\n engine.snap.ensureFresh(waitUntil);\n\n // There is no socket peer at the edge: with no trusted-proxy config (or no XFF) the\n // ip resolves to null and ip rules simply don't enforce — a spoofed X-Forwarded-For\n // can never reach the blocklist. On Vercel the engine defaults to {mode:'vercel'}.\n const ip = resolveClientIp(null, req.headers.get('x-forwarded-for'), trustedProxy(engine));\n const path = new URL(req.url).pathname;\n\n // the same _sfp session as the collector and @camada/node: sid/ns comparable across taps\n const existingSid = cookieValue(req.headers.get('cookie') || '', SESSION_COOKIE);\n\n // The custom rules read the user agent and the request headers (§D3); without them every\n // `ua` and `header` condition is false. `Headers.get` is case-insensitive, so the\n // lower-cased name the matcher asks with finds whatever spelling the client sent.\n const v = engine.snap.verdict({\n ip, path, ua: req.headers.get('user-agent'), header: (n) => req.headers.get(n),\n }); // cold start fails open ('cold')\n if (v.block) {\n const ev = buildEvent(req, path, ip, crypto.randomUUID(), existingSid, false);\n ev.st = 403; // blocked requests always ship, unsampled\n ev.blk = v.reason; // SDK-01: the reason rides the event so the analyst counts SDK blocks, not the app's own 403s ('rule' when a rule decided)\n if (v.rule) ev.rl = v.rule;\n engine.queue.push(ev);\n engine.queue.flush(waitUntil);\n const headers: Record<string, string> = {\n 'content-type': 'text/plain',\n 'x-block-reason': String(v.reason ?? ''),\n '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 // `warn` passes the request and only marks its event; a skip passes with nothing\n // stamped — it is the absence of enforcement.\n const warnRule = v.warn ? v.rule ?? null : null;\n\n // A challenge verdict: serve the proof-of-work page unless this client already passed.\n // The verify route answers its own endpoint, so never challenge that path. A challenge\n // needs a resolved ip (the nonce and `_cch` are bound to it) — without one, fail open,\n // the same stance ip rules take at this position.\n // A client that HAS passed falls through to the normal capture path: it keeps its rid,\n // its session cookie and its event, so an hour of `_cch` is not an hour of blindness.\n // guardedAsync covers the whole branch — the synchronous catch below cannot see a\n // rejection from these awaits, and WebCrypto is not guaranteed to exist.\n if (v.challenge && ip && challengeEnabled() && !isChallengeRoute(path)) {\n return guardedAsync(async () => (\n (await challengePassed(engine, req, ip))\n ? capture(engine, req, path, ip, existingSid, warnRule, waitUntil)\n : serveChallenge(engine, req, ip, path + new URL(req.url).search, waitUntil)\n ), passThrough(req));\n }\n\n if (isBeaconRoute(req.method, path)) return passThrough(req); // no row, no x-rid: the route handler answers it\n\n return guardedAsync(() => capture(engine, req, path, ip, existingSid, warnRule, waitUntil), passThrough(req));\n } catch (err) {\n logRateLimited(err); // fail open: the app proceeds as if camada were not installed\n return guarded(() => passThrough(req), undefined);\n }\n };\n}\n","// The lazy singleton engine behind every @camada/next entry point: one SnapshotClient in\n// 'lazy' mode (edge runtime — ensureFresh per request, no interval timers) plus one\n// EventQueue, wired from the environment on first use. Unconfigured -> engine is null, every\n// entry point stays inert, and one rate-limited console.error says why. CAMADA_DISABLED=1 is\n// checked per request (isDisabled), never memoized here.\n//\n// This module is part of the middleware's import graph and therefore MUST stay\n// edge-runtime-safe: Web APIs and process.env reads only, no node: imports.\nimport {\n SnapshotClient, EventQueue, parseKey, parseTrustedProxyEnv, logRateLimited,\n type SnapshotVersion, type TrustedProxyConfig,\n} from '@camada/core';\n\nimport { SDK_ID } from './version';\n\nexport { parseTrustedProxyEnv }; // re-export for callers that had reached into this module\n\nexport interface ResolvedEnv {\n ingestToken: string;\n snapToken: string;\n secret: string; // HMAC key for the challenge nonce/cookie — never leaves the process\n ingestUrl: string;\n snapshotUrl: string;\n trustedProxy: TrustedProxyConfig | null; // null = defer to server-delivered config\n}\n\n/** Returns null (SDK stays inert, one log line) rather than throwing on bad config. */\nexport function resolveEnv(env: Record<string, string | undefined>): ResolvedEnv | null {\n const key = parseKey(env.CAMADA_KEY);\n const ingestToken = key?.ingestToken ?? env.CAMADA_TOKEN;\n const snapToken = key?.snapToken ?? env.CAMADA_SNAPSHOT_TOKEN;\n if (!ingestToken || !snapToken) return null;\n const ingestUrl = (env.CAMADA_INGEST_URL || 'https://in.camada.app').replace(/\\/$/, '');\n return {\n ingestToken, snapToken, ingestUrl,\n secret: env.CAMADA_KEY || `${ingestToken}.${snapToken}`,\n snapshotUrl: env.CAMADA_SNAPSHOT_URL || `${ingestUrl}/snapshot`,\n // On Vercel the platform overwrites X-Forwarded-For, so its rightmost entry is\n // trustworthy: default to vercel mode there unless explicitly overridden.\n trustedProxy: parseTrustedProxyEnv(env.CAMADA_TRUSTED_PROXY) ?? (env.VERCEL ? { mode: 'vercel' } : null),\n };\n}\n\nexport interface Engine {\n env: ResolvedEnv;\n snap: SnapshotClient;\n queue: EventQueue;\n fetchImpl: typeof fetch; // for the beacon relay; snapshot/queue carry their own copy\n}\n\nexport interface ConfigureOptions {\n env?: Record<string, string | undefined>;\n fetchImpl?: typeof fetch;\n challenge?: boolean; // enforce `challenge` verdicts with the first-party page (default true)\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}\n\nlet engine: Engine | null | undefined; // undefined = not built yet; null = unconfigured\nlet overrides: ConfigureOptions = {};\n\n/** Internal test/reset hook (mirrors @camada/node's configure): replaces the singleton. */\nexport function configure(opts: ConfigureOptions = {}): void {\n engine?.snap.stop();\n engine?.queue.stop();\n overrides = opts;\n engine = undefined;\n}\n\nexport function envSource(): Record<string, string | undefined> {\n return overrides.env ?? (typeof process !== 'undefined' && process.env ? process.env : {});\n}\n\n/** Kill switch, checked per request so an env flip disables camada without a code change. */\nexport function isDisabled(): boolean {\n return envSource().CAMADA_DISABLED === '1';\n}\n\n/** SDK-04 is on unless the app opts out in code or with CAMADA_CHALLENGE=0. */\nexport function challengeEnabled(): boolean {\n return overrides.challenge !== false && envSource().CAMADA_CHALLENGE !== '0';\n}\n\nexport function getEngine(): Engine | null {\n if (engine !== undefined) return engine;\n const env = resolveEnv(envSource());\n if (!env) {\n logRateLimited(new Error('CAMADA_KEY (or CAMADA_TOKEN + CAMADA_SNAPSHOT_TOKEN) not set — camada is inactive'));\n return (engine = null);\n }\n const fetchImpl = overrides.fetchImpl;\n const injected = fetchImpl ? { fetchImpl } : {}; // never pass an explicit undefined key\n engine = {\n env,\n fetchImpl: fetchImpl ?? fetch,\n // snapshotVersion left undefined takes core's default (5); the client drops undefined keys.\n snap: new SnapshotClient({ url: env.snapshotUrl, token: env.snapToken, mode: 'lazy', sdk: SDK_ID, snapshotVersion: overrides.snapshotVersion, ...injected }),\n queue: new EventQueue({ url: env.ingestUrl, token: env.ingestToken, sdk: SDK_ID, ...injected }),\n };\n return engine;\n}\n\n/** The trusted-proxy config in force: explicit local/env override wins, then the\n * server-delivered tenant config, then none (socket-less edge -> ip stays null). */\nexport function trustedProxy(e: Engine): TrustedProxyConfig | null {\n return e.env.trustedProxy ?? e.snap.config?.trusted_proxy ?? null;\n}\n","{\n \"version\": \"0.3.0\",\n \"license\": \"MIT\",\n \"type\": \"module\",\n \"engines\": {\n \"node\": \">=20\"\n },\n \"files\": [\n \"dist\",\n \"README.md\",\n \"LICENSE\"\n ],\n \"scripts\": {\n \"build\": \"tsup\",\n \"dev\": \"tsup --watch\",\n \"test\": \"vitest run\",\n \"check\": \"tsc --noEmit\"\n },\n \"name\": \"@camada/next\",\n \"description\": \"camada backend SDK for Next.js: edge-safe middleware/proxy, /api/camada route handlers, <CamadaBeacon/> with the request rid\",\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 \"@camada/react\": \"file:../camada-react\"\n },\n \"peerDependencies\": {\n \"next\": \">=15\",\n \"react\": \">=18\"\n },\n \"devDependencies\": {\n \"typescript\": \"^5.7.2\",\n \"tsup\": \"^8.3.5\",\n \"vitest\": \"^3.0.0\",\n \"next\": \"^16.0.0\",\n \"react\": \"^19.0.0\",\n \"@types/react\": \"^19.0.0\",\n \"@edge-runtime/vm\": \"^5.0.0\",\n \"esbuild\": \"^0.24.0\",\n \"@types/node\": \"^22.10.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","// The challenge for @camada/next (SDK-04). Edge-runtime-safe: Web APIs and WebCrypto only, no\n// node: imports — this module sits in the middleware's import graph, which\n// test/edge-safety.test.ts bundles for a bare edge runtime.\n//\n// The page is served by the middleware (or by challengeGate() from a route the app gates\n// itself) and posts a form to POST /api/camada/challenge, handled by camadaRoute().\nimport {\n createChallengeAsync, challengePage, challengeCookie, safeReturnTo, wantsHtml, parseFormBody,\n resolveClientIp, CHALLENGE_COOKIE, type AsyncChallengeKit,\n} from '@camada/core';\nimport { getEngine, isDisabled, challengeEnabled, trustedProxy, type Engine } from './engine';\nimport { buildEvent, cookieValue, middlewareMarkValid, SESSION_COOKIE } from './event';\n\n/** Where the page posts. The catch-all route handler must be mounted at /api/camada/[...camada]\n * (the documented install) — the served page hard-codes this action. */\nexport const VERIFY_PATH = '/api/camada/challenge';\n\nconst BODY_MAX = 4 * 1024; // the verify form is ~120 bytes; anything larger is not ours\n\n/** The app may mount the catch-all elsewhere, so the middleware matches on the tail. */\nexport const isChallengeRoute = (path: string): boolean => /\\/camada\\/challenge\\/?$/.test(path);\n\n// One kit per engine; the engine is replaced wholesale by configure(), so the map never grows.\nconst kits = new WeakMap<Engine, AsyncChallengeKit>();\nexport function kitFor(engine: Engine): AsyncChallengeKit {\n let k = kits.get(engine);\n if (!k) { k = createChallengeAsync({ secret: engine.env.secret }); kits.set(engine, k); }\n return k;\n}\n\nexport const clientIp = (engine: Engine, req: Request): string | null =>\n resolveClientIp(null, req.headers.get('x-forwarded-for'), trustedProxy(engine));\n\nexport function challengePassed(engine: Engine, req: Request, ip: string): Promise<boolean> {\n return kitFor(engine).tokenValid(ip, Date.now(), cookieValue(req.headers.get('cookie') || '', CHALLENGE_COOKIE));\n}\n\n/** 403 + the proof-of-work page (HTML navigations) or 403 JSON (everything else), plus the\n * `blk: \"challenge\"` event — a served challenge is reported like a block (contract §D2). */\nexport async function serveChallenge(engine: Engine, req: Request, ip: string, target: string, waitUntil?: (p: Promise<unknown>) => void, ship = true): Promise<Response> {\n if (ship) {\n const ev = buildEvent(req, new URL(req.url).pathname, ip, crypto.randomUUID(), cookieValue(req.headers.get('cookie') || '', SESSION_COOKIE), false);\n ev.st = 403;\n ev.blk = 'challenge';\n engine.queue.push(ev);\n engine.queue.flush(waitUntil);\n }\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 kitFor(engine).nonce(ip, Date.now()), action: VERIFY_PATH, 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 handler for `…/camada/challenge`: validate, set `_cch`, 302 back, ship `{ st: 200, ch: 1 }`. */\nexport async function verifyChallenge(engine: Engine, req: Request): Promise<Response> {\n const url = new URL(req.url);\n const ip = clientIp(engine, req);\n const kit = kitFor(engine);\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 (!ip || !(await kit.verify(ip, now, form.nonce, form.solution))) {\n const html = ip ? challengePage({ nonce: await kit.nonce(ip, now), action: url.pathname, to }) : 'Forbidden';\n return new Response(html, { status: 403, headers: { 'content-type': ip ? 'text/html; charset=utf-8' : 'text/plain', 'cache-control': 'no-store' } });\n }\n const ev = buildEvent(req, url.pathname, ip, crypto.randomUUID(), cookieValue(req.headers.get('cookie') || '', SESSION_COOKIE), false);\n ev.st = 200;\n ev.ch = 1; // challenge passed (contract §A3 ingest field)\n engine.queue.push(ev);\n void engine.queue.flush();\n return new Response(null, {\n status: 302,\n headers: {\n location: to,\n 'set-cookie': challengeCookie(await kit.issue(ip, now), url.protocol === 'https:'),\n 'cache-control': 'no-store',\n },\n });\n}\n\n/** For a route that wants to gate itself (the example's /challenge-me): the challenge Response,\n * or null when this client already holds a valid `_cch` and the route should render normally.\n *\n * One request, one event. The middleware stamps `x-camada-rid` and its `x-camada-mw` proof on\n * the request it forwards, so a mark that verifies means the middleware already shipped this\n * request's row (a client-sent rid alone proves nothing); the gate then serves\n * the page WITHOUT a second row, which would double-count the request at the analyst. Exclude\n * the gated route from the middleware matcher and the gate ships the `blk: \"challenge\"` row\n * itself — that is the configuration to use when you want the challenge counted. */\nexport async function challengeGate(req: Request): Promise<Response | null> {\n if (isDisabled() || !challengeEnabled()) return null;\n const engine = getEngine();\n if (!engine) return null;\n const ip = clientIp(engine, req);\n if (!ip || await challengePassed(engine, req, ip)) return null; // unidentifiable client: fail open\n const url = new URL(req.url);\n const shipped = await middlewareMarkValid(engine.env.secret, req).catch(() => false);\n return serveChallenge(engine, req, ip, url.pathname + url.search, undefined, !shipped);\n}\n","// One wire-event builder for every @camada/next entry (middleware + beacon route). Web APIs\n// only: this sits in the middleware's edge-safe import graph.\nimport { buildWireEvent, hmacHex, safeEqual, TAP_NEXT, type WireEvent } from '@camada/core';\n\nexport function buildEvent(req: Request, path: string, ip: string | null, rid: string, sid: string | null, newSession: boolean): WireEvent {\n const url = new URL(req.url);\n return buildWireEvent(\n {\n method: req.method,\n host: req.headers.get('host') ?? url.host,\n path,\n query: url.search,\n // The edge runtime sorts header names, so hord is alphabetical at this tap — still\n // shipped; the scorer knows sdk-next lacks the raw-wire-order signal (capability mask).\n headers: [...req.headers.entries()],\n ip,\n httpVersion: null, // not observable at this position\n },\n { tap: TAP_NEXT, rid, sid, newSession, ja4: req.headers.get('x-vercel-ja4-digest') },\n );\n}\n\n/** Stamped by the middleware next to x-camada-rid on the request it forwards: proof, for withCamada(), that this request's\n * event already shipped. It only ever spares the wrapper a second event; enforcement never depends on it. */\nexport const MW_HEADER = 'x-camada-mw';\nexport const MW_WINDOW_MS = 30_000; // middleware → route handler takes milliseconds; 30 s covers a cold function start\nconst MW_SKEW_MS = 5_000; // the middleware's clock may run ahead of the function's (edge vs. Node on Vercel)\n\n/** What the mark binds: when it was made, the method, the path and query, the rid, and the client chain as forwarded.\n * JSON keeps the fields apart, since the rid and X-Forwarded-For are whatever the client sent where the middleware did not run. */\nconst markInput = (ts: number, req: Request, rid: string): string => {\n const url = new URL(req.url);\n return 'mw:' + JSON.stringify([ts, req.method, url.pathname + url.search, rid, req.headers.get('x-forwarded-for') ?? '']);\n};\n\n/** `<ms timestamp>.<HMAC-SHA256 hex>` under the server-only secret. */\nexport async function middlewareMark(secret: string, req: Request, rid: string, now = Date.now()): Promise<string> {\n return `${now}.${await hmacHex(secret, markInput(now, req, rid))}`;\n}\n\n/** True only for a mark this deployment's middleware made for this very request line and client, within the window.\n * A copied pair fails on another path, query, method or client, and on any request once the window has passed. */\nexport async function middlewareMarkValid(secret: string, req: Request, now = Date.now()): Promise<boolean> {\n const m = /^(\\d{13})\\.([0-9a-f]{64})$/.exec(req.headers.get(MW_HEADER) ?? '');\n const rid = req.headers.get('x-camada-rid');\n if (!m || !rid) return false;\n const ts = Number(m[1]);\n if (now - ts > MW_WINDOW_MS || ts - now > MW_SKEW_MS) return false;\n return safeEqual(m[2], await hmacHex(secret, markInput(ts, req, rid)));\n}\n\nexport const SESSION_COOKIE = '_sfp'; // the same session cookie as the edge collector and @camada/node: sid/ns comparable across taps\n\nexport const 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","// Route handlers for app/api/camada/[...camada]/route.ts:\n// GET …/b.js -> the first-party beacon IIFE (its auto-init derives the fp endpoint from\n// the script URL's final path segment and the rid from ?r=)\n// POST …/fp -> queue the beacon as a sig:1 row with the trusted-proxy-resolved client IP\n// and tap 'sdk-next': it rides the event batch (one request per flush at the\n// analyst, not one per page view) — the mirror of @camada/node's /_cam/*.\n// Works on BOTH runtimes (edge and node): Web APIs only, no node: imports. Unconfigured or\n// disabled: GET 404s, POST answers 204 and drops — inert, never an error.\nimport iife from '@camada/browser/iife-string';\nimport { guardedAsync, resolveClientIp, TAP_NEXT } from '@camada/core';\nimport { getEngine, isDisabled, challengeEnabled, trustedProxy, type Engine } from './engine';\nimport { verifyChallenge } from './challenge';\nimport { buildEvent, cookieValue, SESSION_COOKIE } from './event';\n\nconst FP_MAX = 32 * 1024; // matches the server's /fp cap: never accept what ingest will 413\nconst encoder = new TextEncoder();\n\nconst notFound = () => new Response(null, { status: 404 });\nconst noContent = () => new Response(null, { status: 204, headers: { 'cache-control': 'no-store' } });\n\n// The route handlers enforce too: a middleware matcher that excludes /api/ (a common pattern)\n// must not leave the beacon endpoints serving blocked clients.\nfunction blocked(engine: Engine, req: Request): Response | null {\n const ip = resolveClientIp(null, req.headers.get('x-forwarded-for'), trustedProxy(engine));\n const path = new URL(req.url).pathname;\n // §D3: `ua` and `header` rules need these; `Headers.get` already matches names case-insensitively\n const v = engine.snap.verdict({ ip, path, ua: req.headers.get('user-agent'), header: (n) => req.headers.get(n) });\n if (!v.block) return null;\n const ev = buildEvent(req, path, ip, crypto.randomUUID(), cookieValue(req.headers.get('cookie') || '', SESSION_COOKIE), false);\n ev.st = 403; ev.blk = v.reason; // SDK-01: a beacon-route deny is a block like any other — it ships, unsampled\n if (v.rule) ev.rl = v.rule;\n engine.queue.push(ev);\n void engine.queue.flush();\n const headers: Record<string, string> = { 'x-block-reason': String(v.reason ?? ''), 'x-block-version': v.version ?? '' };\n if (v.rule) headers['x-block-rule'] = v.rule;\n return new Response('Forbidden', { status: 403, headers });\n}\n\nfunction lastSegment(req: Request): string {\n const parts = new URL(req.url).pathname.split('/');\n return parts[parts.length - 1];\n}\n\nfunction activeEngine(): Engine | null {\n return isDisabled() ? null : getEngine();\n}\n\n// tenant switch: both beacon endpoints stand down together (b.js and fp), as node's and hono's do\nconst beaconOff = (engine: Engine): boolean => engine.snap.config?.beacon === false;\n\nexport function camadaRoute(): {\n GET: (req: Request) => Promise<Response>;\n POST: (req: Request) => Promise<Response>;\n} {\n return {\n GET: (req) => guardedAsync(async () => {\n if (lastSegment(req) !== 'b.js') return notFound();\n const engine = activeEngine();\n if (!engine) return notFound();\n engine.snap.ensureFresh();\n const deny = blocked(engine, req);\n if (deny) return deny;\n if (beaconOff(engine)) return notFound();\n return new Response(iife, {\n status: 200,\n headers: { 'content-type': 'application/javascript', 'cache-control': 'public, max-age=3600' },\n });\n }, notFound()),\n\n POST: (req) => guardedAsync(async () => {\n if (lastSegment(req) === 'challenge') {\n const engine = activeEngine();\n if (!engine || !challengeEnabled()) return notFound();\n engine.snap.ensureFresh();\n const deny = blocked(engine, req); // a blocked client cannot buy its way past with a solution\n if (deny) return deny;\n return verifyChallenge(engine, req);\n }\n if (lastSegment(req) !== 'fp') return notFound();\n const body = await req.text();\n if (encoder.encode(body).byteLength > FP_MAX) return new Response(null, { status: 413 });\n const engine = activeEngine();\n if (engine) {\n engine.snap.ensureFresh();\n const deny = blocked(engine, req);\n if (deny) return deny;\n if (beaconOff(engine)) return notFound();\n const ip = resolveClientIp(null, req.headers.get('x-forwarded-for'), trustedProxy(engine));\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)) {\n engine.queue.push({ ...(parsed as Record<string, unknown>), sig: 1, ip, tap: TAP_NEXT });\n void engine.queue.flush(); // fire-and-forget, as the relay was: a serverless runtime may freeze right after the response\n }\n }\n return noContent();\n }, noContent()),\n };\n}\n","// <CamadaBeacon/> — an async React Server Component for the app-router layout <head>: reads\n// the middleware-stamped x-camada-rid request header (so the beacon POST joins the server's\n// wire event) and renders the @camada/react beacon tag pointing at the camada route.\n// 'next/headers' is imported lazily inside the component so this module only touches it\n// server-side, in a request scope.\nimport { CamadaBeacon as Beacon } from '@camada/react';\n\nexport interface CamadaBeaconProps {\n /** Where camadaRoute() serves the beacon script. Default: '/api/camada/b.js'. */\n src?: string;\n}\n\nexport async function CamadaBeacon({ src = '/api/camada/b.js' }: CamadaBeaconProps = {}) {\n let rid: string | null = null;\n try {\n const { headers } = await import('next/headers');\n rid = (await headers()).get('x-camada-rid');\n } catch { /* outside a request scope (static render, tests): beacon ships without a rid */ }\n return <Beacon src={src} rid={rid} />;\n}\n","// track() — app-context outcome events recorded from a server action, route handler, or server\n// component, joined to the middleware's wire event. The name is free-form; the analyst's rules read\n// this vocabulary: login_failed, login_succeeded, signup, password_reset, mfa_failed, payment_failed,\n// payment_succeeded, coupon_failed (@camada/node README has the table). Inside withCamada() an\n// event joins on the wrapper's own rid and session; elsewhere on the x-camada-rid the middleware\n// stamps and the _sfp session cookie. The identifier is\n// HMAC-hashed in-process with the ingest token: the raw value never reaches the queue, and\n// the analyst drops anything that is not hash-shaped anyway.\n// next/headers is imported lazily inside the call (the beacon.tsx pattern) so this module\n// stays importable everywhere; calling track() outside a request scope is a no-op.\nimport { hashUserId, logRateLimited, resolveClientIp, TAP_NEXT } from '@camada/core';\nimport { getEngine, isDisabled, trustedProxy } from './engine';\nimport { requestWaitUntil } from './wait-until';\n\nconst SESSION_COOKIE = '_sfp';\n\ninterface Scope { rid: string; sid: string | null }\ninterface Als { run<R>(store: Scope, fn: () => R): R; getStore(): Scope | undefined }\nlet als: Als | null | undefined;\n/** Next puts AsyncLocalStorage on globalThis in both runtimes (node-environment-baseline, the edge sandbox). */\nconst scopes = (): Als | null => {\n if (als === undefined) {\n const Ctor = (globalThis as { AsyncLocalStorage?: new () => Als }).AsyncLocalStorage;\n als = Ctor ? new Ctor() : null; // ponytail: without it (never under Next) a wrapped route's track() falls back to the header\n }\n return als;\n};\n\n/** withCamada() runs its handler here, so track() inside it takes the wrapper's rid and never a\n * client-sent x-camada-rid: a wrapped route sits outside the matcher, where nothing strips it. */\nexport const withTrackScope = <R>(scope: Scope, fn: () => R): R => { const s = scopes(); return s ? s.run(scope, fn) : fn(); };\n\n/** Never throws, never blocks the response: a camada problem must not break a login. */\nexport async function track(event: string, data?: { user?: string }): Promise<void> {\n try {\n if (isDisabled()) return;\n const engine = getEngine();\n if (!engine) return;\n const { headers, cookies } = await import('next/headers');\n const [h, c] = await Promise.all([headers(), cookies()]);\n const scope = scopes()?.getStore();\n const uid = data?.user ? await hashUserId(data.user, engine.env.ingestToken) : null;\n engine.queue.push({\n tap: TAP_NEXT,\n et: event,\n uid,\n rid: scope ? scope.rid : h.get('x-camada-rid'),\n sid: scope?.sid ?? c.get(SESSION_COOKIE)?.value ?? null,\n ip: resolveClientIp(null, h.get('x-forwarded-for'), trustedProxy(engine)),\n ts: Date.now(),\n });\n void engine.queue.flush(requestWaitUntil()); // a serverless runtime may freeze right after the response: hold it open for the flush\n } catch (err) {\n logRateLimited(err); // outside a request scope, unconfigured, or a camada bug: swallow\n }\n}\n","// A waitUntil for code that runs inside a route handler or server action (withCamada, track()). A\n// serverless host (Vercel) may freeze the function once the response has gone out, and the event's\n// flush only starts then, when the body is done. Next's after() (15.1+) holds the function open\n// until its callback settles, but it must be called inside the request scope, which the body-done\n// callback is not. So the first promise handed in registers ONE after() callback; it runs once the\n// response has closed and drains every promise handed in, including any that arrive while it waits.\n// Without after() (Next 15.0, or outside a request scope) the host's request context waitUntil is\n// used, the one @vercel/functions reads; with neither, the promise is left to run, which a\n// long-lived `next start` process does anyway.\nimport * as nextServer from 'next/server';\n\ntype WaitUntil = (p: Promise<unknown>) => void;\ntype After = (task: () => Promise<unknown>) => void;\ntype RequestContext = { get?: () => { waitUntil?: WaitUntil } | undefined };\n\nfunction contextWaitUntil(): WaitUntil | undefined {\n const g = globalThis as Record<symbol, RequestContext | undefined>;\n for (const name of ['@next/request-context', '@vercel/request-context']) {\n const ctx = g[Symbol.for(name)]?.get?.();\n if (typeof ctx?.waitUntil === 'function') return (p) => ctx.waitUntil!(p);\n }\n return undefined;\n}\n\n/** Call once per request, inside its scope; hand the result to everything that flushes for it. Never throws. */\nexport function requestWaitUntil(): WaitUntil {\n const pending: Promise<unknown>[] = [];\n let hold: WaitUntil | null | undefined; // undefined = not decided yet; null = nothing to hold the host open with\n const drain = async () => {\n for (let i = 0; i < pending.length; i++) await pending[i].catch(() => {});\n };\n return (p) => {\n if (hold === undefined) {\n hold = null;\n try {\n const after = (nextServer as { after?: After }).after;\n if (typeof after !== 'function') throw new Error('no after()');\n after(drain); // throws outside a request scope\n hold = (q) => { pending.push(q); };\n } catch {\n try { hold = contextWaitUntil() ?? null; } catch { hold = null; }\n }\n }\n try { hold?.(p); } catch { /* a host that refuses the promise costs only the hold */ }\n };\n}\n","// withCamada(handler): the route-handler position, where the response is visible. The middleware\n// runs before the route and ships its event pre-response (st and dur null); a wrapped route\n// handler runs @camada/core's fetch pipeline itself (verdict, block, challenge, the session) and\n// ships one event with the real status and a dur up to the response (a text/event-stream body to its last byte).\n//\n// Threat model. Every request header can come from the client: the middleware does not run for a\n// route left out of its matcher, and Next offers no server-only channel from the middleware to the\n// route (on Vercel they run in different processes, often different regions). So:\n// - Enforcement here never depends on a header. Verdict, block and challenge run on every request.\n// - The one thing a header may decide is whether this request's event already shipped. Wrapped\n// routes belong outside the matcher; if the middleware ran anyway it stamps `x-camada-mw`, an\n// HMAC under the server-only CAMADA_KEY of a timestamp, the method, the path and query, the rid\n// and X-Forwarded-For. A mark that verifies within 30 s skips the second event and the second\n// session cookie, nothing else. A copied mark fails on another path, query, method or client\n// and after the window; within it, it can at most hide a replay's event, never pass a block.\n// - The middleware strips any x-camada-mw the client sent before forwarding.\n// - track() inside the handler takes this wrapper's rid and session (withTrackScope), never a\n// client-sent x-camada-rid; only a request the middleware already shipped keeps its stamped one.\nimport iife from '@camada/browser/iife-string';\nimport { logRateLimited, TAP_NEXT } from '@camada/core';\nimport { createFetchCamada, withSetCookie } from '@camada/core/fetch';\nimport { envSource, getEngine } from './engine';\nimport { VERIFY_PATH } from './challenge';\nimport { middlewareMarkValid } from './event';\nimport { requestWaitUntil } from './wait-until';\nimport { withTrackScope } from './track';\nimport { SDK_ID } from './version';\n\nconst cam = createFetchCamada({ tap: TAP_NEXT, sdk: SDK_ID, iife }, { challengePath: VERIFY_PATH }); // lazy mode: edge and serverless\n\nasync function middlewareShipped(req: Request): Promise<boolean> {\n const secret = getEngine()?.env.secret;\n return !!secret && (await middlewareMarkValid(secret, req));\n}\n\n/** Next's redirect() / notFound() throw an error whose digest ends in the status they answer with. */\nconst thrownStatus = (err: unknown): number =>\n Number(/;(\\d{3});?$/.exec(String((err as { digest?: unknown } | null)?.digest ?? ''))?.[1] ?? 500);\n\n/**\n * `export const GET = withCamada(async (req) => Response.json(...))` in a route.ts. Same\n * CAMADA_* env as the middleware; inert without a key or with CAMADA_DISABLED=1. Never breaks\n * the handler: a camada failure costs the event, not the response.\n */\nexport function withCamada<R extends Request, A extends unknown[]>(\n handler: (req: R, ...rest: A) => Response | Promise<Response>,\n): (req: R, ...rest: A) => Promise<Response> {\n return async (req, ...rest) => {\n const env = envSource();\n // On Vercel the platform overwrites X-Forwarded-For, so its rightmost entry is trustworthy: the middleware's default too.\n const r = await cam.before(req, {\n env: { ...env, CAMADA_TRUSTED_PROXY: env.CAMADA_TRUSTED_PROXY || (env.VERCEL ? 'vercel' : undefined) },\n waitUntil: requestWaitUntil(), // the event flushes after the body: hold a serverless function open for it\n });\n if (!r) return handler(req, ...rest);\n if (r.response) return r.response; // block, challenge: whatever headers the request carries\n let shipped = false;\n try { shipped = await middlewareShipped(req); } catch (err) { logRateLimited(err); }\n if (shipped) {\n logRateLimited(new Error(`withCamada: the middleware also matched ${new URL(req.url).pathname}, so its pre-response event stands and dur is not reported; exclude wrapped routes from the matcher`));\n return handler(req, ...rest); // and its _sfp cookie: no second session\n }\n let res: Response;\n try {\n res = await withTrackScope({ rid: r.vars.rid, sid: r.vars.sid }, () => handler(req, ...rest));\n } catch (err) {\n cam.after(req, r.vars, thrownStatus(err));\n throw err;\n }\n res = withRid(res, r.vars.rid);\n return cam.finish(req, r.vars, r.vars.sessionCookie ? withSetCookie(res, r.vars.sessionCookie) : res);\n };\n}\n\n/** x-rid, the rid of this request's row, the same header the middleware stamps. Set in place, or on a copy when the\n * headers are immutable (`Response.redirect()`, a `fetch()` result), as withSetCookie does: same status and body.\n * A 101 upgrade is left alone. Not on a request the middleware shipped: its own x-rid, that row's, reaches the client. */\nfunction withRid(res: Response, rid: string): Response {\n if (res.status === 101) return res;\n try {\n res.headers.set('x-rid', rid);\n return res;\n } catch {\n // ponytail: core's copyResponse is not exported; its Deno and @hono/node-server cases never reach a Next route.\n const out = new Response(res.body, res);\n out.headers.set('x-rid', rid);\n return out;\n }\n}\n"],"mappings":"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","names":["resolveClientIp","logRateLimited","name","engine","engine","resolveClientIp","logRateLimited","guardedAsync","resolveClientIp","TAP_NEXT","engine","resolveClientIp","guardedAsync","TAP_NEXT","logRateLimited","resolveClientIp","TAP_NEXT","name","after","SESSION_COOKIE","engine","TAP_NEXT","resolveClientIp","logRateLimited","iife","logRateLimited","TAP_NEXT","TAP_NEXT","iife","logRateLimited"]}
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
- "version": "0.2.0",
2
+ "version": "0.3.0",
3
3
  "license": "MIT",
4
4
  "type": "module",
5
5
  "engines": {
6
- "node": ">=18"
6
+ "node": ">=20"
7
7
  },
8
8
  "files": [
9
9
  "dist",
@@ -23,9 +23,9 @@
23
23
  "module": "./dist/index.js",
24
24
  "types": "./dist/index.d.ts",
25
25
  "dependencies": {
26
- "@camada/core": "^0.3.0",
27
- "@camada/browser": "^0.2.0",
28
- "@camada/react": "^0.2.0"
26
+ "@camada/core": "^0.5.0",
27
+ "@camada/browser": "^0.3.0",
28
+ "@camada/react": "^0.3.0"
29
29
  },
30
30
  "peerDependencies": {
31
31
  "next": ">=15",