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 +7 -8
- package/SKILL-API.md +0 -1
- package/SKILL-CLI.md +2 -1
- package/app-config.js +10 -7
- package/cli/apps.js +36 -8
- package/cli/diagnostics.js +1 -1
- package/mcp/catalog.js +1 -1
- package/nextjs.js +0 -9
- package/nuxt-server-plugin.mjs +0 -11
- package/package.json +1 -2
- package/tracing.js +0 -6
- package/web-vite.mjs +0 -76
- package/free-trial-banner.js +0 -175
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
|
-
##
|
|
213
|
+
## Firewall
|
|
214
214
|
|
|
215
|
-
The
|
|
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
|
|
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
|
-
|
|
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
|
-
- **
|
|
245
|
-
- **
|
|
246
|
-
- **
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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
|
|
183
|
+
const CLOUD_LABEL = `${ui.c.green(SECURENOW_CLOUD_NAME)} ${ui.c.dim('— managed collector, no setup needed')}`;
|
|
159
184
|
|
|
160
|
-
const choices = [{ label:
|
|
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(
|
|
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(
|
|
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;
|
package/cli/diagnostics.js
CHANGED
|
@@ -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
|
|
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)');
|
package/nuxt-server-plugin.mjs
CHANGED
|
@@ -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.
|
|
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
|
}
|
package/free-trial-banner.js
DELETED
|
@@ -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 };
|