@seliseblocks/cli-os 0.1.1 → 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 (41) hide show
  1. package/AI_USAGE_GUIDE.md +285 -305
  2. package/LICENSE +21 -21
  3. package/README.md +136 -159
  4. package/bin/run.js +2 -2
  5. package/dist/commands/auth/status.js +16 -51
  6. package/dist/commands/doctor.js +11 -7
  7. package/dist/commands/init.js +4 -2
  8. package/dist/commands/login.d.ts +0 -1
  9. package/dist/commands/login.js +15 -55
  10. package/dist/commands/new/web.js +1 -1
  11. package/dist/index.js +129 -175
  12. package/dist/lib/auth.d.ts +4 -11
  13. package/dist/lib/auth.js +43 -18
  14. package/dist/lib/config.d.ts +2 -2
  15. package/dist/lib/config.js +45 -13
  16. package/dist/lib/open-browser.js +26 -3
  17. package/dist/lib/secret-store.d.ts +0 -1
  18. package/dist/lib/secret-store.js +1 -4
  19. package/dist/lib/token-store.d.ts +0 -1
  20. package/dist/lib/token-store.js +1 -5
  21. package/dist/lib/token.d.ts +3 -1
  22. package/dist/lib/token.js +34 -7
  23. package/package.json +47 -47
  24. package/dist/commands/auth/add.d.ts +0 -1
  25. package/dist/commands/auth/add.js +0 -51
  26. package/dist/commands/auth/list.d.ts +0 -1
  27. package/dist/commands/auth/list.js +0 -33
  28. package/dist/commands/auth/repair.d.ts +0 -1
  29. package/dist/commands/auth/repair.js +0 -39
  30. package/dist/commands/auth/show.d.ts +0 -1
  31. package/dist/commands/auth/show.js +0 -23
  32. package/dist/commands/auth/use.d.ts +0 -1
  33. package/dist/commands/auth/use.js +0 -15
  34. package/dist/commands/login-device.d.ts +0 -1
  35. package/dist/commands/login-device.js +0 -6
  36. package/dist/lib/login-server.d.ts +0 -1
  37. package/dist/lib/login-server.js +0 -43
  38. package/dist/lib/pkce.d.ts +0 -4
  39. package/dist/lib/pkce.js +0 -9
  40. package/dist/lib/prompt.d.ts +0 -2
  41. package/dist/lib/prompt.js +0 -36
