@seliseblocks/cli-os 0.2.11 → 0.2.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AI_USAGE_GUIDE.md +551 -560
- package/README.md +171 -173
- package/dist/commands/auth/refresh.js +21 -2
- package/dist/commands/mfa/generate.js +7 -4
- package/dist/commands/mfa/method-set.js +13 -4
- package/dist/commands/mfa/totp-enable.d.ts +3 -3
- package/dist/commands/mfa/totp-enable.js +4 -4
- package/dist/commands/mfa/verify.js +4 -2
- package/dist/commands/projects/create.js +110 -16
- package/dist/index.js +696 -703
- package/package.json +2 -2
- package/dist/commands/sdk/client.d.ts +0 -1
- package/dist/commands/sdk/client.js +0 -99
- package/dist/commands/skill/add.d.ts +0 -1
- package/dist/commands/skill/add.js +0 -19
- package/dist/commands/skill/list.d.ts +0 -1
- package/dist/commands/skill/list.js +0 -15
- package/dist/commands/skill/show.d.ts +0 -1
- package/dist/commands/skill/show.js +0 -15
- package/dist/lib/skills.d.ts +0 -17
- package/dist/lib/skills.js +0 -69
- package/dist/skills/blocks-data-gateway-configuration/SKILL.md +0 -204
- package/dist/skills/blocks-data-gateway-crud/SKILL.md +0 -223
- package/dist/skills/blocks-data-storage/SKILL.md +0 -253
- package/dist/skills/blocks-data-storage/flows/object-management.md +0 -124
- package/dist/skills/blocks-frontend-local-https/SKILL.md +0 -100
- package/dist/skills/blocks-iam-access-control/SKILL.md +0 -49
- package/dist/skills/blocks-iam-access-control/flows/feature-gating.md +0 -38
- package/dist/skills/blocks-iam-access-control/flows/manage-roles-permissions.md +0 -110
- package/dist/skills/blocks-iam-account/SKILL.md +0 -169
- package/dist/skills/blocks-iam-mfa/SKILL.md +0 -124
- package/dist/skills/blocks-iam-organizations/SKILL.md +0 -43
- package/dist/skills/blocks-iam-organizations/flows/admin-mutations.md +0 -89
- package/dist/skills/blocks-iam-organizations/flows/read-and-switch.md +0 -57
- package/dist/skills/blocks-iam-sso-oidc-configuration/SKILL.md +0 -105
- package/dist/skills/blocks-iam-sso-oidc-implementation/SKILL.md +0 -80
- package/dist/skills/blocks-iam-users/SKILL.md +0 -131
- package/dist/skills/blocks-localization-configuration/SKILL.md +0 -149
- package/dist/skills/blocks-localization-implementation/SKILL.md +0 -63
- package/dist/skills/blocks-mail/SKILL.md +0 -95
- package/dist/skills/blocks-notification/SKILL.md +0 -69
- package/dist/skills/blocks-notifier/SKILL.md +0 -107
- package/dist/skills/blocks-onboarding/SKILL.md +0 -77
- package/dist/skills/blocks-release-deployment/SKILL.md +0 -81
- package/dist/skills/blocks-secrets/SKILL.md +0 -81
- package/dist/skills/blocks-storage-configuration/SKILL.md +0 -93
- package/dist/skills/lint.mjs +0 -168
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: blocks-frontend-local-https
|
|
3
|
-
description: "Run a scaffolded (`blocks new web`) Blocks app locally over HTTPS on its real project domain — required for hosted IAM login, since plain HTTP or localhost never gets the session cookie. The scaffold already automates cert generation (npm run cert, no OpenSSL needed) and HTTPS serving via vite.config.ts. Covers running that flow, trusting the cert, the hosts-file entry, and troubleshooting. Use when running a scaffolded app over HTTPS, hitting 'SSO cookie not set' / Vite 'Blocked request' errors, trusting the dev cert, or asking why local login redirects back but doesn't stay signed in."
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Blocks Frontend — Local HTTPS for a Scaffolded App
|
|
7
|
-
|
|
8
|
-
This is the local dev loop for an app already created with `blocks new web` (see the blocks-onboarding skill for the scaffold command itself — this skill doesn't repeat it). The scaffold **already generates its own cert tooling**; nothing here is built from scratch, and nothing uses raw `openssl`/`curl`/`fetch`.
|
|
9
|
-
|
|
10
|
-
## Why localhost doesn't work
|
|
11
|
-
|
|
12
|
-
Browser login goes through the hosted Blocks IAM IdP flow (`blocksClient.auth.idp.redirectToProvider()` / `.callback()`), which finishes by IAM setting a **Secure, domain-scoped session cookie**. Browsers won't store or send that cookie on plain `http://localhost` — it has to be HTTPS, and it has to be on the project's real domain, or the cookie silently never lands and the app looks "logged in" for a second then bounces back to logged-out.
|
|
13
|
-
|
|
14
|
-
## Where the domain comes from
|
|
15
|
-
|
|
16
|
-
`--app-domain` on `blocks new web` is the app's real Blocks origin, e.g. `https://dbpdba.seliseblocks.com`. The generated `.env` keeps two derived values:
|
|
17
|
-
|
|
18
|
-
- `VITE_BLOCKS_APP_DOMAIN` — the full value as passed, with scheme (`https://dbpdba.seliseblocks.com`).
|
|
19
|
-
- `VITE_BLOCKS_DEV_HOST` — the same host with no scheme (`dbpdba.seliseblocks.com`), computed by the scaffold generator and used everywhere locally: hosts file, cert `commonName`/SAN, and `vite.config.ts`'s `server.host`/`allowedHosts`.
|
|
20
|
-
|
|
21
|
-
You never need to look this up separately — it's already sitting in `.env` after scaffolding.
|
|
22
|
-
|
|
23
|
-
## The flow
|
|
24
|
-
|
|
25
|
-
```bash
|
|
26
|
-
cd <appName>
|
|
27
|
-
npm install
|
|
28
|
-
npm run cert
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
`npm run cert` runs `scripts/generate-cert.mjs`, which reads `VITE_BLOCKS_DEV_HOST` from `.env` (or `process.env`, or an explicit `npm run cert -- <domain>` argument) and uses the `selfsigned` npm dependency to write `.cert/dev-key.pem` and `.cert/dev-cert.pem`, with `subjectAltName` covering the domain, `localhost`, and `127.0.0.1`. This is pure Node — it works from a plain PowerShell prompt with no OpenSSL and no Git Bash/WSL switch.
|
|
32
|
-
|
|
33
|
-
Next, map the domain to your machine — add one line to the hosts file (needs admin/elevated):
|
|
34
|
-
|
|
35
|
-
```text
|
|
36
|
-
127.0.0.1 dbpdba.seliseblocks.com
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
Windows path: `C:\Windows\System32\drivers\etc\hosts` (edit as Administrator).
|
|
40
|
-
|
|
41
|
-
Then:
|
|
42
|
-
|
|
43
|
-
```bash
|
|
44
|
-
npm run dev
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
`vite.config.ts` picks up `.cert/dev-key.pem` + `.cert/dev-cert.pem` automatically when both exist and serves HTTPS on `VITE_BLOCKS_DEV_HOST`:`VITE_BLOCKS_DEV_PORT` (default port `5173`, `strictPort: true` because the port is baked into the registered OIDC redirect URI). It also sets `allowedHosts` to that domain, working around Vite's default DNS-rebinding protection which otherwise 404s custom hosts with "Blocked request."
|
|
48
|
-
|
|
49
|
-
Finally, open the app at:
|
|
50
|
-
|
|
51
|
-
```text
|
|
52
|
-
https://dbpdba.seliseblocks.com:5173
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
**Not** `http://`, **not** `localhost` — either one skips the cookie entirely even though the app loads.
|
|
56
|
-
|
|
57
|
-
## Trusting the cert (optional but recommended)
|
|
58
|
-
|
|
59
|
-
The cert is self-signed, so the browser shows a one-time warning until trusted. `npm run cert` prints the exact trust command for your OS when it finishes:
|
|
60
|
-
|
|
61
|
-
- Windows (elevated prompt): `certutil -addstore -f Root .cert\dev-cert.pem`
|
|
62
|
-
- macOS: `sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain .cert/dev-cert.pem`
|
|
63
|
-
- Linux: `sudo cp .cert/dev-cert.pem /usr/local/share/ca-certificates/blocks-dev.crt && sudo update-ca-certificates`
|
|
64
|
-
|
|
65
|
-
Restart the browser after trusting so it picks up the new trust store entry. `.cert/` is already gitignored by the scaffold — it's per-machine, never committed.
|
|
66
|
-
|
|
67
|
-
## Still need a public OIDC client
|
|
68
|
-
|
|
69
|
-
Local HTTPS alone doesn't make login succeed if no OIDC client is registered yet, or if its `redirect_uris` don't include this exact dev origin. That registration is portal-only (see blocks-onboarding's Gotchas) — out of scope here, but it's the next thing to check if HTTPS is right and login still fails. The redirect URI must match byte-for-byte, including `:5173`.
|
|
70
|
-
|
|
71
|
-
## Gotchas
|
|
72
|
-
|
|
73
|
-
**Custom app domain: the API base URL must share its registrable domain, or the cookie never lands.** `blocks new web --app-domain` isn't limited to `*.seliseblocks.com` — it also supports custom domains (e.g. `abc.slsblx.com`, `xyz.blx10.com`). On a custom domain, the hosted-login session cookie is only stored by the browser if `VITE_BLOCKS_API_URL` shares the app's registrable domain. Concretely:
|
|
74
|
-
|
|
75
|
-
- `abc.slsblx.com` → `VITE_BLOCKS_API_URL` must be `https://blocksapi.slsblx.com`
|
|
76
|
-
- `xyz.blx10.com` → `VITE_BLOCKS_API_URL` must be `https://blocksapi.blx10.com`
|
|
77
|
-
|
|
78
|
-
If `VITE_BLOCKS_API_URL` is left at the default `https://api.seliseblocks.com` while the app itself runs on a custom domain, the browser treats the API as cross-site relative to the app and never stores the cookie — login still redirects back and *looks* successful, but cookie-based calls (`/iam/me`, organization switching, logout) silently fail. Check `VITE_BLOCKS_API_URL` in `.env` first whenever the app domain is not `*.seliseblocks.com` and auth-dependent calls are failing despite HTTPS and the cert being set up correctly.
|
|
79
|
-
|
|
80
|
-
## Troubleshooting
|
|
81
|
-
|
|
82
|
-
| Symptom | Likely cause | Fix |
|
|
83
|
-
|---|---|---|
|
|
84
|
-
| Login redirects back but app shows logged-out | Opened on `http://` or `localhost` instead of the HTTPS dev host | Check the URL bar: must be `https://<VITE_BLOCKS_DEV_HOST>:5173` exactly |
|
|
85
|
-
| Browser "not private" warning | Self-signed cert not trusted yet | Run the OS trust command `npm run cert` printed, then restart the browser |
|
|
86
|
-
| Vite: "Blocked request. This host is not allowed" | `allowedHosts` doesn't include the host being requested — usually `.env`'s `VITE_BLOCKS_DEV_HOST` was edited/missing after scaffold, or the browser is hitting a different hostname than expected | Confirm `.env` has the right `VITE_BLOCKS_DEV_HOST` and that the URL bar matches it exactly |
|
|
87
|
-
| Page doesn't load at all / DNS error | Hosts file entry missing or wrong | Add `127.0.0.1 <VITE_BLOCKS_DEV_HOST>` to `C:\Windows\System32\drivers\etc\hosts` (as Administrator) |
|
|
88
|
-
| `npm run cert` errors "No domain given" | `.env` missing or `VITE_BLOCKS_DEV_HOST` not set | Confirm `.env` exists with `VITE_BLOCKS_DEV_HOST=<host>`, or run `npm run cert -- <domain>` explicitly |
|
|
89
|
-
| `npm run dev` fails to bind the port | Port 5173 already in use (dev server uses `strictPort`, won't fall back) | Free port 5173, or the port is fixed because it's part of the registered OIDC redirect URI — don't just change it without updating the OIDC client too |
|
|
90
|
-
| HTTPS works, cert trusted, login still fails | No OIDC client registered yet, or its redirect URI doesn't match this origin exactly | Register/update the public OIDC client in the portal with `https://<VITE_BLOCKS_DEV_HOST>:5173/login/callback` as a redirect URI (portal-only, see blocks-onboarding) |
|
|
91
|
-
| Works for one dev, fails for a teammate | Each machine needs its own cert + hosts entry — `.cert/` is gitignored on purpose | Teammate runs `npm run cert` and adds the hosts entry on their own machine |
|
|
92
|
-
|
|
93
|
-
## Example trigger prompts
|
|
94
|
-
|
|
95
|
-
- "Run my app locally over HTTPS on its real domain so SSO works"
|
|
96
|
-
- "My local login isn't working — it just bounces back to the login page"
|
|
97
|
-
- "How do I set up the dev cert for this scaffolded app?"
|
|
98
|
-
- "I'm getting 'Blocked request. This host is not allowed' from Vite"
|
|
99
|
-
- "Do I need OpenSSL to run npm run cert on Windows?"
|
|
100
|
-
- "Why does login work in production but not on localhost?"
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: blocks-iam-access-control
|
|
3
|
-
description: "Work with SELISE Blocks RBAC (roles & permissions) via `blocks iam roles/permissions *` (CLI, project-scoped) or `blocksClient.iam.*` (SDK), never raw fetch/curl. Two facets: read-only feature-gating by the current user's own roles/permissions (common, safe) vs. creating/editing role and permission definitions (sensitive, human-confirmed only — CLI `--dry-run`→`--yes` or an in-app admin screen). OIDC/identity-provider provisioning stays portal-only, a different concern. Use for permission-gated UI, role/permission pickers, or building/scripting role & permission admin ('gate this button by permission', 'create a role and grant permissions', 'list permissions by severity')."
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Blocks IAM — Access Control (Permissions & Roles)
|
|
7
|
-
|
|
8
|
-
This skill covers **permission and role definitions** in SELISE Blocks — the RBAC model itself, not who has which role (that's the blocks-iam-users skill). Everything goes through either `blocks iam roles/permissions *` (CLI) or `blocksClient.iam.*` from **`@seliseblocks/client`**, the single SDK instance every `blocks new web` scaffold wires up at `src/lib/blocks/client.ts` and exports as `blocksClient`. No raw `fetch`/`curl` for either surface.
|
|
9
|
-
|
|
10
|
-
```ts
|
|
11
|
-
import { blocksClient } from "../../lib/blocks/client";
|
|
12
|
-
```
|
|
13
|
-
|
|
14
|
-
## The platform boundary — read this before writing any code
|
|
15
|
-
|
|
16
|
-
Role and permission administration is **not** portal-only or app-UI-only — `blocks` has a full, working CLI surface for it too. There are two equally real surfaces for the same operations, and the choice is about *where the human is*, not which one is "allowed" — see [flows/manage-roles-permissions.md](flows/manage-roles-permissions.md) for the full command reference and the CLI-vs-SDK decision.
|
|
17
|
-
|
|
18
|
-
Identity-provider/OIDC client provisioning is the one piece that really is **portal-only, human-driven**, at `https://os.seliseblocks.com` — unrelated to roles/permissions, don't bolt it onto this skill.
|
|
19
|
-
|
|
20
|
-
Keep the two facets below (read-only feature-gating vs. sensitive admin mutations) separate in your head (and in your code) — they have very different risk profiles regardless of which surface (CLI or SDK) you're using.
|
|
21
|
-
|
|
22
|
-
## Facet 1 — Feature-gating a frontend by the user's own permissions (common, low risk)
|
|
23
|
-
|
|
24
|
-
Read-only, scoped to whoever is signed in, needs no special confirmation. `useCurrentUser()` + `iam.resources.features()` + `iam.roles.assignable()`.
|
|
25
|
-
|
|
26
|
-
→ Full walkthrough: [flows/feature-gating.md](flows/feature-gating.md)
|
|
27
|
-
|
|
28
|
-
## Facet 2 — Creating/editing roles & permissions (sensitive)
|
|
29
|
-
|
|
30
|
-
Legitimate only in direct response to a human's explicit, in-the-moment instruction — CLI (`--dry-run` reviewed, then `--yes`) or a signed-in admin's own in-app screen. Never something an agent decides to invoke on its own initiative.
|
|
31
|
-
|
|
32
|
-
→ Full command reference, SDK methods, and confirm-before-mutating pattern: [flows/manage-roles-permissions.md](flows/manage-roles-permissions.md)
|
|
33
|
-
|
|
34
|
-
## Gotchas
|
|
35
|
-
|
|
36
|
-
- **CLI mutations are project-scoped, not account-scoped** — they run against the impersonated-project token; `blocks iam me` is the one IAM command that uses the account token instead.
|
|
37
|
-
- **Role hierarchy and permission assignment key off `slug`**, not `itemId`.
|
|
38
|
-
- **Never fire a create/update/assign-permissions call — CLI or SDK — without a human confirming that specific change first.** See [flows/manage-roles-permissions.md](flows/manage-roles-permissions.md) for the full discipline.
|
|
39
|
-
- **OIDC/identity-provider client provisioning is always portal-only**, independent of everything above.
|
|
40
|
-
|
|
41
|
-
## Example trigger prompts
|
|
42
|
-
|
|
43
|
-
- "Only show the delete button to users who have the `order::delete` permission." → Facet 1
|
|
44
|
-
- "Hide this whole admin section unless the signed-in user has an admin role." → Facet 1
|
|
45
|
-
- "What roles am I allowed to assign to other users?" → Facet 1
|
|
46
|
-
- "Show me permissions grouped by severity in a settings panel." → Facet 1
|
|
47
|
-
- "Build an admin page where I can create a role and pick which permissions it gets." → Facet 2
|
|
48
|
-
- "Create a `content-editor` role from the CLI with these permissions." → Facet 2
|
|
49
|
-
- "Can you just set up a few default roles for my project?" → confirm the exact list with the human first (in chat, or via a reviewed `--dry-run`), then run each `blocks iam roles create`/`assign-permissions` with `--yes` only after they say go — don't auto-provision without that per-change confirmation.
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
# Flow: Feature-gating a frontend by the user's own permissions (common, low risk)
|
|
2
|
-
|
|
3
|
-
This is read-only against IAM and scoped to whoever is signed in, so it needs no special confirmation — build it the same way you'd build any other data-fetching feature.
|
|
4
|
-
|
|
5
|
-
The scaffold already gives you a `useCurrentUser()` hook (`src/features/profile/useCurrentUser.ts`) wrapping `blocksClient.iam.me()` with TanStack Query; `me()` returns `{ data: { itemId, email, firstName, lastName, roles: string[], permissions: string[], ... } }`. Reuse it instead of re-fetching:
|
|
6
|
-
|
|
7
|
-
```ts
|
|
8
|
-
// src/features/access/usePermission.ts
|
|
9
|
-
import { useCurrentUser } from "../profile/useCurrentUser";
|
|
10
|
-
|
|
11
|
-
export function useHasPermission(permission: string): boolean {
|
|
12
|
-
const me = useCurrentUser();
|
|
13
|
-
return me.data?.data?.permissions?.includes(permission) ?? false;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export function useHasRole(role: string): boolean {
|
|
17
|
-
const me = useCurrentUser();
|
|
18
|
-
return me.data?.data?.roles?.includes(role) ?? false;
|
|
19
|
-
}
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
```tsx
|
|
23
|
-
// src/shared/ui/RequirePermission.tsx
|
|
24
|
-
import type { ReactNode } from "react";
|
|
25
|
-
import { useHasPermission } from "../../features/access/usePermission";
|
|
26
|
-
|
|
27
|
-
export function RequirePermission({ permission, children }: { permission: string; children: ReactNode }) {
|
|
28
|
-
if (!useHasPermission(permission)) return null;
|
|
29
|
-
return <>{children}</>;
|
|
30
|
-
}
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
Two more read methods round this out:
|
|
34
|
-
|
|
35
|
-
- `blocksClient.iam.resources.features(query?)` — feature/resource flags for the active user context; use this to drive nav items or feature flags that are more granular than a flat permission string.
|
|
36
|
-
- `blocksClient.iam.roles.assignable()` — lists roles the **current caller** is allowed to assign. If you're building a "grant this user a role" picker, populate it from `assignable()`, not from `roles.list()` — don't assume every role in the system is one this particular admin may hand out.
|
|
37
|
-
|
|
38
|
-
There is also a CLI read path for the same data, useful outside an app (scripting/inspection): `blocks iam roles list/get/assignable` and `blocks iam permissions list/get/by-severity` — see [manage-roles-permissions.md](manage-roles-permissions.md) for the full CLI command reference (it covers both reads and mutations).
|
|
@@ -1,110 +0,0 @@
|
|
|
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,169 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: blocks-iam-account
|
|
3
|
-
description: "Signed-in (or partially-signed-in) user's own SELISE Blocks IAM account actions via @seliseblocks/client — never raw fetch/curl. Covers activation, forgot/reset/change password, logout(-all), profile bootstrap (iam.me/updateMe), self-service MFA, signup, and login-options discovery. Use for activation/password pages, logout buttons, profile bootstrap, signup forms, or letting a user manage their own MFA. The self-service half of IAM — not admin CRUD on other users (blocks-iam-users/blocks-iam-access-control), not hosted-login redirect (blocks-iam-sso-oidc-implementation)."
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Blocks IAM — Account Self-Service
|
|
7
|
-
|
|
8
|
-
Account-lifecycle and account-security actions the signed-in (or not-yet-fully-signed-in) user takes on **their own** account, all through the single `@seliseblocks/client` instance the scaffold gives you — `blocks new web` wires up `createBlocksClient({ apiUrl, xBlocksKey, oidc, accessToken })` once; every call below hangs off that instance's `.auth`, `.iam`, or `.mfa` namespace. **Never** hand-roll `fetch`/`curl` against `api.seliseblocks.com` for these.
|
|
9
|
-
|
|
10
|
-
Source of truth: `auth-client.ts`, `iam-client.ts`, and `mfa-client.ts` in `@seliseblocks/client`. Every method has a What/Why/How docstring in source — this skill surfaces them, it doesn't add new ones.
|
|
11
|
-
|
|
12
|
-
## Scope: this vs. the other IAM skills
|
|
13
|
-
|
|
14
|
-
- **This skill** — the current user acting on themselves: activate their own invite, reset their own forgotten password, change their own password, log themselves out, read/edit their own profile, sign up, discover login options, enroll/manage their own MFA.
|
|
15
|
-
- **blocks-iam-users / blocks-iam-access-control** — an admin managing *other* users (create, deactivate, grant/revoke access). Different actor, different skill. Don't duplicate that here.
|
|
16
|
-
- **blocks-iam-sso-oidc-implementation** — the hosted-login redirect/callback flow (`auth.idp.initiate`/`redirectToProvider`/`callback`, `oidc.refreshToken`). This skill only covers direct account-lifecycle calls (activate, recover, reset, change-password, logout) that a user takes outside that redirect dance — don't reimplement hosted login here.
|
|
17
|
-
- **blocks-iam-mfa** (not yet written) — the full self-service MFA walkthrough (enrollment UX, challenge flows, backup codes). This skill only notes that `mfa.*` exists and is in scope; go there for depth.
|
|
18
|
-
|
|
19
|
-
## The SDK never owns your session
|
|
20
|
-
|
|
21
|
-
Every method here just relays IAM's request/response. The SDK **does not** read or write cookies, localStorage, or any token store — your app decides where the access token, refresh token, and "am I logged in" flag live, and passes the access token in via the `accessToken` option (string or async callback) on `createBlocksClient`. After `logout`/`logoutAll`, activation, or a password reset, **you** clear/update that app-owned state; the SDK call alone doesn't do it for you.
|
|
22
|
-
|
|
23
|
-
Request/payload types for most of these methods are intentionally loose (`Record<string, unknown>` passthrough — IAM, not the SDK, defines the exact fields). `BlocksLogoutRequest` is the one exception with a typed hint (`refreshToken?: string`). Where the SDK doesn't pin the shape, confirm exact field names against your tenant's IAM contract rather than guessing — the examples below show the well-known fields, not an exhaustive schema.
|
|
24
|
-
|
|
25
|
-
## Activation — finishing account setup
|
|
26
|
-
|
|
27
|
-
Three related calls, all under `blocksClient.auth`, all public (no bearer token needed for `activate`/`validateActivation` — the emailed code is the credential):
|
|
28
|
-
|
|
29
|
-
- **`auth.validateActivation(request)`** — no auth required. Check the activation code/state *before* showing the final "set your password" step, so an expired/invalid link fails fast with a clear message instead of after the user fills out the form.
|
|
30
|
-
- **`auth.activate(request)`** — no auth required. Completes setup for a user created/invited in an inactive state: pass the emailed `code` plus the new password (and whatever else your tenant's activation contract needs, e.g. `firstName`/`lastName`) after your UI confirms password === confirm-password client-side (don't send a confirm field — that's a UI-only check).
|
|
31
|
-
- **`auth.resendActivation(request)`** — Send a new code/link when the old one expired. This call attaches the bearer token if one happens to be configured, but works either way — typical callers are not-yet-active, so don't gate this behind requiring a token.
|
|
32
|
-
|
|
33
|
-
```ts
|
|
34
|
-
// after the user opens /activate?code=... and submits password + confirm
|
|
35
|
-
const state = await blocksClient.auth.validateActivation({ code });
|
|
36
|
-
if (!state.valid) {
|
|
37
|
-
// show "this link expired" + a resend option
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
await blocksClient.auth.activate({
|
|
41
|
-
code,
|
|
42
|
-
password,
|
|
43
|
-
firstName,
|
|
44
|
-
lastName
|
|
45
|
-
});
|
|
46
|
-
// account is now active — route to login / hosted-login (blocks-iam-sso-oidc-implementation)
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
## Password — forgot, reset, and authenticated change
|
|
50
|
-
|
|
51
|
-
- **`auth.recover(request)`** — no auth required. Public entry point for "forgot password" — typically just the account's email. Triggers IAM to send a reset link/code.
|
|
52
|
-
- **`auth.resetPassword(request)`** — no auth required. Completes the recovery: pass the emailed reset token plus the new password. IAM owns token validation and password-policy enforcement — surface its response/errors directly rather than pre-validating password rules yourself.
|
|
53
|
-
- **`auth.changePassword(request)`** — requires an access token (an authenticated account-security action, not part of the recovery flow). Use this for a signed-in "change my password" settings-page action — current password + new password.
|
|
54
|
-
|
|
55
|
-
```ts
|
|
56
|
-
// forgot-password page
|
|
57
|
-
await blocksClient.auth.recover({ email });
|
|
58
|
-
|
|
59
|
-
// reset-password page (link from the recovery email)
|
|
60
|
-
await blocksClient.auth.resetPassword({ code, password: newPassword });
|
|
61
|
-
|
|
62
|
-
// signed-in settings page
|
|
63
|
-
await blocksClient.auth.changePassword({ oldPassword, newPassword });
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
## Logout — end this session or all sessions
|
|
67
|
-
|
|
68
|
-
- **`auth.logout(request = {})`** — Ends the current session; commonly takes `{ refreshToken }` if your app manages a refresh token directly (the typed field on `BlocksLogoutRequest`). If your app relies on the hosted IdP's session cookie instead, an empty `{}` is enough — the SDK always sends the request with `credentials: "include"`.
|
|
69
|
-
- **`auth.logoutAll(request = {})`** — "Sign out everywhere" — invalidates every session for the account, not just the current one. Good for a security settings page next to change-password.
|
|
70
|
-
|
|
71
|
-
```ts
|
|
72
|
-
async function signOut() {
|
|
73
|
-
try {
|
|
74
|
-
await blocksClient.auth.logout({ refreshToken });
|
|
75
|
-
} finally {
|
|
76
|
-
// clear app-owned session state even if the network call fails,
|
|
77
|
-
// so the UI never shows a stale signed-in state
|
|
78
|
-
clearLocalSession();
|
|
79
|
-
navigate("/login");
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
## Profile bootstrap and self-edit
|
|
85
|
-
|
|
86
|
-
- **`iam.me()`** — The current IAM user record: roles, permissions, active organization context, resolved from the access token. This is the right call to bootstrap an app's profile/account page or a permission-gated shell after login — don't reconstruct this from token claims yourself.
|
|
87
|
-
- **`iam.updateMe(request)`** — Updates the CURRENT authenticated user's own profile fields (name, etc., per your tenant's IAM contract). The backend resolves the user id from the token — **never** pass another user's id here; that's `iam.users.update(id, request)` in the admin skill, a different call entirely.
|
|
88
|
-
|
|
89
|
-
```ts
|
|
90
|
-
const me = await blocksClient.iam.me();
|
|
91
|
-
// me.data?.roles / me.data?.permissions -> gate nav items, feature flags, etc.
|
|
92
|
-
// (iam.me() wraps the user record in a { data } envelope, not the fields directly)
|
|
93
|
-
|
|
94
|
-
await blocksClient.iam.updateMe({ firstName, lastName });
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
## Self-service MFA
|
|
98
|
-
|
|
99
|
-
Enrolling, challenging, or turning off MFA for the **signed-in user's own** account, via `blocksClient.mfa.*` (see `mfa-client.ts`'s own docstrings — they call this out as self-service, distinct from `mfa.saveConfig`, which is a tenant/admin policy action, not covered here):
|
|
100
|
-
|
|
101
|
-
- **`mfa.totp.setup()`** — Starts authenticator-app enrollment; render IAM's returned secret/QR in your UI.
|
|
102
|
-
- **`mfa.totp.verifySetup({ code })`** — Confirms enrollment with the 6-digit code from the authenticator app.
|
|
103
|
-
- **`mfa.generate({ mfaType, sendPhoneNumberAsEmailDomain? })`** — Sends an email/SMS OTP challenge; returns an `mfaId` for `resend`/`verify`.
|
|
104
|
-
- **`mfa.resend({ mfaId, sendPhoneNumberAsEmailDomain? })`** — Re-sends a pending OTP.
|
|
105
|
-
- **`mfa.verify({ mfaId, verificationCode, authType, isFromTokenCall? })`** — Confirms an OTP or step-up challenge; set `isFromTokenCall` when verifying as part of a login/token exchange.
|
|
106
|
-
- **`mfa.setMethod({ mfaType })`** — Switches which enrolled method is active.
|
|
107
|
-
- **`mfa.disable()`** — Self-service opt-out, where the tenant's policy allows it.
|
|
108
|
-
- **`mfa.backupCodes.list()`** / **`.generate()`** / **`.use({ code, userId })`** — View remaining recovery codes, mint a fresh set (treat the response as sensitive, show once), or consume one when the primary method is unavailable.
|
|
109
|
-
|
|
110
|
-
```ts
|
|
111
|
-
await blocksClient.mfa.totp.setup();
|
|
112
|
-
await blocksClient.mfa.totp.verifySetup({ code });
|
|
113
|
-
```
|
|
114
|
-
|
|
115
|
-
The same self-service surface is also reachable from a terminal via `blocks mfa totp setup/verify-setup/enable`, `blocks mfa generate/resend/verify`, `blocks mfa method set`, `blocks mfa disable`, and `blocks mfa backup-codes list/generate/use` (project-scoped, impersonated-user token). **See also:** `blocks-iam-mfa` for the full enrollment/challenge walkthrough — this section only flags that self-service MFA exists and is in this skill's scope.
|
|
116
|
-
|
|
117
|
-
## Signup and login discovery
|
|
118
|
-
|
|
119
|
-
- **`auth.signup(request)`** — no auth required. Registers a new account; IAM owns account-creation rules — send its expected payload and render its response/errors directly rather than pre-validating fields yourself.
|
|
120
|
-
- **`auth.loginOptions()`** — no auth required. Discovers which login methods the tenant supports; call before rendering the login screen so you only show controls IAM actually accepts.
|
|
121
|
-
|
|
122
|
-
```ts
|
|
123
|
-
const options = await blocksClient.auth.loginOptions();
|
|
124
|
-
// options -> render enabled login methods (password, social, etc.)
|
|
125
|
-
|
|
126
|
-
await blocksClient.auth.signup({ email, password, firstName, lastName });
|
|
127
|
-
```
|
|
128
|
-
|
|
129
|
-
## Signup/invite dedup checks
|
|
130
|
-
|
|
131
|
-
Useful inside a signup or invite form before submit — both still send `x-blocks-key` even though they don't require a signed-in user:
|
|
132
|
-
|
|
133
|
-
- **`iam.users.emailAvailable(query)`** — no auth required. Returns an availability flag (`isAvailable`/`IsAvailable` — IAM's casing varies, check both) for a candidate email.
|
|
134
|
-
- **`iam.users.exists(email)`** — Existence check by email.
|
|
135
|
-
|
|
136
|
-
```ts
|
|
137
|
-
const availability = await blocksClient.iam.users.emailAvailable({ email });
|
|
138
|
-
if (availability.isAvailable === false || availability.IsAvailable === false) {
|
|
139
|
-
// show "email already in use" before the user finishes the form
|
|
140
|
-
}
|
|
141
|
-
```
|
|
142
|
-
|
|
143
|
-
## Gotchas
|
|
144
|
-
|
|
145
|
-
- **Don't invent payload fields.** Several of these methods (`activate`, `resendActivation`, `validateActivation`, `changePassword`, `recover`, `resetPassword`, `logoutAll`, `updateMe`) take an untyped `Record<string, unknown>` in the SDK — the shape is IAM's contract, not something the client library enforces. Use the well-known fields shown above; confirm anything beyond that against the tenant's actual IAM behavior instead of guessing new field names.
|
|
146
|
-
- **`activate`/`validateActivation`/`recover`/`resetPassword` are public (no bearer token)** — the emailed code/token *is* the credential. `changePassword` and `updateMe` require an access token to be configured on the client (via `accessToken` on `createBlocksClient`). `logout`/`logoutAll`/`resendActivation` will attach a bearer token if one is configured, but don't require it.
|
|
147
|
-
- **`iam.me()` is not `auth.userInfo()` or `auth.isAuthenticated()`.** `auth.userInfo()`/`isAuthenticated()` (OIDC-style claims, session-cookie aware) belong to the SSO/OIDC login-flow territory. `iam.me()` is the full IAM user record — roles, permissions, org context — wrapped in a `{ data }` envelope (`BlocksMeResponse = BlocksQueryResponse<BlocksUser>`), so read `me.data?.roles` etc., not `me.roles` directly.
|
|
148
|
-
- **Always clear local app state after logout, even on failure.** The SDK doesn't clear anything for you; a network error from `logout`/`logoutAll` shouldn't leave the UI showing a signed-in user.
|
|
149
|
-
- **`updateMe` never takes a user id.** If you find yourself passing an id, you want the admin `iam.users.update(id, request)` call instead — wrong skill for that.
|
|
150
|
-
- **Confirm-password fields are UI-only.** IAM's `activate`/`resetPassword`/`changePassword` contracts want the new password once; matching against a second "confirm" field is validated client-side and never sent.
|
|
151
|
-
|
|
152
|
-
## Example trigger prompts
|
|
153
|
-
|
|
154
|
-
- "Activate a new account with the emailed code."
|
|
155
|
-
- "Build the /activate page that sets a password from an invite link."
|
|
156
|
-
- "The activation link expired — let the user request a new one."
|
|
157
|
-
- "Add a forgot-password flow to the login page."
|
|
158
|
-
- "Build the reset-password page for the emailed reset link."
|
|
159
|
-
- "Let a signed-in user change their password from account settings."
|
|
160
|
-
- "Add a logout button."
|
|
161
|
-
- "Add a 'sign out of all devices' option."
|
|
162
|
-
- "Fetch the current user's roles and permissions after login."
|
|
163
|
-
- "Let a user edit their own name on their profile page."
|
|
164
|
-
- "Check if an email is already taken before letting someone submit the signup form."
|
|
165
|
-
- "Register a new account from the signup page."
|
|
166
|
-
- "Show which login methods are enabled before rendering the login screen."
|
|
167
|
-
- "Let a signed-in user enroll in authenticator-app MFA."
|
|
168
|
-
- "Add a 'turn off MFA' option to account security settings."
|
|
169
|
-
- "Let a user view or regenerate their MFA backup codes."
|
|
@@ -1,124 +0,0 @@
|
|
|
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."
|