# SecureNow SDK — Agent Skill

Instrument any Node.js application with OpenTelemetry tracing, structured logging, request body capture, and a multi-layer IP firewall. Supports Express, Fastify, NestJS, Koa, Hapi, Next.js, Nuxt 3, Vite (browser), and raw `http.createServer` — with zero code changes for most setups.

**CLI parity:** every capability exposed below (redaction, CIDR matching, log/span emission, firewall preload, config inspection) has an equivalent `securenow` CLI command. See [SKILL-CLI.md](./SKILL-CLI.md) for the terminal surface.

## Installation

```bash
npm install securenow
```

### Install This Skill in Cursor

Save this file as `.cursor/skills/securenow-api/SKILL.md` in your project. Your AI agent will auto-discover it whenever you ask about integrating securenow, configuring tracing, setting up the firewall, or instrumenting any framework.

## Quick Start — Any Node.js Framework

### 1. Set Environment Variables

```bash
# .env or .env.local
SECURENOW_APPID=my-app
SECURENOW_INSTANCE=https://your-collector:4318
```

### 2. Run With Instrumentation

**Option A — CLI (recommended):**

```bash
npx securenow run src/index.js
```

**Option B — Node preload flag:**

```bash
node -r securenow/register src/index.js
```

**Option C — Auto-setup for Next.js:**

```bash
npx securenow init --key snk_live_...
```

That's it. Traces and logs flow to your OTLP collector. No code changes for Express, Fastify, NestJS, Koa, Hapi, and raw Node.

### 3. Enable the Firewall (Optional)

Since v7.1.0 the firewall key lives in your credentials file — no env var required:

```bash
npx securenow login              # pick app + click "Enable firewall" in browser
# or, if you already have one:
npx securenow api-key set snk_live_abc123...
```

Both paths write the key to `.securenow/credentials.json` (auto-gitignored) and the firewall activates on next start. Setting `SECURENOW_API_KEY=snk_live_...` in the environment still works and takes precedence.

The firewall syncs your blocklist and enforces it on every request — zero code changes.

---

## Import Map

| Import | Purpose | Type |
|--------|---------|------|
| `securenow` / `securenow/register` | Auto-register tracing + firewall (side-effect) | Preload (`-r`) |
| `securenow/tracing` | Core OTel SDK; exports `getLogger()`, `isLoggingEnabled()` | CJS |
| `securenow/nextjs` | Next.js instrumentation; exports `registerSecureNow(options?)` | CJS |
| `securenow/nextjs-webpack-config` | Next.js config wrapper; exports `withSecureNow()`, `getSecureNowWebpackConfig()`, `EXTERNAL_PACKAGES` | CJS |
| `securenow/nextjs-middleware` | Edge middleware body capture; exports `middleware()`, `redactSensitiveData()`, `DEFAULT_SENSITIVE_FIELDS` | CJS |
| `securenow/nextjs-wrapper` | Route handler wrappers; exports `withSecureNow()`, `withSecureNowAsync()`, `captureRequestBody()`, `redactSensitiveData()` | CJS |
| `securenow/nextjs-auto-capture` | Auto-patch Next request for body capture; exports `patchNextRequest()`, `safeBodyCapture()`, `redactSensitiveData()`, `isBodyCaptureEnabled()` | CJS |
| `securenow/nuxt` | Nuxt 3 module (add to `modules` array) | ESM |
| `securenow/firewall` | Standalone firewall; exports `init()`, `shutdown()`, `getStats()`, `getMatcher()`, `getAllowlistMatcher()` | CJS |
| `securenow/firewall-only` | Preload: dotenv + firewall only, no tracing | Preload (`-r`) |
| `securenow/cidr` | CIDR utilities; exports `createMatcher()`, `ipToInt()`, `parseCidr()`, `matchesCidr()` | CJS |
| `securenow/resolve-ip` | IP resolution; exports `resolveClientIp()`, `resolveSocketIp()`, `isFromTrustedProxy()` | CJS |
| `securenow/console-instrumentation` | Console→OTLP bridge; exports `originalConsole`, `restoreConsole()` | CJS |
| `securenow/web-vite` | Browser OTel (document load, fetch, XHR, user interaction); default export `startSecurenowWeb()` | ESM |
| `securenow/register-vite` | CJS bridge for Vite preload | CJS |

