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
@@ -3,7 +3,8 @@
3
3
  // template) and is baked into build/client/features/index.html at build time.
4
4
  export const metadata: Toil.Metadata = {
5
5
  title: 'Features',
6
- description: 'Live demos of every ToilJS feature: routing, data, head/SEO, components, realtime, and binary IO.',
6
+ description:
7
+ 'Live demos of every ToilJS feature: routing, data, REST + RPC, post-quantum auth and sessions, cookies, Web Crypto, head/SEO, components, realtime, and binary IO.',
7
8
  openGraph: { title: 'Every ToilJS feature, demoed', type: 'website' }
8
9
  };
9
10
 
@@ -32,7 +33,37 @@ const groups: { heading: string; items: { href: Toil.Href; label: string; note:
32
33
  href: '/features/actions',
33
34
  label: 'Actions + Form',
34
35
  note: 'useAction / <Form>, pending state, revalidate'
35
- }
36
+ },
37
+ { href: '/io', label: 'Binary IO', note: 'DataWriter / DataReader / FastSet, no import' }
38
+ ]
39
+ },
40
+ {
41
+ heading: 'Server API',
42
+ items: [
43
+ { href: '/rest', label: 'REST controllers', note: '@rest / @get / @post, typed body + RouteContext' },
44
+ { href: '/rpc', label: 'Typed RPC', note: '@service / @remote, called as Server.* with no fetch' },
45
+ { href: '/search', label: 'Search', note: 'server-backed search endpoint' }
46
+ ]
47
+ },
48
+ {
49
+ heading: 'Auth and security',
50
+ items: [
51
+ {
52
+ href: '/pq',
53
+ label: 'Post-quantum auth',
54
+ note: 'ML-DSA-44: derive + sign in the browser, edge verifies (crypto.mldsa_verify)'
55
+ },
56
+ {
57
+ href: '/auth',
58
+ label: 'Sessions and @user / @auth',
59
+ note: 'signed session cookie, guarded /session/me, typed getUser()'
60
+ },
61
+ {
62
+ href: '/cookies',
63
+ label: 'Cookies and SecureCookies',
64
+ note: 'Cookie builder + HMAC-signed / AES-GCM cookies, no import'
65
+ },
66
+ { href: '/crypto', label: 'Web Crypto', note: 'crypto.sha256 / subtle, global, runs in the server wasm' }
36
67
  ]
37
68
  },
