Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,19 @@ jobs:
- run: pnpm install --frozen-lockfile
- run: pnpm test:coverage
- run: pnpm test:types

# Node 22 only: the docs build exercises VitePress, not the package runtime,
# so the 18/20/22 matrix would just repeat the same work three times.
docs:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- run: pnpm install --frozen-lockfile
# A broken docs build (including dead links) should fail the PR, not the deploy.
- run: pnpm run docs:build
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -119,3 +119,8 @@ dist

.cursor/plans
.pnpm-store

# VitePress (the bare `dist` / `.cache` rules above do not match these paths)
docs/.vitepress/dist
docs/.vitepress/cache
/.claude/launch.json
17 changes: 15 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,10 @@ pnpm test:coverage # c8 coverage over src/
pnpm lint # oxlint src test
pnpm format # oxfmt src test (format:check for CI)
pnpm bench # ops/sec benchmark harness (bench/bench.js)
pnpm docs:dev # VitePress dev server for docs/ (docs:build / docs:preview too)
```

Linting/formatting is **oxlint/oxfmt** (`.oxlintrc.json`, `.oxfmtrc.json`; the `correctness` category is intentionally off) — their native bindings require Node ≥20.19, so CI (`.github/workflows/ci.yml`) runs `lint`/`format:check` in a single job pinned to Node 22, separate from the `test` job, which runs `test:coverage` + `test:types` across the Node 18/20/22 matrix.
Linting/formatting is **oxlint/oxfmt** (`.oxlintrc.json`, `.oxfmtrc.json`; the `correctness` category is intentionally off) — their native bindings require Node ≥20.19, so CI (`.github/workflows/ci.yml`) runs `lint`/`format:check` in a single job pinned to Node 22, separate from the `test` job, which runs `test:coverage` + `test:types` across the Node 18/20/22 matrix, and a `docs` job (Node 22) that runs `docs:build`. Lint/format deliberately target `src test scripts bench` only, so `docs/` is not covered by them.

Style: 2-space indent, single quotes, semicolons, 120-char lines (see `.editorconfig`, `.oxfmtrc.json`).

Expand Down Expand Up @@ -92,7 +93,19 @@ Two layers, both SpiceDB-inspired (see the "borrow vs skip" notes in `README.md`

### Public exports

The full package surface is assembled in `src/index.js` (main entry), `tests.js` (dev-only `/tests` subpath) and `relations.js` (`/relations` subpath) — check all three when adding a new export, and update `index.d.ts` / `tests.d.ts` / `relations.d.ts` in the repo root accordingly, since types are hand-maintained (not generated).
The full package surface is assembled in `src/index.js` (main entry), `tests.js` (dev-only `/tests` subpath) and `relations.js` (`/relations` subpath) — check all three when adding a new export, and update `index.d.ts` / `tests.d.ts` / `relations.d.ts` in the repo root accordingly, since types are hand-maintained (not generated). Also update `docs/api/exports.md`, which mirrors those three tables for the docs site.

### Documentation site (`docs/`)

VitePress site deployed to Vercel (`vercel.json` at the repo root pins the build command and output dir); `docs/` is never published to npm — the `files` field in `package.json` is an explicit list that omits it. Structure follows the migronaut sibling repo: a single `docs/.vitepress/config.mts`, a `theme/` that only extends `DefaultTheme` with a `custom.css` of brand CSS variables (Cerbos-style amber `#FFC11E` on ink `#1B1C1E`), local MiniSearch, and `docs/public/` for `robots.txt` / `llms.txt` / logo assets.

Conventions and gotchas:

- Only `docs/index.md` carries frontmatter (`layout: home`); every other page starts directly with its `# H1`. Links between pages are absolute and extensionless (`/guide/scopes#…`) to match `cleanUrls: true`.
- `README.md` deliberately keeps the full narrative (npm renders it) — the docs pages duplicate it. When you change a documented behavior, update both.
- `ignoreDeadLinks` is intentionally off: `pnpm docs:build` failing on a dead link is the check that cross-page anchors are still valid.
- Mermaid comes from `vitepress-plugin-mermaid`. Its transitive deps (`@braintree/sanitize-url`, `dayjs`, `debug`, `cytoscape`, `cytoscape-cose-bilkent`) are direct devDependencies **because** the plugin hardcodes them into `optimizeDeps.include` and pnpm's strict linking otherwise leaves them unresolvable in `docs:dev`. Diagram labels also need the `line-height` override at the bottom of `custom.css` — mermaid sizes nodes without knowing VitePress' global line-height, so multi-line labels get clipped without it.
- The version in the nav dropdown (`v3.1.0`) and the `hostname` constant are hand-synced — bump the former with `package.json` at release time.

