# auth.md — onRun Agent Registration & Authentication

This document provides machine-readable and human-readable authentication and agent registration instructions for the onRun platform, per the [Auth.md specification](https://workos.com/auth-md).

## Discovery & Metadata

- **OAuth Protected Resource Metadata (RFC 9728):** [`/.well-known/oauth-protected-resource`](/.well-known/oauth-protected-resource) — points to Firebase Auth (`https://securetoken.google.com/sportify-api`) as the actual token issuer. onRun does not publish `openid-configuration` / `oauth-authorization-server` documents of its own: onRun is a resource server, not an OIDC issuer, and RFC 8414 requires a discovery document's `issuer` to match the domain it's served from.
- **HTTP Message Signatures Directory (Web Bot Auth):** [`/.well-known/http-message-signatures-directory`](/.well-known/http-message-signatures-directory)
- **Agentic Commerce Protocol (ACP):** [`/.well-known/acp.json`](/.well-known/acp.json)
- **OpenAPI 3.1 Specification:** [`/openapi.json`](/openapi.json)

## Pick a Method

Choose the appropriate authentication approach based on the caller type and action:

1. **Browse-Only Agents & Crawlers**:
   - For read requests against public endpoints (`/api/v1/events`, `/api/v1/runners`, `/api/v1/clubs`, and content-negotiated `/api/md/*` markdown representations), **no authentication** is required.
2. **Tool-Calling Agents Acting for a User**:
   - For user-scoped read operations (`GET /api/v1/me`) or event checkout registrations (`/api/v1/checkout_sessions*`), use **OAuth / Bearer Token** authentication. Send the user's Firebase ID token in the `Authorization: Bearer <token>` header as advertised in RFC 9728 protected resource metadata.
3. **Web Bot Auth (Request Attribution)**:
   - Automated agents wishing to provide verifiable cryptographic identity may sign HTTP requests using HTTP Message Signatures (RFC 9421). onRun publishes its public verification keys in [`/.well-known/http-message-signatures-directory`](/.well-known/http-message-signatures-directory).

## Register

- **Programmatic Dynamic Client Registration (RFC 7591)**: Not yet available. Dedicated machine-to-machine client registration endpoints are currently reserved.
- **Agent Operator Credentials**: onRun does not currently issue standalone developer API keys or client-credentials grants for autonomous bot execution.
- **Obtaining Access**: To access user-gated endpoints, an agent operator must integrate with onRun's user authentication flow. A human user must sign in to onRun via Firebase Auth (email/password, Google, Apple, or phone authentication) for the `sportify-api` project, producing a Firebase ID token that the agent passes in requests.

## Claim

- **Agent-to-Account Association**: An interactive user-consent delegation or "claim" screen (such as an OAuth 2.1 authorization code flow with third-party app scopes) is not yet available.
- **Identity Binding**: The agent is associated with a human onRun account directly via the verified Firebase ID token supplied on each request. When the backend receives a bearer token, `admin.auth().verifyIdToken(idToken, true)` extracts the user's `uid` and verified `email`. All subsequent database lookups and writes (such as event roster entries or profile reads) are strictly scoped to that authenticated `uid`.

## Authenticating to the onRun API

Most of the onRun agent API (`/api/v1/events`, `/api/v1/runners`, `/api/v1/clubs`) is
public and requires no authentication.

Two surfaces act on behalf of a specific onRun user and require a Firebase ID token for
that user account, sent as a standard bearer token:

```
Authorization: Bearer <firebase-id-token>
```

The authorization server is Firebase Auth for the
`sportify-api` project (`https://securetoken.google.com/sportify-api`) and `https://onrun.com` — see
[`/.well-known/oauth-protected-resource`](/.well-known/oauth-protected-resource) (RFC 9728)
for the machine-readable pointer. An agent must already hold a token obtained through the
user's own Firebase sign-in flow (there is no client-credentials or agent-only grant).

## `GET /api/v1/me` — read

Returns the caller's own public-ish profile fields (display name, handle, photo, email
from the verified token).

## `/api/v1/checkout_sessions*` — write (Agentic Commerce Protocol)

