toiljs 0.0.34 → 0.0.36

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 (110) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +1 -0
  3. package/as-pect.config.js +8 -2
  4. package/build/cli/.tsbuildinfo +1 -1
  5. package/build/cli/index.js +97 -0
  6. package/build/client/.tsbuildinfo +1 -1
  7. package/build/client/auth.d.ts +42 -0
  8. package/build/client/auth.js +179 -0
  9. package/build/client/index.d.ts +5 -1
  10. package/build/client/index.js +3 -1
  11. package/build/client/routing/loader.d.ts +1 -0
  12. package/build/client/routing/loader.js +37 -0
  13. package/build/client/routing/mount.js +32 -1
  14. package/build/client/ssr/markers.d.ts +34 -0
  15. package/build/client/ssr/markers.js +49 -0
  16. package/build/compiler/.tsbuildinfo +1 -1
  17. package/build/compiler/docs.js +88 -1
  18. package/build/compiler/generate.d.ts +2 -0
  19. package/build/compiler/generate.js +2 -2
  20. package/build/compiler/index.js +2 -0
  21. package/build/compiler/ssr-codegen.d.ts +2 -0
  22. package/build/compiler/ssr-codegen.js +36 -0
  23. package/build/compiler/template-build.d.ts +29 -0
  24. package/build/compiler/template-build.js +150 -0
  25. package/build/compiler/template.d.ts +22 -0
  26. package/build/compiler/template.js +169 -0
  27. package/build/devserver/.tsbuildinfo +1 -1
  28. package/build/devserver/crypto.js +15 -0
  29. package/build/devserver/host.js +1 -0
  30. package/build/devserver/module.d.ts +1 -0
  31. package/build/devserver/module.js +23 -1
  32. package/docs/README.md +56 -0
  33. package/docs/auth.md +261 -0
  34. package/docs/caching.md +115 -0
  35. package/docs/cookies.md +457 -0
  36. package/docs/crypto.md +130 -0
  37. package/docs/data.md +131 -0
  38. package/docs/getting-started.md +128 -0
  39. package/docs/routing.md +259 -0
  40. package/docs/rpc.md +149 -0
  41. package/docs/ssr.md +184 -0
  42. package/docs/time.md +43 -0
  43. package/examples/basic/client/routes/auth.tsx +198 -0
  44. package/examples/basic/client/routes/cookies.tsx +199 -0
  45. package/examples/basic/client/routes/features/index.tsx +34 -10
  46. package/examples/basic/client/routes/hello.tsx +43 -0
  47. package/examples/basic/client/routes/pq.tsx +135 -0
  48. package/examples/basic/server/AuthTestHandler.ts +15 -0
  49. package/examples/basic/server/AuthVerifyHandler.ts +23 -0
  50. package/examples/basic/server/CacheHandler.ts +25 -0
  51. package/examples/basic/server/DecoCache.ts +18 -0
  52. package/examples/basic/server/FastTrapHandler.ts +8 -0
  53. package/examples/basic/server/SpinHandler.ts +18 -0
  54. package/examples/basic/server/SsrGreetingRender.ts +27 -0
  55. package/examples/basic/server/authexample-main.ts +8 -0
  56. package/examples/basic/server/authtest-main.ts +8 -0
  57. package/examples/basic/server/authverify-main.ts +8 -0
  58. package/examples/basic/server/cache-main.ts +8 -0
  59. package/examples/basic/server/core/AppHandler.ts +243 -0
  60. package/examples/basic/server/deco-main.ts +18 -0
  61. package/examples/basic/server/main.ts +2 -0
  62. package/examples/basic/server/routes/Auth.ts +184 -0
  63. package/examples/basic/server/routes/PqDemo.ts +109 -0
  64. package/examples/basic/server/routes/Session.ts +73 -0
  65. package/examples/basic/server/spin-main.ts +13 -0
  66. package/examples/basic/server/ssr/greeting.slots.ts +19 -0
  67. package/examples/basic/server/ssr-main.ts +18 -0
  68. package/examples/basic/server/toil-server-env.d.ts +94 -0
  69. package/examples/basic/server/trap-main.ts +8 -0
  70. package/package.json +5 -3
  71. package/server/globals/auth.ts +281 -0
  72. package/server/runtime/README.md +61 -0
  73. package/server/runtime/env/Server.ts +12 -0
  74. package/server/runtime/exports/index.ts +17 -0
  75. package/server/runtime/exports/render.ts +51 -0
  76. package/server/runtime/http/base64.ts +104 -0
  77. package/server/runtime/http/cookie.ts +416 -0
  78. package/server/runtime/http/cookies.ts +197 -0
  79. package/server/runtime/http/date.ts +72 -0
  80. package/server/runtime/http/percent.ts +76 -0
  81. package/server/runtime/http/securecookies.ts +224 -0
  82. package/server/runtime/index.ts +17 -0
  83. package/server/runtime/request.ts +24 -0
  84. package/server/runtime/response.ts +29 -0
  85. package/server/runtime/ssr/Ssr.ts +43 -0
  86. package/server/runtime/ssr/encode.ts +110 -0
  87. package/server/runtime/ssr/escape.ts +83 -0
  88. package/server/runtime/ssr/slots.ts +144 -0
  89. package/server/runtime/time.ts +29 -0
  90. package/src/cli/create.ts +105 -0
  91. package/src/client/auth.ts +322 -0
  92. package/src/client/index.ts +5 -1
  93. package/src/client/routing/loader.ts +56 -0
  94. package/src/client/routing/mount.tsx +37 -1
  95. package/src/client/ssr/markers.tsx +140 -0
  96. package/src/compiler/docs.ts +88 -1
  97. package/src/compiler/generate.ts +2 -2
  98. package/src/compiler/index.ts +5 -0
  99. package/src/compiler/ssr-codegen.ts +85 -0
  100. package/src/compiler/template-build.ts +275 -0
  101. package/src/compiler/template.ts +265 -0
  102. package/src/devserver/crypto.ts +23 -0
  103. package/src/devserver/host.ts +4 -0
  104. package/src/devserver/module.ts +39 -1
  105. package/test/assembly/cookie.spec.ts +302 -0
  106. package/test/assembly/example.spec.ts +5 -1
  107. package/test/assembly/ssr.spec.ts +94 -0
  108. package/test/devserver.test.ts +42 -0
  109. package/test/ssr-render.test.ts +128 -0
  110. package/test/ssr-template.test.tsx +348 -0
