@seliseblocks/cli-os 0.2.4 → 0.2.6

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.
@@ -1,110 +1,110 @@
1
- # Flow: Creating/editing roles & permissions (sensitive)
2
-
3
- This is the write side, and it's legitimate **only in direct response to a human's explicit, in-the-moment instruction** — either that human running `blocks iam roles/permissions create|update|assign-permissions` themselves (with a `--dry-run` reviewed first), or a signed-in admin operating their own in-app admin screen. Never something an agent decides to invoke on its own initiative.
4
-
5
- ## CLI command reference
6
-
7
- All project-scoped — every command resolves the active project (`blocks use <tenantId>` or an explicit `--project <tenantId>`) and runs on an impersonated project token, not the account token `iam me` uses.
8
-
9
- ```
10
- blocks iam roles list [--page] [--page-size] [--search] [--slugs a,b] [--organization-id] [--filter '<json>'] [--json]
11
- blocks iam roles get <id> [--json]
12
- blocks iam roles create --name <n> [--slug] [--description] [--parent-role-slug] [--can-create-own] [--body '<json>'|--file <path>] [--dry-run] [--yes] [--json]
13
- blocks iam roles update <itemId> [--name] [--description] [--parent-role-slug] [--propagate-to-other-org] [--can-create-own] [--body '<json>'|--file <path>] [--dry-run] [--yes] [--json]
14
- blocks iam roles assign-permissions <slug> [--add-permissions a,b] [--remove-permissions a,b] [--organization-id] [--dry-run] [--yes] [--json]
15
- blocks iam roles assignable [--json]
16
-
17
- blocks iam permissions list [--page] [--page-size] [--search] [--type <0-3>] [--severity <0-4>] [--resource-group] [--tags a,b] [--resources a,b] [--is-built-in] [--is-archived] [--roles a,b] [--organization-id] [--filter '<json>'] [--json]
18
- blocks iam permissions get <id> [--json]
19
- blocks iam permissions create --name <n> [--type] [--description] [--resource] [--resource-group] [--tags a,b] [--severity] [--is-built-in] [--dependent-permissions a,b] [--body '<json>'|--file <path>] [--dry-run] [--yes] [--json]
20
- blocks iam permissions update <id> [same flags as create, plus --is-archived] [--dry-run] [--yes] [--json]
21
- blocks iam permissions by-severity [--json]
22
- ```
23
-
24
- Mutating commands (`create`, `update`, `assign-permissions`) follow the same discipline as every other mutating command in this CLI: pass `--dry-run` first to see the exact request body and endpoint with no network call, then re-run with `--yes` (or you'll be prompted to confirm) to actually send it.
25
-
26
- ## Two equally real surfaces
27
-
28
- The choice is about *where the human is*, not which one is "allowed":
29
-
30
- - **`blocks iam roles/permissions *` from a terminal** — a human (or an agent acting on that human's explicit, in-the-moment instruction) runs the CLI directly. `--dry-run` is the CLI's own review step; treat a clean dry-run plus the human's go-ahead as the confirmation, then run with `--yes`.
31
- - **`blocksClient.iam.roles`/`iam.permissions` from inside an app** — for building an admin settings screen. If you're building that user their own admin screen, and *they* click "Create role" through that screen, calling `iam.roles.create()` from the app's code is exactly what the SDK method is for.
32
-
33
- The actual line is not "CLI vs SDK" — it's:
34
-
35
- - **Fine:** a command run because a human just asked for this specific change (terminal or in-app), bounded by that human's own IAM permissions.
36
- - **Not fine:** an agent deciding on its own to call `create`/`update`/`assignPermissions` (CLI or SDK) — whether to "help out," to fix something it noticed, or as part of a larger task the user didn't ask it to take this specific action for.
37
-
38
- ## SDK methods (write side)
39
-
40
- All from `blocksClient.iam`, per `iam-client.ts`:
41
-
42
- - `permissions.create(request)` / `permissions.update(id, request)` — define or edit a permission.
43
- - `permissions.list(request)` — paged/filtered search.
44
- - `permissions.bySeverity()` — permissions grouped by severity, handy for a categorized picker.
45
- - `permissions.get(id)` — one permission's detail.
46
- - `roles.create(request)` / `roles.update(request)` — define or edit a role.
47
- - `roles.list(request)` / `roles.get(id)` — search / fetch one role.
48
- - `roles.assignPermissions(request)` — attach/detach permissions on a role.
49
- - `roles.assignable()` — same read method as feature-gating; also useful here to limit which roles this admin's screen lets them touch at all.
50
- - `resources.groups()` — metadata for grouping permissions by resource in the UI (e.g. a permissions picker organized by resource/module).
51
-
52
- The SDK deliberately leaves these request bodies as open `Record<string, unknown>` rather than locking you to a fixed shape — confirm exact field names against the portal or a `list()`/`get()` response before hardcoding new ones. Two fields the SDK's own types do pin down: a role's `slug` (`BlocksRole.slug`) is its stable key — use it, not `itemId`, anywhere the API expects a role reference (e.g. `assignPermissions`); a permission's `resource` and `roles[]` (`BlocksPermission`) tell you what it's scoped to and which roles already hold it.
53
-
54
- ## Confirm before mutating
55
-
56
- **Before calling any of `permissions.create`, `permissions.update`, `roles.create`, `roles.update`, or `roles.assignPermissions` from your admin screen's code, the screen itself must get an explicit, in-the-moment confirmation from the admin operating it** — mirror the discipline `blocks iam roles/permissions create|update|assign-permissions` enforces natively with `--dry-run` before `--yes`. Concretely, that means the screen should:
57
-
58
- 1. Let the admin build up the change in the UI (pick a role, check/uncheck permissions, edit a name) without calling anything yet.
59
- 2. Show a clear summary of exactly what will change — "Grant `Editor` role: + `content::publish`, − `content::archive`" — before any network call.
60
- 3. Only fire the `create`/`update`/`assignPermissions` call after the admin clicks an explicit confirm ("Save changes", "Create role") for *that specific* change.
61
-
62
- Don't collapse steps 2–3 into an auto-save on every checkbox click, and don't have an agent call these methods proactively (e.g. as part of "let me clean up your roles" or "I'll just add the permission you mentioned needing") — only in direct response to the human's own confirmed action through the screen.
63
-
64
- ## Examples
65
-
66
- SDK, wired to a confirm step:
67
-
68
- ```tsx
69
- // src/features/admin/roles/useAssignPermissions.ts
70
- import { useMutation, useQueryClient } from "@tanstack/react-query";
71
- import { blocksClient } from "../../../lib/blocks/client";
72
-
73
- export function useAssignPermissions() {
74
- const queryClient = useQueryClient();
75
- return useMutation({
76
- mutationFn: (delta: { slug: string; addPermissions: string[]; removePermissions: string[] }) =>
77
- blocksClient.iam.roles.assignPermissions(delta),
78
- onSuccess: () => queryClient.invalidateQueries({ queryKey: ["iam", "roles"] })
79
- });
80
- }
81
- ```
82
-
83
- ```tsx
84
- // in the role editor component, only after the admin reviews a diff and clicks "Save":
85
- const assign = useAssignPermissions();
86
- const onConfirmSave = () =>
87
- assign.mutate({ slug: role.slug, addPermissions: pendingAdds, removePermissions: pendingRemoves });
88
- ```
89
-
90
- The equivalent from a terminal, doing the same review-then-confirm dance the CLI enforces natively:
91
-
92
- ```
93
- blocks use my-tenant-id # or pass --project my-tenant-id on every call below
94
- blocks iam roles create --name Editor --description "Content editors" --dry-run
95
- # review the printed request body, then:
96
- blocks iam roles create --name Editor --description "Content editors" --yes
97
-
98
- blocks iam roles assign-permissions editor --add-permissions content::publish --dry-run
99
- blocks iam roles assign-permissions editor --add-permissions content::publish --yes
100
- ```
101
-
102
- ## Gotchas
103
-
104
- - **CLI mutations are project-scoped, not account-scoped** — `blocks iam roles create/update/assign-permissions` and `blocks iam permissions create/update` all require a selected project (`blocks use <tenantId>` or `--project <tenantId>`) and run against the impersonated-project token; `blocks iam me` is the one IAM command that uses the account token instead, so don't expect `iam me`'s auth context to carry over to these.
105
- - **Role hierarchy and permission assignment key off `slug`**, not `itemId` — grab it from `roles.list()`/`roles.get()` (or `blocks iam roles list/get`) before calling `assignPermissions`.
106
- - **Permission assignment ultimately uses permission `itemId`s** — the CLI resolves `resource` strings like `content::publish` before mutation; SDK/backend callers should pass permission ids directly in `addPermissions` / `removePermissions`.
107
- - **`roles.assignPermissions` is additive/subtractive** (`addPermissions[]` / `removePermissions[]` in one call), not a full-set replace — compute the delta from what's checked/unchecked, don't resend the entire permission list as "adds."
108
- - **`roles.assignable()` scopes to the caller** — always populate role pickers from it rather than `roles.list()`, so an admin can't be shown (or attempt to grant) a role above their own authority.
109
- - **Never fire a create/update/assign-permissions call — CLI or SDK — without a human confirming that specific change first** (a reviewed `--dry-run` plus explicit go-ahead on the CLI, an explicit in-UI confirm for the SDK) — no auto-provisioning "default roles," no agent-initiated cleanup of permissions, no batch edits without a per-change confirm.
110
- - **OIDC/identity-provider client provisioning is always portal-only**, independent of everything above — if a request drifts into "create an OIDC client" or "add an identity provider," that's a different skill's territory (or no skill's — send the user to the portal), not something to bolt onto this one.
1
+ # Flow: Creating/editing roles & permissions (sensitive)
2
+
3
+ This is the write side, and it's legitimate **only in direct response to a human's explicit, in-the-moment instruction** — either that human running `blocks iam roles/permissions create|update|assign-permissions` themselves (with a `--dry-run` reviewed first), or a signed-in admin operating their own in-app admin screen. Never something an agent decides to invoke on its own initiative.
4
+
5
+ ## CLI command reference
6
+
7
+ All project-scoped — every command resolves the active project (`blocks use <tenantId>` or an explicit `--project <tenantId>`) and runs on an impersonated project token, not the account token `iam me` uses.
8
+
9
+ ```
10
+ blocks iam roles list [--page] [--page-size] [--search] [--slugs a,b] [--organization-id] [--filter '<json>'] [--json]
11
+ blocks iam roles get <id> [--json]
12
+ blocks iam roles create --name <n> [--slug] [--description] [--parent-role-slug] [--can-create-own] [--body '<json>'|--file <path>] [--dry-run] [--yes] [--json]
13
+ blocks iam roles update <itemId> [--name] [--description] [--parent-role-slug] [--propagate-to-other-org] [--can-create-own] [--body '<json>'|--file <path>] [--dry-run] [--yes] [--json]
14
+ blocks iam roles assign-permissions <slug> [--add-permissions a,b] [--remove-permissions a,b] [--organization-id] [--dry-run] [--yes] [--json]
15
+ blocks iam roles assignable [--json]
16
+
17
+ blocks iam permissions list [--page] [--page-size] [--search] [--type <0-3>] [--severity <0-4>] [--resource-group] [--tags a,b] [--resources a,b] [--is-built-in] [--is-archived] [--roles a,b] [--organization-id] [--filter '<json>'] [--json]
18
+ blocks iam permissions get <id> [--json]
19
+ blocks iam permissions create --name <n> [--type] [--description] [--resource] [--resource-group] [--tags a,b] [--severity] [--is-built-in] [--dependent-permissions a,b] [--body '<json>'|--file <path>] [--dry-run] [--yes] [--json]
20
+ blocks iam permissions update <id> [same flags as create, plus --is-archived] [--dry-run] [--yes] [--json]
21
+ blocks iam permissions by-severity [--json]
22
+ ```
23
+
24
+ Mutating commands (`create`, `update`, `assign-permissions`) follow the same discipline as every other mutating command in this CLI: pass `--dry-run` first to see the exact request body and endpoint with no network call, then re-run with `--yes` (or you'll be prompted to confirm) to actually send it.
25
+
26
+ ## Two equally real surfaces
27
+
28
+ The choice is about *where the human is*, not which one is "allowed":
29
+
30
+ - **`blocks iam roles/permissions *` from a terminal** — a human (or an agent acting on that human's explicit, in-the-moment instruction) runs the CLI directly. `--dry-run` is the CLI's own review step; treat a clean dry-run plus the human's go-ahead as the confirmation, then run with `--yes`.
31
+ - **`blocksClient.iam.roles`/`iam.permissions` from inside an app** — for building an admin settings screen. If you're building that user their own admin screen, and *they* click "Create role" through that screen, calling `iam.roles.create()` from the app's code is exactly what the SDK method is for.
32
+
33
+ The actual line is not "CLI vs SDK" — it's:
34
+
35
+ - **Fine:** a command run because a human just asked for this specific change (terminal or in-app), bounded by that human's own IAM permissions.
36
+ - **Not fine:** an agent deciding on its own to call `create`/`update`/`assignPermissions` (CLI or SDK) — whether to "help out," to fix something it noticed, or as part of a larger task the user didn't ask it to take this specific action for.
37
+
38
+ ## SDK methods (write side)
39
+
40
+ All from `blocksClient.iam`, per `iam-client.ts`:
41
+
42
+ - `permissions.create(request)` / `permissions.update(id, request)` — define or edit a permission.
43
+ - `permissions.list(request)` — paged/filtered search.
44
+ - `permissions.bySeverity()` — permissions grouped by severity, handy for a categorized picker.
45
+ - `permissions.get(id)` — one permission's detail.
46
+ - `roles.create(request)` / `roles.update(request)` — define or edit a role.
47
+ - `roles.list(request)` / `roles.get(id)` — search / fetch one role.
48
+ - `roles.assignPermissions(request)` — attach/detach permissions on a role.
49
+ - `roles.assignable()` — same read method as feature-gating; also useful here to limit which roles this admin's screen lets them touch at all.
50
+ - `resources.groups()` — metadata for grouping permissions by resource in the UI (e.g. a permissions picker organized by resource/module).
51
+
52
+ The SDK deliberately leaves these request bodies as open `Record<string, unknown>` rather than locking you to a fixed shape — confirm exact field names against the portal or a `list()`/`get()` response before hardcoding new ones. Two fields the SDK's own types do pin down: a role's `slug` (`BlocksRole.slug`) is its stable key — use it, not `itemId`, anywhere the API expects a role reference (e.g. `assignPermissions`); a permission's `resource` and `roles[]` (`BlocksPermission`) tell you what it's scoped to and which roles already hold it.
53
+
54
+ ## Confirm before mutating
55
+
56
+ **Before calling any of `permissions.create`, `permissions.update`, `roles.create`, `roles.update`, or `roles.assignPermissions` from your admin screen's code, the screen itself must get an explicit, in-the-moment confirmation from the admin operating it** — mirror the discipline `blocks iam roles/permissions create|update|assign-permissions` enforces natively with `--dry-run` before `--yes`. Concretely, that means the screen should:
57
+
58
+ 1. Let the admin build up the change in the UI (pick a role, check/uncheck permissions, edit a name) without calling anything yet.
59
+ 2. Show a clear summary of exactly what will change — "Grant `Editor` role: + `content::publish`, − `content::archive`" — before any network call.
60
+ 3. Only fire the `create`/`update`/`assignPermissions` call after the admin clicks an explicit confirm ("Save changes", "Create role") for *that specific* change.
61
+
62
+ Don't collapse steps 2–3 into an auto-save on every checkbox click, and don't have an agent call these methods proactively (e.g. as part of "let me clean up your roles" or "I'll just add the permission you mentioned needing") — only in direct response to the human's own confirmed action through the screen.
63
+
64
+ ## Examples
65
+
66
+ SDK, wired to a confirm step:
67
+
68
+ ```tsx
69
+ // src/features/admin/roles/useAssignPermissions.ts
70
+ import { useMutation, useQueryClient } from "@tanstack/react-query";
71
+ import { blocksClient } from "../../../lib/blocks/client";
72
+
73
+ export function useAssignPermissions() {
74
+ const queryClient = useQueryClient();
75
+ return useMutation({
76
+ mutationFn: (delta: { slug: string; addPermissions: string[]; removePermissions: string[] }) =>
77
+ blocksClient.iam.roles.assignPermissions(delta),
78
+ onSuccess: () => queryClient.invalidateQueries({ queryKey: ["iam", "roles"] })
79
+ });
80
+ }
81
+ ```
82
+
83
+ ```tsx
84
+ // in the role editor component, only after the admin reviews a diff and clicks "Save":
85
+ const assign = useAssignPermissions();
86
+ const onConfirmSave = () =>
87
+ assign.mutate({ slug: role.slug, addPermissions: pendingAdds, removePermissions: pendingRemoves });
88
+ ```
89
+
90
+ The equivalent from a terminal, doing the same review-then-confirm dance the CLI enforces natively:
91
+
92
+ ```
93
+ blocks use my-tenant-id # or pass --project my-tenant-id on every call below
94
+ blocks iam roles create --name Editor --description "Content editors" --dry-run
95
+ # review the printed request body, then:
96
+ blocks iam roles create --name Editor --description "Content editors" --yes
97
+
98
+ blocks iam roles assign-permissions editor --add-permissions content::publish --dry-run
99
+ blocks iam roles assign-permissions editor --add-permissions content::publish --yes
100
+ ```
101
+
102
+ ## Gotchas
103
+
104
+ - **CLI mutations are project-scoped, not account-scoped** — `blocks iam roles create/update/assign-permissions` and `blocks iam permissions create/update` all require a selected project (`blocks use <tenantId>` or `--project <tenantId>`) and run against the impersonated-project token; `blocks iam me` is the one IAM command that uses the account token instead, so don't expect `iam me`'s auth context to carry over to these.
105
+ - **Role hierarchy and permission assignment key off `slug`**, not `itemId` — grab it from `roles.list()`/`roles.get()` (or `blocks iam roles list/get`) before calling `assignPermissions`.
106
+ - **Permission assignment ultimately uses permission `itemId`s** — the CLI resolves `resource` strings like `content::publish` before mutation; SDK/backend callers should pass permission ids directly in `addPermissions` / `removePermissions`.
107
+ - **`roles.assignPermissions` is additive/subtractive** (`addPermissions[]` / `removePermissions[]` in one call), not a full-set replace — compute the delta from what's checked/unchecked, don't resend the entire permission list as "adds."
108
+ - **`roles.assignable()` scopes to the caller** — always populate role pickers from it rather than `roles.list()`, so an admin can't be shown (or attempt to grant) a role above their own authority.
109
+ - **Never fire a create/update/assign-permissions call — CLI or SDK — without a human confirming that specific change first** (a reviewed `--dry-run` plus explicit go-ahead on the CLI, an explicit in-UI confirm for the SDK) — no auto-provisioning "default roles," no agent-initiated cleanup of permissions, no batch edits without a per-change confirm.
110
+ - **OIDC/identity-provider client provisioning is always portal-only**, independent of everything above — if a request drifts into "create an OIDC client" or "add an identity provider," that's a different skill's territory (or no skill's — send the user to the portal), not something to bolt onto this one.
@@ -1,124 +1,124 @@
1
- ---
2
- name: blocks-iam-mfa
3
- description: "Self-service MFA for the signed-in user's own account — TOTP enroll/verify, OTP generate/resend/verify, method switch, disable, backup codes — via `blocksClient.mfa.*` or the project-scoped `blocks mfa *` CLI, plus tenant-wide MFA policy admin (`mfa.saveConfig` / `blocks mfa config get/save`). Use for an MFA settings screen, scripted enrollment/checks, or reading/setting a tenant's MFA policy. Not for admin-forcing MFA onto another specific user."
4
- ---
5
-
6
- # Blocks IAM — MFA (Multi-Factor Authentication)
7
-
8
- Two different things live under "MFA," and this skill covers both without conflating them:
9
-
10
- 1. **Self-service enrollment/verification** — the signed-in user setting up, challenging, switching, or removing MFA on **their own** account. This is `blocksClient.mfa.*` (minus `saveConfig`) in an app, or `blocks mfa totp *` / `mfa generate` / `mfa resend` / `mfa verify` / `mfa method set` / `mfa disable` / `mfa backup-codes *` from a terminal.
11
- 2. **Tenant-wide MFA policy admin** — whether MFA is enabled/required for the tenant at all, which methods are allowed, backup-code settings, and which roles are required/exempt. This is `blocksClient.mfa.config()` / `mfa.saveConfig(request)`, or `blocks mfa config get` / `mfa config save`. It configures the tenant's rules, not any one user's enrollment state.
12
-
13
- Source of truth: the `@seliseblocks/client` SDK's `mfa` namespace and the `blocks mfa *` CLI command family — this skill surfaces their documented behavior, it doesn't add new capability.
14
-
15
- ## Scope: this vs. the other IAM skills
16
-
17
- - **This skill** — the signed-in user's own MFA enrollment/verification, and tenant-wide MFA policy configuration (`config`/`saveConfig`).
18
- - **blocks-iam-account** — the rest of the signed-in user's own account lifecycle (activation, forgot/reset/change password, logout, profile bootstrap, signup). It links here for MFA depth; don't duplicate that material in this file.
19
- - **blocks-iam-users** — an admin managing *other* users' IAM records (create, deactivate, grant/revoke access). Nothing in the SDK's `mfa` namespace or the CLI's `mfa` command family lets an admin force-enroll, reset, or disable MFA on a specific *other* user's account — the closest thing is tenant-wide, role-based policy (`mfaRequiredRoles`/`mfaExemptRoles` in `saveConfig`), which applies to a role, not a targeted user id. If a caller wants to act on another named user's MFA specifically, that capability wasn't found in this source; don't invent an endpoint for it.
20
-
21
- ## SDK — `blocksClient.mfa.*`
22
-
23
- | Method | What |
24
- |---|---|
25
- | `mfa.config()` | Reads the tenant's current MFA policy. Tenant-wide, not per-user. |
26
- | `mfa.saveConfig(request)` | Saves the tenant's MFA policy (enable/require MFA, allowed methods, backup-code settings, required/exempt roles). Admin/tenant-settings action — IAM enforces the required role, the SDK does not gate it. |
27
- | `mfa.totp.setup()` | Starts authenticator-app enrollment for the signed-in user; render IAM's returned secret/QR payload in your UI. |
28
- | `mfa.totp.verifySetup({ code })` | Confirms enrollment with the 6-digit code from the authenticator app. |
29
- | `mfa.generate({ mfaType, sendPhoneNumberAsEmailDomain? })` | Sends an email/SMS OTP challenge; IAM returns an `mfaId` to pass to `resend`/`verify`. |
30
- | `mfa.resend({ mfaId, sendPhoneNumberAsEmailDomain? })` | Re-sends a pending OTP (e.g. SMS delay, spam filtering). |
31
- | `mfa.verify({ mfaId, verificationCode, authType, isFromTokenCall? })` | Confirms an OTP or step-up challenge; set `isFromTokenCall` when verifying as part of a login/token exchange rather than a standalone check. |
32
- | `mfa.setMethod({ mfaType })` | Switches which enrolled method is active for a user with more than one enrolled. |
33
- | `mfa.disable()` | Self-service opt-out for the signed-in user, where the tenant's policy allows it. |
34
- | `mfa.backupCodes.list()` | Lists the signed-in user's backup codes (e.g. remaining-count display). |
35
- | `mfa.backupCodes.generate()` | Mints a fresh set of recovery codes. Treat the response as sensitive and show it to the user only once. |
36
- | `mfa.backupCodes.use({ code, userId })` | Consumes one backup code to complete login/step-up when the primary method is unavailable. `userId` is the signed-in user's own id, not a target for an admin acting on someone else. |
37
-
38
- ```ts
39
- // enrollment
40
- await blocksClient.mfa.totp.setup(); // render the returned secret/QR
41
- await blocksClient.mfa.totp.verifySetup({ code });
42
-
43
- // OTP challenge (email/SMS-based methods)
44
- const { mfaId } = await blocksClient.mfa.generate({ mfaType });
45
- await blocksClient.mfa.verify({ mfaId, verificationCode, authType });
46
-
47
- // tenant policy (admin screen only)
48
- const policy = await blocksClient.mfa.config();
49
- await blocksClient.mfa.saveConfig({ enableMfa: true, requireMfaForAllUsers: false });
50
- ```
51
-
52
- `mfaType` and `authType` are plain numbers defined by IAM's own contract, treated as opaque by both the SDK and CLI — their enum names aren't published anywhere. Neither documents what integer means "TOTP" vs. "SMS" vs. "email" for a given tenant; confirm the value against the tenant's actual IAM config (`mfa.config()` / `blocks mfa config get`) rather than guessing one.
53
-
54
- ## CLI — `blocks mfa *`
55
-
56
- Every `mfa` command is **project-scoped**: it requires a project already selected (`blocks use <tenantId>`) and calls IAM with an impersonated project token — never the account token. Command segments joined by a space also accept a colon form (`mfa:totp:setup`, `mfa:backup-codes:generate`, etc.) — both resolve to the same command.
57
-
58
- Tenant policy admin (reads/mutates the tenant's rules, not a user's enrollment):
59
-
60
- | Command | What |
61
- |---|---|
62
- | `blocks mfa config get [--json]` | Reads the tenant's MFA policy. |
63
- | `blocks mfa config save [--enable] [--require-for-all-users] [--allow-user-opt-out] [--allow-backup-codes] [--backup-codes-count <n>] [--user-mfa-type 0,1] [--required-roles a,b] [--exempt-roles a,b] [--body '<json>'\|--file <path>] [--dry-run] [--yes] [--json]` | Saves the tenant's MFA policy. `--body`/`--file` supplies a base JSON payload; any convenience flag also passed overwrites the matching key on top of it. Requires `--dry-run` (preview only) or `--yes`/an interactive `yes` before it executes. |
64
-
65
- Self-service enrollment, challenge, and recovery for the calling (impersonated) user:
66
-
67
- | Command | What |
68
- |---|---|
69
- | `blocks mfa totp setup [--json]` | Starts TOTP enrollment; prints IAM's secret/QR payload. |
70
- | `blocks mfa totp verify-setup <code> [--json]` | Confirms TOTP enrollment with the 6-digit code. |
71
- | `blocks mfa totp enable --mfa-type <n> [--code <c>] [--dry-run] [--yes] [--json]` | Composed enrollment — see below. |
72
- | `blocks mfa generate --mfa-type <n> [--send-phone-number-as-email-domain <domain>] [--json]` | Sends an OTP challenge; returns an `mfaId` to pass to `resend`/`verify`. |
73
- | `blocks mfa resend <mfaId> [--send-phone-number-as-email-domain <domain>] [--json]` | Re-sends a pending OTP. |
74
- | `blocks mfa verify <mfaId> <code> --auth-type <n> [--from-token-call] [--json]` | Confirms an OTP/step-up challenge. |
75
- | `blocks mfa method set --mfa-type <n> [--json]` | Switches the impersonated user's active MFA method. |
76
- | `blocks mfa disable [--dry-run] [--yes] [--json]` | Disables MFA for the impersonated user. Mutating: needs `--dry-run` or `--yes`. |
77
- | `blocks mfa backup-codes list [--json]` | Lists the impersonated user's backup codes. Read-only. |
78
- | `blocks mfa backup-codes generate [--dry-run] [--yes] [--json]` | Generates a fresh set, invalidating existing ones. Mutating: needs `--dry-run` or `--yes`. |
79
- | `blocks mfa backup-codes use <userId> <code> [--json]` | Consumes one backup code. |
80
-
81
- ```sh
82
- blocks mfa config get --json # check tenant policy before prompting enrollment
83
- blocks mfa totp setup # prints secret/QR
84
- blocks mfa totp verify-setup 123456
85
- blocks mfa method set --mfa-type 0
86
- blocks mfa backup-codes generate --dry-run # preview, no call
87
- blocks mfa backup-codes generate --yes # after explicit confirmation
88
- ```
89
-
90
- ## The composed `mfa totp enable` command
91
-
92
- `mfa totp enable --mfa-type <n>` chains the individual TOTP steps into one enrollment sitting, with one confirmation, rather than four separate commands run at different times:
93
-
94
- `totp setup` → prints the QR/secret → **verification code** (from `--code`, or an interactive prompt if omitted) → `totp verify-setup <code>` → `method set --mfa-type <n>` → `backup-codes generate --yes`.
95
-
96
- Two things worth calling out precisely, both confirmed against source:
97
-
98
- - **`--mfa-type` is required, never defaulted.** The numeric value that means "TOTP" for a given tenant isn't documented anywhere in this CLI or SDK (the same opaque-integer caveat as `mfa generate`/`method set` above) — the command throws rather than guessing one. Look it up (e.g. via `mfa config get`, or ask the user) instead of assuming a value like `0`.
99
- - **Two separate prompts can block a non-interactive run**, not just one: the command asks the operator to type `yes` before it starts (skipped by `--yes`, same as `--dry-run`), and then, independently, if `--code` isn't passed, it prompts for the verification code from stdin with no timeout — in a non-interactive/agent context with no TTY to answer it, this hangs indefinitely rather than failing fast. An agent or script running this command must pass **both** `--yes` and `--code <c>` (the code sourced from wherever the authenticator output is captured) to avoid a hang.
100
- - `--dry-run` short-circuits before either prompt and before any network call: it prints the planned step list (`mfa:totp:setup`, the scan/enter-code step, `mfa:totp:verify-setup <code>`, `mfa:method:set <n>`, `mfa:backup-codes:generate`) and exits.
101
-
102
- Deliberately excluded from this composed command: `mfa config save`. That's the separate tenant-wide policy action covered above, not part of enrolling one user.
103
-
104
- ## Gotchas
105
-
106
- - **`config`/`saveConfig` (SDK) and `mfa config get`/`config save` (CLI) are tenant policy, not a user's enrollment state.** Don't call these expecting to see or change one user's MFA status — that's every other method/command in this file.
107
- - **`mfaType`/`authType` are opaque, tenant-defined integers.** Neither the SDK's types nor the CLI documents what number means "TOTP," "SMS," or "email" — confirm against the tenant's own IAM config rather than guessing.
108
- - **`mfa totp enable` can hang waiting on stdin twice over** if run non-interactively without `--yes` and `--code` — see above. Always pass both when scripting or agent-driving this command.
109
- - **Backup codes are shown once.** Treat `backupCodes.generate()` / `mfa backup-codes generate`'s response as sensitive; there's no re-display endpoint for the raw codes, only `list()`/`backup-codes list`, which is for a remaining-count style view, not for recovering codes you didn't save.
110
- - **No admin "force MFA on this specific user" capability was found in this source.** The nearest thing is tenant-wide, role-based policy (`mfaRequiredRoles`/`mfaExemptRoles` via `saveConfig`/`config save`), which targets a role, not a user id. If a request needs a specific other user's MFA changed, that's out of this skill's scope — don't fabricate an endpoint to satisfy it.
111
- - **Every `mfa` CLI command is project-scoped and impersonation-only**, same rule as the rest of the project-scoped CLI surface — `blocks use <tenantId>` first, or commands fail with `project_not_selected`.
112
- - **Mutating CLI commands (`config save`, `disable`, `backup-codes generate`, and the composed `totp enable`) require `--dry-run` or `--yes`/an interactive `yes`** before they execute — apply the same confirm-before-mutating discipline an agent uses for any other mutating Blocks CLI command: state the exact change and get explicit go-ahead first.
113
-
114
- ## Example trigger prompts
115
-
116
- - "Let a signed-in user enroll in authenticator-app (TOTP) MFA."
117
- - "Build the MFA settings screen: enroll, switch method, disable, view backup codes."
118
- - "Check whether this tenant requires MFA before showing the enrollment prompt."
119
- - "Turn on MFA for the whole tenant and require it for the admin role."
120
- - "Send an OTP code to the user and verify what they typed."
121
- - "Let a user regenerate their MFA backup codes."
122
- - "From the terminal, enroll the current project's impersonated user in TOTP MFA end to end."
123
- - "Run TOTP enrollment non-interactively from a script" → pass both `--yes` and `--code <c>` to `blocks mfa totp enable`, never run it unattended without them.
124
- - "Read the tenant's current MFA policy from the CLI."
1
+ ---
2
+ name: blocks-iam-mfa
3
+ description: "Self-service MFA for the signed-in user's own account — TOTP enroll/verify, OTP generate/resend/verify, method switch, disable, backup codes — via `blocksClient.mfa.*` or the project-scoped `blocks mfa *` CLI, plus tenant-wide MFA policy admin (`mfa.saveConfig` / `blocks mfa config get/save`). Use for an MFA settings screen, scripted enrollment/checks, or reading/setting a tenant's MFA policy. Not for admin-forcing MFA onto another specific user."
4
+ ---
5
+
6
+ # Blocks IAM — MFA (Multi-Factor Authentication)
7
+
8
+ Two different things live under "MFA," and this skill covers both without conflating them:
9
+
10
+ 1. **Self-service enrollment/verification** — the signed-in user setting up, challenging, switching, or removing MFA on **their own** account. This is `blocksClient.mfa.*` (minus `saveConfig`) in an app, or `blocks mfa totp *` / `mfa generate` / `mfa resend` / `mfa verify` / `mfa method set` / `mfa disable` / `mfa backup-codes *` from a terminal.
11
+ 2. **Tenant-wide MFA policy admin** — whether MFA is enabled/required for the tenant at all, which methods are allowed, backup-code settings, and which roles are required/exempt. This is `blocksClient.mfa.config()` / `mfa.saveConfig(request)`, or `blocks mfa config get` / `mfa config save`. It configures the tenant's rules, not any one user's enrollment state.
12
+
13
+ Source of truth: the `@seliseblocks/client` SDK's `mfa` namespace and the `blocks mfa *` CLI command family — this skill surfaces their documented behavior, it doesn't add new capability.
14
+
15
+ ## Scope: this vs. the other IAM skills
16
+
17
+ - **This skill** — the signed-in user's own MFA enrollment/verification, and tenant-wide MFA policy configuration (`config`/`saveConfig`).
18
+ - **blocks-iam-account** — the rest of the signed-in user's own account lifecycle (activation, forgot/reset/change password, logout, profile bootstrap, signup). It links here for MFA depth; don't duplicate that material in this file.
19
+ - **blocks-iam-users** — an admin managing *other* users' IAM records (create, deactivate, grant/revoke access). Nothing in the SDK's `mfa` namespace or the CLI's `mfa` command family lets an admin force-enroll, reset, or disable MFA on a specific *other* user's account — the closest thing is tenant-wide, role-based policy (`mfaRequiredRoles`/`mfaExemptRoles` in `saveConfig`), which applies to a role, not a targeted user id. If a caller wants to act on another named user's MFA specifically, that capability wasn't found in this source; don't invent an endpoint for it.
20
+
21
+ ## SDK — `blocksClient.mfa.*`
22
+
23
+ | Method | What |
24
+ |---|---|
25
+ | `mfa.config()` | Reads the tenant's current MFA policy. Tenant-wide, not per-user. |
26
+ | `mfa.saveConfig(request)` | Saves the tenant's MFA policy (enable/require MFA, allowed methods, backup-code settings, required/exempt roles). Admin/tenant-settings action — IAM enforces the required role, the SDK does not gate it. |
27
+ | `mfa.totp.setup()` | Starts authenticator-app enrollment for the signed-in user; render IAM's returned secret/QR payload in your UI. |
28
+ | `mfa.totp.verifySetup({ code })` | Confirms enrollment with the 6-digit code from the authenticator app. |
29
+ | `mfa.generate({ mfaType, sendPhoneNumberAsEmailDomain? })` | Sends an email/SMS OTP challenge; IAM returns an `mfaId` to pass to `resend`/`verify`. |
30
+ | `mfa.resend({ mfaId, sendPhoneNumberAsEmailDomain? })` | Re-sends a pending OTP (e.g. SMS delay, spam filtering). |
31
+ | `mfa.verify({ mfaId, verificationCode, authType, isFromTokenCall? })` | Confirms an OTP or step-up challenge; set `isFromTokenCall` when verifying as part of a login/token exchange rather than a standalone check. |
32
+ | `mfa.setMethod({ mfaType })` | Switches which enrolled method is active for a user with more than one enrolled. |
33
+ | `mfa.disable()` | Self-service opt-out for the signed-in user, where the tenant's policy allows it. |
34
+ | `mfa.backupCodes.list()` | Lists the signed-in user's backup codes (e.g. remaining-count display). |
35
+ | `mfa.backupCodes.generate()` | Mints a fresh set of recovery codes. Treat the response as sensitive and show it to the user only once. |
36
+ | `mfa.backupCodes.use({ code, userId })` | Consumes one backup code to complete login/step-up when the primary method is unavailable. `userId` is the signed-in user's own id, not a target for an admin acting on someone else. |
37
+
38
+ ```ts
39
+ // enrollment
40
+ await blocksClient.mfa.totp.setup(); // render the returned secret/QR
41
+ await blocksClient.mfa.totp.verifySetup({ code });
42
+
43
+ // OTP challenge (email/SMS-based methods)
44
+ const { mfaId } = await blocksClient.mfa.generate({ mfaType });
45
+ await blocksClient.mfa.verify({ mfaId, verificationCode, authType });
46
+
47
+ // tenant policy (admin screen only)
48
+ const policy = await blocksClient.mfa.config();
49
+ await blocksClient.mfa.saveConfig({ enableMfa: true, requireMfaForAllUsers: false });
50
+ ```
51
+
52
+ `mfaType` and `authType` are plain numbers defined by IAM's own contract, treated as opaque by both the SDK and CLI — their enum names aren't published anywhere. Neither documents what integer means "TOTP" vs. "SMS" vs. "email" for a given tenant; confirm the value against the tenant's actual IAM config (`mfa.config()` / `blocks mfa config get`) rather than guessing one.
53
+
54
+ ## CLI — `blocks mfa *`
55
+
56
+ Every `mfa` command is **project-scoped**: it requires a project already selected (`blocks use <tenantId>`) and calls IAM with an impersonated project token — never the account token. Command segments joined by a space also accept a colon form (`mfa:totp:setup`, `mfa:backup-codes:generate`, etc.) — both resolve to the same command.
57
+
58
+ Tenant policy admin (reads/mutates the tenant's rules, not a user's enrollment):
59
+
60
+ | Command | What |
61
+ |---|---|
62
+ | `blocks mfa config get [--json]` | Reads the tenant's MFA policy. |
63
+ | `blocks mfa config save [--enable] [--require-for-all-users] [--allow-user-opt-out] [--allow-backup-codes] [--backup-codes-count <n>] [--user-mfa-type 0,1] [--required-roles a,b] [--exempt-roles a,b] [--body '<json>'\|--file <path>] [--dry-run] [--yes] [--json]` | Saves the tenant's MFA policy. `--body`/`--file` supplies a base JSON payload; any convenience flag also passed overwrites the matching key on top of it. Requires `--dry-run` (preview only) or `--yes`/an interactive `yes` before it executes. |
64
+
65
+ Self-service enrollment, challenge, and recovery for the calling (impersonated) user:
66
+
67
+ | Command | What |
68
+ |---|---|
69
+ | `blocks mfa totp setup [--json]` | Starts TOTP enrollment; prints IAM's secret/QR payload. |
70
+ | `blocks mfa totp verify-setup <code> [--json]` | Confirms TOTP enrollment with the 6-digit code. |
71
+ | `blocks mfa totp enable --mfa-type <n> [--code <c>] [--dry-run] [--yes] [--json]` | Composed enrollment — see below. |
72
+ | `blocks mfa generate --mfa-type <n> [--send-phone-number-as-email-domain <domain>] [--json]` | Sends an OTP challenge; returns an `mfaId` to pass to `resend`/`verify`. |
73
+ | `blocks mfa resend <mfaId> [--send-phone-number-as-email-domain <domain>] [--json]` | Re-sends a pending OTP. |
74
+ | `blocks mfa verify <mfaId> <code> --auth-type <n> [--from-token-call] [--json]` | Confirms an OTP/step-up challenge. |
75
+ | `blocks mfa method set --mfa-type <n> [--json]` | Switches the impersonated user's active MFA method. |
76
+ | `blocks mfa disable [--dry-run] [--yes] [--json]` | Disables MFA for the impersonated user. Mutating: needs `--dry-run` or `--yes`. |
77
+ | `blocks mfa backup-codes list [--json]` | Lists the impersonated user's backup codes. Read-only. |
78
+ | `blocks mfa backup-codes generate [--dry-run] [--yes] [--json]` | Generates a fresh set, invalidating existing ones. Mutating: needs `--dry-run` or `--yes`. |
79
+ | `blocks mfa backup-codes use <userId> <code> [--json]` | Consumes one backup code. |
80
+
81
+ ```sh
82
+ blocks mfa config get --json # check tenant policy before prompting enrollment
83
+ blocks mfa totp setup # prints secret/QR
84
+ blocks mfa totp verify-setup 123456
85
+ blocks mfa method set --mfa-type 0
86
+ blocks mfa backup-codes generate --dry-run # preview, no call
87
+ blocks mfa backup-codes generate --yes # after explicit confirmation
88
+ ```
89
+
90
+ ## The composed `mfa totp enable` command
91
+
92
+ `mfa totp enable --mfa-type <n>` chains the individual TOTP steps into one enrollment sitting, with one confirmation, rather than four separate commands run at different times:
93
+
94
+ `totp setup` → prints the QR/secret → **verification code** (from `--code`, or an interactive prompt if omitted) → `totp verify-setup <code>` → `method set --mfa-type <n>` → `backup-codes generate --yes`.
95
+
96
+ Two things worth calling out precisely, both confirmed against source:
97
+
98
+ - **`--mfa-type` is required, never defaulted.** The numeric value that means "TOTP" for a given tenant isn't documented anywhere in this CLI or SDK (the same opaque-integer caveat as `mfa generate`/`method set` above) — the command throws rather than guessing one. Look it up (e.g. via `mfa config get`, or ask the user) instead of assuming a value like `0`.
99
+ - **Two separate prompts can block a non-interactive run**, not just one: the command asks the operator to type `yes` before it starts (skipped by `--yes`, same as `--dry-run`), and then, independently, if `--code` isn't passed, it prompts for the verification code from stdin with no timeout — in a non-interactive/agent context with no TTY to answer it, this hangs indefinitely rather than failing fast. An agent or script running this command must pass **both** `--yes` and `--code <c>` (the code sourced from wherever the authenticator output is captured) to avoid a hang.
100
+ - `--dry-run` short-circuits before either prompt and before any network call: it prints the planned step list (`mfa:totp:setup`, the scan/enter-code step, `mfa:totp:verify-setup <code>`, `mfa:method:set <n>`, `mfa:backup-codes:generate`) and exits.
101
+
102
+ Deliberately excluded from this composed command: `mfa config save`. That's the separate tenant-wide policy action covered above, not part of enrolling one user.
103
+
104
+ ## Gotchas
105
+
106
+ - **`config`/`saveConfig` (SDK) and `mfa config get`/`config save` (CLI) are tenant policy, not a user's enrollment state.** Don't call these expecting to see or change one user's MFA status — that's every other method/command in this file.
107
+ - **`mfaType`/`authType` are opaque, tenant-defined integers.** Neither the SDK's types nor the CLI documents what number means "TOTP," "SMS," or "email" — confirm against the tenant's own IAM config rather than guessing.
108
+ - **`mfa totp enable` can hang waiting on stdin twice over** if run non-interactively without `--yes` and `--code` — see above. Always pass both when scripting or agent-driving this command.
109
+ - **Backup codes are shown once.** Treat `backupCodes.generate()` / `mfa backup-codes generate`'s response as sensitive; there's no re-display endpoint for the raw codes, only `list()`/`backup-codes list`, which is for a remaining-count style view, not for recovering codes you didn't save.
110
+ - **No admin "force MFA on this specific user" capability was found in this source.** The nearest thing is tenant-wide, role-based policy (`mfaRequiredRoles`/`mfaExemptRoles` via `saveConfig`/`config save`), which targets a role, not a user id. If a request needs a specific other user's MFA changed, that's out of this skill's scope — don't fabricate an endpoint to satisfy it.
111
+ - **Every `mfa` CLI command is project-scoped and impersonation-only**, same rule as the rest of the project-scoped CLI surface — `blocks use <tenantId>` first, or commands fail with `project_not_selected`.
112
+ - **Mutating CLI commands (`config save`, `disable`, `backup-codes generate`, and the composed `totp enable`) require `--dry-run` or `--yes`/an interactive `yes`** before they execute — apply the same confirm-before-mutating discipline an agent uses for any other mutating Blocks CLI command: state the exact change and get explicit go-ahead first.
113
+
114
+ ## Example trigger prompts
115
+
116
+ - "Let a signed-in user enroll in authenticator-app (TOTP) MFA."
117
+ - "Build the MFA settings screen: enroll, switch method, disable, view backup codes."
118
+ - "Check whether this tenant requires MFA before showing the enrollment prompt."
119
+ - "Turn on MFA for the whole tenant and require it for the admin role."
120
+ - "Send an OTP code to the user and verify what they typed."
121
+ - "Let a user regenerate their MFA backup codes."
122
+ - "From the terminal, enroll the current project's impersonated user in TOTP MFA end to end."
123
+ - "Run TOTP enrollment non-interactively from a script" → pass both `--yes` and `--code <c>` to `blocks mfa totp enable`, never run it unattended without them.
124
+ - "Read the tenant's current MFA policy from the CLI."
@@ -1,43 +1,43 @@
1
- ---
2
- name: blocks-iam-organizations
3
- description: "Work with organizations (multi-tenant workspaces) via `iam.organizations`/`iam.signupSettings`/`auth.switchOrganization` (SDK, in-app) or `blocks iam organizations *`/`iam signup-settings *` (CLI, project-scoped, `--dry-run`→`--yes`) — never raw fetch/curl. Covers the org switcher, switching active org context (SDK/app-only, no CLI equivalent), public signup policy, and — human-confirmed only — creating/editing organizations and org/signup config. Use for 'org switcher', 'switch organization', 'multi-org', 'create/update an organization', 'organization settings', 'signup settings'. Users/roles within an org are blocks-iam-users/blocks-iam-access-control; SSO/OIDC client setup is blocks-onboarding, portal-optional now."
4
- ---
5
-
6
- # Blocks IAM — Organizations
7
-
8
- Organizations are the tenancy/workspace unit inside a Blocks project. Two equally real surfaces exist for managing them:
9
-
10
- - **Inside a Blocks app** — `@seliseblocks/client` SDK calls from app code (`blocksClient` in `src/lib/blocks/client.ts`, from `blocks new web`). Never write raw `fetch`/`curl` against Blocks APIs.
11
- - **Outside an app UI** — scripting, one-off inspection, CI, an ops task — the `blocks` CLI has a real, project-scoped command surface for the same operations: `blocks iam organizations list/get/create/update/my/config get/config save` and `blocks iam signup-settings get/save`.
12
-
13
- The one thing neither surface papers over as a gap is `auth.switchOrganization` — there is no `blocks iam organizations switch` or similar; switching the *active session's* org context only makes sense from inside the app that owns that session, so it stays SDK-only.
14
-
15
- **Prerequisite:** the app is a `blocks new web` scaffold with a project selected. If auth/project state is unknown, run the blocks-onboarding skill first.
16
-
17
- ## The platform boundary — read this before writing any admin-CRUD call
18
-
19
- Full organization management (create, update, config, signup-settings) **is** exposed today, on both surfaces above. What's restricted isn't the surface but *who authorizes a mutation and when* — organizations are a tenant-isolation boundary, so creating or reconfiguring them carries the same "human explicitly directs this specific change" discipline as the portal-only OIDC provisioning in blocks-onboarding, just enforced on two different mechanisms (SDK call vs. CLI command) instead of one.
20
-
21
- Concretely: **before wiring up or invoking any `create`/`update`/`saveConfig`/`save` call — SDK or CLI — restate the exact change back to the user in plain terms (which organization, which fields, old value vs. new value) and get an explicit go-ahead.** Never something an agent calls on its own initiative, in an unattended script, or as a side effect of some other task.
22
-
23
- ## Two flows
24
-
25
- - **[flows/read-and-switch.md](flows/read-and-switch.md)** — the org switcher (`organizations.my`), switching active org context (`auth.switchOrganization`, SDK-only), and public signup policy (`signupSettings.get`). Safe, no special caveat.
26
- - **[flows/admin-mutations.md](flows/admin-mutations.md)** — creating/editing organizations, org-level config, and signup policy, on both the SDK and CLI surfaces, including the full CLI command reference. Sensitive — human-confirmed only.
27
-
28
- ## Gotchas
29
-
30
- - **The CLI has real organization/signup-settings commands** — don't tell a user there's no `blocks` command for organizations.
31
- - **The one genuine gap is `switchOrganization`** — SDK/app-only, no CLI equivalent, don't invent one.
32
- - **Confirm the payload, not just the intent**, before any mutating call — SDK or CLI. See [flows/admin-mutations.md](flows/admin-mutations.md) for the full discipline.
33
- - **Multi-org must be enabled** (`isMultiOrgEnabled`) for switching orgs to be meaningful — check this first if a user reports "switching doesn't do anything."
34
-
35
- ## Example trigger prompts
36
-
37
- - "Add an org switcher to the sidebar using the current user's organizations." → read-and-switch
38
- - "Let a multi-org user switch which organization they're working in." → read-and-switch
39
- - "Show the tenant's signup policy on our public signup page." → read-and-switch
40
- - "Build an admin screen to create a new organization." → admin-mutations (confirm the fields with the user before calling `create`)
41
- - "Turn on multi-org for this project." → admin-mutations (read `getConfig`/`config get` first, confirm the change, then save)
42
- - "Is there a `blocks` command to list organizations?" → yes, `blocks iam organizations list` (project-scoped, requires `blocks use <tenantId>` first); use the SDK instead only when the ask is a feature inside the app itself.
43
- - "Is there a CLI way to switch which organization I'm working in?" → no, that's the one gap — `auth.switchOrganization` is SDK/app-only since it mutates the calling session's own tokens.
1
+ ---
2
+ name: blocks-iam-organizations
3
+ description: "Work with organizations (multi-tenant workspaces) via `iam.organizations`/`iam.signupSettings`/`auth.switchOrganization` (SDK, in-app) or `blocks iam organizations *`/`iam signup-settings *` (CLI, project-scoped, `--dry-run`→`--yes`) — never raw fetch/curl. Covers the org switcher, switching active org context (SDK/app-only, no CLI equivalent), public signup policy, and — human-confirmed only — creating/editing organizations and org/signup config. Use for 'org switcher', 'switch organization', 'multi-org', 'create/update an organization', 'organization settings', 'signup settings'. Users/roles within an org are blocks-iam-users/blocks-iam-access-control; SSO/OIDC client setup is blocks-onboarding, portal-optional now."
4
+ ---
5
+
6
+ # Blocks IAM — Organizations
7
+
8
+ Organizations are the tenancy/workspace unit inside a Blocks project. Two equally real surfaces exist for managing them:
9
+
10
+ - **Inside a Blocks app** — `@seliseblocks/client` SDK calls from app code (`blocksClient` in `src/lib/blocks/client.ts`, from `blocks new web`). Never write raw `fetch`/`curl` against Blocks APIs.
11
+ - **Outside an app UI** — scripting, one-off inspection, CI, an ops task — the `blocks` CLI has a real, project-scoped command surface for the same operations: `blocks iam organizations list/get/create/update/my/config get/config save` and `blocks iam signup-settings get/save`.
12
+
13
+ The one thing neither surface papers over as a gap is `auth.switchOrganization` — there is no `blocks iam organizations switch` or similar; switching the *active session's* org context only makes sense from inside the app that owns that session, so it stays SDK-only.
14
+
15
+ **Prerequisite:** the app is a `blocks new web` scaffold with a project selected. If auth/project state is unknown, run the blocks-onboarding skill first.
16
+
17
+ ## The platform boundary — read this before writing any admin-CRUD call
18
+
19
+ Full organization management (create, update, config, signup-settings) **is** exposed today, on both surfaces above. What's restricted isn't the surface but *who authorizes a mutation and when* — organizations are a tenant-isolation boundary, so creating or reconfiguring them carries the same "human explicitly directs this specific change" discipline as the portal-only OIDC provisioning in blocks-onboarding, just enforced on two different mechanisms (SDK call vs. CLI command) instead of one.
20
+
21
+ Concretely: **before wiring up or invoking any `create`/`update`/`saveConfig`/`save` call — SDK or CLI — restate the exact change back to the user in plain terms (which organization, which fields, old value vs. new value) and get an explicit go-ahead.** Never something an agent calls on its own initiative, in an unattended script, or as a side effect of some other task.
22
+
23
+ ## Two flows
24
+
25
+ - **[flows/read-and-switch.md](flows/read-and-switch.md)** — the org switcher (`organizations.my`), switching active org context (`auth.switchOrganization`, SDK-only), and public signup policy (`signupSettings.get`). Safe, no special caveat.
26
+ - **[flows/admin-mutations.md](flows/admin-mutations.md)** — creating/editing organizations, org-level config, and signup policy, on both the SDK and CLI surfaces, including the full CLI command reference. Sensitive — human-confirmed only.
27
+
28
+ ## Gotchas
29
+
30
+ - **The CLI has real organization/signup-settings commands** — don't tell a user there's no `blocks` command for organizations.
31
+ - **The one genuine gap is `switchOrganization`** — SDK/app-only, no CLI equivalent, don't invent one.
32
+ - **Confirm the payload, not just the intent**, before any mutating call — SDK or CLI. See [flows/admin-mutations.md](flows/admin-mutations.md) for the full discipline.
33
+ - **Multi-org must be enabled** (`isMultiOrgEnabled`) for switching orgs to be meaningful — check this first if a user reports "switching doesn't do anything."
34
+
35
+ ## Example trigger prompts
36
+
37
+ - "Add an org switcher to the sidebar using the current user's organizations." → read-and-switch
38
+ - "Let a multi-org user switch which organization they're working in." → read-and-switch
39
+ - "Show the tenant's signup policy on our public signup page." → read-and-switch
40
+ - "Build an admin screen to create a new organization." → admin-mutations (confirm the fields with the user before calling `create`)
41
+ - "Turn on multi-org for this project." → admin-mutations (read `getConfig`/`config get` first, confirm the change, then save)
42
+ - "Is there a `blocks` command to list organizations?" → yes, `blocks iam organizations list` (project-scoped, requires `blocks use <tenantId>` first); use the SDK instead only when the ask is a feature inside the app itself.
43
+ - "Is there a CLI way to switch which organization I'm working in?" → no, that's the one gap — `auth.switchOrganization` is SDK/app-only since it mutates the calling session's own tokens.