securenow 7.7.9 → 7.7.10

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/NPM_README.md CHANGED
@@ -186,7 +186,7 @@ codex mcp add securenow -- npx securenow mcp
186
186
  npx -p securenow securenow-mcp
187
187
  ```
188
188
 
189
- The MCP surface exposes tools for applications, traces, logs, firewall, IP intelligence, forensics, notifications, blocklist, allowlist, trusted IPs, and docs-backed prompts/resources. Write actions require `confirm:true` and a reason. Use `securenow_blocklist_unblock` to stop firewall enforcement while keeping the block report/history; `securenow_blocklist_remove` is a compatibility alias.
189
+ The MCP surface exposes tools for applications, traces, logs, firewall, IP intelligence, forensics, notifications, blocklist, allowlist, trusted IPs, rate-limit remediation, and docs-backed prompts/resources. Write actions require `confirm:true` and a reason. Use `securenow_blocklist_unblock` to stop firewall enforcement while keeping the block report/history; `securenow_blocklist_remove` is a compatibility alias.
190
190
 
191
191
  For hosted clients, SecureNow can expose the same surface at `https://api.securenow.ai/mcp`. The hosted endpoint uses the same API authentication and scope checks as the rest of SecureNow.
192
192
 
@@ -256,6 +256,8 @@ npx securenow alerts rules
256
256
  npx securenow alerts rules show <rule-id>
257
257
  npx securenow alerts rules update <rule-id> --applications-all
258
258
  npx securenow alerts rules update <rule-id> --apps key1,key2
259
+ npx securenow alerts rules dry-run-query <rule-id> --sql @candidate.sql --app <key> --wait
260
+ npx securenow alerts rules tune-query <rule-id> --sql @candidate.sql --reason "Preserve exploit detector, remove noisy broad match" --apply-globally --yes
259
261
  npx securenow alerts channels
260
262
  npx securenow alerts history --limit 20
261
263
  ```
@@ -525,6 +527,9 @@ npx securenow logs --json --level error | jq '.logs'
525
527
  | | `blocklist unblock <id>` | Unblock IP and retain report/history |
526
528
  | | `blocklist remove <id>` | Compatibility alias for unblock |
527
529
  | | `blocklist stats` | Block stats |
530
+ | | `ratelimit parse "<request>"` | Fill a rate-limit draft from natural language |
531
+ | | `ratelimit from-text "<request>" --yes` | Create a soft rate-limit rule from natural language |
532
+ | | `ratelimit test <ip> --path /api/login` | Check whether a request would match rate-limit rules |
528
533
  | | `allowlist` | Allowed IPs (restrict-mode) |
529
534
  | | `allowlist add <ip>` | Allow IP (`--label`, `--reason`) |
530
535
  | | `allowlist remove <id>` | Remove from allowlist |
@@ -1287,6 +1292,7 @@ SecureNow provides multiple entry points depending on your needs:
1287
1292
  | `securenow/nuxt` | `modules: ['securenow/nuxt']` | Yes | Yes | Nuxt 3 module |
1288
1293
  | `securenow/nextjs-webpack-config` | `withSecureNow(config)` | - | - | Next.js config wrapper |
1289
1294
  | `securenow/firewall` | `require('securenow/firewall').init({...})` | No | Yes | Programmatic firewall API |
1295
+ | `securenow/rate-limits` | `require('securenow/rate-limits').parseRateLimitText(...)` | No | Yes | Rate-limit remediation API helper |
1290
1296
  | `securenow/tracing` | `require('securenow/tracing')` | Yes | No | Programmatic tracing API |
1291
1297
 
1292
1298
  ---
package/README.md CHANGED
@@ -214,7 +214,7 @@ codex mcp add securenow -- npx securenow mcp
214
214
  npx -p securenow securenow-mcp
215
215
  ```
216
216
 
217
- The MCP server reuses the same project-local `.securenow/credentials.json` as the CLI and SDK. It exposes tools for apps, traces, logs, firewall, IP intelligence, forensics, blocklist/allowlist/trusted IPs, plus resources for the bundled SecureNow docs and setup prompts.
217
+ The MCP server reuses the same project-local `.securenow/credentials.json` as the CLI and SDK. It exposes tools for apps, traces, logs, firewall, IP intelligence, forensics, blocklist/allowlist/trusted IPs, rate-limit remediation, plus resources for the bundled SecureNow docs and setup prompts.
218
218
 
219
219
  ---
220
220
 
@@ -334,6 +334,8 @@ After install, the `securenow` CLI is available via `npx securenow` or globally
334
334
  | `securenow notifications` | List notifications |
335
335
  | `securenow notifications unread` | Unread count |
336
336
  | `securenow alerts rules` | List alert rules |
337
+ | `securenow alerts rules dry-run-query <id> --sql @candidate.sql --wait` | Dry-run candidate alert SQL without saving |
338
+ | `securenow alerts rules tune-query <id> --sql @candidate.sql --apply-globally --yes` | Admin: update a shared system rule query mapping |
337
339
  | `securenow alerts history` | Alert history |
338
340
 
339
341
  ### Investigate
@@ -359,6 +361,8 @@ After install, the `securenow` CLI is available via `npx securenow` or globally
359
361
  | `securenow blocklist` | List blocked IPs |
360
362
  | `securenow blocklist add <ip> [--reason ...]` | Block an IP |
361
363
  | `securenow blocklist unblock <id> [--reason ...]` | Stop enforcement and keep block history |
364
+ | `securenow ratelimit parse "<request>"` | Fill a rate-limit rule draft from natural language |
365
+ | `securenow ratelimit from-text "<request>" --yes` | Create a soft rate-limit rule from natural language |
362
366
  | `securenow allowlist add <ip>` | Allow an IP (restrict-mode) |
363
367
  | `securenow trusted add <ip>` | Mark an IP as trusted |
364
368
 
package/SKILL-API.md CHANGED
@@ -4,7 +4,9 @@ Instrument any Node.js application with OpenTelemetry tracing, structured loggin
4
4
 
5
5
  **CLI parity:** every capability exposed below (redaction, CIDR matching, log/span emission, firewall preload, config inspection) has an equivalent `securenow` CLI command. See [SKILL-CLI.md](./SKILL-CLI.md) for the terminal surface.
6
6
 
