@seliseblocks/cli-os 0.2.6 → 0.2.8

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/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  CLI for SELISE Blocks Cloud.
4
4
 
5
- - Package: `@seliseblocks/cli-os`
5
+ - Package: [`@seliseblocks/cli-os`](https://www.npmjs.com/package/@seliseblocks/cli-os)
6
6
  - Binary: `blocks`
7
7
 
8
8
  ## Setup
@@ -12,6 +12,7 @@ export async function authConfigSave(argv) {
12
12
  ...compact({
13
13
  absoluteRefreshTokenValidForNumberMinutes: optionalIntegerFlag(flags, "absolute-refresh-token-minutes"),
14
14
  accessTokenValidForNumberMinutes: optionalIntegerFlag(flags, "access-token-minutes"),
15
+ accountActionBaseUrl: stringFlag(flags, "account-action-base-url") || undefined,
15
16
  accountLockDurationInMinutes: optionalIntegerFlag(flags, "account-lock-duration-minutes"),
16
17
  getNumberOfWrongAttemptsToLockTheAccount: optionalIntegerFlag(flags, "wrong-attempts-to-lock"),
17
18
  isOidcEnabled: booleanFlag(flags, "oidc-enabled") || undefined,
@@ -32,10 +33,25 @@ export async function authConfigSave(argv) {
32
33
  projectTenantId: projectKey
33
34
  });
34
35
  const body = { ...current, ...overrides };
36
+ // Turning isOidcEnabled on isn't a single independent flag: the
37
+ // activation-link flow keys off accountActivationPath, which has to point
38
+ // at the OIDC variant once OIDC is on, or activation emails break.
39
+ // accountActionBaseUrl has no safe default this command can guess across
40
+ // environments, so the caller must supply it explicitly when the tenant
41
+ // doesn't already have one.
42
+ const missingActionBaseUrl = Boolean(body.isOidcEnabled) && !body.accountActionBaseUrl;
43
+ if (body.isOidcEnabled)
44
+ body.accountActivationPath = "oidc/activate/";
35
45
  if (booleanFlag(flags, "dry-run")) {
46
+ if (missingActionBaseUrl) {
47
+ console.warn("Warning: this tenant has no accountActionBaseUrl set. Enabling OIDC login requires one -- pass --account-action-base-url <https://your-iam-host> before re-running with --yes.");
48
+ }
36
49
  writeOutput({ dryRun: true, endpoint: "/iam/v4/auth/config", request: body }, flags);
37
50
  return;
38
51
  }
52
+ if (missingActionBaseUrl) {
53
+ throw new Error("Enabling OIDC login requires accountActionBaseUrl, and this tenant doesn't have one set. Pass --account-action-base-url <https://your-iam-host>.");
54
+ }
39
55
  await confirmMutation(flags, "Save AuthController configuration for the selected project.");
40
56
  const result = await blocksRequest("/iam/v4/auth/config", {
41
57
  body,
@@ -23,7 +23,7 @@ export async function newWeb(argv) {
23
23
  const oidcClientId = await resolveOidcClientId(tenantId, appDomain, name, flags);
24
24
  if (oidcClientId) {
25
25
  try {
26
- await ensureOidcLoginEnabled(tenantId, flags);
26
+ await ensureOidcLoginEnabled(tenantId, oidcUrl, flags);
27
27
  }
28
28
  catch (error) {
29
29
  console.warn(`Warning: could not confirm/enable OIDC login on this project's AuthController config: ${error.message}`);
@@ -135,7 +135,15 @@ async function listOidcClientSummaries(tenantId, flags) {
135
135
  // field) -- sending just `{ isOidcEnabled: true }` would reset every other
136
136
  // AuthController setting for this tenant, so the fetched config is spread
137
137
  // back in full with only that one field overridden.
138
- async function ensureOidcLoginEnabled(tenantId, flags) {
138
+ //
139
+ // Turning isOidcEnabled on isn't a single independent flag either: the
140
+ // activation-link flow keys off accountActivationPath, which has to point at
141
+ // the OIDC variant ("oidc/activate/") once OIDC is on, or activation emails
142
+ // break. accountActionBaseUrl (the host those links are built against) has
143
+ // no safe cross-environment default, but this project's own IAM host is
144
+ // already known here as `oidcUrl`, so it's used whenever the tenant doesn't
145
+ // already have one set.
146
+ async function ensureOidcLoginEnabled(tenantId, oidcUrl, flags) {
139
147
  const config = await blocksRequest("/iam/v4/auth/config", {
140
148
  impersonatedProjectAuth: true,
141
149
  projectTenantId: tenantId,
@@ -145,7 +153,12 @@ async function ensureOidcLoginEnabled(tenantId, flags) {
145
153
  return;
146
154
  await confirmMutation(flags, "Enable OIDC login on this project's AuthController configuration.");
147
155
  await blocksRequest("/iam/v4/auth/config", {
148
- body: { ...config, isOidcEnabled: true },
156
+ body: {
157
+ ...config,
158
+ accountActionBaseUrl: config.accountActionBaseUrl || oidcUrl,
159
+ accountActivationPath: "oidc/activate/",
160
+ isOidcEnabled: true
161
+ },
149
162
  impersonatedProjectAuth: true,
150
163
  projectTenantId: tenantId,
151
164
  ...requestContext(flags)
@@ -1,49 +1,49 @@
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
+ ---
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 +1,38 @@
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
+ # 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 +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.