toiljs 0.0.115 → 0.0.117

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 (326) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +16 -7
  3. package/TYPESCRIPT_LAW.md +1946 -2073
  4. package/build/backend/.tsbuildinfo +1 -1
  5. package/build/cli/.tsbuildinfo +1 -1
  6. package/build/cli/index.js +2375 -258
  7. package/build/client/.tsbuildinfo +1 -1
  8. package/build/client/dev/devtools.js +1 -1
  9. package/build/client/dev/error-overlay.js +1 -1
  10. package/build/client/routing/mount.js +1 -1
  11. package/build/compiler/.tsbuildinfo +1 -1
  12. package/build/compiler/auth-emails.js +7 -3
  13. package/build/compiler/emails.js +1 -1
  14. package/build/compiler/index.d.ts +1 -0
  15. package/build/compiler/index.js +25 -4
  16. package/build/compiler/pages.js +2 -3
  17. package/build/compiler/prerender.d.ts +3 -8
  18. package/build/compiler/prerender.js +88 -127
  19. package/build/compiler/ssg.js +3 -4
  20. package/build/compiler/template-build.js +3 -4
  21. package/build/compiler/toil-docs.generated.js +6 -5
  22. package/build/compiler/vite.js +3 -6
  23. package/build/devserver/.tsbuildinfo +1 -1
  24. package/build/devserver/db/database.js +7 -3
  25. package/build/devserver/db/types.js +1 -1
  26. package/build/devserver/production.js +4 -3
  27. package/build/devserver/runtime/crypto.d.ts +1 -1
  28. package/build/io/.tsbuildinfo +1 -1
  29. package/build/logger/.tsbuildinfo +1 -1
  30. package/build/shared/.tsbuildinfo +1 -1
  31. package/examples/basic/client/routes/analytics.tsx +7 -5
  32. package/examples/basic/server/tsconfig.json +1 -3
  33. package/package.json +45 -44
  34. package/presets/no-uint8array-tostring.js +120 -153
  35. package/presets/oxlint.d.ts +5 -0
  36. package/presets/oxlint.js +224 -0
  37. package/server/runtime/tsconfig.json +1 -3
  38. package/tsconfig.json +2 -11
  39. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -38
  40. package/.github/ISSUE_TEMPLATE/bug_report.yml +0 -90
  41. package/.github/ISSUE_TEMPLATE/config.yml +0 -8
  42. package/.github/ISSUE_TEMPLATE/feature_request.md +0 -20
  43. package/.github/PULL_REQUEST_TEMPLATE.md +0 -43
  44. package/.github/changelog-config.json +0 -45
  45. package/.github/dependabot.yml +0 -27
  46. package/.github/workflows/ci.yml +0 -222
  47. package/.vscode/settings.json +0 -10
  48. package/docs/README.md +0 -107
  49. package/docs/auth/README.md +0 -104
  50. package/docs/auth/configuration.md +0 -105
  51. package/docs/auth/emails.md +0 -92
  52. package/docs/auth/extending.md +0 -202
  53. package/docs/auth/how-it-works.md +0 -138
  54. package/docs/auth/usage.md +0 -188
  55. package/docs/backend/README.md +0 -143
  56. package/docs/backend/data.md +0 -351
  57. package/docs/backend/rest.md +0 -402
  58. package/docs/backend/rpc.md +0 -226
  59. package/docs/background/README.md +0 -114
  60. package/docs/background/daemons.md +0 -276
  61. package/docs/background/derive.md +0 -179
  62. package/docs/cli/README.md +0 -379
  63. package/docs/concepts/ai-guide.md +0 -132
  64. package/docs/concepts/config.md +0 -416
  65. package/docs/concepts/decorators.md +0 -127
  66. package/docs/concepts/security.md +0 -108
  67. package/docs/concepts/tiers.md +0 -166
  68. package/docs/concepts/types.md +0 -216
  69. package/docs/database/README.md +0 -143
  70. package/docs/database/capacity.md +0 -350
  71. package/docs/database/counters.md +0 -174
  72. package/docs/database/documents.md +0 -277
  73. package/docs/database/events.md +0 -307
  74. package/docs/database/membership.md +0 -246
  75. package/docs/database/setup.md +0 -155
  76. package/docs/database/unique.md +0 -216
  77. package/docs/database/views.md +0 -246
  78. package/docs/frontend/README.md +0 -104
  79. package/docs/frontend/components.md +0 -286
  80. package/docs/frontend/data-fetching.md +0 -260
  81. package/docs/frontend/images.md +0 -148
  82. package/docs/frontend/metadata.md +0 -344
  83. package/docs/frontend/navigation.md +0 -296
  84. package/docs/frontend/rendering.md +0 -236
  85. package/docs/frontend/routing.md +0 -346
  86. package/docs/frontend/scripts.md +0 -118
  87. package/docs/frontend/search.md +0 -191
  88. package/docs/frontend/styling.md +0 -147
  89. package/docs/frontend/toil-global.md +0 -216
  90. package/docs/getting-started/README.md +0 -81
  91. package/docs/getting-started/create-project.md +0 -131
  92. package/docs/getting-started/deploy.md +0 -101
  93. package/docs/getting-started/first-app.md +0 -215
  94. package/docs/getting-started/installation.md +0 -124
  95. package/docs/getting-started/migrating.md +0 -125
  96. package/docs/getting-started/project-structure.md +0 -163
  97. package/docs/introduction/README.md +0 -66
  98. package/docs/introduction/design-principles.md +0 -81
  99. package/docs/introduction/distributed.md +0 -91
  100. package/docs/introduction/how-it-works.md +0 -121
  101. package/docs/introduction/hyperscale.md +0 -70
  102. package/docs/introduction/modern-stack.md +0 -85
  103. package/docs/introduction/vs-other-frameworks.md +0 -64
  104. package/docs/introduction/why-toil.md +0 -99
  105. package/docs/llms.txt +0 -95
  106. package/docs/realtime/README.md +0 -102
  107. package/docs/realtime/channels.md +0 -211
  108. package/docs/realtime/streams.md +0 -369
  109. package/docs/services/README.md +0 -60
  110. package/docs/services/analytics.md +0 -279
  111. package/docs/services/caching.md +0 -175
  112. package/docs/services/cookies.md +0 -235
  113. package/docs/services/crypto.md +0 -209
  114. package/docs/services/email.md +0 -289
  115. package/docs/services/environment.md +0 -141
  116. package/docs/services/ratelimit.md +0 -174
  117. package/docs/services/time.md +0 -85
  118. package/eslint.config.js +0 -57
  119. package/examples/basic/client/routes/docs/[...slug].tsx +0 -12
  120. package/presets/eslint.js +0 -88
  121. package/src/backend/index.ts +0 -204
  122. package/src/cli/configure.ts +0 -363
  123. package/src/cli/create.ts +0 -924
  124. package/src/cli/db.ts +0 -158
  125. package/src/cli/diagnostics.ts +0 -665
  126. package/src/cli/doctor.ts +0 -982
  127. package/src/cli/features.ts +0 -170
  128. package/src/cli/index.ts +0 -326
  129. package/src/cli/notify.ts +0 -120
  130. package/src/cli/proc.ts +0 -56
  131. package/src/cli/ui.ts +0 -169
  132. package/src/cli/update.ts +0 -248
  133. package/src/cli/updates.ts +0 -92
  134. package/src/cli/validate.ts +0 -31
  135. package/src/cli/version-check.ts +0 -125
  136. package/src/client/auth.ts +0 -869
  137. package/src/client/channel/channel.ts +0 -146
  138. package/src/client/components/Form.tsx +0 -65
  139. package/src/client/components/Image.tsx +0 -160
  140. package/src/client/components/Script.tsx +0 -113
  141. package/src/client/components/Slot.tsx +0 -21
  142. package/src/client/dev/devtools.tsx +0 -1089
  143. package/src/client/dev/error-overlay.tsx +0 -225
  144. package/src/client/errors.ts +0 -11
  145. package/src/client/head/head.ts +0 -182
  146. package/src/client/head/metadata.ts +0 -110
  147. package/src/client/index.ts +0 -130
  148. package/src/client/navigation/Link.tsx +0 -108
  149. package/src/client/navigation/NavLink.tsx +0 -86
  150. package/src/client/navigation/navigation.ts +0 -235
  151. package/src/client/navigation/prefetch.ts +0 -169
  152. package/src/client/navigation/scroll.ts +0 -53
  153. package/src/client/routing/Router.tsx +0 -216
  154. package/src/client/routing/action.ts +0 -122
  155. package/src/client/routing/error-boundary.tsx +0 -43
  156. package/src/client/routing/hooks.ts +0 -140
  157. package/src/client/routing/lazy.ts +0 -77
  158. package/src/client/routing/loader.ts +0 -381
  159. package/src/client/routing/match.ts +0 -47
  160. package/src/client/routing/mount.tsx +0 -90
  161. package/src/client/routing/params-context.ts +0 -10
  162. package/src/client/routing/slot-context.ts +0 -7
  163. package/src/client/rpc.ts +0 -97
  164. package/src/client/search/search.ts +0 -189
  165. package/src/client/search/use-page-search.ts +0 -73
  166. package/src/client/ssr/markers.tsx +0 -172
  167. package/src/client/stream/client.ts +0 -237
  168. package/src/client/types.ts +0 -73
  169. package/src/compiler/auth-emails.ts +0 -312
  170. package/src/compiler/config.ts +0 -369
  171. package/src/compiler/docs.ts +0 -127
  172. package/src/compiler/email-preview.ts +0 -314
  173. package/src/compiler/emails.ts +0 -404
  174. package/src/compiler/fonts.ts +0 -89
  175. package/src/compiler/generate.ts +0 -471
  176. package/src/compiler/image-blur.ts +0 -42
  177. package/src/compiler/image-report.ts +0 -88
  178. package/src/compiler/index.ts +0 -1362
  179. package/src/compiler/pages.ts +0 -49
  180. package/src/compiler/plugin.ts +0 -391
  181. package/src/compiler/prerender.ts +0 -263
  182. package/src/compiler/routes.ts +0 -170
  183. package/src/compiler/seo.ts +0 -415
  184. package/src/compiler/sri.ts +0 -144
  185. package/src/compiler/ssg.ts +0 -177
  186. package/src/compiler/ssr-codegen.ts +0 -85
  187. package/src/compiler/template-build.ts +0 -676
  188. package/src/compiler/template.ts +0 -262
  189. package/src/compiler/toil-docs.generated.ts +0 -77
  190. package/src/compiler/vite.ts +0 -245
  191. package/src/devserver/analytics/index.ts +0 -317
  192. package/src/devserver/config/dotenv.ts +0 -102
  193. package/src/devserver/config/env.ts +0 -30
  194. package/src/devserver/config/ratelimit.ts +0 -123
  195. package/src/devserver/daemon/catalog.ts +0 -120
  196. package/src/devserver/daemon/cron.ts +0 -87
  197. package/src/devserver/daemon/host.ts +0 -134
  198. package/src/devserver/daemon/index.ts +0 -369
  199. package/src/devserver/daemon/runtime.ts +0 -48
  200. package/src/devserver/db/catalog.ts +0 -109
  201. package/src/devserver/db/database.ts +0 -1834
  202. package/src/devserver/db/derives.ts +0 -121
  203. package/src/devserver/db/index.ts +0 -20
  204. package/src/devserver/db/routeKinds.ts +0 -191
  205. package/src/devserver/db/types.ts +0 -168
  206. package/src/devserver/email/caps.ts +0 -0
  207. package/src/devserver/email/config.ts +0 -141
  208. package/src/devserver/email/console.ts +0 -348
  209. package/src/devserver/email/index.ts +0 -155
  210. package/src/devserver/email/providers.ts +0 -131
  211. package/src/devserver/email/status.ts +0 -23
  212. package/src/devserver/email/validate.ts +0 -37
  213. package/src/devserver/email/wire.ts +0 -55
  214. package/src/devserver/http/cache.ts +0 -0
  215. package/src/devserver/http/envelope.ts +0 -157
  216. package/src/devserver/http/proxy.ts +0 -172
  217. package/src/devserver/http/runtime.ts +0 -387
  218. package/src/devserver/index.ts +0 -43
  219. package/src/devserver/production-ipc.ts +0 -63
  220. package/src/devserver/production-worker.ts +0 -83
  221. package/src/devserver/production.ts +0 -718
  222. package/src/devserver/runtime/crypto.ts +0 -587
  223. package/src/devserver/runtime/host.ts +0 -427
  224. package/src/devserver/runtime/module.ts +0 -442
  225. package/src/devserver/server.ts +0 -251
  226. package/src/devserver/ssr.ts +0 -208
  227. package/src/devserver/stream/catalog.ts +0 -106
  228. package/src/devserver/stream/host.ts +0 -42
  229. package/src/devserver/stream/index.ts +0 -301
  230. package/src/devserver/stream/manager.ts +0 -167
  231. package/src/devserver/stream/router.ts +0 -101
  232. package/src/devserver/stream/wire.ts +0 -58
  233. package/src/devserver/stream/ws.ts +0 -78
  234. package/src/devserver/wasm/sections.ts +0 -59
  235. package/src/devserver/wasm/surface.ts +0 -115
  236. package/src/io/FastMap.ts +0 -151
  237. package/src/io/FastSet.ts +0 -113
  238. package/src/io/codec.ts +0 -362
  239. package/src/io/index.ts +0 -10
  240. package/src/io/lengths.ts +0 -14
  241. package/src/io/types.ts +0 -19
  242. package/src/logger/index.ts +0 -22
  243. package/src/shared/index.ts +0 -46
  244. package/test/analytics-dev.test.ts +0 -113
  245. package/test/assembly/aspect-shim.ts +0 -33
  246. package/test/assembly/cookie.spec.ts +0 -303
  247. package/test/assembly/example.spec.ts +0 -22
  248. package/test/assembly/ssr.spec.ts +0 -95
  249. package/test/assembly.test.ts +0 -59
  250. package/test/auth-emails.test.ts +0 -118
  251. package/test/built-ssr.test.ts +0 -148
  252. package/test/channel.test.ts +0 -21
  253. package/test/config-host-port.test.ts +0 -59
  254. package/test/configure.test.ts +0 -94
  255. package/test/daemon-build.test.ts +0 -278
  256. package/test/daemon-catalog.test.ts +0 -349
  257. package/test/daemon-emulation.test.ts +0 -256
  258. package/test/db.test.ts +0 -0
  259. package/test/devserver-database.test.ts +0 -1102
  260. package/test/devserver-email.test.ts +0 -306
  261. package/test/devserver-pqauth.test.ts +0 -93
  262. package/test/devserver-secrets.test.ts +0 -63
  263. package/test/devserver.test.ts +0 -259
  264. package/test/doctor.test.ts +0 -298
  265. package/test/dom/Image.test.tsx +0 -144
  266. package/test/dom/Link.test.tsx +0 -67
  267. package/test/dom/NavLink.test.tsx +0 -37
  268. package/test/dom/Script.test.tsx +0 -48
  269. package/test/dom/action.test.tsx +0 -146
  270. package/test/dom/error-overlay.test.tsx +0 -44
  271. package/test/dom/loader.test.tsx +0 -121
  272. package/test/dom/navigation.test.ts +0 -59
  273. package/test/dom/revalidate.test.tsx +0 -38
  274. package/test/dom/route-head.test.tsx +0 -56
  275. package/test/dom/router-loading.test.tsx +0 -44
  276. package/test/dom/scroll.test.ts +0 -56
  277. package/test/dom/slot.test.tsx +0 -131
  278. package/test/dom/use-metadata.test.tsx +0 -58
  279. package/test/dom/view-transitions.test.tsx +0 -53
  280. package/test/dynamic-routing.test.ts +0 -132
  281. package/test/email-console.test.ts +0 -86
  282. package/test/email-preview.test.ts +0 -73
  283. package/test/emails.test.ts +0 -58
  284. package/test/errors.test.ts +0 -21
  285. package/test/example-guestbook.test.ts +0 -123
  286. package/test/features.test.ts +0 -149
  287. package/test/fixtures/bignum-wire/spec.ts +0 -25
  288. package/test/fixtures/daemon-app.ts +0 -65
  289. package/test/fixtures/stream-echo.ts +0 -26
  290. package/test/fixtures/stream-gate.ts +0 -28
  291. package/test/fixtures/stream-trap.ts +0 -18
  292. package/test/fixtures/stream-typed.ts +0 -41
  293. package/test/fonts.test.ts +0 -28
  294. package/test/global-setup.ts +0 -17
  295. package/test/head.test.ts +0 -42
  296. package/test/image-blur.test.ts +0 -50
  297. package/test/io.test.ts +0 -117
  298. package/test/metadata.test.ts +0 -40
  299. package/test/navlink.test.ts +0 -28
  300. package/test/pages.test.ts +0 -105
  301. package/test/placeholder.test.ts +0 -9
  302. package/test/pqauth-e2e.test.ts +0 -216
  303. package/test/pqauth-email.test.ts +0 -801
  304. package/test/prerender.test.ts +0 -116
  305. package/test/prettier-plugin.test.ts +0 -46
  306. package/test/routes.test.ts +0 -76
  307. package/test/rpc-bignum-wire.test.ts +0 -164
  308. package/test/rpc-dispatch.test.ts +0 -132
  309. package/test/rpc-kinds.test.ts +0 -18
  310. package/test/rpc.test.ts +0 -66
  311. package/test/search.test.ts +0 -114
  312. package/test/seo.test.ts +0 -175
  313. package/test/slot-layouts.test.ts +0 -69
  314. package/test/sri.test.ts +0 -129
  315. package/test/ssg.test.ts +0 -36
  316. package/test/ssr-hydration.test.tsx +0 -122
  317. package/test/ssr-render.test.ts +0 -197
  318. package/test/ssr-template.test.tsx +0 -482
  319. package/test/stream-emulation.test.ts +0 -479
  320. package/test/ui.test.ts +0 -40
  321. package/test/update.test.ts +0 -58
  322. package/test/validate.test.ts +0 -42
  323. package/test/version-check.test.ts +0 -130
  324. package/tests/data-parity/generated-parity.ts +0 -99
  325. package/tests/data-parity/parity.ts +0 -80
  326. package/tests/data-parity/spec.ts +0 -46
