Skip to content

Async & Custom Functions

Declarative conditions are synchronous. For business logic that needs I/O (a DB lookup, an external service), register a custom function and reference it from a grant or gate as { fn, args } — the reference stays JSON‑serializable (the function lives in code).

ac.defineCondition('ipAllowed', async (ctx, args) =>
isAllowed(ctx.ip, args.cidr)
);
ac.grant('admin')
.where({ fn: 'ipAllowed', args: { cidr: '10.0.0.0/8' } })
.readAny('server');

A check that touches a custom function must use the async accessors:

const ok = await ac.can('admin', { ip })
.readAny('server').grantedAsync;
// one-shot form
const perm = await ac.checkAsync({
role: 'admin', resource: 'server', action: 'read:any', context: { ip }
});
perm.granted;

Only { fn } conditions require the async path. Purely declarative grants/gates resolve synchronously even if you happen to await them, so you can use the async accessors uniformly if you prefer.

ac.grant('user').where('$.order.value <= 100').updateAny('order');
await ac.can('user', { order: { value: 50 } })
.updateAny('order').grantedAsync; // works (declarative, but awaitable)

Resolving a permission once memoizes its result: a sync resolve followed by an await won’t re‑resolve (and won’t emit a second access event).