package/README.md CHANGED
@@ -1,159 +1,136 @@
1
- # Blocks OS CLI
2
-
3
- CLI for SELISE Blocks Cloud.
4
-
5
- - Package: `@seliseblocks/cli-os`
6
- - Binary: `blocks-os`
7
-
8
- ## Setup
9
-
10
- Install the npm package where you want to operate the CLI:
11
-
12
- ```bash
13
- npm install -g @seliseblocks/cli-os
14
- blocks-os --version
15
- ```
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:
35
-
36
- ```bash
37
- blocks-os login
38
- blocks-os login:device
39
- ```
40
-
41
- For source development in this repository:
42
-
43
- ```bash
44
- npm install
45
- npm run build
46
- node bin/run.js --version
47
- ```
48
-
49
- ## Commands
50
-
51
- | Command | Description |
52
- |---|---|
53
- | `blocks-os init` | Create local Blocks workspace files: `blocks.json`, data schema/rules folders, release deploy config, and `.env.example`. |
54
- | `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. |
59
- | `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. |
65
- | `blocks-os logout` | Revoke the current refresh token when possible and remove local session data. |
66
- | `blocks-os projects:list [--json]` | List accessible Blocks projects via `/os/v4/Project/Gets` using the account token. Read-only. |
67
- | `blocks-os projects:get [tenantId] [--json]` | Read one project from `Project/Gets`. Uses selected project when `tenantId` is omitted. Read-only. |
68
- | `blocks-os projects:create <name> [--env dev] [--yes] [--dry-run] [--json]` | Create a Blocks project/environment via `/os/v4/Project/Create` using the account token. Mutating; supports dry-run and confirmation. |
69
- | `blocks-os use <tenantId>` | Save the selected project tenant globally and in `blocks.json` when present. Does not call cloud APIs. |
70
- | `blocks-os iam:me [--json]` | Read the current user from IAM using the account token. This is the only IAM admin surface exposed in the MVP. |
71
- | `blocks-os data:validate [--json]` | Validate local `blocks/data/schemas/*.json` and `blocks/data/rules.json` before pushing. Local-only. |
72
- | `blocks-os data:schema:list [--json]` | List project schemas via `/data/v4/schemas` using an impersonated project token. Read-only. |
73
- | `blocks-os data:schema:pull [--json]` | Download project schemas into `blocks/data/schemas/*.json`. Writes local files only. |
74
- | `blocks-os data:schema:push [--dry-run] [--yes] [--json]` | Create or update project schemas via `/data/v4/schemas/define`. Mutating; uses POST for create and PUT for update. |
75
- | `blocks-os data:rules:pull [--json]` | Download data-access policies into `blocks/data/rules.json`. Writes local files only. |
76
- | `blocks-os data:rules:deploy [--dry-run] [--yes] [--json]` | Apply schema security and data-access policies. Mutating; supports dry-run and confirmation. |
77
- | `blocks-os data:reload [--dry-run] [--yes] [--json]` | Reload Data schema configuration so staged schema/rule changes become live. Mutating; calls POST `/data/v4/schema-configurations/reload`. |
78
- | `blocks-os localization:validate --module <name> --language <culture> [--file <path>] [--json]` | Validate a local i18n JSON dictionary. Supports nested JSON input and checks the flattened key/value set locally. |
79
- | `blocks-os localization:push --module <name> --language <culture> [--file <path>] [--route <route>] [--context <text>] [--dry-run] [--yes] [--json]` | Create or update Localization keys from local i18n JSON via `/localization/v4/Key/SaveKeys`. Creates the module first through `/localization/v4/Module/Save` when missing. |
80
- | `blocks-os localization:pull --module <name> --language <culture> [--out <path>] [--json]` | Download published cloud localization via `/localization/v4/Key/GetCloudUilmFile` and write a local JSON dictionary. |
81
- | `blocks-os release:deploy --repo-id <repoId> [--dry-run] [--yes] [--json]` | Trigger a manual Release build/deploy for a configured repository. Mutating; no artifact upload is performed by this CLI. |
82
- | `blocks-os release:status <buildId> [--json]` | Read Release build status by build id. Read-only. |
83
- | `blocks-os release:builds:list --repo-id <repoId> [--json]` | List Release build details for a repository. Read-only. |
84
- | `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`. |
86
-
87
- Use `--json` on commands when AI or automation needs machine-readable output. Use `--dry-run` before mutations and `--yes` only after approval.
88
-
89
- For agent-specific operating rules and command sequences, see [AI_USAGE_GUIDE.md](AI_USAGE_GUIDE.md).
90
-
91
- ### Scaffolded Web App Local HTTPS
92
-
93
- For `blocks-os new web`, `--app-domain` should be the app's real Blocks domain/origin, for example `https://dbpdba.seliseblocks.com`. The generated app 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`.
94
-
95
- Browser login uses the hosted Blocks IAM IdP flow and secure cookies, so local testing on the project domain must run over HTTPS:
96
-
97
- ```bash
98
- cd <appName>
99
- npm install
100
- npm run cert
101
- npm run dev
102
- ```
103
-
104
- Add the generated `VITE_BLOCKS_DEV_HOST` to your hosts file, for example:
105
-
106
- ```text
107
- 127.0.0.1 dbpdba.seliseblocks.com
108
- ```
109
-
110
- 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
-
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.
113
-
114
- Client secrets and OAuth tokens use an OS-aware credential backend:
115
-
116
- | OS | Secret backend |
117
- |---|---|
118
- | Windows | DPAPI-encrypted value in the CLI secret metadata file, scoped to the current Windows user. |
119
- | macOS | Keychain generic password for the `seliseblocks-cli-os` service. |
120
- | Linux | Secret Service through `secret-tool` when available. |
121
- | 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
-
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.
124
-
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:
126
-
127
- ```bash
128
- blocks-os auth:repair --yes
129
- blocks-os auth:add --client-id <cli-client-id> --client-secret <cli-client-secret>
130
- blocks-os login
131
- ```
132
-
133
- ## Workspace
134
-
135
- `blocks-os init` creates:
136
-
137
- ```text
138
- blocks.json
139
- blocks/
140
- data/
141
- schemas/
142
- rules.json
143
- localization/
144
- release/
145
- deploy.json
146
- .env.example
147
- ```
148
-
149
- Localization dictionaries default to `blocks/localization/<module>.<language>.json`, for example `blocks/localization/common.en.json`. AI agents can generate or update that file, run `blocks-os localization:validate`, then push it to the Localization service with `blocks-os localization:push --dry-run` followed by `--yes` after approval. Gateway v4 routes do not include an `/api` segment.
150
-
151
- `blocks-os use <tenantId>` updates the selected project in global CLI state and `blocks.json` when present.
152
-
153
- ## Boundaries
154
-
155
- - IAM is limited to `iam:me` for now.
156
- - Data covers schema/rules/reload/validate only.
157
- - Localization covers dictionary validate/pull/push through the Localization service.
158
- - Release covers deploy trigger and build status/read commands only.
159
- - No direct artifact upload unless Blocks Release adds a confirmed artifact upload API.
1
+ # Blocks OS CLI
2
+
3
+ CLI for SELISE Blocks Cloud.
4
+
5
+ - Package: `@seliseblocks/cli-os`
6
+ - Binary: `blocks-os`
7
+
8
+ ## Setup
9
+
10
+ Install the npm package where you want to operate the CLI:
11
+
12
+ ```bash
13
+ npm install -g @seliseblocks/cli-os
14
+ blocks-os --version
15
+ ```
16
+
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):
20
+
21
+ ```bash
22
+ blocks-os login
23
+ ```
24
+
25
+ For source development in this repository:
26
+
27
+ ```bash
28
+ npm install
29
+ npm run build
30
+ node bin/run.js --version
31
+ ```
32
+
33
+ ## Commands
34
+
35
+ | Command | Description |
36
+ |---|---|
37
+ | `blocks-os init` | Create local Blocks workspace files: `blocks.json`, data schema/rules folders, release deploy config, and `.env.example`. |
38
+ | `blocks-os doctor [--json]` | Check Node.js, OIDC config, token cache, selected project, and config file locations. Does not mutate cloud resources. |
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. |
41
+ | `blocks-os auth:refresh [--project] [--json]` | Force account token refresh, or project token refresh with `--project`. |
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. |
43
+ | `blocks-os logout` | Revoke the current refresh token when possible and remove local session data. |
44
+ | `blocks-os projects:list [--json]` | List accessible Blocks projects via `/os/v4/Project/Gets` using the account token. Read-only. |
45
+ | `blocks-os projects:get [tenantId] [--json]` | Read one project from `Project/Gets`. Uses selected project when `tenantId` is omitted. Read-only. |
46
+ | `blocks-os projects:create <name> [--env dev] [--yes] [--dry-run] [--json]` | Create a Blocks project/environment via `/os/v4/Project/Create` using the account token. Mutating; supports dry-run and confirmation. |
47
+ | `blocks-os use <tenantId>` | Save the selected project tenant globally and in `blocks.json` when present. Does not call cloud APIs. |
48
+ | `blocks-os iam:me [--json]` | Read the current user from IAM using the account token. This is the only IAM admin surface exposed in the MVP. |
49
+ | `blocks-os data:validate [--json]` | Validate local `blocks/data/schemas/*.json` and `blocks/data/rules.json` before pushing. Local-only. |
50
+ | `blocks-os data:schema:list [--json]` | List project schemas via `/data/v4/schemas` using an impersonated project token. Read-only. |
51
+ | `blocks-os data:schema:pull [--json]` | Download project schemas into `blocks/data/schemas/*.json`. Writes local files only. |
52
+ | `blocks-os data:schema:push [--dry-run] [--yes] [--json]` | Create or update project schemas via `/data/v4/schemas/define`. Mutating; uses POST for create and PUT for update. |
53
+ | `blocks-os data:rules:pull [--json]` | Download data-access policies into `blocks/data/rules.json`. Writes local files only. |
54
+ | `blocks-os data:rules:deploy [--dry-run] [--yes] [--json]` | Apply schema security and data-access policies. Mutating; supports dry-run and confirmation. |
55
+ | `blocks-os data:reload [--dry-run] [--yes] [--json]` | Reload Data schema configuration so staged schema/rule changes become live. Mutating; calls POST `/data/v4/schema-configurations/reload`. |
56
+ | `blocks-os localization:validate --module <name> --language <culture> [--file <path>] [--json]` | Validate a local i18n JSON dictionary. Supports nested JSON input and checks the flattened key/value set locally. |
57
+ | `blocks-os localization:push --module <name> --language <culture> [--file <path>] [--route <route>] [--context <text>] [--dry-run] [--yes] [--json]` | Create or update Localization keys from local i18n JSON via `/localization/v4/Key/SaveKeys`. Creates the module first through `/localization/v4/Module/Save` when missing. |
58
+ | `blocks-os localization:pull --module <name> --language <culture> [--out <path>] [--json]` | Download published cloud localization via `/localization/v4/Key/GetCloudUilmFile` and write a local JSON dictionary. |
59
+ | `blocks-os release:deploy --repo-id <repoId> [--dry-run] [--yes] [--json]` | Trigger a manual Release build/deploy for a configured repository. Mutating; no artifact upload is performed by this CLI. |
60
+ | `blocks-os release:status <buildId> [--json]` | Read Release build status by build id. Read-only. |
61
+ | `blocks-os release:builds:list --repo-id <repoId> [--json]` | List Release build details for a repository. Read-only. |
62
+ | `blocks-os release:builds:get <buildId> [--json]` | Alias for `release:status`. Read-only. |
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). |
64
+
65
+ Use `--json` on commands when AI or automation needs machine-readable output. Use `--dry-run` before mutations and `--yes` only after approval.
66
+
67
+ For agent-specific operating rules and command sequences, see [AI_USAGE_GUIDE.md](AI_USAGE_GUIDE.md).
68
+
69
+ ### Scaffolded Web App Local HTTPS
70
+
71
+ For `blocks-os new web`, `--app-domain` should be the app's real Blocks domain/origin, for example `https://dbpdba.seliseblocks.com`. The generated app 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`.
72
+
73
+ Browser login uses the hosted Blocks IAM IdP flow and secure cookies, so local testing on the project domain must run over HTTPS:
74
+
75
+ ```bash
76
+ cd <appName>
77
+ npm install
78
+ npm run cert
79
+ npm run dev
80
+ ```
81
+
82
+ Add the generated `VITE_BLOCKS_DEV_HOST` to your hosts file, for example:
83
+
84
+ ```text
85
+ 127.0.0.1 dbpdba.seliseblocks.com
86
+ ```
87
+
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.
89
+
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.
91
+
92
+ OAuth tokens use an OS-aware credential backend:
93
+
94
+ | OS | Credential backend |
95
+ |---|---|
96
+ | Windows | DPAPI-encrypted value in the CLI secret metadata file, scoped to the current Windows user. |
97
+ | macOS | Keychain generic password for the `seliseblocks-cli-os` service. |
98
+ | Linux | Secret Service through `secret-tool` when available. |
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. |
100
+
101
+ Use `blocks-os doctor` or `blocks-os auth:status` to see which backend is active. Tokens are never printed by CLI status commands.
102
+
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:
104
+
105
+ ```bash
106
+ blocks-os auth:remove <account>
107
+ blocks-os login
108
+ ```
109
+
110
+ ## Workspace
111
+
112
+ `blocks-os init` creates:
113
+
114
+ ```text
115
+ blocks.json
116
+ blocks/
117
+ data/
118
+ schemas/
119
+ rules.json
120
+ localization/
121
+ release/
122
+ deploy.json
123
+ .env.example
124
+ ```
125
+
126
+ Localization dictionaries default to `blocks/localization/<module>.<language>.json`, for example `blocks/localization/common.en.json`. AI agents can generate or update that file, run `blocks-os localization:validate`, then push it to the Localization service with `blocks-os localization:push --dry-run` followed by `--yes` after approval. Gateway v4 routes do not include an `/api` segment.
127
+
128
+ `blocks-os use <tenantId>` updates the selected project in global CLI state and `blocks.json` when present.
129
+
130
+ ## Boundaries
131
+
132
+ - IAM is limited to `iam:me` for now.
133
+ - Data covers schema/rules/reload/validate only.
134
+ - Localization covers dictionary validate/pull/push through the Localization service.
135
+ - Release covers deploy trigger and build status/read commands only.
136
+ - No direct artifact upload unless Blocks Release adds a confirmed artifact upload API.
package/bin/run.js CHANGED
@@ -1,3 +1,3 @@
1
1
  #!/usr/bin/env node
