@seliseblocks/cli-os 0.1.1 → 0.1.3
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 +285 -305
- package/LICENSE +21 -21
- package/README.md +136 -159
- package/bin/run.js +2 -2
- package/dist/commands/auth/status.js +16 -51
- package/dist/commands/doctor.js +11 -7
- package/dist/commands/init.js +4 -2
- package/dist/commands/login.d.ts +0 -1
- package/dist/commands/login.js +15 -55
- package/dist/commands/new/web.js +1 -1
- package/dist/index.js +129 -175
- package/dist/lib/auth.d.ts +4 -11
- package/dist/lib/auth.js +43 -18
- package/dist/lib/config.d.ts +2 -2
- package/dist/lib/config.js +45 -13
- package/dist/lib/open-browser.js +26 -3
- package/dist/lib/secret-store.d.ts +0 -1
- package/dist/lib/secret-store.js +1 -4
- package/dist/lib/token-store.d.ts +0 -1
- package/dist/lib/token-store.js +1 -5
- package/dist/lib/token.d.ts +3 -1
- package/dist/lib/token.js +34 -7
- package/package.json +47 -47
- package/dist/commands/auth/add.d.ts +0 -1
- package/dist/commands/auth/add.js +0 -51
- package/dist/commands/auth/list.d.ts +0 -1
- package/dist/commands/auth/list.js +0 -33
- package/dist/commands/auth/repair.d.ts +0 -1
- package/dist/commands/auth/repair.js +0 -39
- package/dist/commands/auth/show.d.ts +0 -1
- package/dist/commands/auth/show.js +0 -23
- package/dist/commands/auth/use.d.ts +0 -1
- package/dist/commands/auth/use.js +0 -15
- package/dist/commands/login-device.d.ts +0 -1
- package/dist/commands/login-device.js +0 -6
- package/dist/lib/login-server.d.ts +0 -1
- package/dist/lib/login-server.js +0 -43
- package/dist/lib/pkce.d.ts +0 -4
- package/dist/lib/pkce.js +0 -9
- package/dist/lib/prompt.d.ts +0 -2
- package/dist/lib/prompt.js +0 -36
package/dist/index.js
CHANGED
|
@@ -1,11 +1,6 @@
|
|
|
1
|
-
import { authAdd } from "./commands/auth/add.js";
|
|
2
|
-
import { authList } from "./commands/auth/list.js";
|
|
3
1
|
import { authRefresh } from "./commands/auth/refresh.js";
|
|
4
|
-
import { authRepair } from "./commands/auth/repair.js";
|
|
5
2
|
import { authRemove } from "./commands/auth/remove.js";
|
|
6
|
-
import { authShow } from "./commands/auth/show.js";
|
|
7
3
|
import { authStatus } from "./commands/auth/status.js";
|
|
8
|
-
import { authUse } from "./commands/auth/use.js";
|
|
9
4
|
import { doctor } from "./commands/doctor.js";
|
|
10
5
|
import { dataReload } from "./commands/data/reload.js";
|
|
11
6
|
import { dataValidate } from "./commands/data/validate.js";
|
|
@@ -20,7 +15,6 @@ import { localizationPull } from "./commands/localization/pull.js";
|
|
|
20
15
|
import { localizationPush } from "./commands/localization/push.js";
|
|
21
16
|
import { localizationValidate } from "./commands/localization/validate.js";
|
|
22
17
|
import { login } from "./commands/login.js";
|
|
23
|
-
import { loginDevice } from "./commands/login-device.js";
|
|
24
18
|
import { logout } from "./commands/logout.js";
|
|
25
19
|
import { newWeb } from "./commands/new/web.js";
|
|
26
20
|
import { createProject } from "./commands/projects/create.js";
|
|
@@ -40,18 +34,6 @@ try {
|
|
|
40
34
|
else if (!command || command === "help" || command === "--help" || command === "-h") {
|
|
41
35
|
printHelp();
|
|
42
36
|
}
|
|
43
|
-
else if (command === "auth:add" || (command === "auth" && subcommand === "add")) {
|
|
44
|
-
await authAdd(command === "auth" ? rest : [subcommand, ...rest].filter(Boolean));
|
|
45
|
-
}
|
|
46
|
-
else if (command === "auth:list" || (command === "auth" && subcommand === "list")) {
|
|
47
|
-
await authList(command === "auth" ? rest : [subcommand, ...rest].filter(Boolean));
|
|
48
|
-
}
|
|
49
|
-
else if (command === "auth:use" || (command === "auth" && subcommand === "use")) {
|
|
50
|
-
await authUse(command === "auth" ? rest : [subcommand, ...rest].filter(Boolean));
|
|
51
|
-
}
|
|
52
|
-
else if (command === "auth:show" || (command === "auth" && subcommand === "show")) {
|
|
53
|
-
await authShow(command === "auth" ? rest : [subcommand, ...rest].filter(Boolean));
|
|
54
|
-
}
|
|
55
37
|
else if (command === "auth:remove" || (command === "auth" && subcommand === "remove")) {
|
|
56
38
|
await authRemove(command === "auth" ? rest : [subcommand, ...rest].filter(Boolean));
|
|
57
39
|
}
|
|
@@ -61,27 +43,18 @@ try {
|
|
|
61
43
|
else if (command === "auth:refresh") {
|
|
62
44
|
await authRefresh([subcommand, ...rest].filter(Boolean));
|
|
63
45
|
}
|
|
64
|
-
else if (command === "auth:repair") {
|
|
65
|
-
await authRepair([subcommand, ...rest].filter(Boolean));
|
|
66
|
-
}
|
|
67
46
|
else if (command === "auth" && subcommand === "status") {
|
|
68
47
|
await authStatus(rest);
|
|
69
48
|
}
|
|
70
49
|
else if (command === "auth" && subcommand === "refresh") {
|
|
71
50
|
await authRefresh(rest);
|
|
72
51
|
}
|
|
73
|
-
else if (command === "auth" && subcommand === "repair") {
|
|
74
|
-
await authRepair(rest);
|
|
75
|
-
}
|
|
76
52
|
else if (command === "doctor") {
|
|
77
53
|
await doctor([subcommand, ...rest].filter(Boolean));
|
|
78
54
|
}
|
|
79
55
|
else if (command === "init") {
|
|
80
56
|
await init();
|
|
81
57
|
}
|
|
82
|
-
else if (command === "login:device") {
|
|
83
|
-
await loginDevice([subcommand, ...rest].filter(Boolean));
|
|
84
|
-
}
|
|
85
58
|
else if (command === "login") {
|
|
86
59
|
await login([subcommand, ...rest].filter(Boolean));
|
|
87
60
|
}
|
|
@@ -181,7 +154,7 @@ function toCliError(error) {
|
|
|
181
154
|
return { code: "not_logged_in", message, nextStep: "blocks-os login" };
|
|
182
155
|
}
|
|
183
156
|
if (message.includes("token expired") || message.includes("refresh token") || message.includes("cannot decrypt")) {
|
|
184
|
-
return { code: "auth_repair_required", message, nextStep: "blocks-os
|
|
157
|
+
return { code: "auth_repair_required", message, nextStep: "blocks-os login" };
|
|
185
158
|
}
|
|
186
159
|
if (message.includes("No project selected")) {
|
|
187
160
|
return { code: "project_not_selected", message, nextStep: "blocks-os use <tenantId>" };
|
|
@@ -192,152 +165,133 @@ function toCliError(error) {
|
|
|
192
165
|
return { code: "command_failed", message };
|
|
193
166
|
}
|
|
194
167
|
function printHelp() {
|
|
195
|
-
console.log(`Blocks OS CLI
|
|
196
|
-
|
|
197
|
-
Usage:
|
|
198
|
-
blocks-os <command> [options]
|
|
199
|
-
|
|
200
|
-
Global options:
|
|
201
|
-
--version Print CLI version.
|
|
202
|
-
--json Print machine-readable JSON where supported.
|
|
203
|
-
--api-url <url> Override Blocks API URL for this command.
|
|
204
|
-
--account <name> Use a named account profile; default is implicit.
|
|
205
|
-
--project <tenantId> Use a project tenant for project-scoped commands.
|
|
206
|
-
--dry-run Show planned mutation without calling the API.
|
|
207
|
-
--yes Skip mutation confirmation after explicit approval.
|
|
208
|
-
|
|
209
|
-
Setup and health:
|
|
210
|
-
blocks-os init
|
|
211
|
-
Create local Blocks workspace files: blocks.json, data schema/rules folders,
|
|
212
|
-
release deploy config, and .env.example.
|
|
213
|
-
|
|
214
|
-
blocks-os doctor [--json]
|
|
215
|
-
Check local Node.js, OIDC config, token cache, selected project, and config
|
|
216
|
-
file locations. Does not mutate cloud resources.
|
|
217
|
-
|
|
218
|
-
Auth:
|
|
219
|
-
blocks-os
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
blocks-os
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
blocks-os
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
blocks-os
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
blocks-os release:deploy --repo-id <repoId> [--dry-run] [--yes] [--json]
|
|
324
|
-
Trigger a manual Release build/deploy for a configured repository. Mutating;
|
|
325
|
-
no artifact upload is performed by this CLI.
|
|
326
|
-
|
|
327
|
-
blocks-os release:status <buildId> [--json]
|
|
328
|
-
Read Release build status by build id. Read-only.
|
|
329
|
-
|
|
330
|
-
blocks-os release:builds:list --repo-id <repoId> [--json]
|
|
331
|
-
List Release build details for a repository. Read-only.
|
|
332
|
-
|
|
333
|
-
blocks-os release:builds:get <buildId> [--json]
|
|
334
|
-
Alias for release:status. Read-only.
|
|
335
|
-
|
|
336
|
-
Scaffold:
|
|
337
|
-
blocks-os new web <name> --x-blocks-key <tenantId> --app-domain <domain> --blocks-api-url <url> [--client-id <oidcClientId>] [--oidc-url <url>]
|
|
338
|
-
Create a Vite React starter app with a real browser Authorization Code +
|
|
339
|
-
PKCE login flow against Blocks IAM (login page, /login/callback handler,
|
|
340
|
-
route guards), plus Data schema listing, Release build lookup, environment
|
|
341
|
-
config, and safe .gitignore defaults.
|
|
168
|
+
console.log(`Blocks OS CLI
|
|
169
|
+
|
|
170
|
+
Usage:
|
|
171
|
+
blocks-os <command> [options]
|
|
172
|
+
|
|
173
|
+
Global options:
|
|
174
|
+
--version Print CLI version.
|
|
175
|
+
--json Print machine-readable JSON where supported.
|
|
176
|
+
--api-url <url> Override Blocks API URL for this command.
|
|
177
|
+
--account <name> Use a named account profile; default is implicit.
|
|
178
|
+
--project <tenantId> Use a project tenant for project-scoped commands.
|
|
179
|
+
--dry-run Show planned mutation without calling the API.
|
|
180
|
+
--yes Skip mutation confirmation after explicit approval.
|
|
181
|
+
|
|
182
|
+
Setup and health:
|
|
183
|
+
blocks-os init
|
|
184
|
+
Create local Blocks workspace files: blocks.json, data schema/rules folders,
|
|
185
|
+
release deploy config, and .env.example.
|
|
186
|
+
|
|
187
|
+
blocks-os doctor [--json]
|
|
188
|
+
Check local Node.js, OIDC config, token cache, selected project, and config
|
|
189
|
+
file locations. Does not mutate cloud resources.
|
|
190
|
+
|
|
191
|
+
Auth:
|
|
192
|
+
blocks-os login
|
|
193
|
+
Device-code login. Prints a verification URL and user code, opens the
|
|
194
|
+
browser to the verification page when possible so you only need to click
|
|
195
|
+
approve, then polls until the device is authorized; stores account access
|
|
196
|
+
and refresh tokens and auto-refreshes later.
|
|
197
|
+
|
|
198
|
+
blocks-os auth:status [--json]
|
|
199
|
+
Show only whether account/project access and refresh tokens are missing,
|
|
200
|
+
valid, expired, or available. Does not print account config values.
|
|
201
|
+
|
|
202
|
+
blocks-os auth:refresh [--project] [--json]
|
|
203
|
+
Force account token refresh, or project token refresh with --project.
|
|
204
|
+
|
|
205
|
+
blocks-os auth:remove <account>
|
|
206
|
+
Clear cached tokens and stored local credentials for that account. The
|
|
207
|
+
packaged default OS account is restored from package defaults.
|
|
208
|
+
|
|
209
|
+
blocks-os logout
|
|
210
|
+
Revoke the current refresh token when possible and remove local session data.
|
|
211
|
+
|
|
212
|
+
Projects:
|
|
213
|
+
blocks-os projects:list [--json]
|
|
214
|
+
List accessible Blocks projects via /os/v4/Project/Gets using the account
|
|
215
|
+
token. Read-only.
|
|
216
|
+
|
|
217
|
+
blocks-os projects:get [tenantId] [--json]
|
|
218
|
+
Read one project from Project/Gets. Uses selected project when tenantId is
|
|
219
|
+
omitted. Read-only.
|
|
220
|
+
|
|
221
|
+
blocks-os projects:create <name> [--env dev] [--yes] [--dry-run] [--json]
|
|
222
|
+
Create a Blocks project/environment via /os/v4/Project/Create using the
|
|
223
|
+
account token. Mutating; supports dry-run and confirmation.
|
|
224
|
+
|
|
225
|
+
blocks-os use <project-tenant-id>
|
|
226
|
+
Save the selected project tenant globally and in blocks.json when present.
|
|
227
|
+
Does not call cloud APIs.
|
|
228
|
+
|
|
229
|
+
IAM:
|
|
230
|
+
blocks-os iam:me [--json]
|
|
231
|
+
Read the current user from IAM using the account token. This is the only IAM
|
|
232
|
+
admin surface exposed in the MVP.
|
|
233
|
+
|
|
234
|
+
Data:
|
|
235
|
+
blocks-os data:validate [--json]
|
|
236
|
+
Validate local blocks/data/schemas/*.json and blocks/data/rules.json before
|
|
237
|
+
pushing. Local-only.
|
|
238
|
+
|
|
239
|
+
blocks-os data:schema:list [--json]
|
|
240
|
+
List project schemas via /data/v4/schemas using an impersonated project
|
|
241
|
+
token. Read-only.
|
|
242
|
+
|
|
243
|
+
blocks-os data:schema:pull [--json]
|
|
244
|
+
Download project schemas into blocks/data/schemas/*.json. Writes local files
|
|
245
|
+
only.
|
|
246
|
+
|
|
247
|
+
blocks-os data:schema:push [--dry-run] [--yes] [--json]
|
|
248
|
+
Create or update project schemas via /data/v4/schemas/define. Mutating;
|
|
249
|
+
uses POST for create and PUT for update.
|
|
250
|
+
|
|
251
|
+
blocks-os data:rules:pull [--json]
|
|
252
|
+
Download data-access policies into blocks/data/rules.json. Writes local
|
|
253
|
+
files only.
|
|
254
|
+
|
|
255
|
+
blocks-os data:rules:deploy [--dry-run] [--yes] [--json]
|
|
256
|
+
Apply schema security and data-access policies. Mutating; supports dry-run
|
|
257
|
+
and confirmation.
|
|
258
|
+
|
|
259
|
+
blocks-os data:reload [--dry-run] [--yes] [--json]
|
|
260
|
+
Reload Data schema configuration so staged schema/rule changes become live.
|
|
261
|
+
Mutating; calls POST /data/v4/schema-configurations/reload.
|
|
262
|
+
|
|
263
|
+
Localization:
|
|
264
|
+
blocks-os localization:validate --module <name> --language <culture> [--file <path>] [--json]
|
|
265
|
+
Validate a local i18n JSON dictionary. Supports nested JSON input and
|
|
266
|
+
validates the flattened key/value set locally.
|
|
267
|
+
|
|
268
|
+
blocks-os localization:push --module <name> --language <culture> [--file <path>] [--route <route>] [--context <text>] [--dry-run] [--yes] [--json]
|
|
269
|
+
Create or update Localization keys from a local i18n JSON dictionary via
|
|
270
|
+
/localization/v4/Key/SaveKeys. Creates the module first when it is missing.
|
|
271
|
+
|
|
272
|
+
blocks-os localization:pull --module <name> --language <culture> [--out <path>] [--json]
|
|
273
|
+
Download published cloud localization via
|
|
274
|
+
/localization/v4/Key/GetCloudUilmFile and write a local JSON dictionary.
|
|
275
|
+
|
|
276
|
+
Release:
|
|
277
|
+
blocks-os release:deploy --repo-id <repoId> [--dry-run] [--yes] [--json]
|
|
278
|
+
Trigger a manual Release build/deploy for a configured repository. Mutating;
|
|
279
|
+
no artifact upload is performed by this CLI.
|
|
280
|
+
|
|
281
|
+
blocks-os release:status <buildId> [--json]
|
|
282
|
+
Read Release build status by build id. Read-only.
|
|
283
|
+
|
|
284
|
+
blocks-os release:builds:list --repo-id <repoId> [--json]
|
|
285
|
+
List Release build details for a repository. Read-only.
|
|
286
|
+
|
|
287
|
+
blocks-os release:builds:get <buildId> [--json]
|
|
288
|
+
Alias for release:status. Read-only.
|
|
289
|
+
|
|
290
|
+
Scaffold:
|
|
291
|
+
blocks-os new web <name> --x-blocks-key <tenantId> --app-domain <domain> [--client-id <oidcClientId>]
|
|
292
|
+
Create a Vite React starter app with a real browser Authorization Code +
|
|
293
|
+
PKCE login flow against Blocks IAM (login page, /login/callback handler,
|
|
294
|
+
route guards), plus Data schema listing, Release build lookup, environment
|
|
295
|
+
config, and safe .gitignore defaults.
|
|
342
296
|
`);
|
|
343
297
|
}
|
package/dist/lib/auth.d.ts
CHANGED
|
@@ -20,18 +20,11 @@ export type DeviceAuthorizationResponse = {
|
|
|
20
20
|
verification_uri: string;
|
|
21
21
|
verification_uri_complete?: string;
|
|
22
22
|
};
|
|
23
|
-
export
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
clientSecret?: string;
|
|
27
|
-
code: string;
|
|
28
|
-
codeVerifier?: string;
|
|
29
|
-
oidcUrl: string;
|
|
30
|
-
redirectUri: string;
|
|
31
|
-
rootTenantId?: string;
|
|
32
|
-
}): Promise<TokenResponse>;
|
|
23
|
+
export type DevicePollingOptions = {
|
|
24
|
+
onWait?: (seconds: number) => void;
|
|
25
|
+
};
|
|
33
26
|
export declare function requestDeviceAuthorization(profile: AccountProfile): Promise<DeviceAuthorizationResponse>;
|
|
34
|
-
export declare function pollDeviceToken(profile: AccountProfile, device: DeviceAuthorizationResponse): Promise<TokenResponse>;
|
|
27
|
+
export declare function pollDeviceToken(profile: AccountProfile, device: DeviceAuthorizationResponse, options?: DevicePollingOptions): Promise<TokenResponse>;
|
|
35
28
|
export declare function getAccountSession(accountOverride?: string): Promise<AccountSession>;
|
|
36
29
|
export declare function selectProject(tenantId: string): Promise<void>;
|
|
37
30
|
export declare function getImpersonatedProjectSession(accountOverride?: string, tenantOverride?: string): Promise<ProjectSession>;
|
package/dist/lib/auth.js
CHANGED
|
@@ -5,18 +5,7 @@ import { getClientSecret } from "./secret-store.js";
|
|
|
5
5
|
import { CliActionableError } from "./errors.js";
|
|
6
6
|
const DEVICE_GRANT = "urn:ietf:params:oauth:grant-type:device_code";
|
|
7
7
|
const FALLBACK_IMPERSONATION_CLIENT_ID = "57214b67-aa9c-4307-92ab-a25e35180fac";
|
|
8
|
-
|
|
9
|
-
const body = new URLSearchParams({
|
|
10
|
-
client_id: args.clientId,
|
|
11
|
-
code: args.code,
|
|
12
|
-
grant_type: "authorization_code",
|
|
13
|
-
redirect_uri: args.redirectUri
|
|
14
|
-
});
|
|
15
|
-
if (args.codeVerifier)
|
|
16
|
-
body.set("code_verifier", args.codeVerifier);
|
|
17
|
-
applyClientSecret(body, args.clientSecret);
|
|
18
|
-
return postFormToken(args.oidcUrl, body, args.rootTenantId);
|
|
19
|
-
}
|
|
8
|
+
const MAX_CONSECUTIVE_TRANSIENT_ERRORS = 3;
|
|
20
9
|
export async function requestDeviceAuthorization(profile) {
|
|
21
10
|
const rootTenantId = await resolveRootTenantForDevice(profile);
|
|
22
11
|
if (!rootTenantId) {
|
|
@@ -45,34 +34,70 @@ export async function requestDeviceAuthorization(profile) {
|
|
|
45
34
|
}
|
|
46
35
|
return data;
|
|
47
36
|
}
|
|
48
|
-
export async function pollDeviceToken(profile, device) {
|
|
37
|
+
export async function pollDeviceToken(profile, device, options = {}) {
|
|
49
38
|
const rootTenantId = await resolveRootTenantForDevice(profile);
|
|
50
39
|
if (!rootTenantId) {
|
|
51
40
|
throw new Error("Device token polling requires rootTenantId in the account profile.");
|
|
52
41
|
}
|
|
53
42
|
let intervalSeconds = Math.max(device.interval ?? 5, 1);
|
|
54
43
|
const deadline = Date.now() + device.expires_in * 1000;
|
|
44
|
+
let consecutiveTransientErrors = 0;
|
|
55
45
|
while (Date.now() < deadline) {
|
|
56
|
-
|
|
46
|
+
options.onWait?.(intervalSeconds);
|
|
47
|
+
await delay(Math.min(intervalSeconds * 1000, Math.max(deadline - Date.now(), 0)));
|
|
57
48
|
const body = new URLSearchParams({
|
|
58
49
|
client_id: profile.clientId,
|
|
59
50
|
device_code: device.device_code,
|
|
60
51
|
grant_type: DEVICE_GRANT
|
|
61
52
|
});
|
|
62
53
|
applyClientSecret(body, await getSecretForProfile(profile));
|
|
63
|
-
|
|
64
|
-
|
|
54
|
+
let response;
|
|
55
|
+
try {
|
|
56
|
+
response = await postFormToken(profile.oidcUrl, body, rootTenantId, false);
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
// The token endpoint itself never throws (postFormToken with
|
|
60
|
+
// throwOnOAuthError=false only returns error payloads) -- a thrown
|
|
61
|
+
// error here means fetch() failed before any response came back
|
|
62
|
+
// (DNS, connection reset, etc.). Treat it as transient and keep
|
|
63
|
+
// polling rather than aborting a multi-minute wait on one blip.
|
|
64
|
+
throwIfTooManyTransientErrors(++consecutiveTransientErrors, error);
|
|
65
65
|
continue;
|
|
66
|
+
}
|
|
67
|
+
if (response.error === "authorization_pending") {
|
|
68
|
+
consecutiveTransientErrors = 0;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
66
71
|
if (response.error === "slow_down") {
|
|
72
|
+
consecutiveTransientErrors = 0;
|
|
67
73
|
intervalSeconds += 5;
|
|
68
74
|
continue;
|
|
69
75
|
}
|
|
76
|
+
if (response.error === "token_request_failed") {
|
|
77
|
+
// Synthetic error from postFormToken for a non-JSON HTTP failure
|
|
78
|
+
// (e.g. a 502 from an upstream proxy) -- not a real OAuth rejection,
|
|
79
|
+
// so treat it the same as a network blip.
|
|
80
|
+
throwIfTooManyTransientErrors(++consecutiveTransientErrors, response.error_description ?? response.error);
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (response.error === "access_denied") {
|
|
84
|
+
throw new CliActionableError("Device authorization was denied.", "device_login_denied", "blocks-os login");
|
|
85
|
+
}
|
|
86
|
+
if (response.error === "expired_token") {
|
|
87
|
+
throw new CliActionableError("Device login expired before approval.", "device_login_expired", "blocks-os login");
|
|
88
|
+
}
|
|
70
89
|
if (response.error) {
|
|
71
|
-
throw new
|
|
90
|
+
throw new CliActionableError(response.error_description ?? response.error, "device_login_failed", "blocks-os login");
|
|
72
91
|
}
|
|
73
92
|
return response;
|
|
74
93
|
}
|
|
75
|
-
throw new
|
|
94
|
+
throw new CliActionableError("Device login expired before approval.", "device_login_expired", "blocks-os login");
|
|
95
|
+
}
|
|
96
|
+
function throwIfTooManyTransientErrors(count, cause) {
|
|
97
|
+
if (count <= MAX_CONSECUTIVE_TRANSIENT_ERRORS)
|
|
98
|
+
return;
|
|
99
|
+
const detail = cause instanceof Error ? cause.message : String(cause);
|
|
100
|
+
throw new CliActionableError(`Could not reach the identity provider while waiting for device approval (${detail}).`, "device_login_network_error", "Check your network connection and run 'blocks-os login' again.");
|
|
76
101
|
}
|
|
77
102
|
export async function getAccountSession(accountOverride) {
|
|
78
103
|
let config = await readConfig();
|
package/dist/lib/config.d.ts
CHANGED
|
@@ -4,7 +4,6 @@ export type AccountProfile = {
|
|
|
4
4
|
createdAt: string;
|
|
5
5
|
oidcUrl: string;
|
|
6
6
|
osUrl: string;
|
|
7
|
-
redirectUri: string;
|
|
8
7
|
rootTenantId?: string;
|
|
9
8
|
scope: string;
|
|
10
9
|
updatedAt: string;
|
|
@@ -15,6 +14,7 @@ export type TokenSet = {
|
|
|
15
14
|
expiresAt?: string;
|
|
16
15
|
idToken?: string;
|
|
17
16
|
refreshToken?: string;
|
|
17
|
+
refreshTokenExpiresAt?: string;
|
|
18
18
|
scope?: string;
|
|
19
19
|
tokenType?: string;
|
|
20
20
|
};
|
|
@@ -30,9 +30,9 @@ export type BlocksCliConfig = {
|
|
|
30
30
|
};
|
|
31
31
|
export declare function defaults(): {
|
|
32
32
|
apiUrl: string;
|
|
33
|
+
osClientId: string;
|
|
33
34
|
oidcUrl: string;
|
|
34
35
|
osUrl: string;
|
|
35
|
-
redirectUri: string;
|
|
36
36
|
rootTenantId: string;
|
|
37
37
|
scope: string;
|
|
38
38
|
};
|
package/dist/lib/config.js
CHANGED
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
2
2
|
import { homedir, platform } from "node:os";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
|
-
const
|
|
4
|
+
const BAD_GATEWAY_OS_URL = "https://api.seliseblocks.com/os/v4";
|
|
5
|
+
const DEFAULT_API_URL = "https://os.seliseblocks.com";
|
|
6
|
+
const DEFAULT_OS_CLIENT_ID = "4a633b13-1108-4fbf-84fd-b196c9dcdee2";
|
|
5
7
|
const DEFAULT_OIDC_URL = "https://iam.seliseblocks.com";
|
|
6
8
|
const DEFAULT_OS_URL = "https://os.seliseblocks.com";
|
|
7
|
-
const
|
|
9
|
+
const DEFAULT_PROFILE_TIMESTAMP = "2026-01-01T00:00:00.000Z";
|
|
8
10
|
const DEFAULT_ROOT_TENANT_ID = "d7e5554c758541db8a18694b64ef423d";
|
|
9
11
|
const DEFAULT_SCOPE = "openid profile offline_access";
|
|
10
|
-
const DEFAULT_REDIRECT_URI = "http://127.0.0.1:8976/callback";
|
|
11
12
|
export function defaults() {
|
|
12
13
|
return {
|
|
13
14
|
apiUrl: DEFAULT_API_URL,
|
|
15
|
+
osClientId: DEFAULT_OS_CLIENT_ID,
|
|
14
16
|
oidcUrl: DEFAULT_OIDC_URL,
|
|
15
17
|
osUrl: DEFAULT_OS_URL,
|
|
16
|
-
redirectUri: DEFAULT_REDIRECT_URI,
|
|
17
18
|
rootTenantId: DEFAULT_ROOT_TENANT_ID,
|
|
18
19
|
scope: DEFAULT_SCOPE
|
|
19
20
|
};
|
|
@@ -40,9 +41,7 @@ export async function readConfig() {
|
|
|
40
41
|
}
|
|
41
42
|
catch (error) {
|
|
42
43
|
if (error.code === "ENOENT") {
|
|
43
|
-
return
|
|
44
|
-
accounts: {}
|
|
45
|
-
};
|
|
44
|
+
return defaultConfig();
|
|
46
45
|
}
|
|
47
46
|
throw error;
|
|
48
47
|
}
|
|
@@ -61,7 +60,7 @@ export function normalizeAccountName(account) {
|
|
|
61
60
|
export function getActiveAccountName(config, override) {
|
|
62
61
|
const account = normalizeAccountName(override ?? config.activeAccount);
|
|
63
62
|
if (!config.accounts[account]) {
|
|
64
|
-
throw new Error("OIDC account is not configured.
|
|
63
|
+
throw new Error("OIDC account is not configured.");
|
|
65
64
|
}
|
|
66
65
|
return account;
|
|
67
66
|
}
|
|
@@ -73,21 +72,54 @@ export function getAccountProfile(config, account) {
|
|
|
73
72
|
};
|
|
74
73
|
}
|
|
75
74
|
function normalizeConfig(config) {
|
|
75
|
+
const env = defaults();
|
|
76
76
|
const accounts = {};
|
|
77
77
|
for (const [name, profile] of Object.entries(config.accounts ?? {})) {
|
|
78
|
-
accounts[name] = {
|
|
78
|
+
accounts[name] = name === "default" ? defaultProfile(profile) : {
|
|
79
79
|
...profile,
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
80
|
+
apiUrl: profile.apiUrl ?? env.apiUrl,
|
|
81
|
+
clientId: profile.clientId ?? env.osClientId,
|
|
82
|
+
createdAt: profile.createdAt ?? DEFAULT_PROFILE_TIMESTAMP,
|
|
83
|
+
oidcUrl: profile.oidcUrl ?? env.oidcUrl,
|
|
84
|
+
osUrl: !profile.osUrl || profile.osUrl === BAD_GATEWAY_OS_URL ? env.osUrl : profile.osUrl,
|
|
85
|
+
rootTenantId: profile.rootTenantId ?? env.rootTenantId,
|
|
86
|
+
scope: profile.scope ?? env.scope,
|
|
87
|
+
updatedAt: profile.updatedAt ?? DEFAULT_PROFILE_TIMESTAMP
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
if (Object.keys(accounts).length === 0) {
|
|
91
|
+
return {
|
|
92
|
+
...defaultConfig(),
|
|
93
|
+
selectedProject: config.selectedProject
|
|
83
94
|
};
|
|
84
95
|
}
|
|
85
96
|
return {
|
|
86
|
-
activeAccount: config.activeAccount,
|
|
97
|
+
activeAccount: config.activeAccount ?? "default",
|
|
87
98
|
accounts,
|
|
88
99
|
selectedProject: config.selectedProject
|
|
89
100
|
};
|
|
90
101
|
}
|
|
102
|
+
function defaultConfig() {
|
|
103
|
+
return {
|
|
104
|
+
activeAccount: "default",
|
|
105
|
+
accounts: {
|
|
106
|
+
default: defaultProfile()
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
function defaultProfile(existing) {
|
|
111
|
+
const env = defaults();
|
|
112
|
+
return {
|
|
113
|
+
apiUrl: env.apiUrl,
|
|
114
|
+
clientId: env.osClientId,
|
|
115
|
+
createdAt: existing?.createdAt ?? DEFAULT_PROFILE_TIMESTAMP,
|
|
116
|
+
oidcUrl: env.oidcUrl,
|
|
117
|
+
osUrl: env.osUrl,
|
|
118
|
+
rootTenantId: env.rootTenantId,
|
|
119
|
+
scope: env.scope,
|
|
120
|
+
updatedAt: DEFAULT_PROFILE_TIMESTAMP
|
|
121
|
+
};
|
|
122
|
+
}
|
|
91
123
|
function nonEmptyEnv(name) {
|
|
92
124
|
const value = process.env[name]?.trim();
|
|
93
125
|
return value || undefined;
|
package/dist/lib/open-browser.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { platform } from "node:os";
|
|
3
|
+
const LAUNCH_GRACE_PERIOD_MS = 500;
|
|
3
4
|
export async function openBrowser(url) {
|
|
4
5
|
const os = platform();
|
|
5
6
|
const command = os === "win32" ? "rundll32" : os === "darwin" ? "open" : "xdg-open";
|
|
@@ -10,10 +11,32 @@ export async function openBrowser(url) {
|
|
|
10
11
|
stdio: "ignore",
|
|
11
12
|
windowsHide: true
|
|
12
13
|
});
|
|
13
|
-
|
|
14
|
-
|
|
14
|
+
let settled = false;
|
|
15
|
+
const settle = (result) => {
|
|
16
|
+
if (settled)
|
|
17
|
+
return;
|
|
18
|
+
settled = true;
|
|
19
|
+
resolve(result);
|
|
20
|
+
};
|
|
21
|
+
child.once("error", () => settle(false));
|
|
22
|
+
// A launcher like xdg-open can spawn successfully and still exit
|
|
23
|
+
// non-zero almost immediately when no browser/display session is
|
|
24
|
+
// available (headless SSH, missing DISPLAY, etc.). Give it a short
|
|
25
|
+
// grace period to fail fast before treating the launch as a success
|
|
26
|
+
// and detaching the process.
|
|
27
|
+
const grace = setTimeout(() => {
|
|
15
28
|
child.unref();
|
|
16
|
-
|
|
29
|
+
settle(true);
|
|
30
|
+
}, LAUNCH_GRACE_PERIOD_MS);
|
|
31
|
+
child.once("exit", (code) => {
|
|
32
|
+
clearTimeout(grace);
|
|
33
|
+
if (code === 0 || code === null) {
|
|
34
|
+
child.unref();
|
|
35
|
+
settle(true);
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
settle(false);
|
|
39
|
+
}
|
|
17
40
|
});
|
|
18
41
|
});
|
|
19
42
|
}
|