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.
- package/.github/workflows/ci.yml +31 -0
- package/CHANGELOG.md +15 -0
- package/build/cli/.tsbuildinfo +1 -1
- package/build/cli/index.js +311 -118
- package/build/client/.tsbuildinfo +1 -1
- package/build/client/index.d.ts +1 -1
- package/build/client/index.js +1 -1
- package/build/client/routing/mount.js +12 -1
- package/build/client/ssr/markers.d.ts +1 -0
- package/build/client/ssr/markers.js +3 -0
- package/build/compiler/.tsbuildinfo +1 -1
- package/build/compiler/config.d.ts +21 -0
- package/build/compiler/config.js +35 -0
- package/build/compiler/docs.d.ts +2 -1
- package/build/compiler/docs.js +33 -304
- package/build/compiler/index.d.ts +13 -0
- package/build/compiler/index.js +113 -21
- package/build/compiler/template-build.d.ts +21 -1
- package/build/compiler/template-build.js +110 -26
- package/build/compiler/toil-docs.generated.d.ts +1 -0
- package/build/compiler/toil-docs.generated.js +20 -0
- package/build/devserver/.tsbuildinfo +1 -1
- package/build/devserver/daemon/catalog.d.ts +26 -0
- package/build/devserver/daemon/catalog.js +48 -0
- package/build/devserver/daemon/cron.d.ts +4 -0
- package/build/devserver/daemon/cron.js +50 -0
- package/build/devserver/daemon/host.d.ts +37 -0
- package/build/devserver/daemon/host.js +94 -0
- package/build/devserver/daemon/index.d.ts +34 -0
- package/build/devserver/daemon/index.js +241 -0
- package/build/devserver/db/catalog.d.ts +2 -0
- package/build/devserver/db/catalog.js +80 -0
- package/build/devserver/db/database.d.ts +80 -0
- package/build/devserver/db/database.js +1032 -0
- package/build/devserver/db/index.d.ts +3 -0
- package/build/devserver/db/index.js +3 -0
- package/build/devserver/db/routeKinds.d.ts +8 -0
- package/build/devserver/db/routeKinds.js +139 -0
- package/build/devserver/db/types.d.ts +121 -0
- package/build/devserver/db/types.js +52 -0
- package/build/devserver/email/index.js +1 -1
- package/build/devserver/index.d.ts +19 -24
- package/build/devserver/index.js +11 -165
- package/build/devserver/mstore/store.d.ts +18 -0
- package/build/devserver/mstore/store.js +82 -0
- package/build/devserver/{host.d.ts → runtime/host.d.ts} +7 -1
- package/build/devserver/{host.js → runtime/host.js} +51 -7
- package/build/devserver/{module.d.ts → runtime/module.d.ts} +2 -1
- package/build/devserver/{module.js → runtime/module.js} +34 -1
- package/build/devserver/server.d.ts +23 -0
- package/build/devserver/server.js +223 -0
- package/build/devserver/ssr.d.ts +25 -0
- package/build/devserver/ssr.js +114 -0
- package/build/devserver/wasm/sections.d.ts +2 -0
- package/build/devserver/wasm/sections.js +42 -0
- package/build/devserver/wasm/surface.d.ts +18 -0
- package/build/devserver/wasm/surface.js +41 -0
- package/docs/README.md +4 -4
- package/docs/auth-todo.md +6 -6
- package/docs/caching.md +5 -5
- package/docs/cli.md +15 -0
- package/docs/client.md +40 -0
- package/docs/crypto.md +4 -4
- package/docs/data.md +6 -6
- package/docs/email.md +28 -28
- package/docs/environment.md +10 -10
- package/docs/index.md +26 -0
- package/docs/ratelimit.md +10 -10
- package/docs/routing.md +2 -2
- package/docs/server.md +61 -0
- package/docs/ssr.md +561 -113
- package/docs/styling.md +22 -0
- package/docs/time.md +3 -3
- package/eslint.config.js +10 -1
- package/examples/basic/client/components/Header.tsx +3 -0
- package/examples/basic/client/routes/features/actions.tsx +0 -2
- package/examples/basic/client/routes/hello.tsx +89 -19
- package/examples/basic/client/styles/main.css +48 -0
- package/examples/basic/server/SsrHelloRender.ts +97 -0
- package/examples/basic/server/main.ts +5 -0
- package/examples/basic/server/migrations/GuestEntry.migration.ts +39 -0
- package/examples/basic/server/streams/Echo.ts +49 -0
- package/package.json +12 -10
- package/scripts/gen-toil-docs.mjs +96 -0
- package/server/runtime/time.ts +3 -3
- package/src/cli/create.ts +40 -3
- package/src/cli/db.ts +158 -0
- package/src/cli/diagnostics.ts +19 -0
- package/src/cli/doctor.ts +20 -0
- package/src/cli/index.ts +10 -0
- package/src/cli/update.ts +58 -0
- package/src/client/index.ts +1 -1
- package/src/client/routing/mount.tsx +18 -2
- package/src/client/ssr/markers.tsx +22 -0
- package/src/compiler/config.ts +88 -2
- package/src/compiler/docs.ts +47 -308
- package/src/compiler/index.ts +236 -32
- package/src/compiler/ssr-codegen.ts +1 -1
- package/src/compiler/template-build.ts +247 -46
- package/src/compiler/toil-docs.generated.ts +26 -0
- package/src/devserver/daemon/catalog.ts +120 -0
- package/src/devserver/daemon/cron.ts +87 -0
- package/src/devserver/daemon/host.ts +224 -0
- package/src/devserver/daemon/index.ts +349 -0
- package/src/devserver/db/catalog.ts +108 -0
- package/src/devserver/db/database.ts +1633 -0
- package/src/devserver/db/index.ts +18 -0
- package/src/devserver/db/routeKinds.ts +147 -0
- package/src/devserver/db/types.ts +139 -0
- package/src/devserver/email/index.ts +1 -1
- package/src/devserver/index.ts +31 -287
- package/src/devserver/mstore/store.ts +121 -0
- package/src/devserver/{host.ts → runtime/host.ts} +98 -7
- package/src/devserver/{module.ts → runtime/module.ts} +47 -1
- package/src/devserver/server.ts +393 -0
- package/src/devserver/ssr.ts +166 -0
- package/src/devserver/wasm/sections.ts +59 -0
- package/src/devserver/wasm/surface.ts +88 -0
- package/test/daemon-build.test.ts +198 -0
- package/test/daemon-catalog.test.ts +265 -0
- package/test/daemon-emulation.test.ts +216 -0
- package/test/db.test.ts +0 -0
- package/test/devserver-database.test.ts +510 -14
- package/test/devserver-pqauth.test.ts +1 -1
- package/test/devserver-secrets.test.ts +5 -1
- package/test/doctor.test.ts +13 -0
- package/test/email-preview.test.ts +6 -1
- package/test/example-guestbook.test.ts +43 -1
- package/test/fixtures/daemon-app.ts +56 -0
- package/test/global-setup.ts +17 -0
- package/test/pqauth-e2e.test.ts +1 -1
- package/test/ssr-render.test.ts +94 -27
- package/test/ssr-template.test.tsx +44 -1
- package/vitest.config.ts +3 -0
- package/build/devserver/database.d.ts +0 -8
- package/build/devserver/database.js +0 -418
- package/src/devserver/database.ts +0 -618
- /package/build/devserver/{dotenv.d.ts → config/dotenv.d.ts} +0 -0
- /package/build/devserver/{dotenv.js → config/dotenv.js} +0 -0
- /package/build/devserver/{env.d.ts → config/env.d.ts} +0 -0
- /package/build/devserver/{env.js → config/env.js} +0 -0
- /package/build/devserver/{ratelimit.d.ts → config/ratelimit.d.ts} +0 -0
- /package/build/devserver/{ratelimit.js → config/ratelimit.js} +0 -0
- /package/build/devserver/{cache.d.ts → http/cache.d.ts} +0 -0
- /package/build/devserver/{cache.js → http/cache.js} +0 -0
- /package/build/devserver/{envelope.d.ts → http/envelope.d.ts} +0 -0
- /package/build/devserver/{envelope.js → http/envelope.js} +0 -0
- /package/build/devserver/{proxy.d.ts → http/proxy.d.ts} +0 -0
- /package/build/devserver/{proxy.js → http/proxy.js} +0 -0
- /package/build/devserver/{crypto.d.ts → runtime/crypto.d.ts} +0 -0
- /package/build/devserver/{crypto.js → runtime/crypto.js} +0 -0
- /package/src/devserver/{dotenv.ts → config/dotenv.ts} +0 -0
- /package/src/devserver/{env.ts → config/env.ts} +0 -0
- /package/src/devserver/{ratelimit.ts → config/ratelimit.ts} +0 -0
- /package/src/devserver/{cache.ts → http/cache.ts} +0 -0
- /package/src/devserver/{envelope.ts → http/envelope.ts} +0 -0
- /package/src/devserver/{proxy.ts → http/proxy.ts} +0 -0
- /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
|
|
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"
|
|
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
|
|
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
|
|
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
|
|
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
|
|
75
|
+
## Tier 2, protocol hardening
|
|
76
76
|
|
|
77
|
-
### 2.6 A properly bound session key
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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)
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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)
|
|
22
|
+
- `encodeInto(w: DataWriter)` / `static decodeFrom(r: DataReader)`, the codec
|
|
23
23
|
without the type-id frame, for nesting.
|
|
24
|
-
- `toJSON()` / `static fromJSON(v)
|
|
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
|
|
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
|
|
51
|
-
challenge messages
|
|
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
|
|
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
|
|
14
|
-
- **`EmailTemplate
|
|
13
|
+
- **`EmailService`**, send one email.
|
|
14
|
+
- **`EmailTemplate`**, a reusable template with `{{placeholder}}` substitution
|
|
15
15
|
(plain text and/or HTML).
|
|
16
|
-
- **`emails/*.tsx
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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"`)
|
|
66
|
+
**Resend** (`provider = "resend"`), a JSON API; `api_key` holds the API key.
|
|
67
67
|
|
|
68
|
-
**Gmail** (`TOIL_EMAIL_PROVIDER=gmail`)
|
|
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`)
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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)
|
|
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)
|
|
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)
|
|
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)
|
|
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)
|
|
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
|
|
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
|
|
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
|
|
309
|
+
- **Per-recipient cap**, `max_per_recipient_per_hour`. Over it →
|
|
310
310
|
`RecipientCapped`.
|
|
311
|
-
- **Dedup
|
|
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
|
|
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)
|
|
324
|
+
- [Rate limiting](./ratelimit.md), protect your routes (including any email
|
|
325
325
|
trigger) with `@ratelimit`.
|
|
326
|
-
- [Web Crypto](./crypto.md)
|
|
326
|
+
- [Web Crypto](./crypto.md), the `crypto` global `TwoFactor` builds on.
|
package/docs/environment.md
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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)
|
|
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
|
|
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
|
|
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
|
|
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
|
|
31
|
+
- **`strategy`**, a `RateLimit` value (ambient global, no import):
|
|
32
32
|
`RateLimit.FixedWindow`, `RateLimit.SlidingWindow`, or `RateLimit.TokenBucket`.
|
|
33
|
-
- **`limit`** and **`window
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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)
|
|
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)
|
|
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
|
|
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
|
|
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.
|