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
@@ -1,5 +1,6 @@
1
1
  import { Response, RouteContext } from 'toiljs/server/runtime';
2
2
  import { DataReader, DataWriter } from 'data';
3
+ import { Record } from 'toildb';
3
4
 
4
5
  import { encodeSessionUser } from './Session';
5
6
 
@@ -15,11 +16,11 @@ import { encodeSessionUser } from './Session';
15
16
  * auth). See `server/globals/auth.ts` (the `AuthService` global) and the client
16
17
  * half in `toiljs/client` (`Auth.register` / `Auth.login`).
17
18
  *
18
- * STORAGE: backed by the DEV-ONLY `kv.*` host imports (see
19
- * `src/devserver/kv.ts`) so the register -> login chain spans requests under
20
- * `toiljs dev`. REMOVE LATER: this is a stand-in; once toildb is implemented,
21
- * the Accounts/Challenges stores move onto it and this goes away. `kv.*` is not
22
- * on the production edge.
19
+ * STORAGE: backed by ToilDB (`@database AuthDb`). Accounts are a `record`
20
+ * collection keyed by username; login challenges are a `record` consumed exactly
21
+ * once with `getDelete` (atomic fetch-and-delete). The dev server emulates these
22
+ * `env.data.*` host imports in process (so register -> login spans requests under
23
+ * `toiljs dev`); the production edge backs the SAME API with ScyllaDB.
23
24
  *
24
25
  * Wire: every body/response is binary (`DataWriter`/`DataReader`), never JSON.
25
26
  */
@@ -36,19 +37,6 @@ const DEMO_PAR: u32 = 1;
36
37
  const CHALLENGE_TTL_SECS: u64 = 120;
37
38
  const SESSION_TTL_SECS: u64 = 3600;
38
39
 
