securegate-cli-tool 2.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/index.js +351 -0
- package/package.json +35 -0
- package/scripts/postinstall.js +37 -0
- package/src/api.js +205 -0
- package/src/commands/connect.js +102 -0
- package/src/commands/keys.js +232 -0
- package/src/commands/login.js +60 -0
- package/src/commands/providers.js +51 -0
- package/src/commands/status.js +78 -0
- package/src/config.js +82 -0
- package/src/index.js +155 -0
- package/templates/SKILL.md +59 -0
package/index.js
ADDED
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* SecureGate CLI - Setup and manage security keys
|
|
5
|
+
* Run: npx securegate setup
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const https = require('https');
|
|
9
|
+
const http = require('http');
|
|
10
|
+
const crypto = require('crypto');
|
|
11
|
+
const os = require('os');
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
const readline = require('readline');
|
|
15
|
+
|
|
16
|
+
// Configuration
|
|
17
|
+
const SUPABASE_URL = 'https://pbrmsfoowrjqsikgkijb.supabase.co';
|
|
18
|
+
const CONFIG_DIR = path.join(os.homedir(), '.securegate');
|
|
19
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
|
20
|
+
|
|
21
|
+
// ANSI colors
|
|
22
|
+
const colors = {
|
|
23
|
+
reset: '\x1b[0m',
|
|
24
|
+
green: '\x1b[32m',
|
|
25
|
+
yellow: '\x1b[33m',
|
|
26
|
+
cyan: '\x1b[36m',
|
|
27
|
+
red: '\x1b[31m',
|
|
28
|
+
bold: '\x1b[1m',
|
|
29
|
+
dim: '\x1b[2m',
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
function log(msg, color = '') {
|
|
33
|
+
console.log(`${color}${msg}${colors.reset}`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function banner() {
|
|
37
|
+
console.log(`
|
|
38
|
+
${colors.cyan}${colors.bold}🔐 SecureGate Setup${colors.reset}
|
|
39
|
+
${colors.dim}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${colors.reset}
|
|
40
|
+
`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Get device fingerprint (machine-based)
|
|
44
|
+
function getDeviceFingerprint() {
|
|
45
|
+
const cpus = os.cpus();
|
|
46
|
+
const networkInterfaces = os.networkInterfaces();
|
|
47
|
+
|
|
48
|
+
// Get first MAC address
|
|
49
|
+
let mac = '';
|
|
50
|
+
for (const [name, interfaces] of Object.entries(networkInterfaces)) {
|
|
51
|
+
for (const iface of interfaces) {
|
|
52
|
+
if (iface.mac && iface.mac !== '00:00:00:00:00:00') {
|
|
53
|
+
mac = iface.mac;
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (mac) break;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const fingerprint = [
|
|
61
|
+
os.hostname(),
|
|
62
|
+
os.platform(),
|
|
63
|
+
os.arch(),
|
|
64
|
+
cpus[0]?.model || 'unknown',
|
|
65
|
+
mac,
|
|
66
|
+
os.totalmem().toString(),
|
|
67
|
+
].join('|');
|
|
68
|
+
|
|
69
|
+
return crypto.createHash('sha256').update(fingerprint).digest('hex').substring(0, 32);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Get public IP
|
|
73
|
+
async function getPublicIP() {
|
|
74
|
+
return new Promise((resolve) => {
|
|
75
|
+
https.get('https://api.ipify.org?format=json', (res) => {
|
|
76
|
+
let data = '';
|
|
77
|
+
res.on('data', chunk => data += chunk);
|
|
78
|
+
res.on('end', () => {
|
|
79
|
+
try {
|
|
80
|
+
resolve(JSON.parse(data).ip);
|
|
81
|
+
} catch {
|
|
82
|
+
resolve('unknown');
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
}).on('error', () => resolve('unknown'));
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Get geolocation
|
|
90
|
+
async function getGeoLocation(ip) {
|
|
91
|
+
return new Promise((resolve) => {
|
|
92
|
+
http.get(`http://ip-api.com/json/${ip}?fields=status,country,countryCode,city`, (res) => {
|
|
93
|
+
let data = '';
|
|
94
|
+
res.on('data', chunk => data += chunk);
|
|
95
|
+
res.on('end', () => {
|
|
96
|
+
try {
|
|
97
|
+
const result = JSON.parse(data);
|
|
98
|
+
if (result.status === 'success') {
|
|
99
|
+
resolve({ country: result.country, code: result.countryCode, city: result.city });
|
|
100
|
+
} else {
|
|
101
|
+
resolve(null);
|
|
102
|
+
}
|
|
103
|
+
} catch {
|
|
104
|
+
resolve(null);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
}).on('error', () => resolve(null));
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Make HTTP request
|
|
112
|
+
function httpRequest(url, options = {}) {
|
|
113
|
+
return new Promise((resolve, reject) => {
|
|
114
|
+
const urlObj = new URL(url);
|
|
115
|
+
const isHttps = urlObj.protocol === 'https:';
|
|
116
|
+
const lib = isHttps ? https : http;
|
|
117
|
+
|
|
118
|
+
const req = lib.request(url, {
|
|
119
|
+
method: options.method || 'GET',
|
|
120
|
+
headers: options.headers || {},
|
|
121
|
+
}, (res) => {
|
|
122
|
+
let data = '';
|
|
123
|
+
res.on('data', chunk => data += chunk);
|
|
124
|
+
res.on('end', () => {
|
|
125
|
+
try {
|
|
126
|
+
resolve({ status: res.statusCode, data: JSON.parse(data) });
|
|
127
|
+
} catch {
|
|
128
|
+
resolve({ status: res.statusCode, data });
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
req.on('error', reject);
|
|
134
|
+
|
|
135
|
+
if (options.body) {
|
|
136
|
+
req.write(options.body);
|
|
137
|
+
}
|
|
138
|
+
req.end();
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Create readline interface
|
|
143
|
+
function createPrompt() {
|
|
144
|
+
return readline.createInterface({
|
|
145
|
+
input: process.stdin,
|
|
146
|
+
output: process.stdout,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Ask question
|
|
151
|
+
function ask(rl, question) {
|
|
152
|
+
return new Promise((resolve) => {
|
|
153
|
+
rl.question(question, (answer) => {
|
|
154
|
+
resolve(answer.trim());
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Load config
|
|
160
|
+
function loadConfig() {
|
|
161
|
+
try {
|
|
162
|
+
if (fs.existsSync(CONFIG_FILE)) {
|
|
163
|
+
return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
|
|
164
|
+
}
|
|
165
|
+
} catch { }
|
|
166
|
+
return {};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Save config
|
|
170
|
+
function saveConfig(config) {
|
|
171
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
172
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
173
|
+
}
|
|
174
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Main setup flow
|
|
178
|
+
async function setup() {
|
|
179
|
+
banner();
|
|
180
|
+
|
|
181
|
+
const rl = createPrompt();
|
|
182
|
+
|
|
183
|
+
try {
|
|
184
|
+
// Step 1: Get JWT token
|
|
185
|
+
log('1. Authentication', colors.bold);
|
|
186
|
+
log(' Enter your Supabase JWT token (from browser after login):', colors.dim);
|
|
187
|
+
const token = await ask(rl, ' Token: ');
|
|
188
|
+
|
|
189
|
+
if (!token) {
|
|
190
|
+
log(' ❌ Token required', colors.red);
|
|
191
|
+
process.exit(1);
|
|
192
|
+
}
|
|
193
|
+
log(' ✓ Token received', colors.green);
|
|
194
|
+
|
|
195
|
+
// Step 2: Get connection ID
|
|
196
|
+
log('\n2. Connection', colors.bold);
|
|
197
|
+
log(' Enter your connection_id (e.g., open_abc123):', colors.dim);
|
|
198
|
+
const connectionId = await ask(rl, ' Connection ID: ');
|
|
199
|
+
|
|
200
|
+
if (!connectionId) {
|
|
201
|
+
log(' ❌ Connection ID required', colors.red);
|
|
202
|
+
process.exit(1);
|
|
203
|
+
}
|
|
204
|
+
log(` ✓ Connection: ${connectionId}`, colors.green);
|
|
205
|
+
|
|
206
|
+
// Step 3: Detect environment
|
|
207
|
+
log('\n3. Detecting environment...', colors.bold);
|
|
208
|
+
|
|
209
|
+
const ip = await getPublicIP();
|
|
210
|
+
log(` ✓ IP: ${ip}`, colors.green);
|
|
211
|
+
|
|
212
|
+
const geo = await getGeoLocation(ip);
|
|
213
|
+
if (geo) {
|
|
214
|
+
log(` ✓ Location: ${geo.city}, ${geo.country} (${geo.code})`, colors.green);
|
|
215
|
+
} else {
|
|
216
|
+
log(' ⚠ Could not detect location', colors.yellow);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const fingerprint = getDeviceFingerprint();
|
|
220
|
+
log(` ✓ Device: ${os.hostname()} (${os.platform()})`, colors.green);
|
|
221
|
+
log(` ✓ Fingerprint: ${fingerprint.substring(0, 8)}...`, colors.dim);
|
|
222
|
+
|
|
223
|
+
// Step 4: Activate key
|
|
224
|
+
log('\n4. Generating security key...', colors.bold);
|
|
225
|
+
|
|
226
|
+
const response = await httpRequest(`${SUPABASE_URL}/functions/v1/activate-key`, {
|
|
227
|
+
method: 'POST',
|
|
228
|
+
headers: {
|
|
229
|
+
'Authorization': `Bearer ${token}`,
|
|
230
|
+
'Content-Type': 'application/json',
|
|
231
|
+
},
|
|
232
|
+
body: JSON.stringify({
|
|
233
|
+
connection_id: connectionId,
|
|
234
|
+
device_fingerprint: fingerprint,
|
|
235
|
+
label: `${os.hostname()} (${os.platform()})`,
|
|
236
|
+
}),
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
if (response.status !== 201) {
|
|
240
|
+
log(` ❌ Failed: ${response.data.error || 'Unknown error'}`, colors.red);
|
|
241
|
+
process.exit(1);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const { security_key, bound_to, provider, usage } = response.data;
|
|
245
|
+
|
|
246
|
+
log(' ✓ Key activated!', colors.green);
|
|
247
|
+
|
|
248
|
+
// Save to config
|
|
249
|
+
const config = loadConfig();
|
|
250
|
+
config[connectionId] = {
|
|
251
|
+
security_key,
|
|
252
|
+
provider,
|
|
253
|
+
bound_to,
|
|
254
|
+
created_at: new Date().toISOString(),
|
|
255
|
+
};
|
|
256
|
+
saveConfig(config);
|
|
257
|
+
|
|
258
|
+
// Display result
|
|
259
|
+
console.log(`
|
|
260
|
+
${colors.dim}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${colors.reset}
|
|
261
|
+
|
|
262
|
+
${colors.bold}${colors.green}✓ Security Key Generated!${colors.reset}
|
|
263
|
+
|
|
264
|
+
${colors.yellow}⚠ SAVE THIS KEY - IT WILL NOT BE SHOWN AGAIN!${colors.reset}
|
|
265
|
+
|
|
266
|
+
${colors.cyan}Security Key:${colors.reset}
|
|
267
|
+
${colors.bold}${security_key}${colors.reset}
|
|
268
|
+
|
|
269
|
+
${colors.cyan}Bound to:${colors.reset}
|
|
270
|
+
• IP: ${bound_to.ip}
|
|
271
|
+
• Country: ${bound_to.country}
|
|
272
|
+
• Device: ${bound_to.device}
|
|
273
|
+
|
|
274
|
+
${colors.cyan}Usage:${colors.reset}
|
|
275
|
+
${colors.dim}const openai = new OpenAI({
|
|
276
|
+
apiKey: '${security_key}',
|
|
277
|
+
baseURL: '${usage.baseURL}'
|
|
278
|
+
});${colors.reset}
|
|
279
|
+
|
|
280
|
+
${colors.dim}Config saved to: ${CONFIG_FILE}${colors.reset}
|
|
281
|
+
|
|
282
|
+
${colors.dim}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${colors.reset}
|
|
283
|
+
`);
|
|
284
|
+
|
|
285
|
+
} finally {
|
|
286
|
+
rl.close();
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// List configured keys
|
|
291
|
+
async function list() {
|
|
292
|
+
banner();
|
|
293
|
+
|
|
294
|
+
const config = loadConfig();
|
|
295
|
+
const keys = Object.entries(config);
|
|
296
|
+
|
|
297
|
+
if (keys.length === 0) {
|
|
298
|
+
log('No security keys configured yet.', colors.dim);
|
|
299
|
+
log('Run: npx securegate setup', colors.cyan);
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
log('Configured Security Keys:', colors.bold);
|
|
304
|
+
console.log();
|
|
305
|
+
|
|
306
|
+
for (const [connectionId, data] of keys) {
|
|
307
|
+
const keyPreview = data.security_key.substring(0, 12) + '...';
|
|
308
|
+
log(` ${connectionId}`, colors.cyan);
|
|
309
|
+
log(` Key: ${keyPreview}`, colors.dim);
|
|
310
|
+
log(` Provider: ${data.provider}`, colors.dim);
|
|
311
|
+
log(` IP: ${data.bound_to?.ip || 'unknown'}`, colors.dim);
|
|
312
|
+
log(` Created: ${data.created_at}`, colors.dim);
|
|
313
|
+
console.log();
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Show usage instructions
|
|
318
|
+
function usage() {
|
|
319
|
+
console.log(`
|
|
320
|
+
${colors.cyan}${colors.bold}SecureGate CLI${colors.reset}
|
|
321
|
+
|
|
322
|
+
${colors.bold}Usage:${colors.reset}
|
|
323
|
+
npx securegate setup Generate a new security key
|
|
324
|
+
npx securegate list List configured keys
|
|
325
|
+
npx securegate help Show this help
|
|
326
|
+
|
|
327
|
+
${colors.bold}Example:${colors.reset}
|
|
328
|
+
1. First, create a connection in the SecureGate dashboard
|
|
329
|
+
2. Run: npx securegate setup
|
|
330
|
+
3. Enter your JWT token and connection ID
|
|
331
|
+
4. Use the generated key with OpenAI SDK
|
|
332
|
+
`);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Main
|
|
336
|
+
const command = process.argv[2] || 'help';
|
|
337
|
+
|
|
338
|
+
switch (command) {
|
|
339
|
+
case 'setup':
|
|
340
|
+
setup().catch(err => {
|
|
341
|
+
log(`Error: ${err.message}`, colors.red);
|
|
342
|
+
process.exit(1);
|
|
343
|
+
});
|
|
344
|
+
break;
|
|
345
|
+
case 'list':
|
|
346
|
+
list();
|
|
347
|
+
break;
|
|
348
|
+
case 'help':
|
|
349
|
+
default:
|
|
350
|
+
usage();
|
|
351
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "securegate-cli-tool",
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "SecureGate CLI — Secure your AI agent API keys from the terminal",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"securegate": "src/index.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"start": "node src/index.js",
|
|
11
|
+
"test": "node src/index.js --help",
|
|
12
|
+
"postinstall": "node scripts/postinstall.js"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"api-security",
|
|
16
|
+
"openai",
|
|
17
|
+
"anthropic",
|
|
18
|
+
"ai-agent",
|
|
19
|
+
"security-key",
|
|
20
|
+
"device-lock",
|
|
21
|
+
"mcp",
|
|
22
|
+
"cli"
|
|
23
|
+
],
|
|
24
|
+
"author": "SecureGate",
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=16"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"commander": "^12.1.0",
|
|
31
|
+
"inquirer": "^8.2.6",
|
|
32
|
+
"chalk": "^4.1.2",
|
|
33
|
+
"ora": "^5.4.1"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
|
|
7
|
+
try {
|
|
8
|
+
const homeDir = os.homedir();
|
|
9
|
+
const openClawPath = path.join(homeDir, '.openclaw');
|
|
10
|
+
const skillsDir = path.join(openClawPath, 'skills');
|
|
11
|
+
const targetDir = path.join(skillsDir, 'securegate');
|
|
12
|
+
const templatePath = path.join(__dirname, '../templates/SKILL.md');
|
|
13
|
+
|
|
14
|
+
// Only proceed if .openclaw config directory exists (implies usage)
|
|
15
|
+
// Or if the user wants to force it? No, auto-detect is safer.
|
|
16
|
+
if (fs.existsSync(openClawPath)) {
|
|
17
|
+
console.log('OpenClaw detected. Installing SecureGate skill...');
|
|
18
|
+
|
|
19
|
+
// Ensure directories exist
|
|
20
|
+
if (!fs.existsSync(skillsDir)) {
|
|
21
|
+
fs.mkdirSync(skillsDir, { recursive: true });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (!fs.existsSync(targetDir)) {
|
|
25
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Copy skill file
|
|
29
|
+
fs.copyFileSync(templatePath, path.join(targetDir, 'SKILL.md'));
|
|
30
|
+
|
|
31
|
+
console.log('✅ SecureGate skill installed to ~/.openclaw/skills/securegate/SKILL.md');
|
|
32
|
+
}
|
|
33
|
+
} catch (error) {
|
|
34
|
+
// Silently fail or log warning - we don't want to break the install just for this
|
|
35
|
+
console.warn('Note: Could not auto-install OpenClaw skill (optional step).');
|
|
36
|
+
// console.warn(error.message);
|
|
37
|
+
}
|
package/src/api.js
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SecureGate CLI — API Client
|
|
3
|
+
* Wraps all Supabase Edge Function calls
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const https = require('https');
|
|
7
|
+
const http = require('http');
|
|
8
|
+
const { SUPABASE_URL, SUPABASE_ANON_KEY, getAuth, setAuth } = require('./config');
|
|
9
|
+
|
|
10
|
+
// ── HTTP Helper ──────────────────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
function httpRequest(url, options = {}) {
|
|
13
|
+
return new Promise((resolve, reject) => {
|
|
14
|
+
const urlObj = new URL(url);
|
|
15
|
+
const lib = urlObj.protocol === 'https:' ? https : http;
|
|
16
|
+
|
|
17
|
+
const req = lib.request(url, {
|
|
18
|
+
method: options.method || 'GET',
|
|
19
|
+
headers: options.headers || {},
|
|
20
|
+
}, (res) => {
|
|
21
|
+
let data = '';
|
|
22
|
+
res.on('data', chunk => data += chunk);
|
|
23
|
+
res.on('end', () => {
|
|
24
|
+
try {
|
|
25
|
+
resolve({ status: res.statusCode, data: JSON.parse(data) });
|
|
26
|
+
} catch {
|
|
27
|
+
resolve({ status: res.statusCode, data });
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
req.on('error', reject);
|
|
33
|
+
if (options.body) req.write(options.body);
|
|
34
|
+
req.end();
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ── Auth Helpers ─────────────────────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
async function login(email, password) {
|
|
41
|
+
const res = await httpRequest(`${SUPABASE_URL}/auth/v1/token?grant_type=password`, {
|
|
42
|
+
method: 'POST',
|
|
43
|
+
headers: {
|
|
44
|
+
'Content-Type': 'application/json',
|
|
45
|
+
'apikey': SUPABASE_ANON_KEY,
|
|
46
|
+
},
|
|
47
|
+
body: JSON.stringify({ email, password }),
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
if (res.status !== 200) {
|
|
51
|
+
throw new Error(res.data?.error_description || res.data?.msg || 'Login failed');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
setAuth({
|
|
55
|
+
access_token: res.data.access_token,
|
|
56
|
+
refresh_token: res.data.refresh_token,
|
|
57
|
+
user: {
|
|
58
|
+
id: res.data.user?.id,
|
|
59
|
+
email: res.data.user?.email,
|
|
60
|
+
},
|
|
61
|
+
expires_at: Date.now() + (res.data.expires_in * 1000),
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
return res.data;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function refreshToken() {
|
|
68
|
+
const auth = getAuth();
|
|
69
|
+
if (!auth?.refresh_token) throw new Error('Not logged in. Run: securegate login');
|
|
70
|
+
|
|
71
|
+
const res = await httpRequest(`${SUPABASE_URL}/auth/v1/token?grant_type=refresh_token`, {
|
|
72
|
+
method: 'POST',
|
|
73
|
+
headers: {
|
|
74
|
+
'Content-Type': 'application/json',
|
|
75
|
+
'apikey': SUPABASE_ANON_KEY,
|
|
76
|
+
},
|
|
77
|
+
body: JSON.stringify({ refresh_token: auth.refresh_token }),
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
if (res.status !== 200) {
|
|
81
|
+
throw new Error('Session expired. Please run: securegate login');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
setAuth({
|
|
85
|
+
access_token: res.data.access_token,
|
|
86
|
+
refresh_token: res.data.refresh_token,
|
|
87
|
+
user: auth.user,
|
|
88
|
+
expires_at: Date.now() + (res.data.expires_in * 1000),
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
return res.data.access_token;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function getValidToken() {
|
|
95
|
+
const auth = getAuth();
|
|
96
|
+
if (!auth) throw new Error('Not logged in. Run: securegate login');
|
|
97
|
+
|
|
98
|
+
// Refresh if expires within 5 minutes
|
|
99
|
+
if (auth.expires_at && (Date.now() > auth.expires_at - 300000)) {
|
|
100
|
+
return await refreshToken();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return auth.access_token;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ── Edge Function Wrappers ───────────────────────────────────────────────────
|
|
107
|
+
|
|
108
|
+
async function authedRequest(functionName, body = null, method = 'POST') {
|
|
109
|
+
const token = await getValidToken();
|
|
110
|
+
|
|
111
|
+
const options = {
|
|
112
|
+
method,
|
|
113
|
+
headers: {
|
|
114
|
+
'Authorization': `Bearer ${token}`,
|
|
115
|
+
'Content-Type': 'application/json',
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
if (body) options.body = JSON.stringify(body);
|
|
120
|
+
|
|
121
|
+
const res = await httpRequest(`${SUPABASE_URL}/functions/v1/${functionName}`, options);
|
|
122
|
+
|
|
123
|
+
if (res.status === 403) {
|
|
124
|
+
throw new Error('Unauthorized. Your session may have expired. Run: securegate login');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return res;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function listConnections() {
|
|
131
|
+
return authedRequest('list-connections', null, 'GET');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function getConnection(connectionId) {
|
|
135
|
+
return authedRequest(`get-connection?connection_id=${connectionId}`, null, 'GET');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function createConnection({ provider, apiKey, customName, baseUrl }) {
|
|
139
|
+
return authedRequest('create-connection', {
|
|
140
|
+
provider,
|
|
141
|
+
api_key: apiKey,
|
|
142
|
+
custom_name: customName,
|
|
143
|
+
base_url: baseUrl,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async function generateKey(connectionId, label) {
|
|
148
|
+
const os = require('os');
|
|
149
|
+
const crypto = require('crypto');
|
|
150
|
+
|
|
151
|
+
// Generate device fingerprint
|
|
152
|
+
const networkInterfaces = os.networkInterfaces();
|
|
153
|
+
let mac = '';
|
|
154
|
+
for (const [, interfaces] of Object.entries(networkInterfaces)) {
|
|
155
|
+
for (const iface of interfaces) {
|
|
156
|
+
if (iface.mac && iface.mac !== '00:00:00:00:00:00') {
|
|
157
|
+
mac = iface.mac;
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (mac) break;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const fingerprint = [
|
|
165
|
+
os.hostname(), os.platform(), os.arch(),
|
|
166
|
+
os.cpus()[0]?.model || 'unknown', mac,
|
|
167
|
+
os.totalmem().toString(),
|
|
168
|
+
].join('|');
|
|
169
|
+
|
|
170
|
+
const deviceFingerprint = crypto.createHash('sha256').update(fingerprint).digest('hex').substring(0, 32);
|
|
171
|
+
|
|
172
|
+
return authedRequest('generate-key', {
|
|
173
|
+
connection_id: connectionId,
|
|
174
|
+
device_fingerprint: deviceFingerprint,
|
|
175
|
+
label: label || `${os.hostname()} (${os.platform()})`,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async function deleteKey(keyId) {
|
|
180
|
+
return authedRequest('delete-key', { key_id: keyId });
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async function lockKey(keyId, ip) {
|
|
184
|
+
return authedRequest('update-key', {
|
|
185
|
+
key_id: keyId,
|
|
186
|
+
update_data: { bound_ip: ip }
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async function getProfile() {
|
|
191
|
+
return authedRequest('get-profile', null, 'GET');
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
module.exports = {
|
|
195
|
+
login,
|
|
196
|
+
refreshToken,
|
|
197
|
+
getValidToken,
|
|
198
|
+
listConnections,
|
|
199
|
+
getConnection,
|
|
200
|
+
createConnection,
|
|
201
|
+
generateKey,
|
|
202
|
+
deleteKey,
|
|
203
|
+
lockKey,
|
|
204
|
+
getProfile,
|
|
205
|
+
};
|