@stackable-labs/cli-app-extension 1.91.2 → 1.92.1

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 +241 -91
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -2,14 +2,14 @@
2
2
  import { createRequire } from 'module';
3
3
  import { program } from 'commander';
4
4
  import { render, useApp, Box, Text, useInput, useFocus, useFocusManager } from 'ink';
5
- import { readFile, writeFile, mkdir, readdir, rm } from 'fs/promises';
5
+ import { unlink, readFile, writeFile, mkdir, readdir, rm } from 'fs/promises';
6
6
  import { join, dirname } from 'path';
7
7
  import Spinner5 from 'ink-spinner';
8
8
  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 { homedir } from 'os';
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
 
@@ -1127,6 +1166,92 @@ var TemplateSelect = ({ onSubmit, onBack }) => {
1127
1166
  }
1128
1167
  );
1129
1168
  };
1169
+
1170
+ // ../../lib/contracts/src/base.ts
1171
+ var asClerkUserId = (value) => value;
1172
+ var asClerkOrgId = (value) => value;
1173
+
1174
+ // ../../lib/contracts/src/permissions.ts
1175
+ var SUPER_ROLE = {
1176
+ ADMIN: "org:super_admin"
1177
+ };
1178
+ var ORG_ROLE = {
1179
+ ADMIN: "org:admin",
1180
+ OWNER: "org:owner"};
1181
+ var EDITOR_ROLES = [
1182
+ SUPER_ROLE.ADMIN,
1183
+ ORG_ROLE.ADMIN,
1184
+ ORG_ROLE.OWNER
1185
+ ];
1186
+ [
1187
+ ...Object.values(SUPER_ROLE),
1188
+ ...Object.values(EDITOR_ROLES)
1189
+ ];
1190
+
1191
+ // ../../lib/utils-auth/src/constants.ts
1192
+ var STANDALONE_CLIENT_DATA = {
1193
+ CLI: { name: "@stackable-labs/cli-app-extension", authFile: "cli-auth.json" },
1194
+ MCP: { name: "@stackable-labs/mcp-app-extension", authFile: "mcp-auth.json" }
1195
+ };
1196
+ var STANDALONE_CLIENT = Object.fromEntries(
1197
+ Object.entries(STANDALONE_CLIENT_DATA).map(([k, v]) => [k, v.name])
1198
+ );
1199
+ var STANDALONE_CLIENT_AUTH_FILE = Object.fromEntries(
1200
+ Object.entries(STANDALONE_CLIENT_DATA).map(([k, v]) => [k, v.authFile])
1201
+ );
1202
+ Object.values(STANDALONE_CLIENT);
1203
+
1204
+ // ../../lib/utils-auth/src/index.ts
1205
+ var deriveClientId = async (clientName) => (await getDigest(clientName)).slice(0, 32);
1206
+ var AUTH_DIR = join(homedir(), ".stackable");
1207
+ join(AUTH_DIR, STANDALONE_CLIENT_AUTH_FILE.CLI);
1208
+ join(AUTH_DIR, STANDALONE_CLIENT_AUTH_FILE.MCP);
1209
+ var resolveAuthFile = (filename) => join(AUTH_DIR, STANDALONE_CLIENT_AUTH_FILE.CLI);
1210
+ var readAuthState = async (filename) => {
1211
+ try {
1212
+ const content = await readFile(resolveAuthFile(filename), "utf8");
1213
+ return JSON.parse(content);
1214
+ } catch {
1215
+ return null;
1216
+ }
1217
+ };
1218
+ var writeAuthState = async (state, filename) => {
1219
+ await mkdir(AUTH_DIR, { recursive: true, mode: 448 });
1220
+ await writeFile(resolveAuthFile(), JSON.stringify(state, null, 2), { mode: 384 });
1221
+ };
1222
+ var clearAuthState = async (filename) => {
1223
+ try {
1224
+ await unlink(resolveAuthFile(filename));
1225
+ } catch {
1226
+ }
1227
+ };
1228
+ var decodeJwtPayload = (token) => {
1229
+ try {
1230
+ const [, payload] = token.split(".");
1231
+ if (!payload) {
1232
+ return null;
1233
+ }
1234
+ const json = Buffer.from(payload, "base64url").toString("utf8");
1235
+ return JSON.parse(json);
1236
+ } catch {
1237
+ return null;
1238
+ }
1239
+ };
1240
+ var getToken = async (filename) => {
1241
+ const state = await readAuthState(filename);
1242
+ if (!state) {
1243
+ throw new Error("Not authenticated. Run `stackable-app-extension auth login` first.");
1244
+ }
1245
+ const payload = decodeJwtPayload(state.token);
1246
+ if (payload?.exp && typeof payload.exp === "number") {
1247
+ if (Date.now() >= payload.exp * 1e3) {
1248
+ throw new Error("Session expired. Run `stackable-app-extension auth login` to re-authenticate.");
1249
+ }
1250
+ }
1251
+ return state.token;
1252
+ };
1253
+
1254
+ // src/lib/api.ts
1130
1255
  var DEFAULT_ADMIN_API_URL = "https://api-use1.stackablelabs.io/admin";
