Sign in to a subscription
A subscription backend takes a short-lived token, not an API key. @providerkit/core/auth gets
that token and renews it. It uses only fetch, so it runs in a browser extension, a worker or a
server.
Once you have signed a person in, getting a token is one call:
const { accessToken } = await token.getToken(); // a token that works nowtoken comes from tokenSource, below. If the stored token has expired, this call renews it first.
To sign in the first time, your app supplies the two things a package can’t do: opening a browser page and catching a redirect. It also decides where tokens are stored.
import { createAuthFlow, tokenSource, withToken, type AuthHost } from "@providerkit/core/auth";import { createPresetProvider } from "@providerkit/core";
const host: AuthHost = { appName: "MyApp/1.0", // sent to Copilot and xAI to say who is signing in openUrl: (url) => openInBrowser(url), captureRedirect: ({ authorizeUrl, redirectUri, state, signal }) => openAndWaitForCode(authorizeUrl, redirectUri, state, signal), // resolves to the `code`};
const flow = createAuthFlow("github-copilot", host);
// Once: show the code, wait for the user to approve it, keep the result.const credential = await flow.signIn(signal, ({ url, userCode }) => showCode(userCode, url));await save(credential);
// Every call: a token that works now.const token = tokenSource({ load, save, refresh: flow.refresh });const provider = withToken(token, (auth) => createPresetProvider("github-copilot", auth));withToken gets a token for each call and hands it to the preset as apiKey, along with the
host the sign-in named (baseUrl) and ChatGPT’s ChatGPT-Account-Id header. If the server
answers 401 before any output, it renews the token and sends the call once more. A second 401 is
thrown as an auth error. To add headers of your own, merge them with auth.headers instead of
replacing them.
withToken builds a new provider for every call. The chatgpt preset sends a session id, and
each new provider makes up its own, so the backend sees every call as a new session and its
prompt cache misses. Pick one id and pass it each time:
createPresetProvider("chatgpt", { ...auth, instanceSessionId }).
createAuthFlow takes chatgpt, kimi-plan, grok, github-copilot, meta or openrouter.
ChatGPT and OpenRouter send the user through a redirect, so they call your captureRedirect. The
others show a code and call your openUrl with the approval page.
What getToken does
Section titled “What getToken does”- Two calls at the same moment refresh once, and the new token is saved before anyone uses it.
- If the server rejects the refresh, it reloads what is stored. When another tab or process saved a newer token in the meantime, it uses that one. Otherwise it throws.
- A network failure keeps the credential and rethrows the network error. The user is not told to sign in again because their wifi dropped.
- A server can refuse a token before it expires. Pass that token as
getToken({ rejected: accessToken })and it is renewed anyway.withTokendoes this for you. - Each request to a sign-in server gives up after 30 seconds, so a server that never answers
can’t hold every call that waits on the refresh. Set
timeoutMson the host to change it. A timeout keeps the credential, like a network failure.
Read the stored credential in load every time. These tokens rotate when they are used, and a
copy you kept in memory may hold one that is already spent.
One case is not covered: two contexts that both lose a refresh before either saves. Closing it needs a lock on your storage.
Errors
Section titled “Errors”A sign-in that ends without a credential throws SignInError with a reason of denied,
expired or cancelled. The rest are ProviderErrors with a code:
| Code | What happened |
|---|---|
token_refused |
The sign-in server said no. status and body say why. |
token_incomplete |
Its reply had no token or no expiry. |
device_response_invalid |
Its reply had no code, or no http(s) approval page. |
setup_required |
Meta account without Muse set up. The message has the link. |
refresh_dead |
The refresh was rejected, or nobody is signed in. Sign in again. |
Show your own text for each code. The package’s messages are English and meant for logs.
GitHub Copilot
Section titled “GitHub Copilot”Copilot’s device sign-in ends in a GitHub token that the Copilot API doesn’t take. The flow trades
it for a Copilot token that lasts about 25 minutes. Trading again is the refresh, so
getToken handles it. The credential also carries baseUrl, because each plan has its own host.
withToken passes it to the preset, as above.
The preset sends X-Initiator: user when the last message is the user’s and agent after a tool
result. GitHub bills one premium request per user turn, so this decides what the run costs. To
name your app in Editor-Version, pass headers: copilotHeaders("MyApp/1.0", "user") too.
GitHub Enterprise Server isn’t supported: its endpoints live on the tenant’s own domain.