toiljs 0.0.119 → 0.0.121
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/README.md +1 -0
- package/build/cli/.tsbuildinfo +1 -1
- package/build/cli/index.js +1106 -91
- package/build/client/.tsbuildinfo +1 -1
- package/build/client/head/head.js +1 -1
- package/build/client/routing/error-boundary.js +2 -0
- package/build/client/routing/loader.js +2 -2
- package/build/compiler/.tsbuildinfo +1 -1
- package/build/compiler/index.js +4 -2
- package/build/compiler/toil-docs.generated.js +10 -10
- package/build/devserver/.tsbuildinfo +1 -1
- package/build/devserver/daemon/index.js +3 -2
- package/build/devserver/db/database.d.ts +5 -0
- package/build/devserver/db/database.js +107 -23
- package/build/devserver/db/types.d.ts +1 -0
- package/build/devserver/db/types.js +1 -1
- package/build/devserver/email/caps.js +0 -0
- package/build/devserver/http/cache.js +0 -0
- package/build/devserver/runtime/module.js +8 -1
- package/package.json +4 -3
- package/server/auth/AuthController.ts +4 -5
- package/server/auth/AuthReads.ts +15 -0
- package/server/auth/AuthStateDb.ts +51 -0
- package/server/auth/FactorSettings.ts +6 -27
- package/server/auth/SessionEpochs.ts +6 -24
- package/server/globals/auth.ts +30 -4
- package/server/runtime/request.ts +6 -0
- package/server/auth/SessionRevocations.ts +0 -13
|
@@ -20,6 +20,16 @@ function readKey(ref, ptr, len) {
|
|
|
20
20
|
throw new Error('data: key too long');
|
|
21
21
|
return readCopy(ref, ptr, len);
|
|
22
22
|
}
|
|
23
|
+
function checkValueSize(length, name = 'value') {
|
|
24
|
+
if (length > MAX_VALUE)
|
|
25
|
+
throw new Error(`data: encoded ${name} is ${String(length)} bytes; maximum is ${String(MAX_VALUE)} bytes (2 MiB). ` +
|
|
26
|
+
'Split large files across records or store their content outside ToilDB.');
|
|
27
|
+
}
|
|
28
|
+
function checkKeyValueSize(keyLength, valueLength, name = 'value') {
|
|
29
|
+
if (keyLength > MAX_KEY)
|
|
30
|
+
throw new Error('data: key too long');
|
|
31
|
+
checkValueSize(valueLength, name);
|
|
32
|
+
}
|
|
23
33
|
function storeKey(collection, key) {
|
|
24
34
|
return collection + '\0' + key.toString('latin1');
|
|
25
35
|
}
|
|
@@ -123,6 +133,8 @@ var DbOp;
|
|
|
123
133
|
DbOp[DbOp["ViewPublish"] = 26] = "ViewPublish";
|
|
124
134
|
DbOp[DbOp["CapacitySetTotal"] = 27] = "CapacitySetTotal";
|
|
125
135
|
DbOp[DbOp["EventsSince"] = 28] = "EventsSince";
|
|
136
|
+
DbOp[DbOp["EventsGet"] = 29] = "EventsGet";
|
|
137
|
+
DbOp[DbOp["EventsLast"] = 30] = "EventsLast";
|
|
126
138
|
})(DbOp || (DbOp = {}));
|
|
127
139
|
var UpsertResult;
|
|
128
140
|
(function (UpsertResult) {
|
|
@@ -141,6 +153,8 @@ function isReadOp(op) {
|
|
|
141
153
|
op === DbOp.UniqueLookup ||
|
|
142
154
|
op === DbOp.Latest ||
|
|
143
155
|
op === DbOp.EventsSince ||
|
|
156
|
+
op === DbOp.EventsGet ||
|
|
157
|
+
op === DbOp.EventsLast ||
|
|
144
158
|
op === DbOp.CapacityAvailable);
|
|
145
159
|
}
|
|
146
160
|
function isScanOp(op) {
|
|
@@ -191,6 +205,7 @@ export class DevDatabase {
|
|
|
191
205
|
events = new Map();
|
|
192
206
|
deriveCheckpoints = new Map();
|
|
193
207
|
eventDedup = new Map();
|
|
208
|
+
eventPositions = new Map();
|
|
194
209
|
capacity = new Map();
|
|
195
210
|
versions = new Map();
|
|
196
211
|
eventVersions = new Map();
|
|
@@ -267,6 +282,7 @@ export class DevDatabase {
|
|
|
267
282
|
counterIdem: {},
|
|
268
283
|
events: {},
|
|
269
284
|
eventDedup: {},
|
|
285
|
+
eventPositions: {},
|
|
270
286
|
deriveCheckpoints: {},
|
|
271
287
|
capacity: {},
|
|
272
288
|
};
|
|
@@ -297,6 +313,8 @@ export class DevDatabase {
|
|
|
297
313
|
const ver = this.eventVersions.get(k) ?? [];
|
|
298
314
|
snap.events[k] = log.map((b, i) => ({ v: b.toString('base64'), sv: ver[i] ?? 0 }));
|
|
299
315
|
}
|
|
316
|
+
for (const [k, positions] of this.eventPositions)
|
|
317
|
+
snap.eventPositions[k] = [...positions];
|
|
300
318
|
for (const [k, s] of this.eventDedup)
|
|
301
319
|
snap.eventDedup[k] = [...s];
|
|
302
320
|
for (const [k, v] of this.deriveCheckpoints)
|
|
@@ -366,6 +384,8 @@ export class DevDatabase {
|
|
|
366
384
|
this.events.set(k, log.map((e) => Buffer.from(e.v, 'base64')));
|
|
367
385
|
this.eventVersions.set(k, log.map((e) => e.sv));
|
|
368
386
|
}
|
|
387
|
+
for (const [k, positions] of Object.entries(snap.eventPositions ?? {}))
|
|
388
|
+
this.eventPositions.set(k, new Map(positions));
|
|
369
389
|
for (const [k, ids] of Object.entries(snap.eventDedup ?? {}))
|
|
370
390
|
this.eventDedup.set(k, new Set(ids));
|
|
371
391
|
for (const [k, v] of Object.entries(snap.deriveCheckpoints ?? {}))
|
|
@@ -398,6 +418,7 @@ export class DevDatabase {
|
|
|
398
418
|
this.eventVersions.clear();
|
|
399
419
|
this.events.clear();
|
|
400
420
|
this.eventDedup.clear();
|
|
421
|
+
this.eventPositions.clear();
|
|
401
422
|
this.deriveCheckpoints.clear();
|
|
402
423
|
this.capacity.clear();
|
|
403
424
|
}
|
|
@@ -470,15 +491,20 @@ export class DevDatabase {
|
|
|
470
491
|
return v.length;
|
|
471
492
|
}
|
|
472
493
|
getMany(ref, db, handle, keysPtr, keysLen) {
|
|
473
|
-
const coll = collForOp(db, handle, DbOp.GetMany, CollectionFamily.Record, CollectionFamily.View);
|
|
494
|
+
const coll = collForOp(db, handle, DbOp.GetMany, CollectionFamily.Record, CollectionFamily.View, CollectionFamily.Unique);
|
|
474
495
|
if (typeof coll === 'number')
|
|
475
496
|
return coll;
|
|
476
497
|
if (keysLen > MAX_VALUE)
|
|
477
|
-
throw new Error(
|
|
498
|
+
throw new Error(`data: encoded keys blob is ${String(keysLen)} bytes; maximum is ${String(MAX_VALUE)} bytes (2 MiB). Use smaller getMany batches.`);
|
|
478
499
|
const table = coll.family === CollectionFamily.View ? this.views : this.store;
|
|
479
500
|
const r = new DataReader(readCopy(ref, keysPtr, keysLen));
|
|
480
501
|
const count = r.readU32();
|
|
481
|
-
|
|
502
|
+
const maxKeys = db.functionKind === DbFunctionKind.Query
|
|
503
|
+
? 32
|
|
504
|
+
: db.functionKind === DbFunctionKind.Action
|
|
505
|
+
? 64
|
|
506
|
+
: 1024;
|
|
507
|
+
if (count > maxKeys)
|
|
482
508
|
return TOO_MANY_KEYS;
|
|
483
509
|
const w = new DataWriter();
|
|
484
510
|
w.writeU32(count);
|
|
@@ -575,8 +601,7 @@ export class DevDatabase {
|
|
|
575
601
|
const coll = collForOp(db, handle, DbOp.Create, CollectionFamily.Record);
|
|
576
602
|
if (typeof coll === 'number')
|
|
577
603
|
return coll;
|
|
578
|
-
|
|
579
|
-
throw new Error('data: key/value too large');
|
|
604
|
+
checkKeyValueSize(keyLen, valLen);
|
|
580
605
|
const key = readKey(ref, keyPtr, keyLen);
|
|
581
606
|
const value = readCopy(ref, valPtr, valLen);
|
|
582
607
|
const idem = readIdem(ref, idemPtr);
|
|
@@ -600,8 +625,7 @@ export class DevDatabase {
|
|
|
600
625
|
const coll = collForOp(db, handle, DbOp.Patch, CollectionFamily.Record);
|
|
601
626
|
if (typeof coll === 'number')
|
|
602
627
|
return coll;
|
|
603
|
-
|
|
604
|
-
throw new Error('data: key/patch too large');
|
|
628
|
+
checkKeyValueSize(keyLen, patchLen, 'patch');
|
|
605
629
|
const key = readKey(ref, keyPtr, keyLen);
|
|
606
630
|
const v = readCopy(ref, patchPtr, patchLen);
|
|
607
631
|
const idem = readIdem(ref, idemPtr);
|
|
@@ -627,8 +651,7 @@ export class DevDatabase {
|
|
|
627
651
|
const coll = collForOp(db, handle, DbOp.Upsert, CollectionFamily.Record);
|
|
628
652
|
if (typeof coll === 'number')
|
|
629
653
|
return coll;
|
|
630
|
-
|
|
631
|
-
throw new Error('data: key/value too large');
|
|
654
|
+
checkKeyValueSize(keyLen, valLen);
|
|
632
655
|
const key = readKey(ref, keyPtr, keyLen);
|
|
633
656
|
const value = readCopy(ref, valPtr, valLen);
|
|
634
657
|
const sk = storeKey(coll.name, key);
|
|
@@ -699,8 +722,7 @@ export class DevDatabase {
|
|
|
699
722
|
const coll = collForOp(db, handle, DbOp.UniqueClaim, CollectionFamily.Unique);
|
|
700
723
|
if (typeof coll === 'number')
|
|
701
724
|
return coll;
|
|
702
|
-
|
|
703
|
-
throw new Error('data: key/value too large');
|
|
725
|
+
checkKeyValueSize(keyLen, valLen);
|
|
704
726
|
const sk = storeKey(coll.name, readKey(ref, keyPtr, keyLen));
|
|
705
727
|
const owner = readCopy(ref, valPtr, valLen);
|
|
706
728
|
const idem = readIdem(ref, idemPtr)?.toString('hex') ?? '';
|
|
@@ -745,8 +767,7 @@ export class DevDatabase {
|
|
|
745
767
|
const coll = collForOp(db, handle, DbOp.MembershipAdd, CollectionFamily.Membership);
|
|
746
768
|
if (typeof coll === 'number')
|
|
747
769
|
return coll;
|
|
748
|
-
|
|
749
|
-
throw new Error('data: set/member too large');
|
|
770
|
+
checkKeyValueSize(setLen, memberLen, 'member');
|
|
750
771
|
const sk = storeKey(coll.name, readKey(ref, setPtr, setLen));
|
|
751
772
|
const member = readCopy(ref, memberPtr, memberLen);
|
|
752
773
|
let set = this.members.get(sk);
|
|
@@ -782,7 +803,11 @@ export class DevDatabase {
|
|
|
782
803
|
const set = this.members.get(sk);
|
|
783
804
|
const mv = this.memberVersions.get(sk);
|
|
784
805
|
const n = Math.max(0, Math.min(limit, 0xffff));
|
|
785
|
-
const members = set === undefined
|
|
806
|
+
const members = set === undefined
|
|
807
|
+
? []
|
|
808
|
+
: Array.from(set.values())
|
|
809
|
+
.sort((a, b) => Buffer.compare(a, b))
|
|
810
|
+
.slice(0, n);
|
|
786
811
|
const w = new DataWriter();
|
|
787
812
|
w.writeU32(members.length);
|
|
788
813
|
for (const m of members) {
|
|
@@ -807,8 +832,7 @@ export class DevDatabase {
|
|
|
807
832
|
const coll = collForOp(db, handle, DbOp.ViewPublish, CollectionFamily.View);
|
|
808
833
|
if (typeof coll === 'number')
|
|
809
834
|
return coll;
|
|
810
|
-
|
|
811
|
-
throw new Error('data: key/view too large');
|
|
835
|
+
checkKeyValueSize(keyLen, valLen, 'view');
|
|
812
836
|
const sk = storeKey(coll.name, readKey(ref, keyPtr, keyLen));
|
|
813
837
|
this.views.set(sk, readCopy(ref, valPtr, valLen));
|
|
814
838
|
this.stampVersion(coll, sk);
|
|
@@ -852,8 +876,7 @@ export class DevDatabase {
|
|
|
852
876
|
const coll = collForOp(db, handle, DbOp.Append, CollectionFamily.Events);
|
|
853
877
|
if (typeof coll === 'number')
|
|
854
878
|
return coll;
|
|
855
|
-
|
|
856
|
-
throw new Error('data: key/event too large');
|
|
879
|
+
checkKeyValueSize(keyLen, evLen, 'event');
|
|
857
880
|
const key = readKey(ref, keyPtr, keyLen);
|
|
858
881
|
const sk = storeKey(coll.name, key);
|
|
859
882
|
const idem = readIdem(ref, idemPtr);
|
|
@@ -888,8 +911,7 @@ export class DevDatabase {
|
|
|
888
911
|
const coll = collForOp(db, handle, DbOp.AppendOnce, CollectionFamily.Events);
|
|
889
912
|
if (typeof coll === 'number')
|
|
890
913
|
return coll;
|
|
891
|
-
|
|
892
|
-
throw new Error('data: key/event too large');
|
|
914
|
+
checkKeyValueSize(keyLen, evLen, 'event');
|
|
893
915
|
const sk = storeKey(coll.name, readKey(ref, keyPtr, keyLen));
|
|
894
916
|
const evid = readCopy(ref, evidPtr, evidLen).toString('latin1');
|
|
895
917
|
let seen = this.eventDedup.get(sk);
|
|
@@ -902,6 +924,7 @@ export class DevDatabase {
|
|
|
902
924
|
const ev = readCopy(ref, evPtr, evLen);
|
|
903
925
|
const sv = this.currentSchemaVersion(coll);
|
|
904
926
|
const log = this.events.get(sk);
|
|
927
|
+
const position = log?.length ?? 0;
|
|
905
928
|
if (log === undefined) {
|
|
906
929
|
this.events.set(sk, [ev]);
|
|
907
930
|
this.eventVersions.set(sk, [sv]);
|
|
@@ -912,16 +935,73 @@ export class DevDatabase {
|
|
|
912
935
|
versions.push(sv);
|
|
913
936
|
this.eventVersions.set(sk, versions);
|
|
914
937
|
}
|
|
938
|
+
let positions = this.eventPositions.get(sk);
|
|
939
|
+
if (positions === undefined) {
|
|
940
|
+
positions = new Map();
|
|
941
|
+
this.eventPositions.set(sk, positions);
|
|
942
|
+
}
|
|
943
|
+
positions.set(evid, position);
|
|
915
944
|
seen.add(evid);
|
|
916
945
|
this.recordWrite(db, coll);
|
|
917
946
|
return 1;
|
|
918
947
|
}
|
|
948
|
+
eventsGet(ref, db, handle, keyPtr, keyLen, evidPtr, evidLen) {
|
|
949
|
+
const coll = collForOp(db, handle, DbOp.EventsGet, CollectionFamily.Events);
|
|
950
|
+
if (typeof coll === 'number')
|
|
951
|
+
return coll;
|
|
952
|
+
if (evidLen > MAX_KEY)
|
|
953
|
+
throw new Error('data: event ID too large');
|
|
954
|
+
const sk = storeKey(coll.name, readKey(ref, keyPtr, keyLen));
|
|
955
|
+
const id = readCopy(ref, evidPtr, evidLen).toString('latin1');
|
|
956
|
+
const position = this.eventPositions.get(sk)?.get(id);
|
|
957
|
+
if (position === undefined && this.eventDedup.get(sk)?.has(id))
|
|
958
|
+
return UNAVAILABLE;
|
|
959
|
+
return this.eventResult(db, sk, position);
|
|
960
|
+
}
|
|
961
|
+
eventsLast(ref, db, handle, keyPtr, keyLen) {
|
|
962
|
+
const coll = collForOp(db, handle, DbOp.EventsLast, CollectionFamily.Events);
|
|
963
|
+
if (typeof coll === 'number')
|
|
964
|
+
return coll;
|
|
965
|
+
const sk = storeKey(coll.name, readKey(ref, keyPtr, keyLen));
|
|
966
|
+
return this.eventResult(db, sk, (this.events.get(sk)?.length ?? 0) - 1);
|
|
967
|
+
}
|
|
968
|
+
eventResult(db, sk, position) {
|
|
969
|
+
const value = position === undefined ? undefined : this.events.get(sk)?.[position];
|
|
970
|
+
db.lastResult = value === undefined ? null : Buffer.from(value);
|
|
971
|
+
db.lastResultVersion =
|
|
972
|
+
value === undefined || position === undefined
|
|
973
|
+
? -1
|
|
974
|
+
: (this.eventVersions.get(sk)?.[position] ?? 0);
|
|
975
|
+
return value === undefined ? ABSENT : value.length;
|
|
976
|
+
}
|
|
977
|
+
compareExchange(ref, db, handle, keyPtr, keyLen, expectedPtr, expectedLen, valuePtr, valueLen) {
|
|
978
|
+
const coll = collForOp(db, handle, DbOp.Patch, CollectionFamily.Record);
|
|
979
|
+
if (typeof coll === 'number')
|
|
980
|
+
return coll;
|
|
981
|
+
checkKeyValueSize(keyLen, valueLen);
|
|
982
|
+
checkValueSize(expectedLen, 'expected value');
|
|
983
|
+
if (expectedLen < -1)
|
|
984
|
+
throw new Error('data: invalid CAS expected length');
|
|
985
|
+
const key = readKey(ref, keyPtr, keyLen);
|
|
986
|
+
const value = readCopy(ref, valuePtr, valueLen);
|
|
987
|
+
if (this.uniqueValuesOf(coll, value).length > 0)
|
|
988
|
+
throw new Error('CAS does not support @unique fields');
|
|
989
|
+
const sk = storeKey(coll.name, key);
|
|
990
|
+
const prior = this.store.get(sk);
|
|
991
|
+
if (expectedLen === -1
|
|
992
|
+
? prior !== undefined
|
|
993
|
+
: prior === undefined || !prior.equals(readCopy(ref, expectedPtr, expectedLen)))
|
|
994
|
+
return 0;
|
|
995
|
+
this.store.set(sk, value);
|
|
996
|
+
this.stampVersion(coll, sk);
|
|
997
|
+
this.recordWrite(db, coll);
|
|
998
|
+
return 1;
|
|
999
|
+
}
|
|
919
1000
|
enqueue(ref, db, handle, keyPtr, keyLen, valPtr, valLen, idemPtr) {
|
|
920
1001
|
const coll = collForOp(db, handle, DbOp.Enqueue, CollectionFamily.Record);
|
|
921
1002
|
if (typeof coll === 'number')
|
|
922
1003
|
return coll;
|
|
923
|
-
|
|
924
|
-
throw new Error('data: key/value too large');
|
|
1004
|
+
checkKeyValueSize(keyLen, valLen);
|
|
925
1005
|
const key = readKey(ref, keyPtr, keyLen);
|
|
926
1006
|
const value = readCopy(ref, valPtr, valLen);
|
|
927
1007
|
const idem = readIdem(ref, idemPtr);
|
|
@@ -964,7 +1044,7 @@ export class DevDatabase {
|
|
|
964
1044
|
if (typeof coll === 'number')
|
|
965
1045
|
return coll;
|
|
966
1046
|
const sk = storeKey(coll.name, readKey(ref, keyPtr, keyLen));
|
|
967
|
-
const ckKey = `${db.deriveId}
|
|
1047
|
+
const ckKey = `${db.deriveId}\0${sk}`;
|
|
968
1048
|
const log = this.events.get(sk) ?? [];
|
|
969
1049
|
const vers = this.eventVersions.get(sk) ?? [];
|
|
970
1050
|
const cursor = db.pendingCheckpoints.get(ckKey) ?? this.deriveCheckpoints.get(ckKey) ?? 0;
|
|
@@ -1160,8 +1240,12 @@ export function buildDatabaseImports(ref, db) {
|
|
|
1160
1240
|
'data.counter_add': (handle, keyPtr, keyLen, delta, idemPtr) => devDb.counterAdd(ref, db, handle, keyPtr, keyLen, delta, idemPtr),
|
|
1161
1241
|
'data.append': (handle, keyPtr, keyLen, evPtr, evLen, idemPtr) => devDb.append(ref, db, handle, keyPtr, keyLen, evPtr, evLen, idemPtr),
|
|
1162
1242
|
'data.append_once': (handle, keyPtr, keyLen, evidPtr, evidLen, evPtr, evLen) => devDb.appendOnce(ref, db, handle, keyPtr, keyLen, evidPtr, evidLen, evPtr, evLen),
|
|
1243
|
+
'data.get_current': (handle, kp, kl) => devDb.get(ref, db, handle, kp, kl),
|
|
1244
|
+
'data.compare_exchange': (handle, kp, kl, ep, el, vp, vl) => devDb.compareExchange(ref, db, handle, kp, kl, ep, el, vp, vl),
|
|
1163
1245
|
'data.enqueue': (handle, keyPtr, keyLen, valPtr, valLen, idemPtr) => devDb.enqueue(ref, db, handle, keyPtr, keyLen, valPtr, valLen, idemPtr),
|
|
1164
1246
|
'data.latest': (handle, keyPtr, keyLen, limit) => devDb.latest(ref, db, handle, keyPtr, keyLen, limit),
|
|
1247
|
+
'data.events_get': (handle, keyPtr, keyLen, evidPtr, evidLen) => devDb.eventsGet(ref, db, handle, keyPtr, keyLen, evidPtr, evidLen),
|
|
1248
|
+
'data.events_last': (handle, keyPtr, keyLen) => devDb.eventsLast(ref, db, handle, keyPtr, keyLen),
|
|
1165
1249
|
'data.events_since': (handle, keyPtr, keyLen, limit) => devDb.since(ref, db, handle, keyPtr, keyLen, limit),
|
|
1166
1250
|
'data.capacity_set_total': (handle, keyPtr, keyLen, total, _idemPtr) => devDb.capacitySetTotal(ref, db, handle, keyPtr, keyLen, total),
|
|
1167
1251
|
'data.capacity_available': (handle, keyPtr, keyLen) => devDb.capacityAvailable(ref, db, handle, keyPtr, keyLen),
|
|
@@ -34,7 +34,7 @@ export const MAX_RESERVATIONS = 4096;
|
|
|
34
34
|
export const MAX_RESERVATION_TTL_MS = 86_400_000;
|
|
35
35
|
export const MAX_NAME = 512;
|
|
36
36
|
export const MAX_KEY = 4096;
|
|
37
|
-
export const MAX_VALUE =
|
|
37
|
+
export const MAX_VALUE = 2 * 1024 * 1024;
|
|
38
38
|
export const DEFAULT_FILL_WAIT_MS = 50;
|
|
39
39
|
export const MAX_FILL_WAIT_MS = 60_000;
|
|
40
40
|
const I64_MIN = -(2n ** 63n);
|
|
Binary file
|
|
Binary file
|
|
@@ -77,8 +77,12 @@ const PROVIDED_IMPORTS = new Set([
|
|
|
77
77
|
'data.append',
|
|
78
78
|
'data.append_once',
|
|
79
79
|
'data.enqueue',
|
|
80
|
+
'data.compare_exchange',
|
|
81
|
+
'data.get_current',
|
|
80
82
|
'data.latest',
|
|
81
83
|
'data.events_since',
|
|
84
|
+
'data.events_get',
|
|
85
|
+
'data.events_last',
|
|
82
86
|
'data.capacity_set_total',
|
|
83
87
|
'data.capacity_available',
|
|
84
88
|
'data.capacity_reserve',
|
|
@@ -141,7 +145,10 @@ export class WasmServerModule {
|
|
|
141
145
|
state.clientIp = req.clientIp ?? '';
|
|
142
146
|
const rpcPath = req.path.split('?')[0] ?? req.path;
|
|
143
147
|
const rpcMethod = req.method.toUpperCase();
|
|
144
|
-
const rpcMutating = rpcMethod === 'POST' ||
|
|
148
|
+
const rpcMutating = rpcMethod === 'POST' ||
|
|
149
|
+
rpcMethod === 'PUT' ||
|
|
150
|
+
rpcMethod === 'PATCH' ||
|
|
151
|
+
rpcMethod === 'DELETE';
|
|
145
152
|
if (rpcPath === '/__toil_rpc' && rpcMutating) {
|
|
146
153
|
const idHeader = req.headers.find(([n]) => n.toLowerCase() === 'dacely-rpc')?.[1];
|
|
147
154
|
const id = idHeader !== undefined && /^\d+$/.test(idHeader) ? Number(idHeader) : NaN;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "toiljs",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.121",
|
|
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": {
|
|
@@ -101,6 +101,7 @@
|
|
|
101
101
|
"tag": "latest"
|
|
102
102
|
},
|
|
103
103
|
"scripts": {
|
|
104
|
+
"prepare": "npm run build",
|
|
104
105
|
"watch": "tsc -p tsconfig.json --watch",
|
|
105
106
|
"prepublishOnly": "npm run build",
|
|
106
107
|
"gen:docs": "node scripts/gen-toil-docs.mjs",
|
|
@@ -141,7 +142,7 @@
|
|
|
141
142
|
"picocolors": "^1.1.1",
|
|
142
143
|
"semver": "^7.8.5",
|
|
143
144
|
"sharp": "^0.35.4",
|
|
144
|
-
"toilscript": ">=0.1.
|
|
145
|
+
"toilscript": ">=0.1.63",
|
|
145
146
|
"vite": "^8.3.0",
|
|
146
147
|
"vite-imagetools": "^12.0.1",
|
|
147
148
|
"vite-plugin-node-polyfills": "^0.28.0"
|
|
@@ -150,7 +151,7 @@
|
|
|
150
151
|
"prettier": ">=3.0.0",
|
|
151
152
|
"react": "^19.3.0",
|
|
152
153
|
"react-dom": "^19.3.0",
|
|
153
|
-
"toilscript": ">=0.1.
|
|
154
|
+
"toilscript": ">=0.1.63",
|
|
154
155
|
"typescript": ">=7.0.2 <8.0.0"
|
|
155
156
|
},
|
|
156
157
|
"overrides": {
|
|
@@ -63,7 +63,6 @@ const ITERS: u32 = 2;
|
|
|
63
63
|
const PAR: u32 = 1;
|
|
64
64
|
|
|
65
65
|
const CHALLENGE_TTL_SECS: u64 = 120;
|
|
66
|
-
const SESSION_TTL_SECS: u64 = 3600;
|
|
67
66
|
const CONFIRM_TTL_SECS: u64 = 86400; // email confirm link valid 24h
|
|
68
67
|
const RESET_TTL_SECS: u64 = 3600; // password reset link valid 1h
|
|
69
68
|
|
|
@@ -862,8 +861,8 @@ class Auth {
|
|
|
862
861
|
w.writeBytes(userData); // opaque session token (the readable user payload)
|
|
863
862
|
w.writeBytes(confirm);
|
|
864
863
|
const resp = Response.bytes(w.toBytes());
|
|
865
|
-
resp.setCookie(AuthService.mintSession(userData,
|
|
866
|
-
resp.setCookie(AuthService.userCookie(userData,
|
|
864
|
+
resp.setCookie(AuthService.mintSession(userData, AuthService.DEFAULT_SESSION_TTL_SECS, ch.sessionEpoch));
|
|
865
|
+
resp.setCookie(AuthService.userCookie(userData, AuthService.DEFAULT_SESSION_TTL_SECS));
|
|
867
866
|
return resp;
|
|
868
867
|
}
|
|
869
868
|
|
|
@@ -1082,8 +1081,8 @@ class Auth {
|
|
|
1082
1081
|
w.writeU8(ST_OK);
|
|
1083
1082
|
w.writeBytes(userData);
|
|
1084
1083
|
const resp = Response.bytes(w.toBytes());
|
|
1085
|
-
resp.setCookie(AuthService.mintSession(userData,
|
|
1086
|
-
resp.setCookie(AuthService.userCookie(userData,
|
|
1084
|
+
resp.setCookie(AuthService.mintSession(userData, AuthService.DEFAULT_SESSION_TTL_SECS, ch.sessionEpoch));
|
|
1085
|
+
resp.setCookie(AuthService.userCookie(userData, AuthService.DEFAULT_SESSION_TTL_SECS));
|
|
1087
1086
|
return resp;
|
|
1088
1087
|
}
|
|
1089
1088
|
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** An unavailable security record must never be mistaken for an absent restriction. */
|
|
2
|
+
export class AuthReads {
|
|
3
|
+
public static document<K, V>(collection: Documents<K, V>, key: K): V | null {
|
|
4
|
+
const value = collection.get(key);
|
|
5
|
+
if (value == null && Db.lastError() != DbError.None)
|
|
6
|
+
throw new Error('Authentication state temporarily unavailable (status ' + (-1000 - <i32>Db.lastError()).toString() + ')');
|
|
7
|
+
return value;
|
|
8
|
+
}
|
|
9
|
+
public static owner<K, V>(collection: Unique<K, V>, key: K): V | null {
|
|
10
|
+
const value = collection.lookup(key);
|
|
11
|
+
if (value == null && Db.lastError() != DbError.None)
|
|
12
|
+
throw new Error('Authentication state temporarily unavailable (status ' + (-1000 - <i32>Db.lastError()).toString() + ')');
|
|
13
|
+
return value;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
@data
|
|
2
|
+
export class SessionUserKey {
|
|
3
|
+
id: string = '';
|
|
4
|
+
constructor(id: string = '') { this.id = id; }
|
|
5
|
+
}
|
|
6
|
+
@data
|
|
7
|
+
export class SessionEpochKey {
|
|
8
|
+
id: string = '';
|
|
9
|
+
revision: u64 = 0;
|
|
10
|
+
constructor(id: string = '', revision: u64 = 0) { this.id = id; this.revision = revision; }
|
|
11
|
+
}
|
|
12
|
+
@data
|
|
13
|
+
export class SessionEpoch {
|
|
14
|
+
revision: u64 = 0;
|
|
15
|
+
}
|
|
16
|
+
@data
|
|
17
|
+
export class FactorKey {
|
|
18
|
+
host: string = '';
|
|
19
|
+
username: string = '';
|
|
20
|
+
constructor(host: string = '', username: string = '') { this.host = host; this.username = username; }
|
|
21
|
+
}
|
|
22
|
+
@data
|
|
23
|
+
export class FactorRevision {
|
|
24
|
+
host: string = '';
|
|
25
|
+
username: string = '';
|
|
26
|
+
revision: u64 = 0;
|
|
27
|
+
constructor(host: string = '', username: string = '', revision: u64 = 0) { this.host = host; this.username = username; this.revision = revision; }
|
|
28
|
+
}
|
|
29
|
+
@data
|
|
30
|
+
export class FactorSettings {
|
|
31
|
+
method: u8 = 0;
|
|
32
|
+
revision: u64 = 0;
|
|
33
|
+
}
|
|
34
|
+
@data
|
|
35
|
+
export class SessionDigest {
|
|
36
|
+
digest: string = '';
|
|
37
|
+
constructor(digest: string = '') { this.digest = digest; }
|
|
38
|
+
}
|
|
39
|
+
@data
|
|
40
|
+
export class RevokedSession {
|
|
41
|
+
revoked: bool = true;
|
|
42
|
+
}
|
|
43
|
+
/** Tenant-scoped security state; separate collections, one database namespace. */
|
|
44
|
+
@database
|
|
45
|
+
export class AuthStateDb {
|
|
46
|
+
@collection static revokedSessions: Unique<SessionDigest, RevokedSession>;
|
|
47
|
+
@collection static epochHeads: Documents<SessionUserKey, SessionEpoch>;
|
|
48
|
+
@collection static epochRevisions: Unique<SessionEpochKey, SessionEpoch>;
|
|
49
|
+
@collection static factorHeads: Documents<FactorKey, FactorSettings>;
|
|
50
|
+
@collection static factorRevisions: Unique<FactorRevision, FactorSettings>;
|
|
51
|
+
}
|
|
@@ -1,32 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
host: string = '';
|
|
4
|
-
username: string = '';
|
|
5
|
-
constructor(host: string = '', username: string = '') { this.host = host; this.username = username; }
|
|
6
|
-
}
|
|
7
|
-
@data
|
|
8
|
-
class FactorRevision {
|
|
9
|
-
host: string = '';
|
|
10
|
-
username: string = '';
|
|
11
|
-
revision: u64 = 0;
|
|
12
|
-
constructor(host: string = '', username: string = '', revision: u64 = 0) { this.host = host; this.username = username; this.revision = revision; }
|
|
13
|
-
}
|
|
14
|
-
@data
|
|
15
|
-
export class FactorSettings {
|
|
16
|
-
method: u8 = 0;
|
|
17
|
-
revision: u64 = 0;
|
|
18
|
-
}
|
|
19
|
-
@database
|
|
20
|
-
class FactorDb {
|
|
21
|
-
@collection static heads: Documents<FactorKey, FactorSettings>;
|
|
22
|
-
@collection static revisions: Unique<FactorRevision, FactorSettings>;
|
|
23
|
-
}
|
|
1
|
+
import { AuthReads } from 'toiljs/server/auth/AuthReads';
|
|
2
|
+
import { AuthStateDb, FactorKey, FactorRevision, FactorSettings } from './AuthStateDb';
|
|
24
3
|
export class FactorRepository {
|
|
25
4
|
public static read(host: string, username: string, legacy: u8): FactorSettings {
|
|
26
|
-
let settings =
|
|
5
|
+
let settings = AuthReads.document(AuthStateDb.factorHeads, new FactorKey(host, username));
|
|
27
6
|
if (settings == null) { settings = new FactorSettings(); settings.method = legacy; }
|
|
28
7
|
for (let i: i32 = 0; i < 64; i++) {
|
|
29
|
-
const next =
|
|
8
|
+
const next = AuthReads.owner(AuthStateDb.factorRevisions, new FactorRevision(host, username, settings.revision + 1));
|
|
30
9
|
if (next == null) return settings;
|
|
31
10
|
settings = next;
|
|
32
11
|
}
|
|
@@ -34,8 +13,8 @@ export class FactorRepository {
|
|
|
34
13
|
}
|
|
35
14
|
public static save(host: string, username: string, method: u8, expected: u64): bool {
|
|
36
15
|
const settings = new FactorSettings(); settings.method = method; settings.revision = expected + 1;
|
|
37
|
-
if (!
|
|
38
|
-
|
|
16
|
+
if (!AuthStateDb.factorRevisions.claim(new FactorRevision(host, username, settings.revision), settings).claimed) return false;
|
|
17
|
+
AuthStateDb.factorHeads.upsert(new FactorKey(host, username), settings);
|
|
39
18
|
return true;
|
|
40
19
|
}
|
|
41
20
|
}
|
|
@@ -1,24 +1,6 @@
|
|
|
1
|
+
import { AuthReads } from 'toiljs/server/auth/AuthReads';
|
|
1
2
|
import { DataReader } from 'data';
|
|
2
|
-
|
|
3
|
-
class SessionUserKey {
|
|
4
|
-
id: string = '';
|
|
5
|
-
constructor(id: string = '') { this.id = id; }
|
|
6
|
-
}
|
|
7
|
-
@data
|
|
8
|
-
class SessionEpochKey {
|
|
9
|
-
id: string = '';
|
|
10
|
-
revision: u64 = 0;
|
|
11
|
-
constructor(id: string = '', revision: u64 = 0) { this.id = id; this.revision = revision; }
|
|
12
|
-
}
|
|
13
|
-
@data
|
|
14
|
-
class SessionEpoch {
|
|
15
|
-
revision: u64 = 0;
|
|
16
|
-
}
|
|
17
|
-
@database
|
|
18
|
-
class SessionEpochDb {
|
|
19
|
-
@collection static heads: Documents<SessionUserKey, SessionEpoch>;
|
|
20
|
-
@collection static revisions: Unique<SessionEpochKey, SessionEpoch>;
|
|
21
|
-
}
|
|
3
|
+
import { AuthStateDb, SessionUserKey, SessionEpochKey, SessionEpoch } from './AuthStateDb';
|
|
22
4
|
/** Immutable per-user generations fence sessions and in-flight login challenges without clock assumptions. */
|
|
23
5
|
export class SessionEpochs {
|
|
24
6
|
private static key(userData: Uint8Array): string {
|
|
@@ -29,10 +11,10 @@ export class SessionEpochs {
|
|
|
29
11
|
return crypto.toHex(id);
|
|
30
12
|
}
|
|
31
13
|
private static read(id: string): SessionEpoch {
|
|
32
|
-
let epoch =
|
|
14
|
+
let epoch = AuthReads.document(AuthStateDb.epochHeads, new SessionUserKey(id));
|
|
33
15
|
if (epoch == null) epoch = new SessionEpoch();
|
|
34
16
|
for (let i: i32 = 0; i < 64; i++) {
|
|
35
|
-
const next =
|
|
17
|
+
const next = AuthReads.owner(AuthStateDb.epochRevisions, new SessionEpochKey(id, epoch.revision + 1));
|
|
36
18
|
if (next == null) return epoch;
|
|
37
19
|
epoch = next;
|
|
38
20
|
}
|
|
@@ -49,8 +31,8 @@ export class SessionEpochs {
|
|
|
49
31
|
for (let i: i32 = 0; i < 3; i++) {
|
|
50
32
|
const epoch = SessionEpochs.read(id);
|
|
51
33
|
epoch.revision += 1;
|
|
52
|
-
if (!
|
|
53
|
-
|
|
34
|
+
if (!AuthStateDb.epochRevisions.claim(new SessionEpochKey(id, epoch.revision), epoch).claimed) continue;
|
|
35
|
+
AuthStateDb.epochHeads.upsert(new SessionUserKey(id), epoch);
|
|
54
36
|
return;
|
|
55
37
|
}
|
|
56
38
|
throw new Error('Session revocation conflicted; retry');
|