securenow 8.22.0 → 8.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/NPM_README.md CHANGED
@@ -584,6 +584,10 @@ npx securenow credentials runtime --env production
584
584
  # also works when the canonical credentials.json file is absent.
585
585
  # The SDK checks named files in a fixed order and does not use env vars
586
586
  # to pick the credentials filename.
587
+ # Hosts without file mounts (Vercel, Railway, Render, Heroku, Fly):
588
+ npx securenow credentials runtime --env production --format env
589
+ # -> SECURENOW_CREDENTIALS=<base64>; store it as a secret environment variable.
590
+ # It takes precedence over credential files.
587
591
 
588
592
  # Use --json for machine-readable output
589
593
  npx securenow logs --json --level error | jq '.logs'
@@ -719,12 +723,16 @@ module.exports = {
719
723
  apps: [{
720
724
  name: 'my-app',
721
725
  script: './app.js',
722
- node_args: '-r securenow/register',
726
+ instances: 'max', // or leave out for fork mode
727
+ // An absolute path, resolved when PM2 loads this file: in cluster mode Node resolves a bare
728
+ // '-r securenow/register' from the PM2 daemon's folder, so workers started by `pm2 resurrect`
729
+ // at boot crash-loop with "Cannot find module 'securenow/register'".
730
+ node_args: ['-r', require.resolve('securenow/register')],
723
731
  }]
724
732
  };
725
733
  ```
726
734
 
727
- > **Important:** Put `.securenow/runtime.json` in the PM2 app root for local/dev deployments, or mount the production runtime export as `.securenow/credentials.json`. Keep `node_args: '-r securenow/register'`. PM2 restarts will then load the SDK, traces/logs, body capture, and firewall without any SecureNow env vars.
735
+ > **Important:** Put `.securenow/runtime.json` in the PM2 app root for local/dev deployments, or mount the production runtime export as `.securenow/credentials.json`. PM2 restarts will then load the SDK, traces/logs, body capture, and firewall without any SecureNow env vars. Fork and cluster mode both read the app folder PM2 started (`pm_cwd`), whichever folder the PM2 daemon itself was started from (before 8.24.0 a cluster worker searched the daemon's folder and could start without credentials).
728
736
 
729
737
  ---
730
738
 
@@ -840,7 +848,7 @@ PM2 config:
840
848
  name: 'my-nestjs-app',
841
849
  script: 'app.ts',
842
850
  interpreter: 'node',
843
- node_args: '-r securenow/register -r ts-node/register',
851
+ node_args: ['-r', require.resolve('securenow/register'), '-r', require.resolve('ts-node/register')],
844
852
  }
845
853
  ```
846
854
 
@@ -1082,12 +1090,16 @@ npx securenow init
1082
1090
 
1083
1091
  ```typescript
1084
1092
  export async function register() {
1085
- if (process.env.NEXT_RUNTIME !== 'nodejs') return;
1086
-
1087
- const securenowNext = await import(/* webpackIgnore: true */ 'securenow/nextjs');
1088
- const registerSecureNow = securenowNext.registerSecureNow || securenowNext.default?.registerSecureNow;
1089
- registerSecureNow({ captureBody: true });
1090
- await import(/* webpackIgnore: true */ 'securenow/nextjs-auto-capture');
1093
+ if (process.env.NEXT_RUNTIME === 'nodejs') {
1094
+ try {
1095
+ const securenowNext = await import('securenow/nextjs');
1096
+ const registerSecureNow = securenowNext.registerSecureNow || securenowNext.default?.registerSecureNow;
1097
+ registerSecureNow({ captureBody: true });
1098
+ await import('securenow/nextjs-auto-capture');
1099
+ } catch (error) {
1100
+ console.error('[securenow] SecureNow did not start; the app is running without it:', error);
1101
+ }
1102
+ }
1091
1103
  }
1092
1104
  ```
1093
1105
 
