wawesome 0.15.0 → 0.17.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 +62 -18
- package/dist/index.mjs +229 -37
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -118,6 +118,7 @@ npx wawesome deploy
|
|
|
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
120
|
| `npx wawesome domains` | Show the app's domain, its state, and the DNS records to add |
|
|
121
|
+
| `npx wawesome domains adopt` | Write the domain the app answers at into wawesome-function.json |
|
|
121
122
|
| `npx wawesome domains detach <host>` | Stop serving the app at a domain you attached |
|
|
122
123
|
| `npx wawesome credentials` | List deploy credentials: name, prefix, capabilities, Apps, last use |
|
|
123
124
|
| `npx wawesome credentials mint <name>` | Mint a deploy credential and print its secret, once |
|
|
@@ -477,9 +478,9 @@ concluding the platform cannot do it.
|
|
|
477
478
|
|
|
478
479
|
| Tool | What it does | Needs |
|
|
479
480
|
|:---------------------------|:------------------------------------------------------------------------|:--------------|
|
|
480
|
-
| `whoami` | The workspace, where it is administered, the words this credential carries, the Apps it reaches | nothing |
|
|
481
|
+
| `whoami` | The workspace, where it is administered, the words this credential carries and which of them a tool here asks for, the Apps it reaches | nothing |
|
|
481
482
|
| `list_templates` | The template catalogue, with what each one is for | nothing |
|
|
482
|
-
| `get_template` | One template's
|
|
483
|
+
| `get_template` | One template's source, environment contract and outbound providers; its tests and build configuration are named, and read on request | nothing |
|
|
483
484
|
| `list_apps` | The Apps the credential reaches, each with its address and its dashboard page | `read:apps` |
|
|
484
485
|
| `list_functions` | The Functions in one App, and what their live version carries | `read:functions` |
|
|
485
486
|
| `list_versions` | A Function's Versions, and which one is live | `read:functions` |
|
|
@@ -492,7 +493,12 @@ concluding the platform cannot do it.
|
|
|
492
493
|
| `deploy_function` | Deploys code and the static files it carries | `write:functions` |
|
|
493
494
|
| `deploy_status` | How a deploy went, by the handle it answered with | `read:functions` |
|
|
494
495
|
| `rollback_function` | Puts a Version the Function already has back on its public address | `write:functions` |
|
|
496
|
+
| `request_upload` | Asks the person you are working with for files the conversation cannot produce, and answers a link to give them | `write:functions` |
|
|
497
|
+
| `list_uploads` | What the person sent against one upload session, ready to declare on the next deploy | `read:functions` |
|
|
495
498
|
| `set_env_var` | Sets an App's environment variable, read at the next invocation | `write:env` |
|
|
499
|
+
| `attach_domain` | Attaches a domain to an App and answers the DNS records that make it work | `write:domains` |
|
|
500
|
+
| `cancel_domain_claim` | Gives up a domain claim nobody has proved control of, freeing the App's place | `write:domains` |
|
|
501
|
+
| `check_domain` | Where the domain attached to an App got to, asked at the certificate vendor | `read:domains` |
|
|
496
502
|
| `invoke_function` | Runs a deployed Function once and answers with the Invocation id | `write:runs` |
|
|
497
503
|
| `fetch_function` | Fetches one path from a Function's live Version and answers what it served | `write:runs` |
|
|
498
504
|
|
|
@@ -501,8 +507,11 @@ beside it, and the presets below are named in exactly these words.
|
|
|
501
507
|
|
|
502
508
|
`whoami` is the one to call first. It answers the workspace, every word the credential carries and
|
|
503
509
|
the Apps it reaches, and it asks for nothing — so your agent can say what it is able to do before it
|
|
504
|
-
offers, rather than finding the edge by being refused in front of you.
|
|
505
|
-
|
|
510
|
+
offers, rather than finding the edge by being refused in front of you. Each word also says whether a
|
|
511
|
+
tool here asks for it. Your credential can carry `write:schedules` with no schedule tool on this
|
|
512
|
+
endpoint, and the mark is what stops an agent offering to schedule a job: a word with no tool behind
|
|
513
|
+
it is done on the dashboard rather than being withheld from you. A member's session calling the endpoint gets
|
|
514
|
+
the same answer for the role they hold.
|
|
506
515
|
|
|
507
516
|
A deploy is two calls. The first says which of your files the platform does not already hold, by
|
|
508
517
|
content hash; your agent uploads those bytes to `PUT /v1/assets/{content_hash}` with the same
|
|
@@ -553,7 +562,9 @@ npx wawesome credentials mint ci --preset deployer -a prod
|
|
|
553
562
|
npx wawesome credentials mint agent --preset member -a prod
|
|
554
563
|
```
|
|
555
564
|
|
|
556
|
-
Add `write:domains` where the project declares a custom domain the deploy should claim
|
|
565
|
+
Add `write:domains` where the project declares a custom domain the deploy should claim, or where an
|
|
566
|
+
agent attaches one for somebody who has no terminal — it also gives back a claim nobody has proved
|
|
567
|
+
control of yet, and never takes a live name off an App. Nothing gives
|
|
557
568
|
an agent billing, a plan change, credential management or the ability to rename your workspace: those
|
|
558
569
|
are closed to every credential, whatever it carries. Reading the workspace's name and address is not
|
|
559
570
|
among them — `whoami` answers both to any credential, which is how the agent knows where what it
|
|
@@ -675,6 +686,18 @@ Two rules to know about:
|
|
|
675
686
|
`favicon.ico`, `robots.txt` and a `.well-known/` directory are what that is for; hashed build
|
|
676
687
|
output belongs under `assets/`.
|
|
677
688
|
|
|
689
|
+
**A version carries exactly the files its deploy declared.** Nothing is inherited from the version
|
|
690
|
+
before it, so taking `"assets"` out of `wawesome-function.json` — or pointing it at a directory your
|
|
691
|
+
build no longer writes to — is not an edit but every file on the site going at once. A deploy that
|
|
692
|
+
would do that against a Function serving files today is refused, and tells you how many would go:
|
|
693
|
+
|
|
694
|
+
```bash
|
|
695
|
+
npx wawesome deploy --drop-assets
|
|
696
|
+
```
|
|
697
|
+
|
|
698
|
+
Removing *some* of the files is ordinary editing and needs nothing: only the wholesale drop is
|
|
699
|
+
refused, because that is the shape of a mistake rather than of a decision.
|
|
700
|
+
|
|
678
701
|
**A page is a file like any other.** An `index.html` your build wrote is deployed, hashed, retained
|
|
679
702
|
and billed exactly as your other files are, and it is what a directory-style address resolves to: a
|
|
680
703
|
path whose last segment carries no extension is served the `index.html` beneath it, so `/`, `/about`,
|
|
@@ -784,10 +807,16 @@ Add `"domain"` and your App answers at a name you own, beside the address it alr
|
|
|
784
807
|
}
|
|
785
808
|
```
|
|
786
809
|
|
|
787
|
-
The deploy attaches it and prints
|
|
788
|
-
own the name
|
|
789
|
-
|
|
790
|
-
|
|
810
|
+
The deploy attaches it and prints three DNS records to add at your registrar. The TXT record proves
|
|
811
|
+
you own the name. The `_acme-challenge` CNAME beside it issues the certificate and keeps renewing it
|
|
812
|
+
for as long as it stays there, so nobody comes back to your registrar at renewal time — if an
|
|
813
|
+
`_acme-challenge` TXT record is already at that name, delete it, because a TXT and this CNAME cannot
|
|
814
|
+
both sit there. The last CNAME points traffic here, and you add that one once the certificate is
|
|
815
|
+
issued, so a site that is already live never spends a minute answering on a certificate that is not
|
|
816
|
+
there yet.
|
|
817
|
+
|
|
818
|
+
Nothing you are asked to add ever changes. Copy a value into your registrar, come back an hour later,
|
|
819
|
+
and it is the same value.
|
|
791
820
|
|
|
792
821
|
Your deploy waits for neither. It attaches the domain, prints the records and finishes, and every
|
|
793
822
|
later deploy states where the domain got to: not verified, ownership verified, certificate issued, or
|
|
@@ -801,10 +830,10 @@ npx wawesome domains
|
|
|
801
830
|
|
|
802
831
|
It prints the domain, where it got to, when the platform last checked, and the records to add. It
|
|
803
832
|
changes nothing, and a deploy credential carrying `read:domains` can run it, so a pipeline reads the
|
|
804
|
-
same thing you do.
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
833
|
+
same thing you do. Where the platform does not hold a record yet it prints a row saying so rather
|
|
834
|
+
than dropping the record from the list. That row is what this command fills in, and it is where the
|
|
835
|
+
deploy sends you: deploying again would change nothing, and a deploy that changes nothing is
|
|
836
|
+
refused.
|
|
808
837
|
|
|
809
838
|
A custom domain is granted from the Solo plan upwards, one per App. On a plan that grants none, the
|
|
810
839
|
deploy prints the refusal and lands everything else it was doing.
|
|
@@ -812,13 +841,27 @@ deploy prints the refusal and lands everything else it was doing.
|
|
|
812
841
|
The address derived from your workspace slug keeps serving after you attach a domain. Both names
|
|
813
842
|
answer, so webhooks and integrations already pointed at the old one keep working.
|
|
814
843
|
|
|
815
|
-
An apex domain, `client.com` with no `www`,
|
|
816
|
-
|
|
817
|
-
|
|
844
|
+
An apex domain, `client.com` with no `www`, claims `www.client.com` with it, and attaching the `www`
|
|
845
|
+
claims the apex. Both halves are one domain against your App's one place, and both are printed with
|
|
846
|
+
their own state and their own records, because a visitor typing either one should reach your site.
|
|
847
|
+
Where your DNS provider cannot hold a CNAME at your zone root, that half is reported as not claimed
|
|
848
|
+
with the reason, and attaching the same name again picks it up once your DNS can carry it.
|
|
849
|
+
|
|
850
|
+
The file is not the only door. A domain can also be attached from the dashboard or by an agent, and
|
|
851
|
+
then your App answers at a name `wawesome-function.json` has never heard of. A deploy says so and
|
|
852
|
+
edits nothing — what was committed is what ships — so the file is written by a command of your own:
|
|
853
|
+
|
|
854
|
+
```bash
|
|
855
|
+
npx wawesome domains adopt
|
|
856
|
+
```
|
|
857
|
+
|
|
858
|
+
It writes the attached hostname into `wawesome-function.json` and changes nothing about the domain.
|
|
859
|
+
Where the file already declares a *different* name, it refuses and prints both: an App carries one
|
|
860
|
+
domain, and which of the two names it should be is yours to settle rather than a file to overwrite.
|
|
818
861
|
|
|
819
862
|
Deleting the line detaches nothing. Detaching takes a live site dark, which is not something a
|
|
820
863
|
deploy should infer from a deleted line. So the deploy reports the domain it found and left serving,
|
|
821
|
-
and names the one command that takes it off:
|
|
864
|
+
names the command that writes it into the file, and names the one command that takes it off:
|
|
822
865
|
|
|
823
866
|
```bash
|
|
824
867
|
npx wawesome domains detach shop.client.com
|
|
@@ -837,7 +880,7 @@ handler sees it, and off your response before the caller does. So **do not name
|
|
|
837
880
|
own on that prefix**. It is dropped silently rather than rejected, and you will not get an error
|
|
838
881
|
telling you why it vanished.
|
|
839
882
|
|
|
840
|
-
|
|
883
|
+
Five headers arrive or leave on it, and the stripping is what makes them worth trusting:
|
|
841
884
|
|
|
842
885
|
| Header | Direction | What it means |
|
|
843
886
|
| --- | --- | --- |
|
|
@@ -845,6 +888,7 @@ Four headers arrive or leave on it, and the stripping is what makes them worth t
|
|
|
845
888
|
| `x-wawesome-trigger` | inbound | How this run started: `caller` when someone called your address, `schedule` when fired by a Schedule. A caller cannot forge it in production (stripped inbound). On the local development surface, pass `x-wawesome-trigger: schedule` to exercise a background run by hand with the collapsed budget. |
|
|
846
889
|
| `x-wawesome-invocation-id` | outbound | The id of this run, the key to fetch its logs with `npx wawesome logs --invocation <id>`. |
|
|
847
890
|
| `x-wawesome-error` | outbound | Present only when the platform failed, never when your Function did. Its *absence* means the status on the wire is yours, up to the moment your response is committed and no further. |
|
|
891
|
+
| `x-wawesome-document` | outbound | Set it to the path of a document your own deploy carried (`index.html`) and the platform streams that file in place of the body you returned. Your status and your other headers stand; the content type and the length are the file's. Naming a path your deploy does not carry is a `500`, and your logs say which path you named. |
|
|
848
892
|
|
|
849
893
|
### Testing against the guest's JavaScript surface
|
|
850
894
|
|
package/dist/index.mjs
CHANGED
|
@@ -232,12 +232,11 @@ function deleteCredentials() {
|
|
|
232
232
|
}
|
|
233
233
|
return false;
|
|
234
234
|
}
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
235
|
+
function getFunctionConfigPath(projectDir) {
|
|
236
|
+
return path.join(projectDir || process.cwd(), "wawesome-function.json");
|
|
237
|
+
}
|
|
238
238
|
function readFunctionConfig(projectDir) {
|
|
239
|
-
const
|
|
240
|
-
const configPath = path.join(dir, "wawesome-function.json");
|
|
239
|
+
const configPath = getFunctionConfigPath(projectDir);
|
|
241
240
|
if (!fs.existsSync(configPath)) return null;
|
|
242
241
|
try {
|
|
243
242
|
const raw = fs.readFileSync(configPath, "utf-8");
|
|
@@ -906,7 +905,7 @@ async function buildHandler(entryInput, config, options) {
|
|
|
906
905
|
* that has to name this version — `--version`, the dependency a scaffolded
|
|
907
906
|
* project pins — reads it here, so a release bumps one file.
|
|
908
907
|
*/
|
|
909
|
-
const CLI_VERSION = "0.
|
|
908
|
+
const CLI_VERSION = "0.17.0";
|
|
910
909
|
//#endregion
|
|
911
910
|
//#region src/prompt.ts
|
|
912
911
|
/**
|
|
@@ -1579,7 +1578,7 @@ async function applyDeclaredDomain(creds, app, hostname) {
|
|
|
1579
1578
|
said: whatWentWrong(err)
|
|
1580
1579
|
};
|
|
1581
1580
|
}
|
|
1582
|
-
const attached = held.find((domain) =>
|
|
1581
|
+
const attached = held.find((domain) => carries(domain, hostname));
|
|
1583
1582
|
if (attached) return {
|
|
1584
1583
|
kind: "unchanged",
|
|
1585
1584
|
domain: attached
|
|
@@ -1631,6 +1630,15 @@ function whatWentWrong(err) {
|
|
|
1631
1630
|
return errorText(err);
|
|
1632
1631
|
}
|
|
1633
1632
|
/**
|
|
1633
|
+
* Whether a claim answers at the declared name — at the leg it reads as, or at
|
|
1634
|
+
* the other half it carries. A file naming the `www` of an apex-led claim
|
|
1635
|
+
* declares a name the App already answers at, and re-attaching it every deploy
|
|
1636
|
+
* would report a change nothing made.
|
|
1637
|
+
*/
|
|
1638
|
+
function carries(domain, declared) {
|
|
1639
|
+
return [domain.hostname, ...(domain.legs ?? []).filter((leg) => leg.claimed).map((leg) => leg.hostname)].some((name) => isSameHostname(name, declared));
|
|
1640
|
+
}
|
|
1641
|
+
/**
|
|
1634
1642
|
* The comparison the platform makes: a name is held lowercase without its
|
|
1635
1643
|
* trailing dot, so `shop.client.com.` in the file is the domain already
|
|
1636
1644
|
* attached rather than a second one to ask for on every deploy.
|
|
@@ -1722,6 +1730,105 @@ async function detachDomainCommand(hostname, options = {}) {
|
|
|
1722
1730
|
}
|
|
1723
1731
|
}
|
|
1724
1732
|
/**
|
|
1733
|
+
* The project's configuration as a file to write rather than as settings to
|
|
1734
|
+
* read: the raw text, so what is written back keeps the shape of what was
|
|
1735
|
+
* committed, and a file that does not parse is refused rather than replaced.
|
|
1736
|
+
*/
|
|
1737
|
+
function projectConfig() {
|
|
1738
|
+
const path = getFunctionConfigPath();
|
|
1739
|
+
if (!fs.existsSync(path)) {
|
|
1740
|
+
console.error("[wawesome] Error: No wawesome-function.json found in the current directory.");
|
|
1741
|
+
console.error("[wawesome] Run this from the project that deploys the app.");
|
|
1742
|
+
process.exit(1);
|
|
1743
|
+
}
|
|
1744
|
+
const text = fs.readFileSync(path, "utf-8");
|
|
1745
|
+
try {
|
|
1746
|
+
return {
|
|
1747
|
+
path,
|
|
1748
|
+
text,
|
|
1749
|
+
parsed: JSON.parse(text)
|
|
1750
|
+
};
|
|
1751
|
+
} catch (err) {
|
|
1752
|
+
console.error(`[wawesome] Error: wawesome-function.json is not valid JSON: ${errorText(err)}`);
|
|
1753
|
+
console.error("[wawesome] Nothing was written. Fix the file and run this again.");
|
|
1754
|
+
process.exit(1);
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
/**
|
|
1758
|
+
* The declared domain written back, in the indentation the file already uses so
|
|
1759
|
+
* the diff is the one line.
|
|
1760
|
+
*/
|
|
1761
|
+
function writeDeclaredDomain(config, hostname) {
|
|
1762
|
+
const indent = /\n([ \t]+)"/.exec(config.text)?.[1] ?? " ";
|
|
1763
|
+
const written = JSON.stringify({
|
|
1764
|
+
...config.parsed,
|
|
1765
|
+
domain: hostname
|
|
1766
|
+
}, null, indent);
|
|
1767
|
+
fs.writeFileSync(config.path, config.text.endsWith("\n") ? `${written}\n` : written, "utf-8");
|
|
1768
|
+
}
|
|
1769
|
+
/**
|
|
1770
|
+
* Write the domain the App carries into wawesome-function.json.
|
|
1771
|
+
*
|
|
1772
|
+
* The fix named beside the drift a deploy reports (ADR-0034). The platform row
|
|
1773
|
+
* is what serves and the file is a desired state, so a name attached from the
|
|
1774
|
+
* dashboard or by an agent is a name this file has never heard of — and a
|
|
1775
|
+
* deploy will not write it, because a deploy never edits a tracked file.
|
|
1776
|
+
*
|
|
1777
|
+
* A file naming a different domain is two names in play and a person's
|
|
1778
|
+
* question: both are printed and nothing is written.
|
|
1779
|
+
*/
|
|
1780
|
+
async function adoptDomainCommand(hostname, options = {}) {
|
|
1781
|
+
if (hostname?.trim()) {
|
|
1782
|
+
console.error(`[wawesome] Error: 'wawesome domains adopt' takes no hostname, and '${hostname.trim()}' was given.`);
|
|
1783
|
+
console.error("[wawesome] It writes the name the app already answers at, which it reads from the platform.");
|
|
1784
|
+
process.exit(1);
|
|
1785
|
+
}
|
|
1786
|
+
const config = projectConfig();
|
|
1787
|
+
const { creds, app } = credentialsAndApp(options);
|
|
1788
|
+
const deploysTo = typeof config.parsed.app === "string" ? config.parsed.app.trim() : void 0;
|
|
1789
|
+
if (deploysTo && deploysTo !== app) {
|
|
1790
|
+
console.error(`[wawesome] Error: wawesome-function.json deploys to app '${deploysTo}', and --app names '${app}'.`);
|
|
1791
|
+
console.error("[wawesome] Nothing was written: a name off another app does not belong in this file.");
|
|
1792
|
+
process.exit(1);
|
|
1793
|
+
}
|
|
1794
|
+
let held;
|
|
1795
|
+
try {
|
|
1796
|
+
held = await listDomains(creds, app);
|
|
1797
|
+
} catch (err) {
|
|
1798
|
+
console.error(`[wawesome] Error: ${whatWentWrong(err)}`);
|
|
1799
|
+
process.exit(1);
|
|
1800
|
+
}
|
|
1801
|
+
const attached = held.find((domain) => !domain.detaching);
|
|
1802
|
+
if (!attached) {
|
|
1803
|
+
console.log(`[wawesome] No domain is attached to app '${app}', so there is nothing to adopt.`);
|
|
1804
|
+
console.log(held.length > 0 ? "[wawesome] The one it carried is being detached." : "[wawesome] Attach one from the dashboard, from an agent, or by declaring it here and deploying.");
|
|
1805
|
+
return;
|
|
1806
|
+
}
|
|
1807
|
+
const declared = declaredDomain(config.parsed.domain);
|
|
1808
|
+
if (declared && carries(attached, declared)) {
|
|
1809
|
+
console.log(`[wawesome] wawesome-function.json already declares '${declared}'.`);
|
|
1810
|
+
console.log(`[wawesome] App '${app}' answers at it, so nothing was written.`);
|
|
1811
|
+
return;
|
|
1812
|
+
}
|
|
1813
|
+
if (declared) {
|
|
1814
|
+
console.error(`[wawesome] Error: Two domains are in play on app '${app}'.`);
|
|
1815
|
+
console.error(`[wawesome] wawesome-function.json declares '${declared}'`);
|
|
1816
|
+
console.error(`[wawesome] the app answers at '${attached.hostname}'`);
|
|
1817
|
+
console.error("[wawesome] Nothing was written. An App carries one domain, so which of the two");
|
|
1818
|
+
console.error("[wawesome] it is is yours to settle: edit the line yourself, or take the attached");
|
|
1819
|
+
console.error("[wawesome] one off with");
|
|
1820
|
+
console.error(`[wawesome] \x1b[36mwawesome domains detach ${attached.hostname}\x1b[0m`);
|
|
1821
|
+
console.error("[wawesome] and deploy to attach the declared one.");
|
|
1822
|
+
process.exit(1);
|
|
1823
|
+
}
|
|
1824
|
+
writeDeclaredDomain(config, attached.hostname);
|
|
1825
|
+
console.log(`[wawesome] wawesome-function.json now declares '${attached.hostname}'.`);
|
|
1826
|
+
const answersAt = claimedHostnames(attached);
|
|
1827
|
+
if (answersAt.length > 1) console.log(`[wawesome] The app answers at both halves of the name: ${answersAt.join(", ")}.`);
|
|
1828
|
+
console.log("[wawesome] Nothing about the domain changed: it was attached already, and the file");
|
|
1829
|
+
console.log("[wawesome] now says so. Commit it, and every deploy from here agrees.");
|
|
1830
|
+
}
|
|
1831
|
+
/**
|
|
1725
1832
|
* What this App answers at, read and printed.
|
|
1726
1833
|
*
|
|
1727
1834
|
* The one place a Tenant can ask. The records used to exist only in the output
|
|
@@ -1752,10 +1859,12 @@ async function showDomainCommand(options = {}) {
|
|
|
1752
1859
|
async function domainsCommand(action, target, options = {}) {
|
|
1753
1860
|
const verb = (action || "").toLowerCase().trim();
|
|
1754
1861
|
if (verb === "detach") return detachDomainCommand(target, options);
|
|
1862
|
+
if (verb === "adopt") return adoptDomainCommand(target, options);
|
|
1755
1863
|
if (!verb) return showDomainCommand(options);
|
|
1756
1864
|
console.error(`[wawesome] Error: Unknown command '${["domains", action].filter(Boolean).join(" ")}'.`);
|
|
1757
|
-
console.error("[wawesome] Usage: wawesome domains [detach <hostname>]");
|
|
1758
|
-
console.error("[wawesome] A domain is attached by declaring it in wawesome-function.json
|
|
1865
|
+
console.error("[wawesome] Usage: wawesome domains [adopt | detach <hostname>]");
|
|
1866
|
+
console.error("[wawesome] A domain is attached by declaring it in wawesome-function.json, from the");
|
|
1867
|
+
console.error("[wawesome] dashboard, or by an agent.");
|
|
1759
1868
|
process.exit(1);
|
|
1760
1869
|
}
|
|
1761
1870
|
function domainsUrl(creds, app) {
|
|
@@ -1764,10 +1873,9 @@ function domainsUrl(creds, app) {
|
|
|
1764
1873
|
/**
|
|
1765
1874
|
* Where a domain got to, said beside the name itself.
|
|
1766
1875
|
*
|
|
1767
|
-
* Each state names the one record that is outstanding
|
|
1768
|
-
*
|
|
1769
|
-
*
|
|
1770
|
-
* certificate that is not there yet.
|
|
1876
|
+
* Each state names the one record that is outstanding. The two that prove the
|
|
1877
|
+
* name go in first and the traffic CNAME a day later, so a site that is already
|
|
1878
|
+
* live never answers on a certificate that is not there yet.
|
|
1771
1879
|
*
|
|
1772
1880
|
* Read off the records too, never the states alone: naming a record the payload
|
|
1773
1881
|
* does not carry left a reader holding the CNAME alone (#610).
|
|
@@ -1776,22 +1884,26 @@ function whereItGotTo(domain) {
|
|
|
1776
1884
|
const { state } = domain;
|
|
1777
1885
|
if (state.serving) return "serving";
|
|
1778
1886
|
if (state.certificate_active) return "certificate issued, so point the CNAME here to cut over";
|
|
1779
|
-
if (state.ownership_verified) return "
|
|
1780
|
-
if (domain.
|
|
1781
|
-
return "not verified yet, so add the
|
|
1887
|
+
if (state.ownership_verified) return domain.certificate_record_pending ? "name verified, and the certificate record is not ready" : "name verified, so add the certificate CNAME below";
|
|
1888
|
+
if (domain.ownership_record_pending) return "not verified yet, and the TXT record is not ready";
|
|
1889
|
+
return "not verified yet, so add the records below";
|
|
1782
1890
|
}
|
|
1783
1891
|
/**
|
|
1784
1892
|
* What a record is for, and when to add it.
|
|
1785
1893
|
*
|
|
1786
|
-
* The order is stated rather than implied. The TXT proves
|
|
1787
|
-
*
|
|
1788
|
-
* serving a live site never answers on a
|
|
1789
|
-
* That is why the platform validates before
|
|
1790
|
-
* the
|
|
1791
|
-
* instruction to wait (#617).
|
|
1894
|
+
* The order is stated rather than implied. The TXT proves the name is theirs
|
|
1895
|
+
* and the CNAME beside it issues the certificate; the traffic record is pointed
|
|
1896
|
+
* here afterwards, so a name already serving a live site never answers on a
|
|
1897
|
+
* certificate that is not there yet. That is why the platform validates before
|
|
1898
|
+
* the cutover, and a reader holding the traffic record alone could read the old
|
|
1899
|
+
* caption as a label rather than as an instruction to wait (#617).
|
|
1792
1900
|
*/
|
|
1793
1901
|
function whatItIsFor(purpose) {
|
|
1794
|
-
|
|
1902
|
+
switch (purpose) {
|
|
1903
|
+
case "ownership": return "Add this first. It proves you own the name";
|
|
1904
|
+
case "certificate": return "Add this first too. It issues the certificate and keeps it renewed";
|
|
1905
|
+
default: return "Add this once the certificate is active. It points traffic here";
|
|
1906
|
+
}
|
|
1795
1907
|
}
|
|
1796
1908
|
/** What the dashboard's empty state shows. Kept the same by hand, not by anything. */
|
|
1797
1909
|
const DOMAIN_CONFIG_LINE = "\"domain\": \"shop.client.com\"";
|
|
@@ -1809,10 +1921,14 @@ function domainLines(outcome) {
|
|
|
1809
1921
|
if (outcome.kind === "undeclared") {
|
|
1810
1922
|
const { hostname, state } = outcome.domain;
|
|
1811
1923
|
const left = state.serving ? "it is still serving" : "nothing changed";
|
|
1924
|
+
const answersAt = claimedHostnames(outcome.domain);
|
|
1812
1925
|
return [
|
|
1813
1926
|
"",
|
|
1814
1927
|
` Domain: \x1b[33m${hostname}: attached, and this project declares no domain\x1b[0m`,
|
|
1815
|
-
...
|
|
1928
|
+
...answersAt.length > 1 ? indented(`Both halves of the name are attached: ${answersAt.join(", ")}.`) : [],
|
|
1929
|
+
...indented(`Nothing was detached and ${left}. A domain comes off an App when somebody names it, never because a line was deleted. To write it into wawesome-function.json, run`),
|
|
1930
|
+
`${VALUE_COLUMN} \x1b[36mwawesome domains adopt\x1b[0m`,
|
|
1931
|
+
...indented("To take it off the App instead, run"),
|
|
1816
1932
|
`${VALUE_COLUMN} \x1b[36mwawesome domains detach ${hostname}\x1b[0m`
|
|
1817
1933
|
];
|
|
1818
1934
|
}
|
|
@@ -1835,7 +1951,7 @@ function domainLines(outcome) {
|
|
|
1835
1951
|
* the whole of #610 and #617.
|
|
1836
1952
|
*/
|
|
1837
1953
|
function attachedDomainLines(domain, printedBy = "deploy") {
|
|
1838
|
-
const { hostname, records, state, last_checked_at, detaching, certificate_record_pending } = domain;
|
|
1954
|
+
const { hostname, records, state, last_checked_at, detaching, ownership_record_pending, certificate_record_pending } = domain;
|
|
1839
1955
|
if (detaching) return [
|
|
1840
1956
|
"",
|
|
1841
1957
|
` Domain: \x1b[33m${hostname}: being detached\x1b[0m`,
|
|
@@ -1843,20 +1959,80 @@ function attachedDomainLines(domain, printedBy = "deploy") {
|
|
|
1843
1959
|
`${VALUE_COLUMN}Remove 'domain' from wawesome-function.json, or deploy again`,
|
|
1844
1960
|
`${VALUE_COLUMN}once it is gone to attach it afresh.`
|
|
1845
1961
|
];
|
|
1962
|
+
const legs = domain.legs ?? [];
|
|
1963
|
+
if (legs.length > 1) return pairLines(domain, legs, printedBy);
|
|
1846
1964
|
const lines = ["", ` Domain: \x1b[36m${hostname}\x1b[0m: ${whereItGotTo(domain)}`];
|
|
1847
1965
|
if (last_checked_at) lines.push(`${VALUE_COLUMN}Last checked ${asUtc$1(last_checked_at)}.`);
|
|
1848
1966
|
if (state.serving) {
|
|
1849
1967
|
lines.push(`${VALUE_COLUMN}This app's own address answers too, so nothing pointed at it breaks.`);
|
|
1850
1968
|
return lines;
|
|
1851
1969
|
}
|
|
1852
|
-
const
|
|
1853
|
-
|
|
1970
|
+
const pending = Boolean(ownership_record_pending || certificate_record_pending);
|
|
1971
|
+
lines.push("", " DNS records to add at your registrar:");
|
|
1972
|
+
lines.push(...recordLines(records, {
|
|
1973
|
+
ownership_record_pending,
|
|
1974
|
+
certificate_record_pending
|
|
1975
|
+
}, " "));
|
|
1976
|
+
lines.push("", ...closingLines(pending, printedBy));
|
|
1977
|
+
return lines;
|
|
1978
|
+
}
|
|
1979
|
+
/**
|
|
1980
|
+
* A claim carrying both halves of a name: where the claim got to, then each leg
|
|
1981
|
+
* with its own state and its own records.
|
|
1982
|
+
*
|
|
1983
|
+
* Every leg is named, the half nothing claimed included. Four records to add
|
|
1984
|
+
* rather than two is the cost of a name that answers at both halves, and a leg
|
|
1985
|
+
* left out of this block is a record nobody adds on the name a visitor typed.
|
|
1986
|
+
*/
|
|
1987
|
+
function pairLines(domain, legs, printedBy) {
|
|
1988
|
+
const lines = ["", ` Domain: \x1b[36m${domain.hostname}\x1b[0m: ${domain.state.serving ? "serving at both halves of the name" : "coming up, and each half of the name is below"}`];
|
|
1989
|
+
if (domain.last_checked_at) lines.push(`${VALUE_COLUMN}Last checked ${asUtc$1(domain.last_checked_at)}.`);
|
|
1990
|
+
lines.push("");
|
|
1991
|
+
for (const leg of legs) {
|
|
1992
|
+
lines.push(`${VALUE_COLUMN}${leg.hostname}: ${whereTheLegGotTo(leg)}`);
|
|
1993
|
+
if (leg.unclaimed_reason) lines.push(...wrapped(leg.unclaimed_reason).map((line) => `${VALUE_COLUMN} ${line}`));
|
|
1994
|
+
}
|
|
1995
|
+
const outstanding = legs.filter((leg) => leg.claimed && !leg.state.serving);
|
|
1996
|
+
if (outstanding.length === 0) {
|
|
1997
|
+
lines.push(`${VALUE_COLUMN}This app's own address answers too, so nothing pointed at it breaks.`);
|
|
1998
|
+
return lines;
|
|
1999
|
+
}
|
|
1854
2000
|
lines.push("", " DNS records to add at your registrar:");
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
2001
|
+
for (const leg of outstanding) {
|
|
2002
|
+
lines.push("", ` ${leg.hostname}:`);
|
|
2003
|
+
lines.push(...recordLines(leg.records ?? [], leg, " "));
|
|
2004
|
+
}
|
|
2005
|
+
const pending = outstanding.some((leg) => leg.ownership_record_pending || leg.certificate_record_pending);
|
|
2006
|
+
lines.push("", ...closingLines(pending, printedBy));
|
|
1858
2007
|
return lines;
|
|
1859
2008
|
}
|
|
2009
|
+
function whereTheLegGotTo(leg) {
|
|
2010
|
+
return leg.claimed ? whereItGotTo(leg) : "not claimed";
|
|
2011
|
+
}
|
|
2012
|
+
/**
|
|
2013
|
+
* The records one leg is waiting on, each under what it is for.
|
|
2014
|
+
*
|
|
2015
|
+
* What each record is for goes above it rather than beside it. A DCV value is
|
|
2016
|
+
* 40-odd characters a person has to copy exactly, and a fourth column puts the
|
|
2017
|
+
* row past the width of the terminal it is being copied out of.
|
|
2018
|
+
*/
|
|
2019
|
+
function recordLines(records, pending, indent) {
|
|
2020
|
+
const nameWidth = Math.max(0, ...records.map((record) => record.name.length));
|
|
2021
|
+
const row = (type, name, value = "") => `${indent} ${type.padEnd(5)} ${name.padEnd(nameWidth)} ${value}`.trimEnd();
|
|
2022
|
+
const lines = [];
|
|
2023
|
+
if (pending.ownership_record_pending) lines.push("", `${indent}${whatItIsFor("ownership")}:`, row("TXT", "not ready yet"));
|
|
2024
|
+
if (pending.certificate_record_pending) lines.push("", `${indent}${whatItIsFor("certificate")}:`, row("CNAME", "not ready yet"));
|
|
2025
|
+
for (const record of records) {
|
|
2026
|
+
lines.push("", `${indent}${whatItIsFor(record.purpose)}:`, row(record.type, record.name, record.value));
|
|
2027
|
+
if (record.purpose === "certificate") lines.push(`${indent} Delete any _acme-challenge TXT record already at that name.`);
|
|
2028
|
+
}
|
|
2029
|
+
return lines;
|
|
2030
|
+
}
|
|
2031
|
+
/** Every hostname a claim answers at, for a block naming the claim in one line. */
|
|
2032
|
+
function claimedHostnames(domain) {
|
|
2033
|
+
const claimed = (domain.legs ?? []).filter((leg) => leg.claimed).map((leg) => leg.hostname);
|
|
2034
|
+
return claimed.length > 0 ? claimed : [domain.hostname];
|
|
2035
|
+
}
|
|
1860
2036
|
/**
|
|
1861
2037
|
* What is left to do, which is nothing.
|
|
1862
2038
|
*
|
|
@@ -1864,9 +2040,9 @@ function attachedDomainLines(domain, printedBy = "deploy") {
|
|
|
1864
2040
|
* nothing is refused, so the one piece of advice the reader was given was the
|
|
1865
2041
|
* one they could not take.
|
|
1866
2042
|
*/
|
|
1867
|
-
function closingLines(
|
|
1868
|
-
if (!
|
|
1869
|
-
const asking = "
|
|
2043
|
+
function closingLines(aRecordIsPending, printedBy) {
|
|
2044
|
+
if (!aRecordIsPending) return indented(`${printedBy === "deploy" ? "The deploy is finished. " : ""}This domain comes up on its own once the records resolve.`);
|
|
2045
|
+
const asking = "A record is not ready yet. The platform asks the certificate vendor again on its own.";
|
|
1870
2046
|
if (printedBy === "domains") return indented(`${asking} Run this again in a minute.`);
|
|
1871
2047
|
return [...indented(`${asking} The deploy is finished either way. To read the record when it lands, run`), `${VALUE_COLUMN} \x1b[36mwawesome domains\x1b[0m`];
|
|
1872
2048
|
}
|
|
@@ -2803,7 +2979,8 @@ function shadowingLines(shadowed) {
|
|
|
2803
2979
|
function declaredFields(declared) {
|
|
2804
2980
|
return {
|
|
2805
2981
|
...declared.visibility ? { visibility: declared.visibility } : {},
|
|
2806
|
-
...declared.confirmPublish ? { confirm_publish: true } : {}
|
|
2982
|
+
...declared.confirmPublish ? { confirm_publish: true } : {},
|
|
2983
|
+
...declared.confirmAssetDrop ? { confirm_asset_drop: true } : {}
|
|
2807
2984
|
};
|
|
2808
2985
|
}
|
|
2809
2986
|
/**
|
|
@@ -2817,6 +2994,15 @@ function declaredVisibility(value) {
|
|
|
2817
2994
|
console.error("[wawesome] It must be \"public\" or \"private\".");
|
|
2818
2995
|
process.exit(1);
|
|
2819
2996
|
}
|
|
2997
|
+
function reportAssetDropRefusal(message) {
|
|
2998
|
+
console.error(`\n[wawesome] \x1b[31mError: ${message}\x1b[0m`);
|
|
2999
|
+
console.error("[wawesome] Nothing was deployed. A version carries exactly the files its");
|
|
3000
|
+
console.error("[wawesome] deploy declared, so this one would take every file the live");
|
|
3001
|
+
console.error("[wawesome] version serves off the site at once.");
|
|
3002
|
+
console.error("[wawesome] Point \x1B[36m\"assets\"\x1B[0m in wawesome-function.json at the directory");
|
|
3003
|
+
console.error("[wawesome] holding them, or re-run with \x1B[36mwawesome deploy --drop-assets\x1B[0m to");
|
|
3004
|
+
console.error("[wawesome] remove them.\n");
|
|
3005
|
+
}
|
|
2820
3006
|
function reportPublishRefusal(message) {
|
|
2821
3007
|
console.error(`\n[wawesome] \x1b[31mError: ${message}\x1b[0m`);
|
|
2822
3008
|
console.error("[wawesome] Nothing was deployed. This Function is private, so its address");
|
|
@@ -2844,7 +3030,8 @@ async function deploy(entryInput, options) {
|
|
|
2844
3030
|
const { app, function: funcName } = config;
|
|
2845
3031
|
const declared = {
|
|
2846
3032
|
visibility: declaredVisibility(config.visibility),
|
|
2847
|
-
confirmPublish: Boolean(options.publish)
|
|
3033
|
+
confirmPublish: Boolean(options.publish),
|
|
3034
|
+
confirmAssetDrop: Boolean(options.dropAssets)
|
|
2848
3035
|
};
|
|
2849
3036
|
const domainName = declaredDomain(config.domain);
|
|
2850
3037
|
if (isVerbose) {
|
|
@@ -2935,6 +3122,10 @@ async function deploy(entryInput, options) {
|
|
|
2935
3122
|
reportPublishRefusal(refusal.message);
|
|
2936
3123
|
process.exit(1);
|
|
2937
3124
|
}
|
|
3125
|
+
if (refusal.reason === "asset-drop-needs-confirmation") {
|
|
3126
|
+
reportAssetDropRefusal(refusal.message);
|
|
3127
|
+
process.exit(1);
|
|
3128
|
+
}
|
|
2938
3129
|
console.error(`\n[wawesome] \x1b[31mError: ${refusal.message}\x1b[0m`);
|
|
2939
3130
|
const advice = planLimitAdvice(refusal.reason);
|
|
2940
3131
|
if (advice) console.error(`[wawesome] ${advice}\n`);
|
|
@@ -5755,7 +5946,7 @@ async function credentialsCommand(action, target, options) {
|
|
|
5755
5946
|
//#region src/index.ts
|
|
5756
5947
|
const cli = cac("wawesome");
|
|
5757
5948
|
cli.command("build [entry]", "Bundle a serverless function to an optimized JS file").option("-o, --out <path>", "Output JS bundle path", { default: "dist/index.js" }).option("-v, --verbose", "Enable verbose debug output").action((entry, options) => buildJs(entry, options));
|
|
5758
|
-
cli.command("deploy [entry]", "Build, upload, and promote a serverless function").option("-o, --out <path>", "Output JS bundle path", { default: "dist/index.js" }).option("-v, --verbose", "Enable verbose debug output").option("--skip-build", "Skip the build step, deploy an already-built bundle").option("--publish", "Confirm putting a private function back on its public address").action((entry, options) => deploy(entry, options));
|
|
5949
|
+
cli.command("deploy [entry]", "Build, upload, and promote a serverless function").option("-o, --out <path>", "Output JS bundle path", { default: "dist/index.js" }).option("-v, --verbose", "Enable verbose debug output").option("--skip-build", "Skip the build step, deploy an already-built bundle").option("--publish", "Confirm putting a private function back on its public address").option("--drop-assets", "Confirm a deploy carrying no static files where the live version serves some").action((entry, options) => deploy(entry, options));
|
|
5759
5950
|
cli.command("version [action] [target]", "Manage versions (e.g. 'version list', 'version switch [version]')").option("-e, --env <environment>", "Target environment (default: production)").option("-v, --verbose", "Enable verbose debug output").action((action, target, options) => {
|
|
5760
5951
|
if (!action || action === "list" || action === "ls") return listVersions(options);
|
|
5761
5952
|
if (action === "switch" || action === "use" || action === "select") return switchVersion(target, options);
|
|
@@ -5767,7 +5958,8 @@ cli.command("env [action] [key] [value]", "Manage environment variables (set, li
|
|
|
5767
5958
|
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));
|
|
5768
5959
|
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));
|
|
5769
5960
|
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));
|
|
5770
|
-
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,
|
|
5961
|
+
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, every half of it, where each got to, when the platform last checked, and the records to add.\n\nActions:\n adopt Write the attached domain into wawesome-function.json\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, from the dashboard, or by an agent.").example("wawesome domains").example("wawesome domains adopt").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));
|
|
5962
|
+
cli.command("domains adopt", "Write the domain the current app answers at into wawesome-function.json").option("-a, --app <slug>", "App to act on (default: the app in wawesome-function.json)").action((options) => adoptDomainCommand(void 0, options));
|
|
5771
5963
|
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));
|
|
5772
5964
|
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));
|
|
5773
5965
|
cli.command("logout", "Clear stored authentication credentials").action(() => logout());
|