Documentation

Alpha

Device flow

Sign people in from a CLI, an agent or a device without a browser, with a short code they enter on upstream.codes.

The Upstream API is in alpha. Endpoints, fields and limits can change before it's stable. Send feedback to support@coline.app.

The device flow signs someone in to a program that can't open a browser for them. The program shows a short code, the person enters it at upstream.codes/login/device, and the program gets an access token. The endpoints and errors match GitHub's.

Turn it on

Turn on Allow device flow under Sign-in in your app's settings. In a manifest, set device_flow_enabled. It's on by default for public clients. Polling doesn't need a client secret, so public clients work. If you do send a secret, it must be right.

1. Ask for a code

curl -X POST https://upstream.codes/login/device/code \
  -H "Accept: application/json" \
  -d client_id=$CLIENT_ID \
  -d "scope=repo:read issues:write"
{
  "device_code": "udc_…",
  "user_code": "WDJB-MJHT",
  "verification_uri": "https://upstream.codes/login/device",
  "verification_uri_complete": "https://upstream.codes/login/device?user_code=WDJB-MJHT",
  "expires_in": 900,
  "interval": 5
}

scope is optional. Without it, the token gets all of your app's permissions. A scope can only narrow them. GitHub scope names such as repo work too.

2. Show the code

Tell the person to open verification_uri and enter user_code, or send them to verification_uri_complete, which fills the code in. They sign in, see your app and what it asks for, and approve or deny. Codes ignore case, and the dash is optional.

3. Poll for the token

curl -X POST https://upstream.codes/login/oauth/access_token \
  -H "Accept: application/json" \
  -d client_id=$CLIENT_ID \
  -d device_code=$DEVICE_CODE \
  -d grant_type=urn:ietf:params:oauth:grant-type:device_code

Poll every interval seconds. Once the person approves, you get:

{
  "access_token": "uou_…",
  "token_type": "Bearer",
  "expires_in": 28800,
  "refresh_token": "uor_…",
  "refresh_token_expires_in": 15552000,
  "scope": "repo:read issues:read issues:write"
}

These are the same tokens as the sign-in flow. The access token lasts eight hours and the refresh token rotates on each use. A device code gives one token.

Errors

Until the person decides, polling returns HTTP 400:

{ "error": "authorization_pending", "error_description": "Waiting for the person to enter the code." }
errorWhat to do
authorization_pendingWait interval seconds and poll again.
slow_downYou polled too soon. Add 5 seconds to your interval.
expired_tokenThe code is older than 15 minutes. Start again.
access_deniedThe person denied the request. Stop.
invalid_grantThe code was already used, or belongs to another app. Start again.
device_flow_disabledTurn on Allow device flow for the app.
invalid_scope/login/device/code got a scope the app doesn't have.
invalid_clientThe client ID is unknown or the secret is wrong. HTTP 401.

With Octokit

@octokit/auth-oauth-device works against Upstream. Give it a request with https://upstream.codes as the base URL, so it calls /login/device/code and /login/oauth/access_token there. Then use the token with the API's base URL.

npm install @octokit/auth-oauth-device @octokit/request @octokit/rest
import { createOAuthDeviceAuth } from "@octokit/auth-oauth-device";
import { request } from "@octokit/request";
import { Octokit } from "@octokit/rest";

const auth = createOAuthDeviceAuth({
  clientType: "oauth-app",
  clientId: process.env.UPSTREAM_CLIENT_ID!,
  scopes: ["repo:read", "issues:write"],
  request: request.defaults({ baseUrl: "https://upstream.codes" }),
  onVerification(verification) {
    console.log(`Open ${verification.verification_uri} and enter ${verification.user_code}`);
  },
});

const { token } = await auth({ type: "oauth" });
const octokit = new Octokit({ baseUrl: "https://api.upstream.codes/api/v3", auth: token });
const { data: issues } = await octokit.rest.issues.listForRepo({ owner: "acme", repo: "api" });

The library waits through authorization_pending and slow_down for you. Use clientType: "github-app" and leave out scopes to get all of the app's permissions.