@seliseblocks/cli-os 0.1.2 → 0.1.3

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 (39) hide show
  1. package/AI_USAGE_GUIDE.md +18 -38
  2. package/README.md +13 -36
  3. package/dist/commands/auth/status.js +16 -55
  4. package/dist/commands/doctor.js +3 -4
  5. package/dist/commands/init.js +4 -2
  6. package/dist/commands/login.d.ts +0 -1
  7. package/dist/commands/login.js +15 -55
  8. package/dist/commands/new/web.js +1 -1
  9. package/dist/index.js +10 -56
  10. package/dist/lib/auth.d.ts +4 -11
  11. package/dist/lib/auth.js +43 -18
  12. package/dist/lib/config.d.ts +1 -2
  13. package/dist/lib/config.js +45 -13
  14. package/dist/lib/open-browser.js +26 -3
  15. package/dist/lib/secret-store.d.ts +0 -1
  16. package/dist/lib/secret-store.js +1 -4
  17. package/dist/lib/token-store.d.ts +0 -1
  18. package/dist/lib/token-store.js +1 -5
  19. package/dist/lib/token.d.ts +0 -1
  20. package/dist/lib/token.js +0 -4
  21. package/package.json +1 -1
  22. package/dist/commands/auth/add.d.ts +0 -1
  23. package/dist/commands/auth/add.js +0 -51
  24. package/dist/commands/auth/list.d.ts +0 -1
  25. package/dist/commands/auth/list.js +0 -33
  26. package/dist/commands/auth/repair.d.ts +0 -1
  27. package/dist/commands/auth/repair.js +0 -39
  28. package/dist/commands/auth/show.d.ts +0 -1
  29. package/dist/commands/auth/show.js +0 -23
  30. package/dist/commands/auth/use.d.ts +0 -1
  31. package/dist/commands/auth/use.js +0 -15
  32. package/dist/commands/login-device.d.ts +0 -1
  33. package/dist/commands/login-device.js +0 -6
  34. package/dist/lib/login-server.d.ts +0 -1
  35. package/dist/lib/login-server.js +0 -43
  36. package/dist/lib/pkce.d.ts +0 -4
  37. package/dist/lib/pkce.js +0 -9
  38. package/dist/lib/prompt.d.ts +0 -2
  39. package/dist/lib/prompt.js +0 -36
package/dist/lib/auth.js CHANGED
@@ -5,18 +5,7 @@ import { getClientSecret } from "./secret-store.js";
5
5
  import { CliActionableError } from "./errors.js";
6
6
  const DEVICE_GRANT = "urn:ietf:params:oauth:grant-type:device_code";
7
7
  const FALLBACK_IMPERSONATION_CLIENT_ID = "57214b67-aa9c-4307-92ab-a25e35180fac";
