diff --git a/packages/authz/src/engine.ts b/packages/authz/src/engine.ts index a67a063f..b864c281 100644 --- a/packages/authz/src/engine.ts +++ b/packages/authz/src/engine.ts @@ -54,7 +54,13 @@ export function expandRoles(catalog: AuthzCatalog, roles: readonly string[]): Se return out; } -/** Exact match, root wildcard, or a namespace wildcard at any depth. */ +/** + * Exact match, root wildcard, or a namespace wildcard at any depth. + * + * Do NOT gate access by matching against `permissionsFor()`'s result — that set + * cannot represent a narrow deny beneath a broad grant, so the composition + * returns true where `decide()` refuses. Use `decide()` / `can()` instead. + */ export function permissionMatches(granted: Set, permission: string): boolean { if (granted.has("*") || granted.has(permission)) return true; for (let at = permission.indexOf(":"); at !== -1; at = permission.indexOf(":", at + 1)) { diff --git a/packages/authz/test/exports.test.ts b/packages/authz/test/exports.test.ts index 6c5867e9..85e2d5ef 100644 --- a/packages/authz/test/exports.test.ts +++ b/packages/authz/test/exports.test.ts @@ -43,8 +43,14 @@ describe("@wrnexus/authz exports", () => { "decideFor", "guardPermission", "filterCan", + "scopeKey", + "safeRecord", ]) { expect(typeof (authz as Record)[name]).toBe("function"); } }); + + test("exports the locals key used to reach the per-request resolver", () => { + expect(typeof (authz as Record).AUTHZ_LOCALS_KEY).toBe("string"); + }); });