38
69
  {
@@ -46,14 +77,7 @@ const groups: { heading: string; items: { href: Toil.Href; label: string; note:
46
77
  heading: 'Components and runtime',
47
78
  items: [
48
79
  { href: '/features/script', label: 'Script', note: 'Toil.Script with a load strategy' },
49
- { href: '/features/realtime', label: 'WebSocket channel', note: 'Toil.useChannel against /_toil' },
50
- { href: '/io', label: 'Binary IO', note: 'DataWriter / DataReader / FastSet, no import' }
51
- ]
52
- },
53
- {
54
- heading: 'Server',
55
- items: [
56
- { href: '/crypto', label: 'Web Crypto', note: 'crypto.sha256 / subtle, global, runs in the server wasm' }
80
+ { href: '/features/realtime', label: 'WebSocket channel', note: 'Toil.useChannel against /_toil' }
57
81
  ]
58
82
  }
59
83
  ];
@@ -0,0 +1,43 @@
1
+ /**
2
+ * An edge-SSR route. `export const ssr = true` opts it into the template
3
+ * extractor: at build time toil renders it once into a template-with-holes
4
+ * (`_ssr/hello.{tmpl,slots}` + a guest `Slot` module); at request time the edge
5
+ * splices the guest's hole values into that template (no per-request render).
6
+ *
7
+ * SSR routes must render under static markup: use the hole markers (`Hole`,
8
+ * `Repeat`, `RawHtml`) and `useLoaderData`, and keep router-hook-dependent or
9
+ * client-only bits inside an `<Island>`.
10
+ */
11
+ import { Hole, Repeat, useLoaderData } from 'toiljs/client';
12
+
13
+ export const ssr = true;
14
+
15
+ interface HelloData {
16
+ name: string;
17
+ items: string[];
18
+ }
19
+
20
+ export const loader = ({ params }: { params: Record<string, string> }): HelloData => ({
21
+ name: params.name ?? 'world',
22
+ items: ['alpha', 'beta', 'gamma'],
23
+ });
24
+
25
+ export default function Hello(): React.JSX.Element {
26
+ const d = useLoaderData<typeof loader>();
27
+ return (
28
+ <section>
29
+ <h1>
30
+ Hello <Hole id="name">{d.name}</Hole>
31
+ </h1>
32
+ <ul>
33
+ <Repeat id="items" each={d.items}>
34
+ {(s: string) => (
35
+ <li>
36
+ <Hole id="item">{s}</Hole>
37
+ </li>
38
+ )}
39
+ </Repeat>
40
+ </ul>
41
+ </section>
42
+ );
43
+ }
@@ -0,0 +1,260 @@
1
+ // Post-quantum identity demo, challenge-response. The browser fetches a
2
+ // SERVER-issued challenge (a fresh nonce the edge HMAC-signs into a token),
3
+ // derives an ML-DSA-44 keypair from a password (Argon2id, all client-side, the
4
+ // password never leaves), signs the message built from the server's nonce, and
5
+ // POSTs the public key + signature + token to the edge, which re-opens the token
6
+ // (rejecting a forged/expired one) and verifies via `crypto.mldsa_verify`
7
+ // (server/routes/PqDemo.ts). The secret key is wiped right after signing.
8
+ //
9
+ // The nonce is server-chosen and tamper-proof, so a client cannot pre-sign or
10
+ // swap in its own. It still isn't the full production login (no single-use
11
+ // consume -> within the TTL a captured proof could be replayed; that needs a
12
+ // store) -- see Auth.login / server/routes/Auth.ts and docs/auth.md.
13
+ import { useCallback, useEffect, useState } from 'react';
14
+
15
+ import { Auth, type IdentityProof } from 'toiljs/client';
16
+ import { Account } from 'shared/server';
17
+
18
+ /** Read the readable companion cookie under either name (HTTP `toil_user` or
19
+ * HTTPS `__Secure-toil_user`) and decode it. Display-only / untrusted. */
20
+ function readCompanion(): Account | null {
21
+ const pairs = (document.cookie || '').split('; ');
22
+ let raw: string | null = null;
23
+ for (const p of pairs) {
24
+ const eq = p.indexOf('=');
25
+ const name = eq > 0 ? p.slice(0, eq) : '';
26
+ if (name === 'toil_user' || name === '__Secure-toil_user') {
27
+ raw = p.slice(eq + 1);
28
+ break;
29
+ }
30
+ }
31
+ if (raw === null) return null;
32
+ try {
33
+ let b = raw.replace(/-/g, '+').replace(/_/g, '/');
34
+ while (b.length % 4) b += '=';
35
+ const bin = atob(b);
36
+ const bytes = new Uint8Array(bin.length);
37
+ for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
38
+ return Account.decode(bytes);
39
+ } catch {
40
+ return null;
41
+ }
42
+ }
43
+
44
+ interface VerifiedUser {
45
+ username: string;
46
+ admin: boolean;
47
+ score: string;
48
+ }
49
+
50
+ export const metadata: Toil.Metadata = {
51
+ title: 'Post-quantum auth',
52
+ description:
53
+ 'ML-DSA-44 (FIPS 204) end-to-end: the browser derives a keypair from a password (Argon2id) and signs; the edge verifies via crypto.mldsa_verify. No secret ever leaves the client.',
54
+ };
55
+
56
+ type Result = { ok: boolean; status: number; text: string } | { error: string };
57
+
58
+ async function postVerify(envelope: Uint8Array): Promise<Result> {
59
+ try {
60
+ const res = await fetch('/pq/verify', {
61
+ method: 'POST',
62
+ headers: { 'content-type': 'application/octet-stream' },
63
+ body: envelope as BodyInit,
64
+ });
65
+ return { ok: res.ok, status: res.status, text: (await res.text()).trim() };
66
+ } catch (e) {
67
+ return { error: e instanceof Error ? e.message : String(e) };
68
+ }
69
+ }
70
+
71
+ /** Flip one byte of the signature (last field) so the proof must fail. */
72
+ function tamper(envelope: Uint8Array): Uint8Array {
73
+ const out = envelope.slice();
74
+ if (out.length > 0) out[out.length - 1] ^= 0x01;
75
+ return out;
76
+ }
77
+
78
+ export default function Pq(): React.JSX.Element {
79
+ const [username, setUsername] = useState('ada');
80
+ const [password, setPassword] = useState('correct horse battery staple');
81
+ const [busy, setBusy] = useState(false);
82
+ const [proof, setProof] = useState<IdentityProof | null>(null);
83
+ const [result, setResult] = useState<Result | null>(null);
84
+ const [companion, setCompanion] = useState<Account | null>(null);
85
+ const [verified, setVerified] = useState<VerifiedUser | null | 'none'>(null);
86
+
87
+ const refreshCompanion = useCallback(() => setCompanion(readCompanion()), []);
88
+ useEffect(refreshCompanion, [refreshCompanion]);
89
+
90
+ const prove = useCallback(
91
+ async (doTamper: boolean) => {
92
+ setBusy(true);
93
+ setResult(null);
94
+ setVerified(null);
95
+ try {
96
+ const p = await Auth.proveIdentity(username, password);
97
+ setProof(p);
98
+ // A real (untampered) proof logs in: /pq/verify mints the session.
99
+ const r = await postVerify(doTamper ? tamper(p.envelope) : p.envelope);
100
+ setResult(r);
101
+ refreshCompanion();
102
+ } catch (e) {
103
+ setResult({ error: e instanceof Error ? e.message : String(e) });
104
+ } finally {
105
+ setBusy(false);
106
+ }
107
+ },
108
+ [username, password, refreshCompanion],
109
+ );
110
+
111
+ /** Hit the @auth-guarded /session/me to prove the PQ login established a
112
+ * real session the server re-verifies. */
113
+ const checkSession = useCallback(async () => {
114
+ setBusy(true);
115
+ try {
116
+ const res = await fetch('/session/me', { credentials: 'same-origin' });
117
+ if (res.status === 401) {
118
+ setVerified('none');
119
+ return;
120
+ }
121
+ const r = new DataReader(new Uint8Array(await res.arrayBuffer()));
122
+ setVerified({ username: r.readString(), admin: r.readBool(), score: r.readU64().toString() });
123
+ } finally {
124
+ setBusy(false);
125
+ }
126
+ }, []);
127
+
128
+ const logout = useCallback(async () => {
129
+ setBusy(true);
130
+ try {
131
+ await fetch('/session/logout', { method: 'POST', credentials: 'same-origin' });
132
+ refreshCompanion();
133
+ setVerified(null);
134
+ setResult(null);
135
+ } finally {
136
+ setBusy(false);
137
+ }
138
+ }, [refreshCompanion]);
139
+
140
+ return (
141
+ <main style={{ maxWidth: 680 }}>
142
+ <h1>Post-quantum login</h1>
143
+ <p>
144
+ The edge issues a fresh, HMAC-signed <strong>challenge</strong> (a server-chosen nonce). The browser
145
+ stretches the password with <strong>Argon2id</strong>, expands it into an <strong>ML-DSA-44</strong>{' '}
146
+ (FIPS 204) keypair, and signs the message built from <em>that</em> nonce. Only the public key,
147
+ signature, and the server's token are sent back; the edge re-opens the token and verifies with the{' '}
148
+ <code>crypto.mldsa_verify</code> host import. The password and secret key never leave this tab.
149
+ </p>
150
+
151
+ <div style={{ display: 'grid', gap: 8, maxWidth: 420 }}>
152
+ <label>
153
+ Username
154
+ <input value={username} onChange={(e) => setUsername(e.target.value)} style={{ width: '100%' }} />
155
+ </label>
156
+ <label>
157
+ Password
158
+ <input
159
+ value={password}
160
+ onChange={(e) => setPassword(e.target.value)}
161
+ style={{ width: '100%' }}
162
+ />
163
+ </label>
164
+ <div style={{ display: 'flex', gap: 8 }}>
165
+ <button onClick={() => prove(false)} disabled={busy}>
166
+ {busy ? 'Deriving + signing…' : 'Log in'}
167
+ </button>
168
+ <button onClick={() => prove(true)} disabled={busy} title="Flip a signature byte: must fail">
169
+ Tamper, then verify
170
+ </button>
171
+ </div>
172
+ <p style={{ fontSize: '0.8rem', opacity: 0.7, margin: 0 }}>
173
+ Demo: pre-filled <code>ada</code> / <code>correct horse battery staple</code> &mdash; but any
174
+ username + password works. The keypair is derived from the <strong>username AND password</strong>:
175
+ Argon2id is salted with the username (<code>sha256(&quot;pq-demo|&quot; + username)</code>), so two
176
+ people with the same password get different identities. Same username+password always maps to the
177
+ same keypair (no signup). What a real app adds (and this stateless demo can&apos;t, without a
178
+ store): binding a username to a <em>registered</em> key, so here the username is self-asserted
179
+ &mdash; <code>server/routes/Auth.ts</code>.
180
+ </p>
181
+ </div>
182
+
183
+ {proof && (
184
+ <p style={{ marginTop: 16, fontFamily: 'monospace', fontSize: '0.85rem', opacity: 0.8 }}>
185
+ server nonce {proof.nonceHex}… · Argon2id {proof.deriveMs} ms · public key {proof.publicKeyHex}…
186
+ (1312 B) · signature {proof.signatureLen} B
187
+ </p>
188
+ )}
189
+
190
+ {result && (
191
+ <p
192
+ style={{
193
+ marginTop: 8,
194
+ fontWeight: 600,
195
+ color: 'error' in result ? '#c0392b' : result.ok ? '#1e8449' : '#c0392b',
196
+ }}
197
+ >
198
+ {'error' in result
199
+ ? `error: ${result.error}`
200
+ : `POST /pq/verify -> ${result.status}: ${result.text}`}
201
+ </p>
202
+ )}
203
+
204
+ {companion && (
205
+ <section style={{ marginTop: 20, borderTop: '1px solid #8884', paddingTop: 16 }}>
206
+ <h2 style={{ fontSize: '1.05rem' }}>
207
+ Signed in &mdash; the post-quantum proof minted a session
208
+ </h2>
209
+ <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1rem' }}>
210
+ <div style={{ border: '1px solid #2563ff55', borderRadius: 8, padding: '0.6rem 0.9rem' }}>
211
+ <strong>getUser(), client</strong>
212
+ <div style={{ fontSize: '0.8em', opacity: 0.7 }}>readable companion, untrusted</div>
213
+ <pre>
214
+ {JSON.stringify(
215
+ { username: companion.username, admin: companion.admin, score: String(companion.score) },
216
+ null,
217
+ 2,
218
+ )}
219
+ </pre>
220
+ </div>
221
+ <div style={{ border: '1px solid #7c3aed55', borderRadius: 8, padding: '0.6rem 0.9rem' }}>
222
+ <strong>
223
+ /session/me, <code>@auth</code>
224
+ </strong>
225
+ <div style={{ fontSize: '0.8em', opacity: 0.7 }}>the server re-verifies the session</div>
226
+ {verified === null ? (
227
+ <p>not checked</p>
228
+ ) : verified === 'none' ? (
229
+ <p>401, no session</p>
230
+ ) : (
231
+ <pre>{JSON.stringify(verified, null, 2)}</pre>
232
+ )}
233
+ </div>
234
+ </div>
235
+ <div style={{ display: 'flex', gap: 8, marginTop: 10 }}>
236
+ <button onClick={checkSession} disabled={busy}>
237
+ Check /session/me (@auth)
238
+ </button>
239
+ <button onClick={logout} disabled={busy}>
240
+ Logout
241
+ </button>
242
+ </div>
243
+ </section>
244
+ )}
245
+
246
+ <p style={{ marginTop: 24, opacity: 0.7, fontSize: '0.9rem' }}>
247
+ This is the full login: the password derives an ML-DSA-44 keypair client-side, the edge verifies the
248
+ signature, and on success it mints the signed <code>__Host-toil_sess</code> session, so every{' '}
249
+ <code>@auth</code> route (like <code>/session/me</code>) and <code>getUser()</code> now recognise you.
250
+ The challenge is server-issued and tamper-proof but stateless, so it has no single-use consume yet
251
+ (within the TTL a captured proof could be replayed; the atomic-consume shape is in{' '}
252
+ <code>server/routes/Auth.ts</code>). Plain sessions are on the{' '}
253
+ <Toil.Link href="/auth">Auth</Toil.Link> page.
254
+ </p>
255
+ <p>
256
+ <Toil.Link href="/features">Back to features</Toil.Link>
257
+ </p>
258
+ </main>
259
+ );
260
+ }
@@ -0,0 +1,15 @@
1
+ import { Request, Response, ToilHandler } from 'toiljs/server/runtime';
2
+
3
+ // AuthService is used with NO import (a global via toilscript --lib).
4
+ export class AuthTestHandler extends ToilHandler {
5
+ public handle(req: Request): Response {
6
+ const cid = new Uint8Array(16);
7
+ const nonce = new Uint8Array(32);
8
+ const msg = AuthService.buildLoginMessage('alice', 'toil-demo', cid, nonce, 1000, 2000);
9
+ // Dummy pk/sig -> verify must be false (also exercises the host import binding).
10
+ const pk = new Uint8Array(AuthService.PUBLIC_KEY_LEN);
11
+ const sig = new Uint8Array(AuthService.SIGNATURE_LEN);
12
+ const ok = AuthService.verifyLogin(pk, msg, sig);
13
+ return Response.text('msglen=' + msg.length.toString() + ' verify=' + (ok ? '1' : '0') + '\n');
14
+ }
15
+ }
@@ -0,0 +1,23 @@
1
+ import { Method, Request, Response, ToilHandler } from 'toiljs/server/runtime';
2
+ import { DataReader } from 'data';
3
+
4
+ // Reads {sub, aud, cid, nonce, iat, exp, pk, sig} as a binary body, rebuilds
5
+ // the login message with the AS AuthService (server-authoritative encoding),
6
+ // and verifies the client signature. Proves the full client->edge chain.
7
+ export class AuthVerifyHandler extends ToilHandler {
8
+ public handle(req: Request): Response {
9
+ if (req.method != Method.POST) return Response.empty(405);
10
+ const r = new DataReader(req.body);
11
+ const sub = r.readString();
12
+ const aud = r.readString();
13
+ const cid = r.readBytes();
14
+ const nonce = r.readBytes();
15
+ const iat = r.readU64();
16
+ const exp = r.readU64();
17
+ const pk = r.readBytes();
18
+ const sig = r.readBytes();
19
+ const msg = AuthService.buildLoginMessage(sub, aud, cid, nonce, iat, exp);
20
+ const ok = AuthService.verifyLogin(pk, msg, sig);
21
+ return Response.text((ok ? '1' : '0') + '\n');
22
+ }
23
+ }
@@ -0,0 +1,25 @@
1
+ import { Method, Request, Response, ToilHandler } from 'toiljs/server/runtime';
2
+
3
+ // Exercises the tenant-directed cache. Detection is via the host's
4
+ // `Toil-Cache` response header (MISS on first compute+store, HIT on reuse,
5
+ // DYNAMIC when not cached), so the body need not vary.
6
+ export class CacheHandler extends ToilHandler {
7
+ public handle(req: Request): Response {
8
+ if (req.method == Method.GET && req.path == '/cacheable') {
9
+ // edge-cache 5 min + browser Cache-Control max-age=60
10
+ return Response.json('{"cached":true}').cache(5, 60);
11
+ }
12
+ if (req.method == Method.GET && req.path == '/auth-ok') {
13
+ // edge-cache even when the request carries auth (allowAuth)
14
+ return Response.json('{"authok":true}').cache(5, 0, false, true);
15
+ }
16
+ if (req.method == Method.GET && req.path == '/uncacheable') {
17
+ return Response.json('{"cached":false}');
18
+ }
19
+ if (req.method == Method.POST && req.path == '/echo') {
20
+ // cache per (path, body): same body hits, different body misses
21
+ return Response.bytes(req.body).cache(5);
22
+ }
23
+ return Response.notFound();
24
+ }
25
+ }
@@ -0,0 +1,18 @@
1
+ import { Response, RouteContext } from 'toiljs/server/runtime';
2
+
3
+ // @rest controller exercising the new compile-time @cache decorator.
4
+ @rest('deco')
5
+ class DecoCache {
6
+ // edge-cache 5 min + browser Cache-Control max-age=60, via the decorator
7
+ @get('/cached')
8
+ @cache(5, 60)
9
+ public cached(ctx: RouteContext): Response {
10
+ return Response.json('{"deco":"cached"}');
11
+ }
12
+
13
+ // no @cache -> never cached
14
+ @get('/plain')
15
+ public plain(ctx: RouteContext): Response {
16
+ return Response.json('{"deco":"plain"}');
17
+ }
18
+ }
@@ -0,0 +1,8 @@
1
+ import { Request, Response, ToilHandler } from 'toiljs/server/runtime';
2
+
3
+ export class FastTrapHandler extends ToilHandler {
4
+ public handle(req: Request): Response {
5
+ unreachable(); // wasm `unreachable` -> instant trap, ~0 gas
6
+ return Response.text('x');
7
+ }
8
+ }
@@ -0,0 +1,18 @@
1
+ import { Request, Response, ToilHandler } from 'toiljs/server/runtime';
2
+
3
+ // Module-level counter so the loop body has an observable side effect the
4
+ // optimizer cannot remove (and even a bare loop would still be gas-metered).
5
+ let counter: i64 = 0;
6
+
7
+ export class SpinHandler extends ToilHandler {
8
+ public handle(req: Request): Response {
9
+ // Infinite CPU burn on EVERY request. The edge's per-request gas
10
+ // budget (MAX_GAS_WASM_INIT) must trap this and 502 instead of
11
+ // freezing the worker.
12
+ while (true) {
13
+ counter = counter + 1;
14
+ }
15
+ // unreachable
16
+ return Response.text('unreachable\n');
17
+ }
18
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * A hand-written edge-SSR `render` for the `/hello` route, authored against the
3
+ * generated typed `Slot` enum + `HASH`. It derives its data from the request
4
+ * and fills the holes; the host splices these values into the precompiled
5
+ * template. Registers itself with the `Ssr` router (compiler-injected in a real
6
+ * build; explicit here).
7
+ */
8
+ import { Request } from 'toiljs/server/runtime';
9
+ import { HtmlBuilder, SlotValues, Ssr } from 'toiljs/server/runtime';
10
+ import { HASH, Slot } from './ssr/greeting.slots';
11
+
12
+ function renderGreeting(req: Request): SlotValues | null {
13
+ if (req.path != '/hello') return null;
14
+ const v = new SlotValues(HASH);
15
+ // A text hole: React-escaped (note the `&` and `<>` get entities).
16
+ v.setText(Slot.greeting, 'world & <friends>');
17
+ // A repeat region: three stamped rows.
18
+ const rows = new HtmlBuilder();
19
+ const items: string[] = ['a & b', '<c>', 'd'];
20
+ for (let i = 0; i < items.length; i++) {
21
+ rows.raw('<li>').text(items[i]).raw('</li>');
22
+ }
23
+ v.setRepeat(Slot.count, rows);
24
+ return v;
25
+ }
26
+
27
+ Ssr.register(renderGreeting);
@@ -0,0 +1,8 @@
1
+ import { Server } from 'toiljs/server/runtime';
2
+ import { revertOnError } from 'toiljs/server/runtime/abort/abort';
3
+ import { Request, Response, Rest, ToilHandler } from 'toiljs/server/runtime';
4
+ import './routes/Auth';
5
+ class H extends ToilHandler { public handle(req: Request): Response { const h = Rest.dispatch(req); return h != null ? h : Response.notFound(); } }
6
+ Server.handler = () => { return new H(); };
7
+ export * from 'toiljs/server/runtime/exports';
8
+ export function abort(m: string, f: string, l: u32, c: u32): void { revertOnError(m, f, l, c); }
@@ -0,0 +1,8 @@
1
+ import { Server } from 'toiljs/server/runtime';
2
+ import { revertOnError } from 'toiljs/server/runtime/abort/abort';
3
+ import { AuthTestHandler } from './AuthTestHandler';
4
+ Server.handler = () => { return new AuthTestHandler(); };
5
+ export * from 'toiljs/server/runtime/exports';
6
+ export function abort(message: string, fileName: string, line: u32, column: u32): void {
7
+ revertOnError(message, fileName, line, column);
8
+ }
@@ -0,0 +1,8 @@
1
+ import { Server } from 'toiljs/server/runtime';
2
+ import { revertOnError } from 'toiljs/server/runtime/abort/abort';
3
+ import { AuthVerifyHandler } from './AuthVerifyHandler';
4
+ Server.handler = () => { return new AuthVerifyHandler(); };
5
+ export * from 'toiljs/server/runtime/exports';
6
+ export function abort(message: string, fileName: string, line: u32, column: u32): void {
7
+ revertOnError(message, fileName, line, column);
8
+ }
@@ -0,0 +1,8 @@
1
+ import { Server } from 'toiljs/server/runtime';
2
+ import { revertOnError } from 'toiljs/server/runtime/abort/abort';
3
+ import { CacheHandler } from './CacheHandler';
4
+ Server.handler = () => { return new CacheHandler(); };
5
+ export * from 'toiljs/server/runtime/exports';
6
+ export function abort(message: string, fileName: string, line: u32, column: u32): void {
7
+ revertOnError(message, fileName, line, column);
8
+ }