@@ -1,105 +0,0 @@
1
- # Configuring auth for production
2
-
3
- Built-in auth runs locally with **zero configuration**, it falls back to published, insecure DEV secrets
4
- so `toiljs dev` Just Works. **A deployment MUST replace all three secrets and pin its KEM key.** This page
5
- is the checklist.
6
-
7
- ## The secrets
8
-
9
- Auth reads these from the tenant environment store (locally, `.env.secrets`; on the edge, the per-host
10
- secure env). They resolve **lazily** the first time auth runs, so no startup wiring is needed.
11
-
12
- | Key | What it is | Dev fallback |
13
- | --- | --- | --- |
14
- | `AUTH_SESSION_SECRET` | HMAC-SHA256 key that signs the session cookie. Must be identical on every edge instance (a cookie minted anywhere must verify everywhere). | a public constant, **anyone can forge a session** |
15
- | `AUTH_OPRF_SEED` | Master seed for the per-user OPRF salt key. Rotating it invalidates every password (users must re-register). | a hashed public constant |
16
- | `AUTH_KEM_SK` | The server's ML-KEM-768 **secret** key (hex). Its public half is what the client encapsulates to. | a pinned dev key pair |
17
-
18
- ```bash
19
- # .env.secrets (gitignored; mode 0600 on the edge, NEVER under hosts/, NEVER in the .wasm)
20
- AUTH_SESSION_SECRET=…64 hex chars (32 bytes)…
21
- AUTH_OPRF_SEED=…64 hex chars (32 bytes)…
22
- AUTH_KEM_SK=…hex of an ML-KEM-768 secret key…
23
- ```
24
-
25
- ### Generating them
26
-
27
- `AUTH_SESSION_SECRET` and `AUTH_OPRF_SEED` are just 32 random bytes each:
28
-
29
- ```bash
30
- node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
31
- ```
32
-
33
- `AUTH_KEM_SK` is an ML-KEM-768 key pair. Generate it and keep BOTH halves, the secret goes in the env, the
34
- public half is pinned in the client (next section):
35
-
36
- ```ts
37
- import { ml_kem768 } from '@dacely/noble-post-quantum/ml-kem';
38
- const { secretKey, publicKey } = ml_kem768.keygen();
39
- console.log('AUTH_KEM_SK =', Buffer.from(secretKey).toString('hex'));
40
- console.log('client serverKemPublicKey =', Buffer.from(publicKey).toString('hex'));
41
- ```
42
-
43
- ## Pin the client's KEM public key
44
-
45
- The browser must know the server's genuine KEM public key to run the mutual-auth handshake, this is the
46
- anti-phishing anchor. The `toiljs/client` `Auth` helper ships with the **dev** key pinned, so a deployment
47
- MUST pass its own:
48
-
49
- ```ts
50
- import { Auth } from 'toiljs/client';
51
-
52
- const SERVER_KEM_PUBLIC_KEY = /* the publicKey bytes from AUTH_KEM_SK */;
53
-
54
- await Auth.login(username, password, { serverKemPublicKey: SERVER_KEM_PUBLIC_KEY });
55
- await Auth.register(username, password, { serverKemPublicKey: SERVER_KEM_PUBLIC_KEY });
56
- ```
57
-
58
- Ship the public key with your client bundle (it's public, safe to embed). If it doesn't match the server's
59
- `AUTH_KEM_SK`, login's `serverConfirm` check fails and the client aborts.
60
-
61
- ## Optional: audience & domain
62
-
63
- Both are optional and have sensible defaults; set them for stability across host aliases:
64
-
65
- | Key | Meaning | Default |
66
- | --- | --- | --- |
67
- | `TOIL_AUTH_AUDIENCE` | The service audience bound into the signed login message. | `"toil"` |
68
- | `TOIL_AUTH_DOMAIN` | The `domain` input of the stable `ToilUserId` (`sha256(pubkey ‖ username ‖ domain)`). | the request `Host` header, else `localhost` |
69
-
70
- Set `TOIL_AUTH_DOMAIN` explicitly if your site answers on multiple hostnames, otherwise the same user could
71
- get different `ToilUserId`s from different aliases. Once users exist, changing it changes everyone's id, so
72
- pick it before launch.
73
-
74
- ## Email confirmation
75
-
76
- Two plain env vars (tenant-readable, like `PUBLIC_BASE_URL`) control the built-in email-verification flow. They are separate on purpose: sending a verify email at registration is one decision, blocking login until confirmed is another.
77
-
78
- | Key | What it does |
79
- | --- | --- |
80
- | `AUTH_EMAIL_CONFIRMATION` | Registration emails a one-time confirm link (`/confirm#token=…`) and stores the account **unconfirmed**. Login is **not** blocked: the user can sign in, and you nudge them to verify. Use this for "verify at signup, but let people in". |
81
- | `AUTH_REQUIRE_EMAIL_CONFIRMATION` | The stricter form: does everything above AND **refuses login** until the email is confirmed (a distinguishable status the client surfaces as `EmailNotConfirmedError`, so the UI can prompt "confirm your email / resend"). Implies `AUTH_EMAIL_CONFIRMATION`. On the managed edge this is force-set from the per-domain Dacely setting. |
82
-
83
- With neither set, accounts are auto-confirmed at registration and no confirm email is sent. The one-time link is delivered by email; in `toiljs dev` it is drawn in the console (no mailer needed). Both use the standard non-enumerating responses (a generic 200 that never reveals whether an address exists).
84
-
85
- ## Argon2id strength (known limitation)
86
-
87
- The built-in controller currently uses **demo-light** Argon2id params (32 MiB, 2 iterations, 1 lane) so it
88
- stays responsive in a browser tab. These are baked into the shipped controller today; **config-driven
89
- tuning is a planned follow-up.** For a high-value production deployment you should either wait for the
90
- config knob or hand-write your own controller with `≥ 256 MiB / ≥ 3 iterations`. The OPRF still provides the
91
- primary offline-attack resistance regardless, but raise these before protecting anything sensitive.
92
-
93
- The client always derives against whatever params the server returns in `/login/start`, so when the config
94
- knob lands you can raise them server-side with **no client change**.
95
-
96
- ## Deploy checklist
97
-
98
- - [ ] `AUTH_SESSION_SECRET` set (32 random bytes), identical on every edge instance.
99
- - [ ] `AUTH_OPRF_SEED` set (32 random bytes).
100
- - [ ] `AUTH_KEM_SK` set (an ML-KEM-768 secret key), and its **public** half pinned in the client via
101
- `serverKemPublicKey`.
102
- - [ ] `TOIL_AUTH_DOMAIN` set if you serve multiple hostnames (stable `ToilUserId`).
103
- - [ ] (Recommended) Argon2id params reviewed for your threat model.
104
-
105
- The CLI doctor warns when `server.auth` is on and the secrets are missing, run it before you ship.
@@ -1,92 +0,0 @@
1
- # Customizing the auth emails
2
-
3
- Built-in auth sends three transactional emails: the email-verification link, the password-reset link, and the two-factor code. Each ships with a plain, working default. You can replace any of them with your own branded template, and you never touch the auth code to do it.
4
-
5
- ## The three emails
6
-
7
- | Email | When it sends | The value it gives your template |
8
- | --- | --- | --- |
9
- | Email verification | On register, when email confirmation is on | `link` (the confirm URL) |
10
- | Password reset | On `POST /auth/reset/request` | `link` (the reset URL) |
11
- | Two-factor code | On login or 2FA setup, for the email method | `code` (the numeric code) and `action` (the flow phrase) |
12
-
13
- The defaults are intentionally minimal so a fresh project sends real, correct mail with no setup. To match your brand, override them.
14
-
15
- ## How to override
16
-
17
- Drop a React template in your project's `emails/` folder with the reserved name for that email. If the file exists, the build uses it; if not, the default stands. This is the same `emails/*.tsx` system you use for your own mail (see [Email](../services/email.md)), so you get full markup and inline CSS.
18
-
19
- | Reserved file | Overrides | Prop it must read |
20
- | --- | --- | --- |
21
- | `emails/auth-confirm.tsx` | Email verification | `link` |
22
- | `emails/auth-reset.tsx` | Password reset | `link` |
23
- | `emails/auth-2fa.tsx` | Two-factor code | `code`, `action` |
24
-
25
- Each template is a default-exported React component. Read the one prop it is given, and it becomes a `{{token}}` hole the edge fills per send. Reading any other prop renders empty, so the build warns you.
26
-
27
- ```tsx
28
- // emails/auth-confirm.tsx -> overrides the email-verification message.
29
- export const subject = 'Verify your Acme account';
30
-
31
- export default function AuthConfirm({ link }: { link: string }) {
32
- return (
33
- <div style={{ fontFamily: 'system-ui', padding: 24 }}>
34
- <h1 style={{ color: '#cb9820' }}>Welcome to Acme</h1>
35
- <p>Tap below to verify your email and finish signing up.</p>
36
- <p>
37
- <a
38
- href={link}
39
- style={{ background: '#cb9820', color: '#fff', padding: '10px 18px', borderRadius: 8, textDecoration: 'none' }}
40
- >
41
- Verify my email
42
- </a>
43
- </p>
44
- <p style={{ color: '#666', fontSize: 13 }}>Or paste this link: {link}</p>
45
- </div>
46
- );
47
- }
48
- ```
49
-
50
- The reset template works the same way with the `link` prop.
51
-
52
- ## The subject line
53
-
54
- For verification and reset, set the subject with `export const subject = '...'`. Leave it out and the default subject is used (`Confirm your account`, `Reset your password`).
55
-
56
- The two-factor email is different. It serves three flows (signing in, enabling 2FA, and turning 2FA off), so besides the `code` it also gets an `action` prop: the framework fills it with the flow phrase, `sign in`, `enable two-factor authentication`, or `turn off two-factor authentication`. Write it into a sentence like "Enter this code to {action}." and the one template reads correctly for every flow. The subject is likewise set by the framework and stays contextual, so a `subject` export in this file is ignored.
57
-
58
- ```tsx
59
- // emails/auth-2fa.tsx -> overrides the 2FA code message (subject is contextual).
60
- export default function Auth2fa({ code, action }: { code: string; action: string }) {
61
- return (
62
- <div style={{ fontFamily: 'system-ui', padding: 24, textAlign: 'center' }}>
63
- <p>Your Acme verification code:</p>
64
- <p style={{ fontSize: 32, fontWeight: 700, letterSpacing: 6 }}>{code}</p>
65
- <p>Enter it to {action}.</p>
66
- <p style={{ color: '#666' }}>It expires in a few minutes.</p>
67
- </div>
68
- );
69
- }
70
- ```
71
-
72
- ## What you can and cannot put in
73
-
74
- Each template is rendered once, at build time, into a static string of HTML with your styles inlined, and the prop becomes a fill hole. So a fixed layout and inline styles work, but per-send logic does not: a `{items.map(...)}` or a conditional runs at build, not per email. For these three messages that is all you need, one link or one code.
75
-
76
- The only values auth hands each template are the ones in the table above (`link`, or `code` plus `action` for 2FA). There is no user name, no app name, no expiry value. Write those into the template text directly.
77
-
78
- Styling rules are the same as any toiljs email: write inline `style={{ ... }}`, or import a stylesheet and its rules are inlined for you. A bare CSS import with no inlining has no effect in an email client. See [Email](../services/email.md) for the details.
79
-
80
- ## Delivery stays safe
81
-
82
- You cannot weaken the security of these flows by overriding a template. Every auth email is sent detached (fire-and-forget), so the response time never reveals whether an address maps to an account. That holds whether the message is the default or your override. Overriding only changes what the email looks like, never how it is sent.
83
-
84
- ## Where the templates go
85
-
86
- There is nothing to import and nothing to register. The build reads `emails/auth-*.tsx`, bakes the effective templates into the compiled server, and the auth controller sends them. Changing a template and rebuilding (or saving under `toiljs dev`) is all it takes.
87
-
88
- ## Related
89
-
90
- - [Email](../services/email.md): the full `emails/*.tsx` system, `EmailService`, and `EmailTemplate`.
91
- - [Configuration](./configuration.md): the auth secrets and the email provider a deployment must set.
92
- - [Usage](./usage.md): enabling auth, the client API, and guarding routes.
@@ -1,202 +0,0 @@
1
- # Extending & integrating auth
2
-
3
- Built-in auth is deliberately opinionated so the common case is one line. This page covers the identity
4
- you build ON, `ToilUserId`, and how to go beyond the defaults: keying your own data on a user, a custom
5
- user shape, and hand-writing auth from the same primitives.
6
-
7
- ## `ToilUserId`
8
-
9
- The stable, tenant-scoped user identity: `sha256(mldsaPublicKey ‖ identifier ‖ domain)`, a 256-bit value.
10
- It's a **global** (no import), like `crypto`.
11
-
12
- ```ts
13
- // Read the current user's id in any handler (gate on hasSession(), see the null note below).
14
- const id: ToilUserId = AuthService.userId()!;
15
-
16
- // Or derive one yourself.
17
- const id2 = ToilUserId.derive(mldsaPublicKey, 'alice@example.com', 'acme.dacely.com');
18
- ```
19
-
20
- | Member | Description |
21
- | --- | --- |
22
- | `ToilUserId.derive(pk, identifier, domain)` | Derive from an ML-DSA public key + email/username + tenant domain. Deterministic. |
23
- | `ToilUserId.fromBytes(b)` | Rebuild from a 32-byte digest (from `toBytes()` or storage). |
24
- | `toBytes(): Uint8Array` | The 32 identity bytes. |
25
- | `toHex(): string` | Lowercase 64-char hex, a convenient string key. |
26
- | `isZero(): bool` | True for the unset / anonymous id. |
27
- | `equals(other): bool` | Value equality. |
28
- | `a == b` / `a != b` | Overloaded value comparison, **O(1)** (four `u64` word compares, no byte loop, no allocation). |
29
-
30
- ```ts
31
- const a = ToilUserId.derive(pk, 'alice', 'acme.com');
32
- const b = ToilUserId.derive(pk, 'alice', 'acme.com');
33
- const c = ToilUserId.derive(pk, 'bob', 'acme.com');
34
- a == b; // true, same inputs, same id
35
- a != c; // true, different user
36
- ```
37
-
38
- > **Null-check gotcha:** because `ToilUserId` overloads `==`, `AuthService.userId() == null` does NOT
39
- > type-check (`==` expects a `ToilUserId`). Gate with `AuthService.hasSession()` and then `userId()!`, or
40
- > compare with `getUser()` (a plain nullable). `===` is reference identity in AssemblyScript and is not
41
- > overloadable, use `==` for value equality.
42
-
43
- ## Keying your own data on the user
44
-
45
- `toilUserId` is the right key for per-user data, it's stable across sessions/devices and opaque. Use the
46
- hex as a string key, or the bytes in a `@data` key class:
47
-
48
- ```ts
49
- @data
50
- class UserKey {
51
- id: Uint8Array = new Uint8Array(0); // toilUserId bytes
52
- constructor(id: Uint8Array = new Uint8Array(0)) { this.id = id; }
53
- }
54
-
55
- @data class Profile { displayName: string = ''; bio: string = ''; }
56
-
57
- @database
58
- class AppDb {
59
- @collection static profiles: Documents<UserKey, Profile>;
60
- }
61
-
62
- @rest('profile')
63
- class ProfileApi {
64
- @auth
65
- @post('/')
66
- public save(ctx: RouteContext): Response {
67
- const key = new UserKey(AuthService.userId()!.toBytes());
68
- const p = Profile.decode(ctx.request.body);
69
- // Save this user's profile. create is insert-only, so the first save creates
70
- // the record and later saves overwrite the existing one with enqueue.
71
- if (!AppDb.profiles.create(key, p)) {
72
- AppDb.profiles.enqueue(key, p);
73
- }
74
- return Response.text('saved\n');
75
- }
76
- }
77
- ```
78
-
79
- ## Extending the user: add your own fields
80
-
81
- Built-in auth reserves exactly two fields on the authenticated user: `toilUserId` and `username`. To carry
82
- more (a role, a display name, a tenant), just declare your OWN `@user` in your server code while
83
- `server.auth` is on. The build detects it and **extends** it: it injects the reserved `toilUserId` +
84
- `username` as the first two fields and mints sessions for your shape automatically. You do not opt out of
85
- anything, and there is still exactly one `@user` per program.
86
-
87
- ```ts
88
- @user
89
- class Account {
90
- admin: bool = false;
91
- displayName: string = '';
92
- tenant: string = '';
93
- // toilUserId + username are INJECTED by the build; do not declare them here.
94
- }
95
- ```
96
-
97
- Rules:
98
-
99
- - Do **not** declare `toilUserId` or `username` yourself. They are reserved and injected; declaring either is
100
- a compile error (`'username' is reserved by built-in auth`).
101
- - Your `@user` must be default-constructible (give every field an initializer), like any `@data` class.
102
-
103
- `AuthService.getUser()` is now typed to YOUR shape:
104
-
105
- ```ts
106
- const user = AuthService.getUser(); // { toilUserId, username, admin, displayName, tenant } | null
107
- ```
108
-
109
- **Populating your fields.** Login fills `toilUserId` + `username`; your extra fields start at their declared
110
- defaults (`admin = false`, and so on). The built-in controller cannot know your business fields, so you set
111
- them yourself on one of your own `@auth` routes by reading the user, updating it, and re-minting the session:
112
-
113
- ```ts
114
- @rest('account')
115
- class AccountApi {
116
- @auth @post('/promote')
117
- public promote(): Response {
118
- const user = AuthService.getUser()!;
119
- user.admin = true;
120
- const resp = Response.text('promoted\n');
121
- resp.setCookie(AuthService.mintSession(user.encode())); // re-sign the session with the new fields
122
- resp.setCookie(AuthService.userCookie(user.encode())); // update the readable companion cookie
123
- return resp;
124
- }
125
- }
126
- ```
127
-
128
- If you would rather hand-write the whole controller instead, you still can: do **not** enable `server.auth`,
129
- and build your own `@user` + routes from the [AuthService primitives](#the-authservice-primitive-reference)
130
- below. But for most apps, extending is all you need.
131
-
132
- ## Adding email verification / 2FA
133
-
134
- Layer a second factor on top of the session with `TwoFactor` (stateless email codes, no DB; see
135
- [email](../services/email.md)). Typical flow: after login, require a verified email before granting access to
136
- sensitive routes.
137
-
138
- ```ts
139
- @rest('2fa')
140
- class TwoFactorApi {
141
- // Step 1: email a code to the logged-in user, hand back the signed token.
142
- @auth @post('/send')
143
- public send(): Response {
144
- const email = /* the user's email, e.g. their username, or a stored profile field */;
145
- const ch = TwoFactor.send(email, 'login'); // emails the code, returns { token, status }
146
- return Response.bytes(new DataWriter().writeString(ch.token).toBytes());
147
- }
148
-
149
- // Step 2: verify the code the user typed against the token.
150
- @auth @post('/verify')
151
- public verify(ctx: RouteContext): Response {
152
- const r = new DataReader(ctx.request.body);
153
- const token = r.readString(); const email = r.readString(); const code = r.readString();
154
- if (!TwoFactor.verify(token, email, code)) return Response.text('bad code\n', 401);
155
- // Mark this session 2FA-verified: re-mint the session with a flag in your own @user, or store a
156
- // per-user "verified" record keyed on AuthService.userId().
157
- return Response.text('verified\n');
158
- }
159
- }
160
- ```
161
-
162
- `TwoFactor` gives integrity + expiry but not single-use (a code re-verifies within its TTL); keep the TTL
163
- short. For a branded email, use `TwoFactor.issue(...)` (returns the code without sending) + your own
164
- `Emails.*` template. Call `TwoFactor.setSecret(...)` once at startup in production.
165
-
166
- ## The `AuthService` primitive reference
167
-
168
- Everything the built-in controller is built from, available for hand-written auth. All are ambient globals
169
- (no import).
170
-
171
- **Sessions & cookies**
172
- - `mintSession(userData: Uint8Array, ttlSecs?: u64): Cookie`: the signed `__Host-toil_sess` cookie.
173
- - `userCookie(userData, ttlSecs?): Cookie`: the readable `__Secure-toil_user` companion.
174
- - `clearSession(): Cookie` / `clearUserCookie(): Cookie`.
175
- - `hasSession(): bool`: the `@auth` predicate.
176
- - `getSessionBytes(): Uint8Array | null`: the verified `@user` payload bytes.
177
- - `getUser(): <your @user> | null`: decoded, typed to your `@user`.
178
- - `userId(): ToilUserId | null`: the stable id (built-in `@user` layout).
179
- - `setSecret(secret: Uint8Array)`: override the session HMAC key programmatically.
180
-
181
- **Post-quantum login crypto**
182
- - `oprfEvaluate(username, blinded): Uint8Array`: server-keyed OPRF eval.
183
- - `buildRegisterMessage(username, pk)` / `verifyRegister(pk, msg, sig): bool`: proof-of-possession.
184
- - `buildLoginMessage(sub, aud, cid, nonce, iat, exp, ct, memKiB, iterations, parallelism, serverKemKeyId)`
185
- / `verifyLogin(pk, msg, sig): bool`.
186
- - `mlkemDecapsulate(ct): Uint8Array`, `serverKemKeyId(): Uint8Array`.
187
- - `deriveSessionKey(sharedSecret, transcriptHash)`, `serverConfirmTag(sessionKey, transcriptHash)`: the
188
- mutual-auth confirmation.
189
- - `sha256(data): Uint8Array`.
190
- - `setOprfSeed(seed)`, `setServerKemSecretKey(sk)`, `setServerKemPublicKey(pk)`: override the seeds/keys.
191
- - Sizes: `PUBLIC_KEY_LEN`, `SIGNATURE_LEN`, `OPRF_ELEMENT_LEN`, `KEM_CIPHERTEXT_LEN`, `SHARED_SECRET_LEN`, …
192
-
193
- **Related globals**, `TwoFactor` (email codes; see [email](../services/email.md)), `RateLimitService` (used by
194
- `@ratelimit`), `Environment` (the secret store).
195
-
196
- ## Two ways in, one behavior
197
-
198
- The config flag and the import are identical at build time, both make the build append the shipped
199
- `@user` + `@rest('auth')` controller to the toilscript **entry** set, where their decorators weave and the
200
- `@rest` class self-mounts. (A framework decorator source only weaves as an entry; that's why a plain
201
- `import` of the controller isn't enough on its own, the marker import is detected by the build, which does
202
- the entry injection.) This is why built-in auth needs no runtime registration call.
@@ -1,138 +0,0 @@
1
- # How Toil auth works
2
-
3
- Toil auth is a **post-quantum, password-based, mutually-authenticated** login. The design goal: the user
4
- types a password, but the server never sees it, never stores it, and can't be phished into leaking a
5
- verifier, and every primitive is quantum-resistant.
6
-
7
- This page explains the protocol. You do not need to understand it to use auth (see [Usage](./usage.md)),
8
- but you should understand the guarantees before you ship.
9
-
10
- ## The building blocks
11
-
12
- | Primitive | Role |
13
- | --- | --- |
14
- | **OPRF** (RFC 9497, ristretto255-SHA512) | A *server-keyed* salt. The client blinds its password, the server evaluates it under a per-user key, the client unblinds. The result can't be computed without the server, so a stolen database can't be brute-forced offline. |
15
- | **Argon2id** | Stretches the OPRF output into key material (memory-hard, GPU/ASIC-resistant). Runs in the browser. |
16
- | **ML-DSA-44** (FIPS 204) | The user's identity key pair is *derived* from the Argon2id output. The server stores only the **public** key. Login is proved by an ML-DSA signature. |
17
- | **ML-KEM-768** (FIPS 203) | Key encapsulation on login: the server proves it holds its KEM secret by returning a confirmation tag only derivable from the decapsulated shared secret → **mutual** auth (anti-phishing). |
18
- | **HMAC-SHA256** | Signs the session cookie (`AUTH_SESSION_SECRET`). |
19
-
20
- > The whole thing is sometimes called an *aPAKE* (asymmetric password-authenticated key exchange). Do not
21
- > call it OPAQUE, this is Toil's own OPRF + KEM + signature construction.
22
-
23
- ## What the server stores
24
-
25
- Per account, in ToilDB (`@database AuthDb`):
26
-
27
- - `username`, a deterministic `salt`, the **ML-DSA public key**, and the Argon2id params.
28
-
29
- That's it. **No password, no password hash, no verifier that can be brute-forced without the OPRF key.**
30
- Login challenges are a second collection, consumed exactly once (atomic `getDelete`).
31
-
32
- ## Registration
33
-
34
- ```
35
- Browser (client) Toil edge (server)
36
- ───────────────── ──────────────────
37
- blind(password) OPRF key = f(seed, username)
38
- │ username, blinded │
39
- │ ───────── POST /auth/register/start ───────────► │
40
- │ │ evaluated = OPRF(blinded)
41
- │ ◄──── {mem,iters,par, salt, evaluated} ───────── │ (salt is deterministic per user)
42
- │ │
43
- unblind → oprfOut │
44
- seed = Argon2id(oprfOut, salt, params) │
45
- (pk, sk) = ML-DSA-44.keygen(seed) │
46
- proof = ML-DSA.sign(sk, "register|username|pk") │
47
- │ username, pk, proof │
48
- │ ───────── POST /auth/register/finish ──────────► │
49
- │ │ verifyRegister(pk, msg, proof) ✓ proof-of-possession
50
- │ ◄──────────── {status: 0 ok | 1 taken} ──────── │ store AuthAccount{username, salt, pk, params}
51
- ```
52
-
53
- The server never learns the password or the secret key `sk`, only the public key `pk` and a proof the
54
- client holds the matching `sk`. A duplicate username returns a **distinguishable** `status = 1` (so the UI
55
- can say "taken, log in instead"); everything else fails generically.
56
-
57
- ## Login (with mutual auth)
58
-
59
- ```
60
- Browser (client) Toil edge (server)
61
- ───────────────── ──────────────────
62
- blind(password) │
63
- │ username, blinded │
64
- │ ───────── POST /auth/login/start ──────────────► │ evaluated = OPRF(blinded) (ALWAYS, even unknown user)
65
- │ │ if known: store Challenge{cid, nonce, iat, exp}
66
- │ ◄─ {cid, aud, params, salt, nonce, iat, exp, ── │ (identical response whether the user exists or not)
67
- │ evaluated} │
68
- unblind → Argon2id → (pk, sk) = ML-DSA.keygen │
69
- (ct, ssС) = ML-KEM-768.encapsulate(serverKemPublicKey) │
70
- msg = "login|username|aud|cid|nonce|iat|exp|ct|params|kid" │
71
- sig = ML-DSA.sign(sk, msg) │
72
- │ cid, ct, sig │
73
- │ ───────── POST /auth/login/finish ─────────────► │ ch = challenges.getDelete(cid) (consume once; check exp)
74
- │ │ rebuild msg from OUR stored values + ct
75
- │ │ verifyLogin(acct.pk, msg, sig) ✓ it's really this user
76
- │ │ ssS = ML-KEM.decapsulate(ct) ✓ we hold the KEM key
77
- │ │ K = deriveSessionKey(ssS, H(msg))
78
- │ ◄──── {0, sessionToken, serverConfirm} ──────── │ serverConfirm = tag(K, H(msg))
79
- │ + Set-Cookie: __Host-toil_sess=… │ mint session cookie
80
- verify serverConfirm using ssC ✓ the server is genuine │
81
- ```
82
-
83
- Two verifications, both required:
84
- 1. **The server verifies the client**: the ML-DSA signature over a message bound to the challenge, the
85
- Argon2id params, and the server's KEM key id. Replays fail (the challenge is consumed).
86
- 2. **The client verifies the server**: the `serverConfirm` tag is derivable only from the ML-KEM shared
87
- secret, which only the holder of the KEM secret key can decapsulate. A phishing site can't forge it.
88
-
89
- ## Anti-enumeration
90
-
91
- `/login/start` behaves identically for a known and an unknown user: it always runs the OPRF (a decoy key
92
- for unknown users), returns a **deterministic** per-user salt and constant params, and a fresh challenge.
93
- The challenge is persisted only for a real account, and `/login/finish` fails generically at consume for an
94
- unknown user. So an attacker can't probe which usernames exist. Every failure path returns the same
95
- `401 auth: request failed`.
96
-
97
- ## Sessions & cookies
98
-
99
- On successful login the server mints **two** cookies:
100
-
101
- - **`__Host-toil_sess`**: the authoritative session. HMAC-SHA256 signed with `AUTH_SESSION_SECRET`,
102
- `HttpOnly`, `Secure`, `SameSite=Lax`. It carries the `@user` codec payload. `@auth` and
103
- `AuthService.getUser()` open + verify this cookie server-side; a forged or tampered cookie fails.
104
- - **`__Secure-toil_user`**: a **readable** companion carrying the same payload, so the browser can show
105
- "logged in as …" via the client's `getUser()`. The server **never trusts it**, it is display-only.
106
-
107
- Each request runs in a fresh wasm instance, but the signed cookie is self-contained, so no server-side
108
- session store is needed. `AUTH_SESSION_SECRET` must be identical across every edge instance (it is, via the
109
- env store) so a cookie minted anywhere verifies everywhere.
110
-
111
- ## The stable user identity: `ToilUserId`
112
-
113
- At login the server derives a stable, tenant-scoped id and stores it in the session (the first field of the
114
- built-in `@user`):
115
-
116
- ```
117
- toilUserId = SHA-256( mldsaPublicKey ‖ username ‖ domain ) // 256 bits
118
- ```
119
-
120
- - **Stable:** same login key + username on the same tenant `domain` → same id, forever, across sessions
121
- and devices. Key your own data on it.
122
- - **Opaque + one-way:** it's a hash: safe to store, log, or expose without leaking the key or the address.
123
- - Read it anywhere with `AuthService.userId()`. See [Extending](./extending.md#toiluserid) for the
124
- `ToilUserId` API (O(1) `==` / `!=`, `toHex()`, …).
125
-
126
- ## Threat-model summary
127
-
128
- - **Server database stolen** → attacker gets public keys + salts, not passwords. Brute-forcing needs the
129
- OPRF key (server-side) *and* Argon2id work per guess.
130
- - **Server compromised / malicious** → still can't recover passwords (only public keys) and can't forge a
131
- past session without `AUTH_SESSION_SECRET`.
132
- - **Phishing site** → can't produce the `serverConfirm` tag (no KEM secret), so a correct client aborts.
133
- - **Quantum adversary** → ML-DSA + ML-KEM are post-quantum; the OPRF/Argon2id/HMAC pieces are classical but
134
- not the long-term identity or key-exchange.
135
- - **Replay** → challenges are single-use (`getDelete`) with a short TTL.
136
-
137
- Residual responsibilities are yours: set the [secrets](./configuration.md), pin your deployment's KEM
138
- public key in the client, and raise the Argon2id params for production.