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 +121 -114
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
-
|
|
1622
|
-
const
|
|
1623
|
-
const
|
|
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
|
|
1638
|
-
const
|
|
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
|
-
|
|
1650
|
-
// Don't follow redirects
|
|
1580
|
+
provider,
|
|
1581
|
+
id_token: idToken
|
|
1582
|
+
})
|
|
1651
1583
|
});
|
|
1652
|
-
if (response.
|
|
1653
|
-
const
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
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
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
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
|
-
|
|
1667
|
-
|
|
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
|
|
1677
|
-
|
|
1678
|
-
const
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
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
|
|
1684
|
-
await (0, import_open.default)(
|
|
1686
|
+
const codePromise = waitForOAuthCallback(port);
|
|
1687
|
+
await (0, import_open.default)(authUrl.toString());
|
|
1685
1688
|
console.log("Waiting for authentication...");
|
|
1686
|
-
const
|
|
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);
|