toiljs 0.0.53 → 0.0.54

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 CHANGED
@@ -1,5 +1,10 @@
1
1
  # Changelog
2
2
 
3
+ ## [v0.0.53] - 2026-06-17
4
+
5
+ - No changes
6
+
7
+
3
8
  ## [v0.0.52] - 2026-06-17
4
9
 
5
10
  ### Other Changes
@@ -7,24 +7,6 @@ import { Method, Request, Response, Rest, ToilHandler } from 'toiljs/server/runt
7
7
  */
8
8
  export class AppHandler extends ToilHandler {
9
9
  public handle(req: Request): Response {
10
- // Session signing secret: set on EVERY request, BEFORE routing. The signed
11
- // session cookie is minted in one route (auth login / session dev-login) but
12
- // verified by the `@auth` gate in another, and each request runs in a FRESH
13
- // wasm instance -- so a secret configured inside a single handler is absent on
14
- // the instance that verifies `/session/me`, and the HMAC check fails (401).
15
- // Setting it here, at the one entry point every request passes through, makes
16
- // mint and verify always agree. Read from the env store with a clearly-insecure
17
- // DEV fallback so the demo runs with zero config; set AUTH_SESSION_SECRET in
18
- // `.env.secrets` for any non-throwaway use.
19
- const sessionSecret = Environment.getSecure('AUTH_SESSION_SECRET');
20
- AuthService.setSecret(
21
- Uint8Array.wrap(
22
- String.UTF8.encode(
23
- sessionSecret != null ? sessionSecret : 'toil-demo-insecure-session-secret-change-me',
24
- ),
25
- ),
26
- );
27
-
28
10
  // Rest.dispatch returns the first matching route's Response, or null if nothing
29
11
  // matched - then we fall through to our own logic. REST composes; it never takes
30
12
  // over handle().
@@ -36,38 +36,10 @@ const DEMO_PAR: u32 = 1;
36
36
  const CHALLENGE_TTL_SECS: u64 = 120;
37
37
  const SESSION_TTL_SECS: u64 = 3600;
38
38
 
39
- // DEV fallback for the server ML-KEM-768 secret (decapsulation) key, hex, used
40
- // only when `AUTH_KEM_SK` is not set in `.env.secrets`. Matches the PINNED public
41
- // key in `src/client/auth.ts`. A real deployment sets AUTH_KEM_SK (and pins its
42
- // own public key in the client) and rotates it; never ship a real one here.
43
- const DEMO_KEM_SK_HEX: string = '3156a8eb11c62bdb4af9fc57bef470f880ae340373bcc61662748a9742a639b9ad6bc55a77a82e0caa99ede237b4783ce70ab08ecc5802a9478c4ca3de67acd7a2147db43fdba408e9765443f37e9e90cc09f836d53879b890126bd6c33d55a6d97636a28ba10e18ac919aa9d37c2e4d07b6c930a5cb3238c8338fbb1abe7dac124c93462ebc5ae81cb132947993a74f9602610eab68b7fc9407b58e958aca054443246240c484c650962408168632c303cfc738d3b918ee04a37c2436b6f7300b8c6e7bd528bc5c229673c3a1bc4ae4265772f654ed8377b285626c67a4ef715a5a04a56804c3fae93ca5e3219cd68649622ee0d77bcb664a68e377260a3a38c2739b81c3c9ec510b66acde5041f3b52922a17019dc9afaec71c3e3c3102686ceb019da138b22463ad7f452640526d1d8b21c9111ca844149d1391c937b84287f1a228342c06ccb87c31cb14227e175007c5c4497c11e8647377234a84ab2640aa8ee7acb54954f99155cf7d768446b104ac149f59ca1d0029401570db9341c93db0041d52fbbd62726a75f9ab177e4ea5176e675d28a1f9852c28b38074c91cec8064b6ba116db8b59c0434fbd1b207cd921fbf29b06740b53c7304b17b253652ad469b2cb10bf7ed3bcc5b1b6168c2d30a889f67a01ae79455100ac582ba2f764a4a4b134b9115d7c548032d55d4916ce25c0ce7c42160e446298fb10f747302e781a70b2b7962b0b54f3c0e3a4677e99cc02e41e66b0861d02d072b94ce3f8a04fd20d2ec220cea3737922808f00080186421e60b7d1076e5ca40099d54da33033021349e31bb65e12aa259b37bc975582aa6441ab2fabdc9cee0aab0c11c7e3489b93bab26e13bf399ab8a37949baba3c2f8a94fd97a9a551c96d582b5c1ba97b4547701656ee02567dd6a8362c1043c5874760c7d1133292f05c9d3689beccb903d4bd65f09e3e3255d0229daf9050ebaa107e51371fc9248393239575466a9c45b4a239e1b29b07d9701cf1bb488a95a004a98fcb1f6d548cc8554a3eb25a5fc90892618e5d33b04938567e748ab9ba79b0d39d611864b2140666c1791e79c5c0943a03038f7306551db3b271b08dec32443ae14674e16d6c42956ef36499348e7424bbc4883c37675a4f8bb28cd68f30b532ba80104e7214b9a4886045a152d161821a006ae03ae3742e36f63d997c858b850119e1004f4022a04a9533749d993641763a83dce5256f3826ae9b0584c72d69c77d6784444737a0192789e0d63a2f2808ce88b07c33383e588f68b13b892ac6998c9f2db14ba3e10eee4b9717761efc298e026974231a143b89009a724a7121292bb9292662b87502beadb9cbea3cc89de1997b376575f466b6693e18eb70630ba1823cae5f03698ae662190207156ca8d1a4a3cb926d20c92b524180c0804f057491c292024641bf9b21b52214bf2a2b42d16596e22935317bc712e64f64c143b257ca6f663223a1a2b6537b55746a2a739b2adbbfa004354a1555cc8b8215aa06413b27b7fa8c860386c13876b8d55b743860a13c0005dc4ac5e003cd3431c7a29edcc73c50b991e56a12423ac1f2842ed2999b7b31b6e01aaa83c01af658bae959b2cb256f1e7bba29d765e8083182891302569b3712a856e564fdd484b0706b0c68568d5ab7edc742cf74459d64595455a60f267973aa55e43c5be61925a3822eafcca445e36dc4655636e31e6fc9bec338b253f94290008ef7f40dbddb49c15c690f6755a23a1b3c85cfd5207e71a607086a6fc6d74a05080f43276901a19cafdb8de7771d58ea07f0f1056b905127b22223d08e75173199f13ab13c5dcd3b51ac784f84e520484a262b845a897c41cf27324ab6ba545c78c9ccab361051e0bba53498af26240fa0d566d1572684f4b42e253e6d052c848650915063c35641e1121ef8d9cfd17b667b351103c56d195007c9376d0c08aa268396814490eab4c364175a94533267a1933862cc4c33bcf0a13d1fa2b9d6c5082eeca1480672f2526cbe013beff14dc908a386e0b633c8761023cbed760deac6709bc328d865ac82e12307b673d96711dbb27a4d939230d25b53d594169a318be0200fa33550e9418e2a3b30e9719edc09d5fc4306f1abfd021eab14637a8a72c5931d25dc9b56db0e6ab677522b10f25307dbb804a6774ce05b87b0976a4b227bfe6caf20a79e64004fbd27b1eea018b3ab8ffa629f2dc87f19278f95168e94e44660a3370c537795678eb2f056260609769740583b51b291862927a1938737c6a37f40b78f00671cccbcb88ac3427b37915ed58782998f84051647707d48995472baad3f64a7cca54e1c0734db08751c614a34f28b84f2c1b5a6817355ab61957c486b7acffbc092bc8a7b46387f33b53ed372f7168d31a71cd008539928b0cdf91e835aa97f6a2be6d327b87a6ae478701d75a59a25179cb14997bb2552853014724170a1c49b82c2bcebc3279024e1fa44c53c7afdc43f0bd22116490f3b74c90e7296be58b9a91168f2fa0c3d378a3bcac959f357825c9976a8c9ee944f29b45e96d7345d9b478431a20cf1c5d3a3227c717fd204619777636c0cb140db5c50d2a3302334461030bee34e4eb1a6f02b733f9ccda4290fa168bc039568373241542728d00030d1f251e83737cb215adbdc1de75978675a0cd0d75b12748abdda7a9852629c63697d145af2c69854b06e03f37c4b064e4c9a4c03f2ad4d081e70180e9547247921918118086b62b4f7727f46b24e3e79ba3f28209f32b5102035bf935856232f83642268c0292ec6bf8e9462382163d30a20b4bcb7b4439310ec9d0a148193907fc07697342967cf1a16c6b3c71558951fa915400736cf699262b54b723abb2ecc27b74b68ee494287595ef818388adb49e883c67bfa5c226c0eef037a0851a29d34675912c1ea1068310b6dfcd017c809c8fbfc2c3ae78dfef07299960eeefba182662a90fa422c1790f356a2ea909012b15623a9b9e450a282cb530589a68368b3583159d9010ac3e52cc974753c342e58279516339dfb691df94b13a223ad97eb6a09c21dafe6304a3642d6d2067b5238497661fe88ad1227ca3557be2a576b6e17c5a7f997ea07929e76407e376aba74c44cd8504804776f39bbb8327624188a63501e83b404d9438cade0b11dc3ac61856447fb072b91761c228878f01b2eb6b4b21ba664c2c75882431603b25a449ffeb8410b910558581777562aa9b2181fd9c04713ad9326462d3e842121c4997f9aa932417c67851625816de66e0d65637434629f39d157cc40cbafccc4429c35caeda482299013baf565d0f38b8f2886b9641ae6bea5b2bfccd9e6f3000d1a2734414e5b6875828f9ca9b6c3d0ddeaf704111e2b38';
44
-
45
-
46
39
  function utf8(s: string): Uint8Array {
47
40
  return Uint8Array.wrap(String.UTF8.encode(s));
48
41
  }
49
42
 
50
- /** A secret from the env store (`Environment.getSecure`, backed by `.env.secrets`),
51
- * falling back to a DEV default so the example runs with zero config. Set the
52
- * real value in `.env.secrets` for any non-throwaway use. */
53
- function envSecretOr(key: string, devDefault: string): string {
54
- const v = Environment.getSecure(key);
55
- return v != null ? v : devDefault;
56
- }
57
-
58
- function fromHex(s: string): Uint8Array {
59
- const out = new Uint8Array(s.length >> 1);
60
- for (let i = 0; i < out.length; i++) {
61
- out[i] = <u8>(hexNibble(s.charCodeAt(i * 2)) * 16 + hexNibble(s.charCodeAt(i * 2 + 1)));
62
- }
63
- return out;
64
- }
65
- function hexNibble(c: i32): i32 {
66
- if (c >= 48 && c <= 57) return c - 48; // 0-9
67
- if (c >= 97 && c <= 102) return c - 87; // a-f
68
- if (c >= 65 && c <= 70) return c - 55; // A-F
69
- return 0;
70
- }
71
43
  function toHex(b: Uint8Array): string {
72
44
  let s = '';
73
45
  for (let i = 0; i < b.length; i++) {
@@ -105,34 +77,6 @@ function deriveSalt(username: string): Uint8Array {
105
77
  }
106
78
 
107
79
 
108
- let __configured = false;
109
- /** Lazily configure the auth-route crypto material (OPRF seed + server ML-KEM
110
- * key) on first use; only the register/login handlers below read these.
111
- *
112
- * The session HMAC secret is deliberately NOT set here. The session is verified
113
- * by the `@auth` gate in `routes/Session` (a DIFFERENT route -> a different fresh
114
- * wasm instance per request), so it must be configured for EVERY request, not
115
- * just auth ones. That happens once, before routing, in `core/AppHandler`. */
116
- function ensureConfigured(): void {
117
- if (__configured) return;
118
- // Both secrets come from the env store (`.env.secrets`), with DEV fallbacks
119
- // so the example runs with zero config. Set the real values in `.env.secrets`
120
- // (see `.env.secrets.example`) for any non-throwaway use.
121
-
122
- // OPRF master seed: hashed to a 32-byte (RFC 9497 Ns) seed so any env value
123
- // works. Per-user OPRF keys derive from this + the username.
124
- AuthService.setOprfSeed(crypto.sha256Text(envSecretOr('AUTH_OPRF_SEED', 'toil-demo-oprf-seed-v1')));
125
- // Server static ML-KEM secret key (must match the client's pinned public key).
126
- const sk = fromHex(envSecretOr('AUTH_KEM_SK', DEMO_KEM_SK_HEX));
127
- AuthService.setServerKemSecretKey(sk);
128
- // The ML-KEM-768 public key (ek) is embedded in the decapsulation key at
129
- // bytes [1152, 2336) (FIPS 203 dk layout); use it for the key id the login
130
- // message binds. Identical to the public key the client pins.
131
- AuthService.setServerKemPublicKey(sk.slice(1152, 2336));
132
- __configured = true;
133
- }
134
-
135
-
136
80
  // @ts-ignore: decorator
137
81
  @external('env', 'kv.put')
138
82
  declare function __kvPut(keyPtr: usize, keyLen: i32, valPtr: usize, valLen: i32): void;
@@ -240,7 +184,6 @@ class Auth {
240
184
  * No taken-oracle: always succeeds; register/finish rejects a duplicate. */
241
185
  @post('/register/start')
242
186
  public registerStart(ctx: RouteContext): Response {
243
- ensureConfigured();
244
187
  const r = new DataReader(ctx.request.body);
245
188
  const username = r.readString();
246
189
  const blinded = r.readBytes();
@@ -263,7 +206,6 @@ class Auth {
263
206
  * proof-of-possession before storing the key. */
264
207
  @post('/register/finish')
265
208
  public registerFinish(ctx: RouteContext): Response {
266
- ensureConfigured();
267
209
  const r = new DataReader(ctx.request.body);
268
210
  const username = r.readString();
269
211
  const pk = r.readBytes();
@@ -300,7 +242,6 @@ class Auth {
300
242
  * and a fresh challenge -- a known and an unknown user are indistinguishable. */
301
243
  @post('/login/start')
302
244
  public loginStart(ctx: RouteContext): Response {
303
- ensureConfigured();
304
245
  const r = new DataReader(ctx.request.body);
305
246
  const username = r.readString();
306
247
  const blinded = r.readBytes();
@@ -344,7 +285,6 @@ class Auth {
344
285
  * resp: u8(status) [+ bytes(sessionToken) bytes(serverConfirm)] + Set-Cookie */
345
286
  @post('/login/finish')
346
287
  public loginFinish(ctx: RouteContext): Response {
347
- ensureConfigured();
348
288
  const r = new DataReader(ctx.request.body);
349
289
  const cid = r.readBytes();
350
290
  const ct = r.readBytes();
@@ -16,11 +16,11 @@ import { DataReader, DataWriter } from 'data';
16
16
  * this `/session/dev-login` mints one for a caller-named demo user so the flow is
17
17
  * runnable without the external account store the login example stubs out.
18
18
  *
19
- * The session secret is configured once per request, BEFORE routing, in
20
- * `core/AppHandler` (every request runs in a fresh wasm instance, so the secret
21
- * must be set on each one, and the mint side and this `@auth` verify side are
22
- * different routes). It reads `AUTH_SESSION_SECRET` from the env store with a
23
- * clearly-insecure DEV fallback; a real deployment sets it in `.env.secrets`.
19
+ * No secret wiring is needed: `AuthService` reads `AUTH_SESSION_SECRET` from the
20
+ * env store automatically (with a clearly-insecure DEV fallback), so the cookie
21
+ * minted on login and re-verified here by the `@auth` gate always agree, even
22
+ * though each request runs in its own fresh wasm instance. A real deployment just
23
+ * sets `AUTH_SESSION_SECRET` in `.env.secrets`.
24
24
  */
25
25
 
26
26
  // @user: the authenticated-user shape. Exactly one per program.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "toiljs",
3
3
  "type": "module",
4
- "version": "0.0.53",
4
+ "version": "0.0.54",
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": {
@@ -70,39 +70,90 @@ declare function __toilVoprfEvaluate(
70
70
  outPtr: usize,
71
71
  ): i32;
72
72
 
73
- // HMAC key for signing session cookies. The SAME secret must be configured on
74
- // every edge instance (a sealed cookie minted by one is opened by another) and
75
- // must NEVER reach the client. There is no host-config secret mechanism yet, so
76
- // the tenant supplies one at startup via `AuthService.setSecret(...)` (a
77
- // build-time constant is consistent across instances). The default below is a
78
- // well-known DEV placeholder: a deployment that does not call `setSecret` gets a
79
- // loud, insecure-but-functional session so local dev works out of the box.
80
- // TODO(secret): replace with a per-deployment host-config secret.
81
- let __sessionSecret: Uint8Array = Uint8Array.wrap(
82
- String.UTF8.encode('toil-dev-insecure-session-secret-CHANGE-ME'),
83
- );
84
-
85
- // OPRF master seed (RFC 9497 DeriveKeyPair input). Per-user OPRF keys are
86
- // derived from this + the username, so it is a server secret of the same
87
- // sensitivity as a password-hash pepper: a leak enables an offline dictionary
88
- // attack (but precomputation stays impossible until it leaks). Configured at
89
- // startup via `AuthService.setOprfSeed`; the DEV default below is well-known.
90
- // 32 bytes (RFC 9497 Ns for ristretto255); `setOprfSeed` MUST also pass 32.
91
- let __oprfSeed: Uint8Array = Uint8Array.wrap(
92
- String.UTF8.encode('toil-dev-oprf-seedXXCHANGE-ME-32'),
93
- );
94
-
95
- // Server static ML-KEM-768 secret (decapsulation) key. The matching public key
96
- // is PINNED in the client; only the holder of this key can decapsulate, so a
97
- // correct shared secret authenticates the server. Configured at startup via
98
- // `AuthService.setServerKemSecretKey` (2400 bytes). Empty until set; mutual-auth
99
- // calls fail closed if unset.
100
- let __serverKemSk: Uint8Array = new Uint8Array(0);
101
-
102
- // Server static ML-KEM-768 PUBLIC (encapsulation) key, used only to compute the
103
- // key identity bound into the login transcript (`serverKemKeyId`). The client
104
- // pins the same key. Configured at startup via `setServerKemPublicKey`.
105
- let __serverKemPk: Uint8Array = new Uint8Array(0);
73
+ // Secret configuration is AUTOMATIC. Every secret below resolves on first use
74
+ // from, in order: (1) an explicit `set*()` override, (2) the tenant's env store
75
+ // (`Environment.getSecure`, backed by `.env.secrets` / the dashboard) under the
76
+ // key named below, then (3) a well-known, clearly-insecure DEV fallback so local
77
+ // dev and the examples run with zero config. The resolved value is cached in the
78
+ // module global. Because every request runs in a FRESH wasm instance that reads
79
+ // the SAME env value the SAME way, there is no per-route / per-instance secret to
80
+ // keep in sync by hand: a cookie minted by one route can never fail to verify in
81
+ // another for want of a `setSecret` call. A real deployment sets the env values
82
+ // (and pins its own server KEM public key in the client); the DEV fallbacks are
83
+ // never a place to put a real secret.
84
+
85
+ /** Env-store keys the framework reads automatically (tenant-set secrets). */
86
+ const ENV_SESSION_SECRET: string = 'AUTH_SESSION_SECRET';
87
+ const ENV_OPRF_SEED: string = 'AUTH_OPRF_SEED';
88
+ const ENV_KEM_SK: string = 'AUTH_KEM_SK';
89
+
90
+ /** Well-known DEV fallbacks (insecure; overridden by env or `set*()`). */
91
+ const DEV_SESSION_SECRET: string = 'toil-dev-insecure-session-secret-CHANGE-ME';
92
+ const DEV_OPRF_SEED_SRC: string = 'toil-dev-oprf-seed-v1';
93
+ // A well-known DEV ML-KEM-768 decapsulation key (hex). Its public half (the `ek`
94
+ // at bytes [1152, 2336) of the dk) is PINNED in the client (`src/client/auth.ts`,
95
+ // `SERVER_KEM_PUBLIC_KEY`), so the PQ-auth example runs with zero config. A real
96
+ // deployment sets `AUTH_KEM_SK` and pins its own public key; never treat this as
97
+ // secret. Tree-shaken away unless a server actually calls the ML-KEM path.
98
+ const DEV_KEM_SK_HEX: string = '3156a8eb11c62bdb4af9fc57bef470f880ae340373bcc61662748a9742a639b9ad6bc55a77a82e0caa99ede237b4783ce70ab08ecc5802a9478c4ca3de67acd7a2147db43fdba408e9765443f37e9e90cc09f836d53879b890126bd6c33d55a6d97636a28ba10e18ac919aa9d37c2e4d07b6c930a5cb3238c8338fbb1abe7dac124c93462ebc5ae81cb132947993a74f9602610eab68b7fc9407b58e958aca054443246240c484c650962408168632c303cfc738d3b918ee04a37c2436b6f7300b8c6e7bd528bc5c229673c3a1bc4ae4265772f654ed8377b285626c67a4ef715a5a04a56804c3fae93ca5e3219cd68649622ee0d77bcb664a68e377260a3a38c2739b81c3c9ec510b66acde5041f3b52922a17019dc9afaec71c3e3c3102686ceb019da138b22463ad7f452640526d1d8b21c9111ca844149d1391c937b84287f1a228342c06ccb87c31cb14227e175007c5c4497c11e8647377234a84ab2640aa8ee7acb54954f99155cf7d768446b104ac149f59ca1d0029401570db9341c93db0041d52fbbd62726a75f9ab177e4ea5176e675d28a1f9852c28b38074c91cec8064b6ba116db8b59c0434fbd1b207cd921fbf29b06740b53c7304b17b253652ad469b2cb10bf7ed3bcc5b1b6168c2d30a889f67a01ae79455100ac582ba2f764a4a4b134b9115d7c548032d55d4916ce25c0ce7c42160e446298fb10f747302e781a70b2b7962b0b54f3c0e3a4677e99cc02e41e66b0861d02d072b94ce3f8a04fd20d2ec220cea3737922808f00080186421e60b7d1076e5ca40099d54da33033021349e31bb65e12aa259b37bc975582aa6441ab2fabdc9cee0aab0c11c7e3489b93bab26e13bf399ab8a37949baba3c2f8a94fd97a9a551c96d582b5c1ba97b4547701656ee02567dd6a8362c1043c5874760c7d1133292f05c9d3689beccb903d4bd65f09e3e3255d0229daf9050ebaa107e51371fc9248393239575466a9c45b4a239e1b29b07d9701cf1bb488a95a004a98fcb1f6d548cc8554a3eb25a5fc90892618e5d33b04938567e748ab9ba79b0d39d611864b2140666c1791e79c5c0943a03038f7306551db3b271b08dec32443ae14674e16d6c42956ef36499348e7424bbc4883c37675a4f8bb28cd68f30b532ba80104e7214b9a4886045a152d161821a006ae03ae3742e36f63d997c858b850119e1004f4022a04a9533749d993641763a83dce5256f3826ae9b0584c72d69c77d6784444737a0192789e0d63a2f2808ce88b07c33383e588f68b13b892ac6998c9f2db14ba3e10eee4b9717761efc298e026974231a143b89009a724a7121292bb9292662b87502beadb9cbea3cc89de1997b376575f466b6693e18eb70630ba1823cae5f03698ae662190207156ca8d1a4a3cb926d20c92b524180c0804f057491c292024641bf9b21b52214bf2a2b42d16596e22935317bc712e64f64c143b257ca6f663223a1a2b6537b55746a2a739b2adbbfa004354a1555cc8b8215aa06413b27b7fa8c860386c13876b8d55b743860a13c0005dc4ac5e003cd3431c7a29edcc73c50b991e56a12423ac1f2842ed2999b7b31b6e01aaa83c01af658bae959b2cb256f1e7bba29d765e8083182891302569b3712a856e564fdd484b0706b0c68568d5ab7edc742cf74459d64595455a60f267973aa55e43c5be61925a3822eafcca445e36dc4655636e31e6fc9bec338b253f94290008ef7f40dbddb49c15c690f6755a23a1b3c85cfd5207e71a607086a6fc6d74a05080f43276901a19cafdb8de7771d58ea07f0f1056b905127b22223d08e75173199f13ab13c5dcd3b51ac784f84e520484a262b845a897c41cf27324ab6ba545c78c9ccab361051e0bba53498af26240fa0d566d1572684f4b42e253e6d052c848650915063c35641e1121ef8d9cfd17b667b351103c56d195007c9376d0c08aa268396814490eab4c364175a94533267a1933862cc4c33bcf0a13d1fa2b9d6c5082eeca1480672f2526cbe013beff14dc908a386e0b633c8761023cbed760deac6709bc328d865ac82e12307b673d96711dbb27a4d939230d25b53d594169a318be0200fa33550e9418e2a3b30e9719edc09d5fc4306f1abfd021eab14637a8a72c5931d25dc9b56db0e6ab677522b10f25307dbb804a6774ce05b87b0976a4b227bfe6caf20a79e64004fbd27b1eea018b3ab8ffa629f2dc87f19278f95168e94e44660a3370c537795678eb2f056260609769740583b51b291862927a1938737c6a37f40b78f00671cccbcb88ac3427b37915ed58782998f84051647707d48995472baad3f64a7cca54e1c0734db08751c614a34f28b84f2c1b5a6817355ab61957c486b7acffbc092bc8a7b46387f33b53ed372f7168d31a71cd008539928b0cdf91e835aa97f6a2be6d327b87a6ae478701d75a59a25179cb14997bb2552853014724170a1c49b82c2bcebc3279024e1fa44c53c7afdc43f0bd22116490f3b74c90e7296be58b9a91168f2fa0c3d378a3bcac959f357825c9976a8c9ee944f29b45e96d7345d9b478431a20cf1c5d3a3227c717fd204619777636c0cb140db5c50d2a3302334461030bee34e4eb1a6f02b733f9ccda4290fa168bc039568373241542728d00030d1f251e83737cb215adbdc1de75978675a0cd0d75b12748abdda7a9852629c63697d145af2c69854b06e03f37c4b064e4c9a4c03f2ad4d081e70180e9547247921918118086b62b4f7727f46b24e3e79ba3f28209f32b5102035bf935856232f83642268c0292ec6bf8e9462382163d30a20b4bcb7b4439310ec9d0a148193907fc07697342967cf1a16c6b3c71558951fa915400736cf699262b54b723abb2ecc27b74b68ee494287595ef818388adb49e883c67bfa5c226c0eef037a0851a29d34675912c1ea1068310b6dfcd017c809c8fbfc2c3ae78dfef07299960eeefba182662a90fa422c1790f356a2ea909012b15623a9b9e450a282cb530589a68368b3583159d9010ac3e52cc974753c342e58279516339dfb691df94b13a223ad97eb6a09c21dafe6304a3642d6d2067b5238497661fe88ad1227ca3557be2a576b6e17c5a7f997ea07929e76407e376aba74c44cd8504804776f39bbb8327624188a63501e83b404d9438cade0b11dc3ac61856447fb072b91761c228878f01b2eb6b4b21ba664c2c75882431603b25a449ffeb8410b910558581777562aa9b2181fd9c04713ad9326462d3e842121c4997f9aa932417c67851625816de66e0d65637434629f39d157cc40cbafccc4429c35caeda482299013baf565d0f38b8f2886b9641ae6bea5b2bfccd9e6f3000d1a2734414e5b6875828f9ca9b6c3d0ddeaf704111e2b38';
99
+
100
+ // Resolved-and-cached per instance; `null` = not yet resolved and not overridden.
101
+ let __sessionSecret: Uint8Array | null = null;
102
+ let __oprfSeed: Uint8Array | null = null;
103
+ let __serverKemSk: Uint8Array | null = null;
104
+ let __serverKemPk: Uint8Array | null = null;
105
+
106
+ function __hexNibble(c: i32): i32 {
107
+ if (c >= 48 && c <= 57) return c - 48; // 0-9
108
+ if (c >= 97 && c <= 102) return c - 87; // a-f
109
+ if (c >= 65 && c <= 70) return c - 55; // A-F
110
+ return 0;
111
+ }
112
+ function __fromHex(s: string): Uint8Array {
113
+ const out = new Uint8Array(s.length >> 1);
114
+ for (let i = 0; i < out.length; i++) {
115
+ out[i] = <u8>((__hexNibble(s.charCodeAt(i * 2)) << 4) | __hexNibble(s.charCodeAt(i * 2 + 1)));
116
+ }
117
+ return out;
118
+ }
119
+
120
+ /** The session HMAC secret (UTF-8 of the env value or the DEV fallback). */
121
+ function __resolveSessionSecret(): Uint8Array {
122
+ let s = __sessionSecret;
123
+ if (s != null) return s;
124
+ const v = Environment.getSecure(ENV_SESSION_SECRET);
125
+ s = Uint8Array.wrap(String.UTF8.encode(v != null ? v : DEV_SESSION_SECRET));
126
+ __sessionSecret = s;
127
+ return s;
128
+ }
129
+ /** The OPRF master seed, hashed to 32 bytes (RFC 9497 Ns) so any env value works. */
130
+ function __resolveOprfSeed(): Uint8Array {
131
+ let s = __oprfSeed;
132
+ if (s != null) return s;
133
+ const v = Environment.getSecure(ENV_OPRF_SEED);
134
+ s = crypto.sha256Text(v != null ? v : DEV_OPRF_SEED_SRC);
135
+ __oprfSeed = s;
136
+ return s;
137
+ }
138
+ /** The server static ML-KEM-768 secret (decapsulation) key (2400 bytes). */
139
+ function __resolveServerKemSk(): Uint8Array {
140
+ let s = __serverKemSk;
141
+ if (s != null) return s;
142
+ const v = Environment.getSecure(ENV_KEM_SK);
143
+ s = __fromHex(v != null ? v : DEV_KEM_SK_HEX);
144
+ __serverKemSk = s;
145
+ return s;
146
+ }
147
+ /** The server static ML-KEM-768 PUBLIC key (the `ek` embedded in the dk at bytes
148
+ * [1152, 2336), FIPS 203 layout), used for the key id bound into the login
149
+ * transcript. The client pins the same key. */
150
+ function __resolveServerKemPk(): Uint8Array {
151
+ let s = __serverKemPk;
152
+ if (s != null) return s;
153
+ s = __resolveServerKemSk().slice(1152, 2336);
154
+ __serverKemPk = s;
155
+ return s;
156
+ }
106
157
 
107
158
  // HMAC-SHA256(key, msg) via the ambient Web Crypto (same path SecureCookies
108
159
  // uses). The session-key derivation and the mutual-auth confirmation tag are
@@ -165,9 +216,10 @@ export namespace AuthService {
165
216
  export const DEFAULT_SESSION_TTL_SECS: u64 = 86400; // 24h
166
217
 
167
218
  /**
168
- * Configure the server secret used to sign session cookies. Call once at
169
- * startup from the tenant's `main.ts`. Must be identical on every edge
170
- * instance and kept out of any client bundle.
219
+ * Override the session-signing secret programmatically. OPTIONAL: by default
220
+ * AuthService reads `AUTH_SESSION_SECRET` from the env store (with a DEV
221
+ * fallback), so most apps never call this. An override takes precedence over
222
+ * the env value for the current request; keep it out of any client bundle.
171
223
  */
172
224
  export function setSecret(secret: Uint8Array): void {
173
225
  __sessionSecret = secret;
@@ -183,7 +235,7 @@ export namespace AuthService {
183
235
  const req = Server.currentRequest;
184
236
  if (req == null) return null;
185
237
 
186
- const sealed = SecureCookies.signed(__sessionSecret).open(
238
+ const sealed = SecureCookies.signed(__resolveSessionSecret()).open(
187
239
  req.cookies(),
188
240
  sessionCookieName(__reqIsSecure()),
189
241
  );
@@ -246,7 +298,7 @@ export namespace AuthService {
246
298
  .sameSite(SameSite.Lax)
247
299
  .maxAge(<i64>ttlSecs);
248
300
  cookie = secure ? cookie.asHostPrefixed() : cookie.path('/');
249
- return SecureCookies.signed(__sessionSecret).seal(cookie);
301
+ return SecureCookies.signed(__resolveSessionSecret()).seal(cookie);
250
302
  }
251
303
 
252
304
  /** A `Set-Cookie` that immediately clears the session (logout). */
@@ -384,28 +436,30 @@ export namespace AuthService {
384
436
  export const OPRF_SEED_LEN: i32 = 32;
385
437
 
386
438
  /**
387
- * Configure the OPRF master seed (32 bytes). Per-user OPRF keys are derived
388
- * from this + the username. Call once at startup from `main.ts`; identical
389
- * on every instance, kept out of any client bundle.
439
+ * Override the OPRF master seed (32 bytes) programmatically. OPTIONAL: by
440
+ * default AuthService reads `AUTH_OPRF_SEED` from the env store (hashed to 32
441
+ * bytes, with a DEV fallback). Per-user OPRF keys derive from this + the
442
+ * username; keep it out of any client bundle.
390
443
  */
391
444
  export function setOprfSeed(seed: Uint8Array): void {
392
445
  __oprfSeed = seed;
393
446
  }
394
447
 
395
448
  /**
396
- * Configure the server static ML-KEM-768 secret (decapsulation) key (2400
397
- * bytes). The matching public key is pinned in the client. Call once at
398
- * startup; identical on every instance, never in a client bundle.
449
+ * Override the server static ML-KEM-768 secret (decapsulation) key (2400
450
+ * bytes) programmatically. OPTIONAL: by default AuthService reads `AUTH_KEM_SK`
451
+ * (hex) from the env store, with a DEV fallback whose public half is pinned in
452
+ * the client. Never put this in a client bundle.
399
453
  */
400
454
  export function setServerKemSecretKey(secretKey: Uint8Array): void {
401
455
  __serverKemSk = secretKey;
402
456
  }
403
457
 
404
458
  /**
405
- * Configure the server static ML-KEM-768 PUBLIC key (1184 bytes), used to
406
- * compute {@link serverKemKeyId}. Must be the key the client pins. (It is the
407
- * `ek` embedded in the decapsulation key, so a tenant can pass
408
- * `secretKey.slice(1152, 2336)` rather than store it twice.)
459
+ * Override the server static ML-KEM-768 PUBLIC key (1184 bytes), used to
460
+ * compute {@link serverKemKeyId}. OPTIONAL: by default it is derived from the
461
+ * secret key (the `ek` embedded at bytes [1152, 2336) of the dk), so setting
462
+ * the secret key is enough. Must be the key the client pins.
409
463
  */
410
464
  export function setServerKemPublicKey(publicKey: Uint8Array): void {
411
465
  __serverKemPk = publicKey;
@@ -421,9 +475,10 @@ export namespace AuthService {
421
475
  if (blinded.length != OPRF_ELEMENT_LEN) return new Uint8Array(0);
422
476
  const info = Uint8Array.wrap(String.UTF8.encode(username));
423
477
  const out = new Uint8Array(OPRF_ELEMENT_LEN);
478
+ const seed = __resolveOprfSeed();
424
479
  const rc = __toilVoprfEvaluate(
425
- __oprfSeed.dataStart,
426
- __oprfSeed.length,
480
+ seed.dataStart,
481
+ seed.length,
427
482
  info.dataStart,
428
483
  info.length,
429
484
  blinded.dataStart,
@@ -439,15 +494,16 @@ export namespace AuthService {
439
494
  * Only the genuine server can produce this, so it underpins mutual auth.
440
495
  */
441
496
  export function mlkemDecapsulate(ciphertext: Uint8Array): Uint8Array {
442
- if (__serverKemSk.length != KEM_SECRET_KEY_LEN || ciphertext.length != KEM_CIPHERTEXT_LEN) {
497
+ const sk = __resolveServerKemSk();
498
+ if (sk.length != KEM_SECRET_KEY_LEN || ciphertext.length != KEM_CIPHERTEXT_LEN) {
443
499
  return new Uint8Array(0);
444
500
  }
445
501
  const out = new Uint8Array(SHARED_SECRET_LEN);
446
502
  const rc = __toilMlkemDecapsulate(
447
503
  ciphertext.dataStart,
448
504
  ciphertext.length,
449
- __serverKemSk.dataStart,
450
- __serverKemSk.length,
505
+ sk.dataStart,
506
+ sk.length,
451
507
  out.dataStart,
452
508
  );
453
509
  return rc == 0 ? out : new Uint8Array(0);
@@ -462,7 +518,7 @@ export namespace AuthService {
462
518
  * message, so the signature commits to which server key the client
463
519
  * encapsulated to. The client computes the same hash over its pinned key. */
464
520
  export function serverKemKeyId(): Uint8Array {
465
- return sha256(__serverKemPk);
521
+ return sha256(__resolveServerKemPk());
466
522
  }
467
523
 
468
524
  /** Domain separators for the session-key derivation and confirmation tag. */