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
@@ -26,6 +26,8 @@ export function freshDbState() {
26
26
  lastResultVersion: -1,
27
27
  functionKind: DbFunctionKind.Job,
28
28
  writtenCollections: new Set(),
29
+ deriveId: 0,
30
+ pendingCheckpoints: new Map(),
29
31
  };
30
32
  }
31
33
  export const MAX_RESERVATIONS = 4096;
@@ -1,5 +1,5 @@
1
1
  import fs from 'node:fs';
2
- import { DbFunctionKind, derivesForWrites, parseDerives, persistDb, setDbCatalog, } from '../db/index.js';
2
+ import { commitDeriveCheckpoints, DbFunctionKind, derivesForWrites, parseDerives, persistDb, setDbCatalog, } from '../db/index.js';
3
3
  import { parseRouteKinds, parseRpcKinds, routeKindForRequest, rpcKindForId, } from '../db/routeKinds.js';
4
4
  import { decodeResponseEnvelope, encodeRequestEnvelope, unpackHandleResult, } from '../http/envelope.js';
5
5
  import { buildHostImports, freshDispatchState } from './host.js';
@@ -76,6 +76,7 @@ const PROVIDED_IMPORTS = new Set([
76
76
  'data.append_once',
77
77
  'data.enqueue',
78
78
  'data.latest',
79
+ 'data.events_since',
79
80
  'data.capacity_set_total',
80
81
  'data.capacity_available',
81
82
  'data.capacity_reserve',
@@ -229,12 +230,14 @@ export class WasmServerModule {
229
230
  const ref = { memory: null };
230
231
  const state = freshDispatchState();
231
232
  state.db.functionKind = DbFunctionKind.Derive;
233
+ state.db.deriveId = deriveId;
232
234
  const instance = new WebAssembly.Instance(this.module, buildHostImports(ref, state));
233
235
  const exports = instance.exports;
234
236
  ref.memory = exports.memory;
235
237
  if (typeof exports.derive_run !== 'function')
236
238
  return;
237
239
  exports.derive_run(deriveId);
240
+ commitDeriveCheckpoints(state.db);
238
241
  }
239
242
  assertImportSurface(module) {
240
243
  const missing = WebAssembly.Module.imports(module)
package/docs/README.md CHANGED
@@ -1,65 +1,104 @@
1
- # toiljs docs
2
-
3
- Reference documentation for the toiljs server runtime, the decorators the
4
- ToilScript compiler understands, and the generated client surface.
5
-
6
- The server runs as a WebAssembly module: your handler code is written in
7
- ToilScript (a TypeScript/AssemblyScript dialect), compiled to wasm, and run one
8
- fresh instance per request on both the dev server and the edge. Most of the
9
- runtime is exposed two ways: as an ambient **global** (no import, like `crypto`)
10
- and as a named export from `toiljs/server/runtime`.
11
-
12
- ## Guides
13
-
14
- - [Getting started](./getting-started.md): project layout, `toiljs dev` /
15
- `toiljs build`, `main.ts` wiring, and the request lifecycle.
16
-
17
- ## Reference
18
-
19
- - [Routing](./routing.md): `@rest` controllers, the `@get/@post/...` verb
20
- decorators and `@route`, path params, `RouteContext`, `Request`, `Response`,
21
- and how dispatch + the 404 fallback work.
22
- - [Data codec (`@data`)](./data.md): the `@data` decorator and the
23
- `DataWriter` / `DataReader` binary codec (JSON vs Binary streams), with the
24
- exact wire format.
25
- - [RPC and the generated client](./rpc.md): `@service` / `@remote`, the
26
- `--rpcModule` generated `shared/server.ts`, the typed `Server` proxy, and the
27
- REST fetch client.
28
- - [Caching](./caching.md): the `@cache` decorator and `Response.cache(...)`
29
- (edge vs browser TTL, private scope, auth gating).
30
- - [Rate limiting](./ratelimit.md): the `@ratelimit` decorator (FixedWindow /
31
- SlidingWindow / TokenBucket), keyed on the unspoofable client IP, with
32
- `429` + `Retry-After`.
33
- - [Auth, sessions, and `@user`](./auth.md): `@auth` route guards, the `@user`
34
- type, `AuthService` (post-quantum login, signed sessions, `getUser()`), and
35
- the client half.
36
- - [Environment variables & secrets](./environment.md): `Environment.get` /
37
- `getSecure`, per-tenant config + secrets set out of band (GitHub-Actions
38
- style), so the `.wasm` carries no credentials. Two disjoint buckets, read-only.
39
- - [Email](./email.md): `EmailService`, `EmailTemplate`, the `emails/` React
40
- template pipeline, the stateless `TwoFactor` codes, provider config
41
- (Resend / Gmail / SMTP, in the host-only `[email]` env namespace), and limits.
42
- - [Cookies](./cookies.md): the `Cookie` builder, the `Cookies` parser/codec,
43
- `CookieMap`, `SecureCookies` (HMAC signing and AES-256-GCM encryption), the
44
- `base64url` helpers, and the `Request` / `Response` integration.
45
- - [Time](./time.md): `Time.nowMillis()` / `Time.nowSeconds()`, the host
46
- wall-clock binding.
47
- - [SSR templates](./ssr.md): the `render` entrypoint, `SlotValues`,
48
- `HtmlBuilder`, and React-exact escaping.
49
- - [Web Crypto](./crypto.md): the synchronous `crypto` global and
50
- `crypto.subtle` (digests, HMAC, AES-GCM, ECDSA, key import/derive), plus the
51
- ML-DSA-44 post-quantum verify import.
52
-
53
- ## Conventions
54
-
55
- - **"Global, no import"**, a symbol marked `@global` in the runtime is in scope
56
- everywhere in a tenant without an `import`, exactly like `crypto`. The
57
- matching named export exists so editors resolve the type and so the module is
58
- pulled into every build. Either form works.
59
- - **Binary, not JSON, on the hot paths**, request/response bodies, sessions,
60
- and cookies use the deterministic `DataWriter`/`DataReader` codec. JSON is
61
- available for `@rest` routes but binary is the default for anything
62
- performance- or security-sensitive.
63
- - **One fresh instance per request**, guest memory is wiped between requests,
64
- so nothing persists in module globals across requests. Use a host-backed store
65
- for anything that must outlive a single request.
1
+ # toiljs
2
+
3
+ toiljs is a full-stack web framework. You write your **frontend in React** and your **backend in
4
+ TypeScript**, and toiljs turns the backend into a tiny, fast **WebAssembly** program that runs at the edge
5
+ (on servers close to your users, all over the world). One language, one project, one deploy.
6
+
7
+ If you have used Next.js, this will feel familiar: file-based routes, server code next to client code, a dev
8
+ server with hot reload. The difference is what happens underneath. Your server code is compiled by
9
+ **toilscript** (a TypeScript-to-WebAssembly compiler) into a sandboxed `.wasm` module, and it runs on the
10
+ **Dacely edge** with a built-in worldwide database (**ToilDB**), streaming, background jobs, and auth all
11
+ included.
12
+
13
+ ## The mental model
14
+
15
+ ```mermaid
16
+ flowchart LR
17
+ A["Your project<br/>(TypeScript + React)"] -->|toiljs build| B["client bundle<br/>(React, runs in the browser)"]
18
+ A -->|toilscript compile| C["server.wasm<br/>(your backend, sandboxed)"]
19
+ B --> U["User's browser"]
20
+ C --> E["Dacely edge<br/>(worldwide)"]
21
+ E --> D[("ToilDB<br/>global database")]
22
+ U <-->|HTTP / WebTransport| E
23
+ ```
24
+
25
+ - **client/** is your React app (pages, components, styles). It runs in the browser.
26
+ - **server/** is your backend (routes, database, auth). It compiles to WebAssembly and runs on the edge.
27
+ - **shared/** is the typed bridge: toiljs generates a client here so the browser calls your server with full
28
+ type safety.
29
+
30
+ ## Hello, toiljs
31
+
32
+ ```ts
33
+ // server/routes/Hello.ts (a backend HTTP route)
34
+ import { Response } from 'toiljs/server/runtime';
35
+
36
+ @rest('hello')
37
+ class Hello {
38
+ @get('/')
39
+ public hi(): Response {
40
+ return Response.text('Hello from the edge!\n');
41
+ }
42
+ }
43
+ ```
44
+
45
+ ```tsx
46
+ // client/routes/index.tsx (a frontend page)
47
+ export default function Home() {
48
+ return <main><h1>Welcome</h1></main>;
49
+ }
50
+ ```
51
+
52
+ Run `toiljs dev`, open the browser, and both are live with hot reload. That is the whole loop.
53
+
54
+ ## Learn toiljs
55
+
56
+ **Understand toil first**
57
+ - [Understanding toil](./introduction/README.md): what toil is and the one big idea, then
58
+ [why toil and who it is for](./introduction/why-toil.md),
59
+ [the modern stack you get](./introduction/modern-stack.md),
60
+ [how it works](./introduction/how-it-works.md),
61
+ [what makes it hyper-scalable](./introduction/hyperscale.md),
62
+ [how it is distributed](./introduction/distributed.md),
63
+ [toil versus other frameworks](./introduction/vs-other-frameworks.md), and
64
+ [why it is built this way](./introduction/design-principles.md).
65
+
66
+ **Start here**
67
+ - [Getting started](./getting-started/README.md): install, create a project, project structure, your first
68
+ app, and migrating an existing React app.
69
+ - [The CLI](./cli/README.md): `dev`, `build`, `create`, `doctor`, and every flag.
70
+ - [Deploy](./getting-started/deploy.md): build for production, self-host it, and how the managed edge fits in.
71
+
72
+ **Build the frontend**
73
+ - [Frontend overview](./frontend/README.md), [Routing](./frontend/routing.md),
74
+ [Rendering and SSR](./frontend/rendering.md), [Styling](./frontend/styling.md),
75
+ [Images](./frontend/images.md), [Metadata and SEO](./frontend/metadata.md),
76
+ [Fetching data](./frontend/data-fetching.md), [Scripts](./frontend/scripts.md),
77
+ [Search](./frontend/search.md).
78
+
79
+ **Build the backend**
80
+ - [Backend overview](./backend/README.md), [HTTP routes (`@rest`)](./backend/rest.md),
81
+ [Typed RPC (`@service`/`@remote`)](./backend/rpc.md), [Data types (`@data`)](./backend/data.md).
82
+
83
+ **The database (ToilDB)**
84
+ - [Database overview and choosing a family](./database/README.md), [Setup (`@database`)](./database/setup.md),
85
+ [Documents](./database/documents.md), [Unique](./database/unique.md), [Counters](./database/counters.md),
86
+ [Events](./database/events.md), [Views and `@derive`](./database/views.md),
87
+ [Membership](./database/membership.md), [Capacity](./database/capacity.md).
88
+
89
+ **Auth**: [the full auth guide](./auth/README.md) covers post-quantum login, sessions, and `ToilUserId`.
90
+
91
+ **Realtime and background**
92
+ - [Streams](./realtime/README.md) and [channels](./realtime/channels.md), [Daemons and scheduled
93
+ jobs](./background/daemons.md), [Derived views (`@derive`)](./background/derive.md).
94
+
95
+ **Platform services**
96
+ - [Caching](./services/caching.md), [Rate limiting](./services/ratelimit.md),
97
+ [Environment and secrets](./services/environment.md), [Email and 2FA](./services/email.md),
98
+ [Analytics](./services/analytics.md), [Crypto](./services/crypto.md), [Cookies](./services/cookies.md),
99
+ [Time](./services/time.md).
100
+
101
+ **Concepts and reference**
102
+ - [Compute tiers (L1 to L4)](./concepts/tiers.md), [Types (u64, u256, and friends)](./concepts/types.md),
103
+ [Every decorator](./concepts/decorators.md), [Configuration](./concepts/config.md),
104
+ [Security and SRI](./concepts/security.md).
@@ -0,0 +1,102 @@
1
+ # Authentication
2
+
3
+ Toil ships a complete, **post-quantum password login** you turn on with one line. No passwords on your
4
+ server, no third-party identity provider, no hand-written crypto: enable it and you get a `/auth/*` API,
5
+ signed sessions, `@auth`-guarded routes, and a stable per-user identity.
6
+
7
+ ```ts
8
+ // toil.config.ts
9
+ import { defineConfig } from 'toiljs/compiler';
10
+
11
+ export default defineConfig({
12
+ server: { auth: true }, // ← mounts the full /auth/* API + sessions
13
+ });
14
+ ```
15
+
16
+ That is the whole setup. The build appends the framework's auth controller to your server, so `/auth/*`
17
+ is live and `@auth` works everywhere.
18
+
19
+ ## What you get
20
+
21
+ | Endpoint | Purpose |
22
+ | --- | --- |
23
+ | `POST /auth/register/start`, `/register/finish` | Create an account (password never leaves the browser) |
24
+ | `POST /auth/login/start`, `/login/finish` | Log in; sets a signed session cookie |
25
+ | `GET /auth/me` *(`@auth`)* | The current user (`toilUserId` + `username`) |
26
+ | `POST /auth/logout` *(`@auth`)* | Clear the session |
27
+
28
+ Plus, in your own code:
29
+
30
+ - **`@auth`** on any route (or a whole `@rest` class) → 401 unless there's a valid session.
31
+ - **`AuthService.getUser()`** → the typed logged-in user.
32
+ - **`AuthService.userId()`** → the stable [`ToilUserId`](./extending.md#toiluserid) (a 256-bit id you can
33
+ key your data on).
34
+ - **The client**: `import { Auth } from 'toiljs/client'`, then `Auth.register(username, password)` /
35
+ `Auth.login(username, password)`. It does all the browser-side crypto and talks to `/auth/*` for you.
36
+
37
+ ## A login page in full
38
+
39
+ ```tsx
40
+ // client/routes/login.tsx
41
+ import { useState } from 'react';
42
+ import { Auth } from 'toiljs/client';
43
+
44
+ export default function Login() {
45
+ const [u, setU] = useState('');
46
+ const [p, setP] = useState('');
47
+ const [msg, setMsg] = useState('');
48
+
49
+ const register = async () => {
50
+ try { await Auth.register(u, p); setMsg('registered, now log in'); }
51
+ catch (e) { setMsg(parseError(e)); }
52
+ };
53
+ const login = async () => {
54
+ try { await Auth.login(u, p); setMsg('logged in'); } // sets the session cookie
55
+ catch (e) { setMsg(parseError(e)); }
56
+ };
57
+
58
+ return (
59
+ <main>
60
+ <input value={u} onChange={(e) => setU(e.currentTarget.value)} placeholder="username" />
61
+ <input value={p} type="password" onChange={(e) => setP(e.currentTarget.value)} placeholder="password" />
62
+ <button onClick={register}>Register</button>
63
+ <button onClick={login}>Log in</button>
64
+ <p>{msg}</p>
65
+ </main>
66
+ );
67
+ }
68
+ ```
69
+
70
+ ```ts
71
+ // server/routes/Secret.ts, a route only a logged-in user can reach
72
+ import { Response } from 'toiljs/server/runtime';
73
+
74
+ @rest('secret')
75
+ class Secret {
76
+ @auth // 401 without a valid session
77
+ @get('/')
78
+ public secret(): Response {
79
+ const user = AuthService.getUser()!; // typed: { toilUserId, username }
80
+ return Response.text('hello ' + user.username + '\n');
81
+ }
82
+ }
83
+ ```
84
+
85
+ That's a real, production-grade auth system, the password is stretched with Argon2id in the browser into
86
+ an ML-DSA-44 key pair, your server only ever stores a public key, and login is a mutually-authenticated
87
+ ML-KEM-768 challenge. You didn't write any of it.
88
+
89
+ ## Where to go next
90
+
91
+ - **[How it works](./how-it-works.md)**: the protocol (OPRF + Argon2id + ML-DSA + ML-KEM), sessions,
92
+ cookies, and the `ToilUserId`, with sequence diagrams.
93
+ - **[Usage](./usage.md)**: enabling it, the client API, guarding routes, reading the user, the full wire
94
+ contract of each endpoint.
95
+ - **[Configuration](./configuration.md)**: the secrets a deployment MUST set, the audience/domain, tuning
96
+ Argon2id, and the deploy checklist.
97
+ - **[Extending & integrating](./extending.md)**: `ToilUserId`, keying your own data on a user, a custom
98
+ user shape / opting out, and the `AuthService` primitive reference.
99
+
100
+ > **One rule before you ship:** built-in auth runs with **insecure DEV fallback secrets** so it Just Works
101
+ > locally. A deployment MUST set `AUTH_SESSION_SECRET`, `AUTH_OPRF_SEED`, and `AUTH_KEM_SK`. See
102
+ > [Configuration](./configuration.md).
@@ -0,0 +1,94 @@
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
+ ## Argon2id strength (known limitation)
75
+
76
+ The built-in controller currently uses **demo-light** Argon2id params (32 MiB, 2 iterations, 1 lane) so it
77
+ stays responsive in a browser tab. These are baked into the shipped controller today; **config-driven
78
+ tuning is a planned follow-up.** For a high-value production deployment you should either wait for the
79
+ config knob or hand-write your own controller with `≥ 256 MiB / ≥ 3 iterations`. The OPRF still provides the
80
+ primary offline-attack resistance regardless, but raise these before protecting anything sensitive.
81
+
82
+ The client always derives against whatever params the server returns in `/login/start`, so when the config
83
+ knob lands you can raise them server-side with **no client change**.
84
+
85
+ ## Deploy checklist
86
+
87
+ - [ ] `AUTH_SESSION_SECRET` set (32 random bytes), identical on every edge instance.
88
+ - [ ] `AUTH_OPRF_SEED` set (32 random bytes).
89
+ - [ ] `AUTH_KEM_SK` set (an ML-KEM-768 secret key), and its **public** half pinned in the client via
90
+ `serverKemPublicKey`.
91
+ - [ ] `TOIL_AUTH_DOMAIN` set if you serve multiple hostnames (stable `ToilUserId`).
92
+ - [ ] (Recommended) Argon2id params reviewed for your threat model.
93
+
94
+ The CLI doctor warns when `server.auth` is on and the secrets are missing, run it before you ship.
@@ -0,0 +1,202 @@
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.