@greatwait/cli 0.1.0-alpha.5 → 0.1.0-alpha.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/dist/operate.js CHANGED
@@ -1,10 +1,10 @@
1
- import { billingAutoTopUpApplyRequestSchema, billingAutoTopUpPreviewRequestSchema, billingAutoTopUpPreviewSchema, billingCeilingUpdateRequestSchema, billingFundingQuoteRequestSchema, billingFundingQuoteSchema, billingNotificationSettingsUpdateRequestSchema, billingRefundRequestCreateSchema, billingRefundResolutionRequestSchema, projectBillingQuoteSchema, projectBillingQuoteRequestSchema, } from "@greatwait/contracts";
1
+ import { httpRequestSchema, endpointHeaderChangesSchema, billingAutoTopUpApplyRequestSchema, billingAutoTopUpPreviewRequestSchema, billingAutoTopUpPreviewSchema, billingCeilingUpdateRequestSchema, billingFundingQuoteRequestSchema, billingFundingQuoteSchema, billingNotificationSettingsUpdateRequestSchema, billingRefundRequestCreateSchema, billingRefundResolutionRequestSchema, projectBillingQuoteSchema, projectBillingQuoteRequestSchema, } from "@greatwait/contracts";
2
2
  import { parseDuration } from "./args.js";
3
3
  import { confirmChange } from "./confirm.js";
4
4
  import { redactSecret } from "./output.js";
5
5
  import { CliError } from "./runtime.js";
6
6
  import { openSession } from "./session.js";
7
- import { apiUrlOption, conditionalHeaders, continuationNote, drainPages, freshnessNote, pageRequest, parseInstant, parseResponse, positionals, readJsonOption, table, } from "./support.js";
7
+ import { apiUrlOption, conditionalHeaders, continuationNote, drainPages, freshnessNote, pageRequest, parseInstant, parseResponse, positionals, readJsonOption, readPrivateJsonOption, table, } from "./support.js";
8
8
  /* -------------------------------------------------------------------------- */
9
9
  /* Shared plumbing */
10
10
  /* -------------------------------------------------------------------------- */
@@ -102,18 +102,78 @@ async function scheduleRequest(runtime, args, usage) {
102
102
  ? { everySeconds: parseDuration(value) }
103
103
  : { cron: value };
104
104
  const endpoint = args.options["--endpoint"];
105
+ const url = args.options["--url"];
106
+ if (endpoint !== undefined && url !== undefined) {
107
+ // Refused here as well as by the service, because "you gave both" is a
108
+ // sentence about the line somebody typed. The service's own refusal says
109
+ // the same thing a round trip later.
110
+ throw new CliError("give --endpoint or --url, not both", "usage", "--url uses this project's destination at that address, or creates one. --endpoint names a destination that already exists.");
111
+ }
105
112
  const data = await readJsonOption(runtime, "--data", args.options["--data"]);
113
+ const httpInput = await readPrivateJsonOption(runtime, "--http", args.options["--http"]);
114
+ const http = httpInput === undefined
115
+ ? undefined
116
+ : httpRequestSchema.safeParse(httpInput);
117
+ if (http !== undefined && !http.success)
118
+ throw new CliError("--http settings are invalid", "usage", http.error.issues.map((issue) => issue.message).join("; "));
106
119
  const latencyClass = args.options["--latency-class"];
107
120
  const partitionKey = args.options["--partition-key"];
108
121
  return {
109
122
  ...(data === undefined ? {} : { data }),
123
+ ...(http?.success ? { http: http.data } : {}),
110
124
  ...(endpoint === undefined ? {} : { endpoint }),
125
+ ...(url === undefined ? {} : { url }),
111
126
  ...(latencyClass === undefined ? {} : { latencyClass }),
112
127
  ...(partitionKey === undefined ? {} : { partitionKey }),
113
128
  ...(timeZone === undefined ? {} : { timeZone }),
114
129
  ...recurrence,
115
130
  };
116
131
  }
