toiljs 0.0.59 → 0.0.61

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 (158) hide show
  1. package/.github/workflows/ci.yml +31 -0
  2. package/CHANGELOG.md +15 -0
  3. package/build/cli/.tsbuildinfo +1 -1
  4. package/build/cli/index.js +311 -118
  5. package/build/client/.tsbuildinfo +1 -1
  6. package/build/client/index.d.ts +1 -1
  7. package/build/client/index.js +1 -1
  8. package/build/client/routing/mount.js +12 -1
  9. package/build/client/ssr/markers.d.ts +1 -0
  10. package/build/client/ssr/markers.js +3 -0
  11. package/build/compiler/.tsbuildinfo +1 -1
  12. package/build/compiler/config.d.ts +21 -0
  13. package/build/compiler/config.js +35 -0
  14. package/build/compiler/docs.d.ts +2 -1
  15. package/build/compiler/docs.js +33 -304
  16. package/build/compiler/index.d.ts +13 -0
  17. package/build/compiler/index.js +113 -21
  18. package/build/compiler/template-build.d.ts +21 -1
  19. package/build/compiler/template-build.js +110 -26
  20. package/build/compiler/toil-docs.generated.d.ts +1 -0
  21. package/build/compiler/toil-docs.generated.js +20 -0
  22. package/build/devserver/.tsbuildinfo +1 -1
  23. package/build/devserver/daemon/catalog.d.ts +26 -0
  24. package/build/devserver/daemon/catalog.js +48 -0
  25. package/build/devserver/daemon/cron.d.ts +4 -0
  26. package/build/devserver/daemon/cron.js +50 -0
  27. package/build/devserver/daemon/host.d.ts +37 -0
  28. package/build/devserver/daemon/host.js +94 -0
  29. package/build/devserver/daemon/index.d.ts +34 -0
  30. package/build/devserver/daemon/index.js +241 -0
  31. package/build/devserver/db/catalog.d.ts +2 -0
  32. package/build/devserver/db/catalog.js +80 -0
  33. package/build/devserver/db/database.d.ts +80 -0
  34. package/build/devserver/db/database.js +1032 -0
  35. package/build/devserver/db/index.d.ts +3 -0
  36. package/build/devserver/db/index.js +3 -0
  37. package/build/devserver/db/routeKinds.d.ts +8 -0
  38. package/build/devserver/db/routeKinds.js +139 -0
  39. package/build/devserver/db/types.d.ts +121 -0
  40. package/build/devserver/db/types.js +52 -0
  41. package/build/devserver/email/index.js +1 -1
  42. package/build/devserver/index.d.ts +19 -24
  43. package/build/devserver/index.js +11 -165
  44. package/build/devserver/mstore/store.d.ts +18 -0
  45. package/build/devserver/mstore/store.js +82 -0
  46. package/build/devserver/{host.d.ts → runtime/host.d.ts} +7 -1
  47. package/build/devserver/{host.js → runtime/host.js} +51 -7
  48. package/build/devserver/{module.d.ts → runtime/module.d.ts} +2 -1
  49. package/build/devserver/{module.js → runtime/module.js} +34 -1
  50. package/build/devserver/server.d.ts +23 -0
  51. package/build/devserver/server.js +223 -0
  52. package/build/devserver/ssr.d.ts +25 -0
  53. package/build/devserver/ssr.js +114 -0
  54. package/build/devserver/wasm/sections.d.ts +2 -0
  55. package/build/devserver/wasm/sections.js +42 -0
  56. package/build/devserver/wasm/surface.d.ts +18 -0
  57. package/build/devserver/wasm/surface.js +41 -0
  58. package/docs/README.md +4 -4
  59. package/docs/auth-todo.md +6 -6
  60. package/docs/caching.md +5 -5
  61. package/docs/cli.md +15 -0
  62. package/docs/client.md +40 -0
  63. package/docs/crypto.md +4 -4
  64. package/docs/data.md +6 -6
  65. package/docs/email.md +28 -28
  66. package/docs/environment.md +10 -10
  67. package/docs/index.md +26 -0
  68. package/docs/ratelimit.md +10 -10
  69. package/docs/routing.md +2 -2
  70. package/docs/server.md +61 -0
  71. package/docs/ssr.md +561 -113
  72. package/docs/styling.md +22 -0
  73. package/docs/time.md +3 -3
  74. package/eslint.config.js +10 -1
  75. package/examples/basic/client/components/Header.tsx +3 -0
  76. package/examples/basic/client/routes/features/actions.tsx +0 -2
  77. package/examples/basic/client/routes/hello.tsx +89 -19
  78. package/examples/basic/client/styles/main.css +48 -0
  79. package/examples/basic/server/SsrHelloRender.ts +97 -0
  80. package/examples/basic/server/main.ts +5 -0
  81. package/examples/basic/server/migrations/GuestEntry.migration.ts +39 -0
  82. package/examples/basic/server/streams/Echo.ts +49 -0
  83. package/package.json +12 -10
  84. package/scripts/gen-toil-docs.mjs +96 -0
  85. package/server/runtime/time.ts +3 -3
  86. package/src/cli/create.ts +40 -3
  87. package/src/cli/db.ts +158 -0
  88. package/src/cli/diagnostics.ts +19 -0
  89. package/src/cli/doctor.ts +20 -0
  90. package/src/cli/index.ts +10 -0
  91. package/src/cli/update.ts +58 -0
  92. package/src/client/index.ts +1 -1
  93. package/src/client/routing/mount.tsx +18 -2
  94. package/src/client/ssr/markers.tsx +22 -0
  95. package/src/compiler/config.ts +88 -2
  96. package/src/compiler/docs.ts +47 -308
  97. package/src/compiler/index.ts +236 -32
  98. package/src/compiler/ssr-codegen.ts +1 -1
  99. package/src/compiler/template-build.ts +247 -46
  100. package/src/compiler/toil-docs.generated.ts +26 -0
  101. package/src/devserver/daemon/catalog.ts +120 -0
  102. package/src/devserver/daemon/cron.ts +87 -0
  103. package/src/devserver/daemon/host.ts +224 -0
  104. package/src/devserver/daemon/index.ts +349 -0
  105. package/src/devserver/db/catalog.ts +108 -0
  106. package/src/devserver/db/database.ts +1633 -0
  107. package/src/devserver/db/index.ts +18 -0
  108. package/src/devserver/db/routeKinds.ts +147 -0
  109. package/src/devserver/db/types.ts +139 -0
  110. package/src/devserver/email/index.ts +1 -1
  111. package/src/devserver/index.ts +31 -287
  112. package/src/devserver/mstore/store.ts +121 -0
  113. package/src/devserver/{host.ts → runtime/host.ts} +98 -7
  114. package/src/devserver/{module.ts → runtime/module.ts} +47 -1
  115. package/src/devserver/server.ts +393 -0
  116. package/src/devserver/ssr.ts +166 -0
  117. package/src/devserver/wasm/sections.ts +59 -0
  118. package/src/devserver/wasm/surface.ts +88 -0
  119. package/test/daemon-build.test.ts +198 -0
  120. package/test/daemon-catalog.test.ts +265 -0
  121. package/test/daemon-emulation.test.ts +216 -0
  122. package/test/db.test.ts +0 -0
  123. package/test/devserver-database.test.ts +510 -14
  124. package/test/devserver-pqauth.test.ts +1 -1
  125. package/test/devserver-secrets.test.ts +5 -1
  126. package/test/doctor.test.ts +13 -0
  127. package/test/email-preview.test.ts +6 -1
  128. package/test/example-guestbook.test.ts +43 -1
  129. package/test/fixtures/daemon-app.ts +56 -0
  130. package/test/global-setup.ts +17 -0
  131. package/test/pqauth-e2e.test.ts +1 -1
  132. package/test/ssr-render.test.ts +94 -27
  133. package/test/ssr-template.test.tsx +44 -1
  134. package/vitest.config.ts +3 -0
  135. package/build/devserver/database.d.ts +0 -8
  136. package/build/devserver/database.js +0 -418
  137. package/src/devserver/database.ts +0 -618
  138. /package/build/devserver/{dotenv.d.ts → config/dotenv.d.ts} +0 -0
  139. /package/build/devserver/{dotenv.js → config/dotenv.js} +0 -0
  140. /package/build/devserver/{env.d.ts → config/env.d.ts} +0 -0
  141. /package/build/devserver/{env.js → config/env.js} +0 -0
  142. /package/build/devserver/{ratelimit.d.ts → config/ratelimit.d.ts} +0 -0
  143. /package/build/devserver/{ratelimit.js → config/ratelimit.js} +0 -0
  144. /package/build/devserver/{cache.d.ts → http/cache.d.ts} +0 -0
  145. /package/build/devserver/{cache.js → http/cache.js} +0 -0
  146. /package/build/devserver/{envelope.d.ts → http/envelope.d.ts} +0 -0
  147. /package/build/devserver/{envelope.js → http/envelope.js} +0 -0
  148. /package/build/devserver/{proxy.d.ts → http/proxy.d.ts} +0 -0
  149. /package/build/devserver/{proxy.js → http/proxy.js} +0 -0
  150. /package/build/devserver/{crypto.d.ts → runtime/crypto.d.ts} +0 -0
  151. /package/build/devserver/{crypto.js → runtime/crypto.js} +0 -0
  152. /package/src/devserver/{dotenv.ts → config/dotenv.ts} +0 -0
  153. /package/src/devserver/{env.ts → config/env.ts} +0 -0
  154. /package/src/devserver/{ratelimit.ts → config/ratelimit.ts} +0 -0
  155. /package/src/devserver/{cache.ts → http/cache.ts} +0 -0
  156. /package/src/devserver/{envelope.ts → http/envelope.ts} +0 -0
  157. /package/src/devserver/{proxy.ts → http/proxy.ts} +0 -0
  158. /package/src/devserver/{crypto.ts → runtime/crypto.ts} +0 -0
