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 +168 -76
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
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
|
|
1470
|
-
return new Promise((resolve
|
|
1471
|
-
const server = http.createServer(
|
|
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
|
-
|
|
1473
|
+
server.close(() => resolve(true));
|
|
1514
1474
|
});
|
|
1515
|
-
server.on("error",
|
|
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("
|
|
1669
|
+
console.log("Creating login flow...");
|
|
1670
|
+
const flowId = await createNativeLoginFlow();
|
|
1595
1671
|
const port = await findAvailablePort();
|
|
1596
|
-
const
|
|
1597
|
-
const
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
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
|
|
1603
|
-
await (0, import_open.default)(
|
|
1686
|
+
const codePromise = waitForOAuthCallback(port);
|
|
1687
|
+
await (0, import_open.default)(authUrl.toString());
|
|
1604
1688
|
console.log("Waiting for authentication...");
|
|
1605
|
-
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");
|
|
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(
|
|
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
|
}
|