toiljs 0.0.34 → 0.0.37

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 (110) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +1 -0
  3. package/as-pect.config.js +8 -2
  4. package/build/cli/.tsbuildinfo +1 -1
  5. package/build/cli/index.js +97 -0
  6. package/build/client/.tsbuildinfo +1 -1
  7. package/build/client/auth.d.ts +42 -0
  8. package/build/client/auth.js +182 -0
  9. package/build/client/index.d.ts +5 -1
  10. package/build/client/index.js +3 -1
  11. package/build/client/routing/loader.d.ts +1 -0
  12. package/build/client/routing/loader.js +37 -0
  13. package/build/client/routing/mount.js +32 -1
  14. package/build/client/ssr/markers.d.ts +34 -0
  15. package/build/client/ssr/markers.js +49 -0
  16. package/build/compiler/.tsbuildinfo +1 -1
  17. package/build/compiler/docs.js +88 -1
  18. package/build/compiler/generate.d.ts +2 -0
  19. package/build/compiler/generate.js +2 -2
  20. package/build/compiler/index.js +2 -0
  21. package/build/compiler/ssr-codegen.d.ts +2 -0
  22. package/build/compiler/ssr-codegen.js +36 -0
  23. package/build/compiler/template-build.d.ts +29 -0
  24. package/build/compiler/template-build.js +150 -0
  25. package/build/compiler/template.d.ts +22 -0
  26. package/build/compiler/template.js +169 -0
  27. package/build/devserver/.tsbuildinfo +1 -1
  28. package/build/devserver/crypto.js +15 -0
  29. package/build/devserver/host.js +1 -0
  30. package/build/devserver/module.d.ts +1 -0
  31. package/build/devserver/module.js +23 -1
  32. package/docs/README.md +56 -0
  33. package/docs/auth.md +261 -0
  34. package/docs/caching.md +115 -0
  35. package/docs/cookies.md +457 -0
  36. package/docs/crypto.md +130 -0
  37. package/docs/data.md +131 -0
  38. package/docs/getting-started.md +128 -0
  39. package/docs/routing.md +259 -0
  40. package/docs/rpc.md +149 -0
  41. package/docs/ssr.md +184 -0
  42. package/docs/time.md +43 -0
  43. package/examples/basic/client/routes/auth.tsx +198 -0
  44. package/examples/basic/client/routes/cookies.tsx +199 -0
  45. package/examples/basic/client/routes/features/index.tsx +34 -10
  46. package/examples/basic/client/routes/hello.tsx +43 -0
  47. package/examples/basic/client/routes/pq.tsx +260 -0
  48. package/examples/basic/server/AuthTestHandler.ts +15 -0
  49. package/examples/basic/server/AuthVerifyHandler.ts +23 -0
  50. package/examples/basic/server/CacheHandler.ts +25 -0
  51. package/examples/basic/server/DecoCache.ts +18 -0
  52. package/examples/basic/server/FastTrapHandler.ts +8 -0
  53. package/examples/basic/server/SpinHandler.ts +18 -0
  54. package/examples/basic/server/SsrGreetingRender.ts +27 -0
  55. package/examples/basic/server/authexample-main.ts +8 -0
  56. package/examples/basic/server/authtest-main.ts +8 -0
  57. package/examples/basic/server/authverify-main.ts +8 -0
  58. package/examples/basic/server/cache-main.ts +8 -0
  59. package/examples/basic/server/core/AppHandler.ts +243 -0
  60. package/examples/basic/server/deco-main.ts +18 -0
  61. package/examples/basic/server/main.ts +2 -0
  62. package/examples/basic/server/routes/Auth.ts +184 -0
  63. package/examples/basic/server/routes/PqDemo.ts +130 -0
  64. package/examples/basic/server/routes/Session.ts +74 -0
  65. package/examples/basic/server/spin-main.ts +13 -0
  66. package/examples/basic/server/ssr/greeting.slots.ts +19 -0
  67. package/examples/basic/server/ssr-main.ts +18 -0
  68. package/examples/basic/server/toil-server-env.d.ts +94 -0
  69. package/examples/basic/server/trap-main.ts +8 -0
  70. package/package.json +5 -3
  71. package/server/globals/auth.ts +281 -0
  72. package/server/runtime/README.md +61 -0
  73. package/server/runtime/env/Server.ts +12 -0
  74. package/server/runtime/exports/index.ts +17 -0
  75. package/server/runtime/exports/render.ts +51 -0
  76. package/server/runtime/http/base64.ts +104 -0
  77. package/server/runtime/http/cookie.ts +416 -0
  78. package/server/runtime/http/cookies.ts +197 -0
  79. package/server/runtime/http/date.ts +72 -0
  80. package/server/runtime/http/percent.ts +76 -0
  81. package/server/runtime/http/securecookies.ts +224 -0
  82. package/server/runtime/index.ts +17 -0
  83. package/server/runtime/request.ts +24 -0
  84. package/server/runtime/response.ts +29 -0
  85. package/server/runtime/ssr/Ssr.ts +43 -0
  86. package/server/runtime/ssr/encode.ts +110 -0
  87. package/server/runtime/ssr/escape.ts +83 -0
  88. package/server/runtime/ssr/slots.ts +144 -0
  89. package/server/runtime/time.ts +29 -0
  90. package/src/cli/create.ts +105 -0
  91. package/src/client/auth.ts +327 -0
  92. package/src/client/index.ts +5 -1
  93. package/src/client/routing/loader.ts +56 -0
  94. package/src/client/routing/mount.tsx +37 -1
  95. package/src/client/ssr/markers.tsx +140 -0
  96. package/src/compiler/docs.ts +88 -1
  97. package/src/compiler/generate.ts +2 -2
  98. package/src/compiler/index.ts +5 -0
  99. package/src/compiler/ssr-codegen.ts +85 -0
  100. package/src/compiler/template-build.ts +275 -0
  101. package/src/compiler/template.ts +265 -0
  102. package/src/devserver/crypto.ts +23 -0
  103. package/src/devserver/host.ts +4 -0
  104. package/src/devserver/module.ts +39 -1
  105. package/test/assembly/cookie.spec.ts +302 -0
  106. package/test/assembly/example.spec.ts +5 -1
  107. package/test/assembly/ssr.spec.ts +94 -0
  108. package/test/devserver.test.ts +42 -0
  109. package/test/ssr-render.test.ts +128 -0
  110. package/test/ssr-template.test.tsx +348 -0