---

## Framework Integration Guides

### Express / Fastify / NestJS / Koa / Hapi / Raw Node

No code changes. Use the preload:

```bash
node -r securenow/register app.js
```

Or with the CLI:

```bash
npx securenow run app.js
```

**PM2:**

```javascript
// ecosystem.config.cjs
module.exports = {
  apps: [{
    name: 'my-app',
    script: './app.js',
    instances: 4,
    node_args: '-r securenow/register',
    env: {
      SECURENOW_APPID: 'my-app',
      SECURENOW_INSTANCE: 'https://your-collector:4318',
      SECURENOW_NO_UUID: '1',
    },
  }],
};
```

**Docker:**

```dockerfile
ENV SECURENOW_APPID=my-app
ENV SECURENOW_INSTANCE=https://your-collector:4318
CMD ["node", "-r", "securenow/register", "app.js"]
```

---

### Next.js

Three files to touch:

**1. `next.config.js`** (or `.mjs` / `.ts`):

```javascript
const { withSecureNow } = require('securenow/nextjs-webpack-config');

module.exports = withSecureNow({
  // your existing config
});
```

`withSecureNow()` auto-detects Next.js version:
- **>=15**: sets `serverExternalPackages`
- **<15**: sets `experimental.serverComponentsExternalPackages` + `experimental.instrumentationHook` + webpack ignore rules

**2. `instrumentation.ts`** (or `.js`, can be in `src/`):

```typescript
export async function register() {
  if (process.env.NEXT_RUNTIME === 'nodejs') {
    const { registerSecureNow } = require('securenow/nextjs');
    registerSecureNow();
  }
}
```

`registerSecureNow(options?)` accepts:

```typescript
interface RegisterOptions {
  serviceName?: string;   // override SECURENOW_APPID
  endpoint?: string;      // override SECURENOW_INSTANCE
  noUuid?: boolean;       // override SECURENOW_NO_UUID
  captureBody?: boolean;  // override SECURENOW_CAPTURE_BODY
}
```

On Vercel it uses `@vercel/otel`; self-hosted uses vanilla `@opentelemetry/sdk-node`.

**3. `.env.local`:**

```bash
SECURENOW_APPID=my-nextjs-app
SECURENOW_INSTANCE=https://your-collector:4318
```

#### Next.js Body Capture

**Option A — Auto-capture (recommended):**

Add to your `instrumentation.ts`:

```typescript
export async function register() {
  if (process.env.NEXT_RUNTIME === 'nodejs') {
    const { registerSecureNow } = require('securenow/nextjs');
    registerSecureNow({ captureBody: true });
    require('securenow/nextjs-auto-capture');
  }
}
```

**Option B — Middleware:**

```typescript
// middleware.ts
export { middleware } from 'securenow/nextjs-middleware';
export const config = { matcher: ['/api/:path*'] };
```

**Option C — Per-route wrapper:**

```typescript
import { withSecureNow } from 'securenow/nextjs-wrapper';

export const POST = withSecureNow(async (req) => {
  // handler
});
```

#### Next.js with `securenow init`

```bash
npx securenow init --key snk_live_abc123...
```

Auto-detects Next.js, creates `instrumentation.ts`, suggests `next.config` changes, writes API key to `.env.local`.

---

### Nuxt 3

**`nuxt.config.ts`:**

```typescript
export default defineNuxtConfig({
  modules: ['securenow/nuxt'],
  securenow: {
    // optional overrides (defaults come from env vars)
  },
});
```

**`.env`:**

```bash
SECURENOW_APPID=my-nuxt-app
SECURENOW_INSTANCE=https://your-collector:4318
```

