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:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user