### Why the package doesn't ship a separate ESM build

Expand Down
199 changes: 199 additions & 0 deletions docs/.vitepress/config.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
import { defineConfig } from 'vitepress';
import { withMermaid } from 'vitepress-plugin-mermaid';

const ogTitle = 'Kerberos.js — embedded authorization engine for Node.js & the browser';
const ogDescription =
'Zero-dependency, in-process authorization engine for JavaScript. Cerbos-style RBAC + ABAC policies, ' +
'Zanzibar-inspired ReBAC relations and Cerbos-compatible query plans — no server to deploy, ~25 KB min+gzip.';
const repo = 'https://github.com/Alexis-Technologies/kerberos';
const base = '/';
const hostname = 'https://kerberosjs.vercel.app/';
const ogImage = `${hostname}logo.png`;

// Mirrors package.json "keywords" — kept in one line-per-term list so the two stay easy to diff.
const keywords = [
'authorization',
'authorization engine',
'access control',
'fine-grained authorization',
'permissions',
'policy-as-code',
'rbac',
'abac',
'rebac',
'zanzibar',
'spicedb',
'openfga',
'cerbos',
'cerbos alternative',
'derived roles',
'query plan',
'in-process authorization',
'zero dependency authorization',
'nodejs authorization',
'browser authorization',
'opentelemetry',
'kerberos.js',
'@alexify/kerberos',
].join(', ');

// schema.org structured data — helps search and AI engines understand the package
// as a software entity, not just text on a page.
const jsonLd = {
'@context': 'https://schema.org',
'@type': 'SoftwareApplication',
name: '@alexify/kerberos',
alternateName: 'Kerberos.js',
description: ogDescription,
applicationCategory: 'DeveloperApplication',
operatingSystem: 'Node.js >= 18, modern browsers',
url: hostname,
downloadUrl: 'https://www.npmjs.com/package/@alexify/kerberos',
codeRepository: repo,
license: 'https://opensource.org/licenses/MIT',
keywords,
author: { '@type': 'Organization', name: 'Alexis Technologies' },
offers: { '@type': 'Offer', price: '0', priceCurrency: 'USD' },
};

