securenow 7.7.16 → 8.0.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/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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
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, enableFirewall: true });
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,17 +254,28 @@ 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, app, enableFirewall: true });
257
- if (apiKey) config.setApiKey(apiKey, { local });
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 ? 'Credentials saved to project .securenow/ (local)' : 'Credentials saved to ~/.securenow/ (global)');
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
- ui.info(`Firewall API key saved — the firewall will activate automatically on next start`);
278
+ ui.info(`Runtime API key saved — telemetry ingestion and firewall sync will authenticate automatically on next start`);
267
279
  }
268
280
  if (exp) {
269
281
  const days = Math.ceil((exp - Date.now()) / (1000 * 60 * 60 * 24));
@@ -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('Runtime API key saved; SDK telemetry and firewall sync can run without an admin token.');
347
+ } else {
348
+ ui.warn('No runtime API key was returned. Run `securenow api-key create` before sending telemetry.');
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.loadCredentials();
356
+ const creds = config.loadAdminCredentials();
291
357
  config.clearCredentials({ local });
292
358
  if (creds.email) {
293
- ui.success(`Logged out from ${ui.c.bold(creds.email)}`);
359
+ ui.success(`Admin auth logged out from ${ui.c.bold(creds.email)}`);
294
360
  } else {
295
- ui.success('Logged out');
361
+ ui.success('Admin auth logged out');
296
362
  }
297
- ui.info(local ? 'Cleared project-local credentials (.securenow/)' : 'Cleared global credentials (~/.securenow/)');
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 creds = config.loadCredentials();
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('Current Session');
374
+ ui.heading('SecureNow Connection Status');
312
375
  console.log('');
313
- const pairs = [
314
- ['Email', creds.email || payload?.email || 'unknown'],
315
- ['User ID', payload?.sub || 'unknown'],
316
- ['API', config.getApiUrl()],
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 (creds.expiresAt) {
320
- const days = Math.ceil((creds.expiresAt - Date.now()) / (1000 * 60 * 60 * 24));
321
- pairs.push(['Expires', days > 0 ? `in ${days} days` : ui.c.red('expired')]);
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
+ ['Runtime API 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
- pairs.push(['Default App', defaultApp]);
403
+ runtimePairs.push(['CLI Default App', defaultApp]);
326
404
  }
327
- ui.keyValue(pairs);
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('Session expired. Run `securenow login` to re-authenticate.', 401));
55
- return;
56
- }
57
- if (res.statusCode === 403) {
58
- reject(new CLIError('Access denied. You may need to upgrade your plan.', 403));
59
- return;
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('Not logged in. Run `securenow login` first.');
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,89 @@ 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 !!appConfig.resolveLocalCredentialsFile();
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
- if (process.env.SECURENOW_TOKEN) return 'env (SECURENOW_TOKEN)';
70
- if (appConfig.resolveLocalCredentialsFile()) return 'project (.securenow/)';
71
- return 'global (~/.securenow/)';
130
+ const file = appConfig.resolveLocalAdminCredentialsFile() || appConfig.resolveGlobalAdminCredentialsFile();
131
+ if (!file) return 'not configured';
132
+ if (file.includes(`${path.sep}.securenow${path.sep}`) && file.startsWith(process.cwd())) {
133
+ return file.endsWith('admin.json') ? 'project admin (.securenow/admin.json)' : 'project legacy (.securenow/credentials.json)';
134
+ }
135
+ if (file.endsWith('admin.json')) return 'global admin (~/.securenow/admin.json)';
136
+ return 'global legacy (~/.securenow/credentials.json)';
137
+ }
138
+
139
+ function getRuntimeSource() {
140
+ const file = appConfig.resolveLocalCredentialsFile() || appConfig.resolveGlobalCredentialsFile();
141
+ if (!file) return 'not configured';
142
+ if (file.includes(`${path.sep}.securenow${path.sep}`) && file.startsWith(process.cwd())) {
143
+ return file.endsWith('runtime.json') ? 'project runtime (.securenow/runtime.json)' : 'project legacy (.securenow/credentials.json)';
144
+ }
145
+ if (file.endsWith('runtime.json')) return 'global runtime (~/.securenow/runtime.json)';
146
+ return 'global legacy (~/.securenow/credentials.json)';
72
147
  }
73
148
 
74
149
  function loadConfig() {
@@ -98,14 +173,109 @@ function setConfigValue(key, value) {
98
173
  }
99
174
 
100
175
  function loadCredentials() {
101
- const credentialsFile = resolveCredentialsFile();
102
- const credentials = loadJSON(credentialsFile);
103
- return appConfig.withCredentialDefaults(credentials) || {};
176
+ const legacy = splitCredentialDocument(loadJSON(resolveCredentialsFile()));
177
+ const runtime = loadRuntimeCredentials();
178
+ const admin = loadAdminCredentials();
179
+ return appConfig.mergeCredentials(
180
+ normalizeRuntimeCredentials(runtime || legacy.runtime),
181
+ normalizeAdminCredentials(admin || legacy.admin)
182
+ ) || {};
104
183
  }
105
184
 
106
185
  function saveCredentials(creds, { local = false } = {}) {
107
- const targetFile = credentialsFileForLocal(local);
108
- saveJSON(targetFile, appConfig.withCredentialDefaults(creds) || {});
186
+ saveRuntimeCredentials(creds, { local });
187
+ }
188
+
189
+ function loadAdminCredentials() {
190
+ const adminFile = pickExistingFile(
191
+ appConfig.resolveLocalAdminCredentialsFile(),
192
+ appConfig.resolveGlobalAdminCredentialsFile()
193
+ );
194
+ if (!adminFile) return {};
195
+ const { admin } = splitCredentialDocument(loadJSON(adminFile));
196
+ return normalizeAdminCredentials(admin);
197
+ }
198
+
199
+ function saveAdminCredentials(creds, { local = false } = {}) {
200
+ const targetFile = adminCredentialsFileForLocal(local);
201
+ const existing = normalizeAdminCredentials(loadJSON(targetFile));
202
+ const payload = normalizeAdminCredentials({ ...existing, ...(creds || {}) });
203
+ payload._securenow = {
204
+ ...(payload._securenow || {}),
205
+ schemaVersion: appConfig.CONFIG_SCHEMA_VERSION,
206
+ note: 'SecureNow admin/control-plane CLI and MCP auth. This file may contain a user session token; do not commit it.',
207
+ runtimeSeparate: 'SDK runtime app credentials live in runtime.json and are not changed by admin login/logout.',
208
+ };
209
+ saveJSON(targetFile, payload);
210
+ }
211
+
212
+ function stripAdminFromCredentialFile(filepath) {
213
+ try {
214
+ if (!filepath || !fs.existsSync(filepath)) return;
215
+ const doc = loadJSON(filepath);
216
+ if (!doc || typeof doc !== 'object' || Array.isArray(doc)) return;
217
+
218
+ let changed = false;
219
+ if (doc.admin) {
220
+ delete doc.admin;
221
+ changed = true;
222
+ }
223
+ for (const field of ['token', 'email', 'expiresAt', 'roles', 'plan', 'user', 'userId']) {
224
+ if (Object.prototype.hasOwnProperty.call(doc, field)) {
225
+ delete doc[field];
226
+ changed = true;
227
+ }
228
+ }
229
+ if (changed) saveJSON(filepath, doc);
230
+ } catch {}
231
+ }
232
+
233
+ function clearAdminCredentials({ local } = {}) {
234
+ try {
235
+ const useLocal = local === true || (local == null && pickExistingFile(LOCAL_ADMIN_CREDENTIALS_FILE, LOCAL_CREDENTIALS_FILE));
236
+ if (useLocal) {
237
+ fs.unlinkSync(LOCAL_ADMIN_CREDENTIALS_FILE);
238
+ } else {
239
+ fs.unlinkSync(ADMIN_CREDENTIALS_FILE);
240
+ }
241
+ } catch {}
242
+ if (local === true) {
243
+ stripAdminFromCredentialFile(LOCAL_CREDENTIALS_FILE);
244
+ } else if (local === false) {
245
+ stripAdminFromCredentialFile(CREDENTIALS_FILE);
246
+ } else if (pickExistingFile(LOCAL_CREDENTIALS_FILE)) {
247
+ stripAdminFromCredentialFile(LOCAL_CREDENTIALS_FILE);
248
+ } else {
249
+ stripAdminFromCredentialFile(CREDENTIALS_FILE);
250
+ }
251
+ }
252
+
253
+ function loadRuntimeCredentials() {
254
+ const runtimeFile = pickExistingFile(
255
+ appConfig.resolveLocalCredentialsFile(),
256
+ appConfig.resolveGlobalCredentialsFile()
257
+ );
258
+ if (!runtimeFile) return {};
259
+ const { runtime } = splitCredentialDocument(loadJSON(runtimeFile));
260
+ return normalizeRuntimeCredentials(runtime);
261
+ }
262
+
263
+ function saveRuntimeCredentials(creds, { local = false } = {}) {
264
+ const targetFile = runtimeCredentialsFileForLocal(local);
265
+ const existing = normalizeRuntimeCredentials(loadJSON(targetFile));
266
+ saveJSON(targetFile, normalizeRuntimeCredentials(appConfig.mergeCredentials(existing, creds || {}) || {}));
267
+ }
268
+
269
+ function clearRuntimeCredentials({ local } = {}) {
270
+ try {
271
+ if (local === true) {
272
+ fs.unlinkSync(LOCAL_RUNTIME_CREDENTIALS_FILE);
273
+ } else if (local === false || !pickExistingFile(LOCAL_RUNTIME_CREDENTIALS_FILE)) {
274
+ fs.unlinkSync(RUNTIME_CREDENTIALS_FILE);
275
+ } else {
276
+ fs.unlinkSync(LOCAL_RUNTIME_CREDENTIALS_FILE);
277
+ }
278
+ } catch {}
109
279
  }
110
280
 
111
281
  function withOnboardingFirewallEnabled(creds) {
@@ -118,8 +288,8 @@ function withOnboardingFirewallEnabled(creds) {
118
288
 
119
289
  function ensureCredentialDefaults({ local, enableFirewall = false } = {}) {
120
290
  const useLocal = local === true || (local == null && hasLocalCredentials());
121
- const targetFile = credentialsFileForLocal(useLocal);
122
- const existing = useLocal ? credentialsForWrite(targetFile) : loadJSON(targetFile);
291
+ const targetFile = runtimeCredentialsFileForLocal(useLocal);
292
+ const existing = useLocal ? loadRuntimeCredentials() : loadJSON(targetFile);
123
293
  const payload = enableFirewall
124
294
  ? withOnboardingFirewallEnabled(existing || {})
125
295
  : appConfig.withCredentialDefaults(existing || {}) || {};
@@ -127,21 +297,11 @@ function ensureCredentialDefaults({ local, enableFirewall = false } = {}) {
127
297
  }
128
298
 
129
299
  function clearCredentials({ local } = {}) {
130
- try {
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 {}
300
+ clearAdminCredentials({ local });
139
301
  }
140
302
 
141
303
  function getToken() {
142
- if (process.env.SECURENOW_TOKEN) return process.env.SECURENOW_TOKEN;
143
-
144
- const creds = loadCredentials();
304
+ const creds = loadAdminCredentials();
145
305
  if (!creds.token) return null;
146
306
 
147
307
  if (creds.expiresAt && Date.now() > creds.expiresAt) {
@@ -150,66 +310,62 @@ function getToken() {
150
310
  return creds.token;
151
311
  }
152
312
 
153
- function setAuth(token, email, expiresAt, { local = false, app = null, enableFirewall = false } = {}) {
154
- const targetFile = credentialsFileForLocal(local);
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
- );
313
+ function setAuth(token, email, expiresAt, { local = false } = {}) {
314
+ saveAdminCredentials({ token, email, expiresAt }, { local });
166
315
  }
167
316
 
168
317
  function getApp() {
169
- const creds = loadCredentials();
318
+ const creds = loadRuntimeCredentials();
170
319
  return creds && creds.app ? creds.app : null;
171
320
  }
172
321
 
173
322
  function setApiKey(apiKey, { local } = {}) {
174
323
  const useLocal = local === true || (local == null && hasLocalCredentials());
175
- const targetFile = credentialsFileForLocal(useLocal);
176
- const existing = useLocal ? credentialsForWrite(targetFile) : loadJSON(targetFile);
177
- saveJSON(targetFile, withOnboardingFirewallEnabled({ ...existing, apiKey }));
324
+ const existing = loadRuntimeCredentials();
325
+ saveRuntimeCredentials(withOnboardingFirewallEnabled({ ...existing, apiKey }), { local: useLocal });
178
326
  }
179
327
 
180
328
  function clearApiKey({ local } = {}) {
181
329
  const useLocal = local === true || (local == null && hasLocalCredentials());
182
- const targetFile = credentialsFileForLocal(useLocal);
183
- const existing = loadJSON(targetFile);
330
+ const existing = loadRuntimeCredentials();
184
331
  if (!existing || !existing.apiKey) return;
185
332
  delete existing.apiKey;
186
- saveJSON(targetFile, appConfig.withCredentialDefaults(existing) || existing);
333
+ saveRuntimeCredentials(appConfig.withCredentialDefaults(existing) || existing, { local: useLocal });
187
334
  }
188
335
 
189
336
  function getApiKey() {
190
- const creds = loadCredentials();
337
+ const creds = loadRuntimeCredentials();
191
338
  return creds && creds.apiKey ? creds.apiKey : null;
192
339
  }
193
340
 
194
341
  function setApp(app, { local } = {}) {
195
342
  const useLocal = local === true || (local == null && hasLocalCredentials());
196
- const targetFile = credentialsFileForLocal(useLocal);
197
- const existing = useLocal ? credentialsForWrite(targetFile) : loadJSON(targetFile);
198
- saveJSON(targetFile, appConfig.withCredentialDefaults({
343
+ const existing = loadRuntimeCredentials();
344
+ saveRuntimeCredentials(appConfig.withCredentialDefaults({
199
345
  ...existing,
200
346
  app: {
201
347
  key: app.key || null,
202
348
  name: app.name || null,
203
349
  },
204
- }) || {});
350
+ }) || {}, { local: useLocal });
351
+ }
352
+
353
+ function setRuntime(runtime, { local } = {}) {
354
+ const useLocal = local === true || (local == null && hasLocalCredentials());
355
+ const existing = loadRuntimeCredentials();
356
+ saveRuntimeCredentials(withOnboardingFirewallEnabled(appConfig.mergeCredentials(existing, runtime || {}) || {}), { local: useLocal });
205
357
  }
206
358
 
207
359
  function ensureLocalGitignore() {
208
360
  const gitignorePath = path.join(process.cwd(), '.gitignore');
209
361
  const legacyEntry = '.securenow/';
210
362
  const entries = [
363
+ '.securenow/admin.json',
364
+ '.securenow/runtime.json',
211
365
  '.securenow/credentials.json',
212
366
  '.securenow/credentials.*.json',
367
+ '!.securenow/admin.example.json',
368
+ '!.securenow/runtime.example.json',
213
369
  '!.securenow/credentials.example.json',
214
370
  '!.securenow/credentials.*.example.json',
215
371
  ];
@@ -250,41 +406,52 @@ function ensureLocalGitignore() {
250
406
  }
251
407
 
252
408
  function getApiUrl() {
253
- if (process.env.SECURENOW_API_URL) return process.env.SECURENOW_API_URL;
254
409
  const cfg = loadConfig();
255
410
  if (cfg.apiUrl && cfg.apiUrl !== DEFAULTS.apiUrl) return cfg.apiUrl;
256
- return appConfig.env('SECURENOW_API_URL') || cfg.apiUrl;
411
+ return cfg.apiUrl;
257
412
  }
258
413
 
259
414
  function getAppUrl() {
260
- return process.env.SECURENOW_APP_URL || loadConfig().appUrl;
415
+ return loadConfig().appUrl;
261
416
  }
262
417
 
263
418
  function getDefaultApp() {
264
- return process.env.SECURENOW_APP || loadConfig().defaultApp;
419
+ return loadConfig().defaultApp;
265
420
  }
266
421
 
267
422
  module.exports = {
268
423
  CONFIG_DIR,
269
424
  CONFIG_FILE,
270
425
  CREDENTIALS_FILE,
426
+ ADMIN_CREDENTIALS_FILE,
427
+ RUNTIME_CREDENTIALS_FILE,
271
428
  LOCAL_CONFIG_DIR,
272
429
  LOCAL_CREDENTIALS_FILE,
430
+ LOCAL_ADMIN_CREDENTIALS_FILE,
431
+ LOCAL_RUNTIME_CREDENTIALS_FILE,
273
432
  loadConfig,
274
433
  saveConfig,
275
434
  getConfigValue,
276
435
  setConfigValue,
277
436
  loadCredentials,
278
437
  saveCredentials,
438
+ loadAdminCredentials,
439
+ saveAdminCredentials,
440
+ clearAdminCredentials,
441
+ loadRuntimeCredentials,
442
+ saveRuntimeCredentials,
443
+ clearRuntimeCredentials,
279
444
  clearCredentials,
280
445
  getToken,
281
446
  setAuth,
282
447
  getApp,
283
448
  setApp,
449
+ setRuntime,
284
450
  setApiKey,
285
451
  clearApiKey,
286
452
  getApiKey,
287
453
  getAuthSource,
454
+ getRuntimeSource,
288
455
  hasLocalCredentials,
289
456
  ensureCredentialDefaults,
290
457
  withOnboardingFirewallEnabled,