# auth.md

> Agent access guide for Max Song — https://maxsong.me. Read this before calling anything on this origin. It follows the auth.md convention (https://github.com/workos/auth.md): discover → pick a method → register → claim → exchange → use → errors → revocation.

You are an agent. This origin is a **public, read-only personal website**: who Max Song is, the websites he built, a timeline of what he has worked on, and his music. Everything a visitor can read is served anonymously over plain `GET` — no account, no credential. The one exception is the **media API** behind the https://maxsong.me/sound/ player (the music library manifest, audio and analysis files): it accepts an OAuth 2.1 access token from this origin's own authorization server, and that server issues tokens **only when the site owner signs in and approves the request**. If you are not acting for Max Song, the anonymous method is the whole story. Follow the steps in order; each one is short.

## Audience

- Assistants and answer engines answering questions about Max Song, his projects, Journal entries, or tracks.
- Search and AI crawlers. Public pages and discovery files are open to all bots, without authentication or a verified crawler identity. Crawl permissions are declared in `/robots.txt`. Its AI usage preference lines are quoted below with comments and surrounding whitespace removed; a line that starts with a path applies under that path only, and the longest matching path wins (Content-Usage defines this; path-scoped Content-Signal lines are not standardised). `/.well-known/tdmrep.json` (W3C TDMRep) reserves text and data mining on the same paths.
  - `Content-Signal: search=yes, ai-input=yes, ai-train=yes`
  - `Content-Signal: /assets/ search=no, ai-input=no, ai-train=no`
  - `Content-Signal: /images/generated/third-party- search=yes, ai-input=yes, ai-train=no`
  - `Content-Usage: /assets/ train-ai=n, ai-use=n, search=n`
  - `Content-Usage: /images/generated/third-party- train-ai=n`
- Agents acting for a person who wants to read a page, subscribe to the feed, or get in touch.
- Agents acting for the site owner that need the music library itself — the only audience of the OAuth flow.

## Step 1 — Discover

Every discovery document is an anonymous `GET` on this origin:

| URL | What it is |
| --- | --- |
| https://maxsong.me/llms.txt | Site summary in llms.txt form: identity, live projects, tools & research, agent skills, papers, journal, sound, API, contact |
| https://maxsong.me/sitemap.xml | Every canonical page |
| https://maxsong.me/rss.xml | One item per Journal entry, linking to its place on the timeline |
| https://maxsong.me/robots.txt | Crawl rules and AI usage preferences (Content-Signal, Content-Usage) |
| https://maxsong.me/.well-known/tdmrep.json | W3C TDMRep: paths whose text and data mining rights are reserved |
| https://maxsong.me/.well-known/api-catalog | RFC 9727 API catalog of the public content API (OpenAPI 3.1 + docs) |
| https://maxsong.me/.well-known/oauth-protected-resource | RFC 9728 Protected Resource Metadata of the media API: which authorization server, which scope |
| https://maxsong.me/.well-known/oauth-authorization-server | RFC 8414 Authorization Server Metadata: every OAuth endpoint below, plus the agent_auth block |
| https://maxsong.me/auth.md | This document |

Any HTML page is also served as Markdown by content negotiation: send `Accept: text/markdown`.