@@ -1104,7 +1116,9 @@ const nextConfig = {
1104
1116
  export default nextConfig;
1105
1117
  ```
1106
1118
 
1107
- No `.env.local` is needed locally or in production. Local setup reads `.securenow/runtime.json`; production should use `npx securenow credentials runtime --env production` and mount/copy the resulting JSON as `.securenow/credentials.json`.
1119
+ No `.env.local` is needed locally or in production. Local setup reads `.securenow/runtime.json`; production should use `npx securenow credentials runtime --env production` and mount/copy the resulting JSON as `.securenow/credentials.json` (in a standalone Docker image, next to `server.js`). On Vercel or another host without file mounts, run `npx securenow credentials runtime --env production --format env` and store the printed `SECURENOW_CREDENTIALS` value as a secret environment variable.
1120
+
1121
+ Keep the imports inside the `nodejs` block and without `webpackIgnore`: Next.js then drops them from the Edge build and traces `securenow` and its dependencies into `output: 'standalone'`. An instrumentation file written by `init` before 8.24.0 used `webpackIgnore`, so standalone images shipped without OpenTelemetry and every request returned 500; run `npx securenow init` again to replace it.
1108
1122
 
1109
1123
  #### Option B: Manual configuration
1110
1124
 
@@ -1133,11 +1147,11 @@ module.exports = {
1133
1147
  };
1134
1148
  ```
1135
1149
 
1136
- **Why is this needed?** Next.js bundles server code with webpack, which can break OpenTelemetry's dynamic `require()` calls and monkey-patching. Externalizing `securenow` keeps the SDK as normal Node.js runtime code. `outputFileTracingIncludes` keeps SecureNow's runtime modules available in standalone/self-hosted output, including the firewall.
1150
+ **Why is this needed?** Next.js bundles server code with webpack or Turbopack, which can break OpenTelemetry's dynamic `require()` calls and monkey-patching. Externalizing `securenow` keeps the SDK as normal Node.js runtime code, and Next.js traces its dependencies into standalone output. `outputFileTracingIncludes` adds SecureNow's own files that are loaded at runtime, including the firewall.
1137
1151
 
1138
1152
  ---
1139
1153
 
1140
- ### Nuxt 3
1154
+ ### Nuxt 3 and 4
1141
1155
 
1142
1156
  ```bash
1143
1157
  npm install securenow
@@ -1151,22 +1165,11 @@ export default defineNuxtConfig({
1151
1165
  });
1152
1166
  ```
1153
1167
 
1154
- Local app identity, runtime API key, and secure defaults come from `.securenow/runtime.json`; production can mount the generated tokenless runtime file as `.securenow/credentials.json` or `.securenow/credentials.<env>.json`.
1168
+ Local app identity, runtime API key, and secure defaults come from `.securenow/runtime.json`; production can mount the generated tokenless runtime file as `.securenow/credentials.json` or `.securenow/credentials.<env>.json` next to the `.output` server, or set `SECURENOW_CREDENTIALS` (see `credentials runtime --format env`).
1155
1169
 
1156
- That's it -- the Nuxt module handles OTel SDK initialization, Nitro externalization, firewall activation, and request tracing automatically. Optional config:
1157
-
1158
- ```typescript
1159
- export default defineNuxtConfig({
1160
- modules: ['securenow/nuxt'],
1161
- securenow: {
1162
- captureBody: true,
1163
- logging: true,
1164
- noUuid: true,
1165
- },
1166
- });
1167
- ```
1170
+ That's it: the module's Nitro plugin loads the same runtime as `node -r securenow/register` (tracing with header and body capture, logs, crash capture, firewall, rate limits, challenges) and keeps `securenow` external, so Nitro copies it with its dependencies into `.output/server/node_modules`. It also records the matched route as `http.route` and handler errors as exceptions. Settings go in `.securenow/runtime.json` (`config.capture.body`, `config.logging.enabled`, ...); `securenow: { ... }` options in `nuxt.config` are no longer used, and the build prints a warning if you set them.
1168
1171
 
1169
- The Nuxt server plugin (v5.13.0+) initializes the firewall independently from OpenTelemetry, so IP blocking works even if tracing encounters an error.
1172
+ Before 8.24.0 the module's plugin crashed the `nuxt build` server at startup. Starting `.output/server/index.mjs` with `node -r securenow/register` and no module also works.
1170
1173
 
1171
1174
  ---
1172
1175
 
@@ -1174,18 +1177,19 @@ The Nuxt server plugin (v5.13.0+) initializes the firewall independently from Op
1174
1177
 
1175
1178
  | Framework | Traces | Logs | Body Capture | Firewall | Notes |
1176
1179
  |-----------|--------|------|--------------|----------|-------|
1177
- | Express | Yes | Yes | Yes | Yes | Fully compatible |
1178
- | Fastify | Yes | Yes | Yes | Yes | Default on; use `config.capture.body=false` only for local stream conflicts |
1179
- | Koa | Yes | Yes | Yes | Yes | Needs `koa-bodyparser` |
1180
- | NestJS | Yes | Yes | Yes | Yes | Use `-r ts-node/register` |
1181
- | Hapi | Yes | Yes | Yes | Yes | Default on; use `config.capture.body=false` only for local stream conflicts |
1182
- | h3 | Yes | Yes | Yes | Yes | Uses `toNodeListener()` |
1183
- | Polka | Yes | Yes | Yes | Yes | Needs manual body parser |
1184
- | Micro/HTTP | Yes | Yes | Yes | Yes | Full control |
1185
- | Hono | Yes | Yes | Yes | Yes | Use ESM `-r` preload |
1186
- | Feathers | Yes | Yes | Yes | Yes | Uses Express transport |
1187
- | Next.js | Yes | Yes | Yes | Yes | Use `instrumentation.ts` + `serverExternalPackages` + `outputFileTracingIncludes` |
1188
- | Nuxt 3 | Yes | Yes | Yes | Yes | Use `securenow/nuxt` module |
1180
+ | Express 4 and 5 | Yes | Yes | Yes | Yes | Fully compatible (CommonJS, ESM, TypeScript via tsx) |
1181
+ | Fastify 5 | Yes | Yes | Yes | Yes | `http.route` and handler exceptions recorded through Fastify's `fastify.initialization` channel (8.24.0+) |
1182
+ | Koa 3 | Yes | Yes | Yes | Yes | Body needs a body parser; `http.route` needs `@koa/router` |
1183
+ | NestJS 11 | Yes | Yes | Yes | Yes | `-r securenow/register` before `-r ts-node/register` for TypeScript |
1184
+ | Hapi 21 | Yes | Yes | Yes | Yes | Fully compatible |
1185
+ | h3 1 | Yes | Yes | Yes | Yes | Uses `toNodeListener()`; no `http.route` (raw path) |
1186
+ | Polka | Yes | Yes | Yes | Yes | Needs a body parser; no `http.route`; pass errors to `next(err)` (a thrown error crashes Polka) |
1187
+ | Micro/HTTP | Yes | Yes | Yes | Yes | Full control; no route templates |
1188
+ | Hono 4 (`@hono/node-server`) | Yes | Yes | Yes | Yes | No `http.route` (raw path); errors reach the logs through Hono's default handler |
1189
+ | Feathers 5 | Yes | Yes | Yes | Yes | Koa or Express transport; no `http.route` |
1190
+ | Next.js 15 and 16 | Yes | Yes | Yes | Yes | `instrumentation.ts` + `serverExternalPackages` + `outputFileTracingIncludes`; `next start` and `output: 'standalone'` |
1191
+ | Nuxt 3 and 4 | Yes | Yes | Yes | Yes | `securenow/nuxt` module; `nuxt build` output |
1192
+ | Bun 1.3 (`node:http` apps) | Yes | Yes | Yes | Yes | `bun --preload securenow/register`; `Bun.serve` is not supported |
1189
1193
 
1190
1194
  ---
1191
1195
 
@@ -1339,8 +1343,9 @@ SecureNow provides multiple entry points depending on your needs:
1339
1343
  |-------------|-------|-------------------|-------------------|-------|
1340
1344
  | `securenow/register` | `node -r securenow/register app.js` | Yes | Yes | Default -- full tracing + firewall |
1341
1345
  | `securenow/firewall-only` | `node -r securenow/firewall-only app.js` | No | Yes | Firewall only, no OTel overhead |
1342
- | `securenow/nextjs` | `await import(/* webpackIgnore: true */ 'securenow/nextjs')` | Yes | Yes | Next.js instrumentation hook |
1343
- | `securenow/nuxt` | `modules: ['securenow/nuxt']` | Yes | Yes | Nuxt 3 module |
1346
+ | `securenow/nextjs` | `await import('securenow/nextjs')` inside the `nodejs` block of `instrumentation.ts` | Yes | Yes | Next.js instrumentation hook |
1347
+ | `securenow/nuxt` | `modules: ['securenow/nuxt']` | Yes | Yes | Nuxt 3 and 4 module |
1348
+ | `securenow/register-vite` | `node -r securenow/register-vite node_modules/vite/bin/vite.js preview` | Yes | Yes | Same as `securenow/register`, for the Node process serving a Vite app |
1344
1349
  | `securenow/nextjs-webpack-config` | `withSecureNow(config)` | - | - | Next.js config wrapper |
1345
1350
  | `securenow/firewall` | `require('securenow/firewall').init({...})` | No | Yes | Programmatic firewall API |
1346
1351
  | `securenow/rate-limits` | `require('securenow/rate-limits').parseRateLimitText(...)` | No | Yes | Rate-limit remediation API helper |
@@ -1619,12 +1624,16 @@ const enabled: boolean = isLoggingEnabled();
1619
1624
  ```typescript
1620
1625
  // instrumentation.ts
1621
1626
  export async function register() {
1622
- if (process.env.NEXT_RUNTIME !== 'nodejs') return;
1623
-
1624
- const securenowNext = await import(/* webpackIgnore: true */ 'securenow/nextjs');
1625
- const registerSecureNow = securenowNext.registerSecureNow || securenowNext.default?.registerSecureNow;
1626
- registerSecureNow({ captureBody: true });
1627
- await import(/* webpackIgnore: true */ 'securenow/nextjs-auto-capture');
1627
+ if (process.env.NEXT_RUNTIME === 'nodejs') {
1628
+ try {
1629
+ const securenowNext = await import('securenow/nextjs');
1630
+ const registerSecureNow = securenowNext.registerSecureNow || securenowNext.default?.registerSecureNow;
1631
+ registerSecureNow({ captureBody: true });
1632
+ await import('securenow/nextjs-auto-capture');
1633
+ } catch (error) {
1634
+ console.error('[securenow] SecureNow did not start; the app is running without it:', error);
1635
+ }
1636
+ }
1628
1637
  }
1629
1638
  ```
1630
1639
 
@@ -1759,7 +1768,7 @@ SecureNow trusts private, loopback, and same-host proxy peers automatically. For
1759
1768
 
1760
1769
  **Check 6: Using PM2?**
1761
1770
 
1762
- Make sure `node_args: '-r securenow/register'` is in your `ecosystem.config.cjs`. Without it, PM2 restarts skip the SDK entirely.
1771
+ Make sure `node_args: ['-r', require.resolve('securenow/register')]` is in your `ecosystem.config.cjs`. Without it, PM2 restarts skip the SDK entirely; with a bare `'-r securenow/register'`, cluster workers started from another folder (`pm2 resurrect` at boot) crash with `Cannot find module 'securenow/register'`.
1763
1772
 
1764
1773
  ### Logs Not Appearing
1765
1774
 
@@ -1913,12 +1922,12 @@ module.exports = {
1913
1922
  name: 'my-app',
1914
1923
  script: './app.js',
1915
1924
  instances: 4,
1916
- node_args: '-r securenow/register',
1925
+ node_args: ['-r', require.resolve('securenow/register')],
1917
1926
  }]
