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.
@@ -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('data: keys blob too large');
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
- if (count > 1024)
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
- if (keyLen > MAX_KEY || valLen > MAX_VALUE)
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
- if (keyLen > MAX_KEY || patchLen > MAX_VALUE)
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
- if (keyLen > MAX_KEY || valLen > MAX_VALUE)
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
- if (keyLen > MAX_KEY || valLen > MAX_VALUE)
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
- if (setLen > MAX_KEY || memberLen > MAX_VALUE)
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 ? [] : Array.from(set.values()).sort((a, b) => Buffer.compare(a, b)).slice(0, n);
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
- if (keyLen > MAX_KEY || valLen > MAX_VALUE)
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
- if (keyLen > MAX_KEY || evLen > MAX_VALUE)
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
- if (keyLen > MAX_KEY || evLen > MAX_VALUE)
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
- if (keyLen > MAX_KEY || valLen > MAX_VALUE)
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}${sk}`;
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),
@@ -85,6 +85,7 @@ export interface DbSnapshot {
85
85
  sv: number;
86
86
  }[]>;
87
87
  eventDedup: Record<string, string[]>;
88
+ eventPositions?: Record<string, [string, number][]>;
88
89
  deriveCheckpoints?: Record<string, number>;
89
90
  capacity: Record<string, {
90
91
  total: string;
@@ -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 = 256 * 1024;
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' || rpcMethod === 'PUT' || rpcMethod === 'PATCH' || rpcMethod === 'DELETE';
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.119",
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.60",
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.60",
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, SESSION_TTL_SECS, ch.sessionEpoch));
866
- resp.setCookie(AuthService.userCookie(userData, SESSION_TTL_SECS));
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, SESSION_TTL_SECS, ch.sessionEpoch));
1086
- resp.setCookie(AuthService.userCookie(userData, SESSION_TTL_SECS));
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
- @data
2
- class FactorKey {
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 = FactorDb.heads.get(new FactorKey(host, username));
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 = FactorDb.revisions.lookup(new FactorRevision(host, username, settings.revision + 1));
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 (!FactorDb.revisions.claim(new FactorRevision(host, username, settings.revision), settings).claimed) return false;
38
- FactorDb.heads.upsert(new FactorKey(host, username), settings);
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
- @data
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 = SessionEpochDb.heads.get(new SessionUserKey(id));
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 = SessionEpochDb.revisions.lookup(new SessionEpochKey(id, epoch.revision + 1));
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 (!SessionEpochDb.revisions.claim(new SessionEpochKey(id, epoch.revision), epoch).claimed) continue;
53
- SessionEpochDb.heads.upsert(new SessionUserKey(id), epoch);
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');