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
5 changes: 5 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ on:
push:
branches: [main]
workflow_dispatch:
# Docs are fetched from the product repos' HEADs at build time, so a
# scheduled run is the only thing that picks up doc changes that land
# there without a corresponding push to this repo.
schedule:
- cron: '17 9 * * *'

permissions:
contents: read
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
dist/
# generated types
.astro/
# Lighthouse CI report output
.lighthouseci/

# dependencies
node_modules/
Expand Down
24 changes: 13 additions & 11 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co

## Project Overview

Static documentation site for Major Context products, built with Astro. Currently hosts documentation for [Moat](https://github.com/majorcontext/moat), with architecture designed to support multiple products.
Static documentation site for Major Context products, built with Astro. Hosts documentation for three products: [Moat](https://github.com/majorcontext/moat), [Keep](https://github.com/majorcontext/keep), and [Gatekeeper](https://github.com/majorcontext/gatekeeper).

**Key characteristic**: Documentation content is fetched from source repositories at build time, not stored in this repo.

Expand All @@ -38,6 +38,8 @@ bun run test:lighthouse # Run Lighthouse performance tests
```bash
bun run fetch:docs # Fetch all product documentation from GitHub
bun run fetch:moat # Fetch only Moat documentation
bun run fetch:keep # Fetch only Keep documentation
bun run fetch:gatekeeper # Fetch only Gatekeeper documentation
bun run check:assets # Verify required assets exist (logo.svg, favicons)
```

Expand All @@ -55,7 +57,7 @@ bun run validate:links # Check for broken internal/external links
3. **Link rewriting**: Markdown links (`../concepts/file.md`) are rewritten to Astro routes (`/moat/concepts/slug`)
4. **Static generation**: Astro builds static HTML with proper routing

**Critical**: Never manually edit files in `src/content/moat/` — they're overwritten on every build.
**Critical**: Never manually edit files in `src/content/{moat,keep,gatekeeper}/` — they're overwritten on every build.

### Multi-Product System

Expand All @@ -64,7 +66,7 @@ The site is architected to host documentation for multiple products:
- **Product registry**: `src/lib/products.ts` defines all products with their GitHub repos and doc paths
- **Content collections**: Each product gets its own collection in `src/content/config.ts`
- **Dynamic routing**: Pages use `[category]/[slug]` pattern to support any product structure
- **Navigation config**: `src/config/navigation.ts` defines sidebar structure (currently Moat-only)
- **Navigation config**: `src/config/navigation.ts` builds sidebar structure per product (`buildNavigation(docs, productId)`), shared across Moat, Keep, and Gatekeeper

To add a new product:
1. Add entry to `src/lib/products.ts`
Expand Down Expand Up @@ -114,7 +116,7 @@ BaseLayout.astro

`scripts/fetch-docs.ts` handles fetching docs from GitHub:

**Authentication**: Uses `gh` CLI (GitHub CLI). In CI, set `MOAT_DOCS_TOKEN` secret for private repos.
**Authentication**: Uses `gh` CLI (GitHub CLI). CI runs authenticated with the built-in `GH_TOKEN` (`github.token`) — sufficient since all three product repos are public; a broader-scoped token would be needed if a source repo went private.

**Fetching logic**:
1. Calls GitHub API via `gh api` to list directory contents
Expand Down Expand Up @@ -143,7 +145,7 @@ See `docs/style-guide.md` for complete guidelines. Key principles:
- Background: `stone-100` (warm paper)
- Sidebar: `stone-200`
- Text: `stone-800` / `stone-600` (muted)
- Accent: `sky-700` (links, active states)
- Accent: per-product (`sky-700` Moat, `amber-700` Keep, `emerald-700` Gatekeeper) for links and active states; `sky-700` is the site-wide default outside product pages
- Code blocks: `stone-900` background

**Spacing philosophy**: Prefer consistent Tailwind spacing (4, 6, 8, 12). Avoid arbitrary values except for specific design needs (e.g., tracking).
Expand All @@ -166,7 +168,7 @@ See `docs/style-guide.md` for complete guidelines. Key principles:

**Adding a navigation item**: Edit `src/config/navigation.ts` and add to appropriate section.

**Modifying page layout**: Edit `src/layouts/DocsLayout.astro` for global changes, or `src/pages/moat/[category]/[slug].astro` for content-specific changes.
**Modifying page layout**: Edit `src/layouts/DocsLayout.astro` for global changes, or `src/pages/{moat,keep,gatekeeper}/[category]/[slug].astro` for content-specific changes.

**Styling H2 headings**: All H2 styling is in DocsLayout's prose classes. Look for `prose-h2:` prefixes.

Expand All @@ -175,15 +177,15 @@ See `docs/style-guide.md` for complete guidelines. Key principles:
- Scroll offset: `scroll-mt-24` in DocsLayout prose classes
- Right padding to prevent overlap: `lg:pr-72` on main element

**Testing with real content**: Run `bun run fetch:moat` to pull latest Moat docs from GitHub, then `bun run dev`.
**Testing with real content**: Run `bun run fetch:docs` (or a per-product variant like `bun run fetch:moat`) to pull latest docs from GitHub, then `bun run dev`.

## CI/CD

GitHub Actions workflow in `.github/workflows/`:
GitHub Actions workflows in `.github/workflows/`:
- Runs validation on push and PRs
- Builds and deploys to GitHub Pages
- Requires `MOAT_DOCS_TOKEN` secret if source repos are private
- Builds and deploys to GitHub Pages on push to `main`, manual dispatch, and a daily schedule (`deploy.yml`) — the schedule exists because docs are fetched from the product repos' HEADs at build time, and nothing else triggers a rebuild when only a docs repo changes
- Uses the built-in `GH_TOKEN`; a custom token secret is only needed if a source repo becomes private

## OG Images

Dynamic OG (social share) images generated at `/og/[...path].png` using `@vercel/og`. Images render page title and Moat branding.
Dynamic OG (social share) images generated at `/og/[...path].png` using `@vercel/og`. Images render page title, description, and a per-product brand color (sky for Moat, amber for Keep, emerald for Gatekeeper; see `BRAND_COLORS` in `src/pages/og/[...path].png.ts`).
14 changes: 14 additions & 0 deletions astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,20 @@ export default defineConfig({
],
shikiConfig: {
theme: 'github-dark-dimmed',
transformers: [
{
name: 'bump-comment-contrast',
// github-dark-dimmed renders comments as #768390 on a #22272e
// background -- a 3.87:1 contrast ratio, short of the 4.5:1 WCAG
// AA minimum for body text. Lighten the same hue just enough
// (4.61:1) to pass while keeping the muted "comment" look.
span(node) {
if (node.properties?.style === 'color:#768390') {
node.properties.style = 'color:#84909b';
}
},
},
],
},
},
output: 'static',
Expand Down
3 changes: 2 additions & 1 deletion lighthouserc.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
"categories:performance": ["error", {"minScore": 0.9}],
"categories:accessibility": ["error", {"minScore": 0.9}],
"categories:best-practices": ["error", {"minScore": 0.9}],
"categories:seo": ["error", {"minScore": 0.9}]
"categories:seo": ["error", {"minScore": 0.9}],
"network-dependency-tree-insight": ["warn", {"minScore": 0.9}]
}
},
"upload": {
Expand Down
7 changes: 5 additions & 2 deletions src/layouts/BaseLayout.astro
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,11 @@ const organizationSchema = {
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link rel="preload" href="https://fonts.gstatic.com/s/jetbrainsmono/v18/tDbY2o-flEEny0FZhsfKu5WU4zr3E_BX0PnT8RD8yKxTOlOVgaY.woff2" as="font" type="font/woff2" crossorigin />
<link rel="preload" href="https://fonts.gstatic.com/s/newsreader/v20/cY9qfjOCX1hbuyalUrK49dLac06G1ZGsZBtoBCzBDXXD9JVF438w-I_ADOxEPjCggA.woff2" as="font" type="font/woff2" crossorigin />
<!-- No rel=preload here: Google Fonts rotates the hashed file URLs the
css2 stylesheet below resolves to, so a pinned preload URL silently
goes stale (404) once Google retires that version. This already
happened to the Newsreader preload, which was the source of a
console error breaking the errors-in-console Lighthouse audit. -->
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&family=Newsreader:ital,wght@0,400;0,600;1,400&display=swap" rel="stylesheet" />

<!-- FOUC prevention: apply dark class synchronously before first paint -->
Expand Down
1 change: 0 additions & 1 deletion src/layouts/DocsLayout.astro
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,6 @@ const navigationSections = buildNavigation(docs, product.id);
id="sidebar"
class="w-64 bg-stone-200 dark:bg-stone-800 fixed top-0 pt-[57px] h-screen overflow-y-auto z-40
transform -translate-x-full md:translate-x-0 transition-transform duration-300 ease-in-out"
aria-hidden="true"
>
<div class="px-4 md:px-6 py-6">

Expand Down
7 changes: 4 additions & 3 deletions src/lib/gatekeeper-llms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export { SITE_URL, MAIN_CATEGORIES, OPTIONAL_CATEGORIES } from './llms-shared';
export const INTRO = `Gatekeeper is a standalone credential-injecting TLS-intercepting proxy. Route HTTPS \
traffic through Gatekeeper and it transparently injects authentication headers based on \
hostname matching — clients never see raw credentials. Pluggable credential sources cover \
environment variables, static values, AWS Secrets Manager, GCP Secret Manager, GitHub App \
tokens, and RFC 8693 token exchange. Includes an MCP relay, network policy enforcement, \
and OpenTelemetry observability. Open source, actively developed.`;
environment variables, static values, host commands, AWS Secrets Manager, GCP Secret Manager, \
GCP service account tokens, GitHub App tokens, and RFC 8693 token exchange. Includes an MCP \
relay, a Postgres data plane, LLM policy evaluation, network policy enforcement, and \
OpenTelemetry observability. Open source, actively developed.`;
29 changes: 29 additions & 0 deletions src/pages/gatekeeper/[category]/[slug].md.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import type { APIRoute } from 'astro';
import { getCollection } from 'astro:content';
import { slugify } from '../../../utils/strings';
import { SITE_URL } from '../../../lib/gatekeeper-llms';

export async function getStaticPaths() {
const docs = await getCollection('gatekeeper');

return docs.map((doc) => {
const parts = doc.id.split('/');
const category = parts[0];
const fileName = parts[1];
const slug = slugify(fileName);

return {
params: { category, slug },
props: { doc },
};
});
}

const FOOTER = `\n\n---\n[← Gatekeeper documentation index](${SITE_URL}/gatekeeper/llms.txt)\n`;

export const GET: APIRoute = ({ props }) => {
const { doc } = props as { doc: Awaited<ReturnType<typeof getCollection<'gatekeeper'>>>[number] };
return new Response(doc.body + FOOTER, {
headers: { 'Content-Type': 'text/markdown; charset=utf-8' },
});
};
54 changes: 42 additions & 12 deletions src/pages/gatekeeper/index.astro
Original file line number Diff line number Diff line change
Expand Up @@ -17,43 +17,46 @@ import DocsLayout from '../../layouts/DocsLayout.astro';
<strong class="text-stone-800 dark:text-stone-300 font-semibold">Ships as a CLI, Go library, and MCP relay.</strong><br />Powers Moat's network-layer credential injection. Works standalone or embedded.
</p>

<pre class="astro-code github-dark-dimmed mb-16 overflow-x-auto p-6 rounded-md" style="background-color:#22272e;color:#adbac7" tabindex="0" data-language="bash"><code><span class="line"><span style="color:#768390"># Install</span></span>
<pre class="astro-code github-dark-dimmed mb-16 overflow-x-auto p-6 rounded-md" style="background-color:#22272e;color:#adbac7" tabindex="0" data-language="bash"><code><span class="line"><span style="color:#84909b"># Install</span></span>
<span class="line"><span style="color:#dcbdfb">$</span><span style="color:#96d0ff"> go install github.com/majorcontext/gatekeeper/cmd/gatekeeper@latest</span></span>
<span class="line"></span>
<span class="line"><span style="color:#768390"># Start the proxy</span></span>
<span class="line"><span style="color:#84909b"># ...or pull the container image</span></span>
<span class="line"><span style="color:#dcbdfb">$</span><span style="color:#96d0ff"> docker pull ghcr.io/majorcontext/gatekeeper:latest</span></span>
<span class="line"></span>
<span class="line"><span style="color:#84909b"># Start the proxy</span></span>
<span class="line"><span style="color:#dcbdfb">$</span><span style="color:#96d0ff"> gatekeeper --config gatekeeper.yaml</span></span>
<span class="line"></span>
<span class="line"><span style="color:#768390"># Make a request — credential injected automatically</span></span>
<span class="line"><span style="color:#84909b"># Make a request — credential injected automatically</span></span>
<span class="line"><span style="color:#dcbdfb">$</span><span style="color:#96d0ff"> curl --proxy http://127.0.0.1:9080 --cacert ca.crt https://api.github.com/user</span></span></code></pre>

<h2 class="text-sm font-semibold uppercase tracking-widest text-stone-600 dark:text-stone-400 mb-4">Credentials</h2>
<div class="grid md:grid-cols-2 border border-stone-300 dark:border-stone-700 mb-16">
<div class="border-b border-stone-300 dark:border-stone-700 md:border-r py-8 px-6">
<div class="text-4xl font-mono text-stone-300 dark:text-stone-700 mb-1.5">01</div>
<div class="text-4xl font-mono text-stone-500 mb-1.5">01</div>
<h2 class="text-sm font-normal mb-1.5">TLS Interception</h2>
<p class="text-sm text-stone-600 dark:text-stone-400 leading-relaxed">
Per-host certificates generated on demand from your CA. The proxy terminates TLS, reads the request, injects the header, and forwards over a fresh upstream TLS connection.
</p>
</div>

<div class="border-b border-stone-300 dark:border-stone-700 py-8 px-6">
<div class="text-4xl font-mono text-stone-300 dark:text-stone-700 mb-1.5">02</div>
<div class="text-4xl font-mono text-stone-500 mb-1.5">02</div>
<h2 class="text-sm font-normal mb-1.5">Hostname-Based Injection</h2>
<p class="text-sm text-stone-600 dark:text-stone-400 leading-relaxed">
Map upstream hosts to credentials in YAML. Headers are added only for matching requests — everything else passes through untouched.
</p>
</div>

<div class="border-b md:border-b-0 border-stone-300 dark:border-stone-700 md:border-r py-8 px-6">
<div class="text-4xl font-mono text-stone-300 dark:text-stone-700 mb-1.5">03</div>
<div class="text-4xl font-mono text-stone-500 mb-1.5">03</div>
<h2 class="text-sm font-normal mb-1.5">Pluggable Sources</h2>
<p class="text-sm text-stone-600 dark:text-stone-400 leading-relaxed">
Env vars and static values for development. AWS Secrets Manager, GCP Secret Manager, GitHub App tokens, and RFC 8693 token exchange for production.
Env vars and static values for development. Host commands, AWS Secrets Manager, GCP Secret Manager, GCP service account tokens, GitHub App tokens, and RFC 8693 token exchange for production.
</p>
</div>

<div class="py-8 px-6">
<div class="text-4xl font-mono text-stone-300 dark:text-stone-700 mb-1.5">04</div>
<div class="text-4xl font-mono text-stone-500 mb-1.5">04</div>
<h2 class="text-sm font-normal mb-1.5">Network Policy</h2>
<p class="text-sm text-stone-600 dark:text-stone-400 leading-relaxed">
Permissive by default, strict allowlist when you need it. Block egress to anything outside your declared host list.
Expand All @@ -64,38 +67,65 @@ import DocsLayout from '../../layouts/DocsLayout.astro';
<h2 class="text-sm font-semibold uppercase tracking-widest text-stone-600 dark:text-stone-400 mb-4">Integration</h2>
<div class="grid md:grid-cols-2 border border-stone-300 dark:border-stone-700 mb-16">
<div class="border-b border-stone-300 dark:border-stone-700 md:border-r py-8 px-6">
<div class="text-4xl font-mono text-stone-300 dark:text-stone-700 mb-1.5">05</div>
<div class="text-4xl font-mono text-stone-500 mb-1.5">05</div>
<h2 class="text-sm font-normal mb-1.5">Standard HTTP Proxy</h2>
<p class="text-sm text-stone-600 dark:text-stone-400 leading-relaxed">
Any client that honors <code class="font-mono text-xs bg-stone-200 dark:bg-stone-800 px-1.5 py-0.5 rounded-sm">HTTPS_PROXY</code> can route through Gatekeeper. No SDK or library required.
</p>
</div>

<div class="border-b border-stone-300 dark:border-stone-700 py-8 px-6">
<div class="text-4xl font-mono text-stone-300 dark:text-stone-700 mb-1.5">06</div>
<div class="text-4xl font-mono text-stone-500 mb-1.5">06</div>
<h2 class="text-sm font-normal mb-1.5">MCP Relay</h2>
<p class="text-sm text-stone-600 dark:text-stone-400 leading-relaxed">
Forward Model Context Protocol requests to upstream MCP servers with credential injection and SSE streaming preserved.
</p>
</div>

<div class="border-b md:border-b-0 border-stone-300 dark:border-stone-700 md:border-r py-8 px-6">
<div class="text-4xl font-mono text-stone-300 dark:text-stone-700 mb-1.5">07</div>
<div class="text-4xl font-mono text-stone-500 mb-1.5">07</div>
<h2 class="text-sm font-normal mb-1.5">OpenTelemetry</h2>
<p class="text-sm text-stone-600 dark:text-stone-400 leading-relaxed">
Traces, metrics, and logs configured entirely via standard <code class="font-mono text-xs bg-stone-200 dark:bg-stone-800 px-1.5 py-0.5 rounded-sm">OTEL_*</code> environment variables. Canonical log line per request.
</p>
</div>

<div class="py-8 px-6">
<div class="text-4xl font-mono text-stone-300 dark:text-stone-700 mb-1.5">08</div>
<div class="text-4xl font-mono text-stone-500 mb-1.5">08</div>
<h2 class="text-sm font-normal mb-1.5">Go Library</h2>
<p class="text-sm text-stone-600 dark:text-stone-400 leading-relaxed">
Embed the proxy directly in your Go tooling. Full programmatic control over config, credential sources, and lifecycle.
</p>
</div>
</div>

<h2 class="text-sm font-semibold uppercase tracking-widest text-stone-600 dark:text-stone-400 mb-4">Data Plane &amp; Policy</h2>
<div class="grid md:grid-cols-2 border border-stone-300 dark:border-stone-700 mb-16">
<div class="border-b border-stone-300 dark:border-stone-700 md:border-r py-8 px-6">
<div class="text-4xl font-mono text-stone-500 mb-1.5">09</div>
<h2 class="text-sm font-normal mb-1.5">Postgres Data Plane</h2>
<p class="text-sm text-stone-600 dark:text-stone-400 leading-relaxed">
A second listener speaks the Postgres wire protocol, authenticating clients with their run token in place of a database password. Gatekeeper resolves the real per-branch Neon password and completes SCRAM-SHA-256 upstream.
</p>
</div>

<div class="border-b border-stone-300 dark:border-stone-700 py-8 px-6">
<div class="text-4xl font-mono text-stone-500 mb-1.5">10</div>
<h2 class="text-sm font-normal mb-1.5">LLM Policy</h2>
<p class="text-sm text-stone-600 dark:text-stone-400 leading-relaxed">
Anthropic API responses are evaluated against <a href="/keep" class="underline hover:text-emerald-700 dark:hover:text-emerald-400">Keep</a> policy rules before reaching the client. Denials are blocked fail-closed, including streamed SSE responses.
</p>
</div>

<div class="py-8 px-6">
<div class="text-4xl font-mono text-stone-500 mb-1.5">11</div>
<h2 class="text-sm font-normal mb-1.5">Host Gateway</h2>
<p class="text-sm text-stone-600 dark:text-stone-400 leading-relaxed">
Maps a synthetic in-container hostname to the host machine's real IP. Containers reach host services through the same credential injection and network policy as any other request.
</p>
</div>
</div>

<div class="mb-16">
<h2 class="text-sm font-semibold uppercase tracking-widest text-stone-600 dark:text-stone-400 pb-3 mb-4 border-b border-stone-300 dark:border-stone-700">Why This Exists</h2>

Expand Down
Loading
Loading