@camada/node 0.3.1 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -3
- package/dist/{camada-Dh_9D6Sx.d.cts → camada-CFmJTruP.d.cts} +27 -2
- package/dist/{camada-Dh_9D6Sx.d.ts → camada-CFmJTruP.d.ts} +27 -2
- package/dist/{chunk-PNJ4BVBD.js → chunk-V3UWO5OT.js} +166 -51
- package/dist/chunk-V3UWO5OT.js.map +1 -0
- package/dist/fastify.cjs +164 -47
- 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 +164 -47
- 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 +166 -47
- 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 +164 -47
- 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/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { IncomingMessage, ServerResponse, Server } from 'node:http';
|
|
2
|
-
import { a as CamadaOptions, C as Camada } from './camada-
|
|
3
|
-
export { R as ResolvedEnv, c as createCamada, r as resolveEnv } from './camada-
|
|
2
|
+
import { a as CamadaOptions, C as Camada } from './camada-CFmJTruP.js';
|
|
3
|
+
export { R as ResolvedEnv, c as createCamada, r as resolveEnv } from './camada-CFmJTruP.js';
|
|
4
4
|
export { parseTrustedProxyEnv } from '@camada/core';
|
|
5
5
|
|
|
6
6
|
declare function getDefault(): Camada;
|
package/dist/index.js
CHANGED
package/dist/koa.cjs
CHANGED
|
@@ -86,10 +86,41 @@ function readBody(req, done) {
|
|
|
86
86
|
req.on("end", () => done(dead ? "" : Buffer.concat(chunks).toString("utf8")));
|
|
87
87
|
req.on("error", () => done(""));
|
|
88
88
|
}
|
|
89
|
+
var FORM_FIELDS = ["nonce", "solution", "to"];
|
|
90
|
+
function parsedBody(req) {
|
|
91
|
+
const b = req.body;
|
|
92
|
+
if (typeof b === "string") return b;
|
|
93
|
+
if (Buffer.isBuffer(b)) return b.toString("utf8");
|
|
94
|
+
return b && typeof b === "object" ? b : null;
|
|
95
|
+
}
|
|
96
|
+
function readForm(req, done) {
|
|
97
|
+
if (!req.readableEnded) {
|
|
98
|
+
readBody(req, (body) => done((0, import_core2.parseFormBody)(body)));
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const b = parsedBody(req);
|
|
102
|
+
if (typeof b === "string") {
|
|
103
|
+
done(b.length > BODY_MAX ? {} : (0, import_core2.parseFormBody)(b));
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
const form = /* @__PURE__ */ Object.create(null);
|
|
107
|
+
for (const k of FORM_FIELDS) {
|
|
108
|
+
const v = b?.[k];
|
|
109
|
+
if (typeof v === "string") form[k] = v;
|
|
110
|
+
}
|
|
111
|
+
done(form);
|
|
112
|
+
}
|
|
89
113
|
var isHttps = (req) => !!req.socket?.encrypted || req.headers["x-forwarded-proto"] === "https";
|
|
90
|
-
function writeChallengePage(res,
|
|
91
|
-
res.
|
|
92
|
-
res.
|
|
114
|
+
function writeChallengePage(res, o) {
|
|
115
|
+
res.removeHeader("content-security-policy");
|
|
116
|
+
res.removeHeader("content-security-policy-report-only");
|
|
117
|
+
res.writeHead(403, {
|
|
118
|
+
"content-type": "text/html; charset=utf-8",
|
|
119
|
+
"cache-control": "no-store",
|
|
120
|
+
"x-camada-challenge": "1",
|
|
121
|
+
"content-security-policy": (0, import_core2.challengePageCsp)(o.bits)
|
|
122
|
+
});
|
|
123
|
+
res.end((0, import_core2.challengePage)(o));
|
|
93
124
|
}
|
|
94
125
|
function writeChallengeJson(res) {
|
|
95
126
|
res.writeHead(403, { "content-type": "application/json", "cache-control": "no-store", "x-camada-challenge": "1" });
|
|
@@ -97,7 +128,7 @@ function writeChallengeJson(res) {
|
|
|
97
128
|
}
|
|
98
129
|
|
|
99
130
|
// package.json
|
|
100
|
-
var version = "0.3.
|
|
131
|
+
var version = "0.3.2";
|
|
101
132
|
var name = "@camada/node";
|
|
102
133
|
|
|
103
134
|
// src/version.ts
|
|
@@ -113,6 +144,28 @@ var headerReader = (req) => (name2) => {
|
|
|
113
144
|
const v = req.headers[name2];
|
|
114
145
|
return v == null ? null : Array.isArray(v) ? v.join(", ") : v;
|
|
115
146
|
};
|
|
147
|
+
var splitUrl = (raw) => {
|
|
148
|
+
const qi = raw.indexOf("?");
|
|
149
|
+
return qi === -1 ? { path: raw, query: "" } : { path: raw.slice(0, qi), query: raw.slice(qi) };
|
|
150
|
+
};
|
|
151
|
+
function pathAndQuery(req) {
|
|
152
|
+
return splitUrl(req.originalUrl || req.url || "/");
|
|
153
|
+
}
|
|
154
|
+
var atEndpoint = (req, path, endpoint) => path === endpoint || splitUrl(req.url || "/").path === endpoint;
|
|
155
|
+
function mountPrefix(req) {
|
|
156
|
+
if (!req.originalUrl) return "";
|
|
157
|
+
const full = splitUrl(req.originalUrl).path, rel = splitUrl(req.url || "/").path;
|
|
158
|
+
return rel !== "/" && full !== rel && full.endsWith(rel) ? full.slice(0, full.length - rel.length) : "";
|
|
159
|
+
}
|
|
160
|
+
function onUnixListener(socket) {
|
|
161
|
+
const srv = socket?.server;
|
|
162
|
+
if (!srv) return false;
|
|
163
|
+
try {
|
|
164
|
+
if (typeof srv.address?.() === "string") return true;
|
|
165
|
+
} catch {
|
|
166
|
+
}
|
|
167
|
+
return typeof srv._pipeName === "string";
|
|
168
|
+
}
|
|
116
169
|
var cookieValue = (cookie, name2) => {
|
|
117
170
|
const src = "; " + cookie;
|
|
118
171
|
const i = src.indexOf("; " + name2 + "=");
|
|
@@ -141,15 +194,22 @@ var Camada = class {
|
|
|
141
194
|
fpPath;
|
|
142
195
|
challengeOn;
|
|
143
196
|
challengePath;
|
|
197
|
+
challengePathSet;
|
|
198
|
+
// configured explicitly: used as given, never prefixed
|
|
199
|
+
scriptPathSet;
|
|
144
200
|
kit = null;
|
|
145
201
|
envSource;
|
|
202
|
+
waitUntil;
|
|
146
203
|
constructor(opts = {}) {
|
|
147
204
|
this.envSource = opts.env ?? process.env;
|
|
148
205
|
this.fetchImpl = opts.fetchImpl ?? fetch;
|
|
206
|
+
this.waitUntil = opts.waitUntil;
|
|
149
207
|
this.scriptPath = opts.scriptPath ?? SCRIPT_PATH;
|
|
150
208
|
this.fpPath = opts.fpPath ?? FP_PATH;
|
|
151
209
|
this.challengeOn = opts.challenge !== false && this.envSource.CAMADA_CHALLENGE !== "0";
|
|
152
210
|
this.challengePath = opts.challengePath ?? CHALLENGE_PATH;
|
|
211
|
+
this.challengePathSet = opts.challengePath != null;
|
|
212
|
+
this.scriptPathSet = opts.scriptPath != null;
|
|
153
213
|
this.env = resolveEnv(this.envSource);
|
|
154
214
|
if (!this.env) return;
|
|
155
215
|
if (this.envSource.CAMADA_DISABLED === "1") return;
|
|
@@ -173,9 +233,43 @@ var Camada = class {
|
|
|
173
233
|
trustedProxy() {
|
|
174
234
|
return this.env?.trustedProxy ?? this.snap?.config?.trusted_proxy ?? null;
|
|
175
235
|
}
|
|
236
|
+
/** The client IP per the trusted-proxy config (team contract C6 v3). No socket at all, or a
|
|
237
|
+
* unix-socket listener (nginx/Passenger in front, where Node never has a peer): core's no-peer
|
|
238
|
+
* X-Forwarded-For walk, which trusts the local proxy. A TCP socket with no readable peer (the
|
|
239
|
+
* client reset the connection before camada ran, e.g. behind an async middleware) is
|
|
240
|
+
* unidentifiable: the walk would believe whatever the client wrote. So: null, the stance every
|
|
241
|
+
* ip-bound feature already takes (no ip rules, no challenge). */
|
|
242
|
+
clientIp(req) {
|
|
243
|
+
const peer = req.socket?.remoteAddress;
|
|
244
|
+
if (!peer && req.socket && !onUnixListener(req.socket)) return null;
|
|
245
|
+
return (0, import_core3.resolveClientIp)(peer ?? null, req.headers["x-forwarded-for"], this.trustedProxy());
|
|
246
|
+
}
|
|
247
|
+
/** Where the challenge form posts: under the mount prefix, so it reaches camada, unless the
|
|
248
|
+
* path was configured explicitly. */
|
|
249
|
+
challengeAction(req) {
|
|
250
|
+
return this.challengePathSet ? this.challengePath : (req.camadaPrefix ?? mountPrefix(req)) + this.challengePath;
|
|
251
|
+
}
|
|
176
252
|
beaconEnabled() {
|
|
177
253
|
return this.snap?.config?.beacon !== false;
|
|
178
254
|
}
|
|
255
|
+
/** Queues a row. With CAMADA_SERVERLESS=1 it also starts the POST now: the platform freezes the
|
|
256
|
+
* process once the response is out, so the 15 s interval would never run. Never throws. */
|
|
257
|
+
enqueue(row, req) {
|
|
258
|
+
const q = this.queue;
|
|
259
|
+
q.push(row);
|
|
260
|
+
if (!this.env?.serverless) return;
|
|
261
|
+
if (req?.camadaOpen) return;
|
|
262
|
+
this.flushFor(req);
|
|
263
|
+
}
|
|
264
|
+
/** Serverless: start the POST now and hand it to waitUntil. Never throws. */
|
|
265
|
+
flushFor(req) {
|
|
266
|
+
const q = this.queue;
|
|
267
|
+
(0, import_core3.guarded)(() => {
|
|
268
|
+
const tracks = req?.camadaTracks ?? [];
|
|
269
|
+
const p = Promise.allSettled(tracks).then(() => q.flush()).then(() => q.size > 0 ? q.flush() : void 0);
|
|
270
|
+
this.waitUntil?.(p);
|
|
271
|
+
}, void 0);
|
|
272
|
+
}
|
|
179
273
|
/** True when camada fully handled the request. Synchronous; never throws. */
|
|
180
274
|
handle(req, res) {
|
|
181
275
|
return (0, import_core3.guarded)(() => this.handleInner(req, res), false);
|
|
@@ -183,15 +277,13 @@ var Camada = class {
|
|
|
183
277
|
handleInner(req, res) {
|
|
184
278
|
if (this.disabled || !this.snap || !this.queue || !this.env) return false;
|
|
185
279
|
const t0 = Date.now();
|
|
186
|
-
this.snap.ensureFresh();
|
|
187
|
-
const
|
|
188
|
-
const
|
|
189
|
-
|
|
190
|
-
const query = qi === -1 ? "" : rawUrl.slice(qi);
|
|
191
|
-
const ip = (0, import_core3.resolveClientIp)(req.socket?.remoteAddress, req.headers["x-forwarded-for"], this.trustedProxy());
|
|
280
|
+
this.snap.ensureFresh(this.env.serverless ? this.waitUntil : void 0);
|
|
281
|
+
const { path, query } = pathAndQuery(req);
|
|
282
|
+
const ip = this.clientIp(req);
|
|
283
|
+
req.camadaPrefix = mountPrefix(req);
|
|
192
284
|
const v = this.snap.verdict({ ip, path, ua: req.headers["user-agent"], header: headerReader(req) });
|
|
193
285
|
if (v.block) {
|
|
194
|
-
const headers = { "x-block-reason": v.reason ?? "", "x-block-version": v.version ?? "", "content-type": "text/plain" };
|
|
286
|
+
const headers = { "x-block-reason": v.reason ?? "", "x-block-version": v.version ?? "", "content-type": "text/plain", "cache-control": "no-store" };
|
|
195
287
|
if (v.rule) headers["x-block-rule"] = v.rule;
|
|
196
288
|
res.writeHead(403, headers);
|
|
197
289
|
res.end("Forbidden");
|
|
@@ -199,11 +291,11 @@ var Camada = class {
|
|
|
199
291
|
ev.st = 403;
|
|
200
292
|
ev.blk = v.reason;
|
|
201
293
|
if (v.rule) ev.rl = v.rule;
|
|
202
|
-
this.
|
|
294
|
+
this.enqueue(ev);
|
|
203
295
|
return true;
|
|
204
296
|
}
|
|
205
297
|
if (this.challengeOn && this.kit && ip) {
|
|
206
|
-
if (req.method === "POST" && path
|
|
298
|
+
if (req.method === "POST" && atEndpoint(req, path, this.challengePath)) {
|
|
207
299
|
this.verifyChallenge(req, res, ip);
|
|
208
300
|
return true;
|
|
209
301
|
}
|
|
@@ -213,12 +305,12 @@ var Camada = class {
|
|
|
213
305
|
}
|
|
214
306
|
}
|
|
215
307
|
if (this.beaconEnabled()) {
|
|
216
|
-
if (req.method === "GET" && path
|
|
308
|
+
if (req.method === "GET" && atEndpoint(req, path, this.scriptPath)) {
|
|
217
309
|
res.writeHead(200, { "content-type": "application/javascript", "cache-control": "public, max-age=3600" });
|
|
218
310
|
res.end(import_iife_string.default);
|
|
219
311
|
return true;
|
|
220
312
|
}
|
|
221
|
-
if (req.method === "POST" && path
|
|
313
|
+
if (req.method === "POST" && atEndpoint(req, path, this.fpPath)) {
|
|
222
314
|
this.relayBeacon(req, res, ip);
|
|
223
315
|
return true;
|
|
224
316
|
}
|
|
@@ -237,16 +329,22 @@ var Camada = class {
|
|
|
237
329
|
const excluded = (cfg?.exclude || []).some((x) => path.startsWith(x));
|
|
238
330
|
if (!excluded && Math.random() < (cfg?.sample ?? 1)) {
|
|
239
331
|
let shipped = false;
|
|
332
|
+
req.camadaOpen = true;
|
|
240
333
|
const ship = () => (0, import_core3.guarded)(() => {
|
|
241
|
-
|
|
334
|
+
req.camadaOpen = false;
|
|
335
|
+
if (shipped) return;
|
|
242
336
|
shipped = true;
|
|
337
|
+
if (req.camadaChallenged) {
|
|
338
|
+
if (this.env?.serverless) this.flushFor(req);
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
243
341
|
const ev = this.buildEvent(req, path, query, ip, { rid, sid, newSession });
|
|
244
342
|
ev.ts = t0;
|
|
245
343
|
ev.st = res.statusCode;
|
|
246
344
|
ev.dur = Date.now() - t0;
|
|
247
345
|
if (req.route?.path) ev.rt = String(req.route.path);
|
|
248
346
|
if (v.warn && v.rule) ev.wrn = v.rule;
|
|
249
|
-
this.
|
|
347
|
+
this.enqueue(ev, req);
|
|
250
348
|
}, void 0);
|
|
251
349
|
res.once("finish", ship);
|
|
252
350
|
res.once("close", ship);
|
|
@@ -264,8 +362,32 @@ var Camada = class {
|
|
|
264
362
|
}
|
|
265
363
|
/** Reads the beacon POST (≤32 KB), answers 204 immediately, and queues the beacon as a `sig: 1`
|
|
266
364
|
* row with the trusted-proxy-resolved client IP: it rides the next event batch, so the analyst
|
|
267
|
-
* sees one request per flush instead of one per page view. Junk bodies are dropped, never shipped.
|
|
365
|
+
* sees one request per flush instead of one per page view. Junk bodies are dropped, never shipped.
|
|
366
|
+
* A body parser mounted before camada (express.json(), always on Nest) has already drained the
|
|
367
|
+
* stream: its `req.body` is used instead, and the request is answered either way. */
|
|
268
368
|
relayBeacon(req, res, ip) {
|
|
369
|
+
const accept = (parsed) => {
|
|
370
|
+
res.writeHead(204, { "cache-control": "no-store" });
|
|
371
|
+
res.end();
|
|
372
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed) || Buffer.isBuffer(parsed)) return;
|
|
373
|
+
this.enqueue({ ...parsed, sig: 1, ip, tap: import_core3.TAP_NODE });
|
|
374
|
+
};
|
|
375
|
+
const fromText = (text) => {
|
|
376
|
+
try {
|
|
377
|
+
return JSON.parse(text);
|
|
378
|
+
} catch {
|
|
379
|
+
return null;
|
|
380
|
+
}
|
|
381
|
+
};
|
|
382
|
+
if (req.readableEnded) {
|
|
383
|
+
const b = parsedBody(req);
|
|
384
|
+
if ((typeof b === "string" ? b : b ? JSON.stringify(b) : "").length > FP_MAX) {
|
|
385
|
+
res.writeHead(413).end();
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
accept(typeof b === "string" ? fromText(b) : b);
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
269
391
|
const chunks = [];
|
|
270
392
|
let size = 0, dead = false;
|
|
271
393
|
req.on("data", (c) => {
|
|
@@ -280,16 +402,7 @@ var Camada = class {
|
|
|
280
402
|
});
|
|
281
403
|
req.on("end", () => (0, import_core3.guarded)(() => {
|
|
282
404
|
if (dead) return;
|
|
283
|
-
|
|
284
|
-
res.end();
|
|
285
|
-
let parsed;
|
|
286
|
-
try {
|
|
287
|
-
parsed = JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
288
|
-
} catch {
|
|
289
|
-
return;
|
|
290
|
-
}
|
|
291
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return;
|
|
292
|
-
this.queue.push({ ...parsed, sig: 1, ip, tap: import_core3.TAP_NODE });
|
|
405
|
+
accept(fromText(Buffer.concat(chunks).toString("utf8")));
|
|
293
406
|
}, void 0));
|
|
294
407
|
req.on("error", () => {
|
|
295
408
|
try {
|
|
@@ -306,7 +419,7 @@ var Camada = class {
|
|
|
306
419
|
serveChallengeInner(req, res, ip, target) {
|
|
307
420
|
const to = (0, import_core3.safeReturnTo)(target);
|
|
308
421
|
if ((0, import_core3.wantsHtml)(req.headers.accept ?? null, req.headers["sec-fetch-dest"] ?? null)) {
|
|
309
|
-
writeChallengePage(res,
|
|
422
|
+
writeChallengePage(res, { nonce: this.kit.nonce(ip, Date.now()), action: this.challengeAction(req), to });
|
|
310
423
|
} else {
|
|
311
424
|
writeChallengeJson(res);
|
|
312
425
|
}
|
|
@@ -321,7 +434,7 @@ var Camada = class {
|
|
|
321
434
|
);
|
|
322
435
|
ev.st = 403;
|
|
323
436
|
ev.blk = "challenge";
|
|
324
|
-
this.
|
|
437
|
+
this.enqueue(ev);
|
|
325
438
|
}, void 0);
|
|
326
439
|
}
|
|
327
440
|
/** The request's existing session, so challenge rows join the session that produced them. */
|
|
@@ -331,12 +444,11 @@ var Camada = class {
|
|
|
331
444
|
/** POST from the challenge page: validate the nonce and the proof of work, set `_cch`, 302
|
|
332
445
|
* back to the (sanitised, same-site) original URL, and ship `{ st: 200, ch: 1 }`. */
|
|
333
446
|
verifyChallenge(req, res, ip) {
|
|
334
|
-
|
|
335
|
-
const form = (0, import_core3.parseFormBody)(body);
|
|
447
|
+
readForm(req, (form) => (0, import_core3.guarded)(() => {
|
|
336
448
|
const to = (0, import_core3.safeReturnTo)(form.to);
|
|
337
449
|
const now = Date.now();
|
|
338
450
|
if (!this.kit.verify(ip, now, form.nonce, form.solution)) {
|
|
339
|
-
writeChallengePage(res,
|
|
451
|
+
writeChallengePage(res, { nonce: this.kit.nonce(ip, now), action: this.challengeAction(req), to });
|
|
340
452
|
return;
|
|
341
453
|
}
|
|
342
454
|
res.writeHead(302, {
|
|
@@ -345,10 +457,10 @@ var Camada = class {
|
|
|
345
457
|
"cache-control": "no-store"
|
|
346
458
|
});
|
|
347
459
|
res.end();
|
|
348
|
-
const ev = this.buildEvent(req, this.
|
|
460
|
+
const ev = this.buildEvent(req, this.challengeAction(req), "", ip, { rid: (0, import_node_crypto2.randomUUID)(), sid: this.sessionOf(req), newSession: false });
|
|
349
461
|
ev.st = 200;
|
|
350
462
|
ev.ch = 1;
|
|
351
|
-
this.
|
|
463
|
+
this.enqueue(ev);
|
|
352
464
|
}, void 0));
|
|
353
465
|
}
|
|
354
466
|
/**
|
|
@@ -380,17 +492,15 @@ var Camada = class {
|
|
|
380
492
|
}
|
|
381
493
|
shipUpgrade(req, t0) {
|
|
382
494
|
if (this.disabled || !this.queue) return;
|
|
383
|
-
const
|
|
384
|
-
const qi = rawUrl.indexOf("?");
|
|
385
|
-
const path = qi === -1 ? rawUrl : rawUrl.slice(0, qi);
|
|
495
|
+
const { path, query } = pathAndQuery(req);
|
|
386
496
|
const cfg = this.snap?.config;
|
|
387
497
|
if ((cfg?.exclude || []).some((x) => path.startsWith(x)) || Math.random() >= (cfg?.sample ?? 1)) return;
|
|
388
|
-
const ip =
|
|
389
|
-
const ev = this.buildEvent(req, path,
|
|
498
|
+
const ip = this.clientIp(req);
|
|
499
|
+
const ev = this.buildEvent(req, path, query, ip, { rid: (0, import_node_crypto2.randomUUID)(), sid: this.sessionOf(req), newSession: false });
|
|
390
500
|
ev.ts = t0;
|
|
391
501
|
ev.st = 101;
|
|
392
502
|
ev.dur = Date.now() - t0;
|
|
393
|
-
this.
|
|
503
|
+
this.enqueue(ev);
|
|
394
504
|
}
|
|
395
505
|
/** Serve the challenge for this request on demand — for a route the app wants to gate itself
|
|
396
506
|
* (the example's /challenge-me). Returns false when the client already holds a valid `_cch`,
|
|
@@ -398,10 +508,11 @@ var Camada = class {
|
|
|
398
508
|
serveChallenge(req, res) {
|
|
399
509
|
return (0, import_core3.guarded)(() => {
|
|
400
510
|
if (this.disabled || !this.kit || !this.queue) return false;
|
|
401
|
-
const ip =
|
|
511
|
+
const ip = this.clientIp(req);
|
|
402
512
|
if (!ip || this.challengePassed(req, ip)) return false;
|
|
403
513
|
req.camadaChallenged = true;
|
|
404
|
-
|
|
514
|
+
const { path, query } = pathAndQuery(req);
|
|
515
|
+
this.serveChallengeInner(req, res, ip, path + query);
|
|
405
516
|
return true;
|
|
406
517
|
}, false);
|
|
407
518
|
}
|
|
@@ -409,7 +520,9 @@ var Camada = class {
|
|
|
409
520
|
scriptTag(req) {
|
|
410
521
|
if (this.disabled || !this.beaconEnabled()) return "";
|
|
411
522
|
const rid = req.camada?.rid;
|
|
412
|
-
|
|
523
|
+
const r = req;
|
|
524
|
+
const src = this.scriptPathSet ? this.scriptPath : (r.camadaPrefix ?? mountPrefix(r)) + this.scriptPath;
|
|
525
|
+
return `<script src="${src}${rid ? `?r=${rid}` : ""}" async></script>`;
|
|
413
526
|
}
|
|
414
527
|
/** App-context outcome events (login failed, signup, …). The identifier is HMAC-hashed
|
|
415
528
|
* in-process; the raw value never reaches the queue. */
|
|
@@ -417,10 +530,12 @@ var Camada = class {
|
|
|
417
530
|
(0, import_core3.guarded)(() => {
|
|
418
531
|
if (this.disabled || !this.queue || !this.env) return;
|
|
419
532
|
const ctx = req.camada;
|
|
420
|
-
|
|
533
|
+
const r = req;
|
|
534
|
+
const row = (async () => {
|
|
421
535
|
const uid = data?.user ? await (0, import_core3.hashUserId)(data.user, this.env.ingestToken, (globalThis.crypto ?? import_node_crypto2.webcrypto).subtle) : null;
|
|
422
|
-
this.
|
|
536
|
+
this.enqueue({ tap: import_core3.TAP_NODE, et: event, uid, rid: ctx?.rid ?? null, sid: ctx?.sid ?? null, ip: ctx?.ip ?? null, ts: Date.now() }, r);
|
|
423
537
|
})().catch(import_core3.logRateLimited);
|
|
538
|
+
if (r.camadaOpen) (r.camadaTracks ??= []).push(row);
|
|
424
539
|
}, void 0);
|
|
425
540
|
}
|
|
426
541
|
/** Express/Connect middleware (also what NestJS on the default platform consumes). */
|
|
@@ -429,9 +544,11 @@ var Camada = class {
|
|
|
429
544
|
if (!this.handle(req, res)) next();
|
|
430
545
|
};
|
|
431
546
|
}
|
|
547
|
+
/** Stops polling and the event queue. The promise settles once rows still queued behind a drain
|
|
548
|
+
* in flight or a 429 hold are sent (or counted dropped); it never rejects. */
|
|
432
549
|
stop() {
|
|
433
550
|
this.snap?.stop();
|
|
434
|
-
this.queue?.stop();
|
|
551
|
+
return this.queue?.stop() ?? Promise.resolve();
|
|
435
552
|
}
|
|
436
553
|
};
|
|
437
554
|
function createCamada(opts = {}) {
|
|
@@ -449,6 +566,8 @@ function getDefault() {
|
|
|
449
566
|
// src/koa.ts
|
|
450
567
|
function camadaKoa(engine) {
|
|
451
568
|
return async (ctx, next) => {
|
|
569
|
+
ctx.req.body ??= ctx.request?.body;
|
|
570
|
+
ctx.req.originalUrl ??= ctx.originalUrl;
|
|
452
571
|
if ((engine ?? getDefault()).handle(ctx.req, ctx.res)) {
|
|
453
572
|
ctx.respond = false;
|
|
454
573
|
return;
|