@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.
Files changed (53) hide show
  1. package/AI_USAGE_GUIDE.md +11 -20
  2. package/LICENSE +21 -21
  3. package/README.md +3 -5
  4. package/bin/run.js +2 -2
  5. package/dist/commands/auth/oidc-clients/save.js +4 -1
  6. package/dist/commands/auth/refresh.js +21 -2
  7. package/dist/commands/mfa/generate.js +7 -4
  8. package/dist/commands/mfa/method-set.js +13 -4
  9. package/dist/commands/mfa/totp-enable.d.ts +3 -3
  10. package/dist/commands/mfa/totp-enable.js +4 -4
  11. package/dist/commands/mfa/verify.js +4 -2
  12. package/dist/commands/new/web.js +3 -2
  13. package/dist/commands/projects/create.js +110 -16
  14. package/dist/index.js +34 -40
  15. package/dist/lib/oidc-discovery.d.ts +2 -0
  16. package/dist/lib/oidc-discovery.js +12 -0
  17. package/package.json +47 -47
  18. package/dist/commands/sdk/client.d.ts +0 -1
  19. package/dist/commands/sdk/client.js +0 -99
  20. package/dist/commands/skill/add.d.ts +0 -1
  21. package/dist/commands/skill/add.js +0 -19
  22. package/dist/commands/skill/list.d.ts +0 -1
  23. package/dist/commands/skill/list.js +0 -15
  24. package/dist/commands/skill/show.d.ts +0 -1
  25. package/dist/commands/skill/show.js +0 -15
  26. package/dist/lib/skills.d.ts +0 -17
  27. package/dist/lib/skills.js +0 -69
  28. package/dist/skills/blocks-data-gateway-configuration/SKILL.md +0 -204
  29. package/dist/skills/blocks-data-gateway-crud/SKILL.md +0 -223
  30. package/dist/skills/blocks-data-storage/SKILL.md +0 -253
  31. package/dist/skills/blocks-data-storage/flows/object-management.md +0 -124
  32. package/dist/skills/blocks-frontend-local-https/SKILL.md +0 -100
  33. package/dist/skills/blocks-iam-access-control/SKILL.md +0 -49
  34. package/dist/skills/blocks-iam-access-control/flows/feature-gating.md +0 -38
  35. package/dist/skills/blocks-iam-access-control/flows/manage-roles-permissions.md +0 -110
  36. package/dist/skills/blocks-iam-account/SKILL.md +0 -169
  37. package/dist/skills/blocks-iam-mfa/SKILL.md +0 -124
  38. package/dist/skills/blocks-iam-organizations/SKILL.md +0 -43
  39. package/dist/skills/blocks-iam-organizations/flows/admin-mutations.md +0 -89
  40. package/dist/skills/blocks-iam-organizations/flows/read-and-switch.md +0 -57
  41. package/dist/skills/blocks-iam-sso-oidc-configuration/SKILL.md +0 -105
  42. package/dist/skills/blocks-iam-sso-oidc-implementation/SKILL.md +0 -80
  43. package/dist/skills/blocks-iam-users/SKILL.md +0 -131
  44. package/dist/skills/blocks-localization-configuration/SKILL.md +0 -149
  45. package/dist/skills/blocks-localization-implementation/SKILL.md +0 -63
  46. package/dist/skills/blocks-mail/SKILL.md +0 -95
  47. package/dist/skills/blocks-notification/SKILL.md +0 -69
  48. package/dist/skills/blocks-notifier/SKILL.md +0 -107
  49. package/dist/skills/blocks-onboarding/SKILL.md +0 -77
  50. package/dist/skills/blocks-release-deployment/SKILL.md +0 -81
  51. package/dist/skills/blocks-secrets/SKILL.md +0 -81
  52. package/dist/skills/blocks-storage-configuration/SKILL.md +0 -93
  53. 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
- `projects create` is currently disabled in this build (commented out pending a product decision) - do not tell users it's available, and do not try to work around its absence with a raw API call. Projects must already exist (created from the Blocks portal) before selecting one below.
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>`, same as `sdk client`
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 - the tenant-specific integer meaning "TOTP" isn't documented anywhere in this CLI; don't guess it, ask the user or check `mfa config get`. **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).
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` | **Currently disabled** (commented out, not deleted) - the dispatch entry, import, and help text were removed pending a product decision. Do not tell users this command is available. |
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 - the tenant-specific integer meaning "TOTP" isn't documented anywhere in this CLI. Deliberately excludes `mfa config save` (a separate, tenant-wide admin policy, not part of one user's enrollment). |
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` is currently disabled in this build (commented out pending a product decision) - don't reference it as available.
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
- console.log(`Account '${account.account}' session ready for tenant ${account.accountTenant}`);
13
- if (!project)
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 { integerFlag, stringFlag } from "../../lib/args.js";
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
- const mfaType = integerFlag(flags, "mfa-type", NaN) || Number(args[0]);
9
- if (!Number.isInteger(mfaType))
10
- throw new Error("Provide --mfa-type <n> (or a positional integer).");
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 { integerFlag } from "../../lib/args.js";
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
- const mfaType = integerFlag(flags, "mfa-type", NaN) || Number(args[0]);
9
- if (!Number.isInteger(mfaType))
10
- throw new Error("Provide --mfa-type <n> (or a positional integer).");
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: the numeric MFA-method value that means
7
- * "TOTP" for a given tenant isn't documented anywhere in this CLI (mfa:method:set/mfa:generate
8
- * take the same raw, tenant-defined integer), so it's not safe to guess one here.
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: the numeric MFA-method value that means
16
- * "TOTP" for a given tenant isn't documented anywhere in this CLI (mfa:method:set/mfa:generate
17
- * take the same raw, tenant-defined integer), so it's not safe to guess one here.
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 numeric MFA method value your tenant uses for TOTP (the same value 'mfa:method:set' expects).");
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
- throw new Error("Provide --auth-type <n>.");
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: {
@@ -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 { parseCommand } from "../../lib/workspace.js";
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", { required: true });
10
- const env = stringFlag(flags, "env", { defaultValue: "dev" });
11
- const domain = stringFlag(flags, "domain", { defaultValue: `https://${slug(name)}-${env}.seliseblocks.com` });
12
- const cookieDomain = stringFlag(flags, "cookie-domain", { defaultValue: "seliseblocks.com" });
13
- const production = booleanFlag(flags, "production");
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: production,
44
+ isProduction: false,
19
45
  resources: [],
20
46
  applicationContexts: [
21
47
  {
22
- cookieDomain,
23
- domain,
24
- environment: env
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
- await confirmMutation(flags, `Create Blocks project '${name}' with '${env}' environment.`);
33
- const result = await blocksRequest("/os/v4/Project/Create", {
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
- writeOutput(result, flags);
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 slug(value) {
41
- return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 40) || "blocks-app";
134
+ function delay(ms) {
135
+ return new Promise((resolve) => setTimeout(resolve, ms));
42
136
  }