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,43 @@
1
+ /**
2
+ * The auto-populated SSR render router. Every compiler-generated route render
3
+ * self-registers here at module init; the `render` wasm export calls
4
+ * `Ssr.dispatch(req)` to find the one whose path matches. The host has already
5
+ * decided this is a template route, but the guest re-derives WHICH route from
6
+ * the request path (the template name is not in the request envelope), exactly
7
+ * as a `@rest` controller matches its own prefix.
8
+ *
9
+ * First matching render wins; `null` means no route matched (a guest/host
10
+ * coherence problem) and the export emits a fail-safe empty result.
11
+ */
12
+
13
+ import { Request } from '../request';
14
+ import { SlotValues } from './slots';
15
+
16
+ /** A route render: returns filled hole values on a path hit, null on a miss. */
17
+ export type RenderFn = (req: Request) => SlotValues | null;
18
+
19
+ export class SsrRegistry {
20
+ private fns: Array<RenderFn> = new Array<RenderFn>();
21
+
22
+ /** Compiler-injected: registers a route's render. Not for direct use. */
23
+ register(fn: RenderFn): void {
24
+ this.fns.push(fn);
25
+ }
26
+
27
+ /** Try every registered render in registration order; first match wins. */
28
+ dispatch(req: Request): SlotValues | null {
29
+ for (let i = 0; i < this.fns.length; i++) {
30
+ const hit = this.fns[i](req);
31
+ if (hit != null) return hit;
32
+ }
33
+ return null;
34
+ }
35
+
36
+ /** Number of registered renders (diagnostics / tests). */
37
+ get size(): i32 {
38
+ return this.fns.length;
39
+ }
40
+ }
41
+
42
+ /** The process-wide SSR render router singleton. */
43
+ export const Ssr: SsrRegistry = new SsrRegistry();
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Serialiser for the SSR **values envelope** (guest -> host), byte-for-byte
3
+ * compatible with `toil-backend/src/host/template/assemble.rs::decode_values`.
4
+ *
5
+ * Layout (LE, no padding):
6
+ *
7
+ * u16 status
8
+ * [32] template_hash
9
+ * u16 n_headers
10
+ * for each header: u16 name_len, u16 val_len, [u8] name, [u8] val
11
+ * u16 n_slots
12
+ * for each value: u16 slot_id, u8 kind, u32 value_len, [u8] value
13
+ *
14
+ * The host keys values by `slot_id` and inserts each at the manifest-fixed
15
+ * offset, so the guest can never choose where bytes land.
16
+ */
17
+
18
+ import {
19
+ utf8Length,
20
+ writeBytes,
21
+ writeU16,
22
+ writeU32,
23
+ writeU8,
24
+ writeUtf8,
25
+ } from '../memory';
26
+ import { HASH_LEN, SlotValues } from './slots';
27
+
28
+ /** Serialise `v` into linear memory at `dst_ofs`; returns bytes written. On any
29
+ * representability violation (counts/lengths over their field widths, or a
30
+ * wrong-sized hash) a minimal 500 envelope is written instead so the host never
31
+ * sees an encoder fault. */
32
+ export function encodeValues(v: SlotValues, dst_ofs: usize): u32 {
33
+ if (v.templateHash.length != HASH_LEN) return encodeFallback(dst_ofs);
34
+ if (v.headers.length > 0xffff || v.slots.length > 0xffff) return encodeFallback(dst_ofs);
35
+ for (let i = 0; i < v.headers.length; i++) {
36
+ const h = v.headers[i];
37
+ if (utf8Length(h.name) > 0xffff || utf8Length(h.value) > 0xffff) {
38
+ return encodeFallback(dst_ofs);
39
+ }
40
+ }
41
+ for (let i = 0; i < v.slots.length; i++) {
42
+ if (<u64>v.slots[i].bytes.length > <u64>0xffffffff) return encodeFallback(dst_ofs);
43
+ }
44
+
45
+ let cur: usize = dst_ofs;
46
+
47
+ writeU16(cur, v.status);
48
+ cur += 2;
49
+
50
+ for (let i = 0; i < HASH_LEN; i++) {
51
+ writeU8(cur + <usize>i, v.templateHash[i]);
52
+ }
53
+ cur += <usize>HASH_LEN;
54
+
55
+ writeU16(cur, <u16>v.headers.length);
56
+ cur += 2;
57
+ for (let i = 0; i < v.headers.length; i++) {
58
+ const h = v.headers[i];
59
+ writeU16(cur, <u16>utf8Length(h.name));
60
+ cur += 2;
61
+ writeU16(cur, <u16>utf8Length(h.value));
62
+ cur += 2;
63
+ cur += writeUtf8(cur, h.name);
64
+ cur += writeUtf8(cur, h.value);
65
+ }
66
+
67
+ writeU16(cur, <u16>v.slots.length);
68
+ cur += 2;
69
+ for (let i = 0; i < v.slots.length; i++) {
70
+ const s = v.slots[i];
71
+ writeU16(cur, <u16>s.slotId);
72
+ cur += 2;
73
+ writeU8(cur, s.kind);
74
+ cur += 1;
75
+ writeU32(cur, <u32>s.bytes.length);
76
+ cur += 4;
77
+ cur += writeBytes(cur, s.bytes);
78
+ }
79
+
80
+ return <u32>(cur - dst_ofs);
81
+ }
82
+
83
+ /** Upper bound on the encoded size of `v`, used to size the response slot the
84
+ * `render` export allocates before encoding. */
85
+ export function valuesEncodedBound(v: SlotValues): usize {
86
+ // status + hash + n_headers + n_slots field overheads.
87
+ let bound: usize = 2 + <usize>HASH_LEN + 2 + 2;
88
+ for (let i = 0; i < v.headers.length; i++) {
89
+ const h = v.headers[i];
90
+ bound += 4 + <usize>utf8Length(h.name) + <usize>utf8Length(h.value);
91
+ }
92
+ for (let i = 0; i < v.slots.length; i++) {
93
+ bound += 7 + <usize>v.slots[i].bytes.length;
94
+ }
95
+ return bound;
96
+ }
97
+
98
+ function encodeFallback(dst_ofs: usize): u32 {
99
+ // status=500, zeroed 32-byte hash (host rejects as a coherence mismatch),
100
+ // 0 headers, 0 slots.
101
+ writeU16(dst_ofs, 500);
102
+ let cur = dst_ofs + 2;
103
+ for (let i = 0; i < HASH_LEN; i++) {
104
+ writeU8(cur + <usize>i, 0);
105
+ }
106
+ cur += <usize>HASH_LEN;
107
+ writeU16(cur, 0);
108
+ writeU16(cur + 2, 0);
109
+ return <u32>(2 + HASH_LEN + 2 + 2);
110
+ }
@@ -0,0 +1,83 @@
1
+ /**
2
+ * HTML escaping for edge-SSR hole values, byte-for-byte identical to
3
+ * react-dom/server's `escapeTextForBrowser` (the regex `/["'&<>]/`). React
4
+ * runs the SAME escaper for text children and attribute values, so a single
5
+ * function serves both kinds. Matching React exactly is load-bearing: the
6
+ * browser's `hydrateRoot` compares the server markup to its own first render,
7
+ * and any escaping difference triggers a hydration mismatch.
8
+ *
9
+ * Note the entities: `'` becomes `&#x27;` (React's choice, NOT `&#39;` or
10
+ * `&apos;`), and `"` becomes `&quot;`. Do not "simplify" these.
11
+ */
12
+
13
+ @inline function needsEscape(c: i32): bool {
14
+ // 0x22 " | 0x26 & | 0x27 ' | 0x3C < | 0x3E >
15
+ return c == 0x22 || c == 0x26 || c == 0x27 || c == 0x3c || c == 0x3e;
16
+ }
17
+
18
+ /** React-exact HTML text/attribute escape. */
19
+ export function escapeHtml(s: string): string {
20
+ let hit = false;
21
+ for (let i = 0; i < s.length; i++) {
22
+ if (needsEscape(s.charCodeAt(i))) {
23
+ hit = true;
24
+ break;
25
+ }
26
+ }
27
+ if (!hit) return s;
28
+
29
+ let out = '';
30
+ let last = 0;
31
+ for (let i = 0; i < s.length; i++) {
32
+ const c = s.charCodeAt(i);
33
+ let rep: string = '';
34
+ if (c == 0x22) rep = '&quot;';
35
+ else if (c == 0x26) rep = '&amp;';
36
+ else if (c == 0x27) rep = '&#x27;';
37
+ else if (c == 0x3c) rep = '&lt;';
38
+ else if (c == 0x3e) rep = '&gt;';
39
+ else continue;
40
+ out += s.substring(last, i);
41
+ out += rep;
42
+ last = i + 1;
43
+ }
44
+ out += s.substring(last, s.length);
45
+ return out;
46
+ }
47
+
48
+ /**
49
+ * Escape a JSON string for safe embedding inside a `<script>` element (the
50
+ * `__toil_state` hydration blob). Neutralises `<`, `>`, `&` and the JS line
51
+ * terminators U+2028/U+2029 to their `\uXXXX` forms so the JSON can never
52
+ * close the script element or break the parse. Mirrors the standard
53
+ * "serialize-javascript"/Next.js approach; the bytes stay valid JSON.
54
+ */
55
+ export function escapeJsonForScript(json: string): string {
56
+ let hit = false;
57
+ for (let i = 0; i < json.length; i++) {
58
+ const c = json.charCodeAt(i);
59
+ if (c == 0x3c || c == 0x3e || c == 0x26 || c == 0x2028 || c == 0x2029) {
60
+ hit = true;
61
+ break;
62
+ }
63
+ }
64
+ if (!hit) return json;
65
+
66
+ let out = '';
67
+ let last = 0;
68
+ for (let i = 0; i < json.length; i++) {
69
+ const c = json.charCodeAt(i);
70
+ let rep: string = '';
71
+ if (c == 0x3c) rep = '\\u003c';
72
+ else if (c == 0x3e) rep = '\\u003e';
73
+ else if (c == 0x26) rep = '\\u0026';
74
+ else if (c == 0x2028) rep = '\\u2028';
75
+ else if (c == 0x2029) rep = '\\u2029';
76
+ else continue;
77
+ out += json.substring(last, i);
78
+ out += rep;
79
+ last = i + 1;
80
+ }
81
+ out += json.substring(last, json.length);
82
+ return out;
83
+ }
@@ -0,0 +1,144 @@
1
+ /**
2
+ * The values the guest `render` returns for one request: the hole values the
3
+ * edge splices into the precompiled template. The guest never emits the static
4
+ * template bytes (the host has those, mmap'd); it only fills the holes.
5
+ *
6
+ * Authoring is normally generated by the toiljs compiler from the route's JSX,
7
+ * but the typed API here is hand-writable too (it is the escape hatch). A
8
+ * generated render looks like:
9
+ *
10
+ * ```ts
11
+ * export function renderProfile(req: Request): SlotValues {
12
+ * const v = new SlotValues(HASH);
13
+ * v.setText(Slot.username, params(req, 'name'));
14
+ * v.setRaw(Slot.bio, bioHtml);
15
+ * const rows = new HtmlBuilder();
16
+ * for (let i = 0; i < posts.length; i++) rows.raw('<li>').text(posts[i]).raw('</li>');
17
+ * v.setRepeat(Slot.posts, rows);
18
+ * return v;
19
+ * }
20
+ * ```
21
+ */
22
+
23
+ import { Header } from '../request';
24
+ import { escapeHtml } from './escape';
25
+
26
+ /** Slot kind discriminants. Mirror `toil-backend` `SlotKind` and the on-disk
27
+ * `.slots` manifest; do not reorder. */
28
+ export namespace SlotKind {
29
+ // @ts-ignore: decorator
30
+ @inline export const TEXT: u8 = 0;
31
+ // @ts-ignore: decorator
32
+ @inline export const RAW: u8 = 1;
33
+ // @ts-ignore: decorator
34
+ @inline export const ATTR: u8 = 2;
35
+ // @ts-ignore: decorator
36
+ @inline export const REPEAT: u8 = 3;
37
+ }
38
+
39
+ /** Number of bytes in the coherence hash; must match the host's `HASH_LEN`. */
40
+ // @ts-ignore: decorator
41
+ @inline export const HASH_LEN: i32 = 32;
42
+
43
+ /** One filled hole: its stable id, kind, and the already-escaped bytes. The id
44
+ * is held as `i32` so a generated `Slot` enum member (AS enums are `i32`) is
45
+ * passed without a cast; it is narrowed to `u16` only at encode time. */
46
+ export class SlotValue {
47
+ constructor(
48
+ public slotId: i32,
49
+ public kind: u8,
50
+ public bytes: Uint8Array,
51
+ ) {}
52
+ }
53
+
54
+ @inline function utf8(s: string): Uint8Array {
55
+ return Uint8Array.wrap(String.UTF8.encode(s));
56
+ }
57
+
58
+ /**
59
+ * Append-only HTML byte builder for repeat regions (and any place the
60
+ * generated code assembles a run of static chunks + escaped values). `raw`
61
+ * appends verbatim template bytes; `text`/`attr` append React-escaped values.
62
+ * Chaining keeps generated loops terse.
63
+ */
64
+ export class HtmlBuilder {
65
+ private parts: Array<string> = new Array<string>();
66
+
67
+ raw(s: string): HtmlBuilder {
68
+ this.parts.push(s);
69
+ return this;
70
+ }
71
+
72
+ text(s: string): HtmlBuilder {
73
+ this.parts.push(escapeHtml(s));
74
+ return this;
75
+ }
76
+
77
+ /** Attribute escaping is identical to text escaping in React. */
78
+ attr(s: string): HtmlBuilder {
79
+ this.parts.push(escapeHtml(s));
80
+ return this;
81
+ }
82
+
83
+ toBytes(): Uint8Array {
84
+ return utf8(this.parts.join(''));
85
+ }
86
+ }
87
+
88
+ /**
89
+ * The render result: status, coherence hash, response headers, and the ordered
90
+ * filled holes. Serialised by `encodeValues` into the values envelope the host
91
+ * splices against the template manifest.
92
+ */
93
+ export class SlotValues {
94
+ status: u16 = 200;
95
+ headers: Array<Header> = new Array<Header>();
96
+ slots: Array<SlotValue> = new Array<SlotValue>();
97
+
98
+ /** `hash` is the route's compiled-in coherence hash (32 bytes); the host
99
+ * rejects the response if it does not match the deployed template. */
100
+ constructor(public templateHash: StaticArray<u8>) {}
101
+
102
+ /** A text hole: React-escaped. */
103
+ setText(slotId: i32, value: string): SlotValues {
104
+ this.slots.push(new SlotValue(slotId, SlotKind.TEXT, utf8(escapeHtml(value))));
105
+ return this;
106
+ }
107
+
108
+ /** A raw-HTML hole: inserted verbatim. The author owns sanitisation, same
109
+ * as React `dangerouslySetInnerHTML`. */
110
+ setRaw(slotId: i32, html: string): SlotValues {
111
+ this.slots.push(new SlotValue(slotId, SlotKind.RAW, utf8(html)));
112
+ return this;
113
+ }
114
+
115
+ /** An attribute-value hole. */
116
+ setAttr(slotId: i32, value: string): SlotValues {
117
+ this.slots.push(new SlotValue(slotId, SlotKind.ATTR, utf8(escapeHtml(value))));
118
+ return this;
119
+ }
120
+
121
+ /** A repeat region: the guest pre-stamped + concatenated rows (built via
122
+ * [`HtmlBuilder`]); the host inserts them verbatim at the region offset. */
123
+ setRepeat(slotId: i32, rows: HtmlBuilder): SlotValues {
124
+ this.slots.push(new SlotValue(slotId, SlotKind.REPEAT, rows.toBytes()));
125
+ return this;
126
+ }
127
+
128
+ /** Add a response header (e.g. a Set-Cookie or Cache-Control). */
129
+ setHeader(name: string, value: string): SlotValues {
130
+ this.headers.push(new Header(name, value));
131
+ return this;
132
+ }
133
+
134
+ setStatus(status: u16): SlotValues {
135
+ this.status = status;
136
+ return this;
137
+ }
138
+ }
139
+
140
+ /** A zeroed 32-byte hash. Used for the fail-safe empty result, which the host
141
+ * rejects as a hash mismatch (-> 500) rather than serving a broken page. */
142
+ export function zeroHash(): StaticArray<u8> {
143
+ return new StaticArray<u8>(HASH_LEN);
144
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * `Time` — the guest's wall-clock, backed by the host's `Date.now()` binding
3
+ * (`env.Date.now`). Both the edge (`toil-backend` `date_now_import.rs`) and the
4
+ * dev server (`toiljs/src/devserver/host.ts`) provide it, so time behaves the
5
+ * same in `toiljs dev` and in production.
6
+ *
7
+ * This is the toiljs-blessed way to read the clock; prefer it over a raw
8
+ * `Date.now()` so the host boundary (and its single millisecond unit) is
9
+ * explicit and easy to find. Like browser `Date.now()` it is WALL-CLOCK, not
10
+ * monotonic: it can step backward across an NTP correction, so never use it to
11
+ * measure elapsed time, only to stamp/compare absolute instants (session
12
+ * `iat`/`exp`, challenge expiry, cache ages).
13
+ *
14
+ * Exposed as an ambient global (`@global`, usable with no import in a handler)
15
+ * and re-exported from `toiljs/server/runtime`.
16
+ */
17
+ @global
18
+ export class Time {
19
+ /** Milliseconds since the Unix epoch (the host `Date.now()` value). */
20
+ static nowMillis(): i64 {
21
+ return <i64>Date.now();
22
+ }
23
+
24
+ /** Whole seconds since the Unix epoch (`nowMillis() / 1000`), the unit used
25
+ * for session and challenge timestamps. */
26
+ static nowSeconds(): u64 {
27
+ return <u64>(Date.now() / 1000);
28
+ }
29
+ }
package/src/cli/create.ts CHANGED
@@ -201,6 +201,10 @@ function scaffold(
201
201
  'multi-value',
202
202
  ],
203
203
  runtime: 'stub',
204
+ // toiljs server globals (exports become ambient, used
205
+ // with no import -- e.g. `AuthService` for the
206
+ // post-quantum auth primitive), exactly like `crypto`.
207
+ lib: ['node_modules/toiljs/server/globals'],
204
208
  // Reserve [0, 64 KiB) for the request envelope the
205
209
  // edge writes at offset 0. Static data starts ABOVE
206
210
  // it, so a large request can never overwrite guest
@@ -259,6 +263,105 @@ function scaffold(
259
263
  return files;
260
264
  }
261
265
 
266
+ /**
267
+ * Editor-only ambient declarations for the toiljs cookie globals (`Cookie` /
268
+ * `Cookies` / `SecureCookies` / `SameSite` / ...). They are `@global` in the
269
+ * runtime, so handlers use them without an import (like `crypto`); this gives the
270
+ * editor their shapes. Auto-included via the server tsconfig and ignored by the
271
+ * compiler. Keep in sync with `toiljs/server/runtime/http/*`.
272
+ */
273
+ const TOIL_SERVER_ENV_DTS = `/**
274
+ * Editor-only ambient declarations for the toiljs cookie globals.
275
+ *
276
+ * \`Cookie\`, \`Cookies\`, \`SecureCookies\`, and the \`SameSite\` / \`CookieEncoding\` /
277
+ * \`CookiePrefix\` enums are \`@global\` in the toiljs server runtime, so a handler
278
+ * uses them with no import (exactly like \`crypto\`). The toilscript compiler
279
+ * registers them from the runtime; this file just gives the editor their shapes.
280
+ * It is auto-included by the server tsconfig and ignored by the compiler.
281
+ */
282
+
283
+ declare enum SameSite {
284
+ Default = 0,
285
+ None = 1,
286
+ Lax = 2,
287
+ Strict = 3,
288
+ }
289
+
290
+ declare enum CookieEncoding {
291
+ Percent = 0,
292
+ Raw = 1,
293
+ Base64Url = 2,
294
+ }
295
+
296
+ declare enum CookiePrefix {
297
+ None = 0,
298
+ Secure = 1,
299
+ Host = 2,
300
+ }
301
+
302
+ declare class CookieValidation {
303
+ valid: bool;
304
+ errors: Array<string>;
305
+ fail(msg: string): void;
306
+ }
307
+
308
+ declare class Cookie {
309
+ name: string;
310
+ value: string;
311
+ encoding: CookieEncoding;
312
+ constructor(name: string, value: string);
313
+ static create(name: string, value: string): Cookie;
314
+ domain(v: string): Cookie;
315
+ path(v: string): Cookie;
316
+ maxAge(seconds: i64): Cookie;
317
+ expires(epochSeconds: i64): Cookie;
318
+ expiresRaw(date: string): Cookie;
319
+ secure(on?: bool): Cookie;
320
+ httpOnly(on?: bool): Cookie;
321
+ sameSite(s: SameSite): Cookie;
322
+ partitioned(on?: bool): Cookie;
323
+ priority(p: string): Cookie;
324
+ extension(av: string): Cookie;
325
+ withEncoding(e: CookieEncoding): Cookie;
326
+ asSecurePrefixed(): Cookie;
327
+ asHostPrefixed(): Cookie;
328
+ detectedPrefix(): CookiePrefix;
329
+ encodedValue(): string;
330
+ validate(): CookieValidation;
331
+ serialize(strict?: bool): string;
332
+ toString(): string;
333
+ }
334
+
335
+ declare class CookieMap {
336
+ set(name: string, value: string): void;
337
+ get(name: string): string | null;
338
+ has(name: string): bool;
339
+ names(): Array<string>;
340
+ readonly size: i32;
341
+ }
342
+
343
+ declare class Cookies {
344
+ static parse(cookieHeader: string): CookieMap;
345
+ static get(cookieHeader: string, name: string): string | null;
346
+ static serialize(name: string, value: string): string;
347
+ static parseSetCookie(setCookie: string): Cookie;
348
+ static encodeValue(raw: string): string;
349
+ static decodeValue(enc: string): string;
350
+ }
351
+
352
+ declare class SecureCookies {
353
+ static signed(key: Uint8Array): SecureCookies;
354
+ static encrypted(key: Uint8Array): SecureCookies;
355
+ addKey(key: Uint8Array): SecureCookies;
356
+ sign(name: string, value: string): string;
357
+ unsign(name: string, sealed: string): string | null;
358
+ encrypt(name: string, value: string): string;
359
+ decrypt(name: string, sealed: string): string | null;
360
+ seal(cookie: Cookie): Cookie;
361
+ open(jar: CookieMap, name: string): string | null;
362
+ }
363
+ `;
364
+
262
365
  /**
263
366
  * A minimal but working server for the `minimal` template (the `app` template copies
264
367
  * examples/basic/server). Same folder conventions as the full starter, just fewer files:
@@ -267,6 +370,7 @@ function scaffold(
267
370
  */
268
371
  function minimalServer(): Record<string, string> {
269
372
  return {
373
+ 'server/toil-server-env.d.ts': TOIL_SERVER_ENV_DTS,
270
374
  'server/core/AppHandler.ts':
271
375
  "import { ToilHandler, Request, Response, Method } from 'toiljs/server/runtime';\n\n" +
272
376
  '/** Every request enters here. Add `@rest` controllers under routes/ as you grow. */\n' +
@@ -612,6 +716,7 @@ export async function runCreate(opts: CreateOptions): Promise<void> {
612
716
  'main.ts',
613
717
  'README.md',
614
718
  'tsconfig.json',
719
+ 'toil-server-env.d.ts',
615
720
  'core',
616
721
  'models',
617
722
  'routes',