# Authenticating with CDN.MN

CDN.MN is an image CDN an AI coding agent can set up and operate — with
least-privilege scopes that run autonomously by default; money (adding funds,
changing a plan) always needs the owner's approval, and any other sensitive
scope needs it only where the owner turned on the per-scope "require my approval"
valve.

**You never need the user's password, and you never get a permanent key.**

## How to get access (device-claim flow)

1. **Register.** Ask for the narrowest scopes that do the job.

   ```http
   POST https://app.cdn.mn/api/agent/register
   Content-Type: application/json

   { "provider": "your-agent", "label": "Your Agent", "scopes": ["site:analyze", "project:read"] }
   ```

   You get back a `device_code` (your secret — keep it), a `user_code`, and a
   `verification_uri_complete`.

2. **Hand the human the link.** Show them `user_code` and ask them to open
   `verification_uri_complete`. They sign in, see exactly the scopes you asked
   for, and grant all or **some** of them. Codes expire in 15 minutes.

3. **Poll for your token.** Every 5 seconds:

   > **Owner approves by email (optional).** If you only have your owner's
   > email, add `"owner_email": "owner@example.com"` to the register body. We
   > email that address a review link and extend the claim window to **24
   > hours**, so the owner can approve from the email instead of you showing
   > them the code out-of-band. This automates *delivering* the approval
   > request — **a one-time human approval is still required**; the agent
   > cannot grant its own access. Everything else is identical; just poll longer
   > (up to 24h) and back off politely. The claim is BOUND to that address (only
   > that account can approve it), and the register response is the same whether
   > the email was sent or rate-limited.

   > **Provision a LIVE workspace up front (optional).** Add
   > `"provision_workspace": true` alongside `owner_email` and we create a
   > real workspace you operate IMMEDIATELY — your token issues on the first
   > poll, no human step. The response adds `workspace_id`, `granted_scopes`
   > (your requested scopes minus the withheld sensitive ones **and minus any
   > reserved, not-yet-shipped scopes**, **plus** `domain:activate` so you can
   > publish your generated hostname), and
   > `provisioned: true`; `verification_uri_complete` becomes the owner's
   > adoption link. The workspace starts with an EMPTY wallet: create a site and
   > **verify a domain** — the first verification funds it with a one-time **$5**
   > starter credit (once per domain), after which the generated hostname serves.
   > It bills **no card** and can't touch billing/deletion/custom origins until
   > the owner adopts it; it is deleted after ~72 hours if unclaimed — so do your
   > setup, then tell your human to check their inbox. A
   > `429 {"error":"slow_down"}` means that owner already has unclaimed agent
   > workspaces pending — stop and ask them to review those first.

   ```http
   POST https://app.cdn.mn/api/agent/token
   Content-Type: application/json

   { "device_code": "…" }
   ```

   - `{"error":"authorization_pending"}` → keep polling.
   - `{"error":"access_denied"}` → stop. The code expired, was revoked, or is unknown.
   - `200` → `{ "access_token": "cdnmn_at_…", "token_type": "Bearer", "expires_in": 86400, "scope": "…" }`

   The token is shown **once**. Store it as a secret. It expires in 24 hours —
   re-register to get a new one.

4. **Call the API.** `Authorization: Bearer cdnmn_at_…`

## MCP server

`POST https://app.cdn.mn/api/mcp` speaks MCP (protocol revision 2025-06-18) over plain
JSON — one request per POST, no streaming, no session header. Send your bearer
token on every request. The flow a standard client runs:

1. `initialize` → capabilities (tools only)
2. `notifications/initialized` → acknowledged with an empty 202
3. `tools/list` → the tool catalog with JSON Schemas
4. `tools/call` → structured results

Read tools return data directly. A **sensitive** tool call either runs
immediately (returning `status: executed`) or answers `approval_required`
with an `approval_id`, depending on how the user set you up — see "Autonomy
and the approval gate" below.

## Scopes

Ask for less. A scope you didn't request can never be granted to you, and the
user may grant a subset of what you asked for — always read the `scope` field
in the token response and adapt. Scopes marked **reserved** below are valid
names whose tools haven't shipped yet: you may request one, but it is never
granted (it won't appear in your token's `scope`), so don't depend on it.

| Scope | Needs a human approval each use? |
| --- | --- |
| `site:analyze` | no |
| `project:read` | no |
| `project:create` | no |
| `project:delete` | autonomous by default — approval only if the owner enabled the per-scope valve |
| `origin:read` | no |
| `origin:write` | autonomous by default — approval only if the owner enabled the per-scope valve |
| `integration:plan` | _reserved — no tool yet; requestable but never granted_ |
| `integration:preview` | _reserved — no tool yet; requestable but never granted_ |
| `integration:apply` | autonomous by default — approval only if the owner enabled the per-scope valve |
| `domain:read` | no |
| `domain:verify` | no |
| `domain:activate` | autonomous by default — approval only if the owner enabled the per-scope valve |
| `analytics:read` | no |
| `cache:purge` | no |
| `billing:read` | no |
| `billing:write` | **always — owner approves each use** |

## Autonomy and the approval gate

When you call a tool that uses a sensitive scope, one of two things happens,
depending on how the user set you up at claim time:

- **Autonomous** (the default for a scope the user granted you): the action
  runs immediately and the result comes back in the same call (e.g.
  `{"status":"executed", "execution":{...}}`). Act, then report what you did.
- **Approval required** (the user ticked "require my approval" for that scope):
  the call returns `approval_required` with an `approval_id`. Surface it to
  the user, then **poll `get_approval_status`** for the outcome — do **not**
  re-call the tool to "retry" (that opens a second request; a duplicate call
  with the same `idempotency_key` but changed arguments is rejected as a
  conflict). `get_approval_status` returns `executed` + `execution` once
  the user approves and the action runs.

Either way you can only ever do what your granted scopes allow, and money and
production changes stay bounded. Do not attempt to work around the gate.

## Rules

- The user can revoke you at any time; a revoked token stops working immediately.
- **Read the token from an environment variable or a secret store at the moment
  of use.** Never ask a user to paste it into chat, never print it in logs, never
  commit it, and never write it to a file you will share. The `cdnmn_at_` prefix
  is deliberately greppable so a leaked token is easy for a secret scanner to catch.
- Never log or echo the `device_code` either.
- Rate limits apply to registration; back off on `429`.
- Every state-changing action you take — a mutation, an approval opened or
  executed, a verification check — is written to the workspace's audit log.

## Discovery

- `https://app.cdn.mn/.well-known/oauth-protected-resource`
- `https://app.cdn.mn/.well-known/oauth-authorization-server`
- **CLI & SDK** — `npm i -g @cdnmn/cli` (or the `@cdnmn/sdk`) implement this
  device-claim flow and the MCP calls for you: `https://app.cdn.mn/docs/cli`. Read the
  token from the environment or a secret store at the point of use; never paste
  it into chat, print it, or commit it (the `cdnmn_at_` prefix makes it easy to
  grep for and revoke).
