@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/AI_USAGE_GUIDE.md CHANGED
@@ -27,58 +27,38 @@ For local package development only, contributors may run `node bin/run.js ...` f
27
27
  - Prefer `--json` for automation and parsing.
28
28
  - Use `--dry-run` before any mutating command.
29
29
  - Do not run real mutating cloud commands unless the user explicitly approved the exact action.
30
- - Never print, commit, scaffold, or document client secrets, access tokens, refresh tokens, cookies, or JWTs.
30
+ - Never print, commit, scaffold, or document access tokens, refresh tokens, cookies, JWTs, or other secrets.
31
31
  - Treat any secret pasted into chat or logs as exposed and rotate it before production use.
32
- - Generated apps must not contain CLI client secrets or CLI tokens.
32
+ - Generated apps must not contain CLI tokens.
33
33
  - If a CLI command returns an error, fix or report the CLI path. Do not bypass the CLI with a one-off API request when the command exists.
34
34
 
35
- ## Account Setup
36
-
37
- Add the CLI OIDC account:
38
-
39
- ```bash
40
- blocks-os auth:add --client-id <clientId> --client-secret <clientSecret>
41
- ```
42
-
43
- Normal production defaults are built in:
44
-
45
- ```text
46
- API URL: https://api.seliseblocks.com
47
- OIDC URL: https://iam.seliseblocks.com
48
- OS portal URL: https://os.seliseblocks.com
49
- Redirect URI: http://127.0.0.1:8976/callback
50
- Scope: openid profile offline_access
51
- Root tenant: d7e5554c758541db8a18694b64ef423d
52
- ```
53
-
54
- The CLI stores client secrets and OAuth tokens in the OS credential backend when available. Do not manually copy token files between machines.
55
-
56
35
  ## Login
57
36
 
58
- Use browser login when an interactive browser is available:
37
+ Device-code login uses the packaged OS client id. It prints a verification URL and user code, opens the browser to the verification page when possible, then polls until approved:
59
38
 
60
39
  ```bash
61
40
  blocks-os login
62
41
  ```
63
42
 
64
- Use device login when a browser callback is not convenient:
43
+ Check current auth state:
65
44
 
66
45
  ```bash
67
- blocks-os login:device
46
+ blocks-os auth:status --json
68
47
  ```
69
48
 
70
- Check current auth state:
49
+ If local auth state is stale or corrupted (Windows profile change, machine migration, Keychain reset), clear local auth state and log in again:
71
50
 
72
51
  ```bash
73
- blocks-os auth:status --json
52
+ blocks-os auth:remove <account>
53
+ blocks-os login
74
54
  ```
75
55
 
76
- Repair stale or corrupted local auth storage:
56
+ Use `blocks-os logout` to revoke the current refresh token when possible and remove local session data. Use `blocks-os auth:refresh --json` to force account token refresh, and `blocks-os auth:refresh --project --json` after a project session already exists.
57
+
58
+ Run health checks without mutation:
77
59
 
78
60
  ```bash
79
- blocks-os auth:repair --yes
80
- blocks-os auth:add --client-id <clientId> --client-secret <clientSecret>
81
- blocks-os login
61
+ blocks-os doctor --json
82
62
  ```
83
63
 
84
64
  ## Project Workflow
@@ -113,17 +93,15 @@ blocks-os projects:get --json
113
93
  Generate a React/Vite Blocks app:
114
94
 
115
95
  ```bash
116
- blocks-os new web <appName> --x-blocks-key <projectTenantId> --app-domain <appDomainOrUrl> --blocks-api-url https://api.seliseblocks.com
96
+ blocks-os new web <appName> --x-blocks-key <projectTenantId> --app-domain <appDomainOrUrl>
117
97
  ```
118
98
 
119
99
  If a public browser OIDC client exists for the app, include it:
120
100
 
121
101
  ```bash
122
- blocks-os new web <appName> --x-blocks-key <projectTenantId> --app-domain <appDomainOrUrl> --blocks-api-url https://api.seliseblocks.com --client-id <publicOidcClientId>
102
+ blocks-os new web <appName> --x-blocks-key <projectTenantId> --app-domain <appDomainOrUrl> --client-id <publicOidcClientId>
123
103
  ```
124
104
 
125
- Use `--oidc-url <url>` only for non-production IAM authority overrides; it defaults to `https://iam.seliseblocks.com`.
126
-
127
105
  Validate the scaffold:
128
106
 
129
107
  ```bash
@@ -132,7 +110,7 @@ npm install
132
110
  npm run build
133
111
  ```
134
112
 
135
- Do not pass the CLI client secret to the scaffolded app. Browser apps must use a public OIDC client and the SDK hosted IdP flow: `blocksClient.auth.idp.redirectToProvider()` on login click and `blocksClient.auth.idp.callback()` on `/login/callback`.
113
+ Do not pass CLI auth state to the scaffolded app. Browser apps must use a public OIDC client and the SDK hosted IdP flow: `blocksClient.auth.idp.redirectToProvider()` on login click and `blocksClient.auth.idp.callback()` on `/login/callback`.
136
114
 
