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.
- package/CHANGELOG.md +14 -0
- package/build/cli/.tsbuildinfo +1 -1
- package/build/cli/index.js +40 -91
- package/build/client/.tsbuildinfo +1 -1
- package/build/client/auth.js +6 -3
- package/build/compiler/.tsbuildinfo +1 -1
- package/build/compiler/generate.d.ts +1 -0
- package/build/compiler/generate.js +21 -0
- package/build/compiler/index.d.ts +1 -1
- package/build/compiler/index.js +10 -2
- package/examples/basic/client/routes/pq.tsx +134 -9
- package/examples/basic/server/routes/PqDemo.ts +24 -6
- package/examples/basic/server/routes/Session.ts +10 -0
- package/package.json +1 -1
- package/src/cli/create.ts +25 -90
- package/src/cli/doctor.ts +27 -1
- package/src/client/auth.ts +9 -4
- package/src/compiler/generate.ts +33 -0
- package/src/compiler/index.ts +15 -2
- package/examples/basic/server/toil-server-env.d.ts +0 -94
|
@@ -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
|
-
|
|
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
|
|
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…' : '
|
|
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> — 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("pq-demo|" + 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't, without a
|
|
178
|
+
store): binding a username to a <em>registered</em> key, so here the username is self-asserted
|
|
179
|
+
— <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 — 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
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
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
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
declare
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
declare
|
|
297
|
-
|
|
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
|
|
package/src/client/auth.ts
CHANGED
|
@@ -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
|
|
236
|
-
|
|
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
|
/**
|
package/src/compiler/generate.ts
CHANGED
|
@@ -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
|
package/src/compiler/index.ts
CHANGED
|
@@ -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
|
-
}
|