authorize
authorize(options?: AuthorizeOptions): RouteBuilder<Current>
Declare an authorization requirement on the next route. Route-only, same staging convention as .id, .title, .description, .input, .output, .tag, and .batch: it writes onto the next-route options. Calling a pipeline op (.to, .transform, .process, ...) while authorizers are staged but no .from() has opened the next route throws RC2001 with a message that lists .authorize alongside the other staging ops. For a mid-pipeline check use .validate(authorize({ ... })) directly.
The check runs at route entry, before any pipeline step. It verifies that the inbound exchange carries an authenticated principal and (optionally) that the principal has every required role and scope. It does NOT issue, mint, or attach any credential: it asserts an existing identity meets the criteria. Multiple .authorize() calls stack and AND-combine in declaration order, so a missing role in the first call short-circuits before later predicates run.
.authorize() can also act as a route-starter when chaining routes: craft().from(s1).to(d1).authorize({...}).from(s2).to(d2) opens route 2 with the authorizer staged, no explicit .id("next") required.
For mid-pipeline checks (rare, for example after a .process() swaps the principal or inside a .choice() branch), use .validate(authorize({ ... })) directly with the underlying validator function.
AuthorizeOptions:
Match actors by the (issuer, subject) pair: a bare subject matches a same-named actor from any issuer, which is ambiguous the moment two issuers exist.
Failure modes:
- No principal on the exchange: throws
RC5012. The source did not authenticate (noauth:configured) and no.authenticate()step ran before the check. - Principal not authentic (self-asserted object): throws
RC5023. - Actor present but not admitted (or required but absent): throws
RC5034. - Subject constraint failed: throws
RC5035. - Delegation chain deeper than
maxDelegationDepth: throwsRC5036. - Missing role or failed predicate: throws
RC5015. Permanent: no ceremony changes who the subject is. - Missing scope: throws
RC5038. Recoverable: the cause carriesmissing.scopesso a consent flow can request exactly what is absent.
All codes flow through the route's normal error path: .error() handles them like any other validation failure; without .error(), exchange:failed fires.
Breaking change: delegation is opt-in per route
The actor default is 'none'. Routes written before delegation existed keep exactly their old behavior for direct callers, but a principal carrying an actor (minted by .delegate() or parsed from a token's act claim) is rejected with RC5034 until the route declares its permitted actor(s). This is deliberate: a capability is not agent-reachable unless it says so.
Because stacked .authorize() calls AND-combine, every guard on a route carries its own actor default. Adding .authorize({ actor: 'any' }) next to an existing .authorize({ roles: ['admin'] }) still fails with RC5034, since the first guard rejects the actor before the second runs. Put the actor clause on the guard that needs it, or fold the guards into one.
import { craft, mcp } from '@routecraft/routecraft'
// Route-entry guard: authentication at the source boundary,
// authorization declared on the route.
craft()
.id('delete-user')
.description('Delete a user by id')
.authorize({ roles: ['admin'] })
.from(mcp({ annotations: { destructiveHint: true } }))
.to(deleteUserDestination)
// Stacked authorizers (AND-combined; first failure short-circuits)
craft()
.id('billing-admin')
.authorize({ roles: ['admin'] })
.authorize({ scopes: ['billing:write'] })
.from(http({ path: '/admin/billing', method: 'POST' }))
.to(billingDestination)
// Delegation-aware declarations: the same grammar covers a person
// acting directly, an agent acting on a person's behalf, and an agent
// acting under its own standing authority.
// Humans only; never reachable through delegation (this is the default).
craft()
.id('delete-invoice')
.authorize({ roles: ['finance'], actor: 'none' })
.from(mcp({ annotations: { destructiveHint: true } }))
.to(deleteInvoice)
// A member directly, OR one named agent acting for a member.
craft()
.id('send-reply')
.authorize({
roles: ['member'],
scopes: ['mail:send'],
actor: ['none', { subject: 'agent:zoe', issuer: 'https://agents.example.com' }],
})
.from(direct())
.to(mail())
// Autonomous agents only (e.g. a cron-triggered heartbeat).
craft()
.id('write-daily-note')
.authorize({ subject: { profile: 'ai_agent' }, actor: 'none' })
.from(direct())
.to(writeNote)
// Mid-pipeline check: route mints a principal from an inbound email
// with .authenticate() and authorizes it. authorize() trusts only
// principals minted this way (or attached by a source verifier); a
// plain object written to the principal header is rejected (RC5023).
import { authorize } from '@routecraft/routecraft'
craft()
.from(mail('INBOX', { /* ... */ }))
.authenticate((ex) => {
// The mail source puts the sender on a header, not the body.
const from = ex.headers['routecraft.mail.from']
return {
scheme: 'email',
subject: from ?? 'anonymous',
email: from,
claims: { tenant: deriveTenant(from) },
}
})
.validate(authorize({
predicate: (p) => p.email?.endsWith('@yourcompany.com') === true,
}))
.to(yourDestination)