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:
John Dvorak
2026-07-31 13:44:06 -07:00
commit 717ae1031e
373 changed files with 654131 additions and 0 deletions
+346
View File
@@ -0,0 +1,346 @@
/**
* rigor/challenge-rule.test.js — js-rigor property tests for ChallengeRule.evaluate.
*
* ChallengeRule.resolveSubjectKey and ChallengeRule.resolveWithinMs are
* pure functions on rule config. Properties:
* - subjectKey explicit override beats subject type
* - subject=user → userKey
* - subject=object → objectKey
* - subject=session → sessionKey (else userKey)
* - withinMs/withinSeconds/withinMinutes/withinHours are equivalent (each unit * factor)
* - at most one of the four `within` keys is used (others ignored)
* - if none of the four is set, withinMs is null
*
* The proof lookup (ChallengeRule.evaluate path) is tested separately in
* challenge-proof.test.js; here we focus on the resolver surface.
*/
import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { rigor } from '@rigor/core';
import { ChallengeRule } from '../../src/authorization/rules/ChallengeRule.js';
const SUBJECT_TYPES = ['user', 'object', 'session', null, 'unknown'];
/**
* Stub arbiter that satisfies BaseRule + ChallengeRule's surface needs.
*/
function makeStubArbiter() {
return {
nodeIdByKey: new Map(),
keyByNodeId: new Map(),
relations: [],
nodes: new Map(),
resolveNodeId(key /* , options */) { return 1; }
};
}
function makeRule() {
return new ChallengeRule(makeStubArbiter());
}
describe('ChallengeRule._resolveSubjectKey (rigor)', () => {
it('explicit rule.subjectKey wins over rule.subject', async () => {
async function check(subjectKey, subjectType, userKey, objectKey, sessionKey) {
const rule = makeRule();
const result = rule._resolveSubjectKey(
{ subjectKey, subject: subjectType },
userKey, objectKey, { sessionKey }
);
if (result !== subjectKey) {
throw new Error(
`expected ${subjectKey}, got ${result}. subject=${subjectType}, userKey=${userKey}, objectKey=${objectKey}`
);
}
return result;
}
const report = await rigor.campaign(
[
rigor.fn('check', check,
rigor.args(
rigor.gen.string(1, 20),
rigor.gen.enum(SUBJECT_TYPES),
rigor.gen.string(1, 20),
rigor.gen.string(1, 20),
rigor.gen.string(1, 20)
)
)
],
rigor.crucible([
rigor.invariant('subjectKey-wins', ({ error, errorMessage }) => !error && !errorMessage)
])
).run({ effort: 1000 });
if (process.env.TEST_DEBUG === '1') console.log(report.toTAP());
const inv = report.crucibleVerdict?.invariants?.find(i => i.name === 'subjectKey-wins');
assert.ok(inv);
assert.equal(inv.passed, true,
`subjectKey override did not win in ${inv.failureCount} cases`);
});
it('subject=user → userKey; subject=object → objectKey; subject=session → sessionKey or userKey', async () => {
async function check(subjectType, userKey, objectKey, sessionKey, hasSession) {
const rule = makeRule();
const result = rule._resolveSubjectKey(
{ subject: subjectType },
userKey, objectKey,
hasSession ? { sessionKey } : {}
);
let expected;
switch (subjectType) {
case 'object': expected = objectKey; break;
case 'session': expected = sessionKey || userKey; break;
case 'user':
case null:
case 'unknown':
default: expected = userKey; break;
}
if (result !== expected) {
throw new Error(
`subject=${subjectType}: expected ${expected}, got ${result}`
);
}
return result;
}
const report = await rigor.campaign(
[
rigor.fn('check', check,
rigor.args(
rigor.gen.enum(SUBJECT_TYPES),
rigor.gen.string(1, 20),
rigor.gen.string(1, 20),
rigor.gen.string(1, 20),
rigor.gen.boolean()
)
)
],
rigor.crucible([
rigor.invariant('subject-mapping', ({ error, errorMessage }) => !error && !errorMessage)
])
).run({ effort: 1500 });
if (process.env.TEST_DEBUG === '1') console.log(report.toTAP());
const inv = report.crucibleVerdict?.invariants?.find(i => i.name === 'subject-mapping');
assert.ok(inv);
assert.equal(inv.passed, true,
`subject type mapping incorrect in ${inv.failureCount} cases`);
});
});
describe('ChallengeRule._resolveWithinMs (rigor)', () => {
/**
* Custom generator: pick one of 5 candidate shapes and produce the
* corresponding rule. Avoids the `undefined` field trap that
* rigor.gen.object doesn't support.
*/
const withinRuleGen = rigor.gen.oneOf([
// Only withinMs set
rigor.gen.object({
withinMs: rigor.gen.int(1, 10000),
comparator: rigor.gen.constant(null)
}),
// Only withinSeconds set (no withinMs)
rigor.gen.object({
withinMs: rigor.gen.constant(null),
withinSeconds: rigor.gen.int(1, 100)
}),
// Only withinMinutes set
rigor.gen.object({
withinMs: rigor.gen.constant(null),
withinSeconds: rigor.gen.constant(null),
withinMinutes: rigor.gen.int(1, 10)
}),
// Only withinHours set
rigor.gen.object({
withinMs: rigor.gen.constant(null),
withinSeconds: rigor.gen.constant(null),
withinMinutes: rigor.gen.constant(null),
withinHours: rigor.gen.int(1, 5)
}),
// Empty (no within key)
rigor.gen.object({
comparator: rigor.gen.string()
})
]);
it('withinMs/withinSeconds/withinMinutes/withinHours are equivalent', async () => {
async function check(rule) {
const r = makeRule();
const result = r._resolveWithinMs(rule);
// The rule produced by withinRuleGen may have a `null` value for
// some within* keys. _resolveWithinMs treats both undefined AND
// null as "absent" (its `!== undefined && !== null` check). So
// for our generator, `null` and missing both count as absent.
let expected = null;
if (rule.withinMs !== undefined && rule.withinMs !== null) {
expected = rule.withinMs;
} else if (rule.withinSeconds !== undefined && rule.withinSeconds !== null) {
expected = rule.withinSeconds * 1000;
} else if (rule.withinMinutes !== undefined && rule.withinMinutes !== null) {
expected = rule.withinMinutes * 60 * 1000;
} else if (rule.withinHours !== undefined && rule.withinHours !== null) {
expected = rule.withinHours * 60 * 60 * 1000;
}
if (result !== expected) {
throw new Error(
`rule=${JSON.stringify(rule)}: expected ${expected}, got ${result}`
);
}
return result;
}
const report = await rigor.campaign(
[
rigor.fn('check', check,
rigor.args(withinRuleGen)
)
],
rigor.crucible([
rigor.invariant('within-units', ({ error, errorMessage }) => !error && !errorMessage)
])
).run({ effort: 800 });
if (process.env.TEST_DEBUG === '1') console.log(report.toTAP());
const inv = report.crucibleVerdict?.invariants?.find(i => i.name === 'within-units');
assert.ok(inv);
assert.equal(inv.passed, true,
`withinMs/withinSeconds/withinMinutes/withinHours conversion wrong in ${inv.failureCount} cases`);
});
it('priority order: withinMs > withinSeconds > withinMinutes > withinHours', async () => {
async function check(rule) {
const r = makeRule();
const result = r._resolveWithinMs(rule);
let expected = null;
if (rule.withinMs != null) expected = rule.withinMs;
else if (rule.withinSeconds != null) expected = rule.withinSeconds * 1000;
else if (rule.withinMinutes != null) expected = rule.withinMinutes * 60 * 1000;
else if (rule.withinHours != null) expected = rule.withinHours * 60 * 60 * 1000;
if (result !== expected) {
throw new Error(`expected ${expected}, got ${result} for ${JSON.stringify(rule)}`);
}
return result;
}
// Generate rules where all four keys are populated. The priority
// chain must pick withinMs.
const allFourSet = rigor.gen.object({
withinMs: rigor.gen.int(100, 500),
withinSeconds: rigor.gen.int(1, 100),
withinMinutes: rigor.gen.int(1, 10),
withinHours: rigor.gen.int(1, 5)
});
// withinMs=0 — must still win (it's "set", even if value is 0)
const msZero = rigor.gen.object({
withinMs: rigor.gen.constant(0),
withinSeconds: rigor.gen.int(1, 100),
withinMinutes: rigor.gen.int(1, 10),
withinHours: rigor.gen.int(1, 5)
});
// withinMs absent, withinSeconds present
const noMs = rigor.gen.object({
withinMs: rigor.gen.constant(null),
withinSeconds: rigor.gen.int(1, 100),
withinMinutes: rigor.gen.int(1, 10),
withinHours: rigor.gen.int(1, 5)
});
// only withinMinutes present
const onlyMin = rigor.gen.object({
withinMs: rigor.gen.constant(null),
withinSeconds: rigor.gen.constant(null),
withinMinutes: rigor.gen.int(1, 10),
withinHours: rigor.gen.int(1, 5)
});
const report = await rigor.campaign(
[
rigor.fn('check', check,
rigor.args(rigor.gen.oneOf([allFourSet, msZero, noMs, onlyMin]))
)
],
rigor.crucible([
rigor.invariant('within-priority', ({ error, errorMessage }) => !error && !errorMessage)
])
).run({ effort: 800 });
if (process.env.TEST_DEBUG === '1') console.log(report.toTAP());
const inv = report.crucibleVerdict?.invariants?.find(i => i.name === 'within-priority');
assert.ok(inv);
assert.equal(inv.passed, true,
`within key priority wrong in ${inv.failureCount} cases`);
});
it('returns null when no within key is set', async () => {
async function check(rule) {
const r = makeRule();
const result = r._resolveWithinMs(rule);
if (result !== null) {
throw new Error(`expected null, got ${result} for ${JSON.stringify(rule)}`);
}
return result;
}
const report = await rigor.campaign(
[
rigor.fn('check', check,
rigor.args(
rigor.gen.object({
other: rigor.gen.int(),
comparator: rigor.gen.string()
})
)
)
],
rigor.crucible([
rigor.invariant('null-when-absent', ({ error, errorMessage }) => !error && !errorMessage)
])
).run({ effort: 500 });
if (process.env.TEST_DEBUG === '1') console.log(report.toTAP());
const inv = report.crucibleVerdict?.invariants?.find(i => i.name === 'null-when-absent');
assert.ok(inv);
assert.equal(inv.passed, true,
`_resolveWithinMs returned non-null in ${inv.failureCount} cases when no key was set`);
});
});
describe('ChallengeRule._buildRequirement (rigor)', () => {
it('preserves challenge, subject, withinMs, status fields', async () => {
async function check(challenge, subject, withinMs, status) {
const r = makeRule();
const result = r._buildRequirement(challenge, subject, withinMs, status);
const expected = {
name: challenge,
subject,
withinMs: withinMs || null,
status
};
assert.deepStrictEqual(result, expected,
`mismatch: result=${JSON.stringify(result)} expected=${JSON.stringify(expected)}`);
return result;
}
const report = await rigor.campaign(
[
rigor.fn('check', check,
rigor.args(
rigor.gen.string(1, 30),
rigor.gen.string(1, 30),
rigor.gen.option(rigor.gen.int(0, 100000)),
rigor.gen.enum(['missing', 'missing_context', 'expired'])
)
)
],
rigor.crucible([
rigor.invariant('buildRequirement', ({ error, errorMessage }) => !error && !errorMessage)
])
).run({ effort: 800 });
if (process.env.TEST_DEBUG === '1') console.log(report.toTAP());
const inv = report.crucibleVerdict?.invariants?.find(i => i.name === 'buildRequirement');
assert.ok(inv);
assert.equal(inv.passed, true,
`_buildRequirement contract violated in ${inv.failureCount} cases`);
});
});