@cardstack/boxel-cli 0.9.0-unstable.5 → 0.9.0-unstable.6
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/bundled-test-harness/assets/{ai-module-creation-test-BNiZ9Als.js → ai-module-creation-test-CYdqfeyE.js} +1 -1
- package/bundled-test-harness/assets/{app-CP2kZsut.js → app-DEPzUD4Y.js} +1 -1
- package/bundled-test-harness/assets/{binary-upload-dedupe-test-c4W0iix2.js → binary-upload-dedupe-test-BIf-AWak.js} +1 -1
- package/bundled-test-harness/assets/{can-invoke-test-DyozfCFg.js → can-invoke-test-BjgjfYqo.js} +1 -1
- package/bundled-test-harness/assets/{card-chooser-paging-test-BKdBwPQ3.js → card-chooser-paging-test-C2v55yZ3.js} +1 -1
- package/bundled-test-harness/assets/{card-chooser-test-DAPWVfJL.js → card-chooser-test-OqnnAsAY.js} +1 -1
- package/bundled-test-harness/assets/{card-def-field-def-relationships-test-Dz1qg9ZW.js → card-def-field-def-relationships-test-TNzXYsVI.js} +1 -1
- package/bundled-test-harness/assets/{card-delete-test-BmI-DLtn.js → card-delete-test-BVnE3BU6.js} +1 -1
- package/bundled-test-harness/assets/{choreo-feature-reel-test-S0XwN4xA.js → choreo-feature-reel-test-DrvZlnG0.js} +1 -1
- package/bundled-test-harness/assets/{choreo-gallery-B0nnktwP.js → choreo-gallery-D8__QZCe.js} +11 -11
- package/bundled-test-harness/assets/{choreo-gallery-build-order-test-C6q9CDNY.js → choreo-gallery-build-order-test-7TpxKQd2.js} +1 -1
- package/bundled-test-harness/assets/{choreo-gallery-compositor-test-C-GwjEUw.js → choreo-gallery-compositor-test-BgMWB-xz.js} +1 -1
- package/bundled-test-harness/assets/choreo-gallery-cover-test-uR-CNV8g.js +205 -0
- package/bundled-test-harness/assets/{choreo-gallery-crossing-stage-test-B7jRQTAh.js → choreo-gallery-crossing-stage-test-CmGH0E5q.js} +1 -1
- package/bundled-test-harness/assets/{choreo-gallery-demos-alive-test-BLqE-zk1.js → choreo-gallery-demos-alive-test-DZ1sPqe8.js} +8 -2
- package/bundled-test-harness/assets/{choreo-gallery-drift-test-DOXLQNB9.js → choreo-gallery-drift-test-q7KosXIw.js} +1 -1
- package/bundled-test-harness/assets/{choreo-gallery-escort-test-lfBfmp-4.js → choreo-gallery-escort-test-CH_qyw6w.js} +1 -1
- package/bundled-test-harness/assets/{choreo-gallery-films-test-M761cc8Q.js → choreo-gallery-films-test-CYmek5qU.js} +1 -1
- package/bundled-test-harness/assets/{choreo-gallery-filter-test-BrUWMDaJ.js → choreo-gallery-filter-test-Curg5Fg7.js} +1 -1
- package/bundled-test-harness/assets/{choreo-gallery-fold-test-CTtdGSAI.js → choreo-gallery-fold-test-BDTkaH6H.js} +1 -1
- package/bundled-test-harness/assets/{choreo-gallery-guides-test-PHmvqxfk.js → choreo-gallery-guides-test-B1vY7Fkx.js} +2 -2
- package/bundled-test-harness/assets/{choreo-gallery-hang-test-DP5lQG0y.js → choreo-gallery-hang-test-BJJghTnK.js} +1 -1
- package/bundled-test-harness/assets/{choreo-gallery-highlight-test-CLhvRo6X.js → choreo-gallery-highlight-test-DGT0jmDf.js} +2 -2
- package/bundled-test-harness/assets/{choreo-gallery-inline-edit-test-jOddqOxH.js → choreo-gallery-inline-edit-test-DwF8ZeCB.js} +1 -1
- package/bundled-test-harness/assets/{choreo-gallery-interruption-test-hdQSH8Mi.js → choreo-gallery-interruption-test-DvfkLdWd.js} +1 -1
- package/bundled-test-harness/assets/{choreo-gallery-lone-stack-test-AHGiG8yJ.js → choreo-gallery-lone-stack-test-CDwE2xKJ.js} +1 -1
- package/bundled-test-harness/assets/{choreo-gallery-navigation-test-BlGmMyFM.js → choreo-gallery-navigation-test-DG6IdX6k.js} +1 -1
- package/bundled-test-harness/assets/{choreo-gallery-playhead-transport-test-8aNOk09i.js → choreo-gallery-playhead-transport-test-CITO-DAn.js} +1 -1
- package/bundled-test-harness/assets/{choreo-gallery-poplayout-subtree-test-BCJLJdyC.js → choreo-gallery-poplayout-subtree-test-Cs4S9Ky9.js} +1 -1
- package/bundled-test-harness/assets/{choreo-gallery-presence-modes-test-CbhdndwS.js → choreo-gallery-presence-modes-test-p8mppXAD.js} +1 -1
- package/bundled-test-harness/assets/{choreo-gallery-presentation-test-DDUagg6a.js → choreo-gallery-presentation-test-CT0lZepE.js} +36 -1
- package/bundled-test-harness/assets/{choreo-gallery-sheet-standalone-test-BNyhCtW2.js → choreo-gallery-sheet-standalone-test-D-MLCQ6u.js} +1 -1
- package/bundled-test-harness/assets/{choreo-gallery-sheet-test-NEe_oYnA.js → choreo-gallery-sheet-test-unHvAt2e.js} +1 -1
- package/bundled-test-harness/assets/{choreo-gallery-site-test-hT5mISBG.js → choreo-gallery-site-test-BRHuPInI.js} +1 -1
- package/bundled-test-harness/assets/{choreo-gallery-stage-Bmz9m8sP.js → choreo-gallery-stage-DVGS6XI-.js} +1 -1
- package/bundled-test-harness/assets/choreo-gallery-stage-keyboard-test-BYpAwZPD.js +102 -0
- package/bundled-test-harness/assets/{choreo-gallery-stages-test-DZjVu0Zq.js → choreo-gallery-stages-test-DPFv-GKb.js} +2 -2
- package/bundled-test-harness/assets/{choreo-gallery-subdivision-test-DBjnIZ8l.js → choreo-gallery-subdivision-test-C4rwq-DA.js} +1 -1
- package/bundled-test-harness/assets/{choreo-gallery-table-plan-test-DTSu2DjF.js → choreo-gallery-table-plan-test-CAQdsCBB.js} +1 -1
- package/bundled-test-harness/assets/{choreo-gallery-theater-test-BsLF8HcI.js → choreo-gallery-theater-test-BxvAn-i8.js} +2 -2
- package/bundled-test-harness/assets/{choreo-gallery-tuning-test-B1JW701Z.js → choreo-gallery-tuning-test-CvAiiRMn.js} +1 -1
- package/bundled-test-harness/assets/{choreo-gallery-widget-room-test-DcEIPub1.js → choreo-gallery-widget-room-test-DLkgh8Ws.js} +2 -2
- package/bundled-test-harness/assets/{choreo-gallery-wires-test-6VOc5SF8.js → choreo-gallery-wires-test-DMdHX2zj.js} +1 -1
- package/bundled-test-harness/assets/{codeblocks-test-DGtwtURg.js → codeblocks-test-DDMirwuJ.js} +1 -1
- package/bundled-test-harness/assets/{compaction-test-CTNozvI6.js → compaction-test-BKQ_668T.js} +1 -1
- package/bundled-test-harness/assets/{container-Az9Nq0LL.js → container-BrZklF85.js} +5 -2
- package/bundled-test-harness/assets/{create-listing-modal-test-cdE1T_YD.js → create-listing-modal-test-Bsgixn9Z.js} +1 -1
- package/bundled-test-harness/assets/{debug-message-test-BfS52C_5.js → debug-message-test-8e9w8xNX.js} +1 -1
- package/bundled-test-harness/assets/{fallback-banner-live-delete-test-C6Rl_3oe.js → fallback-banner-live-delete-test-CNsd-0FB.js} +1 -1
- package/bundled-test-harness/assets/{fallback-banner-test-CsSGrseR.js → fallback-banner-test-DlT-8Lpv.js} +1 -1
- package/bundled-test-harness/assets/{fallback-models-test-D7FBFpyT.js → fallback-models-test-CwG7Ydui.js} +1 -1
- package/bundled-test-harness/assets/{file-attachment-test-DtPZTxy4.js → file-attachment-test-Cvs-ulUS.js} +1 -1
- package/bundled-test-harness/assets/{general-test-CMH4p9LD.js → general-test-BDjfjhSB.js} +1 -1
- package/bundled-test-harness/assets/{main-DQBeCoaR.js → main-BeQF5cZd.js} +1 -1
- package/bundled-test-harness/assets/{model-cost-tier-test-DR2G3zVF.js → model-cost-tier-test-FFf8YNd5.js} +1 -1
- package/bundled-test-harness/assets/{operator-mode-card-chooser-test-Dtd5macc.js → operator-mode-card-chooser-test-DKo-UoRQ.js} +1 -1
- package/bundled-test-harness/assets/{operator-mode-links-test-BW7fJlL7.js → operator-mode-links-test-Dl5g5COc.js} +1 -1
- package/bundled-test-harness/assets/{operator-mode-test-OwSg0jux.js → operator-mode-test-Bo8diNLE.js} +1 -1
- package/bundled-test-harness/assets/{operator-mode-ui-test-gV3LQASK.js → operator-mode-ui-test-CSc-CYTU.js} +1 -1
- package/bundled-test-harness/assets/{overlay-menu-items-test-DhsN6QTO.js → overlay-menu-items-test-Bs1zoaym.js} +1 -1
- package/bundled-test-harness/assets/{past-sessions-test-DD6A7KuB.js → past-sessions-test-BGg7ZyON.js} +1 -1
- package/bundled-test-harness/assets/{permission-checking-realm-test-bQz4K37U.js → permission-checking-realm-test-Be6jGmyM.js} +1 -1
- package/bundled-test-harness/assets/{realm-config-policy-standing-test-Cs9XpWCX.js → realm-config-policy-standing-test-nM6CGX-Q.js} +1 -1
- package/bundled-test-harness/assets/{realm-config-routing-rule-edit-test-_D_xuw3p.js → realm-config-routing-rule-edit-test-TIuE2nug.js} +1 -1
- package/bundled-test-harness/assets/{realm-config-settings-test-DNqVtlJT.js → realm-config-settings-test-CWqwPMkw.js} +1 -1
- package/bundled-test-harness/assets/{reasoning-test-B74P8G_y.js → reasoning-test-CfRBo1ut.js} +1 -1
- package/bundled-test-harness/assets/{scrolling-test-EMUFGWKu.js → scrolling-test-DTJ7HVdE.js} +1 -1
- package/bundled-test-harness/assets/{sending-test-BIADsBdp.js → sending-test-D6iQh346.js} +1 -1
- package/bundled-test-harness/assets/{skills-test-AcW3L6RD.js → skills-test-Ca1EpjMI.js} +1 -1
- package/bundled-test-harness/assets/{store-test-CgPJ9v5Z.js → store-test-BS1Ue4Rt.js} +1 -1
- package/bundled-test-harness/assets/{tests-QZJXdl8x.js → tests-CPowoOp0.js} +471 -469
- package/bundled-test-harness/assets/{text-input-validator-test-DybvZTP3.js → text-input-validator-test-XpZxMbh0.js} +1 -1
- package/bundled-test-harness/assets/{to-device-streaming-test-BbgQFsQk.js → to-device-streaming-test-CKu1Ud8R.js} +1 -1
- package/bundled-test-harness/assets/{token-usage-test-BkQp1TTc.js → token-usage-test-Cszf7EU8.js} +1 -1
- package/bundled-test-harness/assets/{tools-test-DEfT44PZ.js → tools-test-DdlkGAFH.js} +1 -1
- package/bundled-test-harness/index.html +4 -4
- package/bundled-test-harness/tests/index.html +4 -4
- package/bundled-types/host-app/components/host-mode/content.gts +9 -0
- package/dist/api.js +166 -165
- package/dist/index.js +271 -270
- package/package.json +1 -1
- package/src/commands/realm-server/data-keys.ts +7 -14
- package/src/commands/realm-server/index.ts +2 -0
- package/src/commands/realm-server/keys.ts +496 -0
- package/src/lib/prompt.ts +45 -12
- package/src/lib/seed-auth.ts +63 -3
package/package.json
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import type { Command } from 'commander';
|
|
2
|
-
import { ensureTrailingSlash } from '@cardstack/runtime-common/paths';
|
|
3
2
|
import { cliLog } from '../../lib/cli-log.ts';
|
|
4
3
|
import { FG_GREEN, FG_YELLOW, RESET } from '../../lib/colors.ts';
|
|
5
4
|
import { resolveRealmSecretSeed } from '../../lib/prompt.ts';
|
|
6
|
-
import {
|
|
5
|
+
import { operatorRequest } from '../../lib/seed-auth.ts';
|
|
7
6
|
|
|
8
7
|
// `boxel realm-server data-keys`: where a rotation of the data key that seals
|
|
9
8
|
// the realm server's stored secrets stands, and the pass that re-seals stored
|
|
@@ -45,22 +44,16 @@ export interface DataKeysOptions {
|
|
|
45
44
|
}
|
|
46
45
|
|
|
47
46
|
async function operatorFetch(
|
|
48
|
-
|
|
49
|
-
realmServerUrl,
|
|
50
|
-
realmSecretSeed,
|
|
51
|
-
fetch = globalThis.fetch,
|
|
52
|
-
}: DataKeysOptions,
|
|
47
|
+
options: DataKeysOptions,
|
|
53
48
|
path: string,
|
|
54
49
|
method: 'GET' | 'POST',
|
|
55
50
|
): Promise<any> {
|
|
56
|
-
let url =
|
|
57
|
-
|
|
51
|
+
let { url, response } = await operatorRequest(
|
|
52
|
+
options,
|
|
53
|
+
'data-keys',
|
|
54
|
+
path,
|
|
58
55
|
method,
|
|
59
|
-
|
|
60
|
-
Accept: 'application/json',
|
|
61
|
-
Authorization: `Bearer ${mintOperatorToken(realmSecretSeed, 'data-keys')}`,
|
|
62
|
-
},
|
|
63
|
-
});
|
|
56
|
+
);
|
|
64
57
|
if (!response.ok) {
|
|
65
58
|
let body = await response.text().catch(() => '');
|
|
66
59
|
throw new Error(
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Command } from 'commander';
|
|
2
2
|
import { registerDataKeysCommand } from './data-keys.ts';
|
|
3
|
+
import { registerKeysCommand } from './keys.ts';
|
|
3
4
|
|
|
4
5
|
// Operator commands for a realm server, authorized with its realm secret
|
|
5
6
|
// seed. The namespace stays out of the general help.
|
|
@@ -11,4 +12,5 @@ export function registerRealmServerCommand(program: Command): void {
|
|
|
11
12
|
);
|
|
12
13
|
|
|
13
14
|
registerDataKeysCommand(realmServer);
|
|
15
|
+
registerKeysCommand(realmServer);
|
|
14
16
|
}
|
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
import type { Command } from 'commander';
|
|
2
|
+
import { readFile } from 'fs/promises';
|
|
3
|
+
import { cliLog } from '../../lib/cli-log.ts';
|
|
4
|
+
import { FG_GREEN, RESET } from '../../lib/colors.ts';
|
|
5
|
+
import {
|
|
6
|
+
prompt,
|
|
7
|
+
promptPassword,
|
|
8
|
+
resolveRealmSecretSeed,
|
|
9
|
+
} from '../../lib/prompt.ts';
|
|
10
|
+
import { operatorRequest } from '../../lib/seed-auth.ts';
|
|
11
|
+
|
|
12
|
+
// `boxel realm-server keys`: the realm server's system keys, the platform's
|
|
13
|
+
// own vendor keys that `_request-forward` attaches. Authorized with the realm
|
|
14
|
+
// secret seed. A value is typed at a masked prompt or read from stdin or a
|
|
15
|
+
// file, never taken as an argument, and no command prints one: the server
|
|
16
|
+
// never answers with a value. Every change carries an operator note, which
|
|
17
|
+
// the server records in the key's history next to what it observed itself: the
|
|
18
|
+
// caller's address and when the operator token was signed. A name in a note is
|
|
19
|
+
// as the operator typed it; nothing verifies it.
|
|
20
|
+
|
|
21
|
+
export type SystemKeyAuthMethod = 'header' | 'url-parameter';
|
|
22
|
+
export type SystemKeyCreditStrategy = 'openrouter' | 'none';
|
|
23
|
+
|
|
24
|
+
export interface SystemKey {
|
|
25
|
+
id: string;
|
|
26
|
+
destination: string;
|
|
27
|
+
authMethod: string;
|
|
28
|
+
paramName: string | null;
|
|
29
|
+
creditStrategy: string;
|
|
30
|
+
supportsStreaming: boolean;
|
|
31
|
+
hasValue: boolean;
|
|
32
|
+
keyId: string | null;
|
|
33
|
+
createdAt: string;
|
|
34
|
+
updatedAt: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface SystemKeyChange {
|
|
38
|
+
action: 'add' | 'replace' | 'delete';
|
|
39
|
+
destination: string;
|
|
40
|
+
note: string;
|
|
41
|
+
keyId: string | null;
|
|
42
|
+
changedAt: string;
|
|
43
|
+
clientAddress: string | null;
|
|
44
|
+
tokenIssuedAt: string | null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface SystemKeysOptions {
|
|
48
|
+
realmServerUrl: string;
|
|
49
|
+
realmSecretSeed: string;
|
|
50
|
+
fetch?: typeof globalThis.fetch;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface NewSystemKey {
|
|
54
|
+
destination: string;
|
|
55
|
+
authMethod?: SystemKeyAuthMethod;
|
|
56
|
+
paramName?: string;
|
|
57
|
+
creditStrategy: SystemKeyCreditStrategy;
|
|
58
|
+
supportsStreaming?: boolean;
|
|
59
|
+
value: string;
|
|
60
|
+
note: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function operatorFetch(
|
|
64
|
+
options: SystemKeysOptions,
|
|
65
|
+
path: string,
|
|
66
|
+
method: 'GET' | 'POST' | 'PATCH' | 'DELETE',
|
|
67
|
+
body?: unknown,
|
|
68
|
+
): Promise<any> {
|
|
69
|
+
let { url, response } = await operatorRequest(
|
|
70
|
+
options,
|
|
71
|
+
'system-keys',
|
|
72
|
+
path,
|
|
73
|
+
method,
|
|
74
|
+
body,
|
|
75
|
+
);
|
|
76
|
+
if (!response.ok) {
|
|
77
|
+
let text = await response.text().catch(() => '');
|
|
78
|
+
let message = text.slice(0, 300);
|
|
79
|
+
try {
|
|
80
|
+
let errors = JSON.parse(text)?.errors;
|
|
81
|
+
if (Array.isArray(errors) && errors.length > 0) {
|
|
82
|
+
message = errors.join('; ');
|
|
83
|
+
}
|
|
84
|
+
} catch {
|
|
85
|
+
// Not JSON; report the text as it is.
|
|
86
|
+
}
|
|
87
|
+
throw new Error(
|
|
88
|
+
`${method} ${url} answered HTTP ${response.status}: ${message}`,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
return response.json();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export async function listSystemKeys(
|
|
95
|
+
options: SystemKeysOptions,
|
|
96
|
+
): Promise<SystemKey[]> {
|
|
97
|
+
return (await operatorFetch(options, '_system-keys', 'GET')).keys;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export async function showSystemKey(
|
|
101
|
+
options: SystemKeysOptions,
|
|
102
|
+
id: string,
|
|
103
|
+
): Promise<{ key: SystemKey; history: SystemKeyChange[] }> {
|
|
104
|
+
return operatorFetch(
|
|
105
|
+
options,
|
|
106
|
+
`_system-keys/${encodeURIComponent(id)}`,
|
|
107
|
+
'GET',
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export async function addSystemKey(
|
|
112
|
+
options: SystemKeysOptions,
|
|
113
|
+
key: NewSystemKey,
|
|
114
|
+
): Promise<SystemKey> {
|
|
115
|
+
return (await operatorFetch(options, '_system-keys', 'POST', key)).key;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export async function replaceSystemKey(
|
|
119
|
+
options: SystemKeysOptions,
|
|
120
|
+
id: string,
|
|
121
|
+
change: { value: string; note: string },
|
|
122
|
+
): Promise<SystemKey> {
|
|
123
|
+
return (
|
|
124
|
+
await operatorFetch(
|
|
125
|
+
options,
|
|
126
|
+
`_system-keys/${encodeURIComponent(id)}`,
|
|
127
|
+
'PATCH',
|
|
128
|
+
change,
|
|
129
|
+
)
|
|
130
|
+
).key;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export async function deleteSystemKey(
|
|
134
|
+
options: SystemKeysOptions,
|
|
135
|
+
id: string,
|
|
136
|
+
change: { note: string },
|
|
137
|
+
): Promise<SystemKey> {
|
|
138
|
+
return (
|
|
139
|
+
await operatorFetch(
|
|
140
|
+
options,
|
|
141
|
+
`_system-keys/${encodeURIComponent(id)}`,
|
|
142
|
+
'DELETE',
|
|
143
|
+
change,
|
|
144
|
+
)
|
|
145
|
+
).deleted;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// The id of the key named by `--id`, or of the key for `--destination`,
|
|
149
|
+
// compared as the server stores destinations (as the URL parser spells them).
|
|
150
|
+
export async function resolveSystemKeyId(
|
|
151
|
+
options: SystemKeysOptions,
|
|
152
|
+
selector: { id?: string; destination?: string },
|
|
153
|
+
): Promise<string> {
|
|
154
|
+
if (selector.id && selector.destination) {
|
|
155
|
+
throw new Error('Name the key with --id or --destination, not both');
|
|
156
|
+
}
|
|
157
|
+
if (selector.id) {
|
|
158
|
+
return selector.id;
|
|
159
|
+
}
|
|
160
|
+
if (!selector.destination) {
|
|
161
|
+
throw new Error('Name the key with --id or --destination');
|
|
162
|
+
}
|
|
163
|
+
let destination: string;
|
|
164
|
+
try {
|
|
165
|
+
destination = new URL(selector.destination).href;
|
|
166
|
+
} catch {
|
|
167
|
+
throw new Error(`${selector.destination} is not a URL`);
|
|
168
|
+
}
|
|
169
|
+
let match = (await listSystemKeys(options)).find(
|
|
170
|
+
(key) => key.destination === destination,
|
|
171
|
+
);
|
|
172
|
+
if (!match) {
|
|
173
|
+
throw new Error(`No system key has the destination ${destination}`);
|
|
174
|
+
}
|
|
175
|
+
return match.id;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function stripOneTrailingNewline(text: string): string {
|
|
179
|
+
return text.replace(/\r?\n$/, '');
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export interface KeyValueSource {
|
|
183
|
+
stdin?: boolean;
|
|
184
|
+
keyFile?: string;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* The key value, from exactly one source: `--key-file`, `--stdin`, or, when
|
|
189
|
+
* stdin is a terminal, a masked prompt. One trailing newline is dropped from
|
|
190
|
+
* a file or stdin. Without a terminal and without either flag it refuses,
|
|
191
|
+
* rather than hang waiting for keystrokes.
|
|
192
|
+
*/
|
|
193
|
+
export async function readKeyValue(
|
|
194
|
+
source: KeyValueSource,
|
|
195
|
+
io: {
|
|
196
|
+
input?: NodeJS.ReadStream;
|
|
197
|
+
output?: NodeJS.WritableStream;
|
|
198
|
+
} = {},
|
|
199
|
+
): Promise<string> {
|
|
200
|
+
let input = io.input ?? process.stdin;
|
|
201
|
+
if (source.stdin && source.keyFile) {
|
|
202
|
+
throw new Error('Pass --stdin or --key-file, not both');
|
|
203
|
+
}
|
|
204
|
+
let value: string;
|
|
205
|
+
if (source.keyFile) {
|
|
206
|
+
value = stripOneTrailingNewline(await readFile(source.keyFile, 'utf8'));
|
|
207
|
+
} else if (source.stdin) {
|
|
208
|
+
let chunks: Buffer[] = [];
|
|
209
|
+
for await (let chunk of input) {
|
|
210
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
211
|
+
}
|
|
212
|
+
value = stripOneTrailingNewline(Buffer.concat(chunks).toString('utf8'));
|
|
213
|
+
} else if (input.isTTY) {
|
|
214
|
+
value = await promptPassword('Key value: ', {
|
|
215
|
+
revealLast: 4,
|
|
216
|
+
input,
|
|
217
|
+
output: io.output,
|
|
218
|
+
});
|
|
219
|
+
} else {
|
|
220
|
+
throw new Error(
|
|
221
|
+
'Cannot prompt for the key value: stdin is not a terminal. Pass --stdin or --key-file',
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
if (!value) {
|
|
225
|
+
throw new Error('The key value is empty');
|
|
226
|
+
}
|
|
227
|
+
return value;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* The operator note for a change: `--note`, or a prompt when stdin is a
|
|
232
|
+
* terminal that the value didn't come through.
|
|
233
|
+
*/
|
|
234
|
+
export async function readNote(
|
|
235
|
+
note: string | undefined,
|
|
236
|
+
{ stdinUsed }: { stdinUsed: boolean },
|
|
237
|
+
): Promise<string> {
|
|
238
|
+
if (note !== undefined) {
|
|
239
|
+
if (!note.trim()) {
|
|
240
|
+
throw new Error('The operator note is empty');
|
|
241
|
+
}
|
|
242
|
+
return note;
|
|
243
|
+
}
|
|
244
|
+
if (stdinUsed || !process.stdin.isTTY) {
|
|
245
|
+
throw new Error(
|
|
246
|
+
'Pass --note: the operator note, recorded in the key history, says who made the change and why. A name in it is recorded as given, not verified',
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
let answer = await prompt(
|
|
250
|
+
'Operator note (who and why; a name here is recorded as given, not verified): ',
|
|
251
|
+
);
|
|
252
|
+
if (!answer) {
|
|
253
|
+
throw new Error('The operator note is empty');
|
|
254
|
+
}
|
|
255
|
+
return answer;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function printKey(key: SystemKey): void {
|
|
259
|
+
console.log(`${key.destination}`);
|
|
260
|
+
console.log(` id: ${key.id}`);
|
|
261
|
+
console.log(
|
|
262
|
+
` auth: ${key.authMethod}${key.paramName ? ` (${key.paramName})` : ''}`,
|
|
263
|
+
);
|
|
264
|
+
console.log(` credit strategy: ${key.creditStrategy}`);
|
|
265
|
+
console.log(` streaming: ${key.supportsStreaming ? 'yes' : 'no'}`);
|
|
266
|
+
console.log(
|
|
267
|
+
` value: ${key.hasValue ? (key.keyId ? `sealed under data key ${key.keyId}` : 'in plain text only, until the backfill seals it') : 'none'}`,
|
|
268
|
+
);
|
|
269
|
+
console.log(` updated: ${key.updatedAt}`);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function printHistory(history: SystemKeyChange[]): void {
|
|
273
|
+
if (history.length === 0) {
|
|
274
|
+
console.log(' history: none recorded');
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
console.log(' history:');
|
|
278
|
+
for (let change of history) {
|
|
279
|
+
console.log(` ${change.changedAt} ${change.action}: ${change.note}`);
|
|
280
|
+
console.log(
|
|
281
|
+
` from ${change.clientAddress ?? 'an unknown address'}, token signed ${change.tokenIssuedAt ?? 'at an unknown time'}`,
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
interface KeysCliOptions {
|
|
287
|
+
realmServer: string;
|
|
288
|
+
id?: string;
|
|
289
|
+
destination?: string;
|
|
290
|
+
auth?: SystemKeyAuthMethod;
|
|
291
|
+
paramName?: string;
|
|
292
|
+
creditStrategy?: SystemKeyCreditStrategy;
|
|
293
|
+
streaming?: boolean;
|
|
294
|
+
stdin?: boolean;
|
|
295
|
+
keyFile?: string;
|
|
296
|
+
note?: string;
|
|
297
|
+
json?: boolean;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
async function seedOptions(
|
|
301
|
+
options: KeysCliOptions,
|
|
302
|
+
): Promise<SystemKeysOptions> {
|
|
303
|
+
let realmSecretSeed = await resolveRealmSecretSeed(true);
|
|
304
|
+
if (!realmSecretSeed) {
|
|
305
|
+
throw new Error('A realm secret seed is required');
|
|
306
|
+
}
|
|
307
|
+
return { realmServerUrl: options.realmServer, realmSecretSeed };
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
async function run(fn: () => Promise<void>): Promise<void> {
|
|
311
|
+
try {
|
|
312
|
+
await fn();
|
|
313
|
+
} catch (e) {
|
|
314
|
+
console.error(`Error: ${e instanceof Error ? e.message : String(e)}`);
|
|
315
|
+
process.exit(1);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function output(json: boolean | undefined, value: unknown, print: () => void) {
|
|
320
|
+
if (json) {
|
|
321
|
+
cliLog.output(JSON.stringify(value, null, 2));
|
|
322
|
+
} else {
|
|
323
|
+
print();
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const SELECT_HELP = {
|
|
328
|
+
id: 'The key, by its id',
|
|
329
|
+
destination: 'The key, by its destination URL',
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
function valueOptions(command: Command): Command {
|
|
333
|
+
return command
|
|
334
|
+
.option('--stdin', 'Read the key value from stdin')
|
|
335
|
+
.option('--key-file <path>', 'Read the key value from a file')
|
|
336
|
+
.option(
|
|
337
|
+
'--note <text>',
|
|
338
|
+
'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',
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
export function registerKeysCommand(realmServer: Command): void {
|
|
343
|
+
let keys = realmServer
|
|
344
|
+
.command('keys')
|
|
345
|
+
.description(
|
|
346
|
+
"Manage the realm server's system keys: the platform's own vendor keys that the proxy attaches. No command shows a value",
|
|
347
|
+
);
|
|
348
|
+
|
|
349
|
+
keys
|
|
350
|
+
.command('list')
|
|
351
|
+
.description('List the system keys, without their values')
|
|
352
|
+
.requiredOption('--realm-server <url>', 'The realm server to ask')
|
|
353
|
+
.option('--json', 'Output JSON')
|
|
354
|
+
.action(async (options: KeysCliOptions) =>
|
|
355
|
+
run(async () => {
|
|
356
|
+
let list = await listSystemKeys(await seedOptions(options));
|
|
357
|
+
output(options.json, list, () => {
|
|
358
|
+
if (list.length === 0) {
|
|
359
|
+
console.log('No system keys.');
|
|
360
|
+
}
|
|
361
|
+
list.forEach(printKey);
|
|
362
|
+
});
|
|
363
|
+
}),
|
|
364
|
+
);
|
|
365
|
+
|
|
366
|
+
keys
|
|
367
|
+
.command('show')
|
|
368
|
+
.description(
|
|
369
|
+
'Show one system key and its recent history, without its value',
|
|
370
|
+
)
|
|
371
|
+
.requiredOption('--realm-server <url>', 'The realm server to ask')
|
|
372
|
+
.option('--id <id>', SELECT_HELP.id)
|
|
373
|
+
.option('--destination <url>', SELECT_HELP.destination)
|
|
374
|
+
.option('--json', 'Output JSON')
|
|
375
|
+
.action(async (options: KeysCliOptions) =>
|
|
376
|
+
run(async () => {
|
|
377
|
+
let seed = await seedOptions(options);
|
|
378
|
+
let shown = await showSystemKey(
|
|
379
|
+
seed,
|
|
380
|
+
await resolveSystemKeyId(seed, options),
|
|
381
|
+
);
|
|
382
|
+
output(options.json, shown, () => {
|
|
383
|
+
printKey(shown.key);
|
|
384
|
+
printHistory(shown.history);
|
|
385
|
+
});
|
|
386
|
+
}),
|
|
387
|
+
);
|
|
388
|
+
|
|
389
|
+
valueOptions(
|
|
390
|
+
keys
|
|
391
|
+
.command('add')
|
|
392
|
+
.description(
|
|
393
|
+
'Add a system key for a destination that has none. The value is typed at a masked prompt, or read with --stdin or --key-file',
|
|
394
|
+
)
|
|
395
|
+
.requiredOption('--realm-server <url>', 'The realm server to change')
|
|
396
|
+
.requiredOption(
|
|
397
|
+
'--destination <url>',
|
|
398
|
+
'The URL the key is sent to; requests under it get the key',
|
|
399
|
+
)
|
|
400
|
+
.requiredOption(
|
|
401
|
+
'--credit-strategy <strategy>',
|
|
402
|
+
'How calls are charged: openrouter or none',
|
|
403
|
+
)
|
|
404
|
+
.option(
|
|
405
|
+
'--auth <method>',
|
|
406
|
+
'Where the key goes: header or url-parameter',
|
|
407
|
+
'header',
|
|
408
|
+
)
|
|
409
|
+
.option(
|
|
410
|
+
'--param-name <name>',
|
|
411
|
+
'The header (default Authorization) or URL parameter (default key) that carries the key. A header always carries it as a Bearer token',
|
|
412
|
+
)
|
|
413
|
+
.option('--streaming', 'The destination supports streamed responses'),
|
|
414
|
+
)
|
|
415
|
+
.option('--json', 'Output JSON')
|
|
416
|
+
.action(async (options: KeysCliOptions) =>
|
|
417
|
+
run(async () => {
|
|
418
|
+
if (!['openrouter', 'none'].includes(options.creditStrategy!)) {
|
|
419
|
+
throw new Error('--credit-strategy must be openrouter or none');
|
|
420
|
+
}
|
|
421
|
+
if (!['header', 'url-parameter'].includes(options.auth!)) {
|
|
422
|
+
throw new Error('--auth must be header or url-parameter');
|
|
423
|
+
}
|
|
424
|
+
let seed = await seedOptions(options);
|
|
425
|
+
let value = await readKeyValue(options);
|
|
426
|
+
let note = await readNote(options.note, {
|
|
427
|
+
stdinUsed: Boolean(options.stdin),
|
|
428
|
+
});
|
|
429
|
+
let key = await addSystemKey(seed, {
|
|
430
|
+
destination: options.destination!,
|
|
431
|
+
authMethod: options.auth,
|
|
432
|
+
paramName: options.paramName,
|
|
433
|
+
creditStrategy: options.creditStrategy!,
|
|
434
|
+
supportsStreaming: Boolean(options.streaming),
|
|
435
|
+
value,
|
|
436
|
+
note,
|
|
437
|
+
});
|
|
438
|
+
output(options.json, key, () => {
|
|
439
|
+
console.log(`${FG_GREEN}Added the system key.${RESET}`);
|
|
440
|
+
printKey(key);
|
|
441
|
+
});
|
|
442
|
+
}),
|
|
443
|
+
);
|
|
444
|
+
|
|
445
|
+
valueOptions(
|
|
446
|
+
keys
|
|
447
|
+
.command('replace')
|
|
448
|
+
.description(
|
|
449
|
+
"Replace a system key's value, keeping its destination and settings",
|
|
450
|
+
)
|
|
451
|
+
.requiredOption('--realm-server <url>', 'The realm server to change')
|
|
452
|
+
.option('--id <id>', SELECT_HELP.id)
|
|
453
|
+
.option('--destination <url>', SELECT_HELP.destination),
|
|
454
|
+
)
|
|
455
|
+
.option('--json', 'Output JSON')
|
|
456
|
+
.action(async (options: KeysCliOptions) =>
|
|
457
|
+
run(async () => {
|
|
458
|
+
let seed = await seedOptions(options);
|
|
459
|
+
let id = await resolveSystemKeyId(seed, options);
|
|
460
|
+
let value = await readKeyValue(options);
|
|
461
|
+
let note = await readNote(options.note, {
|
|
462
|
+
stdinUsed: Boolean(options.stdin),
|
|
463
|
+
});
|
|
464
|
+
let key = await replaceSystemKey(seed, id, { value, note });
|
|
465
|
+
output(options.json, key, () => {
|
|
466
|
+
console.log(`${FG_GREEN}Replaced the system key.${RESET}`);
|
|
467
|
+
printKey(key);
|
|
468
|
+
});
|
|
469
|
+
}),
|
|
470
|
+
);
|
|
471
|
+
|
|
472
|
+
keys
|
|
473
|
+
.command('delete')
|
|
474
|
+
.description('Delete a system key; the proxy stops serving its destination')
|
|
475
|
+
.requiredOption('--realm-server <url>', 'The realm server to change')
|
|
476
|
+
.option('--id <id>', SELECT_HELP.id)
|
|
477
|
+
.option('--destination <url>', SELECT_HELP.destination)
|
|
478
|
+
.option(
|
|
479
|
+
'--note <text>',
|
|
480
|
+
'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',
|
|
481
|
+
)
|
|
482
|
+
.option('--json', 'Output JSON')
|
|
483
|
+
.action(async (options: KeysCliOptions) =>
|
|
484
|
+
run(async () => {
|
|
485
|
+
let seed = await seedOptions(options);
|
|
486
|
+
let id = await resolveSystemKeyId(seed, options);
|
|
487
|
+
let note = await readNote(options.note, { stdinUsed: false });
|
|
488
|
+
let key = await deleteSystemKey(seed, id, { note });
|
|
489
|
+
output(options.json, key, () => {
|
|
490
|
+
console.log(
|
|
491
|
+
`${FG_GREEN}Deleted the system key for ${key.destination}.${RESET}`,
|
|
492
|
+
);
|
|
493
|
+
});
|
|
494
|
+
}),
|
|
495
|
+
);
|
|
496
|
+
}
|
package/src/lib/prompt.ts
CHANGED
|
@@ -23,23 +23,48 @@ export function prompt(question: string): Promise<string> {
|
|
|
23
23
|
});
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
export interface PromptPasswordOptions {
|
|
27
|
+
/**
|
|
28
|
+
* After Enter, show this many of the value's last characters, so the
|
|
29
|
+
* operator can tell the value they pasted is the one they meant. Never more
|
|
30
|
+
* than a quarter of the value is shown.
|
|
31
|
+
*/
|
|
32
|
+
revealLast?: number;
|
|
33
|
+
/** @internal The streams to read from and write to; tests pass their own. */
|
|
34
|
+
input?: NodeJS.ReadStream;
|
|
35
|
+
output?: NodeJS.WritableStream;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The trailing characters of `value` that `revealLast` may show: at most
|
|
40
|
+
* `revealLast`, and at most a quarter of the value.
|
|
41
|
+
*/
|
|
42
|
+
export function revealedSuffix(value: string, revealLast: number): string {
|
|
43
|
+
let count = Math.min(revealLast, Math.floor(value.length / 4));
|
|
44
|
+
return count > 0 ? value.slice(-count) : '';
|
|
45
|
+
}
|
|
46
|
+
|
|
26
47
|
/**
|
|
27
|
-
* Read a secret from stdin without echoing it to the TTY.
|
|
28
|
-
*
|
|
48
|
+
* Read a secret from stdin without echoing it to the TTY. Every character
|
|
49
|
+
* echoes as `*` and Ctrl+C exits. Intended for seeds, passwords, keys and other
|
|
29
50
|
* sensitive CLI input that must not appear in shell history or `ps aux`.
|
|
30
51
|
*/
|
|
31
|
-
export function promptPassword(
|
|
52
|
+
export function promptPassword(
|
|
53
|
+
question: string,
|
|
54
|
+
options: PromptPasswordOptions = {},
|
|
55
|
+
): Promise<string> {
|
|
56
|
+
const stdin = options.input ?? process.stdin;
|
|
57
|
+
const stdout = options.output ?? process.stdout;
|
|
32
58
|
const mutableOutput = new Writable({
|
|
33
59
|
write: (_chunk, _encoding, callback) => callback(),
|
|
34
60
|
});
|
|
35
61
|
const rl = readline.createInterface({
|
|
36
|
-
input:
|
|
62
|
+
input: stdin,
|
|
37
63
|
output: mutableOutput,
|
|
38
64
|
terminal: true,
|
|
39
65
|
});
|
|
40
66
|
|
|
41
67
|
return new Promise((resolve, reject) => {
|
|
42
|
-
const stdin = process.stdin;
|
|
43
68
|
const wasFlowing = stdin.readableFlowing;
|
|
44
69
|
|
|
45
70
|
if (stdin.isTTY) {
|
|
@@ -65,14 +90,22 @@ export function promptPassword(question: string): Promise<string> {
|
|
|
65
90
|
// Ctrl+C inside a paste still work.
|
|
66
91
|
const raw = chunk
|
|
67
92
|
.toString()
|
|
68
|
-
.split('
|
|
93
|
+
.split('\u001b[200~')
|
|
69
94
|
.join('')
|
|
70
|
-
.split('
|
|
95
|
+
.split('\u001b[201~')
|
|
71
96
|
.join('');
|
|
72
97
|
for (const c of raw) {
|
|
73
98
|
if (c === '\n' || c === '\r') {
|
|
74
99
|
cleanup();
|
|
75
|
-
|
|
100
|
+
stdout.write('\n');
|
|
101
|
+
const suffix = options.revealLast
|
|
102
|
+
? revealedSuffix(password, options.revealLast)
|
|
103
|
+
: '';
|
|
104
|
+
if (suffix) {
|
|
105
|
+
stdout.write(
|
|
106
|
+
` (${[...password].length} characters, ending in ${suffix})\n`,
|
|
107
|
+
);
|
|
108
|
+
}
|
|
76
109
|
resolve(password);
|
|
77
110
|
return;
|
|
78
111
|
} else if (c === '\u0003') {
|
|
@@ -82,13 +115,13 @@ export function promptPassword(question: string): Promise<string> {
|
|
|
82
115
|
} else if (c === '\u007F' || c === '\b') {
|
|
83
116
|
// Backspace
|
|
84
117
|
if (password.length > 0) {
|
|
85
|
-
password = password.slice(0, -1);
|
|
86
|
-
|
|
118
|
+
password = [...password].slice(0, -1).join('');
|
|
119
|
+
stdout.write('\b \b');
|
|
87
120
|
}
|
|
88
121
|
} else if (c >= ' ') {
|
|
89
122
|
// Printable character; suppress other control bytes entirely.
|
|
90
123
|
password += c;
|
|
91
|
-
|
|
124
|
+
stdout.write('*');
|
|
92
125
|
}
|
|
93
126
|
}
|
|
94
127
|
} catch (e) {
|
|
@@ -99,7 +132,7 @@ export function promptPassword(question: string): Promise<string> {
|
|
|
99
132
|
|
|
100
133
|
let password = '';
|
|
101
134
|
try {
|
|
102
|
-
|
|
135
|
+
stdout.write(question);
|
|
103
136
|
stdin.on('data', onData);
|
|
104
137
|
stdin.resume();
|
|
105
138
|
} catch (e) {
|