@clauth/device 1.0.0 → 1.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/device-auth.mjs +91 -58
- package/package.json +2 -2
package/device-auth.mjs
CHANGED
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import { createECDH, createHash, createDecipheriv, createSign, createPrivateKey, randomBytes } from 'node:crypto';
|
|
15
15
|
import { exec, spawn } from 'node:child_process';
|
|
16
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
16
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync } from 'node:fs';
|
|
17
|
+
import { createServer, createConnection } from 'node:net';
|
|
17
18
|
import { homedir } from 'node:os';
|
|
18
19
|
import { join } from 'node:path';
|
|
19
20
|
|
|
@@ -29,6 +30,7 @@ const TIMEOUT = 120_000;
|
|
|
29
30
|
const CURVE = 'prime256v1';
|
|
30
31
|
const CONFIG_DIR = join(homedir(), '.config', 'device-auth');
|
|
31
32
|
const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
|
|
33
|
+
const SOCKET_PATH = join(CONFIG_DIR, 'daemon.sock');
|
|
32
34
|
|
|
33
35
|
// ─── Local config (url, domain) ──────────────────────────────────────────────
|
|
34
36
|
|
|
@@ -203,6 +205,7 @@ function clearSession() {
|
|
|
203
205
|
if (command === 'stop') {
|
|
204
206
|
const pid = keychainGet('pid');
|
|
205
207
|
if (pid) { try { process.kill(parseInt(pid)); } catch {} }
|
|
208
|
+
if (existsSync(SOCKET_PATH)) unlinkSync(SOCKET_PATH);
|
|
206
209
|
clearSession();
|
|
207
210
|
process.stderr.write('Session stopped.\n');
|
|
208
211
|
process.exit(0);
|
|
@@ -226,68 +229,40 @@ if (command === 'status') {
|
|
|
226
229
|
process.exit(1);
|
|
227
230
|
}
|
|
228
231
|
|
|
229
|
-
// ─── Token command —
|
|
232
|
+
// ─── Token command — ask daemon via unix socket ──────────────────────────────
|
|
230
233
|
|
|
231
234
|
if (command === 'token') {
|
|
232
|
-
|
|
233
|
-
const { privateKey, publicKey } = getOrCreateEcdhKeys();
|
|
234
|
-
const { jwt: deviceJwt, deviceId } = signDeviceJwt(privateKey, publicKey);
|
|
235
|
-
|
|
236
|
-
if (!url) {
|
|
235
|
+
if (!existsSync(SOCKET_PATH)) {
|
|
237
236
|
process.stderr.write('Error: No active session. Run `device-auth start --url <server>` first.\n');
|
|
238
237
|
process.exit(1);
|
|
239
238
|
}
|
|
240
239
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
const doc = new Y.Doc();
|
|
244
|
-
const provider = new HocuspocusProvider({
|
|
245
|
-
url: syncUrl, name: 'device', token: deviceJwt,
|
|
246
|
-
document: doc, WebSocketPolyfill: WebSocket,
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
await new Promise((resolve, reject) => {
|
|
250
|
-
const t = setTimeout(() => reject(new Error('Timeout connecting')), 10000);
|
|
251
|
-
provider.on('synced', () => { clearTimeout(t); resolve(); });
|
|
252
|
-
provider.on('authenticationFailed', ({ reason }) => { clearTimeout(t); reject(new Error(reason)); });
|
|
253
|
-
});
|
|
254
|
-
|
|
255
|
-
// Find the LATEST completed token for this resource (most recent by created_at)
|
|
256
|
-
const tokensMap = doc.getMap('tokens');
|
|
257
|
-
const requests = doc.getMap('requests');
|
|
258
|
-
let foundToken = null;
|
|
259
|
-
let latestTime = 0;
|
|
240
|
+
const client = createConnection(SOCKET_PATH);
|
|
241
|
+
client.end(JSON.stringify({ command: 'token', resource }) + '\n');
|
|
260
242
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
243
|
+
let data = '';
|
|
244
|
+
client.on('data', (chunk) => { data += chunk.toString(); });
|
|
245
|
+
client.on('end', () => {
|
|
264
246
|
try {
|
|
265
|
-
const
|
|
266
|
-
if (
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
const req = JSON.parse(reqRaw);
|
|
270
|
-
if (req.resource === resource) {
|
|
271
|
-
const time = new Date(req.created_at || 0).getTime();
|
|
272
|
-
if (time >= latestTime) {
|
|
273
|
-
latestTime = time;
|
|
274
|
-
foundToken = data;
|
|
275
|
-
}
|
|
276
|
-
}
|
|
247
|
+
const res = JSON.parse(data);
|
|
248
|
+
if (res.error) {
|
|
249
|
+
process.stderr.write(`Error: ${res.error}\n`);
|
|
250
|
+
process.exit(1);
|
|
277
251
|
}
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
252
|
+
output(res.token);
|
|
253
|
+
process.exit(0);
|
|
254
|
+
} catch (e) {
|
|
255
|
+
process.stderr.write(`Error: Invalid response from daemon\n`);
|
|
256
|
+
process.exit(1);
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
client.on('error', (err) => {
|
|
260
|
+
process.stderr.write(`Error: Cannot connect to daemon (${err.message}). Run \`device-auth start\` first.\n`);
|
|
285
261
|
process.exit(1);
|
|
286
|
-
}
|
|
262
|
+
});
|
|
287
263
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
process.exit(0);
|
|
264
|
+
// Prevent falling through to auth flow
|
|
265
|
+
await new Promise(() => {});
|
|
291
266
|
}
|
|
292
267
|
|
|
293
268
|
// ─── Start command: fork into background ─────────────────────────────────────
|
|
@@ -408,15 +383,73 @@ async function main() {
|
|
|
408
383
|
// Step 6: Decrypt
|
|
409
384
|
const plainToken = ecdhDecrypt(tokenData.ciphertext, tokenData.serverPublicKey, privateKey);
|
|
410
385
|
|
|
411
|
-
// ─── Daemon mode:
|
|
386
|
+
// ─── Daemon mode: start socket server, keep WS alive ────────────────────────
|
|
412
387
|
if (isDaemon || command === 'start') {
|
|
413
388
|
saveSession(serverUrl);
|
|
414
389
|
saveConfig({ url: serverUrl, domain, tid });
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
390
|
+
|
|
391
|
+
// Start unix socket server for `device-auth token` commands
|
|
392
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
393
|
+
if (existsSync(SOCKET_PATH)) unlinkSync(SOCKET_PATH);
|
|
394
|
+
|
|
395
|
+
const socketServer = createServer((conn) => {
|
|
396
|
+
let data = '';
|
|
397
|
+
conn.on('data', (chunk) => { data += chunk.toString(); });
|
|
398
|
+
conn.on('end', () => {
|
|
399
|
+
try {
|
|
400
|
+
const req = JSON.parse(data);
|
|
401
|
+
if (req.command === 'token') {
|
|
402
|
+
const reqResource = req.resource || resource;
|
|
403
|
+
// Read from live Yjs doc
|
|
404
|
+
const tokensMap = deviceDoc.getMap('tokens');
|
|
405
|
+
const requests = deviceDoc.getMap('requests');
|
|
406
|
+
let found = null;
|
|
407
|
+
let latestTime = 0;
|
|
408
|
+
for (const [rid, raw] of tokensMap.entries()) {
|
|
409
|
+
if (!raw) continue;
|
|
410
|
+
try {
|
|
411
|
+
const td = JSON.parse(raw);
|
|
412
|
+
if (td.status !== 'complete') continue;
|
|
413
|
+
const reqRaw = requests.get(rid);
|
|
414
|
+
if (reqRaw) {
|
|
415
|
+
const r = JSON.parse(reqRaw);
|
|
416
|
+
if (r.resource === reqResource) {
|
|
417
|
+
const time = new Date(r.created_at || 0).getTime();
|
|
418
|
+
if (time >= latestTime) { latestTime = time; found = td; }
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
} catch {}
|
|
422
|
+
}
|
|
423
|
+
if (!found) {
|
|
424
|
+
conn.end(JSON.stringify({ error: `No token for "${reqResource}"` }));
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
const token = ecdhDecrypt(found.ciphertext, found.serverPublicKey, privateKey);
|
|
428
|
+
conn.end(JSON.stringify({ token }));
|
|
429
|
+
} else {
|
|
430
|
+
conn.end(JSON.stringify({ error: 'Unknown command' }));
|
|
431
|
+
}
|
|
432
|
+
} catch (e) {
|
|
433
|
+
conn.end(JSON.stringify({ error: e.message }));
|
|
434
|
+
}
|
|
435
|
+
});
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
socketServer.listen(SOCKET_PATH, () => {
|
|
439
|
+
log('');
|
|
440
|
+
log('✓ Session ready. Use `device-auth token` to get the token.');
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
function cleanup() {
|
|
444
|
+
socketServer.close();
|
|
445
|
+
if (existsSync(SOCKET_PATH)) unlinkSync(SOCKET_PATH);
|
|
446
|
+
provider.destroy();
|
|
447
|
+
clearSession();
|
|
448
|
+
process.exit(0);
|
|
449
|
+
}
|
|
450
|
+
process.on('SIGTERM', cleanup);
|
|
451
|
+
process.on('SIGINT', cleanup);
|
|
452
|
+
return; // keep alive
|
|
420
453
|
}
|
|
421
454
|
|
|
422
455
|
// ─── One-shot: print and exit ──────────────────────────────────────────────
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@clauth/device",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "OAuth Device Authorization CLI with WebSocket token delivery and local encrypted cache",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
],
|
|
14
14
|
"publishConfig": {
|
|
15
15
|
"access": "public",
|
|
16
|
-
"tag": "1.0.
|
|
16
|
+
"tag": "1.0.2"
|
|
17
17
|
},
|
|
18
18
|
"scripts": {
|
|
19
19
|
"start": "node device-auth.mjs"
|