toiljs 0.0.33 → 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 +19 -0
- package/README.md +1 -0
- package/as-pect.config.js +8 -2
- package/build/cli/.tsbuildinfo +1 -1
- package/build/cli/index.js +124 -7
- 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/cache.d.ts +8 -0
- package/build/devserver/cache.js +0 -0
- package/build/devserver/crypto.js +15 -0
- package/build/devserver/host.js +1 -0
- package/build/devserver/index.js +10 -1
- 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/README.md +19 -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 +290 -0
- package/examples/basic/server/core/store.ts +31 -0
- package/examples/basic/server/deco-main.ts +18 -0
- package/examples/basic/server/main.ts +13 -2
- package/examples/basic/server/models/NewPlayer.ts +5 -0
- package/examples/basic/server/models/Player.ts +8 -0
- package/examples/basic/server/models/ScoreDelta.ts +5 -0
- package/examples/basic/server/models/Standings.ts +7 -0
- package/examples/basic/server/routes/Auth.ts +184 -0
- package/examples/basic/server/routes/Leaderboard.ts +20 -0
- package/examples/basic/server/routes/Players.ts +53 -0
- package/examples/basic/server/routes/PqDemo.ts +109 -0
- package/examples/basic/server/routes/Session.ts +73 -0
- package/examples/basic/server/scheduled/README.md +7 -0
- package/examples/basic/server/services/Stats.ts +11 -0
- package/examples/basic/server/services/remotes.ts +7 -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 +85 -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 +159 -14
- 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/cache.ts +0 -0
- package/src/devserver/crypto.ts +23 -0
- package/src/devserver/host.ts +4 -0
- package/src/devserver/index.ts +21 -1
- 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 +48 -4
- package/test/fixtures/bignum-wire/spec.ts +27 -0
- package/test/rpc-bignum-wire.test.ts +164 -0
- package/test/ssr-render.test.ts +128 -0
- package/test/ssr-template.test.tsx +348 -0
- package/examples/basic/server/HelloHandler.ts +0 -42
- package/examples/basic/server/api.ts +0 -137
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build-time edge-SSR template extraction.
|
|
3
|
+
*
|
|
4
|
+
* After a route component is rendered to HTML with the marker components in
|
|
5
|
+
* sentinel mode (`__setSsrBuild(true)`), the output carries PUA sentinel tokens
|
|
6
|
+
* at every hole. This module scans that HTML, strips the tokens, records their
|
|
7
|
+
* BYTE offsets, and emits:
|
|
8
|
+
*
|
|
9
|
+
* - `<route>.tmpl` — the stripped static scaffold (React's own bytes, holes
|
|
10
|
+
* removed); the edge mmaps this.
|
|
11
|
+
* - `<route>.slots` — the binary manifest the Rust host parses
|
|
12
|
+
* (`toil-backend/src/host/template/manifest.rs`).
|
|
13
|
+
*
|
|
14
|
+
* The repeat-region row sub-template + the holes' binding metadata are also
|
|
15
|
+
* captured (for the guest `render()` codegen), but those do NOT go in `.slots`
|
|
16
|
+
* (the host only needs insertion points; the guest pre-stamps repeat rows).
|
|
17
|
+
*
|
|
18
|
+
* The pure functions here (`extractFromHtml`, `encodeSlots`, `coherenceHash`,
|
|
19
|
+
* `reactEscapeHtml`, `spliceTemplate`) are deterministic and unit-tested; the
|
|
20
|
+
* `extractTemplates` orchestration drives a short-lived Vite SSR server, the
|
|
21
|
+
* same pattern as `ssg.ts`.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { createHash } from 'node:crypto';
|
|
25
|
+
|
|
26
|
+
/** PUA sentinel framing, mirroring `src/client/ssr/markers.tsx`. */
|
|
27
|
+
const START = String.fromCharCode(0xe000);
|
|
28
|
+
const END = String.fromCharCode(0xe002);
|
|
29
|
+
|
|
30
|
+
export type SlotKind = 'text' | 'raw' | 'attr' | 'repeat';
|
|
31
|
+
|
|
32
|
+
/** Byte value of a kind on the wire / in `.slots`. Mirrors host `SlotKind`. */
|
|
33
|
+
export function kindByte(kind: SlotKind): number {
|
|
34
|
+
switch (kind) {
|
|
35
|
+
case 'text':
|
|
36
|
+
return 0;
|
|
37
|
+
case 'raw':
|
|
38
|
+
return 1;
|
|
39
|
+
case 'attr':
|
|
40
|
+
return 2;
|
|
41
|
+
case 'repeat':
|
|
42
|
+
return 3;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** One extracted hole. `offset` is a byte offset into the enclosing template
|
|
47
|
+
* (the `.tmpl` for top-level slots, or the row sub-template for nested ones). */
|
|
48
|
+
export interface SlotRecord {
|
|
49
|
+
id: string;
|
|
50
|
+
kind: SlotKind;
|
|
51
|
+
offset: number;
|
|
52
|
+
/** Repeat only: the captured single-row scaffold (bytes) ... */
|
|
53
|
+
rowTemplate?: Buffer;
|
|
54
|
+
/** ... and the holes inside that row, offsets relative to `rowTemplate`. */
|
|
55
|
+
rowSlots?: SlotRecord[];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface Extracted {
|
|
59
|
+
/** Stripped static scaffold; the mmap'd `.tmpl`. */
|
|
60
|
+
tmpl: Buffer;
|
|
61
|
+
/** Top-level holes in document order, byte offsets into `tmpl`. */
|
|
62
|
+
slots: SlotRecord[];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
interface ScanResult {
|
|
66
|
+
text: string;
|
|
67
|
+
byteLen: number;
|
|
68
|
+
slots: SlotRecord[];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Scan one HTML string, stripping sentinel tokens and recording hole offsets.
|
|
72
|
+
* Recurses into repeat regions to capture the row sub-template. */
|
|
73
|
+
function scan(html: string): ScanResult {
|
|
74
|
+
let out = '';
|
|
75
|
+
let byteLen = 0;
|
|
76
|
+
const slots: SlotRecord[] = [];
|
|
77
|
+
let i = 0;
|
|
78
|
+
|
|
79
|
+
const emit = (chunk: string): void => {
|
|
80
|
+
out += chunk;
|
|
81
|
+
byteLen += Buffer.byteLength(chunk, 'utf8');
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
while (i < html.length) {
|
|
85
|
+
const start = html.indexOf(START, i);
|
|
86
|
+
if (start === -1) {
|
|
87
|
+
emit(html.slice(i));
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
if (start > i) emit(html.slice(i, start));
|
|
91
|
+
|
|
92
|
+
const kindChar = html[start + 1];
|
|
93
|
+
const tokEnd = html.indexOf(END, start + 2);
|
|
94
|
+
if (tokEnd === -1) throw new Error('toil ssr: unterminated sentinel token');
|
|
95
|
+
const id = html.slice(start + 2, tokEnd);
|
|
96
|
+
const afterTok = tokEnd + 1;
|
|
97
|
+
|
|
98
|
+
if (kindChar === 'R') {
|
|
99
|
+
// Repeat region: collect everything up to the matching close token.
|
|
100
|
+
const closeTok = START + 'r' + id + END;
|
|
101
|
+
const closeIdx = html.indexOf(closeTok, afterTok);
|
|
102
|
+
if (closeIdx === -1) {
|
|
103
|
+
throw new Error(`toil ssr: unterminated repeat region "${id}"`);
|
|
104
|
+
}
|
|
105
|
+
const innerHtml = html.slice(afterTok, closeIdx);
|
|
106
|
+
const inner = scan(innerHtml);
|
|
107
|
+
slots.push({
|
|
108
|
+
id,
|
|
109
|
+
kind: 'repeat',
|
|
110
|
+
offset: byteLen, // region collapses to a zero-width insertion point
|
|
111
|
+
rowTemplate: Buffer.from(inner.text, 'utf8'),
|
|
112
|
+
rowSlots: inner.slots,
|
|
113
|
+
});
|
|
114
|
+
i = closeIdx + closeTok.length;
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const kind: SlotKind | null =
|
|
119
|
+
kindChar === 't' ? 'text' : kindChar === 'h' ? 'raw' : kindChar === 'a' ? 'attr' : null;
|
|
120
|
+
if (kind === null) {
|
|
121
|
+
throw new Error(`toil ssr: unknown sentinel kind "${kindChar ?? ''}"`);
|
|
122
|
+
}
|
|
123
|
+
slots.push({ id, kind, offset: byteLen });
|
|
124
|
+
i = afterTok;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return { text: out, byteLen, slots };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Strip sentinels from a rendered-with-markers HTML string into a `.tmpl` +
|
|
131
|
+
* ordered slot records. */
|
|
132
|
+
export function extractFromHtml(html: string): Extracted {
|
|
133
|
+
const r = scan(html);
|
|
134
|
+
return { tmpl: Buffer.from(r.text, 'utf8'), slots: r.slots };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Assign stable numeric slot ids to top-level holes in document order. These
|
|
138
|
+
* are the ids the host `.slots` and the guest `Slot` enum share. */
|
|
139
|
+
export function assignSlotIds(slots: SlotRecord[]): Map<string, number> {
|
|
140
|
+
const ids = new Map<string, number>();
|
|
141
|
+
let next = 0;
|
|
142
|
+
for (const s of slots) {
|
|
143
|
+
if (!ids.has(s.id)) ids.set(s.id, next++);
|
|
144
|
+
}
|
|
145
|
+
return ids;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Encode the `.slots` binary manifest the Rust host parses. Only top-level
|
|
149
|
+
* slots are emitted (the host's insertion points); repeat row data is for the
|
|
150
|
+
* guest codegen, not the host. */
|
|
151
|
+
export function encodeSlots(
|
|
152
|
+
tmplLen: number,
|
|
153
|
+
hash: Buffer,
|
|
154
|
+
slots: SlotRecord[],
|
|
155
|
+
slotIds: Map<string, number>,
|
|
156
|
+
): Buffer {
|
|
157
|
+
if (hash.length !== 32) throw new Error('toil ssr: coherence hash must be 32 bytes');
|
|
158
|
+
const buf = Buffer.alloc(4 + 2 + 2 + 4 + 32 + 2 + slots.length * 8);
|
|
159
|
+
let o = 0;
|
|
160
|
+
buf.write('TSLT', o, 'ascii'); // magic, read as u32 LE on the host
|
|
161
|
+
o += 4;
|
|
162
|
+
buf.writeUInt16LE(1, o); // version
|
|
163
|
+
o += 2;
|
|
164
|
+
buf.writeUInt16LE(0, o); // flags
|
|
165
|
+
o += 2;
|
|
166
|
+
buf.writeUInt32LE(tmplLen, o);
|
|
167
|
+
o += 4;
|
|
168
|
+
hash.copy(buf, o);
|
|
169
|
+
o += 32;
|
|
170
|
+
buf.writeUInt16LE(slots.length, o);
|
|
171
|
+
o += 2;
|
|
172
|
+
for (const s of slots) {
|
|
173
|
+
const id = slotIds.get(s.id);
|
|
174
|
+
if (id === undefined) throw new Error(`toil ssr: no slot id for "${s.id}"`);
|
|
175
|
+
buf.writeUInt32LE(s.offset, o);
|
|
176
|
+
o += 4;
|
|
177
|
+
buf.writeUInt16LE(id, o);
|
|
178
|
+
o += 2;
|
|
179
|
+
buf.writeUInt8(kindByte(s.kind), o);
|
|
180
|
+
o += 1;
|
|
181
|
+
buf.writeUInt8(0, o); // reserved
|
|
182
|
+
o += 1;
|
|
183
|
+
}
|
|
184
|
+
return buf;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Canonical serialisation of the slot structure (recursive), used by the
|
|
188
|
+
* coherence hash so a change to any hole, kind, or nesting rotates it. */
|
|
189
|
+
function canonicalManifest(slots: SlotRecord[]): string {
|
|
190
|
+
return JSON.stringify(
|
|
191
|
+
slots.map((s) => ({
|
|
192
|
+
id: s.id,
|
|
193
|
+
kind: s.kind,
|
|
194
|
+
offset: s.offset,
|
|
195
|
+
row: s.rowSlots ? canonicalManifest(s.rowSlots) : undefined,
|
|
196
|
+
rowLen: s.rowTemplate ? s.rowTemplate.length : undefined,
|
|
197
|
+
})),
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** Coherence hash binding the `.tmpl` + slot structure. Stored in `.slots` and
|
|
202
|
+
* baked into the guest; the host 500s on a mismatch (deploy skew). */
|
|
203
|
+
export function coherenceHash(tmpl: Buffer, slots: SlotRecord[]): Buffer {
|
|
204
|
+
return createHash('sha256')
|
|
205
|
+
.update(tmpl)
|
|
206
|
+
.update('\0')
|
|
207
|
+
.update(canonicalManifest(slots), 'utf8')
|
|
208
|
+
.digest();
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* React-exact HTML escape (`react-dom/server` `escapeTextForBrowser`, regex
|
|
213
|
+
* `/["'&<>]/`). The SAME function React uses for text AND attributes. Must stay
|
|
214
|
+
* byte-identical to the guest's `server/runtime/ssr/escape.ts`, or hydration
|
|
215
|
+
* mismatches. Used by the golden byte-identity test to simulate the guest.
|
|
216
|
+
*/
|
|
217
|
+
export function reactEscapeHtml(s: string): string {
|
|
218
|
+
let out = '';
|
|
219
|
+
let last = 0;
|
|
220
|
+
for (let i = 0; i < s.length; i++) {
|
|
221
|
+
let rep: string;
|
|
222
|
+
switch (s.charCodeAt(i)) {
|
|
223
|
+
case 34:
|
|
224
|
+
rep = '"';
|
|
225
|
+
break;
|
|
226
|
+
case 38:
|
|
227
|
+
rep = '&';
|
|
228
|
+
break;
|
|
229
|
+
case 39:
|
|
230
|
+
rep = ''';
|
|
231
|
+
break;
|
|
232
|
+
case 60:
|
|
233
|
+
rep = '<';
|
|
234
|
+
break;
|
|
235
|
+
case 62:
|
|
236
|
+
rep = '>';
|
|
237
|
+
break;
|
|
238
|
+
default:
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
out += s.slice(last, i) + rep;
|
|
242
|
+
last = i + 1;
|
|
243
|
+
}
|
|
244
|
+
return last === 0 ? s : out + s.slice(last);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** Generic splice: interleave template slices with hole values at ascending
|
|
248
|
+
* byte offsets. Mirrors the Rust host `assemble`; used by the golden test (and
|
|
249
|
+
* any tooling that needs to materialise a full page from a template + values).
|
|
250
|
+
* `values` maps a byte offset to the bytes inserted there (offsets may repeat
|
|
251
|
+
* is not allowed; pass them in `slots` order). */
|
|
252
|
+
export function spliceTemplate(
|
|
253
|
+
tmpl: Buffer,
|
|
254
|
+
inserts: { offset: number; value: Buffer }[],
|
|
255
|
+
): Buffer {
|
|
256
|
+
const parts: Buffer[] = [];
|
|
257
|
+
let prev = 0;
|
|
258
|
+
for (const ins of inserts) {
|
|
259
|
+
if (ins.offset > prev) parts.push(tmpl.subarray(prev, ins.offset));
|
|
260
|
+
if (ins.value.length > 0) parts.push(ins.value);
|
|
261
|
+
prev = ins.offset;
|
|
262
|
+
}
|
|
263
|
+
if (tmpl.length > prev) parts.push(tmpl.subarray(prev));
|
|
264
|
+
return Buffer.concat(parts);
|
|
265
|
+
}
|
|
Binary file
|
package/src/devserver/crypto.ts
CHANGED
|
@@ -17,6 +17,8 @@
|
|
|
17
17
|
|
|
18
18
|
import * as nodeCrypto from 'node:crypto';
|
|
19
19
|
|
|
20
|
+
import { ml_dsa44 } from '@btc-vision/post-quantum/ml-dsa.js';
|
|
21
|
+
|
|
20
22
|
import type { MemoryRef } from './host.js';
|
|
21
23
|
|
|
22
24
|
// --- ABI id tables (must match the std + Rust backend) ----------------------
|
|
@@ -211,6 +213,27 @@ export function buildCryptoImports(
|
|
|
211
213
|
|
|
212
214
|
'crypto.derive_bits': (h: number, pp: number, pl: number, lengthBits: number): number =>
|
|
213
215
|
deriveBitsOp(cs, ref, h, pp, pl, lengthBits),
|
|
216
|
+
|
|
217
|
+
// ML-DSA-44 (FIPS 204) verify for the auth primitive. Mirrors the edge
|
|
218
|
+
// host (`mldsa_verify_import.rs`): same size asserts, 1/0/neg result.
|
|
219
|
+
// Backed by the same noble lib the client signs with, so dev == prod.
|
|
220
|
+
'crypto.mldsa_verify': (
|
|
221
|
+
pkPtr: number, pkLen: number,
|
|
222
|
+
msgPtr: number, msgLen: number,
|
|
223
|
+
sigPtr: number, sigLen: number,
|
|
224
|
+
ctxPtr: number, ctxLen: number,
|
|
225
|
+
): number => {
|
|
226
|
+
if (pkLen !== 1312 || sigLen !== 2420 || ctxLen > 255) return -4;
|
|
227
|
+
try {
|
|
228
|
+
const pk = new Uint8Array(readBytes(ref, pkPtr, pkLen));
|
|
229
|
+
const msg = new Uint8Array(readBytes(ref, msgPtr, msgLen));
|
|
230
|
+
const sig = new Uint8Array(readBytes(ref, sigPtr, sigLen));
|
|
231
|
+
const ctx = new Uint8Array(readBytes(ref, ctxPtr, ctxLen));
|
|
232
|
+
return ml_dsa44.verify(sig, msg, pk, { context: ctx }) ? 1 : 0;
|
|
233
|
+
} catch {
|
|
234
|
+
return -1;
|
|
235
|
+
}
|
|
236
|
+
},
|
|
214
237
|
};
|
|
215
238
|
}
|
|
216
239
|
|
package/src/devserver/host.ts
CHANGED
|
@@ -137,6 +137,10 @@ export function buildHostImports(ref: MemoryRef, state: DispatchState): WebAssem
|
|
|
137
137
|
|
|
138
138
|
thread_spawn: (_startArg: number): number => -1,
|
|
139
139
|
|
|
140
|
+
// `Date.now()` -> wall-clock milliseconds, matching the edge host.
|
|
141
|
+
// The guest divides by 1000 for Unix seconds (sessions, challenges).
|
|
142
|
+
'Date.now': (): number => Date.now(),
|
|
143
|
+
|
|
140
144
|
// Web Crypto host functions (`env.crypto.*`), backed by Node's
|
|
141
145
|
// `crypto`. The dev server skips metering, so these charge nothing.
|
|
142
146
|
...buildCryptoImports(ref, state.crypto),
|
package/src/devserver/index.ts
CHANGED
|
@@ -25,6 +25,7 @@ import path from 'node:path';
|
|
|
25
25
|
import { Server, type Request, type Response } from '@dacely/hyper-express';
|
|
26
26
|
import pc from 'picocolors';
|
|
27
27
|
|
|
28
|
+
import { applyCacheRule, lookupCache } from './cache.js';
|
|
28
29
|
import { METHOD_CODES, type EnvelopeRequest } from './envelope.js';
|
|
29
30
|
import { WasmServerModule } from './module.js';
|
|
30
31
|
import { proxyToVite, wireWebsocketProxy, type ViteTarget } from './proxy.js';
|
|
@@ -210,10 +211,29 @@ export async function startDevServer(options: DevServerOptions): Promise<Running
|
|
|
210
211
|
|
|
211
212
|
if (dispatchable && module.available) {
|
|
212
213
|
const envelopeReq = await toEnvelopeRequest(request);
|
|
214
|
+
// Honor the tenant cache directive locally, same rules as the
|
|
215
|
+
// edge: serve an identical request from the per-process cache,
|
|
216
|
+
// else dispatch and apply/strip the directive on the response.
|
|
217
|
+
const cacheHost = request.headers.host ?? 'dev';
|
|
218
|
+
const hasAuth =
|
|
219
|
+
request.headers.cookie !== undefined || request.headers.authorization !== undefined;
|
|
220
|
+
const cached = lookupCache(cacheHost, request.method, request.url, envelopeReq.body);
|
|
221
|
+
if (cached !== null) {
|
|
222
|
+
sendWasmResponse(response, root, cached);
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
213
225
|
try {
|
|
214
226
|
const result = module.dispatch(envelopeReq);
|
|
215
227
|
if (!result.unhandled) {
|
|
216
|
-
|
|
228
|
+
const finalized = applyCacheRule(
|
|
229
|
+
cacheHost,
|
|
230
|
+
request.method,
|
|
231
|
+
request.url,
|
|
232
|
+
envelopeReq.body,
|
|
233
|
+
hasAuth,
|
|
234
|
+
result,
|
|
235
|
+
);
|
|
236
|
+
sendWasmResponse(response, root, finalized);
|
|
217
237
|
return;
|
|
218
238
|
}
|
|
219
239
|
} catch (e) {
|
package/src/devserver/module.ts
CHANGED
|
@@ -52,11 +52,12 @@ interface HandleExports {
|
|
|
52
52
|
|
|
53
53
|
/** Host functions the dev server provides under `env` (see `host.ts`). */
|
|
54
54
|
const PROVIDED_IMPORTS = new Set([
|
|
55
|
-
'abort', 'set_status', 'set_header', 'respond_file', 'thread_spawn',
|
|
55
|
+
'abort', 'set_status', 'set_header', 'respond_file', 'thread_spawn', 'Date.now',
|
|
56
56
|
// Web Crypto host functions (see ./crypto.ts).
|
|
57
57
|
'crypto.fill_random', 'crypto.random_uuid', 'crypto.take_result', 'crypto.digest',
|
|
58
58
|
'crypto.import_key', 'crypto.export_key', 'crypto.encrypt', 'crypto.decrypt',
|
|
59
59
|
'crypto.sign', 'crypto.verify', 'crypto.derive_bits',
|
|
60
|
+
'crypto.mldsa_verify',
|
|
60
61
|
]);
|
|
61
62
|
|
|
62
63
|
export class WasmServerModule {
|
|
@@ -151,6 +152,43 @@ export class WasmServerModule {
|
|
|
151
152
|
};
|
|
152
153
|
}
|
|
153
154
|
|
|
155
|
+
/**
|
|
156
|
+
* Run one request through the guest `render` entrypoint (edge SSR). Returns
|
|
157
|
+
* the raw values-envelope bytes (status + template_hash + headers + slot
|
|
158
|
+
* values) that the edge splices against the template manifest. Throws if
|
|
159
|
+
* the module has no `render` export. Mirrors {@link dispatch}'s fresh-
|
|
160
|
+
* instance + grow-and-write-envelope contract.
|
|
161
|
+
*/
|
|
162
|
+
dispatchRender(req: EnvelopeRequest): Uint8Array {
|
|
163
|
+
if (this.module === null) throw new Error(`server wasm not loaded (${this.wasmPath})`);
|
|
164
|
+
|
|
165
|
+
const envelope = encodeRequestEnvelope(req);
|
|
166
|
+
const ref: MemoryRef = { memory: null };
|
|
167
|
+
const state = freshDispatchState();
|
|
168
|
+
const instance = new WebAssembly.Instance(this.module, buildHostImports(ref, state));
|
|
169
|
+
const exports = instance.exports as unknown as HandleExports & {
|
|
170
|
+
render?: (reqOfs: number, reqLen: number) => bigint;
|
|
171
|
+
};
|
|
172
|
+
if (typeof exports.render !== 'function')
|
|
173
|
+
throw new Error(`guest wasm has no 'render' export (${this.wasmPath})`);
|
|
174
|
+
ref.memory = exports.memory;
|
|
175
|
+
|
|
176
|
+
const reqOfs = exports.memory.buffer.byteLength;
|
|
177
|
+
exports.memory.grow(Math.ceil(envelope.length / WASM_PAGE) || 1);
|
|
178
|
+
Buffer.from(exports.memory.buffer).set(envelope, reqOfs);
|
|
179
|
+
|
|
180
|
+
const packed = exports.render(reqOfs, envelope.length);
|
|
181
|
+
const { ptr, len } = unpackHandleResult(packed);
|
|
182
|
+
|
|
183
|
+
const memSize = exports.memory.buffer.byteLength;
|
|
184
|
+
if (len > memSize || ptr + len > memSize)
|
|
185
|
+
throw new Error(
|
|
186
|
+
`guest returned out-of-bounds values: ptr=${String(ptr)} len=${String(len)}`,
|
|
187
|
+
);
|
|
188
|
+
// Copy out of the (about-to-be-dropped) instance's memory.
|
|
189
|
+
return new Uint8Array(exports.memory.buffer, ptr, len).slice();
|
|
190
|
+
}
|
|
191
|
+
|
|
154
192
|
/** Fail instantiation up front, with names, when the guest needs imports we do not provide. */
|
|
155
193
|
private assertImportSurface(module: WebAssembly.Module): void {
|
|
156
194
|
const missing = WebAssembly.Module.imports(module)
|