From f492f35f4c091164f8cc26a8f51d6a26bbdf53dc Mon Sep 17 00:00:00 2001 From: Abhishek Doshi Date: Wed, 23 Sep 2026 17:21:55 +0530 Subject: [PATCH] feat: point auth.md and the Link header at the real oauth-authorization-server _shorebird#3031 merged on 2026-09-11 and auth.shorebird.dev now serves real RFC 8414 metadata: issuer, /login, /token, PKCE, the actual scopes (admin, preview_read, zap_preview_publish). Verified live before touching anything. Added an accurate pointer to it in auth.md's Protected resources section and restored the oauth-authorization-server Link header rel, both pointing at auth.shorebird.dev, not a copy on this domain. Deliberately not republishing a copy at docs.shorebird.dev/.well-known/oauth-authorization-server, even though isitagentready.com's oauth-discovery check only looks at the scanned domain's own well-known paths and wouldn't pass otherwise: that is exactly the mistake _shorebird's own review caught and #659 removed from this repo. RFC 8414 resolves the well-known path from the issuer, so a copy here would not be found by standards-based discovery and would silently drift if the real document ever changes. Chose not to re-add the mistake for one check's score. oauth-protected-resource (_shorebird#3043) is still open/unmerged and 404s on api.shorebird.dev, so left untouched. Confirmed no stale scope names (apps:read etc, deleted in #659) remain anywhere in this repo. Also confirmed the real shorebird_mcp_server now exists and is live at mcp.shorebird.dev/mcp (401 on an unauthenticated request, matching its RFC 9728 design), but it backs Shorebird Zap, a prototype documented only on a deliberately unlisted page (src/unlisted.ts) for a limited beta. Not publishing an MCP Server Card for it: that would publicly advertise a capability the product side has deliberately kept out of the sitemap, llms.txt, and search indexes. Cleaned up 6 pre-existing em-dashes in functions/_middleware.ts and one in auth.md while in both files. Verified via wrangler pages dev: the new Link header and auth.md content, and the full existing negotiation scenario suite, against a currently-live page (the earlier /ci/view-logs/ test target from prior sessions was removed in #685 since Shorebird CI is deprecated). --- functions/_middleware.ts | 15 ++++++++------- public/auth.md | 14 ++++++++++---- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/functions/_middleware.ts b/functions/_middleware.ts index b6337790..b5a47725 100644 --- a/functions/_middleware.ts +++ b/functions/_middleware.ts @@ -3,13 +3,13 @@ // Content negotiation for AI agents: a request for a normal docs URL with // `Accept: text/markdown` gets that page's Markdown sibling instead of HTML, // on the same URL a person would visit. The `.md` URLs themselves (added in -// #654) keep working unchanged — this adds the "ask the canonical URL for +// #654) keep working unchanged: this adds the "ask the canonical URL for // Markdown" path on top, which is what agent-friendliness scanners check for. // // Cloudflare's dashboard-level "Markdown for Agents" (AI Crawl Control) does // the same thing, but only for a zone on Cloudflare DNS with a Pro/Business -// plan — shorebird.dev isn't (DNS lives elsewhere, just CNAMed to Pages), so -// this reimplements the negotiation ourselves. Recipe: +// plan, and shorebird.dev isn't (DNS lives elsewhere, just CNAMed to Pages), +// so this reimplements the negotiation ourselves. Recipe: // https://acceptmarkdown.com/recipes/cloudflare-workers import { markdownSiblingPath } from '../src/utils/markdown-path'; @@ -20,7 +20,7 @@ interface Env { // Static assets never have a Markdown or JSON sibling; skip negotiation for // them entirely. `/.well-known/*` is skipped too: those are single-format -// discovery/config files (RFC 8615) with their own declared Content-Type — +// discovery/config files (RFC 8615) with their own declared Content-Type; // they don't participate in the markdown/html/json negotiation this // middleware does for docs pages, and running them through it would 406 // a request that correctly sends that file's own declared Accept type, @@ -81,6 +81,7 @@ const AGENT_LINK_HEADERS = [ '; rel="agent"', '; rel="service-doc"', '; rel="service-desc"; type="application/json"', + '; rel="oauth-authorization-server"', '; rel="alternate"; type="text/plain"', '; rel="search"; type="application/opensearchdescription+xml"', '; rel="author"; type="text/plain"', @@ -117,7 +118,7 @@ const JSON_404_BODY = JSON.stringify( ); // The build already renders a proper Markdown 404 page (via the -// [...slug].md.ts route from #654, since 404.md is a normal docs entry) — +// [...slug].md.ts route from #654, since 404.md is a normal docs entry): // fetch that instead of hand-maintaining a second copy of its link list // here, which would drift from the real page over time. async function fetch404Markdown(assets: Fetcher, url: URL): Promise { @@ -189,8 +190,8 @@ function negotiate(acceptHeader: string | null): Preference { if (markdown < 0 && html < 0 && json < 0) { // Only 406 when the client explicitly rejected everything (an // unqualified `*/*;q=0`). A client that just didn't list one of our - // three representations — e.g. a health check sending - // `Accept: text/plain` — gets the default HTML rather than a hard + // three representations (e.g. a health check sending + // `Accept: text/plain`) gets the default HTML rather than a hard // failure; RFC 9110 §12.5.1 permits serving a non-preferred // representation instead of 406 for exactly this reason. const rejectsEverything = entries.some( diff --git a/public/auth.md b/public/auth.md index f4765ba7..7b1a883b 100644 --- a/public/auth.md +++ b/public/auth.md @@ -1,6 +1,6 @@ -# Shorebird auth.md — Authentication guide for agents and automated tooling +# Shorebird auth.md: Authentication guide for agents and automated tooling This document describes how AI agents, scripts, and CI/CD pipelines authenticate with Shorebird services. @@ -46,15 +46,21 @@ creating an `sb_api_*` API key, as described under the credential issuance step; once issued, it's the only artifact an agent needs to authenticate. -Registration page: `https://console.shorebird.dev` (Account > API Keys) — this -is an authenticated web console, not a callable API. A human (or an agent -driving a browser) must sign in there to issue a key; there is no anonymous, +Registration page: `https://console.shorebird.dev` (Account > API Keys). This is +an authenticated web console, not a callable API: a human (or an agent driving a +browser) must sign in there to issue a key, and there is no anonymous, unauthenticated, or programmatic registration path. ## Protected resources - **API base URL**: `https://api.shorebird.dev/api/v1` - **OpenAPI specification**: `https://api.shorebird.dev/openapi.json` +- **Authorization server metadata (RFC 8414)**: + `https://auth.shorebird.dev/.well-known/oauth-authorization-server`. This is + published by the authorization server itself, not mirrored here: RFC 8414 + resolves it from the issuer (`https://auth.shorebird.dev`), so a copy at this + domain would not be found by standards-based discovery and could drift from + the real one. - **API catalog**: `https://docs.shorebird.dev/.well-known/api-catalog` - **Agent card**: `https://docs.shorebird.dev/.well-known/agent.json` - **Reachability verification**: Verify endpoint status at