7
- **MCP parity (v7.5+):** `npx securenow mcp` starts a local stdio MCP server for Codex, Claude, and other MCP clients. It reuses the same `.securenow/credentials.json` file as the CLI/SDK and exposes SecureNow tools, bundled docs resources, and setup prompts to agents.
7
+ **MCP parity (v7.5+):** `npx securenow mcp` starts a local stdio MCP server for Codex, Claude, and other MCP clients. It reuses the same `.securenow/credentials.json` file as the CLI/SDK and exposes SecureNow tools, bundled docs resources, and setup prompts to agents. Alert-rule operators can inspect notifications, read exact `metadata.matchedSubdetectors`, dry-run candidate SQL with `securenow_alert_rule_candidate_test`, and apply global system-rule query fixes with `securenow_alert_rule_query_update` when evidence is clear.
8
+
9
+ **Noisy alert-rule reviews:** prefer fixing a generic system-rule detector over creating customer-specific false positives. Dry-run candidate SQL first, preserve tenant scoping with `__USER_APP_KEYS__`, keep exploit-specific indicators, then save the shared query mapping only with an audit reason and explicit confirmation.
8
10
 
9
11
  ## Installation
10
12
 
@@ -97,9 +99,10 @@ operator needs to ensure those defaults immediately.
97
99
  | `securenow/nextjs-middleware` | Edge middleware body capture; exports `middleware()`, `redactSensitiveData()`, `DEFAULT_SENSITIVE_FIELDS` | CJS |
98
100
  | `securenow/nextjs-wrapper` | Route handler wrappers; exports `withSecureNow()`, `withSecureNowAsync()`, `captureRequestBody()`, `redactSensitiveData()` | CJS |
99
101
  | `securenow/nextjs-auto-capture` | Auto-patch Next request for body capture; exports `patchNextRequest()`, `safeBodyCapture()`, `redactSensitiveData()`, `isBodyCaptureEnabled()` | CJS |
100
- | `securenow/nuxt` | Nuxt 3 module (add to `modules` array) | ESM |
101
- | `securenow/firewall` | Standalone firewall; exports `init()`, `shutdown()`, `getStats()`, `getMatcher()`, `getAllowlistMatcher()` | CJS |
102
- | `securenow/firewall-only` | Preload: dotenv + firewall only, no tracing | Preload (`-r`) |
102
+ | `securenow/nuxt` | Nuxt 3 module (add to `modules` array) | ESM |
103
+ | `securenow/firewall` | Standalone firewall; exports `init()`, `shutdown()`, `getStats()`, `getMatcher()`, `getAllowlistMatcher()` | CJS |
104
+ | `securenow/rate-limits` | Rate-limit remediation API helper; exports `parseRateLimitText()`, `createRateLimitFromText()`, `createRateLimit()`, `listRateLimits()` | CJS |
105
+ | `securenow/firewall-only` | Preload: dotenv + firewall only, no tracing | Preload (`-r`) |
103
106
  | `securenow/cidr` | CIDR utilities; exports `createMatcher()`, `ipToInt()`, `parseCidr()`, `matchesCidr()` | CJS |
104
107
  | `securenow/resolve-ip` | IP resolution; exports `resolveClientIp()`, `resolveSocketIp()`, `isFromTrustedProxy()` | CJS |
105
108
  | `securenow/console-instrumentation` | Console→OTLP bridge; exports `originalConsole`, `restoreConsole()` | CJS |
package/SKILL-CLI.md CHANGED
@@ -24,7 +24,7 @@ codex mcp add securenow -- npx securenow mcp
24
24
  npx -p securenow securenow-mcp
25
25
  ```
26
26
 
27
- The MCP server reuses `.securenow/credentials.json` and exposes apps, traces, logs, firewall, IP intelligence, forensics, notifications, remediation tools, bundled docs resources, and setup prompts. Write tools require `confirm:true` plus a reason.
27
+ The MCP server reuses `.securenow/credentials.json` and exposes apps, traces, logs, firewall, IP intelligence, forensics, notifications, remediation tools, alert-rule review/tuning tools, bundled docs resources, and setup prompts. Write tools require `confirm:true` plus a reason.
28
28
 
29
29
  ### Authenticate
30
30
 
@@ -239,7 +239,7 @@ MCP parity:
239
239
  - `securenow_human_case_action_update`
240
240
  - prompts: `investigate_human_action_row`, `work_human_actions`
241
241
 
242
- Write tools still require `confirm:true` plus a reason. False positives should stay restrictive to the app, alert rule, path, method/status, user-agent, body pattern, or other exact evidence the AI report supports.
242
+ Write tools still require `confirm:true` plus a reason. False positives should stay restrictive to the app, alert rule, path, method/status, user-agent, body pattern, or other exact evidence the AI report supports. Notification IP investigations can include `metadata.matchedSubdetectors`; prefer that exact field over inferring which subdetector fired from the title or sample body.
243
243
 
244
244
  ### Alerts
245
245
 
@@ -250,10 +250,22 @@ securenow alerts rules show <id> # one rule; JSON: --json
250
250
  securenow alerts rules update <id> --applications-all # all current & future apps
251
251
  securenow alerts rules update <id> --apps key1,key2 # explicit app keys only
252
252
  securenow alerts rules test <id> --mode dry_run --wait # validate a rule query
253
+ securenow alerts rules dry-run-query <id> --sql @candidate.sql --app <key> --wait
254
+ securenow alerts rules tune-query <id> --sql @candidate.sql --reason "Preserve exploit detector, remove benign broad match" --apply-globally --yes
253
255
  securenow alerts rules exclusions <id> list # embedded rule exclusions
254
256
  securenow alerts channels # list alert channels (Slack, email, etc.)
255
- securenow alerts history [--limit N] # past triggered alerts
256
- ```
257
+ securenow alerts history [--limit N] # past triggered alerts
258
+ ```
259
+
260
+ MCP parity for noisy alert-rule reviews:
261
+
262
+ - `securenow_alert_rule_get`
263
+ - `securenow_alert_rule_candidate_test` dry-runs a full candidate SQL query without saving it.
264
+ - `securenow_alert_rule_test_result` polls the dry-run.
265
+ - `securenow_alert_rule_query_update` updates the shared public query mapping behind a system rule for all customer copies. It is admin-only, requires `confirm:true`, `applyGlobally:true`, `reason`, and SQL that keeps `__USER_APP_KEYS__` tenant scoping.
266
+ - `securenow_alert_rule_exclusion_add` remains the last-resort customer-specific path; it supports restrictive conditions plus `matchMode` and should not be used to hide a generic system-rule bug.
267
+
268
+ For system-rule tuning, dry-run the candidate SQL first, then save with `tune-query`/`securenow_alert_rule_query_update` only when the guard preserves attack detection. Good guards add exact exploit tokens, dangerous schemes, matched subdetectors, sensitive path/status evidence, malicious user agents, or repeat thresholds; bad guards simply suppress a noisy path.
257
269
 
258
270
  ---
259
271
 
@@ -322,6 +334,21 @@ work:
322
334
  - `securenow_blocklist_pending_bulk_reject`
323
335
  - prompt: `cleanup_legacy_pending_blocks`
324
336
 
337
+ ### Rate Limits - Soft Remediation
338
+
339
+ ```bash
340
+ securenow ratelimit list --env production
341
+ securenow ratelimit add 203.0.113.42 --route /api/login --limit 2 --window 1m --duration 24h
342
+ securenow ratelimit parse "rate limit 203.0.113.42 on POST /api/login to 2 attempts per minute"
343
+ securenow ratelimit from-text "rate limit /api/login for this IP to 2 attempts per minute for 24h" --yes
344
+ securenow ratelimit test 203.0.113.42 --path /api/login --method POST
345
+ ```
346
+
347
+ MCP parity:
348
+
349
+ - `securenow_rate_limit_parse`
350
+ - `securenow_rate_limit_create_from_text`
351
+
325
352
  ### Automation Rules
326
353
 
327
354
  ```bash