The Nuxt module auto-configures Nitro externals, runtime config, and a server plugin that sets up OTel tracing + logging + firewall.

---

### Vite / Browser

```javascript
import startSecurenowWeb from 'securenow/web-vite';

startSecurenowWeb({
  serviceName: 'my-frontend',
  endpoint: 'https://your-collector:4318',
});
```

Instruments document load, fetch, XMLHttpRequest, and user interactions with browser-side OpenTelemetry.

---

## Firewall — Multi-Layer IP Blocking

The firewall auto-activates once an API key is resolvable. Since **v7.1.0** the key is read from `.securenow/credentials.json` (written by `npx securenow login` or `securenow api-key set`), so the `SECURENOW_API_KEY` env var is optional. Resolution order: env (must start with `snk_live_`) → project `./.securenow/credentials.json` → global `~/.securenow/credentials.json`.

```
Layer 4: Cloud/Edge WAF    →  blocked at CDN (Cloudflare, AWS WAF, GCP Cloud Armor)
Layer 3: OS Firewall       →  kernel-level DROP (iptables/nftables)
Layer 2: TCP Socket        →  socket.destroy() before HTTP parsing
Layer 1: HTTP Handler      →  403 JSON response (always active)
```

### Activate

```bash
# Zero-config (recommended) — writes the key to .securenow/credentials.json
npx securenow login              # pick app + click "Enable firewall"
# or, if you already have a key:
npx securenow api-key set snk_live_abc123...

# Optional .env overrides for the stronger layers
SECURENOW_FIREWALL_TCP=1                  # opt-in Layer 2
SECURENOW_FIREWALL_IPTABLES=1             # opt-in Layer 3 (Linux, needs root)
SECURENOW_FIREWALL_CLOUD=cloudflare       # opt-in Layer 4
# SECURENOW_API_KEY=snk_live_...          # still honored; only wins if it starts with snk_live_
```

### Firewall-Only Mode (No Tracing Overhead)

```bash
node -r securenow/firewall-only app.js

# Or via the CLI (same effect)
securenow run --firewall-only app.js
```

Loads only dotenv + firewall. No OpenTelemetry, no tracing, no external packages needed.

### Programmatic Firewall API

```javascript
const firewall = require('securenow/firewall');

await firewall.init({
  apiKey: process.env.SECURENOW_API_KEY,
  apiUrl: 'https://api.securenow.ai',
  syncInterval: 300,        // full sync every 5 min
  versionCheckInterval: 10, // lightweight version check every 10s
  failMode: 'open',         // 'open' or 'closed'
  statusCode: 403,
  log: true,
  tcp: false,
  iptables: false,
  cloud: null,              // 'cloudflare' | 'aws' | 'gcp'
});

const stats = firewall.getStats();
const matcher = firewall.getMatcher();       // (ip) => boolean
const allowMatcher = firewall.getAllowlistMatcher();

await firewall.shutdown();
```

### Cloud WAF Providers

**Cloudflare:**
```bash
SECURENOW_FIREWALL_CLOUD=cloudflare
CLOUDFLARE_API_TOKEN=your-token
CLOUDFLARE_ACCOUNT_ID=your-account-id
```

**AWS WAF:**
```bash
SECURENOW_FIREWALL_CLOUD=aws
AWS_WAF_IP_SET_ID=your-ip-set-id
# + standard AWS credentials (env, profile, or IAM role)
```
Requires peer dep: `npm install @aws-sdk/client-wafv2`

**GCP Cloud Armor:**
```bash
SECURENOW_FIREWALL_CLOUD=gcp
GCP_PROJECT_ID=your-project
GCP_SECURITY_POLICY=your-policy-name
```
Requires peer dep: `npm install @google-cloud/compute`

**Dry-run (log only, no actual WAF changes):**
```bash
SECURENOW_FIREWALL_CLOUD_DRY_RUN=1
```

---

## Logging

