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 +21 -1
- package/cli/apiKey.js +55 -0
- package/cli/auth.js +13 -2
- package/cli/config.js +24 -0
- package/cli/diagnostics.js +3 -1
- package/cli.js +24 -0
- package/firewall-only.js +3 -2
- package/nextjs.js +4 -2
- package/package.json +1 -1
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
|
|
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, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
@@ -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,
|
package/cli/diagnostics.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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