@@ -0,0 +1,457 @@
1
+ # Cookies
2
+
3
+ A complete HTTP cookie layer for the toiljs server runtime, covering the full
4
+ RFC 6265bis surface (including `SameSite`, the `Partitioned`/CHIPS attribute, and
5
+ the `__Host-` / `__Secure-` prefixes) plus cryptographic signing and encryption.
6
+
7
+ `Cookie`, `Cookies`, `CookieMap`, `SecureCookies`, and the `SameSite` /
8
+ `CookieEncoding` / `CookiePrefix` enums are **ambient globals**: a handler uses
9
+ them with **no import**, exactly like `crypto`. They are also exported from
10
+ `toiljs/server/runtime` for anyone who prefers an explicit import.
11
+
12
+ - [How "global, no import" works](#how-global-no-import-works)
13
+ - [Quick start](#quick-start)
14
+ - [The `Cookie` builder](#the-cookie-builder)
15
+ - [The `Cookies` parser and codec](#the-cookies-parser-and-codec)
16
+ - [`CookieMap`](#cookiemap)
17
+ - [`SecureCookies` signing and encryption](#securecookies-signing-and-encryption)
18
+ - [`Request` and `Response` integration](#request-and-response-integration)
19
+ - [`base64url` helpers](#base64url-helpers)
20
+ - [Encoding vs encryption](#encoding-vs-encryption)
21
+ - [Security notes](#security-notes)
22
+ - [Spec compliance](#spec-compliance)
23
+ - [Testing](#testing)
24
+ - [API reference](#api-reference)
25
+
26
+ ---
27
+
28
+ ## How "global, no import" works
29
+
30
+ The cookie types are declared with ToilScript's `@global` decorator and pulled
31
+ into every server build (re-exported from `toiljs/server/runtime` and
32
+ side-effect-imported by `toiljs/server/runtime/exports`, which every `main.ts`
33
+ re-exports). At compile time the symbols register in the global scope, so a
34
+ handler can write `Cookie.create(...)` or `req.cookie(...)` without importing
35
+ anything.
36
+
37
+ For the editor, `toiljs create` scaffolds `server/toil-server-env.d.ts` with
38
+ ambient `declare`s for these globals (the toilscript compiler ignores `.d.ts`;
39
+ it only feeds the language service). If you would rather import them:
40
+
41
+ ```ts
42
+ import { Cookie, Cookies, SecureCookies, SameSite } from 'toiljs/server/runtime';
43
+ ```
44
+
45
+ ---
46
+
47
+ ## Quick start
48
+
49
+ ```ts
50
+ import { ToilHandler, Request, Response } from 'toiljs/server/runtime';
51
+
52
+ export class AppHandler extends ToilHandler {
53
+ public handle(req: Request): Response {
54
+ // Read (no import needed for Cookie / Cookies / SameSite, they are global).
55
+ const sid = req.cookie('sid'); // string | null
56
+
57
+ // Write a hardened session cookie.
58
+ return Response.json('{"ok":true}').setCookie(
59
+ Cookie.create('sid', 'abc123')
60
+ .httpOnly()
61
+ .secure()
62
+ .sameSite(SameSite.Lax)
63
+ .maxAge(3600)
64
+ .asHostPrefixed(), // forces Secure + Path=/ + no Domain
65
+ );
66
+ }
67
+ }
68
+ ```
69
+
70
+ ---
71
+
72
+ ## The `Cookie` builder
73
+
74
+ A fluent builder that serializes to one `Set-Cookie` field value. Every setter
75
+ returns the cookie, so calls chain.
76
+
77
+ ```ts
78
+ const c = Cookie.create('id', 'abc123')
79
+ .domain('example.com')
80
+ .path('/app')
81
+ .maxAge(3600)
82
+ .secure()
83
+ .httpOnly()
84
+ .sameSite(SameSite.Lax);
85
+
86
+ c.serialize();
87
+ // "id=abc123; Domain=example.com; Path=/app; Max-Age=3600; SameSite=Lax; Secure; HttpOnly"
88
+ ```
89
+
90
+ ### Fields
91
+
92
+ | Field | Type | Notes |
93
+ | --- | --- | --- |
94
+ | `name` | `string` | The cookie name (a token; never encoded). |
95
+ | `value` | `string` | The logical value (encoded per `encoding` on serialize). |
96
+ | `encoding` | `CookieEncoding` | Wire encoding for the value. Default `Percent`. |
97
+
98
+ ### Construction
99
+
100
+ - `new Cookie(name, value)`
101
+ - `Cookie.create(name, value): Cookie`, a builder-style alias.
102
+
103
+ ### Attribute setters
104
+
105
+ | Method | Attribute |
106
+ | --- | --- |
107
+ | `domain(v: string)` | `Domain` |
108
+ | `path(v: string)` | `Path` (must begin with `/`) |
109
+ | `maxAge(seconds: i64)` | `Max-Age` (`0` / negative expire immediately) |
110
+ | `expires(epochSeconds: i64)` | `Expires`, formatted as an IMF-fixdate (`Sun, 06 Nov 1994 08:49:37 GMT`) |
111
+ | `expiresRaw(date: string)` | `Expires` verbatim (escape hatch) |
112
+ | `secure(on: bool = true)` | `Secure` |
113
+ | `httpOnly(on: bool = true)` | `HttpOnly` |
114
+ | `sameSite(s: SameSite)` | `SameSite` |
115
+ | `partitioned(on: bool = true)` | `Partitioned` (CHIPS) |
116
+ | `priority(p: string)` | `Priority` (`Low` / `Medium` / `High`) |
117
+ | `extension(av: string)` | An arbitrary extension attribute, appended verbatim |
118
+ | `withEncoding(e: CookieEncoding)` | Choose the value wire encoding |
119
+
120
+ ### Prefixes
121
+
122
+ - `asSecurePrefixed(): Cookie`, prepends `__Secure-` and forces `Secure`.
123
+ - `asHostPrefixed(): Cookie`, prepends `__Host-` and forces `Secure`, `Path=/`, and no `Domain`.
124
+ - `detectedPrefix(): CookiePrefix`, the prefix detected on the current name (case-insensitive).
125
+
126
+ ### Output
127
+
128
+ - `serialize(strict: bool = false): string`, returns the `Set-Cookie` value. Lenient by
129
+ default (always returns a best-effort cookie); pass `strict = true` to throw on
130
+ a hard validation failure. `Secure` is added automatically when `SameSite=None`
131
+ or `Partitioned` is set; `Max-Age` is clamped to the 400-day cap; control
132
+ characters are stripped from the name, value, and attributes.
133
+ - `toString(): string`, alias for `serialize()`.
134
+ - `encodedValue(): string`, the value transformed per `encoding`.
135
+
136
+ ### Validation
137
+
138
+ `validate(): CookieValidation` checks the cookie against RFC 6265bis and returns
139
+ a structured result:
140
+
141
+ ```ts
142
+ class CookieValidation {
143
+ valid: bool;
144
+ errors: Array<string>;
145
+ }
146
+ ```
147
+
148
+ It flags: a non-token name, name+value over 4096 bytes, a `Domain`/`Path` over
149
+ 1024 bytes, a `Path` not starting with `/`, a `Raw` value outside `cookie-octet`,
150
+ the `__Host-` / `__Secure-` prefix requirements, `SameSite=None` or `Partitioned`
151
+ without `Secure`, and a `Max-Age` beyond the 400-day cap.
152
+
153
+ ### Attribute serialization order
154
+
155
+ `name=value` then, when set: `Domain`, `Path`, `Expires`, `Max-Age`, `SameSite`,
156
+ `Secure`, `HttpOnly`, `Partitioned`, `Priority`, then any `extension(...)` values.
157
+ (Attribute order is not significant to user agents; the order is stable so output
158
+ is predictable.)
159
+
160
+ ### Enums
161
+
162
+ ```ts
163
+ enum SameSite { Default, None, Lax, Strict } // Default omits the attribute
164
+ enum CookieEncoding { Percent, Raw, Base64Url } // value wire encoding
165
+ enum CookiePrefix { None, Secure, Host }
166
+ ```
167
+
168
+ ---
169
+
170
+ ## The `Cookies` parser and codec
171
+
172
+ Static helpers for the read side and a one-shot serializer.
173
+
174
+ | Method | Description |
175
+ | --- | --- |
176
+ | `Cookies.parse(cookieHeader: string): CookieMap` | Parse a request `Cookie` header (`a=1; b=2`). Values are percent-decoded; one layer of surrounding quotes is stripped; malformed pairs and empty names are skipped. On a duplicate name the first wins. |
177
+ | `Cookies.get(cookieHeader: string, name: string): string \| null` | Parse and return one value. |
178
+ | `Cookies.serialize(name: string, value: string): string` | One-shot `name=value` with no attributes (percent-encoded). For attributes, build a `Cookie`. |
179
+ | `Cookies.parseSetCookie(setCookie: string): Cookie` | Parse a `Set-Cookie` line back into a `Cookie` (for clients, tests, proxies). Kept verbatim (`CookieEncoding.Raw`) so re-serializing reproduces the wire form. |
180
+ | `Cookies.encodeValue(raw: string): string` | Percent-encode a value (the default `Cookie` encoding). |
181
+ | `Cookies.decodeValue(enc: string): string` | Percent-decode a value (the inverse). |
182
+
183
+ ```ts
184
+ const jar = Cookies.parse('sid=abc123; theme=dark');
185
+ jar.get('sid'); // "abc123"
186
+
187
+ Cookies.serialize('sid', 'a b'); // "sid=a%20b"
188
+ ```
189
+
190
+ ---
191
+
192
+ ## `CookieMap`
193
+
194
+ The ordered name to value view returned by `Cookies.parse` and `Request.cookies()`.
195
+ Backed by parallel arrays (a request carries a handful of cookies, so a linear
196
+ scan beats hashing and keeps the runtime small).
197
+
198
+ | Member | Description |
199
+ | --- | --- |
200
+ | `get(name: string): string \| null` | The value, or `null`. |
201
+ | `has(name: string): bool` | Whether the cookie is present. |
202
+ | `names(): Array<string>` | A copy of the names, in encounter order. |
203
+ | `size: i32` | The number of cookies. |
204
+ | `set(name: string, value: string): void` | Insert unless present (keep-first). Used by `parse`; rarely called directly. |
205
+
206
+ ---
207
+
208
+ ## `SecureCookies` signing and encryption
209
+
210
+ Tamper-proof and confidential cookie values, built on the `crypto` global (no new
211
+ host functions).
212
+
213
+ - **`SecureCookies.signed(key)`**: HMAC-SHA256. The value stays readable but is
214
+ bound to the cookie name, so it cannot be tampered with or moved to another
215
+ cookie. Sealed form: `base64url(value) "." base64url(mac)`.
216
+ - **`SecureCookies.encrypted(key)`**: AES-256-GCM (or AES-128-GCM) with a random
217
+ 96-bit IV and the cookie name as additional authenticated data. The value is
218
+ confidential and authenticated. Sealed form: `base64url(iv ‖ ciphertext ‖ tag)`.
219
+
220
+ Keys are caller-supplied raw bytes:
221
+
222
+ - HMAC: any length (32+ bytes recommended).
223
+ - AES: exactly 16 or 32 bytes (enforced up front; a wrong length is rejected by
224
+ the factory).
225
+
226
+ ```ts
227
+ // A real app loads a long random secret from config; never hard-code one.
228
+ const key = Uint8Array.wrap(String.UTF8.encode('0123456789abcdef0123456789abcdef'));
229
+
230
+ // Signed (readable, tamper-proof)
231
+ const signer = SecureCookies.signed(key);
232
+ const sealed = signer.sign('session', 'user-42');
233
+ const user = signer.unsign('session', sealed); // "user-42", or null if tampered
234
+
235
+ // Encrypted (confidential + authenticated)
236
+ const box = SecureCookies.encrypted(key);
237
+ resp.setCookie(box.seal(Cookie.create('secret', 'top-secret').httpOnly()));
238
+ const secret = box.open(req.cookies(), 'secret'); // "top-secret", or null
239
+ ```
240
+
241
+ | Method | Description |
242
+ | --- | --- |
243
+ | `SecureCookies.signed(key: Uint8Array)` | HMAC-SHA256 signer/verifier. |
244
+ | `SecureCookies.encrypted(key: Uint8Array)` | AES-GCM (16- or 32-byte key). |
245
+ | `addKey(key: Uint8Array): SecureCookies` | Add a fallback key for rotation: seal with the first, open with any. |
246
+ | `sign(name, value): string` | Sealed signed value. |
247
+ | `unsign(name, sealed): string \| null` | Verify and recover, or `null`. |
248
+ | `encrypt(name, value): string` | Sealed encrypted value. |
249
+ | `decrypt(name, sealed): string \| null` | Decrypt, or `null`. |
250
+ | `seal(cookie: Cookie): Cookie` | Seal a cookie's value in place (sign or encrypt per the instance mode) and mark it `Raw`. Returns the same cookie. |
251
+ | `open(jar: CookieMap, name): string \| null` | Read and open cookie `name` from a parsed jar. |
252
+
253
+ **Key rotation:** seal with `keys[0]`; `unsign` / `decrypt` try every key in turn,
254
+ so you can add a new key as the first and keep an old one as a fallback while
255
+ existing cookies age out.
256
+
257
+ ```ts
258
+ const signer = SecureCookies.signed(newKey).addKey(oldKey);
259
+ ```
260
+
261
+ ---
262
+
263
+ ## `Request` and `Response` integration
264
+
265
+ Because every handler already has a `Request` and returns a `Response`, the most
266
+ common operations live there directly.
267
+
268
+ **Read (`Request`):**
269
+
270
+ | Method | Description |
271
+ | --- | --- |
272
+ | `req.cookies(): CookieMap` | All cookies, parsed from the `Cookie` header (cached for the request). |
273
+ | `req.cookie(name: string): string \| null` | One cookie value. |
274
+
275
+ **Write (`Response`, builder-style):**
276
+
277
+ | Method | Description |
278
+ | --- | --- |
279
+ | `resp.setCookie(cookie: Cookie): Response` | Append a `Set-Cookie`. Each call adds its own header (cookies are never folded). |
280
+ | `resp.setCookieKV(name, value): Response` | Shorthand for `setCookie(new Cookie(name, value))`. |
281
+ | `resp.clearCookie(name, path = '/', domain = ''): Response` | Append a deletion cookie (empty value, `Max-Age=0`, epoch `Expires`). `path` / `domain` must match the original. |
282
+
283
+ ---
284
+
285
+ ## `base64url` helpers
286
+
287
+ Unpadded base64url (RFC 4648 §5), used internally by `SecureCookies` and exported
288
+ for convenience. Its alphabet (`A-Z a-z 0-9 - _`) is within the `cookie-octet`
289
+ grammar and invariant under percent-encoding, so encoded values round-trip
290
+ cleanly through the default cookie codec.
291
+
292
+ | Function | Description |
293
+ | --- | --- |
294
+ | `base64UrlEncode(data: Uint8Array): string` | Encode bytes as unpadded base64url. |
295
+ | `base64UrlDecode(s: string): Uint8Array \| null` | Decode base64url/base64 (padding and whitespace tolerated); `null` on an invalid character or length. |
296
+
297
+ ---
298
+
299
+ ## Encoding vs encryption
300
+
301
+ Two independent layers, easy to mix up:
302
+
303
+ - **Encoding** (`CookieEncoding`) is transport-only and reversible by anyone. It
304
+ keeps an arbitrary value inside the `cookie-octet` grammar.
305
+ - `Percent` (default): `encodeURIComponent`-style; arbitrary UTF-8 is safe.
306
+ - `Base64Url`: UTF-8 then base64url.
307
+ - `Raw`: no transformation (the value must already be valid `cookie-octet`).
308
+ - **Signing / encryption** (`SecureCookies`) is cryptographic. Signing keeps the
309
+ value readable but tamper-proof; encryption makes it unreadable and
310
+ authenticated. Both require a secret key.
311
+
312
+ `SecureCookies.seal` sets the value to its sealed (base64url) form and marks the
313
+ cookie `Raw`, so it passes through the default parse path untouched.
314
+
315
+ ---
316
+
317
+ ## Security notes
318
+
319
+ - **Panic-free verification.** `unsign` and `decrypt` return `null` on a tampered,
320
+ truncated, or wrong-key value, never a trap. (`decrypt` reads the host return
321
+ code directly instead of letting the underlying crypto throw, because the
322
+ server runs with exceptions disabled.) This makes them safe to call on
323
+ attacker-controlled input.
324
+ - **Name-binding.** Signing MACs `name + "=" + value`; encryption uses the name as
325
+ AAD. A sealed value made for one cookie name will not verify or decrypt under
326
+ another.
327
+ - **Control characters are stripped** from the name, value, and attribute values
328
+ on serialize, as a defense-in-depth guard against header injection (CR/LF).
329
+ Control characters are invalid in all of these per the grammar, so nothing
330
+ legitimate is lost. The default value encoding already neutralizes CR/LF.
331
+ - **Prefixes.** `asHostPrefixed()` / `asSecurePrefixed()` apply and enforce the
332
+ browser-recognized guarantees; `validate()` reports a name that carries a prefix
333
+ without satisfying its requirements.
334
+ - **`SameSite=None` and `Partitioned` imply `Secure`** and are emitted with it
335
+ automatically.
336
+ - **Lifetime is clamped** to the RFC 400-day cap on serialize; sizes are checked by
337
+ `validate()`.
338
+ - **Local development.** Browsers treat `http://localhost` as a secure context, so
339
+ `Secure` and `__Host-` cookies work under `toiljs dev` over plain HTTP.
340
+
341
+ When putting untrusted input into a cookie **name** or **attribute** (rather than
342
+ the value, which is encoded by default), check `validate()` or use
343
+ `serialize(true)`.
344
+
345
+ ---
346
+
347
+ ## Spec compliance
348
+
349
+ Implements RFC 6265bis (HTTP State Management) and the `Partitioned` (CHIPS)
350
+ companion: the `cookie-name` token and `cookie-value` `cookie-octet` grammars,
351
+ the `Expires` / `Max-Age` / `Domain` / `Path` / `Secure` / `HttpOnly` /
352
+ `SameSite` / `Partitioned` attributes plus `Priority` and arbitrary extensions,
353
+ the `__Host-` / `__Secure-` prefixes (matched case-insensitively), the 4096-byte
354
+ name+value and 1024-byte attribute limits, the 400-day lifetime cap, the
355
+ `SameSite=None` ⇒ `Secure` rule, and the requirement that each cookie occupy its
356
+ own `Set-Cookie` header (never folded).
357
+
358
+ ---
359
+
360
+ ## Testing
361
+
362
+ - Pure cookie logic (builder, parser, codec, validation, `Request` / `Response`
363
+ integration) is unit-tested with as-pect in `test/assembly/cookie.spec.ts`
364
+ (`npm run test:server`).
365
+ - `SecureCookies` is exercised end-to-end against the real toilscript-compiled
366
+ wasm with the Node-backed crypto host in `test/devserver.test.ts`
367
+ (`npm test`). It is tested there rather than under as-pect because the as-pect
368
+ compiler does not ship the toilscript crypto standard library.
369
+
370
+ A live demo (every attribute's serialized output, set/inspect/clear, and an
371
+ interactive sign/encrypt) is in the example app: run `toiljs dev` in
372
+ `examples/basic` and open `/cookies`. The backend lives in
373
+ `examples/basic/server/core/AppHandler.ts`.
374
+
375
+ ---
376
+
377
+ ## API reference
378
+
379
+ ```ts
380
+ // Globals (also exported from 'toiljs/server/runtime')
381
+
382
+ enum SameSite { Default, None, Lax, Strict }
383
+ enum CookieEncoding { Percent, Raw, Base64Url }
384
+ enum CookiePrefix { None, Secure, Host }
385
+
386
+ class CookieValidation {
387
+ valid: bool;
388
+ errors: Array<string>;
389
+ }
390
+
391
+ class Cookie {
392
+ name: string;
393
+ value: string;
394
+ encoding: CookieEncoding;
395
+ static create(name: string, value: string): Cookie;
396
+ domain(v: string): Cookie;
397
+ path(v: string): Cookie;
398
+ maxAge(seconds: i64): Cookie;
399
+ expires(epochSeconds: i64): Cookie;
400
+ expiresRaw(date: string): Cookie;
401
+ secure(on?: bool): Cookie;
402
+ httpOnly(on?: bool): Cookie;
403
+ sameSite(s: SameSite): Cookie;
404
+ partitioned(on?: bool): Cookie;
405
+ priority(p: string): Cookie;
406
+ extension(av: string): Cookie;
407
+ withEncoding(e: CookieEncoding): Cookie;
408
+ asSecurePrefixed(): Cookie;
409
+ asHostPrefixed(): Cookie;
410
+ detectedPrefix(): CookiePrefix;
411
+ encodedValue(): string;
412
+ validate(): CookieValidation;
413
+ serialize(strict?: bool): string;
414
+ toString(): string;
415
+ }
416
+
417
+ class CookieMap {
418
+ get(name: string): string | null;
419
+ has(name: string): bool;
420
+ names(): Array<string>;
421
+ size: i32;
422
+ set(name: string, value: string): void;
423
+ }
424
+
425
+ class Cookies {
426
+ static parse(cookieHeader: string): CookieMap;
427
+ static get(cookieHeader: string, name: string): string | null;
428
+ static serialize(name: string, value: string): string;
429
+ static parseSetCookie(setCookie: string): Cookie;
430
+ static encodeValue(raw: string): string;
431
+ static decodeValue(enc: string): string;
432
+ }
433
+
434
+ class SecureCookies {
435
+ static signed(key: Uint8Array): SecureCookies;
436
+ static encrypted(key: Uint8Array): SecureCookies;
437
+ addKey(key: Uint8Array): SecureCookies;
438
+ sign(name: string, value: string): string;
439
+ unsign(name: string, sealed: string): string | null;
440
+ encrypt(name: string, value: string): string;
441
+ decrypt(name: string, sealed: string): string | null;
442
+ seal(cookie: Cookie): Cookie;
443
+ open(jar: CookieMap, name: string): string | null;
444
+ }
445
+
446
+ function base64UrlEncode(data: Uint8Array): string;
447
+ function base64UrlDecode(s: string): Uint8Array | null;
448
+
449
+ // On Request
450
+ req.cookies(): CookieMap;
451
+ req.cookie(name: string): string | null;
452
+
453
+ // On Response (builder-style)
454
+ resp.setCookie(cookie: Cookie): Response;
455
+ resp.setCookieKV(name: string, value: string): Response;
456
+ resp.clearCookie(name: string, path?: string, domain?: string): Response;
457
+ ```
package/docs/crypto.md ADDED
@@ -0,0 +1,130 @@
1
+ # Web Crypto
2
+
3
+ The guest gets a synchronous Web Crypto surface through the ambient `crypto`
4
+ global, backed by host functions. It mirrors the browser `crypto` /
5
+ `crypto.subtle` API but **without Promises** — ToilScript has no `async`, so
6
+ every call returns its result directly. Keys are opaque per-request handles in a
7
+ host keystore; a `CryptoKey` is valid only for the request that created it.
8
+
9
+ ```ts
10
+ const mac = crypto.hmacSha256(key, message); // Uint8Array
11
+ const id = crypto.randomUUID(); // string
12
+ ```
13
+
14
+ This is also what [`SecureCookies`](./cookies.md) and
15
+ [`AuthService`](./auth.md) are built on, so most apps use crypto indirectly.
16
+
17
+ ## `crypto` namespace
18
+
19
+ Convenience helpers (all synchronous):
20
+
21
+ | Function | Signature | Notes |
22
+ | --- | --- | --- |
23
+ | `getRandomValues` | `(array: Uint8Array): void` | Fill with CSPRNG bytes. |
24
+ | `randomUUID` | `(): string` | RFC 4122 v4 UUID. |
25
+ | `sha1` / `sha256` / `sha384` / `sha512` | `(data: Uint8Array): Uint8Array` | One-shot digests. |
26
+ | `sha1Text` … `sha512Text` | `(s: string): Uint8Array` | UTF-8 encode then digest. |
27
+ | `hmacSha256` | `(key: Uint8Array, msg: Uint8Array): Uint8Array` | One-shot HMAC-SHA256. |
28
+ | `hmacSha256Text` | `(key: Uint8Array, msg: string): Uint8Array` | HMAC-SHA256 over a UTF-8 string. |
29
+ | `toHex` | `(bytes: Uint8Array): string` | Lowercase hex. |
30
+ | `subtle` | `SubtleCrypto` | The full primitive surface (below). |
31
+
32
+ ## `crypto.subtle`
33
+
34
+ | Method | Signature |
35
+ | --- | --- |
36
+ | `digest` | `digest(algorithm: string, data: Uint8Array): Uint8Array` |
37
+ | `importKey` | `importKey(format: string, keyData: Uint8Array, algorithm: AlgorithmParams, extractable: bool, usages: i32): CryptoKey` |
38
+ | `exportKey` | `exportKey(format: string, key: CryptoKey): Uint8Array` |
39
+ | `encrypt` | `encrypt(algorithm: AlgorithmParams, key: CryptoKey, data: Uint8Array): Uint8Array` |
40
+ | `decrypt` | `decrypt(algorithm: AlgorithmParams, key: CryptoKey, data: Uint8Array): Uint8Array` |
41
+ | `sign` | `sign(algorithm: AlgorithmParams, key: CryptoKey, data: Uint8Array): Uint8Array` |
42
+ | `verify` | `verify(algorithm: AlgorithmParams, key: CryptoKey, signature: Uint8Array, data: Uint8Array): bool` |
43
+ | `deriveBits` | `deriveBits(algorithm: AlgorithmParams, baseKey: CryptoKey, length: i32): Uint8Array` |
44
+ | `deriveKey` | `deriveKey(algorithm, baseKey, lengthBits, derivedKeyAlgorithm, extractable, usages): CryptoKey` |
45
+
46
+ `digest` takes a named algorithm string (`"SHA-1"`, `"SHA-256"`, `"SHA-384"`,
47
+ `"SHA-512"`, `"SHA3-256"`, `"SHA3-384"`, `"SHA3-512"`). `verify` returns a bool
48
+ (it does not throw on a mismatch). Formats are `raw`, `pkcs8`, `spki`; **`jwk`
49
+ is not supported**.
50
+
51
+ ### Algorithm parameter classes
52
+
53
+ `crypto` and `crypto.subtle` are ambient globals (no import). The params classes
54
+ and the `ALG_*` / `USAGE_*` / `FMT_*` / `CURVE_*` constants and the `CryptoKey`
55
+ type are imported from the `'crypto'` module:
56
+
57
+ ```ts
58
+ import { AesGcmParams, HmacImportParams, ALG_SHA_256, USAGE_SIGN } from 'crypto';
59
+ ```
60
+
61
+ Each algorithm has a small params class you pass to `importKey`/`sign`/etc.:
62
+
63
+ | Class | Constructor |
64
+ | --- | --- |
65
+ | `AesGcmParams` | `(iv, additionalData?, tagLength = 128)` |
66
+ | `AesCbcParams` | `(iv)` |
67
+ | `AesCtrParams` | `(counter, length = 128)` |
68
+ | `HmacImportParams` | `(hash)` |
69
+ | `HmacParams` | `()` |
70
+ | `Pbkdf2Params` | `(hash, salt, iterations)` |
71
+ | `HkdfParams` | `(hash, salt, info?)` |
72
+ | `EcdsaParams` | `(hash)` |
73
+ | `EcKeyImportParams` | `(alg, namedCurve)` |
74
+ | `Ed25519Params` | `()` |
75
+ | `X25519ImportParams` | `()` |
76
+ | `EcdhParams` | `(alg, publicKeyHandle)` |
77
+
78
+ ### Constants
79
+
80
+ - **Hashes / algorithms:** `ALG_SHA_1`, `ALG_SHA_256`, `ALG_SHA_384`,
81
+ `ALG_SHA_512`, `ALG_SHA3_256/384/512`, `ALG_AES_GCM`, `ALG_AES_CBC`,
82
+ `ALG_AES_CTR`, `ALG_HMAC`, `ALG_ECDSA`, `ALG_ED25519`, `ALG_ECDH`, `ALG_HKDF`,
83
+ `ALG_PBKDF2`.
84
+ - **Key formats:** `FMT_RAW`, `FMT_PKCS8`, `FMT_SPKI` (`FMT_JWK` is rejected).
85
+ - **Usages (bitmask):** `USAGE_ENCRYPT`, `USAGE_DECRYPT`, `USAGE_SIGN`,
86
+ `USAGE_VERIFY`, `USAGE_DERIVE_KEY`, `USAGE_DERIVE_BITS`, `USAGE_WRAP_KEY`,
87
+ `USAGE_UNWRAP_KEY` — OR them together.
88
+ - **Named curves:** `CURVE_P256`, `CURVE_P384` (`CURVE_P521` is not supported).
89
+
90
+ ### `CryptoKey`
91
+
92
+ An opaque handle plus metadata: `handle: i32`, `type: string`
93
+ (`secret`/`public`/`private`), `extractable: bool`, `algorithm: i32`,
94
+ `usages: i32`, with `algorithmName()` and `hasUsage(u)`. A key is valid only for
95
+ the request that imported it.
96
+
97
+ ## Examples
98
+
99
+ HMAC-SHA256 (one-shot):
100
+
101
+ ```ts
102
+ const mac = crypto.hmacSha256(key, message);
103
+ const hex = crypto.toHex(mac);
104
+ ```
105
+
106
+ AES-256-GCM via `subtle`:
107
+
108
+ ```ts
109
+ const key = new Uint8Array(32); crypto.getRandomValues(key);
110
+ const iv = new Uint8Array(12); crypto.getRandomValues(iv);
111
+
112
+ const k = crypto.subtle.importKey('raw', key, new AesGcmParams(iv, aad, 128), false, USAGE_ENCRYPT);
113
+ const ct = crypto.subtle.encrypt(new AesGcmParams(iv, aad, 128), k, plaintext);
114
+ ```
115
+
116
+ ## Post-quantum verify
117
+
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
120
+ underpins the [auth primitive](./auth.md). Most code reaches it through
121
+ `AuthService.verifyLogin(publicKey, message, signature)` rather than calling the
122
+ import directly. Public key is 1312 bytes, signature 2420 bytes, with a FIPS 204
123
+ domain-separation context.
124
+
125
+ ## Limitations
126
+
127
+ - **No Promises** — every call is synchronous.
128
+ - **No RSA** and **no JWK** key format.
129
+ - **P-521** is not supported (P-256 and P-384 are).
130
+ - Signature *generation* for ML-DSA is client-side only; the server verifies.