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
@@ -0,0 +1,235 @@
1
+ # Cookies
2
+
3
+ **Cookies let you store a small piece of state in the user's browser and read it back on the next request.** toiljs ships a complete cookie layer: read incoming cookies, set them on a response, and (optionally) sign or encrypt their values so they cannot be tampered with.
4
+
5
+ ## What a cookie is
6
+
7
+ A **cookie** is a tiny named value the browser holds for your site. The browser sends it back on every request to your site (in a `Cookie` request header), and you can change or add cookies by putting a `Set-Cookie` header on your response. Cookies are how a site remembers things between requests: a theme preference, a feature flag, a session id.
8
+
9
+ Two directions to keep straight:
10
+
11
+ - **Reading** happens on the `Request` (the browser sent its cookies to you).
12
+ - **Writing** happens on the `Response` (you tell the browser to store or clear a cookie).
13
+
14
+ ```mermaid
15
+ sequenceDiagram
16
+ participant B as Browser
17
+ participant S as Your handler
18
+ B->>S: Request<br/>Cookie: theme=dark
19
+ Note over S: req.cookie('theme') -> "dark"
20
+ S-->>B: Response<br/>Set-Cookie: theme=light; ...
21
+ Note over B: browser stores the new cookie<br/>and sends it next time
22
+ ```
23
+
24
+ The cookie types (`Cookie`, `Cookies`, `CookieMap`, `SecureCookies`, and the `SameSite` / `CookieEncoding` / `CookiePrefix` enums) are **ambient globals**: use them with no import, like `crypto`. They are also exported from `toiljs/server/runtime` if you prefer an explicit import.
25
+
26
+ ## Reading incoming cookies
27
+
28
+ On the `Request`, two methods cover almost everything:
29
+
30
+ ```ts
31
+ const theme = req.cookie('theme'); // one value: string | null
32
+ const jar = req.cookies(); // all of them, parsed once and cached
33
+ jar.get('theme'); // "dark", or null
34
+ jar.has('theme'); // true / false
35
+ ```
36
+
37
+ `req.cookies()` returns a `CookieMap`, an ordered name-to-value view. It is parsed once per request and cached, so calling it repeatedly is cheap.
38
+
39
+ ## Setting a cookie on a response
40
+
41
+ Build a cookie with the fluent `Cookie` builder, then attach it with `setCookie`. Every setter returns the cookie, so calls chain:
42
+
43
+ ```ts
44
+ import { Response } from 'toiljs/server/runtime';
45
+
46
+ return Response.json('{"ok":true}').setCookie(
47
+ Cookie.create('theme', 'dark')
48
+ .path('/')
49
+ .maxAge(60 * 60 * 24 * 365) // one year, in seconds
50
+ .sameSite(SameSite.Lax)
51
+ .secure(),
52
+ );
53
+ ```
54
+
55
+ Shorthands for the common cases:
56
+
57
+ ```ts
58
+ resp.setCookieKV('theme', 'dark'); // name=value, no attributes
59
+ resp.clearCookie('theme'); // delete it (empty value, expired)
60
+ ```
61
+
62
+ Each `setCookie` adds its own `Set-Cookie` header; cookies are never folded into one header.
63
+
64
+ ### The attributes you will actually use
65
+
66
+ | Method | Sets | What it means |
67
+ | --- | --- | --- |
68
+ | `path('/')` | `Path` | Which URL paths the cookie applies to. `/` means the whole site. |
69
+ | `maxAge(seconds)` | `Max-Age` | How long the browser keeps it. `0` or negative deletes it now. |
70
+ | `secure()` | `Secure` | Only send over HTTPS. Use it for anything that matters. |
71
+ | `httpOnly()` | `HttpOnly` | Hide it from JavaScript in the browser (blocks theft via XSS). Use it for session cookies. |
72
+ | `sameSite(SameSite.Lax)` | `SameSite` | Controls whether the cookie is sent on cross-site requests. `Lax` is a good default; `Strict` is tighter; `None` allows cross-site (and forces `Secure`). |
73
+
74
+ You rarely need the rest, but they exist: `domain`, `expires` (an absolute time instead of a duration), `partitioned` (CHIPS), `priority`, and `extension` for anything custom.
75
+
76
+ ## The `__Host-` and `__Secure-` prefixes
77
+
78
+ Two special name prefixes give the browser extra guarantees. They are not magic strings you invent; browsers recognize them and **refuse** to accept a cookie that carries the prefix without meeting its rules.
79
+
80
+ - **`__Secure-`**: the cookie must be `Secure` (HTTPS only).
81
+ - **`__Host-`**: the cookie must be `Secure`, have `Path=/`, and have **no** `Domain`. This locks the cookie to exactly your host, so a sibling subdomain cannot set or read it. It is the strongest option and the right choice for session cookies.
82
+
83
+ You do not spell the prefix yourself. Two helpers apply it and enforce the rules for you:
84
+
85
+ ```ts
86
+ Cookie.create('sid', 'abc123')
87
+ .httpOnly()
88
+ .sameSite(SameSite.Lax)
89
+ .maxAge(3600)
90
+ .asHostPrefixed(); // prepends __Host-, forces Secure + Path=/ + no Domain
91
+ ```
92
+
93
+ `asSecurePrefixed()` is the lighter version (prepends `__Secure-` and forces `Secure`).
94
+
95
+ > Under `toiljs dev`, browsers treat `http://localhost` as a secure context, so `Secure` and `__Host-` cookies work over plain HTTP locally. You do not need HTTPS to test them.
96
+
97
+ ## Worked example: a theme preference cookie
98
+
99
+ A tiny handler that reads a `theme` cookie and lets the user flip it. This is the canonical "remember a preference" pattern: a plain, non-secret value.
100
+
101
+ ```ts
102
+ import { Response, RouteContext } from 'toiljs/server/runtime';
103
+
104
+ @rest('prefs')
105
+ class Prefs {
106
+ // GET /prefs/theme -> current theme (defaults to "light")
107
+ @get('/theme')
108
+ read(ctx: RouteContext): Response {
109
+ const theme = ctx.request.cookie('theme');
110
+ return Response.text((theme != null ? theme : 'light') + '\n');
111
+ }
112
+
113
+ // POST /prefs/theme/dark -> remember "dark" for a year
114
+ @post('/theme/dark')
115
+ setDark(ctx: RouteContext): Response {
116
+ return Response.text('saved\n').setCookie(
117
+ Cookie.create('theme', 'dark')
118
+ .path('/')
119
+ .maxAge(60 * 60 * 24 * 365)
120
+ .sameSite(SameSite.Lax)
121
+ .secure(),
122
+ );
123
+ }
124
+ }
125
+ ```
126
+
127
+ A preference like this does not need to be secret or tamper-proof (the worst a user can do is change their own theme), so a plain cookie is fine. When the value **does** matter, reach for `SecureCookies`.
128
+
129
+ ## Secure cookies: signing and encryption
130
+
131
+ Sometimes a cookie value must not be forged or read by the user. `SecureCookies` covers both, built on the `crypto` global (no extra setup):
132
+
133
+ - **Signed** (`SecureCookies.signed(key)`): the value stays readable but is stamped with a signature (HMAC-SHA256) bound to the cookie's name. The user can see it but cannot change it or move it to another cookie without the signature failing. Use this for a value the client may read but must not tamper with.
134
+ - **Encrypted** (`SecureCookies.encrypted(key)`): the value is scrambled (AES-GCM) so the user cannot read **or** change it. Use this for something confidential.
135
+
136
+ The `key` is raw secret bytes you supply (load it from a secret via [`Environment.getSecure`](./environment.md), never hard-code it):
137
+
138
+ ```ts
139
+ // In a real app, load this from a secret, do not inline it.
140
+ const key = Uint8Array.wrap(String.UTF8.encode('0123456789abcdef0123456789abcdef'));
141
+
142
+ // Signed: readable but tamper-proof.
143
+ const signer = SecureCookies.signed(key);
144
+ const sealed = signer.sign('session', 'user-42');
145
+ const user = signer.unsign('session', sealed); // "user-42", or null if tampered
146
+
147
+ // Encrypted: unreadable and authenticated.
148
+ const box = SecureCookies.encrypted(key);
149
+ resp.setCookie(box.seal(Cookie.create('secret', 'top-secret').httpOnly()));
150
+ const secret = box.open(req.cookies(), 'secret'); // "top-secret", or null
151
+ ```
152
+
153
+ Two important safety properties:
154
+
155
+ - **Verification never crashes.** `unsign` and `decrypt` return `null` on a tampered, truncated, or wrong-key value; they never throw. That makes them safe to call directly on attacker-controlled input.
156
+ - **Values are bound to the cookie name.** A sealed value made for cookie `a` will not verify or decrypt under cookie `b`.
157
+
158
+ For HMAC keys, use 32 or more bytes. For AES, the key must be exactly 16 or 32 bytes (a wrong length is rejected immediately). You can rotate keys by sealing with a new key while still accepting an old one:
159
+
160
+ ```ts
161
+ const signer = SecureCookies.signed(newKey).addKey(oldKey); // seal with new, open with either
162
+ ```
163
+
164
+ ## Encoding is not encryption
165
+
166
+ One common mix-up. **Encoding** (the `CookieEncoding` on a `Cookie`, default percent-encoding) only makes a value safe to put on the wire; anyone can reverse it. **Signing / encryption** (`SecureCookies`) is the cryptographic protection and needs a secret key. If you want a value the user cannot forge, you need `SecureCookies`, not a fancier encoding.
167
+
168
+ ## Relationship to auth session cookies
169
+
170
+ You do not manage login cookies by hand. The [auth system](../auth/usage.md) sets and reads its own hardened, `__Host-` prefixed, signed session cookie for you, and enforces access with the `@auth` decorator. This page is for **your own** cookies (preferences, flags, small bits of state). If you find yourself building a login cookie from scratch, use auth instead; it already does the hard, security-sensitive parts correctly.
171
+
172
+ ## Advanced reference: `Cookie` and `Cookies` helpers
173
+
174
+ Most handlers only need the builders above. These extra members are here for lower-level work: validating a cookie before you send it, parsing a `Set-Cookie` header back into a `Cookie` (for a proxy or a test), or controlling the exact wire encoding.
175
+
176
+ ### More `Cookie` methods
177
+
178
+ | Method | Returns | What it does |
179
+ | --- | --- | --- |
180
+ | `validate()` | `CookieValidation` | Check the cookie against RFC 6265bis (name is a token, name+value within the 4096-byte cap, `Path` form, prefix rules, `SameSite=None` / `Partitioned` imply `Secure`, the 400-day lifetime cap) **without** sending it. Never throws. |
181
+ | `serialize(strict)` | `string` | Build the `Set-Cookie` value. Lenient by default (always emits a best-effort cookie); pass `serialize(true)` to **throw** on a hard violation instead. `setCookie(...)` calls this for you. |
182
+ | `withEncoding(enc)` | `Cookie` | Choose how the value goes on the wire: `CookieEncoding.Percent` (default), `.Raw`, or `.Base64Url`. Chains like the other setters. |
183
+ | `detectedPrefix()` | `CookiePrefix` | Report which special prefix the name carries (`CookiePrefix.Host`, `.Secure`, or `.None`), detected case-insensitively. |
184
+ | `encodedValue()` | `string` | The value after the chosen encoding is applied, exactly as it will appear on the wire. |
185
+ | `expiresRaw(date)` | `Cookie` | Set `Expires` to a verbatim date string (an escape hatch; it is **not** validated, unlike `expires(epochSeconds)`). |
186
+
187
+ `validate()` returns a `CookieValidation`, a small result object:
188
+
189
+ - `valid: bool` is `true` when there were no problems.
190
+ - `errors: Array<string>` holds one human-readable message per problem (empty when valid).
191
+
192
+ ```ts
193
+ // A __Host- cookie must be Secure with Path=/; here we forgot both.
194
+ const c = Cookie.create('__Host-sid', 'abc');
195
+ const check = c.validate();
196
+ if (!check.valid) {
197
+ // check.errors includes "__Host- prefix requires the Secure attribute"
198
+ // (asHostPrefixed() would have set those attributes for you)
199
+ }
200
+ ```
201
+
202
+ ### `Cookies` static helpers
203
+
204
+ `Cookies` is the read side, plus a couple of codec shortcuts. Every method is static, so call them as `Cookies.xxx(...)`.
205
+
206
+ | Call | Returns | What it does |
207
+ | --- | --- | --- |
208
+ | `Cookies.parse(header)` | `CookieMap` | Parse a request `Cookie` header (`a=1; b=2`) into a name-to-value map. Values are percent-decoded; malformed pairs are skipped, never thrown. |
209
+ | `Cookies.get(header, name)` | `string \| null` | Shorthand: parse `header` and return one value (or `null`). |
210
+ | `Cookies.serialize(name, value)` | `string` | One-shot `Set-Cookie` value for `name=value` with no attributes. For attributes, build a `Cookie` and call `serialize()`. |
211
+ | `Cookies.parseSetCookie(header)` | `Cookie` | Parse a `Set-Cookie` field value back into a `Cookie` (handy for proxies or tests). The value is kept verbatim (`CookieEncoding.Raw`) so re-serializing reproduces the original. |
212
+ | `Cookies.encodeValue(raw)` | `string` | Percent-encode a value the way the default `Cookie` encoding would. |
213
+ | `Cookies.decodeValue(enc)` | `string` | Percent-decode a value (the inverse of `encodeValue`). |
214
+
215
+ ```ts
216
+ // Round-trip a Set-Cookie header (for example, inspecting an upstream response in a proxy):
217
+ const cookie = Cookies.parseSetCookie('sid=abc123; Path=/; HttpOnly; SameSite=Lax');
218
+ cookie.name; // "sid"
219
+ cookie.serialize(); // "sid=abc123; Path=/; SameSite=Lax; HttpOnly"
220
+ ```
221
+
222
+ ## Gotchas
223
+
224
+ - **Read on the `Request`, write on the `Response`.** Setting a cookie does not change what `req.cookie(...)` returns for the current request; it takes effect on the browser's next request.
225
+ - **`maxAge` is in seconds.** `maxAge(3600)` is one hour, not one millisecond.
226
+ - **To delete a cookie, the `path` (and `domain`) must match** the ones you set it with. Use `clearCookie(name, path, domain)`.
227
+ - **A `Set-Cookie` opts a response out of edge caching.** By design, a response that sets a cookie is never edge-cached, because a cookie is per-user state. See [Caching](./caching.md).
228
+ - **Do not put secrets in a plain cookie value.** Use `SecureCookies.encrypted(...)`, and load the key from a [secret](./environment.md).
229
+
230
+ ## Related
231
+
232
+ - [Auth, sessions, and `@user`](../auth/usage.md): the built-in, hardened session cookie.
233
+ - [Crypto](./crypto.md): the primitives under `SecureCookies`.
234
+ - [Environment and secrets](./environment.md): where to store your signing / encryption key.
235
+ - [Caching](./caching.md): why setting a cookie disables edge caching.
@@ -0,0 +1,209 @@
1
+ # Crypto
2
+
3
+ A small, safe cryptography toolkit (hashing, random bytes, HMAC, symmetric encryption, key derivation, and signatures) available with no import through the ambient `crypto` global.
4
+
5
+ Reach for `crypto` to fingerprint a value (hashing), generate an unguessable token or ID (random bytes), prove a value has not been tampered with (HMAC or a signature), or encrypt data you store yourself (AES). Skip it for **login passwords**: use [auth](../auth/README.md), which is purpose-built and does the hard parts for you. `crypto` is also the engine under [signed cookies](./cookies.md) and auth, so most apps use it indirectly without ever calling it.
6
+
7
+ ## The shape of the API
8
+
9
+ `crypto` mirrors the browser Web Crypto API, with one big difference: **there are no Promises**. ToilScript (the language your backend is written in) has no `async`, so every call returns its result **directly**, right away.
10
+
11
+ ```ts
12
+ const digest = crypto.sha256Text('hello'); // Uint8Array, no await
13
+ const id = crypto.randomUUID(); // string, no await
14
+ ```
15
+
16
+ There are two layers:
17
+
18
+ - **`crypto.*`**: ergonomic one-call helpers (hash this string, give me a UUID, HMAC these bytes). Start here.
19
+ - **`crypto.subtle`**: the full primitive surface (import a key, encrypt, sign, derive bits) for when you need more control.
20
+
21
+ Both are ambient globals (no import). The small helper classes and the `ALG_*` / `FMT_*` / `USAGE_*` / `CURVE_*` constants you pass to `subtle` are imported from the `'crypto'` module.
22
+
23
+ ## Quick helpers (`crypto.*`)
24
+
25
+ Every helper is synchronous and returns its value directly.
26
+
27
+ | Helper | Signature | What it does |
28
+ | --- | --- | --- |
29
+ | `getRandomValues` | `(array: Uint8Array): void` | Fill the array with cryptographically strong random bytes. |
30
+ | `randomUUID` | `(): string` | A random RFC 4122 v4 UUID string. |
31
+ | `sha1` / `sha256` / `sha384` / `sha512` | `(data: Uint8Array): Uint8Array` | Hash raw bytes. |
32
+ | `sha1Text` … `sha512Text` | `(s: string): Uint8Array` | UTF-8 encode the string, then hash. |
33
+ | `hmacSha256` | `(key: Uint8Array, msg: Uint8Array): Uint8Array` | Keyed fingerprint (HMAC-SHA-256) of bytes. |
34
+ | `hmacSha256Text` | `(key: Uint8Array, msg: string): Uint8Array` | HMAC-SHA-256 over a string. |
35
+ | `toHex` | `(bytes: Uint8Array): string` | Lowercase hex string (for display or storage). |
36
+ | `subtle` | `SubtleCrypto` | The full primitive surface (below). |
37
+
38
+ **Hashing** turns any input into a fixed-size fingerprint. The same input always gives the same output, and you cannot work backward from the fingerprint to the input. Use it to compare or index a value without storing the original (for example, keying a cache by the hash of a URL).
39
+
40
+ **Random bytes** come from a CSPRNG (a cryptographically secure random generator), which means the output is unpredictable and safe for tokens, session IDs, and IVs. Never use `Math.random()` for anything security-related.
41
+
42
+ **HMAC** is a fingerprint computed *with a secret key*. Anyone can hash a value, but only someone who holds the key can produce the correct HMAC, so it proves a value came from you and was not altered. This is exactly what [`TwoFactor`](./email.md) tokens and [signed cookies](./cookies.md) use.
43
+
44
+ ## Worked example: hash a value and generate a token
45
+
46
+ ```ts
47
+ import { RouteContext } from 'toiljs/server/runtime';
48
+
49
+ @rest('demo')
50
+ class Demo {
51
+ @get('/')
52
+ public example(ctx: RouteContext): string {
53
+ // Hash a value to a hex fingerprint (safe to log or use as a key).
54
+ const digest = crypto.sha256Text('alice@example.com');
55
+ const fingerprint = crypto.toHex(digest); // 64 hex chars
56
+
57
+ // Generate a 128-bit unguessable token as hex.
58
+ const bytes = new Uint8Array(16);
59
+ crypto.getRandomValues(bytes);
60
+ const token = crypto.toHex(bytes); // 32 hex chars
61
+
62
+ // A ready-made unique id.
63
+ const id = crypto.randomUUID();
64
+
65
+ return `fp=${fingerprint} token=${token} id=${id}`;
66
+ }
67
+ }
68
+ ```
69
+
70
+ To fingerprint a value *with a secret* (so only your server can produce or check it), use HMAC:
71
+
72
+ ```ts
73
+ const key = Uint8Array.wrap(String.UTF8.encode(Environment.getSecure('SIGNING_KEY')!));
74
+ const mac = crypto.hmacSha256Text(key, 'order:42');
75
+ const macHex = crypto.toHex(mac);
76
+ ```
77
+
78
+ ## The primitive surface (`crypto.subtle`)
79
+
80
+ Use `subtle` when the helpers are not enough: symmetric encryption, signatures, or key derivation. Every method is synchronous.
81
+
82
+ | Method | Signature |
83
+ | --- | --- |
84
+ | `digest` | `digest(algorithm: i32, data: Uint8Array): Uint8Array` |
85
+ | `importKey` | `importKey(format: i32, keyData: Uint8Array, algorithm: AlgorithmParams, extractable: bool, usages: i32): CryptoKey` |
86
+ | `exportKey` | `exportKey(format: i32, key: CryptoKey): Uint8Array` |
87
+ | `encrypt` | `encrypt(algorithm: AlgorithmParams, key: CryptoKey, data: Uint8Array): Uint8Array` |
88
+ | `decrypt` | `decrypt(algorithm: AlgorithmParams, key: CryptoKey, data: Uint8Array): Uint8Array` |
89
+ | `sign` | `sign(algorithm: AlgorithmParams, key: CryptoKey, data: Uint8Array): Uint8Array` |
90
+ | `verify` | `verify(algorithm: AlgorithmParams, key: CryptoKey, signature: Uint8Array, data: Uint8Array): bool` |
91
+ | `deriveBits` | `deriveBits(algorithm: AlgorithmParams, baseKey: CryptoKey, length: i32): Uint8Array` |
92
+ | `deriveKey` | `deriveKey(algorithm, baseKey, lengthBits, derivedKeyAlgorithm, extractable, usages): CryptoKey` |
93
+
94
+ Two things differ from the browser API, both because of the missing-Promise design:
95
+
96
+ - **`algorithm` and `format` are integer constants, not strings.** You pass `ALG_SHA_256` (not `"SHA-256"`) and `FMT_RAW` (not `"raw"`). The constants are listed below.
97
+ - **`verify` returns a `bool`.** A signature mismatch returns `false` (it does not throw). A real error, like an invalid key, does throw.
98
+
99
+ ### Keys are per-request handles
100
+
101
+ You never hold raw key bytes in your code for long. `importKey` gives you back a **`CryptoKey`**, which is an opaque handle into a host-side keystore. That keystore is **wiped when the request ends**, so a `CryptoKey` is valid only within the request that created it. Never cache one across requests; import it again each time.
102
+
103
+ ```mermaid
104
+ sequenceDiagram
105
+ participant G as Your handler
106
+ participant H as Host keystore (per request)
107
+ G->>H: importKey(...) -> a CryptoKey handle
108
+ G->>H: sign(params, key, data)
109
+ H-->>G: signature bytes
110
+ Note over H: the keystore is wiped when the request ends
111
+ ```
112
+
113
+ ### Algorithm parameter classes
114
+
115
+ Each algorithm takes a small parameters object you build and pass in. Import the class you need from `'crypto'`:
116
+
117
+ ```ts
118
+ import { AesGcmParams, HmacImportParams, ALG_SHA_256, USAGE_ENCRYPT, USAGE_DECRYPT } from 'crypto';
119
+ ```
120
+
121
+ | Class | Constructor | Used for |
122
+ | --- | --- | --- |
123
+ | `AesGcmParams` | `(iv, additionalData?, tagLength = 128)` | AES-GCM encrypt/decrypt |
124
+ | `AesCbcParams` | `(iv)` | AES-CBC |
125
+ | `AesCtrParams` | `(counter, length = 128)` | AES-CTR |
126
+ | `HmacImportParams` | `(hash)` | importing an HMAC key |
127
+ | `HmacParams` | `()` | HMAC sign/verify (hash comes from the key) |
128
+ | `Pbkdf2Params` | `(hash, salt, iterations)` | deriving a key from a password |
129
+ | `HkdfParams` | `(hash, salt, info?)` | deriving a key from key material |
130
+ | `EcdsaParams` | `(hash)` | ECDSA sign/verify |
131
+ | `EcKeyImportParams` | `(alg, namedCurve)` | importing an EC key |
132
+ | `Ed25519Params` | `()` | Ed25519 sign/verify |
133
+ | `X25519ImportParams` | `()` | importing an X25519 key |
134
+ | `EcdhParams` | `(alg, publicKeyHandle)` | ECDH / X25519 key agreement |
135
+
136
+ ### Constants
137
+
138
+ - **Hashes and algorithms:** `ALG_SHA_1`, `ALG_SHA_256`, `ALG_SHA_384`, `ALG_SHA_512`, `ALG_SHA3_256`, `ALG_SHA3_384`, `ALG_SHA3_512`, `ALG_AES_GCM`, `ALG_AES_CBC`, `ALG_AES_CTR`, `ALG_AES_KW`, `ALG_HMAC`, `ALG_ECDSA`, `ALG_ED25519`, `ALG_ECDH`, `ALG_X25519`, `ALG_HKDF`, `ALG_PBKDF2`.
139
+ - **Key formats:** `FMT_RAW`, `FMT_PKCS8`, `FMT_SPKI`. (`FMT_JWK` is rejected.)
140
+ - **Key usages (a bitmask, OR them together):** `USAGE_ENCRYPT`, `USAGE_DECRYPT`, `USAGE_SIGN`, `USAGE_VERIFY`, `USAGE_DERIVE_KEY`, `USAGE_DERIVE_BITS`, `USAGE_WRAP_KEY`, `USAGE_UNWRAP_KEY`.
141
+ - **Named curves:** `CURVE_P256`, `CURVE_P384`. (`CURVE_P521` is not supported.)
142
+
143
+ The `digest` selector also accepts the SHA3 constants, so `crypto.subtle.digest(ALG_SHA3_256, data)` works even though there is no `sha3` quick helper.
144
+
145
+ ### `CryptoKey`
146
+
147
+ A `CryptoKey` is a handle plus metadata: `handle: i32`, `type: string` (`"secret"`, `"public"`, or `"private"`), `extractable: bool`, `algorithm: i32`, and `usages: i32`, with `algorithmName()` and `hasUsage(u)` helpers. Remember it is only valid within the current request.
148
+
149
+ ## Example: AES-256-GCM encrypt and decrypt
150
+
151
+ AES-GCM is authenticated encryption: it both hides the data and detects tampering. It needs a 32-byte key and a fresh 12-byte IV (a "number used once"). **Never reuse an IV with the same key**; generate a new one every time.
152
+
153
+ ```ts
154
+ import { AesGcmParams, ALG_AES_GCM, FMT_RAW, USAGE_ENCRYPT, USAGE_DECRYPT } from 'crypto';
155
+
156
+ // 32-byte key and a fresh 12-byte IV.
157
+ const rawKey = new Uint8Array(32); crypto.getRandomValues(rawKey);
158
+ const iv = new Uint8Array(12); crypto.getRandomValues(iv);
159
+
160
+ // Import the key once; grant it both usages so it can round-trip.
161
+ const key = crypto.subtle.importKey(
162
+ FMT_RAW, rawKey, new AesGcmParams(iv), false, USAGE_ENCRYPT | USAGE_DECRYPT,
163
+ );
164
+
165
+ const plaintext = Uint8Array.wrap(String.UTF8.encode('secret note'));
166
+ const ciphertext = crypto.subtle.encrypt(new AesGcmParams(iv), key, plaintext);
167
+ const recovered = crypto.subtle.decrypt(new AesGcmParams(iv), key, ciphertext);
168
+ // recovered == plaintext
169
+ ```
170
+
171
+ Store the `iv` next to the ciphertext (it is not secret); you need the same IV to decrypt.
172
+
173
+ ## Example: HMAC with `subtle`
174
+
175
+ The quick `crypto.hmacSha256` helper does this in one line, but here is the explicit form, which mirrors how [`TwoFactor`](./email.md) signs its tokens:
176
+
177
+ ```ts
178
+ import { HmacImportParams, HmacParams, ALG_SHA_256, FMT_RAW, USAGE_SIGN, USAGE_VERIFY } from 'crypto';
179
+
180
+ const key = crypto.subtle.importKey(
181
+ FMT_RAW, rawKeyBytes, new HmacImportParams(ALG_SHA_256), false, USAGE_SIGN | USAGE_VERIFY,
182
+ );
183
+
184
+ const mac = crypto.subtle.sign(new HmacParams(), key, message);
185
+ const ok: bool = crypto.subtle.verify(new HmacParams(), key, mac, message); // true
186
+
187
+ // verify compares in constant time and returns false on a mismatch (it does not throw).
188
+ ```
189
+
190
+ ## Limitations
191
+
192
+ - **No Promises.** Every call is synchronous.
193
+ - **No RSA.** It was dropped because the only pure-Rust implementation had an unfixable timing weakness. Use ECDSA or Ed25519 for signatures.
194
+ - **No JWK key format.** Use `FMT_RAW`, `FMT_PKCS8`, or `FMT_SPKI`.
195
+ - **No on-host key generation.** Keys are always **imported**, never generated on the server. Generate them elsewhere and import the bytes.
196
+ - **No P-521.** `CURVE_P256` and `CURVE_P384` are supported.
197
+ - **Keys do not survive the request.** A `CryptoKey` is a per-request handle; re-import each request.
198
+ - **Every call is metered.** Crypto operations cost compute (charged up front from the sizes involved), so an over-budget call fails cleanly instead of burning CPU.
199
+
200
+ ### Post-quantum signature verify
201
+
202
+ Auth's login uses a post-quantum signature scheme (ML-DSA) that the host can **verify** (it never holds a private key). This underpins the [auth login stack](../auth/how-it-works.md); you reach it through `AuthService`, not through `crypto` directly, so it is documented there rather than here.
203
+
204
+ ## Related
205
+
206
+ - [Auth: how it works](../auth/how-it-works.md), the post-quantum login stack built on host-side verify.
207
+ - [Cookies](./cookies.md), signed and encrypted cookies built on `crypto`.
208
+ - [Email](./email.md), whose `TwoFactor` codes are HMAC tokens.
209
+ - [Environment and secrets](./environment.md), where you keep signing and encryption keys.