137
115
  `--app-domain` is the app's real Blocks domain/origin, for example `https://dbpdba.seliseblocks.com`. The generated `.env` keeps that full value as `VITE_BLOCKS_APP_DOMAIN` and derives the local dev host without a scheme as `VITE_BLOCKS_DEV_HOST=dbpdba.seliseblocks.com`.
138
116
 
@@ -278,7 +256,9 @@ blocks-os release:builds:list --repo-id <repoId> --json
278
256
  ## Agent Failure Handling
279
257
 
280
258
  - `not_logged_in`: run `blocks-os login`.
281
- - `auth_repair_required`: run `blocks-os auth:repair --yes`, re-add credentials, then login.
259
+ - `refresh_token_rejected`: run `blocks-os login`.
260
+ - `refresh_network_error`: check the network and configured OIDC URL, then retry.
261
+ - `auth_repair_required`: inspect `blocks-os auth:status --json`; if local storage is unreadable or stale, run `blocks-os auth:remove <account>`, then `blocks-os auth:status --json` and `blocks-os login`.
282
262
  - `project_not_selected`: run `blocks-os use <projectTenantId>`.
283
263
  - `api_auth_failed`: run `blocks-os auth:status --json`, then login again.
284
264
  - HTML returned from an API command means the command endpoint path is wrong and must be fixed in the CLI.
package/README.md CHANGED
@@ -14,28 +14,12 @@ npm install -g @seliseblocks/cli-os
14
14
  blocks-os --version
15
15
  ```
16
16
 
17
- Configure one OIDC account once:
18
-
19
- ```bash
20
- blocks-os auth:add --client-id <cli-client-id> --client-secret <cli-client-secret>
21
- ```
22
-
23
- The CLI fixes these defaults unless you pass advanced override flags:
24
-
25
- ```text
26
- API URL: https://api.seliseblocks.com
27
- OIDC URL: https://iam.seliseblocks.com
28
- OS portal URL: https://os.seliseblocks.com
29
- Redirect URI: http://127.0.0.1:8976/callback
30
- Scope: openid profile offline_access
31
- Root tenant: d7e5554c758541db8a18694b64ef423d
32
- ```
33
-
34
- Then use either login method with the same account:
17
+ Then log in (device-code flow - prints a verification URL and code, opens
18
+ your browser to the verification page when possible so you only need to
19
+ click approve, then polls until approved):
35
20
 
36
21
  ```bash
37
22
  blocks-os login
38
- blocks-os login:device
39
23
  ```
40
24
 
41
25
  For source development in this repository:
@@ -52,16 +36,10 @@ node bin/run.js --version
52
36
  |---|---|
53
37
  | `blocks-os init` | Create local Blocks workspace files: `blocks.json`, data schema/rules folders, release deploy config, and `.env.example`. |
54
38
  | `blocks-os doctor [--json]` | Check Node.js, OIDC config, token cache, selected project, and config file locations. Does not mutate cloud resources. |
55
- | `blocks-os auth:add [account] --client-id <id> --client-secret <secret>` | Save OIDC client settings for the CLI. Defaults target production Blocks URLs, root tenant, scope, and localhost callback. Secret is stored separately. |
56
- | `blocks-os login` | Browser Authorization Code + PKCE login. Opens IAM, captures the localhost callback, stores account tokens, and auto-refreshes later. |
57
- | `blocks-os login:device` | Device-code login for terminals or agents where a browser callback is not convenient. Uses the same saved account profile. |
58
- | `blocks-os auth:status [--json]` | Show account config, token freshness, refresh-token availability, and selected project without printing secrets. |
39
+ | `blocks-os login` | Device-code login. Prints a verification URL and user code, opens the browser to the verification page when possible so you only need to click approve, then polls until the device is authorized; stores account tokens and auto-refreshes later. |
40
+ | `blocks-os auth:status [--json]` | Show only whether account/project access and refresh tokens are missing, valid, expired, or available. Does not print account config values. |
59
41
  | `blocks-os auth:refresh [--project] [--json]` | Force account token refresh, or project token refresh with `--project`. |
60
- | `blocks-os auth:repair [account] [--dry-run] [--yes] [--json]` | Clear broken local secure-store auth state while keeping account profile config. Use after OS credential migration, stale DPAPI, or corrupted tokens. |
61
- | `blocks-os auth:list [--json]` | List saved account profiles without showing secrets. |
62
- | `blocks-os auth:show [account]` | Print one account profile with secret redacted as `configured` or `missing`. |
63
- | `blocks-os auth:use <account>` | Switch the active named account profile. Normal single-account use does not need this. |
64
- | `blocks-os auth:remove <account>` | Remove account config, cached tokens, and stored client secret for that account. |
42
+ | `blocks-os auth:remove <account>` | Clear cached tokens and stored local credentials for that account. The packaged default OS account is restored from package defaults. |
65
43
  | `blocks-os logout` | Revoke the current refresh token when possible and remove local session data. |
66
44
  | `blocks-os projects:list [--json]` | List accessible Blocks projects via `/os/v4/Project/Gets` using the account token. Read-only. |
67
45
  | `blocks-os projects:get [tenantId] [--json]` | Read one project from `Project/Gets`. Uses selected project when `tenantId` is omitted. Read-only. |
@@ -82,7 +60,7 @@ node bin/run.js --version
82
60
  | `blocks-os release:status <buildId> [--json]` | Read Release build status by build id. Read-only. |
83
61
  | `blocks-os release:builds:list --repo-id <repoId> [--json]` | List Release build details for a repository. Read-only. |
84
62
  | `blocks-os release:builds:get <buildId> [--json]` | Alias for `release:status`. Read-only. |
85
- | `blocks-os new web <name> --x-blocks-key <tenantId> --app-domain <domain-or-url> --blocks-api-url <url> [--client-id <oidcClientId>] [--oidc-url <url>]` | Create a Vite React starter app that talks to Blocks exclusively through `@seliseblocks/client` (a single `createBlocksClient()` instance) using the SDK hosted IdP flow: `blocksClient.auth.idp.redirectToProvider()` on login click and `blocksClient.auth.idp.callback()` on `/login/callback`. Includes route guards, auto-refresh through `auth.oidc.refreshToken()`, live `auth`/`iam`/`data`/`localization` SDK examples, environment config, and safe `.gitignore` defaults. Register a **public** OIDC client with redirect URIs for both your local HTTPS dev origin and `--app-domain`, then pass its id as `--client-id` (or set `VITE_BLOCKS_OIDC_CLIENT_ID` in `.env` afterwards). `--oidc-url` defaults to `https://iam.seliseblocks.com`. |
63
+ | `blocks-os new web <name> --x-blocks-key <tenantId> --app-domain <domain-or-url> [--client-id <oidcClientId>]` | Create a Vite React starter app that talks to Blocks exclusively through `@seliseblocks/client` (a single `createBlocksClient()` instance) using the SDK hosted IdP flow: `blocksClient.auth.idp.redirectToProvider()` on login click and `blocksClient.auth.idp.callback()` on `/login/callback`. Includes route guards, auto-refresh through `auth.oidc.refreshToken()`, live `auth`/`iam`/`data`/`localization` SDK examples, environment config, and safe `.gitignore` defaults. Register a **public** OIDC client with redirect URIs for both your local HTTPS dev origin and `--app-domain`, then pass its id as `--client-id` (or set `VITE_BLOCKS_OIDC_CLIENT_ID` in `.env` afterwards). |
86
64
 
