# Device authorization

URL: https://docs.spacerun.co/docs/platform/guides/device-authorization

> Issue codes, poll with backoff, exchange the login hash, and revoke device access.

Use this flow for a CLI or device that cannot complete a browser sign-in itself. Prerequisites: the deployment's Platform API and Supabase auth URLs, and a user who can sign in to the portal. Examples use the Supabase client from [Quickstart](https://docs.spacerun.co/docs/platform/guides/quickstart).

## Issue a code and request approval

[createDeviceCode](https://docs.spacerun.co/docs/platform/reference/device-authorization/createDeviceCode) is public and limited to 3 requests per minute per IP:

```ts
const response = await fetch(`${apiBaseUrl}/v1/public/device/code`, {
  method: "POST", headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ client_name: "Example CLI" }),
});
if (!response.ok) throw new Error(await response.text());
const code = await response.json();
console.log(code.verification_uri_complete, code.user_code);
```

The code expires after 900 seconds and starts with a 5-second polling interval. Display only the user code and verification URI. Keep `device_code` private on the requesting device. The signed-in user approves, denies or inspects the request using [respondDeviceAuthorization](https://docs.spacerun.co/docs/platform/reference/device-authorization/respondDeviceAuthorization).

## Poll, then exchange the hash

Expected HTTP 400 polling responses are part of the flow, so handle them directly rather than using a helper that throws on every non-2xx status:

```ts
let interval = code.interval;
const deadline = Date.now() + code.expires_in * 1000;
let session;
while (Date.now() < deadline) {
  await new Promise(resolve => setTimeout(resolve, interval * 1000));
  const response = await fetch(`${apiBaseUrl}/v1/public/device/token`, {
    method: "POST", headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      grant_type: "urn:ietf:params:oauth:grant-type:device_code",
      device_code: code.device_code,
    }),
  });
  const result = await response.json();
  if (response.status === 503 && result.error === "panic_mode") {
    throw new Error("Maintenance: restart authorization after the retry delay");
  }
  if (response.ok) {
    const verified = await supabase.auth.verifyOtp({
      token_hash: result.token_hash, type: "magiclink",
    });
    if (verified.error) throw verified.error;
    session = verified.data.session;
    break;
  }
  if (result.error === "authorization_pending" || result.error === "slow_down") {
    interval = result.interval ?? interval;
    continue;
  }
  if (result.error === "server_error") continue;
  throw new Error(result.error);
}
if (!session) throw new Error("Authorization expired; request a new code");
```

Success returns a **single-use** `token_hash`, not an access token. Exchange it within its returned 60-second lifetime. `slow_down` can increase the interval; the per-IP polling guard can also return it. Denied, expired, consumed or invalid codes should stop the loop rather than retry forever. A transient network failure can be retried within the deadline, but keep the polling interval.

Inspect/revoke your device authorizations through [listDeviceAuthorizations](https://docs.spacerun.co/docs/platform/reference/device-authorization/listDeviceAuthorizations) and [revokeDeviceAuthorization](https://docs.spacerun.co/docs/platform/reference/device-authorization/revokeDeviceAuthorization). Revocation is separate from stopping a game session.
