@seliseblocks/cli-os 0.1.2 → 0.1.4
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 +18 -38
- package/README.md +13 -36
- package/dist/commands/auth/status.js +16 -55
- package/dist/commands/deselect.d.ts +1 -0
- package/dist/commands/deselect.js +31 -0
- package/dist/commands/doctor.js +3 -4
- 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/commands/release/builds/list.js +4 -2
- package/dist/commands/release/deploy.js +5 -3
- package/dist/commands/release/status.js +4 -2
- package/dist/index.js +32 -60
- package/dist/lib/auth.d.ts +4 -11
- package/dist/lib/auth.js +43 -18
- package/dist/lib/config.d.ts +1 -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 +0 -1
- package/dist/lib/token.js +0 -4
- package/dist/lib/workspace.d.ts +1 -0
- package/dist/lib/workspace.js +17 -0
- package/package.json +1 -1
- 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,7 @@
|
|
|
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 {
|
|
4
|
+
import { deselectProject } from "./commands/deselect.js";
|
|
9
5
|
import { doctor } from "./commands/doctor.js";
|
|
10
6
|
import { dataReload } from "./commands/data/reload.js";
|
|
11
7
|
import { dataValidate } from "./commands/data/validate.js";
|
|
@@ -20,7 +16,6 @@ import { localizationPull } from "./commands/localization/pull.js";
|
|
|
20
16
|
import { localizationPush } from "./commands/localization/push.js";
|
|
21
17
|
import { localizationValidate } from "./commands/localization/validate.js";
|
|
22
18
|
import { login } from "./commands/login.js";
|
|
23
|
-
import { loginDevice } from "./commands/login-device.js";
|
|
24
19
|
import { logout } from "./commands/logout.js";
|
|
25
20
|
import { newWeb } from "./commands/new/web.js";
|
|
26
21
|
import { createProject } from "./commands/projects/create.js";
|
|
@@ -40,18 +35,6 @@ try {
|
|
|
40
35
|
else if (!command || command === "help" || command === "--help" || command === "-h") {
|
|
41
36
|
printHelp();
|
|
42
37
|
}
|
|
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
38
|
else if (command === "auth:remove" || (command === "auth" && subcommand === "remove")) {
|
|
56
39
|
await authRemove(command === "auth" ? rest : [subcommand, ...rest].filter(Boolean));
|
|
57
40
|
}
|
|
@@ -61,27 +44,18 @@ try {
|
|
|
61
44
|
else if (command === "auth:refresh") {
|
|
62
45
|
await authRefresh([subcommand, ...rest].filter(Boolean));
|
|
63
46
|
}
|
|
64
|
-
else if (command === "auth:repair") {
|
|
65
|
-
await authRepair([subcommand, ...rest].filter(Boolean));
|
|
66
|
-
}
|
|
67
47
|
else if (command === "auth" && subcommand === "status") {
|
|
68
48
|
await authStatus(rest);
|
|
69
49
|
}
|
|
70
50
|
else if (command === "auth" && subcommand === "refresh") {
|
|
71
51
|
await authRefresh(rest);
|
|
72
52
|
}
|
|
73
|
-
else if (command === "auth" && subcommand === "repair") {
|
|
74
|
-
await authRepair(rest);
|
|
75
|
-
}
|
|
76
53
|
else if (command === "doctor") {
|
|
77
54
|
await doctor([subcommand, ...rest].filter(Boolean));
|
|
78
55
|
}
|
|
79
56
|
else if (command === "init") {
|
|
80
57
|
await init();
|
|
81
58
|
}
|
|
82
|
-
else if (command === "login:device") {
|
|
83
|
-
await loginDevice([subcommand, ...rest].filter(Boolean));
|
|
84
|
-
}
|
|
85
59
|
else if (command === "login") {
|
|
86
60
|
await login([subcommand, ...rest].filter(Boolean));
|
|
87
61
|
}
|
|
@@ -106,6 +80,9 @@ try {
|
|
|
106
80
|
else if (command === "use") {
|
|
107
81
|
await useProject([subcommand, ...rest].filter(Boolean));
|
|
108
82
|
}
|
|
83
|
+
else if (command === "deselect") {
|
|
84
|
+
await deselectProject([subcommand, ...rest].filter(Boolean));
|
|
85
|
+
}
|
|
109
86
|
else if (command === "new" && subcommand === "web") {
|
|
110
87
|
await newWeb(rest);
|
|
111
88
|
}
|
|
@@ -181,7 +158,7 @@ function toCliError(error) {
|
|
|
181
158
|
return { code: "not_logged_in", message, nextStep: "blocks-os login" };
|
|
182
159
|
}
|
|
183
160
|
if (message.includes("token expired") || message.includes("refresh token") || message.includes("cannot decrypt")) {
|
|
184
|
-
return { code: "auth_repair_required", message, nextStep: "blocks-os
|
|
161
|
+
return { code: "auth_repair_required", message, nextStep: "blocks-os login" };
|
|
185
162
|
}
|
|
186
163
|
if (message.includes("No project selected")) {
|
|
187
164
|
return { code: "project_not_selected", message, nextStep: "blocks-os use <tenantId>" };
|
|
@@ -216,41 +193,22 @@ Setup and health:
|
|
|
216
193
|
file locations. Does not mutate cloud resources.
|
|
217
194
|
|
|
218
195
|
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
196
|
blocks-os login
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
Device-code login for terminals or agents where a browser callback is not
|
|
229
|
-
convenient. Uses the same saved account profile.
|
|
197
|
+
Device-code login. Prints a verification URL and user code, opens the
|
|
198
|
+
browser to the verification page when possible so you only need to click
|
|
199
|
+
approve, then polls until the device is authorized; stores account access
|
|
200
|
+
and refresh tokens and auto-refreshes later.
|
|
230
201
|
|
|
231
202
|
blocks-os auth:status [--json]
|
|
232
|
-
Show
|
|
233
|
-
|
|
203
|
+
Show only whether account/project access and refresh tokens are missing,
|
|
204
|
+
valid, expired, or available. Does not print account config values.
|
|
234
205
|
|
|
235
206
|
blocks-os auth:refresh [--project] [--json]
|
|
236
207
|
Force account token refresh, or project token refresh with --project.
|
|
237
208
|
|
|
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
209
|
blocks-os auth:remove <account>
|
|
252
|
-
|
|
253
|
-
account.
|
|
210
|
+
Clear cached tokens and stored local credentials for that account. The
|
|
211
|
+
packaged default OS account is restored from package defaults.
|
|
254
212
|
|
|
255
213
|
blocks-os logout
|
|
256
214
|
Revoke the current refresh token when possible and remove local session data.
|
|
@@ -272,6 +230,12 @@ Projects:
|
|
|
272
230
|
Save the selected project tenant globally and in blocks.json when present.
|
|
273
231
|
Does not call cloud APIs.
|
|
274
232
|
|
|
233
|
+
blocks-os deselect
|
|
234
|
+
Clear the selected project tenant (globally and in blocks.json) and drop
|
|
235
|
+
its cached impersonation token. Use this to recover when an impersonated
|
|
236
|
+
project token has expired or failed, then run 'blocks-os use <tenantId>'
|
|
237
|
+
again to reselect and re-impersonate.
|
|
238
|
+
|
|
275
239
|
IAM:
|
|
276
240
|
blocks-os iam:me [--json]
|
|
277
241
|
Read the current user from IAM using the account token. This is the only IAM
|
|
@@ -321,23 +285,31 @@ Localization:
|
|
|
321
285
|
|
|
322
286
|
Release:
|
|
323
287
|
blocks-os release:deploy --repo-id <repoId> [--dry-run] [--yes] [--json]
|
|
324
|
-
Trigger a manual Release build/deploy for a configured repository
|
|
325
|
-
no artifact upload is performed by
|
|
288
|
+
Trigger a manual Release build/deploy for a configured repository using an
|
|
289
|
+
impersonated project token. Mutating; no artifact upload is performed by
|
|
290
|
+
this CLI.
|
|
326
291
|
|
|
327
292
|
blocks-os release:status <buildId> [--json]
|
|
328
|
-
Read Release build status by build id
|
|
293
|
+
Read Release build status by build id using an impersonated project
|
|
294
|
+
token. Read-only.
|
|
329
295
|
|
|
330
296
|
blocks-os release:builds:list --repo-id <repoId> [--json]
|
|
331
|
-
List Release build details for a repository
|
|
297
|
+
List Release build details for a repository using an impersonated project
|
|
298
|
+
token. Read-only.
|
|
332
299
|
|
|
333
300
|
blocks-os release:builds:get <buildId> [--json]
|
|
334
301
|
Alias for release:status. Read-only.
|
|
335
302
|
|
|
336
303
|
Scaffold:
|
|
337
|
-
blocks-os new web <name> --x-blocks-key <tenantId> --app-domain <domain>
|
|
304
|
+
blocks-os new web <name> --x-blocks-key <tenantId> --app-domain <domain> [--client-id <oidcClientId>]
|
|
305
|
+
[--blocks-api-url <url>] [--oidc-url <url>]
|
|
338
306
|
Create a Vite React starter app with a real browser Authorization Code +
|
|
339
307
|
PKCE login flow against Blocks IAM (login page, /login/callback handler,
|
|
340
308
|
route guards), plus Data schema listing, Release build lookup, environment
|
|
341
309
|
config, and safe .gitignore defaults.
|
|
310
|
+
--blocks-api-url defaults to the OS API (os.seliseblocks.com) if omitted -
|
|
311
|
+
pass the runtime Data/IAM/Localization gateway URL explicitly (typically
|
|
312
|
+
https://api.seliseblocks.com) for the scaffolded app to work at runtime.
|
|
313
|
+
--oidc-url defaults to https://iam.seliseblocks.com.
|
|
342
314
|
`);
|
|
343
315
|
}
|
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;
|
|
@@ -31,9 +30,9 @@ export type BlocksCliConfig = {
|
|
|
31
30
|
};
|
|
32
31
|
export declare function defaults(): {
|
|
33
32
|
apiUrl: string;
|
|
33
|
+
osClientId: string;
|
|
34
34
|
oidcUrl: string;
|
|
35
35
|
osUrl: string;
|
|
36
|
-
redirectUri: string;
|
|
37
36
|
rootTenantId: string;
|
|
38
37
|
scope: string;
|
|
39
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
|
}
|
|
@@ -17,7 +17,6 @@ export declare function writeSecretStore(store: BlocksSecretStore): Promise<void
|
|
|
17
17
|
export declare function setClientSecret(account: string, clientSecret: string): Promise<void>;
|
|
18
18
|
export declare function getClientSecret(account: string): Promise<string | undefined>;
|
|
19
19
|
export declare function removeAccountSecrets(account: string): Promise<void>;
|
|
20
|
-
export declare function clearSecretStore(): Promise<void>;
|
|
21
20
|
export declare function setSecretValue(key: string, value: string): Promise<void>;
|
|
22
21
|
export declare function getSecretValue(key: string): Promise<string | undefined>;
|
|
23
22
|
export declare function removeSecretValue(key: string): Promise<void>;
|
package/dist/lib/secret-store.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { execFile, spawn } from "node:child_process";
|
|
2
|
-
import { mkdir, readFile,
|
|
2
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
3
|
import { platform } from "node:os";
|
|
4
4
|
import { dirname, join } from "node:path";
|
|
5
5
|
import { promisify } from "node:util";
|
|
@@ -57,9 +57,6 @@ export async function removeAccountSecrets(account) {
|
|
|
57
57
|
await removeSecretValue(`client-secret:${account}`);
|
|
58
58
|
await removeFallbackSecret(account);
|
|
59
59
|
}
|
|
60
|
-
export async function clearSecretStore() {
|
|
61
|
-
await rm(secretPath(), { force: true });
|
|
62
|
-
}
|
|
63
60
|
export async function setSecretValue(key, value) {
|
|
64
61
|
const backend = await resolveBackend();
|
|
65
62
|
if (backend === "macos-keychain") {
|
|
@@ -15,4 +15,3 @@ export declare function tokenStoreInfo(): Promise<{
|
|
|
15
15
|
export declare function readTokenStore(): Promise<BlocksTokenStore>;
|
|
16
16
|
export declare function writeTokenStore(store: BlocksTokenStore): Promise<void>;
|
|
17
17
|
export declare function removeAccountTokens(account: string): Promise<void>;
|
|
18
|
-
export declare function clearTokenStore(): Promise<void>;
|
package/dist/lib/token-store.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import { configDir, configPath } from "./config.js";
|
|
4
|
-
import { getSecretValue,
|
|
4
|
+
import { getSecretValue, secretStoreInfo, setSecretValue } from "./secret-store.js";
|
|
5
5
|
const TOKEN_SECRET_KEY = "oauth-token-store";
|
|
6
6
|
export function tokenPath() {
|
|
7
7
|
return join(configDir(), "tokens.json");
|
|
@@ -56,10 +56,6 @@ export async function removeAccountTokens(account) {
|
|
|
56
56
|
const { [account]: _, ...accounts } = store.accounts;
|
|
57
57
|
await writeTokenStore({ accounts });
|
|
58
58
|
}
|
|
59
|
-
export async function clearTokenStore() {
|
|
60
|
-
await removeSecretValue(TOKEN_SECRET_KEY);
|
|
61
|
-
await rm(tokenPath(), { force: true });
|
|
62
|
-
}
|
|
63
59
|
function normalizeTokenStore(store) {
|
|
64
60
|
return {
|
|
65
61
|
accounts: store?.accounts ?? {}
|
package/dist/lib/token.d.ts
CHANGED
|
@@ -13,7 +13,6 @@ export type TokenResponse = {
|
|
|
13
13
|
scope?: string;
|
|
14
14
|
token_type?: string;
|
|
15
15
|
};
|
|
16
|
-
export declare function randomState(): string;
|
|
17
16
|
export declare function expiresAtFromToken(response: TokenResponse, accessToken: string): string | undefined;
|
|
18
17
|
export declare function expiresAtFromJwtFirst(response: TokenResponse, accessToken: string): string | undefined;
|
|
19
18
|
export declare function refreshExpiresAtFromToken(response: TokenResponse, refreshToken?: string): string | undefined;
|
package/dist/lib/token.js
CHANGED
|
@@ -1,10 +1,6 @@
|
|
|
1
|
-
import { randomBytes } from "node:crypto";
|
|
2
1
|
import { decodeJwtPayload, tenantFromToken } from "./jwt.js";
|
|
3
2
|
const EXPIRY_SKEW_MS = 60_000;
|
|
4
3
|
const DEFAULT_REFRESH_TOKEN_LIFETIME_SECONDS = 30 * 60;
|
|
5
|
-
export function randomState() {
|
|
6
|
-
return randomBytes(24).toString("base64url");
|
|
7
|
-
}
|
|
8
4
|
export function expiresAtFromToken(response, accessToken) {
|
|
9
5
|
if (typeof response.expires_in === "number" && response.expires_in > 0) {
|
|
10
6
|
return new Date(Date.now() + response.expires_in * 1000).toISOString();
|
package/dist/lib/workspace.d.ts
CHANGED
|
@@ -20,6 +20,7 @@ export declare function readWorkspaceConfig(): Promise<BlocksWorkspaceConfig>;
|
|
|
20
20
|
export declare function writeWorkspaceConfig(config: BlocksWorkspaceConfig): Promise<void>;
|
|
21
21
|
export declare function selectedProject(flags: Record<string, string | boolean>): Promise<string>;
|
|
22
22
|
export declare function saveSelectedProject(tenantId: string): Promise<void>;
|
|
23
|
+
export declare function clearSelectedProject(): Promise<string | undefined>;
|
|
23
24
|
export declare function pathsFromWorkspace(config: BlocksWorkspaceConfig): {
|
|
24
25
|
dictionaries: string;
|
|
25
26
|
releaseConfig: string;
|
package/dist/lib/workspace.js
CHANGED
|
@@ -47,6 +47,23 @@ export async function saveSelectedProject(tenantId) {
|
|
|
47
47
|
});
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
|
+
export async function clearSelectedProject() {
|
|
51
|
+
const global = await readConfig();
|
|
52
|
+
const tenantId = global.selectedProject?.tenantId;
|
|
53
|
+
if (!tenantId)
|
|
54
|
+
return undefined;
|
|
55
|
+
const { selectedProject: _dropped, ...rest } = global;
|
|
56
|
+
await writeConfig(rest);
|
|
57
|
+
const local = await readWorkspaceConfig();
|
|
58
|
+
if (local.project?.tenantId) {
|
|
59
|
+
const { tenantId: _localTenantId, ...restProject } = local.project;
|
|
60
|
+
await writeWorkspaceConfig({
|
|
61
|
+
...local,
|
|
62
|
+
project: Object.keys(restProject).length > 0 ? restProject : undefined
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
return tenantId;
|
|
66
|
+
}
|
|
50
67
|
export function pathsFromWorkspace(config) {
|
|
51
68
|
return {
|
|
52
69
|
dictionaries: config.localization?.dictionaries ?? "blocks/localization",
|
package/package.json
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare function authAdd(argv: string[]): Promise<void>;
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import { parseFlags, stringFlag } from "../../lib/args.js";
|
|
2
|
-
import { defaults, normalizeAccountName, readConfig, writeConfig } from "../../lib/config.js";
|
|
3
|
-
import { promptSecret, promptValue } from "../../lib/prompt.js";
|
|
4
|
-
import { getClientSecret, setClientSecret } from "../../lib/secret-store.js";
|
|
5
|
-
export async function authAdd(argv) {
|
|
6
|
-
const { args, flags } = parseFlags(argv);
|
|
7
|
-
const account = normalizeAccountName(args[0]);
|
|
8
|
-
const current = await readConfig();
|
|
9
|
-
const previous = current.accounts[account];
|
|
10
|
-
const now = new Date().toISOString();
|
|
11
|
-
const apiUrl = stringFlag(flags, "api-url") || previous?.apiUrl || defaults().apiUrl;
|
|
12
|
-
const oidcUrl = stringFlag(flags, "oidc-url") || previous?.oidcUrl || defaults().oidcUrl;
|
|
13
|
-
const osUrl = stringFlag(flags, "os-url") || defaults().osUrl;
|
|
14
|
-
const clientId = await value(flags, "client-id", "OIDC client id", previous?.clientId);
|
|
15
|
-
const existingSecret = await getClientSecret(account);
|
|
16
|
-
const clientSecret = await value(flags, "client-secret", "OIDC client secret", existingSecret, true);
|
|
17
|
-
const scope = stringFlag(flags, "scope") || previous?.scope || defaults().scope;
|
|
18
|
-
const redirectUri = stringFlag(flags, "redirect-uri") || previous?.redirectUri || defaults().redirectUri;
|
|
19
|
-
const rootTenantId = optionalStringFlag(flags, "root-tenant") ?? previous?.rootTenantId ?? defaults().rootTenantId;
|
|
20
|
-
await writeConfig({
|
|
21
|
-
...current,
|
|
22
|
-
activeAccount: current.activeAccount ?? account,
|
|
23
|
-
accounts: {
|
|
24
|
-
...current.accounts,
|
|
25
|
-
[account]: {
|
|
26
|
-
apiUrl,
|
|
27
|
-
clientId,
|
|
28
|
-
createdAt: previous?.createdAt ?? now,
|
|
29
|
-
oidcUrl,
|
|
30
|
-
osUrl,
|
|
31
|
-
redirectUri,
|
|
32
|
-
rootTenantId,
|
|
33
|
-
scope,
|
|
34
|
-
updatedAt: now
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
});
|
|
38
|
-
await setClientSecret(account, clientSecret);
|
|
39
|
-
console.log(`Saved OIDC account '${account}'.`);
|
|
40
|
-
}
|
|
41
|
-
async function value(flags, name, label, defaultValue, secret = false) {
|
|
42
|
-
const flag = stringFlag(flags, name);
|
|
43
|
-
const resolved = flag || await (secret ? promptSecret(label, defaultValue) : promptValue(label, defaultValue));
|
|
44
|
-
if (!resolved)
|
|
45
|
-
throw new Error(`Missing --${name}`);
|
|
46
|
-
return resolved;
|
|
47
|
-
}
|
|
48
|
-
function optionalStringFlag(flags, name) {
|
|
49
|
-
const value = flags[name];
|
|
50
|
-
return typeof value === "string" && value ? value : undefined;
|
|
51
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare function authList(argv?: string[]): Promise<void>;
|