Skip to content

Blog 3 - #1

Merged
motdotla merged 3 commits into
mainfrom
blog-3
Feb 23, 2026
Merged

Blog 3#1
motdotla merged 3 commits into
mainfrom
blog-3

Conversation

@motdotla

Copy link
Copy Markdown
Contributor

No description provided.

@charliecreates charliecreates Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Global Rouge styling is a good direction, but placing a large highlight/CSS block in _includes/layouts/meta.html increases cross-page coupling and makes future overrides harder. The new blog post has a real correctness issue (json = {...} without declaration) and a likely reader-confusion issue around .well-known key discovery depending on whether req.agentUid is set. There’s also potential API confusion in the tutorial due to switching between vestauth.primitives.verify and vestauth.tool.verify without explanation.

Summary of changes

Summary

This PR introduces Rouge-based syntax highlighting across the site and adds a new blog post.

Key updates

  • Jekyll config: switches from no highlighter to rouge via kramdown.syntax_highlighter and highlighter.
  • Global code styling: adds a sizable block of CSS in _includes/layouts/meta.html to style inline code and Rouge highlight output (div.highlighter-rouge, figure.highlight, token colors) with light/dark scheme support.
  • Blog layout cleanup: removes blog-specific code block styles in _layouts/blog.html (presumably replaced by the global Rouge styles) and removes text-transform: lowercase.
  • Homepage snippets: replaces inline <pre><code> blocks with Liquid {% highlight %} blocks for shell/JS examples.
  • Content: adds a new post blog/_posts/2026-02-22-run-your-own-vestauth-infrastructure.md and an associated image assets/img/blog/blog-3.png.

Comment on lines +50 to +92
main :not(pre) > code {
background: #f4f4f5;
color: #111827;
font-size: 0.9em;
border-radius: 6px;
padding: 0.08rem 0.32rem;
}

main div.highlighter-rouge,
main pre.highlight,
main figure.highlight {
margin-top: 0.15rem;
margin-bottom: 1rem;
background: #f5f8fc;
border: 1px solid #d7e3f0;
border-left: 3px solid #7aa2d6;
border-radius: 10px;
padding: 0.75rem 0.9rem;
overflow-x: auto;
font-size: 0.76rem;
line-height: 1.55;
}

main div.highlighter-rouge .highlight,
main div.highlighter-rouge pre.highlight,
main figure.highlight pre {
margin: 0;
border: 0;
padding: 0;
background: transparent;
border-left: 0;
border-radius: 0;
font-size: inherit;
overflow: visible;
}

