toiljs 0.0.54 → 0.0.56
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/build/backend/.tsbuildinfo +1 -1
- package/build/cli/.tsbuildinfo +1 -1
- package/build/cli/index.js +9 -5
- package/build/client/.tsbuildinfo +1 -1
- package/build/client/auth.js +1 -1
- package/build/client/components/Image.d.ts +1 -1
- package/build/client/dev/devtools.js +3 -1
- package/build/client/index.d.ts +2 -2
- package/build/client/index.js +2 -2
- package/build/client/routing/Router.js +1 -1
- package/build/client/routing/mount.js +1 -1
- package/build/compiler/.tsbuildinfo +1 -1
- package/build/compiler/docs.js +1 -1
- package/build/compiler/seo.js +1 -3
- package/build/compiler/template-build.js +1 -1
- package/build/devserver/.tsbuildinfo +1 -1
- package/build/devserver/cache.js +0 -0
- package/build/devserver/crypto.js +45 -17
- package/build/devserver/database.d.ts +8 -0
- package/build/devserver/database.js +416 -0
- package/build/devserver/email/caps.js +0 -0
- package/build/devserver/email/config.js +7 -2
- package/build/devserver/email/validate.js +1 -4
- package/build/devserver/host.d.ts +2 -0
- package/build/devserver/host.js +3 -2
- package/build/devserver/index.d.ts +1 -1
- package/build/devserver/index.js +3 -2
- package/build/devserver/module.js +52 -7
- package/build/devserver/proxy.js +2 -1
- package/build/io/.tsbuildinfo +1 -1
- package/build/io/codec.d.ts +5 -5
- package/build/io/codec.js +193 -77
- package/examples/basic/client/components/HoneycombBackground.tsx +1 -1
- package/examples/basic/client/public/images/logo.svg +37 -34
- package/examples/basic/client/public/index.html +14 -14
- package/examples/basic/client/routes/auth.tsx +18 -10
- package/examples/basic/client/routes/cookies.tsx +15 -24
- package/examples/basic/client/routes/crypto.tsx +4 -5
- package/examples/basic/client/routes/features/template/template.tsx +1 -1
- package/examples/basic/client/routes/hello.tsx +1 -1
- package/examples/basic/client/routes/pq.tsx +14 -14
- package/examples/basic/client/routes/rest.tsx +50 -1
- package/examples/basic/client/styles/main.css +25 -22
- package/examples/basic/client/toil.tsx +1 -1
- package/examples/basic/server/README.md +8 -8
- package/examples/basic/server/core/AppHandler.ts +4 -7
- 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 +50 -106
- package/examples/basic/server/routes/EnvDemo.ts +9 -3
- package/examples/basic/server/routes/Guestbook.ts +62 -0
- package/package.json +2 -2
- package/server/globals/auth.ts +3 -3
- package/server/globals/twofactor.ts +2 -1
- package/server/runtime/http/securecookies.ts +3 -2
- package/src/backend/index.ts +4 -2
- package/src/cli/doctor.ts +10 -3
- package/src/cli/notify.ts +1 -6
- package/src/cli/ui.ts +3 -3
- package/src/cli/version-check.ts +5 -1
- package/src/client/auth.ts +33 -10
- package/src/client/components/Form.tsx +2 -2
- package/src/client/components/Image.tsx +1 -1
- package/src/client/components/Script.tsx +1 -1
- package/src/client/components/Slot.tsx +1 -1
- package/src/client/dev/devtools.tsx +121 -54
- package/src/client/dev/error-overlay.tsx +7 -1
- package/src/client/head/metadata.ts +1 -1
- package/src/client/index.ts +13 -2
- package/src/client/routing/Router.tsx +2 -2
- package/src/client/routing/error-boundary.tsx +1 -1
- package/src/client/routing/loader.ts +2 -2
- package/src/client/routing/mount.tsx +5 -6
- package/src/compiler/docs.ts +1 -1
- package/src/compiler/email-preview.ts +1 -1
- package/src/compiler/generate.ts +1 -1
- package/src/compiler/seo.ts +1 -3
- package/src/compiler/ssg.ts +10 -4
- package/src/compiler/template-build.ts +2 -7
- package/src/compiler/template.ts +1 -4
- package/src/compiler/vite.ts +1 -1
- package/src/devserver/cache.ts +0 -0
- package/src/devserver/crypto.ts +140 -51
- package/src/devserver/database.ts +600 -0
- package/src/devserver/dotenv.ts +10 -2
- package/src/devserver/email/caps.ts +0 -0
- package/src/devserver/email/config.ts +8 -2
- package/src/devserver/email/index.ts +3 -3
- package/src/devserver/email/validate.ts +1 -4
- package/src/devserver/envelope.ts +3 -3
- package/src/devserver/host.ts +22 -9
- package/src/devserver/index.ts +15 -6
- package/src/devserver/module.ts +59 -11
- package/src/devserver/proxy.ts +5 -7
- package/src/io/codec.ts +226 -83
- package/test/devserver-database.test.ts +364 -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,416 @@
|
|
|
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 CAPACITY = new Map();
|
|
7
|
+
function capLedger(sk) {
|
|
8
|
+
let l = CAPACITY.get(sk);
|
|
9
|
+
if (l === undefined) {
|
|
10
|
+
l = { total: 0n, confirmed: 0n, holds: new Map(), nextId: 1n };
|
|
11
|
+
CAPACITY.set(sk, l);
|
|
12
|
+
}
|
|
13
|
+
return l;
|
|
14
|
+
}
|
|
15
|
+
function capPrune(l, nowMs) {
|
|
16
|
+
for (const [id, h] of l.holds)
|
|
17
|
+
if (h.expiresMs <= nowMs)
|
|
18
|
+
l.holds.delete(id);
|
|
19
|
+
}
|
|
20
|
+
function capHeld(l) {
|
|
21
|
+
let sum = 0n;
|
|
22
|
+
for (const h of l.holds.values())
|
|
23
|
+
sum += h.amount;
|
|
24
|
+
return sum;
|
|
25
|
+
}
|
|
26
|
+
const MAX_NAME = 512;
|
|
27
|
+
const MAX_KEY = 4096;
|
|
28
|
+
const MAX_VALUE = 256 * 1024;
|
|
29
|
+
const I64_MIN = -(2n ** 63n);
|
|
30
|
+
const I64_MAX = 2n ** 63n - 1n;
|
|
31
|
+
function satI64(v) {
|
|
32
|
+
return v < I64_MIN ? I64_MIN : v > I64_MAX ? I64_MAX : v;
|
|
33
|
+
}
|
|
34
|
+
const ABSENT = -2;
|
|
35
|
+
const TOO_SMALL = -1;
|
|
36
|
+
const INVALID_HANDLE = -1001;
|
|
37
|
+
const PRODUCT_ERR = -1000;
|
|
38
|
+
export function freshDbState() {
|
|
39
|
+
return { handles: [], lastResult: null };
|
|
40
|
+
}
|
|
41
|
+
function mem(ref) {
|
|
42
|
+
if (!ref.memory)
|
|
43
|
+
throw new Error('data host import called before memory was bound');
|
|
44
|
+
return Buffer.from(ref.memory.buffer);
|
|
45
|
+
}
|
|
46
|
+
function readCopy(ref, ptr, len) {
|
|
47
|
+
const m = mem(ref);
|
|
48
|
+
if (ptr < 0 || len < 0 || ptr + len > m.length)
|
|
49
|
+
throw new Error(`data read out of bounds: ptr=${String(ptr)} len=${String(len)}`);
|
|
50
|
+
return Buffer.from(m.subarray(ptr, ptr + len));
|
|
51
|
+
}
|
|
52
|
+
function storeKey(collection, key) {
|
|
53
|
+
return collection + '\0' + key.toString('latin1');
|
|
54
|
+
}
|
|
55
|
+
function collOf(db, handle) {
|
|
56
|
+
return handle >= 0 && handle < db.handles.length ? db.handles[handle] : null;
|
|
57
|
+
}
|
|
58
|
+
export function buildDatabaseImports(ref, db) {
|
|
59
|
+
return {
|
|
60
|
+
'data.resolve_collection': (namePtr, nameLen, outHandlePtr) => {
|
|
61
|
+
if (nameLen < 0 || nameLen > MAX_NAME)
|
|
62
|
+
throw new Error('data: collection name too long');
|
|
63
|
+
const name = readCopy(ref, namePtr, nameLen).toString('utf8');
|
|
64
|
+
const handle = db.handles.length;
|
|
65
|
+
db.handles.push(name);
|
|
66
|
+
const m = mem(ref);
|
|
67
|
+
if (outHandlePtr < 0 || outHandlePtr + 4 > m.length)
|
|
68
|
+
throw new Error('data: resolve out-handle out of bounds');
|
|
69
|
+
m.writeUInt32LE(handle, outHandlePtr);
|
|
70
|
+
return 0;
|
|
71
|
+
},
|
|
72
|
+
'data.get': (handle, keyPtr, keyLen) => {
|
|
73
|
+
const coll = collOf(db, handle);
|
|
74
|
+
if (coll === null)
|
|
75
|
+
return INVALID_HANDLE;
|
|
76
|
+
if (keyLen > MAX_KEY)
|
|
77
|
+
throw new Error('data: key too long');
|
|
78
|
+
const v = STORE.get(storeKey(coll, readCopy(ref, keyPtr, keyLen)));
|
|
79
|
+
if (v === undefined)
|
|
80
|
+
return ABSENT;
|
|
81
|
+
db.lastResult = v;
|
|
82
|
+
return v.length;
|
|
83
|
+
},
|
|
84
|
+
'data.get_many': (handle, keysPtr, keysLen) => {
|
|
85
|
+
const coll = collOf(db, handle);
|
|
86
|
+
if (coll === null)
|
|
87
|
+
return INVALID_HANDLE;
|
|
88
|
+
if (keysLen > MAX_VALUE)
|
|
89
|
+
throw new Error('data: keys blob too large');
|
|
90
|
+
const blob = readCopy(ref, keysPtr, keysLen);
|
|
91
|
+
let off = 0;
|
|
92
|
+
const count = blob.readUInt32LE(off);
|
|
93
|
+
off += 4;
|
|
94
|
+
if (count > 1024)
|
|
95
|
+
return PRODUCT_ERR;
|
|
96
|
+
const header = Buffer.alloc(4);
|
|
97
|
+
header.writeUInt32LE(count, 0);
|
|
98
|
+
const parts = [header];
|
|
99
|
+
for (let i = 0; i < count; i++) {
|
|
100
|
+
const len = blob.readUInt32LE(off);
|
|
101
|
+
off += 4;
|
|
102
|
+
const key = blob.subarray(off, off + len);
|
|
103
|
+
off += len;
|
|
104
|
+
const v = STORE.get(storeKey(coll, key));
|
|
105
|
+
if (v === undefined) {
|
|
106
|
+
parts.push(Buffer.from([0]));
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
const h = Buffer.alloc(5);
|
|
110
|
+
h.writeUInt8(1, 0);
|
|
111
|
+
h.writeUInt32LE(v.length, 1);
|
|
112
|
+
parts.push(h, v);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
db.lastResult = Buffer.concat(parts);
|
|
116
|
+
return db.lastResult.length;
|
|
117
|
+
},
|
|
118
|
+
'data.exists': (handle, keyPtr, keyLen) => {
|
|
119
|
+
const coll = collOf(db, handle);
|
|
120
|
+
if (coll === null)
|
|
121
|
+
return INVALID_HANDLE;
|
|
122
|
+
return STORE.has(storeKey(coll, readCopy(ref, keyPtr, keyLen))) ? 1 : 0;
|
|
123
|
+
},
|
|
124
|
+
'data.create': (handle, keyPtr, keyLen, valPtr, valLen, _idemPtr) => {
|
|
125
|
+
const coll = collOf(db, handle);
|
|
126
|
+
if (coll === null)
|
|
127
|
+
return INVALID_HANDLE;
|
|
128
|
+
if (keyLen > MAX_KEY || valLen > MAX_VALUE)
|
|
129
|
+
throw new Error('data: key/value too large');
|
|
130
|
+
const sk = storeKey(coll, readCopy(ref, keyPtr, keyLen));
|
|
131
|
+
if (STORE.has(sk))
|
|
132
|
+
return PRODUCT_ERR;
|
|
133
|
+
STORE.set(sk, readCopy(ref, valPtr, valLen));
|
|
134
|
+
return 0;
|
|
135
|
+
},
|
|
136
|
+
'data.patch': (handle, keyPtr, keyLen, patchPtr, patchLen, _idemPtr) => {
|
|
137
|
+
const coll = collOf(db, handle);
|
|
138
|
+
if (coll === null)
|
|
139
|
+
return INVALID_HANDLE;
|
|
140
|
+
if (keyLen > MAX_KEY || patchLen > MAX_VALUE)
|
|
141
|
+
throw new Error('data: key/patch too large');
|
|
142
|
+
const sk = storeKey(coll, readCopy(ref, keyPtr, keyLen));
|
|
143
|
+
if (!STORE.has(sk))
|
|
144
|
+
return PRODUCT_ERR;
|
|
145
|
+
const v = readCopy(ref, patchPtr, patchLen);
|
|
146
|
+
STORE.set(sk, v);
|
|
147
|
+
db.lastResult = v;
|
|
148
|
+
return v.length;
|
|
149
|
+
},
|
|
150
|
+
'data.delete': (handle, keyPtr, keyLen, _idemPtr) => {
|
|
151
|
+
const coll = collOf(db, handle);
|
|
152
|
+
if (coll === null)
|
|
153
|
+
return INVALID_HANDLE;
|
|
154
|
+
STORE.delete(storeKey(coll, readCopy(ref, keyPtr, keyLen)));
|
|
155
|
+
return 0;
|
|
156
|
+
},
|
|
157
|
+
'data.get_delete': (handle, keyPtr, keyLen, _idemPtr) => {
|
|
158
|
+
const coll = collOf(db, handle);
|
|
159
|
+
if (coll === null)
|
|
160
|
+
return INVALID_HANDLE;
|
|
161
|
+
const sk = storeKey(coll, readCopy(ref, keyPtr, keyLen));
|
|
162
|
+
const v = STORE.get(sk);
|
|
163
|
+
if (v === undefined)
|
|
164
|
+
return ABSENT;
|
|
165
|
+
STORE.delete(sk);
|
|
166
|
+
db.lastResult = v;
|
|
167
|
+
return v.length;
|
|
168
|
+
},
|
|
169
|
+
'data.unique_lookup': (handle, keyPtr, keyLen) => {
|
|
170
|
+
const coll = collOf(db, handle);
|
|
171
|
+
if (coll === null)
|
|
172
|
+
return INVALID_HANDLE;
|
|
173
|
+
const v = STORE.get(storeKey(coll, readCopy(ref, keyPtr, keyLen)));
|
|
174
|
+
if (v === undefined)
|
|
175
|
+
return ABSENT;
|
|
176
|
+
db.lastResult = v;
|
|
177
|
+
return v.length;
|
|
178
|
+
},
|
|
179
|
+
'data.unique_claim': (handle, keyPtr, keyLen, valPtr, valLen, _idemPtr) => {
|
|
180
|
+
const coll = collOf(db, handle);
|
|
181
|
+
if (coll === null)
|
|
182
|
+
return INVALID_HANDLE;
|
|
183
|
+
if (keyLen > MAX_KEY || valLen > MAX_VALUE)
|
|
184
|
+
throw new Error('data: key/value too large');
|
|
185
|
+
const sk = storeKey(coll, readCopy(ref, keyPtr, keyLen));
|
|
186
|
+
const owner = readCopy(ref, valPtr, valLen);
|
|
187
|
+
const existing = STORE.get(sk);
|
|
188
|
+
if (existing === undefined) {
|
|
189
|
+
STORE.set(sk, owner);
|
|
190
|
+
return 0;
|
|
191
|
+
}
|
|
192
|
+
if (existing.equals(owner))
|
|
193
|
+
return 2;
|
|
194
|
+
db.lastResult = existing;
|
|
195
|
+
return 1;
|
|
196
|
+
},
|
|
197
|
+
'data.unique_release': (handle, keyPtr, keyLen, valPtr, valLen, _idemPtr) => {
|
|
198
|
+
const coll = collOf(db, handle);
|
|
199
|
+
if (coll === null)
|
|
200
|
+
return INVALID_HANDLE;
|
|
201
|
+
const sk = storeKey(coll, readCopy(ref, keyPtr, keyLen));
|
|
202
|
+
const existing = STORE.get(sk);
|
|
203
|
+
if (existing === undefined)
|
|
204
|
+
return 0;
|
|
205
|
+
if (!existing.equals(readCopy(ref, valPtr, valLen)))
|
|
206
|
+
return PRODUCT_ERR;
|
|
207
|
+
STORE.delete(sk);
|
|
208
|
+
return 0;
|
|
209
|
+
},
|
|
210
|
+
'data.membership_contains': (handle, setPtr, setLen, memberPtr, memberLen) => {
|
|
211
|
+
const coll = collOf(db, handle);
|
|
212
|
+
if (coll === null)
|
|
213
|
+
return INVALID_HANDLE;
|
|
214
|
+
const set = MEMBERS.get(storeKey(coll, readCopy(ref, setPtr, setLen)));
|
|
215
|
+
if (set === undefined)
|
|
216
|
+
return 0;
|
|
217
|
+
return set.has(readCopy(ref, memberPtr, memberLen).toString('latin1')) ? 1 : 0;
|
|
218
|
+
},
|
|
219
|
+
'data.membership_add': (handle, setPtr, setLen, memberPtr, memberLen, _idemPtr) => {
|
|
220
|
+
const coll = collOf(db, handle);
|
|
221
|
+
if (coll === null)
|
|
222
|
+
return INVALID_HANDLE;
|
|
223
|
+
if (setLen > MAX_KEY || memberLen > MAX_VALUE)
|
|
224
|
+
throw new Error('data: set/member too large');
|
|
225
|
+
const sk = storeKey(coll, readCopy(ref, setPtr, setLen));
|
|
226
|
+
const member = readCopy(ref, memberPtr, memberLen);
|
|
227
|
+
let set = MEMBERS.get(sk);
|
|
228
|
+
if (set === undefined) {
|
|
229
|
+
set = new Map();
|
|
230
|
+
MEMBERS.set(sk, set);
|
|
231
|
+
}
|
|
232
|
+
set.set(member.toString('latin1'), member);
|
|
233
|
+
return 0;
|
|
234
|
+
},
|
|
235
|
+
'data.membership_remove': (handle, setPtr, setLen, memberPtr, memberLen, _idemPtr) => {
|
|
236
|
+
const coll = collOf(db, handle);
|
|
237
|
+
if (coll === null)
|
|
238
|
+
return INVALID_HANDLE;
|
|
239
|
+
const set = MEMBERS.get(storeKey(coll, readCopy(ref, setPtr, setLen)));
|
|
240
|
+
if (set !== undefined)
|
|
241
|
+
set.delete(readCopy(ref, memberPtr, memberLen).toString('latin1'));
|
|
242
|
+
return 0;
|
|
243
|
+
},
|
|
244
|
+
'data.membership_list': (handle, setPtr, setLen, limit) => {
|
|
245
|
+
const coll = collOf(db, handle);
|
|
246
|
+
if (coll === null)
|
|
247
|
+
return INVALID_HANDLE;
|
|
248
|
+
const set = MEMBERS.get(storeKey(coll, readCopy(ref, setPtr, setLen)));
|
|
249
|
+
const n = Math.max(0, Math.min(limit, 0xffff));
|
|
250
|
+
const members = set === undefined ? [] : Array.from(set.values()).sort(Buffer.compare).slice(0, n);
|
|
251
|
+
const header = Buffer.alloc(4);
|
|
252
|
+
header.writeUInt32LE(members.length, 0);
|
|
253
|
+
const parts = [header];
|
|
254
|
+
for (const m of members) {
|
|
255
|
+
const h = Buffer.alloc(4);
|
|
256
|
+
h.writeUInt32LE(m.length, 0);
|
|
257
|
+
parts.push(h, m);
|
|
258
|
+
}
|
|
259
|
+
db.lastResult = Buffer.concat(parts);
|
|
260
|
+
return db.lastResult.length;
|
|
261
|
+
},
|
|
262
|
+
'data.view_get': (handle, keyPtr, keyLen) => {
|
|
263
|
+
const coll = collOf(db, handle);
|
|
264
|
+
if (coll === null)
|
|
265
|
+
return INVALID_HANDLE;
|
|
266
|
+
const v = VIEWS.get(storeKey(coll, readCopy(ref, keyPtr, keyLen)));
|
|
267
|
+
if (v === undefined)
|
|
268
|
+
return ABSENT;
|
|
269
|
+
db.lastResult = v;
|
|
270
|
+
return v.length;
|
|
271
|
+
},
|
|
272
|
+
'data.view_publish': (handle, keyPtr, keyLen, valPtr, valLen, _idemPtr) => {
|
|
273
|
+
const coll = collOf(db, handle);
|
|
274
|
+
if (coll === null)
|
|
275
|
+
return INVALID_HANDLE;
|
|
276
|
+
if (keyLen > MAX_KEY || valLen > MAX_VALUE)
|
|
277
|
+
throw new Error('data: key/view too large');
|
|
278
|
+
VIEWS.set(storeKey(coll, readCopy(ref, keyPtr, keyLen)), readCopy(ref, valPtr, valLen));
|
|
279
|
+
return 0;
|
|
280
|
+
},
|
|
281
|
+
'data.counter_get': (handle, keyPtr, keyLen) => {
|
|
282
|
+
const coll = collOf(db, handle);
|
|
283
|
+
if (coll === null)
|
|
284
|
+
return INVALID_HANDLE;
|
|
285
|
+
const sum = COUNTERS.get(storeKey(coll, readCopy(ref, keyPtr, keyLen))) ?? 0n;
|
|
286
|
+
const out = Buffer.alloc(8);
|
|
287
|
+
out.writeBigInt64LE(sum);
|
|
288
|
+
db.lastResult = out;
|
|
289
|
+
return out.length;
|
|
290
|
+
},
|
|
291
|
+
'data.counter_add': (handle, keyPtr, keyLen, delta, _idemPtr) => {
|
|
292
|
+
const coll = collOf(db, handle);
|
|
293
|
+
if (coll === null)
|
|
294
|
+
return INVALID_HANDLE;
|
|
295
|
+
const sk = storeKey(coll, readCopy(ref, keyPtr, keyLen));
|
|
296
|
+
COUNTERS.set(sk, satI64((COUNTERS.get(sk) ?? 0n) + BigInt(delta)));
|
|
297
|
+
return 0;
|
|
298
|
+
},
|
|
299
|
+
'data.append': (handle, keyPtr, keyLen, evPtr, evLen, _idemPtr) => {
|
|
300
|
+
const coll = collOf(db, handle);
|
|
301
|
+
if (coll === null)
|
|
302
|
+
return INVALID_HANDLE;
|
|
303
|
+
if (keyLen > MAX_KEY || evLen > MAX_VALUE)
|
|
304
|
+
throw new Error('data: key/event too large');
|
|
305
|
+
const sk = storeKey(coll, readCopy(ref, keyPtr, keyLen));
|
|
306
|
+
const log = EVENTS.get(sk);
|
|
307
|
+
const ev = readCopy(ref, evPtr, evLen);
|
|
308
|
+
if (log === undefined)
|
|
309
|
+
EVENTS.set(sk, [ev]);
|
|
310
|
+
else
|
|
311
|
+
log.push(ev);
|
|
312
|
+
return 0;
|
|
313
|
+
},
|
|
314
|
+
'data.latest': (handle, keyPtr, keyLen, limit) => {
|
|
315
|
+
const coll = collOf(db, handle);
|
|
316
|
+
if (coll === null)
|
|
317
|
+
return INVALID_HANDLE;
|
|
318
|
+
const log = EVENTS.get(storeKey(coll, readCopy(ref, keyPtr, keyLen))) ?? [];
|
|
319
|
+
const n = Math.max(0, Math.min(limit, 0xffff));
|
|
320
|
+
const newest = log.slice(Math.max(0, log.length - n)).reverse();
|
|
321
|
+
let size = 4;
|
|
322
|
+
for (const ev of newest)
|
|
323
|
+
size += 4 + ev.length;
|
|
324
|
+
const out = Buffer.alloc(size);
|
|
325
|
+
let off = out.writeUInt32LE(newest.length, 0);
|
|
326
|
+
for (const ev of newest) {
|
|
327
|
+
off = out.writeUInt32LE(ev.length, off);
|
|
328
|
+
off += ev.copy(out, off);
|
|
329
|
+
}
|
|
330
|
+
db.lastResult = out;
|
|
331
|
+
return out.length;
|
|
332
|
+
},
|
|
333
|
+
'data.capacity_set_total': (handle, keyPtr, keyLen, total, _idemPtr) => {
|
|
334
|
+
const coll = collOf(db, handle);
|
|
335
|
+
if (coll === null)
|
|
336
|
+
return INVALID_HANDLE;
|
|
337
|
+
const l = capLedger(storeKey(coll, readCopy(ref, keyPtr, keyLen)));
|
|
338
|
+
const t = BigInt(total);
|
|
339
|
+
l.total = satI64(t < 0n ? 0n : t);
|
|
340
|
+
return 0;
|
|
341
|
+
},
|
|
342
|
+
'data.capacity_available': (handle, keyPtr, keyLen) => {
|
|
343
|
+
const coll = collOf(db, handle);
|
|
344
|
+
if (coll === null)
|
|
345
|
+
return INVALID_HANDLE;
|
|
346
|
+
const l = capLedger(storeKey(coll, readCopy(ref, keyPtr, keyLen)));
|
|
347
|
+
capPrune(l, Date.now());
|
|
348
|
+
const avail = l.total - l.confirmed - capHeld(l);
|
|
349
|
+
const out = Buffer.alloc(8);
|
|
350
|
+
out.writeBigInt64LE(avail < 0n ? 0n : avail);
|
|
351
|
+
db.lastResult = out;
|
|
352
|
+
return out.length;
|
|
353
|
+
},
|
|
354
|
+
'data.capacity_reserve': (handle, keyPtr, keyLen, amount, ttlMs, _idemPtr) => {
|
|
355
|
+
const coll = collOf(db, handle);
|
|
356
|
+
if (coll === null)
|
|
357
|
+
return INVALID_HANDLE;
|
|
358
|
+
const want = BigInt(amount);
|
|
359
|
+
if (want <= 0n)
|
|
360
|
+
return ABSENT;
|
|
361
|
+
const l = capLedger(storeKey(coll, readCopy(ref, keyPtr, keyLen)));
|
|
362
|
+
const now = Date.now();
|
|
363
|
+
capPrune(l, now);
|
|
364
|
+
if (l.total - l.confirmed - capHeld(l) < want)
|
|
365
|
+
return ABSENT;
|
|
366
|
+
const id = l.nextId++;
|
|
367
|
+
l.holds.set(id, { amount: want, expiresMs: now + Math.max(0, Number(ttlMs)) });
|
|
368
|
+
const out = Buffer.alloc(8);
|
|
369
|
+
out.writeBigUInt64LE(id);
|
|
370
|
+
db.lastResult = out;
|
|
371
|
+
return out.length;
|
|
372
|
+
},
|
|
373
|
+
'data.capacity_confirm': (handle, keyPtr, keyLen, reservationId, _idemPtr) => {
|
|
374
|
+
const coll = collOf(db, handle);
|
|
375
|
+
if (coll === null)
|
|
376
|
+
return INVALID_HANDLE;
|
|
377
|
+
const l = capLedger(storeKey(coll, readCopy(ref, keyPtr, keyLen)));
|
|
378
|
+
capPrune(l, Date.now());
|
|
379
|
+
const h = l.holds.get(BigInt(reservationId));
|
|
380
|
+
if (h === undefined)
|
|
381
|
+
return 0;
|
|
382
|
+
l.holds.delete(BigInt(reservationId));
|
|
383
|
+
l.confirmed = satI64(l.confirmed + h.amount);
|
|
384
|
+
return 1;
|
|
385
|
+
},
|
|
386
|
+
'data.capacity_cancel': (handle, keyPtr, keyLen, reservationId, _idemPtr) => {
|
|
387
|
+
const coll = collOf(db, handle);
|
|
388
|
+
if (coll === null)
|
|
389
|
+
return INVALID_HANDLE;
|
|
390
|
+
const l = capLedger(storeKey(coll, readCopy(ref, keyPtr, keyLen)));
|
|
391
|
+
capPrune(l, Date.now());
|
|
392
|
+
return l.holds.delete(BigInt(reservationId)) ? 1 : 0;
|
|
393
|
+
},
|
|
394
|
+
'data.take_result': (outPtr, outCap) => {
|
|
395
|
+
const v = db.lastResult;
|
|
396
|
+
if (v === null)
|
|
397
|
+
return 0;
|
|
398
|
+
if (v.length > outCap)
|
|
399
|
+
return TOO_SMALL;
|
|
400
|
+
const m = mem(ref);
|
|
401
|
+
if (outPtr < 0 || outPtr + v.length > m.length)
|
|
402
|
+
throw new Error('data: take_result out of bounds');
|
|
403
|
+
v.copy(m, outPtr);
|
|
404
|
+
db.lastResult = null;
|
|
405
|
+
return v.length;
|
|
406
|
+
},
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
export function __resetDbForTests() {
|
|
410
|
+
STORE.clear();
|
|
411
|
+
VIEWS.clear();
|
|
412
|
+
MEMBERS.clear();
|
|
413
|
+
COUNTERS.clear();
|
|
414
|
+
EVENTS.clear();
|
|
415
|
+
CAPACITY.clear();
|
|
416
|
+
}
|
|
Binary file
|
|
@@ -46,7 +46,9 @@ export function resolveEmailConfig(cfg, reserved) {
|
|
|
46
46
|
else if (providerId === 'gmail' || providerId === 'smtp') {
|
|
47
47
|
provider = 'smtp';
|
|
48
48
|
const isGmail = providerId === 'gmail';
|
|
49
|
-
const host = envOf(reserved, 'SMTP_HOST') ??
|
|
49
|
+
const host = envOf(reserved, 'SMTP_HOST') ??
|
|
50
|
+
c.smtp?.host?.trim() ??
|
|
51
|
+
(isGmail ? 'smtp.gmail.com' : '');
|
|
50
52
|
if (!host) {
|
|
51
53
|
return { config: null, warning: 'provider `smtp` requires TOIL_EMAIL_SMTP_HOST' };
|
|
52
54
|
}
|
|
@@ -55,7 +57,10 @@ export function resolveEmailConfig(cfg, reserved) {
|
|
|
55
57
|
smtp = { host, port, user };
|
|
56
58
|
}
|
|
57
59
|
else {
|
|
58
|
-
return {
|
|
60
|
+
return {
|
|
61
|
+
config: null,
|
|
62
|
+
warning: `unknown email provider "${providerId}" (resend|gmail|smtp)`,
|
|
63
|
+
};
|
|
59
64
|
}
|
|
60
65
|
return {
|
|
61
66
|
config: {
|
|
@@ -10,10 +10,7 @@ export function validRecipient(s) {
|
|
|
10
10
|
if (parts.length !== 2)
|
|
11
11
|
return false;
|
|
12
12
|
const [local, domain] = parts;
|
|
13
|
-
return (local.length > 0 &&
|
|
14
|
-
domain.includes('.') &&
|
|
15
|
-
!domain.startsWith('.') &&
|
|
16
|
-
!domain.endsWith('.'));
|
|
13
|
+
return (local.length > 0 && domain.includes('.') && !domain.startsWith('.') && !domain.endsWith('.'));
|
|
17
14
|
}
|
|
18
15
|
export function validFrom(s) {
|
|
19
16
|
return (Buffer.byteLength(s, 'utf8') <= 320 &&
|
|
@@ -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
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { EmailBackendConfig } from 'toiljs/shared';
|
|
2
2
|
import { type ViteTarget } from './proxy.js';
|
|
3
|
-
export { METHOD_CODES, encodeRequestEnvelope, decodeResponseEnvelope, unpackHandleResult } from './envelope.js';
|
|
3
|
+
export { METHOD_CODES, encodeRequestEnvelope, decodeResponseEnvelope, unpackHandleResult, } from './envelope.js';
|
|
4
4
|
export type { EnvelopeRequest, EnvelopeResponse } from './envelope.js';
|
|
5
5
|
export { WasmServerModule, WasmAbortError, UNHANDLED_HEADER } from './module.js';
|
|
6
6
|
export type { WasmDispatchResult } from './module.js';
|
package/build/devserver/index.js
CHANGED
|
@@ -7,7 +7,7 @@ import { initEmailService } from './email/index.js';
|
|
|
7
7
|
import { METHOD_CODES } from './envelope.js';
|
|
8
8
|
import { WasmServerModule } from './module.js';
|
|
9
9
|
import { proxyToVite, wireWebsocketProxy } from './proxy.js';
|
|
10
|
-
export { METHOD_CODES, encodeRequestEnvelope, decodeResponseEnvelope, unpackHandleResult } from './envelope.js';
|
|
10
|
+
export { METHOD_CODES, encodeRequestEnvelope, decodeResponseEnvelope, unpackHandleResult, } from './envelope.js';
|
|
11
11
|
export { WasmServerModule, WasmAbortError, UNHANDLED_HEADER } from './module.js';
|
|
12
12
|
export { buildHostImports, freshDispatchState } from './host.js';
|
|
13
13
|
const DEFAULT_MAX_BODY_LENGTH = 1024 * 1024 * 8;
|
|
@@ -146,7 +146,8 @@ export async function startDevServer(options) {
|
|
|
146
146
|
}
|
|
147
147
|
}
|
|
148
148
|
catch (e) {
|
|
149
|
-
process.stdout.write(pc.red(` ✗ ${request.method} ${request.path} server error: ${String(e)}`) +
|
|
149
|
+
process.stdout.write(pc.red(` ✗ ${request.method} ${request.path} server error: ${String(e)}`) +
|
|
150
|
+
'\n');
|
|
150
151
|
response.status(500).send('internal error\n');
|
|
151
152
|
return;
|
|
152
153
|
}
|
|
@@ -5,13 +5,58 @@ export { WasmAbortError } from './host.js';
|
|
|
5
5
|
export const UNHANDLED_HEADER = 'x-toil-unhandled';
|
|
6
6
|
const WASM_PAGE = 65536;
|
|
7
7
|
const PROVIDED_IMPORTS = new Set([
|
|
8
|
-
'abort',
|
|
9
|
-
'
|
|
10
|
-
'
|
|
11
|
-
'
|
|
12
|
-
'
|
|
13
|
-
'
|
|
14
|
-
'
|
|
8
|
+
'abort',
|
|
9
|
+
'set_status',
|
|
10
|
+
'set_header',
|
|
11
|
+
'respond_file',
|
|
12
|
+
'thread_spawn',
|
|
13
|
+
'Date.now',
|
|
14
|
+
'client_ip',
|
|
15
|
+
'ratelimit_check',
|
|
16
|
+
'email_send',
|
|
17
|
+
'env_get',
|
|
18
|
+
'env_get_secure',
|
|
19
|
+
'crypto.fill_random',
|
|
20
|
+
'crypto.random_uuid',
|
|
21
|
+
'crypto.take_result',
|
|
22
|
+
'crypto.digest',
|
|
23
|
+
'crypto.import_key',
|
|
24
|
+
'crypto.export_key',
|
|
25
|
+
'crypto.encrypt',
|
|
26
|
+
'crypto.decrypt',
|
|
27
|
+
'crypto.sign',
|
|
28
|
+
'crypto.verify',
|
|
29
|
+
'crypto.derive_bits',
|
|
30
|
+
'crypto.mldsa_verify',
|
|
31
|
+
'crypto.mlkem_decapsulate',
|
|
32
|
+
'crypto.voprf_evaluate',
|
|
33
|
+
'data.resolve_collection',
|
|
34
|
+
'data.get',
|
|
35
|
+
'data.get_many',
|
|
36
|
+
'data.exists',
|
|
37
|
+
'data.create',
|
|
38
|
+
'data.patch',
|
|
39
|
+
'data.delete',
|
|
40
|
+
'data.get_delete',
|
|
41
|
+
'data.unique_lookup',
|
|
42
|
+
'data.unique_claim',
|
|
43
|
+
'data.unique_release',
|
|
44
|
+
'data.view_get',
|
|
45
|
+
'data.view_publish',
|
|
46
|
+
'data.membership_contains',
|
|
47
|
+
'data.membership_add',
|
|
48
|
+
'data.membership_remove',
|
|
49
|
+
'data.membership_list',
|
|
50
|
+
'data.counter_get',
|
|
51
|
+
'data.counter_add',
|
|
52
|
+
'data.append',
|
|
53
|
+
'data.latest',
|
|
54
|
+
'data.capacity_set_total',
|
|
55
|
+
'data.capacity_available',
|
|
56
|
+
'data.capacity_reserve',
|
|
57
|
+
'data.capacity_confirm',
|
|
58
|
+
'data.capacity_cancel',
|
|
59
|
+
'data.take_result',
|
|
15
60
|
]);
|
|
16
61
|
export class WasmServerModule {
|
|
17
62
|
wasmPath;
|
package/build/devserver/proxy.js
CHANGED
|
@@ -90,7 +90,8 @@ export function wireWebsocketProxy(app, target) {
|
|
|
90
90
|
pending.push(m);
|
|
91
91
|
});
|
|
92
92
|
ws.on('close', () => {
|
|
93
|
-
if (upstream.readyState === WebSocket.OPEN ||
|
|
93
|
+
if (upstream.readyState === WebSocket.OPEN ||
|
|
94
|
+
upstream.readyState === WebSocket.CONNECTING) {
|
|
94
95
|
upstream.close();
|
|
95
96
|
}
|
|
96
97
|
});
|