@@ -0,0 +1,41 @@
1
+ import { DataReader } from 'toiljs/io';
2
+ import { customSection } from './sections.js';
3
+ export function parseSurface(wasm) {
4
+ let sec;
5
+ try {
6
+ sec = customSection(wasm, 'toil.surface');
7
+ }
8
+ catch {
9
+ return 'invalid';
10
+ }
11
+ if (sec === null)
12
+ return 'absent';
13
+ const r = new DataReader(sec);
14
+ r.readU16();
15
+ const targetMode = r.readU8() === 1 ? 'cold' : 'hot';
16
+ r.readU8();
17
+ const f = r.readU32();
18
+ const abiVersion = r.readU16();
19
+ const buildId = r.readString();
20
+ const fingerprint = r.readU32();
21
+ const dataCoherenceHash = r.readU32();
22
+ const pairCoherenceHash = r.readU32();
23
+ if (!r.ok)
24
+ return 'invalid';
25
+ return {
26
+ targetMode,
27
+ flags: {
28
+ rest: !!(f & 1),
29
+ stream: !!(f & 2),
30
+ daemon: !!(f & 4),
31
+ scheduled: !!(f & 8),
32
+ database: !!(f & 16),
33
+ render: !!(f & 32),
34
+ },
35
+ abiVersion,
36
+ buildId,
37
+ fingerprint,
38
+ dataCoherenceHash,
39
+ pairCoherenceHash,
40
+ };
41
+ }
package/docs/README.md CHANGED
@@ -34,7 +34,7 @@ and as a named export from `toiljs/server/runtime`.
34
34
  type, `AuthService` (post-quantum login, signed sessions, `getUser()`), and
35
35
  the client half.
36
36
  - [Environment variables & secrets](./environment.md): `Environment.get` /
