wawesome 0.13.0 → 0.14.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.
Files changed (3) hide show
  1. package/README.md +203 -8
  2. package/dist/index.mjs +136 -12
  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
 
@@ -314,8 +316,8 @@ A credential carries `deploy`, reaches every App in the workspace, and expires i
314
316
  you say otherwise:
315
317
 
316
318
  ```bash
317
- # reads logs and cannot deploy, restricted to one App, never expires
318
- npx wawesome credentials mint agent -c logs:read -a prod --expires never
319
+ # reads and runs what somebody else shipped, restricted to one App, never expires
320
+ npx wawesome credentials mint agent -c read,logs:read,invoke -a prod --expires never
319
321
 
320
322
  # ships code and attaches the domain the project declares
321
323
  npx wawesome credentials mint ci -c deploy,domains:attach -a prod
@@ -323,7 +325,7 @@ npx wawesome credentials mint ci -c deploy,domains:attach -a prod
323
325
 
324
326
  | Flag | Meaning |
325
327
  |:---------------------|:------------------------------------------------------------------------------|
326
- | `-c, --capability` | `deploy`, `logs:read`, `domains:attach`, or any mix. Repeatable or comma-separated. Default `deploy` |
328
+ | `-c, --capability` | `deploy`, `read`, `logs:read`, `invoke`, `domains:attach`, or any mix. Repeatable or comma-separated. Default `deploy` |
327
329
  | `-a, --app` | Restrict to these Apps, by slug. Repeatable. Default: every App in the workspace |
328
330
  | `--expires` | Days, or `never`. Default: 90 |
329
331
 
@@ -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,178 @@ 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
+
396
+ ---
397
+
398
+ ## Your agent, over MCP
399
+
400
+ The same deploy credential connects a coding agent to your workspace, over MCP. Nothing to install and
401
+ no local process to keep running: it is an endpoint on the gateway, `https://api.wawesome.io/v1/mcp`,
402
+ and the credential is the whole of the authentication.
403
+
404
+ ### Install it
405
+
406
+ In Claude Code, one command:
407
+
408
+ ```bash
409
+ claude mcp add --transport http wawesome https://api.wawesome.io/v1/mcp \
410
+ --header "Authorization: Bearer wawe_..."
411
+ ```
412
+
413
+ In Cursor, `.cursor/mcp.json` in the project, or `~/.cursor/mcp.json` for every project:
414
+
415
+ ```json
416
+ {
417
+ "mcpServers": {
418
+ "wawesome": {
419
+ "url": "https://api.wawesome.io/v1/mcp",
420
+ "headers": { "Authorization": "Bearer wawe_..." }
421
+ }
422
+ }
423
+ }
424
+ ```
425
+
426
+ In VS Code, `.vscode/mcp.json` in the workspace, or the file **MCP: Open User Configuration** opens.
427
+ The shape is the same address and header map, under the key `servers` rather than `mcpServers`:
428
+
429
+ ```json
430
+ {
431
+ "servers": {
432
+ "wawesome": {
433
+ "type": "http",
434
+ "url": "https://api.wawesome.io/v1/mcp",
435
+ "headers": { "Authorization": "Bearer wawe_..." }
436
+ }
437
+ }
438
+ }
439
+ ```
440
+
441
+ A file inside the project is a file your repository carries, and the secret with it. VS Code takes an
442
+ `inputs` entry and a `${input:...}` placeholder in the header, so it asks once and holds the value
443
+ itself. In Cursor, put the credential in `~/.cursor/mcp.json`, which is outside every repository.
444
+
445
+ ### Where a bearer credential reaches
446
+
447
+ It works in Claude Code, the Claude Agent SDK, Cursor, VS Code, Windsurf, Zed and the Gemini CLI. It
448
+ works in every agent framework that takes a URL and a header map, among them the OpenAI Agents SDK,
449
+ LangChain, the Vercel AI SDK and Pydantic AI. Anything that speaks streamable HTTP MCP and lets you
450
+ set a header can connect.
451
+
452
+ Whichever protocol revision your client speaks, the endpoint answers it. It holds both eras at one
453
+ address: the current revision, `2026-07-28`, and the three handshake-based ones the installed base is
454
+ still on. A client ahead of your editor's next release connects, and so does one behind it.
455
+
456
+ It does **not** work in the connector dialog in claude.ai or Claude Desktop, or in ChatGPT
457
+ connectors. Those three accept "no authentication" or OAuth and have no field for a token, so there
458
+ is nowhere to put `wawe_...`. Reaching them needs an OAuth authorization server, which the platform
459
+ does not run today, and nothing you can configure works around that.
460
+
461
+ ### What your agent gets
462
+
463
+ Every tool is listed for every credential, and each one names the capability it needs. A credential
464
+ that does not carry that word is refused by name, so your agent tells you what to mint rather than
465
+ concluding the platform cannot do it.
466
+
467
+ | Tool | What it does | Needs |
468
+ |:---------------------------|:------------------------------------------------------------------------|:--------------|
469
+ | `list_templates` | The template catalogue, with what each one is for | nothing |
470
+ | `get_template` | One template's files, environment contract and outbound providers | nothing |
471
+ | `list_apps` | The Apps in the workspace, with the slug each is addressed by | `deploy` |
472
+ | `list_functions` | The Functions in one App, and what their live version carries | `read` |
473
+ | `list_versions` | A Function's Versions, and which one is live | `read` |
474
+ | `get_usage` | The Tenant's usage and headroom | `read` |
475
+ | `list_invocations` | Recent Invocations of a Function, failures included | `logs:read` |
476
+ | `get_invocation` | One Invocation by id | `logs:read` |
477
+ | `read_invocation_logs` | An Invocation's log body, a page at a time | `logs:read` |
478
+ | `diagnose_latest_failure` | The latest failure, its logs and the Version that ran it, in one call | `logs:read`, `read` |
479
+ | `deploy_function` | Deploys code and the static files it carries | `deploy` |
480
+ | `deploy_status` | How a deploy went, by the handle it answered with | `deploy` |
481
+ | `rollback_function` | Puts a Version the Function already has back on its public address | `deploy` |
482
+ | `set_env_var` | Sets an App's environment variable, read at the next invocation | `deploy` |
483
+ | `invoke_function` | Runs a deployed Function once and answers with the Invocation id | `invoke` |
484
+
485
+ A deploy is two calls. The first says which of your files the platform does not already hold, by
486
+ content hash; your agent uploads those bytes to `PUT /v1/assets/{content_hash}` with the same
487
+ credential and asks again, so a redeploy that changed only its code uploads nothing. The second
488
+ answers with a handle rather than waiting for the build, because a dropped connection would otherwise
489
+ cancel a deploy that was going fine. `deploy_status` reads the handle whenever your agent asks.
490
+
491
+ When the deploy is out and something is wrong, `diagnose_latest_failure` answers the latest failed
492
+ run of a Function, that run's log output, and the version that ran it, in one call. Mirroring the
493
+ routes there costs four round trips and four chances to carry the wrong identifier between them.
494
+
495
+ Log output comes back a page at a time, with a cursor for the next one, and nothing is held open.
496
+ Your own `wawesome logs --follow` keeps running while your agent reads. One live tail per credential
497
+ is all the platform permits, and an agent taking it would end yours.
498
+
499
+ ### Which credential for which job
500
+
501
+ A credential carries the words its job needs and stops there. They are the same words
502
+ [Deploy credentials](#mint-one) mints with.
503
+
504
+ **Read and investigate, without the ability to ship.** The agent reads Functions, Versions,
505
+ Invocations and log bodies, diagnoses a failure, checks usage, and browses templates. Every tool that
506
+ ships code or runs it refuses:
507
+
508
+ ```bash
509
+ npx wawesome credentials mint investigator -c read,logs:read -a prod
510
+ ```
511
+
512
+ One thing to know before you hand it over: `list_apps` sits behind `deploy`, the same word the HTTP
513
+ route behind it asks for, so an investigation credential cannot enumerate your Apps. Name the App in
514
+ the task and every other read works.
515
+
516
+ **Read, investigate and run.** The same, plus `invoke_function`, so the agent can exercise a Function
517
+ and read what it did. It still cannot deploy, roll back, or set a variable:
518
+
519
+ ```bash
520
+ npx wawesome credentials mint runner -c read,logs:read,invoke -a prod
521
+ ```
522
+
523
+ **The whole loop.** Deploy, run, diagnose, roll back, set a key. Every tool in the table:
524
+
525
+ ```bash
526
+ npx wawesome credentials mint agent -c deploy,read,logs:read,invoke -a prod
527
+ ```
528
+
529
+ Add `domains:attach` where the project declares a custom domain the deploy should claim. Nothing
530
+ gives an agent billing, plan changes, credential management or your workspace's slug: those are
531
+ closed to every credential, whatever it carries.
532
+
533
+ Keep `-a` on all three. A credential restricted to one App is refused on every tool that names
534
+ another, which is what stops an agent working on one client's project from touching another's.
535
+ `get_invocation` and `read_invocation_logs` take an Invocation id rather than an App, and neither is
536
+ held to the restriction yet, so an agent already holding an id can read that Invocation's telemetry
537
+ and log body whichever App it belongs to.
538
+
539
+ ### When a tool refuses
540
+
541
+ A refusal is the tool's answer, not a protocol error, and it carries the same `reason` code the CLI
542
+ prints:
543
+
544
+ - `credential-missing-capability`. The credential does not carry a word this tool needs. The refusal
545
+ names it in `missing_capability`, which is the word to mint a credential with.
546
+ - `app-outside-credential-scope`. The credential is restricted to named Apps and this is not one of
547
+ them. A wider capability changes nothing here.
548
+
549
+ A quota refusal carries its numbers under `allowance` beside the prose, so your agent reports
550
+ "3 of 3 App slots" rather than handing you a sentence to read.
551
+
357
552
  ---
358
553
 
359
554
  ## Configuration and a custom gateway
@@ -666,8 +861,8 @@ While it is set:
666
861
  - a refusal names which of four things happened: the credential is unknown, revoked, expired, or does not carry what
667
862
  the command needs. It does not ask you to sign in again, which a pipeline cannot do;
668
863
  - `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;
864
+ - `credentials` is refused by the platform, because minting, listing, regenerating, revoking and
865
+ deleting are closed to credentials whatever they carry;
671
866
  - `whoami` names the credential by its prefix, so you can match it against the one in your CI settings.
672
867
 
673
868
  ---
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.1";
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,20 +5512,27 @@ 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
5402
5521
  wawesome credentials mint ci-pipeline
5403
- wawesome credentials mint agent -c logs:read --expires never
5522
+ wawesome credentials mint agent -c read,logs:read,invoke --expires never
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));
5409
- 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));
5529
+ wawesome credentials revoke wawe_ab3k9x --yes
5530
+ wawesome credentials delete ci-pipeline`).option("-c, --capability <capability>", "Capability to grant: deploy, read, logs:read, invoke, 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));
5531
+ cli.command("credentials mint <name>", "Mint a deploy credential, printing its secret exactly once").option("-c, --capability <capability>", "Capability to grant: deploy, read, logs:read, invoke, 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.1",
4
4
  "description": "CLI tool for building and deploying serverless functions on wawesome.io platform",
5
5
  "type": "module",
6
6
  "bin": {