132
+ /**
133
+ * Where a schedule written with `--url` delivers, and where its signing
134
+ * secret is.
135
+ *
136
+ * **Only the API's notice, never a secret.** A schedule write that names a
137
+ * `url` answers with `endpointSigningSecret` — `returned: false` and the
138
+ * dashboard page where a member reveals the secret — and nothing else about
139
+ * it, so there is nothing here that could print one. Absent when the write
140
+ * named `--endpoint`: that caller already holds the destination it chose.
141
+ */
142
+ export function destinationLines(schedule, notice,
143
+ /**
144
+ * The `--url` that was sent. A schedule write answers without
145
+ * `endpointUrl` (only reads carry it), and the address is the one the
146
+ * destination was found or made at.
147
+ */
148
+ requestedUrl) {
149
+ if (notice === undefined)
150
+ return [];
151
+ const address = schedule.endpointUrl ?? canonical(requestedUrl);
152
+ return [
153
+ ...(schedule.endpoint === undefined
154
+ ? []
155
+ : [
156
+ `Destination ${schedule.endpoint}${address === undefined ? "" : ` at ${address}`}`,
157
+ ]),
158
+ ``,
159
+ `GreatWait signs every call to this address. Your receiver checks the`,
160
+ `signature with the destination's signing secret, which isn't shown here.`,
161
+ notice.revealUrl === undefined
162
+ ? `A project member can reveal it on the destination's page in the dashboard.`
163
+ : `Reveal it on the destination's page: ${notice.revealUrl}`,
164
+ ];
165
+ }
166
+ /** An address as the service stores it, or as typed if it will not parse. */
167
+ function canonical(url) {
168
+ if (url === undefined)
169
+ return undefined;
170
+ try {
171
+ return new URL(url).href;
172
+ }
173
+ catch {
174
+ return url;
175
+ }
176
+ }
117
177
  /** How a schedule's recurrence reads in one column. */
118
178
  function recurrenceOf(schedule) {
119
179
  return schedule.recurrence.kind;
@@ -176,6 +236,12 @@ const destinationsUpdate = async (runtime, args) => {
176
236
  if (url === undefined) {
177
237
  throw new CliError(`${runtime.programName} destinations update needs --url`, "usage", `Usage: ${runtime.programName} destinations update <destination> --url https://your-receiver.example.com/greatwait`);
178
238
  }
239
+ const headerInput = await readPrivateJsonOption(runtime, "--headers", args.options["--headers"]);
240
+ const headers = headerInput === undefined
241
+ ? undefined
242
+ : endpointHeaderChangesSchema.safeParse(headerInput);
243
+ if (headers !== undefined && !headers.success)
244
+ throw new CliError("--headers settings are invalid", "usage", headers.error.issues.map((issue) => issue.message).join("; "));
179
245
  const active = await session(runtime, args);
180
246
  // Read first, so the preview is the destination as it stands rather than the
181
247
  // arguments read back.
@@ -188,7 +254,7 @@ const destinationsUpdate = async (runtime, args) => {
188
254
  `Deliveries in flight keep going to the receiver they started for.`,
189
255
  ],
190
256
  });
191
- const updated = parseResponse("endpoints.update", await active.client.request("endpoints_update", id, { url }, conditionalHeaders(args)));
257
+ const updated = parseResponse("endpoints.update", await active.client.request("endpoints_update", id, { url, ...(headers?.success ? { headers: headers.data } : {}) }, conditionalHeaders(args)));
192
258
  return {
193
259
  data: { confirmation, endpoint: updated },
194
260
  human: endpointBlock(updated).join("\n"),
@@ -279,6 +345,9 @@ async function scheduleChange(runtime, args, options) {
279
345
  return {
280
346
  data: {
281
347
  ...(confirmation === undefined ? {} : { confirmation }),
348
+ ...(view.endpointSigningSecret === undefined
349
+ ? {}
350
+ : { endpointSigningSecret: view.endpointSigningSecret }),
282
351
  ...(view.occurrence === undefined ? {} : { occurrence: view.occurrence }),
283
352
  schedule: view.schedule,
284
353
  },
@@ -292,12 +361,13 @@ async function scheduleChange(runtime, args, options) {
292
361
  : [
293
362
  `Next ${view.occurrence.id} planned for ${view.occurrence.plannedFor} (${view.occurrence.state})`,
294
363
  ]),
364
+ ...destinationLines(view.schedule, view.endpointSigningSecret, args.options["--url"]),
295
365
  ].join("\n"),
296
366
  ...(nextMove === undefined ? {} : { nextMove }),
297
367
  };
298
368
  }
