@modelprofile.com/authswitch-signin
v11.4.1
Published
Sign a ChatGPT account in by device code and keep its grant usable across processes, over a store the caller supplies.
Readme
@modelprofile.com/authswitch-signin
Sign a ChatGPT account in by device code, and keep its grant usable for any number of processes that share one store. Part of authswitch, which runs its own sign-ins on this package.
Issue Reporting and Security
For reporting bugs, issues, or security vulnerabilities, please visit community.foss.global/. This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a code.foss.global/ account to submit Pull Requests directly.
Install
pnpm add @modelprofile.com/authswitch-signinThe package has no runtime dependencies; it needs Node 24 and the global fetch, or a fetch you pass.
What it does
- The sign-in protocol Codex uses: the device code, its poll, the authorization-code exchange, the
refresh-token grant, the revocation and the claims of the tokens (
requestOpenAiDeviceCode,pollOpenAiDeviceCodeOnce,pollOpenAiDeviceCode,exchangeOpenAiDeviceAuthorization,refreshOpenAiTokens,revokeOpenAiRefreshToken,parseOpenAiTokenClaims). - A device sign-in that survives a restart (
startOpenAiDeviceSignIn,pollOpenAiDeviceSignIn). - A grant manager over a store you supply (
OpenAiGrantManager).
Every failure has a fixed message. No response body, token, code or header crosses an error, a status or a return value other than the credential itself.
A device sign-in any process can finish
import {
pollOpenAiDeviceSignIn,
startOpenAiDeviceSignIn,
} from '@modelprofile.com/authswitch-signin';
const { prompt, state } = await startOpenAiDeviceSignIn();
// Show prompt.verificationUrl and prompt.userCode; the code expires at prompt.expiresAt.
// Keep `state` sealed: its deviceAuthId with its userCode complete the sign-in.
const poll = await pollOpenAiDeviceSignIn(state);
if (poll.status === 'approved') {
// poll.result.credential is the sign-in; poll.result.account says whose it is.
} else if (poll.status === 'pending') {
// Poll again at poll.nextPollAt at the earliest.
} else {
// 'expired': start a new sign-in.
}startOpenAiDeviceSignIn stamps the state with issuedAt and expiresAt. The code lives fifteen
minutes, the time Codex polls it and tells the person. pollOpenAiDeviceSignIn asks once, bounded by the
time the code has left and by thirty seconds, and answers expired without a request once that time is
up. A malformed state (its shape, or a window longer than fifteen minutes) is refused before anything is
sent; only shape and window are checked, not where the state came from.
The service answers the authorization once. Poll one state from one caller at a time. After approved,
or after a failed exchange, start a new sign-in rather than polling the same state again.
A grant several processes share
OpenAiGrantManager answers current access for each provider request. It refreshes the tokens when the
access expires within five minutes, or is older than eight days (Codex's two rules), or the provider
rejected exactly the current generation.
import {
OpenAiGrantManager,
type IOpenAiGrantStore,
} from '@modelprofile.com/authswitch-signin';
const store: IOpenAiGrantStore = createMyStore(); // load / create / compareAndSet / remove
const grants = new OpenAiGrantManager({ store });
await grants.create('organization-1', poll.result); // after an approved sign-in
const access = await grants.resolveAccess('organization-1', {
signal,
rejectedGeneration, // the generation a 401 rejected, if any
});
// { accessToken, accountId, isFedrampAccount, expiresAt, generation }That answer is the resolveAccess result FlexHarness's
createOpenAiChatGptResolvingModelConnection() takes:
const connection = createOpenAiChatGptResolvingModelConnection({
expectedIdentity: { accountId, isFedrampAccount },
resolveAccess: ({ signal, rejectedGeneration }) =>
grants.resolveAccess('organization-1', { signal, rejectedGeneration }),
});The store
The store is yours. load reads a record, create inserts a new one, and compareAndSet and remove
write only while the record is still at the revision the manager read. Both must compare and write in one
atomic step, as a database's conditional update does. The record carries the credential. Seal it at rest,
never log it, and hand it back unchanged. The manager checks every record it reads, and refuses one it would
not have written with invalid_record.
The rules
A refresh token whose outcome is unknown is never sent a second time. Before it sends a refresh, the manager stores the grant as
refresh_uncertain, with an attempt that is given up after sixty seconds. A success stores the rotated tokens asactiveand moves the generation on.Every other outcome after the request was handed to
fetchmakes the grantreauth_required: a refusal, an HTTP error, a lost connection, or the end of the attempt without an answer. So does a refresh that answers another workspace or user. Only a new sign-in, throughreplace, repairs it. This is deliberate: any answer to a sent refresh other than new tokens, a 429 or a 5xx included, means the refresh token's outcome is unknown, and such a token is never sent again. So an outage at OpenAI can require signing in again.A refresh that was never sent keeps the grant
activeand setsretryAt, with exponential backoff and the service'sRetry-After. Access that is still valid long enough is served meanwhile. Otherwise the call fails withaccess_not_fresh.Within a process, one refresh per grant runs at a time. Across processes, the store's compare-and-set decides, and a process that finds another's refresh in flight waits for its outcome. A caller that stops waiting (
signal) leaves the refresh running to its end, so its outcome is recorded.keepFresh(grantId)refreshes a grant that is due, for a periodic job that keeps an idle sign-in alive.remove(grantId)deletes the grant, then revokes its refresh token at the service.revocationis:confirmedwhen the service revoked the current token;unconfirmedwhen a refresh may have replaced that token before;failedwhen the service did not confirm.
The grant is removed in every case.
Errors are OpenAiGrantError with a code:
not_foundreauth_required, withdetails.reauthReason:refresh_failed,refresh_interruptedoridentity_changedaccess_not_fresh, withdetails.retryAtinvalid_argumentinvalid_recordabortedcontended
status(grantId) answers the grant's state, workspace, plan and times without a token.
License and Legal Information
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the repository license file.
The ChatGPT sign-in code is adapted from OpenAI Codex, which is licensed under the Apache License, Version 2.0. Its notices are in third-party-notices.md and its license in openai-codex-license.txt; both ship with this package.
Please note: The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
Trademarks
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH or third parties, and are not included within the scope of the MIT license granted herein.
Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines or the guidelines of the respective third-party owners, and any usage must be approved in writing. Third-party trademarks used herein are the property of their respective owners and used only in a descriptive manner, e.g. for an implementation of an API or similar.
Company Information
Task Venture Capital GmbH Registered at District Court Bremen HRB 35230 HB, Germany
For any legal inquiries or further information, please contact us via email at [email protected].
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.
