@authomata/core
v0.0.0
Published
Async, configuration-driven authorization for Authomata.
Readme
@authomata/core
@authomata/core is an asynchronous, storage-agnostic authorization library. For most applications, create a ConfigAuthorizer from a JSON, TOML, or YAML policy document. It parses the document once and uses Map indexes for authorization lookups. Extend Engine only when policies must be loaded from a database, cache, or API.
Installation
npm install @authomata/coreGuides
- Getting Started: first authorization walkthrough
- Concepts: evaluation order and policy design
- Core: policy documents, precedence, groups, and JEL
- Custom Engines: asynchronous storage adapters and batch hooks
- Redis: Redis-backed policy stores
Quick Start
createAuthorizer() is the standard in-memory implementation. Policy decisions are strings in configuration documents; the API returns Decision values.
import { createAuthorizer } from "@authomata/core";
const authorizer = createAuthorizer(`
defaultDecision = "deny"
[users.alice]
groups = ["editor"]
[users.alice.decisions]
"document.read" = "allow"
[users.bob]
groups = ["editor", "reviewer"]
[users.bob.decisions]
"document.write" = "deny"
[groups.editor]
priority = 100
[groups.editor.decisions]
"document.write" = "allow"
[groups.reviewer]
priority = 200
[groups.reviewer.decisions]
"document.read" = "allow"
`, { format: "toml" });
const [aliceCanWrite, bobCanRead, bobCanWrite] = await Promise.all([
authorizer.isAllowed({
user: "alice",
action: "document.write",
context: {},
}),
authorizer.isAllowed({
user: "bob",
action: "document.read",
context: {},
}),
authorizer.isAllowed({
user: "bob",
action: "document.write",
context: {},
}),
]);aliceCanWrite and bobCanRead are true. bobCanWrite is false because the user-specific decision takes precedence over the Editor group's decision. authorize() and isAllowed() are always asynchronous; isAllowed() resolves to true only when the final decision is Decision.ALLOW.
Policy Documents
Pass an object directly, or pass a string with an explicit format of "json", "toml", or "yaml".
const authorizer = createAuthorizer(
`
defaultDecision: deny
users:
user-123:
decisions:
document.read: allow
`,
{ format: "yaml" },
);The following YAML shows the complete document shape. The same fields are available in JSON and TOML.
defaultDecision: deny
contextRules:
- action: document.read
condition: [eq, [get, request.ip], 203.0.113.10]
decision: deny
users:
alice:
groups: [editor]
decisions:
document.read: allow
bob:
groups: [editor, reviewer]
decisions:
document.write: deny
groups:
editor:
priority: 100
decisions:
document.write: allow
reviewer:
priority: 200
decisions:
document.read: allow| Field | Description |
| --- | --- |
| defaultDecision | Optional fallback: allow, deny, or none. none ultimately denies the request. |
| contextRules | Optional rules with a JEL condition, decision, and optional action. Omit action to apply a rule to every action. |
| users | A map of user identifier to groups and an optional action-to-decision map. |
| groups | A map of group identifier to priority and an action-to-decision map. |
ConfigAuthorizer creates Map indexes by action, user, and group ID. It resolves each user's group IDs when loading the document and does not mutate the supplied configuration. Call replaceConfig() to rebuild the complete cache from a new document.
Decisions and Evaluation Order
Every policy produces one of three values.
| Decision | Meaning |
| --- | --- |
| Decision.ALLOW | Permit the request |
| Decision.DENY | Reject the request |
| Decision.NONE | Make no decision at this stage |
authorize() evaluates stages in the following order. Once a stage resolves to ALLOW or DENY, later stages are not evaluated.
- Context rules whose JEL conditions match
- A user-specific decision
- Group decisions in priority order
- The default decision
DENYwhen all preceding stages returnNONEorundefined
Group priority is an integer from 0 through 65535; 0 is highest. 0..63 and 65472..65535 are reserved by convention, but remain valid.
Context Rules and JEL
The condition of a context rule is a JEL Bool and only matches when it evaluates to true. Include every value referenced by the expression in request.context.
const result = await authorizer.authorize({
user: "user-123",
action: "document.read",
context: {
request: { ip: "203.0.113.10" },
},
});JEL supports property access (get), logical operators, comparisons, collection checks, conditional expressions, and wildcard path segments. Validate or evaluate an expression independently with JEL.returnsBoolean() and JEL.evaluate().
Merging Same-Priority Decisions
Matching context rules and group decisions at the same priority are combined by a merge function.
| Function | Result |
| --- | --- |
| Merge.hasAllow | Returns ALLOW if any input is ALLOW. This is the default. |
| Merge.hasDeny | Returns DENY if any input is DENY. |
| Merge.majority | Returns the majority of ALLOW and DENY; ties return NONE. |
Provide a custom synchronous or asynchronous merge function through createAuthorizer or an Engine constructor.
const authorizer = createAuthorizer(policy, {
merge: async (decisions) =>
decisions.includes(Decision.DENY)
? Decision.DENY
: Decision.NONE,
});Custom Asynchronous Storage
Extend Engine when policies do not fit the configuration document. Each retrieval hook accepts either an immediate value or a promise, while the public evaluation API always returns a promise.
Backends that can reduce round trips may override getUserAndDefaultDecisions(request), getGroupPriorities(groups, request), and getGroupDecisions(groups, request). The user/default hook may return both scopes together; its default returns undefined, preserving lazy default evaluation for existing engines. The group hooks receive a collection of groups and return a ReadonlyMap keyed by those group instances. Their default implementations call the corresponding single-scope hook concurrently. Decisions are fetched one priority band at a time to preserve short-circuit evaluation. For one-request storage snapshots, override createAuthorizationState(request); its result is passed as the optional final argument to every retrieval hook.
import {
Decision,
Engine,
type AuthorizationRequest,
type ContextRule,
} from "@authomata/core";
type User = { id: string };
type Group = { id: string; priority: number };
type Action = "document.read" | "document.write";
type RequestContext = Record<string, unknown>;
class ApplicationEngine extends Engine<User, Group, Action, RequestContext> {
protected async getContextRules(
_request: AuthorizationRequest<User, Action, RequestContext>,
): Promise<Iterable<ContextRule>> {
return [];
}
protected async getUserDecision(
request: AuthorizationRequest<User, Action, RequestContext>,
): Promise<Decision | undefined> {
return policyStore.findUserDecision(request.user.id, request.action);
}
protected async getGroups(
request: AuthorizationRequest<User, Action, RequestContext>,
): Promise<Iterable<Group>> {
return groupStore.findByUser(request.user.id);
}
protected getGroupPriority(
group: Group,
_request: AuthorizationRequest<User, Action, RequestContext>,
): number {
return group.priority;
}
protected async getGroupDecision(
group: Group,
request: AuthorizationRequest<User, Action, RequestContext>,
): Promise<Decision | undefined> {
return policyStore.findGroupDecision(group.id, request.action);
}
protected getDefaultDecision(
_request: AuthorizationRequest<User, Action, RequestContext>,
): Decision {
return Decision.DENY;
}
}
const decision = await new ApplicationEngine().authorize({
user: { id: "user-123" },
action: "document.read",
context: {},
});API
| Export | Purpose |
| --- | --- |
| createAuthorizer / ConfigAuthorizer | Default JSON, TOML, YAML, and Map-cached authorizer |
| parseAuthorizationConfig | Parse a JSON, TOML, or YAML policy string without filesystem access |
| Engine | Abstract asynchronous engine for custom storage adapters |
| Decision | The ALLOW, DENY, and NONE values |
| Merge / MergeFunction | Synchronous or asynchronous functions for same-priority decisions |
| JEL / Bool | Context-condition validation and evaluation |
| AuthorizationConfig | Typed shape for the policy document |
| GROUP_PRIORITY_* | Constants for valid and reserved group-priority ranges |
