toiljs 0.0.53 → 0.0.55

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.
@@ -1,5 +1,6 @@
1
1
  import { Response, RouteContext } from 'toiljs/server/runtime';
2
2
  import { DataReader, DataWriter } from 'data';
3
+ import { Record } from 'toildb';
3
4
 
4
5
  import { encodeSessionUser } from './Session';
5
6
 
@@ -15,11 +16,11 @@ import { encodeSessionUser } from './Session';
15
16
  * auth). See `server/globals/auth.ts` (the `AuthService` global) and the client
16
17
  * half in `toiljs/client` (`Auth.register` / `Auth.login`).
17
18
  *
18
- * STORAGE: backed by the DEV-ONLY `kv.*` host imports (see
19
- * `src/devserver/kv.ts`) so the register -> login chain spans requests under
20
- * `toiljs dev`. REMOVE LATER: this is a stand-in; once toildb is implemented,
21
- * the Accounts/Challenges stores move onto it and this goes away. `kv.*` is not
22
- * on the production edge.
19
+ * STORAGE: backed by ToilDB (`@database AuthDb`). Accounts are a `record`
20
+ * collection keyed by username; login challenges are a `record` consumed exactly
21
+ * once with `getDelete` (atomic fetch-and-delete). The dev server emulates these
22
+ * `env.data.*` host imports in process (so register -> login spans requests under
23
+ * `toiljs dev`); the production edge backs the SAME API with ScyllaDB.
23
24
  *
24
25
  * Wire: every body/response is binary (`DataWriter`/`DataReader`), never JSON.
25
26
  */
@@ -36,47 +37,6 @@ const DEMO_PAR: u32 = 1;
36
37
  const CHALLENGE_TTL_SECS: u64 = 120;
37
38
  const SESSION_TTL_SECS: u64 = 3600;
38
39
 