Logging is enabled by default (`SECURENOW_LOGGING_ENABLED=1`). Logs are exported to your OTLP collector alongside traces.

### Get a Logger

```javascript
const { getLogger } = require('securenow/tracing');

const logger = getLogger('my-module', '1.0.0');
if (logger) {
  logger.emit({ body: 'User login succeeded', severityText: 'INFO', attributes: { userId: '123' } });
}
```

**CLI equivalent** (for shell scripts, cron, CI):

```bash
securenow log send "User login succeeded" --level info --attrs userId=123
securenow test-span "ci.smoke-test"          # emit a span without booting the SDK
```

### Console Instrumentation

When tracing is active, `console.log/warn/error` are automatically patched to emit OTLP log records correlated with the active trace span. To access the original console:

```javascript
const { originalConsole, restoreConsole } = require('securenow/console-instrumentation');
originalConsole.log('This bypasses OTLP');
restoreConsole(); // undo the patch
```

---

## IP Resolution Utilities

```javascript
const { resolveClientIp, resolveSocketIp, isFromTrustedProxy } = require('securenow/resolve-ip');

// From an HTTP request (respects X-Forwarded-For from trusted proxies)
const clientIp = resolveClientIp(req);

// Direct socket IP
const socketIp = resolveSocketIp(req);

// Check if request comes from a trusted proxy
const trusted = isFromTrustedProxy(req);
```

### CIDR Matching

```javascript
const { createMatcher, ipToInt, parseCidr, matchesCidr } = require('securenow/cidr');

const isBlocked = createMatcher(['10.0.0.0/8', '192.168.1.0/24']);
isBlocked('10.0.0.5');   // true
isBlocked('8.8.8.8');    // false
```

**CLI equivalent:**

```bash
securenow cidr match 10.0.0.5 10.0.0.0/8,192.168.1.0/24    # exit 0 = match, 2 = miss
securenow cidr parse 10.0.0.0/8                             # network/broadcast/mask/size
```

---

## Sensitive Data Redaction

Available from multiple entry points (`securenow/nextjs-middleware`, `securenow/nextjs-wrapper`, `securenow/nextjs-auto-capture`):

```javascript
const { redactSensitiveData, DEFAULT_SENSITIVE_FIELDS } = require('securenow/nextjs-middleware');

const safe = redactSensitiveData({ username: 'alice', password: 's3cret', token: 'abc' });
// { username: 'alice', password: '[REDACTED]', token: '[REDACTED]' }
```

**Auto-redacted fields:** `password`, `passwd`, `pwd`, `secret`, `token`, `api_key`, `apikey`, `access_token`, `auth`, `credentials`, `mysql_pwd`, `stripeToken`, `card`, `cardnumber`, `ccv`, `cvc`, `cvv`, `ssn`, `pin`.

Add custom fields via `SECURENOW_SENSITIVE_FIELDS=field1,field2`.

**CLI equivalent** (for piping, scripts, debugging what a payload looks like post-redaction):

```bash
securenow redact '{"user":"alice","password":"s3cret"}'
securenow redact @request.json --fields internal_id,sessionHash
```

---

## Environment Variables — Complete Reference

### Required

| Variable | Description | Default |
|----------|-------------|---------|
| `SECURENOW_APPID` | Service name / app identifier | *(auto-generated with UUID)* |
| `SECURENOW_INSTANCE` | OTLP collector base URL | `https://freetrial.securenow.ai:4318` |

### Service Naming

| Variable | Description | Default |
|----------|-------------|---------|
| `OTEL_SERVICE_NAME` | OpenTelemetry standard; overrides `SECURENOW_APPID` | — |
| `SECURENOW_NO_UUID` | `1` to use exact app ID without UUID suffix | `0` |
| `SECURENOW_STRICT` | `1` to exit if APPID missing in PM2 cluster | `0` |

### OTLP Connection

