wawesome 0.13.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 +44 -5
  2. package/dist/index.mjs +134 -10
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -121,7 +121,9 @@ npx wawesome deploy
121
121
  | `npx wawesome domains detach <host>` | Stop serving the app at a domain you attached |
122
122
  | `npx wawesome credentials` | List deploy credentials: name, prefix, capabilities, Apps, last use |
123
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 |
124
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 |
125
127
 
126
128
  ---
127
129
 
@@ -292,9 +294,9 @@ keyboard to log in. It belongs to the workspace rather than to you: it keeps wor
292
294
  and revoking it does not end your own session. Once you have one, [Deploying from CI](#deploying-from-ci)
293
295
  is what to do with it.
294
296
 
295
- Minting, listing and revoking belong to the workspace owner. They are closed to deploy credentials
296
- themselves, whatever those carry, so a leaked credential cannot mint another. All three run on your
297
- 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.
298
300
 
299
301
  ### Mint one
300
302
 
@@ -342,6 +344,27 @@ Name, displayable prefix, capabilities, Apps, last use and expiry. Never the sec
342
344
  platform no longer holds. `LAST USED` lags by a few minutes and reads `never` for a credential
343
345
  nothing has ever presented, which is a better reason to revoke one than any calendar date.
344
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
+
345
368
  ### Revoke one
346
369
 
347
370
  ```bash
@@ -354,6 +377,22 @@ where nobody is at the keyboard, and a prefix naming more than one credential is
354
377
  guessed at. A revocation lands on the very next request presenting the secret, and nothing un-revokes
355
378
  it.
356
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
+
357
396
  ---
358
397
 
359
398
  ## Configuration and a custom gateway
@@ -666,8 +705,8 @@ While it is set:
666
705
  - a refusal names which of four things happened: the credential is unknown, revoked, expired, or does not carry what
667
706
  the command needs. It does not ask you to sign in again, which a pipeline cannot do;
668
707
  - `login` and `logout` refuse, because neither would change what the next command authenticates as;
669
- - `credentials` is refused by the platform, because minting, listing and revoking are closed to
670
- 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;
671
710
  - `whoami` names the credential by its prefix, so you can match it against the one in your CI settings.
672
711
 
673
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.13.0";
886
+ const CLI_VERSION = "0.14.0";
887
887
  //#endregion
888
888
  //#region src/prompt.ts
889
889
  /**
@@ -4669,6 +4669,7 @@ async function workspaceCommand(action, target, options) {
4669
4669
  //#endregion
4670
4670
  //#region src/deploy-credentials.ts
4671
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.";
4672
4673
  function requireCredentials$1() {
4673
4674
  const creds = readCredentials();
4674
4675
  if (!creds) {
@@ -4750,6 +4751,18 @@ async function fetchCredentials(creds) {
4750
4751
  if (!res.ok) throw await refusal(res, `Failed to read the credentials (HTTP ${res.status}).`);
4751
4752
  return (await res.json()).credentials ?? [];
4752
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
+ }
4753
4766
  async function revokeCredential(creds, credentialId) {
4754
4767
  const res = await authorizedFetch(`${creds.gateway_url}/v1/tenant/credentials/${encodeURIComponent(credentialId)}/revoke`, {
4755
4768
  method: "POST",
@@ -4758,6 +4771,13 @@ async function revokeCredential(creds, credentialId) {
4758
4771
  if (!res.ok) throw await refusal(res, `Failed to revoke the credential (HTTP ${res.status}).`);
4759
4772
  return await res.json();
4760
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
+ }
4761
4781
  /**
4762
4782
  * Mint one credential.
4763
4783
  *
@@ -4785,12 +4805,22 @@ async function mintCredentialCommand(name, options) {
4785
4805
  } catch (err) {
4786
4806
  fail(errorText(err));
4787
4807
  }
4788
- 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`);
4789
4819
  console.error(` Prefix: ${credential.prefix}`);
4790
4820
  console.error(` Capabilities: ${credential.capabilities.join(", ")}`);
4791
4821
  console.error(` Apps: ${namedApps(credential.apps) ?? "every App in the workspace"}`);
