statelyai 0.5.3 → 0.6.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/README.md CHANGED
@@ -7,20 +7,32 @@ This package contains the `statelyai` command implementation.
7
7
  <!-- happy-path CLI usage and statelyai.json shape derived from packages/cli/src/cli.ts#COMMANDS and packages/cli/src/projectConfig.ts -->
8
8
  ## Happy Path
9
9
 
10
- 1. Get an API key from [Stately API Key settings](https://stately.ai/registry/user/my-settings?tab=API+Key).
11
- 2. Log in once:
10
+ 1. Log in once:
12
11
 
13
12
  ```bash
14
13
  statelyai login
15
14
  ```
16
15
 
17
- 3. Initialize the current repo:
16
+ Automation can still pass `--api-key`, set `STATELY_API_KEY`, or set
17
+ `STATELY_ACCESS_TOKEN`. For no-auth self-hosted deployments, run
18
+ `statelyai login --auth none`.
19
+
20
+ For a self-hosted or local MCP/API resource, discover OAuth from that resource:
21
+
22
+ ```bash
23
+ statelyai login --base-url http://localhost:3000/registry/api/mcp
24
+ ```
25
+
26
+ `statelyai login --auth bearer` uses the same browser OAuth flow explicitly.
27
+ `statelyai login --api-key` keeps the legacy manual API-key path.
28
+
29
+ 2. Initialize the current repo:
18
30
 
19
31
  ```bash
20
32
  statelyai init
21
33
  ```
22
34
 
23
- 4. Optionally scan the repo and save suggested source globs:
35
+ 3. Optionally scan the repo and save suggested source globs:
24
36
 
25
37
  ```bash
26
38
  statelyai init --scan
@@ -43,7 +55,7 @@ That writes a `statelyai.json` like:
43
55
  }
44
56
  ```
45
57
 
46
- 5. Push local machines:
58
+ 4. Push local machines:
47
59
 
48
60
  ```bash
49
61
  statelyai push
@@ -57,7 +69,7 @@ statelyai push --dry-run
57
69
 
58
70
  If a saved `// @statelyai id=...` points to a deleted or inaccessible remote machine, `push` will prompt to relink the file as a new remote machine and replace the local id.
59
71
 
60
- 6. Pull remote changes back into linked local files and create new local files for remote-only project machines when `newMachinesDir` is configured:
72
+ 5. Pull remote changes back into linked local files and create new local files for remote-only project machines when `newMachinesDir` is configured:
61
73
 
62
74
  ```bash
63
75
  statelyai pull
package/dist/bin.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { p as run } from "./cli-DB0A-O4Y.mjs";
2
+ import { _ as run } from "./cli-Buz_BC3I.mjs";
3
3
 
4
4
  //#region src/bin.ts
5
5
  run();
@@ -1,5 +1,7 @@
1
+ import * as crypto from "node:crypto";
1
2
  import fs, { promises, watch } from "node:fs";
2
3
  import fsPromises from "node:fs/promises";
4
+ import * as http from "node:http";
3
5
  import { execFile, execFileSync, spawn } from "node:child_process";
4
6
  import * as path$1 from "node:path";
5
7
  import path from "node:path";
@@ -8,10 +10,8 @@ import { Writable } from "node:stream";
8
10
  import { fileURLToPath, pathToFileURL } from "node:url";
9
11
  import { promisify } from "node:util";
10
12
  import { Args, Command, Flags, flush, handle, run } from "@oclif/core";
11
- import * as crypto from "node:crypto";
12
- import * as http from "node:http";
13
13
  import os from "node:os";
14
- import { StudioApiError, createStatelyClient, getStatelyPragma } from "@statelyai/sdk";
14
+ import { StudioApiError, buildAuthorizeUrl, createPkcePair, createStatelyClient, discoverAuthorizationServer, discoverProtectedResource, exchangeCodeForToken, getStatelyPragma } from "@statelyai/sdk";
15
15
  import { planSync, pullSync, pushLocalMachineLinks } from "@statelyai/sdk/sync";
16
16
 
17
17
  //#region src/cliHost.ts
@@ -701,36 +701,109 @@ function getCredentialsFilePath() {
701
701
  return path.join(getConfigDir(), CREDENTIALS_FILE_NAME);
702
702
  }
703
703
  function shouldUseFileBackendOnly() {
704
- return process.env.STATELYAI_CREDENTIALS_BACKEND === "file";
704
+ return process.env.STATELYAI_CREDENTIALS_BACKEND === "file" || process.env.STATELYAI_CONFIG_DIR !== void 0;
705
705
  }
706
706
  async function readFileCredentials() {
707
+ const storedCredential = await readFileStoredCredential();
708
+ if (storedCredential?.credential?.type !== "api_key") return;
709
+ return {
710
+ apiKey: storedCredential.credential.token,
711
+ backend: storedCredential.backend,
712
+ location: storedCredential.location
713
+ };
714
+ }
715
+ function parseStoredCredential(raw, backend, location) {
707
716
  try {
708
- const raw = await promises.readFile(getCredentialsFilePath(), "utf8");
709
717
  const parsed = JSON.parse(raw);
710
- if (typeof parsed.apiKey !== "string" || parsed.apiKey.length === 0) return;
718
+ if (typeof parsed.apiKey === "string" && parsed.apiKey.length > 0) return {
719
+ credential: {
720
+ type: "api_key",
721
+ token: parsed.apiKey
722
+ },
723
+ backend,
724
+ location
725
+ };
726
+ if (parsed.authMode === "none") return {
727
+ authMode: "none",
728
+ backend,
729
+ location
730
+ };
731
+ if (typeof parsed.credential === "object" && parsed.credential !== null && "type" in parsed.credential) {
732
+ const credential = parsed.credential;
733
+ if (credential.type === "api_key" && typeof credential.token === "string" && credential.token.length > 0) return {
734
+ credential: {
735
+ type: "api_key",
736
+ token: credential.token
737
+ },
738
+ backend,
739
+ location
740
+ };
741
+ if (credential.type === "oauth" && typeof credential.accessToken === "string" && credential.accessToken.length > 0) return {
742
+ credential: {
743
+ type: "oauth",
744
+ accessToken: credential.accessToken,
745
+ ...typeof credential.refreshToken === "string" ? { refreshToken: credential.refreshToken } : {},
746
+ ...typeof credential.expiresAt === "number" ? { expiresAt: credential.expiresAt } : {}
747
+ },
748
+ backend,
749
+ location
750
+ };
751
+ }
752
+ return;
753
+ } catch {
754
+ const token = raw.trim();
755
+ if (!token) return;
711
756
  return {
712
- apiKey: parsed.apiKey,
713
- backend: "file",
714
- location: getCredentialsFilePath()
757
+ credential: {
758
+ type: "api_key",
759
+ token
760
+ },
761
+ backend,
762
+ location
715
763
  };
764
+ }
765
+ }
766
+ async function readFileStoredCredential() {
767
+ try {
768
+ const credentialsPath = getCredentialsFilePath();
769
+ return parseStoredCredential(await promises.readFile(credentialsPath, "utf8"), "file", credentialsPath);
716
770
  } catch {
717
771
  return;
718
772
  }
719
773
  }
720
774
  async function writeFileCredentials(apiKey) {
775
+ const stored = await writeFileStoredCredential({
776
+ type: "api_key",
777
+ token: apiKey
778
+ });
779
+ return {
780
+ apiKey,
781
+ backend: stored.backend,
782
+ location: stored.location
783
+ };
784
+ }
785
+ async function writeFileStoredCredential(credential, authMode) {
721
786
  const configDir = getConfigDir();
722
787
  const credentialsPath = getCredentialsFilePath();
788
+ const payload = authMode === "none" ? {
789
+ version: 1,
790
+ authMode: "none"
791
+ } : {
792
+ version: 1,
793
+ credential
794
+ };
723
795
  await promises.mkdir(configDir, {
724
796
  recursive: true,
725
797
  mode: 448
726
798
  });
727
- await promises.writeFile(credentialsPath, `${JSON.stringify({ apiKey }, null, 2)}\n`, {
799
+ await promises.writeFile(credentialsPath, `${JSON.stringify(payload, null, 2)}\n`, {
728
800
  encoding: "utf8",
729
801
  mode: 384
730
802
  });
731
803
  await promises.chmod(credentialsPath, 384);
732
804
  return {
733
- apiKey,
805
+ ...credential ? { credential } : {},
806
+ ...authMode ? { authMode } : {},
734
807
  backend: "file",
735
808
  location: credentialsPath
736
809
  };
@@ -744,6 +817,15 @@ async function deleteFileCredentials() {
744
817
  }
745
818
  }
746
819
  async function readMacOSKeychain() {
820
+ const storedCredential = await readMacOSStoredCredential();
821
+ if (storedCredential?.credential?.type !== "api_key") return;
822
+ return {
823
+ apiKey: storedCredential.credential.token,
824
+ backend: storedCredential.backend,
825
+ location: storedCredential.location
826
+ };
827
+ }
828
+ async function readMacOSStoredCredential() {
747
829
  if (process.platform !== "darwin") return;
748
830
  try {
749
831
  const { stdout } = await execFile$1("security", [
@@ -754,19 +836,31 @@ async function readMacOSKeychain() {
754
836
  ACCOUNT_NAME,
755
837
  "-w"
756
838
  ]);
757
- const apiKey = stdout.trim();
758
- if (!apiKey) return;
759
- return {
760
- apiKey,
761
- backend: "macos-keychain",
762
- location: "macOS Keychain"
763
- };
839
+ return parseStoredCredential(stdout, "macos-keychain", "macOS Keychain");
764
840
  } catch {
765
841
  return;
766
842
  }
767
843
  }
768
844
  async function writeMacOSKeychain(apiKey) {
845
+ const stored = await writeMacOSStoredCredential({
846
+ type: "api_key",
847
+ token: apiKey
848
+ });
849
+ return stored ? {
850
+ apiKey,
851
+ backend: stored.backend,
852
+ location: stored.location
853
+ } : void 0;
854
+ }
855
+ async function writeMacOSStoredCredential(credential, authMode) {
769
856
  if (process.platform !== "darwin") return;
857
+ const payload = authMode === "none" ? {
858
+ version: 1,
859
+ authMode: "none"
860
+ } : {
861
+ version: 1,
862
+ credential
863
+ };
770
864
  try {
771
865
  await execFile$1("security", [
772
866
  "add-generic-password",
@@ -776,10 +870,11 @@ async function writeMacOSKeychain(apiKey) {
776
870
  "-a",
777
871
  ACCOUNT_NAME,
778
872
  "-w",
779
- apiKey
873
+ JSON.stringify(payload)
780
874
  ]);
781
875
  return {
782
- apiKey,
876
+ ...credential ? { credential } : {},
877
+ ...authMode ? { authMode } : {},
783
878
  backend: "macos-keychain",
784
879
  location: "macOS Keychain"
785
880
  };
@@ -803,6 +898,15 @@ async function deleteMacOSKeychain() {
803
898
  }
804
899
  }
805
900
  async function readLinuxSecretTool() {
901
+ const storedCredential = await readLinuxStoredCredential();
902
+ if (storedCredential?.credential?.type !== "api_key") return;
903
+ return {
904
+ apiKey: storedCredential.credential.token,
905
+ backend: storedCredential.backend,
906
+ location: storedCredential.location
907
+ };
908
+ }
909
+ async function readLinuxStoredCredential() {
806
910
  if (process.platform !== "linux") return;
807
911
  try {
808
912
  const { stdout } = await execFile$1("secret-tool", [
@@ -812,19 +916,31 @@ async function readLinuxSecretTool() {
812
916
  "account",
813
917
  ACCOUNT_NAME
814
918
  ]);
815
- const apiKey = stdout.trim();
816
- if (!apiKey) return;
817
- return {
818
- apiKey,
819
- backend: "linux-secret-tool",
820
- location: "Secret Service (secret-tool)"
821
- };
919
+ return parseStoredCredential(stdout, "linux-secret-tool", "Secret Service (secret-tool)");
822
920
  } catch {
823
921
  return;
824
922
  }
825
923
  }
826
924
  async function writeLinuxSecretTool(apiKey) {
925
+ const stored = await writeLinuxStoredCredential({
926
+ type: "api_key",
927
+ token: apiKey
928
+ });
929
+ return stored ? {
930
+ apiKey,
931
+ backend: stored.backend,
932
+ location: stored.location
933
+ } : void 0;
934
+ }
935
+ async function writeLinuxStoredCredential(credential, authMode) {
827
936
  if (process.platform !== "linux") return;
937
+ const payload = authMode === "none" ? {
938
+ version: 1,
939
+ authMode: "none"
940
+ } : {
941
+ version: 1,
942
+ credential
943
+ };
828
944
  try {
829
945
  execFileSync("secret-tool", [
830
946
  "store",
@@ -834,9 +950,10 @@ async function writeLinuxSecretTool(apiKey) {
834
950
  SERVICE_NAME,
835
951
  "account",
836
952
  ACCOUNT_NAME
837
- ], { input: apiKey });
953
+ ], { input: JSON.stringify(payload) });
838
954
  return {
839
- apiKey,
955
+ ...credential ? { credential } : {},
956
+ ...authMode ? { authMode } : {},
840
957
  backend: "linux-secret-tool",
841
958
  location: "Secret Service (secret-tool)"
842
959
  };
@@ -863,10 +980,22 @@ async function getStoredApiKey() {
863
980
  if (shouldUseFileBackendOnly()) return readFileCredentials();
864
981
  return await readMacOSKeychain() ?? await readLinuxSecretTool() ?? await readFileCredentials();
865
982
  }
983
+ async function getStoredCredential() {
984
+ if (shouldUseFileBackendOnly()) return readFileStoredCredential();
985
+ return await readMacOSStoredCredential() ?? await readLinuxStoredCredential() ?? await readFileStoredCredential();
986
+ }
866
987
  async function setStoredApiKey(apiKey) {
867
988
  if (shouldUseFileBackendOnly()) return writeFileCredentials(apiKey);
868
989
  return await writeMacOSKeychain(apiKey) ?? await writeLinuxSecretTool(apiKey) ?? await writeFileCredentials(apiKey);
869
990
  }
991
+ async function setStoredCredential(credential) {
992
+ if (shouldUseFileBackendOnly()) return writeFileStoredCredential(credential);
993
+ return await writeMacOSStoredCredential(credential) ?? await writeLinuxStoredCredential(credential) ?? await writeFileStoredCredential(credential);
994
+ }
995
+ async function setStoredNoAuth() {
996
+ if (shouldUseFileBackendOnly()) return writeFileStoredCredential(void 0, "none");
997
+ return await writeMacOSStoredCredential(void 0, "none") ?? await writeLinuxStoredCredential(void 0, "none") ?? await writeFileStoredCredential(void 0, "none");
998
+ }
870
999
  async function deleteStoredApiKey() {
871
1000
  if (shouldUseFileBackendOnly()) {
872
1001
  const deleted = await deleteFileCredentials();
@@ -1168,6 +1297,11 @@ function loadLocalEnv() {
1168
1297
  }
1169
1298
  loadLocalEnv();
1170
1299
  function getEnvApiKey() {
1300
+ const accessToken = process.env.STATELY_ACCESS_TOKEN;
1301
+ if (accessToken) return {
1302
+ apiKey: accessToken,
1303
+ variable: "STATELY_ACCESS_TOKEN"
1304
+ };
1171
1305
  const statelyApiKey = process.env.STATELY_API_KEY;
1172
1306
  if (statelyApiKey) return {
1173
1307
  apiKey: statelyApiKey,
@@ -1179,6 +1313,32 @@ function getEnvApiKey() {
1179
1313
  variable: "NEXT_PUBLIC_STATELY_API_KEY"
1180
1314
  };
1181
1315
  }
1316
+ function getEnvCredential() {
1317
+ const accessToken = process.env.STATELY_ACCESS_TOKEN;
1318
+ if (accessToken) return {
1319
+ credential: {
1320
+ type: "oauth",
1321
+ accessToken
1322
+ },
1323
+ variable: "STATELY_ACCESS_TOKEN"
1324
+ };
1325
+ const statelyApiKey = process.env.STATELY_API_KEY;
1326
+ if (statelyApiKey) return {
1327
+ credential: {
1328
+ type: "api_key",
1329
+ token: statelyApiKey
1330
+ },
1331
+ variable: "STATELY_API_KEY"
1332
+ };
1333
+ const publicApiKey = process.env.NEXT_PUBLIC_STATELY_API_KEY;
1334
+ if (publicApiKey) return {
1335
+ credential: {
1336
+ type: "api_key",
1337
+ token: publicApiKey
1338
+ },
1339
+ variable: "NEXT_PUBLIC_STATELY_API_KEY"
1340
+ };
1341
+ }
1182
1342
  async function resolveApiKey(explicitApiKey) {
1183
1343
  if (explicitApiKey) return {
1184
1344
  apiKey: explicitApiKey,
@@ -1208,6 +1368,37 @@ function getDefaultEditorUrl() {
1208
1368
  function getResolvedStudioUrl(baseUrl) {
1209
1369
  return baseUrl ?? getDefaultBaseUrl() ?? "https://stately.ai";
1210
1370
  }
1371
+ function getDefaultMcpResourceUrl() {
1372
+ return process.env.STATELY_MCP_RESOURCE_URL ?? process.env.NEXT_PUBLIC_MCP_RESOURCE_URL ?? process.env.STATELY_MCP_API_BASE_URL ?? "https://editor.stately.ai/api/mcp";
1373
+ }
1374
+ function normalizeLoginResourceUrl(baseUrl) {
1375
+ const raw = baseUrl ?? getDefaultMcpResourceUrl();
1376
+ const url = new URL(raw);
1377
+ if (url.pathname === "/" || url.pathname === "") url.pathname = "/api/mcp";
1378
+ return url.toString();
1379
+ }
1380
+ function getOauthClientId() {
1381
+ return process.env.STATELY_OAUTH_CLIENT_ID ?? process.env.NEXT_PUBLIC_STATELY_OAUTH_CLIENT_ID ?? "statelyai-cli";
1382
+ }
1383
+ async function discoverOAuthLogin(options = {}) {
1384
+ const resourceUrl = normalizeLoginResourceUrl(options.baseUrl);
1385
+ const protectedResource = await discoverProtectedResource(resourceUrl, { fetch: options.fetch });
1386
+ const [issuer] = protectedResource.authorization_servers ?? [];
1387
+ if (!issuer) throw new Error(`No OAuth authorization server advertised by ${resourceUrl}. Use \`statelyai login --auth none\` for no-auth self-hosted deployments.`);
1388
+ const authorizationServer = await discoverAuthorizationServer(issuer, { fetch: options.fetch });
1389
+ const authorizationEndpoint = authorizationServer.authorization_endpoint;
1390
+ const tokenEndpoint = authorizationServer.token_endpoint;
1391
+ if (!authorizationEndpoint || !tokenEndpoint) throw new Error(`Authorization server ${issuer} did not advertise authorization and token endpoints.`);
1392
+ return {
1393
+ authorizationServer,
1394
+ authorizationEndpoint,
1395
+ clientId: options.clientId ?? getOauthClientId(),
1396
+ protectedResource,
1397
+ resourceUrl,
1398
+ scopes: protectedResource.scopes_supported ?? [],
1399
+ tokenEndpoint
1400
+ };
1401
+ }
1211
1402
  function parseGitHubRemote(remoteUrl) {
1212
1403
  const httpsMatch = remoteUrl.match(/^https:\/\/github\.com\/([^/]+)\/([^/]+?)(?:\.git)?$/);
1213
1404
  if (httpsMatch) {
@@ -1289,6 +1480,147 @@ async function promptForApiKey() {
1289
1480
  rl.close();
1290
1481
  }
1291
1482
  }
1483
+ async function promptForOAuthCallback() {
1484
+ if (!process.stdin.isTTY || !process.stdout.isTTY) throw new Error("No interactive terminal available to paste the OAuth callback URL.");
1485
+ const rl = createInterface({
1486
+ input: process.stdin,
1487
+ output: process.stdout,
1488
+ terminal: true
1489
+ });
1490
+ try {
1491
+ return (await rl.question("Paste the callback URL or code: ")).trim();
1492
+ } finally {
1493
+ rl.close();
1494
+ }
1495
+ }
1496
+ function parseOAuthCallback(value) {
1497
+ try {
1498
+ const url = new URL(value);
1499
+ const code = url.searchParams.get("code");
1500
+ const state = url.searchParams.get("state") ?? void 0;
1501
+ if (!code) throw new Error("Callback URL did not include a code parameter.");
1502
+ return {
1503
+ code,
1504
+ state
1505
+ };
1506
+ } catch (error) {
1507
+ if (error instanceof Error && error.message.includes("code parameter")) throw error;
1508
+ if (!value) throw new Error("OAuth code cannot be empty.");
1509
+ return { code: value };
1510
+ }
1511
+ }
1512
+ function createOAuthCredential(tokenResponse, now = Date.now()) {
1513
+ if (!tokenResponse.access_token) throw new Error("Token response did not include an access_token.");
1514
+ const expiresAt = typeof tokenResponse.expires_at === "number" ? tokenResponse.expires_at * 1e3 : typeof tokenResponse.expires_in === "number" ? now + tokenResponse.expires_in * 1e3 : void 0;
1515
+ return {
1516
+ type: "oauth",
1517
+ accessToken: tokenResponse.access_token,
1518
+ ...tokenResponse.refresh_token ? { refreshToken: tokenResponse.refresh_token } : {},
1519
+ ...expiresAt ? { expiresAt } : {}
1520
+ };
1521
+ }
1522
+ async function createLocalOAuthCallbackServer(options) {
1523
+ let resolveCode;
1524
+ let rejectCode;
1525
+ const codePromise = new Promise((resolve, reject) => {
1526
+ resolveCode = resolve;
1527
+ rejectCode = reject;
1528
+ });
1529
+ const server = http.createServer((req, res) => {
1530
+ const requestUrl = new URL(req.url ?? "/", "http://127.0.0.1");
1531
+ const code = requestUrl.searchParams.get("code");
1532
+ const state = requestUrl.searchParams.get("state");
1533
+ const error = requestUrl.searchParams.get("error");
1534
+ res.setHeader("Content-Type", "text/html; charset=utf-8");
1535
+ if (error) {
1536
+ res.statusCode = 400;
1537
+ res.end("<!doctype html><title>Sign in failed</title><p>Sign in failed. Return to the terminal.</p>");
1538
+ rejectCode(new Error(error));
1539
+ return;
1540
+ }
1541
+ if (!code || state !== options.state) {
1542
+ res.statusCode = 400;
1543
+ res.end("<!doctype html><title>Sign in failed</title><p>Invalid sign-in callback. Return to the terminal.</p>");
1544
+ rejectCode(/* @__PURE__ */ new Error("Invalid OAuth callback."));
1545
+ return;
1546
+ }
1547
+ res.end("<!doctype html><title>Signed in</title><p>Signed in. You can close this window.</p>");
1548
+ resolveCode(code);
1549
+ });
1550
+ await new Promise((resolve) => {
1551
+ server.listen(0, "127.0.0.1", resolve);
1552
+ });
1553
+ const address = server.address();
1554
+ if (!address || typeof address === "string") throw new Error("Could not start OAuth callback server.");
1555
+ return {
1556
+ redirectUri: `http://127.0.0.1:${address.port}/callback`,
1557
+ waitForCode: () => codePromise,
1558
+ close: () => new Promise((resolve) => {
1559
+ server.close(() => resolve());
1560
+ })
1561
+ };
1562
+ }
1563
+ async function buildOAuthLoginStart(options) {
1564
+ const [discovery, pkce] = await Promise.all([discoverOAuthLogin({
1565
+ baseUrl: options.baseUrl,
1566
+ clientId: options.clientId,
1567
+ fetch: options.fetch
1568
+ }), createPkcePair()]);
1569
+ return {
1570
+ authorizeUrl: buildAuthorizeUrl({
1571
+ authorizationEndpoint: discovery.authorizationEndpoint,
1572
+ clientId: discovery.clientId,
1573
+ redirectUri: options.redirectUri,
1574
+ codeChallenge: pkce.codeChallenge,
1575
+ state: options.state,
1576
+ scopes: discovery.scopes
1577
+ }),
1578
+ codeVerifier: pkce.codeVerifier,
1579
+ discovery
1580
+ };
1581
+ }
1582
+ function formatStoredCredentialSource(storedCredential) {
1583
+ if (storedCredential.credential?.type === "api_key") return `Credential source: stored api_key (${describeCredentialBackend(storedCredential.backend, storedCredential.location)}).`;
1584
+ if (storedCredential.credential?.type === "oauth") return `Credential source: stored oauth (${describeCredentialBackend(storedCredential.backend, storedCredential.location)}).`;
1585
+ if (storedCredential.authMode === "none") return `Credential source: stored no-auth mode (${describeCredentialBackend(storedCredential.backend, storedCredential.location)}).`;
1586
+ return `Credential source: stored unknown (${describeCredentialBackend(storedCredential.backend, storedCredential.location)}).`;
1587
+ }
1588
+ async function runOAuthBrowserLogin(options) {
1589
+ const state = crypto.randomBytes(16).toString("base64url");
1590
+ const callbackServer = await createLocalOAuthCallbackServer({ state });
1591
+ try {
1592
+ const { authorizeUrl, codeVerifier, discovery } = await buildOAuthLoginStart({
1593
+ baseUrl: options.baseUrl,
1594
+ redirectUri: callbackServer.redirectUri,
1595
+ state
1596
+ });
1597
+ options.log(`Opening browser for ${discovery.resourceUrl}`);
1598
+ options.log(authorizeUrl);
1599
+ let openedBrowser = false;
1600
+ try {
1601
+ openBrowser(authorizeUrl);
1602
+ openedBrowser = true;
1603
+ } catch {
1604
+ options.log("Could not open a browser automatically.");
1605
+ }
1606
+ const codePromise = callbackServer.waitForCode();
1607
+ const pastedCodePromise = !openedBrowser && process.stdin.isTTY && process.stdout.isTTY ? promptForOAuthCallback().then((value) => {
1608
+ const callback = parseOAuthCallback(value);
1609
+ if (callback.state && callback.state !== state) throw new Error("OAuth state did not match.");
1610
+ return callback.code;
1611
+ }) : void 0;
1612
+ const code = await (pastedCodePromise ? Promise.race([codePromise, pastedCodePromise]) : codePromise);
1613
+ return setStoredCredential(createOAuthCredential(await exchangeCodeForToken({
1614
+ tokenEndpoint: discovery.tokenEndpoint,
1615
+ code,
1616
+ codeVerifier,
1617
+ redirectUri: callbackServer.redirectUri,
1618
+ clientId: discovery.clientId
1619
+ })));
1620
+ } finally {
1621
+ await callbackServer.close();
1622
+ }
1623
+ }
1292
1624
  async function promptYesNo(question, defaultValue = true) {
1293
1625
  if (!process.stdin.isTTY || !process.stdout.isTTY) throw new Error("No interactive terminal available.");
1294
1626
  const rl = createInterface({
@@ -2062,23 +2394,46 @@ var InitCommand = class InitCommand extends Command {
2062
2394
  };
2063
2395
  var LoginCommand = class LoginCommand extends Command {
2064
2396
  static enableJsonFlag = false;
2065
- static summary = "Store a Stately API key for future CLI use.";
2066
- static description = "Stores a Stately API key in the OS credential store when available, with a private file fallback.";
2397
+ static summary = "Store Stately credentials for future CLI use.";
2398
+ static description = "Stores Stately credentials in the OS credential store when available, with a private file fallback.";
2067
2399
  static flags = {
2068
2400
  help: Flags.help({ char: "h" }),
2069
2401
  "api-key": Flags.string({ description: "API key to store without an interactive prompt" }),
2402
+ "base-url": Flags.string({ description: "MCP/API resource URL used for OAuth discovery" }),
2070
2403
  stdin: Flags.boolean({
2071
2404
  description: "Read the API key from standard input",
2072
2405
  default: false
2406
+ }),
2407
+ auth: Flags.string({
2408
+ description: "Authentication mode for the target server",
2409
+ options: ["none", "bearer"]
2073
2410
  })
2074
2411
  };
2075
2412
  async run() {
2076
2413
  const { flags } = await this.parse(LoginCommand);
2414
+ if (flags["api-key"] && flags.auth === "bearer") this.error("Pass either --api-key or --auth bearer, not both.");
2415
+ if (flags.auth === "none") {
2416
+ if (flags.stdin || flags["api-key"]) this.error("Pass either --auth none or an API key, not both.");
2417
+ const stored = await setStoredNoAuth();
2418
+ this.log(`Stored no-auth mode in ${describeCredentialBackend(stored.backend, stored.location)}.`);
2419
+ return;
2420
+ }
2421
+ if (!flags["api-key"] && !flags.stdin) {
2422
+ const stored = await runOAuthBrowserLogin({
2423
+ baseUrl: flags["base-url"],
2424
+ log: (message) => this.log(message)
2425
+ });
2426
+ this.log(`Stored OAuth credential in ${describeCredentialBackend(stored.backend, stored.location)}.`);
2427
+ return;
2428
+ }
2077
2429
  if (flags.stdin && flags["api-key"]) this.error("Pass either --api-key or --stdin, not both.");
2078
2430
  if (!flags["api-key"] && !flags.stdin && process.stdin.isTTY && process.stdout.isTTY) this.log(`Get or create an API key at ${STATELY_API_KEY_SETTINGS_URL}`);
2079
2431
  const apiKey = normalizeApiKey(flags["api-key"] ?? (!process.stdin.isTTY || flags.stdin ? await readApiKeyFromStdin() : await promptForApiKey()));
2080
2432
  if (!apiKey) this.error(`API key cannot be empty.\nGet or create an API key at ${STATELY_API_KEY_SETTINGS_URL}`);
2081
- const stored = await setStoredApiKey(apiKey);
2433
+ const stored = flags["api-key"] || flags.stdin ? await setStoredApiKey(apiKey) : await setStoredCredential({
2434
+ type: "api_key",
2435
+ token: apiKey
2436
+ });
2082
2437
  this.log(`Stored API key in ${describeCredentialBackend(stored.backend, stored.location)}.`);
2083
2438
  }
2084
2439
  };
@@ -2102,15 +2457,20 @@ var WhoamiCommand = class extends Command {
2102
2457
  static description = "Reports whether the CLI would use a flag, environment variable, or stored credential.";
2103
2458
  static flags = { help: Flags.help({ char: "h" }) };
2104
2459
  async run() {
2105
- const envApiKey = getEnvApiKey();
2106
- const storedApiKey = await getStoredApiKey();
2107
- if (envApiKey) {
2108
- this.log(`API key source: environment (${envApiKey.variable}).`);
2460
+ const envCredential = getEnvCredential();
2461
+ const storedCredential = await getStoredCredential();
2462
+ const storedApiKey = storedCredential?.credential?.type === "api_key" ? {
2463
+ apiKey: storedCredential.credential.token,
2464
+ backend: storedCredential.backend,
2465
+ location: storedCredential.location
2466
+ } : void 0;
2467
+ if (envCredential) {
2468
+ this.log(`Credential source: environment ${envCredential.credential.type} (${envCredential.variable}).`);
2109
2469
  if (storedApiKey) this.log(`Stored credential also available in ${describeCredentialBackend(storedApiKey.backend, storedApiKey.location)}.`);
2110
2470
  return;
2111
2471
  }
2112
- if (storedApiKey) {
2113
- this.log(`API key source: stored credential (${describeCredentialBackend(storedApiKey.backend, storedApiKey.location)}).`);
2472
+ if (storedCredential) {
2473
+ this.log(formatStoredCredentialSource(storedCredential));
2114
2474
  return;
2115
2475
  }
2116
2476
  this.log(getMissingApiKeyMessage());
@@ -2149,4 +2509,4 @@ function isDirectExecution() {
2149
2509
  if (isDirectExecution()) run$1();
2150
2510
 
2151
2511
  //#endregion
2152
- export { formatMissingNewMachineDirMessage as a, inferInitProjectName as c, resolveApiKey as d, resolveConfiguredPullTargets as f, createStatelyProjectConfig as h, discoverRemoteProjectMachineTargets as i, initProject as l, scanProjectSources as m, classifyPushCandidates as n, formatPlanSummary as o, run$1 as p, discoverLinkedPullTargets as r, getEnvApiKey as s, COMMANDS as t, isFileGitDirty as u };
2512
+ export { run$1 as _, discoverOAuthLogin as a, formatPlanSummary as c, getEnvCredential as d, inferInitProjectName as f, resolveConfiguredPullTargets as g, resolveApiKey as h, discoverLinkedPullTargets as i, formatStoredCredentialSource as l, isFileGitDirty as m, buildOAuthLoginStart as n, discoverRemoteProjectMachineTargets as o, initProject as p, classifyPushCandidates as r, formatMissingNewMachineDirMessage as s, COMMANDS as t, getEnvApiKey as u, scanProjectSources as v, createStatelyProjectConfig as y };
package/dist/index.d.mts CHANGED
@@ -1,8 +1,26 @@
1
1
  import { Command } from "@oclif/core";
2
- import { ConnectedRepo, CreateProjectInput, ProjectData, StudioClient } from "@statelyai/sdk";
2
+ import { ConnectedRepo, CreateProjectInput, OAuthAuthorizationServerMetadata, OAuthProtectedResourceMetadata, ProjectData, StudioClient } from "@statelyai/sdk";
3
3
  import { SyncPlan } from "@statelyai/sdk/sync";
4
4
  import * as _oclif_core_interfaces0 from "@oclif/core/interfaces";
5
5
 
6
+ //#region src/credentials.d.ts
7
+ type StatelyCredential = {
8
+ type: 'api_key';
9
+ token: string;
10
+ } | {
11
+ type: 'oauth';
12
+ accessToken: string;
13
+ refreshToken?: string;
14
+ expiresAt?: number;
15
+ };
16
+ type CredentialBackend = 'file' | 'linux-secret-tool' | 'macos-keychain';
17
+ type StoredCredential = {
18
+ credential?: StatelyCredential;
19
+ authMode?: 'none';
20
+ backend: CredentialBackend;
21
+ location: string;
22
+ };
23
+ //#endregion
6
24
  //#region src/projectConfig.d.ts
7
25
  interface StatelySourceConfig {
8
26
  include: string[];
@@ -90,10 +108,40 @@ type ApiKeyResolution = {
90
108
  };
91
109
  declare function getEnvApiKey(): {
92
110
  apiKey: string;
93
- variable: 'NEXT_PUBLIC_STATELY_API_KEY' | 'STATELY_API_KEY';
111
+ variable: 'NEXT_PUBLIC_STATELY_API_KEY' | 'STATELY_API_KEY' | 'STATELY_ACCESS_TOKEN';
112
+ } | undefined;
113
+ declare function getEnvCredential(): {
114
+ credential: StatelyCredential;
115
+ variable: 'NEXT_PUBLIC_STATELY_API_KEY' | 'STATELY_API_KEY' | 'STATELY_ACCESS_TOKEN';
94
116
  } | undefined;
95
117
  declare function resolveApiKey(explicitApiKey?: string): Promise<ApiKeyResolution>;
118
+ interface OAuthLoginDiscovery {
119
+ authorizationServer: OAuthAuthorizationServerMetadata;
120
+ authorizationEndpoint: string;
121
+ clientId: string;
122
+ protectedResource: OAuthProtectedResourceMetadata;
123
+ resourceUrl: string;
124
+ scopes: string[];
125
+ tokenEndpoint: string;
126
+ }
127
+ declare function discoverOAuthLogin(options?: {
128
+ baseUrl?: string;
129
+ clientId?: string;
130
+ fetch?: typeof fetch;
131
+ }): Promise<OAuthLoginDiscovery>;
96
132
  declare function inferInitProjectName(cwd: string, repo?: ConnectedRepo): string;
133
+ declare function buildOAuthLoginStart(options: {
134
+ baseUrl?: string;
135
+ clientId?: string;
136
+ fetch?: typeof fetch;
137
+ redirectUri: string;
138
+ state: string;
139
+ }): Promise<{
140
+ authorizeUrl: string;
141
+ codeVerifier: string;
142
+ discovery: OAuthLoginDiscovery;
143
+ }>;
144
+ declare function formatStoredCredentialSource(storedCredential: StoredCredential): string;
97
145
  declare function formatMissingNewMachineDirMessage(options: {
98
146
  config: Pick<StatelyProjectConfig, 'include' | 'newMachinesDir'>;
99
147
  remoteMachineCount: number;
@@ -230,7 +278,9 @@ declare class LoginCommand extends Command {
230
278
  static flags: {
231
279
  help: _oclif_core_interfaces0.BooleanFlag<void>;
232
280
  'api-key': _oclif_core_interfaces0.OptionFlag<string | undefined, _oclif_core_interfaces0.CustomOptions>;
281
+ 'base-url': _oclif_core_interfaces0.OptionFlag<string | undefined, _oclif_core_interfaces0.CustomOptions>;
233
282
  stdin: _oclif_core_interfaces0.BooleanFlag<boolean>;
283
+ auth: _oclif_core_interfaces0.OptionFlag<string | undefined, _oclif_core_interfaces0.CustomOptions>;
234
284
  };
235
285
  run(): Promise<void>;
236
286
  }
@@ -265,4 +315,4 @@ declare const COMMANDS: {
265
315
  };
266
316
  declare function run(argv?: any, entryUrl?: string): Promise<void>;
267
317
  //#endregion
268
- export { ApiKeyResolution, COMMANDS, InitProjectOptions, InitProjectResult, LinkedPullTarget, PushCandidateClassification, RemoteProjectMachineTarget, ResolvedConfiguredPullTargets, ScanProjectSourcesOptions, classifyPushCandidates, createStatelyProjectConfig, discoverLinkedPullTargets, discoverRemoteProjectMachineTargets, formatMissingNewMachineDirMessage, formatPlanSummary, getEnvApiKey, inferInitProjectName, initProject, isFileGitDirty, resolveApiKey, resolveConfiguredPullTargets, run, scanProjectSources };
318
+ export { ApiKeyResolution, COMMANDS, InitProjectOptions, InitProjectResult, LinkedPullTarget, OAuthLoginDiscovery, PushCandidateClassification, RemoteProjectMachineTarget, ResolvedConfiguredPullTargets, ScanProjectSourcesOptions, buildOAuthLoginStart, classifyPushCandidates, createStatelyProjectConfig, discoverLinkedPullTargets, discoverOAuthLogin, discoverRemoteProjectMachineTargets, formatMissingNewMachineDirMessage, formatPlanSummary, formatStoredCredentialSource, getEnvApiKey, getEnvCredential, inferInitProjectName, initProject, isFileGitDirty, resolveApiKey, resolveConfiguredPullTargets, run, scanProjectSources };
package/dist/index.mjs CHANGED
@@ -1,3 +1,3 @@
1
- import { a as formatMissingNewMachineDirMessage, c as inferInitProjectName, d as resolveApiKey, f as resolveConfiguredPullTargets, h as createStatelyProjectConfig, i as discoverRemoteProjectMachineTargets, l as initProject, m as scanProjectSources, n as classifyPushCandidates, o as formatPlanSummary, p as run, r as discoverLinkedPullTargets, s as getEnvApiKey, t as COMMANDS, u as isFileGitDirty } from "./cli-DB0A-O4Y.mjs";
1
+ import { _ as run, a as discoverOAuthLogin, c as formatPlanSummary, d as getEnvCredential, f as inferInitProjectName, g as resolveConfiguredPullTargets, h as resolveApiKey, i as discoverLinkedPullTargets, l as formatStoredCredentialSource, m as isFileGitDirty, n as buildOAuthLoginStart, o as discoverRemoteProjectMachineTargets, p as initProject, r as classifyPushCandidates, s as formatMissingNewMachineDirMessage, t as COMMANDS, u as getEnvApiKey, v as scanProjectSources, y as createStatelyProjectConfig } from "./cli-Buz_BC3I.mjs";
2
2
 
3
- export { COMMANDS, classifyPushCandidates, createStatelyProjectConfig, discoverLinkedPullTargets, discoverRemoteProjectMachineTargets, formatMissingNewMachineDirMessage, formatPlanSummary, getEnvApiKey, inferInitProjectName, initProject, isFileGitDirty, resolveApiKey, resolveConfiguredPullTargets, run, scanProjectSources };
3
+ export { COMMANDS, buildOAuthLoginStart, classifyPushCandidates, createStatelyProjectConfig, discoverLinkedPullTargets, discoverOAuthLogin, discoverRemoteProjectMachineTargets, formatMissingNewMachineDirMessage, formatPlanSummary, formatStoredCredentialSource, getEnvApiKey, getEnvCredential, inferInitProjectName, initProject, isFileGitDirty, resolveApiKey, resolveConfiguredPullTargets, run, scanProjectSources };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "statelyai",
3
- "version": "0.5.3",
3
+ "version": "0.6.0",
4
4
  "description": "Command-line tools for Stately",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -21,7 +21,7 @@
21
21
  },
22
22
  "dependencies": {
23
23
  "@oclif/core": "^4.10.3",
24
- "@statelyai/sdk": "0.10.2"
24
+ "@statelyai/sdk": "0.12.0"
25
25
  },
26
26
  "devDependencies": {
27
27
  "tsdown": "0.21.0-beta.2",