@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.
- package/dist_ts_signin/index.d.ts +5 -0
- package/dist_ts_signin/index.js +6 -0
- package/dist_ts_signin/plugins.d.ts +2 -0
- package/dist_ts_signin/plugins.js +4 -0
- package/dist_ts_signin/signin.credential.d.ts +29 -0
- package/dist_ts_signin/signin.credential.js +78 -0
- package/dist_ts_signin/signin.device.d.ts +56 -0
- package/dist_ts_signin/signin.device.js +85 -0
- package/dist_ts_signin/signin.grants.d.ts +195 -0
- package/dist_ts_signin/signin.grants.js +408 -0
- package/dist_ts_signin/signin.interfaces.d.ts +62 -0
- package/dist_ts_signin/signin.interfaces.js +3 -0
- package/dist_ts_signin/signin.protocol.d.ts +93 -0
- package/dist_ts_signin/signin.protocol.js +397 -0
- package/license.md +21 -0
- package/openai-codex-license.txt +201 -0
- package/package.json +37 -0
- package/readme.md +163 -0
- package/third-party-notices.md +34 -0
- package/ts_signin/index.ts +5 -0
- package/ts_signin/plugins.ts +4 -0
- package/ts_signin/readme.md +163 -0
- package/ts_signin/signin.credential.ts +91 -0
- package/ts_signin/signin.device.ts +137 -0
- package/ts_signin/signin.grants.ts +518 -0
- package/ts_signin/signin.interfaces.ts +75 -0
- package/ts_signin/signin.protocol.ts +454 -0
|
@@ -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,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,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic2lnbmluLmNyZWRlbnRpYWwuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi90c19zaWduaW4vc2lnbmluLmNyZWRlbnRpYWwudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsMkpBQTJKO0FBRTNKLE9BQU8sRUFDTCxlQUFlLEVBQUUsc0JBQXNCLEVBQUUsbUJBQW1CLEdBQzdELE1BQU0sc0JBQXNCLENBQUM7QUFLOUIsa0dBQWtHO0FBQ2xHLE1BQU0sT0FBTyxxQkFBc0IsU0FBUSxLQUFLO0lBQzlDO1FBQ0UsS0FBSyxDQUFDLG1DQUFtQyxDQUFDLENBQUM7UUFDM0MsSUFBSSxDQUFDLElBQUksR0FBRyx1QkFBdUIsQ0FBQztJQUN0QyxDQUFDO0NBQ0Y7QUFFRDs7O0dBR0c7QUFDSCxNQUFNLENBQUMsTUFBTSx3QkFBd0IsR0FBRyxDQUFDLFVBQWtDLEVBQW1CLEVBQUU7SUFDOUYsSUFBSSxVQUFVLEtBQUssSUFBSSxJQUFJLE9BQU8sVUFBVSxLQUFLLFFBQVEsSUFBSSxVQUFVLENBQUMsSUFBSSxLQUFLLGNBQWM7V0FDMUYsVUFBVSxDQUFDLFVBQVUsS0FBSyxRQUFRLElBQUksT0FBTyxVQUFVLENBQUMsV0FBVyxLQUFLLFFBQVEsSUFBSSxDQUFDLFVBQVUsQ0FBQyxXQUFXO1dBQzNHLE9BQU8sVUFBVSxDQUFDLFlBQVksS0FBSyxRQUFRLElBQUksQ0FBQyxVQUFVLENBQUMsWUFBWTtXQUN2RSxPQUFPLFVBQVUsQ0FBQyxPQUFPLEtBQUssUUFBUSxJQUFJLENBQUMsVUFBVSxDQUFDLE9BQU8sRUFBRSxDQUFDO1FBQ25FLE1BQU0sSUFBSSxxQkFBcUIsRUFBRSxDQUFDO0lBQ3BDLENBQUM7SUFDRCxJQUFJLENBQUM7UUFDSCxPQUFPLEVBQUUsV0FBVyxFQUFFLFVBQVUsQ0FBQyxXQUFXLEVBQUUsWUFBWSxFQUFFLFVBQVUsQ0FBQyxZQUFZLEVBQUUsT0FBTyxFQUFFLFVBQVUsQ0FBQyxPQUFPO1lBQzlHLE1BQU0sRUFBRSxzQkFBc0Is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
|
|
@@ -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,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic2lnbmluLmRldmljZS5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3RzX3NpZ25pbi9zaWduaW4uZGV2aWNlLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLDJKQUEySjtBQUUzSixPQUFPLEVBQ0wsaUNBQWlDLEVBQUUsOEJBQThCLEVBQUUsZUFBZSxFQUFFLHdCQUF3QixFQUM1Ryx1QkFBdUIsR0FDeEIsTUFBTSxzQkFBc0IsQ0FBQztBQUM5QixPQUFPLEVBQUUsd0JBQXdCLEVBQUUseUJBQXlCLEVBQUUsTUFBTSx3QkFBd0IsQ0FBQztBQUc3RixNQUFNLHNCQUFzQixHQUFHLElBQUksQ0FBQztBQUNwQyxNQUFNLGtCQUFrQixHQUFHLE1BQU0sQ0FBQztBQTBDbEMsTUFBTSxZQUFZLEdBQUcsR0FBb0IsRUFBRSxDQUN6QyxJQUFJLGVBQWUsQ0FBQyx5Q0FBeUMsRUFBRSxFQUFFLGNBQWMsRUFBRSxTQUFTLEVBQUUsQ0FBQyxDQUFDO0FBRWhHLE1BQU0sZUFBZSxHQUFHLENBQUMsS0FBYyxFQUFtQixFQUFFLENBQzFELE9BQU8sS0FBSyxLQUFLLFFBQVEsSUFBSSxLQUFLLENBQUMsTUFBTSxHQUFHLENBQUMsSUFBSSxNQUFNLENBQUMsVUFBVSxDQUFDLEtBQUssRUFBRSxNQUFNLENBQUMsSUFBSSxzQkFBc0IsQ0FBQztBQUU5RyxNQUFNLE1BQU0sR0FBRyxDQUFDLEtBQWMsRUFBVSxFQUFFO0lBQ3hDLElBQUksT0FBTyxLQUFLLEtBQUssUUFBUTtRQUFFLE9BQU8sR0FBRyxDQUFDO0lBQzFDLE1BQU0sSUFBSSxHQUFHLElBQUksQ0FBQyxLQUFLLENBQUMsS0FBSyxDQUFDLENBQUM7SUFDL0IsT0FBTyxNQUFNLENBQUMsUUFBUSxDQUFDLElBQUksQ0FBQyxJQUFJLElBQUksSUFBSSxDQUFDLElBQUksQ0FBQyxDQUFDLFdBQVcsRUFBRSxLQUFLLEtBQUssQ0FBQyxDQUFDLENBQUMsSUFBSSxDQUFDLENBQUMsQ0FBQyxHQUFHLENBQUM7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
|
|
@@ -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
|
+
}
|