// https://vitepress.dev/reference/site-config
export default withMermaid(
defineConfig({
title: '@alexify/kerberos',
titleTemplate: ':title — Kerberos.js',
description: ogDescription,
lang: 'en-US',
base,
cleanUrls: true,
lastUpdated: true,
sitemap: { hostname },

head: [
['link', { rel: 'icon', type: 'image/svg+xml', href: `${base}logo-mark.svg` }],
['link', { rel: 'icon', type: 'image/png', href: `${base}favicon.png` }],
['meta', { name: 'theme-color', content: '#FFC11E' }],
['meta', { name: 'author', content: 'Alexis Technologies' }],
['meta', { name: 'keywords', content: keywords }],
['meta', { name: 'robots', content: 'index, follow' }],
['meta', { property: 'og:type', content: 'website' }],
['meta', { property: 'og:site_name', content: '@alexify/kerberos' }],
['meta', { property: 'og:title', content: ogTitle }],
['meta', { property: 'og:description', content: ogDescription }],
['meta', { property: 'og:image', content: ogImage }],
['meta', { name: 'twitter:card', content: 'summary_large_image' }],
['meta', { name: 'twitter:title', content: ogTitle }],
['meta', { name: 'twitter:description', content: ogDescription }],
['meta', { name: 'twitter:image', content: ogImage }],
['script', { type: 'application/ld+json' }, JSON.stringify(jsonLd)],
],

// Per-page canonical + og:url for clean SEO indexing
transformPageData(pageData) {
const path = pageData.relativePath.replace(/index\.md$/, '').replace(/\.md$/, '');
const canonical = `${hostname}${path}`;
pageData.frontmatter.head ??= [];
pageData.frontmatter.head.push(
['link', { rel: 'canonical', href: canonical }],
['meta', { property: 'og:url', content: canonical }],
);
},

themeConfig: {
logo: '/logo-mark.svg',

// ─── Top navigation ──────────────────────────────────────────────
nav: [
{ text: 'Guide', link: '/guide/why', activeMatch: '/guide/' },
{ text: 'API', link: '/api/kerberos', activeMatch: '/api/' },
{ text: 'Reference', link: '/reference/plan-operators', activeMatch: '/reference/' },
{
// Hand-synced with package.json "version" — part of the release checklist.
text: 'v3.1.0',
items: [
{ text: 'Changelog', link: `${repo}/blob/main/CHANGELOG.md` },
{ text: 'npm', link: 'https://www.npmjs.com/package/@alexify/kerberos' },
{ text: 'Releases', link: `${repo}/releases` },
],
},
],

// ─── Sidebar ─────────────────────────────────────────────────────
sidebar: {
'/guide/': [
{
text: 'Introduction',
items: [
{ text: 'Why Kerberos.js?', link: '/guide/why' },
{ text: 'Installation', link: '/guide/installation' },
{ text: 'Quick Start', link: '/guide/getting-started' },
{ text: 'Policy Types', link: '/guide/policy-types' },
{ text: 'Scopes & Versions', link: '/guide/scopes' },
],
},
{
text: 'Core features',
items: [
{ text: 'Configuration', link: '/guide/configuration' },
{ text: 'Outputs', link: '/guide/outputs' },
{ text: 'Decision metadata', link: '/guide/decision-metadata' },
{ text: 'Schema validation', link: '/guide/schema-validation' },
{ text: 'Testing', link: '/guide/testing' },
],
},
{
text: 'Advanced',
items: [
{ text: 'Caching & dynamic policies', link: '/guide/caching' },
{ text: 'Serialization & security', link: '/guide/serialization' },
{ text: 'ReBAC (Relations)', link: '/guide/rebac' },
{ text: 'Built-in resolver', link: '/guide/relations-resolver' },
{ text: 'Query plans', link: '/guide/query-plans' },
{ text: 'OpenTelemetry', link: '/guide/telemetry' },
{ text: 'Benchmarks', link: '/guide/benchmarks' },
],
},
],
'/api/': [
{
text: 'API Reference',
items: [
{ text: 'Kerberos class', link: '/api/kerberos' },
{ text: 'Errors', link: '/api/errors' },
{ text: 'Exports', link: '/api/exports' },
],
},
],
'/reference/': [
{
text: 'Reference',
items: [
{ text: 'Plan operators', link: '/reference/plan-operators' },
{ text: 'Safe builtins', link: '/reference/safe-builtins' },
{ text: 'Security', link: '/reference/security' },
],
},
],
},

// ─── Local, zero-config full-text search ─────────────────────────
search: { provider: 'local' },

socialLinks: [{ icon: 'github', link: repo }],

editLink: {
pattern: `${repo}/edit/main/docs/:path`,
text: 'Edit this page on GitHub',
},

footer: {
message: 'Released under the MIT License.',
copyright: 'Copyright © 2026 Alexis Technologies',
},

docFooter: {
prev: 'Previous page',
next: 'Next page',
},
},
}),
);
135 changes: 135 additions & 0 deletions docs/.vitepress/theme/custom.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
/**
* Brand theme for Kerberos.js — same approach Pinia/Vite/Vue use:
* override VitePress CSS variables to restyle the default theme.
* Palette: Cerbos amber (#FFC11E / #FFCD4B / #FFE08F) on ink (#1B1C1E).
*
* Note on contrast: pure #FFC11E fails WCAG AA as link text on white, so in the
* light theme brand-1/-2 are darkened amber (text/links) and the pure brand
* yellow is reserved for filled surfaces, where the text on top is ink.
*/