1918
1927
  };
1919
1928
  ```
1920
1929
 
1921
- Without `node_args`, PM2 starts your script directly without the securenow preload, so neither tracing nor the firewall will be active.
1930
+ Without `node_args`, PM2 starts your script directly without the securenow preload, so neither tracing nor the firewall will be active. Use the absolute path: in cluster mode Node resolves a bare `-r securenow/register` from the PM2 daemon's folder, and the workers crash-loop when the daemon was started elsewhere (as `pm2 resurrect` does at boot).
1922
1931
 
1923
1932
  ---
1924
1933
 
package/README.md CHANGED
@@ -75,7 +75,7 @@ npx securenow login
75
75
  node -r securenow/register src/index.js
76
76
  ```
77
77
 
78
- No `.env` edits, no API keys to paste. Runtime config lives in `.securenow/runtime.json`; it is never read from environment variables.
78
+ No `.env` edits, no API keys to paste. Runtime config lives in `.securenow/runtime.json`. The only environment variables the SDK reads are `SECURENOW_CREDENTIALS`, for hosts that cannot mount a file (see Production), and the `SECURENOW_DISABLED` test switch.
79
79
 
80
80
  ```json
81
81
  "scripts": {
@@ -86,9 +86,9 @@ No `.env` edits, no API keys to paste. Runtime config lives in `.securenow/runti
86
86
 
