@sharnix/agent 1.0.7 → 1.0.9
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 +128 -69
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -10,7 +10,7 @@ const { randomBytes, createHash } = require('crypto');
|
|
|
10
10
|
|
|
11
11
|
const RELAY_BASE = (process.env.SHARNIX_URL || 'https://relay.sharnix.com').replace(/\/$/, '');
|
|
12
12
|
const WS_BASE = RELAY_BASE.replace(/^http/, 'ws');
|
|
13
|
-
|
|
13
|
+
let API_KEY = process.env.SHARNIX_API_KEY || process.env.SHARNIX_KEY || '';
|
|
14
14
|
|
|
15
15
|
// ── Config persistence (declared early so dispatch below can rely on it) ──────
|
|
16
16
|
const CONFIG_DIR = path.join(os.homedir(), '.sharnix');
|
|
@@ -34,10 +34,10 @@ if (has('--help') || has('-h')) {
|
|
|
34
34
|
sharnix — tunnel your local app through Sharnix
|
|
35
35
|
|
|
36
36
|
Usage:
|
|
37
|
-
npx @sharnix/agent
|
|
38
|
-
npx @sharnix/agent
|
|
39
|
-
npx @sharnix/agent
|
|
40
|
-
|
|
37
|
+
npx @sharnix/agent --port <port> Tunnel a local port (auto-runs setup if no key)
|
|
38
|
+
npx @sharnix/agent --port <port> --share Tunnel and print a share link on connect
|
|
39
|
+
npx @sharnix/agent setup Re-run setup explicitly (writes MCP config too)
|
|
40
|
+
npx @sharnix/agent setup --print-url Print auth URL and exit (legacy non-blocking mode)
|
|
41
41
|
|
|
42
42
|
Options:
|
|
43
43
|
--port, -p <n> Local port to forward (default: 3000)
|
|
@@ -47,23 +47,17 @@ if (has('--help') || has('-h')) {
|
|
|
47
47
|
--help, -h Show this message
|
|
48
48
|
|
|
49
49
|
Environment:
|
|
50
|
-
SHARNIX_API_KEY API key
|
|
51
|
-
|
|
50
|
+
SHARNIX_API_KEY API key (also: SHARNIX_KEY). If unset and ~/.sharnix/key.json
|
|
51
|
+
doesn't exist, the CLI will start the device flow automatically.
|
|
52
|
+
SHARNIX_URL Override relay base URL (default: https://relay.sharnix.com)
|
|
52
53
|
`);
|
|
53
54
|
process.exit(0);
|
|
54
55
|
}
|
|
55
56
|
|
|
56
|
-
// ──
|
|
57
|
+
// ── Dispatch ──────────────────────────────────────────────────────────────────
|
|
57
58
|
if (args[0] === 'setup') {
|
|
58
59
|
runSetup(has('--print-url')).catch((err) => { console.error(`\n Error: ${err.message}\n`); process.exit(1); });
|
|
59
60
|
} else {
|
|
60
|
-
if (!API_KEY) {
|
|
61
|
-
console.error('\n Error: SHARNIX_API_KEY is not set.\n');
|
|
62
|
-
console.error(' On your local machine: npx @sharnix/agent setup');
|
|
63
|
-
console.error(' On a remote machine: SHARNIX_API_KEY=shx_... npx @sharnix/agent --port 3000');
|
|
64
|
-
console.error('\n Get your key from relay.sharnix.com/app/settings → API Keys\n');
|
|
65
|
-
process.exit(1);
|
|
66
|
-
}
|
|
67
61
|
if (isNaN(port) || port < 1 || port > 65535) {
|
|
68
62
|
console.error(`\n Error: invalid port "${get('--port') || get('-p')}"\n`);
|
|
69
63
|
process.exit(1);
|
|
@@ -181,18 +175,37 @@ function connect() {
|
|
|
181
175
|
}
|
|
182
176
|
|
|
183
177
|
async function createShareLink() {
|
|
178
|
+
let orgs;
|
|
184
179
|
try {
|
|
185
|
-
|
|
186
|
-
if (!orgs.length) return;
|
|
187
|
-
const orgSlug = creds.orgSlug || orgs[0].slug;
|
|
188
|
-
const result = await api('POST', `/api/v1/orgs/${orgSlug}/tunnels/${tunnelId}/links`, {
|
|
189
|
-
permission: 'read-only',
|
|
190
|
-
label: label || 'Shared via agent',
|
|
191
|
-
});
|
|
192
|
-
console.log(` Share link : ${result.url}`);
|
|
193
|
-
console.log(` Permission : read-only\n`);
|
|
180
|
+
orgs = await api('GET', '/api/v1/orgs');
|
|
194
181
|
} catch (err) {
|
|
195
182
|
console.error(` Could not create share link: ${err.message}`);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
if (!orgs.length) return;
|
|
186
|
+
const orgSlug = creds.orgSlug || orgs[0].slug;
|
|
187
|
+
|
|
188
|
+
// The relay inserts the tunnel row from an async handler triggered by our WS REGISTER.
|
|
189
|
+
// The HTTP POST below can arrive before that insert completes, yielding tunnel_not_found.
|
|
190
|
+
// Retry briefly on that specific error; surface any other error immediately.
|
|
191
|
+
const delays = [400, 800, 1500, 2500];
|
|
192
|
+
for (let attempt = 0; ; attempt++) {
|
|
193
|
+
try {
|
|
194
|
+
const result = await api('POST', `/api/v1/orgs/${orgSlug}/tunnels/${tunnelId}/links`, {
|
|
195
|
+
permission: 'read-only',
|
|
196
|
+
label: label || 'Shared via agent',
|
|
197
|
+
});
|
|
198
|
+
console.log(` Share link : ${result.url}`);
|
|
199
|
+
console.log(` Permission : read-only\n`);
|
|
200
|
+
return;
|
|
201
|
+
} catch (err) {
|
|
202
|
+
if (err.message === 'tunnel_not_found' && attempt < delays.length) {
|
|
203
|
+
await new Promise((r) => setTimeout(r, delays[attempt]));
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
console.error(` Could not create share link: ${err.message}`);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
196
209
|
}
|
|
197
210
|
}
|
|
198
211
|
|
|
@@ -266,70 +279,116 @@ process.on('SIGINT', () => {
|
|
|
266
279
|
process.exit(0);
|
|
267
280
|
});
|
|
268
281
|
|
|
269
|
-
// ──
|
|
270
|
-
function
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
282
|
+
// ── Key resolution + headless detection ──────────────────────────────────────
|
|
283
|
+
function resolveApiKey() {
|
|
284
|
+
if (API_KEY) return API_KEY;
|
|
285
|
+
for (const name of ['key.json', 'sharnix-key.json']) {
|
|
286
|
+
const data = loadJson(path.join(CONFIG_DIR, name));
|
|
287
|
+
if (data?.apiKey) return data.apiKey;
|
|
288
|
+
}
|
|
289
|
+
return null;
|
|
275
290
|
}
|
|
276
291
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
292
|
+
function isHeadlessEnv() {
|
|
293
|
+
if (!process.stdout.isTTY) return true;
|
|
294
|
+
if (process.env.SSH_CLIENT || process.env.SSH_TTY || process.env.SSH_CONNECTION) return true;
|
|
295
|
+
if (process.env.CI) return true;
|
|
296
|
+
if (process.platform === 'linux' && !process.env.DISPLAY && !process.env.WAYLAND_DISPLAY) return true;
|
|
297
|
+
return false;
|
|
298
|
+
}
|
|
282
299
|
|
|
283
|
-
|
|
284
|
-
|
|
300
|
+
// Device-flow bootstrap — returns the API key without writing MCP config.
|
|
301
|
+
// Shared by both auto-bootstrap (main path) and the explicit `setup` command.
|
|
302
|
+
async function obtainKeyViaDeviceFlow() {
|
|
285
303
|
const r = await fetch(`${RELAY_BASE}/api/v1/setup-cli`, { method: 'POST' });
|
|
286
|
-
if (!r.ok) throw new Error(
|
|
304
|
+
if (!r.ok) throw new Error(`Could not reach ${RELAY_BASE}. Check your connection.`);
|
|
287
305
|
const { code, authUrl } = await r.json();
|
|
288
|
-
process.stdout.write(' done\n\n');
|
|
289
306
|
|
|
290
|
-
|
|
291
|
-
// Non-blocking mode for agents running on remote machines
|
|
292
|
-
console.log(' Authorization URL (open this in your browser):\n');
|
|
293
|
-
console.log(` ${authUrl}\n`);
|
|
294
|
-
console.log(' After authorizing, run the tunnel with:');
|
|
295
|
-
console.log(` SHARNIX_API_KEY=<your-key> npx @sharnix/agent --port 3000\n`);
|
|
296
|
-
console.log(' Your key will be shown on the authorization page after you click "Authorize".\n');
|
|
297
|
-
process.exit(0);
|
|
298
|
-
}
|
|
307
|
+
const headless = isHeadlessEnv();
|
|
299
308
|
|
|
300
|
-
console.log('
|
|
301
|
-
console.log(
|
|
309
|
+
console.log('\n ──────────────────────────────────────────────────────────');
|
|
310
|
+
console.log(' No Sharnix key found — let\'s set one up. (10-second flow.)');
|
|
311
|
+
console.log(' ──────────────────────────────────────────────────────────\n');
|
|
312
|
+
console.log(' Open this URL in any browser to authorize (your phone works fine):\n');
|
|
313
|
+
console.log(` ${authUrl}\n`);
|
|
302
314
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
315
|
+
if (!headless) {
|
|
316
|
+
try {
|
|
317
|
+
const { spawn } = require('child_process');
|
|
318
|
+
if (process.platform === 'win32') {
|
|
319
|
+
spawn('cmd', ['/c', 'start', '', authUrl], { detached: true, stdio: 'ignore' });
|
|
320
|
+
} else {
|
|
321
|
+
const cmd = process.platform === 'darwin' ? 'open' : 'xdg-open';
|
|
322
|
+
spawn(cmd, [authUrl], { detached: true, stdio: 'ignore' });
|
|
323
|
+
}
|
|
324
|
+
} catch {}
|
|
325
|
+
}
|
|
312
326
|
|
|
313
|
-
|
|
314
|
-
process.stdout.write(' Waiting for browser authorization');
|
|
315
|
-
let apiKey = null;
|
|
327
|
+
process.stdout.write(' Waiting for authorization');
|
|
316
328
|
const deadline = Date.now() + 10 * 60 * 1000;
|
|
317
329
|
while (Date.now() < deadline) {
|
|
318
|
-
await new Promise((r) => setTimeout(r,
|
|
330
|
+
await new Promise((r) => setTimeout(r, 3000));
|
|
319
331
|
process.stdout.write('.');
|
|
320
332
|
const poll = await fetch(`${RELAY_BASE}/api/v1/setup-cli/${code}`);
|
|
321
333
|
if (!poll.ok) { process.stdout.write('\n'); throw new Error('Setup session expired.'); }
|
|
322
334
|
const data = await poll.json();
|
|
323
|
-
if (data.done) {
|
|
335
|
+
if (data.done) { process.stdout.write(' ✓\n'); return data.key; }
|
|
324
336
|
}
|
|
325
337
|
process.stdout.write('\n');
|
|
326
|
-
|
|
338
|
+
throw new Error('Setup timed out after 10 minutes.');
|
|
339
|
+
}
|
|
327
340
|
|
|
328
|
-
|
|
341
|
+
function saveKeyToDisk(apiKey) {
|
|
342
|
+
saveJson(path.join(CONFIG_DIR, 'key.json'), { apiKey, createdAt: new Date().toISOString() });
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// ── Tunnel main ───────────────────────────────────────────────────────────────
|
|
346
|
+
async function main() {
|
|
347
|
+
console.log(`\n Sharnix Agent → localhost:${port}`);
|
|
348
|
+
|
|
349
|
+
// Resolve a key, or trigger device flow inline if none.
|
|
350
|
+
API_KEY = resolveApiKey();
|
|
351
|
+
if (!API_KEY) {
|
|
352
|
+
try {
|
|
353
|
+
API_KEY = await obtainKeyViaDeviceFlow();
|
|
354
|
+
saveKeyToDisk(API_KEY);
|
|
355
|
+
console.log(` ✓ Key saved to ${path.join(CONFIG_DIR, 'key.json')}\n`);
|
|
356
|
+
} catch (err) {
|
|
357
|
+
console.error(`\n Error: ${err.message}\n`);
|
|
358
|
+
process.exit(1);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
try {
|
|
363
|
+
creds = await bootstrap();
|
|
364
|
+
connect();
|
|
365
|
+
} catch (err) {
|
|
366
|
+
console.error(`\n Error: ${err.message}\n`);
|
|
367
|
+
process.exit(1);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
329
370
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
371
|
+
// ── Setup: explicit `setup` subcommand — bootstrap + MCP config write ────────
|
|
372
|
+
async function runSetup(printUrlOnly = false) {
|
|
373
|
+
console.log('\n Sharnix Setup\n');
|
|
374
|
+
|
|
375
|
+
if (printUrlOnly) {
|
|
376
|
+
// Legacy non-blocking mode — start device flow, print URL, exit.
|
|
377
|
+
process.stdout.write(' Starting setup…');
|
|
378
|
+
const r = await fetch(`${RELAY_BASE}/api/v1/setup-cli`, { method: 'POST' });
|
|
379
|
+
if (!r.ok) throw new Error('Could not reach relay.sharnix.com. Check your internet connection.');
|
|
380
|
+
const { authUrl } = await r.json();
|
|
381
|
+
process.stdout.write(' done\n\n');
|
|
382
|
+
console.log(' Authorization URL (open this in your browser):\n');
|
|
383
|
+
console.log(` ${authUrl}\n`);
|
|
384
|
+
console.log(' After authorizing, just run the tunnel — the CLI will read the saved key:');
|
|
385
|
+
console.log(' npx @sharnix/agent --port 3000\n');
|
|
386
|
+
process.exit(0);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const apiKey = await obtainKeyViaDeviceFlow();
|
|
390
|
+
console.log('\n API key received!\n');
|
|
391
|
+
saveKeyToDisk(apiKey);
|
|
333
392
|
|
|
334
393
|
// Detect and write MCP configs
|
|
335
394
|
const mcpBlock = {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sharnix/agent",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.9",
|
|
4
4
|
"description": "Tunnel your local app through Sharnix — share previews with one command",
|
|
5
5
|
"keywords": ["tunnel", "preview", "sharing", "localhost", "sharnix"],
|
|
6
6
|
"homepage": "https://relay.sharnix.com",
|