Conversation
There was a problem hiding this comment.
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
rougeviakramdown.syntax_highlighterandhighlighter. - Global code styling: adds a sizable block of CSS in
_includes/layouts/meta.htmlto 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 removestext-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.mdand an associated imageassets/img/blog/blog-3.png.
| 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; | ||
| } |
There was a problem hiding this comment.
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.
|
|
||
| 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; | ||
| } | ||
| } |
There was a problem hiding this comment.
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.
| ```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 }) | ||
| }) | ||
| ``` |
There was a problem hiding this comment.
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.
| ```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) { |
There was a problem hiding this comment.
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.
| ## 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> 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 }}) | ||
| } | ||
| }) | ||
| ``` |
There was a problem hiding this comment.
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.
| ... | ||
| 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 }}) | ||
| } | ||
| }) |
There was a problem hiding this comment.
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.
| markdown: kramdown | ||
| kramdown: | ||
| input: GFM | ||
| syntax_highlighter: none | ||
| syntax_highlighter_opts: | ||
| disable: true | ||
| syntax_highlighter: rouge | ||
|
|
||
| highlighter: none | ||
| highlighter: rouge | ||
|
|
||
| plugins: |
There was a problem hiding this comment.
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.
No description provided.