PolicykitDocs

API reference

Every export of @daanvandenbergh/policykit: the Policy class, the cross-policy functions, the types, the errors, and the React components.

7 min readUpdated 11 August 2026
API reference

The complete public surface of @daanvandenbergh/policykit. For how the pieces fit together, read how it works.

Entry points

ImportContains
@daanvandenbergh/policykitPolicy, noticeQueue, pendingNotice, requiredConsentRevision, assertValidAll, DEFAULT_NOTICE_HORIZON_DAYS, every public type, PolicyValidationError
@daanvandenbergh/policykit/reactPolicyDocument, PolicyDocumentProps, PolicyBanner, PolicyBannerProps
@daanvandenbergh/policykit/package.jsonThe package manifest, for version lookups and tooling

The root entry's module graph imports only node:fs, node:path, and gray-matter - never react, react-dom, next, next-mdx-remote, or server-only - so a server-side package under a no-react architecture rule can import it safely. A test asserts this over the whole graph, on every import, re-export, and dynamic import() form.

Policy

new Policy(config: PolicyConfig)

Construction does zero filesystem access. It validates only what needs none: dir must be absolute, and defaultLocale must be one of locales. Everything else fails at first use.

PolicyConfig

OptionTypeRequiredDefault
slugstringyes-
dirstring (absolute)yes-
localesreadonly string[]yes-
defaultLocalestringno"en"

Throws PolicyValidationError on a relative dir, or a defaultLocale outside locales.

Fields

FieldTypeDescription
slugstringThe policy's identifier, as configured.
localesreadonly string[]The configured locales, copied.

Methods

Every method walks and validates the whole directory, so every one can throw PolicyValidationError. The walk is never memoized; only the per-file parse is cached.

MethodReturnsThrows
revisions()readonly PolicyRevision[] - all revisions, oldest firstPolicyValidationError
latest()PolicyRevision - the newest revision by revision date, effective or notPolicyValidationError
effective(now: Date)PolicyRevision | undefined - the newest revision whose effectiveFrom is on or before now's UTC day; undefined before the first onePolicyValidationError
pending(now: Date)PolicyRevision | undefined - the next revision that will actually bind, superseded ones skipped; undefined when nothing is pendingPolicyValidationError
revision(revision: string)PolicyRevision | undefined - exact date-string lookup; a miss is undefined, never a throwPolicyValidationError
content(revision: string, locale: string)PolicyContent | undefined - the MDX body and that locale file's own titlePolicyValidationError
has(revision: string, locale: string)boolean - true only when the revision exists and carries that localePolicyValidationError
owedNotices(options: { now: Date; horizonDays?: number })readonly PolicyRevision[] - this policy's owed notices, ascendingPolicyValidationError, TypeError
assertValid()voidPolicyValidationError

pending() ignores the notice tier, so it can return a notice: "none" revision. For a banner, use pendingNotice instead - see notices and consent.

Functions

const DEFAULT_NOTICE_HORIZON_DAYS = 60;

How far back noticeQueue and Policy.owedNotices look when the caller names no horizon.

noticeQueue(
    policies: readonly Policy[],
    options: { now: Date; horizonDays?: number },
): readonly { policy: Policy; revision: PolicyRevision }[]

Every revision across policies that owes notice (notice !== "none"), will bind or already did, and whose effectiveFrom is within horizonDays before now or still in the future. Ordered by policy, then revision ascending. Throws PolicyValidationError, and TypeError on a non-finite or negative horizonDays.

pendingNotice(
    policies: readonly Policy[],
    now: Date,
): { policy: Policy; revision: PolicyRevision } | undefined

The one published, not-yet-effective, notice-owing revision taking effect soonest across policies, skipping revisions that are superseded and so will never bind; undefined when there is none. On an effectiveFrom tie the first policy in the given order wins. Throws PolicyValidationError.

requiredConsentRevision(policies: readonly Policy[], now: Date): string

