Skip to content

docs(jwt): add JWT verification guide for resource servers#118

Merged
appleboy merged 9 commits intomainfrom
worktree-rs256
Mar 21, 2026
Merged

docs(jwt): add JWT verification guide for resource servers#118
appleboy merged 9 commits intomainfrom
worktree-rs256

Conversation

@appleboy
Copy link
Copy Markdown
Member

Summary

  • Add a comprehensive JWT Verification Guide (docs/JWT_VERIFICATION.md) for resource server developers who need to verify AuthGate-issued tokens locally using JWKS public keys (RS256/ES256)
  • Add a condensed web UI version (internal/templates/docs/jwt-verification.md) accessible at /docs/jwt-verification in the sidebar
  • Add JWT Verification link to navbar docs dropdown (both logged-in and logged-out states), README Advanced Topics, and all existing doc pages Related sections
  • Add prism-javascript syntax highlighting to docs pages (also fixes existing auth-code-flow.md JS blocks)

Guide covers

  • OIDC Discovery -> JWKS -> local verification workflow with sequence diagrams
  • Complete code examples in Go (keyfunc + golang-jwt), Python (PyJWT), and Node.js (jose)
  • JWKS caching best practices and key rotation handling
  • Common pitfalls (HS256 + JWKS confusion, missing kid check, accepting refresh tokens, etc.)

Test plan

  • make generate passes
  • make lint passes with 0 issues
  • make test passes (pre-existing build failed on root package due to missing dist static files is unrelated)
  • Visit /docs/jwt-verification - page renders with Mermaid diagram, syntax-highlighted code blocks, and working sidebar navigation
  • Verify navbar Docs dropdown shows JWT Verification for both logged-in and logged-out users
  • Verify Related/Next Steps links on other doc pages point to the new guide

Generated with Claude Code

- Add comprehensive JWT verification guide with JWKS workflow, code examples (Go, Python, Node.js), caching best practices, and key rotation guidance
- Add condensed web UI version accessible at /docs/jwt-verification
- Add JWT Verification link to navbar docs dropdown, README, and all existing doc pages
- Add prism-javascript syntax highlighting for docs pages

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings March 21, 2026 03:15
@codecov
Copy link
Copy Markdown

codecov bot commented Mar 21, 2026

Codecov Report

❌ Patch coverage is 0% with 2 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
internal/templates/props.go 0.00% 2 Missing ⚠️

📢 Thoughts on this report? Let us know!

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds end-user and developer documentation for verifying AuthGate-issued JWTs locally via JWKS (RS256/ES256), and wires the new guide into the web docs UI and repository docs.

Changes:

  • Adds a new JWT verification guide in /docs and a web-rendered version under /docs/jwt-verification.
  • Updates docs navigation (navbar + docs sidebar ordering) and cross-links “Related” sections across existing docs pages.
  • Enables Prism JavaScript syntax highlighting on docs pages.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 16 comments.