87
65
  Use `--json` on commands when AI or automation needs machine-readable output. Use `--dry-run` before mutations and `--yes` only after approval.
88
66
 
@@ -109,24 +87,23 @@ Add the generated `VITE_BLOCKS_DEV_HOST` to your hosts file, for example:
109
87
 
110
88
  Then open `https://<VITE_BLOCKS_DEV_HOST>:5173`, not plain `http://`. The generated cert script uses a Node dependency, so it works from normal PowerShell after `npm install`; OpenSSL/Git Bash is not required.
111
89
 
112
- OIDC account settings are saved in the OS-specific config directory. Access and refresh tokens are stored in the same OS-aware secure backend as client secrets when available, and are refreshed automatically before cloud commands when a refresh token is available.
90
+ OIDC account settings are saved in the OS-specific config directory. Access and refresh tokens are stored in an OS-aware secure backend when available, and are refreshed automatically before cloud commands when a refresh token is available.
113
91
 
114
- Client secrets and OAuth tokens use an OS-aware credential backend:
92
+ OAuth tokens use an OS-aware credential backend:
115
93
 
116
- | OS | Secret backend |
94
+ | OS | Credential backend |
117
95
  |---|---|
118
96
  | Windows | DPAPI-encrypted value in the CLI secret metadata file, scoped to the current Windows user. |
119
97
  | macOS | Keychain generic password for the `seliseblocks-cli-os` service. |
120
98
  | Linux | Secret Service through `secret-tool` when available. |
121
99
  | Fallback | `0600` file storage in the CLI config directory. Set `BLOCKS_OS_SECRET_STORE=file` to force this mode for CI or minimal containers. |
122
100
 
123
- Use `blocks-os doctor` or `blocks-os auth:status` to see which backend is active. Secrets and tokens are never printed by CLI status/show commands.
101
+ Use `blocks-os doctor` or `blocks-os auth:status` to see which backend is active. Tokens are never printed by CLI status commands.
124
102
 
125
- If the active OS credential backend cannot decrypt old local auth state after a Windows profile change, machine migration, Keychain reset, or corrupted token cache, repair local state and login again:
103
+ If the active OS credential backend cannot decrypt old local auth state after a Windows profile change, machine migration, Keychain reset, or corrupted token cache, clear local auth state and log in again:
126
104
 
127
105
  ```bash
128
- blocks-os auth:repair --yes
129
- blocks-os auth:add --client-id <cli-client-id> --client-secret <cli-client-secret>
106
+ blocks-os auth:remove <account>
130
107
  blocks-os login
131
108
  ```
132
109
 
@@ -1,75 +1,36 @@
1
1
  import { parseFlags, stringFlag } from "../../lib/args.js";
2
2
  import { getAccountProfile, readConfig } from "../../lib/config.js";
3
3
  import { isExpiring } from "../../lib/token.js";
