Files
WRNexusJS/packages/authz/src/resource.ts
T
Clintchiz a3ddd39b7b
Quality / quality (ubuntu-latest) (push) Failing after 14m38s
Quality / quality (windows-latest) (push) Canceled after 0s
feat: centralize application framework primitives
2026-08-22 23:07:46 +05:30

60 lines
2.3 KiB
TypeScript

import type { Context } from "@wrnexus/core";
import { decideFor } from "./middleware.ts";
export interface AuthorizedHandlerOptions<Resource, Result extends Response = Response> {
permission: string;
resource?: (ctx: Context) => Resource | null | undefined | Promise<Resource | null | undefined>;
exposeReason?: boolean;
mayDiscover?: (ctx: Context) => boolean | Promise<boolean>;
handle(ctx: Context & { resource: Resource }): Result | Promise<Result>;
}
/** Define an API handler whose resource loading and permission check cannot be skipped. */
export function defineAuthorizedHandler<Resource = undefined, Result extends Response = Response>(
options: AuthorizedHandlerOptions<Resource, Result>,
): (ctx: Context) => Promise<Response> {
return async (ctx) => {
if (!ctx.user) return Response.json({ ok: false, error: "Unauthorized" }, { status: 401 });
const resource = options.resource ? await options.resource(ctx) : (undefined as Resource);
if (options.resource && resource == null) {
const mayDiscover = await options.mayDiscover?.(ctx);
return mayDiscover
? Response.json({ ok: false, error: "Not Found" }, { status: 404 })
: Response.json({ ok: false, error: "Forbidden" }, { status: 403 });
}
const decision = await decideFor(ctx, options.permission, resource);
if (!decision.allowed) {
return ctx.authz!.forbidden(decision, { exposeReason: options.exposeReason });
}
ctx.resource = resource;
return options.handle(ctx as Context & { resource: Resource });
};
}
export interface OwnedResourceDefinition<Resource> {
name: string;
owner(resource: Resource): string | null | undefined;
permissions: {
read?: string;
create?: string;
update?: string;
delete?: string;
readAny?: string;
writeAny?: string;
};
}
/** Shared ownership and override vocabulary for application resource modules. */
export function defineOwnedResource<Resource>(definition: OwnedResourceDefinition<Resource>) {
return Object.freeze({
...definition,
attributes(resource: Resource) {
return { ownerId: definition.owner(resource) };
},
isOwner(subjectId: string, resource: Resource) {
const ownerId = definition.owner(resource);
return Boolean(subjectId && ownerId && subjectId === ownerId);
},
});
}