toiljs 0.0.36 → 0.0.38

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.
@@ -10,9 +10,42 @@
10
10
  // swap in its own. It still isn't the full production login (no single-use
11
11
  // consume -> within the TTL a captured proof could be replayed; that needs a
12
12
  // store) -- see Auth.login / server/routes/Auth.ts and docs/auth.md.
13
- import { useCallback, useState } from 'react';
13
+ import { useCallback, useEffect, useState } from 'react';
14
14
 
15
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
+ }
16
49
 
17
50
  export const metadata: Toil.Metadata = {
18
51
  title: 'Post-quantum auth',
@@ -48,27 +81,65 @@ export default function Pq(): React.JSX.Element {
48
81
  const [busy, setBusy] = useState(false);
49
82
  const [proof, setProof] = useState<IdentityProof | null>(null);
50
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]);
51
89
 
52
90
  const prove = useCallback(
53
91
  async (doTamper: boolean) => {
54
92
  setBusy(true);
55
93
  setResult(null);
94
+ setVerified(null);
56
95
  try {
57
96
  const p = await Auth.proveIdentity(username, password);
58
97
  setProof(p);
59
- setResult(await postVerify(doTamper ? tamper(p.envelope) : p.envelope));
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();
60
102
  } catch (e) {
61
103
  setResult({ error: e instanceof Error ? e.message : String(e) });
62
104
  } finally {
63
105
  setBusy(false);
64
106
  }
65
107
  },
66
- [username, password],
108
+ [username, password, refreshCompanion],
67
109
  );