8
- export async function exchangeAuthorizationCode(args) {
9
- const body = new URLSearchParams({
10
- client_id: args.clientId,
11
- code: args.code,
12
- grant_type: "authorization_code",
13
- redirect_uri: args.redirectUri
14
- });
15
- if (args.codeVerifier)
16
- body.set("code_verifier", args.codeVerifier);
17
- applyClientSecret(body, args.clientSecret);
18
- return postFormToken(args.oidcUrl, body, args.rootTenantId);
19
- }
8
+ const MAX_CONSECUTIVE_TRANSIENT_ERRORS = 3;
20
9
  export async function requestDeviceAuthorization(profile) {
21
10
  const rootTenantId = await resolveRootTenantForDevice(profile);
22
11
  if (!rootTenantId) {
@@ -45,34 +34,70 @@ export async function requestDeviceAuthorization(profile) {
45
34
  }
46
35
  return data;
47
36
  }
48
- export async function pollDeviceToken(profile, device) {
37
+ export async function pollDeviceToken(profile, device, options = {}) {
49
38
  const rootTenantId = await resolveRootTenantForDevice(profile);
50
39
  if (!rootTenantId) {
51
40
  throw new Error("Device token polling requires rootTenantId in the account profile.");
52
41
  }
53
42
  let intervalSeconds = Math.max(device.interval ?? 5, 1);
54
43
  const deadline = Date.now() + device.expires_in * 1000;
44
+ let consecutiveTransientErrors = 0;
55
45
  while (Date.now() < deadline) {
56
- await delay(intervalSeconds * 1000);
46
+ options.onWait?.(intervalSeconds);
47
+ await delay(Math.min(intervalSeconds * 1000, Math.max(deadline - Date.now(), 0)));
57
48
  const body = new URLSearchParams({
58
49
  client_id: profile.clientId,
59
50
  device_code: device.device_code,
60
51
  grant_type: DEVICE_GRANT
61
52
  });
62
53
  applyClientSecret(body, await getSecretForProfile(profile));
63
- const response = await postFormToken(profile.oidcUrl, body, rootTenantId, false);
64
- if (response.error === "authorization_pending")
54
+ let response;
55
+ try {
56
+ response = await postFormToken(profile.oidcUrl, body, rootTenantId, false);
57
+ }
58
+ catch (error) {
59
+ // The token endpoint itself never throws (postFormToken with
60
+ // throwOnOAuthError=false only returns error payloads) -- a thrown
61
+ // error here means fetch() failed before any response came back
62
+ // (DNS, connection reset, etc.). Treat it as transient and keep
63
+ // polling rather than aborting a multi-minute wait on one blip.
64
+ throwIfTooManyTransientErrors(++consecutiveTransientErrors, error);
65
65
  continue;
66
+ }
67
+ if (response.error === "authorization_pending") {
68
+ consecutiveTransientErrors = 0;
69
+ continue;
70
+ }
66
71
  if (response.error === "slow_down") {
72
+ consecutiveTransientErrors = 0;
67
73
  intervalSeconds += 5;
68
74
  continue;
69
75
  }
76
+ if (response.error === "token_request_failed") {
77
+ // Synthetic error from postFormToken for a non-JSON HTTP failure
78
+ // (e.g. a 502 from an upstream proxy) -- not a real OAuth rejection,
79
+ // so treat it the same as a network blip.
80
+ throwIfTooManyTransientErrors(++consecutiveTransientErrors, response.error_description ?? response.error);
81
+ continue;
82
+ }
83
+ if (response.error === "access_denied") {
84
+ throw new CliActionableError("Device authorization was denied.", "device_login_denied", "blocks-os login");
85
+ }
86
+ if (response.error === "expired_token") {
87
+ throw new CliActionableError("Device login expired before approval.", "device_login_expired", "blocks-os login");
88
+ }
70
89
  if (response.error) {
71
- throw new Error(response.error_description ?? response.error);
90
+ throw new CliActionableError(response.error_description ?? response.error, "device_login_failed", "blocks-os login");
72
91
  }
73
92
  return response;
74
93
  }
75
- throw new Error("Device login expired before approval.");
94
+ throw new CliActionableError("Device login expired before approval.", "device_login_expired", "blocks-os login");
95
+ }
96
+ function throwIfTooManyTransientErrors(count, cause) {
97
+ if (count <= MAX_CONSECUTIVE_TRANSIENT_ERRORS)
98
+ return;
99
+ const detail = cause instanceof Error ? cause.message : String(cause);
100
+ throw new CliActionableError(`Could not reach the identity provider while waiting for device approval (${detail}).`, "device_login_network_error", "Check your network connection and run 'blocks-os login' again.");
76
101
  }
77
102
  export async function getAccountSession(accountOverride) {
78
103
  let config = await readConfig();
@@ -4,7 +4,6 @@ export type AccountProfile = {
4
4
  createdAt: string;
5
5
  oidcUrl: string;
6
6
  osUrl: string;
7
- redirectUri: string;
8
7
  rootTenantId?: string;
9
8
  scope: string;
10
9
  updatedAt: string;
@@ -31,9 +30,9 @@ export type BlocksCliConfig = {
31
30
  };
32
31
  export declare function defaults(): {
33
32
  apiUrl: string;
33
+ osClientId: string;
34
34
  oidcUrl: string;
35
35
  osUrl: string;
36
- redirectUri: string;
37
36
  rootTenantId: string;
38
37
  scope: string;
39
38
  };
@@ -1,19 +1,20 @@
1
1
  import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
2
2
  import { homedir, platform } from "node:os";
3
3
  import { dirname, join } from "node:path";
4
- const DEFAULT_API_URL = "https://api.seliseblocks.com";
4
+ const BAD_GATEWAY_OS_URL = "https://api.seliseblocks.com/os/v4";
5
+ const DEFAULT_API_URL = "https://os.seliseblocks.com";
6
+ const DEFAULT_OS_CLIENT_ID = "4a633b13-1108-4fbf-84fd-b196c9dcdee2";
5
7
  const DEFAULT_OIDC_URL = "https://iam.seliseblocks.com";
6
8
  const DEFAULT_OS_URL = "https://os.seliseblocks.com";
7
- const BAD_GATEWAY_OS_URL = "https://api.seliseblocks.com/os/v4";
9
+ const DEFAULT_PROFILE_TIMESTAMP = "2026-01-01T00:00:00.000Z";
8
10
  const DEFAULT_ROOT_TENANT_ID = "d7e5554c758541db8a18694b64ef423d";
9
11
  const DEFAULT_SCOPE = "openid profile offline_access";
10
- const DEFAULT_REDIRECT_URI = "http://127.0.0.1:8976/callback";
11
12
  export function defaults() {
12
13
  return {
13
14
  apiUrl: DEFAULT_API_URL,
15
+ osClientId: DEFAULT_OS_CLIENT_ID,
14
16
  oidcUrl: DEFAULT_OIDC_URL,
15
17
  osUrl: DEFAULT_OS_URL,
16
- redirectUri: DEFAULT_REDIRECT_URI,
17
18
  rootTenantId: DEFAULT_ROOT_TENANT_ID,
18
19
  scope: DEFAULT_SCOPE
19
20
  };
@@ -40,9 +41,7 @@ export async function readConfig() {
40
41
  }
41
42
  catch (error) {
42
43
  if (error.code === "ENOENT") {
43
- return {
44
- accounts: {}
45
- };
44
+ return defaultConfig();
46
45
  }
47
46
  throw error;
48
47
  }
@@ -61,7 +60,7 @@ export function normalizeAccountName(account) {
61
60
  export function getActiveAccountName(config, override) {
62
61
  const account = normalizeAccountName(override ?? config.activeAccount);
63
62
  if (!config.accounts[account]) {
64
- throw new Error("OIDC account is not configured. Run 'blocks-os auth:add --client-id <id> --client-secret <secret>' first.");
63
+ throw new Error("OIDC account is not configured.");
65
64
  }
66
65
  return account;
67
66
  }
@@ -73,21 +72,54 @@ export function getAccountProfile(config, account) {
73
72
  };
74
73
  }
75
74
  function normalizeConfig(config) {
75
+ const env = defaults();
76
76
  const accounts = {};
77
77
  for (const [name, profile] of Object.entries(config.accounts ?? {})) {
78
- accounts[name] = {
78
+ accounts[name] = name === "default" ? defaultProfile(profile) : {
79
79
  ...profile,
80
- oidcUrl: profile.oidcUrl ?? DEFAULT_OIDC_URL,
81
- osUrl: !profile.osUrl || profile.osUrl === BAD_GATEWAY_OS_URL ? DEFAULT_OS_URL : profile.osUrl,
82
- rootTenantId: profile.rootTenantId ?? DEFAULT_ROOT_TENANT_ID
80
+ apiUrl: profile.apiUrl ?? env.apiUrl,
81
+ clientId: profile.clientId ?? env.osClientId,
82
+ createdAt: profile.createdAt ?? DEFAULT_PROFILE_TIMESTAMP,
83
+ oidcUrl: profile.oidcUrl ?? env.oidcUrl,
84
+ osUrl: !profile.osUrl || profile.osUrl === BAD_GATEWAY_OS_URL ? env.osUrl : profile.osUrl,
85
+ rootTenantId: profile.rootTenantId ?? env.rootTenantId,
86
+ scope: profile.scope ?? env.scope,
87
+ updatedAt: profile.updatedAt ?? DEFAULT_PROFILE_TIMESTAMP
88
+ };
89
+ }
90
+ if (Object.keys(accounts).length === 0) {
91
+ return {
92
+ ...defaultConfig(),
93
+ selectedProject: config.selectedProject
83
94
  };
84
95
  }
85
96
  return {
86
- activeAccount: config.activeAccount,
97
+ activeAccount: config.activeAccount ?? "default",
87
98
  accounts,
88
99
  selectedProject: config.selectedProject
89
100
  };
90
101
  }
102
+ function defaultConfig() {
103
+ return {
104
+ activeAccount: "default",
105
+ accounts: {
106
+ default: defaultProfile()
107
+ }
108
+ };
109
+ }
110
+ function defaultProfile(existing) {
111
+ const env = defaults();
112
+ return {
113
+ apiUrl: env.apiUrl,
114
+ clientId: env.osClientId,
115
+ createdAt: existing?.createdAt ?? DEFAULT_PROFILE_TIMESTAMP,
116
+ oidcUrl: env.oidcUrl,
117
+ osUrl: env.osUrl,
118
+ rootTenantId: env.rootTenantId,
119
+ scope: env.scope,
120
+ updatedAt: DEFAULT_PROFILE_TIMESTAMP
121
+ };
122
+ }
91
123
  function nonEmptyEnv(name) {
92
124
  const value = process.env[name]?.trim();
93
125
  return value || undefined;
@@ -1,5 +1,6 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { platform } from "node:os";
3
+ const LAUNCH_GRACE_PERIOD_MS = 500;
3
4
  export async function openBrowser(url) {
4
5
  const os = platform();
5
6
  const command = os === "win32" ? "rundll32" : os === "darwin" ? "open" : "xdg-open";
@@ -10,10 +11,32 @@ export async function openBrowser(url) {
10
11
  stdio: "ignore",
11
12
  windowsHide: true
12
13
  });
13
- child.once("error", () => resolve(false));
14
- child.once("spawn", () => {
14
+ let settled = false;
15
+ const settle = (result) => {
16
+ if (settled)
17
+ return;
18
+ settled = true;
19
+ resolve(result);
20
+ };
21
+ child.once("error", () => settle(false));
22
+ // A launcher like xdg-open can spawn successfully and still exit
23
+ // non-zero almost immediately when no browser/display session is
24
+ // available (headless SSH, missing DISPLAY, etc.). Give it a short
25
+ // grace period to fail fast before treating the launch as a success
26
+ // and detaching the process.
27
+ const grace = setTimeout(() => {
15
28
  child.unref();
16
- resolve(true);
29
+ settle(true);
30
+ }, LAUNCH_GRACE_PERIOD_MS);
31
+ child.once("exit", (code) => {
32
+ clearTimeout(grace);
33
+ if (code === 0 || code === null) {
34
+ child.unref();
35
+ settle(true);
36
+ }
37
+ else {
38
+ settle(false);
39
+ }
17
40
  });
18
41
  });
19
42
  }
@@ -17,7 +17,6 @@ export declare function writeSecretStore(store: BlocksSecretStore): Promise<void
17
17
  export declare function setClientSecret(account: string, clientSecret: string): Promise<void>;
18
18
  export declare function getClientSecret(account: string): Promise<string | undefined>;
19
19
  export declare function removeAccountSecrets(account: string): Promise<void>;
20
- export declare function clearSecretStore(): Promise<void>;
21
20
  export declare function setSecretValue(key: string, value: string): Promise<void>;
22
21
  export declare function getSecretValue(key: string): Promise<string | undefined>;
23
22
  export declare function removeSecretValue(key: string): Promise<void>;
@@ -1,5 +1,5 @@
1
1
  import { execFile, spawn } from "node:child_process";
2
- import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
2
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
3
3
  import { platform } from "node:os";
4
4
  import { dirname, join } from "node:path";
5
5
  import { promisify } from "node:util";
@@ -57,9 +57,6 @@ export async function removeAccountSecrets(account) {
57
57
  await removeSecretValue(`client-secret:${account}`);
58
58
  await removeFallbackSecret(account);
59
59
  }
60
- export async function clearSecretStore() {
61
- await rm(secretPath(), { force: true });
62
- }
63
60
  export async function setSecretValue(key, value) {
64
61
  const backend = await resolveBackend();
65
62
  if (backend === "macos-keychain") {
@@ -15,4 +15,3 @@ export declare function tokenStoreInfo(): Promise<{
15
15
  export declare function readTokenStore(): Promise<BlocksTokenStore>;
16
16
  export declare function writeTokenStore(store: BlocksTokenStore): Promise<void>;
17
17
  export declare function removeAccountTokens(account: string): Promise<void>;
18
- export declare function clearTokenStore(): Promise<void>;
@@ -1,7 +1,7 @@
1
1
  import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
2
2
  import { dirname, join } from "node:path";
3
3
  import { configDir, configPath } from "./config.js";
4
- import { getSecretValue, removeSecretValue, secretStoreInfo, setSecretValue } from "./secret-store.js";
4
+ import { getSecretValue, secretStoreInfo, setSecretValue } from "./secret-store.js";
5
5
  const TOKEN_SECRET_KEY = "oauth-token-store";
6
6
  export function tokenPath() {
7
7
  return join(configDir(), "tokens.json");
@@ -56,10 +56,6 @@ export async function removeAccountTokens(account) {
56
56
  const { [account]: _, ...accounts } = store.accounts;
57
57
  await writeTokenStore({ accounts });
58
58
  }
59
- export async function clearTokenStore() {
60
- await removeSecretValue(TOKEN_SECRET_KEY);
61
- await rm(tokenPath(), { force: true });
62
- }
63
59
  function normalizeTokenStore(store) {
64
60
  return {
65
61
  accounts: store?.accounts ?? {}
@@ -13,7 +13,6 @@ export type TokenResponse = {
13
13
  scope?: string;
14
14
  token_type?: string;
15
15
  };
16
- export declare function randomState(): string;
17
16
  export declare function expiresAtFromToken(response: TokenResponse, accessToken: string): string | undefined;
18
17
  export declare function expiresAtFromJwtFirst(response: TokenResponse, accessToken: string): string | undefined;
19
18
  export declare function refreshExpiresAtFromToken(response: TokenResponse, refreshToken?: string): string | undefined;
package/dist/lib/token.js CHANGED
@@ -1,10 +1,6 @@
1
- import { randomBytes } from "node:crypto";
2
1
  import { decodeJwtPayload, tenantFromToken } from "./jwt.js";
3
2
  const EXPIRY_SKEW_MS = 60_000;
4
3
  const DEFAULT_REFRESH_TOKEN_LIFETIME_SECONDS = 30 * 60;
5
- export function randomState() {
6
- return randomBytes(24).toString("base64url");
7
- }
8
4
  export function expiresAtFromToken(response, accessToken) {
9
5
  if (typeof response.expires_in === "number" && response.expires_in > 0) {
10
6
  return new Date(Date.now() + response.expires_in * 1000).toISOString();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seliseblocks/cli-os",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "CLI for SELISE Blocks OS project setup and configuration.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -1 +0,0 @@
1
- export declare function authAdd(argv: string[]): Promise<void>;
@@ -1,51 +0,0 @@
1
- import { parseFlags, stringFlag } from "../../lib/args.js";
2
- import { defaults, normalizeAccountName, readConfig, writeConfig } from "../../lib/config.js";
3
- import { promptSecret, promptValue } from "../../lib/prompt.js";
4
- import { getClientSecret, setClientSecret } from "../../lib/secret-store.js";
5
- export async function authAdd(argv) {
6
- const { args, flags } = parseFlags(argv);
7
- const account = normalizeAccountName(args[0]);
8
- const current = await readConfig();
9
- const previous = current.accounts[account];
10
- const now = new Date().toISOString();
11
- const apiUrl = stringFlag(flags, "api-url") || previous?.apiUrl || defaults().apiUrl;
12
- const oidcUrl = stringFlag(flags, "oidc-url") || previous?.oidcUrl || defaults().oidcUrl;
13
- const osUrl = stringFlag(flags, "os-url") || defaults().osUrl;
14
- const clientId = await value(flags, "client-id", "OIDC client id", previous?.clientId);
15
- const existingSecret = await getClientSecret(account);
16
- const clientSecret = await value(flags, "client-secret", "OIDC client secret", existingSecret, true);
17
- const scope = stringFlag(flags, "scope") || previous?.scope || defaults().scope;
18
- const redirectUri = stringFlag(flags, "redirect-uri") || previous?.redirectUri || defaults().redirectUri;
19
- const rootTenantId = optionalStringFlag(flags, "root-tenant") ?? previous?.rootTenantId ?? defaults().rootTenantId;
20
- await writeConfig({
21
- ...current,
22
- activeAccount: current.activeAccount ?? account,
23
- accounts: {
24
- ...current.accounts,
25
- [account]: {
26
- apiUrl,
27
- clientId,
28
- createdAt: previous?.createdAt ?? now,
29
- oidcUrl,
30
- osUrl,
31
- redirectUri,
32
- rootTenantId,
33
- scope,
34
- updatedAt: now
35
- }
36
- }
37
- });
38
- await setClientSecret(account, clientSecret);
39
- console.log(`Saved OIDC account '${account}'.`);
40
- }
41
- async function value(flags, name, label, defaultValue, secret = false) {
42
- const flag = stringFlag(flags, name);
43
- const resolved = flag || await (secret ? promptSecret(label, defaultValue) : promptValue(label, defaultValue));
44
- if (!resolved)
45
- throw new Error(`Missing --${name}`);
46
- return resolved;
47
- }
48
- function optionalStringFlag(flags, name) {
49
- const value = flags[name];
50
- return typeof value === "string" && value ? value : undefined;
51
- }
@@ -1 +0,0 @@
1
- export declare function authList(argv?: string[]): Promise<void>;
@@ -1,33 +0,0 @@
1
- import { parseFlags } from "../../lib/args.js";
2
- import { readConfig } from "../../lib/config.js";
3
- import { writeOutput } from "../../lib/output.js";
4
- export async function authList(argv = []) {
5
- const { flags } = parseFlags(argv);
6
- const config = await readConfig();
7
- const names = Object.keys(config.accounts);
8
- if (names.length === 0) {
9
- if (flags.json) {
10
- writeOutput({ activeAccount: config.activeAccount ?? null, accounts: [] }, flags);
11
- return;
12
- }
13
- console.log("No OIDC accounts configured.");
14
- return;
15
- }
16
- if (flags.json) {
17
- writeOutput({
18
- activeAccount: config.activeAccount ?? null,
19
- accounts: names.map((name) => ({
20
- name,
21
- active: config.activeAccount === name,
22
- apiUrl: config.accounts[name].apiUrl,
23
- oidcUrl: config.accounts[name].oidcUrl
24
- }))
25
- }, flags);
26
- return;
27
- }
28
- for (const name of names) {
29
- const profile = config.accounts[name];
30
- const active = config.activeAccount === name ? "*" : " ";
31
- console.log(`${active} ${name} ${profile.apiUrl} ${profile.oidcUrl}`);
32
- }
33
- }
@@ -1 +0,0 @@
1
- export declare function authRepair(argv: string[]): Promise<void>;
@@ -1,39 +0,0 @@
1
- import { booleanFlag, parseFlags } from "../../lib/args.js";
2
- import { confirmMutation } from "../../lib/confirm.js";
3
- import { getAccountProfile, normalizeAccountName, readConfig } from "../../lib/config.js";
4
- import { writeOutput } from "../../lib/output.js";
5
- import { removeAccountSecrets } from "../../lib/secret-store.js";
6
- import { clearTokenStore } from "../../lib/token-store.js";
7
- export async function authRepair(argv) {
8
- const { args, flags } = parseFlags(argv);
9
- const config = await readConfig();
10
- const account = args[0] ? normalizeAccountName(args[0]) : getAccountProfile(config).name;
11
- if (!config.accounts[account]) {
12
- throw new Error(`OIDC account '${account}' is not configured.`);
13
- }
14
- const result = {
15
- account,
16
- clearedClientSecret: true,
17
- clearedTokenCache: "all accounts",
18
- nextSteps: [
19
- ["blocks-os auth:add", account === "default" ? "" : account, "--client-id <id>", "--client-secret <secret>"].filter(Boolean).join(" "),
20
- `blocks-os login${account === "default" ? "" : ` --account ${account}`}`
21
- ]
22
- };
23
- if (booleanFlag(flags, "dry-run")) {
24
- writeOutput({ dryRun: true, ...result }, flags);
25
- return;
26
- }
27
- await confirmMutation(flags, `Repair local auth state for '${account}'. This clears the account client secret and all cached OAuth tokens, but keeps account profile config.`);
28
- await removeAccountSecrets(account);
29
- await clearTokenStore();
30
- if (flags.json) {
31
- writeOutput(result, flags);
32
- return;
33
- }
34
- console.log(`Repaired local auth state for '${account}'.`);
35
- console.log("Client secret and cached OAuth tokens were cleared.");
36
- console.log("Next:");
37
- for (const step of result.nextSteps)
38
- console.log(` ${step}`);
39
- }
@@ -1 +0,0 @@
1
- export declare function authShow(argv: string[]): Promise<void>;
@@ -1,23 +0,0 @@
1
- import { parseFlags, stringFlag } from "../../lib/args.js";
2
- import { getAccountProfile, readConfig } from "../../lib/config.js";
3
- import { getClientSecret, secretStoreInfo } from "../../lib/secret-store.js";
4
- export async function authShow(argv) {
5
- const { args, flags } = parseFlags(argv);
6
- const config = await readConfig();
7
- const account = args[0] || stringFlag(flags, "account") || config.activeAccount;
8
- const { name, profile } = getAccountProfile(config, account);
9
- const secretInfo = await secretStoreInfo();
10
- console.log(JSON.stringify({
11
- account: name,
12
- active: config.activeAccount === name,
13
- apiUrl: profile.apiUrl,
14
- clientId: profile.clientId,
15
- clientSecret: await getClientSecret(name) ? "configured" : "missing",
16
- secretStorage: secretInfo,
17
- oidcUrl: profile.oidcUrl,
18
- osUrl: profile.osUrl,
19
- redirectUri: profile.redirectUri,
20
- rootTenantId: profile.rootTenantId,
21
- scope: profile.scope
22
- }, null, 2));
23
- }
@@ -1 +0,0 @@
1
- export declare function authUse(argv: string[]): Promise<void>;
@@ -1,15 +0,0 @@
1
- import { parseFlags } from "../../lib/args.js";
2
- import { normalizeAccountName, readConfig, writeConfig } from "../../lib/config.js";
3
- export async function authUse(argv) {
4
- const { args } = parseFlags(argv);
5
- const account = normalizeAccountName(args[0]);
6
- const config = await readConfig();
7
- if (!config.accounts[account]) {
8
- throw new Error("OIDC account is not configured. Run 'blocks-os auth:add --client-id <id> --client-secret <secret>' first.");
9
- }
10
- await writeConfig({
11
- ...config,
12
- activeAccount: account
13
- });
14
- console.log(`Active account is now '${account}'.`);
15
- }
@@ -1 +0,0 @@
1
- export declare function loginDevice(argv: string[]): Promise<void>;
@@ -1,6 +0,0 @@
1
- import { parseFlags, stringFlag } from "../lib/args.js";
2
- import { loginWithDeviceProfile } from "./login.js";
3
- export async function loginDevice(argv) {
4
- const { flags } = parseFlags(argv);
5
- await loginWithDeviceProfile(stringFlag(flags, "account"));
6
- }
@@ -1 +0,0 @@
1
- export declare function waitForAuthorizationCode(port: number, expectedState?: string): Promise<string>;
@@ -1,43 +0,0 @@
1
- import { createServer } from "node:http";
2
- export async function waitForAuthorizationCode(port, expectedState) {
3
- return new Promise((resolve, reject) => {
4
- const server = createServer((request, response) => {
5
- const url = new URL(request.url ?? "/", `http://127.0.0.1:${port}`);
6
- if (url.pathname !== "/callback") {
7
- response.writeHead(404);
8
- response.end("Not found");
9
- return;
10
- }
11
- const error = url.searchParams.get("error");
12
- const code = url.searchParams.get("code");
13
- const state = url.searchParams.get("state");
14
- if (error) {
15
- response.writeHead(400, { "Content-Type": "text/plain" });
16
- response.end(`Blocks OS login failed: ${error}`);
17
- server.close();
18
- reject(new Error(error));
19
- return;
20
- }
21
- if (expectedState && state !== expectedState) {
22
- response.writeHead(400, { "Content-Type": "text/plain" });
23
- response.end("Blocks OS login failed: invalid state");
24
- server.close();
25
- reject(new Error("Invalid authorization state"));
26
- return;
27
- }
28
- if (!code) {
29
- response.writeHead(400, { "Content-Type": "text/plain" });
30
- response.end("Blocks OS login failed: missing code");
31
- server.close();
32
- reject(new Error("Missing authorization code"));
33
- return;
34
- }
35
- response.writeHead(200, { "Content-Type": "text/plain" });
36
- response.end("Blocks OS login complete. You can close this tab.");
37
- server.close();
38
- resolve(code);
39
- });
40
- server.on("error", reject);
41
- server.listen(port, "127.0.0.1");
42
- });
43
- }
@@ -1,4 +0,0 @@
1
- export declare function createPkcePair(): {
2
- challenge: string;
3
- verifier: string;
4
- };
package/dist/lib/pkce.js DELETED
@@ -1,9 +0,0 @@
1
- import { createHash, randomBytes } from "node:crypto";
2
- function base64Url(input) {
3
- return input.toString("base64").replaceAll("+", "-").replaceAll("/", "_").replaceAll("=", "");
4
- }
5
- export function createPkcePair() {
6
- const verifier = base64Url(randomBytes(32));
7
- const challenge = base64Url(createHash("sha256").update(verifier).digest());
8
- return { challenge, verifier };
9
- }
@@ -1,2 +0,0 @@
1
- export declare function promptValue(label: string, defaultValue?: string): Promise<string>;
2
- export declare function promptSecret(label: string, defaultValue?: string): Promise<string>;
@@ -1,36 +0,0 @@
1
- import { createInterface } from "node:readline";
2
- import { createInterface as createPromiseInterface } from "node:readline/promises";
3
- import { stdin as input, stdout as output } from "node:process";
4
- export async function promptValue(label, defaultValue) {
5
- const rl = createPromiseInterface({ input, output });
6
- try {
7
- const suffix = defaultValue ? ` (${defaultValue})` : "";
8
- const answer = await rl.question(`${label}${suffix}: `);
9
- return answer.trim() || defaultValue || "";
10
- }
11
- finally {
12
- rl.close();
13
- }
14
- }
15
- export async function promptSecret(label, defaultValue) {
16
- if (!input.isTTY || !output.isTTY)
17
- return promptValue(label, defaultValue);
18
- return new Promise((resolve) => {
19
- const rl = createInterface({ input, output, terminal: true });
20
- const muted = rl;
21
- muted._writeToOutput = (value) => {
22
- if (value === "\r\n" || value === "\n" || value === "\r") {
23
- output.write(value);
24
- return;
25
- }
26
- output.write("*".repeat(value.length));
27
- };
28
- const suffix = defaultValue ? " (configured, press Enter to keep)" : "";
29
- output.write(`${label}${suffix}: `);
30
- rl.question("", (answer) => {
31
- rl.close();
32
- output.write("\n");
33
- resolve(answer.trim() || defaultValue || "");
34
- });
35
- });
36
- }