87
87
  Can't change the script? Use `NODE_OPTIONS="-r securenow/register" npm start`, or the wrapper: `npx securenow run src/index.js`.
88
88
 
89
- **Next.js** — `npx securenow init` creates `instrumentation.ts` and patches `next.config.*` when it can do so safely (adds `securenow` to `serverExternalPackages` on Next.js 15+). If your config needs human judgment, `init` prints an agent-ready prompt with the exact edits to merge.
89
+ **Next.js** — `npx securenow init` creates `instrumentation.ts` and patches `next.config.*` when it can do so safely (adds `securenow` to `serverExternalPackages` on Next.js 15+). If your config needs human judgment, `init` prints an agent-ready prompt with the exact edits to merge. `next start` and `output: 'standalone'` (Docker) both work. If `init` wrote your instrumentation file before 8.24.0, run it again: it replaces that file, whose `webpackIgnore` imports left SecureNow's dependencies out of standalone builds.
90
90
 
91
- **Nuxt 3** — add the module, then run `npx securenow init`:
91
+ **Nuxt 3 and 4** — add the module, then run `npx securenow init`. The `nuxt build` output is fully covered (tested on Nuxt 3.21 and 4.6, with `.output` running on its own like a Docker image). In `nuxt dev`, requests reach Nitro through Nuxt's dev proxy, so the client IP is unknown there and IP blocking only applies to the built server:
92
92
 
93
93
  ```ts
94
94
  export default defineNuxtConfig({
@@ -96,15 +96,30 @@ export default defineNuxtConfig({
96
96
  });
97
97
  ```
98
98
 
99
- **What's captured automatically:** HTTP spans (Express, Fastify, NestJS, Koa, Hapi, Next.js, Nuxt, raw `http`), database spans (Postgres, MySQL, MongoDB, Redis), `console.*` as logs with trace correlation, crashes (uncaught exceptions and unhandled rejections) as `FATAL` logs with their stack, request bodies with auto-redaction, multipart metadata (never file content), plus the firewall. Tune everything in `.securenow/runtime.json` under `config`.
99
+ Settings come from `.securenow/runtime.json` like every other integration; `securenow: { ... }` options in `nuxt.config` are ignored.
100
100
 
