@seliseblocks/cli-os 0.2.5 → 0.2.7

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.
@@ -7,11 +7,12 @@ import { requestContext } from "../../../lib/request-context.js";
7
7
  import { parseCommand, selectedProject } from "../../../lib/workspace.js";
8
8
  export async function authConfigSave(argv) {
9
9
  const { flags } = parseCommand(argv);
10
- const body = {
10
+ const overrides = {
11
11
  ...(await jsonBodyFlag(flags)),
12
12
  ...compact({
13
13
  absoluteRefreshTokenValidForNumberMinutes: optionalIntegerFlag(flags, "absolute-refresh-token-minutes"),
14
14
  accessTokenValidForNumberMinutes: optionalIntegerFlag(flags, "access-token-minutes"),
15
+ accountActionBaseUrl: stringFlag(flags, "account-action-base-url") || undefined,
15
16
  accountLockDurationInMinutes: optionalIntegerFlag(flags, "account-lock-duration-minutes"),
16
17
  getNumberOfWrongAttemptsToLockTheAccount: optionalIntegerFlag(flags, "wrong-attempts-to-lock"),
17
18
  isOidcEnabled: booleanFlag(flags, "oidc-enabled") || undefined,
@@ -21,12 +22,37 @@ export async function authConfigSave(argv) {
21
22
  rememberMeRefreshTokenValidForNumberMinutes: optionalIntegerFlag(flags, "remember-me-refresh-token-minutes")
22
23
  })
23
24
  };
25
+ const projectKey = await selectedProject(flags);
26
+ // POST /auth/config replaces the whole config document rather than merging
27
+ // (confirmed against the portal's own save call, which always resends every
28
+ // field it read on load) -- fetch the current config first so fields the
29
+ // caller didn't mention here survive the round trip instead of resetting.
30
+ const current = await blocksRequest("/iam/v4/auth/config", {
31
+ impersonatedProjectAuth: true,
32
+ ...requestContext(flags),
33
+ projectTenantId: projectKey
34
+ });
35
+ const body = { ...current, ...overrides };
36
+ // Turning isOidcEnabled on isn't a single independent flag: the
37
+ // activation-link flow keys off accountActivationPath, which has to point
38
+ // at the OIDC variant once OIDC is on, or activation emails break.
39
+ // accountActionBaseUrl has no safe default this command can guess across
40
+ // environments, so the caller must supply it explicitly when the tenant
41
+ // doesn't already have one.
42
+ const missingActionBaseUrl = Boolean(body.isOidcEnabled) && !body.accountActionBaseUrl;
43
+ if (body.isOidcEnabled)
44
+ body.accountActivationPath = "oidc/activate/";
24
45
  if (booleanFlag(flags, "dry-run")) {
46
+ if (missingActionBaseUrl) {
47
+ console.warn("Warning: this tenant has no accountActionBaseUrl set. Enabling OIDC login requires one -- pass --account-action-base-url <https://your-iam-host> before re-running with --yes.");
48
+ }
25
49
  writeOutput({ dryRun: true, endpoint: "/iam/v4/auth/config", request: body }, flags);
26
50
  return;
27
51
  }
52
+ if (missingActionBaseUrl) {
53
+ throw new Error("Enabling OIDC login requires accountActionBaseUrl, and this tenant doesn't have one set. Pass --account-action-base-url <https://your-iam-host>.");
54
+ }
28
55
  await confirmMutation(flags, "Save AuthController configuration for the selected project.");
29
- const projectKey = await selectedProject(flags);
30
56
  const result = await blocksRequest("/iam/v4/auth/config", {
31
57
  body,
32
58
  impersonatedProjectAuth: true,
@@ -8,7 +8,7 @@ import { parseCommand, selectedProject } from "../../../lib/workspace.js";
8
8
  /** Upsert: omit --item-id to register a new OIDC client, pass it to update an existing one. */
9
9
  export async function authOidcClientsSave(argv) {
10
10
  const { flags } = parseCommand(argv);
11
- const body = {
11
+ const overrides = {
12
12
  ...(await jsonBodyFlag(flags)),
13
13
  ...compact({
14
14
  allowedMfaMethods: listFlag(flags, "allowed-mfa-methods")?.map(Number),
@@ -36,12 +36,27 @@ export async function authOidcClientsSave(argv) {
36
36
  useTokensCookie: optionalBooleanFlag(flags, "use-tokens-cookie")
37
37
  })
38
38
  };
39
+ const projectKey = await selectedProject(flags);
40
+ const itemId = typeof overrides.itemId === "string" ? overrides.itemId : undefined;
41
+ // Saving an existing client (itemId set) replaces the whole client document
42
+ // rather than merging -- the portal's own Edit dialog always resubmits every
43
+ // field, including ones this command wasn't asked to change. Fetch the
44
+ // current client first so unmentioned fields (redirectUris, scope, PKCE, ...)
45
+ // survive instead of being reset to defaults. A new client (no itemId) has
46
+ // no prior state to merge.
47
+ const current = itemId
48
+ ? await blocksRequest(`/iam/v4/oidc-clients/${encodeURIComponent(itemId)}`, {
49
+ impersonatedProjectAuth: true,
50
+ ...requestContext(flags),
51
+ projectTenantId: projectKey
52
+ })
53
+ : {};
54
+ const body = { ...current, ...overrides };
39
55
  if (booleanFlag(flags, "dry-run")) {
40
56
  writeOutput({ dryRun: true, endpoint: "/iam/v4/oidc-clients", request: redactSecret(body) }, flags);
41
57
  return;
42
58
  }
43
59
  await confirmMutation(flags, `Save OIDC client '${body.clientDisplayName ?? body.itemId ?? "(new)"}'. The response's client secret is shown once.`);
44
- const projectKey = await selectedProject(flags);
45
60
  const result = await blocksRequest("/iam/v4/oidc-clients", {
46
61
  body,
47
62
  impersonatedProjectAuth: true,
@@ -21,6 +21,15 @@ export async function newWeb(argv) {
21
21
  const appDomain = await resolveAppDomain(project, flags);
22
22
  const apiUrl = stringFlag(flags, "blocks-api-url") || apiUrlFromAppDomain(appDomain);
23
23
  const oidcClientId = await resolveOidcClientId(tenantId, appDomain, name, flags);
24
+ if (oidcClientId) {
25
+ try {
26
+ await ensureOidcLoginEnabled(tenantId, oidcUrl, flags);
27
+ }
28
+ catch (error) {
29
+ console.warn(`Warning: could not confirm/enable OIDC login on this project's AuthController config: ${error.message}`);
30
+ console.warn("Enable it manually: 'blocks auth:config:save --oidc-enabled --project " + tenantId + "', or in the Blocks portal under IAM > Auth Config.");
31
+ }
32
+ }
24
33
  await scaffoldWebProject({
25
34
  apiUrl,
26
35
  appDomain,
@@ -116,6 +125,45 @@ async function listOidcClientSummaries(tenantId, flags) {
116
125
  }
117
126
  return summaries;
118
127
  }
128
+ // Creating/selecting an OIDC client is not enough for the hosted login flow to
129
+ // work: AuthController separately gates the whole OIDC/IdP path behind
130
+ // isOidcEnabled on the tenant's auth config, off by default. Without this,
131
+ // scaffolded apps 404/error on login until someone flips it manually in the
132
+ // portal (IAM > Auth Config), so check-and-enable it here as part of setup.
133
+ // POST /auth/config replaces the whole config document rather than merging
134
+ // (confirmed against the portal's own save call, which always resends every
135
+ // field) -- sending just `{ isOidcEnabled: true }` would reset every other
136
+ // AuthController setting for this tenant, so the fetched config is spread
137
+ // back in full with only that one field overridden.
138
+ //
139
+ // Turning isOidcEnabled on isn't a single independent flag either: the
140
+ // activation-link flow keys off accountActivationPath, which has to point at
141
+ // the OIDC variant ("oidc/activate/") once OIDC is on, or activation emails
142
+ // break. accountActionBaseUrl (the host those links are built against) has
143
+ // no safe cross-environment default, but this project's own IAM host is
144
+ // already known here as `oidcUrl`, so it's used whenever the tenant doesn't
145
+ // already have one set.
146
+ async function ensureOidcLoginEnabled(tenantId, oidcUrl, flags) {
147
+ const config = await blocksRequest("/iam/v4/auth/config", {
148
+ impersonatedProjectAuth: true,
149
+ projectTenantId: tenantId,
150
+ ...requestContext(flags)
151
+ });
152
+ if (config.isOidcEnabled)
153
+ return;
154
+ await confirmMutation(flags, "Enable OIDC login on this project's AuthController configuration.");
155
+ await blocksRequest("/iam/v4/auth/config", {
156
+ body: {
157
+ ...config,
158
+ accountActionBaseUrl: config.accountActionBaseUrl || oidcUrl,
159
+ accountActivationPath: "oidc/activate/",
160
+ isOidcEnabled: true
161
+ },
162
+ impersonatedProjectAuth: true,
163
+ projectTenantId: tenantId,
164
+ ...requestContext(flags)
165
+ });
166
+ }
119
167
  function normalizeList(raw) {
120
168
  if (Array.isArray(raw))
121
169
  return raw;
@@ -135,10 +183,15 @@ async function createOidcClientInteractively(tenantId, appDomain, appName, flags
135
183
  // clientType drives IAM's tokenEndpointAuthMethod: omitting it stores this browser
136
184
  // app as confidential ("client_secret_post") and lets it request client_credentials.
137
185
  // The scaffold only ever produces a PKCE SPA, so it is always "public".
186
+ // isAutoRedirect: the scaffolded login page's startLogin() already navigates
187
+ // straight to the provider via window.location.assign -- without this flag IAM
188
+ // shows an interstitial "continue" click on the hosted login page instead of
189
+ // redirecting immediately, which is dead weight for a flow the SPA already drives.
138
190
  const body = {
139
191
  clientDisplayName: displayName,
140
192
  clientType: "public",
141
193
  isActive: true,
194
+ isAutoRedirect: true,
142
195
  redirectUris: [redirectUri],
143
196
  registerAsIdentityProvider: true,
144
197
  requirePkce: true,
@@ -65,8 +65,8 @@ Run `blocks init` once per project directory to create `blocks.json`, `blocks/da
65
65
 
66
66
  Then route to what the user actually wants:
67
67
  - Building a frontend from scratch → resolve the app's public OIDC client first, then scaffold:
68
- - `blocks auth oidc-clients list --json` — check whether a client already registered for this project fits. If none fits, create one directly (no portal visit needed): `blocks auth oidc-clients save --client-display-name <appName> --client-type public --redirect-uris https://<domain>:5173/login/callback --scope "openid profile" --require-pkce --register-as-identity-provider --dry-run --json`, then re-run with `--yes` after showing the dry-run output and getting approval. `--client-type public` is required — IAM derives `tokenEndpointAuthMethod` from it, so omitting it stores a browser client as confidential. `--register-as-identity-provider` creates the linked identity provider in the same call; nothing further to run. See the blocks-iam-sso-oidc-configuration skill for the full decision tree and field-level gotchas.
69
- - `blocks new web <name> --x-blocks-key <tenantId> --app-domain <domain> --client-id <the-resolved-client-id>`. **Always pass `--client-id` and `--app-domain` explicitly** — omitting either drops `new web` into an interactive pick-list prompt with no non-interactive escape (not even to "skip"), which hangs a scripted/agent run with no stdin to answer it. Omit `--blocks-api-url` unless the project uses a non-default gateway; the scaffold derives it from the app domain, e.g. `https://dqrsf.slsblx.com` -> `https://blocksapi.slsblx.com`.
68
+ - `blocks auth oidc-clients list --json` — check whether a client already registered for this project fits. If none fits, create one directly (no portal visit needed): `blocks auth oidc-clients save --client-display-name <appName> --client-type public --redirect-uris https://<domain>:5173/login/callback --scope "openid profile" --require-pkce --register-as-identity-provider --auto-redirect --dry-run --json`, then re-run with `--yes` after showing the dry-run output and getting approval. `--client-type public` is required — IAM derives `tokenEndpointAuthMethod` from it, so omitting it stores a browser client as confidential. `--register-as-identity-provider` creates the linked identity provider in the same call; nothing further to run. `--auto-redirect` matters too — the scaffolded login page already navigates straight to the provider itself, so without it IAM's hosted login page shows a redundant manual "continue" click. When updating an *existing* client instead of creating one, always pass `--item-id` — the save endpoint replaces the whole client document, and the CLI fetches the current one first to merge your change into it rather than resetting the rest. See the blocks-iam-sso-oidc-configuration skill for the full decision tree and field-level gotchas.
69
+ - `blocks new web <name> --x-blocks-key <tenantId> --app-domain <domain> --client-id <the-resolved-client-id>`. **Always pass `--client-id` and `--app-domain` explicitly** — omitting either drops `new web` into an interactive pick-list prompt with no non-interactive escape (not even to "skip"), which hangs a scripted/agent run with no stdin to answer it. Omit `--blocks-api-url` unless the project uses a non-default gateway; the scaffold derives it from the app domain, e.g. `https://dqrsf.slsblx.com` -> `https://blocksapi.slsblx.com`. Once it resolves the client id, `new web` also checks the tenant's AuthController config and turns on `isOidcEnabled` if it's off — nothing further to do for login to actually work; if you're wiring an existing app instead (`blocks sdk client`, no `new web` call), check that yourself first: `blocks auth config get --json`, and if `isOidcEnabled` is `false`, `blocks auth config save --oidc-enabled --dry-run --json` then `--yes`.
70
70
  - Defining data / CRUD / localization / release on an existing project → hand off to the matching skill; the project is already selected via `blocks use`, so its commands can proceed directly.
71
71
 
72
72
  ## Gotchas
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seliseblocks/cli-os",
3
- "version": "0.2.5",
3
+ "version": "0.2.7",
4
4
  "description": "CLI for SELISE Blocks project setup and configuration.",
5
5
  "license": "MIT",
6
6
  "type": "module",