Files
core/src/ast/interpreter/PredicateResolver.js
T
John Dvorak 717ae1031e initial commit: @arbiter/core authorization engine with js-rigor hardening
Zanzibar-style authorization graph engine (direct/chain/TTU/defeasible/
binary modes, condensed snapshots, value relations) with 39 rigor test
campaigns. Includes fixes for snapshot binary writer/reader format
mismatch (snapshot-of-snapshot corruption), possibility write-boundary
validation, empty-graph snapshot serialization, relation lookup cache
direction collision, config-redefinition cache invalidation, binary
threshold semantics, defeasible compiled routing, and comparator
reason whitelisting.
2026-07-31 13:44:06 -07:00

168 lines
4.1 KiB
JavaScript

/**
* Predicate Resolver
*
* Maps predicate names to existing compiled DSL rules across all graph scopes.
* Does NOT create new rules - only looks up existing ones.
*/
export class PredicateResolver {
constructor(context) {
this.context = context;
this.graphStores = context.graphStores || {};
this.cache = new Map();
}
/**
* Resolve a predicate name to its DSL rule
*
* @param {string} predicateName - Name of the predicate
* @returns {Object|null} Rule info or null if not found
*/
resolve(predicateName) {
// Check cache first
if (this.cache.has(predicateName)) {
return this.cache.get(predicateName);
}
// Look up in all graph scopes
const rule = this.findRule(predicateName);
if (rule) {
this.cache.set(predicateName, rule);
}
return rule;
}
/**
* Find a rule across all graph scopes
* Prefers logical rules over direct rules for evidence predicates
*/
findRule(predicateName) {
const scopes = [
'tenantExternal',
'tenantInternal',
'rootExternal',
'rootInternal',
'masterExternal',
'masterInternal'
];
let directRule = null;
let directScope = null;
for (const scopeName of scopes) {
const graphStore = this.graphStores[scopeName];
if (!graphStore) {
continue;
}
// Handle both: Arbiter directly (has .check()) or wrapper with .arbiter
const arbiter = graphStore.arbiter || graphStore;
const config = arbiter.relationConfigs?.get(predicateName);
if (config) {
// Prefer logical rules (intersection/union) over direct rules
// This ensures evidence rules work correctly across all scopes
if (config.type === 'intersection' || config.type === 'union' || config.type === 'logical') {
return {
name: predicateName,
scope: scopeName,
config: config,
arity: this.inferArity(config)
};
}
// Remember the first direct rule as fallback
if (!directRule && config.type === 'direct') {
directRule = config;
directScope = scopeName;
}
}
}
// Return direct rule if no logical rule found
if (directRule) {
return {
name: predicateName,
scope: directScope,
config: directRule,
arity: this.inferArity(directRule)
};
}
return null;
}
/**
* Infer the arity (parameter count) from rule config
*/
inferArity(config) {
// Most DSL evidence rules have 1 or 2 parameters:
// - 1 param: just the subject (user)
// - 2 params: subject (user) + object
if (config.arity) {
return config.arity;
}
// Default to checking if it's a relation that typically needs an object
// This is a heuristic - in practice, the DSL defines this explicitly
if (config.type === 'tuple_to_userset' || config.type === 'direct') {
return 2; // Likely needs subject + object
}
return 1; // Default to 1 param
}
/**
* Check if a predicate exists without full resolution
*/
exists(predicateName) {
return this.resolve(predicateName) !== null;
}
/**
* Get all available predicates across all scopes
*/
getAllPredicates() {
const predicates = [];
const scopes = [
'tenantExternal',
'tenantInternal',
'rootExternal',
'rootInternal',
'masterExternal',
'masterInternal'
];
for (const scopeName of scopes) {
const graphStore = this.graphStores[scopeName];
if (!graphStore) {
continue;
}
// Handle both: Arbiter directly (has .check()) or wrapper with .arbiter
const arbiter = graphStore.arbiter || graphStore;
if (arbiter.relationConfigs) {
for (const [name, config] of arbiter.relationConfigs) {
predicates.push({
name,
scope: scopeName,
arity: this.inferArity(config)
});
}
}
}
return predicates;
}
/**
* Clear the cache (useful for testing or when rules change)
*/
clearCache() {
this.cache.clear();
}
}