main div.highlighter-rouge code,
main pre.highlight code {
background: transparent;
padding: 0;
color: inherit;
font-size: inherit;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The inline-code selector main :not(pre) > code will also style code inside other containers you likely don’t want treated as “inline” (e.g., figure.highlight code can still match depending on Rouge’s structure, nested wrappers, or future markup changes). It’s safer to explicitly exclude Rouge/highlight containers so you don’t end up double-styling or fighting specificity later.

Suggestion

Tighten the selector to target only inline code while explicitly excluding Rouge blocks. For example:

main :not(pre):not(.highlight):not(.highlighter-rouge) > code {
  /* inline code styles */
}

/* Or, more explicit: */
main p > code,
main li > code,
main td > code {
  /* inline code styles */
}

This reduces unintended styling collisions with Rouge-generated markup. Reply with "@CharlieHelps yes please" if you’d like me to add a commit with this change.

Comment on lines +49 to +185

main :not(pre) > code {
background: #f4f4f5;
color: #111827;
font-size: 0.9em;
border-radius: 6px;
padding: 0.08rem 0.32rem;
}

main div.highlighter-rouge,
main pre.highlight,
main figure.highlight {
margin-top: 0.15rem;
margin-bottom: 1rem;
background: #f5f8fc;
border: 1px solid #d7e3f0;
border-left: 3px solid #7aa2d6;
border-radius: 10px;
padding: 0.75rem 0.9rem;
overflow-x: auto;
font-size: 0.76rem;
line-height: 1.55;
}

main div.highlighter-rouge .highlight,
main div.highlighter-rouge pre.highlight,
main figure.highlight pre {
margin: 0;
border: 0;
padding: 0;
background: transparent;
border-left: 0;
border-radius: 0;
font-size: inherit;
overflow: visible;
}

main div.highlighter-rouge code,
main pre.highlight code {
background: transparent;
padding: 0;
color: inherit;
font-size: inherit;
}

main .highlight .c,
main .highlight .c1,
main .highlight .cm {
color: #64748b;
font-style: italic;
}

main .highlight .k,
main .highlight .kd,
main .highlight .kn,
main .highlight .kp,
main .highlight .kr,
main .highlight .kt {
color: #b45309;
}

main .highlight .s,
main .highlight .s1,
main .highlight .s2,
main .highlight .sb,
main .highlight .se,
main .highlight .sh {
color: #0369a1;
}

main .highlight .m,
main .highlight .mi,
main .highlight .mf {
color: #be123c;
}

main .highlight .na,
main .highlight .nb,
main .highlight .nc,
main .highlight .nf,
main .highlight .nx {
color: #1d4ed8;
}

@media (prefers-color-scheme: dark) {
main :not(pre) > code {
background: #0f141a;
color: #e5e7eb;
}

main div.highlighter-rouge,
main pre.highlight,
main figure.highlight {
background: #0f141a;
border-color: #27323d;
border-left-color: #64748b;
color: #e5e7eb;
}

main .highlight .c,
main .highlight .c1,
main .highlight .cm {
color: #7c8aa0;
}

main .highlight .k,
main .highlight .kd,
main .highlight .kn,
main .highlight .kp,
main .highlight .kr,
main .highlight .kt {
color: #fbbf24;
}

main .highlight .s,
main .highlight .s1,
main .highlight .s2,
main .highlight .sb,
main .highlight .se,
main .highlight .sh {
color: #34d399;
}

main .highlight .m,
main .highlight .mi,
main .highlight .mf {
color: #fb923c;
}

main .highlight .na,
main .highlight .nb,
main .highlight .nc,
main .highlight .nf,
main .highlight .nx {
color: #fda4af;
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All Rouge/highlighting styles are being injected into a shared meta.html include. This is a large, domain-specific styling block that will apply to every page and increases the cost of future style changes (and makes CSS harder to discover/override). If the site grows, this becomes a maintainability pain point and can cause unexpected cross-page regressions.

Suggestion

Move these code/highlight styles into a dedicated CSS file (e.g., assets/css/highlight.css) and include it via <link> so it’s cacheable and logically separated.

If you want to keep it inline for now, at least wrap with a comment header and scope to a class on layouts that need it (e.g., .has-code-blocks main ...). Reply with "@CharlieHelps yes please" if you’d like me to add a commit moving this into an asset and wiring it up.

Comment on lines +201 to +210
```js
...
app.get('/.well-known/http-message-signatures-directory', (req, res) => {
const keys = PUBLIC_JWKS
.filter(jwk => jwk.agent_uid === req.agentUid)
.map(jwk => jwk.value)

res.json({ keys })
})
```

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The .well-known endpoint example filters keys by req.agentUid, but req.agentUid is only set for *.localhost hosts in the later middleware. If you hit http://localhost:3000/.well-known/... directly, req.agentUid will be undefined and this will always return an empty key set, which can confuse readers when they test the endpoint during the tutorial.

Suggestion

Adjust the example to handle the non-subdomain case explicitly (and explain it). For example:

app.get('/.well-known/http-message-signatures-directory', (req, res) => {
  if (!req.agentUid) {
    return res.status(400).json({
      error: 'Missing agent subdomain. Use http://<agent-uid>.localhost:3000/…'
    })
  }

  const keys = PUBLIC_JWKS
    .filter(jwk => jwk.agent_uid === req.agentUid)
    .map(jwk => jwk.value)

  res.json({ keys })
})

Reply with "@CharlieHelps yes please" if you’d like me to add a commit updating the post with this clarification and snippet.

Comment on lines +155 to +189
```js
...
const crypto = require('crypto')

const AGENTS = []
const PUBLIC_JWKS = []

app.post('/register', async (req, res) => {
try {
const url = `${req.protocol}://${req.get('host')}${req.originalUrl}`
const verified = await vestauth.primitives.verify(req.method, url, req.headers, req.body.public_jwk)

// insert agent
const uid = `agent-${crypto.randomBytes(12).toString('hex')}`
const agent = { uid }
AGENTS.push(agent)

// insert public_jwk
const publicJwk = {
agent_uid: agent.uid,
kid: verified.kid,
value: verified.public_jwk
}
PUBLIC_JWKS.push(publicJwk)

// response must be this format
json = {
uid: agent.uid,
kid: publicJwk.kid,
public_jwk: verified.public_jwk,
is_new: true
}

res.json(json)
} catch (err) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the /register example, json = { ... } assigns to an undeclared variable. In Node this can create a global (or throw in strict mode), and it’s a bad pattern to show in docs since readers will copy/paste it.

Suggestion

Declare the response object with const (or let) in the example:

const json = {
  uid: agent.uid,
  kid: publicJwk.kid,
  public_jwk: verified.public_jwk,
  is_new: true
}

res.json(json)

Reply with "@CharlieHelps yes please" if you’d like me to add a commit fixing this in the post.

Comment on lines +56 to +254
## POST /register

Next, add a **/register** endpoint that verifies the incoming signed request.

```js
const vestauth = require('vestauth')

...

app.post('/register', async (req, res) => {
const url = `${req.protocol}://${req.get('host')}${req.originalUrl}`

const verified = await vestauth.primitives.verify(
req.method,
url,
req.headers,
req.body.public_jwk
)

res.json(verified)
})
```

Install the new dependencies.

```sh
$ npm install vestauth --save
```

Restart your server so the new dependency is loaded.

```sh
$ node server.js
Server is running on http://localhost:3000
```

Now test making a POST to /register.

```sh
$ curl -X POST http://localhost:3000/register -d '{}' -H "Content-Type: application/json"
```

You should receive a `MISSING_SIGNATURE_INPUT` error.

```sh
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Error</title>
</head>
<body>
<pre>Error: [MISSING_SIGNATURE_INPUT] missing --signature-input<br> &nbsp; &nbsp;at Errors.missingSignatureInput...</pre>
</body>
</html>
```

Let's catch the error and return a `401` status code.

```js
app.post('/register', async (req, res) => {
try {
const url = `${req.protocol}://${req.get('host')}${req.originalUrl}`

const verified = await vestauth.primitives.verify(
req.method,
url,
req.headers,
req.body.public_jwk
)

res.json(verified)
} catch (err) {
res.status(401).json({ error: { status: 401, code: 401, message: err.message }})
}
})
```

Next, we're ready to initialize our agent.

## Initialize agent

Create a folder to place your agent in.

```sh
$ mkdir your-agent
$ cd your-agent
```

And then initialize your agent. This will call your `/register` endpoint.

```sh
$ vestauth agent init --hostname http://localhost:3000
```

## Add datastore

We'll add a simple in-memory datastore, but you can replace this with a robust datastore like postgres.

```js
...
const crypto = require('crypto')

