securenow 7.0.0-anas.3 → 7.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.
package/app-config.js CHANGED
@@ -14,6 +14,10 @@
14
14
  * - What human label to show? (resolveAppName) — display only, never
15
15
  * sent to the collector.
16
16
  * - Which OTLP collector to hit? (resolveInstance)
17
+ * - Which firewall API key? (resolveApiKey) — the snk_live_... key the
18
+ * firewall sends as Bearer to /api/firewall
19
+ * for blocklist sync. Separate from the
20
+ * app routing key: this one is user-scoped.
17
21
  *
18
22
  * Resolution order (first non-empty wins):
19
23
  *
@@ -25,7 +29,7 @@
25
29
  * 5. Hard default / null
26
30
  *
27
31
  * Credentials file schema:
28
- * { token, email, expiresAt, app: { key: <uuid>, name: <display>, instance } }
32
+ * { token, email, expiresAt, apiKey: <snk_live_...>, app: { key, name, instance } }
29
33
  */
30
34
 
31
35
  const fs = require('fs');
@@ -112,6 +116,21 @@ function resolveAppId() {
112
116
  return resolveAppKey() || resolveAppName();
113
117
  }
114
118
 
119
+ // Firewall / user-scoped API key (snk_live_...).
120
+ // Distinct from resolveAppKey: that one is the application UUID for OTel
121
+ // routing. This one authenticates the firewall blocklist sync to /api/firewall,
122
+ // so it must look like a real `snk_live_` key — the app UUID won't pass auth.
123
+ function resolveApiKey() {
124
+ const fromEnv = pick(process.env.SECURENOW_API_KEY);
125
+ if (fromEnv && fromEnv.startsWith('snk_live_')) return fromEnv;
126
+
127
+ const creds = loadCredentials();
128
+ const fromCreds = creds && pick(creds.apiKey);
129
+ if (fromCreds && fromCreds.startsWith('snk_live_')) return fromCreds;
130
+
131
+ return null;
132
+ }
133
+
115
134
  function resolveInstance() {
116
135
  const fromEnv =
117
136
  pick(process.env.SECURENOW_INSTANCE) ||
@@ -143,6 +162,7 @@ module.exports = {
143
162
  resolveAppKey,
144
163
  resolveAppName,
145
164
  resolveAppId,
165
+ resolveApiKey,
146
166
  resolveInstance,
147
167
  resolveAll,
148
168
  loadCredentials,
package/cli/apiKey.js ADDED
@@ -0,0 +1,55 @@
1
+ 'use strict';
2
+
3
+ const config = require('./config');
4
+ const ui = require('./ui');
5
+
6
+ function maskKey(key) {
7
+ if (!key || key.length < 16) return key || '';
8
+ return `${key.slice(0, 12)}••••••${key.slice(-4)}`;
9
+ }
10
+
11
+ async function set(args, flags) {
12
+ const key = args[0];
13
+ if (!key) {
14
+ ui.error('API key is required. Usage: securenow api-key set <snk_live_...>');
15
+ process.exit(1);
16
+ }
17
+ if (!key.startsWith('snk_live_')) {
18
+ ui.error('API key must start with "snk_live_"');
19
+ ui.info('Create one in the dashboard: Settings → API Keys');
20
+ process.exit(1);
21
+ }
22
+
23
+ const local = flags.global ? false : true;
24
+ config.setApiKey(key, { local });
25
+ if (local) config.ensureLocalGitignore();
26
+
27
+ ui.success(`API key saved (${maskKey(key)})`);
28
+ ui.info(local
29
+ ? 'Stored in project .securenow/credentials.json (local)'
30
+ : 'Stored in ~/.securenow/credentials.json (global)');
31
+ ui.info('The firewall will now pick it up automatically — no SECURENOW_API_KEY env var needed.');
32
+ }
33
+
34
+ async function clear(args, flags) {
35
+ const local = flags.global ? false : true;
36
+ const existing = config.getApiKey();
37
+ config.clearApiKey({ local });
38
+ if (existing) {
39
+ ui.success(`Cleared API key (${maskKey(existing)})`);
40
+ } else {
41
+ ui.info('No API key was stored');
42
+ }
43
+ }
44
+
45
+ async function show() {
46
+ const key = config.getApiKey();
47
+ if (!key) {
48
+ ui.info('No API key stored in credentials. Falling back to SECURENOW_API_KEY env var.');
49
+ return;
50
+ }
51
+ console.log(maskKey(key));
52
+ ui.info(`Source: ${config.getAuthSource()}`);
53
+ }
54
+
55
+ module.exports = { set, clear, show };
package/cli/auth.js CHANGED
@@ -45,6 +45,7 @@ async function loginWithBrowser() {
45
45
  return new Promise((resolve, reject) => {
46
46
  let pendingToken = null;
47
47
  let pendingApp = null;
48
+ let pendingApiKey = null;
48
49
 
49
50
  const server = http.createServer((req, res) => {
50
51
  const url = new URL(req.url, `http://127.0.0.1`);
@@ -56,6 +57,7 @@ async function loginWithBrowser() {
56
57
  const appKey = url.searchParams.get('app_key');
57
58
  const appName = url.searchParams.get('app_name');
58
59
  const appInstance = url.searchParams.get('app_instance');
60
+ const apiKey = url.searchParams.get('api_key');
59
61
 
60
62
  res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
61
63
 
@@ -82,6 +84,9 @@ async function loginWithBrowser() {
82
84
  instance: appInstance || null,
83
85
  };
84
86
  }
87
+ if (apiKey && apiKey.startsWith('snk_live_')) {
88
+ pendingApiKey = apiKey;
89
+ }
85
90
  const payload = decodeJwtPayload(token);
86
91
  const email = payload?.email || 'unknown account';
87
92
  const safeEmail = email.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
@@ -139,10 +144,12 @@ async function loginWithBrowser() {
139
144
  res.end('{"ok":true}');
140
145
  const token = pendingToken;
141
146
  const app = pendingApp;
147
+ const apiKeyFromLogin = pendingApiKey;
142
148
  pendingToken = null;
143
149
  pendingApp = null;
150
+ pendingApiKey = null;
144
151
  server.close();
145
- resolve({ token, app });
152
+ resolve({ token, app, apiKey: apiKeyFromLogin });
146
153
  return;
147
154
  }
148
155
 
@@ -219,12 +226,13 @@ async function login(args, flags) {
219
226
  }
220
227
 
221
228
  try {
222
- const { token, app } = await loginWithBrowser();
229
+ const { token, app, apiKey } = await loginWithBrowser();
223
230
  const payload = decodeJwtPayload(token);
224
231
  const email = payload?.email || 'unknown';
225
232
  const exp = payload?.exp ? payload.exp * 1000 : null;
226
233
 
227
234
  config.setAuth(token, email, exp, { local, app });
235
+ if (apiKey) config.setApiKey(apiKey, { local });
228
236
  if (local) config.ensureLocalGitignore();
229
237
  console.log('');
230
238
  ui.success(`Logged in as ${ui.c.bold(email)}`);
@@ -232,6 +240,9 @@ async function login(args, flags) {
232
240
  if (app && (app.name || app.key)) {
233
241
  ui.info(`Linked to app ${ui.c.bold(app.name || app.key)}${app.key ? ` (${ui.c.dim(app.key)})` : ''}`);
234
242
  }
243
+ if (apiKey) {
244
+ ui.info(`Firewall API key saved — the firewall will activate automatically on next start`);
245
+ }
235
246
  if (exp) {
236
247
  const days = Math.ceil((exp - Date.now()) / (1000 * 60 * 60 * 24));
237
248
  ui.info(`Session expires in ${days} days`);
package/cli/config.js CHANGED
@@ -135,6 +135,27 @@ function getApp() {
135
135
  return creds && creds.app ? creds.app : null;
136
136
  }
137
137
 
138
+ function setApiKey(apiKey, { local } = {}) {
139
+ const useLocal = local === true || (local == null && hasLocalCredentials());
140
+ const targetFile = useLocal ? LOCAL_CREDENTIALS_FILE : CREDENTIALS_FILE;
141
+ const existing = loadJSON(targetFile);
142
+ saveJSON(targetFile, { ...existing, apiKey });
143
+ }
144
+
145
+ function clearApiKey({ local } = {}) {
146
+ const useLocal = local === true || (local == null && hasLocalCredentials());
147
+ const targetFile = useLocal ? LOCAL_CREDENTIALS_FILE : CREDENTIALS_FILE;
148
+ const existing = loadJSON(targetFile);
149
+ if (!existing || !existing.apiKey) return;
150
+ delete existing.apiKey;
151
+ saveJSON(targetFile, existing);
152
+ }
153
+
154
+ function getApiKey() {
155
+ const creds = loadCredentials();
156
+ return creds && creds.apiKey ? creds.apiKey : null;
157
+ }
158
+
138
159
  function setApp(app, { local } = {}) {
139
160
  const useLocal = local === true || (local == null && hasLocalCredentials());
140
161
  const targetFile = useLocal ? LOCAL_CREDENTIALS_FILE : CREDENTIALS_FILE;
@@ -193,6 +214,9 @@ module.exports = {
193
214
  setAuth,
194
215
  getApp,
195
216
  setApp,
217
+ setApiKey,
218
+ clearApiKey,
219
+ getApiKey,
196
220
  getAuthSource,
197
221
  hasLocalCredentials,
198
222
  ensureLocalGitignore,
@@ -25,7 +25,9 @@ function resolvedConfig() {
25
25
  ? `${process.env.OTEL_EXPORTER_OTLP_ENDPOINT.replace(/\/$/, '')}/v1/logs`
26
26
  : `${instance.replace(/\/$/, '')}/v1/logs`);
27
27
  const headers = process.env.OTEL_EXPORTER_OTLP_HEADERS || '';
28
- const apiKey = process.env.SECURENOW_API_KEY || '';
28
+ // Resolve firewall API key the same way the SDK does: env, then
29
+ // .securenow/credentials.json (project-local, then global).
30
+ const apiKey = require('../app-config').resolveApiKey() || '';
29
31
  const apiUrl = config.getApiUrl();
30
32
  const loggingEnabled = !/^(0|false)$/i.test(String(process.env.SECURENOW_LOGGING_ENABLED ?? ''));
31
33
  const captureBody = !/^(0|false)$/i.test(String(process.env.SECURENOW_CAPTURE_BODY ?? ''));
package/cli.js CHANGED
@@ -73,6 +73,30 @@ const COMMANDS = {
73
73
  usage: 'securenow whoami',
74
74
  run: () => require('./cli/auth').whoami(),
75
75
  },
76
+ 'api-key': {
77
+ desc: 'Manage the firewall API key stored in .securenow/credentials.json',
78
+ usage: 'securenow api-key <subcommand> [options]',
79
+ sub: {
80
+ set: {
81
+ desc: 'Save an API key (snk_live_...) to the credentials file',
82
+ usage: 'securenow api-key set <snk_live_...> [--global]',
83
+ flags: { global: 'Save to ~/.securenow/ instead of project-local' },
84
+ run: (a, f) => require('./cli/apiKey').set(a, f),
85
+ },
86
+ clear: {
87
+ desc: 'Remove the stored API key',
88
+ usage: 'securenow api-key clear [--global]',
89
+ flags: { global: 'Clear from ~/.securenow/ instead of project-local' },
90
+ run: (a, f) => require('./cli/apiKey').clear(a, f),
91
+ },
92
+ show: {
93
+ desc: 'Print the masked API key currently in use',
94
+ usage: 'securenow api-key show',
95
+ run: () => require('./cli/apiKey').show(),
96
+ },
97
+ },
98
+ defaultSub: 'show',
99
+ },
76
100
  apps: {
77
101
  desc: 'Manage applications',
78
102
  usage: 'securenow apps <subcommand> [options]',
package/firewall-only.js CHANGED
@@ -8,7 +8,7 @@
8
8
  * NODE_OPTIONS='-r securenow/firewall-only' next start
9
9
  *
10
10
  * Reads .env via dotenv (if installed), then initialises the HTTP-level
11
- * firewall when SECURENOW_API_KEY is present.
11
+ * firewall when an API key is resolvable (env var or .securenow/credentials.json).
12
12
  */
13
13
 
14
14
  try { require('dotenv').config(); } catch (_) {}
@@ -16,7 +16,8 @@ try { require('dotenv').config(); } catch (_) {}
16
16
  const env = (k) =>
17
17
  process.env[k] ?? process.env[k.toUpperCase()] ?? process.env[k.toLowerCase()];
18
18
 
19
- const firewallApiKey = env('SECURENOW_API_KEY');
19
+ const { resolveApiKey } = require('./app-config');
20
+ const firewallApiKey = resolveApiKey();
20
21
 
21
22
  if (firewallApiKey && env('SECURENOW_FIREWALL_ENABLED') !== '0') {
22
23
  require('./firewall').init({
package/nextjs.js CHANGED
@@ -610,8 +610,10 @@ function registerSecureNow(options = {}) {
610
610
  }
611
611
  }
612
612
 
613
- // Firewall — runs independently from OTel so it works even if tracing fails
614
- const firewallApiKey = env('SECURENOW_API_KEY');
613
+ // Firewall — runs independently from OTel so it works even if tracing fails.
614
+ // Key comes from env OR .securenow/credentials.json (set via
615
+ // `npx securenow api-key set snk_live_...`), so you don't need a .env entry.
616
+ const firewallApiKey = require('./app-config').resolveApiKey();
615
617
  if (firewallApiKey && env('SECURENOW_FIREWALL_ENABLED') !== '0') {
616
618
  try {
617
619
  require('./firewall').init({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "securenow",
3
- "version": "7.0.0-anas.3",
3
+ "version": "7.1.0",
4
4
  "description": "OpenTelemetry instrumentation for Node.js, Next.js, and Nuxt - Send traces and logs to any OTLP-compatible backend",
5
5
  "type": "commonjs",
6
6
  "main": "register.js",