diff --git a/src/authorization/AuthorizationChecker.js b/src/authorization/AuthorizationChecker.js index 3644d59..afffad8 100644 --- a/src/authorization/AuthorizationChecker.js +++ b/src/authorization/AuthorizationChecker.js @@ -659,37 +659,6 @@ export class AuthorizationChecker { }; } - // Check for cycles using efficient approach - const useKeyedVisited = this._getVisitedMode(_visited); - const visitKey = useKeyedVisited ? this._getVisitedKey(userId, relation, objectId) : null; - if (useKeyedVisited) { - if (_visited.has(visitKey)) { - return { - possibility: 0, - reliability: 0, - validity: includeMeta ? DEFAULT_VALIDITY : minimalValidity(DEFAULT_VALIDITY), - reason: 'cycle', - binary: true, - ...(evaluation && { evaluation }) - }; - } - } else { - for (const visited of _visited) { - if (visited.userKey === userKey && visited.relation === relation && visited.objectKey === objectKey) { - return { - possibility: 0, - reliability: 0, - validity: includeMeta ? DEFAULT_VALIDITY : minimalValidity(DEFAULT_VALIDITY), - reason: 'cycle', - binary: true, - ...(evaluation && { evaluation }) - }; - } - } - } - - _visited.add(useKeyedVisited ? visitKey : { userKey, relation, objectKey }); - const config = this.arbiter.relationConfigs.get(relation); if (!config) { return { @@ -766,6 +735,41 @@ export class AuthorizationChecker { } } + // Check for cycles using efficient approach. + // Deliberately placed AFTER the direct fast path: direct checks never + // recurse, so they must not pay for key allocation / Set mutation. Only + // the rule-evaluation branches (which recurse via evaluateRule with the + // shared _visited) need cycle detection. + const useKeyedVisited = this._getVisitedMode(_visited); + const visitKey = useKeyedVisited ? this._getVisitedKey(userId, relation, objectId) : null; + if (useKeyedVisited) { + if (_visited.has(visitKey)) { + return { + possibility: 0, + reliability: 0, + validity: includeMeta ? DEFAULT_VALIDITY : minimalValidity(DEFAULT_VALIDITY), + reason: 'cycle', + binary: true, + ...(evaluation && { evaluation }) + }; + } + } else { + for (const visited of _visited) { + if (visited.userKey === userKey && visited.relation === relation && visited.objectKey === objectKey) { + return { + possibility: 0, + reliability: 0, + validity: includeMeta ? DEFAULT_VALIDITY : minimalValidity(DEFAULT_VALIDITY), + reason: 'cycle', + binary: true, + ...(evaluation && { evaluation }) + }; + } + } + } + + _visited.add(useKeyedVisited ? visitKey : { userKey, relation, objectKey }); + // Handle logical operators with binary evaluation if (config.union || config.intersection || config.exclusion) { const res = this.ruleEvaluator.evaluateRule(