estatehelm 1.0.2 → 1.0.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.
package/dist/index.js CHANGED
@@ -1314,7 +1314,8 @@ var KEYTAR_ACCOUNTS = {
1314
1314
  };
1315
1315
  var API_BASE_URL = process.env.ESTATEHELM_API_URL || "https://api.estatehelm.com";
1316
1316
  var APP_URL = process.env.ESTATEHELM_APP_URL || "https://app.estatehelm.com";
1317
- function setServerUrls(apiUrl, appUrl) {
1317
+ var KRATOS_URL = process.env.ESTATEHELM_KRATOS_URL || "https://pauth.estatehelm.com";
1318
+ function setServerUrls(apiUrl, appUrl, kratosUrl) {
1318
1319
  if (apiUrl) {
1319
1320
  API_BASE_URL = apiUrl;
1320
1321
  console.error(`[Config] Using API: ${apiUrl}`);
@@ -1323,6 +1324,10 @@ function setServerUrls(apiUrl, appUrl) {
1323
1324
  APP_URL = appUrl;
1324
1325
  console.error(`[Config] Using App: ${appUrl}`);
1325
1326
  }
1327
+ if (kratosUrl) {
1328
+ KRATOS_URL = kratosUrl;
1329
+ console.error(`[Config] Using Kratos: ${kratosUrl}`);
1330
+ }
1326
1331
  }
1327
1332
  var DEFAULT_CONFIG = {
1328
1333
  defaultMode: "full"
@@ -1449,74 +1454,25 @@ function prompt(question) {
1449
1454
  });
1450
1455
  });
1451
1456
  }
