Checks
A check is a per-(caller, resource) boolean predicate, expressed as an Effect so it can reach a
repository, a bus, or anything else through the Effect environment.
type Check<Caller, Resource, E = never, R = never> = ( caller: Caller, resource: Resource,) => Effect.Effect<boolean, E, R>;Why boolean, not void-or-denial
Section titled “Why boolean, not void-or-denial”Returning boolean is what lets checks compose before the single lift to a denial happens at the
hasPermissions boundary. A check that raised the host’s denial error itself could not be OR’d with
another — the first one to run would have already failed the effect.
One denial, at one place, is also what keeps the error out of every intermediate signature.
Combinators
Section titled “Combinators”import { Check } from "@effect-server-utils/authz";
Check.any(isOwner, isAdmin); // OR — short-circuits on the first trueCheck.all(isOwner, notArchived); // AND — short-circuits on the first falseThe empty cases are chosen deliberately:
Check.any()with no checks returnsfalse— denying by default is the safer floor.Check.all()with no checks returnstrue, vacuously — the same semantics asArray.every.
Both parameters stay open on Check itself, so you can compose checks that are not registered anywhere.
The typed registry is what pins Caller to your configured caller and Resource to the resource’s
registered type.
CallerCheck — the narrower arity
Section titled “CallerCheck — the narrower arity”A check that inspects only the caller declares one parameter:
type CallerCheck<Caller, E = never, R = never> = (caller: Caller) => Effect.Effect<boolean, E, R>;
const isAdmin: CallerCheck<Caller> = (caller) => Effect.succeed(caller.roles.includes("admin"));Declaring the narrower arity — rather than a Check<Caller, unknown> that ignores its second parameter —
is what lets one such check serve both an unscoped resource, whose checks are handed no resource at
all, and a scoped one. TypeScript accepts a fewer-parameter function wherever a more-parameter one is
expected, never the reverse.
So isAdmin above can be registered against platform.administer and OR’d into todo.update in the
same file.
Writing a check
Section titled “Writing a check”const isOwner = (caller: Caller, todo: Todo) => Effect.succeed(todo.ownerId === caller.userId);
const isInSameOrg = (caller: Caller, todo: Todo) => Effect.gen(function* () { const membership = yield* organizationAcl.membershipFor(caller.userId); return membership.organizationId === todo.organizationId; });The second one reaches a store, and can therefore fail with the configured CheckFailure. That is
expected: a transient outage during authorization is the caller’s problem to retry, and a check that
died on it instead would report a retryable outage as an internal error.
Closed over their dependencies
Section titled “Closed over their dependencies”Registered checks are typed with R = never:
type ResourceCheck<Resource> = ( caller: Caller, resource: Resource,) => Effect.Effect<boolean, CheckFailure, never>;A module needing cross-module data closes over its own ACL port at the registration site rather than
reaching a shared service through the environment. That is what keeps R = never, and what lets a
policy unit test provide nothing at all:
export const makeTodoPolicies = (organizationAcl: OrganizationAcl) => ({ todo: { update: Check.any(isOwner, isOrgAdmin(organizationAcl)), },});Type reference
Section titled “Type reference”| Export | What it is |
|---|---|
Check.any(...checks) |
OR, short-circuits on the first true, empty ⇒ false |
Check.all(...checks) |
AND, short-circuits on the first false, empty ⇒ true |
Check<Caller, Resource, E, R> |
the open predicate type |
CallerCheck<Caller, E, R> |
caller-only arity, usable in both positions |
ResourceCheck<Resource> |
what a scoped resource’s registry entry must be |
UnscopedCheck |
what an unscoped resource’s registry entry must be |