Skip to main content

Examples (JavaScript)

Schema-only check

No conditions needed — this checks whether the action/subject pair is allowed at all, ignoring any specific instance:

policy.can(Actions.Create, Subjects.Article);

Condition-based check

const article = Subjects.Article.wrap({ id: 1, owner_id: userId, status: "published" });
policy.can(Actions.Update, article);

Multiple conditions

new PolicyBuilder()
.allow(Actions.Update, Subjects.Article, {
$and: [
{ owner_id: userId },
{ status: { $not: "archived" } },
],
})
.buildDef();

Field mappers for renamed or computed fields

A SubjectFieldMapper resolves a condition field through an explicit getter instead of property access — handy when a policy-facing field name doesn't match the instance's own shape, like flattening a nested instance.author.name into a single top-level authorName field (recall conditions can only narrow one field deep — see Condition Operators):

interface Post { status: string; author: { name: string } }

const read = createAction("Read");
const post = createSubject<Post>("Post", {
authorName: (instance) => instance.author.name,
});

const policy = Policy.from({
version: "1.0",
rules: [["allow", "Read", "Post", { authorName: "Alice" }]],
});

policy.can(read, post.wrap({ status: "draft", author: { name: "Alice" } })); // true

A field the mapper doesn't define still falls back to ordinary property access, and a mapped field's own value can still use any non-field operator ($substr, $in, ...) — only narrowing is restricted to one level, not operator use.

KeycardConfig

Bundle actions/subjects/operators/field mappers into one object shared by PolicyBuilder and Policy, instead of keeping each in sync by hand:

const post = createSubject<Post>("Post");
const read = createAction("Read");

const mappers = new SubjectFieldMapperCatalog({
Post: { authorName: (instance: Post) => instance.author.name },
});

const config = { subjects: [post], mapper: mappers };

// config.subjects widens the meta.subjects catalog, so "Post" is accepted
// here even though this raw definition declares no meta.subjects of its
// own (see Policy Definition's `meta` section for catalog enforcement):
const policy = Policy.from(
{ version: "1.0", rules: [["allow", "Read", "Post", { authorName: "Alice" }]] },
{},
config,
);
note

PolicyBuilder.allow()'s conditions parameter is typed against the subject's real fields (Condition<Post> here) — a field that only exists through a SubjectFieldMapper, like authorName above, has no static type, so a mapped-only field can't be referenced through .allow()'s typed API. Build that rule as a plain object instead, as shown above.

Custom error handling

try {
policy.require(Actions.Delete, article);
} catch (err) {
console.warn("Access denied:", err.message);
sendError(403, "You do not have permission to delete this article");
}

Cross-language usage

PolicyDefinitions are plain JSON, so a policy built in one language can be evaluated in another:

// Build & serialize in JavaScript
const json = JSON.stringify(policy.def());

// ...ship `json` to a Java service, a Rust service, a browser, etc.
// It can be loaded with any conformant KeyCard implementation.

See src/example.ts in the impl/js package for a complete working example.