estatehelm 1.0.3 → 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
@@ -1475,80 +1475,6 @@ function isPortAvailable(port) {
1475
1475
  server.on("error", () => resolve(false));
1476
1476
  });
1477
1477
  }
1478
- function waitForCallback(port) {
1479
- return new Promise((resolve, reject) => {
1480
- const server = http.createServer((req, res) => {
1481
- const url = new URL(req.url || "/", `http://127.0.0.1:${port}`);
1482
- const sessionToken = url.searchParams.get("session_token");
1483
- const error = url.searchParams.get("error");
1484
- const errorDescription = url.searchParams.get("error_description");
1485
- if (error) {
1486
- res.writeHead(400, { "Content-Type": "text/html" });
1487
- res.end(`
1488
- <!DOCTYPE html>
1489
- <html>
1490
- <head>
1491
- <title>Authentication Failed</title>
1492
- <style>
1493
- body { font-family: system-ui, sans-serif; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; background: #fef2f2; }
1494
- .card { background: white; padding: 2rem; border-radius: 1rem; box-shadow: 0 10px 25px rgba(0,0,0,0.1); text-align: center; max-width: 400px; }
1495
- h1 { color: #dc2626; margin: 0 0 0.5rem; }
1496
- p { color: #6b7280; margin: 0.5rem 0; }
1497
- .error { font-family: monospace; background: #f3f4f6; padding: 0.5rem; border-radius: 0.25rem; font-size: 0.875rem; }
1498
- </style>
1499
- </head>
1500
- <body>
1501
- <div class="card">
1502
- <h1>Authentication Failed</h1>
1503
- <p>${errorDescription || error || "An error occurred during authentication."}</p>
1504
- <p class="error">${error}</p>
1505
- </div>
1506
- </body>
1507
- </html>
1508
- `);
1509
- server.close();
1510
- reject(new Error(errorDescription || error || "Authentication failed"));
1511
- return;
1512
- }
1513
- if (sessionToken) {
1514
- res.writeHead(200, { "Content-Type": "text/html" });
1515
- res.end(`
1516
- <!DOCTYPE html>
1517
- <html>
1518
- <head>
1519
- <title>CLI Authentication Successful</title>
1520
- <style>
1521
- 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%); }
1522
- .card { background: white; padding: 2rem; border-radius: 1rem; box-shadow: 0 10px 25px rgba(0,0,0,0.1); text-align: center; }
1523
- h1 { color: #059669; margin: 0 0 0.5rem; }
1524
- p { color: #6b7280; margin: 0; }
1525
- </style>
1526
- </head>
1527
- <body>
1528
- <div class="card">
1529
- <h1>\u2713 Authentication Successful</h1>
1530
- <p>You can close this window and return to your terminal.</p>
1531
- </div>
1532
- </body>
1533
- </html>
1534
- `);
1535
- server.close();
1536
- resolve(sessionToken);
1537
- } else {
1538
- res.writeHead(404, { "Content-Type": "text/plain" });
1539
- res.end("Not found");
1540
- }
1541
- });
1542
- server.listen(port, "127.0.0.1", () => {
1543
- console.log(`Callback server listening on http://127.0.0.1:${port}`);
1544
- });
1545
- server.on("error", reject);
1546
- setTimeout(() => {
1547
- server.close();
1548
- reject(new Error("Authentication timed out. Please try again."));
1549
- }, 5 * 60 * 1e3);
1550
- });
1551
- }
1552
1478
  function createApiClient(token) {
1553
1479
  return new ApiClient({
1554
1480
  baseUrl: API_BASE_URL,
@@ -1618,9 +1544,20 @@ async function decryptDeviceCredentials(encryptedPayload) {
1618
1544
  );
1619
1545
  return new Uint8Array(plaintext);
1620
1546
  }
1621
- async function createNativeLoginFlow(returnTo) {
1622
- const url = `${KRATOS_URL}/self-service/login/api?return_to=${encodeURIComponent(returnTo)}`;
1623
- const response = await fetch(url, {
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`, {
1624
1561
  method: "GET",
1625
1562
  headers: { Accept: "application/json" }
1626
1563
  });
@@ -1629,14 +1566,10 @@ async function createNativeLoginFlow(returnTo) {
1629
1566
  throw new Error(`Failed to create login flow: ${response.status} - ${error}`);
1630
1567
  }
1631
1568
  const flow = await response.json();
1632
- return {
1633
- flowId: flow.id,
1634
- flowUrl: flow.request_url || `${KRATOS_URL}/self-service/login?flow=${flow.id}`
1635
- };
1569
+ return flow.id;
1636
1570
  }
1637
- async function initiateOidcFlow(flowId, provider = "google") {
1638
- const url = `${KRATOS_URL}/self-service/login?flow=${flowId}`;
1639
- const response = await fetch(url, {
1571
+ async function submitIdTokenToKratos(flowId, idToken, provider = "google") {
1572
+ const response = await fetch(`${KRATOS_URL}/self-service/login?flow=${flowId}`, {
1640
1573
  method: "POST",
1641
1574
  headers: {
1642
1575
  "Content-Type": "application/json",
@@ -1644,46 +1577,120 @@ async function initiateOidcFlow(flowId, provider = "google") {
1644
1577
  },
1645
1578
  body: JSON.stringify({
1646
1579
  method: "oidc",
1647
- provider
1648
- }),
1649
- redirect: "manual"
1650
- // Don't follow redirects
1580
+ provider,
1581
+ id_token: idToken
1582
+ })
1651
1583
  });
1652
- if (response.status === 422) {
1653
- const data = await response.json();
1654
- const redirectUrl = data.redirect_browser_to;
1655
- if (redirectUrl) {
1656
- return redirectUrl;
1657
- }
1658
- throw new Error("No redirect URL in OIDC response");
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
+ );
1659
1589
  }
1660
- if (response.ok) {
1661
- const data = await response.json();
1662
- if (data.session_token) {
1663
- return data.session_token;
1664
- }
1590
+ const result = await response.json();
1591
+ if (!result.session_token) {
1592
+ throw new Error("No session_token in Kratos response");
1665
1593
  }
1666
- const error = await response.text();
1667
- throw new Error(`Failed to initiate OIDC flow: ${response.status} - ${error}`);
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
+ });
1668
1665
  }
1669
1666
  async function login() {
1670
1667
  console.log("\nEstateHelm Login");
1671
1668
  console.log("================\n");
1672
- console.log("Starting authentication server...");
1673
- const port = await findAvailablePort();
1674
- const callbackUrl = `http://127.0.0.1:${port}/callback`;
1675
1669
  console.log("Creating login flow...");
1676
- const { flowId } = await createNativeLoginFlow(callbackUrl);
1677
- console.log("Initiating OAuth flow...");
1678
- const oauthUrl = await initiateOidcFlow(flowId, "google");
1679
- console.log(`
1680
- Opening browser for Google authentication...`);
1681
- console.log(`If the browser doesn't open, visit: ${oauthUrl}
1670
+ const flowId = await createNativeLoginFlow();
1671
+ const port = await findAvailablePort();
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()}
1682
1685
  `);
1683
- const callbackPromise = waitForCallback(port);
1684
- await (0, import_open.default)(oauthUrl);
1686
+ const codePromise = waitForOAuthCallback(port);
1687
+ await (0, import_open.default)(authUrl.toString());
1685
1688
  console.log("Waiting for authentication...");
1686
- 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");
1687
1694
  console.log("Authentication successful!");
1688
1695
  console.log(`Token: ${sanitizeToken(sessionToken)}`);
1689
1696
  await saveBearerToken(sessionToken);