js-rigor: security affordances — gated provenance, audit hook, DoS hardening, explicit semantics

Per the trust-boundary direction (the caller owns evidence validation):

- Explicit possibilistic semantics module (src/core/possibility.js): the
  single authoritative home for what each operator means (max = disjunctive
  already-valid; min = unvalidified conjunctive ranking with the K-
  validification and surfaced conflict mass; product = Thm-4 heuristic;
  interior OWA = non-maxitive heuristic; reliability = adaptation, never
  conflated with plausibility).
- Provenance is opt-in (re-entrant tracing practice): default check
  results carry only {label, operator, regime}; conflictMass,
  validifiedPossibility, sources, and nonMaxitive appear only under
  includeMeta and on the explain surface. The direct-check cache now
  caches only the meta-less form — includeMeta callers always get a fresh
  full evaluation (previously a cached minimal result was served for
  includeMeta requests, silently stripping detail).
- Audit affordance: new Arbiter({ audit }) emits one record per check
  (decision, possibility, binary, partialGraphUsed, validityLabel,
  sources). The engine stores nothing — the caller owns persistence;
  zero cost when the hook is absent (and the full validity is forced only
  on audit-enabled deployments).
- DoS hardening: partial-graph size limits are enforced BEFORE the
  context allocation (the caller-supplied overlay is the per-check
  allocation point); the CondensedGraphBinary reader gained full bounds
  guards so malformed snapshot buffers fail with clean errors instead of
  RangeError crashes or oversized allocations.
- New security-affordance pins: gating, audit records, and pre-allocation
  limits.
This commit is contained in:
John Dvorak
2026-08-02 09:28:57 -07:00
parent 58e8b0e030
commit 86729715f1
10 changed files with 268 additions and 25 deletions
+14 -11
View File
@@ -1,4 +1,4 @@
import { buildValidity, normalizeValidity, mergeValidity, DEFAULT_VALIDITY } from '../core/validity.js';
import { buildValidity, normalizeValidity, mergeValidity, minimalValidity, DEFAULT_VALIDITY } from '../core/validity.js';
import { Arbiter } from '../core/Arbiter.js';
import { RuleEvaluator } from './RuleEvaluator.js';
import { RuleCollector } from './RuleCollector.js';
@@ -86,7 +86,7 @@ export class AuthorizationChecker {
// Check cache first using composite key (if caching is enabled)
let cachedResult = null;
let cacheHint = null;
if (!hasPartialGraph && this.decisionCache.directEnabled) {
if (!hasPartialGraph && this.decisionCache.directEnabled && !includeMeta) {
const cacheKey = this._getDirectCheckCacheKey(userKey, relation, objectKey);
const [hitResult, status] = this.decisionCache.peekDirect(cacheKey);
cachedResult = status === 'hit' || status === 'expired' ? { result: hitResult, timestamp: 0 } : null;
@@ -111,8 +111,9 @@ export class AuthorizationChecker {
reason: 'missing_node'
};
// Cache the result (only when no partial graph — same guard as success path)
if (!explain && !hasPartialGraph) {
// Cache the result (only when no partial graph and the default
// meta-less form — includeMeta callers get a fresh full evaluation)
if (!explain && !hasPartialGraph && !includeMeta) {
this._cacheDirectCheckResult(userKey, relation, objectKey, result);
}
return result;
@@ -163,9 +164,11 @@ export class AuthorizationChecker {
} else {
result = {
possibility: directRel.possibility,
validity: directRel.validity !== undefined
? buildValidity('identity', [effectiveRelation], [normalizeValidity(directRel.validity)], 1, directRel.possibility)
: DEFAULT_VALIDITY,
validity: includeMeta
? buildValidity('identity', [effectiveRelation], [directRel.validity !== undefined ? normalizeValidity(directRel.validity) : 'heuristic'], 1, directRel.possibility)
: minimalValidity(directRel.validity !== undefined
? buildValidity('identity', [effectiveRelation], [normalizeValidity(directRel.validity)], 1, directRel.possibility)
: DEFAULT_VALIDITY),
// A denied decision (possibility 0) must not leak the
// relation's reliability — the rule-collection path zeroes it.
reliability: directRel.possibility > 0
@@ -223,8 +226,8 @@ export class AuthorizationChecker {
result.meta.cache = cacheHint;
}
// Cache the result using composite key
if (!explain && !hasPartialGraph) {
// Cache the result using composite key (meta-less form only)
if (!explain && !hasPartialGraph && !includeMeta) {
this._cacheDirectCheckResult(userKey, relation, objectKey, result);
}
return result;
@@ -348,7 +351,7 @@ export class AuthorizationChecker {
const finalResult = {
possibility: resPossibility || 0,
reliability: res.reliability !== undefined ? res.reliability : 1.0,
validity: res.validity || DEFAULT_VALIDITY,
validity: includeMeta ? (res.validity || DEFAULT_VALIDITY) : minimalValidity(res.validity || DEFAULT_VALIDITY),
...(collectValues && res.collectedValues && Array.isArray(res.collectedValues) && { collectedValues: res.collectedValues }),
...(includeMeta && {
meta: {
@@ -566,7 +569,7 @@ export class AuthorizationChecker {
const result = {
possibility: maxAllow,
reliability: maxAllow > 0 ? bestAllowReliability : maxDeny > 0 ? bestDenyReliability : 0,
validity: finalValidity,
validity: includeMeta ? finalValidity : minimalValidity(finalValidity),
...(includeMeta && {
meta: {
allow: bestAllow,