@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.
Files changed (41) hide show
  1. package/AI_USAGE_GUIDE.md +285 -305
  2. package/LICENSE +21 -21
  3. package/README.md +136 -159
  4. package/bin/run.js +2 -2
  5. package/dist/commands/auth/status.js +16 -51
  6. package/dist/commands/doctor.js +11 -7
  7. package/dist/commands/init.js +4 -2
  8. package/dist/commands/login.d.ts +0 -1
  9. package/dist/commands/login.js +15 -55
  10. package/dist/commands/new/web.js +1 -1
  11. package/dist/index.js +129 -175
  12. package/dist/lib/auth.d.ts +4 -11
  13. package/dist/lib/auth.js +43 -18
  14. package/dist/lib/config.d.ts +2 -2
  15. package/dist/lib/config.js +45 -13
  16. package/dist/lib/open-browser.js +26 -3
  17. package/dist/lib/secret-store.d.ts +0 -1
  18. package/dist/lib/secret-store.js +1 -4
  19. package/dist/lib/token-store.d.ts +0 -1
  20. package/dist/lib/token-store.js +1 -5
  21. package/dist/lib/token.d.ts +3 -1
  22. package/dist/lib/token.js +34 -7
  23. package/package.json +47 -47
  24. package/dist/commands/auth/add.d.ts +0 -1
  25. package/dist/commands/auth/add.js +0 -51
  26. package/dist/commands/auth/list.d.ts +0 -1
  27. package/dist/commands/auth/list.js +0 -33
  28. package/dist/commands/auth/repair.d.ts +0 -1
  29. package/dist/commands/auth/repair.js +0 -39
  30. package/dist/commands/auth/show.d.ts +0 -1
  31. package/dist/commands/auth/show.js +0 -23
  32. package/dist/commands/auth/use.d.ts +0 -1
  33. package/dist/commands/auth/use.js +0 -15
  34. package/dist/commands/login-device.d.ts +0 -1
  35. package/dist/commands/login-device.js +0 -6
  36. package/dist/lib/login-server.d.ts +0 -1
  37. package/dist/lib/login-server.js +0 -43
  38. package/dist/lib/pkce.d.ts +0 -4
  39. package/dist/lib/pkce.js +0 -9
  40. package/dist/lib/prompt.d.ts +0 -2
  41. 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 auth:repair --yes && blocks-os auth:add --client-id <id> --client-secret <secret> && blocks-os login" };
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 auth:add [account] --client-id <id> --client-secret <secret>
220
- Save OIDC client settings for the CLI. Defaults target production Blocks
221
- URLs, root tenant, scope, and localhost callback. Secret is stored separately.
222
-
223
- blocks-os login
224
- Browser Authorization Code + PKCE login. Opens IAM, captures the localhost
225
- callback, stores account access and refresh tokens, and auto-refreshes later.
226
-
227
- blocks-os login:device
228
- Device-code login for terminals or agents where a browser callback is not
229
- convenient. Uses the same saved account profile.
230
-
231
- blocks-os auth:status [--json]
232
- Show configured account, token freshness, refresh-token availability, and
233
- selected project without printing secrets.
234
-
235
- blocks-os auth:refresh [--project] [--json]
236
- Force account token refresh, or project token refresh with --project.
237
-
238
- blocks-os auth:repair [account] [--dry-run] [--yes] [--json]
239
- Clear broken local secure-store auth state while keeping account profile
240
- config. Use after OS credential migration, stale DPAPI, or corrupted tokens.
241
-
242
- blocks-os auth:list [--json]
243
- List saved account profiles without showing secrets.
244
-
245
- blocks-os auth:show [account]
246
- Print one account profile with secret redacted as configured/missing.
247
-
248
- blocks-os auth:use <account>
249
- Switch the active named account profile.
250
-
251
- blocks-os auth:remove <account>
252
- Remove account config, cached tokens, and stored client secret for that
253
- account.
254
-
255
- blocks-os logout
256
- Revoke the current refresh token when possible and remove local session data.
257
-
258
- Projects:
259
- blocks-os projects:list [--json]
260
- List accessible Blocks projects via /os/v4/Project/Gets using the account
261
- token. Read-only.
262
-
263
- blocks-os projects:get [tenantId] [--json]
264
- Read one project from Project/Gets. Uses selected project when tenantId is
265
- omitted. Read-only.
266
-
267
- blocks-os projects:create <name> [--env dev] [--yes] [--dry-run] [--json]
268
- Create a Blocks project/environment via /os/v4/Project/Create using the
269
- account token. Mutating; supports dry-run and confirmation.
270
-
271
- blocks-os use <project-tenant-id>
272
- Save the selected project tenant globally and in blocks.json when present.
273
- Does not call cloud APIs.
274
-
275
- IAM:
276
- blocks-os iam:me [--json]
277
- Read the current user from IAM using the account token. This is the only IAM
278
- admin surface exposed in the MVP.
279
-
280
- Data:
281
- blocks-os data:validate [--json]
282
- Validate local blocks/data/schemas/*.json and blocks/data/rules.json before
283
- pushing. Local-only.
284
-
285
- blocks-os data:schema:list [--json]
286
- List project schemas via /data/v4/schemas using an impersonated project
287
- token. Read-only.
288
-
289
- blocks-os data:schema:pull [--json]
290
- Download project schemas into blocks/data/schemas/*.json. Writes local files
291
- only.
292
-
293
- blocks-os data:schema:push [--dry-run] [--yes] [--json]
294
- Create or update project schemas via /data/v4/schemas/define. Mutating;
295
- uses POST for create and PUT for update.
296
-
297
- blocks-os data:rules:pull [--json]
298
- Download data-access policies into blocks/data/rules.json. Writes local
299
- files only.
300
-
301
- blocks-os data:rules:deploy [--dry-run] [--yes] [--json]
302
- Apply schema security and data-access policies. Mutating; supports dry-run
303
- and confirmation.
304
-
305
- blocks-os data:reload [--dry-run] [--yes] [--json]
306
- Reload Data schema configuration so staged schema/rule changes become live.
307
- Mutating; calls POST /data/v4/schema-configurations/reload.
308
-
309
- Localization:
310
- blocks-os localization:validate --module <name> --language <culture> [--file <path>] [--json]
311
- Validate a local i18n JSON dictionary. Supports nested JSON input and
312
- validates the flattened key/value set locally.
313
-
314
- blocks-os localization:push --module <name> --language <culture> [--file <path>] [--route <route>] [--context <text>] [--dry-run] [--yes] [--json]
315
- Create or update Localization keys from a local i18n JSON dictionary via
316
- /localization/v4/Key/SaveKeys. Creates the module first when it is missing.
317
-
318
- blocks-os localization:pull --module <name> --language <culture> [--out <path>] [--json]
319
- Download published cloud localization via
320
- /localization/v4/Key/GetCloudUilmFile and write a local JSON dictionary.
321
-
322
- Release:
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
  }
@@ -20,18 +20,11 @@ export type DeviceAuthorizationResponse = {
20
20
  verification_uri: string;
21
21
  verification_uri_complete?: string;
22
22
  };
23
- export declare function exchangeAuthorizationCode(args: {
24
- account: string;
25
- clientId: string;
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
- export async function exchangeAuthorizationCode(args) {
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
- await delay(intervalSeconds * 1000);
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
- const response = await postFormToken(profile.oidcUrl, body, rootTenantId, false);
64
- if (response.error === "authorization_pending")
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 Error(response.error_description ?? response.error);
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 Error("Device login expired before approval.");
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();
@@ -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
  };
@@ -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 DEFAULT_API_URL = "https://api.seliseblocks.com";
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 BAD_GATEWAY_OS_URL = "https://api.seliseblocks.com/os/v4";
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. Run 'blocks-os auth:add --client-id <id> --client-secret <secret>' first.");
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
- oidcUrl: profile.oidcUrl ?? DEFAULT_OIDC_URL,
81
- osUrl: !profile.osUrl || profile.osUrl === BAD_GATEWAY_OS_URL ? DEFAULT_OS_URL : profile.osUrl,
82
- rootTenantId: profile.rootTenantId ?? DEFAULT_ROOT_TENANT_ID
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;
@@ -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
- child.once("error", () => resolve(false));
14
- child.once("spawn", () => {
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
- resolve(true);
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
  }