@roboteby/parry 1.1.0-rc.1 → 2.0.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.
@@ -0,0 +1,255 @@
1
+ 'use strict';
2
+
3
+ const ipaddr = require('ipaddr.js');
4
+ const { NOSQL_DANGEROUS_OPERATORS, NOSQL_SUSPICIOUS_OPERATORS } = require('../constants/patterns');
5
+
6
+ const HEADER_NAME_PATTERN = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
7
+ const NOSQL_PATH_PATTERN = /^(?:body|query|params)(?:\.|\[|$)/;
8
+
9
+ function assertObject(value, name) {
10
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
11
+ throw new TypeError(`${name} must be an object`);
12
+ }
13
+ }
14
+
15
+ function assertPositive(value, name, { integer = false, allowZero = false } = {}) {
16
+ if (value === undefined) return;
17
+ if (
18
+ typeof value !== 'number' ||
19
+ !Number.isFinite(value) ||
20
+ (integer && !Number.isInteger(value)) ||
21
+ (allowZero ? value < 0 : value <= 0)
22
+ ) {
23
+ throw new TypeError(
24
+ `${name} must be a ${allowZero ? 'non-negative' : 'positive'}${integer ? ' integer' : ' number'}`
25
+ );
26
+ }
27
+ }
28
+
29
+ function validateIpOrCidr(value, name) {
30
+ if (typeof value !== 'string' || value.trim() === '') {
31
+ throw new TypeError(`${name} must be a non-empty IP address or CIDR`);
32
+ }
33
+
34
+ try {
35
+ if (value.includes('/')) ipaddr.parseCIDR(value);
36
+ else ipaddr.parse(value);
37
+ } catch {
38
+ throw new TypeError(`${name} contains an invalid IP address or CIDR: ${value}`);
39
+ }
40
+ }
41
+
42
+ function validateTrustedProxies(value, name = 'trustedProxies') {
43
+ if (value === undefined) return;
44
+ if (!Array.isArray(value)) throw new TypeError(`${name} must be an array`);
45
+ value.forEach((entry, index) => validateIpOrCidr(entry, `${name}[${index}]`));
46
+ }
47
+
48
+ function normalizeHeadersConfig(value) {
49
+ const defaultHeaders = ['user-agent', 'referer', 'x-forwarded-for', 'cookie'];
50
+ if (value === undefined) return { scan: defaultHeaders };
51
+ assertObject(value, 'headers');
52
+ const scan = value.scan === undefined ? defaultHeaders : value.scan;
53
+ if (!Array.isArray(scan)) throw new TypeError('headers.scan must be an array');
54
+
55
+ const normalized = [];
56
+ const seen = new Set();
57
+ for (const header of scan) {
58
+ validateHeaderName(header, 'headers.scan');
59
+ const name = header.toLowerCase();
60
+ if (!seen.has(name)) {
61
+ seen.add(name);
62
+ normalized.push(name);
63
+ }
64
+ }
65
+ return { scan: normalized };
66
+ }
67
+
68
+ function validateHeaderName(value, name = 'header') {
69
+ if (typeof value !== 'string' || !HEADER_NAME_PATTERN.test(value)) {
70
+ throw new TypeError(`${name} contains an invalid header name: ${String(value)}`);
71
+ }
72
+ }
73
+
74
+ function normalizeNoSQLConfig(value) {
75
+ if (value === undefined) return { enabled: true, allowedOperators: {} };
76
+ if (typeof value === 'boolean') return { enabled: value, allowedOperators: {} };
77
+ assertObject(value, 'nosql');
78
+
79
+ if (value.enabled !== undefined && typeof value.enabled !== 'boolean') {
80
+ throw new TypeError('nosql.enabled must be a boolean');
81
+ }
82
+ const allowedOperators = value.allowedOperators || {};
83
+ assertObject(allowedOperators, 'nosql.allowedOperators');
84
+
85
+ const normalized = Object.create(null);
86
+ for (const [path, operators] of Object.entries(allowedOperators)) {
87
+ if (!NOSQL_PATH_PATTERN.test(path)) {
88
+ throw new TypeError(`nosql.allowedOperators contains an invalid exact path: ${path}`);
89
+ }
90
+ if (!Array.isArray(operators)) {
91
+ throw new TypeError(`nosql.allowedOperators.${path} must be an array`);
92
+ }
93
+
94
+ normalized[path] = [];
95
+ const seen = new Set();
96
+ for (const operator of operators) {
97
+ if (typeof operator !== 'string' || !operator.startsWith('$')) {
98
+ throw new TypeError(`nosql.allowedOperators.${path} contains an invalid operator`);
99
+ }
100
+ if (NOSQL_DANGEROUS_OPERATORS.has(operator)) {
101
+ throw new TypeError(`NoSQL operator ${operator} can never be allowlisted`);
102
+ }
103
+ if (!NOSQL_SUSPICIOUS_OPERATORS.has(operator)) {
104
+ throw new TypeError(`Only suspicious NoSQL operators may be allowlisted: ${operator}`);
105
+ }
106
+ if (!seen.has(operator)) {
107
+ seen.add(operator);
108
+ normalized[path].push(operator);
109
+ }
110
+ }
111
+ }
112
+
113
+ return {
114
+ enabled: value.enabled !== false,
115
+ allowedOperators: normalized,
116
+ };
117
+ }
118
+
119
+ function validateRateConfig(rate, name) {
120
+ if (rate === undefined) return;
121
+ assertObject(rate, name);
122
+ assertPositive(rate.maxRequests, `${name}.maxRequests`, { integer: true });
123
+ assertPositive(rate.windowMs, `${name}.windowMs`, { integer: true });
124
+ }
125
+
126
+ function validateMatch(match, name) {
127
+ assertObject(match, name);
128
+ if (match.method === undefined && match.path === undefined) {
129
+ throw new TypeError(`${name} must define method or path`);
130
+ }
131
+ const validateList = (value, field, validateEntry) => {
132
+ const values = Array.isArray(value) ? value : [value];
133
+ if (values.length === 0 || values.some((entry) => !validateEntry(entry))) {
134
+ throw new TypeError(`${name}.${field} is invalid`);
135
+ }
136
+ };
137
+ if (match.method !== undefined) {
138
+ validateList(
139
+ match.method,
140
+ 'method',
141
+ (entry) => typeof entry === 'string' && entry.trim() !== ''
142
+ );
143
+ }
144
+ if (match.path !== undefined) {
145
+ validateList(
146
+ match.path,
147
+ 'path',
148
+ (entry) => entry instanceof RegExp || (typeof entry === 'string' && entry.trim() !== '')
149
+ );
150
+ }
151
+ }
152
+
153
+ function validateParryOptions(options) {
154
+ if (options === undefined) return;
155
+ assertObject(options, 'options');
156
+
157
+ assertPositive(options.maxRequests, 'maxRequests', { integer: true });
158
+ assertPositive(options.windowMs, 'windowMs', { integer: true });
159
+ assertPositive(options.banDurationMs, 'banDurationMs', { integer: true });
160
+ assertPositive(options.suspiciousThreshold, 'suspiciousThreshold', { integer: true });
161
+ assertPositive(options.maxObjectDepth, 'maxObjectDepth', { integer: true, allowZero: true });
162
+ if (options.rateLimit && typeof options.rateLimit === 'object') {
163
+ assertPositive(options.rateLimit.max, 'rateLimit.max', { integer: true });
164
+ assertPositive(options.rateLimit.maxRequests, 'rateLimit.maxRequests', { integer: true });
165
+ assertPositive(options.rateLimit.windowMs, 'rateLimit.windowMs', { integer: true });
166
+ }
167
+
168
+ if (options.requestShape !== undefined) {
169
+ assertObject(options.requestShape, 'requestShape');
170
+ assertPositive(options.requestShape.maxDepth, 'requestShape.maxDepth', {
171
+ integer: true,
172
+ allowZero: true,
173
+ });
174
+ assertPositive(options.requestShape.maxKeys, 'requestShape.maxKeys', { integer: true });
175
+ assertPositive(options.requestShape.maxArrayLength, 'requestShape.maxArrayLength', {
176
+ integer: true,
177
+ });
178
+ assertPositive(options.requestShape.maxStringLength, 'requestShape.maxStringLength', {
179
+ integer: true,
180
+ });
181
+ }
182
+
183
+ if (options.events !== undefined) {
184
+ assertObject(options.events, 'events');
185
+ assertPositive(options.events.maxEvents, 'events.maxEvents', { integer: true });
186
+ }
187
+
188
+ if (options.requestId !== undefined) {
189
+ assertObject(options.requestId, 'requestId');
190
+ if (options.requestId.header !== undefined) {
191
+ validateHeaderName(options.requestId.header, 'requestId.header');
192
+ }
193
+ if (
194
+ options.requestId.responseHeader !== undefined &&
195
+ options.requestId.responseHeader !== false
196
+ ) {
197
+ validateHeaderName(options.requestId.responseHeader, 'requestId.responseHeader');
198
+ }
199
+ }
200
+
201
+ if (options.admin !== undefined) {
202
+ assertObject(options.admin, 'admin');
203
+ if (options.admin.path !== undefined && !String(options.admin.path).startsWith('/')) {
204
+ throw new TypeError('admin.path must start with /');
205
+ }
206
+ }
207
+
208
+ if (options.bruteForce !== undefined) {
209
+ assertObject(options.bruteForce, 'bruteForce');
210
+ }
211
+
212
+ if (options.policies !== undefined) {
213
+ if (!Array.isArray(options.policies)) throw new TypeError('policies must be an array');
214
+ options.policies.forEach((policy, index) => {
215
+ assertObject(policy, `policies[${index}]`);
216
+ if (typeof policy.name !== 'string' || policy.name.trim() === '') {
217
+ throw new TypeError(`policies[${index}].name must be a non-empty string`);
218
+ }
219
+ validateMatch(policy.match, `policies[${index}].match`);
220
+ validateRateConfig(policy.rateLimit, `policies[${index}].rateLimit`);
221
+ if (policy.rateLimit) {
222
+ assertPositive(policy.rateLimit.max, `policies[${index}].rateLimit.max`, {
223
+ integer: true,
224
+ });
225
+ }
226
+ if (policy.bruteForce) {
227
+ assertObject(policy.bruteForce, `policies[${index}].bruteForce`);
228
+ assertPositive(policy.bruteForce.maxAttempts, `policies[${index}].bruteForce.maxAttempts`, {
229
+ integer: true,
230
+ });
231
+ assertPositive(policy.bruteForce.windowMs, `policies[${index}].bruteForce.windowMs`, {
232
+ integer: true,
233
+ });
234
+ assertPositive(
235
+ policy.bruteForce.blockDurationMs,
236
+ `policies[${index}].bruteForce.blockDurationMs`,
237
+ { integer: true }
238
+ );
239
+ }
240
+ });
241
+ }
242
+
243
+ validateTrustedProxies(options.trustedProxies);
244
+ normalizeHeadersConfig(options.headers);
245
+ normalizeNoSQLConfig(options.nosql);
246
+ }
247
+
248
+ module.exports = {
249
+ normalizeHeadersConfig,
250
+ normalizeNoSQLConfig,
251
+ validateHeaderName,
252
+ validateIpOrCidr,
253
+ validateParryOptions,
254
+ validateTrustedProxies,
255
+ };
@@ -17,8 +17,7 @@ const SQL_PATTERNS = [
17
17
  ];