Show a summary per file
File Description
internal/templates/navbar_component.templ Adds “JWT Verification” entry to the Docs dropdown (logged-in and logged-out).
internal/templates/docs_page.templ Loads Prism JavaScript grammar for highlighting JS code blocks.
internal/templates/docs/jwt-verification.md Adds new web docs page content for JWT verification via JWKS.
internal/templates/docs/getting-started.md Adds “JWT Verification” to Related links.
internal/templates/docs/device-flow.md Adds “JWT Verification” to Related links.
internal/templates/docs/client-credentials.md Adds “JWT Verification” to Related links.
internal/templates/docs/auth-code-flow.md Adds “JWT Verification” to Related links.
internal/handlers/docs.go Registers jwt-verification slug/title in docs sidebar ordering and routing map.
docs/JWT_VERIFICATION.md Adds comprehensive repository-level JWT verification guide.
docs/CONFIGURATION.md Links to the new JWT verification guide from JWT signing/JWKS config section.
README.md Adds JWT verification guide to Advanced Topics list.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +135 to +151
"scope": "openid profile email",
"type": "access_token",
"exp": 1700000000,
"iat": 1699996400,
"iss": "https://your-authgate",
"sub": "user-uuid",
"jti": "unique-token-id"
}
```

| Claim | Description |
| ----------- | -------------------------------------- |
| `user_id` | User identifier |
| `client_id` | OAuth client that requested the token |
| `scope` | Space-separated granted scopes |
| `type` | `access_token` or `refresh_token` |
| `exp` | Expiration time (Unix timestamp) |
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

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

The example token payload/table uses type: "access_token" / "refresh_token", but AuthGate’s JWT type claim values are "access" and "refresh" (see internal/token/types.go). As written, this would cause resource servers to reject valid tokens if they implement the check literally.

Copilot uses AI. Check for mistakes.
Comment on lines +233 to +253
```json
{
"user_id": "user-uuid",
"client_id": "client-uuid",
"scope": "openid profile email",
"type": "access_token",
"exp": 1700000000,
"iat": 1699996400,
"iss": "https://your-authgate",
"sub": "user-uuid",
"jti": "unique-token-id"
}
```

| Claim | Description |
| ----------- | -------------------------------------- |
| `user_id` | User identifier |
| `client_id` | OAuth client that requested the token |
| `scope` | Space-separated list of granted scopes |
| `type` | `access_token` or `refresh_token` |
| `exp` | Expiration time (Unix timestamp) |
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

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

The example token payload/table uses type: "access_token" / "refresh_token", but AuthGate’s JWT type claim values are "access" and "refresh" (see internal/token/types.go). Readers implementing the doc as-is will incorrectly reject valid tokens.

Copilot uses AI. Check for mistakes.
Comment on lines +3 to +4
This guide explains how **resource servers** (your APIs and microservices) can verify AuthGate-issued JWT tokens locally using public keys, without calling back to AuthGate on every request.

Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

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

The guide emphasizes “no callback to AuthGate” for verification, but AuthGate’s authoritative validation (/oauth/tokeninfo) also checks DB state (revoked/disabled/expired). Please add a brief warning that purely local verification won’t detect revocation until expiry, and point readers to /oauth/tokeninfo if they need real-time revocation enforcement.

Copilot uses AI. Check for mistakes.
Comment on lines +265 to +269
5. **Validate standard claims**:
- `exp` — token is not expired
- `iss` — matches your expected AuthGate URL
- `type` — is `access_token` (not `refresh_token`)
6. **Check authorization** — verify `scope` and `client_id` match your requirements
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

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

AuthGate access tokens use type: "access" (and refresh tokens "refresh"), but this section says to validate type is access_token. Please update to the actual claim values so resource servers don’t implement a failing check.

Copilot uses AI. Check for mistakes.
Comment on lines +446 to +451
// Check token type
if (payload.type !== "access_token") {
res.writeHead(401);
res.end(JSON.stringify({ error: "Not an access token" }));
return;
}
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

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

This Node.js example checks payload.type !== "access_token", but AuthGate access tokens use type: "access". Please update the example to match the real claim values to avoid misleading resource server implementations.

Copilot uses AI. Check for mistakes.
return jsonify({"error": f"Invalid token: {e}"}), 401

# Check token type
if payload.get("type") != "access_token":
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

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

This Python example checks payload.get("type") != "access_token", but AuthGate uses type: "access" for access tokens. Please update the check to the actual claim value so the example doesn’t reject valid tokens.

Suggested change
if payload.get("type") != "access_token":
if payload.get("type") != "access":

Copilot uses AI. Check for mistakes.

- **JWKS empty for HS256** — Switch to RS256 or ES256 for JWKS-based verification
- **Not validating `iss`** — Always check the issuer matches your AuthGate URL
- **Accepting refresh tokens** — Always verify `type` is `access_token`
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

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

This bullet says to verify type is access_token, but AuthGate access tokens use type: "access". Please update to the actual claim value so the pitfall guidance is correct.

Suggested change
- **Accepting refresh tokens** — Always verify `type` is `access_token`
- **Accepting refresh tokens** — Always verify `type` is `access`

Copilot uses AI. Check for mistakes.
Comment on lines +3 to +4
Verify AuthGate-issued JWT tokens at your resource servers using public keys — no callback to AuthGate needed.

Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

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

The intro says no callback to AuthGate is needed, but local verification cannot detect server-side revocation/disabled tokens because AuthGate also enforces token state via DB in /oauth/tokeninfo (TokenService.ValidateToken). Please add a short note about this tradeoff and when to use token introspection/tokeninfo instead of (or in addition to) purely local verification.

Copilot uses AI. Check for mistakes.
}

// Check token type
if claims["type"] != "access_token" {
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

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

This Go example checks claims["type"] != "access_token", but AuthGate sets the JWT type claim to "access" for access tokens. Update the check (and any surrounding prose) to match the real claim values.

Suggested change
if claims["type"] != "access_token" {
if claims["type"] != "access" {

Copilot uses AI. Check for mistakes.
Comment on lines +141 to +148
```json
{
"issuer": "https://your-authgate",
"jwks_uri": "https://your-authgate/.well-known/jwks.json",
"id_token_signing_alg_values_supported": ["RS256"],
"token_endpoint": "https://your-authgate/oauth/token",
"authorization_endpoint": "https://your-authgate/oauth/authorize",
"userinfo_endpoint": "https://your-authgate/oauth/userinfo"
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

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

The OIDC discovery response example hard-codes id_token_signing_alg_values_supported to RS256, but AuthGate returns the configured algorithm (RS256 or ES256). Please make the example/config note reflect that this value changes with JWT_SIGNING_ALGORITHM (and that the field may be omitted if ID tokens aren’t supported).

Copilot uses AI. Check for mistakes.
- Fix type claim values: access_token → access, refresh_token → refresh
- Add revocation tradeoff warning (local verification vs /oauth/tokeninfo)
- Clarify HS256 JWKS behavior (endpoint exists but returns empty key set)
- Note that id_token_signing_alg_values_supported varies with JWT_SIGNING_ALGORITHM

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated 6 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +143 to +151
```json
{
"issuer": "https://your-authgate",
"jwks_uri": "https://your-authgate/.well-known/jwks.json",
"id_token_signing_alg_values_supported": ["RS256"], // Reflects JWT_SIGNING_ALGORITHM (RS256 or ES256)
"token_endpoint": "https://your-authgate/oauth/token",
"authorization_endpoint": "https://your-authgate/oauth/authorize",
"userinfo_endpoint": "https://your-authgate/oauth/userinfo"
}
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

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

This fenced block is labeled as json but contains a // ... comment, which is not valid JSON. Either remove the comment (and explain it in surrounding prose) or change the code fence to a format that permits comments (e.g., jsonc/javascript) to avoid confusing readers and breaking copy/paste.

Copilot uses AI. Check for mistakes.

## Key Rotation

AuthGate supports key rotation with zero-downtime:
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

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

“AuthGate supports key rotation with zero-downtime” is not guaranteed by the described procedure (it requires restarting AuthGate). Please reword to clarify that zero-downtime requires a rolling restart / multiple instances behind a load balancer, or drop the zero-downtime claim.

Suggested change
AuthGate supports key rotation with zero-downtime:
AuthGate supports key rotation. To achieve zero-downtime, run multiple AuthGate instances behind a load balancer and perform a rolling restart:

Copilot uses AI. Check for mistakes.
Comment on lines +508 to +509
> For key management security practices, see the [Security Guide](SECURITY.md#asymmetric-key-management-rs256es256).

Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

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

This link target appears to be broken: SECURITY.md#asymmetric-key-management-rs256es256 doesn’t correspond to a heading anchor in docs/SECURITY.md (the matching text isn’t a markdown heading). Please update the link to a valid anchor, or convert the referenced Security section into a heading so the anchor exists.

Copilot uses AI. Check for mistakes.
Comment on lines +213 to +219
claims := token.Claims.(jwt.MapClaims)
if claims["type"] != "access" {
http.Error(w, "Invalid token type", http.StatusUnauthorized)
return
}

userID := claims["user_id"].(string)
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

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

The Go example uses unchecked type assertions (token.Claims.(jwt.MapClaims) and direct indexing) which can panic if claims aren’t in the expected shape. Since this is guidance for resource servers, it’d be safer to use the claims, ok := ... pattern and handle missing/non-string claim values with a 401 instead of crashing.

Suggested change
claims := token.Claims.(jwt.MapClaims)
if claims["type"] != "access" {
http.Error(w, "Invalid token type", http.StatusUnauthorized)
return
}
userID := claims["user_id"].(string)
claims, ok := token.Claims.(jwt.MapClaims)
if !ok {
http.Error(w, "Invalid token claims", http.StatusUnauthorized)
return
}
tokenType, ok := claims["type"].(string)
if !ok || tokenType != "access" {
http.Error(w, "Invalid token type", http.StatusUnauthorized)
return
}
userID, ok := claims["user_id"].(string)
if !ok {
http.Error(w, "Missing or invalid user_id claim", http.StatusUnauthorized)
return
}

Copilot uses AI. Check for mistakes.
Comment on lines +213 to +219
claims := token.Claims.(jwt.MapClaims)
if claims["type"] != "access" {
http.Error(w, "Invalid token type", http.StatusUnauthorized)
return
}

userID := claims["user_id"].(string)
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

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

claims["user_id"].(string) will panic if user_id is missing or not a string (e.g., token from a different issuer). Consider using a safe type assertion and returning 401 on failure to keep the example robust.

Suggested change
claims := token.Claims.(jwt.MapClaims)
if claims["type"] != "access" {
http.Error(w, "Invalid token type", http.StatusUnauthorized)
return
}
userID := claims["user_id"].(string)
claims, ok := token.Claims.(jwt.MapClaims)
if !ok {
http.Error(w, "Invalid token claims", http.StatusUnauthorized)
return
}
tokenType, ok := claims["type"].(string)
if !ok || tokenType != "access" {
http.Error(w, "Invalid token type", http.StatusUnauthorized)
return
}
userID, ok := claims["user_id"].(string)
if !ok {
http.Error(w, "Invalid or missing user_id claim", http.StatusUnauthorized)
return
}

Copilot uses AI. Check for mistakes.
Comment on lines +283 to +288
"context"
"fmt"
"log"
"net/http"
"strings"
"time"
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

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

The Go example won’t compile as written: context and time are imported but never used. Please remove unused imports or update the snippet to actually use them (e.g., context-aware JWKS refresh options) so copy/paste works.

Suggested change
"context"
"fmt"
"log"
"net/http"
"strings"
"time"
"fmt"
"log"
"net/http"
"strings"

Copilot uses AI. Check for mistakes.
- Remove invalid JSON comment, move note to surrounding prose
- Fix zero-downtime claim: clarify rolling restart requirement
- Fix broken SECURITY.md anchor link
- Add safe type assertions in Go examples to prevent panics
- Remove unused imports (context, time) from Go example

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@appleboy appleboy requested review from Copilot and removed request for Copilot March 21, 2026 03:31
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated 11 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +282 to +289
import (
"fmt"
"log"
"net/http"
"strings"

"github.com/MicahParks/keyfunc/v3"
"github.com/golang-jwt/jwt/v5"
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

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

The Go example won’t compile as written: it imports context and time but neither is used in the snippet. Remove unused imports (or add code that uses them) so copy/paste works cleanly.

Copilot uses AI. Check for mistakes.
// Parse and verify the JWT
token, err := jwt.Parse(tokenString, k.Keyfunc,
jwt.WithIssuer("https://your-authgate"),
jwt.WithExpirationRequired(),
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

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

The Go verification example should explicitly restrict accepted JWT algorithms (e.g., to RS256/ES256) when parsing. Relying on whatever alg appears in the header is a common footgun for copy/paste consumers; add a parser option that enforces an allowlist of expected algorithms.

Suggested change
jwt.WithExpirationRequired(),
jwt.WithExpirationRequired(),
jwt.WithValidMethods([]string{"RS256", "ES256"}),

Copilot uses AI. Check for mistakes.
Comment on lines +334 to +341
scopeStr, _ := claims["scope"].(string)
scopes := strings.Fields(scopeStr)
if !contains(scopes, "read") {
http.Error(w, "Insufficient scope", http.StatusForbidden)
return
}

userID, _ := claims["user_id"].(string)
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

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

This sample uses unchecked type assertions (claims["scope"].(string), claims["user_id"].(string)), which can panic on malformed tokens or unexpected claim shapes. Prefer checked assertions / claims["..."] extraction with ok handling so the example fails closed with 401/403 instead of crashing the server.

Suggested change
scopeStr, _ := claims["scope"].(string)
scopes := strings.Fields(scopeStr)
if !contains(scopes, "read") {
http.Error(w, "Insufficient scope", http.StatusForbidden)
return
}
userID, _ := claims["user_id"].(string)
scopeStr, ok := claims["scope"].(string)
if !ok || scopeStr == "" {
http.Error(w, "Insufficient scope", http.StatusForbidden)
return
}
scopes := strings.Fields(scopeStr)
if !contains(scopes, "read") {
http.Error(w, "Insufficient scope", http.StatusForbidden)
return
}
userID, ok := claims["user_id"].(string)
if !ok || userID == "" {
http.Error(w, "Invalid token claims", http.StatusUnauthorized)
return
}

Copilot uses AI. Check for mistakes.
Comment on lines +149 to +157
| `user_id` | User identifier |
| `client_id` | OAuth client that requested the token |
| `scope` | Space-separated granted scopes |
| `type` | `access` or `refresh` |
| `exp` | Expiration time (Unix timestamp) |
| `iss` | Issuer URL (AuthGate's BASE_URL) |
| `sub` | Subject (same as user_id) |
| `jti` | Unique token identifier (UUID) |

Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

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

Access tokens for client_credentials use a synthetic subject ("client:") rather than a user UUID, so the payload/table shouldn’t imply user_id/sub are always a human user identifier. Please clarify this edge case to prevent incorrect downstream authorization decisions.

Suggested change
| `user_id` | User identifier |
| `client_id` | OAuth client that requested the token |
| `scope` | Space-separated granted scopes |
| `type` | `access` or `refresh` |
| `exp` | Expiration time (Unix timestamp) |
| `iss` | Issuer URL (AuthGate's BASE_URL) |
| `sub` | Subject (same as user_id) |
| `jti` | Unique token identifier (UUID) |
| `user_id` | End-user identifier when the token represents a user; may be absent for `client_credentials` tokens |
| `client_id` | OAuth client that requested the token |
| `scope` | Space-separated granted scopes |
| `type` | `access` or `refresh` |
| `exp` | Expiration time (Unix timestamp) |
| `iss` | Issuer URL (AuthGate's BASE_URL) |
| `sub` | Subject identifier: user UUID for user tokens, or `client:<client_id>` for `client_credentials` tokens |
| `jti` | Unique token identifier (UUID) |
> **Note:** For access tokens issued via the `client_credentials` grant, there is no end user. In that case, `sub` will be a synthetic client subject (`client:<client_id>`), and `user_id` is omitted.

Copilot uses AI. Check for mistakes.
// Parse and verify the JWT using JWKS
token, err := jwt.Parse(tokenString, k.Keyfunc,
jwt.WithIssuer("https://your-authgate"),
jwt.WithExpirationRequired(),
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

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

The Go snippet should restrict accepted JWT algorithms (RS256/ES256) during parsing. Without an explicit allowlist, copy/paste consumers may inadvertently accept unexpected alg values depending on library defaults and key selection behavior.

Suggested change
jwt.WithExpirationRequired(),
jwt.WithExpirationRequired(),
jwt.WithValidMethods([]string{"RS256", "ES256"}),

Copilot uses AI. Check for mistakes.
Comment on lines +235 to +258
```json
{
"user_id": "user-uuid",
"client_id": "client-uuid",
"scope": "openid profile email",
"type": "access",
"exp": 1700000000,
"iat": 1699996400,
"iss": "https://your-authgate",
"sub": "user-uuid",
"jti": "unique-token-id"
}
```

| Claim | Description |
| ----------- | -------------------------------------- |
| `user_id` | User identifier |
| `client_id` | OAuth client that requested the token |
| `scope` | Space-separated list of granted scopes |
| `type` | `access` or `refresh` |
| `exp` | Expiration time (Unix timestamp) |
| `iat` | Issued-at time (Unix timestamp) |
| `iss` | Issuer URL (AuthGate's `BASE_URL`) |
| `sub` | Subject (same as `user_id`) |
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

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

JWT access tokens issued by AuthGate set user_id/sub to the token’s subject. For client_credentials, the subject is a synthetic identity like client:<clientID> (see LocalTokenProvider comment). The example payload/table currently implies sub/user_id are always a user UUID; please clarify this to avoid resource servers incorrectly assuming a human user is always present.

Copilot uses AI. Check for mistakes.
try {
// Verify the JWT using JWKS (auto-fetched and cached)
const { payload } = await jwtVerify(token, JWKS, {
issuer: AUTHGATE_URL,
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

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

In the jose example, jwtVerify should be configured with an explicit algorithm allowlist (e.g., RS256/ES256) rather than accepting whatever alg is present in the JWT header. This reduces the risk of alg-confusion issues for developers copying the snippet.

Suggested change
issuer: AUTHGATE_URL,
issuer: AUTHGATE_URL,
algorithms: ["RS256", "ES256"],

Copilot uses AI. Check for mistakes.
Comment on lines +486 to +507
AuthGate supports key rotation. To achieve zero-downtime, run multiple AuthGate instances behind a load balancer and perform a rolling restart:

1. **Generate a new key pair** (see [Configuring AuthGate](#configuring-authgate))
2. **Update `JWT_PRIVATE_KEY_PATH`** (and optionally `JWT_KEY_ID`) in AuthGate's configuration
3. **Restart AuthGate** — new tokens are signed with the new key; the JWKS endpoint serves the new public key
4. **Resource servers adapt automatically** — tokens with an unknown `kid` trigger a JWKS re-fetch

### Timeline

```
T+0: AuthGate restarts with new key
T+0: New tokens signed with new kid
T+0: JWKS endpoint serves new public key
T+0~1h: Resource servers with cached old JWKS re-fetch on unknown kid
T+1h: All old access tokens have expired (default expiry = 1 hour)
```

### Limitations

- AuthGate serves **a single active public key** in the JWKS response (multi-key JWKS is not currently supported)
- During rotation, resource servers that don't handle unknown `kid` gracefully may reject new tokens until their JWKS cache expires

Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

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

The Key Rotation section claims “zero-downtime”, but AuthGate’s JWKS handler is built at startup and serves only the single current public key. After rotation, any still-unexpired tokens signed with the previous key will fail verification once a resource server refreshes its JWKS cache (periodic refresh, restart, etc.). Please adjust the wording/steps to reflect this limitation and provide an operationally safe rotation strategy (e.g., rotate only after old access tokens have expired, or note that multi-key JWKS is required for true overlap).

Copilot uses AI. Check for mistakes.
Comment on lines +291 to +295
if (!auth.startsWith("Bearer ")) {
res.writeHead(401);
res.end(JSON.stringify({ error: "Missing Bearer token" }));
return;
}
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

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

The Node jose example should set an explicit algorithm allowlist in jwtVerify options (e.g., RS256/ES256). This prevents consumers from accidentally accepting tokens signed with unexpected algorithms if they copy the snippet.

Copilot uses AI. Check for mistakes.
Comment on lines +331 to +336
1. Generate a new key pair and update `JWT_PRIVATE_KEY_PATH` in AuthGate
2. Restart AuthGate — new tokens are signed with the new key
3. Resource servers detect the unknown `kid` and re-fetch JWKS automatically

> AuthGate currently serves a single active key. During rotation, allow up to 1 hour for cached JWKS to expire at resource servers.

Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

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

This rotation guidance implies seamless behavior, but AuthGate serves only one active key in JWKS. Once a resource server refreshes JWKS after rotation, it will no longer be able to verify still-unexpired tokens signed by the old key. Please clarify the limitation and recommend a safe rotation procedure (rotate after old access tokens expire, or note that multi-key JWKS is needed for overlap).

Suggested change
1. Generate a new key pair and update `JWT_PRIVATE_KEY_PATH` in AuthGate
2. Restart AuthGate — new tokens are signed with the new key
3. Resource servers detect the unknown `kid` and re-fetch JWKS automatically
> AuthGate currently serves a single active key. During rotation, allow up to 1 hour for cached JWKS to expire at resource servers.
> **Important limitation**: AuthGate's JWKS endpoint exposes **only one active key at a time**. Once you rotate keys and a resource server refreshes JWKS, it will **no longer be able to verify still-unexpired tokens** that were signed with the old key.
>
> Seamless, overlapping rotation (where both old and new keys are accepted during a transition period) would require AuthGate to serve **multiple keys in JWKS simultaneously**, which is not currently supported.
### Safe rotation procedure (single-key JWKS)
To avoid breaking verification for valid tokens:
1. Determine your **maximum access token lifetime** (e.g., 15 minutes, 1 hour).
2. Choose a rotation time and ensure that, **for at least one full token lifetime before rotation**, no new tokens are issued with the old key (for example, by scheduling rotation after a planned downtime or after disabling issuance to affected clients).
3. After you are confident that all access tokens signed with the old key have expired, generate a new key pair and update `JWT_PRIVATE_KEY_PATH` in AuthGate.
4. Restart AuthGate — new tokens are now signed with the new key, and the new public key is served via JWKS.
5. Resource servers will continue to verify existing tokens with the current JWKS; when they next refresh JWKS, **all still-active tokens should already be signed with the new key**.
> If you rotate the key **before** all old access tokens have expired, any resource server that refreshes JWKS will start rejecting those old-but-unexpired tokens. Plan rotations accordingly, or wait for multi-key JWKS support if you require fully seamless overlap.

Copilot uses AI. Check for mistakes.
- Add WithValidMethods to restrict accepted JWT algorithms in Go example
- Add algorithms allowlist to Node.js jose example
- Strengthen all Go type assertions with ok checks and error handling
- Clarify client_credentials tokens: sub uses synthetic client subject, user_id may be absent

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated 5 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +147 to +156
| Claim | Description |
| ----------- | -------------------------------------- |
| `user_id` | End-user identifier; may be absent for `client_credentials` tokens |
| `client_id` | OAuth client that requested the token |
| `scope` | Space-separated granted scopes |
| `type` | `access` or `refresh` |
| `exp` | Expiration time (Unix timestamp) |
| `iss` | Issuer URL (AuthGate's BASE_URL) |
| `sub` | Subject: user UUID for user tokens, or `client:<client_id>` for `client_credentials` tokens |
| `jti` | Unique token identifier (UUID) |
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

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

The claims table says user_id “may be absent for client_credentials tokens”, but AuthGate access token JWTs always include user_id (for client_credentials it is set to the synthetic machine identity client:<client_id>). Please update the claim description (and any surrounding text) to match actual token claims so resource servers don’t implement incorrect parsing logic.

Copilot uses AI. Check for mistakes.
Comment on lines +228 to +233
userID, ok := claims["user_id"].(string)
if !ok || userID == "" {
http.Error(w, "Invalid token claims", http.StatusUnauthorized)
return
}
fmt.Fprintf(w, "Hello, user %s!", userID)
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

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

This Go example rejects tokens when user_id is missing/empty and treats it as an end-user identifier. In AuthGate, client_credentials access tokens use user_id/sub like client:<client_id> (no end user). Consider adjusting the example to handle both cases (user token vs client token), e.g., use sub for a generic subject and/or branch on the client: prefix instead of requiring an end-user ID.

Suggested change
userID, ok := claims["user_id"].(string)
if !ok || userID == "" {
http.Error(w, "Invalid token claims", http.StatusUnauthorized)
return
}
fmt.Fprintf(w, "Hello, user %s!", userID)
// Use "sub" as the generic subject. AuthGate may encode client credentials as "client:<client_id>".
subject, _ := claims["sub"].(string)
if subject == "" {
// Fallback to "user_id" for older tokens
subject, _ = claims["user_id"].(string)
}
if subject == "" {
http.Error(w, "Invalid token claims: missing subject", http.StatusUnauthorized)
return
}
if strings.HasPrefix(subject, "client:") {
clientID := strings.TrimPrefix(subject, "client:")
if clientID == "" {
http.Error(w, "Invalid token claims: malformed client subject", http.StatusUnauthorized)
return
}
fmt.Fprintf(w, "Hello, client %s!", clientID)
return
}
fmt.Fprintf(w, "Hello, user %s!", subject)

Copilot uses AI. Check for mistakes.
}
```

> The `jwks_uri` field is only present when RS256 or ES256 is configured. The `id_token_signing_alg_values_supported` value reflects the configured `JWT_SIGNING_ALGORITHM` (e.g., `["ES256"]` when using ES256).
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

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

The note implies id_token_signing_alg_values_supported is always present when RS256/ES256 is configured, but the discovery handler omits this field when ID tokens aren’t supported. Please mention it may be absent (similar to docs/JWT_VERIFICATION.md) so readers don’t rely on it unconditionally.

Suggested change
> The `jwks_uri` field is only present when RS256 or ES256 is configured. The `id_token_signing_alg_values_supported` value reflects the configured `JWT_SIGNING_ALGORITHM` (e.g., `["ES256"]` when using ES256).
> The `jwks_uri` field is only present when RS256 or ES256 is configured. When present, the `id_token_signing_alg_values_supported` value reflects the configured `JWT_SIGNING_ALGORITHM` (e.g., `["ES256"]` when using ES256), but this field may be omitted entirely when ID tokens are not supported, so clients should not rely on it unconditionally.

Copilot uses AI. Check for mistakes.
Comment on lines +249 to +262
| Claim | Description |
| ----------- | -------------------------------------- |
| `user_id` | End-user identifier; may be absent for `client_credentials` tokens |
| `client_id` | OAuth client that requested the token |
| `scope` | Space-separated list of granted scopes |
| `type` | `access` or `refresh` |
| `exp` | Expiration time (Unix timestamp) |
| `iat` | Issued-at time (Unix timestamp) |
| `iss` | Issuer URL (AuthGate's `BASE_URL`) |
| `sub` | Subject: user UUID for user tokens, or `client:<client_id>` for `client_credentials` tokens |
| `jti` | Unique token identifier (UUID) |

> **Note:** For access tokens issued via the `client_credentials` grant, there is no end user. In that case, `sub` is a synthetic client subject (`client:<client_id>`), and `user_id` may be omitted.

Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

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

The claims table states user_id may be absent for client_credentials tokens, but AuthGate access token JWTs always include user_id; for client_credentials it is set to the synthetic machine identity client:<client_id>. Please update this row (and the note below it) to reflect actual behavior so resource servers parse subjects correctly.

Copilot uses AI. Check for mistakes.
Comment on lines +348 to +353
userID, ok := claims["user_id"].(string)
if !ok || userID == "" {
http.Error(w, "Invalid token claims", http.StatusUnauthorized)
return
}
fmt.Fprintf(w, "Hello, user %s!", userID)
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

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

The examples assume user_id represents an end user (and error if it’s missing). For client_credentials, AuthGate uses a machine identity (client:<client_id>) in user_id/sub, so example resource servers should either treat sub as the subject generically or branch on the client: prefix rather than requiring an end-user. Updating the example avoids confusing M2M consumers.

Suggested change
userID, ok := claims["user_id"].(string)
if !ok || userID == "" {
http.Error(w, "Invalid token claims", http.StatusUnauthorized)
return
}
fmt.Fprintf(w, "Hello, user %s!", userID)
// Determine the subject of the token (user or client)
var subject string
if sub, ok := claims["sub"].(string); ok && sub != "" {
subject = sub
} else if userID, ok := claims["user_id"].(string); ok && userID != "" {
subject = userID
}
if subject == "" {
http.Error(w, "Invalid token claims: missing subject", http.StatusUnauthorized)
return
}
// Example: differentiate between user and client subjects
if strings.HasPrefix(subject, "client:") {
fmt.Fprintf(w, "Hello, client %s!", strings.TrimPrefix(subject, "client:"))
} else {
fmt.Fprintf(w, "Hello, user %s!", subject)
}

Copilot uses AI. Check for mistakes.
- Fix client_credentials claim docs: user_id is synthetic "client:<id>", not absent
- Use sub claim in Go example for universal user/client handling
- Clarify id_token_signing_alg_values_supported may be omitted when ID tokens unsupported

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated 1 comment.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

- AuthGate serves **a single active public key** in the JWKS response (multi-key JWKS is not currently supported)
- During rotation, resource servers that don't handle unknown `kid` gracefully may reject new tokens until their JWKS cache expires

> For key management security practices, see the [Security Guide](SECURITY.md#asymmetric-key-management-rs256es256).
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

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

The link to SECURITY.md#asymmetric-key-management-rs256es256 appears to be a broken anchor. In docs/SECURITY.md the “Asymmetric Key Management (RS256/ES256)” text is not a markdown heading, so it won’t generate that fragment identifier. Consider either changing this link to an existing heading (e.g. SECURITY.md#secrets-management) or converting the target section in SECURITY.md into a proper heading so the anchor exists.

Suggested change
> For key management security practices, see the [Security Guide](SECURITY.md#asymmetric-key-management-rs256es256).
> For key management security practices, see the [Security Guide](SECURITY.md#secrets-management).

Copilot uses AI. Check for mistakes.
…ction

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated 4 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

JWKS_URL = f"{AUTHGATE_URL}/.well-known/jwks.json"

# PyJWKClient caches JWKS keys automatically
jwks_client = PyJWKClient(JWKS_URL, cache_jwk_set=True, lifespan=3600)
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

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

The Python example uses PyJWKClient(JWKS_URL, cache_jwk_set=True, lifespan=3600), but cache_jwk_set is not a supported constructor argument in common PyJWT versions (the caching flag is typically cache_keys). As written, this snippet is likely to raise a TypeError for readers; update the example to use the correct PyJWT constructor parameters (or omit the unsupported flag and rely on the default caching behavior).

Suggested change
jwks_client = PyJWKClient(JWKS_URL, cache_jwk_set=True, lifespan=3600)
jwks_client = PyJWKClient(JWKS_URL, cache_keys=True, lifespan=3600)

Copilot uses AI. Check for mistakes.
JWKS_URL = f"{AUTHGATE_URL}/.well-known/jwks.json"

# PyJWKClient caches JWKS keys automatically
jwks_client = PyJWKClient(JWKS_URL, cache_jwk_set=True, lifespan=3600)
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

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

The Python example uses PyJWKClient(JWKS_URL, cache_jwk_set=True, lifespan=3600), but cache_jwk_set is not a supported constructor argument in common PyJWT versions (the caching flag is typically cache_keys). As written, this snippet is likely to raise a TypeError for readers; update the example to use the correct PyJWT constructor parameters (or omit the unsupported flag and rely on the default caching behavior).

Suggested change
jwks_client = PyJWKClient(JWKS_URL, cache_jwk_set=True, lifespan=3600)
jwks_client = PyJWKClient(JWKS_URL, cache_keys=True, lifespan=3600)

Copilot uses AI. Check for mistakes.
Comment on lines +27 to +32
@NavDropdown("Docs", props.ActiveLink == "docs-getting-started" || props.ActiveLink == "docs-device-flow" || props.ActiveLink == "docs-auth-code-flow" || props.ActiveLink == "docs-client-credentials" || props.ActiveLink == "docs-jwt-verification") {
@NavDropdownItem("/docs/getting-started", "Getting Started", props.ActiveLink == "docs-getting-started", false, false)
@NavDropdownItem("/docs/device-flow", "Device Flow", props.ActiveLink == "docs-device-flow", false, false)
@NavDropdownItem("/docs/auth-code-flow", "Auth Code Flow", props.ActiveLink == "docs-auth-code-flow", false, false)
@NavDropdownItem("/docs/client-credentials", "Client Credentials", props.ActiveLink == "docs-client-credentials", false, false)
@NavDropdownItem("/docs/jwt-verification", "JWT Verification", props.ActiveLink == "docs-jwt-verification", false, false)
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

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

The docs dropdown active-state check is an ever-growing hardcoded OR list and is duplicated for logged-in vs logged-out nav. Since docs pages already set ActiveLink to "docs-"+slug, this can be simplified to a single prefix check (e.g., any ActiveLink that starts with docs-), reducing the chance of future docs pages not activating the dropdown.

Copilot uses AI. Check for mistakes.
Comment on lines +52 to +57
@NavDropdown("Docs", props.ActiveLink == "docs-getting-started" || props.ActiveLink == "docs-device-flow" || props.ActiveLink == "docs-auth-code-flow" || props.ActiveLink == "docs-client-credentials" || props.ActiveLink == "docs-jwt-verification") {
@NavDropdownItem("/docs/getting-started", "Getting Started", props.ActiveLink == "docs-getting-started", false, false)
@NavDropdownItem("/docs/device-flow", "Device Flow", props.ActiveLink == "docs-device-flow", false, false)
@NavDropdownItem("/docs/auth-code-flow", "Auth Code Flow", props.ActiveLink == "docs-auth-code-flow", false, false)
@NavDropdownItem("/docs/client-credentials", "Client Credentials", props.ActiveLink == "docs-client-credentials", false, false)
@NavDropdownItem("/docs/jwt-verification", "JWT Verification", props.ActiveLink == "docs-jwt-verification", false, false)
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

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

Same as above: the logged-out navbar duplicates a hardcoded OR list for docs active-state. Consider switching to a simple docs- prefix check (consistent with how docs pages set ActiveLink) to avoid needing to update this list for every new docs page.

Copilot uses AI. Check for mistakes.
- Fix Python PyJWKClient param: cache_jwk_set → cache_keys
- Simplify navbar docs dropdown active check with IsDocsActive() helper
  to avoid hardcoded OR list that must be updated for each new docs page

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 12 out of 12 changed files in this pull request and generated 1 comment.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +21 to +27
@@ -22,6 +22,11 @@ type NavbarProps struct {
PendingClientsCount int // Badge count for admin → OAuth Clients link
}

// IsDocsActive returns true if the current ActiveLink belongs to a docs page.
func (p *NavbarProps) IsDocsActive() bool {
return len(p.ActiveLink) > 5 && p.ActiveLink[:5] == "docs-"
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

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

IsDocsActive hard-codes the prefix length (> 5 / [:5]) and duplicates prefix logic in a less readable way. Consider switching to strings.HasPrefix(p.ActiveLink, "docs-") (no magic number / slicing), and update the ActiveLink field comment since it can also be docs-<slug> now (not just the non-docs values listed).

Copilot uses AI. Check for mistakes.
…comment

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@appleboy appleboy requested a review from Copilot March 21, 2026 04:13
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 12 out of 12 changed files in this pull request and generated 1 comment.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 21 to +28
IsAdmin bool
ActiveLink string // "device", "sessions", "clients", "audit"
ActiveLink string // e.g. "device", "sessions", "clients", "audit", "docs-<slug>"
PendingClientsCount int // Badge count for admin → OAuth Clients link
}

// IsDocsActive returns true if the current ActiveLink belongs to a docs page.
func (p *NavbarProps) IsDocsActive() bool {
return strings.HasPrefix(p.ActiveLink, "docs-")
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

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

IsDocsActive uses a hard-coded length check/slice (len(...) > 5 and ActiveLink[:5] == "docs-"). This is a bit brittle and also makes the > 5 check inconsistent with the 5-char prefix. Consider switching to strings.HasPrefix(p.ActiveLink, "docs-") (and/or using len("docs-")) and updating the ActiveLink field comment above to reflect that it can contain docs-<slug> values as well.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 12 out of 12 changed files in this pull request and generated 1 comment.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +499 to +503
AuthGate supports key rotation. To achieve zero-downtime, run multiple AuthGate instances behind a load balancer and perform a rolling restart:

1. **Generate a new key pair** (see [Configuring AuthGate](#configuring-authgate))
2. **Update `JWT_PRIVATE_KEY_PATH`** (and optionally `JWT_KEY_ID`) in AuthGate's configuration
3. **Restart AuthGate** — new tokens are signed with the new key; the JWKS endpoint serves the new public key
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

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

The Key Rotation section claims “To achieve zero-downtime” with a rolling restart behind a load balancer, but AuthGate’s JWKS currently serves only a single active key. During a rolling restart with mixed old/new instances, clients may receive tokens signed with one key while fetching JWKS from an instance serving the other key, causing intermittent verification failures. Please reword this to avoid promising zero-downtime, or clarify the extra coordination required (e.g., keep all instances on the same signing key until cutover, pre-warm resource-server JWKS caches, or implement multi-key JWKS support).

Copilot uses AI. Check for mistakes.
Remove zero-downtime claim; explain that all instances must switch
simultaneously and resource servers should re-fetch JWKS on unknown kid.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 12 out of 12 changed files in this pull request and generated no new comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@appleboy appleboy merged commit 15076c7 into main Mar 21, 2026
20 checks passed
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.

2 participants