@webdecoy/fcaptcha 1.38.0 → 1.39.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.
Files changed (2) hide show
  1. package/package.json +5 -5
  2. package/webbotauth.js +32 -27
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webdecoy/fcaptcha",
3
- "version": "1.38.0",
3
+ "version": "1.39.0",
4
4
  "description": "Open source CAPTCHA with PoW, bot detection, and Vision AI protection",
5
5
  "main": "index.js",
6
6
  "exports": {
@@ -22,13 +22,13 @@
22
22
  },
23
23
  "dependencies": {
24
24
  "cors": "^2.8.5",
25
- "express": "^4.22.3",
25
+ "express": "^5.2.1",
26
26
  "ipaddr.js": "^2.4.0",
27
- "redis": "^4.6.12",
28
- "web-bot-auth": "^0.1.3"
27
+ "redis": "^6.2.1",
28
+ "web-bot-auth": "^0.2.0"
29
29
  },
30
30
  "peerDependencies": {
31
- "express": "^4.18.0"
31
+ "express": "^4.18.0 || ^5.0.0"
32
32
  },
33
33
  "peerDependenciesMeta": {
34
34
  "express": {
package/webbotauth.js CHANGED
@@ -59,7 +59,6 @@ const ipaddr = require('ipaddr.js');
59
59
  const {
60
60
  verify,
61
61
  jwkToKeyID,
62
- helpers,
63
62
  HTTP_MESSAGE_SIGNATURES_DIRECTORY,
64
63
  } = require('web-bot-auth');
65
64
  const { verifierFromJWK } = require('web-bot-auth/crypto');
@@ -69,11 +68,6 @@ const directoryAgent = new https.Agent({ keepAlive: true, maxTotalSockets: 16, m
69
68
  const MAX_DIRECTORY_BYTES = 256 * 1024;
70
69
  const DIRECTORY_CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour
71
70
 
72
- // The exact message web-bot-auth/crypto throws when crypto.subtle.verify()
73
- // returns false. This — and only this — is affirmative evidence of a forged
74
- // signature. Everything else (fetch failure, expiry, missing key) fails open.
75
- const CRYPTO_FAILURE_MESSAGE = 'invalid signature';
76
-
77
71
  // Discovery types from protocol-00. `directory` is the default when the header
78
72
  // carries no `type` parameter.
79
73
  const DISCOVERY_DIRECTORY = 'directory';
@@ -292,7 +286,9 @@ async function resolveKey(signatureAgentHeader, keyid) {
292
286
  for (const jwk of keys) {
293
287
  let thumbprint;
294
288
  try {
295
- thumbprint = await jwkToKeyID(jwk, helpers.WEBCRYPTO_SHA256, helpers.BASE64URL_DECODE);
289
+ thumbprint = await jwkToKeyID(jwk,
290
+ value => crypto.subtle.digest('SHA-256', value),
291
+ value => Buffer.from(value).toString('base64url'));
296
292
  } catch {
297
293
  continue; // unsupported/malformed key in the set — skip it
298
294
  }
@@ -426,38 +422,47 @@ async function checkWebBotAuth(req) {
426
422
  }
427
423
  const path = req.originalUrl || req.url || '/';
428
424
  const message = {
425
+ kind: 'request',
429
426
  method: (req.method || 'POST').toUpperCase(),
430
- url: `${forwardedScheme(req)}://${host}${path}`,
431
- headers,
427
+ targetUri: `${forwardedScheme(req)}://${host}${path}`,
428
+ fields: Object.entries(headers).flatMap(([name, value]) =>
429
+ (Array.isArray(value) ? value : [value]).filter(v => v != null)
430
+ .map(v => ({ name, value: String(v) }))),
432
431
  };
433
432
 
434
433
  let cryptoFailed = false;
435
434
  let resolvedKeyId = '';
436
435
  let resolvedAlg = '';
436
+ let verifiedAgent = agent;
437
437
 
438
438
  try {
439
439
  await withTimeout(
440
- verify(message, async (data, signature, params) => {
441
- if (!sigAgentRaw) {
442
- throw new FetchFailure('no Signature-Agent to resolve key');
443
- }
444
- assertWebBotAuthParams(params);
445
- const jwk = await resolveKey(sigAgentRaw, params.keyid);
446
- resolvedKeyId = params.keyid;
447
- resolvedAlg = jwk.alg || (jwk.crv === 'Ed25519' ? 'ed25519' : '');
448
- const verifyFn = await verifierFromJWK(jwk);
449
- try {
450
- await verifyFn(data, signature, params);
451
- } catch (e) {
452
- if (e && e.message === CRYPTO_FAILURE_MESSAGE) {
453
- cryptoFailed = true;
454
- }
455
- throw e;
456
- }
440
+ verify(message, {
441
+ async resolver(candidate) {
442
+ if (!candidate.signatureAgent) throw new FetchFailure('no Signature-Agent to resolve key');
443
+ // Resolve the covered dictionary member, not an unsigned first member.
444
+ const entry = candidate.signatureAgent;
445
+ const sourceHeader = `sig1=${JSON.stringify(entry.uri)};type=${entry.type}`;
446
+ const jwk = await resolveKey(sourceHeader, candidate.keyid);
447
+ const verifier = await verifierFromJWK(jwk);
448
+ resolvedKeyId = candidate.keyid;
449
+ resolvedAlg = verifier.algorithm;
450
+ verifiedAgent = displayAgent(JSON.stringify(entry.uri));
451
+ return {
452
+ ...verifier,
453
+ async verify(data, signature) {
454
+ const valid = await verifier.verify(data, signature);
455
+ // Only a completed cryptographic rejection is evidence of forgery.
456
+ if (valid === false) cryptoFailed = true;
457
+ return valid;
458
+ },
459
+ };
460
+ },
461
+ validate: assertWebBotAuthParams,
457
462
  }),
458
463
  VERIFY_TIMEOUT_MS,
459
464
  );
460
- return [verifiedDetection(agent, resolvedKeyId, resolvedAlg)];
465
+ return [verifiedDetection(verifiedAgent, resolvedKeyId, resolvedAlg)];
461
466
  } catch (e) {
462
467
  if (cryptoFailed) {
463
468
  return [forgedDetection(agent, e)];