39
- // 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
- function utf8(s: string): Uint8Array {
47
- return Uint8Array.wrap(String.UTF8.encode(s));
48
- }
49
-
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
- function toHex(b: Uint8Array): string {
72
- let s = '';
73
- for (let i = 0; i < b.length; i++) {
74
- const v = b[i];
75
- s += (v < 16 ? '0' : '') + (<u32>v).toString(16);
76
- }
77
- return s;
78
- }
79
-
80
40
  function randomBytes(n: i32): Uint8Array {
81
41
  const b = new Uint8Array(n);
82
42
  crypto.getRandomValues(b);
@@ -105,72 +65,28 @@ function deriveSalt(username: string): Uint8Array {
105
65
  }
106
66
 
107
67
 
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
-
68
+ // ToilDB collections (the `kv.*` dev placeholder is gone). The key + value are
69
+ // `@data` types: the binary codec is generated, the host marshals it, and the
70
+ // challenge is consumed exactly once with `getDelete`.
135
71
 
136
- // @ts-ignore: decorator
137
- @external('env', 'kv.put')
138
- declare function __kvPut(keyPtr: usize, keyLen: i32, valPtr: usize, valLen: i32): void;
139
- // @ts-ignore: decorator
140
- @external('env', 'kv.get')
141
- declare function __kvGet(keyPtr: usize, keyLen: i32, outPtr: usize, outCap: i32): i32;
142
- // @ts-ignore: decorator
143
- @external('env', 'kv.getdel')
144
- declare function __kvGetDel(keyPtr: usize, keyLen: i32, outPtr: usize, outCap: i32): i32;
145
-
146
- const KV_CAP: i32 = 8192; // bounds account (~1.5 KiB) + challenge (~100 B) records
147
-
148
- function kvPut(key: Uint8Array, val: Uint8Array): void {
149
- __kvPut(key.dataStart, key.length, val.dataStart, val.length);
150
- }
151
- function kvGet(key: Uint8Array): Uint8Array | null {
152
- const out = new Uint8Array(KV_CAP);
153
- const n = __kvGet(key.dataStart, key.length, out.dataStart, KV_CAP);
154
- if (n < 0) return null;
155
- return out.slice(0, n);
156
- }
157
- /** Atomic fetch-and-delete: consumes a login challenge exactly once. */
158
- function kvGetDel(key: Uint8Array): Uint8Array | null {
159
- const out = new Uint8Array(KV_CAP);
160
- const n = __kvGetDel(key.dataStart, key.length, out.dataStart, KV_CAP);
161
- if (n < 0) return null;
162
- return out.slice(0, n);
72
+ @data
73
+ class Username {
74
+ name: string = '';
75
+ constructor(name: string = '') {
76
+ this.name = name;
77
+ }
163
78
  }
164
79
 
165
- function acctKey(username: string): Uint8Array {
166
- return utf8('acct:' + username);
167
- }
168
- function chalKey(cid: Uint8Array): Uint8Array {
169
- return utf8('chal:' + toHex(cid));
80
+ @data
81
+ class ChallengeId {
82
+ cid: Uint8Array = new Uint8Array(0);
83
+ constructor(cid: Uint8Array = new Uint8Array(0)) {
84
+ this.cid = cid;
85
+ }
170
86
  }
171
87
 
172
-
173
- class Account {
88
+ @data
89
+ class AuthAccount {
174
90
  username: string = '';
175
91
  salt: Uint8Array = new Uint8Array(0);
176
92
  publicKey: Uint8Array = new Uint8Array(0);
@@ -179,30 +95,7 @@ class Account {
179
95
  parallelism: u32 = 0;
180
96
  }
181
97
 
182
- function putAccount(a: Account): void {
183
- const w = new DataWriter();
184
- w.writeString(a.username);
185
- w.writeBytes(a.salt);
186
- w.writeBytes(a.publicKey);
187
- w.writeU32(a.memKiB);
188
- w.writeU32(a.iterations);
189
- w.writeU32(a.parallelism);
190
- kvPut(acctKey(a.username), w.toBytes());
191
- }
192
- function getAccount(username: string): Account | null {
193
- const raw = kvGet(acctKey(username));
194
- if (raw == null) return null;
195
- const r = new DataReader(raw);
196
- const a = new Account();
197
- a.username = r.readString();
198
- a.salt = r.readBytes();
199
- a.publicKey = r.readBytes();
200
- a.memKiB = r.readU32();
201
- a.iterations = r.readU32();
202
- a.parallelism = r.readU32();
203
- return r.ok ? a : null;
204
- }
205
-
98
+ @data
206
99
  class Challenge {
207
100
  cid: Uint8Array = new Uint8Array(0);
208
101
  username: string = '';
@@ -211,26 +104,10 @@ class Challenge {
211
104
  exp: u64 = 0;
212
105
  }
213
106
 
214
- function putChallenge(c: Challenge): void {
215
- const w = new DataWriter();
216
- w.writeBytes(c.cid);
217
- w.writeString(c.username);
218
- w.writeBytes(c.nonce);
219
- w.writeU64(c.iat);
220
- w.writeU64(c.exp);
221
- kvPut(chalKey(c.cid), w.toBytes());
222
- }
223
- function consumeChallenge(cid: Uint8Array): Challenge | null {
224
- const raw = kvGetDel(chalKey(cid));
225
- if (raw == null) return null;
226
- const r = new DataReader(raw);
227
- const c = new Challenge();
228
- c.cid = r.readBytes();
229
- c.username = r.readString();
230
- c.nonce = r.readBytes();
231
- c.iat = r.readU64();
232
- c.exp = r.readU64();
233
- return r.ok ? c : null;
107
+ @database
108
+ class AuthDb {
109
+ @collection accounts!: Record<AuthAccount, Username>;
110
+ @collection challenges!: Record<Challenge, ChallengeId>;
234
111
  }
235
112
 
236
113
  @rest('auth')
@@ -240,7 +117,6 @@ class Auth {
240
117
  * No taken-oracle: always succeeds; register/finish rejects a duplicate. */
241
118
  @post('/register/start')
242
119
  public registerStart(ctx: RouteContext): Response {
243
- ensureConfigured();
244
120
  const r = new DataReader(ctx.request.body);
245
121
  const username = r.readString();
246
122
  const blinded = r.readBytes();
@@ -263,7 +139,6 @@ class Auth {
263
139
  * proof-of-possession before storing the key. */
264
140
  @post('/register/finish')
265
141
  public registerFinish(ctx: RouteContext): Response {
266
- ensureConfigured();
267
142
  const r = new DataReader(ctx.request.body);
268
143
  const username = r.readString();
269
144
  const pk = r.readBytes();
@@ -272,7 +147,7 @@ class Auth {
272
147
  if (pk.length != AuthService.PUBLIC_KEY_LEN) return fail();
273
148
  // Already registered: a distinguishable status (not the generic 401) so the
274
149
  // client can say "username taken, log in instead" rather than a blank error.
275
- if (getAccount(username) != null) {
150
+ if (AuthDb.accounts.exists(new Username(username))) {
276
151
  return Response.bytes(new DataWriter().writeU8(1).toBytes());
277
152
  }
278
153
 
@@ -281,14 +156,17 @@ class Auth {
281
156
  const regMsg = AuthService.buildRegisterMessage(username, pk);
282
157
  if (!AuthService.verifyRegister(pk, regMsg, proof)) return fail();
283
158
 
284
- const a = new Account();
159
+ const a = new AuthAccount();
285
160
  a.username = username;
286
161
  a.salt = deriveSalt(username);
287
162
  a.publicKey = pk;
288
163
  a.memKiB = DEMO_MEM_KIB;
289
164
  a.iterations = DEMO_ITERS;
290
165
  a.parallelism = DEMO_PAR;
291
- putAccount(a);
166
+ // create-if-absent: a racing duplicate registration loses here, not above.
167
+ if (!AuthDb.accounts.create(new Username(username), a)) {
168
+ return Response.bytes(new DataWriter().writeU8(1).toBytes());
169
+ }
292
170
  return Response.bytes(new DataWriter().writeU8(0).toBytes());
293
171
  }
294
172
 
@@ -300,7 +178,6 @@ class Auth {
300
178
  * and a fresh challenge -- a known and an unknown user are indistinguishable. */
301
179
  @post('/login/start')
302
180
  public loginStart(ctx: RouteContext): Response {
303
- ensureConfigured();
304
181
  const r = new DataReader(ctx.request.body);
305
182
  const username = r.readString();
306
183
  const blinded = r.readBytes();
@@ -308,7 +185,7 @@ class Auth {
308
185
  const evaluated = AuthService.oprfEvaluate(username, blinded);
309
186
  if (evaluated.length != AuthService.OPRF_ELEMENT_LEN) return fail();
310
187
 
311
- const acct = getAccount(username);
188
+ const known = AuthDb.accounts.exists(new Username(username));
312
189
  const cid = randomBytes(16);
313
190
  const nonce = randomBytes(32);
314
191
  const iat = nowSecs();
@@ -316,14 +193,14 @@ class Auth {
316
193
 
317
194
  // Persist only for a real account; the response is identical either way,
318
195
  // and login/finish for an unknown user fails generically at consume.
319
- if (acct != null) {
196
+ if (known) {
320
197
  const c = new Challenge();
321
198
  c.cid = cid;
322
199
  c.username = username;
323
200
  c.nonce = nonce;
324
201
  c.iat = iat;
325
202
  c.exp = exp;
326
- putChallenge(c);
203
+ AuthDb.challenges.create(new ChallengeId(cid), c);
327
204
  }
328
205
 
329
206
  const w = new DataWriter();
@@ -344,7 +221,6 @@ class Auth {
344
221
  * resp: u8(status) [+ bytes(sessionToken) bytes(serverConfirm)] + Set-Cookie */
345
222
  @post('/login/finish')
346
223
  public loginFinish(ctx: RouteContext): Response {
347
- ensureConfigured();
348
224
  const r = new DataReader(ctx.request.body);
349
225
  const cid = r.readBytes();
350
226
  const ct = r.readBytes();
@@ -352,13 +228,13 @@ class Auth {
352
228
  if (!r.ok) return fail();
353
229
 
354
230
  // 1. CONSUME FIRST: atomic fetch-and-delete. Unknown/used/expired => fail.
355
- const ch = consumeChallenge(cid);
231
+ const ch = AuthDb.challenges.getDelete(new ChallengeId(cid));
356
232
  if (ch == null) return fail();
357
233
  if (nowSecs() >= ch.exp) return fail();
358
234
 
359
235
  // 2. Rebuild the message from OUR stored values + the client's ct (and
360
236
  // the bound params + server key id), load the account key, verify.
361
- const acct = getAccount(ch.username);
237
+ const acct = AuthDb.accounts.get(new Username(ch.username));
362
238
  if (acct == null) return fail();
363
239
  const message = AuthService.buildLoginMessage(
364
240
  ch.username, AUD, cid, ch.nonce, ch.iat, ch.exp,
@@ -0,0 +1,62 @@
1
+ import { Counter, Events } from 'toildb';
2
+
3
+ import { GuestEntry } from '../models/GuestEntry';
4
+ import { GuestbookView } from '../models/GuestbookView';
5
+ import { NewMessage } from '../models/NewMessage';
6
+
7
+ /**
8
+ * A PERSISTENT guestbook, mounted at `/guestbook`, backed by ToilDB.
9
+ *
10
+ * The contrast with `Players` (whose comment notes "memory resets next request")
11
+ * is the whole point: every signature is appended to an `events` stream and
12
+ * tallied in a `counter`, so the data SURVIVES across requests under `toiljs dev`
13
+ * (the in-process ToilDB emulator) and runs on ScyllaDB at the edge - same code,
14
+ * no connection string. On the client:
15
+ *
16
+ * await Server.REST.guestbook.sign({ body: new NewMessage('Ada', 'hi!') });
17
+ * const book = await Server.REST.guestbook.list(); // { total, entries: [...] }
18
+ */
19
+
20
+ // The guestbook is one global stream; a single fixed key addresses it.
21
+ @data
22
+ class GuestKey {
23
+ room: string = 'main';
24
+ constructor(room: string = 'main') {
25
+ this.room = room;
26
+ }
27
+ }
28
+
29
+ @database
30
+ class GuestbookDb {
31
+ @collection entries!: Events<GuestEntry, GuestKey>;
32
+ @collection totals!: Counter<GuestKey>;
33
+ }
34
+
35
+ /** The current total + the 10 newest entries. */
36
+ function snapshot(): GuestbookView {
37
+ const key = new GuestKey('main');
38
+ const view = new GuestbookView();
39
+ view.total = GuestbookDb.totals.get(key);
40
+ view.entries = GuestbookDb.entries.latest(key, 10);
41
+ return view;
42
+ }
43
+
44
+ @rest('guestbook')
45
+ class Guestbook {
46
+ /** `GET /guestbook` - the running total + the most recent signatures. */
47
+ @get('/')
48
+ public list(): GuestbookView {
49
+ return snapshot();
50
+ }
51
+
52
+ /** `POST /guestbook` - append a signature (PERSISTED) and return the
53
+ * updated book. Sign twice and the total keeps climbing across requests. */
54
+ @post('/')
55
+ public sign(input: NewMessage): GuestbookView {
56
+ const key = new GuestKey('main');
57
+ const at = <u64>(Date.now() / 1000);
58
+ GuestbookDb.entries.append(key, new GuestEntry(input.author, input.message, at));
59
+ GuestbookDb.totals.add(key, 1);
60
+ return snapshot();
61
+ }
62
+ }
@@ -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.55",
5
5
  "author": "Dacely",
6
6
  "description": "The modern React framework: a file-based React frontend and a ToilScript-compiled WebAssembly backend.",
7
7
  "repository": {
@@ -131,7 +131,7 @@
131
131
  "nodemailer": "^9.0.0",
132
132
  "picocolors": "^1.1.1",
133
133
  "sharp": "^0.35.0",
134
- "toilscript": "^0.1.27",
134
+ "toilscript": "^0.1.28",
135
135
  "typescript-eslint": "^8.60.0",
136
136
  "vite": "^8.0.14",
137
137
  "vite-imagetools": "^10.0.0",