toiljs 0.0.54 → 0.0.56

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 (105) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/build/backend/.tsbuildinfo +1 -1
  3. package/build/cli/.tsbuildinfo +1 -1
  4. package/build/cli/index.js +9 -5
  5. package/build/client/.tsbuildinfo +1 -1
  6. package/build/client/auth.js +1 -1
  7. package/build/client/components/Image.d.ts +1 -1
  8. package/build/client/dev/devtools.js +3 -1
  9. package/build/client/index.d.ts +2 -2
  10. package/build/client/index.js +2 -2
  11. package/build/client/routing/Router.js +1 -1
  12. package/build/client/routing/mount.js +1 -1
  13. package/build/compiler/.tsbuildinfo +1 -1
  14. package/build/compiler/docs.js +1 -1
  15. package/build/compiler/seo.js +1 -3
  16. package/build/compiler/template-build.js +1 -1
  17. package/build/devserver/.tsbuildinfo +1 -1
  18. package/build/devserver/cache.js +0 -0
  19. package/build/devserver/crypto.js +45 -17
  20. package/build/devserver/database.d.ts +8 -0
  21. package/build/devserver/database.js +416 -0
  22. package/build/devserver/email/caps.js +0 -0
  23. package/build/devserver/email/config.js +7 -2
  24. package/build/devserver/email/validate.js +1 -4
  25. package/build/devserver/host.d.ts +2 -0
  26. package/build/devserver/host.js +3 -2
  27. package/build/devserver/index.d.ts +1 -1
  28. package/build/devserver/index.js +3 -2
  29. package/build/devserver/module.js +52 -7
  30. package/build/devserver/proxy.js +2 -1
  31. package/build/io/.tsbuildinfo +1 -1
  32. package/build/io/codec.d.ts +5 -5
  33. package/build/io/codec.js +193 -77
  34. package/examples/basic/client/components/HoneycombBackground.tsx +1 -1
  35. package/examples/basic/client/public/images/logo.svg +37 -34
  36. package/examples/basic/client/public/index.html +14 -14
  37. package/examples/basic/client/routes/auth.tsx +18 -10
  38. package/examples/basic/client/routes/cookies.tsx +15 -24
  39. package/examples/basic/client/routes/crypto.tsx +4 -5
  40. package/examples/basic/client/routes/features/template/template.tsx +1 -1
  41. package/examples/basic/client/routes/hello.tsx +1 -1
  42. package/examples/basic/client/routes/pq.tsx +14 -14
  43. package/examples/basic/client/routes/rest.tsx +50 -1
  44. package/examples/basic/client/styles/main.css +25 -22
  45. package/examples/basic/client/toil.tsx +1 -1
  46. package/examples/basic/server/README.md +8 -8
  47. package/examples/basic/server/core/AppHandler.ts +4 -7
  48. package/examples/basic/server/main.ts +1 -0
  49. package/examples/basic/server/models/GuestEntry.ts +12 -0
  50. package/examples/basic/server/models/GuestbookView.ts +10 -0
  51. package/examples/basic/server/models/NewMessage.ts +6 -0
  52. package/examples/basic/server/routes/Auth.ts +50 -106
  53. package/examples/basic/server/routes/EnvDemo.ts +9 -3
  54. package/examples/basic/server/routes/Guestbook.ts +62 -0
  55. package/package.json +2 -2
  56. package/server/globals/auth.ts +3 -3
  57. package/server/globals/twofactor.ts +2 -1
  58. package/server/runtime/http/securecookies.ts +3 -2
  59. package/src/backend/index.ts +4 -2
  60. package/src/cli/doctor.ts +10 -3
  61. package/src/cli/notify.ts +1 -6
  62. package/src/cli/ui.ts +3 -3
  63. package/src/cli/version-check.ts +5 -1
  64. package/src/client/auth.ts +33 -10
  65. package/src/client/components/Form.tsx +2 -2
  66. package/src/client/components/Image.tsx +1 -1
  67. package/src/client/components/Script.tsx +1 -1
  68. package/src/client/components/Slot.tsx +1 -1
  69. package/src/client/dev/devtools.tsx +121 -54
  70. package/src/client/dev/error-overlay.tsx +7 -1
  71. package/src/client/head/metadata.ts +1 -1
  72. package/src/client/index.ts +13 -2
  73. package/src/client/routing/Router.tsx +2 -2
  74. package/src/client/routing/error-boundary.tsx +1 -1
  75. package/src/client/routing/loader.ts +2 -2
  76. package/src/client/routing/mount.tsx +5 -6
  77. package/src/compiler/docs.ts +1 -1
  78. package/src/compiler/email-preview.ts +1 -1
  79. package/src/compiler/generate.ts +1 -1
  80. package/src/compiler/seo.ts +1 -3
  81. package/src/compiler/ssg.ts +10 -4
  82. package/src/compiler/template-build.ts +2 -7
  83. package/src/compiler/template.ts +1 -4
  84. package/src/compiler/vite.ts +1 -1
  85. package/src/devserver/cache.ts +0 -0
  86. package/src/devserver/crypto.ts +140 -51
  87. package/src/devserver/database.ts +600 -0
  88. package/src/devserver/dotenv.ts +10 -2
  89. package/src/devserver/email/caps.ts +0 -0
  90. package/src/devserver/email/config.ts +8 -2
  91. package/src/devserver/email/index.ts +3 -3
  92. package/src/devserver/email/validate.ts +1 -4
  93. package/src/devserver/envelope.ts +3 -3
  94. package/src/devserver/host.ts +22 -9
  95. package/src/devserver/index.ts +15 -6
  96. package/src/devserver/module.ts +59 -11
  97. package/src/devserver/proxy.ts +5 -7
  98. package/src/io/codec.ts +226 -83
  99. package/test/devserver-database.test.ts +364 -0
  100. package/test/devserver-pqauth.test.ts +5 -65
  101. package/test/example-guestbook.test.ts +78 -0
  102. package/test/pqauth-e2e.test.ts +6 -6
  103. package/build/devserver/kv.d.ts +0 -3
  104. package/build/devserver/kv.js +0 -53
  105. package/src/devserver/kv.ts +0 -93
