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,104 @@
1
+ /**
2
+ * base64url (RFC 4648 §5), unpadded. The toilscript std ships `Encoding.Hex`
3
+ * and `Encoding.Varint` but no base64, and `SecureCookies` needs a compact,
4
+ * cookie-safe transport for raw bytes (IV ‖ ciphertext ‖ tag, or an HMAC).
5
+ *
6
+ * base64url is a deliberate fit for cookies: its alphabet
7
+ * (`A-Z a-z 0-9 - _`) is entirely within the RFC 6265bis `cookie-octet` set
8
+ * and is invariant under percent-encoding, so a sealed value round-trips
9
+ * cleanly through the default cookie encoder/decoder untouched.
10
+ *
11
+ * Internal to the cookie library (not a global).
12
+ */
13
+
14
+ const ALPHABET: string = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';
15
+
16
+ /** Encode `data` as unpadded base64url. */
17
+ export function base64UrlEncode(data: Uint8Array): string {
18
+ const n = data.length;
19
+ if (n == 0) return '';
20
+
21
+ let out = '';
22
+ let i = 0;
23
+ while (i + 3 <= n) {
24
+ const b0 = <i32>data[i];
25
+ const b1 = <i32>data[i + 1];
26
+ const b2 = <i32>data[i + 2];
27
+ out += ALPHABET.charAt(b0 >> 2);
28
+ out += ALPHABET.charAt(((b0 & 3) << 4) | (b1 >> 4));
29
+ out += ALPHABET.charAt(((b1 & 15) << 2) | (b2 >> 6));
30
+ out += ALPHABET.charAt(b2 & 63);
31
+ i += 3;
32
+ }
33
+
34
+ const rem = n - i;
35
+ if (rem == 1) {
36
+ const b0 = <i32>data[i];
37
+ out += ALPHABET.charAt(b0 >> 2);
38
+ out += ALPHABET.charAt((b0 & 3) << 4);
39
+ } else if (rem == 2) {
40
+ const b0 = <i32>data[i];
41
+ const b1 = <i32>data[i + 1];
42
+ out += ALPHABET.charAt(b0 >> 2);
43
+ out += ALPHABET.charAt(((b0 & 3) << 4) | (b1 >> 4));
44
+ out += ALPHABET.charAt((b1 & 15) << 2);
45
+ }
46
+ return out;
47
+ }
48
+
49
+ /** Decode a single base64url (or base64) sextet character, or -1 if invalid. */
50
+ function sextet(c: i32): i32 {
51
+ if (c >= 65 && c <= 90) return c - 65; // A-Z
52
+ if (c >= 97 && c <= 122) return c - 97 + 26; // a-z
53
+ if (c >= 48 && c <= 57) return c - 48 + 52; // 0-9
54
+ if (c == 45 || c == 43) return 62; // '-' (url) or '+' (standard)
55
+ if (c == 95 || c == 47) return 63; // '_' (url) or '/' (standard)
56
+ return -1;
57
+ }
58
+
59
+ /**
60
+ * Decode unpadded (or padded) base64url/base64 into bytes, or `null` if the
61
+ * input contains an invalid character or has an impossible length. `=` padding
62
+ * and ASCII whitespace are tolerated and ignored.
63
+ */
64
+ export function base64UrlDecode(s: string): Uint8Array | null {
65
+ const vals = new Array<i32>();
66
+ for (let i = 0; i < s.length; i++) {
67
+ const c = s.charCodeAt(i);
68
+ if (c == 61 || c == 32 || c == 9 || c == 10 || c == 13) continue; // '=' or WS
69
+ const v = sextet(c);
70
+ if (v < 0) return null;
71
+ vals.push(v);
72
+ }
73
+
74
+ const nq = vals.length;
75
+ const rem = nq & 3;
76
+ if (rem == 1) return null; // a single trailing sextet can't exist
77
+
78
+ const outLen = (nq * 6) / 8;
79
+ const out = new Uint8Array(outLen);
80
+ let oi = 0;
81
+ let i = 0;
82
+ while (i + 4 <= nq) {
83
+ const v0 = vals[i];
84
+ const v1 = vals[i + 1];
85
+ const v2 = vals[i + 2];
86
+ const v3 = vals[i + 3];
87
+ out[oi++] = <u8>(((v0 << 2) | (v1 >> 4)) & 0xff);
88
+ out[oi++] = <u8>(((v1 << 4) | (v2 >> 2)) & 0xff);
89
+ out[oi++] = <u8>(((v2 << 6) | v3) & 0xff);
90
+ i += 4;
91
+ }
92
+ if (rem == 2) {
93
+ const v0 = vals[i];
94
+ const v1 = vals[i + 1];
95
+ out[oi++] = <u8>(((v0 << 2) | (v1 >> 4)) & 0xff);
96
+ } else if (rem == 3) {
97
+ const v0 = vals[i];
98
+ const v1 = vals[i + 1];
99
+ const v2 = vals[i + 2];
100
+ out[oi++] = <u8>(((v0 << 2) | (v1 >> 4)) & 0xff);
101
+ out[oi++] = <u8>(((v1 << 4) | (v2 >> 2)) & 0xff);
102
+ }
103
+ return out;
104
+ }
@@ -0,0 +1,416 @@
1
+ /**
2
+ * `Cookie` — a fluent builder, serializer, and validator for a single
3
+ * `Set-Cookie`, covering the full RFC 6265bis attribute set plus the
4
+ * `Partitioned` (CHIPS) and `Priority` attributes.
5
+ *
6
+ * Exposed as an ambient global (`@global`, no import needed in a handler) and
7
+ * also exported from `toiljs/server/runtime`. Pairs with `Cookies` (parsing /
8
+ * the request side) and `SecureCookies` (signing / encryption).
9
+ *
10
+ * ```ts
11
+ * resp.setCookie(
12
+ * Cookie.create('sid', token)
13
+ * .httpOnly()
14
+ * .secure()
15
+ * .sameSite(SameSite.Lax)
16
+ * .maxAge(3600)
17
+ * .asHostPrefixed(),
18
+ * );
19
+ * ```
20
+ */
21
+
22
+ import { imfFixdate } from './date';
23
+ import { base64UrlEncode } from './base64';
24
+ import { percentEncode } from './percent';
25
+
26
+ /** `SameSite` attribute. `Default` omits the attribute (the UA applies Lax). */
27
+ @global
28
+ export enum SameSite {
29
+ Default = 0,
30
+ None = 1,
31
+ Lax = 2,
32
+ Strict = 3,
33
+ }
34
+
35
+ /** How a cookie value is encoded onto the wire. */
36
+ @global
37
+ export enum CookieEncoding {
38
+ /** `encodeURIComponent`-style percent-encoding (default): arbitrary UTF-8 is safe. */
39
+ Percent = 0,
40
+ /** No transformation. The value must already be valid `cookie-octet`. */
41
+ Raw = 1,
42
+ /** UTF-8 then unpadded base64url. */
43
+ Base64Url = 2,
44
+ }
45
+
46
+ /** Cookie name prefix with browser-enforced guarantees (RFC 6265bis §4.1.3). */
47
+ @global
48
+ export enum CookiePrefix {
49
+ None = 0,
50
+ /** `__Secure-`: requires `Secure`. */
51
+ Secure = 1,
52
+ /** `__Host-`: requires `Secure`, `Path=/`, and no `Domain`. */
53
+ Host = 2,
54
+ }
55
+
56
+ /** SHOULD-NOT-exceed lifetime cap from RFC 6265bis §5.5: 400 days, in seconds. */
57
+ export const MAX_LIFETIME_SECONDS: i64 = 34560000;
58
+
59
+ // --- grammar predicates -----------------------------------------------------
60
+
61
+ /** RFC 7230 `token` char: ALPHA / DIGIT / "!#$%&'*+-.^_`|~". */
62
+ function isTokenChar(c: i32): bool {
63
+ if (c >= 65 && c <= 90) return true; // A-Z
64
+ if (c >= 97 && c <= 122) return true; // a-z
65
+ if (c >= 48 && c <= 57) return true; // 0-9
66
+ return (
67
+ c == 33 || c == 35 || c == 36 || c == 37 || c == 38 || c == 39 ||
68
+ c == 42 || c == 43 || c == 45 || c == 46 || c == 94 || c == 95 ||
69
+ c == 96 || c == 124 || c == 126
70
+ );
71
+ }
72
+
73
+ function isToken(s: string): bool {
74
+ if (s.length == 0) return false;
75
+ for (let i = 0; i < s.length; i++) {
76
+ if (!isTokenChar(s.charCodeAt(i))) return false;
77
+ }
78
+ return true;
79
+ }
80
+
81
+ /** RFC 6265bis `cookie-octet`: %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E. */
82
+ export function isCookieOctet(c: i32): bool {
83
+ return (
84
+ c == 0x21 ||
85
+ (c >= 0x23 && c <= 0x2b) ||
86
+ (c >= 0x2d && c <= 0x3a) ||
87
+ (c >= 0x3c && c <= 0x5b) ||
88
+ (c >= 0x5d && c <= 0x7e)
89
+ );
90
+ }
91
+
92
+ function allCookieOctet(s: string): bool {
93
+ for (let i = 0; i < s.length; i++) {
94
+ if (!isCookieOctet(s.charCodeAt(i))) return false;
95
+ }
96
+ return true;
97
+ }
98
+
99
+ /**
100
+ * Keep only RFC 7230 `token` characters. A cookie name MUST be a token, so this
101
+ * drops anything (CR/LF, `;`, `=`, whitespace, ...) that could break out of the
102
+ * name and inject an attribute or a header. Fast path returns a clean input as-is.
103
+ */
104
+ function tokenize(s: string): string {
105
+ let clean = true;
106
+ for (let i = 0; i < s.length; i++) {
107
+ if (!isTokenChar(s.charCodeAt(i))) {
108
+ clean = false;
109
+ break;
110
+ }
111
+ }
112
+ if (clean) return s;
113
+ let out = '';
114
+ for (let i = 0; i < s.length; i++) {
115
+ const c = s.charCodeAt(i);
116
+ if (isTokenChar(c)) out += String.fromCharCode(c);
117
+ }
118
+ return out;
119
+ }
120
+
121
+ /**
122
+ * Strip the characters that could break out of a value or attribute on the wire:
123
+ * control characters (C0 + DEL), which enable CR/LF header injection, and `;`,
124
+ * which would otherwise start a new cookie attribute. Defense in depth: the
125
+ * default percent encoding already removes these from the value, and they are
126
+ * invalid in these positions per the cookie grammar, so nothing legitimate is
127
+ * lost (base64url sealed values are unaffected). Fast path returns a clean input.
128
+ */
129
+ function stripUnsafe(s: string): string {
130
+ let clean = true;
131
+ for (let i = 0; i < s.length; i++) {
132
+ const c = s.charCodeAt(i);
133
+ if (c < 0x20 || c == 0x7f || c == 0x3b) {
134
+ clean = false;
135
+ break;
136
+ }
137
+ }
138
+ if (clean) return s;
139
+ let out = '';
140
+ for (let i = 0; i < s.length; i++) {
141
+ const c = s.charCodeAt(i);
142
+ if (c >= 0x20 && c != 0x7f && c != 0x3b) out += String.fromCharCode(c);
143
+ }
144
+ return out;
145
+ }
146
+
147
+ /** Result of {@link Cookie#validate}: `valid` plus the list of problems found. */
148
+ export class CookieValidation {
149
+ valid: bool = true;
150
+ errors: Array<string> = new Array<string>();
151
+
152
+ fail(msg: string): void {
153
+ this.valid = false;
154
+ this.errors.push(msg);
155
+ }
156
+ }
157
+
158
+ @global
159
+ export class Cookie {
160
+ /** Cookie name (a token; never encoded). */
161
+ name: string;
162
+ /** Logical cookie value (encoded per {@link encoding} on serialize). */
163
+ value: string;
164
+ /** Wire encoding applied to {@link value} by {@link serialize}. */
165
+ encoding: CookieEncoding = CookieEncoding.Percent;
166
+
167
+ private _domain: string = '';
168
+ private _path: string = '';
169
+ private _maxAge: i64 = 0;
170
+ private _hasMaxAge: bool = false;
171
+ private _expires: i64 = 0;
172
+ private _hasExpires: bool = false;
173
+ private _expiresRaw: string = '';
174
+ private _secure: bool = false;
175
+ private _httpOnly: bool = false;
176
+ private _sameSite: SameSite = SameSite.Default;
177
+ private _partitioned: bool = false;
178
+ private _priority: string = '';
179
+ private _extensions: Array<string> = new Array<string>();
180
+
181
+ constructor(name: string, value: string) {
182
+ this.name = name;
183
+ this.value = value;
184
+ }
185
+
186
+ /** Construct a cookie. Builder-style alias for `new Cookie(name, value)`. */
187
+ static create(name: string, value: string): Cookie {
188
+ return new Cookie(name, value);
189
+ }
190
+
191
+ // --- fluent attribute setters (each returns `this`) ---------------------
192
+
193
+ /** `Domain` attribute. */
194
+ domain(v: string): Cookie {
195
+ this._domain = v;
196
+ return this;
197
+ }
198
+
199
+ /** `Path` attribute (must begin with `/`). */
200
+ path(v: string): Cookie {
201
+ this._path = v;
202
+ return this;
203
+ }
204
+
205
+ /** `Max-Age` in seconds. `0` / negative expire the cookie immediately. */
206
+ maxAge(seconds: i64): Cookie {
207
+ this._maxAge = seconds;
208
+ this._hasMaxAge = true;
209
+ return this;
210
+ }
211
+
212
+ /** `Expires` from a Unix timestamp (seconds), formatted as an IMF-fixdate. */
213
+ expires(epochSeconds: i64): Cookie {
214
+ this._expires = epochSeconds;
215
+ this._hasExpires = true;
216
+ return this;
217
+ }
218
+
219
+ /** `Expires` as a verbatim date string (escape hatch; not validated). */
220
+ expiresRaw(date: string): Cookie {
221
+ this._expiresRaw = date;
222
+ return this;
223
+ }
224
+
225
+ /** `Secure` attribute. */
226
+ secure(on: bool = true): Cookie {
227
+ this._secure = on;
228
+ return this;
229
+ }
230
+
231
+ /** `HttpOnly` attribute. */
232
+ httpOnly(on: bool = true): Cookie {
233
+ this._httpOnly = on;
234
+ return this;
235
+ }
236
+
237
+ /** `SameSite` attribute. */
238
+ sameSite(s: SameSite): Cookie {
239
+ this._sameSite = s;
240
+ return this;
241
+ }
242
+
243
+ /** `Partitioned` attribute (CHIPS). Implies `Secure` on serialize. */
244
+ partitioned(on: bool = true): Cookie {
245
+ this._partitioned = on;
246
+ return this;
247
+ }
248
+
249
+ /** `Priority` attribute (`Low` / `Medium` / `High`). */
250
+ priority(p: string): Cookie {
251
+ this._priority = p;
252
+ return this;
253
+ }
254
+
255
+ /** Append a raw extension attribute verbatim, e.g. `extension('CustomFlag')`. */
256
+ extension(av: string): Cookie {
257
+ this._extensions.push(av);
258
+ return this;
259
+ }
260
+
261
+ /** Choose the wire encoding for the value. */
262
+ withEncoding(e: CookieEncoding): Cookie {
263
+ this.encoding = e;
264
+ return this;
265
+ }
266
+
267
+ // --- prefixes -----------------------------------------------------------
268
+
269
+ /** Apply the `__Secure-` prefix and force `Secure`. */
270
+ asSecurePrefixed(): Cookie {
271
+ if (this.name.indexOf('__Secure-') != 0) this.name = '__Secure-' + this.name;
272
+ this._secure = true;
273
+ return this;
274
+ }
275
+
276
+ /** Apply the `__Host-` prefix and force `Secure`, `Path=/`, and no `Domain`. */
277
+ asHostPrefixed(): Cookie {
278
+ if (this.name.indexOf('__Host-') != 0) this.name = '__Host-' + this.name;
279
+ this._secure = true;
280
+ this._path = '/';
281
+ this._domain = '';
282
+ return this;
283
+ }
284
+
285
+ // --- helpers ------------------------------------------------------------
286
+
287
+ /** SameSite=None and Partitioned both require Secure; reflect that here. */
288
+ private effectiveSecure(): bool {
289
+ return this._secure || this._sameSite == SameSite.None || this._partitioned;
290
+ }
291
+
292
+ /** The name prefix detected case-insensitively (RFC 6265bis §5.4). */
293
+ detectedPrefix(): CookiePrefix {
294
+ const lower = this.name.toLowerCase();
295
+ if (lower.startsWith('__host-')) return CookiePrefix.Host;
296
+ if (lower.startsWith('__secure-')) return CookiePrefix.Secure;
297
+ return CookiePrefix.None;
298
+ }
299
+
300
+ /** The value transformed per {@link encoding}, ready for the wire. */
301
+ encodedValue(): string {
302
+ if (this.encoding == CookieEncoding.Raw) return this.value;
303
+ if (this.encoding == CookieEncoding.Base64Url) {
304
+ return base64UrlEncode(Uint8Array.wrap(String.UTF8.encode(this.value)));
305
+ }
306
+ return percentEncode(this.value);
307
+ }
308
+
309
+ /**
310
+ * Validate against RFC 6265bis: name token, name+value ≤ 4096 bytes,
311
+ * attribute sizes, `Path` form, prefix guarantees, `SameSite=None`/
312
+ * `Partitioned` ⇒ `Secure`, and the 400-day lifetime cap.
313
+ */
314
+ validate(): CookieValidation {
315
+ const v = new CookieValidation();
316
+
317
+ if (!isToken(this.name)) {
318
+ v.fail('invalid cookie name (must be a non-empty RFC token): "' + this.name + '"');
319
+ }
320
+
321
+ const enc = this.encodedValue();
322
+ if (String.UTF8.byteLength(this.name) + String.UTF8.byteLength(enc) > 4096) {
323
+ v.fail('cookie name+value exceeds the 4096-byte limit');
324
+ }
325
+ if (this.encoding == CookieEncoding.Raw && !allCookieOctet(enc)) {
326
+ v.fail('raw cookie value contains characters outside cookie-octet');
327
+ }
328
+
329
+ if (this._domain.length > 0 && String.UTF8.byteLength(this._domain) > 1024) {
330
+ v.fail('Domain exceeds the 1024-byte limit');
331
+ }
332
+ if (this._path.length > 0) {
333
+ if (!this._path.startsWith('/')) v.fail("Path must start with '/'");
334
+ if (String.UTF8.byteLength(this._path) > 1024) v.fail('Path exceeds the 1024-byte limit');
335
+ }
336
+
337
+ const prefix = this.detectedPrefix();
338
+ const secure = this.effectiveSecure();
339
+ if (prefix == CookiePrefix.Secure && !secure) {
340
+ v.fail('__Secure- prefix requires the Secure attribute');
341
+ }
342
+ if (prefix == CookiePrefix.Host) {
343
+ if (!secure) v.fail('__Host- prefix requires the Secure attribute');
344
+ if (this._domain.length > 0) v.fail('__Host- prefix forbids the Domain attribute');
345
+ if (this._path != '/') v.fail('__Host- prefix requires Path=/');
346
+ }
347
+ if (this._sameSite == SameSite.None && !secure) {
348
+ v.fail('SameSite=None requires the Secure attribute');
349
+ }
350
+ if (this._partitioned && !secure) {
351
+ v.fail('Partitioned requires the Secure attribute');
352
+ }
353
+ if (this._hasMaxAge && this._maxAge > MAX_LIFETIME_SECONDS) {
354
+ v.fail('Max-Age exceeds the 400-day cap (clamped on serialize)');
355
+ }
356
+
357
+ return v;
358
+ }
359
+
360
+ /**
361
+ * Serialize to a `Set-Cookie` field value. Lenient by default (always emits
362
+ * a best-effort cookie); pass `strict = true` to throw on a hard violation.
363
+ * `Secure` is emitted automatically when `SameSite=None` or `Partitioned` is
364
+ * set, and `Max-Age` is clamped to the 400-day cap.
365
+ */
366
+ serialize(strict: bool = false): string {
367
+ if (strict) {
368
+ const v = this.validate();
369
+ if (!v.valid) {
370
+ throw new Error('invalid cookie: ' + (v.errors.length > 0 ? v.errors[0] : 'unknown'));
371
+ }
372
+ }
373
+
374
+ // Sanitize every caller-supplied part to its grammar as defense in depth
375
+ // against header injection (CR/LF) and cookie-attribute injection (`;`).
376
+ // The name is reduced to RFC token characters; values and attribute
377
+ // values have controls and `;` stripped. These are invalid in these
378
+ // positions per the cookie grammar, so nothing legitimate is dropped, and
379
+ // base64url sealed values pass through untouched.
380
+ let s = tokenize(this.name) + '=' + stripUnsafe(this.encodedValue());
381
+
382
+ if (this._domain.length > 0) s += '; Domain=' + stripUnsafe(this._domain);
383
+ if (this._path.length > 0) s += '; Path=' + stripUnsafe(this._path);
384
+
385
+ if (this._expiresRaw.length > 0) {
386
+ s += '; Expires=' + stripUnsafe(this._expiresRaw);
387
+ } else if (this._hasExpires) {
388
+ s += '; Expires=' + imfFixdate(this._expires);
389
+ }
390
+
391
+ if (this._hasMaxAge) {
392
+ let age = this._maxAge;
393
+ if (age > MAX_LIFETIME_SECONDS) age = MAX_LIFETIME_SECONDS;
394
+ s += '; Max-Age=' + age.toString();
395
+ }
396
+
397
+ if (this._sameSite == SameSite.Strict) s += '; SameSite=Strict';
398
+ else if (this._sameSite == SameSite.Lax) s += '; SameSite=Lax';
399
+ else if (this._sameSite == SameSite.None) s += '; SameSite=None';
400
+
401
+ if (this.effectiveSecure()) s += '; Secure';
402
+ if (this._httpOnly) s += '; HttpOnly';
403
+ if (this._partitioned) s += '; Partitioned';
404
+ if (this._priority.length > 0) s += '; Priority=' + stripUnsafe(this._priority);
405
+
406
+ for (let i = 0; i < this._extensions.length; i++) {
407
+ s += '; ' + stripUnsafe(this._extensions[i]);
408
+ }
409
+
410
+ return s;
411
+ }
412
+
413
+ toString(): string {
414
+ return this.serialize();
415
+ }
416
+ }