@statewalker/webrun-biscuit 0.1.0

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/src/datalog.ts ADDED
@@ -0,0 +1,722 @@
1
+ /**
2
+ * Biscuit Datalog: term model, fact/rule evaluation with origin tracking, and
3
+ * the stack-based expression virtual machine.
4
+ */
5
+
6
+ export const AUTHORIZER = 0xffffffff; // block id of the authorizer (usize::MAX in Rust)
7
+
8
+ /* -------------------------------------------------------------------- terms */
9
+
10
+ export type MapKey = { t: "int"; v: bigint } | { t: "str"; v: string };
11
+
12
+ export type Term =
13
+ | { t: "var"; v: number }
14
+ | { t: "int"; v: bigint }
15
+ | { t: "str"; v: string }
16
+ | { t: "date"; v: bigint }
17
+ | { t: "bytes"; v: Uint8Array }
18
+ | { t: "bool"; v: boolean }
19
+ | { t: "set"; v: Term[] }
20
+ | { t: "null" }
21
+ | { t: "array"; v: Term[] }
22
+ | { t: "map"; v: [MapKey, Term][] };
23
+
24
+ const hex = (b: Uint8Array): string =>
25
+ Array.from(b, (x) => x.toString(16).padStart(2, "0")).join("");
26
+
27
+ /** Canonical string key: JS Map/Set are reference-keyed, so every structural
28
+ * comparison and every de-duplication in the engine goes through this. */
29
+ export function termKey(t: Term): string {
30
+ switch (t.t) {
31
+ case "var":
32
+ return `v${t.v}`;
33
+ case "int":
34
+ return `i${t.v}`;
35
+ case "str":
36
+ return `s${t.v.length}:${t.v}`;
37
+ case "date":
38
+ return `d${t.v}`;
39
+ case "bytes":
40
+ return `b${hex(t.v)}`;
41
+ case "bool":
42
+ return t.v ? "T" : "F";
43
+ case "null":
44
+ return "N";
45
+ case "set":
46
+ return `S[${t.v.map(termKey).sort().join(",")}]`;
47
+ case "array":
48
+ return `A[${t.v.map(termKey).join(",")}]`;
49
+ case "map":
50
+ return `M[${t.v
51
+ .map(([k, v]) => `${mapKeyKey(k)}=>${termKey(v)}`)
52
+ .sort()
53
+ .join(",")}]`;
54
+ }
55
+ }
56
+
57
+ export const mapKeyKey = (k: MapKey): string =>
58
+ k.t === "int" ? `i${k.v}` : `s${k.v.length}:${k.v}`;
59
+ export const termEq = (a: Term, b: Term): boolean => termKey(a) === termKey(b);
60
+
61
+ /** de-duplicated, deterministically ordered set contents */
62
+ export function normalizeSet(items: Term[]): Term[] {
63
+ const seen = new Map<string, Term>();
64
+ for (const i of items) seen.set(termKey(i), i);
65
+ return [...seen.entries()]
66
+ .sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0))
67
+ .map((e) => e[1]);
68
+ }
69
+
70
+ export interface Predicate {
71
+ name: string;
72
+ terms: Term[];
73
+ }
74
+ export interface Fact {
75
+ predicate: Predicate;
76
+ }
77
+ export type Op =
78
+ | { kind: "value"; value: Term }
79
+ | { kind: "unary"; op: number; ffi?: string }
80
+ | { kind: "binary"; op: number; ffi?: string }
81
+ | { kind: "closure"; params: number[]; ops: Op[] };
82
+
83
+ export type Scope =
84
+ | { kind: "authority" }
85
+ | { kind: "previous" }
86
+ | { kind: "publicKey"; key: string };
87
+
88
+ export interface Rule {
89
+ head: Predicate;
90
+ body: Predicate[];
91
+ expressions: Op[][];
92
+ scopes: Scope[];
93
+ }
94
+ export type CheckKind = "one" | "all" | "reject";
95
+ export interface Check {
96
+ queries: Rule[];
97
+ kind: CheckKind;
98
+ }
99
+
100
+ export const factKey = (f: Fact): string =>
101
+ `${f.predicate.name}/${f.predicate.terms.length}(${f.predicate.terms.map(termKey).join(",")})`;
102
+
103
+ /* ------------------------------------------------------------------ origins */
104
+
105
+ export class Origin {
106
+ constructor(readonly ids: number[] = []) {}
107
+ static of(...ids: number[]): Origin {
108
+ return new Origin([...new Set(ids)].sort((a, b) => a - b));
109
+ }
110
+ union(other: Origin): Origin {
111
+ return Origin.of(...this.ids, ...other.ids);
112
+ }
113
+ with(id: number): Origin {
114
+ return Origin.of(...this.ids, id);
115
+ }
116
+ get key(): string {
117
+ return this.ids.join(",");
118
+ }
119
+ }
120
+
121
+ export class TrustedOrigins {
122
+ private readonly set: Set<number>;
123
+ constructor(ids: Iterable<number>) {
124
+ this.set = new Set(ids);
125
+ }
126
+ static default(): TrustedOrigins {
127
+ return new TrustedOrigins([AUTHORIZER, 0]);
128
+ }
129
+ /** the trusted set is a superset of the fact's origin */
130
+ contains(origin: Origin): boolean {
131
+ for (const id of origin.ids) if (!this.set.has(id)) return false;
132
+ return true;
133
+ }
134
+ get key(): string {
135
+ return [...this.set].sort((a, b) => a - b).join(",");
136
+ }
137
+ /** the trusted block ids, for deriving a new set from this one */
138
+ ids(): Iterable<number> {
139
+ return this.set;
140
+ }
141
+ }
142
+
143
+ export function trustedOriginsFromScopes(
144
+ ruleScopes: Scope[],
145
+ defaults: TrustedOrigins,
146
+ currentBlock: number,
147
+ publicKeyToBlockIds: Map<string, number[]>,
148
+ ): TrustedOrigins {
149
+ if (ruleScopes.length === 0) {
150
+ const ids = new Set<number>(defaults.ids());
151
+ ids.add(currentBlock);
152
+ ids.add(AUTHORIZER);
153
+ return new TrustedOrigins(ids);
154
+ }
155
+ const ids = new Set<number>([AUTHORIZER, currentBlock]);
156
+ for (const scope of ruleScopes) {
157
+ if (scope.kind === "authority") ids.add(0);
158
+ else if (scope.kind === "previous") {
159
+ if (currentBlock !== AUTHORIZER) for (let i = 0; i <= currentBlock; i++) ids.add(i);
160
+ } else for (const id of publicKeyToBlockIds.get(scope.key) ?? []) ids.add(id);
161
+ }
162
+ return new TrustedOrigins(ids);
163
+ }
164
+
165
+ /* ------------------------------------------------------------------- errors */
166
+
167
+ export type ExecutionErrorKind =
168
+ | "Overflow"
169
+ | "DivideByZero"
170
+ | "InvalidType"
171
+ | "UnknownVariable"
172
+ | "ShadowedVariable"
173
+ | "InvalidStack"
174
+ | "UndefinedExtern"
175
+ | "TooManyFacts"
176
+ | "TooManyIterations"
177
+ | "Timeout";
178
+
179
+ export class ExecutionError extends Error {
180
+ constructor(
181
+ readonly kind: ExecutionErrorKind,
182
+ message?: string,
183
+ ) {
184
+ super(message ?? kind);
185
+ }
186
+ }
187
+
188
+ /* --------------------------------------------------------- expression VM */
189
+
190
+ const U = { Negate: 0, Parens: 1, Length: 2, TypeOf: 3, Ffi: 4 };
191
+ const B = {
192
+ LessThan: 0,
193
+ GreaterThan: 1,
194
+ LessOrEqual: 2,
195
+ GreaterOrEqual: 3,
196
+ Equal: 4,
197
+ Contains: 5,
198
+ Prefix: 6,
199
+ Suffix: 7,
200
+ Regex: 8,
201
+ Add: 9,
202
+ Sub: 10,
203
+ Mul: 11,
204
+ Div: 12,
205
+ And: 13,
206
+ Or: 14,
207
+ Intersection: 15,
208
+ Union: 16,
209
+ BitwiseAnd: 17,
210
+ BitwiseOr: 18,
211
+ BitwiseXor: 19,
212
+ NotEqual: 20,
213
+ HeterogeneousEqual: 21,
214
+ HeterogeneousNotEqual: 22,
215
+ LazyAnd: 23,
216
+ LazyOr: 24,
217
+ All: 25,
218
+ Any: 26,
219
+ Get: 27,
220
+ Ffi: 28,
221
+ TryOr: 29,
222
+ };
223
+ export const BinaryOp = B;
224
+ export const UnaryOp = U;
225
+
226
+ const I64_MIN = -(2n ** 63n);
227
+ const I64_MAX = 2n ** 63n - 1n;
228
+ function checkedI64(v: bigint): Term {
229
+ if (v < I64_MIN || v > I64_MAX) throw new ExecutionError("Overflow");
230
+ return { t: "int", v };
231
+ }
232
+
233
+ const utf8len = (s: string): number => new TextEncoder().encode(s).length;
234
+
235
+ export type ExternFn = (left: Term, right?: Term) => Term;
236
+
237
+ type Bindings = Map<number, Term>;
238
+
239
+ function typeName(t: Term): string {
240
+ switch (t.t) {
241
+ case "int":
242
+ return "integer";
243
+ case "str":
244
+ return "string";
245
+ case "date":
246
+ return "date";
247
+ case "bytes":
248
+ return "bytes";
249
+ case "bool":
250
+ return "bool";
251
+ case "set":
252
+ return "set";
253
+ case "null":
254
+ return "null";
255
+ case "array":
256
+ return "array";
257
+ case "map":
258
+ return "map";
259
+ default:
260
+ throw new ExecutionError("InvalidType");
261
+ }
262
+ }
263
+
264
+ function unary(op: number, ffi: string | undefined, v: Term, externs: Map<string, ExternFn>): Term {
265
+ switch (op) {
266
+ case U.Negate:
267
+ if (v.t !== "bool") throw new ExecutionError("InvalidType");
268
+ return { t: "bool", v: !v.v };
269
+ case U.Parens:
270
+ return v;
271
+ case U.Length:
272
+ if (v.t === "str") return { t: "int", v: BigInt(utf8len(v.v)) };
273
+ if (v.t === "bytes") return { t: "int", v: BigInt(v.v.length) };
274
+ if (v.t === "set" || v.t === "array" || v.t === "map")
275
+ return { t: "int", v: BigInt(v.v.length) };
276
+ throw new ExecutionError("InvalidType");
277
+ case U.TypeOf:
278
+ return { t: "str", v: typeName(v) };
279
+ case U.Ffi: {
280
+ const f = externs.get(ffi!);
281
+ if (!f) throw new ExecutionError("UndefinedExtern", ffi);
282
+ return f(v);
283
+ }
284
+ default:
285
+ throw new ExecutionError("InvalidType");
286
+ }
287
+ }
288
+
289
+ function setContains(set: Term[], value: Term): boolean {
290
+ const k = termKey(value);
291
+ return set.some((t) => termKey(t) === k);
292
+ }
293
+
294
+ function binary(
295
+ op: number,
296
+ ffi: string | undefined,
297
+ l: Term,
298
+ r: Term,
299
+ externs: Map<string, ExternFn>,
300
+ ): Term {
301
+ const bool = (v: boolean): Term => ({ t: "bool", v });
302
+ const strictEqable =
303
+ (l.t === r.t && l.t !== "var") ||
304
+ (l.t === "set" && r.t === "set") ||
305
+ (l.t === "map" && r.t === "map");
306
+
307
+ switch (op) {
308
+ case B.LessThan:
309
+ case B.GreaterThan:
310
+ case B.LessOrEqual:
311
+ case B.GreaterOrEqual: {
312
+ if (!((l.t === "int" && r.t === "int") || (l.t === "date" && r.t === "date")))
313
+ throw new ExecutionError("InvalidType");
314
+ const a = l.v as bigint;
315
+ const b = r.v as bigint;
316
+ return bool(
317
+ op === B.LessThan
318
+ ? a < b
319
+ : op === B.GreaterThan
320
+ ? a > b
321
+ : op === B.LessOrEqual
322
+ ? a <= b
323
+ : a >= b,
324
+ );
325
+ }
326
+ case B.Equal:
327
+ case B.NotEqual: {
328
+ if (!strictEqable) throw new ExecutionError("InvalidType");
329
+ const eq = termEq(l, r);
330
+ return bool(op === B.Equal ? eq : !eq);
331
+ }
332
+ case B.HeterogeneousEqual:
333
+ return bool(l.t === r.t && termEq(l, r));
334
+ case B.HeterogeneousNotEqual:
335
+ return bool(!(l.t === r.t && termEq(l, r)));
336
+ case B.Add:
337
+ if (l.t === "int" && r.t === "int") return checkedI64(l.v + r.v);
338
+ if (l.t === "str" && r.t === "str") return { t: "str", v: l.v + r.v };
339
+ throw new ExecutionError("InvalidType");
340
+ case B.Sub:
341
+ if (l.t !== "int" || r.t !== "int") throw new ExecutionError("InvalidType");
342
+ return checkedI64(l.v - r.v);
343
+ case B.Mul:
344
+ if (l.t !== "int" || r.t !== "int") throw new ExecutionError("InvalidType");
345
+ return checkedI64(l.v * r.v);
346
+ case B.Div:
347
+ if (l.t !== "int" || r.t !== "int") throw new ExecutionError("InvalidType");
348
+ if (r.v === 0n) throw new ExecutionError("DivideByZero");
349
+ return checkedI64(l.v / r.v);
350
+ case B.BitwiseAnd:
351
+ if (l.t !== "int" || r.t !== "int") throw new ExecutionError("InvalidType");
352
+ return { t: "int", v: BigInt.asIntN(64, l.v & r.v) };
353
+ case B.BitwiseOr:
354
+ if (l.t !== "int" || r.t !== "int") throw new ExecutionError("InvalidType");
355
+ return { t: "int", v: BigInt.asIntN(64, l.v | r.v) };
356
+ case B.BitwiseXor:
357
+ if (l.t !== "int" || r.t !== "int") throw new ExecutionError("InvalidType");
358
+ return { t: "int", v: BigInt.asIntN(64, l.v ^ r.v) };
359
+ case B.And:
360
+ if (l.t !== "bool" || r.t !== "bool") throw new ExecutionError("InvalidType");
361
+ return bool(l.v && r.v);
362
+ case B.Or:
363
+ if (l.t !== "bool" || r.t !== "bool") throw new ExecutionError("InvalidType");
364
+ return bool(l.v || r.v);
365
+ case B.Prefix:
366
+ if (l.t === "str" && r.t === "str") return bool(l.v.startsWith(r.v));
367
+ if (l.t === "array" && r.t === "array")
368
+ return bool(r.v.every((x, i) => i < l.v.length && termEq(l.v[i], x)));
369
+ throw new ExecutionError("InvalidType");
370
+ case B.Suffix:
371
+ if (l.t === "str" && r.t === "str") return bool(l.v.endsWith(r.v));
372
+ if (l.t === "array" && r.t === "array") {
373
+ const off = l.v.length - r.v.length;
374
+ return bool(off >= 0 && r.v.every((x, i) => termEq(l.v[off + i], x)));
375
+ }
376
+ throw new ExecutionError("InvalidType");
377
+ case B.Regex: {
378
+ if (l.t !== "str" || r.t !== "str") throw new ExecutionError("InvalidType");
379
+ try {
380
+ return bool(new RegExp(r.v).test(l.v)); // unanchored, like Rust's is_match
381
+ } catch {
382
+ return bool(false); // invalid pattern is false, not an error
383
+ }
384
+ }
385
+ case B.Contains:
386
+ if (l.t === "str" && r.t === "str") return bool(l.v.includes(r.v));
387
+ if (l.t === "set" && r.t === "set") return bool(r.v.every((x) => setContains(l.v, x)));
388
+ if (l.t === "set") return bool(setContains(l.v, r));
389
+ if (l.t === "array") return bool(setContains(l.v, r));
390
+ if (l.t === "map")
391
+ return bool(
392
+ l.v.some(([k]) =>
393
+ r.t === "int"
394
+ ? k.t === "int" && k.v === r.v
395
+ : r.t === "str" && k.t === "str" && k.v === r.v,
396
+ ),
397
+ );
398
+ throw new ExecutionError("InvalidType");
399
+ case B.Intersection: {
400
+ if (l.t !== "set" || r.t !== "set") throw new ExecutionError("InvalidType");
401
+ return { t: "set", v: normalizeSet(l.v.filter((x) => setContains(r.v, x))) };
402
+ }
403
+ case B.Union: {
404
+ if (l.t !== "set" || r.t !== "set") throw new ExecutionError("InvalidType");
405
+ return { t: "set", v: normalizeSet([...l.v, ...r.v]) };
406
+ }
407
+ case B.Get:
408
+ if (l.t === "array" && r.t === "int") {
409
+ const i = r.v < 0n || r.v >= BigInt(l.v.length) ? -1 : Number(r.v);
410
+ return i < 0 ? { t: "null" } : l.v[i];
411
+ }
412
+ if (l.t === "map" && (r.t === "int" || r.t === "str")) {
413
+ const found = l.v.find(([k]) =>
414
+ r.t === "int" ? k.t === "int" && k.v === r.v : k.t === "str" && k.v === r.v,
415
+ );
416
+ return found ? found[1] : { t: "null" };
417
+ }
418
+ throw new ExecutionError("InvalidType");
419
+ case B.Ffi: {
420
+ const f = externs.get(ffi!);
421
+ if (!f) throw new ExecutionError("UndefinedExtern", ffi);
422
+ return f(l, r);
423
+ }
424
+ default:
425
+ throw new ExecutionError("InvalidType");
426
+ }
427
+ }
428
+
429
+ type StackElem = { s: "term"; v: Term } | { s: "closure"; params: number[]; ops: Op[] };
430
+
431
+ export function evaluateExpression(
432
+ ops: Op[],
433
+ values: Bindings,
434
+ externs: Map<string, ExternFn> = new Map(),
435
+ ): Term {
436
+ const stack: StackElem[] = [];
437
+ for (const op of ops) {
438
+ switch (op.kind) {
439
+ case "value":
440
+ if (op.value.t === "var") {
441
+ const bound = values.get(op.value.v);
442
+ if (bound === undefined) throw new ExecutionError("UnknownVariable", String(op.value.v));
443
+ stack.push({ s: "term", v: bound });
444
+ } else stack.push({ s: "term", v: op.value });
445
+ break;
446
+ case "unary": {
447
+ const a = stack.pop();
448
+ if (!a || a.s !== "term") throw new ExecutionError("InvalidStack");
449
+ stack.push({ s: "term", v: unary(op.op, op.ffi, a.v, externs) });
450
+ break;
451
+ }
452
+ case "closure":
453
+ stack.push({ s: "closure", params: op.params, ops: op.ops });
454
+ break;
455
+ case "binary": {
456
+ const right = stack.pop();
457
+ const left = stack.pop();
458
+ if (!right || !left) throw new ExecutionError("InvalidStack");
459
+ if (right.s === "term" && left.s === "term") {
460
+ stack.push({ s: "term", v: binary(op.op, op.ffi, left.v, right.v, externs) });
461
+ } else {
462
+ const closure = right.s === "closure" ? right : left.s === "closure" ? left : null;
463
+ const term = right.s === "term" ? right : left.s === "term" ? left : null;
464
+ if (!closure || !term) throw new ExecutionError("InvalidStack");
465
+ for (const p of closure.params)
466
+ if (values.has(p)) throw new ExecutionError("ShadowedVariable", String(p));
467
+ stack.push({
468
+ s: "term",
469
+ v: evaluateWithClosure(op.op, term.v, closure.ops, closure.params, values, externs),
470
+ });
471
+ }
472
+ break;
473
+ }
474
+ }
475
+ }
476
+ if (stack.length !== 1) throw new ExecutionError("InvalidStack");
477
+ const top = stack[0];
478
+ if (top.s !== "term") throw new ExecutionError("InvalidStack");
479
+ return top.v;
480
+ }
481
+
482
+ function evaluateWithClosure(
483
+ op: number,
484
+ left: Term,
485
+ ops: Op[],
486
+ params: number[],
487
+ values: Bindings,
488
+ externs: Map<string, ExternFn>,
489
+ ): Term {
490
+ if (op === B.TryOr && params.length === 0) {
491
+ try {
492
+ return evaluateExpression(ops, values, externs);
493
+ } catch {
494
+ return left;
495
+ }
496
+ }
497
+ if ((op === B.LazyOr || op === B.LazyAnd) && params.length === 0) {
498
+ if (left.t !== "bool") throw new ExecutionError("InvalidType");
499
+ if (op === B.LazyOr && left.v) return { t: "bool", v: true };
500
+ if (op === B.LazyAnd && !left.v) return { t: "bool", v: false };
501
+ return evaluateExpression(ops, values, externs);
502
+ }
503
+ if ((op === B.All || op === B.Any) && params.length === 1) {
504
+ const param = params[0];
505
+ let items: Term[];
506
+ if (left.t === "set" || left.t === "array") items = left.v;
507
+ else if (left.t === "map")
508
+ items = left.v.map(
509
+ ([k, v]): Term => ({
510
+ t: "array",
511
+ v: [k.t === "int" ? { t: "int", v: k.v } : { t: "str", v: k.v }, v],
512
+ }),
513
+ );
514
+ else throw new ExecutionError("InvalidType");
515
+
516
+ const wanted = op === B.All;
517
+ for (const item of items) {
518
+ const scoped = new Map(values);
519
+ scoped.set(param, item);
520
+ const res = evaluateExpression(ops, scoped, externs);
521
+ if (res.t !== "bool") throw new ExecutionError("InvalidType");
522
+ if (res.v !== wanted) return { t: "bool", v: !wanted };
523
+ }
524
+ return { t: "bool", v: wanted };
525
+ }
526
+ throw new ExecutionError("InvalidType");
527
+ }
528
+
529
+ /* -------------------------------------------------------------------- world */
530
+
531
+ export interface RunLimits {
532
+ maxFacts: number;
533
+ maxIterations: number;
534
+ maxTimeMs: number;
535
+ }
536
+ /**
537
+ * Note the divergence: the reference defaults `max_time` to **1 millisecond**,
538
+ * which is unreachably tight for a cold JS engine and would make ordinary
539
+ * tokens fail non-deterministically. 1 second is generous by comparison, so a
540
+ * caller exposed to untrusted tokens should lower it deliberately rather than
541
+ * rely on this default as a denial-of-service bound.
542
+ */
543
+ export const DEFAULT_LIMITS: RunLimits = { maxFacts: 1000, maxIterations: 100, maxTimeMs: 1000 };
544
+
545
+ function matchPredicate(rule: Predicate, fact: Predicate): boolean {
546
+ if (rule.name !== fact.name || rule.terms.length !== fact.terms.length) return false;
547
+ for (let i = 0; i < rule.terms.length; i++) {
548
+ const rt = rule.terms[i];
549
+ const ft = fact.terms[i];
550
+ if (ft.t === "var") return false; // facts never contain variables
551
+ if (rt.t === "var") continue;
552
+ if (rt.t !== ft.t || !termEq(rt, ft)) return false;
553
+ }
554
+ return true;
555
+ }
556
+
557
+ function variablesOf(rule: Rule): Set<number> {
558
+ const out = new Set<number>();
559
+ for (const p of rule.body) for (const t of p.terms) if (t.t === "var") out.add(t.v);
560
+ return out;
561
+ }
562
+
563
+ type OriginFact = [Origin, Fact];
564
+ /** facts grouped by `name/arity`, so a join never scans unrelated predicates */
565
+ type FactIndex = Map<string, OriginFact[]>;
566
+
567
+ /** backtracking join over the rule body, mirroring Rust's CombineIt */
568
+ function* combine(
569
+ predicates: Predicate[],
570
+ facts: FactIndex,
571
+ bindings: Bindings,
572
+ variables: Set<number>,
573
+ ): Generator<[Origin, Bindings]> {
574
+ if (predicates.length === 0) {
575
+ for (const v of variables) if (!bindings.has(v)) return;
576
+ yield [Origin.of(), new Map(bindings)];
577
+ return;
578
+ }
579
+ const [head, ...rest] = predicates;
580
+ for (const [origin, fact] of facts.get(`${head.name}/${head.terms.length}`) ?? []) {
581
+ if (!matchPredicate(head, fact.predicate)) continue;
582
+ const next = new Map(bindings);
583
+ let ok = true;
584
+ for (let i = 0; i < head.terms.length; i++) {
585
+ const rt = head.terms[i];
586
+ if (rt.t !== "var") continue;
587
+ const existing = next.get(rt.v);
588
+ if (existing === undefined) next.set(rt.v, fact.predicate.terms[i]);
589
+ else if (!termEq(existing, fact.predicate.terms[i])) {
590
+ ok = false;
591
+ break;
592
+ }
593
+ }
594
+ if (!ok) continue;
595
+ for (const [subOrigin, result] of combine(rest, facts, next, variables))
596
+ yield [subOrigin.union(origin), result];
597
+ }
598
+ }
599
+
600
+ export class World {
601
+ /** facts indexed by origin key */
602
+ readonly facts = new Map<string, { origin: Origin; items: Map<string, Fact> }>();
603
+ readonly rules: { origin: number; trusted: TrustedOrigins; rule: Rule }[] = [];
604
+ externs = new Map<string, ExternFn>();
605
+ iterations = 0;
606
+ private generation = 0;
607
+ private indexCache = new Map<string, { generation: number; index: FactIndex }>();
608
+
609
+ addFact(origin: Origin, fact: Fact): void {
610
+ let bucket = this.facts.get(origin.key);
611
+ if (!bucket) {
612
+ bucket = { origin, items: new Map() };
613
+ this.facts.set(origin.key, bucket);
614
+ }
615
+ const key = factKey(fact);
616
+ if (!bucket.items.has(key)) {
617
+ bucket.items.set(key, fact);
618
+ this.generation++;
619
+ }
620
+ }
621
+
622
+ addRule(origin: number, trusted: TrustedOrigins, rule: Rule): void {
623
+ this.rules.push({ origin, trusted, rule });
624
+ }
625
+
626
+ factCount(): number {
627
+ let n = 0;
628
+ for (const b of this.facts.values()) n += b.items.size;
629
+ return n;
630
+ }
631
+
632
+ private visible(trusted: TrustedOrigins): FactIndex {
633
+ const cached = this.indexCache.get(trusted.key);
634
+ if (cached && cached.generation === this.generation) return cached.index;
635
+
636
+ const index: FactIndex = new Map();
637
+ for (const bucket of this.facts.values()) {
638
+ if (!trusted.contains(bucket.origin)) continue;
639
+ for (const f of bucket.items.values()) {
640
+ const key = `${f.predicate.name}/${f.predicate.terms.length}`;
641
+ const list = index.get(key);
642
+ if (list) list.push([bucket.origin, f]);
643
+ else index.set(key, [[bucket.origin, f]]);
644
+ }
645
+ }
646
+ this.indexCache.set(trusted.key, { generation: this.generation, index });
647
+ return index;
648
+ }
649
+
650
+ /** naive fixpoint: apply every rule until no new fact appears */
651
+ run(limits: RunLimits = DEFAULT_LIMITS): void {
652
+ const deadline = Date.now() + limits.maxTimeMs;
653
+ let index = 0;
654
+ for (;;) {
655
+ const generated: [Origin, Fact][] = [];
656
+ for (const { origin, trusted, rule } of this.rules) {
657
+ const facts = this.visible(trusted);
658
+ for (const [o, f] of this.apply(rule, facts, origin)) generated.push([o, f]);
659
+ }
660
+ const before = this.factCount();
661
+ for (const [o, f] of generated) this.addFact(o, f);
662
+ if (this.factCount() === before) break;
663
+
664
+ index++;
665
+ if (index === limits.maxIterations) throw new ExecutionError("TooManyIterations");
666
+ if (this.factCount() >= limits.maxFacts) throw new ExecutionError("TooManyFacts");
667
+ if (Date.now() >= deadline) throw new ExecutionError("Timeout");
668
+ }
669
+ this.iterations += index;
670
+ }
671
+
672
+ *apply(rule: Rule, facts: FactIndex, ruleOrigin: number): Generator<[Origin, Fact]> {
673
+ const variables = variablesOf(rule);
674
+ for (const [origin, bindings] of combine(rule.body, facts, new Map(), variables)) {
675
+ let pass = true;
676
+ for (const ops of rule.expressions) {
677
+ const res = evaluateExpression(ops, bindings, this.externs);
678
+ if (res.t !== "bool") throw new ExecutionError("InvalidType");
679
+ if (!res.v) {
680
+ pass = false;
681
+ break;
682
+ }
683
+ }
684
+ if (!pass) continue;
685
+ const terms: Term[] = [];
686
+ let complete = true;
687
+ for (const t of rule.head.terms) {
688
+ if (t.t === "var") {
689
+ const bound = bindings.get(t.v);
690
+ if (bound === undefined) {
691
+ complete = false; // head variables must be bound in the body
692
+ break;
693
+ }
694
+ terms.push(bound);
695
+ } else terms.push(t);
696
+ }
697
+ if (!complete) continue;
698
+ yield [origin.with(ruleOrigin), { predicate: { name: rule.head.name, terms } }];
699
+ }
700
+ }
701
+
702
+ /** `check if` / policies: does at least one combination match? */
703
+ queryMatch(rule: Rule, origin: number, trusted: TrustedOrigins): boolean {
704
+ for (const _ of this.apply(rule, this.visible(trusted), origin)) return true;
705
+ return false;
706
+ }
707
+
708
+ /** `check all`: every matching combination must satisfy the expressions */
709
+ queryMatchAll(rule: Rule, trusted: TrustedOrigins): boolean {
710
+ const variables = variablesOf(rule);
711
+ let found = false;
712
+ for (const [, bindings] of combine(rule.body, this.visible(trusted), new Map(), variables)) {
713
+ found = true;
714
+ for (const ops of rule.expressions) {
715
+ const res = evaluateExpression(ops, bindings, this.externs);
716
+ if (res.t !== "bool") throw new ExecutionError("InvalidType");
717
+ if (!res.v) return false;
718
+ }
719
+ }
720
+ return found;
721
+ }
722
+ }