1131
1256
  var DEFAULT_PUBLIC_API_URL = "https://api.stackablelabs.io/app-extension/latest";
1132
1257
  var getAdminApiBaseUrl = () => process.env.ADMIN_API_BASE_URL ?? DEFAULT_ADMIN_API_URL;
@@ -1526,13 +1651,15 @@ export const appStore = createStore<AppState>({
1526
1651
  {
1527
1652
  path: "surfaces/Content.tsx",
1528
1653
  title: "Content Surface with Loading State",
1529
- code: `import { ui, useStore, useContextData, useSettings, Surface } from '@stackable-labs/sdk-extension-react'
1654
+ code: `import { ui, useStore, useContextData, Surface } from '@stackable-labs/sdk-extension-react'
1530
1655
  import { appStore } from '../store'
1531
1656
 
1532
1657
  export function Content() {
1533
1658
  const viewState = useStore(appStore, (s) => s.viewState)
1534
1659
  const { loading } = useContextData()
1535
- const settings = useSettings() // Non-secret settings from settingsSchema
1660
+ // Non-secret settings from settingsSchema (add settingsSchema to manifest.json to use). Ex:
1661
+ // const settings = useSettings()
1662
+ // const apiEndpoint = settings.apiEndpoint as string
1536
1663
 
1537
1664
  if (loading) {
1538
1665
  return (
@@ -1661,29 +1788,6 @@ export function createFetchApi(fetch: FetchFn) {
1661
1788
  }`
1662
1789
  }
1663
1790
  ];
1664
-
1665
- // ../../lib/contracts/src/base.ts
1666
- var asClerkUserId = (value) => value;
1667
- var asClerkOrgId = (value) => value;
1668
-
1669
- // ../../lib/contracts/src/permissions.ts
1670
- var SUPER_ROLE = {
1671
- ADMIN: "org:super_admin"
1672
- };
1673
- var ORG_ROLE = {
1674
- ADMIN: "org:admin",
1675
- OWNER: "org:owner"};
1676
- var EDITOR_ROLES = [
1677
- SUPER_ROLE.ADMIN,
1678
- ORG_ROLE.ADMIN,
1679
- ORG_ROLE.OWNER
1680
- ];
1681
- [
1682
- ...Object.values(SUPER_ROLE),
1683
- ...Object.values(EDITOR_ROLES)
1684
- ];
1685
-
1686
- // src/lib/devContext.ts
1687
1791
  var parseEnvFile = (content) => {
1688
1792
  const lines = content.split("\n");
1689
1793
  const env = {};
@@ -3381,7 +3485,102 @@ var callbackPage = (heading, sub, redirectUrl) => `<!DOCTYPE html>
3381
3485
  </head><body><div class="card"><h2>${heading}</h2><p>${sub}</p><p class="hint" id="h"></p></div>
3382
3486
  ${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
3487
  </body></html>`;
3384
- var AuthLogin = ({ dashboardUrl }) => {
3488
+ var performCLIOAuthFlow = async ({ dashboardUrl, adminApiBaseUrl }) => {
3489
+ const clientId = await deriveClientId(STANDALONE_CLIENT.CLI);
3490
+ const codeVerifier = getVerifier();
3491
+ const codeChallenge = await getDigest(codeVerifier, "base64url");
3492
+ const state = getNonce();
3493
+ let resolveCode;
3494
+ let rejectCode;
3495
+ const codePromise = new Promise((resolve, reject) => {
3496
+ resolveCode = resolve;
3497
+ rejectCode = reject;
3498
+ });
3499
+ let server;
3500
+ server = createServer((req, res) => {
3501
+ const url = new URL(req.url, "http://localhost");
3502
+ if (url.pathname === "/callback") {
3503
+ const error = url.searchParams.get("error");
3504
+ if (error) {
3505
+ res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
3506
+ res.end(callbackPage("Authentication failed", "You can close this tab."));
3507
+ rejectCode(new Error(url.searchParams.get("error_description") ?? error));
3508
+ return;
3509
+ }
3510
+ const code2 = url.searchParams.get("code");
3511
+ const returnedState = url.searchParams.get("state");
3512
+ if (!code2) {
3513
+ res.writeHead(400, { "content-type": "text/plain" });
3514
+ res.end("Missing authorization code");
3515
+ return;
3516
+ }
3517
+ if (returnedState !== state) {
3518
+ res.writeHead(400, { "content-type": "text/plain" });
3519
+ res.end("State mismatch");
3520
+ rejectCode(new Error("OAuth state mismatch \u2014 possible CSRF attack"));
3521
+ return;
3522
+ }
3523
+ res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
3524
+ res.end(callbackPage("CLI authenticated", "You can return to your terminal.", dashboardUrl));
3525
+ resolveCode(code2);
3526
+ } else {
3527
+ res.writeHead(404);
3528
+ res.end();
3529
+ }
3530
+ });
3531
+ await new Promise((r) => server.listen(0, "127.0.0.1", r));
3532
+ const port = server.address().port;
3533
+ const redirectUri = `http://127.0.0.1:${port}/callback`;
3534
+ const authUrl = new URL(`${dashboardUrl}/oauth/authorize`);
3535
+ authUrl.searchParams.set("client_id", clientId);
3536
+ authUrl.searchParams.set("redirect_uri", redirectUri);
3537
+ authUrl.searchParams.set("code_challenge", codeChallenge);
3538
+ authUrl.searchParams.set("code_challenge_method", "S256");
3539
+ authUrl.searchParams.set("state", state);
3540
+ authUrl.searchParams.set("response_type", "code");
3541
+ const loginUrl = authUrl.toString();
3542
+ await open(loginUrl);
3543
+ const timeout = setTimeout(() => {
3544
+ server.close();
3545
+ rejectCode(new Error("Authentication timed out. Please try again."));
3546
+ }, LOGIN_TIMEOUT_MS);
3547
+ let code;
3548
+ try {
3549
+ code = await codePromise;
3550
+ } catch (err) {
3551
+ clearTimeout(timeout);
3552
+ server.close();
3553
+ throw err;
3554
+ }
3555
+ clearTimeout(timeout);
3556
+ server.close();
3557
+ const tokenRes = await fetch(`${adminApiBaseUrl}/oauth/token`, {
3558
+ method: "POST",
3559
+ headers: { "content-type": "application/x-www-form-urlencoded" },
3560
+ body: new URLSearchParams({
3561
+ grant_type: "authorization_code",
3562
+ code,
3563
+ code_verifier: codeVerifier,
3564
+ client_id: clientId,
3565
+ redirect_uri: redirectUri
3566
+ }).toString()
3567
+ });
3568
+ if (!tokenRes.ok) {
3569
+ const error = await tokenRes.json().catch(() => ({}));
3570
+ throw new Error(error.error_description ?? error.error ?? `Token exchange failed: ${tokenRes.status}`);
3571
+ }
3572
+ const tokenData = await tokenRes.json();
3573
+ const [, payloadB64] = tokenData.access_token.split(".");
3574
+ const payload = JSON.parse(Buffer.from(payloadB64, "base64url").toString());
3575
+ const authState = {
3576
+ token: tokenData.access_token,
3577
+ userId: asClerkUserId(payload.sub),
3578
+ orgId: asClerkOrgId(payload.orgId)
3579
+ };
3580
+ await writeAuthState(authState);
3581
+ return { authState, loginUrl };
3582
+ };
3583
+ var AuthLogin = ({ dashboardUrl, adminApiBaseUrl }) => {
3385
3584
  const { exit } = useApp();
3386
3585
  const [state, setState] = useState("waiting");
3387
3586
  const [loginUrl, setLoginUrl] = useState("");
@@ -3389,78 +3588,29 @@ var AuthLogin = ({ dashboardUrl }) => {
3389
3588
  const [orgIdLabel, setOrgIdLabel] = useState("");
3390
3589
  const [errorMessage, setErrorMessage] = useState("");
3391
3590
  useEffect(() => {
3392
- let server;
3393
- let timeout;
3591
+ let cancelled = false;
3394
3592
  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
3593
  try {
3437
- token = await tokenPromise;
3594
+ const result = await performCLIOAuthFlow({ dashboardUrl, adminApiBaseUrl });
3595
+ if (cancelled) {
3596
+ return;
3597
+ }
3598
+ setLoginUrl(result.loginUrl);
3599
+ setUserIdLabel(result.authState.userId);
3600
+ setOrgIdLabel(result.authState.orgId);
3601
+ setState("success");
3438
3602
  } catch (err) {
3439
- clearTimeout(timeout);
3440
- server.close();
3603
+ if (cancelled) {
3604
+ return;
3605
+ }
3441
3606
  setErrorMessage(err instanceof Error ? err.message : String(err));
3442
3607
  setState("error");
3443
- exit();
3444
- return;
3445
3608
  }
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
3609
  exit();
3459
3610
  };
3460
3611
  run();
3461
3612
  return () => {
3462
- clearTimeout(timeout);
3463
- server?.close();
3613
+ cancelled = true;
3464
3614
  };
3465
3615
  }, []);
3466
3616
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
@@ -3708,7 +3858,7 @@ program.command("dev" /* DEV */).description("Start dev servers with a public tu
3708
3858
  var DASHBOARD_URL = process.env.ADMIN_DASHBOARD_URL ?? "https://admin.stackablelabs.com";
3709
3859
  var auth = program.command("auth").description("Manage CLI authentication");
3710
3860
  auth.command("login").description("Authenticate with Stackable via browser").action(async () => {
3711
- render(/* @__PURE__ */ jsx(AuthLogin, { dashboardUrl: DASHBOARD_URL }));
3861
+ render(/* @__PURE__ */ jsx(AuthLogin, { dashboardUrl: DASHBOARD_URL, adminApiBaseUrl: getAdminApiBaseUrl() }));
3712
3862
  });
3713
3863
  auth.command("logout").description("Clear stored CLI credentials").action(async () => {
3714
3864
  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.1",
4
4
  "type": "module",
5
5
  "private": false,
6
6
  "bin": {