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.
Files changed (3) hide show
  1. package/README.md +12 -974
  2. package/dist/index.mjs +103 -9
  3. 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 WebAssembly functions on the
3
+ > Official CLI for bundling and deploying serverless functions on the
4
4
  > [wawesome.io](https://wawesome.io) platform.
5
5
 
6
6
  [![npm version](https://img.shields.io/npm/v/wawesome.svg?color=cyan)](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
- ### 3. Initialize a project
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 my-wasm-app && cd my-wasm-app
19
+ mkdir hello && cd hello
32
20
  npx wawesome init
33
21
  ```
34
22
 
35
- `init` asks for the function name and for the **App slug**. An App groups the Functions of one
36
- project, and its slug is part of the public URL your client sees. Both default to the directory
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
- - **Platform Homepage**: [https://wawesome.io](https://wawesome.io)
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.17.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 has `/index.html`
2907
- * appended, so the Document at `about/index.html` is the address `about`.
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.17.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",