@cardstack/boxel-cli 0.12.0-unstable.3 → 0.12.0-unstable.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/package.json
CHANGED
|
@@ -8,13 +8,15 @@ import { operatorRequest } from '../../lib/seed-auth.ts';
|
|
|
8
8
|
export { readKeyValue, type KeyValueSource } from '../../lib/key-value.ts';
|
|
9
9
|
|
|
10
10
|
// `boxel realm-server keys`: the realm server's system keys, the platform's
|
|
11
|
-
// own vendor keys that `_request-forward` attaches
|
|
11
|
+
// own vendor keys that `_request-forward` attaches, and with
|
|
12
|
+
// `--kind oauth-client` its platform clients, the OAuth apps Boxel registered
|
|
13
|
+
// with a vendor that connect mode offers everyone. Authorized with the realm
|
|
12
14
|
// secret seed. A value is typed at a masked prompt or read from stdin or a
|
|
13
15
|
// file, never taken as an argument, and no command prints one: the server
|
|
14
|
-
// never answers with a value. Every change
|
|
15
|
-
// the server records in the key's history next to what
|
|
16
|
-
// caller's address and when the operator token was
|
|
17
|
-
// as the operator typed it; nothing verifies it.
|
|
16
|
+
// never answers with a value. Every change to a system key carries an
|
|
17
|
+
// operator note, which the server records in the key's history next to what
|
|
18
|
+
// it observed itself: the caller's address and when the operator token was
|
|
19
|
+
// signed. A name in a note is as the operator typed it; nothing verifies it.
|
|
18
20
|
|
|
19
21
|
export type SystemKeyAuthMethod = 'header' | 'url-parameter';
|
|
20
22
|
export type SystemKeyCreditStrategy = 'openrouter' | 'none';
|
|
@@ -58,6 +60,38 @@ export interface NewSystemKey {
|
|
|
58
60
|
note: string;
|
|
59
61
|
}
|
|
60
62
|
|
|
63
|
+
export type SystemKeyKind = 'system' | 'oauth-client';
|
|
64
|
+
|
|
65
|
+
export interface PlatformClient {
|
|
66
|
+
id: string;
|
|
67
|
+
name: string;
|
|
68
|
+
label: string;
|
|
69
|
+
clientId: string;
|
|
70
|
+
grantType: string;
|
|
71
|
+
authorizeURL: string | null;
|
|
72
|
+
tokenURL: string;
|
|
73
|
+
scopes: string;
|
|
74
|
+
tokenAuth: string;
|
|
75
|
+
apiHosts: string[];
|
|
76
|
+
state: string;
|
|
77
|
+
createdAt: string;
|
|
78
|
+
updatedAt: string;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface NewPlatformClient {
|
|
82
|
+
name: string;
|
|
83
|
+
label: string;
|
|
84
|
+
secret: string;
|
|
85
|
+
oauth: {
|
|
86
|
+
clientId: string;
|
|
87
|
+
authorizeUrl: string;
|
|
88
|
+
tokenUrl: string;
|
|
89
|
+
scopes?: string;
|
|
90
|
+
tokenAuth?: 'client_secret_post' | 'client_secret_basic';
|
|
91
|
+
apiHosts: string[];
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
61
95
|
async function operatorFetch(
|
|
62
96
|
options: SystemKeysOptions,
|
|
63
97
|
path: string,
|
|
@@ -143,6 +177,94 @@ export async function deleteSystemKey(
|
|
|
143
177
|
).deleted;
|
|
144
178
|
}
|
|
145
179
|
|
|
180
|
+
export async function listPlatformClients(
|
|
181
|
+
options: SystemKeysOptions,
|
|
182
|
+
): Promise<PlatformClient[]> {
|
|
183
|
+
return (await operatorFetch(options, '_platform-oauth-clients', 'GET'))
|
|
184
|
+
.clients;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export async function addPlatformClient(
|
|
188
|
+
options: SystemKeysOptions,
|
|
189
|
+
client: NewPlatformClient,
|
|
190
|
+
): Promise<PlatformClient> {
|
|
191
|
+
return (
|
|
192
|
+
await operatorFetch(options, '_platform-oauth-clients', 'POST', client)
|
|
193
|
+
).client;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Replacing the secret also clears a client its token endpoint refused.
|
|
197
|
+
export async function replacePlatformClientSecret(
|
|
198
|
+
options: SystemKeysOptions,
|
|
199
|
+
name: string,
|
|
200
|
+
secret: string,
|
|
201
|
+
): Promise<PlatformClient> {
|
|
202
|
+
return (
|
|
203
|
+
await operatorFetch(
|
|
204
|
+
options,
|
|
205
|
+
`_platform-oauth-clients/${encodeURIComponent(name)}`,
|
|
206
|
+
'PATCH',
|
|
207
|
+
{ secret },
|
|
208
|
+
)
|
|
209
|
+
).client;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export async function deletePlatformClient(
|
|
213
|
+
options: SystemKeysOptions,
|
|
214
|
+
name: string,
|
|
215
|
+
): Promise<void> {
|
|
216
|
+
await operatorFetch(
|
|
217
|
+
options,
|
|
218
|
+
`_platform-oauth-clients/${encodeURIComponent(name)}`,
|
|
219
|
+
'DELETE',
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Deleting a platform client leaves every connection through it needing an
|
|
225
|
+
* administrator, and adding it again under the same name doesn't take them
|
|
226
|
+
* back. So the command shows the client and asks first, unless `--yes` says
|
|
227
|
+
* not to; without a terminal to ask in, `--yes` is required.
|
|
228
|
+
*/
|
|
229
|
+
export async function confirmPlatformClientDelete(
|
|
230
|
+
options: SystemKeysOptions,
|
|
231
|
+
name: string,
|
|
232
|
+
{
|
|
233
|
+
yes,
|
|
234
|
+
json,
|
|
235
|
+
isTTY = Boolean(process.stdin.isTTY),
|
|
236
|
+
confirm = async (question: string) =>
|
|
237
|
+
/^y(es)?$/i.test(await prompt(question)),
|
|
238
|
+
}: {
|
|
239
|
+
yes?: boolean;
|
|
240
|
+
json?: boolean;
|
|
241
|
+
isTTY?: boolean;
|
|
242
|
+
confirm?: (question: string) => Promise<boolean>;
|
|
243
|
+
},
|
|
244
|
+
): Promise<void> {
|
|
245
|
+
if (yes) {
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
if (json || !isTTY) {
|
|
249
|
+
throw new Error(
|
|
250
|
+
'Pass --yes to delete a platform client without a terminal to confirm in',
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
let client = (await listPlatformClients(options)).find(
|
|
254
|
+
(candidate) => candidate.name === name,
|
|
255
|
+
);
|
|
256
|
+
if (!client) {
|
|
257
|
+
throw new Error(`No platform client is named ${name}`);
|
|
258
|
+
}
|
|
259
|
+
printClient(client);
|
|
260
|
+
console.log(
|
|
261
|
+
'Every connection made through it will need an administrator, and adding the client again does not restore them.',
|
|
262
|
+
);
|
|
263
|
+
if (!(await confirm(`Delete the platform client ${name}? [y/N] `))) {
|
|
264
|
+
throw new Error('Not deleted');
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
146
268
|
// The id of the key named by `--id`, or of the key for `--destination`,
|
|
147
269
|
// compared as the server stores destinations (as the URL parser spells them).
|
|
148
270
|
export async function resolveSystemKeyId(
|
|
@@ -215,6 +337,22 @@ function printKey(key: SystemKey): void {
|
|
|
215
337
|
console.log(` updated: ${key.updatedAt}`);
|
|
216
338
|
}
|
|
217
339
|
|
|
340
|
+
function printClient(client: PlatformClient): void {
|
|
341
|
+
console.log(`${client.name}: ${client.label}`);
|
|
342
|
+
console.log(` id: ${client.id}`);
|
|
343
|
+
console.log(` client id: ${client.clientId}`);
|
|
344
|
+
console.log(` signs in at: ${client.authorizeURL}`);
|
|
345
|
+
console.log(` token endpoint: ${client.tokenURL} (${client.tokenAuth})`);
|
|
346
|
+
console.log(` scopes: ${client.scopes || 'none'}`);
|
|
347
|
+
console.log(
|
|
348
|
+
` offered for: ${client.apiHosts.length > 0 ? client.apiHosts.join(', ') : 'no API host'}`,
|
|
349
|
+
);
|
|
350
|
+
console.log(
|
|
351
|
+
` state: ${client.state === 'broken' ? 'broken: its token endpoint refused the secret; replace it' : client.state}`,
|
|
352
|
+
);
|
|
353
|
+
console.log(` updated: ${client.updatedAt}`);
|
|
354
|
+
}
|
|
355
|
+
|
|
218
356
|
function printHistory(history: SystemKeyChange[]): void {
|
|
219
357
|
if (history.length === 0) {
|
|
220
358
|
console.log(' history: none recorded');
|
|
@@ -229,8 +367,17 @@ function printHistory(history: SystemKeyChange[]): void {
|
|
|
229
367
|
}
|
|
230
368
|
}
|
|
231
369
|
|
|
232
|
-
interface KeysCliOptions {
|
|
370
|
+
export interface KeysCliOptions {
|
|
233
371
|
realmServer: string;
|
|
372
|
+
kind?: string;
|
|
373
|
+
name?: string;
|
|
374
|
+
label?: string;
|
|
375
|
+
clientId?: string;
|
|
376
|
+
authorizeUrl?: string;
|
|
377
|
+
tokenUrl?: string;
|
|
378
|
+
scopes?: string;
|
|
379
|
+
tokenAuth?: string;
|
|
380
|
+
apiHost?: string[];
|
|
234
381
|
id?: string;
|
|
235
382
|
destination?: string;
|
|
236
383
|
auth?: SystemKeyAuthMethod;
|
|
@@ -240,6 +387,7 @@ interface KeysCliOptions {
|
|
|
240
387
|
stdin?: boolean;
|
|
241
388
|
keyFile?: string;
|
|
242
389
|
note?: string;
|
|
390
|
+
yes?: boolean;
|
|
243
391
|
json?: boolean;
|
|
244
392
|
}
|
|
245
393
|
|
|
@@ -270,9 +418,71 @@ function output(json: boolean | undefined, value: unknown, print: () => void) {
|
|
|
270
418
|
}
|
|
271
419
|
}
|
|
272
420
|
|
|
421
|
+
function kindOf(options: KeysCliOptions): SystemKeyKind {
|
|
422
|
+
let kind = options.kind ?? 'system';
|
|
423
|
+
if (kind !== 'system' && kind !== 'oauth-client') {
|
|
424
|
+
throw new Error('--kind must be system or oauth-client');
|
|
425
|
+
}
|
|
426
|
+
return kind;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// A platform client is named by `--name`; a system key by `--id` or
|
|
430
|
+
// `--destination`. Each refuses the other's selectors.
|
|
431
|
+
function clientName(options: KeysCliOptions): string {
|
|
432
|
+
refuseNoteForClient(options);
|
|
433
|
+
if (options.id || options.destination) {
|
|
434
|
+
throw new Error(
|
|
435
|
+
'Name a platform client with --name, not --id or --destination',
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
if (!options.name) {
|
|
439
|
+
throw new Error('Name the platform client with --name');
|
|
440
|
+
}
|
|
441
|
+
return options.name;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// The server keeps no history of platform clients, so a note would be
|
|
445
|
+
// recorded nowhere.
|
|
446
|
+
export function refuseNoteForClient(options: KeysCliOptions): void {
|
|
447
|
+
if (options.note !== undefined) {
|
|
448
|
+
throw new Error(
|
|
449
|
+
"--note is recorded in a system key's history; the server keeps none for a platform client",
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
export function refuseClientOptions(options: KeysCliOptions): void {
|
|
455
|
+
let clientOnly = (
|
|
456
|
+
[
|
|
457
|
+
['name', '--name'],
|
|
458
|
+
['label', '--label'],
|
|
459
|
+
['clientId', '--client-id'],
|
|
460
|
+
['authorizeUrl', '--authorize-url'],
|
|
461
|
+
['tokenUrl', '--token-url'],
|
|
462
|
+
['scopes', '--scopes'],
|
|
463
|
+
['tokenAuth', '--token-auth'],
|
|
464
|
+
['apiHost', '--api-host'],
|
|
465
|
+
['yes', '--yes'],
|
|
466
|
+
] as const
|
|
467
|
+
).filter(([option]) => options[option] !== undefined);
|
|
468
|
+
if (clientOnly.length > 0) {
|
|
469
|
+
throw new Error(
|
|
470
|
+
`${clientOnly.map(([, flag]) => flag).join(', ')} ${clientOnly.length === 1 ? 'is' : 'are'} for a platform client: pass --kind oauth-client`,
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
const KIND_HELP =
|
|
476
|
+
'system (the default): a system key; oauth-client: a platform client, an OAuth app Boxel registered with a vendor';
|
|
477
|
+
|
|
478
|
+
function collect(value: string, previous: string[] = []): string[] {
|
|
479
|
+
return [...previous, value];
|
|
480
|
+
}
|
|
481
|
+
|
|
273
482
|
const SELECT_HELP = {
|
|
274
483
|
id: 'The key, by its id',
|
|
275
484
|
destination: 'The key, by its destination URL',
|
|
485
|
+
name: 'The platform client, by its name (with --kind oauth-client)',
|
|
276
486
|
};
|
|
277
487
|
|
|
278
488
|
function valueOptions(command: Command): Command {
|
|
@@ -289,16 +499,27 @@ export function registerKeysCommand(realmServer: Command): void {
|
|
|
289
499
|
let keys = realmServer
|
|
290
500
|
.command('keys')
|
|
291
501
|
.description(
|
|
292
|
-
"Manage the realm server's system keys
|
|
502
|
+
"Manage the realm server's system keys (the platform's own vendor keys that the proxy attaches) and, with --kind oauth-client, its platform clients. No command shows a value",
|
|
293
503
|
);
|
|
294
504
|
|
|
295
505
|
keys
|
|
296
506
|
.command('list')
|
|
297
|
-
.description('List the system keys, without
|
|
507
|
+
.description('List the system keys or platform clients, without secrets')
|
|
298
508
|
.requiredOption('--realm-server <url>', 'The realm server to ask')
|
|
509
|
+
.option('--kind <kind>', KIND_HELP)
|
|
299
510
|
.option('--json', 'Output JSON')
|
|
300
511
|
.action(async (options: KeysCliOptions) =>
|
|
301
512
|
run(async () => {
|
|
513
|
+
if (kindOf(options) === 'oauth-client') {
|
|
514
|
+
let clients = await listPlatformClients(await seedOptions(options));
|
|
515
|
+
output(options.json, clients, () => {
|
|
516
|
+
if (clients.length === 0) {
|
|
517
|
+
console.log('No platform clients.');
|
|
518
|
+
}
|
|
519
|
+
clients.forEach(printClient);
|
|
520
|
+
});
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
302
523
|
let list = await listSystemKeys(await seedOptions(options));
|
|
303
524
|
output(options.json, list, () => {
|
|
304
525
|
if (list.length === 0) {
|
|
@@ -312,14 +533,28 @@ export function registerKeysCommand(realmServer: Command): void {
|
|
|
312
533
|
keys
|
|
313
534
|
.command('show')
|
|
314
535
|
.description(
|
|
315
|
-
'Show one system key and its recent history, without
|
|
536
|
+
'Show one system key and its recent history, or one platform client, without secrets',
|
|
316
537
|
)
|
|
317
538
|
.requiredOption('--realm-server <url>', 'The realm server to ask')
|
|
539
|
+
.option('--kind <kind>', KIND_HELP)
|
|
318
540
|
.option('--id <id>', SELECT_HELP.id)
|
|
319
541
|
.option('--destination <url>', SELECT_HELP.destination)
|
|
542
|
+
.option('--name <name>', SELECT_HELP.name)
|
|
320
543
|
.option('--json', 'Output JSON')
|
|
321
544
|
.action(async (options: KeysCliOptions) =>
|
|
322
545
|
run(async () => {
|
|
546
|
+
if (kindOf(options) === 'oauth-client') {
|
|
547
|
+
let name = clientName(options);
|
|
548
|
+
let client = (
|
|
549
|
+
await listPlatformClients(await seedOptions(options))
|
|
550
|
+
).find((c) => c.name === name);
|
|
551
|
+
if (!client) {
|
|
552
|
+
throw new Error(`No platform client is named ${name}`);
|
|
553
|
+
}
|
|
554
|
+
output(options.json, client, () => printClient(client));
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
refuseClientOptions(options);
|
|
323
558
|
let seed = await seedOptions(options);
|
|
324
559
|
let shown = await showSystemKey(
|
|
325
560
|
seed,
|
|
@@ -336,35 +571,74 @@ export function registerKeysCommand(realmServer: Command): void {
|
|
|
336
571
|
keys
|
|
337
572
|
.command('add')
|
|
338
573
|
.description(
|
|
339
|
-
|
|
574
|
+
"Add a system key for a destination that has none, or with --kind oauth-client a platform client. The value (a platform client's client secret) is typed at a masked prompt, or read with --stdin or --key-file",
|
|
340
575
|
)
|
|
341
576
|
.requiredOption('--realm-server <url>', 'The realm server to change')
|
|
342
|
-
.
|
|
577
|
+
.option('--kind <kind>', KIND_HELP)
|
|
578
|
+
.option(
|
|
343
579
|
'--destination <url>',
|
|
344
580
|
'The URL the key is sent to; requests under it get the key',
|
|
345
581
|
)
|
|
346
|
-
.
|
|
582
|
+
.option(
|
|
347
583
|
'--credit-strategy <strategy>',
|
|
348
584
|
'How calls are charged: openrouter or none',
|
|
349
585
|
)
|
|
350
586
|
.option(
|
|
351
587
|
'--auth <method>',
|
|
352
|
-
'Where the key goes: header or url-parameter',
|
|
353
|
-
'header',
|
|
588
|
+
'Where the key goes: header (the default) or url-parameter',
|
|
354
589
|
)
|
|
355
590
|
.option(
|
|
356
591
|
'--param-name <name>',
|
|
357
592
|
'The header (default Authorization) or URL parameter (default key) that carries the key. A header always carries it as a Bearer token',
|
|
358
593
|
)
|
|
359
|
-
.option('--streaming', 'The destination supports streamed responses')
|
|
594
|
+
.option('--streaming', 'The destination supports streamed responses')
|
|
595
|
+
.option(
|
|
596
|
+
'--name <name>',
|
|
597
|
+
'A platform client: its name, lower-case letters, digits and dashes (such as github)',
|
|
598
|
+
)
|
|
599
|
+
.option(
|
|
600
|
+
'--label <label>',
|
|
601
|
+
'A platform client: the name connect mode shows (such as "Boxel (read-only)")',
|
|
602
|
+
)
|
|
603
|
+
.option('--client-id <id>', "A platform client: the vendor's client id")
|
|
604
|
+
.option(
|
|
605
|
+
'--authorize-url <url>',
|
|
606
|
+
"A platform client: the vendor's authorization page",
|
|
607
|
+
)
|
|
608
|
+
.option(
|
|
609
|
+
'--token-url <url>',
|
|
610
|
+
"A platform client: the vendor's token endpoint",
|
|
611
|
+
)
|
|
612
|
+
.option(
|
|
613
|
+
'--scopes <scopes>',
|
|
614
|
+
'A platform client: the scopes it asks for, space-separated (none for a GitHub App, whose permissions are its own)',
|
|
615
|
+
)
|
|
616
|
+
.option(
|
|
617
|
+
'--token-auth <method>',
|
|
618
|
+
'A platform client: client_secret_post (the default) or client_secret_basic',
|
|
619
|
+
)
|
|
620
|
+
.option(
|
|
621
|
+
'--api-host <url>',
|
|
622
|
+
'A platform client: an API host it serves, where connect mode offers it; repeat for more',
|
|
623
|
+
collect,
|
|
624
|
+
),
|
|
360
625
|
)
|
|
361
626
|
.option('--json', 'Output JSON')
|
|
362
627
|
.action(async (options: KeysCliOptions) =>
|
|
363
628
|
run(async () => {
|
|
364
|
-
if (
|
|
629
|
+
if (kindOf(options) === 'oauth-client') {
|
|
630
|
+
await addClientCommand(options);
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
refuseClientOptions(options);
|
|
634
|
+
if (!options.destination) {
|
|
635
|
+
throw new Error('A system key needs --destination');
|
|
636
|
+
}
|
|
637
|
+
if (!['openrouter', 'none'].includes(options.creditStrategy ?? '')) {
|
|
365
638
|
throw new Error('--credit-strategy must be openrouter or none');
|
|
366
639
|
}
|
|
367
|
-
|
|
640
|
+
let auth = options.auth ?? 'header';
|
|
641
|
+
if (!['header', 'url-parameter'].includes(auth)) {
|
|
368
642
|
throw new Error('--auth must be header or url-parameter');
|
|
369
643
|
}
|
|
370
644
|
let seed = await seedOptions(options);
|
|
@@ -373,8 +647,8 @@ export function registerKeysCommand(realmServer: Command): void {
|
|
|
373
647
|
stdinUsed: Boolean(options.stdin),
|
|
374
648
|
});
|
|
375
649
|
let key = await addSystemKey(seed, {
|
|
376
|
-
destination: options.destination
|
|
377
|
-
authMethod:
|
|
650
|
+
destination: options.destination,
|
|
651
|
+
authMethod: auth as SystemKeyAuthMethod,
|
|
378
652
|
paramName: options.paramName,
|
|
379
653
|
creditStrategy: options.creditStrategy!,
|
|
380
654
|
supportsStreaming: Boolean(options.streaming),
|
|
@@ -392,15 +666,31 @@ export function registerKeysCommand(realmServer: Command): void {
|
|
|
392
666
|
keys
|
|
393
667
|
.command('replace')
|
|
394
668
|
.description(
|
|
395
|
-
"Replace a system key's value, keeping its destination and settings",
|
|
669
|
+
"Replace a system key's value, keeping its destination and settings, or with --kind oauth-client a platform client's secret, which also clears a secret its token endpoint refused",
|
|
396
670
|
)
|
|
397
671
|
.requiredOption('--realm-server <url>', 'The realm server to change')
|
|
672
|
+
.option('--kind <kind>', KIND_HELP)
|
|
398
673
|
.option('--id <id>', SELECT_HELP.id)
|
|
399
|
-
.option('--destination <url>', SELECT_HELP.destination)
|
|
674
|
+
.option('--destination <url>', SELECT_HELP.destination)
|
|
675
|
+
.option('--name <name>', SELECT_HELP.name),
|
|
400
676
|
)
|
|
401
677
|
.option('--json', 'Output JSON')
|
|
402
678
|
.action(async (options: KeysCliOptions) =>
|
|
403
679
|
run(async () => {
|
|
680
|
+
if (kindOf(options) === 'oauth-client') {
|
|
681
|
+
let name = clientName(options);
|
|
682
|
+
let seed = await seedOptions(options);
|
|
683
|
+
let secret = await readKeyValue(options);
|
|
684
|
+
let client = await replacePlatformClientSecret(seed, name, secret);
|
|
685
|
+
output(options.json, client, () => {
|
|
686
|
+
console.log(
|
|
687
|
+
`${FG_GREEN}Replaced the platform client's secret.${RESET}`,
|
|
688
|
+
);
|
|
689
|
+
printClient(client);
|
|
690
|
+
});
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
refuseClientOptions(options);
|
|
404
694
|
let seed = await seedOptions(options);
|
|
405
695
|
let id = await resolveSystemKeyId(seed, options);
|
|
406
696
|
let value = await readKeyValue(options);
|
|
@@ -417,17 +707,38 @@ export function registerKeysCommand(realmServer: Command): void {
|
|
|
417
707
|
|
|
418
708
|
keys
|
|
419
709
|
.command('delete')
|
|
420
|
-
.description(
|
|
710
|
+
.description(
|
|
711
|
+
'Delete a system key, so the proxy stops serving its destination, or with --kind oauth-client a platform client, whose connections then need an administrator',
|
|
712
|
+
)
|
|
421
713
|
.requiredOption('--realm-server <url>', 'The realm server to change')
|
|
714
|
+
.option('--kind <kind>', KIND_HELP)
|
|
422
715
|
.option('--id <id>', SELECT_HELP.id)
|
|
423
716
|
.option('--destination <url>', SELECT_HELP.destination)
|
|
717
|
+
.option('--name <name>', SELECT_HELP.name)
|
|
424
718
|
.option(
|
|
425
719
|
'--note <text>',
|
|
426
720
|
'The operator note recorded with the change: who made it and why (prompted for when omitted). A name in it is recorded as given, not verified',
|
|
427
721
|
)
|
|
722
|
+
.option(
|
|
723
|
+
'--yes',
|
|
724
|
+
"With --kind oauth-client, don't show the client and ask to confirm",
|
|
725
|
+
)
|
|
428
726
|
.option('--json', 'Output JSON')
|
|
429
727
|
.action(async (options: KeysCliOptions) =>
|
|
430
728
|
run(async () => {
|
|
729
|
+
if (kindOf(options) === 'oauth-client') {
|
|
730
|
+
let name = clientName(options);
|
|
731
|
+
let seed = await seedOptions(options);
|
|
732
|
+
await confirmPlatformClientDelete(seed, name, options);
|
|
733
|
+
await deletePlatformClient(seed, name);
|
|
734
|
+
output(options.json, { deleted: name }, () => {
|
|
735
|
+
console.log(
|
|
736
|
+
`${FG_GREEN}Deleted the platform client ${name}.${RESET}`,
|
|
737
|
+
);
|
|
738
|
+
});
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
refuseClientOptions(options);
|
|
431
742
|
let seed = await seedOptions(options);
|
|
432
743
|
let id = await resolveSystemKeyId(seed, options);
|
|
433
744
|
let note = await readNote(options.note, { stdinUsed: false });
|
|
@@ -440,3 +751,74 @@ export function registerKeysCommand(realmServer: Command): void {
|
|
|
440
751
|
}),
|
|
441
752
|
);
|
|
442
753
|
}
|
|
754
|
+
|
|
755
|
+
// A platform client takes none of a system key's settings.
|
|
756
|
+
export function refuseSystemOptions(options: KeysCliOptions): void {
|
|
757
|
+
let systemOnly = (
|
|
758
|
+
[
|
|
759
|
+
['destination', '--destination'],
|
|
760
|
+
['creditStrategy', '--credit-strategy'],
|
|
761
|
+
['auth', '--auth'],
|
|
762
|
+
['paramName', '--param-name'],
|
|
763
|
+
['streaming', '--streaming'],
|
|
764
|
+
] as const
|
|
765
|
+
).filter(([option]) => options[option] !== undefined);
|
|
766
|
+
if (systemOnly.length > 0) {
|
|
767
|
+
throw new Error(
|
|
768
|
+
`${systemOnly.map(([, flag]) => flag).join(', ')} ${systemOnly.length === 1 ? 'is' : 'are'} for a system key, not a platform client`,
|
|
769
|
+
);
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
async function addClientCommand(options: KeysCliOptions): Promise<void> {
|
|
774
|
+
refuseNoteForClient(options);
|
|
775
|
+
refuseSystemOptions(options);
|
|
776
|
+
let missing = (
|
|
777
|
+
[
|
|
778
|
+
['name', '--name'],
|
|
779
|
+
['label', '--label'],
|
|
780
|
+
['clientId', '--client-id'],
|
|
781
|
+
['authorizeUrl', '--authorize-url'],
|
|
782
|
+
['tokenUrl', '--token-url'],
|
|
783
|
+
['apiHost', '--api-host'],
|
|
784
|
+
] as const
|
|
785
|
+
).filter(([option]) => !options[option]);
|
|
786
|
+
if (missing.length > 0) {
|
|
787
|
+
throw new Error(
|
|
788
|
+
`A platform client needs ${missing.map(([, flag]) => flag).join(', ')}`,
|
|
789
|
+
);
|
|
790
|
+
}
|
|
791
|
+
if (
|
|
792
|
+
options.tokenAuth !== undefined &&
|
|
793
|
+
!['client_secret_post', 'client_secret_basic'].includes(options.tokenAuth)
|
|
794
|
+
) {
|
|
795
|
+
throw new Error(
|
|
796
|
+
'--token-auth must be client_secret_post or client_secret_basic',
|
|
797
|
+
);
|
|
798
|
+
}
|
|
799
|
+
let seed = await seedOptions(options);
|
|
800
|
+
let secret = await readKeyValue(options);
|
|
801
|
+
let client = await addPlatformClient(seed, {
|
|
802
|
+
name: options.name!,
|
|
803
|
+
label: options.label!,
|
|
804
|
+
secret,
|
|
805
|
+
oauth: {
|
|
806
|
+
clientId: options.clientId!,
|
|
807
|
+
authorizeUrl: options.authorizeUrl!,
|
|
808
|
+
tokenUrl: options.tokenUrl!,
|
|
809
|
+
...(options.scopes !== undefined ? { scopes: options.scopes } : {}),
|
|
810
|
+
...(options.tokenAuth !== undefined
|
|
811
|
+
? {
|
|
812
|
+
tokenAuth: options.tokenAuth as
|
|
813
|
+
| 'client_secret_post'
|
|
814
|
+
| 'client_secret_basic',
|
|
815
|
+
}
|
|
816
|
+
: {}),
|
|
817
|
+
apiHosts: options.apiHost!,
|
|
818
|
+
},
|
|
819
|
+
});
|
|
820
|
+
output(options.json, client, () => {
|
|
821
|
+
console.log(`${FG_GREEN}Added the platform client.${RESET}`);
|
|
822
|
+
printClient(client);
|
|
823
|
+
});
|
|
824
|
+
}
|