toiljs 0.0.120 → 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.120",
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": {
@@ -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');
@@ -1,3 +1,4 @@
1
+ import { AuthReads } from 'toiljs/server/auth/AuthReads';
1
2
  // AuthService: the server half of the post-quantum auth primitive, available
2
3
  // as a no-import global (registered via the toilscript `--lib` mechanism, the
3
4
  // same way `crypto` is a global). The client derives an ML-DSA-44 keypair from
@@ -11,11 +12,12 @@
11
12
 
12
13
  import { DataWriter, DataReader } from 'data';
13
14
  import { SessionEpochs } from 'toiljs/server/auth/SessionEpochs';
14
- import { SessionRevocations, SessionDigest, RevokedSession } from 'toiljs/server/auth/SessionRevocations';
15
+ import { AuthStateDb, SessionDigest, RevokedSession } from 'toiljs/server/auth/AuthStateDb';
15
16
  import { HmacImportParams, HmacParams, ALG_SHA_256, FMT_RAW, USAGE_SIGN, USAGE_VERIFY } from 'crypto';
16
17
 
17
18
  import {
18
19
  Server,
20
+ Request,
19
21
  SecureCookies,
20
22
  Cookie,
21
23
  SameSite,
@@ -274,6 +276,7 @@ export namespace AuthService {
274
276
  * the env value for the current request; keep it out of any client bundle.
275
277
  */
276
278
  export function setSecret(secret: Uint8Array): void {
279
+ invalidateSession();
277
280
  __sessionSecret = secret;
278
281
  }
279
282
 
@@ -286,13 +289,33 @@ export namespace AuthService {
286
289
  export function getSessionBytes(): Uint8Array | null {
287
290
  const req = Server.currentRequest;
288
291
  if (req == null) return null;
292
+ if (!req.__authChecked) {
293
+ // Publish only after verification completes. A host fault must never
294
+ // turn into a cached anonymous result if the application handles it.
295
+ req.__authBytes = verifySession(req);
296
+ req.__authChecked = true;
297
+ }
298
+ if (Time.nowSeconds() >= req.__authExpires) return null;
299
+ const bytes = req.__authBytes;
300
+ // Callers may decode/mutate their copy without changing later auth checks.
301
+ return bytes == null ? null : bytes.slice();
302
+ }
303
+
304
+ function invalidateSession(): void {
305
+ const req = Server.currentRequest;
306
+ if (req == null) return;
307
+ req.__authChecked = false;
308
+ req.__authBytes = null;
309
+ req.__authExpires = 0;
310
+ }
289
311
 
312
+ function verifySession(req: Request): Uint8Array | null {
290
313
  const sealed = SecureCookies.signed(__resolveSessionSecret()).open(
291
314
  req.cookies(),
292
315
  sessionCookieName(__reqIsSecure()),
293
316
  );
294
317
  if (sealed == null) return null;
295
- if (SessionRevocations.sessions.lookup(new SessionDigest(crypto.toHex(crypto.sha256Text(sealed)))) != null) return null;
318
+ if (AuthReads.owner(AuthStateDb.revokedSessions, new SessionDigest(crypto.toHex(crypto.sha256Text(sealed)))) != null) return null;
296
319
 
297
320
  const payload = base64UrlDecode(sealed);
298
321
  if (payload == null) return null;
@@ -310,6 +333,7 @@ export namespace AuthService {
310
333
  if (Time.nowSeconds() >= exp) return null; // expired
311
334
  if (!SessionEpochs.allowed(userBytes, generation)) return null;
312
335
 
336
+ req.__authExpires = exp;
313
337
  return userBytes;
314
338
  }
315
339
 
@@ -397,6 +421,7 @@ export namespace AuthService {
397
421
 
398
422
  /** Invalidate all browser sessions authorized under the prior session generation. */
399
423
  export function revokeUserSessions(userData: Uint8Array): void {
424
+ invalidateSession();
400
425
  SessionEpochs.revoke(userData);
401
426
  }
402
427
 
@@ -406,7 +431,8 @@ export namespace AuthService {
406
431
  const req = Server.currentRequest!;
407
432
  const sealed = SecureCookies.signed(__resolveSessionSecret()).open(req.cookies(), sessionCookieName(__reqIsSecure()));
408
433
  if (sealed == null) return;
409
- SessionRevocations.sessions.claim(new SessionDigest(crypto.toHex(crypto.sha256Text(sealed))), new RevokedSession());
434
+ invalidateSession();
435
+ AuthStateDb.revokedSessions.claim(new SessionDigest(crypto.toHex(crypto.sha256Text(sealed))), new RevokedSession());
410
436
  }
411
437
 
412
438
  /** A `Set-Cookie` that immediately clears the session (logout). */
@@ -33,6 +33,12 @@ export class Request {
33
33
  headers: Array<Header>;
34
34
  body: Uint8Array;
35
35
 
36
+ // Auth verification belongs to this request object, never a process/tenant cache.
37
+ // Internal AuthService state; cleared before local revocations or secret changes.
38
+ __authChecked: bool = false;
39
+ __authBytes: Uint8Array | null = null;
40
+ __authExpires: u64 = 0;
41
+
36
42
  // Lazily parsed `Cookie` header, cached for the life of the request.
37
43
  private _cookies: CookieMap | null = null;
38
44
 
@@ -1,13 +0,0 @@
1
- @data
2
- export class SessionDigest {
3
- digest: string = '';
4
- constructor(digest: string = '') { this.digest = digest; }
5
- }
6
- @data
7
- export class RevokedSession {
8
- revoked: bool = true;
9
- }
10
- @database
11
- export class SessionRevocations {
12
- @collection static sessions: Unique<SessionDigest, RevokedSession>;
13
- }