68
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
+
69
140
  return (
70
141
  <main style={{ maxWidth: 680 }}>
71
- <h1>Post-quantum identity</h1>
142
+ <h1>Post-quantum login</h1>
72
143
  <p>
73
144
  The edge issues a fresh, HMAC-signed <strong>challenge</strong> (a server-chosen nonce). The browser
74
145
  stretches the password with <strong>Argon2id</strong>, expands it into an <strong>ML-DSA-44</strong>{' '}
@@ -92,12 +163,21 @@ export default function Pq(): React.JSX.Element {
92
163
  </label>
93
164
  <div style={{ display: 'flex', gap: 8 }}>
94
165
  <button onClick={() => prove(false)} disabled={busy}>
95
- {busy ? 'Deriving + signing…' : 'Prove identity'}
166
+ {busy ? 'Deriving + signing…' : 'Log in'}
96
167
  </button>
97
168
  <button onClick={() => prove(true)} disabled={busy} title="Flip a signature byte: must fail">
98
169
  Tamper, then verify
99
170
  </button>
100
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>
101
181
  </div>
102
182
 
103
183
  {proof && (
@@ -121,11 +201,56 @@ export default function Pq(): React.JSX.Element {
121
201
  </p>
122
202
  )}
123
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
+
124
246
  <p style={{ marginTop: 24, opacity: 0.7, fontSize: '0.9rem' }}>
125
- The challenge is server-issued and tamper-proof, but stateless, so it has no single-use consume (within
126
- the TTL a captured proof could be replayed). The full register/login protocol with an atomic
127
- challenge consume is in <code>server/routes/Auth.ts</code>; sessions and <code>getUser()</code> are on
128
- the <Toil.Link href="/auth">Auth</Toil.Link> page.
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.
129
254
  </p>
130
255
  <p>
131
256
  <Toil.Link href="/features">Back to features</Toil.Link>
@@ -1,6 +1,8 @@
1
1
  import { Response, RouteContext, SecureCookies, base64UrlEncode, base64UrlDecode } from 'toiljs/server/runtime';
2
2
  import { DataReader, DataWriter } from 'data';
3
3
 
4
+ import { encodeSessionUser } from './Session';
5
+
4
6
  /**
5
7
  * Post-quantum identity demo (server half), challenge-response.
6
8
  *
@@ -95,15 +97,31 @@ class PqDemo {
95
97
  if (!br.ok) return Response.text('INVALID: malformed challenge\n', 401);
96
98
  if (Time.nowSeconds() >= exp) return Response.text('INVALID: challenge expired\n', 401);
97
99
 
100
+ // TODO(db): single-use consume is NOT implemented yet (no KV/DB host
101
+ // binding available). The real fix is an ATOMIC consume of `cid` here --
102
+ // Redis GETDEL / SQL DELETE ... RETURNING -- so a given challenge verifies
103
+ // at most once. Until that exists, this token is replayable within its
104
+ // TTL: a captured {token, signature} re-verifies until `exp`. The
105
+ // production login (server/routes/Auth.ts) shows the atomic-consume shape.
106
+
98
107
  // 2. Rebuild the message from the SERVER's values (inside the token,
99
108
  // never client-echoed) and verify the ML-DSA-44 signature.
100
109
  const message = AuthService.buildLoginMessage(sub, AUD, cid, nonce, iat, exp);
101
- const ok = AuthService.verifyLogin(pk, message, sig);
102
- return Response.text(
103
- ok
104
- ? 'VALID: server-issued challenge signed and verified (ML-DSA-44, FIPS 204)\n'
105
- : 'INVALID: signature did not verify\n',
106
- ok ? 200 : 401,
110
+ if (!AuthService.verifyLogin(pk, message, sig)) {
111
+ return Response.text('INVALID: signature did not verify\n', 401);
112
+ }
113
+
114
+ // 3. FULL AUTH: a valid post-quantum proof logs the user in. Mint the
115
+ // signed session for the proven `sub` via the @user codec, plus the
116
+ // readable companion. Every `@auth` route now recognises this user
117
+ // and `AuthService.getUser()` returns `{ username: sub, ... }`.
118
+ const userData = encodeSessionUser(sub);
119
+ const resp = Response.text(
120
+ 'VALID: ML-DSA-44 (FIPS 204) verified; session established (@auth ready)\n',
121
+ 200,
107
122
  );
123
+ resp.setCookie(AuthService.mintSession(userData, 3600));
124
+ resp.setCookie(AuthService.userCookie(userData, 3600));
125
+ return resp;
108
126
  }
109
127
  }
@@ -28,6 +28,16 @@ class Account {
28
28
  score: u64 = 0;
29
29
  }
30
30
 
31
+ /** Encode the @user session payload for `username` (admin if `root`). Exported
32
+ * as a FUNCTION (not the class, which would warn AS235 "only ... become WASM
33
+ * exports") so the PQ login can mint a session via the same generated codec. */
34
+ export function encodeSessionUser(username: string): Uint8Array {
35
+ const u = new Account();
36
+ u.username = username;
37
+ u.admin = username == 'root';
38
+ return u.encode();
39
+ }
40
+
31
41
  @rest('session')
32
42
  class Session {
33
43
  /** POST /session/dev-login body: str(username) -> sets the session cookie.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "toiljs",
3
3
  "type": "module",
4
- "version": "0.0.36",
4
+ "version": "0.0.38",
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": {
package/src/cli/create.ts CHANGED
@@ -270,96 +270,31 @@ function scaffold(
270
270
  * editor their shapes. Auto-included via the server tsconfig and ignored by the
271
271
  * compiler. Keep in sync with `toiljs/server/runtime/http/*`.
272
272
  */
273
- const TOIL_SERVER_ENV_DTS = `/**
274
- * Editor-only ambient declarations for the toiljs cookie globals.
275
- *
276
- * \`Cookie\`, \`Cookies\`, \`SecureCookies\`, and the \`SameSite\` / \`CookieEncoding\` /
277
- * \`CookiePrefix\` enums are \`@global\` in the toiljs server runtime, so a handler
278
- * uses them with no import (exactly like \`crypto\`). The toilscript compiler
279
- * registers them from the runtime; this file just gives the editor their shapes.
280
- * It is auto-included by the server tsconfig and ignored by the compiler.
281
- */
282
-
283
- declare enum SameSite {
284
- Default = 0,
285
- None = 1,
286
- Lax = 2,
287
- Strict = 3,
288
- }
289
-
290
- declare enum CookieEncoding {
291
- Percent = 0,
292
- Raw = 1,
293
- Base64Url = 2,
294
- }
295
-
296
- declare enum CookiePrefix {
297
- None = 0,
298
- Secure = 1,
299
- Host = 2,
300
- }
301
-
302
- declare class CookieValidation {
303
- valid: bool;
304
- errors: Array<string>;
305
- fail(msg: string): void;
306
- }
307
-
308
- declare class Cookie {
309
- name: string;
310
- value: string;
311
- encoding: CookieEncoding;
312
- constructor(name: string, value: string);
313
- static create(name: string, value: string): Cookie;
314
- domain(v: string): Cookie;
315
- path(v: string): Cookie;
316
- maxAge(seconds: i64): Cookie;
317
- expires(epochSeconds: i64): Cookie;
318
- expiresRaw(date: string): Cookie;
319
- secure(on?: bool): Cookie;
320
- httpOnly(on?: bool): Cookie;
321
- sameSite(s: SameSite): Cookie;
322
- partitioned(on?: bool): Cookie;
323
- priority(p: string): Cookie;
324
- extension(av: string): Cookie;
325
- withEncoding(e: CookieEncoding): Cookie;
326
- asSecurePrefixed(): Cookie;
327
- asHostPrefixed(): Cookie;
328
- detectedPrefix(): CookiePrefix;
329
- encodedValue(): string;
330
- validate(): CookieValidation;
331
- serialize(strict?: bool): string;
332
- toString(): string;
333
- }
334
-
335
- declare class CookieMap {
336
- set(name: string, value: string): void;
337
- get(name: string): string | null;
338
- has(name: string): bool;
339
- names(): Array<string>;
340
- readonly size: i32;
341
- }
342
-
343
- declare class Cookies {
344
- static parse(cookieHeader: string): CookieMap;
345
- static get(cookieHeader: string, name: string): string | null;
346
- static serialize(name: string, value: string): string;
347
- static parseSetCookie(setCookie: string): Cookie;
348
- static encodeValue(raw: string): string;
349
- static decodeValue(enc: string): string;
350
- }
351
-
352
- declare class SecureCookies {
353
- static signed(key: Uint8Array): SecureCookies;
354
- static encrypted(key: Uint8Array): SecureCookies;
355
- addKey(key: Uint8Array): SecureCookies;
356
- sign(name: string, value: string): string;
357
- unsign(name: string, sealed: string): string | null;
358
- encrypt(name: string, value: string): string;
359
- decrypt(name: string, sealed: string): string | null;
360
- seal(cookie: Cookie): Cookie;
361
- open(jar: CookieMap, name: string): string | null;
362
- }
273
+ // Editor-only ambient declarations for the server-runtime globals, scaffolded
274
+ // into the server dir. Kept BYTE-IDENTICAL to `TOIL_SERVER_ENV_DTS` in
275
+ // src/compiler/generate.ts (which `toiljs build`/`dev` regenerate, and `doctor
276
+ // --fix` rewrites), so create / build / doctor never disagree and flip-flop.
277
+ export const TOIL_SERVER_ENV_DTS = `// AUTO-GENERATED by toil, do not edit. Editor-only ambient declarations for
278
+ // the toiljs server-runtime globals (Cookie, Cookies, SecureCookies, the
279
+ // cookie enums): @global in the runtime, used with no import. These alias the
280
+ // real runtime types so a global-built Cookie is exactly what Response.setCookie
281
+ // / SecureCookies.seal expect. The toilscript compiler registers them itself.
282
+ declare const SameSite: typeof import('toiljs/server/runtime/http/cookie').SameSite;
283
+ type SameSite = import('toiljs/server/runtime/http/cookie').SameSite;
284
+ declare const CookieEncoding: typeof import('toiljs/server/runtime/http/cookie').CookieEncoding;
285
+ type CookieEncoding = import('toiljs/server/runtime/http/cookie').CookieEncoding;
286
+ declare const CookiePrefix: typeof import('toiljs/server/runtime/http/cookie').CookiePrefix;
287
+ type CookiePrefix = import('toiljs/server/runtime/http/cookie').CookiePrefix;
288
+ declare const CookieValidation: typeof import('toiljs/server/runtime/http/cookie').CookieValidation;
289
+ type CookieValidation = import('toiljs/server/runtime/http/cookie').CookieValidation;
290
+ declare const Cookie: typeof import('toiljs/server/runtime/http/cookie').Cookie;
291
+ type Cookie = import('toiljs/server/runtime/http/cookie').Cookie;
292
+ declare const CookieMap: typeof import('toiljs/server/runtime/http/cookies').CookieMap;
293
+ type CookieMap = import('toiljs/server/runtime/http/cookies').CookieMap;
294
+ declare const Cookies: typeof import('toiljs/server/runtime/http/cookies').Cookies;
295
+ type Cookies = import('toiljs/server/runtime/http/cookies').Cookies;
296
+ declare const SecureCookies: typeof import('toiljs/server/runtime/http/securecookies').SecureCookies;
297
+ type SecureCookies = import('toiljs/server/runtime/http/securecookies').SecureCookies;
363
298
  `;
364
299
 
365
300
  /**
package/src/cli/doctor.ts CHANGED
@@ -10,7 +10,7 @@ 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 } from 'toiljs/compiler';
13
+ import { loadConfig, type ResolvedToilConfig, scanRoutes, TOIL_SERVER_ENV_DTS } from 'toiljs/compiler';
14
14
 
15
15
  import {
16
16
  type Check,
@@ -330,6 +330,32 @@ function applyRpcFix(root: string): RpcFixResult {
330
330
  changed.push('.gitignore');
331
331
  }
332
332
 
333
+ // Migrate the editor-only server-globals d.ts to the current shapes (the
334
+ // exact content `toiljs build`/`dev` regenerate), in each server source dir.
335
+ // This is what fixes a stale, scaffolded `toil-server-env.d.ts` whose
336
+ // standalone class decls made a second, incompatible `Cookie`.
337
+ const serverToilconfig = readJsonObject(path.join(root, 'toilconfig.json'));
338
+ if (serverToilconfig !== null) {
339
+ const entries = Array.isArray(serverToilconfig.entries)
340
+ ? (serverToilconfig.entries as unknown[]).filter((e): e is string => typeof e === 'string')
341
+ : [];
342
+ const dirs = new Set<string>();
343
+ for (const e of entries) dirs.add(path.dirname(path.resolve(root, e)));
344
+ if (dirs.size === 0) dirs.add(path.join(root, 'server'));
345
+ for (const dir of dirs) {
346
+ const envPath = path.join(dir, 'toil-server-env.d.ts');
347
+ if (readFile(envPath) !== TOIL_SERVER_ENV_DTS) {
348
+ try {
349
+ fs.mkdirSync(dir, { recursive: true });
350
+ writeFile(envPath, TOIL_SERVER_ENV_DTS);
351
+ changed.push(path.relative(root, envPath));
352
+ } catch {
353
+ // editor-only; ignore write failures
354
+ }
355
+ }
356
+ }
357
+ }
358
+
333
359
  return { changed, skipped };
334
360
  }
335
361
 
@@ -11,7 +11,7 @@
11
11
  * JSON, byte-identical to the server's `AuthService.buildLoginMessage`.
12
12
  */
13
13
 
14
- import { argon2id } from 'hash-wasm';
14
+ import { argon2id, sha256 } from 'hash-wasm';
15
15
  import { ml_dsa44 } from '@btc-vision/post-quantum/ml-dsa.js';
16
16
 
17
17
  import { DataReader, DataWriter } from 'toiljs/io';
@@ -230,10 +230,15 @@ export interface IdentityProof {
230
230
  }
231
231
 
232
232
  /** Deterministic 16-byte Argon2id salt for the demo, so the same
233
- * username + password always maps to the same identity (keypair). */
233
+ * username + password always maps to the same identity (keypair).
234
+ * Uses hash-wasm's SHA-256 (pure WebAssembly), not `crypto.subtle`, so it
235
+ * works in an insecure context (plain HTTP), where `crypto.subtle` is
236
+ * undefined. */
234
237
  async function demoSalt(username: string): Promise<Uint8Array> {
235
- const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode('pq-demo|' + username));
236
- return new Uint8Array(digest).slice(0, 16);
238
+ const hex = await sha256('pq-demo|' + username);
239
+ const out = new Uint8Array(16);
240
+ for (let i = 0; i < 16; i++) out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
241
+ return out;
237
242
  }
238
243
 
239
244
  /**
@@ -76,6 +76,39 @@ export const TOIL_ENV_DTS =
76
76
  ` export const pages: import('toiljs/client').PageMeta[];\n` +
77
77
  `}\n`;
78
78
 
79
+ /**
80
+ * Contents of `<serverDir>/toil-server-env.d.ts`: editor-only ambient
81
+ * declarations for the toiljs server-runtime globals. ALIASES the real runtime
82
+ * types (the same trick the IO globals use above), so a global-built `Cookie`
83
+ * is the exact type the runtime APIs (`Response.setCookie`, `SecureCookies.seal`)
84
+ * expect; redeclaring them as standalone classes makes a second, nominally-
85
+ * incompatible `Cookie` (private fields). Regenerated by `buildServer` on every
86
+ * build/dev so existing projects auto-migrate. Keep the declarations in sync
87
+ * with the `toiljs create` scaffold (`src/cli/create.ts`).
88
+ */
89
+ export const TOIL_SERVER_ENV_DTS =
90
+ `// AUTO-GENERATED by toil, do not edit. Editor-only ambient declarations for\n` +
91
+ `// the toiljs server-runtime globals (Cookie, Cookies, SecureCookies, the\n` +
92
+ `// cookie enums): @global in the runtime, used with no import. These alias the\n` +
93
+ `// real runtime types so a global-built Cookie is exactly what Response.setCookie\n` +
94
+ `// / SecureCookies.seal expect. The toilscript compiler registers them itself.\n` +
95
+ `declare const SameSite: typeof import('toiljs/server/runtime/http/cookie').SameSite;\n` +
96
+ `type SameSite = import('toiljs/server/runtime/http/cookie').SameSite;\n` +
97
+ `declare const CookieEncoding: typeof import('toiljs/server/runtime/http/cookie').CookieEncoding;\n` +
98
+ `type CookieEncoding = import('toiljs/server/runtime/http/cookie').CookieEncoding;\n` +
99
+ `declare const CookiePrefix: typeof import('toiljs/server/runtime/http/cookie').CookiePrefix;\n` +
100
+ `type CookiePrefix = import('toiljs/server/runtime/http/cookie').CookiePrefix;\n` +
101
+ `declare const CookieValidation: typeof import('toiljs/server/runtime/http/cookie').CookieValidation;\n` +
102
+ `type CookieValidation = import('toiljs/server/runtime/http/cookie').CookieValidation;\n` +
103
+ `declare const Cookie: typeof import('toiljs/server/runtime/http/cookie').Cookie;\n` +
104
+ `type Cookie = import('toiljs/server/runtime/http/cookie').Cookie;\n` +
105
+ `declare const CookieMap: typeof import('toiljs/server/runtime/http/cookies').CookieMap;\n` +
106
+ `type CookieMap = import('toiljs/server/runtime/http/cookies').CookieMap;\n` +
107
+ `declare const Cookies: typeof import('toiljs/server/runtime/http/cookies').Cookies;\n` +
108
+ `type Cookies = import('toiljs/server/runtime/http/cookies').Cookies;\n` +
109
+ `declare const SecureCookies: typeof import('toiljs/server/runtime/http/securecookies').SecureCookies;\n` +
110
+ `type SecureCookies = import('toiljs/server/runtime/http/securecookies').SecureCookies;\n`;
111
+
79
112
  /**
80
113
  * Returns a `./`-prefixed, **extensionless** POSIX module specifier from `.toil` to `abs`, for use
81
114
  * in generated `import(...)` calls. Extensionless so TypeScript doesn't demand
@@ -12,7 +12,7 @@ import { build as viteBuild, createServer, mergeConfig, type ViteDevServer } fro
12
12
  import type { RunningBackend } from 'toiljs/backend';
13
13
 
14
14
  import { loadConfig } from './config.js';
15
- import { generate } from './generate.js';
15
+ import { generate, TOIL_SERVER_ENV_DTS } from './generate.js';
16
16
  import { prerenderStaticParams } from './ssg.js';
17
17
  import { extractTemplates } from './template-build.js';
18
18
  import { createViteConfig } from './vite.js';
@@ -109,6 +109,19 @@ function serverEntryFiles(root: string): string[] {
109
109
  async function buildServer(root: string): Promise<void> {
110
110
  if (!fs.existsSync(path.join(root, 'toilconfig.json'))) return;
111
111
 
112
+ // Regenerate the editor-only server-globals d.ts each build (the same way
113
+ // `generate` rewrites `toil-env.d.ts`), so an existing project auto-migrates
114
+ // to the current shapes without re-scaffolding or running doctor. Best
115
+ // effort; an unwritable dir never blocks the build.
116
+ for (const dir of serverDirs(root)) {
117
+ try {
118
+ fs.mkdirSync(dir, { recursive: true });
119
+ fs.writeFileSync(path.join(dir, 'toil-server-env.d.ts'), TOIL_SERVER_ENV_DTS);
120
+ } catch {
121
+ // editor-only; ignore write failures
122
+ }
123
+ }
124
+
112
125
  const require = createRequire(path.join(root, 'package.json'));
113
126
  let binJs: string;
114
127
  try {
@@ -337,7 +350,7 @@ export async function start(opts: ToilCommandOptions = {}): Promise<RunningBacke
337
350
  export { defineConfig, loadConfig, AiProvider } from './config.js';
338
351
  export { scanRoutes } from './routes.js';
339
352
  export type { ScannedRoute } from './routes.js';
340
- export { TOIL_ENV_DTS } from './generate.js';
353
+ export { TOIL_ENV_DTS, TOIL_SERVER_ENV_DTS } from './generate.js';
341
354
  export { AI_HELPERS, AI_HELPER_IDS, aiHelperFiles, TOIL_DOCS } from './docs.js';
342
355
  export type { AiHelper } from './docs.js';
343
356
  export type {
@@ -1,94 +0,0 @@
1
- /**
2
- * Editor-only ambient declarations for the toiljs cookie globals.
3
- *
4
- * `Cookie`, `Cookies`, `SecureCookies`, and the `SameSite` / `CookieEncoding` /
5
- * `CookiePrefix` enums are `@global` in the toiljs server runtime, so a handler
6
- * uses them with no import (exactly like `crypto`). The toilscript compiler
7
- * registers them from the runtime; this file just gives the editor their shapes
8
- * so it does not flag the unimported names. It is auto-included by the server
9
- * `tsconfig.json` (`include: ["./**/*.ts"]`) and ignored by the compiler.
10
- *
11
- * `toiljs create` scaffolds this file; keep it in sync with
12
- * `toiljs/server/runtime/http/*`.
13
- */
14
-
15
- declare enum SameSite {
16
- Default = 0,
17
- None = 1,
18
- Lax = 2,
19
- Strict = 3,
20
- }
21
-
22
- declare enum CookieEncoding {
23
- Percent = 0,
24
- Raw = 1,
25
- Base64Url = 2,
26
- }
27
-
28
- declare enum CookiePrefix {
29
- None = 0,
30
- Secure = 1,
31
- Host = 2,
32
- }
33
-
34
- declare class CookieValidation {
35
- valid: bool;
36
- errors: Array<string>;
37
- fail(msg: string): void;
38
- }
39
-
40
- declare class Cookie {
41
- name: string;
42
- value: string;
43
- encoding: CookieEncoding;
44
- constructor(name: string, value: string);
45
- static create(name: string, value: string): Cookie;
46
- domain(v: string): Cookie;
47
- path(v: string): Cookie;
48
- maxAge(seconds: i64): Cookie;
49
- expires(epochSeconds: i64): Cookie;
50
- expiresRaw(date: string): Cookie;
51
- secure(on?: bool): Cookie;
52
- httpOnly(on?: bool): Cookie;
53
- sameSite(s: SameSite): Cookie;
54
- partitioned(on?: bool): Cookie;
55
- priority(p: string): Cookie;
56
- extension(av: string): Cookie;
57
- withEncoding(e: CookieEncoding): Cookie;
58
- asSecurePrefixed(): Cookie;
59
- asHostPrefixed(): Cookie;
60
- detectedPrefix(): CookiePrefix;
61
- encodedValue(): string;
62
- validate(): CookieValidation;
63
- serialize(strict?: bool): string;
64
- toString(): string;
65
- }
66
-
67
- declare class CookieMap {
68
- set(name: string, value: string): void;
69
- get(name: string): string | null;
70
- has(name: string): bool;
71
- names(): Array<string>;
72
- readonly size: i32;
73
- }
74
-
75
- declare class Cookies {
76
- static parse(cookieHeader: string): CookieMap;
77
- static get(cookieHeader: string, name: string): string | null;
78
- static serialize(name: string, value: string): string;
79
- static parseSetCookie(setCookie: string): Cookie;
80
- static encodeValue(raw: string): string;
81
- static decodeValue(enc: string): string;
82
- }
83
-
84
- declare class SecureCookies {
85
- static signed(key: Uint8Array): SecureCookies;
86
- static encrypted(key: Uint8Array): SecureCookies;
87
- addKey(key: Uint8Array): SecureCookies;
88
- sign(name: string, value: string): string;
89
- unsign(name: string, sealed: string): string | null;
90
- encrypt(name: string, value: string): string;
91
- decrypt(name: string, sealed: string): string | null;
92
- seal(cookie: Cookie): Cookie;
93
- open(jar: CookieMap, name: string): string | null;
94
- }