:root {
/* ─── Brand colors (buttons, links, accents) ──────────────────────── */
--vp-c-brand-1: #8a6100;
--vp-c-brand-2: #b37e00;
--vp-c-brand-3: #ffc11e;
--vp-c-brand-soft: rgba(255, 193, 30, 0.16);

/* Default theme alias mappings (kept in sync with brand) */
--vp-c-default-1: var(--vp-c-gray-1);
--vp-c-default-2: var(--vp-c-gray-2);
--vp-c-default-3: var(--vp-c-gray-3);
--vp-c-default-soft: var(--vp-c-gray-soft);

--vp-c-tip-1: var(--vp-c-brand-1);
--vp-c-tip-2: var(--vp-c-brand-2);
--vp-c-tip-3: var(--vp-c-brand-3);
--vp-c-tip-soft: var(--vp-c-brand-soft);
}

.dark {
--vp-c-brand-1: #ffc11e;
--vp-c-brand-2: #ffcd4b;
--vp-c-brand-3: #b37e00;
--vp-c-brand-soft: rgba(255, 193, 30, 0.18);
}

/* ─── Buttons: ink on amber, the way Cerbos does its CTAs ──────────── */
:root {
--vp-button-brand-bg: #ffc11e;
--vp-button-brand-hover-bg: #ffcd4b;
--vp-button-brand-active-bg: #b37e00;
--vp-button-brand-border: transparent;
--vp-button-brand-hover-border: transparent;
--vp-button-brand-active-border: transparent;
--vp-button-brand-text: #1b1c1e;
--vp-button-brand-hover-text: #1b1c1e;
--vp-button-brand-active-text: #1b1c1e;
}

/* ─── Home hero: gradient title + glowing logo blob (the "Pinia look") ─ */
:root {
--vp-home-hero-name-color: transparent;
--vp-home-hero-name-background: -webkit-linear-gradient(
120deg,
#ffc11e 25%,
#ffe08f
);

/* Deeper than the mark itself: the logo is #FFC11E, so an equally bright halo
would swallow it. Bronze keeps the mark reading as the brightest thing. */
--vp-home-hero-image-background-image: linear-gradient(
-45deg,
#b37e00 40%,
#6b4b00 100%
);
--vp-home-hero-image-filter: blur(48px);
}

@media (min-width: 640px) {
:root {
--vp-home-hero-image-filter: blur(72px);
}
}

@media (min-width: 960px) {
:root {
--vp-home-hero-image-filter: blur(96px);
}
}

/* ─── Code block inline highlight accent ──────────────────────────────── */
:root {
--vp-code-block-bg: var(--vp-c-bg-alt);
}

/* Slightly larger, friendlier hero on wide screens */
.VPHero .name {
letter-spacing: -0.02em;
}

/* ─── Home feature cards: glow on hover (clickable, no layout shift) ───── */
.VPFeatures .VPFeature {
transition:
border-color 0.25s ease,
background-color 0.25s ease,
box-shadow 0.25s ease;
}

.VPFeatures .VPFeature.link:hover {
border-color: var(--vp-c-brand-3);
box-shadow: 0 8px 24px rgba(179, 126, 0, 0.16);
}

.dark .VPFeatures .VPFeature.link:hover {
box-shadow: 0 8px 24px rgba(255, 193, 30, 0.14);
}

/* Make the "learn more" link adopt the brand color on card hover */
.VPFeatures .VPFeature.link:hover .link-text-value {
color: var(--vp-c-brand-1);
}

/* ─── Mermaid diagrams: centre them and let wide graphs scroll ─────────── */
.mermaid {
display: flex;
justify-content: center;
overflow-x: auto;
margin: 20px 0;
}

/**
* Mermaid sizes a node from its own measurement pass, which does not know about
* VitePress' global line-height (1.5). Multi-line labels then overflow the box
* they were fitted to. Pinning the label line-height back to mermaid's own
* assumption keeps the text inside the shape.
*/
.mermaid .nodeLabel,
.mermaid .edgeLabel,
.mermaid .cluster-label,
.mermaid foreignObject div,
.mermaid foreignObject span,
.mermaid foreignObject p {
line-height: 1.25;
margin: 0;
}
Loading
Loading