@greatwait/cli 0.1.0-alpha.3 → 0.1.0-alpha.5
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/README.md +1 -1
- package/dist/args.d.ts +2 -2
- package/dist/args.js +33 -16
- package/dist/catalog.d.ts +196 -0
- package/dist/catalog.js +1521 -0
- package/dist/cli.d.ts +11 -2
- package/dist/cli.js +45 -32
- package/dist/commands.d.ts +33 -1
- package/dist/commands.js +207 -148
- package/dist/confirm.d.ts +33 -0
- package/dist/confirm.js +17 -0
- package/dist/exit.d.ts +24 -2
- package/dist/exit.js +44 -2
- package/dist/gw.d.ts +2 -0
- package/dist/gw.js +15 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -1
- package/dist/journey.d.ts +2 -2
- package/dist/journey.js +20 -4
- package/dist/main.js +6 -16
- package/dist/node-runtime.d.ts +15 -1
- package/dist/node-runtime.js +32 -2
- package/dist/oauth.d.ts +1 -1
- package/dist/oauth.js +16 -16
- package/dist/operate.d.ts +39 -0
- package/dist/operate.js +1558 -0
- package/dist/remediation.d.ts +35 -0
- package/dist/remediation.js +81 -0
- package/dist/runtime.d.ts +21 -1
- package/dist/session.js +5 -5
- package/dist/start.d.ts +15 -0
- package/dist/start.js +26 -0
- package/dist/support.d.ts +129 -0
- package/dist/support.js +261 -0
- package/package.json +6 -5
package/dist/exit.d.ts
CHANGED
|
@@ -33,6 +33,18 @@ export declare const EXIT_CLASSES: {
|
|
|
33
33
|
readonly retryable: 6;
|
|
34
34
|
/** A deadline this command was given passed before the answer arrived. */
|
|
35
35
|
readonly timeout: 7;
|
|
36
|
+
/**
|
|
37
|
+
* The service is not configured to do this, and repeating will not change
|
|
38
|
+
* that.
|
|
39
|
+
*
|
|
40
|
+
* **A separate number from `retryable` because a script must not loop on
|
|
41
|
+
* it.** Both are 5xx and both mean "the service, not you" — but one clears
|
|
42
|
+
* on its own and the other waits on somebody changing the deployment. Until
|
|
43
|
+
* FXO-2497 these shared class 6, so a client told "the identical request is
|
|
44
|
+
* safe to repeat" would retry a condition that is permanent, and a customer
|
|
45
|
+
* was offered a "Try again" control that could never work.
|
|
46
|
+
*/
|
|
47
|
+
readonly unconfigured: 8;
|
|
36
48
|
/**
|
|
37
49
|
* Interrupted.
|
|
38
50
|
*
|
|
@@ -44,6 +56,15 @@ export declare const EXIT_CLASSES: {
|
|
|
44
56
|
readonly interrupted: 130;
|
|
45
57
|
};
|
|
46
58
|
export type ExitClass = keyof typeof EXIT_CLASSES;
|
|
59
|
+
/**
|
|
60
|
+
* What each class means, in the words the reference publishes.
|
|
61
|
+
*
|
|
62
|
+
* **Beside the numbers rather than in the page**, because a table of exit codes
|
|
63
|
+
* written out in documentation is a table that describes last month's classes.
|
|
64
|
+
* The reference renders this, so adding a class without describing it does not
|
|
65
|
+
* compile.
|
|
66
|
+
*/
|
|
67
|
+
export declare const EXIT_CLASS_MEANINGS: Readonly<Record<ExitClass, string>>;
|
|
47
68
|
/**
|
|
48
69
|
* The class an HTTP status falls in.
|
|
49
70
|
*
|
|
@@ -51,8 +72,9 @@ export type ExitClass = keyof typeof EXIT_CLASSES;
|
|
|
51
72
|
* A refusal from an edge proxy carries no problem document at all — there is no
|
|
52
73
|
* code to read — and a client that branched on the code first would classify
|
|
53
74
|
* every one of those as internal. The code refines the answer where it
|
|
54
|
-
* disagrees with the status, which is exactly
|
|
55
|
-
* and belongs in `authorization`,
|
|
75
|
+
* disagrees with the status, which is exactly twice: `scope_missing` is a `403`
|
|
76
|
+
* and belongs in `authorization`, `capability_not_configured` is a `503` that
|
|
77
|
+
* must not be called retryable, and everything else in the 4xx range that is
|
|
56
78
|
* not authentication is the caller's request to fix.
|
|
57
79
|
*/
|
|
58
80
|
export declare function classifyStatus(status: number, problemCode?: string): ExitClass;
|
package/dist/exit.js
CHANGED
|
@@ -33,6 +33,18 @@ export const EXIT_CLASSES = {
|
|
|
33
33
|
retryable: 6,
|
|
34
34
|
/** A deadline this command was given passed before the answer arrived. */
|
|
35
35
|
timeout: 7,
|
|
36
|
+
/**
|
|
37
|
+
* The service is not configured to do this, and repeating will not change
|
|
38
|
+
* that.
|
|
39
|
+
*
|
|
40
|
+
* **A separate number from `retryable` because a script must not loop on
|
|
41
|
+
* it.** Both are 5xx and both mean "the service, not you" — but one clears
|
|
42
|
+
* on its own and the other waits on somebody changing the deployment. Until
|
|
43
|
+
* FXO-2497 these shared class 6, so a client told "the identical request is
|
|
44
|
+
* safe to repeat" would retry a condition that is permanent, and a customer
|
|
45
|
+
* was offered a "Try again" control that could never work.
|
|
46
|
+
*/
|
|
47
|
+
unconfigured: 8,
|
|
36
48
|
/**
|
|
37
49
|
* Interrupted.
|
|
38
50
|
*
|
|
@@ -43,6 +55,26 @@ export const EXIT_CLASSES = {
|
|
|
43
55
|
*/
|
|
44
56
|
interrupted: 130,
|
|
45
57
|
};
|
|
58
|
+
/**
|
|
59
|
+
* What each class means, in the words the reference publishes.
|
|
60
|
+
*
|
|
61
|
+
* **Beside the numbers rather than in the page**, because a table of exit codes
|
|
62
|
+
* written out in documentation is a table that describes last month's classes.
|
|
63
|
+
* The reference renders this, so adding a class without describing it does not
|
|
64
|
+
* compile.
|
|
65
|
+
*/
|
|
66
|
+
export const EXIT_CLASS_MEANINGS = {
|
|
67
|
+
authentication: "No usable credential. Sign in.",
|
|
68
|
+
authorization: "A credential that authenticated and may not do this.",
|
|
69
|
+
internal: "A defect in this program.",
|
|
70
|
+
interrupted: "Stopped — Ctrl-C, or a confirmation declined.",
|
|
71
|
+
ok: "It did what it was asked.",
|
|
72
|
+
retryable: "The service could not answer now. The identical request may work later.",
|
|
73
|
+
timeout: "A deadline this command was given passed before the answer arrived.",
|
|
74
|
+
unconfigured: "The service is not configured to do this. Repeating will not help; it needs somebody to change the service.",
|
|
75
|
+
usage: "The command line was wrong.",
|
|
76
|
+
validation: "The request was refused on its merits: invalid, missing, or conflicting.",
|
|
77
|
+
};
|
|
46
78
|
/**
|
|
47
79
|
* The class an HTTP status falls in.
|
|
48
80
|
*
|
|
@@ -50,13 +82,23 @@ export const EXIT_CLASSES = {
|
|
|
50
82
|
* A refusal from an edge proxy carries no problem document at all — there is no
|
|
51
83
|
* code to read — and a client that branched on the code first would classify
|
|
52
84
|
* every one of those as internal. The code refines the answer where it
|
|
53
|
-
* disagrees with the status, which is exactly
|
|
54
|
-
* and belongs in `authorization`,
|
|
85
|
+
* disagrees with the status, which is exactly twice: `scope_missing` is a `403`
|
|
86
|
+
* and belongs in `authorization`, `capability_not_configured` is a `503` that
|
|
87
|
+
* must not be called retryable, and everything else in the 4xx range that is
|
|
55
88
|
* not authentication is the caller's request to fix.
|
|
56
89
|
*/
|
|
57
90
|
export function classifyStatus(status, problemCode) {
|
|
58
91
|
if (problemCode === "scope_missing")
|
|
59
92
|
return "authorization";
|
|
93
|
+
// Without this the status alone decides, and a 503 falls through to
|
|
94
|
+
// `retryable` below — which is the defect: a permanent configuration refusal
|
|
95
|
+
// telling a script that repeating it is safe.
|
|
96
|
+
if (problemCode === "capability_not_configured")
|
|
97
|
+
return "unconfigured";
|
|
98
|
+
// The SDK uses zero when no HTTP answer survives its transport retries.
|
|
99
|
+
// This is a failed exchange, not a successful command.
|
|
100
|
+
if (status === 0)
|
|
101
|
+
return "retryable";
|
|
60
102
|
if (status === 401)
|
|
61
103
|
return "authentication";
|
|
62
104
|
if (status === 403)
|
package/dist/gw.d.ts
ADDED
package/dist/gw.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { start } from "./start.js";
|
|
3
|
+
/**
|
|
4
|
+
* The same command, under the short name.
|
|
5
|
+
*
|
|
6
|
+
* Everything it prints names `gw`, because the whole point of a two-letter
|
|
7
|
+
* command is not typing the long one — and a tool that answers `gw schedules
|
|
8
|
+
* read` with "Next: greatwait attempts list …" hands back the thing it was
|
|
9
|
+
* installed to avoid.
|
|
10
|
+
*
|
|
11
|
+
* `greatwait` stays the canonical name: it is what the published reference, the
|
|
12
|
+
* quickstarts and the first-value journey say, and what one name has to be for
|
|
13
|
+
* a written instruction to be followable by somebody who installed either.
|
|
14
|
+
*/
|
|
15
|
+
await start("gw");
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { run,
|
|
1
|
+
export { run, usage } from "./cli.js";
|
|
2
2
|
export { COMMANDS } from "./commands.js";
|
|
3
3
|
export { configDirectory, configLocations, createCredentialStore, originKey, UnsafeConfigError, type CredentialStore, type StoredCredential, } from "./config.js";
|
|
4
4
|
export { EXIT_CLASSES, classifyStatus, type ExitClass } from "./exit.js";
|
|
@@ -6,4 +6,6 @@ export { nodeRuntime, openLoopback } from "./node-runtime.js";
|
|
|
6
6
|
export { OUTPUT_VERSION, redactSecret, renderFailure, renderResult, } from "./output.js";
|
|
7
7
|
export { parseArguments, parseDuration } from "./args.js";
|
|
8
8
|
export { publishedWait, renderJourney, STEP_COMMANDS } from "./journey.js";
|
|
9
|
+
export { CANONICAL_PROGRAM_NAME } from "./catalog.js";
|
|
10
|
+
export { start } from "./start.js";
|
|
9
11
|
export { CliError, InterruptedError, type CliRuntime, type LoopbackListener, } from "./runtime.js";
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { run,
|
|
1
|
+
export { run, usage } from "./cli.js";
|
|
2
2
|
export { COMMANDS } from "./commands.js";
|
|
3
3
|
export { configDirectory, configLocations, createCredentialStore, originKey, UnsafeConfigError, } from "./config.js";
|
|
4
4
|
export { EXIT_CLASSES, classifyStatus } from "./exit.js";
|
|
@@ -6,4 +6,6 @@ export { nodeRuntime, openLoopback } from "./node-runtime.js";
|
|
|
6
6
|
export { OUTPUT_VERSION, redactSecret, renderFailure, renderResult, } from "./output.js";
|
|
7
7
|
export { parseArguments, parseDuration } from "./args.js";
|
|
8
8
|
export { publishedWait, renderJourney, STEP_COMMANDS } from "./journey.js";
|
|
9
|
+
export { CANONICAL_PROGRAM_NAME } from "./catalog.js";
|
|
10
|
+
export { start } from "./start.js";
|
|
9
11
|
export { CliError, InterruptedError, } from "./runtime.js";
|
package/dist/journey.d.ts
CHANGED
|
@@ -23,8 +23,8 @@ export interface RenderedStep {
|
|
|
23
23
|
readonly purpose: string;
|
|
24
24
|
readonly title: string;
|
|
25
25
|
}
|
|
26
|
-
export declare function renderStep(step: JourneyStep): RenderedStep;
|
|
27
|
-
export declare function renderJourney(): {
|
|
26
|
+
export declare function renderStep(step: JourneyStep, programName?: string): RenderedStep;
|
|
27
|
+
export declare function renderJourney(programName?: string): {
|
|
28
28
|
readonly prerequisites: readonly {
|
|
29
29
|
readonly action: string;
|
|
30
30
|
readonly because: string;
|
package/dist/journey.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { FIRST_VALUE_JOURNEY, journeyStepVariables, } from "@greatwait/contracts";
|
|
2
|
+
import { CANONICAL_PROGRAM_NAME } from "./catalog.js";
|
|
2
3
|
/**
|
|
3
4
|
* The published first-value sequence, rendered for a terminal.
|
|
4
5
|
*
|
|
@@ -25,9 +26,24 @@ export const STEP_COMMANDS = {
|
|
|
25
26
|
"read-schedule": "greatwait schedules read <schedule>",
|
|
26
27
|
"replay-one-shot": "greatwait schedules create --endpoint <destination> --in 30s --idempotency-key <the same key>",
|
|
27
28
|
};
|
|
28
|
-
|
|
29
|
+
/**
|
|
30
|
+
* A step's command, under the name this executable was invoked as.
|
|
31
|
+
*
|
|
32
|
+
* **`STEP_COMMANDS` stays canonical and this rewrites the first word.** That
|
|
33
|
+
* table is re-exported into `first-value-projection.ts`, which renders it into
|
|
34
|
+
* the website's journey, the curl page and the JavaScript page — surfaces with
|
|
35
|
+
* no invocation to speak of. So the published text says `greatwait` and the
|
|
36
|
+
* terminal says whichever of the two names is on the person's PATH.
|
|
37
|
+
*/
|
|
38
|
+
function stepCommand(key, programName) {
|
|
39
|
+
const command = STEP_COMMANDS[key];
|
|
40
|
+
return command === undefined
|
|
41
|
+
? undefined
|
|
42
|
+
: command.replace(/^greatwait\b/u, programName);
|
|
43
|
+
}
|
|
44
|
+
export function renderStep(step, programName = CANONICAL_PROGRAM_NAME) {
|
|
29
45
|
return {
|
|
30
|
-
command:
|
|
46
|
+
command: stepCommand(step.key, programName),
|
|
31
47
|
key: step.key,
|
|
32
48
|
// What the step must be given, as the journey names it — the path
|
|
33
49
|
// parameters an earlier step captured, and the references in its body and
|
|
@@ -38,14 +54,14 @@ export function renderStep(step) {
|
|
|
38
54
|
title: step.title,
|
|
39
55
|
};
|
|
40
56
|
}
|
|
41
|
-
export function renderJourney() {
|
|
57
|
+
export function renderJourney(programName = CANONICAL_PROGRAM_NAME) {
|
|
42
58
|
return {
|
|
43
59
|
prerequisites: FIRST_VALUE_JOURNEY.prerequisites.map((prerequisite) => ({
|
|
44
60
|
action: prerequisite.action,
|
|
45
61
|
because: prerequisite.because,
|
|
46
62
|
title: prerequisite.title,
|
|
47
63
|
})),
|
|
48
|
-
steps: FIRST_VALUE_JOURNEY.steps.map(renderStep),
|
|
64
|
+
steps: FIRST_VALUE_JOURNEY.steps.map((step) => renderStep(step, programName)),
|
|
49
65
|
};
|
|
50
66
|
}
|
|
51
67
|
/**
|
package/dist/main.js
CHANGED
|
@@ -1,24 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import { nodeRuntime } from "./node-runtime.js";
|
|
2
|
+
import { start } from "./start.js";
|
|
4
3
|
/**
|
|
5
|
-
* The installed executable.
|
|
4
|
+
* The installed executable, under the name the package is called.
|
|
6
5
|
*
|
|
7
6
|
* **The whole file is the part the repository's own test suite cannot reach**,
|
|
8
7
|
* which is why criterion 7 asks for a regression against a `bin` that does
|
|
9
|
-
* nothing: every case
|
|
8
|
+
* nothing: every case drives `run` directly, so a package whose source is
|
|
10
9
|
* perfect and whose `bin` entry points at a file that does not exist passes all
|
|
11
10
|
* of them. `scripts/verify-clean-install.ts` installs the packed tarball and
|
|
12
|
-
* executes this path.
|
|
13
|
-
*
|
|
14
|
-
* `process.exitCode` rather than `process.exit`: the second truncates whatever
|
|
15
|
-
* is still buffered on stdout, and this command's last act is to print.
|
|
11
|
+
* executes this path — and `gw.ts` beside it, because a second `bin` is a
|
|
12
|
+
* second thing that can be wired to nothing.
|
|
16
13
|
*/
|
|
17
|
-
|
|
18
|
-
process.on("SIGINT", () => {
|
|
19
|
-
controller.abort();
|
|
20
|
-
});
|
|
21
|
-
process.on("SIGTERM", () => {
|
|
22
|
-
controller.abort();
|
|
23
|
-
});
|
|
24
|
-
process.exitCode = await run(nodeRuntime(controller.signal));
|
|
14
|
+
await start("greatwait");
|
package/dist/node-runtime.d.ts
CHANGED
|
@@ -32,4 +32,18 @@ export declare function promptSecret(question: string, streams?: {
|
|
|
32
32
|
readonly input: NodeJS.ReadStream;
|
|
33
33
|
readonly output: NodeJS.WriteStream;
|
|
34
34
|
}): Promise<string>;
|
|
35
|
-
|
|
35
|
+
/**
|
|
36
|
+
* Asks a question on the terminal and reads the answer back, echoed.
|
|
37
|
+
*
|
|
38
|
+
* **The question goes to standard error and the answer is read from standard
|
|
39
|
+
* input**, so a command whose result is being piped somewhere still asks a
|
|
40
|
+
* person who can see it. Ctrl-C at the prompt is an interruption rather than an
|
|
41
|
+
* empty answer, for the same reason it is at the secret prompt: a shell reports
|
|
42
|
+
* 130 for a process the person stopped, and treating it as "no" would be
|
|
43
|
+
* indistinguishable from a deliberate decline in a log.
|
|
44
|
+
*/
|
|
45
|
+
export declare function promptLine(question: string, streams?: {
|
|
46
|
+
readonly input: NodeJS.ReadStream;
|
|
47
|
+
readonly output: NodeJS.WriteStream;
|
|
48
|
+
}): Promise<string>;
|
|
49
|
+
export declare function nodeRuntime(signal: AbortSignal, programName: string): CliRuntime;
|
package/dist/node-runtime.js
CHANGED
|
@@ -49,7 +49,7 @@ export async function openLoopback() {
|
|
|
49
49
|
return;
|
|
50
50
|
}
|
|
51
51
|
response.writeHead(200, { "content-type": "text/plain; charset=utf-8" });
|
|
52
|
-
response.end("
|
|
52
|
+
response.end("GreatWait received the answer. You can close this tab.\n");
|
|
53
53
|
deliver?.(url.searchParams);
|
|
54
54
|
});
|
|
55
55
|
await new Promise((resolve, reject) => {
|
|
@@ -148,6 +148,34 @@ export async function promptSecret(question, streams = { input: process.stdin, o
|
|
|
148
148
|
readline.close();
|
|
149
149
|
}
|
|
150
150
|
}
|
|
151
|
+
/**
|
|
152
|
+
* Asks a question on the terminal and reads the answer back, echoed.
|
|
153
|
+
*
|
|
154
|
+
* **The question goes to standard error and the answer is read from standard
|
|
155
|
+
* input**, so a command whose result is being piped somewhere still asks a
|
|
156
|
+
* person who can see it. Ctrl-C at the prompt is an interruption rather than an
|
|
157
|
+
* empty answer, for the same reason it is at the secret prompt: a shell reports
|
|
158
|
+
* 130 for a process the person stopped, and treating it as "no" would be
|
|
159
|
+
* indistinguishable from a deliberate decline in a log.
|
|
160
|
+
*/
|
|
161
|
+
export async function promptLine(question, streams = { input: process.stdin, output: process.stderr }) {
|
|
162
|
+
const readline = createInterface({
|
|
163
|
+
input: streams.input,
|
|
164
|
+
output: streams.output,
|
|
165
|
+
});
|
|
166
|
+
try {
|
|
167
|
+
return await new Promise((resolve, reject) => {
|
|
168
|
+
readline.once("SIGINT", () => {
|
|
169
|
+
streams.output.write("\n");
|
|
170
|
+
reject(new InterruptedError());
|
|
171
|
+
});
|
|
172
|
+
readline.question(question, resolve);
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
finally {
|
|
176
|
+
readline.close();
|
|
177
|
+
}
|
|
178
|
+
}
|
|
151
179
|
async function readStdin() {
|
|
152
180
|
if (process.stdin.isTTY)
|
|
153
181
|
return "";
|
|
@@ -177,7 +205,7 @@ function sleep(milliseconds, signal) {
|
|
|
177
205
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
178
206
|
});
|
|
179
207
|
}
|
|
180
|
-
export function nodeRuntime(signal) {
|
|
208
|
+
export function nodeRuntime(signal, programName) {
|
|
181
209
|
return {
|
|
182
210
|
argv: process.argv.slice(2),
|
|
183
211
|
env: process.env,
|
|
@@ -188,6 +216,8 @@ export function nodeRuntime(signal) {
|
|
|
188
216
|
now: () => Date.now(),
|
|
189
217
|
openBrowser,
|
|
190
218
|
platform: process.platform,
|
|
219
|
+
programName,
|
|
220
|
+
promptLine: (question) => promptLine(question),
|
|
191
221
|
promptSecret: (question) => promptSecret(question),
|
|
192
222
|
randomBytes: (length) => crypto.getRandomValues(new Uint8Array(length)),
|
|
193
223
|
randomUuid: () => crypto.randomUUID(),
|
package/dist/oauth.d.ts
CHANGED
|
@@ -66,7 +66,7 @@ export declare function buildAuthorization(runtime: CliRuntime, server: Authoriz
|
|
|
66
66
|
* else's state is not a code to exchange — it is a request this process did not
|
|
67
67
|
* make — and reading the code first would exchange it anyway.
|
|
68
68
|
*/
|
|
69
|
-
export declare function codeFrom(returned: URLSearchParams, state: string): string;
|
|
69
|
+
export declare function codeFrom(returned: URLSearchParams, state: string, programName: string): string;
|
|
70
70
|
export declare function exchangeCode(runtime: CliRuntime, server: AuthorizationServer, options: {
|
|
71
71
|
readonly clientId: string;
|
|
72
72
|
readonly code: string;
|
package/dist/oauth.js
CHANGED
|
@@ -17,9 +17,9 @@ async function readJson(runtime, url, init) {
|
|
|
17
17
|
}
|
|
18
18
|
return { body, status: response.status };
|
|
19
19
|
}
|
|
20
|
-
function requireString(value, field, source) {
|
|
20
|
+
function requireString(value, field, source, programName) {
|
|
21
21
|
if (typeof value !== "string" || value === "") {
|
|
22
|
-
throw new CliError(`${source} does not name a ${field}`, "internal",
|
|
22
|
+
throw new CliError(`${source} does not name a ${field}`, "internal", `This origin publishes authorization metadata this command cannot use. Sign in with an API key instead: ${programName} login --api-key`);
|
|
23
23
|
}
|
|
24
24
|
return value;
|
|
25
25
|
}
|
|
@@ -35,13 +35,13 @@ export async function discover(runtime, origin) {
|
|
|
35
35
|
const resourceUrl = `${origin}/.well-known/oauth-protected-resource`;
|
|
36
36
|
const resource = await readJson(runtime, resourceUrl);
|
|
37
37
|
if (resource.status !== 200) {
|
|
38
|
-
throw new CliError(`${origin} does not publish protected-resource metadata (${String(resource.status)})`, "validation",
|
|
38
|
+
throw new CliError(`${origin} does not publish protected-resource metadata (${String(resource.status)})`, "validation", `Check the origin. A GreatWait API answers GET /__greatwait/version with no credential; try ${runtime.programName} doctor --api-url <origin>.`);
|
|
39
39
|
}
|
|
40
40
|
const resourceBody = resource.body;
|
|
41
41
|
const servers = Array.isArray(resourceBody.authorization_servers)
|
|
42
42
|
? resourceBody.authorization_servers
|
|
43
43
|
: [];
|
|
44
|
-
const issuer = requireString(servers[0], "authorization server", resourceUrl);
|
|
44
|
+
const issuer = requireString(servers[0], "authorization server", resourceUrl, runtime.programName);
|
|
45
45
|
const metadataUrl = `${issuer}/.well-known/oauth-authorization-server`;
|
|
46
46
|
const metadata = await readJson(runtime, metadataUrl);
|
|
47
47
|
if (metadata.status !== 200) {
|
|
@@ -50,7 +50,7 @@ export async function discover(runtime, origin) {
|
|
|
50
50
|
const body = metadata.body;
|
|
51
51
|
const scopes = body["scopes_supported"];
|
|
52
52
|
return {
|
|
53
|
-
authorizationEndpoint: requireString(body["authorization_endpoint"], "authorization endpoint", metadataUrl),
|
|
53
|
+
authorizationEndpoint: requireString(body["authorization_endpoint"], "authorization endpoint", metadataUrl, runtime.programName),
|
|
54
54
|
registrationEndpoint: typeof body["registration_endpoint"] === "string"
|
|
55
55
|
? body["registration_endpoint"]
|
|
56
56
|
: undefined,
|
|
@@ -59,23 +59,23 @@ export async function discover(runtime, origin) {
|
|
|
59
59
|
// minted for one audience must not be accepted by another, and a client
|
|
60
60
|
// that names an audience it invented is asserting something it did not
|
|
61
61
|
// check.
|
|
62
|
-
resource: requireString(resourceBody.resource, "resource", resourceUrl),
|
|
62
|
+
resource: requireString(resourceBody.resource, "resource", resourceUrl, runtime.programName),
|
|
63
63
|
revocationEndpoint: typeof body["revocation_endpoint"] === "string"
|
|
64
64
|
? body["revocation_endpoint"]
|
|
65
65
|
: undefined,
|
|
66
66
|
scopesSupported: Array.isArray(scopes)
|
|
67
67
|
? scopes.filter((scope) => typeof scope === "string")
|
|
68
68
|
: [],
|
|
69
|
-
tokenEndpoint: requireString(body["token_endpoint"], "token endpoint", metadataUrl),
|
|
69
|
+
tokenEndpoint: requireString(body["token_endpoint"], "token endpoint", metadataUrl, runtime.programName),
|
|
70
70
|
};
|
|
71
71
|
}
|
|
72
72
|
export async function registerClient(runtime, server, options) {
|
|
73
73
|
if (server.registrationEndpoint === undefined) {
|
|
74
|
-
throw new CliError("this authorization server does not offer dynamic client registration", "validation",
|
|
74
|
+
throw new CliError("this authorization server does not offer dynamic client registration", "validation", `Sign in with an API key instead: ${runtime.programName} login --api-key`);
|
|
75
75
|
}
|
|
76
76
|
const registered = await readJson(runtime, server.registrationEndpoint, {
|
|
77
77
|
body: JSON.stringify({
|
|
78
|
-
client_name: "
|
|
78
|
+
client_name: "GreatWait CLI",
|
|
79
79
|
client_uri: "https://greatwait.dev",
|
|
80
80
|
grant_types: ["authorization_code", "refresh_token"],
|
|
81
81
|
redirect_uris: [options.redirectUri],
|
|
@@ -92,7 +92,7 @@ export async function registerClient(runtime, server, options) {
|
|
|
92
92
|
throw new CliError(`dynamic client registration was refused (${String(registered.status)}: ${registered.body?.error ??
|
|
93
93
|
"no reason given"})`, "validation");
|
|
94
94
|
}
|
|
95
|
-
return requireString(registered.body.client_id, "client_id", server.registrationEndpoint);
|
|
95
|
+
return requireString(registered.body.client_id, "client_id", server.registrationEndpoint, runtime.programName);
|
|
96
96
|
}
|
|
97
97
|
export async function buildAuthorization(runtime, server, options) {
|
|
98
98
|
const state = base64Url(runtime.randomBytes(24));
|
|
@@ -116,16 +116,16 @@ export async function buildAuthorization(runtime, server, options) {
|
|
|
116
116
|
* else's state is not a code to exchange — it is a request this process did not
|
|
117
117
|
* make — and reading the code first would exchange it anyway.
|
|
118
118
|
*/
|
|
119
|
-
export function codeFrom(returned, state) {
|
|
119
|
+
export function codeFrom(returned, state, programName) {
|
|
120
120
|
const error = returned.get("error");
|
|
121
121
|
if (error !== null) {
|
|
122
122
|
throw new CliError(`authorization was refused (${error})`, "authentication", error === "access_denied"
|
|
123
|
-
?
|
|
123
|
+
? `Nothing was granted. Run ${programName} login again and approve the request, or use an API key.`
|
|
124
124
|
: undefined);
|
|
125
125
|
}
|
|
126
126
|
const returnedState = returned.get("state");
|
|
127
127
|
if (returnedState !== state) {
|
|
128
|
-
throw new CliError(`the redirect carried ${returnedState === null ? "no state" : "a state this run did not issue"}`, "authentication",
|
|
128
|
+
throw new CliError(`the redirect carried ${returnedState === null ? "no state" : "a state this run did not issue"}`, "authentication", `A browser tab left open from an earlier sign-in is the usual cause. Close it and run ${programName} login again.`);
|
|
129
129
|
}
|
|
130
130
|
const code = returned.get("code");
|
|
131
131
|
if (code === null) {
|
|
@@ -142,14 +142,14 @@ async function exchange(runtime, server, form) {
|
|
|
142
142
|
if (response.status !== 200) {
|
|
143
143
|
const error = response.body?.error;
|
|
144
144
|
throw new CliError(`the authorization server refused the token request (${error ?? String(response.status)})`, "authentication", error === "invalid_grant"
|
|
145
|
-
?
|
|
145
|
+
? `The grant is no longer usable — it expired, was revoked, or was already spent. Run ${runtime.programName} login again.`
|
|
146
146
|
: undefined);
|
|
147
147
|
}
|
|
148
148
|
const body = response.body;
|
|
149
149
|
return {
|
|
150
|
-
accessToken: requireString(body["access_token"], "access_token", server.tokenEndpoint),
|
|
150
|
+
accessToken: requireString(body["access_token"], "access_token", server.tokenEndpoint, runtime.programName),
|
|
151
151
|
expiresInSeconds: typeof body["expires_in"] === "number" ? body["expires_in"] : 0,
|
|
152
|
-
refreshToken: requireString(body["refresh_token"], "refresh_token", server.tokenEndpoint),
|
|
152
|
+
refreshToken: requireString(body["refresh_token"], "refresh_token", server.tokenEndpoint, runtime.programName),
|
|
153
153
|
scope: typeof body["scope"] === "string" ? body["scope"] : "",
|
|
154
154
|
};
|
|
155
155
|
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { type ParsedArguments } from "./args.js";
|
|
2
|
+
import { type CommandResult } from "./output.js";
|
|
3
|
+
import { type CliRuntime } from "./runtime.js";
|
|
4
|
+
/**
|
|
5
|
+
* Operating and diagnosing, after first value has already happened.
|
|
6
|
+
*
|
|
7
|
+
* W10-01 built the shortest path from an empty machine to an observed callback.
|
|
8
|
+
* This is the rest of the lifecycle: listing what exists, changing it, watching
|
|
9
|
+
* it, and recovering from it. The split is by *when somebody runs it* rather
|
|
10
|
+
* than by resource, which is why `schedules create` is still next to `login`
|
|
11
|
+
* and `schedules cancel` is here.
|
|
12
|
+
*
|
|
13
|
+
* Every command in this file follows the same three rules. It reads before it
|
|
14
|
+
* changes, so the confirmation shows the object as it is now rather than
|
|
15
|
+
* repeating the arguments back. It sends the conditional header when one was
|
|
16
|
+
* given, so a change to something somebody else has moved is refused rather
|
|
17
|
+
* than applied. And it renders identifiers in a column somebody can select,
|
|
18
|
+
* because the next command needs them.
|
|
19
|
+
*/
|
|
20
|
+
type Command = (runtime: CliRuntime, args: ParsedArguments) => Promise<CommandResult>;
|
|
21
|
+
/**
|
|
22
|
+
* A schedule definition from the command line.
|
|
23
|
+
*
|
|
24
|
+
* **One recurrence, named explicitly.** The four spellings are the four the
|
|
25
|
+
* contract publishes, and giving none or giving two is refused here rather than
|
|
26
|
+
* sent: "you gave --in and --cron" is a sentence about the line somebody typed,
|
|
27
|
+
* and `invalid_request` a round trip later is not.
|
|
28
|
+
*
|
|
29
|
+
* `--time-zone` is what keeps daylight saving honest. A cron expression without
|
|
30
|
+
* one fires on a fixed offset, so "every weekday at 09:00" drifts an hour twice
|
|
31
|
+
* a year; with one, the service moves the instant and the local time stays put.
|
|
32
|
+
* The command line does not choose a default zone, because guessing the
|
|
33
|
+
* machine's would make the same expression mean different things on a laptop
|
|
34
|
+
* and on a build runner.
|
|
35
|
+
*/
|
|
36
|
+
declare function scheduleRequest(runtime: CliRuntime, args: ParsedArguments, usage: string): Promise<Readonly<Record<string, unknown>>>;
|
|
37
|
+
/** The operate-and-diagnose commands, keyed by the words that select them. */
|
|
38
|
+
export declare const OPERATE_COMMANDS: Readonly<Record<string, Command>>;
|
|
39
|
+
export { scheduleRequest, type Command };
|