@@ -3,14 +3,14 @@
3
3
  // surface plus HMAC signing and AES-256-GCM encryption, running in the server wasm.
4
4
  // These controls call the `/api/cookies/*` routes in `server/core/AppHandler.ts`.
5
5
  // Needs the server running to respond.
6
- import { useState, type CSSProperties } from 'react';
6
+ import { type CSSProperties, useState } from 'react';
7
7
 
8
8
  import { useBrowserValue } from '../lib/useBrowserValue';
9
9
 
10
10
  export const metadata: Toil.Metadata = {
11
11
  title: 'Cookies',
12
12
  description:
13
- 'Server-side cookies as a global: the Cookie builder, parsing, HMAC signing, and AES-256-GCM encryption, running in the server wasm.',
13
+ 'Server-side cookies as a global: the Cookie builder, parsing, HMAC signing, and AES-256-GCM encryption, running in the server wasm.'
14
14
  };
15
15
 
16
16
  interface SetResp {
@@ -39,7 +39,7 @@ const card: CSSProperties = {
39
39
  borderRadius: 8,
40
40
  padding: '12px 16px',
41
41
  margin: '12px 0',
42
- background: '#0c1218',
42
+ background: '#0c1218'
43
43
  };
44
44
  const label: CSSProperties = { opacity: 0.7, fontSize: '0.8rem', marginTop: 6 };
45
45
 
@@ -94,22 +94,18 @@ export default function CookiesDemo() {
94
94
  readJs();
95
95
  });
96
96
  const doSeal = (): Promise<void> =>
97
- guard(async () =>
98
- setSeal(await getJSON<SealResp>('/api/cookies/seal?v=' + encodeURIComponent(sealInput))),
99
- );
97
+ guard(async () => setSeal(await getJSON<SealResp>('/api/cookies/seal?v=' + encodeURIComponent(sealInput))));
100
98
 
