@stackable-labs/cli-app-extension 1.91.2 → 1.92.0
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 +154 -67
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -9,7 +9,7 @@ import { useState, useCallback, useEffect, useRef, useMemo } from 'react';
|
|
|
9
9
|
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
10
10
|
import TextInput from 'ink-text-input';
|
|
11
11
|
import { SURFACE_TARGET } from '@stackable-labs/sdk-extension-contracts';
|
|
12
|
-
import { clearAuthState, readAuthState, getToken, STANDALONE_CLIENT, writeAuthState } from '@stackable-labs/utils-auth';
|
|
12
|
+
import { clearAuthState, readAuthState, getToken, STANDALONE_CLIENT, deriveClientId, writeAuthState } from '@stackable-labs/utils-auth';
|
|
13
13
|
import { execFile, spawn } from 'child_process';
|
|
14
14
|
import { promisify } from 'util';
|
|
15
15
|
import { installDependencies } from 'nypm';
|
|
@@ -22,6 +22,45 @@ import { createServer } from 'http';
|
|
|
22
22
|
import open from 'open';
|
|
23
23
|
import https from 'https';
|
|
24
24
|
|
|
25
|
+
// ../../lib/utils-js/src/crypto.ts
|
|
26
|
+
var getCrypto = () => {
|
|
27
|
+
if (typeof globalThis !== "undefined" && globalThis.crypto) {
|
|
28
|
+
return globalThis.crypto;
|
|
29
|
+
}
|
|
30
|
+
throw new Error("Web Crypto API not available \u2014 requires Node.js 22+ or a modern browser");
|
|
31
|
+
};
|
|
32
|
+
var getSubtle = () => {
|
|
33
|
+
const crypto = getCrypto();
|
|
34
|
+
if (crypto.subtle) {
|
|
35
|
+
return crypto.subtle;
|
|
36
|
+
}
|
|
37
|
+
throw new Error("SubtleCrypto not available \u2014 requires a secure context (HTTPS) or Node.js 22+");
|
|
38
|
+
};
|
|
39
|
+
var encodeBytes = (bytes, encoding) => {
|
|
40
|
+
if (encoding === "hex") {
|
|
41
|
+
return [...bytes].map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
42
|
+
}
|
|
43
|
+
const base64 = btoa(String.fromCharCode(...bytes));
|
|
44
|
+
if (encoding === "base64url") {
|
|
45
|
+
return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
46
|
+
}
|
|
47
|
+
return base64;
|
|
48
|
+
};
|
|
49
|
+
var getRandomBytes = (length, encoding = "base64") => {
|
|
50
|
+
const bytes = new Uint8Array(length);
|
|
51
|
+
getCrypto().getRandomValues(bytes);
|
|
52
|
+
return encodeBytes(bytes, encoding);
|
|
53
|
+
};
|
|
54
|
+
var getDigest = async (input, encoding = "hex") => {
|
|
55
|
+
const digest = await getSubtle().digest(
|
|
56
|
+
"SHA-256",
|
|
57
|
+
new TextEncoder().encode(input)
|
|
58
|
+
);
|
|
59
|
+
return encodeBytes(new Uint8Array(digest), encoding);
|
|
60
|
+
};
|
|
61
|
+
var getNonce = () => getRandomBytes(16, "base64url");
|
|
62
|
+
var getVerifier = () => getRandomBytes(32, "base64url");
|
|
63
|
+
|
|
25
64
|
// ../../lib/utils-js/src/format.ts
|
|
26
65
|
var toKebabCase = (value) => value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
27
66
|
|
|
@@ -1526,13 +1565,15 @@ export const appStore = createStore<AppState>({
|
|
|
1526
1565
|
{
|
|
1527
1566
|
path: "surfaces/Content.tsx",
|
|
1528
1567
|
title: "Content Surface with Loading State",
|
|
1529
|
-
code: `import { ui, useStore, useContextData,
|
|
1568
|
+
code: `import { ui, useStore, useContextData, Surface } from '@stackable-labs/sdk-extension-react'
|
|
1530
1569
|
import { appStore } from '../store'
|
|
1531
1570
|
|
|
1532
1571
|
export function Content() {
|
|
1533
1572
|
const viewState = useStore(appStore, (s) => s.viewState)
|
|
1534
1573
|
const { loading } = useContextData()
|
|
1535
|
-
|
|
1574
|
+
// Non-secret settings from settingsSchema (add settingsSchema to manifest.json to use). Ex:
|
|
1575
|
+
// const settings = useSettings()
|
|
1576
|
+
// const apiEndpoint = settings.apiEndpoint as string
|
|
1536
1577
|
|
|
1537
1578
|
if (loading) {
|
|
1538
1579
|
return (
|
|
@@ -3381,7 +3422,102 @@ var callbackPage = (heading, sub, redirectUrl) => `<!DOCTYPE html>
|
|
|
3381
3422
|
</head><body><div class="card"><h2>${heading}</h2><p>${sub}</p><p class="hint" id="h"></p></div>
|
|
3382
3423
|
${redirectUrl ? `<script>(function(){var s=3,el=document.getElementById('h');function t(){if(s<=0){location.href='${redirectUrl}';return}el.textContent='Redirecting in '+s+'s\u2026';s--;setTimeout(t,1000)}t()})()</script>` : ""}
|
|
3383
3424
|
</body></html>`;
|
|
3384
|
-
var
|
|
3425
|
+
var performCLIOAuthFlow = async ({ dashboardUrl, adminApiBaseUrl }) => {
|
|
3426
|
+
const clientId = await deriveClientId(STANDALONE_CLIENT.CLI);
|
|
3427
|
+
const codeVerifier = getVerifier();
|
|
3428
|
+
const codeChallenge = await getDigest(codeVerifier, "base64url");
|
|
3429
|
+
const state = getNonce();
|
|
3430
|
+
let resolveCode;
|
|
3431
|
+
let rejectCode;
|
|
3432
|
+
const codePromise = new Promise((resolve, reject) => {
|
|
3433
|
+
resolveCode = resolve;
|
|
3434
|
+
rejectCode = reject;
|
|
3435
|
+
});
|
|
3436
|
+
let server;
|
|
3437
|
+
server = createServer((req, res) => {
|
|
3438
|
+
const url = new URL(req.url, "http://localhost");
|
|
3439
|
+
if (url.pathname === "/callback") {
|
|
3440
|
+
const error = url.searchParams.get("error");
|
|
3441
|
+
if (error) {
|
|
3442
|
+
res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
|
3443
|
+
res.end(callbackPage("Authentication failed", "You can close this tab."));
|
|
3444
|
+
rejectCode(new Error(url.searchParams.get("error_description") ?? error));
|
|
3445
|
+
return;
|
|
3446
|
+
}
|
|
3447
|
+
const code2 = url.searchParams.get("code");
|
|
3448
|
+
const returnedState = url.searchParams.get("state");
|
|
3449
|
+
if (!code2) {
|
|
3450
|
+
res.writeHead(400, { "content-type": "text/plain" });
|
|
3451
|
+
res.end("Missing authorization code");
|
|
3452
|
+
return;
|
|
3453
|
+
}
|
|
3454
|
+
if (returnedState !== state) {
|
|
3455
|
+
res.writeHead(400, { "content-type": "text/plain" });
|
|
3456
|
+
res.end("State mismatch");
|
|
3457
|
+
rejectCode(new Error("OAuth state mismatch \u2014 possible CSRF attack"));
|
|
3458
|
+
return;
|
|
3459
|
+
}
|
|
3460
|
+
res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
|
3461
|
+
res.end(callbackPage("CLI authenticated", "You can return to your terminal.", dashboardUrl));
|
|
3462
|
+
resolveCode(code2);
|
|
3463
|
+
} else {
|
|
3464
|
+
res.writeHead(404);
|
|
3465
|
+
res.end();
|
|
3466
|
+
}
|
|
3467
|
+
});
|
|
3468
|
+
await new Promise((r) => server.listen(0, "127.0.0.1", r));
|
|
3469
|
+
const port = server.address().port;
|
|
3470
|
+
const redirectUri = `http://127.0.0.1:${port}/callback`;
|
|
3471
|
+
const authUrl = new URL(`${dashboardUrl}/oauth/authorize`);
|
|
3472
|
+
authUrl.searchParams.set("client_id", clientId);
|
|
3473
|
+
authUrl.searchParams.set("redirect_uri", redirectUri);
|
|
3474
|
+
authUrl.searchParams.set("code_challenge", codeChallenge);
|
|
3475
|
+
authUrl.searchParams.set("code_challenge_method", "S256");
|
|
3476
|
+
authUrl.searchParams.set("state", state);
|
|
3477
|
+
authUrl.searchParams.set("response_type", "code");
|
|
3478
|
+
const loginUrl = authUrl.toString();
|
|
3479
|
+
await open(loginUrl);
|
|
3480
|
+
const timeout = setTimeout(() => {
|
|
3481
|
+
server.close();
|
|
3482
|
+
rejectCode(new Error("Authentication timed out. Please try again."));
|
|
3483
|
+
}, LOGIN_TIMEOUT_MS);
|
|
3484
|
+
let code;
|
|
3485
|
+
try {
|
|
3486
|
+
code = await codePromise;
|
|
3487
|
+
} catch (err) {
|
|
3488
|
+
clearTimeout(timeout);
|
|
3489
|
+
server.close();
|
|
3490
|
+
throw err;
|
|
3491
|
+
}
|
|
3492
|
+
clearTimeout(timeout);
|
|
3493
|
+
server.close();
|
|
3494
|
+
const tokenRes = await fetch(`${adminApiBaseUrl}/oauth/token`, {
|
|
3495
|
+
method: "POST",
|
|
3496
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
3497
|
+
body: new URLSearchParams({
|
|
3498
|
+
grant_type: "authorization_code",
|
|
3499
|
+
code,
|
|
3500
|
+
code_verifier: codeVerifier,
|
|
3501
|
+
client_id: clientId,
|
|
3502
|
+
redirect_uri: redirectUri
|
|
3503
|
+
}).toString()
|
|
3504
|
+
});
|
|
3505
|
+
if (!tokenRes.ok) {
|
|
3506
|
+
const error = await tokenRes.json().catch(() => ({}));
|
|
3507
|
+
throw new Error(error.error_description ?? error.error ?? `Token exchange failed: ${tokenRes.status}`);
|
|
3508
|
+
}
|
|
3509
|
+
const tokenData = await tokenRes.json();
|
|
3510
|
+
const [, payloadB64] = tokenData.access_token.split(".");
|
|
3511
|
+
const payload = JSON.parse(Buffer.from(payloadB64, "base64url").toString());
|
|
3512
|
+
const authState = {
|
|
3513
|
+
token: tokenData.access_token,
|
|
3514
|
+
userId: asClerkUserId(payload.sub),
|
|
3515
|
+
orgId: asClerkOrgId(payload.orgId)
|
|
3516
|
+
};
|
|
3517
|
+
await writeAuthState(authState);
|
|
3518
|
+
return { authState, loginUrl };
|
|
3519
|
+
};
|
|
3520
|
+
var AuthLogin = ({ dashboardUrl, adminApiBaseUrl }) => {
|
|
3385
3521
|
const { exit } = useApp();
|
|
3386
3522
|
const [state, setState] = useState("waiting");
|
|
3387
3523
|
const [loginUrl, setLoginUrl] = useState("");
|
|
@@ -3389,78 +3525,29 @@ var AuthLogin = ({ dashboardUrl }) => {
|
|
|
3389
3525
|
const [orgIdLabel, setOrgIdLabel] = useState("");
|
|
3390
3526
|
const [errorMessage, setErrorMessage] = useState("");
|
|
3391
3527
|
useEffect(() => {
|
|
3392
|
-
let
|
|
3393
|
-
let timeout;
|
|
3528
|
+
let cancelled = false;
|
|
3394
3529
|
const run = async () => {
|
|
3395
|
-
let resolveToken;
|
|
3396
|
-
let rejectToken;
|
|
3397
|
-
const tokenPromise = new Promise((resolve, reject) => {
|
|
3398
|
-
resolveToken = resolve;
|
|
3399
|
-
rejectToken = reject;
|
|
3400
|
-
});
|
|
3401
|
-
server = createServer((req, res) => {
|
|
3402
|
-
const url2 = new URL(req.url, "http://localhost");
|
|
3403
|
-
if (url2.pathname === "/callback") {
|
|
3404
|
-
const error = url2.searchParams.get("error");
|
|
3405
|
-
if (error) {
|
|
3406
|
-
res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
|
3407
|
-
res.end(callbackPage("Authentication failed", "You can close this tab."));
|
|
3408
|
-
rejectToken(new Error(error));
|
|
3409
|
-
return;
|
|
3410
|
-
}
|
|
3411
|
-
const token2 = url2.searchParams.get("token");
|
|
3412
|
-
if (token2) {
|
|
3413
|
-
res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
|
3414
|
-
res.end(callbackPage("CLI authenticated", "You can return to your terminal.", dashboardUrl));
|
|
3415
|
-
resolveToken(token2);
|
|
3416
|
-
} else {
|
|
3417
|
-
res.writeHead(400, { "content-type": "text/plain; charset=utf-8" });
|
|
3418
|
-
res.end("Missing token");
|
|
3419
|
-
}
|
|
3420
|
-
} else {
|
|
3421
|
-
res.writeHead(404);
|
|
3422
|
-
res.end();
|
|
3423
|
-
}
|
|
3424
|
-
});
|
|
3425
|
-
await new Promise((r) => server.listen(0, "127.0.0.1", r));
|
|
3426
|
-
const port = server.address().port;
|
|
3427
|
-
const callbackUrl = `http://localhost:${port}/callback`;
|
|
3428
|
-
const url = `${dashboardUrl}/cli-auth?callback=${encodeURIComponent(callbackUrl)}`;
|
|
3429
|
-
setLoginUrl(url);
|
|
3430
|
-
await open(url);
|
|
3431
|
-
timeout = setTimeout(() => {
|
|
3432
|
-
server.close();
|
|
3433
|
-
rejectToken(new Error("Login timed out. Please try again."));
|
|
3434
|
-
}, LOGIN_TIMEOUT_MS);
|
|
3435
|
-
let token;
|
|
3436
3530
|
try {
|
|
3437
|
-
|
|
3531
|
+
const result = await performCLIOAuthFlow({ dashboardUrl, adminApiBaseUrl });
|
|
3532
|
+
if (cancelled) {
|
|
3533
|
+
return;
|
|
3534
|
+
}
|
|
3535
|
+
setLoginUrl(result.loginUrl);
|
|
3536
|
+
setUserIdLabel(result.authState.userId);
|
|
3537
|
+
setOrgIdLabel(result.authState.orgId);
|
|
3538
|
+
setState("success");
|
|
3438
3539
|
} catch (err) {
|
|
3439
|
-
|
|
3440
|
-
|
|
3540
|
+
if (cancelled) {
|
|
3541
|
+
return;
|
|
3542
|
+
}
|
|
3441
3543
|
setErrorMessage(err instanceof Error ? err.message : String(err));
|
|
3442
3544
|
setState("error");
|
|
3443
|
-
exit();
|
|
3444
|
-
return;
|
|
3445
3545
|
}
|
|
3446
|
-
clearTimeout(timeout);
|
|
3447
|
-
server.close();
|
|
3448
|
-
const [, payloadB64] = token.split(".");
|
|
3449
|
-
const payload = JSON.parse(Buffer.from(payloadB64, "base64url").toString());
|
|
3450
|
-
await writeAuthState({
|
|
3451
|
-
token,
|
|
3452
|
-
userId: asClerkUserId(payload.sub),
|
|
3453
|
-
orgId: asClerkOrgId(payload.orgId)
|
|
3454
|
-
});
|
|
3455
|
-
setUserIdLabel(payload.sub);
|
|
3456
|
-
setOrgIdLabel(payload.orgId);
|
|
3457
|
-
setState("success");
|
|
3458
3546
|
exit();
|
|
3459
3547
|
};
|
|
3460
3548
|
run();
|
|
3461
3549
|
return () => {
|
|
3462
|
-
|
|
3463
|
-
server?.close();
|
|
3550
|
+
cancelled = true;
|
|
3464
3551
|
};
|
|
3465
3552
|
}, []);
|
|
3466
3553
|
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
|
|
@@ -3708,7 +3795,7 @@ program.command("dev" /* DEV */).description("Start dev servers with a public tu
|
|
|
3708
3795
|
var DASHBOARD_URL = process.env.ADMIN_DASHBOARD_URL ?? "https://admin.stackablelabs.com";
|
|
3709
3796
|
var auth = program.command("auth").description("Manage CLI authentication");
|
|
3710
3797
|
auth.command("login").description("Authenticate with Stackable via browser").action(async () => {
|
|
3711
|
-
render(/* @__PURE__ */ jsx(AuthLogin, { dashboardUrl: DASHBOARD_URL }));
|
|
3798
|
+
render(/* @__PURE__ */ jsx(AuthLogin, { dashboardUrl: DASHBOARD_URL, adminApiBaseUrl: getAdminApiBaseUrl() }));
|
|
3712
3799
|
});
|
|
3713
3800
|
auth.command("logout").description("Clear stored CLI credentials").action(async () => {
|
|
3714
3801
|
await clearAuthState();
|