toiljs 0.0.60 → 0.0.61
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/.github/workflows/ci.yml +31 -0
- package/CHANGELOG.md +5 -0
- package/build/cli/.tsbuildinfo +1 -1
- package/build/cli/index.js +2 -2
- package/build/client/.tsbuildinfo +1 -1
- package/build/client/index.d.ts +1 -1
- package/build/client/index.js +1 -1
- package/build/client/routing/mount.js +12 -1
- package/build/client/ssr/markers.d.ts +1 -0
- package/build/client/ssr/markers.js +3 -0
- package/build/compiler/.tsbuildinfo +1 -1
- package/build/compiler/config.d.ts +21 -0
- package/build/compiler/config.js +35 -0
- package/build/compiler/docs.d.ts +2 -1
- package/build/compiler/docs.js +33 -304
- package/build/compiler/index.d.ts +13 -0
- package/build/compiler/index.js +113 -21
- package/build/compiler/template-build.d.ts +21 -1
- package/build/compiler/template-build.js +110 -26
- package/build/compiler/toil-docs.generated.d.ts +1 -0
- package/build/compiler/toil-docs.generated.js +20 -0
- package/build/devserver/.tsbuildinfo +1 -1
- package/build/devserver/daemon/catalog.d.ts +26 -0
- package/build/devserver/daemon/catalog.js +48 -0
- package/build/devserver/daemon/cron.d.ts +4 -0
- package/build/devserver/daemon/cron.js +50 -0
- package/build/devserver/daemon/host.d.ts +37 -0
- package/build/devserver/daemon/host.js +94 -0
- package/build/devserver/daemon/index.d.ts +34 -0
- package/build/devserver/daemon/index.js +241 -0
- package/build/devserver/db/catalog.d.ts +2 -1
- package/build/devserver/db/catalog.js +44 -44
- package/build/devserver/db/database.d.ts +27 -11
- package/build/devserver/db/database.js +539 -169
- package/build/devserver/db/index.d.ts +1 -1
- package/build/devserver/db/index.js +1 -1
- package/build/devserver/db/routeKinds.d.ts +8 -0
- package/build/devserver/db/routeKinds.js +139 -0
- package/build/devserver/db/types.d.ts +64 -1
- package/build/devserver/db/types.js +33 -1
- package/build/devserver/index.d.ts +10 -0
- package/build/devserver/index.js +7 -0
- package/build/devserver/mstore/store.d.ts +18 -0
- package/build/devserver/mstore/store.js +82 -0
- package/build/devserver/runtime/host.d.ts +6 -0
- package/build/devserver/runtime/host.js +45 -1
- package/build/devserver/runtime/module.d.ts +1 -0
- package/build/devserver/runtime/module.js +27 -1
- package/build/devserver/server.d.ts +6 -0
- package/build/devserver/server.js +59 -0
- package/build/devserver/ssr.d.ts +25 -0
- package/build/devserver/ssr.js +114 -0
- package/build/devserver/wasm/sections.d.ts +2 -0
- package/build/devserver/wasm/sections.js +42 -0
- package/build/devserver/wasm/surface.d.ts +18 -0
- package/build/devserver/wasm/surface.js +41 -0
- package/docs/README.md +4 -4
- package/docs/auth-todo.md +6 -6
- package/docs/caching.md +5 -5
- package/docs/cli.md +15 -0
- package/docs/client.md +40 -0
- package/docs/crypto.md +4 -4
- package/docs/data.md +6 -6
- package/docs/email.md +28 -28
- package/docs/environment.md +10 -10
- package/docs/index.md +26 -0
- package/docs/ratelimit.md +10 -10
- package/docs/routing.md +2 -2
- package/docs/server.md +61 -0
- package/docs/ssr.md +561 -113
- package/docs/styling.md +22 -0
- package/docs/time.md +1 -1
- package/eslint.config.js +10 -1
- package/examples/basic/client/components/Header.tsx +3 -0
- package/examples/basic/client/routes/features/actions.tsx +0 -2
- package/examples/basic/client/routes/hello.tsx +89 -19
- package/examples/basic/client/styles/main.css +48 -0
- package/examples/basic/server/SsrHelloRender.ts +97 -0
- package/examples/basic/server/main.ts +5 -0
- package/examples/basic/server/streams/Echo.ts +49 -0
- package/package.json +12 -10
- package/scripts/gen-toil-docs.mjs +96 -0
- package/src/cli/create.ts +2 -2
- package/src/client/index.ts +1 -1
- package/src/client/routing/mount.tsx +18 -2
- package/src/client/ssr/markers.tsx +22 -0
- package/src/compiler/config.ts +88 -2
- package/src/compiler/docs.ts +47 -308
- package/src/compiler/index.ts +236 -32
- package/src/compiler/ssr-codegen.ts +1 -1
- package/src/compiler/template-build.ts +247 -46
- package/src/compiler/toil-docs.generated.ts +26 -0
- package/src/devserver/daemon/catalog.ts +120 -0
- package/src/devserver/daemon/cron.ts +87 -0
- package/src/devserver/daemon/host.ts +224 -0
- package/src/devserver/daemon/index.ts +349 -0
- package/src/devserver/db/catalog.ts +61 -53
- package/src/devserver/db/database.ts +613 -149
- package/src/devserver/db/index.ts +1 -1
- package/src/devserver/db/routeKinds.ts +147 -0
- package/src/devserver/db/types.ts +65 -2
- package/src/devserver/index.ts +12 -0
- package/src/devserver/mstore/store.ts +121 -0
- package/src/devserver/runtime/host.ts +92 -1
- package/src/devserver/runtime/module.ts +35 -1
- package/src/devserver/server.ts +101 -0
- package/src/devserver/ssr.ts +166 -0
- package/src/devserver/wasm/sections.ts +59 -0
- package/src/devserver/wasm/surface.ts +88 -0
- package/test/daemon-build.test.ts +198 -0
- package/test/daemon-catalog.test.ts +265 -0
- package/test/daemon-emulation.test.ts +216 -0
- package/test/devserver-database.test.ts +396 -5
- package/test/email-preview.test.ts +6 -1
- package/test/fixtures/daemon-app.ts +56 -0
- package/test/global-setup.ts +17 -0
- package/test/ssr-render.test.ts +94 -27
- package/test/ssr-template.test.tsx +44 -1
- package/vitest.config.ts +3 -0
package/docs/email.md
CHANGED
|
@@ -5,17 +5,17 @@ toiljs can send transactional email from a route handler. A handler calls
|
|
|
5
5
|
`emails/` folder, or the stateless `TwoFactor` helper); the edge hands the
|
|
6
6
|
message to a single off-core mailer thread that talks to the provider over the
|
|
7
7
|
kernel network (never the worker cores), and **suspends** the wasm call until the
|
|
8
|
-
provider responds
|
|
8
|
+
provider responds, so a slow send never blocks the worker.
|
|
9
9
|
|
|
10
10
|
Everything here is an ambient **global** (no import), like `crypto` and
|
|
11
11
|
`AuthService`. A tenant that never sends email pulls none of it into its build.
|
|
12
12
|
|
|
13
|
-
- **`EmailService
|
|
14
|
-
- **`EmailTemplate
|
|
13
|
+
- **`EmailService`**, send one email.
|
|
14
|
+
- **`EmailTemplate`**, a reusable template with `{{placeholder}}` substitution
|
|
15
15
|
(plain text and/or HTML).
|
|
16
|
-
- **`emails/*.tsx
|
|
16
|
+
- **`emails/*.tsx`**, author emails as React components; the build renders them
|
|
17
17
|
to static HTML and generates a typed `Emails.<Name>.send(...)`.
|
|
18
|
-
- **`TwoFactor
|
|
18
|
+
- **`TwoFactor`**, stateless email verification codes (2FA / confirm), no DB.
|
|
19
19
|
|
|
20
20
|
> **The one rule of HTML email:** email clients run **no JavaScript** and strip
|
|
21
21
|
> `<style>`/external CSS. So HTML email is a static, inline-styled string, and
|
|
@@ -24,7 +24,7 @@ Everything here is an ambient **global** (no import), like `crypto` and
|
|
|
24
24
|
|
|
25
25
|
## Configure email
|
|
26
26
|
|
|
27
|
-
Email is a **framework-reserved namespace of the tenant's environment
|
|
27
|
+
Email is a **framework-reserved namespace of the tenant's environment**, the
|
|
28
28
|
same out-of-band [Environment](./environment.md) store that backs
|
|
29
29
|
`Environment.get` / `getSecure`, but the `[email]` block is **host-only**: it is
|
|
30
30
|
read and used in Rust (the off-core mailer) and is **never exposed to the
|
|
@@ -35,7 +35,7 @@ On the edge today it lives in the tenant's env secrets file,
|
|
|
35
35
|
`$TOIL_ENV_DIR/<host>.env.secrets` (default dir `/run/toil/env`), kept `0600` and
|
|
36
36
|
**out of `hosts/`** so the config watcher never sees a credential (the dashboard /
|
|
37
37
|
edge DB replaces this file later). Email config is a set of **reserved
|
|
38
|
-
`TOIL_EMAIL_*` keys
|
|
38
|
+
`TOIL_EMAIL_*` keys**, host-only, stripped from the guest buckets, so a tenant
|
|
39
39
|
can never read them via `Environment.getSecure`:
|
|
40
40
|
|
|
41
41
|
```bash
|
|
@@ -57,15 +57,15 @@ reserved namespace the framework consumes.
|
|
|
57
57
|
When `enabled` is `false` (the default) the host has no email capability and
|
|
58
58
|
`EmailService.send` returns `Disabled`. The env is loaded **lazily** (on the
|
|
59
59
|
first send) and the `api_key` is materialized into a zeroizing secret in host
|
|
60
|
-
memory
|
|
60
|
+
memory, never written to logs or `/_admin`. A malformed `[email]` block is
|
|
61
61
|
treated as "no email" (the host returns `Disabled`); validate config on the
|
|
62
62
|
dashboard before deploying.
|
|
63
63
|
|
|
64
64
|
### Providers
|
|
65
65
|
|
|
66
|
-
**Resend** (`provider = "resend"`)
|
|
66
|
+
**Resend** (`provider = "resend"`), a JSON API; `api_key` holds the API key.
|
|
67
67
|
|
|
68
|
-
**Gmail** (`TOIL_EMAIL_PROVIDER=gmail`)
|
|
68
|
+
**Gmail** (`TOIL_EMAIL_PROVIDER=gmail`), SMTP with Gmail defaults:
|
|
69
69
|
`smtp.gmail.com`, port `587` (STARTTLS), username = `from`. `TOIL_EMAIL_API_KEY`
|
|
70
70
|
holds a Gmail **App Password** (create one at
|
|
71
71
|
<https://myaccount.google.com/apppasswords>; the account needs 2-Step
|
|
@@ -78,7 +78,7 @@ TOIL_EMAIL_PROVIDER=gmail
|
|
|
78
78
|
TOIL_EMAIL_API_KEY=abcd efgh ijkl mnop
|
|
79
79
|
```
|
|
80
80
|
|
|
81
|
-
**Generic SMTP** (`TOIL_EMAIL_PROVIDER=smtp`)
|
|
81
|
+
**Generic SMTP** (`TOIL_EMAIL_PROVIDER=smtp`), any submission server (Outlook,
|
|
82
82
|
SendGrid/Mailgun SMTP, your own). Requires `TOIL_EMAIL_SMTP_HOST`; port defaults
|
|
83
83
|
to `587` (STARTTLS), or set `465` for implicit TLS. `TOIL_EMAIL_SMTP_USER`
|
|
84
84
|
defaults to `from`.
|
|
@@ -95,9 +95,9 @@ TOIL_EMAIL_SMTP_USER=noreply@example.com
|
|
|
95
95
|
|
|
96
96
|
### In dev
|
|
97
97
|
|
|
98
|
-
`toiljs dev` runs the **full email pipeline** in Node
|
|
98
|
+
`toiljs dev` runs the **full email pipeline** in Node, recipient validation,
|
|
99
99
|
dedup, and the per-minute / per-day / per-recipient caps all behave exactly like
|
|
100
|
-
the edge
|
|
100
|
+
the edge, and **really sends** once you configure a provider. Configure it in
|
|
101
101
|
`toil.config.ts` (non-secret) with the API key in `.env.secrets`:
|
|
102
102
|
|
|
103
103
|
```ts
|
|
@@ -153,7 +153,7 @@ class Notify {
|
|
|
153
153
|
|
|
154
154
|
| Status | Meaning | Retry? |
|
|
155
155
|
| ----------------- | ----------------------------------------------------------- | ---------------- |
|
|
156
|
-
| `Sent` | Accepted by the provider
|
|
156
|
+
| `Sent` | Accepted by the provider |, |
|
|
157
157
|
| `Deduped` | An identical recent `(recipient, purpose)` was collapsed | treat as sent |
|
|
158
158
|
| `Budget` | The host's per-minute budget is exhausted | yes, later |
|
|
159
159
|
| `TryLater` | The mailer was saturated / a queue was full | yes, back off |
|
|
@@ -193,7 +193,7 @@ const status = welcome.send('alice@example.com', vars, 'welcome');
|
|
|
193
193
|
- `template.render(vars)` returns the rendered `{ subject, body, html }` without
|
|
194
194
|
sending (useful for preview/testing).
|
|
195
195
|
|
|
196
|
-
For anything richer than `{{token}}` substitution
|
|
196
|
+
For anything richer than `{{token}}` substitution, real layout, CSS, brand,
|
|
197
197
|
author the email as a React component instead.
|
|
198
198
|
|
|
199
199
|
## React email templates
|
|
@@ -247,7 +247,7 @@ Authoring notes:
|
|
|
247
247
|
email name), `export const text` (a plain-text alternative; otherwise derived
|
|
248
248
|
from the HTML), `export const purpose`.
|
|
249
249
|
- **Build-time, field substitution only.** Because the component renders once at
|
|
250
|
-
build, per-send data is `{{token}}` substitution
|
|
250
|
+
build, per-send data is `{{token}}` substitution, a runtime `{items.map(...)}`
|
|
251
251
|
or conditional bakes in at build, it does not re-run per recipient. That covers
|
|
252
252
|
transactional / 2FA / confirmation email; dynamic lists need a different
|
|
253
253
|
approach.
|
|
@@ -265,7 +265,7 @@ editor. It refreshes live as you edit the template or its CSS.
|
|
|
265
265
|
## Email verification codes (`TwoFactor`)
|
|
266
266
|
|
|
267
267
|
`TwoFactor` is a **stateless** email-code primitive (2FA, email confirmation,
|
|
268
|
-
magic codes)
|
|
268
|
+
magic codes), no database. It emails a random code and returns a signed
|
|
269
269
|
**token** that commits to the code via HMAC, without putting the code in the
|
|
270
270
|
token (the code is only in the email). Verification recomputes the HMAC from the
|
|
271
271
|
token plus the user-entered code, so a valid `(token, code)` pair can only come
|
|
@@ -281,18 +281,18 @@ const challenge = TwoFactor.send('alice@example.com', 'login'); // emails the co
|
|
|
281
281
|
const ok: bool = TwoFactor.verify(challenge.token, 'alice@example.com', userEntered);
|
|
282
282
|
```
|
|
283
283
|
|
|
284
|
-
- **`send(recipient, purpose, ttlSecs = 600, digits = 6)
|
|
284
|
+
- **`send(recipient, purpose, ttlSecs = 600, digits = 6)`**, issues a code,
|
|
285
285
|
emails it with a built-in template, returns `{ token, status }`.
|
|
286
|
-
- **`issue(recipient, purpose, ttlSecs, digits)
|
|
286
|
+
- **`issue(recipient, purpose, ttlSecs, digits)`**, returns `{ code, token }`
|
|
287
287
|
**without** sending, so you can email `code` with your own `EmailTemplate` /
|
|
288
288
|
`Emails.*` for a branded message.
|
|
289
|
-
- **`verify(token, recipient, code)
|
|
289
|
+
- **`verify(token, recipient, code)`**, `true` only for a code issued for that
|
|
290
290
|
recipient that hasn't expired. Constant-time compare.
|
|
291
|
-
- **`TwoFactor.setSecret(secret)
|
|
291
|
+
- **`TwoFactor.setSecret(secret)`**, the HMAC secret for the tokens. Call once
|
|
292
292
|
at startup in `main.ts`; it must be identical on every edge instance and out of
|
|
293
293
|
any client bundle. (This is separate from the provider `api_key`.)
|
|
294
294
|
|
|
295
|
-
**Limitation:** this gives integrity + expiry but **not single-use
|
|
295
|
+
**Limitation:** this gives integrity + expiry but **not single-use**, a valid
|
|
296
296
|
code verifies repeatedly within its TTL, because there is no server state to burn
|
|
297
297
|
it. Keep the TTL short; for true single-use, store a per-recipient
|
|
298
298
|
last-verified-at and reject at or before it.
|
|
@@ -302,13 +302,13 @@ last-verified-at and reject at or before it.
|
|
|
302
302
|
All enforced authoritatively in the single mailer (so the counts are exact across
|
|
303
303
|
all workers):
|
|
304
304
|
|
|
305
|
-
- **Per-host budget
|
|
305
|
+
- **Per-host budget**, two rolling windows, both enforced: a 1-minute cap
|
|
306
306
|
(`max_per_min`) and a 24-hour cap (`max_per_day`, `0` = unlimited). Over either
|
|
307
307
|
one → `Budget`. Each host's caps, in-window sends, and reject counts are visible
|
|
308
308
|
per host at `GET /_admin/email`.
|
|
309
|
-
- **Per-recipient cap
|
|
309
|
+
- **Per-recipient cap**, `max_per_recipient_per_hour`. Over it →
|
|
310
310
|
`RecipientCapped`.
|
|
311
|
-
- **Dedup
|
|
311
|
+
- **Dedup**, identical `(host, recipient, purpose)` within ~30s → `Deduped`.
|
|
312
312
|
|
|
313
313
|
Editing these in the host config takes effect on the next send (no restart).
|
|
314
314
|
|
|
@@ -316,11 +316,11 @@ Editing these in the host config takes effect on the next send (no restart).
|
|
|
316
316
|
|
|
317
317
|
`GET /_admin/email` returns process-wide counters by reason (JSON), e.g.
|
|
318
318
|
`submitted`, `sent`, `deduped`, `budget`, `recipient_capped`, `try_later`,
|
|
319
|
-
`bad_recipient`, `provider_error`. It exposes **counts only
|
|
319
|
+
`bad_recipient`, `provider_error`. It exposes **counts only**, never a
|
|
320
320
|
recipient, code, subject, body, or secret.
|
|
321
321
|
|
|
322
322
|
## See also
|
|
323
323
|
|
|
324
|
-
- [Rate limiting](./ratelimit.md)
|
|
324
|
+
- [Rate limiting](./ratelimit.md), protect your routes (including any email
|
|
325
325
|
trigger) with `@ratelimit`.
|
|
326
|
-
- [Web Crypto](./crypto.md)
|
|
326
|
+
- [Web Crypto](./crypto.md), the `crypto` global `TwoFactor` builds on.
|
package/docs/environment.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
`Environment` gives a tenant **per-app environment variables and secrets**, set
|
|
4
4
|
out of band (a dashboard, like GitHub Actions) so the deployed `.wasm` carries
|
|
5
|
-
**no credentials**. It is read-only from app code
|
|
5
|
+
**no credentials**. It is read-only from app code, there is no `set`; values are
|
|
6
6
|
configured on the deployment side, never from the module.
|
|
7
7
|
|
|
8
8
|
```ts
|
|
@@ -20,15 +20,15 @@ class Cfg {
|
|
|
20
20
|
}
|
|
21
21
|
```
|
|
22
22
|
|
|
23
|
-
`Environment` is a global
|
|
23
|
+
`Environment` is a global, no import needed (like `EmailService` / `AuthService`).
|
|
24
24
|
|
|
25
25
|
## Two disjoint buckets
|
|
26
26
|
|
|
27
27
|
Just like GitHub Actions' `vars` vs `secrets`:
|
|
28
28
|
|
|
29
|
-
- **`Environment.get(key)`** reads **plain vars
|
|
29
|
+
- **`Environment.get(key)`** reads **plain vars**, non-sensitive config (a public
|
|
30
30
|
API base URL, a feature flag, a region). Returns the string, or `null`.
|
|
31
|
-
- **`Environment.getSecure(key)`** reads **secrets
|
|
31
|
+
- **`Environment.getSecure(key)`** reads **secrets**, sensitive values (a
|
|
32
32
|
third-party API key). Returns the string, or `null`.
|
|
33
33
|
|
|
34
34
|
The buckets are **disjoint**: a secret is **never** returned by `get()`, and a
|
|
@@ -37,13 +37,13 @@ through a code path that logs the result of a `get()`. Keys are case-sensitive,
|
|
|
37
37
|
exact-match.
|
|
38
38
|
|
|
39
39
|
> Secrets you read with `getSecure` are plaintext in your module at runtime
|
|
40
|
-
> (that's the point
|
|
40
|
+
> (that's the point, you need them to call out). Don't log them, don't put them
|
|
41
41
|
> in a response, and don't copy them into a client bundle.
|
|
42
42
|
|
|
43
43
|
## What is NOT here
|
|
44
44
|
|
|
45
|
-
Framework-reserved namespaces (today: **email** provider config) are **host-only
|
|
46
|
-
|
|
45
|
+
Framework-reserved namespaces (today: **email** provider config) are **host-only**,
|
|
46
|
+
resolved and used in Rust where the framework needs them, and **never exposed to
|
|
47
47
|
the `.wasm`**. There is no `Environment.email`; you configure email in the
|
|
48
48
|
`[email]` block of the same env file and the platform uses it for you (see
|
|
49
49
|
[Email](./email.md)). The env imports only ever see your own `vars` / `secrets`.
|
|
@@ -53,7 +53,7 @@ the `.wasm`**. There is no `Environment.email`; you configure email in the
|
|
|
53
53
|
Vars and secrets live in **two separate dotenv (`.env`) files**, so the disjoint
|
|
54
54
|
split is structural and the secrets file can be locked down on its own. On the
|
|
55
55
|
edge they are per host, **out of `hosts/`** (so the config watcher never sees a
|
|
56
|
-
credential)
|
|
56
|
+
credential), the dashboard / edge database replaces them later:
|
|
57
57
|
|
|
58
58
|
```bash
|
|
59
59
|
# $TOIL_ENV_DIR/<host>.env (default dir /run/toil/env)
|
|
@@ -63,7 +63,7 @@ REGION=eu
|
|
|
63
63
|
# $TOIL_ENV_DIR/<host>.env.secrets (mode 0600)
|
|
64
64
|
STRIPE_KEY=sk_live_xxx # -> Environment.getSecure("STRIPE_KEY")
|
|
65
65
|
|
|
66
|
-
# host-only email config
|
|
66
|
+
# host-only email config, reserved TOIL_EMAIL_* keys, NEVER exposed to the .wasm
|
|
67
67
|
TOIL_EMAIL_ENABLED=true
|
|
68
68
|
TOIL_EMAIL_PROVIDER=resend
|
|
69
69
|
TOIL_EMAIL_FROM=noreply@example.com
|
|
@@ -72,7 +72,7 @@ TOIL_EMAIL_API_KEY=re_xxx
|
|
|
72
72
|
|
|
73
73
|
Each file is plain dotenv: `KEY=value` per line, `#` comments, optional `export`,
|
|
74
74
|
optional quotes. Keys with the reserved **`TOIL_`** prefix are framework/host-only
|
|
75
|
-
and are stripped from BOTH guest buckets
|
|
75
|
+
and are stripped from BOTH guest buckets, a tenant can never read them via
|
|
76
76
|
`get`/`getSecure` (see [Email](./email.md) for `TOIL_EMAIL_*`).
|
|
77
77
|
|
|
78
78
|
On the edge, env is loaded **lazily** (the first time your code reads it) into a
|
package/docs/index.md
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# toiljs
|
|
2
|
+
|
|
3
|
+
A full-stack React framework: a Vite-bundled client SPA with file-based routing, plus a
|
|
4
|
+
toilscript-to-WebAssembly server target.
|
|
5
|
+
|
|
6
|
+
## Project layout
|
|
7
|
+
|
|
8
|
+
- `client/`, the app: `routes/` (file-based), `layout.tsx`, `components/`, `styles/`,
|
|
9
|
+
`public/`, and `toil.tsx` (the entry that calls `Toil.mount`).
|
|
10
|
+
- `server/`, the toilscript → WASM target (`@main` entry), compiled by `toilscript`.
|
|
11
|
+
`@data`/`@remote`/`@service` here generate the typed client `Server` API (see [server.md](./server.md)).
|
|
12
|
+
- `toil.config.ts`, configuration via `defineConfig` (`toiljs.config.ts` also works).
|
|
13
|
+
- Generated, gitignored, do not edit: `.toil/` (working dir), `toil-env.d.ts` (ambient
|
|
14
|
+
globals), `toil-routes.d.ts` (typed routes), `shared/server.ts` (the typed RPC module,
|
|
15
|
+
emitted by the server build; import `@data` classes from `shared/server`).
|
|
16
|
+
|
|
17
|
+
## Key ideas
|
|
18
|
+
|
|
19
|
+
- `Toil` is a native global (no import): `Toil.Link`, `Toil.useRouter`, `Toil.useLoaderData`,
|
|
20
|
+
etc. The IO classes (`FastMap`, `FastSet`, `DataWriter`, `DataReader`), `parseError`, and the
|
|
21
|
+
generated `Server` RPC surface are globals too.
|
|
22
|
+
- Scripts: `npm run dev` (HMR), `npm run build` (→ `build/client` + `build/server`),
|
|
23
|
+
`npm start` (self-host the build).
|
|
24
|
+
|
|
25
|
+
See [routing.md](./routing.md), [client.md](./client.md), [styling.md](./styling.md),
|
|
26
|
+
[server.md](./server.md), [ssr.md](./ssr.md), [cli.md](./cli.md).
|
package/docs/ratelimit.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Rate limiting
|
|
2
2
|
|
|
3
|
-
The `@ratelimit` decorator throttles **any** `@rest` route
|
|
3
|
+
The `@ratelimit` decorator throttles **any** `@rest` route, a login, a signup, a
|
|
4
4
|
public API, an email trigger, anything. It is enforced at the edge, before your
|
|
5
5
|
handler runs, and keyed by default on the connecting client's **unspoofable** IP,
|
|
6
6
|
so it works as an abuse / brute-force control out of the box.
|
|
@@ -28,9 +28,9 @@ class Auth {
|
|
|
28
28
|
|
|
29
29
|
`@ratelimit(strategy, limit, window)`:
|
|
30
30
|
|
|
31
|
-
- **`strategy
|
|
31
|
+
- **`strategy`**, a `RateLimit` value (ambient global, no import):
|
|
32
32
|
`RateLimit.FixedWindow`, `RateLimit.SlidingWindow`, or `RateLimit.TokenBucket`.
|
|
33
|
-
- **`limit`** and **`window
|
|
33
|
+
- **`limit`** and **`window`**, integer literals whose meaning depends on the
|
|
34
34
|
strategy (see below).
|
|
35
35
|
|
|
36
36
|
When a request is over the limit the edge returns **`429 Too Many Requests`**
|
|
@@ -39,7 +39,7 @@ guard runs **before `@auth`**, so unauthenticated floods are limited too.
|
|
|
39
39
|
|
|
40
40
|
> Both arguments must be **integer literals** and the strategy a `RateLimit`
|
|
41
41
|
> member (or a bare integer tag). A malformed decorator emits no guard rather
|
|
42
|
-
> than miscompiling
|
|
42
|
+
> than miscompiling, the same fail-safe rule as `@cache`.
|
|
43
43
|
|
|
44
44
|
## Strategies
|
|
45
45
|
|
|
@@ -64,17 +64,17 @@ Examples:
|
|
|
64
64
|
|
|
65
65
|
## How requests are keyed
|
|
66
66
|
|
|
67
|
-
By default the limiter keys on the **client IP
|
|
67
|
+
By default the limiter keys on the **client IP**, specifically the TCP peer
|
|
68
68
|
address the edge observed (`ctx.clientIp()`), **not** a header like
|
|
69
69
|
`X-Forwarded-For`, which a client can forge. That makes it a real abuse control:
|
|
70
70
|
a caller can't reset their bucket by spoofing a header.
|
|
71
71
|
|
|
72
72
|
The count is **exact across all 14 edge workers** (a given IP always maps to one
|
|
73
73
|
authoritative shard), so the limit is global per route, not per worker. Only
|
|
74
|
-
routes that opt in with `@ratelimit` ever pay anything
|
|
74
|
+
routes that opt in with `@ratelimit` ever pay anything, the lock-free fast path
|
|
75
75
|
for everything else is untouched.
|
|
76
76
|
|
|
77
|
-
> Each rate-limited route has its own independent limiter
|
|
77
|
+
> Each rate-limited route has its own independent limiter, a limit on `/login`
|
|
78
78
|
> does not consume the budget of `/signup`.
|
|
79
79
|
|
|
80
80
|
## Notes and limits
|
|
@@ -82,14 +82,14 @@ for everything else is untouched.
|
|
|
82
82
|
- **Route-level only.** Put `@ratelimit` on each route you want limited; there is
|
|
83
83
|
no controller-wide form yet (unlike `@auth`).
|
|
84
84
|
- **Keyed on IP.** The decorator keys on the peer IP today. (A per-user / custom
|
|
85
|
-
key
|
|
85
|
+
key, limiting by account instead of IP, exists in the runtime but is not yet
|
|
86
86
|
exposed through the decorator.)
|
|
87
87
|
- **In dev.** `toiljs dev` runs a single-process mirror of the same three
|
|
88
88
|
strategies, so a limited route behaves the same locally as on the edge.
|
|
89
89
|
|
|
90
90
|
## See also
|
|
91
91
|
|
|
92
|
-
- [Email](./email.md)
|
|
92
|
+
- [Email](./email.md), `@ratelimit` pairs well with email triggers (verification
|
|
93
93
|
codes, password resets) to blunt abuse.
|
|
94
|
-
- [Auth, sessions, and `@user`](./auth.md)
|
|
94
|
+
- [Auth, sessions, and `@user`](./auth.md), `@ratelimit` runs before the `@auth`
|
|
95
95
|
guard, so it protects the login itself.
|
package/docs/routing.md
CHANGED
|
@@ -134,11 +134,11 @@ serialized.
|
|
|
134
134
|
|
|
135
135
|
Each route is either **JSON** (default) or **Binary**:
|
|
136
136
|
|
|
137
|
-
- **JSON
|
|
137
|
+
- **JSON**, the body is `JSON.parse`d and revived via the `@data` type's
|
|
138
138
|
`fromJSON`; the response is the type's `toJSON()`. 64-bit-and-larger integers
|
|
139
139
|
cross the wire as decimal strings (exact at any size). Best for endpoints a
|
|
140
140
|
browser or third party calls directly.
|
|
141
|
-
- **Binary
|
|
141
|
+
- **Binary**, the body is `Body.decode(bytes)` and the response is
|
|
142
142
|
`value.encode()`, using the deterministic `DataWriter`/`DataReader` codec. No
|
|
143
143
|
precision loss, smaller, faster. Best for app-to-app and anything
|
|
144
144
|
security-sensitive.
|
package/docs/server.md
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Server (toilscript → WebAssembly)
|
|
2
|
+
|
|
3
|
+
`server/` is the toilscript source, compiled to WebAssembly by `toilscript`.
|
|
4
|
+
|
|
5
|
+
- `server/main.ts`, the `@main` entry, exported as the WASM `main`.
|
|
6
|
+
- `server/index.ts`, your functions.
|
|
7
|
+
- `server/tsconfig.json`, extends `toilscript/std/assembly.json` (AssemblyScript/toilscript
|
|
8
|
+
globals like `i32`, not the DOM), so editors resolve server types correctly.
|
|
9
|
+
- `npm run build:server` (or `npm run build`) emits `build/server/release.wasm` and
|
|
10
|
+
regenerates `shared/server.ts` (the typed client RPC module).
|
|
11
|
+
|
|
12
|
+
## Typed RPC (`@data` / `@remote` / `@service`)
|
|
13
|
+
|
|
14
|
+
Tag server code and the build generates a typed client `Server` surface:
|
|
15
|
+
|
|
16
|
+
- `@data class X {}`, a serializable struct. Generates a client class with the same fields
|
|
17
|
+
plus `encode`/`decode`; construct it on the client: `import { X } from "shared/server"`.
|
|
18
|
+
- `@remote function f(a: T): R`, a client-callable endpoint, becomes `Server.f(a)`.
|
|
19
|
+
- `@service class S { @remote m(...) {} }`, namespaces methods: `Server.s.m(...)`.
|
|
20
|
+
|
|
21
|
+
On the client, `Server` is a global (no import) and fully typed; every call is async
|
|
22
|
+
(`Promise<R>`). Inputs/outputs are scalars, arrays, or `@data` classes, both directions.
|
|
23
|
+
|
|
24
|
+
Note: the client↔server transport is not wired yet, so calling a `Server` method throws
|
|
25
|
+
until it lands; the typed surface + codec are generated and ready.
|
|
26
|
+
|
|
27
|
+
## HTTP REST (`@rest` / `@route`)
|
|
28
|
+
|
|
29
|
+
Tag a class `@rest` and its methods with a verb to expose a real HTTP API. Unlike RPC,
|
|
30
|
+
the generated client is working `fetch` code (it is just HTTP).
|
|
31
|
+
|
|
32
|
+
- `@rest("api") class Todos {}`, mounts the controller at `/api` (bare `@rest` → `/`).
|
|
33
|
+
- `@get("/todos/:id")` / `@post` / `@del` / `@put` / `@patch` / `@head` / `@options`, verb
|
|
34
|
+
shortcuts; or `@route({ method: Methods.GET, path: "/todos", stream: DataStream.JSON })`.
|
|
35
|
+
- A method takes an optional `@data` body + an optional `ctx: RouteContext` (path params via
|
|
36
|
+
`ctx.param("id")`, `ctx.query(...)`, `ctx.header(...)`). It returns either a `@data` type,
|
|
37
|
+
which the compiler encodes per `stream` (`DataStream.JSON` default, or `DataStream.Binary`,
|
|
38
|
+
lossless for large `u64`/bignum), or a `Response` for full control - custom status and
|
|
39
|
+
headers, e.g. `Response.json(value.toJSON().toString()).setHeader("cache-control", "no-store")`
|
|
40
|
+
or `Response.notFound()`. (The editor sees the compiler-injected `@data` `toJSON`/`encode`
|
|
41
|
+
members via the toilscript plugin, so serializing into a `Response` is editor-clean.)
|
|
42
|
+
|
|
43
|
+
Each `@rest` class self-registers; dispatch them from your handler - it composes, it never
|
|
44
|
+
takes over `handle()`:
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
import { ToilHandler, Request, Response, Rest } from "toiljs/server/runtime";
|
|
48
|
+
export class App extends ToilHandler {
|
|
49
|
+
public handle(req: Request): Response {
|
|
50
|
+
const hit = Rest.dispatch(req); // try every @rest controller
|
|
51
|
+
if (hit != null) return hit;
|
|
52
|
+
return Response.notFound(); // your own logic / static fallback
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
For a REST-only project, `Server.handler = () => new RestHandler()` does the same with no
|
|
58
|
+
boilerplate. On the client: `Server.REST.todos.getTodo({ params: { id } })` (see [client.md](./client.md)).
|
|
59
|
+
|
|
60
|
+
For the full reference (`@rest`/verb decorators, `RouteContext`, `Request`, `Response`,
|
|
61
|
+
dispatch + the 404 fallback) see [routing.md](./routing.md).
|