diff --git a/.auths/roots b/.auths/roots new file mode 100644 index 0000000..42a5528 --- /dev/null +++ b/.auths/roots @@ -0,0 +1,2 @@ +# Pinned by auths init β€” the trusted root for this identity. +did:keri:EB5cPHY0t-ejNC_rUzPS1dclTvd6kG-R9mQzjozCuGgd diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..1d44441 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,34 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + +permissions: + contents: read + +jobs: + check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: oven-sh/setup-bun@v2 + + - name: Install + run: bun install --frozen-lockfile + + # Markdoc validation + the frontmatter contract run inside the build + # (parseDoc throws on critical errors; getAllDocs throws on a missing + # field), so a page that would render wrong fails here, not in prod. + - name: Build + run: bun run build + + - name: Type-check + run: bun run type-check + + # Copy lint, verdict lint, and the link check β€” the drift that bit the + # marketing site, encoded as a gate. External links included. + - name: Docs check + run: bun run check diff --git a/app/docs/[...slug]/page.tsx b/app/docs/[...slug]/page.tsx new file mode 100644 index 0000000..cb2c695 --- /dev/null +++ b/app/docs/[...slug]/page.tsx @@ -0,0 +1,76 @@ +import fs from 'fs' +import path from 'path' +import type { Metadata } from 'next' +import { notFound } from 'next/navigation' +import { getAllDocs, getDocBySlug, getPrevNext } from '@/lib/content' +import { parseDoc } from '@/lib/markdoc' +import { renderDoc } from '@/lib/markdoc-components' +import { DocsPrevNext } from '@/components/docs/DocsPrevNext' + +/** + * The one route for every doc page: content is data (`content/docs/**.md`), + * metadata comes from frontmatter, and the nav can never drift from disk. + */ + +type Props = { params: Promise<{ slug: string[] }> } + +export function generateStaticParams() { + return getAllDocs().map((d) => ({ slug: d.slug })) +} + +export async function generateMetadata({ params }: Props): Promise { + const { slug } = await params + const doc = getDocBySlug(slug) + if (!doc) return { title: 'Not found' } + return { + title: `${doc.frontmatter.title} β€” Auths docs`, + description: doc.frontmatter.description, + } +} + +export default async function DocPage({ params }: Props) { + const { slug } = await params + const doc = getDocBySlug(slug) + if (!doc) notFound() + + const source = fs.readFileSync(doc.filePath, 'utf8') + const { content } = parseDoc(source) + const { prev, next } = getPrevNext(doc.href) + + const editUrl = `https://github.com/auths-dev/auths-docs/edit/main/${path + .relative(process.cwd(), doc.filePath) + .split(path.sep) + .join('/')}` + + return ( +
+
+

+ {doc.frontmatter.section} +

+

+ {doc.frontmatter.title} +

+

{doc.frontmatter.description}

