# Authentication and Delegation

Public documentation, `llms.txt` and OpenAPI do not require authentication. Data and management operations do. Use the [canonical OpenAPI schemas](/openapi.json) for exact fields and scopes.

## Standalone Identities

New agents first call `POST /api/v1/agents/identity/challenges` with JSON `domain`, `method` (`dns` or `https`) and `recovery_key`. Optional fields are `purpose` (`register`, the default), `name` and `contact_email`. Generate the recovery key locally from 32 cryptographically random bytes represented as 64 lowercase hexadecimal characters; store it before the request. Contact email is optional and does not create a human login. Challenge creation does not reserve a domain or create an account.

Publish the returned `data.value` as a TXT record at `data.dns_name`, or as the HTTPS body at `data.https_url`, according to the requested method. Call `POST /api/v1/agents/identity/registrations` with JSON `challenge_id`, requested `scopes` and the original `recovery_key`. This verifies the proof and activates the machine account, confidential OAuth client and initial private site together. A challenge expires within 30 minutes; use its returned `expires_at`. Never send a recovery key to a proof location. Published proof alone cannot complete registration.

These public identity endpoints require JSON and a private `Idempotency-Key` of 64 lowercase hexadecimal characters generated independently from the recovery key. A successful response is HTTP 201 with `{data, request_id}`; `data` contains the confidential client credentials and granted site identifiers, never the recovery key. A retry with the same key and body can replay that confidential response. Keep the header private, redact it from logs and respect `Retry-After` on throttling. Identity responses are not cacheable.

After activation, Passport handles confidential-client token exchange at `POST /oauth/token`. Send form-encoded `grant_type=client_credentials`, `client_id`, `client_secret`, requested space-separated `scope` and the exact management `resource` from activation (`https://purestats.io/api/v1/management`). OAuth responses use top-level fields `token_type`, `expires_in` and `access_token`, not a `data` envelope. Machine tokens expire within 15 minutes and do not include a refresh token. Obtain another through client credentials when needed. The API validates issuer, audience, scopes, active ownership and site grants on every request; use `Authorization: Bearer <access token>`. Credentials belong on the server, never in the browser or tracker snippet. Client-credentials access does not implicitly grant a human's account.

## Machine Recovery

Recovery needs both the old recovery key and fresh domain proof. Generate and securely store a different new 32-byte recovery key. Call `POST /api/v1/agents/identity/challenges` with `domain`, `method`, `purpose: "recover"` and the new `recovery_key`; include optional `client_id` if known. Publish this fresh proof, then call `POST /api/v1/agents/identity/recoveries` with its `challenge_id`, the old `recovery_key` and the same optional `client_id`. When recovering a lost activation response, omit `client_id` from both requests so the active credential can be resolved by the proven domain.

Use a distinct identity idempotency key for each operation. Success returns replacement confidential client credentials, retains the account, scopes and granted sites, and revokes the old client and its access and refresh tokens. Save the new credentials and new recovery key atomically in your secret store. No recovery key is returned, and reusing the old key as its replacement is rejected. A contact email or domain proof without the old key is not recovery authorization.

## Existing-Account Delegation

Use the OAuth device authorization flow rather than asking an owner for a password, session cookie or personal API key. Bootstrap a pending confidential client at `POST /api/v1/agents/identity/delegations` with JSON `name`, `scopes` and optional `resource` (management is the default), plus the identity idempotency header. The HTTP 201 `data` contains `client_id`, `client_secret`, requested scopes and OAuth endpoints. Initially `owner_id` is null, effective `scopes` and `site_ids` are empty, and client credentials cannot issue account access. A human browser cookie sent to bootstrap does not approve anything.

Send form-encoded `client_id`, `client_secret` and space-separated `scope` to `POST /oauth/device/code`. Surface only the returned `verification_uri` and `user_code` so the owner can sign in and explicitly approve scopes and sites in the browser. Do not automate the consent decision or claim that opening the URI is approval.

A human with zero sites can approve consent with no existing sites selected when granting the explicit `sites:create` permission, or an account-only capability such as `account.capabilities` (`account:read`). Request only the intended capabilities. `sites:create` authorizes creation of new sites and grants those created sites to this client; `sites:write` configures existing granted sites and is not permission to create them. Read or configure a newly created site only with the separately consented `sites:read` or `sites:write` scope. Empty site selection never grants access to foreign sites: existing-site operations still enforce explicit grants and the owner's membership.

Consent can require fresh password or two-factor reauthentication. The owner completes that step directly in the PureStats browser session; the agent must not collect, transmit or log the owner's reauthentication secret.

Poll `POST /oauth/token` with form-encoded `grant_type=urn:ietf:params:oauth:grant-type:device_code`, the returned `device_code`, `client_id`, `client_secret` and the client's exact `resource`. Honor the returned polling `interval` and `expires_in`. Keep waiting on `authorization_pending`, increase the interval on `slow_down`, and stop on `access_denied` or `expired_token`. Only use a returned token after approval. The device code is a credential and must not be exposed alongside the user-facing code.

Approved delegation may return a refresh token. Refresh at `/oauth/token` using `grant_type=refresh_token`, `refresh_token`, `client_id`, `client_secret`, the granted `scope` and exact `resource`. Save a replacement refresh token atomically; rotation revokes the previous refresh token and associated access token. Scopes cannot grow beyond browser consent, and refresh does not grant additional existing sites. New sites created through explicitly consented `sites:create` are granted to the creating client; this is not authority over other accounts' sites. The flow semantics follow [RFC 8628](https://www.rfc-editor.org/rfc/rfc8628.html); deployed schemas and consent remain authoritative.

## Secret Handling

Read secrets from a secret manager or environment: `PURESTATS_CLIENT_SECRET`, `PURESTATS_ACCESS_TOKEN`, `PURESTATS_RECOVERY_SECRET`. Keep them outside the worktree. Disable command tracing and never print registration or token responses containing credentials. Redact `Authorization`, identity idempotency keys, client secrets, device codes, recovery secrets, refresh tokens, cookies, API keys and sensitive payloads from debug output. A public domain challenge is not recovery material.

Do not commit secrets, embed them in URLs, install them in browser source, pass them to third-party documentation tools or quote them in chat. Public documentation examples intentionally contain no live credential. Rotate or revoke leaked access through supported identity operations rather than editing documentation or reusing old recovery material.
