@seliseblocks/cli-os 0.2.0 → 0.2.2
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 +9 -9
- package/README.md +2 -2
- package/dist/commands/data/schema/aggregation.js +4 -1
- package/dist/commands/iam/permissions/list.js +13 -6
- package/dist/commands/iam/roles/assign-permissions.js +66 -5
- package/dist/commands/iam/roles/list.js +13 -6
- package/dist/commands/new/web.js +2 -1
- package/dist/index.js +11 -8
- package/dist/lib/domains.d.ts +2 -0
- package/dist/lib/domains.js +14 -0
- package/dist/lib/scaffold-web/fs.d.ts +2 -1
- package/dist/lib/scaffold-web/fs.js +2 -8
- package/dist/lib/scaffold-web/root-files.js +158 -0
- package/dist/skills/blocks-iam-access-control/flows/manage-roles-permissions.md +1 -0
- package/dist/skills/blocks-onboarding/SKILL.md +1 -1
- package/package.json +1 -1
package/AI_USAGE_GUIDE.md
CHANGED
|
@@ -128,10 +128,10 @@ Then run with explicit flags so no prompt is reached:
|
|
|
128
128
|
blocks new web <appName> --x-blocks-key <projectTenantId> --app-domain <appDomainOrUrl> --client-id <publicOidcClientId>
|
|
129
129
|
```
|
|
130
130
|
|
|
131
|
-
`new web` also accepts `--blocks-api-url <url>` and `--oidc-url <url>`, same as `sdk client`
|
|
132
|
-
below. `--blocks-api-url`
|
|
133
|
-
|
|
134
|
-
non-default
|
|
131
|
+
`new web` also accepts `--blocks-api-url <url>` and `--oidc-url <url>`, same as `sdk client`
|
|
132
|
+
below. When `--blocks-api-url` is omitted, the scaffold derives it from the app domain as
|
|
133
|
+
`https://blocksapi.<registrable-domain>`; for example `https://dqrsf.slsblx.com` becomes
|
|
134
|
+
`https://blocksapi.slsblx.com`. Pass `--blocks-api-url` only when targeting a non-default Blocks gateway. `--oidc-url` defaults to `https://iam.seliseblocks.com`.
|
|
135
135
|
|
|
136
136
|
Validate the scaffold:
|
|
137
137
|
|
|
@@ -160,10 +160,10 @@ The generated cert script uses the `selfsigned` Node dependency, so it works fro
|
|
|
160
160
|
`sdk client` answers "I want to use the Blocks SDK - show me the client." It resolves this project's `@seliseblocks/client` config (same values `new web` scaffolds an app with) and prints a ready-to-paste `createBlocksClient(...)` snippet - **it never writes a file or mutates anything**. To scaffold a full app instead, use `new web` above.
|
|
161
161
|
|
|
162
162
|
```bash
|
|
163
|
-
blocks sdk client --x-blocks-key <projectTenantId> --app-domain <appDomainOrUrl> --client-id <publicOidcClientId> --blocks-api-url https://api.seliseblocks.com
|
|
163
|
+
blocks sdk client --x-blocks-key <projectTenantId> --app-domain <appDomainOrUrl> --client-id <publicOidcClientId> --blocks-api-url https://api.seliseblocks.com
|
|
164
164
|
```
|
|
165
165
|
|
|
166
|
-
|
|
166
|
+
Unlike `new web`, `sdk client` keeps `--blocks-api-url` defaulted to `https://api.seliseblocks.com`; only pass it explicitly if your project uses a different gateway URL. Passing both `--app-domain` and `--client-id` skips the project lookup entirely, so it needs no CLI login at all - useful for a quick, non-interactive check. Omit either one and it resolves from the selected project instead (auto-picks when there's exactly one match, otherwise lists the options and asks you to pass the flag explicitly - it does not prompt or create anything, since this command is read-only). Use `--json` for the resolved values instead of the snippet.
|
|
167
167
|
|
|
168
168
|
## Skills
|
|
169
169
|
|
|
@@ -181,9 +181,9 @@ Every other command below is project-scoped: it requires a project already selec
|
|
|
181
181
|
|
|
182
182
|
Command families (run `blocks --help` for the full flag reference on each):
|
|
183
183
|
|
|
184
|
-
- `iam users *`, `iam email available` - list/get/create/update/activate/deactivate, access grant/revoke, existence and email-availability checks.
|
|
185
|
-
- `iam roles *` - list/get/create/update, assign-permissions, assignable.
|
|
186
|
-
- `iam permissions *` - list/get/create/update, by-severity.
|
|
184
|
+
- `iam users *`, `iam email available` - list/get/create/update/activate/deactivate, access grant/revoke, existence and email-availability checks.
|
|
185
|
+
- `iam roles *` - list/get/create/update, assign-permissions, assignable. `assign-permissions` accepts permission resource strings and resolves them to itemIds before sending IAM's id-based mutation.
|
|
186
|
+
- `iam permissions *` - list/get/create/update, by-severity.
|
|
187
187
|
- `iam resources *` - resource groups and feature flags (read-only).
|
|
188
188
|
- `iam organizations *` - list/get/create/update, `my`, and organization config get/save.
|
|
189
189
|
- `iam signup-settings *` - get/save tenant signup policy.
|
package/README.md
CHANGED
|
@@ -95,9 +95,9 @@ Global options available on every command:
|
|
|
95
95
|
| `blocks release status <buildId> [--json]` | Read Release build status by build id. Read-only. |
|
|
96
96
|
| `blocks release builds list [repoId] [--repo-id <repoId>] [--json]` | List Release build details for a repository. When `repoId` is omitted, resolves it from the selected project's linked repo assets - auto-picked if there's exactly one, otherwise you're prompted to choose. Read-only. |
|
|
97
97
|
| `blocks release builds get <buildId> [--json]` | Alias for `release status`. Read-only. |
|
|
98
|
-
| `blocks new web <name> [--app-domain <domain>] [--client-id <oidcClientId>] [--x-blocks-key <tenantId>] [--blocks-api-url <url>] [--oidc-url <url>]` | Create a Vite React starter app that talks to Blocks exclusively through `@seliseblocks/client` (a single `createBlocksClient()` instance) using the SDK hosted IdP flow: `blocksClient.auth.idp.redirectToProvider()` on login click and `blocksClient.auth.idp.callback()` on `/login/callback`. Includes route guards, auto-refresh through `auth.oidc.refreshToken()`, live `auth`/`iam`/`data`/`localization` SDK examples, environment config, and safe `.gitignore` defaults. Uses the selected project (see `use`) unless `--x-blocks-key` overrides it. `--app-domain` and `--client-id` are resolved from the project record when omitted: the domain auto-picks if the project has exactly one, otherwise you're prompted to choose; the OIDC client is picked from the project's existing clients, or you can create a minimal one (display name + redirect URI) on the spot, or skip and register one later from the portal or `auth oidc-clients save`. `--blocks-api-url`
|
|
98
|
+
| `blocks new web <name> [--app-domain <domain>] [--client-id <oidcClientId>] [--x-blocks-key <tenantId>] [--blocks-api-url <url>] [--oidc-url <url>]` | Create a Vite React starter app that talks to Blocks exclusively through `@seliseblocks/client` (a single `createBlocksClient()` instance) using the SDK hosted IdP flow: `blocksClient.auth.idp.redirectToProvider()` on login click and `blocksClient.auth.idp.callback()` on `/login/callback`. Includes route guards, auto-refresh through `auth.oidc.refreshToken()`, live `auth`/`iam`/`data`/`localization` SDK examples, environment config, and safe `.gitignore` defaults. Uses the selected project (see `use`) unless `--x-blocks-key` overrides it. `--app-domain` and `--client-id` are resolved from the project record when omitted: the domain auto-picks if the project has exactly one, otherwise you're prompted to choose; the OIDC client is picked from the project's existing clients, or you can create a minimal one (display name + redirect URI) on the spot, or skip and register one later from the portal or `auth oidc-clients save`. When `--blocks-api-url` is omitted, `new web` derives it from the app domain as `https://blocksapi.<registrable-domain>`; for example `https://dqrsf.slsblx.com` becomes `https://blocksapi.slsblx.com`. Pass a different Data/IAM/Localization/OS gateway URL explicitly only if your project uses a non-default one. `--oidc-url` defaults to `https://iam.seliseblocks.com`. |
|
|
99
99
|
| `blocks skill list [--json]` / `skill show <name> [--json]` / `skill add <name> [--dir <path>]` | Local-only, no cloud calls: list/print the bundled `blocks-skills/*/SKILL.md` agent context docs, or copy a skill's entire directory (`SKILL.md` plus any supporting files) into `<dir>/<name>/` (default `./blocks-skills`) for use in a project outside this monorepo. `list`'s output and `show`/`add`'s "unknown skill" error both point at the full public skill catalog in case the bundled set is out of date. |
|
|
100
|
-
| `blocks sdk client [--app-domain <domain>] [--client-id <oidcClientId>] [--x-blocks-key <tenantId>] [--blocks-api-url <url>] [--oidc-url <url>] [--json]` | Read-only: resolves this project's `@seliseblocks/client` config
|
|
100
|
+
| `blocks sdk client [--app-domain <domain>] [--client-id <oidcClientId>] [--x-blocks-key <tenantId>] [--blocks-api-url <url>] [--oidc-url <url>] [--json]` | Read-only: resolves this project's `@seliseblocks/client` config and prints a ready-to-paste `createBlocksClient(...)` snippet. Unlike `new web`, the API URL defaults to `https://api.seliseblocks.com` unless `--blocks-api-url` is passed. Passing both `--app-domain` and `--client-id` skips the project lookup entirely (no login required). Writes nothing - use `new web` to scaffold a full app. |
|
|
101
101
|
|
|
102
102
|
Use `--json` on commands when AI or automation needs machine-readable output. Use `--dry-run` before mutations and `--yes` only after approval.
|
|
103
103
|
|
|
@@ -6,6 +6,9 @@ import { parseCommand, selectedProject } from "../../../lib/workspace.js";
|
|
|
6
6
|
export async function dataSchemaAggregation(argv) {
|
|
7
7
|
const { flags } = parseCommand(argv);
|
|
8
8
|
const projectKey = await selectedProject(flags);
|
|
9
|
+
const page = integerFlag(flags, "page", 1);
|
|
10
|
+
if (page < 1)
|
|
11
|
+
throw new Error("--page must be greater than or equal to 1");
|
|
9
12
|
const result = await blocksRequest("/data/v4/schemas/aggregation", {
|
|
10
13
|
impersonatedProjectAuth: true,
|
|
11
14
|
...requestContext(flags),
|
|
@@ -13,7 +16,7 @@ export async function dataSchemaAggregation(argv) {
|
|
|
13
16
|
query: {
|
|
14
17
|
CollectionName: stringFlag(flags, "collection-name") || undefined,
|
|
15
18
|
Keyword: stringFlag(flags, "keyword") || undefined,
|
|
16
|
-
PageNo:
|
|
19
|
+
PageNo: page,
|
|
17
20
|
PageSize: integerFlag(flags, "page-size", 100),
|
|
18
21
|
ProjectKey: projectKey,
|
|
19
22
|
SchemaName: stringFlag(flags, "schema-name") || undefined,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { integerFlag, optionalBooleanFlag, optionalIntegerFlag, stringFlag } from "../../../lib/args.js";
|
|
1
|
+
import { booleanFlag, integerFlag, optionalBooleanFlag, optionalIntegerFlag, stringFlag } from "../../../lib/args.js";
|
|
2
2
|
import { blocksRequest } from "../../../lib/api.js";
|
|
3
3
|
import { compact, jsonBodyFlag, listFlag } from "../../../lib/json-flag.js";
|
|
4
4
|
import { writeOutput } from "../../../lib/output.js";
|
|
@@ -7,6 +7,7 @@ import { parseCommand, selectedProject } from "../../../lib/workspace.js";
|
|
|
7
7
|
export async function iamPermissionsList(argv) {
|
|
8
8
|
const { flags } = parseCommand(argv);
|
|
9
9
|
const projectKey = await selectedProject(flags);
|
|
10
|
+
const sortBy = stringFlag(flags, "sort-by");
|
|
10
11
|
const filter = {
|
|
11
12
|
...(await jsonBodyFlag(flags)).filter,
|
|
12
13
|
...compact({
|
|
@@ -23,13 +24,13 @@ export async function iamPermissionsList(argv) {
|
|
|
23
24
|
const body = {
|
|
24
25
|
filter,
|
|
25
26
|
organizationId: stringFlag(flags, "organization-id") || undefined,
|
|
26
|
-
page:
|
|
27
|
+
page: iamBackendPage(flags),
|
|
27
28
|
pageSize: integerFlag(flags, "page-size", 20),
|
|
28
29
|
roles: listFlag(flags, "roles"),
|
|
29
|
-
sort: {
|
|
30
|
-
isDescending:
|
|
31
|
-
property:
|
|
32
|
-
}
|
|
30
|
+
sort: sortBy ? {
|
|
31
|
+
isDescending: booleanFlag(flags, "sort-desc"),
|
|
32
|
+
property: sortBy
|
|
33
|
+
} : undefined
|
|
33
34
|
};
|
|
34
35
|
const result = await blocksRequest("/iam/v4/iam/permissions", {
|
|
35
36
|
body,
|
|
@@ -39,3 +40,9 @@ export async function iamPermissionsList(argv) {
|
|
|
39
40
|
});
|
|
40
41
|
writeOutput(result, flags);
|
|
41
42
|
}
|
|
43
|
+
function iamBackendPage(flags) {
|
|
44
|
+
const page = integerFlag(flags, "page", 1);
|
|
45
|
+
if (page < 1)
|
|
46
|
+
throw new Error("--page must be greater than or equal to 1");
|
|
47
|
+
return page - 1;
|
|
48
|
+
}
|
|
@@ -8,10 +8,13 @@ import { parseCommand, selectedProject } from "../../../lib/workspace.js";
|
|
|
8
8
|
export async function iamRolesAssignPermissions(argv) {
|
|
9
9
|
const { args, flags } = parseCommand(argv);
|
|
10
10
|
const slug = args[0] || stringFlag(flags, "slug", { required: true });
|
|
11
|
+
const addPermissions = listFlag(flags, "add-permissions");
|
|
12
|
+
const removePermissions = listFlag(flags, "remove-permissions");
|
|
13
|
+
const organizationId = stringFlag(flags, "organization-id") || undefined;
|
|
11
14
|
const body = {
|
|
12
|
-
addPermissions
|
|
13
|
-
|
|
14
|
-
removePermissions
|
|
15
|
+
addPermissions,
|
|
16
|
+
organizationId,
|
|
17
|
+
removePermissions,
|
|
15
18
|
slug
|
|
16
19
|
};
|
|
17
20
|
if (!body.addPermissions && !body.removePermissions) {
|
|
@@ -21,13 +24,71 @@ export async function iamRolesAssignPermissions(argv) {
|
|
|
21
24
|
writeOutput({ dryRun: true, endpoint: "/iam/v4/iam/roles/assign-permissions", request: body }, flags);
|
|
22
25
|
return;
|
|
23
26
|
}
|
|
24
|
-
await confirmMutation(flags, `Change permission assignments for IAM role '${slug}'.`);
|
|
25
27
|
const projectKey = await selectedProject(flags);
|
|
28
|
+
const resolvedBody = {
|
|
29
|
+
...body,
|
|
30
|
+
addPermissions: await resolvePermissionIdentifiers(addPermissions, organizationId, flags, projectKey),
|
|
31
|
+
removePermissions: await resolvePermissionIdentifiers(removePermissions, organizationId, flags, projectKey)
|
|
32
|
+
};
|
|
33
|
+
await confirmMutation(flags, `Change permission assignments for IAM role '${slug}'.`);
|
|
26
34
|
const result = await blocksRequest("/iam/v4/iam/roles/assign-permissions", {
|
|
27
|
-
body,
|
|
35
|
+
body: resolvedBody,
|
|
28
36
|
impersonatedProjectAuth: true,
|
|
29
37
|
...requestContext(flags),
|
|
30
38
|
projectTenantId: projectKey
|
|
31
39
|
});
|
|
32
40
|
writeOutput(result, flags);
|
|
33
41
|
}
|
|
42
|
+
async function resolvePermissionIdentifiers(identifiers, organizationId, flags, projectKey) {
|
|
43
|
+
if (!identifiers?.length)
|
|
44
|
+
return identifiers;
|
|
45
|
+
const resources = identifiers.filter((item) => item.includes("::"));
|
|
46
|
+
if (resources.length === 0)
|
|
47
|
+
return identifiers;
|
|
48
|
+
const response = await blocksRequest("/iam/v4/iam/permissions", {
|
|
49
|
+
body: {
|
|
50
|
+
filter: {
|
|
51
|
+
isArchived: false,
|
|
52
|
+
resources
|
|
53
|
+
},
|
|
54
|
+
organizationId,
|
|
55
|
+
page: 0,
|
|
56
|
+
pageSize: Math.max(resources.length, 20)
|
|
57
|
+
},
|
|
58
|
+
impersonatedProjectAuth: true,
|
|
59
|
+
...requestContext(flags),
|
|
60
|
+
projectTenantId: projectKey
|
|
61
|
+
});
|
|
62
|
+
const permissions = extractPermissionRows(response);
|
|
63
|
+
const byResource = new Map();
|
|
64
|
+
for (const permission of permissions) {
|
|
65
|
+
const resource = stringProperty(permission, "resource");
|
|
66
|
+
const itemId = stringProperty(permission, "itemId") || stringProperty(permission, "_id") || stringProperty(permission, "id");
|
|
67
|
+
if (resource && itemId)
|
|
68
|
+
byResource.set(resource, itemId);
|
|
69
|
+
}
|
|
70
|
+
const missing = resources.filter((resource) => !byResource.has(resource));
|
|
71
|
+
if (missing.length > 0) {
|
|
72
|
+
throw new Error(`Could not resolve IAM permission resource(s) to itemId: ${missing.join(", ")}`);
|
|
73
|
+
}
|
|
74
|
+
return identifiers.map((identifier) => byResource.get(identifier) ?? identifier);
|
|
75
|
+
}
|
|
76
|
+
function extractPermissionRows(response) {
|
|
77
|
+
if (Array.isArray(response))
|
|
78
|
+
return response.filter(isRecord);
|
|
79
|
+
if (!isRecord(response))
|
|
80
|
+
return [];
|
|
81
|
+
for (const key of ["data", "Data", "items", "Items", "permissions", "Permissions"]) {
|
|
82
|
+
const value = response[key];
|
|
83
|
+
if (Array.isArray(value))
|
|
84
|
+
return value.filter(isRecord);
|
|
85
|
+
}
|
|
86
|
+
return [];
|
|
87
|
+
}
|
|
88
|
+
function isRecord(value) {
|
|
89
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
90
|
+
}
|
|
91
|
+
function stringProperty(record, name) {
|
|
92
|
+
const value = record[name] ?? record[name[0].toUpperCase() + name.slice(1)];
|
|
93
|
+
return typeof value === "string" ? value : "";
|
|
94
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { integerFlag, stringFlag } from "../../../lib/args.js";
|
|
1
|
+
import { booleanFlag, integerFlag, stringFlag } from "../../../lib/args.js";
|
|
2
2
|
import { blocksRequest } from "../../../lib/api.js";
|
|
3
3
|
import { compact, jsonBodyFlag, listFlag } from "../../../lib/json-flag.js";
|
|
4
4
|
import { writeOutput } from "../../../lib/output.js";
|
|
@@ -7,6 +7,7 @@ import { parseCommand, selectedProject } from "../../../lib/workspace.js";
|
|
|
7
7
|
export async function iamRolesList(argv) {
|
|
8
8
|
const { flags } = parseCommand(argv);
|
|
9
9
|
const projectKey = await selectedProject(flags);
|
|
10
|
+
const sortBy = stringFlag(flags, "sort-by");
|
|
10
11
|
const filter = {
|
|
11
12
|
...(await jsonBodyFlag(flags)).filter,
|
|
12
13
|
...compact({
|
|
@@ -17,12 +18,12 @@ export async function iamRolesList(argv) {
|
|
|
17
18
|
const body = {
|
|
18
19
|
filter,
|
|
19
20
|
organizationId: stringFlag(flags, "organization-id") || undefined,
|
|
20
|
-
page:
|
|
21
|
+
page: iamBackendPage(flags),
|
|
21
22
|
pageSize: integerFlag(flags, "page-size", 20),
|
|
22
|
-
sort: {
|
|
23
|
-
isDescending:
|
|
24
|
-
property:
|
|
25
|
-
}
|
|
23
|
+
sort: sortBy ? {
|
|
24
|
+
isDescending: booleanFlag(flags, "sort-desc"),
|
|
25
|
+
property: sortBy
|
|
26
|
+
} : undefined
|
|
26
27
|
};
|
|
27
28
|
const result = await blocksRequest("/iam/v4/iam/roles", {
|
|
28
29
|
body,
|
|
@@ -32,3 +33,9 @@ export async function iamRolesList(argv) {
|
|
|
32
33
|
});
|
|
33
34
|
writeOutput(result, flags);
|
|
34
35
|
}
|
|
36
|
+
function iamBackendPage(flags) {
|
|
37
|
+
const page = integerFlag(flags, "page", 1);
|
|
38
|
+
if (page < 1)
|
|
39
|
+
throw new Error("--page must be greater than or equal to 1");
|
|
40
|
+
return page - 1;
|
|
41
|
+
}
|
package/dist/commands/new/web.js
CHANGED
|
@@ -2,6 +2,7 @@ import { stringFlag } from "../../lib/args.js";
|
|
|
2
2
|
import { blocksRequest } from "../../lib/api.js";
|
|
3
3
|
import { confirmMutation } from "../../lib/confirm.js";
|
|
4
4
|
import { defaults, readConfig, writeConfig } from "../../lib/config.js";
|
|
5
|
+
import { apiUrlFromAppDomain } from "../../lib/domains.js";
|
|
5
6
|
import { CliActionableError } from "../../lib/errors.js";
|
|
6
7
|
import { findProjectByTenantId } from "../../lib/project-info.js";
|
|
7
8
|
import { promptText, selectFromList } from "../../lib/prompt.js";
|
|
@@ -16,9 +17,9 @@ export async function newWeb(argv) {
|
|
|
16
17
|
const tenantId = stringFlag(flags, "x-blocks-key") || (await selectedProject(flags));
|
|
17
18
|
const explicitAppDomain = stringFlag(flags, "app-domain");
|
|
18
19
|
const project = explicitAppDomain ? {} : (await findProjectByTenantId(tenantId, flags)).project;
|
|
19
|
-
const apiUrl = stringFlag(flags, "blocks-api-url", { defaultValue: defaults().apiUrl });
|
|
20
20
|
const oidcUrl = stringFlag(flags, "oidc-url", { defaultValue: defaults().oidcUrl });
|
|
21
21
|
const appDomain = await resolveAppDomain(project, flags);
|
|
22
|
+
const apiUrl = stringFlag(flags, "blocks-api-url") || apiUrlFromAppDomain(appDomain);
|
|
22
23
|
const oidcClientId = await resolveOidcClientId(tenantId, appDomain, name, flags);
|
|
23
24
|
await scaffoldWebProject({
|
|
24
25
|
apiUrl,
|
package/dist/index.js
CHANGED
|
@@ -1089,9 +1089,11 @@ Scaffold:
|
|
|
1089
1089
|
name + redirect URI, active, registered as a Blocks OIDC identity
|
|
1090
1090
|
provider) on the spot, or skip and register one later from the portal or
|
|
1091
1091
|
'auth oidc-clients save'.
|
|
1092
|
-
--blocks-api-url
|
|
1093
|
-
|
|
1094
|
-
|
|
1092
|
+
If --blocks-api-url is omitted, it is derived from the app domain:
|
|
1093
|
+
https://blocksapi.<registrable-domain> (for example, app domain
|
|
1094
|
+
https://dqrsf.slsblx.com uses https://blocksapi.slsblx.com). Pass a
|
|
1095
|
+
different Data/IAM/Localization/OS gateway URL explicitly only if your
|
|
1096
|
+
project uses a non-default one.
|
|
1095
1097
|
--oidc-url defaults to https://iam.seliseblocks.com.
|
|
1096
1098
|
|
|
1097
1099
|
Skills:
|
|
@@ -1109,11 +1111,12 @@ Skills:
|
|
|
1109
1111
|
SDK:
|
|
1110
1112
|
blocks sdk client [--app-domain <domain>] [--client-id <oidcClientId>]
|
|
1111
1113
|
[--x-blocks-key <tenantId>] [--blocks-api-url <url>] [--oidc-url <url>] [--json]
|
|
1112
|
-
Read-only: "I want to use the Blocks SDK -- show me the client." Resolves this
|
|
1113
|
-
project's @seliseblocks/client config
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1114
|
+
Read-only: "I want to use the Blocks SDK -- show me the client." Resolves this
|
|
1115
|
+
project's @seliseblocks/client config using the selected project unless
|
|
1116
|
+
--x-blocks-key overrides it, and the project's registered domain/OIDC client
|
|
1117
|
+
when --app-domain/--client-id are omitted. Its API URL defaults to
|
|
1118
|
+
https://api.seliseblocks.com unless --blocks-api-url is passed.
|
|
1119
|
+
Prints a ready-to-paste createBlocksClient(...) snippet.
|
|
1117
1120
|
Passing both --app-domain and --client-id skips the project lookup entirely
|
|
1118
1121
|
(no login required). Never writes a file; to scaffold a new app use 'new web'.
|
|
1119
1122
|
`);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export function hostFromAppDomain(appDomain) {
|
|
2
|
+
try {
|
|
3
|
+
return new URL(appDomain).host;
|
|
4
|
+
}
|
|
5
|
+
catch {
|
|
6
|
+
return appDomain.replace(/^https?:\/\//, "").replace(/\/.*$/, "");
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
export function apiUrlFromAppDomain(appDomain) {
|
|
10
|
+
const hostname = hostFromAppDomain(appDomain).split(":")[0].toLowerCase();
|
|
11
|
+
const labels = hostname.split(".").filter(Boolean);
|
|
12
|
+
const registrableDomain = labels.length >= 2 ? labels.slice(-2).join(".") : hostname;
|
|
13
|
+
return `https://blocksapi.${registrableDomain}`;
|
|
14
|
+
}
|
|
@@ -1,15 +1,9 @@
|
|
|
1
1
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
|
+
import { hostFromAppDomain } from "../domains.js";
|
|
4
|
+
export { hostFromAppDomain };
|
|
3
5
|
export async function write(root, path, content) {
|
|
4
6
|
const fullPath = join(root, path);
|
|
5
7
|
await mkdir(dirname(fullPath), { recursive: true });
|
|
6
8
|
await writeFile(fullPath, `${content.endsWith("\n") ? content : `${content}\n`}`);
|
|
7
9
|
}
|
|
8
|
-
export function hostFromAppDomain(appDomain) {
|
|
9
|
-
try {
|
|
10
|
-
return new URL(appDomain).host;
|
|
11
|
-
}
|
|
12
|
-
catch {
|
|
13
|
-
return appDomain.replace(/^https?:\/\//, "").replace(/\/.*$/, "");
|
|
14
|
-
}
|
|
15
|
-
}
|
|
@@ -8,6 +8,14 @@ export async function writeRootFiles(root, options) {
|
|
|
8
8
|
type: "module",
|
|
9
9
|
scripts: {
|
|
10
10
|
build: "vite build",
|
|
11
|
+
"build:dev": "vite build --mode dev && node scripts/write-release-env.mjs dev",
|
|
12
|
+
"build:test": "vite build --mode test && node scripts/write-release-env.mjs test",
|
|
13
|
+
"build:stg": "vite build --mode stg && node scripts/write-release-env.mjs stg",
|
|
14
|
+
"build:iat": "vite build --mode iat && node scripts/write-release-env.mjs iat",
|
|
15
|
+
"build:uat": "vite build --mode uat && node scripts/write-release-env.mjs uat",
|
|
16
|
+
"build:preprod": "vite build --mode preprod && node scripts/write-release-env.mjs preprod",
|
|
17
|
+
"build:prodshadow": "vite build --mode prodshadow && node scripts/write-release-env.mjs prodshadow",
|
|
18
|
+
"build:prod": "vite build --mode prod && node scripts/write-release-env.mjs prod",
|
|
11
19
|
cert: "node scripts/generate-cert.mjs",
|
|
12
20
|
dev: "vite",
|
|
13
21
|
lint: "tsc --noEmit",
|
|
@@ -74,9 +82,76 @@ export async function writeRootFiles(root, options) {
|
|
|
74
82
|
".env",
|
|
75
83
|
".env.local",
|
|
76
84
|
".env.*.local",
|
|
85
|
+
"env.*",
|
|
77
86
|
".cert/",
|
|
78
87
|
""
|
|
79
88
|
].join("\n"));
|
|
89
|
+
await write(root, "nginx.conf", [
|
|
90
|
+
"server {",
|
|
91
|
+
" listen 8080;",
|
|
92
|
+
" root /usr/share/nginx/html/;",
|
|
93
|
+
" server_tokens off;",
|
|
94
|
+
" client_max_body_size 200m;",
|
|
95
|
+
"",
|
|
96
|
+
" gzip on;",
|
|
97
|
+
" gzip_comp_level 6;",
|
|
98
|
+
" gzip_min_length 1000;",
|
|
99
|
+
" gzip_proxied expired no-cache no-store private auth;",
|
|
100
|
+
" gzip_types text/plain application/x-javascript text/xml text/css application/xml text/javascript application/javascript application/json application/font-woff application/font-woff2 application/vnd.ms-fontobject application/x-font-ttf font/opentype;",
|
|
101
|
+
"",
|
|
102
|
+
" location / {",
|
|
103
|
+
" try_files $uri $uri/ /index.html;",
|
|
104
|
+
" }",
|
|
105
|
+
"}",
|
|
106
|
+
""
|
|
107
|
+
].join("\n"));
|
|
108
|
+
await write(root, "Dockerfile", [
|
|
109
|
+
"FROM node:22-alpine AS builder",
|
|
110
|
+
"",
|
|
111
|
+
"WORKDIR /app",
|
|
112
|
+
"",
|
|
113
|
+
"COPY package*.json ./",
|
|
114
|
+
"",
|
|
115
|
+
"RUN if [ -f package-lock.json ]; then npm ci; else npm install; fi",
|
|
116
|
+
"",
|
|
117
|
+
"COPY . .",
|
|
118
|
+
"",
|
|
119
|
+
"ARG ci_build=dev",
|
|
120
|
+
"ARG VITE_BLOCKS_API_URL",
|
|
121
|
+
"ARG VITE_BLOCKS_PROJECT_KEY",
|
|
122
|
+
"ARG VITE_BLOCKS_X_BLOCKS_KEY",
|
|
123
|
+
"ARG VITE_BLOCKS_APP_DOMAIN",
|
|
124
|
+
"ARG VITE_BLOCKS_OIDC_URL=https://iam.seliseblocks.com",
|
|
125
|
+
"ARG VITE_BLOCKS_OIDC_CLIENT_ID",
|
|
126
|
+
"ARG VITE_BLOCKS_OIDC_SCOPE=\"openid profile\"",
|
|
127
|
+
"ARG VITE_BLOCKS_REDIRECT_URI",
|
|
128
|
+
"ARG VITE_BLOCKS_HOSTED_LOGIN=true",
|
|
129
|
+
"",
|
|
130
|
+
"ENV VITE_BLOCKS_API_URL=${VITE_BLOCKS_API_URL}",
|
|
131
|
+
"ENV VITE_BLOCKS_PROJECT_KEY=${VITE_BLOCKS_PROJECT_KEY}",
|
|
132
|
+
"ENV VITE_BLOCKS_X_BLOCKS_KEY=${VITE_BLOCKS_X_BLOCKS_KEY}",
|
|
133
|
+
"ENV VITE_BLOCKS_APP_DOMAIN=${VITE_BLOCKS_APP_DOMAIN}",
|
|
134
|
+
"ENV VITE_BLOCKS_OIDC_URL=${VITE_BLOCKS_OIDC_URL}",
|
|
135
|
+
"ENV VITE_BLOCKS_OIDC_CLIENT_ID=${VITE_BLOCKS_OIDC_CLIENT_ID}",
|
|
136
|
+
"ENV VITE_BLOCKS_OIDC_SCOPE=${VITE_BLOCKS_OIDC_SCOPE}",
|
|
137
|
+
"ENV VITE_BLOCKS_REDIRECT_URI=${VITE_BLOCKS_REDIRECT_URI}",
|
|
138
|
+
"ENV VITE_BLOCKS_HOSTED_LOGIN=${VITE_BLOCKS_HOSTED_LOGIN}",
|
|
139
|
+
"",
|
|
140
|
+
"RUN NODE_OPTIONS=\"--max-old-space-size=4096\" npx vite build --mode \"${ci_build}\" \\",
|
|
141
|
+
" && node scripts/write-release-env.mjs \"${ci_build}\"",
|
|
142
|
+
"",
|
|
143
|
+
"FROM nginxinc/nginx-unprivileged:1.29-alpine",
|
|
144
|
+
"",
|
|
145
|
+
"COPY --from=builder /app/dist /usr/share/nginx/html",
|
|
146
|
+
"",
|
|
147
|
+
"COPY nginx.conf /etc/nginx/conf.d/default.conf",
|
|
148
|
+
"",
|
|
149
|
+
"EXPOSE 8080",
|
|
150
|
+
"",
|
|
151
|
+
"# HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \\",
|
|
152
|
+
"# CMD wget -qO- http://localhost:8080/ || exit 1",
|
|
153
|
+
""
|
|
154
|
+
].join("\n"));
|
|
80
155
|
await write(root, "README.md", [
|
|
81
156
|
`# ${options.name}`,
|
|
82
157
|
"",
|
|
@@ -116,6 +191,12 @@ export async function writeRootFiles(root, options) {
|
|
|
116
191
|
"",
|
|
117
192
|
"`.cert/` is gitignored — each developer generates and trusts their own cert.",
|
|
118
193
|
"",
|
|
194
|
+
"## Blocks Release deployment",
|
|
195
|
+
"",
|
|
196
|
+
"The scaffold includes `Dockerfile` and `nginx.conf` for Blocks Release. The Release service must pass Docker build arg `ci_build=<environment>` plus the public `VITE_BLOCKS_*` build args documented in the Dockerfile. The generated `package.json` also provides `build:dev`, `build:test`, `build:stg`, `build:iat`, `build:uat`, `build:preprod`, `build:prodshadow`, and `build:prod` scripts for local checks.",
|
|
197
|
+
"",
|
|
198
|
+
"During each environment build, `scripts/write-release-env.mjs` writes `dist/env.<environment>` from client-safe Docker build args or local `.env` files. Root `.env` remains gitignored and must not be committed.",
|
|
199
|
+
"",
|
|
119
200
|
"## What's included",
|
|
120
201
|
"",
|
|
121
202
|
"- `/login` — login page (redirects to Blocks IAM).",
|
|
@@ -275,4 +356,81 @@ export async function writeRootFiles(root, options) {
|
|
|
275
356
|
"}",
|
|
276
357
|
""
|
|
277
358
|
].join("\n"));
|
|
359
|
+
await write(root, "scripts/write-release-env.mjs", [
|
|
360
|
+
"#!/usr/bin/env node",
|
|
361
|
+
"// Writes the client-safe Blocks env snapshot into dist for release artifacts.",
|
|
362
|
+
"import { existsSync, mkdirSync, readFileSync, writeFileSync } from \"node:fs\";",
|
|
363
|
+
"import { join } from \"node:path\";",
|
|
364
|
+
"",
|
|
365
|
+
"const mode = (process.argv[2] || \"dev\").trim();",
|
|
366
|
+
"if (!mode) {",
|
|
367
|
+
" console.error(\"Usage: node scripts/write-release-env.mjs <environment>\");",
|
|
368
|
+
" process.exit(1);",
|
|
369
|
+
"}",
|
|
370
|
+
"",
|
|
371
|
+
"const env = readEnv(mode);",
|
|
372
|
+
"const appDomain = env.VITE_BLOCKS_APP_DOMAIN || \"\";",
|
|
373
|
+
"const redirectUri = env.VITE_BLOCKS_REDIRECT_URI || callbackUri(appDomain);",
|
|
374
|
+
"const projectKey = env.VITE_BLOCKS_X_BLOCKS_KEY || env.VITE_BLOCKS_PROJECT_KEY || \"\";",
|
|
375
|
+
"",
|
|
376
|
+
"const output = [",
|
|
377
|
+
" \"# SELISE Blocks production/default configuration.\",",
|
|
378
|
+
" \"# Only client-safe values belong here. Never put passwords, PTOKs, JWTs,\",",
|
|
379
|
+
" \"# refresh tokens, cookies, or client secrets in VITE_-prefixed variables.\",",
|
|
380
|
+
" \"\",",
|
|
381
|
+
" `VITE_BLOCKS_API_URL=${env.VITE_BLOCKS_API_URL || \"\"}`,",
|
|
382
|
+
" `VITE_BLOCKS_PROJECT_KEY=${projectKey}`,",
|
|
383
|
+
" `VITE_BLOCKS_X_BLOCKS_KEY=${projectKey}`,",
|
|
384
|
+
" `VITE_BLOCKS_APP_DOMAIN=${appDomain}`,",
|
|
385
|
+
" `VITE_BLOCKS_OIDC_URL=${env.VITE_BLOCKS_OIDC_URL || \"\"}`,",
|
|
386
|
+
" `VITE_BLOCKS_OIDC_CLIENT_ID=${env.VITE_BLOCKS_OIDC_CLIENT_ID || \"\"}`,",
|
|
387
|
+
" `VITE_BLOCKS_OIDC_SCOPE=${env.VITE_BLOCKS_OIDC_SCOPE || \"openid profile\"}`,",
|
|
388
|
+
" `VITE_BLOCKS_REDIRECT_URI=${redirectUri}`,",
|
|
389
|
+
" \"VITE_BLOCKS_HOSTED_LOGIN=true\",",
|
|
390
|
+
" \"\"",
|
|
391
|
+
"].join(\"\\n\");",
|
|
392
|
+
"",
|
|
393
|
+
"mkdirSync(\"dist\", { recursive: true });",
|
|
394
|
+
"writeFileSync(join(\"dist\", `env.${mode}`), output);",
|
|
395
|
+
"console.log(`Wrote dist/env.${mode}`);",
|
|
396
|
+
"",
|
|
397
|
+
"function readEnv(mode) {",
|
|
398
|
+
" const result = {};",
|
|
399
|
+
" for (const file of [\".env\", `.env.${mode}`, \".env.local\", `.env.${mode}.local`]) {",
|
|
400
|
+
" if (!existsSync(file)) continue;",
|
|
401
|
+
" Object.assign(result, parseEnv(readFileSync(file, \"utf8\")));",
|
|
402
|
+
" }",
|
|
403
|
+
" for (const [key, value] of Object.entries(process.env)) {",
|
|
404
|
+
" if (!key.startsWith(\"VITE_BLOCKS_\") || value === undefined) continue;",
|
|
405
|
+
" result[key] = value;",
|
|
406
|
+
" }",
|
|
407
|
+
" return result;",
|
|
408
|
+
"}",
|
|
409
|
+
"",
|
|
410
|
+
"function parseEnv(content) {",
|
|
411
|
+
" const values = {};",
|
|
412
|
+
" for (const rawLine of content.split(/\\r?\\n/)) {",
|
|
413
|
+
" const line = rawLine.trim();",
|
|
414
|
+
" if (!line || line.startsWith(\"#\")) continue;",
|
|
415
|
+
" const equals = line.indexOf(\"=\");",
|
|
416
|
+
" if (equals < 1) continue;",
|
|
417
|
+
" const key = line.slice(0, equals).trim();",
|
|
418
|
+
" let value = line.slice(equals + 1).trim();",
|
|
419
|
+
" if ((value.startsWith(\"\\\"\") && value.endsWith(\"\\\"\")) || (value.startsWith(\"'\") && value.endsWith(\"'\"))) {",
|
|
420
|
+
" value = value.slice(1, -1);",
|
|
421
|
+
" }",
|
|
422
|
+
" values[key] = value;",
|
|
423
|
+
" }",
|
|
424
|
+
" return values;",
|
|
425
|
+
"}",
|
|
426
|
+
"",
|
|
427
|
+
"function callbackUri(appDomain) {",
|
|
428
|
+
" if (!appDomain) return \"\";",
|
|
429
|
+
" const origin = appDomain.startsWith(\"http://\") || appDomain.startsWith(\"https://\")",
|
|
430
|
+
" ? appDomain.replace(/\\/+$/, \"\")",
|
|
431
|
+
" : `https://${appDomain.replace(/\\/+$/, \"\")}`;",
|
|
432
|
+
" return `${origin}/login/callback`;",
|
|
433
|
+
"}",
|
|
434
|
+
""
|
|
435
|
+
].join("\n"));
|
|
278
436
|
}
|
|
@@ -103,6 +103,7 @@ blocks iam roles assign-permissions editor --add-permissions content::publish --
|
|
|
103
103
|
|
|
104
104
|
- **CLI mutations are project-scoped, not account-scoped** — `blocks iam roles create/update/assign-permissions` and `blocks iam permissions create/update` all require a selected project (`blocks use <tenantId>` or `--project <tenantId>`) and run against the impersonated-project token; `blocks iam me` is the one IAM command that uses the account token instead, so don't expect `iam me`'s auth context to carry over to these.
|
|
105
105
|
- **Role hierarchy and permission assignment key off `slug`**, not `itemId` — grab it from `roles.list()`/`roles.get()` (or `blocks iam roles list/get`) before calling `assignPermissions`.
|
|
106
|
+
- **Permission assignment ultimately uses permission `itemId`s** — the CLI resolves `resource` strings like `content::publish` before mutation; SDK/backend callers should pass permission ids directly in `addPermissions` / `removePermissions`.
|
|
106
107
|
- **`roles.assignPermissions` is additive/subtractive** (`addPermissions[]` / `removePermissions[]` in one call), not a full-set replace — compute the delta from what's checked/unchecked, don't resend the entire permission list as "adds."
|
|
107
108
|
- **`roles.assignable()` scopes to the caller** — always populate role pickers from it rather than `roles.list()`, so an admin can't be shown (or attempt to grant) a role above their own authority.
|
|
108
109
|
- **Never fire a create/update/assign-permissions call — CLI or SDK — without a human confirming that specific change first** (a reviewed `--dry-run` plus explicit go-ahead on the CLI, an explicit in-UI confirm for the SDK) — no auto-provisioning "default roles," no agent-initiated cleanup of permissions, no batch edits without a per-change confirm.
|
|
@@ -66,7 +66,7 @@ Run `blocks init` once per project directory to create `blocks.json`, `blocks/da
|
|
|
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
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> --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. 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> --
|
|
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`.
|
|
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
|