@seliseblocks/cli-os 0.2.7 → 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 +1 -1
- 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-onboarding/SKILL.md +5 -6
- 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,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.
|
|
@@ -1,95 +1,95 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: blocks-mail
|
|
3
|
-
description: "Send transactional email via the SDK's `blocksClient.mail.send()`/`sendToAny()`, or administer mail via the project-scoped `blocks mail config|template|mailbox *` CLI — server config, template CRUD/clone, mailbox reads, none of which have an SDK equivalent. CLI also exposes `mail send`/`sendtoany` as an admin/terminal mirror of the SDK calls. CLI mutations require `--dry-run` before `--yes`. Use for app email sending, or managing SMTP/inbound providers, templates, mailbox history."
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Blocks Mail
|
|
7
|
-
|
|
8
|
-
Blocks mail has **two distinct surfaces that don't fully overlap**:
|
|
9
|
-
|
|
10
|
-
- **SDK — `blocksClient.mail.send()` / `sendToAny()`** — the only mail operations exposed to app code. Use this when the question is "how do I send an email from my app."
|
|
11
|
-
- **CLI — `blocks mail config|template|mailbox *`** — server/provider configuration, email template CRUD/clone, and mailbox message reads. **No SDK equivalent at all.** The SDK's own documentation says so directly: mail server/template/mailbox management is a CLI/admin concern, not exposed to app code. If a user asks "how do I configure our SMTP provider from my app" or "how do I edit a template from code," the answer is: you don't — that's a `blocks mail config *` / `blocks mail template *` terminal command, not an SDK call.
|
|
12
|
-
- **CLI — `blocks mail send` / `blocks mail sendtoany`** — also exist, hitting the *same* underlying send as the SDK's `send`/`sendToAny`. These are the terminal/admin-token way to fire the same send, not a different feature — useful for testing a template from a shell or scripting a one-off send, but app runtime code should use the SDK call instead of shelling out.
|
|
13
|
-
|
|
14
|
-
## SDK — sending mail (`blocksClient.mail.*`)
|
|
15
|
-
|
|
16
|
-
```ts
|
|
17
|
-
import { blocksClient } from "../../lib/blocks/client";
|
|
18
|
-
|
|
19
|
-
await blocksClient.mail.send({
|
|
20
|
-
to: ["jane@example.com"],
|
|
21
|
-
purpose: "welcome",
|
|
22
|
-
language: "en",
|
|
23
|
-
subjectDataContext: { firstName: "Jane" },
|
|
24
|
-
bodyDataContext: { firstName: "Jane" }
|
|
25
|
-
});
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
- **`blocksClient.mail.send(request)`** — sends through the tenant's default mail configuration.
|
|
29
|
-
- **`blocksClient.mail.sendToAny(request)`** — same request shape plus `isTestMail`.
|
|
30
|
-
|
|
31
|
-
`BlocksSendMailRequest` fields: `to?: string[]`, `cc?: string[]`, `bcc?: string[]`, `replyTo?: string[]`, `attachments?: string[]`, `language?: string`, `purpose?: string`, `projectKey?: string` (defaults to the tenant's `x-blocks-key` server-side when omitted), `subjectDataContext?: Record<string, string>`, `bodyDataContext?: Record<string, string>`, `sendPhoneNumberAsEmail?: boolean`. `BlocksSendMailToAnyRequest` extends that with `isTestMail?: boolean`. The response type is an untyped `Record<string, unknown>` — the SDK doesn't shape the response further.
|
|
32
|
-
|
|
33
|
-
`purpose`/`language` are how the send picks a template server-side; the CLI and SDK don't define what `purpose` values exist for a given tenant — that comes from whatever templates were saved via `mail template save` (see below), so don't guess a purpose string that hasn't been confirmed to exist.
|
|
34
|
-
|
|
35
|
-
## CLI — administering mail (`blocks mail config|template|mailbox *`)
|
|
36
|
-
|
|
37
|
-
Everything under `mail config`, `mail template`, and `mail mailbox` is project-scoped: every command requires an impersonated project session, resolving the target project from whichever project is selected with `blocks use <tenantId>`, the workspace's `blocks.json`, or an explicit `--project <tenantId>`. There is no account-level mode for any mail command, including `mail send`/`mail sendtoany`.
|
|
38
|
-
|
|
39
|
-
### `mail config` — SMTP/inbound provider configuration
|
|
40
|
-
|
|
41
|
-
- **`blocks mail config list [--json]`** — read-only.
|
|
42
|
-
- **`blocks mail config get <name> [--json]`** — read-only (positional arg, or `--name`).
|
|
43
|
-
- **`blocks mail config save [--configuration-id <id>] [--name <n>] [--host <h>] [--port <p>] [--enable-ssl] [--inbound] [--provider <n>] [--sender-name <n>] [--sender-address <addr>] [--sender-username <u>] [--account-password <p>] [--body '<json>'|--file <path>] [--dry-run] [--yes] [--json]`** — upsert: omit `--configuration-id` to create, pass it to update. `--provider` and `--port` are raw integers (the CLI doesn't document the provider enum's meaning — don't guess a value). `--account-password` is redacted (`***`) in `--dry-run` output only; the live response and stored value are still sensitive.
|
|
44
|
-
- **`blocks mail config delete <configurationId> [--dry-run] [--yes] [--json]`**
|
|
45
|
-
- **`blocks mail config duplicate <configurationId> [--dry-run] [--yes] [--json]`**
|
|
46
|
-
|
|
47
|
-
### `mail template` — email template CRUD/clone
|
|
48
|
-
|
|
49
|
-
- **`blocks mail template list [--configuration-id <id>] [--language <l>] [--search <q>] [--sort-by <field>] [--sort-desc] [--page-number 1] [--page-size 20] [--json]`** — read-only.
|
|
50
|
-
- **`blocks mail template get <itemId> [--json]`** — read-only.
|
|
51
|
-
- **`blocks mail template save [--item-id <id>] [--name <n>] [--configuration-id <id>] [--language <l>] [--subject <s>] [--template-body <html>] [--json-content <json>] [--image-id <id>] [--image-url <url>] [--body '<json>'|--file <path>] [--dry-run] [--yes] [--json]`** — upsert: omit `--item-id` to create, pass it to update.
|
|
52
|
-
- **`blocks mail template delete <itemId> [--dry-run] [--yes] [--json]`**
|
|
53
|
-
- **`blocks mail template clone <itemId> [--name <n>] [--configuration-id <id>] [--language <l>] [--subject <s>] [--dry-run] [--yes] [--json]`**
|
|
54
|
-
|
|
55
|
-
### `mail mailbox` — mailbox message reads
|
|
56
|
-
|
|
57
|
-
- **`blocks mail mailbox list [--inbound] [--page-number 1] [--page-size 20] [--search <q>] [--start-date <date>] [--end-date <date>] [--status <s>] [--json]`** — read-only. There is **no `--configuration-id` flag** on this command (see Gotchas — this corrects a stale example elsewhere in this repo's own docs).
|
|
58
|
-
- **`blocks mail mailbox get <messageId> [--json]`** — read-only (positional arg, or `--id`).
|
|
59
|
-
|
|
60
|
-
### `mail send` / `mail sendtoany` — CLI mirror of the SDK send calls
|
|
61
|
-
|
|
62
|
-
- **`blocks mail send [--to a,b] [--cc a,b] [--bcc a,b] [--reply-to a,b] [--purpose <p>] [--language <l>] [--project-key <k>] [--subject-data-context '<json>'] [--body-data-context '<json>'] [--attachments '<json>'] [--send-phone-number-as-email] [--body '<json>'|--file <path>] [--dry-run] [--yes] [--json]`** — `--project-key` defaults to the selected project's tenant id.
|
|
63
|
-
- **`blocks mail sendtoany [same flags, plus --is-test-mail] [--dry-run] [--yes] [--json]`**
|
|
64
|
-
|
|
65
|
-
`--to`/`--cc`/`--bcc`/`--reply-to` are comma-separated lists (`a@x.com,b@y.com`); `--attachments`/`--subject-data-context`/`--body-data-context` take raw JSON strings (parsed as JSON, so quote them for the shell).
|
|
66
|
-
|
|
67
|
-
## Mutation discipline
|
|
68
|
-
|
|
69
|
-
Every write command (`config save/delete/duplicate`, `template save/delete/clone`, `send`, `sendtoany`) follows the same two-gate pattern used throughout this CLI:
|
|
70
|
-
|
|
71
|
-
1. **`--dry-run`** short-circuits before any network call and prints a full preview of exactly what would be sent, with secrets already redacted.
|
|
72
|
-
2. Without `--dry-run`, a confirmation step either accepts `--yes` outright or, interactively, prompts to type "yes" to continue, and cancels on anything else. There is no way to mutate without one of these two gates.
|
|
73
|
-
|
|
74
|
-
`list`/`get` commands under `config`, `template`, and `mailbox` never mutate and need neither flag.
|
|
75
|
-
|
|
76
|
-
## Gotchas
|
|
77
|
-
|
|
78
|
-
- **The premise that mail has no SDK path at all is wrong for sending.** `blocksClient.mail.send()`/`sendToAny()` exist and are the correct answer for "send email from my app." Only `config`/`template`/`mailbox` administration is CLI-only.
|
|
79
|
-
- **`mail mailbox list` does not take `--configuration-id`.** This CLI's own usage guide has previously shown an example with that flag that isn't backed by the actual flag list — the real command only reads `--inbound`, `--page-number`, `--page-size`, `--search`, `--start-date`, `--end-date`, `--status`. The CLI's flag parser silently ignores unrecognized `--` flags rather than erroring, so a stale example like that "works" without doing what it implies. Don't repeat it; use the real flags above.
|
|
80
|
-
- **`--account-password` (config save) is redacted only in `--dry-run` output.** The live `config save`/`config get` response is not redacted — treat it as a secret regardless.
|
|
81
|
-
- **`--provider` and `--port` on `config save` are raw values with no documented enum/meaning in the CLI** — don't invent what a given integer means; ask the user or read it back from `config get` on an existing configuration.
|
|
82
|
-
- **`purpose`/`language` on `send`/`sendtoany` select a template implicitly** — there's no lookup or validation for which `purpose` strings are valid for a tenant. Confirm against `mail template list`/`get` rather than guessing a purpose name.
|
|
83
|
-
- **`mail send` and `mail sendtoany` are still project-scoped CLI commands**, not account-level — same project-selection/impersonated-token requirement as `config`/`template`/`mailbox`.
|
|
84
|
-
- **`--dry-run` before `--yes`, always** — same discipline as every other mutating `blocks` command in this pack; never jump straight to `--yes` on a mail write.
|
|
85
|
-
|
|
86
|
-
## Example trigger prompts
|
|
87
|
-
|
|
88
|
-
- "Send a welcome email to jane@example.com from the app." → SDK `blocksClient.mail.send(...)`.
|
|
89
|
-
- "Send a test email to this address from the terminal." → `blocks mail sendtoany --to <addr> --is-test-mail --dry-run --json`, then `--yes` after approval.
|
|
90
|
-
- "List the mail server configurations for this project." → `blocks mail config list --json`.
|
|
91
|
-
- "Set up a new SMTP configuration for this project." → `blocks mail config save --name <n> --host <h> --port <p> --enable-ssl --sender-name <n> --sender-address <addr> --account-password <p> --dry-run --json`, then `--yes`.
|
|
92
|
-
- "Show me the password-reset email template." → `blocks mail template list --search <query> --json`, then `blocks mail template get <itemId> --json`.
|
|
93
|
-
- "Clone this template into a new language." → `blocks mail template clone <itemId> --language <code> --name <n> --dry-run --json`.
|
|
94
|
-
- "What mail was sent out last week?" → `blocks mail mailbox list --start-date <date> --end-date <date> --json`.
|
|
95
|
-
- "How do I edit an email template from my app's code?" → not supported; template CRUD is CLI-only (`blocks mail template save`), no SDK path.
|
|
1
|
+
---
|
|
2
|
+
name: blocks-mail
|
|
3
|
+
description: "Send transactional email via the SDK's `blocksClient.mail.send()`/`sendToAny()`, or administer mail via the project-scoped `blocks mail config|template|mailbox *` CLI — server config, template CRUD/clone, mailbox reads, none of which have an SDK equivalent. CLI also exposes `mail send`/`sendtoany` as an admin/terminal mirror of the SDK calls. CLI mutations require `--dry-run` before `--yes`. Use for app email sending, or managing SMTP/inbound providers, templates, mailbox history."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Blocks Mail
|
|
7
|
+
|
|
8
|
+
Blocks mail has **two distinct surfaces that don't fully overlap**:
|
|
9
|
+
|
|
10
|
+
- **SDK — `blocksClient.mail.send()` / `sendToAny()`** — the only mail operations exposed to app code. Use this when the question is "how do I send an email from my app."
|
|
11
|
+
- **CLI — `blocks mail config|template|mailbox *`** — server/provider configuration, email template CRUD/clone, and mailbox message reads. **No SDK equivalent at all.** The SDK's own documentation says so directly: mail server/template/mailbox management is a CLI/admin concern, not exposed to app code. If a user asks "how do I configure our SMTP provider from my app" or "how do I edit a template from code," the answer is: you don't — that's a `blocks mail config *` / `blocks mail template *` terminal command, not an SDK call.
|
|
12
|
+
- **CLI — `blocks mail send` / `blocks mail sendtoany`** — also exist, hitting the *same* underlying send as the SDK's `send`/`sendToAny`. These are the terminal/admin-token way to fire the same send, not a different feature — useful for testing a template from a shell or scripting a one-off send, but app runtime code should use the SDK call instead of shelling out.
|
|
13
|
+
|
|
14
|
+
## SDK — sending mail (`blocksClient.mail.*`)
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
import { blocksClient } from "../../lib/blocks/client";
|
|
18
|
+
|
|
19
|
+
await blocksClient.mail.send({
|
|
20
|
+
to: ["jane@example.com"],
|
|
21
|
+
purpose: "welcome",
|
|
22
|
+
language: "en",
|
|
23
|
+
subjectDataContext: { firstName: "Jane" },
|
|
24
|
+
bodyDataContext: { firstName: "Jane" }
|
|
25
|
+
});
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
- **`blocksClient.mail.send(request)`** — sends through the tenant's default mail configuration.
|
|
29
|
+
- **`blocksClient.mail.sendToAny(request)`** — same request shape plus `isTestMail`.
|
|
30
|
+
|
|
31
|
+
`BlocksSendMailRequest` fields: `to?: string[]`, `cc?: string[]`, `bcc?: string[]`, `replyTo?: string[]`, `attachments?: string[]`, `language?: string`, `purpose?: string`, `projectKey?: string` (defaults to the tenant's `x-blocks-key` server-side when omitted), `subjectDataContext?: Record<string, string>`, `bodyDataContext?: Record<string, string>`, `sendPhoneNumberAsEmail?: boolean`. `BlocksSendMailToAnyRequest` extends that with `isTestMail?: boolean`. The response type is an untyped `Record<string, unknown>` — the SDK doesn't shape the response further.
|
|
32
|
+
|
|
33
|
+
`purpose`/`language` are how the send picks a template server-side; the CLI and SDK don't define what `purpose` values exist for a given tenant — that comes from whatever templates were saved via `mail template save` (see below), so don't guess a purpose string that hasn't been confirmed to exist.
|
|
34
|
+
|
|
35
|
+
## CLI — administering mail (`blocks mail config|template|mailbox *`)
|
|
36
|
+
|
|
37
|
+
Everything under `mail config`, `mail template`, and `mail mailbox` is project-scoped: every command requires an impersonated project session, resolving the target project from whichever project is selected with `blocks use <tenantId>`, the workspace's `blocks.json`, or an explicit `--project <tenantId>`. There is no account-level mode for any mail command, including `mail send`/`mail sendtoany`.
|
|
38
|
+
|
|
39
|
+
### `mail config` — SMTP/inbound provider configuration
|
|
40
|
+
|
|
41
|
+
- **`blocks mail config list [--json]`** — read-only.
|
|
42
|
+
- **`blocks mail config get <name> [--json]`** — read-only (positional arg, or `--name`).
|
|
43
|
+
- **`blocks mail config save [--configuration-id <id>] [--name <n>] [--host <h>] [--port <p>] [--enable-ssl] [--inbound] [--provider <n>] [--sender-name <n>] [--sender-address <addr>] [--sender-username <u>] [--account-password <p>] [--body '<json>'|--file <path>] [--dry-run] [--yes] [--json]`** — upsert: omit `--configuration-id` to create, pass it to update. `--provider` and `--port` are raw integers (the CLI doesn't document the provider enum's meaning — don't guess a value). `--account-password` is redacted (`***`) in `--dry-run` output only; the live response and stored value are still sensitive.
|
|
44
|
+
- **`blocks mail config delete <configurationId> [--dry-run] [--yes] [--json]`**
|
|
45
|
+
- **`blocks mail config duplicate <configurationId> [--dry-run] [--yes] [--json]`**
|
|
46
|
+
|
|
47
|
+
### `mail template` — email template CRUD/clone
|
|
48
|
+
|
|
49
|
+
- **`blocks mail template list [--configuration-id <id>] [--language <l>] [--search <q>] [--sort-by <field>] [--sort-desc] [--page-number 1] [--page-size 20] [--json]`** — read-only.
|
|
50
|
+
- **`blocks mail template get <itemId> [--json]`** — read-only.
|
|
51
|
+
- **`blocks mail template save [--item-id <id>] [--name <n>] [--configuration-id <id>] [--language <l>] [--subject <s>] [--template-body <html>] [--json-content <json>] [--image-id <id>] [--image-url <url>] [--body '<json>'|--file <path>] [--dry-run] [--yes] [--json]`** — upsert: omit `--item-id` to create, pass it to update.
|
|
52
|
+
- **`blocks mail template delete <itemId> [--dry-run] [--yes] [--json]`**
|
|
53
|
+
- **`blocks mail template clone <itemId> [--name <n>] [--configuration-id <id>] [--language <l>] [--subject <s>] [--dry-run] [--yes] [--json]`**
|
|
54
|
+
|
|
55
|
+
### `mail mailbox` — mailbox message reads
|
|
56
|
+
|
|
57
|
+
- **`blocks mail mailbox list [--inbound] [--page-number 1] [--page-size 20] [--search <q>] [--start-date <date>] [--end-date <date>] [--status <s>] [--json]`** — read-only. There is **no `--configuration-id` flag** on this command (see Gotchas — this corrects a stale example elsewhere in this repo's own docs).
|
|
58
|
+
- **`blocks mail mailbox get <messageId> [--json]`** — read-only (positional arg, or `--id`).
|
|
59
|
+
|
|
60
|
+
### `mail send` / `mail sendtoany` — CLI mirror of the SDK send calls
|
|
61
|
+
|
|
62
|
+
- **`blocks mail send [--to a,b] [--cc a,b] [--bcc a,b] [--reply-to a,b] [--purpose <p>] [--language <l>] [--project-key <k>] [--subject-data-context '<json>'] [--body-data-context '<json>'] [--attachments '<json>'] [--send-phone-number-as-email] [--body '<json>'|--file <path>] [--dry-run] [--yes] [--json]`** — `--project-key` defaults to the selected project's tenant id.
|
|
63
|
+
- **`blocks mail sendtoany [same flags, plus --is-test-mail] [--dry-run] [--yes] [--json]`**
|
|
64
|
+
|
|
65
|
+
`--to`/`--cc`/`--bcc`/`--reply-to` are comma-separated lists (`a@x.com,b@y.com`); `--attachments`/`--subject-data-context`/`--body-data-context` take raw JSON strings (parsed as JSON, so quote them for the shell).
|
|
66
|
+
|
|
67
|
+
## Mutation discipline
|
|
68
|
+
|
|
69
|
+
Every write command (`config save/delete/duplicate`, `template save/delete/clone`, `send`, `sendtoany`) follows the same two-gate pattern used throughout this CLI:
|
|
70
|
+
|
|
71
|
+
1. **`--dry-run`** short-circuits before any network call and prints a full preview of exactly what would be sent, with secrets already redacted.
|
|
72
|
+
2. Without `--dry-run`, a confirmation step either accepts `--yes` outright or, interactively, prompts to type "yes" to continue, and cancels on anything else. There is no way to mutate without one of these two gates.
|
|
73
|
+
|
|
74
|
+
`list`/`get` commands under `config`, `template`, and `mailbox` never mutate and need neither flag.
|
|
75
|
+
|
|
76
|
+
## Gotchas
|
|
77
|
+
|
|
78
|
+
- **The premise that mail has no SDK path at all is wrong for sending.** `blocksClient.mail.send()`/`sendToAny()` exist and are the correct answer for "send email from my app." Only `config`/`template`/`mailbox` administration is CLI-only.
|
|
79
|
+
- **`mail mailbox list` does not take `--configuration-id`.** This CLI's own usage guide has previously shown an example with that flag that isn't backed by the actual flag list — the real command only reads `--inbound`, `--page-number`, `--page-size`, `--search`, `--start-date`, `--end-date`, `--status`. The CLI's flag parser silently ignores unrecognized `--` flags rather than erroring, so a stale example like that "works" without doing what it implies. Don't repeat it; use the real flags above.
|
|
80
|
+
- **`--account-password` (config save) is redacted only in `--dry-run` output.** The live `config save`/`config get` response is not redacted — treat it as a secret regardless.
|
|
81
|
+
- **`--provider` and `--port` on `config save` are raw values with no documented enum/meaning in the CLI** — don't invent what a given integer means; ask the user or read it back from `config get` on an existing configuration.
|
|
82
|
+
- **`purpose`/`language` on `send`/`sendtoany` select a template implicitly** — there's no lookup or validation for which `purpose` strings are valid for a tenant. Confirm against `mail template list`/`get` rather than guessing a purpose name.
|
|
83
|
+
- **`mail send` and `mail sendtoany` are still project-scoped CLI commands**, not account-level — same project-selection/impersonated-token requirement as `config`/`template`/`mailbox`.
|
|
84
|
+
- **`--dry-run` before `--yes`, always** — same discipline as every other mutating `blocks` command in this pack; never jump straight to `--yes` on a mail write.
|
|
85
|
+
|
|
86
|
+
## Example trigger prompts
|
|
87
|
+
|
|
88
|
+
- "Send a welcome email to jane@example.com from the app." → SDK `blocksClient.mail.send(...)`.
|
|
89
|
+
- "Send a test email to this address from the terminal." → `blocks mail sendtoany --to <addr> --is-test-mail --dry-run --json`, then `--yes` after approval.
|
|
90
|
+
- "List the mail server configurations for this project." → `blocks mail config list --json`.
|
|
91
|
+
- "Set up a new SMTP configuration for this project." → `blocks mail config save --name <n> --host <h> --port <p> --enable-ssl --sender-name <n> --sender-address <addr> --account-password <p> --dry-run --json`, then `--yes`.
|
|
92
|
+
- "Show me the password-reset email template." → `blocks mail template list --search <query> --json`, then `blocks mail template get <itemId> --json`.
|
|
93
|
+
- "Clone this template into a new language." → `blocks mail template clone <itemId> --language <code> --name <n> --dry-run --json`.
|
|
94
|
+
- "What mail was sent out last week?" → `blocks mail mailbox list --start-date <date> --end-date <date> --json`.
|
|
95
|
+
- "How do I edit an email template from my app's code?" → not supported; template CRUD is CLI-only (`blocks mail template save`), no SDK path.
|