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