The revision string a user must have accepted to count as consented across policies: the maximum effective, still-binding revision that is either the policy's baseline - its first revision that ever binds, i.e. the first non-superseded one - or carries notice: "reconsent". Superseded revisions never move the gate. Returns "" when nothing is effective yet. Compare a stored acceptance with lexicographic >=. Throws PolicyValidationError.

assertValidAll(policies: readonly Policy[]): void

Runs every policy's assertValid() and rejects duplicate slugs across the set. Throws PolicyValidationError. See validation.

Types

type PolicyNotice = "none" | "notify" | "reconsent";
TierMeaning
"none"Non-material, or a change forced by law or security that owes no notice.
"notify"Users must be told, but existing consent stands; it never re-prompts anyone.
"reconsent"Users must expressly accept again before the revision binds them.
interface PolicyConfig {
    slug: string;
    dir: string;
    locales: readonly string[];
    defaultLocale?: string;
}
interface PolicyRevision {
    readonly revision: string;
    readonly effectiveFrom: string;
    readonly notice: PolicyNotice;
    readonly changeSummary: string;
    readonly locales: readonly string[];
    readonly title?: string;
}
interface PolicyContent {
    readonly source: string;
    readonly title?: string;
}
interface PolicyValidationErrorDetails {
    slug: string;
    file?: string;
    field?: string;
}

The title asymmetry is deliberate: PolicyRevision.title is the default-locale file's title (it describes the revision as a whole), while PolicyContent.title is the requested locale file's own title with no fallback - so a page never shows an English heading above Dutch text.

Errors

ThrownWhen
PolicyValidationErrorThe constructor rejects the config, or any accessor finds a layout-grammar or frontmatter breach. Carries slug, optional file (relative to the policy directory), and optional field, alongside the message.
TypeErrorhorizonDays passed to noticeQueue or Policy.owedNotices is not a finite, non-negative number.
ErrorPolicyDocument resolved a (revision, locale) pair with no content. A plain Error, not a validation error: the corpus is valid, the ask was wrong.
class PolicyValidationError extends Error {
    readonly slug: string;
    readonly file?: string;
    readonly field?: string;
    constructor(message: string, details: PolicyValidationErrorDetails);
}

name is always "PolicyValidationError" - read it when an instanceof check cannot cross a bundle boundary. The error re-throws on every call, because the walk is never cached, so a bad file fails next build and assertValid() repeatably.

React (@daanvandenbergh/policykit/react)

PolicyDocument

function PolicyDocument(props: PolicyDocumentProps): Promise<JSX.Element>

Async server component rendering one revision's MDX body through next-mdx-remote, with remark-gfm always on. It renders the body only - no title, no styling. It reads the filesystem, so render it on the server, never inside a "use client" tree.

PropTypeRequiredDefault
policyPolicyyes-
localestringyes-
revisionstringno(policy.effective(new Date()) ?? policy.latest()).revision
componentsMDXRemoteProps["components"]noundefined

Those four props are the exported PolicyDocumentProps interface.

Throws a plain Error when the resolved pair has no content. Guard routes with policy.has() and 404 first - see rendering policies.

PolicyBanner

function PolicyBanner(props: PolicyBannerProps): ReactNode

Headless server component: it resolves pendingNotice and calls children with the result, or renders null when nothing pending owes notice. It reads the filesystem, so it is server-only too.

PropTypeRequiredDefault
policiesreadonly Policy[]yes-
nowDatenonew Date()
children(announcement: { policy: Policy; revision: PolicyRevision }) => ReactNodeyes-

Those three props are the exported PolicyBannerProps interface. Throws PolicyValidationError when any policy's directory is invalid.

Dependencies

Installed unconditionally with the package:

PackageRangeUsed by
gray-matter^4.0.3The root entry, to parse frontmatter
remark-gfm^4.0.1The /react subpath, so pipe tables render

Peer dependencies

PackageRange
next>=14
next-mdx-remote^5 || ^6
react>=18
react-dom>=18

All four are optional, and none is required by the root entry - only the /react subpath needs them.

Was this page helpful?