37
- `getSecure` per-tenant config + secrets set out of band (GitHub-Actions
37
+ `getSecure`, per-tenant config + secrets set out of band (GitHub-Actions
38
38
  style), so the `.wasm` carries no credentials. Two disjoint buckets, read-only.
39
39
  - [Email](./email.md): `EmailService`, `EmailTemplate`, the `emails/` React
40
40
  template pipeline, the stateless `TwoFactor` codes, provider config
@@ -52,14 +52,14 @@ and as a named export from `toiljs/server/runtime`.
52
52
 
53
53
  ## Conventions
54
54
 
55
- - **"Global, no import"** a symbol marked `@global` in the runtime is in scope
55
+ - **"Global, no import"**, a symbol marked `@global` in the runtime is in scope
56
56
  everywhere in a tenant without an `import`, exactly like `crypto`. The
57
57
  matching named export exists so editors resolve the type and so the module is
58
58
  pulled into every build. Either form works.
59
- - **Binary, not JSON, on the hot paths** request/response bodies, sessions,
59
+ - **Binary, not JSON, on the hot paths**, request/response bodies, sessions,
60
60
  and cookies use the deterministic `DataWriter`/`DataReader` codec. JSON is
61
61
  available for `@rest` routes but binary is the default for anything
62
62
  performance- or security-sensitive.
63
- - **One fresh instance per request** guest memory is wiped between requests,
63
+ - **One fresh instance per request**, guest memory is wiped between requests,
64
64
  so nothing persists in module globals across requests. Use a host-backed store
65
65
  for anything that must outlive a single request.
package/docs/auth-todo.md CHANGED
@@ -24,7 +24,7 @@ imports in `toil-backend` (`crypto.mlkem_decapsulate`, `crypto.voprf_evaluate`),
24
24
 
25
25
  ---
26
26
 
27
- ## Tier 1 blocks production (cannot run on the edge yet)
27
+ ## Tier 1, blocks production (cannot run on the edge yet)
28
28
 
29
29
  ### 1.1 Storage on toildb (THE blocker) -- "when building toildb, consider this"
30
30
  The accounts + login challenges live in the **DEV-ONLY** `kv.*` Map
@@ -72,17 +72,17 @@ in-prod `mldsa_verify` import). Do before trusting in prod.
72
72
 
73
73
  ---
74
74
 
75
- ## Tier 2 protocol hardening
75
+ ## Tier 2, protocol hardening
76
76
 
77
- ### 2.6 A properly bound session key DONE (on main, unreleased)
77
+ ### 2.6 A properly bound session key, DONE (on main, unreleased)
78
78
  The session key is now `K = HMAC-SHA256(sharedSecret, SESSION_KEY_LABEL || H(M))` and the
79
79
  mutual-auth tag is `HMAC-SHA256(K, SERVER_CONFIRM_LABEL || H(M))` (`AuthService.deriveSessionKey`
80
80
  + `serverConfirmTag`; client mirrors it with hash-wasm `createHMAC`). REMAINING: binding the
81
81
  *session cookie* to the transport (so a stolen cookie is useless on another channel) needs
82
- the TLS exporter, which the wasm guest cannot see an edge/transport follow-up, not doable
82
+ the TLS exporter, which the wasm guest cannot see, an edge/transport follow-up, not doable
83
83
  purely in the guest.
84
84
 
85
- ### 2.7 Bind the KDF params + server key into the transcript DONE (on main, unreleased)
85
+ ### 2.7 Bind the KDF params + server key into the transcript, DONE (on main, unreleased)
86
86
  The single `buildLoginMessage` now binds the ML-KEM ciphertext, the Argon2id params
87
87
  (mem/iters/par), and `serverKemKeyId = SHA-256(serverKemPublicKey)`. Closes