package/cli/rateLimits.js CHANGED
@@ -47,6 +47,54 @@ function describeTarget(rule) {
47
47
  return parts.join(' ') || 'all traffic';
48
48
  }
49
49
 
50
+ function naturalText(args, flags) {
51
+ return String(flags.text || flags.prompt || flags.query || args.join(' ') || '').trim();
52
+ }
53
+
54
+ function draftFromFlags(flags) {
55
+ const draft = {};
56
+ const appKey = resolveApp(flags);
57
+ const environment = resolveEnvironment(flags, null);
58
+ if (appKey) draft.appKey = appKey;
59
+ if (environment) draft.environment = environment;
60
+ if (flags.ip) draft.ip = flags.ip;
61
+ if (flags.route || flags.path || flags.pattern) draft.pathPattern = flags.route || flags.path || flags.pattern;
62
+ if (flags.mode || flags['path-mode']) draft.pathMatchMode = flags.mode || flags['path-mode'];
63
+ if (flags.method) draft.method = flags.method;
64
+ if (flags['key-by'] || flags.keyBy) draft.keyBy = flags['key-by'] || flags.keyBy;
65
+ if (flags.limit) draft.limit = Number(flags.limit);
66
+ if (flags.window || flags['window-seconds']) draft.windowSeconds = parseWindow(flags.window || flags['window-seconds']);
67
+ if (flags.duration) draft.duration = flags.duration;
68
+ if (flags.reason) draft.reason = flags.reason;
69
+ if (flags.name) draft.name = flags.name;
70
+ return draft;
71
+ }
72
+
73
+ function printParsedDraft(data) {
74
+ const draft = data.draft || {};
75
+ ui.keyValue([
76
+ ['Name', draft.name || '-'],
77
+ ['Target', describeTarget(draft)],
78
+ ['Limit', `${draft.limit || '-'} / ${draft.windowSeconds || '-'}s`],
79
+ ['Key by', draft.keyBy || 'ip'],
80
+ ['App', draft.appKey || 'all'],
81
+ ['Environment', draft.environment || 'all'],
82
+ ['Duration', draft.duration || 'none'],
83
+ ['Status', draft.status || 'active'],
84
+ ['Reason', draft.reason || '-'],
85
+ ['Provider', data.provider || '-'],
86
+ ['Confidence', data.confidence != null ? String(data.confidence) : '-'],
87
+ ]);
88
+ if (Array.isArray(data.missingFields) && data.missingFields.length) {
89
+ console.log('');
90
+ console.log(` ${ui.c.yellow('Missing:')} ${data.missingFields.join(', ')}`);
91
+ }
92
+ if (Array.isArray(data.warnings) && data.warnings.length) {
93
+ console.log('');
94
+ for (const warning of data.warnings.slice(0, 5)) console.log(` ${ui.c.yellow('!')} ${warning}`);
95
+ }
96
+ }
97
+
50
98
  async function list(args, flags) {
51
99
  requireAuth();
52
100
  const s = ui.spinner('Fetching rate limits');
@@ -76,6 +124,58 @@ async function list(args, flags) {
76
124
  }
77
125
  }
78
126
 
127
+ async function parseText(args, flags) {
128
+ requireAuth();
129
+ const text = naturalText(args, flags);
130
+ if (!text) {
131
+ ui.error('Usage: securenow ratelimit parse "rate limit /api/login to 2 attempts per minute"');
132
+ process.exit(1);
133
+ }
134
+
135
+ const s = ui.spinner('Parsing natural-language rate limit');
136
+ try {
137
+ const data = await api.post('/rate-limits/parse', { text, draft: draftFromFlags(flags) });
138
+ s.stop('Rate-limit draft parsed');
139
+ if (flags.json) { ui.json(data); return; }
140
+ console.log('');
141
+ printParsedDraft(data);
142
+ console.log('');
143
+ console.log(` ${ui.c.dim('Create it with:')} securenow ratelimit from-text ${JSON.stringify(text)} --yes`);
144
+ console.log('');
145
+ } catch (err) {
146
+ s.fail('Failed to parse rate limit');
147
+ throw err;
148
+ }
149
+ }
150
+
151
+ async function fromText(args, flags) {
152
+ requireAuth();
153
+ const text = naturalText(args, flags);
154
+ if (!text) {
155
+ ui.error('Usage: securenow ratelimit from-text "rate limit /api/login to 2 attempts per minute" --yes');
156
+ process.exit(1);
157
+ }
158
+ if (!flags.yes && !flags.force) {
159
+ const ok = await ui.confirm('Create this rate-limit rule from natural language?');
160
+ if (!ok) { ui.info('Cancelled'); return; }
161
+ }
162
+
163
+ const s = ui.spinner('Creating rate limit from natural language');
164
+ try {
165
+ const data = await api.post('/rate-limits/from-text', { text, draft: draftFromFlags(flags) });
166
+ s.stop('Rate limit created');
167
+ if (flags.json) { ui.json(data); return; }
168
+ const r = data.rateLimit;
169
+ console.log('');
170
+ console.log(` ${ui.c.green('ACTIVE')} ${describeTarget(r)} at ${r.limit}/${r.windowSeconds}s`);
171
+ console.log(` ${ui.c.dim('ID:')} ${r.id || r._id}`);
172
+ console.log('');
173
+ } catch (err) {
174
+ s.fail('Failed to create rate limit from text');
175
+ throw err;
176
+ }
177
+ }
178
+
79
179
  async function show(args, flags) {
80
180
  requireAuth();
81
181
  const id = args[0];
@@ -242,4 +342,4 @@ async function test(args, flags) {
242
342
  }
243
343
  }
244
344
 
245
- module.exports = { list, show, add, enable, disable, remove, test };
345
+ module.exports = { list, show, add, parseText, fromText, enable, disable, remove, test };
package/cli/security.js CHANGED
@@ -1,8 +1,9 @@
1
- 'use strict';
2
-
3
- const { api, requireAuth } = require('./client');
4
- const config = require('./config');
5
- const ui = require('./ui');
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const { api, requireAuth } = require('./client');
5
+ const config = require('./config');
6
+ const ui = require('./ui');
6
7
 
