wawesome 0.9.0 → 0.11.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 +154 -107
- package/dist/index.mjs +321 -61
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
|
-
#
|
|
1
|
+
# wawesome
|
|
2
2
|
|
|
3
|
-
> Official CLI for
|
|
4
|
-
>
|
|
3
|
+
> Official CLI for bundling and deploying serverless WebAssembly functions on the
|
|
4
|
+
> [wawesome.io](https://wawesome.io) platform.
|
|
5
5
|
|
|
6
6
|
[](https://www.npmjs.com/package/wawesome)
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
Write a TypeScript function, deploy it from your terminal, and get back the address it answers on.
|
|
9
9
|
|
|
10
10
|
---
|
|
11
11
|
|
|
12
|
-
##
|
|
12
|
+
## Quick start
|
|
13
13
|
|
|
14
14
|
### 1. Create an account
|
|
15
15
|
|
|
16
16
|
Sign up for free at **[https://wawesome.io](https://wawesome.io)** to set up your workspace.
|
|
17
17
|
|
|
18
|
-
### 2. Authenticate CLI
|
|
18
|
+
### 2. Authenticate the CLI
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
Log in through your browser:
|
|
21
21
|
|
|
22
22
|
```bash
|
|
23
23
|
npx wawesome login
|
|
@@ -32,14 +32,14 @@ mkdir my-wasm-app && cd my-wasm-app
|
|
|
32
32
|
npx wawesome init
|
|
33
33
|
```
|
|
34
34
|
|
|
35
|
-
`init` asks for the function name and for the **App slug
|
|
35
|
+
`init` asks for the function name and for the **App slug**. An App groups the Functions of one
|
|
36
36
|
project, and its slug is part of the public URL your client sees. Both default to the directory
|
|
37
37
|
name, so naming is usually a matter of pressing enter.
|
|
38
38
|
|
|
39
39
|
An App slug must be a legal hostname label: lowercase letters, numbers, and single hyphens between
|
|
40
|
-
them (no leading or trailing hyphen, 63 characters at most). Type something else
|
|
41
|
-
and the CLI shows you the slug it would become (`my-client`) and asks again, rather
|
|
42
|
-
your answer behind your back.
|
|
40
|
+
them (no leading or trailing hyphen, 63 characters at most). Type something else, such as
|
|
41
|
+
`My Client`, and the CLI shows you the slug it would become (`my-client`) and asks again, rather
|
|
42
|
+
than rewriting your answer behind your back.
|
|
43
43
|
|
|
44
44
|
#### Starting from a template
|
|
45
45
|
|
|
@@ -51,39 +51,40 @@ npx wawesome init --template stripe-webhook
|
|
|
51
51
|
```
|
|
52
52
|
|
|
53
53
|
This goes from nothing to a deployed Function in one command. The template is downloaded over
|
|
54
|
-
plain HTTP and unpacked
|
|
55
|
-
they are published. Only the `name` in `package.json`, the App and
|
|
56
|
-
`wawesome-function.json`, and the `wawesome` dependency
|
|
57
|
-
get depends on the CLI that scaffolded it rather than the one the template was released
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
54
|
+
plain HTTP and unpacked, so **git is not required** on any platform, and its files land exactly as
|
|
55
|
+
they are published. Only three things are touched: the `name` in `package.json`, the App and
|
|
56
|
+
Function names in `wawesome-function.json`, and the `wawesome` dependency. That last one is so the
|
|
57
|
+
project you get depends on the CLI that scaffolded it rather than the one the template was released
|
|
58
|
+
against.
|
|
59
|
+
|
|
60
|
+
Templates declare what they need rather than shipping placeholders. The CLI asks for each
|
|
61
|
+
environment variable the template requires, showing where in the provider's own dashboard to find
|
|
62
|
+
the value. It stores them (secrets write-only), enables any outbound providers the template calls,
|
|
62
63
|
and deploys. If a value does not exist yet, leave it blank; the CLI tells you the `env set` command
|
|
63
64
|
to run once it does.
|
|
64
65
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
template's imports. Pass `--no-install` to do it yourself. A failed install never stops the flow
|
|
68
|
-
the command to re-run
|
|
66
|
+
The CLI installs your dependencies with whatever package manager launched it (`npx`, `pnpm dlx`,
|
|
67
|
+
`bun x`), so the types are there when you open the project and the bundler can resolve the
|
|
68
|
+
template's imports. Pass `--no-install` to do it yourself. A failed install never stops the flow,
|
|
69
|
+
and the CLI prints the command to re-run.
|
|
69
70
|
|
|
70
71
|
Before any of that is sent anywhere, the CLI shows you the **public URL your Function will have**
|
|
71
|
-
and offers to change your workspace address
|
|
72
|
-
project's, and the same one `wawesome workspace show` prints.
|
|
73
|
-
stays changeable until your first deploy, at which point it locks for good, because
|
|
74
|
-
it. Press enter to keep it. If it is already locked, the
|
|
75
|
-
|
|
72
|
+
and offers to change your workspace address. That is the part of the URL which is yours rather
|
|
73
|
+
than this project's, and it is the same one `wawesome workspace show` prints. Signup mints it at
|
|
74
|
+
random and it stays changeable until your first deploy, at which point it locks for good, because
|
|
75
|
+
live URLs carry it. Press enter to keep it. If it is already locked, the CLI does not make the
|
|
76
|
+
offer and says why.
|
|
76
77
|
|
|
77
|
-
|
|
78
|
-
offers you a login there and then, and declining still leaves you the project plus
|
|
79
|
-
that finish it.
|
|
78
|
+
The CLI checks you are logged in **before** the first question rather than at the deploy. An
|
|
79
|
+
expired session offers you a login there and then, and declining still leaves you the project plus
|
|
80
|
+
the two commands that finish it.
|
|
80
81
|
|
|
81
|
-
Run it in an empty directory
|
|
82
|
-
|
|
82
|
+
Run it in an empty directory. If any file would be overwritten, nothing is written at all and the
|
|
83
|
+
CLI names the collision.
|
|
83
84
|
|
|
84
|
-
### 4. Build
|
|
85
|
+
### 4. Build and deploy
|
|
85
86
|
|
|
86
|
-
Deploy your serverless function to
|
|
87
|
+
Deploy your serverless function to the platform:
|
|
87
88
|
|
|
88
89
|
```bash
|
|
89
90
|
npx wawesome deploy
|
|
@@ -91,7 +92,7 @@ npx wawesome deploy
|
|
|
91
92
|
|
|
92
93
|
---
|
|
93
94
|
|
|
94
|
-
##
|
|
95
|
+
## Command reference
|
|
95
96
|
|
|
96
97
|
| Command | Description |
|
|
97
98
|
|:----------------------------------------|:--------------------------------------------------------------------|
|
|
@@ -116,37 +117,37 @@ npx wawesome deploy
|
|
|
116
117
|
| `npx wawesome env list` | View environment variables for the current app |
|
|
117
118
|
| `npx wawesome env set <key> <val>` | Set an environment variable (add `--secret` for write-only) |
|
|
118
119
|
| `npx wawesome env rm <key>` | Delete an environment variable |
|
|
119
|
-
| `npx wawesome credentials` | List deploy credentials
|
|
120
|
+
| `npx wawesome credentials` | List deploy credentials: name, prefix, capabilities, Apps, last use |
|
|
120
121
|
| `npx wawesome credentials mint <name>` | Mint a deploy credential and print its secret, once |
|
|
121
122
|
| `npx wawesome credentials revoke <name>`| Revoke a deploy credential by name or prefix |
|
|
122
123
|
|
|
123
124
|
---
|
|
124
125
|
|
|
125
|
-
##
|
|
126
|
+
## Unsupported globals
|
|
126
127
|
|
|
127
128
|
Every build scans the bundle it just produced against the platform's declared guest surface, and says
|
|
128
129
|
nothing unless it finds something.
|
|
129
130
|
|
|
130
|
-
- **`Intl` is not provided.** Where the bundle reaches it as it loads
|
|
131
|
-
dependency's
|
|
132
|
-
evaluate on the platform. Where the reference sits inside a function that may never be called,
|
|
131
|
+
- **`Intl` is not provided.** Where the bundle reaches it as it loads, in your own module scope or
|
|
132
|
+
a dependency's, the build is refused before a deploy uploads anything, because that bundle would
|
|
133
|
+
not evaluate on the platform. Where the reference sits inside a function that may never be called,
|
|
133
134
|
behind a `typeof` check, or inside a `try`, you get a warning and the deploy proceeds.
|
|
134
135
|
- **`toLocaleString`, `toLocaleDateString`, `toLocaleTimeString`, `toLocaleLowerCase` and
|
|
135
136
|
`toLocaleUpperCase` ignore their locale argument.** They run and return an unlocalised answer, so
|
|
136
137
|
these warn.
|
|
137
138
|
|
|
138
139
|
Each message names the global, the file and line in *your* source, and what to do about it. Declaring
|
|
139
|
-
an `Intl` polyfill in your `package.json
|
|
140
|
-
reads](#testing-against-the-guests-javascript-surface)
|
|
140
|
+
an `Intl` polyfill in your `package.json`, the same declaration [local parity
|
|
141
|
+
reads](#testing-against-the-guests-javascript-surface), silences the report, as does installing one
|
|
141
142
|
on `globalThis` in the bundle itself. The scan reads static references only: a global reached through
|
|
142
143
|
`globalThis['Intl']` is invisible to it, so it never refuses a deploy on a guess. `deploy
|
|
143
144
|
--skip-build` scans the bundle it found on disk before uploading it.
|
|
144
145
|
|
|
145
|
-
##
|
|
146
|
+
## Invocation logs
|
|
146
147
|
|
|
147
148
|
Inspect past function runs or view raw `stdout` / `stderr` log outputs directly in your terminal.
|
|
148
149
|
|
|
149
|
-
### 1. List
|
|
150
|
+
### 1. List recent invocations
|
|
150
151
|
|
|
151
152
|
List past executions (including status, trigger type, timestamp, and duration) for the function in the
|
|
152
153
|
current directory:
|
|
@@ -161,7 +162,7 @@ Or list invocations for a specific function by name:
|
|
|
161
162
|
npx wawesome logs my-function
|
|
162
163
|
```
|
|
163
164
|
|
|
164
|
-
Filter by invocation status
|
|
165
|
+
Filter by invocation status, to show only errors or timeouts:
|
|
165
166
|
|
|
166
167
|
```bash
|
|
167
168
|
npx wawesome logs my-function --error
|
|
@@ -169,7 +170,7 @@ npx wawesome logs my-function --status timeout
|
|
|
169
170
|
npx wawesome logs my-function --running
|
|
170
171
|
```
|
|
171
172
|
|
|
172
|
-
### 2. View
|
|
173
|
+
### 2. View the invocation log body (`stdout`/`stderr`)
|
|
173
174
|
|
|
174
175
|
Fetch and print the captured `console.log` / `console.error` text for a specific invocation:
|
|
175
176
|
|
|
@@ -183,9 +184,9 @@ Or pass the UUID directly as the target:
|
|
|
183
184
|
npx wawesome logs 019fb344-ea0c-78f2-8a9b-d04e188b9823
|
|
184
185
|
```
|
|
185
186
|
|
|
186
|
-
### 3. Follow
|
|
187
|
+
### 3. Follow live output (`--follow`)
|
|
187
188
|
|
|
188
|
-
Stream an invocation's output as it runs
|
|
189
|
+
Stream an invocation's output as it runs, like `tail -f` for your serverless function.
|
|
189
190
|
|
|
190
191
|
#### Follow by function name (recommended)
|
|
191
192
|
|
|
@@ -205,13 +206,13 @@ npx wawesome logs 019fb344-ea0c-78f2-8a9b-d04e188b9823 --follow
|
|
|
205
206
|
|
|
206
207
|
#### Reconnection
|
|
207
208
|
|
|
208
|
-
On transient network
|
|
209
|
-
|
|
210
|
-
|
|
209
|
+
On a transient network error or a 5xx, the CLI reconnects with exponential back-off, up to three
|
|
210
|
+
retries. It does not retry an authentication failure (401) or an unknown invocation (404). Those
|
|
211
|
+
exit at once, naming what happened.
|
|
211
212
|
|
|
212
213
|
---
|
|
213
214
|
|
|
214
|
-
##
|
|
215
|
+
## Manual invocation
|
|
215
216
|
|
|
216
217
|
Fire a background run of a Function immediately without waiting for a schedule tick or deploying code:
|
|
217
218
|
|
|
@@ -231,11 +232,11 @@ npx wawesome invoke --no-follow
|
|
|
231
232
|
|
|
232
233
|
---
|
|
233
234
|
|
|
234
|
-
##
|
|
235
|
+
## Schedules and cron management
|
|
235
236
|
|
|
236
237
|
Manage recurring Schedules and inspect background run history directly from your terminal.
|
|
237
238
|
|
|
238
|
-
### 1. List
|
|
239
|
+
### 1. List schedules
|
|
239
240
|
|
|
240
241
|
List a Function's Schedules with expression, state, and next run in UTC:
|
|
241
242
|
|
|
@@ -250,12 +251,12 @@ npx wawesome cron list my-function
|
|
|
250
251
|
npx wawesome cron list --app my-app
|
|
251
252
|
```
|
|
252
253
|
|
|
253
|
-
The output
|
|
254
|
+
The output tells the three off-states apart:
|
|
254
255
|
- `paused`: stopped by a user, resumable with `wawesome cron resume <name>`
|
|
255
256
|
- `not in this config file`: disabled because it was removed from configuration, resumable only by declaring it again in code
|
|
256
257
|
- `suspended`: suspended by the non-payment ladder, resumable only after settling workspace balance
|
|
257
258
|
|
|
258
|
-
### 2. Pause and
|
|
259
|
+
### 2. Pause and resume schedules
|
|
259
260
|
|
|
260
261
|
```bash
|
|
261
262
|
# Pause a schedule by name (stops queued ticks and survives future deploys)
|
|
@@ -268,7 +269,7 @@ npx wawesome cron pause nightly-reconcile --reason "database maintenance"
|
|
|
268
269
|
npx wawesome cron resume nightly-reconcile
|
|
269
270
|
```
|
|
270
271
|
|
|
271
|
-
### 3. Read
|
|
272
|
+
### 3. Read run history
|
|
272
273
|
|
|
273
274
|
Inspect past scheduled and manual runs, showing when each run was due, when it started, pool delay, and how it ended:
|
|
274
275
|
|
|
@@ -282,15 +283,15 @@ npx wawesome cron history my-function --state failed
|
|
|
282
283
|
|
|
283
284
|
---
|
|
284
285
|
|
|
285
|
-
##
|
|
286
|
+
## Deploy credentials
|
|
286
287
|
|
|
287
288
|
A deploy credential is what a CI pipeline or an agent authenticates with, where there is nobody at a
|
|
288
289
|
keyboard to log in. It belongs to the workspace rather than to you: it keeps working after you leave,
|
|
289
|
-
and revoking it does not end your own session. Once you have one, [Deploying from CI](
|
|
290
|
+
and revoking it does not end your own session. Once you have one, [Deploying from CI](#deploying-from-ci)
|
|
290
291
|
is what to do with it.
|
|
291
292
|
|
|
292
|
-
Minting, listing and revoking
|
|
293
|
-
themselves whatever
|
|
293
|
+
Minting, listing and revoking belong to the workspace owner. They are closed to deploy credentials
|
|
294
|
+
themselves, whatever those carry, so a leaked credential cannot mint another. All three run on your
|
|
294
295
|
ordinary `npx wawesome login` session.
|
|
295
296
|
|
|
296
297
|
### Mint one
|
|
@@ -299,8 +300,8 @@ ordinary `npx wawesome login` session.
|
|
|
299
300
|
npx wawesome credentials mint ci-pipeline
|
|
300
301
|
```
|
|
301
302
|
|
|
302
|
-
The secret is printed once and never again
|
|
303
|
-
rebuild it. It goes to stdout on a line of its own and everything else the command prints goes to
|
|
303
|
+
The secret is printed once and never again. The platform stores a digest of it, so no later read
|
|
304
|
+
can rebuild it. It goes to stdout on a line of its own and everything else the command prints goes to
|
|
304
305
|
stderr, so a redirect catches the secret and not one character besides:
|
|
305
306
|
|
|
306
307
|
```bash
|
|
@@ -321,9 +322,9 @@ npx wawesome credentials mint agent -c logs:read -a prod --expires never
|
|
|
321
322
|
| `-a, --app` | Restrict to these Apps, by slug. Repeatable. Default: every App in the workspace |
|
|
322
323
|
| `--expires` | Days, or `never`. Default: 90 |
|
|
323
324
|
|
|
324
|
-
An App named in a restriction does not have to exist yet
|
|
325
|
+
An App named in a restriction does not have to exist yet. A pipeline whose first deploy creates the
|
|
325
326
|
App it was minted for is the ordinary case. A restriction bounds what a credential can disturb, not
|
|
326
|
-
what it can read
|
|
327
|
+
what it can read. A Function deployed into one App still reads the whole workspace's environment at
|
|
327
328
|
runtime.
|
|
328
329
|
|
|
329
330
|
### List them
|
|
@@ -332,7 +333,7 @@ runtime.
|
|
|
332
333
|
npx wawesome credentials
|
|
333
334
|
```
|
|
334
335
|
|
|
335
|
-
Name, displayable prefix, capabilities, Apps, last use and expiry
|
|
336
|
+
Name, displayable prefix, capabilities, Apps, last use and expiry. Never the secret, which the
|
|
336
337
|
platform no longer holds. `LAST USED` lags by a few minutes and reads `never` for a credential
|
|
337
338
|
nothing has ever presented, which is a better reason to revoke one than any calendar date.
|
|
338
339
|
|
|
@@ -350,7 +351,7 @@ it.
|
|
|
350
351
|
|
|
351
352
|
---
|
|
352
353
|
|
|
353
|
-
##
|
|
354
|
+
## Configuration and a custom gateway
|
|
354
355
|
|
|
355
356
|
### `wawesome-function.json`
|
|
356
357
|
|
|
@@ -364,7 +365,7 @@ Every project directory includes a `wawesome-function.json` file generated durin
|
|
|
364
365
|
}
|
|
365
366
|
```
|
|
366
367
|
|
|
367
|
-
`app` is the App this Function is deployed into, and it is client-facing
|
|
368
|
+
`app` is the App this Function is deployed into, and it is client-facing. Every deploy from this
|
|
368
369
|
directory is scoped to it.
|
|
369
370
|
|
|
370
371
|
`function` is the address. Changing it does not rename anything: your Function's URL is built from
|
|
@@ -385,40 +386,40 @@ Add `"assets"` to deploy static files beside your code:
|
|
|
385
386
|
```
|
|
386
387
|
|
|
387
388
|
Everything under that directory is deployed with the version and served at its path beneath your
|
|
388
|
-
Function's URL
|
|
389
|
+
Function's URL, so `dist/client/assets/index-a1.js` answers at `https://<app-host>/<function>/assets/index-a1.js`.
|
|
389
390
|
The CLI hashes each file and asks the platform which of them it does not already hold, so a redeploy
|
|
390
|
-
that changed one chunk uploads one chunk
|
|
391
|
-
|
|
391
|
+
that changed one chunk uploads one chunk. A deploy that changed nothing at all is refused before a
|
|
392
|
+
byte moves.
|
|
392
393
|
|
|
393
394
|
Files are served straight from object storage; your Function is never invoked for one, and no
|
|
394
|
-
invocation is recorded. They answer on your App's own hostname and nowhere else
|
|
395
|
+
invocation is recorded. They answer on your App's own hostname and nowhere else. On the
|
|
395
396
|
development path form (`/x/<tenant>/<app>/<function>/...`) the same address reaches your handler
|
|
396
397
|
as it always has, because a file on an origin every workspace shares would be same-origin with
|
|
397
398
|
all of them. Each carries `Cache-Control: public, max-age=31536000, immutable` and an
|
|
398
|
-
`ETag`, so name your build output by content hash
|
|
399
|
+
`ETag`, so name your build output by content hash. A file's bytes must never change under a name a
|
|
399
400
|
browser has already cached for a year. The content type comes from the extension against a fixed
|
|
400
401
|
allowlist and is never sniffed; anything off it is served as a download.
|
|
401
402
|
|
|
402
403
|
Two rules to know about:
|
|
403
404
|
|
|
404
405
|
- **Everything beneath `assets/` is static**, whatever the deploy carries. A request there never
|
|
405
|
-
reaches your handler
|
|
406
|
+
reaches your handler, and an unknown path under it is a 404 rather than a route for you to answer.
|
|
406
407
|
- **At most 100 files may sit outside `assets/`.** Those paths travel on the version record so a
|
|
407
408
|
request can be routed without a lookup per file. Put bulk output under `assets/`, where a file
|
|
408
409
|
costs nothing; `favicon.ico`, `robots.txt` and a `.well-known/` directory are what the rest is
|
|
409
410
|
for.
|
|
410
411
|
|
|
411
412
|
**HTML is refused at deploy time.** Your Function renders its own markup, and a document served from
|
|
412
|
-
your App's own origin is the sharpest same-origin
|
|
413
|
+
your App's own origin is the sharpest same-origin risk a static file carries.
|
|
413
414
|
|
|
414
415
|
**An SVG is served inert.** The rule behind the refusal above is that nothing you deploy as a file
|
|
415
|
-
becomes a page on your App's own origin, and an SVG opened directly in a browser would
|
|
416
|
-
script, and it renders whatever HTML a `<foreignObject>` holds. It is served rather than
|
|
417
|
-
because making it inert costs the file nothing
|
|
418
|
-
is never checked against the policy, so your drawings render as they always did. Every
|
|
419
|
-
file carries `Content-Security-Policy: script-src 'none'; sandbox`, and only navigating
|
|
420
|
-
one loses anything
|
|
421
|
-
navigate and a page embedding it through `<object>` or `<iframe>` cannot reach into its DOM.
|
|
416
|
+
becomes a page on your App's own origin, and an SVG opened directly in a browser would become one.
|
|
417
|
+
It runs script, and it renders whatever HTML a `<foreignObject>` holds. It is served rather than
|
|
418
|
+
refused because making it inert costs the file nothing. An `<img src="logo.svg">` never ran that
|
|
419
|
+
script and is never checked against the policy, so your drawings render as they always did. Every
|
|
420
|
+
SVG and XML file carries `Content-Security-Policy: script-src 'none'; sandbox`, and only navigating
|
|
421
|
+
straight to one loses anything. Such a file is sandboxed onto an origin of its own, so its links no
|
|
422
|
+
longer navigate and a page embedding it through `<object>` or `<iframe>` cannot reach into its DOM.
|
|
422
423
|
|
|
423
424
|
### Schedules
|
|
424
425
|
|
|
@@ -433,12 +434,12 @@ Add `"schedules"` to run a Function on a recurring timer, with no caller:
|
|
|
433
434
|
}
|
|
434
435
|
```
|
|
435
436
|
|
|
436
|
-
An expression is **five fields, read in UTC
|
|
437
|
-
is no seconds field
|
|
437
|
+
An expression is **five fields, read in UTC**: minute, hour, day of month, month, day of week. There
|
|
438
|
+
is no seconds field and no timezone. A local zone would make one night a year fire a job twice and
|
|
438
439
|
another night not at all.
|
|
439
440
|
|
|
440
441
|
The name is yours to choose and is what the platform keys the schedule by, so editing an expression
|
|
441
|
-
is a change to the same schedule rather than the deletion of one and the creation of another
|
|
442
|
+
is a change to the same schedule rather than the deletion of one and the creation of another. Its
|
|
442
443
|
history and its paused state stay attached to it.
|
|
443
444
|
|
|
444
445
|
Your deploy applies them and prints each one with the time it will next run. It is refused, before
|
|
@@ -448,7 +449,7 @@ five minutes, or if one Function declares more than five schedules.
|
|
|
448
449
|
Two rules worth knowing before you edit the file:
|
|
449
450
|
|
|
450
451
|
- **A schedule you delete from the file is disabled, not deleted.** Its history stays, and declaring
|
|
451
|
-
it again is what turns it back on
|
|
452
|
+
it again is what turns it back on, so a typo costs you a deploy rather than a job's record.
|
|
452
453
|
- **A deploy never resumes a schedule a person paused.** When it runs is code; whether it is running
|
|
453
454
|
is not, and an unrelated commit the next morning must not restart what you stopped at 3am.
|
|
454
455
|
|
|
@@ -469,7 +470,7 @@ A job on a timer should not also be sitting at a guessable URL where a stranger
|
|
|
469
470
|
}
|
|
470
471
|
```
|
|
471
472
|
|
|
472
|
-
A private Function has **no public address at all** in production
|
|
473
|
+
A private Function has **no public address at all** in production, not a hidden one and not one
|
|
473
474
|
behind a credential. A request for it against your App's own hostname is answered with the same 404
|
|
474
475
|
as a Function that was never deployed. This is how you deploy a job with side effects without leaving
|
|
475
476
|
it where a stranger who guesses the slug can fire it.
|
|
@@ -497,16 +498,62 @@ the line is refused, and the deploy tells you so having written nothing.
|
|
|
497
498
|
npx wawesome deploy --publish
|
|
498
499
|
```
|
|
499
500
|
|
|
500
|
-
That is deliberate
|
|
501
|
+
That is deliberate. The line that keeps a job off the internet is one line, and a deploy that
|
|
501
502
|
quietly honoured its deletion would put the job back on the open internet with nothing said. Every
|
|
502
503
|
deploy prints the visibility it landed, beside the URL or in place of it.
|
|
503
504
|
|
|
505
|
+
### A domain of your own
|
|
506
|
+
|
|
507
|
+
Add `"domain"` and your App answers at a name you own, beside the address it already has:
|
|
508
|
+
|
|
509
|
+
```json
|
|
510
|
+
{
|
|
511
|
+
"app": "my-app",
|
|
512
|
+
"function": "api",
|
|
513
|
+
"entry": "src/index.ts",
|
|
514
|
+
"domain": "shop.client.com"
|
|
515
|
+
}
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
The deploy attaches it and prints two DNS records to add at your registrar. The TXT record proves you
|
|
519
|
+
own the name, which is what issues the certificate. The CNAME points traffic here, and you add that
|
|
520
|
+
one once the certificate is issued, so a site that is already live never spends a minute answering on
|
|
521
|
+
a certificate that is not there yet.
|
|
522
|
+
|
|
523
|
+
Your deploy waits for neither. It attaches the domain, prints the records and finishes, and every
|
|
524
|
+
later deploy states where the domain got to: not verified, ownership verified, certificate issued, or
|
|
525
|
+
serving. A domain that is already serving is a line saying so and nothing else.
|
|
526
|
+
|
|
527
|
+
A custom domain is granted from the Solo plan upwards, one per App. On a plan that grants none, the
|
|
528
|
+
deploy prints the refusal and lands everything else it was doing.
|
|
529
|
+
|
|
530
|
+
The address derived from your workspace slug keeps serving after you attach a domain. Both names
|
|
531
|
+
answer, so webhooks and integrations already pointed at the old one keep working.
|
|
532
|
+
|
|
533
|
+
An apex domain, `client.com` with no `www`, attaches like any other name. Whether a CNAME may sit at
|
|
534
|
+
your zone root is your DNS provider's rule, and where it cannot, attach `www.client.com` and configure
|
|
535
|
+
the redirect at the provider.
|
|
536
|
+
|
|
537
|
+
Deleting the line detaches nothing. Detaching takes a live site dark, which is not something a
|
|
538
|
+
deploy should infer from a deleted line. So the deploy reports the domain it found and left serving,
|
|
539
|
+
and names the one command that takes it off:
|
|
540
|
+
|
|
541
|
+
```bash
|
|
542
|
+
npx wawesome domains detach shop.client.com
|
|
543
|
+
```
|
|
544
|
+
|
|
545
|
+
The hostname is typed because an App carries exactly one domain, and the command that removes it
|
|
546
|
+
should not be one you can run by reflex. It says what it is about to do first: the domain stops
|
|
547
|
+
resolving here, this App's own address keeps serving, and the certificate is given back. Afterwards
|
|
548
|
+
the name is free to attach again by declaring it and deploying, which is also what the next deploy
|
|
549
|
+
does if you leave the line in the file.
|
|
550
|
+
|
|
504
551
|
### Reserved headers
|
|
505
552
|
|
|
506
553
|
`x-wawesome-*` belongs to the platform in both directions. It is stripped off the request before your
|
|
507
|
-
handler sees it, and off your response before the caller does
|
|
508
|
-
on that prefix
|
|
509
|
-
you why it vanished.
|
|
554
|
+
handler sees it, and off your response before the caller does. So **do not name a header of your
|
|
555
|
+
own on that prefix**. It is dropped silently rather than rejected, and you will not get an error
|
|
556
|
+
telling you why it vanished.
|
|
510
557
|
|
|
511
558
|
Four headers arrive or leave on it, and the stripping is what makes them worth trusting:
|
|
512
559
|
|
|
@@ -514,13 +561,13 @@ Four headers arrive or leave on it, and the stripping is what makes them worth t
|
|
|
514
561
|
| --- | --- | --- |
|
|
515
562
|
| `x-wawesome-forwarded-prefix` | inbound | The mount that was stripped from the path. Join it to the path you observe to rebuild the caller's URL. |
|
|
516
563
|
| `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. |
|
|
517
|
-
| `x-wawesome-invocation-id` | outbound | The id of this run
|
|
518
|
-
| `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
|
|
564
|
+
| `x-wawesome-invocation-id` | outbound | The id of this run, the key to fetch its logs with `npx wawesome logs --invocation <id>`. |
|
|
565
|
+
| `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. |
|
|
519
566
|
|
|
520
567
|
### Testing against the guest's JavaScript surface
|
|
521
568
|
|
|
522
569
|
Your Function does not run on Node. The engine has no `Intl`, and its `toLocaleString` ignores the
|
|
523
|
-
locale you pass
|
|
570
|
+
locale you pass. `(1234.5).toLocaleString('en-US')` comes back as `"1234.5"`, not `"1,234.50"`. On
|
|
524
571
|
Node both work, which is how a green suite ships a Function that throws in production, or renders
|
|
525
572
|
markup the browser then refuses to hydrate.
|
|
526
573
|
|
|
@@ -537,19 +584,19 @@ export default defineConfig({
|
|
|
537
584
|
|
|
538
585
|
Templates scaffolded with `wawesome init --template` ship this already. With it in place `Intl` is
|
|
539
586
|
gone, `MessageChannel` is the platform's own implementation rather than Node's, and the
|
|
540
|
-
locale-sensitive methods throw with a message naming the remedy
|
|
587
|
+
locale-sensitive methods throw with a message naming the remedy. They throw rather than return the
|
|
541
588
|
engine's unlocalised answer because the platform declares them unsupported, and a wrong string that
|
|
542
589
|
fails nowhere is the thing this is here to stop you shipping.
|
|
543
590
|
|
|
544
|
-
If you bundle an `Intl` polyfill, declare it in your `package.json` as you normally would
|
|
591
|
+
If you bundle an `Intl` polyfill, declare it in your `package.json` as you normally would. A
|
|
545
592
|
dependency that provides `Intl` is left in place rather than stripped out from under you.
|
|
546
593
|
|
|
547
|
-
### Local
|
|
594
|
+
### Local development and gateway overrides
|
|
548
595
|
|
|
549
596
|
If you are running a local gateway or self-hosted instance, you can configure your CLI Gateway URL using any of the
|
|
550
597
|
following:
|
|
551
598
|
|
|
552
|
-
#### 1. Custom
|
|
599
|
+
#### 1. Custom settings (`~/.wawesome/settings.json`)
|
|
553
600
|
|
|
554
601
|
Create `~/.wawesome/settings.json`:
|
|
555
602
|
|
|
@@ -559,13 +606,13 @@ Create `~/.wawesome/settings.json`:
|
|
|
559
606
|
}
|
|
560
607
|
```
|
|
561
608
|
|
|
562
|
-
#### 2. Environment
|
|
609
|
+
#### 2. Environment variables
|
|
563
610
|
|
|
564
611
|
```bash
|
|
565
612
|
export WAWESOME_GATEWAY_URL="http://localhost:3000"
|
|
566
613
|
```
|
|
567
614
|
|
|
568
|
-
#### 3. CLI
|
|
615
|
+
#### 3. CLI flag
|
|
569
616
|
|
|
570
617
|
```bash
|
|
571
618
|
npx wawesome login --gateway http://localhost:3000
|
|
@@ -578,10 +625,10 @@ gateway along with the credentials.
|
|
|
578
625
|
|
|
579
626
|
---
|
|
580
627
|
|
|
581
|
-
##
|
|
628
|
+
## Deploying from CI
|
|
582
629
|
|
|
583
630
|
A **deploy credential** is what deploys without a person at a keyboard. Your workspace mints one, and the CLI reads it
|
|
584
|
-
from a single environment variable
|
|
631
|
+
from a single environment variable. There is no login, and no credentials file is involved at any point:
|
|
585
632
|
|
|
586
633
|
```bash
|
|
587
634
|
export WAWESOME_DEPLOY_CREDENTIAL="wawe_..."
|
|
@@ -598,8 +645,8 @@ While it is set:
|
|
|
598
645
|
- it wins over any `~/.wawesome/credentials.json` on the machine, so a runner with a stale cached home directory still
|
|
599
646
|
deploys as the credential rather than as whoever last logged in there;
|
|
600
647
|
- nothing is written to disk, and the secret stays in the environment where you put it;
|
|
601
|
-
- a refusal
|
|
602
|
-
command needs
|
|
648
|
+
- a refusal names which of four things happened: the credential is unknown, revoked, expired, or does not carry what
|
|
649
|
+
the command needs. It does not ask you to sign in again, which a pipeline cannot do;
|
|
603
650
|
- `login` and `logout` refuse, because neither would change what the next command authenticates as;
|
|
604
651
|
- `credentials` is refused by the platform, because minting, listing and revoking are closed to
|
|
605
652
|
credentials whatever they carry;
|
|
@@ -607,7 +654,7 @@ While it is set:
|
|
|
607
654
|
|
|
608
655
|
---
|
|
609
656
|
|
|
610
|
-
##
|
|
657
|
+
## Security and secrets
|
|
611
658
|
|
|
612
659
|
Wawesome encrypts environment variables at rest using two-tier envelope encryption (AES-256-GCM with per-app data keys
|
|
613
660
|
and AAD context binding). Use `--secret` when setting sensitive keys:
|
|
@@ -618,7 +665,7 @@ npx wawesome env set STRIPE_SECRET_KEY sk_live_xxx --secret
|
|
|
618
665
|
|
|
619
666
|
---
|
|
620
667
|
|
|
621
|
-
##
|
|
668
|
+
## Resources and support
|
|
622
669
|
|
|
623
670
|
- **Platform Homepage**: [https://wawesome.io](https://wawesome.io)
|
|
624
671
|
- **Documentation**: [https://docs.wawesome.io](https://docs.wawesome.io)
|
package/dist/index.mjs
CHANGED
|
@@ -331,10 +331,10 @@ function decodeVlq(encoded) {
|
|
|
331
331
|
//#endregion
|
|
332
332
|
//#region src/guest-surface-scan.ts
|
|
333
333
|
const MAX_LOCATIONS = 3;
|
|
334
|
-
const ABSENT_PROBLEM = "
|
|
335
|
-
const METHOD_PROBLEM = "
|
|
336
|
-
const GUARDED_PROBLEM = "
|
|
337
|
-
const DEFERRED_PROBLEM = "
|
|
334
|
+
const ABSENT_PROBLEM = "The guest does not define it, so evaluating this reference throws a ReferenceError.";
|
|
335
|
+
const METHOD_PROBLEM = "The guest carries no ICU, so it ignores the locale argument.";
|
|
336
|
+
const GUARDED_PROBLEM = "The guest does not define it. This bundle guards the reference, so what runs is the path it takes when the global is missing.";
|
|
337
|
+
const DEFERRED_PROBLEM = "The guest does not define it. The code holding this reference throws a ReferenceError if it ever runs.";
|
|
338
338
|
const GLOBAL_OBJECTS = /* @__PURE__ */ new Set([
|
|
339
339
|
"globalThis",
|
|
340
340
|
"window",
|
|
@@ -472,7 +472,7 @@ function surfaceReportLines(findings) {
|
|
|
472
472
|
const lines = [];
|
|
473
473
|
for (const finding of findings) {
|
|
474
474
|
const headline = finding.severity === "refuse" ? "Refusing to deploy" : "Warning";
|
|
475
|
-
lines.push(`[wawesome] ${headline}: \`${finding.name}
|
|
475
|
+
lines.push(`[wawesome] ${headline}: \`${finding.name}\`. ${finding.problem}`);
|
|
476
476
|
for (const location of finding.locations) lines.push(`[wawesome] at ${location.file}:${location.line}:${location.column}`);
|
|
477
477
|
if (finding.undisplayedLocations > 0) lines.push(`[wawesome] and ${finding.undisplayedLocations} more reference${finding.undisplayedLocations === 1 ? "" : "s"}`);
|
|
478
478
|
lines.push(`[wawesome] Remedy: ${finding.remedy}`);
|
|
@@ -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.11.0";
|
|
887
887
|
//#endregion
|
|
888
888
|
//#region src/prompt.ts
|
|
889
889
|
/**
|
|
@@ -1158,7 +1158,7 @@ async function promptForWorkspaceName(options) {
|
|
|
1158
1158
|
const supplied = options.workspace?.trim();
|
|
1159
1159
|
if (supplied) return supplied;
|
|
1160
1160
|
if (!isInteractive()) throw new Error("No workspace found, and no name to create one with. Re-run with --workspace <name>.");
|
|
1161
|
-
console.log("\n[wawesome] You don't have a workspace yet
|
|
1161
|
+
console.log("\n[wawesome] You don't have a workspace yet. Let's create one.");
|
|
1162
1162
|
console.log(" (this is the name you'll see in the dashboard; its public address is set separately)");
|
|
1163
1163
|
const session = openPromptSession();
|
|
1164
1164
|
try {
|
|
@@ -1454,11 +1454,11 @@ async function reportCredential(creds) {
|
|
|
1454
1454
|
function whenItRuns(schedule) {
|
|
1455
1455
|
const undeclared = schedule.declared === false;
|
|
1456
1456
|
if (schedule.state === "suspended") {
|
|
1457
|
-
const why = "
|
|
1458
|
-
return undeclared ? `suspended, and not in this config file
|
|
1457
|
+
const why = "The workspace owes payment, its subscription ended, or it has spent past its plan's allowance";
|
|
1458
|
+
return undeclared ? `suspended, and not in this config file. ${why}; declare it again too` : `suspended. ${why}`;
|
|
1459
1459
|
}
|
|
1460
|
-
if (schedule.state === "paused") return undeclared ? "paused, and not in this config file
|
|
1461
|
-
if (undeclared) return "not in this config file
|
|
1460
|
+
if (schedule.state === "paused") return undeclared ? "paused, and not in this config file. Declare it again, then resume" : "paused. Resume it to run again";
|
|
1461
|
+
if (undeclared) return "not in this config file, so it is disabled. Its history is kept";
|
|
1462
1462
|
if (schedule.state !== "active") return schedule.state;
|
|
1463
1463
|
return schedule.next_fire_at ? `next run ${asUtc$1(schedule.next_fire_at)}` : "next run unknown";
|
|
1464
1464
|
}
|
|
@@ -1481,6 +1481,283 @@ function scheduleLines(schedules) {
|
|
|
1481
1481
|
return [" Schedules:", ...[...schedules].sort((a, b) => a.name.localeCompare(b.name)).map((schedule) => ` ${schedule.name.padEnd(nameWidth)} ${schedule.expression.padEnd(expressionWidth)} ${whenItRuns(schedule)}`)];
|
|
1482
1482
|
}
|
|
1483
1483
|
//#endregion
|
|
1484
|
+
//#region src/billing.ts
|
|
1485
|
+
function billingPageUrl() {
|
|
1486
|
+
const base = getDashboardUrl().replace(/\/+$/, "");
|
|
1487
|
+
try {
|
|
1488
|
+
return new URL("billing", `${base}/`).toString();
|
|
1489
|
+
} catch {
|
|
1490
|
+
return `${base}/billing`;
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
/** Refusals whose remedy is on the billing page and nowhere else. */
|
|
1494
|
+
const PLAN_LIMITS = [
|
|
1495
|
+
"app-slots-exhausted",
|
|
1496
|
+
"storage-exhausted",
|
|
1497
|
+
"paid-plan-required",
|
|
1498
|
+
"payment-required",
|
|
1499
|
+
"schedule-suspended"
|
|
1500
|
+
];
|
|
1501
|
+
/** The rule itself is the gateway's prose, and is deliberately not restated here. */
|
|
1502
|
+
function planLimitAdvice(reason) {
|
|
1503
|
+
if (!reason || !PLAN_LIMITS.includes(reason)) return "";
|
|
1504
|
+
return `Where to resolve it: ${billingPageUrl()}`;
|
|
1505
|
+
}
|
|
1506
|
+
//#endregion
|
|
1507
|
+
//#region src/domains.ts
|
|
1508
|
+
/**
|
|
1509
|
+
* The domain this project declares, refused here rather than at the gateway so
|
|
1510
|
+
* a value of the wrong shape is a message at the keyboard. What makes a legal
|
|
1511
|
+
* hostname is the platform's rule and is deliberately not restated: a name it
|
|
1512
|
+
* turns down is reported by the deploy that asked, without failing it.
|
|
1513
|
+
*/
|
|
1514
|
+
function declaredDomain(value) {
|
|
1515
|
+
if (value === void 0 || value === null) return void 0;
|
|
1516
|
+
if (typeof value === "string" && value.trim()) return value.trim();
|
|
1517
|
+
console.error(`[wawesome] Error: 'domain' in wawesome-function.json is '${String(value)}'.`);
|
|
1518
|
+
console.error("[wawesome] It must be one hostname, e.g. \"shop.client.com\".");
|
|
1519
|
+
process.exit(1);
|
|
1520
|
+
}
|
|
1521
|
+
/**
|
|
1522
|
+
* Attach the declared domain to the App unless it is already attached.
|
|
1523
|
+
*
|
|
1524
|
+
* Never throws, and never blocks on a domain coming up: by the time this runs
|
|
1525
|
+
* the code is deployed, and a certificate nobody has issued yet must not turn a
|
|
1526
|
+
* landed deploy into a failed one. Whatever the platform refuses is carried
|
|
1527
|
+
* back as prose to print rather than raised.
|
|
1528
|
+
*/
|
|
1529
|
+
async function applyDeclaredDomain(creds, app, hostname) {
|
|
1530
|
+
let held;
|
|
1531
|
+
try {
|
|
1532
|
+
held = await listDomains(creds, app);
|
|
1533
|
+
} catch (err) {
|
|
1534
|
+
return {
|
|
1535
|
+
kind: "unread",
|
|
1536
|
+
hostname,
|
|
1537
|
+
said: whatWentWrong(err)
|
|
1538
|
+
};
|
|
1539
|
+
}
|
|
1540
|
+
const attached = held.find((domain) => isSameHostname(domain.hostname, hostname));
|
|
1541
|
+
if (attached) return {
|
|
1542
|
+
kind: "unchanged",
|
|
1543
|
+
domain: attached
|
|
1544
|
+
};
|
|
1545
|
+
try {
|
|
1546
|
+
return {
|
|
1547
|
+
kind: "attached",
|
|
1548
|
+
domain: await attachDomain(creds, app, hostname)
|
|
1549
|
+
};
|
|
1550
|
+
} catch (err) {
|
|
1551
|
+
const refusal = err instanceof GatewayError ? err : void 0;
|
|
1552
|
+
return {
|
|
1553
|
+
kind: "refused",
|
|
1554
|
+
hostname,
|
|
1555
|
+
said: whatWentWrong(err),
|
|
1556
|
+
advice: planLimitAdvice(refusal?.reason),
|
|
1557
|
+
held: held.find((domain) => !domain.detaching)?.hostname
|
|
1558
|
+
};
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
/**
|
|
1562
|
+
* What the App carries that its configuration does not, for a deploy that
|
|
1563
|
+
* declares no domain at all.
|
|
1564
|
+
*
|
|
1565
|
+
* Reports and never acts. A read that fails says nothing rather than guessing:
|
|
1566
|
+
* a project with no domain in its file has nothing to be told about one, and a
|
|
1567
|
+
* false alarm here is one about a client's site that is up.
|
|
1568
|
+
*/
|
|
1569
|
+
async function findUndeclaredDomain(creds, app) {
|
|
1570
|
+
let held;
|
|
1571
|
+
try {
|
|
1572
|
+
held = await listDomains(creds, app);
|
|
1573
|
+
} catch {
|
|
1574
|
+
return null;
|
|
1575
|
+
}
|
|
1576
|
+
const domain = held.find((candidate) => !candidate.detaching);
|
|
1577
|
+
return domain ? {
|
|
1578
|
+
kind: "undeclared",
|
|
1579
|
+
domain
|
|
1580
|
+
} : null;
|
|
1581
|
+
}
|
|
1582
|
+
/**
|
|
1583
|
+
* The platform's own words where it has any. A credential refusal is read
|
|
1584
|
+
* through the same reading every other command gives one, so a CI runner is
|
|
1585
|
+
* told what its credential lacks rather than to sign in again.
|
|
1586
|
+
*/
|
|
1587
|
+
function whatWentWrong(err) {
|
|
1588
|
+
if (err instanceof GatewayError && (err.status === 401 || err.status === 403)) return unauthorizedMessage(err.status, err.body);
|
|
1589
|
+
return errorText(err);
|
|
1590
|
+
}
|
|
1591
|
+
/**
|
|
1592
|
+
* The comparison the platform makes: a name is held lowercase without its
|
|
1593
|
+
* trailing dot, so `shop.client.com.` in the file is the domain already
|
|
1594
|
+
* attached rather than a second one to ask for on every deploy.
|
|
1595
|
+
*/
|
|
1596
|
+
function isSameHostname(held, declared) {
|
|
1597
|
+
const same = (name) => name.trim().replace(/\.+$/, "").toLowerCase();
|
|
1598
|
+
return same(held) === same(declared);
|
|
1599
|
+
}
|
|
1600
|
+
async function listDomains(creds, app) {
|
|
1601
|
+
const res = await authorizedFetch(domainsUrl(creds, app), {
|
|
1602
|
+
method: "GET",
|
|
1603
|
+
headers: { Authorization: `Bearer ${creds.tenant_jwt}` }
|
|
1604
|
+
});
|
|
1605
|
+
if (!res.ok) throw await asGatewayError(res, `The App's domains could not be read (HTTP ${res.status}).`);
|
|
1606
|
+
const { domains = [] } = await res.json();
|
|
1607
|
+
return domains;
|
|
1608
|
+
}
|
|
1609
|
+
async function attachDomain(creds, app, hostname) {
|
|
1610
|
+
const res = await authorizedFetch(domainsUrl(creds, app), {
|
|
1611
|
+
method: "POST",
|
|
1612
|
+
headers: {
|
|
1613
|
+
Authorization: `Bearer ${creds.tenant_jwt}`,
|
|
1614
|
+
"Content-Type": "application/json"
|
|
1615
|
+
},
|
|
1616
|
+
body: JSON.stringify({ hostname })
|
|
1617
|
+
});
|
|
1618
|
+
if (!res.ok) throw await asGatewayError(res, `'${hostname}' could not be attached (HTTP ${res.status}).`);
|
|
1619
|
+
const { domain } = await res.json();
|
|
1620
|
+
return domain;
|
|
1621
|
+
}
|
|
1622
|
+
/**
|
|
1623
|
+
* Take a domain off the App, naming it. The name is the route's requirement and
|
|
1624
|
+
* ADR-0025's argument, not this function's.
|
|
1625
|
+
*/
|
|
1626
|
+
async function detachDomain(creds, app, hostname) {
|
|
1627
|
+
const res = await authorizedFetch(`${domainsUrl(creds, app)}/${encodeURIComponent(hostname)}`, {
|
|
1628
|
+
method: "DELETE",
|
|
1629
|
+
headers: { Authorization: `Bearer ${creds.tenant_jwt}` }
|
|
1630
|
+
});
|
|
1631
|
+
if (!res.ok) throw await asGatewayError(res, `'${hostname}' could not be detached (HTTP ${res.status}).`);
|
|
1632
|
+
}
|
|
1633
|
+
/** A missing name is refused here rather than sent, so the reason reads at the keyboard. */
|
|
1634
|
+
async function detachDomainCommand(hostname, options = {}) {
|
|
1635
|
+
if (!hostname || !hostname.trim()) {
|
|
1636
|
+
console.error("[wawesome] Error: Missing domain. Usage: wawesome domains detach <hostname>");
|
|
1637
|
+
console.error("[wawesome] The domain is typed out because detaching one takes it dark.");
|
|
1638
|
+
process.exit(1);
|
|
1639
|
+
}
|
|
1640
|
+
const creds = readCredentials();
|
|
1641
|
+
if (!creds) {
|
|
1642
|
+
console.error("[wawesome] Error: Not logged in. Run 'wawesome login' first.");
|
|
1643
|
+
process.exit(1);
|
|
1644
|
+
}
|
|
1645
|
+
const app = options.app?.trim() || readFunctionConfig()?.app?.trim();
|
|
1646
|
+
if (!app) {
|
|
1647
|
+
console.error("[wawesome] Error: No wawesome-function.json found in the current directory.");
|
|
1648
|
+
console.error("[wawesome] Run this from your project, or name the app with --app <slug>.");
|
|
1649
|
+
process.exit(1);
|
|
1650
|
+
}
|
|
1651
|
+
const name = hostname.trim();
|
|
1652
|
+
console.log(`[wawesome] Detaching '${name}' from app '${app}':`);
|
|
1653
|
+
console.log("[wawesome] • the domain stops resolving here, so anything pointed at it stops");
|
|
1654
|
+
console.log("[wawesome] reaching this app");
|
|
1655
|
+
console.log("[wawesome] • this app's own address is unaffected and keeps serving");
|
|
1656
|
+
console.log("[wawesome] • the certificate is given back, and none is held for the name");
|
|
1657
|
+
try {
|
|
1658
|
+
await detachDomain(creds, app, name);
|
|
1659
|
+
} catch (err) {
|
|
1660
|
+
console.error(`[wawesome] Error: ${whatWentWrong(err)}`);
|
|
1661
|
+
process.exit(1);
|
|
1662
|
+
}
|
|
1663
|
+
console.log(`[wawesome] ✅ Detached '${name}' from app '${app}'.`);
|
|
1664
|
+
console.log("[wawesome] The domain is free to attach again by declaring it and deploying.");
|
|
1665
|
+
const declared = readFunctionConfig()?.domain;
|
|
1666
|
+
if (typeof declared === "string" && isSameHostname(declared, name)) {
|
|
1667
|
+
console.log(`[wawesome] wawesome-function.json still declares '${declared}'. Remove the line,`);
|
|
1668
|
+
console.log("[wawesome] or the next deploy attaches the domain again.");
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
async function domainsCommand(action, target, options = {}) {
|
|
1672
|
+
if ((action || "").toLowerCase().trim() === "detach") return detachDomainCommand(target, options);
|
|
1673
|
+
console.error(`[wawesome] Error: Unknown command '${["domains", action].filter(Boolean).join(" ")}'.`);
|
|
1674
|
+
console.error("[wawesome] Usage: wawesome domains detach <hostname>");
|
|
1675
|
+
console.error("[wawesome] A domain is attached by declaring it in wawesome-function.json.");
|
|
1676
|
+
process.exit(1);
|
|
1677
|
+
}
|
|
1678
|
+
function domainsUrl(creds, app) {
|
|
1679
|
+
return `${creds.gateway_url}/v1/apps/${encodeURIComponent(app)}/domains`;
|
|
1680
|
+
}
|
|
1681
|
+
/**
|
|
1682
|
+
* Where a domain got to, said beside the name itself.
|
|
1683
|
+
*
|
|
1684
|
+
* Each state names the one record that is outstanding, because the two are
|
|
1685
|
+
* added a day apart: the TXT issues the certificate, and the CNAME is pointed
|
|
1686
|
+
* here afterwards so a site that is already live never answers on a
|
|
1687
|
+
* certificate that is not there yet.
|
|
1688
|
+
*/
|
|
1689
|
+
function whereItGotTo(state) {
|
|
1690
|
+
if (state.serving) return "serving";
|
|
1691
|
+
if (state.certificate_active) return "certificate issued, so point the CNAME here to cut over";
|
|
1692
|
+
if (state.ownership_verified) return "ownership verified, and the certificate is being issued";
|
|
1693
|
+
return "not verified yet, so add the TXT record below";
|
|
1694
|
+
}
|
|
1695
|
+
function whatItIsFor(record) {
|
|
1696
|
+
return record.purpose === "certificate" ? "Proves you own the name, and issues the certificate" : "Points traffic here, once the certificate is issued";
|
|
1697
|
+
}
|
|
1698
|
+
const VALUE_COLUMN = " ".repeat(14);
|
|
1699
|
+
/**
|
|
1700
|
+
* The block a deploy prints for the domain its configuration declares, in the
|
|
1701
|
+
* columns the visibility and URL lines above it already use.
|
|
1702
|
+
*/
|
|
1703
|
+
function domainLines(outcome) {
|
|
1704
|
+
if (outcome.kind === "unread") return [
|
|
1705
|
+
"",
|
|
1706
|
+
` Domain: \x1b[33m${outcome.hostname}: where it got to is unknown\x1b[0m`,
|
|
1707
|
+
...wrapped(outcome.said).map((line) => `${VALUE_COLUMN}${line}`),
|
|
1708
|
+
`${VALUE_COLUMN}Nothing about the domain changed, and the deploy landed.`
|
|
1709
|
+
];
|
|
1710
|
+
if (outcome.kind === "undeclared") {
|
|
1711
|
+
const { hostname, state } = outcome.domain;
|
|
1712
|
+
const left = state.serving ? "it is still serving" : "nothing changed";
|
|
1713
|
+
return [
|
|
1714
|
+
"",
|
|
1715
|
+
` Domain: \x1b[33m${hostname}: attached, and this project declares no domain\x1b[0m`,
|
|
1716
|
+
...wrapped(`Nothing was detached and ${left}. A domain comes off an App when somebody names it, never because a line was deleted. Declare it again in wawesome-function.json, or take it off with`).map((line) => `${VALUE_COLUMN}${line}`),
|
|
1717
|
+
`${VALUE_COLUMN} \x1b[36mwawesome domains detach ${hostname}\x1b[0m`
|
|
1718
|
+
];
|
|
1719
|
+
}
|
|
1720
|
+
if (outcome.kind === "refused") return [
|
|
1721
|
+
"",
|
|
1722
|
+
` Domain: \x1b[33m${outcome.hostname}: not attached\x1b[0m`,
|
|
1723
|
+
...wrapped(outcome.said).map((line) => `${VALUE_COLUMN}${line}`),
|
|
1724
|
+
...outcome.advice ? [`${VALUE_COLUMN}${outcome.advice}`] : [],
|
|
1725
|
+
...outcome.held ? [`${VALUE_COLUMN}Nothing was detached. Take '${outcome.held}' off first with`, `${VALUE_COLUMN} \x1b[36mwawesome domains detach ${outcome.held}\x1b[0m`] : [],
|
|
1726
|
+
`${VALUE_COLUMN}The deploy landed, and this App's other address still answers.`
|
|
1727
|
+
];
|
|
1728
|
+
const { hostname, records, state, last_checked_at, detaching } = outcome.domain;
|
|
1729
|
+
if (detaching) return [
|
|
1730
|
+
"",
|
|
1731
|
+
` Domain: \x1b[33m${hostname}: being detached\x1b[0m`,
|
|
1732
|
+
`${VALUE_COLUMN}It has stopped answering, and the certificate is being given back.`,
|
|
1733
|
+
`${VALUE_COLUMN}Remove 'domain' from wawesome-function.json, or deploy again`,
|
|
1734
|
+
`${VALUE_COLUMN}once it is gone to attach it afresh.`
|
|
1735
|
+
];
|
|
1736
|
+
const lines = ["", ` Domain: \x1b[36m${hostname}\x1b[0m: ${whereItGotTo(state)}`];
|
|
1737
|
+
if (state.serving) {
|
|
1738
|
+
lines.push(`${VALUE_COLUMN}The address above answers too, so nothing pointed at it breaks.`);
|
|
1739
|
+
return lines;
|
|
1740
|
+
}
|
|
1741
|
+
if (last_checked_at) lines.push(`${VALUE_COLUMN}Last checked ${asUtc$1(last_checked_at)}.`);
|
|
1742
|
+
const nameWidth = Math.max(...records.map((record) => record.name.length));
|
|
1743
|
+
lines.push("", " DNS records to add at your registrar:");
|
|
1744
|
+
for (const record of records) lines.push("", ` ${whatItIsFor(record)}:`, ` ${record.type.padEnd(5)} ${record.name.padEnd(nameWidth)} ${record.value}`);
|
|
1745
|
+
lines.push("", `${VALUE_COLUMN}The deploy is finished. This domain comes up on its own`, `${VALUE_COLUMN}once the records resolve.`);
|
|
1746
|
+
return lines;
|
|
1747
|
+
}
|
|
1748
|
+
/** The platform's own prose, kept inside the block it is printed in. */
|
|
1749
|
+
function wrapped(said) {
|
|
1750
|
+
const width = 64;
|
|
1751
|
+
const lines = [];
|
|
1752
|
+
let line = "";
|
|
1753
|
+
for (const word of said.split(/\s+/).filter(Boolean)) if (line && `${line} ${word}`.length > width) {
|
|
1754
|
+
lines.push(line);
|
|
1755
|
+
line = word;
|
|
1756
|
+
} else line = line ? `${line} ${word}` : word;
|
|
1757
|
+
if (line) lines.push(line);
|
|
1758
|
+
return lines;
|
|
1759
|
+
}
|
|
1760
|
+
//#endregion
|
|
1484
1761
|
//#region src/usage.ts
|
|
1485
1762
|
const USAGE_TIMEOUT_MS = 2e3;
|
|
1486
1763
|
/**
|
|
@@ -1557,7 +1834,7 @@ const MONTHS = [
|
|
|
1557
1834
|
];
|
|
1558
1835
|
/** Indented to sit inside the receipt the deploy already prints. */
|
|
1559
1836
|
function headroomLines(usage) {
|
|
1560
|
-
const lines = [` Plan: ${usage.plan.name}
|
|
1837
|
+
const lines = [` Plan: ${usage.plan.name}. This deploy does not change your bill.`, ` Apps: ${usage.occupied_app_slots} / ${usage.plan.limits.app_slots} slots`];
|
|
1561
1838
|
const stored = storedLine(usage);
|
|
1562
1839
|
if (stored !== null) lines.push(stored);
|
|
1563
1840
|
lines.push(` Usage: ${periodLabel(usage.period.start, usage.period.end)}`);
|
|
@@ -1640,29 +1917,6 @@ function widest(values) {
|
|
|
1640
1917
|
return values.reduce((longest, value) => Math.max(longest, value.length), 0);
|
|
1641
1918
|
}
|
|
1642
1919
|
//#endregion
|
|
1643
|
-
//#region src/billing.ts
|
|
1644
|
-
function billingPageUrl() {
|
|
1645
|
-
const base = getDashboardUrl().replace(/\/+$/, "");
|
|
1646
|
-
try {
|
|
1647
|
-
return new URL("billing", `${base}/`).toString();
|
|
1648
|
-
} catch {
|
|
1649
|
-
return `${base}/billing`;
|
|
1650
|
-
}
|
|
1651
|
-
}
|
|
1652
|
-
/** Refusals whose remedy is on the billing page and nowhere else. */
|
|
1653
|
-
const PLAN_LIMITS = [
|
|
1654
|
-
"app-slots-exhausted",
|
|
1655
|
-
"storage-exhausted",
|
|
1656
|
-
"paid-plan-required",
|
|
1657
|
-
"payment-required",
|
|
1658
|
-
"schedule-suspended"
|
|
1659
|
-
];
|
|
1660
|
-
/** The rule itself is the gateway's prose, and is deliberately not restated here. */
|
|
1661
|
-
function planLimitAdvice(reason) {
|
|
1662
|
-
if (!reason || !PLAN_LIMITS.includes(reason)) return "";
|
|
1663
|
-
return `Where to resolve it: ${billingPageUrl()}`;
|
|
1664
|
-
}
|
|
1665
|
-
//#endregion
|
|
1666
1920
|
//#region src/assets.ts
|
|
1667
1921
|
/**
|
|
1668
1922
|
* The files under `dir`, hashed.
|
|
@@ -1862,8 +2116,8 @@ function divergenceLines(previous, target, addresses) {
|
|
|
1862
2116
|
`[wawesome] wawesome-function.json now says ${qualifiedName(target)}.`,
|
|
1863
2117
|
"",
|
|
1864
2118
|
"[wawesome] Deploying will not rename it. A Function's address is built from its",
|
|
1865
|
-
"[wawesome] name, so this deploy lands on a Function of its own
|
|
1866
|
-
"[wawesome]
|
|
2119
|
+
"[wawesome] name, so this deploy lands on a Function of its own. The old one stays",
|
|
2120
|
+
"[wawesome] promoted, and still answers the code on it:",
|
|
1867
2121
|
"",
|
|
1868
2122
|
`[wawesome] stays live: ${addresses.previous ?? qualifiedName(previous)}`,
|
|
1869
2123
|
`[wawesome] gets this deploy: ${addresses.next ?? qualifiedName(target)}`,
|
|
@@ -1917,7 +2171,7 @@ function declaredVisibility(value) {
|
|
|
1917
2171
|
function reportPublishRefusal(message) {
|
|
1918
2172
|
console.error(`\n[wawesome] \x1b[31mError: ${message}\x1b[0m`);
|
|
1919
2173
|
console.error("[wawesome] Nothing was deployed. This Function is private, so its address");
|
|
1920
|
-
console.error("[wawesome] resolves for nobody
|
|
2174
|
+
console.error("[wawesome] resolves for nobody. Publishing it puts it back on the open");
|
|
1921
2175
|
console.error("[wawesome] internet.");
|
|
1922
2176
|
console.error("[wawesome] Re-run with \x1B[36mwawesome deploy --publish\x1B[0m to publish it, or put");
|
|
1923
2177
|
console.error("[wawesome] \x1B[36m\"visibility\": \"private\"\x1B[0m back in wawesome-function.json.\n");
|
|
@@ -1943,6 +2197,7 @@ async function deploy(entryInput, options) {
|
|
|
1943
2197
|
visibility: declaredVisibility(config.visibility),
|
|
1944
2198
|
confirmPublish: Boolean(options.publish)
|
|
1945
2199
|
};
|
|
2200
|
+
const domainName = declaredDomain(config.domain);
|
|
1946
2201
|
if (isVerbose) {
|
|
1947
2202
|
console.log(`[wawesome:verbose] Deploying to app=${app}, function=${funcName}`);
|
|
1948
2203
|
console.log(`[wawesome:verbose] Gateway: ${creds.gateway_url}`);
|
|
@@ -2077,6 +2332,7 @@ async function deploy(entryInput, options) {
|
|
|
2077
2332
|
} catch (err) {
|
|
2078
2333
|
if (isVerbose) console.log(`[wawesome:verbose] Could not resolve the workspace address: ${err instanceof Error ? err.message : err}`);
|
|
2079
2334
|
}
|
|
2335
|
+
const domain = domainName ? await applyDeclaredDomain(creds, app, domainName) : await findUndeclaredDomain(creds, app);
|
|
2080
2336
|
let headroom = null;
|
|
2081
2337
|
try {
|
|
2082
2338
|
headroom = headroomLines(await fetchTenantUsage(creds));
|
|
@@ -2096,16 +2352,17 @@ async function deploy(entryInput, options) {
|
|
|
2096
2352
|
for (const line of scheduleLines(schedules)) console.log(line);
|
|
2097
2353
|
}
|
|
2098
2354
|
if (visibility === "private") {
|
|
2099
|
-
console.log("\n URL: none
|
|
2355
|
+
console.log("\n URL: none. This Function is private, so it is not reachable");
|
|
2100
2356
|
console.log(" from the web at all.");
|
|
2101
2357
|
} else if (address) {
|
|
2102
2358
|
console.log(`\n URL: \x1b[36m${address}\x1b[0m`);
|
|
2103
2359
|
console.log(` ${SUBTREE_NOTE}`);
|
|
2104
2360
|
} else if (surface) {
|
|
2105
|
-
console.log("\n URL: none
|
|
2361
|
+
console.log("\n URL: none. This gateway serves no public address form.");
|
|
2106
2362
|
console.log(" Set CONTENT_ORIGIN on it, or ALLOW_PATH_INVOCATION_FORM");
|
|
2107
2363
|
console.log(" for local development.");
|
|
2108
2364
|
}
|
|
2365
|
+
if (domain) for (const line of domainLines(domain)) console.log(line);
|
|
2109
2366
|
if (headroom) {
|
|
2110
2367
|
console.log("");
|
|
2111
2368
|
for (const line of headroom) console.log(line);
|
|
@@ -2116,7 +2373,8 @@ async function deploy(entryInput, options) {
|
|
|
2116
2373
|
functionName: funcName,
|
|
2117
2374
|
version,
|
|
2118
2375
|
address,
|
|
2119
|
-
schedules
|
|
2376
|
+
schedules,
|
|
2377
|
+
domain
|
|
2120
2378
|
};
|
|
2121
2379
|
}
|
|
2122
2380
|
/**
|
|
@@ -2196,8 +2454,8 @@ async function uploadAssets(creds, app, funcName, bundle, assets, declared, isVe
|
|
|
2196
2454
|
//#region src/env.ts
|
|
2197
2455
|
const STANDARD_SECRET_MESSAGES = [
|
|
2198
2456
|
"Encrypted at rest using AES-256",
|
|
2199
|
-
"This value can't be viewed again after you save it
|
|
2200
|
-
"Only decrypted at the moment your function runs
|
|
2457
|
+
"This value can't be viewed again after you save it, only updated or deleted",
|
|
2458
|
+
"Only decrypted at the moment your function runs, and never returned by the CLI, dashboard, or API after creation."
|
|
2201
2459
|
];
|
|
2202
2460
|
/**
|
|
2203
2461
|
* Load credentials and validate project config.
|
|
@@ -2886,7 +3144,7 @@ async function listTemplates(options) {
|
|
|
2886
3144
|
}
|
|
2887
3145
|
console.log("\n📦 \x1B[1mTemplates\x1B[0m\n");
|
|
2888
3146
|
for (const template of templates) {
|
|
2889
|
-
console.log(` \x1b[36m${template.name}\x1b[0m
|
|
3147
|
+
console.log(` \x1b[36m${template.name}\x1b[0m: ${template.title}`);
|
|
2890
3148
|
console.log(` ${template.description}`);
|
|
2891
3149
|
if (template.tags.length > 0) console.log(` \x1b[90m${template.tags.join(", ")}\x1b[0m`);
|
|
2892
3150
|
console.log(` \x1b[90m${template.source_url}\x1b[0m`);
|
|
@@ -3008,7 +3266,7 @@ async function ensureSession(options) {
|
|
|
3008
3266
|
const probed = await probeSession(stored);
|
|
3009
3267
|
if (probed) return probed;
|
|
3010
3268
|
}
|
|
3011
|
-
console.log(stored ? "\n[wawesome] Your session has expired." : "\n[wawesome] You're not logged in yet
|
|
3269
|
+
console.log(stored ? "\n[wawesome] Your session has expired." : "\n[wawesome] You're not logged in yet, which is what a deploy needs.");
|
|
3012
3270
|
if (!isInteractive()) {
|
|
3013
3271
|
console.log("[wawesome] Run 'wawesome login', then 'wawesome deploy'.");
|
|
3014
3272
|
return null;
|
|
@@ -3072,12 +3330,12 @@ async function offerWorkspaceAddress(session, creds, tenant, appSlug, functionNa
|
|
|
3072
3330
|
return;
|
|
3073
3331
|
}
|
|
3074
3332
|
if (tenant.slug_locked) {
|
|
3075
|
-
console.log(` 🔒 '${current}' is fixed
|
|
3333
|
+
console.log(` 🔒 '${current}' is fixed. A Function version has been promoted, and live URLs already carry it.\n`);
|
|
3076
3334
|
return;
|
|
3077
3335
|
}
|
|
3078
|
-
console.log(` '${current}' is your workspace address
|
|
3079
|
-
console.log(" rather than this project's
|
|
3080
|
-
console.log(" for good the first time you deploy, because live URLs carry it.");
|
|
3336
|
+
console.log(` '${current}' is your workspace address, the part of that URL that is`);
|
|
3337
|
+
console.log(" yours rather than this project's. Now is the moment to change it. It");
|
|
3338
|
+
console.log(" locks for good the first time you deploy, because live URLs carry it.");
|
|
3081
3339
|
console.log(" Press enter to keep it.\n");
|
|
3082
3340
|
let refusal;
|
|
3083
3341
|
for (let attempt = 0; attempt < MAX_RENAME_ATTEMPTS; attempt++) {
|
|
@@ -3109,7 +3367,7 @@ async function offerWorkspaceAddress(session, creds, tenant, appSlug, functionNa
|
|
|
3109
3367
|
* template from needing a CLI release.
|
|
3110
3368
|
*/
|
|
3111
3369
|
async function wireUp(creds, appSlug, manifest, answers) {
|
|
3112
|
-
const scaffolded = "Your project is scaffolded
|
|
3370
|
+
const scaffolded = "Your project is scaffolded. Fix this, then run 'wawesome deploy'.";
|
|
3113
3371
|
try {
|
|
3114
3372
|
await ensureApp(creds, appSlug);
|
|
3115
3373
|
} catch (err) {
|
|
@@ -3192,7 +3450,7 @@ async function initFromTemplate(templateName, options) {
|
|
|
3192
3450
|
const session = openPromptSession();
|
|
3193
3451
|
try {
|
|
3194
3452
|
functionName = await promptForSlug(session, "Function name", readFunctionConfig(staging)?.function || dirName);
|
|
3195
|
-
console.log(" (an App groups the Functions of one project
|
|
3453
|
+
console.log(" (an App groups the Functions of one project, and its slug is part of the public URL)");
|
|
3196
3454
|
appSlug = await promptForSlug(session, "App slug", dirName);
|
|
3197
3455
|
let repinned = null;
|
|
3198
3456
|
try {
|
|
@@ -3206,7 +3464,7 @@ async function initFromTemplate(templateName, options) {
|
|
|
3206
3464
|
fail$1(errorText(err), `Some of '${template.name}' may have been written to ${cwd}.`);
|
|
3207
3465
|
}
|
|
3208
3466
|
console.log(`\n[wawesome] ✅ Scaffolded ${files.length} files from '${template.name}'.`);
|
|
3209
|
-
if (repinned) console.log(`[wawesome] Using wawesome ${repinned.to}
|
|
3467
|
+
if (repinned) console.log(`[wawesome] Using wawesome ${repinned.to}. The template pinned ${repinned.from}.`);
|
|
3210
3468
|
if (tenantSession) await offerWorkspaceAddress(session, tenantSession.creds, tenantSession.tenant, appSlug, functionName);
|
|
3211
3469
|
answers = tenantSession ? await promptForDeclaredEnv(session, manifest.env) : [];
|
|
3212
3470
|
} finally {
|
|
@@ -3255,7 +3513,7 @@ async function init(options) {
|
|
|
3255
3513
|
functionName = "root";
|
|
3256
3514
|
console.log(" Function name? root (locked by --root)");
|
|
3257
3515
|
} else functionName = await promptForSlug(session, "Function name", dirName);
|
|
3258
|
-
console.log(" (an App groups the Functions of one project
|
|
3516
|
+
console.log(" (an App groups the Functions of one project, and its slug is part of the public URL)");
|
|
3259
3517
|
appSlug = await promptForSlug(session, "App slug", dirName);
|
|
3260
3518
|
} finally {
|
|
3261
3519
|
session.close();
|
|
@@ -4262,7 +4520,7 @@ async function showWorkspace(options) {
|
|
|
4262
4520
|
console.log(` ${SUBTREE_NOTE}`);
|
|
4263
4521
|
}
|
|
4264
4522
|
}
|
|
4265
|
-
if (tenant.slug_locked) console.log("\n 🔒 The address is fixed
|
|
4523
|
+
if (tenant.slug_locked) console.log("\n 🔒 The address is fixed. A Function version has been promoted, and live URLs carry it.");
|
|
4266
4524
|
else {
|
|
4267
4525
|
console.log("\n The address can still be changed: \x1B[36mwawesome workspace rename <name>\x1B[0m");
|
|
4268
4526
|
console.log(" It locks for good the first time you deploy.");
|
|
@@ -5034,6 +5292,8 @@ cli.command("env [action] [key] [value]", "Manage environment variables (set, li
|
|
|
5034
5292
|
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));
|
|
5035
5293
|
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));
|
|
5036
5294
|
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));
|
|
5295
|
+
cli.command("domains [action] [hostname]", "Manage the custom domain on the current app (detach)").usage("domains <action> [hostname]\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 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));
|
|
5296
|
+
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));
|
|
5037
5297
|
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));
|
|
5038
5298
|
cli.command("logout", "Clear stored authentication credentials").action(() => logout());
|
|
5039
5299
|
cli.command("whoami", "Show current login session info").action(() => whoami());
|
|
@@ -5059,30 +5319,30 @@ cli.command("credentials [action] [target]", "Mint, list and revoke deploy crede
|
|
|
5059
5319
|
wawesome credentials revoke ci-pipeline
|
|
5060
5320
|
wawesome credentials revoke wawe_ab3k9x --yes`).option("-c, --capability <capability>", "Capability to grant: deploy, logs:read (repeatable or comma-separated, default: deploy)").option("-a, --app <app>", "Restrict the credential to this App (repeatable or comma-separated, default: every App)").option("--expires <days|never>", "Days until the credential expires, or 'never' (default: 90)").option("-y, --yes", "Skip the revoke confirmation").option("-v, --verbose", "Enable verbose debug output").action((action, target, options) => credentialsCommand(action, target, options));
|
|
5061
5321
|
cli.command("credentials mint <name>", "Mint a deploy credential, printing its secret exactly once").option("-c, --capability <capability>", "Capability to grant: deploy, logs:read (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));
|
|
5062
|
-
cli.command("credentials list", "List deploy credentials
|
|
5322
|
+
cli.command("credentials list", "List deploy credentials, never their secrets").alias("credentials ls").option("-v, --verbose", "Enable verbose debug output").action((options) => listCredentialsCommand(options));
|
|
5063
5323
|
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));
|
|
5064
5324
|
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));
|
|
5065
5325
|
cli.command("logs [function-name-or-invocation-id]", "View invocation history, fetch log output, or follow live").usage(`logs [target] [options]
|
|
5066
5326
|
|
|
5067
5327
|
The target argument determines what the command does:
|
|
5068
5328
|
|
|
5069
|
-
MODE 1
|
|
5329
|
+
MODE 1: list invocations (no UUID target)
|
|
5070
5330
|
wawesome logs # list invocations for the function in the current directory
|
|
5071
5331
|
wawesome logs my-function # list invocations for 'my-function'
|
|
5072
5332
|
wawesome logs my-function --error # only show failed invocations
|
|
5073
5333
|
wawesome logs my-function --running # only show currently running invocations
|
|
5074
5334
|
|
|
5075
|
-
MODE 2
|
|
5335
|
+
MODE 2: fetch the captured log body (UUID target or --invocation)
|
|
5076
5336
|
wawesome logs <invocation-id> # print stdout/stderr for a specific invocation
|
|
5077
5337
|
wawesome logs my-function --invocation <id> # same, explicit flag form
|
|
5078
5338
|
|
|
5079
|
-
MODE 3
|
|
5339
|
+
MODE 3: follow live output (--follow / -f)
|
|
5080
5340
|
wawesome logs my-function --follow # follow the function: stream output from every invocation as it runs
|
|
5081
5341
|
wawesome logs <invocation-id> --follow # follow one specific in-flight invocation by ID
|
|
5082
5342
|
|
|
5083
|
-
With a function name, --follow streams the function's output
|
|
5084
|
-
|
|
5085
|
-
specific
|
|
5343
|
+
With a function name, --follow streams the function's output across invocations.
|
|
5344
|
+
New output appears each time the function runs, so you never have to catch a
|
|
5345
|
+
specific one. Press Ctrl-C to stop at any time.`).option("-f, --follow", "Stream live output (tail -f style). Follows a running invocation or waits for the next one. Ctrl-C to stop").option("-i, --invocation <id>", "Fetch stdout/stderr log body for a specific invocation ID").option("-a, --app <app>", "App slug override (defaults to wawesome-function.json)").option("-s, --status <status>", "Filter invocations by status (success, error, timeout, running)").option("--success", "Shorthand for --status success").option("--error", "Shorthand for --status error").option("--timeout", "Shorthand for --status timeout").option("--running", "Shorthand for --status running").option("-v, --verbose", "Enable verbose debug output").action((target, options) => logsCommand(target, options));
|
|
5086
5346
|
cli.command("invoke [function]", "Fire a background run of a serverless function").usage(`invoke [function] [options]
|
|
5087
5347
|
|
|
5088
5348
|
Fire a Function run now through the authenticated API, without deploying anything
|