@blinkhost/cli 2.0.0
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/LICENSE +21 -0
- package/README.md +146 -0
- package/dist/api.d.ts +20 -0
- package/dist/api.js +135 -0
- package/dist/auth.d.ts +9 -0
- package/dist/auth.js +71 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +443 -0
- package/dist/config.d.ts +28 -0
- package/dist/config.js +67 -0
- package/dist/credentials.d.ts +3 -0
- package/dist/credentials.js +66 -0
- package/dist/detect.d.ts +2 -0
- package/dist/detect.js +61 -0
- package/dist/errors.d.ts +20 -0
- package/dist/errors.js +20 -0
- package/dist/manifest.d.ts +50 -0
- package/dist/manifest.js +193 -0
- package/dist/project.d.ts +13 -0
- package/dist/project.js +177 -0
- package/dist/remote.d.ts +19 -0
- package/dist/remote.js +366 -0
- package/dist/templates.d.ts +17 -0
- package/dist/templates.js +109 -0
- package/dist/workflows.d.ts +8 -0
- package/dist/workflows.js +209 -0
- package/package.json +18 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 BLINKHOST LTD
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# BlinkHost CLI
|
|
2
|
+
|
|
3
|
+
The BlinkHost CLI brings project setup, local development, source control, previews, builds, releases, resources, and diagnostics into the terminal while BlinkHost continues to enforce workspace roles, plan limits, protected environments, verified artifacts, and audit history.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
Node.js 22.12 or newer is required.
|
|
8
|
+
|
|
9
|
+
Install the signed release archive directly from BlinkHost's public repository:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install --global https://github.com/blinkhost-ltd/blinkhost-cli/releases/download/v2.0.0/blinkhost-cli-2.0.0.tgz
|
|
13
|
+
blinkhost --version
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Checksums, the CycloneDX SBOM, and Sigstore verification bundle are published with the archive at <https://github.com/blinkhost-ltd/blinkhost-cli/releases/tag/v2.0.0>.
|
|
17
|
+
|
|
18
|
+
## Connect an account
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
blinkhost auth login
|
|
22
|
+
blinkhost auth status
|
|
23
|
+
blinkhost auth sessions
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
The CLI opens a BlinkHost approval page in your browser. It never accepts your BlinkHost password. Access tokens last 15 minutes; rotating refresh credentials are stored only in the operating-system credential service. Use named profiles for separate accounts:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
blinkhost auth login --profile work
|
|
30
|
+
blinkhost profile use work
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Revoke a device with `blinkhost auth revoke SESSION_ID` or disconnect all CLI sessions for the active profile with `blinkhost auth logout`.
|
|
34
|
+
|
|
35
|
+
## Create or adopt a project
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
blinkhost create study-circle --template astro --module search:rust --module reminders:python --database PRIMARY_DB
|
|
39
|
+
cd study-circle
|
|
40
|
+
blinkhost validate
|
|
41
|
+
blinkhost projects link PROJECT_ID
|
|
42
|
+
blinkhost dev
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Supported frontends are Astro, HTML, React, Solid, Svelte, and Vue. Backend modules may use Go, Python, or Rust. `blinkhost init` detects supported metadata in an existing repository and creates `blinkhost.yaml` for review.
|
|
46
|
+
|
|
47
|
+
Creation is atomic and refuses to replace an existing path. Dependency lifecycle scripts are disabled. Validation rejects unknown manifest fields, duplicate YAML keys, aliases, traversal, unsafe symbolic links, invalid cross-platform paths, duplicate module names, and missing declared inputs.
|
|
48
|
+
|
|
49
|
+
## Remote workflows
|
|
50
|
+
|
|
51
|
+
Resource commands use the same pattern:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
blinkhost projects list
|
|
55
|
+
blinkhost projects get PROJECT_ID
|
|
56
|
+
blinkhost previews list --query project=PROJECT_ID
|
|
57
|
+
blinkhost builds list --query project=PROJECT_ID
|
|
58
|
+
blinkhost deployments get DEPLOYMENT_ID
|
|
59
|
+
blinkhost modules list --query site_id=PROJECT_ID
|
|
60
|
+
blinkhost databases list --query project=PROJECT_ID
|
|
61
|
+
blinkhost organizations get ORGANIZATION_ID
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Create and update operations accept a JSON object or `@path` to a JSON file:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
blinkhost projects create --data @project.json
|
|
68
|
+
blinkhost deployments action DEPLOYMENT_ID rollback --data @rollback.json
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Destructive operations require the exact resource ID twice:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
blinkhost previews delete PREVIEW_ID --confirm PREVIEW_ID
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Connected repository, release, approval, agency handoff, enterprise policy, and template APIs are available through `repositories`, `connections`, `builds`, `approvals`, `handoffs`, `policies`, and `templates`. Advanced customer API operations can use `blinkhost api METHOD /api/path/`; internal, staff, and authentication routes are blocked.
|
|
78
|
+
|
|
79
|
+
## Secrets and resources
|
|
80
|
+
|
|
81
|
+
Secret values never appear in command arguments or `blinkhost.yaml`. Pipe them over standard input:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
printf '%s' "$MY_SECRET" | blinkhost secrets set API_TOKEN --project PROJECT_ID --environment production
|
|
85
|
+
printf '%s' "$NEW_VALUE" | blinkhost secrets rotate SECRET_ID
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Database, binding, module, and asset operations remain subject to workspace permissions and plan storage limits. Upload reservations and final integrity checks use the project asset API.
|
|
89
|
+
|
|
90
|
+
## Observability and support
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
blinkhost logs --project PROJECT_ID --since 1h --limit 100
|
|
94
|
+
blinkhost metrics --project PROJECT_ID --since 24h
|
|
95
|
+
blinkhost analytics --project PROJECT_ID
|
|
96
|
+
blinkhost doctor
|
|
97
|
+
blinkhost support bundle
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Support bundles are local, redacted JSON files with mode `0600`. They exclude tokens, secret values, source code, filenames, and repository URLs.
|
|
101
|
+
|
|
102
|
+
## CI and automation
|
|
103
|
+
|
|
104
|
+
All commands support `--json`. Register one exact GitHub branch, tag, or protected environment as a workload identity, then grant the workflow `id-token: write`. The CLI exchanges GitHub's signed OIDC assertion for a ten-minute BlinkHost token automatically; no BlinkHost secret is stored in GitHub.
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
blinkhost workloads create --data @workload.json
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
`workload.json` identifies the workspace, exact GitHub OIDC subject, and narrow permissions. For non-GitHub systems, an externally issued short-lived credential may be provided as `BLINKHOST_ACCESS_TOKEN`; do not persist it in the repository or pass it as an argument.
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
blinkhost ci check --json
|
|
114
|
+
blinkhost validate --json
|
|
115
|
+
blinkhost builds create --data @build.json --json
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Stable exit codes distinguish usage, validation, filesystem, authentication, network, remote-service, and conflict failures. Mutations include request identifiers and idempotency keys.
|
|
119
|
+
|
|
120
|
+
## Shell completion, updates, and plugins
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
blinkhost completion bash # also zsh, fish, or powershell
|
|
124
|
+
blinkhost update check
|
|
125
|
+
blinkhost plugins add /absolute/path/to/plugin --name example
|
|
126
|
+
blinkhost plugins verify example
|
|
127
|
+
blinkhost plugins run example -- arguments
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Update checks never install software. Plugins require explicit local approval, are pinned to their executable SHA-256 digest, stop when the executable changes, and receive neither BlinkHost credentials nor the parent environment. A plugin is still third-party code running with your operating-system account; review it before adding it.
|
|
131
|
+
|
|
132
|
+
## Exit codes
|
|
133
|
+
|
|
134
|
+
| Code | Meaning |
|
|
135
|
+
| --- | --- |
|
|
136
|
+
| 0 | Success |
|
|
137
|
+
| 2 | Invalid command or option |
|
|
138
|
+
| 3 | Project or input validation failed |
|
|
139
|
+
| 4 | Filesystem or local executable failure |
|
|
140
|
+
| 5 | Unexpected internal failure |
|
|
141
|
+
| 6 | Authentication or authorization failure |
|
|
142
|
+
| 7 | Network or timeout failure |
|
|
143
|
+
| 8 | Remote service failure |
|
|
144
|
+
| 9 | State conflict or approval required |
|
|
145
|
+
|
|
146
|
+
Run `blinkhost --help` for the complete command index.
|
package/dist/api.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type Profile } from './config.js';
|
|
2
|
+
export declare function publicRequest(apiOrigin: string, path: string, init?: RequestInit): Promise<{
|
|
3
|
+
response: Response;
|
|
4
|
+
data: unknown;
|
|
5
|
+
}>;
|
|
6
|
+
export declare class ApiClient {
|
|
7
|
+
readonly profileName: string;
|
|
8
|
+
readonly profile: Profile;
|
|
9
|
+
private readonly ephemeral;
|
|
10
|
+
private accessToken;
|
|
11
|
+
private constructor();
|
|
12
|
+
static create(profileName?: string): Promise<ApiClient>;
|
|
13
|
+
static fromAccessToken(profileName: string, profile: Profile, accessToken: string): ApiClient;
|
|
14
|
+
request(path: string, init?: RequestInit): Promise<unknown>;
|
|
15
|
+
rememberIdentity(identity: {
|
|
16
|
+
id?: number;
|
|
17
|
+
username?: string;
|
|
18
|
+
}): Promise<void>;
|
|
19
|
+
}
|
|
20
|
+
export declare function encodeQuery(values: Record<string, string | undefined>): string;
|
package/dist/api.js
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { activeProfile, writeConfig } from './config.js';
|
|
3
|
+
import { deleteRefreshCredential, getRefreshCredential, setRefreshCredential } from './credentials.js';
|
|
4
|
+
import { CliError, EXIT } from './errors.js';
|
|
5
|
+
const REQUEST_TIMEOUT_MS = 30_000;
|
|
6
|
+
function messageFrom(payload, fallback) {
|
|
7
|
+
if (payload && typeof payload === 'object') {
|
|
8
|
+
const candidate = payload;
|
|
9
|
+
for (const key of ['detail', 'error', 'message'])
|
|
10
|
+
if (typeof candidate[key] === 'string')
|
|
11
|
+
return candidate[key];
|
|
12
|
+
const first = Object.values(candidate)[0];
|
|
13
|
+
if (Array.isArray(first) && typeof first[0] === 'string')
|
|
14
|
+
return first[0];
|
|
15
|
+
}
|
|
16
|
+
return fallback;
|
|
17
|
+
}
|
|
18
|
+
async function parseResponse(response) {
|
|
19
|
+
if (response.status === 204)
|
|
20
|
+
return null;
|
|
21
|
+
const type = response.headers.get('content-type') || '';
|
|
22
|
+
if (type.includes('application/json'))
|
|
23
|
+
return response.json();
|
|
24
|
+
const text = await response.text();
|
|
25
|
+
return text.length <= 1024 * 1024 ? text : `${text.slice(0, 1024 * 1024)}\n[truncated]`;
|
|
26
|
+
}
|
|
27
|
+
export async function publicRequest(apiOrigin, path, init = {}) {
|
|
28
|
+
const controller = new AbortController();
|
|
29
|
+
const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
|
30
|
+
try {
|
|
31
|
+
const response = await fetch(new URL(path, `${apiOrigin}/`), {
|
|
32
|
+
...init,
|
|
33
|
+
redirect: 'error',
|
|
34
|
+
signal: controller.signal,
|
|
35
|
+
headers: { Accept: 'application/json', 'Content-Type': 'application/json', 'User-Agent': 'BlinkHost-CLI/2.0.0', ...Object.fromEntries(new Headers(init.headers).entries()) },
|
|
36
|
+
});
|
|
37
|
+
return { response, data: await parseResponse(response) };
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
if (error instanceof CliError)
|
|
41
|
+
throw error;
|
|
42
|
+
const timeoutMessage = error instanceof DOMException && error.name === 'AbortError';
|
|
43
|
+
throw new CliError(timeoutMessage ? 'The BlinkHost API request timed out.' : 'The BlinkHost API could not be reached.', EXIT.network, timeoutMessage ? 'request_timeout' : 'network_error');
|
|
44
|
+
}
|
|
45
|
+
finally {
|
|
46
|
+
clearTimeout(timeout);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
export class ApiClient {
|
|
50
|
+
profileName;
|
|
51
|
+
profile;
|
|
52
|
+
ephemeral;
|
|
53
|
+
accessToken;
|
|
54
|
+
constructor(profileName, profile, accessToken, ephemeral) {
|
|
55
|
+
this.profileName = profileName;
|
|
56
|
+
this.profile = profile;
|
|
57
|
+
this.ephemeral = ephemeral;
|
|
58
|
+
this.accessToken = accessToken;
|
|
59
|
+
}
|
|
60
|
+
static async create(profileName) {
|
|
61
|
+
const selected = await activeProfile(profileName);
|
|
62
|
+
const workload = process.env.BLINKHOST_ACCESS_TOKEN;
|
|
63
|
+
if (workload)
|
|
64
|
+
return new ApiClient(selected.name, selected.profile, workload, true);
|
|
65
|
+
if (process.env.ACTIONS_ID_TOKEN_REQUEST_URL && process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN) {
|
|
66
|
+
const requestUrl = process.env.ACTIONS_ID_TOKEN_REQUEST_URL;
|
|
67
|
+
const requestToken = process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;
|
|
68
|
+
let oidcUrl;
|
|
69
|
+
try {
|
|
70
|
+
oidcUrl = new URL(requestUrl);
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
throw new CliError('GitHub Actions supplied an invalid OIDC endpoint.', EXIT.auth, 'ci_oidc_invalid');
|
|
74
|
+
}
|
|
75
|
+
if (oidcUrl.protocol !== 'https:' || !(oidcUrl.hostname === 'actions.githubusercontent.com' || oidcUrl.hostname.endsWith('.actions.githubusercontent.com')))
|
|
76
|
+
throw new CliError('GitHub Actions supplied an untrusted OIDC endpoint.', EXIT.auth, 'ci_oidc_invalid');
|
|
77
|
+
oidcUrl.searchParams.set('audience', 'https://api.blinkhost.me/api/cli/v2/workload/exchange/');
|
|
78
|
+
const oidcResponse = await fetch(oidcUrl, { headers: { Authorization: `Bearer ${requestToken}`, Accept: 'application/json' }, redirect: 'error' });
|
|
79
|
+
const oidc = await oidcResponse.json();
|
|
80
|
+
if (!oidcResponse.ok || !oidc.value)
|
|
81
|
+
throw new CliError('GitHub Actions could not issue a workload identity token.', EXIT.auth, 'ci_oidc_failed');
|
|
82
|
+
const exchanged = await publicRequest(selected.profile.apiOrigin, '/api/cli/v2/workload/exchange/', { method: 'POST', body: JSON.stringify({ assertion: oidc.value }) });
|
|
83
|
+
const payload = exchanged.data;
|
|
84
|
+
if (!exchanged.response.ok || !payload.access_token)
|
|
85
|
+
throw new CliError(payload.detail || 'BlinkHost rejected this workload identity.', EXIT.auth, 'ci_exchange_failed');
|
|
86
|
+
return new ApiClient(selected.name, selected.profile, payload.access_token, true);
|
|
87
|
+
}
|
|
88
|
+
const refresh = process.env.BLINKHOST_REFRESH_TOKEN || await getRefreshCredential(selected.name);
|
|
89
|
+
if (!refresh)
|
|
90
|
+
throw new CliError('Sign in with `blinkhost auth login` or provide BLINKHOST_ACCESS_TOKEN for CI.', EXIT.auth, 'not_authenticated');
|
|
91
|
+
const result = await publicRequest(selected.profile.apiOrigin, '/api/cli/v2/token/refresh/', { method: 'POST', body: JSON.stringify({ refresh_token: refresh }) });
|
|
92
|
+
if (!result.response.ok) {
|
|
93
|
+
if (!process.env.BLINKHOST_REFRESH_TOKEN)
|
|
94
|
+
await deleteRefreshCredential(selected.name);
|
|
95
|
+
throw new CliError(messageFrom(result.data, 'Your CLI session has expired. Sign in again.'), EXIT.auth, 'session_expired');
|
|
96
|
+
}
|
|
97
|
+
const tokens = result.data;
|
|
98
|
+
if (!process.env.BLINKHOST_REFRESH_TOKEN)
|
|
99
|
+
await setRefreshCredential(selected.name, tokens.refresh_token);
|
|
100
|
+
return new ApiClient(selected.name, selected.profile, tokens.access_token, Boolean(process.env.BLINKHOST_REFRESH_TOKEN));
|
|
101
|
+
}
|
|
102
|
+
static fromAccessToken(profileName, profile, accessToken) {
|
|
103
|
+
return new ApiClient(profileName, profile, accessToken, true);
|
|
104
|
+
}
|
|
105
|
+
async request(path, init = {}) {
|
|
106
|
+
const headers = new Headers(init.headers);
|
|
107
|
+
headers.set('Authorization', `Bearer ${this.accessToken}`);
|
|
108
|
+
headers.set('X-Request-ID', headers.get('X-Request-ID') || randomUUID());
|
|
109
|
+
if (init.method && init.method !== 'GET' && !headers.has('Idempotency-Key'))
|
|
110
|
+
headers.set('Idempotency-Key', randomUUID());
|
|
111
|
+
const result = await publicRequest(this.profile.apiOrigin, path, { ...init, headers });
|
|
112
|
+
if (!result.response.ok) {
|
|
113
|
+
const exit = result.response.status === 401 || result.response.status === 403 ? EXIT.auth : result.response.status === 409 ? EXIT.conflict : EXIT.remote;
|
|
114
|
+
throw new CliError(messageFrom(result.data, `BlinkHost returned HTTP ${result.response.status}.`), exit, `api_${result.response.status}`);
|
|
115
|
+
}
|
|
116
|
+
return result.data;
|
|
117
|
+
}
|
|
118
|
+
async rememberIdentity(identity) {
|
|
119
|
+
if (this.ephemeral)
|
|
120
|
+
return;
|
|
121
|
+
const selected = await activeProfile(this.profileName);
|
|
122
|
+
selected.config.profiles[this.profileName] = { ...this.profile, ...(identity.id ? { userId: identity.id } : {}), ...(identity.username ? { username: identity.username } : {}) };
|
|
123
|
+
selected.config.activeProfile = this.profileName;
|
|
124
|
+
await writeConfig(selected.config);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
export function encodeQuery(values) {
|
|
128
|
+
const query = new URLSearchParams();
|
|
129
|
+
for (const [key, value] of Object.entries(values))
|
|
130
|
+
if (value !== undefined)
|
|
131
|
+
query.set(key, value);
|
|
132
|
+
const text = query.toString();
|
|
133
|
+
return text ? `?${text}` : '';
|
|
134
|
+
}
|
|
135
|
+
//# sourceMappingURL=api.js.map
|
package/dist/auth.d.ts
ADDED
package/dist/auth.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { createHash, randomBytes } from 'node:crypto';
|
|
2
|
+
import { hostname } from 'node:os';
|
|
3
|
+
import { spawn } from 'node:child_process';
|
|
4
|
+
import { activeProfile, validateApiOrigin, validateProfileName, writeConfig } from './config.js';
|
|
5
|
+
import { deleteRefreshCredential, setRefreshCredential } from './credentials.js';
|
|
6
|
+
import { ApiClient, publicRequest } from './api.js';
|
|
7
|
+
import { CliError, EXIT } from './errors.js';
|
|
8
|
+
const VERSION = '2.0.0';
|
|
9
|
+
const sleep = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
10
|
+
function openBrowser(url) {
|
|
11
|
+
const command = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'cmd.exe' : 'xdg-open';
|
|
12
|
+
const args = process.platform === 'win32' ? ['/d', '/s', '/c', 'start', '', url] : [url];
|
|
13
|
+
const child = spawn(command, args, { detached: true, stdio: 'ignore', shell: false });
|
|
14
|
+
child.on('error', () => { });
|
|
15
|
+
child.unref();
|
|
16
|
+
}
|
|
17
|
+
function errorCode(data) {
|
|
18
|
+
return data && typeof data === 'object' && typeof data.error === 'string' ? data.error : '';
|
|
19
|
+
}
|
|
20
|
+
export async function login(options) {
|
|
21
|
+
const selected = await activeProfile(options.profile);
|
|
22
|
+
const profileName = validateProfileName(options.profile || selected.name);
|
|
23
|
+
const apiOrigin = validateApiOrigin(options.apiOrigin || selected.profile.apiOrigin);
|
|
24
|
+
const verifier = randomBytes(48).toString('base64url');
|
|
25
|
+
const challenge = createHash('sha256').update(verifier).digest('base64url');
|
|
26
|
+
const started = await publicRequest(apiOrigin, '/api/cli/v2/device/start/', {
|
|
27
|
+
method: 'POST',
|
|
28
|
+
body: JSON.stringify({ code_challenge: challenge, client_name: 'BlinkHost CLI', client_version: VERSION, device_name: hostname().slice(0, 120) }),
|
|
29
|
+
});
|
|
30
|
+
if (!started.response.ok)
|
|
31
|
+
throw new CliError('BlinkHost could not start CLI authorization.', EXIT.auth, errorCode(started.data) || 'authorization_start_failed');
|
|
32
|
+
const device = started.data;
|
|
33
|
+
options.progress?.(`Open ${device.verification_uri_complete}`);
|
|
34
|
+
options.progress?.(`Confirm code ${device.user_code}`);
|
|
35
|
+
if (options.openBrowser !== false)
|
|
36
|
+
openBrowser(device.verification_uri_complete);
|
|
37
|
+
const deadline = Date.now() + device.expires_in * 1000;
|
|
38
|
+
while (Date.now() < deadline) {
|
|
39
|
+
await sleep(Math.max(device.interval, 5) * 1000);
|
|
40
|
+
const polled = await publicRequest(apiOrigin, '/api/cli/v2/device/poll/', { method: 'POST', body: JSON.stringify({ device_code: device.device_code, code_verifier: verifier }) });
|
|
41
|
+
const code = errorCode(polled.data);
|
|
42
|
+
if (code === 'authorization_pending' || code === 'slow_down')
|
|
43
|
+
continue;
|
|
44
|
+
if (!polled.response.ok)
|
|
45
|
+
throw new CliError(code === 'access_denied' ? 'CLI authorization was denied.' : 'CLI authorization could not be completed.', EXIT.auth, code || 'authorization_failed');
|
|
46
|
+
const tokens = polled.data;
|
|
47
|
+
await setRefreshCredential(profileName, tokens.refresh_token);
|
|
48
|
+
selected.config.profiles[profileName] = { apiOrigin };
|
|
49
|
+
selected.config.activeProfile = profileName;
|
|
50
|
+
await writeConfig(selected.config);
|
|
51
|
+
const client = await ApiClient.create(profileName);
|
|
52
|
+
const capabilities = await client.request('/api/cli/v2/capabilities/');
|
|
53
|
+
await client.rememberIdentity(capabilities.actor || {});
|
|
54
|
+
return { profile: profileName, api_origin: apiOrigin, account: capabilities.actor, permissions: capabilities.features };
|
|
55
|
+
}
|
|
56
|
+
throw new CliError('CLI authorization expired before it was approved.', EXIT.auth, 'authorization_expired');
|
|
57
|
+
}
|
|
58
|
+
export async function logout(profile) {
|
|
59
|
+
const selected = await activeProfile(profile);
|
|
60
|
+
try {
|
|
61
|
+
const client = await ApiClient.create(selected.name);
|
|
62
|
+
await client.request('/api/cli/v2/sessions/current/', { method: 'DELETE' });
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
if (!(error instanceof CliError) || error.code !== 'session_expired')
|
|
66
|
+
throw error;
|
|
67
|
+
}
|
|
68
|
+
await deleteRefreshCredential(selected.name);
|
|
69
|
+
return { profile: selected.name };
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=auth.js.map
|
package/dist/cli.d.ts
ADDED