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.
- package/README.md +203 -8
- package/dist/index.mjs +136 -12
- 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
|
|
296
|
-
themselves, whatever those carry, so a leaked credential cannot mint another
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
|
|
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 ${
|
|
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
|
-
|
|
4880
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
5409
|
-
|
|
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
|
|