release: WRNexusJS 0.4.0

This commit is contained in:
2026-07-27 12:42:18 +05:30
parent 8b728a3e5d
commit 30e5721e84
250 changed files with 10065 additions and 3923 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/authz",
"version": "0.3.6",
"version": "0.4.0",
"private": true,
"type": "module",
"main": "src/index.ts",
+92
View File
@@ -0,0 +1,92 @@
import type { Context, Middleware } from "@wrnexus/core";
import type { Policy, Subject } from "./index.ts";
export interface AuthorizationDecision {
allowed: boolean;
reason?: string;
policy?: string;
metadata?: Record<string, unknown>;
}
export type DecisionPolicy<S = Subject, R = unknown> = (
subject: S,
resource?: R,
) => AuthorizationDecision | Promise<AuthorizationDecision>;
export function allow(reason?: string, metadata?: Record<string, unknown>): AuthorizationDecision {
return { allowed: true, reason, metadata };
}
export function deny(
reason = "Forbidden",
metadata?: Record<string, unknown>,
): AuthorizationDecision {
return { allowed: false, reason, metadata };
}
export function decision<S, R>(
name: string,
policy: Policy<S, R>,
denial = "Policy denied access",
): DecisionPolicy<S, R> {
return async (subject, resource) => {
const allowed = await policy(subject, resource);
return {
allowed,
reason: allowed ? undefined : denial,
policy: name,
};
};
}
export function owner<SubjectType extends Subject, Resource extends Record<string, unknown>>(
subjectKey: keyof SubjectType = "id",
resourceKey: keyof Resource | string = "userId",
): DecisionPolicy<SubjectType, Resource> {
return (subject, resource) =>
resource && Object.is(subject[subjectKey], resource[resourceKey as keyof Resource])
? allow("resource owner")
: deny("resource ownership required");
}
export function anyDecision<S, R>(...policies: DecisionPolicy<S, R>[]): DecisionPolicy<S, R> {
return async (subject, resource) => {
const denied: AuthorizationDecision[] = [];
for (const policy of policies) {
const result = await policy(subject, resource);
if (result.allowed) return result;
denied.push(result);
}
return deny(
denied
.map((item) => item.reason)
.filter(Boolean)
.join("; ") || "No policy allowed access",
);
};
}
export function allDecisions<S, R>(...policies: DecisionPolicy<S, R>[]): DecisionPolicy<S, R> {
return async (subject, resource) => {
for (const policy of policies) {
const result = await policy(subject, resource);
if (!result.allowed) return result;
}
return allow("all policies passed");
};
}
export function authorizeDecision(
evaluate: (ctx: Context) => AuthorizationDecision | Promise<AuthorizationDecision>,
): Middleware {
return async (ctx, next) => {
const result = await evaluate(ctx);
if (result.allowed) return next();
return Response.json(
{ ok: false, error: "Forbidden", reason: result.reason, policy: result.policy },
{ status: 403 },
);
};
}
export function filterAuthorized<S, R>(
subject: S,
values: readonly R[],
policy: Policy<S, R>,
): Promise<R[]> {
return Promise.all(
values.map(async (value) => ({ value, allowed: await policy(subject, value) })),
).then((results) => results.filter((result) => result.allowed).map((result) => result.value));
}
+11
View File
@@ -128,3 +128,14 @@ export function requireRole(...roles: string[]): Middleware {
export function requirePermission(rbac: Rbac, permission: string): Middleware {
return authorize((ctx) => rbac.can(ctx.user as Subject | undefined, permission));
}
export {
allow,
deny,
decision,
owner,
anyDecision,
allDecisions,
authorizeDecision,
filterAuthorized,
} from "./advanced.ts";
export type { AuthorizationDecision, DecisionPolicy } from "./advanced.ts";
+15
View File
@@ -9,6 +9,7 @@ import {
any,
all,
attr,
decision,
type Policy,
} from "../src/index.ts";
@@ -78,3 +79,17 @@ test("guards: authorize / requireRole / requirePermission", async () => {
(await authorize((c) => (c.user as User)?.id === "u1")(ctx({ id: "u1" }), ok)).status,
).toBe(200);
});
test("explainable decisions only include denial reasons when denied", async () => {
const policy = decision("owner", (subject: User) => subject.id === "u1");
expect(await policy({ id: "u1" })).toEqual({
allowed: true,
reason: undefined,
policy: "owner",
});
expect(await policy({ id: "u2" })).toEqual({
allowed: false,
reason: "Policy denied access",
policy: "owner",
});
});