toiljs 0.0.85 → 0.0.87
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/CHANGELOG.md +5 -0
- package/README.md +2 -2
- package/build/cli/.tsbuildinfo +1 -1
- package/build/cli/index.js +303 -293
- package/build/compiler/.tsbuildinfo +1 -1
- package/build/compiler/config.d.ts +2 -0
- package/build/compiler/config.js +1 -0
- package/build/compiler/docs.js +8 -24
- package/build/compiler/generate.js +4 -2
- package/build/compiler/index.d.ts +1 -1
- package/build/compiler/index.js +69 -6
- package/build/compiler/toil-docs.generated.js +64 -22
- package/build/devserver/.tsbuildinfo +1 -1
- package/build/devserver/analytics/index.js +7 -3
- package/build/devserver/db/database.d.ts +4 -0
- package/build/devserver/db/database.js +43 -1
- package/build/devserver/db/index.d.ts +1 -1
- package/build/devserver/db/index.js +1 -1
- package/build/devserver/db/types.d.ts +3 -0
- package/build/devserver/db/types.js +2 -0
- package/build/devserver/runtime/module.js +4 -1
- package/docs/README.md +104 -65
- package/docs/auth/README.md +102 -0
- package/docs/auth/configuration.md +94 -0
- package/docs/auth/extending.md +202 -0
- package/docs/auth/how-it-works.md +138 -0
- package/docs/auth/usage.md +188 -0
- package/docs/backend/README.md +143 -0
- package/docs/backend/data.md +351 -0
- package/docs/backend/rest.md +402 -0
- package/docs/backend/rpc.md +226 -0
- package/docs/background/README.md +114 -0
- package/docs/background/daemons.md +230 -0
- package/docs/background/derive.md +179 -0
- package/docs/cli/README.md +377 -0
- package/docs/concepts/config.md +416 -0
- package/docs/concepts/decorators.md +127 -0
- package/docs/concepts/security.md +108 -0
- package/docs/concepts/tiers.md +166 -0
- package/docs/concepts/types.md +216 -0
- package/docs/database/README.md +143 -0
- package/docs/database/capacity.md +350 -0
- package/docs/database/counters.md +174 -0
- package/docs/database/documents.md +255 -0
- package/docs/database/events.md +307 -0
- package/docs/database/membership.md +246 -0
- package/docs/database/setup.md +155 -0
- package/docs/database/unique.md +216 -0
- package/docs/database/views.md +246 -0
- package/docs/frontend/README.md +101 -0
- package/docs/frontend/data-fetching.md +243 -0
- package/docs/frontend/images.md +148 -0
- package/docs/frontend/metadata.md +344 -0
- package/docs/frontend/rendering.md +236 -0
- package/docs/frontend/routing.md +344 -0
- package/docs/frontend/scripts.md +118 -0
- package/docs/frontend/search.md +191 -0
- package/docs/frontend/styling.md +147 -0
- package/docs/getting-started/README.md +81 -0
- package/docs/getting-started/create-project.md +131 -0
- package/docs/getting-started/deploy.md +101 -0
- package/docs/getting-started/first-app.md +215 -0
- package/docs/getting-started/installation.md +106 -0
- package/docs/getting-started/migrating.md +125 -0
- package/docs/getting-started/project-structure.md +163 -0
- package/docs/introduction/README.md +41 -0
- package/docs/introduction/design-principles.md +55 -0
- package/docs/introduction/distributed.md +74 -0
- package/docs/introduction/how-it-works.md +74 -0
- package/docs/introduction/hyperscale.md +51 -0
- package/docs/introduction/modern-stack.md +36 -0
- package/docs/introduction/vs-other-frameworks.md +48 -0
- package/docs/introduction/why-toil.md +93 -0
- package/docs/llms.txt +90 -0
- package/docs/realtime/README.md +102 -0
- package/docs/realtime/channels.md +211 -0
- package/docs/realtime/streams.md +369 -0
- package/docs/services/README.md +60 -0
- package/docs/services/analytics.md +268 -0
- package/docs/services/caching.md +175 -0
- package/docs/services/cookies.md +235 -0
- package/docs/services/crypto.md +209 -0
- package/docs/services/email.md +289 -0
- package/docs/services/environment.md +141 -0
- package/docs/services/ratelimit.md +174 -0
- package/docs/services/time.md +85 -0
- package/examples/basic/client/routes/analytics.tsx +13 -12
- package/examples/basic/server/models/SiteAnalytics.ts +29 -17
- package/examples/basic/server/routes/Analytics.ts +15 -17
- package/examples/basic/server/routes/UserId.ts +22 -0
- package/package.json +15 -11
- package/scripts/gen-toil-docs.mjs +24 -35
- package/server/auth/AuthController.ts +336 -0
- package/server/auth/AuthUser.ts +23 -0
- package/server/auth/index.ts +16 -0
- package/server/globals/auth.ts +31 -0
- package/server/globals/userid.ts +107 -0
- package/src/compiler/config.ts +13 -0
- package/src/compiler/docs.ts +16 -33
- package/src/compiler/generate.ts +6 -2
- package/src/compiler/index.ts +114 -6
- package/src/compiler/toil-docs.generated.ts +64 -22
- package/src/devserver/analytics/index.ts +10 -4
- package/src/devserver/db/database.ts +67 -1
- package/src/devserver/db/index.ts +1 -0
- package/src/devserver/db/types.ts +13 -0
- package/src/devserver/runtime/module.ts +7 -0
- package/test/analytics-dev.test.ts +2 -1
- package/test/devserver-database.test.ts +113 -0
- package/docs/auth-todo.md +0 -149
- package/docs/auth.md +0 -322
- package/docs/caching.md +0 -115
- package/docs/cli.md +0 -17
- package/docs/client.md +0 -39
- package/docs/cookies.md +0 -457
- package/docs/crypto.md +0 -130
- package/docs/daemon.md +0 -123
- package/docs/data.md +0 -131
- package/docs/derive.md +0 -159
- package/docs/email.md +0 -326
- package/docs/environment.md +0 -97
- package/docs/getting-started.md +0 -128
- package/docs/index.md +0 -30
- package/docs/ratelimit.md +0 -95
- package/docs/routing.md +0 -259
- package/docs/rpc.md +0 -149
- package/docs/server.md +0 -61
- package/docs/ssr.md +0 -632
- package/docs/streams.md +0 -178
- package/docs/styling.md +0 -22
- package/docs/tiers.md +0 -133
- package/docs/time.md +0 -43
package/docs/email.md
DELETED
|
@@ -1,326 +0,0 @@
|
|
|
1
|
-
# Email
|
|
2
|
-
|
|
3
|
-
toiljs can send transactional email from a route handler. A handler calls
|
|
4
|
-
`EmailService.send(...)` (or a typed `EmailTemplate` / `Emails.*` from the
|
|
5
|
-
`emails/` folder, or the stateless `TwoFactor` helper); the edge hands the
|
|
6
|
-
message to a single off-core mailer thread that talks to the provider over the
|
|
7
|
-
kernel network (never the worker cores), and **suspends** the wasm call until the
|
|
8
|
-
provider responds, so a slow send never blocks the worker.
|
|
9
|
-
|
|
10
|
-
Everything here is an ambient **global** (no import), like `crypto` and
|
|
11
|
-
`AuthService`. A tenant that never sends email pulls none of it into its build.
|
|
12
|
-
|
|
13
|
-
- **`EmailService`**, send one email.
|
|
14
|
-
- **`EmailTemplate`**, a reusable template with `{{placeholder}}` substitution
|
|
15
|
-
(plain text and/or HTML).
|
|
16
|
-
- **`emails/*.tsx`**, author emails as React components; the build renders them
|
|
17
|
-
to static HTML and generates a typed `Emails.<Name>.send(...)`.
|
|
18
|
-
- **`TwoFactor`**, stateless email verification codes (2FA / confirm), no DB.
|
|
19
|
-
|
|
20
|
-
> **The one rule of HTML email:** email clients run **no JavaScript** and strip
|
|
21
|
-
> `<style>`/external CSS. So HTML email is a static, inline-styled string, and
|
|
22
|
-
> any "rendering" (React, CSS files) happens at **build time**, not at send time.
|
|
23
|
-
> See [React email templates](#react-email-templates).
|
|
24
|
-
|
|
25
|
-
## Configure email
|
|
26
|
-
|
|
27
|
-
Email is a **framework-reserved namespace of the tenant's environment**, the
|
|
28
|
-
same out-of-band [Environment](./environment.md) store that backs
|
|
29
|
-
`Environment.get` / `getSecure`, but the `[email]` block is **host-only**: it is
|
|
30
|
-
read and used in Rust (the off-core mailer) and is **never exposed to the
|
|
31
|
-
`.wasm`**. The provider key never lives in the deployed module or in the
|
|
32
|
-
inotify-watched `hosts/<host>.toml`.
|
|
33
|
-
|
|
34
|
-
On the edge today it lives in the tenant's env secrets file,
|
|
35
|
-
`$TOIL_ENV_DIR/<host>.env.secrets` (default dir `/run/toil/env`), kept `0600` and
|
|
36
|
-
**out of `hosts/`** so the config watcher never sees a credential (the dashboard /
|
|
37
|
-
edge DB replaces this file later). Email config is a set of **reserved
|
|
38
|
-
`TOIL_EMAIL_*` keys**, host-only, stripped from the guest buckets, so a tenant
|
|
39
|
-
can never read them via `Environment.getSecure`:
|
|
40
|
-
|
|
41
|
-
```bash
|
|
42
|
-
# $TOIL_ENV_DIR/<host>.env.secrets (mode 0600; NOT under hosts/, NOT in the .wasm)
|
|
43
|
-
|
|
44
|
-
TOIL_EMAIL_ENABLED=true
|
|
45
|
-
TOIL_EMAIL_FROM=you@example.com # validated; single address, no CRLF
|
|
46
|
-
TOIL_EMAIL_PROVIDER=resend # resend | gmail | smtp
|
|
47
|
-
TOIL_EMAIL_API_KEY=re_xxxxxxxxxxxx # the provider credential
|
|
48
|
-
TOIL_EMAIL_MAX_PER_MIN=60 # per-host send budget: rolling 1-minute cap
|
|
49
|
-
TOIL_EMAIL_MAX_PER_DAY=0 # per-host send budget: rolling 24-hour cap (0 = unlimited)
|
|
50
|
-
TOIL_EMAIL_MAX_PER_RECIPIENT_PER_HOUR=5 # anti-abuse cap per recipient
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
The same file also carries the tenant's own secrets (and `<host>.env` their plain
|
|
54
|
-
vars; see [Environment](./environment.md)); the `TOIL_EMAIL_*` keys are just the
|
|
55
|
-
reserved namespace the framework consumes.
|
|
56
|
-
|
|
57
|
-
When `enabled` is `false` (the default) the host has no email capability and
|
|
58
|
-
`EmailService.send` returns `Disabled`. The env is loaded **lazily** (on the
|
|
59
|
-
first send) and the `api_key` is materialized into a zeroizing secret in host
|
|
60
|
-
memory, never written to logs or `/_admin`. A malformed `[email]` block is
|
|
61
|
-
treated as "no email" (the host returns `Disabled`); validate config on the
|
|
62
|
-
dashboard before deploying.
|
|
63
|
-
|
|
64
|
-
### Providers
|
|
65
|
-
|
|
66
|
-
**Resend** (`provider = "resend"`), a JSON API; `api_key` holds the API key.
|
|
67
|
-
|
|
68
|
-
**Gmail** (`TOIL_EMAIL_PROVIDER=gmail`), SMTP with Gmail defaults:
|
|
69
|
-
`smtp.gmail.com`, port `587` (STARTTLS), username = `from`. `TOIL_EMAIL_API_KEY`
|
|
70
|
-
holds a Gmail **App Password** (create one at
|
|
71
|
-
<https://myaccount.google.com/apppasswords>; the account needs 2-Step
|
|
72
|
-
Verification). No extra keys needed:
|
|
73
|
-
|
|
74
|
-
```bash
|
|
75
|
-
TOIL_EMAIL_ENABLED=true
|
|
76
|
-
TOIL_EMAIL_FROM=you@gmail.com
|
|
77
|
-
TOIL_EMAIL_PROVIDER=gmail
|
|
78
|
-
TOIL_EMAIL_API_KEY=abcd efgh ijkl mnop
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
**Generic SMTP** (`TOIL_EMAIL_PROVIDER=smtp`), any submission server (Outlook,
|
|
82
|
-
SendGrid/Mailgun SMTP, your own). Requires `TOIL_EMAIL_SMTP_HOST`; port defaults
|
|
83
|
-
to `587` (STARTTLS), or set `465` for implicit TLS. `TOIL_EMAIL_SMTP_USER`
|
|
84
|
-
defaults to `from`.
|
|
85
|
-
|
|
86
|
-
```bash
|
|
87
|
-
TOIL_EMAIL_ENABLED=true
|
|
88
|
-
TOIL_EMAIL_FROM=noreply@example.com
|
|
89
|
-
TOIL_EMAIL_PROVIDER=smtp
|
|
90
|
-
TOIL_EMAIL_API_KEY=the-smtp-password
|
|
91
|
-
TOIL_EMAIL_SMTP_HOST=smtp.example.com
|
|
92
|
-
TOIL_EMAIL_SMTP_PORT=587
|
|
93
|
-
TOIL_EMAIL_SMTP_USER=noreply@example.com
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
### In dev
|
|
97
|
-
|
|
98
|
-
`toiljs dev` runs the **full email pipeline** in Node, recipient validation,
|
|
99
|
-
dedup, and the per-minute / per-day / per-recipient caps all behave exactly like
|
|
100
|
-
the edge, and **really sends** once you configure a provider. Configure it in
|
|
101
|
-
`toil.config.ts` (non-secret) with the API key in `.env.secrets`:
|
|
102
|
-
|
|
103
|
-
```ts
|
|
104
|
-
// toil.config.ts
|
|
105
|
-
import { defineConfig } from 'toiljs/compiler';
|
|
106
|
-
export default defineConfig({
|
|
107
|
-
server: {
|
|
108
|
-
email: { provider: 'resend', from: 'you@example.com', maxPerMin: 60 },
|
|
109
|
-
},
|
|
110
|
-
});
|
|
111
|
-
```
|
|
112
|
-
|
|
113
|
-
```bash
|
|
114
|
-
# .env.secrets (gitignored)
|
|
115
|
-
TOIL_EMAIL_API_KEY=re_xxxxxxxxxxxx
|
|
116
|
-
```
|
|
117
|
-
|
|
118
|
-
`TOIL_EMAIL_*` env vars override the config file (so the same `.env.secrets` the
|
|
119
|
-
edge uses works in dev too). Supports `resend` and `gmail`/`smtp` (SMTP via
|
|
120
|
-
nodemailer). **Not configured?** `EmailService.send` stays a log-only mock and
|
|
121
|
-
returns `Sent`, so a flow that sends email still works without setup.
|
|
122
|
-
|
|
123
|
-
> Because the dev server runs the guest **synchronously**, the actual network
|
|
124
|
-
> send is fire-and-forget: validation + caps return their exact status
|
|
125
|
-
> immediately, but a `Sent` is optimistic and the real delivery outcome (or
|
|
126
|
-
> `ProviderError`) is logged, not returned. The ABI is identical to the edge, so
|
|
127
|
-
> code that runs in dev runs on the edge.
|
|
128
|
-
|
|
129
|
-
## Sending email
|
|
130
|
-
|
|
131
|
-
```ts
|
|
132
|
-
import { Response, RouteContext } from 'toiljs/server/runtime';
|
|
133
|
-
|
|
134
|
-
@rest('notify')
|
|
135
|
-
class Notify {
|
|
136
|
-
@post('/welcome')
|
|
137
|
-
welcome(ctx: RouteContext): Response {
|
|
138
|
-
const status = EmailService.send(
|
|
139
|
-
'alice@example.com',
|
|
140
|
-
'Welcome!',
|
|
141
|
-
'Thanks for signing up.', // plain-text body
|
|
142
|
-
'welcome', // purpose tag (dedup / abuse keying)
|
|
143
|
-
'<h1>Thanks for signing up.</h1>', // optional HTML body
|
|
144
|
-
);
|
|
145
|
-
return status == EmailStatus.Sent
|
|
146
|
-
? Response.text('sent\n')
|
|
147
|
-
: Response.text('could not send\n', 503);
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
```
|
|
151
|
-
|
|
152
|
-
`send(to, subject, body, purpose = 'tx', html = '')` returns an **`EmailStatus`**:
|
|
153
|
-
|
|
154
|
-
| Status | Meaning | Retry? |
|
|
155
|
-
| ----------------- | ----------------------------------------------------------- | ---------------- |
|
|
156
|
-
| `Sent` | Accepted by the provider |, |
|
|
157
|
-
| `Deduped` | An identical recent `(recipient, purpose)` was collapsed | treat as sent |
|
|
158
|
-
| `Budget` | The host's per-minute budget is exhausted | yes, later |
|
|
159
|
-
| `TryLater` | The mailer was saturated / a queue was full | yes, back off |
|
|
160
|
-
| `RecipientCapped` | The per-recipient hourly cap was hit | no (this window) |
|
|
161
|
-
| `BadRecipient` | The address failed validation (CRLF, multiple addresses) | no |
|
|
162
|
-
| `Disabled` | This host has no `[email]` capability | no |
|
|
163
|
-
| `ProviderError` | The provider rejected it, or transport failed after retries | no |
|
|
164
|
-
|
|
165
|
-
`purpose` is a short, non-PII tag (`"welcome"`, `"reset"`, …). The mailer folds
|
|
166
|
-
it into the **dedup** key (identical `(host, recipient, purpose)` within ~30s is
|
|
167
|
-
collapsed to one send) and the abuse counters. It is never logged in the clear.
|
|
168
|
-
|
|
169
|
-
The recipient is validated host-side (exactly one address, no CR/LF/`<>`), so a
|
|
170
|
-
guest can never smuggle a second envelope recipient or a header into the send.
|
|
171
|
-
|
|
172
|
-
## Templates
|
|
173
|
-
|
|
174
|
-
`EmailTemplate` is a reusable message with `{{placeholder}}` substitution, for
|
|
175
|
-
when the same email is sent with different values:
|
|
176
|
-
|
|
177
|
-
```ts
|
|
178
|
-
const welcome = new EmailTemplate(
|
|
179
|
-
'Welcome, {{name}}!', // subject
|
|
180
|
-
'Hi {{name}}, your code is {{code}}.', // plain-text body
|
|
181
|
-
'<h1>Welcome, {{name}}</h1><p>Code: <b>{{code}}</b></p>', // html (optional)
|
|
182
|
-
);
|
|
183
|
-
|
|
184
|
-
const vars = new Map<string, string>();
|
|
185
|
-
vars.set('name', 'Alice');
|
|
186
|
-
vars.set('code', '123456');
|
|
187
|
-
const status = welcome.send('alice@example.com', vars, 'welcome');
|
|
188
|
-
```
|
|
189
|
-
|
|
190
|
-
- `{{ name }}` (with surrounding spaces) is accepted; an unknown placeholder
|
|
191
|
-
renders to the empty string.
|
|
192
|
-
- Omit the `html` argument for a plain-text template.
|
|
193
|
-
- `template.render(vars)` returns the rendered `{ subject, body, html }` without
|
|
194
|
-
sending (useful for preview/testing).
|
|
195
|
-
|
|
196
|
-
For anything richer than `{{token}}` substitution, real layout, CSS, brand,
|
|
197
|
-
author the email as a React component instead.
|
|
198
|
-
|
|
199
|
-
## React email templates
|
|
200
|
-
|
|
201
|
-
Write emails as React components in an **`emails/`** folder. At `toiljs build`
|
|
202
|
-
each one is rendered **once, at build time**, to static inline-CSS HTML (because
|
|
203
|
-
the inbox runs no JS), with the component's props turned into `{{token}}` holes;
|
|
204
|
-
the build then generates a typed `Emails.<Name>.send(...)` your server calls.
|
|
205
|
-
|
|
206
|
-
```tsx
|
|
207
|
-
// emails/Welcome.tsx
|
|
208
|
-
export const subject = 'Welcome, {{name}}!';
|
|
209
|
-
|
|
210
|
-
export default function Welcome({ name, code }: { name: string; code: string }) {
|
|
211
|
-
return (
|
|
212
|
-
<table
|
|
213
|
-
width="100%"
|
|
214
|
-
style={{ fontFamily: 'Arial, sans-serif' }}>
|
|
215
|
-
<tbody>
|
|
216
|
-
<tr>
|
|
217
|
-
<td style={{ padding: '24px' }}>
|
|
218
|
-
<h1 style={{ color: '#111' }}>Welcome, {name}!</h1>
|
|
219
|
-
<p>
|
|
220
|
-
Your code is <b>{code}</b>.
|
|
221
|
-
</p>
|
|
222
|
-
</td>
|
|
223
|
-
</tr>
|
|
224
|
-
</tbody>
|
|
225
|
-
</table>
|
|
226
|
-
);
|
|
227
|
-
}
|
|
228
|
-
```
|
|
229
|
-
|
|
230
|
-
The generated `Emails.Welcome.send(...)` takes the recipient, then one argument
|
|
231
|
-
per `{{token}}` **in alphabetical order**, then an optional `purpose`:
|
|
232
|
-
|
|
233
|
-
```ts
|
|
234
|
-
// emails/Welcome.tsx uses {{code}} and {{name}} -> params are (code, name)
|
|
235
|
-
const status = Emails.Welcome.send('alice@example.com', '123456', 'Alice');
|
|
236
|
-
```
|
|
237
|
-
|
|
238
|
-
Authoring notes:
|
|
239
|
-
|
|
240
|
-
- **Styles must end up inline.** Email clients strip `<style>`/external CSS, so
|
|
241
|
-
write inline `style={{ ... }}`, or import a stylesheet and its rules are
|
|
242
|
-
inlined into element `style="…"` for you at build (a bare CSS import has no
|
|
243
|
-
effect on its own under SSR). Keep email-only styles next to the email, e.g.
|
|
244
|
-
`import './styles/email.css'`, or **reuse existing project CSS** with `import
|
|
245
|
-
'client/styles/…'` (the `client/*` alias points at your client source).
|
|
246
|
-
- **Optional exports:** `export const subject` (a token template; defaults to the
|
|
247
|
-
email name), `export const text` (a plain-text alternative; otherwise derived
|
|
248
|
-
from the HTML), `export const purpose`.
|
|
249
|
-
- **Build-time, field substitution only.** Because the component renders once at
|
|
250
|
-
build, per-send data is `{{token}}` substitution, a runtime `{items.map(...)}`
|
|
251
|
-
or conditional bakes in at build, it does not re-run per recipient. That covers
|
|
252
|
-
transactional / 2FA / confirmation email; dynamic lists need a different
|
|
253
|
-
approach.
|
|
254
|
-
- The generated `server/_emails.ts` is regenerated on `build`/`dev` and should be
|
|
255
|
-
gitignored.
|
|
256
|
-
|
|
257
|
-
### Preview while you author
|
|
258
|
-
|
|
259
|
-
While `toiljs dev` runs, open **`/__toil/emails`** (the dev banner prints the
|
|
260
|
-
link). It lists every `emails/*.tsx`, renders the selected one exactly as the
|
|
261
|
-
build does (imported `client/*` CSS inlined), lets you fill each `{{token}}` to
|
|
262
|
-
see the result, toggle the HTML and plain-text parts, and open the file in your
|
|
263
|
-
editor. It refreshes live as you edit the template or its CSS.
|
|
264
|
-
|
|
265
|
-
## Email verification codes (`TwoFactor`)
|
|
266
|
-
|
|
267
|
-
`TwoFactor` is a **stateless** email-code primitive (2FA, email confirmation,
|
|
268
|
-
magic codes), no database. It emails a random code and returns a signed
|
|
269
|
-
**token** that commits to the code via HMAC, without putting the code in the
|
|
270
|
-
token (the code is only in the email). Verification recomputes the HMAC from the
|
|
271
|
-
token plus the user-entered code, so a valid `(token, code)` pair can only come
|
|
272
|
-
from someone who both received the email and holds the token.
|
|
273
|
-
|
|
274
|
-
```ts
|
|
275
|
-
// 1. Issue + email a code; hand `token` to the client (a cookie or hidden field).
|
|
276
|
-
const challenge = TwoFactor.send('alice@example.com', 'login'); // emails the code
|
|
277
|
-
// challenge.token -> give to the client
|
|
278
|
-
// challenge.status -> the EmailStatus of the send
|
|
279
|
-
|
|
280
|
-
// 2. Later, verify what the user typed.
|
|
281
|
-
const ok: bool = TwoFactor.verify(challenge.token, 'alice@example.com', userEntered);
|
|
282
|
-
```
|
|
283
|
-
|
|
284
|
-
- **`send(recipient, purpose, ttlSecs = 600, digits = 6)`**, issues a code,
|
|
285
|
-
emails it with a built-in template, returns `{ token, status }`.
|
|
286
|
-
- **`issue(recipient, purpose, ttlSecs, digits)`**, returns `{ code, token }`
|
|
287
|
-
**without** sending, so you can email `code` with your own `EmailTemplate` /
|
|
288
|
-
`Emails.*` for a branded message.
|
|
289
|
-
- **`verify(token, recipient, code)`**, `true` only for a code issued for that
|
|
290
|
-
recipient that hasn't expired. Constant-time compare.
|
|
291
|
-
- **`TwoFactor.setSecret(secret)`**, the HMAC secret for the tokens. Call once
|
|
292
|
-
at startup in `main.ts`; it must be identical on every edge instance and out of
|
|
293
|
-
any client bundle. (This is separate from the provider `api_key`.)
|
|
294
|
-
|
|
295
|
-
**Limitation:** this gives integrity + expiry but **not single-use**, a valid
|
|
296
|
-
code verifies repeatedly within its TTL, because there is no server state to burn
|
|
297
|
-
it. Keep the TTL short; for true single-use, store a per-recipient
|
|
298
|
-
last-verified-at and reject at or before it.
|
|
299
|
-
|
|
300
|
-
## Limits and abuse controls
|
|
301
|
-
|
|
302
|
-
All enforced authoritatively in the single mailer (so the counts are exact across
|
|
303
|
-
all workers):
|
|
304
|
-
|
|
305
|
-
- **Per-host budget**, two rolling windows, both enforced: a 1-minute cap
|
|
306
|
-
(`max_per_min`) and a 24-hour cap (`max_per_day`, `0` = unlimited). Over either
|
|
307
|
-
one → `Budget`. Each host's caps, in-window sends, and reject counts are visible
|
|
308
|
-
per host at `GET /_admin/email`.
|
|
309
|
-
- **Per-recipient cap**, `max_per_recipient_per_hour`. Over it →
|
|
310
|
-
`RecipientCapped`.
|
|
311
|
-
- **Dedup**, identical `(host, recipient, purpose)` within ~30s → `Deduped`.
|
|
312
|
-
|
|
313
|
-
Editing these in the host config takes effect on the next send (no restart).
|
|
314
|
-
|
|
315
|
-
## Observability
|
|
316
|
-
|
|
317
|
-
`GET /_admin/email` returns process-wide counters by reason (JSON), e.g.
|
|
318
|
-
`submitted`, `sent`, `deduped`, `budget`, `recipient_capped`, `try_later`,
|
|
319
|
-
`bad_recipient`, `provider_error`. It exposes **counts only**, never a
|
|
320
|
-
recipient, code, subject, body, or secret.
|
|
321
|
-
|
|
322
|
-
## See also
|
|
323
|
-
|
|
324
|
-
- [Rate limiting](./ratelimit.md), protect your routes (including any email
|
|
325
|
-
trigger) with `@ratelimit`.
|
|
326
|
-
- [Web Crypto](./crypto.md), the `crypto` global `TwoFactor` builds on.
|
package/docs/environment.md
DELETED
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
# Environment variables & secrets
|
|
2
|
-
|
|
3
|
-
`Environment` gives a tenant **per-app environment variables and secrets**, set
|
|
4
|
-
out of band (a dashboard, like GitHub Actions) so the deployed `.wasm` carries
|
|
5
|
-
**no credentials**. It is read-only from app code, there is no `set`; values are
|
|
6
|
-
configured on the deployment side, never from the module.
|
|
7
|
-
|
|
8
|
-
```ts
|
|
9
|
-
import { Response, RouteContext } from 'toiljs/server/runtime';
|
|
10
|
-
|
|
11
|
-
@rest('cfg')
|
|
12
|
-
class Cfg {
|
|
13
|
-
@get('/')
|
|
14
|
-
show(ctx: RouteContext): Response {
|
|
15
|
-
const base = Environment.get('PUBLIC_API_BASE'); // plain var, or null
|
|
16
|
-
const key = Environment.getSecure('STRIPE_KEY'); // secret, or null
|
|
17
|
-
// Use `key` to call a third party; never log it or return it to a client.
|
|
18
|
-
return Response.text(base != null ? base : 'unset');
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
`Environment` is a global, no import needed (like `EmailService` / `AuthService`).
|
|
24
|
-
|
|
25
|
-
## Two disjoint buckets
|
|
26
|
-
|
|
27
|
-
Just like GitHub Actions' `vars` vs `secrets`:
|
|
28
|
-
|
|
29
|
-
- **`Environment.get(key)`** reads **plain vars**, non-sensitive config (a public
|
|
30
|
-
API base URL, a feature flag, a region). Returns the string, or `null`.
|
|
31
|
-
- **`Environment.getSecure(key)`** reads **secrets**, sensitive values (a
|
|
32
|
-
third-party API key). Returns the string, or `null`.
|
|
33
|
-
|
|
34
|
-
The buckets are **disjoint**: a secret is **never** returned by `get()`, and a
|
|
35
|
-
plain var is never returned by `getSecure()`. That keeps a secret from leaking
|
|
36
|
-
through a code path that logs the result of a `get()`. Keys are case-sensitive,
|
|
37
|
-
exact-match.
|
|
38
|
-
|
|
39
|
-
> Secrets you read with `getSecure` are plaintext in your module at runtime
|
|
40
|
-
> (that's the point, you need them to call out). Don't log them, don't put them
|
|
41
|
-
> in a response, and don't copy them into a client bundle.
|
|
42
|
-
|
|
43
|
-
## What is NOT here
|
|
44
|
-
|
|
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
|
-
the `.wasm`**. There is no `Environment.email`; you configure email in the
|
|
48
|
-
`[email]` block of the same env file and the platform uses it for you (see
|
|
49
|
-
[Email](./email.md)). The env imports only ever see your own `vars` / `secrets`.
|
|
50
|
-
|
|
51
|
-
## Where values live
|
|
52
|
-
|
|
53
|
-
Vars and secrets live in **two separate dotenv (`.env`) files**, so the disjoint
|
|
54
|
-
split is structural and the secrets file can be locked down on its own. On the
|
|
55
|
-
edge they are per host, **out of `hosts/`** (so the config watcher never sees a
|
|
56
|
-
credential), the dashboard / edge database replaces them later:
|
|
57
|
-
|
|
58
|
-
```bash
|
|
59
|
-
# $TOIL_ENV_DIR/<host>.env (default dir /run/toil/env)
|
|
60
|
-
PUBLIC_API_BASE=https://api.example.com # -> Environment.get("PUBLIC_API_BASE")
|
|
61
|
-
REGION=eu
|
|
62
|
-
|
|
63
|
-
# $TOIL_ENV_DIR/<host>.env.secrets (mode 0600)
|
|
64
|
-
STRIPE_KEY=sk_live_xxx # -> Environment.getSecure("STRIPE_KEY")
|
|
65
|
-
|
|
66
|
-
# host-only email config, reserved TOIL_EMAIL_* keys, NEVER exposed to the .wasm
|
|
67
|
-
TOIL_EMAIL_ENABLED=true
|
|
68
|
-
TOIL_EMAIL_PROVIDER=resend
|
|
69
|
-
TOIL_EMAIL_FROM=noreply@example.com
|
|
70
|
-
TOIL_EMAIL_API_KEY=re_xxx
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
Each file is plain dotenv: `KEY=value` per line, `#` comments, optional `export`,
|
|
74
|
-
optional quotes. Keys with the reserved **`TOIL_`** prefix are framework/host-only
|
|
75
|
-
and are stripped from BOTH guest buckets, a tenant can never read them via
|
|
76
|
-
`get`/`getSecure` (see [Email](./email.md) for `TOIL_EMAIL_*`).
|
|
77
|
-
|
|
78
|
-
On the edge, env is loaded **lazily** (the first time your code reads it) into a
|
|
79
|
-
**bounded, shared, read-only cache** with idle eviction: the data lives in one
|
|
80
|
-
place and is never copied per request, a host that never reads env costs nothing,
|
|
81
|
-
secrets are wiped when a host goes cold, and a flood of requests to many distinct
|
|
82
|
-
hosts can never grow memory without bound.
|
|
83
|
-
|
|
84
|
-
## In dev
|
|
85
|
-
|
|
86
|
-
`toiljs dev` reads `.env` (vars) and `.env.secrets` (secrets) at your project
|
|
87
|
-
root, and overlays `process.env` as plain vars for convenience. Both are
|
|
88
|
-
gitignored by the scaffold. The ABI is identical to the edge, so code that runs
|
|
89
|
-
in dev runs on the edge.
|
|
90
|
-
|
|
91
|
-
```bash
|
|
92
|
-
# .env (vars)
|
|
93
|
-
PUBLIC_API_BASE=http://localhost:4000
|
|
94
|
-
|
|
95
|
-
# .env.secrets (secrets; 0600; gitignored)
|
|
96
|
-
STRIPE_KEY=sk_test_xxx
|
|
97
|
-
```
|
package/docs/getting-started.md
DELETED
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
# Getting started
|
|
2
|
-
|
|
3
|
-
A toiljs app has two halves that build and ship together:
|
|
4
|
-
|
|
5
|
-
- a **server** written in ToilScript, compiled to a single WebAssembly module
|
|
6
|
-
(`build/server/release.wasm`), and
|
|
7
|
-
- a **client** (Vite + React) that talks to the server through a generated,
|
|
8
|
-
fully typed `Server` proxy.
|
|
9
|
-
|
|
10
|
-
The server runs one fresh wasm instance per request, identically on the dev
|
|
11
|
-
server and on the edge. There is no Node.js in the request path: your handler is
|
|
12
|
-
wasm.
|
|
13
|
-
|
|
14
|
-
## Project layout
|
|
15
|
-
|
|
16
|
-
```
|
|
17
|
-
project/
|
|
18
|
-
toilconfig.json server (wasm) build config: entries, target, AS options
|
|
19
|
-
toil.config.ts client config (defineConfig: dev/build/SEO options)
|
|
20
|
-
|
|
21
|
-
server/
|
|
22
|
-
main.ts wires Server.handler, re-exports the wasm exports + abort
|
|
23
|
-
routes/*.ts @rest controllers (auto-discovered)
|
|
24
|
-
services/*.ts @service / @remote (auto-discovered)
|
|
25
|
-
core/AppHandler.ts your top-level ToilHandler
|
|
26
|
-
models/*.ts @data / @user classes
|
|
27
|
-
|
|
28
|
-
shared/
|
|
29
|
-
server.ts GENERATED by the server build (--rpcModule): the typed
|
|
30
|
-
client surface (Server proxy, @data codecs, getUser)
|
|
31
|
-
|
|
32
|
-
client/
|
|
33
|
-
routes/*.tsx file-based pages
|
|
34
|
-
layout.tsx, 404.tsx root layout / not-found
|
|
35
|
-
styles/*.css
|
|
36
|
-
|
|
37
|
-
build/
|
|
38
|
-
server/release.wasm compiled server (+ release.wat text form)
|
|
39
|
-
client/ Vite output
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
The compiler discovers every `.ts` under `server/` that declares a decorated
|
|
43
|
-
surface (`@rest`, `@service`, `@remote`, `@data`, `@user`) on its own. Importing
|
|
44
|
-
those modules from `main.ts` is still good practice: it keeps a direct
|
|
45
|
-
`toilscript` run (which only sees the `toilconfig.json` entries) building the
|
|
46
|
-
exact same server.
|
|
47
|
-
|
|
48
|
-
## `main.ts`
|
|
49
|
-
|
|
50
|
-
Three things are required, and the comments in the scaffold say so:
|
|
51
|
-
|
|
52
|
-
```ts
|
|
53
|
-
import { Server } from 'toiljs/server/runtime';
|
|
54
|
-
import { revertOnError } from 'toiljs/server/runtime/abort/abort';
|
|
55
|
-
|
|
56
|
-
import { AppHandler } from './core/AppHandler';
|
|
57
|
-
|
|
58
|
-
// Pull every decorated surface into a direct `toilscript` build.
|
|
59
|
-
import './routes/Players';
|
|
60
|
-
import './services/Stats';
|
|
61
|
-
|
|
62
|
-
// 1. The handler factory: one fresh handler instance per request.
|
|
63
|
-
Server.handler = () => new AppHandler();
|
|
64
|
-
|
|
65
|
-
// 2. Re-export the wasm entrypoints (`handle`, `render`).
|
|
66
|
-
export * from 'toiljs/server/runtime/exports';
|
|
67
|
-
|
|
68
|
-
// 3. The AssemblyScript trap hook.
|
|
69
|
-
export function abort(message: string, fileName: string, line: u32, column: u32): void {
|
|
70
|
-
revertOnError(message, fileName, line, column);
|
|
71
|
-
}
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
If all you need is `@rest` routing, your handler can be `RestHandler` (see
|
|
75
|
-
[Routing](./routing.md)) and you do not have to write an `AppHandler` at all.
|
|
76
|
-
|
|
77
|
-
## The request lifecycle
|
|
78
|
-
|
|
79
|
-
For each request the runtime (`server/runtime/exports`):
|
|
80
|
-
|
|
81
|
-
1. decodes the request envelope into a [`Request`](./routing.md#request),
|
|
82
|
-
2. publishes it ambiently as `Server.currentRequest` (so `AuthService.getUser()`
|
|
83
|
-
and friends can read its cookies with no argument),
|
|
84
|
-
3. builds the handler via `Server.handler()` and calls
|
|
85
|
-
`onRequestStarted` → `handle(req)` → `onRequestCompleted`,
|
|
86
|
-
4. encodes the returned [`Response`](./routing.md#response) and clears the
|
|
87
|
-
ambient request.
|
|
88
|
-
|
|
89
|
-
Because the instance is fresh and memory is wiped between requests, **nothing in
|
|
90
|
-
module globals survives across requests.** Anything that must persist (accounts,
|
|
91
|
-
sessions you do not put in a cookie, rate-limit counters) belongs in an external
|
|
92
|
-
store reached through a host binding.
|
|
93
|
-
|
|
94
|
-
## CLI
|
|
95
|
-
|
|
96
|
-
The `toiljs` CLI drives both halves:
|
|
97
|
-
|
|
98
|
-
| Command | What it does |
|
|
99
|
-
| ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
100
|
-
| `toiljs create [name]` | Scaffold a new app (templates, styling, options). |
|
|
101
|
-
| `toiljs dev` | Dev server with hot reload: watches `server/`, rebuilds the wasm via toilscript, regenerates `shared/server.ts`, and runs Vite for the client. Flags: `--root <dir>`, `--port <n>`, `--host`. |
|
|
102
|
-
| `toiljs build` | Production build: server wasm first (so `shared/server.ts` is fresh), then the Vite client + static prerender. Flags: `--root <dir>`, `--server` (server only). |
|
|
103
|
-
| `toiljs start` | Self-host a built app with production uWS/static workers, no Vite. Flags: `--root`, `--port`, `--host`, `--threads`. |
|
|
104
|
-
| `toiljs doctor` | Diagnose setup/deps (`--json`, `--fix`). |
|
|
105
|
-
|
|
106
|
-
In dev, requests whose method matches a dispatchable verb go into the wasm
|
|
107
|
-
first; if the guest reports "no route matched" (the `x-toil-unhandled` marker)
|
|
108
|
-
the request falls through to Vite, so client routes and assets just work
|
|
109
|
-
alongside your API.
|
|
110
|
-
|
|
111
|
-
## Building the server by hand
|
|
112
|
-
|
|
113
|
-
`toiljs build` runs toilscript for you, but you can invoke it directly (this is
|
|
114
|
-
what the examples do):
|
|
115
|
-
|
|
116
|
-
```sh
|
|
117
|
-
toilscript --target release --rpcModule shared/server.ts
|
|
118
|
-
```
|
|
119
|
-
|
|
120
|
-
`--target release` reads `toilconfig.json` and emits the wasm at
|
|
121
|
-
`targets.release.outFile`; `--rpcModule shared/server.ts` writes the generated
|
|
122
|
-
typed client (see [RPC](./rpc.md)).
|
|
123
|
-
|
|
124
|
-
## Next
|
|
125
|
-
|
|
126
|
-
- [Routing](./routing.md) to expose HTTP endpoints.
|
|
127
|
-
- [Data codec](./data.md) for request/response bodies.
|
|
128
|
-
- [Auth](./auth.md) for login and sessions.
|
package/docs/index.md
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
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
|
-
- Compute tiers: the server can span L1 request (`server/main.ts`, `@rest`/`@service`/`@remote`),
|
|
25
|
-
L2/L3 stream (`server/main.stream.ts`, `@stream`), and L4 daemon (`server/main.daemon.ts`,
|
|
26
|
-
`@daemon`/`@scheduled`); each tier compiles into its own artifact. See [tiers.md](./tiers.md).
|
|
27
|
-
|
|
28
|
-
See [routing.md](./routing.md), [client.md](./client.md), [styling.md](./styling.md),
|
|
29
|
-
[server.md](./server.md), [ssr.md](./ssr.md), [rpc.md](./rpc.md), [tiers.md](./tiers.md),
|
|
30
|
-
[streams.md](./streams.md), [daemon.md](./daemon.md), [derive.md](./derive.md), [cli.md](./cli.md).
|
package/docs/ratelimit.md
DELETED
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
# Rate limiting
|
|
2
|
-
|
|
3
|
-
The `@ratelimit` decorator throttles **any** `@rest` route, a login, a signup, a
|
|
4
|
-
public API, an email trigger, anything. It is enforced at the edge, before your
|
|
5
|
-
handler runs, and keyed by default on the connecting client's **unspoofable** IP,
|
|
6
|
-
so it works as an abuse / brute-force control out of the box.
|
|
7
|
-
|
|
8
|
-
It composes with the other route decorators and is independent of email or auth.
|
|
9
|
-
|
|
10
|
-
## Using `@ratelimit`
|
|
11
|
-
|
|
12
|
-
Add it to a route alongside the verb decorator:
|
|
13
|
-
|
|
14
|
-
```ts
|
|
15
|
-
import { Response, RouteContext } from 'toiljs/server/runtime';
|
|
16
|
-
|
|
17
|
-
@rest('auth')
|
|
18
|
-
class Auth {
|
|
19
|
-
// At most 5 login attempts per 60 seconds per client IP.
|
|
20
|
-
@ratelimit(RateLimit.SlidingWindow, 5, 60)
|
|
21
|
-
@post('/login')
|
|
22
|
-
login(ctx: RouteContext): Response {
|
|
23
|
-
// ... only runs if under the limit ...
|
|
24
|
-
return Response.text('ok\n');
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
`@ratelimit(strategy, limit, window)`:
|
|
30
|
-
|
|
31
|
-
- **`strategy`**, a `RateLimit` value (ambient global, no import):
|
|
32
|
-
`RateLimit.FixedWindow`, `RateLimit.SlidingWindow`, or `RateLimit.TokenBucket`.
|
|
33
|
-
- **`limit`** and **`window`**, integer literals whose meaning depends on the
|
|
34
|
-
strategy (see below).
|
|
35
|
-
|
|
36
|
-
When a request is over the limit the edge returns **`429 Too Many Requests`**
|
|
37
|
-
with a **`Retry-After`** header (whole seconds), and your handler never runs. The
|
|
38
|
-
guard runs **before `@auth`**, so unauthenticated floods are limited too.
|
|
39
|
-
|
|
40
|
-
> Both arguments must be **integer literals** and the strategy a `RateLimit`
|
|
41
|
-
> member (or a bare integer tag). A malformed decorator emits no guard rather
|
|
42
|
-
> than miscompiling, the same fail-safe rule as `@cache`.
|
|
43
|
-
|
|
44
|
-
## Strategies
|
|
45
|
-
|
|
46
|
-
| Strategy | `limit`, `window` mean | Behavior |
|
|
47
|
-
| --- | --- | --- |
|
|
48
|
-
| `FixedWindow` | `limit` events per `window` seconds | Cheapest. Counts in aligned wall-clock buckets; a caller hammering a boundary can briefly get up to ~2× `limit` across two adjacent windows. |
|
|
49
|
-
| `SlidingWindow` | `limit` events per `window` seconds | Smooths the fixed-window boundary by weighting the previous window. Best general choice for "N per period". |
|
|
50
|
-
| `TokenBucket` | `limit` = burst size, `window` = refill **per second** | Allows an initial burst of `limit`, then a steady `window` tokens/sec. Good for bursty-but-bounded APIs. |
|
|
51
|
-
|
|
52
|
-
Examples:
|
|
53
|
-
|
|
54
|
-
```ts
|
|
55
|
-
// 100 requests / minute, smoothed:
|
|
56
|
-
@ratelimit(RateLimit.SlidingWindow, 100, 60)
|
|
57
|
-
|
|
58
|
-
// Burst of 20, then 5 per second sustained:
|
|
59
|
-
@ratelimit(RateLimit.TokenBucket, 20, 5)
|
|
60
|
-
|
|
61
|
-
// Exactly 3 per hour, cheapest:
|
|
62
|
-
@ratelimit(RateLimit.FixedWindow, 3, 3600)
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
## How requests are keyed
|
|
66
|
-
|
|
67
|
-
By default the limiter keys on the **client IP**, specifically the TCP peer
|
|
68
|
-
address the edge observed (`ctx.clientIp()`), **not** a header like
|
|
69
|
-
`X-Forwarded-For`, which a client can forge. That makes it a real abuse control:
|
|
70
|
-
a caller can't reset their bucket by spoofing a header.
|
|
71
|
-
|
|
72
|
-
The count is **exact across all 14 edge workers** (a given IP always maps to one
|
|
73
|
-
authoritative shard), so the limit is global per route, not per worker. Only
|
|
74
|
-
routes that opt in with `@ratelimit` ever pay anything, the lock-free fast path
|
|
75
|
-
for everything else is untouched.
|
|
76
|
-
|
|
77
|
-
> Each rate-limited route has its own independent limiter, a limit on `/login`
|
|
78
|
-
> does not consume the budget of `/signup`.
|
|
79
|
-
|
|
80
|
-
## Notes and limits
|
|
81
|
-
|
|
82
|
-
- **Route-level only.** Put `@ratelimit` on each route you want limited; there is
|
|
83
|
-
no controller-wide form yet (unlike `@auth`).
|
|
84
|
-
- **Keyed on IP.** The decorator keys on the peer IP today. (A per-user / custom
|
|
85
|
-
key, limiting by account instead of IP, exists in the runtime but is not yet
|
|
86
|
-
exposed through the decorator.)
|
|
87
|
-
- **In dev.** `toiljs dev` runs a single-process mirror of the same three
|
|
88
|
-
strategies, so a limited route behaves the same locally as on the edge.
|
|
89
|
-
|
|
90
|
-
## See also
|
|
91
|
-
|
|
92
|
-
- [Email](./email.md), `@ratelimit` pairs well with email triggers (verification
|
|
93
|
-
codes, password resets) to blunt abuse.
|
|
94
|
-
- [Auth, sessions, and `@user`](./auth.md), `@ratelimit` runs before the `@auth`
|
|
95
|
-
guard, so it protects the login itself.
|