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/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,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, 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
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.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
+ ['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
- 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,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 !!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
130
  if (process.env.SECURENOW_TOKEN) return 'env (SECURENOW_TOKEN)';
70
- if (appConfig.resolveLocalCredentialsFile()) return 'project (.securenow/)';
71
- return 'global (~/.securenow/)';
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 credentialsFile = resolveCredentialsFile();
102
- const credentials = loadJSON(credentialsFile);
103
- return appConfig.withCredentialDefaults(credentials) || {};
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
- const targetFile = credentialsFileForLocal(local);
108
- saveJSON(targetFile, appConfig.withCredentialDefaults(creds) || {});
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 = credentialsFileForLocal(useLocal);
122
- const existing = useLocal ? credentialsForWrite(targetFile) : loadJSON(targetFile);
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
- 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 {}
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 = loadCredentials();
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, 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
- );
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 = loadCredentials();
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 targetFile = credentialsFileForLocal(useLocal);
176
- const existing = useLocal ? credentialsForWrite(targetFile) : loadJSON(targetFile);
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 targetFile = credentialsFileForLocal(useLocal);
183
- const existing = loadJSON(targetFile);
336
+ const existing = loadRuntimeCredentials();
184
337
  if (!existing || !existing.apiKey) return;
185
338
  delete existing.apiKey;
186
- saveJSON(targetFile, appConfig.withCredentialDefaults(existing) || existing);
339
+ saveRuntimeCredentials(appConfig.withCredentialDefaults(existing) || existing, { local: useLocal });
187
340
  }
188
341
 
189
342
  function getApiKey() {
190
- const creds = loadCredentials();
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 targetFile = credentialsFileForLocal(useLocal);
197
- const existing = useLocal ? credentialsForWrite(targetFile) : loadJSON(targetFile);
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,
@@ -14,7 +14,7 @@ function maskSecret(value) {
14
14
  }
15
15
 
16
16
  function buildRuntimeCredentials(options = {}) {
17
- const creds = config.loadCredentials() || {};
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 login` first so telemetry routes to the selected app.');
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 login` or `npx securenow api-key set snk_live_...` before generating production runtime credentials.');
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) {