@camada/node 0.3.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +72 -6
- package/dist/{camada-Dh_9D6Sx.d.cts → camada-DuHQ4sLn.d.cts} +46 -4
- package/dist/{camada-Dh_9D6Sx.d.ts → camada-DuHQ4sLn.d.ts} +46 -4
- package/dist/{chunk-PNJ4BVBD.js → chunk-44FSZJ5F.js} +280 -87
- package/dist/chunk-44FSZJ5F.js.map +1 -0
- package/dist/fastify.cjs +275 -82
- package/dist/fastify.cjs.map +1 -1
- package/dist/fastify.d.cts +1 -1
- package/dist/fastify.d.ts +1 -1
- package/dist/fastify.js +1 -1
- package/dist/index.cjs +275 -82
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/koa.cjs +277 -82
- package/dist/koa.cjs.map +1 -1
- package/dist/koa.d.cts +9 -2
- package/dist/koa.d.ts +9 -2
- package/dist/koa.js +3 -1
- package/dist/koa.js.map +1 -1
- package/dist/nest.cjs +275 -82
- package/dist/nest.cjs.map +1 -1
- package/dist/nest.d.cts +1 -1
- package/dist/nest.d.ts +1 -1
- package/dist/nest.js +1 -1
- package/package.json +48 -15
- package/dist/chunk-PNJ4BVBD.js.map +0 -1
package/README.md
CHANGED
|
@@ -23,8 +23,16 @@ CAMADA_INGEST_URL=http://localhost:8787 # dev only; defaults to productio
|
|
|
23
23
|
**Express / Connect**
|
|
24
24
|
```js
|
|
25
25
|
import camada from '@camada/node';
|
|
26
|
-
app.use(camada.express());
|
|
26
|
+
app.use(camada.express()); // before express.json() / urlencoded() and before helmet
|
|
27
27
|
```
|
|
28
|
+
Mount camada first. It still works behind body parsers (it uses the `req.body` they leave for
|
|
29
|
+
its challenge and beacon POSTs) and behind helmet (the challenge page replaces any inherited
|
|
30
|
+
`Content-Security-Policy` with its own), but first is cheapest: a blocked request never pays for
|
|
31
|
+
parsing. Under a prefix (`app.use('/api', camada.express())`) rules and events see the full path
|
|
32
|
+
(`/api/…`), and camada's own endpoints answer under the prefix (`/api/_cam/b.js`, `/api/_cam/fp`,
|
|
33
|
+
`/api/__camada/challenge`). The beacon tag (`scriptTag()`) and the challenge form point under the
|
|
34
|
+
prefix too, unless you set `scriptPath` or `challengePath` yourself: an explicit path is used
|
|
35
|
+
exactly as given. The beacon posts next to its script, so `fpPath` can stay as it is.
|
|
28
36
|
|
|
29
37
|
**Fastify**
|
|
30
38
|
```js
|
|
@@ -37,12 +45,18 @@ await app.register(camadaFastify);
|
|
|
37
45
|
import { CamadaModule } from '@camada/node/nest';
|
|
38
46
|
@Module({ imports: [CamadaModule.forRoot()] }) // Nest ≤10: CamadaModule.forRoot({ routes: '*' })
|
|
39
47
|
```
|
|
48
|
+
Nest registers its body parsers before module middleware; camada reads what they parsed, so the
|
|
49
|
+
default `bodyParser: true` is fine. Tested on Nest 11. Nest 10 (`routes: '*'`) is checked through
|
|
50
|
+
the Express 4 wildcard mount it registers, not against Nest 10 itself.
|
|
40
51
|
|
|
41
52
|
**Koa**
|
|
42
53
|
```js
|
|
43
54
|
import { camadaKoa } from '@camada/node/koa';
|
|
44
|
-
app.use(camadaKoa());
|
|
55
|
+
app.use(camadaKoa()); // before @koa/bodyparser
|
|
45
56
|
```
|
|
57
|
+
Register it first. Behind `@koa/bodyparser` or `koa-bodyparser` it reads the form and beacon from
|
|
58
|
+
`ctx.request.body`. Under `koa-mount` it works like an Express prefix mount: rules and events see the
|
|
59
|
+
full path, and its own endpoints answer under the prefix.
|
|
46
60
|
|
|
47
61
|
## What it does per request
|
|
48
62
|
|
|
@@ -50,6 +64,9 @@ app.use(camadaKoa());
|
|
|
50
64
|
poll and event batch carries `x-camada-sdk: @camada/node/<version>`.
|
|
51
65
|
2. Resolves the client IP per your tenant's trusted-proxy config — raw `X-Forwarded-For` is
|
|
52
66
|
never trusted without it (`CAMADA_TRUSTED_PROXY=hops:1|cidrs:…|vercel` overrides locally).
|
|
67
|
+
On a unix-socket listener (nginx or Passenger in front) Node has no peer address, so the walk
|
|
68
|
+
runs without one and trusts the local proxy that owns the socket. A TCP request whose peer is
|
|
69
|
+
already gone (the client reset the connection) gets no IP: no ip rules, no challenge.
|
|
53
70
|
3. Runs your ordered custom rules (see below), then the allow, block and challenge lists.
|
|
54
71
|
Blocked → `403` with `x-block-reason` before your app; the event still ships, with
|
|
55
72
|
`st: 403` and `blk: <reason>` (ip4|ip6|path|rule) so the analyst counts SDK blocks apart
|
|
@@ -72,7 +89,7 @@ carries one of four actions:
|
|
|
72
89
|
|---|---|---|
|
|
73
90
|
| `skip` | passes the request | nothing |
|
|
74
91
|
| `block` | `403` before your app | `blk: "rule"`, `rl: "<rule id>"` |
|
|
75
|
-
| `challenge` | serves the proof-of-work page (`challenge: false` opts out) | `blk: "challenge"` |
|
|
92
|
+
| `challenge` | serves the proof-of-work page to page loads (`challenge: false` opts out). A non-HTML request (fetch/XHR/API) is not challenged: it reaches your app with `x-camada-challenge: 1` on the response | page: `blk: "challenge"`; non-HTML: its normal event plus `chs: 1` (never sampled out or excluded) |
|
|
76
93
|
| `warn` | passes the request and marks it for the analyst | `wrn: "<rule id>"` |
|
|
77
94
|
|
|
78
95
|
A skip rule also carries a *record matches* flag, which only the analyst reads: a recorded skip
|
|
@@ -123,6 +140,40 @@ fire on your app's own truth instead of path heuristics:
|
|
|
123
140
|
| `payment_failed` / `payment_succeeded` | a payment authorisation settled |
|
|
124
141
|
| `coupon_failed` | a promo/voucher code was rejected |
|
|
125
142
|
|
|
143
|
+
## TLS fingerprint behind Cloudflare
|
|
144
|
+
|
|
145
|
+
Node has no TLS handshake to read, but Cloudflare does. Set `CAMADA_TLSX_HEADER=x-camada-tlsx`
|
|
146
|
+
(or `createCamada({ tlsxHeader: 'x-camada-tlsx' })`) and add a Transform Rule that sets it:
|
|
147
|
+
*Rules > Transform Rules > Modify Request Header*, "Set dynamic", header `x-camada-tlsx`, value
|
|
148
|
+
`cf.tls_client_extensions_sha1`, on every request. The rule is required and must **set**
|
|
149
|
+
(overwrite) the header: without it a client-sent `x-camada-tlsx` passes through Cloudflare
|
|
150
|
+
untouched. The value feeds the same matcher as `@camada/hono`'s `request.cf.tlsClientExtensionsSha1`:
|
|
151
|
+
the snapshot's `tls` entries block with `x-block-reason: tls`, and `tlsx is` / `is_in` rule
|
|
152
|
+
conditions apply.
|
|
153
|
+
|
|
154
|
+
This needs Cloudflare to connect to the app directly. The header is believed only in `cidrs` mode,
|
|
155
|
+
when the socket peer is a non-loopback address inside Cloudflare's ranges, because anyone else can
|
|
156
|
+
write it:
|
|
157
|
+
|
|
158
|
+
```
|
|
159
|
+
CAMADA_TRUSTED_PROXY=cidrs:173.245.48.0/20,103.21.244.0/22,… # Cloudflare's published ranges
|
|
160
|
+
CAMADA_TLSX_HEADER=x-camada-tlsx
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Every range in `CAMADA_TRUSTED_PROXY=cidrs:…` is trusted to write the header, the same trust it gets
|
|
164
|
+
for `X-Forwarded-For`, so list only Cloudflare's ranges there. A non-Cloudflare proxy range (a docker
|
|
165
|
+
bridge, an internal load balancer) would let anyone who reaches that proxy set the fingerprint.
|
|
166
|
+
|
|
167
|
+
Behind a local reverse proxy (nginx or Caddy on loopback) or a cloudflared tunnel the peer is
|
|
168
|
+
loopback, so the header is not read and `tlsx` conditions fail open; `X-Forwarded-For` is never
|
|
169
|
+
consulted. The same holds in `none`, `hops` or `vercel` mode, from any other peer, with no readable
|
|
170
|
+
peer (unix listeners included), or with a malformed value (over 128 characters, outside
|
|
171
|
+
`[A-Za-z0-9+/=_-]`, or a repeated header). The Transform Rule must overwrite the header on every
|
|
172
|
+
request: on a request it skips, a client-sent value passes through. The same trusted value is sent
|
|
173
|
+
to camada as `tlsx` on request events (pass, block, challenge and upgrade rows); node never adds it
|
|
174
|
+
to `track()` rows or the beacon relay, and a value that failed the checks above is never sent. The
|
|
175
|
+
analyst uses it to group traffic by TLS fingerprint and to recommend or block rare fingerprints.
|
|
176
|
+
|
|
126
177
|
## WebSockets
|
|
127
178
|
|
|
128
179
|
Node hands an upgrade request (a WebSocket handshake) to the server's `'upgrade'` listeners,
|
|
@@ -145,10 +196,25 @@ handshake your library then rejects still ships `st: 101`.
|
|
|
145
196
|
|
|
146
197
|
- `CAMADA_DISABLED=1` — kill switch, checked per request.
|
|
147
198
|
- `CAMADA_SERVERLESS=1` — lazy snapshot refresh (no interval timer); cold invocations fail
|
|
148
|
-
open and catch up asynchronously.
|
|
199
|
+
open and catch up asynchronously. Each request's event (and every `track()` row) is POSTed as
|
|
200
|
+
the response finishes instead of waiting for the 15 s batch timer, which a frozen function
|
|
201
|
+
never runs. Pass the platform's `waitUntil` so the invocation stays alive for that POST:
|
|
202
|
+
```js
|
|
203
|
+
import { waitUntil } from '@vercel/functions';
|
|
204
|
+
const camada = createCamada({ waitUntil }); // also holds the lazy snapshot refresh open
|
|
205
|
+
```
|
|
206
|
+
Where the platform has no `waitUntil`, the POST starts before the response completes but can
|
|
207
|
+
still be frozen mid-flight; use a fetch adapter (`@camada/hono`, `@camada/next`) there.
|
|
208
|
+
- The challenge page sends its own `Content-Security-Policy` allowing only its inline script and
|
|
209
|
+
style by `sha256` hash, and drops any `Content-Security-Policy(-Report-Only)` set before it.
|
|
149
210
|
- Enforcement scope at this position: IP, path, user-agent and request-header conditions. ASN,
|
|
150
|
-
country
|
|
151
|
-
|
|
211
|
+
country conditions can't be evaluated in-app and fail open (a rule that needs one never
|
|
212
|
+
matches here; run `@camada/hono` on Cloudflare Workers for those). TLS-fingerprint conditions
|
|
213
|
+
match only with the opt-in `CAMADA_TLSX_HEADER` (see "TLS fingerprint behind Cloudflare");
|
|
214
|
+
without it they fail open too.
|
|
215
|
+
- `await camada.stop()` in your own graceful shutdown stops polling and the event queue. It
|
|
216
|
+
returns a promise that settles once queued rows (a drain in flight, or rows held by an ingest
|
|
217
|
+
429) are sent, or counted as dropped if the send fails; it never rejects.
|
|
152
218
|
- Memory: ~5 MB per loaded snapshot.
|
|
153
219
|
|
|
154
220
|
## Develop
|
|
@@ -9,6 +9,7 @@ interface ResolvedEnv {
|
|
|
9
9
|
snapshotUrl: string;
|
|
10
10
|
serverless: boolean;
|
|
11
11
|
trustedProxy: TrustedProxyConfig | null;
|
|
12
|
+
tlsxHeader: string | null;
|
|
12
13
|
}
|
|
13
14
|
/** Returns null (SDK stays disabled, one log line) rather than throwing on bad config. */
|
|
14
15
|
declare function resolveEnv(env?: Record<string, string | undefined>): ResolvedEnv | null;
|
|
@@ -20,8 +21,12 @@ interface CamadaOptions {
|
|
|
20
21
|
scriptPath?: string;
|
|
21
22
|
fpPath?: string;
|
|
22
23
|
challenge?: boolean;
|
|
24
|
+
tlsxHeader?: string;
|
|
23
25
|
challengePath?: string;
|
|
24
26
|
snapshotVersion?: SnapshotVersion;
|
|
27
|
+
/** With CAMADA_SERVERLESS=1: handed every per-request event flush and snapshot refresh, so the
|
|
28
|
+
* platform keeps the invocation alive until they finish (e.g. `waitUntil` from `@vercel/functions`). */
|
|
29
|
+
waitUntil?: (p: Promise<unknown>) => void;
|
|
25
30
|
}
|
|
26
31
|
declare class Camada {
|
|
27
32
|
readonly env: ResolvedEnv | null;
|
|
@@ -32,22 +37,56 @@ declare class Camada {
|
|
|
32
37
|
private readonly fpPath;
|
|
33
38
|
private readonly challengeOn;
|
|
34
39
|
private readonly challengePath;
|
|
40
|
+
private readonly challengePathSet;
|
|
41
|
+
private readonly scriptPathSet;
|
|
35
42
|
private readonly kit;
|
|
36
43
|
private readonly envSource;
|
|
44
|
+
private readonly tlsxHeader;
|
|
45
|
+
private readonly waitUntil?;
|
|
37
46
|
constructor(opts?: CamadaOptions);
|
|
38
47
|
get disabled(): boolean;
|
|
39
48
|
private trustedProxy;
|
|
49
|
+
/** The client IP per the trusted-proxy config (team contract C6 v3). No socket at all, or a
|
|
50
|
+
* unix-socket listener (nginx/Passenger in front, where Node never has a peer): core's no-peer
|
|
51
|
+
* X-Forwarded-For walk, which trusts the local proxy. A TCP socket with no readable peer (the
|
|
52
|
+
* client reset the connection before camada ran, e.g. behind an async middleware) is
|
|
53
|
+
* unidentifiable: the walk would believe whatever the client wrote. So: null, the stance every
|
|
54
|
+
* ip-bound feature already takes (no ip rules, no challenge). */
|
|
55
|
+
private clientIp;
|
|
56
|
+
/** The TLS fingerprint Cloudflare (a Transform Rule) put in the opt-in header, or null. Trusted only
|
|
57
|
+
* in `cidrs` mode and only when the socket peer itself is a NON-loopback address core's cidrs walk
|
|
58
|
+
* trusts, i.e. Cloudflare connects to the app directly. A loopback peer (a local reverse proxy or
|
|
59
|
+
* cloudflared) is never trusted and X-Forwarded-For is never consulted, so the header is not read
|
|
60
|
+
* and tlsx conditions fail open. Trust is core's isTrustedPeer, the same first-hop test the cidrs
|
|
61
|
+
* walk uses. No readable peer (unix listener included) -> null, since core's no-peer walk would
|
|
62
|
+
* trust everyone. */
|
|
63
|
+
private tlsxOf;
|
|
64
|
+
/** Where the challenge form posts: under the mount prefix, so it reaches camada, unless the
|
|
65
|
+
* path was configured explicitly. */
|
|
66
|
+
private challengeAction;
|
|
40
67
|
private beaconEnabled;
|
|
68
|
+
/** Queues a row. With CAMADA_SERVERLESS=1 it also starts the POST now: the platform freezes the
|
|
69
|
+
* process once the response is out, so the 15 s interval would never run. Never throws. */
|
|
70
|
+
private enqueue;
|
|
71
|
+
/** Serverless: start the POST now and hand it to waitUntil. Never throws. */
|
|
72
|
+
private flushFor;
|
|
41
73
|
/** True when camada fully handled the request. Synchronous; never throws. */
|
|
42
74
|
handle(req: IncomingMessage, res: ServerResponse): boolean;
|
|
43
75
|
private handleInner;
|
|
76
|
+
/** Mints the request's rid and session (setting the cookie on a first visit), records them on
|
|
77
|
+
* `req.camada` and sets `x-rid`. handle() and serveChallenge() (when handle() did not run) share it. */
|
|
78
|
+
private begin;
|
|
79
|
+
/** Installs the response-finish hook that ships the request's normal capture event. */
|
|
80
|
+
private hookCapture;
|
|
44
81
|
private buildEvent;
|
|
45
82
|
/** Reads the beacon POST (≤32 KB), answers 204 immediately, and queues the beacon as a `sig: 1`
|
|
46
83
|
* row with the trusted-proxy-resolved client IP: it rides the next event batch, so the analyst
|
|
47
|
-
* sees one request per flush instead of one per page view. Junk bodies are dropped, never shipped.
|
|
84
|
+
* sees one request per flush instead of one per page view. Junk bodies are dropped, never shipped.
|
|
85
|
+
* A body parser mounted before camada (express.json(), always on Nest) has already drained the
|
|
86
|
+
* stream: its `req.body` is used instead, and the request is answered either way. */
|
|
48
87
|
private relayBeacon;
|
|
49
88
|
private challengePassed;
|
|
50
|
-
/** 403 + the proof-of-work page (HTML navigations
|
|
89
|
+
/** 403 + the proof-of-work page (HTML navigations only), plus the
|
|
51
90
|
* `blk: "challenge"` event — a served challenge is reported like a block (contract §D2). */
|
|
52
91
|
private serveChallengeInner;
|
|
53
92
|
/** The request's existing session, so challenge rows join the session that produced them. */
|
|
@@ -68,7 +107,8 @@ declare class Camada {
|
|
|
68
107
|
private shipUpgrade;
|
|
69
108
|
/** Serve the challenge for this request on demand — for a route the app wants to gate itself
|
|
70
109
|
* (the example's /challenge-me). Returns false when the client already holds a valid `_cch`,
|
|
71
|
-
* so the caller renders its own page.
|
|
110
|
+
* so the caller renders its own page. A non-HTML request is never challenged: it gets the
|
|
111
|
+
* `x-camada-challenge: 1` header and a `chs: 1` event, and false, so the app answers. */
|
|
72
112
|
serveChallenge(req: IncomingMessage, res: ServerResponse): boolean;
|
|
73
113
|
/** For HTML templates: the first-party beacon tag with the request's rid. */
|
|
74
114
|
scriptTag(req: IncomingMessage): string;
|
|
@@ -79,7 +119,9 @@ declare class Camada {
|
|
|
79
119
|
}): void;
|
|
80
120
|
/** Express/Connect middleware (also what NestJS on the default platform consumes). */
|
|
81
121
|
express(): (req: IncomingMessage, res: ServerResponse, next: (err?: unknown) => void) => void;
|
|
82
|
-
|
|
122
|
+
/** Stops polling and the event queue. The promise settles once rows still queued behind a drain
|
|
123
|
+
* in flight or a 429 hold are sent (or counted dropped); it never rejects. */
|
|
124
|
+
stop(): Promise<void>;
|
|
83
125
|
}
|
|
84
126
|
declare function createCamada(opts?: CamadaOptions): Camada;
|
|
85
127
|
|
|
@@ -9,6 +9,7 @@ interface ResolvedEnv {
|
|
|
9
9
|
snapshotUrl: string;
|
|
10
10
|
serverless: boolean;
|
|
11
11
|
trustedProxy: TrustedProxyConfig | null;
|
|
12
|
+
tlsxHeader: string | null;
|
|
12
13
|
}
|
|
13
14
|
/** Returns null (SDK stays disabled, one log line) rather than throwing on bad config. */
|
|
14
15
|
declare function resolveEnv(env?: Record<string, string | undefined>): ResolvedEnv | null;
|
|
@@ -20,8 +21,12 @@ interface CamadaOptions {
|
|
|
20
21
|
scriptPath?: string;
|
|
21
22
|
fpPath?: string;
|
|
22
23
|
challenge?: boolean;
|
|
24
|
+
tlsxHeader?: string;
|
|
23
25
|
challengePath?: string;
|
|
24
26
|
snapshotVersion?: SnapshotVersion;
|
|
27
|
+
/** With CAMADA_SERVERLESS=1: handed every per-request event flush and snapshot refresh, so the
|
|
28
|
+
* platform keeps the invocation alive until they finish (e.g. `waitUntil` from `@vercel/functions`). */
|
|
29
|
+
waitUntil?: (p: Promise<unknown>) => void;
|
|
25
30
|
}
|
|
26
31
|
declare class Camada {
|
|
27
32
|
readonly env: ResolvedEnv | null;
|
|
@@ -32,22 +37,56 @@ declare class Camada {
|
|
|
32
37
|
private readonly fpPath;
|
|
33
38
|
private readonly challengeOn;
|
|
34
39
|
private readonly challengePath;
|
|
40
|
+
private readonly challengePathSet;
|
|
41
|
+
private readonly scriptPathSet;
|
|
35
42
|
private readonly kit;
|
|
36
43
|
private readonly envSource;
|
|
44
|
+
private readonly tlsxHeader;
|
|
45
|
+
private readonly waitUntil?;
|
|
37
46
|
constructor(opts?: CamadaOptions);
|
|
38
47
|
get disabled(): boolean;
|
|
39
48
|
private trustedProxy;
|
|
49
|
+
/** The client IP per the trusted-proxy config (team contract C6 v3). No socket at all, or a
|
|
50
|
+
* unix-socket listener (nginx/Passenger in front, where Node never has a peer): core's no-peer
|
|
51
|
+
* X-Forwarded-For walk, which trusts the local proxy. A TCP socket with no readable peer (the
|
|
52
|
+
* client reset the connection before camada ran, e.g. behind an async middleware) is
|
|
53
|
+
* unidentifiable: the walk would believe whatever the client wrote. So: null, the stance every
|
|
54
|
+
* ip-bound feature already takes (no ip rules, no challenge). */
|
|
55
|
+
private clientIp;
|
|
56
|
+
/** The TLS fingerprint Cloudflare (a Transform Rule) put in the opt-in header, or null. Trusted only
|
|
57
|
+
* in `cidrs` mode and only when the socket peer itself is a NON-loopback address core's cidrs walk
|
|
58
|
+
* trusts, i.e. Cloudflare connects to the app directly. A loopback peer (a local reverse proxy or
|
|
59
|
+
* cloudflared) is never trusted and X-Forwarded-For is never consulted, so the header is not read
|
|
60
|
+
* and tlsx conditions fail open. Trust is core's isTrustedPeer, the same first-hop test the cidrs
|
|
61
|
+
* walk uses. No readable peer (unix listener included) -> null, since core's no-peer walk would
|
|
62
|
+
* trust everyone. */
|
|
63
|
+
private tlsxOf;
|
|
64
|
+
/** Where the challenge form posts: under the mount prefix, so it reaches camada, unless the
|
|
65
|
+
* path was configured explicitly. */
|
|
66
|
+
private challengeAction;
|
|
40
67
|
private beaconEnabled;
|
|
68
|
+
/** Queues a row. With CAMADA_SERVERLESS=1 it also starts the POST now: the platform freezes the
|
|
69
|
+
* process once the response is out, so the 15 s interval would never run. Never throws. */
|
|
70
|
+
private enqueue;
|
|
71
|
+
/** Serverless: start the POST now and hand it to waitUntil. Never throws. */
|
|
72
|
+
private flushFor;
|
|
41
73
|
/** True when camada fully handled the request. Synchronous; never throws. */
|
|
42
74
|
handle(req: IncomingMessage, res: ServerResponse): boolean;
|
|
43
75
|
private handleInner;
|
|
76
|
+
/** Mints the request's rid and session (setting the cookie on a first visit), records them on
|
|
77
|
+
* `req.camada` and sets `x-rid`. handle() and serveChallenge() (when handle() did not run) share it. */
|
|
78
|
+
private begin;
|
|
79
|
+
/** Installs the response-finish hook that ships the request's normal capture event. */
|
|
80
|
+
private hookCapture;
|
|
44
81
|
private buildEvent;
|
|
45
82
|
/** Reads the beacon POST (≤32 KB), answers 204 immediately, and queues the beacon as a `sig: 1`
|
|
46
83
|
* row with the trusted-proxy-resolved client IP: it rides the next event batch, so the analyst
|
|
47
|
-
* sees one request per flush instead of one per page view. Junk bodies are dropped, never shipped.
|
|
84
|
+
* sees one request per flush instead of one per page view. Junk bodies are dropped, never shipped.
|
|
85
|
+
* A body parser mounted before camada (express.json(), always on Nest) has already drained the
|
|
86
|
+
* stream: its `req.body` is used instead, and the request is answered either way. */
|
|
48
87
|
private relayBeacon;
|
|
49
88
|
private challengePassed;
|
|
50
|
-
/** 403 + the proof-of-work page (HTML navigations
|
|
89
|
+
/** 403 + the proof-of-work page (HTML navigations only), plus the
|
|
51
90
|
* `blk: "challenge"` event — a served challenge is reported like a block (contract §D2). */
|
|
52
91
|
private serveChallengeInner;
|
|
53
92
|
/** The request's existing session, so challenge rows join the session that produced them. */
|
|
@@ -68,7 +107,8 @@ declare class Camada {
|
|
|
68
107
|
private shipUpgrade;
|
|
69
108
|
/** Serve the challenge for this request on demand — for a route the app wants to gate itself
|
|
70
109
|
* (the example's /challenge-me). Returns false when the client already holds a valid `_cch`,
|
|
71
|
-
* so the caller renders its own page.
|
|
110
|
+
* so the caller renders its own page. A non-HTML request is never challenged: it gets the
|
|
111
|
+
* `x-camada-challenge: 1` header and a `chs: 1` event, and false, so the app answers. */
|
|
72
112
|
serveChallenge(req: IncomingMessage, res: ServerResponse): boolean;
|
|
73
113
|
/** For HTML templates: the first-party beacon tag with the request's rid. */
|
|
74
114
|
scriptTag(req: IncomingMessage): string;
|
|
@@ -79,7 +119,9 @@ declare class Camada {
|
|
|
79
119
|
}): void;
|
|
80
120
|
/** Express/Connect middleware (also what NestJS on the default platform consumes). */
|
|
81
121
|
express(): (req: IncomingMessage, res: ServerResponse, next: (err?: unknown) => void) => void;
|
|
82
|
-
|
|
122
|
+
/** Stops polling and the event queue. The promise settles once rows still queued behind a drain
|
|
123
|
+
* in flight or a 429 hold are sent (or counted dropped); it never rejects. */
|
|
124
|
+
stop(): Promise<void>;
|
|
83
125
|
}
|
|
84
126
|
declare function createCamada(opts?: CamadaOptions): Camada;
|
|
85
127
|
|