# auth.md

<!--
  Floppy's agent-auth guide, modeled on the WorkOS auth.md template (https://github.com/workos/auth.md, AUTH.md).
  VERIFY-ONLINE: re-check the template's sections against https://workos.com/auth-md/docs/auth-md before launch.
  The agent_auth block in /.well-known/oauth-authorization-server and the RFC 9728 document at
  /.well-known/oauth-protected-resource (served by workers/oauth/metadata.ts) both point back here via agent_auth.skill.
-->

Resource server: `https://fpy.sh` (REST under `/api/v1`, uploads at `/api/drop`, MCP at `/mcp`).
Authorization server: `https://fpy.sh` (OAuth 2.1 for MCP clients, sign-in with Google).

Every credential is a bearer token sent as:

```
Authorization: Bearer fpy_live_…
```

Tokens start with `fpy_live_` so secret scanners recognize them. Floppy stores only their SHA-256 hash; a token is shown once, when it is created.

## Step 1 — Discover

A request without valid credentials gets `401` with a `WWW-Authenticate: Bearer resource_metadata="…"` header and a JSON body whose `next` and `docs` fields point here.

### 1a. Fetch the Protected Resource Metadata

`GET https://fpy.sh/.well-known/oauth-protected-resource` (RFC 9728) lists the authorization server and the scopes.

### 1b. Fetch the Authorization Server metadata

`GET https://fpy.sh/.well-known/oauth-authorization-server` (RFC 8414) lists the OAuth endpoints, including dynamic client registration for MCP clients.

## Step 2 — Pick a method

| Method | Use it when | You get |
|---|---|---|
| **Anonymous** | You have no account and want to upload now | An agent key for a new unclaimed account, plus a claim URL for your human |
| **OAuth** | You are an MCP client and a human is there to sign in | An access token for that human's account |
| **API key** | A human made a key for you at https://fpy.sh/app/keys | A key for their account |

## Step 3 — Register

### anonymous

Upload with no credentials. The response creates the account and the key in one call:

```sh
curl -T deck.pdf https://fpy.sh/api/drop/deck.pdf
```

```json
{ "share_url": "https://fpy.sh/k3x9", "agent_key": "fpy_live_…", "claim_url": "https://fpy.sh/claim/7f3c…", "scan": "scanning", "next": "…" }
```

- `agent_key` is shown once. Save it and send it on every later call so your uploads stay in one account.
- The unclaimed account has 1 GB of storage and safe file types only, and is deleted after 72 hours unless claimed.
- The key has access `read_write` and scope `own`: it sees only the links it created.

### oauth

MCP clients connect to `https://fpy.sh/mcp` and follow the standard MCP authorization flow: discover the metadata above, identify themselves with a Client ID Metadata Document (an https `client_id`) or dynamic client registration (`POST https://fpy.sh/oauth/register`), then send the human through `https://fpy.sh/oauth/authorize`, where they sign in with Google and approve the app. Most clients (Claude, ChatGPT, Cursor, Codex, VS Code) do this on their own; see https://fpy.sh/docs/agents#connect.

- Authorization code flow with PKCE (`S256` only) and `resource=https://fpy.sh/mcp`. One scope, `account`.
- `POST https://fpy.sh/oauth/token` returns `{ access_token, token_type: "Bearer", refresh_token, scope }`. The access token is a normal `fpy_live_…` key (read + write, whole account), so it also works on the REST API.
- Access tokens don't expire on their own. Refreshing returns a new access token and a new refresh token, and the old ones stop working. Reusing an old refresh token disconnects the app.

### service_auth (API keys)

A signed-in human creates keys at https://fpy.sh/app/keys. Keys can't create other keys.

- Access: `read` (links and analytics), `write` (upload, links, shares), or both.
- Scope: `account` (everything in the account; the default for keys made in the dashboard) or `own` (only the links the key created; the default for agent keys).

## Step 4 — Claim ceremony

Anonymous uploads belong to an unclaimed account until a human claims them.

### 4a. Materials

`claim_url` from the drop response. Lost it? `GET https://fpy.sh/api/v1/claim` with your agent key, or the `get_claim_link` MCP tool, returns `{ claim_url, expires_at }`.

### 4b. Hand off to the user

Give your human the share link and the claim link, for example: "To keep this file and see who opens it, claim it within 72 hours: https://fpy.sh/claim/7f3c…". They sign in with Google, and every link, file and view moves to their account. Claim links work once.

### 4c. Poll

`GET https://fpy.sh/api/v1/usage` with your agent key. `claimed` turns `true` once your human has claimed the account.

## Step 5 — Exchange the assertion

Nothing to exchange. The agent key you already hold keeps working after the claim, still limited to the links it created. The human can widen it to the whole account at https://fpy.sh/app/keys.

## Step 6 — Use the access_token

Send the token on every request:

```sh
curl https://fpy.sh/api/v1/links -H "Authorization: Bearer fpy_live_…"
```

The same header works for the MCP server at `https://fpy.sh/mcp`. Bearer requests don't need cookies or a same-origin check.

## Errors

Every error is `{ "error": { "code", "message", "next", "docs" } }`.

| Status | Code | What to do |
|---|---|---|
| 401 | `unauthorized` | The token is missing, malformed, revoked or expired. Start again at Step 1 |
| 403 | `forbidden` | The token can't do this: a `read` key tried to write, or an `own` key touched a link it didn't create |
| 402 | `limit_reached` | Give your human `claim_url` (unclaimed) or `upgrade_url` (claimed) |
| 429 | `slow_down` | Wait `retry_after` seconds |

## Revocation

Humans revoke keys and disconnect OAuth apps at https://fpy.sh/app/keys. OAuth clients can revoke their own tokens with `POST https://fpy.sh/oauth/revoke` (RFC 7009). Revocation takes effect on the next request. An unclaimed account's agent key stops working when the account is deleted after 72 hours.

Full docs: https://fpy.sh/docs/agents