101
99
  return (
102
100
  <main style={{ maxWidth: 760 }}>
103
101
  <h1>Cookies</h1>
104
102
  <p>
105
- <code>Cookie</code>, <code>Cookies</code>, and <code>SecureCookies</code> are globals in
106
- the server (no import), exactly like <code>crypto</code>: the full RFC 6265bis surface
107
- plus HMAC signing and AES-256-GCM encryption, running in the server wasm. See{' '}
108
- <code>server/core/AppHandler.ts</code>. Needs the server running (<code>toiljs dev</code>).
103
+ <code>Cookie</code>, <code>Cookies</code>, and <code>SecureCookies</code> are globals in the server (no
104
+ import), exactly like <code>crypto</code>: the full RFC 6265bis surface plus HMAC signing and
105
+ AES-256-GCM encryption, running in the server wasm. See <code>server/core/AppHandler.ts</code>. Needs
106
+ the server running (<code>toiljs dev</code>).
109
107
  </p>
110
-
111
108
  {err ? <p style={{ color: '#ff6b6b', ...mono }}>{err}</p> : null}
112
-
113
109
  <h2>1. Everything you can do</h2>
114
110
  <p>Every attribute and cookie type, with the exact `Set-Cookie` string it serializes to.</p>
115
111
  <button onClick={showGallery}>Show the gallery</button>
@@ -123,12 +119,11 @@ export default function CookiesDemo() {
123
119
  ))}
124
120
  </div>
125
121
  ) : null}
126
-
127
122
  <h2>2. Set cookies</h2>
128
123
  <p>
129
124
  Stores three real cookies: a plain <code>visits</code> counter, an HMAC-signed{' '}
130
- <code>__Host-session</code>, and an AES-GCM-encrypted <code>secret</code>. The last two
131
- are <code>HttpOnly</code>, so JavaScript cannot read them, only the server can.
125
+ <code>__Host-session</code>, and an AES-GCM-encrypted <code>secret</code>. The last two are{' '}
126
+ <code>HttpOnly</code>, so JavaScript cannot read them, only the server can.
132
127
  </p>
133
128
  <button onClick={doSet}>Set cookies</button>
134
129
  {setResp ? (
@@ -141,12 +136,11 @@ export default function CookiesDemo() {
141
136
  ))}
142
137
  </div>
143
138
  ) : null}
144
-
145
139
  <h2>3. What JS sees vs what the server sees</h2>
146
140
  <p>
147
- <code>document.cookie</code> only exposes non-<code>HttpOnly</code> cookies, so the
148
- signed session and encrypted secret are hidden from it. The server parses all of them
149
- and verifies/decrypts the protected ones.
141
+ <code>document.cookie</code> only exposes non-<code>HttpOnly</code> cookies, so the signed session and
142
+ encrypted secret are hidden from it. The server parses all of them and verifies/decrypts the protected
143
+ ones.
150
144
  </p>
151
145
  <button onClick={readJs}>Read document.cookie</button>{' '}
152
146
  <button onClick={doInspect}>Ask the server (/inspect)</button>
@@ -163,7 +157,6 @@ export default function CookiesDemo() {
163
157
  <div style={mono}>secret (AES-GCM-decrypted): {inspect.secret ?? 'null (missing or tampered)'}</div>
164
158
  </div>
165
159
  ) : null}
166
-
167
160
  <h2>4. Clear</h2>
168
161
  <button onClick={doClear}>Clear the demo cookies</button>
169
162
  {cleared ? (
@@ -175,12 +168,11 @@ export default function CookiesDemo() {
175
168
  ))}
176
169
  </div>
177
170
  ) : null}
178
-
179
171
  <h2>5. Sign &amp; encrypt a value</h2>
180
172
  <p>
181
173
  <code>SecureCookies.signed(key)</code> (HMAC-SHA256, readable but tamper-proof) and{' '}
182
- <code>SecureCookies.encrypted(key)</code> (AES-256-GCM, confidential). Both bind the
183
- value to the cookie name, and a tampered signature fails to verify.
174
+ <code>SecureCookies.encrypted(key)</code> (AES-256-GCM, confidential). Both bind the value to the cookie
175
+ name, and a tampered signature fails to verify.
184
176
  </p>
185
177
  <input
186
178
  value={sealInput}
