docs: resolve two pre-flight conflicts in the authz plan

- Global Constraints said the change was additive while Task 8 changed
  authorizeDecision's 403 body. Ruled: the security fix governs; the
  constraint now names it as the one approved exception.
- Task 6 defined permissionsFor and then re-implemented it inline in
  decide. Both now call a single loadEffective helper.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-04 16:24:07 +05:30
co-authored by Claude Opus 5
parent 10da210b0a
commit 0ac648bc26
@@ -14,7 +14,7 @@
- Zero runtime npm dependencies. Use only Bun/WebCrypto/node: builtins.
- `@wrnexus/core` MUST NOT import `@wrnexus/authz`. `can()` stays off `Context`; the resolver lives in `ctx.locals._authz`.
- `@wrnexus/authz` may import **types only** from `@wrnexus/core` (`import type { Context, Middleware }`).
- Existing exports of `@wrnexus/authz` must keep working unchanged. This is additive.
- Existing exports of `@wrnexus/authz` keep working unchanged, with ONE approved exception: Task 8 changes the default 403 body of `authorizeDecision` to stop disclosing policy internals. That break is intentional and ruled on; everything else is additive.
- Framework-owned tables use the `_wrn_` prefix (matching `_wrn_tenant`, `_wrn_cursor`). The spec wrote `wrn_authz_assignment`; use `_wrn_authz_assignment` and `_wrn_authz_grant`.
- `requirePermission` is already exported with signature `(rbac: Rbac, permission: string)`. Do not change it. The new resource-aware guard is named `guardPermission`.
- Every failure path denies. Never fail open.
@@ -1344,13 +1344,21 @@ export function createAuthzResolver(options: AuthzResolverOptions): AuthzResolve
const { catalog, store, audit } = options;
const strict = options.strict ?? !isProduction();
const permissionsFor = async (subjectId: string, scope?: AuthzScope): Promise<Set<string>> => {
/**
* Single source of truth for "what does this subject hold?". Returns the
* raw assignments too, because `decide` needs `denies` and `permissionsFor`
* does not — do NOT duplicate this logic in either caller.
*/
const loadEffective = async (subjectId: string, scope?: AuthzScope) => {
const assignments = await store.assignmentsFor(subjectId, scope);
const granted = expandRoles(catalog, assignments.roles);
for (const grant of assignments.grants) granted.add(grant);
return granted;
return { assignments, granted };
};
const permissionsFor = async (subjectId: string, scope?: AuthzScope): Promise<Set<string>> =>
(await loadEffective(subjectId, scope)).granted;
const finish = (input: DecideInput, result: AuthorizationDecision): AuthorizationDecision => {
if (!result.allowed || options.auditAllows) {
safeRecord(audit, {
@@ -1399,9 +1407,7 @@ export function createAuthzResolver(options: AuthzResolverOptions): AuthzResolve
let assignments;
let granted: Set<string>;
try {
assignments = await store.assignmentsFor(subjectId, scope);
granted = expandRoles(catalog, assignments.roles);
for (const grant of assignments.grants) granted.add(grant);
({ assignments, granted } = await loadEffective(subjectId, scope));
} catch (error) {
console.error("[wrnexus:authz] permission store failed; denying", error);
return finish(input, { allowed: false, reason: "Authorization store unavailable" });