securenow 6.0.2 → 6.1.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.
Files changed (87) hide show
  1. package/CONSUMING-APPS-GUIDE.md +455 -0
  2. package/NPM_README.md +2029 -0
  3. package/README.md +297 -40
  4. package/SKILL-API.md +634 -0
  5. package/SKILL-CLI.md +454 -0
  6. package/cidr.js +83 -0
  7. package/cli/apps.js +585 -0
  8. package/cli/auth.js +280 -0
  9. package/cli/client.js +115 -0
  10. package/cli/config.js +173 -0
  11. package/cli/diagnostics.js +387 -0
  12. package/cli/firewall.js +100 -0
  13. package/cli/fp.js +638 -0
  14. package/cli/init.js +201 -0
  15. package/cli/monitor.js +440 -0
  16. package/cli/run.js +148 -0
  17. package/cli/security.js +980 -0
  18. package/cli/ui.js +386 -0
  19. package/cli/utils.js +127 -0
  20. package/cli.js +466 -455
  21. package/console-instrumentation.js +147 -136
  22. package/docs/ALL-FRAMEWORKS-QUICKSTART.md +1377 -455
  23. package/docs/API-KEYS-GUIDE.md +233 -0
  24. package/docs/ARCHITECTURE.md +3 -3
  25. package/docs/AUTO-BODY-CAPTURE.md +1 -1
  26. package/docs/AUTO-SETUP-SUMMARY.md +331 -0
  27. package/docs/AUTO-SETUP.md +4 -4
  28. package/docs/AUTOMATIC-IP-CAPTURE.md +5 -5
  29. package/docs/BODY-CAPTURE-FIX.md +261 -0
  30. package/docs/BODY-CAPTURE-QUICKSTART.md +2 -2
  31. package/docs/CHANGELOG-NEXTJS.md +1 -35
  32. package/docs/COMPLETION-REPORT.md +408 -0
  33. package/docs/CUSTOMER-GUIDE.md +16 -16
  34. package/docs/EASIEST-SETUP.md +5 -5
  35. package/docs/ENVIRONMENT-VARIABLES.md +880 -652
  36. package/docs/EXPRESS-BODY-CAPTURE.md +13 -12
  37. package/docs/EXPRESS-SETUP-GUIDE.md +719 -720
  38. package/docs/FINAL-SOLUTION.md +335 -0
  39. package/docs/FIREWALL-GUIDE.md +426 -0
  40. package/docs/IMPLEMENTATION-SUMMARY.md +410 -0
  41. package/docs/INDEX.md +22 -4
  42. package/docs/LOGGING-GUIDE.md +701 -708
  43. package/docs/LOGGING-QUICKSTART.md +234 -255
  44. package/docs/NEXTJS-BODY-CAPTURE-COMPARISON.md +323 -0
  45. package/docs/NEXTJS-BODY-CAPTURE.md +2 -2
  46. package/docs/NEXTJS-GUIDE.md +14 -14
  47. package/docs/NEXTJS-QUICKSTART.md +1 -1
  48. package/docs/NEXTJS-SETUP-COMPLETE.md +795 -0
  49. package/docs/NEXTJS-WRAPPER-APPROACH.md +1 -1
  50. package/docs/NUXT-GUIDE.md +166 -0
  51. package/docs/QUICKSTART-BODY-CAPTURE.md +2 -2
  52. package/docs/REDACTION-EXAMPLES.md +1 -1
  53. package/docs/REQUEST-BODY-CAPTURE.md +19 -10
  54. package/docs/SOLUTION-SUMMARY.md +312 -0
  55. package/docs/VERCEL-OTEL-MIGRATION.md +3 -3
  56. package/examples/README.md +6 -6
  57. package/examples/instrumentation-with-auto-capture.ts +1 -1
  58. package/examples/nextjs-env-example.txt +2 -2
  59. package/examples/nextjs-instrumentation.js +1 -1
  60. package/examples/nextjs-instrumentation.ts +1 -1
  61. package/examples/nextjs-with-logging-example.md +6 -6
  62. package/examples/nextjs-with-options.ts +1 -1
  63. package/examples/test-nextjs-setup.js +1 -1
  64. package/firewall-cloud.js +212 -0
  65. package/firewall-iptables.js +139 -0
  66. package/firewall-only.js +38 -0
  67. package/firewall-tcp.js +74 -0
  68. package/firewall.js +720 -0
  69. package/free-trial-banner.js +174 -0
  70. package/nextjs-auto-capture.js +199 -207
  71. package/nextjs-middleware.js +186 -181
  72. package/nextjs-webpack-config.js +88 -53
  73. package/nextjs-wrapper.js +158 -158
  74. package/nextjs.d.ts +1 -1
  75. package/nextjs.js +639 -647
  76. package/nuxt-server-plugin.mjs +423 -0
  77. package/nuxt.d.ts +60 -0
  78. package/nuxt.mjs +75 -0
  79. package/package.json +186 -164
  80. package/postinstall.js +6 -6
  81. package/register.d.ts +1 -1
  82. package/register.js +39 -4
  83. package/resolve-ip.js +77 -0
  84. package/tracing.d.ts +2 -1
  85. package/tracing.js +295 -34
  86. package/web-vite.mjs +239 -156
  87. package/LICENSE +0 -15