+
+ + {renderDoc(content)} + + + + +
+ ) +} diff --git a/app/docs/authentication/page.tsx b/app/docs/authentication/page.tsx deleted file mode 100644 index 6018faa..0000000 --- a/app/docs/authentication/page.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import fs from 'fs' -import path from 'path' -import { parseDoc } from '@/lib/markdoc' -import { DocsPrevNext } from '@/components/docs/DocsPrevNext' - -export default async function Page() { - const source = fs.readFileSync(path.join(process.cwd(), 'content/docs/authentication.md'), 'utf8') - const { html, frontmatter } = parseDoc(source) - - return ( -
-
-

{frontmatter.title}

-

{frontmatter.description}

-
-
- -
- ) -} - -export function generateMetadata() { - return { - title: 'Authentication - Auths Docs', - description: 'Authenticate your API requests with Auths API keys', - } -} diff --git a/app/docs/build-agents/page.tsx b/app/docs/build-agents/page.tsx deleted file mode 100644 index 06f6be0..0000000 --- a/app/docs/build-agents/page.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import fs from 'fs' -import path from 'path' -import { parseDoc } from '@/lib/markdoc' -import { DocsPrevNext } from '@/components/docs/DocsPrevNext' - -export default async function Page() { - const source = fs.readFileSync(path.join(process.cwd(), 'content/docs/build-agents.md'), 'utf8') - const { html, frontmatter } = parseDoc(source) - - return ( -
-
-

{frontmatter.title}

-

{frontmatter.description}

-
-
- -
- ) -} - -export function generateMetadata() { - return { title: 'Build Agents - Auths Docs', description: 'Automate code signing with agents in your CI/CD pipeline' } -} diff --git a/app/docs/concepts/[topic]/page.tsx b/app/docs/concepts/[topic]/page.tsx deleted file mode 100644 index 68f6d52..0000000 --- a/app/docs/concepts/[topic]/page.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import fs from 'fs' -import path from 'path' -import { parseDoc } from '@/lib/markdoc' -import { DocsPrevNext } from '@/components/docs/DocsPrevNext' - -interface PageProps { - params: Promise<{ topic: string }> -} - -export default async function Page(props: PageProps) { - const { topic } = await props.params - const filePath = path.join(process.cwd(), `content/docs/concepts/${topic}.md`) - - if (!fs.existsSync(filePath)) { - return
Page not found
- } - - const source = fs.readFileSync(filePath, 'utf8') - const { html, frontmatter } = parseDoc(source) - - return ( -
-
-

{frontmatter.title}

-

{frontmatter.description}

-
-
- -
- ) -} - -export async function generateStaticParams() { - const dir = path.join(process.cwd(), 'content/docs/concepts') - if (!fs.existsSync(dir)) return [] - return fs - .readdirSync(dir) - .filter((f) => f.endsWith('.md')) - .map((f) => ({ topic: f.replace(/\.md$/, '') })) -} - -export async function generateMetadata(props: PageProps) { - const { topic } = await props.params - const filePath = path.join(process.cwd(), `content/docs/concepts/${topic}.md`) - if (!fs.existsSync(filePath)) return {} - const source = fs.readFileSync(filePath, 'utf8') - const { frontmatter } = parseDoc(source) - return { title: `${frontmatter.title} - Auths Docs`, description: frontmatter.description } -} diff --git a/app/docs/installation/page.tsx b/app/docs/installation/page.tsx deleted file mode 100644 index 7952bfc..0000000 --- a/app/docs/installation/page.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import fs from 'fs' -import path from 'path' -import { parseDoc } from '@/lib/markdoc' -import { DocsPrevNext } from '@/components/docs/DocsPrevNext' - -export default async function Page() { - const source = fs.readFileSync(path.join(process.cwd(), 'content/docs/installation.md'), 'utf8') - const { html, frontmatter } = parseDoc(source) - - return ( -
-
-

{frontmatter.title}

-

{frontmatter.description}

-
-
- -
- ) -} - -export function generateMetadata() { - return { title: 'Installation - Auths Docs', description: 'Get the Auths CLI up and running' } -} diff --git a/app/docs/layout.tsx b/app/docs/layout.tsx index 398458a..b2b4a26 100644 --- a/app/docs/layout.tsx +++ b/app/docs/layout.tsx @@ -1,39 +1,31 @@ import { DocsTopBar } from '@/components/docs/DocsTopBar' import { DocsSidebar } from '@/components/docs/DocsSidebar' -import { CodeExamples } from '@/components/docs/CodeExamples' import { DocsMobileDrawer } from '@/components/docs/DocsMobileDrawer' +import { OnThisPage } from '@/components/docs/OnThisPage' +import { LanguageProvider } from '@/components/markdoc/language-context' +import { getNavigation } from '@/lib/content' + +export default function DocsLayout({ children }: { children: React.ReactNode }) { + const nav = getNavigation() -export default function DocsLayout({ - children, -}: { - children: React.ReactNode -}) { return ( - <> + -
-
-
- {/* Left Sidebar */} - +
+
+ - {/* Main Content */} -
- {children} -
+
{children}
- {/* Right Sidebar (Code Examples) */} - -
+
- {/* Mobile drawer */} - - + + ) } diff --git a/app/docs/mcp-setup/page.tsx b/app/docs/mcp-setup/page.tsx deleted file mode 100644 index 9e8f2c2..0000000 --- a/app/docs/mcp-setup/page.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import fs from 'fs' -import path from 'path' -import { parseDoc } from '@/lib/markdoc' -import { DocsPrevNext } from '@/components/docs/DocsPrevNext' - -export default async function Page() { - const source = fs.readFileSync(path.join(process.cwd(), 'content/docs/mcp-setup.md'), 'utf8') - const { html, frontmatter } = parseDoc(source) - - return ( -
-
-

{frontmatter.title}

-

{frontmatter.description}

-
-
- -
- ) -} - -export function generateMetadata() { - return { - title: 'MCP Setup - Auths Docs', - description: 'Give a Claude or Cursor agent a scoped, expiring, revocable Auths passport', - } -} diff --git a/app/docs/page.tsx b/app/docs/page.tsx index b3ffef9..8422fb5 100644 --- a/app/docs/page.tsx +++ b/app/docs/page.tsx @@ -1,68 +1,6 @@ -import Link from 'next/link' +import { redirect } from 'next/navigation' -export default function DocsPage() { - return ( -
-
-

Documentation

-

- Learn how to use Auths to sign commits, manage identities, and prove code provenance. -

-
- -
- -
πŸ“
-

- Sign Commits -

-

- Get started with cryptographic commit signing. Learn the basics and best practices. -

- - - -
πŸ‘₯
-

- Team Identities -

-

- Manage multiple identities for your team. Organize and verify contributor keys. -

- - - -
πŸ€–
-

- Build Agents -

-

- Automate code signing with agents. Integrate signing into your CI/CD pipeline. -

- - - -
πŸ”—
-

- Prove Provenance -

-

- Verify the origin of code. Prove that commits are authentic and traceable. -

- -
-
- ) +/** The docs land on the product that leads: the bounded agent. */ +export default function DocsIndex() { + redirect('/docs/mcp') } diff --git a/app/docs/prove-provenance/page.tsx b/app/docs/prove-provenance/page.tsx deleted file mode 100644 index 0df2b8e..0000000 --- a/app/docs/prove-provenance/page.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import fs from 'fs' -import path from 'path' -import { parseDoc } from '@/lib/markdoc' -import { DocsPrevNext } from '@/components/docs/DocsPrevNext' - -export default async function Page() { - const source = fs.readFileSync(path.join(process.cwd(), 'content/docs/prove-provenance.md'), 'utf8') - const { html, frontmatter } = parseDoc(source) - - return ( -
-
-

{frontmatter.title}

-

{frontmatter.description}

-
-
- -
- ) -} - -export function generateMetadata() { - return { title: 'Prove Provenance - Auths Docs', description: 'Verify the authenticity and origin of code commits' } -} diff --git a/app/docs/quickstart/page.tsx b/app/docs/quickstart/page.tsx deleted file mode 100644 index 2bdc57e..0000000 --- a/app/docs/quickstart/page.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import fs from 'fs' -import path from 'path' -import { parseDoc } from '@/lib/markdoc' -import { DocsPrevNext } from '@/components/docs/DocsPrevNext' - -export default async function Page() { - const source = fs.readFileSync(path.join(process.cwd(), 'content/docs/quickstart.md'), 'utf8') - const { html, frontmatter } = parseDoc(source) - - return ( -
-
-

{frontmatter.title}

-

{frontmatter.description}

-
-
- -
- ) -} - -export function generateMetadata() { - return { - title: 'Quickstart - Auths Docs', - description: 'Install Auths, create an identity, and verify your first signed commit in five minutes', - } -} diff --git a/app/docs/reference/[topic]/page.tsx b/app/docs/reference/[topic]/page.tsx deleted file mode 100644 index b7c01b2..0000000 --- a/app/docs/reference/[topic]/page.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import fs from 'fs' -import path from 'path' -import { parseDoc } from '@/lib/markdoc' -import { DocsPrevNext } from '@/components/docs/DocsPrevNext' - -interface PageProps { - params: Promise<{ topic: string }> -} - -export default async function Page(props: PageProps) { - const { topic } = await props.params - const filePath = path.join(process.cwd(), `content/docs/reference/${topic}.md`) - - if (!fs.existsSync(filePath)) { - return
Page not found
- } - - const source = fs.readFileSync(filePath, 'utf8') - const { html, frontmatter } = parseDoc(source) - - return ( -
-
-

{frontmatter.title}

-

{frontmatter.description}

-
-
- -
- ) -} - -export async function generateStaticParams() { - const dir = path.join(process.cwd(), 'content/docs/reference') - if (!fs.existsSync(dir)) return [] - return fs - .readdirSync(dir) - .filter((f) => f.endsWith('.md')) - .map((f) => ({ topic: f.replace(/\.md$/, '') })) -} - -export async function generateMetadata(props: PageProps) { - const { topic } = await props.params - const filePath = path.join(process.cwd(), `content/docs/reference/${topic}.md`) - if (!fs.existsSync(filePath)) return {} - const source = fs.readFileSync(filePath, 'utf8') - const { frontmatter } = parseDoc(source) - return { title: `${frontmatter.title} - Auths Docs`, description: frontmatter.description } -} diff --git a/app/docs/sign-commits/page.tsx b/app/docs/sign-commits/page.tsx deleted file mode 100644 index 7cc1f32..0000000 --- a/app/docs/sign-commits/page.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import fs from 'fs' -import path from 'path' -import { parseDoc } from '@/lib/markdoc' -import { DocsPrevNext } from '@/components/docs/DocsPrevNext' - -export default async function Page() { - const source = fs.readFileSync( - path.join(process.cwd(), 'content/docs/sign-commits.md'), - 'utf8' - ) - const { html, frontmatter } = parseDoc(source) - - return ( -
-
-

- {frontmatter.title as string} -

-

- {frontmatter.description as string} -

-
-
- -
- ) -} - -export function generateMetadata() { - return { - title: 'Sign Commits - Auths Docs', - description: 'Learn how to cryptographically sign your commits with Auths', - } -} diff --git a/app/docs/team-identities/page.tsx b/app/docs/team-identities/page.tsx deleted file mode 100644 index d55fbf9..0000000 --- a/app/docs/team-identities/page.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import fs from 'fs' -import path from 'path' -import { parseDoc } from '@/lib/markdoc' -import { DocsPrevNext } from '@/components/docs/DocsPrevNext' - -export default async function Page() { - const source = fs.readFileSync(path.join(process.cwd(), 'content/docs/team-identities.md'), 'utf8') - const { html, frontmatter } = parseDoc(source) - - return ( -
-
-

{frontmatter.title}

-

{frontmatter.description}

-
-
- -
- ) -} - -export function generateMetadata() { - return { title: 'Team Identities - Auths Docs', description: 'Manage multiple identities and signing keys for your team' } -} diff --git a/app/globals.css b/app/globals.css index 7806d96..8da13a5 100644 --- a/app/globals.css +++ b/app/globals.css @@ -1,96 +1,198 @@ @import "tailwindcss"; +/* --------------------------------------------------------------------------- + * The ledger, for reading: the marketing site's paper-and-ink palette tuned + * for long-form docs. One accent (the seal); the reserved red means exactly + * one thing β€” a denial. Terminals and code panes are the only dark objects + * in light mode. Typeface: the system stack (unchanged). + * ------------------------------------------------------------------------ */ + +:root { + --paper: #f6f3ec; + --paper-deep: #ece7db; + --ink: #1c1814; + --ink-soft: #5b5448; + --ink-faint: #726c5e; + --rule: rgba(28, 24, 20, 0.16); + --seal: #c2401b; + --seal-deep: #9c3214; + --deny: #c0442e; + --terminal: #15130f; +} + +.dark { + --paper: #141210; + --paper-deep: #1d1a16; + --ink: #eae5db; + --ink-soft: #b3aa9b; + --ink-faint: #948b7c; + --rule: rgba(234, 229, 219, 0.14); + --seal: #e8845c; + --seal-deep: #f09b78; + --deny: #e2664a; + --terminal: #100e0b; +} + +@theme inline { + --color-paper: var(--paper); + --color-paper-deep: var(--paper-deep); + --color-ink: var(--ink); + --color-ink-soft: var(--ink-soft); + --color-ink-faint: var(--ink-faint); + --color-rule: var(--rule); + --color-seal: var(--seal); + --color-seal-deep: var(--seal-deep); + --color-deny: var(--deny); + --color-terminal: var(--terminal); +} + html { scroll-behavior: smooth; + scroll-padding-top: 5rem; +} + +body { + background: var(--paper); + color: var(--ink); } -/* Prose typography */ +/* --------------------------------------------------------------------------- + * Prose β€” one comfortable measure, generous rhythm, hairline rules. + * Code fences render through the CodeBlock component, not prose styles. + * ------------------------------------------------------------------------ */ + article.prose { - color: theme(colors.gray.900); - line-height: 1.75; + color: var(--ink-soft); + line-height: 1.7; + font-size: 1rem; } .prose h2 { - margin-top: 2rem; + margin-top: 3rem; margin-bottom: 1rem; - font-size: 1.875rem; + padding-top: 1.25rem; + border-top: 1px solid var(--rule); + font-size: 1.5rem; font-weight: 700; + letter-spacing: -0.015em; + color: var(--ink); + scroll-margin-top: 6rem; } .prose h3 { - margin-top: 1.5rem; + margin-top: 2rem; margin-bottom: 0.75rem; - font-size: 1.25rem; + font-size: 1.125rem; font-weight: 600; + color: var(--ink); + scroll-margin-top: 6rem; } .prose p { margin-bottom: 1rem; } -.prose ul, -.prose ol { +.prose strong { + color: var(--ink); + font-weight: 600; +} + +.prose ul:not(:where(.not-prose, .not-prose *)), +.prose ol:not(:where(.not-prose, .not-prose *)) { margin-bottom: 1rem; - margin-left: 1.5rem; - padding-left: 1rem; + padding-left: 1.5rem; } -.prose li { - margin-bottom: 0.5rem; +.prose li:not(:where(.not-prose, .not-prose *)) { + margin-bottom: 0.4rem; } -.prose ul li { +.prose ul li:not(:where(.not-prose, .not-prose *)) { list-style-type: disc; } -.prose ol li { +.prose ol li:not(:where(.not-prose, .not-prose *)) { list-style-type: decimal; } -/* Code blocks */ -.prose pre { - background-color: #1f2937; - color: #f3f4f6; - padding: 1rem; - border-radius: 0.5rem; - overflow-x: auto; - margin-bottom: 1rem; -} - -.prose pre code { - background-color: transparent; - color: inherit; - padding: 0; - font-size: 0.875rem; - line-height: 1.5; +.prose li:not(:where(.not-prose, .not-prose *))::marker { + color: var(--ink-faint); } -/* Inline code */ +/* Inline code β€” an ink chip, never a color of its own. */ .prose :not(pre) > code { - background-color: #f3f4f6; - color: #dc2626; + background: color-mix(in srgb, var(--ink) 6%, transparent); + color: var(--ink); padding: 0.125rem 0.375rem; border-radius: 0.25rem; - font-family: ui-monospace, monospace; + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 0.875em; - font-weight: 600; + font-weight: 500; } -/* Links */ .prose a { - color: #2563eb; + color: var(--seal); text-decoration: none; + border-bottom: 1px solid color-mix(in srgb, var(--seal) 40%, transparent); + transition: border-color 150ms ease; } .prose a:hover { - text-decoration: underline; + border-bottom-color: var(--seal); +} + +.prose a:has(code) { + border-bottom: none; } -/* Blockquotes */ .prose blockquote { - border-left: 4px solid #e5e7eb; + border-left: 2px solid color-mix(in srgb, var(--seal) 50%, transparent); padding-left: 1rem; font-style: italic; - color: #6b7280; + color: var(--ink); margin: 1.5rem 0; } + +/* Tables β€” hairlines, mono heads, no zebra. */ +.prose table { + width: 100%; + margin: 1.5rem 0; + border-collapse: collapse; + font-size: 0.9rem; +} + +.prose thead th { + text-align: left; + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-size: 0.72rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.08em; + color: var(--ink-faint); + border-bottom: 1px solid var(--rule); + padding: 0.5rem 0.75rem 0.5rem 0; +} + +.prose tbody td { + border-bottom: 1px solid var(--rule); + padding: 0.6rem 0.75rem 0.6rem 0; + vertical-align: top; +} + +.prose tbody tr:last-child td { + border-bottom: none; +} + +.prose hr { + border: none; + border-top: 1px solid var(--rule); + margin: 2.5rem 0; +} + +/* Anchored headings: hover reveals a link affordance. */ +.prose h2:hover::after, +.prose h3:hover::after { + content: " #"; + color: var(--ink-faint); + font-weight: 400; +} diff --git a/app/layout.tsx b/app/layout.tsx index 3c8ca43..36eeff3 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,21 +1,32 @@ import type { Metadata } from 'next' import './globals.css' +const SEAL_FAVICON = + 'data:image/svg+xml,' + export const metadata: Metadata = { - title: 'Auths Docs', - description: 'Documentation for Auths - proof of provenance for code.', - icons: { - icon: 'data:image/svg+xml,πŸ”', - }, + title: 'Auths docs', + description: + 'Bound an AI agent to a scope, a budget, and an expiry β€” and prove it with receipts anyone can verify.', + icons: { icon: SEAL_FAVICON }, } -export default function RootLayout({ - children, -}: { - children: React.ReactNode -}) { +/** Applies the stored (or system) theme before paint β€” no flash. */ +const THEME_SCRIPT = ` +try { + var t = localStorage.getItem('auths.docs.theme'); + if (t === 'dark' || (!t && window.matchMedia('(prefers-color-scheme: dark)').matches)) { + document.documentElement.classList.add('dark'); + } +} catch (e) {} +` + +export default function RootLayout({ children }: { children: React.ReactNode }) { return ( - + + +