@webdecoy/fcaptcha 1.28.2 → 1.29.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/config.js ADDED
@@ -0,0 +1,22 @@
1
+ 'use strict';
2
+
3
+ const INSECURE_DEFAULT_SECRET = 'dev-secret-change-in-production';
4
+
5
+ function signingSecretFromEnv(env = process.env) {
6
+ const secret = String(env.FCAPTCHA_SECRET || '').trim();
7
+ if (secret && secret !== INSECURE_DEFAULT_SECRET) return secret;
8
+ if (/^(1|true|yes|on)$/i.test(String(env.FCAPTCHA_INSECURE_DEV_MODE || '').trim())) {
9
+ console.warn('WARNING: FCAPTCHA_INSECURE_DEV_MODE enabled; tokens use a public signing key. Never expose this server to a network.');
10
+ return INSECURE_DEFAULT_SECRET;
11
+ }
12
+ throw new Error('FCAPTCHA_SECRET is required and must not be the public development key. For local-only development, explicitly set FCAPTCHA_INSECURE_DEV_MODE=1.');
13
+ }
14
+
15
+ function signingSecret(explicit, env = process.env) {
16
+ if (explicit !== undefined && explicit !== null) {
17
+ return signingSecretFromEnv({ ...env, FCAPTCHA_SECRET: explicit });
18
+ }
19
+ return signingSecretFromEnv(env);
20
+ }
21
+
22
+ module.exports = { INSECURE_DEFAULT_SECRET, signingSecret, signingSecretFromEnv };
package/index.js CHANGED
@@ -13,6 +13,7 @@ const crypto = require('crypto');
13
13
  const detection = require('./detection');
14
14
  const { ProxyTrust } = require('./clientip');
15
15
  const { SuspicionLedger, computeChallengeCost, BASE_MIN_AGE_MS } = require('./suspicion');
16
+ const { signingSecret } = require('./config');
16
17
 
17
18
  // =============================================================================
18
19
  // PoW Challenge Store (can be extended with Redis)
@@ -20,7 +21,7 @@ const { SuspicionLedger, computeChallengeCost, BASE_MIN_AGE_MS } = require('./su
20
21
 
21
22
  class PoWChallengeStore {
22
23
  constructor(options = {}) {
23
- this.secret = options.secret || 'dev-secret-change-in-production';
24
+ this.secret = signingSecret(options.secret);
24
25
  this.challenges = new Map();
25
26
  this.usedSolutions = new Set();
26
27
  this.expirationMs = options.expirationMs || 5 * 60 * 1000; // 5 minutes
@@ -212,7 +213,7 @@ const AUTOMATION_UA_PATTERNS = [
212
213
 
213
214
  class ScoringEngine {
214
215
  constructor(options = {}) {
215
- this.secret = options.secret || 'dev-secret-change-in-production';
216
+ this.secret = signingSecret(options.secret);
216
217
  this.powStore = options.powStore || new PoWChallengeStore({ secret: this.secret });
217
218
  this.rateLimiter = options.rateLimiter || new RateLimiter();
218
219
  this.fingerprintStore = options.fingerprintStore || new FingerprintStore();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webdecoy/fcaptcha",
3
- "version": "1.28.2",
3
+ "version": "1.29.0",
4
4
  "description": "Open source CAPTCHA with PoW, bot detection, and Vision AI protection",
5
5
  "main": "index.js",
6
6
  "exports": {
@@ -15,7 +15,7 @@
15
15
  "test:clientip": "node clientip.test.js",
16
16
  "test:limits": "node limits.test.js",
17
17
  "test:detection": "node detection.test.js",
18
- "test": "node clientip.test.js && node limits.test.js && node detection.test.js && node inputforensics.test.js && node webbotauth.test.js && node siteverify.test.js && node index.test.js && node --test suspicion.test.js",
18
+ "test": "node config.test.js && node clientip.test.js && node limits.test.js && node detection.test.js && node inputforensics.test.js && node webbotauth.test.js && node siteverify.test.js && node index.test.js && node --test suspicion.test.js",
19
19
  "test:siteverify": "node siteverify.test.js",
20
20
  "test:forensics": "node inputforensics.test.js",
21
21
  "test:suspicion": "node --test suspicion.test.js"
package/server.js CHANGED
@@ -12,6 +12,7 @@ const detection = require('./detection');
12
12
  const webbotauth = require('./webbotauth');
13
13
  const { ProxyTrust } = require('./clientip');
14
14
  const { BoundedMap, BoundedSet, SiteKeyGuard } = require('./limits');
15
+ const { signingSecretFromEnv } = require('./config');
15
16
  const { SuspicionLedger, computeChallengeCost, BASE_MIN_AGE_MS } = require('./suspicion');
16
17
  const { detectInputForensics } = require('./inputforensics');
17
18
  const {
@@ -27,13 +28,16 @@ const {
27
28
 
28
29
  const app = express();
29
30
  app.use(cors());
30
- app.use(express.json());
31
+ // Captured widget payloads are about 10-15 KiB. Four times that leaves ample
32
+ // room for future signals while bounding work done by the public JSON parsers.
33
+ const MAX_REQUEST_BODY_BYTES = 64 * 1024;
34
+ app.use(express.json({ limit: MAX_REQUEST_BODY_BYTES }));
31
35
  // The siteverify contract accepts form-encoded bodies as well as JSON, and most
32
36
  // PHP and Python integrations in the wild post form-encoded. Parsing both here
33
37
  // costs nothing on the JSON path.
34
- app.use(express.urlencoded({ extended: false }));
38
+ app.use(express.urlencoded({ extended: false, limit: MAX_REQUEST_BODY_BYTES }));
35
39
 
36
- const SECRET_KEY = process.env.FCAPTCHA_SECRET || 'dev-secret-change-in-production';
40
+ const SECRET_KEY = signingSecretFromEnv();
37
41
 
38
42
  // The credential a backend presents to validate a token. Defaults to the signing
39
43
  // key, which is what the README has always documented, but can be separated:
@@ -1787,6 +1791,16 @@ app.get('/api/challenge', (req, res) => {
1787
1791
  });
1788
1792
  });
1789
1793
 
1794
+ // Express identifies parser limit failures with status 413 / entity.too.large.
1795
+ // Handle them explicitly so all three servers expose the same status instead
1796
+ // of falling through to Express's HTML error response.
1797
+ app.use((err, req, res, next) => {
1798
+ if (err && (err.status === 413 || err.type === 'entity.too.large')) {
1799
+ return res.status(413).json({ error: 'request_too_large' });
1800
+ }
1801
+ return next(err);
1802
+ });
1803
+
1790
1804
  // =============================================================================
1791
1805
  // Start
1792
1806
  // =============================================================================