@bctrl/cli 0.1.5 → 0.1.7
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 +15 -0
- package/README.md +109 -95
- package/dist/api/auth.d.ts +10 -1
- package/dist/api/auth.js +10 -1
- package/dist/api/client.d.ts +2 -1
- package/dist/api/client.js +37 -37
- package/dist/api/device-auth.d.ts +39 -0
- package/dist/api/device-auth.js +93 -0
- package/dist/api/errors.d.ts +1 -0
- package/dist/api/errors.js +8 -0
- package/dist/bin/bctrl.js +0 -0
- package/dist/commands/ai/index.js +10 -11
- package/dist/commands/api-key/index.js +3 -8
- package/dist/commands/auth/device-login.d.ts +6 -0
- package/dist/commands/auth/device-login.js +19 -0
- package/dist/commands/auth/login.d.ts +11 -0
- package/dist/commands/auth/login.js +140 -20
- package/dist/commands/auth/logout.d.ts +2 -0
- package/dist/commands/auth/logout.js +19 -2
- package/dist/commands/auth/status.d.ts +1 -0
- package/dist/commands/auth/status.js +22 -5
- package/dist/commands/auth/token.js +6 -0
- package/dist/commands/browser-extension/index.js +7 -15
- package/dist/commands/file/index.js +8 -7
- package/dist/commands/help/index.js +5 -4
- package/dist/commands/notification-recipient/index.d.ts +3 -0
- package/dist/commands/notification-recipient/index.js +72 -0
- package/dist/commands/proxy/index.js +6 -4
- package/dist/commands/run/index.js +31 -32
- package/dist/commands/runtime/index.js +186 -107
- package/dist/commands/shared/help.d.ts +26 -3
- package/dist/commands/shared/help.js +87 -23
- package/dist/commands/shared/io.d.ts +1 -0
- package/dist/commands/shared/io.js +4 -2
- package/dist/commands/shared/operation.d.ts +50 -3
- package/dist/commands/shared/operation.js +171 -25
- package/dist/commands/shared/output.js +3 -7
- package/dist/commands/space/index.js +35 -32
- package/dist/commands/space/list.js +6 -5
- package/dist/commands/subaccount/index.js +13 -15
- package/dist/commands/usage/index.js +9 -5
- package/dist/commands/vault/index.js +15 -21
- package/dist/config/auth-store.d.ts +75 -9
- package/dist/config/auth-store.js +133 -27
- package/dist/config/config.d.ts +2 -2
- package/dist/config/config.js +9 -17
- package/dist/config/paths.d.ts +16 -0
- package/dist/config/paths.js +34 -0
- package/dist/config/pending-auth.d.ts +19 -0
- package/dist/config/pending-auth.js +72 -0
- package/dist/config/secret-store.d.ts +24 -0
- package/dist/config/secret-store.js +123 -0
- package/dist/factory.js +2 -1
- package/dist/generated/help.d.ts +7218 -2535
- package/dist/generated/help.js +10057 -3928
- package/dist/generated/openapi-routes.d.ts +52 -8
- package/dist/generated/openapi-routes.js +20 -9
- package/dist/generated/openapi-types.d.ts +2729 -849
- package/dist/root.js +2 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +4 -0
- package/package.json +56 -47
package/LICENSE
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
ISC License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 BCTRL
|
|
4
|
+
|
|
5
|
+
Permission to use, copy, modify, and/or distribute this software for any purpose
|
|
6
|
+
with or without fee is hereby granted, provided that the above copyright notice
|
|
7
|
+
and this permission notice appear in all copies.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
10
|
+
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
|
|
11
|
+
FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
12
|
+
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
|
|
13
|
+
OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
|
|
14
|
+
TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
|
|
15
|
+
THIS SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,95 +1,109 @@
|
|
|
1
|
-
# BCTRL CLI
|
|
2
|
-
|
|
3
|
-
CLI for controlling BCTRL runtimes, files, runs, vault secrets, tools, and account resources from scripts or AI agents.
|
|
4
|
-
|
|
5
|
-
## Install
|
|
6
|
-
|
|
7
|
-
```bash
|
|
8
|
-
npm install -g @bctrl/cli
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
## Auth
|
|
12
|
-
|
|
13
|
-
The CLI authenticates with a BCTRL API key.
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
```bash
|
|
18
|
-
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
```bash
|
|
24
|
-
bctrl auth login
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
```bash
|
|
32
|
-
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
bctrl
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
bctrl runtime
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
```
|
|
1
|
+
# BCTRL CLI
|
|
2
|
+
|
|
3
|
+
CLI for controlling BCTRL runtimes, files, runs, vault secrets, tools, and account resources from scripts or AI agents.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g @bctrl/cli
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Auth
|
|
12
|
+
|
|
13
|
+
The CLI authenticates with either a browser-approved CLI session or a BCTRL API key.
|
|
14
|
+
|
|
15
|
+
For a local terminal, start a browser approval and let the CLI poll until you approve it:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
bctrl auth login --url --wait
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
For agents that need to hand the URL to a human, split the flow into two commands:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
bctrl auth login --url
|
|
25
|
+
# approve the printed URL
|
|
26
|
+
bctrl auth login
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Use an API key for CI or a single shell:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
export BCTRL_API_KEY="bctrl_..."
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
`BCTRL_API_KEY` takes precedence over stored credentials.
|
|
36
|
+
|
|
37
|
+
Check the active auth:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
bctrl auth status
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Point contributors at a local or staging stack with `BCTRL_API_URL`:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
BCTRL_API_URL=http://127.0.0.1:8787/v1 bctrl auth login --url --wait
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Browser Runtime Flow
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
bctrl space list
|
|
53
|
+
bctrl runtime create --space sp_123 --name checkout-test
|
|
54
|
+
bctrl runtime start rt_123
|
|
55
|
+
bctrl runtime stop rt_123
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## JSON Input
|
|
59
|
+
|
|
60
|
+
Use `--input` for full request bodies:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
bctrl runtime create --input runtime.json
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Use `-` to read from stdin:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
echo '{"type":"browser","spaceId":"sp_123","name":"agent-runtime"}' \
|
|
70
|
+
| bctrl runtime create --input -
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Output
|
|
74
|
+
|
|
75
|
+
Print full JSON:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
bctrl runtime list --space sp_123 --json
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Print selected JSON fields:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
bctrl runtime list --space sp_123 --json id,status,name
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Filter with `jq`:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
bctrl runtime list --space sp_123 --json --jq '.data[] | select(.status == "active")'
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Render with a template:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
bctrl runtime list \
|
|
97
|
+
--space sp_123 \
|
|
98
|
+
--json \
|
|
99
|
+
--template '{{#each data}}{{id}} {{status}}{{newline}}{{/each}}'
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Help
|
|
103
|
+
|
|
104
|
+
Use command help for exact input and output fields:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
bctrl runtime create --help
|
|
108
|
+
bctrl runtime start --help
|
|
109
|
+
```
|
package/dist/api/auth.d.ts
CHANGED
|
@@ -7,7 +7,16 @@ export declare const AuthWhoamiSchema: z.ZodObject<{
|
|
|
7
7
|
}>;
|
|
8
8
|
organizationId: z.ZodString;
|
|
9
9
|
subaccountId: z.ZodNullable<z.ZodString>;
|
|
10
|
-
defaultSpaceId: z.ZodString
|
|
10
|
+
defaultSpaceId: z.ZodNullable<z.ZodString>;
|
|
11
|
+
effectiveScope: z.ZodObject<{
|
|
12
|
+
scope: z.ZodEnum<{
|
|
13
|
+
organization: "organization";
|
|
14
|
+
subaccount: "subaccount";
|
|
15
|
+
}>;
|
|
16
|
+
organizationId: z.ZodString;
|
|
17
|
+
subaccountId: z.ZodNullable<z.ZodString>;
|
|
18
|
+
defaultSpaceId: z.ZodNullable<z.ZodString>;
|
|
19
|
+
}, z.core.$strict>;
|
|
11
20
|
keyId: z.ZodString;
|
|
12
21
|
plan: z.ZodString;
|
|
13
22
|
}, z.core.$strict>;
|
package/dist/api/auth.js
CHANGED
|
@@ -1,13 +1,22 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { CliError } from '../runtime/errors.js';
|
|
3
3
|
import { apiErrorFromResponse } from './errors.js';
|
|
4
|
+
const AuthEffectiveScopeSchema = z
|
|
5
|
+
.object({
|
|
6
|
+
scope: z.enum(['organization', 'subaccount']),
|
|
7
|
+
organizationId: z.string().min(1),
|
|
8
|
+
subaccountId: z.string().min(1).nullable(),
|
|
9
|
+
defaultSpaceId: z.string().min(1).nullable(),
|
|
10
|
+
})
|
|
11
|
+
.strict();
|
|
4
12
|
export const AuthWhoamiSchema = z
|
|
5
13
|
.object({
|
|
6
14
|
email: z.string().email().nullable().optional(),
|
|
7
15
|
scope: z.enum(['organization', 'subaccount']),
|
|
8
16
|
organizationId: z.string().min(1),
|
|
9
17
|
subaccountId: z.string().min(1).nullable(),
|
|
10
|
-
defaultSpaceId: z.string().min(1),
|
|
18
|
+
defaultSpaceId: z.string().min(1).nullable(),
|
|
19
|
+
effectiveScope: AuthEffectiveScopeSchema,
|
|
11
20
|
keyId: z.string().min(1),
|
|
12
21
|
plan: z.string().min(1),
|
|
13
22
|
})
|
package/dist/api/client.d.ts
CHANGED
|
@@ -16,8 +16,9 @@ export type BctrlApiClient = {
|
|
|
16
16
|
export type RequestOptions = {
|
|
17
17
|
query?: Record<string, string | number | boolean | undefined>;
|
|
18
18
|
idempotencyKey?: string;
|
|
19
|
+
actingSubaccountId?: string;
|
|
19
20
|
};
|
|
20
21
|
export type JsonRequestOptions = RequestOptions & {
|
|
21
22
|
body?: unknown;
|
|
22
23
|
};
|
|
23
|
-
export declare function createBctrlApiClient(config: BctrlConfig): BctrlApiClient;
|
|
24
|
+
export declare function createBctrlApiClient(config: BctrlConfig, env?: NodeJS.ProcessEnv): BctrlApiClient;
|
package/dist/api/client.js
CHANGED
|
@@ -1,26 +1,23 @@
|
|
|
1
1
|
import { AuthError, CliError } from '../runtime/errors.js';
|
|
2
|
-
import {
|
|
3
|
-
|
|
2
|
+
import { clearCredential } from '../config/auth-store.js';
|
|
3
|
+
import { apiErrorFromResponse, isUnauthorizedApiError } from './errors.js';
|
|
4
|
+
export function createBctrlApiClient(config, env = process.env) {
|
|
4
5
|
return {
|
|
5
|
-
get: (path, options) => requestJson(config, 'GET', path, options),
|
|
6
|
-
post: (path, options) => requestJson(config, 'POST', path, options),
|
|
7
|
-
patch: (path, options) => requestJson(config, 'PATCH', path, options),
|
|
8
|
-
put: (path, options) => requestJson(config, 'PUT', path, options),
|
|
9
|
-
delete: (path, options) => requestJson(config, 'DELETE', path, options),
|
|
10
|
-
download: (path, options) => requestBinary(config, 'GET', path, options),
|
|
11
|
-
streamText: (path, options) => requestTextStream(config, path, options),
|
|
12
|
-
uploadFile: (path, options) => uploadFile(config, path, options),
|
|
6
|
+
get: (path, options) => requestJson(config, env, 'GET', path, options),
|
|
7
|
+
post: (path, options) => requestJson(config, env, 'POST', path, options),
|
|
8
|
+
patch: (path, options) => requestJson(config, env, 'PATCH', path, options),
|
|
9
|
+
put: (path, options) => requestJson(config, env, 'PUT', path, options),
|
|
10
|
+
delete: (path, options) => requestJson(config, env, 'DELETE', path, options),
|
|
11
|
+
download: (path, options) => requestBinary(config, env, 'GET', path, options),
|
|
12
|
+
streamText: (path, options) => requestTextStream(config, env, path, options),
|
|
13
|
+
uploadFile: (path, options) => uploadFile(config, env, path, options),
|
|
13
14
|
};
|
|
14
15
|
}
|
|
15
|
-
async function requestJson(config, method, path, options) {
|
|
16
|
+
async function requestJson(config, env, method, path, options) {
|
|
16
17
|
if (!config.activeToken) {
|
|
17
18
|
throw new AuthError();
|
|
18
19
|
}
|
|
19
|
-
const headers =
|
|
20
|
-
accept: 'application/json',
|
|
21
|
-
authorization: `Bearer ${config.activeToken.token}`,
|
|
22
|
-
'user-agent': 'BCTRL CLI',
|
|
23
|
-
};
|
|
20
|
+
const headers = requestHeaders(config.activeToken.token, options, 'application/json');
|
|
24
21
|
if (options?.idempotencyKey) {
|
|
25
22
|
headers['idempotency-key'] = options.idempotencyKey;
|
|
26
23
|
}
|
|
@@ -35,7 +32,7 @@ async function requestJson(config, method, path, options) {
|
|
|
35
32
|
body,
|
|
36
33
|
});
|
|
37
34
|
if (!response.ok) {
|
|
38
|
-
throw await apiErrorFromResponse(response);
|
|
35
|
+
throw await reconcileStoredCredentialAuthError(config, env, await apiErrorFromResponse(response));
|
|
39
36
|
}
|
|
40
37
|
if (response.status === 204) {
|
|
41
38
|
return { ok: true };
|
|
@@ -43,36 +40,29 @@ async function requestJson(config, method, path, options) {
|
|
|
43
40
|
const text = await response.text();
|
|
44
41
|
return (text ? JSON.parse(text) : { ok: true });
|
|
45
42
|
}
|
|
46
|
-
async function requestBinary(config, method, path, options) {
|
|
43
|
+
async function requestBinary(config, env, method, path, options) {
|
|
47
44
|
if (!config.activeToken) {
|
|
48
45
|
throw new AuthError();
|
|
49
46
|
}
|
|
50
47
|
const response = await fetch(buildUrl(config.apiBaseUrl, path, options?.query), {
|
|
51
48
|
method,
|
|
52
|
-
headers:
|
|
53
|
-
authorization: `Bearer ${config.activeToken.token}`,
|
|
54
|
-
'user-agent': 'BCTRL CLI',
|
|
55
|
-
},
|
|
49
|
+
headers: requestHeaders(config.activeToken.token, options),
|
|
56
50
|
});
|
|
57
51
|
if (!response.ok) {
|
|
58
|
-
throw await apiErrorFromResponse(response);
|
|
52
|
+
throw await reconcileStoredCredentialAuthError(config, env, await apiErrorFromResponse(response));
|
|
59
53
|
}
|
|
60
54
|
return new Uint8Array(await response.arrayBuffer());
|
|
61
55
|
}
|
|
62
|
-
async function requestTextStream(config, path, options) {
|
|
56
|
+
async function requestTextStream(config, env, path, options) {
|
|
63
57
|
if (!config.activeToken) {
|
|
64
58
|
throw new AuthError();
|
|
65
59
|
}
|
|
66
60
|
const response = await fetch(buildUrl(config.apiBaseUrl, path, options?.query), {
|
|
67
61
|
method: 'GET',
|
|
68
|
-
headers:
|
|
69
|
-
accept: 'text/event-stream',
|
|
70
|
-
authorization: `Bearer ${config.activeToken.token}`,
|
|
71
|
-
'user-agent': 'BCTRL CLI',
|
|
72
|
-
},
|
|
62
|
+
headers: requestHeaders(config.activeToken.token, options, 'text/event-stream'),
|
|
73
63
|
});
|
|
74
64
|
if (!response.ok) {
|
|
75
|
-
throw await apiErrorFromResponse(response);
|
|
65
|
+
throw await reconcileStoredCredentialAuthError(config, env, await apiErrorFromResponse(response));
|
|
76
66
|
}
|
|
77
67
|
if (!response.body) {
|
|
78
68
|
throw new CliError('BCTRL API response did not include a stream body');
|
|
@@ -99,7 +89,7 @@ async function requestTextStream(config, path, options) {
|
|
|
99
89
|
},
|
|
100
90
|
};
|
|
101
91
|
}
|
|
102
|
-
async function uploadFile(config, path, options) {
|
|
92
|
+
async function uploadFile(config, env, path, options) {
|
|
103
93
|
if (!config.activeToken) {
|
|
104
94
|
throw new AuthError();
|
|
105
95
|
}
|
|
@@ -110,18 +100,28 @@ async function uploadFile(config, path, options) {
|
|
|
110
100
|
}
|
|
111
101
|
const response = await fetch(buildUrl(config.apiBaseUrl, path, options.query), {
|
|
112
102
|
method: 'POST',
|
|
113
|
-
headers:
|
|
114
|
-
accept: 'application/json',
|
|
115
|
-
authorization: `Bearer ${config.activeToken.token}`,
|
|
116
|
-
'user-agent': 'BCTRL CLI',
|
|
117
|
-
},
|
|
103
|
+
headers: requestHeaders(config.activeToken.token, options, 'application/json'),
|
|
118
104
|
body: form,
|
|
119
105
|
});
|
|
120
106
|
if (!response.ok) {
|
|
121
|
-
throw await apiErrorFromResponse(response);
|
|
107
|
+
throw await reconcileStoredCredentialAuthError(config, env, await apiErrorFromResponse(response));
|
|
122
108
|
}
|
|
123
109
|
return (await response.json());
|
|
124
110
|
}
|
|
111
|
+
async function reconcileStoredCredentialAuthError(config, env, error) {
|
|
112
|
+
if (config.activeToken?.source === 'stored' && isUnauthorizedApiError(error)) {
|
|
113
|
+
await clearCredential(env, config.apiBaseUrl);
|
|
114
|
+
}
|
|
115
|
+
return error;
|
|
116
|
+
}
|
|
117
|
+
function requestHeaders(token, options, accept) {
|
|
118
|
+
return {
|
|
119
|
+
...(accept ? { accept } : {}),
|
|
120
|
+
authorization: `Bearer ${token}`,
|
|
121
|
+
'user-agent': 'BCTRL CLI',
|
|
122
|
+
...(options?.actingSubaccountId ? { 'BCTRL-Subaccount-Id': options.actingSubaccountId } : {}),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
125
|
function buildUrl(baseUrl, path, query) {
|
|
126
126
|
const url = new URL(`${baseUrl}${path.startsWith('/') ? path : `/${path}`}`);
|
|
127
127
|
for (const [key, value] of Object.entries(query ?? {})) {
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
declare const DeviceStartResponseSchema: z.ZodObject<{
|
|
3
|
+
deviceCode: z.ZodString;
|
|
4
|
+
userCode: z.ZodString;
|
|
5
|
+
verificationUri: z.ZodString;
|
|
6
|
+
verificationUriComplete: z.ZodString;
|
|
7
|
+
expiresIn: z.ZodNumber;
|
|
8
|
+
interval: z.ZodNumber;
|
|
9
|
+
}, z.core.$strict>;
|
|
10
|
+
declare const DeviceSessionRevokeResponseSchema: z.ZodObject<{
|
|
11
|
+
revoked: z.ZodBoolean;
|
|
12
|
+
}, z.core.$strict>;
|
|
13
|
+
export type DeviceStartResponse = z.infer<typeof DeviceStartResponseSchema>;
|
|
14
|
+
type DeviceSessionRevokeResponse = z.infer<typeof DeviceSessionRevokeResponseSchema>;
|
|
15
|
+
/**
|
|
16
|
+
* Poll outcome. The control-plane returns the RFC 8628 polling states as a 200
|
|
17
|
+
* status union; a 429 (rate limiter) is surfaced as `rate_limited` so the poll
|
|
18
|
+
* loop can back off rather than treating it as a hard failure.
|
|
19
|
+
*/
|
|
20
|
+
export type DevicePollResult = {
|
|
21
|
+
status: 'authorization_pending';
|
|
22
|
+
} | {
|
|
23
|
+
status: 'access_denied';
|
|
24
|
+
} | {
|
|
25
|
+
status: 'expired_token';
|
|
26
|
+
} | {
|
|
27
|
+
status: 'rate_limited';
|
|
28
|
+
} | {
|
|
29
|
+
status: 'complete';
|
|
30
|
+
token: string;
|
|
31
|
+
};
|
|
32
|
+
export declare function startDeviceAuth(apiBaseUrl: string, body?: {
|
|
33
|
+
clientName?: string;
|
|
34
|
+
clientKind?: 'cli' | 'mcp';
|
|
35
|
+
deviceName?: string;
|
|
36
|
+
}): Promise<DeviceStartResponse>;
|
|
37
|
+
export declare function pollDeviceAuth(apiBaseUrl: string, deviceCode: string): Promise<DevicePollResult>;
|
|
38
|
+
export declare function revokeDeviceSession(apiBaseUrl: string, token: string): Promise<DeviceSessionRevokeResponse>;
|
|
39
|
+
export {};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { CliError } from '../runtime/errors.js';
|
|
3
|
+
import { apiErrorFromResponse } from './errors.js';
|
|
4
|
+
// Hand-written client for the OAuth 2.0 device-authorization endpoints (RFC 8628).
|
|
5
|
+
// These are unauthenticated (the CLI has no credential yet) and intentionally kept
|
|
6
|
+
// out of the generated public OpenAPI/SDK (docs-hidden).
|
|
7
|
+
const DeviceStartResponseSchema = z
|
|
8
|
+
.object({
|
|
9
|
+
deviceCode: z.string().min(1),
|
|
10
|
+
userCode: z.string().min(1),
|
|
11
|
+
verificationUri: z.string().min(1),
|
|
12
|
+
verificationUriComplete: z.string().min(1),
|
|
13
|
+
expiresIn: z.number().int().positive(),
|
|
14
|
+
interval: z.number().int().positive(),
|
|
15
|
+
})
|
|
16
|
+
.strict();
|
|
17
|
+
const DeviceTokenResponseSchema = z
|
|
18
|
+
.object({
|
|
19
|
+
status: z.enum(['authorization_pending', 'access_denied', 'expired_token', 'complete']),
|
|
20
|
+
token: z.string().optional(),
|
|
21
|
+
})
|
|
22
|
+
.strict();
|
|
23
|
+
const DeviceSessionRevokeResponseSchema = z.object({ revoked: z.boolean() }).strict();
|
|
24
|
+
function deviceEndpoint(apiBaseUrl, path) {
|
|
25
|
+
return new URL(`${apiBaseUrl.replace(/\/+$/, '')}${path}`);
|
|
26
|
+
}
|
|
27
|
+
export async function startDeviceAuth(apiBaseUrl, body = {}) {
|
|
28
|
+
const response = await fetch(deviceEndpoint(apiBaseUrl, '/auth/device'), {
|
|
29
|
+
method: 'POST',
|
|
30
|
+
headers: {
|
|
31
|
+
accept: 'application/json',
|
|
32
|
+
'content-type': 'application/json',
|
|
33
|
+
'user-agent': 'BCTRL CLI',
|
|
34
|
+
},
|
|
35
|
+
body: JSON.stringify(body),
|
|
36
|
+
});
|
|
37
|
+
if (!response.ok) {
|
|
38
|
+
throw await apiErrorFromResponse(response, 'BCTRL device login');
|
|
39
|
+
}
|
|
40
|
+
const parsed = DeviceStartResponseSchema.safeParse(await response.json());
|
|
41
|
+
if (!parsed.success) {
|
|
42
|
+
throw new CliError(`BCTRL device login returned an unexpected response: ${parsed.error.message}`);
|
|
43
|
+
}
|
|
44
|
+
return parsed.data;
|
|
45
|
+
}
|
|
46
|
+
export async function pollDeviceAuth(apiBaseUrl, deviceCode) {
|
|
47
|
+
const response = await fetch(deviceEndpoint(apiBaseUrl, '/auth/device/token'), {
|
|
48
|
+
method: 'POST',
|
|
49
|
+
headers: {
|
|
50
|
+
accept: 'application/json',
|
|
51
|
+
'content-type': 'application/json',
|
|
52
|
+
'user-agent': 'BCTRL CLI',
|
|
53
|
+
},
|
|
54
|
+
body: JSON.stringify({ deviceCode }),
|
|
55
|
+
});
|
|
56
|
+
// The poll route is rate-limited; a 429 is expected under fast polling and just
|
|
57
|
+
// means "back off and keep waiting", not a terminal error.
|
|
58
|
+
if (response.status === 429) {
|
|
59
|
+
return { status: 'rate_limited' };
|
|
60
|
+
}
|
|
61
|
+
if (!response.ok) {
|
|
62
|
+
throw await apiErrorFromResponse(response, 'BCTRL device login');
|
|
63
|
+
}
|
|
64
|
+
const parsed = DeviceTokenResponseSchema.safeParse(await response.json());
|
|
65
|
+
if (!parsed.success) {
|
|
66
|
+
throw new CliError(`BCTRL device login returned an unexpected response: ${parsed.error.message}`);
|
|
67
|
+
}
|
|
68
|
+
if (parsed.data.status === 'complete') {
|
|
69
|
+
if (!parsed.data.token) {
|
|
70
|
+
throw new CliError('BCTRL device login completed without returning a token.');
|
|
71
|
+
}
|
|
72
|
+
return { status: 'complete', token: parsed.data.token };
|
|
73
|
+
}
|
|
74
|
+
return { status: parsed.data.status };
|
|
75
|
+
}
|
|
76
|
+
export async function revokeDeviceSession(apiBaseUrl, token) {
|
|
77
|
+
const response = await fetch(deviceEndpoint(apiBaseUrl, '/auth/device/session'), {
|
|
78
|
+
method: 'DELETE',
|
|
79
|
+
headers: {
|
|
80
|
+
accept: 'application/json',
|
|
81
|
+
authorization: `Bearer ${token}`,
|
|
82
|
+
'user-agent': 'BCTRL CLI',
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
if (!response.ok) {
|
|
86
|
+
throw await apiErrorFromResponse(response, 'BCTRL device logout');
|
|
87
|
+
}
|
|
88
|
+
const parsed = DeviceSessionRevokeResponseSchema.safeParse(await response.json());
|
|
89
|
+
if (!parsed.success) {
|
|
90
|
+
throw new CliError(`BCTRL device logout returned an unexpected response: ${parsed.error.message}`);
|
|
91
|
+
}
|
|
92
|
+
return parsed.data;
|
|
93
|
+
}
|
package/dist/api/errors.d.ts
CHANGED
package/dist/api/errors.js
CHANGED
|
@@ -16,6 +16,14 @@ export async function apiErrorFromResponse(response, context = 'BCTRL API reques
|
|
|
16
16
|
apiError,
|
|
17
17
|
});
|
|
18
18
|
}
|
|
19
|
+
export function isUnauthorizedApiError(error) {
|
|
20
|
+
if (!(error instanceof CliError))
|
|
21
|
+
return false;
|
|
22
|
+
return (error.apiError?.status === 401 &&
|
|
23
|
+
(error.apiError.code === 'auth.required' ||
|
|
24
|
+
error.apiError.code === 'auth.invalid' ||
|
|
25
|
+
error.apiError.code === undefined));
|
|
26
|
+
}
|
|
19
27
|
function parseErrorBody(bodyText) {
|
|
20
28
|
if (!bodyText)
|
|
21
29
|
return {};
|
package/dist/bin/bctrl.js
CHANGED
|
File without changes
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
|
-
import { readJsonFile } from '../shared/io.js';
|
|
3
2
|
import { addOutputFlags } from '../shared/output.js';
|
|
4
|
-
import { createOperationDeleteCommand, createOperationJsonBodyCommand, createOperationListCommand, createOperationViewCommand, outputFlags, requestOperationAndPrint, } from '../shared/operation.js';
|
|
3
|
+
import { actingSubaccountOption, buildOperationInput, createOperationDeleteCommand, createOperationJsonBodyCommand, createOperationListCommand, createOperationViewCommand, outputFlags, requestOperationAndPrint, } from '../shared/operation.js';
|
|
5
4
|
export function createAiCommand(factory) {
|
|
6
5
|
const command = new Command('ai').description('Manage AI models and credentials');
|
|
7
6
|
command.addCommand(createAiModelsCommand(factory));
|
|
@@ -50,11 +49,13 @@ function createAiCredentialsCommand(factory) {
|
|
|
50
49
|
}));
|
|
51
50
|
command.addCommand(createAiCredentialWriteCommand(factory, 'create', 'ai.credentials.create'));
|
|
52
51
|
command.addCommand(createAiCredentialWriteCommand(factory, 'patch', 'ai.credentials.update', ['credentialId']));
|
|
53
|
-
command.addCommand(addOutputFlags(new Command('test').description('Test an AI credential').argument('<credentialId>'))
|
|
54
|
-
|
|
52
|
+
command.addCommand(addOutputFlags(new Command('test').description('Test an AI credential').argument('<credentialId>'))
|
|
53
|
+
.option('--params <json>', 'Path/query parameters as a JSON object (inline, @file, or - for stdin)')
|
|
54
|
+
.action(async (credentialId, options) => {
|
|
55
|
+
await requestOperationAndPrint(factory, 'ai.credentials.test', await buildOperationInput('ai.credentials.test', options, {
|
|
55
56
|
pathParams: { credentialId },
|
|
56
57
|
output: outputFlags(options),
|
|
57
|
-
});
|
|
58
|
+
}));
|
|
58
59
|
}));
|
|
59
60
|
command.addCommand(createOperationDeleteCommand(factory, {
|
|
60
61
|
operationId: 'ai.credentials.delete',
|
|
@@ -76,22 +77,20 @@ function createAiCredentialWriteCommand(factory, name, operationId, argNames = [
|
|
|
76
77
|
.option('--status <status>', 'Credential status')
|
|
77
78
|
.option('--default-model <model>', 'Default model')
|
|
78
79
|
.option('--base-url <url>', 'Custom provider base URL')
|
|
79
|
-
.option('--subaccount-id <id>', 'Subaccount scope when using a parent/org key')
|
|
80
|
-
.option('--input <path>', 'Read full JSON request body from file, or - for stdin'),
|
|
80
|
+
.option('--subaccount-id <id>', 'Subaccount scope when using a parent/org key'),
|
|
81
81
|
body: async (_args, options) => {
|
|
82
|
-
if (typeof options.input === 'string') {
|
|
83
|
-
return (await readJsonFile(options.input));
|
|
84
|
-
}
|
|
85
82
|
return {
|
|
86
83
|
name: options.name,
|
|
87
84
|
provider: options.provider,
|
|
88
85
|
apiKey: options.apiKey,
|
|
89
86
|
status: options.status,
|
|
90
|
-
subaccountId: options.subaccountId,
|
|
91
87
|
defaultModel: options.defaultModel,
|
|
92
88
|
baseUrl: options.baseUrl,
|
|
93
89
|
};
|
|
94
90
|
},
|
|
91
|
+
actingSubaccountId: operationId === 'ai.credentials.create'
|
|
92
|
+
? (_args, options) => actingSubaccountOption()(options)
|
|
93
|
+
: undefined,
|
|
95
94
|
});
|
|
96
95
|
}
|
|
97
96
|
function parseBooleanFlag(value) {
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
|
-
import { readJsonFile } from '../shared/io.js';
|
|
3
2
|
import { createOperationDeleteCommand, createOperationJsonBodyCommand, createOperationListCommand, } from '../shared/operation.js';
|
|
4
3
|
export function createApiKeyCommand(factory) {
|
|
5
4
|
const command = new Command('api-key').description('Manage API keys');
|
|
@@ -8,12 +7,12 @@ export function createApiKeyCommand(factory) {
|
|
|
8
7
|
description: 'List API keys',
|
|
9
8
|
configure: (cmd) => cmd
|
|
10
9
|
.option('--subaccount-id <id>', 'Filter by subaccount id')
|
|
11
|
-
.option('--
|
|
10
|
+
.option('--type <type>', 'Filter by API key type: organization or subaccount')
|
|
12
11
|
.option('-L, --limit <number>', 'Maximum number of results to return')
|
|
13
12
|
.option('--cursor <cursor>', 'Pagination cursor'),
|
|
14
13
|
query: (options) => ({
|
|
15
14
|
subaccountId: typeof options.subaccountId === 'string' ? options.subaccountId : undefined,
|
|
16
|
-
|
|
15
|
+
type: typeof options.type === 'string' ? options.type : undefined,
|
|
17
16
|
limit: typeof options.limit === 'string' ? Number(options.limit) : undefined,
|
|
18
17
|
cursor: typeof options.cursor === 'string' ? options.cursor : undefined,
|
|
19
18
|
}),
|
|
@@ -25,12 +24,8 @@ export function createApiKeyCommand(factory) {
|
|
|
25
24
|
configure: (cmd) => cmd
|
|
26
25
|
.option('--name <name>', 'API key name')
|
|
27
26
|
.option('--subaccount-id <id>', 'Create a subaccount-scoped key')
|
|
28
|
-
.option('--expires-at <iso>', 'Expiration timestamp')
|
|
29
|
-
.option('--input <path>', 'Read full JSON request body from file, or - for stdin'),
|
|
27
|
+
.option('--expires-at <iso>', 'Expiration timestamp'),
|
|
30
28
|
body: async (_args, options) => {
|
|
31
|
-
if (typeof options.input === 'string') {
|
|
32
|
-
return (await readJsonFile(options.input));
|
|
33
|
-
}
|
|
34
29
|
return {
|
|
35
30
|
name: options.name,
|
|
36
31
|
subaccountId: options.subaccountId,
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { startDeviceAuth as defaultStartDeviceAuth, type DeviceStartResponse } from '../../api/device-auth.js';
|
|
2
|
+
export type DeviceLoginStartDeps = {
|
|
3
|
+
apiBaseUrl: string;
|
|
4
|
+
startDeviceAuth?: typeof defaultStartDeviceAuth;
|
|
5
|
+
};
|
|
6
|
+
export declare function startDeviceLoginSession(deps: DeviceLoginStartDeps): Promise<DeviceStartResponse>;
|