4
- import { readTokenStore, tokenStoreInfo } from "../../lib/token-store.js";
4
+ import { readTokenStore } from "../../lib/token-store.js";
5
5
  import { writeOutput } from "../../lib/output.js";
6
- import { getClientSecret, secretStoreInfo } from "../../lib/secret-store.js";
7
6
  export async function authStatus(argv = []) {
8
7
  const { flags } = parseFlags(argv);
9
8
  const config = await readConfig();
10
9
  const store = await readTokenStore();
11
- const tokenInfo = await tokenStoreInfo();
12
- const accountOverride = stringFlag(flags, "account");
13
- if (Object.keys(config.accounts).length === 0) {
14
- if (flags.json) {
15
- writeOutput({ configured: false, message: "No OIDC accounts configured." }, flags);
16
- return;
17
- }
18
- console.log("No OIDC accounts configured. Run 'blocks-os auth:add --client-id <id> --client-secret <secret>'.");
19
- return;
20
- }
21
- const { name, profile } = getAccountProfile(config, accountOverride);
22
- const clientSecret = await getClientSecret(name);
23
- const secretInfo = await secretStoreInfo();
10
+ const accountOverride = stringFlag(flags, "account") || undefined;
11
+ const { name } = getAccountProfile(config, accountOverride);
24
12
  const accountToken = store.accounts[name]?.account;
25
13
  const projectToken = config.selectedProject?.tenantId
26
14
  ? store.accounts[name]?.projects?.[config.selectedProject.tenantId]
27
15
  : undefined;
28
16
  if (flags.json) {
29
17
  writeOutput({
30
- configured: true,
31
- activeAccount: config.activeAccount ?? null,
32
- account: name,
33
- apiUrl: profile.apiUrl,
34
- clientId: profile.clientId,
35
- clientSecret: clientSecret ? "configured" : "missing",
36
- secretStorage: secretInfo,
37
- tokenStorage: tokenInfo,
38
- oidcUrl: profile.oidcUrl,
39
- osUrl: profile.osUrl,
40
- scope: profile.scope,
41
- rootTenant: profile.rootTenantId ?? accountToken?.accountTenant ?? null,
42
- accountAccessToken: accountToken?.accessToken ? tokenState(accountToken.expiresAt) : "missing",
43
- accountRefreshToken: accountToken?.refreshToken ? "available" : "missing",
44
- accountRefreshTokenExpiresAt: accountToken?.refreshTokenExpiresAt ?? null,
45
- selectedProject: config.selectedProject?.tenantId ?? null,
46
- projectAccessToken: projectToken?.accessToken ? tokenState(projectToken.expiresAt) : "not created",
47
- projectRefreshToken: projectToken?.refreshToken ? "available" : "missing",
48
- projectRefreshTokenExpiresAt: projectToken?.refreshTokenExpiresAt ?? null
18
+ accountAccessToken: tokenState(accountToken?.accessToken, accountToken?.expiresAt),
19
+ accountRefreshToken: tokenState(accountToken?.refreshToken, accountToken?.refreshTokenExpiresAt),
20
+ projectAccessToken: tokenState(projectToken?.accessToken, projectToken?.expiresAt),
21
+ projectRefreshToken: tokenState(projectToken?.refreshToken, projectToken?.refreshTokenExpiresAt)
49
22
  }, flags);
50
23
  return;
51
24
  }
52
- console.log(`Active account: ${config.activeAccount ?? "missing"}`);
53
- console.log(`Account: ${name}`);
54
- console.log(`API URL: ${profile.apiUrl}`);
55
- console.log(`OIDC URL: ${profile.oidcUrl}`);
56
- console.log(`OS URL: ${profile.osUrl}`);
57
- console.log(`OIDC client id: ${profile.clientId}`);
58
- console.log(`OIDC client secret: ${clientSecret ? "configured" : "missing"}`);
59
- console.log(`Secret storage: ${secretInfo.backend} (${secretInfo.detail})`);
60
- console.log(`Token storage: ${tokenInfo.backend} (${tokenInfo.detail})`);
61
- console.log(`Scope: ${profile.scope}`);
62
- console.log(`Root tenant: ${profile.rootTenantId ?? accountToken?.accountTenant ?? "missing"}`);
63
- console.log(`Account access token: ${accountToken?.accessToken ? tokenState(accountToken.expiresAt) : "missing"}`);
64
- console.log(`Account refresh token: ${accountToken?.refreshToken ? "available" : "missing"}`);
65
- console.log(`Account refresh token expires: ${accountToken?.refreshTokenExpiresAt ?? "unknown"}`);
66
- console.log(`Selected project: ${config.selectedProject?.tenantId ?? "not selected"}`);
67
- console.log(`Project access token: ${projectToken?.accessToken ? tokenState(projectToken.expiresAt) : "not created"}`);
68
- console.log(`Project refresh token: ${projectToken?.refreshToken ? "available" : "missing"}`);
69
- console.log(`Project refresh token expires: ${projectToken?.refreshTokenExpiresAt ?? "unknown"}`);
25
+ console.log(`Account access token: ${tokenState(accountToken?.accessToken, accountToken?.expiresAt)}`);
26
+ console.log(`Account refresh token: ${tokenState(accountToken?.refreshToken, accountToken?.refreshTokenExpiresAt)}`);
27
+ console.log(`Project access token: ${tokenState(projectToken?.accessToken, projectToken?.expiresAt)}`);
28
+ console.log(`Project refresh token: ${tokenState(projectToken?.refreshToken, projectToken?.refreshTokenExpiresAt)}`);
70
29
  }