101
- **Production** — same file structure, no env vars. Generate a tokenless runtime credentials file and mount it as a deployment secret:
101
+ **PM2** — in `ecosystem.config.cjs`, pass the preload as an absolute path: `node_args: ['-r', require.resolve('securenow/register')]`. That works in fork and cluster mode (`instances: 'max'`) wherever the PM2 daemon was started. (In cluster mode Node resolves a bare `-r securenow/register` from the daemon's folder, so after `pm2 resurrect` at boot the workers crash-loop with `Cannot find module 'securenow/register'`.) Each process reads the `.securenow/` folder of the app PM2 started.
102
+
103
+ **Bun** — `bun --preload securenow/register app.js` works for apps served through `node:http` (tested with Express 5 on Bun 1.3). `Bun.serve` is not traced or protected. Deno is not supported.
104
+
105
+ **What's captured automatically:** HTTP spans (Express, Fastify, NestJS, Koa, Hapi, Hono, Polka, Feathers, h3, Next.js, Nuxt, raw `http`), database spans (Postgres, MySQL, MongoDB, Redis), `console.*` as logs with trace correlation, crashes (uncaught exceptions and unhandled rejections) as `FATAL` logs with their stack, plus the spans that ended just before the crash, request headers and bodies with auto-redaction, multipart metadata (never file content), plus the firewall. `http.route` is recorded for Express, Fastify, NestJS, Koa with `@koa/router`, Hapi, Nuxt, and Next.js 16 (on Next.js's own span; Next.js 15 records `next.route`); Hono, Polka, Feathers and plain h3 show the raw path. Tune everything in `.securenow/runtime.json` under `config`.
106
+
107
+ **Production** — same file structure. Generate a tokenless runtime credentials file and mount it as a deployment secret (Docker, Kubernetes, VMs):
102
108
 
103
109
  ```bash
104
110
  npx securenow credentials runtime --env production
105
111
  # store the output, mount it at <app-root>/.securenow/credentials.json
106
112
  ```
107
113
 
114
+ **Hosts without file mounts** (Vercel, Railway, Render, Heroku, Fly) — print the same credentials as one environment variable and store it as a secret there:
115
+
116
+ ```bash
117
+ npx securenow credentials runtime --env production --format env
118
+ # SECURENOW_CREDENTIALS=eyJ... -> add as a secret environment variable
119
+ ```
120
+
121
+ `SECURENOW_CREDENTIALS` (raw JSON or base64) takes precedence over credential files.
122
+
108
123
  **Verify** any environment with:
109
124
 
110
125
  ```bash
@@ -113,7 +128,7 @@ npx securenow doctor # diagnose config + connectivity
113
128
  npx securenow status # dashboard summary
114
129
  ```
115
130
 
116
- **Tests** — test runs otherwise report to your real app with the project's `.securenow` credentials. `SECURENOW_DISABLED=1 npm test` turns the SDK off for that process: no traces, logs, events, firewall or session sync, and `guard()` just calls `next()`. This is the only setting read from the environment.
131
+ **Tests** — test runs otherwise report to your real app with the project's `.securenow` credentials. `SECURENOW_DISABLED=1 npm test` turns the SDK off for that process: no traces, logs, events, firewall or session sync, and `guard()` just calls `next()`. Apart from `SECURENOW_CREDENTIALS`, this is the only setting read from the environment.
117
132
 
118
133
  ---
119
134
 
package/SKILL-API.md CHANGED
@@ -119,8 +119,8 @@ operator needs to ensure those defaults immediately.
119
119
  | `securenow/cidr` | CIDR utilities; exports `createMatcher()`, `ipToInt()`, `parseCidr()`, `matchesCidr()` | CJS |
120
120
  | `securenow/resolve-ip` | IP resolution; exports `resolveClientIp()`, `resolveSocketIp()`, `isFromTrustedProxy()` | CJS |
121
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()` | ESM |
123
- | `securenow/register-vite` | CJS bridge for Vite preload | 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
124
 
125
125
  ---
126
126
 
@@ -149,8 +149,10 @@ module.exports = {
149
149
  name: 'my-app',
150
150
  script: './app.js',
151
151
  instances: 4,
152
- node_args: '-r securenow/register',
153
- // Reads .securenow/runtime.json from the project root.
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.
154
156
  }],
155
157
  };
156
158
  ```
@@ -187,12 +189,16 @@ For Next.js < 15, add `securenow` to `experimental.serverComponentsExternalPacka
187
189
 
188
190
  ```typescript