const AGENTS = []
const PUBLIC_JWKS = []

app.post('/register', async (req, res) => {
try {
const url = `${req.protocol}://${req.get('host')}${req.originalUrl}`
const verified = await vestauth.primitives.verify(req.method, url, req.headers, req.body.public_jwk)

// insert agent
const uid = `agent-${crypto.randomBytes(12).toString('hex')}`
const agent = { uid }
AGENTS.push(agent)

// insert public_jwk
const publicJwk = {
agent_uid: agent.uid,
kid: verified.kid,
value: verified.public_jwk
}
PUBLIC_JWKS.push(publicJwk)

// response must be this format
json = {
uid: agent.uid,
kid: publicJwk.kid,
public_jwk: verified.public_jwk,
is_new: true
}

res.json(json)
} catch (err) {
res.status(401).json({ error: { status: 401, code: 401, message: err.message }})
}
})
```

Great! We can now track registered agents and their public json web keys. (We'll leave it an exercise for the reader to handle upserts and relationships correctly).

## Add `.well-known` endpoint

Next we need to add the endpoint for public key discovery. This is according to the emerging [web-bot-auth](https://datatracker.ietf.org/doc/html/draft-meunier-web-bot-auth-architecture) standard.

```js
...
app.get('/.well-known/http-message-signatures-directory', (req, res) => {
const keys = PUBLIC_JWKS
.filter(jwk => jwk.agent_uid === req.agentUid)
.map(jwk => jwk.value)

res.json({ keys })
})
```

## Add wildcard support

Next, add support for wildcard subdomains. This is according to the [spec](https://datatracker.ietf.org/doc/html/draft-meunier-web-bot-auth-architecture).

Add it to your `app.use()` middleware.

```js
...
const app = express()
app.use(express.json())
app.use((req, res, next) => {
const hostNoPort = (req.headers.host || '').split(':')[0].toLowerCase()

// agent-c235... .localhost
if (hostNoPort.endsWith('.localhost')) {
const sub = hostNoPort.slice(0, -'.localhost'.length) // "agent-c235..."
req.agentUid = sub
return next()
}

next()
})
...
```

## Add `/whoami` endpoint

Add a whoami endpoint to surface the well known endpoint.

```js
...
app.get('/whoami', async (req, res) => {
try {
const url = `${req.protocol}://${req.get('host')}${req.originalUrl}`
const verified = await vestauth.tool.verify(req.method, url, req.headers)

res.json(verified)
} catch(err) {
console.log(err)
res.status(401).json({ error: { status: 401, code: 401, message: err.message }})
}
})
```

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The post alternates between vestauth.primitives.verify(...) and vestauth.tool.verify(...) (later in /whoami). If that’s intentional, it needs a quick explanation; otherwise it reads like a mistake and will lead to copy/paste confusion when readers implement both endpoints.

Suggestion

Either standardize on one verification API in the tutorial or add a short callout explaining the difference (e.g., tool.verify convenience wrapper vs lower-level primitives.verify). If you want to standardize, consider using the same function signature for both endpoints.

Reply with "@CharlieHelps yes please" if you’d like me to add a commit that updates the post for consistency/clarity.

Comment on lines +156 to +192
...
const crypto = require('crypto')

const AGENTS = []
const PUBLIC_JWKS = []

app.post('/register', async (req, res) => {
try {
const url = `${req.protocol}://${req.get('host')}${req.originalUrl}`
const verified = await vestauth.primitives.verify(req.method, url, req.headers, req.body.public_jwk)

