securenow 7.7.16 → 7.8.1
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/NPM_README.md +35 -22
- package/README.md +50 -32
- package/SKILL-API.md +48 -24
- package/SKILL-CLI.md +61 -40
- package/app-config.js +79 -16
- package/cli/apiKey.js +7 -7
- package/cli/apps.js +3 -3
- package/cli/auth.js +113 -31
- package/cli/client.js +14 -13
- package/cli/config.js +219 -45
- package/cli/credentials.js +3 -3
- package/cli/diagnostics.js +5 -6
- package/cli/firewall.js +19 -7
- package/cli/init.js +5 -5
- package/cli/security.js +31 -11
- package/cli.js +57 -22
- package/firewall-only.js +3 -4
- package/firewall.js +110 -35
- package/mcp/catalog.js +43 -30
- package/mcp/server.js +73 -12
- package/nextjs.js +4 -1
- package/nuxt-server-plugin.mjs +7 -4
- package/otel-defaults.js +11 -0
- package/package.json +2 -1
- package/tracing.js +4 -1
package/cli/auth.js
CHANGED
|
@@ -50,9 +50,10 @@ function decodeJwtPayload(token) {
|
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
async function loginWithBrowser() {
|
|
53
|
+
async function loginWithBrowser(options = {}) {
|
|
54
54
|
const appUrl = config.getAppUrl();
|
|
55
55
|
const nonce = crypto.randomBytes(24).toString('base64url');
|
|
56
|
+
const mode = options.mode || null;
|
|
56
57
|
|
|
57
58
|
return new Promise((resolve, reject) => {
|
|
58
59
|
let pendingToken = null;
|
|
@@ -108,7 +109,7 @@ async function loginWithBrowser() {
|
|
|
108
109
|
const email = payload?.email || 'unknown account';
|
|
109
110
|
const safeEmail = email.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
110
111
|
const port = server.address().port;
|
|
111
|
-
const switchUrl = buildCliAuthUrl(appUrl, port, nonce, { force_login: 1 });
|
|
112
|
+
const switchUrl = buildCliAuthUrl(appUrl, port, nonce, { force_login: 1, ...(mode ? { mode } : {}) });
|
|
112
113
|
|
|
113
114
|
res.end([
|
|
114
115
|
'<!DOCTYPE html><html><head><meta charset="utf-8"><title>SecureNow CLI Login</title></head>',
|
|
@@ -181,7 +182,7 @@ async function loginWithBrowser() {
|
|
|
181
182
|
|
|
182
183
|
server.listen(0, '127.0.0.1', () => {
|
|
183
184
|
const port = server.address().port;
|
|
184
|
-
const authUrl = buildCliAuthUrl(appUrl, port, nonce);
|
|
185
|
+
const authUrl = buildCliAuthUrl(appUrl, port, nonce, mode ? { mode } : {});
|
|
185
186
|
|
|
186
187
|
console.log('');
|
|
187
188
|
ui.info('Opening browser for authentication...');
|
|
@@ -200,7 +201,7 @@ async function loginWithBrowser() {
|
|
|
200
201
|
|
|
201
202
|
const timeout = setTimeout(() => {
|
|
202
203
|
closeServer();
|
|
203
|
-
reject(new CLIError('Login timed out after 5 minutes. Try `securenow login --token <TOKEN>` instead.'));
|
|
204
|
+
reject(new CLIError('Login timed out after 5 minutes. Try `securenow admin login --token <TOKEN>` instead.'));
|
|
204
205
|
}, 5 * 60 * 1000);
|
|
205
206
|
|
|
206
207
|
server.on('close', () => clearTimeout(timeout));
|
|
@@ -235,7 +236,7 @@ async function login(args, flags) {
|
|
|
235
236
|
const email = payload?.email || 'unknown';
|
|
236
237
|
const exp = payload?.exp ? payload.exp * 1000 : null;
|
|
237
238
|
|
|
238
|
-
config.setAuth(token, email, exp, { local
|
|
239
|
+
config.setAuth(token, email, exp, { local });
|
|
239
240
|
if (local) config.ensureLocalGitignore();
|
|
240
241
|
console.log('');
|
|
241
242
|
ui.success(`Logged in as ${ui.c.bold(email)}`);
|
|
@@ -253,14 +254,25 @@ async function login(args, flags) {
|
|
|
253
254
|
const email = payload?.email || 'unknown';
|
|
254
255
|
const exp = payload?.exp ? payload.exp * 1000 : null;
|
|
255
256
|
|
|
256
|
-
config.setAuth(token, email, exp, { local
|
|
257
|
-
if (
|
|
257
|
+
config.setAuth(token, email, exp, { local });
|
|
258
|
+
if (app && (app.key || app.name)) {
|
|
259
|
+
config.setRuntime({
|
|
260
|
+
apiKey: apiKey || config.getApiKey() || null,
|
|
261
|
+
app: {
|
|
262
|
+
key: app.key || null,
|
|
263
|
+
name: app.name || null,
|
|
264
|
+
},
|
|
265
|
+
}, { local });
|
|
266
|
+
}
|
|
258
267
|
if (local) config.ensureLocalGitignore();
|
|
259
268
|
console.log('');
|
|
260
269
|
ui.success(`Logged in as ${ui.c.bold(email)}`);
|
|
261
|
-
ui.info(local ? '
|
|
270
|
+
ui.info(local ? 'Admin auth saved to project .securenow/admin.json' : 'Admin auth saved to ~/.securenow/admin.json');
|
|
262
271
|
if (app && (app.name || app.key)) {
|
|
263
272
|
ui.info(`Linked to app ${ui.c.bold(app.name || app.key)}${app.key ? ` (${ui.c.dim(app.key)})` : ''}`);
|
|
273
|
+
ui.info(local ? 'Runtime app config saved to project .securenow/runtime.json' : 'Runtime app config saved to ~/.securenow/runtime.json');
|
|
274
|
+
} else {
|
|
275
|
+
ui.info('Runtime app config was not changed.');
|
|
264
276
|
}
|
|
265
277
|
if (apiKey) {
|
|
266
278
|
ui.info(`Firewall API key saved — the firewall will activate automatically on next start`);
|
|
@@ -276,7 +288,7 @@ async function login(args, flags) {
|
|
|
276
288
|
console.log('');
|
|
277
289
|
console.log(` 1. Go to ${ui.c.cyan(config.getAppUrl() + '/dashboard/settings')}`);
|
|
278
290
|
console.log(` 2. Copy your CLI token`);
|
|
279
|
-
console.log(` 3. Run: ${ui.c.bold('securenow login --token <YOUR_TOKEN>')}`);
|
|
291
|
+
console.log(` 3. Run: ${ui.c.bold('securenow admin login --token <YOUR_TOKEN>')}`);
|
|
280
292
|
console.log('');
|
|
281
293
|
} else {
|
|
282
294
|
throw err;
|
|
@@ -284,48 +296,118 @@ async function login(args, flags) {
|
|
|
284
296
|
}
|
|
285
297
|
}
|
|
286
298
|
|
|
299
|
+
async function adminLogin(args, flags) {
|
|
300
|
+
const local = flags.global ? false : true;
|
|
301
|
+
|
|
302
|
+
if (flags.token) {
|
|
303
|
+
const token = flags.token;
|
|
304
|
+
await loginWithToken(token);
|
|
305
|
+
const payload = decodeJwtPayload(token);
|
|
306
|
+
const email = payload?.email || 'unknown';
|
|
307
|
+
const exp = payload?.exp ? payload.exp * 1000 : null;
|
|
308
|
+
config.setAuth(token, email, exp, { local });
|
|
309
|
+
if (local) config.ensureLocalGitignore();
|
|
310
|
+
ui.success(`Admin auth connected as ${ui.c.bold(email)}`);
|
|
311
|
+
ui.info(local ? 'Saved to project .securenow/admin.json' : 'Saved to ~/.securenow/admin.json');
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const { token } = await loginWithBrowser({ mode: 'admin' });
|
|
316
|
+
const payload = decodeJwtPayload(token);
|
|
317
|
+
const email = payload?.email || 'unknown';
|
|
318
|
+
const exp = payload?.exp ? payload.exp * 1000 : null;
|
|
319
|
+
config.setAuth(token, email, exp, { local });
|
|
320
|
+
if (local) config.ensureLocalGitignore();
|
|
321
|
+
ui.success(`Admin auth connected as ${ui.c.bold(email)}`);
|
|
322
|
+
ui.info(local ? 'Saved to project .securenow/admin.json' : 'Saved to ~/.securenow/admin.json');
|
|
323
|
+
ui.info('Runtime app config was not changed.');
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
async function appConnect(args, flags) {
|
|
327
|
+
const local = flags.global ? false : true;
|
|
328
|
+
const { app, apiKey } = await loginWithBrowser({ mode: 'runtime' });
|
|
329
|
+
|
|
330
|
+
if (!app || !app.key) {
|
|
331
|
+
throw new CLIError('No app was selected. Runtime app config was not changed.');
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
config.setRuntime({
|
|
335
|
+
apiKey: apiKey || config.getApiKey() || null,
|
|
336
|
+
app: {
|
|
337
|
+
key: app.key,
|
|
338
|
+
name: app.name || null,
|
|
339
|
+
},
|
|
340
|
+
}, { local });
|
|
341
|
+
if (local) config.ensureLocalGitignore();
|
|
342
|
+
|
|
343
|
+
ui.success(`Runtime app connected to ${ui.c.bold(app.name || app.key)}`);
|
|
344
|
+
ui.info(local ? 'Saved to project .securenow/runtime.json' : 'Saved to ~/.securenow/runtime.json');
|
|
345
|
+
if (apiKey) {
|
|
346
|
+
ui.info('Firewall API key saved; SDK runtime can enforce firewall without an admin token.');
|
|
347
|
+
} else {
|
|
348
|
+
ui.warn('No firewall API key was returned. Run `securenow api-key create` if firewall sync needs a key.');
|
|
349
|
+
}
|
|
350
|
+
ui.info('Admin auth was not changed.');
|
|
351
|
+
}
|
|
352
|
+
|
|
287
353
|
async function logout(args, flags) {
|
|
288
354
|
// Default: clear project-local. --global clears ~/.securenow/.
|
|
289
355
|
const local = !(flags && flags.global);
|
|
290
|
-
const creds = config.
|
|
356
|
+
const creds = config.loadAdminCredentials();
|
|
291
357
|
config.clearCredentials({ local });
|
|
292
358
|
if (creds.email) {
|
|
293
|
-
ui.success(`
|
|
359
|
+
ui.success(`Admin auth logged out from ${ui.c.bold(creds.email)}`);
|
|
294
360
|
} else {
|
|
295
|
-
ui.success('
|
|
361
|
+
ui.success('Admin auth logged out');
|
|
296
362
|
}
|
|
297
|
-
ui.info(local ? 'Cleared project-local
|
|
363
|
+
ui.info(local ? 'Cleared project-local admin auth (.securenow/admin.json)' : 'Cleared global admin auth (~/.securenow/admin.json)');
|
|
364
|
+
ui.info('Runtime app config was not changed.');
|
|
298
365
|
}
|
|
299
366
|
|
|
300
367
|
async function whoami() {
|
|
301
|
-
const
|
|
368
|
+
const admin = config.loadAdminCredentials();
|
|
369
|
+
const runtime = config.loadRuntimeCredentials();
|
|
302
370
|
const token = config.getToken();
|
|
303
371
|
|
|
304
|
-
if (!token) {
|
|
305
|
-
ui.error('Not logged in. Run `securenow login` to authenticate.');
|
|
306
|
-
process.exit(1);
|
|
307
|
-
}
|
|
308
|
-
|
|
309
372
|
const payload = decodeJwtPayload(token);
|
|
310
373
|
|
|
311
|
-
ui.heading('
|
|
374
|
+
ui.heading('SecureNow Connection Status');
|
|
312
375
|
console.log('');
|
|
313
|
-
const
|
|
314
|
-
['
|
|
315
|
-
['
|
|
316
|
-
['
|
|
317
|
-
['Auth Source', config.getAuthSource()],
|
|
376
|
+
const adminPairs = [
|
|
377
|
+
['Status', token ? ui.c.green('connected') : ui.c.red('not connected')],
|
|
378
|
+
['Email', token ? (admin.email || payload?.email || 'unknown') : '—'],
|
|
379
|
+
['User ID', token ? (payload?.sub || 'unknown') : '—'],
|
|
380
|
+
['Auth Source', token ? config.getAuthSource() : '—'],
|
|
381
|
+
['System-rule admin', token ? 'server-enforced; use admin tools to verify' : 'no admin token'],
|
|
318
382
|
];
|
|
319
|
-
if (
|
|
320
|
-
const days = Math.ceil((
|
|
321
|
-
|
|
383
|
+
if (admin.expiresAt) {
|
|
384
|
+
const days = Math.ceil((admin.expiresAt - Date.now()) / (1000 * 60 * 60 * 24));
|
|
385
|
+
adminPairs.push(['Expires', days > 0 ? `in ${days} days` : ui.c.red('expired')]);
|
|
322
386
|
}
|
|
387
|
+
|
|
388
|
+
ui.heading('Admin CLI / MCP');
|
|
389
|
+
ui.keyValue(adminPairs);
|
|
390
|
+
console.log('');
|
|
391
|
+
|
|
392
|
+
const runtimePairs = [
|
|
393
|
+
['Status', runtime.app?.key ? ui.c.green('connected') : ui.c.red('no app selected')],
|
|
394
|
+
['App', runtime.app?.name || '—'],
|
|
395
|
+
['App Key', runtime.app?.key || '—'],
|
|
396
|
+
['Firewall Key', runtime.apiKey ? ui.c.green('present') : ui.c.yellow('missing')],
|
|
397
|
+
['Environment', runtime.config?.runtime?.deploymentEnvironment || 'production'],
|
|
398
|
+
['Runtime Source', config.getRuntimeSource()],
|
|
399
|
+
['API', config.getApiUrl()],
|
|
400
|
+
];
|
|
323
401
|
const defaultApp = config.getDefaultApp();
|
|
324
402
|
if (defaultApp) {
|
|
325
|
-
|
|
403
|
+
runtimePairs.push(['CLI Default App', defaultApp]);
|
|
326
404
|
}
|
|
327
|
-
ui.
|
|
405
|
+
ui.heading('SDK Runtime');
|
|
406
|
+
ui.keyValue(runtimePairs);
|
|
328
407
|
console.log('');
|
|
408
|
+
|
|
409
|
+
if (!token) ui.info('Run `securenow admin login` for admin/control-plane CLI and MCP tools.');
|
|
410
|
+
if (!runtime.app?.key) ui.info('Run `securenow app connect` to select an app and write SDK runtime config.');
|
|
329
411
|
}
|
|
330
412
|
|
|
331
|
-
module.exports = { login, logout, whoami };
|
|
413
|
+
module.exports = { login, logout, whoami, adminLogin, appConnect, loginWithBrowser };
|
package/cli/client.js
CHANGED
|
@@ -50,14 +50,15 @@ function request(method, endpoint, { body, query, token, raw } = {}) {
|
|
|
50
50
|
parsed = data;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
if (res.statusCode === 401) {
|
|
54
|
-
reject(new CLIError('
|
|
55
|
-
return;
|
|
56
|
-
}
|
|
57
|
-
if (res.statusCode === 403) {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
53
|
+
if (res.statusCode === 401) {
|
|
54
|
+
reject(new CLIError('Admin auth is missing or expired. Run `securenow admin login` to re-authenticate. Runtime app credentials are unrelated.', 401));
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (res.statusCode === 403) {
|
|
58
|
+
const msg = parsed?.error || parsed?.message || 'Access denied';
|
|
59
|
+
reject(new CLIError(`${msg}. Admin auth is connected, but this user/plan may lack permission for this control-plane operation. Runtime app connection is unrelated.`, 403));
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
61
62
|
if (res.statusCode >= 400) {
|
|
62
63
|
const msg = parsed?.error || parsed?.message || `Request failed (HTTP ${res.statusCode})`;
|
|
63
64
|
const details = parsed?.details || parsed?.unauthorizedKeys;
|
|
@@ -96,11 +97,11 @@ class CLIError extends Error {
|
|
|
96
97
|
}
|
|
97
98
|
|
|
98
99
|
function requireAuth() {
|
|
99
|
-
const token = config.getToken();
|
|
100
|
-
if (!token) {
|
|
101
|
-
ui.error('
|
|
102
|
-
process.exit(1);
|
|
103
|
-
}
|
|
100
|
+
const token = config.getToken();
|
|
101
|
+
if (!token) {
|
|
102
|
+
ui.error('Admin auth is not connected. Run `securenow admin login` first.');
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
104
105
|
return token;
|
|
105
106
|
}
|
|
106
107
|
|
package/cli/config.js
CHANGED
|
@@ -8,10 +8,14 @@ const appConfig = require('../app-config');
|
|
|
8
8
|
const CONFIG_DIR = path.join(os.homedir(), '.securenow');
|
|
9
9
|
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
|
10
10
|
const CREDENTIALS_FILE = path.join(CONFIG_DIR, 'credentials.json');
|
|
11
|
+
const ADMIN_CREDENTIALS_FILE = path.join(CONFIG_DIR, 'admin.json');
|
|
12
|
+
const RUNTIME_CREDENTIALS_FILE = path.join(CONFIG_DIR, 'runtime.json');
|
|
11
13
|
|
|
12
14
|
const LOCAL_CONFIG_DIR = path.join(process.cwd(), '.securenow');
|
|
13
15
|
const LOCAL_CONFIG_FILE = path.join(LOCAL_CONFIG_DIR, 'config.json');
|
|
14
16
|
const LOCAL_CREDENTIALS_FILE = path.join(LOCAL_CONFIG_DIR, 'credentials.json');
|
|
17
|
+
const LOCAL_ADMIN_CREDENTIALS_FILE = path.join(LOCAL_CONFIG_DIR, 'admin.json');
|
|
18
|
+
const LOCAL_RUNTIME_CREDENTIALS_FILE = path.join(LOCAL_CONFIG_DIR, 'runtime.json');
|
|
15
19
|
|
|
16
20
|
const DEFAULTS = {
|
|
17
21
|
apiUrl: 'https://api.securenow.ai',
|
|
@@ -57,18 +61,90 @@ function credentialsFileForLocal(local) {
|
|
|
57
61
|
return local ? LOCAL_CREDENTIALS_FILE : CREDENTIALS_FILE;
|
|
58
62
|
}
|
|
59
63
|
|
|
64
|
+
function adminCredentialsFileForLocal(local) {
|
|
65
|
+
return local ? LOCAL_ADMIN_CREDENTIALS_FILE : ADMIN_CREDENTIALS_FILE;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function runtimeCredentialsFileForLocal(local) {
|
|
69
|
+
return local ? LOCAL_RUNTIME_CREDENTIALS_FILE : RUNTIME_CREDENTIALS_FILE;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function normalizeRuntimeCredentials(creds) {
|
|
73
|
+
const payload = { ...(creds || {}) };
|
|
74
|
+
delete payload.token;
|
|
75
|
+
delete payload.email;
|
|
76
|
+
delete payload.expiresAt;
|
|
77
|
+
delete payload.admin;
|
|
78
|
+
return appConfig.withCredentialDefaults(payload) || {};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function normalizeAdminCredentials(creds) {
|
|
82
|
+
const payload = { ...(creds || {}) };
|
|
83
|
+
delete payload.apiKey;
|
|
84
|
+
delete payload.app;
|
|
85
|
+
delete payload.config;
|
|
86
|
+
delete payload.runtime;
|
|
87
|
+
return payload;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function splitCredentialDocument(raw = {}) {
|
|
91
|
+
const doc = raw && typeof raw === 'object' && !Array.isArray(raw) ? raw : {};
|
|
92
|
+
const runtime = doc.runtime && typeof doc.runtime === 'object' && !Array.isArray(doc.runtime)
|
|
93
|
+
? doc.runtime
|
|
94
|
+
: doc;
|
|
95
|
+
const admin = doc.admin && typeof doc.admin === 'object' && !Array.isArray(doc.admin)
|
|
96
|
+
? doc.admin
|
|
97
|
+
: doc;
|
|
98
|
+
return { runtime, admin };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function pickExistingFile(...files) {
|
|
102
|
+
for (const file of files) {
|
|
103
|
+
try {
|
|
104
|
+
if (file && fs.existsSync(file)) return file;
|
|
105
|
+
} catch {}
|
|
106
|
+
}
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
|
|
60
110
|
function hasLocalCredentials() {
|
|
61
|
-
return !!
|
|
111
|
+
return !!(
|
|
112
|
+
appConfig.resolveLocalCredentialsFile() ||
|
|
113
|
+
appConfig.resolveLocalAdminCredentialsFile()
|
|
114
|
+
);
|
|
62
115
|
}
|
|
63
116
|
|
|
64
117
|
function resolveCredentialsFile() {
|
|
65
118
|
return appConfig.resolveLocalCredentialsFile() || appConfig.resolveGlobalCredentialsFile() || CREDENTIALS_FILE;
|
|
66
119
|
}
|
|
67
120
|
|
|
121
|
+
function resolveAdminCredentialsFile() {
|
|
122
|
+
return appConfig.resolveLocalAdminCredentialsFile() || appConfig.resolveGlobalAdminCredentialsFile() || ADMIN_CREDENTIALS_FILE;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function resolveRuntimeCredentialsFile() {
|
|
126
|
+
return appConfig.resolveLocalCredentialsFile() || appConfig.resolveGlobalCredentialsFile() || RUNTIME_CREDENTIALS_FILE;
|
|
127
|
+
}
|
|
128
|
+
|
|
68
129
|
function getAuthSource() {
|
|
69
130
|
if (process.env.SECURENOW_TOKEN) return 'env (SECURENOW_TOKEN)';
|
|
70
|
-
|
|
71
|
-
return '
|
|
131
|
+
const file = appConfig.resolveLocalAdminCredentialsFile() || appConfig.resolveGlobalAdminCredentialsFile();
|
|
132
|
+
if (!file) return 'not configured';
|
|
133
|
+
if (file.includes(`${path.sep}.securenow${path.sep}`) && file.startsWith(process.cwd())) {
|
|
134
|
+
return file.endsWith('admin.json') ? 'project admin (.securenow/admin.json)' : 'project legacy (.securenow/credentials.json)';
|
|
135
|
+
}
|
|
136
|
+
if (file.endsWith('admin.json')) return 'global admin (~/.securenow/admin.json)';
|
|
137
|
+
return 'global legacy (~/.securenow/credentials.json)';
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function getRuntimeSource() {
|
|
141
|
+
const file = appConfig.resolveLocalCredentialsFile() || appConfig.resolveGlobalCredentialsFile();
|
|
142
|
+
if (!file) return 'not configured';
|
|
143
|
+
if (file.includes(`${path.sep}.securenow${path.sep}`) && file.startsWith(process.cwd())) {
|
|
144
|
+
return file.endsWith('runtime.json') ? 'project runtime (.securenow/runtime.json)' : 'project legacy (.securenow/credentials.json)';
|
|
145
|
+
}
|
|
146
|
+
if (file.endsWith('runtime.json')) return 'global runtime (~/.securenow/runtime.json)';
|
|
147
|
+
return 'global legacy (~/.securenow/credentials.json)';
|
|
72
148
|
}
|
|
73
149
|
|
|
74
150
|
function loadConfig() {
|
|
@@ -98,14 +174,112 @@ function setConfigValue(key, value) {
|
|
|
98
174
|
}
|
|
99
175
|
|
|
100
176
|
function loadCredentials() {
|
|
101
|
-
const
|
|
102
|
-
const
|
|
103
|
-
|
|
177
|
+
const legacy = splitCredentialDocument(loadJSON(resolveCredentialsFile()));
|
|
178
|
+
const runtime = loadRuntimeCredentials();
|
|
179
|
+
const admin = loadAdminCredentials();
|
|
180
|
+
return appConfig.mergeCredentials(
|
|
181
|
+
normalizeRuntimeCredentials(runtime || legacy.runtime),
|
|
182
|
+
normalizeAdminCredentials(admin || legacy.admin)
|
|
183
|
+
) || {};
|
|
104
184
|
}
|
|
105
185
|
|
|
106
186
|
function saveCredentials(creds, { local = false } = {}) {
|
|
107
|
-
|
|
108
|
-
|
|
187
|
+
saveRuntimeCredentials(creds, { local });
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function loadAdminCredentials() {
|
|
191
|
+
if (process.env.SECURENOW_TOKEN) {
|
|
192
|
+
return { token: process.env.SECURENOW_TOKEN, source: 'env' };
|
|
193
|
+
}
|
|
194
|
+
const adminFile = pickExistingFile(
|
|
195
|
+
appConfig.resolveLocalAdminCredentialsFile(),
|
|
196
|
+
appConfig.resolveGlobalAdminCredentialsFile()
|
|
197
|
+
);
|
|
198
|
+
if (!adminFile) return {};
|
|
199
|
+
const { admin } = splitCredentialDocument(loadJSON(adminFile));
|
|
200
|
+
return normalizeAdminCredentials(admin);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function saveAdminCredentials(creds, { local = false } = {}) {
|
|
204
|
+
const targetFile = adminCredentialsFileForLocal(local);
|
|
205
|
+
const existing = normalizeAdminCredentials(loadJSON(targetFile));
|
|
206
|
+
const payload = normalizeAdminCredentials({ ...existing, ...(creds || {}) });
|
|
207
|
+
payload._securenow = {
|
|
208
|
+
...(payload._securenow || {}),
|
|
209
|
+
schemaVersion: appConfig.CONFIG_SCHEMA_VERSION,
|
|
210
|
+
note: 'SecureNow admin/control-plane CLI and MCP auth. This file may contain a user session token; do not commit it.',
|
|
211
|
+
runtimeSeparate: 'SDK runtime app credentials live in runtime.json and are not changed by admin login/logout.',
|
|
212
|
+
};
|
|
213
|
+
saveJSON(targetFile, payload);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function stripAdminFromCredentialFile(filepath) {
|
|
217
|
+
try {
|
|
218
|
+
if (!filepath || !fs.existsSync(filepath)) return;
|
|
219
|
+
const doc = loadJSON(filepath);
|
|
220
|
+
if (!doc || typeof doc !== 'object' || Array.isArray(doc)) return;
|
|
221
|
+
|
|
222
|
+
let changed = false;
|
|
223
|
+
if (doc.admin) {
|
|
224
|
+
delete doc.admin;
|
|
225
|
+
changed = true;
|
|
226
|
+
}
|
|
227
|
+
for (const field of ['token', 'email', 'expiresAt', 'roles', 'plan', 'user', 'userId']) {
|
|
228
|
+
if (Object.prototype.hasOwnProperty.call(doc, field)) {
|
|
229
|
+
delete doc[field];
|
|
230
|
+
changed = true;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
if (changed) saveJSON(filepath, doc);
|
|
234
|
+
} catch {}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function clearAdminCredentials({ local } = {}) {
|
|
238
|
+
try {
|
|
239
|
+
const useLocal = local === true || (local == null && pickExistingFile(LOCAL_ADMIN_CREDENTIALS_FILE, LOCAL_CREDENTIALS_FILE));
|
|
240
|
+
if (useLocal) {
|
|
241
|
+
fs.unlinkSync(LOCAL_ADMIN_CREDENTIALS_FILE);
|
|
242
|
+
} else {
|
|
243
|
+
fs.unlinkSync(ADMIN_CREDENTIALS_FILE);
|
|
244
|
+
}
|
|
245
|
+
} catch {}
|
|
246
|
+
if (local === true) {
|
|
247
|
+
stripAdminFromCredentialFile(LOCAL_CREDENTIALS_FILE);
|
|
248
|
+
} else if (local === false) {
|
|
249
|
+
stripAdminFromCredentialFile(CREDENTIALS_FILE);
|
|
250
|
+
} else if (pickExistingFile(LOCAL_CREDENTIALS_FILE)) {
|
|
251
|
+
stripAdminFromCredentialFile(LOCAL_CREDENTIALS_FILE);
|
|
252
|
+
} else {
|
|
253
|
+
stripAdminFromCredentialFile(CREDENTIALS_FILE);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function loadRuntimeCredentials() {
|
|
258
|
+
const runtimeFile = pickExistingFile(
|
|
259
|
+
appConfig.resolveLocalCredentialsFile(),
|
|
260
|
+
appConfig.resolveGlobalCredentialsFile()
|
|
261
|
+
);
|
|
262
|
+
if (!runtimeFile) return {};
|
|
263
|
+
const { runtime } = splitCredentialDocument(loadJSON(runtimeFile));
|
|
264
|
+
return normalizeRuntimeCredentials(runtime);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function saveRuntimeCredentials(creds, { local = false } = {}) {
|
|
268
|
+
const targetFile = runtimeCredentialsFileForLocal(local);
|
|
269
|
+
const existing = normalizeRuntimeCredentials(loadJSON(targetFile));
|
|
270
|
+
saveJSON(targetFile, normalizeRuntimeCredentials(appConfig.mergeCredentials(existing, creds || {}) || {}));
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function clearRuntimeCredentials({ local } = {}) {
|
|
274
|
+
try {
|
|
275
|
+
if (local === true) {
|
|
276
|
+
fs.unlinkSync(LOCAL_RUNTIME_CREDENTIALS_FILE);
|
|
277
|
+
} else if (local === false || !pickExistingFile(LOCAL_RUNTIME_CREDENTIALS_FILE)) {
|
|
278
|
+
fs.unlinkSync(RUNTIME_CREDENTIALS_FILE);
|
|
279
|
+
} else {
|
|
280
|
+
fs.unlinkSync(LOCAL_RUNTIME_CREDENTIALS_FILE);
|
|
281
|
+
}
|
|
282
|
+
} catch {}
|
|
109
283
|
}
|
|
110
284
|
|
|
111
285
|
function withOnboardingFirewallEnabled(creds) {
|
|
@@ -118,8 +292,8 @@ function withOnboardingFirewallEnabled(creds) {
|
|
|
118
292
|
|
|
119
293
|
function ensureCredentialDefaults({ local, enableFirewall = false } = {}) {
|
|
120
294
|
const useLocal = local === true || (local == null && hasLocalCredentials());
|
|
121
|
-
const targetFile =
|
|
122
|
-
const existing = useLocal ?
|
|
295
|
+
const targetFile = runtimeCredentialsFileForLocal(useLocal);
|
|
296
|
+
const existing = useLocal ? loadRuntimeCredentials() : loadJSON(targetFile);
|
|
123
297
|
const payload = enableFirewall
|
|
124
298
|
? withOnboardingFirewallEnabled(existing || {})
|
|
125
299
|
: appConfig.withCredentialDefaults(existing || {}) || {};
|
|
@@ -127,21 +301,13 @@ function ensureCredentialDefaults({ local, enableFirewall = false } = {}) {
|
|
|
127
301
|
}
|
|
128
302
|
|
|
129
303
|
function clearCredentials({ local } = {}) {
|
|
130
|
-
|
|
131
|
-
if (local === true) {
|
|
132
|
-
fs.unlinkSync(LOCAL_CREDENTIALS_FILE);
|
|
133
|
-
} else if (local === false || !hasLocalCredentials()) {
|
|
134
|
-
fs.unlinkSync(CREDENTIALS_FILE);
|
|
135
|
-
} else {
|
|
136
|
-
fs.unlinkSync(LOCAL_CREDENTIALS_FILE);
|
|
137
|
-
}
|
|
138
|
-
} catch {}
|
|
304
|
+
clearAdminCredentials({ local });
|
|
139
305
|
}
|
|
140
306
|
|
|
141
307
|
function getToken() {
|
|
142
308
|
if (process.env.SECURENOW_TOKEN) return process.env.SECURENOW_TOKEN;
|
|
143
309
|
|
|
144
|
-
const creds =
|
|
310
|
+
const creds = loadAdminCredentials();
|
|
145
311
|
if (!creds.token) return null;
|
|
146
312
|
|
|
147
313
|
if (creds.expiresAt && Date.now() > creds.expiresAt) {
|
|
@@ -150,66 +316,62 @@ function getToken() {
|
|
|
150
316
|
return creds.token;
|
|
151
317
|
}
|
|
152
318
|
|
|
153
|
-
function setAuth(token, email, expiresAt, { local = false
|
|
154
|
-
|
|
155
|
-
const payload = { ...loadJSON(targetFile), token, email, expiresAt };
|
|
156
|
-
if (app && (app.key || app.name)) {
|
|
157
|
-
payload.app = {
|
|
158
|
-
key: app.key || null,
|
|
159
|
-
name: app.name || null,
|
|
160
|
-
};
|
|
161
|
-
}
|
|
162
|
-
saveJSON(
|
|
163
|
-
targetFile,
|
|
164
|
-
enableFirewall ? withOnboardingFirewallEnabled(payload) : appConfig.withCredentialDefaults(payload) || {}
|
|
165
|
-
);
|
|
319
|
+
function setAuth(token, email, expiresAt, { local = false } = {}) {
|
|
320
|
+
saveAdminCredentials({ token, email, expiresAt }, { local });
|
|
166
321
|
}
|
|
167
322
|
|
|
168
323
|
function getApp() {
|
|
169
|
-
const creds =
|
|
324
|
+
const creds = loadRuntimeCredentials();
|
|
170
325
|
return creds && creds.app ? creds.app : null;
|
|
171
326
|
}
|
|
172
327
|
|
|
173
328
|
function setApiKey(apiKey, { local } = {}) {
|
|
174
329
|
const useLocal = local === true || (local == null && hasLocalCredentials());
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
saveJSON(targetFile, withOnboardingFirewallEnabled({ ...existing, apiKey }));
|
|
330
|
+
const existing = loadRuntimeCredentials();
|
|
331
|
+
saveRuntimeCredentials(withOnboardingFirewallEnabled({ ...existing, apiKey }), { local: useLocal });
|
|
178
332
|
}
|
|
179
333
|
|
|
180
334
|
function clearApiKey({ local } = {}) {
|
|
181
335
|
const useLocal = local === true || (local == null && hasLocalCredentials());
|
|
182
|
-
const
|
|
183
|
-
const existing = loadJSON(targetFile);
|
|
336
|
+
const existing = loadRuntimeCredentials();
|
|
184
337
|
if (!existing || !existing.apiKey) return;
|
|
185
338
|
delete existing.apiKey;
|
|
186
|
-
|
|
339
|
+
saveRuntimeCredentials(appConfig.withCredentialDefaults(existing) || existing, { local: useLocal });
|
|
187
340
|
}
|
|
188
341
|
|
|
189
342
|
function getApiKey() {
|
|
190
|
-
const creds =
|
|
343
|
+
const creds = loadRuntimeCredentials();
|
|
191
344
|
return creds && creds.apiKey ? creds.apiKey : null;
|
|
192
345
|
}
|
|
193
346
|
|
|
194
347
|
function setApp(app, { local } = {}) {
|
|
195
348
|
const useLocal = local === true || (local == null && hasLocalCredentials());
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
-
saveJSON(targetFile, appConfig.withCredentialDefaults({
|
|
349
|
+
const existing = loadRuntimeCredentials();
|
|
350
|
+
saveRuntimeCredentials(appConfig.withCredentialDefaults({
|
|
199
351
|
...existing,
|
|
200
352
|
app: {
|
|
201
353
|
key: app.key || null,
|
|
202
354
|
name: app.name || null,
|
|
203
355
|
},
|
|
204
|
-
}) || {});
|
|
356
|
+
}) || {}, { local: useLocal });
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function setRuntime(runtime, { local } = {}) {
|
|
360
|
+
const useLocal = local === true || (local == null && hasLocalCredentials());
|
|
361
|
+
const existing = loadRuntimeCredentials();
|
|
362
|
+
saveRuntimeCredentials(withOnboardingFirewallEnabled(appConfig.mergeCredentials(existing, runtime || {}) || {}), { local: useLocal });
|
|
205
363
|
}
|
|
206
364
|
|
|
207
365
|
function ensureLocalGitignore() {
|
|
208
366
|
const gitignorePath = path.join(process.cwd(), '.gitignore');
|
|
209
367
|
const legacyEntry = '.securenow/';
|
|
210
368
|
const entries = [
|
|
369
|
+
'.securenow/admin.json',
|
|
370
|
+
'.securenow/runtime.json',
|
|
211
371
|
'.securenow/credentials.json',
|
|
212
372
|
'.securenow/credentials.*.json',
|
|
373
|
+
'!.securenow/admin.example.json',
|
|
374
|
+
'!.securenow/runtime.example.json',
|
|
213
375
|
'!.securenow/credentials.example.json',
|
|
214
376
|
'!.securenow/credentials.*.example.json',
|
|
215
377
|
];
|
|
@@ -268,23 +430,35 @@ module.exports = {
|
|
|
268
430
|
CONFIG_DIR,
|
|
269
431
|
CONFIG_FILE,
|
|
270
432
|
CREDENTIALS_FILE,
|
|
433
|
+
ADMIN_CREDENTIALS_FILE,
|
|
434
|
+
RUNTIME_CREDENTIALS_FILE,
|
|
271
435
|
LOCAL_CONFIG_DIR,
|
|
272
436
|
LOCAL_CREDENTIALS_FILE,
|
|
437
|
+
LOCAL_ADMIN_CREDENTIALS_FILE,
|
|
438
|
+
LOCAL_RUNTIME_CREDENTIALS_FILE,
|
|
273
439
|
loadConfig,
|
|
274
440
|
saveConfig,
|
|
275
441
|
getConfigValue,
|
|
276
442
|
setConfigValue,
|
|
277
443
|
loadCredentials,
|
|
278
444
|
saveCredentials,
|
|
445
|
+
loadAdminCredentials,
|
|
446
|
+
saveAdminCredentials,
|
|
447
|
+
clearAdminCredentials,
|
|
448
|
+
loadRuntimeCredentials,
|
|
449
|
+
saveRuntimeCredentials,
|
|
450
|
+
clearRuntimeCredentials,
|
|
279
451
|
clearCredentials,
|
|
280
452
|
getToken,
|
|
281
453
|
setAuth,
|
|
282
454
|
getApp,
|
|
283
455
|
setApp,
|
|
456
|
+
setRuntime,
|
|
284
457
|
setApiKey,
|
|
285
458
|
clearApiKey,
|
|
286
459
|
getApiKey,
|
|
287
460
|
getAuthSource,
|
|
461
|
+
getRuntimeSource,
|
|
288
462
|
hasLocalCredentials,
|
|
289
463
|
ensureCredentialDefaults,
|
|
290
464
|
withOnboardingFirewallEnabled,
|
package/cli/credentials.js
CHANGED
|
@@ -14,7 +14,7 @@ function maskSecret(value) {
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
function buildRuntimeCredentials(options = {}) {
|
|
17
|
-
const creds = config.
|
|
17
|
+
const creds = config.loadRuntimeCredentials() || {};
|
|
18
18
|
const deploymentEnvironment =
|
|
19
19
|
options.environment ||
|
|
20
20
|
options.env ||
|
|
@@ -67,10 +67,10 @@ async function runtime(_args, flags) {
|
|
|
67
67
|
const warn = flags.stdout ? (msg) => console.error(`! ${msg}`) : ui.warn;
|
|
68
68
|
|
|
69
69
|
if (!creds.app || !creds.app.key) {
|
|
70
|
-
warn('No app key found. Run `npx securenow
|
|
70
|
+
warn('No app key found. Run `npx securenow app connect` first so telemetry routes to the selected app.');
|
|
71
71
|
}
|
|
72
72
|
if (!creds.apiKey) {
|
|
73
|
-
warn('Runtime firewall enforcement key is missing. Run `npx securenow
|
|
73
|
+
warn('Runtime firewall enforcement key is missing. Run `npx securenow app connect` or `npx securenow api-key set snk_live_...` before generating production runtime credentials.');
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
if (flags.stdout) {
|