Runtime validation
The adapter returns data from a database that Aegis Guard does not control. You
can validate that data at runtime with any validation library, or none. A
validator is just a function (data: unknown) => T that returns the value or
throws.
Register validators
Pass validators to initAegis or createGuard:
import { z } from "zod"; // your dependency, not Aegis Guard's
initAegis({
adapter,
getSecurityContext,
validators: {
context: (d) => z.object({ userId: z.string().nullable() }).strict().parse(d),
roles: (d) => z.array(z.string()).parse(d), // getUserRoles and getAllowedRoles
resource: (d) => z.object({ key: z.string(), description: z.string().optional() }).parse(d),
body: (d) => z.object({ resource: z.string().min(1), roles: z.array(z.string()) }).parse(d),
},
});The same shape works with other libraries, with no Aegis Guard dependency on them:
// Valibot
roles: (d) => v.parse(v.array(v.string()), d),
// ArkType
roles: type("string[]").assert,
// Hand written
roles: (d) => {
if (!Array.isArray(d) || d.some((x) => typeof x !== "string")) {
throw new TypeError("roles must be string[]");
}
return d;
},Fail closed
If a validator throws, the call rejects with AegisValidationError (which extends
AegisError, code VALIDATION_FAILED) before any decision is scored. A check can
never silently grant access on corrupt data. A denied request still throws
AegisDeniedError, so you can tell a real denial (map to 403) apart from corrupt
data (map to 500):
import { AegisDeniedError, AegisValidationError } from "aegis-guard/server";
try {
await requirePermission("admin.page");
} catch (error) {
if (error instanceof AegisDeniedError) {
return new Response("Forbidden", { status: 403 });
}
if (error instanceof AegisValidationError) {
return new Response("Server error", { status: 500 });
}
throw error;
}AegisValidationError carries .source (which boundary failed: context,
getUserRoles, getAllowedRoles, or getAllResources), .resourceKey when
known, and the original library error on .cause.
Notes
- Validators are opt in. With none configured, behavior is identical to before and adds no overhead: the adapter is passed through by reference.
- Adapter validators must only assert, never transform. A validator that adds
roles or claims would widen access. Use strict schemas for
contextso unexpected fields are rejected. - Validators are synchronous.