The two `/.well-known/oauth-*` documents are served live by the authorization server itself (issuer https://maxsong.me); every endpoint they name exists and answers. There is no OpenID Connect provider here (no `/.well-known/openid-configuration`, no ID tokens): the server authorizes access to the media API, it does not assert identities to third parties.

## Step 2 — Pick a method

Two access methods exist:

- **anonymous** — for pages, feeds, discovery files and the content API described in https://maxsong.me/.well-known/api-catalog. No credential is needed and none is honoured. Almost every agent stops here.
- **oauth** — for the media API only (Step 6). OAuth 2.1 authorization code flow with PKCE (S256), scope `media:read`, resource `https://maxsong.me`. The client registers anonymously (Step 3); the credential is granted only after the site owner approves in a browser (Step 4). Anyone else is refused at the authorization page — do not start this flow unless you act for the site owner.

Whatever you already hold:

- You hold an ID-JAG, a verified email, an API key, or a token from another issuer → keep it. Nothing on this origin accepts it; the only credential honoured here is an access token from this origin's own token endpoint.
- You hold nothing and want public content → you already have everything required.

## Step 3 — Register

**anonymous:** Not applicable. There is no sign-up, provisioning, or identity endpoint for the public site, and nothing to submit.

**oauth:** Dynamic client registration (RFC 7591). `POST https://maxsong.me/oauth/register` with a JSON body such as `{"client_name": "…", "redirect_uris": ["…"], "token_endpoint_auth_method": "none", "grant_types": ["authorization_code", "refresh_token"], "response_types": ["code"]}` → `201` with your `client_id`. Clients are public (no secret is issued; PKCE is mandatory) and expire after 90 days. Loopback redirect URIs (`http://127.0.0.1:<any port>/…`, `http://localhost:<any port>/…`) are accepted with any port. A Client ID Metadata Document URL may be used as `client_id` instead of registering.

## Step 4 — Claim ceremony

**anonymous:** Not applicable. There is no credential to claim and no user code to relay to anyone.

**oauth:** The claim is the site owner's approval. Build the authorization URL from `https://maxsong.me/oauth/authorize` with `response_type=code`, `client_id`, `redirect_uri`, `state`, `code_challenge` + `code_challenge_method=S256`, `scope=media:read` and `resource=https://maxsong.me`, then open it in the site owner's browser (or show it to them). Cloudflare Access asks them for a one-time PIN sent to their email; a consent page then shows your client name, redirect URI and scope, and they approve or deny. On approval the browser is redirected to your `redirect_uri` with `code`, `state` and `iss=https://maxsong.me`. The code is single-use and short-lived. Anyone who is not the site owner cannot pass the sign-in and no code is ever issued to them.

## Step 5 — Exchange the assertion

**anonymous:** Not applicable. There is no token endpoint to call.

**oauth:** `POST https://maxsong.me/oauth/token` (form-encoded) with `grant_type=authorization_code`, `code`, `redirect_uri`, `client_id`, `code_verifier` and `resource=https://maxsong.me` → JSON with `access_token` (opaque, valid 1 hour), `refresh_token` (valid 30 days, rotates on use), `token_type`, `expires_in` and `scope`. Renew with `grant_type=refresh_token`. Tokens are bound to the resource `https://maxsong.me` and carry the `media:read` scope — nothing else can be requested.

## Step 6 — Use the site

**anonymous:** Send **no** credentials: no `Authorization` header, no cookie, no API key. None is required, and none is honoured. Then:

- Prefer `/llms.txt`, `/rss.xml`, and the Markdown form of pages over scraping HTML.
- Honor `Cache-Control` and `ETag`: pages revalidate cheaply, hashed assets are immutable.
- Identify yourself with a descriptive `User-Agent` and keep to a few requests per second.

**oauth:** Send `Authorization: Bearer <access_token>` on requests to the media API prefixes below, and only to `https://maxsong.me` (the token's resource; the `www` and `home` hosts answer bearer tokens with 401). `GET https://maxsong.me/oauth/whoami` with the token returns who it belongs to, its scope and expiry. Do not send the token to public pages — it is ignored there.

### Media API — bearer token required

These prefixes serve the https://maxsong.me/sound/ player. Browsers get in through a same-origin session; agents get in only with an access token from the flow above. Without one, scripted requests get **403** (no credential) or **401** (bad credential), always with a `WWW-Authenticate` header whose resource_metadata parameter is https://maxsong.me/.well-known/oauth-protected-resource (no trailing punctuation belongs to that URL). Do not retry without a token; do not try to imitate a browser.

- `/api/*` — the music library manifest
- `/audio/*` — audio renditions
- `/analysis/*` — precomputed waveform and spectrum data

Track titles are listed in `/llms.txt`; the library manifest is the same list with media URLs. For anything beyond that — or if you need the music for something other than listening on https://maxsong.me/sound/ — write to the address at the end.

## Errors

| Status | Where | Meaning | What to do |
| --- | --- | --- | --- |
| 403 or `cf-mitigated: challenge` | public pages or discovery files | An edge security control rejected or challenged this request; unverified bot identity alone is not a reason to deny public content | Do not bypass the control. If a normal read request is blocked, contact the site owner with the URL, status and Cloudflare Ray ID; do not send credentials |
| 403, `WWW-Authenticate: Bearer realm=…, resource_metadata=…` (no `error`) | `/api/*`, `/audio/*`, `/analysis/*` | No credential and not a browser session | Obtain a token through the oauth method, or stop if you are not acting for the site owner |
| 401, `error="invalid_token"` | media API, `/oauth/whoami` | Token missing, malformed, expired or revoked | Refresh it; if refreshing fails, run the authorization flow again |
| 403, `error="insufficient_scope"` | media API | Token lacks `media:read` | Request `scope=media:read`; nothing else is granted here |
| 400 JSON with `error` | `/oauth/*` | Protocol error (RFC 6749 §5.2): bad code, wrong verifier, unknown client, unknown scope… | Fix the request; codes are single-use |
| 401 at `/oauth/authorize` | authorization page | Reached without a Cloudflare Access sign-in of the site owner | Only the site owner can open it, in a browser |
| 404 | anywhere | Nothing at that URL | Check `/sitemap.xml`; there is no soft 404 |
| 429 or 5xx | anywhere | Rate limit or transient error | Back off exponentially and retry `GET`s only |

## Revocation

- Revoke a token you hold: `POST https://maxsong.me/oauth/token` with `token=<access or refresh token>` and your `client_id` (RFC 7009). Revoked and expired tokens get `401` everywhere.
- The site owner can revoke a grant, a client, or every token at once on the server side; tokens do not survive that, and registrations lapse after 90 days unless renewed.
- Anonymous access has nothing to revoke. Access policy for the public site lives in `/robots.txt` (Content-Signal, Content-Usage), in `/.well-known/tdmrep.json` and in this file; a change is published there, not pushed to you.

## What the OAuth metadata means

https://maxsong.me/.well-known/oauth-protected-resource names `https://maxsong.me` as the protected resource, this origin as its authorization server and `media:read` as the only scope; https://maxsong.me/.well-known/oauth-authorization-server lists the endpoints used above (`authorization_code` and `refresh_token` grants, PKCE `S256`, no implicit flow), and an `agent_auth` block that mirrors this document (register at `register_uri`, be approved at `claim_uri`, revoke at `revocation_uri`; identity type `anonymous`). Tokens are opaque, so there is no `jwks_uri`. Every URL in both documents is live: this file and the metadata describe the same server, and nothing here points at an endpoint that does not exist.

## Contact

hello@maxsong.me — for anything the public site does not answer, including use of the music beyond listening on https://maxsong.me/sound/.
