@seliseblocks/cli-os 0.2.10 → 0.2.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AI_USAGE_GUIDE.md +11 -20
- package/LICENSE +21 -21
- package/README.md +3 -5
- package/bin/run.js +2 -2
- package/dist/commands/auth/oidc-clients/save.js +4 -1
- package/dist/commands/auth/refresh.js +21 -2
- package/dist/commands/mfa/generate.js +7 -4
- package/dist/commands/mfa/method-set.js +13 -4
- package/dist/commands/mfa/totp-enable.d.ts +3 -3
- package/dist/commands/mfa/totp-enable.js +4 -4
- package/dist/commands/mfa/verify.js +4 -2
- package/dist/commands/new/web.js +3 -2
- package/dist/commands/projects/create.js +110 -16
- package/dist/index.js +34 -40
- package/dist/lib/oidc-discovery.d.ts +2 -0
- package/dist/lib/oidc-discovery.js +12 -0
- package/package.json +47 -47
- package/dist/commands/sdk/client.d.ts +0 -1
- package/dist/commands/sdk/client.js +0 -99
- package/dist/commands/skill/add.d.ts +0 -1
- package/dist/commands/skill/add.js +0 -19
- package/dist/commands/skill/list.d.ts +0 -1
- package/dist/commands/skill/list.js +0 -15
- package/dist/commands/skill/show.d.ts +0 -1
- package/dist/commands/skill/show.js +0 -15
- package/dist/lib/skills.d.ts +0 -17
- package/dist/lib/skills.js +0 -69
- package/dist/skills/blocks-data-gateway-configuration/SKILL.md +0 -204
- package/dist/skills/blocks-data-gateway-crud/SKILL.md +0 -223
- package/dist/skills/blocks-data-storage/SKILL.md +0 -253
- package/dist/skills/blocks-data-storage/flows/object-management.md +0 -124
- package/dist/skills/blocks-frontend-local-https/SKILL.md +0 -100
- package/dist/skills/blocks-iam-access-control/SKILL.md +0 -49
- package/dist/skills/blocks-iam-access-control/flows/feature-gating.md +0 -38
- package/dist/skills/blocks-iam-access-control/flows/manage-roles-permissions.md +0 -110
- package/dist/skills/blocks-iam-account/SKILL.md +0 -169
- package/dist/skills/blocks-iam-mfa/SKILL.md +0 -124
- package/dist/skills/blocks-iam-organizations/SKILL.md +0 -43
- package/dist/skills/blocks-iam-organizations/flows/admin-mutations.md +0 -89
- package/dist/skills/blocks-iam-organizations/flows/read-and-switch.md +0 -57
- package/dist/skills/blocks-iam-sso-oidc-configuration/SKILL.md +0 -105
- package/dist/skills/blocks-iam-sso-oidc-implementation/SKILL.md +0 -80
- package/dist/skills/blocks-iam-users/SKILL.md +0 -131
- package/dist/skills/blocks-localization-configuration/SKILL.md +0 -149
- package/dist/skills/blocks-localization-implementation/SKILL.md +0 -63
- package/dist/skills/blocks-mail/SKILL.md +0 -95
- package/dist/skills/blocks-notification/SKILL.md +0 -69
- package/dist/skills/blocks-notifier/SKILL.md +0 -107
- package/dist/skills/blocks-onboarding/SKILL.md +0 -77
- package/dist/skills/blocks-release-deployment/SKILL.md +0 -81
- package/dist/skills/blocks-secrets/SKILL.md +0 -81
- package/dist/skills/blocks-storage-configuration/SKILL.md +0 -93
- package/dist/skills/lint.mjs +0 -168
|
@@ -1,105 +0,0 @@
|
|
|
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,80 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: blocks-iam-sso-oidc-implementation
|
|
3
|
-
description: "Extend or debug the hosted SSO/OIDC login flow `blocks new web` scaffolds into every Blocks app: redirectToProvider → `/login/callback` → session, via the single `blocksClient`. Covers `AuthProvider` status/claims, `RequireAuth`/`RedirectIfAuthenticated` guards, and token refresh. Use for a login button, the OIDC callback, protected routes, a disabled login button, redirect loops, or a session that doesn't stick — on an app `blocks new web` already created. Requires a registered OIDC client (`blocks-iam-sso-oidc-configuration`) and HTTPS on the real domain for testing (`blocks-frontend-local-https`)."
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Blocks IAM — SSO / OIDC Implementation (scaffolded frontend)
|
|
7
|
-
|
|
8
|
-
`blocks new web <name>` already generates a complete, working hosted-login flow. Don't reinvent it — read what's there, extend it, or fix it. Every Blocks call in this flow goes through the single `blocksClient` instance (`src/lib/blocks/client.ts`, `@seliseblocks/client`); there is no raw `fetch`/`curl` anywhere in this stack.
|
|
9
|
-
|
|
10
|
-
## The files, and what each one actually does
|
|
11
|
-
|
|
12
|
-
| File | Role |
|
|
13
|
-
|---|---|
|
|
14
|
-
| `src/lib/blocks/config.ts` | Reads `VITE_BLOCKS_*` env vars; `isLoginConfigured()` = `apiUrl && oidcUrl && oidcClientId` all present |
|
|
15
|
-
| `src/lib/blocks/client.ts` | The one `blocksClient = createBlocksClient({...})` instance, with `oidc: { clientId, scope, url: oidcUrl }` |
|
|
16
|
-
| `src/lib/blocks/auth.ts` | `startLogin`, `completeLogin`, `fetchSessionClaims`, `logout`, `getValidAccessToken` — the session/token logic |
|
|
17
|
-
| `src/lib/blocks/jwt.ts` | `decodeJwtPayload`/`isJwtExpired` — only relevant if a tenant's OIDC config returns bearer tokens in the body |
|
|
18
|
-
| `src/app/providers/AuthProvider.tsx` | React context: `status`/`claims`/`login`/`logout`/`refresh`, polling + visibility-driven refresh |
|
|
19
|
-
| `src/app/router/guards.tsx` | `RequireAuth`, `RedirectIfAuthenticated` |
|
|
20
|
-
| `src/app/router/routes.tsx` | Wires `/login`, `/login/callback`, and the protected route table (`/`, `/assets`, `/profile`, `/error`) |
|
|
21
|
-
| `src/features/auth/LoginPage.tsx` | The login button |
|
|
22
|
-
| `src/features/auth/CallbackPage.tsx` | The `/login/callback` handler |
|
|
23
|
-
|
|
24
|
-
## The flow, traced through the generated code
|
|
25
|
-
|
|
26
|
-
1. **Login button.** `LoginPage`'s button calls `useAuth().login(returnTo)`, which is `AuthProvider`'s `login` calling `startLogin(returnTo)` in `lib/blocks/auth.ts`. `startLogin` throws a clear error if `oidcClientId` isn't set (`"Login is not configured. Set VITE_BLOCKS_OIDC_CLIENT_ID in .env."`), stashes `returnTo` (default `"/"`) in `sessionStorage`, then calls `blocksClient.auth.idp.redirectToProvider()` with no arguments — it relies entirely on the client's configured `oidc` defaults.
|
|
27
|
-
- The button itself is `disabled={!configured || pending}` — if `isLoginConfigured()` is false, `LoginPage` renders a warning `Alert` with the exact callback URL (`{origin}/login/callback`) to register, instead of letting the click fail. **"Login button does nothing" is almost always an empty `VITE_BLOCKS_OIDC_CLIENT_ID`.**
|
|
28
|
-
2. **`redirectToProvider()`** (SDK, `auth-client.ts`) calls `auth.idp.initiate()`, then `window.location.assign(response.redirect_uri)`. `initiate` itself is also directly callable (e.g. to get the URL without immediately navigating, such as opening it in a new tab) but the scaffold never calls it directly; only `redirectToProvider` is wired to the button.
|
|
29
|
-
3. The user authenticates on Blocks-hosted IAM.
|
|
30
|
-
4. IAM redirects back to `<origin>/login/callback?code=...&state=...`. That path is the SDK's *default* `redirectUri` — the scaffold's `client.ts` never passes an explicit `redirectUri`, so `createBlocksClient` derives `${window.location.origin}/login/callback` at runtime (see `browserRedirectUri()` in the SDK's `client.ts`). This is exactly the route `routes.tsx` handles, so it lines up with zero config — **but** it means the OIDC client's registered `redirect_uris` must include `/login/callback` under **every origin** this app runs on (dev HTTPS origin and prod origin both — see the scaffold's own README and `blocks-iam-sso-oidc-configuration`).
|
|
31
|
-
5. `routes.tsx` matches `path === "/login/callback"` and renders `CallbackPage` directly — **not** wrapped in `RequireAuth` or `RedirectIfAuthenticated`, since the user is by definition not yet authenticated when they land here.
|
|
32
|
-
6. `CallbackPage`'s one-shot effect (guarded with a `useRef` so React 18 Strict Mode's double-invoke doesn't run it twice) calls `completeLogin(window.location.href)`. `completeLogin` reads and clears the stashed `returnTo`, then calls `blocksClient.auth.idp.callback(callbackUrl)`, passing the full URL so the SDK parses `code`/`state`/`error` itself.
|
|
33
|
-
- On the default cookie flow, IAM sets the session as a **Secure, httpOnly cookie** via `Set-Cookie` on this response and returns no token in the body — `completeLogin` only caches a bearer token if the response body actually contains one (a non-default, explicit-token OIDC config). The SDK never stores tokens itself either way; every call sets `credentials: "include"` so the cookie rides along automatically once IAM has set it.
|
|
34
|
-
- If `data.error` is present, `completeLogin` returns `{ ok: false, message }` and `CallbackPage` shows an inline error `Alert` plus a button back to `/login` — it never silently strands the user on a blank screen.
|
|
35
|
-
7. On success, `CallbackPage` calls `refresh()` (from `AuthProvider`) and then `onNavigate(result.returnTo)`. `refresh()` calls `fetchSessionClaims()` → `blocksClient.auth.userInfo()` to confirm the cookie actually landed and to populate `claims`/`status` before the app navigates away from the callback screen.
|
|
36
|
-
|
|
37
|
-
## Session state and route guards
|
|
38
|
-
|
|
39
|
-
- **`AuthProvider`** is the single source of truth for `status` (`"loading" | "authenticated" | "unauthenticated"`) and `claims`. It calls `refresh()` on mount, every 5 minutes (`STATUS_POLL_MS`, a backup interval — not the primary signal), and immediately whenever the tab regains visibility (catches sign-out in another tab or session expiry while backgrounded). It never inspects local storage to decide auth state — asking IAM directly (`userInfo()`) is the only source of truth, because the default flow holds no locally readable token by design.
|
|
40
|
-
- **`RequireAuth`** wraps every protected route in `routes.tsx` (`/`, `/assets`, `/profile`, `/error`). While `status !== "authenticated"` it renders `LoadingScreen`; once `status` resolves to `"unauthenticated"` it navigates to `/login?returnTo=<currentPath>` from a `useEffect` (not render-time — reading `window.location` live at render would double-nest the `returnTo` param under Strict Mode's double-invoked effects).
|
|
41
|
-
- **`RedirectIfAuthenticated`** wraps `/login` itself so an already-signed-in user hitting `/login` bounces straight to `/` instead of seeing the login button again.
|
|
42
|
-
- Adding a new protected page: add it to the `protectedRoutes` map in `routes.tsx` — it's automatically wrapped in `RequireAuth` and `AppShell` by the existing router code, nothing else to wire.
|
|
43
|
-
|
|
44
|
-
## The `@seliseblocks/client` methods behind all of this
|
|
45
|
-
|
|
46
|
-
All under `blocksClient.auth`:
|
|
47
|
-
|
|
48
|
-
- **`idp.initiate(request?)`** — starts the flow, returns `{ redirect_uri }`. Uses the client's configured `oidc` defaults (`clientId`, `redirectUri`) unless you pass overrides per call.
|
|
49
|
-
- **`idp.redirectToProvider(request?)`** — calls `initiate` then `window.location.assign(...)`. This is what `startLogin` (and therefore the login button) actually calls; reach for this directly in any new login entry point rather than re-implementing initiate+navigate.
|
|
50
|
-
- **`idp.callback(callbackUrlOrObject)`** — completes the flow. Pass `window.location.href` directly (what `completeLogin` does), or `{ code, state, error?, error_description? }` if you've parsed the URL yourself. Returns IAM's auth response as-is; the SDK never stores tokens — your app decides what, if anything, to keep (the scaffold keeps nothing in the default cookie flow).
|
|
51
|
-
- **`idp.uiConfig()`** — public UI config (e.g. captcha settings). **Not currently called anywhere in the scaffold** — if you're extending `LoginPage` with captcha or tenant-specific login UI, call this before rendering that UI, not before.
|
|
52
|
-
- **`oidc.refreshToken(request?)`** — a separate call from the IdP-controller hosted flow, using a refresh-token grant. `getValidAccessToken()` in `lib/blocks/auth.ts` is already wired as the 401-retry/expiry path: it returns a cached, unexpired token if present, otherwise calls this (de-duplicating concurrent callers via `refreshInFlight`) if a refresh token happens to be cached. In the default cookie-only flow there's usually nothing cached to refresh, so this mostly matters for tenants whose OIDC config explicitly returns tokens in the response body.
|
|
53
|
-
- **`isAuthenticated()`** — returns a plain boolean. The scaffold's own `fetchSessionClaims()` calls the lower-level `userInfo()` instead (same underlying check) because `AuthProvider` needs the claims payload, not just a boolean — reach for `isAuthenticated()` yourself for a one-off check that doesn't need claims, rather than hand-rolling another call.
|
|
54
|
-
|
|
55
|
-
## Config
|
|
56
|
-
|
|
57
|
-
`createBlocksClient` needs an `oidc` block: `clientId` (required), `url` (required — kept for app metadata, not used to build the authorize URL), `redirectUri`/`scope` (optional, default to `${origin}/login/callback` / `openid profile`). The scaffold populates this from `VITE_BLOCKS_OIDC_CLIENT_ID` / `VITE_BLOCKS_OIDC_URL` / `VITE_BLOCKS_OIDC_SCOPE` in `.env`.
|
|
58
|
-
|
|
59
|
-
**This `clientId` is the public OIDC client registered for *this app*** — see the sibling **`blocks-iam-sso-oidc-configuration`** skill for how to resolve or create one (`blocks auth oidc-clients list` / `save`, no portal needed). Don't confuse it with `blocks login` itself, which authenticates the CLI with no setup and needs no registration at all (see **blocks-onboarding**) — the two are unrelated and neither can substitute for the other.
|
|
60
|
-
|
|
61
|
-
**`--client-id` and `--app-domain` are non-interactive-unsafe when omitted.** `blocks new web`'s client-id and domain resolution both fall back to an interactive selection prompt ("Choose an OIDC client... or create/skip" / "Multiple domains found... choose one") when the flag is missing and there's more than one candidate (or, for the client id, always — even zero candidates offers "Create"/"Skip"). There is no stdin in a non-interactive/agent-driven run, so this hangs waiting for a selection instead of quietly scaffolding with a blank/default value. A blank `oidcClientId` only happens if a human sitting at the terminal interactively picks "Skip". An agent running `blocks new web` should always resolve and pass both `--client-id` and `--app-domain` explicitly up front — see **`blocks-iam-sso-oidc-configuration`** for resolving/creating an OIDC client via `auth oidc-clients list`/`save`, and `project.applications[].domain` (from the project record) for the app domain — rather than omitting either and hoping for a graceful non-interactive default.
|
|
62
|
-
|
|
63
|
-
## Gotchas
|
|
64
|
-
|
|
65
|
-
- **Disabled login button, no error** → `isLoginConfigured()` is false, almost always because `VITE_BLOCKS_OIDC_CLIENT_ID` is empty in `.env`. Don't assume `blocks new web` was just run without `--client-id` and "left this blank on purpose" — omitting `--client-id` (or `--app-domain`, when a project has multiple domains) drops into an interactive `selectFromList()` prompt with no graceful non-interactive fallback; in an agent-driven run with no stdin, that hangs rather than scaffolding a blank value. A blank client id only results from a human interactively choosing "Skip." Always pass `--client-id` explicitly (see the Config section above).
|
|
66
|
-
- **Login redirects back but the app still shows logged out** → this is an HTTPS/cookie problem, not an app-logic bug — the session cookie is Secure and won't be stored/sent on `http://localhost`. Cross-reference **`blocks-frontend-local-https`** rather than debugging `AuthProvider`.
|
|
67
|
-
- **Redirect URI mismatch** → the SDK derives `redirectUri` from `window.location.origin` at runtime; if the app runs under more than one origin (dev HTTPS host, prod domain), the registered OIDC client's `redirect_uris` must list `/login/callback` under **each** of them, or IAM rejects the authorize request for the ones missing.
|
|
68
|
-
- **Activation is a separate concern.** Already-activated users go straight through this flow. Only users invited/created inactive via the portal or API need a one-time `/activate` step first — out of scope here, see **`blocks-iam-account`**.
|
|
69
|
-
- **Don't add a `RequireAuth`/`RedirectIfAuthenticated` guard around `/login/callback`** — it must stay reachable while the user is still unauthenticated, by design.
|
|
70
|
-
- **Don't hand-roll a "check if logged in" fetch** — call `blocksClient.auth.isAuthenticated()` or reuse `AuthProvider`'s `status`/`refresh()`, never infer auth state from `sessionStorage`/`localStorage` (the default flow keeps no readable token there at all).
|
|
71
|
-
- **Custom app domain, session never sticks (cookie calls silently fail)** → on a custom (non-`*.seliseblocks.com`) app domain, the hosted-login session cookie is only stored/sent if `VITE_BLOCKS_API_URL` shares the app's registrable domain. The default `https://api.seliseblocks.com` does not share a registrable domain with e.g. `abc.slsblx.com`, so the browser never stores the cross-site cookie and cookie-based calls (`userInfo()`/`/iam/me`, `logout`, the OIDC callback flow this skill documents) silently fail. For a custom domain, `VITE_BLOCKS_API_URL` must be `https://blocksapi.<registrable-domain>` (e.g. `abc.slsblx.com` → `https://blocksapi.slsblx.com`), not the default.
|
|
72
|
-
|
|
73
|
-
## Example trigger prompts
|
|
74
|
-
|
|
75
|
-
- "Add a login button and handle the OIDC callback"
|
|
76
|
-
- "Why is my login button disabled?"
|
|
77
|
-
- "Add a new protected page that requires the user to be signed in"
|
|
78
|
-
- "The user gets redirected back from IAM but the app still shows them as logged out"
|
|
79
|
-
- "Wire up token refresh for when the session expires"
|
|
80
|
-
- "How does this scaffolded app know if someone is logged in?"
|
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: blocks-iam-users
|
|
3
|
-
description: "Manage OTHER users' IAM records via `blocksClient.iam.users.*` (never raw fetch/curl), or the equivalent project-scoped `blocks iam users *` / `blocks iam email available` CLI. Covers reads (`get`, `list`, `emailAvailable`, `exists`) and admin mutations (`create`, `update`, `activate`, `deactivate`, `updateAccess`, `revokeAccess`) — CLI mutations require `--dry-run`/`--yes`. Use to invite, edit, deactivate/reactivate, list/search users, or grant/revoke roles/org access. Not for the current user's own profile (blocks-iam-account) or role/permission definitions (blocks-iam-access-control)."
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Blocks IAM — Managing Other Users
|
|
7
|
-
|
|
8
|
-
This skill is about an **admin managing other people's IAM accounts** from inside a Blocks app — inviting them, editing their profile, changing their access, deactivating them. It is not about the signed-in user managing their own account (that's the **blocks-iam-account** skill) and not about defining the roles/permissions being assigned (that's **blocks-iam-access-control**).
|
|
9
|
-
|
|
10
|
-
Everything here goes through the SDK: `blocksClient.iam.users.*` on the app's single `@seliseblocks/client` instance (created once, typically at `src/lib/blocks/client.ts` by `blocks new web`). **Never raw `fetch`/`curl` against `api.seliseblocks.com`.**
|
|
11
|
-
|
|
12
|
-
```ts
|
|
13
|
-
import { blocksClient } from "../../lib/blocks/client";
|
|
14
|
-
|
|
15
|
-
const { data } = await blocksClient.iam.users.get(userId);
|
|
16
|
-
```
|
|
17
|
-
|
|
18
|
-
## Two surfaces, same operations: SDK (in-app) and CLI (`blocks iam users *`)
|
|
19
|
-
|
|
20
|
-
There are two legitimate ways to drive full user administration (create, update, deactivate, activate, access grant/revoke) — both are covered by this skill:
|
|
21
|
-
|
|
22
|
-
- **SDK — `blocksClient.iam.users.*`** — build the capability **as a feature inside a signed-in admin's own app**: the admin is looking at a screen, clicking "Deactivate" on a specific user row, and their own IAM permissions gate whether the call succeeds.
|
|
23
|
-
- **CLI — `blocks iam users *` / `blocks iam email available`** — the same operations, invoked directly from a terminal or an agent's shell tool. These are fully wired, project-scoped commands (see "CLI surface" below), not a read-only stub — `iam me` is a separate, account-scoped command for the CLI operator's own identity and is not the only IAM command the CLI has.
|
|
24
|
-
|
|
25
|
-
What is **not** legitimate on either surface: an agent deciding on its own, without the human explicitly directing that specific action in the moment, to call `create`/`update`/`deactivate`/`activate`/`updateAccess`/`revokeAccess` (SDK) or `users create`/`update`/`activate`/`deactivate`/`access grant`/`access revoke` (CLI). State the exact change in plain language and get the user's explicit go-ahead first, every time, even if they asked for something adjacent a moment ago. The CLI enforces this mechanically — every mutating command requires `--dry-run` (preview only, no call) or `--yes`/an interactive "yes" before it executes — but that built-in gate doesn't replace stating the change and getting a real go-ahead when an agent is the one typing the command.
|
|
26
|
-
|
|
27
|
-
## Safe surface — reads and checks, no confirmation needed
|
|
28
|
-
|
|
29
|
-
These don't change anything, so there's no caveat to apply:
|
|
30
|
-
|
|
31
|
-
| Method | What it does |
|
|
32
|
-
|---|---|
|
|
33
|
-
| `iam.users.get(id, { organizationId? })` | One user record, optionally scoped to an org. |
|
|
34
|
-
| `iam.users.list(request)` | Paged/filtered user query. **This is a POST-read contract** — `list` sends `{ pageNo, pageSize, filter, search, ... }` as a POST body, it is not a GET. |
|
|
35
|
-
| `iam.users.emailAvailable(query)` | Public duplicate-email check for invite/signup forms. No auth needed. |
|
|
36
|
-
| `iam.users.exists(email)` | Existence check by email. |
|
|
37
|
-
|
|
38
|
-
```ts
|
|
39
|
-
const page = await blocksClient.iam.users.list({ pageNo: 1, pageSize: 20, search: "jane" });
|
|
40
|
-
const check = await blocksClient.iam.users.emailAvailable({ email: "new.hire@example.com" });
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
## Sensitive surface — confirm the exact change before calling
|
|
44
|
-
|
|
45
|
-
Every method below mutates a real account. Before calling any of them, restate to the user in plain language exactly what will change (which user, which field, which effect) and wait for an explicit yes — do not infer consent from an earlier, more general request.
|
|
46
|
-
|
|
47
|
-
| Method | What it does |
|
|
48
|
-
|---|---|
|
|
49
|
-
| `iam.users.create(request)` | Invites/provisions a user in the active tenant/organization. |
|
|
50
|
-
| `iam.users.update(id, request)` | Edits an IAM profile's fields. |
|
|
51
|
-
| `iam.users.deactivate(request)` | Removes access without deleting the record. |
|
|
52
|
-
| `iam.users.activate(request)` | Restores access for a previously deactivated account. |
|
|
53
|
-
| `iam.users.updateAccess(request)` | Grants or changes roles/permissions/org access for a user. |
|
|
54
|
-
| `iam.users.revokeAccess(request)` | Removes roles/permissions/org access from a user. |
|
|
55
|
-
|
|
56
|
-
Example — deactivating a user:
|
|
57
|
-
|
|
58
|
-
> Agent: "This will deactivate **jane.doe@example.com** (user id `usr_8a2f`) — she'll immediately lose access but her record and history stay intact. Confirm?"
|
|
59
|
-
> User: "Yes, deactivate her."
|
|
60
|
-
> *(only then)* `await blocksClient.iam.users.deactivate({ userId: "usr_8a2f" });`
|
|
61
|
-
|
|
62
|
-
Never chain a mutation straight off a read (e.g. don't look a user up and deactivate them in the same breath just because the user asked to "find inactive-looking accounts") — surface what you found, then get a decision on each mutation separately.
|
|
63
|
-
|
|
64
|
-
```ts
|
|
65
|
-
// After the user explicitly confirms creating this exact invite:
|
|
66
|
-
await blocksClient.iam.users.create({
|
|
67
|
-
email: "new.hire@example.com",
|
|
68
|
-
firstName: "New",
|
|
69
|
-
lastName: "Hire",
|
|
70
|
-
roles: ["member"]
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
// After the user explicitly confirms this exact access change:
|
|
74
|
-
await blocksClient.iam.users.updateAccess({ userId: "usr_8a2f", roles: ["editor"] });
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
## CLI surface — `blocks iam users *`, `blocks iam email available`
|
|
78
|
-
|
|
79
|
-
These are real, fully-wired commands — not a stub and not limited to `iam me`. `iam me` is a separate, account-scoped command (current CLI operator's own identity via the account token); every command below is **project-scoped**: it requires a project already selected (`blocks use <project-tenant-id>`) and calls IAM with an impersonated project token, same as the rest of the project-scoped CLI surface.
|
|
80
|
-
|
|
81
|
-
Reads — no confirmation needed:
|
|
82
|
-
|
|
83
|
-
| Command | What it does |
|
|
84
|
-
|---|---|
|
|
85
|
-
| `blocks iam users list [--page 1] [--page-size 20] [--email <e>] [--name <n>] [--organization-id <id>] [--sort-by <field>] [--sort-desc] [--filter '<json>'] [--json]` | Paged/filtered user query. `--filter` merges a raw JSON object over the convenience flags. |
|
|
86
|
-
| `blocks iam users get <id> [--organization-id <id>] [--json]` | One user record, optionally scoped to an org. |
|
|
87
|
-
| `blocks iam users exists <email> [--json]` | Existence check by email. |
|
|
88
|
-
| `blocks iam email available <email> [--json]` | Duplicate-email check. |
|
|
89
|
-
|
|
90
|
-
Mutations — every one supports `--dry-run` (print the request body and exit, no call) and requires either `--yes` or a typed `yes` at an interactive prompt before it executes:
|
|
91
|
-
|
|
92
|
-
| Command | What it does |
|
|
93
|
-
|---|---|
|
|
94
|
-
| `blocks iam users create --email <e>\|--user-name <n> [--first-name] [--last-name] [--password] [--phone-number] [--organization-id] [--roles a,b] [--permissions a,b] [--body '<json>'\|--file <path>] [--dry-run] [--yes] [--json]` | Invites/provisions a user. |
|
|
95
|
-
| `blocks iam users update <id> [--first-name] [--last-name] [--phone-number] [--organization-id] [--roles a,b] [--permissions a,b] [--body '<json>'\|--file <path>] [--dry-run] [--yes] [--json]` | Edits an IAM profile's fields. |
|
|
96
|
-
| `blocks iam users activate <userId> [--reason <text>] [--dry-run] [--yes] [--json]` | Restores access for a previously deactivated account. |
|
|
97
|
-
| `blocks iam users deactivate <userId> [--dry-run] [--yes] [--json]` | Removes access without deleting the record. |
|
|
98
|
-
| `blocks iam users access grant <userId> [--roles a,b] [--permissions a,b] [--organization-id] [--dry-run] [--yes] [--json]` | Grants roles/permissions/org access (requires at least one of `--roles`/`--permissions`). |
|
|
99
|
-
| `blocks iam users access revoke <userId> [--organization-id] [--dry-run] [--yes] [--json]` | Revokes org access for a user. |
|
|
100
|
-
|
|
101
|
-
Command segments joined by a space also accept a colon (`iam:users:access:grant` etc.) — both forms resolve to the same handler; `blocks iam users --help`-style docs in the CLI's own `--help` output use the space form shown above.
|
|
102
|
-
|
|
103
|
-
Example — deactivating a user from the CLI, dry-run first:
|
|
104
|
-
|
|
105
|
-
```sh
|
|
106
|
-
blocks iam users deactivate usr_8a2f --dry-run # preview the request body, no call made
|
|
107
|
-
blocks iam users deactivate usr_8a2f --yes # after the user explicitly confirms
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
Apply the same confirm-before-mutating discipline here as with the SDK: state which user and which effect, wait for an explicit yes, don't chain a mutating command straight off a `list`/`get` just because the user asked to "find" something.
|
|
111
|
-
|
|
112
|
-
## Gotchas
|
|
113
|
-
|
|
114
|
-
- **`list` is a POST**, not a GET — don't assume query-string filtering.
|
|
115
|
-
- **Roles are referenced by slug**, as defined in blocks-iam-access-control — not by their internal item ids.
|
|
116
|
-
- **`organizationId`** matters in multi-org projects — pass it to `get` when you need a user's record in a specific org context.
|
|
117
|
-
- **Every request/response type in the SDK is a loosely-typed `Record<string, unknown>`** (`BlocksUser`, `BlocksBaseResponse`, etc. only guarantee a few common fields) — treat fields defensively and confirm shape against a live response for the project rather than assuming a fixed schema.
|
|
118
|
-
- **The CLI is project-scoped, not account-scoped** — `blocks iam users *`/`blocks iam email available` need a selected project (`blocks use <project-tenant-id>`) and use an impersonated project token; `iam me` is the one exception that runs on the account token instead.
|
|
119
|
-
- **Don't duplicate blocks-iam-account** — if the ask is "let me update my own profile" or "let me reset my password," that's the current user acting on themselves, not this skill.
|
|
120
|
-
|
|
121
|
-
## Example triggers
|
|
122
|
-
|
|
123
|
-
- "Invite a user and set their roles"
|
|
124
|
-
- "Deactivate this user's account"
|
|
125
|
-
- "List all users in the org, filtered by status"
|
|
126
|
-
- "Check if this email is already registered before I show the invite form"
|
|
127
|
-
- "Grant this user the editor role"
|
|
128
|
-
- "Revoke this user's access to the finance org"
|
|
129
|
-
- "Update this user's phone number"
|
|
130
|
-
- "Reactivate this account"
|
|
131
|
-
- "From the terminal, deactivate user usr_8a2f in the current project" → use `blocks iam users deactivate usr_8a2f`, `--dry-run` first, then `--yes` after explicit confirmation
|
|
@@ -1,149 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: blocks-localization-configuration
|
|
3
|
-
description: "Configure app translations (i18n) for a SELISE Blocks project through the `blocks` CLI — never raw fetch/curl. Covers authoring local i18n JSON dictionaries, validate/push/pull with the Localization service, managing languages and modules directly, glossary terms, AI translation suggestions, and the composed translate-and-export flow. Use for 'add translations for my login screen', 'push/pull localization changes', 'create a module', 'add a new language'."
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Blocks Localization — Configuration
|
|
7
|
-
|
|
8
|
-
Translations (i18n) for a Blocks project's static UI text — labels, titles, button copy — are authored locally as JSON and synced to the Localization service entirely through the `blocks` CLI. There is no supported reason to hand-roll raw `fetch`/`curl` calls anymore, and there's no SDK-based authoring path either — `@seliseblocks/client`'s localization surface (`languages()`, `modules()`, `languagesForCurrentTenant()`, `translations()`, `cloudTranslations()`, `keysByNames()`) is entirely **read-only**, meant for apps to *consume* translations at runtime, not to author them. Authoring is CLI-only.
|
|
9
|
-
|
|
10
|
-
**Prerequisite:** `blocks init` has been run and a project is selected (`blocks use <tenantId>`). Note `blocks init` does **not** create a `blocks/localization/` folder — it only scaffolds `blocks/data/schemas/`, `blocks/data/rules.json`, and `.env.example`. The `blocks/localization/` directory and its dictionary files come into existence lazily, the first time `blocks localization pull` writes one out (or the first time you author one by hand). If either the project selection is missing, or auth state is unknown, run the blocks-onboarding skill first — it covers `auth status` probing, login, and project selection in detail; this skill assumes that's already done.
|
|
11
|
-
|
|
12
|
-
## The three commands
|
|
13
|
-
|
|
14
|
-
| Command | What it does |
|
|
15
|
-
|---|---|
|
|
16
|
-
| `blocks localization validate --module <name> --language <culture> [--file <path>] [--json]` | Validates a local i18n JSON dictionary. **Local-only, no API call.** |
|
|
17
|
-
| `blocks localization push --module <name> --language <culture> [--file <path>] [--route <route>] [--context <text>] [--dry-run] [--yes] [--json]` | Creates/updates keys from the local dictionary. If the module doesn't exist yet, creates it first — **this is the only way this tooling creates a module.** Mutating. |
|
|
18
|
-
| `blocks localization pull --module <name> --language <culture> [--out <path>] [--json]` | Downloads the **published** cloud dictionary into a local JSON file. Read-only, overwrites the local file. |
|
|
19
|
-
|
|
20
|
-
`--module` is the feature-area bundle name (`common`, `login`, `dashboard`, …). `--language` is a culture code (`en`, `de-DE`, `bn-BD`, …) — see the culture-matching gotcha below before picking one.
|
|
21
|
-
|
|
22
|
-
## File convention
|
|
23
|
-
|
|
24
|
-
Local dictionaries default to:
|
|
25
|
-
|
|
26
|
-
```text
|
|
27
|
-
blocks/localization/<module>.<language>.json
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
for example `blocks/localization/login.de-DE.json`. Pass `--file`/`--out` to override the path. Content is a flat or nested JSON object of string values — nested objects are flattened with `.` before validation/push, so either of these is fine and produces the same keys:
|
|
31
|
-
|
|
32
|
-
```json
|
|
33
|
-
{ "login.title": "Anmelden", "login.submit": "Absenden" }
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
```json
|
|
37
|
-
{ "login": { "title": "Anmelden", "submit": "Absenden" } }
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
Key names must match `^[A-Za-z0-9][A-Za-z0-9._:-]*$` (letters, numbers, dot, dash, underscore, colon — no spaces) after flattening, and every value must be a non-empty string. `localization validate` enforces exactly this, locally, before anything touches the network.
|
|
41
|
-
|
|
42
|
-
## Workflow: add or update translations
|
|
43
|
-
|
|
44
|
-
1. **Generate or edit the local dictionary** at `blocks/localization/<module>.<language>.json` — write the JSON yourself (nested or flat), covering every key the screen/feature needs.
|
|
45
|
-
2. **Validate locally, no API call:**
|
|
46
|
-
```bash
|
|
47
|
-
blocks localization validate --module login --language de-DE --json
|
|
48
|
-
```
|
|
49
|
-
Fix every flagged key/value before moving on.
|
|
50
|
-
3. **Dry-run the push** to see exactly what would happen (module create-or-reuse, key count, target project):
|
|
51
|
-
```bash
|
|
52
|
-
blocks localization push --module login --language de-DE --dry-run --json
|
|
53
|
-
```
|
|
54
|
-
4. **Get user approval, then push for real:**
|
|
55
|
-
```bash
|
|
56
|
-
blocks localization push --module login --language de-DE --yes --json
|
|
57
|
-
```
|
|
58
|
-
This is mutating. Never skip straight to `--yes`. Every key in the file is saved as immediately published, so a successful push is live for reads right away — there is no separate "generate/publish" step in this CLI.
|
|
59
|
-
|
|
60
|
-
Optional flags on `push`: `--route <route>` tags every key in this push with one route (e.g. the screen path the strings belong to); `--context <text>` attaches one context/hint string to every key in the push — both apply to the whole file, not per-key.
|
|
61
|
-
|
|
62
|
-
## Workflow: multiple languages for the same screen
|
|
63
|
-
|
|
64
|
-
Each `push` call carries exactly one `--language` (one culture stamped onto every key in that file). Translating one module into several languages means **one dictionary file and one push per language**, all against the same `--module`:
|
|
65
|
-
|
|
66
|
-
```bash
|
|
67
|
-
blocks localization validate --module login --language de-DE --json
|
|
68
|
-
blocks localization validate --module login --language bn-BD --json
|
|
69
|
-
blocks localization push --module login --language de-DE --dry-run --json
|
|
70
|
-
blocks localization push --module login --language bn-BD --dry-run --json
|
|
71
|
-
# after approval:
|
|
72
|
-
blocks localization push --module login --language de-DE --yes --json
|
|
73
|
-
blocks localization push --module login --language bn-BD --yes --json
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
The module (`login`) is only created on the *first* push that needs it; the second push reuses the module the first one created.
|
|
77
|
-
|
|
78
|
-
## Refreshing local files from the cloud
|
|
79
|
-
|
|
80
|
-
```bash
|
|
81
|
-
blocks localization pull --module login --language de-DE --out blocks/localization/login.de-DE.json --json
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
Use this to pull down what's actually published before editing further — same reasoning as pulling data schemas before editing them: don't blindly overwrite translations someone else edited in the portal or in a prior session.
|
|
85
|
-
|
|
86
|
-
## Managing languages directly
|
|
87
|
-
|
|
88
|
-
Pushing translations into an existing language and *configuring the tenant's set of languages* are different operations — the latter has its own standalone commands, independent of `push`/`pull`:
|
|
89
|
-
|
|
90
|
-
| Command | What it does |
|
|
91
|
-
|---|---|
|
|
92
|
-
| `blocks localization language save --language-name <n> --language-code <c> [--is-default] [--item-id <id>] [--dry-run] [--yes] [--json]` | Creates or updates a language. Omit `--item-id` to create a new one. Mutating, full dry-run/confirm gate. |
|
|
93
|
-
| `blocks localization language delete <languageName> [--dry-run] [--yes] [--json]` | Deletes a language. Mutating. |
|
|
94
|
-
| `blocks localization language set-default <languageName> [--dry-run] [--yes] [--json]` | Marks a language as the tenant default. Mutating. |
|
|
95
|
-
| `blocks localization language list [--json]` | Lists all languages. Read-only. |
|
|
96
|
-
| `blocks localization language list-for-tenant [--json]` | Lists languages configured for the current tenant. Read-only. |
|
|
97
|
-
|
|
98
|
-
So "add German as a supported language for the tenant" is a real, supported request: `blocks localization language save --language-name German --language-code de-DE --dry-run`, get approval, then re-run with `--yes`. This is distinct from `localization push --language de-DE`, which stamps translations onto keys and doesn't touch the tenant's language configuration at all.
|
|
99
|
-
|
|
100
|
-
## Managing modules directly
|
|
101
|
-
|
|
102
|
-
A module is still created implicitly by the first `localization push` into it, but it can also be created or updated on its own, with no keys, via a standalone command:
|
|
103
|
-
|
|
104
|
-
| Command | What it does |
|
|
105
|
-
|---|---|
|
|
106
|
-
| `blocks localization module save --module-name <n> [--item-id <id>] [--dry-run] [--yes] [--json]` | Creates or updates a module. Omit `--item-id` to create a new one. Mutating. |
|
|
107
|
-
| `blocks localization module list [--json]` | Lists all modules. Read-only. |
|
|
108
|
-
| `blocks localization module list-for-tenant [--json]` | Lists modules configured for the current tenant. Read-only. |
|
|
109
|
-
|
|
110
|
-
So "create a `billing` module with no keys yet" is directly supported: `blocks localization module save --module-name billing --dry-run` → approve → `--yes`. `localization push`'s implicit module creation is just a convenience on top of the same underlying call, not the only path to it.
|
|
111
|
-
|
|
112
|
-
## Other localization commands
|
|
113
|
-
|
|
114
|
-
A few more commands round out the surface beyond push/pull/validate/language/module — useful, but secondary to the core authoring workflow above:
|
|
115
|
-
|
|
116
|
-
| Command | What it does |
|
|
117
|
-
|---|---|
|
|
118
|
-
| `blocks localization key translate-and-export --module-id <id> [--wait] [--dry-run] [--yes] [--json]` | Composed flow: `translate-all` (machine-translates every untranslated key in the module) → if `--wait`, polls until the operation settles → `generate-uilm-file` → `uilm-export`. Without `--wait` the three steps just fire back-to-back. Mutating. |
|
|
119
|
-
| `blocks localization glossary save --name <n> [--item-id <id>] [--language <c>] [--type <t>] [--context <text>] [--additional-note <text>] [--is-global] [--module-ids a,b] [--dry-run] [--yes] [--json]` | Creates or updates a glossary term. Mutating. |
|
|
120
|
-
| `blocks localization glossary list [--search <text>] [--module-id <id>] [--is-global] [--page-number <n>] [--page-size <n>] [--json]` | Lists glossary terms. Read-only. |
|
|
121
|
-
| `blocks localization glossary get <itemId> [--json]` | Fetches one glossary term. Read-only. |
|
|
122
|
-
| `blocks localization glossary suggested <itemId> [--max-results <n>] [--json]` | Suggests glossary terms relevant to an item. Read-only. |
|
|
123
|
-
| `blocks localization glossary delete <itemId> [--dry-run] [--yes] [--json]` | Deletes a glossary term. Mutating. |
|
|
124
|
-
| `blocks localization assistant translation-suggestion --source-text <text> [--current-language <c>] [--destination-language <name>] [--destination-language-code <c>] [--module-id <id>] [--glossary-ids a,b] [--element-type <t>] [--element-application-context <text>] [--element-detail-context <text>] [--max-character-length <n>] [--temperature <n>] [--json]` | Gets an AI translation suggestion for a single string. Read-only (no confirm gate). |
|
|
125
|
-
| `blocks localization config get-webhook [--json]` | Gets the tenant's localization webhook config. Read-only. |
|
|
126
|
-
| `blocks localization config save-webhook --url <url> --content-type <type> --secret <s> --header-key <k> [--is-disabled] [--item-id <id>] [--dry-run] [--yes] [--json]` | Saves the tenant's localization webhook config. Mutating; the secret is redacted in `--dry-run` output. |
|
|
127
|
-
|
|
128
|
-
## Gotchas
|
|
129
|
-
|
|
130
|
-
- **`--dry-run` before `--yes`** on `localization push` — always. Same pattern as every other mutating `blocks` command.
|
|
131
|
-
- **`--language` on `push` is not validated against configured cultures.** `localization push` stamps whatever string you pass as `--language` directly into each key's `culture` field — it does not check that culture against the tenant's actual configured languages, and doesn't call `language list`/`list-for-tenant` to look. Get the culture code wrong (`de` instead of `de-DE`, or a culture the tenant never configured via `language save`) and the key saves without error but may never surface at runtime, because runtime lookups match by the tenant's real configured `languageCode`. Confirm the exact culture code with the user (or run `localization language list-for-tenant`) before pushing, especially for less common languages like Bengali (`bn-BD` vs `bn`).
|
|
132
|
-
- **Module auto-create is silent and permanent.** The first push against a new `--module` name creates it with no separate confirmation prompt beyond the push's own `--dry-run`/`--yes` gate — `--dry-run` output will tell you a module lookup happened, but won't distinguish "will create" from "already exists" as clearly as it could, so read the dry-run JSON's module info carefully, or ask the user to confirm the module name is intentional (typos become new, mostly-empty modules). Prefer `localization module save` first if the user wants the module created deliberately, without an accompanying key push.
|
|
133
|
-
- **`localization validate` is local-only** — it confirms the JSON is well-formed and keys/values pass the naming rules; it does not confirm the push will succeed against the server (module resolution, auth, project selection). Still run `--dry-run` on the actual push.
|
|
134
|
-
- **One culture per file/push.** Don't try to cram multiple languages into one dictionary file — the format is flat key → string value, not key → {culture: value}. Multiple languages means multiple files and multiple push invocations (see above).
|
|
135
|
-
- **No standalone "generate" or "publish" step for `push`.** `shouldPublish: true` is baked into every key `localization push` sends — once the push succeeds, the translations are live. That's separate from `key generate-uilm-file`/`key uilm-export` (and the composed `key translate-and-export`), which build downloadable runtime language files rather than affect `push`'s own publish behavior.
|
|
136
|
-
- **`language delete` and `set-default` are permanent, mutating calls** — always dry-run first, and confirm with the user before deleting a language or changing the tenant default, since either can affect what's visible at runtime for existing translations.
|
|
137
|
-
|
|
138
|
-
## Example trigger prompts
|
|
139
|
-
|
|
140
|
-
- "Add German translations for my login screen." → push `login.de-DE.json` after validate + dry-run + approval.
|
|
141
|
-
- "Add German and Bengali translations for my login screen." → two dictionary files, two validate/push pairs (`de-DE`, `bn-BD`), same module.
|
|
142
|
-
- "Set up a `common` module for shared strings like Save/Cancel/Delete." → write `common.<language>.json` with those keys, validate, push (this is what creates the `common` module) — or use `localization module save --module-name common` directly if no keys exist yet.
|
|
143
|
-
- "Pull the latest translations for the dashboard module before I edit them." → `localization pull --module dashboard --language en`.
|
|
144
|
-
- "Validate my localization file before pushing." → `localization validate` only, no network call.
|
|
145
|
-
- "Can we add Bengali as a new supported language for the tenant?" → `localization language save --language-name Bengali --language-code bn-BD --dry-run`, confirm, then `--yes`.
|
|
146
|
-
- "Create a new translation module called `billing` with no keys yet." → `localization module save --module-name billing --dry-run` → confirm → `--yes`.
|
|
147
|
-
- "Machine-translate the whole `login` module and give me the export." → `localization key translate-and-export --module-id <id> --wait --dry-run` → confirm → `--yes`.
|
|
148
|
-
- "Suggest a translation for this button label." → `localization assistant translation-suggestion --source-text "Save changes" --destination-language-code de-DE`.
|
|
149
|
-
- "What's our webhook config for localization events?" → `localization config get-webhook`.
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: blocks-localization-implementation
|
|
3
|
-
description: "Consume SELISE Blocks localization at runtime in a scaffolded frontend, entirely through the `@seliseblocks/client` SDK's `localization` namespace — never raw fetch/curl. Use for making a Blocks web app multilingual on the client: language/module discovery, loading dictionaries, the built-in `t()` lookup, and a language switcher that reloads and re-renders. Frontend consumption only — authoring/pushing translation content is the sibling skill blocks-localization-configuration."
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Blocks Localization — Implementation (frontend)
|
|
7
|
-
|
|
8
|
-
Make a scaffolded Blocks web app render its UI in the user's language, using only the `localization` namespace on the SDK client — `createBlocksClient(...).localization`. No fetch, no manual query strings, no hand-rolled caching: the SDK client already does all of that.
|
|
9
|
-
|
|
10
|
-
The translations themselves (keys, modules, per-language values) are authored and pushed with the blocks-localization-configuration skill (uses `blocks localization *`). This skill only covers loading and rendering them in the app.
|
|
11
|
-
|
|
12
|
-
## The client and its cache
|
|
13
|
-
|
|
14
|
-
`localization` is a `BlocksLocalizationClient` instance created once inside `createBlocksClient()`. It holds its dictionary cache as instance state — a `Map` keyed by `` `${language}:${moduleName}` `` plus an `activeLanguage`. That cache is **only as shared as the client instance is**: the canonical scaffold creates one `blocksClient` singleton (`src/lib/blocks/client.ts`, from `createBlocksClient()`) and imports it everywhere, so every `t()` call sees every dictionary loaded anywhere in the app. If you instantiate a second `createBlocksClient()` somewhere, it gets its own empty cache — don't do that; import the one singleton.
|
|
15
|
-
|
|
16
|
-
## Public vs. authenticated methods
|
|
17
|
-
|
|
18
|
-
Every method has a matching pair — one public, one tenant/session-scoped:
|
|
19
|
-
|
|
20
|
-
| Public (no token sent) | Authenticated (sends token when configured) |
|
|
21
|
-
|---|---|
|
|
22
|
-
| `languages()` | `languagesForCurrentTenant()` |
|
|
23
|
-
| `modules()` | `modulesForCurrentTenant()` |
|
|
24
|
-
| `translations(moduleName, language)` | `cloudTranslations(moduleName, language)` |
|
|
25
|
-
| `load(language, modules[])` | `loadCloud(language, modules[])` |
|
|
26
|
-
|
|
27
|
-
- `languages()`/`modules()`/`translations()`/`load()` are explicitly public — all tenant-supported cultures and translation bundles, usable pre-login for a language picker or startup locale selection.
|
|
28
|
-
- `languagesForCurrentTenant()`/`modulesForCurrentTenant()`/`cloudTranslations()`/`loadCloud()` are scoped to whichever tenant the active `x-blocks-key` resolves to (and send the caller's access token if the client is configured with one) — use these for protected, tenant-specific dictionaries behind a signed-in session.
|
|
29
|
-
|
|
30
|
-
`keysByNames({ keyNames, moduleId? })` fetches specific key records (metadata/translations) without downloading a whole module dictionary; useful for a one-off label or an admin screen that inspects individual keys.
|
|
31
|
-
|
|
32
|
-
**Argument order matters**: `translations(moduleName, language)` and `cloudTranslations(moduleName, language)` take module first, language second — easy to transpose.
|
|
33
|
-
|
|
34
|
-
## Startup sequence
|
|
35
|
-
|
|
36
|
-
1. **List languages** on app boot (or in a query hook) — `blocksClient.localization.languagesForCurrentTenant()` (or `languages()` if you want it available pre-login). Use `isDefault`/`languageCode` from the result to build the picker and preselect a default, falling back to a persisted user choice (e.g. `localStorage`).
|
|
37
|
-
2. **Load dictionaries** for the active language — `blocksClient.localization.load(language, modules)` where `modules` is the list of bundles the app needs (e.g. `["common", "dashboard", "assets"]`). `load()` fetches each module's dictionary in parallel via `translations()` and merges them into one object, with later modules in the array overwriting earlier ones on key collision. Use `loadCloud()` instead for protected dictionaries once the user is signed in.
|
|
38
|
-
3. **Render labels** with `blocksClient.localization.t(key, fallback, { language, moduleName })`. It reads from dictionaries already loaded by `translations()`/`load()` (or their cloud equivalents) — it does not fetch anything itself. Missing key → `fallback` → the raw key, in that order.
|
|
39
|
-
4. **Switch language**: on switcher change, call `load()` (or `loadCloud()`) again with the new language and the same module list, then re-render. There's no separate "invalidate" step — loading a language populates its own cache entries; you don't need to clear the old language's entries (they just stop being read once `activeLanguage`/your app state moves on).
|
|
40
|
-
|
|
41
|
-
The canonical scaffold (`blocks new web`) wires exactly this pattern in `src/lib/i18n/LocalizationProvider.tsx`: a React context holds `language` state (seeded from `localStorage`), a `useEffect` on `language` calls `blocksClient.localization.load(language, MODULES)` and stores the merged dictionary in state, and `t(key, fallback)` reads `cloudDictionary[key] ?? defaultDictionary[key] ?? fallback ?? key` — layering the network dictionary over a build-time `defaultDictionary` (from `src/lib/i18n/dictionary.ts`, generated from the same keys as the seed JSON in `blocks/localization/*.json`) as an offline/first-paint safety net, itself falling back to the caller-supplied fallback and finally the key. Mirror this shape rather than inventing your own provider — it's already generated into new projects. Note the scaffold's own `t()` is a plain function on context, not the SDK's `localization.t()` — either is fine; the SDK's built-in `t()` needs no separate context/provider if you're happy reading `blocksClient.localization.t(...)` directly in components.
|
|
42
|
-
|
|
43
|
-
## `t()` lookup details worth knowing
|
|
44
|
-
|
|
45
|
-
- If you pass `moduleName`, `t()` does an exact `` `${language}:${moduleName}` `` cache lookup — deterministic.
|
|
46
|
-
- If you omit `moduleName`, `t()` scans all cached dictionaries and returns the first match whose cache key starts with `` `${language}:` `` (or any language if you didn't pass one). Scan order follows Map insertion order, which is the order the underlying HTTP requests *resolved* in (not necessarily the order you listed modules in `load()`) — fine when keys are unique across modules, ambiguous if two modules define the same key. Pass `moduleName` explicitly whenever you know it and key collisions across modules are possible.
|
|
47
|
-
- `t()` never throws and never fetches — call `load()`/`translations()` (or the cloud variants) first, or every lookup falls straight to `fallback`/the key.
|
|
48
|
-
|
|
49
|
-
## Gotchas
|
|
50
|
-
|
|
51
|
-
- **No raw fetch/curl, ever** — every read here goes through `blocksClient.localization.*`. The SDK already sends `x-blocks-key` and, for the authenticated variants, the caller's access token when configured.
|
|
52
|
-
- **One client, one cache** — don't call `createBlocksClient()` more than once in the app; import the scaffold's `blocksClient` singleton everywhere `t()`/`load()` is needed, or dictionaries loaded in one part of the app won't be visible in another.
|
|
53
|
-
- **`translations`/`cloudTranslations` take `(moduleName, language)`** — module first.
|
|
54
|
-
- **Dictionaries only contain string values** — the SDK strips any non-string fields from the raw response (and unwraps a `data` envelope if present) before caching, so don't expect nested objects in a loaded dictionary.
|
|
55
|
-
- **This skill doesn't author content.** Adding a new key/module or changing a translated value goes through `blocks-localization-configuration`'s `blocks localization *` commands, not this skill.
|
|
56
|
-
|
|
57
|
-
## Example trigger prompts
|
|
58
|
-
|
|
59
|
-
- "Add a language switcher and translate the UI."
|
|
60
|
-
- "Load the `common` and `dashboard` translation modules on app startup and use them to render labels."
|
|
61
|
-
- "The Assets page still has hard-coded English strings — replace them with `t()` lookups."
|
|
62
|
-
- "Show only the languages this tenant actually has configured, with the default one preselected."
|
|
63
|
-
- "I need one specific translated key without pulling down the whole module."
|