wawesome 0.12.0 → 0.14.0

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.
Files changed (3) hide show
  1. package/README.md +59 -5
  2. package/dist/index.mjs +234 -30
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -117,9 +117,13 @@ npx wawesome deploy
117
117
  | `npx wawesome env list` | View environment variables for the current app |
118
118
  | `npx wawesome env set <key> <val>` | Set an environment variable (add `--secret` for write-only) |
119
119
  | `npx wawesome env rm <key>` | Delete an environment variable |
120
+ | `npx wawesome domains` | Show the app's domain, its state, and the DNS records to add |
121
+ | `npx wawesome domains detach <host>` | Stop serving the app at a domain you attached |
120
122
  | `npx wawesome credentials` | List deploy credentials: name, prefix, capabilities, Apps, last use |
121
123
  | `npx wawesome credentials mint <name>` | Mint a deploy credential and print its secret, once |
124
+ | `npx wawesome credentials regenerate <name>`| Replace a deploy credential's secret, keeping its name |
122
125
  | `npx wawesome credentials revoke <name>`| Revoke a deploy credential by name or prefix |
126
+ | `npx wawesome credentials delete <name>`| Delete a revoked or expired credential, freeing its name |
123
127
 
124
128
  ---
125
129
 
@@ -290,9 +294,9 @@ keyboard to log in. It belongs to the workspace rather than to you: it keeps wor
290
294
  and revoking it does not end your own session. Once you have one, [Deploying from CI](#deploying-from-ci)
291
295
  is what to do with it.
292
296
 
293
- Minting, listing and revoking belong to the workspace owner. They are closed to deploy credentials
294
- themselves, whatever those carry, so a leaked credential cannot mint another. All three run on your
295
- ordinary `npx wawesome login` session.
297
+ Minting, listing, regenerating, revoking and deleting belong to the workspace owner. They are closed
298
+ to deploy credentials themselves, whatever those carry, so a leaked credential cannot mint another or
299
+ renew its own expiry. All five run on your ordinary `npx wawesome login` session.
296
300
 
297
301
  ### Mint one
298
302
 
@@ -340,6 +344,27 @@ Name, displayable prefix, capabilities, Apps, last use and expiry. Never the sec
340
344
  platform no longer holds. `LAST USED` lags by a few minutes and reads `never` for a credential
341
345
  nothing has ever presented, which is a better reason to revoke one than any calendar date.
342
346
 
347
+ ### Rotate one
348
+
349
+ ```bash
350
+ npx wawesome credentials regenerate ci-pipeline
351
+ npx wawesome credentials regenerate ci --expires never --yes
352
+ npx wawesome credentials regenerate ci-pipeline > secret.txt
353
+ ```
354
+
355
+ Regenerating replaces the secret where it stands. The credential keeps its id, its name, its
356
+ capabilities and the Apps it may reach, so the only thing to change in your CI settings is the secret
357
+ itself. Its displayable prefix changes with it, since the prefix is what you match one against. The
358
+ new secret is printed once, on stdout alone, exactly as at minting.
359
+
360
+ It asks first, because anything holding the old secret stops working the moment this lands and has to
361
+ be given the new one; `--yes` is the answer where nobody is at the keyboard. Without `--expires` the
362
+ new secret lives ninety days, the same rule minting follows, including on a credential that never
363
+ expired.
364
+
365
+ An expired credential regenerates into a working one — that is the ordinary reason to reach for this.
366
+ A revoked one is refused, because nothing un-revokes a credential.
367
+
343
368
  ### Revoke one
344
369
 
345
370
  ```bash
@@ -352,6 +377,22 @@ where nobody is at the keyboard, and a prefix naming more than one credential is
352
377
  guessed at. A revocation lands on the very next request presenting the secret, and nothing un-revokes
353
378
  it.
354
379
 
380
+ ### Delete one
381
+
382
+ ```bash
383
+ npx wawesome credentials delete ci-pipeline
384
+ npx wawesome credentials delete wawe_ab3k9x --yes
385
+ ```
386
+
387
+ Deleting is a second act, not a shortcut through revoking, so only a credential that has already
388
+ stopped working can be deleted. Revoke a live one first. It resolves its target the way revoking
389
+ does, by name or by displayable prefix, and asks before it does it unless you pass `--yes`.
390
+
391
+ What deleting gives up is the reason revoking left the record behind. The credential leaves the
392
+ listing, the name comes free to mint again, and a pipeline still presenting the secret is told the
393
+ secret is unknown rather than that somebody revoked it. Whoever is debugging that pipeline goes
394
+ looking in their CI settings instead of your dashboard.
395
+
355
396
  ---
356
397
 
357
398
  ## Configuration and a custom gateway
@@ -527,6 +568,19 @@ Your deploy waits for neither. It attaches the domain, prints the records and fi
527
568
  later deploy states where the domain got to: not verified, ownership verified, certificate issued, or
528
569
  serving. A domain that is already serving is a line saying so and nothing else.
529
570
 
571
+ The same block is there when you are not deploying:
572
+
573
+ ```bash
574
+ npx wawesome domains
575
+ ```
576
+
577
+ It prints the domain, where it got to, when the platform last checked, and the records to add. It
578
+ changes nothing, and a deploy credential carrying `deploy` can run it, so a pipeline reads the same
579
+ thing you do. The TXT record is minted a moment after the name is claimed, and an attach that outran
580
+ it prints a row saying so rather than dropping the record from the list. That row is what this
581
+ command fills in, and it is where the deploy sends you: deploying again would change nothing, and a
582
+ deploy that changes nothing is refused.
583
+
530
584
  A custom domain is granted from the Solo plan upwards, one per App. On a plan that grants none, the
531
585
  deploy prints the refusal and lands everything else it was doing.
532
586
 
@@ -651,8 +705,8 @@ While it is set:
651
705
  - a refusal names which of four things happened: the credential is unknown, revoked, expired, or does not carry what
652
706
  the command needs. It does not ask you to sign in again, which a pipeline cannot do;
653
707
  - `login` and `logout` refuse, because neither would change what the next command authenticates as;
654
- - `credentials` is refused by the platform, because minting, listing and revoking are closed to
655
- credentials whatever they carry;
708
+ - `credentials` is refused by the platform, because minting, listing, regenerating, revoking and
709
+ deleting are closed to credentials whatever they carry;
656
710
  - `whoami` names the credential by its prefix, so you can match it against the one in your CI settings.
657
711
 
658
712
  ---
package/dist/index.mjs CHANGED
@@ -883,7 +883,7 @@ async function buildJs(entryInput, options) {
883
883
  * that has to name this version — `--version`, the dependency a scaffolded
884
884
  * project pins — reads it here, so a release bumps one file.
885
885
  */
886
- const CLI_VERSION = "0.12.0";
886
+ const CLI_VERSION = "0.14.0";
887
887
  //#endregion
888
888
  //#region src/prompt.ts
889
889
  /**
@@ -1630,13 +1630,11 @@ async function detachDomain(creds, app, hostname) {
1630
1630
  });
1631
1631
  if (!res.ok) throw await asGatewayError(res, `'${hostname}' could not be detached (HTTP ${res.status}).`);
1632
1632
  }
1633
- /** A missing name is refused here rather than sent, so the reason reads at the keyboard. */
1634
- async function detachDomainCommand(hostname, options = {}) {
1635
- if (!hostname || !hostname.trim()) {
1636
- console.error("[wawesome] Error: Missing domain. Usage: wawesome domains detach <hostname>");
1637
- console.error("[wawesome] The domain is typed out because detaching one takes it dark.");
1638
- process.exit(1);
1639
- }
1633
+ /**
1634
+ * Who is asking and which App they are asking about. A deploy credential in the
1635
+ * environment is a caller here, so this never insists on a session.
1636
+ */
1637
+ function credentialsAndApp(options) {
1640
1638
  const creds = readCredentials();
1641
1639
  if (!creds) {
1642
1640
  console.error("[wawesome] Error: Not logged in. Run 'wawesome login' first.");
@@ -1648,6 +1646,19 @@ async function detachDomainCommand(hostname, options = {}) {
1648
1646
  console.error("[wawesome] Run this from your project, or name the app with --app <slug>.");
1649
1647
  process.exit(1);
1650
1648
  }
1649
+ return {
1650
+ creds,
1651
+ app
1652
+ };
1653
+ }
1654
+ /** A missing name is refused here rather than sent, so the reason reads at the keyboard. */
1655
+ async function detachDomainCommand(hostname, options = {}) {
1656
+ if (!hostname || !hostname.trim()) {
1657
+ console.error("[wawesome] Error: Missing domain. Usage: wawesome domains detach <hostname>");
1658
+ console.error("[wawesome] The domain is typed out because detaching one takes it dark.");
1659
+ process.exit(1);
1660
+ }
1661
+ const { creds, app } = credentialsAndApp(options);
1651
1662
  const name = hostname.trim();
1652
1663
  console.log(`[wawesome] Detaching '${name}' from app '${app}':`);
1653
1664
  console.log("[wawesome] • the domain stops resolving here, so anything pointed at it stops");
@@ -1668,10 +1679,40 @@ async function detachDomainCommand(hostname, options = {}) {
1668
1679
  console.log("[wawesome] or the next deploy attaches the domain again.");
1669
1680
  }
1670
1681
  }
1682
+ /**
1683
+ * What this App answers at, read and printed.
1684
+ *
1685
+ * The one place a Tenant can ask. The records used to exist only in the output
1686
+ * of a deploy that changed something, so a deploy that outran the vendor left
1687
+ * nothing to run but a second deploy, which is refused for changing nothing
1688
+ * (#617). Open to a deploy credential carrying `deploy`, since a pipeline asks
1689
+ * this too.
1690
+ */
1691
+ async function showDomainCommand(options = {}) {
1692
+ const { creds, app } = credentialsAndApp(options);
1693
+ let held;
1694
+ try {
1695
+ held = await listDomains(creds, app);
1696
+ } catch (err) {
1697
+ console.error(`[wawesome] Error: ${whatWentWrong(err)}`);
1698
+ process.exit(1);
1699
+ }
1700
+ if (held.length === 0) {
1701
+ console.log(`[wawesome] No domain is attached to app '${app}'.`);
1702
+ console.log("[wawesome] Declare one in wawesome-function.json and deploy:");
1703
+ console.log(`[wawesome] \x1b[36m${DOMAIN_CONFIG_LINE}\x1b[0m`);
1704
+ console.log("[wawesome] The deploy attaches it and prints the records to add.");
1705
+ return;
1706
+ }
1707
+ console.log(`[wawesome] The domain on app '${app}':`);
1708
+ for (const domain of held) for (const line of attachedDomainLines(domain, "domains")) console.log(line);
1709
+ }
1671
1710
  async function domainsCommand(action, target, options = {}) {
1672
- if ((action || "").toLowerCase().trim() === "detach") return detachDomainCommand(target, options);
1711
+ const verb = (action || "").toLowerCase().trim();
1712
+ if (verb === "detach") return detachDomainCommand(target, options);
1713
+ if (!verb) return showDomainCommand(options);
1673
1714
  console.error(`[wawesome] Error: Unknown command '${["domains", action].filter(Boolean).join(" ")}'.`);
1674
- console.error("[wawesome] Usage: wawesome domains detach <hostname>");
1715
+ console.error("[wawesome] Usage: wawesome domains [detach <hostname>]");
1675
1716
  console.error("[wawesome] A domain is attached by declaring it in wawesome-function.json.");
1676
1717
  process.exit(1);
1677
1718
  }
@@ -1697,10 +1738,22 @@ function whereItGotTo(domain) {
1697
1738
  if (domain.certificate_record_pending) return "not verified yet, and the TXT record is not ready";
1698
1739
  return "not verified yet, so add the TXT record below";
1699
1740
  }
1700
- function whatItIsFor(record) {
1701
- return record.purpose === "certificate" ? "Proves you own the name, and issues the certificate" : "Points traffic here, once the certificate is issued";
1741
+ /**
1742
+ * What a record is for, and when to add it.
1743
+ *
1744
+ * The order is stated rather than implied. The TXT proves control and issues
1745
+ * the certificate; the CNAME is pointed here afterwards, so a name already
1746
+ * serving a live site never answers on a certificate that is not there yet.
1747
+ * That is why the platform validates before the cutover, and a reader holding
1748
+ * the CNAME alone could read the old caption as a label rather than as an
1749
+ * instruction to wait (#617).
1750
+ */
1751
+ function whatItIsFor(purpose) {
1752
+ return purpose === "certificate" ? "Add this first. It proves you own the name and issues the certificate" : "Add this once the certificate is active. It points traffic here";
1702
1753
  }
1703
1754
  const VALUE_COLUMN = " ".repeat(14);
1755
+ /** What the dashboard's empty state shows. Kept the same by hand, not by anything. */
1756
+ const DOMAIN_CONFIG_LINE = "\"domain\": \"shop.client.com\"";
1704
1757
  /**
1705
1758
  * The block a deploy prints for the domain its configuration declares, in the
1706
1759
  * columns the visibility and URL lines above it already use.
@@ -1730,7 +1783,18 @@ function domainLines(outcome) {
1730
1783
  ...outcome.held ? [`${VALUE_COLUMN}Nothing was detached. Take '${outcome.held}' off first with`, `${VALUE_COLUMN} \x1b[36mwawesome domains detach ${outcome.held}\x1b[0m`] : [],
1731
1784
  `${VALUE_COLUMN}The deploy landed, and this App's other address still answers.`
1732
1785
  ];
1733
- const { hostname, records, state, last_checked_at, detaching, certificate_record_pending } = outcome.domain;
1786
+ return attachedDomainLines(outcome.domain, "deploy");
1787
+ }
1788
+ /**
1789
+ * The block for a domain the App carries: where it got to, when the platform
1790
+ * last looked, and the records to add.
1791
+ *
1792
+ * The deploy and `wawesome domains` print this same block. One renderer rather
1793
+ * than two, because two of them drift, and a record going missing from one is
1794
+ * the whole of #610 and #617.
1795
+ */
1796
+ function attachedDomainLines(domain, printedBy = "deploy") {
1797
+ const { hostname, records, state, last_checked_at, detaching, certificate_record_pending } = domain;
1734
1798
  if (detaching) return [
1735
1799
  "",
1736
1800
  ` Domain: \x1b[33m${hostname}: being detached\x1b[0m`,
@@ -1738,20 +1802,36 @@ function domainLines(outcome) {
1738
1802
  `${VALUE_COLUMN}Remove 'domain' from wawesome-function.json, or deploy again`,
1739
1803
  `${VALUE_COLUMN}once it is gone to attach it afresh.`
1740
1804
  ];
1741
- const lines = ["", ` Domain: \x1b[36m${hostname}\x1b[0m: ${whereItGotTo(outcome.domain)}`];
1805
+ const lines = ["", ` Domain: \x1b[36m${hostname}\x1b[0m: ${whereItGotTo(domain)}`];
1806
+ if (last_checked_at) lines.push(`${VALUE_COLUMN}Last checked ${asUtc$1(last_checked_at)}.`);
1742
1807
  if (state.serving) {
1743
- lines.push(`${VALUE_COLUMN}The address above answers too, so nothing pointed at it breaks.`);
1808
+ lines.push(`${VALUE_COLUMN}This app's own address answers too, so nothing pointed at it breaks.`);
1744
1809
  return lines;
1745
1810
  }
1746
- if (last_checked_at) lines.push(`${VALUE_COLUMN}Last checked ${asUtc$1(last_checked_at)}.`);
1747
1811
  const nameWidth = Math.max(...records.map((record) => record.name.length));
1812
+ const row = (type, name, value = "") => ` ${type.padEnd(5)} ${name.padEnd(nameWidth)} ${value}`.trimEnd();
1748
1813
  lines.push("", " DNS records to add at your registrar:");
1749
- for (const record of records) lines.push("", ` ${whatItIsFor(record)}:`, ` ${record.type.padEnd(5)} ${record.name.padEnd(nameWidth)} ${record.value}`);
1750
- lines.push("");
1751
- if (certificate_record_pending) lines.push(`${VALUE_COLUMN}The TXT record that proves you own the name is not ready`, `${VALUE_COLUMN}yet. Deploy again in a minute and it prints here. The`, `${VALUE_COLUMN}deploy is finished either way.`);
1752
- else lines.push(`${VALUE_COLUMN}The deploy is finished. This domain comes up on its own`, `${VALUE_COLUMN}once the records resolve.`);
1814
+ if (certificate_record_pending) lines.push("", ` ${whatItIsFor("certificate")}:`, row("TXT", "not ready yet"));
1815
+ for (const record of records) lines.push("", ` ${whatItIsFor(record.purpose)}:`, row(record.type, record.name, record.value));
1816
+ lines.push("", ...closingLines(certificate_record_pending, printedBy));
1753
1817
  return lines;
1754
1818
  }
1819
+ /**
1820
+ * What is left to do, which is nothing.
1821
+ *
1822
+ * The pending case pointed at another deploy until #617: a deploy that changed
1823
+ * nothing is refused, so the one piece of advice the reader was given was the
1824
+ * one they could not take.
1825
+ */
1826
+ function closingLines(certificateRecordPending, printedBy) {
1827
+ if (!certificateRecordPending) return indented(`${printedBy === "deploy" ? "The deploy is finished. " : ""}This domain comes up on its own once the records resolve.`);
1828
+ const asking = "The TXT record is not ready yet. The platform asks the certificate vendor again on its own.";
1829
+ if (printedBy === "domains") return indented(`${asking} Run this again in a minute.`);
1830
+ return [...indented(`${asking} The deploy is finished either way. To read the record when it lands, run`), `${VALUE_COLUMN} \x1b[36mwawesome domains\x1b[0m`];
1831
+ }
1832
+ function indented(said) {
1833
+ return wrapped(said).map((line) => `${VALUE_COLUMN}${line}`);
1834
+ }
1755
1835
  /** The platform's own prose, kept inside the block it is printed in. */
1756
1836
  function wrapped(said) {
1757
1837
  const width = 64;
@@ -4589,6 +4669,7 @@ async function workspaceCommand(action, target, options) {
4589
4669
  //#endregion
4590
4670
  //#region src/deploy-credentials.ts
4591
4671
  const SECRET_SHOWN_ONCE = "Copy it now. This is the only time it will be shown. If you lose it, revoke this credential and mint another.";
4672
+ const REGENERATED_SECRET_SHOWN_ONCE = "Copy it now. This is the only time it will be shown. If you lose it, regenerate this credential again.";
4592
4673
  function requireCredentials$1() {
4593
4674
  const creds = readCredentials();
4594
4675
  if (!creds) {
@@ -4670,6 +4751,18 @@ async function fetchCredentials(creds) {
4670
4751
  if (!res.ok) throw await refusal(res, `Failed to read the credentials (HTTP ${res.status}).`);
4671
4752
  return (await res.json()).credentials ?? [];
4672
4753
  }
4754
+ async function regenerateCredential(creds, credentialId, payload) {
4755
+ const res = await authorizedFetch(`${creds.gateway_url}/v1/tenant/credentials/${encodeURIComponent(credentialId)}/regenerate`, {
4756
+ method: "POST",
4757
+ headers: {
4758
+ Authorization: `Bearer ${creds.tenant_jwt}`,
4759
+ "Content-Type": "application/json"
4760
+ },
4761
+ body: JSON.stringify(payload)
4762
+ });
4763
+ if (!res.ok) throw await refusal(res, `Failed to regenerate the credential (HTTP ${res.status}).`);
4764
+ return await res.json();
4765
+ }
4673
4766
  async function revokeCredential(creds, credentialId) {
4674
4767
  const res = await authorizedFetch(`${creds.gateway_url}/v1/tenant/credentials/${encodeURIComponent(credentialId)}/revoke`, {
4675
4768
  method: "POST",
@@ -4678,6 +4771,13 @@ async function revokeCredential(creds, credentialId) {
4678
4771
  if (!res.ok) throw await refusal(res, `Failed to revoke the credential (HTTP ${res.status}).`);
4679
4772
  return await res.json();
4680
4773
  }
4774
+ async function deleteCredential(creds, credentialId) {
4775
+ const res = await authorizedFetch(`${creds.gateway_url}/v1/tenant/credentials/${encodeURIComponent(credentialId)}`, {
4776
+ method: "DELETE",
4777
+ headers: { Authorization: `Bearer ${creds.tenant_jwt}` }
4778
+ });
4779
+ if (!res.ok) throw await refusal(res, `Failed to delete the credential (HTTP ${res.status}).`);
4780
+ }
4681
4781
  /**
4682
4782
  * Mint one credential.
4683
4783
  *
@@ -4705,12 +4805,22 @@ async function mintCredentialCommand(name, options) {
4705
4805
  } catch (err) {
4706
4806
  fail(errorText(err));
4707
4807
  }
4708
- console.error(`\n[wawesome] ✔ Minted deploy credential '${credential.name}'.\n`);
4808
+ reveal(`Minted deploy credential '${credential.name}'.`, credential, SECRET_SHOWN_ONCE);
4809
+ }
4810
+ /**
4811
+ * Hand the secret over, once.
4812
+ *
4813
+ * The secret goes to stdout on a line of its own and everything a person reads
4814
+ * goes to stderr, so `wawesome credentials mint ci > secret` writes the secret
4815
+ * and not one character besides.
4816
+ */
4817
+ function reveal(headline, credential, shownOnce) {
4818
+ console.error(`\n[wawesome] ✔ ${headline}\n`);
4709
4819
  console.error(` Prefix: ${credential.prefix}`);
4710
4820
  console.error(` Capabilities: ${credential.capabilities.join(", ")}`);
4711
4821
  console.error(` Apps: ${namedApps(credential.apps) ?? "every App in the workspace"}`);
4712
4822
  console.error(` Expires: ${asUtc(credential.expires_at)}`);
4713
- console.error(`\n ${SECRET_SHOWN_ONCE}\n`);
4823
+ console.error(`\n ${shownOnce}\n`);
4714
4824
  console.log(credential.secret);
4715
4825
  console.error("");
4716
4826
  }
@@ -4796,9 +4906,8 @@ function matchCredentials(credentials, target) {
4796
4906
  const wanted = typed.startsWith("wawe_") ? credentialPrefix(typed) : typed;
4797
4907
  return credentials.filter((credential) => credential.prefix.startsWith(wanted));
4798
4908
  }
4799
- async function revokeCredentialCommand(target, options) {
4800
- if (!target || !target.trim()) fail("No credential given. Usage: wawesome credentials revoke <name-or-prefix>");
4801
- const creds = requireCredentials$1();
4909
+ /** The one credential a typed name or prefix means, or an exit saying why none. */
4910
+ async function resolveCredential(creds, target, options) {
4802
4911
  if (options.verbose) console.error(`[wawesome:verbose] GET ${creds.gateway_url}/v1/tenant/credentials`);
4803
4912
  let credentials;
4804
4913
  try {
@@ -4809,7 +4918,57 @@ async function revokeCredentialCommand(target, options) {
4809
4918
  const matched = matchCredentials(credentials, target);
4810
4919
  if (matched.length === 0) fail(`No deploy credential named or prefixed '${asShown(target)}' in this workspace.`, "Run 'wawesome credentials list' to see what this workspace has.");
4811
4920
  if (matched.length > 1) fail(`'${asShown(target)}' names more than one credential:`, ...matched.map((credential) => ` ${credential.name} (${credential.prefix})`), "Name one of them, or give more of its prefix.");
4812
- const credential = matched[0];
4921
+ return matched[0];
4922
+ }
4923
+ /**
4924
+ * Replace a credential's secret where it stands, keeping the name a pipeline's
4925
+ * settings already refer to.
4926
+ *
4927
+ * Confirmed like a revoke, because it is one for whatever holds the old secret:
4928
+ * the pipeline stops on its very next request until somebody gives it the new
4929
+ * one.
4930
+ */
4931
+ async function regenerateCredentialCommand(target, options) {
4932
+ if (!target || !target.trim()) fail("No credential given. Usage: wawesome credentials regenerate <name-or-prefix>");
4933
+ const creds = requireCredentials$1();
4934
+ const credential = await resolveCredential(creds, target, options);
4935
+ if (credential.revoked_at) fail(`'${credential.name}' (${credential.prefix}) was revoked at ${asUtc(credential.revoked_at)}, and nothing un-revokes one.`, `Mint a replacement, or delete this one first to free its name for it.`);
4936
+ const payload = {};
4937
+ const expiry = parseExpiry(options.expires);
4938
+ if (expiry !== void 0) payload.expiry = expiry;
4939
+ if (!options.yes) {
4940
+ if (!isInteractive()) fail(`Regenerating asks for confirmation, and there is nobody to ask.`, `Pass --yes to regenerate '${credential.name}' (${credential.prefix}) without being asked.`);
4941
+ let confirmed;
4942
+ try {
4943
+ confirmed = await confirm({
4944
+ message: `Regenerate '${credential.name}' (${credential.prefix})? Anything holding the old secret stops working at once and has to be given the new one.`,
4945
+ default: false
4946
+ });
4947
+ } catch {
4948
+ confirmed = false;
4949
+ }
4950
+ if (!confirmed) {
4951
+ console.error(`[wawesome] Cancelled. '${credential.name}' still works.`);
4952
+ return;
4953
+ }
4954
+ }
4955
+ if (options.verbose) {
4956
+ console.error(`[wawesome:verbose] POST ${creds.gateway_url}/v1/tenant/credentials/${credential.id}/regenerate`);
4957
+ console.error(`[wawesome:verbose] Payload: ${JSON.stringify(payload)}`);
4958
+ }
4959
+ let regenerated;
4960
+ try {
4961
+ regenerated = await regenerateCredential(creds, credential.id, payload);
4962
+ } catch (err) {
4963
+ if (err instanceof GatewayError && err.status === 404) fail(`'${credential.name}' (${credential.prefix}) no longer exists in this workspace.`);
4964
+ fail(errorText(err));
4965
+ }
4966
+ reveal(`Regenerated '${regenerated.name}'. Anything still presenting the old secret is refused from its very next request.`, regenerated, REGENERATED_SECRET_SHOWN_ONCE);
4967
+ }
4968
+ async function revokeCredentialCommand(target, options) {
4969
+ if (!target || !target.trim()) fail("No credential given. Usage: wawesome credentials revoke <name-or-prefix>");
4970
+ const creds = requireCredentials$1();
4971
+ const credential = await resolveCredential(creds, target, options);
4813
4972
  if (credential.revoked_at) {
4814
4973
  console.log(`[wawesome] '${credential.name}' (${credential.prefix}) was already revoked at ${asUtc(credential.revoked_at)}. Nothing to do.`);
4815
4974
  return;
@@ -4841,11 +5000,49 @@ async function revokeCredentialCommand(target, options) {
4841
5000
  console.log(`\n[wawesome] ✔ Revoked '${revoked.name}' (${revoked.prefix}).`);
4842
5001
  console.log("[wawesome] Anything still presenting it is refused from its very next request.\n");
4843
5002
  }
5003
+ async function deleteCredentialCommand(target, options) {
5004
+ if (!target || !target.trim()) fail("No credential given. Usage: wawesome credentials delete <name-or-prefix>");
5005
+ const creds = requireCredentials$1();
5006
+ const credential = await resolveCredential(creds, target, options);
5007
+ const named = `'${credential.name}' (${credential.prefix})`;
5008
+ if (statusOf(credential, Date.now()) === "active") fail(`${named} still works, and only a credential that has stopped working is deleted.`, `Revoke it first: wawesome credentials revoke ${credential.name}`);
5009
+ if (!options.yes) {
5010
+ if (!isInteractive()) fail("Deleting asks for confirmation, and there is nobody to ask.", `Pass --yes to delete ${named} without being asked.`);
5011
+ console.log(`\n[wawesome] Deleting ${named} forgets it.`);
5012
+ console.log(`[wawesome] It leaves the listing, and the name '${credential.name}' comes free.`);
5013
+ console.log("[wawesome] Anything still presenting its secret is told the secret is unknown, rather");
5014
+ console.log("[wawesome] than that somebody here revoked it.\n");
5015
+ let confirmed;
5016
+ try {
5017
+ confirmed = await confirm({
5018
+ message: `Delete ${named}?`,
5019
+ default: false
5020
+ });
5021
+ } catch {
5022
+ confirmed = false;
5023
+ }
5024
+ if (!confirmed) {
5025
+ console.log(`[wawesome] Cancelled. ${named} is still listed.`);
5026
+ return;
5027
+ }
5028
+ }
5029
+ if (options.verbose) console.error(`[wawesome:verbose] DELETE ${creds.gateway_url}/v1/tenant/credentials/${credential.id}`);
5030
+ try {
5031
+ await deleteCredential(creds, credential.id);
5032
+ } catch (err) {
5033
+ if (err instanceof GatewayError && err.status === 404) fail(`${named} no longer exists in this workspace.`);
5034
+ fail(errorText(err));
5035
+ }
5036
+ console.log(`\n[wawesome] ✔ Deleted ${named}. The name '${credential.name}' is free again.`);
5037
+ console.log("[wawesome] Anything still presenting its secret is told the secret is unknown.\n");
5038
+ }
4844
5039
  async function credentialsCommand(action, target, options) {
4845
5040
  if (!action || action === "list" || action === "ls") return listCredentialsCommand(options);
4846
5041
  if (action === "mint") return mintCredentialCommand(target, options);
5042
+ if (action === "regenerate") return regenerateCredentialCommand(target, options);
4847
5043
  if (action === "revoke") return revokeCredentialCommand(target, options);
4848
- fail(`Unknown credentials action '${action}'.`, "Usage: wawesome credentials [list|mint <name>|revoke <name-or-prefix>]");
5044
+ if (action === "delete") return deleteCredentialCommand(target, options);
5045
+ fail(`Unknown credentials action '${action}'.`, "Usage: wawesome credentials [list|mint <name>|regenerate <name-or-prefix>|revoke <name-or-prefix>|delete <name-or-prefix>]");
4849
5046
  }
4850
5047
  //#endregion
4851
5048
  //#region src/cron.ts
@@ -5299,14 +5496,14 @@ cli.command("env [action] [key] [value]", "Manage environment variables (set, li
5299
5496
  cli.command("env set <key> <value>", "Set or overwrite an environment variable on the current app").option("-s, --secret", "Flag variable as secret (write-only)").option("-v, --verbose", "Enable verbose debug output").action((key, value, options) => setEnvVar(key, value, options));
5300
5497
  cli.command("env list", "List environment variables for the current app").alias("env ls").option("-v, --verbose", "Enable verbose debug output").action((options) => listEnvVars(options));
5301
5498
  cli.command("env rm <key>", "Delete an environment variable from the current app").alias("env remove").alias("env delete").alias("env unset").option("-v, --verbose", "Enable verbose debug output").action((key, options) => removeEnvVar(key, options));
5302
- cli.command("domains [action] [hostname]", "Manage the custom domain on the current app (detach)").usage("domains <action> [hostname]\n\nActions:\n detach <hostname> Stop serving the app at a domain you attached\n\nA domain is attached by declaring it in wawesome-function.json and deploying.").example("wawesome domains detach shop.client.com").option("-a, --app <slug>", "App to act on (default: the app in wawesome-function.json)").action((action, hostname, options) => domainsCommand(action, hostname, options));
5499
+ cli.command("domains [action] [hostname]", "Show the custom domain on the current app, its state and its DNS records").usage("domains [action] [hostname]\n\nWith no action, prints the app's domain, where it got to, when the platform last checked, and the records to add.\n\nActions:\n detach <hostname> Stop serving the app at a domain you attached\n\nA domain is attached by declaring it in wawesome-function.json and deploying.").example("wawesome domains").example("wawesome domains detach shop.client.com").option("-a, --app <slug>", "App to act on (default: the app in wawesome-function.json)").action((action, hostname, options) => domainsCommand(action, hostname, options));
5303
5500
  cli.command("domains detach <hostname>", "Stop serving the current app at a domain you attached").option("-a, --app <slug>", "App to act on (default: the app in wawesome-function.json)").action((hostname, options) => detachDomainCommand(hostname, options));
5304
5501
  cli.command("login", "Authenticate with the wawesome.io platform").option("--api <url>", "API URL (default: https://api.wawesome.io)").option("--gateway <url>", "Alias for --api <url>").option("--dashboard <url>", "Dashboard URL (default: https://dashboard.wawesome.io)").option("--provider <name>", "OAuth provider (default: github)").option("--workspace <name>", "Name for the workspace, when signing up without a terminal to prompt").option("--accept-terms", "Accept the terms of service and privacy policy, when signing up without a terminal to prompt").option("-v, --verbose", "Enable verbose debug output").action((options) => login(options));
5305
5502
  cli.command("logout", "Clear stored authentication credentials").action(() => logout());
5306
5503
  cli.command("whoami", "Show current login session info").action(() => whoami());
5307
5504
  cli.command("workspace [action] [name]", "Show the workspace, or rename its public address").usage("workspace <action> [name]\n\nActions:\n show Show the workspace name, address, and whether it can still change\n rename <name> Change the public address, while nothing live depends on it").example("wawesome workspace").example("wawesome workspace rename northwind").option("-v, --verbose", "Enable verbose debug output").action((action, name, options) => workspaceCommand(action, name, options));
5308
5505
  cli.command("templates [action]", "Browse the template catalog").usage("templates [action]\n\nActions:\n list (ls) Show every available template (default)").example("wawesome templates").example("wawesome templates list").option("--api <url>", "API URL (default: https://api.wawesome.io)").option("-v, --verbose", "Enable verbose debug output").action((action, options) => templatesCommand(action, options));
5309
- cli.command("credentials [action] [target]", "Mint, list and revoke deploy credentials").alias("credential").usage(`credentials <action> [options]
5506
+ cli.command("credentials [action] [target]", "Mint, list, regenerate, revoke and delete deploy credentials").alias("credential").usage(`credentials <action> [options]
5310
5507
 
5311
5508
  A deploy credential is what a CI pipeline or an agent authenticates with, where
5312
5509
  there is nobody at a keyboard to log in. Minting is the only time the secret is
@@ -5315,7 +5512,9 @@ cli.command("credentials [action] [target]", "Mint, list and revoke deploy crede
5315
5512
  Actions:
5316
5513
  list (ls) Show every credential: name, prefix, capabilities, Apps, last use, expiry (default)
5317
5514
  mint <name> Mint one, printing the secret exactly once
5515
+ regenerate <name-or-prefix> Replace one's secret, keeping its name, capabilities and Apps
5318
5516
  revoke <name-or-prefix> Revoke one, immediately and for good
5517
+ delete <name-or-prefix> Forget a revoked or expired one, freeing its name
5319
5518
 
5320
5519
  Examples:
5321
5520
  wawesome credentials
@@ -5324,11 +5523,16 @@ cli.command("credentials [action] [target]", "Mint, list and revoke deploy crede
5324
5523
  wawesome credentials mint prod-deploy -c deploy -a prod --expires 30
5325
5524
  wawesome credentials mint ci -c deploy,domains:attach -a prod
5326
5525
  wawesome credentials mint ci-pipeline > /tmp/secret
5526
+ wawesome credentials regenerate ci-pipeline
5527
+ wawesome credentials regenerate ci --expires never --yes
5327
5528
  wawesome credentials revoke ci-pipeline
5328
- wawesome credentials revoke wawe_ab3k9x --yes`).option("-c, --capability <capability>", "Capability to grant: deploy, logs:read, domains:attach (repeatable or comma-separated, default: deploy)").option("-a, --app <app>", "Restrict the credential to this App (repeatable or comma-separated, default: every App)").option("--expires <days|never>", "Days until the credential expires, or 'never' (default: 90)").option("-y, --yes", "Skip the revoke confirmation").option("-v, --verbose", "Enable verbose debug output").action((action, target, options) => credentialsCommand(action, target, options));
5529
+ wawesome credentials revoke wawe_ab3k9x --yes
5530
+ wawesome credentials delete ci-pipeline`).option("-c, --capability <capability>", "Capability to grant: deploy, logs:read, domains:attach (repeatable or comma-separated, default: deploy)").option("-a, --app <app>", "Restrict the credential to this App (repeatable or comma-separated, default: every App)").option("--expires <days|never>", "Days until the credential expires, or 'never' (default: 90)").option("-y, --yes", "Skip the confirmation a regenerate, revoke or delete asks for").option("-v, --verbose", "Enable verbose debug output").action((action, target, options) => credentialsCommand(action, target, options));
5329
5531
  cli.command("credentials mint <name>", "Mint a deploy credential, printing its secret exactly once").option("-c, --capability <capability>", "Capability to grant: deploy, logs:read, domains:attach (repeatable or comma-separated)").option("-a, --app <app>", "Restrict the credential to this App (repeatable or comma-separated)").option("--expires <days|never>", "Days until the credential expires, or 'never' (default: 90)").option("-v, --verbose", "Enable verbose debug output").action((name, options) => mintCredentialCommand(name, options));
5330
5532
  cli.command("credentials list", "List deploy credentials, never their secrets").alias("credentials ls").option("-v, --verbose", "Enable verbose debug output").action((options) => listCredentialsCommand(options));
5533
+ cli.command("credentials regenerate <name-or-prefix>", "Replace a deploy credential's secret, keeping its name, capabilities and Apps").option("--expires <days|never>", "Days until the regenerated credential expires, or 'never' (default: 90)").option("-y, --yes", "Regenerate without being asked to confirm").option("-v, --verbose", "Enable verbose debug output").action((target, options) => regenerateCredentialCommand(target, options));
5331
5534
  cli.command("credentials revoke <name-or-prefix>", "Revoke a deploy credential, immediately and for good").option("-y, --yes", "Revoke without being asked to confirm").option("-v, --verbose", "Enable verbose debug output").action((target, options) => revokeCredentialCommand(target, options));
5535
+ cli.command("credentials delete <name-or-prefix>", "Delete a revoked or expired deploy credential, freeing its name").option("-y, --yes", "Delete without being asked to confirm").option("-v, --verbose", "Enable verbose debug output").action((target, options) => deleteCredentialCommand(target, options));
5332
5536
  cli.command("init", "Scaffold a new function project in the current directory").usage("init [options]\n\nWith --template, the project is fetched from the template catalog, wired up\nfrom what the template declares it needs, and deployed. Run 'wawesome templates'\nto see what is available.").example("wawesome init").example("wawesome init --template stripe-webhook").option("-t, --template <name>", "Scaffold from a catalog template and deploy it").option("--api <url>", "API URL (default: https://api.wawesome.io)").option("--dashboard <url>", "Dashboard URL (default: https://dashboard.wawesome.io)").option("--no-install", "Skip installing dependencies after scaffolding").option("--root", "Generate a root function router template").option("-v, --verbose", "Enable verbose debug output").action((options) => init(options));
5333
5537
  cli.command("logs [function-name-or-invocation-id]", "View invocation history, fetch log output, or follow live").usage(`logs [target] [options]
5334
5538
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wawesome",
3
- "version": "0.12.0",
3
+ "version": "0.14.0",
4
4
  "description": "CLI tool for building and deploying serverless functions on wawesome.io platform",
5
5
  "type": "module",
6
6
  "bin": {