7
8
  function resolveApp(flags) {
8
9
  return flags.app || config.getDefaultApp();
@@ -44,6 +45,12 @@ async function alertRulesRoute(args, flags) {
44
45
  if (sub === 'test') {
45
46
  return alertRuleTest(args.slice(1), flags);
46
47
  }
48
+ if (sub === 'dry-run-query' || sub === 'candidate-test') {
49
+ return alertRuleCandidateTest(args.slice(1), flags);
50
+ }
51
+ if (sub === 'tune-query' || sub === 'query-update') {
52
+ return alertRuleQueryUpdate(args.slice(1), flags);
53
+ }
47
54
  if (sub === 'exclusions') {
48
55
  return alertRuleExclusions(args.slice(1), flags);
49
56
  }
@@ -180,6 +187,16 @@ async function alertRuleUpdate(args, flags) {
180
187
  }
181
188
  }
182
189
 
190
+ function readSqlArg(flags) {
191
+ const raw = flags.sql || flags.query || flags.file;
192
+ if (!raw) return null;
193
+ if (flags.file) return fs.readFileSync(flags.file, 'utf8');
194
+ const text = String(raw);
195
+ if (text === '-') return fs.readFileSync(0, 'utf8');
196
+ if (text.startsWith('@')) return fs.readFileSync(text.slice(1), 'utf8');
197
+ return text;
198
+ }
199
+
183
200
  async function alertRuleTest(args, flags) {
184
201
  requireAuth();
185
202
  const id = args[0];
@@ -219,6 +236,100 @@ async function alertRuleTest(args, flags) {
219
236
  }
220
237
  }
221
238
 
239
+ async function alertRuleCandidateTest(args, flags) {
240
+ requireAuth();
241
+ const id = args[0];
242
+ const candidateSqlQuery = readSqlArg(flags);
243
+ if (!id || !candidateSqlQuery) {
244
+ ui.error('Usage: securenow alerts rules dry-run-query <rule-id> --sql <sql|@file|-> [--app <key>] [--wait]');
245
+ process.exit(1);
246
+ }
247
+
248
+ const body = { mode: 'dry_run', candidateSqlQuery };
249
+ if (flags.app) body.applicationKey = flags.app;
250
+
251
+ const s = ui.spinner('Starting candidate SQL dry-run');
252
+ try {
253
+ let data = await api.post(`/alert-rules/${id}/test`, body);
254
+ if (flags.wait && data.testId) {
255
+ s.update('Waiting for candidate SQL dry-run results');
256
+ for (let i = 0; i < 40; i++) {
257
+ await new Promise((resolve) => setTimeout(resolve, 2000));
258
+ data = await api.get(`/alert-rules/${id}/test/${data.testId}`);
259
+ if (['complete', 'failed'].includes(data.status)) break;
260
+ }
261
+ }
262
+ s.stop('Candidate dry-run ready');
263
+ if (flags.json) { ui.json(data); return; }
264
+ console.log('');
265
+ ui.keyValue([
266
+ ['Test ID', data.testId || '-'],
267
+ ['Status', data.status || '-'],
268
+ ['Candidate', data.candidate ? 'yes' : 'no'],
269
+ ['Current SQL hash', data.currentSqlHash || '-'],
270
+ ['Candidate SQL hash', data.candidateSqlHash || '-'],
271
+ ['Result count', String(data.resultCount ?? '-')],
272
+ ['Error', data.error || '-'],
273
+ ]);
274
+ console.log('');
275
+ } catch (err) {
276
+ s.fail('Failed to dry-run candidate SQL');
277
+ throw err;
278
+ }
279
+ }
280
+
281
+ async function alertRuleQueryUpdate(args, flags) {
282
+ requireAuth();
283
+ const id = args[0];
284
+ const sqlQuery = readSqlArg(flags);
285
+ const reason = flags.reason;
286
+ if (!id || !sqlQuery || !reason) {
287
+ ui.error('Usage: securenow alerts rules tune-query <rule-id> --sql <sql|@file|-> --reason "..." --apply-globally --yes');
288
+ process.exit(1);
289
+ }
290
+ if (!flags['apply-globally'] && flags.applyGlobally !== true) {
291
+ ui.error('System query tuning requires --apply-globally.');
292
+ process.exit(1);
293
+ }
294
+
295
+ if (!flags.force && !flags.yes) {
296
+ const ok = await ui.confirm('Update the shared system query mapping for all customer copies?');
297
+ if (!ok) {
298
+ ui.info('Cancelled');
299
+ return;
300
+ }
301
+ }
302
+
303
+ const body = {
304
+ sqlQuery,
305
+ reason,
306
+ applyGlobally: true,
307
+ reactivatePausedCopies: !!(flags['reactivate-paused'] || flags.reactivatePausedCopies),
308
+ };
309
+ if (flags['expected-hash']) body.expectedCurrentSqlHash = flags['expected-hash'];
310
+ if (flags.notification) body.reviewNotificationId = flags.notification;
311
+ if (flags.note) body.reviewNote = flags.note;
312
+
313
+ const s = ui.spinner('Updating system query mapping');
314
+ try {
315
+ const data = await api.put(`/alert-rules/${id}/query-mapping`, body);
316
+ s.stop('System query mapping updated');
317
+ if (flags.json) { ui.json(data); return; }
318
+ console.log('');
319
+ ui.keyValue([
320
+ ['Query mapping', data.queryMapping?.name || data.queryMapping?.id || '-'],
321
+ ['Previous SQL hash', data.queryMapping?.previousSqlHash || '-'],
322
+ ['New SQL hash', data.queryMapping?.newSqlHash || '-'],
323
+ ['Affected system rules', String(data.affectedSystemRules ?? '-')],
324
+ ['Reactivated paused copies', data.reactivatedPausedCopies ? 'yes' : 'no'],
325
+ ]);
326
+ console.log('');
327
+ } catch (err) {
328
+ s.fail('Failed to update system query mapping');
329
+ throw err;
330
+ }
331
+ }
332
+
222
333
  async function alertRuleExclusions(args, flags) {
223
334
  requireAuth();
224
335
  const id = args[0];
package/cli.js CHANGED
@@ -208,16 +208,30 @@ const COMMANDS = {
208
208
  desc: 'Manage alerting',
209
209
  usage: 'securenow alerts <subcommand> [options]',
210
210
  sub: {
211
- rules: {
212
- desc: 'List, show, or update alert rules',
213
- flags: {
214
- json: 'Output as JSON',
215
- 'applications-all': 'With update: scope rule to all apps',
216
- 'no-applications-all': 'With update: scope to explicit --apps list',
217
- apps: 'Comma-separated app keys (with update)',
218
- },
219
- run: (a, f) => require('./cli/security').alertRulesRoute(a, f),
220
- },
211
+ rules: {
212
+ desc: 'List, show, update, test, or tune alert rules',
213
+ flags: {
214
+ json: 'Output as JSON',
215
+ 'applications-all': 'With update: scope rule to all apps',
216
+ 'no-applications-all': 'With update: scope to explicit --apps list',
217
+ apps: 'Comma-separated app keys (with update)',
218
+ app: 'Application key for rule tests',
219
+ mode: 'Rule test mode: dry_run or live',
220
+ wait: 'Wait for rule test completion',
221
+ sql: 'Candidate/replacement SQL, @file, or - for stdin',
222
+ query: 'Alias for --sql',
223
+ file: 'Read candidate/replacement SQL from a file',
224
+ reason: 'Audit reason for a write',
225
+ 'apply-globally': 'Required for system query tuning',
226
+ 'reactivate-paused': 'Reactivate paused system copies after tuning',
227
+ 'expected-hash': 'Expected current SQL SHA-256 hash',
228
+ notification: 'Review notification id tied to tuning',
229
+ note: 'Review note tied to tuning',
230
+ yes: 'Confirm write prompts',
231
+ force: 'Confirm write prompts',
232
+ },
233
+ run: (a, f) => require('./cli/security').alertRulesRoute(a, f),
234
+ },
221
235
  channels: { desc: 'List alert channels', run: (a, f) => require('./cli/security').alertChannelsList(a, f) },
222
236
  history: { desc: 'View alert history', flags: { limit: 'Max results' }, run: (a, f) => require('./cli/security').alertHistoryList(a, f) },
223
237
  },
@@ -255,7 +269,7 @@ const COMMANDS = {
255
269
  },
256
270
  ratelimit: {
257
271
  desc: 'Manage soft rate-limit remediation rules',
258
- usage: 'securenow ratelimit <list|add|show|test|enable|disable|remove> [options]',
272
+ usage: 'securenow ratelimit <list|add|parse|from-text|show|test|enable|disable|remove> [options]',
259
273
  flags: {
260
274
  app: 'Scope to app key (defaults to logged-in app)',
261
275
  env: 'Scope to environment (default for create/test: production)',
@@ -269,10 +283,13 @@ const COMMANDS = {
269
283
  mode: 'Path mode: exact, prefix, or regex',
270
284
  method: 'HTTP method, or ALL',
271
285
  reason: 'Audit reason',
286
+ text: 'Natural-language rate-limit request',
272
287
  },
273
288
  sub: {
274
289
  list: { desc: 'List rate-limit remediation rules', run: (a, f) => require('./cli/rateLimits').list(a, f) },
275
290
  add: { desc: 'Create a rate-limit rule', usage: 'securenow ratelimit add [ip] --route /api/login --limit 5 --window 1m --duration 24h', run: (a, f) => require('./cli/rateLimits').add(a, f) },
291
+ parse: { desc: 'Parse natural language into a rate-limit draft', usage: 'securenow ratelimit parse "rate limit /api/login to 2 attempts per minute"', run: (a, f) => require('./cli/rateLimits').parseText(a, f) },
292
+ 'from-text': { desc: 'Create a rate-limit rule from natural language', usage: 'securenow ratelimit from-text "rate limit /api/login to 2 attempts per minute" --yes', run: (a, f) => require('./cli/rateLimits').fromText(a, f) },
276
293
  show: { desc: 'Show one rate-limit rule', usage: 'securenow ratelimit show <id>', run: (a, f) => require('./cli/rateLimits').show(a, f) },
277
294
  test: { desc: 'Check whether a request would match rate-limit rules', usage: 'securenow ratelimit test <ip> --path /api/login --method POST', run: (a, f) => require('./cli/rateLimits').test(a, f) },
278
295
  enable: { desc: 'Enable a rate-limit rule', usage: 'securenow ratelimit enable <id>', run: (a, f) => require('./cli/rateLimits').enable(a, f) },
@@ -283,10 +300,27 @@ const COMMANDS = {
283
300
  },
284
301
  'rate-limit': {
285
302
  desc: 'Alias for ratelimit',
286
- usage: 'securenow rate-limit <list|add|show|test|enable|disable|remove> [options]',
303
+ usage: 'securenow rate-limit <list|add|parse|from-text|show|test|enable|disable|remove> [options]',
304
+ flags: {
305
+ app: 'Scope to app key (defaults to logged-in app)',
306
+ env: 'Scope to environment (default for create/test: production)',
307
+ environment: 'Alias for --env',
308
+ json: 'Output as JSON',
309
+ limit: 'Allowed requests per window',
310
+ window: 'Window size, e.g. 30s, 1m, 1h',
311
+ duration: 'Expiry, e.g. 24h or 7d',
312
+ route: 'Path pattern such as /api/login',
313
+ path: 'Alias for --route',
314
+ mode: 'Path mode: exact, prefix, or regex',
315
+ method: 'HTTP method, or ALL',
316
+ reason: 'Audit reason',
317
+ text: 'Natural-language rate-limit request',
318
+ },
287
319
  sub: {
288
320
  list: { desc: 'List rate-limit remediation rules', run: (a, f) => require('./cli/rateLimits').list(a, f) },
289
321
  add: { desc: 'Create a rate-limit rule', usage: 'securenow rate-limit add [ip] --route /api/login --limit 5 --window 1m --duration 24h', run: (a, f) => require('./cli/rateLimits').add(a, f) },
322
+ parse: { desc: 'Parse natural language into a rate-limit draft', usage: 'securenow rate-limit parse "rate limit /api/login to 2 attempts per minute"', run: (a, f) => require('./cli/rateLimits').parseText(a, f) },
323
+ 'from-text': { desc: 'Create a rate-limit rule from natural language', usage: 'securenow rate-limit from-text "rate limit /api/login to 2 attempts per minute" --yes', run: (a, f) => require('./cli/rateLimits').fromText(a, f) },
290
324
  show: { desc: 'Show one rate-limit rule', usage: 'securenow rate-limit show <id>', run: (a, f) => require('./cli/rateLimits').show(a, f) },
291
325
  test: { desc: 'Check whether a request would match rate-limit rules', usage: 'securenow rate-limit test <ip> --path /api/login --method POST', run: (a, f) => require('./cli/rateLimits').test(a, f) },
292
326
  enable: { desc: 'Enable a rate-limit rule', usage: 'securenow rate-limit enable <id>', run: (a, f) => require('./cli/rateLimits').enable(a, f) },
package/mcp/catalog.js CHANGED
@@ -169,7 +169,7 @@ const decisionReportInput = {
169
169
  description: 'Structured audit report explaining the decision, evidence reviewed, trace IDs, missing proof, and recommendations.',
170
170
  },
171
171
  decisionSummary: string('Short decision summary to record on the IP/case history.'),
172
- outcome: string('Decision outcome: blocked, false_positive, clean, deferred, case_action, rule_tuning, new_alert_rule, ambiguous, skipped, or other.'),
172
+ outcome: string('Decision outcome: blocked, rate_limited, false_positive, clean, deferred, case_action, rule_tuning, new_alert_rule, ambiguous, skipped, or other.'),
173
173
  evidence: arrayOfStrings('Evidence strings that support the decision.'),
174
174
  reviewedHistory: arrayOfStrings('History/proofs reviewed before deciding.'),
175
175
  traceIds: arrayOfStrings('Trace IDs reviewed for this decision.'),
@@ -513,7 +513,7 @@ const TOOLS = [
513
513
  {
514
514
  name: 'securenow_human_action_decision_report_add',
515
515
  title: 'Record Human Action Decision Report',
516
- description: 'Attach a structured analyst/MCP decision report to one Requires Human IP row without changing its status. Use for skipped, ambiguous, rule-tuning-needed, or already-handled rows. Write action; requires confirmation.',
516
+ description: 'Attach a structured analyst/MCP decision report to one Requires Human IP row without changing its status. Use for rate-limited, skipped, ambiguous, rule-tuning-needed, or already-handled rows. Write action; requires confirmation.',
517
517
  scope: 'notifications:write',
518
518
  readOnly: false,
519
519
  confirm: true,
@@ -802,6 +802,36 @@ const TOOLS = [
802
802
  ...confirmSchema,
803
803
  }, ['id', 'confirm', 'reason']),
804
804
  },
805
+ {
806
+ name: 'securenow_rate_limit_parse',
807
+ title: 'Parse Rate Limit Text',
808
+ description: 'Use SecureNow AI to convert natural language into a rate-limit remediation draft without creating it.',
809
+ scope: 'rate_limits:read',
810
+ readOnly: true,
811
+ method: 'POST',
812
+ endpoint: '/rate-limits/parse',
813
+ bodyFields: ['text', 'draft'],
814
+ inputSchema: objectSchema({
815
+ text: string('Natural-language rate-limit request, e.g. "rate limit /api/login to 2 attempts per minute for this IP".'),
816
+ draft: { type: 'object', additionalProperties: true, description: 'Optional existing draft fields to preserve or resolve phrases such as "this IP".' },
817
+ }, ['text']),
818
+ },
819
+ {
820
+ name: 'securenow_rate_limit_create_from_text',
821
+ title: 'Create Rate Limit From Text',
822
+ description: 'Create a rate-limit remediation rule from natural language. Write action; requires confirmation.',
823
+ scope: 'rate_limits:write',
824
+ readOnly: false,
825
+ confirm: true,
826
+ method: 'POST',
827
+ endpoint: '/rate-limits/from-text',
828
+ bodyFields: ['text', 'draft', 'reason'],
829
+ inputSchema: objectSchema({
830
+ text: string('Natural-language rate-limit request.'),
831
+ draft: { type: 'object', additionalProperties: true, description: 'Optional existing draft fields or scope hints.' },
832
+ ...confirmSchema,
833
+ }, ['text', 'confirm', 'reason']),
834
+ },
805
835
  {
806
836
  name: 'securenow_alert_rules_list',
807
837
  title: 'List Alert Rules',
@@ -852,6 +882,29 @@ const TOOLS = [
852
882
  ...confirmSchema,
853
883
  }, ['id', 'confirm', 'reason']),
854
884
  },
885
+ {
886
+ name: 'securenow_alert_rule_query_update',
887
+ title: 'Update System Alert Rule Query',
888
+ description: 'Admin-only: update the shared public query mapping behind a system alert rule for all customer copies. Write action; requires confirmation.',
889
+ scope: 'alerts:write',
890
+ readOnly: false,
891
+ confirm: true,
892
+ method: 'PUT',
893
+ endpoint: '/alert-rules/:id/query-mapping',
894
+ pathParams: ['id'],
895
+ bodyFields: ['sqlQuery', 'reason', 'expectedCurrentSqlHash', 'applyGlobally', 'reactivatePausedCopies', 'reviewNotificationId', 'reviewNote', 'source'],
896
+ fixedBody: { applyGlobally: true, source: 'mcp' },
897
+ inputSchema: objectSchema({
898
+ id: string('Alert rule id. Must be a system rule.'),
899
+ sqlQuery: string('Full replacement SQL query. System SQL must keep __USER_APP_KEYS__ for tenant scoping.'),
900
+ expectedCurrentSqlHash: string('Optional SHA-256 hash of the current SQL to prevent stale overwrites.'),
901
+ applyGlobally: boolean('Must be true. Updates the shared public query mapping used by all system rule copies.'),
902
+ reactivatePausedCopies: boolean('Whether to reactivate paused noisy copies after the query is tuned.'),
903
+ reviewNotificationId: string('Optional notification id tied to this review/tuning action.'),
904
+ reviewNote: string('Optional review note. Defaults to reason.'),
905
+ ...confirmSchema,
906
+ }, ['id', 'sqlQuery', 'confirm', 'reason']),
907
+ },
855
908
  {
856
909
  name: 'securenow_alert_rule_test',
857
910
  title: 'Test Alert Rule',
@@ -870,6 +923,25 @@ const TOOLS = [
870
923
  ...confirmSchema,
871
924
  }, ['id', 'confirm', 'reason']),
872
925
  },
926
+ {
927
+ name: 'securenow_alert_rule_candidate_test',
928
+ title: 'Dry-Run Candidate Alert Rule SQL',
929
+ description: 'Dry-run a candidate SQL/query guard for an alert rule without saving it.',
930
+ scope: 'alerts:write',
931
+ readOnly: false,
932
+ confirm: true,
933
+ method: 'POST',
934
+ endpoint: '/alert-rules/:id/test',
935
+ pathParams: ['id'],
936
+ bodyFields: ['applicationKey', 'candidateSqlQuery'],
937
+ fixedBody: { mode: 'dry_run' },
938
+ inputSchema: objectSchema({
939
+ id: string('Alert rule id.'),
940
+ applicationKey: string('Application key to test.'),
941
+ candidateSqlQuery: string('Full candidate SQL query to dry-run without saving.'),
942
+ ...confirmSchema,
943
+ }, ['id', 'candidateSqlQuery', 'confirm', 'reason']),
944
+ },
873
945
  {
874
946
  name: 'securenow_alert_rule_test_result',
875
947
  title: 'Get Alert Rule Test Result',
@@ -1386,9 +1458,11 @@ function promptMessages(name, args = {}) {
1386
1458
  `Fetch page=${page}, limit=${limit} with securenow_human_actions_list, select row ${rowNumber}, then call securenow_notifications_get and securenow_human_action_report for that notificationId and IP.`,
1387
1459
  'Read the AI report, finalDecision, investigation steps, findings, proofs, metadata paths/user agents/status codes, and trace IDs.',
1388
1460
  'Open trace evidence with securenow_traces_show and correlated logs with securenow_logs_for_trace when trace IDs are available.',
1389
- 'Return one clear outcome: Block IP, False Positive, Rule Tuning Needed, or Ambiguous. If evidence is ambiguous, stop and explain what is missing.',
1461
+ 'Return one clear outcome: Block IP, Rate Limit, False Positive, Rule Tuning Needed, or Ambiguous. If evidence is ambiguous, stop and explain what is missing.',
1462
+ 'Use rate limiting only as temporary soft remediation for repeated route-specific abuse such as login brute force, credential stuffing, scraping/API bursts, enumeration, recon/probing, or repeated noisy payloads where risk/impact evidence is below the block threshold.',
1463
+ 'Do not rate-limit instead of blocking when there is confirmed exploit success, token/data exposure, SSRF reachability, file read, RCE, persistence, malware/C2, or riskScore >= 85 with high-confidence malicious evidence. Do not rate-limit benign false positives, trusted monitors, app-server/proxy attribution problems, isolated one-off requests, or broad noisy rules that need alert tuning.',
1390
1464
  confirmWrites
1391
- ? 'The user requested execution. If evidence supports the decision, call securenow_human_action_block or securenow_human_action_false_positive with confirm:true, a precise reason, and a decisionReport containing summary, evidence, reviewedHistory, traceIds, and missingProof when relevant. If you skip/mark ambiguous but still need to record the audit trail, call securenow_human_action_decision_report_add.'
1465
+ ? 'The user requested execution. If evidence supports the decision, call securenow_human_action_block, securenow_rate_limit_create_from_text plus securenow_human_action_decision_report_add(outcome=rate_limited), or securenow_human_action_false_positive with confirm:true, a precise reason, and a decisionReport containing summary, evidence, reviewedHistory, traceIds, and missingProof when relevant. If you skip/mark ambiguous but still need to record the audit trail, call securenow_human_action_decision_report_add.'
1392
1466
  : 'Do not execute write tools yet. Prepare the recommended decision and exact tool call the user can approve.',
1393
1467
  'If many IPs share the same benign path/status/user-agent pattern, recommend tightening the alert rule with a precise guard instead of reviewing each IP as malicious.',
1394
1468
  'False positives must be narrow: app + alert rule + path + method/status/user-agent/body evidence where possible. Never globally trust an IP by default.',
@@ -1410,12 +1484,14 @@ function promptMessages(name, args = {}) {
1410
1484
  'Work my SecureNow Requires Human queue like a senior security analyst using the MCP tools.',
1411
1485
  `Review up to ${limit} row(s), most urgent first.${args.search ? ` Search filter: ${args.search}.` : ''}`,
1412
1486
  'Start with securenow_human_actions_list. For each row, call securenow_notifications_get and securenow_human_action_report, inspect the AI report/investigation steps/proofs/trace IDs, and fetch trace/log evidence where useful.',
1413
- 'For each row choose exactly one outcome: Block IP, False Positive, Rule Tuning Needed, or Skip because evidence is insufficient. Explain skipped rows.',
1487
+ 'For each row choose exactly one outcome: Block IP, Rate Limit, False Positive, Rule Tuning Needed, or Skip because evidence is insufficient. Explain skipped rows.',
1488
+ 'Use Rate Limit only for repeated route-specific abuse where temporary friction is safer than blocking: login brute force/credential stuffing, password reset/account enumeration, scraping/API bursts, path or ID enumeration, recon/probing, or repeated noisy payloads without confirmed exploit success.',
1489
+ 'Do not rate-limit confirmed high-risk attacks that should be blocked, benign traffic that should be false-positive scoped, broad noisy rules that need tuning, app-server/proxy attribution problems, or isolated one-off requests.',
1414
1490
  confirmWrites
1415
1491
  ? 'The user requested execution. For supported decisions, call the correct write tool with confirm:true, a precise reason, and a decisionReport containing summary, evidence, reviewedHistory, traceIds, and missingProof when relevant, then continue. For skipped/ambiguous/rule-tuning-needed rows that should be auditable without changing IP status, use securenow_human_action_decision_report_add.'
1416
1492
  : 'Do not execute write tools yet. Produce a row-by-row action plan and exact MCP write calls for user approval.',
1417
- 'For block decisions, use securenow_human_action_block. For false positives, use securenow_human_action_false_positive with restrictive conditions. For case-level tune_rule/create_exclusion rows, inspect securenow_notifications_get and then use securenow_human_case_action_update only when the action is safe to approve/reject.',
1418
- 'End with counts: handled, proposed block, proposed false positive, rule tuning needed, skipped, still waiting.',
1493
+ 'For block decisions, use securenow_human_action_block. For rate-limit decisions, use securenow_rate_limit_create_from_text and then securenow_human_action_decision_report_add with outcome=rate_limited. For false positives, use securenow_human_action_false_positive with restrictive conditions. For case-level tune_rule/create_exclusion rows, inspect securenow_notifications_get and then use securenow_human_case_action_update only when the action is safe to approve/reject.',
1494
+ 'End with counts: handled, proposed block, rate limits created/proposed, proposed false positive, rule tuning needed, skipped, still waiting.',
1419
1495
  ].join('\n'),
1420
1496
  },
1421
1497
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "securenow",
3
- "version": "7.7.9",
3
+ "version": "7.7.10",
4
4
  "description": "OpenTelemetry instrumentation for Node.js, Next.js, and Nuxt - Send traces and logs to any OTLP-compatible backend",
5
5
  "type": "commonjs",
6
6
  "main": "register.js",
@@ -79,6 +79,10 @@
79
79
  "./firewall": {
80
80
  "default": "./firewall.js"
81
81
  },
82
+ "./rate-limits": {
83
+ "types": "./rate-limits.d.ts",
84
+ "default": "./rate-limits.js"
85
+ },
82
86
  "./firewall-only": {
83
87
  "default": "./firewall-only.js"
84
88
  },
@@ -128,6 +132,8 @@
128
132
  "resolve-ip.js",
129
133
  "cidr.js",
130
134
  "firewall.js",
135
+ "rate-limits.js",
136
+ "rate-limits.d.ts",
131
137
  "firewall-only.js",
132
138
  "firewall-tcp.js",
133
139
  "firewall-iptables.js",
@@ -0,0 +1,86 @@
1
+ export interface RateLimitDraft {
2
+ name?: string;
3
+ appKey?: string;
4
+ applicationKey?: string;
5
+ environment?: string;
6
+ ip?: string;
7
+ pathPattern?: string;
8
+ path?: string;
9
+ route?: string;
10
+ pathMatchMode?: 'exact' | 'prefix' | 'regex';
11
+ method?: 'ALL' | 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS';
12
+ keyBy?: 'ip' | 'global';
13
+ limit?: number;
14
+ windowSeconds?: number;
15
+ duration?: string;
16
+ reason?: string;
17
+ status?: 'active' | 'disabled';
18
+ }
19
+
20
+ export interface RateLimitAuthOptions {
21
+ token?: string;
22
+ apiKey?: string;
23
+ authToken?: string;
24
+ }
25
+
26
+ export interface RateLimitTextOptions extends RateLimitAuthOptions {
27
+ draft?: RateLimitDraft;
28
+ reason?: string;
29
+ }
30
+
31
+ export interface ParsedRateLimitText {
32
+ success: boolean;
33
+ draft: RateLimitDraft;
34
+ payload: Record<string, unknown>;
35
+ confidence: number;
36
+ missingFields: string[];
37
+ warnings: string[];
38
+ explanation?: string;
39
+ provider: 'anthropic' | 'local-fallback' | string;
40
+ }
41
+
42
+ export interface RateLimitRule {
43
+ id?: string;
44
+ _id?: string;
45
+ applicationKey?: string | null;
46
+ environment?: string | null;
47
+ name?: string;
48
+ ip?: string;
49
+ method?: string;
50
+ pathPattern?: string;
51
+ pathMatchMode?: string;
52
+ keyBy?: string;
53
+ limit: number;
54
+ windowSeconds: number;
55
+ reason?: string;
56
+ source?: string;
57
+ status?: string;
58
+ expiresAt?: string | null;
59
+ metadata?: Record<string, unknown>;
60
+ createdAt?: string;
61
+ updatedAt?: string;
62
+ }
63
+
64
+ export interface RateLimitResponse {
65
+ success: boolean;
66
+ rateLimit: RateLimitRule;
67
+ parsed?: ParsedRateLimitText;
68
+ }
69
+
70
+ export function parseRateLimitText(text: string, options?: RateLimitTextOptions): Promise<ParsedRateLimitText>;
71
+ export function createRateLimitFromText(text: string, options?: RateLimitTextOptions): Promise<RateLimitResponse>;
72
+ export function createRateLimit(payload: Record<string, unknown>, options?: RateLimitAuthOptions): Promise<RateLimitResponse>;
73
+ export function listRateLimits(query?: Record<string, unknown>, options?: RateLimitAuthOptions): Promise<{
74
+ success: boolean;
75
+ rateLimits: RateLimitRule[];
76
+ total: number;
77
+ page: number;
78
+ limit: number;
79
+ }>;
80
+ export function testRateLimit(query?: Record<string, unknown>, options?: RateLimitAuthOptions): Promise<{
81
+ success: boolean;
82
+ limited: boolean;
83
+ matches: RateLimitRule[];
84
+ }>;
85
+ export function updateRateLimit(id: string, payload?: Record<string, unknown>, options?: RateLimitAuthOptions): Promise<RateLimitResponse>;
86
+ export function removeRateLimit(id: string, options?: RateLimitAuthOptions & { reason?: string }): Promise<RateLimitResponse>;
package/rate-limits.js ADDED
@@ -0,0 +1,88 @@
1
+ 'use strict';
2
+
3
+ const { api } = require('./cli/client');
4
+ const config = require('./cli/config');
5
+
6
+ function cleanText(value) {
7
+ return String(value || '').trim();
8
+ }
9
+
10
+ function authToken(options = {}) {
11
+ return (
12
+ options.token ||
13
+ options.apiKey ||
14
+ options.authToken ||
15
+ process.env.SECURENOW_TOKEN ||
16
+ process.env.SECURENOW_API_KEY ||
17
+ config.getToken() ||
18
+ config.getApiKey() ||
19
+ undefined
20
+ );
21
+ }
22
+
23
+ function requestOptions(options = {}) {
24
+ const token = authToken(options);
25
+ return token ? { token } : undefined;
26
+ }
27
+
28
+ function requireText(text) {
29
+ const value = cleanText(text);
30
+ if (!value) throw new Error('text is required');
31
+ return value;
32
+ }
33
+
34
+ async function parseRateLimitText(text, options = {}) {
35
+ return api.post(
36
+ '/rate-limits/parse',
37
+ {
38
+ text: requireText(text),
39
+ draft: options.draft && typeof options.draft === 'object' ? options.draft : {},
40
+ },
41
+ requestOptions(options)
42
+ );
43
+ }
44
+
45
+ async function createRateLimitFromText(text, options = {}) {
46
+ const body = {
47
+ text: requireText(text),
48
+ draft: options.draft && typeof options.draft === 'object' ? options.draft : {},
49
+ };
50
+ if (options.reason) body.reason = String(options.reason);
51
+ return api.post('/rate-limits/from-text', body, requestOptions(options));
52
+ }
53
+
54
+ async function createRateLimit(payload, options = {}) {
55
+ return api.post('/rate-limits', payload || {}, requestOptions(options));
56
+ }
57
+
58
+ async function listRateLimits(query = {}, options = {}) {
59
+ return api.get('/rate-limits', { query, ...(requestOptions(options) || {}) });
60
+ }
61
+
62
+ async function testRateLimit(query = {}, options = {}) {
63
+ return api.get('/rate-limits/check', { query, ...(requestOptions(options) || {}) });
64
+ }
65
+
66
+ async function updateRateLimit(id, payload = {}, options = {}) {
67
+ if (!id) throw new Error('id is required');
68
+ return api.put(`/rate-limits/${encodeURIComponent(id)}`, payload, requestOptions(options));
69
+ }
70
+
71
+ async function removeRateLimit(id, options = {}) {
72
+ if (!id) throw new Error('id is required');
73
+ const query = options.reason ? { reason: options.reason } : undefined;
74
+ return api.delete(`/rate-limits/${encodeURIComponent(id)}`, {
75
+ query,
76
+ ...(requestOptions(options) || {}),
77
+ });
78
+ }
79
+
80
+ module.exports = {
81
+ parseRateLimitText,
82
+ createRateLimitFromText,
83
+ createRateLimit,
84
+ listRateLimits,
85
+ testRateLimit,
86
+ updateRateLimit,
87
+ removeRateLimit,
88
+ };
package/resolve-ip.js CHANGED
@@ -110,8 +110,9 @@ function resolveClientIpWithDetails(request) {
110
110
  }
111
111
 
112
112
  if (LOOPBACK_RE.test(socketIp)) {
113
- const hostIp = [...getHostIps()][0] || '';
114
- if (hostIp) return { ...details, ip: hostIp, source: 'host-network' };
113
+ // A localhost request without proxy headers is app/server self-traffic.
114
+ // Rewriting it to the host's public IP can cause the app to block itself.
115
+ return { ...details, source: 'loopback' };
115
116
  }
116
117
 
117
118
  if (trustedProxy && getHostIps().has(socketIp)) {