| Variable | Description | Default |
|----------|-------------|---------|
| `OTEL_EXPORTER_OTLP_ENDPOINT` | Standard OTel endpoint; overrides `SECURENOW_INSTANCE` | — |
| `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` | Override traces endpoint specifically | `{instance}/v1/traces` |
| `OTEL_EXPORTER_OTLP_LOGS_ENDPOINT` | Override logs endpoint specifically | `{instance}/v1/logs` |
| `OTEL_EXPORTER_OTLP_HEADERS` | Comma-separated `key=value` headers for OTLP requests | — |

### Behavior

| Variable | Description | Default |
|----------|-------------|---------|
| `SECURENOW_LOGGING_ENABLED` | Enable OTLP log export | `1` |
| `SECURENOW_CAPTURE_BODY` | Capture HTTP request bodies | `0` |
| `SECURENOW_MAX_BODY_SIZE` | Max body size in bytes | `10240` |
| `SECURENOW_CAPTURE_MULTIPART` | Capture multipart/form-data (streaming, metadata only) | `0` |
| `SECURENOW_SENSITIVE_FIELDS` | Comma-separated extra fields to redact | — |
| `SECURENOW_DISABLE_INSTRUMENTATIONS` | Comma-separated packages to skip (e.g. `fs,dns`) | — |
| `SECURENOW_TEST_SPAN` | `1` to emit a test span on startup | `0` |
| `SECURENOW_HIDE_BANNER` | `1` to suppress free-trial upgrade banner | `0` |
| `OTEL_LOG_LEVEL` | SDK log level: `none`, `error`, `warn`, `info`, `debug` | `none` |
| `NODE_ENV` | Sent as `deployment.environment` attribute | `production` |

### Firewall

| Variable | Description | Default |
|----------|-------------|---------|
| `SECURENOW_API_KEY` | API key (`snk_live_...`); activates firewall when set. Since v7.1.0, this is also read from `.securenow/credentials.json` — env var only wins when it starts with `snk_live_`. | — |
| `SECURENOW_API_URL` | SecureNow API base URL | `https://api.securenow.ai` |
| `SECURENOW_FIREWALL_ENABLED` | Master kill-switch (`0` to disable) | `1` |
| `SECURENOW_FIREWALL_VERSION_INTERVAL` | Seconds between lightweight version checks | `10` |
| `SECURENOW_FIREWALL_SYNC_INTERVAL` | Full blocklist refresh interval in seconds | `300` |
| `SECURENOW_FIREWALL_FAIL_MODE` | `open` (allow all when unavailable) or `closed` | `open` |
| `SECURENOW_FIREWALL_STATUS_CODE` | HTTP status for blocked requests | `403` |
| `SECURENOW_FIREWALL_LOG` | Log blocked requests | `1` |
| `SECURENOW_FIREWALL_TCP` | Enable Layer 2 TCP blocking | `0` |
| `SECURENOW_FIREWALL_IPTABLES` | Enable Layer 3 iptables/nftables | `0` |
| `SECURENOW_FIREWALL_CLOUD` | Cloud WAF: `cloudflare`, `aws`, or `gcp` | — |
| `SECURENOW_FIREWALL_CLOUD_DRY_RUN` | `1` to log cloud pushes without applying | `0` |
| `SECURENOW_TRUSTED_PROXIES` | Comma-separated trusted proxy IPs | — |

**Resilience:** The firewall SDK includes a circuit breaker (opens after 5 consecutive errors, 2-min cooldown), in-flight request guards (prevents overlapping requests), 429 Retry-After support, and exponential backoff on both version checks and initial sync retries.

### Cloud WAF Provider Variables

| Provider | Variables |
|----------|-----------|
| Cloudflare | `CLOUDFLARE_API_TOKEN`, `CLOUDFLARE_ACCOUNT_ID` |
| AWS WAF | `AWS_WAF_IP_SET_ID`, `AWS_WAF_IP_SET_NAME`, `AWS_WAF_SCOPE` |
| GCP | `GCP_PROJECT_ID`, `GCP_SECURITY_POLICY` |

### Priority Order

