securenow 8.24.0 → 8.25.1
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 +693 -305
- package/README.md +76 -28
- package/SKILL-API.md +648 -525
- package/SKILL-CLI.md +293 -663
- package/app-config.js +19 -1
- package/cidr.js +139 -20
- package/cli/init.js +77 -15
- package/cli/utils.js +26 -14
- package/cli.js +7 -5
- package/console-instrumentation.js +6 -0
- package/firewall-cloud.js +81 -32
- package/firewall-iptables.js +123 -62
- package/firewall-only.js +2 -0
- package/firewall-tcp.js +19 -5
- package/firewall.js +21 -1
- package/framework-routes.js +222 -0
- package/log-format.js +2 -0
- package/log-redaction.js +135 -0
- package/mcp/catalog.js +14 -9
- package/next-route.js +61 -0
- package/nextjs-middleware.js +8 -4
- package/nextjs-wrapper.js +8 -4
- package/nextjs.d.ts +35 -67
- package/nextjs.js +29 -6
- package/package.json +5 -2
- package/register.d.ts +27 -34
- package/request-capture.js +11 -6
- package/resolve-ip.js +23 -5
- package/sessions.d.ts +27 -0
- package/sessions.js +71 -10
- package/tracing.d.ts +40 -121
- package/tracing.js +12 -4
package/SKILL-API.md
CHANGED
|
@@ -1,193 +1,150 @@
|
|
|
1
|
-
|
|
1
|
+
# SecureNow SDK — Agent Skill
|
|
2
2
|
|
|
3
3
|
> **Node.js security:** Use a supported, patched runtime: Node.js 22.23.4+, 24.22.1+, or 26.12.1+ on its respective release line. Earlier patches are affected by [CVE-2026-111497](https://nodejs.org/en/blog/vulnerability/october-2026-security-releases). Upgrading the SecureNow package does not patch Node.js; upgrade the runtime and restart every application process.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
The integration playbook for the `securenow` npm package. A coding agent that reads only this file can plug SecureNow into a Node.js app, turn on every feature, deploy it and prove it works. It describes 8.25.1; items marked **(8.25.0+)** are new in 8.25.0, so an older install needs `npm install securenow@latest` for them (check with `npx securenow version`). The terminal reference is [SKILL-CLI.md](./SKILL-CLI.md). Both files ship in `node_modules/securenow/`.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
## Rules for the agent
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
1. Never print, paste or commit `.securenow/admin.json`, `.securenow/runtime.json` or `.securenow/credentials*.json`: they hold the admin token and the runtime API key (`snk_live_...`). Mask keys in summaries.
|
|
10
|
+
2. Start authentication only with `npx securenow login` from the project root, then wait for the person to finish the browser step. Never open an auth URL yourself (the CLI generates the callback and state).
|
|
11
|
+
3. Configuration lives in `.securenow/runtime.json`. The SDK reads exactly two environment variables, `SECURENOW_CREDENTIALS` and `SECURENOW_DISABLED`. Every other `SECURENOW_*` variable (`SECURENOW_APPID`, `SECURENOW_API_KEY`, `SECURENOW_INSTANCE`, `SECURENOW_NO_UUID`, `SECURENOW_LOGGING_ENABLED`, `SECURENOW_CAPTURE_BODY`, ...) is ignored, whatever an older guide says. Do not create `.env` files for SecureNow.
|
|
12
|
+
4. Ask the person before anything that changes enforcement in production: blocks, allowlist entries, rate limits, challenges, session revocations, rule promotion, blocking mode. CLI writes reach the running app within about 10 seconds; MCP writes need `confirm: true` and a reason.
|
|
13
|
+
5. Put the preload on the command that actually starts the server process (start script, Dockerfile `CMD`, PM2 `node_args`, systemd `ExecStart`). Keep the project's existing middleware, instrumentation and scripts.
|
|
14
|
+
6. Confirm any CLI command or flag with `npx securenow help <command>` (works offline). Don't invent URLs or ports: smoke-test only a URL the project or the person gives you.
|
|
10
15
|
|
|
11
|
-
|
|
16
|
+
## 1. Your stack → what to do
|
|
12
17
|
|
|
13
|
-
|
|
18
|
+
| Your stack | Wire it with | Section |
|
|
19
|
+
|---|---|---|
|
|
20
|
+
| Express, Fastify, NestJS, Koa, Hapi, Hono (`@hono/node-server`), Polka, Feathers, h3, raw `http`/`https`/`http2`, any server on `node:http` | `node -r securenow/register <entry>`, the same flag for CommonJS, ESM and TypeScript | 3.0–3.11 |
|
|
21
|
+
| Next.js 15 or 16 | `npx securenow init` (writes `instrumentation.ts`, patches `next.config.*`) | 3.12 |
|
|
22
|
+
| Next.js 14 | `npx securenow init` **(8.25.0+)** | 3.13 |
|
|
23
|
+
| Nuxt 3 or 4 | `modules: ['securenow/nuxt']` in `nuxt.config`, then `npx securenow init` | 3.14 |
|
|
24
|
+
| Vite (`vite preview`, a Vite SSR server) | `node -r securenow/register-vite ...` or `-r securenow/register` | 3.15 |
|
|
25
|
+
| Bun 1.3, app served through `node:http` | `bun --preload securenow/register app.js` | 3.16 |
|
|
26
|
+
| IP blocking only, no telemetry | `node -r securenow/firewall-only <entry>` | 3.17 |
|
|
27
|
+
| PM2, Docker, Kubernetes, systemd, Railway/Render/Heroku/Fly, Vercel | the wiring above, plus production credentials | 6 |
|
|
28
|
+
| Deno, Cloudflare Workers, Vercel Edge / Next.js Edge runtime, `Bun.serve`, browser-only apps | not supported: SecureNow runs inside a Node.js process | 9 |
|
|
29
|
+
| AWS Lambda and other serverless functions | not supported or tested: don't promise it | 9 |
|
|
14
30
|
|
|
15
|
-
|
|
16
|
-
npm install securenow@latest
|
|
17
|
-
```
|
|
31
|
+
## 2. Setup checklist
|
|
18
32
|
|
|
19
|
-
|
|
33
|
+
1. **Install.** `npm install securenow@latest` (or the project's package manager), then `npx securenow version`.
|
|
34
|
+
2. **Log in.** `npx securenow login` from the project root. The person picks or creates the app in the browser. It writes `.securenow/runtime.json` (app key, runtime API key, SDK config) and `.securenow/admin.json` (CLI/MCP auth) and adds both to `.gitignore` without ignoring the whole `.securenow/` folder. One lane at a time: `npx securenow app connect` (runtime only) or `npx securenow admin login` (admin only). Check with `npx securenow whoami`.
|
|
35
|
+
3. **Init.** `npx securenow init` detects the framework (Next.js, Nuxt, Express, Fastify, Koa, Hapi, otherwise plain Node), fills `.securenow/runtime.json` with defaults and an explanation for each key (`deploymentEnvironment: "local"`; change it with `--env staging` etc.), writes `instrumentation.ts` and patches `next.config.*` for Next.js, prints the start-script change for plain Node, reminds you to add the module for Nuxt, and warns about a PM2 ecosystem file that uses a bare `-r securenow/register`. When an instrumentation file or config is too custom to patch, it prints an agent-ready prompt with the exact edits instead.
|
|
36
|
+
4. **Wire.** Section 3 for the stack.
|
|
37
|
+
5. **Deploy credentials.** `npx securenow credentials runtime --env production` writes `.securenow/credentials.production.json`. Ship it as `<app-root>/.securenow/credentials.json` (secret file or mount), or, on hosts without file mounts, print it as one variable with `--format env` and store `SECURENOW_CREDENTIALS` as a secret (section 6).
|
|
38
|
+
6. **Verify.** `npx securenow whoami`, `npx securenow doctor`, `npx securenow test-span`, start the app and read its `[securenow]` lines, send one real request, then `npx securenow status` and `npx securenow today` (section 7).
|
|
20
39
|
|
|
21
|
-
|
|
40
|
+
## 3. Wiring per framework
|
|
22
41
|
|
|
23
|
-
|
|
42
|
+
Every server built on `node:http` is covered by one preload: tracing, logs, header and body capture, crash capture, the firewall, rate limits, challenges and, **(8.25.0+)**, the zero-code session guard. No application code changes.
|
|
24
43
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
node -
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
npx securenow admin login # admin/control-plane CLI + MCP auth only
|
|
33
|
-
npx securenow app connect # SDK runtime app + runtime API key only
|
|
34
|
-
npx securenow init
|
|
44
|
+
```json
|
|
45
|
+
{
|
|
46
|
+
"scripts": {
|
|
47
|
+
"start": "node -r securenow/register src/index.js",
|
|
48
|
+
"dev": "node -r securenow/register --watch src/index.js"
|
|
49
|
+
}
|
|
50
|
+
}
|
|
35
51
|
```
|
|
36
52
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
### 2. Run With Instrumentation
|
|
53
|
+
Other ways to load the same preload:
|
|
40
54
|
|
|
41
|
-
|
|
55
|
+
- `npx securenow run src/index.js`: spawns Node with the preload (and the ESM hook for ESM apps) and passes Node flags through (`--watch`, `--inspect`). `npx securenow src/index.js` is a shorthand.
|
|
56
|
+
- `NODE_OPTIONS="-r securenow/register" npm start`, when the start command can't be edited. Every Node process started with that variable loads SecureNow, npm itself included, so prefer the flag in the script.
|
|
57
|
+
- CommonJS only: `require('securenow/register');` as the very first line of the entry file, before any other `require`.
|
|
42
58
|
|
|
43
|
-
|
|
44
|
-
npx securenow run src/index.js
|
|
45
|
-
```
|
|
59
|
+
### 3.0 CommonJS, ESM and TypeScript
|
|
46
60
|
|
|
47
|
-
**
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
node -r securenow/register src/index.js
|
|
51
|
-
```
|
|
61
|
+
- **CommonJS:** the `-r` flag, or the first-line `require`.
|
|
62
|
+
- **ESM** (`"type": "module"` or an `.mjs` entry): use the `-r` flag. It registers the OpenTelemetry ESM hook through `module.register()` (Node 20.6+) and prints `[securenow] ESM loader hook auto-registered (module.register)`. Don't `import 'securenow/register'` inside the app: static imports load before it runs, so Express, pg and the rest are not instrumented.
|
|
63
|
+
- **TypeScript:** `node -r securenow/register --import tsx src/index.ts`, or `node -r securenow/register -r ts-node/register src/index.ts` (SecureNow first), or run the compiled output: `node -r securenow/register dist/index.js`. Types ship for `securenow/events`, `securenow/sessions`, `securenow/tracing`, `securenow/nextjs`, `securenow/nuxt`.
|
|
52
64
|
|
|
53
|
-
|
|
65
|
+
### 3.1 Express
|
|
54
66
|
|
|
55
|
-
```
|
|
56
|
-
|
|
67
|
+
```js
|
|
68
|
+
// src/index.js: no SecureNow code
|
|
69
|
+
const express = require('express');
|
|
70
|
+
const app = express();
|
|
71
|
+
app.use(express.json()); // SecureNow records the body the app reads
|
|
72
|
+
app.post('/api/login', (req, res) => res.json({ ok: true }));
|
|
73
|
+
app.listen(3000);
|
|
57
74
|
```
|
|
58
75
|
|
|
59
|
-
|
|
76
|
+
Start with `node -r securenow/register src/index.js`. Records `http.route` (`/users/:id`). One span per middleware is off by default (`config.otel.middlewareSpans`). Tested on Express 4.22 and 5.3 (CommonJS, ESM, tsx, `NODE_OPTIONS`, `securenow run`).
|
|
60
77
|
|
|
61
|
-
|
|
78
|
+
### 3.2 Fastify
|
|
62
79
|
|
|
63
|
-
|
|
80
|
+
Start with `node -r securenow/register server.js`. The route template (`http.route`, span name `GET /users/:id`) and handler exceptions come from Fastify's `fastify.initialization` diagnostics channel. Tested on Fastify 5.12.
|
|
64
81
|
|
|
65
|
-
|
|
66
|
-
the user picks or creates an app. The runtime API key lives in `.securenow/runtime.json`
|
|
67
|
-
so admin CLI/MCP login cannot overwrite it:
|
|
82
|
+
### 3.3 NestJS
|
|
68
83
|
|
|
69
84
|
```bash
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
npx securenow api-key set snk_live_abc123...
|
|
85
|
+
npm run build && node -r securenow/register dist/main.js # compiled (production)
|
|
86
|
+
node -r securenow/register -r ts-node/register src/main.ts # TypeScript directly
|
|
73
87
|
```
|
|
74
88
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
The automatically created key is scoped to the selected app only. Its
|
|
78
|
-
`runtime_app` scopes are `traces:write`, `logs:write`, `firewall:read`,
|
|
79
|
-
`blocklist:read`, and `allowlist:read`. `securenow api-key create` uses the
|
|
80
|
-
current app from `.securenow/runtime.json` by default, resolves that app UUID to
|
|
81
|
-
the server app id, and stores the new one-time plaintext key back as `apiKey`.
|
|
82
|
-
|
|
83
|
-
The firewall syncs your blocklist and enforces it on every request — zero code changes.
|
|
89
|
+
`-r securenow/register` goes before `-r ts-node/register`. Records `http.route`. Tested on NestJS 11.2 with the default Express adapter.
|
|
84
90
|
|
|
85
|
-
|
|
86
|
-
mark the active block as `removed`, invalidate firewall sync, clear expiry to
|
|
87
|
-
avoid TTL deletion, and retain block reports/history for future review or
|
|
88
|
-
reblock context.
|
|
91
|
+
### 3.4 Koa
|
|
89
92
|
|
|
90
|
-
|
|
91
|
-
`config.firewall.versionCheckInterval` to `1` or `2` in the protected app's
|
|
92
|
-
`.securenow/runtime.json` or production runtime credentials. The SDK polls `/firewall/sync` with ETag/304, so
|
|
93
|
-
unchanged checks are lightweight; keep `config.firewall.syncInterval` high as a
|
|
94
|
-
safety-net full refresh.
|
|
93
|
+
Start with the preload. `http.route` needs `@koa/router`; body capture needs a body parser (`koa-bodyparser` or `@koa/bodyparser`), because SecureNow records the body as the app reads it. Tested on Koa 3.3 with `@koa/router`.
|
|
95
94
|
|
|
96
|
-
|
|
97
|
-
idempotently provisions risk-score rules for all apps/environments:
|
|
98
|
-
`riskScore >= 95` blocks for 7 days, `90-94` for 72h, and `85-89` for 24h.
|
|
99
|
-
Run `securenow automation defaults --yes` or the API backfill script when an
|
|
100
|
-
operator needs to ensure those defaults immediately.
|
|
95
|
+
### 3.5 Hapi
|
|
101
96
|
|
|
102
|
-
|
|
97
|
+
Start with the preload. Records `http.route`. Tested on Hapi 21.4.
|
|
103
98
|
|
|
104
|
-
|
|
99
|
+
### 3.6 Hono (`@hono/node-server`)
|
|
105
100
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
| `securenow/nextjs` | Next.js instrumentation; exports `registerSecureNow(options?)` | CJS |
|
|
111
|
-
| `securenow/nextjs-webpack-config` | Next.js config wrapper; exports `withSecureNow()`, `getSecureNowWebpackConfig()`, `EXTERNAL_PACKAGES` | CJS |
|
|
112
|
-
| `securenow/nextjs-middleware` | Edge middleware body capture; exports `middleware()`, `redactSensitiveData()`, `DEFAULT_SENSITIVE_FIELDS` | CJS |
|
|
113
|
-
| `securenow/nextjs-wrapper` | Route handler wrappers; exports `withSecureNow()`, `withSecureNowAsync()`, `captureRequestBody()`, `redactSensitiveData()` | CJS |
|
|
114
|
-
| `securenow/nextjs-auto-capture` | Auto-patch Next request for body capture; exports `patchNextRequest()`, `safeBodyCapture()`, `redactSensitiveData()`, `isBodyCaptureEnabled()` | CJS |
|
|
115
|
-
| `securenow/nuxt` | Nuxt 3 module (add to `modules` array) | ESM |
|
|
116
|
-
| `securenow/firewall` | Standalone firewall; exports `init()`, `shutdown()`, `getStats()`, `getMatcher()`, `getAllowlistMatcher()` | CJS |
|
|
117
|
-
| `securenow/rate-limits` | Rate-limit remediation API helper; exports `parseRateLimitText()`, `createRateLimitFromText()`, `createRateLimit()`, `listRateLimits()` | CJS |
|
|
118
|
-
| `securenow/firewall-only` | Preload: credentials-file firewall only, no tracing | Preload (`-r`) |
|
|
119
|
-
| `securenow/cidr` | CIDR utilities; exports `createMatcher()`, `ipToInt()`, `parseCidr()`, `matchesCidr()` | CJS |
|
|
120
|
-
| `securenow/resolve-ip` | IP resolution; exports `resolveClientIp()`, `resolveSocketIp()`, `isFromTrustedProxy()` | CJS |
|
|
121
|
-
| `securenow/console-instrumentation` | Console→OTLP bridge; exports `originalConsole`, `restoreConsole()` | CJS |
|
|
122
|
-
| `securenow/web-vite` | Browser OTel (document load, fetch, XHR, user interaction); default export `startSecurenowWeb()`. Sending browser telemetry to SecureNow is disabled since 8.0 (no browser-safe auth yet) | ESM |
|
|
123
|
-
| `securenow/register-vite` | Preload: the same runtime as `securenow/register`, for the Node process serving a Vite app (`vite preview`, Vite SSR) | Preload (`-r`) |
|
|
124
|
-
|
|
125
|
-
---
|
|
126
|
-
|
|
127
|
-
## Framework Integration Guides
|
|
101
|
+
```js
|
|
102
|
+
// app.mjs
|
|
103
|
+
import { serve } from '@hono/node-server';
|
|
104
|
+
import { Hono } from 'hono';
|
|
128
105
|
|
|
129
|
-
|
|
106
|
+
const app = new Hono();
|
|
107
|
+
app.get('/users/:id', (c) => c.json({ id: c.req.param('id') }));
|
|
108
|
+
serve({ fetch: app.fetch, port: 3000 });
|
|
109
|
+
```
|
|
130
110
|
|
|
131
|
-
|
|
111
|
+
Start with `node -r securenow/register app.mjs`. Only Hono served by `@hono/node-server` is covered (not `Bun.serve`, Deno or Workers). `http.route` and handler exceptions (5xx, with their stack) on the request span **(8.25.0+)**; before, spans show the raw path. Tested on Hono 4.
|
|
132
112
|
|
|
133
|
-
|
|
134
|
-
node -r securenow/register app.js
|
|
135
|
-
```
|
|
113
|
+
### 3.7 Polka
|
|
136
114
|
|
|
137
|
-
|
|
115
|
+
Start with the preload. Polka has no body parser: add one for body capture. Pass errors to `next(err)`; a thrown error crashes Polka. `http.route` and `next(err)` exceptions on the span **(8.25.0+)**. Tested on Polka 0.5.
|
|
138
116
|
|
|
139
|
-
|
|
140
|
-
npx securenow run app.js
|
|
141
|
-
```
|
|
117
|
+
### 3.8 Feathers
|
|
142
118
|
|
|
143
|
-
**
|
|
119
|
+
Feathers 5 with the Express (`@feathersjs/express`) or Koa transport: start with the preload. `http.route` and 5xx service errors on the span **(8.25.0+)**. Tested on Feathers 5.0.
|
|
144
120
|
|
|
145
|
-
|
|
146
|
-
// ecosystem.config.cjs
|
|
147
|
-
module.exports = {
|
|
148
|
-
apps: [{
|
|
149
|
-
name: 'my-app',
|
|
150
|
-
script: './app.js',
|
|
151
|
-
instances: 4,
|
|
152
|
-
// Absolute path: in cluster mode Node resolves a bare '-r securenow/register' from the PM2
|
|
153
|
-
// daemon's folder, and workers started by `pm2 resurrect` crash with "Cannot find module".
|
|
154
|
-
node_args: ['-r', require.resolve('securenow/register')],
|
|
155
|
-
// Reads .securenow/runtime.json from the project root (pm_cwd), wherever the daemon runs.
|
|
156
|
-
}],
|
|
157
|
-
};
|
|
158
|
-
```
|
|
121
|
+
### 3.9 h3
|
|
159
122
|
|
|
160
|
-
|
|
123
|
+
```js
|
|
124
|
+
const http = require('http');
|
|
125
|
+
const { createApp, createRouter, defineEventHandler, toNodeListener } = require('h3');
|
|
161
126
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
127
|
+
const app = createApp();
|
|
128
|
+
const router = createRouter().get('/users/:id', defineEventHandler((event) => ({ id: event.context.params.id })));
|
|
129
|
+
app.use(router);
|
|
130
|
+
http.createServer(toNodeListener(app)).listen(3000);
|
|
165
131
|
```
|
|
166
132
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
### Next.js
|
|
133
|
+
Start with the preload. `http.route` and handler exceptions on the span **(8.25.0+)**. Tested on h3 1.15. Nitro inside Nuxt: section 3.14.
|
|
170
134
|
|
|
171
|
-
|
|
135
|
+
### 3.10 Raw `http`, `https`, `http2`
|
|
172
136
|
|
|
173
|
-
|
|
137
|
+
Start with the preload. `http.createServer`, `https.createServer` and the HTTP/2 compatibility API (the `'request'` event) are covered; an HTTP/2 server driven only by raw `'stream'` events is not. Spans carry the raw path (no route templates).
|
|
174
138
|
|
|
175
|
-
|
|
176
|
-
const nextConfig = {
|
|
177
|
-
serverExternalPackages: ['securenow'],
|
|
178
|
-
outputFileTracingIncludes: {
|
|
179
|
-
'/*': ['./node_modules/securenow/**/*'],
|
|
180
|
-
},
|
|
181
|
-
};
|
|
139
|
+
### 3.11 Other servers on `node:http`
|
|
182
140
|
|
|
183
|
-
|
|
184
|
-
```
|
|
141
|
+
Any framework that ends in a `node:http` server gets traces, capture and the firewall from the preload. Only the frameworks above are tested, and others show the raw path instead of a route template.
|
|
185
142
|
|
|
186
|
-
|
|
143
|
+
### 3.12 Next.js 15 and 16 (App Router and Pages Router)
|
|
187
144
|
|
|
188
|
-
|
|
145
|
+
Run `npx securenow init`. It writes `instrumentation.ts` (`.js` when there is no `tsconfig.json`):
|
|
189
146
|
|
|
190
|
-
```
|
|
147
|
+
```ts
|
|
191
148
|
export async function register() {
|
|
192
149
|
if (process.env.NEXT_RUNTIME === 'nodejs') {
|
|
193
150
|
try {
|
|
@@ -202,527 +159,693 @@ export async function register() {
|
|
|
202
159
|
}
|
|
203
160
|
```
|
|
204
161
|
|
|
205
|
-
`
|
|
206
|
-
|
|
207
|
-
```typescript
|
|
208
|
-
interface RegisterOptions {
|
|
209
|
-
serviceName?: string; // override credentials app key/name
|
|
210
|
-
endpoint?: string; // advanced OTLP endpoint override
|
|
211
|
-
noUuid?: boolean; // override credentials config.runtime.noUuid
|
|
212
|
-
captureBody?: boolean; // override credentials config.capture.body
|
|
213
|
-
}
|
|
214
|
-
```
|
|
162
|
+
and patches `next.config.*` (or creates `next.config.mjs`):
|
|
215
163
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
"app": {
|
|
223
|
-
"key": "<uuid>",
|
|
224
|
-
"name": "my-nextjs-app",
|
|
225
|
-
"instance": "https://ingest.securenow.ai"
|
|
164
|
+
```js
|
|
165
|
+
/** @type {import('next').NextConfig} */
|
|
166
|
+
const nextConfig = {
|
|
167
|
+
serverExternalPackages: ['securenow'],
|
|
168
|
+
outputFileTracingIncludes: {
|
|
169
|
+
'/*': ['./node_modules/securenow/**/*'],
|
|
226
170
|
},
|
|
227
|
-
|
|
228
|
-
"logging": { "enabled": true },
|
|
229
|
-
"capture": { "body": true, "multipart": true, "maxBodySize": 10240 },
|
|
230
|
-
"firewall": { "enabled": true }
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
```
|
|
234
|
-
|
|
235
|
-
Local development and production do not need `.env.local`; `npx securenow app connect` and `npx securenow init` keep `.securenow/runtime.json` filled with runtime credentials and secure defaults. For production, run `npx securenow credentials runtime --env production` and mount/copy the generated JSON as `.securenow/credentials.json` or `.securenow/credentials.production.json`.
|
|
236
|
-
|
|
237
|
-
#### Next.js Body Capture
|
|
238
|
-
|
|
239
|
-
**Option A — Auto-capture (recommended):**
|
|
240
|
-
|
|
241
|
-
Add to your `instrumentation.ts`:
|
|
242
|
-
|
|
243
|
-
```typescript
|
|
244
|
-
export async function register() {
|
|
245
|
-
if (process.env.NEXT_RUNTIME === 'nodejs') {
|
|
246
|
-
try {
|
|
247
|
-
const securenowNext = await import('securenow/nextjs');
|
|
248
|
-
const registerSecureNow = securenowNext.registerSecureNow || securenowNext.default?.registerSecureNow;
|
|
249
|
-
registerSecureNow({ captureBody: true });
|
|
250
|
-
await import('securenow/nextjs-auto-capture');
|
|
251
|
-
} catch (error) {
|
|
252
|
-
console.error('[securenow] SecureNow did not start; the app is running without it:', error);
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
```
|
|
257
|
-
|
|
258
|
-
**Option B — Middleware:**
|
|
171
|
+
};
|
|
259
172
|
|
|
260
|
-
|
|
261
|
-
// middleware.ts
|
|
262
|
-
export { middleware } from 'securenow/nextjs-middleware';
|
|
263
|
-
export const config = { matcher: ['/api/:path*'] };
|
|
173
|
+
export default nextConfig;
|
|
264
174
|
```
|
|
265
175
|
|
|
266
|
-
|
|
176
|
+
- Keep the imports inside the `nodejs` block, with no `webpackIgnore` comment and no early `return`. Next.js then drops them from the Edge build and traces `securenow` and its dependencies into `output: 'standalone'`. The `try/catch` keeps the app serving if SecureNow can't start.
|
|
177
|
+
- **`src/` layout:** Next.js loads the instrumentation file from the folder that holds `app/` or `pages/`. With `src/app` or `src/pages`, `init` writes `src/instrumentation.ts` **(8.25.0+; before, it wrote the project root, which Next.js 14 and 15 never load there)** and moves a root file it generated earlier. `--src`/`--root` force the folder, `--ts`/`--js` the language, and `--force` replaces an existing file (the old one is kept as `.bak`).
|
|
178
|
+
- Re-running `init` replaces an instrumentation file that `init` generated before 8.24.0 (its `webpackIgnore` imports left OpenTelemetry out of standalone images and every request returned 500). A file you edited is left alone and `init` prints the merge prompt.
|
|
179
|
+
- `registerSecureNow(options?)` accepts `{ captureBody?, serviceName?, endpoint?, environment?, noUuid? }`. Leave `serviceName` and `noUuid` alone: the dashboard finds the app by `service.name`, which must equal the app key.
|
|
180
|
+
- Captured: HTTP server spans with client IP, redacted headers and bodies (JSON, form, GraphQL, multipart metadata), console logs, the firewall, rate limits and challenges. `http.route` on the HTTP span **(8.25.0+)**; before, Next.js 16 put it on Next's own span and Next.js 15 recorded `next.route`.
|
|
181
|
+
- The Edge runtime is not instrumented: `middleware.ts` on the Edge and routes with `export const runtime = 'edge'` get nothing. Node.js route handlers, server components and API routes are covered.
|
|
182
|
+
- Works with `next start` and `output: 'standalone'`, built with Turbopack or webpack. Standalone and Vercel: section 6.
|
|
183
|
+
- You don't need `securenow/nextjs-middleware` or `securenow/nextjs-wrapper`: since 8.24.0 bodies are captured at the HTTP layer, and `securenow/nextjs-auto-capture` (imported above) is only a fallback.
|
|
267
184
|
|
|
268
|
-
|
|
269
|
-
import { withSecureNow } from 'securenow/nextjs-wrapper';
|
|
185
|
+
### 3.13 Next.js 14 (8.25.0+)
|
|
270
186
|
|
|
271
|
-
|
|
272
|
-
// handler
|
|
273
|
-
});
|
|
274
|
-
```
|
|
187
|
+
`npx securenow init` writes the same instrumentation file. A new config is:
|
|
275
188
|
|
|
276
|
-
|
|
189
|
+
```js
|
|
190
|
+
/** @type {import('next').NextConfig} */
|
|
191
|
+
const nextConfig = {
|
|
192
|
+
experimental: {
|
|
193
|
+
instrumentationHook: true,
|
|
194
|
+
serverComponentsExternalPackages: ['securenow'],
|
|
195
|
+
outputFileTracingIncludes: {
|
|
196
|
+
'/*': ['./node_modules/securenow/**/*'],
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
};
|
|
277
200
|
|
|
278
|
-
|
|
279
|
-
npx securenow app connect
|
|
280
|
-
npx securenow init
|
|
201
|
+
export default nextConfig;
|
|
281
202
|
```
|
|
282
203
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
---
|
|
204
|
+
With an existing config, `init` prints these exact edits (Next.js 14 keeps the three keys under `experimental`). Tested on Next.js 14.2 with `next start` and standalone output.
|
|
286
205
|
|
|
287
|
-
### Nuxt 3 and 4
|
|
206
|
+
### 3.14 Nuxt 3 and 4
|
|
288
207
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
```typescript
|
|
208
|
+
```ts
|
|
209
|
+
// nuxt.config.ts
|
|
292
210
|
export default defineNuxtConfig({
|
|
293
211
|
modules: ['securenow/nuxt'],
|
|
294
212
|
});
|
|
295
213
|
```
|
|
296
214
|
|
|
297
|
-
|
|
215
|
+
Then run `npx securenow init` to fill `.securenow/runtime.json`.
|
|
298
216
|
|
|
299
|
-
|
|
217
|
+
- The module keeps `securenow` external (Nitro copies it, with its dependencies, into `.output/server/node_modules`) and adds a Nitro plugin that loads the same runtime as `-r securenow/register`, plus `http.route` from the matched route and handler errors as exceptions.
|
|
218
|
+
- No `securenow: { ... }` options in `nuxt.config`: they are ignored, with a build warning. Settings live in `.securenow/runtime.json`.
|
|
219
|
+
- Production: `node .output/server/index.mjs`, with `.securenow/credentials.json` in the folder you start it from, or `SECURENOW_CREDENTIALS`.
|
|
220
|
+
- `nuxt dev` is traced too. Its requests come through Nuxt's dev proxy: the client IP is known from 8.25.0 (before, it was unknown, so IP blocking only applied to the built server).
|
|
221
|
+
- Without the module: `node -r securenow/register .output/server/index.mjs`.
|
|
222
|
+
- Tested on Nuxt 3.21 and 4.6 (module, `.output` copied on its own as in a Docker image, `nuxt dev`, `-r` preload).
|
|
300
223
|
|
|
301
|
-
### Vite
|
|
224
|
+
### 3.15 Vite
|
|
302
225
|
|
|
303
|
-
Instrument the Node process that serves the app
|
|
226
|
+
Browser telemetry is not sent to SecureNow (browser ingestion is disabled since 8.0, and `securenow/web-vite` sends nothing). Instrument the Node process that serves the app:
|
|
304
227
|
|
|
305
228
|
```bash
|
|
306
229
|
node -r securenow/register-vite node_modules/vite/bin/vite.js preview
|
|
230
|
+
node -r securenow/register server.js # a Vite SSR server (Express + Vite middleware, ...)
|
|
307
231
|
```
|
|
308
232
|
|
|
309
|
-
`securenow/register-vite` is the same runtime as `securenow/register`.
|
|
310
|
-
|
|
311
|
-
---
|
|
233
|
+
`securenow/register-vite` is the same runtime as `securenow/register`. A static build served by nginx or a CDN has no Node process to protect; wire SecureNow into its API server instead.
|
|
312
234
|
|
|
313
|
-
|
|
235
|
+
### 3.16 Bun
|
|
314
236
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
```
|
|
318
|
-
Layer 4: Cloud/Edge WAF → blocked at CDN (Cloudflare, AWS WAF, GCP Cloud Armor)
|
|
319
|
-
Layer 3: OS Firewall → kernel-level DROP (iptables/nftables)
|
|
320
|
-
Layer 2: TCP Socket → socket.destroy() before HTTP parsing
|
|
321
|
-
Layer 1: HTTP Handler → 403 JSON response (always active)
|
|
237
|
+
```bash
|
|
238
|
+
bun --preload securenow/register app.js
|
|
322
239
|
```
|
|
323
240
|
|
|
324
|
-
|
|
241
|
+
For apps served through `node:http` (tested: Express 5 on Bun 1.3). `Bun.serve` is not traced or protected.
|
|
325
242
|
|
|
326
|
-
|
|
327
|
-
# Zero-config runtime (recommended) - writes the key to .securenow/runtime.json
|
|
328
|
-
npx securenow app connect # pick/create app; firewall connects automatically
|
|
329
|
-
# or, if you already have a key:
|
|
330
|
-
npx securenow api-key set snk_live_abc123...
|
|
243
|
+
### 3.17 Firewall only
|
|
331
244
|
|
|
332
|
-
|
|
333
|
-
|
|
245
|
+
```bash
|
|
246
|
+
node -r securenow/firewall-only src/index.js
|
|
247
|
+
npx securenow run --firewall-only src/index.js
|
|
334
248
|
```
|
|
335
249
|
|
|
336
|
-
|
|
250
|
+
Blocklist, allowlist, scoped blocks, rate limits, challenges and, **(8.25.0+)**, the zero-code session guard. No traces, no logs, no OpenTelemetry. It needs the runtime API key like the full preload.
|
|
337
251
|
|
|
338
|
-
|
|
339
|
-
|
|
252
|
+
## 4. Configuration: `.securenow/runtime.json`
|
|
253
|
+
|
|
254
|
+
### File anatomy
|
|
340
255
|
|
|
341
256
|
```json
|
|
342
257
|
{
|
|
343
|
-
"
|
|
344
|
-
"
|
|
345
|
-
"
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
258
|
+
"apiKey": "snk_live_...",
|
|
259
|
+
"app": { "key": "<app-uuid>", "name": "my-api" },
|
|
260
|
+
"config": {
|
|
261
|
+
"runtime": { "deploymentEnvironment": "local" },
|
|
262
|
+
"logging": { "enabled": true },
|
|
263
|
+
"capture": { "body": true, "multipart": true, "maxBodySize": 10240, "sensitiveFields": [] },
|
|
264
|
+
"networking": { "trustedProxies": [] },
|
|
265
|
+
"firewall": { "failMode": "open" }
|
|
266
|
+
}
|
|
350
267
|
}
|
|
351
268
|
```
|
|
352
269
|
|
|
353
|
-
`
|
|
354
|
-
|
|
270
|
+
- `apiKey`: the app-scoped runtime key (scopes `traces:write`, `logs:write`, `firewall:read`, `blocklist:read`, `allowlist:read`). Without it the ingest accepts nothing and the firewall does not start. Rotate it with `npx securenow api-key create` or `npx securenow api-key set snk_live_...`.
|
|
271
|
+
- `app.key`: the app's UUID. It becomes `service.name`, which the dashboard matches exactly.
|
|
272
|
+
- `_securenow`: a schema version and an explanation for every key, written by `init`. Leave it.
|
|
273
|
+
- A key you leave out takes its default. `app.instance` in older files is ignored (use `config.otel.endpoint`).
|
|
274
|
+
|
|
275
|
+
### Where the SDK looks (first match wins, files are never merged)
|
|
276
|
+
|
|
277
|
+
1. `SECURENOW_CREDENTIALS`: the same JSON, raw or base64.
|
|
278
|
+
2. A project file. The SDK searches the working directory (and npm's `INIT_CWD`) and the entry script's folder, then each of their parent folders; under PM2 it searches the app folder PM2 recorded and the script's folder instead. In each folder the order is `.securenow/runtime.json`, `.securenow/credentials.json`, then `credentials.staging.json`, `credentials.production.json`, `credentials.preview.json`, `credentials.local.json`, `credentials.test.json`, `credentials.development.json`, `credentials.dev.json`, `credentials.prod.json`.
|
|
279
|
+
3. The same names in `~/.securenow/`.
|
|
280
|
+
|
|
281
|
+
So a `runtime.json` left on a server beats the `credentials.production.json` next to it, and `credentials.staging.json` beats `credentials.production.json`. The startup line says which source won: `credentials=/srv/app/.securenow/credentials.json` or `credentials=env:SECURENOW_CREDENTIALS` (printed by `-r securenow/register` and Nuxt; Next.js prints `service.name` and `Environment` lines instead).
|
|
282
|
+
|
|
283
|
+
### Every key (all under `config` except `apiKey` and `app`)
|
|
284
|
+
|
|
285
|
+
| Key | Default | Change it when |
|
|
286
|
+
|---|---|---|
|
|
287
|
+
| `logging.enabled` | `true` | You don't want `console.*` exported as logs (crash capture needs it on) |
|
|
288
|
+
| `logging.redact` | `true` | **(8.25.0+)** You need log lines exported exactly as written (redaction replaces credential fields and secret shapes with `[REDACTED]`) |
|
|
289
|
+
| `capture.body` | `true` | Bodies must never leave the process. Detections then lose body payloads |
|
|
290
|
+
| `capture.multipart` | `true` | You don't want multipart field values and file metadata (file content is never captured) |
|
|
291
|
+
| `capture.maxBodySize` | `10240` bytes (minimum 1024) | Your JSON bodies are larger: bigger bodies are recorded as `[TOO LARGE: <n> bytes]` |
|
|
292
|
+
| `capture.sensitiveFields` | `[]` | You have extra field names to redact (added to the built-ins; also applied to header names) |
|
|
293
|
+
| `otel.endpoint` | `null` = `https://ingest.securenow.ai` | Advanced: sending to your own OTLP collector (the SecureNow dashboard then sees nothing) |
|
|
294
|
+
| `otel.tracesEndpoint`, `otel.logsEndpoint` | `null` | Advanced: split collectors |
|
|
295
|
+
| `otel.headers` | `{}` | Extra OTLP headers. App key, auth and environment headers are added for you |
|
|
296
|
+
| `otel.logLevel` | `"error"` | `debug` while troubleshooting; `none` hides export errors |
|
|
297
|
+
| `otel.disableInstrumentations` | `[]` | You want to skip an instrumentation: full package names, e.g. `"@opentelemetry/instrumentation-dns"` |
|
|
298
|
+
| `otel.middlewareSpans` | `false` | You want one span per Express/Koa middleware per request (high volume, no detection uses them) |
|
|
299
|
+
| `runtime.deploymentEnvironment` | `"production"` (`init` writes `"local"`) | Every deployment: `production`, `staging`, `preview`, `local`, `test`... (`dev`/`development` become `local`, `prod` becomes `production`) |
|
|
300
|
+
| `runtime.noUuid` | `null` = automatic (one service name for every worker when `app.key` is set) | Never: `false` adds a per-process suffix and the dashboard no longer finds the app |
|
|
301
|
+
| `runtime.strict` | `false` | PM2/cluster workers should exit when no app identity resolves |
|
|
302
|
+
| `runtime.testSpan` | `false` | You want a `securenow.startup.smoke` span at every boot (`npx securenow test-span` is usually better) |
|
|
303
|
+
| `networking.trustedProxies` | `[]` | A load balancer, CDN or proxy with a public address sits in front of the app: IPv4/IPv6 addresses, CIDRs (IPv6 ranges 8.25.0+) or `"cloudflare"` |
|
|
304
|
+
| `firewall.failMode` | `"open"` | `"closed"`: every request gets 403 until the first sync succeeds |
|
|
305
|
+
| `firewall.statusCode` | `403` | You want another status for blocked requests |
|
|
306
|
+
| `firewall.log` | `true` | You want no firewall lines in the console (this also hides the startup `Firewall:` lines) |
|
|
307
|
+
| `firewall.tcp` | `false` | Layer 2: drop blocked clients at the TCP connection (direct connections only) |
|
|
308
|
+
| `firewall.iptables` | `false` | Layer 3: kernel DROP (Linux, root or `CAP_NET_ADMIN`) |
|
|
309
|
+
| `firewall.cloud` | `null` | Layer 4: `"cloudflare"`, `"aws"` or `"gcp"` |
|
|
310
|
+
| `firewall.cloudDryRun` | `false` | Log the WAF pushes without applying them |
|
|
311
|
+
| `firewall.cloudflare.apiToken`, `.accountId` | `null` | Layer 4 on Cloudflare (secret) |
|
|
312
|
+
| `firewall.aws.wafIpSetId`, `.wafIpSetName`, `.wafScope` | `null`, `"securenow-blocklist"`, `"REGIONAL"` | Layer 4 on AWS WAF (`npm i @aws-sdk/client-wafv2`; AWS credentials from the standard chain) |
|
|
313
|
+
| `firewall.gcp.projectId`, `.securityPolicy` | `null` | Layer 4 on Cloud Armor (`npm i @google-cloud/compute`) |
|
|
314
|
+
| `firewall.rateLimitStore` | `null` (each process counts alone) | Several workers or servers must share rate-limit counters: `"redis://host:6379"` or `rediss://...` |
|
|
315
|
+
| `firewall.versionCheckInterval` | `10` seconds | `1`–`2` for near-instant propagation of blocks (unchanged checks are a cheap 304) |
|
|
316
|
+
| `firewall.syncInterval` | `3600` seconds | Safety-net full resync; rarely changed |
|
|
317
|
+
| `firewall.apiUrl` | SecureNow | Leave unset |
|
|
318
|
+
| `firewall.enabled` | ignored | Deprecated. Turn the firewall on or off per app and environment with `npx securenow firewall enable` / `disable` |
|
|
319
|
+
| `sessions.guard` **(8.25.0+)** | `false` | Revoked sessions should get a 401 in any framework, with no code |
|
|
320
|
+
| `sessions.capture` **(8.25.0+)** | `false` | Requests with a session should send `session.seen` for session-theft detection |
|
|
321
|
+
| `sessions.cookieNames` **(8.25.0+)** | `[]` | Your session cookie is not one of the defaults (section 5.10) |
|
|
322
|
+
|
|
323
|
+
### Environment variables
|
|
324
|
+
|
|
325
|
+
- `SECURENOW_CREDENTIALS`: the runtime credentials JSON, raw or base64, for hosts that can't mount a file. It wins over every file. `npx securenow credentials runtime --env production --format env` prints `SECURENOW_CREDENTIALS=<base64>`. A value that isn't credentials JSON is ignored with a warning.
|
|
326
|
+
- `SECURENOW_DISABLED=1` (or `true`, `yes`, `on`): the SDK stays off in that process, with no traces, logs, events, firewall or session sync; `guard()` just calls `next()`. It prints `[securenow] disabled by SECURENOW_DISABLED (register not started)`. For test runs: `SECURENOW_DISABLED=1 npm test`.
|
|
327
|
+
- Nothing else is read by SecureNow. OpenTelemetry's own `OTEL_*` variables still reach the OpenTelemetry SDK underneath, and you need none of them. SecureNow sets `OTEL_METRICS_EXPORTER=none` when it is unset, so no metrics reader retries `localhost:4318`.
|
|
328
|
+
|
|
329
|
+
## 5. Features
|
|
330
|
+
|
|
331
|
+
### 5.1 Traces
|
|
332
|
+
|
|
333
|
+
- **What:** one server span per request with method, route (`http.route` where section 3 says so), status code, client IP (`http.client_ip`, `http.client_ip.source`, `http.socket_ip`, `http.proxy.trusted`), redacted headers and body, plus child spans for outgoing HTTP and databases (Postgres, MySQL, MongoDB, Redis...). `service.name` is the app key; `deployment.environment` comes from the config.
|
|
334
|
+
- **On:** automatic once wired. To skip one instrumentation, list its package name in `config.otel.disableInstrumentations`.
|
|
335
|
+
- **Verify:** `npx securenow test-span` prints `Span accepted (HTTP 200)`. Send one request to the app, then `npx securenow traces --env <env> --limit 5` and `npx securenow traces show <traceId> --env <env>`.
|
|
336
|
+
|
|
337
|
+
### 5.2 Logs
|
|
338
|
+
|
|
339
|
+
- **What:** `console.log`, `info`, `warn`, `error` and `debug` become log records (INFO, INFO, WARN, ERROR, DEBUG) linked to the active trace, and still print to the terminal. In code:
|
|
340
|
+
|
|
341
|
+
```js
|
|
342
|
+
const { getLogger } = require('securenow/tracing');
|
|
343
|
+
const logger = getLogger('billing', '1.0.0'); // null when logging is disabled
|
|
344
|
+
logger?.emit({ severityNumber: 13, severityText: 'WARN', body: 'Card declined', attributes: { orderId: 'o_123' } });
|
|
345
|
+
```
|
|
355
346
|
|
|
356
|
-
|
|
347
|
+
**Redacted before export (8.25.0+):** credential fields (password, token, apiKey, authorization, cookie, OTP codes, card numbers...) in logged objects, JSON and `key=value` text, and secret shapes anywhere in the line (JWTs, Bearer/Basic credentials, `sk_`/`pk_`/`rk_`/`snk_` keys, card numbers, emails) become `[REDACTED]`. The terminal still shows the line as written, and attributes you pass to `getLogger().emit()` are exported as set. `config.logging.redact: false` turns it off. Redaction goes by field names and value shapes, so a secret in free text ("the new password is hunter2") still goes out: don't log secrets. Before 8.25.0, lines were exported as written.
|
|
348
|
+
- **On:** `config.logging.enabled` (default `true`); redaction `config.logging.redact` (default `true`).
|
|
349
|
+
- **Verify:** the startup line `Logging: ENABLED → https://ingest.securenow.ai/v1/logs`; `npx securenow logs --env <env> --since 15m`; `npx securenow logs errors --since 1h` (failures grouped by message). From a shell or CI: `npx securenow log send "Deploy done" --level info --attrs version=1.2.3`.
|
|
350
|
+
|
|
351
|
+
### 5.3 Header and body capture, redaction
|
|
352
|
+
|
|
353
|
+
- **What:**
|
|
354
|
+
- `http.request.headers`: JSON of the request headers, each value cut at 500 characters, dropped if the whole set exceeds 8 KB.
|
|
355
|
+
- `http.request.body` for `application/json`, `application/x-www-form-urlencoded` and `application/graphql`, up to `maxBodySize`; a larger body is recorded as `[TOO LARGE: <n> bytes]`. Other content types (text, XML, binary) are not captured.
|
|
356
|
+
- `multipart/form-data` as metadata: text fields (up to 1000 characters each, 100 parts) and files as field, name, type and size, never their content.
|
|
357
|
+
- The body is observed while the app reads it, never consumed. A route that never reads its body (no body parser) records none.
|
|
358
|
+
- **Redaction**, before anything leaves the process:
|
|
359
|
+
- Header names: `cookie`, `set-cookie`, `authorization`, `proxy-authorization`, any name containing `token`, `secret`, `password`, `passwd`, `csrf`, `xsrf`, `session`, `api-key`, `apikey`, `api_key`, `credential`, `signature`, `private-key` or `x-auth`, and OTP names.
|
|
360
|
+
- Body field names, case-insensitive substring match (68 built-in names; the startup line counts them): passwords, secrets, tokens, `auth`, `authorization`, `bearer`, credentials, session ids, cookies, card, CVV, IBAN, account, routing and sort codes, tax ids, SSN, PIN, email, phone, mobile, date of birth, first/last/full name, address, postcode, zip, passport, license. OTP names (`code`, `otp`, `otpCode`, `oneTimeCode`, `verificationCode`, `passcode`, `mfaCode`, `totpCode`, `recoveryCode`, `backupCode`...) match exactly, so `status_code` and `error_code` stay readable.
|
|
361
|
+
- Values that look like secrets, under any field name: JWTs, `Bearer`/`Basic` credentials, `sk_`/`pk_`/`rk_`/`snk_` live and test keys, email addresses, card numbers that pass the Luhn check.
|
|
362
|
+
- URL query parameters with OTP names. Other query values are kept (attack payloads in URLs are what detections read), so keep secrets out of URLs.
|
|
363
|
+
- **On:** on by default. Tune `config.capture.body`, `multipart`, `maxBodySize` and `sensitiveFields` (extra names; they apply to header names too, ignoring case and punctuation).
|
|
364
|
+
- **Verify:** `npx securenow redact '{"email":"a@b.co","password":"x","otpCode":"123456","status_code":200}'` shows what is kept; after a real POST, `npx securenow traces show <traceId>` shows the redacted `http.request.body`.
|
|
365
|
+
|
|
366
|
+
### 5.4 Client IP and trusted proxies
|
|
367
|
+
|
|
368
|
+
- **What:** the client IP is the connection's address unless the connection comes from a trusted proxy. Then it is the right-most untrusted address in `X-Forwarded-For`, or else `CF-Connecting-IP`, `True-Client-IP`, `X-Real-IP` or `X-Client-IP`. Trusted by default: private ranges (`10/8`, `172.16/12`, `192.168/16`, `100.64/10`, `169.254/16`, `fc00::/7`), loopback, the host's own addresses and, **(8.25.0+)**, Unix-socket and named-pipe connections (nginx `proxy_pass http://unix:/run/app.sock`, Nuxt's dev proxy). A client can't choose its IP through `X-Forwarded-For`. The firewall, rate limits, `session.seen` and traces all use this IP.
|
|
369
|
+
- **On:** `config.networking.trustedProxies` takes exact IPv4/IPv6 addresses, IPv4 CIDRs, IPv6 CIDRs **(8.25.0+)**, and `"cloudflare"` (Cloudflare's published edge ranges, IPv6 ones included from 8.25.0):
|
|
370
|
+
|
|
371
|
+
```json
|
|
372
|
+
{ "config": { "networking": { "trustedProxies": ["cloudflare", "203.0.113.10", "198.51.100.0/24"] } } }
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
The proxy must forward the visitor, for nginx: `proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;`.
|
|
376
|
+
- **Verify:** in `npx securenow traces show <traceId>`, `http.client_ip` is the visitor and `http.client_ip.source` is `x-forwarded-for` (or `cf-connecting-ip`...), not `socket` with the proxy's address. `npx securenow protect status` says whether the app sees real client IPs: blocking mode is refused while more than 20% of its traffic comes through a proxy it doesn't trust.
|
|
377
|
+
|
|
378
|
+
### 5.5 Firewall (layers 1 to 4)
|
|
379
|
+
|
|
380
|
+
- **What:** syncs the app's blocklist (yours plus SecureNow's known-bad IP feed, refreshed hourly), allowlist, scoped blocks, rate limits and challenges. It checks a version every `versionCheckInterval` (10 s, ETag/304) and resyncs fully every `syncInterval` (3600 s); matching happens in memory. It fails open by default, and a circuit breaker pauses polling for 2 minutes after 5 consecutive API errors. Each request goes through: allowlist (when one is active, an unlisted IP is blocked and a listed IP skips every other check), then blocklist, scoped blocks (route, method), challenge, rate limit.
|
|
381
|
+
|
|
382
|
+
| Layer | Turn on | What it does | Limits |
|
|
383
|
+
|---|---|---|---|
|
|
384
|
+
| 1 HTTP | always, once the runtime API key is present | 403 JSON `{"error":"Forbidden","ip":"..."}`, or an HTML page with the masked IP for browsers; status from `firewall.statusCode` | Works behind proxies (forwarding headers) |
|
|
385
|
+
| 2 TCP | `"tcp": true` | Destroys the socket before HTTP parsing, zero bytes sent | Sees only the connection's source IP; connections from trusted proxies are left to layer 1. Use 8.25.0+: before, it hooked `listen()` and missed the servers most apps start before the SDK has finished starting |
|
|
386
|
+
| 3 OS | `"iptables": true` | Kernel DROP: a `SECURENOW_BLOCK` chain in iptables (ip6tables for IPv6), or a `securenow` nft table where iptables is missing **(8.25.0+)** | Linux, root or `CAP_NET_ADMIN`, `iptables`/`nft` installed; direct client connections only. Use 8.25.0+: it validates every entry, runs without a shell and applies each sync as one batch, and an IPv6 entry no longer stops the sync |
|
|
387
|
+
| 4 Cloud WAF | `"cloud": "cloudflare"`, `"aws"` or `"gcp"` | Pushes the global blocklist to the edge, at most every 30 s | Cloudflare: fills an account IP list `securenow-blocklist` that your own WAF rule must reference. AWS: WAFv2 IP set. GCP: Cloud Armor rules at priority 2000, `deny(403)`. Use 8.25.0+ (IPv6 ranges, Cloud Armor's 10-range limit, stale rules fixed). Request format tested, not run against real accounts |
|
|
388
|
+
|
|
389
|
+
Layers 2 to 4 carry the global blocklist. Scoped blocks, rate limits and challenges are layer 1 only.
|
|
390
|
+
- **On:** automatic with the runtime API key. Each app and environment also has a remote switch, `npx securenow firewall enable|disable --app <key> --env production`, applied without a deploy. `config.firewall.enabled` is ignored.
|
|
391
|
+
- **Verify:** startup lines `Firewall: ENABLED`, `Firewall: environment=<env>`, `Firewall: Layer 1 (HTTP 403) active` and `Firewall: synced <n> global blocked IPs (<x> exact + <y> CIDR ranges) and <z> scoped block rules`. Then `npx securenow firewall status --env <env>`, `npx securenow firewall test-ip 203.0.113.7 --path /admin --env <env>` and `npx securenow firewall hits --since 1h`.
|
|
392
|
+
|
|
393
|
+
### 5.6 Rate limits
|
|
394
|
+
|
|
395
|
+
- **What:** 429 with `Retry-After`, `X-RateLimit-Limit`, `X-RateLimit-Window` and `X-SecureNow-Rate-Limit-Rule` (JSON `{"error":"Too Many Requests","ip":"...","retryAfter":<s>}`, or a small HTML page for browsers). A rule matches a route (exact, prefix or regex), a method, optionally an IP or CIDR, an app and an environment, and counts per IP, globally, per request-header value (API key, tenant) or per cookie value (session); header and cookie values are hashed, and requests without them count per IP. Rules live in SecureNow, not in your code; running apps apply them within about 10 seconds.
|
|
396
|
+
- **On:** create from the CLI, MCP or dashboard:
|
|
397
|
+
|
|
398
|
+
```bash
|
|
399
|
+
npx securenow ratelimit add --route /api/login --method POST --limit 5 --window 1m --env production --reason "login brute force"
|
|
400
|
+
npx securenow ratelimit add --route /api/ --limit 100 --window 1m --header x-api-key --env production # per API key
|
|
401
|
+
npx securenow ratelimit add --route /app --limit 300 --window 1m --cookie session --env production # per session cookie
|
|
402
|
+
npx securenow ratelimit from-text "5 per minute per IP on POST /api/login for 2h" --yes
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
Counters live in each process. With PM2 cluster or several servers, each process counts on its own (the effective limit is multiplied) unless they share Redis: `"config": { "firewall": { "rateLimitStore": "redis://your-redis:6379" } }` (`rediss://`, `user:password@` and `/db` work). The firewall never waits on Redis; at most a request or two per process can slip past while a reply is in flight, and it falls back to in-process counting if Redis is unreachable.
|
|
406
|
+
- **Verify:** `npx securenow ratelimit test 203.0.113.42 --path /api/login --method POST --env production`; startup line `Firewall: synced <n> rate-limit rules` (and `shared rate-limit counters: connecting to ...` with Redis); `npx securenow firewall hits --action rate_limited --since 1h`. Rules are per environment and the CLI defaults to `production`: to try one against a local app, create it with `--env local`.
|
|
407
|
+
|
|
408
|
+
### 5.7 Challenges (proof of work)
|
|
409
|
+
|
|
410
|
+
- **What:** a softer step than a block. Browsers (`Accept: text/html`) get a 403 interstitial that solves a proof of work by itself (14 bits by default, about a second or less) and receives a pass cookie bound to the IP and user agent, valid 30 minutes by default. Clients that don't accept HTML get a 403 JSON `{"error":"challenge_required","challenge":{...}}` with the header `X-SecureNow-Challenge: required`, so don't challenge API routes that non-browser clients call. A challenge can escalate to a block after repeated failures.
|
|
411
|
+
- **On:**
|
|
412
|
+
|
|
413
|
+
```bash
|
|
414
|
+
npx securenow challenge add --route /login --clearance 30m --env production
|
|
415
|
+
npx securenow challenge add 203.0.113.0/24 --route /signup --difficulty 16 --escalate-to-block --fail-threshold 10 --block-ttl-hours 24
|
|
416
|
+
```
|
|
357
417
|
|
|
358
|
-
|
|
359
|
-
node -r securenow/firewall-only app.js
|
|
418
|
+
- **Verify:** `npx securenow challenge test 203.0.113.9 --path /login --method GET --env production`; `npx securenow firewall hits --action captcha_challenged --since 1h`.
|
|
360
419
|
|
|
361
|
-
|
|
362
|
-
securenow run --firewall-only app.js
|
|
363
|
-
```
|
|
420
|
+
### 5.8 Blocklist, allowlist, trusted IPs
|
|
364
421
|
|
|
365
|
-
|
|
422
|
+
- **Blocklist:** block an IP address or CIDR range, IPv4 or IPv6, everywhere or on a route, optionally with an expiry (IPv6 ranges are enforced by securenow 8.25.0+). An unblock stops enforcement and keeps the history.
|
|
366
423
|
|
|
367
|
-
|
|
424
|
+
```bash
|
|
425
|
+
npx securenow blocklist add 203.0.113.42 --env production --duration 24h --reason "credential stuffing"
|
|
426
|
+
npx securenow blocklist add 203.0.113.42 --route /admin --mode prefix --method ALL --reason "admin probing"
|
|
427
|
+
npx securenow blocklist unblock <id> --reason "false alarm after review"
|
|
428
|
+
```
|
|
368
429
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
...appConfig.resolveFirewallOptions(),
|
|
375
|
-
apiUrl: 'https://ingest.securenow.ai',
|
|
376
|
-
syncInterval: 3600, // safety-net full sync every hour
|
|
377
|
-
versionCheckInterval: 10, // lightweight ETag check every 10s
|
|
378
|
-
failMode: 'open', // 'open' or 'closed'
|
|
379
|
-
statusCode: 403,
|
|
380
|
-
log: true,
|
|
381
|
-
tcp: false,
|
|
382
|
-
iptables: false,
|
|
383
|
-
cloud: null, // 'cloudflare' | 'aws' | 'gcp'
|
|
384
|
-
});
|
|
430
|
+
- **Allowlist (dangerous):** deny-by-default. Once any active entry exists for an app and environment, only listed IPs get through and everyone else gets 403. Listed IPs also skip the blocklist, rate limits and challenges. Use it only to lock an app to known IPs, with the person's approval: `npx securenow allowlist add <ip> --app <key> --env production --label "Office" --reason "..."`.
|
|
431
|
+
- **Trusted IPs:** known-safe infrastructure (uptime monitors, office VPN, your own servers). Alerts on them are suppressed; they restrict nobody: `npx securenow trusted add <ip> --label "Uptime monitor"`.
|
|
432
|
+
- **Verify:** `npx securenow blocklist list --env production`, `npx securenow firewall test-ip <ip> --env production`, `npx securenow firewall hits --ip <ip>`. Blocks reach running apps within about 10 seconds.
|
|
433
|
+
|
|
434
|
+
### 5.9 Custom security events: `track()`
|
|
385
435
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
436
|
+
- **What:** your app reports what only it knows (sign-ins, OTPs, password resets, MFA). Events feed brute-force, credential-stuffing and account-takeover detection, and alert rules can query them (`attributes_string['event.type']`, `['enduser.id']`, `['session.id']`, `['http.client_ip']`). `track()` is fire-and-forget: it buffers (1000 events at most, oldest dropped), sends batches of up to 100 every 2 seconds and never throws. It uses the runtime credentials, so it also works in a script without the preload; `flush()` returns a promise for scripts that exit right away.
|
|
437
|
+
|
|
438
|
+
```js
|
|
439
|
+
const { track } = require('securenow/events'); // ESM: import { track } from 'securenow/events';
|
|
440
|
+
track('auth.login.failure', { userId, sessionId, ip, userAgent, severity: 'warn', attributes: { reason: 'bad_password' } });
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
Properties: `userId`, `sessionId`, `ip`, `userAgent`, `severity` (`trace`, `debug`, `info`, `warn`, `error`, `fatal`), `ts` (epoch ms), `attributes` (strings, numbers, booleans).
|
|
444
|
+
|
|
445
|
+
- **Recommended auth events:**
|
|
446
|
+
|
|
447
|
+
| Event | Emit when |
|
|
448
|
+
|---|---|
|
|
449
|
+
| `auth.login.failure` | A sign-in fails (unknown user, wrong password) |
|
|
450
|
+
| `auth.login.success` | A sign-in succeeds |
|
|
451
|
+
| `auth.otp.send.requested` | Someone asks for a one-time code or magic link |
|
|
452
|
+
| `auth.otp.send.result` | The SMS/email provider accepted or refused the send |
|
|
453
|
+
| `auth.otp.verify.failure` | A wrong or expired code is entered |
|
|
454
|
+
| `auth.otp.verify.success` | A code is accepted |
|
|
455
|
+
| `auth.password_reset.requested` | A reset is requested |
|
|
456
|
+
| `auth.password_reset.completed` | A password is changed through a reset |
|
|
457
|
+
| `auth.mfa.challenge.failure` | A second factor fails |
|
|
458
|
+
| `auth.mfa.challenge.success` | A second factor succeeds |
|
|
459
|
+
| `auth.session.revoked` | Your app signs a user out everywhere or kills a session |
|
|
389
460
|
|
|
390
|
-
|
|
391
|
-
```
|
|
461
|
+
- **Login route example (Express):**
|
|
392
462
|
|
|
393
|
-
|
|
463
|
+
```js
|
|
464
|
+
const { track } = require('securenow/events');
|
|
465
|
+
const { resolveClientIp } = require('securenow/resolve-ip');
|
|
394
466
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
}
|
|
467
|
+
app.post('/api/login', async (req, res) => {
|
|
468
|
+
const ip = resolveClientIp(req); // the same trusted-proxy rules as the firewall
|
|
469
|
+
const userAgent = req.get('user-agent');
|
|
470
|
+
const user = await findUserByEmail(req.body.email);
|
|
471
|
+
if (!user || !(await verifyPassword(user, req.body.password))) {
|
|
472
|
+
track('auth.login.failure', {
|
|
473
|
+
userId: user ? user.id : undefined, // your internal id, never the password
|
|
474
|
+
ip, userAgent, severity: 'warn',
|
|
475
|
+
attributes: { reason: user ? 'bad_password' : 'unknown_user', method: 'password' },
|
|
476
|
+
});
|
|
477
|
+
return res.status(401).json({ error: 'Invalid email or password' });
|
|
405
478
|
}
|
|
406
|
-
|
|
407
|
-
}
|
|
408
|
-
|
|
479
|
+
const session = await createSession(user);
|
|
480
|
+
track('auth.login.success', { userId: user.id, ip, userAgent, attributes: { method: 'password' } });
|
|
481
|
+
res.json({ ok: true });
|
|
482
|
+
});
|
|
483
|
+
```
|
|
409
484
|
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
{
|
|
413
|
-
"config": {
|
|
414
|
-
"firewall": {
|
|
415
|
-
"cloud": "aws",
|
|
416
|
-
"aws": {
|
|
417
|
-
"wafIpSetId": "your-ip-set-id",
|
|
418
|
-
"wafIpSetName": "your-ip-set-name",
|
|
419
|
-
"wafScope": "REGIONAL"
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
```
|
|
425
|
-
AWS authentication should use the standard AWS credential chain outside the
|
|
426
|
-
SecureNow SDK config.
|
|
427
|
-
Requires peer dep: `npm install @aws-sdk/client-wafv2`
|
|
485
|
+
If you also send `sessionId` and want it to match revocations made through the session guard, send what the guard computes, `require('securenow/sessions').sha256(<session cookie value exactly as sent>)`, or give the guard your own `getSessionId` (section 5.10).
|
|
486
|
+
- **Verify:** `npx securenow telemetry fields --since 1h --json` lists the custom event types observed for the app. From a shell or a non-JS service: `npx securenow event send auth.login.failure --user u_test --ip 203.0.113.10 --attrs reason=test` (add `--env local` to tag it local).
|
|
428
487
|
|
|
429
|
-
|
|
430
|
-
```json
|
|
431
|
-
{
|
|
432
|
-
"config": {
|
|
433
|
-
"firewall": {
|
|
434
|
-
"cloud": "gcp",
|
|
435
|
-
"gcp": {
|
|
436
|
-
"projectId": "your-project",
|
|
437
|
-
"securityPolicy": "your-policy-name"
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
```
|
|
443
|
-
Requires peer dep: `npm install @google-cloud/compute`
|
|
488
|
+
### 5.10 Session revocation
|
|
444
489
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
}
|
|
453
|
-
}
|
|
490
|
+
Revoke a stolen session or every session of a user, from the CLI, MCP-capable automation or the dashboard; the SDK pulls active revocations every 30 seconds (outbound, ETag-cached) and rejects matching requests locally. Fail-open: an outage never blocks sign-ins.
|
|
491
|
+
|
|
492
|
+
```bash
|
|
493
|
+
npx securenow revoke session <session-id> --reason "impossible travel" --duration 24h --app <key>
|
|
494
|
+
npx securenow revoke user <user-id> --reason "account takeover" # default expiry 7d
|
|
495
|
+
npx securenow revoke list
|
|
496
|
+
npx securenow revoke restore <revocation-id> --reason "verified with the user"
|
|
454
497
|
```
|
|
455
498
|
|
|
456
|
-
|
|
499
|
+
The session id is what the SDK computes: the SHA-256 of the Bearer token, or else of the first session cookie found. Cookies looked up, in order: your `sessions.cookieNames` **(8.25.0+)**, then `__Secure-next-auth.session-token`, `__Host-next-auth.session-token`, `next-auth.session-token`, `connect.sid`, `session`, `sid`. The user id is the `sub` claim of a JWT Bearer token. To get the id of one session for a test: `node -e "console.log(require('securenow/sessions').sha256(process.argv[1]))" '<cookie value>'`.
|
|
457
500
|
|
|
458
|
-
|
|
501
|
+
Pick one way to enforce:
|
|
459
502
|
|
|
460
|
-
|
|
503
|
+
1. **Zero code, every framework (8.25.0+).** In `.securenow/runtime.json`:
|
|
461
504
|
|
|
462
|
-
|
|
505
|
+
```json
|
|
506
|
+
{ "config": { "sessions": { "guard": true, "capture": true, "cookieNames": ["my_app_session"] } } }
|
|
507
|
+
```
|
|
463
508
|
|
|
464
|
-
|
|
465
|
-
timestamp types, map access, tenant scope, automatic fields, custom-event mapping, client-IP
|
|
466
|
-
expression, and safe SQL templates:
|
|
509
|
+
`guard` answers a revoked session or user with 401 (body `Session revoked`) and clears the session cookies; `capture` sends `session.seen`, deduplicated per session and IP every 5 minutes, for session-theft detection (impossible travel, concurrent networks). It runs in the firewall's request hook, so it needs the runtime API key, and it works with `-r securenow/register`, Next.js, Nuxt and `securenow/firewall-only`.
|
|
467
510
|
|
|
468
|
-
|
|
469
|
-
const telemetrySchema = require('securenow/telemetry-schema');
|
|
470
|
-
```
|
|
511
|
+
2. **Express-style middleware: `guard()`** (Express, Connect, Polka, NestJS on the Express adapter, Feathers with Express):
|
|
471
512
|
|
|
472
|
-
|
|
513
|
+
```js
|
|
514
|
+
const sessions = require('securenow/sessions');
|
|
515
|
+
app.use(sessions.guard()); // default extraction above
|
|
516
|
+
// or with your own ids (after your session middleware):
|
|
517
|
+
app.use(sessions.guard({ getSessionId: (req) => req.session?.id, getUserId: (req) => req.user?.id }));
|
|
518
|
+
```
|
|
473
519
|
|
|
474
|
-
|
|
475
|
-
securenow telemetry schema --json
|
|
476
|
-
securenow telemetry fields --app <key> --env production --since 24h --json
|
|
477
|
-
securenow alerts rules validate --sql @rule.sql --app <key>
|
|
478
|
-
```
|
|
520
|
+
A revoked request gets 401 with the session cookies cleared: JSON `{"error":"Session revoked. Please sign in again.","code":"session_revoked"}` where `res.json` exists (Express), plain `Session revoked` otherwise. Options: `getSessionId(req)`, `getUserId(req)`, `onRevoked(req, res, next, { sessionId, userId })`, `capture` (default `true`: also sends `session.seen`), `captureWindowMs` (default 300000), `syncIntervalMs` (default 30000). In NestJS, call `app.use(sessions.guard())` in `main.ts`.
|
|
479
521
|
|
|
480
|
-
|
|
481
|
-
never values. A coding agent should inspect the customer repository for routes and `track()` calls,
|
|
482
|
-
use the local contract to construct SQL, use live discovery to verify emitted fields, then validate
|
|
483
|
-
the candidate query. Full guidance is shipped in `TELEMETRY-SQL.md`.
|
|
522
|
+
3. **Anything else: `check()` (8.25.0+).** `sessions.check(req, options?)` takes a Node request or a Web `Request` and returns `{ revoked, sessionId, userId }`; you answer 401 yourself. Options: `getSessionId`, `getUserId`, `capture` (default `true`), `ip` (for `session.seen` when `req` is a Web `Request`, which has no socket).
|
|
484
523
|
|
|
485
|
-
|
|
524
|
+
```js
|
|
525
|
+
const sessions = require('securenow/sessions');
|
|
486
526
|
|
|
487
|
-
|
|
488
|
-
|
|
527
|
+
// Fastify
|
|
528
|
+
fastify.addHook('onRequest', async (request, reply) => {
|
|
529
|
+
if (sessions.check(request.raw).revoked) return reply.code(401).send({ error: 'session_revoked' });
|
|
530
|
+
});
|
|
489
531
|
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
532
|
+
// Koa
|
|
533
|
+
app.use(async (ctx, next) => {
|
|
534
|
+
if (sessions.check(ctx.req).revoked) { ctx.status = 401; ctx.body = { error: 'session_revoked' }; return; }
|
|
535
|
+
await next();
|
|
536
|
+
});
|
|
495
537
|
|
|
496
|
-
|
|
538
|
+
// Hono (c.req.raw is a Web Request)
|
|
539
|
+
app.use('*', async (c, next) => {
|
|
540
|
+
if (sessions.check(c.req.raw).revoked) return c.json({ error: 'session_revoked' }, 401);
|
|
541
|
+
await next();
|
|
542
|
+
});
|
|
497
543
|
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
544
|
+
// h3
|
|
545
|
+
app.use(defineEventHandler((event) => {
|
|
546
|
+
if (sessions.check(event.node.req).revoked) { setResponseStatus(event, 401); return { error: 'session_revoked' }; }
|
|
547
|
+
}));
|
|
548
|
+
```
|
|
502
549
|
|
|
503
|
-
|
|
550
|
+
```ts
|
|
551
|
+
// Next.js route handler (Node.js runtime), app/api/account/route.ts
|
|
552
|
+
import { check } from 'securenow/sessions';
|
|
504
553
|
|
|
505
|
-
|
|
554
|
+
export async function GET(request: Request) {
|
|
555
|
+
if (check(request).revoked) return Response.json({ error: 'session_revoked' }, { status: 401 });
|
|
556
|
+
return Response.json({ ok: true });
|
|
557
|
+
}
|
|
558
|
+
```
|
|
506
559
|
|
|
507
|
-
|
|
508
|
-
const { originalConsole, restoreConsole } = require('securenow/console-instrumentation');
|
|
509
|
-
originalConsole.log('This bypasses OTLP');
|
|
510
|
-
restoreConsole(); // undo the patch
|
|
511
|
-
```
|
|
560
|
+
4. **Lower level:** `sessions.isRevoked({ sessionId, userId })` returns a boolean. Call `sessions.start()` once at boot when this is all you use (`guard()`, `check()` and the zero-code guard start the sync themselves).
|
|
512
561
|
|
|
513
|
-
|
|
562
|
+
- **Verify:** revoke a test session, wait up to 30 seconds, then send a request with that cookie or token: it gets 401 and `Set-Cookie` headers that clear the session cookies. `npx securenow revoke list` shows the entry; `npx securenow revoke restore <id>` lifts it.
|
|
514
563
|
|
|
515
|
-
|
|
564
|
+
### 5.11 Crash capture
|
|
516
565
|
|
|
517
|
-
|
|
518
|
-
|
|
566
|
+
- **What:** an uncaught exception or unhandled promise rejection becomes a FATAL log record with `exception.type`, `exception.message` and `exception.stacktrace`. Logs and the spans that ended before the crash are flushed (up to 2 seconds), then the process exits as Node would (stack on stderr, exit code 1). If the app registered its own handler, the error is recorded as ERROR and the app decides. OTLP export errors never crash the app.
|
|
567
|
+
- **On:** automatic while `config.logging.enabled` is `true`.
|
|
568
|
+
- **Verify:** `npx securenow logs --level fatal --since 1h --env <env>`.
|
|
519
569
|
|
|
520
|
-
|
|
521
|
-
const clientIp = resolveClientIp(req);
|
|
570
|
+
### 5.12 Detection
|
|
522
571
|
|
|
523
|
-
|
|
524
|
-
const socketIp = resolveSocketIp(req);
|
|
572
|
+
- **Starter protection:** when an app is created or first sends traffic it gets about 20 rules (SQL injection, XSS, traversal, command and template injection, Log4Shell, Spring4Shell, metadata SSRF, CRLF, prototype pollution, `.env`/`.git`/backup probes, exploit kits, attack-tool user agents, failed sign-ins, 404 scanning) in **monitor** mode: they alert and block nothing. In **block** mode the block-ready rules block the address for 6 hours on exploit signatures. Only a signed-in person can turn blocking on, and only when the app sees real client IPs (5.4).
|
|
525
573
|
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
574
|
+
```bash
|
|
575
|
+
npx securenow protect status # mode, rules, alerts, ready to block?
|
|
576
|
+
npx securenow protect block --app <key> # a person, after reviewing
|
|
577
|
+
npx securenow protect monitor --app <key> # back to alerts only
|
|
578
|
+
```
|
|
529
579
|
|
|
530
|
-
|
|
580
|
+
- **Alert rules from English:** SecureNow drafts an instant rule (each request, as it arrives) or a periodic one (SQL on a schedule) from your app's real data, checks it, and creates it in **test mode**: it alerts but nothing acts on it (no instant block, automation or agent action).
|
|
531
581
|
|
|
532
|
-
```
|
|
533
|
-
|
|
582
|
+
```bash
|
|
583
|
+
npx securenow alerts rules from-text "one IP failing 10 sign-ins in 5 minutes" --app <key> --draft-only # review
|
|
584
|
+
npx securenow alerts rules from-text "one IP failing 10 sign-ins in 5 minutes" --app <key> # create (asks to confirm)
|
|
585
|
+
npx securenow alerts rules test <rule-id> --wait # dry run on real data
|
|
586
|
+
npx securenow alerts rules test <rule-id> --mode live --wait # inserts 6 synthetic attack requests first
|
|
587
|
+
npx securenow alerts rules runs <rule-id> --since 1h # what each run saw, and why it did or didn't notify
|
|
588
|
+
npx securenow alerts rules why <rule-id> --ip 203.0.113.50 # why it did or didn't alert on one address
|
|
589
|
+
npx securenow alerts rules promote <rule-id> --yes # a person moves it to prod
|
|
590
|
+
```
|
|
534
591
|
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
```
|
|
592
|
+
Agents don't promote: they ask with `npx securenow alerts rules request-promotion <rule-id> --reason "..." --evidence "..."`, and a person decides (`npx securenow human promotions`).
|
|
593
|
+
- **SQL rules:** read the contract first (`npx securenow telemetry schema --json`, offline, and `node_modules/securenow/TELEMETRY-SQL.md`), check fields really sent (`npx securenow telemetry fields --app <key> --since 24h --json`), dry-run (`npx securenow alerts rules validate --sql @rule.sql --app <key>`), then `alerts rules create --name "..." --sql @rule.sql --apps <key>`. Scope app keys with `__USER_APP_KEYS__` and return an `ip` column.
|
|
594
|
+
- **What fires:** `npx securenow today` (the Overview), `npx securenow attacker <ip>`, `npx securenow review` (rules that fired too wide), `npx securenow human` (decisions waiting for a person). Details: SKILL-CLI.md.
|
|
539
595
|
|
|
540
|
-
|
|
596
|
+
### 5.13 MCP for coding agents
|
|
541
597
|
|
|
542
|
-
|
|
543
|
-
securenow cidr match 10.0.0.5 10.0.0.0/8,192.168.1.0/24 # exit 0 = match, 2 = miss
|
|
544
|
-
securenow cidr parse 10.0.0.0/8 # network/broadcast/mask/size
|
|
545
|
-
```
|
|
598
|
+
- **Local stdio server** (after `npx securenow login`):
|
|
546
599
|
|
|
547
|
-
|
|
600
|
+
```bash
|
|
601
|
+
claude mcp add securenow -- npx securenow mcp
|
|
602
|
+
codex mcp add securenow -- npx securenow mcp
|
|
603
|
+
```
|
|
548
604
|
|
|
549
|
-
|
|
605
|
+
Cursor, `.cursor/mcp.json`: `{ "mcpServers": { "securenow": { "command": "npx", "args": ["securenow", "mcp"] } } }`.
|
|
606
|
+
- **Hosted server:** `https://api.securenow.ai/mcp` with OAuth sign-in (an API key as `Authorization: Bearer <key>` also works), e.g. `claude mcp add --transport http securenow https://api.securenow.ai/mcp`.
|
|
607
|
+
- Read tools are annotated read-only; every write tool refuses to run without `confirm: true` and a non-empty `reason`. `npx securenow mcp --profile investigator|detection|responder` serves fewer tools. Details and tool counts: SKILL-CLI.md.
|
|
550
608
|
|
|
551
|
-
|
|
609
|
+
## 6. Deployment
|
|
552
610
|
|
|
553
|
-
|
|
611
|
+
### 6.1 Production credentials
|
|
554
612
|
|
|
555
|
-
```
|
|
556
|
-
|
|
613
|
+
```bash
|
|
614
|
+
npx securenow credentials runtime --env production # writes .securenow/credentials.production.json
|
|
615
|
+
npx securenow credentials runtime --env production --format env # prints SECURENOW_CREDENTIALS=<base64>
|
|
616
|
+
```
|
|
557
617
|
|
|
558
|
-
|
|
559
|
-
|
|
618
|
+
The file has the app key, the runtime API key and the config, with `deploymentEnvironment` set, and never the admin token. Ship it as `<app-root>/.securenow/credentials.json` (the generated name also works when it is the only file), or store the printed line as a secret environment variable. Both contain the runtime API key: secret storage only, never the image registry or the repository. Keep `.securenow/runtime.json` and `admin.json` out of images with a `.dockerignore`:
|
|
619
|
+
|
|
620
|
+
```
|
|
621
|
+
.securenow/admin.json
|
|
622
|
+
.securenow/runtime.json
|
|
623
|
+
.securenow/credentials*.json
|
|
560
624
|
```
|
|
561
625
|
|
|
562
|
-
|
|
626
|
+
### 6.2 Docker
|
|
563
627
|
|
|
564
|
-
|
|
628
|
+
Plain Node:
|
|
565
629
|
|
|
566
|
-
|
|
630
|
+
```dockerfile
|
|
631
|
+
FROM node:24-slim
|
|
632
|
+
WORKDIR /app
|
|
633
|
+
COPY package*.json ./
|
|
634
|
+
RUN npm ci --omit=dev
|
|
635
|
+
COPY . .
|
|
636
|
+
CMD ["node", "-r", "securenow/register", "src/index.js"]
|
|
637
|
+
```
|
|
567
638
|
|
|
568
639
|
```bash
|
|
569
|
-
|
|
570
|
-
|
|
640
|
+
docker run -v "$PWD/.securenow/credentials.production.json:/app/.securenow/credentials.json:ro" my-api
|
|
641
|
+
# or, without a file:
|
|
642
|
+
npx securenow credentials runtime --env production --format env > securenow.env # secret: don't commit
|
|
643
|
+
docker run --env-file securenow.env my-api
|
|
571
644
|
```
|
|
572
645
|
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
## Credentials Configuration
|
|
646
|
+
Next.js standalone (`output: 'standalone'` in `next.config`):
|
|
576
647
|
|
|
577
|
-
|
|
648
|
+
```dockerfile
|
|
649
|
+
FROM node:24-slim AS build
|
|
650
|
+
WORKDIR /app
|
|
651
|
+
COPY package*.json ./
|
|
652
|
+
RUN npm ci
|
|
653
|
+
COPY . .
|
|
654
|
+
RUN npm run build
|
|
578
655
|
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
| `config.otel.logLevel` | SDK diagnostics: `error`, `warn`, `info`, `debug`, or `none` |
|
|
589
|
-
| `config.otel.disableInstrumentations` | OTel instrumentation package names to skip |
|
|
590
|
-
| `config.logging.enabled` | Enable/disable OTLP log export |
|
|
591
|
-
| `config.capture.body` | Capture HTTP request bodies |
|
|
592
|
-
| `config.capture.maxBodySize` | Max body size in bytes |
|
|
593
|
-
| `config.capture.multipart` | Capture multipart metadata, never file content |
|
|
594
|
-
| `config.capture.sensitiveFields` | Extra redaction field fragments |
|
|
595
|
-
| `config.runtime.deploymentEnvironment` | Sent as `deployment.environment` |
|
|
596
|
-
| `config.runtime.noUuid` | Use exact app key as service name without UUID suffix |
|
|
597
|
-
| `config.runtime.strict` | Exit clustered workers when no app identity resolves |
|
|
598
|
-
| `config.runtime.testSpan` | Emit a startup test span |
|
|
599
|
-
| `config.firewall.*` | Local firewall behavior, API URL, intervals, cloud WAF, and trusted proxy settings |
|
|
656
|
+
FROM node:24-slim
|
|
657
|
+
WORKDIR /app
|
|
658
|
+
ENV NODE_ENV=production
|
|
659
|
+
COPY --from=build /app/.next/standalone ./
|
|
660
|
+
COPY --from=build /app/.next/static ./.next/static
|
|
661
|
+
COPY --from=build /app/public ./public
|
|
662
|
+
# credentials: mount /app/.securenow/credentials.json (next to server.js) or pass SECURENOW_CREDENTIALS
|
|
663
|
+
CMD ["node", "server.js"]
|
|
664
|
+
```
|
|
600
665
|
|
|
601
|
-
|
|
666
|
+
Nuxt (`.output`):
|
|
602
667
|
|
|
603
|
-
|
|
668
|
+
```dockerfile
|
|
669
|
+
FROM node:24-slim AS build
|
|
670
|
+
WORKDIR /app
|
|
671
|
+
COPY package*.json ./
|
|
672
|
+
RUN npm ci
|
|
673
|
+
COPY . .
|
|
674
|
+
RUN npm run build
|
|
604
675
|
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
676
|
+
FROM node:24-slim
|
|
677
|
+
WORKDIR /app
|
|
678
|
+
COPY --from=build /app/.output ./.output
|
|
679
|
+
# credentials: mount /app/.securenow/credentials.json or pass SECURENOW_CREDENTIALS
|
|
680
|
+
CMD ["node", ".output/server/index.mjs"]
|
|
681
|
+
```
|
|
610
682
|
|
|
611
|
-
|
|
683
|
+
Layer 3 (`firewall.iptables`) in a container needs `--cap-add NET_ADMIN`, a root process and the `iptables` package in the image. Layers 2 and 3 act on the connection's source address, so they only help when clients connect to the container directly, not through a load balancer or proxy (layer 1 handles those with forwarding headers).
|
|
612
684
|
|
|
613
|
-
|
|
685
|
+
### 6.3 Kubernetes
|
|
614
686
|
|
|
615
|
-
|
|
687
|
+
As a file:
|
|
616
688
|
|
|
617
|
-
|
|
689
|
+
```bash
|
|
690
|
+
kubectl create secret generic securenow-credentials --from-file=credentials.json=.securenow/credentials.production.json
|
|
691
|
+
```
|
|
618
692
|
|
|
619
|
-
|
|
693
|
+
```yaml
|
|
694
|
+
containers:
|
|
695
|
+
- name: api
|
|
696
|
+
image: my-api
|
|
697
|
+
volumeMounts:
|
|
698
|
+
- name: securenow
|
|
699
|
+
mountPath: /app/.securenow/credentials.json
|
|
700
|
+
subPath: credentials.json
|
|
701
|
+
readOnly: true
|
|
702
|
+
volumes:
|
|
703
|
+
- name: securenow
|
|
704
|
+
secret:
|
|
705
|
+
secretName: securenow-credentials
|
|
706
|
+
```
|
|
620
707
|
|
|
621
|
-
|
|
708
|
+
Or as an environment variable:
|
|
622
709
|
|
|
623
710
|
```bash
|
|
624
|
-
|
|
625
|
-
|
|
711
|
+
npx securenow credentials runtime --env production --format env > securenow.env
|
|
712
|
+
kubectl create secret generic securenow-env --from-env-file=securenow.env
|
|
626
713
|
```
|
|
627
714
|
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
{ "scripts": { "start": "node -r securenow/register src/index.js" } }
|
|
715
|
+
```yaml
|
|
716
|
+
envFrom:
|
|
717
|
+
- secretRef:
|
|
718
|
+
name: securenow-env
|
|
633
719
|
```
|
|
634
720
|
|
|
635
|
-
|
|
721
|
+
### 6.4 PM2
|
|
636
722
|
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
723
|
+
```js
|
|
724
|
+
// ecosystem.config.cjs (.cjs so require.resolve works in "type": "module" projects too)
|
|
725
|
+
module.exports = {
|
|
726
|
+
apps: [{
|
|
727
|
+
name: 'api',
|
|
728
|
+
script: './src/index.js',
|
|
729
|
+
cwd: __dirname,
|
|
730
|
+
exec_mode: 'cluster', // or 'fork'
|
|
731
|
+
instances: 'max',
|
|
732
|
+
node_args: ['-r', require.resolve('securenow/register')],
|
|
733
|
+
}],
|
|
734
|
+
};
|
|
642
735
|
```
|
|
643
736
|
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
737
|
+
- Always the absolute path from `require.resolve`, computed when PM2 loads the file. In cluster mode Node resolves a bare `-r securenow/register` from the PM2 daemon's folder, so workers started from elsewhere (`pm2 resurrect` at boot) crash-loop with `Cannot find module 'securenow/register'`.
|
|
738
|
+
- Each process reads the `.securenow/` folder of the app PM2 started, wherever the daemon runs, and every worker reports under one service name. A PM2 process without credentials says so at startup.
|
|
739
|
+
- TypeScript: `node_args: ['-r', require.resolve('securenow/register'), '-r', require.resolve('ts-node/register')]`.
|
|
740
|
+
- Several workers count rate limits separately unless `firewall.rateLimitStore` points at Redis.
|
|
741
|
+
- After changing `node_args`: `pm2 delete api && pm2 start ecosystem.config.cjs && pm2 save`.
|
|
647
742
|
|
|
648
|
-
|
|
743
|
+
### 6.5 systemd and plain VMs
|
|
649
744
|
|
|
650
|
-
```
|
|
651
|
-
|
|
745
|
+
```ini
|
|
746
|
+
[Service]
|
|
747
|
+
WorkingDirectory=/srv/my-api
|
|
748
|
+
ExecStart=/usr/bin/node -r securenow/register src/index.js
|
|
749
|
+
Restart=always
|
|
750
|
+
User=my-api
|
|
751
|
+
# credentials: /srv/my-api/.securenow/credentials.json (mode 600, owned by my-api)
|
|
752
|
+
# or: EnvironmentFile=/etc/my-api/securenow.env (the line printed by --format env)
|
|
652
753
|
```
|
|
653
754
|
|
|
654
|
-
|
|
755
|
+
Credentials are searched from the working directory and the entry script's folder, so set `WorkingDirectory` to the app folder.
|
|
655
756
|
|
|
656
|
-
###
|
|
757
|
+
### 6.6 Hosts without file mounts (Railway, Render, Heroku, Fly...)
|
|
657
758
|
|
|
658
|
-
|
|
659
|
-
{
|
|
660
|
-
"apiKey": "snk_live_...",
|
|
661
|
-
"app": {
|
|
662
|
-
"key": "prod-api-uuid",
|
|
663
|
-
"name": "prod-api",
|
|
664
|
-
"instance": "https://collector.prod.internal:4318"
|
|
665
|
-
},
|
|
666
|
-
"config": {
|
|
667
|
-
"runtime": { "noUuid": true, "strict": true, "deploymentEnvironment": "production" },
|
|
668
|
-
"logging": { "enabled": true },
|
|
669
|
-
"capture": { "body": true, "multipart": true },
|
|
670
|
-
"firewall": { "enabled": true, "tcp": true, "syncInterval": 30, "failMode": "open" },
|
|
671
|
-
"otel": { "logLevel": "error" }
|
|
672
|
-
}
|
|
673
|
-
}
|
|
674
|
-
```
|
|
759
|
+
Store the output of `npx securenow credentials runtime --env production --format env` as a secret variable named `SECURENOW_CREDENTIALS` (dashboard, `fly secrets set`, `heroku config:set`...) and keep the preload in the start command (`web: node -r securenow/register src/index.js` in a Procfile).
|
|
675
760
|
|
|
676
|
-
###
|
|
761
|
+
### 6.7 Vercel
|
|
677
762
|
|
|
678
|
-
|
|
679
|
-
FROM node:24.22.1-slim
|
|
680
|
-
WORKDIR /app
|
|
681
|
-
COPY package*.json ./
|
|
682
|
-
RUN npm ci --omit=dev
|
|
683
|
-
COPY . .
|
|
684
|
-
# Mount/copy secret file at runtime as /app/.securenow/credentials.json.
|
|
763
|
+
Next.js on Vercel goes through `@vercel/otel` (an optional dependency); set `SECURENOW_CREDENTIALS` in the project's environment variables. Not tested end to end, and the firewall is not verified on Vercel serverless functions. Edge functions and Edge middleware are not instrumented. AWS Lambda and other serverless platforms are not supported or tested.
|
|
685
764
|
|
|
686
|
-
|
|
687
|
-
```
|
|
765
|
+
## 7. Verification checklist
|
|
688
766
|
|
|
689
|
-
|
|
767
|
+
| Step | Command | Good output |
|
|
768
|
+
|---|---|---|
|
|
769
|
+
| Version | `npx securenow version` | `securenow v8.24.0` or newer (8.25.0+ for the marked items) |
|
|
770
|
+
| Auth | `npx securenow whoami` | Admin auth and the runtime app both present |
|
|
771
|
+
| Config | `npx securenow env` | `Service name` = the app key UUID, the expected `Environment`, `Firewall enabled` (not `missing firewall API key`) |
|
|
772
|
+
| Connectivity | `npx securenow doctor` | `All checks passed.` (traces, logs, API, firewall sync) |
|
|
773
|
+
| Ingest | `npx securenow test-span` | `Span accepted (HTTP 200)` |
|
|
774
|
+
| App start | start the app | `[securenow] pid=... appId="<app-uuid>" instance=... apiKey=set credentials=<path or env:SECURENOW_CREDENTIALS> → service.name=<app-uuid> ...` (`apiKey=missing`: no runtime API key, so the gateway rejects telemetry and firewall sync; before 8.25.0 the field showed whether the app key was set), `OTel SDK started → https://ingest.securenow.ai/v1/traces`, `Logging: ENABLED`, `Request body capture: ENABLED (max: 10240 bytes, redacting 68 sensitive fields)`, `Firewall: ENABLED`, `Firewall: environment=<env>`, `Firewall: synced <n> global blocked IPs (...) and <n> scoped block rules`. ESM apps first print `ESM loader hook auto-registered (module.register)`. Next.js prints `Next.js App -> service.name=<app-uuid> apiKey=set credentials=<...>` (the `apiKey` and `credentials` fields 8.25.0+), `Environment: <env>`, `OpenTelemetry started for Next.js -> ...` and the same Firewall lines |
|
|
775
|
+
| Traffic | one real request, then `npx securenow traces --env <env> --limit 5` | The request, with `http.client_ip` and (where supported) `http.route` |
|
|
776
|
+
| Overview | `npx securenow status --env <env>` and `npx securenow today` | The app covered: data flowing, firewall on, detection monitoring |
|
|
777
|
+
| Firewall | `npx securenow firewall status --env <env>`, `npx securenow firewall test-ip 203.0.113.7 --env <env>` | Firewall on for the app and environment; the verdict for the IP |
|
|
690
778
|
|
|
691
|
-
|
|
692
|
-
{
|
|
693
|
-
"app": { "key": "k8s-service", "name": "k8s-service" },
|
|
694
|
-
"config": {
|
|
695
|
-
"otel": {
|
|
696
|
-
"tracesEndpoint": "http://tempo:4318/v1/traces",
|
|
697
|
-
"logsEndpoint": "http://loki:4318/v1/logs"
|
|
698
|
-
},
|
|
699
|
-
"logging": { "enabled": true },
|
|
700
|
-
"runtime": { "noUuid": true }
|
|
701
|
-
}
|
|
702
|
-
}
|
|
703
|
-
```
|
|
779
|
+
Bad signs: no `[securenow]` line at all (the preload is not on the command that starts the server), `credentials=none`, `service.name=securenow-free-<uuid>` or `nextjs-app-<uuid>` (no app key found), `Firewall: initial sync failed` (key, network or environment), `disabled by SECURENOW_DISABLED`.
|
|
704
780
|
|
|
705
|
-
|
|
781
|
+
## 8. Troubleshooting
|
|
706
782
|
|
|
707
|
-
|
|
783
|
+
| Symptom | Cause | Fix |
|
|
784
|
+
|---|---|---|
|
|
785
|
+
| No data in the dashboard | The process that serves HTTP didn't load the SDK (no `[securenow]` lines) | Put the preload on the real start command (script, `CMD`, PM2 `node_args`, `ExecStart`) |
|
|
786
|
+
| | Looking at the wrong environment: `init` writes `local`, the CLI and dashboard default to `production` | `--env local` or `--env all`, or set `deploymentEnvironment` for that deployment |
|
|
787
|
+
| | Runtime API key missing (`npx securenow env` says `missing firewall API key`) | `npx securenow app connect` or `npx securenow api-key create` |
|
|
788
|
+
| | `service.name` isn't the app key (`noUuid: false`, a custom `serviceName`) | Remove the override |
|
|
789
|
+
| | `SECURENOW_DISABLED` set; outbound HTTPS to `ingest.securenow.ai` blocked; trial ended (HTTP 402) | Unset it; open egress; subscribe |
|
|
790
|
+
| Wrong credentials picked up | `SECURENOW_CREDENTIALS` in the environment, a stray `runtime.json` next to the production file, a `credentials.staging.json`, a parent folder's or `~/.securenow/` file | Read the `credentials=` startup line, then remove the stray source (order in section 4) |
|
|
791
|
+
| PM2 cluster crash-loop, `Cannot find module 'securenow/register'` | Bare `-r securenow/register` resolved from the PM2 daemon's folder | `node_args: ['-r', require.resolve('securenow/register')]`, then `pm2 delete <name> && pm2 start ecosystem.config.cjs && pm2 save` |
|
|
792
|
+
| Next.js standalone: every request 500, `Cannot find module '@opentelemetry/...'` | Instrumentation file from `init` before 8.24.0 (`webpackIgnore` imports) | Run `npx securenow init` again (it replaces that file) or remove `webpackIgnore`; rebuild |
|
|
793
|
+
| Next.js: no SecureNow lines at startup | Instrumentation file in the wrong folder (Next.js 14 and 15 ignore a root file in a `src/` layout), or Next.js 14 without `experimental.instrumentationHook` | Rerun `npx securenow init` (8.25.0+ writes or moves it to `src/`) or move it to `src/instrumentation.ts`; Next.js 14 config in section 3.13 |
|
|
794
|
+
| Body not captured | Content type other than JSON, form, GraphQL or multipart; the route never reads its body; body over `maxBodySize` (`[TOO LARGE: <n> bytes]`); `capture.body: false`; Edge runtime | Add a body parser, raise `maxBodySize`, check the config, use the Node.js runtime |
|
|
795
|
+
| Client IP is the proxy's address | The proxy has a public address SecureNow doesn't trust, or doesn't send `X-Forwarded-For` | Add it to `networking.trustedProxies` (IP, CIDR or `"cloudflare"`), make it forward the client; `http.client_ip.source` should read `x-forwarded-for` |
|
|
796
|
+
| Blocked IP still gets through | Firewall not running (no `Firewall: synced` line); block scoped to another app, environment or route; firewall switched off for that environment; the IP resolves to the proxy; the IP is on the allowlist (listed IPs skip the blocklist); not yet propagated; layer 2/3 behind a proxy | `npx securenow firewall test-ip <ip> --path <path> --env <env>`, `npx securenow firewall apps`, `npx securenow firewall hits --ip <ip>`; wait one `versionCheckInterval` (10 s) |
|
|
797
|
+
| ESM app: no `http.route`, no library spans | ESM hook not registered: SDK loaded with an `import` inside the app instead of `-r`, Node older than 20.6, or a wrapper that hides the entry script | `node -r securenow/register <entry>` directly; look for `ESM loader hook auto-registered`. Hono, h3, Feathers and Polka routes need 8.25.0+ |
|
|
798
|
+
| Rate limit not applied | Rule created for `production` while the app reports `local`; route or method mismatch; counted per worker | `npx securenow ratelimit test <ip> --path <path> --method <m> --env <env>`; Redis store for clusters |
|
|
799
|
+
| Revoked session still works | Sync not yet run (every 30 s); no enforcement wired (`sessions.guard` 8.25.0+, `guard()` or `check()`); different session id (SHA-256 of the exact cookie value) or a custom cookie name | Wait 30 s; wire one of 5.10; add `sessions.cookieNames` (8.25.0+) or a `getSessionId` |
|
|
800
|
+
| Every request 403 at boot | `firewall.failMode: "closed"` and the first sync failed | Fix connectivity, or keep `failMode: "open"` |
|
|
801
|
+
| Tests send data to the real app | Test runs load the project's `.securenow` credentials | `SECURENOW_DISABLED=1 npm test` |
|
|
708
802
|
|
|
709
|
-
|
|
803
|
+
## 9. Tested on, and honest limits
|
|
710
804
|
|
|
711
|
-
|
|
712
|
-
[securenow] app.key="my-app" → service.name=my-app
|
|
713
|
-
[securenow] OTel SDK started → https://collector:4318/v1/traces
|
|
714
|
-
[securenow] Logging: ENABLED → https://collector:4318/v1/logs
|
|
715
|
-
[securenow] Request body capture: ENABLED (max: 10240 bytes)
|
|
716
|
-
[securenow] Firewall: ENABLED
|
|
717
|
-
[securenow] Firewall: synced 142 blocked IPs
|
|
718
|
-
```
|
|
805
|
+
**Tested end to end** (traces, client IP, body reaching the app, body capture with redaction, header redaction, 403 block, 429 rate limit, challenge, error span, exception, logs): Node 24.22 (CI: Node 24); Express 4.22 and 5.3; Fastify 5.12; NestJS 11.2; Koa 3.3; Hapi 21.4; Hono 4; Polka 0.5; Feathers 5.0; h3 1.15; Next.js 14.2 (`next start` and standalone), 15.5 and 16.4 (Turbopack and webpack, `next start` and standalone); Nuxt 3.21 and 4.6; Bun 1.3; PM2 5.4 (fork and cluster). Older majors are expected to work but are not tested: say "tested on", not "supports every version".
|
|
719
806
|
|
|
720
|
-
|
|
807
|
+
**Not supported:** Edge runtimes (Cloudflare Workers, Vercel Edge, Next.js Edge middleware and routes), Deno, `Bun.serve`, browser telemetry.
|
|
808
|
+
**Not tested:** Vercel (firewall unverified on its serverless functions), AWS Lambda and other serverless platforms.
|
|
809
|
+
**Layers 2 to 4:** turn them on with 8.25.0+ only (in 8.24.0 layer 2 dropped nothing for most apps and layers 3 and 4 mishandled IPv6). **Cloud WAF (layer 4):** request format tested, not run against real Cloudflare, AWS or GCP accounts; on Cloudflare your own WAF rule must reference the `securenow-blocklist` list.
|
|
810
|
+
**IPv6:** the blocklist, allowlist, trusted IPs, rate limits, challenges and API-key IP limits take IPv6 addresses and CIDR ranges, stored in canonical form (`2001:DB8::1` = `2001:db8::1`); layer 1 and trusted proxies match them **(8.25.0+; older SDKs match exact IPv6 addresses but ignore IPv6 ranges)**. A trusted-IP range can be no wider than /8 (IPv4) or /32 (IPv6).
|
|
811
|
+
**`nuxt dev`:** before 8.25.0 the client IP was unknown, so IP blocking only applied to the built server.
|
|
812
|
+
**Log redaction (8.25.0+)** goes by field names and value shapes: a secret in free text with no recognizable shape is exported as written.
|
|
721
813
|
|
|
722
|
-
|
|
814
|
+
## 10. Package entry points
|
|
723
815
|
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
securenow
|
|
727
|
-
securenow
|
|
816
|
+
| Import | Use |
|
|
817
|
+
|---|---|
|
|
818
|
+
| `securenow/register` (also `securenow`) | The preload: tracing, logs, capture, crash capture, firewall (`node -r securenow/register`) |
|
|
819
|
+
| `securenow/firewall-only` | Preload: firewall only, no OpenTelemetry |
|
|
820
|
+
| `securenow/register-vite` | Same runtime as `securenow/register`, for the Node process serving a Vite app |
|
|
821
|
+
| `securenow/nextjs` | `registerSecureNow(options?)`, called from `instrumentation.ts` |
|
|
822
|
+
| `securenow/nextjs-auto-capture` | Body-capture fallback imported by the generated instrumentation file |
|
|
823
|
+
| `securenow/nuxt` | Nuxt 3/4 module (`modules: ['securenow/nuxt']`) |
|
|
824
|
+
| `securenow/events` | `track(type, props)`, `flush()` |
|
|
825
|
+
| `securenow/sessions` | `guard()`, `capture()`, `isRevoked()`, `start()`, `sha256()`; `check()` **(8.25.0+)** |
|
|
826
|
+
| `securenow/tracing` | `getLogger(name, version)`, `isLoggingEnabled()` |
|
|
827
|
+
| `securenow/resolve-ip` | `resolveClientIp(req)`, `resolveClientIpWithDetails(req)`, `resolveSocketIp(socket)`, `isFromTrustedProxy(ip)` |
|
|
828
|
+
| `securenow/cidr` | `createMatcher(list)` returns `{ isBlocked(ip), stats() }` for IPv4 and IPv6 addresses and ranges (IPv6 ranges 8.25.0+); `parseCidr`, `parseCidr6`, `parseIp`, `ipToInt`, `matchesCidr` |
|
|
829
|
+
| `securenow/firewall` | `getStats()`, `getMatcher()`, `getAllowlistMatcher()`, `init()`/`shutdown()` for custom setups |
|
|
830
|
+
| `securenow/rate-limits` | API helpers: `parseRateLimitText()`, `createRateLimitFromText()`, `createRateLimit()`, `listRateLimits()` (use the CLI or MCP for one-off rules) |
|
|
831
|
+
| `securenow/telemetry-schema` | The ClickHouse table/field contract for alert SQL (also `npx securenow telemetry schema --json`) |
|
|
832
|
+
| `securenow/app-config` | The resolved configuration (`resolveAll()`, `resolveFirewallOptions()`...) |
|
|
833
|
+
| `securenow/nextjs-webpack-config`, `securenow/nextjs-middleware`, `securenow/nextjs-wrapper`, `securenow/console-instrumentation`, `securenow/web-vite` | Legacy: not needed for a new setup |
|
|
834
|
+
|
|
835
|
+
Examples:
|
|
836
|
+
|
|
837
|
+
```js
|
|
838
|
+
// An internal health endpoint that shows the firewall is synced (the preload's own module instance)
|
|
839
|
+
const firewall = require('securenow/firewall');
|
|
840
|
+
app.get('/healthz/securenow', (req, res) => {
|
|
841
|
+
const s = firewall.getStats();
|
|
842
|
+
res.json({ initialized: s.initialized, blocked: s.blocked, rateLimited: s.rateLimited, circuit: s.circuitState });
|
|
843
|
+
});
|
|
844
|
+
|
|
845
|
+
// CIDR matching
|
|
846
|
+
const { createMatcher } = require('securenow/cidr');
|
|
847
|
+
const office = createMatcher(['10.0.0.0/8', '192.168.1.0/24']);
|
|
848
|
+
office.isBlocked('10.0.0.5'); // true
|
|
728
849
|
```
|
|
850
|
+
|
|
851
|
+
The same helpers exist in the CLI without writing Node: `npx securenow redact '<json>'`, `npx securenow cidr match <ip> <cidr,...>` (exit 0 on a match, 2 otherwise), `npx securenow cidr parse <cidr>`, `npx securenow env`, `npx securenow log send`, `npx securenow event send`, `npx securenow test-span`.
|