299
369
  const schedulesReplace = async (runtime, args) => {
300
- const usage = `${runtime.programName} schedules replace <schedule> --endpoint <destination> (--in <duration> | --at <instant> | --every <duration> | --cron <expression>)`;
370
+ const usage = `${runtime.programName} schedules replace <schedule> (--endpoint <destination> | --url <https url>) (--in <duration> | --at <instant> | --every <duration> | --cron <expression>)`;
301
371
  const [id] = positionals(args, ["<schedule>"], usage);
302
372
  const request = await scheduleRequest(runtime, args, usage);
303
373
  return scheduleChange(runtime, args, {
@@ -306,7 +376,8 @@ const schedulesReplace = async (runtime, args) => {
306
376
  action: `Replace the definition of schedule ${current.id}.`,
307
377
  effect: [
308
378
  `now ${recurrenceOf(current)}, ${current.status}, version ${String(current.version)}`,
309
- `after the definition on this command line, and nothing of the old one`,
379
+ `after the timing on this command line. Destination, data and HTTP`,
380
+ ` settings are kept unless this command line gives new ones.`,
310
381
  `Occurrences already planned under the old definition are discarded.`,
311
382
  ],
312
383
  }),
@@ -1387,7 +1458,9 @@ const sinksCreate = async (runtime, args) => {
1387
1458
  const program = await readJsonOption(runtime, "--program", args.options["--program"]);
1388
1459
  const sink = parseResponse("testsinks.create", await active.client.request("testsinks_create", {
1389
1460
  endpoint,
1390
- ...(program === undefined ? {} : { program }),
1461
+ // The program is passed as typed and validated by the service, which
1462
+ // names the field it refuses; the client's type cannot know it yet.
1463
+ ...(program === undefined ? {} : { program: program }),
1391
1464
  }));
1392
1465
  return {
1393
1466
  data: { sink },
@@ -30,6 +30,6 @@ export declare function remediationFor(problem: Problem | undefined, retryable:
30
30
  * A case asserting what the advice *says* wants one spelling; a case asserting
31
31
  * that it follows the invoked name passes its own.
32
32
  */
33
- export declare const REMEDIATION: Readonly<Record<"invalid_request" | "not_found" | "state_conflict" | "version_conflict" | "authentication_required" | "billing_not_ready" | "capability_not_configured" | "credential_expired" | "credential_revoked" | "idempotency_key_reused" | "payload_too_large" | "plan_limit_reached" | "project_archived" | "rate_limited" | "revision_conflict" | "scope_missing" | "service_unavailable" | "test_to_live_escalation", string>>;
33
+ export declare const REMEDIATION: Readonly<Record<"authentication_required" | "billing_not_ready" | "capability_not_configured" | "credential_expired" | "credential_revoked" | "idempotency_key_reused" | "invalid_request" | "not_found" | "payload_too_large" | "plan_limit_reached" | "project_archived" | "rate_limited" | "revision_conflict" | "scope_missing" | "service_unavailable" | "state_conflict" | "test_to_live_escalation" | "version_conflict", string>>;
34
34
  /** Exposed so a case can assert the records cover the published vocabularies. */
35
35
  export { isRetryable, remediationTable };
package/dist/support.d.ts CHANGED
@@ -31,6 +31,8 @@ export declare function expectNoExtraArguments(args: ParsedArguments, used: numb
31
31
  * "unexpected argument" rule is applied from the same count.
32
32
  */
33
33
  export declare function positionals(args: ParsedArguments, names: readonly string[], usage: string): readonly string[];
34
+ /** Secret-bearing option documents are accepted through files/stdin only. */
35
+ export declare function readPrivateJsonOption(runtime: CliRuntime, flag: string, value: string | undefined): Promise<unknown>;
34
36
  /** The payload, from an argument, a file, or standard input. */
35
37
  export declare function readJsonOption(runtime: CliRuntime, flag: string, value: string | undefined): Promise<unknown>;
36
38
  /**
package/dist/support.js CHANGED
@@ -76,6 +76,13 @@ export function positionals(args, names, usage) {
76
76
  expectNoExtraArguments(args, names.length, usage);
77
77
  return found;
78
78
  }
79
+ /** Secret-bearing option documents are accepted through files/stdin only. */
80
+ export async function readPrivateJsonOption(runtime, flag, value) {
81
+ if (value !== undefined && value !== "-" && !value.startsWith("@")) {
82
+ throw new CliError(`${flag} must be @file.json or -`, "usage", "Read secret-bearing settings from a private file or standard input, never a command-line argument.");
83
+ }
84
+ return readJsonOption(runtime, flag, value);
85
+ }
79
86
  /** The payload, from an argument, a file, or standard input. */
80
87
  export async function readJsonOption(runtime, flag, value) {
81
88
  if (value === undefined)
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.1.0-alpha.5",
11
- "@greatwait/contracts": "0.1.0-alpha.5"
10
+ "@greatwait/api-client": "0.1.0-alpha.6",
11
+ "@greatwait/contracts": "0.1.0-alpha.6"
12
12
  },
13
13
  "description": "The GreatWait command line: sign in, create a destination and a one-shot schedule, and wait for the callback, over the public API.",
14
14
  "engines": {
@@ -48,5 +48,5 @@
48
48
  },
49
49
  "sideEffects": false,
50
50
  "type": "module",
51
- "version": "0.1.0-alpha.5"
51
+ "version": "0.1.0-alpha.6"
52
52
  }