88
88
  key-substitution and param-downgrade confusion. (There is ONE login message format, no
@@ -96,7 +96,7 @@ a reviewable artifact.
96
96
 
97
97
  ---
98
98
 
99
- ## Tier 3 account lifecycle (missing today)
99
+ ## Tier 3, account lifecycle (missing today)
100
100
 
101
101
  ### 3.1 Password change / key rotation
102
102
  Re-derive under a fresh salt while authenticated, re-register the new public key. None
package/docs/caching.md CHANGED
@@ -59,14 +59,14 @@ return Response.bytes(blob).cacheFor(5);
59
59
 
60
60
  The cache layer refuses to store anything unsafe, regardless of the directive:
61
61
 
62
- - **5xx** responses are never cached a server error is transient, and `@cache`
62
+ - **5xx** responses are never cached, a server error is transient, and `@cache`
63
63
  wraps the whole route, so a `@cache`d route that hits a blip returns its 500
64
64
  carrying the directive; caching it would serve the failure for the full TTL.
65
65
  **2xx, 3xx, and 4xx are cacheable** (a redirect or a `404`/`410` is a
66
66
  deterministic function of the request key);
67
67
  - a response that sets a **`Set-Cookie`** is never cached;
68
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
69
+ `allowAuth = true`, this prevents one user's personalized response from being
70
70
  served to another;
71
71
  - the edge TTL is **clamped to 24 hours**.
72
72
 
@@ -75,7 +75,7 @@ an unauthorized request is rejected with 401 before anything is cached, and a
75
75
  cached entry is only ever produced from a handler that actually ran.
76
76
 
77
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
78
+ (i.e. no `@cache` / `Response.cache(...)`) is never stored, there is no blind
79
79
  "cache every GET" mode, because an automatic window cannot tell a personalized
80
80
  response from a public one and would key it without a per-user component.
81
81
 
@@ -84,11 +84,11 @@ response from a public one and would key it without a per-user component.
84
84
  The edge cache is per-core and hard-capped so it can never exhaust node memory.
85
85
  It has two tiers:
86
86
 
87
- - **RAM tier** small, short-TTL responses. Bounded by a per-core byte budget
87
+ - **RAM tier**, small, short-TTL responses. Bounded by a per-core byte budget
88
88
  (each core holds at most ~128 MB) plus an entry-count cap; an insert that would
89
89
  exceed the budget drops expired entries first, then evicts the soonest-to-expire
90
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**
91
+ - **Disk tier (spill)**, when the operator enables `--spill-dir`, a **big**
92
92
  (over the ~256 KB RAM cap) or **long-TTL** (≥ 10 min) cacheable response is
93
93
  written to disk instead and served back zero-RAM via a memory map, the same way
94
94
  static files are served. This keeps the RAM tier for the hot working set while
package/docs/cli.md ADDED
@@ -0,0 +1,15 @@
1
+ # CLI
2
+
3
+ - `toiljs create [name]`, scaffold a project. Flags: `--template app|minimal`,
4
+ `--style css|sass|less|stylus`, `--tailwind`, `--no-ai`, `-y`/`--yes`.
5
+ - `toiljs dev`, dev server with HMR (`--port`, `--root`). With a `toilconfig.json` it builds
6
+ the server first, then rebuilds it whenever a `server/` file changes (regenerating
7
+ `shared/server.ts`, which Vite HMRs into the client); client-only edits just HMR the client.
8
+ - `toiljs build`, production build. With a `toilconfig.json` it builds the server (toilscript,
9
+ regenerating `shared/server.ts`) first, then the client → `build/client`. `--server` builds
10
+ only the server. Every `server/` file declaring a surface (`@data`/`@rest`/...) is compiled.
11
+ - `toiljs start`, self-host the built app (hyper-express) with a `/_toil` WebSocket channel.
12
+ - `toiljs configure`, toggle styling features on an existing project (see [styling.md](./styling.md)).
13
+ - `toiljs doctor`, diagnose project setup (`--json` for CI). `--fix` auto-wires the typed-RPC
14
+ setup (build scripts, tsconfig `shared` + alias, `.gitignore`, toilscript version, and the
15
+ toilscript prettier plugin) so an existing project upgrades in one command.
package/docs/client.md ADDED
@@ -0,0 +1,40 @@
1
+ # Client runtime
2
+
3
+ Everything is on the `Toil` global, no imports needed in route files.
4
+
5
+ ## Entry
6
+
7
+ `client/toil.tsx` imports the route table + global styles and mounts the app:
8
+
9
+ ```tsx
10
+ import { routes, layout, notFound } from "toiljs/routes";
11
+ import "./styles/main.css";
12
+ Toil.mount(routes, layout, notFound);
13
+ ```
14
+
15
+ ## API (on `Toil`)
16
+
17
+ - Components: `Link`, `NavLink`, `Head`
18
+ - Navigation: `navigate`, `useRouter`, `useNavigate`
19
+ - Location: `usePathname`, `useSearchParams`, `useParams`, `useNavigationPending`
20
+ - Data: `useLoaderData` (see [routing.md](./routing.md))
21
+ - Head: `useHead`, `useTitle`, `<Head>`, set the `<title>` / meta per route
22
+ - Realtime: `useChannel`, `connectChannel` (WebSocket to the backend at `/_toil`)
23
+ - IO globals (no `Toil.` prefix): `FastMap`, `FastSet`, `DataWriter`, `DataReader`
24
+ - `parseError(err)` global: message from an unknown caught value (handy in `catch`)
25
+ - `Server` global: the typed RPC surface generated from the server (see [server.md](./server.md))
26
+ - `Server.REST.<controller>.<route>(args)`: a working, typed `fetch` client for your
27
+ `@rest` controllers, e.g. `await Server.REST.todos.getTodo({ params: { id } })` or
28
+ `await Server.REST.todos.add({ body: new AddTodo("milk") })`. `args` is
29
+ `{ params?, body?, query?, headers? }`; returns are typed (`@data` classes are parsed for
30
+ you). The REST client attaches when you import from `shared/server`.
31
+
32
+ ## Head example
33
+
34
+ ```tsx
35
+ Toil.useHead({
36
+ title: "Blog",
37
+ titleTemplate: "%s, MyApp",
38
+ meta: [{ name: "description", content: "..." }],
39
+ });
40
+ ```
package/docs/crypto.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  The guest gets a synchronous Web Crypto surface through the ambient `crypto`
4
4
  global, backed by host functions. It mirrors the browser `crypto` /
5
- `crypto.subtle` API but **without Promises** ToilScript has no `async`, so
5
+ `crypto.subtle` API but **without Promises**, ToilScript has no `async`, so
6
6
  every call returns its result directly. Keys are opaque per-request handles in a
7
7
  host keystore; a `CryptoKey` is valid only for the request that created it.
8
8
 
@@ -84,7 +84,7 @@ Each algorithm has a small params class you pass to `importKey`/`sign`/etc.:
84
84
  - **Key formats:** `FMT_RAW`, `FMT_PKCS8`, `FMT_SPKI` (`FMT_JWK` is rejected).
85
85
  - **Usages (bitmask):** `USAGE_ENCRYPT`, `USAGE_DECRYPT`, `USAGE_SIGN`,
86
86
  `USAGE_VERIFY`, `USAGE_DERIVE_KEY`, `USAGE_DERIVE_BITS`, `USAGE_WRAP_KEY`,
87
- `USAGE_UNWRAP_KEY` OR them together.
87
+ `USAGE_UNWRAP_KEY`, OR them together.
88
88
  - **Named curves:** `CURVE_P256`, `CURVE_P384` (`CURVE_P521` is not supported).
89
89
 
90
90
  ### `CryptoKey`
@@ -116,7 +116,7 @@ const ct = crypto.subtle.encrypt(new AesGcmParams(iv, aad, 128), k, plaintext);
116
116
  ## Post-quantum verify
117
117
 
118
118
  The host also exposes ML-DSA-44 (FIPS 204) signature verification as
119
- `crypto.mldsa_verify`. It is verify-only the host never holds a secret key and
119
+ `crypto.mldsa_verify`. It is verify-only, the host never holds a secret key, and
120
120
  underpins the [auth primitive](./auth.md). Most code reaches it through
121
121
  `AuthService.verifyLogin(publicKey, message, signature)` rather than calling the
122
122
  import directly. Public key is 1312 bytes, signature 2420 bytes, with a FIPS 204
@@ -124,7 +124,7 @@ domain-separation context.
124
124
 
125
125
  ## Limitations
126
126
 
127
- - **No Promises** every call is synchronous.
127
+ - **No Promises**, every call is synchronous.
128
128
  - **No RSA** and **no JWK** key format.
129
129
  - **P-521** is not supported (P-256 and P-384 are).
130
130
  - Signature *generation* for ML-DSA is client-side only; the server verifies.
package/docs/data.md CHANGED
@@ -17,13 +17,13 @@ class Player {
17
17
 
18
18
  From that the compiler synthesizes, on the class:
19
19
 
20
- - `encode(): Uint8Array` / `static decode(buf): T` the binary codec (with a
20
+ - `encode(): Uint8Array` / `static decode(buf): T`, the binary codec (with a
21
21
  4-byte type id prefix).
22
- - `encodeInto(w: DataWriter)` / `static decodeFrom(r: DataReader)` the codec
22
+ - `encodeInto(w: DataWriter)` / `static decodeFrom(r: DataReader)`, the codec
23
23
  without the type-id frame, for nesting.
24
- - `toJSON()` / `static fromJSON(v)` the JSON codec (64-bit-and-larger integers
24
+ - `toJSON()` / `static fromJSON(v)`, the JSON codec (64-bit-and-larger integers
25
25
  as decimal strings, so they survive `JSON.parse` exactly).
26
- - `static dataId(): u32` a stable FNV-1a hash of the class name, written as the
26
+ - `static dataId(): u32`, a stable FNV-1a hash of the class name, written as the
27
27
  type-id prefix by `encode()`.
28
28
 
29
29
  Fields may be scalars (`u8`..`u256`, `i8`..`i256`, `f32`, `f64`, `bool`),
@@ -47,8 +47,8 @@ public blob(input: FileData): FileResult { /* input.decode, result.encode */ }
47
47
 
48
48
  ## The binary codec: `DataWriter` / `DataReader`
49
49
 
50
- When you need to lay out bytes yourself custom bodies, session payloads,
51
- challenge messages use the codec directly. It lives in the `data` module:
50
+ When you need to lay out bytes yourself, custom bodies, session payloads,
51
+ challenge messages, use the codec directly. It lives in the `data` module:
52
52
 
53
53
  ```ts
54
54
  import { DataWriter, DataReader } from 'data';
package/docs/email.md CHANGED
@@ -5,17 +5,17 @@ toiljs can send transactional email from a route handler. A handler calls
5
5
  `emails/` folder, or the stateless `TwoFactor` helper); the edge hands the
6
6
  message to a single off-core mailer thread that talks to the provider over the
7
7
  kernel network (never the worker cores), and **suspends** the wasm call until the
8
- provider responds so a slow send never blocks the worker.
8
+ provider responds, so a slow send never blocks the worker.
9
9
 
10
10
  Everything here is an ambient **global** (no import), like `crypto` and
11
11
  `AuthService`. A tenant that never sends email pulls none of it into its build.
12
12
 
13
- - **`EmailService`** send one email.
14
- - **`EmailTemplate`** a reusable template with `{{placeholder}}` substitution
13
+ - **`EmailService`**, send one email.
14
+ - **`EmailTemplate`**, a reusable template with `{{placeholder}}` substitution
15
15
  (plain text and/or HTML).
16
- - **`emails/*.tsx`** author emails as React components; the build renders them
16
+ - **`emails/*.tsx`**, author emails as React components; the build renders them
17
17
  to static HTML and generates a typed `Emails.<Name>.send(...)`.
18
- - **`TwoFactor`** stateless email verification codes (2FA / confirm), no DB.
18
+ - **`TwoFactor`**, stateless email verification codes (2FA / confirm), no DB.
19
19
 
20
20
  > **The one rule of HTML email:** email clients run **no JavaScript** and strip
21
21
  > `<style>`/external CSS. So HTML email is a static, inline-styled string, and
@@ -24,7 +24,7 @@ Everything here is an ambient **global** (no import), like `crypto` and
24
24
 
25
25
  ## Configure email
26
26
 
27
- Email is a **framework-reserved namespace of the tenant's environment** the
27
+ Email is a **framework-reserved namespace of the tenant's environment**, the
28
28
  same out-of-band [Environment](./environment.md) store that backs
29
29
  `Environment.get` / `getSecure`, but the `[email]` block is **host-only**: it is
30
30
  read and used in Rust (the off-core mailer) and is **never exposed to the
@@ -35,7 +35,7 @@ On the edge today it lives in the tenant's env secrets file,
35
35
  `$TOIL_ENV_DIR/<host>.env.secrets` (default dir `/run/toil/env`), kept `0600` and
36
36
  **out of `hosts/`** so the config watcher never sees a credential (the dashboard /
37
37
  edge DB replaces this file later). Email config is a set of **reserved
38
- `TOIL_EMAIL_*` keys** host-only, stripped from the guest buckets, so a tenant
38
+ `TOIL_EMAIL_*` keys**, host-only, stripped from the guest buckets, so a tenant
39
39
  can never read them via `Environment.getSecure`:
40
40
 
41
41
  ```bash
@@ -57,15 +57,15 @@ reserved namespace the framework consumes.
57
57
  When `enabled` is `false` (the default) the host has no email capability and
58
58
  `EmailService.send` returns `Disabled`. The env is loaded **lazily** (on the
59
59
  first send) and the `api_key` is materialized into a zeroizing secret in host
60
- memory never written to logs or `/_admin`. A malformed `[email]` block is
60
+ memory, never written to logs or `/_admin`. A malformed `[email]` block is
61
61
  treated as "no email" (the host returns `Disabled`); validate config on the
62
62
  dashboard before deploying.
63
63
 
64
64
  ### Providers
65
65
 
66
- **Resend** (`provider = "resend"`) a JSON API; `api_key` holds the API key.
66
+ **Resend** (`provider = "resend"`), a JSON API; `api_key` holds the API key.
67
67
 
68
- **Gmail** (`TOIL_EMAIL_PROVIDER=gmail`) SMTP with Gmail defaults:
68
+ **Gmail** (`TOIL_EMAIL_PROVIDER=gmail`), SMTP with Gmail defaults:
69
69
  `smtp.gmail.com`, port `587` (STARTTLS), username = `from`. `TOIL_EMAIL_API_KEY`
70
70
  holds a Gmail **App Password** (create one at
71
71
  <https://myaccount.google.com/apppasswords>; the account needs 2-Step
@@ -78,7 +78,7 @@ TOIL_EMAIL_PROVIDER=gmail
78
78
  TOIL_EMAIL_API_KEY=abcd efgh ijkl mnop
79
79
  ```
80
80
 
81
- **Generic SMTP** (`TOIL_EMAIL_PROVIDER=smtp`) any submission server (Outlook,
81
+ **Generic SMTP** (`TOIL_EMAIL_PROVIDER=smtp`), any submission server (Outlook,
82
82
  SendGrid/Mailgun SMTP, your own). Requires `TOIL_EMAIL_SMTP_HOST`; port defaults
83
83
  to `587` (STARTTLS), or set `465` for implicit TLS. `TOIL_EMAIL_SMTP_USER`
84
84
  defaults to `from`.
@@ -95,9 +95,9 @@ TOIL_EMAIL_SMTP_USER=noreply@example.com
95
95
 
96
96
  ### In dev
97
97
 
98
- `toiljs dev` runs the **full email pipeline** in Node recipient validation,
98
+ `toiljs dev` runs the **full email pipeline** in Node, recipient validation,
99
99
  dedup, and the per-minute / per-day / per-recipient caps all behave exactly like
100
- the edge and **really sends** once you configure a provider. Configure it in
100
+ the edge, and **really sends** once you configure a provider. Configure it in
101
101
  `toil.config.ts` (non-secret) with the API key in `.env.secrets`:
102
102
 
103
103
  ```ts
@@ -153,7 +153,7 @@ class Notify {
153
153
 
154
154
  | Status | Meaning | Retry? |
155
155
  | ----------------- | ----------------------------------------------------------- | ---------------- |
156
- | `Sent` | Accepted by the provider | |
156
+ | `Sent` | Accepted by the provider |, |
157
157
  | `Deduped` | An identical recent `(recipient, purpose)` was collapsed | treat as sent |
158
158
  | `Budget` | The host's per-minute budget is exhausted | yes, later |
159
159
  | `TryLater` | The mailer was saturated / a queue was full | yes, back off |
@@ -193,7 +193,7 @@ const status = welcome.send('alice@example.com', vars, 'welcome');
193
193
  - `template.render(vars)` returns the rendered `{ subject, body, html }` without
194
194
  sending (useful for preview/testing).
195
195
 
196
- For anything richer than `{{token}}` substitution real layout, CSS, brand
196
+ For anything richer than `{{token}}` substitution, real layout, CSS, brand,
197
197
  author the email as a React component instead.
198
198
 
199
199
  ## React email templates
@@ -247,7 +247,7 @@ Authoring notes:
247
247
  email name), `export const text` (a plain-text alternative; otherwise derived
248
248
  from the HTML), `export const purpose`.
249
249
  - **Build-time, field substitution only.** Because the component renders once at
250
- build, per-send data is `{{token}}` substitution a runtime `{items.map(...)}`
250
+ build, per-send data is `{{token}}` substitution, a runtime `{items.map(...)}`
251
251
  or conditional bakes in at build, it does not re-run per recipient. That covers
252
252
  transactional / 2FA / confirmation email; dynamic lists need a different
253
253
  approach.
@@ -265,7 +265,7 @@ editor. It refreshes live as you edit the template or its CSS.
265
265
  ## Email verification codes (`TwoFactor`)
266
266
 
267
267
  `TwoFactor` is a **stateless** email-code primitive (2FA, email confirmation,
268
- magic codes) no database. It emails a random code and returns a signed
268
+ magic codes), no database. It emails a random code and returns a signed
269
269
  **token** that commits to the code via HMAC, without putting the code in the
270
270
  token (the code is only in the email). Verification recomputes the HMAC from the
271
271
  token plus the user-entered code, so a valid `(token, code)` pair can only come
@@ -281,18 +281,18 @@ const challenge = TwoFactor.send('alice@example.com', 'login'); // emails the co
281
281
  const ok: bool = TwoFactor.verify(challenge.token, 'alice@example.com', userEntered);
282
282
  ```
283
283
 
284
- - **`send(recipient, purpose, ttlSecs = 600, digits = 6)`** issues a code,
284
+ - **`send(recipient, purpose, ttlSecs = 600, digits = 6)`**, issues a code,
285
285
  emails it with a built-in template, returns `{ token, status }`.
286
- - **`issue(recipient, purpose, ttlSecs, digits)`** returns `{ code, token }`
286
+ - **`issue(recipient, purpose, ttlSecs, digits)`**, returns `{ code, token }`
287
287
  **without** sending, so you can email `code` with your own `EmailTemplate` /
288
288
  `Emails.*` for a branded message.
289
- - **`verify(token, recipient, code)`** `true` only for a code issued for that
289
+ - **`verify(token, recipient, code)`**, `true` only for a code issued for that
290
290
  recipient that hasn't expired. Constant-time compare.
291
- - **`TwoFactor.setSecret(secret)`** the HMAC secret for the tokens. Call once
291
+ - **`TwoFactor.setSecret(secret)`**, the HMAC secret for the tokens. Call once
292
292
  at startup in `main.ts`; it must be identical on every edge instance and out of
293
293
  any client bundle. (This is separate from the provider `api_key`.)
294
294
 
295
- **Limitation:** this gives integrity + expiry but **not single-use** a valid
295
+ **Limitation:** this gives integrity + expiry but **not single-use**, a valid
296
296
  code verifies repeatedly within its TTL, because there is no server state to burn
297
297
  it. Keep the TTL short; for true single-use, store a per-recipient
298
298
  last-verified-at and reject at or before it.
@@ -302,13 +302,13 @@ last-verified-at and reject at or before it.
302
302
  All enforced authoritatively in the single mailer (so the counts are exact across
303
303
  all workers):
304
304
 
305
- - **Per-host budget** two rolling windows, both enforced: a 1-minute cap
305
+ - **Per-host budget**, two rolling windows, both enforced: a 1-minute cap
306
306
  (`max_per_min`) and a 24-hour cap (`max_per_day`, `0` = unlimited). Over either
307
307
  one → `Budget`. Each host's caps, in-window sends, and reject counts are visible
308
308
  per host at `GET /_admin/email`.
309
- - **Per-recipient cap** `max_per_recipient_per_hour`. Over it →
309
+ - **Per-recipient cap**, `max_per_recipient_per_hour`. Over it →
310
310
  `RecipientCapped`.
311
- - **Dedup** identical `(host, recipient, purpose)` within ~30s → `Deduped`.
311
+ - **Dedup**, identical `(host, recipient, purpose)` within ~30s → `Deduped`.
312
312
 
313
313
  Editing these in the host config takes effect on the next send (no restart).
314
314
 
@@ -316,11 +316,11 @@ Editing these in the host config takes effect on the next send (no restart).
316
316
 
317
317
  `GET /_admin/email` returns process-wide counters by reason (JSON), e.g.
318
318
  `submitted`, `sent`, `deduped`, `budget`, `recipient_capped`, `try_later`,
319
- `bad_recipient`, `provider_error`. It exposes **counts only** never a
319
+ `bad_recipient`, `provider_error`. It exposes **counts only**, never a
320
320
  recipient, code, subject, body, or secret.
321
321
 
322
322
  ## See also
323
323
 
324
- - [Rate limiting](./ratelimit.md) protect your routes (including any email
324
+ - [Rate limiting](./ratelimit.md), protect your routes (including any email
325
325
  trigger) with `@ratelimit`.
326
- - [Web Crypto](./crypto.md) the `crypto` global `TwoFactor` builds on.
326
+ - [Web Crypto](./crypto.md), the `crypto` global `TwoFactor` builds on.
@@ -2,7 +2,7 @@
2
2
 
3
3
  `Environment` gives a tenant **per-app environment variables and secrets**, set
4
4
  out of band (a dashboard, like GitHub Actions) so the deployed `.wasm` carries
5
- **no credentials**. It is read-only from app code there is no `set`; values are
5
+ **no credentials**. It is read-only from app code, there is no `set`; values are
6
6
  configured on the deployment side, never from the module.
7
7
 
8
8
  ```ts
@@ -20,15 +20,15 @@ class Cfg {
20
20
  }
21
21
  ```
22
22
 
23
- `Environment` is a global no import needed (like `EmailService` / `AuthService`).
23
+ `Environment` is a global, no import needed (like `EmailService` / `AuthService`).
24
24
 
25
25
  ## Two disjoint buckets
26
26
 
27
27
  Just like GitHub Actions' `vars` vs `secrets`:
28
28
 
29
- - **`Environment.get(key)`** reads **plain vars** non-sensitive config (a public
29
+ - **`Environment.get(key)`** reads **plain vars**, non-sensitive config (a public
30
30
  API base URL, a feature flag, a region). Returns the string, or `null`.
31
- - **`Environment.getSecure(key)`** reads **secrets** sensitive values (a
31
+ - **`Environment.getSecure(key)`** reads **secrets**, sensitive values (a
32
32
  third-party API key). Returns the string, or `null`.
33
33
 
34
34
  The buckets are **disjoint**: a secret is **never** returned by `get()`, and a
@@ -37,13 +37,13 @@ through a code path that logs the result of a `get()`. Keys are case-sensitive,
37
37
  exact-match.
38
38
 
39
39
  > Secrets you read with `getSecure` are plaintext in your module at runtime
40
- > (that's the point you need them to call out). Don't log them, don't put them
40
+ > (that's the point, you need them to call out). Don't log them, don't put them
41
41
  > in a response, and don't copy them into a client bundle.
42
42
 
43
43
  ## What is NOT here
44
44
 
45
- Framework-reserved namespaces (today: **email** provider config) are **host-only**
46
- resolved and used in Rust where the framework needs them, and **never exposed to
45
+ Framework-reserved namespaces (today: **email** provider config) are **host-only**,
46
+ resolved and used in Rust where the framework needs them, and **never exposed to
47
47
  the `.wasm`**. There is no `Environment.email`; you configure email in the
48
48
  `[email]` block of the same env file and the platform uses it for you (see
49
49
  [Email](./email.md)). The env imports only ever see your own `vars` / `secrets`.
@@ -53,7 +53,7 @@ the `.wasm`**. There is no `Environment.email`; you configure email in the
53
53
  Vars and secrets live in **two separate dotenv (`.env`) files**, so the disjoint
54
54
  split is structural and the secrets file can be locked down on its own. On the
55
55
  edge they are per host, **out of `hosts/`** (so the config watcher never sees a
56
- credential) the dashboard / edge database replaces them later:
56
+ credential), the dashboard / edge database replaces them later:
57
57
 
58
58
  ```bash
59
59
  # $TOIL_ENV_DIR/<host>.env (default dir /run/toil/env)
@@ -63,7 +63,7 @@ REGION=eu
63
63
  # $TOIL_ENV_DIR/<host>.env.secrets (mode 0600)
64
64
  STRIPE_KEY=sk_live_xxx # -> Environment.getSecure("STRIPE_KEY")
65
65
 
66
- # host-only email config reserved TOIL_EMAIL_* keys, NEVER exposed to the .wasm
66
+ # host-only email config, reserved TOIL_EMAIL_* keys, NEVER exposed to the .wasm
67
67
  TOIL_EMAIL_ENABLED=true
68
68
  TOIL_EMAIL_PROVIDER=resend
69
69
  TOIL_EMAIL_FROM=noreply@example.com
@@ -72,7 +72,7 @@ TOIL_EMAIL_API_KEY=re_xxx
72
72
 
73
73
  Each file is plain dotenv: `KEY=value` per line, `#` comments, optional `export`,
74
74
  optional quotes. Keys with the reserved **`TOIL_`** prefix are framework/host-only
75
- and are stripped from BOTH guest buckets a tenant can never read them via
75
+ and are stripped from BOTH guest buckets, a tenant can never read them via
76
76
  `get`/`getSecure` (see [Email](./email.md) for `TOIL_EMAIL_*`).
77
77
 
78
78
  On the edge, env is loaded **lazily** (the first time your code reads it) into a
package/docs/index.md ADDED
@@ -0,0 +1,26 @@
1
+ # toiljs
2
+
3
+ A full-stack React framework: a Vite-bundled client SPA with file-based routing, plus a
4
+ toilscript-to-WebAssembly server target.
5
+
6
+ ## Project layout
7
+
8
+ - `client/`, the app: `routes/` (file-based), `layout.tsx`, `components/`, `styles/`,
9
+ `public/`, and `toil.tsx` (the entry that calls `Toil.mount`).
10
+ - `server/`, the toilscript → WASM target (`@main` entry), compiled by `toilscript`.
11
+ `@data`/`@remote`/`@service` here generate the typed client `Server` API (see [server.md](./server.md)).
12
+ - `toil.config.ts`, configuration via `defineConfig` (`toiljs.config.ts` also works).
13
+ - Generated, gitignored, do not edit: `.toil/` (working dir), `toil-env.d.ts` (ambient
14
+ globals), `toil-routes.d.ts` (typed routes), `shared/server.ts` (the typed RPC module,
15
+ emitted by the server build; import `@data` classes from `shared/server`).
16
+
17
+ ## Key ideas
18
+
19
+ - `Toil` is a native global (no import): `Toil.Link`, `Toil.useRouter`, `Toil.useLoaderData`,
20
+ etc. The IO classes (`FastMap`, `FastSet`, `DataWriter`, `DataReader`), `parseError`, and the
21
+ generated `Server` RPC surface are globals too.
22
+ - Scripts: `npm run dev` (HMR), `npm run build` (→ `build/client` + `build/server`),
23
+ `npm start` (self-host the build).
24
+
25
+ See [routing.md](./routing.md), [client.md](./client.md), [styling.md](./styling.md),
26
+ [server.md](./server.md), [ssr.md](./ssr.md), [cli.md](./cli.md).
package/docs/ratelimit.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Rate limiting
2
2
 
3
- The `@ratelimit` decorator throttles **any** `@rest` route a login, a signup, a
3
+ The `@ratelimit` decorator throttles **any** `@rest` route, a login, a signup, a
4
4
  public API, an email trigger, anything. It is enforced at the edge, before your
5
5
  handler runs, and keyed by default on the connecting client's **unspoofable** IP,
6
6
  so it works as an abuse / brute-force control out of the box.
@@ -28,9 +28,9 @@ class Auth {
28
28
 
29
29
  `@ratelimit(strategy, limit, window)`:
30
30
 
31
- - **`strategy`** a `RateLimit` value (ambient global, no import):
31
+ - **`strategy`**, a `RateLimit` value (ambient global, no import):
32
32
  `RateLimit.FixedWindow`, `RateLimit.SlidingWindow`, or `RateLimit.TokenBucket`.
33
- - **`limit`** and **`window`** integer literals whose meaning depends on the
33
+ - **`limit`** and **`window`**, integer literals whose meaning depends on the
34
34
  strategy (see below).
35
35
 
36
36
  When a request is over the limit the edge returns **`429 Too Many Requests`**
@@ -39,7 +39,7 @@ guard runs **before `@auth`**, so unauthenticated floods are limited too.
39
39
 
40
40
  > Both arguments must be **integer literals** and the strategy a `RateLimit`
41
41
  > member (or a bare integer tag). A malformed decorator emits no guard rather
42
- > than miscompiling the same fail-safe rule as `@cache`.
42
+ > than miscompiling, the same fail-safe rule as `@cache`.
43
43
 
44
44
  ## Strategies
45
45
 
@@ -64,17 +64,17 @@ Examples:
64
64
 
65
65
  ## How requests are keyed
66
66
 
67
- By default the limiter keys on the **client IP** specifically the TCP peer
67
+ By default the limiter keys on the **client IP**, specifically the TCP peer
68
68
  address the edge observed (`ctx.clientIp()`), **not** a header like
69
69
  `X-Forwarded-For`, which a client can forge. That makes it a real abuse control:
70
70
  a caller can't reset their bucket by spoofing a header.
71
71
 
72
72
  The count is **exact across all 14 edge workers** (a given IP always maps to one
73
73
  authoritative shard), so the limit is global per route, not per worker. Only
74
- routes that opt in with `@ratelimit` ever pay anything the lock-free fast path
74
+ routes that opt in with `@ratelimit` ever pay anything, the lock-free fast path
75
75
  for everything else is untouched.
76
76
 
77
- > Each rate-limited route has its own independent limiter a limit on `/login`
77
+ > Each rate-limited route has its own independent limiter, a limit on `/login`
78
78
  > does not consume the budget of `/signup`.
79
79
 
80
80
  ## Notes and limits
@@ -82,14 +82,14 @@ for everything else is untouched.
82
82
  - **Route-level only.** Put `@ratelimit` on each route you want limited; there is
83
83
  no controller-wide form yet (unlike `@auth`).
84
84
  - **Keyed on IP.** The decorator keys on the peer IP today. (A per-user / custom
85
- key limiting by account instead of IP exists in the runtime but is not yet
85
+ key, limiting by account instead of IP, exists in the runtime but is not yet
86
86
  exposed through the decorator.)
87
87
  - **In dev.** `toiljs dev` runs a single-process mirror of the same three
88
88
  strategies, so a limited route behaves the same locally as on the edge.
89
89
 
90
90
  ## See also
91
91
 
92
- - [Email](./email.md) `@ratelimit` pairs well with email triggers (verification
92
+ - [Email](./email.md), `@ratelimit` pairs well with email triggers (verification
93
93
  codes, password resets) to blunt abuse.
94
- - [Auth, sessions, and `@user`](./auth.md) `@ratelimit` runs before the `@auth`
94
+ - [Auth, sessions, and `@user`](./auth.md), `@ratelimit` runs before the `@auth`
95
95
  guard, so it protects the login itself.
package/docs/routing.md CHANGED
@@ -134,11 +134,11 @@ serialized.
134
134
 
135
135
  Each route is either **JSON** (default) or **Binary**:
136
136
 
137
- - **JSON** the body is `JSON.parse`d and revived via the `@data` type's
137
+ - **JSON**, the body is `JSON.parse`d and revived via the `@data` type's
138
138
  `fromJSON`; the response is the type's `toJSON()`. 64-bit-and-larger integers
139
139
  cross the wire as decimal strings (exact at any size). Best for endpoints a
140
140
  browser or third party calls directly.
141
- - **Binary** the body is `Body.decode(bytes)` and the response is
141
+ - **Binary**, the body is `Body.decode(bytes)` and the response is
142
142
  `value.encode()`, using the deterministic `DataWriter`/`DataReader` codec. No
143
143
  precision loss, smaller, faster. Best for app-to-app and anything
144
144
  security-sensitive.