> ## Documentation Index
> Fetch the complete documentation index at: https://docs.myrqen.cc/llms.txt
> Use this file to discover all available pages before exploring further.

# Authentication

> Three credentials, three purposes, deliberately not interchangeable.

## Device credential — for the CLI

```http theme={null}
Authorization: Bearer myrq_dev_…
```

Obtained once from the [device-link exchange](/getting-started/link-a-device) and stored in
the OS keystore. The server stores only a hash.

The server never trusts a CLI-supplied user id — identity comes from the credential row, and
a revoked credential answers `DEVICE_REVOKED` (401).

```bash theme={null}
curl http://localhost:3000/api/v1/sync-quota \
  -H "authorization: Bearer $MYRQEN_DEVICE_TOKEN"
```

<Tip>
  For CI, `MYRQEN_DEVICE_TOKEN` supplies the credential to the CLI directly, bypassing the
  keystore. Treat it as a secret: it is a bearer token for the whole account's cloud
  operations.
</Tip>

## Session cookie — for the browser

```http theme={null}
Cookie: myrqen_session=…
```

| Property   | Value                             |
| ---------- | --------------------------------- |
| `httpOnly` | true                              |
| `sameSite` | `lax`                             |
| `secure`   | true when `APP_BASE_URL` is https |
| `path`     | `/`                               |
| Lifetime   | 30 days                           |

Set by `POST /api/v1/auth/signup`, `/auth/login`, and `/auth/recover`; revoked server-side
by `/auth/logout`.

This is deliberately separate from the device credential. A browser session cannot act as a
CLI, and a device credential cannot approve its own device link — that separation is the
whole point of the approval step.

## Share password — for a shared report

```http theme={null}
x-myrqen-share-password: …
```

Only for `shares/slug/{slug}/report` and `shares/slug/{slug}/export/{format}`. The export
route also accepts it as a `password` query parameter.

Attempts are throttled at 10 per 10 minutes per link. Correct attempts are not counted, so
one attacker cannot lock out the link's legitimate readers.

## Which endpoints accept what

| Endpoints                                                                         | Device token | Session cookie | Neither   |
| --------------------------------------------------------------------------------- | ------------ | -------------- | --------- |
| `health`, `ready`                                                                 |              |                | ✓         |
| `auth/signup`, `auth/login`, `auth/recover`                                       |              |                | ✓         |
| `auth/logout`                                                                     |              | ✓              | ✓ (no-op) |
| `device-links` (create), `…/status`, `…/exchange`                                 |              |                | ✓         |
| `device-links/{id}/approve`                                                       |              | ✓              |           |
| `workspaces`, `projects`, `sync-quota`                                            | ✓            | ✓              |           |
| `analytics/events`                                                                | ✓            | ✓              |           |
| `reports` (create), `…/progress`, `…/upload-intent`, `…/finalize`, `uploads/{id}` | ✓            |                |           |
| `reports/{id}`, `…/events`, `…/export/{format}`, `…/shares`                       |              | ✓              |           |
| `shares/{shareId}` (PATCH, DELETE)                                                |              | ✓              |           |
| `shares/slug/{slug}/…`                                                            |              | optional       | ✓         |
| `updates/manifest`                                                                |              |                | ✓         |

A workspace-scoped read accepts either principal, because both the CLI and the dashboard
legitimately need it. Report **writes** are device-only, and report **reads** are
session-only — the CLI has no reason to read back a report it produced.

`/api/v1/admin/*` requires a browser session belonging to a platform administrator, and
`/api/v1/billing/*` a browser session or a verified provider signature. Both are
[operator-only and not documented here](/api-reference/introduction#what-is-not-documented-here).

## Authorization, beyond authentication

<AccordionGroup>
  <Accordion title="Workspace membership is checked, never inferred">
    Creating a project and creating a report both verify membership first. A non-member gets
    the same answer as a non-existent workspace, so these routes cannot be used to discover
    workspace identifiers.
  </Accordion>

  <Accordion title="Report writes require ownership">
    Progress, upload-intent, finalize, and the upload endpoint all check that the report
    belongs to the authenticated device's user.
  </Accordion>

  <Accordion title="Share management requires ownership, not membership">
    A workspace member can read a report. Only the **owner** can create, change, or revoke a
    share link — and `reportId` must be supplied on update and revoke, because owning the
    named report is what authorizes the change.
  </Accordion>

  <Accordion title="Platform admin is a role, not a superpower">
    The administrator role is granted **only** to the address configured in
    `PLATFORM_ADMIN_EMAIL`, and only when that address signs up. Nobody is promoted
    implicitly, and a deployment with the variable unset has no administrator at all.

    The role grants aggregated operational metadata. It does **not** grant report content:
    `GET /api/v1/reports/{id}` returns `NOT_FOUND` to an administrator who is not the owner.
    Reaching a customer's report body requires a separate, unexpired
    [support grant](/reference/error-codes#platform-support) with a written reason, a bounded
    expiry, and a password re-entry — and every use is counted and audited.
  </Accordion>

  <Accordion title="Analytics attribution is verified">
    Server-side identity overrides whatever the client claims, and a `workspaceId`,
    `projectId`, or `reportId` the principal does not hold is refused with `FORBIDDEN`
    rather than silently dropped — dropping it would leave the caller believing it was
    recorded.
  </Accordion>
</AccordionGroup>

## Full linking sequence

```bash theme={null}
# 1. Bootstrap — unauthenticated. Keep the verifier; send only its hash.
curl -X POST http://localhost:3000/api/v1/device-links \
  -H "content-type: application/json" \
  -d '{"verifierHash":"<sha256 of verifier>","deviceName":"work-laptop","clientVersion":"0.1.0","osFamily":"darwin","arch":"arm64"}'

# 2. Open approvalUrl in a browser and approve while signed in.

# 3. Poll until approved.
curl http://localhost:3000/api/v1/device-links/<publicRequestId>/status

# 4. Exchange the verifier. The token is returned exactly once.
curl -X POST http://localhost:3000/api/v1/device-links/<publicRequestId>/exchange \
  -H "content-type: application/json" \
  -d '{"verifier":"<the verifier>"}'
```

In practice `myrqen link` does all four steps for you.