189
191
  export async function register() {
190
- if (process.env.NEXT_RUNTIME !== 'nodejs') return;
191
-
192
- const securenowNext = await import(/* webpackIgnore: true */ 'securenow/nextjs');
193
- const registerSecureNow = securenowNext.registerSecureNow || securenowNext.default?.registerSecureNow;
194
- registerSecureNow({ captureBody: true });
195
- await import(/* webpackIgnore: true */ 'securenow/nextjs-auto-capture');
192
+ if (process.env.NEXT_RUNTIME === 'nodejs') {
193
+ try {
194
+ const securenowNext = await import('securenow/nextjs');
195
+ const registerSecureNow = securenowNext.registerSecureNow || securenowNext.default?.registerSecureNow;
196
+ registerSecureNow({ captureBody: true });
197
+ await import('securenow/nextjs-auto-capture');
198
+ } catch (error) {
199
+ console.error('[securenow] SecureNow did not start; the app is running without it:', error);
200
+ }
201
+ }
196
202
  }
197
203
  ```
198
204
 
@@ -236,12 +242,16 @@ Add to your `instrumentation.ts`:
236
242
 
237
243
  ```typescript
238
244
  export async function register() {
239
- if (process.env.NEXT_RUNTIME !== 'nodejs') return;
240
-
241
- const securenowNext = await import(/* webpackIgnore: true */ 'securenow/nextjs');
242
- const registerSecureNow = securenowNext.registerSecureNow || securenowNext.default?.registerSecureNow;
243
- registerSecureNow({ captureBody: true });
244
- await import(/* webpackIgnore: true */ 'securenow/nextjs-auto-capture');
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
+ }
245
255
  }
246
256
  ```
247
257
 
@@ -270,39 +280,33 @@ npx securenow app connect
270
280
  npx securenow init
271
281
  ```
272
282
 
273
- Auto-detects Next.js, creates `instrumentation.ts`, adds `serverExternalPackages: ['securenow']` plus `outputFileTracingIncludes` when safe, and reuses the app, instance, runtime API key, and secure defaults in `.securenow/runtime.json`. If files already exist, it prints an agent-ready prompt with the exact edits to propose.
283
+ Auto-detects Next.js, creates `instrumentation.ts`, adds `serverExternalPackages: ['securenow']` plus `outputFileTracingIncludes` when safe, and reuses the app, instance, runtime API key, and secure defaults in `.securenow/runtime.json`. An instrumentation file it generated before 8.24.0 is replaced automatically (its `webpackIgnore` imports left SecureNow's dependencies out of standalone builds); for any other existing file it prints an agent-ready prompt with the exact edits to propose.
274
284
 
275
285
  ---
276
286
 
277
- ### Nuxt 3
287
+ ### Nuxt 3 and 4
278
288
 
279
289
  **`nuxt.config.ts`:**
280
290
 
281
291
  ```typescript
282
292
  export default defineNuxtConfig({
283
293
  modules: ['securenow/nuxt'],
284
- securenow: {
285
- // optional overrides (defaults come from .securenow/runtime.json)
286
- },
287
294
  });
288
295
  ```
289
296
 
290
- The Nuxt module auto-configures Nitro externals, runtime config, and a server plugin that sets up OTel tracing + logging + firewall. Local app identity, runtime API key, and secure defaults come from `.securenow/runtime.json`; production can use tokenless `.securenow/credentials.<env>.json`.
297
+ The Nuxt module keeps `securenow` external (Nitro copies it with its dependencies into `.output/server/node_modules`) and adds a server plugin that loads the same runtime as `node -r securenow/register`: tracing with header/body capture, logging, crash capture, firewall, rate limits and challenges, plus `http.route` from the matched route and handler exceptions. Settings come from `.securenow/runtime.json` (production: tokenless `.securenow/credentials.<env>.json` or `SECURENOW_CREDENTIALS`); `securenow: { ... }` options in `nuxt.config` are no longer used. Before 8.24.0 this plugin crashed the `nuxt build` server at startup.
291
298
 
292
299
  ---
293
300
 
294
- ### Vite / Browser
301
+ ### Vite
295
302
 
296
- ```javascript
297
- import startSecurenowWeb from 'securenow/web-vite';
303
+ Instrument the Node process that serves the app (`vite preview`, a Vite SSR server):
298
304
 
299
- startSecurenowWeb({
300
- serviceName: 'my-frontend',
301
- endpoint: 'https://ingest.securenow.ai',
302
- });
305
+ ```bash
306
+ node -r securenow/register-vite node_modules/vite/bin/vite.js preview
303
307
  ```
304
308
 
305
- Instruments document load, fetch, XMLHttpRequest, and user interactions with browser-side OpenTelemetry.
309
+ `securenow/register-vite` is the same runtime as `securenow/register`. Browser-side telemetry (`securenow/web-vite`) is not sent to SecureNow: browser ingestion is disabled since 8.0 until browser-safe auth exists.
306
310
 
307
311
  ---
308
312
 
package/SKILL-CLI.md CHANGED
@@ -208,10 +208,28 @@ Auto-detects framework (Next.js, Nuxt, Express, Fastify, Koa, Hapi, Node) from `
208
208
  ### Dashboard & Status
209
209
 
210
210
  ```bash
211
+ securenow today [--since 24h|7d] [--attackers 20] [--json] # the Overview: what happened, what SecureNow did, what needs you
212
+ securenow attacker <ip> [--json] # why an address was flagged: verdict, signals, traffic vs normal, detections, timeline
211
213
  securenow status [--app <key>] [--env local|production|all] # dashboard overview
212
214
  securenow analytics [--app <key>] # response analytics
213
215
  ```
214
216
 
217
+ `today` is the dashboard's home in the terminal:
218
+ - whether each app is covered (data flowing, firewall on, detection monitoring or blocking);
219
+ - attack requests, attackers, what the firewall stopped and what got through;
220
+ - the decisions waiting for you, the top attackers, and what SecureNow did (the agent's actions, automatic blocks,
221
+ closed probes).
222
+
223
+ `attacker <ip>` explains one address:
224
+ - the verdict and score with the signals behind them, and its reputation;
225
+ - where it is blocked;
226
+ - its last 3 days of traffic against your apps' normal (error rate, share of traffic, scanner probes, attack
227
+ payloads answered 2xx);
228
+ - the detections it triggered and what happened to each, a timeline, and questions to paste into
229
+ `securenow forensics chat`.
230
+
231
+ MCP: `securenow_overview` and `securenow_attacker` (read).
232
+
215
233
  ---
