@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
|
@@ -1,80 +1,83 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
|
-
import {
|
|
3
|
-
import { outputFlags, requestOperationAndPrint } from '../shared/operation.js';
|
|
2
|
+
import { buildOperationInput, outputFlags, requestOperationAndPrint } from '../shared/operation.js';
|
|
4
3
|
import { addOutputFlags } from '../shared/output.js';
|
|
5
4
|
import { CliError } from '../../runtime/errors.js';
|
|
6
5
|
import { createSpaceListCommand } from './list.js';
|
|
7
6
|
export function createSpaceCommand(factory) {
|
|
8
7
|
const command = new Command('space').description('Manage BCTRL spaces');
|
|
9
8
|
command.addCommand(createSpaceListCommand(factory));
|
|
10
|
-
command.addCommand(addOutputFlags(new Command('get')
|
|
11
|
-
|
|
9
|
+
command.addCommand(addOutputFlags(new Command('get')
|
|
10
|
+
.description('View a space')
|
|
11
|
+
.argument('<spaceId>')
|
|
12
|
+
.option('--params <json>', 'Path/query parameters as a JSON object (inline, @file, or - for stdin)')).action(async (spaceId, options) => {
|
|
13
|
+
await requestOperationAndPrint(factory, 'spaces.get', await buildOperationInput('spaces.get', options, {
|
|
12
14
|
pathParams: { spaceId },
|
|
13
15
|
output: outputFlags(options),
|
|
14
|
-
});
|
|
16
|
+
}));
|
|
15
17
|
}));
|
|
16
18
|
command.addCommand(addOutputFlags(new Command('create')
|
|
17
19
|
.description('Create a space')
|
|
18
20
|
.option('--name <name>', 'Space name')
|
|
19
21
|
.option('--subaccount-id <id>', 'Subaccount id')
|
|
20
|
-
.option('--
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
:
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
},
|
|
22
|
+
.option('--params <json>', 'Path/query parameters as a JSON object (inline, @file, or - for stdin)')
|
|
23
|
+
.option('--body <json>', 'Request body as JSON (inline, @file, or - for stdin)')).action(async (options) => {
|
|
24
|
+
await requestOperationAndPrint(factory, 'spaces.create', await buildOperationInput('spaces.create', options, {
|
|
25
|
+
body: {
|
|
26
|
+
name: options.name,
|
|
27
|
+
},
|
|
28
|
+
actingSubaccountId: options.subaccountId,
|
|
28
29
|
output: outputFlags(options),
|
|
29
|
-
});
|
|
30
|
+
}));
|
|
30
31
|
}));
|
|
31
32
|
command.addCommand(addOutputFlags(new Command('patch')
|
|
32
33
|
.description('Edit a space')
|
|
33
34
|
.argument('<spaceId>')
|
|
34
35
|
.option('--name <name>', 'Space name')
|
|
35
|
-
.option('--
|
|
36
|
-
|
|
36
|
+
.option('--params <json>', 'Path/query parameters as a JSON object (inline, @file, or - for stdin)')
|
|
37
|
+
.option('--body <json>', 'Request body as JSON (inline, @file, or - for stdin)')).action(async (spaceId, options) => {
|
|
38
|
+
await requestOperationAndPrint(factory, 'spaces.update', await buildOperationInput('spaces.update', options, {
|
|
37
39
|
pathParams: { spaceId },
|
|
38
|
-
body:
|
|
39
|
-
? (await readJsonFile(options.input))
|
|
40
|
-
: { name: options.name },
|
|
40
|
+
body: { name: options.name },
|
|
41
41
|
output: outputFlags(options),
|
|
42
|
-
});
|
|
42
|
+
}));
|
|
43
43
|
}));
|
|
44
44
|
command.addCommand(addOutputFlags(new Command('delete')
|
|
45
45
|
.description('Delete a space')
|
|
46
46
|
.argument('<spaceId>')
|
|
47
|
-
.option('-y, --yes', 'Confirm deletion')
|
|
47
|
+
.option('-y, --yes', 'Confirm deletion')
|
|
48
|
+
.option('--params <json>', 'Path/query parameters as a JSON object (inline, @file, or - for stdin)')).action(async (spaceId, options) => {
|
|
48
49
|
if (options.yes !== true) {
|
|
49
50
|
throw new CliError('Refusing to delete without --yes');
|
|
50
51
|
}
|
|
51
|
-
await requestOperationAndPrint(factory, 'spaces.delete', {
|
|
52
|
+
await requestOperationAndPrint(factory, 'spaces.delete', await buildOperationInput('spaces.delete', options, {
|
|
52
53
|
pathParams: { spaceId },
|
|
53
54
|
output: outputFlags(options),
|
|
54
|
-
});
|
|
55
|
+
}));
|
|
55
56
|
}));
|
|
56
57
|
command.addCommand(createSpaceEnvironmentCommand(factory));
|
|
57
58
|
return command;
|
|
58
59
|
}
|
|
59
60
|
function createSpaceEnvironmentCommand(factory) {
|
|
60
61
|
const command = new Command('env').description('Manage a space environment');
|
|
61
|
-
command.addCommand(addOutputFlags(new Command('get')
|
|
62
|
-
|
|
62
|
+
command.addCommand(addOutputFlags(new Command('get')
|
|
63
|
+
.description('Get a space environment')
|
|
64
|
+
.argument('<spaceId>')
|
|
65
|
+
.option('--params <json>', 'Path/query parameters as a JSON object (inline, @file, or - for stdin)')).action(async (spaceId, options) => {
|
|
66
|
+
await requestOperationAndPrint(factory, 'spaces.environment.get', await buildOperationInput('spaces.environment.get', options, {
|
|
63
67
|
pathParams: { spaceId },
|
|
64
68
|
output: outputFlags(options),
|
|
65
|
-
});
|
|
69
|
+
}));
|
|
66
70
|
}));
|
|
67
71
|
command.addCommand(addOutputFlags(new Command('patch')
|
|
68
72
|
.description('Update a space environment')
|
|
69
73
|
.argument('<spaceId>')
|
|
70
|
-
.option('--
|
|
71
|
-
|
|
74
|
+
.option('--params <json>', 'Path/query parameters as a JSON object (inline, @file, or - for stdin)')
|
|
75
|
+
.option('--body <json>', 'Request body as JSON (inline, @file, or - for stdin)')).action(async (spaceId, options) => {
|
|
76
|
+
await requestOperationAndPrint(factory, 'spaces.environment.update', await buildOperationInput('spaces.environment.update', options, {
|
|
72
77
|
pathParams: { spaceId },
|
|
73
|
-
body:
|
|
74
|
-
? (await readJsonFile(options.input))
|
|
75
|
-
: {},
|
|
78
|
+
body: {},
|
|
76
79
|
output: outputFlags(options),
|
|
77
|
-
});
|
|
80
|
+
}));
|
|
78
81
|
}));
|
|
79
82
|
return command;
|
|
80
83
|
}
|
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
|
-
import { addPaginationFlags, outputFlags, requestOperationAndPrint } from '../shared/operation.js';
|
|
2
|
+
import { addPaginationFlags, buildOperationInput, outputFlags, requestOperationAndPrint, } from '../shared/operation.js';
|
|
3
3
|
import { addOutputFlags } from '../shared/output.js';
|
|
4
4
|
export function createSpaceListCommand(factory) {
|
|
5
5
|
return addOutputFlags(addPaginationFlags(new Command('list')
|
|
6
6
|
.description('List spaces')
|
|
7
|
-
.option('--subaccount <id>', 'Filter by subaccount id')
|
|
8
|
-
|
|
7
|
+
.option('--subaccount <id>', 'Filter by subaccount id')
|
|
8
|
+
.option('--params <json>', 'Path/query parameters as a JSON object (inline, @file, or - for stdin)'))).action(async (options) => {
|
|
9
|
+
await requestOperationAndPrint(factory, 'spaces.list', await buildOperationInput('spaces.list', options, {
|
|
9
10
|
query: {
|
|
10
|
-
subaccountId: options.subaccount,
|
|
11
11
|
limit: options.limit,
|
|
12
12
|
cursor: options.cursor,
|
|
13
13
|
},
|
|
14
|
+
actingSubaccountId: options.subaccount,
|
|
14
15
|
output: outputFlags(options),
|
|
15
|
-
});
|
|
16
|
+
}));
|
|
16
17
|
});
|
|
17
18
|
}
|
|
@@ -2,7 +2,7 @@ import { Command } from 'commander';
|
|
|
2
2
|
import { readJsonFile } from '../shared/io.js';
|
|
3
3
|
import { parsePositiveInteger } from '../shared/options.js';
|
|
4
4
|
import { addOutputFlags } from '../shared/output.js';
|
|
5
|
-
import { addPaginationFlags, createOperationJsonBodyCommand, createOperationListCommand, createOperationViewCommand, outputFlags, requestOperationAndPrint, } from '../shared/operation.js';
|
|
5
|
+
import { addPaginationFlags, buildOperationInput, createOperationJsonBodyCommand, createOperationListCommand, createOperationViewCommand, outputFlags, requestOperationAndPrint, } from '../shared/operation.js';
|
|
6
6
|
export function createSubaccountCommand(factory) {
|
|
7
7
|
const command = new Command('subaccount').description('Manage subaccounts');
|
|
8
8
|
command.addCommand(createOperationListCommand(factory, {
|
|
@@ -37,11 +37,12 @@ export function createSubaccountCommand(factory) {
|
|
|
37
37
|
command.addCommand(addOutputFlags(new Command('archive')
|
|
38
38
|
.description('Archive a subaccount')
|
|
39
39
|
.argument('<subaccountId>')
|
|
40
|
-
.requiredOption('-y, --yes', 'Confirm archive')
|
|
41
|
-
|
|
40
|
+
.requiredOption('-y, --yes', 'Confirm archive')
|
|
41
|
+
.option('--params <json>', 'Path/query parameters as a JSON object (inline, @file, or - for stdin)')).action(async (subaccountId, options) => {
|
|
42
|
+
await requestOperationAndPrint(factory, 'subaccounts.archive', await buildOperationInput('subaccounts.archive', options, {
|
|
42
43
|
pathParams: { subaccountId },
|
|
43
44
|
output: outputFlags(options),
|
|
44
|
-
});
|
|
45
|
+
}));
|
|
45
46
|
}));
|
|
46
47
|
command.addCommand(createSubaccountUsageCommand(factory));
|
|
47
48
|
return command;
|
|
@@ -55,12 +56,8 @@ function createSubaccountWriteCommand(factory, name, operationId, argNames = [])
|
|
|
55
56
|
configure: (cmd) => cmd
|
|
56
57
|
.option('--name <name>', 'Subaccount name')
|
|
57
58
|
.option('--external-id <id>', 'External id')
|
|
58
|
-
.option('--metadata-file <path>', 'Metadata JSON file')
|
|
59
|
-
.option('--input <path>', 'Read full JSON request body from file, or - for stdin'),
|
|
59
|
+
.option('--metadata-file <path>', 'Metadata JSON file'),
|
|
60
60
|
body: async (_args, options) => {
|
|
61
|
-
if (typeof options.input === 'string') {
|
|
62
|
-
return (await readJsonFile(options.input));
|
|
63
|
-
}
|
|
64
61
|
return {
|
|
65
62
|
name: options.name,
|
|
66
63
|
externalId: options.externalId,
|
|
@@ -76,18 +73,19 @@ function createSubaccountUsageCommand(factory) {
|
|
|
76
73
|
.description('Show subaccount usage')
|
|
77
74
|
.argument('[id]')
|
|
78
75
|
.option('-L, --limit <number>', 'Maximum number of usage records to return', parsePositiveInteger)
|
|
79
|
-
.option('--cursor <cursor>', 'Pagination cursor')
|
|
76
|
+
.option('--cursor <cursor>', 'Pagination cursor')
|
|
77
|
+
.option('--params <json>', 'Path/query parameters as a JSON object (inline, @file, or - for stdin)')).action(async (id, options) => {
|
|
80
78
|
if (id) {
|
|
81
|
-
await requestOperationAndPrint(factory, 'subaccounts.get', {
|
|
79
|
+
await requestOperationAndPrint(factory, 'subaccounts.get', await buildOperationInput('subaccounts.get', options, {
|
|
82
80
|
pathParams: { subaccountId: id },
|
|
83
|
-
query: { include: 'usage' },
|
|
81
|
+
query: { include: ['usage'] },
|
|
84
82
|
output: outputFlags(options),
|
|
85
|
-
});
|
|
83
|
+
}));
|
|
86
84
|
return;
|
|
87
85
|
}
|
|
88
|
-
await requestOperationAndPrint(factory, 'subaccounts.usage.list', {
|
|
86
|
+
await requestOperationAndPrint(factory, 'subaccounts.usage.list', await buildOperationInput('subaccounts.usage.list', options, {
|
|
89
87
|
query: { limit: options.limit, cursor: options.cursor },
|
|
90
88
|
output: outputFlags(options),
|
|
91
|
-
});
|
|
89
|
+
}));
|
|
92
90
|
});
|
|
93
91
|
}
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import { addOutputFlags } from '../shared/output.js';
|
|
3
|
-
import { outputFlags, requestOperationAndPrint } from '../shared/operation.js';
|
|
3
|
+
import { buildOperationInput, outputFlags, requestOperationAndPrint } from '../shared/operation.js';
|
|
4
4
|
export function createUsageCommand(factory) {
|
|
5
5
|
const command = addOutputFlags(new Command('usage').description('Get organization usage'));
|
|
6
|
-
command
|
|
7
|
-
|
|
6
|
+
command
|
|
7
|
+
.option('--params <json>', 'Path/query parameters as a JSON object (inline, @file, or - for stdin)')
|
|
8
|
+
.action(async (options) => {
|
|
9
|
+
await requestOperationAndPrint(factory, 'usage.get', await buildOperationInput('usage.get', options, { output: outputFlags(options) }));
|
|
8
10
|
});
|
|
9
|
-
command.addCommand(addOutputFlags(new Command('get').description('Get organization usage'))
|
|
10
|
-
|
|
11
|
+
command.addCommand(addOutputFlags(new Command('get').description('Get organization usage'))
|
|
12
|
+
.option('--params <json>', 'Path/query parameters as a JSON object (inline, @file, or - for stdin)')
|
|
13
|
+
.action(async (options) => {
|
|
14
|
+
await requestOperationAndPrint(factory, 'usage.get', await buildOperationInput('usage.get', options, { output: outputFlags(options) }));
|
|
11
15
|
}));
|
|
12
16
|
return command;
|
|
13
17
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import { CliError } from '../../runtime/errors.js';
|
|
3
|
-
import {
|
|
3
|
+
import { readText } from '../shared/io.js';
|
|
4
4
|
import { parsePositiveInteger } from '../shared/options.js';
|
|
5
5
|
import { addOutputFlags } from '../shared/output.js';
|
|
6
|
-
import { createOperationDeleteCommand, createOperationJsonBodyCommand, createOperationListCommand, createOperationViewCommand, outputFlags, requestOperationAndPrint, } from '../shared/operation.js';
|
|
6
|
+
import { buildOperationInput, createOperationDeleteCommand, createOperationJsonBodyCommand, createOperationListCommand, createOperationViewCommand, outputFlags, requestOperationAndPrint, } from '../shared/operation.js';
|
|
7
7
|
export function createVaultCommand(factory) {
|
|
8
8
|
const command = new Command('vault').description('Manage vault secrets and TOTP codes');
|
|
9
9
|
command.addCommand(createOperationListCommand(factory, {
|
|
@@ -42,14 +42,15 @@ function createVaultReadCommand(factory) {
|
|
|
42
42
|
return addOutputFlags(new Command('read')
|
|
43
43
|
.description('Read a vault secret value')
|
|
44
44
|
.argument('<key>')
|
|
45
|
-
.option('--reveal', 'Allow printing a secret to an interactive terminal')
|
|
45
|
+
.option('--reveal', 'Allow printing a secret to an interactive terminal')
|
|
46
|
+
.option('--params <json>', 'Path/query parameters as a JSON object (inline, @file, or - for stdin)')).action(async (key, options) => {
|
|
46
47
|
if (factory.io.isStdoutTTY() && options.reveal !== true) {
|
|
47
48
|
throw new CliError('Refusing to print a secret to a terminal without --reveal');
|
|
48
49
|
}
|
|
49
|
-
await requestOperationAndPrint(factory, 'vault.secrets.value', {
|
|
50
|
+
await requestOperationAndPrint(factory, 'vault.secrets.value', await buildOperationInput('vault.secrets.value', options, {
|
|
50
51
|
pathParams: { key },
|
|
51
52
|
output: outputFlags(options),
|
|
52
|
-
});
|
|
53
|
+
}));
|
|
53
54
|
});
|
|
54
55
|
}
|
|
55
56
|
function createVaultSetCommand(factory) {
|
|
@@ -68,12 +69,8 @@ function createVaultSetCommand(factory) {
|
|
|
68
69
|
.option('--totp-secret <secret>', 'TOTP seed')
|
|
69
70
|
.option('--label <label>', 'Display label')
|
|
70
71
|
.option('--origin <origin...>', 'Allowed origin')
|
|
71
|
-
.option('--origin-pattern <pattern...>', 'Allowed origin pattern')
|
|
72
|
-
.option('--input <path>', 'Read full JSON request body from file, or - for stdin'),
|
|
72
|
+
.option('--origin-pattern <pattern...>', 'Allowed origin pattern'),
|
|
73
73
|
body: async (args, options) => {
|
|
74
|
-
if (typeof options.input === 'string') {
|
|
75
|
-
return (await readJsonFile(options.input));
|
|
76
|
-
}
|
|
77
74
|
const password = typeof options.passwordFromFile === 'string'
|
|
78
75
|
? (await readText(options.passwordFromFile)).trimEnd()
|
|
79
76
|
: options.password;
|
|
@@ -97,7 +94,7 @@ function createVaultSetCommand(factory) {
|
|
|
97
94
|
}
|
|
98
95
|
if (type === 'value') {
|
|
99
96
|
if (typeof value !== 'string') {
|
|
100
|
-
throw new CliError('Vault value secrets require --value, --value-from-file, or --
|
|
97
|
+
throw new CliError('Vault value secrets require --value, --value-from-file, or --body');
|
|
101
98
|
}
|
|
102
99
|
return {
|
|
103
100
|
...common,
|
|
@@ -106,7 +103,7 @@ function createVaultSetCommand(factory) {
|
|
|
106
103
|
};
|
|
107
104
|
}
|
|
108
105
|
if (typeof options.username !== 'string' || typeof password !== 'string') {
|
|
109
|
-
throw new CliError('Vault login secrets require --username and --password, --password-from-file, or --
|
|
106
|
+
throw new CliError('Vault login secrets require --username and --password, --password-from-file, or --body');
|
|
110
107
|
}
|
|
111
108
|
return {
|
|
112
109
|
...common,
|
|
@@ -137,12 +134,8 @@ function createVaultPatchCommand(factory) {
|
|
|
137
134
|
.option('--origin <origin...>', 'Allowed origin')
|
|
138
135
|
.option('--clear-origins', 'Remove origins')
|
|
139
136
|
.option('--origin-pattern <pattern...>', 'Allowed origin pattern')
|
|
140
|
-
.option('--clear-origin-patterns', 'Remove origin patterns')
|
|
141
|
-
.option('--input <path>', 'Read full JSON request body from file, or - for stdin'),
|
|
137
|
+
.option('--clear-origin-patterns', 'Remove origin patterns'),
|
|
142
138
|
body: async (_args, options) => {
|
|
143
|
-
if (typeof options.input === 'string') {
|
|
144
|
-
return (await readJsonFile(options.input));
|
|
145
|
-
}
|
|
146
139
|
const password = typeof options.passwordFromFile === 'string'
|
|
147
140
|
? (await readText(options.passwordFromFile)).trimEnd()
|
|
148
141
|
: options.password;
|
|
@@ -159,7 +152,7 @@ function createVaultPatchCommand(factory) {
|
|
|
159
152
|
originPatterns: options.clearOriginPatterns === true ? null : options.originPattern,
|
|
160
153
|
};
|
|
161
154
|
if (!Object.values(body).some((value) => value !== undefined)) {
|
|
162
|
-
throw new CliError('Vault patch requires at least one field or --
|
|
155
|
+
throw new CliError('Vault patch requires at least one field or --body');
|
|
163
156
|
}
|
|
164
157
|
return body;
|
|
165
158
|
},
|
|
@@ -168,10 +161,11 @@ function createVaultPatchCommand(factory) {
|
|
|
168
161
|
function createVaultTotpCommand(factory) {
|
|
169
162
|
return addOutputFlags(new Command('totp')
|
|
170
163
|
.description('Generate the current TOTP code for a vault secret')
|
|
171
|
-
.argument('<key>')
|
|
172
|
-
|
|
164
|
+
.argument('<key>')
|
|
165
|
+
.option('--params <json>', 'Path/query parameters as a JSON object (inline, @file, or - for stdin)')).action(async (key, options) => {
|
|
166
|
+
await requestOperationAndPrint(factory, 'vault.secrets.totp', await buildOperationInput('vault.secrets.totp', options, {
|
|
173
167
|
pathParams: { key },
|
|
174
168
|
output: outputFlags(options),
|
|
175
|
-
});
|
|
169
|
+
}));
|
|
176
170
|
});
|
|
177
171
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
import { type SecretBackend } from './secret-store.js';
|
|
2
3
|
export declare const StoredWhoamiSchema: z.ZodObject<{
|
|
3
4
|
email: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
4
5
|
scope: z.ZodEnum<{
|
|
@@ -7,14 +8,33 @@ export declare const StoredWhoamiSchema: z.ZodObject<{
|
|
|
7
8
|
}>;
|
|
8
9
|
organizationId: z.ZodString;
|
|
9
10
|
subaccountId: z.ZodNullable<z.ZodString>;
|
|
10
|
-
defaultSpaceId: z.ZodString
|
|
11
|
+
defaultSpaceId: z.ZodNullable<z.ZodString>;
|
|
12
|
+
effectiveScope: z.ZodObject<{
|
|
13
|
+
scope: z.ZodEnum<{
|
|
14
|
+
organization: "organization";
|
|
15
|
+
subaccount: "subaccount";
|
|
16
|
+
}>;
|
|
17
|
+
organizationId: z.ZodString;
|
|
18
|
+
subaccountId: z.ZodNullable<z.ZodString>;
|
|
19
|
+
defaultSpaceId: z.ZodNullable<z.ZodString>;
|
|
20
|
+
}, z.core.$strict>;
|
|
11
21
|
keyId: z.ZodString;
|
|
12
22
|
plan: z.ZodString;
|
|
13
23
|
}, z.core.$strict>;
|
|
14
24
|
export type StoredWhoami = z.infer<typeof StoredWhoamiSchema>;
|
|
15
|
-
|
|
25
|
+
/** How the stored credential was obtained. `device` is reserved for the device-login flow. */
|
|
26
|
+
export declare const AuthMethodSchema: z.ZodEnum<{
|
|
27
|
+
"api-key": "api-key";
|
|
28
|
+
device: "device";
|
|
29
|
+
}>;
|
|
30
|
+
export type AuthMethod = z.infer<typeof AuthMethodSchema>;
|
|
31
|
+
/**
|
|
32
|
+
* On-disk credential metadata. This file NEVER contains the secret — the token
|
|
33
|
+
* lives in the OS keychain (or a separate 0600 secret file when keychain is
|
|
34
|
+
* unavailable). See secret-store.ts.
|
|
35
|
+
*/
|
|
36
|
+
export declare const StoredAuthMetadataSchema: z.ZodObject<{
|
|
16
37
|
apiBaseUrl: z.ZodString;
|
|
17
|
-
token: z.ZodString;
|
|
18
38
|
whoami: z.ZodObject<{
|
|
19
39
|
email: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
20
40
|
scope: z.ZodEnum<{
|
|
@@ -23,15 +43,61 @@ export declare const StoredAuthSchema: z.ZodObject<{
|
|
|
23
43
|
}>;
|
|
24
44
|
organizationId: z.ZodString;
|
|
25
45
|
subaccountId: z.ZodNullable<z.ZodString>;
|
|
26
|
-
defaultSpaceId: z.ZodString
|
|
46
|
+
defaultSpaceId: z.ZodNullable<z.ZodString>;
|
|
47
|
+
effectiveScope: z.ZodObject<{
|
|
48
|
+
scope: z.ZodEnum<{
|
|
49
|
+
organization: "organization";
|
|
50
|
+
subaccount: "subaccount";
|
|
51
|
+
}>;
|
|
52
|
+
organizationId: z.ZodString;
|
|
53
|
+
subaccountId: z.ZodNullable<z.ZodString>;
|
|
54
|
+
defaultSpaceId: z.ZodNullable<z.ZodString>;
|
|
55
|
+
}, z.core.$strict>;
|
|
27
56
|
keyId: z.ZodString;
|
|
28
57
|
plan: z.ZodString;
|
|
29
58
|
}, z.core.$strict>;
|
|
59
|
+
method: z.ZodEnum<{
|
|
60
|
+
"api-key": "api-key";
|
|
61
|
+
device: "device";
|
|
62
|
+
}>;
|
|
63
|
+
backend: z.ZodEnum<{
|
|
64
|
+
keychain: "keychain";
|
|
65
|
+
file: "file";
|
|
66
|
+
}>;
|
|
30
67
|
createdAt: z.ZodString;
|
|
31
68
|
validatedAt: z.ZodString;
|
|
32
69
|
}, z.core.$strict>;
|
|
33
|
-
export type
|
|
34
|
-
|
|
35
|
-
export
|
|
36
|
-
|
|
37
|
-
|
|
70
|
+
export type StoredAuthMetadata = z.infer<typeof StoredAuthMetadataSchema>;
|
|
71
|
+
/** Metadata + the resolved secret, assembled in memory only. */
|
|
72
|
+
export type StoredCredential = StoredAuthMetadata & {
|
|
73
|
+
token: string;
|
|
74
|
+
};
|
|
75
|
+
export declare function normalizeApiBaseUrl(url: string): string;
|
|
76
|
+
export declare function readStoredMetadata(env?: NodeJS.ProcessEnv): Promise<StoredAuthMetadata | null>;
|
|
77
|
+
/**
|
|
78
|
+
* Resolve the full stored credential (metadata + secret) for the given active
|
|
79
|
+
* API base URL. Returns null when there is no metadata, when it is for a
|
|
80
|
+
* different base URL, or when the secret is missing (e.g. the keychain entry was
|
|
81
|
+
* deleted out from under a stale metadata file) — all of which mean "logged out".
|
|
82
|
+
*/
|
|
83
|
+
export declare function loadCredential(env?: NodeJS.ProcessEnv, activeApiBaseUrl?: string): Promise<StoredCredential | null>;
|
|
84
|
+
export type SaveCredentialResult = {
|
|
85
|
+
path: string;
|
|
86
|
+
backend: SecretBackend;
|
|
87
|
+
keychainFallback: boolean;
|
|
88
|
+
};
|
|
89
|
+
export declare function saveCredential(input: {
|
|
90
|
+
apiBaseUrl: string;
|
|
91
|
+
token: string;
|
|
92
|
+
whoami: StoredWhoami;
|
|
93
|
+
method: AuthMethod;
|
|
94
|
+
createdAt: string;
|
|
95
|
+
validatedAt: string;
|
|
96
|
+
}, env?: NodeJS.ProcessEnv): Promise<SaveCredentialResult>;
|
|
97
|
+
/**
|
|
98
|
+
* Remove a stored credential. Deletes the secret from BOTH backends keyed by the
|
|
99
|
+
* (normalized) API base URL — independent of whether the metadata file exists,
|
|
100
|
+
* so a corrupt/missing metadata file can never orphan the secret — then removes
|
|
101
|
+
* the metadata file. Returns whether anything was removed.
|
|
102
|
+
*/
|
|
103
|
+
export declare function clearCredential(env: NodeJS.ProcessEnv | undefined, apiBaseUrl: string): Promise<boolean>;
|
|
@@ -1,61 +1,164 @@
|
|
|
1
|
-
import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
1
|
+
import { mkdir, readFile, rm, stat, writeFile } from 'node:fs/promises';
|
|
2
2
|
import { dirname, join } from 'node:path';
|
|
3
|
-
import { homedir } from 'node:os';
|
|
4
3
|
import { z } from 'zod';
|
|
5
|
-
|
|
4
|
+
import { getMetadataFilePath } from './paths.js';
|
|
5
|
+
import { deleteSecretAllBackends, resolveSecretStore, } from './secret-store.js';
|
|
6
|
+
const StoredEffectiveScopeSchema = z
|
|
7
|
+
.object({
|
|
8
|
+
scope: z.enum(['organization', 'subaccount']),
|
|
9
|
+
organizationId: z.string().min(1),
|
|
10
|
+
subaccountId: z.string().min(1).nullable(),
|
|
11
|
+
defaultSpaceId: z.string().min(1).nullable(),
|
|
12
|
+
})
|
|
13
|
+
.strict();
|
|
6
14
|
export const StoredWhoamiSchema = z
|
|
7
15
|
.object({
|
|
8
16
|
email: z.string().email().nullable().optional(),
|
|
9
17
|
scope: z.enum(['organization', 'subaccount']),
|
|
10
18
|
organizationId: z.string().min(1),
|
|
11
19
|
subaccountId: z.string().min(1).nullable(),
|
|
12
|
-
defaultSpaceId: z.string().min(1),
|
|
20
|
+
defaultSpaceId: z.string().min(1).nullable(),
|
|
21
|
+
effectiveScope: StoredEffectiveScopeSchema,
|
|
13
22
|
keyId: z.string().min(1),
|
|
14
23
|
plan: z.string().min(1),
|
|
15
24
|
})
|
|
16
25
|
.strict();
|
|
17
|
-
|
|
26
|
+
/** How the stored credential was obtained. `device` is reserved for the device-login flow. */
|
|
27
|
+
export const AuthMethodSchema = z.enum(['api-key', 'device']);
|
|
28
|
+
/**
|
|
29
|
+
* On-disk credential metadata. This file NEVER contains the secret — the token
|
|
30
|
+
* lives in the OS keychain (or a separate 0600 secret file when keychain is
|
|
31
|
+
* unavailable). See secret-store.ts.
|
|
32
|
+
*/
|
|
33
|
+
export const StoredAuthMetadataSchema = z
|
|
18
34
|
.object({
|
|
19
35
|
apiBaseUrl: z.string().url(),
|
|
20
|
-
token: z.string().min(1),
|
|
21
36
|
whoami: StoredWhoamiSchema,
|
|
37
|
+
method: AuthMethodSchema,
|
|
38
|
+
backend: z.enum(['keychain', 'file']),
|
|
22
39
|
createdAt: z.string().datetime(),
|
|
23
40
|
validatedAt: z.string().datetime(),
|
|
24
41
|
})
|
|
25
42
|
.strict();
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
? join(env.XDG_CONFIG_HOME.trim(), 'bctrl')
|
|
33
|
-
: join(homedir(), '.config', 'bctrl'));
|
|
34
|
-
return join(configDir, DEFAULT_AUTH_FILE_NAME);
|
|
43
|
+
const AUTH_WRITE_MARKER_FILE_NAME = 'auth.write';
|
|
44
|
+
const AUTH_WRITE_WAIT_TIMEOUT_MS = 2000;
|
|
45
|
+
const AUTH_WRITE_WAIT_INTERVAL_MS = 100;
|
|
46
|
+
const AUTH_WRITE_STALE_MS = 30000;
|
|
47
|
+
export function normalizeApiBaseUrl(url) {
|
|
48
|
+
return url.replace(/\/+$/, '');
|
|
35
49
|
}
|
|
36
|
-
export async function
|
|
37
|
-
const path =
|
|
50
|
+
export async function readStoredMetadata(env = process.env) {
|
|
51
|
+
const path = getMetadataFilePath(env);
|
|
38
52
|
try {
|
|
39
|
-
const
|
|
40
|
-
|
|
53
|
+
const parsed = StoredAuthMetadataSchema.safeParse(JSON.parse(await readFile(path, 'utf8')));
|
|
54
|
+
// A schema mismatch means a legacy/corrupt metadata file (e.g. a pre-keychain
|
|
55
|
+
// auth.json that embedded a token). Treat it as "logged out" rather than
|
|
56
|
+
// throwing, so the CLI degrades to re-login instead of erroring on startup.
|
|
57
|
+
return parsed.success ? parsed.data : null;
|
|
41
58
|
}
|
|
42
59
|
catch (error) {
|
|
43
60
|
if (isFileNotFound(error))
|
|
44
61
|
return null;
|
|
62
|
+
if (error instanceof SyntaxError)
|
|
63
|
+
return null;
|
|
45
64
|
const reason = error instanceof Error ? error.message : String(error);
|
|
46
|
-
throw new Error(`Failed to read BCTRL auth
|
|
65
|
+
throw new Error(`Failed to read BCTRL auth metadata at ${path}: ${reason}`);
|
|
47
66
|
}
|
|
48
67
|
}
|
|
49
|
-
|
|
50
|
-
|
|
68
|
+
/**
|
|
69
|
+
* Resolve the full stored credential (metadata + secret) for the given active
|
|
70
|
+
* API base URL. Returns null when there is no metadata, when it is for a
|
|
71
|
+
* different base URL, or when the secret is missing (e.g. the keychain entry was
|
|
72
|
+
* deleted out from under a stale metadata file) — all of which mean "logged out".
|
|
73
|
+
*/
|
|
74
|
+
export async function loadCredential(env = process.env, activeApiBaseUrl) {
|
|
75
|
+
const meta = await readStoredMetadataAfterPendingWrite(env);
|
|
76
|
+
if (!meta)
|
|
77
|
+
return null;
|
|
78
|
+
if (activeApiBaseUrl &&
|
|
79
|
+
normalizeApiBaseUrl(meta.apiBaseUrl) !== normalizeApiBaseUrl(activeApiBaseUrl)) {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
const { store } = resolveSecretStore(env);
|
|
83
|
+
const token = await store.get(normalizeApiBaseUrl(meta.apiBaseUrl));
|
|
84
|
+
if (!token)
|
|
85
|
+
return null;
|
|
86
|
+
return { ...meta, token };
|
|
87
|
+
}
|
|
88
|
+
export async function saveCredential(input, env = process.env) {
|
|
89
|
+
await markCredentialWriteStarted(env);
|
|
90
|
+
const { store, keychainFallback } = resolveSecretStore(env);
|
|
91
|
+
try {
|
|
92
|
+
await store.set(normalizeApiBaseUrl(input.apiBaseUrl), input.token);
|
|
93
|
+
const metadata = {
|
|
94
|
+
apiBaseUrl: input.apiBaseUrl,
|
|
95
|
+
whoami: input.whoami,
|
|
96
|
+
method: input.method,
|
|
97
|
+
backend: store.backend,
|
|
98
|
+
createdAt: input.createdAt,
|
|
99
|
+
validatedAt: input.validatedAt,
|
|
100
|
+
};
|
|
101
|
+
const path = getMetadataFilePath(env);
|
|
102
|
+
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
103
|
+
await writeFile(path, `${JSON.stringify(metadata, null, 2)}\n`, { mode: 0o600 });
|
|
104
|
+
return { path, backend: store.backend, keychainFallback };
|
|
105
|
+
}
|
|
106
|
+
finally {
|
|
107
|
+
await clearCredentialWriteMarker(env);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Remove a stored credential. Deletes the secret from BOTH backends keyed by the
|
|
112
|
+
* (normalized) API base URL — independent of whether the metadata file exists,
|
|
113
|
+
* so a corrupt/missing metadata file can never orphan the secret — then removes
|
|
114
|
+
* the metadata file. Returns whether anything was removed.
|
|
115
|
+
*/
|
|
116
|
+
export async function clearCredential(env = process.env, apiBaseUrl) {
|
|
117
|
+
const secretRemoved = await deleteSecretAllBackends(normalizeApiBaseUrl(apiBaseUrl), env);
|
|
118
|
+
let metadataRemoved = false;
|
|
119
|
+
try {
|
|
120
|
+
await rm(getMetadataFilePath(env));
|
|
121
|
+
metadataRemoved = true;
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
if (!isFileNotFound(error))
|
|
125
|
+
throw error;
|
|
126
|
+
}
|
|
127
|
+
return secretRemoved || metadataRemoved;
|
|
128
|
+
}
|
|
129
|
+
function getAuthWriteMarkerPath(env = process.env) {
|
|
130
|
+
return join(dirname(getMetadataFilePath(env)), AUTH_WRITE_MARKER_FILE_NAME);
|
|
131
|
+
}
|
|
132
|
+
async function markCredentialWriteStarted(env) {
|
|
133
|
+
const path = getAuthWriteMarkerPath(env);
|
|
51
134
|
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
52
|
-
await writeFile(path,
|
|
53
|
-
|
|
135
|
+
await writeFile(path, new Date().toISOString(), { mode: 0o600 });
|
|
136
|
+
}
|
|
137
|
+
async function clearCredentialWriteMarker(env) {
|
|
138
|
+
try {
|
|
139
|
+
await rm(getAuthWriteMarkerPath(env));
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
if (!isFileNotFound(error))
|
|
143
|
+
throw error;
|
|
144
|
+
}
|
|
54
145
|
}
|
|
55
|
-
|
|
146
|
+
async function readStoredMetadataAfterPendingWrite(env) {
|
|
147
|
+
const startedAt = Date.now();
|
|
148
|
+
for (;;) {
|
|
149
|
+
const meta = await readStoredMetadata(env);
|
|
150
|
+
if (meta)
|
|
151
|
+
return meta;
|
|
152
|
+
if (!(await isCredentialWriteInProgress(env)) || Date.now() - startedAt >= AUTH_WRITE_WAIT_TIMEOUT_MS) {
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
await sleep(AUTH_WRITE_WAIT_INTERVAL_MS);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
async function isCredentialWriteInProgress(env) {
|
|
56
159
|
try {
|
|
57
|
-
await
|
|
58
|
-
return
|
|
160
|
+
const marker = await stat(getAuthWriteMarkerPath(env));
|
|
161
|
+
return Date.now() - marker.mtimeMs < AUTH_WRITE_STALE_MS;
|
|
59
162
|
}
|
|
60
163
|
catch (error) {
|
|
61
164
|
if (isFileNotFound(error))
|
|
@@ -63,6 +166,9 @@ export async function deleteStoredAuth(env = process.env) {
|
|
|
63
166
|
throw error;
|
|
64
167
|
}
|
|
65
168
|
}
|
|
169
|
+
function sleep(ms) {
|
|
170
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
171
|
+
}
|
|
66
172
|
function isFileNotFound(error) {
|
|
67
173
|
return (typeof error === 'object' &&
|
|
68
174
|
error !== null &&
|