Agora integration
Opt-in bridges to the Agora ecosystem — tenant auto-scope, the global-role bridge, event-driven provisioning, and the /authz/can endpoint.
All four features below are opt-in and default-OFF. None of them adds a hard dependency: with the rest of Agora absent, each one degrades to its default and authz keeps working standalone with unchanged behavior.
Tenant auto-scope
When a permission check gets no explicit tenant and your tenant resolver
yields none, default the tenant to the active Agora context's tenantId.
resolveTenant is a function — () => string | undefined — called on every
check that still has no tenant. The package ships one ready to use,
tenantFromContext, which reads the active Agora context's tenantId:
import { defineConfig, stores, tenantFromContext } from '@adonis-agora/authz'
export default defineConfig({
default: 'lucid',
stores: { lucid: stores.lucid() },
resolveTenant: tenantFromContext,
})Any zero-argument function works, so you are never tied to the Agora context — read the tenant from wherever your app already keeps it:
import { HttpContext } from '@adonisjs/core/http'
export default defineConfig({
// ...
resolveTenant: () => HttpContext.get()?.request.header('x-tenant'),
})Precedence: explicit scope arg → tenant resolver → resolveTenant. With
resolveTenant unset (the default), no context means the global '' scope, as
before.
Global-role bridge
Bridge an auth provider's global roles — the ones authkit puts on the active Agora context — into authz checks at check time, with nothing seeded in the database.
defineConfig({
// A user holding one of these global roles is allowed everything.
superAdminRoles: ['platform:super'],
// Role → permissions/wildcards, unioned into permission checks.
roleGrants: {
auditor: ['audit.*'],
},
})A super-admin global role short-circuits every decision — can, hasRole,
hasAnyRole and scope — to allow. Otherwise, the permissions roleGrants
maps onto the user's effective roles are unioned with the store's grants and
the whole set goes through the wildcard matcher.
roleGrants applies to the user's effective roles — the context claim, the
resolveRoles seam and the store alike — so it is not limited to global ones.
See Effective roles
for the full model, and Roles
for how superAdminRoles composes with the superAdmin hook.
Reading the context yourself
The two features above are built on a small set of readers the package exports, and you can use them for your own wiring. Each one degrades to a safe default when the Agora context is not there, so calling them is never conditional:
import {
globalRolesFromContext,
readContextValue,
tenantFromContext,
} from '@adonis-agora/authz'
tenantFromContext() // the active tenant id, or undefined
globalRolesFromContext() // the token's global roles, or []
readContextValue('locale') // any other key the context carries, or undefined| Export | Returns | Without a context |
|---|---|---|
tenantFromContext() | The active tenant id. | undefined |
globalRolesFromContext() | The user's global role names. | [] |
readContextValue(key) | Any value the context carries. | undefined |
readContextAccessor() | The context accessor itself. | undefined |
AGORA_CONTEXT_ACCESSOR | The symbol the accessor is published under. | — |
tenantFromContext is the reader you pass straight to resolveTenant;
globalRolesFromContext is the one superAdminRoles and the effective-role
union consult. Reach for readContextAccessor / AGORA_CONTEXT_ACCESSOR only
when you are writing something that publishes or replaces a context — a test
harness, for example.
Event-driven provisioning
defineAuthzProvisioning subscribes to authkit's diagnostics events (via the
optional @adonis-agora/diagnostics peer) and runs config-mapped actions
against the store. It is best-effort — actions never throw into the host, and a
missing diagnostics package is a no-op.
import { defineAuthzProvisioning } from '@adonis-agora/authz/provisioning'
const authz = await app.container.make(AuthzService)
await defineAuthzProvisioning({
store: authz.store,
on: {
'organization.created': (ev, store) => {
if (!ev.accountId || !ev.orgId) return
return store.assignRole({ type: 'user', id: ev.accountId }, 'org:owner', {
tenantId: ev.orgId,
})
},
'member.added': (ev, store) => {
if (!ev.accountId || !ev.orgId) return
return store.assignRole({ type: 'user', id: ev.accountId }, 'org:member', {
tenantId: ev.orgId,
})
},
},
})Keys are the bare authkit event type (subscribed on channels
agora:authkit:<type>). The call returns a handle with .stop() to
unsubscribe.
Read the ids from the top level of the event, not from metadata. authkit
redacts its audit event before publishing it on the diagnostics bus: email,
ip and the free-form metadata are stripped, and what reaches a subscriber is
the event type plus the opaque accountId, actorId, clientId and orgId.
An action written against ev.metadata.orgId reads undefined in production.
metadata survives only when the host feeds the bus itself, or wires
defineAuthzProvisioning to authkit's un-redacted events.onEvent.
The /authz/can endpoint
registerCanEndpoint registers a route that answers a permission check for the
active user. Optional peer: @adonisjs/core (router types — already a peer).
import router from '@adonisjs/core/services/router'
import { registerCanEndpoint } from '@adonis-agora/authz/http'
const authz = await app.container.make(AuthzService)
registerCanEndpoint(router, { service: authz }) // default path: /authz/canContract (the frontend depends on this exactly):
- Request JSON:
{ "permission": string, "resource"?: string } - Response JSON:
{ "allowed": boolean }
An anonymous/unmappable user or a missing permission yields
{ "allowed": false }.
Override the path and user resolution as needed:
registerCanEndpoint(router, {
service: authz,
path: '/api/authz/can',
resolveUser: (ctx) => ctx.auth?.user,
})Route middleware
AuthzRoleMiddleware — an any-of requireRole route guard on the user's effective roles (global ∪ app ∪ store), with guest and denied redirects.
React / Inertia
Client-side authorization for Inertia + React — AuthzProvider, useAuthz, useCan, the <Can> gating component, and the server-side buildAuthzShare() that pushes effective grants to the frontend.