Skip to content
repo-sdk
Esc
navigateopen⌘Jpreview
On this page

Authentication overview

How repo-sdk consumes provider credentials and surfaces auth failures.

repo-sdk consumes tokens only — it never runs an OAuth flow or mints credentials for you. You pass the provider factory a ready-made token (or a token provider), and the SDK attaches it to every request. When a credential is missing or invalid, calls fail with a RepoError whose code is unauthorized.

The token-only model

repo-sdk deliberately does not implement OAuth authorization flows, token exchange, or refresh. It takes a credential you already hold and attaches it to every request:

  • No OAuth dance. You obtain the token however you like — a personal access token in an env var, an OAuth token from your own auth layer, or a tokenProvider callback that mints short-lived tokens.
  • No token minting. The SDK never obtains a credential on its own. The one exception is GitHub App auth, where the SDK mints and caches installation access tokens internally from your app credentials.
  • You re-authenticate. With a static token, an expired or revoked credential makes the next call throw a RepoError with code: 'unauthorized' — catch it, obtain a fresh token, and construct a new client. With a tokenProvider, refresh is handled for you (see below).
import { createClient, RepoError } from 'repo-sdk';
import { github } from 'repo-sdk/github';

try {
  const client = createClient({ provider: github({ auth: { token } }) });
  await client.repos.get({ repo: 'capawesome-team/repo-sdk' });
} catch (error) {
  if (error instanceof RepoError && error.code === 'unauthorized') {
    // Token is missing, expired, or revoked — obtain a new one and retry.
  }
}

Expiring tokens: tokenProvider

Every provider also accepts auth: { tokenProvider } — an async callback the SDK invokes on every request to obtain the current bearer token. It’s built for OAuth-style expiring credentials: cache inside the callback and return the cached token while it’s valid.

import type { TokenProvider } from 'repo-sdk';

const tokenProvider: TokenProvider = async ({ forceRefresh }) => {
  if (forceRefresh || cachedTokenExpired()) {
    return refreshAndCacheToken();
  }
  return cachedToken();
};

const client = createClient({ provider: github({ auth: { tokenProvider } }) });

When a request comes back 401, the SDK re-invokes the callback once with forceRefresh: true and retries that request a single time. forceRefresh means the previous token was rejected — bypass your cache and mint a fresh token, otherwise the retry replays the same stale credential and the call fails with unauthorized. There is no second retry.

Two caveats:

  • repos.getCloneUrl embeds the current token in the returned URL (or headers on Azure DevOps with Entra auth), so the URL stops working when that token expires — fetch a fresh one per use.
  • Gitea sends a tokenProvider token with the Bearer scheme (OAuth2 tokens), while a static { token } uses Gitea’s token scheme for personal access tokens.

Who is this token?

users.me() resolves the account behind the configured credentials — a stable provider user id for storing the connection, plus display fields for a “connected as” UI:

const me = await client.users.me();
me.id; // stable provider user id
me.username; // login / handle
me.name; // display name, when the provider exposes one
me.email; // when the provider exposes one — see "Resolving a hidden email"
me.avatarUrl;

It is gated by the userProfile capability: credentials without a user identity — e.g. GitHub App installation tokens — throw unsupported. Branch on client.capabilities.userProfile before calling it when the token type isn’t known up front.

Resolving a hidden email

Two providers omit the email from the profile endpoint: GitHub hides it for private-email accounts (the default for new accounts), and Bitbucket never returns one there. Pass includeEmail: true to have those two make at most one extra request (/user/emails) and return the primary verified address:

const me = await client.users.me({ includeEmail: true });
  • Scopes: GitHub needs user:email (classic PATs / OAuth) or the “Email addresses” read permission (fine-grained PATs); Bitbucket needs the email scope.
  • A missing scope does not throw. The emails call’s forbidden error is swallowed and email stays unset — the email is enrichment, not a precondition. Any other failure (rate limit, network error) propagates as usual.
  • GitLab, Azure DevOps, and Gitea already return the email on the profile call, so the flag is a documented no-op there. On GitHub the extra request is also skipped when the profile already carries a public email.

Self-hosted and enterprise instances

Four of the five providers accept a baseUrl so you can target a self-managed deployment. Point it at your instance’s API root and everything else works unchanged:

Provider Self-hosted product baseUrl example
GitHub Enterprise Server (GHES) https://ghe.example.com/api/v3
GitLab Self-managed https://gitlab.example.com/api/v4
Azure DevOps Azure DevOps Server https://azure.example.com/tfs
Gitea Self-hosted / Forgejo https://gitea.example.com/api/v1
Bitbucket — (Cloud only) not supported

Per-provider guides

Was this page helpful?