The one write-capable action an agent can take on a user's behalf: registering that user
for a paid onRun event. It implements the
[Agentic Commerce Protocol](https://agenticcommerce.dev) agentic-checkout REST transport,
version `2026-04-17`, discoverable at
[`/.well-known/acp.json`](/.well-known/acp.json).

```
POST /api/v1/checkout_sessions                       create (holds a spot, opens a real charge)
GET  /api/v1/checkout_sessions/{id}                  retrieve
POST /api/v1/checkout_sessions/{id}/complete         finalize once the charge has succeeded
POST /api/v1/checkout_sessions/{id}/cancel           release the held spot
```

`Idempotency-Key` is required on every POST, per the ACP spec.

### How this differs from the reference ACP deployment — read this before integrating

The canonical ACP checkout authenticates the **agent platform** and takes payment with a
Stripe **Shared Payment Token** (`credential.type: "spt"`), which the platform obtains
through Stripe's delegated-payments flow. Issuing and accepting SPTs requires a gated
Stripe merchant enrolment that onRun does not currently hold, so onRun **cannot receive or
verify a Shared Payment Token**. Rather than accept a token type it cannot honour, this
deployment diverges in two documented ways:

1. **Auth is the end user's Firebase ID token, not an agent-platform credential.** The
   agent must be acting for an already-signed-in onRun user, exactly as with
   `GET /api/v1/me`. A request carrying an SPT-style credential is rejected with
   `422 unsupported`.
2. **The payment credential is a Stripe PaymentIntent the buyer already confirmed.**
   `POST /checkout_sessions` returns a live Stripe PaymentIntent (id and client secret) in
   `metadata`. The buyer confirms it with their own payment method — via Stripe.js, or by
   following `continue_url` to the event page. `POST /checkout_sessions/{id}/complete`
   then takes that PaymentIntent id as
   `payment_data.instrument.credential` with
   `handler_id: "onrun_stripe_delegated"` and `type: "onrun_stripe_payment_intent"`, and
   onRun re-verifies it with Stripe before writing the registration.

Everything else is the production path, not a sandbox: real capacity and discount-code
checks, a real Stripe Connect destination charge on the organizer's account, and a real
write adding the runner to the event roster. If payment has not succeeded when `/complete`
is called, the session comes back `requires_escalation` with a `continue_url` — it never
reports a success that did not happen.

`POST /checkout_sessions/{id}` (the ACP update endpoint) is **not implemented** and returns
`405 unsupported`: an onRun session is a single event at quantity 1 with no shipping
address and an immutable post-reservation price, so there is nothing an update could
legitimately change. Cancel and create a new session instead.

## Errors

The onRun API emits the following status codes and payload structures across its endpoints:

- **400 Bad Request**:
  - Validation failures on query parameters or payload inputs.
  - Public directory format: `{"error": "missing_query", "message": "The \"q\" query parameter is required."}` or `{"error": "invalid_limit", "message": "The \"limit\" parameter must be an integer between 1 and 50."}`.
- **401 Unauthorized**:
  - Missing, malformed, expired, or revoked bearer token on auth-gated endpoints (`/api/v1/me`, `/api/v1/checkout_sessions*`).
  - Auth helper format: `{"error": "Unauthorized", "reason": "Missing Authorization Bearer token"}` (or `"Invalid or expired token"`).
  - ACP endpoints format: `{"type": "invalid_request", "code": "unauthorized", "message": "A valid Firebase ID token is required..."}` with header `API-Version: 2026-04-17`.
- **403 Forbidden**:
  - Reserved for insufficient permissions or cross-account access attempts.
- **404 Not Found**:
  - Returned when the requested document does not exist.
  - Format: `{"error": "not_found"}` (e.g., user account document not found for caller's UID).
- **405 Method Not Allowed**:
  - ACP unsupported endpoint format: `{"type": "invalid_request", "code": "unsupported", "message": "..."}`.
- **422 Unprocessable Entity**:
  - ACP unsupported credential format: `{"type": "invalid_request", "code": "unsupported", "message": "..."}` (e.g. attempting to supply a Stripe Shared Payment Token).
- **429 Too Many Requests**:
  - Rate limit exceeded.
  - Standard format: `{"error": "Too many requests", "retryAfterSeconds": 10}` accompanied by a `Retry-After: <seconds>` HTTP header.
  - ACP format: `{"type": "service_unavailable", "code": "rate_limited", "message": "Too many checkout requests. Retry shortly."}`.
- **5xx Server Errors**:
  - `500 Internal Server Error` or `503 Service Unavailable` returned when upstream datastores (Firestore, Cloud Functions, Stripe) are unreachable.
  - ACP format: `{"type": "service_unavailable", "code": "not_configured" | "upstream_error", "message": "..."}`.

### Known Gaps
Error responses are currently heterogeneous across route families: public search endpoints return `{error, message}`, authentication wrappers return `{error, reason}`, single-entity lookups return `{error: "not_found"}`, and ACP checkout endpoints return `{type, code, message}` with `API-Version` headers. Harmonization under RFC 9457 Problem Details (`application/problem+json`) is planned for a future API milestone.

## Revocation

- **Token Revocation**:
  - When a human user signs out, changes credentials, or revokes active sessions via Firebase Auth, the token is revoked in Firebase.
  - onRun validates tokens with `checkRevoked: true` (`verifyIdToken(idToken, true)`). Once revoked, subsequent API requests fail immediately with `401 Unauthorized`.
- **In-Flight Requests**:
  - Individual HTTP transactions that have already started execution will run to completion. If a multi-step checkout session is in progress, revoking credentials before calling `/complete` will prevent the finalization step from executing.
- **Web Bot Auth Key Rotation**:
  - The Ed25519 signing key (`kid: "onrun-2026-08"`) published in `/.well-known/http-message-signatures-directory` has a defined validity window:
    - `nbf`: 1785542400 (2026-08-01T00:00:00Z)
    - `exp`: 1817078400 (2027-08-01T00:00:00Z)
  - Agents and verifiers must observe these Unix timestamps and refresh keys from the directory upon expiration.
