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.
Files changed (132) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/README.md +2 -2
  3. package/build/cli/.tsbuildinfo +1 -1
  4. package/build/cli/index.js +303 -293
  5. package/build/compiler/.tsbuildinfo +1 -1
  6. package/build/compiler/config.d.ts +2 -0
  7. package/build/compiler/config.js +1 -0
  8. package/build/compiler/docs.js +8 -24
  9. package/build/compiler/generate.js +4 -2
  10. package/build/compiler/index.d.ts +1 -1
  11. package/build/compiler/index.js +69 -6
  12. package/build/compiler/toil-docs.generated.js +64 -22
  13. package/build/devserver/.tsbuildinfo +1 -1
  14. package/build/devserver/analytics/index.js +7 -3
  15. package/build/devserver/db/database.d.ts +4 -0
  16. package/build/devserver/db/database.js +43 -1
  17. package/build/devserver/db/index.d.ts +1 -1
  18. package/build/devserver/db/index.js +1 -1
  19. package/build/devserver/db/types.d.ts +3 -0
  20. package/build/devserver/db/types.js +2 -0
  21. package/build/devserver/runtime/module.js +4 -1
  22. package/docs/README.md +104 -65
  23. package/docs/auth/README.md +102 -0
  24. package/docs/auth/configuration.md +94 -0
  25. package/docs/auth/extending.md +202 -0
  26. package/docs/auth/how-it-works.md +138 -0
  27. package/docs/auth/usage.md +188 -0
  28. package/docs/backend/README.md +143 -0
  29. package/docs/backend/data.md +351 -0
  30. package/docs/backend/rest.md +402 -0
  31. package/docs/backend/rpc.md +226 -0
  32. package/docs/background/README.md +114 -0
  33. package/docs/background/daemons.md +230 -0
  34. package/docs/background/derive.md +179 -0
  35. package/docs/cli/README.md +377 -0
  36. package/docs/concepts/config.md +416 -0
  37. package/docs/concepts/decorators.md +127 -0
  38. package/docs/concepts/security.md +108 -0
  39. package/docs/concepts/tiers.md +166 -0
  40. package/docs/concepts/types.md +216 -0
  41. package/docs/database/README.md +143 -0
  42. package/docs/database/capacity.md +350 -0
  43. package/docs/database/counters.md +174 -0
  44. package/docs/database/documents.md +255 -0
  45. package/docs/database/events.md +307 -0
  46. package/docs/database/membership.md +246 -0
  47. package/docs/database/setup.md +155 -0
  48. package/docs/database/unique.md +216 -0
  49. package/docs/database/views.md +246 -0
  50. package/docs/frontend/README.md +101 -0
  51. package/docs/frontend/data-fetching.md +243 -0
  52. package/docs/frontend/images.md +148 -0
  53. package/docs/frontend/metadata.md +344 -0
  54. package/docs/frontend/rendering.md +236 -0
  55. package/docs/frontend/routing.md +344 -0
  56. package/docs/frontend/scripts.md +118 -0
  57. package/docs/frontend/search.md +191 -0
  58. package/docs/frontend/styling.md +147 -0
  59. package/docs/getting-started/README.md +81 -0
  60. package/docs/getting-started/create-project.md +131 -0
  61. package/docs/getting-started/deploy.md +101 -0
  62. package/docs/getting-started/first-app.md +215 -0
  63. package/docs/getting-started/installation.md +106 -0
  64. package/docs/getting-started/migrating.md +125 -0
  65. package/docs/getting-started/project-structure.md +163 -0
  66. package/docs/introduction/README.md +41 -0
  67. package/docs/introduction/design-principles.md +55 -0
  68. package/docs/introduction/distributed.md +74 -0
  69. package/docs/introduction/how-it-works.md +74 -0
  70. package/docs/introduction/hyperscale.md +51 -0
  71. package/docs/introduction/modern-stack.md +36 -0
  72. package/docs/introduction/vs-other-frameworks.md +48 -0
  73. package/docs/introduction/why-toil.md +93 -0
  74. package/docs/llms.txt +90 -0
  75. package/docs/realtime/README.md +102 -0
  76. package/docs/realtime/channels.md +211 -0
  77. package/docs/realtime/streams.md +369 -0
  78. package/docs/services/README.md +60 -0
  79. package/docs/services/analytics.md +268 -0
  80. package/docs/services/caching.md +175 -0
  81. package/docs/services/cookies.md +235 -0
  82. package/docs/services/crypto.md +209 -0
  83. package/docs/services/email.md +289 -0
  84. package/docs/services/environment.md +141 -0
  85. package/docs/services/ratelimit.md +174 -0
  86. package/docs/services/time.md +85 -0
  87. package/examples/basic/client/routes/analytics.tsx +13 -12
  88. package/examples/basic/server/models/SiteAnalytics.ts +29 -17
  89. package/examples/basic/server/routes/Analytics.ts +15 -17
  90. package/examples/basic/server/routes/UserId.ts +22 -0
  91. package/package.json +15 -11
  92. package/scripts/gen-toil-docs.mjs +24 -35
  93. package/server/auth/AuthController.ts +336 -0
  94. package/server/auth/AuthUser.ts +23 -0
  95. package/server/auth/index.ts +16 -0
  96. package/server/globals/auth.ts +31 -0
  97. package/server/globals/userid.ts +107 -0
  98. package/src/compiler/config.ts +13 -0
  99. package/src/compiler/docs.ts +16 -33
  100. package/src/compiler/generate.ts +6 -2
  101. package/src/compiler/index.ts +114 -6
  102. package/src/compiler/toil-docs.generated.ts +64 -22
  103. package/src/devserver/analytics/index.ts +10 -4
  104. package/src/devserver/db/database.ts +67 -1
  105. package/src/devserver/db/index.ts +1 -0
  106. package/src/devserver/db/types.ts +13 -0
  107. package/src/devserver/runtime/module.ts +7 -0
  108. package/test/analytics-dev.test.ts +2 -1
  109. package/test/devserver-database.test.ts +113 -0
  110. package/docs/auth-todo.md +0 -149
  111. package/docs/auth.md +0 -322
  112. package/docs/caching.md +0 -115
  113. package/docs/cli.md +0 -17
  114. package/docs/client.md +0 -39
  115. package/docs/cookies.md +0 -457
  116. package/docs/crypto.md +0 -130
  117. package/docs/daemon.md +0 -123
  118. package/docs/data.md +0 -131
  119. package/docs/derive.md +0 -159
  120. package/docs/email.md +0 -326
  121. package/docs/environment.md +0 -97
  122. package/docs/getting-started.md +0 -128
  123. package/docs/index.md +0 -30
  124. package/docs/ratelimit.md +0 -95
  125. package/docs/routing.md +0 -259
  126. package/docs/rpc.md +0 -149
  127. package/docs/server.md +0 -61
  128. package/docs/ssr.md +0 -632
  129. package/docs/streams.md +0 -178
  130. package/docs/styling.md +0 -22
  131. package/docs/tiers.md +0 -133
  132. 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.
@@ -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
- ```
@@ -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.