216
234
 
217
235
  ### Traces
package/app-config.js CHANGED
@@ -8,14 +8,16 @@
8
8
  * Named runtime files such as ./.securenow/credentials.staging.json and
9
9
  * ./.securenow/credentials.production.json are also accepted when the
10
10
  * canonical file is not present. Filename selection is deterministic and does
11
- * not read environment variables.
11
+ * not depend on NODE_ENV.
12
12
  * Every SDK setting has a file-backed equivalent so customers do not need
13
- * .env files.
13
+ * .env files. Hosts that cannot mount a file (Vercel, Railway, Render, Heroku,
14
+ * Fly) pass the same JSON in one variable, SECURENOW_CREDENTIALS.
14
15
  */
15
16
 
16
17
  const fs = require('fs');
17
18
  const path = require('path');
18
19
  const os = require('os');
20
+ const { readPm2Env } = require('./pm2-env');
19
21
 
20
22
  // Shared SecureNow Cloud ingestion gateway (production collector for every
21
23
  // paying account and for the 14-day trial).
@@ -240,7 +242,20 @@ function credentialRelativePaths() {
240
242
  return runtimeCredentialRelativePaths();
241
243
  }
242
244
 
243
- function resolveLocalFileFromRelativePaths(relativePaths) {
245
+ // Where to look for .securenow/ files and package.json. A PM2 process is searched from the app
246
+ // folder PM2 recorded and nowhere else: in cluster mode the worker's working directory is still
247
+ // the PM2 daemon's when the preload runs, so searching from it found nothing, or another
248
+ // project's credentials if the daemon was started inside that project.
249
+ function searchStartDirs() {
250
+ const pm2 = readPm2Env();
251
+ if (pm2 && (pm2.cwd || pm2.execPath)) {
252
+ const appDir = pm2.cwd ? path.resolve(pm2.cwd) : null;
253
+ return uniq([
254
+ appDir,
255
+ pm2.execPath ? path.dirname(path.resolve(appDir || '.', pm2.execPath)) : null,
256
+ ]);
257
+ }
258
+
244
259
  const starts = [];
245
260
  try {
246
261
  if (typeof process !== 'undefined' && process.cwd) starts.push(process.cwd());
@@ -250,8 +265,12 @@ function resolveLocalFileFromRelativePaths(relativePaths) {
250
265
  if (process.argv && process.argv[1]) starts.push(path.dirname(process.argv[1]));
251
266
  if (require.main && require.main.filename) starts.push(path.dirname(require.main.filename));
252
267
 
268
+ return uniq(starts);
269
+ }
270
+
271
+ function resolveLocalFileFromRelativePaths(relativePaths) {
253
272
  const candidates = relativePaths;
254
- for (const start of uniq(starts)) {
273
+ for (const start of searchStartDirs()) {
255
274
  const found = findUpFirstFile(start, candidates);
256
275
  if (found) return found;
257
276
  }
@@ -314,16 +333,7 @@ function runtimeCredentialsFromDocument(credentials) {
314
333
  }
315
334
 
316
335
  function resolvePackageJsonFile() {
317
- const starts = [];
318
- try {
319
- if (typeof process !== 'undefined' && process.cwd) starts.push(process.cwd());
320
- } catch {}
321
-
322
- if (process.env.INIT_CWD) starts.push(process.env.INIT_CWD);
323
- if (process.argv && process.argv[1]) starts.push(path.dirname(process.argv[1]));
324
- if (require.main && require.main.filename) starts.push(path.dirname(require.main.filename));
325
-
326
- for (const start of uniq(starts)) {
336
+ for (const start of searchStartDirs()) {
327
337
  const found = findUpFile(start, 'package.json');
328
338
  if (found) return found;
329
339
  }
@@ -347,7 +357,50 @@ function loadGlobalCredentials() {
347
357
  }
348
358
  }
349
359
 
360
+ const ENV_CREDENTIALS_VAR = 'SECURENOW_CREDENTIALS';
361
+ let _envCredentials = { raw: undefined, value: null };
362
+ let _warnedBadEnvCredentials = false;
363
+
364
+ // Runtime credentials passed in the environment, for hosts that cannot mount a file. The value is
365
+ // the JSON written by `npx securenow credentials runtime`, raw or base64-encoded (`--format env`
366
+ // prints it ready to paste). When set it takes precedence over credential files.
367
+ function loadEnvCredentials() {
368
+ const raw = typeof process !== 'undefined' && process.env ? process.env[ENV_CREDENTIALS_VAR] : undefined;
369
+ if (raw !== _envCredentials.raw) {
370
+ let value = null;
371
+ const text = raw == null ? '' : String(raw).trim();
372
+ if (text) {
373
+ try {
374
+ const json = text.startsWith('{') ? text : Buffer.from(text, 'base64').toString('utf8');
375
+ value = withCredentialDefaults(runtimeCredentialsFromDocument(JSON.parse(json.replace(/^\uFEFF/, ''))));
376
+ } catch {
377
+ value = null;
378
+ }
379
+ if (!value && !_warnedBadEnvCredentials) {
380
+ _warnedBadEnvCredentials = true;
381
+ try {
382
+ console.warn('[securenow] %s is set but is not runtime credentials JSON (raw or base64); ignoring it.', ENV_CREDENTIALS_VAR);
383
+ } catch {}
384
+ }
385
+ }
386
+ _envCredentials = { raw, value };
387
+ }
388
+ return _envCredentials.value ? clone(_envCredentials.value) : null;
389
+ }
390
+
391
+ // Where the runtime credentials came from, for the startup log: never their content.
392
+ function describeCredentialsSource() {
393
+ try {
394
+ if (loadEnvCredentials()) return `env:${ENV_CREDENTIALS_VAR}`;
395
+ return resolveLocalCredentialsFile() || resolveGlobalCredentialsFile() || 'none';
396
+ } catch {
397
+ return 'none';
398
+ }
399
+ }
400
+
350
401
  function loadCredentials() {
402
+ const fromEnv = loadEnvCredentials();
403
+ if (fromEnv) return fromEnv;
351
404
  try {
352
405
  const localFile = resolveLocalCredentialsFile();
353
406
  if (localFile) {
@@ -781,6 +834,10 @@ module.exports = {
781
834
  resolveLocalAdminCredentialsFile,
782
835
  resolveGlobalAdminCredentialsFile,
783
836
  loadCredentials,
837
+ loadEnvCredentials,
838
+ describeCredentialsSource,
839
+ searchStartDirs,
840
+ ENV_CREDENTIALS_VAR,
784
841
  loadLocalCredentials,
785
842
  loadGlobalCredentials,
786
843
  loadPackageJsonName,
@@ -73,6 +73,17 @@ async function runtime(_args, flags) {
73
73
  warn('Runtime API key is missing. Run `npx securenow app connect` or `npx securenow api-key create` before generating production runtime credentials.');
74
74
  }
75
75
 
76
+ // For hosts that inject environment variables but cannot mount a file (Vercel, Railway,
77
+ // Render, Heroku, Fly): one line to paste as a secret. The SDK reads SECURENOW_CREDENTIALS
78
+ // (raw JSON or base64) before any credentials file.
79
+ if (flags.format === 'env' || flags['env-var']) {
80
+ const { ENV_CREDENTIALS_VAR } = appConfig;
81
+ const encoded = Buffer.from(JSON.stringify(creds), 'utf8').toString('base64');
82
+ console.log(`${ENV_CREDENTIALS_VAR}=${encoded}`);
83
+ console.error(`! This line contains the runtime API key. Store it as a secret variable named ${ENV_CREDENTIALS_VAR}; do not commit it.`);
84
+ return;
85
+ }
86
+
76
87
  if (flags.stdout) {
77
88
  console.log(JSON.stringify(creds, null, 2));
78
89
  return;