@devshub198211/devguard 2.0.2 → 2.0.3

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/dist/auth.cjs DELETED
@@ -1,787 +0,0 @@
1
- 'use strict';
2
-
3
- var crypto2 = require('crypto');
4
- var https = require('https');
5
-
6
- function _interopNamespace(e) {
7
- if (e && e.__esModule) return e;
8
- var n = Object.create(null);
9
- if (e) {
10
- Object.keys(e).forEach(function (k) {
11
- if (k !== 'default') {
12
- var d = Object.getOwnPropertyDescriptor(e, k);
13
- Object.defineProperty(n, k, d.get ? d : {
14
- enumerable: true,
15
- get: function () { return e[k]; }
16
- });
17
- }
18
- });
19
- }
20
- n.default = e;
21
- return Object.freeze(n);
22
- }
23
-
24
- var crypto2__namespace = /*#__PURE__*/_interopNamespace(crypto2);
25
- var https__namespace = /*#__PURE__*/_interopNamespace(https);
26
-
27
- // src/auth/zero-trust-jwt.ts
28
- var MIN_SECRET_LENGTH = 16;
29
- var CLOCK_SKEW_TOLERANCE = 30;
30
- function b64urlDecode(s) {
31
- const base64 = s.replace(/-/g, "+").replace(/_/g, "/");
32
- const padLen = (4 - base64.length % 4) % 4;
33
- const padded = base64 + "=".repeat(padLen);
34
- return Buffer.from(padded, "base64");
35
- }
36
- function b64urlEncode(buf) {
37
- return buf.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
38
- }
39
- function decodeJWT(token) {
40
- if (typeof token !== "string") return null;
41
- const parts = token.split(".");
42
- if (parts.length !== 3) return null;
43
- if (!parts[0] || !parts[1] || !parts[2]) return null;
44
- try {
45
- const header = JSON.parse(b64urlDecode(parts[0]).toString());
46
- const payload = JSON.parse(b64urlDecode(parts[1]).toString());
47
- if (typeof header !== "object" || header === null) return null;
48
- if (typeof payload !== "object" || payload === null) return null;
49
- return { header, payload, sig: parts[2], signingInput: `${parts[0]}.${parts[1]}` };
50
- } catch {
51
- return null;
52
- }
53
- }
54
- function signHMAC(payload, secret, algorithm = "HS256") {
55
- if (!secret || typeof secret !== "string") throw new Error("JWT secret is required");
56
- if (secret.length < MIN_SECRET_LENGTH) throw new Error(`JWT secret must be at least ${MIN_SECRET_LENGTH} characters (got ${secret.length})`);
57
- if (typeof payload !== "object" || payload === null) throw new Error("Payload must be a non-null object");
58
- const h = b64urlEncode(Buffer.from(JSON.stringify({ alg: algorithm, typ: "JWT" })));
59
- const p = b64urlEncode(Buffer.from(JSON.stringify(payload)));
60
- const input = `${h}.${p}`;
61
- const sig = b64urlEncode(crypto2__namespace.createHmac(algorithm === "HS512" ? "sha512" : "sha256", secret).update(input).digest());
62
- return `${input}.${sig}`;
63
- }
64
- function verifyHMAC(token, secret, expectedAlg) {
65
- if (!secret || typeof secret !== "string") return { valid: false, error: "Secret is required" };
66
- const decoded = decodeJWT(token);
67
- if (!decoded) return { valid: false, error: "Malformed JWT" };
68
- const alg = decoded.header.alg;
69
- if (!["HS256", "HS512"].includes(alg)) return { valid: false, error: `Unsupported algorithm: ${alg}` };
70
- if (expectedAlg && alg !== expectedAlg) return { valid: false, error: `Algorithm mismatch: expected ${expectedAlg}, got ${alg}` };
71
- const expected = b64urlEncode(crypto2__namespace.createHmac(alg === "HS512" ? "sha512" : "sha256", secret).update(decoded.signingInput).digest());
72
- const sigBuf = Buffer.from(decoded.sig);
73
- const expBuf = Buffer.from(expected);
74
- if (sigBuf.length !== expBuf.length || !crypto2__namespace.timingSafeEqual(sigBuf, expBuf)) return { valid: false, error: "Invalid signature" };
75
- const now = Math.floor(Date.now() / 1e3);
76
- if (decoded.payload.exp && now > decoded.payload.exp + CLOCK_SKEW_TOLERANCE) return { valid: false, error: "Token expired" };
77
- if (decoded.payload.nbf && now < decoded.payload.nbf - CLOCK_SKEW_TOLERANCE) return { valid: false, error: "Token not yet valid" };
78
- return { valid: true, payload: decoded.payload };
79
- }
80
- var JWKS_TTL_MS = 5 * 60 * 1e3;
81
- var JWKS_MAX_ENTRIES = 50;
82
- var BoundedJWKSCache = class {
83
- constructor() {
84
- this.cache = /* @__PURE__ */ new Map();
85
- this.pruneTimer = null;
86
- this.pruneTimer = setInterval(() => this.prune(), 6e4);
87
- if (this.pruneTimer.unref) this.pruneTimer.unref();
88
- }
89
- get(uri) {
90
- const entry = this.cache.get(uri);
91
- if (!entry) return null;
92
- if (Date.now() - entry.fetchedAt > JWKS_TTL_MS) {
93
- this.cache.delete(uri);
94
- return null;
95
- }
96
- return entry.keys;
97
- }
98
- set(uri, keys) {
99
- if (this.cache.size >= JWKS_MAX_ENTRIES) {
100
- let oldestKey = null;
101
- let oldestTime = Infinity;
102
- for (const [k, v] of this.cache) {
103
- if (v.fetchedAt < oldestTime) {
104
- oldestTime = v.fetchedAt;
105
- oldestKey = k;
106
- }
107
- }
108
- if (oldestKey) this.cache.delete(oldestKey);
109
- }
110
- this.cache.set(uri, { keys, fetchedAt: Date.now() });
111
- }
112
- prune() {
113
- const now = Date.now();
114
- let pruned = 0;
115
- for (const [uri, entry] of this.cache) {
116
- if (now - entry.fetchedAt > JWKS_TTL_MS) {
117
- this.cache.delete(uri);
118
- pruned++;
119
- }
120
- }
121
- return pruned;
122
- }
123
- size() {
124
- return this.cache.size;
125
- }
126
- destroy() {
127
- if (this.pruneTimer) {
128
- clearInterval(this.pruneTimer);
129
- this.pruneTimer = null;
130
- }
131
- this.cache.clear();
132
- }
133
- };
134
- var jwksCache = new BoundedJWKSCache();
135
- var MAX_JWKS_SIZE = 512 * 1024;
136
- function httpsGet(url) {
137
- return new Promise((resolve, reject) => {
138
- let parsed;
139
- try {
140
- parsed = new URL(url);
141
- } catch {
142
- reject(new Error("Invalid JWKS URL"));
143
- return;
144
- }
145
- if (parsed.protocol !== "https:") {
146
- reject(new Error("JWKS URL must use HTTPS"));
147
- return;
148
- }
149
- const req = https__namespace.get(url, { headers: { "Accept": "application/json" } }, (res) => {
150
- let body = "";
151
- let size = 0;
152
- res.on("data", (d) => {
153
- size += typeof d === "string" ? d.length : d.length;
154
- if (size > MAX_JWKS_SIZE) {
155
- req.destroy();
156
- reject(new Error("JWKS response too large"));
157
- return;
158
- }
159
- body += d;
160
- });
161
- res.on("end", () => resolve(body));
162
- });
163
- req.on("error", (e) => reject(new Error(`JWKS fetch failed: ${e.message}`)));
164
- req.setTimeout(5e3, () => {
165
- req.destroy();
166
- reject(new Error("JWKS fetch timeout"));
167
- });
168
- });
169
- }
170
- async function fetchJWKS(jwksUri) {
171
- const cached = jwksCache.get(jwksUri);
172
- if (cached) return cached;
173
- const body = await httpsGet(jwksUri);
174
- let parsed;
175
- try {
176
- parsed = JSON.parse(body);
177
- } catch {
178
- throw new Error("Invalid JWKS JSON response");
179
- }
180
- if (!parsed || !Array.isArray(parsed.keys)) throw new Error("JWKS response missing 'keys' array");
181
- const keys = parsed.keys;
182
- jwksCache.set(jwksUri, keys);
183
- return keys;
184
- }
185
- var jwksCacheUtils = {
186
- prune: () => jwksCache.prune(),
187
- size: () => jwksCache.size(),
188
- destroy: () => jwksCache.destroy()
189
- };
190
- function jwkToPublicKey(jwk) {
191
- if (jwk.kty !== "RSA" || !jwk.n || !jwk.e) throw new Error("Unsupported JWK type \u2014 only RSA is supported");
192
- return crypto2__namespace.createPublicKey({ key: { kty: "RSA", n: jwk.n, e: jwk.e }, format: "jwk" });
193
- }
194
- async function verifyRS256(token, jwksUri) {
195
- const decoded = decodeJWT(token);
196
- if (!decoded) return { valid: false, error: "Malformed JWT" };
197
- if (decoded.header.alg !== "RS256") return { valid: false, error: "Expected RS256" };
198
- let keys;
199
- try {
200
- keys = await fetchJWKS(jwksUri);
201
- } catch (e) {
202
- return { valid: false, error: `JWKS fetch failed: ${e instanceof Error ? e.message : "Unknown error"}` };
203
- }
204
- const matching = decoded.header.kid ? keys.filter((k) => k.kid === decoded.header.kid) : keys;
205
- if (!matching.length) return { valid: false, error: "No matching JWK found" };
206
- for (const jwk of matching) {
207
- try {
208
- const pubKey = jwkToPublicKey(jwk);
209
- const isValid = crypto2__namespace.verify("sha256", Buffer.from(decoded.signingInput), { key: pubKey, padding: crypto2__namespace.constants.RSA_PKCS1_PADDING }, b64urlDecode(decoded.sig));
210
- if (isValid) {
211
- const now = Math.floor(Date.now() / 1e3);
212
- if (decoded.payload.exp && now > decoded.payload.exp + CLOCK_SKEW_TOLERANCE) return { valid: false, error: "Token expired" };
213
- if (decoded.payload.nbf && now < decoded.payload.nbf - CLOCK_SKEW_TOLERANCE) return { valid: false, error: "Token not yet valid" };
214
- return { valid: true, payload: decoded.payload };
215
- }
216
- } catch {
217
- continue;
218
- }
219
- }
220
- return { valid: false, error: "Signature verification failed" };
221
- }
222
- var MAX_REVOCATION_SIZE = 1e5;
223
- var RevocationList = class {
224
- constructor() {
225
- this.revoked = /* @__PURE__ */ new Map();
226
- }
227
- revoke(jti) {
228
- if (!jti || typeof jti !== "string") return;
229
- if (this.revoked.size >= MAX_REVOCATION_SIZE && !this.revoked.has(jti)) {
230
- let oldestKey = null;
231
- let oldestTime = Infinity;
232
- for (const [k, ts] of this.revoked) {
233
- if (ts < oldestTime) {
234
- oldestTime = ts;
235
- oldestKey = k;
236
- }
237
- }
238
- if (oldestKey) this.revoked.delete(oldestKey);
239
- }
240
- this.revoked.set(jti, Date.now());
241
- }
242
- isRevoked(jti) {
243
- return jti ? this.revoked.has(jti) : false;
244
- }
245
- revokedCount() {
246
- return this.revoked.size;
247
- }
248
- prune(maxAgeMs = 30 * 24 * 60 * 60 * 1e3) {
249
- const cutoff = Date.now() - maxAgeMs;
250
- let pruned = 0;
251
- for (const [jti, ts] of this.revoked) if (ts < cutoff) {
252
- this.revoked.delete(jti);
253
- pruned++;
254
- }
255
- return pruned;
256
- }
257
- exportJSON() {
258
- return JSON.stringify([...this.revoked.entries()]);
259
- }
260
- importJSON(json) {
261
- let data;
262
- try {
263
- data = JSON.parse(json);
264
- } catch {
265
- return;
266
- }
267
- if (!Array.isArray(data)) return;
268
- for (const entry of data) {
269
- if (Array.isArray(entry) && entry.length === 2 && typeof entry[0] === "string" && typeof entry[1] === "number") {
270
- if (this.revoked.size >= MAX_REVOCATION_SIZE) break;
271
- this.revoked.set(entry[0], entry[1]);
272
- }
273
- }
274
- }
275
- };
276
- function detectAnomalies(payload, ctx) {
277
- const warnings = [];
278
- let score = 0;
279
- if (!payload.iat) {
280
- warnings.push("Missing iat");
281
- score += 20;
282
- }
283
- if (!payload.jti) {
284
- warnings.push("Missing jti \u2014 revocation not possible");
285
- score += 15;
286
- }
287
- if (!payload.iss) {
288
- warnings.push("Missing iss");
289
- score += 10;
290
- }
291
- if (!payload.sub) {
292
- warnings.push("Missing sub");
293
- score += 10;
294
- }
295
- if (ctx?.expectedIss && payload.iss !== ctx.expectedIss) {
296
- warnings.push(`Issuer mismatch: ${payload.iss}`);
297
- score += 40;
298
- }
299
- if (ctx?.expectedAud) {
300
- const aud = Array.isArray(payload.aud) ? payload.aud : [payload.aud];
301
- if (!aud.includes(ctx.expectedAud)) {
302
- warnings.push("Audience mismatch");
303
- score += 40;
304
- }
305
- }
306
- if (payload.exp && payload.iat && payload.exp - payload.iat > 86400 * 30) {
307
- warnings.push(`Token lifetime > 30 days`);
308
- score += 25;
309
- }
310
- if (payload.nbf && payload.iat && payload.nbf > payload.iat + 86400) {
311
- warnings.push("nbf is more than 1 day after iat");
312
- score += 10;
313
- }
314
- if (!payload.exp) {
315
- warnings.push("Missing exp \u2014 token never expires");
316
- score += 20;
317
- }
318
- return { score: Math.min(100, score), warnings, level: score >= 50 ? "dangerous" : score >= 20 ? "suspicious" : "safe" };
319
- }
320
- var JWTVerifier = class {
321
- constructor(opts) {
322
- this.opts = opts;
323
- this.revocation = new RevocationList();
324
- }
325
- async verify(token) {
326
- if (!token || typeof token !== "string") return { valid: false, error: "Token is required" };
327
- let result;
328
- if (this.opts.secret) result = verifyHMAC(token, this.opts.secret, this.opts.expectedAlg);
329
- else if (this.opts.jwksUri) result = await verifyRS256(token, this.opts.jwksUri);
330
- else return { valid: false, error: "No secret or jwksUri configured" };
331
- if (!result.valid || !result.payload) return result;
332
- if (this.revocation.isRevoked(result.payload.jti)) return { valid: false, error: "Token has been revoked" };
333
- return { ...result, anomalies: detectAnomalies(result.payload, { expectedIss: this.opts.expectedIss, expectedAud: this.opts.expectedAud }) };
334
- }
335
- revoke(jti) {
336
- this.revocation.revoke(jti);
337
- }
338
- getRevocationList() {
339
- return this.revocation;
340
- }
341
- };
342
-
343
- // src/auth/bot-fence.ts
344
- var BOT_UA = /bot|crawl|spider|scraper|python-?requests|axios\/|node-?fetch|undici|got\/|superagent|aiohttp|httpx|libwww|java\/|curl\/|wget\//i;
345
- var HEADLESS_UA = /headlesschrome|playwright|puppeteer|selenium|phantomjs|zombie|slimer|cypress/i;
346
- var AI_UA = /GPTBot|ChatGPT|Claude-Web|anthropic-ai|cohere-ai|meta-externalagent|Bytespider|CCBot/i;
347
- var KNOWN_GOOD_UA = /Mozilla\/5\.[01].+(Chrome\/[1-9]\d{2}|Firefox\/[1-9]\d{2}|Safari\/[0-9]+)/;
348
- var SIGNALS = [
349
- { signal: "Bot user-agent", weight: 65, test: (fp) => BOT_UA.test(fp.userAgent ?? "") },
350
- { signal: "Headless browser UA", weight: 80, test: (fp) => HEADLESS_UA.test(fp.userAgent ?? "") },
351
- { signal: "Known AI crawler", weight: 70, test: (fp) => AI_UA.test(fp.userAgent ?? "") },
352
- { signal: "Missing user-agent", weight: 45, test: (fp) => !fp.userAgent?.trim() },
353
- { signal: "No Accept-Language", weight: 20, test: (fp) => !fp.acceptLanguage && !fp.headers?.["accept-language"] },
354
- { signal: "No Accept-Encoding", weight: 15, test: (fp) => !fp.acceptEncoding && !fp.headers?.["accept-encoding"] },
355
- { signal: "Missing Referer on form", weight: 10, test: (fp) => !fp.referer && !fp.headers?.["referer"] },
356
- { signal: "Suspicious timing <80ms", weight: 30, test: (fp) => fp.timingMs !== void 0 && fp.timingMs < 80 },
357
- { signal: "Zero mouse events", weight: 25, test: (fp) => fp.mouseEvents !== void 0 && fp.mouseEvents === 0 },
358
- { signal: "No keyboard entropy", weight: 20, test: (fp) => fp.keystrokeIntervals !== void 0 && fp.keystrokeIntervals.length === 0 },
359
- { signal: "Non-browser UA format", weight: 20, test: (fp) => !!fp.userAgent && !KNOWN_GOOD_UA.test(fp.userAgent) && !BOT_UA.test(fp.userAgent) },
360
- { signal: "Duplicate headers", weight: 15, test: (fp) => fp.headers ? Object.values(fp.headers).some((v) => Array.isArray(v) && v.length > 1) : false }
361
- ];
362
- function scoreRequest(fp) {
363
- const results = SIGNALS.map((s) => ({ signal: s.signal, weight: s.weight, matched: s.test(fp) }));
364
- const raw = results.filter((r) => r.matched).reduce((acc, r) => acc + r.weight, 0);
365
- const score = Math.min(100, raw);
366
- const verdict = score >= 70 ? "bot" : score >= 45 ? "suspicious" : score >= 20 ? "likely_human" : "human";
367
- const recommendation = score >= 70 ? "block" : score >= 45 ? "challenge" : "allow";
368
- return { score, verdict, signals: results, recommendation };
369
- }
370
- function normalizeIP(ip) {
371
- if (!ip || typeof ip !== "string") return "";
372
- const trimmed = ip.trim();
373
- const v4mapped = trimmed.match(/^::ffff:(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/i);
374
- if (v4mapped) return v4mapped[1];
375
- return trimmed;
376
- }
377
- var MAX_TRACKED_IPS = 1e5;
378
- var PRUNE_INTERVAL = 100;
379
- var IPRateLimiter = class {
380
- constructor(maxRequests = 60, windowMs = 6e4, blockDurationMs = 5 * 6e4) {
381
- this.maxRequests = maxRequests;
382
- this.windowMs = windowMs;
383
- this.blockDurationMs = blockDurationMs;
384
- this.windows = /* @__PURE__ */ new Map();
385
- this.blocked = /* @__PURE__ */ new Map();
386
- // ip → blockedUntil timestamp
387
- this.checkCount = 0;
388
- }
389
- check(ip) {
390
- const normalizedIp = normalizeIP(ip);
391
- if (!normalizedIp) return { allowed: false, remaining: 0, retryAfterMs: this.blockDurationMs };
392
- this.checkCount++;
393
- if (this.checkCount % PRUNE_INTERVAL === 0) this.prune();
394
- const blockedUntil = this.blocked.get(normalizedIp);
395
- if (blockedUntil !== void 0) {
396
- if (Date.now() < blockedUntil) {
397
- return { allowed: false, remaining: 0, retryAfterMs: blockedUntil - Date.now() };
398
- }
399
- this.blocked.delete(normalizedIp);
400
- }
401
- const now = Date.now();
402
- let w = this.windows.get(normalizedIp);
403
- if (!w || now - w.windowStart >= this.windowMs) {
404
- w = { count: 0, windowStart: now };
405
- this.windows.set(normalizedIp, w);
406
- }
407
- w.count++;
408
- if (w.count > this.maxRequests) {
409
- this.blocked.set(normalizedIp, Date.now() + this.blockDurationMs);
410
- return { allowed: false, remaining: 0, retryAfterMs: this.blockDurationMs };
411
- }
412
- return { allowed: true, remaining: this.maxRequests - w.count };
413
- }
414
- /** Prune stale windows and expired blocks to prevent memory growth */
415
- prune() {
416
- const now = Date.now();
417
- const windowCutoff = now - this.windowMs * 2;
418
- for (const [ip, w] of this.windows) if (w.windowStart < windowCutoff) this.windows.delete(ip);
419
- for (const [ip, until] of this.blocked) if (now >= until) this.blocked.delete(ip);
420
- if (this.windows.size > MAX_TRACKED_IPS) {
421
- const sorted = [...this.windows.entries()].sort((a, b) => a[1].windowStart - b[1].windowStart);
422
- const toRemove = sorted.slice(0, this.windows.size - MAX_TRACKED_IPS);
423
- for (const [ip] of toRemove) this.windows.delete(ip);
424
- }
425
- }
426
- };
427
- function createMiddleware(opts = {}) {
428
- const { blockThreshold = 70, challengeThreshold = 50, whitelist = [], blacklist = [] } = opts;
429
- const wSet = new Set(whitelist.map(normalizeIP));
430
- const bSet = new Set(blacklist.map(normalizeIP));
431
- return function botFenceMiddleware(req, res, next) {
432
- const rawIp = req.ip ?? req.connection?.remoteAddress ?? "";
433
- const ip = normalizeIP(rawIp);
434
- if (wSet.has(ip)) return next();
435
- if (bSet.has(ip)) return res.status(403).json({ error: "Access denied" });
436
- if (opts.rateLimiter) {
437
- const rl = opts.rateLimiter.check(ip);
438
- if (!rl.allowed) {
439
- res.setHeader("Retry-After", Math.ceil((rl.retryAfterMs ?? 6e4) / 1e3));
440
- return res.status(429).json({ error: "Too many requests" });
441
- }
442
- }
443
- const fp = {
444
- ip,
445
- userAgent: req.headers["user-agent"],
446
- headers: req.headers,
447
- acceptLanguage: req.headers["accept-language"],
448
- acceptEncoding: req.headers["accept-encoding"],
449
- referer: req.headers["referer"],
450
- origin: req.headers["origin"]
451
- };
452
- const botScore = scoreRequest(fp);
453
- req.botScore = botScore;
454
- if (botScore.score >= blockThreshold) {
455
- opts.onBlock?.(fp, botScore);
456
- return res.status(403).json({ error: "Automated request detected", verdict: botScore.verdict });
457
- }
458
- if (botScore.score >= challengeThreshold) {
459
- res.setHeader("X-Bot-Challenge", "true");
460
- }
461
- next();
462
- };
463
- }
464
- var MAX_CBOR_DEPTH = 20;
465
- var MAX_CBOR_SIZE = 1 * 1024 * 1024;
466
- function cborDecode(buf) {
467
- if (buf.length > MAX_CBOR_SIZE) throw new Error("CBOR input too large");
468
- let offset = 0;
469
- function read(depth) {
470
- if (depth > MAX_CBOR_DEPTH) throw new Error("CBOR nesting too deep");
471
- if (offset >= buf.length) throw new Error("CBOR: unexpected end of input");
472
- const b = buf[offset++];
473
- const mt = b >> 5 & 7;
474
- let ai = b & 31;
475
- let val = ai;
476
- if (ai === 24) {
477
- if (offset >= buf.length) throw new Error("CBOR: unexpected end of input");
478
- val = buf[offset++];
479
- } else if (ai === 25) {
480
- if (offset + 2 > buf.length) throw new Error("CBOR: unexpected end of input");
481
- val = buf.readUInt16BE(offset);
482
- offset += 2;
483
- } else if (ai === 26) {
484
- if (offset + 4 > buf.length) throw new Error("CBOR: unexpected end of input");
485
- val = buf.readUInt32BE(offset);
486
- offset += 4;
487
- } else if (ai >= 28) {
488
- if (ai === 31 && (mt === 2 || mt === 3)) throw new Error("CBOR: indefinite length not supported");
489
- if (ai >= 28 && ai <= 30) throw new Error("CBOR: reserved additional info");
490
- }
491
- if (mt === 0) return val;
492
- if (mt === 1) return -1 - val;
493
- if (mt === 2) {
494
- if (offset + val > buf.length) throw new Error("CBOR: byte string exceeds buffer");
495
- const s = buf.slice(offset, offset + val);
496
- offset += val;
497
- return s;
498
- }
499
- if (mt === 3) {
500
- if (offset + val > buf.length) throw new Error("CBOR: text string exceeds buffer");
501
- const s = buf.slice(offset, offset + val).toString("utf8");
502
- offset += val;
503
- return s;
504
- }
505
- if (mt === 4) {
506
- if (val > 1e4) throw new Error("CBOR: array too large");
507
- const arr = [];
508
- for (let i = 0; i < val; i++) arr.push(read(depth + 1));
509
- return arr;
510
- }
511
- if (mt === 5) {
512
- if (val > 1e4) throw new Error("CBOR: map too large");
513
- const obj = {};
514
- for (let i = 0; i < val; i++) {
515
- const k = read(depth + 1);
516
- const v = read(depth + 1);
517
- if (k === "__proto__" || k === "constructor" || k === "prototype") continue;
518
- obj[k] = v;
519
- }
520
- return obj;
521
- }
522
- if (mt === 7) {
523
- if (ai === 20) return false;
524
- if (ai === 21) return true;
525
- if (ai === 22) return null;
526
- if (ai === 23) return void 0;
527
- }
528
- return null;
529
- }
530
- return read(0);
531
- }
532
- var MAX_CHALLENGES = 1e4;
533
- var ChallengeStore = class {
534
- constructor() {
535
- this.challenges = /* @__PURE__ */ new Map();
536
- }
537
- create(ttlMs = 3e5) {
538
- if (this.challenges.size >= MAX_CHALLENGES) {
539
- this._prune();
540
- if (this.challenges.size >= MAX_CHALLENGES) {
541
- throw new Error("Challenge store capacity exceeded \u2014 too many pending challenges");
542
- }
543
- }
544
- const challenge = crypto2__namespace.randomBytes(32).toString("base64url");
545
- this.challenges.set(challenge, { challenge, expiresAt: Date.now() + ttlMs });
546
- return challenge;
547
- }
548
- consume(challenge) {
549
- if (!challenge || typeof challenge !== "string") return false;
550
- const entry = this.challenges.get(challenge);
551
- if (!entry) return false;
552
- this.challenges.delete(challenge);
553
- return Date.now() <= entry.expiresAt;
554
- }
555
- _prune() {
556
- const now = Date.now();
557
- for (const [k, v] of this.challenges) if (v.expiresAt < now) this.challenges.delete(k);
558
- }
559
- /** Get the number of active challenges */
560
- size() {
561
- return this.challenges.size;
562
- }
563
- };
564
- var MAX_CREDENTIALS_PER_USER = 50;
565
- var CredentialStore = class {
566
- constructor() {
567
- this.creds = /* @__PURE__ */ new Map();
568
- this.byUser = /* @__PURE__ */ new Map();
569
- }
570
- save(cred) {
571
- const userCreds = this.byUser.get(cred.userId);
572
- if (userCreds && !userCreds.has(cred.credentialId) && userCreds.size >= MAX_CREDENTIALS_PER_USER) {
573
- throw new Error(`User ${cred.userId} has reached the maximum credential limit (${MAX_CREDENTIALS_PER_USER})`);
574
- }
575
- this.creds.set(cred.credentialId, cred);
576
- if (!this.byUser.has(cred.userId)) this.byUser.set(cred.userId, /* @__PURE__ */ new Set());
577
- this.byUser.get(cred.userId).add(cred.credentialId);
578
- }
579
- get(credentialId) {
580
- return this.creds.get(credentialId);
581
- }
582
- getByUser(userId) {
583
- const ids = this.byUser.get(userId) ?? /* @__PURE__ */ new Set();
584
- return [...ids].map((id) => this.creds.get(id)).filter(Boolean);
585
- }
586
- update(credentialId, patch) {
587
- const existing = this.creds.get(credentialId);
588
- if (existing) {
589
- const safeUpdate = {};
590
- if (typeof patch.signCount === "number") safeUpdate.signCount = patch.signCount;
591
- if (typeof patch.lastUsedAt === "number") safeUpdate.lastUsedAt = patch.lastUsedAt;
592
- if (Array.isArray(patch.transports)) safeUpdate.transports = patch.transports;
593
- this.creds.set(credentialId, { ...existing, ...safeUpdate });
594
- }
595
- }
596
- delete(credentialId) {
597
- const cred = this.creds.get(credentialId);
598
- if (cred) {
599
- this.byUser.get(cred.userId)?.delete(credentialId);
600
- this.creds.delete(credentialId);
601
- }
602
- }
603
- exportJSON() {
604
- return JSON.stringify([...this.creds.values()], null, 2);
605
- }
606
- importJSON(json) {
607
- let creds;
608
- try {
609
- creds = JSON.parse(json);
610
- } catch {
611
- return;
612
- }
613
- if (!Array.isArray(creds)) return;
614
- for (const c of creds) {
615
- if (c && typeof c === "object" && typeof c.credentialId === "string" && typeof c.publicKeyPem === "string") {
616
- try {
617
- this.save(c);
618
- } catch {
619
- }
620
- }
621
- }
622
- }
623
- };
624
- function generateRegistrationOptions(opts) {
625
- if (!opts.rpId || !opts.rpName || !opts.userId || !opts.userName) {
626
- throw new Error("rpId, rpName, userId, and userName are required");
627
- }
628
- const store = opts.challengeStore ?? new ChallengeStore();
629
- return {
630
- challenge: store.create(),
631
- rp: { id: opts.rpId, name: opts.rpName },
632
- user: { id: Buffer.from(opts.userId).toString("base64url"), name: opts.userName, displayName: opts.userDisplayName ?? opts.userName },
633
- pubKeyCredParams: [{ type: "public-key", alg: -7 }, { type: "public-key", alg: -257 }],
634
- timeout: 6e4,
635
- attestation: "none",
636
- authenticatorSelection: { userVerification: "preferred", residentKey: "preferred" }
637
- };
638
- }
639
- function parseAuthenticatorData(buf) {
640
- if (buf.length < 37) throw new Error("AuthenticatorData too short");
641
- let offset = 0;
642
- const rpIdHash = buf.slice(offset, offset + 32);
643
- offset += 32;
644
- const flags = buf[offset++];
645
- const signCount = buf.readUInt32BE(offset);
646
- offset += 4;
647
- let aaguid;
648
- let credentialId;
649
- let coseKey;
650
- if (flags & 64) {
651
- if (offset + 18 > buf.length) throw new Error("AuthenticatorData truncated (attested credential data)");
652
- aaguid = buf.slice(offset, offset + 16).toString("hex");
653
- offset += 16;
654
- const credIdLen = buf.readUInt16BE(offset);
655
- offset += 2;
656
- if (credIdLen > 1024) throw new Error("Credential ID too large");
657
- if (offset + credIdLen > buf.length) throw new Error("AuthenticatorData truncated (credential ID)");
658
- credentialId = buf.slice(offset, offset + credIdLen);
659
- offset += credIdLen;
660
- if (offset >= buf.length) throw new Error("AuthenticatorData truncated (COSE key)");
661
- coseKey = cborDecode(buf.slice(offset));
662
- }
663
- return { rpIdHash, flags, signCount, aaguid, credentialId, coseKey };
664
- }
665
- function coseToPublicKeyPem(coseKey) {
666
- if (!coseKey || typeof coseKey !== "object") throw new Error("Invalid COSE key");
667
- const kty = coseKey[1];
668
- coseKey[3];
669
- if (kty === 2) {
670
- const x = Buffer.isBuffer(coseKey[-2]) ? coseKey[-2] : Buffer.from(coseKey[-2]);
671
- const y = Buffer.isBuffer(coseKey[-3]) ? coseKey[-3] : Buffer.from(coseKey[-3]);
672
- if (x.length !== 32 || y.length !== 32) throw new Error("Invalid EC2 key coordinates");
673
- const key = crypto2__namespace.createPublicKey({ key: { kty: "EC", crv: "P-256", x: x.toString("base64url"), y: y.toString("base64url") }, format: "jwk" });
674
- return { pem: key.export({ type: "spki", format: "pem" }), alg: -7 };
675
- }
676
- if (kty === 3) {
677
- const n = Buffer.isBuffer(coseKey[-1]) ? coseKey[-1] : Buffer.from(coseKey[-1]);
678
- const e = Buffer.isBuffer(coseKey[-2]) ? coseKey[-2] : Buffer.from(coseKey[-2]);
679
- if (n.length < 128) throw new Error("RSA key modulus too short");
680
- const key = crypto2__namespace.createPublicKey({ key: { kty: "RSA", n: n.toString("base64url"), e: e.toString("base64url") }, format: "jwk" });
681
- return { pem: key.export({ type: "spki", format: "pem" }), alg: -257 };
682
- }
683
- throw new Error(`Unsupported COSE key type: ${kty}`);
684
- }
685
- async function verifyRegistration(opts) {
686
- try {
687
- if (!opts.response || !opts.expectedChallenge || !opts.expectedOrigin || !opts.expectedRpId || !opts.userId) {
688
- return { verified: false, error: "Missing required parameters" };
689
- }
690
- const clientDataBuf = Buffer.from(opts.response.clientDataJSON, "base64url");
691
- if (clientDataBuf.length > MAX_CBOR_SIZE) return { verified: false, error: "clientDataJSON too large" };
692
- const clientData = JSON.parse(clientDataBuf.toString());
693
- if (clientData.type !== "webauthn.create") return { verified: false, error: "Wrong clientData type" };
694
- if (clientData.challenge !== opts.expectedChallenge) return { verified: false, error: "Challenge mismatch" };
695
- if (clientData.origin !== opts.expectedOrigin) return { verified: false, error: "Origin mismatch" };
696
- const attObjBuf = Buffer.from(opts.response.attestationObject, "base64url");
697
- if (attObjBuf.length > MAX_CBOR_SIZE) return { verified: false, error: "attestationObject too large" };
698
- const attObj = cborDecode(attObjBuf);
699
- const authData = parseAuthenticatorData(Buffer.isBuffer(attObj.authData) ? attObj.authData : Buffer.from(attObj.authData));
700
- const expectedHash = crypto2__namespace.createHash("sha256").update(opts.expectedRpId).digest();
701
- if (!expectedHash.equals(authData.rpIdHash)) return { verified: false, error: "RP ID hash mismatch" };
702
- if (!(authData.flags & 1)) return { verified: false, error: "User presence not verified" };
703
- if (!authData.credentialId || !authData.coseKey) return { verified: false, error: "No credential in authData" };
704
- const { pem, alg } = coseToPublicKeyPem(authData.coseKey);
705
- const credential = {
706
- credentialId: authData.credentialId.toString("base64url"),
707
- publicKeyPem: pem,
708
- publicKeyAlg: alg,
709
- userId: opts.userId,
710
- userHandle: opts.userId,
711
- signCount: authData.signCount,
712
- createdAt: Date.now(),
713
- lastUsedAt: Date.now(),
714
- aaguid: authData.aaguid
715
- };
716
- return { verified: true, credential };
717
- } catch (e) {
718
- return { verified: false, error: e?.message ?? "Verification failed" };
719
- }
720
- }
721
- function generateAuthenticationOptions(opts) {
722
- if (!opts.rpId) throw new Error("rpId is required");
723
- const store = opts.challengeStore ?? new ChallengeStore();
724
- return {
725
- challenge: store.create(),
726
- rpId: opts.rpId,
727
- timeout: 6e4,
728
- userVerification: opts.userVerification ?? "preferred",
729
- allowCredentials: opts.allowCredentialIds?.map((id) => ({ type: "public-key", id }))
730
- };
731
- }
732
- async function verifyAuthentication(opts) {
733
- try {
734
- if (!opts.response || !opts.expectedChallenge || !opts.expectedOrigin || !opts.expectedRpId || !opts.storedCredential) {
735
- return { verified: false, error: "Missing required parameters" };
736
- }
737
- const { response, storedCredential } = opts;
738
- const clientDataBuf = Buffer.from(response.clientDataJSON, "base64url");
739
- if (clientDataBuf.length > MAX_CBOR_SIZE) return { verified: false, error: "clientDataJSON too large" };
740
- const clientData = JSON.parse(clientDataBuf.toString());
741
- if (clientData.type !== "webauthn.get") return { verified: false, error: "Wrong clientData type" };
742
- if (clientData.challenge !== opts.expectedChallenge) return { verified: false, error: "Challenge mismatch" };
743
- if (clientData.origin !== opts.expectedOrigin) return { verified: false, error: "Origin mismatch" };
744
- const authDataBuf = Buffer.from(response.authenticatorData, "base64url");
745
- if (authDataBuf.length > MAX_CBOR_SIZE) return { verified: false, error: "authenticatorData too large" };
746
- const authData = parseAuthenticatorData(authDataBuf);
747
- const expectedHash = crypto2__namespace.createHash("sha256").update(opts.expectedRpId).digest();
748
- if (!expectedHash.equals(authData.rpIdHash)) return { verified: false, error: "RP ID hash mismatch" };
749
- if (!(authData.flags & 1)) return { verified: false, error: "User presence not verified" };
750
- if (opts.requireUserVerification && !(authData.flags & 4)) {
751
- return { verified: false, error: "User verification required but not performed" };
752
- }
753
- if (storedCredential.signCount > 0 && authData.signCount <= storedCredential.signCount) {
754
- return { verified: false, error: `Sign count replay detected: got ${authData.signCount}, expected > ${storedCredential.signCount}` };
755
- }
756
- const clientDataHash = crypto2__namespace.createHash("sha256").update(Buffer.from(response.clientDataJSON, "base64url")).digest();
757
- const signedData = Buffer.concat([authDataBuf, clientDataHash]);
758
- const sigBuf = Buffer.from(response.signature, "base64url");
759
- const pubKey = crypto2__namespace.createPublicKey(storedCredential.publicKeyPem);
760
- const alg = storedCredential.publicKeyAlg === -7 ? "SHA256" : "RSA-SHA256";
761
- const isValid = crypto2__namespace.verify(alg, signedData, pubKey, sigBuf);
762
- if (!isValid) return { verified: false, error: "Signature verification failed" };
763
- const updated = { ...storedCredential, signCount: authData.signCount, lastUsedAt: Date.now() };
764
- return { verified: true, credential: updated };
765
- } catch (e) {
766
- return { verified: false, error: e?.message ?? "Authentication verification failed" };
767
- }
768
- }
769
-
770
- exports.ChallengeStore = ChallengeStore;
771
- exports.CredentialStore = CredentialStore;
772
- exports.IPRateLimiter = IPRateLimiter;
773
- exports.JWTVerifier = JWTVerifier;
774
- exports.RevocationList = RevocationList;
775
- exports.createMiddleware = createMiddleware;
776
- exports.decodeJWT = decodeJWT;
777
- exports.detectAnomalies = detectAnomalies;
778
- exports.fetchJWKS = fetchJWKS;
779
- exports.generateAuthenticationOptions = generateAuthenticationOptions;
780
- exports.generateRegistrationOptions = generateRegistrationOptions;
781
- exports.jwksCacheUtils = jwksCacheUtils;
782
- exports.scoreRequest = scoreRequest;
783
- exports.signHMAC = signHMAC;
784
- exports.verifyAuthentication = verifyAuthentication;
785
- exports.verifyHMAC = verifyHMAC;
786
- exports.verifyRS256 = verifyRS256;
787
- exports.verifyRegistration = verifyRegistration;