@seliseblocks/cli-os 0.2.10 → 0.2.12
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 +11 -20
- package/LICENSE +21 -21
- package/README.md +3 -5
- package/bin/run.js +2 -2
- package/dist/commands/auth/oidc-clients/save.js +4 -1
- package/dist/commands/auth/refresh.js +21 -2
- package/dist/commands/mfa/generate.js +7 -4
- package/dist/commands/mfa/method-set.js +13 -4
- package/dist/commands/mfa/totp-enable.d.ts +3 -3
- package/dist/commands/mfa/totp-enable.js +4 -4
- package/dist/commands/mfa/verify.js +4 -2
- package/dist/commands/new/web.js +3 -2
- package/dist/commands/projects/create.js +110 -16
- package/dist/index.js +34 -40
- package/dist/lib/oidc-discovery.d.ts +2 -0
- package/dist/lib/oidc-discovery.js +12 -0
- package/package.json +47 -47
- package/dist/commands/sdk/client.d.ts +0 -1
- package/dist/commands/sdk/client.js +0 -99
- package/dist/commands/skill/add.d.ts +0 -1
- package/dist/commands/skill/add.js +0 -19
- package/dist/commands/skill/list.d.ts +0 -1
- package/dist/commands/skill/list.js +0 -15
- package/dist/commands/skill/show.d.ts +0 -1
- package/dist/commands/skill/show.js +0 -15
- package/dist/lib/skills.d.ts +0 -17
- package/dist/lib/skills.js +0 -69
- package/dist/skills/blocks-data-gateway-configuration/SKILL.md +0 -204
- package/dist/skills/blocks-data-gateway-crud/SKILL.md +0 -223
- package/dist/skills/blocks-data-storage/SKILL.md +0 -253
- package/dist/skills/blocks-data-storage/flows/object-management.md +0 -124
- package/dist/skills/blocks-frontend-local-https/SKILL.md +0 -100
- package/dist/skills/blocks-iam-access-control/SKILL.md +0 -49
- package/dist/skills/blocks-iam-access-control/flows/feature-gating.md +0 -38
- package/dist/skills/blocks-iam-access-control/flows/manage-roles-permissions.md +0 -110
- package/dist/skills/blocks-iam-account/SKILL.md +0 -169
- package/dist/skills/blocks-iam-mfa/SKILL.md +0 -124
- package/dist/skills/blocks-iam-organizations/SKILL.md +0 -43
- package/dist/skills/blocks-iam-organizations/flows/admin-mutations.md +0 -89
- package/dist/skills/blocks-iam-organizations/flows/read-and-switch.md +0 -57
- package/dist/skills/blocks-iam-sso-oidc-configuration/SKILL.md +0 -105
- package/dist/skills/blocks-iam-sso-oidc-implementation/SKILL.md +0 -80
- package/dist/skills/blocks-iam-users/SKILL.md +0 -131
- package/dist/skills/blocks-localization-configuration/SKILL.md +0 -149
- package/dist/skills/blocks-localization-implementation/SKILL.md +0 -63
- package/dist/skills/blocks-mail/SKILL.md +0 -95
- package/dist/skills/blocks-notification/SKILL.md +0 -69
- package/dist/skills/blocks-notifier/SKILL.md +0 -107
- package/dist/skills/blocks-onboarding/SKILL.md +0 -77
- package/dist/skills/blocks-release-deployment/SKILL.md +0 -81
- package/dist/skills/blocks-secrets/SKILL.md +0 -81
- package/dist/skills/blocks-storage-configuration/SKILL.md +0 -93
- package/dist/skills/lint.mjs +0 -168
package/AI_USAGE_GUIDE.md
CHANGED
|
@@ -82,7 +82,13 @@ List projects:
|
|
|
82
82
|
blocks projects list --json
|
|
83
83
|
```
|
|
84
84
|
|
|
85
|
-
|
|
85
|
+
Create a project when none suitable exists (ask the user first - it accepts the Blocks terms on their behalf):
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
blocks projects create "<project name>" --json # add --dry-run first to show the payload
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
It always creates exactly one application in the `dev` environment; environment, domain, cookie domain, and production flag are fixed. Adding further environments (`test`, `stg`, `prod`, ...) to an existing project is still portal-only. The command does not select the new project - run `blocks use <tenantId>` with the `tenantId` it prints.
|
|
86
92
|
|
|
87
93
|
Select a project:
|
|
88
94
|
|
|
@@ -128,8 +134,7 @@ Then run with explicit flags so no prompt is reached:
|
|
|
128
134
|
blocks new web <appName> --x-blocks-key <projectTenantId> --app-domain <appDomainOrUrl> --client-id <publicOidcClientId>
|
|
129
135
|
```
|
|
130
136
|
|
|
131
|
-
`new web` also accepts `--blocks-api-url <url>` and `--oidc-url <url
|
|
132
|
-
below. When `--blocks-api-url` is omitted, the scaffold derives it from the app domain as
|
|
137
|
+
`new web` also accepts `--blocks-api-url <url>` and `--oidc-url <url>`. When `--blocks-api-url` is omitted, the scaffold derives it from the app domain as
|
|
133
138
|
`https://blocksapi.<registrable-domain>`; for example `https://dqrsf.slsblx.com` becomes
|
|
134
139
|
`https://blocksapi.slsblx.com`. Pass `--blocks-api-url` only when targeting a non-default Blocks gateway. `--oidc-url` defaults to `https://iam.seliseblocks.com`.
|
|
135
140
|
|
|
@@ -147,7 +152,7 @@ Do not pass CLI auth state to the scaffolded app. Browser apps must use a public
|
|
|
147
152
|
|
|
148
153
|
For local browser login on the real host domain:
|
|
149
154
|
|
|
150
|
-
1. Add `127.0.0.1 <VITE_BLOCKS_DEV_HOST>` to the hosts file.
|
|
155
|
+
1. Add `127.0.0.1 <VITE_BLOCKS_DEV_HOST>` to the hosts file **yourself** (elevated on Windows, `sudo` elsewhere), then tell the user it was added - never stop and ask the user to edit the hosts file by hand. The blocks-frontend-local-https skill has the idempotent add-and-verify commands for each OS.
|
|
151
156
|
2. Run `npm install`.
|
|
152
157
|
3. Run `npm run cert`.
|
|
153
158
|
4. Run `npm run dev`.
|
|
@@ -155,20 +160,6 @@ For local browser login on the real host domain:
|
|
|
155
160
|
|
|
156
161
|
The generated cert script uses the `selfsigned` Node dependency, so it works from normal PowerShell after `npm install`; do not tell Windows users to switch to Git Bash just for OpenSSL. If hosted login or secure cookies fail locally, confirm the app is opened with the HTTPS dev URL from `VITE_BLOCKS_DEV_HOST`.
|
|
157
162
|
|
|
158
|
-
## SDK Client (read-only)
|
|
159
|
-
|
|
160
|
-
`sdk client` answers "I want to use the Blocks SDK - show me the client." It resolves this project's `@seliseblocks/client` config (same values `new web` scaffolds an app with) and prints a ready-to-paste `createBlocksClient(...)` snippet - **it never writes a file or mutates anything**. To scaffold a full app instead, use `new web` above.
|
|
161
|
-
|
|
162
|
-
```bash
|
|
163
|
-
blocks sdk client --x-blocks-key <projectTenantId> --app-domain <appDomainOrUrl> --client-id <publicOidcClientId> --blocks-api-url https://api.seliseblocks.com
|
|
164
|
-
```
|
|
165
|
-
|
|
166
|
-
Unlike `new web`, `sdk client` keeps `--blocks-api-url` defaulted to `https://api.seliseblocks.com`; only pass it explicitly if your project uses a different gateway URL. Passing both `--app-domain` and `--client-id` skips the project lookup entirely, so it needs no CLI login at all - useful for a quick, non-interactive check. Omit either one and it resolves from the selected project instead (auto-picks when there's exactly one match, otherwise lists the options and asks you to pass the flag explicitly - it does not prompt or create anything, since this command is read-only). Use `--json` for the resolved values instead of the snippet.
|
|
167
|
-
|
|
168
|
-
## Skills
|
|
169
|
-
|
|
170
|
-
`skill list [--json]` / `skill show <name> [--json]` / `skill add <name> [--dir <path>]` read this package's bundled copy of `blocks-skills/*/SKILL.md` - local-only, no cloud calls. `skill add` copies a skill's **entire directory** (`SKILL.md` plus any supporting files, e.g. `flows/*.md`) into `<dir>/<name>/` (default `./blocks-skills`) in the current directory, for pulling a single skill into a project outside this monorepo. `skill list`'s human-readable output (and the "unknown skill" error from `show`/`add`) both point at the full public skill catalog, in case the locally bundled set is out of date. As with any skill file, verify command names against this guide or `blocks --help` before running them - skills are conversational context, not command ground truth.
|
|
171
|
-
|
|
172
163
|
## IAM, MFA, and Auth Admin
|
|
173
164
|
|
|
174
165
|
`iam me` reads the CLI operator's own account identity (bootstrapping, not a project resource):
|
|
@@ -187,8 +178,8 @@ Command families (run `blocks --help` for the full flag reference on each):
|
|
|
187
178
|
- `iam resources *` - resource groups and feature flags (read-only).
|
|
188
179
|
- `iam organizations *` - list/get/create/update, `my`, and organization config get/save.
|
|
189
180
|
- `iam signup-settings *` - get/save tenant signup policy.
|
|
190
|
-
- `mfa config *`, `mfa totp *`, `mfa generate`/`resend`/`verify`, `mfa method set`, `mfa disable`, `mfa backup-codes *` - tenant MFA policy plus enrollment/verification/backup-code flows.
|
|
191
|
-
- `mfa totp enable --mfa-type <n>` - composed TOTP enrollment: `totp setup` → prints the QR/secret → `totp verify-setup` → `method set` → `backup-codes generate`, one confirmation. Prefer this over running the individual steps. `--mfa-type` is required and not defaulted -
|
|
181
|
+
- `mfa config *`, `mfa totp *`, `mfa generate`/`resend`/`verify`, `mfa method set`, `mfa disable`, `mfa backup-codes *` - tenant MFA policy plus enrollment/verification/backup-code flows. `mfa method set` only switches on `1`/`2`; every other value makes IAM disable the user's MFA. A tenant policy with `enableMfa` but an empty `userMfaType` list never actually requires MFA at login.
|
|
182
|
+
- `mfa totp enable --mfa-type <n>` - composed TOTP enrollment: `totp setup` → prints the QR/secret → `totp verify-setup` → `method set` → `backup-codes generate`, one confirmation. Prefer this over running the individual steps. `--mfa-type` is required and not defaulted - pass `1`, IAM's `UserMfaType` value for TOTP (`0` None, `1` TOTP, `2` Email, `3` Sms and `4` WhatsApp are declared but have no provider). The same enum drives `--auth-type`, `--user-mfa-type`, and a client's `--allowed-mfa-methods`. **Prompts interactively for the verification code unless `--code <c>` is given** - an agent running this non-interactively must supply `--code` (from wherever the user's authenticator app output is captured) or it will hang waiting on stdin. Deliberately excludes `mfa config save` (a separate tenant-wide admin policy, not part of one user's enrollment).
|
|
192
183
|
- `auth idp *` - identity provider (SSO/OIDC) configuration: list/get/create/update/delete/status.
|
|
193
184
|
- `auth config *` - AuthController tenant config (token lifetimes, lockout policy, etc.).
|
|
194
185
|
- `auth client-credentials *` - machine-to-machine client credentials: list/save/delete.
|
package/LICENSE
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) SELISE Blocks
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) SELISE Blocks
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -58,13 +58,13 @@ Global options available on every command:
|
|
|
58
58
|
| `blocks logout` | Revoke the current refresh token when possible and remove local session data. |
|
|
59
59
|
| `blocks projects list [--json]` | List accessible Blocks projects via `/os/v4/Project/Gets` using the account token. Read-only. |
|
|
60
60
|
| `blocks projects get [tenantId] [--deployment] [--json]` | Read one project from `Project/Gets`. Uses selected project when `tenantId` is omitted. Pass `--deployment` to also include the environment, tenantGroupId, and linked repo assets that `release deploy` resolves internally. Read-only. |
|
|
61
|
-
| `blocks projects create` |
|
|
61
|
+
| `blocks projects create <name> [--allow-duplicate-name] [--yes] [--dry-run] [--json]` | Create a new project via `/os/v4/Project/Create` with the account token (no project needs to be selected). Always creates **exactly one application, in the `dev` environment** - environment, domain, cookie domain, and production flag are not configurable, and the domain sent is a placeholder the platform replaces with the one it assigns. Confirms first because it accepts the Blocks terms on your behalf. Refuses a name already used by another project unless `--allow-duplicate-name` is passed, then verifies the result against `Project/Gets` and prints the new `tenantId`, `tenantGroupId`, and assigned domain. Does not select the project - run `blocks use <tenantId>` next. |
|
|
62
62
|
| `blocks use <tenantId>` | Save the selected project tenant globally and in `blocks.json` when present. Does not call cloud APIs. |
|
|
63
63
|
| `blocks deselect` | Clear the selected project tenant (globally and in `blocks.json`) and drop its cached impersonation token. Use this to recover when an impersonated project token has expired or failed, then run `blocks use <tenantId>` again to reselect and re-impersonate. |
|
|
64
64
|
| `blocks iam me [--json]` | Read the current user from IAM using the account token (CLI operator identity, not a project resource). |
|
|
65
65
|
| `blocks iam users *`, `iam email available`, `iam roles *`, `iam permissions *`, `iam resources *`, `iam organizations *`, `iam signup-settings *` | Full IAM admin surface for the selected project (users, roles, permissions, resource metadata, organizations and their config, signup settings). Project-scoped: requires a selected project and always uses an impersonated project token, never the account token. Mutating commands support `--dry-run`/`--yes`; rich payloads accept `--body '<json>'`/`--file <path>` on top of common convenience flags. Run `blocks --help` for the full command/flag list. |
|
|
66
66
|
| `blocks mfa config *`, `mfa totp *`, `mfa generate`, `mfa resend`, `mfa verify`, `mfa method set`, `mfa disable`, `mfa backup-codes *` | Project-scoped MFA admin and self-service surface (tenant MFA policy, TOTP enrollment, OTP challenge/verify, method switch, backup codes). Same project-selection and impersonation-only rules as IAM above. |
|
|
67
|
-
| `blocks mfa totp enable --mfa-type <n> [--code <c>]` | Composed TOTP enrollment: `totp setup` → prints the QR/secret → prompts for the code (or `--code` to skip the prompt) → `totp verify-setup` → `method set` → `backup-codes generate`, one confirmation. `--mfa-type` is required and not defaulted -
|
|
67
|
+
| `blocks mfa totp enable --mfa-type <n> [--code <c>]` | Composed TOTP enrollment: `totp setup` → prints the QR/secret → prompts for the code (or `--code` to skip the prompt) → `totp verify-setup` → `method set` → `backup-codes generate`, one confirmation. `--mfa-type` is required and not defaulted - `1` is TOTP in IAM's `UserMfaType` enum (`0` None, `1` TOTP, `2` Email, `3` Sms, `4` WhatsApp; only 1 and 2 have a provider). Deliberately excludes `mfa config save` (a separate, tenant-wide admin policy, not part of one user's enrollment). |
|
|
68
68
|
| `blocks auth idp *`, `auth config *`, `auth client-credentials *`, `auth oidc-clients *` | Auth admin surface: identity providers, AuthController tenant config, machine-to-machine client credentials, and OIDC client app registrations. Project-scoped, impersonated project token only. `idp delete`, `client-credentials save`/`delete`, and `oidc-clients save`/`delete`/`rotate-secret` return or handle secrets shown only once - treat CLI output as sensitive and never log it. |
|
|
69
69
|
| `blocks mail config *`, `mail template *`, `mail mailbox *` | Project-scoped mail admin surface via `/os/v4/Mail/*`: SMTP/inbound configuration upsert/delete/duplicate, mail template CRUD/clone, and mailbox message reads. Impersonated project token only. `mail config save`'s `--account-password` is redacted in `--dry-run` output only. |
|
|
70
70
|
| `blocks mail send [--to a,b] [--purpose <p>] [--language <c>] ...` / `mail sendtoany [same, plus --is-test-mail]` | Send an email via `/logic/v4/Mail/Send` / `/logic/v4/Mail/SendToAny`. Impersonated project token only; `--project-key` defaults to the selected project. |
|
|
@@ -96,8 +96,6 @@ Global options available on every command:
|
|
|
96
96
|
| `blocks release builds list [repoId] [--repo-id <repoId>] [--json]` | List Release build details for a repository. When `repoId` is omitted, resolves it from the selected project's linked repo assets - auto-picked if there's exactly one, otherwise you're prompted to choose. Read-only. |
|
|
97
97
|
| `blocks release builds get <buildId> [--json]` | Alias for `release status`. Read-only. |
|
|
98
98
|
| `blocks new web <name> [--app-domain <domain>] [--client-id <oidcClientId>] [--x-blocks-key <tenantId>] [--blocks-api-url <url>] [--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. Uses the selected project (see `use`) unless `--x-blocks-key` overrides it. `--app-domain` and `--client-id` are resolved from the project record when omitted: the domain auto-picks if the project has exactly one, otherwise you're prompted to choose; the OIDC client is picked from the project's existing clients, or you can create a minimal one (display name + redirect URI) on the spot, or skip and register one later from the portal or `auth oidc-clients save`. When `--blocks-api-url` is omitted, `new web` derives it from the app domain as `https://blocksapi.<registrable-domain>`; for example `https://dqrsf.slsblx.com` becomes `https://blocksapi.slsblx.com`. Pass a different Data/IAM/Localization/OS gateway URL explicitly only if your project uses a non-default one. `--oidc-url` defaults to `https://iam.seliseblocks.com`. |
|
|
99
|
-
| `blocks skill list [--json]` / `skill show <name> [--json]` / `skill add <name> [--dir <path>]` | Local-only, no cloud calls: list/print the bundled `blocks-skills/*/SKILL.md` agent context docs, or copy a skill's entire directory (`SKILL.md` plus any supporting files) into `<dir>/<name>/` (default `./blocks-skills`) for use in a project outside this monorepo. `list`'s output and `show`/`add`'s "unknown skill" error both point at the full public skill catalog in case the bundled set is out of date. |
|
|
100
|
-
| `blocks sdk client [--app-domain <domain>] [--client-id <oidcClientId>] [--x-blocks-key <tenantId>] [--blocks-api-url <url>] [--oidc-url <url>] [--json]` | Read-only: resolves this project's `@seliseblocks/client` config and prints a ready-to-paste `createBlocksClient(...)` snippet. Unlike `new web`, the API URL defaults to `https://api.seliseblocks.com` unless `--blocks-api-url` is passed. Passing both `--app-domain` and `--client-id` skips the project lookup entirely (no login required). Writes nothing - use `new web` to scaffold a full app. |
|
|
101
99
|
|
|
102
100
|
Use `--json` on commands when AI or automation needs machine-readable output. Use `--dry-run` before mutations and `--yes` only after approval.
|
|
103
101
|
|
|
@@ -170,4 +168,4 @@ Localization dictionaries are not created by `init` - the default path is `block
|
|
|
170
168
|
- Localization covers dictionary validate/pull/push plus the full raw `/localization/v4/*` API surface (assistant, config, glossary, key, language, module). Prefer `localization key translate-and-export` over running translate/generate/export by hand.
|
|
171
169
|
- Release covers deploy trigger and build status/read commands only.
|
|
172
170
|
- No direct artifact upload unless Blocks Release adds a confirmed artifact upload API.
|
|
173
|
-
- `projects create`
|
|
171
|
+
- `projects create` creates a `dev`-only, single-application project. It cannot add environments to an existing project or create a non-`dev` one - those still go through the Blocks portal.
|
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,7 +1,9 @@
|
|
|
1
1
|
import { booleanFlag, optionalBooleanFlag, stringFlag } from "../../../lib/args.js";
|
|
2
2
|
import { blocksRequest } from "../../../lib/api.js";
|
|
3
3
|
import { confirmMutation } from "../../../lib/confirm.js";
|
|
4
|
+
import { defaults } from "../../../lib/config.js";
|
|
4
5
|
import { compact, jsonBodyFlag, listFlag } from "../../../lib/json-flag.js";
|
|
6
|
+
import { withBlocksIdentityProviderDiscovery } from "../../../lib/oidc-discovery.js";
|
|
5
7
|
import { writeOutput } from "../../../lib/output.js";
|
|
6
8
|
import { requestContext } from "../../../lib/request-context.js";
|
|
7
9
|
import { parseCommand, selectedProject } from "../../../lib/workspace.js";
|
|
@@ -37,6 +39,7 @@ export async function authOidcClientsSave(argv) {
|
|
|
37
39
|
})
|
|
38
40
|
};
|
|
39
41
|
const projectKey = await selectedProject(flags);
|
|
42
|
+
const oidcUrl = stringFlag(flags, "oidc-url", { defaultValue: defaults().oidcUrl });
|
|
40
43
|
const itemId = typeof overrides.itemId === "string" ? overrides.itemId : undefined;
|
|
41
44
|
// Saving an existing client (itemId set) replaces the whole client document
|
|
42
45
|
// rather than merging -- the portal's own Edit dialog always resubmits every
|
|
@@ -51,7 +54,7 @@ export async function authOidcClientsSave(argv) {
|
|
|
51
54
|
projectTenantId: projectKey
|
|
52
55
|
})
|
|
53
56
|
: {};
|
|
54
|
-
const body = { ...current, ...overrides };
|
|
57
|
+
const body = withBlocksIdentityProviderDiscovery({ ...current, ...overrides }, oidcUrl, projectKey);
|
|
55
58
|
if (booleanFlag(flags, "dry-run")) {
|
|
56
59
|
writeOutput({ dryRun: true, endpoint: "/iam/v4/oidc-clients", request: redactSecret(body) }, flags);
|
|
57
60
|
return;
|
|
@@ -2,6 +2,7 @@ import { parseFlags, stringFlag } from "../../lib/args.js";
|
|
|
2
2
|
import { getAccountSession, getImpersonatedProjectSession } from "../../lib/auth.js";
|
|
3
3
|
import { readConfig } from "../../lib/config.js";
|
|
4
4
|
import { readTokenStore } from "../../lib/token-store.js";
|
|
5
|
+
import { writeOutput } from "../../lib/output.js";
|
|
5
6
|
export async function authRefresh(argv) {
|
|
6
7
|
const { flags } = parseFlags(argv);
|
|
7
8
|
const accountName = stringFlag(flags, "account");
|
|
@@ -9,9 +10,17 @@ export async function authRefresh(argv) {
|
|
|
9
10
|
const config = await readConfig();
|
|
10
11
|
const store = await readTokenStore();
|
|
11
12
|
const account = await getAccountSession(accountName);
|
|
12
|
-
|
|
13
|
-
|
|
13
|
+
if (!project) {
|
|
14
|
+
// --json has to produce JSON on success too, not just on the error path, or an
|
|
15
|
+
// agent that asked for machine-readable output gets a prose line to parse.
|
|
16
|
+
if (flags.json) {
|
|
17
|
+
writeOutput({ account: account.account, accountTenant: account.accountTenant, refreshed: "account" }, flags);
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
console.log(`Account '${account.account}' session ready for tenant ${account.accountTenant}`);
|
|
21
|
+
}
|
|
14
22
|
return;
|
|
23
|
+
}
|
|
15
24
|
if (!config.selectedProject?.tenantId) {
|
|
16
25
|
throw new Error("No project selected. Run `blocks use <tenantId>` first.");
|
|
17
26
|
}
|
|
@@ -20,5 +29,15 @@ export async function authRefresh(argv) {
|
|
|
20
29
|
throw new Error("No project session exists yet. A service command must create impersonation first.");
|
|
21
30
|
}
|
|
22
31
|
const projectSession = await getImpersonatedProjectSession(account.account);
|
|
32
|
+
if (flags.json) {
|
|
33
|
+
writeOutput({
|
|
34
|
+
account: account.account,
|
|
35
|
+
accountTenant: account.accountTenant,
|
|
36
|
+
projectTenantId: projectSession.tenantId,
|
|
37
|
+
refreshed: "account+project"
|
|
38
|
+
}, flags);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
console.log(`Account '${account.account}' session ready for tenant ${account.accountTenant}`);
|
|
23
42
|
console.log(`Project session ready for tenant ${projectSession.tenantId}`);
|
|
24
43
|
}
|
|
@@ -1,13 +1,16 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { optionalIntegerFlag, stringFlag } from "../../lib/args.js";
|
|
2
2
|
import { blocksRequest } from "../../lib/api.js";
|
|
3
3
|
import { writeOutput } from "../../lib/output.js";
|
|
4
4
|
import { requestContext } from "../../lib/request-context.js";
|
|
5
5
|
import { parseCommand, selectedProject } from "../../lib/workspace.js";
|
|
6
6
|
export async function mfaGenerate(argv) {
|
|
7
7
|
const { args, flags } = parseCommand(argv);
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
// optionalIntegerFlag, not `integerFlag(...) || Number(args[0])`: mfaType 0 (None) is a
|
|
9
|
+
// valid enum value, and `0 || ...` silently fell through to the missing positional.
|
|
10
|
+
const mfaType = optionalIntegerFlag(flags, "mfa-type") ?? Number(args[0]);
|
|
11
|
+
if (!Number.isInteger(mfaType)) {
|
|
12
|
+
throw new Error("Provide --mfa-type <n> (or a positional integer). Only 1 (TOTP) and 2 (Email) have an OTP provider; IAM rejects the rest.");
|
|
13
|
+
}
|
|
11
14
|
const projectKey = await selectedProject(flags);
|
|
12
15
|
const result = await blocksRequest("/iam/v4/mfa/generate", {
|
|
13
16
|
body: {
|
|
@@ -1,13 +1,22 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { optionalIntegerFlag } from "../../lib/args.js";
|
|
2
2
|
import { blocksRequest } from "../../lib/api.js";
|
|
3
3
|
import { writeOutput } from "../../lib/output.js";
|
|
4
4
|
import { requestContext } from "../../lib/request-context.js";
|
|
5
5
|
import { parseCommand, selectedProject } from "../../lib/workspace.js";
|
|
6
6
|
export async function mfaMethodSet(argv) {
|
|
7
7
|
const { args, flags } = parseCommand(argv);
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
// optionalIntegerFlag, not `integerFlag(...) || Number(args[0])`: mfaType 0 (None) is a
|
|
9
|
+
// valid enum value, and `0 || ...` silently fell through to the missing positional.
|
|
10
|
+
const mfaType = optionalIntegerFlag(flags, "mfa-type") ?? Number(args[0]);
|
|
11
|
+
if (!Number.isInteger(mfaType)) {
|
|
12
|
+
throw new Error("Provide --mfa-type <n> (or a positional integer). IAM's UserMfaType: 0 None, 1 TOTP, 2 Email, 3 Sms, 4 WhatsApp.");
|
|
13
|
+
}
|
|
14
|
+
// IAM's PUT /mfa/method only branches on TOTP and Email; every other value falls
|
|
15
|
+
// through to its disable path and turns the user's MFA off. Say so rather than
|
|
16
|
+
// letting "switch to SMS" silently unenroll them.
|
|
17
|
+
if (mfaType !== 1 && mfaType !== 2) {
|
|
18
|
+
console.warn(`Warning: IAM only switches to 1 (TOTP) or 2 (Email). --mfa-type ${mfaType} disables MFA for this user instead -- use 'blocks mfa disable' if that is what you meant.`);
|
|
19
|
+
}
|
|
11
20
|
const projectKey = await selectedProject(flags);
|
|
12
21
|
const result = await blocksRequest("/iam/v4/mfa/method", {
|
|
13
22
|
body: { mfaType },
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
* method to TOTP -> generate backup codes. This is one real enrollment sitting, not four
|
|
4
4
|
* independent tasks -- nobody enables TOTP setup and comes back next week to verify it.
|
|
5
5
|
*
|
|
6
|
-
* --mfa-type is still required, not defaulted:
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* --mfa-type is still required, not defaulted: it is IAM's UserMfaType value to make active
|
|
7
|
+
* after enrollment (1 = TOTP), the same integer mfa:method:set and mfa:generate take. Keeping
|
|
8
|
+
* it explicit means the caller states which method they are enrolling, not this command.
|
|
9
9
|
*/
|
|
10
10
|
export declare function mfaTotpEnable(argv: string[]): Promise<void>;
|
|
@@ -12,15 +12,15 @@ import { mfaTotpVerifySetup } from "./totp-verify-setup.js";
|
|
|
12
12
|
* method to TOTP -> generate backup codes. This is one real enrollment sitting, not four
|
|
13
13
|
* independent tasks -- nobody enables TOTP setup and comes back next week to verify it.
|
|
14
14
|
*
|
|
15
|
-
* --mfa-type is still required, not defaulted:
|
|
16
|
-
*
|
|
17
|
-
*
|
|
15
|
+
* --mfa-type is still required, not defaulted: it is IAM's UserMfaType value to make active
|
|
16
|
+
* after enrollment (1 = TOTP), the same integer mfa:method:set and mfa:generate take. Keeping
|
|
17
|
+
* it explicit means the caller states which method they are enrolling, not this command.
|
|
18
18
|
*/
|
|
19
19
|
export async function mfaTotpEnable(argv) {
|
|
20
20
|
const { flags } = parseCommand(argv);
|
|
21
21
|
const mfaType = integerFlag(flags, "mfa-type", NaN);
|
|
22
22
|
if (!Number.isInteger(mfaType)) {
|
|
23
|
-
throw new Error("Provide --mfa-type <n> -- the
|
|
23
|
+
throw new Error("Provide --mfa-type <n> -- the UserMfaType value to make active after enrollment (1 is TOTP, the same value 'mfa:method:set' expects).");
|
|
24
24
|
}
|
|
25
25
|
if (booleanFlag(flags, "dry-run")) {
|
|
26
26
|
writeOutput({
|
|
@@ -8,8 +8,10 @@ export async function mfaVerify(argv) {
|
|
|
8
8
|
const mfaId = args[0] || stringFlag(flags, "mfa-id", { required: true });
|
|
9
9
|
const verificationCode = args[1] || stringFlag(flags, "code", { required: true });
|
|
10
10
|
const authType = integerFlag(flags, "auth-type", NaN);
|
|
11
|
-
if (!Number.isInteger(authType))
|
|
12
|
-
|
|
11
|
+
if (!Number.isInteger(authType)) {
|
|
12
|
+
// Same UserMfaType enum as --mfa-type -- IAM types this field as UserMfaType too.
|
|
13
|
+
throw new Error("Provide --auth-type <n> -- the method that issued the challenge: 1 TOTP, 2 Email.");
|
|
14
|
+
}
|
|
13
15
|
const projectKey = await selectedProject(flags);
|
|
14
16
|
const result = await blocksRequest("/iam/v4/mfa/verify", {
|
|
15
17
|
body: {
|
package/dist/commands/new/web.js
CHANGED
|
@@ -4,6 +4,7 @@ import { confirmMutation } from "../../lib/confirm.js";
|
|
|
4
4
|
import { defaults, readConfig, writeConfig } from "../../lib/config.js";
|
|
5
5
|
import { apiUrlFromAppDomain } from "../../lib/domains.js";
|
|
6
6
|
import { CliActionableError } from "../../lib/errors.js";
|
|
7
|
+
import { withBlocksIdentityProviderDiscovery } from "../../lib/oidc-discovery.js";
|
|
7
8
|
import { findProjectByTenantId } from "../../lib/project-info.js";
|
|
8
9
|
import { promptText, selectFromList } from "../../lib/prompt.js";
|
|
9
10
|
import { requestContext } from "../../lib/request-context.js";
|
|
@@ -187,7 +188,7 @@ async function createOidcClientInteractively(tenantId, appDomain, appName, flags
|
|
|
187
188
|
// straight to the provider via window.location.assign -- without this flag IAM
|
|
188
189
|
// shows an interstitial "continue" click on the hosted login page instead of
|
|
189
190
|
// redirecting immediately, which is dead weight for a flow the SPA already drives.
|
|
190
|
-
const body = {
|
|
191
|
+
const body = withBlocksIdentityProviderDiscovery({
|
|
191
192
|
clientDisplayName: displayName,
|
|
192
193
|
clientType: "public",
|
|
193
194
|
isActive: true,
|
|
@@ -196,7 +197,7 @@ async function createOidcClientInteractively(tenantId, appDomain, appName, flags
|
|
|
196
197
|
registerAsIdentityProvider: true,
|
|
197
198
|
requirePkce: true,
|
|
198
199
|
scope: "openid profile"
|
|
199
|
-
};
|
|
200
|
+
}, stringFlag(flags, "oidc-url", { defaultValue: defaults().oidcUrl }), tenantId);
|
|
200
201
|
await confirmMutation(flags, `Create OIDC client '${displayName}' for this project. The response's client secret (if any) is shown once.`);
|
|
201
202
|
const result = await blocksRequest("/iam/v4/oidc-clients", {
|
|
202
203
|
body,
|
|
@@ -1,42 +1,136 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
1
2
|
import { booleanFlag, stringFlag } from "../../lib/args.js";
|
|
2
3
|
import { blocksRequest } from "../../lib/api.js";
|
|
3
4
|
import { confirmMutation } from "../../lib/confirm.js";
|
|
5
|
+
import { CliActionableError } from "../../lib/errors.js";
|
|
6
|
+
import { isRecord } from "../../lib/data-response.js";
|
|
4
7
|
import { writeOutput } from "../../lib/output.js";
|
|
5
|
-
import {
|
|
8
|
+
import { listProjectGroups } from "../../lib/project-info.js";
|
|
6
9
|
import { requestContext } from "../../lib/request-context.js";
|
|
10
|
+
import { parseCommand } from "../../lib/workspace.js";
|
|
11
|
+
const CREATE_ENDPOINT = "/os/v4/Project/Create";
|
|
12
|
+
// This command creates exactly one application, always in 'dev'. More
|
|
13
|
+
// environments are added later from the portal (or by a future command that
|
|
14
|
+
// passes tenantGroupId); nothing here may widen that.
|
|
15
|
+
const ENVIRONMENT = "dev";
|
|
16
|
+
// Project/Gets builds each tenantId as the environment letter prefixed onto
|
|
17
|
+
// the tenant group id -- 'd' for dev, uppercased.
|
|
18
|
+
const ENVIRONMENT_LETTER = "d";
|
|
19
|
+
const ENVIRONMENT_TENANT_PREFIX = ENVIRONMENT_LETTER.toUpperCase();
|
|
20
|
+
// Only ever a placeholder: the server validates that the domain parses as an
|
|
21
|
+
// absolute http(s) URL and then discards it, assigning its own domain during
|
|
22
|
+
// provisioning (environment letter + tenant slug, e.g.
|
|
23
|
+
// https://dboafe.slsblx.com). Mirror that shape so a dry-run payload looks
|
|
24
|
+
// like what the platform actually hands back.
|
|
25
|
+
const PLACEHOLDER_BASE_DOMAIN = "slsblx.com";
|
|
26
|
+
const PLACEHOLDER_COOKIE_DOMAIN = "slsblx.com";
|
|
27
|
+
const NAME_MIN_LENGTH = 3;
|
|
28
|
+
const NAME_MAX_LENGTH = 100;
|
|
29
|
+
const VERIFY_ATTEMPTS = 5;
|
|
30
|
+
const VERIFY_INTERVAL_MS = 2000;
|
|
7
31
|
export async function createProject(argv) {
|
|
8
32
|
const { args, flags } = parseCommand(argv);
|
|
9
|
-
const name = args[0] || stringFlag(flags, "name"
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
33
|
+
const name = (args[0] || stringFlag(flags, "name")).trim();
|
|
34
|
+
if (!name) {
|
|
35
|
+
throw new CliActionableError("Missing project name.", "missing_project_name", 'blocks projects create "<name>"');
|
|
36
|
+
}
|
|
37
|
+
if (name.length < NAME_MIN_LENGTH || name.length > NAME_MAX_LENGTH) {
|
|
38
|
+
throw new CliActionableError(`Project name must be between ${NAME_MIN_LENGTH} and ${NAME_MAX_LENGTH} characters.`, "invalid_project_name", `blocks projects create "<${NAME_MIN_LENGTH}-${NAME_MAX_LENGTH} character name>"`);
|
|
39
|
+
}
|
|
14
40
|
const body = {
|
|
15
41
|
name,
|
|
16
42
|
isAcceptBlocksTerms: true,
|
|
17
43
|
isUseBlocksExclusively: true,
|
|
18
|
-
isProduction:
|
|
44
|
+
isProduction: false,
|
|
19
45
|
resources: [],
|
|
20
46
|
applicationContexts: [
|
|
21
47
|
{
|
|
22
|
-
cookieDomain,
|
|
23
|
-
domain,
|
|
24
|
-
environment:
|
|
48
|
+
cookieDomain: PLACEHOLDER_COOKIE_DOMAIN,
|
|
49
|
+
domain: placeholderDomain(),
|
|
50
|
+
environment: ENVIRONMENT
|
|
25
51
|
}
|
|
26
52
|
]
|
|
27
53
|
};
|
|
28
54
|
if (booleanFlag(flags, "dry-run")) {
|
|
29
|
-
writeOutput({ dryRun: true, request: body }, flags);
|
|
55
|
+
writeOutput({ dryRun: true, endpoint: CREATE_ENDPOINT, request: body }, flags);
|
|
30
56
|
return;
|
|
31
57
|
}
|
|
32
|
-
|
|
33
|
-
|
|
58
|
+
if (!booleanFlag(flags, "allow-duplicate-name")) {
|
|
59
|
+
await assertNameIsFree(name, flags);
|
|
60
|
+
}
|
|
61
|
+
await confirmMutation(flags, `Create Blocks project '${name}' with a single '${ENVIRONMENT}' environment. This accepts the Blocks terms (isAcceptBlocksTerms, isUseBlocksExclusively) on your behalf.`);
|
|
62
|
+
// No tenantGroupId in the body: omitting it is what makes this a new
|
|
63
|
+
// project. Sending one would instead add an environment to an existing
|
|
64
|
+
// project, which this command deliberately cannot do.
|
|
65
|
+
const result = await blocksRequest(CREATE_ENDPOINT, {
|
|
34
66
|
accountAuth: true,
|
|
35
67
|
...requestContext(flags),
|
|
36
68
|
body
|
|
37
69
|
});
|
|
38
|
-
|
|
70
|
+
// Project/Create answers validation failures with HTTP 200 and
|
|
71
|
+
// { isSuccess: false, errors: { property: message } }, so the status code
|
|
72
|
+
// alone never proves the project was created.
|
|
73
|
+
if (!result?.isSuccess || !result.tenantGroupId) {
|
|
74
|
+
throw new CliActionableError(`Project/Create rejected '${name}': ${formatErrors(result?.errors)}`, "project_create_failed", "blocks projects list --json");
|
|
75
|
+
}
|
|
76
|
+
const tenantGroupId = result.tenantGroupId;
|
|
77
|
+
const created = await findCreatedProject(tenantGroupId, flags);
|
|
78
|
+
const tenantId = created?.tenantId ?? `${ENVIRONMENT_TENANT_PREFIX}${tenantGroupId}`;
|
|
79
|
+
writeOutput({
|
|
80
|
+
domain: created?.applications?.[0]?.domain ?? "",
|
|
81
|
+
environment: created?.environment ?? ENVIRONMENT,
|
|
82
|
+
name: created?.name ?? name,
|
|
83
|
+
tenantGroupId,
|
|
84
|
+
tenantId,
|
|
85
|
+
verified: Boolean(created)
|
|
86
|
+
}, flags);
|
|
87
|
+
if (!flags.json) {
|
|
88
|
+
if (!created) {
|
|
89
|
+
console.log(`Project/Gets has not listed '${tenantId}' yet. Re-run 'blocks projects list' to confirm before using it.`);
|
|
90
|
+
}
|
|
91
|
+
console.log(`Next: blocks use ${tenantId}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
async function assertNameIsFree(name, flags) {
|
|
95
|
+
const groups = await listProjectGroups(flags);
|
|
96
|
+
const target = name.toLowerCase();
|
|
97
|
+
const clash = groups.some((group) => (group.name ?? "").trim().toLowerCase() === target ||
|
|
98
|
+
(group.projects ?? []).some((project) => (project.name ?? "").trim().toLowerCase() === target));
|
|
99
|
+
if (!clash)
|
|
100
|
+
return;
|
|
101
|
+
// A retried create makes a second tenant group with the same name rather
|
|
102
|
+
// than failing, so an accidental duplicate is only preventable here.
|
|
103
|
+
throw new CliActionableError(`A project named '${name}' already exists on this account.`, "project_name_taken", "blocks projects list --json, or re-run with --allow-duplicate-name to create a second project with this name");
|
|
104
|
+
}
|
|
105
|
+
// Provisioning is asynchronous (Project/Create hands the tenant to a
|
|
106
|
+
// consumer queue) but fast, so poll Project/Gets briefly instead of
|
|
107
|
+
// reporting a tenantId the platform has not published yet.
|
|
108
|
+
async function findCreatedProject(tenantGroupId, flags) {
|
|
109
|
+
const expectedTenantId = `${ENVIRONMENT_TENANT_PREFIX}${tenantGroupId}`;
|
|
110
|
+
for (let attempt = 0; attempt < VERIFY_ATTEMPTS; attempt += 1) {
|
|
111
|
+
if (attempt > 0)
|
|
112
|
+
await delay(VERIFY_INTERVAL_MS);
|
|
113
|
+
const groups = await listProjectGroups(flags).catch(() => []);
|
|
114
|
+
const group = groups.find((item) => item.tenantGroupId === tenantGroupId);
|
|
115
|
+
const project = (group?.projects ?? []).find((item) => item.tenantId === expectedTenantId || item.environment === ENVIRONMENT);
|
|
116
|
+
if (project)
|
|
117
|
+
return project;
|
|
118
|
+
}
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
121
|
+
function placeholderDomain() {
|
|
122
|
+
return `https://${ENVIRONMENT_LETTER}${randomSlug(5)}.${PLACEHOLDER_BASE_DOMAIN}`;
|
|
123
|
+
}
|
|
124
|
+
function randomSlug(length) {
|
|
125
|
+
const letters = "abcdefghijklmnopqrstuvwxyz";
|
|
126
|
+
return Array.from(randomBytes(length), (byte) => letters[byte % letters.length]).join("");
|
|
127
|
+
}
|
|
128
|
+
function formatErrors(errors) {
|
|
129
|
+
if (!isRecord(errors))
|
|
130
|
+
return "no error detail returned.";
|
|
131
|
+
const entries = Object.entries(errors).map(([key, value]) => `${key}: ${String(value)}`);
|
|
132
|
+
return entries.length > 0 ? entries.join("; ") : "no error detail returned.";
|
|
39
133
|
}
|
|
40
|
-
function
|
|
41
|
-
return
|
|
134
|
+
function delay(ms) {
|
|
135
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
42
136
|
}
|