// insert agent
const uid = `agent-${crypto.randomBytes(12).toString('hex')}`
const agent = { uid }
AGENTS.push(agent)

// insert public_jwk
const publicJwk = {
agent_uid: agent.uid,
kid: verified.kid,
value: verified.public_jwk
}
PUBLIC_JWKS.push(publicJwk)

// response must be this format
json = {
uid: agent.uid,
kid: publicJwk.kid,
public_jwk: verified.public_jwk,
is_new: true
}

res.json(json)
} catch (err) {
res.status(401).json({ error: { status: 401, code: 401, message: err.message }})
}
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The snippet uses vestauth.primitives.verify(req.method, url, req.headers, req.body.public_jwk) when handling /register. If the request body is {} (as shown in the earlier curl test) req.body.public_jwk is undefined, which can produce a confusing error relative to what you’re trying to demonstrate (missing signature headers).

Because this is a guide, it’s better to either: (a) keep the earlier verbose call where the 4th arg is clearly the public key and add a short note that it’s expected to be present for successful registration, or (b) add a guard that returns 400 when public_jwk is missing so readers get a clear, intentional failure mode.

Suggestion

Add an explicit early check in the snippet to improve the tutorial’s copy/paste experience:

if (!req.body?.public_jwk) {
  return res.status(400).json({
    error: { status: 400, code: 'MISSING_PUBLIC_JWK', message: 'Missing `public_jwk` in request body.' },
  })
}

Reply with "@CharlieHelps yes please" if you’d like me to add a commit updating the snippet accordingly.

Comment thread _config.yml
Comment on lines 45 to 52
markdown: kramdown
kramdown:
input: GFM
syntax_highlighter: none
syntax_highlighter_opts:
disable: true
syntax_highlighter: rouge

highlighter: none
highlighter: rouge

plugins:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Switching kramdown.syntax_highlighter and highlighter to rouge is fine, but it changes generated HTML structure and class names for code blocks across the site. You already updated index.md to use {% highlight %}, but any existing posts/pages that relied on prior <pre> styling may render differently.

Given you also removed .blog-content pre styles, you should verify at least one older post/page with fenced code blocks still looks acceptable (light + dark).

Suggestion

Do a quick audit of existing markdown posts/pages that contain fenced code blocks (```), verify the generated HTML uses Rouge wrappers, and ensure the new CSS covers them. If gaps exist, add a small compatibility selector (e.g. for bare pre > code) or update the markdown to use {% highlight %} consistently.

Reply with "@CharlieHelps yes please" if you’d like me to add a commit that adds minimal compatibility CSS for non-Rouge code blocks.

@charliecreates
charliecreates Bot removed the request for review from CharlieHelps February 23, 2026 03:37
@motdotla
motdotla merged commit 7746f8f into main Feb 23, 2026
@motdotla
motdotla deleted the blog-3 branch February 23, 2026 04:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant