@stigmer/cli 3.13.0 → 3.14.0-dev.20260910084630

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 (74) hide show
  1. package/auth/login.d.ts.map +1 -1
  2. package/auth/login.js +30 -5
  3. package/auth/login.js.map +1 -1
  4. package/auth/token.d.ts +10 -5
  5. package/auth/token.d.ts.map +1 -1
  6. package/auth/token.js +30 -21
  7. package/auth/token.js.map +1 -1
  8. package/commands/apikey/index.d.ts.map +1 -1
  9. package/commands/apikey/index.js +11 -5
  10. package/commands/apikey/index.js.map +1 -1
  11. package/commands/auth/index.d.ts.map +1 -1
  12. package/commands/auth/index.js +19 -8
  13. package/commands/auth/index.js.map +1 -1
  14. package/commands/config/backend.d.ts.map +1 -1
  15. package/commands/config/backend.js +134 -12
  16. package/commands/config/backend.js.map +1 -1
  17. package/commands/connect.d.ts.map +1 -1
  18. package/commands/connect.js +6 -3
  19. package/commands/connect.js.map +1 -1
  20. package/commands/share.d.ts.map +1 -1
  21. package/commands/share.js +8 -6
  22. package/commands/share.js.map +1 -1
  23. package/config/config.d.ts +56 -7
  24. package/config/config.d.ts.map +1 -1
  25. package/config/config.js +115 -16
  26. package/config/config.js.map +1 -1
  27. package/config/index.d.ts +1 -1
  28. package/config/index.d.ts.map +1 -1
  29. package/config/index.js +1 -1
  30. package/config/index.js.map +1 -1
  31. package/config/keys.d.ts +1 -1
  32. package/config/keys.d.ts.map +1 -1
  33. package/config/keys.js +29 -6
  34. package/config/keys.js.map +1 -1
  35. package/config/resolve.d.ts +28 -20
  36. package/config/resolve.d.ts.map +1 -1
  37. package/config/resolve.js +74 -36
  38. package/config/resolve.js.map +1 -1
  39. package/local/seedpack/content.d.ts +3 -3
  40. package/local/seedpack/content.d.ts.map +1 -1
  41. package/local/seedpack/content.js +19 -13
  42. package/local/seedpack/content.js.map +1 -1
  43. package/package.json +5 -5
  44. package/resources/connect/connect.d.ts +4 -3
  45. package/resources/connect/connect.d.ts.map +1 -1
  46. package/resources/connect/connect.js +17 -5
  47. package/resources/connect/connect.js.map +1 -1
  48. package/resources/connect/oauth.d.ts +5 -2
  49. package/resources/connect/oauth.d.ts.map +1 -1
  50. package/resources/connect/oauth.js +5 -4
  51. package/resources/connect/oauth.js.map +1 -1
  52. package/src/auth/login.ts +54 -9
  53. package/src/auth/token.test.ts +69 -13
  54. package/src/auth/token.ts +46 -22
  55. package/src/client/client.test.ts +10 -2
  56. package/src/commands/apikey/index.ts +42 -11
  57. package/src/commands/auth/index.ts +52 -14
  58. package/src/commands/config/backend.test.ts +122 -0
  59. package/src/commands/config/backend.ts +195 -14
  60. package/src/commands/connect.test.ts +22 -4
  61. package/src/commands/connect.ts +37 -13
  62. package/src/commands/share.ts +36 -16
  63. package/src/config/config.test.ts +62 -6
  64. package/src/config/config.ts +165 -24
  65. package/src/config/index.ts +6 -0
  66. package/src/config/keys.test.ts +27 -4
  67. package/src/config/keys.ts +46 -9
  68. package/src/config/resolve.test.ts +135 -34
  69. package/src/config/resolve.ts +83 -36
  70. package/src/local/seedpack/content.ts +52 -21
  71. package/src/resources/connect/connect.integration.test.ts +79 -26
  72. package/src/resources/connect/connect.ts +72 -19
  73. package/src/resources/connect/oauth.test.ts +38 -12
  74. package/src/resources/connect/oauth.ts +27 -11
package/src/auth/login.ts CHANGED
@@ -10,7 +10,12 @@
10
10
  // Progress goes to stderr; the command layer renders the final result.
11
11
 
12
12
  import open from "open";
13
- import { load, save } from "../config/index.js";
13
+ import {
14
+ CLOUD_BACKEND_NAME,
15
+ activeBackend,
16
+ load,
17
+ save,
18
+ } from "../config/index.js";
14
19
  import { CliExitError, ExitCode } from "../errors/index.js";
15
20
  import { log } from "../logger.js";
16
21
  import { buildAuthorizeUrl, CALLBACK_PATH, CALLBACK_PORT } from "./auth0.js";
@@ -21,6 +26,27 @@ import { exchangeCode } from "./token.js";
21
26
  const LOGIN_TIMEOUT_MS = 5 * 60 * 1000;
22
27
 
23
28
  export async function login(): Promise<void> {
29
+ // Browser PKCE targets Stigmer Cloud only: the Auth0 coordinates are
30
+ // compiled in. From the local backend, login lands on (creating if
31
+ // needed) the reserved "cloud" entry — today's behavior in the named
32
+ // model. On a selfhost backend the flow refuses with the self-host
33
+ // credential story (API tokens) instead of silently writing cloud
34
+ // credentials the active backend would never send.
35
+ const preflight = load();
36
+ const { name, entry } = activeBackend(preflight);
37
+ if (entry?.type === "selfhost") {
38
+ throw new CliExitError(
39
+ `backend "${name}" is self-hosted — browser login targets Stigmer Cloud only`,
40
+ ExitCode.Usage,
41
+ [
42
+ "Authenticate a self-hosted backend with an API token:",
43
+ " stigmer apikey create --name cli",
44
+ ` stigmer config backend add ${name} --endpoint <host:port> --api-key <key>`,
45
+ "or switch first: stigmer config backend use cloud",
46
+ ],
47
+ );
48
+ }
49
+
24
50
  const verifier = generateVerifier();
25
51
  const codeChallenge = challengeS256(verifier);
26
52
  const state = generateState();
@@ -30,30 +56,49 @@ export async function login(): Promise<void> {
30
56
  try {
31
57
  const authUrl = buildAuthorizeUrl({ state, codeChallenge });
32
58
  process.stderr.write("Opening browser for authentication...\n");
33
- process.stderr.write(`If the browser doesn't open automatically, visit:\n${authUrl}\n\n`);
59
+ process.stderr.write(
60
+ `If the browser doesn't open automatically, visit:\n${authUrl}\n\n`,
61
+ );
34
62
 
35
63
  try {
36
64
  await open(authUrl);
37
65
  } catch (err) {
38
- log.debug(`failed to open browser automatically: ${(err as Error).message}`);
39
- process.stderr.write("Could not open the browser automatically. Please open the URL above.\n");
66
+ log.debug(
67
+ `failed to open browser automatically: ${(err as Error).message}`,
68
+ );
69
+ process.stderr.write(
70
+ "Could not open the browser automatically. Please open the URL above.\n",
71
+ );
40
72
  }
41
73
 
42
74
  const callback = await server.waitForCallback(LOGIN_TIMEOUT_MS);
43
75
  if (callback.state !== state) {
44
- throw new CliExitError("OAuth state mismatch — possible CSRF attack, aborting login", ExitCode.Auth);
76
+ throw new CliExitError(
77
+ "OAuth state mismatch — possible CSRF attack, aborting login",
78
+ ExitCode.Auth,
79
+ );
45
80
  }
46
81
 
47
- const tokens = await exchangeCode({ code: callback.code, codeVerifier: verifier });
82
+ const tokens = await exchangeCode({
83
+ code: callback.code,
84
+ codeVerifier: verifier,
85
+ });
48
86
 
49
87
  const config = load();
50
- config.backend.type = "cloud";
51
- config.backend.cloud = {
52
- ...config.backend.cloud,
88
+ const active = activeBackend(config);
89
+ // A cloud-type backend keeps its own login; anything else (the local
90
+ // daemon) lands on the reserved "cloud" entry and switches to it.
91
+ const targetName =
92
+ active.entry?.type === "cloud" ? active.name : CLOUD_BACKEND_NAME;
93
+ const backends = (config.backends ??= {});
94
+ backends[targetName] = {
95
+ ...(backends[targetName] ?? { type: "cloud" }),
96
+ type: "cloud",
53
97
  token: tokens.accessToken,
54
98
  refresh_token: tokens.refreshToken,
55
99
  token_expiry: tokens.expiresAt,
56
100
  };
101
+ config.current_backend = targetName;
57
102
  save(config);
58
103
  } finally {
59
104
  await server.shutdown();
@@ -2,7 +2,7 @@ import { mkdtempSync } from "node:fs";
2
2
  import { tmpdir } from "node:os";
3
3
  import { join } from "node:path";
4
4
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
5
- import type { Config } from "../config/index.js";
5
+ import type { Config, NamedBackendConfig } from "../config/index.js";
6
6
  import { createRefreshingTokenProvider } from "./token.js";
7
7
 
8
8
  // The provider persists refreshed tokens via config.save(), which writes to
@@ -10,11 +10,35 @@ import { createRefreshingTokenProvider } from "./token.js";
10
10
  // the developer's real config.
11
11
  let originalHome: string | undefined;
12
12
 
13
- function cloudConfig(overrides: Partial<NonNullable<Config["backend"]["cloud"]>>): Config {
14
- return { backend: { type: "cloud", cloud: { endpoint: "api.stigmer.ai:443", ...overrides } } };
13
+ function cloudConfig(overrides: Partial<NamedBackendConfig>): Config {
14
+ return {
15
+ backend: { type: "cloud" },
16
+ backends: {
17
+ cloud: { type: "cloud", endpoint: "api.stigmer.ai:443", ...overrides },
18
+ },
19
+ current_backend: "cloud",
20
+ };
15
21
  }
16
22
 
17
- function mockFetch(response: { ok: boolean; status?: number; body: unknown }): void {
23
+ function selfhostConfig(overrides: Partial<NamedBackendConfig> = {}): Config {
24
+ return {
25
+ backend: { type: "cloud" },
26
+ backends: {
27
+ staging: {
28
+ type: "selfhost",
29
+ endpoint: "stigmer.example.com:7234",
30
+ ...overrides,
31
+ },
32
+ },
33
+ current_backend: "staging",
34
+ };
35
+ }
36
+
37
+ function mockFetch(response: {
38
+ ok: boolean;
39
+ status?: number;
40
+ body: unknown;
41
+ }): void {
18
42
  vi.stubGlobal(
19
43
  "fetch",
20
44
  vi.fn(async () => ({
@@ -42,7 +66,9 @@ describe("createRefreshingTokenProvider", () => {
42
66
  process.env.STIGMER_API_KEY = "env-key";
43
67
  const fetchSpy = vi.fn();
44
68
  vi.stubGlobal("fetch", fetchSpy);
45
- const provider = createRefreshingTokenProvider(cloudConfig({ token: "stale", token_expiry: past() }));
69
+ const provider = createRefreshingTokenProvider(
70
+ cloudConfig({ token: "stale", token_expiry: past() }),
71
+ );
46
72
  expect(await provider()).toBe("env-key");
47
73
  expect(fetchSpy).not.toHaveBeenCalled();
48
74
  });
@@ -50,24 +76,52 @@ describe("createRefreshingTokenProvider", () => {
50
76
  it("returns a non-expired token without refreshing", async () => {
51
77
  const fetchSpy = vi.fn();
52
78
  vi.stubGlobal("fetch", fetchSpy);
53
- const provider = createRefreshingTokenProvider(cloudConfig({ token: "good", token_expiry: future() }));
79
+ const provider = createRefreshingTokenProvider(
80
+ cloudConfig({ token: "good", token_expiry: future() }),
81
+ );
54
82
  expect(await provider()).toBe("good");
55
83
  expect(fetchSpy).not.toHaveBeenCalled();
56
84
  });
57
85
 
58
- it("refreshes an expired token and persists the new one", async () => {
59
- mockFetch({ ok: true, body: { access_token: "fresh", refresh_token: "r2", expires_in: 3600 } });
60
- const config = cloudConfig({ token: "expired", token_expiry: past(), refresh_token: "r1" });
86
+ it("refreshes an expired token and persists the new one into the entry", async () => {
87
+ mockFetch({
88
+ ok: true,
89
+ body: { access_token: "fresh", refresh_token: "r2", expires_in: 3600 },
90
+ });
91
+ const config = cloudConfig({
92
+ token: "expired",
93
+ token_expiry: past(),
94
+ refresh_token: "r1",
95
+ });
61
96
  const provider = createRefreshingTokenProvider(config);
62
97
  expect(await provider()).toBe("fresh");
63
- expect(config.backend.cloud?.token).toBe("fresh");
64
- expect(config.backend.cloud?.refresh_token).toBe("r2");
98
+ expect(config.backends?.["cloud"]?.token).toBe("fresh");
99
+ expect(config.backends?.["cloud"]?.refresh_token).toBe("r2");
100
+ });
101
+
102
+ it("returns the selfhost entry's api_key without any refresh machinery", async () => {
103
+ const fetchSpy = vi.fn();
104
+ vi.stubGlobal("fetch", fetchSpy);
105
+ const provider = createRefreshingTokenProvider(
106
+ selfhostConfig({ api_key: "stk_stored" }),
107
+ );
108
+ expect(await provider()).toBe("stk_stored");
109
+ expect(fetchSpy).not.toHaveBeenCalled();
110
+ });
111
+
112
+ it("returns null on a selfhost backend with no stored key", async () => {
113
+ const provider = createRefreshingTokenProvider(selfhostConfig());
114
+ expect(await provider()).toBeNull();
65
115
  });
66
116
 
67
117
  it("falls back to the existing token when refresh fails", async () => {
68
118
  mockFetch({ ok: false, status: 401, body: { error: "invalid_grant" } });
69
119
  const provider = createRefreshingTokenProvider(
70
- cloudConfig({ token: "expired", token_expiry: past(), refresh_token: "r1" }),
120
+ cloudConfig({
121
+ token: "expired",
122
+ token_expiry: past(),
123
+ refresh_token: "r1",
124
+ }),
71
125
  );
72
126
  expect(await provider()).toBe("expired");
73
127
  });
@@ -80,7 +134,9 @@ describe("createRefreshingTokenProvider", () => {
80
134
  it("treats a token without an expiry as valid (legacy)", async () => {
81
135
  const fetchSpy = vi.fn();
82
136
  vi.stubGlobal("fetch", fetchSpy);
83
- const provider = createRefreshingTokenProvider(cloudConfig({ token: "legacy" }));
137
+ const provider = createRefreshingTokenProvider(
138
+ cloudConfig({ token: "legacy" }),
139
+ );
84
140
  expect(await provider()).toBe("legacy");
85
141
  expect(fetchSpy).not.toHaveBeenCalled();
86
142
  });
package/src/auth/token.ts CHANGED
@@ -2,7 +2,12 @@
2
2
  // expired access token, and expose a token provider that refreshes silently.
3
3
 
4
4
  import type { TokenProvider } from "@stigmer/sdk";
5
- import { type Config, save } from "../config/index.js";
5
+ import {
6
+ type Config,
7
+ type NamedBackendConfig,
8
+ activeBackend,
9
+ save,
10
+ } from "../config/index.js";
6
11
  import { CliExitError, ExitCode } from "../errors/index.js";
7
12
  import { CLIENT_ID, callbackUrl, TOKEN_URL } from "./auth0.js";
8
13
 
@@ -28,7 +33,10 @@ interface Auth0TokenResponse {
28
33
  }
29
34
 
30
35
  /** Exchange a PKCE authorization code for an access (+refresh) token. */
31
- export async function exchangeCode(params: { code: string; codeVerifier: string }): Promise<TokenSet> {
36
+ export async function exchangeCode(params: {
37
+ code: string;
38
+ codeVerifier: string;
39
+ }): Promise<TokenSet> {
32
40
  return postToken({
33
41
  grant_type: "authorization_code",
34
42
  client_id: CLIENT_ID,
@@ -39,7 +47,9 @@ export async function exchangeCode(params: { code: string; codeVerifier: string
39
47
  }
40
48
 
41
49
  /** Exchange a refresh token for a fresh access token. */
42
- export async function refreshAccessToken(refreshToken: string): Promise<TokenSet> {
50
+ export async function refreshAccessToken(
51
+ refreshToken: string,
52
+ ): Promise<TokenSet> {
43
53
  return postToken({
44
54
  grant_type: "refresh_token",
45
55
  client_id: CLIENT_ID,
@@ -50,12 +60,17 @@ export async function refreshAccessToken(refreshToken: string): Promise<TokenSet
50
60
  /**
51
61
  * A token provider for the backend client that keeps the access token fresh.
52
62
  *
53
- * Precedence and behavior, evaluated per request:
63
+ * Precedence and behavior, evaluated per request against the ACTIVE named
64
+ * backend:
54
65
  * 1. STIGMER_API_KEY (env / bridged --api-key) wins and is never refreshed.
55
- * 2. A non-expired access token is returned as-is.
56
- * 3. An expired token with a refresh token is refreshed; the new token,
57
- * rotated refresh token, and expiry are persisted, then returned.
58
- * 4. Otherwise the (possibly stale) token or null is returned, letting the
66
+ * 2. selfhost backends return their stored api_key (no refresh machinery —
67
+ * server-minted `stk_` tokens are opaque and live until deleted or
68
+ * expired server-side); local returns null (no auth).
69
+ * 3. A cloud backend's non-expired access token is returned as-is.
70
+ * 4. An expired token with a refresh token is refreshed; the new token,
71
+ * rotated refresh token, and expiry are persisted INTO THE ENTRY, then
72
+ * returned.
73
+ * 5. Otherwise the (possibly stale) token or null is returned, letting the
59
74
  * server reject it so the error maps to a clear "please log in" exit.
60
75
  *
61
76
  * Closes over `config` so a refresh updates the same object the rest of the
@@ -66,30 +81,38 @@ export function createRefreshingTokenProvider(config: Config): TokenProvider {
66
81
  const envKey = process.env.STIGMER_API_KEY;
67
82
  if (envKey !== undefined && envKey !== "") return envKey;
68
83
 
69
- const cloud = config.backend.cloud;
70
- if (cloud?.token === undefined || cloud.token === "") {
71
- return cloud?.refresh_token ? refreshAndPersist(config, cloud.refresh_token) : null;
84
+ const { entry } = activeBackend(config);
85
+ if (entry === undefined) return null;
86
+ if (entry.type === "selfhost") return entry.api_key ?? null;
87
+
88
+ if (entry.token === undefined || entry.token === "") {
89
+ return entry.refresh_token
90
+ ? refreshAndPersist(config, entry, entry.refresh_token)
91
+ : null;
72
92
  }
73
- if (!isExpired(cloud.token_expiry)) return cloud.token;
74
- if (cloud.refresh_token === undefined || cloud.refresh_token === "") return cloud.token;
75
- return refreshAndPersist(config, cloud.refresh_token);
93
+ if (!isExpired(entry.token_expiry)) return entry.token;
94
+ if (entry.refresh_token === undefined || entry.refresh_token === "")
95
+ return entry.token;
96
+ return refreshAndPersist(config, entry, entry.refresh_token);
76
97
  };
77
98
  }
78
99
 
79
- async function refreshAndPersist(config: Config, refreshToken: string): Promise<string | null> {
100
+ async function refreshAndPersist(
101
+ config: Config,
102
+ entry: NamedBackendConfig,
103
+ refreshToken: string,
104
+ ): Promise<string | null> {
80
105
  let next: TokenSet;
81
106
  try {
82
107
  next = await refreshAccessToken(refreshToken);
83
108
  } catch {
84
109
  // Refresh failed (revoked/expired refresh token). Fall back to the current
85
110
  // token if any; the server will reject it and the user is prompted to log in.
86
- return config.backend.cloud?.token ?? null;
111
+ return entry.token ?? null;
87
112
  }
88
- const cloud = config.backend.cloud ?? {};
89
- cloud.token = next.accessToken;
90
- if (next.refreshToken !== undefined) cloud.refresh_token = next.refreshToken;
91
- cloud.token_expiry = next.expiresAt;
92
- config.backend.cloud = cloud;
113
+ entry.token = next.accessToken;
114
+ if (next.refreshToken !== undefined) entry.refresh_token = next.refreshToken;
115
+ entry.token_expiry = next.expiresAt;
93
116
  save(config);
94
117
  return next.accessToken;
95
118
  }
@@ -120,7 +143,8 @@ async function postToken(form: Record<string, string>): Promise<TokenSet> {
120
143
 
121
144
  const body = (await response.json().catch(() => ({}))) as Auth0TokenResponse;
122
145
  if (!response.ok || body.access_token === undefined) {
123
- const detail = body.error_description ?? body.error ?? `HTTP ${response.status}`;
146
+ const detail =
147
+ body.error_description ?? body.error ?? `HTTP ${response.status}`;
124
148
  throw new CliExitError(`authentication failed: ${detail}`, ExitCode.Auth);
125
149
  }
126
150
 
@@ -4,7 +4,13 @@ import type { Config } from "../config/index.js";
4
4
  import { createBackendClient } from "./client.js";
5
5
 
6
6
  function cloud(token?: string): Config {
7
- return { backend: { type: "cloud", cloud: { endpoint: "api.stigmer.ai:443", token } } };
7
+ return {
8
+ backend: { type: "cloud" },
9
+ backends: {
10
+ cloud: { type: "cloud", endpoint: "api.stigmer.ai:443", token },
11
+ },
12
+ current_backend: "cloud",
13
+ };
8
14
  }
9
15
 
10
16
  afterEach(() => {
@@ -19,7 +25,9 @@ describe("createBackendClient — endpoint normalization", () => {
19
25
  });
20
26
 
21
27
  it("normalizes the local endpoint to a plaintext base URL", () => {
22
- const client = createBackendClient({ config: { backend: { type: "local" } } });
28
+ const client = createBackendClient({
29
+ config: { backend: { type: "local" } },
30
+ });
23
31
  expect(client.stigmer.baseUrl).toBe("http://localhost:7234");
24
32
  });
25
33
  });
@@ -6,11 +6,22 @@
6
6
  import { createHash } from "node:crypto";
7
7
  import { create } from "@bufbuild/protobuf";
8
8
  import { timestampDate } from "@bufbuild/protobuf/wkt";
9
- import { type ApiKey, ApiKeySchema } from "@stigmer/protos/ai/stigmer/iam/apikey/v1/api_pb";
9
+ import {
10
+ type ApiKey,
11
+ ApiKeySchema,
12
+ } from "@stigmer/protos/ai/stigmer/iam/apikey/v1/api_pb";
10
13
  import { ApiKeyHashSchema } from "@stigmer/protos/ai/stigmer/iam/apikey/v1/io_pb";
11
14
  import type { Command } from "commander";
12
- import { ensureAuthenticated, resolveContextOrganization } from "../../config/index.js";
13
- import { type OutputFlags, type OutputFormat, renderProtoJson, renderProtoYaml } from "../../output/index.js";
15
+ import {
16
+ ensureAuthenticated,
17
+ resolveContextOrganization,
18
+ } from "../../config/index.js";
19
+ import {
20
+ type OutputFlags,
21
+ type OutputFormat,
22
+ renderProtoJson,
23
+ renderProtoYaml,
24
+ } from "../../output/index.js";
14
25
  import { addReadFlags, readFormat } from "../shared.js";
15
26
  import { parseExpiration } from "./duration.js";
16
27
 
@@ -23,7 +34,9 @@ interface ApiKeyCreateFlags extends OutputFlags {
23
34
  }
24
35
 
25
36
  export function registerApiKey(program: Command): void {
26
- const apikey = program.command("apikey").description("manage API keys for Stigmer Cloud authentication");
37
+ const apikey = program
38
+ .command("apikey")
39
+ .description("manage API keys for Stigmer Cloud authentication");
27
40
 
28
41
  const create = apikey
29
42
  .command("create")
@@ -71,14 +84,23 @@ async function runCreate(options: ApiKeyCreateFlags): Promise<void> {
71
84
  process.stdout.write(renderCreatedBanner(created));
72
85
  }
73
86
 
74
- async function runFingerprint(rawKey: string, options: OutputFlags): Promise<void> {
75
- const hash = createHash("sha256").update(rawKey).digest("hex");
87
+ async function runFingerprint(
88
+ rawKey: string,
89
+ options: OutputFlags,
90
+ ): Promise<void> {
91
+ // Base64URL without padding — the server's storage encoding (the Java
92
+ // ApiKeyHasher and the TS keymaterial module agree). This was hex until
93
+ // O3, which meant the computed hash could never match a stored key_hash
94
+ // and the lookup below always answered NotFound (gate ruling Q7).
95
+ const hash = createHash("sha256").update(rawKey).digest("base64url");
76
96
 
77
97
  const { connectBackend } = await import("../../backend.js");
78
98
  const client = connectBackend();
79
99
  ensureAuthenticated(client.config);
80
100
 
81
- const key = await client.stigmer.apiKey.getByKeyHash(create(ApiKeyHashSchema, { value: hash }));
101
+ const key = await client.stigmer.apiKey.getByKeyHash(
102
+ create(ApiKeyHashSchema, { value: hash }),
103
+ );
82
104
  renderApiKey(key, readFormat(options));
83
105
  }
84
106
 
@@ -101,7 +123,8 @@ function renderApiKey(key: ApiKey, format: OutputFormat): void {
101
123
  }
102
124
  const lines = [`API Key: ${key.metadata?.id ?? ""}`, ""];
103
125
  if (key.metadata?.name) lines.push(` Name: ${key.metadata.name}`);
104
- if (key.spec?.fingerprint) lines.push(` Fingerprint: ***${key.spec.fingerprint}`);
126
+ if (key.spec?.fingerprint)
127
+ lines.push(` Fingerprint: ***${key.spec.fingerprint}`);
105
128
  lines.push(` Expires: ${formatExpiry(key)}`);
106
129
  process.stdout.write(`${lines.join("\n")}\n`);
107
130
  }
@@ -121,13 +144,21 @@ function renderCreatedBanner(key: ApiKey): string {
121
144
  ` ID: ${key.metadata?.id ?? ""}`,
122
145
  ];
123
146
  if (key.metadata?.name) lines.push(` Name: ${key.metadata.name}`);
124
- if (key.spec?.fingerprint) lines.push(` Fingerprint: ***${key.spec.fingerprint}`);
125
- lines.push(` Expires: ${formatExpiry(key)}`, "", "Usage:", ` export STIGMER_API_KEY='${key.spec?.keyHash ?? ""}'`, "");
147
+ if (key.spec?.fingerprint)
148
+ lines.push(` Fingerprint: ***${key.spec.fingerprint}`);
149
+ lines.push(
150
+ ` Expires: ${formatExpiry(key)}`,
151
+ "",
152
+ "Usage:",
153
+ ` export STIGMER_API_KEY='${key.spec?.keyHash ?? ""}'`,
154
+ "",
155
+ );
126
156
  return `${lines.join("\n")}\n`;
127
157
  }
128
158
 
129
159
  function formatExpiry(key: ApiKey): string {
130
160
  if (key.spec?.neverExpires) return "Never";
131
- if (key.spec?.expiresAt !== undefined) return timestampDate(key.spec.expiresAt).toISOString();
161
+ if (key.spec?.expiresAt !== undefined)
162
+ return timestampDate(key.spec.expiresAt).toISOString();
132
163
  return "Never";
133
164
  }
@@ -4,12 +4,24 @@
4
4
  // the standard mutating-output flags.
5
5
 
6
6
  import type { Command } from "commander";
7
- import { ensureAuthenticated, load, resolveContextOrganization, save } from "../../config/index.js";
8
- import { CommandResult, type OutputFlags, renderResult } from "../../output/index.js";
7
+ import {
8
+ activeBackend,
9
+ ensureAuthenticated,
10
+ load,
11
+ resolveContextOrganization,
12
+ save,
13
+ } from "../../config/index.js";
14
+ import {
15
+ CommandResult,
16
+ type OutputFlags,
17
+ renderResult,
18
+ } from "../../output/index.js";
9
19
  import { addResultFlags, resultFormat } from "../shared.js";
10
20
 
11
21
  export function registerAuth(program: Command): void {
12
- const auth = program.command("auth").description("manage authentication with Stigmer Cloud");
22
+ const auth = program
23
+ .command("auth")
24
+ .description("manage authentication with Stigmer Cloud");
13
25
 
14
26
  auth
15
27
  .command("login")
@@ -44,15 +56,32 @@ export function registerAuth(program: Command): void {
44
56
 
45
57
  function runLogout(): CommandResult {
46
58
  const config = load();
47
- const cloud = config.backend.cloud;
48
- if (cloud === undefined || (cloud.token === undefined && cloud.refresh_token === undefined)) {
49
- return CommandResult.warning("Not currently logged in").hint("Run 'stigmer auth login' to authenticate.");
59
+ const { name, entry } = activeBackend(config);
60
+ if (entry?.type === "selfhost") {
61
+ if (entry.api_key === undefined) {
62
+ return CommandResult.warning("No API key stored for this backend");
63
+ }
64
+ entry.api_key = undefined;
65
+ save(config);
66
+ return CommandResult.success(
67
+ `Cleared the stored API key for backend "${name}"`,
68
+ );
69
+ }
70
+ if (
71
+ entry === undefined ||
72
+ (entry.token === undefined && entry.refresh_token === undefined)
73
+ ) {
74
+ return CommandResult.warning("Not currently logged in").hint(
75
+ "Run 'stigmer auth login' to authenticate.",
76
+ );
50
77
  }
51
- cloud.token = undefined;
52
- cloud.refresh_token = undefined;
53
- cloud.token_expiry = undefined;
78
+ entry.token = undefined;
79
+ entry.refresh_token = undefined;
80
+ entry.token_expiry = undefined;
54
81
  save(config);
55
- return CommandResult.success("Logged out from Stigmer Cloud").hint("Run 'stigmer auth login' to authenticate again.");
82
+ return CommandResult.success("Logged out from Stigmer Cloud").hint(
83
+ "Run 'stigmer auth login' to authenticate again.",
84
+ );
56
85
  }
57
86
 
58
87
  async function runWhoami(): Promise<CommandResult> {
@@ -66,21 +95,30 @@ async function runWhoami(): Promise<CommandResult> {
66
95
 
67
96
  if (account.metadata !== undefined) {
68
97
  section.field("Account ID", account.metadata.id);
69
- if (account.metadata.name !== "") section.field("Name", account.metadata.name);
98
+ if (account.metadata.name !== "")
99
+ section.field("Name", account.metadata.name);
70
100
  }
71
101
  if (account.spec !== undefined) {
72
102
  if (account.spec.email !== "") section.field("Email", account.spec.email);
73
103
  if (account.spec.firstName !== "" || account.spec.lastName !== "") {
74
- section.field("Full Name", `${account.spec.firstName} ${account.spec.lastName}`.trim());
104
+ section.field(
105
+ "Full Name",
106
+ `${account.spec.firstName} ${account.spec.lastName}`.trim(),
107
+ );
75
108
  }
76
- section.field("Account Type", account.spec.isMachineAccount ? "Machine Account" : "User Account");
109
+ section.field(
110
+ "Account Type",
111
+ account.spec.isMachineAccount ? "Machine Account" : "User Account",
112
+ );
77
113
  }
78
114
 
79
115
  const org = resolveContextOrganization(client.config);
80
116
  if (org !== "") {
81
117
  section.field("Organization", org);
82
118
  } else {
83
- result.hint("No organization set. Use: stigmer config context set --org <slug>");
119
+ result.hint(
120
+ "No organization set. Use: stigmer config context set --org <slug>",
121
+ );
84
122
  }
85
123
 
86
124
  return result;