securenow 5.3.4 โ†’ 5.5.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/cli/apps.js CHANGED
@@ -5,6 +5,7 @@ const config = require('./config');
5
5
  const ui = require('./ui');
6
6
 
7
7
  const FREE_TRIAL_URL = 'https://freetrial.securenow.ai:4318';
8
+ const DOMAIN_REGEX = /^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/;
8
9
 
9
10
  function instanceUrl(inst) {
10
11
  if (!inst) return FREE_TRIAL_URL;
@@ -143,6 +144,10 @@ async function create(args, flags) {
143
144
  if (flags.json) {
144
145
  ui.json({ ...app, instanceUrl: envUrl });
145
146
  }
147
+
148
+ if (app.hosts && app.hosts.length > 0) {
149
+ await offerSubdomainDiscovery(app.hosts, selectedInstanceId, flags);
150
+ }
146
151
  } catch (err) {
147
152
  s.fail('Failed to create application');
148
153
  throw err;
@@ -194,11 +199,11 @@ async function pickInstance() {
194
199
  }
195
200
 
196
201
  function openBrowser(url) {
197
- const { execSync } = require('child_process');
202
+ const { execFileSync } = require('child_process');
198
203
  try {
199
- if (process.platform === 'darwin') execSync(`open "${url}"`);
200
- else if (process.platform === 'win32') execSync(`start "" "${url}"`);
201
- else execSync(`xdg-open "${url}"`);
204
+ if (process.platform === 'darwin') execFileSync('open', [url], { stdio: 'ignore' });
205
+ else if (process.platform === 'win32') execFileSync('cmd', ['/c', 'start', '', url], { stdio: 'ignore' });
206
+ else execFileSync('xdg-open', [url], { stdio: 'ignore' });
202
207
  } catch {
203
208
  ui.warn(`Could not open browser. Visit: ${url}`);
204
209
  }
@@ -293,4 +298,288 @@ async function setDefault(args) {
293
298
  ui.success(`Default application set to ${ui.c.bold(key)}`);
294
299
  }
295
300
 
296
- module.exports = { list, create, info, remove, setDefault };
301
+ function extractRootDomains(hosts) {
302
+ const domains = new Set();
303
+ for (const host of hosts || []) {
304
+ let cleaned = host.trim().toLowerCase();
305
+ try {
306
+ if (cleaned.startsWith('http://') || cleaned.startsWith('https://')) {
307
+ cleaned = new URL(cleaned).hostname;
308
+ }
309
+ } catch {}
310
+ if (DOMAIN_REGEX.test(cleaned)) {
311
+ const parts = cleaned.split('.');
312
+ const root = parts.length > 2 ? parts.slice(-2).join('.') : cleaned;
313
+ domains.add(root);
314
+ }
315
+ }
316
+ return [...domains];
317
+ }
318
+
319
+ async function offerSubdomainDiscovery(hosts, instanceId, flags) {
320
+ if (!process.stdin.isTTY) return;
321
+
322
+ const domains = extractRootDomains(hosts);
323
+ if (domains.length === 0) return;
324
+
325
+ const shouldDiscover = await ui.confirm(
326
+ `Domain${domains.length > 1 ? 's' : ''} detected (${domains.join(', ')}). Discover subdomains and add them as apps?`
327
+ );
328
+ if (!shouldDiscover) return;
329
+
330
+ await discoverAndAdd(domains, instanceId, flags);
331
+ }
332
+
333
+ async function discoverAndAdd(domains, instanceId, flags) {
334
+ const s = ui.spinner(`Scanning ${domains.length} domain${domains.length > 1 ? 's' : ''} for subdomains`);
335
+
336
+ let allSubdomains = [];
337
+ try {
338
+ for (const domain of domains) {
339
+ s.update(`Scanning ${domain}...`);
340
+ const result = await api.get(`/subdomains`, { query: { domain } });
341
+ if (result.success && result.subdomains) {
342
+ allSubdomains.push(...result.subdomains);
343
+ }
344
+ }
345
+ s.stop(`Found ${allSubdomains.length} subdomain${allSubdomains.length !== 1 ? 's' : ''}`);
346
+ } catch (err) {
347
+ s.fail('Failed to discover subdomains');
348
+ throw err;
349
+ }
350
+
351
+ if (allSubdomains.length === 0) {
352
+ ui.info('No subdomains found.');
353
+ return;
354
+ }
355
+
356
+ // Filter out subdomains already registered as apps
357
+ let existingHosts;
358
+ try {
359
+ const appData = await api.get('/applications');
360
+ const apps = appData.applications || [];
361
+ existingHosts = new Set(apps.flatMap(a => (a.hosts || []).map(h => h.toLowerCase())));
362
+ } catch {
363
+ existingHosts = new Set();
364
+ }
365
+
366
+ const newSubdomains = allSubdomains.filter(
367
+ (s) => !existingHosts.has(s.subdomain.toLowerCase())
368
+ );
369
+
370
+ if (newSubdomains.length === 0) {
371
+ ui.info('All discovered subdomains are already registered as apps.');
372
+ return;
373
+ }
374
+
375
+ console.log('');
376
+ ui.info(`${newSubdomains.length} new subdomain${newSubdomains.length !== 1 ? 's' : ''} found (${allSubdomains.length - newSubdomains.length} already tracked):`);
377
+
378
+ if (flags && flags.json) {
379
+ ui.json(newSubdomains);
380
+ return;
381
+ }
382
+
383
+ const choices = newSubdomains.map((sub) => ({
384
+ label: sub.subdomain,
385
+ detail: `IP: ${sub.ip || 'none'}${sub.cloudflare ? ' ยท Cloudflare' : ''}`,
386
+ value: sub.subdomain,
387
+ }));
388
+
389
+ const selected = await ui.multiSelect(
390
+ 'Which subdomains should be added as monitored apps?',
391
+ choices
392
+ );
393
+
394
+ if (selected.length === 0) {
395
+ ui.info('No subdomains selected.');
396
+ return;
397
+ }
398
+
399
+ const createSpinner = ui.spinner(`Creating ${selected.length} application${selected.length !== 1 ? 's' : ''}`);
400
+ try {
401
+ const result = await api.post('/applications/bulk', {
402
+ subdomains: selected,
403
+ instanceId: instanceId || null,
404
+ });
405
+ const count = result.count || selected.length;
406
+ createSpinner.stop(`Created ${count} application${count !== 1 ? 's' : ''}`);
407
+
408
+ console.log('');
409
+ if (result.applications) {
410
+ const rows = result.applications.map((app) => [
411
+ app.name,
412
+ ui.c.dim(app.key),
413
+ app.hosts?.join(', ') || '',
414
+ ]);
415
+ ui.table(['Name', 'Key', 'Hosts'], rows);
416
+ }
417
+ console.log('');
418
+ } catch (err) {
419
+ createSpinner.fail('Failed to create applications');
420
+ throw err;
421
+ }
422
+ }
423
+
424
+ async function discover(args, flags) {
425
+ requireAuth();
426
+
427
+ const appId = args[0];
428
+ let domains = [];
429
+ let instanceId = null;
430
+
431
+ if (appId) {
432
+ const s = ui.spinner('Fetching application');
433
+ try {
434
+ const data = await api.get(`/applications/${appId}`);
435
+ const app = data.application || data;
436
+ s.stop(`Application: ${app.name}`);
437
+ domains = extractRootDomains(app.hosts);
438
+ instanceId = app.instanceId;
439
+ } catch (err) {
440
+ s.fail('Failed to fetch application');
441
+ throw err;
442
+ }
443
+ } else if (flags.domain) {
444
+ domains = flags.domain.split(',').map(d => d.trim());
445
+ } else {
446
+ const domain = await ui.prompt('Domain to scan (e.g. example.com)');
447
+ if (!domain) {
448
+ ui.error('Domain is required');
449
+ process.exit(1);
450
+ }
451
+ domains = domain.split(',').map(d => d.trim());
452
+ }
453
+
454
+ if (domains.length === 0) {
455
+ ui.error('No domains found in application hosts. Specify a domain with --domain');
456
+ process.exit(1);
457
+ }
458
+
459
+ if (flags.instance) {
460
+ instanceId = flags.instance;
461
+ }
462
+
463
+ await discoverAndAdd(domains, instanceId, flags);
464
+ }
465
+
466
+ async function scan(args, flags) {
467
+ requireAuth();
468
+ const s = ui.spinner('Running subdomain discovery across all applications');
469
+
470
+ try {
471
+ const appData = await api.get('/applications');
472
+ const apps = appData.applications || [];
473
+
474
+ const allDomains = new Set();
475
+ let firstInstanceId = null;
476
+ for (const app of apps) {
477
+ const roots = extractRootDomains(app.hosts);
478
+ for (const r of roots) allDomains.add(r);
479
+ if (!firstInstanceId && app.instanceId) firstInstanceId = app.instanceId;
480
+ }
481
+
482
+ if (allDomains.size === 0) {
483
+ s.stop('No domains found across your applications');
484
+ ui.info('Add domains to your application hosts first, then run this command again.');
485
+ return;
486
+ }
487
+
488
+ s.update(`Found ${allDomains.size} unique domain${allDomains.size !== 1 ? 's' : ''}, scanning...`);
489
+
490
+ const existingHosts = new Set(
491
+ apps.flatMap((a) => (a.hosts || []).map((h) => h.toLowerCase()))
492
+ );
493
+
494
+ let totalNew = 0;
495
+ let totalCreated = 0;
496
+ const allNew = [];
497
+
498
+ for (const domain of allDomains) {
499
+ s.update(`Scanning ${domain}...`);
500
+ try {
501
+ const result = await api.get('/subdomains', { query: { domain } });
502
+ if (result.success && result.subdomains) {
503
+ const newSubs = result.subdomains.filter(
504
+ (sub) => !existingHosts.has(sub.subdomain.toLowerCase())
505
+ );
506
+ totalNew += newSubs.length;
507
+ allNew.push(...newSubs);
508
+ for (const sub of newSubs) {
509
+ existingHosts.add(sub.subdomain.toLowerCase());
510
+ }
511
+ }
512
+ } catch {
513
+ ui.warn(`Failed to scan ${domain}`);
514
+ }
515
+ }
516
+
517
+ s.stop(`Scanned ${allDomains.size} domain${allDomains.size !== 1 ? 's' : ''}, found ${totalNew} new subdomain${totalNew !== 1 ? 's' : ''}`);
518
+
519
+ if (totalNew === 0) {
520
+ ui.success('All subdomains are already tracked. Nothing to add.');
521
+ return;
522
+ }
523
+
524
+ if (flags.yes || flags.force) {
525
+ const createSpinner = ui.spinner(`Creating ${totalNew} application${totalNew !== 1 ? 's' : ''}`);
526
+ try {
527
+ const subdomainNames = allNew.map((s) => s.subdomain);
528
+ const result = await api.post('/applications/bulk', {
529
+ subdomains: subdomainNames,
530
+ instanceId: firstInstanceId || null,
531
+ });
532
+ totalCreated = result.count || subdomainNames.length;
533
+ createSpinner.stop(`Created ${totalCreated} application${totalCreated !== 1 ? 's' : ''}`);
534
+ } catch (err) {
535
+ createSpinner.fail('Failed to create applications');
536
+ throw err;
537
+ }
538
+ } else if (process.stdin.isTTY) {
539
+ const choices = allNew.map((sub) => ({
540
+ label: sub.subdomain,
541
+ detail: `IP: ${sub.ip || 'none'}${sub.cloudflare ? ' ยท Cloudflare' : ''}`,
542
+ value: sub.subdomain,
543
+ }));
544
+
545
+ const selected = await ui.multiSelect(
546
+ 'Which subdomains should be added as monitored apps?',
547
+ choices
548
+ );
549
+
550
+ if (selected.length === 0) {
551
+ ui.info('No subdomains selected.');
552
+ return;
553
+ }
554
+
555
+ const createSpinner = ui.spinner(`Creating ${selected.length} application${selected.length !== 1 ? 's' : ''}`);
556
+ try {
557
+ const result = await api.post('/applications/bulk', {
558
+ subdomains: selected,
559
+ instanceId: firstInstanceId || null,
560
+ });
561
+ totalCreated = result.count || selected.length;
562
+ createSpinner.stop(`Created ${totalCreated} application${totalCreated !== 1 ? 's' : ''}`);
563
+ } catch (err) {
564
+ createSpinner.fail('Failed to create applications');
565
+ throw err;
566
+ }
567
+ } else {
568
+ console.log('');
569
+ for (const sub of allNew) {
570
+ console.log(` ${sub.subdomain} ${ui.c.dim(`(IP: ${sub.ip || 'none'})`)}`);
571
+ }
572
+ console.log('');
573
+ ui.info(`Run with --yes to auto-create all ${totalNew} apps.`);
574
+ }
575
+
576
+ if (flags.json) {
577
+ ui.json({ domainsScanned: allDomains.size, newSubdomains: totalNew, appsCreated: totalCreated });
578
+ }
579
+ } catch (err) {
580
+ s.fail('Subdomain scan failed');
581
+ throw err;
582
+ }
583
+ }
584
+
585
+ module.exports = { list, create, info, remove, setDefault, discover, scan };
package/cli/auth.js CHANGED
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const http = require('http');
4
- const { execSync } = require('child_process');
4
+ const { execFileSync } = require('child_process');
5
5
  const config = require('./config');
6
6
  const { api, CLIError } = require('./client');
7
7
  const ui = require('./ui');
@@ -9,9 +9,9 @@ const ui = require('./ui');
9
9
  function openBrowser(url) {
10
10
  try {
11
11
  const platform = process.platform;
12
- if (platform === 'darwin') execSync(`open "${url}"`);
13
- else if (platform === 'win32') execSync(`start "" "${url}"`);
14
- else execSync(`xdg-open "${url}"`);
12
+ if (platform === 'darwin') execFileSync('open', [url], { stdio: 'ignore' });
13
+ else if (platform === 'win32') execFileSync('cmd', ['/c', 'start', '', url], { stdio: 'ignore' });
14
+ else execFileSync('xdg-open', [url], { stdio: 'ignore' });
15
15
  return true;
16
16
  } catch {
17
17
  return false;
package/cli/ui.js CHANGED
@@ -213,6 +213,50 @@ async function select(question, choices) {
213
213
  return choices[idx].value ?? choices[idx];
214
214
  }
215
215
 
216
+ async function multiSelect(question, choices) {
217
+ console.log(`\n ${c.cyan('?')} ${question}\n`);
218
+ choices.forEach((choice, i) => {
219
+ const label = choice.label || choice;
220
+ const detail = choice.detail ? c.dim(` โ€” ${choice.detail}`) : '';
221
+ console.log(` ${c.bold(String(i + 1).padStart(3))} ${label}${detail}`);
222
+ });
223
+ console.log('');
224
+ console.log(c.dim(` Enter numbers separated by commas, a range (e.g. 1-5), or "all"`));
225
+ const answer = await prompt('Selection');
226
+
227
+ if (!answer) return [];
228
+
229
+ if (answer.toLowerCase() === 'all') {
230
+ return choices.map((ch) => ch.value ?? ch);
231
+ }
232
+
233
+ const indices = new Set();
234
+ for (const part of answer.split(',')) {
235
+ const trimmed = part.trim();
236
+ if (trimmed.includes('-')) {
237
+ const [startStr, endStr] = trimmed.split('-');
238
+ const start = parseInt(startStr, 10);
239
+ const end = parseInt(endStr, 10);
240
+ if (!isNaN(start) && !isNaN(end)) {
241
+ for (let i = Math.min(start, end); i <= Math.max(start, end); i++) {
242
+ indices.add(i);
243
+ }
244
+ }
245
+ } else {
246
+ const num = parseInt(trimmed, 10);
247
+ if (!isNaN(num)) indices.add(num);
248
+ }
249
+ }
250
+
251
+ const selected = [];
252
+ for (const idx of indices) {
253
+ if (idx >= 1 && idx <= choices.length) {
254
+ selected.push(choices[idx - 1].value ?? choices[idx - 1]);
255
+ }
256
+ }
257
+ return selected;
258
+ }
259
+
216
260
  function json(data) {
217
261
  console.log(JSON.stringify(data, null, 2));
218
262
  }
@@ -302,6 +346,7 @@ module.exports = {
302
346
  prompt,
303
347
  confirm,
304
348
  select,
349
+ multiSelect,
305
350
  json,
306
351
  hr,
307
352
  timeAgo,
package/cli.js CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
  'use strict';
3
3
 
4
4
  const ui = require('./cli/ui');
@@ -64,6 +64,8 @@ const COMMANDS = {
64
64
  info: { desc: 'Show application details', usage: 'securenow apps info <id>', run: (a, f) => require('./cli/apps').info(a, f) },
65
65
  delete: { desc: 'Delete an application', usage: 'securenow apps delete <id> [--force]', run: (a, f) => require('./cli/apps').remove(a, f) },
66
66
  default: { desc: 'Set default application', usage: 'securenow apps default <key>', run: (a, f) => require('./cli/apps').setDefault(a, f) },
67
+ discover: { desc: 'Discover subdomains and add as apps', usage: 'securenow apps discover [appId] [--domain example.com]', run: (a, f) => require('./cli/apps').discover(a, f) },
68
+ scan: { desc: 'Scan all app domains for new subdomains', usage: 'securenow apps scan [--yes]', run: (a, f) => require('./cli/apps').scan(a, f) },
67
69
  },
68
70
  defaultSub: 'list',
69
71
  },
package/nextjs.js CHANGED
@@ -5,18 +5,36 @@
5
5
  *
6
6
  * Usage in Next.js app:
7
7
  *
8
- * 1. Create instrumentation.ts (or .js) in your project root:
8
+ * 1. Add serverExternalPackages to next.config.js (REQUIRED to avoid webpack bundling issues):
9
+ *
10
+ * const nextConfig = {
11
+ * serverExternalPackages: [
12
+ * "securenow",
13
+ * "@opentelemetry/sdk-node",
14
+ * "@opentelemetry/auto-instrumentations-node",
15
+ * "@opentelemetry/instrumentation-http",
16
+ * "@opentelemetry/exporter-trace-otlp-http",
17
+ * "@opentelemetry/exporter-logs-otlp-http",
18
+ * "@opentelemetry/sdk-logs",
19
+ * "@opentelemetry/instrumentation",
20
+ * "@opentelemetry/resources",
21
+ * "@opentelemetry/semantic-conventions",
22
+ * "@opentelemetry/api",
23
+ * "@opentelemetry/api-logs",
24
+ * "@vercel/otel",
25
+ * ],
26
+ * };
27
+ *
28
+ * 2. Create instrumentation.ts (or .js) in your project root:
9
29
  *
10
30
  * import { registerSecureNow } from 'securenow/nextjs';
11
31
  * export function register() {
12
32
  * registerSecureNow();
13
33
  * }
14
34
  *
15
- * 2. Set environment variables:
35
+ * 3. Set environment variables:
16
36
  * SECURENOW_APPID=my-nextjs-app
17
37
  * SECURENOW_INSTANCE=http://your-otlp-backend:4318
18
- *
19
- * That's it! ๐ŸŽ‰ No webpack warnings!
20
38
  */
21
39
 
22
40
  const { v4: uuidv4 } = require('uuid');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "securenow",
3
- "version": "5.3.4",
3
+ "version": "5.5.0",
4
4
  "description": "OpenTelemetry instrumentation for Node.js and Next.js - Send traces and logs to any OTLP-compatible backend",
5
5
  "type": "commonjs",
6
6
  "main": "register.js",
@@ -64,6 +64,7 @@
64
64
  "default": "./nextjs-wrapper.js"
65
65
  },
66
66
  "./nextjs-webpack-config": "./nextjs-webpack-config.js",
67
+ "./package.json": "./package.json",
67
68
  "./register-vite": "./register-vite.js",
68
69
  "./web-vite": {
69
70
  "import": "./web-vite.mjs",
@@ -105,7 +106,7 @@
105
106
  "@opentelemetry/instrumentation": "0.47.0",
106
107
  "@opentelemetry/instrumentation-document-load": "0.47.0",
107
108
  "@opentelemetry/instrumentation-fetch": "0.47.0",
108
- "@opentelemetry/instrumentation-http": "^0.208.0",
109
+ "@opentelemetry/instrumentation-http": "0.47.0",
109
110
  "@opentelemetry/instrumentation-user-interaction": "0.47.0",
110
111
  "@opentelemetry/instrumentation-xml-http-request": "0.47.0",
111
112
  "@opentelemetry/resources": "1.20.0",
package/tracing.js CHANGED
@@ -1,7 +1,10 @@
1
1
  'use strict';
2
2
 
3
3
  /**
4
- * Preload with: NODE_OPTIONS="-r securenow/register"
4
+ * Preload with: node --require securenow/register app.js
5
+ *
6
+ * For ESM apps ("type": "module" in package.json), you MUST also add the ESM loader hook:
7
+ * node --import @opentelemetry/instrumentation/hook.mjs --require securenow/register app.js
5
8
  *
6
9
  * Env:
7
10
  * SECURENOW_APPID=logical-name # or OTEL_SERVICE_NAME=logical-name
@@ -97,6 +100,27 @@ function redactGraphQLQuery(query, sensitiveFields = DEFAULT_SENSITIVE_FIELDS) {
97
100
  return redacted;
98
101
  }
99
102
 
103
+ // -------- ESM detection --------
104
+ (() => {
105
+ try {
106
+ const fs = require('fs');
107
+ const path = require('path');
108
+ const pkgPath = path.resolve(process.cwd(), 'package.json');
109
+ if (fs.existsSync(pkgPath)) {
110
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
111
+ if (pkg.type === 'module') {
112
+ const execArgv = process.execArgv.join(' ');
113
+ const hasEsmHook = execArgv.includes('hook.mjs') || execArgv.includes('import-in-the-middle');
114
+ if (!hasEsmHook) {
115
+ console.warn('[securenow] โš ๏ธ ESM app detected ("type": "module") but no ESM loader hook found.');
116
+ console.warn('[securenow] Instrumentations will NOT work without the ESM hook.');
117
+ console.warn('[securenow] Fix: node --import @opentelemetry/instrumentation/hook.mjs --require securenow/register app.js');
118
+ }
119
+ }
120
+ }
121
+ } catch (_) {}
122
+ })();
123
+
100
124
  // -------- diagnostics --------
101
125
  (() => {
102
126
  const L = (env('OTEL_LOG_LEVEL') || '').toLowerCase();