@greatwait/cli 0.3.0 → 0.3.1
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 +9 -0
- package/dist/catalog.js +29 -1
- package/dist/commands.js +10 -1
- package/dist/operate.d.ts +1 -1
- package/dist/operate.js +44 -22
- package/dist/output.js +3 -0
- package/dist/session.d.ts +11 -0
- package/dist/session.js +34 -32
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -43,6 +43,13 @@ directory, readable by you and nobody else. A staging credential is never
|
|
|
43
43
|
presented to production, because the two are separate entries rather than one
|
|
44
44
|
current context.
|
|
45
45
|
|
|
46
|
+
For a short script that needs a bearer token, `greatwait token` prints the
|
|
47
|
+
credential for the selected API origin, refreshing an expiring OAuth sign-in
|
|
48
|
+
first. Add `--api-url <origin>` to choose an origin explicitly. `--json` prints
|
|
49
|
+
`{token, origin, expiresAt}`; an API key is returned unchanged with `null` expiry.
|
|
50
|
+
This output is a secret: keep it out of logs, shell tracing and shared files.
|
|
51
|
+
Use a scoped API key for unattended automation.
|
|
52
|
+
|
|
46
53
|
## First callback
|
|
47
54
|
|
|
48
55
|
Use a public HTTPS receiver you control. Once signed in, schedule directly to
|
|
@@ -99,6 +106,8 @@ grant and remove local credentials.
|
|
|
99
106
|
## Output and exit codes
|
|
100
107
|
|
|
101
108
|
`--json` is a versioned machine shape, not a serialization of the human text.
|
|
109
|
+
The deliberate secret-output command `token --json` returns the credential object
|
|
110
|
+
described above.
|
|
102
111
|
The exit status is a class rather than a code per failure:
|
|
103
112
|
|
|
104
113
|
| code | meaning |
|
package/dist/catalog.js
CHANGED
|
@@ -493,6 +493,14 @@ export const COMMAND_CATALOG = [
|
|
|
493
493
|
summary: "Sign out, and forget the credential stored on this machine.",
|
|
494
494
|
words: "logout",
|
|
495
495
|
},
|
|
496
|
+
{
|
|
497
|
+
group: "Session",
|
|
498
|
+
operations: [],
|
|
499
|
+
options: [API_URL],
|
|
500
|
+
positionals: [],
|
|
501
|
+
summary: "Print the credential for the selected origin, refreshing OAuth first when needed. Output is a secret: keep it out of logs and shell tracing. --json prints token, origin and expiresAt (null for API keys).",
|
|
502
|
+
words: "token",
|
|
503
|
+
},
|
|
496
504
|
{
|
|
497
505
|
group: "Session",
|
|
498
506
|
operations: ["context.read"],
|
|
@@ -831,10 +839,20 @@ export const COMMAND_CATALOG = [
|
|
|
831
839
|
summary: "JSON your receiver gets with the call. Inline, @file.json, or - for standard input.",
|
|
832
840
|
value: "<json>",
|
|
833
841
|
},
|
|
842
|
+
{
|
|
843
|
+
flag: "--completion",
|
|
844
|
+
value: "<response|callback>",
|
|
845
|
+
summary: "How work finishes: response or callback. Defaults to response on create; replace keeps the stored value when omitted.",
|
|
846
|
+
},
|
|
847
|
+
{
|
|
848
|
+
flag: "--on-overlap",
|
|
849
|
+
value: "<skip|queue>",
|
|
850
|
+
summary: "When the next run is due before work finishes: skip or queue. Defaults to skip on create; replace keeps the stored value when omitted.",
|
|
851
|
+
},
|
|
834
852
|
{
|
|
835
853
|
flag: "--delivery-timeout",
|
|
836
854
|
value: "<duration>",
|
|
837
|
-
summary: "How long the receiver may take, such as 20s or 5m (whole seconds). Defaults to 5s on create; replace keeps the stored timeout. Response completion allows up to 30m;
|
|
855
|
+
summary: "How long the receiver may take, such as 20s or 5m (whole seconds). Defaults to 5s on create; replace keeps the stored timeout. Response completion allows up to 30m; callback completion allows up to 24h. Separate from --timeout for --wait.",
|
|
838
856
|
},
|
|
839
857
|
IDEMPOTENCY,
|
|
840
858
|
{
|
|
@@ -918,6 +936,16 @@ export const COMMAND_CATALOG = [
|
|
|
918
936
|
summary: "JSON your receiver gets with the call. Inline, @file.json, or - for standard input.",
|
|
919
937
|
value: "<json>",
|
|
920
938
|
},
|
|
939
|
+
{
|
|
940
|
+
flag: "--completion",
|
|
941
|
+
value: "<response|callback>",
|
|
942
|
+
summary: "How work finishes: response or callback. Defaults to response on create; replace keeps the stored value when omitted.",
|
|
943
|
+
},
|
|
944
|
+
{
|
|
945
|
+
flag: "--on-overlap",
|
|
946
|
+
value: "<skip|queue>",
|
|
947
|
+
summary: "When the next run is due before work finishes: skip or queue. Defaults to skip on create; replace keeps the stored value when omitted.",
|
|
948
|
+
},
|
|
921
949
|
{
|
|
922
950
|
flag: "--delivery-timeout",
|
|
923
951
|
value: "<duration>",
|
package/dist/commands.js
CHANGED
|
@@ -10,7 +10,7 @@ import { awaitRedirect, buildAuthorization, codeFrom, discover, exchangeCode, re
|
|
|
10
10
|
import { redactSecret } from "./output.js";
|
|
11
11
|
import { CliError, sleepOrAbort } from "./runtime.js";
|
|
12
12
|
import { publishedWait, recoveryFor, renderJourney, STEP_COMMANDS, } from "./journey.js";
|
|
13
|
-
import { anonymousClient, ENVIRONMENT_NAMES, openSession, resolveOrigin, storeFor, } from "./session.js";
|
|
13
|
+
import { anonymousClient, ENVIRONMENT_NAMES, openCredential, openSession, resolveOrigin, storeFor, } from "./session.js";
|
|
14
14
|
import { OPERATE_COMMANDS, destinationLines, scheduleRequest } from "./operate.js";
|
|
15
15
|
import { apiUrlOption, expectNoExtraArguments, parseResponse, positionals, } from "./support.js";
|
|
16
16
|
/* -------------------------------------------------------------------------- */
|
|
@@ -317,6 +317,13 @@ const CREDENTIAL_LABELS = {
|
|
|
317
317
|
"environment-key": `an API credential from ${ENVIRONMENT_NAMES.apiKey}`,
|
|
318
318
|
oauth: "an OAuth grant",
|
|
319
319
|
};
|
|
320
|
+
const token = async (runtime, args) => {
|
|
321
|
+
expectNoExtraArguments(args, 0, `${runtime.programName} token [--api-url <origin>]`);
|
|
322
|
+
const { token, origin, expiresAt } = await openCredential(runtime, {
|
|
323
|
+
apiUrl: apiUrlOption(args),
|
|
324
|
+
});
|
|
325
|
+
return { human: token, data: { token, origin, expiresAt } };
|
|
326
|
+
};
|
|
320
327
|
const whoami = async (runtime, args) => {
|
|
321
328
|
expectNoExtraArguments(args, 0, `${runtime.programName} whoami [--api-url <origin>]`);
|
|
322
329
|
const session = await openSession(runtime, { apiUrl: apiUrlOption(args) });
|
|
@@ -677,6 +684,7 @@ const schedulesCreate = async (runtime, args) => {
|
|
|
677
684
|
const schedule = created.schedule.id;
|
|
678
685
|
const occurrence = created.occurrence.id;
|
|
679
686
|
const data = {
|
|
687
|
+
delivery: created.schedule.delivery,
|
|
680
688
|
...(created.endpointSigningSecret === undefined
|
|
681
689
|
? {}
|
|
682
690
|
: {
|
|
@@ -1052,6 +1060,7 @@ const IMPLEMENTATIONS = {
|
|
|
1052
1060
|
"occurrences wait": occurrencesWait,
|
|
1053
1061
|
"schedules create": schedulesCreate,
|
|
1054
1062
|
"schedules read": schedulesRead,
|
|
1063
|
+
token,
|
|
1055
1064
|
whoami,
|
|
1056
1065
|
};
|
|
1057
1066
|
/**
|
package/dist/operate.d.ts
CHANGED
|
@@ -34,7 +34,7 @@ type Command = (runtime: CliRuntime, args: ParsedArguments) => Promise<CommandRe
|
|
|
34
34
|
* machine's would make the same expression mean different things on a laptop
|
|
35
35
|
* and on a build runner.
|
|
36
36
|
*/
|
|
37
|
-
declare function scheduleRequest(runtime: CliRuntime, args: ParsedArguments, usage: string): Promise<Readonly<Record<string, unknown>>>;
|
|
37
|
+
declare function scheduleRequest(runtime: CliRuntime, args: ParsedArguments, usage: string, mode?: "create" | "replace"): Promise<Readonly<Record<string, unknown>>>;
|
|
38
38
|
/**
|
|
39
39
|
* Where a schedule written with `--url` delivers, and where its signing
|
|
40
40
|
* secret is.
|
package/dist/operate.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { BULK_COMMANDS } from "./bulk.js";
|
|
2
2
|
import { writeFile } from "node:fs/promises";
|
|
3
|
-
import { projectNotificationSettingsUpdateRequestSchema, statusSigningKeyRequestSchema, autoPauseAfterFailuresSchema, deliveryDurationSecondsSchema, httpRequestSchema, externalIdentifierSchema, scheduleTagsSchema, scheduleTagSchema, scheduleSelectionSchema, normalizeScheduleSelection, projectAttemptSearchRequestSchema, normalizeAttemptSelection, sanitizedDiagnosticText, diagnosticBodyText, responseBodySentence, requestCaptureSentence, attemptStatusGuidance, endpointHeaderChangesSchema, billingAutoTopUpApplyRequestSchema, billingAutoTopUpPreviewRequestSchema, billingAutoTopUpPreviewSchema, billingCeilingUpdateRequestSchema, billingFundingQuoteRequestSchema, billingFundingQuoteSchema, billingNotificationSettingsUpdateRequestSchema, billingRefundRequestCreateSchema, billingRefundResolutionRequestSchema, billingRedeemRequestSchema, projectBillingQuoteSchema, projectBillingQuoteRequestSchema, } from "@greatwait/contracts";
|
|
3
|
+
import { projectNotificationSettingsUpdateRequestSchema, statusSigningKeyRequestSchema, autoPauseAfterFailuresSchema, deliveryDurationSecondsSchema, deliveryCompletionModeSchema, deliveryOverlapPolicySchema, scheduleRequestSchema, httpRequestSchema, externalIdentifierSchema, scheduleTagsSchema, scheduleTagSchema, scheduleSelectionSchema, normalizeScheduleSelection, projectAttemptSearchRequestSchema, normalizeAttemptSelection, sanitizedDiagnosticText, diagnosticBodyText, responseBodySentence, requestCaptureSentence, attemptStatusGuidance, endpointHeaderChangesSchema, billingAutoTopUpApplyRequestSchema, billingAutoTopUpPreviewRequestSchema, billingAutoTopUpPreviewSchema, billingCeilingUpdateRequestSchema, billingFundingQuoteRequestSchema, billingFundingQuoteSchema, billingNotificationSettingsUpdateRequestSchema, billingRefundRequestCreateSchema, billingRefundResolutionRequestSchema, billingRedeemRequestSchema, projectBillingQuoteSchema, projectBillingQuoteRequestSchema, } from "@greatwait/contracts";
|
|
4
4
|
import { parseDuration } from "./args.js";
|
|
5
5
|
import { confirmChange } from "./confirm.js";
|
|
6
6
|
import { redactSecret } from "./output.js";
|
|
@@ -80,7 +80,7 @@ const RECURRENCE_FLAGS = ["--in", "--at", "--every", "--cron"];
|
|
|
80
80
|
* machine's would make the same expression mean different things on a laptop
|
|
81
81
|
* and on a build runner.
|
|
82
82
|
*/
|
|
83
|
-
async function scheduleRequest(runtime, args, usage) {
|
|
83
|
+
async function scheduleRequest(runtime, args, usage, mode = "create") {
|
|
84
84
|
const given = RECURRENCE_FLAGS.filter((flag) => args.options[flag] !== undefined);
|
|
85
85
|
if (given.length !== 1) {
|
|
86
86
|
throw new CliError(given.length === 0
|
|
@@ -132,6 +132,27 @@ async function scheduleRequest(runtime, args, usage) {
|
|
|
132
132
|
: deliveryDurationSecondsSchema.safeParse(parseDuration(timeoutInput));
|
|
133
133
|
if (timeout !== undefined && !timeout.success)
|
|
134
134
|
throw new CliError("--delivery-timeout is invalid", "usage", timeout.error.issues.map((issue) => issue.message).join("; "));
|
|
135
|
+
const completionInput = args.options["--completion"];
|
|
136
|
+
const completion = completionInput === undefined
|
|
137
|
+
? undefined
|
|
138
|
+
: deliveryCompletionModeSchema.safeParse(completionInput);
|
|
139
|
+
if (completion !== undefined && !completion.success)
|
|
140
|
+
throw new CliError('--completion (delivery.completion) must be "response" or "callback"', "usage");
|
|
141
|
+
const overlapInput = args.options["--on-overlap"];
|
|
142
|
+
const overlap = overlapInput === undefined
|
|
143
|
+
? undefined
|
|
144
|
+
: deliveryOverlapPolicySchema.safeParse(overlapInput);
|
|
145
|
+
if (overlap !== undefined && !overlap.success)
|
|
146
|
+
throw new CliError('--on-overlap (delivery.onOverlap) must be "skip" or "queue"', "usage");
|
|
147
|
+
const delivery = {
|
|
148
|
+
...(timeout?.success ? { maxDurationSeconds: timeout.data } : {}),
|
|
149
|
+
...(completion?.success ? { completion: completion.data } : {}),
|
|
150
|
+
...(overlap?.success ? { onOverlap: overlap.data } : {}),
|
|
151
|
+
};
|
|
152
|
+
// Replacement validates the resulting pair after reading the stored settings.
|
|
153
|
+
// An omitted completion there can mean a retained callback, not response.
|
|
154
|
+
if (mode === "create")
|
|
155
|
+
validateDelivery(delivery);
|
|
135
156
|
const external_identifier = args.options["--external-identifier"];
|
|
136
157
|
if (external_identifier !== undefined &&
|
|
137
158
|
!externalIdentifierSchema.safeParse(external_identifier).success)
|
|
@@ -145,9 +166,7 @@ async function scheduleRequest(runtime, args, usage) {
|
|
|
145
166
|
...(external_identifier === undefined ? {} : { external_identifier }),
|
|
146
167
|
...(parsedTags?.success ? { tags: parsedTags.data } : {}),
|
|
147
168
|
...(http?.success ? { http: http.data } : {}),
|
|
148
|
-
...(
|
|
149
|
-
? { delivery: { maxDurationSeconds: timeout.data } }
|
|
150
|
-
: {}),
|
|
169
|
+
...(Object.keys(delivery).length === 0 ? {} : { delivery }),
|
|
151
170
|
...(endpoint === undefined ? {} : { endpoint }),
|
|
152
171
|
...(url === undefined ? {} : { url }),
|
|
153
172
|
...(latencyClass === undefined ? {} : { latencyClass }),
|
|
@@ -157,6 +176,14 @@ async function scheduleRequest(runtime, args, usage) {
|
|
|
157
176
|
...recurrence,
|
|
158
177
|
};
|
|
159
178
|
}
|
|
179
|
+
/** Use the canonical contract's relation between completion and timeout. */
|
|
180
|
+
function validateDelivery(delivery) {
|
|
181
|
+
const parsed = scheduleRequestSchema.safeParse({ delaySeconds: 1, delivery });
|
|
182
|
+
if (!parsed.success)
|
|
183
|
+
throw new CliError(parsed.error.issues
|
|
184
|
+
.map((issue) => `${issue.path.join(".")}: ${issue.message}`)
|
|
185
|
+
.join("; "), "usage");
|
|
186
|
+
}
|
|
160
187
|
/**
|
|
161
188
|
* Where a schedule written with `--url` delivers, and where its signing
|
|
162
189
|
* secret is.
|
|
@@ -518,35 +545,30 @@ async function scheduleChange(runtime, args, options) {
|
|
|
518
545
|
const schedulesReplace = async (runtime, args) => {
|
|
519
546
|
const usage = `${runtime.programName} schedules replace <schedule> (--endpoint <destination> | --url <https url>) (--in <duration> | --at <instant> | --every <duration> | --cron <expression>)`;
|
|
520
547
|
const [id] = positionals(args, ["<schedule>"], usage);
|
|
521
|
-
const request = await scheduleRequest(runtime, args, usage);
|
|
548
|
+
const request = await scheduleRequest(runtime, args, usage, "replace");
|
|
522
549
|
return scheduleChange(runtime, args, {
|
|
523
550
|
act: (active, scheduleId, current) => {
|
|
524
551
|
if (current === undefined)
|
|
525
552
|
throw new CliError("Replacement needs its schedule read", "internal");
|
|
526
|
-
const delivery =
|
|
527
|
-
|
|
528
|
-
|
|
553
|
+
const delivery = {
|
|
554
|
+
...current.schedule.delivery,
|
|
555
|
+
...request["delivery"],
|
|
556
|
+
};
|
|
557
|
+
const changesDelivery = request["delivery"] !== undefined;
|
|
558
|
+
validateDelivery(delivery);
|
|
559
|
+
if (changesDelivery &&
|
|
529
560
|
current.schedule.endpoint === undefined &&
|
|
530
561
|
request["endpoint"] === undefined &&
|
|
531
562
|
request["url"] === undefined)
|
|
532
|
-
throw new CliError("
|
|
563
|
+
throw new CliError("Delivery settings need a callback destination", "usage", "Give --endpoint or --url when attaching a callback to a timer-only schedule.");
|
|
533
564
|
// The API replaces delivery settings with defaults when callback fields
|
|
534
565
|
// are supplied. Carry all stored settings, including completion/overlap.
|
|
535
|
-
// Naming the retained endpoint also makes a
|
|
566
|
+
// Naming the retained endpoint also makes a delivery-only change reach
|
|
536
567
|
// that callback replacement path. Fence the read against concurrent edits.
|
|
537
568
|
return active.client.requestDetailed("schedules_replace", scheduleId, {
|
|
538
569
|
...request,
|
|
539
|
-
...(delivery ===
|
|
540
|
-
|
|
541
|
-
: {
|
|
542
|
-
delivery: {
|
|
543
|
-
...delivery,
|
|
544
|
-
...(timeout === undefined
|
|
545
|
-
? {}
|
|
546
|
-
: { maxDurationSeconds: parseDuration(timeout) }),
|
|
547
|
-
},
|
|
548
|
-
}),
|
|
549
|
-
...(timeout !== undefined &&
|
|
570
|
+
...(Object.keys(delivery).length === 0 ? {} : { delivery }),
|
|
571
|
+
...(changesDelivery &&
|
|
550
572
|
request["endpoint"] === undefined &&
|
|
551
573
|
request["url"] === undefined &&
|
|
552
574
|
current.schedule.endpoint !== undefined
|
package/dist/output.js
CHANGED
|
@@ -27,6 +27,9 @@ export function renderResult(command, result, json) {
|
|
|
27
27
|
? `${result.human}\n`
|
|
28
28
|
: `${result.human}\n\n${result.nextMove}\n`;
|
|
29
29
|
}
|
|
30
|
+
// The token command is a shell credential primitive with a bare JSON shape.
|
|
31
|
+
if (command === "token")
|
|
32
|
+
return `${JSON.stringify(result.data)}\n`;
|
|
30
33
|
return `${JSON.stringify({
|
|
31
34
|
command,
|
|
32
35
|
data: result.data,
|
package/dist/session.d.ts
CHANGED
|
@@ -49,9 +49,20 @@ export declare function anonymousClient(runtime: CliRuntime, origin: string): De
|
|
|
49
49
|
* reason nothing on their machine explains. Persisting first costs one write
|
|
50
50
|
* and removes the whole failure.
|
|
51
51
|
*/
|
|
52
|
+
export interface SessionCredential {
|
|
53
|
+
readonly token: string;
|
|
54
|
+
readonly expiresAt: string | null;
|
|
55
|
+
readonly credentialKind: Session["credentialKind"];
|
|
56
|
+
readonly origin: string;
|
|
57
|
+
readonly originSource: ResolvedOrigin["source"];
|
|
58
|
+
}
|
|
52
59
|
export declare function openSession(runtime: CliRuntime, options: {
|
|
53
60
|
readonly apiUrl?: string | undefined;
|
|
54
61
|
}): Promise<Session>;
|
|
62
|
+
/** Resolve the exact credential used by commands, persisting refresh before use. */
|
|
63
|
+
export declare function openCredential(runtime: CliRuntime, options: {
|
|
64
|
+
readonly apiUrl?: string | undefined;
|
|
65
|
+
}): Promise<SessionCredential>;
|
|
55
66
|
/**
|
|
56
67
|
* How long before expiry a grant is refreshed rather than presented.
|
|
57
68
|
*
|
package/dist/session.js
CHANGED
|
@@ -69,31 +69,28 @@ export function anonymousClient(runtime, origin) {
|
|
|
69
69
|
fetch: runtime.fetch,
|
|
70
70
|
});
|
|
71
71
|
}
|
|
72
|
-
/**
|
|
73
|
-
* An authenticated session, refreshing an expired grant on the way.
|
|
74
|
-
*
|
|
75
|
-
* **The rotated pair is written back before the token is used.** A refresh
|
|
76
|
-
* exchange invalidates the refresh token it was given, so a process that
|
|
77
|
-
* refreshed, made its call, and crashed would leave a stored refresh token the
|
|
78
|
-
* service no longer honours — and the person would have to sign in again for a
|
|
79
|
-
* reason nothing on their machine explains. Persisting first costs one write
|
|
80
|
-
* and removes the whole failure.
|
|
81
|
-
*/
|
|
82
72
|
export async function openSession(runtime, options) {
|
|
73
|
+
const credential = await openCredential(runtime, options);
|
|
74
|
+
return {
|
|
75
|
+
client: createGreatwaitClient({
|
|
76
|
+
baseUrl: credential.origin,
|
|
77
|
+
credential: credential.token,
|
|
78
|
+
fetch: runtime.fetch,
|
|
79
|
+
}),
|
|
80
|
+
credentialKind: credential.credentialKind,
|
|
81
|
+
origin: credential.origin,
|
|
82
|
+
originSource: credential.originSource,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
/** Resolve the exact credential used by commands, persisting refresh before use. */
|
|
86
|
+
export async function openCredential(runtime, options) {
|
|
83
87
|
const store = storeFor(runtime);
|
|
84
88
|
const resolved = await resolveOrigin(runtime, store, options.apiUrl);
|
|
85
|
-
// **The environment wins, and only for an API key.** Non-interactive
|
|
86
|
-
// automation is the case this exists for, and the journey's own variable is
|
|
87
|
-
// the one it already exports. A stored OAuth grant is a person's, and a cron
|
|
88
|
-
// job that silently used one would be acting as them.
|
|
89
89
|
const fromEnvironment = runtime.env[ENVIRONMENT_NAMES.apiKey];
|
|
90
90
|
if (fromEnvironment !== undefined && fromEnvironment !== "") {
|
|
91
91
|
return {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
credential: fromEnvironment,
|
|
95
|
-
fetch: runtime.fetch,
|
|
96
|
-
}),
|
|
92
|
+
token: fromEnvironment,
|
|
93
|
+
expiresAt: null,
|
|
97
94
|
credentialKind: "environment-key",
|
|
98
95
|
origin: resolved.origin,
|
|
99
96
|
originSource: resolved.source,
|
|
@@ -101,15 +98,11 @@ export async function openSession(runtime, options) {
|
|
|
101
98
|
}
|
|
102
99
|
const stored = await store.read(resolved.origin);
|
|
103
100
|
if (stored === undefined) {
|
|
104
|
-
throw new CliError(`no credential is stored for ${resolved.origin}`, "authentication", `Sign in
|
|
101
|
+
throw new CliError(`no credential is stored for ${resolved.origin}`, "authentication", `Sign in with ${runtime.programName} login --api-url ${resolved.origin} — or export ${ENVIRONMENT_NAMES.apiKey} for a non-interactive run.`);
|
|
105
102
|
}
|
|
106
|
-
const
|
|
103
|
+
const secret = await presentableSecret(runtime, store, resolved.origin, stored);
|
|
107
104
|
return {
|
|
108
|
-
|
|
109
|
-
baseUrl: resolved.origin,
|
|
110
|
-
credential,
|
|
111
|
-
fetch: runtime.fetch,
|
|
112
|
-
}),
|
|
105
|
+
...secret,
|
|
113
106
|
credentialKind: stored.kind,
|
|
114
107
|
origin: resolved.origin,
|
|
115
108
|
originSource: resolved.source,
|
|
@@ -132,26 +125,35 @@ async function presentableSecret(runtime, store, origin, stored) {
|
|
|
132
125
|
if (stored.secret === undefined) {
|
|
133
126
|
throw new CliError(`the stored credential for ${origin} carries no secret`, "authentication", `Sign in again: ${runtime.programName} login --api-url ${origin} --api-key`);
|
|
134
127
|
}
|
|
135
|
-
return stored.secret;
|
|
128
|
+
return { token: stored.secret, expiresAt: null };
|
|
136
129
|
}
|
|
137
130
|
const { accessToken, clientId, expiresAtMs, refreshToken } = stored;
|
|
138
131
|
if (accessToken === undefined || clientId === undefined) {
|
|
139
132
|
throw new CliError(`the stored grant for ${origin} is incomplete`, "authentication", `Sign in again: ${runtime.programName} login --api-url ${origin}`);
|
|
140
133
|
}
|
|
141
134
|
if (expiresAtMs === undefined ||
|
|
142
|
-
expiresAtMs - REFRESH_MARGIN_MS > runtime.now()
|
|
143
|
-
|
|
144
|
-
|
|
135
|
+
expiresAtMs - REFRESH_MARGIN_MS > runtime.now()) {
|
|
136
|
+
return {
|
|
137
|
+
token: accessToken,
|
|
138
|
+
expiresAt: expiresAtMs === undefined ? null : new Date(expiresAtMs).toISOString(),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
if (refreshToken === undefined) {
|
|
142
|
+
throw new CliError(`the stored grant for ${origin} needs refreshing but carries no refresh token`, "authentication", `Sign in with ${runtime.programName} login --api-url ${origin}`);
|
|
145
143
|
}
|
|
146
144
|
const server = await discover(runtime, origin);
|
|
147
145
|
const rotated = await refresh(runtime, server, { clientId, refreshToken });
|
|
146
|
+
const refreshedExpiresAtMs = runtime.now() + rotated.expiresInSeconds * 1000;
|
|
148
147
|
await store.write(origin, {
|
|
149
148
|
accessToken: rotated.accessToken,
|
|
150
149
|
clientId,
|
|
151
|
-
expiresAtMs:
|
|
150
|
+
expiresAtMs: refreshedExpiresAtMs,
|
|
152
151
|
kind: "oauth",
|
|
153
152
|
refreshToken: rotated.refreshToken,
|
|
154
153
|
scope: rotated.scope,
|
|
155
154
|
});
|
|
156
|
-
return
|
|
155
|
+
return {
|
|
156
|
+
token: rotated.accessToken,
|
|
157
|
+
expiresAt: new Date(refreshedExpiresAtMs).toISOString(),
|
|
158
|
+
};
|
|
157
159
|
}
|
package/package.json
CHANGED
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
"url": "https://github.com/flowxo/greatwait/issues"
|
|
8
8
|
},
|
|
9
9
|
"dependencies": {
|
|
10
|
-
"@greatwait/api-client": "0.3.
|
|
11
|
-
"@greatwait/contracts": "0.3.
|
|
10
|
+
"@greatwait/api-client": "0.3.1",
|
|
11
|
+
"@greatwait/contracts": "0.3.1"
|
|
12
12
|
},
|
|
13
13
|
"description": "The GreatWait command line: sign in, schedule a call to a URL, and read its delivery outcome over the public API.",
|
|
14
14
|
"engines": {
|
|
@@ -48,5 +48,5 @@
|
|
|
48
48
|
},
|
|
49
49
|
"sideEffects": false,
|
|
50
50
|
"type": "module",
|
|
51
|
-
"version": "0.3.
|
|
51
|
+
"version": "0.3.1"
|
|
52
52
|
}
|