@seliseblocks/cli-os 0.2.4 → 0.2.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/auth/config/save.js +12 -2
- package/dist/commands/auth/oidc-clients/save.js +17 -2
- package/dist/commands/new/web.js +40 -0
- package/dist/lib/scaffold-web/app-core.js +6 -1
- package/dist/lib/scaffold-web/blocks-lib.js +36 -2
- package/dist/skills/blocks-iam-access-control/SKILL.md +49 -49
- package/dist/skills/blocks-iam-access-control/flows/feature-gating.md +38 -38
- package/dist/skills/blocks-iam-access-control/flows/manage-roles-permissions.md +110 -110
- package/dist/skills/blocks-iam-mfa/SKILL.md +124 -124
- package/dist/skills/blocks-iam-organizations/SKILL.md +43 -43
- package/dist/skills/blocks-iam-organizations/flows/admin-mutations.md +89 -89
- package/dist/skills/blocks-iam-organizations/flows/read-and-switch.md +57 -57
- package/dist/skills/blocks-iam-sso-oidc-configuration/SKILL.md +105 -105
- package/dist/skills/blocks-mail/SKILL.md +95 -95
- package/dist/skills/blocks-notification/SKILL.md +69 -69
- package/dist/skills/blocks-notifier/SKILL.md +107 -107
- package/dist/skills/blocks-release-deployment/SKILL.md +81 -81
- package/dist/skills/blocks-secrets/SKILL.md +81 -81
- package/dist/skills/lint.mjs +168 -168
- package/package.json +1 -1
|
@@ -1,89 +1,89 @@
|
|
|
1
|
-
# Flow: Admin-CRUD surface — confirm the exact change first
|
|
2
|
-
|
|
3
|
-
These mutate tenant-level state. Build them as an explicit feature in an admin settings screen (SDK), or run them as an operator from a terminal with a reviewed `--dry-run` (CLI). Both surfaces exist and are equally real — see "The platform boundary" in [SKILL.md](../SKILL.md).
|
|
4
|
-
|
|
5
|
-
## SDK methods
|
|
6
|
-
|
|
7
|
-
### `organizations.list(query)` / `organizations.get(id)` — browse and inspect
|
|
8
|
-
|
|
9
|
-
Read-only, so no confirmation is needed to call them — but they're part of the admin org-management surface (browsing arbitrary orgs, not just "my own"), so gate the screen itself behind whatever admin permission the app already checks. Typical use: an org-picker/detail view feeding into an edit form, populated before a `create`/`update` call.
|
|
10
|
-
|
|
11
|
-
```ts
|
|
12
|
-
const page = await blocksClient.iam.organizations.list({ Page: 1, PageSize: 20, "Filter.Name": search });
|
|
13
|
-
const org = await blocksClient.iam.organizations.get(organizationId);
|
|
14
|
-
```
|
|
15
|
-
|
|
16
|
-
### `organizations.create(request)` / `organizations.update(id, request)`
|
|
17
|
-
|
|
18
|
-
Create/edit an organization's record (name, description, contact info, branding, addresses, default roles/permissions for new members, etc. — verify current field names against what the admin UI/API actually accepts rather than assuming they haven't changed).
|
|
19
|
-
|
|
20
|
-
```ts
|
|
21
|
-
// Only after the admin has reviewed and confirmed this exact payload:
|
|
22
|
-
await blocksClient.iam.organizations.create({
|
|
23
|
-
name: "Acme Logistics",
|
|
24
|
-
description: "Acme's logistics division workspace"
|
|
25
|
-
// ...remaining fields the user confirmed
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
await blocksClient.iam.organizations.update(organizationId, {
|
|
29
|
-
name: "Acme Logistics (Renamed)"
|
|
30
|
-
// ...only the fields the user asked to change
|
|
31
|
-
});
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
### `organizations.getConfig()` / `organizations.saveConfig(request)` — org-level settings
|
|
35
|
-
|
|
36
|
-
Project/org-wide policy, including multi-org enablement. Read the current config first, show the user the diff of what would change, then save.
|
|
37
|
-
|
|
38
|
-
```ts
|
|
39
|
-
const current = await blocksClient.iam.organizations.getConfig();
|
|
40
|
-
// ...user reviews current vs. proposed, confirms...
|
|
41
|
-
await blocksClient.iam.organizations.saveConfig({
|
|
42
|
-
...current,
|
|
43
|
-
isMultiOrgEnabled: true
|
|
44
|
-
});
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
### `signupSettings.save(request)` — save signup policy
|
|
48
|
-
|
|
49
|
-
Same confirm-first rule: this changes what the public signup screen allows for every future visitor, so restate the exact policy change (e.g., "turn on org creation from signup") before calling.
|
|
50
|
-
|
|
51
|
-
```ts
|
|
52
|
-
await blocksClient.iam.signupSettings.save({
|
|
53
|
-
...currentSettings,
|
|
54
|
-
allowOrgCreationFromSignup: true
|
|
55
|
-
});
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
## CLI surface — scripting/inspection, outside an app UI
|
|
59
|
-
|
|
60
|
-
For anything that isn't a feature inside a running Blocks app (one-off lookups, ops scripts, CI checks), the `blocks` CLI has a matching, project-scoped command for every SDK method above except `switchOrganization` (see [read-and-switch.md](read-and-switch.md)). Every command below requires a project already selected (`blocks use <tenantId>`) — they all call IAM through an impersonated project token via `selectedProject()`, the same tenant scoping the SDK calls get from the app's own session.
|
|
61
|
-
|
|
62
|
-
| CLI command | Maps to | Notes |
|
|
63
|
-
|---|---|---|
|
|
64
|
-
| `blocks iam organizations list` | `organizations.list()` | `--search`, `--is-disabled`, `--parent-organization-id`, `--ids <a,b>`, `--page`, `--page-size`, `--sort-by`, `--sort-desc`. Read-only. |
|
|
65
|
-
| `blocks iam organizations get <id>` (or `--id`) | `organizations.get(id)` | Read-only. |
|
|
66
|
-
| `blocks iam organizations my` | `organizations.my()` | Read-only. |
|
|
67
|
-
| `blocks iam organizations create --name <n> [...]` | `organizations.create()` | `--name` (required), `--description`, `--email`, `--phone-number`, `--website-url`, `--default-permissions`, `--default-roles`, or a full `--body '<json>'`/`--file <path.json>`. Mutation. |
|
|
68
|
-
| `blocks iam organizations update <id> [...]` | `organizations.update(id, ...)` | `--name`, `--description`, `--email`, `--phone-number`, `--website-url`, `--currency`, `--industry`, `--locale`, `--time-zone`, `--is-enabled`, or `--body`/`--file`. Mutation. |
|
|
69
|
-
| `blocks iam organizations config get` | `organizations.getConfig()` | Read-only. |
|
|
70
|
-
| `blocks iam organizations config save [...]` | `organizations.saveConfig()` | `--multi-org-enabled`, `--consent-for-multi-org-enable`, `--allow-org-creation-from-signup`, `--allow-org-creation-from-portal`, `--allow-org-creation-from-cloud`, `--allow-org-creation-from-construct`, or `--body`/`--file`. Mutation. |
|
|
71
|
-
| `blocks iam signup-settings get` | `signupSettings.get()` | Read-only. |
|
|
72
|
-
| `blocks iam signup-settings save [...]` | `signupSettings.save()` | `--email-password-signup`, `--sso-signup`, `--default-permissions`, `--default-roles`, or `--body`/`--file`. Mutation. |
|
|
73
|
-
|
|
74
|
-
```bash
|
|
75
|
-
blocks use <tenantId> # select the project once per session
|
|
76
|
-
blocks iam organizations list --search acme --json
|
|
77
|
-
blocks iam organizations config get --json
|
|
78
|
-
blocks iam organizations config save --multi-org-enabled --dry-run # review the request body first
|
|
79
|
-
blocks iam organizations config save --multi-org-enabled --yes # only after the user confirms
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
Every mutating command (`create`, `update`, `config save`, `signup-settings save`) supports `--dry-run` (prints the resolved request body without calling the API) and requires either `--yes` or an interactive `yes` at a confirmation prompt to actually run — the same "state the exact change, get an explicit go-ahead" discipline as the SDK guidance above, just enforced by the CLI itself instead of app code you write.
|
|
83
|
-
|
|
84
|
-
## Gotchas
|
|
85
|
-
|
|
86
|
-
- **The CLI has real organization/signup-settings commands** — don't tell a user there's no `blocks` command for organizations; there is, it's just project-scoped and separate from the SDK path used inside an app.
|
|
87
|
-
- **Confirm the payload, not just the intent**, before any `create`/`update`/`saveConfig`/`signupSettings.save` call — SDK or CLI — restate which organization and which fields are changing, old value vs. new, and wait for a clear yes. On the CLI this means running with `--dry-run` first and only adding `--yes` after that review.
|
|
88
|
-
- **These are legitimate app/operator features, not agent shortcuts.** It's fine to build a "Create Organization" admin screen with a confirm dialog, or run the CLI command yourself with a human's go-ahead — that's the intended use. It's not fine for an agent to call `organizations.create`/`saveConfig` (CLI or SDK) on its own initiative (e.g., to "set things up" for a demo) without that human-in-the-loop step.
|
|
89
|
-
- **Different response shapes per call** — `my()`/`list()` return an array-shaped payload, `get(id)` returns a single organization, `create` returns an id. Don't assume a single envelope shape across all of them; check the actual response before wiring UI to a field path.
|
|
1
|
+
# Flow: Admin-CRUD surface — confirm the exact change first
|
|
2
|
+
|
|
3
|
+
These mutate tenant-level state. Build them as an explicit feature in an admin settings screen (SDK), or run them as an operator from a terminal with a reviewed `--dry-run` (CLI). Both surfaces exist and are equally real — see "The platform boundary" in [SKILL.md](../SKILL.md).
|
|
4
|
+
|
|
5
|
+
## SDK methods
|
|
6
|
+
|
|
7
|
+
### `organizations.list(query)` / `organizations.get(id)` — browse and inspect
|
|
8
|
+
|
|
9
|
+
Read-only, so no confirmation is needed to call them — but they're part of the admin org-management surface (browsing arbitrary orgs, not just "my own"), so gate the screen itself behind whatever admin permission the app already checks. Typical use: an org-picker/detail view feeding into an edit form, populated before a `create`/`update` call.
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
const page = await blocksClient.iam.organizations.list({ Page: 1, PageSize: 20, "Filter.Name": search });
|
|
13
|
+
const org = await blocksClient.iam.organizations.get(organizationId);
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
### `organizations.create(request)` / `organizations.update(id, request)`
|
|
17
|
+
|
|
18
|
+
Create/edit an organization's record (name, description, contact info, branding, addresses, default roles/permissions for new members, etc. — verify current field names against what the admin UI/API actually accepts rather than assuming they haven't changed).
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
// Only after the admin has reviewed and confirmed this exact payload:
|
|
22
|
+
await blocksClient.iam.organizations.create({
|
|
23
|
+
name: "Acme Logistics",
|
|
24
|
+
description: "Acme's logistics division workspace"
|
|
25
|
+
// ...remaining fields the user confirmed
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
await blocksClient.iam.organizations.update(organizationId, {
|
|
29
|
+
name: "Acme Logistics (Renamed)"
|
|
30
|
+
// ...only the fields the user asked to change
|
|
31
|
+
});
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### `organizations.getConfig()` / `organizations.saveConfig(request)` — org-level settings
|
|
35
|
+
|
|
36
|
+
Project/org-wide policy, including multi-org enablement. Read the current config first, show the user the diff of what would change, then save.
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
const current = await blocksClient.iam.organizations.getConfig();
|
|
40
|
+
// ...user reviews current vs. proposed, confirms...
|
|
41
|
+
await blocksClient.iam.organizations.saveConfig({
|
|
42
|
+
...current,
|
|
43
|
+
isMultiOrgEnabled: true
|
|
44
|
+
});
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### `signupSettings.save(request)` — save signup policy
|
|
48
|
+
|
|
49
|
+
Same confirm-first rule: this changes what the public signup screen allows for every future visitor, so restate the exact policy change (e.g., "turn on org creation from signup") before calling.
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
await blocksClient.iam.signupSettings.save({
|
|
53
|
+
...currentSettings,
|
|
54
|
+
allowOrgCreationFromSignup: true
|
|
55
|
+
});
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## CLI surface — scripting/inspection, outside an app UI
|
|
59
|
+
|
|
60
|
+
For anything that isn't a feature inside a running Blocks app (one-off lookups, ops scripts, CI checks), the `blocks` CLI has a matching, project-scoped command for every SDK method above except `switchOrganization` (see [read-and-switch.md](read-and-switch.md)). Every command below requires a project already selected (`blocks use <tenantId>`) — they all call IAM through an impersonated project token via `selectedProject()`, the same tenant scoping the SDK calls get from the app's own session.
|
|
61
|
+
|
|
62
|
+
| CLI command | Maps to | Notes |
|
|
63
|
+
|---|---|---|
|
|
64
|
+
| `blocks iam organizations list` | `organizations.list()` | `--search`, `--is-disabled`, `--parent-organization-id`, `--ids <a,b>`, `--page`, `--page-size`, `--sort-by`, `--sort-desc`. Read-only. |
|
|
65
|
+
| `blocks iam organizations get <id>` (or `--id`) | `organizations.get(id)` | Read-only. |
|
|
66
|
+
| `blocks iam organizations my` | `organizations.my()` | Read-only. |
|
|
67
|
+
| `blocks iam organizations create --name <n> [...]` | `organizations.create()` | `--name` (required), `--description`, `--email`, `--phone-number`, `--website-url`, `--default-permissions`, `--default-roles`, or a full `--body '<json>'`/`--file <path.json>`. Mutation. |
|
|
68
|
+
| `blocks iam organizations update <id> [...]` | `organizations.update(id, ...)` | `--name`, `--description`, `--email`, `--phone-number`, `--website-url`, `--currency`, `--industry`, `--locale`, `--time-zone`, `--is-enabled`, or `--body`/`--file`. Mutation. |
|
|
69
|
+
| `blocks iam organizations config get` | `organizations.getConfig()` | Read-only. |
|
|
70
|
+
| `blocks iam organizations config save [...]` | `organizations.saveConfig()` | `--multi-org-enabled`, `--consent-for-multi-org-enable`, `--allow-org-creation-from-signup`, `--allow-org-creation-from-portal`, `--allow-org-creation-from-cloud`, `--allow-org-creation-from-construct`, or `--body`/`--file`. Mutation. |
|
|
71
|
+
| `blocks iam signup-settings get` | `signupSettings.get()` | Read-only. |
|
|
72
|
+
| `blocks iam signup-settings save [...]` | `signupSettings.save()` | `--email-password-signup`, `--sso-signup`, `--default-permissions`, `--default-roles`, or `--body`/`--file`. Mutation. |
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
blocks use <tenantId> # select the project once per session
|
|
76
|
+
blocks iam organizations list --search acme --json
|
|
77
|
+
blocks iam organizations config get --json
|
|
78
|
+
blocks iam organizations config save --multi-org-enabled --dry-run # review the request body first
|
|
79
|
+
blocks iam organizations config save --multi-org-enabled --yes # only after the user confirms
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Every mutating command (`create`, `update`, `config save`, `signup-settings save`) supports `--dry-run` (prints the resolved request body without calling the API) and requires either `--yes` or an interactive `yes` at a confirmation prompt to actually run — the same "state the exact change, get an explicit go-ahead" discipline as the SDK guidance above, just enforced by the CLI itself instead of app code you write.
|
|
83
|
+
|
|
84
|
+
## Gotchas
|
|
85
|
+
|
|
86
|
+
- **The CLI has real organization/signup-settings commands** — don't tell a user there's no `blocks` command for organizations; there is, it's just project-scoped and separate from the SDK path used inside an app.
|
|
87
|
+
- **Confirm the payload, not just the intent**, before any `create`/`update`/`saveConfig`/`signupSettings.save` call — SDK or CLI — restate which organization and which fields are changing, old value vs. new, and wait for a clear yes. On the CLI this means running with `--dry-run` first and only adding `--yes` after that review.
|
|
88
|
+
- **These are legitimate app/operator features, not agent shortcuts.** It's fine to build a "Create Organization" admin screen with a confirm dialog, or run the CLI command yourself with a human's go-ahead — that's the intended use. It's not fine for an agent to call `organizations.create`/`saveConfig` (CLI or SDK) on its own initiative (e.g., to "set things up" for a demo) without that human-in-the-loop step.
|
|
89
|
+
- **Different response shapes per call** — `my()`/`list()` return an array-shaped payload, `get(id)` returns a single organization, `create` returns an id. Don't assume a single envelope shape across all of them; check the actual response before wiring UI to a field path.
|
|
@@ -1,57 +1,57 @@
|
|
|
1
|
-
# Flow: Read / self-service surface (safe, no special caveat)
|
|
2
|
-
|
|
3
|
-
These are read-only or scoped to switching the *caller's own* context. No confirmation ritual needed beyond normal engineering judgment.
|
|
4
|
-
|
|
5
|
-
## `organizations.my()` — the org switcher
|
|
6
|
-
|
|
7
|
-
Returns the signed-in user's own available organizations — the standard source for an org switcher / "pick your workspace" UI. Requires the user to already be authenticated (pair with `useCurrentUser` / `blocksClient.iam.me()` from the onboarding/profile scaffold).
|
|
8
|
-
|
|
9
|
-
```ts
|
|
10
|
-
// src/features/organizations/useMyOrganizations.ts
|
|
11
|
-
import { useQuery } from "@tanstack/react-query";
|
|
12
|
-
import { useAuth } from "../../app/providers/AuthProvider";
|
|
13
|
-
import { blocksClient } from "../../lib/blocks/client";
|
|
14
|
-
|
|
15
|
-
export function useMyOrganizations() {
|
|
16
|
-
const { status } = useAuth();
|
|
17
|
-
return useQuery({
|
|
18
|
-
enabled: status === "authenticated",
|
|
19
|
-
queryFn: () => blocksClient.iam.organizations.my(),
|
|
20
|
-
queryKey: ["iam", "organizations", "my"]
|
|
21
|
-
});
|
|
22
|
-
}
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
CLI equivalent: `blocks iam organizations my` (read-only, project-scoped).
|
|
26
|
-
|
|
27
|
-
## `auth.switchOrganization(request)` — change active org context
|
|
28
|
-
|
|
29
|
-
For a multi-org user, switches which organization the session is scoped to. Pass `{ organizationId, refreshToken }`; the response is a fresh `BlocksAuthResponse` (new tokens for the new org context). **If your app tracks its own session state (stored tokens, an auth context/provider), replace it with this response** — don't just call the endpoint and leave the old tokens in place, or subsequent calls will still act in the old org.
|
|
30
|
-
|
|
31
|
-
```ts
|
|
32
|
-
async function switchToOrganization(organizationId: string) {
|
|
33
|
-
const refreshToken = getRefreshToken(); // however this app's AuthProvider stores it
|
|
34
|
-
const response = await blocksClient.auth.switchOrganization({ organizationId, refreshToken });
|
|
35
|
-
applyAuthResponse(response); // app-owned: persist new tokens, refresh useCurrentUser/useMyOrganizations
|
|
36
|
-
}
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
This is user-directed (they picked an org in the switcher) so it doesn't need the admin-CRUD confirmation ritual — but it does change what the rest of the session sees, so trigger it from an explicit user action (selecting an item in the switcher), not silently.
|
|
40
|
-
|
|
41
|
-
**No CLI equivalent exists for this one** — switching the *active session's* org context only makes sense from inside the app that owns that session, so it's SDK/app-only. Don't invent a `blocks iam organizations switch` command.
|
|
42
|
-
|
|
43
|
-
## `signupSettings.get()` — public signup screen
|
|
44
|
-
|
|
45
|
-
Public — no auth required (the SDK still sends `x-blocks-key`). Read this on a public signup page to know the tenant's current signup policy (e.g., whether self-signup or org-creation-from-signup is allowed) before rendering the form.
|
|
46
|
-
|
|
47
|
-
```ts
|
|
48
|
-
const settings = await blocksClient.iam.signupSettings.get();
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
CLI equivalent for scripting/inspection: `blocks iam signup-settings get --json` (no SDK needed, no app context needed).
|
|
52
|
-
|
|
53
|
-
## Gotchas
|
|
54
|
-
|
|
55
|
-
- **`switchOrganization` replaces session state.** If the app persists tokens (localStorage, an AuthProvider, React Query cache), apply the new `BlocksAuthResponse` fully — a stale access token after switching orgs will produce confusing "wrong org" data on the next call.
|
|
56
|
-
- **`organizations.my()` needs the user already authenticated** — call it after `iam.me()`/`useCurrentUser` resolves, not before, or you'll get an auth failure that looks like "no orgs" but actually means "not logged in yet."
|
|
57
|
-
- **Multi-org must be enabled** (`isMultiOrgEnabled` via `organizations.getConfig()` — see [admin-mutations.md](admin-mutations.md)) for more than one org per user to be meaningful — if a user reports "switching orgs doesn't do anything," check this first.
|
|
1
|
+
# Flow: Read / self-service surface (safe, no special caveat)
|
|
2
|
+
|
|
3
|
+
These are read-only or scoped to switching the *caller's own* context. No confirmation ritual needed beyond normal engineering judgment.
|
|
4
|
+
|
|
5
|
+
## `organizations.my()` — the org switcher
|
|
6
|
+
|
|
7
|
+
Returns the signed-in user's own available organizations — the standard source for an org switcher / "pick your workspace" UI. Requires the user to already be authenticated (pair with `useCurrentUser` / `blocksClient.iam.me()` from the onboarding/profile scaffold).
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
// src/features/organizations/useMyOrganizations.ts
|
|
11
|
+
import { useQuery } from "@tanstack/react-query";
|
|
12
|
+
import { useAuth } from "../../app/providers/AuthProvider";
|
|
13
|
+
import { blocksClient } from "../../lib/blocks/client";
|
|
14
|
+
|
|
15
|
+
export function useMyOrganizations() {
|
|
16
|
+
const { status } = useAuth();
|
|
17
|
+
return useQuery({
|
|
18
|
+
enabled: status === "authenticated",
|
|
19
|
+
queryFn: () => blocksClient.iam.organizations.my(),
|
|
20
|
+
queryKey: ["iam", "organizations", "my"]
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
CLI equivalent: `blocks iam organizations my` (read-only, project-scoped).
|
|
26
|
+
|
|
27
|
+
## `auth.switchOrganization(request)` — change active org context
|
|
28
|
+
|
|
29
|
+
For a multi-org user, switches which organization the session is scoped to. Pass `{ organizationId, refreshToken }`; the response is a fresh `BlocksAuthResponse` (new tokens for the new org context). **If your app tracks its own session state (stored tokens, an auth context/provider), replace it with this response** — don't just call the endpoint and leave the old tokens in place, or subsequent calls will still act in the old org.
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
async function switchToOrganization(organizationId: string) {
|
|
33
|
+
const refreshToken = getRefreshToken(); // however this app's AuthProvider stores it
|
|
34
|
+
const response = await blocksClient.auth.switchOrganization({ organizationId, refreshToken });
|
|
35
|
+
applyAuthResponse(response); // app-owned: persist new tokens, refresh useCurrentUser/useMyOrganizations
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
This is user-directed (they picked an org in the switcher) so it doesn't need the admin-CRUD confirmation ritual — but it does change what the rest of the session sees, so trigger it from an explicit user action (selecting an item in the switcher), not silently.
|
|
40
|
+
|
|
41
|
+
**No CLI equivalent exists for this one** — switching the *active session's* org context only makes sense from inside the app that owns that session, so it's SDK/app-only. Don't invent a `blocks iam organizations switch` command.
|
|
42
|
+
|
|
43
|
+
## `signupSettings.get()` — public signup screen
|
|
44
|
+
|
|
45
|
+
Public — no auth required (the SDK still sends `x-blocks-key`). Read this on a public signup page to know the tenant's current signup policy (e.g., whether self-signup or org-creation-from-signup is allowed) before rendering the form.
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
const settings = await blocksClient.iam.signupSettings.get();
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
CLI equivalent for scripting/inspection: `blocks iam signup-settings get --json` (no SDK needed, no app context needed).
|
|
52
|
+
|
|
53
|
+
## Gotchas
|
|
54
|
+
|
|
55
|
+
- **`switchOrganization` replaces session state.** If the app persists tokens (localStorage, an AuthProvider, React Query cache), apply the new `BlocksAuthResponse` fully — a stale access token after switching orgs will produce confusing "wrong org" data on the next call.
|
|
56
|
+
- **`organizations.my()` needs the user already authenticated** — call it after `iam.me()`/`useCurrentUser` resolves, not before, or you'll get an auth failure that looks like "no orgs" but actually means "not logged in yet."
|
|
57
|
+
- **Multi-org must be enabled** (`isMultiOrgEnabled` via `organizations.getConfig()` — see [admin-mutations.md](admin-mutations.md)) for more than one org per user to be meaningful — if a user reports "switching orgs doesn't do anything," check this first.
|
|
@@ -1,105 +1,105 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: blocks-iam-sso-oidc-configuration
|
|
3
|
-
description: "Enable/configure SSO for a Blocks project — register an OIDC client and identity provider so end users can log into the app via hosted login. Use for 'enable SSO', 'set up an OIDC identity provider', 'configure single sign-on', 'add a login provider'. CLI-driven by default (`blocks auth oidc-clients *` / `auth idp *`, project-scoped, --dry-run→--yes), not portal-only — the portal remains a valid alternative, especially for federated external providers (Google/Azure/Okta). Don't confuse with `blocks login` (the CLI's own login — see blocks-onboarding)."
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Blocks IAM — SSO / OIDC Configuration
|
|
7
|
-
|
|
8
|
-
Setting up SSO for a Blocks project means provisioning two related tenant records: an **OIDC client** (the app-facing public client used for hosted login) and an **identity provider** (the record the hosted-login redirect/callback flow actually authenticates against). Both are exposed by real, implemented `blocks` CLI commands — this is not a portal-only action.
|
|
9
|
-
|
|
10
|
-
## The one thing to get right: which login is this?
|
|
11
|
-
|
|
12
|
-
Don't conflate the CLI's own login with the identity provider this skill configures.
|
|
13
|
-
|
|
14
|
-
| | `blocks login` | The one THIS skill covers |
|
|
15
|
-
|---|---|---|
|
|
16
|
-
| What it's for | Lets `blocks` itself authenticate | Lets **end users log into the user's own app** via hosted SSO |
|
|
17
|
-
| Client type | Packaged into the CLI - nothing to register, no secret to hold | Public (browser client, no secret) |
|
|
18
|
-
| Registered via | Nothing to register - just run `blocks login` | `blocks auth oidc-clients save` / `blocks auth idp create`, or the portal |
|
|
19
|
-
| Owned by | **blocks-onboarding** skill | **This skill**, handing off to **blocks-iam-sso-oidc-implementation** |
|
|
20
|
-
|
|
21
|
-
If the user is asking "how do I get `blocks` logged in" or hits `not_logged_in`, that's **blocks-onboarding**, not this skill. This skill is about the identity provider that sits in front of *the user's own application's* login page.
|
|
22
|
-
|
|
23
|
-
## Decision tree
|
|
24
|
-
|
|
25
|
-
All of these commands are project-scoped: they need a selected project (`blocks use <tenantId>` or `--project`) and run against an impersonated project token, not the CLI's own account token.
|
|
26
|
-
|
|
27
|
-
1. **Check for an existing OIDC client.** `blocks auth oidc-clients list [--json]` / `blocks auth oidc-clients get <clientId> [--json]`. `client_secret` is excluded from list/get responses — you only ever see it once, at creation or `rotate-secret` time. If a suitable public client already exists (matching redirect URI / display name), reuse its id — you're done, skip to handoff.
|
|
28
|
-
2. **If none exists, create one:**
|
|
29
|
-
```
|
|
30
|
-
blocks auth oidc-clients save \
|
|
31
|
-
--client-display-name "<app name>" \
|
|
32
|
-
--client-type public \
|
|
33
|
-
--redirect-uris "https://<app-domain>/login/callback" \
|
|
34
|
-
--require-pkce --active \
|
|
35
|
-
--scope "openid profile" \
|
|
36
|
-
--register-as-identity-provider \
|
|
37
|
-
[--dry-run] [--yes]
|
|
38
|
-
```
|
|
39
|
-
This mirrors exactly what `blocks new web`'s interactive OIDC-client prompt does when scaffolding a new web app. `--register-as-identity-provider` is what turns this from "just an OIDC client" into something the hosted-login redirect flow (`auth.idp.redirectToProvider()` / `auth.idp.callback()`) can authenticate against — per the CLI's own scaffold help text, this registers the client "as a Blocks OIDC identity provider" in the same call.
|
|
40
|
-
3. **Verify the auto-created provider before handing off.** `--register-as-identity-provider` creates the provider record for you — but check what landed in it with `blocks auth idp list --json`, because on the common path several fields come back null. See the footguns below. If `authorizationUrl` is null, hosted login will not redirect: `GET /iam/v4/idp/initiate` (what `auth.idp.redirectToProvider()` calls) builds its target as `provider.AuthorizationUrl ?? ""` plus a query string, so the browser navigates to the app's own origin with OIDC params attached. The repair, for a provider that already exists in that state:
|
|
41
|
-
```
|
|
42
|
-
blocks auth idp update <providerItemId> \
|
|
43
|
-
--authorization-url "<tenant authorize endpoint>" \
|
|
44
|
-
--token-url "<tenant token endpoint>" \
|
|
45
|
-
--user-info-url "<tenant userinfo endpoint>" \
|
|
46
|
-
[--dry-run] [--yes]
|
|
47
|
-
```
|
|
48
|
-
`idp update` is the only route that persists these three — IAM's create path accepts them in its request model and drops them, and the repository's update is a plain replace with no re-discovery, so values set here stick. **Read the tenant's discovery document for the correct endpoint values rather than composing them by hand** — see the last footgun.
|
|
49
|
-
4. **Inspect/manage the resulting identity-provider record** with `blocks auth idp list [--json]` / `blocks auth idp get <id> [--json]`. Use `blocks auth idp status <id> --active|--active=false` to enable/disable without deleting, and `blocks auth idp delete <id>` to remove it — deleting an identity provider **also deletes its related OIDC client registration**, so treat `idp delete` as the higher-blast-radius operation of the two.
|
|
50
|
-
5. **`blocks auth idp create`/`update` exist as a separate, more general path** for constructing an identity-provider record directly — most relevant when federating an *external* identity provider (Google, Azure AD, Okta, etc.) rather than using Blocks' own OIDC client as the login mechanism:
|
|
51
|
-
```
|
|
52
|
-
blocks auth idp create --provider <p> --provider-type <t> --protocol <proto> \
|
|
53
|
-
--client-id <id> [--client-secret <secret>] [--display-name] [--issuer] \
|
|
54
|
-
[--scope] [--redirect-uris a,b] [--active] \
|
|
55
|
-
[--authorization-url] [--token-url] [--user-info-url] [--jwks-uri] \
|
|
56
|
-
[--well-known-url] [--response-type] [--grant-types a,b] [--require-pkce] \
|
|
57
|
-
[--token-endpoint-auth-method] [--initial-roles a,b] [--initial-permissions a,b] \
|
|
58
|
-
[--icon] [--body '<json>'|--file <path>] [--dry-run] [--yes]
|
|
59
|
-
```
|
|
60
|
-
`--provider`, `--provider-type`, `--protocol`, and `--client-id` are required on create, and are immutable afterward — `auth idp update <id>` accepts the same flags but IAM requires you to either omit them or echo the existing values exactly. Apple-specific fields (`teamId`, `keyId`, `privateKey`, `appleAudience`) go through `--body`/`--file` so no private key lands in shell history. Note that `create` stores `issuer`, `jwksUri` and `wellKnownUrl` but silently drops `authorizationUrl`, `tokenUrl` and `userInfoUrl` — pass those to `idp update` in a second call. **How exactly a `clientId` passed here pairs with an OIDC client record is not shown anywhere documented** — the two collections are related (per the cascading delete behavior above) but the create/update commands don't expose an explicit "link to this OIDC client" field beyond passing the same id. If you need to federate an external provider, treat `idp create`'s field values as IAM's contract and confirm anything beyond the flags above against the tenant's actual behavior rather than guessing.
|
|
61
|
-
6. **Hand off.** Once a client id (and, if relevant, an identity-provider id) exists, the frontend wiring — login button, callback route, token handling, `client.auth.idp.initiate()`/`redirectToProvider()`/`.callback()` from `@seliseblocks/client` — is owned by **blocks-iam-sso-oidc-implementation**. Do not duplicate that work here; route to it.
|
|
62
|
-
|
|
63
|
-
## Mutation discipline
|
|
64
|
-
|
|
65
|
-
Every create/update/delete/status/rotate-secret command above follows the same pattern as the rest of `blocks`:
|
|
66
|
-
- `--dry-run` prints the request body and target endpoint without sending it (secrets are redacted in the printed body).
|
|
67
|
-
- Without `--dry-run`, the command prompts "Type 'yes' to continue" before mutating anything, unless `--yes` is passed to skip the prompt.
|
|
68
|
-
- These are real tenant-security actions (an identity provider or public OIDC client controls who can authenticate as a given app's users) — always show the user what will happen (favor `--dry-run` first) rather than running mutations silently, and don't add `--yes` to a call the user hasn't actually approved.
|
|
69
|
-
|
|
70
|
-
Never raw `fetch`/`curl` these endpoints to route around the CLI's confirmation/dry-run discipline — use the commands above so the same guardrails apply.
|
|
71
|
-
|
|
72
|
-
## Verified footguns
|
|
73
|
-
|
|
74
|
-
- **`--client-type public` is not cosmetic — omitting it stores a browser app as confidential.** IAM derives `tokenEndpointAuthMethod` from `clientType`: `public` (or any device-flow client) becomes `"none"`, anything else becomes `"client_secret_post"`. Leave `--client-type` off and a PKCE SPA is persisted as a confidential client that is also eligible for the `client_credentials` grant. Always pass `--client-type public` for a browser client. `--require-pkce` alone does not imply it.
|
|
75
|
-
- **The auto-created provider's endpoint URLs come from discovery, and discovery is driven by one field.** IAM's repository-level `CreateIdentityProviderAsync` runs `PopulateProviderEndpointsFromWellKnownAsync` before inserting: if `wellKnownUrl` is set it fetches the document and fills `authorizationUrl`, `tokenUrl`, `userInfoUrl`, `jwksUri` and `issuer` from it. The only input that reaches `wellKnownUrl` on this path is `oidc-clients save --external-discovery-endpoint`. Omit it and the `else` branch runs `GetSocialMetadata(provider)`, which matches only names containing `google` or `microsoft` — for an app-named provider it returns null, so all five fields are written null **and `scope` is overwritten with `"openid profile email"`**, discarding the `offline_access` the OIDC client had just been given. Check both `authorizationUrl` and `scope` on the provider after registering.
|
|
76
|
-
- **This only happens at create.** Re-saving the same OIDC client does not re-run discovery: the `existingProvider` branch never touches `wellKnownUrl`, and the repository's update is a plain replace. A provider already written with null URLs cannot be repaired by re-saving the client — use `idp update`, or delete and recreate.
|
|
77
|
-
- **PKCE and the discovery URL exist on both records and mean different things.** `requirePkce` on the OIDC client governs the app's own authorize flow; `--require-pkce` on `auth idp` governs the *upstream* handshake `/idp/initiate` performs. `--external-discovery-endpoint` on the client is read only as the linked provider's `wellKnownUrl`; on the provider record itself use `--well-known-url`.
|
|
78
|
-
- **Do not compose the tenant's own discovery or authorize URL from a template.** `DiscoveryController` declares `/{tenant_id}/.well-known/openid-configuration` as an absolute route, outside the `/iam/v4` prefix that every other IAM endpoint sits behind, and every `wellKnownUrl` example in IAM's own source and tests is an *external* provider (`accounts.google.com`, `login.microsoftonline.com`, `idp.example.com`) — there is no in-repo example of a Blocks tenant pointing at itself. Whether that route resolves through the `blocksapi.<domain>` gateway as-is or needs an extra segment is **not settled in source**. Fetch the tenant's discovery document and read the endpoints out of it, or ask the user; do not assert a shape you have not seen respond.
|
|
79
|
-
|
|
80
|
-
## Secondary, optional: the SDK's `identityProviders` admin methods
|
|
81
|
-
|
|
82
|
-
`@seliseblocks/client` (see `auth-client.ts`, the `readonly identityProviders = { list, get, create, update, updateStatus, delete }` block) also exposes typed methods that call the same identity-provider resource the CLI's `auth idp` commands hit. Reach for this when you're building an **in-app admin settings screen** for a signed-in administrator, where *they* click a button labeled something like "Add identity provider" and *they* fill in a form, in the moment they personally intend to make that change:
|
|
83
|
-
|
|
84
|
-
```tsx
|
|
85
|
-
// A settings page for a signed-in admin user. The admin types into the form
|
|
86
|
-
// and clicks "Save" themselves — the SDK call fires from THEIR click handler.
|
|
87
|
-
async function onSaveClicked(formValues: IdentityProviderFormValues) {
|
|
88
|
-
await client.auth.identityProviders.create(formValues); // admin-initiated, in the moment
|
|
89
|
-
}
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
Request/payload types on these SDK methods are intentionally loose (`Record<string, unknown>` passthrough) — confirm field names against the same contract the CLI's `auth idp create` flags document (`provider`, `providerType`, `protocol`, `clientId`, etc.) rather than guessing new ones.
|
|
93
|
-
|
|
94
|
-
## Related skills
|
|
95
|
-
|
|
96
|
-
- **blocks-onboarding** — owns `blocks login` itself (authenticates with no setup, nothing to register or look up). Go there first if `blocks` itself isn't authenticated, or if the user is conflating "logging in the CLI" with "SSO for my app."
|
|
97
|
-
- **blocks-iam-sso-oidc-implementation** — owns everything that happens once an identity provider/client id exists: wiring the login button, callback route, and token/session handling in the scaffolded React app using `@seliseblocks/client`. This skill hands off to it and does not duplicate its content.
|
|
98
|
-
|
|
99
|
-
## Example trigger prompts → routing
|
|
100
|
-
|
|
101
|
-
- "Enable SSO for my project" / "Set up an OIDC identity provider" / "Configure single sign-on for my app" → confirm it's the app's end-user login (not the CLI's), run the decision tree above (`auth oidc-clients list/get` → `auth oidc-clients save --register-as-identity-provider` if none exists), then hand off to **blocks-iam-sso-oidc-implementation**.
|
|
102
|
-
- "Register an OIDC client so users can log in" → `blocks auth oidc-clients list`/`get` first to avoid duplicates, then `blocks auth oidc-clients save` with `--dry-run` shown to the user before confirming.
|
|
103
|
-
- "Can you just create the identity provider via the API so I don't have to click through the portal?" → yes — walk them through `blocks auth oidc-clients save` / `blocks auth idp create` with `--dry-run` first, get explicit confirmation before dropping `--yes`, and mention the portal (https://os.seliseblocks.com) as an alternative if they'd rather use a GUI, especially for federated external providers where they need to register with that provider first.
|
|
104
|
-
- "blocks login isn't working" / "not_logged_in" → this is the CLI's own login, not this skill — route to **blocks-onboarding**.
|
|
105
|
-
- "I want an admin page in my app where I can manage identity providers" → this skill's SDK section applies: help build the settings screen calling `identityProviders.list/create/update/delete` from the admin's own button clicks.
|
|
1
|
+
---
|
|
2
|
+
name: blocks-iam-sso-oidc-configuration
|
|
3
|
+
description: "Enable/configure SSO for a Blocks project — register an OIDC client and identity provider so end users can log into the app via hosted login. Use for 'enable SSO', 'set up an OIDC identity provider', 'configure single sign-on', 'add a login provider'. CLI-driven by default (`blocks auth oidc-clients *` / `auth idp *`, project-scoped, --dry-run→--yes), not portal-only — the portal remains a valid alternative, especially for federated external providers (Google/Azure/Okta). Don't confuse with `blocks login` (the CLI's own login — see blocks-onboarding)."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Blocks IAM — SSO / OIDC Configuration
|
|
7
|
+
|
|
8
|
+
Setting up SSO for a Blocks project means provisioning two related tenant records: an **OIDC client** (the app-facing public client used for hosted login) and an **identity provider** (the record the hosted-login redirect/callback flow actually authenticates against). Both are exposed by real, implemented `blocks` CLI commands — this is not a portal-only action.
|
|
9
|
+
|
|
10
|
+
## The one thing to get right: which login is this?
|
|
11
|
+
|
|
12
|
+
Don't conflate the CLI's own login with the identity provider this skill configures.
|
|
13
|
+
|
|
14
|
+
| | `blocks login` | The one THIS skill covers |
|
|
15
|
+
|---|---|---|
|
|
16
|
+
| What it's for | Lets `blocks` itself authenticate | Lets **end users log into the user's own app** via hosted SSO |
|
|
17
|
+
| Client type | Packaged into the CLI - nothing to register, no secret to hold | Public (browser client, no secret) |
|
|
18
|
+
| Registered via | Nothing to register - just run `blocks login` | `blocks auth oidc-clients save` / `blocks auth idp create`, or the portal |
|
|
19
|
+
| Owned by | **blocks-onboarding** skill | **This skill**, handing off to **blocks-iam-sso-oidc-implementation** |
|
|
20
|
+
|
|
21
|
+
If the user is asking "how do I get `blocks` logged in" or hits `not_logged_in`, that's **blocks-onboarding**, not this skill. This skill is about the identity provider that sits in front of *the user's own application's* login page.
|
|
22
|
+
|
|
23
|
+
## Decision tree
|
|
24
|
+
|
|
25
|
+
All of these commands are project-scoped: they need a selected project (`blocks use <tenantId>` or `--project`) and run against an impersonated project token, not the CLI's own account token.
|
|
26
|
+
|
|
27
|
+
1. **Check for an existing OIDC client.** `blocks auth oidc-clients list [--json]` / `blocks auth oidc-clients get <clientId> [--json]`. `client_secret` is excluded from list/get responses — you only ever see it once, at creation or `rotate-secret` time. If a suitable public client already exists (matching redirect URI / display name), reuse its id — you're done, skip to handoff.
|
|
28
|
+
2. **If none exists, create one:**
|
|
29
|
+
```
|
|
30
|
+
blocks auth oidc-clients save \
|
|
31
|
+
--client-display-name "<app name>" \
|
|
32
|
+
--client-type public \
|
|
33
|
+
--redirect-uris "https://<app-domain>/login/callback" \
|
|
34
|
+
--require-pkce --active \
|
|
35
|
+
--scope "openid profile" \
|
|
36
|
+
--register-as-identity-provider \
|
|
37
|
+
[--dry-run] [--yes]
|
|
38
|
+
```
|
|
39
|
+
This mirrors exactly what `blocks new web`'s interactive OIDC-client prompt does when scaffolding a new web app. `--register-as-identity-provider` is what turns this from "just an OIDC client" into something the hosted-login redirect flow (`auth.idp.redirectToProvider()` / `auth.idp.callback()`) can authenticate against — per the CLI's own scaffold help text, this registers the client "as a Blocks OIDC identity provider" in the same call.
|
|
40
|
+
3. **Verify the auto-created provider before handing off.** `--register-as-identity-provider` creates the provider record for you — but check what landed in it with `blocks auth idp list --json`, because on the common path several fields come back null. See the footguns below. If `authorizationUrl` is null, hosted login will not redirect: `GET /iam/v4/idp/initiate` (what `auth.idp.redirectToProvider()` calls) builds its target as `provider.AuthorizationUrl ?? ""` plus a query string, so the browser navigates to the app's own origin with OIDC params attached. The repair, for a provider that already exists in that state:
|
|
41
|
+
```
|
|
42
|
+
blocks auth idp update <providerItemId> \
|
|
43
|
+
--authorization-url "<tenant authorize endpoint>" \
|
|
44
|
+
--token-url "<tenant token endpoint>" \
|
|
45
|
+
--user-info-url "<tenant userinfo endpoint>" \
|
|
46
|
+
[--dry-run] [--yes]
|
|
47
|
+
```
|
|
48
|
+
`idp update` is the only route that persists these three — IAM's create path accepts them in its request model and drops them, and the repository's update is a plain replace with no re-discovery, so values set here stick. **Read the tenant's discovery document for the correct endpoint values rather than composing them by hand** — see the last footgun.
|
|
49
|
+
4. **Inspect/manage the resulting identity-provider record** with `blocks auth idp list [--json]` / `blocks auth idp get <id> [--json]`. Use `blocks auth idp status <id> --active|--active=false` to enable/disable without deleting, and `blocks auth idp delete <id>` to remove it — deleting an identity provider **also deletes its related OIDC client registration**, so treat `idp delete` as the higher-blast-radius operation of the two.
|
|
50
|
+
5. **`blocks auth idp create`/`update` exist as a separate, more general path** for constructing an identity-provider record directly — most relevant when federating an *external* identity provider (Google, Azure AD, Okta, etc.) rather than using Blocks' own OIDC client as the login mechanism:
|
|
51
|
+
```
|
|
52
|
+
blocks auth idp create --provider <p> --provider-type <t> --protocol <proto> \
|
|
53
|
+
--client-id <id> [--client-secret <secret>] [--display-name] [--issuer] \
|
|
54
|
+
[--scope] [--redirect-uris a,b] [--active] \
|
|
55
|
+
[--authorization-url] [--token-url] [--user-info-url] [--jwks-uri] \
|
|
56
|
+
[--well-known-url] [--response-type] [--grant-types a,b] [--require-pkce] \
|
|
57
|
+
[--token-endpoint-auth-method] [--initial-roles a,b] [--initial-permissions a,b] \
|
|
58
|
+
[--icon] [--body '<json>'|--file <path>] [--dry-run] [--yes]
|
|
59
|
+
```
|
|
60
|
+
`--provider`, `--provider-type`, `--protocol`, and `--client-id` are required on create, and are immutable afterward — `auth idp update <id>` accepts the same flags but IAM requires you to either omit them or echo the existing values exactly. Apple-specific fields (`teamId`, `keyId`, `privateKey`, `appleAudience`) go through `--body`/`--file` so no private key lands in shell history. Note that `create` stores `issuer`, `jwksUri` and `wellKnownUrl` but silently drops `authorizationUrl`, `tokenUrl` and `userInfoUrl` — pass those to `idp update` in a second call. **How exactly a `clientId` passed here pairs with an OIDC client record is not shown anywhere documented** — the two collections are related (per the cascading delete behavior above) but the create/update commands don't expose an explicit "link to this OIDC client" field beyond passing the same id. If you need to federate an external provider, treat `idp create`'s field values as IAM's contract and confirm anything beyond the flags above against the tenant's actual behavior rather than guessing.
|
|
61
|
+
6. **Hand off.** Once a client id (and, if relevant, an identity-provider id) exists, the frontend wiring — login button, callback route, token handling, `client.auth.idp.initiate()`/`redirectToProvider()`/`.callback()` from `@seliseblocks/client` — is owned by **blocks-iam-sso-oidc-implementation**. Do not duplicate that work here; route to it.
|
|
62
|
+
|
|
63
|
+
## Mutation discipline
|
|
64
|
+
|
|
65
|
+
Every create/update/delete/status/rotate-secret command above follows the same pattern as the rest of `blocks`:
|
|
66
|
+
- `--dry-run` prints the request body and target endpoint without sending it (secrets are redacted in the printed body).
|
|
67
|
+
- Without `--dry-run`, the command prompts "Type 'yes' to continue" before mutating anything, unless `--yes` is passed to skip the prompt.
|
|
68
|
+
- These are real tenant-security actions (an identity provider or public OIDC client controls who can authenticate as a given app's users) — always show the user what will happen (favor `--dry-run` first) rather than running mutations silently, and don't add `--yes` to a call the user hasn't actually approved.
|
|
69
|
+
|
|
70
|
+
Never raw `fetch`/`curl` these endpoints to route around the CLI's confirmation/dry-run discipline — use the commands above so the same guardrails apply.
|
|
71
|
+
|
|
72
|
+
## Verified footguns
|
|
73
|
+
|
|
74
|
+
- **`--client-type public` is not cosmetic — omitting it stores a browser app as confidential.** IAM derives `tokenEndpointAuthMethod` from `clientType`: `public` (or any device-flow client) becomes `"none"`, anything else becomes `"client_secret_post"`. Leave `--client-type` off and a PKCE SPA is persisted as a confidential client that is also eligible for the `client_credentials` grant. Always pass `--client-type public` for a browser client. `--require-pkce` alone does not imply it.
|
|
75
|
+
- **The auto-created provider's endpoint URLs come from discovery, and discovery is driven by one field.** IAM's repository-level `CreateIdentityProviderAsync` runs `PopulateProviderEndpointsFromWellKnownAsync` before inserting: if `wellKnownUrl` is set it fetches the document and fills `authorizationUrl`, `tokenUrl`, `userInfoUrl`, `jwksUri` and `issuer` from it. The only input that reaches `wellKnownUrl` on this path is `oidc-clients save --external-discovery-endpoint`. Omit it and the `else` branch runs `GetSocialMetadata(provider)`, which matches only names containing `google` or `microsoft` — for an app-named provider it returns null, so all five fields are written null **and `scope` is overwritten with `"openid profile email"`**, discarding the `offline_access` the OIDC client had just been given. Check both `authorizationUrl` and `scope` on the provider after registering.
|
|
76
|
+
- **This only happens at create.** Re-saving the same OIDC client does not re-run discovery: the `existingProvider` branch never touches `wellKnownUrl`, and the repository's update is a plain replace. A provider already written with null URLs cannot be repaired by re-saving the client — use `idp update`, or delete and recreate.
|
|
77
|
+
- **PKCE and the discovery URL exist on both records and mean different things.** `requirePkce` on the OIDC client governs the app's own authorize flow; `--require-pkce` on `auth idp` governs the *upstream* handshake `/idp/initiate` performs. `--external-discovery-endpoint` on the client is read only as the linked provider's `wellKnownUrl`; on the provider record itself use `--well-known-url`.
|
|
78
|
+
- **Do not compose the tenant's own discovery or authorize URL from a template.** `DiscoveryController` declares `/{tenant_id}/.well-known/openid-configuration` as an absolute route, outside the `/iam/v4` prefix that every other IAM endpoint sits behind, and every `wellKnownUrl` example in IAM's own source and tests is an *external* provider (`accounts.google.com`, `login.microsoftonline.com`, `idp.example.com`) — there is no in-repo example of a Blocks tenant pointing at itself. Whether that route resolves through the `blocksapi.<domain>` gateway as-is or needs an extra segment is **not settled in source**. Fetch the tenant's discovery document and read the endpoints out of it, or ask the user; do not assert a shape you have not seen respond.
|
|
79
|
+
|
|
80
|
+
## Secondary, optional: the SDK's `identityProviders` admin methods
|
|
81
|
+
|
|
82
|
+
`@seliseblocks/client` (see `auth-client.ts`, the `readonly identityProviders = { list, get, create, update, updateStatus, delete }` block) also exposes typed methods that call the same identity-provider resource the CLI's `auth idp` commands hit. Reach for this when you're building an **in-app admin settings screen** for a signed-in administrator, where *they* click a button labeled something like "Add identity provider" and *they* fill in a form, in the moment they personally intend to make that change:
|
|
83
|
+
|
|
84
|
+
```tsx
|
|
85
|
+
// A settings page for a signed-in admin user. The admin types into the form
|
|
86
|
+
// and clicks "Save" themselves — the SDK call fires from THEIR click handler.
|
|
87
|
+
async function onSaveClicked(formValues: IdentityProviderFormValues) {
|
|
88
|
+
await client.auth.identityProviders.create(formValues); // admin-initiated, in the moment
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Request/payload types on these SDK methods are intentionally loose (`Record<string, unknown>` passthrough) — confirm field names against the same contract the CLI's `auth idp create` flags document (`provider`, `providerType`, `protocol`, `clientId`, etc.) rather than guessing new ones.
|
|
93
|
+
|
|
94
|
+
## Related skills
|
|
95
|
+
|
|
96
|
+
- **blocks-onboarding** — owns `blocks login` itself (authenticates with no setup, nothing to register or look up). Go there first if `blocks` itself isn't authenticated, or if the user is conflating "logging in the CLI" with "SSO for my app."
|
|
97
|
+
- **blocks-iam-sso-oidc-implementation** — owns everything that happens once an identity provider/client id exists: wiring the login button, callback route, and token/session handling in the scaffolded React app using `@seliseblocks/client`. This skill hands off to it and does not duplicate its content.
|
|
98
|
+
|
|
99
|
+
## Example trigger prompts → routing
|
|
100
|
+
|
|
101
|
+
- "Enable SSO for my project" / "Set up an OIDC identity provider" / "Configure single sign-on for my app" → confirm it's the app's end-user login (not the CLI's), run the decision tree above (`auth oidc-clients list/get` → `auth oidc-clients save --register-as-identity-provider` if none exists), then hand off to **blocks-iam-sso-oidc-implementation**.
|
|
102
|
+
- "Register an OIDC client so users can log in" → `blocks auth oidc-clients list`/`get` first to avoid duplicates, then `blocks auth oidc-clients save` with `--dry-run` shown to the user before confirming.
|
|
103
|
+
- "Can you just create the identity provider via the API so I don't have to click through the portal?" → yes — walk them through `blocks auth oidc-clients save` / `blocks auth idp create` with `--dry-run` first, get explicit confirmation before dropping `--yes`, and mention the portal (https://os.seliseblocks.com) as an alternative if they'd rather use a GUI, especially for federated external providers where they need to register with that provider first.
|
|
104
|
+
- "blocks login isn't working" / "not_logged_in" → this is the CLI's own login, not this skill — route to **blocks-onboarding**.
|
|
105
|
+
- "I want an admin page in my app where I can manage identity providers" → this skill's SDK section applies: help build the settings screen calling `identityProviders.list/create/update/delete` from the admin's own button clicks.
|