@agent-idle/cli 0.1.3 → 0.1.4

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.
Files changed (2) hide show
  1. package/index.js +64 -16
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -177,6 +177,18 @@ function resolveConvexUrl() {
177
177
  }
178
178
  var DEFAULT_CONVEX_URL = resolveConvexUrl();
179
179
  var AUTH_URL = process.env.AGENT_IDLE_AUTH_URL ?? BUILD_AUTH_URL ?? "http://localhost:1420";
180
+ function cliAuthUrl(userCode2) {
181
+ try {
182
+ const url = new URL(AUTH_URL);
183
+ url.searchParams.set("login", "1");
184
+ if (userCode2) url.searchParams.set("code", userCode2);
185
+ return url.toString();
186
+ } catch {
187
+ const join2 = AUTH_URL.includes("?") ? "&" : "?";
188
+ const code = userCode2 ? `&code=${encodeURIComponent(userCode2)}` : "";
189
+ return `${AUTH_URL}${join2}login=1${code}`;
190
+ }
191
+ }
180
192
  function ensureDir(path, mode) {
181
193
  const dir = dirname(path);
182
194
  mkdirSync(dir, { recursive: true, ...mode !== void 0 ? { mode } : {} });
@@ -244,19 +256,6 @@ async function pingDaemon() {
244
256
  return false;
245
257
  }
246
258
  }
247
- async function daemonToken() {
248
- try {
249
- const controller = new AbortController();
250
- const timer = setTimeout(() => controller.abort(), 800);
251
- const res = await fetch(`${DAEMON_URL}/token`, { signal: controller.signal });
252
- clearTimeout(timer);
253
- if (!res.ok) return null;
254
- const { token } = await res.json();
255
- return token ?? null;
256
- } catch {
257
- return null;
258
- }
259
- }
260
259
 
261
260
  // src/outbox.ts
262
261
  import { appendFileSync, chmodSync as chmodSync2, existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
@@ -1279,7 +1278,46 @@ import { cancel, isCancel, multiselect } from "@clack/prompts";
1279
1278
 
1280
1279
  // src/auth.ts
1281
1280
  import { spawn as spawn2 } from "child_process";
1281
+ import { randomBytes, randomUUID as randomUUID2, webcrypto } from "crypto";
1282
+ import { ConvexHttpClient as ConvexHttpClient2 } from "convex/browser";
1283
+ import { makeFunctionReference as makeFunctionReference2 } from "convex/server";
1282
1284
  var sleep3 = (ms) => new Promise((r) => setTimeout(r, ms));
1285
+ var createDeviceAuth = makeFunctionReference2("deviceAuth:create");
1286
+ var pollDeviceAuth = makeFunctionReference2("deviceAuth:poll");
1287
+ function userCode() {
1288
+ return randomBytes(5).toString("hex").toUpperCase();
1289
+ }
1290
+ function b64ToBytes(value) {
1291
+ return Buffer.from(value, "base64");
1292
+ }
1293
+ async function createDeviceKeyPair() {
1294
+ const keyPair = await webcrypto.subtle.generateKey(
1295
+ {
1296
+ name: "RSA-OAEP",
1297
+ modulusLength: 2048,
1298
+ publicExponent: new Uint8Array([1, 0, 1]),
1299
+ hash: "SHA-256"
1300
+ },
1301
+ true,
1302
+ ["encrypt", "decrypt"]
1303
+ );
1304
+ const publicJwk = await webcrypto.subtle.exportKey("jwk", keyPair.publicKey);
1305
+ return { publicKeyJwk: JSON.stringify(publicJwk), privateKey: keyPair.privateKey };
1306
+ }
1307
+ async function decryptToken(payload, privateKey) {
1308
+ const rawKey = await webcrypto.subtle.decrypt(
1309
+ { name: "RSA-OAEP" },
1310
+ privateKey,
1311
+ b64ToBytes(payload.encryptedKey)
1312
+ );
1313
+ const key = await webcrypto.subtle.importKey("raw", rawKey, { name: "AES-GCM" }, false, ["decrypt"]);
1314
+ const plaintext = await webcrypto.subtle.decrypt(
1315
+ { name: "AES-GCM", iv: b64ToBytes(payload.iv) },
1316
+ key,
1317
+ b64ToBytes(payload.ciphertext)
1318
+ );
1319
+ return new TextDecoder().decode(plaintext);
1320
+ }
1283
1321
  function openBrowser(url) {
1284
1322
  const [bin, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
1285
1323
  try {
@@ -1300,21 +1338,31 @@ async function signIn() {
1300
1338
  spawnDaemon();
1301
1339
  const settleBy = Date.now() + 5e3;
1302
1340
  while (Date.now() < settleBy && !await pingDaemon()) await sleep3(250);
1341
+ const client = new ConvexHttpClient2(resolveConvexUrl());
1342
+ const deviceId = randomUUID2();
1343
+ const code = userCode();
1344
+ const { publicKeyJwk, privateKey } = await createDeviceKeyPair();
1345
+ await client.mutation(createDeviceAuth, { deviceId, userCode: code, publicKeyJwk });
1346
+ const authUrl = cliAuthUrl(code);
1303
1347
  console.log(`
1304
- Opening ${AUTH_URL} to sign in (GitHub or email + password)\u2026`);
1348
+ Opening ${authUrl} to sign in (GitHub or email + password)\u2026`);
1349
+ console.log(`Your device code is ${code}.`);
1305
1350
  console.log(
1306
1351
  "If it doesn't open, visit that URL manually. Waiting for sign-in\u2026 (Ctrl-C to cancel)"
1307
1352
  );
1308
- openBrowser(AUTH_URL);
1353
+ openBrowser(authUrl);
1309
1354
  const deadline = Date.now() + 12e4;
1310
1355
  while (Date.now() < deadline) {
1311
1356
  await sleep3(1500);
1312
- if (await daemonToken() ?? readToken()) {
1357
+ const res = await client.mutation(pollDeviceAuth, { deviceId });
1358
+ if (res.status === "approved") {
1359
+ writeToken(await decryptToken(res.encryptedToken, privateKey));
1313
1360
  console.log(
1314
1361
  "\u2713 Signed in \u2014 shared session established for the app, CLI, and daemon."
1315
1362
  );
1316
1363
  return;
1317
1364
  }
1365
+ if (res.status === "expired") break;
1318
1366
  }
1319
1367
  console.log(
1320
1368
  "Timed out waiting for sign-in. Re-run `agent-idle setup` once you've signed in."
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-idle/cli",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "agent-idle CLI: setup, ui, and the headless sensor daemon.",
5
5
  "license": "AGPL-3.0-only",
6
6
  "type": "module",