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/auth.md DELETED
@@ -1,322 +0,0 @@
1
- # Auth, sessions, and `@user`
2
-
3
- toiljs ships **Toil PQ-Auth**: a post-quantum password login where the password
4
- never leaves the browser and the server stores only public verifier material.
5
- On top of it sit HMAC-signed session cookies, a `@auth` route guard, and a
6
- `@user` type that makes the signed-in user available - fully typed, no type
7
- argument - on both the server (`AuthService.getUser()`) and the generated client
8
- (`getUser()`).
9
-
10
- > **Status.** PQ-Auth is a hybrid construction (see [What it is](#what-it-is)).
11
- > It is opt-in and **not hardened to production yet** - the example storage is a
12
- > dev stand-in, the secrets are dev placeholders, and the composition has not had
13
- > an external cryptographic review. See [`docs/auth-todo.md`](./auth-todo.md) for
14
- > the remaining work before it backs real credentials.
15
-
16
- `AuthService` is an ambient global (no import). The pieces:
17
-
18
- - **`@user`** - declares the authenticated user's shape and registers it as *the* user type.
19
- - **`@auth`** - guards a route (or a whole `@rest` class): a valid session is required or `401`.
20
- - **`AuthService`** - the server runtime: the PQ-Auth crypto, plus mint/read/clear a session and `getUser()`.
21
- - **client `Auth` + generated `getUser()`** - run the login from the browser and read the user for display.
22
-
23
- ---
24
-
25
- ## What it is
26
-
27
- A password is a weak, low-entropy secret. PQ-Auth turns it into a strong,
28
- **post-quantum** credential and proves possession to the server without the
29
- server (or anyone on the wire, or a future quantum adversary) ever seeing
30
- anything they can replay. It is built from three independent ideas, each
31
- defending a specific attack:
32
-
33
- | Layer | Primitive | Defends against |
34
- | --- | --- | --- |
35
- | **Keyed salt** | OPRF (RFC 9497, ristretto255-SHA512) | A breached server (or a passive observer) **precomputing** a password dictionary. |
36
- | **Credential** | Argon2id → ML-DSA-44 (FIPS 204) keypair | The password ever crossing the wire; a stolen verifier being usable without an expensive per-guess attack. |
37
- | **Mutual auth + key** | ML-KEM-768 (FIPS 203) | A phishing/MITM server impersonating the real one; a session with no key to bind to. |
38
-
39
- The password is stretched into a signing keypair entirely client-side; only the
40
- **public** key is registered. Login is a challenge-response signature *plus* a
41
- key encapsulation, so both parties authenticate each other. Authentication
42
- (ML-DSA) and key agreement (ML-KEM) are post-quantum; the keyed-salt OPRF is
43
- classical ristretto255 (the one non-PQ layer - a quantum break of it degrades to
44
- a post-breach offline attack, no worse than a plain salt, while defeating
45
- precomputation for everyone else).
46
-
47
- ### Why a keyed salt (the OPRF)
48
-
49
- A normal salted hash (`Argon2id(password, salt)`) lets anyone who learns the
50
- salt - including a future attacker who simply asks the login endpoint for it -
51
- **precompute** a dictionary offline and crack the stored verifier the instant
52
- they breach it. PQ-Auth replaces the salt with the output of a **server-keyed**
53
- OPRF:
54
-
55
- ```
56
- oprfOutput = OPRF_finalize(password, OPRF_evaluate(k_user, blind(password)))
57
- seed = Argon2id(oprfOutput, salt)
58
- ```
59
-
60
- The client **blinds** the password (so the server learns nothing about it),
61
- the server **evaluates** the blinded element under a per-user key `k_user`
62
- derived from a server-secret master seed, and the client **unblinds** to recover
63
- a deterministic, high-entropy `oprfOutput`. Because `k_user` is a server secret,
64
- **no offline work is possible until that secret leaks** - precomputation is
65
- impossible, and even a passive observer who captures a login learns nothing.
66
- The per-user key (`k_user = DeriveKeyPair(masterSeed, username)`) means two
67
- accounts with the same password get different outputs - no cross-account
68
- password-equality leak.
69
-
70
- ### Why a password-derived signing key
71
-
72
- `seed = Argon2id(oprfOutput, salt)` deterministically expands into an
73
- **ML-DSA-44 keypair**. The client registers only the 1312-byte **public** key;
74
- the secret key and seed are zeroized the instant signing is done. The server
75
- stores the public key as a verifier and can only ever *verify* - it never holds
76
- a secret (`crypto.mldsa_verify` is verify-only on the edge). A full server breach
77
- yields public keys, not passwords; recovering a password still requires an
78
- offline Argon2id dictionary attack **and** the leaked OPRF master seed.
79
-
80
- ### Why ML-KEM (mutual auth + session key)
81
-
82
- A signature proves the *client* to the server, but nothing proves the *server*
83
- to the client. PQ-Auth pins the server's static **ML-KEM-768 public key** in the
84
- client. At login the client **encapsulates** a shared secret to that key; only
85
- the genuine server (holding the matching secret key) can **decapsulate** it. Both
86
- sides derive the same session key and the server returns a confirmation tag the
87
- client checks - so a phishing/MITM server that lacks the secret key cannot
88
- complete the handshake.
89
-
90
- ---
91
-
92
- ## Flow at a glance
93
-
94
- ```mermaid
95
- sequenceDiagram
96
- autonumber
97
- actor U as User
98
- participant C as Browser
99
- participant S as Edge wasm
100
- participant DB as Your store
101
-
102
- rect rgb(14, 21, 32)
103
- Note over U,DB: Register, password never leaves the browser
104
- U->>C: Auth.register(username, password)
105
- C->>S: POST /auth/register/start (username, blinded)
106
- S->>S: OPRF-evaluate under k_user, issue salt and KDF params
107
- S-->>C: salt, params, evaluated
108
- C->>C: finalize OPRF, Argon2id, ML-DSA-44 keypair, sign PoP
109
- C->>S: POST /auth/register/finish (username, publicKey, regProof)
110
- S->>S: verifyRegister(publicKey, PoP)
111
- S->>DB: store Account (username, salt, params, publicKey)
112
- S-->>C: ok
113
- end
114
-
115
- rect rgb(22, 15, 31)
116
- Note over U,DB: Login, mutual authentication
117
- U->>C: Auth.login(username, password)
118
- C->>S: POST /auth/login/start (username, blinded)
119
- S->>DB: store challenge (cid, nonce, iat, exp)
120
- S-->>C: cid, aud, salt, params, nonce, iat, exp, evaluated
121
- C->>C: finalize OPRF, derive seed, ML-DSA keypair, ML-KEM encapsulate, sign M
122
- C->>S: POST /auth/login/finish (cid, ct, signature)
123
- S->>DB: atomic consume challenge(cid)
124
- S->>S: rebuild M, verifyLogin, decapsulate, derive K, build confirm
125
- alt signature valid
126
- S-->>C: ok, sessionToken, serverConfirm, Set-Cookie
127
- C->>C: re-derive K, check serverConfirm, server authenticated
128
- else invalid or unknown user
129
- S-->>C: 401 generic, anti-enumeration
130
- end
131
- end
132
-
133
- rect rgb(13, 25, 18)
134
- Note over U,DB: Guarded request, the @auth guard
135
- U->>C: open or call an @auth route
136
- C->>S: request, cookies sent automatically
137
- S->>S: @auth verifies HMAC and expiry on __Host-toil_sess
138
- alt valid session
139
- S->>S: handler runs, AuthService.getUser() returns the @user
140
- S-->>C: 200
141
- else missing or invalid
142
- S-->>C: 401 before handler and body-decode
143
- end
144
- end
145
- ```
146
-
147
- ### The signed transcript
148
-
149
- The login message `M` the client signs (and the server rebuilds from its own
150
- stored values) is a single fixed binary layout - no JSON, no version negotiation:
151
-
152
- ```
153
- u8 tag = 1
154
- str sub (username)
155
- str aud (service audience; server constant)
156
- bytes cid (challenge id)
157
- bytes nonce (32 random bytes, server-issued)
158
- u64 iat, u64 exp (challenge validity window)
159
- bytes ct (ML-KEM ciphertext)
160
- u32 memKiB, iterations, parallelism (Argon2id params)
161
- bytes serverKemKeyId (SHA-256 of the server KEM public key)
162
- ```
163
-
164
- Signing over all of this binds the login to: the exact challenge (so it can't be
165
- replayed - and `cid` is consumed atomically), the **ciphertext** (so a MITM can't
166
- swap the key encapsulation), the **KDF params** (so a downgrade can't be slipped
167
- past the signature), and the **server key identity** (so it commits to which
168
- server key was used). The mutual-auth tag is then:
169
-
170
- ```
171
- K = HMAC-SHA256(sharedSecret, "toil-session-key-v1" || SHA-256(M))
172
- confirm = HMAC-SHA256(K, "toil-server-confirm-v1" || SHA-256(M))
173
- ```
174
-
175
- `K` is the authenticated session key, derived from the KEM shared secret and
176
- bound to the transcript. Only a server that decapsulated correctly derives the
177
- same `K`, so the client checking `confirm` proves the server's identity. (`K` is
178
- the handle for future channel binding; binding the session *cookie* to the
179
- transport needs the TLS exporter, which the wasm guest can't see - a follow-up.)
180
-
181
- ### Anti-enumeration
182
-
183
- `login/start` returns a fully-formed response for **every** username: it always
184
- OPRF-evaluates (a real `k_user` for known users, a deterministic decoy key for
185
- unknown ones) and returns a **deterministic per-user salt** and constant params.
186
- Known and unknown users are byte-indistinguishable, and the eventual signature
187
- simply fails for a non-account. Failures return one generic `401`.
188
-
189
- ---
190
-
191
- ## `@user`
192
-
193
- Mark one class per program as the user type. It becomes a `@data` codec (so it
194
- serializes into the session) and the return type of `getUser()` everywhere.
195
-
196
- ```ts
197
- @user
198
- class Account {
199
- username: string = '';
200
- admin: bool = false;
201
- score: u64 = 0;
202
- }
203
- ```
204
-
205
- There is exactly one `@user` per program; a second is a compile error.
206
-
207
- ## `@auth`
208
-
209
- Put `@auth` on a route, or on the `@rest` class to guard every route in it. The
210
- generated dispatcher checks for a valid, unexpired session **before** the handler
211
- runs (and before any body-decode or cache write); without one it returns `401`.
212
-
213
- ```ts
214
- @rest('session')
215
- class Session {
216
- @auth
217
- @get('/me')
218
- public me(): Response {
219
- const u = AuthService.getUser(); // Account | null, auto-typed
220
- if (u == null) return Response.text('no session\n', 401);
221
- return Response.bytes(new DataWriter()
222
- .writeString(u.username).writeBool(u.admin).writeU64(u.score).toBytes());
223
- }
224
- }
225
- ```
226
-
227
- `@auth` on the class form guards all routes in it.
228
-
229
- ## `AuthService` (server)
230
-
231
- A global namespace. Session methods read the ambient request
232
- (`Server.currentRequest`), so `getUser()`/`hasSession()` take no argument and are
233
- only meaningful during a dispatch.
234
-
235
- ### PQ-Auth crypto
236
-
237
- Startup config (call once in `main.ts`; identical on every edge instance; never
238
- in a client bundle):
239
-
240
- | Member | Notes |
241
- | --- | --- |
242
- | `setSecret(secret)` | HMAC secret for session cookies. |
243
- | `setOprfSeed(seed)` | 32-byte OPRF master seed; per-user keys derive from this + the username. |
244
- | `setServerKemSecretKey(sk)` | Server static ML-KEM-768 secret key (2400 B) used to decapsulate. |
245
- | `setServerKemPublicKey(pk)` | The matching public key (1184 B) for `serverKemKeyId`; it is embedded in `sk` at bytes `[1152, 2336)`, so you can pass `sk.slice(1152, 2336)`. |
246
-
247
- Per-request building blocks:
248
-
249
- | Member | Notes |
250
- | --- | --- |
251
- | `oprfEvaluate(username, blinded)` | OPRF server step: blind-evaluate under `k_user` derived from the seed + username. Returns the 32-byte evaluated element. |
252
- | `mlkemDecapsulate(ct)` | Recover the 32-byte shared secret from the client ciphertext with the server secret key. |
253
- | `buildLoginMessage(sub, aud, cid, nonce, iat, exp, ct, memKiB, iterations, parallelism, serverKemKeyId)` | The canonical login message `M`. Call it with the server's **own** stored values, never client-echoed fields. |
254
- | `verifyLogin(publicKey, message, signature)` | Verify the ML-DSA login signature under `LOGIN_CONTEXT`. |
255
- | `serverKemKeyId()` | `SHA-256(serverKemPublicKey)` - the key id bound into `M`. |
256
- | `sha256(data)` | SHA-256, for the transcript hash. |
257
- | `deriveSessionKey(sharedSecret, transcriptHash)` | `K = HMAC(sharedSecret, SESSION_KEY_LABEL || transcriptHash)`. |
258
- | `serverConfirmTag(sessionKey, transcriptHash)` | The mutual-auth tag `HMAC(K, SERVER_CONFIRM_LABEL || transcriptHash)`. |
259
- | `buildRegisterMessage(username, publicKey)` / `verifyRegister(...)` | Registration proof-of-possession (under `REGISTER_CONTEXT`). |
260
- | `LOGIN_CONTEXT` / `REGISTER_CONTEXT` | `qauth:login:v1` / `qauth:register:v1` - FIPS 204 signing contexts. |
261
- | `PUBLIC_KEY_LEN` `SIGNATURE_LEN` `KEM_*` `SHARED_SECRET_LEN` `OPRF_*` | Fixed sizes. |
262
-
263
- The full register/login orchestration (the four binary endpoints, the
264
- anti-enumeration decoy, the atomic challenge-consume) is in
265
- `examples/basic/server/routes/Auth.ts`. **Storage is the app's** - a tenant's
266
- wasm memory is wiped per request, so accounts and challenges live in an external
267
- store, and challenge-consume **must** be an atomic fetch-and-delete (a
268
- read-then-delete race makes a captured login replayable). The example uses a
269
- **dev-only** KV for this; production wires toildb (see `docs/auth-todo.md`).
270
-
271
- ### Sessions
272
-
273
- | Member | Signature | Notes |
274
- | --- | --- | --- |
275
- | `getUser()` | `(): AuthUser \| null` | The signed-in user, decoded from the verified session, auto-typed to your `@user`. |
276
- | `hasSession()` | `(): bool` | Whether the request carries a valid, unexpired session. What `@auth` calls. |
277
- | `mintSession(userData, ttlSecs?)` | `(Uint8Array, u64=86400): Cookie` | Signed `__Host-toil_sess` cookie carrying `user.encode()`. HttpOnly, Secure, SameSite=Lax. |
278
- | `clearSession()` / `userCookie(...)` / `clearUserCookie()` | | Logout; the readable `__Secure-toil_user` companion (display-only); clear it. |
279
-
280
- The session payload is `u8 version || u64 iat || u64 exp || bytes userData`, sealed
281
- with HMAC-SHA256. The HttpOnly `__Host-toil_sess` is the **only** cookie the
282
- server trusts; the readable `__Secure-toil_user` exists solely so the client
283
- `getUser()` can show a name without a round-trip and must never gate anything.
284
-
285
- ## The client half
286
-
287
- ```ts
288
- import { Auth } from 'toiljs/client';
289
-
290
- await Auth.register(username, password); // OPRF + Argon2id + ML-DSA keypair, send only the public key + PoP
291
- await Auth.login(username, password); // + ML-KEM encapsulate; resolves only if the server's confirm tag verifies
292
- ```
293
-
294
- `login` resolves **only after** the client verifies the server's confirmation tag
295
- - so a resolved `login` means mutual authentication succeeded. The secret key,
296
- seed, and shared secret are zeroized as soon as they are used. There is no
297
- recovery: the password *is* the key (see `docs/auth-todo.md` for the recovery
298
- work).
299
-
300
- The generated `shared/server.ts` also exports a typed, no-argument client
301
- `getUser()` that reads the readable companion cookie. It is **display-only and
302
- untrusted** - a client can forge it, fooling only its own UI. The server
303
- re-verifies the signed session on every `@auth` request, so authorization never
304
- depends on the readable cookie.
305
-
306
- ## Security checklist
307
-
308
- - Set real secrets in `main.ts`: `setSecret`, `setOprfSeed`, and the server KEM
309
- keypair - per-deployment, identical on every instance, never in a client
310
- bundle. The defaults are insecure DEV placeholders.
311
- - Pin **your** server KEM public key in the client and rotate it; the example
312
- ships a throwaway dev keypair.
313
- - Use a production Argon2id cost (≥ 256 MiB, ≥ 3 iterations); the demo is tuned
314
- for browser responsiveness.
315
- - Back accounts/challenges with a shared store and make challenge-consume atomic.
316
- - Rate-limit `register` and `login` (online guessing is not stopped by the
317
- offline-attack resistance).
318
- - Always verify server-side. The server `getUser()` decodes a verified,
319
- expiry-checked session; the client `getUser()` does not and must not gate
320
- anything.
321
- - This is an unreviewed hybrid composition - get a cryptographic review before it
322
- backs real credentials. Tracked in [`docs/auth-todo.md`](./auth-todo.md).
package/docs/caching.md DELETED
@@ -1,115 +0,0 @@
1
- # Caching
2
-
3
- toiljs can cache a response at the edge (shared, across users) and instruct the
4
- browser to cache it too. You opt in per route, either declaratively with the
5
- `@cache` decorator or imperatively with `Response.cache(...)`. The edge keys a
6
- cached entry by host, method, path, and body hash, and honors a per-entry TTL.
7
-
8
- ## `@cache` decorator
9
-
10
- Annotate a route method; the compiler appends the cache directive to whatever
11
- `Response` the route returns, so it composes with every return shape (a
12
- `Response`, a `void` 204, or an auto-encoded `@data` value).
13
-
14
- ```ts
15
- @cache(60) // 60 minutes at the edge
16
- @cache(60, 300) // + 5 minutes (300s) in the browser
17
- @cache(60, 300, true) // + private scope (per-user caches only)
18
- @cache(60, 300, true, true) // + cache even for authenticated requests
19
- @get('/leaderboard')
20
- public top(): Standings { /* … */ }
21
- ```
22
-
23
- Arguments must be integer or boolean literals; a non-literal argument makes the
24
- decorator degrade safely to "not cached" rather than miscompile.
25
-
26
- ## `Response.cache(...)`
27
-
28
- The same controls are available imperatively, which is what `@cache` lowers to:
29
-
30
- ```ts
31
- public cache(
32
- edgeTtlMinutes: u16,
33
- browserTtlSeconds: u32 = 0,
34
- privateScope: bool = false,
35
- allowAuth: bool = false,
36
- ): Response
37
- ```
38
-
39
- ```ts
40
- return Response.json(body).cache(60, 300);
41
- ```
42
-
43
- `cacheFor(minutes)` is the common shorthand for "edge only, no browser caching":
44
-
45
- ```ts
46
- return Response.bytes(blob).cacheFor(5);
47
- ```
48
-
49
- ## Parameters
50
-
51
- | Parameter | Meaning |
52
- | --- | --- |
53
- | `edgeTtlMinutes` | How long the edge may serve the cached response. Clamped to a 24-hour maximum. |
54
- | `browserTtlSeconds` | `max-age` for the browser. `0` (default) means the browser does not cache. |
55
- | `privateScope` | Marks the response `private`: only per-user caches (the browser), never a shared edge/CDN cache. |
56
- | `allowAuth` | Permit caching a response to an authenticated request. Off by default (see safety rails). |
57
-
58
- ## Safety rails
59
-
60
- The cache layer refuses to store anything unsafe, regardless of the directive:
61
-
62
- - **5xx** responses are never cached, a server error is transient, and `@cache`
63
- wraps the whole route, so a `@cache`d route that hits a blip returns its 500
64
- carrying the directive; caching it would serve the failure for the full TTL.
65
- **2xx, 3xx, and 4xx are cacheable** (a redirect or a `404`/`410` is a
66
- deterministic function of the request key);
67
- - a response that sets a **`Set-Cookie`** is never cached;
68
- - a response to an **authenticated** request is not cached unless you pass
69
- `allowAuth = true`, this prevents one user's personalized response from being
70
- served to another;
71
- - the edge TTL is **clamped to 24 hours**.
72
-
73
- Because `@auth` guards and body-decode run before the cache directive is applied,
74
- an unauthorized request is rejected with 401 before anything is cached, and a
75
- cached entry is only ever produced from a handler that actually ran.
76
-
77
- Caching is **always opt-in.** A response with no `Dacely-Cache-Control` directive
78
- (i.e. no `@cache` / `Response.cache(...)`) is never stored, there is no blind
79
- "cache every GET" mode, because an automatic window cannot tell a personalized
80
- response from a public one and would key it without a per-user component.
81
-
82
- ## Memory bounds and disk spill
83
-
84
- The edge cache is per-core and hard-capped so it can never exhaust node memory.
85
- It has two tiers:
86
-
87
- - **RAM tier**, small, short-TTL responses. Bounded by a per-core byte budget
88
- (each core holds at most ~128 MB) plus an entry-count cap; an insert that would
89
- exceed the budget drops expired entries first, then evicts the soonest-to-expire
90
- ones. A response over ~256 KB does not go in the RAM tier.
91
- - **Disk tier (spill)**, when the operator enables `--spill-dir`, a **big**
92
- (over the ~256 KB RAM cap) or **long-TTL** (≥ 10 min) cacheable response is
93
- written to disk instead and served back zero-RAM via a memory map, the same way
94
- static files are served. This keeps the RAM tier for the hot working set while
95
- still caching large bodies and long-lived entries. Writes (and unlinks) are
96
- offloaded to a sibling thread so they never stall the request path; a separate
97
- per-core disk budget caps total spilled bytes, with the same expiry + eviction.
98
- If spill is not enabled, a big response is simply not cached (reported as not
99
- stored by the `Dacely-Cache` tag).
100
-
101
- From a tenant's point of view nothing changes: you still just set a
102
- `Dacely-Cache-Control` directive (via `@cache` / `Response.cache(...)`). The edge
103
- decides RAM vs disk; both honor the same TTL and the same safety rails above.
104
- Expiry is enforced on read (a past-TTL entry is a miss) and reclaimed on the next
105
- insert that needs room. Nothing persists across a process restart.
106
-
107
- ## Choosing TTLs
108
-
109
- - Public, slow-changing data (a leaderboard, a catalog): a few minutes of edge
110
- TTL plus a short browser TTL removes most of the load.
111
- - Per-user data: set `privateScope` so it never lands in a shared cache, and
112
- prefer a small or zero edge TTL.
113
- - Anything with a `Set-Cookie` or behind `@auth`: leave it uncached unless you
114
- have thought through `allowAuth` and are certain the body is identical for
115
- every authorized caller.
package/docs/cli.md DELETED
@@ -1,17 +0,0 @@
1
- # CLI
2
-
3
- - `toiljs create [name]`, scaffold a project. Flags: `--template app|minimal`,
4
- `--style css|sass|less|stylus`, `--tailwind`, `--no-ai`, `-y`/`--yes`.
5
- - `toiljs dev`, dev server with HMR (`--port`, `--root`). With a `toilconfig.json` it builds
6
- the server first, then rebuilds it whenever a `server/` file changes (regenerating
7
- `shared/server.ts`, which Vite HMRs into the client); client-only edits just HMR the client.
8
- - `toiljs build`, production build. With a `toilconfig.json` it builds the server (toilscript,
9
- regenerating `shared/server.ts`) first, then the client → `build/client`. `--server` builds
10
- only the server. Every `server/` file declaring a surface (`@data`/`@rest`/...) is compiled.
11
- - `toiljs start`, self-host the built app with production hyper-express/uWS static workers,
12
- SSR/wasm dispatch, daemon support, and a `/_toil` WebSocket channel. Use `--threads <n>`
13
- (or `server.threads`) to set the worker count; `1` disables the pool.
14
- - `toiljs configure`, toggle styling features on an existing project (see [styling.md](./styling.md)).
15
- - `toiljs doctor`, diagnose project setup (`--json` for CI). `--fix` auto-wires the typed-RPC
16
- setup (build scripts, tsconfig `shared` + alias, `.gitignore`, toilscript version, and the
17
- toilscript prettier plugin) so an existing project upgrades in one command.
package/docs/client.md DELETED
@@ -1,39 +0,0 @@
1
- # Client runtime
2
-
3
- Everything is on the `Toil` global, no imports needed in route files.
4
-
5
- ## Entry
6
-
7
- `client/toil.tsx` imports the route table + global styles and mounts the app:
8
-
9
- ```tsx
10
- import { routes, layout, notFound } from "toiljs/routes";
11
- import "./styles/main.css";
12
- Toil.mount(routes, layout, notFound);
13
- ```
14
-
15
- ## API (on `Toil`)
16
-
17
- - Components: `Link`, `NavLink`, `Head`
18
- - Navigation: `navigate`, `useRouter`, `useNavigate`
19
- - Location: `usePathname`, `useSearchParams`, `useParams`, `useNavigationPending`
20
- - Data: `useLoaderData` (see [routing.md](./routing.md))
21
- - Head: `useHead`, `useTitle`, `<Head>`, set the `<title>` / meta per route
22
- - Realtime: `useChannel`, `connectChannel` (WebSocket to the backend at `/_toil`)
23
- - IO globals (no `Toil.` prefix): `FastMap`, `FastSet`, `DataWriter`, `DataReader`
24
- - `parseError(err)` global: message from an unknown caught value (handy in `catch`)
25
- - `Server` global: the typed RPC surface generated from the server (see [server.md](./server.md))
26
- - `Server.REST.<controller>.<route>(args)`: a working, typed `fetch` client for your
27
- `@rest` controllers, e.g. `await Server.REST.todos.getTodo({ params: { id } })` or
28
- `await Server.REST.todos.add({ body: new AddTodo("milk") })`. `args` is
29
- `{ params?, body?, query?, headers? }`; returns are typed (`@data` classes are parsed for
30
- you). The REST client attaches when you import from `shared/server`.
31
-
32
- ## Head example
33
-
34
- ```tsx
35
- Toil.useHead({
36
- title: "Blog",
37
- meta: [{ name: "description", content: "..." }],
38
- });
39
- ```