securenow 8.11.3 → 8.12.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/README.md CHANGED
@@ -210,9 +210,9 @@ Everything the MCP server can do is also a command. Run `npx securenow help` for
210
210
 
211
211
  ---
212
212
 
213
- ## Free firewall
213
+ ## Firewall
214
214
 
215
- The free tier includes the in-process IP firewall — no tracing required:
215
+ The in-process IP firewall is included in SecureNow (14-day free trial, then $99 per app per month). It runs on its own, no tracing required:
216
216
 
217
217
  ```bash
218
218
  npx securenow run --firewall-only src/index.js
@@ -220,7 +220,7 @@ npx securenow run --firewall-only src/index.js
220
220
  node -r securenow/firewall-only src/index.js
221
221
  ```
222
222
 
223
- It syncs your app's blocklist plus SecureNow's IP database of 500k+ known-bad IPs, refreshed hourly, and blocks in-process with HTTP 403. No proxies, no DNS changes, no network configuration — and it fails open.
223
+ It syncs your app's blocklist plus SecureNow's known-bad IP feed, refreshed hourly, and blocks in-process with HTTP 403. No proxies, no DNS changes, no network configuration — and it fails open.
224
224
 
225
225
  ---
226
226
 
@@ -239,12 +239,11 @@ More at [securenow.ai/security](https://securenow.ai/security).
239
239
 
240
240
  ## Pricing
241
241
 
242
- 14-day free trial. No credit card. Full product.
242
+ One plan: **$99 per app per month**, every feature included — detection, MCP, AI investigations, traces and logs, custom detections, blocking, rate limits, alerts, and the firewall.
243
243
 
244
- - **Free** — $0: local AI security audit, IP intelligence, basic firewall with 500k+ known-bad IPs refreshed hourly, CLI
245
- - **Pro** — $49/app/mo: full production security — detection, MCP, AI investigations, traces/logs, custom detections, blocking, rate limits, alerts, 7-day history
246
- - **Team** — $149/mo: 5 apps, 30-day history, team features
247
- - **Scale** — custom: dedicated isolated instance
244
+ - **Billed per app.** Your subscription quantity is the number of apps on the account (minimum 1). Creating an app adds $99/month, prorated; deleting one removes it. It's automatic.
245
+ - **14-day free trial** for the whole account, every feature, no credit card.
246
+ - **No free tier.** After the trial, everything pauses until you subscribe, including the firewall (the known-bad IP feed and blocklist sync stop being served).
248
247
 
249
248
  Details at [securenow.ai](https://securenow.ai/). Full CLI/SDK reference: `npx securenow help` plus the `SKILL-CLI.md` and `SKILL-API.md` files shipped in this package.
250
249
 
package/SKILL-API.md CHANGED
@@ -590,7 +590,6 @@ Local development uses `.securenow/runtime.json`; legacy `.securenow/credentials
590
590
  | `config.runtime.noUuid` | Use exact app key as service name without UUID suffix |
591
591
  | `config.runtime.strict` | Exit clustered workers when no app identity resolves |
592
592
  | `config.runtime.testSpan` | Emit a startup test span |
593
- | `config.runtime.hideBanner` | Suppress the free-trial upgrade banner |
594
593
  | `config.firewall.*` | Local firewall behavior, API URL, intervals, cloud WAF, and trusted proxy settings |
595
594
 
596
595
  **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 lightweight ETag checks and initial sync retries.
package/SKILL-CLI.md CHANGED
@@ -639,7 +639,8 @@ All commands support `--json` for structured output. When piping to other tools
639
639
  |------------------|---------|----------|
640
640
  | `Session expired` | Admin JWT expired | `securenow admin login` (or `admin login --global` for shared admin auth) |
641
641
  | `Not logged in` | No admin token found | `securenow admin login`; runtime `.securenow/runtime.json` is unrelated |
642
- | `Access denied (403)` | Insufficient plan or permissions | Upgrade plan or check user role |
642
+ | `Access denied (403)` | Insufficient permissions | Check the user's role in the account/organization |
643
+ | `HTTP 402` | The 14-day trial ended and the account has no active subscription (everything, including the firewall, is paused) | Subscribe in the dashboard: $99 per app per month, every feature included |
643
644
  | `Cannot connect` | API unreachable | Check `securenow config get apiUrl` or network |
644
645
  | `Unknown command` | Typo or unrecognized command | `securenow help` |
645
646
 
package/app-config.js CHANGED
@@ -17,9 +17,13 @@ const fs = require('fs');
17
17
  const path = require('path');
18
18
  const os = require('os');
19
19
 
20
- const FREE_TRIAL_INSTANCE = 'https://ingest.securenow.ai';
20
+ // Shared SecureNow Cloud ingestion gateway (production collector for every
21
+ // paying account and for the 14-day trial).
22
+ const SECURENOW_CLOUD_INGEST = 'https://ingest.securenow.ai';
23
+ // Deprecated alias kept for consumers of securenow/app-config.
24
+ const FREE_TRIAL_INSTANCE = SECURENOW_CLOUD_INGEST;
21
25
  const DEFAULT_API_URL = 'https://api.securenow.ai';
22
- const DEFAULT_FIREWALL_API_URL = FREE_TRIAL_INSTANCE;
26
+ const DEFAULT_FIREWALL_API_URL = SECURENOW_CLOUD_INGEST;
23
27
  const LEGACY_SECURENOW_GATEWAY = 'https://api.securenow.ai/api/otlp';
24
28
  const CONFIG_SCHEMA_VERSION = 2;
25
29
  const CREDENTIAL_FILE_ENVIRONMENTS = Object.freeze([
@@ -58,7 +62,6 @@ const DEFAULT_CONFIG = Object.freeze({
58
62
  noUuid: null,
59
63
  strict: false,
60
64
  testSpan: false,
61
- hideBanner: false,
62
65
  },
63
66
  firewall: {
64
67
  enabled: true,
@@ -112,7 +115,6 @@ const CONFIG_EXPLANATIONS = Object.freeze({
112
115
  'config.runtime.noUuid': 'null means auto: true when app.key is present. Set true/false only for advanced routing needs.',
113
116
  'config.runtime.strict': 'If true, PM2/cluster workers exit when no app identity is resolvable.',
114
117
  'config.runtime.testSpan': 'If true, emit a startup smoke span. Prefer `npx securenow test-span` for manual checks.',
115
- 'config.runtime.hideBanner': 'Hide the free-trial response banner when using the managed free-trial collector.',
116
118
  'config.firewall.enabled': 'Deprecated local hint. Runtime firewall enforcement is controlled by the SecureNow dashboard/API app environment toggle; the SDK starts sync whenever apiKey is present.',
117
119
  'config.firewall.apiUrl': 'Optional SecureNow firewall control-plane base URL. Leave unset/default so hosted SDKs sync through the SecureNow ingest gateway.',
118
120
  'config.firewall.versionCheckInterval': 'Seconds between lightweight firewall version/ETag checks.',
@@ -440,13 +442,13 @@ function normalizeInstanceEndpoint(value) {
440
442
  if (value === undefined || value === null || value === '') return value;
441
443
  const endpoint = String(value).trim().replace(/\/$/, '');
442
444
  if (!endpoint) return endpoint;
443
- if (endpoint === LEGACY_SECURENOW_GATEWAY) return FREE_TRIAL_INSTANCE;
445
+ if (endpoint === LEGACY_SECURENOW_GATEWAY) return SECURENOW_CLOUD_INGEST;
444
446
 
445
447
  try {
446
448
  const parseable = /^[a-z][a-z0-9+.-]*:\/\//i.test(endpoint) ? endpoint : `https://${endpoint}`;
447
449
  const url = new URL(parseable);
448
450
  if (url.port === '4318' && url.hostname.toLowerCase().endsWith('.securenow.ai')) {
449
- return FREE_TRIAL_INSTANCE;
451
+ return SECURENOW_CLOUD_INGEST;
450
452
  }
451
453
  } catch {}
452
454
 
@@ -626,7 +628,7 @@ function resolveApiKey() {
626
628
  function resolveInstance() {
627
629
  const fromConfig = pick(resolveConfigPath('otel.endpoint'));
628
630
  if (fromConfig) return normalizeInstanceEndpoint(fromConfig);
629
- return FREE_TRIAL_INSTANCE;
631
+ return SECURENOW_CLOUD_INGEST;
630
632
  }
631
633
 
632
634
  function resolveAll() {
@@ -725,6 +727,7 @@ function resolveFirewallOptions() {
725
727
  }
726
728
 
727
729
  module.exports = {
730
+ SECURENOW_CLOUD_INGEST,
728
731
  FREE_TRIAL_INSTANCE,
729
732
  DEFAULT_API_URL,
730
733
  DEFAULT_FIREWALL_API_URL,
package/cli/apps.js CHANGED
@@ -4,11 +4,31 @@ const { api, requireAuth } = require('./client');
4
4
  const config = require('./config');
5
5
  const ui = require('./ui');
6
6
 
7
- const FREE_TRIAL_URL = 'https://ingest.securenow.ai';
7
+ // Shared SecureNow Cloud collector (the production ingestion gateway).
8
+ const SECURENOW_CLOUD_URL = 'https://ingest.securenow.ai';
9
+ const SECURENOW_CLOUD_NAME = 'SecureNow Cloud';
8
10
  const DOMAIN_REGEX = /^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/;
9
11
 
10
12
  function instanceUrl(inst) {
11
- return FREE_TRIAL_URL;
13
+ return SECURENOW_CLOUD_URL;
14
+ }
15
+
16
+ function formatUsd(amount) {
17
+ const n = Number(amount);
18
+ if (!Number.isFinite(n)) return null;
19
+ return Number.isInteger(n) ? `$${n}` : `$${n.toFixed(2)}`;
20
+ }
21
+
22
+ // POST /applications and /applications/bulk return `billing` for the account.
23
+ // Per-app subscribers pay for every app, so tell them the new monthly total.
24
+ // Human mode only: --json output stays exactly what it was.
25
+ function printBillingNotice(billing, flags) {
26
+ if (!billing || !billing.isPerAppSubscriber || (flags && flags.json)) return;
27
+ const count = Number(billing.appCount) || 0;
28
+ const price = formatUsd(billing.perAppPrice) || '$99';
29
+ const total = formatUsd(billing.estimatedMonthlyTotal);
30
+ const totalText = total ? ` → ${total}/month` : '';
31
+ ui.info(`Billing: this account now has ${count} app${count !== 1 ? 's' : ''}${totalText} (${price}/app, prorated).`);
12
32
  }
13
33
 
14
34
  async function fetchInstanceMap() {
@@ -51,7 +71,7 @@ async function list(args, flags) {
51
71
  const rows = apps.map(app => {
52
72
  const inst = app.instanceId ? instMap[app.instanceId] : null;
53
73
  const url = instanceUrl(inst);
54
- const instLabel = inst ? `${inst.name} ${ui.c.dim(url)}` : `${ui.c.green('Free Trial')} ${ui.c.dim(url)}`;
74
+ const instLabel = inst ? `${inst.name} ${ui.c.dim(url)}` : `${ui.c.green(SECURENOW_CLOUD_NAME)} ${ui.c.dim(url)}`;
55
75
  return [
56
76
  app.name + (app.key === defaultApp ? ui.c.cyan(' (default)') : ''),
57
77
  ui.c.dim(app.key),
@@ -120,7 +140,7 @@ async function create(args, flags) {
120
140
  ['Name', app.name],
121
141
  ['Key', ui.c.green(ui.c.bold(app.key))],
122
142
  ['ID', ui.c.dim(app._id)],
123
- ['Instance', inst ? `${inst.name} ${ui.c.dim(`(${envUrl})`)}` : `${ui.c.green('Free Trial')} ${ui.c.dim(`(${envUrl})`)}`],
143
+ ['Instance', inst ? `${inst.name} ${ui.c.dim(`(${envUrl})`)}` : `${ui.c.green(SECURENOW_CLOUD_NAME)} ${ui.c.dim(`(${envUrl})`)}`],
124
144
  ['Hosts', app.hosts?.length ? app.hosts.join(', ') : ui.c.dim('none')],
125
145
  ]);
126
146
 
@@ -130,6 +150,11 @@ async function create(args, flags) {
130
150
  console.log(` ${ui.c.dim('(writes .securenow/runtime.json - no env var needed)')}`);
131
151
  console.log('');
132
152
 
153
+ if (!flags.json && result.billing?.isPerAppSubscriber) {
154
+ printBillingNotice(result.billing, flags);
155
+ console.log('');
156
+ }
157
+
133
158
  if (flags.json) {
134
159
  ui.json({ ...app, instanceUrl: envUrl });
135
160
  }
@@ -155,9 +180,9 @@ async function pickInstance() {
155
180
  return null;
156
181
  }
157
182
 
158
- const FREE_TRIAL_LABEL = `${ui.c.green('Free Trial')} ${ui.c.dim('— SecureNow managed instance (no setup needed)')}`;
183
+ const CLOUD_LABEL = `${ui.c.green(SECURENOW_CLOUD_NAME)} ${ui.c.dim('— managed collector, no setup needed')}`;
159
184
 
160
- const choices = [{ label: FREE_TRIAL_LABEL, value: null }];
185
+ const choices = [{ label: CLOUD_LABEL, value: null }];
161
186
 
162
187
  for (const inst of instances) {
163
188
  const status = inst.status === 'active' ? ui.c.green('●') : ui.c.red('●');
@@ -233,7 +258,7 @@ async function info(args, flags) {
233
258
  ui.keyValue([
234
259
  ['Key', ui.c.bold(app.key)],
235
260
  ['ID', ui.c.dim(app._id)],
236
- ['Instance', inst ? `${inst.name} ${ui.c.dim(`(${envUrl})`)}` : `${ui.c.green('Free Trial')} ${ui.c.dim(`(${envUrl})`)}`],
261
+ ['Instance', inst ? `${inst.name} ${ui.c.dim(`(${envUrl})`)}` : `${ui.c.green(SECURENOW_CLOUD_NAME)} ${ui.c.dim(`(${envUrl})`)}`],
237
262
  ['Hosts', app.hosts?.length ? app.hosts.join(', ') : ui.c.dim('none')],
238
263
  ['Created', app.createdAt ? new Date(app.createdAt).toLocaleString() : '—'],
239
264
  ['Updated', app.updatedAt ? new Date(app.updatedAt).toLocaleString() : '—'],
@@ -295,7 +320,7 @@ async function setDefault(args) {
295
320
  name: app.name || null,
296
321
  });
297
322
  ui.success(`Default application set to ${ui.c.bold(app.name || key)}`);
298
- ui.info(`SDK will use ${ui.c.dim(FREE_TRIAL_URL)} for telemetry; the gateway routes by app key.`);
323
+ ui.info(`SDK will use ${ui.c.dim(SECURENOW_CLOUD_URL)} for telemetry; the gateway routes by app key.`);
299
324
  } catch {
300
325
  // If we can't reach the API (offline / invalid key), still set the CLI default.
301
326
  ui.success(`Default application set to ${ui.c.bold(key)}`);
@@ -418,6 +443,7 @@ async function discoverAndAdd(domains, instanceId, flags) {
418
443
  ]);
419
444
  ui.table(['Name', 'Key', 'Hosts'], rows);
420
445
  }
446
+ printBillingNotice(result.billing, flags);
421
447
  console.log('');
422
448
  } catch (err) {
423
449
  createSpinner.fail('Failed to create applications');
@@ -535,6 +561,7 @@ async function scan(args, flags) {
535
561
  });
536
562
  totalCreated = result.count || subdomainNames.length;
537
563
  createSpinner.stop(`Created ${totalCreated} application${totalCreated !== 1 ? 's' : ''}`);
564
+ printBillingNotice(result.billing, flags);
538
565
  } catch (err) {
539
566
  createSpinner.fail('Failed to create applications');
540
567
  throw err;
@@ -564,6 +591,7 @@ async function scan(args, flags) {
564
591
  });
565
592
  totalCreated = result.count || selected.length;
566
593
  createSpinner.stop(`Created ${totalCreated} application${totalCreated !== 1 ? 's' : ''}`);
594
+ printBillingNotice(result.billing, flags);
567
595
  } catch (err) {
568
596
  createSpinner.fail('Failed to create applications');
569
597
  throw err;
@@ -497,7 +497,7 @@ async function doctor(_args, flags) {
497
497
  } else if (cfg.instance === 'https://api.securenow.ai/api/otlp') {
498
498
  warnings.push('Using the legacy SecureNow telemetry gateway path. Regenerate credentials so telemetry uses https://ingest.securenow.ai.');
499
499
  } else if (cfg.instance === 'https://freetrial.securenow.ai:4318') {
500
- warnings.push('Using the legacy free-trial collector directly. Regenerate credentials so telemetry flows through https://ingest.securenow.ai.');
500
+ warnings.push('Using the legacy SecureNow Cloud collector address directly. Regenerate credentials so telemetry flows through https://ingest.securenow.ai.');
501
501
  }
502
502
  if (!cfg.apiKey && token) {
503
503
  warnings.push('Admin CLI/MCP auth is connected, but the runtime API key is missing. Run `npx securenow app connect` or `npx securenow api-key create` to refresh .securenow/runtime.json.');
package/mcp/catalog.js CHANGED
@@ -209,7 +209,7 @@ const TOOLS = [
209
209
  {
210
210
  name: 'securenow_apps_create',
211
211
  title: 'Create Application',
212
- description: 'Create a SecureNow application. Write action; requires confirmation.',
212
+ description: 'Create a SecureNow application. Write action; requires confirmation. Each app is billed $99/month on subscribed accounts (free during the 14-day trial). Confirm with the user before creating apps.',
213
213
  scope: 'applications:write',
214
214
  readOnly: false,
215
215
  confirm: true,
package/nextjs.js CHANGED
@@ -268,7 +268,6 @@ function registerSecureNow(options = {}) {
268
268
 
269
269
  // -------- Endpoint Configuration --------
270
270
  const resolvedEndpoints = appConfig.resolveEndpoints({ endpoint: options.endpoint || resolvedApp.instance });
271
- const endpointBase = resolvedEndpoints.endpointBase;
272
271
  const tracesUrl = resolvedEndpoints.tracesUrl;
273
272
  const logsUrl = resolvedEndpoints.logsUrl;
274
273
  const headers = resolvedEndpoints.headers;
@@ -704,14 +703,6 @@ function registerSecureNow(options = {}) {
704
703
 
705
704
  isRegistered = true;
706
705
 
707
- // Free trial banner (optional - may not be bundled in standalone builds)
708
- try {
709
- const { isFreeTrial, patchHttpForBanner } = requireRuntimeModule('./free-trial-banner');
710
- if (isFreeTrial(endpointBase) && !appConfig.boolConfig('runtime.hideBanner', false)) {
711
- patchHttpForBanner();
712
- }
713
- } catch (_) {}
714
-
715
706
  console.log('[securenow] OpenTelemetry started for Next.js -> %s', tracesUrl);
716
707
  console.log('[securenow] Auto-capturing comprehensive request metadata:');
717
708
  console.log('[securenow] - IP addresses (x-forwarded-for, x-real-ip, socket)');
@@ -196,7 +196,6 @@ export default defineNitroPlugin(async (nitroApp) => {
196
196
 
197
197
  // ── Endpoints ──
198
198
  const resolvedEndpoints = appConfig.resolveEndpoints({ endpoint: opts.endpoint || resolvedApp.instance });
199
- const endpointBase = resolvedEndpoints.endpointBase;
200
199
  const tracesUrl = resolvedEndpoints.tracesUrl;
201
200
  const logsUrl = resolvedEndpoints.logsUrl;
202
201
  const headers = resolvedEndpoints.headers;
@@ -400,16 +399,6 @@ export default defineNitroPlugin(async (nitroApp) => {
400
399
  );
401
400
  }
402
401
 
403
- // ── Free trial banner ──
404
- try {
405
- const { isFreeTrial, patchHttpForBanner } = await import('./free-trial-banner.js');
406
- if (isFreeTrial(endpointBase) && !appConfig.boolConfig('runtime.hideBanner', false)) {
407
- patchHttpForBanner();
408
- }
409
- } catch {
410
- // not critical
411
- }
412
-
413
402
  // ── Firewall — runs independently from OTel ──
414
403
  const firewallOptions = appConfig.resolveFirewallOptions();
415
404
  if (firewallOptions.apiKey) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "securenow",
3
- "version": "8.11.3",
3
+ "version": "8.12.0",
4
4
  "description": "OpenTelemetry instrumentation for Node.js, Next.js, and Nuxt - Send traces and logs to any OTLP-compatible backend",
5
5
  "type": "commonjs",
6
6
  "main": "register.js",
@@ -153,7 +153,6 @@
153
153
  "cli.js",
154
154
  "cli/",
155
155
  "mcp/",
156
- "free-trial-banner.js",
157
156
  "resolve-ip.js",
158
157
  "cidr.js",
159
158
  "firewall.js",
package/tracing.js CHANGED
@@ -792,12 +792,6 @@ const sdk = new NodeSDK({
792
792
  const span = tracer.startSpan('securenow.startup.smoke'); span.end();
793
793
  }
794
794
 
795
- // Free trial banner
796
- const { isFreeTrial, patchHttpForBanner } = require('./free-trial-banner');
797
- if (isFreeTrial(endpointBase) && !appConfig.boolConfig('runtime.hideBanner', false)) {
798
- patchHttpForBanner();
799
- }
800
-
801
795
  // Firewall — auto-activates only when a real snk_live_ key is resolvable.
802
796
  // resolveApiKey() enforces the prefix, so we skip cleanly when the app has
803
797
  // only an app-routing UUID (or nothing at all) — no 401 polling loops.
package/web-vite.mjs CHANGED
@@ -24,7 +24,6 @@ const DEFAULT_BROWSER_CONFIG = Object.freeze({
24
24
  noUuid: null,
25
25
  strict: false,
26
26
  testSpan: false,
27
- hideBanner: false,
28
27
  },
29
28
  },
30
29
  });
@@ -258,84 +257,9 @@ export function startSecurenowWeb() {
258
257
  console.log('[securenow] Web OTel started → %s', tracesUrl);
259
258
  }
260
259
 
261
- // ---- Free trial banner (browser DOM injection) ----
262
- function injectFreeTrialBanner() {
263
- const FREE_TRIAL_HOSTS = ['ingest.securenow.ai', 'freetrial.securenow.ai'];
264
- const hideBanner = boolConfig('config.runtime.hideBanner', false);
265
- if (hideBanner || !FREE_TRIAL_HOSTS.some(host => endpointBase.includes(host))) return;
266
- if (typeof document === 'undefined') return;
267
-
268
- function create() {
269
- if (document.getElementById('sn-ft-banner')) return;
270
-
271
- const d = document.createElement('div');
272
- d.id = 'sn-ft-banner';
273
- d.style.cssText =
274
- 'position:fixed;top:0;left:0;right:0;z-index:2147483647;' +
275
- 'background:#FEF3CD;color:#856404;padding:10px 16px;' +
276
- 'font-family:system-ui,-apple-system,sans-serif;font-size:13px;' +
277
- 'text-align:center;border-bottom:2px solid #FFE69C;' +
278
- 'display:flex;align-items:center;justify-content:center;gap:6px;' +
279
- 'box-shadow:0 2px 8px rgba(0,0,0,0.12)';
280
-
281
- const icon = document.createElement('span');
282
- icon.textContent = '\u26a0\ufe0f';
283
- d.appendChild(icon);
284
-
285
- const msg = document.createElement('span');
286
- const strong = document.createElement('strong');
287
- strong.textContent = 'Testing Environment:';
288
- msg.appendChild(strong);
289
- msg.appendChild(document.createTextNode(
290
- ' Only add test applications. For production usage, '
291
- ));
292
-
293
- const link = document.createElement('a');
294
- link.href = 'https://app.securenow.ai/dashboard/settings/instances';
295
- link.target = '_blank';
296
- link.rel = 'noopener';
297
- link.style.cssText = 'color:#664D03;font-weight:600;text-decoration:underline';
298
- link.textContent = 'create a new production instance';
299
- msg.appendChild(link);
300
- msg.appendChild(document.createTextNode('.'));
301
- d.appendChild(msg);
302
-
303
- const upgradeBtn = document.createElement('a');
304
- upgradeBtn.href = 'https://app.securenow.ai/dashboard/settings/instances';
305
- upgradeBtn.target = '_blank';
306
- upgradeBtn.rel = 'noopener';
307
- upgradeBtn.textContent = '\u26a1 Upgrade';
308
- upgradeBtn.style.cssText =
309
- 'display:inline-flex;align-items:center;gap:4px;' +
310
- 'background:#D97706;color:#fff;padding:4px 12px;border-radius:4px;' +
311
- 'font-size:12px;font-weight:600;text-decoration:none;margin-left:10px;' +
312
- 'white-space:nowrap;transition:background 0.15s';
313
- upgradeBtn.onmouseover = () => { upgradeBtn.style.background = '#B45309'; };
314
- upgradeBtn.onmouseout = () => { upgradeBtn.style.background = '#D97706'; };
315
- d.appendChild(upgradeBtn);
316
-
317
- const close = document.createElement('button');
318
- close.textContent = '\u00d7';
319
- close.style.cssText =
320
- 'background:none;border:none;color:#856404;font-size:18px;' +
321
- 'cursor:pointer;margin-left:12px;padding:0 4px;line-height:1';
322
- close.onclick = () => { d.style.display = 'none'; };
323
- d.appendChild(close);
324
-
325
- document.body.prepend(d);
326
- }
327
-
328
- if (document.readyState === 'loading') {
329
- document.addEventListener('DOMContentLoaded', create);
330
- } else {
331
- create();
332
- }
333
- }
334
-
335
260
  // Auto-start
336
261
  try {
337
262
  startSecurenowWeb();
338
- injectFreeTrialBanner();
339
263
  } catch (e) {
340
264
  console.error('[securenow/web-vite] failed to start:', e);
341
265
  }
@@ -1,175 +0,0 @@
1
- 'use strict';
2
-
3
- /**
4
- * Free Trial Banner — auto-injects a visible "Testing Environment" banner
5
- * into HTML pages served by apps using the SecureNow free trial instance.
6
- *
7
- * Opt-out: set config.runtime.hideBanner=true in .securenow/credentials.json
8
- */
9
-
10
- const FREE_TRIAL_HOSTS = ['ingest.securenow.ai', 'freetrial.securenow.ai'];
11
-
12
- function isFreeTrial(endpointBase) {
13
- return !!endpointBase && FREE_TRIAL_HOSTS.some((host) => endpointBase.includes(host));
14
- }
15
-
16
- /* istanbul ignore next — runs in browser, not Node */
17
- function _bannerClientCode() {
18
- if (window.__snBanner) return;
19
- window.__snBanner = 1;
20
-
21
- function create() {
22
- if (document.getElementById('sn-ft-banner')) return;
23
-
24
- var d = document.createElement('div');
25
- d.id = 'sn-ft-banner';
26
- d.style.cssText =
27
- 'position:fixed;top:0;left:0;right:0;z-index:2147483647;' +
28
- 'background:#FEF3CD;color:#856404;padding:10px 16px;' +
29
- 'font-family:system-ui,-apple-system,sans-serif;font-size:13px;' +
30
- 'text-align:center;border-bottom:2px solid #FFE69C;' +
31
- 'display:flex;align-items:center;justify-content:center;gap:6px;' +
32
- 'box-shadow:0 2px 8px rgba(0,0,0,0.12)';
33
-
34
- var icon = document.createElement('span');
35
- icon.textContent = '\u26a0\ufe0f';
36
- d.appendChild(icon);
37
-
38
- var msg = document.createElement('span');
39
- var strong = document.createElement('strong');
40
- strong.textContent = 'Testing Environment:';
41
- msg.appendChild(strong);
42
- msg.appendChild(document.createTextNode(
43
- ' Only add test applications. For production usage, '
44
- ));
45
-
46
- var link = document.createElement('a');
47
- link.href = 'https://app.securenow.ai/dashboard/settings/instances';
48
- link.target = '_blank';
49
- link.rel = 'noopener';
50
- link.style.cssText = 'color:#664D03;font-weight:600;text-decoration:underline';
51
- link.textContent = 'create a new production instance';
52
- msg.appendChild(link);
53
- msg.appendChild(document.createTextNode('.'));
54
- d.appendChild(msg);
55
-
56
- var upgradeBtn = document.createElement('a');
57
- upgradeBtn.href = 'https://app.securenow.ai/dashboard/settings/instances';
58
- upgradeBtn.target = '_blank';
59
- upgradeBtn.rel = 'noopener';
60
- upgradeBtn.textContent = '\u26a1 Upgrade';
61
- upgradeBtn.style.cssText =
62
- 'display:inline-flex;align-items:center;gap:4px;' +
63
- 'background:#D97706;color:#fff;padding:4px 12px;border-radius:4px;' +
64
- 'font-size:12px;font-weight:600;text-decoration:none;margin-left:10px;' +
65
- 'white-space:nowrap;transition:background 0.15s';
66
- upgradeBtn.onmouseover = function () { upgradeBtn.style.background = '#B45309'; };
67
- upgradeBtn.onmouseout = function () { upgradeBtn.style.background = '#D97706'; };
68
- d.appendChild(upgradeBtn);
69
-
70
- var close = document.createElement('button');
71
- close.textContent = '\u00d7';
72
- close.style.cssText =
73
- 'background:none;border:none;color:#856404;font-size:18px;' +
74
- 'cursor:pointer;margin-left:12px;padding:0 4px;line-height:1';
75
- close.onclick = function () { d.style.display = 'none'; };
76
- d.appendChild(close);
77
-
78
- document.body.prepend(d);
79
- }
80
-
81
- if (document.readyState === 'loading') {
82
- document.addEventListener('DOMContentLoaded', create);
83
- } else {
84
- create();
85
- }
86
- }
87
-
88
- var BANNER_SCRIPT =
89
- '<script data-securenow-banner>(' +
90
- _bannerClientCode.toString() +
91
- ')()</scr' + 'ipt>';
92
-
93
- /**
94
- * Monkey-patch http.ServerResponse to inject the banner script into HTML
95
- * responses. Searches for `<head...>` and inserts the script right after it.
96
- * Skips compressed responses and non-HTML content types.
97
- */
98
- function patchHttpForBanner() {
99
- try {
100
- var nodeRequire = typeof __non_webpack_require__ === 'function' ? __non_webpack_require__ : eval('require');
101
- var http = nodeRequire('node:http');
102
- var _origWrite = http.ServerResponse.prototype.write;
103
- var _origEnd = http.ServerResponse.prototype.end;
104
-
105
- function maybeInject(res, chunk) {
106
- if (res._snBannerDone || !chunk) return chunk;
107
-
108
- if (res._snIsHtml === undefined) {
109
- var ct = res.getHeader('content-type');
110
- var ce = res.getHeader('content-encoding');
111
- var csp = res.getHeader('content-security-policy');
112
- res._snIsHtml = !!(ct && String(ct).includes('text/html') && !ce && !csp);
113
- }
114
- if (!res._snIsHtml) {
115
- res._snBannerDone = true;
116
- return chunk;
117
- }
118
-
119
- var isStr = typeof chunk === 'string';
120
- var isBuf = Buffer.isBuffer(chunk);
121
- if (!isStr && !isBuf) return chunk;
122
-
123
- var str = isStr ? chunk : chunk.toString('utf8');
124
- var headIdx = str.indexOf('<head');
125
- if (headIdx === -1) return chunk;
126
-
127
- var gt = str.indexOf('>', headIdx);
128
- if (gt === -1) return chunk;
129
-
130
- res._snBannerDone = true;
131
- var result = str.slice(0, gt + 1) + BANNER_SCRIPT + str.slice(gt + 1);
132
- return isStr ? result : Buffer.from(result, 'utf8');
133
- }
134
-
135
- http.ServerResponse.prototype.write = function (chunk, encoding, cb) {
136
- try {
137
- var modified = maybeInject(this, chunk);
138
- if (modified !== chunk) {
139
- var enc = typeof encoding === 'function' ? 'utf8' : encoding;
140
- var callback = typeof encoding === 'function' ? encoding : cb;
141
- try {
142
- if (this.getHeader('content-length')) {
143
- this.setHeader('content-length', Buffer.byteLength(modified));
144
- }
145
- } catch (_) { /* headers already sent */ }
146
- return _origWrite.call(this, modified, enc, callback);
147
- }
148
- } catch (_) { /* never break the app */ }
149
- return _origWrite.call(this, chunk, encoding, cb);
150
- };
151
-
152
- http.ServerResponse.prototype.end = function (chunk, encoding, cb) {
153
- try {
154
- var modified = maybeInject(this, chunk);
155
- if (modified !== chunk) {
156
- var enc = typeof encoding === 'function' ? 'utf8' : encoding;
157
- var callback = typeof encoding === 'function' ? encoding : cb;
158
- try {
159
- if (this.getHeader('content-length')) {
160
- this.setHeader('content-length', Buffer.byteLength(modified));
161
- }
162
- } catch (_) { /* headers already sent */ }
163
- return _origEnd.call(this, modified, enc, callback);
164
- }
165
- } catch (_) { /* never break the app */ }
166
- return _origEnd.call(this, chunk, encoding, cb);
167
- };
168
-
169
- console.log('[securenow] Free trial banner injection enabled');
170
- } catch (err) {
171
- console.warn('[securenow] Could not setup free trial banner:', err.message);
172
- }
173
- }
174
-
175
- module.exports = { isFreeTrial, patchHttpForBanner, BANNER_SCRIPT };