toiljs 0.0.85 → 0.0.87
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 +5 -0
- package/README.md +2 -2
- package/build/cli/.tsbuildinfo +1 -1
- package/build/cli/index.js +303 -293
- package/build/compiler/.tsbuildinfo +1 -1
- package/build/compiler/config.d.ts +2 -0
- package/build/compiler/config.js +1 -0
- package/build/compiler/docs.js +8 -24
- package/build/compiler/generate.js +4 -2
- package/build/compiler/index.d.ts +1 -1
- package/build/compiler/index.js +69 -6
- package/build/compiler/toil-docs.generated.js +64 -22
- package/build/devserver/.tsbuildinfo +1 -1
- package/build/devserver/analytics/index.js +7 -3
- package/build/devserver/db/database.d.ts +4 -0
- package/build/devserver/db/database.js +43 -1
- package/build/devserver/db/index.d.ts +1 -1
- package/build/devserver/db/index.js +1 -1
- package/build/devserver/db/types.d.ts +3 -0
- package/build/devserver/db/types.js +2 -0
- package/build/devserver/runtime/module.js +4 -1
- package/docs/README.md +104 -65
- package/docs/auth/README.md +102 -0
- package/docs/auth/configuration.md +94 -0
- package/docs/auth/extending.md +202 -0
- package/docs/auth/how-it-works.md +138 -0
- package/docs/auth/usage.md +188 -0
- package/docs/backend/README.md +143 -0
- package/docs/backend/data.md +351 -0
- package/docs/backend/rest.md +402 -0
- package/docs/backend/rpc.md +226 -0
- package/docs/background/README.md +114 -0
- package/docs/background/daemons.md +230 -0
- package/docs/background/derive.md +179 -0
- package/docs/cli/README.md +377 -0
- package/docs/concepts/config.md +416 -0
- package/docs/concepts/decorators.md +127 -0
- package/docs/concepts/security.md +108 -0
- package/docs/concepts/tiers.md +166 -0
- package/docs/concepts/types.md +216 -0
- package/docs/database/README.md +143 -0
- package/docs/database/capacity.md +350 -0
- package/docs/database/counters.md +174 -0
- package/docs/database/documents.md +255 -0
- package/docs/database/events.md +307 -0
- package/docs/database/membership.md +246 -0
- package/docs/database/setup.md +155 -0
- package/docs/database/unique.md +216 -0
- package/docs/database/views.md +246 -0
- package/docs/frontend/README.md +101 -0
- package/docs/frontend/data-fetching.md +243 -0
- package/docs/frontend/images.md +148 -0
- package/docs/frontend/metadata.md +344 -0
- package/docs/frontend/rendering.md +236 -0
- package/docs/frontend/routing.md +344 -0
- package/docs/frontend/scripts.md +118 -0
- package/docs/frontend/search.md +191 -0
- package/docs/frontend/styling.md +147 -0
- package/docs/getting-started/README.md +81 -0
- package/docs/getting-started/create-project.md +131 -0
- package/docs/getting-started/deploy.md +101 -0
- package/docs/getting-started/first-app.md +215 -0
- package/docs/getting-started/installation.md +106 -0
- package/docs/getting-started/migrating.md +125 -0
- package/docs/getting-started/project-structure.md +163 -0
- package/docs/introduction/README.md +41 -0
- package/docs/introduction/design-principles.md +55 -0
- package/docs/introduction/distributed.md +74 -0
- package/docs/introduction/how-it-works.md +74 -0
- package/docs/introduction/hyperscale.md +51 -0
- package/docs/introduction/modern-stack.md +36 -0
- package/docs/introduction/vs-other-frameworks.md +48 -0
- package/docs/introduction/why-toil.md +93 -0
- package/docs/llms.txt +90 -0
- package/docs/realtime/README.md +102 -0
- package/docs/realtime/channels.md +211 -0
- package/docs/realtime/streams.md +369 -0
- package/docs/services/README.md +60 -0
- package/docs/services/analytics.md +268 -0
- package/docs/services/caching.md +175 -0
- package/docs/services/cookies.md +235 -0
- package/docs/services/crypto.md +209 -0
- package/docs/services/email.md +289 -0
- package/docs/services/environment.md +141 -0
- package/docs/services/ratelimit.md +174 -0
- package/docs/services/time.md +85 -0
- package/examples/basic/client/routes/analytics.tsx +13 -12
- package/examples/basic/server/models/SiteAnalytics.ts +29 -17
- package/examples/basic/server/routes/Analytics.ts +15 -17
- package/examples/basic/server/routes/UserId.ts +22 -0
- package/package.json +15 -11
- package/scripts/gen-toil-docs.mjs +24 -35
- package/server/auth/AuthController.ts +336 -0
- package/server/auth/AuthUser.ts +23 -0
- package/server/auth/index.ts +16 -0
- package/server/globals/auth.ts +31 -0
- package/server/globals/userid.ts +107 -0
- package/src/compiler/config.ts +13 -0
- package/src/compiler/docs.ts +16 -33
- package/src/compiler/generate.ts +6 -2
- package/src/compiler/index.ts +114 -6
- package/src/compiler/toil-docs.generated.ts +64 -22
- package/src/devserver/analytics/index.ts +10 -4
- package/src/devserver/db/database.ts +67 -1
- package/src/devserver/db/index.ts +1 -0
- package/src/devserver/db/types.ts +13 -0
- package/src/devserver/runtime/module.ts +7 -0
- package/test/analytics-dev.test.ts +2 -1
- package/test/devserver-database.test.ts +113 -0
- package/docs/auth-todo.md +0 -149
- package/docs/auth.md +0 -322
- package/docs/caching.md +0 -115
- package/docs/cli.md +0 -17
- package/docs/client.md +0 -39
- package/docs/cookies.md +0 -457
- package/docs/crypto.md +0 -130
- package/docs/daemon.md +0 -123
- package/docs/data.md +0 -131
- package/docs/derive.md +0 -159
- package/docs/email.md +0 -326
- package/docs/environment.md +0 -97
- package/docs/getting-started.md +0 -128
- package/docs/index.md +0 -30
- package/docs/ratelimit.md +0 -95
- package/docs/routing.md +0 -259
- package/docs/rpc.md +0 -149
- package/docs/server.md +0 -61
- package/docs/ssr.md +0 -632
- package/docs/streams.md +0 -178
- package/docs/styling.md +0 -22
- package/docs/tiers.md +0 -133
- package/docs/time.md +0 -43
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
import { Response, RouteContext } from 'toiljs/server/runtime';
|
|
2
|
+
import { DataReader, DataWriter } from 'data';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Built-in Toil PQ-Auth controller: the full post-quantum password-login API
|
|
6
|
+
* (`/auth/register|login/start|finish`) plus sessions (`/auth/me`, `/auth/logout`),
|
|
7
|
+
* mounted automatically when an app opts in with `server: { auth: true }` (or
|
|
8
|
+
* `import 'toiljs/server/auth'`). It is a framework-shipped SOURCE file the build
|
|
9
|
+
* APPENDS to the toilscript entry set, so its `@data`/`@database`/`@rest` decorators
|
|
10
|
+
* weave and its `@rest` class self-mounts at `/auth/*` — no hand-written boilerplate.
|
|
11
|
+
*
|
|
12
|
+
* The password never leaves the browser. The client blinds it through the
|
|
13
|
+
* server-keyed OPRF (precomputation-resistant keyed salt), stretches the OPRF
|
|
14
|
+
* output with Argon2id into an ML-DSA-44 keypair, and registers only the public
|
|
15
|
+
* key (+ a proof-of-possession). Login is a challenge-response that also runs an
|
|
16
|
+
* ML-KEM-768 key encapsulation: the server proves its identity by returning a
|
|
17
|
+
* confirmation tag only derivable from the decapsulated shared secret (mutual
|
|
18
|
+
* auth). See `server/globals/auth.ts` (the `AuthService` global) and the client
|
|
19
|
+
* half in `toiljs/client` (`Auth.register` / `Auth.login`).
|
|
20
|
+
*
|
|
21
|
+
* STORAGE: backed by ToilDB (`@database AuthDb`). Accounts are a `record`
|
|
22
|
+
* collection keyed by username; login challenges are a `record` consumed exactly
|
|
23
|
+
* once with `getDelete` (atomic fetch-and-delete). The dev server emulates these
|
|
24
|
+
* `env.data.*` host imports in process (so register -> login spans requests under
|
|
25
|
+
* `toiljs dev`); the production edge backs the SAME API with ScyllaDB.
|
|
26
|
+
*
|
|
27
|
+
* Wire: every body/response is binary (`DataWriter`/`DataReader`), never JSON.
|
|
28
|
+
*
|
|
29
|
+
* Tuning (audience, TTLs, Argon2id params, rate-limit tuples) is fixed here for
|
|
30
|
+
* now; config-driven overrides are a documented follow-up. The secret trio
|
|
31
|
+
* (`AUTH_SESSION_SECRET` / `AUTH_OPRF_SEED` / `AUTH_KEM_SK`) resolves lazily from
|
|
32
|
+
* the tenant env store with DEV fallbacks, so this runs with zero config.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
// Demo-light Argon2id params (responsive in a browser tab). A real deployment
|
|
36
|
+
// uses >= 256 MiB / >= 3 iterations. The client derives against whatever it is
|
|
37
|
+
// handed, so this is the single source of truth.
|
|
38
|
+
const MEM_KIB: u32 = 32768; // 32 MiB
|
|
39
|
+
const ITERS: u32 = 2;
|
|
40
|
+
const PAR: u32 = 1;
|
|
41
|
+
|
|
42
|
+
const CHALLENGE_TTL_SECS: u64 = 120;
|
|
43
|
+
const SESSION_TTL_SECS: u64 = 3600;
|
|
44
|
+
|
|
45
|
+
// Resolved-and-cached per instance; the audience id (server config, never
|
|
46
|
+
// client-echoed). Read lazily (not at module top level) so the env host import
|
|
47
|
+
// runs during a dispatch, when the request host is bound — the same pattern
|
|
48
|
+
// AuthService uses for its secrets.
|
|
49
|
+
let __aud: string | null = null;
|
|
50
|
+
function aud(): string {
|
|
51
|
+
const cached = __aud;
|
|
52
|
+
if (cached != null) return cached;
|
|
53
|
+
const v = Environment.getSecure('TOIL_AUTH_AUDIENCE');
|
|
54
|
+
const resolved = v != null ? v : 'toil';
|
|
55
|
+
__aud = resolved;
|
|
56
|
+
return resolved;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The tenant domain the stable {@link ToilUserId} is scoped to: the configured
|
|
61
|
+
* `TOIL_AUTH_DOMAIN` if set, else the request's `Host` header, else `localhost`.
|
|
62
|
+
* It only needs to be stable per deployment (it is one of the SHA-256 inputs of
|
|
63
|
+
* the user id), so any robust per-deployment host source is fine.
|
|
64
|
+
*/
|
|
65
|
+
function authDomain(ctx: RouteContext): string {
|
|
66
|
+
const configured = Environment.getSecure('TOIL_AUTH_DOMAIN');
|
|
67
|
+
if (configured != null) return configured;
|
|
68
|
+
const host = ctx.request.header('host');
|
|
69
|
+
if (host != null) return host;
|
|
70
|
+
return 'localhost';
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function randomBytes(n: i32): Uint8Array {
|
|
74
|
+
const b = new Uint8Array(n);
|
|
75
|
+
crypto.getRandomValues(b);
|
|
76
|
+
return b;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function nowSecs(): u64 {
|
|
80
|
+
return <u64>(Date.now() / 1000);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** One generic error on every failure path (anti-enumeration, anti-oracle). */
|
|
84
|
+
function fail(): Response {
|
|
85
|
+
return Response.text('auth: request failed\n', 401);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Deterministic per-user Argon2id salt (16 bytes). With the OPRF providing
|
|
90
|
+
* precomputation resistance, a public/deterministic salt is fine: it only needs
|
|
91
|
+
* to be unique per user (the OPRF output already differs per user). Making it
|
|
92
|
+
* deterministic means register and login agree with NO stored salt, and an
|
|
93
|
+
* unknown user yields the SAME stable salt as a known one would -- no
|
|
94
|
+
* enumeration oracle.
|
|
95
|
+
*/
|
|
96
|
+
function deriveSalt(username: string): Uint8Array {
|
|
97
|
+
return crypto.sha256Text('toil-auth-salt-v1:' + username).slice(0, 16);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
@data
|
|
101
|
+
class Username {
|
|
102
|
+
name: string = '';
|
|
103
|
+
constructor(name: string = '') {
|
|
104
|
+
this.name = name;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
@data
|
|
109
|
+
class ChallengeId {
|
|
110
|
+
cid: Uint8Array = new Uint8Array(0);
|
|
111
|
+
constructor(cid: Uint8Array = new Uint8Array(0)) {
|
|
112
|
+
this.cid = cid;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
@data
|
|
117
|
+
class AuthAccount {
|
|
118
|
+
username: string = '';
|
|
119
|
+
salt: Uint8Array = new Uint8Array(0);
|
|
120
|
+
publicKey: Uint8Array = new Uint8Array(0);
|
|
121
|
+
memKiB: u32 = 0;
|
|
122
|
+
iterations: u32 = 0;
|
|
123
|
+
parallelism: u32 = 0;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
@data
|
|
127
|
+
class Challenge {
|
|
128
|
+
cid: Uint8Array = new Uint8Array(0);
|
|
129
|
+
username: string = '';
|
|
130
|
+
nonce: Uint8Array = new Uint8Array(0);
|
|
131
|
+
iat: u64 = 0;
|
|
132
|
+
exp: u64 = 0;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
@database
|
|
136
|
+
class AuthDb {
|
|
137
|
+
@collection static accounts: Documents<Username, AuthAccount>;
|
|
138
|
+
@collection static challenges: Documents<ChallengeId, Challenge>;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
@rest('auth')
|
|
142
|
+
class Auth {
|
|
143
|
+
/** POST /auth/register/start body: str(username) bytes(blinded)
|
|
144
|
+
* resp: u8(status=0) u32(mem) u32(iters) u32(par) bytes(salt) bytes(evaluated)
|
|
145
|
+
* No taken-oracle: always succeeds; register/finish rejects a duplicate. */
|
|
146
|
+
@ratelimit(RateLimit.SlidingWindow, 5, 60)
|
|
147
|
+
@post('/register/start')
|
|
148
|
+
public registerStart(ctx: RouteContext): Response {
|
|
149
|
+
const r = new DataReader(ctx.request.body);
|
|
150
|
+
const username = r.readString();
|
|
151
|
+
const blinded = r.readBytes();
|
|
152
|
+
if (!r.ok) return fail();
|
|
153
|
+
const evaluated = AuthService.oprfEvaluate(username, blinded);
|
|
154
|
+
if (evaluated.length != AuthService.OPRF_ELEMENT_LEN) return fail();
|
|
155
|
+
|
|
156
|
+
const w = new DataWriter();
|
|
157
|
+
w.writeU8(0);
|
|
158
|
+
w.writeU32(MEM_KIB);
|
|
159
|
+
w.writeU32(ITERS);
|
|
160
|
+
w.writeU32(PAR);
|
|
161
|
+
w.writeBytes(deriveSalt(username));
|
|
162
|
+
w.writeBytes(evaluated);
|
|
163
|
+
return Response.bytes(w.toBytes());
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** POST /auth/register/finish body: str(username) bytes(pk) bytes(regProof)
|
|
167
|
+
* resp: u8(status) -- 0 = ok, 1 = username already registered. Verifies
|
|
168
|
+
* proof-of-possession before storing the key. */
|
|
169
|
+
@ratelimit(RateLimit.SlidingWindow, 5, 60)
|
|
170
|
+
@post('/register/finish')
|
|
171
|
+
public registerFinish(ctx: RouteContext): Response {
|
|
172
|
+
const r = new DataReader(ctx.request.body);
|
|
173
|
+
const username = r.readString();
|
|
174
|
+
const pk = r.readBytes();
|
|
175
|
+
const proof = r.readBytes();
|
|
176
|
+
if (!r.ok) return fail();
|
|
177
|
+
if (pk.length != AuthService.PUBLIC_KEY_LEN) return fail();
|
|
178
|
+
// Already registered: a distinguishable status (not the generic 401) so the
|
|
179
|
+
// client can say "username taken, log in instead" rather than a blank error.
|
|
180
|
+
if (AuthDb.accounts.exists(new Username(username))) {
|
|
181
|
+
return Response.bytes(new DataWriter().writeU8(1).toBytes());
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Proof-of-possession: the client signed buildRegisterMessage with the
|
|
185
|
+
// matching secret key, so we confirm it actually holds it.
|
|
186
|
+
const regMsg = AuthService.buildRegisterMessage(username, pk);
|
|
187
|
+
if (!AuthService.verifyRegister(pk, regMsg, proof)) return fail();
|
|
188
|
+
|
|
189
|
+
const a = new AuthAccount();
|
|
190
|
+
a.username = username;
|
|
191
|
+
a.salt = deriveSalt(username);
|
|
192
|
+
a.publicKey = pk;
|
|
193
|
+
a.memKiB = MEM_KIB;
|
|
194
|
+
a.iterations = ITERS;
|
|
195
|
+
a.parallelism = PAR;
|
|
196
|
+
// create-if-absent: a racing duplicate registration loses here, not above.
|
|
197
|
+
if (!AuthDb.accounts.create(new Username(username), a)) {
|
|
198
|
+
return Response.bytes(new DataWriter().writeU8(1).toBytes());
|
|
199
|
+
}
|
|
200
|
+
return Response.bytes(new DataWriter().writeU8(0).toBytes());
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** POST /auth/login/start body: str(username) bytes(blinded)
|
|
204
|
+
* resp: bytes(cid) str(aud) u32(mem) u32(iters) u32(par) bytes(salt)
|
|
205
|
+
* bytes(nonce) u64(iat) u64(exp) bytes(evaluated)
|
|
206
|
+
* Anti-enumeration: ALWAYS OPRF-evaluates (real or decoy key from the same
|
|
207
|
+
* seed+username), returns a deterministic per-user salt + constant params,
|
|
208
|
+
* and a fresh challenge -- a known and an unknown user are indistinguishable. */
|
|
209
|
+
@ratelimit(RateLimit.SlidingWindow, 5, 60)
|
|
210
|
+
@post('/login/start')
|
|
211
|
+
public loginStart(ctx: RouteContext): Response {
|
|
212
|
+
const r = new DataReader(ctx.request.body);
|
|
213
|
+
const username = r.readString();
|
|
214
|
+
const blinded = r.readBytes();
|
|
215
|
+
if (!r.ok) return fail();
|
|
216
|
+
const evaluated = AuthService.oprfEvaluate(username, blinded);
|
|
217
|
+
if (evaluated.length != AuthService.OPRF_ELEMENT_LEN) return fail();
|
|
218
|
+
|
|
219
|
+
const known = AuthDb.accounts.exists(new Username(username));
|
|
220
|
+
const cid = randomBytes(16);
|
|
221
|
+
const nonce = randomBytes(32);
|
|
222
|
+
const iat = nowSecs();
|
|
223
|
+
const exp = iat + CHALLENGE_TTL_SECS;
|
|
224
|
+
|
|
225
|
+
// Persist only for a real account; the response is identical either way,
|
|
226
|
+
// and login/finish for an unknown user fails generically at consume.
|
|
227
|
+
if (known) {
|
|
228
|
+
const c = new Challenge();
|
|
229
|
+
c.cid = cid;
|
|
230
|
+
c.username = username;
|
|
231
|
+
c.nonce = nonce;
|
|
232
|
+
c.iat = iat;
|
|
233
|
+
c.exp = exp;
|
|
234
|
+
AuthDb.challenges.create(new ChallengeId(cid), c);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const w = new DataWriter();
|
|
238
|
+
w.writeBytes(cid);
|
|
239
|
+
w.writeString(aud());
|
|
240
|
+
w.writeU32(MEM_KIB);
|
|
241
|
+
w.writeU32(ITERS);
|
|
242
|
+
w.writeU32(PAR);
|
|
243
|
+
w.writeBytes(deriveSalt(username));
|
|
244
|
+
w.writeBytes(nonce);
|
|
245
|
+
w.writeU64(iat);
|
|
246
|
+
w.writeU64(exp);
|
|
247
|
+
w.writeBytes(evaluated);
|
|
248
|
+
return Response.bytes(w.toBytes());
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** POST /auth/login/finish body: bytes(cid) bytes(ct) bytes(sig)
|
|
252
|
+
* resp: u8(status) [+ bytes(sessionToken) bytes(serverConfirm)] + Set-Cookie */
|
|
253
|
+
@ratelimit(RateLimit.SlidingWindow, 5, 60)
|
|
254
|
+
@post('/login/finish')
|
|
255
|
+
public loginFinish(ctx: RouteContext): Response {
|
|
256
|
+
const r = new DataReader(ctx.request.body);
|
|
257
|
+
const cid = r.readBytes();
|
|
258
|
+
const ct = r.readBytes();
|
|
259
|
+
const sig = r.readBytes();
|
|
260
|
+
if (!r.ok) return fail();
|
|
261
|
+
|
|
262
|
+
// 1. CONSUME FIRST: atomic fetch-and-delete. Unknown/used/expired => fail.
|
|
263
|
+
const ch = AuthDb.challenges.getDelete(new ChallengeId(cid));
|
|
264
|
+
if (ch == null) return fail();
|
|
265
|
+
if (nowSecs() >= ch.exp) return fail();
|
|
266
|
+
|
|
267
|
+
// 2. Rebuild the message from OUR stored values + the client's ct (and
|
|
268
|
+
// the bound params + server key id), load the account key, verify.
|
|
269
|
+
const acct = AuthDb.accounts.get(new Username(ch.username));
|
|
270
|
+
if (acct == null) return fail();
|
|
271
|
+
const message = AuthService.buildLoginMessage(
|
|
272
|
+
ch.username,
|
|
273
|
+
aud(),
|
|
274
|
+
cid,
|
|
275
|
+
ch.nonce,
|
|
276
|
+
ch.iat,
|
|
277
|
+
ch.exp,
|
|
278
|
+
ct,
|
|
279
|
+
MEM_KIB,
|
|
280
|
+
ITERS,
|
|
281
|
+
PAR,
|
|
282
|
+
AuthService.serverKemKeyId(),
|
|
283
|
+
);
|
|
284
|
+
if (!AuthService.verifyLogin(acct.publicKey, message, sig)) return fail();
|
|
285
|
+
|
|
286
|
+
// 3. Decapsulate (proves WE hold the KEM key), derive the session key K
|
|
287
|
+
// bound to the transcript, and build the confirmation tag the client
|
|
288
|
+
// verifies for mutual auth.
|
|
289
|
+
const sharedSecret = AuthService.mlkemDecapsulate(ct);
|
|
290
|
+
if (sharedSecret.length != AuthService.SHARED_SECRET_LEN) return fail();
|
|
291
|
+
const transcriptHash = AuthService.sha256(message);
|
|
292
|
+
const sessionKey = AuthService.deriveSessionKey(sharedSecret, transcriptHash);
|
|
293
|
+
const confirm = AuthService.serverConfirmTag(sessionKey, transcriptHash);
|
|
294
|
+
|
|
295
|
+
// 4. Success: mint the session for whatever `@user` this program has (built-in or the app's own
|
|
296
|
+
// extended one). `__toilEncodeAuthUser` is injected by `--authUser`: it constructs the `@user`
|
|
297
|
+
// (app fields at their defaults), sets the reserved identity, and encodes it. The stable
|
|
298
|
+
// ToilUserId is derived from the login public key + username + tenant domain.
|
|
299
|
+
const domain = authDomain(ctx);
|
|
300
|
+
const toilUserId = ToilUserId.derive(acct.publicKey, ch.username, domain).toBytes();
|
|
301
|
+
const userData = __toilEncodeAuthUser(toilUserId, ch.username);
|
|
302
|
+
const w = new DataWriter();
|
|
303
|
+
w.writeU8(0);
|
|
304
|
+
w.writeBytes(userData); // opaque session token (the readable user payload)
|
|
305
|
+
w.writeBytes(confirm);
|
|
306
|
+
const resp = Response.bytes(w.toBytes());
|
|
307
|
+
resp.setCookie(AuthService.mintSession(userData, SESSION_TTL_SECS));
|
|
308
|
+
resp.setCookie(AuthService.userCookie(userData, SESSION_TTL_SECS));
|
|
309
|
+
return resp;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/** GET /auth/me (@auth: 401 without a valid session) -> the typed user
|
|
313
|
+
* (bytes(toilUserId) str(username)). `AuthService.getUser()` is auto-typed
|
|
314
|
+
* to the built-in `@user` with no type argument. */
|
|
315
|
+
@auth
|
|
316
|
+
@get('/me')
|
|
317
|
+
public me(_ctx: RouteContext): Response {
|
|
318
|
+
const u = AuthService.getUser();
|
|
319
|
+
if (u == null) return Response.text('no session\n', 401);
|
|
320
|
+
const w = new DataWriter();
|
|
321
|
+
w.writeBytes(u.toilUserId);
|
|
322
|
+
w.writeString(u.username);
|
|
323
|
+
return Response.bytes(w.toBytes());
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/** POST /auth/logout (@auth) -> clears both the signed session and the
|
|
327
|
+
* readable companion cookie. */
|
|
328
|
+
@auth
|
|
329
|
+
@post('/logout')
|
|
330
|
+
public logout(_ctx: RouteContext): Response {
|
|
331
|
+
const resp = Response.text('bye\n', 200);
|
|
332
|
+
resp.setCookie(AuthService.clearSession());
|
|
333
|
+
resp.setCookie(AuthService.clearUserCookie());
|
|
334
|
+
return resp;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The framework's built-in authenticated-user shape for `server.auth` (the zero-config `/auth/*` PQ-login
|
|
3
|
+
* controller). Shipped as a SOURCE file the `server.auth` build APPENDS to the toilscript entry set in
|
|
4
|
+
* BUILTIN mode (the app declares no `@user` of its own), so its `@user` decorator weaves as a user entry.
|
|
5
|
+
*
|
|
6
|
+
* The class is deliberately EMPTY. The build runs with `--authUser`, which injects the reserved
|
|
7
|
+
* `toilUserId` + `username` identity fields (toilUserId FIRST, so `AuthService.userId()` reads it straight
|
|
8
|
+
* from the session codec) and a shape-agnostic `__toilEncodeAuthUser(id, username)` global the controller
|
|
9
|
+
* uses to mint a session. This keeps the wire layout identical whether the user is this built-in shape or an
|
|
10
|
+
* app's own.
|
|
11
|
+
*
|
|
12
|
+
* EXTEND mode: an app that wants a richer authenticated user just declares its OWN `@user` (with extra
|
|
13
|
+
* fields like `admin` or `tenant`). The build detects it, extends THAT class with the same reserved fields,
|
|
14
|
+
* and does not append this file. Either way there is exactly one `@user` per program.
|
|
15
|
+
*
|
|
16
|
+
* Named `SessionUser`, NOT `AuthUser`: the toilscript `@user` transform injects a `@global class AuthUser
|
|
17
|
+
* extends <thisClass>`, so naming this class `AuthUser` would self-extend (a compile error).
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
// @user: the authenticated-user shape. The reserved `toilUserId` + `username` fields are INJECTED by the
|
|
21
|
+
// build (`--authUser`); do not declare them here.
|
|
22
|
+
@user
|
|
23
|
+
class SessionUser {}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Escape-hatch marker for the built-in auth surface. A bare
|
|
3
|
+
* `import 'toiljs/server/auth';` in `server/main.ts` is the lighter-weight opt-in
|
|
4
|
+
* (an alternative to the canonical `server: { auth: true }` config flag): the
|
|
5
|
+
* toiljs build DETECTS this import and appends the shipped `@user` shape +
|
|
6
|
+
* `@rest('auth')` controller to the toilscript ENTRY set, so their decorators
|
|
7
|
+
* weave and the controller self-mounts at `/auth/*`.
|
|
8
|
+
*
|
|
9
|
+
* This module is intentionally EMPTY (a pure marker). Framework-shipped
|
|
10
|
+
* decorator sources under `node_modules` only weave when handed to toilscript as
|
|
11
|
+
* explicit ENTRIES; a transitive `import` resolves them under the `~lib/` LIBRARY
|
|
12
|
+
* prefix, where `@data`/`@rest`/`@user` do NOT weave — and, worse, would then
|
|
13
|
+
* collide with the entry-injected copies as duplicate modules. So the barrel must
|
|
14
|
+
* NOT itself import the controller; the build does the entry injection instead.
|
|
15
|
+
*/
|
|
16
|
+
export {};
|
package/server/globals/auth.ts
CHANGED
|
@@ -278,6 +278,37 @@ export namespace AuthService {
|
|
|
278
278
|
return bytes == null ? null : __toilDecodeAuthUser(bytes);
|
|
279
279
|
}
|
|
280
280
|
|
|
281
|
+
/**
|
|
282
|
+
* The stable, tenant-scoped {@link ToilUserId} of the current request's
|
|
283
|
+
* authenticated user, or `null` if there is no valid session. Recovered from
|
|
284
|
+
* the session codec directly (the built-in `server.auth` `@user` shape puts
|
|
285
|
+
* the 32-byte id FIRST, see `server/auth/AuthUser.ts`), so this stays generic
|
|
286
|
+
* — it does NOT depend on the app's injected `@user` type carrying a
|
|
287
|
+
* `toilUserId` field — while matching the built-in layout. An app whose custom
|
|
288
|
+
* `@user` does not lead with the id should read `getUser()` instead.
|
|
289
|
+
*
|
|
290
|
+
* NULL CHECK: `ToilUserId` overloads `==` for value equality, so `userId() ==
|
|
291
|
+
* null` does NOT type-check. Gate on {@link hasSession} first (then the result
|
|
292
|
+
* is non-null), e.g. `if (AuthService.hasSession()) { const id =
|
|
293
|
+
* AuthService.userId()!; ... }`.
|
|
294
|
+
*/
|
|
295
|
+
export function userId(): ToilUserId | null {
|
|
296
|
+
const bytes = getSessionBytes();
|
|
297
|
+
if (bytes == null) return null;
|
|
298
|
+
return ToilUserId.fromBytes(new DataReader(bytes).readBytes());
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* A no-op discoverability marker for the built-in auth surface. The REAL
|
|
303
|
+
* opt-in is `server: { auth: true }` in `toil.config.ts` (or a bare
|
|
304
|
+
* `import 'toiljs/server/auth'`), which appends the shipped `@user` shape +
|
|
305
|
+
* `@rest('auth')` controller to the build's toilscript entry set so they weave
|
|
306
|
+
* and self-mount at `/auth/*`. `enable()` exists so `AuthService.enable()` in
|
|
307
|
+
* `main.ts` type-checks and the surface is discoverable via this namespace; it
|
|
308
|
+
* mounts nothing on its own.
|
|
309
|
+
*/
|
|
310
|
+
export function enable(): void {}
|
|
311
|
+
|
|
281
312
|
/**
|
|
282
313
|
* Mint a signed session cookie carrying `userData` (the `@user` codec bytes,
|
|
283
314
|
* i.e. `myUser.encode()`), valid for `ttlSecs`. Set it on the response with
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ToilUserId` — the stable, tenant-scoped 256-bit user identity.
|
|
3
|
+
*
|
|
4
|
+
* Derived once as `sha256(mldsaPublicKey || identifier || domain)`, so the SAME user (same ML-DSA login
|
|
5
|
+
* public key + the same email/username) on the SAME tenant domain always maps to the SAME id — independent
|
|
6
|
+
* of session, cookie, or device. It is opaque and one-way (a hash), so it is safe to store, log, or use as
|
|
7
|
+
* a stable database key without leaking the underlying key or address.
|
|
8
|
+
*
|
|
9
|
+
* Backed by four 64-bit words (a 256-bit value), NOT a byte array, so equality is four word compares with
|
|
10
|
+
* early-out — no allocation, no byte loop. `==` / `!=` are overloaded to value equality, and `equals()` is
|
|
11
|
+
* the explicit form. (`===` in AssemblyScript is reference identity and is NOT overloadable; use `==` for
|
|
12
|
+
* value equality.)
|
|
13
|
+
*
|
|
14
|
+
* A global (no import), like `crypto` / `AuthService`. `AuthService` mints the current user's id from the
|
|
15
|
+
* login key + identifier + the request's tenant domain, so `@auth` handlers can key on a stable user.
|
|
16
|
+
*/
|
|
17
|
+
export class ToilUserId {
|
|
18
|
+
constructor(
|
|
19
|
+
public w0: u64 = 0,
|
|
20
|
+
public w1: u64 = 0,
|
|
21
|
+
public w2: u64 = 0,
|
|
22
|
+
public w3: u64 = 0,
|
|
23
|
+
) {}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Derive the id from the user's ML-DSA public key, their identifier (email or username), and the
|
|
27
|
+
* tenant `domain` (the site on dacely). Deterministic: identical inputs → identical id. The three
|
|
28
|
+
* inputs are concatenated in order and SHA-256'd; length-framing is unnecessary because the ML-DSA
|
|
29
|
+
* public key is a FIXED length (1312 bytes for ML-DSA-44), so the boundary before `identifier` is
|
|
30
|
+
* unambiguous, and `domain` is the trusted tail supplied by the host, not the user.
|
|
31
|
+
*/
|
|
32
|
+
static derive(mldsaPublicKey: Uint8Array, identifier: string, domain: string): ToilUserId {
|
|
33
|
+
const id = Uint8Array.wrap(String.UTF8.encode(identifier));
|
|
34
|
+
const dom = Uint8Array.wrap(String.UTF8.encode(domain));
|
|
35
|
+
const buf = new Uint8Array(mldsaPublicKey.length + id.length + dom.length);
|
|
36
|
+
buf.set(mldsaPublicKey, 0);
|
|
37
|
+
buf.set(id, mldsaPublicKey.length);
|
|
38
|
+
buf.set(dom, mldsaPublicKey.length + id.length);
|
|
39
|
+
return ToilUserId.fromBytes(crypto.sha256(buf));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Build from a 32-byte digest. Bytes past 32 are ignored; a shorter array zero-pads the tail. */
|
|
43
|
+
static fromBytes(b: Uint8Array): ToilUserId {
|
|
44
|
+
if (b.length >= 32) {
|
|
45
|
+
const p = b.dataStart;
|
|
46
|
+
return new ToilUserId(load<u64>(p, 0), load<u64>(p, 8), load<u64>(p, 16), load<u64>(p, 24));
|
|
47
|
+
}
|
|
48
|
+
const tmp = new Uint8Array(32);
|
|
49
|
+
tmp.set(b, 0);
|
|
50
|
+
const p = tmp.dataStart;
|
|
51
|
+
return new ToilUserId(load<u64>(p, 0), load<u64>(p, 8), load<u64>(p, 16), load<u64>(p, 24));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** The 32-byte identity (the original SHA-256 digest bytes; wasm is little-endian, so this round-trips). */
|
|
55
|
+
toBytes(): Uint8Array {
|
|
56
|
+
const out = new Uint8Array(32);
|
|
57
|
+
const p = out.dataStart;
|
|
58
|
+
store<u64>(p, this.w0, 0);
|
|
59
|
+
store<u64>(p, this.w1, 8);
|
|
60
|
+
store<u64>(p, this.w2, 16);
|
|
61
|
+
store<u64>(p, this.w3, 24);
|
|
62
|
+
return out;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Lowercase hex, 64 chars. */
|
|
66
|
+
toHex(): string {
|
|
67
|
+
const b = this.toBytes();
|
|
68
|
+
const HEX = '0123456789abcdef';
|
|
69
|
+
let s = '';
|
|
70
|
+
for (let i = 0; i < 32; i++) {
|
|
71
|
+
const v = <i32>b[i];
|
|
72
|
+
s += HEX.charAt(v >> 4);
|
|
73
|
+
s += HEX.charAt(v & 0xf);
|
|
74
|
+
}
|
|
75
|
+
return s;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** True when unset (the all-zero id, e.g. an absent / anonymous user). */
|
|
79
|
+
isZero(): bool {
|
|
80
|
+
return (this.w0 | this.w1 | this.w2 | this.w3) == 0;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Value equality: four u64 compares, short-circuiting. O(1), no allocation. */
|
|
84
|
+
@inline
|
|
85
|
+
equals(other: ToilUserId): bool {
|
|
86
|
+
return (
|
|
87
|
+
this.w0 == other.w0 &&
|
|
88
|
+
this.w1 == other.w1 &&
|
|
89
|
+
this.w2 == other.w2 &&
|
|
90
|
+
this.w3 == other.w3
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** `a == b` value equality (overloaded). */
|
|
95
|
+
@operator('==')
|
|
96
|
+
@inline
|
|
97
|
+
eq(other: ToilUserId): bool {
|
|
98
|
+
return this.equals(other);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** `a != b` value inequality (overloaded). */
|
|
102
|
+
@operator('!=')
|
|
103
|
+
@inline
|
|
104
|
+
ne(other: ToilUserId): bool {
|
|
105
|
+
return !this.equals(other);
|
|
106
|
+
}
|
|
107
|
+
}
|
package/src/compiler/config.ts
CHANGED
|
@@ -140,6 +140,16 @@ export interface ServerConfig {
|
|
|
140
140
|
readonly email?: EmailBackendConfig;
|
|
141
141
|
/** Which layer the dev process emulates. Default `all`. */
|
|
142
142
|
readonly nodeMode?: DevNodeMode;
|
|
143
|
+
/**
|
|
144
|
+
* Built-in auth. `true` opts into the framework's post-quantum login: the
|
|
145
|
+
* build appends a shipped `@rest('auth')` controller + its `@user` shape to
|
|
146
|
+
* the toilscript entry set, so the app gets the full `/auth/register|login`
|
|
147
|
+
* API + sessions (`/auth/me`, `/auth/logout`) with no hand-written boilerplate.
|
|
148
|
+
* Default `false`. (The escape hatch `import 'toiljs/server/auth'` in
|
|
149
|
+
* `server/main.ts` does the same without this flag.) An app that opts in must
|
|
150
|
+
* NOT declare its own `@user` — the built-in owns the single per-program one.
|
|
151
|
+
*/
|
|
152
|
+
readonly auth?: boolean;
|
|
143
153
|
/** Daemon (L4) config mirror (dev / self-host). */
|
|
144
154
|
readonly daemon?: DaemonConfig;
|
|
145
155
|
/**
|
|
@@ -202,6 +212,8 @@ export interface ResolvedToilConfig {
|
|
|
202
212
|
readonly email: EmailBackendConfig | null;
|
|
203
213
|
/** Which layer the dev process emulates (dev / self-host). Default `all`. */
|
|
204
214
|
readonly nodeMode: DevNodeMode;
|
|
215
|
+
/** Whether the built-in `/auth/*` PQ-login controller is compiled + mounted. */
|
|
216
|
+
readonly auth: boolean;
|
|
205
217
|
/** Daemon (L4) config mirror (dev / self-host), every field resolved. */
|
|
206
218
|
readonly daemon: ResolvedDaemonConfig;
|
|
207
219
|
/** Self-host HTTP worker count for `toiljs start`. */
|
|
@@ -279,6 +291,7 @@ export async function loadConfig(
|
|
|
279
291
|
seo: client.seo ?? null,
|
|
280
292
|
email: user.server?.email ?? null,
|
|
281
293
|
nodeMode: resolveNodeMode(user.server?.nodeMode),
|
|
294
|
+
auth: user.server?.auth === true,
|
|
282
295
|
daemon: resolveDaemonConfig(user.server?.daemon),
|
|
283
296
|
threads: resolveThreads(user.server?.threads),
|
|
284
297
|
runtimePath: resolveRuntimePath(),
|
package/src/compiler/docs.ts
CHANGED
|
@@ -10,45 +10,23 @@ import { TOIL_DOCS } from './toil-docs.generated.js';
|
|
|
10
10
|
|
|
11
11
|
export { TOIL_DOCS };
|
|
12
12
|
|
|
13
|
-
/**
|
|
14
|
-
* One-line summaries for the pointer files' bullet list, keyed by `.toil/docs/` filename.
|
|
15
|
-
* A doc with no entry here still appears in the list (using its `# ` heading as the label),
|
|
16
|
-
* so adding a `docs/*.md` is picked up automatically without re-listing it by hand.
|
|
17
|
-
*/
|
|
18
|
-
const DOC_SUMMARIES: Record<string, string> = {
|
|
19
|
-
'index.md': 'overview and project layout',
|
|
20
|
-
'getting-started.md': 'the two halves, build/dev, and the request lifecycle',
|
|
21
|
-
'routing.md': 'file-based routing, nested layouts, loading / error files',
|
|
22
|
-
'client.md': 'the `Toil` global, Link / NavLink, router hooks',
|
|
23
|
-
'styling.md': 'CSS / Sass / Less / Stylus / Tailwind (via `toiljs configure`)',
|
|
24
|
-
'server.md': 'the toilscript server target, `@data` / `@remote` / `@rest`',
|
|
25
|
-
'ssr.md': 'server-side rendering (`ssr = true`, hole markers, the server `render`)',
|
|
26
|
-
'rpc.md': '`@service` / `@remote` and the generated typed client',
|
|
27
|
-
'data.md': 'the `@data` decorator and the binary codec',
|
|
28
|
-
'caching.md': 'the `@cache` decorator and `Response.cache(...)`',
|
|
29
|
-
'ratelimit.md': 'the `@ratelimit` decorator',
|
|
30
|
-
'auth.md': '`@auth` guards, `@user`, sessions, the client half',
|
|
31
|
-
'environment.md': '`Environment.get` / `getSecure` config + secrets',
|
|
32
|
-
'email.md': '`EmailService`, templates, provider config',
|
|
33
|
-
'cookies.md': 'the `Cookie` builder, `SecureCookies`, signing / encryption',
|
|
34
|
-
'crypto.md': 'the synchronous `crypto` global and `crypto.subtle`',
|
|
35
|
-
'time.md': '`Time.nowMillis()` / `Time.nowSeconds()`',
|
|
36
|
-
'cli.md': 'toiljs CLI commands',
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
/** First-level `# ` heading of a Markdown doc, used as a fallback bullet label. */
|
|
13
|
+
/** First-level `# ` heading of a Markdown doc, used as the bullet label. */
|
|
40
14
|
function docTitle(content: string): string {
|
|
41
15
|
const m = content.match(/^#\s+(.+)$/m);
|
|
42
16
|
return m ? m[1].trim() : '';
|
|
43
17
|
}
|
|
44
18
|
|
|
45
19
|
/**
|
|
46
|
-
* The pointer files'
|
|
47
|
-
* drifts from the docs actually written into `.toil/docs/` (no second hand-kept list).
|
|
20
|
+
* The pointer files' navigation list, derived from the generated `TOIL_DOCS` map so it never
|
|
21
|
+
* drifts from the docs actually written into `.toil/docs/` (no second hand-kept list). We list
|
|
22
|
+
* the section landing pages (each folder's `README.md`, plus the root one) rather than all
|
|
23
|
+
* fifty-plus pages; every landing links onward to its own topic pages, and
|
|
24
|
+
* `.toil/docs/README.md` is the root map.
|
|
48
25
|
*/
|
|
49
26
|
const DOC_POINTERS = Object.keys(TOIL_DOCS)
|
|
27
|
+
.filter((name) => name === 'README.md' || name.endsWith('/README.md'))
|
|
50
28
|
.map((name) => {
|
|
51
|
-
const summary =
|
|
29
|
+
const summary = docTitle(TOIL_DOCS[name]);
|
|
52
30
|
return `- \`.toil/docs/${name}\`${summary ? `, ${summary}` : ''}`;
|
|
53
31
|
})
|
|
54
32
|
.join('\n');
|
|
@@ -59,8 +37,9 @@ const POINTER_BODY = `# toiljs, AI assistant guide
|
|
|
59
37
|
This is a **toiljs** project, a full-stack React framework (React + Vite client, file-based
|
|
60
38
|
routing, and a toilscript→WebAssembly server).
|
|
61
39
|
|
|
62
|
-
**Before editing this project, read the generated documentation in \`.toil/docs
|
|
63
|
-
the conventions you must follow
|
|
40
|
+
**Before editing this project, read the generated documentation in \`.toil/docs/\`, starting at
|
|
41
|
+
\`.toil/docs/README.md\`.** It describes the conventions you must follow. The sections, each a
|
|
42
|
+
folder whose \`README.md\` links onward to its topic pages:
|
|
64
43
|
|
|
65
44
|
${DOC_POINTERS}
|
|
66
45
|
|
|
@@ -113,6 +92,10 @@ export function writeDocs(toilDir: string): void {
|
|
|
113
92
|
const dir = path.join(toilDir, 'docs');
|
|
114
93
|
fs.mkdirSync(dir, { recursive: true });
|
|
115
94
|
for (const [name, content] of Object.entries(TOIL_DOCS)) {
|
|
116
|
-
|
|
95
|
+
const dest = path.join(dir, name);
|
|
96
|
+
// Doc keys can be nested (e.g. "auth/configuration.md"), so ensure the
|
|
97
|
+
// file's parent directory exists before writing it.
|
|
98
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
99
|
+
fs.writeFileSync(dest, content);
|
|
117
100
|
}
|
|
118
101
|
}
|
package/src/compiler/generate.ts
CHANGED
|
@@ -125,8 +125,12 @@ export const TOIL_SERVER_ENV_DTS =
|
|
|
125
125
|
`declare class TwoFactorIssue { code: string; token: string; constructor(code: string, token: string); }\n` +
|
|
126
126
|
`declare class TwoFactorChallenge { token: string; status: EmailStatus; constructor(token: string, status: EmailStatus); }\n` +
|
|
127
127
|
`declare namespace TwoFactor { const DEFAULT_TTL_SECS: u64; const DEFAULT_DIGITS: i32; function setSecret(secret: Uint8Array): void; function issue(recipient: string, purpose: string, ttlSecs?: u64, digits?: i32): TwoFactorIssue; function send(recipient: string, purpose: string, ttlSecs?: u64, digits?: i32): TwoFactorChallenge; function verify(token: string, recipient: string, code: string): bool; }\n` +
|
|
128
|
-
`declare namespace AuthService { const SESSION_COOKIE: string; const USER_COOKIE: string; const LOGIN_CONTEXT: string; const PUBLIC_KEY_LEN: i32; const SIGNATURE_LEN: i32; const DEFAULT_SESSION_TTL_SECS: u64; function setSecret(secret: Uint8Array): void; function hasSession(): bool; function getSessionBytes(): Uint8Array | null; function getUser(): __ToilAuthUser | null; function mintSession(userData: Uint8Array, ttlSecs?: u64): Cookie; function clearSession(): Cookie; function userCookie(userData: Uint8Array, ttlSecs?: u64): Cookie; function clearUserCookie(): Cookie; function buildLoginMessage(sub: string, aud: string, cid: Uint8Array, nonce: Uint8Array, iat: u64, exp: u64, ciphertext: Uint8Array, memKiB: u32, iterations: u32, parallelism: u32, serverKemKeyId: Uint8Array): Uint8Array; function verifyLogin(publicKey: Uint8Array, message: Uint8Array, signature: Uint8Array): bool; const KEM_CIPHERTEXT_LEN: i32; const KEM_SECRET_KEY_LEN: i32; const KEM_PUBLIC_KEY_LEN: i32; const SHARED_SECRET_LEN: i32; const OPRF_ELEMENT_LEN: i32; const OPRF_SEED_LEN: i32; const SESSION_KEY_LABEL: string; const SERVER_CONFIRM_LABEL: string; function setOprfSeed(seed: Uint8Array): void; function setServerKemSecretKey(secretKey: Uint8Array): void; function setServerKemPublicKey(publicKey: Uint8Array): void; function serverKemKeyId(): Uint8Array; function oprfEvaluate(username: string, blinded: Uint8Array): Uint8Array; function mlkemDecapsulate(ciphertext: Uint8Array): Uint8Array; function sha256(data: Uint8Array): Uint8Array; function deriveSessionKey(sharedSecret: Uint8Array, transcriptHash: Uint8Array): Uint8Array; function serverConfirmTag(sessionKey: Uint8Array, transcriptHash: Uint8Array): Uint8Array; const REGISTER_CONTEXT: string; function buildRegisterMessage(username: string, publicKey: Uint8Array): Uint8Array; function verifyRegister(publicKey: Uint8Array, message: Uint8Array, signature: Uint8Array): bool; }\n` +
|
|
129
|
-
`interface __ToilAuthUser {}\n
|
|
128
|
+
`declare namespace AuthService { const SESSION_COOKIE: string; const USER_COOKIE: string; const LOGIN_CONTEXT: string; const PUBLIC_KEY_LEN: i32; const SIGNATURE_LEN: i32; const DEFAULT_SESSION_TTL_SECS: u64; function setSecret(secret: Uint8Array): void; function hasSession(): bool; function getSessionBytes(): Uint8Array | null; function getUser(): __ToilAuthUser | null; function userId(): ToilUserId | null; function enable(): void; function mintSession(userData: Uint8Array, ttlSecs?: u64): Cookie; function clearSession(): Cookie; function userCookie(userData: Uint8Array, ttlSecs?: u64): Cookie; function clearUserCookie(): Cookie; function buildLoginMessage(sub: string, aud: string, cid: Uint8Array, nonce: Uint8Array, iat: u64, exp: u64, ciphertext: Uint8Array, memKiB: u32, iterations: u32, parallelism: u32, serverKemKeyId: Uint8Array): Uint8Array; function verifyLogin(publicKey: Uint8Array, message: Uint8Array, signature: Uint8Array): bool; const KEM_CIPHERTEXT_LEN: i32; const KEM_SECRET_KEY_LEN: i32; const KEM_PUBLIC_KEY_LEN: i32; const SHARED_SECRET_LEN: i32; const OPRF_ELEMENT_LEN: i32; const OPRF_SEED_LEN: i32; const SESSION_KEY_LABEL: string; const SERVER_CONFIRM_LABEL: string; function setOprfSeed(seed: Uint8Array): void; function setServerKemSecretKey(secretKey: Uint8Array): void; function setServerKemPublicKey(publicKey: Uint8Array): void; function serverKemKeyId(): Uint8Array; function oprfEvaluate(username: string, blinded: Uint8Array): Uint8Array; function mlkemDecapsulate(ciphertext: Uint8Array): Uint8Array; function sha256(data: Uint8Array): Uint8Array; function deriveSessionKey(sharedSecret: Uint8Array, transcriptHash: Uint8Array): Uint8Array; function serverConfirmTag(sessionKey: Uint8Array, transcriptHash: Uint8Array): Uint8Array; const REGISTER_CONTEXT: string; function buildRegisterMessage(username: string, publicKey: Uint8Array): Uint8Array; function verifyRegister(publicKey: Uint8Array, message: Uint8Array, signature: Uint8Array): bool; }\n` +
|
|
129
|
+
`interface __ToilAuthUser {}\n` +
|
|
130
|
+
`declare class ToilUserId { w0: u64; w1: u64; w2: u64; w3: u64; constructor(w0?: u64, w1?: u64, w2?: u64, w3?: u64); static derive(mldsaPublicKey: Uint8Array, identifier: string, domain: string): ToilUserId; static fromBytes(b: Uint8Array): ToilUserId; toBytes(): Uint8Array; toHex(): string; isZero(): bool; equals(other: ToilUserId): bool; }\n` +
|
|
131
|
+
// Built-in auth (server.auth): injected by --authUser. Mints a session for the program's single @user
|
|
132
|
+
// (built-in or the app's extended one): constructs it, sets the reserved identity, encodes it.
|
|
133
|
+
`declare function __toilEncodeAuthUser(toilUserId: Uint8Array, username: string): Uint8Array;\n`;
|
|
130
134
|
|
|
131
135
|
/**
|
|
132
136
|
* Returns a `./`-prefixed, **extensionless** POSIX module specifier from `.toil` to `abs`, for use
|