**Service name:** `OTEL_SERVICE_NAME` > `SECURENOW_APPID` > auto-generated

**Endpoint:** `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` > `OTEL_EXPORTER_OTLP_ENDPOINT` > `SECURENOW_INSTANCE` > `https://freetrial.securenow.ai:4318`

---

## Recipes for Agentic AI

### Add Observability to an Existing Express App

```bash
npm install securenow
```

Create `.env`:
```
SECURENOW_APPID=my-express-api
SECURENOW_INSTANCE=https://your-collector:4318
SECURENOW_LOGGING_ENABLED=1
SECURENOW_CAPTURE_BODY=1
```

Update `package.json`:
```json
{ "scripts": { "start": "node -r securenow/register src/index.js" } }
```

No code changes to the application needed.

### Add Observability + Firewall to a Next.js App

```bash
npm install securenow
npx securenow login              # pick app + "Enable firewall" in browser
```

`securenow login` writes session, app, and firewall key to `.securenow/credentials.json` (auto-gitignored). The `init` command still works for manual setup — it creates `instrumentation.ts` and suggests `next.config` changes. Most users only need this `.env.local`:

```
SECURENOW_APPID=my-nextjs-app
SECURENOW_INSTANCE=https://your-collector:4318
SECURENOW_CAPTURE_BODY=1
# SECURENOW_API_KEY=snk_live_...   (otherwise lives in .securenow/credentials.json)
```

### Enable Firewall With Zero Tracing Overhead

For apps that only need IP blocking:

```bash
node -r securenow/firewall-only app.js
```

Make sure an API key is resolvable — either run `npx securenow login` / `securenow api-key set snk_live_...` (writes the creds file), or set `SECURENOW_API_KEY` in the environment. No other configuration needed.

### Production Hardened Configuration

```bash
SECURENOW_APPID=prod-api
SECURENOW_INSTANCE=https://collector.prod.internal:4318
SECURENOW_NO_UUID=1
SECURENOW_STRICT=1
SECURENOW_LOGGING_ENABLED=1
SECURENOW_CAPTURE_BODY=0
SECURENOW_API_KEY=snk_live_abc123...
SECURENOW_FIREWALL_TCP=1
SECURENOW_FIREWALL_SYNC_INTERVAL=30
SECURENOW_FIREWALL_FAIL_MODE=open
OTEL_LOG_LEVEL=error
NODE_ENV=production
```

### Instrument a Docker Container

```dockerfile
FROM node:20-slim
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .

ENV SECURENOW_APPID=my-service
ENV SECURENOW_INSTANCE=http://otel-collector:4318
ENV SECURENOW_NO_UUID=1
ENV SECURENOW_API_KEY=snk_live_abc123...

CMD ["node", "-r", "securenow/register", "src/index.js"]
```

### Kubernetes with Separate Trace/Log Collectors

```bash
SECURENOW_APPID=k8s-service
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=http://tempo:4318/v1/traces
OTEL_EXPORTER_OTLP_LOGS_ENDPOINT=http://loki:4318/v1/logs
SECURENOW_LOGGING_ENABLED=1
SECURENOW_NO_UUID=1
```

---

## Verification

On startup, securenow logs its configuration:

```
[securenow] pid=12345 SECURENOW_APPID="my-app" → service.name=my-app
[securenow] OTel SDK started → https://collector:4318/v1/traces
[securenow] Logging: ENABLED → https://collector:4318/v1/logs
[securenow] Request body capture: ENABLED (max: 10240 bytes)
[securenow] Firewall: ENABLED
[securenow] Firewall: synced 142 blocked IPs
```

Use `OTEL_LOG_LEVEL=debug` and `SECURENOW_TEST_SPAN=1` to troubleshoot connectivity issues.

**CLI equivalent** (works without booting the SDK — useful when the app won't start):

```bash
securenow env                     # show resolved service name, endpoints, env vars
securenow doctor                  # probe OTLP + API endpoints, exits 1 on failure
securenow test-span               # send a real span to the collector
```
