@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.
- package/AI_USAGE_GUIDE.md +285 -305
- package/LICENSE +21 -21
- package/README.md +136 -159
- package/bin/run.js +2 -2
- package/dist/commands/auth/status.js +16 -51
- package/dist/commands/doctor.js +11 -7
- package/dist/commands/init.js +4 -2
- package/dist/commands/login.d.ts +0 -1
- package/dist/commands/login.js +15 -55
- package/dist/commands/new/web.js +1 -1
- package/dist/index.js +129 -175
- package/dist/lib/auth.d.ts +4 -11
- package/dist/lib/auth.js +43 -18
- package/dist/lib/config.d.ts +2 -2
- package/dist/lib/config.js +45 -13
- package/dist/lib/open-browser.js +26 -3
- package/dist/lib/secret-store.d.ts +0 -1
- package/dist/lib/secret-store.js +1 -4
- package/dist/lib/token-store.d.ts +0 -1
- package/dist/lib/token-store.js +1 -5
- package/dist/lib/token.d.ts +3 -1
- package/dist/lib/token.js +34 -7
- package/package.json +47 -47
- package/dist/commands/auth/add.d.ts +0 -1
- package/dist/commands/auth/add.js +0 -51
- package/dist/commands/auth/list.d.ts +0 -1
- package/dist/commands/auth/list.js +0 -33
- package/dist/commands/auth/repair.d.ts +0 -1
- package/dist/commands/auth/repair.js +0 -39
- package/dist/commands/auth/show.d.ts +0 -1
- package/dist/commands/auth/show.js +0 -23
- package/dist/commands/auth/use.d.ts +0 -1
- package/dist/commands/auth/use.js +0 -15
- package/dist/commands/login-device.d.ts +0 -1
- package/dist/commands/login-device.js +0 -6
- package/dist/lib/login-server.d.ts +0 -1
- package/dist/lib/login-server.js +0 -43
- package/dist/lib/pkce.d.ts +0 -4
- package/dist/lib/pkce.js +0 -9
- package/dist/lib/prompt.d.ts +0 -2
- 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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
blocks-os
|
|
38
|
-
blocks-os
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
|
52
|
-
|
|
53
|
-
| `blocks-os
|
|
54
|
-
| `blocks-os
|
|
55
|
-
| `blocks-os
|
|
56
|
-
| `blocks-os
|
|
57
|
-
| `blocks-os
|
|
58
|
-
| `blocks-os
|
|
59
|
-
| `blocks-os
|
|
60
|
-
| `blocks-os
|
|
61
|
-
| `blocks-os
|
|
62
|
-
| `blocks-os
|
|
63
|
-
| `blocks-os
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
blocks-os
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
|
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
|
|
12
|
-
const
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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(`
|
|
51
|
-
console.log(`Account: ${
|
|
52
|
-
console.log(`
|
|
53
|
-
console.log(`
|
|
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 "
|
|
70
|
-
return
|
|
34
|
+
return "available";
|
|
35
|
+
return isExpiring(expiresAt) ? "expired" : "valid";
|
|
71
36
|
}
|
package/dist/commands/doctor.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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
|
|
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: "
|
|
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 =
|
|
58
|
+
let secretFile = secretInfoForDoctor(tokenInfo.backend);
|
|
58
59
|
try {
|
|
59
60
|
await access(secretPath());
|
|
60
61
|
secretFile = secretPath();
|
|
61
62
|
}
|
|
62
63
|
catch {
|
|
63
|
-
//
|
|
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
|
+
}
|
package/dist/commands/init.js
CHANGED
|
@@ -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:
|
|
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
|
-
|
|
34
|
+
`VITE_BLOCKS_API_URL=${env.apiUrl}`,
|
|
33
35
|
"VITE_BLOCKS_X_BLOCKS_KEY=",
|
|
34
36
|
"VITE_BLOCKS_APP_DOMAIN=",
|
|
35
37
|
""
|
package/dist/commands/login.d.ts
CHANGED
package/dist/commands/login.js
CHANGED
|
@@ -1,75 +1,35 @@
|
|
|
1
|
-
import { URLSearchParams } from "node:url";
|
|
2
1
|
import { parseFlags, stringFlag } from "../lib/args.js";
|
|
3
|
-
import {
|
|
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 {
|
|
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(
|
|
34
|
+
console.log("Login done.");
|
|
75
35
|
}
|
package/dist/commands/new/web.js
CHANGED
|
@@ -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:
|
|
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");
|