package/cli/auth.js ADDED
@@ -0,0 +1,280 @@
1
+ 'use strict';
2
+
3
+ const http = require('http');
4
+ const crypto = require('crypto');
5
+ const { execFileSync } = require('child_process');
6
+ const config = require('./config');
7
+ const { api, CLIError } = require('./client');
8
+ const ui = require('./ui');
9
+
10
+ function openBrowser(url) {
11
+ try {
12
+ const platform = process.platform;
13
+ if (platform === 'darwin') execFileSync('open', [url], { stdio: 'ignore' });
14
+ else if (platform === 'win32') execFileSync('rundll32', ['url.dll,FileProtocolHandler', url], { stdio: 'ignore' });
15
+ else execFileSync('xdg-open', [url], { stdio: 'ignore' });
16
+ return true;
17
+ } catch {
18
+ return false;
19
+ }
20
+ }
21
+
22
+ function decodeJwtPayload(token) {
23
+ try {
24
+ const parts = token.split('.');
25
+ if (parts.length !== 3) return null;
26
+ const payload = Buffer.from(parts[1], 'base64url').toString('utf8');
27
+ return JSON.parse(payload);
28
+ } catch {
29
+ try {
30
+ const parts = token.split('.');
31
+ const base64 = parts[1].replace(/-/g, '+').replace(/_/g, '/');
32
+ const padded = base64 + '='.repeat((4 - base64.length % 4) % 4);
33
+ const payload = Buffer.from(padded, 'base64').toString('utf8');
34
+ return JSON.parse(payload);
35
+ } catch {
36
+ return null;
37
+ }
38
+ }
39
+ }
40
+
41
+ async function loginWithBrowser() {
42
+ const appUrl = config.getAppUrl();
43
+ const nonce = crypto.randomBytes(24).toString('base64url');
44
+
45
+ return new Promise((resolve, reject) => {
46
+ let pendingToken = null;
47
+
48
+ const server = http.createServer((req, res) => {
49
+ const url = new URL(req.url, `http://127.0.0.1`);
50
+
51
+ if (url.pathname === '/callback') {
52
+ const token = url.searchParams.get('token');
53
+ const error = url.searchParams.get('error');
54
+ const returnedState = url.searchParams.get('state');
55
+
56
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
57
+
58
+ if (error) {
59
+ res.end('<html><body style="font-family:system-ui;text-align:center;padding:60px"><h2>Authentication Failed</h2><p>You can close this window.</p></body></html>');
60
+ server.close();
61
+ reject(new CLIError(`Authentication failed: ${error}`));
62
+ return;
63
+ }
64
+
65
+ if (returnedState !== nonce) {
66
+ res.end('<html><body style="font-family:system-ui;text-align:center;padding:60px"><h2>Security Error</h2><p>State mismatch — this request may not have originated from your CLI. Please try again.</p></body></html>');
67
+ server.close();
68
+ reject(new CLIError('State mismatch on callback — possible CSRF. Please retry `securenow login`.'));
69
+ return;
70
+ }
71
+
72
+ if (token) {
73
+ pendingToken = token;
74
+ const payload = decodeJwtPayload(token);
75
+ const email = payload?.email || 'unknown account';
76
+ const safeEmail = email.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
77
+ const port = server.address().port;
78
+ const switchUrl = `${appUrl}/cli/auth?callback=http://127.0.0.1:${port}/callback&state=${encodeURIComponent(nonce)}&force_login=1`;
79
+
80
+ res.end([
81
+ '<!DOCTYPE html><html><head><meta charset="utf-8"><title>SecureNow CLI Login</title></head>',
82
+ '<body style="font-family:system-ui,sans-serif;text-align:center;padding:60px;margin:0;background:#fafafa">',
83
+ '<div style="max-width:420px;margin:0 auto;background:#fff;border-radius:12px;padding:40px 32px;box-shadow:0 2px 12px rgba(0,0,0,.08)">',
84
+ '<div style="width:56px;height:56px;margin:0 auto 20px;background:#f0fdf4;border-radius:50%;display:flex;align-items:center;justify-content:center">',
85
+ '<svg width="28" height="28" fill="none" viewBox="0 0 24 24"><path d="M12 2a5 5 0 015 5v1a2 2 0 012 2v8a2 2 0 01-2 2H7a2 2 0 01-2-2v-8a2 2 0 012-2V7a5 5 0 015-5zm0 2a3 3 0 00-3 3v1h6V7a3 3 0 00-3-3z" fill="#22c55e"/></svg>',
86
+ '</div>',
87
+ '<h2 style="margin:0 0 8px;font-size:22px;color:#111">Connect to SecureNow CLI</h2>',
88
+ `<p style="margin:0 0 24px;color:#666;font-size:15px">You are signing in as</p>`,
89
+ `<div style="background:#f8fafc;border:1px solid #e2e8f0;border-radius:8px;padding:14px 18px;margin:0 0 28px">`,
90
+ `<span style="font-size:17px;font-weight:600;color:#0f172a">${safeEmail}</span>`,
91
+ '</div>',
92
+ '<button id="confirm-btn" style="width:100%;padding:13px 24px;font-size:16px;font-weight:600;color:#fff;background:#22c55e;border:none;border-radius:8px;cursor:pointer;transition:background .15s" ',
93
+ 'onmouseover="this.style.background=\'#16a34a\'" onmouseout="this.style.background=\'#22c55e\'">',
94
+ 'Confirm &amp; Continue</button>',
95
+ `<p style="margin:20px 0 0"><a href="${switchUrl}" style="color:#6366f1;font-size:14px;text-decoration:none" `,
96
+ 'onmouseover="this.style.textDecoration=\'underline\'" onmouseout="this.style.textDecoration=\'none\'">Use a different account</a></p>',
97
+ '<div id="done-msg" style="display:none;margin-top:24px">',
98
+ '<p style="color:#22c55e;font-weight:600;font-size:17px">\u2713 Connected! You can close this window.</p>',
99
+ '</div>',
100
+ '</div>',
101
+ '<script>',
102
+ 'document.getElementById("confirm-btn").addEventListener("click", function(){',
103
+ ' this.disabled=true;this.textContent="Connecting\u2026";this.style.background="#86efac";this.style.cursor="default";',
104
+ ` fetch("/confirm?nonce=${encodeURIComponent(nonce)}").then(function(){`,
105
+ ' document.getElementById("confirm-btn").style.display="none";',
106
+ ' document.getElementById("done-msg").style.display="block";',
107
+ ' });',
108
+ '});',
109
+ '</script>',
110
+ '</body></html>',
111
+ ].join(''));
112
+ return;
113
+ }
114
+
115
+ res.end('<html><body style="font-family:system-ui;text-align:center;padding:60px"><h2>Something went wrong</h2><p>No token received. Please try again.</p></body></html>');
116
+ server.close();
117
+ reject(new CLIError('No token received in callback'));
118
+ return;
119
+ }
120
+
121
+ if (url.pathname === '/confirm' && pendingToken) {
122
+ if (url.searchParams.get('nonce') !== nonce) {
123
+ res.writeHead(403, { 'Content-Type': 'application/json' });
124
+ res.end('{"error":"invalid nonce"}');
125
+ return;
126
+ }
127
+ res.writeHead(200, { 'Content-Type': 'application/json' });
128
+ res.end('{"ok":true}');
129
+ const token = pendingToken;
130
+ pendingToken = null;
131
+ server.close();
132
+ resolve(token);
133
+ return;
134
+ }
135
+
136
+ res.writeHead(404);
137
+ res.end();
138
+ });
139
+
140
+ server.listen(0, '127.0.0.1', () => {
141
+ const port = server.address().port;
142
+ const authUrl = `${appUrl}/cli/auth?callback=http://127.0.0.1:${port}/callback&state=${encodeURIComponent(nonce)}`;
143
+
144
+ console.log('');
145
+ ui.info('Opening browser for authentication...');
146
+ console.log('');
147
+
148
+ const opened = openBrowser(authUrl);
149
+ if (!opened) {
150
+ console.log(' Open this URL in your browser to log in:\n');
151
+ console.log(` ${ui.c.underline(ui.c.cyan(authUrl))}\n`);
152
+ } else {
153
+ console.log(` If the browser didn't open, visit:`);
154
+ console.log(` ${ui.c.underline(ui.c.cyan(authUrl))}\n`);
155
+ }
156
+
157
+ console.log(ui.c.dim(' Waiting for authentication...'));
158
+
159
+ const timeout = setTimeout(() => {
160
+ server.close();
161
+ reject(new CLIError('Login timed out after 5 minutes. Try `securenow login --token <TOKEN>` instead.'));
162
+ }, 5 * 60 * 1000);
163
+
164
+ server.on('close', () => clearTimeout(timeout));
165
+ });
166
+
167
+ server.on('error', (err) => {
168
+ reject(new CLIError(`Failed to start local server: ${err.message}`));
169
+ });
170
+ });
171
+ }
172
+
173
+ async function loginWithToken(token) {
174
+ const s = ui.spinner('Validating token');
175
+ try {
176
+ await api.get('/applications', { token });
177
+ s.stop('Token is valid');
178
+ return token;
179
+ } catch (err) {
180
+ s.fail('Token validation failed');
181
+ throw new CLIError(`Invalid token: ${err.message}`);
182
+ }
183
+ }
184
+
185
+ async function login(args, flags) {
186
+ const local = !!flags.local;
187
+
188
+ if (flags.token) {
189
+ const token = flags.token;
190
+ await loginWithToken(token);
191
+ const payload = decodeJwtPayload(token);
192
+ const email = payload?.email || 'unknown';
193
+ const exp = payload?.exp ? payload.exp * 1000 : null;
194
+
195
+ config.setAuth(token, email, exp, { local });
196
+ if (local) config.ensureLocalGitignore();
197
+ console.log('');
198
+ ui.success(`Logged in as ${ui.c.bold(email)}`);
199
+ if (local) ui.info('Credentials saved to project .securenow/ (local)');
200
+ if (exp) {
201
+ const days = Math.ceil((exp - Date.now()) / (1000 * 60 * 60 * 24));
202
+ ui.info(`Session expires in ${days} days`);
203
+ }
204
+ return;
205
+ }
206
+
207
+ try {
208
+ const token = await loginWithBrowser();
209
+ const payload = decodeJwtPayload(token);
210
+ const email = payload?.email || 'unknown';
211
+ const exp = payload?.exp ? payload.exp * 1000 : null;
212
+
213
+ config.setAuth(token, email, exp, { local });
214
+ if (local) config.ensureLocalGitignore();
215
+ console.log('');
216
+ ui.success(`Logged in as ${ui.c.bold(email)}`);
217
+ if (local) ui.info('Credentials saved to project .securenow/ (local)');
218
+ if (exp) {
219
+ const days = Math.ceil((exp - Date.now()) / (1000 * 60 * 60 * 24));
220
+ ui.info(`Session expires in ${days} days`);
221
+ }
222
+ } catch (err) {
223
+ if (err.message.includes('timed out')) {
224
+ console.log('');
225
+ ui.warn('Browser login timed out. You can also login with a token:');
226
+ console.log('');
227
+ console.log(` 1. Go to ${ui.c.cyan(config.getAppUrl() + '/dashboard/settings')}`);
228
+ console.log(` 2. Copy your CLI token`);
229
+ console.log(` 3. Run: ${ui.c.bold('securenow login --token <YOUR_TOKEN>')}`);
230
+ console.log('');
231
+ } else {
232
+ throw err;
233
+ }
234
+ }
235
+ }
236
+
237
+ async function logout(args, flags) {
238
+ const local = flags ? flags.local : undefined;
239
+ const creds = config.loadCredentials();
240
+ config.clearCredentials({ local });
241
+ if (creds.email) {
242
+ ui.success(`Logged out from ${ui.c.bold(creds.email)}`);
243
+ } else {
244
+ ui.success('Logged out');
245
+ }
246
+ if (local) ui.info('Cleared project-local credentials');
247
+ }
248
+
249
+ async function whoami() {
250
+ const creds = config.loadCredentials();
251
+ const token = config.getToken();
252
+
253
+ if (!token) {
254
+ ui.error('Not logged in. Run `securenow login` to authenticate.');
255
+ process.exit(1);
256
+ }
257
+
258
+ const payload = decodeJwtPayload(token);
259
+
260
+ ui.heading('Current Session');
261
+ console.log('');
262
+ const pairs = [
263
+ ['Email', creds.email || payload?.email || 'unknown'],
264
+ ['User ID', payload?.sub || 'unknown'],
265
+ ['API', config.getApiUrl()],
266
+ ['Auth Source', config.getAuthSource()],
267
+ ];
268
+ if (creds.expiresAt) {
269
+ const days = Math.ceil((creds.expiresAt - Date.now()) / (1000 * 60 * 60 * 24));
270
+ pairs.push(['Expires', days > 0 ? `in ${days} days` : ui.c.red('expired')]);
271
+ }
272
+ const defaultApp = config.getDefaultApp();
273
+ if (defaultApp) {
274
+ pairs.push(['Default App', defaultApp]);
275
+ }
276
+ ui.keyValue(pairs);
277
+ console.log('');
278
+ }
279
+
280
+ module.exports = { login, logout, whoami };
package/cli/client.js ADDED
@@ -0,0 +1,115 @@
1
+ 'use strict';
2
+
3
+ const https = require('https');
4
+ const http = require('http');
5
+ const { URL } = require('url');
6
+ const config = require('./config');
7
+ const ui = require('./ui');
8
+
9
+ function buildQueryString(params) {
10
+ if (!params) return '';
11
+ const entries = Object.entries(params).filter(([, v]) => v != null && v !== '');
12
+ if (!entries.length) return '';
13
+ return '?' + entries.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join('&');
14
+ }
15
+
16
+ function request(method, endpoint, { body, query, token, raw } = {}) {
17
+ const baseUrl = config.getApiUrl();
18
+ const qs = buildQueryString(query);
19
+ const urlStr = `${baseUrl}/api${endpoint}${qs}`;
20
+ const url = new URL(urlStr);
21
+ const mod = url.protocol === 'https:' ? https : http;
22
+
23
+ const authToken = token || config.getToken();
24
+
25
+ return new Promise((resolve, reject) => {
26
+ const options = {
27
+ hostname: url.hostname,
28
+ port: url.port || (url.protocol === 'https:' ? 443 : 80),
29
+ path: url.pathname + url.search,
30
+ method,
31
+ headers: { 'Content-Type': 'application/json', 'User-Agent': 'securenow-cli' },
32
+ };
33
+
34
+ if (authToken) {
35
+ options.headers['Authorization'] = `Bearer ${authToken}`;
36
+ }
37
+
38
+ const req = mod.request(options, (res) => {
39
+ let data = '';
40
+ res.on('data', (chunk) => (data += chunk));
41
+ res.on('end', () => {
42
+ if (raw) {
43
+ return resolve({ status: res.statusCode, headers: res.headers, body: data });
44
+ }
45
+
46
+ let parsed;
47
+ try {
48
+ parsed = JSON.parse(data);
49
+ } catch {
50
+ parsed = data;
51
+ }
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
+ }
61
+ if (res.statusCode >= 400) {
62
+ const msg = parsed?.error || parsed?.message || `Request failed (HTTP ${res.statusCode})`;
63
+ const details = parsed?.details || parsed?.unauthorizedKeys;
64
+ const err = new CLIError(details ? `${msg} — ${details}` : msg, res.statusCode);
65
+ reject(err);
66
+ return;
67
+ }
68
+
69
+ resolve(parsed);
70
+ });
71
+ });
72
+
73
+ req.on('error', (err) => {
74
+ if (err.code === 'ECONNREFUSED') {
75
+ reject(new CLIError(`Cannot connect to ${baseUrl}. Is the API server running?`));
76
+ } else if (err.code === 'ENOTFOUND') {
77
+ reject(new CLIError(`Cannot resolve ${url.hostname}. Check your internet connection.`));
78
+ } else {
79
+ reject(new CLIError(`Network error: ${err.message}`));
80
+ }
81
+ });
82
+
83
+ if (body) {
84
+ req.write(JSON.stringify(body));
85
+ }
86
+ req.end();
87
+ });
88
+ }
89
+
90
+ class CLIError extends Error {
91
+ constructor(message, statusCode) {
92
+ super(message);
93
+ this.name = 'CLIError';
94
+ this.statusCode = statusCode;
95
+ }
96
+ }
97
+
98
+ 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
+ }
104
+ return token;
105
+ }
106
+
107
+ const api = {
108
+ get: (endpoint, opts) => request('GET', endpoint, opts),
109
+ post: (endpoint, body, opts) => request('POST', endpoint, { body, ...opts }),
110
+ put: (endpoint, body, opts) => request('PUT', endpoint, { body, ...opts }),
111
+ patch: (endpoint, body, opts) => request('PATCH', endpoint, { body, ...opts }),
112
+ delete: (endpoint, opts) => request('DELETE', endpoint, opts),
113
+ };
114
+
115
+ module.exports = { api, CLIError, requireAuth, buildQueryString };
package/cli/config.js ADDED
@@ -0,0 +1,173 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const os = require('os');
6
+
7
+ const CONFIG_DIR = path.join(os.homedir(), '.securenow');
8
+ const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
9
+ const CREDENTIALS_FILE = path.join(CONFIG_DIR, 'credentials.json');
10
+
11
+ const LOCAL_CONFIG_DIR = path.join(process.cwd(), '.securenow');
12
+ const LOCAL_CONFIG_FILE = path.join(LOCAL_CONFIG_DIR, 'config.json');
13
+ const LOCAL_CREDENTIALS_FILE = path.join(LOCAL_CONFIG_DIR, 'credentials.json');
14
+
15
+ const DEFAULTS = {
16
+ apiUrl: 'https://api.securenow.ai',
17
+ appUrl: 'https://app.securenow.ai',
18
+ defaultApp: null,
19
+ output: 'table',
20
+ };
21
+
22
+ function ensureDir(dir) {
23
+ if (!fs.existsSync(dir || CONFIG_DIR)) {
24
+ fs.mkdirSync(dir || CONFIG_DIR, { recursive: true, mode: 0o700 });
25
+ }
26
+ }
27
+
28
+ function loadJSON(filepath) {
29
+ try {
30
+ return JSON.parse(fs.readFileSync(filepath, 'utf8'));
31
+ } catch {
32
+ return {};
33
+ }
34
+ }
35
+
36
+ function saveJSON(filepath, data) {
37
+ ensureDir(path.dirname(filepath));
38
+ fs.writeFileSync(filepath, JSON.stringify(data, null, 2), { encoding: 'utf8', mode: 0o600 });
39
+ if (process.platform === 'win32') {
40
+ try {
41
+ const { execFileSync } = require('child_process');
42
+ execFileSync('icacls', [filepath, '/inheritance:r', '/grant:r', `${process.env.USERNAME}:F`], { stdio: 'ignore' });
43
+ } catch (_) {}
44
+ }
45
+ }
46
+
47
+ function hasLocalCredentials() {
48
+ return fs.existsSync(LOCAL_CREDENTIALS_FILE);
49
+ }
50
+
51
+ function resolveCredentialsFile() {
52
+ if (fs.existsSync(LOCAL_CREDENTIALS_FILE)) return LOCAL_CREDENTIALS_FILE;
53
+ return CREDENTIALS_FILE;
54
+ }
55
+
56
+ function getAuthSource() {
57
+ if (process.env.SECURENOW_TOKEN) return 'env (SECURENOW_TOKEN)';
58
+ if (fs.existsSync(LOCAL_CREDENTIALS_FILE)) return 'project (.securenow/)';
59
+ return 'global (~/.securenow/)';
60
+ }
61
+
62
+ function loadConfig() {
63
+ const global = loadJSON(CONFIG_FILE);
64
+ const local = fs.existsSync(LOCAL_CONFIG_FILE) ? loadJSON(LOCAL_CONFIG_FILE) : {};
65
+ if (hasLocalCredentials()) {
66
+ const { defaultApp: _ignored, ...globalWithoutAccountScoped } = global;
67
+ return { ...DEFAULTS, ...globalWithoutAccountScoped, ...local };
68
+ }
69
+ return { ...DEFAULTS, ...global, ...local };
70
+ }
71
+
72
+ function saveConfig(config, { local } = {}) {
73
+ const useLocal = local === true || (local == null && hasLocalCredentials());
74
+ const targetFile = useLocal ? LOCAL_CONFIG_FILE : CONFIG_FILE;
75
+ const existing = loadJSON(targetFile);
76
+ saveJSON(targetFile, { ...existing, ...config });
77
+ }
78
+
79
+ function getConfigValue(key) {
80
+ const config = loadConfig();
81
+ return config[key];
82
+ }
83
+
84
+ function setConfigValue(key, value) {
85
+ saveConfig({ [key]: value });
86
+ }
87
+
88
+ function loadCredentials() {
89
+ return loadJSON(resolveCredentialsFile());
90
+ }
91
+
92
+ function saveCredentials(creds, { local = false } = {}) {
93
+ const targetFile = local ? LOCAL_CREDENTIALS_FILE : CREDENTIALS_FILE;
94
+ saveJSON(targetFile, creds);
95
+ }
96
+
97
+ function clearCredentials({ local } = {}) {
98
+ try {
99
+ if (local === true) {
100
+ fs.unlinkSync(LOCAL_CREDENTIALS_FILE);
101
+ } else if (local === false || !hasLocalCredentials()) {
102
+ fs.unlinkSync(CREDENTIALS_FILE);
103
+ } else {
104
+ fs.unlinkSync(LOCAL_CREDENTIALS_FILE);
105
+ }
106
+ } catch {}
107
+ }
108
+
109
+ function getToken() {
110
+ if (process.env.SECURENOW_TOKEN) return process.env.SECURENOW_TOKEN;
111
+
112
+ const creds = loadCredentials();
113
+ if (!creds.token) return null;
114
+
115
+ if (creds.expiresAt && Date.now() > creds.expiresAt) {
116
+ return null;
117
+ }
118
+ return creds.token;
119
+ }
120
+
121
+ function setAuth(token, email, expiresAt, { local = false } = {}) {
122
+ saveCredentials({ token, email, expiresAt }, { local });
123
+ }
124
+
125
+ function ensureLocalGitignore() {
126
+ const gitignorePath = path.join(process.cwd(), '.gitignore');
127
+ const entry = '.securenow/';
128
+ try {
129
+ if (fs.existsSync(gitignorePath)) {
130
+ const content = fs.readFileSync(gitignorePath, 'utf8');
131
+ if (!content.split('\n').some(line => line.trim() === entry)) {
132
+ fs.appendFileSync(gitignorePath, `\n# SecureNow local credentials\n${entry}\n`);
133
+ }
134
+ } else {
135
+ fs.writeFileSync(gitignorePath, `# SecureNow local credentials\n${entry}\n`);
136
+ }
137
+ } catch {}
138
+ }
139
+
140
+ function getApiUrl() {
141
+ return process.env.SECURENOW_API_URL || loadConfig().apiUrl;
142
+ }
143
+
144
+ function getAppUrl() {
145
+ return process.env.SECURENOW_APP_URL || loadConfig().appUrl;
146
+ }
147
+
148
+ function getDefaultApp() {
149
+ return process.env.SECURENOW_APP || loadConfig().defaultApp;
150
+ }
151
+
152
+ module.exports = {
153
+ CONFIG_DIR,
154
+ CONFIG_FILE,
155
+ CREDENTIALS_FILE,
156
+ LOCAL_CONFIG_DIR,
157
+ LOCAL_CREDENTIALS_FILE,
158
+ loadConfig,
159
+ saveConfig,
160
+ getConfigValue,
161
+ setConfigValue,
162
+ loadCredentials,
163
+ saveCredentials,
164
+ clearCredentials,
165
+ getToken,
166
+ setAuth,
167
+ getAuthSource,
168
+ hasLocalCredentials,
169
+ ensureLocalGitignore,
170
+ getApiUrl,
171
+ getAppUrl,
172
+ getDefaultApp,
173
+ };