package/docs/auth.md ADDED
@@ -0,0 +1,261 @@
1
+ # Auth, sessions, and `@user`
2
+
3
+ toiljs ships a complete authentication primitive: a post-quantum login
4
+ (ML-DSA-44, password-derived), HMAC-signed session cookies, a `@auth` route
5
+ guard, and a `@user` type that makes the signed-in user available — fully typed,
6
+ with no type argument — on both the server (`AuthService.getUser()`) and the
7
+ generated client (`getUser()`).
8
+
9
+ `AuthService` is an ambient global (no import). The pieces:
10
+
11
+ - **`@user`** — declares the authenticated user's shape and registers it as
12
+ *the* user type.
13
+ - **`@auth`** — guards a route (or a whole `@rest` class): a valid session is
14
+ required or the request is rejected with `401`.
15
+ - **`AuthService`** — the server runtime: verify a login, mint/read/clear a
16
+ session, and `getUser()`.
17
+ - **client `Auth` + generated `getUser()`** — derive the keypair from a
18
+ password, register/login, and read the user for display.
19
+
20
+ ## Flow at a glance
21
+
22
+ Register sends only a public key; login proves identity with a one-time
23
+ signature the server verifies (it never holds a secret); `@auth` then checks the
24
+ HMAC-signed session cookie on every guarded request.
25
+
26
+ ```mermaid
27
+ sequenceDiagram
28
+ autonumber
29
+ actor U as User
30
+ participant C as Browser<br/>toiljs/client Auth
31
+ participant S as Edge wasm<br/>AuthService
32
+ participant DB as Your store<br/>accounts + challenges
33
+
34
+ rect rgb(14, 21, 32)
35
+ Note over U,DB: Register, the password never leaves the browser
36
+ U->>C: Auth.register(username, password)
37
+ C->>C: Argon2id, then ML-DSA-44 keypair,<br/>keep public key, zeroize secret key
38
+ C->>S: POST /auth/register { username, publicKey 1312 B }
39
+ S->>DB: store Account { username, publicKey }
40
+ S-->>C: 200 ok
41
+ end
42
+
43
+ rect rgb(22, 15, 31)
44
+ Note over U,DB: Login, the server holds no secret
45
+ U->>C: Auth.login(username, password)
46
+ C->>S: GET /auth/challenge { username }
47
+ S->>DB: create + store challenge (cid, nonce, iat, exp)
48
+ S-->>C: { cid, aud, nonce, iat, exp }
49
+ C->>C: re-derive keypair,<br/>sign buildLoginMessage(...) once
50
+ C->>S: POST /auth/login { cid, signature 2420 B }
51
+ S->>DB: atomic consume challenge(cid)
52
+ S->>S: rebuild message from stored values,<br/>verifyLogin via crypto.mldsa_verify
53
+ alt signature valid
54
+ S-->>C: 200 + Set-Cookie __Host-toil_sess (HttpOnly, signed)<br/>and __Secure-toil_user (readable, display only)
55
+ else invalid or unknown user
56
+ S-->>C: 401, constant time, anti-enumeration
57
+ end
58
+ end
59
+
60
+ rect rgb(13, 25, 18)
61
+ Note over U,DB: Guarded request, the @auth guard
62
+ U->>C: open or call an @auth route
63
+ C->>S: request, cookies sent automatically
64
+ S->>S: @auth checks AuthService.hasSession(),<br/>verify HMAC + expiry on __Host-toil_sess
65
+ alt valid session
66
+ S->>S: handler runs,<br/>AuthService.getUser() returns the @user
67
+ S-->>C: 200 response
68
+ else missing or invalid session
69
+ S-->>C: 401 before the handler and body-decode
70
+ end
71
+ C->>C: getUser() reads __Secure-toil_user,<br/>untrusted, UI only
72
+ end
73
+ ```
74
+
75
+ The two cookies are the trust boundary: the HttpOnly `__Host-toil_sess` is the
76
+ only one the server trusts (it re-verifies its signature and expiry every
77
+ request), while the readable `__Secure-toil_user` exists solely so the client
78
+ `getUser()` can show a name without a round-trip and must never gate anything.
79
+
80
+ ## `@user`
81
+
82
+ Mark one class per program as the user type. It becomes a `@data` codec (so it
83
+ serializes into the session) and the return type of `getUser()` everywhere.
84
+
85
+ ```ts
86
+ @user
87
+ class Account {
88
+ username: string = '';
89
+ admin: bool = false;
90
+ score: u64 = 0;
91
+ }
92
+ ```
93
+
94
+ There is exactly one `@user` per program; a second one is a compile error.
95
+
96
+ ## `@auth`
97
+
98
+ Put `@auth` on a route, or on the `@rest` class to guard every route in it. The
99
+ generated dispatcher checks for a valid, unexpired session **before** the
100
+ handler runs (and before any body-decode or cache write); without one it returns
101
+ `401 unauthorized`.
102
+
103
+ ```ts
104
+ @rest('session')
105
+ class Session {
106
+ @auth
107
+ @get('/me')
108
+ public me(): Response {
109
+ const u = AuthService.getUser(); // Account | null, auto-typed
110
+ if (u == null) return Response.text('no session\n', 401);
111
+ return Response.bytes(new DataWriter()
112
+ .writeString(u.username)
113
+ .writeBool(u.admin)
114
+ .writeU64(u.score)
115
+ .toBytes());
116
+ }
117
+
118
+ @auth
119
+ @post('/logout')
120
+ public logout(): Response {
121
+ return Response.text('bye\n', 200)
122
+ .setCookie(AuthService.clearSession())
123
+ .setCookie(AuthService.clearUserCookie());
124
+ }
125
+ }
126
+ ```
127
+
128
+ `@auth` on the class form guards all routes:
129
+
130
+ ```ts
131
+ @auth
132
+ @rest('admin')
133
+ class Admin { /* every route requires a session */ }
134
+ ```
135
+
136
+ ## `AuthService` (server)
137
+
138
+ `AuthService` is a global namespace. The session methods read the ambient
139
+ request (`Server.currentRequest`), so `getUser()`/`hasSession()` take no
140
+ argument and are only meaningful during a dispatch.
141
+
142
+ ### Sessions
143
+
144
+ | Member | Signature | Notes |
145
+ | --- | --- | --- |
146
+ | `getUser()` | `getUser(): AuthUser \| null` | The signed-in user, decoded from the verified session, auto-typed to your `@user` class. |
147
+ | `hasSession()` | `hasSession(): bool` | Whether the request carries a valid, unexpired session. What `@auth` calls. |
148
+ | `getSessionBytes()` | `getSessionBytes(): Uint8Array \| null` | The raw verified `@user` codec bytes, or `null`. |
149
+ | `mintSession(userData, ttlSecs?)` | `mintSession(userData: Uint8Array, ttlSecs: u64 = 86400): Cookie` | Build the signed `__Host-toil_sess` cookie carrying `userData` (i.e. `user.encode()`). HttpOnly, Secure, SameSite=Lax. |
150
+ | `clearSession()` | `clearSession(): Cookie` | A `Set-Cookie` that clears the session. |
151
+ | `userCookie(userData, ttlSecs?)` | `userCookie(userData: Uint8Array, ttlSecs: u64 = 86400): Cookie` | The readable `__Secure-toil_user` companion cookie for the client's `getUser()`. Secure, **not** HttpOnly. Display-only. |
152
+ | `clearUserCookie()` | `clearUserCookie(): Cookie` | Clears the companion cookie. |
153
+ | `setSecret(secret)` | `setSecret(secret: Uint8Array): void` | Set the HMAC secret used to sign sessions. Call once at startup. |
154
+ | `DEFAULT_SESSION_TTL_SECS` | `u64 = 86400` | 24h default lifetime. |
155
+ | `SESSION_COOKIE` / `USER_COOKIE` | `string` | `__Host-toil_sess` / `__Secure-toil_user`. |
156
+
157
+ To log a user in, mint both cookies on the success response:
158
+
159
+ ```ts
160
+ @post('/dev-login')
161
+ public devLogin(ctx: RouteContext): Response {
162
+ const u = new Account();
163
+ u.username = new DataReader(ctx.request.body).readString();
164
+ const data = u.encode();
165
+ return Response.text('ok\n', 200)
166
+ .setCookie(AuthService.mintSession(data, 3600)) // HttpOnly signed session
167
+ .setCookie(AuthService.userCookie(data, 3600)); // readable companion
168
+ }
169
+ ```
170
+
171
+ The session payload is `u8 version | u64 iat | u64 exp | bytes userData`, sealed
172
+ with HMAC-SHA256 via `SecureCookies.signed`. `getSessionBytes()` verifies the
173
+ signature, checks expiry against `Time.nowSeconds()`, and returns the `userData`
174
+ bytes; `getUser()` decodes those into your `@user` class.
175
+
176
+ ### The server secret
177
+
178
+ Sessions are signed with a server secret that must be **identical on every edge
179
+ instance** and **never shipped to the client**. There is no host-config secret
180
+ mechanism yet, so set it at startup:
181
+
182
+ ```ts
183
+ // in main.ts, once
184
+ AuthService.setSecret(/* 32 bytes, build-time constant or deployment secret */);
185
+ ```
186
+
187
+ If you do not call `setSecret`, a well-known **DEV placeholder** is used so local
188
+ development works out of the box. That default is insecure by design — a real
189
+ deployment must override it.
190
+
191
+ ### Post-quantum login
192
+
193
+ The login primitive proves identity without the server ever holding a secret:
194
+
195
+ - The client derives an **ML-DSA-44** (FIPS 204) keypair from the password via
196
+ **Argon2id**, keeps only the 1312-byte public key on the account, and signs a
197
+ login challenge.
198
+ - The server rebuilds the exact signed message from its own stored values and
199
+ verifies the 2420-byte signature with `crypto.mldsa_verify` (verify-only; the
200
+ host never holds a secret key).
201
+
202
+ `AuthService` provides the message construction and verification:
203
+
204
+ | Member | Signature | Notes |
205
+ | --- | --- | --- |
206
+ | `LOGIN_CONTEXT` | `string = 'qauth:login:v1'` | FIPS 204 signing context (domain separator). Byte-identical on client and server. |
207
+ | `PUBLIC_KEY_LEN` / `SIGNATURE_LEN` | `i32` | `1312` / `2420`. |
208
+ | `buildLoginMessage(sub, aud, cid, nonce, iat, exp)` | `(string, string, Uint8Array, Uint8Array, u64, u64): Uint8Array` | The canonical login message `M`, fixed binary layout. Call it with the server's **own** stored values, never client-echoed fields. |
209
+ | `verifyLogin(publicKey, message, signature)` | `(Uint8Array, Uint8Array, Uint8Array): bool` | Verifies under `LOGIN_CONTEXT`; size-checks first. |
210
+
211
+ The challenge → verify flow (see `examples/basic/server/routes/Auth.ts` for the
212
+ full template, including the anti-enumeration and atomic challenge-consume
213
+ requirements) ends with: rebuild the message, `verifyLogin(...)`, then
214
+ `mintSession(account.encode())` on success. Account and challenge **storage is
215
+ the app's** — a tenant's wasm memory is wiped per request, so back them with an
216
+ external store and make challenge-consume an atomic fetch-and-delete.
217
+
218
+ ## The client half
219
+
220
+ The client SDK lives in `toiljs/client` and never lets the password leave the
221
+ browser:
222
+
223
+ ```ts
224
+ import { Auth } from 'toiljs/client';
225
+
226
+ await Auth.register(username, password); // derive keypair, send only the public key
227
+ await Auth.login(username, password); // fetch challenge, sign once, submit {cid, sig}
228
+ ```
229
+
230
+ `register`/`login` stretch the password with Argon2id into a 32-byte seed,
231
+ expand it to the ML-DSA-44 keypair, and zeroize the secret key and seed the
232
+ instant signing is done. There is no recovery: the password *is* the key.
233
+
234
+ ### `getUser()` on the client
235
+
236
+ The generated `shared/server.ts` exports a typed, no-argument `getUser()` that
237
+ reads the readable `__Secure-toil_user` companion cookie and decodes it with the
238
+ generated `@user` codec:
239
+
240
+ ```ts
241
+ import { getUser } from './shared/server';
242
+
243
+ const user = getUser(); // Account | null, fully typed
244
+ if (user) showName(user.username);
245
+ ```
246
+
247
+ This is **display-only and untrusted**: a client can forge the companion cookie,
248
+ fooling only its own UI. The server re-verifies the HttpOnly signed session on
249
+ every `@auth` request, so authorization never depends on the readable cookie.
250
+
251
+ ## Security checklist
252
+
253
+ - Set a real `AuthService.setSecret(...)` in production; the same value on every
254
+ instance; never in a client bundle.
255
+ - The session cookie is HttpOnly + Secure + `__Host-` scoped; the companion
256
+ cookie is readable and untrusted — only ever use it for display.
257
+ - Always verify server-side. `getUser()` (server) decodes a signature-verified,
258
+ expiry-checked session; the client `getUser()` does not and must not gate
259
+ anything sensitive.
260
+ - Login storage (accounts, challenges) is yours to provide and must consume
261
+ challenges atomically to defeat signature replay.
@@ -0,0 +1,115 @@
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 `Toil-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 `Toil-Cache` tag).
100
+
101
+ From a tenant's point of view nothing changes: you still just set a
102
+ `Toil-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.