@@ -198,7 +190,6 @@ export default function CookiesDemo() {
198
190
  <div style={mono}>tampered signature verifies? {String(seal.tamperVerifies)}</div>
199
191
  </div>
200
192
  ) : null}
201
-
202
193
  <p style={{ marginTop: 24 }}>
203
194
  <Toil.Link href="/features">Back to features</Toil.Link>
204
195
  </p>
@@ -37,11 +37,10 @@ export default function CryptoDemo() {
37
37
  <main>
38
38
  <h1>Web Crypto</h1>
39
39
  <p>
40
- <code>crypto</code> is a global in the server (no import), synchronous, the same
41
- SubtleCrypto-style API as the browser, running in the server wasm via metered host
42
- functions. These buttons call the server&apos;s <code>/api/hash</code> and{' '}
43
- <code>/api/uuid</code> routes (see <code>server/HelloHandler.ts</code>). Needs the
44
- server running to respond.
40
+ <code>crypto</code> is a global in the server (no import), synchronous, the same SubtleCrypto-style API
41
+ as the browser, running in the server wasm via metered host functions. These buttons call the
42
+ server&apos;s <code>/api/hash</code> and <code>/api/uuid</code> routes (see{' '}
43
+ <code>server/HelloHandler.ts</code>). Needs the server running to respond.
45
44
  </p>
46
45
  <button onClick={onHash}>SHA-256</button> <button onClick={onUuid}>random UUID</button>
47
46
  <ul style={{ marginTop: 16, listStyle: 'none', padding: 0 }}>
@@ -1,4 +1,4 @@
1
- import { useState, type ReactNode } from 'react';
1
+ import { type ReactNode, useState } from 'react';
2
2
 
3
3
  // A template wraps a segment like a layout, but RE-MOUNTS on every navigation within it (a layout
4
4
  // persists). This counter increments each time the template mounts, so navigating between the two
@@ -19,7 +19,7 @@ interface HelloData {
19
19
 
20
20
  export const loader = ({ params }: { params: Record<string, string> }): HelloData => ({
21
21
  name: params.name ?? 'world',
22
- items: ['alpha', 'beta', 'gamma'],
22
+ items: ['alpha', 'beta', 'gamma']
23
23
  });
24
24
 
25
25
  export default function Hello(): React.JSX.Element {
@@ -49,7 +49,7 @@ interface VerifiedUser {
49
49
  export const metadata: Toil.Metadata = {
50
50
  title: 'Post-quantum auth',
51
51
  description:
52
- 'A server-keyed-salt OPRF + ML-DSA-44 (FIPS 204) auth + ML-KEM-768 (FIPS 203) mutual auth. The password never leaves the browser.',
52
+ 'A server-keyed-salt OPRF + ML-DSA-44 (FIPS 204) auth + ML-KEM-768 (FIPS 203) mutual auth. The password never leaves the browser.'
53
53
  };
54
54
 
55
55
  type Note = { kind: 'ok' | 'err'; text: string } | null;
@@ -73,7 +73,7 @@ export default function Pq(): React.JSX.Element {
73
73
  await Auth.register(username, password);
74
74
  setNote({
75
75
  kind: 'ok',
76
- text: 'registered: the server stored only your public key and a proof-of-possession. Now log in to run the ML-KEM-768 mutual-auth step.',
76
+ text: 'registered: the server stored only your public key and a proof-of-possession. Now log in to run the ML-KEM-768 mutual-auth step.'
77
77
  });
78
78
  } catch (e) {
79
79
  setNote({ kind: 'err', text: e instanceof Error ? e.message : String(e) });
@@ -91,7 +91,7 @@ export default function Pq(): React.JSX.Element {
91
91
  await Auth.login(username, password);
92
92
  setNote({
93
93
  kind: 'ok',
94
- text: 'logged in: ML-KEM-768 mutual auth verified (the server proved it holds the KEM secret key).',
94
+ text: 'logged in: ML-KEM-768 mutual auth verified (the server proved it holds the KEM secret key).'
95
95
  });
96
96
  refreshCompanion();
97
97
  } catch (e) {
@@ -151,11 +151,7 @@ export default function Pq(): React.JSX.Element {
151
151
  </label>
152
152
  <label>
153
153
  Password
154
- <input
155
- value={password}
156
- onChange={(e) => setPassword(e.target.value)}
157
- style={{ width: '100%' }}
158
- />
154
+ <input value={password} onChange={(e) => setPassword(e.target.value)} style={{ width: '100%' }} />
159
155
  </label>
160
156
  <div style={{ display: 'flex', gap: 8 }}>
161
157
  <button onClick={doRegister} disabled={busy}>
@@ -167,8 +163,8 @@ export default function Pq(): React.JSX.Element {
167
163
  </div>
168
164
  <p style={{ fontSize: '0.8rem', opacity: 0.7, margin: 0 }}>
169
165
  Demo: pre-filled <code>ada</code> / <code>correct horse battery staple</code>. Register once, then
170
- log in. A wrong password fails at login (the derived key won&apos;t match the stored one). Storage is
171
- the DEV-only in-process KV (<code>src/devserver/kv.ts</code>); a real deployment wires an atomic
166
+ log in. A wrong password fails at login (the derived key won&apos;t match the stored one). Storage
167
+ is the DEV-only in-process KV (<code>src/devserver/kv.ts</code>); a real deployment wires an atomic
172
168
  store, <code>server/routes/Auth.ts</code>.
173
169
  </p>
174
170
  </div>
@@ -188,9 +184,13 @@ export default function Pq(): React.JSX.Element {
188
184
  <div style={{ fontSize: '0.8em', opacity: 0.7 }}>readable companion, untrusted</div>
189
185
  <pre>
190
186
  {JSON.stringify(
191
- { username: companion.username, admin: companion.admin, score: String(companion.score) },
187
+ {
188
+ username: companion.username,
189
+ admin: companion.admin,
190
+ score: String(companion.score)
191
+ },
192
192
  null,
193
- 2,
193
+ 2
194
194
  )}
195
195
  </pre>
196
196
  </div>
@@ -220,8 +220,8 @@ export default function Pq(): React.JSX.Element {
220
220
  )}
221
221
 
222
222
  <p style={{ marginTop: 24, opacity: 0.7, fontSize: '0.9rem' }}>
223
- This is the full augmented-PAKE chain: OPRF keyed salt + ML-DSA client auth + ML-KEM mutual auth, with an
224
- atomic single-use challenge consume. The OPRF layer is classical ristretto255 (the one non-PQ piece);
223
+ This is the full augmented-PAKE chain: OPRF keyed salt + ML-DSA client auth + ML-KEM mutual auth, with
224
+ an atomic single-use challenge consume. The OPRF layer is classical ristretto255 (the one non-PQ piece);
225
225
  auth and key agreement are post-quantum. Plain sessions are on the{' '}
226
226
  <Toil.Link href="/auth">Auth</Toil.Link> page.
227
227
  </p>
@@ -6,12 +6,20 @@
6
6
  // `Response` to inspect (status, `.json()`, ...). Needs the server running to respond.
7
7
  import { useState } from 'react';
8
8
 
9
- import { NewPlayer, ScoreDelta } from 'shared/server';
9
+ import { NewMessage, NewPlayer, ScoreDelta } from 'shared/server';
10
10
 
11
11
  export default function RestDemo() {
12
12
  const [log, setLog] = useState<string[]>([]);
13
13
  const note = (line: string) => setLog((prev) => [line, ...prev].slice(0, 8));
14
14
 
15
+ // The guestbook is backed by ToilDB (an `events` stream + a `counter`), so unlike
16
+ // the players below its data PERSISTS across requests. Sign a few times, reload the
17
+ // page, and the total is still there.
18
+ const [book, setBook] = useState<{ total: bigint; entries: { author: string; message: string }[] }>({
19
+ total: 0n,
20
+ entries: []
21
+ });
22
+
15
23
  // POST /players -> typed Promise<Player>, body is a @data class. The server returns the
16
24
  // new player, but it is NOT saved (server memory resets per request); this is a preview.
17
25
  const onCreate = async () => {
@@ -67,6 +75,31 @@ export default function RestDemo() {
67
75
  }
68
76
  };
69
77
 
78
+ // POST /guestbook -> typed Promise<GuestbookView>. This one is PERSISTED in ToilDB
79
+ // (events + counter), so the total keeps climbing across requests and reloads.
80
+ const signers = ['Ada', 'Linus', 'Grace', 'Ken'];
81
+ const onSign = async () => {
82
+ try {
83
+ const author = signers[Math.floor(Math.random() * signers.length)];
84
+ const v = await Server.REST.guestbook.sign({ body: new NewMessage(author, 'was here') });
85
+ setBook(v);
86
+ note(`${author} signed -> ${v.total} signatures (persisted in ToilDB)`);
87
+ } catch (err) {
88
+ note(parseError(err));
89
+ }
90
+ };
91
+
92
+ // GET /guestbook -> the running total + the newest entries.
93
+ const onBook = async () => {
94
+ try {
95
+ const v = await Server.REST.guestbook.list();
96
+ setBook(v);
97
+ note(`guestbook: ${v.total} signatures`);
98
+ } catch (err) {
99
+ note(parseError(err));
100
+ }
101
+ };
102
+
70
103
  return (
71
104
  <main>
72
105
  <h1>REST</h1>
@@ -83,6 +116,22 @@ export default function RestDemo() {
83
116
  <li key={i}>{line}</li>
84
117
  ))}
85
118
  </ul>
119
+ <h2>Guestbook (persisted via ToilDB)</h2>
120
+ <p>
121
+ The same <code>Server.REST.*</code> client, but the route stores each signature in a ToilDB{' '}
122
+ <code>events</code> stream and a <code>counter</code>. So unlike the players above, these{' '}
123
+ <strong>persist</strong> across requests and page reloads - locally under <code>toil dev</code> and on
124
+ ScyllaDB at the edge, with the same code.
125
+ </p>
126
+ <button onClick={onSign}>sign the guestbook</button>{' '}
127
+ <button onClick={onBook}>refresh ({String(book.total)} signatures)</button>
128
+ <ul>
129
+ {book.entries.map((e, i) => (
130
+ <li key={i}>
131
+ <strong>{e.author}</strong>: {e.message}
132
+ </li>
133
+ ))}
134
+ </ul>
86
135
  <Toil.Link href="/">Back home</Toil.Link>
87
136
  </main>
88
137
  );
@@ -23,10 +23,9 @@ body {
23
23
  min-height: 100vh;
24
24
  background: var(--bg);
25
25
  color: var(--text);
26
- font-family:
27
- system-ui,
28
- -apple-system,
29
- sans-serif;
26
+ font-family: system-ui,
27
+ -apple-system,
28
+ sans-serif;
30
29
  line-height: 1.6;
31
30
  }
32
31
 
@@ -34,6 +33,7 @@ a {
34
33
  color: var(--accent);
35
34
  text-decoration: none;
36
35
  }
36
+
37
37
  a:hover {
38
38
  color: var(--accent3);
39
39
  }
@@ -99,6 +99,7 @@ a:hover {
99
99
  .nav-links a {
100
100
  color: var(--muted);
101
101
  }
102
+
102
103
  .nav-links a:hover {
103
104
  color: var(--text);
104
105
  }
@@ -117,9 +118,8 @@ a:hover {
117
118
  border-radius: 6px;
118
119
  font-size: 0.9rem;
119
120
  color: var(--muted) !important;
120
- transition:
121
- color 150ms,
122
- background 150ms;
121
+ transition: color 150ms,
122
+ background 150ms;
123
123
  }
124
124
 
125
125
  .nav-center-link:hover {
@@ -182,10 +182,9 @@ a:hover {
182
182
  transform: scale(1.1);
183
183
  filter: blur(18px) saturate(1.4);
184
184
  opacity: 0.65;
185
- transition:
186
- opacity 300ms,
187
- filter 300ms,
188
- transform 300ms;
185
+ transition: opacity 300ms,
186
+ filter 300ms,
187
+ transform 300ms;
189
188
  }
190
189
 
191
190
  .hero-logo:hover .hero-logo-glow {
@@ -248,9 +247,8 @@ a:hover {
248
247
  border-radius: 999px;
249
248
  font-size: 0.88rem;
250
249
  color: var(--muted);
251
- transition:
252
- border-color 200ms,
253
- color 200ms;
250
+ transition: border-color 200ms,
251
+ color 200ms;
254
252
  }
255
253
 
256
254
  .feature-badge svg {
@@ -289,10 +287,9 @@ a:hover {
289
287
  font-weight: 600;
290
288
  font-family: inherit;
291
289
  cursor: pointer;
292
- transition:
293
- filter 200ms,
294
- background 200ms,
295
- border-color 200ms;
290
+ transition: filter 200ms,
291
+ background 200ms,
292
+ border-color 200ms;
296
293
  text-decoration: none !important;
297
294
  }
298
295
 
@@ -422,10 +419,9 @@ code {
422
419
  display: flex;
423
420
  flex-direction: column;
424
421
  gap: 0.6rem;
425
- transition:
426
- border-color 200ms,
427
- transform 200ms,
428
- box-shadow 200ms;
422
+ transition: border-color 200ms,
423
+ transform 200ms,
424
+ box-shadow 200ms;
429
425
  }
430
426
 
431
427
  .gs-card {
@@ -436,6 +432,7 @@ code {
436
432
  .gs-card--accent1 {
437
433
  border-top: 2px solid var(--accent);
438
434
  }
435
+
439
436
  .gs-card--accent1:hover {
440
437
  box-shadow: 0 8px 32px rgba(37, 99, 255, 0.15);
441
438
  }
@@ -443,6 +440,7 @@ code {
443
440
  .gs-card--accent2 {
444
441
  border-top: 2px solid var(--accent2);
445
442
  }
443
+
446
444
  .gs-card--accent2:hover {
447
445
  box-shadow: 0 8px 32px rgba(124, 58, 237, 0.15);
448
446
  }
@@ -450,6 +448,7 @@ code {
450
448
  .gs-card--accent3 {
451
449
  border-top: 2px solid var(--accent3);
452
450
  }
451
+
453
452
  .gs-card--accent3:hover {
454
453
  box-shadow: 0 8px 32px rgba(34, 227, 171, 0.12);
455
454
  }
@@ -457,6 +456,7 @@ code {
457
456
  .gs-card--accent4 {
458
457
  border-top: 2px solid #f59e0b;
459
458
  }
459
+
460
460
  .gs-card--accent4:hover {
461
461
  box-shadow: 0 8px 32px rgba(245, 158, 11, 0.12);
462
462
  }
@@ -464,6 +464,7 @@ code {
464
464
  .gs-card--flat {
465
465
  border-top: 2px solid var(--accent);
466
466
  }
467
+
467
468
  .gs-card--flat:hover {
468
469
  border-color: var(--accent);
469
470
  box-shadow: 0 8px 32px rgba(37, 99, 255, 0.12);
@@ -485,10 +486,12 @@ code {
485
486
  background: rgba(124, 58, 237, 0.1);
486
487
  color: var(--accent2);
487
488
  }
489
+
488
490
  .gs-card--accent3 .gs-card-icon {
489
491
  background: rgba(34, 227, 171, 0.1);
490
492
  color: var(--accent3);
491
493
  }
494
+
492
495
  .gs-card--accent4 .gs-card-icon {
493
496
  background: rgba(245, 158, 11, 0.1);
494
497
  color: #f59e0b;
@@ -1,4 +1,4 @@
1
- import { routes, layout, notFound, globalError, slots } from 'toiljs/routes';
1
+ import { globalError, layout, notFound, routes, slots } from 'toiljs/routes';
2
2
 
3
3
  import './styles/main.css';
4
4
 
@@ -2,14 +2,14 @@
2
2
 
3
3
  Your ToilScript backend, compiled to a single WebAssembly module. One folder per concern:
4
4
 
5
- | Folder | What lives here |
6
- | --- | --- |
7
- | `main.ts` | The entry point: wires the handler and imports the surface modules. |
8
- | `core/` | The request handler and shared app logic (state, helpers). |
9
- | `models/` | `@data` classes, the typed wire model shared by HTTP and RPC. One type per file. |
10
- | `routes/` | `@rest` controllers (HTTP). One controller per file, named after its class. |
11
- | `services/` | `@service` classes and free `@remote` functions (typed RPC). |
12
- | `scheduled/` | Reserved for scheduled tasks (see its README). |
5
+ | Folder | What lives here |
6
+ |--------------|----------------------------------------------------------------------------------|
7
+ | `main.ts` | The entry point: wires the handler and imports the surface modules. |
8
+ | `core/` | The request handler and shared app logic (state, helpers). |
9
+ | `models/` | `@data` classes, the typed wire model shared by HTTP and RPC. One type per file. |
10
+ | `routes/` | `@rest` controllers (HTTP). One controller per file, named after its class. |
11
+ | `services/` | `@service` classes and free `@remote` functions (typed RPC). |
12
+ | `scheduled/` | Reserved for scheduled tasks (see its README). |
13
13
 
14
14
  Conventions:
15
15
 
@@ -104,7 +104,7 @@ export class AppHandler extends ToilHandler {
104
104
  .partitioned()
105
105
  .priority('Medium')
106
106
  .extension('CustomFlag')
107
- .serialize(),
107
+ .serialize()
108
108
  );
109
109
 
110
110
  let json = '{';
@@ -127,10 +127,10 @@ export class AppHandler extends ToilHandler {
127
127
 
128
128
  const visits = Cookie.create('visits', next).path('/').sameSite(SameSite.Lax).maxAge(86400);
129
129
  const session = SecureCookies.signed(this.demoKey()).seal(
130
- Cookie.create('session', 'user-42').httpOnly().sameSite(SameSite.Strict).asHostPrefixed(),
130
+ Cookie.create('session', 'user-42').httpOnly().sameSite(SameSite.Strict).asHostPrefixed()
131
131
  );
132
132
  const secret = SecureCookies.encrypted(this.demoKey()).seal(
133
- Cookie.create('secret', 'top-secret-value').httpOnly().path('/'),
133
+ Cookie.create('secret', 'top-secret-value').httpOnly().path('/')
134
134
  );
135
135
 
136
136
  const json =
@@ -189,10 +189,7 @@ export class AppHandler extends ToilHandler {
189
189
  '","' +
190
190
  this.esc(this.clearString('secret')) +
191
191
  '"]}';
192
- return Response.json(json)
193
- .clearCookie('visits')
194
- .clearCookie('__Host-session')
195
- .clearCookie('secret');
192
+ return Response.json(json).clearCookie('visits').clearCookie('__Host-session').clearCookie('secret');
196
193
  }
197
194
 
198
195
  // SEAL: sign and encrypt a value (from `?v=`), then recover both and show
@@ -9,6 +9,7 @@ import { AppHandler } from './core/AppHandler';
9
9
  import './routes/Auth';
10
10
  import './routes/Players';
11
11
  import './routes/Leaderboard';
12
+ import './routes/Guestbook';
12
13
  import './routes/Session';
13
14
  import './routes/EnvDemo';
14
15
  import './services/Stats';
@@ -0,0 +1,12 @@
1
+ /** One signed guestbook entry, stored as a ToilDB `events` record. */
2
+ @data
3
+ export class GuestEntry {
4
+ author: string = '';
5
+ message: string = '';
6
+ at: u64 = 0;
7
+ constructor(author: string = '', message: string = '', at: u64 = 0) {
8
+ this.author = author;
9
+ this.message = message;
10
+ this.at = at;
11
+ }
12
+ }
@@ -0,0 +1,10 @@
1
+ import { GuestEntry } from './GuestEntry';
2
+
3
+ /** The guestbook snapshot returned by the routes: the running signature count
4
+ * plus the most-recent entries (newest first). A `@data` wrapper so the
5
+ * `GuestEntry[]` round-trips through the codec. */
6
+ @data
7
+ export class GuestbookView {
8
+ total: i64 = 0;
9
+ entries: GuestEntry[] = [];
10
+ }
@@ -0,0 +1,6 @@
1
+ /** Request body for `POST /guestbook` - a new signature to append. */
2
+ @data
3
+ export class NewMessage {
4
+ author: string = '';
5
+ message: string = '';
6
+ }