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.
This commit is contained in:
@@ -0,0 +1,254 @@
|
||||
import { performance } from 'perf_hooks';
|
||||
import { Arbiter } from '../src/index.js';
|
||||
|
||||
class Benchmark {
|
||||
constructor(name) {
|
||||
this.name = name;
|
||||
this.results = [];
|
||||
this.labels = [];
|
||||
}
|
||||
async run(fn, iterations = 1000, label = '') {
|
||||
for (let i = 0; i < 10; i++) await fn();
|
||||
const times = [];
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
const start = performance.now();
|
||||
await fn();
|
||||
const end = performance.now();
|
||||
times.push(end - start);
|
||||
}
|
||||
const avg = times.reduce((a, b) => a + b, 0) / times.length;
|
||||
const min = Math.min(...times);
|
||||
const max = Math.max(...times);
|
||||
const p95 = times.sort((a, b) => a - b)[Math.floor(times.length * 0.95)];
|
||||
this.results.push({
|
||||
avg: avg.toFixed(3),
|
||||
min: min.toFixed(3),
|
||||
max: max.toFixed(3),
|
||||
p95: p95.toFixed(3)
|
||||
});
|
||||
this.labels.push(label);
|
||||
return { avg, min, max, p95 };
|
||||
}
|
||||
report() {
|
||||
console.log('Configuration | Avg (ms) | Min (ms) | Max (ms) | P95 (ms) | Speedup');
|
||||
console.log('---------------------------------|----------|----------|----------|----------|--------');
|
||||
const baselineTime = parseFloat(this.results[0].avg);
|
||||
this.results.forEach((result, i) => {
|
||||
const label = this.labels[i] || `Test ${i + 1}`;
|
||||
const speedup = i === 0 ? '1.00x' : `${(baselineTime / parseFloat(result.avg)).toFixed(2)}x`;
|
||||
console.log(`${label.padEnd(32)} | ${result.avg.padStart(8)} | ${result.min.padStart(8)} | ${result.max.padStart(8)} | ${result.p95.padStart(8)} | ${speedup.padStart(6)}`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function setupGraph(scale = 'medium') {
|
||||
const scales = {
|
||||
medium: { users: 2000, resources: 1000 },
|
||||
xlarge: { users: 20000, resources: 5000 }
|
||||
};
|
||||
const config = scales[scale];
|
||||
const arbiter = new Arbiter({ fastConstructionMode: true });
|
||||
const users = [], resources = [];
|
||||
for (let i = 0; i < config.users; i++) {
|
||||
const userKey = `user:user${i}`;
|
||||
arbiter.addNode(userKey, 'user');
|
||||
// Assign a random balance between 100 and 10000
|
||||
arbiter.addRelation(userKey, 'has_balance', 'balance', { value: Math.floor(Math.random() * 9900) + 100 });
|
||||
users.push(userKey);
|
||||
}
|
||||
for (let i = 0; i < config.resources; i++) {
|
||||
const resKey = `resource:res${i}`;
|
||||
arbiter.addNode(resKey, 'resource');
|
||||
// Assign a random price between 50 and 5000
|
||||
arbiter.addRelation(resKey, 'has_price', 'price', { value: Math.floor(Math.random() * 4950) + 50 });
|
||||
resources.push(resKey);
|
||||
}
|
||||
// Direct access for a subset
|
||||
for (let i = 0; i < Math.floor(config.users * 0.1); i++) {
|
||||
const user = users[i];
|
||||
const res = resources[i % resources.length];
|
||||
arbiter.addRelation(user, 'can_access', res);
|
||||
}
|
||||
// ChainRule: user->can_access->resource
|
||||
arbiter.setRelationConfig('can_access', { type: 'direct' });
|
||||
arbiter.setRelationConfig('chain_access', {
|
||||
type: 'chain',
|
||||
steps: [ { relation: 'can_access', direction: 'out' } ]
|
||||
});
|
||||
// RelationalComparatorRule: user.balance >= resource.price
|
||||
arbiter.setRelationConfig('balance_check', {
|
||||
type: 'relational_comparator',
|
||||
leftOperand: {
|
||||
rule: { type: 'direct', relation: 'has_balance' },
|
||||
extractValue: true,
|
||||
valueRelation: 'has_balance',
|
||||
aggregator: 'max'
|
||||
},
|
||||
rightOperand: {
|
||||
rule: { type: 'direct', relation: 'has_price' },
|
||||
extractValue: true,
|
||||
valueRelation: 'has_price',
|
||||
aggregator: 'max'
|
||||
},
|
||||
comparator: '>=',
|
||||
fallbackBehavior: 'deny'
|
||||
});
|
||||
return { arbiter, users, resources };
|
||||
}
|
||||
|
||||
async function runRelationalComparatorBenchmarks() {
|
||||
for (const scale of ['medium', 'xlarge']) {
|
||||
console.log(`\n🔬 RelationalComparatorRule Benchmark (${scale.toUpperCase()})`);
|
||||
const { arbiter, users, resources } = setupGraph(scale);
|
||||
const benchmark = new Benchmark(`RelationalComparatorRule (${scale})`);
|
||||
// Direct access baseline
|
||||
await benchmark.run(() => {
|
||||
const user = users[Math.floor(Math.random() * users.length)];
|
||||
const res = resources[Math.floor(Math.random() * resources.length)];
|
||||
arbiter.check(user, 'can_access', res);
|
||||
}, scale === 'medium' ? 500 : 100, 'Direct Access (baseline)');
|
||||
// ChainRule existence
|
||||
await benchmark.run(() => {
|
||||
const user = users[Math.floor(Math.random() * users.length)];
|
||||
const res = resources[Math.floor(Math.random() * resources.length)];
|
||||
arbiter.check(user, 'chain_access', res);
|
||||
}, scale === 'medium' ? 500 : 100, 'ChainRule Existence');
|
||||
// RelationalComparatorRule (value-based)
|
||||
await benchmark.run(() => {
|
||||
const user = users[Math.floor(Math.random() * users.length)];
|
||||
const res = resources[Math.floor(Math.random() * resources.length)];
|
||||
arbiter.check(user, 'balance_check', res);
|
||||
}, scale === 'medium' ? 500 : 100, 'RelationalComparatorRule (balance >= price)');
|
||||
// --- Complex, realistic scenario: sum of all balances user can debit vs. min plan/feature price ---
|
||||
// Setup: users, accounts, features, plans, prices, debit rights
|
||||
const accounts = [], plans = [], features = [];
|
||||
for (let i = 0; i < (scale === 'medium' ? 1000 : 5000); i++) {
|
||||
const accKey = `account:acc${i}`;
|
||||
accounts.push(accKey);
|
||||
arbiter.addNode(accKey, 'account');
|
||||
// Each account has a balance (with timestamp for decay)
|
||||
arbiter.addRelation(accKey, 'has_balance', 'unit:usd', {
|
||||
value: Math.floor(Math.random() * 9900) + 100,
|
||||
updated_last_at: Date.now() - Math.floor(Math.random() * 48 * 60 * 60 * 1000) // up to 48h old
|
||||
});
|
||||
}
|
||||
for (let i = 0; i < (scale === 'medium' ? 100 : 500); i++) {
|
||||
const planKey = `plan:plan${i}`;
|
||||
plans.push(planKey);
|
||||
arbiter.addNode(planKey, 'plan');
|
||||
arbiter.addRelation(planKey, 'has_price', 'unit:usd', { value: Math.floor(Math.random() * 4950) + 50 });
|
||||
}
|
||||
for (let i = 0; i < (scale === 'medium' ? 500 : 2000); i++) {
|
||||
const featKey = `feature:feat${i}`;
|
||||
features.push(featKey);
|
||||
arbiter.addNode(featKey, 'feature');
|
||||
// Each feature has a price
|
||||
arbiter.addRelation(featKey, 'has_price', 'unit:usd', { value: Math.floor(Math.random() * 4950) + 50 });
|
||||
// Some features belong to a plan
|
||||
if (Math.random() < 0.5) {
|
||||
const plan = plans[Math.floor(Math.random() * plans.length)];
|
||||
arbiter.addRelation(featKey, 'belongs_to_plan', plan);
|
||||
}
|
||||
}
|
||||
// Each user can debit a random subset of accounts
|
||||
for (const user of users) {
|
||||
for (let j = 0; j < (scale === 'medium' ? 3 : 5); j++) {
|
||||
const acc = accounts[Math.floor(Math.random() * accounts.length)];
|
||||
arbiter.addRelation(user, 'can_debit', acc);
|
||||
}
|
||||
}
|
||||
arbiter.setRelationConfig('can_debit', { type: 'direct' });
|
||||
arbiter.setRelationConfig('belongs_to_plan', { type: 'direct' });
|
||||
arbiter.setRelationConfig('has_balance', { type: 'direct' });
|
||||
arbiter.setRelationConfig('has_price', { type: 'direct' });
|
||||
// Simplified RelationalComparatorRule: user balance vs feature price
|
||||
arbiter.setRelationConfig('can_afford_feature', {
|
||||
type: 'relational_comparator',
|
||||
leftOperand: {
|
||||
rule: { type: 'direct', relation: 'has_balance' },
|
||||
extractValue: true,
|
||||
valueRelation: 'has_balance',
|
||||
aggregator: 'max'
|
||||
},
|
||||
rightOperand: {
|
||||
rule: { type: 'direct', relation: 'has_price' },
|
||||
extractValue: true,
|
||||
valueRelation: 'has_price',
|
||||
aggregator: 'max'
|
||||
},
|
||||
comparator: '>=',
|
||||
fallbackBehavior: 'deny'
|
||||
});
|
||||
// Benchmark: can user afford feature (complex, multi-hop, aggregated, decayed)
|
||||
await benchmark.run(() => {
|
||||
const user = users[Math.floor(Math.random() * users.length)];
|
||||
const feature = features[Math.floor(Math.random() * features.length)];
|
||||
arbiter.check(user, 'can_afford_feature', feature);
|
||||
}, scale === 'medium' ? 200 : 30, 'Complex: can_debit sum >= min(plan/feature price)');
|
||||
// Optionally: Nested logical operator (e.g., require both can afford AND recent 2FA)
|
||||
// For brevity, just add a dummy 2FA relation and a logical AND
|
||||
for (const user of users) {
|
||||
arbiter.addRelation(user, 'last_2fa', '2fa:recent', { value: Date.now() - Math.floor(Math.random() * 60 * 60 * 1000) });
|
||||
}
|
||||
arbiter.addNode('2fa:recent', '2fa');
|
||||
arbiter.setRelationConfig('last_2fa', { type: 'direct' });
|
||||
arbiter.setRelationConfig('recent_2fa_check', {
|
||||
type: 'relational_comparator',
|
||||
leftOperand: {
|
||||
rule: { type: 'direct', relation: 'last_2fa' },
|
||||
extractValue: true
|
||||
},
|
||||
rightOperand: {
|
||||
rule: { type: 'computed', value: Date.now() - 10 * 60 * 1000 }, // 10 minutes ago
|
||||
extractValue: true
|
||||
},
|
||||
comparator: '>=', // last_2fa >= threshold (i.e., more recent)
|
||||
fallbackBehavior: 'deny'
|
||||
});
|
||||
arbiter.setRelationConfig('can_afford_and_recent_2fa', {
|
||||
type: 'intersection',
|
||||
rules: [
|
||||
{ type: 'relational_comparator',
|
||||
leftOperand: {
|
||||
rule: { type: 'direct', relation: 'has_balance' },
|
||||
extractValue: true,
|
||||
valueRelation: 'has_balance',
|
||||
aggregator: 'max'
|
||||
},
|
||||
rightOperand: {
|
||||
rule: { type: 'direct', relation: 'has_price' },
|
||||
extractValue: true,
|
||||
valueRelation: 'has_price',
|
||||
aggregator: 'max'
|
||||
},
|
||||
comparator: '>=',
|
||||
fallbackBehavior: 'deny'
|
||||
},
|
||||
{ type: 'relational_comparator',
|
||||
leftOperand: {
|
||||
rule: { type: 'direct', relation: 'last_2fa' },
|
||||
extractValue: true
|
||||
},
|
||||
rightOperand: {
|
||||
rule: { type: 'computed', value: Date.now() - 10 * 60 * 1000 },
|
||||
extractValue: true
|
||||
},
|
||||
comparator: '>=',
|
||||
fallbackBehavior: 'deny'
|
||||
}
|
||||
]
|
||||
});
|
||||
await benchmark.run(() => {
|
||||
const user = users[Math.floor(Math.random() * users.length)];
|
||||
const feature = features[Math.floor(Math.random() * features.length)];
|
||||
arbiter.check(user, 'can_afford_and_recent_2fa', feature);
|
||||
}, scale === 'medium' ? 200 : 30, 'Complex: can_afford AND recent_2FA');
|
||||
benchmark.report();
|
||||
}
|
||||
}
|
||||
|
||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
runRelationalComparatorBenchmarks();
|
||||
}
|
||||
|
||||
export { runRelationalComparatorBenchmarks };
|
||||
Reference in New Issue
Block a user