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.
- package/CHANGELOG.md +10 -0
- package/build/compiler/.tsbuildinfo +1 -1
- package/build/devserver/.tsbuildinfo +1 -1
- package/build/devserver/database.d.ts +8 -0
- package/build/devserver/database.js +334 -0
- package/build/devserver/host.d.ts +2 -0
- package/build/devserver/host.js +3 -2
- package/build/devserver/module.js +7 -1
- package/examples/basic/client/routes/rest.tsx +52 -1
- package/examples/basic/server/core/AppHandler.ts +0 -18
- package/examples/basic/server/main.ts +1 -0
- package/examples/basic/server/models/GuestEntry.ts +12 -0
- package/examples/basic/server/models/GuestbookView.ts +10 -0
- package/examples/basic/server/models/NewMessage.ts +6 -0
- package/examples/basic/server/routes/Auth.ts +39 -163
- package/examples/basic/server/routes/Guestbook.ts +62 -0
- package/examples/basic/server/routes/Session.ts +5 -5
- package/package.json +2 -2
- package/server/globals/auth.ts +113 -57
- package/server/globals/twofactor.ts +2 -1
- package/server/runtime/http/securecookies.ts +3 -2
- package/src/devserver/database.ts +459 -0
- package/src/devserver/host.ts +9 -5
- package/src/devserver/module.ts +9 -3
- package/test/devserver-database.test.ts +304 -0
- package/test/devserver-pqauth.test.ts +5 -65
- package/test/example-guestbook.test.ts +78 -0
- package/test/pqauth-e2e.test.ts +6 -6
- package/build/devserver/kv.d.ts +0 -3
- package/build/devserver/kv.js +0 -53
- package/src/devserver/kv.ts +0 -93
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
const STORE = new Map();
|
|
2
|
+
const VIEWS = new Map();
|
|
3
|
+
const MEMBERS = new Map();
|
|
4
|
+
const COUNTERS = new Map();
|
|
5
|
+
const EVENTS = new Map();
|
|
6
|
+
const MAX_NAME = 512;
|
|
7
|
+
const MAX_KEY = 4096;
|
|
8
|
+
const MAX_VALUE = 256 * 1024;
|
|
9
|
+
const I64_MIN = -(2n ** 63n);
|
|
10
|
+
const I64_MAX = 2n ** 63n - 1n;
|
|
11
|
+
function satI64(v) {
|
|
12
|
+
return v < I64_MIN ? I64_MIN : v > I64_MAX ? I64_MAX : v;
|
|
13
|
+
}
|
|
14
|
+
const ABSENT = -2;
|
|
15
|
+
const TOO_SMALL = -1;
|
|
16
|
+
const INVALID_HANDLE = -1001;
|
|
17
|
+
const PRODUCT_ERR = -1000;
|
|
18
|
+
export function freshDbState() {
|
|
19
|
+
return { handles: [], lastResult: null };
|
|
20
|
+
}
|
|
21
|
+
function mem(ref) {
|
|
22
|
+
if (!ref.memory)
|
|
23
|
+
throw new Error('data host import called before memory was bound');
|
|
24
|
+
return Buffer.from(ref.memory.buffer);
|
|
25
|
+
}
|
|
26
|
+
function readCopy(ref, ptr, len) {
|
|
27
|
+
const m = mem(ref);
|
|
28
|
+
if (ptr < 0 || len < 0 || ptr + len > m.length)
|
|
29
|
+
throw new Error(`data read out of bounds: ptr=${String(ptr)} len=${String(len)}`);
|
|
30
|
+
return Buffer.from(m.subarray(ptr, ptr + len));
|
|
31
|
+
}
|
|
32
|
+
function storeKey(collection, key) {
|
|
33
|
+
return collection + '\0' + key.toString('latin1');
|
|
34
|
+
}
|
|
35
|
+
function collOf(db, handle) {
|
|
36
|
+
return handle >= 0 && handle < db.handles.length ? db.handles[handle] : null;
|
|
37
|
+
}
|
|
38
|
+
export function buildDatabaseImports(ref, db) {
|
|
39
|
+
return {
|
|
40
|
+
'data.resolve_collection': (namePtr, nameLen, outHandlePtr) => {
|
|
41
|
+
if (nameLen < 0 || nameLen > MAX_NAME)
|
|
42
|
+
throw new Error('data: collection name too long');
|
|
43
|
+
const name = readCopy(ref, namePtr, nameLen).toString('utf8');
|
|
44
|
+
const handle = db.handles.length;
|
|
45
|
+
db.handles.push(name);
|
|
46
|
+
const m = mem(ref);
|
|
47
|
+
if (outHandlePtr < 0 || outHandlePtr + 4 > m.length)
|
|
48
|
+
throw new Error('data: resolve out-handle out of bounds');
|
|
49
|
+
m.writeUInt32LE(handle, outHandlePtr);
|
|
50
|
+
return 0;
|
|
51
|
+
},
|
|
52
|
+
'data.get': (handle, keyPtr, keyLen) => {
|
|
53
|
+
const coll = collOf(db, handle);
|
|
54
|
+
if (coll === null)
|
|
55
|
+
return INVALID_HANDLE;
|
|
56
|
+
if (keyLen > MAX_KEY)
|
|
57
|
+
throw new Error('data: key too long');
|
|
58
|
+
const v = STORE.get(storeKey(coll, readCopy(ref, keyPtr, keyLen)));
|
|
59
|
+
if (v === undefined)
|
|
60
|
+
return ABSENT;
|
|
61
|
+
db.lastResult = v;
|
|
62
|
+
return v.length;
|
|
63
|
+
},
|
|
64
|
+
'data.get_many': (handle, keysPtr, keysLen) => {
|
|
65
|
+
const coll = collOf(db, handle);
|
|
66
|
+
if (coll === null)
|
|
67
|
+
return INVALID_HANDLE;
|
|
68
|
+
if (keysLen > MAX_VALUE)
|
|
69
|
+
throw new Error('data: keys blob too large');
|
|
70
|
+
const blob = readCopy(ref, keysPtr, keysLen);
|
|
71
|
+
let off = 0;
|
|
72
|
+
const count = blob.readUInt32LE(off);
|
|
73
|
+
off += 4;
|
|
74
|
+
if (count > 1024)
|
|
75
|
+
return PRODUCT_ERR;
|
|
76
|
+
const header = Buffer.alloc(4);
|
|
77
|
+
header.writeUInt32LE(count, 0);
|
|
78
|
+
const parts = [header];
|
|
79
|
+
for (let i = 0; i < count; i++) {
|
|
80
|
+
const len = blob.readUInt32LE(off);
|
|
81
|
+
off += 4;
|
|
82
|
+
const key = blob.subarray(off, off + len);
|
|
83
|
+
off += len;
|
|
84
|
+
const v = STORE.get(storeKey(coll, key));
|
|
85
|
+
if (v === undefined) {
|
|
86
|
+
parts.push(Buffer.from([0]));
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
const h = Buffer.alloc(5);
|
|
90
|
+
h.writeUInt8(1, 0);
|
|
91
|
+
h.writeUInt32LE(v.length, 1);
|
|
92
|
+
parts.push(h, v);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
db.lastResult = Buffer.concat(parts);
|
|
96
|
+
return db.lastResult.length;
|
|
97
|
+
},
|
|
98
|
+
'data.exists': (handle, keyPtr, keyLen) => {
|
|
99
|
+
const coll = collOf(db, handle);
|
|
100
|
+
if (coll === null)
|
|
101
|
+
return INVALID_HANDLE;
|
|
102
|
+
return STORE.has(storeKey(coll, readCopy(ref, keyPtr, keyLen))) ? 1 : 0;
|
|
103
|
+
},
|
|
104
|
+
'data.create': (handle, keyPtr, keyLen, valPtr, valLen, _idemPtr) => {
|
|
105
|
+
const coll = collOf(db, handle);
|
|
106
|
+
if (coll === null)
|
|
107
|
+
return INVALID_HANDLE;
|
|
108
|
+
if (keyLen > MAX_KEY || valLen > MAX_VALUE)
|
|
109
|
+
throw new Error('data: key/value too large');
|
|
110
|
+
const sk = storeKey(coll, readCopy(ref, keyPtr, keyLen));
|
|
111
|
+
if (STORE.has(sk))
|
|
112
|
+
return PRODUCT_ERR;
|
|
113
|
+
STORE.set(sk, readCopy(ref, valPtr, valLen));
|
|
114
|
+
return 0;
|
|
115
|
+
},
|
|
116
|
+
'data.patch': (handle, keyPtr, keyLen, patchPtr, patchLen, _idemPtr) => {
|
|
117
|
+
const coll = collOf(db, handle);
|
|
118
|
+
if (coll === null)
|
|
119
|
+
return INVALID_HANDLE;
|
|
120
|
+
if (keyLen > MAX_KEY || patchLen > MAX_VALUE)
|
|
121
|
+
throw new Error('data: key/patch too large');
|
|
122
|
+
const sk = storeKey(coll, readCopy(ref, keyPtr, keyLen));
|
|
123
|
+
if (!STORE.has(sk))
|
|
124
|
+
return PRODUCT_ERR;
|
|
125
|
+
const v = readCopy(ref, patchPtr, patchLen);
|
|
126
|
+
STORE.set(sk, v);
|
|
127
|
+
db.lastResult = v;
|
|
128
|
+
return v.length;
|
|
129
|
+
},
|
|
130
|
+
'data.delete': (handle, keyPtr, keyLen, _idemPtr) => {
|
|
131
|
+
const coll = collOf(db, handle);
|
|
132
|
+
if (coll === null)
|
|
133
|
+
return INVALID_HANDLE;
|
|
134
|
+
STORE.delete(storeKey(coll, readCopy(ref, keyPtr, keyLen)));
|
|
135
|
+
return 0;
|
|
136
|
+
},
|
|
137
|
+
'data.get_delete': (handle, keyPtr, keyLen, _idemPtr) => {
|
|
138
|
+
const coll = collOf(db, handle);
|
|
139
|
+
if (coll === null)
|
|
140
|
+
return INVALID_HANDLE;
|
|
141
|
+
const sk = storeKey(coll, readCopy(ref, keyPtr, keyLen));
|
|
142
|
+
const v = STORE.get(sk);
|
|
143
|
+
if (v === undefined)
|
|
144
|
+
return ABSENT;
|
|
145
|
+
STORE.delete(sk);
|
|
146
|
+
db.lastResult = v;
|
|
147
|
+
return v.length;
|
|
148
|
+
},
|
|
149
|
+
'data.unique_lookup': (handle, keyPtr, keyLen) => {
|
|
150
|
+
const coll = collOf(db, handle);
|
|
151
|
+
if (coll === null)
|
|
152
|
+
return INVALID_HANDLE;
|
|
153
|
+
const v = STORE.get(storeKey(coll, readCopy(ref, keyPtr, keyLen)));
|
|
154
|
+
if (v === undefined)
|
|
155
|
+
return ABSENT;
|
|
156
|
+
db.lastResult = v;
|
|
157
|
+
return v.length;
|
|
158
|
+
},
|
|
159
|
+
'data.unique_claim': (handle, keyPtr, keyLen, valPtr, valLen, _idemPtr) => {
|
|
160
|
+
const coll = collOf(db, handle);
|
|
161
|
+
if (coll === null)
|
|
162
|
+
return INVALID_HANDLE;
|
|
163
|
+
if (keyLen > MAX_KEY || valLen > MAX_VALUE)
|
|
164
|
+
throw new Error('data: key/value too large');
|
|
165
|
+
const sk = storeKey(coll, readCopy(ref, keyPtr, keyLen));
|
|
166
|
+
const owner = readCopy(ref, valPtr, valLen);
|
|
167
|
+
const existing = STORE.get(sk);
|
|
168
|
+
if (existing === undefined) {
|
|
169
|
+
STORE.set(sk, owner);
|
|
170
|
+
return 0;
|
|
171
|
+
}
|
|
172
|
+
if (existing.equals(owner))
|
|
173
|
+
return 2;
|
|
174
|
+
db.lastResult = existing;
|
|
175
|
+
return 1;
|
|
176
|
+
},
|
|
177
|
+
'data.unique_release': (handle, keyPtr, keyLen, valPtr, valLen, _idemPtr) => {
|
|
178
|
+
const coll = collOf(db, handle);
|
|
179
|
+
if (coll === null)
|
|
180
|
+
return INVALID_HANDLE;
|
|
181
|
+
const sk = storeKey(coll, readCopy(ref, keyPtr, keyLen));
|
|
182
|
+
const existing = STORE.get(sk);
|
|
183
|
+
if (existing === undefined)
|
|
184
|
+
return 0;
|
|
185
|
+
if (!existing.equals(readCopy(ref, valPtr, valLen)))
|
|
186
|
+
return PRODUCT_ERR;
|
|
187
|
+
STORE.delete(sk);
|
|
188
|
+
return 0;
|
|
189
|
+
},
|
|
190
|
+
'data.membership_contains': (handle, setPtr, setLen, memberPtr, memberLen) => {
|
|
191
|
+
const coll = collOf(db, handle);
|
|
192
|
+
if (coll === null)
|
|
193
|
+
return INVALID_HANDLE;
|
|
194
|
+
const set = MEMBERS.get(storeKey(coll, readCopy(ref, setPtr, setLen)));
|
|
195
|
+
if (set === undefined)
|
|
196
|
+
return 0;
|
|
197
|
+
return set.has(readCopy(ref, memberPtr, memberLen).toString('latin1')) ? 1 : 0;
|
|
198
|
+
},
|
|
199
|
+
'data.membership_add': (handle, setPtr, setLen, memberPtr, memberLen, _idemPtr) => {
|
|
200
|
+
const coll = collOf(db, handle);
|
|
201
|
+
if (coll === null)
|
|
202
|
+
return INVALID_HANDLE;
|
|
203
|
+
if (setLen > MAX_KEY || memberLen > MAX_VALUE)
|
|
204
|
+
throw new Error('data: set/member too large');
|
|
205
|
+
const sk = storeKey(coll, readCopy(ref, setPtr, setLen));
|
|
206
|
+
const member = readCopy(ref, memberPtr, memberLen);
|
|
207
|
+
let set = MEMBERS.get(sk);
|
|
208
|
+
if (set === undefined) {
|
|
209
|
+
set = new Map();
|
|
210
|
+
MEMBERS.set(sk, set);
|
|
211
|
+
}
|
|
212
|
+
set.set(member.toString('latin1'), member);
|
|
213
|
+
return 0;
|
|
214
|
+
},
|
|
215
|
+
'data.membership_remove': (handle, setPtr, setLen, memberPtr, memberLen, _idemPtr) => {
|
|
216
|
+
const coll = collOf(db, handle);
|
|
217
|
+
if (coll === null)
|
|
218
|
+
return INVALID_HANDLE;
|
|
219
|
+
const set = MEMBERS.get(storeKey(coll, readCopy(ref, setPtr, setLen)));
|
|
220
|
+
if (set !== undefined)
|
|
221
|
+
set.delete(readCopy(ref, memberPtr, memberLen).toString('latin1'));
|
|
222
|
+
return 0;
|
|
223
|
+
},
|
|
224
|
+
'data.membership_list': (handle, setPtr, setLen, limit) => {
|
|
225
|
+
const coll = collOf(db, handle);
|
|
226
|
+
if (coll === null)
|
|
227
|
+
return INVALID_HANDLE;
|
|
228
|
+
const set = MEMBERS.get(storeKey(coll, readCopy(ref, setPtr, setLen)));
|
|
229
|
+
const n = Math.max(0, Math.min(limit, 0xffff));
|
|
230
|
+
const members = set === undefined ? [] : Array.from(set.values()).sort(Buffer.compare).slice(0, n);
|
|
231
|
+
const header = Buffer.alloc(4);
|
|
232
|
+
header.writeUInt32LE(members.length, 0);
|
|
233
|
+
const parts = [header];
|
|
234
|
+
for (const m of members) {
|
|
235
|
+
const h = Buffer.alloc(4);
|
|
236
|
+
h.writeUInt32LE(m.length, 0);
|
|
237
|
+
parts.push(h, m);
|
|
238
|
+
}
|
|
239
|
+
db.lastResult = Buffer.concat(parts);
|
|
240
|
+
return db.lastResult.length;
|
|
241
|
+
},
|
|
242
|
+
'data.view_get': (handle, keyPtr, keyLen) => {
|
|
243
|
+
const coll = collOf(db, handle);
|
|
244
|
+
if (coll === null)
|
|
245
|
+
return INVALID_HANDLE;
|
|
246
|
+
const v = VIEWS.get(storeKey(coll, readCopy(ref, keyPtr, keyLen)));
|
|
247
|
+
if (v === undefined)
|
|
248
|
+
return ABSENT;
|
|
249
|
+
db.lastResult = v;
|
|
250
|
+
return v.length;
|
|
251
|
+
},
|
|
252
|
+
'data.view_publish': (handle, keyPtr, keyLen, valPtr, valLen, _idemPtr) => {
|
|
253
|
+
const coll = collOf(db, handle);
|
|
254
|
+
if (coll === null)
|
|
255
|
+
return INVALID_HANDLE;
|
|
256
|
+
if (keyLen > MAX_KEY || valLen > MAX_VALUE)
|
|
257
|
+
throw new Error('data: key/view too large');
|
|
258
|
+
VIEWS.set(storeKey(coll, readCopy(ref, keyPtr, keyLen)), readCopy(ref, valPtr, valLen));
|
|
259
|
+
return 0;
|
|
260
|
+
},
|
|
261
|
+
'data.counter_get': (handle, keyPtr, keyLen) => {
|
|
262
|
+
const coll = collOf(db, handle);
|
|
263
|
+
if (coll === null)
|
|
264
|
+
return INVALID_HANDLE;
|
|
265
|
+
const sum = COUNTERS.get(storeKey(coll, readCopy(ref, keyPtr, keyLen))) ?? 0n;
|
|
266
|
+
const out = Buffer.alloc(8);
|
|
267
|
+
out.writeBigInt64LE(sum);
|
|
268
|
+
db.lastResult = out;
|
|
269
|
+
return out.length;
|
|
270
|
+
},
|
|
271
|
+
'data.counter_add': (handle, keyPtr, keyLen, delta, _idemPtr) => {
|
|
272
|
+
const coll = collOf(db, handle);
|
|
273
|
+
if (coll === null)
|
|
274
|
+
return INVALID_HANDLE;
|
|
275
|
+
const sk = storeKey(coll, readCopy(ref, keyPtr, keyLen));
|
|
276
|
+
COUNTERS.set(sk, satI64((COUNTERS.get(sk) ?? 0n) + BigInt(delta)));
|
|
277
|
+
return 0;
|
|
278
|
+
},
|
|
279
|
+
'data.append': (handle, keyPtr, keyLen, evPtr, evLen, _idemPtr) => {
|
|
280
|
+
const coll = collOf(db, handle);
|
|
281
|
+
if (coll === null)
|
|
282
|
+
return INVALID_HANDLE;
|
|
283
|
+
if (keyLen > MAX_KEY || evLen > MAX_VALUE)
|
|
284
|
+
throw new Error('data: key/event too large');
|
|
285
|
+
const sk = storeKey(coll, readCopy(ref, keyPtr, keyLen));
|
|
286
|
+
const log = EVENTS.get(sk);
|
|
287
|
+
const ev = readCopy(ref, evPtr, evLen);
|
|
288
|
+
if (log === undefined)
|
|
289
|
+
EVENTS.set(sk, [ev]);
|
|
290
|
+
else
|
|
291
|
+
log.push(ev);
|
|
292
|
+
return 0;
|
|
293
|
+
},
|
|
294
|
+
'data.latest': (handle, keyPtr, keyLen, limit) => {
|
|
295
|
+
const coll = collOf(db, handle);
|
|
296
|
+
if (coll === null)
|
|
297
|
+
return INVALID_HANDLE;
|
|
298
|
+
const log = EVENTS.get(storeKey(coll, readCopy(ref, keyPtr, keyLen))) ?? [];
|
|
299
|
+
const n = Math.max(0, Math.min(limit, 0xffff));
|
|
300
|
+
const newest = log.slice(Math.max(0, log.length - n)).reverse();
|
|
301
|
+
let size = 4;
|
|
302
|
+
for (const ev of newest)
|
|
303
|
+
size += 4 + ev.length;
|
|
304
|
+
const out = Buffer.alloc(size);
|
|
305
|
+
let off = out.writeUInt32LE(newest.length, 0);
|
|
306
|
+
for (const ev of newest) {
|
|
307
|
+
off = out.writeUInt32LE(ev.length, off);
|
|
308
|
+
off += ev.copy(out, off);
|
|
309
|
+
}
|
|
310
|
+
db.lastResult = out;
|
|
311
|
+
return out.length;
|
|
312
|
+
},
|
|
313
|
+
'data.take_result': (outPtr, outCap) => {
|
|
314
|
+
const v = db.lastResult;
|
|
315
|
+
if (v === null)
|
|
316
|
+
return 0;
|
|
317
|
+
if (v.length > outCap)
|
|
318
|
+
return TOO_SMALL;
|
|
319
|
+
const m = mem(ref);
|
|
320
|
+
if (outPtr < 0 || outPtr + v.length > m.length)
|
|
321
|
+
throw new Error('data: take_result out of bounds');
|
|
322
|
+
v.copy(m, outPtr);
|
|
323
|
+
db.lastResult = null;
|
|
324
|
+
return v.length;
|
|
325
|
+
},
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
export function __resetDbForTests() {
|
|
329
|
+
STORE.clear();
|
|
330
|
+
VIEWS.clear();
|
|
331
|
+
MEMBERS.clear();
|
|
332
|
+
COUNTERS.clear();
|
|
333
|
+
EVENTS.clear();
|
|
334
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type CryptoState } from './crypto.js';
|
|
2
|
+
import { type DbDevState } from './database.js';
|
|
2
3
|
export declare class WasmAbortError extends Error {
|
|
3
4
|
constructor(message: string, fileName: string, line: number, column: number);
|
|
4
5
|
}
|
|
@@ -9,6 +10,7 @@ export interface DispatchState {
|
|
|
9
10
|
sendfile: string | null;
|
|
10
11
|
clientIp: string;
|
|
11
12
|
crypto: CryptoState;
|
|
13
|
+
db: DbDevState;
|
|
12
14
|
}
|
|
13
15
|
export declare function freshDispatchState(): DispatchState;
|
|
14
16
|
export interface MemoryRef {
|
package/build/devserver/host.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { buildCryptoImports, freshCryptoState } from './crypto.js';
|
|
2
|
-
import {
|
|
2
|
+
import { buildDatabaseImports, freshDbState } from './database.js';
|
|
3
3
|
import { EmailStatus, getEmailService } from './email/index.js';
|
|
4
4
|
import { parseEmailBlob } from './email/wire.js';
|
|
5
5
|
import { devEnvGet, devEnvGetSecure } from './env.js';
|
|
@@ -24,6 +24,7 @@ export function freshDispatchState() {
|
|
|
24
24
|
sendfile: null,
|
|
25
25
|
clientIp: '',
|
|
26
26
|
crypto: freshCryptoState(),
|
|
27
|
+
db: freshDbState(),
|
|
27
28
|
};
|
|
28
29
|
}
|
|
29
30
|
function mem(ref) {
|
|
@@ -141,7 +142,7 @@ export function buildHostImports(ref, state) {
|
|
|
141
142
|
thread_spawn: (_startArg) => -1,
|
|
142
143
|
'Date.now': () => Date.now(),
|
|
143
144
|
...buildCryptoImports(ref, state.crypto),
|
|
144
|
-
...
|
|
145
|
+
...buildDatabaseImports(ref, state.db),
|
|
145
146
|
},
|
|
146
147
|
};
|
|
147
148
|
}
|
|
@@ -11,7 +11,13 @@ const PROVIDED_IMPORTS = new Set([
|
|
|
11
11
|
'crypto.import_key', 'crypto.export_key', 'crypto.encrypt', 'crypto.decrypt',
|
|
12
12
|
'crypto.sign', 'crypto.verify', 'crypto.derive_bits',
|
|
13
13
|
'crypto.mldsa_verify', 'crypto.mlkem_decapsulate', 'crypto.voprf_evaluate',
|
|
14
|
-
'
|
|
14
|
+
'data.resolve_collection', 'data.get', 'data.get_many', 'data.exists', 'data.create',
|
|
15
|
+
'data.patch', 'data.delete', 'data.get_delete',
|
|
16
|
+
'data.unique_lookup', 'data.unique_claim', 'data.unique_release',
|
|
17
|
+
'data.view_get', 'data.view_publish',
|
|
18
|
+
'data.membership_contains', 'data.membership_add', 'data.membership_remove', 'data.membership_list',
|
|
19
|
+
'data.counter_get', 'data.counter_add', 'data.append', 'data.latest',
|
|
20
|
+
'data.take_result',
|
|
15
21
|
]);
|
|
16
22
|
export class WasmServerModule {
|
|
17
23
|
wasmPath;
|
|
@@ -6,12 +6,20 @@
|
|
|
6
6
|
// `Response` to inspect (status, `.json()`, ...). Needs the server running to respond.
|
|
7
7
|
import { useState } from 'react';
|
|
8
8
|
|
|
9
|
-
import { NewPlayer, ScoreDelta } from 'shared/server';
|
|
9
|
+
import { NewMessage, NewPlayer, ScoreDelta } from 'shared/server';
|
|
10
10
|
|
|
11
11
|
export default function RestDemo() {
|
|
12
12
|
const [log, setLog] = useState<string[]>([]);
|
|
13
13
|
const note = (line: string) => setLog((prev) => [line, ...prev].slice(0, 8));
|
|
14
14
|
|
|
15
|
+
// The guestbook is backed by ToilDB (an `events` stream + a `counter`), so unlike
|
|
16
|
+
// the players below its data PERSISTS across requests. Sign a few times, reload the
|
|
17
|
+
// page, and the total is still there.
|
|
18
|
+
const [book, setBook] = useState<{ total: bigint; entries: { author: string; message: string }[] }>({
|
|
19
|
+
total: 0n,
|
|
20
|
+
entries: [],
|
|
21
|
+
});
|
|
22
|
+
|
|
15
23
|
// POST /players -> typed Promise<Player>, body is a @data class. The server returns the
|
|
16
24
|
// new player, but it is NOT saved (server memory resets per request); this is a preview.
|
|
17
25
|
const onCreate = async () => {
|
|
@@ -67,6 +75,31 @@ export default function RestDemo() {
|
|
|
67
75
|
}
|
|
68
76
|
};
|
|
69
77
|
|
|
78
|
+
// POST /guestbook -> typed Promise<GuestbookView>. This one is PERSISTED in ToilDB
|
|
79
|
+
// (events + counter), so the total keeps climbing across requests and reloads.
|
|
80
|
+
const signers = ['Ada', 'Linus', 'Grace', 'Ken'];
|
|
81
|
+
const onSign = async () => {
|
|
82
|
+
try {
|
|
83
|
+
const author = signers[Math.floor(Math.random() * signers.length)];
|
|
84
|
+
const v = await Server.REST.guestbook.sign({ body: new NewMessage(author, 'was here') });
|
|
85
|
+
setBook(v);
|
|
86
|
+
note(`${author} signed -> ${v.total} signatures (persisted in ToilDB)`);
|
|
87
|
+
} catch (err) {
|
|
88
|
+
note(parseError(err));
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
// GET /guestbook -> the running total + the newest entries.
|
|
93
|
+
const onBook = async () => {
|
|
94
|
+
try {
|
|
95
|
+
const v = await Server.REST.guestbook.list();
|
|
96
|
+
setBook(v);
|
|
97
|
+
note(`guestbook: ${v.total} signatures`);
|
|
98
|
+
} catch (err) {
|
|
99
|
+
note(parseError(err));
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
|
|
70
103
|
return (
|
|
71
104
|
<main>
|
|
72
105
|
<h1>REST</h1>
|
|
@@ -83,6 +116,24 @@ export default function RestDemo() {
|
|
|
83
116
|
<li key={i}>{line}</li>
|
|
84
117
|
))}
|
|
85
118
|
</ul>
|
|
119
|
+
|
|
120
|
+
<h2>Guestbook (persisted via ToilDB)</h2>
|
|
121
|
+
<p>
|
|
122
|
+
The same <code>Server.REST.*</code> client, but the route stores each signature in a ToilDB{' '}
|
|
123
|
+
<code>events</code> stream and a <code>counter</code>. So unlike the players above, these{' '}
|
|
124
|
+
<strong>persist</strong> across requests and page reloads - locally under <code>toil dev</code> and on
|
|
125
|
+
ScyllaDB at the edge, with the same code.
|
|
126
|
+
</p>
|
|
127
|
+
<button onClick={onSign}>sign the guestbook</button>{' '}
|
|
128
|
+
<button onClick={onBook}>refresh ({String(book.total)} signatures)</button>
|
|
129
|
+
<ul>
|
|
130
|
+
{book.entries.map((e, i) => (
|
|
131
|
+
<li key={i}>
|
|
132
|
+
<strong>{e.author}</strong>: {e.message}
|
|
133
|
+
</li>
|
|
134
|
+
))}
|
|
135
|
+
</ul>
|
|
136
|
+
|
|
86
137
|
<Toil.Link href="/">Back home</Toil.Link>
|
|
87
138
|
</main>
|
|
88
139
|
);
|
|
@@ -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().
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** One signed guestbook entry, stored as a ToilDB `events` record. */
|
|
2
|
+
@data
|
|
3
|
+
export class GuestEntry {
|
|
4
|
+
author: string = '';
|
|
5
|
+
message: string = '';
|
|
6
|
+
at: u64 = 0;
|
|
7
|
+
constructor(author: string = '', message: string = '', at: u64 = 0) {
|
|
8
|
+
this.author = author;
|
|
9
|
+
this.message = message;
|
|
10
|
+
this.at = at;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { GuestEntry } from './GuestEntry';
|
|
2
|
+
|
|
3
|
+
/** The guestbook snapshot returned by the routes: the running signature count
|
|
4
|
+
* plus the most-recent entries (newest first). A `@data` wrapper so the
|
|
5
|
+
* `GuestEntry[]` round-trips through the codec. */
|
|
6
|
+
@data
|
|
7
|
+
export class GuestbookView {
|
|
8
|
+
total: i64 = 0;
|
|
9
|
+
entries: GuestEntry[] = [];
|
|
10
|
+
}
|