wawesome 0.17.0 → 0.18.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -974
- package/dist/index.mjs +103 -9
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -1,998 +1,36 @@
|
|
|
1
1
|
# wawesome
|
|
2
2
|
|
|
3
|
-
> Official CLI for bundling and deploying serverless
|
|
3
|
+
> Official CLI for bundling and deploying serverless functions on the
|
|
4
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
## Quick start
|
|
13
|
-
|
|
14
|
-
### 1. Create an account
|
|
15
|
-
|
|
16
|
-
Sign up for free at **[https://wawesome.io](https://wawesome.io)** to set up your workspace.
|
|
17
|
-
|
|
18
|
-
### 2. Authenticate the CLI
|
|
19
|
-
|
|
20
|
-
Log in through your browser:
|
|
9
|
+
You need Node 22.13 or newer. There is nothing to install, because `npx` fetches the CLI each time you
|
|
10
|
+
name it.
|
|
21
11
|
|
|
22
12
|
```bash
|
|
23
13
|
npx wawesome login
|
|
24
14
|
```
|
|
25
15
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
Create a new directory and scaffold a WebAssembly TypeScript starter function:
|
|
16
|
+
Your browser opens and signs you in. First time here, this makes the account too.
|
|
29
17
|
|
|
30
18
|
```bash
|
|
31
|
-
mkdir
|
|
19
|
+
mkdir hello && cd hello
|
|
32
20
|
npx wawesome init
|
|
33
21
|
```
|
|
34
22
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
name, so naming is usually a matter of pressing enter.
|
|
38
|
-
|
|
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, 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
|
-
|
|
44
|
-
#### Starting from a template
|
|
45
|
-
|
|
46
|
-
`npx wawesome templates` lists what is available; `--template` starts from one:
|
|
47
|
-
|
|
48
|
-
```bash
|
|
49
|
-
mkdir acme-store && cd acme-store
|
|
50
|
-
npx wawesome init --template stripe-webhook
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
This goes from nothing to a deployed Function in one command. The template is downloaded over
|
|
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,
|
|
63
|
-
and deploys. If a value does not exist yet, leave it blank; the CLI tells you the `env set` command
|
|
64
|
-
to run once it does.
|
|
65
|
-
|
|
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.
|
|
70
|
-
|
|
71
|
-
Before any of that is sent anywhere, the CLI shows you the **public URL your Function will have**
|
|
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.
|
|
77
|
-
|
|
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.
|
|
81
|
-
|
|
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.
|
|
84
|
-
|
|
85
|
-
### 4. Build and deploy
|
|
86
|
-
|
|
87
|
-
Deploy your serverless function to the platform:
|
|
23
|
+
Two questions, both defaulting to the directory name, then a project with `src/index.ts` in it.
|
|
24
|
+
Add `--template <name>` to start from one that already works.
|
|
88
25
|
|
|
89
26
|
```bash
|
|
90
27
|
npx wawesome deploy
|
|
91
28
|
```
|
|
92
29
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
## Command reference
|
|
96
|
-
|
|
97
|
-
| Command | Description |
|
|
98
|
-
|:----------------------------------------|:--------------------------------------------------------------------|
|
|
99
|
-
| `npx wawesome login` | Authenticate CLI with your Wawesome account via browser |
|
|
100
|
-
| `npx wawesome logout` | Log out and clear saved credentials from your machine |
|
|
101
|
-
| `npx wawesome whoami` | View current logged-in user, workspace, and gateway info |
|
|
102
|
-
| `npx wawesome templates` | Browse the template catalog (no login required) |
|
|
103
|
-
| `npx wawesome init` | Scaffold a new serverless function project in the current directory |
|
|
104
|
-
| `npx wawesome init --template <name>` | Scaffold from a catalog template, wire up what it needs, and deploy |
|
|
105
|
-
| `npx wawesome build` | Bundle TypeScript entry code into an optimized JS bundle |
|
|
106
|
-
| `npx wawesome deploy` | Build, upload, and promote a function version to production |
|
|
107
|
-
| `npx wawesome logs [func]` | List recent past invocations for a function |
|
|
108
|
-
| `npx wawesome logs --invocation <id>` | Fetch full stdout/stderr log body for a specific invocation |
|
|
109
|
-
| `npx wawesome logs <func> --follow` | Follow live output (waits for the next invocation if needed) |
|
|
110
|
-
| `npx wawesome invoke [func]` | Fire a function run immediately and follow its output live |
|
|
111
|
-
| `npx wawesome cron [func]` | List schedules for a function or app |
|
|
112
|
-
| `npx wawesome cron pause <name>` | Pause a schedule by name (survives future deploys) |
|
|
113
|
-
| `npx wawesome cron resume <name>` | Resume a paused schedule (no backfill) |
|
|
114
|
-
| `npx wawesome cron history [func]` | Read background run history for scheduled and manual runs |
|
|
115
|
-
| `npx wawesome version list` | List version history for the current function |
|
|
116
|
-
| `npx wawesome version switch <v>` | Roll back or promote a specific function version |
|
|
117
|
-
| `npx wawesome env list` | View environment variables for the current app |
|
|
118
|
-
| `npx wawesome env set <key> <val>` | Set an environment variable (add `--secret` for write-only) |
|
|
119
|
-
| `npx wawesome env rm <key>` | Delete an environment variable |
|
|
120
|
-
| `npx wawesome domains` | Show the app's domain, its state, and the DNS records to add |
|
|
121
|
-
| `npx wawesome domains adopt` | Write the domain the app answers at into wawesome-function.json |
|
|
122
|
-
| `npx wawesome domains detach <host>` | Stop serving the app at a domain you attached |
|
|
123
|
-
| `npx wawesome credentials` | List deploy credentials: name, prefix, capabilities, Apps, last use |
|
|
124
|
-
| `npx wawesome credentials mint <name>` | Mint a deploy credential and print its secret, once |
|
|
125
|
-
| `npx wawesome credentials regenerate <name>`| Replace a deploy credential's secret, keeping its name |
|
|
126
|
-
| `npx wawesome credentials revoke <name>`| Revoke a deploy credential by name or prefix |
|
|
127
|
-
| `npx wawesome credentials delete <name>`| Delete a revoked or expired credential, freeing its name |
|
|
128
|
-
|
|
129
|
-
---
|
|
130
|
-
|
|
131
|
-
## Unsupported globals
|
|
132
|
-
|
|
133
|
-
Every build scans the bundle it just produced against the platform's declared guest surface, and says
|
|
134
|
-
nothing unless it finds something.
|
|
135
|
-
|
|
136
|
-
- **`Intl` is not provided.** Where the bundle reaches it as it loads, in your own module scope or
|
|
137
|
-
a dependency's, the build is refused before a deploy uploads anything, because that bundle would
|
|
138
|
-
not evaluate on the platform. Where the reference sits inside a function that may never be called,
|
|
139
|
-
behind a `typeof` check, or inside a `try`, you get a warning and the deploy proceeds.
|
|
140
|
-
- **`toLocaleString`, `toLocaleDateString`, `toLocaleTimeString`, `toLocaleLowerCase` and
|
|
141
|
-
`toLocaleUpperCase` ignore their locale argument.** They run and return an unlocalised answer, so
|
|
142
|
-
these warn.
|
|
143
|
-
|
|
144
|
-
Each message names the global, the file and line in *your* source, and what to do about it. Declaring
|
|
145
|
-
an `Intl` polyfill in your `package.json`, the same declaration [local parity
|
|
146
|
-
reads](#testing-against-the-guests-javascript-surface), silences the report, as does installing one
|
|
147
|
-
on `globalThis` in the bundle itself. The scan reads static references only: a global reached through
|
|
148
|
-
`globalThis['Intl']` is invisible to it, so it never refuses a deploy on a guess. `deploy
|
|
149
|
-
--skip-build` scans the bundle it found on disk before uploading it.
|
|
150
|
-
|
|
151
|
-
## Invocation logs
|
|
152
|
-
|
|
153
|
-
Inspect past function runs or view raw `stdout` / `stderr` log outputs directly in your terminal.
|
|
154
|
-
|
|
155
|
-
### 1. List recent invocations
|
|
156
|
-
|
|
157
|
-
List past executions (including status, trigger type, timestamp, and duration) for the function in the
|
|
158
|
-
current directory:
|
|
159
|
-
|
|
160
|
-
```bash
|
|
161
|
-
npx wawesome logs
|
|
162
|
-
```
|
|
163
|
-
|
|
164
|
-
Or list invocations for a specific function by name:
|
|
165
|
-
|
|
166
|
-
```bash
|
|
167
|
-
npx wawesome logs my-function
|
|
168
|
-
```
|
|
169
|
-
|
|
170
|
-
Filter by invocation status, to show only errors or timeouts:
|
|
171
|
-
|
|
172
|
-
```bash
|
|
173
|
-
npx wawesome logs my-function --error
|
|
174
|
-
npx wawesome logs my-function --status timeout
|
|
175
|
-
npx wawesome logs my-function --running
|
|
176
|
-
```
|
|
177
|
-
|
|
178
|
-
### 2. View the invocation log body (`stdout`/`stderr`)
|
|
179
|
-
|
|
180
|
-
Fetch and print the captured `console.log` / `console.error` text for a specific invocation:
|
|
181
|
-
|
|
182
|
-
```bash
|
|
183
|
-
npx wawesome logs --invocation 019fb344-ea0c-78f2-8a9b-d04e188b9823
|
|
184
|
-
```
|
|
185
|
-
|
|
186
|
-
Or pass the UUID directly as the target:
|
|
187
|
-
|
|
188
|
-
```bash
|
|
189
|
-
npx wawesome logs 019fb344-ea0c-78f2-8a9b-d04e188b9823
|
|
190
|
-
```
|
|
191
|
-
|
|
192
|
-
### 3. Follow live output (`--follow`)
|
|
193
|
-
|
|
194
|
-
Stream an invocation's output as it runs, like `tail -f` for your serverless function.
|
|
195
|
-
|
|
196
|
-
#### Follow by function name (recommended)
|
|
197
|
-
|
|
198
|
-
```bash
|
|
199
|
-
npx wawesome logs my-function --follow
|
|
200
|
-
```
|
|
201
|
-
|
|
202
|
-
If the function is currently running, its output is streamed immediately. If the latest invocation
|
|
203
|
-
already finished, the CLI **waits for the next invocation** to start and then streams it live.
|
|
204
|
-
Press `Ctrl-C` at any time to stop.
|
|
205
|
-
|
|
206
|
-
#### Follow a specific invocation by ID
|
|
207
|
-
|
|
208
|
-
```bash
|
|
209
|
-
npx wawesome logs 019fb344-ea0c-78f2-8a9b-d04e188b9823 --follow
|
|
210
|
-
```
|
|
211
|
-
|
|
212
|
-
#### Reconnection
|
|
213
|
-
|
|
214
|
-
On a transient network error or a 5xx, the CLI reconnects with exponential back-off, up to three
|
|
215
|
-
retries. It does not retry an authentication failure (401) or an unknown invocation (404). Those
|
|
216
|
-
exit at once, naming what happened.
|
|
217
|
-
|
|
218
|
-
---
|
|
219
|
-
|
|
220
|
-
## Manual invocation
|
|
221
|
-
|
|
222
|
-
Fire a background run of a Function immediately without waiting for a schedule tick or deploying code:
|
|
223
|
-
|
|
224
|
-
```bash
|
|
225
|
-
# Invoke the function in the current directory and follow its output
|
|
226
|
-
npx wawesome invoke
|
|
227
|
-
|
|
228
|
-
# Invoke a specific function by name
|
|
229
|
-
npx wawesome invoke my-function
|
|
230
|
-
|
|
231
|
-
# Send custom HTTP method and request body
|
|
232
|
-
npx wawesome invoke -m POST -d '{"event":"audit"}'
|
|
233
|
-
|
|
234
|
-
# Fire without following live output
|
|
235
|
-
npx wawesome invoke --no-follow
|
|
236
|
-
```
|
|
237
|
-
|
|
238
|
-
---
|
|
239
|
-
|
|
240
|
-
## Schedules and cron management
|
|
241
|
-
|
|
242
|
-
Manage recurring Schedules and inspect background run history directly from your terminal.
|
|
243
|
-
|
|
244
|
-
### 1. List schedules
|
|
245
|
-
|
|
246
|
-
List a Function's Schedules with expression, state, and next run in UTC:
|
|
247
|
-
|
|
248
|
-
```bash
|
|
249
|
-
# List schedules for the function in the current directory
|
|
250
|
-
npx wawesome cron
|
|
251
|
-
|
|
252
|
-
# List schedules for a specific function
|
|
253
|
-
npx wawesome cron list my-function
|
|
254
|
-
|
|
255
|
-
# List schedules across all functions in an App
|
|
256
|
-
npx wawesome cron list --app my-app
|
|
257
|
-
```
|
|
258
|
-
|
|
259
|
-
The output tells the three off-states apart:
|
|
260
|
-
- `paused`: stopped by a user, resumable with `wawesome cron resume <name>`
|
|
261
|
-
- `not in this config file`: disabled because it was removed from configuration, resumable only by declaring it again in code
|
|
262
|
-
- `suspended`: suspended by the non-payment ladder, resumable only after settling workspace balance
|
|
263
|
-
|
|
264
|
-
### 2. Pause and resume schedules
|
|
265
|
-
|
|
266
|
-
```bash
|
|
267
|
-
# Pause a schedule by name (stops queued ticks and survives future deploys)
|
|
268
|
-
npx wawesome cron pause nightly-reconcile
|
|
269
|
-
|
|
270
|
-
# Pause with an optional reason for incident context
|
|
271
|
-
npx wawesome cron pause nightly-reconcile --reason "database maintenance"
|
|
272
|
-
|
|
273
|
-
# Resume a paused schedule (recomputes next run from now, no catch-up backfilling)
|
|
274
|
-
npx wawesome cron resume nightly-reconcile
|
|
275
|
-
```
|
|
276
|
-
|
|
277
|
-
### 3. Read run history
|
|
278
|
-
|
|
279
|
-
Inspect past scheduled and manual runs, showing when each run was due, when it started, pool delay, and how it ended:
|
|
280
|
-
|
|
281
|
-
```bash
|
|
282
|
-
# View run history for the current function
|
|
283
|
-
npx wawesome cron history
|
|
284
|
-
|
|
285
|
-
# Filter run history by state (pending, running, dispatched, skipped, missed, cancelled, lost, failed)
|
|
286
|
-
npx wawesome cron history my-function --state failed
|
|
287
|
-
```
|
|
288
|
-
|
|
289
|
-
---
|
|
290
|
-
|
|
291
|
-
## Deploy credentials
|
|
292
|
-
|
|
293
|
-
A deploy credential is what a CI pipeline or an agent authenticates with, where there is nobody at a
|
|
294
|
-
keyboard to log in. It belongs to the workspace rather than to you: it keeps working after you leave,
|
|
295
|
-
and revoking it does not end your own session. Once you have one, [Deploying from CI](#deploying-from-ci)
|
|
296
|
-
is what to do with it.
|
|
297
|
-
|
|
298
|
-
Minting, listing, regenerating, revoking and deleting belong to the workspace owner. They are closed
|
|
299
|
-
to deploy credentials themselves, whatever those carry, so a leaked credential cannot mint another or
|
|
300
|
-
renew its own expiry. All five run on your ordinary `npx wawesome login` session.
|
|
301
|
-
|
|
302
|
-
### Mint one
|
|
303
|
-
|
|
304
|
-
```bash
|
|
305
|
-
npx wawesome credentials mint ci-pipeline
|
|
306
|
-
```
|
|
307
|
-
|
|
308
|
-
The secret is printed once and never again. The platform stores a digest of it, so no later read
|
|
309
|
-
can rebuild it. It goes to stdout on a line of its own and everything else the command prints goes to
|
|
310
|
-
stderr, so a redirect catches the secret and not one character besides:
|
|
311
|
-
|
|
312
|
-
```bash
|
|
313
|
-
npx wawesome credentials mint ci-pipeline > secret.txt
|
|
314
|
-
```
|
|
315
|
-
|
|
316
|
-
A capability is one cell of a grid: a verb, `read` or `write`, and the resource it acts on. A
|
|
317
|
-
credential carries the words a deploy needs, reaches every App in the workspace, and expires in
|
|
318
|
-
ninety days unless you say otherwise:
|
|
319
|
-
|
|
320
|
-
```bash
|
|
321
|
-
# reads what ran and fires a run of what somebody else shipped, on one App, never expires
|
|
322
|
-
npx wawesome credentials mint runner -c read:apps,read:functions,read:invocations,write:runs -a prod --expires never
|
|
323
|
-
|
|
324
|
-
# ships code into an App it may create, and attaches the domain the project declares
|
|
325
|
-
npx wawesome credentials mint ci -c write:apps,write:functions,write:env,read:tenant,write:domains -a prod
|
|
326
|
-
```
|
|
327
|
-
|
|
328
|
-
| Flag | Meaning |
|
|
329
|
-
|:---------------------|:------------------------------------------------------------------------------|
|
|
330
|
-
| `-c, --capability` | A `{read\|write}:{resource}` word — `read:apps`, `write:functions`, `write:env`, `write:runs` and the rest. Repeatable or comma-separated. Default: `write:apps`, `write:functions`, `write:env`, `read:domains`, `read:tenant` |
|
|
331
|
-
| `--preset` | The words one job needs, by the job's name: `viewer`, `deployer`, `member`. [Which credential for which job](#which-credential-for-which-job) is what each one grants. Not with `-c` |
|
|
332
|
-
| `-a, --app` | Restrict to these Apps, by slug. Repeatable. Default: every App in the workspace |
|
|
333
|
-
| `--expires` | Days, or `never`. Default: 90 |
|
|
334
|
-
|
|
335
|
-
The resources are `tenant`, `apps`, `functions`, `env`, `domains`, `invocations`, `runs`,
|
|
336
|
-
`schedules`, `previews` and `egress`. A write reaches the read of the same resource, so
|
|
337
|
-
`write:functions` carries `read:functions` and there is no reason to name both. The command prints
|
|
338
|
-
the words the credential ended up with.
|
|
339
|
-
|
|
340
|
-
Some things are a person's alone, whatever a credential carries: renaming the workspace, detaching a
|
|
341
|
-
domain, widening the egress allowlist, billing, and this workspace's own credentials and members.
|
|
342
|
-
Those are not words to mint — a credential asking for one is told a person has to be at the keyboard.
|
|
343
|
-
|
|
344
|
-
An App named in a restriction does not have to exist yet. A pipeline whose first deploy creates the
|
|
345
|
-
App it was minted for is the ordinary case. A restriction bounds what a credential reads as well as
|
|
346
|
-
what it disturbs, and `write:functions` is the hole in it: a Function deployed into an App the
|
|
347
|
-
credential does reach still reads the whole workspace's environment at runtime.
|
|
348
|
-
|
|
349
|
-
### List them
|
|
350
|
-
|
|
351
|
-
```bash
|
|
352
|
-
npx wawesome credentials
|
|
353
|
-
```
|
|
354
|
-
|
|
355
|
-
Name, displayable prefix, capabilities, Apps, last use and expiry. Never the secret, which the
|
|
356
|
-
platform no longer holds. `LAST USED` lags by a few minutes and reads `never` for a credential
|
|
357
|
-
nothing has ever presented, which is a better reason to revoke one than any calendar date.
|
|
358
|
-
|
|
359
|
-
### Rotate one
|
|
360
|
-
|
|
361
|
-
```bash
|
|
362
|
-
npx wawesome credentials regenerate ci-pipeline
|
|
363
|
-
npx wawesome credentials regenerate ci --expires never --yes
|
|
364
|
-
npx wawesome credentials regenerate ci-pipeline > secret.txt
|
|
365
|
-
```
|
|
366
|
-
|
|
367
|
-
Regenerating replaces the secret where it stands. The credential keeps its id, its name, its
|
|
368
|
-
capabilities and the Apps it may reach, so the only thing to change in your CI settings is the secret
|
|
369
|
-
itself. Its displayable prefix changes with it, since the prefix is what you match one against. The
|
|
370
|
-
new secret is printed once, on stdout alone, exactly as at minting.
|
|
371
|
-
|
|
372
|
-
It asks first, because anything holding the old secret stops working the moment this lands and has to
|
|
373
|
-
be given the new one; `--yes` is the answer where nobody is at the keyboard. Without `--expires` the
|
|
374
|
-
new secret lives ninety days, the same rule minting follows, including on a credential that never
|
|
375
|
-
expired.
|
|
376
|
-
|
|
377
|
-
An expired credential regenerates into a working one — that is the ordinary reason to reach for this.
|
|
378
|
-
A revoked one is refused, because nothing un-revokes a credential.
|
|
379
|
-
|
|
380
|
-
### Revoke one
|
|
381
|
-
|
|
382
|
-
```bash
|
|
383
|
-
npx wawesome credentials revoke ci-pipeline
|
|
384
|
-
npx wawesome credentials revoke wawe_ab3k9x --yes
|
|
385
|
-
```
|
|
386
|
-
|
|
387
|
-
By name or by displayable prefix, whole or partial. It asks before it does it, `--yes` is the answer
|
|
388
|
-
where nobody is at the keyboard, and a prefix naming more than one credential is refused rather than
|
|
389
|
-
guessed at. A revocation lands on the very next request presenting the secret, and nothing un-revokes
|
|
390
|
-
it.
|
|
391
|
-
|
|
392
|
-
### Delete one
|
|
393
|
-
|
|
394
|
-
```bash
|
|
395
|
-
npx wawesome credentials delete ci-pipeline
|
|
396
|
-
npx wawesome credentials delete wawe_ab3k9x --yes
|
|
397
|
-
```
|
|
398
|
-
|
|
399
|
-
Deleting is a second act, not a shortcut through revoking, so only a credential that has already
|
|
400
|
-
stopped working can be deleted. Revoke a live one first. It resolves its target the way revoking
|
|
401
|
-
does, by name or by displayable prefix, and asks before it does it unless you pass `--yes`.
|
|
402
|
-
|
|
403
|
-
What deleting gives up is the reason revoking left the record behind. The credential leaves the
|
|
404
|
-
listing, the name comes free to mint again, and a pipeline still presenting the secret is told the
|
|
405
|
-
secret is unknown rather than that somebody revoked it. Whoever is debugging that pipeline goes
|
|
406
|
-
looking in their CI settings instead of your dashboard.
|
|
407
|
-
|
|
408
|
-
---
|
|
409
|
-
|
|
410
|
-
## Your agent, over MCP
|
|
411
|
-
|
|
412
|
-
The same deploy credential connects a coding agent to your workspace, over MCP. Nothing to install and
|
|
413
|
-
no local process to keep running: it is an endpoint on the gateway, `https://api.wawesome.io/v1/mcp`,
|
|
414
|
-
and the credential is the whole of the authentication.
|
|
415
|
-
|
|
416
|
-
### Install it
|
|
417
|
-
|
|
418
|
-
In Claude Code, one command:
|
|
419
|
-
|
|
420
|
-
```bash
|
|
421
|
-
claude mcp add --transport http wawesome https://api.wawesome.io/v1/mcp \
|
|
422
|
-
--header "Authorization: Bearer wawe_..."
|
|
423
|
-
```
|
|
424
|
-
|
|
425
|
-
In Cursor, `.cursor/mcp.json` in the project, or `~/.cursor/mcp.json` for every project:
|
|
426
|
-
|
|
427
|
-
```json
|
|
428
|
-
{
|
|
429
|
-
"mcpServers": {
|
|
430
|
-
"wawesome": {
|
|
431
|
-
"url": "https://api.wawesome.io/v1/mcp",
|
|
432
|
-
"headers": { "Authorization": "Bearer wawe_..." }
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
```
|
|
437
|
-
|
|
438
|
-
In VS Code, `.vscode/mcp.json` in the workspace, or the file **MCP: Open User Configuration** opens.
|
|
439
|
-
The shape is the same address and header map, under the key `servers` rather than `mcpServers`:
|
|
440
|
-
|
|
441
|
-
```json
|
|
442
|
-
{
|
|
443
|
-
"servers": {
|
|
444
|
-
"wawesome": {
|
|
445
|
-
"type": "http",
|
|
446
|
-
"url": "https://api.wawesome.io/v1/mcp",
|
|
447
|
-
"headers": { "Authorization": "Bearer wawe_..." }
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
```
|
|
452
|
-
|
|
453
|
-
A file inside the project is a file your repository carries, and the secret with it. VS Code takes an
|
|
454
|
-
`inputs` entry and a `${input:...}` placeholder in the header, so it asks once and holds the value
|
|
455
|
-
itself. In Cursor, put the credential in `~/.cursor/mcp.json`, which is outside every repository.
|
|
456
|
-
|
|
457
|
-
### Where a bearer credential reaches
|
|
458
|
-
|
|
459
|
-
It works in Claude Code, the Claude Agent SDK, Cursor, VS Code, Windsurf, Zed and the Gemini CLI. It
|
|
460
|
-
works in every agent framework that takes a URL and a header map, among them the OpenAI Agents SDK,
|
|
461
|
-
LangChain, the Vercel AI SDK and Pydantic AI. Anything that speaks streamable HTTP MCP and lets you
|
|
462
|
-
set a header can connect.
|
|
463
|
-
|
|
464
|
-
Whichever protocol revision your client speaks, the endpoint answers it. It holds both eras at one
|
|
465
|
-
address: the current revision, `2026-07-28`, and the three handshake-based ones the installed base is
|
|
466
|
-
still on. A client ahead of your editor's next release connects, and so does one behind it.
|
|
467
|
-
|
|
468
|
-
It does **not** work in the connector dialog in claude.ai or Claude Desktop, or in ChatGPT
|
|
469
|
-
connectors. Those three accept "no authentication" or OAuth and have no field for a token, so there
|
|
470
|
-
is nowhere to put `wawe_...`. Reaching them needs an OAuth authorization server, which the platform
|
|
471
|
-
does not run today, and nothing you can configure works around that.
|
|
472
|
-
|
|
473
|
-
### What your agent gets
|
|
474
|
-
|
|
475
|
-
Every tool is listed for every credential, and each one names the capability it needs. A credential
|
|
476
|
-
that does not carry that word is refused by name, so your agent tells you what to mint rather than
|
|
477
|
-
concluding the platform cannot do it.
|
|
478
|
-
|
|
479
|
-
| Tool | What it does | Needs |
|
|
480
|
-
|:---------------------------|:------------------------------------------------------------------------|:--------------|
|
|
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 |
|
|
482
|
-
| `list_templates` | The template catalogue, with what each one is for | nothing |
|
|
483
|
-
| `get_template` | One template's source, environment contract and outbound providers; its tests and build configuration are named, and read on request | nothing |
|
|
484
|
-
| `list_apps` | The Apps the credential reaches, each with its address and its dashboard page | `read:apps` |
|
|
485
|
-
| `list_functions` | The Functions in one App, and what their live version carries | `read:functions` |
|
|
486
|
-
| `list_versions` | A Function's Versions, and which one is live | `read:functions` |
|
|
487
|
-
| `get_function_source` | The code a Function is running, and the static files it carries | `read:functions` |
|
|
488
|
-
| `get_usage` | The Tenant's usage and headroom | `read:tenant` |
|
|
489
|
-
| `list_invocations` | Recent Invocations of a Function, failures included | `read:invocations` |
|
|
490
|
-
| `get_invocation` | One Invocation by id | `read:invocations` |
|
|
491
|
-
| `read_invocation_logs` | An Invocation's log body, a page at a time | `read:invocations` |
|
|
492
|
-
| `diagnose_latest_failure` | The latest failure, its logs and the Version that ran it, in one call | `read:invocations`, `read:functions` |
|
|
493
|
-
| `deploy_function` | Deploys code and the static files it carries | `write:functions` |
|
|
494
|
-
| `deploy_status` | How a deploy went, by the handle it answered with | `read:functions` |
|
|
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` |
|
|
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` |
|
|
502
|
-
| `invoke_function` | Runs a deployed Function once and answers with the Invocation id | `write:runs` |
|
|
503
|
-
| `fetch_function` | Fetches one path from a Function's live Version and answers what it served | `write:runs` |
|
|
504
|
-
|
|
505
|
-
A word that writes reaches the read of the same thing, so `write:functions` needs no `read:functions`
|
|
506
|
-
beside it, and the presets below are named in exactly these words.
|
|
507
|
-
|
|
508
|
-
`whoami` is the one to call first. It answers the workspace, every word the credential carries and
|
|
509
|
-
the Apps it reaches, and it asks for nothing — so your agent can say what it is able to do before it
|
|
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.
|
|
515
|
-
|
|
516
|
-
A deploy is two calls. The first says which of your files the platform does not already hold, by
|
|
517
|
-
content hash; your agent uploads those bytes to `PUT /v1/assets/{content_hash}` with the same
|
|
518
|
-
credential and asks again, so a redeploy that changed only its code uploads nothing. The second
|
|
519
|
-
answers with a handle rather than waiting for the build, because a dropped connection would otherwise
|
|
520
|
-
cancel a deploy that was going fine. `deploy_status` reads the handle whenever your agent asks.
|
|
521
|
-
|
|
522
|
-
When the deploy is out and something is wrong, `diagnose_latest_failure` answers the latest failed
|
|
523
|
-
run of a Function, that run's log output, and the version that ran it, in one call. Mirroring the
|
|
524
|
-
routes there costs four round trips and four chances to carry the wrong identifier between them.
|
|
525
|
-
|
|
526
|
-
Log output comes back a page at a time, with a cursor for the next one, and nothing is held open.
|
|
527
|
-
Your own `wawesome logs --follow` keeps running while your agent reads. One live tail per credential
|
|
528
|
-
is all the platform permits, and an agent taking it would end yours.
|
|
529
|
-
|
|
530
|
-
### Which credential for which job
|
|
531
|
-
|
|
532
|
-
A credential carries the words its job needs and stops there. They are the same words
|
|
533
|
-
[Deploy credentials](#mint-one) mints with, and `--preset` fills them in for you: it expands into the
|
|
534
|
-
words below before the request is built, and the command prints the words the credential ended up
|
|
535
|
-
with. A preset and `-c` together are refused rather than merged, since both name the same thing. The
|
|
536
|
-
preset is the name of the job; the words are the grant, and they are what travels.
|
|
537
|
-
|
|
538
|
-
A preset is a workspace role, and the ones offered here are the roles carrying nothing only a person
|
|
539
|
-
can hold. The name a job is minted under and the standing a member is given are one thing, rather
|
|
540
|
-
than two lists somebody has to keep level.
|
|
541
|
-
|
|
542
|
-
**Read and investigate, without the ability to ship** — `viewer`, which is every `read:` word the
|
|
543
|
-
platform has. The agent enumerates your Apps, reads Functions, Versions, Invocations and log bodies,
|
|
544
|
-
diagnoses a failure, checks usage, and browses templates. Every tool that ships code or runs it
|
|
545
|
-
refuses:
|
|
546
|
-
|
|
547
|
-
```bash
|
|
548
|
-
npx wawesome credentials mint reader --preset viewer -a prod
|
|
549
|
-
```
|
|
550
|
-
|
|
551
|
-
**Ship and read back** — `deployer`, which is every read plus `write:apps`, `write:functions`,
|
|
552
|
-
`write:env` and `write:runs`. Deploy, run, diagnose, roll back, set a key. Every tool in the table:
|
|
553
|
-
|
|
554
|
-
```bash
|
|
555
|
-
npx wawesome credentials mint ci --preset deployer -a prod
|
|
556
|
-
```
|
|
557
|
-
|
|
558
|
-
**The whole workspace** — `member`, which is the deployer's words plus `write:schedules` and
|
|
559
|
-
`write:previews`, for an agent that pauses a schedule or cuts a preview as well:
|
|
560
|
-
|
|
561
|
-
```bash
|
|
562
|
-
npx wawesome credentials mint agent --preset member -a prod
|
|
563
|
-
```
|
|
564
|
-
|
|
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
|
|
568
|
-
an agent billing, a plan change, credential management or the ability to rename your workspace: those
|
|
569
|
-
are closed to every credential, whatever it carries. Reading the workspace's name and address is not
|
|
570
|
-
among them — `whoami` answers both to any credential, which is how the agent knows where what it
|
|
571
|
-
deploys will appear.
|
|
572
|
-
|
|
573
|
-
Keep `-a` on all three. A credential restricted to one App is refused on every tool that names
|
|
574
|
-
another, which is what stops an agent working on one client's project from touching another's.
|
|
575
|
-
`get_invocation` and `read_invocation_logs` take an Invocation id rather than an App, and an id
|
|
576
|
-
belonging to an App the credential cannot reach is answered as an id naming nothing. `list_apps`
|
|
577
|
-
answers a set rather than one App, so it is narrowed instead of refused: the credential is listed the
|
|
578
|
-
Apps it reaches and told nothing about the rest.
|
|
579
|
-
|
|
580
|
-
### When a tool refuses
|
|
581
|
-
|
|
582
|
-
A refusal is the tool's answer, not a protocol error, and it carries the same `reason` code the CLI
|
|
583
|
-
prints:
|
|
584
|
-
|
|
585
|
-
- `credential-missing-capability`. The credential does not carry a word this tool needs. The refusal
|
|
586
|
-
names the first in `missing_capability` and every one of them in `missing_capabilities`, which are
|
|
587
|
-
the words to mint a credential with. This one answers `403` rather than `200`, with a
|
|
588
|
-
`WWW-Authenticate: Bearer error="insufficient_scope"` challenge naming the same words and where
|
|
589
|
-
this endpoint's OAuth metadata is — a connector installed through consent reads it and asks its
|
|
590
|
-
human to widen the grant rather than to install again.
|
|
591
|
-
- `app-outside-credential-scope`. The credential is restricted to named Apps and this is not one of
|
|
592
|
-
them. A wider capability changes nothing here.
|
|
593
|
-
|
|
594
|
-
A `401` is the credential itself: revoked, expired, or one this platform never issued. Nothing else
|
|
595
|
-
answers one — a workspace that cannot deploy for a lapsed plan or a restriction is refused under its
|
|
596
|
-
own reason, and re-installing the connector would not fix either.
|
|
597
|
-
|
|
598
|
-
A quota refusal carries its numbers under `allowance` beside the prose, so your agent reports
|
|
599
|
-
"3 of 3 App slots" rather than handing you a sentence to read.
|
|
600
|
-
|
|
601
|
-
---
|
|
602
|
-
|
|
603
|
-
## Configuration and a custom gateway
|
|
604
|
-
|
|
605
|
-
### `wawesome-function.json`
|
|
606
|
-
|
|
607
|
-
Every project directory includes a `wawesome-function.json` file generated during `npx wawesome init`:
|
|
608
|
-
|
|
609
|
-
```json
|
|
610
|
-
{
|
|
611
|
-
"app": "my-app",
|
|
612
|
-
"function": "hello-world",
|
|
613
|
-
"entry": "src/index.ts"
|
|
614
|
-
}
|
|
615
|
-
```
|
|
616
|
-
|
|
617
|
-
`app` is the App this Function is deployed into, and it is client-facing. Every deploy from this
|
|
618
|
-
directory is scoped to it.
|
|
619
|
-
|
|
620
|
-
`function` is the address. Changing it does not rename anything: your Function's URL is built from
|
|
621
|
-
its name, so the next deploy lands on a Function of its own and the old one stays live at the old
|
|
622
|
-
URL, serving the code its callers already hold. The CLI remembers where this directory last
|
|
623
|
-
deployed and asks before that happens, naming both URLs. If you meant it, delete the old Function
|
|
624
|
-
from the dashboard once nothing calls it.
|
|
625
|
-
|
|
626
|
-
### A site with no handler
|
|
627
|
-
|
|
628
|
-
Leave `entry` out, write no `src/index.ts`, and point `"assets"` at the directory your build wrote
|
|
629
|
-
its pages into:
|
|
630
|
-
|
|
631
|
-
```json
|
|
632
|
-
{
|
|
633
|
-
"app": "my-app",
|
|
634
|
-
"function": "root",
|
|
635
|
-
"assets": "dist"
|
|
636
|
-
}
|
|
637
|
-
```
|
|
638
|
-
|
|
639
|
-
That deploys the pages and no code at all, and the site is live at your App's address. Nothing in the
|
|
640
|
-
file declares a kind: a project with a handler is one that has an entry point, and a project with
|
|
641
|
-
both deploys them together and rolls them back together.
|
|
642
|
-
|
|
643
|
-
An `entry` you did name and that is not there is a hard error, so a typo never quietly becomes a site
|
|
644
|
-
with nothing running behind it. Every deploy says which of the two it landed.
|
|
645
|
-
|
|
646
|
-
One thing the deploy will warn you about: everything beneath a Function's address reaches that
|
|
647
|
-
Function, so a page your root Function carries at `about/index.html` is shadowed by a sibling Function
|
|
648
|
-
named `about`. The deploy says so and names both addresses. It refuses nothing — the two Functions are
|
|
649
|
-
versioned independently, and either one may deploy first.
|
|
650
|
-
|
|
651
|
-
### Static files
|
|
652
|
-
|
|
653
|
-
Add `"assets"` to deploy static files beside your code:
|
|
654
|
-
|
|
655
|
-
```json
|
|
656
|
-
{
|
|
657
|
-
"app": "my-app",
|
|
658
|
-
"function": "hello-world",
|
|
659
|
-
"entry": "src/index.ts",
|
|
660
|
-
"assets": "dist/client"
|
|
661
|
-
}
|
|
662
|
-
```
|
|
663
|
-
|
|
664
|
-
Everything under that directory is deployed with the version and served at its path beneath your
|
|
665
|
-
Function's URL, so `dist/client/assets/index-a1.js` answers at `https://<app-host>/<function>/assets/index-a1.js`.
|
|
666
|
-
The CLI hashes each file and asks the platform which of them it does not already hold, so a redeploy
|
|
667
|
-
that changed one chunk uploads one chunk. A deploy that changed nothing at all is refused before a
|
|
668
|
-
byte moves.
|
|
669
|
-
|
|
670
|
-
Files are served straight from object storage; your Function is never invoked for one, and no
|
|
671
|
-
invocation is recorded. They answer on your App's own hostname and nowhere else. On the
|
|
672
|
-
development path form (`/x/<tenant>/<app>/<function>/...`) the same address reaches your handler
|
|
673
|
-
as it always has, because a file on an origin every workspace shares would be same-origin with
|
|
674
|
-
all of them. A file beneath `assets/` carries `Cache-Control: public, max-age=31536000, immutable`,
|
|
675
|
-
so name your build output by content hash: its bytes must never change under a name a browser has
|
|
676
|
-
already cached for a year. A file outside `assets/` keeps one name across every deploy, so it carries
|
|
677
|
-
`max-age=300` instead — five minutes is inside the reach of a deploy and of a rollback, which a year
|
|
678
|
-
is not. The content type comes from the extension against a fixed allowlist and is never sniffed;
|
|
679
|
-
anything off it is served as a download.
|
|
680
|
-
|
|
681
|
-
Two rules to know about:
|
|
682
|
-
|
|
683
|
-
- **Everything beneath `assets/` is static**, whatever the deploy carries. A request there never
|
|
684
|
-
reaches your handler, and an unknown path under it is a 404 rather than a route for you to answer.
|
|
685
|
-
- **A file outside `assets/` answers at its own name**, however many of them a deploy carries.
|
|
686
|
-
`favicon.ico`, `robots.txt` and a `.well-known/` directory are what that is for; hashed build
|
|
687
|
-
output belongs under `assets/`.
|
|
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
|
-
|
|
701
|
-
**A page is a file like any other.** An `index.html` your build wrote is deployed, hashed, retained
|
|
702
|
-
and billed exactly as your other files are, and it is what a directory-style address resolves to: a
|
|
703
|
-
path whose last segment carries no extension is served the `index.html` beneath it, so `/`, `/about`,
|
|
704
|
-
`/about/` and `/blog/hello` each answer with a page. A page revalidates rather than being pinned for a
|
|
705
|
-
year, wherever it sits, so deploying and refreshing is a loop that works.
|
|
706
|
-
|
|
707
|
-
**An SVG is served inert.** Nothing you deploy as a resource should be able to run script on your
|
|
708
|
-
App's own origin, and an SVG opened directly in a browser can: it runs script, and it renders whatever
|
|
709
|
-
HTML a `<foreignObject>` holds. It is served rather than refused because making it inert costs the
|
|
710
|
-
file nothing. An `<img src="logo.svg">` never ran that script and is never checked against the
|
|
711
|
-
policy, so your drawings render as they always did. Every
|
|
712
|
-
SVG and XML file carries `Content-Security-Policy: script-src 'none'; sandbox`, and only navigating
|
|
713
|
-
straight to one loses anything. Such a file is sandboxed onto an origin of its own, so its links no
|
|
714
|
-
longer navigate and a page embedding it through `<object>` or `<iframe>` cannot reach into its DOM.
|
|
715
|
-
|
|
716
|
-
### Schedules
|
|
717
|
-
|
|
718
|
-
Add `"schedules"` to run a Function on a recurring timer, with no caller:
|
|
719
|
-
|
|
720
|
-
```json
|
|
721
|
-
{
|
|
722
|
-
"app": "my-app",
|
|
723
|
-
"function": "nightly-reconcile",
|
|
724
|
-
"entry": "src/index.ts",
|
|
725
|
-
"schedules": [{ "name": "overnight", "expression": "0 3 * * *" }]
|
|
726
|
-
}
|
|
727
|
-
```
|
|
728
|
-
|
|
729
|
-
An expression is **five fields, read in UTC**: minute, hour, day of month, month, day of week. There
|
|
730
|
-
is no seconds field and no timezone. A local zone would make one night a year fire a job twice and
|
|
731
|
-
another night not at all.
|
|
732
|
-
|
|
733
|
-
The name is yours to choose and is what the platform keys the schedule by, so editing an expression
|
|
734
|
-
is a change to the same schedule rather than the deletion of one and the creation of another. Its
|
|
735
|
-
history and its paused state stay attached to it.
|
|
736
|
-
|
|
737
|
-
Your deploy applies them and prints each one with the time it will next run. It is refused, before
|
|
738
|
-
anything is built or uploaded, if an expression cannot be read, if it would run more often than every
|
|
739
|
-
five minutes, or if one Function declares more than five schedules.
|
|
740
|
-
|
|
741
|
-
Two rules worth knowing before you edit the file:
|
|
742
|
-
|
|
743
|
-
- **A schedule you delete from the file is disabled, not deleted.** Its history stays, and declaring
|
|
744
|
-
it again is what turns it back on, so a typo costs you a deploy rather than a job's record.
|
|
745
|
-
- **A deploy never resumes a schedule a person paused.** When it runs is code; whether it is running
|
|
746
|
-
is not, and an unrelated commit the next morning must not restart what you stopped at 3am.
|
|
747
|
-
|
|
748
|
-
Leaving `schedules` out of the file entirely says nothing about them and changes nothing. Writing
|
|
749
|
-
`"schedules": []` says this Function declares none, which disables the ones it used to have.
|
|
750
|
-
|
|
751
|
-
### Keeping a Function off the web
|
|
752
|
-
|
|
753
|
-
A job on a timer should not also be sitting at a guessable URL where a stranger can fire it. Add
|
|
754
|
-
`"visibility"` to make a Function unreachable from the internet:
|
|
755
|
-
|
|
756
|
-
```json
|
|
757
|
-
{
|
|
758
|
-
"app": "my-app",
|
|
759
|
-
"function": "nightly-reconcile",
|
|
760
|
-
"entry": "src/index.ts",
|
|
761
|
-
"visibility": "private"
|
|
762
|
-
}
|
|
763
|
-
```
|
|
764
|
-
|
|
765
|
-
A private Function has **no public address at all** in production, not a hidden one and not one
|
|
766
|
-
behind a credential. A request for it against your App's own hostname is answered with the same 404
|
|
767
|
-
as a Function that was never deployed. This is how you deploy a job with side effects without leaving
|
|
768
|
-
it where a stranger who guesses the slug can fire it.
|
|
769
|
-
|
|
770
|
-
Leave the line out and your Function is public, which is what every Function without it has always
|
|
771
|
-
been.
|
|
772
|
-
|
|
773
|
-
On your own machine, the local development surface serves Functions regardless of visibility, and
|
|
774
|
-
honours an explicit `x-wawesome-trigger` header so you can exercise a scheduled run by hand:
|
|
775
|
-
|
|
776
|
-
```bash
|
|
777
|
-
# Exercise a private or scheduled function locally on the development path form:
|
|
778
|
-
curl -X POST http://localhost:3000/x/my-tenant-slug/default-app/nightly-reconcile \
|
|
779
|
-
-H "x-wawesome-trigger: schedule"
|
|
780
|
-
```
|
|
781
|
-
|
|
782
|
-
Production strips the reserved header namespace inbound, so the same header against your App's own
|
|
783
|
-
hostname reaches nothing that reads it: the run is a `caller`'s, and a private Function is a 404
|
|
784
|
-
either way. Skipping your own authorization for a `schedule` run therefore opens nothing.
|
|
785
|
-
|
|
786
|
-
Making a Function private takes nothing but the deploy. Making it public again does not: deleting
|
|
787
|
-
the line is refused, and the deploy tells you so having written nothing.
|
|
788
|
-
|
|
789
|
-
```bash
|
|
790
|
-
npx wawesome deploy --publish
|
|
791
|
-
```
|
|
792
|
-
|
|
793
|
-
That is deliberate. The line that keeps a job off the internet is one line, and a deploy that
|
|
794
|
-
quietly honoured its deletion would put the job back on the open internet with nothing said. Every
|
|
795
|
-
deploy prints the visibility it landed, beside the URL or in place of it.
|
|
796
|
-
|
|
797
|
-
### A domain of your own
|
|
798
|
-
|
|
799
|
-
Add `"domain"` and your App answers at a name you own, beside the address it already has:
|
|
800
|
-
|
|
801
|
-
```json
|
|
802
|
-
{
|
|
803
|
-
"app": "my-app",
|
|
804
|
-
"function": "api",
|
|
805
|
-
"entry": "src/index.ts",
|
|
806
|
-
"domain": "shop.client.com"
|
|
807
|
-
}
|
|
808
|
-
```
|
|
809
|
-
|
|
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.
|
|
820
|
-
|
|
821
|
-
Your deploy waits for neither. It attaches the domain, prints the records and finishes, and every
|
|
822
|
-
later deploy states where the domain got to: not verified, ownership verified, certificate issued, or
|
|
823
|
-
serving. A domain that is already serving is a line saying so and nothing else.
|
|
824
|
-
|
|
825
|
-
The same block is there when you are not deploying:
|
|
826
|
-
|
|
827
|
-
```bash
|
|
828
|
-
npx wawesome domains
|
|
829
|
-
```
|
|
830
|
-
|
|
831
|
-
It prints the domain, where it got to, when the platform last checked, and the records to add. It
|
|
832
|
-
changes nothing, and a deploy credential carrying `read:domains` can run it, so a pipeline reads the
|
|
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.
|
|
837
|
-
|
|
838
|
-
A custom domain is granted from the Solo plan upwards, one per App. On a plan that grants none, the
|
|
839
|
-
deploy prints the refusal and lands everything else it was doing.
|
|
840
|
-
|
|
841
|
-
The address derived from your workspace slug keeps serving after you attach a domain. Both names
|
|
842
|
-
answer, so webhooks and integrations already pointed at the old one keep working.
|
|
843
|
-
|
|
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.
|
|
861
|
-
|
|
862
|
-
Deleting the line detaches nothing. Detaching takes a live site dark, which is not something a
|
|
863
|
-
deploy should infer from a deleted line. So the deploy reports the domain it found and left serving,
|
|
864
|
-
names the command that writes it into the file, and names the one command that takes it off:
|
|
865
|
-
|
|
866
|
-
```bash
|
|
867
|
-
npx wawesome domains detach shop.client.com
|
|
868
|
-
```
|
|
869
|
-
|
|
870
|
-
The hostname is typed because an App carries exactly one domain, and the command that removes it
|
|
871
|
-
should not be one you can run by reflex. It says what it is about to do first: the domain stops
|
|
872
|
-
resolving here, this App's own address keeps serving, and the certificate is given back. Afterwards
|
|
873
|
-
the name is free to attach again by declaring it and deploying, which is also what the next deploy
|
|
874
|
-
does if you leave the line in the file.
|
|
875
|
-
|
|
876
|
-
### Reserved headers
|
|
877
|
-
|
|
878
|
-
`x-wawesome-*` belongs to the platform in both directions. It is stripped off the request before your
|
|
879
|
-
handler sees it, and off your response before the caller does. So **do not name a header of your
|
|
880
|
-
own on that prefix**. It is dropped silently rather than rejected, and you will not get an error
|
|
881
|
-
telling you why it vanished.
|
|
882
|
-
|
|
883
|
-
Five headers arrive or leave on it, and the stripping is what makes them worth trusting:
|
|
884
|
-
|
|
885
|
-
| Header | Direction | What it means |
|
|
886
|
-
| --- | --- | --- |
|
|
887
|
-
| `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. |
|
|
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. |
|
|
889
|
-
| `x-wawesome-invocation-id` | outbound | The id of this run, the key to fetch its logs with `npx wawesome logs --invocation <id>`. |
|
|
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. |
|
|
892
|
-
|
|
893
|
-
### Testing against the guest's JavaScript surface
|
|
894
|
-
|
|
895
|
-
Your Function does not run on Node. The engine has no `Intl`, and its `toLocaleString` ignores the
|
|
896
|
-
locale you pass. `(1234.5).toLocaleString('en-US')` comes back as `"1234.5"`, not `"1,234.50"`. On
|
|
897
|
-
Node both work, which is how a green suite ships a Function that throws in production, or renders
|
|
898
|
-
markup the browser then refuses to hydrate.
|
|
899
|
-
|
|
900
|
-
Point your test suite at the guest's surface instead:
|
|
901
|
-
|
|
902
|
-
```ts
|
|
903
|
-
// vitest.config.ts
|
|
904
|
-
import { defineConfig } from "vitest/config";
|
|
905
|
-
|
|
906
|
-
export default defineConfig({
|
|
907
|
-
test: { setupFiles: ["wawesome/vitest-setup"] },
|
|
908
|
-
});
|
|
909
|
-
```
|
|
910
|
-
|
|
911
|
-
Templates scaffolded with `wawesome init --template` ship this already. With it in place `Intl` is
|
|
912
|
-
gone, `MessageChannel` is the platform's own implementation rather than Node's, and the
|
|
913
|
-
locale-sensitive methods throw with a message naming the remedy. They throw rather than return the
|
|
914
|
-
engine's unlocalised answer because the platform declares them unsupported, and a wrong string that
|
|
915
|
-
fails nowhere is the thing this is here to stop you shipping.
|
|
916
|
-
|
|
917
|
-
If you bundle an `Intl` polyfill, declare it in your `package.json` as you normally would. A
|
|
918
|
-
dependency that provides `Intl` is left in place rather than stripped out from under you.
|
|
919
|
-
|
|
920
|
-
### Local development and gateway overrides
|
|
921
|
-
|
|
922
|
-
If you are running a local gateway or self-hosted instance, you can configure your CLI Gateway URL using any of the
|
|
923
|
-
following:
|
|
924
|
-
|
|
925
|
-
#### 1. Custom settings (`~/.wawesome/settings.json`)
|
|
926
|
-
|
|
927
|
-
Create `~/.wawesome/settings.json`:
|
|
928
|
-
|
|
929
|
-
```json
|
|
930
|
-
{
|
|
931
|
-
"gateway_url": "http://localhost:3000"
|
|
932
|
-
}
|
|
933
|
-
```
|
|
934
|
-
|
|
935
|
-
#### 2. Environment variables
|
|
936
|
-
|
|
937
|
-
```bash
|
|
938
|
-
export WAWESOME_GATEWAY_URL="http://localhost:3000"
|
|
939
|
-
```
|
|
940
|
-
|
|
941
|
-
#### 3. CLI flag
|
|
942
|
-
|
|
943
|
-
```bash
|
|
944
|
-
npx wawesome login --gateway http://localhost:3000
|
|
945
|
-
```
|
|
946
|
-
|
|
947
|
-
A successful login records the gateway it used, and every later command goes there, because a token is only valid at
|
|
948
|
-
the gateway that issued it. `login` itself ignores that record and reads only the three overrides above, so a bare
|
|
949
|
-
`npx wawesome login` with none of them set goes to `https://api.wawesome.io`. `npx wawesome logout` clears the recorded
|
|
950
|
-
gateway along with the credentials.
|
|
951
|
-
|
|
952
|
-
---
|
|
953
|
-
|
|
954
|
-
## Deploying from CI
|
|
955
|
-
|
|
956
|
-
A **deploy credential** is what deploys without a person at a keyboard. Your workspace mints one, and the CLI reads it
|
|
957
|
-
from a single environment variable. There is no login, and no credentials file is involved at any point:
|
|
958
|
-
|
|
959
|
-
```bash
|
|
960
|
-
export WAWESOME_DEPLOY_CREDENTIAL="wawe_..."
|
|
961
|
-
export WAWESOME_GATEWAY_URL="https://api.wawesome.io"
|
|
962
|
-
|
|
963
|
-
npx wawesome deploy
|
|
964
|
-
```
|
|
965
|
-
|
|
966
|
-
The workspace is resolved from the credential itself, so nothing in your pipeline has to carry a workspace identifier.
|
|
967
|
-
The variable name is prefixed and has no shorter alias, so it cannot collide with another tool's CI variable.
|
|
968
|
-
|
|
969
|
-
While it is set:
|
|
970
|
-
|
|
971
|
-
- it wins over any `~/.wawesome/credentials.json` on the machine, so a runner with a stale cached home directory still
|
|
972
|
-
deploys as the credential rather than as whoever last logged in there;
|
|
973
|
-
- nothing is written to disk, and the secret stays in the environment where you put it;
|
|
974
|
-
- a refusal names which of four things happened: the credential is unknown, revoked, expired, or does not carry what
|
|
975
|
-
the command needs. It does not ask you to sign in again, which a pipeline cannot do;
|
|
976
|
-
- `login` and `logout` refuse, because neither would change what the next command authenticates as;
|
|
977
|
-
- `credentials` is refused by the platform, because minting, listing, regenerating, revoking and
|
|
978
|
-
deleting are closed to credentials whatever they carry;
|
|
979
|
-
- `whoami` names the credential by its prefix, so you can match it against the one in your CI settings.
|
|
980
|
-
|
|
981
|
-
---
|
|
982
|
-
|
|
983
|
-
## Security and secrets
|
|
984
|
-
|
|
985
|
-
Wawesome encrypts environment variables at rest using two-tier envelope encryption (AES-256-GCM with per-app data keys
|
|
986
|
-
and AAD context binding). Use `--secret` when setting sensitive keys:
|
|
987
|
-
|
|
988
|
-
```bash
|
|
989
|
-
npx wawesome env set STRIPE_SECRET_KEY sk_live_xxx --secret
|
|
990
|
-
```
|
|
991
|
-
|
|
992
|
-
---
|
|
993
|
-
|
|
994
|
-
## Resources and support
|
|
30
|
+
This bundles, uploads and promotes, then prints the URL. Every path beneath it reaches your function.
|
|
995
31
|
|
|
996
|
-
|
|
997
|
-
- **Documentation**: [https://docs.wawesome.io](https://docs.wawesome.io)
|
|
32
|
+
## Documentation
|
|
998
33
|
|
|
34
|
+
[Getting started](https://wawesome.io/docs/getting-started) walks the three commands above with the
|
|
35
|
+
output each one prints. The [CLI reference](https://wawesome.io/docs/cli) has every command, every
|
|
36
|
+
flag, and the JavaScript globals the platform does not provide.
|
package/dist/index.mjs
CHANGED
|
@@ -905,7 +905,7 @@ async function buildHandler(entryInput, config, options) {
|
|
|
905
905
|
* that has to name this version — `--version`, the dependency a scaffolded
|
|
906
906
|
* project pins — reads it here, so a release bumps one file.
|
|
907
907
|
*/
|
|
908
|
-
const CLI_VERSION = "0.
|
|
908
|
+
const CLI_VERSION = "0.18.1";
|
|
909
909
|
//#endregion
|
|
910
910
|
//#region src/prompt.ts
|
|
911
911
|
/**
|
|
@@ -1883,6 +1883,7 @@ function domainsUrl(creds, app) {
|
|
|
1883
1883
|
function whereItGotTo(domain) {
|
|
1884
1884
|
const { state } = domain;
|
|
1885
1885
|
if (state.serving) return "serving";
|
|
1886
|
+
if (domain.proxied_in_public_dns) return "the CNAME is in at Cloudflare with the proxy on, so set that record to DNS only";
|
|
1886
1887
|
if (state.certificate_active) return "certificate issued, so point the CNAME here to cut over";
|
|
1887
1888
|
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
1889
|
if (domain.ownership_record_pending) return "not verified yet, and the TXT record is not ready";
|
|
@@ -2252,19 +2253,23 @@ function sha256(bytes) {
|
|
|
2252
2253
|
}
|
|
2253
2254
|
/**
|
|
2254
2255
|
* What this deploy *is*: the server bundle together with the sorted set of
|
|
2255
|
-
* asset path and content-hash pairs.
|
|
2256
|
+
* asset path and content-hash pairs, and the **Fallback document** it declares.
|
|
2256
2257
|
*
|
|
2257
2258
|
* Computed here, before anything is uploaded, so a deploy that changed nothing
|
|
2258
2259
|
* is refused before a byte moves. Mirrors `deploy_digest` in
|
|
2259
2260
|
* `server/crates/core/src/features/assets/identity.rs` — the gateway recomputes
|
|
2260
2261
|
* it from what actually arrives, so the two have to agree exactly.
|
|
2262
|
+
*
|
|
2263
|
+
* A deploy declaring no fallback contributes nothing here rather than a stated
|
|
2264
|
+
* absence, so every digest computed before fallbacks existed is unmoved.
|
|
2261
2265
|
*/
|
|
2262
2266
|
const DEPLOY_DIGEST_DOMAIN = "wawesome-deploy-v1";
|
|
2263
|
-
function deployDigest(bundle, assets) {
|
|
2267
|
+
function deployDigest(bundle, assets, fallback) {
|
|
2264
2268
|
const digest = crypto.createHash("sha256");
|
|
2265
2269
|
digest.update(`${DEPLOY_DIGEST_DOMAIN}\n`);
|
|
2266
2270
|
digest.update(`${sha256(bundle)}\n`);
|
|
2267
2271
|
for (const asset of [...assets].sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0)) digest.update(`${asset.path}\0${asset.content_hash}\n`);
|
|
2272
|
+
if (fallback) digest.update(`fallback\0${fallback.path}\0${fallback.status}\n`);
|
|
2268
2273
|
return digest.digest("hex");
|
|
2269
2274
|
}
|
|
2270
2275
|
/** The manifest as the gateway reads it — the bytes on disk are the CLI's business. */
|
|
@@ -2903,11 +2908,12 @@ async function cronCommand(action, target, subtarget, options = {}) {
|
|
|
2903
2908
|
//#region src/mount-shadow.ts
|
|
2904
2909
|
/**
|
|
2905
2910
|
* The address a **Document** answers at, relative to its Function's mount: a
|
|
2906
|
-
* request path whose last segment carries no extension
|
|
2907
|
-
* appended
|
|
2911
|
+
* request path whose last segment carries no extension resolves to that path,
|
|
2912
|
+
* to it with `.html` appended and to it with `/index.html` appended, so both
|
|
2913
|
+
* `about.html` and `about/index.html` are the address `about`.
|
|
2908
2914
|
*/
|
|
2909
2915
|
function documentAddressPath(assetPath) {
|
|
2910
|
-
return assetPath.replace(/(^|\/)index\.html$/, "");
|
|
2916
|
+
return assetPath.replace(/(^|\/)index\.html$/, "").replace(/\.html$/, "");
|
|
2911
2917
|
}
|
|
2912
2918
|
/** See **Mount** in `CONTEXT.md` for why only the root Function's can be. */
|
|
2913
2919
|
function mountsCanShadow(functionSlug) {
|
|
@@ -2975,6 +2981,53 @@ function shadowingLines(shadowed) {
|
|
|
2975
2981
|
];
|
|
2976
2982
|
}
|
|
2977
2983
|
//#endregion
|
|
2984
|
+
//#region src/document-collision.ts
|
|
2985
|
+
/**
|
|
2986
|
+
* The addresses this deploy carries more than one file for.
|
|
2987
|
+
*
|
|
2988
|
+
* An address resolves to an ordered set of keys and the first one the version
|
|
2989
|
+
* carries answers, so `about`, `about.html` and `about/index.html` all sit at
|
|
2990
|
+
* `/about` and the first of them wins. Nothing is refused: a build that emitted
|
|
2991
|
+
* two is a worse day when the deploy fails than when it says which one answers.
|
|
2992
|
+
*/
|
|
2993
|
+
function collidingDocuments(assetPaths) {
|
|
2994
|
+
const byAddress = /* @__PURE__ */ new Map();
|
|
2995
|
+
for (const assetPath of assetPaths) {
|
|
2996
|
+
const address = documentAddressPath(assetPath);
|
|
2997
|
+
byAddress.set(address, [...byAddress.get(address) ?? [], assetPath]);
|
|
2998
|
+
}
|
|
2999
|
+
return [...byAddress].filter(([, carried]) => carried.length > 1).map(([address, carried]) => {
|
|
3000
|
+
const [answers, ...shadowed] = [...carried].sort((one, other) => preference(one) - preference(other));
|
|
3001
|
+
return {
|
|
3002
|
+
address,
|
|
3003
|
+
answers,
|
|
3004
|
+
shadowed
|
|
3005
|
+
};
|
|
3006
|
+
});
|
|
3007
|
+
}
|
|
3008
|
+
/** Where a file sits in the candidate order the platform resolves an address through. */
|
|
3009
|
+
function preference(assetPath) {
|
|
3010
|
+
if (assetPath === documentAddressPath(assetPath)) return 1;
|
|
3011
|
+
return /(^|\/)index\.html$/.test(assetPath) ? 3 : 2;
|
|
3012
|
+
}
|
|
3013
|
+
/** What the deploy says about an address it carries two pages for. */
|
|
3014
|
+
function collidingLines(colliding) {
|
|
3015
|
+
if (colliding.length === 0) return [];
|
|
3016
|
+
const lines = [];
|
|
3017
|
+
for (const [index, collision] of colliding.entries()) {
|
|
3018
|
+
const at = collision.address === "" ? "/" : `/${collision.address}`;
|
|
3019
|
+
lines.push("", ` ${index === 0 ? "Collision: " : " "} \x1b[33m${collision.answers}\x1b[0m`, ...indented(`This page and ${collision.shadowed.join(", ")} both answer ${at}. ${collision.answers} is the one served there, and ${listed(collision.shadowed)} still answer${collision.shadowed.length === 1 ? "s" : ""} at its own name.`));
|
|
3020
|
+
}
|
|
3021
|
+
return [
|
|
3022
|
+
...lines,
|
|
3023
|
+
`${VALUE_COLUMN}Nothing was refused — every file this deploy declared is`,
|
|
3024
|
+
`${VALUE_COLUMN}stored and served.`
|
|
3025
|
+
];
|
|
3026
|
+
}
|
|
3027
|
+
function listed(paths) {
|
|
3028
|
+
return paths.length === 1 ? paths[0] : `${paths.slice(0, -1).join(", ")} and ${paths[paths.length - 1]}`;
|
|
3029
|
+
}
|
|
3030
|
+
//#endregion
|
|
2978
3031
|
//#region src/deploy.ts
|
|
2979
3032
|
function declaredFields(declared) {
|
|
2980
3033
|
return {
|
|
@@ -2984,6 +3037,28 @@ function declaredFields(declared) {
|
|
|
2984
3037
|
};
|
|
2985
3038
|
}
|
|
2986
3039
|
/**
|
|
3040
|
+
* The fallback this project declares.
|
|
3041
|
+
*
|
|
3042
|
+
* A declaration missing either half is refused here rather than at the gateway,
|
|
3043
|
+
* because the pair is part of what the deploy identifies itself by and that is
|
|
3044
|
+
* computed before anything is sent. Which statuses a document may be sent with,
|
|
3045
|
+
* and whether the path names a file the deploy carries, are the platform's to
|
|
3046
|
+
* judge: duplicating either rule here is two places for it to drift.
|
|
3047
|
+
*/
|
|
3048
|
+
function declaredFallback(value) {
|
|
3049
|
+
if (value === void 0 || value === null) return void 0;
|
|
3050
|
+
const declared = value;
|
|
3051
|
+
if (typeof declared.path === "string" && declared.path.trim() && Number.isInteger(declared.status)) return {
|
|
3052
|
+
path: declared.path.trim(),
|
|
3053
|
+
status: declared.status
|
|
3054
|
+
};
|
|
3055
|
+
console.error("[wawesome] Error: 'fallback' in wawesome-function.json must name a document and the");
|
|
3056
|
+
console.error("[wawesome] status it answers at.");
|
|
3057
|
+
console.error("[wawesome] A not-found page is \x1B[36m{ \"path\": \"404.html\", \"status\": 404 }\x1B[0m and a");
|
|
3058
|
+
console.error("[wawesome] single-page application's shell is \x1B[36m{ \"path\": \"index.html\", \"status\": 200 }\x1B[0m.");
|
|
3059
|
+
process.exit(1);
|
|
3060
|
+
}
|
|
3061
|
+
/**
|
|
2987
3062
|
* The visibility this project declares, refused here rather than at the gateway
|
|
2988
3063
|
* so a typo is a message at the keyboard.
|
|
2989
3064
|
*/
|
|
@@ -3031,7 +3106,8 @@ async function deploy(entryInput, options) {
|
|
|
3031
3106
|
const declared = {
|
|
3032
3107
|
visibility: declaredVisibility(config.visibility),
|
|
3033
3108
|
confirmPublish: Boolean(options.publish),
|
|
3034
|
-
confirmAssetDrop: Boolean(options.dropAssets)
|
|
3109
|
+
confirmAssetDrop: Boolean(options.dropAssets),
|
|
3110
|
+
fallback: declaredFallback(config.fallback)
|
|
3035
3111
|
};
|
|
3036
3112
|
const domainName = declaredDomain(config.domain);
|
|
3037
3113
|
if (isVerbose) {
|
|
@@ -3076,6 +3152,7 @@ async function deploy(entryInput, options) {
|
|
|
3076
3152
|
}
|
|
3077
3153
|
}
|
|
3078
3154
|
const declaresSchedules = config.schedules !== void 0;
|
|
3155
|
+
const declaresFallback = declared.fallback !== void 0;
|
|
3079
3156
|
const assets = config.assets ? collectAssets(path.resolve(config.assets)) : [];
|
|
3080
3157
|
const documents = assets.filter((asset) => isDocument(asset.path));
|
|
3081
3158
|
if (jsCode === null && documents.length === 0) {
|
|
@@ -3090,7 +3167,7 @@ async function deploy(entryInput, options) {
|
|
|
3090
3167
|
const uploadUrl = `${creds.gateway_url}/v1/apps/${encodeURIComponent(app)}/functions/${encodeURIComponent(funcName)}/code`;
|
|
3091
3168
|
if (isVerbose) console.log(`[wawesome:verbose] POST ${uploadUrl}`);
|
|
3092
3169
|
const declaredOnTheWire = declaredFields(declared);
|
|
3093
|
-
const uploadRes = assets.length > 0 || declaresSchedules || Object.keys(declaredOnTheWire).length > 0 || jsCode === null ? await authorizedFetch(uploadUrl, {
|
|
3170
|
+
const uploadRes = assets.length > 0 || declaresSchedules || declaresFallback || Object.keys(declaredOnTheWire).length > 0 || jsCode === null ? await authorizedFetch(uploadUrl, {
|
|
3094
3171
|
method: "POST",
|
|
3095
3172
|
headers: {
|
|
3096
3173
|
Authorization: `Bearer ${creds.tenant_jwt}`,
|
|
@@ -3100,6 +3177,7 @@ async function deploy(entryInput, options) {
|
|
|
3100
3177
|
...jsCode !== null ? { code: jsCode } : {},
|
|
3101
3178
|
...assets.length > 0 ? { assets: manifestOf(assets) } : {},
|
|
3102
3179
|
...declaresSchedules ? { schedules: config.schedules } : {},
|
|
3180
|
+
...declaresFallback ? { fallback: declared.fallback } : {},
|
|
3103
3181
|
...declaredOnTheWire
|
|
3104
3182
|
})
|
|
3105
3183
|
}) : await authorizedFetch(uploadUrl, {
|
|
@@ -3223,6 +3301,7 @@ async function deploy(entryInput, options) {
|
|
|
3223
3301
|
}
|
|
3224
3302
|
if (domain) for (const line of domainLines(domain)) console.log(line);
|
|
3225
3303
|
for (const line of shadowingLines(shadowed)) console.log(line);
|
|
3304
|
+
for (const line of collidingLines(collidingDocuments(assets.map((asset) => asset.path)))) console.log(line);
|
|
3226
3305
|
if (headroom) {
|
|
3227
3306
|
console.log("");
|
|
3228
3307
|
for (const line of headroom) console.log(line);
|
|
@@ -3261,7 +3340,7 @@ async function uploadAssets(creds, app, funcName, bundle, assets, declared, isVe
|
|
|
3261
3340
|
"Content-Type": "application/json"
|
|
3262
3341
|
},
|
|
3263
3342
|
body: JSON.stringify({
|
|
3264
|
-
deploy_digest: deployDigest(Buffer.from(bundle, "utf-8"), assets),
|
|
3343
|
+
deploy_digest: deployDigest(Buffer.from(bundle, "utf-8"), assets, declared.fallback),
|
|
3265
3344
|
assets: manifestOf(assets),
|
|
3266
3345
|
...declaredFields(declared)
|
|
3267
3346
|
})
|
|
@@ -5623,6 +5702,16 @@ function isExpired(credential, now) {
|
|
|
5623
5702
|
const at = new Date(credential.expires_at).getTime();
|
|
5624
5703
|
return !Number.isNaN(at) && at <= now;
|
|
5625
5704
|
}
|
|
5705
|
+
/**
|
|
5706
|
+
* Which rows somebody here minted and which they authorized a product to hold.
|
|
5707
|
+
*
|
|
5708
|
+
* The product is named beside the word rather than left to the label the token
|
|
5709
|
+
* endpoint writes: the label is a convention and this is the row's own answer.
|
|
5710
|
+
*/
|
|
5711
|
+
function kindOf(credential) {
|
|
5712
|
+
if (credential.kind !== "oauth") return "minted";
|
|
5713
|
+
return credential.client ? `connector (${credential.client.name})` : "connector";
|
|
5714
|
+
}
|
|
5626
5715
|
function statusOf(credential, now) {
|
|
5627
5716
|
if (credential.revoked_at) return "revoked";
|
|
5628
5717
|
if (isExpired(credential, now)) return "expired";
|
|
@@ -5745,6 +5834,7 @@ function formatCredentials(credentials, now) {
|
|
|
5745
5834
|
const rows = credentials.map((credential) => ({
|
|
5746
5835
|
name: credential.name,
|
|
5747
5836
|
prefix: credential.prefix,
|
|
5837
|
+
kind: kindOf(credential),
|
|
5748
5838
|
capabilities: credential.capabilities.join(", "),
|
|
5749
5839
|
apps: namedApps(credential.apps) ?? "every App",
|
|
5750
5840
|
lastUsed: asUtc(credential.last_used_at),
|
|
@@ -5760,6 +5850,10 @@ function formatCredentials(credentials, now) {
|
|
|
5760
5850
|
header: "PREFIX",
|
|
5761
5851
|
of: (row) => row.prefix
|
|
5762
5852
|
},
|
|
5853
|
+
{
|
|
5854
|
+
header: "KIND",
|
|
5855
|
+
of: (row) => row.kind
|
|
5856
|
+
},
|
|
5763
5857
|
{
|
|
5764
5858
|
header: "CAPABILITIES",
|
|
5765
5859
|
of: (row) => row.capabilities
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wawesome",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.1",
|
|
4
4
|
"description": "CLI tool for building and deploying serverless functions on wawesome.io platform",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -35,6 +35,9 @@
|
|
|
35
35
|
"dist",
|
|
36
36
|
"vite-env.d.ts"
|
|
37
37
|
],
|
|
38
|
+
"engines": {
|
|
39
|
+
"node": ">=22.13.0"
|
|
40
|
+
},
|
|
38
41
|
"scripts": {
|
|
39
42
|
"build": "tsdown",
|
|
40
43
|
"dev": "tsdown --watch",
|