@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.
Files changed (2) hide show
  1. package/dist/index.js +154 -67
  2. 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, useSettings, Surface } from '@stackable-labs/sdk-extension-react'
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
- const settings = useSettings() // Non-secret settings from settingsSchema
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 AuthLogin = ({ dashboardUrl }) => {
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 server;
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
- token = await tokenPromise;
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
- clearTimeout(timeout);
3440
- server.close();
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
- clearTimeout(timeout);
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();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stackable-labs/cli-app-extension",
3
- "version": "1.91.2",
3
+ "version": "1.92.0",
4
4
  "type": "module",
5
5
  "private": false,
6
6
  "bin": {