@modelprofile.com/authswitch-signin 10.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,5 @@
1
+ export * from './signin.interfaces.js';
2
+ export * from './signin.protocol.js';
3
+ export * from './signin.credential.js';
4
+ export * from './signin.device.js';
5
+ export * from './signin.grants.js';
@@ -0,0 +1,6 @@
1
+ export * from './signin.interfaces.js';
2
+ export * from './signin.protocol.js';
3
+ export * from './signin.credential.js';
4
+ export * from './signin.device.js';
5
+ export * from './signin.grants.js';
6
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi90c19zaWduaW4vaW5kZXgudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsY0FBYyx3QkFBd0IsQ0FBQztBQUN2QyxjQUFjLHNCQUFzQixDQUFDO0FBQ3JDLGNBQWMsd0JBQXdCLENBQUM7QUFDdkMsY0FBYyxvQkFBb0IsQ0FBQztBQUNuQyxjQUFjLG9CQUFvQixDQUFDIn0=
@@ -0,0 +1,2 @@
1
+ import * as crypto from 'node:crypto';
2
+ export { crypto };
@@ -0,0 +1,4 @@
1
+ // Node native modules
2
+ import * as crypto from 'node:crypto';
3
+ export { crypto };
4
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicGx1Z2lucy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3RzX3NpZ25pbi9wbHVnaW5zLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLHNCQUFzQjtBQUN0QixPQUFPLEtBQUssTUFBTSxNQUFNLGFBQWEsQ0FBQztBQUV0QyxPQUFPLEVBQUUsTUFBTSxFQUFFLENBQUMifQ==
@@ -0,0 +1,29 @@
1
+ import { type IOpenAiTokenSet } from './signin.protocol.js';
2
+ import type { IOpenAiAccountSummary, IOpenAiOAuthCredential, TOpenAiRefreshResult } from './signin.interfaces.js';
3
+ /** A stored credential that is not a ChatGPT sign-in this package can read. It names no token. */
4
+ export declare class OpenAiCredentialError extends Error {
5
+ constructor();
6
+ }
7
+ /**
8
+ * The token set a stored credential holds, with its ID-token claims. Throws `OpenAiCredentialError` for anything
9
+ * but a complete ChatGPT sign-in.
10
+ */
11
+ export declare const openAiTokensOfCredential: (credential: IOpenAiOAuthCredential) => IOpenAiTokenSet;
12
+ /** The credential a sign-in or refresh produced; one without an ID token is refused. */
13
+ export declare const openAiCredentialOfTokens: (tokens: IOpenAiTokenSet, lastRefreshAt?: string) => IOpenAiOAuthCredential;
14
+ /** What a credential's claims say about its account; no token. */
15
+ export declare const summarizeOpenAiCredential: (credential: IOpenAiOAuthCredential) => IOpenAiAccountSummary;
16
+ export interface IOpenAiRefreshCredentialOptions {
17
+ fetch?: typeof fetch;
18
+ /** Aborting it after the request was handed to fetch leaves the outcome unknown. */
19
+ signal?: AbortSignal;
20
+ now?: () => Date;
21
+ }
22
+ /**
23
+ * Sends one refresh-token grant for a credential and answers the rotated credential, or why there is none.
24
+ * A refresh that returned the same tokens, or no ID token, proves nothing about the grant it used and fails as
25
+ * `outcomeUnknown`. `requestOutcome: 'notSent'` is the only failure after which the same refresh token may be
26
+ * sent again: after any other the service may already have rotated it. `lastRefreshAt` rises strictly.
27
+ * Throws `OpenAiCredentialError` for a credential that is not a complete ChatGPT sign-in.
28
+ */
29
+ export declare const refreshOpenAiCredential: (credential: IOpenAiOAuthCredential, options?: IOpenAiRefreshCredentialOptions) => Promise<TOpenAiRefreshResult>;
@@ -0,0 +1,78 @@
1
+ // OpenAI protocol portions are adapted in TypeScript from OpenAI Codex (Apache-2.0) and modified by Task Venture Capital GmbH; see third-party-notices.md.
2
+ import { OpenAiAuthError, parseOpenAiTokenClaims, refreshOpenAiTokens, } from './signin.protocol.js';
3
+ /** A stored credential that is not a ChatGPT sign-in this package can read. It names no token. */
4
+ export class OpenAiCredentialError extends Error {
5
+ constructor() {
6
+ super('The OpenAI credential is invalid.');
7
+ this.name = 'OpenAiCredentialError';
8
+ }
9
+ }
10
+ /**
11
+ * The token set a stored credential holds, with its ID-token claims. Throws `OpenAiCredentialError` for anything
12
+ * but a complete ChatGPT sign-in.
13
+ */
14
+ export const openAiTokensOfCredential = (credential) => {
15
+ if (credential === null || typeof credential !== 'object' || credential.kind !== 'chatgptOAuth'
16
+ || credential.providerId !== 'openai' || typeof credential.accessToken !== 'string' || !credential.accessToken
17
+ || typeof credential.refreshToken !== 'string' || !credential.refreshToken
18
+ || typeof credential.idToken !== 'string' || !credential.idToken) {
19
+ throw new OpenAiCredentialError();
20
+ }
21
+ try {
22
+ return { accessToken: credential.accessToken, refreshToken: credential.refreshToken, idToken: credential.idToken,
23
+ claims: parseOpenAiTokenClaims(credential.idToken) };
24
+ }
25
+ catch {
26
+ throw new OpenAiCredentialError();
27
+ }
28
+ };
29
+ /** The credential a sign-in or refresh produced; one without an ID token is refused. */
30
+ export const openAiCredentialOfTokens = (tokens, lastRefreshAt) => {
31
+ if (!tokens.idToken)
32
+ throw new OpenAiCredentialError();
33
+ return { kind: 'chatgptOAuth', providerId: 'openai', accessToken: tokens.accessToken,
34
+ refreshToken: tokens.refreshToken, idToken: tokens.idToken,
35
+ ...(lastRefreshAt === undefined ? {} : { lastRefreshAt }) };
36
+ };
37
+ /** What a credential's claims say about its account; no token. */
38
+ export const summarizeOpenAiCredential = (credential) => {
39
+ const claims = openAiTokensOfCredential(credential).claims;
40
+ return {
41
+ providerId: 'openai', authKind: 'chatgptOAuth',
42
+ ...(claims.chatgptAccountId ? { accountId: claims.chatgptAccountId } : {}),
43
+ ...(claims.email ? { email: claims.email } : {}),
44
+ ...(claims.chatgptPlanType ? { plan: claims.chatgptPlanType } : {}),
45
+ };
46
+ };
47
+ /**
48
+ * Sends one refresh-token grant for a credential and answers the rotated credential, or why there is none.
49
+ * A refresh that returned the same tokens, or no ID token, proves nothing about the grant it used and fails as
50
+ * `outcomeUnknown`. `requestOutcome: 'notSent'` is the only failure after which the same refresh token may be
51
+ * sent again: after any other the service may already have rotated it. `lastRefreshAt` rises strictly.
52
+ * Throws `OpenAiCredentialError` for a credential that is not a complete ChatGPT sign-in.
53
+ */
54
+ export const refreshOpenAiCredential = async (credential, options = {}) => {
55
+ const input = openAiTokensOfCredential(credential);
56
+ try {
57
+ const refreshed = await refreshOpenAiTokens(input, { fetch: options.fetch, signal: options.signal });
58
+ if ((refreshed.accessToken === input.accessToken && refreshed.refreshToken === input.refreshToken)
59
+ || !refreshed.idToken) {
60
+ return { success: false, credential, errorCode: 'REFRESH_FAILED', requestOutcome: 'outcomeUnknown' };
61
+ }
62
+ const previous = credential.lastRefreshAt === undefined ? 0 : Date.parse(credential.lastRefreshAt);
63
+ const now = (options.now ?? (() => new Date()))().getTime();
64
+ const lastRefreshAt = new Date(Math.max(now, Number.isFinite(previous) ? previous + 1 : now)).toISOString();
65
+ const next = openAiCredentialOfTokens(refreshed, lastRefreshAt);
66
+ return { success: true, credential: next, account: summarizeOpenAiCredential(next) };
67
+ }
68
+ catch (error) {
69
+ return {
70
+ success: false, credential,
71
+ errorCode: options.signal?.aborted ? 'ABORTED' : 'REFRESH_FAILED',
72
+ requestOutcome: error instanceof OpenAiAuthError ? error.requestOutcome ?? 'outcomeUnknown' : 'outcomeUnknown',
73
+ ...(error instanceof OpenAiAuthError && error.status !== undefined ? { responseStatus: error.status } : {}),
74
+ ...(error instanceof OpenAiAuthError && error.retryAfterMs !== undefined ? { retryAfterMs: error.retryAfterMs } : {}),
75
+ };
76
+ }
77
+ };
78
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic2lnbmluLmNyZWRlbnRpYWwuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi90c19zaWduaW4vc2lnbmluLmNyZWRlbnRpYWwudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsMkpBQTJKO0FBRTNKLE9BQU8sRUFDTCxlQUFlLEVBQUUsc0JBQXNCLEVBQUUsbUJBQW1CLEdBQzdELE1BQU0sc0JBQXNCLENBQUM7QUFLOUIsa0dBQWtHO0FBQ2xHLE1BQU0sT0FBTyxxQkFBc0IsU0FBUSxLQUFLO0lBQzlDO1FBQ0UsS0FBSyxDQUFDLG1DQUFtQyxDQUFDLENBQUM7UUFDM0MsSUFBSSxDQUFDLElBQUksR0FBRyx1QkFBdUIsQ0FBQztJQUN0QyxDQUFDO0NBQ0Y7QUFFRDs7O0dBR0c7QUFDSCxNQUFNLENBQUMsTUFBTSx3QkFBd0IsR0FBRyxDQUFDLFVBQWtDLEVBQW1CLEVBQUU7SUFDOUYsSUFBSSxVQUFVLEtBQUssSUFBSSxJQUFJLE9BQU8sVUFBVSxLQUFLLFFBQVEsSUFBSSxVQUFVLENBQUMsSUFBSSxLQUFLLGNBQWM7V0FDMUYsVUFBVSxDQUFDLFVBQVUsS0FBSyxRQUFRLElBQUksT0FBTyxVQUFVLENBQUMsV0FBVyxLQUFLLFFBQVEsSUFBSSxDQUFDLFVBQVUsQ0FBQyxXQUFXO1dBQzNHLE9BQU8sVUFBVSxDQUFDLFlBQVksS0FBSyxRQUFRLElBQUksQ0FBQyxVQUFVLENBQUMsWUFBWTtXQUN2RSxPQUFPLFVBQVUsQ0FBQyxPQUFPLEtBQUssUUFBUSxJQUFJLENBQUMsVUFBVSxDQUFDLE9BQU8sRUFBRSxDQUFDO1FBQ25FLE1BQU0sSUFBSSxxQkFBcUIsRUFBRSxDQUFDO0lBQ3BDLENBQUM7SUFDRCxJQUFJLENBQUM7UUFDSCxPQUFPLEVBQUUsV0FBVyxFQUFFLFVBQVUsQ0FBQyxXQUFXLEVBQUUsWUFBWSxFQUFFLFVBQVUsQ0FBQyxZQUFZLEVBQUUsT0FBTyxFQUFFLFVBQVUsQ0FBQyxPQUFPO1lBQzlHLE1BQU0sRUFBRSxzQkFBc0IsQ0FBQyxVQUFVLENBQUMsT0FBTyxDQUFDLEVBQUUsQ0FBQztJQUN6RCxDQUFDO0lBQUMsTUFBTSxDQUFDO1FBQUMsTUFBTSxJQUFJLHFCQUFxQixFQUFFLENBQUM7SUFBQyxDQUFDO0FBQ2hELENBQUMsQ0FBQztBQUVGLHdGQUF3RjtBQUN4RixNQUFNLENBQUMsTUFBTSx3QkFBd0IsR0FBRyxDQUFDLE1BQXVCLEVBQUUsYUFBc0IsRUFBMEIsRUFBRTtJQUNsSCxJQUFJLENBQUMsTUFBTSxDQUFDLE9BQU87UUFBRSxNQUFNLElBQUkscUJBQXFCLEVBQUUsQ0FBQztJQUN2RCxPQUFPLEVBQUUsSUFBSSxFQUFFLGNBQWMsRUFBRSxVQUFVLEVBQUUsUUFBUSxFQUFFLFdBQVcsRUFBRSxNQUFNLENBQUMsV0FBVztRQUNsRixZQUFZLEVBQUUsTUFBTSxDQUFDLFlBQVksRUFBRSxPQUFPLEVBQUUsTUFBTSxDQUFDLE9BQU87UUFDMUQsR0FBRyxDQUFDLGFBQWEsS0FBSyxTQUFTLENBQUMsQ0FBQyxDQUFDLEVBQUUsQ0FBQyxDQUFDLENBQUMsRUFBRSxhQUFhLEVBQUUsQ0FBQyxFQUFFLENBQUM7QUFDaEUsQ0FBQyxDQUFDO0FBRUYsa0VBQWtFO0FBQ2xFLE1BQU0sQ0FBQyxNQUFNLHlCQUF5QixHQUFHLENBQUMsVUFBa0MsRUFBeUIsRUFBRTtJQUNyRyxNQUFNLE1BQU0sR0FBRyx3QkFBd0IsQ0FBQyxVQUFVLENBQUMsQ0FBQyxNQUFNLENBQUM7SUFDM0QsT0FBTztRQUNMLFVBQVUsRUFBRSxRQUFRLEVBQUUsUUFBUSxFQUFFLGNBQWM7UUFDOUMsR0FBRyxDQUFDLE1BQU0sQ0FBQyxnQkFBZ0IsQ0FBQyxDQUFDLENBQUMsRUFBRSxTQUFTLEVBQUUsTUFBTSxDQUFDLGdCQUFnQixFQUFFLENBQUMsQ0FBQyxDQUFDLEVBQUUsQ0FBQztRQUMxRSxHQUFHLENBQUMsTUFBTSxDQUFDLEtBQUssQ0FBQyxDQUFDLENBQUMsRUFBRSxLQUFLLEVBQUUsTUFBTSxDQUFDLEtBQUssRUFBRSxDQUFDLENBQUMsQ0FBQyxFQUFFLENBQUM7UUFDaEQsR0FBRyxDQUFDLE1BQU0sQ0FBQyxlQUFlLENBQUMsQ0FBQyxDQUFDLEVBQUUsSUFBSSxFQUFFLE1BQU0sQ0FBQyxlQUFlLEVBQUUsQ0FBQyxDQUFDLENBQUMsRUFBRSxDQUFDO0tBQ3BFLENBQUM7QUFDSixDQUFDLENBQUM7QUFTRjs7Ozs7O0dBTUc7QUFDSCxNQUFNLENBQUMsTUFBTSx1QkFBdUIsR0FBRyxLQUFLLEVBQUUsVUFBa0MsRUFDOUUsVUFBMkMsRUFBRSxFQUFpQyxFQUFFO0lBQ2hGLE1BQU0sS0FBSyxHQUFHLHdCQUF3QixDQUFDLFVBQVUsQ0FBQyxDQUFDO0lBQ25ELElBQUksQ0FBQztRQUNILE1BQU0sU0FBUyxHQUFHLE1BQU0sbUJBQW1CLENBQUMsS0FBSyxFQUFFLEVBQUUsS0FBSyxFQUFFLE9BQU8sQ0FBQyxLQUFLLEVBQUUsTUFBTSxFQUFFLE9BQU8sQ0FBQyxNQUFNLEVBQUUsQ0FBQyxDQUFDO1FBQ3JHLElBQUksQ0FBQyxTQUFTLENBQUMsV0FBVyxLQUFLLEtBQUssQ0FBQyxXQUFXLElBQUksU0FBUyxDQUFDLFlBQVksS0FBSyxLQUFLLENBQUMsWUFBWSxDQUFDO2VBQzdGLENBQUMsU0FBUyxDQUFDLE9BQU8sRUFBRSxDQUFDO1lBQ3hCLE9BQU8sRUFBRSxPQUFPLEVBQUUsS0FBSyxFQUFFLFVBQVUsRUFBRSxTQUFTLEVBQUUsZ0JBQWdCLEVBQUUsY0FBYyxFQUFFLGdCQUFnQixFQUFFLENBQUM7UUFDdkcsQ0FBQztRQUNELE1BQU0sUUFBUSxHQUFHLFVBQVUsQ0FBQyxhQUFhLEtBQUssU0FBUyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLElBQUksQ0FBQyxLQUFLLENBQUMsVUFBVSxDQUFDLGFBQWEsQ0FBQyxDQUFDO1FBQ25HLE1BQU0sR0FBRyxHQUFHLENBQUMsT0FBTyxDQUFDLEdBQUcsSUFBSSxDQUFDLEdBQUcsRUFBRSxDQUFDLElBQUksSUFBSSxFQUFFLENBQUMsQ0FBQyxFQUFFLENBQUMsT0FBTyxFQUFFLENBQUM7UUFDNUQsTUFBTSxhQUFhLEdBQUcsSUFBSSxJQUFJLENBQUMsSUFBSSxDQUFDLEdBQUcsQ0FBQyxHQUFHLEVBQUUsTUFBTSxDQUFDLFFBQVEsQ0FBQyxRQUFRLENBQUMsQ0FBQyxDQUFDLENBQUMsUUFBUSxHQUFHLENBQUMsQ0FBQyxDQUFDLENBQUMsR0FBRyxDQUFDLENBQUMsQ0FBQyxXQUFXLEVBQUUsQ0FBQztRQUM1RyxNQUFNLElBQUksR0FBRyx3QkFBd0IsQ0FBQyxTQUFTLEVBQUUsYUFBYSxDQUFDLENBQUM7UUFDaEUsT0FBTyxFQUFFLE9BQU8sRUFBRSxJQUFJLEVBQUUsVUFBVSxFQUFFLElBQUksRUFBRSxPQUFPLEVBQUUseUJBQXlCLENBQUMsSUFBSSxDQUFDLEVBQUUsQ0FBQztJQUN2RixDQUFDO0lBQUMsT0FBTyxLQUFLLEVBQUUsQ0FBQztRQUNmLE9BQU87WUFDTCxPQUFPLEVBQUUsS0FBSyxFQUFFLFVBQVU7WUFDMUIsU0FBUyxFQUFFLE9BQU8sQ0FBQyxNQUFNLEVBQUUsT0FBTyxDQUFDLENBQUMsQ0FBQyxTQUFTLENBQUMsQ0FBQyxDQUFDLGdCQUFnQjtZQUNqRSxjQUFjLEVBQUUsS0FBSyxZQUFZLGVBQWUsQ0FBQyxDQUFDLENBQUMsS0FBSyxDQUFDLGNBQWMsSUFBSSxnQkFBZ0IsQ0FBQyxDQUFDLENBQUMsZ0JBQWdCO1lBQzlHLEdBQUcsQ0FBQyxLQUFLLFlBQVksZUFBZSxJQUFJLEtBQUssQ0FBQyxNQUFNLEtBQUssU0FBUyxDQUFDLENBQUMsQ0FBQyxFQUFFLGNBQWMsRUFBRSxLQUFLLENBQUMsTUFBTSxFQUFFLENBQUMsQ0FBQyxDQUFDLEVBQUUsQ0FBQztZQUMzRyxHQUFHLENBQUMsS0FBSyxZQUFZLGVBQWUsSUFBSSxLQUFLLENBQUMsWUFBWSxLQUFLLFNBQVMsQ0FBQyxDQUFDLENBQUMsRUFBRSxZQUFZLEVBQUUsS0FBSyxDQUFDLFlBQVksRUFBRSxDQUFDLENBQUMsQ0FBQyxFQUFFLENBQUM7U0FDdEgsQ0FBQztJQUNKLENBQUM7QUFDSCxDQUFDLENBQUMifQ==
@@ -0,0 +1,56 @@
1
+ import { type IOpenAiAuthOptions } from './signin.protocol.js';
2
+ import type { IOpenAiDevicePrompt, IOpenAiLoginResult } from './signin.interfaces.js';
3
+ /**
4
+ * A device sign-in that any process can continue after a restart. It is a secret: its `deviceAuthId` with its
5
+ * `userCode` complete the sign-in, so it is stored sealed, like a credential, and deleted once it is done.
6
+ */
7
+ export interface IOpenAiDeviceSignInState {
8
+ verificationUrl: string;
9
+ userCode: string;
10
+ deviceAuthId: string;
11
+ intervalSeconds: number;
12
+ /** When the device code was issued, ISO 8601. */
13
+ issuedAt: string;
14
+ /** When the device code stops being accepted: fifteen minutes after `issuedAt`, as Codex tells the person. */
15
+ expiresAt: string;
16
+ }
17
+ /** What the person signing in is shown, with the time the code expires. */
18
+ export interface IOpenAiDeviceSignInPrompt extends IOpenAiDevicePrompt {
19
+ expiresAt: string;
20
+ }
21
+ export interface IOpenAiDeviceSignInStart {
22
+ prompt: IOpenAiDeviceSignInPrompt;
23
+ state: IOpenAiDeviceSignInState;
24
+ }
25
+ /**
26
+ * What one poll found:
27
+ * - `pending`: not approved yet; poll again at `nextPollAt` at the earliest.
28
+ * - `expired`: the code is past `expiresAt`; nothing was sent. Start a new sign-in.
29
+ * - `approved`: the person approved it; `result` holds the credential. The state is spent.
30
+ */
31
+ export type TOpenAiDeviceSignInPoll = {
32
+ status: 'pending';
33
+ nextPollAt: string;
34
+ } | {
35
+ status: 'expired';
36
+ } | {
37
+ status: 'approved';
38
+ result: IOpenAiLoginResult;
39
+ };
40
+ export interface IOpenAiDeviceSignInOptions extends IOpenAiAuthOptions {
41
+ now?: () => Date;
42
+ }
43
+ /**
44
+ * Starts a device sign-in and returns what to show the person and the state to keep. The code lives fifteen
45
+ * minutes from the moment it was requested; `issuedAt` is taken before the request, so `expiresAt` never
46
+ * outlives the code.
47
+ */
48
+ export declare const startOpenAiDeviceSignIn: (options?: IOpenAiDeviceSignInOptions) => Promise<IOpenAiDeviceSignInStart>;
49
+ /**
50
+ * Asks once whether the person approved the sign-in, and on approval exchanges it for the credential. A state
51
+ * past `expiresAt` answers `expired` without a request. One request is bounded by the time the code has left
52
+ * and by thirty seconds. The authorization the service answers is single-use: poll one state from one caller
53
+ * at a time, and after `approved`, or after a failure of the exchange, start a new sign-in rather than polling
54
+ * the same state again. Failures are `OpenAiAuthError`s with fixed messages.
55
+ */
56
+ export declare const pollOpenAiDeviceSignIn: (state: IOpenAiDeviceSignInState, options?: IOpenAiDeviceSignInOptions) => Promise<TOpenAiDeviceSignInPoll>;
@@ -0,0 +1,85 @@
1
+ // OpenAI protocol portions are adapted in TypeScript from OpenAI Codex (Apache-2.0) and modified by Task Venture Capital GmbH; see third-party-notices.md.
2
+ import { exchangeOpenAiDeviceAuthorization, OPENAI_DEVICE_CODE_LIFETIME_MS, OpenAiAuthError, pollOpenAiDeviceCodeOnce, requestOpenAiDeviceCode, } from './signin.protocol.js';
3
+ import { openAiCredentialOfTokens, summarizeOpenAiCredential } from './signin.credential.js';
4
+ const MAX_STATE_STRING_BYTES = 4096;
5
+ const REQUEST_TIMEOUT_MS = 30_000;
6
+ const invalidState = () => new OpenAiAuthError('OpenAI device sign-in state is invalid.', { requestOutcome: 'notSent' });
7
+ const isBoundedString = (value) => typeof value === 'string' && value.length > 0 && Buffer.byteLength(value, 'utf8') <= MAX_STATE_STRING_BYTES;
8
+ const timeOf = (value) => {
9
+ if (typeof value !== 'string')
10
+ return NaN;
11
+ const time = Date.parse(value);
12
+ return Number.isFinite(time) && new Date(time).toISOString() === value ? time : NaN;
13
+ };
14
+ /**
15
+ * Checks a stored state before anything is sent with it: a malformed one (its shape, or a window longer than the
16
+ * fifteen minutes a code lives) is refused. Only shape and window are checked, not where the state came from.
17
+ */
18
+ const readState = (state) => {
19
+ if (state === null || typeof state !== 'object' || !isBoundedString(state.verificationUrl)
20
+ || !isBoundedString(state.userCode) || !isBoundedString(state.deviceAuthId)
21
+ || !Number.isSafeInteger(state.intervalSeconds) || state.intervalSeconds < 1 || state.intervalSeconds > 300) {
22
+ throw invalidState();
23
+ }
24
+ const issuedAt = timeOf(state.issuedAt);
25
+ const expiresAt = timeOf(state.expiresAt);
26
+ if (!Number.isFinite(issuedAt) || !Number.isFinite(expiresAt) || expiresAt <= issuedAt
27
+ || expiresAt - issuedAt > OPENAI_DEVICE_CODE_LIFETIME_MS) {
28
+ throw invalidState();
29
+ }
30
+ return { issuedAt, expiresAt };
31
+ };
32
+ /**
33
+ * Starts a device sign-in and returns what to show the person and the state to keep. The code lives fifteen
34
+ * minutes from the moment it was requested; `issuedAt` is taken before the request, so `expiresAt` never
35
+ * outlives the code.
36
+ */
37
+ export const startOpenAiDeviceSignIn = async (options = {}) => {
38
+ const now = options.now ?? (() => new Date());
39
+ const issuedAt = now().getTime();
40
+ const deviceCode = await requestOpenAiDeviceCode(options);
41
+ const state = {
42
+ verificationUrl: deviceCode.verificationUrl,
43
+ userCode: deviceCode.userCode,
44
+ deviceAuthId: deviceCode.deviceAuthId,
45
+ intervalSeconds: deviceCode.intervalSeconds,
46
+ issuedAt: new Date(issuedAt).toISOString(),
47
+ expiresAt: new Date(issuedAt + OPENAI_DEVICE_CODE_LIFETIME_MS).toISOString(),
48
+ };
49
+ return {
50
+ prompt: { flow: 'device', verificationUrl: state.verificationUrl, userCode: state.userCode, expiresAt: state.expiresAt },
51
+ state,
52
+ };
53
+ };
54
+ /**
55
+ * Asks once whether the person approved the sign-in, and on approval exchanges it for the credential. A state
56
+ * past `expiresAt` answers `expired` without a request. One request is bounded by the time the code has left
57
+ * and by thirty seconds. The authorization the service answers is single-use: poll one state from one caller
58
+ * at a time, and after `approved`, or after a failure of the exchange, start a new sign-in rather than polling
59
+ * the same state again. Failures are `OpenAiAuthError`s with fixed messages.
60
+ */
61
+ export const pollOpenAiDeviceSignIn = async (state, options = {}) => {
62
+ const { expiresAt } = readState(state);
63
+ const now = options.now ?? (() => new Date());
64
+ const remaining = expiresAt - now().getTime();
65
+ if (remaining <= 0)
66
+ return { status: 'expired' };
67
+ const poll = await pollOpenAiDeviceCodeOnce({
68
+ verificationUrl: state.verificationUrl, userCode: state.userCode,
69
+ deviceAuthId: state.deviceAuthId, intervalSeconds: state.intervalSeconds,
70
+ }, { fetch: options.fetch, signal: options.signal, timeoutMs: Math.min(remaining, REQUEST_TIMEOUT_MS) });
71
+ if (poll.status === 'pending') {
72
+ const nextPollAt = Math.min(now().getTime() + state.intervalSeconds * 1000, expiresAt);
73
+ return { status: 'pending', nextPollAt: new Date(nextPollAt).toISOString() };
74
+ }
75
+ const tokens = await exchangeOpenAiDeviceAuthorization(poll.authorization, { fetch: options.fetch, signal: options.signal });
76
+ let credential;
77
+ try {
78
+ credential = openAiCredentialOfTokens(tokens);
79
+ }
80
+ catch {
81
+ throw new OpenAiAuthError('OpenAI auth response is missing id_token.', { requestOutcome: 'outcomeUnknown' });
82
+ }
83
+ return { status: 'approved', result: { credential, account: summarizeOpenAiCredential(credential) } };
84
+ };
85
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic2lnbmluLmRldmljZS5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3RzX3NpZ25pbi9zaWduaW4uZGV2aWNlLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLDJKQUEySjtBQUUzSixPQUFPLEVBQ0wsaUNBQWlDLEVBQUUsOEJBQThCLEVBQUUsZUFBZSxFQUFFLHdCQUF3QixFQUM1Ryx1QkFBdUIsR0FDeEIsTUFBTSxzQkFBc0IsQ0FBQztBQUM5QixPQUFPLEVBQUUsd0JBQXdCLEVBQUUseUJBQXlCLEVBQUUsTUFBTSx3QkFBd0IsQ0FBQztBQUc3RixNQUFNLHNCQUFzQixHQUFHLElBQUksQ0FBQztBQUNwQyxNQUFNLGtCQUFrQixHQUFHLE1BQU0sQ0FBQztBQTBDbEMsTUFBTSxZQUFZLEdBQUcsR0FBb0IsRUFBRSxDQUN6QyxJQUFJLGVBQWUsQ0FBQyx5Q0FBeUMsRUFBRSxFQUFFLGNBQWMsRUFBRSxTQUFTLEVBQUUsQ0FBQyxDQUFDO0FBRWhHLE1BQU0sZUFBZSxHQUFHLENBQUMsS0FBYyxFQUFtQixFQUFFLENBQzFELE9BQU8sS0FBSyxLQUFLLFFBQVEsSUFBSSxLQUFLLENBQUMsTUFBTSxHQUFHLENBQUMsSUFBSSxNQUFNLENBQUMsVUFBVSxDQUFDLEtBQUssRUFBRSxNQUFNLENBQUMsSUFBSSxzQkFBc0IsQ0FBQztBQUU5RyxNQUFNLE1BQU0sR0FBRyxDQUFDLEtBQWMsRUFBVSxFQUFFO0lBQ3hDLElBQUksT0FBTyxLQUFLLEtBQUssUUFBUTtRQUFFLE9BQU8sR0FBRyxDQUFDO0lBQzFDLE1BQU0sSUFBSSxHQUFHLElBQUksQ0FBQyxLQUFLLENBQUMsS0FBSyxDQUFDLENBQUM7SUFDL0IsT0FBTyxNQUFNLENBQUMsUUFBUSxDQUFDLElBQUksQ0FBQyxJQUFJLElBQUksSUFBSSxDQUFDLElBQUksQ0FBQyxDQUFDLFdBQVcsRUFBRSxLQUFLLEtBQUssQ0FBQyxDQUFDLENBQUMsSUFBSSxDQUFDLENBQUMsQ0FBQyxHQUFHLENBQUM7QUFDdEYsQ0FBQyxDQUFDO0FBRUY7OztHQUdHO0FBQ0gsTUFBTSxTQUFTLEdBQUcsQ0FBQyxLQUErQixFQUEyQyxFQUFFO0lBQzdGLElBQUksS0FBSyxLQUFLLElBQUksSUFBSSxPQUFPLEtBQUssS0FBSyxRQUFRLElBQUksQ0FBQyxlQUFlLENBQUMsS0FBSyxDQUFDLGVBQWUsQ0FBQztXQUNyRixDQUFDLGVBQWUsQ0FBQyxLQUFLLENBQUMsUUFBUSxDQUFDLElBQUksQ0FBQyxlQUFlLENBQUMsS0FBSyxDQUFDLFlBQVksQ0FBQztXQUN4RSxDQUFDLE1BQU0sQ0FBQyxhQUFhLENBQUMsS0FBSyxDQUFDLGVBQWUsQ0FBQyxJQUFJLEtBQUssQ0FBQyxlQUFlLEdBQUcsQ0FBQyxJQUFJLEtBQUssQ0FBQyxlQUFlLEdBQUcsR0FBRyxFQUFFLENBQUM7UUFDOUcsTUFBTSxZQUFZLEVBQUUsQ0FBQztJQUN2QixDQUFDO0lBQ0QsTUFBTSxRQUFRLEdBQUcsTUFBTSxDQUFDLEtBQUssQ0FBQyxRQUFRLENBQUMsQ0FBQztJQUN4QyxNQUFNLFNBQVMsR0FBRyxNQUFNLENBQUMsS0FBSyxDQUFDLFNBQVMsQ0FBQyxDQUFDO0lBQzFDLElBQUksQ0FBQyxNQUFNLENBQUMsUUFBUSxDQUFDLFFBQVEsQ0FBQyxJQUFJLENBQUMsTUFBTSxDQUFDLFFBQVEsQ0FBQyxTQUFTLENBQUMsSUFBSSxTQUFTLElBQUksUUFBUTtXQUNqRixTQUFTLEdBQUcsUUFBUSxHQUFHLDhCQUE4QixFQUFFLENBQUM7UUFDM0QsTUFBTSxZQUFZLEVBQUUsQ0FBQztJQUN2QixDQUFDO0lBQ0QsT0FBTyxFQUFFLFFBQVEsRUFBRSxTQUFTLEVBQUUsQ0FBQztBQUNqQyxDQUFDLENBQUM7QUFFRjs7OztHQUlHO0FBQ0gsTUFBTSxDQUFDLE1BQU0sdUJBQXVCLEdBQUcsS0FBSyxFQUMxQyxVQUFzQyxFQUFFLEVBQ0wsRUFBRTtJQUNyQyxNQUFNLEdBQUcsR0FBRyxPQUFPLENBQUMsR0FBRyxJQUFJLENBQUMsR0FBRyxFQUFFLENBQUMsSUFBSSxJQUFJLEVBQUUsQ0FBQyxDQUFDO0lBQzlDLE1BQU0sUUFBUSxHQUFHLEdBQUcsRUFBRSxDQUFDLE9BQU8sRUFBRSxDQUFDO0lBQ2pDLE1BQU0sVUFBVSxHQUFHLE1BQU0sdUJBQXVCLENBQUMsT0FBTyxDQUFDLENBQUM7SUFDMUQsTUFBTSxLQUFLLEdBQTZCO1FBQ3RDLGVBQWUsRUFBRSxVQUFVLENBQUMsZUFBZTtRQUMzQyxRQUFRLEVBQUUsVUFBVSxDQUFDLFFBQVE7UUFDN0IsWUFBWSxFQUFFLFVBQVUsQ0FBQyxZQUFZO1FBQ3JDLGVBQWUsRUFBRSxVQUFVLENBQUMsZUFBZTtRQUMzQyxRQUFRLEVBQUUsSUFBSSxJQUFJLENBQUMsUUFBUSxDQUFDLENBQUMsV0FBVyxFQUFFO1FBQzFDLFNBQVMsRUFBRSxJQUFJLElBQUksQ0FBQyxRQUFRLEdBQUcsOEJBQThCLENBQUMsQ0FBQyxXQUFXLEVBQUU7S0FDN0UsQ0FBQztJQUNGLE9BQU87UUFDTCxNQUFNLEVBQUUsRUFBRSxJQUFJLEVBQUUsUUFBUSxFQUFFLGVBQWUsRUFBRSxLQUFLLENBQUMsZUFBZSxFQUFFLFFBQVEsRUFBRSxLQUFLLENBQUMsUUFBUSxFQUFFLFNBQVMsRUFBRSxLQUFLLENBQUMsU0FBUyxFQUFFO1FBQ3hILEtBQUs7S0FDTixDQUFDO0FBQ0osQ0FBQyxDQUFDO0FBRUY7Ozs7OztHQU1HO0FBQ0gsTUFBTSxDQUFDLE1BQU0sc0JBQXNCLEdBQUcsS0FBSyxFQUN6QyxLQUErQixFQUMvQixVQUFzQyxFQUFFLEVBQ04sRUFBRTtJQUNwQyxNQUFNLEVBQUUsU0FBUyxFQUFFLEdBQUcsU0FBUyxDQUFDLEtBQUssQ0FBQyxDQUFDO0lBQ3ZDLE1BQU0sR0FBRyxHQUFHLE9BQU8sQ0FBQyxHQUFHLElBQUksQ0FBQyxHQUFHLEVBQUUsQ0FBQyxJQUFJLElBQUksRUFBRSxDQUFDLENBQUM7SUFDOUMsTUFBTSxTQUFTLEdBQUcsU0FBUyxHQUFHLEdBQUcsRUFBRSxDQUFDLE9BQU8sRUFBRSxDQUFDO0lBQzlDLElBQUksU0FBUyxJQUFJLENBQUM7UUFBRSxPQUFPLEVBQUUsTUFBTSxFQUFFLFNBQVMsRUFBRSxDQUFDO0lBQ2pELE1BQU0sSUFBSSxHQUFHLE1BQU0sd0JBQXdCLENBQUM7UUFDMUMsZUFBZSxFQUFFLEtBQUssQ0FBQyxlQUFlLEVBQUUsUUFBUSxFQUFFLEtBQUssQ0FBQyxRQUFRO1FBQ2hFLFlBQVksRUFBRSxLQUFLLENBQUMsWUFBWSxFQUFFLGVBQWUsRUFBRSxLQUFLLENBQUMsZUFBZTtLQUN6RSxFQUFFLEVBQUUsS0FBSyxFQUFFLE9BQU8sQ0FBQyxLQUFLLEVBQUUsTUFBTSxFQUFFLE9BQU8sQ0FBQyxNQUFNLEVBQUUsU0FBUyxFQUFFLElBQUksQ0FBQyxHQUFHLENBQUMsU0FBUyxFQUFFLGtCQUFrQixDQUFDLEVBQUUsQ0FBQyxDQUFDO0lBQ3pHLElBQUksSUFBSSxDQUFDLE1BQU0sS0FBSyxTQUFTLEVBQUUsQ0FBQztRQUM5QixNQUFNLFVBQVUsR0FBRyxJQUFJLENBQUMsR0FBRyxDQUFDLEdBQUcsRUFBRSxDQUFDLE9BQU8sRUFBRSxHQUFHLEtBQUssQ0FBQyxlQUFlLEdBQUcsSUFBSSxFQUFFLFNBQVMsQ0FBQyxDQUFDO1FBQ3ZGLE9BQU8sRUFBRSxNQUFNLEVBQUUsU0FBUyxFQUFFLFVBQVUsRUFBRSxJQUFJLElBQUksQ0FBQyxVQUFVLENBQUMsQ0FBQyxXQUFXLEVBQUUsRUFBRSxDQUFDO0lBQy9FLENBQUM7SUFDRCxNQUFNLE1BQU0sR0FBRyxNQUFNLGlDQUFpQyxDQUFDLElBQUksQ0FBQyxhQUFhLEVBQUUsRUFBRSxLQUFLLEVBQUUsT0FBTyxDQUFDLEtBQUssRUFBRSxNQUFNLEVBQUUsT0FBTyxDQUFDLE1BQU0sRUFBRSxDQUFDLENBQUM7SUFDN0gsSUFBSSxVQUFVLENBQUM7SUFDZixJQUFJLENBQUM7UUFBQyxVQUFVLEdBQUcsd0JBQXdCLENBQUMsTUFBTSxDQUFDLENBQUM7SUFBQyxDQUFDO0lBQ3RELE1BQU0sQ0FBQztRQUFDLE1BQU0sSUFBSSxlQUFlLENBQUMsMkNBQTJDLEVBQUUsRUFBRSxjQUFjLEVBQUUsZ0JBQWdCLEVBQUUsQ0FBQyxDQUFDO0lBQUMsQ0FBQztJQUN2SCxPQUFPLEVBQUUsTUFBTSxFQUFFLFVBQVUsRUFBRSxNQUFNLEVBQUUsRUFBRSxVQUFVLEVBQUUsT0FBTyxFQUFFLHlCQUF5QixDQUFDLFVBQVUsQ0FBQyxFQUFFLEVBQUUsQ0FBQztBQUN4RyxDQUFDLENBQUMifQ==
@@ -0,0 +1,195 @@
1
+ import type { IOpenAiLoginResult, IOpenAiOAuthCredential } from './signin.interfaces.js';
2
+ /**
3
+ * The state of a stored grant:
4
+ * - `active`: its tokens are usable and refreshed when due. `retryAt` is set after a refresh that was never sent.
5
+ * - `refresh_uncertain`: a refresh may have been sent. The refresh token may already be spent, so it is never
6
+ * sent again; the attempt either commits the rotated tokens or, once `attempt.expiresAt` passed, the grant
7
+ * needs a new sign-in.
8
+ * - `reauth_required`: only a new sign-in brings the grant back.
9
+ */
10
+ export type TOpenAiGrantState = 'active' | 'refresh_uncertain' | 'reauth_required';
11
+ /**
12
+ * Why a grant needs a new sign-in:
13
+ * - `refresh_failed`: a refresh was sent and did not produce tokens (refused, or its outcome is unknown);
14
+ * - `refresh_interrupted`: a refresh may have been sent and its attempt ended without an answer;
15
+ * - `identity_changed`: a refresh answered for another workspace or user.
16
+ */
17
+ export type TOpenAiGrantReauthReason = 'refresh_failed' | 'refresh_interrupted' | 'identity_changed';
18
+ /**
19
+ * One grant as the store keeps it. The store seals `credential` at rest and hands the record back unchanged;
20
+ * everything else is credential-free.
21
+ */
22
+ export interface IOpenAiGrantRecord {
23
+ /** Rises by one with every change of the record; `compareAndSet` and `remove` compare it. */
24
+ revision: number;
25
+ /** Rises by one with every new access token; a provider's 401 names the generation it rejected. */
26
+ generation: number;
27
+ state: TOpenAiGrantState;
28
+ credential: IOpenAiOAuthCredential;
29
+ /** The ChatGPT workspace the grant acts in; a refresh that states another is refused. */
30
+ accountId: string;
31
+ /** The ChatGPT user the grant was signed in as; a refresh that states another is refused. */
32
+ userId: string;
33
+ isFedrampAccount: boolean;
34
+ /** When the current access token expires, ISO 8601. */
35
+ accessExpiresAt: string;
36
+ /** When the current tokens were issued, by the sign-in or the last refresh, ISO 8601. */
37
+ refreshedAt: string;
38
+ /** After a refresh that was never sent: the earliest time to try again. */
39
+ retryAt: string | null;
40
+ retryCount: number;
41
+ /** While `refresh_uncertain`: the attempt that may have sent the refresh token, and when it is given up. */
42
+ attempt: {
43
+ id: string;
44
+ expiresAt: string;
45
+ } | null;
46
+ /** While `reauth_required`: why. */
47
+ reauthReason: TOpenAiGrantReauthReason | null;
48
+ }
49
+ /**
50
+ * Where grants live, supplied by the caller. Every write names the revision it read, so two processes that act on
51
+ * one grant cannot both win: `compareAndSet` and `remove` must compare and write in one atomic step, as a
52
+ * database's conditional update does. A store keeps `credential` sealed at rest and never logs it.
53
+ */
54
+ export interface IOpenAiGrantStore {
55
+ load(grantId: string): Promise<IOpenAiGrantRecord | null>;
56
+ /** Stores a new grant; `conflict` when one exists under the id. */
57
+ create(grantId: string, record: IOpenAiGrantRecord): Promise<'stored' | 'conflict'>;
58
+ /** Replaces the grant only while it is still at `expectedRevision`. */
59
+ compareAndSet(grantId: string, expectedRevision: number, next: IOpenAiGrantRecord): Promise<'stored' | 'conflict'>;
60
+ /** Deletes the grant only while it is still at `expectedRevision`; `conflict` when it moved or is gone. */
61
+ remove(grantId: string, expectedRevision: number): Promise<'removed' | 'conflict'>;
62
+ }
63
+ /** Access for one request: what FlexHarness's ChatGPT connection takes, with the generation it belongs to. */
64
+ export interface IOpenAiGrantAccess {
65
+ accessToken: string;
66
+ accountId: string;
67
+ isFedrampAccount: boolean;
68
+ expiresAt: string;
69
+ generation: number;
70
+ }
71
+ export interface IOpenAiGrantResolveOptions {
72
+ /** Stops waiting. A refresh already sent keeps running to its end, so its outcome is recorded. */
73
+ signal?: AbortSignal;
74
+ /** The generation the provider just rejected with 401; the grant is refreshed if it is still the current one. */
75
+ rejectedGeneration?: number;
76
+ /** How long the access must stay valid, in milliseconds (at most one hour). Defaults to one minute. */
77
+ minValidityMs?: number;
78
+ }
79
+ /** What a caller may show about a grant: no token. */
80
+ export interface IOpenAiGrantStatus {
81
+ revision: number;
82
+ generation: number;
83
+ state: TOpenAiGrantState;
84
+ reauthReason: TOpenAiGrantReauthReason | null;
85
+ accountId: string;
86
+ isFedrampAccount: boolean;
87
+ plan: string | null;
88
+ accessExpiresAt: string;
89
+ refreshedAt: string;
90
+ retryAt: string | null;
91
+ }
92
+ /**
93
+ * What the service said about revoking a removed grant:
94
+ * - `confirmed`: it revoked the grant's current refresh token;
95
+ * - `unconfirmed`: it revoked the stored token, but a refresh may have replaced it at the service before;
96
+ * - `failed`: it did not confirm the revocation. The sign-in may still be valid at the service.
97
+ */
98
+ export type TOpenAiGrantRevocation = 'confirmed' | 'unconfirmed' | 'failed';
99
+ /**
100
+ * Why a grant cannot answer:
101
+ * - `not_found`: no grant under the id;
102
+ * - `reauth_required`: only a new sign-in brings it back (`reauthReason` says why);
103
+ * - `access_not_fresh`: the grant is intact, but the service could not renew it yet (`retryAt`);
104
+ * - `invalid_argument`, `invalid_record`: the caller's input or the stored record;
105
+ * - `aborted`: the caller's signal;
106
+ * - `contended`: the record kept changing under this call; try again.
107
+ */
108
+ export type TOpenAiGrantErrorCode = 'not_found' | 'reauth_required' | 'access_not_fresh' | 'invalid_argument' | 'invalid_record' | 'aborted' | 'contended';
109
+ /** A grant failure. Its message is fixed and names no token. */
110
+ export declare class OpenAiGrantError extends Error {
111
+ readonly code: TOpenAiGrantErrorCode;
112
+ readonly details: {
113
+ reauthReason?: TOpenAiGrantReauthReason;
114
+ retryAt?: string;
115
+ };
116
+ constructor(code: TOpenAiGrantErrorCode, details?: {
117
+ reauthReason?: TOpenAiGrantReauthReason;
118
+ retryAt?: string;
119
+ });
120
+ }
121
+ export interface IOpenAiGrantManagerOptions {
122
+ store: IOpenAiGrantStore;
123
+ fetch?: typeof fetch;
124
+ now?: () => number;
125
+ /** How often a process polls a grant another process is refreshing. Defaults to 250 ms. */
126
+ waitIntervalMs?: number;
127
+ }
128
+ /**
129
+ * Keeps ChatGPT sign-ins usable for any number of processes that share one store: it answers current access for
130
+ * each request, refreshes the tokens when they are due or the provider rejected exactly the current generation,
131
+ * and never sends a refresh token whose outcome is unknown a second time. Within a process one refresh per grant
132
+ * runs at a time; across processes the store's compare-and-set on the revision decides, and a process that finds
133
+ * another's refresh in flight waits for its outcome. It holds no credential of its own: every call reads the
134
+ * store, and no error, status or return value carries a refresh or ID token.
135
+ */
136
+ export declare class OpenAiGrantManager {
137
+ private readonly store;
138
+ private readonly fetcher;
139
+ private readonly now;
140
+ private readonly waitIntervalMs;
141
+ private readonly refreshes;
142
+ constructor(options: IOpenAiGrantManagerOptions);
143
+ /** The record a new sign-in starts: generation one, active. */
144
+ private recordOf;
145
+ private statusOf;
146
+ private load;
147
+ /** Stores the grant a device sign-in produced. Throws `OpenAiGrantError` `contended` when a grant exists under the id. */
148
+ create(grantId: string, result: IOpenAiLoginResult): Promise<IOpenAiGrantStatus>;
149
+ /**
150
+ * Replaces a grant with a new sign-in, over the revision the caller read: a grant that needs a new sign-in is
151
+ * repaired this way. The generation keeps rising, so a 401 of the old tokens cannot be mistaken for the new.
152
+ * The replaced tokens are not revoked.
153
+ */
154
+ replace(grantId: string, expectedRevision: number, result: IOpenAiLoginResult): Promise<IOpenAiGrantStatus>;
155
+ /** The grant's state without any token; `null` when there is none. */
156
+ status(grantId: string): Promise<IOpenAiGrantStatus | null>;
157
+ private isDue;
158
+ private wait;
159
+ /** Waits for a promise, or rejects `aborted` as soon as the signal fires; the promise runs on regardless. */
160
+ private awaitOrAbort;
161
+ /** Moves a record on over the revision it was read at; `false` when it moved meanwhile. */
162
+ private write;
163
+ /**
164
+ * Current access for one provider request. The access stays valid for `minValidityMs` at least; tokens that
165
+ * are due, or whose generation the provider just rejected, are refreshed first.
166
+ * Throws `OpenAiGrantError`: `reauth_required` when only a new sign-in helps, `access_not_fresh` when the
167
+ * service could not renew the tokens yet.
168
+ */
169
+ resolveAccess(grantId: string, options?: IOpenAiGrantResolveOptions): Promise<IOpenAiGrantAccess>;
170
+ /**
171
+ * Refreshes a grant that is due (by its access expiry or its age) and answers its state; one that is not due is
172
+ * left as it is. For a periodic job, so a sign-in that no request uses stays alive.
173
+ */
174
+ keepFresh(grantId: string, options?: {
175
+ signal?: AbortSignal;
176
+ }): Promise<IOpenAiGrantStatus | null>;
177
+ /**
178
+ * A record whose refresh may have been sent: this process waits for its own attempt; another process's attempt
179
+ * is waited for until the record moves on or the attempt's lease passes, and then the grant needs a new sign-in.
180
+ */
181
+ private settleAttempt;
182
+ /** One refresh per grant in this process; a caller that stops waiting leaves it running to its end. */
183
+ private refresh;
184
+ private performRefresh;
185
+ /**
186
+ * Removes a grant and then asks the service to revoke its refresh token. The grant is removed whatever the
187
+ * service answers; `revocation` says what it answered. `removed: false` when there was no grant.
188
+ */
189
+ remove(grantId: string, options?: {
190
+ signal?: AbortSignal;
191
+ }): Promise<{
192
+ removed: boolean;
193
+ revocation: TOpenAiGrantRevocation | null;
194
+ }>;
195
+ }