2
-
3
- await import("../dist/index.js");
2
+
3
+ await import("../dist/index.js");
@@ -1,71 +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
- selectedProject: config.selectedProject?.tenantId ?? null,
45
- projectAccessToken: projectToken?.accessToken ? tokenState(projectToken.expiresAt) : "not created",
46
- projectRefreshToken: projectToken?.refreshToken ? "available" : "missing"
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)
47
22
  }, flags);
48
23
  return;
49
24
  }
50
- console.log(`Active account: ${config.activeAccount ?? "missing"}`);
51
- console.log(`Account: ${name}`);
52
- console.log(`API URL: ${profile.apiUrl}`);
53
- console.log(`OIDC URL: ${profile.oidcUrl}`);
54
- console.log(`OS URL: ${profile.osUrl}`);
55
- console.log(`OIDC client id: ${profile.clientId}`);
56
- console.log(`OIDC client secret: ${clientSecret ? "configured" : "missing"}`);
57
- console.log(`Secret storage: ${secretInfo.backend} (${secretInfo.detail})`);
58
- console.log(`Token storage: ${tokenInfo.backend} (${tokenInfo.detail})`);
59
- console.log(`Scope: ${profile.scope}`);
60
- console.log(`Root tenant: ${profile.rootTenantId ?? accountToken?.accountTenant ?? "missing"}`);
61
- console.log(`Account access token: ${accountToken?.accessToken ? tokenState(accountToken.expiresAt) : "missing"}`);
62
- console.log(`Account refresh token: ${accountToken?.refreshToken ? "available" : "missing"}`);
63
- console.log(`Selected project: ${config.selectedProject?.tenantId ?? "not selected"}`);
64
- console.log(`Project access token: ${projectToken?.accessToken ? tokenState(projectToken.expiresAt) : "not created"}`);
65
- console.log(`Project refresh token: ${projectToken?.refreshToken ? "available" : "missing"}`);
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)}`);
66
29
  }
67
- function tokenState(expiresAt) {
30
+ function tokenState(token, expiresAt) {
31
+ if (!token)
32
+ return "missing";
68
33
  if (!expiresAt)
69
- return "unknown expiry";
70
- return `${isExpiring(expiresAt) ? "expired/expiring" : "fresh"} until ${expiresAt}`;
34
+ return "available";
35
+ return isExpiring(expiresAt) ? "expired" : "valid";
71
36
  }
@@ -5,11 +5,11 @@ 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();
12
- const store = await readTokenStore();
12
+ let store = await readTokenStore();
13
13
  const tokenInfo = await tokenStoreInfo();
14
14
  let hasFailure = false;
15
15
  const checks = [
@@ -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;
@@ -26,6 +25,7 @@ export async function doctor(argv = []) {
26
25
  if (accountToken?.accessToken) {
27
26
  try {
28
27
  const account = await getAccountSession(name);
28
+ store = await readTokenStore();
29
29
  sessionValid = true;
30
30
  sessionDetail = account.accountTenant;
31
31
  }
@@ -33,10 +33,11 @@ export async function doctor(argv = []) {
33
33
  sessionDetail = error.message;
34
34
  }
35
35
  }
36
+ const refreshedAccountToken = store.accounts[name]?.account;
36
37
  const projectToken = config.selectedProject?.tenantId
37
38
  ? store.accounts[name]?.projects?.[config.selectedProject.tenantId]
38
39
  : undefined;
39
- 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(accountToken?.refreshToken), detail: accountToken?.refreshToken ? "available" : "missing" }, { label: "Account access token cached", ok: Boolean(accountToken?.accessToken && !isExpiring(accountToken.expiresAt)), detail: accountToken?.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" });
40
41
  }
41
42
  let configFile = "missing";
42
43
  try {
@@ -54,13 +55,13 @@ export async function doctor(argv = []) {
54
55
  catch {
55
56
  // Native token backends do not need tokens.json to exist.
56
57
  }
57
- let secretFile = "missing";
58
+ let secretFile = secretInfoForDoctor(tokenInfo.backend);
58
59
  try {
59
60
  await access(secretPath());
60
61
  secretFile = secretPath();
61
62
  }
62
63
  catch {
63
- // keep missing state
64
+ // Native credential backends do not need secrets.json to exist.
64
65
  }
65
66
  checks.push({ label: "config file", ok: configFile !== "missing", detail: configFile === "missing" ? configPath() : configFile }, { label: "token store", ok: true, detail: `${tokenInfo.backend} (${tokenInfo.detail}) ${tokenFile}` }, { label: "secret store", ok: secretFile !== "missing", detail: secretFile === "missing" ? secretPath() : secretFile });
66
67
  for (const check of checks) {
@@ -78,3 +79,6 @@ export async function doctor(argv = []) {
78
79
  if (hasFailure)
79
80
  process.exitCode = 1;
80
81
  }
82
+ function secretInfoForDoctor(tokenBackend) {
83
+ return tokenBackend === "file" || tokenBackend === "windows-dpapi" ? "missing" : "native credential store";
84
+ }
@@ -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");