securenow 5.3.3 → 5.4.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 +376 -18
- package/cli/auth.js +4 -4
- package/cli/ui.js +45 -0
- package/cli.js +2 -0
- package/package.json +1 -1
package/cli/apps.js
CHANGED
|
@@ -4,32 +4,75 @@ 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://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,}$/;
|
|
9
|
+
|
|
10
|
+
function instanceUrl(inst) {
|
|
11
|
+
if (!inst) return FREE_TRIAL_URL;
|
|
12
|
+
return `${inst.protocol || 'https'}://${inst.host}:4318`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async function fetchInstanceMap() {
|
|
16
|
+
try {
|
|
17
|
+
const data = await api.get('/instances');
|
|
18
|
+
const instances = data.instances || [];
|
|
19
|
+
const map = {};
|
|
20
|
+
for (const inst of instances) {
|
|
21
|
+
map[inst._id] = inst;
|
|
22
|
+
}
|
|
23
|
+
return map;
|
|
24
|
+
} catch {
|
|
25
|
+
return {};
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
7
29
|
async function list(args, flags) {
|
|
8
30
|
requireAuth();
|
|
9
31
|
const s = ui.spinner('Fetching applications');
|
|
10
32
|
|
|
11
33
|
try {
|
|
12
|
-
const
|
|
13
|
-
|
|
34
|
+
const [appData, instMap] = await Promise.all([
|
|
35
|
+
api.get('/applications'),
|
|
36
|
+
fetchInstanceMap(),
|
|
37
|
+
]);
|
|
38
|
+
const apps = appData.applications || [];
|
|
14
39
|
s.stop(`Found ${apps.length} application${apps.length !== 1 ? 's' : ''}`);
|
|
15
40
|
console.log('');
|
|
16
41
|
|
|
17
42
|
if (flags.json) {
|
|
18
|
-
|
|
43
|
+
const enriched = apps.map(app => ({
|
|
44
|
+
...app,
|
|
45
|
+
instanceUrl: instanceUrl(app.instanceId ? instMap[app.instanceId] : null),
|
|
46
|
+
}));
|
|
47
|
+
ui.json(enriched);
|
|
19
48
|
return;
|
|
20
49
|
}
|
|
21
50
|
|
|
22
51
|
const defaultApp = config.getDefaultApp();
|
|
23
|
-
const rows = apps.map(app =>
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
52
|
+
const rows = apps.map(app => {
|
|
53
|
+
const inst = app.instanceId ? instMap[app.instanceId] : null;
|
|
54
|
+
const url = instanceUrl(inst);
|
|
55
|
+
const instLabel = inst ? `${inst.name} ${ui.c.dim(url)}` : `${ui.c.green('Free Trial')} ${ui.c.dim(url)}`;
|
|
56
|
+
return [
|
|
57
|
+
app.name + (app.key === defaultApp ? ui.c.cyan(' (default)') : ''),
|
|
58
|
+
ui.c.dim(app.key),
|
|
59
|
+
instLabel,
|
|
60
|
+
ui.timeAgo(app.createdAt),
|
|
61
|
+
];
|
|
62
|
+
});
|
|
29
63
|
|
|
30
|
-
ui.table(['Name', 'Key', '
|
|
64
|
+
ui.table(['Name', 'Key', 'Instance', 'Created'], rows);
|
|
31
65
|
console.log('');
|
|
32
66
|
|
|
67
|
+
if (apps.length > 0) {
|
|
68
|
+
console.log(` ${ui.c.bold('Add to your .env:')}`);
|
|
69
|
+
const first = apps.find(a => a.key === defaultApp) || apps[0];
|
|
70
|
+
const firstInst = first.instanceId ? instMap[first.instanceId] : null;
|
|
71
|
+
console.log(` SECURENOW_APPID=${first.key}`);
|
|
72
|
+
console.log(` SECURENOW_INSTANCE=${instanceUrl(firstInst)}`);
|
|
73
|
+
console.log('');
|
|
74
|
+
}
|
|
75
|
+
|
|
33
76
|
if (!defaultApp && apps.length > 0) {
|
|
34
77
|
ui.info(`Tip: Set a default app with ${ui.c.bold('securenow config set defaultApp <key>')}`);
|
|
35
78
|
console.log('');
|
|
@@ -63,17 +106,29 @@ async function create(args, flags) {
|
|
|
63
106
|
body.instanceId = await pickInstance();
|
|
64
107
|
}
|
|
65
108
|
|
|
109
|
+
const selectedInstanceId = body.instanceId;
|
|
110
|
+
|
|
66
111
|
const s = ui.spinner(`Creating application "${name}"`);
|
|
67
112
|
try {
|
|
68
113
|
const result = await api.post('/applications', body);
|
|
69
114
|
const app = result.application || result;
|
|
70
115
|
s.stop(`Application created`);
|
|
71
116
|
|
|
117
|
+
let inst = null;
|
|
118
|
+
if (selectedInstanceId) {
|
|
119
|
+
try {
|
|
120
|
+
const instData = await api.get(`/instances/${selectedInstanceId}`);
|
|
121
|
+
inst = instData.instance || null;
|
|
122
|
+
} catch {}
|
|
123
|
+
}
|
|
124
|
+
const envUrl = instanceUrl(inst);
|
|
125
|
+
|
|
72
126
|
console.log('');
|
|
73
127
|
ui.keyValue([
|
|
74
128
|
['Name', app.name],
|
|
75
129
|
['Key', ui.c.green(ui.c.bold(app.key))],
|
|
76
130
|
['ID', ui.c.dim(app._id)],
|
|
131
|
+
['Instance', inst ? `${inst.name} ${ui.c.dim(`(${envUrl})`)}` : `${ui.c.green('Free Trial')} ${ui.c.dim(`(${envUrl})`)}`],
|
|
77
132
|
['Hosts', app.hosts?.length ? app.hosts.join(', ') : ui.c.dim('none')],
|
|
78
133
|
]);
|
|
79
134
|
|
|
@@ -81,12 +136,17 @@ async function create(args, flags) {
|
|
|
81
136
|
console.log(` ${ui.c.bold('Add to your .env.local:')}`);
|
|
82
137
|
console.log('');
|
|
83
138
|
console.log(` SECURENOW_APPID=${app.key}`);
|
|
139
|
+
console.log(` SECURENOW_INSTANCE=${envUrl}`);
|
|
84
140
|
console.log('');
|
|
85
141
|
ui.info(`Set as default: ${ui.c.bold(`securenow config set defaultApp ${app.key}`)}`);
|
|
86
142
|
console.log('');
|
|
87
143
|
|
|
88
144
|
if (flags.json) {
|
|
89
|
-
ui.json(app);
|
|
145
|
+
ui.json({ ...app, instanceUrl: envUrl });
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (app.hosts && app.hosts.length > 0) {
|
|
149
|
+
await offerSubdomainDiscovery(app.hosts, selectedInstanceId, flags);
|
|
90
150
|
}
|
|
91
151
|
} catch (err) {
|
|
92
152
|
s.fail('Failed to create application');
|
|
@@ -139,11 +199,11 @@ async function pickInstance() {
|
|
|
139
199
|
}
|
|
140
200
|
|
|
141
201
|
function openBrowser(url) {
|
|
142
|
-
const {
|
|
202
|
+
const { execFileSync } = require('child_process');
|
|
143
203
|
try {
|
|
144
|
-
if (process.platform === 'darwin')
|
|
145
|
-
else if (process.platform === 'win32')
|
|
146
|
-
else
|
|
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' });
|
|
147
207
|
} catch {
|
|
148
208
|
ui.warn(`Could not open browser. Visit: ${url}`);
|
|
149
209
|
}
|
|
@@ -162,10 +222,19 @@ async function info(args, flags) {
|
|
|
162
222
|
try {
|
|
163
223
|
const data = await api.get(`/applications/${id}`);
|
|
164
224
|
const app = data.application || data;
|
|
225
|
+
|
|
226
|
+
let inst = null;
|
|
227
|
+
if (app.instanceId) {
|
|
228
|
+
try {
|
|
229
|
+
const instData = await api.get(`/instances/${app.instanceId}`);
|
|
230
|
+
inst = instData.instance || null;
|
|
231
|
+
} catch {}
|
|
232
|
+
}
|
|
233
|
+
const envUrl = instanceUrl(inst);
|
|
165
234
|
s.stop('Application details loaded');
|
|
166
235
|
|
|
167
236
|
if (flags.json) {
|
|
168
|
-
ui.json(app);
|
|
237
|
+
ui.json({ ...app, instanceUrl: envUrl });
|
|
169
238
|
return;
|
|
170
239
|
}
|
|
171
240
|
|
|
@@ -175,11 +244,16 @@ async function info(args, flags) {
|
|
|
175
244
|
ui.keyValue([
|
|
176
245
|
['Key', ui.c.bold(app.key)],
|
|
177
246
|
['ID', ui.c.dim(app._id)],
|
|
247
|
+
['Instance', inst ? `${inst.name} ${ui.c.dim(`(${envUrl})`)}` : `${ui.c.green('Free Trial')} ${ui.c.dim(`(${envUrl})`)}`],
|
|
178
248
|
['Hosts', app.hosts?.length ? app.hosts.join(', ') : ui.c.dim('none')],
|
|
179
|
-
['Instance', app.instanceId || ui.c.dim('default')],
|
|
180
249
|
['Created', app.createdAt ? new Date(app.createdAt).toLocaleString() : '—'],
|
|
181
250
|
['Updated', app.updatedAt ? new Date(app.updatedAt).toLocaleString() : '—'],
|
|
182
251
|
]);
|
|
252
|
+
|
|
253
|
+
console.log('');
|
|
254
|
+
console.log(` ${ui.c.bold('Environment variables:')}`);
|
|
255
|
+
console.log(` SECURENOW_APPID=${app.key}`);
|
|
256
|
+
console.log(` SECURENOW_INSTANCE=${envUrl}`);
|
|
183
257
|
console.log('');
|
|
184
258
|
} catch (err) {
|
|
185
259
|
s.fail('Failed to fetch application');
|
|
@@ -224,4 +298,288 @@ async function setDefault(args) {
|
|
|
224
298
|
ui.success(`Default application set to ${ui.c.bold(key)}`);
|
|
225
299
|
}
|
|
226
300
|
|
|
227
|
-
|
|
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 {
|
|
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')
|
|
13
|
-
else if (platform === 'win32')
|
|
14
|
-
else
|
|
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
|
@@ -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/package.json
CHANGED