1452
- async function findAvailablePort(startPort = 19847) {
1453
- return new Promise((resolve, reject) => {
1454
- const server = http.createServer();
1455
- server.listen(startPort, "127.0.0.1", () => {
1456
- const address = server.address();
1457
- const port = typeof address === "object" && address ? address.port : startPort;
1458
- server.close(() => resolve(port));
1459
- });
1460
- server.on("error", (err) => {
1461
- if (err.code === "EADDRINUSE") {
1462
- resolve(findAvailablePort(startPort + 1));
1463
- } else {
1464
- reject(err);
1465
- }
1466
- });
1467
- });
1457
+ var ALLOWED_CALLBACK_PORTS = [11033, 11034, 11035];
1458
+ async function findAvailablePort() {
1459
+ for (const port of ALLOWED_CALLBACK_PORTS) {
1460
+ const available = await isPortAvailable(port);
1461
+ if (available) {
1462
+ return port;
1463
+ }
1464
+ }
1465
+ throw new Error(
1466
+ `No available ports for CLI callback. Ports ${ALLOWED_CALLBACK_PORTS.join(", ")} are all in use.`
1467
+ );
1468
1468
  }
1469
- function waitForCallback(port) {
1470
- return new Promise((resolve, reject) => {
1471
- const server = http.createServer((req, res) => {
1472
- const url = new URL(req.url || "/", `http://127.0.0.1:${port}`);
1473
- const sessionToken = url.searchParams.get("session_token");
1474
- if (sessionToken) {
1475
- res.writeHead(200, { "Content-Type": "text/html" });
1476
- res.end(`
1477
- <!DOCTYPE html>
1478
- <html>
1479
- <head>
1480
- <title>CLI Authentication Successful</title>
1481
- <style>
1482
- body { font-family: system-ui, sans-serif; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; background: linear-gradient(115deg, #fff1be 28%, #ee87cb 70%, #b060ff 100%); }
1483
- .card { background: white; padding: 2rem; border-radius: 1rem; box-shadow: 0 10px 25px rgba(0,0,0,0.1); text-align: center; }
1484
- h1 { color: #059669; margin: 0 0 0.5rem; }
1485
- p { color: #6b7280; margin: 0; }
1486
- </style>
1487
- </head>
1488
- <body>
1489
- <div class="card">
1490
- <h1>\u2713 Authentication Successful</h1>
1491
- <p>You can close this window and return to your terminal.</p>
1492
- </div>
1493
- </body>
1494
- </html>
1495
- `);
1496
- server.close();
1497
- resolve(sessionToken);
1498
- } else {
1499
- res.writeHead(400, { "Content-Type": "text/html" });
1500
- res.end(`
1501
- <!DOCTYPE html>
1502
- <html>
1503
- <head><title>Authentication Failed</title></head>
1504
- <body>
1505
- <h1>Authentication Failed</h1>
1506
- <p>No session token received. Please try again.</p>
1507
- </body>
1508
- </html>
1509
- `);
1510
- }
1511
- });
1469
+ function isPortAvailable(port) {
1470
+ return new Promise((resolve) => {
1471
+ const server = http.createServer();
1512
1472
  server.listen(port, "127.0.0.1", () => {
1513
- console.log(`Callback server listening on http://127.0.0.1:${port}`);
1473
+ server.close(() => resolve(true));
1514
1474
  });
1515
- server.on("error", reject);
1516
- setTimeout(() => {
1517
- server.close();
1518
- reject(new Error("Authentication timed out. Please try again."));
1519
- }, 5 * 60 * 1e3);
1475
+ server.on("error", () => resolve(false));
1520
1476
  });
1521
1477
  }
1522
1478
  function createApiClient(token) {
@@ -1588,21 +1544,153 @@ async function decryptDeviceCredentials(encryptedPayload) {
1588
1544
  );
1589
1545
  return new Uint8Array(plaintext);
1590
1546
  }
1547
+ function generatePKCE() {
1548
+ const verifierBytes = crypto.getRandomValues(new Uint8Array(32));
1549
+ const verifier = base64Encode(verifierBytes).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
1550
+ const encoder = new TextEncoder();
1551
+ const data = encoder.encode(verifier);
1552
+ const hashBuffer = crypto.subtle.digestSync ? crypto.subtle.digestSync("SHA-256", data) : null;
1553
+ const cryptoNode = require("crypto");
1554
+ const hash = cryptoNode.createHash("sha256").update(verifier).digest();
1555
+ const challenge = hash.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
1556
+ return { verifier, challenge };
1557
+ }
1558
+ var GOOGLE_CLIENT_ID = "51644152299-ikanidsebtsn6sukgk0sg1hq5h95k2h6.apps.googleusercontent.com";
1559
+ async function createNativeLoginFlow() {
1560
+ const response = await fetch(`${KRATOS_URL}/self-service/login/api`, {
1561
+ method: "GET",
1562
+ headers: { Accept: "application/json" }
1563
+ });
1564
+ if (!response.ok) {
1565
+ const error = await response.text();
1566
+ throw new Error(`Failed to create login flow: ${response.status} - ${error}`);
1567
+ }
1568
+ const flow = await response.json();
1569
+ return flow.id;
1570
+ }
1571
+ async function submitIdTokenToKratos(flowId, idToken, provider = "google") {
1572
+ const response = await fetch(`${KRATOS_URL}/self-service/login?flow=${flowId}`, {
1573
+ method: "POST",
1574
+ headers: {
1575
+ "Content-Type": "application/json",
1576
+ Accept: "application/json"
1577
+ },
1578
+ body: JSON.stringify({
1579
+ method: "oidc",
1580
+ provider,
1581
+ id_token: idToken
1582
+ })
1583
+ });
1584
+ if (!response.ok) {
1585
+ const error = await response.json().catch(() => ({}));
1586
+ throw new Error(
1587
+ error.error?.message || error.ui?.messages?.[0]?.text || `Kratos login failed: ${response.status}`
1588
+ );
1589
+ }
1590
+ const result = await response.json();
1591
+ if (!result.session_token) {
1592
+ throw new Error("No session_token in Kratos response");
1593
+ }
1594
+ return result.session_token;
1595
+ }
1596
+ async function exchangeCodeForTokens(code, codeVerifier, redirectUri) {
1597
+ const response = await fetch("https://oauth2.googleapis.com/token", {
1598
+ method: "POST",
1599
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
1600
+ body: new URLSearchParams({
1601
+ client_id: GOOGLE_CLIENT_ID,
1602
+ code,
1603
+ code_verifier: codeVerifier,
1604
+ grant_type: "authorization_code",
1605
+ redirect_uri: redirectUri
1606
+ })
1607
+ });
1608
+ if (!response.ok) {
1609
+ const error = await response.json().catch(() => ({}));
1610
+ throw new Error(error.error_description || error.error || "Token exchange failed");
1611
+ }
1612
+ const tokens = await response.json();
1613
+ if (!tokens.id_token) {
1614
+ throw new Error("No id_token in Google response");
1615
+ }
1616
+ return {
1617
+ idToken: tokens.id_token,
1618
+ accessToken: tokens.access_token
1619
+ };
1620
+ }
1621
+ function waitForOAuthCallback(port) {
1622
+ return new Promise((resolve, reject) => {
1623
+ const server = http.createServer((req, res) => {
1624
+ const url = new URL(req.url || "/", `http://127.0.0.1:${port}`);
1625
+ const code = url.searchParams.get("code");
1626
+ const error = url.searchParams.get("error");
1627
+ if (error) {
1628
+ res.writeHead(400, { "Content-Type": "text/html" });
1629
+ res.end(`
1630
+ <!DOCTYPE html>
1631
+ <html>
1632
+ <head><title>Authentication Failed</title>
1633
+ <style>body{font-family:system-ui;display:flex;justify-content:center;align-items:center;height:100vh;margin:0;background:#fef2f2}.card{background:white;padding:2rem;border-radius:1rem;box-shadow:0 10px 25px rgba(0,0,0,0.1);text-align:center}h1{color:#dc2626}</style></head>
1634
+ <body><div class="card"><h1>Authentication Failed</h1><p>${url.searchParams.get("error_description") || error}</p></div></body>
1635
+ </html>
1636
+ `);
1637
+ server.close();
1638
+ reject(new Error(url.searchParams.get("error_description") || error));
1639
+ return;
1640
+ }
1641
+ if (code) {
1642
+ res.writeHead(200, { "Content-Type": "text/html" });
1643
+ res.end(`
1644
+ <!DOCTYPE html>
1645
+ <html>
1646
+ <head><title>Authentication Successful</title>
1647
+ <style>body{font-family:system-ui;display:flex;justify-content:center;align-items:center;height:100vh;margin:0;background:linear-gradient(115deg,#fff1be 28%,#ee87cb 70%,#b060ff 100%)}.card{background:white;padding:2rem;border-radius:1rem;box-shadow:0 10px 25px rgba(0,0,0,0.1);text-align:center}h1{color:#059669}</style></head>
1648
+ <body><div class="card"><h1>\u2713 Authentication Successful</h1><p>You can close this window and return to your terminal.</p></div></body>
1649
+ </html>
1650
+ `);
1651
+ server.close();
1652
+ resolve(code);
1653
+ return;
1654
+ }
1655
+ res.writeHead(404);
1656
+ res.end();
1657
+ });
1658
+ server.listen(port, "127.0.0.1");
1659
+ server.on("error", reject);
1660
+ setTimeout(() => {
1661
+ server.close();
1662
+ reject(new Error("Authentication timed out"));
1663
+ }, 5 * 60 * 1e3);
1664
+ });
1665
+ }
1591
1666
  async function login() {
1592
1667
  console.log("\nEstateHelm Login");
1593
1668
  console.log("================\n");
1594
- console.log("Starting authentication server...");
1669
+ console.log("Creating login flow...");
1670
+ const flowId = await createNativeLoginFlow();
1595
1671
  const port = await findAvailablePort();
1596
- const callbackUrl = `http://127.0.0.1:${port}/callback`;
1597
- const loginUrl = `${APP_URL}/cli-auth?callback=${encodeURIComponent(callbackUrl)}`;
1598
- console.log(`
1599
- Opening browser for authentication...`);
1600
- console.log(`If the browser doesn't open, visit: ${loginUrl}
1672
+ const redirectUri = `http://127.0.0.1:${port}/callback`;
1673
+ const { verifier, challenge } = generatePKCE();
1674
+ const authUrl = new URL("https://accounts.google.com/o/oauth2/v2/auth");
1675
+ authUrl.searchParams.set("client_id", GOOGLE_CLIENT_ID);
1676
+ authUrl.searchParams.set("redirect_uri", redirectUri);
1677
+ authUrl.searchParams.set("response_type", "code");
1678
+ authUrl.searchParams.set("scope", "openid email profile");
1679
+ authUrl.searchParams.set("code_challenge", challenge);
1680
+ authUrl.searchParams.set("code_challenge_method", "S256");
1681
+ authUrl.searchParams.set("access_type", "offline");
1682
+ console.log("\nOpening browser for Google authentication...");
1683
+ console.log(`If the browser doesn't open, visit:
1684
+ ${authUrl.toString()}
1601
1685
  `);
1602
- const callbackPromise = waitForCallback(port);
1603
- await (0, import_open.default)(loginUrl);
1686
+ const codePromise = waitForOAuthCallback(port);
1687
+ await (0, import_open.default)(authUrl.toString());
1604
1688
  console.log("Waiting for authentication...");
1605
- const sessionToken = await callbackPromise;
1689
+ const code = await codePromise;
1690
+ console.log("Exchanging authorization code...");
1691
+ const { idToken } = await exchangeCodeForTokens(code, verifier, redirectUri);
1692
+ console.log("Completing authentication...");
1693
+ const sessionToken = await submitIdTokenToKratos(flowId, idToken, "google");
1606
1694
  console.log("Authentication successful!");
1607
1695
  console.log(`Token: ${sanitizeToken(sessionToken)}`);
1608
1696
  await saveBearerToken(sessionToken);
@@ -2878,7 +2966,11 @@ var program = new import_commander.Command();
2878
2966
  program.name("estatehelm").description("EstateHelm CLI - MCP server for AI assistants").version("1.0.0").option("--staging", "Use staging environment (previewapi/previewapp.estatehelm.com)").option("--api-url <url>", "API server URL (default: https://api.estatehelm.com)").option("--app-url <url>", "App server URL (default: https://app.estatehelm.com)").hook("preAction", (thisCommand) => {
2879
2967
  const opts = thisCommand.opts();
2880
2968
  if (opts.staging) {
2881
- setServerUrls("https://previewapi.estatehelm.com", "https://previewapp.estatehelm.com");
2969
+ setServerUrls(
2970
+ "https://previewapi.estatehelm.com",
2971
+ "https://previewapp.estatehelm.com",
2972
+ "https://stauth.estatehelm.com"
2973
+ );
2882
2974
  } else if (opts.apiUrl || opts.appUrl) {
2883
2975
  setServerUrls(opts.apiUrl, opts.appUrl);
2884
2976
  }