71
- function tokenState(expiresAt) {
30
+ function tokenState(token, expiresAt) {
31
+ if (!token)
32
+ return "missing";
72
33
  if (!expiresAt)
73
- return "unknown expiry";
74
- return `${isExpiring(expiresAt) ? "expired/expiring" : "fresh"} until ${expiresAt}`;
34
+ return "available";
35
+ return isExpiring(expiresAt) ? "expired" : "valid";
75
36
  }
@@ -5,7 +5,7 @@ import { parseFlags } from "../lib/args.js";
5
5
  import { writeOutput } from "../lib/output.js";
6
6
  import { isExpiring } from "../lib/token.js";
7
7
  import { readTokenStore, tokenPath, tokenStoreInfo } from "../lib/token-store.js";
8
- import { getClientSecret, secretPath, secretStoreInfo } from "../lib/secret-store.js";
8
+ import { secretPath, secretStoreInfo } from "../lib/secret-store.js";
9
9
  export async function doctor(argv = []) {
10
10
  const { flags } = parseFlags(argv);
11
11
  const config = await readConfig();
@@ -17,8 +17,7 @@ export async function doctor(argv = []) {
17
17
  { label: "OIDC account configured", ok: Object.keys(config.accounts).length > 0, detail: config.activeAccount ?? "missing" }
18
18
  ];
19
19
  if (Object.keys(config.accounts).length > 0) {
20
- const { name, profile } = getAccountProfile(config);
21
- const clientSecret = await getClientSecret(name);
20
+ const { name } = getAccountProfile(config);
22
21
  const secretInfo = await secretStoreInfo();
23
22
  const accountToken = store.accounts[name]?.account;
24
23
  let sessionValid = false;
@@ -38,7 +37,7 @@ export async function doctor(argv = []) {
38
37
  const projectToken = config.selectedProject?.tenantId
39
38
  ? store.accounts[name]?.projects?.[config.selectedProject.tenantId]
40
39
  : undefined;
41
- checks.push({ label: "API URL configured", ok: Boolean(profile.apiUrl), detail: profile.apiUrl }, { label: "OIDC URL configured", ok: Boolean(profile.oidcUrl), detail: profile.oidcUrl }, { label: "OS URL configured", ok: Boolean(profile.osUrl), detail: profile.osUrl }, { label: "OIDC client configured", ok: Boolean(profile.clientId), detail: profile.clientId }, { label: "OIDC client secret", ok: Boolean(clientSecret), detail: clientSecret ? "configured" : "missing" }, { label: "Secret storage backend", ok: true, detail: `${secretInfo.backend} (${secretInfo.detail})` }, { label: "Redirect URI configured", ok: Boolean(profile.redirectUri), detail: profile.redirectUri }, { label: "Account session", ok: sessionValid, detail: sessionDetail }, { label: "Account refresh token", ok: Boolean(refreshedAccountToken?.refreshToken), detail: refreshedAccountToken?.refreshToken ? "available" : "missing" }, { label: "Account access token cached", ok: Boolean(refreshedAccountToken?.accessToken && !isExpiring(refreshedAccountToken.expiresAt)), detail: refreshedAccountToken?.expiresAt ?? "missing" }, { label: "Project selected", ok: Boolean(config.selectedProject?.tenantId), detail: config.selectedProject?.tenantId ?? "missing" }, { label: "Project access token cached", ok: Boolean(!projectToken?.accessToken || !isExpiring(projectToken.expiresAt)), detail: projectToken?.expiresAt ?? "not created yet" });
40
+ checks.push({ label: "Credential storage backend", ok: true, detail: `${secretInfo.backend} (${secretInfo.detail})` }, { label: "Account session", ok: sessionValid, detail: sessionDetail }, { label: "Account refresh token", ok: Boolean(refreshedAccountToken?.refreshToken), detail: refreshedAccountToken?.refreshToken ? "available" : "missing" }, { label: "Account access token cached", ok: Boolean(refreshedAccountToken?.accessToken && !isExpiring(refreshedAccountToken.expiresAt)), detail: refreshedAccountToken?.expiresAt ?? "missing" }, { label: "Project selected", ok: Boolean(config.selectedProject?.tenantId), detail: config.selectedProject?.tenantId ?? "missing" }, { label: "Project access token cached", ok: Boolean(!projectToken?.accessToken || !isExpiring(projectToken.expiresAt)), detail: projectToken?.expiresAt ?? "not created yet" });
42
41
  }
43
42
  let configFile = "missing";
44
43
  try {
@@ -1,13 +1,15 @@
1
1
  import { mkdir, writeFile } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
+ import { defaults } from "../lib/config.js";
3
4
  export async function init() {
5
+ const env = defaults();
4
6
  await mkdir(join(process.cwd(), "blocks", "data", "schemas"), { recursive: true });
5
7
  await mkdir(join(process.cwd(), "blocks", "localization"), { recursive: true });
6
8
  await mkdir(join(process.cwd(), "blocks", "release"), { recursive: true });
7
9
  await writeIfMissing("blocks.json", `${JSON.stringify({
8
10
  project: {
9
11
  tenantId: "",
10
- apiUrl: "https://api.seliseblocks.com",
12
+ apiUrl: env.apiUrl,
11
13
  appDomain: ""
12
14
  },
13
15
  data: {
@@ -29,7 +31,7 @@ export async function init() {
29
31
  strategy: "configured-pipeline"
30
32
  }, null, 2)}\n`);
31
33
  await writeIfMissing(".env.example", [
32
- "VITE_BLOCKS_API_URL=https://api.seliseblocks.com",
34
+ `VITE_BLOCKS_API_URL=${env.apiUrl}`,
33
35
  "VITE_BLOCKS_X_BLOCKS_KEY=",
34
36
  "VITE_BLOCKS_APP_DOMAIN=",
35
37
  ""
@@ -1,2 +1 @@
1
1
  export declare function login(argv: string[]): Promise<void>;
2
- export declare function loginWithDeviceProfile(accountOverride?: string): Promise<void>;
@@ -1,75 +1,35 @@
1
- import { URLSearchParams } from "node:url";
2
1
  import { parseFlags, stringFlag } from "../lib/args.js";
3
- import { exchangeAuthorizationCode, pollDeviceToken, requestDeviceAuthorization } from "../lib/auth.js";
2
+ import { pollDeviceToken, requestDeviceAuthorization } from "../lib/auth.js";
4
3
  import { getAccountProfile, readConfig, writeConfig } from "../lib/config.js";
5
- import { waitForAuthorizationCode } from "../lib/login-server.js";
6
4
  import { openBrowser } from "../lib/open-browser.js";
7
- import { createPkcePair } from "../lib/pkce.js";
8
- import { applyAccountToken, randomState } from "../lib/token.js";
5
+ import { applyAccountToken } from "../lib/token.js";
9
6
  import { readTokenStore, writeTokenStore } from "../lib/token-store.js";
10
- import { getClientSecret } from "../lib/secret-store.js";
11
7
  export async function login(argv) {
12
8
  const { flags } = parseFlags(argv);
13
9
  const accountOverride = stringFlag(flags, "account");
14
- const config = await readConfig();
15
- const { name, profile } = getAccountProfile(config, accountOverride);
16
- const redirectUri = profile.redirectUri;
17
- if (!redirectUri) {
18
- throw new Error("OIDC redirectUri is missing. Run 'blocks-os auth:add --client-id <id> --client-secret <secret>' to restore the default redirect URI.");
19
- }
20
- const url = new URL(redirectUri);
21
- const port = Number(url.port || (url.protocol === "https:" ? 443 : 80));
22
- const state = randomState();
23
- const { challenge, verifier } = createPkcePair();
24
- const authUrl = new URL("/api/oidc/authorize", profile.oidcUrl);
25
- authUrl.search = new URLSearchParams({
26
- client_id: profile.clientId,
27
- code_challenge: challenge,
28
- code_challenge_method: "S256",
29
- redirect_uri: redirectUri,
30
- response_type: "code",
31
- scope: profile.scope,
32
- state,
33
- tenant_id: profile.rootTenantId ?? ""
34
- }).toString();
35
- const codePromise = waitForAuthorizationCode(port, state);
36
- console.log("Open this URL to log in to Blocks OS:");
37
- console.log(authUrl.toString());
38
- const opened = await openBrowser(authUrl.toString());
39
- if (!opened) {
40
- console.log("Browser auto-open is unavailable on this machine. Open the URL manually.");
41
- }
42
- const code = await codePromise;
43
- const token = await exchangeAuthorizationCode({
44
- account: name,
45
- clientId: profile.clientId,
46
- clientSecret: await getClientSecret(name),
47
- code,
48
- codeVerifier: verifier,
49
- oidcUrl: profile.oidcUrl,
50
- redirectUri,
51
- rootTenantId: profile.rootTenantId
52
- });
53
- const latest = await readConfig();
54
- const latestStore = await readTokenStore();
55
- const next = applyAccountToken(latest, latestStore, name, profile.clientId, token);
56
- await writeConfig(next.config);
57
- await writeTokenStore(next.store);
58
- console.log(`Logged in to account '${name}' for tenant ${next.store.accounts[name].account.accountTenant}`);
59
- }
60
- export async function loginWithDeviceProfile(accountOverride) {
61
10
  const config = await readConfig();
62
11
  const { name, profile } = getAccountProfile(config, accountOverride);
63
12
  const device = await requestDeviceAuthorization(profile);
64
13
  console.log("Authorize this device:");
65
14
  console.log(`URL: ${device.verification_uri_complete ?? device.verification_uri}`);
66
15
  console.log(`Code: ${device.user_code}`);
16
+ if (device.verification_uri_complete) {
17
+ const opened = await openBrowser(device.verification_uri_complete);
18
+ console.log(opened
19
+ ? "Opened your browser to approve this device -- just confirm the code above."
20
+ : "Browser auto-open is unavailable on this machine. Open the URL above manually.");
21
+ }
22
+ else {
23
+ console.log("Open the URL above and enter the code to approve this device.");
24
+ }
67
25
  console.log("Waiting for approval...");
68
- const token = await pollDeviceToken(profile, device);
26
+ const token = await pollDeviceToken(profile, device, {
27
+ onWait: (seconds) => console.log(`Checking for approval in ${seconds}s...`)
28
+ });
69
29
  const latest = await readConfig();
70
30
  const latestStore = await readTokenStore();
71
31
  const next = applyAccountToken(latest, latestStore, name, profile.clientId, token);
72
32
  await writeConfig(next.config);
73
33
  await writeTokenStore(next.store);
74
- console.log(`Logged in to account '${name}' for tenant ${next.store.accounts[name].account.accountTenant}`);
34
+ console.log("Login done.");
75
35
  }
@@ -8,7 +8,7 @@ export async function newWeb(argv) {
8
8
  if (!name)
9
9
  throw new Error("Missing web app name.");
10
10
  const appDomain = stringFlag(parsed.flags, "app-domain", { required: true });
11
- const apiUrl = stringFlag(parsed.flags, "blocks-api-url", { defaultValue: "https://api.seliseblocks.com" });
11
+ const apiUrl = stringFlag(parsed.flags, "blocks-api-url", { defaultValue: defaults().apiUrl });
12
12
  const oidcUrl = stringFlag(parsed.flags, "oidc-url", { defaultValue: defaults().oidcUrl });
13
13
  const xBlocksKey = stringFlag(parsed.flags, "x-blocks-key", { required: true });
14
14
  const oidcClientId = stringFlag(parsed.flags, "client-id");
package/dist/index.js CHANGED
@@ -1,11 +1,6 @@
1
- import { authAdd } from "./commands/auth/add.js";
2
- import { authList } from "./commands/auth/list.js";
3
1
  import { authRefresh } from "./commands/auth/refresh.js";
4
- import { authRepair } from "./commands/auth/repair.js";
5
2
  import { authRemove } from "./commands/auth/remove.js";
6
- import { authShow } from "./commands/auth/show.js";
7
3
  import { authStatus } from "./commands/auth/status.js";
8
- import { authUse } from "./commands/auth/use.js";
9
4
  import { doctor } from "./commands/doctor.js";
10
5
  import { dataReload } from "./commands/data/reload.js";
11
6
  import { dataValidate } from "./commands/data/validate.js";
@@ -20,7 +15,6 @@ import { localizationPull } from "./commands/localization/pull.js";
20
15
  import { localizationPush } from "./commands/localization/push.js";
21
16
  import { localizationValidate } from "./commands/localization/validate.js";
22
17
  import { login } from "./commands/login.js";
23
- import { loginDevice } from "./commands/login-device.js";
24
18
  import { logout } from "./commands/logout.js";
25
19
  import { newWeb } from "./commands/new/web.js";
26
20
  import { createProject } from "./commands/projects/create.js";
@@ -40,18 +34,6 @@ try {
40
34
  else if (!command || command === "help" || command === "--help" || command === "-h") {
41
35
  printHelp();
42
36
  }
43
- else if (command === "auth:add" || (command === "auth" && subcommand === "add")) {
44
- await authAdd(command === "auth" ? rest : [subcommand, ...rest].filter(Boolean));
45
- }
46
- else if (command === "auth:list" || (command === "auth" && subcommand === "list")) {
47
- await authList(command === "auth" ? rest : [subcommand, ...rest].filter(Boolean));
48
- }
49
- else if (command === "auth:use" || (command === "auth" && subcommand === "use")) {
50
- await authUse(command === "auth" ? rest : [subcommand, ...rest].filter(Boolean));
51
- }
52
- else if (command === "auth:show" || (command === "auth" && subcommand === "show")) {
53
- await authShow(command === "auth" ? rest : [subcommand, ...rest].filter(Boolean));
54
- }
55
37
  else if (command === "auth:remove" || (command === "auth" && subcommand === "remove")) {
56
38
  await authRemove(command === "auth" ? rest : [subcommand, ...rest].filter(Boolean));
57
39
  }
@@ -61,27 +43,18 @@ try {
61
43
  else if (command === "auth:refresh") {
62
44
  await authRefresh([subcommand, ...rest].filter(Boolean));
63
45
  }
64
- else if (command === "auth:repair") {
65
- await authRepair([subcommand, ...rest].filter(Boolean));
66
- }
67
46
  else if (command === "auth" && subcommand === "status") {
68
47
  await authStatus(rest);
69
48
  }
70
49
  else if (command === "auth" && subcommand === "refresh") {
71
50
  await authRefresh(rest);
72
51
  }
73
- else if (command === "auth" && subcommand === "repair") {
74
- await authRepair(rest);
75
- }
76
52
  else if (command === "doctor") {
77
53
  await doctor([subcommand, ...rest].filter(Boolean));
78
54
  }
79
55
  else if (command === "init") {
80
56
  await init();
81
57
  }
82
- else if (command === "login:device") {
83
- await loginDevice([subcommand, ...rest].filter(Boolean));
84
- }
85
58
  else if (command === "login") {
86
59
  await login([subcommand, ...rest].filter(Boolean));
87
60
  }
@@ -181,7 +154,7 @@ function toCliError(error) {
181
154
  return { code: "not_logged_in", message, nextStep: "blocks-os login" };
182
155
  }
183
156
  if (message.includes("token expired") || message.includes("refresh token") || message.includes("cannot decrypt")) {
184
- return { code: "auth_repair_required", message, nextStep: "blocks-os auth:repair --yes && blocks-os auth:add --client-id <id> --client-secret <secret> && blocks-os login" };
157
+ return { code: "auth_repair_required", message, nextStep: "blocks-os login" };
185
158
  }
186
159
  if (message.includes("No project selected")) {
187
160
  return { code: "project_not_selected", message, nextStep: "blocks-os use <tenantId>" };
@@ -216,41 +189,22 @@ Setup and health:
216
189
  file locations. Does not mutate cloud resources.
217
190
 
218
191
  Auth:
219
- blocks-os auth:add [account] --client-id <id> --client-secret <secret>
220
- Save OIDC client settings for the CLI. Defaults target production Blocks
221
- URLs, root tenant, scope, and localhost callback. Secret is stored separately.
222
-
223
192
  blocks-os login
224
- Browser Authorization Code + PKCE login. Opens IAM, captures the localhost
225
- callback, stores account access and refresh tokens, and auto-refreshes later.
226
-
227
- blocks-os login:device
228
- Device-code login for terminals or agents where a browser callback is not
229
- convenient. Uses the same saved account profile.
193
+ Device-code login. Prints a verification URL and user code, opens the
194
+ browser to the verification page when possible so you only need to click
195
+ approve, then polls until the device is authorized; stores account access
196
+ and refresh tokens and auto-refreshes later.
230
197
 
231
198
  blocks-os auth:status [--json]
232
- Show configured account, token freshness, refresh-token availability, and
233
- selected project without printing secrets.
199
+ Show only whether account/project access and refresh tokens are missing,
200
+ valid, expired, or available. Does not print account config values.
234
201
 
235
202
  blocks-os auth:refresh [--project] [--json]
236
203
  Force account token refresh, or project token refresh with --project.
237
204
 
238
- blocks-os auth:repair [account] [--dry-run] [--yes] [--json]
239
- Clear broken local secure-store auth state while keeping account profile
240
- config. Use after OS credential migration, stale DPAPI, or corrupted tokens.
241
-
242
- blocks-os auth:list [--json]
243
- List saved account profiles without showing secrets.
244
-
245
- blocks-os auth:show [account]
246
- Print one account profile with secret redacted as configured/missing.
247
-
248
- blocks-os auth:use <account>
249
- Switch the active named account profile.
250
-
251
205
  blocks-os auth:remove <account>
252
- Remove account config, cached tokens, and stored client secret for that
253
- account.
206
+ Clear cached tokens and stored local credentials for that account. The
207
+ packaged default OS account is restored from package defaults.
254
208
 
255
209
  blocks-os logout
256
210
  Revoke the current refresh token when possible and remove local session data.
@@ -334,7 +288,7 @@ Release:
334
288
  Alias for release:status. Read-only.
335
289
 
336
290
  Scaffold:
337
- blocks-os new web <name> --x-blocks-key <tenantId> --app-domain <domain> --blocks-api-url <url> [--client-id <oidcClientId>] [--oidc-url <url>]
291
+ blocks-os new web <name> --x-blocks-key <tenantId> --app-domain <domain> [--client-id <oidcClientId>]
338
292
  Create a Vite React starter app with a real browser Authorization Code +
339
293
  PKCE login flow against Blocks IAM (login page, /login/callback handler,
340
294
  route guards), plus Data schema listing, Release build lookup, environment
@@ -20,18 +20,11 @@ export type DeviceAuthorizationResponse = {
20
20
  verification_uri: string;
21
21
  verification_uri_complete?: string;
22
22
  };
23
- export declare function exchangeAuthorizationCode(args: {
24
- account: string;
25
- clientId: string;
26
- clientSecret?: string;
27
- code: string;
28
- codeVerifier?: string;
29
- oidcUrl: string;
30
- redirectUri: string;
31
- rootTenantId?: string;
32
- }): Promise<TokenResponse>;
23
+ export type DevicePollingOptions = {
24
+ onWait?: (seconds: number) => void;
25
+ };
33
26
  export declare function requestDeviceAuthorization(profile: AccountProfile): Promise<DeviceAuthorizationResponse>;
34
- export declare function pollDeviceToken(profile: AccountProfile, device: DeviceAuthorizationResponse): Promise<TokenResponse>;
27
+ export declare function pollDeviceToken(profile: AccountProfile, device: DeviceAuthorizationResponse, options?: DevicePollingOptions): Promise<TokenResponse>;
35
28
  export declare function getAccountSession(accountOverride?: string): Promise<AccountSession>;
36
29
  export declare function selectProject(tenantId: string): Promise<void>;
37
30
  export declare function getImpersonatedProjectSession(accountOverride?: string, tenantOverride?: string): Promise<ProjectSession>;