@camada/next 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +142 -0
- package/dist/index.cjs +406 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +40 -0
- package/dist/index.d.ts +40 -0
- package/dist/index.js +380 -0
- package/dist/index.js.map +1 -0
- package/package.json +42 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 camada
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# @camada/next
|
|
2
|
+
|
|
3
|
+
camada backend SDK for Next.js (app router): edge-safe middleware/proxy enforcement, the
|
|
4
|
+
`/api/camada` route handlers that serve the fingerprint beacon first-party, and a
|
|
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.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
Three files, one env var:
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
// middleware.ts (Next ≤15) or proxy.ts (Next 16) at the project root
|
|
15
|
+
import { camada } from '@camada/next';
|
|
16
|
+
export default camada();
|
|
17
|
+
export const config = { matcher: ['/((?!_next/|favicon.ico).*)'] };
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
// app/api/camada/[...camada]/route.ts
|
|
22
|
+
import { camadaRoute } from '@camada/next';
|
|
23
|
+
export const { GET, POST } = camadaRoute();
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
```tsx
|
|
27
|
+
// app/layout.tsx — in <head>
|
|
28
|
+
import { CamadaBeacon } from '@camada/next';
|
|
29
|
+
<CamadaBeacon />
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
CAMADA_KEY=<ingest_token>.<snap_token>
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Optional env: `CAMADA_INGEST_URL` (default `https://in.camada.dev`), `CAMADA_SNAPSHOT_URL`
|
|
37
|
+
(default ingest + `/snapshot`), `CAMADA_TRUSTED_PROXY` (`none | vercel | hops:N |
|
|
38
|
+
cidrs:a,b`), `CAMADA_DISABLED=1` (kill switch, checked per request). Unconfigured, the SDK
|
|
39
|
+
is inert: the middleware returns `undefined`, the route handlers answer 404/204, and one
|
|
40
|
+
rate-limited `console.error` says why.
|
|
41
|
+
|
|
42
|
+
## What each piece does
|
|
43
|
+
|
|
44
|
+
- **`camada()`** — the middleware: refreshes the blocklist snapshot off-path
|
|
45
|
+
(`event.waitUntil`), enforces it inline (403 with
|
|
46
|
+
`x-block-reason`/`x-block-version`/`x-block-rule`; blocked requests always ship,
|
|
47
|
+
unsampled), stamps `x-camada-rid` onto the forwarded request
|
|
48
|
+
and `x-rid` onto the response, and ships the wire event fire-and-forget with tap
|
|
49
|
+
`sdk-next`. On Vercel, `x-vercel-ja4-digest` — a real TLS fingerprint — rides along as
|
|
50
|
+
`ja4`.
|
|
51
|
+
- **`camadaRoute()`** — `GET …/b.js` serves the `@camada/browser` IIFE first-party (no CSP
|
|
52
|
+
or ad-blocker friction); `POST …/fp` relays the beacon body to ingest with the
|
|
53
|
+
trusted-proxy-resolved client IP and `tap: 'sdk-next'` injected. Works on both runtimes.
|
|
54
|
+
Both endpoints go inert (404) when the project turns the beacon off in its settings.
|
|
55
|
+
- **`<CamadaBeacon/>`** — async server component; reads the middleware's `x-camada-rid` via
|
|
56
|
+
`next/headers` and renders `<script src="/api/camada/b.js?r=<rid>" async>` so the beacon
|
|
57
|
+
POST joins the server-side wire event.
|
|
58
|
+
|
|
59
|
+
## Custom rules
|
|
60
|
+
|
|
61
|
+
Your Rules page holds one ordered list per project, and the middleware walks it before the
|
|
62
|
+
allow, block and challenge lists. First match wins — the order *is* the precedence — and each
|
|
63
|
+
rule carries one of four actions:
|
|
64
|
+
|
|
65
|
+
| action | what the middleware does | on the event |
|
|
66
|
+
|---|---|---|
|
|
67
|
+
| `skip` | passes the request | nothing |
|
|
68
|
+
| `block` | `403` before your app | `blk: "rule"`, `rl: "<rule id>"` |
|
|
69
|
+
| `challenge` | serves the proof-of-work page (`CAMADA_CHALLENGE=0` opts out) | `blk: "challenge"` |
|
|
70
|
+
| `warn` | passes the request and marks it for the analyst | `wrn: "<rule id>"` |
|
|
71
|
+
|
|
72
|
+
A skip rule also carries a *record matches* flag, which only the analyst reads: a recorded skip
|
|
73
|
+
is still scored and shows on your dashboard as Allowed, an unrecorded one is dropped before
|
|
74
|
+
scoring. Either way the request passes here, unstamped — the built-in Allow-list is a skip rule
|
|
75
|
+
with recording on.
|
|
76
|
+
|
|
77
|
+
A rule block also names the row that decided, so the response says which rule to edit:
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
HTTP/1.1 403 Forbidden
|
|
81
|
+
x-block-reason: rule
|
|
82
|
+
x-block-rule: cr_4f2a9c1b7e03
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
`ip`, `path`, `ua` and `header` conditions enforce here; `asn`, `country` and `tlsx` conditions
|
|
86
|
+
cannot be judged at this position and fail open, so a rule that needs one never matches. A
|
|
87
|
+
`header` condition (`is`, `contains`, `matches`) reads the name case-insensitively, and headers
|
|
88
|
+
are the request plane's alone — the analyst never sees them, so a header rule is enforced by a
|
|
89
|
+
v5 SDK like this one or not at all.
|
|
90
|
+
|
|
91
|
+
The rules ride the v5 snapshot, which the SDK asks for by default. A project that has not
|
|
92
|
+
published the container you ask for is answered with the next one down, so asking for the
|
|
93
|
+
newest is always safe.
|
|
94
|
+
|
|
95
|
+
## Edge-runtime safety
|
|
96
|
+
|
|
97
|
+
The middleware's entire import graph is Web-APIs-only — no `node:` imports. This is
|
|
98
|
+
**proven, not promised**: `test/edge-safety.test.ts` bundles `src/middleware.ts` for a bare
|
|
99
|
+
edge target (where `node:` built-ins do not resolve — a canary test pins that the mechanism
|
|
100
|
+
trips) and then executes the bundle inside `@edge-runtime/vm` with no Node globals, driving
|
|
101
|
+
a real cold → blocked flow against the golden snapshot fixture.
|
|
102
|
+
|
|
103
|
+
## Documented caveats (honest limits of this position)
|
|
104
|
+
|
|
105
|
+
- **Client IP needs a trusted-proxy config.** There is no socket peer in middleware, so with
|
|
106
|
+
no `CAMADA_TRUSTED_PROXY` (and no server-delivered tenant config) the IP resolves to
|
|
107
|
+
`null` and IP rules simply don't enforce — a spoofed `X-Forwarded-For` can never reach the
|
|
108
|
+
blocklist. When `VERCEL` is set and no override is given, the SDK defaults to
|
|
109
|
+
`{mode:'vercel'}` (Vercel overwrites XFF, so its rightmost entry is trustworthy).
|
|
110
|
+
- **Header order is alphabetical.** The edge runtime sorts request headers, so `hord` is
|
|
111
|
+
sorted at this tap (still shipped; the scorer's capability mask knows `sdk-next` lacks the
|
|
112
|
+
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>`
|
|
115
|
+
(ip4|ip6|path|rule); the beacon route handlers ship the same event when they deny. Every
|
|
116
|
+
snapshot poll and event batch carries `x-camada-sdk: @camada/next/<version>`.
|
|
117
|
+
- **Serverless cold start fails open**: the first request on a cold instance sees no
|
|
118
|
+
snapshot and passes; the snapshot loads via `waitUntil` and enforcement begins on the next
|
|
119
|
+
request.
|
|
120
|
+
- **App-context tracking works from request scope only.** `track(event, {user?})` records
|
|
121
|
+
outcomes (login failed, signup, …) from server actions, route handlers, and server
|
|
122
|
+
components, joining the middleware's wire event via `x-camada-rid` + the `_sfp` cookie and
|
|
123
|
+
HMAC-hashing the identifier in-process. Outside a request scope it is a silent no-op —
|
|
124
|
+
and it must not be called from middleware, which already ships its own event.
|
|
125
|
+
|
|
126
|
+
## Develop
|
|
127
|
+
|
|
128
|
+
```
|
|
129
|
+
npm install && npm run build && npm test && npm run check
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Sibling checkouts `camada-core`, `camada-browser`, `camada-react` must exist and be built
|
|
133
|
+
(`file:` dependencies). The middleware behavior suite runs under the vitest `edge-runtime`
|
|
134
|
+
environment; route handlers run under node.
|
|
135
|
+
|
|
136
|
+
## Matcher requirement
|
|
137
|
+
|
|
138
|
+
The middleware/proxy matcher decides where enforcement runs. The beacon route handlers
|
|
139
|
+
(`/api/camada/*`) enforce the blocklist themselves, so blocked clients can never fetch the
|
|
140
|
+
beacon even when your matcher excludes `/api/` — but every OTHER route your matcher excludes
|
|
141
|
+
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.
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name2 in all)
|
|
10
|
+
__defProp(target, name2, { get: all[name2], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
CamadaBeacon: () => CamadaBeacon,
|
|
34
|
+
camada: () => camada,
|
|
35
|
+
camadaRoute: () => camadaRoute,
|
|
36
|
+
challengeGate: () => challengeGate,
|
|
37
|
+
track: () => track
|
|
38
|
+
});
|
|
39
|
+
module.exports = __toCommonJS(index_exports);
|
|
40
|
+
|
|
41
|
+
// src/middleware.ts
|
|
42
|
+
var import_server = require("next/server");
|
|
43
|
+
var import_core4 = require("@camada/core");
|
|
44
|
+
|
|
45
|
+
// src/engine.ts
|
|
46
|
+
var import_core = require("@camada/core");
|
|
47
|
+
|
|
48
|
+
// package.json
|
|
49
|
+
var version = "0.2.0";
|
|
50
|
+
var name = "@camada/next";
|
|
51
|
+
|
|
52
|
+
// src/version.ts
|
|
53
|
+
var SDK_ID = `${name}/${version}`;
|
|
54
|
+
|
|
55
|
+
// src/engine.ts
|
|
56
|
+
function resolveEnv(env) {
|
|
57
|
+
const key = (0, import_core.parseKey)(env.CAMADA_KEY);
|
|
58
|
+
const ingestToken = key?.ingestToken ?? env.CAMADA_TOKEN;
|
|
59
|
+
const snapToken = key?.snapToken ?? env.CAMADA_SNAPSHOT_TOKEN;
|
|
60
|
+
if (!ingestToken || !snapToken) return null;
|
|
61
|
+
const ingestUrl = (env.CAMADA_INGEST_URL || "https://in.camada.dev").replace(/\/$/, "");
|
|
62
|
+
return {
|
|
63
|
+
ingestToken,
|
|
64
|
+
snapToken,
|
|
65
|
+
ingestUrl,
|
|
66
|
+
secret: env.CAMADA_KEY || `${ingestToken}.${snapToken}`,
|
|
67
|
+
snapshotUrl: env.CAMADA_SNAPSHOT_URL || `${ingestUrl}/snapshot`,
|
|
68
|
+
// On Vercel the platform overwrites X-Forwarded-For, so its rightmost entry is
|
|
69
|
+
// trustworthy: default to vercel mode there unless explicitly overridden.
|
|
70
|
+
trustedProxy: (0, import_core.parseTrustedProxyEnv)(env.CAMADA_TRUSTED_PROXY) ?? (env.VERCEL ? { mode: "vercel" } : null)
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
var engine;
|
|
74
|
+
var overrides = {};
|
|
75
|
+
function envSource() {
|
|
76
|
+
return overrides.env ?? (typeof process !== "undefined" && process.env ? process.env : {});
|
|
77
|
+
}
|
|
78
|
+
function isDisabled() {
|
|
79
|
+
return envSource().CAMADA_DISABLED === "1";
|
|
80
|
+
}
|
|
81
|
+
function challengeEnabled() {
|
|
82
|
+
return overrides.challenge !== false && envSource().CAMADA_CHALLENGE !== "0";
|
|
83
|
+
}
|
|
84
|
+
function getEngine() {
|
|
85
|
+
if (engine !== void 0) return engine;
|
|
86
|
+
const env = resolveEnv(envSource());
|
|
87
|
+
if (!env) {
|
|
88
|
+
(0, import_core.logRateLimited)(new Error("CAMADA_KEY (or CAMADA_TOKEN + CAMADA_SNAPSHOT_TOKEN) not set \u2014 camada is inactive"));
|
|
89
|
+
return engine = null;
|
|
90
|
+
}
|
|
91
|
+
const fetchImpl = overrides.fetchImpl;
|
|
92
|
+
const injected = fetchImpl ? { fetchImpl } : {};
|
|
93
|
+
engine = {
|
|
94
|
+
env,
|
|
95
|
+
fetchImpl: fetchImpl ?? fetch,
|
|
96
|
+
// snapshotVersion left undefined takes core's default (5); the client drops undefined keys.
|
|
97
|
+
snap: new import_core.SnapshotClient({ url: env.snapshotUrl, token: env.snapToken, mode: "lazy", sdk: SDK_ID, snapshotVersion: overrides.snapshotVersion, ...injected }),
|
|
98
|
+
queue: new import_core.EventQueue({ url: env.ingestUrl, token: env.ingestToken, sdk: SDK_ID, ...injected })
|
|
99
|
+
};
|
|
100
|
+
return engine;
|
|
101
|
+
}
|
|
102
|
+
function trustedProxy(e) {
|
|
103
|
+
return e.env.trustedProxy ?? e.snap.config?.trusted_proxy ?? null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// src/challenge.ts
|
|
107
|
+
var import_core3 = require("@camada/core");
|
|
108
|
+
|
|
109
|
+
// src/event.ts
|
|
110
|
+
var import_core2 = require("@camada/core");
|
|
111
|
+
function buildEvent(req, path, ip, rid, sid, newSession) {
|
|
112
|
+
const url = new URL(req.url);
|
|
113
|
+
return (0, import_core2.buildWireEvent)(
|
|
114
|
+
{
|
|
115
|
+
method: req.method,
|
|
116
|
+
host: req.headers.get("host") ?? url.host,
|
|
117
|
+
path,
|
|
118
|
+
query: url.search,
|
|
119
|
+
// The edge runtime sorts header names, so hord is alphabetical at this tap — still
|
|
120
|
+
// shipped; the scorer knows sdk-next lacks the raw-wire-order signal (capability mask).
|
|
121
|
+
headers: [...req.headers.entries()],
|
|
122
|
+
ip,
|
|
123
|
+
httpVersion: null
|
|
124
|
+
// not observable at this position
|
|
125
|
+
},
|
|
126
|
+
{ tap: import_core2.TAP_NEXT, rid, sid, newSession, ja4: req.headers.get("x-vercel-ja4-digest") }
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
var SESSION_COOKIE = "_sfp";
|
|
130
|
+
var cookieValue = (cookie, name2) => {
|
|
131
|
+
const src = "; " + cookie;
|
|
132
|
+
const i = src.indexOf("; " + name2 + "=");
|
|
133
|
+
if (i === -1) return null;
|
|
134
|
+
const start = i + name2.length + 3;
|
|
135
|
+
const j = src.indexOf(";", start);
|
|
136
|
+
return src.slice(start, j === -1 ? void 0 : j);
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
// src/challenge.ts
|
|
140
|
+
var VERIFY_PATH = "/api/camada/challenge";
|
|
141
|
+
var BODY_MAX = 4 * 1024;
|
|
142
|
+
var isChallengeRoute = (path) => /\/camada\/challenge\/?$/.test(path);
|
|
143
|
+
var kits = /* @__PURE__ */ new WeakMap();
|
|
144
|
+
function kitFor(engine2) {
|
|
145
|
+
let k = kits.get(engine2);
|
|
146
|
+
if (!k) {
|
|
147
|
+
k = (0, import_core3.createChallengeAsync)({ secret: engine2.env.secret });
|
|
148
|
+
kits.set(engine2, k);
|
|
149
|
+
}
|
|
150
|
+
return k;
|
|
151
|
+
}
|
|
152
|
+
var clientIp = (engine2, req) => (0, import_core3.resolveClientIp)(null, req.headers.get("x-forwarded-for"), trustedProxy(engine2));
|
|
153
|
+
function challengePassed(engine2, req, ip) {
|
|
154
|
+
return kitFor(engine2).tokenValid(ip, Date.now(), cookieValue(req.headers.get("cookie") || "", import_core3.CHALLENGE_COOKIE));
|
|
155
|
+
}
|
|
156
|
+
async function serveChallenge(engine2, req, ip, target, waitUntil, ship = true) {
|
|
157
|
+
if (ship) {
|
|
158
|
+
const ev = buildEvent(req, new URL(req.url).pathname, ip, crypto.randomUUID(), cookieValue(req.headers.get("cookie") || "", SESSION_COOKIE), false);
|
|
159
|
+
ev.st = 403;
|
|
160
|
+
ev.blk = "challenge";
|
|
161
|
+
engine2.queue.push(ev);
|
|
162
|
+
engine2.queue.flush(waitUntil);
|
|
163
|
+
}
|
|
164
|
+
if (!(0, import_core3.wantsHtml)(req.headers.get("accept"), req.headers.get("sec-fetch-dest"))) {
|
|
165
|
+
return new Response('{"error":"challenge_required"}', {
|
|
166
|
+
status: 403,
|
|
167
|
+
headers: { "content-type": "application/json", "cache-control": "no-store", "x-camada-challenge": "1" }
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
const html = (0, import_core3.challengePage)({ nonce: await kitFor(engine2).nonce(ip, Date.now()), action: VERIFY_PATH, to: (0, import_core3.safeReturnTo)(target) });
|
|
171
|
+
return new Response(html, {
|
|
172
|
+
status: 403,
|
|
173
|
+
headers: { "content-type": "text/html; charset=utf-8", "cache-control": "no-store", "x-camada-challenge": "1" }
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
async function verifyChallenge(engine2, req) {
|
|
177
|
+
const url = new URL(req.url);
|
|
178
|
+
const ip = clientIp(engine2, req);
|
|
179
|
+
const kit = kitFor(engine2);
|
|
180
|
+
if (Number(req.headers.get("content-length")) > BODY_MAX) return new Response(null, { status: 413 });
|
|
181
|
+
const body = await req.text();
|
|
182
|
+
if (body.length > BODY_MAX) return new Response(null, { status: 413 });
|
|
183
|
+
const form = (0, import_core3.parseFormBody)(body);
|
|
184
|
+
const to = (0, import_core3.safeReturnTo)(form.to);
|
|
185
|
+
const now = Date.now();
|
|
186
|
+
if (!ip || !await kit.verify(ip, now, form.nonce, form.solution)) {
|
|
187
|
+
const html = ip ? (0, import_core3.challengePage)({ nonce: await kit.nonce(ip, now), action: url.pathname, to }) : "Forbidden";
|
|
188
|
+
return new Response(html, { status: 403, headers: { "content-type": ip ? "text/html; charset=utf-8" : "text/plain", "cache-control": "no-store" } });
|
|
189
|
+
}
|
|
190
|
+
const ev = buildEvent(req, url.pathname, ip, crypto.randomUUID(), cookieValue(req.headers.get("cookie") || "", SESSION_COOKIE), false);
|
|
191
|
+
ev.st = 200;
|
|
192
|
+
ev.ch = 1;
|
|
193
|
+
engine2.queue.push(ev);
|
|
194
|
+
void engine2.queue.flush();
|
|
195
|
+
return new Response(null, {
|
|
196
|
+
status: 302,
|
|
197
|
+
headers: {
|
|
198
|
+
location: to,
|
|
199
|
+
"set-cookie": (0, import_core3.challengeCookie)(await kit.issue(ip, now), url.protocol === "https:"),
|
|
200
|
+
"cache-control": "no-store"
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
async function challengeGate(req) {
|
|
205
|
+
if (isDisabled() || !challengeEnabled()) return null;
|
|
206
|
+
const engine2 = getEngine();
|
|
207
|
+
if (!engine2) return null;
|
|
208
|
+
const ip = clientIp(engine2, req);
|
|
209
|
+
if (!ip || await challengePassed(engine2, req, ip)) return null;
|
|
210
|
+
const url = new URL(req.url);
|
|
211
|
+
return serveChallenge(engine2, req, ip, url.pathname + url.search, void 0, !req.headers.has("x-camada-rid"));
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// src/middleware.ts
|
|
215
|
+
function capture(engine2, req, path, ip, existingSid, warnRule, waitUntil) {
|
|
216
|
+
const rid = crypto.randomUUID();
|
|
217
|
+
const sid = existingSid ?? crypto.randomUUID();
|
|
218
|
+
const cfg = engine2.snap.config;
|
|
219
|
+
const excluded = (cfg?.exclude || []).some((x) => path.startsWith(x));
|
|
220
|
+
if (!excluded && Math.random() < (cfg?.sample ?? 1)) {
|
|
221
|
+
const ev = buildEvent(req, path, ip, rid, sid, !existingSid);
|
|
222
|
+
if (warnRule) ev.wrn = warnRule;
|
|
223
|
+
engine2.queue.push(ev);
|
|
224
|
+
engine2.queue.flush(waitUntil);
|
|
225
|
+
}
|
|
226
|
+
const headers = new Headers(req.headers);
|
|
227
|
+
headers.set("x-camada-rid", rid);
|
|
228
|
+
const res = import_server.NextResponse.next({ request: { headers } });
|
|
229
|
+
res.headers.set("x-rid", rid);
|
|
230
|
+
if (!existingSid) {
|
|
231
|
+
const secure = new URL(req.url).protocol === "https:" ? "; Secure" : "";
|
|
232
|
+
res.headers.append("set-cookie", `${SESSION_COOKIE}=${sid}; Path=/; Max-Age=2592000; HttpOnly; SameSite=Lax${secure}`);
|
|
233
|
+
}
|
|
234
|
+
return res;
|
|
235
|
+
}
|
|
236
|
+
function camada(_options) {
|
|
237
|
+
return function camadaMiddleware(req, event) {
|
|
238
|
+
try {
|
|
239
|
+
if (isDisabled()) return void 0;
|
|
240
|
+
const engine2 = getEngine();
|
|
241
|
+
if (!engine2) return void 0;
|
|
242
|
+
const waitUntil = event?.waitUntil ? (p) => event.waitUntil(p) : void 0;
|
|
243
|
+
engine2.snap.ensureFresh(waitUntil);
|
|
244
|
+
const ip = (0, import_core4.resolveClientIp)(null, req.headers.get("x-forwarded-for"), trustedProxy(engine2));
|
|
245
|
+
const path = new URL(req.url).pathname;
|
|
246
|
+
const existingSid = cookieValue(req.headers.get("cookie") || "", SESSION_COOKIE);
|
|
247
|
+
const v = engine2.snap.verdict({
|
|
248
|
+
ip,
|
|
249
|
+
path,
|
|
250
|
+
ua: req.headers.get("user-agent"),
|
|
251
|
+
header: (n) => req.headers.get(n)
|
|
252
|
+
});
|
|
253
|
+
if (v.block) {
|
|
254
|
+
const ev = buildEvent(req, path, ip, crypto.randomUUID(), existingSid, false);
|
|
255
|
+
ev.st = 403;
|
|
256
|
+
ev.blk = v.reason;
|
|
257
|
+
if (v.rule) ev.rl = v.rule;
|
|
258
|
+
engine2.queue.push(ev);
|
|
259
|
+
engine2.queue.flush(waitUntil);
|
|
260
|
+
const headers = {
|
|
261
|
+
"content-type": "text/plain",
|
|
262
|
+
"x-block-reason": String(v.reason ?? ""),
|
|
263
|
+
"x-block-version": v.version ?? ""
|
|
264
|
+
};
|
|
265
|
+
if (v.rule) headers["x-block-rule"] = v.rule;
|
|
266
|
+
return new Response("Forbidden", { status: 403, headers });
|
|
267
|
+
}
|
|
268
|
+
const warnRule = v.warn ? v.rule ?? null : null;
|
|
269
|
+
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);
|
|
271
|
+
}
|
|
272
|
+
return capture(engine2, req, path, ip, existingSid, warnRule, waitUntil);
|
|
273
|
+
} catch (err) {
|
|
274
|
+
(0, import_core4.logRateLimited)(err);
|
|
275
|
+
return void 0;
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// src/route.ts
|
|
281
|
+
var import_iife_string = __toESM(require("@camada/browser/iife-string"), 1);
|
|
282
|
+
var import_core5 = require("@camada/core");
|
|
283
|
+
var FP_MAX = 32 * 1024;
|
|
284
|
+
var encoder = new TextEncoder();
|
|
285
|
+
var notFound = () => new Response(null, { status: 404 });
|
|
286
|
+
var noContent = () => new Response(null, { status: 204, headers: { "cache-control": "no-store" } });
|
|
287
|
+
function blocked(engine2, req) {
|
|
288
|
+
const ip = (0, import_core5.resolveClientIp)(null, req.headers.get("x-forwarded-for"), trustedProxy(engine2));
|
|
289
|
+
const path = new URL(req.url).pathname;
|
|
290
|
+
const v = engine2.snap.verdict({ ip, path, ua: req.headers.get("user-agent"), header: (n) => req.headers.get(n) });
|
|
291
|
+
if (!v.block) return null;
|
|
292
|
+
const ev = buildEvent(req, path, ip, crypto.randomUUID(), cookieValue(req.headers.get("cookie") || "", SESSION_COOKIE), false);
|
|
293
|
+
ev.st = 403;
|
|
294
|
+
ev.blk = v.reason;
|
|
295
|
+
if (v.rule) ev.rl = v.rule;
|
|
296
|
+
engine2.queue.push(ev);
|
|
297
|
+
void engine2.queue.flush();
|
|
298
|
+
const headers = { "x-block-reason": String(v.reason ?? ""), "x-block-version": v.version ?? "" };
|
|
299
|
+
if (v.rule) headers["x-block-rule"] = v.rule;
|
|
300
|
+
return new Response("Forbidden", { status: 403, headers });
|
|
301
|
+
}
|
|
302
|
+
function lastSegment(req) {
|
|
303
|
+
const parts = new URL(req.url).pathname.split("/");
|
|
304
|
+
return parts[parts.length - 1];
|
|
305
|
+
}
|
|
306
|
+
function activeEngine() {
|
|
307
|
+
return isDisabled() ? null : getEngine();
|
|
308
|
+
}
|
|
309
|
+
var beaconOff = (engine2) => engine2.snap.config?.beacon === false;
|
|
310
|
+
function camadaRoute() {
|
|
311
|
+
return {
|
|
312
|
+
GET: (req) => (0, import_core5.guardedAsync)(async () => {
|
|
313
|
+
if (lastSegment(req) !== "b.js") return notFound();
|
|
314
|
+
const engine2 = activeEngine();
|
|
315
|
+
if (!engine2) return notFound();
|
|
316
|
+
engine2.snap.ensureFresh();
|
|
317
|
+
const deny = blocked(engine2, req);
|
|
318
|
+
if (deny) return deny;
|
|
319
|
+
if (beaconOff(engine2)) return notFound();
|
|
320
|
+
return new Response(import_iife_string.default, {
|
|
321
|
+
status: 200,
|
|
322
|
+
headers: { "content-type": "application/javascript", "cache-control": "public, max-age=3600" }
|
|
323
|
+
});
|
|
324
|
+
}, notFound()),
|
|
325
|
+
POST: (req) => (0, import_core5.guardedAsync)(async () => {
|
|
326
|
+
if (lastSegment(req) === "challenge") {
|
|
327
|
+
const engine3 = activeEngine();
|
|
328
|
+
if (!engine3 || !challengeEnabled()) return notFound();
|
|
329
|
+
engine3.snap.ensureFresh();
|
|
330
|
+
const deny = blocked(engine3, req);
|
|
331
|
+
if (deny) return deny;
|
|
332
|
+
return verifyChallenge(engine3, req);
|
|
333
|
+
}
|
|
334
|
+
if (lastSegment(req) !== "fp") return notFound();
|
|
335
|
+
const body = await req.text();
|
|
336
|
+
if (encoder.encode(body).byteLength > FP_MAX) return new Response(null, { status: 413 });
|
|
337
|
+
const engine2 = activeEngine();
|
|
338
|
+
if (engine2) {
|
|
339
|
+
engine2.snap.ensureFresh();
|
|
340
|
+
const deny = blocked(engine2, req);
|
|
341
|
+
if (deny) return deny;
|
|
342
|
+
if (beaconOff(engine2)) return notFound();
|
|
343
|
+
const ip = (0, import_core5.resolveClientIp)(null, req.headers.get("x-forwarded-for"), trustedProxy(engine2));
|
|
344
|
+
let parsed;
|
|
345
|
+
try {
|
|
346
|
+
parsed = JSON.parse(body);
|
|
347
|
+
} catch {
|
|
348
|
+
return noContent();
|
|
349
|
+
}
|
|
350
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
351
|
+
engine2.queue.push({ ...parsed, sig: 1, ip, tap: import_core5.TAP_NEXT });
|
|
352
|
+
void engine2.queue.flush();
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
return noContent();
|
|
356
|
+
}, noContent())
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// src/beacon.tsx
|
|
361
|
+
var import_react = require("@camada/react");
|
|
362
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
363
|
+
async function CamadaBeacon({ src = "/api/camada/b.js" } = {}) {
|
|
364
|
+
let rid = null;
|
|
365
|
+
try {
|
|
366
|
+
const { headers } = await import("next/headers");
|
|
367
|
+
rid = (await headers()).get("x-camada-rid");
|
|
368
|
+
} catch {
|
|
369
|
+
}
|
|
370
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react.CamadaBeacon, { src, rid });
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// src/track.ts
|
|
374
|
+
var import_core6 = require("@camada/core");
|
|
375
|
+
var SESSION_COOKIE2 = "_sfp";
|
|
376
|
+
async function track(event, data) {
|
|
377
|
+
try {
|
|
378
|
+
if (isDisabled()) return;
|
|
379
|
+
const engine2 = getEngine();
|
|
380
|
+
if (!engine2) return;
|
|
381
|
+
const { headers, cookies } = await import("next/headers");
|
|
382
|
+
const [h, c] = await Promise.all([headers(), cookies()]);
|
|
383
|
+
const uid = data?.user ? await (0, import_core6.hashUserId)(data.user, engine2.env.ingestToken) : null;
|
|
384
|
+
engine2.queue.push({
|
|
385
|
+
tap: import_core6.TAP_NEXT,
|
|
386
|
+
et: event,
|
|
387
|
+
uid,
|
|
388
|
+
rid: h.get("x-camada-rid"),
|
|
389
|
+
sid: c.get(SESSION_COOKIE2)?.value ?? null,
|
|
390
|
+
ip: (0, import_core6.resolveClientIp)(null, h.get("x-forwarded-for"), trustedProxy(engine2)),
|
|
391
|
+
ts: Date.now()
|
|
392
|
+
});
|
|
393
|
+
void engine2.queue.flush();
|
|
394
|
+
} catch (err) {
|
|
395
|
+
(0, import_core6.logRateLimited)(err);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
399
|
+
0 && (module.exports = {
|
|
400
|
+
CamadaBeacon,
|
|
401
|
+
camada,
|
|
402
|
+
camadaRoute,
|
|
403
|
+
challengeGate,
|
|
404
|
+
track
|
|
405
|
+
});
|
|
406
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +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"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { NextRequest, NextFetchEvent } from 'next/server';
|
|
2
|
+
import * as react from 'react';
|
|
3
|
+
|
|
4
|
+
interface CamadaMiddlewareOptions {
|
|
5
|
+
}
|
|
6
|
+
/** Next accepts a promise here; only the challenge branch returns one (it awaits WebCrypto). */
|
|
7
|
+
type MiddlewareResult = Response | undefined | Promise<Response | undefined>;
|
|
8
|
+
/**
|
|
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.
|
|
11
|
+
*/
|
|
12
|
+
declare function camada(_options?: CamadaMiddlewareOptions): (req: NextRequest, event: NextFetchEvent) => MiddlewareResult;
|
|
13
|
+
|
|
14
|
+
declare function camadaRoute(): {
|
|
15
|
+
GET: (req: Request) => Promise<Response>;
|
|
16
|
+
POST: (req: Request) => Promise<Response>;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/** For a route that wants to gate itself (the example's /challenge-me): the challenge Response,
|
|
20
|
+
* or null when this client already holds a valid `_cch` and the route should render normally.
|
|
21
|
+
*
|
|
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
|
|
24
|
+
* the page WITHOUT a second row, which would double-count the request at the analyst. Exclude
|
|
25
|
+
* the gated route from the middleware matcher and the gate ships the `blk: "challenge"` row
|
|
26
|
+
* itself — that is the configuration to use when you want the challenge counted. */
|
|
27
|
+
declare function challengeGate(req: Request): Promise<Response | null>;
|
|
28
|
+
|
|
29
|
+
interface CamadaBeaconProps {
|
|
30
|
+
/** Where camadaRoute() serves the beacon script. Default: '/api/camada/b.js'. */
|
|
31
|
+
src?: string;
|
|
32
|
+
}
|
|
33
|
+
declare function CamadaBeacon({ src }?: CamadaBeaconProps): Promise<react.JSX.Element>;
|
|
34
|
+
|
|
35
|
+
/** Never throws, never blocks the response: a camada problem must not break a login. */
|
|
36
|
+
declare function track(event: string, data?: {
|
|
37
|
+
user?: string;
|
|
38
|
+
}): Promise<void>;
|
|
39
|
+
|
|
40
|
+
export { CamadaBeacon, type CamadaBeaconProps, type CamadaMiddlewareOptions, type MiddlewareResult, camada, camadaRoute, challengeGate, track };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { NextRequest, NextFetchEvent } from 'next/server';
|
|
2
|
+
import * as react from 'react';
|
|
3
|
+
|
|
4
|
+
interface CamadaMiddlewareOptions {
|
|
5
|
+
}
|
|
6
|
+
/** Next accepts a promise here; only the challenge branch returns one (it awaits WebCrypto). */
|
|
7
|
+
type MiddlewareResult = Response | undefined | Promise<Response | undefined>;
|
|
8
|
+
/**
|
|
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.
|
|
11
|
+
*/
|
|
12
|
+
declare function camada(_options?: CamadaMiddlewareOptions): (req: NextRequest, event: NextFetchEvent) => MiddlewareResult;
|
|
13
|
+
|
|
14
|
+
declare function camadaRoute(): {
|
|
15
|
+
GET: (req: Request) => Promise<Response>;
|
|
16
|
+
POST: (req: Request) => Promise<Response>;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/** For a route that wants to gate itself (the example's /challenge-me): the challenge Response,
|
|
20
|
+
* or null when this client already holds a valid `_cch` and the route should render normally.
|
|
21
|
+
*
|
|
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
|
|
24
|
+
* the page WITHOUT a second row, which would double-count the request at the analyst. Exclude
|
|
25
|
+
* the gated route from the middleware matcher and the gate ships the `blk: "challenge"` row
|
|
26
|
+
* itself — that is the configuration to use when you want the challenge counted. */
|
|
27
|
+
declare function challengeGate(req: Request): Promise<Response | null>;
|
|
28
|
+
|
|
29
|
+
interface CamadaBeaconProps {
|
|
30
|
+
/** Where camadaRoute() serves the beacon script. Default: '/api/camada/b.js'. */
|
|
31
|
+
src?: string;
|
|
32
|
+
}
|
|
33
|
+
declare function CamadaBeacon({ src }?: CamadaBeaconProps): Promise<react.JSX.Element>;
|
|
34
|
+
|
|
35
|
+
/** Never throws, never blocks the response: a camada problem must not break a login. */
|
|
36
|
+
declare function track(event: string, data?: {
|
|
37
|
+
user?: string;
|
|
38
|
+
}): Promise<void>;
|
|
39
|
+
|
|
40
|
+
export { CamadaBeacon, type CamadaBeaconProps, type CamadaMiddlewareOptions, type MiddlewareResult, camada, camadaRoute, challengeGate, track };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
// src/middleware.ts
|
|
2
|
+
import { NextResponse } from "next/server";
|
|
3
|
+
import { resolveClientIp as resolveClientIp2, logRateLimited as logRateLimited2, guardedAsync } from "@camada/core";
|
|
4
|
+
|
|
5
|
+
// src/engine.ts
|
|
6
|
+
import {
|
|
7
|
+
SnapshotClient,
|
|
8
|
+
EventQueue,
|
|
9
|
+
parseKey,
|
|
10
|
+
parseTrustedProxyEnv,
|
|
11
|
+
logRateLimited
|
|
12
|
+
} from "@camada/core";
|
|
13
|
+
|
|
14
|
+
// package.json
|
|
15
|
+
var version = "0.2.0";
|
|
16
|
+
var name = "@camada/next";
|
|
17
|
+
|
|
18
|
+
// src/version.ts
|
|
19
|
+
var SDK_ID = `${name}/${version}`;
|
|
20
|
+
|
|
21
|
+
// src/engine.ts
|
|
22
|
+
function resolveEnv(env) {
|
|
23
|
+
const key = parseKey(env.CAMADA_KEY);
|
|
24
|
+
const ingestToken = key?.ingestToken ?? env.CAMADA_TOKEN;
|
|
25
|
+
const snapToken = key?.snapToken ?? env.CAMADA_SNAPSHOT_TOKEN;
|
|
26
|
+
if (!ingestToken || !snapToken) return null;
|
|
27
|
+
const ingestUrl = (env.CAMADA_INGEST_URL || "https://in.camada.dev").replace(/\/$/, "");
|
|
28
|
+
return {
|
|
29
|
+
ingestToken,
|
|
30
|
+
snapToken,
|
|
31
|
+
ingestUrl,
|
|
32
|
+
secret: env.CAMADA_KEY || `${ingestToken}.${snapToken}`,
|
|
33
|
+
snapshotUrl: env.CAMADA_SNAPSHOT_URL || `${ingestUrl}/snapshot`,
|
|
34
|
+
// On Vercel the platform overwrites X-Forwarded-For, so its rightmost entry is
|
|
35
|
+
// trustworthy: default to vercel mode there unless explicitly overridden.
|
|
36
|
+
trustedProxy: parseTrustedProxyEnv(env.CAMADA_TRUSTED_PROXY) ?? (env.VERCEL ? { mode: "vercel" } : null)
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
var engine;
|
|
40
|
+
var overrides = {};
|
|
41
|
+
function envSource() {
|
|
42
|
+
return overrides.env ?? (typeof process !== "undefined" && process.env ? process.env : {});
|
|
43
|
+
}
|
|
44
|
+
function isDisabled() {
|
|
45
|
+
return envSource().CAMADA_DISABLED === "1";
|
|
46
|
+
}
|
|
47
|
+
function challengeEnabled() {
|
|
48
|
+
return overrides.challenge !== false && envSource().CAMADA_CHALLENGE !== "0";
|
|
49
|
+
}
|
|
50
|
+
function getEngine() {
|
|
51
|
+
if (engine !== void 0) return engine;
|
|
52
|
+
const env = resolveEnv(envSource());
|
|
53
|
+
if (!env) {
|
|
54
|
+
logRateLimited(new Error("CAMADA_KEY (or CAMADA_TOKEN + CAMADA_SNAPSHOT_TOKEN) not set \u2014 camada is inactive"));
|
|
55
|
+
return engine = null;
|
|
56
|
+
}
|
|
57
|
+
const fetchImpl = overrides.fetchImpl;
|
|
58
|
+
const injected = fetchImpl ? { fetchImpl } : {};
|
|
59
|
+
engine = {
|
|
60
|
+
env,
|
|
61
|
+
fetchImpl: fetchImpl ?? fetch,
|
|
62
|
+
// snapshotVersion left undefined takes core's default (5); the client drops undefined keys.
|
|
63
|
+
snap: new SnapshotClient({ url: env.snapshotUrl, token: env.snapToken, mode: "lazy", sdk: SDK_ID, snapshotVersion: overrides.snapshotVersion, ...injected }),
|
|
64
|
+
queue: new EventQueue({ url: env.ingestUrl, token: env.ingestToken, sdk: SDK_ID, ...injected })
|
|
65
|
+
};
|
|
66
|
+
return engine;
|
|
67
|
+
}
|
|
68
|
+
function trustedProxy(e) {
|
|
69
|
+
return e.env.trustedProxy ?? e.snap.config?.trusted_proxy ?? null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// src/challenge.ts
|
|
73
|
+
import {
|
|
74
|
+
createChallengeAsync,
|
|
75
|
+
challengePage,
|
|
76
|
+
challengeCookie,
|
|
77
|
+
safeReturnTo,
|
|
78
|
+
wantsHtml,
|
|
79
|
+
parseFormBody,
|
|
80
|
+
resolveClientIp,
|
|
81
|
+
CHALLENGE_COOKIE
|
|
82
|
+
} from "@camada/core";
|
|
83
|
+
|
|
84
|
+
// src/event.ts
|
|
85
|
+
import { buildWireEvent, TAP_NEXT } from "@camada/core";
|
|
86
|
+
function buildEvent(req, path, ip, rid, sid, newSession) {
|
|
87
|
+
const url = new URL(req.url);
|
|
88
|
+
return buildWireEvent(
|
|
89
|
+
{
|
|
90
|
+
method: req.method,
|
|
91
|
+
host: req.headers.get("host") ?? url.host,
|
|
92
|
+
path,
|
|
93
|
+
query: url.search,
|
|
94
|
+
// The edge runtime sorts header names, so hord is alphabetical at this tap — still
|
|
95
|
+
// shipped; the scorer knows sdk-next lacks the raw-wire-order signal (capability mask).
|
|
96
|
+
headers: [...req.headers.entries()],
|
|
97
|
+
ip,
|
|
98
|
+
httpVersion: null
|
|
99
|
+
// not observable at this position
|
|
100
|
+
},
|
|
101
|
+
{ tap: TAP_NEXT, rid, sid, newSession, ja4: req.headers.get("x-vercel-ja4-digest") }
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
var SESSION_COOKIE = "_sfp";
|
|
105
|
+
var cookieValue = (cookie, name2) => {
|
|
106
|
+
const src = "; " + cookie;
|
|
107
|
+
const i = src.indexOf("; " + name2 + "=");
|
|
108
|
+
if (i === -1) return null;
|
|
109
|
+
const start = i + name2.length + 3;
|
|
110
|
+
const j = src.indexOf(";", start);
|
|
111
|
+
return src.slice(start, j === -1 ? void 0 : j);
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
// src/challenge.ts
|
|
115
|
+
var VERIFY_PATH = "/api/camada/challenge";
|
|
116
|
+
var BODY_MAX = 4 * 1024;
|
|
117
|
+
var isChallengeRoute = (path) => /\/camada\/challenge\/?$/.test(path);
|
|
118
|
+
var kits = /* @__PURE__ */ new WeakMap();
|
|
119
|
+
function kitFor(engine2) {
|
|
120
|
+
let k = kits.get(engine2);
|
|
121
|
+
if (!k) {
|
|
122
|
+
k = createChallengeAsync({ secret: engine2.env.secret });
|
|
123
|
+
kits.set(engine2, k);
|
|
124
|
+
}
|
|
125
|
+
return k;
|
|
126
|
+
}
|
|
127
|
+
var clientIp = (engine2, req) => resolveClientIp(null, req.headers.get("x-forwarded-for"), trustedProxy(engine2));
|
|
128
|
+
function challengePassed(engine2, req, ip) {
|
|
129
|
+
return kitFor(engine2).tokenValid(ip, Date.now(), cookieValue(req.headers.get("cookie") || "", CHALLENGE_COOKIE));
|
|
130
|
+
}
|
|
131
|
+
async function serveChallenge(engine2, req, ip, target, waitUntil, ship = true) {
|
|
132
|
+
if (ship) {
|
|
133
|
+
const ev = buildEvent(req, new URL(req.url).pathname, ip, crypto.randomUUID(), cookieValue(req.headers.get("cookie") || "", SESSION_COOKIE), false);
|
|
134
|
+
ev.st = 403;
|
|
135
|
+
ev.blk = "challenge";
|
|
136
|
+
engine2.queue.push(ev);
|
|
137
|
+
engine2.queue.flush(waitUntil);
|
|
138
|
+
}
|
|
139
|
+
if (!wantsHtml(req.headers.get("accept"), req.headers.get("sec-fetch-dest"))) {
|
|
140
|
+
return new Response('{"error":"challenge_required"}', {
|
|
141
|
+
status: 403,
|
|
142
|
+
headers: { "content-type": "application/json", "cache-control": "no-store", "x-camada-challenge": "1" }
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
const html = challengePage({ nonce: await kitFor(engine2).nonce(ip, Date.now()), action: VERIFY_PATH, to: safeReturnTo(target) });
|
|
146
|
+
return new Response(html, {
|
|
147
|
+
status: 403,
|
|
148
|
+
headers: { "content-type": "text/html; charset=utf-8", "cache-control": "no-store", "x-camada-challenge": "1" }
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
async function verifyChallenge(engine2, req) {
|
|
152
|
+
const url = new URL(req.url);
|
|
153
|
+
const ip = clientIp(engine2, req);
|
|
154
|
+
const kit = kitFor(engine2);
|
|
155
|
+
if (Number(req.headers.get("content-length")) > BODY_MAX) return new Response(null, { status: 413 });
|
|
156
|
+
const body = await req.text();
|
|
157
|
+
if (body.length > BODY_MAX) return new Response(null, { status: 413 });
|
|
158
|
+
const form = parseFormBody(body);
|
|
159
|
+
const to = safeReturnTo(form.to);
|
|
160
|
+
const now = Date.now();
|
|
161
|
+
if (!ip || !await kit.verify(ip, now, form.nonce, form.solution)) {
|
|
162
|
+
const html = ip ? challengePage({ nonce: await kit.nonce(ip, now), action: url.pathname, to }) : "Forbidden";
|
|
163
|
+
return new Response(html, { status: 403, headers: { "content-type": ip ? "text/html; charset=utf-8" : "text/plain", "cache-control": "no-store" } });
|
|
164
|
+
}
|
|
165
|
+
const ev = buildEvent(req, url.pathname, ip, crypto.randomUUID(), cookieValue(req.headers.get("cookie") || "", SESSION_COOKIE), false);
|
|
166
|
+
ev.st = 200;
|
|
167
|
+
ev.ch = 1;
|
|
168
|
+
engine2.queue.push(ev);
|
|
169
|
+
void engine2.queue.flush();
|
|
170
|
+
return new Response(null, {
|
|
171
|
+
status: 302,
|
|
172
|
+
headers: {
|
|
173
|
+
location: to,
|
|
174
|
+
"set-cookie": challengeCookie(await kit.issue(ip, now), url.protocol === "https:"),
|
|
175
|
+
"cache-control": "no-store"
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
async function challengeGate(req) {
|
|
180
|
+
if (isDisabled() || !challengeEnabled()) return null;
|
|
181
|
+
const engine2 = getEngine();
|
|
182
|
+
if (!engine2) return null;
|
|
183
|
+
const ip = clientIp(engine2, req);
|
|
184
|
+
if (!ip || await challengePassed(engine2, req, ip)) return null;
|
|
185
|
+
const url = new URL(req.url);
|
|
186
|
+
return serveChallenge(engine2, req, ip, url.pathname + url.search, void 0, !req.headers.has("x-camada-rid"));
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// src/middleware.ts
|
|
190
|
+
function capture(engine2, req, path, ip, existingSid, warnRule, waitUntil) {
|
|
191
|
+
const rid = crypto.randomUUID();
|
|
192
|
+
const sid = existingSid ?? crypto.randomUUID();
|
|
193
|
+
const cfg = engine2.snap.config;
|
|
194
|
+
const excluded = (cfg?.exclude || []).some((x) => path.startsWith(x));
|
|
195
|
+
if (!excluded && Math.random() < (cfg?.sample ?? 1)) {
|
|
196
|
+
const ev = buildEvent(req, path, ip, rid, sid, !existingSid);
|
|
197
|
+
if (warnRule) ev.wrn = warnRule;
|
|
198
|
+
engine2.queue.push(ev);
|
|
199
|
+
engine2.queue.flush(waitUntil);
|
|
200
|
+
}
|
|
201
|
+
const headers = new Headers(req.headers);
|
|
202
|
+
headers.set("x-camada-rid", rid);
|
|
203
|
+
const res = NextResponse.next({ request: { headers } });
|
|
204
|
+
res.headers.set("x-rid", rid);
|
|
205
|
+
if (!existingSid) {
|
|
206
|
+
const secure = new URL(req.url).protocol === "https:" ? "; Secure" : "";
|
|
207
|
+
res.headers.append("set-cookie", `${SESSION_COOKIE}=${sid}; Path=/; Max-Age=2592000; HttpOnly; SameSite=Lax${secure}`);
|
|
208
|
+
}
|
|
209
|
+
return res;
|
|
210
|
+
}
|
|
211
|
+
function camada(_options) {
|
|
212
|
+
return function camadaMiddleware(req, event) {
|
|
213
|
+
try {
|
|
214
|
+
if (isDisabled()) return void 0;
|
|
215
|
+
const engine2 = getEngine();
|
|
216
|
+
if (!engine2) return void 0;
|
|
217
|
+
const waitUntil = event?.waitUntil ? (p) => event.waitUntil(p) : void 0;
|
|
218
|
+
engine2.snap.ensureFresh(waitUntil);
|
|
219
|
+
const ip = resolveClientIp2(null, req.headers.get("x-forwarded-for"), trustedProxy(engine2));
|
|
220
|
+
const path = new URL(req.url).pathname;
|
|
221
|
+
const existingSid = cookieValue(req.headers.get("cookie") || "", SESSION_COOKIE);
|
|
222
|
+
const v = engine2.snap.verdict({
|
|
223
|
+
ip,
|
|
224
|
+
path,
|
|
225
|
+
ua: req.headers.get("user-agent"),
|
|
226
|
+
header: (n) => req.headers.get(n)
|
|
227
|
+
});
|
|
228
|
+
if (v.block) {
|
|
229
|
+
const ev = buildEvent(req, path, ip, crypto.randomUUID(), existingSid, false);
|
|
230
|
+
ev.st = 403;
|
|
231
|
+
ev.blk = v.reason;
|
|
232
|
+
if (v.rule) ev.rl = v.rule;
|
|
233
|
+
engine2.queue.push(ev);
|
|
234
|
+
engine2.queue.flush(waitUntil);
|
|
235
|
+
const headers = {
|
|
236
|
+
"content-type": "text/plain",
|
|
237
|
+
"x-block-reason": String(v.reason ?? ""),
|
|
238
|
+
"x-block-version": v.version ?? ""
|
|
239
|
+
};
|
|
240
|
+
if (v.rule) headers["x-block-rule"] = v.rule;
|
|
241
|
+
return new Response("Forbidden", { status: 403, headers });
|
|
242
|
+
}
|
|
243
|
+
const warnRule = v.warn ? v.rule ?? null : null;
|
|
244
|
+
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);
|
|
246
|
+
}
|
|
247
|
+
return capture(engine2, req, path, ip, existingSid, warnRule, waitUntil);
|
|
248
|
+
} catch (err) {
|
|
249
|
+
logRateLimited2(err);
|
|
250
|
+
return void 0;
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// src/route.ts
|
|
256
|
+
import iife from "@camada/browser/iife-string";
|
|
257
|
+
import { guardedAsync as guardedAsync2, resolveClientIp as resolveClientIp3, TAP_NEXT as TAP_NEXT2 } from "@camada/core";
|
|
258
|
+
var FP_MAX = 32 * 1024;
|
|
259
|
+
var encoder = new TextEncoder();
|
|
260
|
+
var notFound = () => new Response(null, { status: 404 });
|
|
261
|
+
var noContent = () => new Response(null, { status: 204, headers: { "cache-control": "no-store" } });
|
|
262
|
+
function blocked(engine2, req) {
|
|
263
|
+
const ip = resolveClientIp3(null, req.headers.get("x-forwarded-for"), trustedProxy(engine2));
|
|
264
|
+
const path = new URL(req.url).pathname;
|
|
265
|
+
const v = engine2.snap.verdict({ ip, path, ua: req.headers.get("user-agent"), header: (n) => req.headers.get(n) });
|
|
266
|
+
if (!v.block) return null;
|
|
267
|
+
const ev = buildEvent(req, path, ip, crypto.randomUUID(), cookieValue(req.headers.get("cookie") || "", SESSION_COOKIE), false);
|
|
268
|
+
ev.st = 403;
|
|
269
|
+
ev.blk = v.reason;
|
|
270
|
+
if (v.rule) ev.rl = v.rule;
|
|
271
|
+
engine2.queue.push(ev);
|
|
272
|
+
void engine2.queue.flush();
|
|
273
|
+
const headers = { "x-block-reason": String(v.reason ?? ""), "x-block-version": v.version ?? "" };
|
|
274
|
+
if (v.rule) headers["x-block-rule"] = v.rule;
|
|
275
|
+
return new Response("Forbidden", { status: 403, headers });
|
|
276
|
+
}
|
|
277
|
+
function lastSegment(req) {
|
|
278
|
+
const parts = new URL(req.url).pathname.split("/");
|
|
279
|
+
return parts[parts.length - 1];
|
|
280
|
+
}
|
|
281
|
+
function activeEngine() {
|
|
282
|
+
return isDisabled() ? null : getEngine();
|
|
283
|
+
}
|
|
284
|
+
var beaconOff = (engine2) => engine2.snap.config?.beacon === false;
|
|
285
|
+
function camadaRoute() {
|
|
286
|
+
return {
|
|
287
|
+
GET: (req) => guardedAsync2(async () => {
|
|
288
|
+
if (lastSegment(req) !== "b.js") return notFound();
|
|
289
|
+
const engine2 = activeEngine();
|
|
290
|
+
if (!engine2) return notFound();
|
|
291
|
+
engine2.snap.ensureFresh();
|
|
292
|
+
const deny = blocked(engine2, req);
|
|
293
|
+
if (deny) return deny;
|
|
294
|
+
if (beaconOff(engine2)) return notFound();
|
|
295
|
+
return new Response(iife, {
|
|
296
|
+
status: 200,
|
|
297
|
+
headers: { "content-type": "application/javascript", "cache-control": "public, max-age=3600" }
|
|
298
|
+
});
|
|
299
|
+
}, notFound()),
|
|
300
|
+
POST: (req) => guardedAsync2(async () => {
|
|
301
|
+
if (lastSegment(req) === "challenge") {
|
|
302
|
+
const engine3 = activeEngine();
|
|
303
|
+
if (!engine3 || !challengeEnabled()) return notFound();
|
|
304
|
+
engine3.snap.ensureFresh();
|
|
305
|
+
const deny = blocked(engine3, req);
|
|
306
|
+
if (deny) return deny;
|
|
307
|
+
return verifyChallenge(engine3, req);
|
|
308
|
+
}
|
|
309
|
+
if (lastSegment(req) !== "fp") return notFound();
|
|
310
|
+
const body = await req.text();
|
|
311
|
+
if (encoder.encode(body).byteLength > FP_MAX) return new Response(null, { status: 413 });
|
|
312
|
+
const engine2 = activeEngine();
|
|
313
|
+
if (engine2) {
|
|
314
|
+
engine2.snap.ensureFresh();
|
|
315
|
+
const deny = blocked(engine2, req);
|
|
316
|
+
if (deny) return deny;
|
|
317
|
+
if (beaconOff(engine2)) return notFound();
|
|
318
|
+
const ip = resolveClientIp3(null, req.headers.get("x-forwarded-for"), trustedProxy(engine2));
|
|
319
|
+
let parsed;
|
|
320
|
+
try {
|
|
321
|
+
parsed = JSON.parse(body);
|
|
322
|
+
} catch {
|
|
323
|
+
return noContent();
|
|
324
|
+
}
|
|
325
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
326
|
+
engine2.queue.push({ ...parsed, sig: 1, ip, tap: TAP_NEXT2 });
|
|
327
|
+
void engine2.queue.flush();
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
return noContent();
|
|
331
|
+
}, noContent())
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// src/beacon.tsx
|
|
336
|
+
import { CamadaBeacon as Beacon } from "@camada/react";
|
|
337
|
+
import { jsx } from "react/jsx-runtime";
|
|
338
|
+
async function CamadaBeacon({ src = "/api/camada/b.js" } = {}) {
|
|
339
|
+
let rid = null;
|
|
340
|
+
try {
|
|
341
|
+
const { headers } = await import("next/headers");
|
|
342
|
+
rid = (await headers()).get("x-camada-rid");
|
|
343
|
+
} catch {
|
|
344
|
+
}
|
|
345
|
+
return /* @__PURE__ */ jsx(Beacon, { src, rid });
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// src/track.ts
|
|
349
|
+
import { hashUserId, logRateLimited as logRateLimited3, resolveClientIp as resolveClientIp4, TAP_NEXT as TAP_NEXT3 } from "@camada/core";
|
|
350
|
+
var SESSION_COOKIE2 = "_sfp";
|
|
351
|
+
async function track(event, data) {
|
|
352
|
+
try {
|
|
353
|
+
if (isDisabled()) return;
|
|
354
|
+
const engine2 = getEngine();
|
|
355
|
+
if (!engine2) return;
|
|
356
|
+
const { headers, cookies } = await import("next/headers");
|
|
357
|
+
const [h, c] = await Promise.all([headers(), cookies()]);
|
|
358
|
+
const uid = data?.user ? await hashUserId(data.user, engine2.env.ingestToken) : null;
|
|
359
|
+
engine2.queue.push({
|
|
360
|
+
tap: TAP_NEXT3,
|
|
361
|
+
et: event,
|
|
362
|
+
uid,
|
|
363
|
+
rid: h.get("x-camada-rid"),
|
|
364
|
+
sid: c.get(SESSION_COOKIE2)?.value ?? null,
|
|
365
|
+
ip: resolveClientIp4(null, h.get("x-forwarded-for"), trustedProxy(engine2)),
|
|
366
|
+
ts: Date.now()
|
|
367
|
+
});
|
|
368
|
+
void engine2.queue.flush();
|
|
369
|
+
} catch (err) {
|
|
370
|
+
logRateLimited3(err);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
export {
|
|
374
|
+
CamadaBeacon,
|
|
375
|
+
camada,
|
|
376
|
+
camadaRoute,
|
|
377
|
+
challengeGate,
|
|
378
|
+
track
|
|
379
|
+
};
|
|
380
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +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"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "0.2.0",
|
|
3
|
+
"license": "MIT",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"engines": {
|
|
6
|
+
"node": ">=18"
|
|
7
|
+
},
|
|
8
|
+
"files": [
|
|
9
|
+
"dist",
|
|
10
|
+
"README.md",
|
|
11
|
+
"LICENSE"
|
|
12
|
+
],
|
|
13
|
+
"name": "@camada/next",
|
|
14
|
+
"description": "camada backend SDK for Next.js: edge-safe middleware/proxy, /api/camada route handlers, <CamadaBeacon/> with the request rid",
|
|
15
|
+
"exports": {
|
|
16
|
+
".": {
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"import": "./dist/index.js",
|
|
19
|
+
"require": "./dist/index.cjs"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"main": "./dist/index.cjs",
|
|
23
|
+
"module": "./dist/index.js",
|
|
24
|
+
"types": "./dist/index.d.ts",
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@camada/core": "^0.3.0",
|
|
27
|
+
"@camada/browser": "^0.2.0",
|
|
28
|
+
"@camada/react": "^0.2.0"
|
|
29
|
+
},
|
|
30
|
+
"peerDependencies": {
|
|
31
|
+
"next": ">=15",
|
|
32
|
+
"react": ">=18"
|
|
33
|
+
},
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "git+https://github.com/camada-app/camada-next.git"
|
|
37
|
+
},
|
|
38
|
+
"homepage": "https://github.com/camada-app/camada-next#readme",
|
|
39
|
+
"bugs": {
|
|
40
|
+
"url": "https://github.com/camada-app/camada-next/issues"
|
|
41
|
+
}
|
|
42
|
+
}
|