import { Arbiter } from '../src/core/Arbiter.js'; import { OWAFusion } from '../src/utils/OWAFusion.js'; function parseArgs(argv) { const args = new Map(); for (let i = 2; i < argv.length; i++) { const value = argv[i]; if (!value.startsWith('--')) continue; if (value.startsWith('--no-')) { const key = value.slice(5); args.set(key, false); continue; } const [key, inline] = value.slice(2).split('='); if (inline !== undefined) { args.set(key, inline); continue; } const next = argv[i + 1]; if (next && !next.startsWith('--')) { args.set(key, next); i++; } else { args.set(key, true); } } return args; } function createRng(seed) { let state = seed >>> 0; return () => { state = (1664525 * state + 1013904223) >>> 0; return state / 0x100000000; }; } function parseSizes(value) { if (!value) return null; return value.split(',').map(item => Number(item.trim())).filter(Number.isFinite); } function percentile(sorted, p) { if (sorted.length === 0) return 0; const index = Math.min(sorted.length - 1, Math.max(0, Math.ceil((p / 100) * sorted.length) - 1)); return sorted[index]; } function randInt(rng, max) { return Math.floor(rng() * max); } function randRange(rng, min, max) { return min + Math.floor(rng() * (max - min)); } function buildUsers(arbiter, count, prefix) { const ids = []; for (let i = 0; i < count; i++) { ids.push(arbiter.addNode(`${prefix}:${i}`, prefix)); } return ids; } function setupOwaUnionScenario(arbiter, size, rng, relationName, unionConfig) { buildUsers(arbiter, size, 'user'); buildUsers(arbiter, size, 'resource'); const trueIds = []; const falseIds = []; for (let i = 0; i < size; i++) { if (i % 3 === 0) { arbiter.addRelation(`user:${i}`, 'viewer', `resource:${i}`, 0.6); } if (i % 5 === 0) { arbiter.addRelation(`user:${i}`, 'owner', `resource:${i}`, 0.9); } if (i % 3 === 0 || i % 5 === 0) { trueIds.push(i); } else { falseIds.push(i); } } arbiter.setRelationConfig('viewer', { type: 'direct' }); arbiter.setRelationConfig('owner', { type: 'direct' }); arbiter.setRelationConfig(relationName, { union: { rules: [ { type: 'direct', relation: 'viewer' }, { type: 'direct', relation: 'owner' } ], ...unionConfig } }); const queries = createQueries( Math.min(2000, size), () => { const userId = trueIds.length > 0 ? trueIds[randInt(rng, trueIds.length)] : 0; return { user: `user:${userId}`, object: `resource:${userId}` }; }, () => { const userId = falseIds.length > 0 ? falseIds[randInt(rng, falseIds.length)] : 0; return { user: `user:${userId}`, object: `resource:${userId}` }; } ); return { queries: shuffleWithRng(queries, rng) }; } function createQueries(count, pickTrue, pickFalse) { const queries = []; const half = Math.floor(count / 2); for (let i = 0; i < half; i++) { queries.push({ ...pickTrue(), expected: true }); } for (let i = 0; i < count - half; i++) { queries.push({ ...pickFalse(), expected: false }); } return queries; } function createChallengeQueries(count, pickTrue, pickFalse) { const queries = []; const half = Math.floor(count / 2); for (let i = 0; i < half; i++) { const entry = pickTrue(); queries.push({ ...entry, expected: true }); } for (let i = 0; i < count - half; i++) { const entry = pickFalse(); queries.push({ ...entry, expected: false }); } return queries; } function shuffle(items) { for (let i = items.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [items[i], items[j]] = [items[j], items[i]]; } return items; } function shuffleWithRng(items, rng) { for (let i = items.length - 1; i > 0; i--) { const j = Math.floor(rng() * (i + 1)); [items[i], items[j]] = [items[j], items[i]]; } return items; } const scenarios = [ { name: 'owner_direct', relation: 'owner', setup: (arbiter, size, rng) => { buildUsers(arbiter, size, 'user'); buildUsers(arbiter, size, 'resource'); for (let i = 0; i < size; i++) { arbiter.addRelation(`user:${i}`, 'owner', `resource:${i}`); } arbiter.setRelationConfig('owner', { type: 'direct' }); const queries = createQueries( Math.min(2000, size * 2), () => { const id = randInt(rng, size); return { user: `user:${id}`, object: `resource:${id}` }; }, () => { const id = randInt(rng, size); return { user: `user:${id}`, object: `resource:${(id + 1) % size}` }; } ); return { queries: shuffleWithRng(queries, rng) }; } }, { name: 'viewer_direct', relation: 'viewer', setup: (arbiter, size, rng) => { buildUsers(arbiter, size, 'user'); buildUsers(arbiter, size, 'resource'); for (let i = 0; i < size; i++) { arbiter.addRelation(`user:${i}`, 'viewer', `resource:${i}`); } arbiter.setRelationConfig('viewer', { type: 'direct' }); const queries = createQueries( Math.min(2000, size * 2), () => { const id = randInt(rng, size); return { user: `user:${id}`, object: `resource:${id}` }; }, () => { const id = randInt(rng, size); return { user: `user:${id}`, object: `resource:${(id + 1) % size}` }; } ); return { queries: shuffleWithRng(queries, rng) }; } }, { name: 'group_tuple_to_userset', relation: 'can_view', setup: (arbiter, size, rng) => { const groupCount = Math.max(1, Math.floor(size / 10)); buildUsers(arbiter, size, 'user'); buildUsers(arbiter, groupCount, 'group'); buildUsers(arbiter, groupCount, 'resource'); for (let i = 0; i < size; i++) { const groupId = i % groupCount; arbiter.addRelation(`user:${i}`, 'member', `group:${groupId}`); } for (let i = 0; i < groupCount; i++) { arbiter.addRelation(`group:${i}`, 'group_access', `resource:${i}`); } arbiter.setRelationConfig('member', { type: 'direct' }); arbiter.setRelationConfig('group_access', { type: 'direct' }); arbiter.setRelationConfig('can_view', { type: 'tuple_to_userset', tuplesetRelation: 'group_access', tuplesetDirection: 'in', computedRelation: 'member' }); const queries = createQueries( Math.min(2000, size), () => { const userId = randInt(rng, size); const groupId = userId % groupCount; return { user: `user:${userId}`, object: `resource:${groupId}` }; }, () => { const userId = randInt(rng, size); const groupId = (userId + 1) % groupCount; return { user: `user:${userId}`, object: `resource:${groupId}` }; } ); return { queries: shuffleWithRng(queries, rng) }; } }, { name: 'plan_chain', relation: 'can_use', setup: (arbiter, size, rng) => { const planCount = Math.max(1, Math.floor(size / 10)); buildUsers(arbiter, size, 'user'); buildUsers(arbiter, planCount, 'plan'); buildUsers(arbiter, planCount, 'resource'); for (let i = 0; i < size; i++) { const planId = i % planCount; arbiter.addRelation(`user:${i}`, 'has_plan', `plan:${planId}`); } for (let i = 0; i < planCount; i++) { arbiter.addRelation(`plan:${i}`, 'plan_grants', `resource:${i}`); } arbiter.setRelationConfig('has_plan', { type: 'direct' }); arbiter.setRelationConfig('plan_grants', { type: 'direct' }); arbiter.setRelationConfig('can_use', { type: 'chain', steps: [ { relation: 'has_plan', direction: 'out' }, { relation: 'plan_grants', direction: 'out' } ], collectValues: false }); const queries = createQueries( Math.min(2000, size), () => { const userId = randInt(rng, size); const planId = userId % planCount; return { user: `user:${userId}`, object: `resource:${planId}` }; }, () => { const userId = randInt(rng, size); const planId = (userId + 1) % planCount; return { user: `user:${userId}`, object: `resource:${planId}` }; } ); return { queries: shuffleWithRng(queries, rng) }; } }, { name: 'account_admin_chain', relation: 'can_admin', setup: (arbiter, size, rng) => { const accountCount = Math.max(1, Math.floor(size / 10)); buildUsers(arbiter, size, 'user'); buildUsers(arbiter, accountCount, 'account'); buildUsers(arbiter, accountCount, 'resource'); for (let i = 0; i < size; i++) { const accountId = i % accountCount; arbiter.addRelation(`user:${i}`, 'admin_of', `account:${accountId}`); } for (let i = 0; i < accountCount; i++) { arbiter.addRelation(`account:${i}`, 'account_owns', `resource:${i}`); } arbiter.setRelationConfig('admin_of', { type: 'direct' }); arbiter.setRelationConfig('account_owns', { type: 'direct' }); arbiter.setRelationConfig('can_admin', { type: 'chain', steps: [ { relation: 'admin_of', direction: 'out' }, { relation: 'account_owns', direction: 'out' } ], collectValues: false }); const queries = createQueries( Math.min(2000, size), () => { const userId = randInt(rng, size); const accountId = userId % accountCount; return { user: `user:${userId}`, object: `resource:${accountId}` }; }, () => { const userId = randInt(rng, size); const accountId = (userId + 1) % accountCount; return { user: `user:${userId}`, object: `resource:${accountId}` }; } ); return { queries: shuffleWithRng(queries, rng) }; } }, { name: 'blocked_unless', relation: 'can_view_blocked', setup: (arbiter, size, rng) => { buildUsers(arbiter, size, 'user'); buildUsers(arbiter, size, 'resource'); for (let i = 0; i < size; i++) { arbiter.addRelation(`user:${i}`, 'viewer', `resource:${i}`); } for (let i = 0; i < Math.floor(size / 4); i++) { arbiter.addRelation(`user:${i}`, 'blocked', `resource:${i}`); } arbiter.setRelationConfig('viewer', { type: 'direct' }); arbiter.setRelationConfig('blocked', { type: 'direct' }); arbiter.setRelationConfig('can_view_blocked', { exclusion: [ { type: 'direct', relation: 'viewer' }, { type: 'direct', relation: 'blocked' } ] }); const queries = createQueries( Math.min(2000, size), () => { const blockedRange = Math.floor(size / 4); const userId = randRange(rng, blockedRange, size); return { user: `user:${userId}`, object: `resource:${userId}` }; }, () => { const userId = randInt(rng, Math.floor(size / 4)); return { user: `user:${userId}`, object: `resource:${userId}` }; } ); return { queries: shuffleWithRng(queries, rng) }; } }, { name: 'age_abac', relation: 'age_ok', setup: (arbiter, size, rng) => { buildUsers(arbiter, size, 'user'); buildUsers(arbiter, size, 'resource'); for (let i = 0; i < size; i++) { arbiter.addRelation(`user:${i}`, 'age', `user:${i}`, 1.0, { value: 18 + (i % 30) }); arbiter.addRelation(`resource:${i}`, 'min_age', `resource:${i}`, 1.0, { value: 18 + (i % 15) }); } arbiter.setRelationConfig('age', { type: 'direct' }); arbiter.setRelationConfig('min_age', { type: 'direct' }); arbiter.setRelationConfig('age_ok', { type: 'relational_comparator', comparator: '>=', fallbackBehavior: 'deny', left: { rule: { type: 'direct', relation: 'age' }, extractValue: true, valueRelation: 'age' }, right: { rule: { type: 'direct', relation: 'min_age' }, extractValue: true, valueRelation: 'min_age', evaluateFrom: 'object' } }); const queries = createQueries( Math.min(2000, size), () => { const userId = randInt(rng, size); const resourceId = userId % size; return { user: `user:${userId}`, object: `resource:${resourceId}` }; }, () => { const userId = randInt(rng, size); const resourceId = (userId + Math.floor(size / 2)) % size; return { user: `user:${userId}`, object: `resource:${resourceId}` }; } ); const withExpected = queries.map(q => { const userId = Number(q.user.split(':')[1]); const resourceId = Number(q.object.split(':')[1]); const age = 18 + (userId % 30); const minAge = 18 + (resourceId % 15); return { ...q, expected: age >= minAge }; }); return { queries: shuffleWithRng(withExpected, rng) }; } }, { name: 'risk_limit', relation: 'risk_ok', setup: (arbiter, size, rng) => { buildUsers(arbiter, size, 'user'); buildUsers(arbiter, size, 'resource'); for (let i = 0; i < size; i++) { const score = i % 100; arbiter.addRelation(`user:${i}`, 'risk_score', `resource:${i}`, 1.0, { value: score }); arbiter.addRelation(`resource:${i}`, 'risk_limit', `resource:${i}`, 1.0, { value: 50 }); } arbiter.setRelationConfig('risk_score', { type: 'direct' }); arbiter.setRelationConfig('risk_limit', { type: 'direct' }); arbiter.setRelationConfig('risk_ok', { type: 'relational_comparator', comparator: '<=', fallbackBehavior: 'deny', left: { rule: { type: 'direct', relation: 'risk_score' }, extractValue: true, valueRelation: 'risk_score' }, right: { rule: { type: 'direct', relation: 'risk_limit' }, extractValue: true, valueRelation: 'risk_limit', evaluateFrom: 'object' } }); const queries = createQueries( Math.min(2000, size), () => { const userId = randInt(rng, Math.min(size, 51)); return { user: `user:${userId}`, object: `resource:${userId}` }; }, () => { const base = Math.min(size - 51, 49); const userId = base + randInt(rng, Math.max(1, size - base)); return { user: `user:${userId}`, object: `resource:${userId}` }; } ); const withExpected = queries.map(q => { const userId = Number(q.user.split(':')[1]); const score = userId % 100; return { ...q, expected: score <= 50 }; }); return { queries: shuffleWithRng(withExpected, rng) }; } }, { name: 'feature_flag', relation: 'can_feature', setup: (arbiter, size, rng) => { const flagCount = Math.max(1, Math.floor(size / 10)); buildUsers(arbiter, size, 'user'); buildUsers(arbiter, size, 'resource'); buildUsers(arbiter, flagCount, 'flag'); for (let i = 0; i < size; i++) { const flagId = i % flagCount; arbiter.addRelation(`user:${i}`, 'has_flag', `flag:${flagId}`); if (i % 2 === 0) { arbiter.addRelation(`resource:${i}`, 'feature_flag', `flag:${flagId}`); } } arbiter.setRelationConfig('has_flag', { type: 'direct' }); arbiter.setRelationConfig('feature_flag', { type: 'direct' }); arbiter.setRelationConfig('can_feature', { type: 'tuple_to_userset', tuplesetRelation: 'feature_flag', tuplesetDirection: 'out', computedRelation: 'has_flag' }); const queries = createQueries( Math.min(2000, size), () => { const userId = randInt(rng, Math.floor(size / 2)) * 2; return { user: `user:${userId}`, object: `resource:${userId}` }; }, () => { const userId = randInt(rng, Math.floor(size / 2)) * 2 + 1; return { user: `user:${userId}`, object: `resource:${userId}` }; } ); return { queries: shuffleWithRng(queries, rng) }; } } ,{ name: 'owa_union', relation: 'can_view_owa', setup: (arbiter, size, rng) => { return setupOwaUnionScenario(arbiter, size, rng, 'can_view_owa', { aggregator: 'owa', owaWeights: [0.7, 0.3] }); } } ,{ name: 'owa_union_max', relation: 'can_view_owa_max', setup: (arbiter, size, rng) => { return setupOwaUnionScenario(arbiter, size, rng, 'can_view_owa_max', { aggregator: 'max' }); } } ,{ name: 'owa_union_min', relation: 'can_view_owa_min', setup: (arbiter, size, rng) => { return setupOwaUnionScenario(arbiter, size, rng, 'can_view_owa_min', { aggregator: 'min' }); } } ,{ name: 'owa_union_top2', relation: 'can_view_owa_top2', setup: (arbiter, size, rng) => { return setupOwaUnionScenario(arbiter, size, rng, 'can_view_owa_top2', { aggregator: 'top2' }); } } ,{ name: 'owa_union_median', relation: 'can_view_owa_median', setup: (arbiter, size, rng) => { return setupOwaUnionScenario(arbiter, size, rng, 'can_view_owa_median', { aggregator: 'median' }); } } ,{ name: 'owa_comparator_nested', relation: 'risk_ok_owa', setup: (arbiter, size, rng) => { buildUsers(arbiter, size, 'user'); buildUsers(arbiter, size, 'resource'); const trueIds = []; const falseIds = []; for (let i = 0; i < size; i++) { const isSafe = i % 2 === 0; const score = isSafe ? 20 : 80; const bonus = isSafe ? 5 : 15; const noise = isSafe ? 0 : 10; arbiter.addRelation(`user:${i}`, 'risk_score', `resource:${i}`, 1.0, { value: score }); arbiter.addRelation(`user:${i}`, 'risk_bonus', `resource:${i}`, 1.0, { value: bonus }); arbiter.addRelation(`user:${i}`, 'risk_noise', `resource:${i}`, 1.0, { value: noise }); arbiter.addRelation(`resource:${i}`, 'risk_limit', `resource:${i}`, 1.0, { value: 40 }); arbiter.addRelation(`resource:${i}`, 'risk_cap', `resource:${i}`, 1.0, { value: 45 }); if (isSafe) { trueIds.push(i); } else { falseIds.push(i); } } arbiter.setRelationConfig('risk_score', { type: 'direct' }); arbiter.setRelationConfig('risk_bonus', { type: 'direct' }); arbiter.setRelationConfig('risk_noise', { type: 'direct' }); arbiter.setRelationConfig('risk_limit', { type: 'direct' }); arbiter.setRelationConfig('risk_cap', { type: 'direct' }); arbiter.setRelationConfig('risk_ok_owa', { type: 'relational_comparator', comparator: '<=', fallbackBehavior: 'deny', left: { rule: { union: { rules: [ { type: 'direct', relation: 'risk_score' }, { type: 'direct', relation: 'risk_bonus' }, { type: 'direct', relation: 'risk_noise' } ], aggregator: 'owa', owaWeights: [0.5, 0.3, 0.2] } }, extractValue: true, valueRelation: 'risk_score', aggregator: 'owa', owaWeights: [0.5, 0.3, 0.2] }, right: { rule: { union: { rules: [ { type: 'direct', relation: 'risk_limit' }, { type: 'direct', relation: 'risk_cap' } ], aggregator: 'owa', owaWeights: [0.6, 0.4] } }, extractValue: true, valueRelation: 'risk_limit', evaluateFrom: 'object', aggregator: 'owa', owaWeights: [0.6, 0.4] } }); const computeExpected = (userId, resourceId) => { const valuesLeft = [ userId % 2 === 0 ? 20 : 80, userId % 2 === 0 ? 5 : 15, userId % 2 === 0 ? 0 : 10 ]; const leftIntervals = valuesLeft.map(value => ({ min: value, max: value })); const leftMeta = valuesLeft.map(() => ({ ruleType: 'defeasible', rule: {} })); const leftFused = OWAFusion.fuseIntervalsWithMeta(leftIntervals, leftMeta, [0.5, 0.3, 0.2], 'owa'); const rightValues = [40, 45]; const rightIntervals = rightValues.map(value => ({ min: value, max: value })); const rightMeta = rightValues.map(() => ({ ruleType: 'defeasible', rule: {} })); const rightFused = OWAFusion.fuseIntervalsWithMeta(rightIntervals, rightMeta, [0.6, 0.4], 'owa'); return leftFused.interval.min <= rightFused.interval.min; }; const pickWithExpectation = (expected) => { const pool = expected ? trueIds : falseIds; if (pool.length === 0) { const userId = randInt(rng, size); return { user: `user:${userId}`, object: `resource:${userId}` }; } const userId = pool[randInt(rng, pool.length)]; return { user: `user:${userId}`, object: `resource:${userId}` }; }; const queries = createQueries( Math.min(2000, size), () => pickWithExpectation(true), () => pickWithExpectation(false) ); return { queries: shuffleWithRng(queries, rng) }; } }, { name: 'challenge_mfa_within', relation: 'secure_action', setup: (arbiter, size, rng) => { buildUsers(arbiter, size, 'user'); buildUsers(arbiter, size, 'resource'); arbiter.setRelationConfig('secure_action', { type: 'challenge', challenge: 'mfa', subject: 'user', withinMs: 5 * 60 * 1000 }); const now = Date.now(); const queries = createChallengeQueries( Math.min(2000, size * 2), () => { const id = randInt(rng, size); return { user: `user:${id}`, object: `resource:${id}`, options: { partialGraph: { challenges: [ { name: 'mfa', subject: `user:${id}`, issuedAt: now } ] } } }; }, () => { const id = randInt(rng, size); return { user: `user:${id}`, object: `resource:${id}`, options: { partialGraph: { challenges: [ { name: 'mfa', subject: `user:${id}`, issuedAt: now - 10 * 60 * 1000 } ] } } }; } ); return { queries: shuffleWithRng(queries, rng) }; } }, { name: 'challenge_composed_union', relation: 'can_delete', setup: (arbiter, size, rng) => { buildUsers(arbiter, size, 'user'); buildUsers(arbiter, size, 'resource'); arbiter.setRelationConfig('secure_connection', { union: { rules: [ { type: 'challenge', challenge: 'mfa', subject: 'user', withinMs: 5 * 60 * 1000 }, { type: 'challenge', challenge: 'webauthn', subject: 'user', withinMs: 5 * 60 * 1000 } ] } }); arbiter.setRelationConfig('can_delete', { type: 'computed', relation: 'secure_connection' }); const now = Date.now(); const queries = createChallengeQueries( Math.min(2000, size * 2), () => { const id = randInt(rng, size); return { user: `user:${id}`, object: `resource:${id}`, options: { partialGraph: { challenges: [ { name: 'mfa', subject: `user:${id}`, issuedAt: now } ] } } }; }, () => { const id = randInt(rng, size); return { user: `user:${id}`, object: `resource:${id}`, options: { partialGraph: { challenges: [] } } }; } ); return { queries: shuffleWithRng(queries, rng) }; } } ]; const args = parseArgs(process.argv); const config = { sizes: parseSizes(args.get('sizes')) || [1000, 5000, 10000], samples: Number(args.get('samples') || 2000), seed: Number(args.get('seed') || 42), maxSeconds: Number(args.get('max-seconds') || 60), verify: args.has('verify'), snapshot: args.has('snapshot') }; const rng = createRng(config.seed); const startTime = Date.now(); console.log('b2c_auth_bench'); console.log('scenario,nodes,edges,build_ms,auth_ms,auth_qps,precision,recall,f1,p95_ms,p99_ms'); for (const scenario of scenarios) { for (const size of config.sizes) { if ((Date.now() - startTime) / 1000 > config.maxSeconds) break; const arbiter = new Arbiter(); const buildStart = Date.now(); const { queries } = scenario.setup(arbiter, size, rng); let buildMs = Date.now() - buildStart; if (config.snapshot) { const snapshotStart = Date.now(); arbiter.enableCondensedSnapshot(); buildMs += Date.now() - snapshotStart; } const actualQueries = queries.slice(0, config.samples); let tp = 0; let tn = 0; let fp = 0; let fn = 0; const latencies = []; const authStart = process.hrtime.bigint(); for (const query of actualQueries) { const start = process.hrtime.bigint(); const options = query.options ? { fastPath: true, ...query.options } : { fastPath: true }; const result = arbiter.check(query.user, scenario.relation, query.object, options); const predicted = result && result.possibility > 0; if (predicted && query.expected) tp++; else if (!predicted && !query.expected) tn++; else if (predicted && !query.expected) fp++; else if (!predicted && query.expected) fn++; latencies.push(Number(process.hrtime.bigint() - start) / 1e6); } const authMs = Number(process.hrtime.bigint() - authStart) / 1e6; const authQps = authMs > 0 ? Math.round((actualQueries.length / authMs) * 1000) : 0; const precision = tp + fp > 0 ? tp / (tp + fp) : 0; const recall = tp + fn > 0 ? tp / (tp + fn) : 0; const f1 = precision + recall > 0 ? (2 * precision * recall) / (precision + recall) : 0; const sorted = latencies.sort((a, b) => a - b); const p95 = percentile(sorted, 95).toFixed(3); const p99 = percentile(sorted, 99).toFixed(3); console.log([ scenario.name, size, arbiter.relations.length, buildMs, Math.round(authMs), authQps, precision.toFixed(3), recall.toFixed(3), f1.toFixed(3), p95, p99 ].join(',')); if (config.verify && (fp + fn) > 0) { throw new Error(`Verification failed for ${scenario.name} size ${size}: fp=${fp} fn=${fn}`); } } }