39
- function utf8(s: string): Uint8Array {
40
- return Uint8Array.wrap(String.UTF8.encode(s));
41
- }
42
-
43
- function toHex(b: Uint8Array): string {
44
- let s = '';
45
- for (let i = 0; i < b.length; i++) {
46
- const v = b[i];
47
- s += (v < 16 ? '0' : '') + (<u32>v).toString(16);
48
- }
49
- return s;
50
- }
51
-
52
40
  function randomBytes(n: i32): Uint8Array {
53
41
  const b = new Uint8Array(n);
54
42
  crypto.getRandomValues(b);
@@ -76,45 +64,28 @@ function deriveSalt(username: string): Uint8Array {
76
64
  return crypto.sha256Text('toil-demo-salt-v1:' + username).slice(0, 16);
77
65
  }
78
66
 
67
+ // ToilDB collections (the `kv.*` dev placeholder is gone). The key + value are
68
+ // `@data` types: the binary codec is generated, the host marshals it, and the
69
+ // challenge is consumed exactly once with `getDelete`.
79
70
 
80
- // @ts-ignore: decorator
81
- @external('env', 'kv.put')
82
- declare function __kvPut(keyPtr: usize, keyLen: i32, valPtr: usize, valLen: i32): void;
83
- // @ts-ignore: decorator
84
- @external('env', 'kv.get')
85
- declare function __kvGet(keyPtr: usize, keyLen: i32, outPtr: usize, outCap: i32): i32;
86
- // @ts-ignore: decorator
87
- @external('env', 'kv.getdel')
88
- declare function __kvGetDel(keyPtr: usize, keyLen: i32, outPtr: usize, outCap: i32): i32;
89
-
90
- const KV_CAP: i32 = 8192; // bounds account (~1.5 KiB) + challenge (~100 B) records
91
-
92
- function kvPut(key: Uint8Array, val: Uint8Array): void {
93
- __kvPut(key.dataStart, key.length, val.dataStart, val.length);
94
- }
95
- function kvGet(key: Uint8Array): Uint8Array | null {
96
- const out = new Uint8Array(KV_CAP);
97
- const n = __kvGet(key.dataStart, key.length, out.dataStart, KV_CAP);
98
- if (n < 0) return null;
99
- return out.slice(0, n);
100
- }
101
- /** Atomic fetch-and-delete: consumes a login challenge exactly once. */
102
- function kvGetDel(key: Uint8Array): Uint8Array | null {
103
- const out = new Uint8Array(KV_CAP);
104
- const n = __kvGetDel(key.dataStart, key.length, out.dataStart, KV_CAP);
105
- if (n < 0) return null;
106
- return out.slice(0, n);
71
+ @data
72
+ class Username {
73
+ name: string = '';
74
+ constructor(name: string = '') {
75
+ this.name = name;
76
+ }
107
77
  }
108
78
 
109
- function acctKey(username: string): Uint8Array {
110
- return utf8('acct:' + username);
111
- }
112
- function chalKey(cid: Uint8Array): Uint8Array {
113
- return utf8('chal:' + toHex(cid));
79
+ @data
80
+ class ChallengeId {
81
+ cid: Uint8Array = new Uint8Array(0);
82
+ constructor(cid: Uint8Array = new Uint8Array(0)) {
83
+ this.cid = cid;
84
+ }
114
85
  }
115
86
 
116
-
117
- class Account {
87
+ @data
88
+ class AuthAccount {
118
89
  username: string = '';
119
90
  salt: Uint8Array = new Uint8Array(0);
120
91
  publicKey: Uint8Array = new Uint8Array(0);
@@ -123,30 +94,7 @@ class Account {
123
94
  parallelism: u32 = 0;
124
95
  }
125
96
 
126
- function putAccount(a: Account): void {
127
- const w = new DataWriter();
128
- w.writeString(a.username);
129
- w.writeBytes(a.salt);
130
- w.writeBytes(a.publicKey);
131
- w.writeU32(a.memKiB);
132
- w.writeU32(a.iterations);
133
- w.writeU32(a.parallelism);
134
- kvPut(acctKey(a.username), w.toBytes());
135
- }
136
- function getAccount(username: string): Account | null {
137
- const raw = kvGet(acctKey(username));
138
- if (raw == null) return null;
139
- const r = new DataReader(raw);
140
- const a = new Account();
141
- a.username = r.readString();
142
- a.salt = r.readBytes();
143
- a.publicKey = r.readBytes();
144
- a.memKiB = r.readU32();
145
- a.iterations = r.readU32();
146
- a.parallelism = r.readU32();
147
- return r.ok ? a : null;
148
- }
149
-
97
+ @data
150
98
  class Challenge {
151
99
  cid: Uint8Array = new Uint8Array(0);
152
100
  username: string = '';
@@ -155,26 +103,10 @@ class Challenge {
155
103
  exp: u64 = 0;
156
104
  }
157
105
 
158
- function putChallenge(c: Challenge): void {
159
- const w = new DataWriter();
160
- w.writeBytes(c.cid);
161
- w.writeString(c.username);
162
- w.writeBytes(c.nonce);
163
- w.writeU64(c.iat);
164
- w.writeU64(c.exp);
165
- kvPut(chalKey(c.cid), w.toBytes());
166
- }
167
- function consumeChallenge(cid: Uint8Array): Challenge | null {
168
- const raw = kvGetDel(chalKey(cid));
169
- if (raw == null) return null;
170
- const r = new DataReader(raw);
171
- const c = new Challenge();
172
- c.cid = r.readBytes();
173
- c.username = r.readString();
174
- c.nonce = r.readBytes();
175
- c.iat = r.readU64();
176
- c.exp = r.readU64();
177
- return r.ok ? c : null;
106
+ @database
107
+ class AuthDb {
108
+ @collection accounts!: Record<AuthAccount, Username>;
109
+ @collection challenges!: Record<Challenge, ChallengeId>;
178
110
  }
179
111
 
180
112
  @rest('auth')
@@ -214,7 +146,7 @@ class Auth {
214
146
  if (pk.length != AuthService.PUBLIC_KEY_LEN) return fail();
215
147
  // Already registered: a distinguishable status (not the generic 401) so the
216
148
  // client can say "username taken, log in instead" rather than a blank error.
217
- if (getAccount(username) != null) {
149
+ if (AuthDb.accounts.exists(new Username(username))) {
218
150
  return Response.bytes(new DataWriter().writeU8(1).toBytes());
219
151
  }
220
152
 
@@ -223,14 +155,17 @@ class Auth {
223
155
  const regMsg = AuthService.buildRegisterMessage(username, pk);
224
156
  if (!AuthService.verifyRegister(pk, regMsg, proof)) return fail();
225
157
 
226
- const a = new Account();
158
+ const a = new AuthAccount();
227
159
  a.username = username;
228
160
  a.salt = deriveSalt(username);
229
161
  a.publicKey = pk;
230
162
  a.memKiB = DEMO_MEM_KIB;
231
163
  a.iterations = DEMO_ITERS;
232
164
  a.parallelism = DEMO_PAR;
233
- putAccount(a);
165
+ // create-if-absent: a racing duplicate registration loses here, not above.
166
+ if (!AuthDb.accounts.create(new Username(username), a)) {
167
+ return Response.bytes(new DataWriter().writeU8(1).toBytes());
168
+ }
234
169
  return Response.bytes(new DataWriter().writeU8(0).toBytes());
235
170
  }
236
171
 
@@ -249,7 +184,7 @@ class Auth {
249
184
  const evaluated = AuthService.oprfEvaluate(username, blinded);
250
185
  if (evaluated.length != AuthService.OPRF_ELEMENT_LEN) return fail();
251
186
 
252
- const acct = getAccount(username);
187
+ const known = AuthDb.accounts.exists(new Username(username));
253
188
  const cid = randomBytes(16);
254
189
  const nonce = randomBytes(32);
255
190
  const iat = nowSecs();
@@ -257,14 +192,14 @@ class Auth {
257
192
 
258
193
  // Persist only for a real account; the response is identical either way,
259
194
  // and login/finish for an unknown user fails generically at consume.
260
- if (acct != null) {
195
+ if (known) {
261
196
  const c = new Challenge();
262
197
  c.cid = cid;
263
198
  c.username = username;
264
199
  c.nonce = nonce;
265
200
  c.iat = iat;
266
201
  c.exp = exp;
267
- putChallenge(c);
202
+ AuthDb.challenges.create(new ChallengeId(cid), c);
268
203
  }
269
204
 
270
205
  const w = new DataWriter();
@@ -292,17 +227,26 @@ class Auth {
292
227
  if (!r.ok) return fail();
293
228
 
294
229
  // 1. CONSUME FIRST: atomic fetch-and-delete. Unknown/used/expired => fail.
295
- const ch = consumeChallenge(cid);
230
+ const ch = AuthDb.challenges.getDelete(new ChallengeId(cid));
296
231
  if (ch == null) return fail();
297
232
  if (nowSecs() >= ch.exp) return fail();
298
233
 
299
234
  // 2. Rebuild the message from OUR stored values + the client's ct (and
300
235
  // the bound params + server key id), load the account key, verify.
301
- const acct = getAccount(ch.username);
236
+ const acct = AuthDb.accounts.get(new Username(ch.username));
302
237
  if (acct == null) return fail();
303
238
  const message = AuthService.buildLoginMessage(
304
- ch.username, AUD, cid, ch.nonce, ch.iat, ch.exp,
305
- ct, DEMO_MEM_KIB, DEMO_ITERS, DEMO_PAR, AuthService.serverKemKeyId(),
239
+ ch.username,
240
+ AUD,
241
+ cid,
242
+ ch.nonce,
243
+ ch.iat,
244
+ ch.exp,
245
+ ct,
246
+ DEMO_MEM_KIB,
247
+ DEMO_ITERS,
248
+ DEMO_PAR,
249
+ AuthService.serverKemKeyId()
306
250
  );
307
251
  if (!AuthService.verifyLogin(acct.publicKey, message, sig)) return fail();
308
252
 
@@ -34,9 +34,15 @@ class EnvDemo {
34
34
  const apiKeySet = Environment.getSecure('DEMO_API_KEY') != null;
35
35
 
36
36
  const body =
37
- 'PUBLIC_GREETING=' + greeting + '\n' +
38
- 'REGION=' + region + '\n' +
39
- 'DEMO_API_KEY set=' + (apiKeySet ? 'yes' : 'no') + '\n';
37
+ 'PUBLIC_GREETING=' +
38
+ greeting +
39
+ '\n' +
40
+ 'REGION=' +
41
+ region +
42
+ '\n' +
43
+ 'DEMO_API_KEY set=' +
44
+ (apiKeySet ? 'yes' : 'no') +
45
+ '\n';
40
46
  return Response.text(body, 200);
41
47
  }
42
48
  }
@@ -0,0 +1,62 @@
1
+ import { Counter, Events } from 'toildb';
2
+
3
+ import { GuestEntry } from '../models/GuestEntry';
4
+ import { GuestbookView } from '../models/GuestbookView';
5
+ import { NewMessage } from '../models/NewMessage';
6
+
7
+ /**
8
+ * A PERSISTENT guestbook, mounted at `/guestbook`, backed by ToilDB.
9
+ *
10
+ * The contrast with `Players` (whose comment notes "memory resets next request")
11
+ * is the whole point: every signature is appended to an `events` stream and
12
+ * tallied in a `counter`, so the data SURVIVES across requests under `toiljs dev`
13
+ * (the in-process ToilDB emulator) and runs on ScyllaDB at the edge - same code,
14
+ * no connection string. On the client:
15
+ *
16
+ * await Server.REST.guestbook.sign({ body: new NewMessage('Ada', 'hi!') });
17
+ * const book = await Server.REST.guestbook.list(); // { total, entries: [...] }
18
+ */
19
+
20
+ // The guestbook is one global stream; a single fixed key addresses it.
21
+ @data
22
+ class GuestKey {
23
+ room: string = 'main';
24
+ constructor(room: string = 'main') {
25
+ this.room = room;
26
+ }
27
+ }
28
+
29
+ @database
30
+ class GuestbookDb {
31
+ @collection entries!: Events<GuestEntry, GuestKey>;
32
+ @collection totals!: Counter<GuestKey>;
33
+ }
34
+
35
+ /** The current total + the 10 newest entries. */
36
+ function snapshot(): GuestbookView {
37
+ const key = new GuestKey('main');
38
+ const view = new GuestbookView();
39
+ view.total = GuestbookDb.totals.get(key);
40
+ view.entries = GuestbookDb.entries.latest(key, 10);
41
+ return view;
42
+ }
43
+
44
+ @rest('guestbook')
45
+ class Guestbook {
46
+ /** `GET /guestbook` - the running total + the most recent signatures. */
47
+ @get('/')
48
+ public list(): GuestbookView {
49
+ return snapshot();
50
+ }
51
+
52
+ /** `POST /guestbook` - append a signature (PERSISTED) and return the
53
+ * updated book. Sign twice and the total keeps climbing across requests. */
54
+ @post('/')
55
+ public sign(input: NewMessage): GuestbookView {
56
+ const key = new GuestKey('main');
57
+ const at = <u64>(Date.now() / 1000);
58
+ GuestbookDb.entries.append(key, new GuestEntry(input.author, input.message, at));
59
+ GuestbookDb.totals.add(key, 1);
60
+ return snapshot();
61
+ }
62
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "toiljs",
3
3
  "type": "module",
4
- "version": "0.0.54",
4
+ "version": "0.0.56",
5
5
  "author": "Dacely",
6
6
  "description": "The modern React framework: a file-based React frontend and a ToilScript-compiled WebAssembly backend.",
7
7
  "repository": {
@@ -131,7 +131,7 @@
131
131
  "nodemailer": "^9.0.0",
132
132
  "picocolors": "^1.1.1",
133
133
  "sharp": "^0.35.0",
134
- "toilscript": "^0.1.27",
134
+ "toilscript": "^0.1.28",
135
135
  "typescript-eslint": "^8.60.0",
136
136
  "vite": "^8.0.14",
137
137
  "vite-imagetools": "^10.0.0",
@@ -10,7 +10,7 @@
10
10
  // and the toiljs dev-server mock).
11
11
 
12
12
  import { DataWriter, DataReader } from 'data';
13
- import { HmacImportParams, HmacParams, ALG_SHA_256, USAGE_SIGN, USAGE_VERIFY } from 'crypto';
13
+ import { HmacImportParams, HmacParams, ALG_SHA_256, FMT_RAW, USAGE_SIGN, USAGE_VERIFY } from 'crypto';
14
14
 
15
15
  import {
16
16
  Server,
@@ -160,7 +160,7 @@ function __resolveServerKemPk(): Uint8Array {
160
160
  // both keyed PRFs over the transcript; the client mirrors this with hash-wasm.
161
161
  function __hmacSha256(key: Uint8Array, msg: Uint8Array): Uint8Array {
162
162
  const k = crypto.subtle.importKey(
163
- 'raw',
163
+ FMT_RAW,
164
164
  key,
165
165
  new HmacImportParams(ALG_SHA_256),
166
166
  false,
@@ -511,7 +511,7 @@ export namespace AuthService {
511
511
 
512
512
  /** SHA-256 over `data` (ambient Web Crypto), for transcript/confirm hashing. */
513
513
  export function sha256(data: Uint8Array): Uint8Array {
514
- return crypto.subtle.digest('SHA-256', data);
514
+ return crypto.subtle.digest(ALG_SHA_256, data);
515
515
  }
516
516
 
517
517
  /** `SHA-256(serverKemPublicKey)` -- the key identity bound into the login
@@ -24,6 +24,7 @@ import {
24
24
  HmacImportParams,
25
25
  HmacParams,
26
26
  ALG_SHA_256,
27
+ FMT_RAW,
27
28
  USAGE_SIGN,
28
29
  USAGE_VERIFY,
29
30
  } from 'crypto';
@@ -45,7 +46,7 @@ const TWOFA_VERSION: u8 = 1;
45
46
 
46
47
  function importHmac(key: Uint8Array): CryptoKey {
47
48
  return crypto.subtle.importKey(
48
- 'raw',
49
+ FMT_RAW,
49
50
  key,
50
51
  new HmacImportParams(ALG_SHA_256),
51
52
  false,
@@ -30,6 +30,7 @@ import {
30
30
  HmacParams,
31
31
  ALG_AES_GCM,
32
32
  ALG_SHA_256,
33
+ FMT_RAW,
33
34
  USAGE_SIGN,
34
35
  USAGE_VERIFY,
35
36
  USAGE_ENCRYPT,
@@ -104,7 +105,7 @@ export class SecureCookies {
104
105
 
105
106
  private importHmac(key: Uint8Array): CryptoKey {
106
107
  return crypto.subtle.importKey(
107
- 'raw',
108
+ FMT_RAW,
108
109
  key,
109
110
  new HmacImportParams(ALG_SHA_256),
110
111
  false,
@@ -114,7 +115,7 @@ export class SecureCookies {
114
115
 
115
116
  private importAes(key: Uint8Array): CryptoKey {
116
117
  return crypto.subtle.importKey(
117
- 'raw',
118
+ FMT_RAW,
118
119
  key,
119
120
  new AesKeyParams(),
120
121
  false,
@@ -10,10 +10,10 @@ import fs from 'node:fs';
10
10
  import path from 'node:path';
11
11
 
12
12
  import {
13
- Server,
14
13
  type MiddlewareNext,
15
14
  type Request,
16
15
  type Response,
16
+ Server,
17
17
  type Websocket,
18
18
  } from '@dacely/hyper-express';
19
19
 
@@ -172,7 +172,9 @@ export async function startBackend(options: BackendOptions): Promise<RunningBack
172
172
  // default upgrade handler (hyper-express links it to the companion ws route). Same-origin and
173
173
  // non-browser clients pass; others get 403.
174
174
  app.upgrade(wsPath, (request: Request, response: Response) => {
175
- if (!isWsOriginAllowed(request.headers.origin, request.headers.host, options.allowedOrigins)) {
175
+ if (
176
+ !isWsOriginAllowed(request.headers.origin, request.headers.host, options.allowedOrigins)
177
+ ) {
176
178
  response.status(403).send();
177
179
  return;
178
180
  }
package/src/cli/doctor.ts CHANGED
@@ -10,7 +10,12 @@ import { createRequire } from 'node:module';
10
10
  import path from 'node:path';
11
11
  import { fileURLToPath } from 'node:url';
12
12
 
13
- import { loadConfig, type ResolvedToilConfig, scanRoutes, TOIL_SERVER_ENV_DTS } from 'toiljs/compiler';
13
+ import {
14
+ loadConfig,
15
+ type ResolvedToilConfig,
16
+ scanRoutes,
17
+ TOIL_SERVER_ENV_DTS,
18
+ } from 'toiljs/compiler';
14
19
 
15
20
  import {
16
21
  type Check,
@@ -41,8 +46,8 @@ import {
41
46
  findRelativeAssets,
42
47
  hasFailures,
43
48
  type RestFacts,
44
- type RpcFacts,
45
49
  RPC_TOILSCRIPT_MIN,
50
+ type RpcFacts,
46
51
  satisfiesMin,
47
52
  type SourceFile,
48
53
  summarize,
@@ -353,7 +358,9 @@ function applyRpcFix(root: string): RpcFixResult {
353
358
  const serverToilconfig = readJsonObject(path.join(root, 'toilconfig.json'));
354
359
  if (serverToilconfig !== null) {
355
360
  const entries = Array.isArray(serverToilconfig.entries)
356
- ? (serverToilconfig.entries as unknown[]).filter((e): e is string => typeof e === 'string')
361
+ ? (serverToilconfig.entries as unknown[]).filter(
362
+ (e): e is string => typeof e === 'string',
363
+ )
357
364
  : [];
358
365
  const dirs = new Set<string>();
359
366
  for (const e of entries) dirs.add(path.dirname(path.resolve(root, e)));
package/src/cli/notify.ts CHANGED
@@ -13,12 +13,7 @@ import { fileURLToPath } from 'node:url';
13
13
 
14
14
  import { detectPackageManager } from './update.js';
15
15
  import { accent, bold, box, dim, version as cliVersion, warn } from './ui.js';
16
- import {
17
- findOutdated,
18
- isCacheFresh,
19
- type OutdatedRow,
20
- parseCheckCache,
21
- } from './version-check.js';
16
+ import { findOutdated, isCacheFresh, type OutdatedRow, parseCheckCache } from './version-check.js';
22
17
 
23
18
  const REGISTRY_URL = 'https://registry.npmjs.org/toiljs/latest';
24
19
  const FETCH_TIMEOUT_MS = 2000;
package/src/cli/ui.ts CHANGED
@@ -118,9 +118,7 @@ function visibleWidth(s: string): number {
118
118
  export function box(lines: readonly string[], paint: (s: string) => string = (s) => s): string {
119
119
  const width = lines.reduce((w, l) => Math.max(w, visibleWidth(l)), 0);
120
120
  const side = paint('│');
121
- const body = lines.map(
122
- (l) => ` ${side} ${l}${' '.repeat(width - visibleWidth(l))} ${side}`,
123
- );
121
+ const body = lines.map((l) => ` ${side} ${l}${' '.repeat(width - visibleWidth(l))} ${side}`);
124
122
  return [
125
123
  ' ' + paint(`╭${'─'.repeat(width + 4)}╮`),
126
124
  ...body,
@@ -167,3 +165,5 @@ export function banner(): void {
167
165
  const ver = `${dim(' v')}${brand(version())}`;
168
166
  process.stdout.write('\n' + lines.join('\n') + '\n\n ' + tagline() + ' ' + ver + '\n\n');
169
167
  }
168
+
169
+
@@ -29,7 +29,11 @@ export function parseCheckCache(raw: string): CheckCache | null {
29
29
  }
30
30
 
31
31
  /** True when the cached answer is still trustworthy (also stale if the clock went backwards). */
32
- export function isCacheFresh(cache: CheckCache, now: number, ttlMs: number = CHECK_TTL_MS): boolean {
32
+ export function isCacheFresh(
33
+ cache: CheckCache,
34
+ now: number,
35
+ ttlMs: number = CHECK_TTL_MS,
36
+ ): boolean {
33
37
  return cache.checkedAt <= now && now - cache.checkedAt < ttlMs;
34
38
  }
35
39
 
@@ -11,7 +11,7 @@
11
11
  * JSON, byte-identical to the server's `AuthService.buildLoginMessage`.
12
12
  */
13
13
 
14
- import { argon2id, createSHA256, createHMAC } from 'hash-wasm';
14
+ import { argon2id, createHMAC, createSHA256 } from 'hash-wasm';
15
15
  import { ml_dsa44 } from '@dacely/noble-post-quantum/ml-dsa.js';
16
16
  import { ml_kem768 } from '@dacely/noble-post-quantum/ml-kem.js';
17
17
  import { ristretto255_oprf } from '@noble/curves/ed25519.js';
@@ -45,7 +45,9 @@ function fromHex(hex: string): Uint8Array {
45
45
  * decapsulate, so a valid confirmation tag authenticates the server. This is
46
46
  * the demo dev key; a real deployment pins its own (and rotates it).
47
47
  */
48
- export const SERVER_KEM_PUBLIC_KEY = fromHex('29d765e8083182891302569b3712a856e564fdd484b0706b0c68568d5ab7edc742cf74459d64595455a60f267973aa55e43c5be61925a3822eafcca445e36dc4655636e31e6fc9bec338b253f94290008ef7f40dbddb49c15c690f6755a23a1b3c85cfd5207e71a607086a6fc6d74a05080f43276901a19cafdb8de7771d58ea07f0f1056b905127b22223d08e75173199f13ab13c5dcd3b51ac784f84e520484a262b845a897c41cf27324ab6ba545c78c9ccab361051e0bba53498af26240fa0d566d1572684f4b42e253e6d052c848650915063c35641e1121ef8d9cfd17b667b351103c56d195007c9376d0c08aa268396814490eab4c364175a94533267a1933862cc4c33bcf0a13d1fa2b9d6c5082eeca1480672f2526cbe013beff14dc908a386e0b633c8761023cbed760deac6709bc328d865ac82e12307b673d96711dbb27a4d939230d25b53d594169a318be0200fa33550e9418e2a3b30e9719edc09d5fc4306f1abfd021eab14637a8a72c5931d25dc9b56db0e6ab677522b10f25307dbb804a6774ce05b87b0976a4b227bfe6caf20a79e64004fbd27b1eea018b3ab8ffa629f2dc87f19278f95168e94e44660a3370c537795678eb2f056260609769740583b51b291862927a1938737c6a37f40b78f00671cccbcb88ac3427b37915ed58782998f84051647707d48995472baad3f64a7cca54e1c0734db08751c614a34f28b84f2c1b5a6817355ab61957c486b7acffbc092bc8a7b46387f33b53ed372f7168d31a71cd008539928b0cdf91e835aa97f6a2be6d327b87a6ae478701d75a59a25179cb14997bb2552853014724170a1c49b82c2bcebc3279024e1fa44c53c7afdc43f0bd22116490f3b74c90e7296be58b9a91168f2fa0c3d378a3bcac959f357825c9976a8c9ee944f29b45e96d7345d9b478431a20cf1c5d3a3227c717fd204619777636c0cb140db5c50d2a3302334461030bee34e4eb1a6f02b733f9ccda4290fa168bc039568373241542728d00030d1f251e83737cb215adbdc1de75978675a0cd0d75b12748abdda7a9852629c63697d145af2c69854b06e03f37c4b064e4c9a4c03f2ad4d081e70180e9547247921918118086b62b4f7727f46b24e3e79ba3f28209f32b5102035bf935856232f83642268c0292ec6bf8e9462382163d30a20b4bcb7b4439310ec9d0a148193907fc07697342967cf1a16c6b3c71558951fa915400736cf699262b54b723abb2ecc27b74b68ee494287595ef818388adb49e883c67bfa5c226c0eef037a0851a29d34675912c1ea1068310b6dfcd017c809c8fbfc2c3ae78dfef07299960eeefba182662a90fa422c1790f356a2ea909012b15623a9b9e450a282cb530589a68368b3583159d9010ac3e52cc974753c342e58279516339dfb691df94b13a223ad97eb6a09c21dafe6304a3642d6d2067b5238497661fe88ad1227ca3557be2a576b6e17c5a7f997ea07929e76407e376aba74c44cd8504804776f39bbb8327624188a63501e83b404d9438cade0b11dc3ac61856447fb072b91761c228878f01b2eb6b4b21ba664c2c75882431603b25a449ffeb8410b910558581777562aa9b2181fd9c04713ad9326462d3e842121c4997f9aa932417c67851625816de66e0d65637434629f39');
48
+ export const SERVER_KEM_PUBLIC_KEY = fromHex(
49
+ '29d765e8083182891302569b3712a856e564fdd484b0706b0c68568d5ab7edc742cf74459d64595455a60f267973aa55e43c5be61925a3822eafcca445e36dc4655636e31e6fc9bec338b253f94290008ef7f40dbddb49c15c690f6755a23a1b3c85cfd5207e71a607086a6fc6d74a05080f43276901a19cafdb8de7771d58ea07f0f1056b905127b22223d08e75173199f13ab13c5dcd3b51ac784f84e520484a262b845a897c41cf27324ab6ba545c78c9ccab361051e0bba53498af26240fa0d566d1572684f4b42e253e6d052c848650915063c35641e1121ef8d9cfd17b667b351103c56d195007c9376d0c08aa268396814490eab4c364175a94533267a1933862cc4c33bcf0a13d1fa2b9d6c5082eeca1480672f2526cbe013beff14dc908a386e0b633c8761023cbed760deac6709bc328d865ac82e12307b673d96711dbb27a4d939230d25b53d594169a318be0200fa33550e9418e2a3b30e9719edc09d5fc4306f1abfd021eab14637a8a72c5931d25dc9b56db0e6ab677522b10f25307dbb804a6774ce05b87b0976a4b227bfe6caf20a79e64004fbd27b1eea018b3ab8ffa629f2dc87f19278f95168e94e44660a3370c537795678eb2f056260609769740583b51b291862927a1938737c6a37f40b78f00671cccbcb88ac3427b37915ed58782998f84051647707d48995472baad3f64a7cca54e1c0734db08751c614a34f28b84f2c1b5a6817355ab61957c486b7acffbc092bc8a7b46387f33b53ed372f7168d31a71cd008539928b0cdf91e835aa97f6a2be6d327b87a6ae478701d75a59a25179cb14997bb2552853014724170a1c49b82c2bcebc3279024e1fa44c53c7afdc43f0bd22116490f3b74c90e7296be58b9a91168f2fa0c3d378a3bcac959f357825c9976a8c9ee944f29b45e96d7345d9b478431a20cf1c5d3a3227c717fd204619777636c0cb140db5c50d2a3302334461030bee34e4eb1a6f02b733f9ccda4290fa168bc039568373241542728d00030d1f251e83737cb215adbdc1de75978675a0cd0d75b12748abdda7a9852629c63697d145af2c69854b06e03f37c4b064e4c9a4c03f2ad4d081e70180e9547247921918118086b62b4f7727f46b24e3e79ba3f28209f32b5102035bf935856232f83642268c0292ec6bf8e9462382163d30a20b4bcb7b4439310ec9d0a148193907fc07697342967cf1a16c6b3c71558951fa915400736cf699262b54b723abb2ecc27b74b68ee494287595ef818388adb49e883c67bfa5c226c0eef037a0851a29d34675912c1ea1068310b6dfcd017c809c8fbfc2c3ae78dfef07299960eeefba182662a90fa422c1790f356a2ea909012b15623a9b9e450a282cb530589a68368b3583159d9010ac3e52cc974753c342e58279516339dfb691df94b13a223ad97eb6a09c21dafe6304a3642d6d2067b5238497661fe88ad1227ca3557be2a576b6e17c5a7f997ea07929e76407e376aba74c44cd8504804776f39bbb8327624188a63501e83b404d9438cade0b11dc3ac61856447fb072b91761c228878f01b2eb6b4b21ba664c2c75882431603b25a449ffeb8410b910558581777562aa9b2181fd9c04713ad9326462d3e842121c4997f9aa932417c67851625816de66e0d65637434629f39',
50
+ );
49
51
 
50
52
  export const PUBLIC_KEY_LEN = 1312;
51
53
  export const SECRET_KEY_LEN = 2560;
@@ -171,7 +173,6 @@ export function buildRegisterMessage(username: string, publicKey: Uint8Array): U
171
173
  return new DataWriter().writeU8(1).writeString(username).writeBytes(publicKey).toBytes();
172
174
  }
173
175
 
174
-
175
176
  function decodeKdf(r: DataReader): KdfParams {
176
177
  return {
177
178
  memKiB: r.readU32(),
@@ -181,7 +182,6 @@ function decodeKdf(r: DataReader): KdfParams {
181
182
  };
182
183
  }
183
184
 
184
-
185
185
  async function postBinary(baseUrl: string, path: string, body: Uint8Array): Promise<DataReader> {
186
186
  const res = await fetch(baseUrl + path, {
187
187
  method: 'POST',
@@ -204,7 +204,11 @@ export interface AuthOptions {
204
204
  * stretched with Argon2id into an ML-DSA-44 keypair, and ONLY the public key
205
205
  * (plus a proof-of-possession signature) is submitted. Throws on failure.
206
206
  */
207
- export async function register(username: string, password: string, opts: AuthOptions = {}): Promise<void> {
207
+ export async function register(
208
+ username: string,
209
+ password: string,
210
+ opts: AuthOptions = {},
211
+ ): Promise<void> {
208
212
  const baseUrl = opts.baseUrl ?? '/auth';
209
213
  const oprf = ristretto255_oprf.oprf;
210
214
  const pw = utf8(password.normalize('NFKC'));
@@ -268,7 +272,11 @@ export async function register(username: string, password: string, opts: AuthOpt
268
272
  * The secret key, seed, and shared secret are wiped as soon as they are used.
269
273
  * Returns the opaque session token. Throws (one generic message) on any failure.
270
274
  */
271
- export async function login(username: string, password: string, opts: AuthOptions = {}): Promise<Uint8Array> {
275
+ export async function login(
276
+ username: string,
277
+ password: string,
278
+ opts: AuthOptions = {},
279
+ ): Promise<Uint8Array> {
272
280
  const baseUrl = opts.baseUrl ?? '/auth';
273
281
  const oprf = ristretto255_oprf.oprf;
274
282
  const pw = utf8(password.normalize('NFKC'));
@@ -299,8 +307,17 @@ export async function login(username: string, password: string, opts: AuthOption
299
307
  const { cipherText, sharedSecret } = ml_kem768.encapsulate(SERVER_KEM_PUBLIC_KEY);
300
308
  const serverKemKeyId = await sha256Bytes(SERVER_KEM_PUBLIC_KEY);
301
309
  const message = buildLoginMessage(
302
- username, aud, cid, nonce, iat, exp,
303
- cipherText, kdf.memKiB, kdf.iterations, kdf.parallelism, serverKemKeyId,
310
+ username,
311
+ aud,
312
+ cid,
313
+ nonce,
314
+ iat,
315
+ exp,
316
+ cipherText,
317
+ kdf.memKiB,
318
+ kdf.iterations,
319
+ kdf.parallelism,
320
+ serverKemKeyId,
304
321
  );
305
322
  let signature: Uint8Array;
306
323
  try {
@@ -333,9 +350,15 @@ export async function login(username: string, password: string, opts: AuthOption
333
350
  // decapsulated correctly derives the same K, so a valid tag proves its
334
351
  // identity. Verify before returning the session.
335
352
  const transcriptHash = await sha256Bytes(message);
336
- const sessionKey = await hmacSha256(sharedSecret, concatBytes(utf8(SESSION_KEY_LABEL), transcriptHash));
353
+ const sessionKey = await hmacSha256(
354
+ sharedSecret,
355
+ concatBytes(utf8(SESSION_KEY_LABEL), transcriptHash),
356
+ );
337
357
  wipe(sharedSecret);
338
- const expected = await hmacSha256(sessionKey, concatBytes(utf8(SERVER_CONFIRM_LABEL), transcriptHash));
358
+ const expected = await hmacSha256(
359
+ sessionKey,
360
+ concatBytes(utf8(SERVER_CONFIRM_LABEL), transcriptHash),
361
+ );
339
362
  if (!bytesEqual(expected, serverConfirm)) throw new Error('auth: server authentication failed');
340
363
 
341
364
  return session; // session token
@@ -1,6 +1,6 @@
1
- import { useRef, type ReactNode, type SyntheticEvent } from 'react';
1
+ import { type ReactNode, type SyntheticEvent, useRef } from 'react';
2
2
 
3
- import { useAction, type ActionState, type RevalidateTarget } from '../routing/action.js';
3
+ import { type ActionState, type RevalidateTarget, useAction } from '../routing/action.js';
4
4
 
5
5
  /** Props for {@link Form}. */
6
6
  export interface FormProps {
@@ -1,4 +1,4 @@
1
- import { useState, type CSSProperties, type ComponentPropsWithRef, type ReactNode } from 'react';
1
+ import { type ComponentPropsWithRef, type CSSProperties, type ReactNode, useState } from 'react';
2
2
 
3
3
  /**
4
4
  * Props for {@link Image}: every standard `<img>` attribute, plus toil's layout/loading controls.
@@ -1,4 +1,4 @@
1
- import { useEffect, type ReactNode } from 'react';
1
+ import { type ReactNode, useEffect } from 'react';
2
2
 
3
3
  /**
4
4
  * When a {@link Script} is injected, relative to the app becoming interactive:
@@ -1,4 +1,4 @@
1
- import { useContext, type ReactNode } from 'react';
1
+ import { type ReactNode, useContext } from 'react';
2
2
 
3
3
  import { SlotContext } from '../routing/slot-context.js';
4
4