4792
4822
  console.error(` Expires: ${asUtc(credential.expires_at)}`);
4793
- console.error(`\n ${SECRET_SHOWN_ONCE}\n`);
4823
+ console.error(`\n ${shownOnce}\n`);
4794
4824
  console.log(credential.secret);
4795
4825
  console.error("");
4796
4826
  }
@@ -4876,9 +4906,8 @@ function matchCredentials(credentials, target) {
4876
4906
  const wanted = typed.startsWith("wawe_") ? credentialPrefix(typed) : typed;
4877
4907
  return credentials.filter((credential) => credential.prefix.startsWith(wanted));
4878
4908
  }
4879
- async function revokeCredentialCommand(target, options) {
4880
- if (!target || !target.trim()) fail("No credential given. Usage: wawesome credentials revoke <name-or-prefix>");
4881
- 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) {
4882
4911
  if (options.verbose) console.error(`[wawesome:verbose] GET ${creds.gateway_url}/v1/tenant/credentials`);
4883
4912
  let credentials;
4884
4913
  try {
@@ -4889,7 +4918,57 @@ async function revokeCredentialCommand(target, options) {
4889
4918
  const matched = matchCredentials(credentials, target);
4890
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.");
4891
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.");
4892
- 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);
4893
4972
  if (credential.revoked_at) {
4894
4973
  console.log(`[wawesome] '${credential.name}' (${credential.prefix}) was already revoked at ${asUtc(credential.revoked_at)}. Nothing to do.`);
4895
4974
  return;
@@ -4921,11 +5000,49 @@ async function revokeCredentialCommand(target, options) {
4921
5000
  console.log(`\n[wawesome] ✔ Revoked '${revoked.name}' (${revoked.prefix}).`);
4922
5001
  console.log("[wawesome] Anything still presenting it is refused from its very next request.\n");
4923
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
+ }
4924
5039
  async function credentialsCommand(action, target, options) {
4925
5040
  if (!action || action === "list" || action === "ls") return listCredentialsCommand(options);
4926
5041
  if (action === "mint") return mintCredentialCommand(target, options);
5042
+ if (action === "regenerate") return regenerateCredentialCommand(target, options);
4927
5043
  if (action === "revoke") return revokeCredentialCommand(target, options);
4928
- 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>]");
4929
5046
  }
4930
5047
  //#endregion
4931
5048
  //#region src/cron.ts
@@ -5386,7 +5503,7 @@ cli.command("logout", "Clear stored authentication credentials").action(() => lo
5386
5503
  cli.command("whoami", "Show current login session info").action(() => whoami());
5387
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));
5388
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));
5389
- 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]
5390
5507
 
5391
5508
  A deploy credential is what a CI pipeline or an agent authenticates with, where
5392
5509
  there is nobody at a keyboard to log in. Minting is the only time the secret is
@@ -5395,7 +5512,9 @@ cli.command("credentials [action] [target]", "Mint, list and revoke deploy crede
5395
5512
  Actions:
5396
5513
  list (ls) Show every credential: name, prefix, capabilities, Apps, last use, expiry (default)
5397
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
5398
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
5399
5518
 
5400
5519
  Examples:
5401
5520
  wawesome credentials
@@ -5404,11 +5523,16 @@ cli.command("credentials [action] [target]", "Mint, list and revoke deploy crede
5404
5523
  wawesome credentials mint prod-deploy -c deploy -a prod --expires 30
5405
5524
  wawesome credentials mint ci -c deploy,domains:attach -a prod
5406
5525
  wawesome credentials mint ci-pipeline > /tmp/secret
5526
+ wawesome credentials regenerate ci-pipeline
5527
+ wawesome credentials regenerate ci --expires never --yes
5407
5528
  wawesome credentials revoke ci-pipeline
5408
- 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));
5409
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));
5410
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));
5411
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));
5412
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));
5413
5537
  cli.command("logs [function-name-or-invocation-id]", "View invocation history, fetch log output, or follow live").usage(`logs [target] [options]
5414
5538
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wawesome",
3
- "version": "0.13.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": {