Authentication Boundary
The host authenticates the request first; permission-core answers authorization questions only after it receives a trusted PermissionSubject. It does not issue sessions, validate passwords or tokens, refresh credentials, or provide login/logout APIs.
Responsibility Model
Text equivalent.Credentials or sessions are authenticated by the host first. The host supplies trusted user identity, scope, and claims to build a PermissionSubject. Only then does permission-core authorize the route, menu projection, or data operation; credential checks and account state remain host responsibilities.
Authentication owns credential checks, account status, session lifetime, and identity recovery. permission-core owns role/rule lookup, deny-first decisions, menu projection, and authorized data operations inside a trusted scope. Business handlers still own object existence and domain invariants.
Accepted Vext Shapes
The built-in Vext resolver requires isAuthenticated: true and accepts exactly one subject shape:
Do not mix permissionSubject with the flat userId/scope shape. Missing req.auth, isAuthenticated: false, incomplete identity, or conflicting subject shapes fail with VEXT_AUTH_REQUIRED or INVALID_SUBJECT.
These snippets show the data that the host authentication middleware writes. They are not permission-core method calls or HTTP responses. req.auth must be established by trusted server code before the permission middleware runs; a client JSON body with the same keys is not trusted.
Custom Subject Resolution
Use a resolver when your authentication plugin stores identity in another trusted shape:
permissionPlugin(options) returns the Vext plugin synchronously. subject.resolve(req) runs lazily when a protected request first needs a permission subject and may return the PermissionSubject synchronously or asynchronously. It must read only trusted authentication state and server context; its return value is not an HTTP response.
If req.auth also carries a normalized permissionSubject or userId + scope, the resolver result must point to the same user and complete scope. Mismatches fail with SCOPE_CONFLICT; the plugin does not silently pick one. Claims can provide policy values, but copying client headers or request-body values into claims does not make them trusted.
Protected and Public Routes
Routes with no permission option, or with permission: false, are public from permission-core's point of view and do not force lazy subject resolution. permission: true and explicit requirements require authentication and authorization before the handler runs. Application code can also request the lazy context:
can returns a boolean. assert returns void on success and maps denied access to 403. This API belongs to the original request; storing it for jobs, queues, or later requests fails closed.
Historical Option
Earlier versions exposed resolveSubject(auth, req). It is still present for historical migrations, but new projects should use subject.resolve(req) only. The two options cannot be configured together; see the Vext Plugin API for the compatibility rules.
Failure Boundary and Next Step
Missing or invalid authenticated subjects map to 401, authenticated-but-denied requests map to 403, and untrusted authorization state maps to 503. Do not downgrade database, schema, route-reload, or persistence failures to allow. Background jobs should create a fresh trusted PermissionSubject and call core directly.
Continue with Multi-Tenant Model, Vext Plugin, and Errors API.