18
18
 
19
19
  const XSS_PATTERNS = [
20
- /<\s*script[\s\S]*?>[\s\S]*?<\s*\/\s*script\s*>/i,
21
- /<\s*script\b[^>]*>/i,
20
+ /<\s*script\b[^>]{0,500}>/i,
22
21
  /\bon\w+\s*=\s*["']?[^"'>]*/i,
23
22
  /javascript\s*:/i,
24
23
  /vbscript\s*:/i,
@@ -27,8 +26,8 @@ const XSS_PATTERNS = [
27
26
  /<\s*svg\b[^>]*>[\s\S]*?(script|onload|onerror)/i,
28
27
  /expression\s*\(/i,
29
28
  /url\s*\(\s*["']?\s*javascript/i,
30
- /\{\{[\s\S]{0,200}\}\}/,
31
- /\$\{[\s\S]{0,200}\}/,
29
+ /\{\{[^{}]{0,200}(?:constructor(?:\.constructor)?|alert\s*\(|document\.|window\.)[^{}]{0,200}\}\}/i,
30
+ /\$\{[^{}]{0,200}(?:constructor(?:\.constructor)?|alert\s*\(|document\.|window\.)[^{}]{0,200}\}/i,
32
31
  /<\s*(base|link|meta|style)\b[^>]*(http-equiv|href|content)\s*=\s*["']?[^"'>]*script/i,
33
32
  /\0|%00/,
34
33
  /autofocus.{0,30}onfocus/i,
package/package.json CHANGED
@@ -1,39 +1,65 @@
1
1
  {
2
2
  "name": "@roboteby/parry",
3
- "version": "1.1.0-rc.1",
3
+ "version": "2.0.0",
4
4
  "description": "Application-layer security middleware for Express.js with injection detection, abuse mitigation, brute-force protection and distributed rate limiting.",
5
5
  "main": "./src/index.js",
6
6
  "types": "./types/index.d.ts",
7
7
  "scripts": {
8
- "test": "node tests/index.js",
9
- "test:unit": "node -e \"(async()=>{ await require('./tests/unit/detectors.test'); await require('./tests/unit/rateLimiter.test'); await require('./tests/unit/memoryStore.test'); await require('./tests/unit/redisStore.test'); await require('./tests/unit/policyMatcher.test'); await require('./tests/unit/keyBuilder.test'); await require('./tests/unit/bruteForceGuard.test'); await require('./tests/unit/engine.test'); await require('./tests/unit/applicationGuards.test'); await require('./tests/unit/observability.test'); await require('./tests/unit/adminAuth.test'); })().catch((err)=>{ console.error(err); process.exit(1); })\"",
10
- "test:integ": "node tests/integration/middleware.test.js",
8
+ "test": "node --test tests/unit/*.test.js tests/integration/*.test.js tests/package/*.test.js",
9
+ "test:unit": "node --test tests/unit/*.test.js",
10
+ "test:integration": "node --test tests/integration/*.test.js",
11
+ "test:integ": "npm run test:integration",
12
+ "test:package": "node --test tests/package/*.test.js",
13
+ "test:types": "tsc -p tests/types/tsconfig.json",
11
14
  "test:fixtures": "node scripts/payloads/validate-fixtures.js",
12
- "test:payload-regression": "node tests/regression/index.js",
15
+ "test:payload-regression": "node --test tests/regression/*.test.js",
13
16
  "test:payload-report": "node scripts/payloads/generate-payload-report.js",
14
17
  "package:check": "node scripts/package/check-package.js",
15
18
  "package:check-tag": "node scripts/package/check-version-tag.js",
16
19
  "package:dry-run": "npm pack --dry-run",
17
20
  "example": "node examples/express-basic.js",
18
- "test:http": "node scripts/run-tests.js",
19
- "start:test": "node scripts/test-server.js",
20
21
  "docker:build:demo": "docker build -f docker/demo-api/Dockerfile -t parry-demo-api .",
21
- "format": "prettier --write \"src/**/*.js\"",
22
- "format:check": "prettier --check \"src/**/*.js\""
22
+ "lint": "eslint .",
23
+ "format": "prettier --write .",
24
+ "format:check": "prettier --check ."
23
25
  },
24
26
  "exports": {
25
27
  ".": {
26
28
  "require": "./src/index.js",
27
29
  "types": "./types/index.d.ts"
28
30
  },
29
- "./core": "./src/core/index.js",
30
- "./detectors": "./src/detectors/index.js",
31
- "./stores": "./src/stores/index.js",
32
- "./policies": "./src/policies/index.js",
33
- "./brute-force": "./src/brute-force/index.js",
34
- "./events": "./src/events/index.js",
35
- "./observability": "./src/observability/index.js",
36
- "./admin": "./src/admin/index.js",
31
+ "./core": {
32
+ "require": "./src/core/index.js",
33
+ "types": "./types/core.d.ts"
34
+ },
35
+ "./detectors": {
36
+ "require": "./src/detectors/index.js",
37
+ "types": "./types/detectors.d.ts"
38
+ },
39
+ "./stores": {
40
+ "require": "./src/stores/index.js",
41
+ "types": "./types/stores.d.ts"
42
+ },
43
+ "./policies": {
44
+ "require": "./src/policies/index.js",
45
+ "types": "./types/policies.d.ts"
46
+ },
47
+ "./brute-force": {
48
+ "require": "./src/brute-force/index.js",
49
+ "types": "./types/brute-force.d.ts"
50
+ },
51
+ "./events": {
52
+ "require": "./src/events/index.js",
53
+ "types": "./types/events.d.ts"
54
+ },
55
+ "./observability": {
56
+ "require": "./src/observability/index.js",
57
+ "types": "./types/observability.d.ts"
58
+ },
59
+ "./admin": {
60
+ "require": "./src/admin/index.js",
61
+ "types": "./types/admin.d.ts"
62
+ },
37
63
  "./package.json": "./package.json"
38
64
  },
39
65
  "files": [
@@ -66,24 +92,29 @@
66
92
  "license": "MIT",
67
93
  "repository": {
68
94
  "type": "git",
69
- "url": "git+ssh://git@github.com/RobotEby/parry-express-security-middleware.git"
95
+ "url": "git+https://github.com/RobotEby/parry.git"
70
96
  },
71
97
  "bugs": {
72
- "url": "https://github.com/RobotEby/parry-express-security-middleware/issues"
98
+ "url": "https://github.com/RobotEby/parry/issues"
73
99
  },
74
- "homepage": "https://github.com/RobotEby/parry-express-security-middleware#readme",
100
+ "homepage": "https://github.com/RobotEby/parry#readme",
75
101
  "peerDependencies": {
76
102
  "express": "^5.2.1"
77
103
  },
78
104
  "devDependencies": {
79
- "eslint-config-prettier": "^10.1.8",
105
+ "@eslint/js": "^10.0.1",
106
+ "@types/express": "^5.0.6",
107
+ "@types/node": "^26.4.0",
108
+ "eslint": "^10.9.1",
80
109
  "express": "^5.2.1",
81
- "prettier": "^3.8.1"
110
+ "globals": "^17.11.0",
111
+ "prettier": "^3.9.6",
112
+ "typescript": "^7.0.2"
82
113
  },
83
114
  "engines": {
84
- "node": ">=18"
115
+ "node": ">=22"
85
116
  },
86
117
  "dependencies": {
87
- "ipaddr.js": "^2.4.0"
118
+ "ipaddr.js": "^2.5.0"
88
119
  }
89
120
  }
@@ -5,27 +5,59 @@ const { authenticateToken } = require('./strategies/token');
5
5
  const { authenticateIpAllowlist } = require('./strategies/ip-allowlist');
6
6
  const { authenticateTrustedProxy } = require('./strategies/trusted-proxy');
7
7
  const { authenticateCombined } = require('./strategies/combined');
8
- const { authenticateNone } = require('./strategies/none');
8
+ const { authenticateNone, warnInsecureAdminApi } = require('./strategies/none');
9
9
  const { authenticateCloudflareAccess } = require('./strategies/cloudflare-access');
10
10
  const { authenticateAlbAuth } = require('./strategies/alb-auth');
11
+ const { validateHeaderName, validateTrustedProxies } = require('../../../config/validate');
12
+
13
+ const SUPPORTED_MODES = new Set([
14
+ 'token',
15
+ 'ip-allowlist',
16
+ 'trusted-proxy',
17
+ 'cloudflare-access',
18
+ 'alb-auth',
19
+ 'cognito-alb',
20
+ 'combined',
21
+ 'none',
22
+ ]);
11
23
 
12
24
  function requireAdminAuth(options = {}, context = null) {
13
25
  if (typeof options.auth === 'function') return createLegacyCallbackMiddleware(options.auth);
14
26
 
15
- if (options.requireAuth && !options.auth) {
27
+ const contextAuth = context?.config?.admin?.auth;
28
+ const authConfig = isAuthConfig(options.auth) ? options.auth : contextAuth;
29
+ if (authConfig) {
30
+ return createAdminAuthMiddleware(authConfig, {
31
+ ...context,
32
+ admin: context?.config?.admin,
33
+ });
34
+ }
35
+
36
+ const insecureOptIn =
37
+ options.allowInsecureAdminApi === true ||
38
+ options.requireAuth === false ||
39
+ context?.config?.admin?.allowInsecureAdminApi === true;
40
+
41
+ if (insecureOptIn) {
42
+ return createAdminAuthMiddleware(
43
+ { mode: 'none', allowInsecureAdminApi: true },
44
+ { ...context, admin: context?.config?.admin }
45
+ );
46
+ }
47
+
48
+ if (process.env.NODE_ENV === 'production') {
49
+ throw new Error('Admin API requires an authentication strategy in production.');
50
+ }
51
+
52
+ if (options.requireAuth === true) {
16
53
  return function missingAuthMiddleware(_req, res) {
17
54
  return unauthorized(res);
18
55
  };
19
56
  }
20
57
 
21
- const contextAuth = context?.config?.admin?.auth;
22
- const authConfig = isAuthConfig(options.auth) ? options.auth : contextAuth;
23
- if (!authConfig) return (_req, _res, next) => next();
24
-
25
- return createAdminAuthMiddleware(authConfig, {
26
- ...context,
27
- admin: context?.config?.admin,
28
- });
58
+ throw new Error(
59
+ 'Admin API requires authentication. Configure auth or explicitly allow insecure local access.'
60
+ );
29
61
  }
30
62
 
31
63
  function createAdminAuthMiddleware(config, context = {}) {
@@ -67,17 +99,51 @@ async function authenticateAdminRequest(req, config, context = {}) {
67
99
  function validateAdminAuthConfig(config, context = {}) {
68
100
  const mode = normalizeMode(config?.mode);
69
101
 
102
+ if (!SUPPORTED_MODES.has(mode)) {
103
+ throw new Error(`Unsupported Admin API auth mode: ${mode}`);
104
+ }
105
+
70
106
  if (mode === 'token' && !hasNonEmptyString(config.token)) {
71
107
  throw new Error('Admin API token auth requires a non-empty token.');
72
108
  }
109
+ if (mode === 'token' && config.header !== undefined) {
110
+ validateHeaderName(config.header, 'auth.header');
111
+ }
73
112
 
74
113
  if (mode === 'ip-allowlist' && !hasNonEmptyArray(config.allowedIps)) {
75
114
  throw new Error('Admin API ip-allowlist auth requires allowedIps.');
76
115
  }
116
+ if (mode === 'ip-allowlist') validateTrustedProxies(config.allowedIps, 'auth.allowedIps');
77
117
 
78
118
  if (mode === 'trusted-proxy' && !hasNonEmptyArray(config.trustedProxies)) {
79
119
  throw new Error('Admin API trusted-proxy auth requires trustedProxies.');
80
120
  }
121
+ if (config.trustedProxies !== undefined) {
122
+ validateTrustedProxies(config.trustedProxies, 'auth.trustedProxies');
123
+ }
124
+
125
+ for (const field of [
126
+ 'proxySharedSecretHeader',
127
+ 'userHeader',
128
+ 'emailHeader',
129
+ 'rolesHeader',
130
+ 'jwtHeader',
131
+ 'dataHeader',
132
+ ]) {
133
+ if (config[field] !== undefined) validateHeaderName(config[field], `auth.${field}`);
134
+ }
135
+ if (config.requiredHeaders !== undefined) {
136
+ if (
137
+ !config.requiredHeaders ||
138
+ typeof config.requiredHeaders !== 'object' ||
139
+ Array.isArray(config.requiredHeaders)
140
+ ) {
141
+ throw new TypeError('auth.requiredHeaders must be an object');
142
+ }
143
+ for (const header of Object.keys(config.requiredHeaders)) {
144
+ validateHeaderName(header, 'auth.requiredHeaders');
145
+ }
146
+ }
81
147
 
82
148
  if (mode === 'cloudflare-access' || mode === 'alb-auth' || mode === 'cognito-alb') {
83
149
  validateExternalAuthConfig(mode, config);
@@ -101,13 +167,11 @@ function validateAdminAuthConfig(config, context = {}) {
101
167
  }
102
168
  }
103
169
 
104
- if (
105
- mode === 'none' &&
106
- process.env.NODE_ENV === 'production' &&
107
- !config.allowInsecureAdminApi &&
108
- !context?.admin?.allowInsecureAdminApi
109
- ) {
110
- throw new Error('Admin API auth mode "none" is not allowed in production.');
170
+ if (mode === 'none') {
171
+ if (process.env.NODE_ENV === 'production') {
172
+ throw new Error('Admin API auth mode "none" is not allowed in production.');
173
+ }
174
+ warnInsecureAdminApi();
111
175
  }
112
176
  }
113
177
 
@@ -5,16 +5,20 @@ const { success } = require('../utils/result');
5
5
 
6
6
  let warned = false;
7
7
 
8
- function authenticateNone(req, config) {
8
+ function warnInsecureAdminApi() {
9
9
  if (!warned) {
10
10
  warned = true;
11
11
  console.warn(
12
12
  '[parry] Admin API auth mode "none" is insecure and intended only for local development.'
13
13
  );
14
14
  }
15
+ }
16
+
17
+ function authenticateNone(req, config) {
18
+ warnInsecureAdminApi();
15
19
 
16
20
  const ip = getClientIp(req, config);
17
21
  return success(req, 'none', { subject: 'insecure-none', ip }, config);
18
22
  }
19
23
 
20
- module.exports = { authenticateNone };
24
+ module.exports = { authenticateNone, warnInsecureAdminApi };