toiljs 0.0.34 → 0.0.36
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/README.md +1 -0
- package/as-pect.config.js +8 -2
- package/build/cli/.tsbuildinfo +1 -1
- package/build/cli/index.js +97 -0
- package/build/client/.tsbuildinfo +1 -1
- package/build/client/auth.d.ts +42 -0
- package/build/client/auth.js +179 -0
- package/build/client/index.d.ts +5 -1
- package/build/client/index.js +3 -1
- package/build/client/routing/loader.d.ts +1 -0
- package/build/client/routing/loader.js +37 -0
- package/build/client/routing/mount.js +32 -1
- package/build/client/ssr/markers.d.ts +34 -0
- package/build/client/ssr/markers.js +49 -0
- package/build/compiler/.tsbuildinfo +1 -1
- package/build/compiler/docs.js +88 -1
- package/build/compiler/generate.d.ts +2 -0
- package/build/compiler/generate.js +2 -2
- package/build/compiler/index.js +2 -0
- package/build/compiler/ssr-codegen.d.ts +2 -0
- package/build/compiler/ssr-codegen.js +36 -0
- package/build/compiler/template-build.d.ts +29 -0
- package/build/compiler/template-build.js +150 -0
- package/build/compiler/template.d.ts +22 -0
- package/build/compiler/template.js +169 -0
- package/build/devserver/.tsbuildinfo +1 -1
- package/build/devserver/crypto.js +15 -0
- package/build/devserver/host.js +1 -0
- package/build/devserver/module.d.ts +1 -0
- package/build/devserver/module.js +23 -1
- package/docs/README.md +56 -0
- package/docs/auth.md +261 -0
- package/docs/caching.md +115 -0
- package/docs/cookies.md +457 -0
- package/docs/crypto.md +130 -0
- package/docs/data.md +131 -0
- package/docs/getting-started.md +128 -0
- package/docs/routing.md +259 -0
- package/docs/rpc.md +149 -0
- package/docs/ssr.md +184 -0
- package/docs/time.md +43 -0
- package/examples/basic/client/routes/auth.tsx +198 -0
- package/examples/basic/client/routes/cookies.tsx +199 -0
- package/examples/basic/client/routes/features/index.tsx +34 -10
- package/examples/basic/client/routes/hello.tsx +43 -0
- package/examples/basic/client/routes/pq.tsx +135 -0
- package/examples/basic/server/AuthTestHandler.ts +15 -0
- package/examples/basic/server/AuthVerifyHandler.ts +23 -0
- package/examples/basic/server/CacheHandler.ts +25 -0
- package/examples/basic/server/DecoCache.ts +18 -0
- package/examples/basic/server/FastTrapHandler.ts +8 -0
- package/examples/basic/server/SpinHandler.ts +18 -0
- package/examples/basic/server/SsrGreetingRender.ts +27 -0
- package/examples/basic/server/authexample-main.ts +8 -0
- package/examples/basic/server/authtest-main.ts +8 -0
- package/examples/basic/server/authverify-main.ts +8 -0
- package/examples/basic/server/cache-main.ts +8 -0
- package/examples/basic/server/core/AppHandler.ts +243 -0
- package/examples/basic/server/deco-main.ts +18 -0
- package/examples/basic/server/main.ts +2 -0
- package/examples/basic/server/routes/Auth.ts +184 -0
- package/examples/basic/server/routes/PqDemo.ts +109 -0
- package/examples/basic/server/routes/Session.ts +73 -0
- package/examples/basic/server/spin-main.ts +13 -0
- package/examples/basic/server/ssr/greeting.slots.ts +19 -0
- package/examples/basic/server/ssr-main.ts +18 -0
- package/examples/basic/server/toil-server-env.d.ts +94 -0
- package/examples/basic/server/trap-main.ts +8 -0
- package/package.json +5 -3
- package/server/globals/auth.ts +281 -0
- package/server/runtime/README.md +61 -0
- package/server/runtime/env/Server.ts +12 -0
- package/server/runtime/exports/index.ts +17 -0
- package/server/runtime/exports/render.ts +51 -0
- package/server/runtime/http/base64.ts +104 -0
- package/server/runtime/http/cookie.ts +416 -0
- package/server/runtime/http/cookies.ts +197 -0
- package/server/runtime/http/date.ts +72 -0
- package/server/runtime/http/percent.ts +76 -0
- package/server/runtime/http/securecookies.ts +224 -0
- package/server/runtime/index.ts +17 -0
- package/server/runtime/request.ts +24 -0
- package/server/runtime/response.ts +29 -0
- package/server/runtime/ssr/Ssr.ts +43 -0
- package/server/runtime/ssr/encode.ts +110 -0
- package/server/runtime/ssr/escape.ts +83 -0
- package/server/runtime/ssr/slots.ts +144 -0
- package/server/runtime/time.ts +29 -0
- package/src/cli/create.ts +105 -0
- package/src/client/auth.ts +322 -0
- package/src/client/index.ts +5 -1
- package/src/client/routing/loader.ts +56 -0
- package/src/client/routing/mount.tsx +37 -1
- package/src/client/ssr/markers.tsx +140 -0
- package/src/compiler/docs.ts +88 -1
- package/src/compiler/generate.ts +2 -2
- package/src/compiler/index.ts +5 -0
- package/src/compiler/ssr-codegen.ts +85 -0
- package/src/compiler/template-build.ts +275 -0
- package/src/compiler/template.ts +265 -0
- package/src/devserver/crypto.ts +23 -0
- package/src/devserver/host.ts +4 -0
- package/src/devserver/module.ts +39 -1
- package/test/assembly/cookie.spec.ts +302 -0
- package/test/assembly/example.spec.ts +5 -1
- package/test/assembly/ssr.spec.ts +94 -0
- package/test/devserver.test.ts +42 -0
- package/test/ssr-render.test.ts +128 -0
- 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 `'` (React's choice, NOT `'` or
|
|
10
|
+
* `'`), and `"` becomes `"`. 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 = '"';
|
|
35
|
+
else if (c == 0x26) rep = '&';
|
|
36
|
+
else if (c == 0x27) rep = ''';
|
|
37
|
+
else if (c == 0x3c) rep = '<';
|
|
38
|
+
else if (c == 0x3e) rep = '>';
|
|
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',
|