@eddyskywalker/dsh-chatgpt-subscription 0.13.2 → 0.14.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/CHANGELOG.md +30 -0
- package/README.md +75 -8
- package/lib/client.js +183 -115
- package/lib/client.js.map +1 -1
- package/lib/index.js +648 -58
- package/lib/types/client/CodexSubscriptionSection.d.ts +1 -1
- package/lib/types/client/CodexSubscriptionSection.d.ts.map +1 -1
- package/lib/types/client/ProviderHubSection.d.ts.map +1 -1
- package/lib/types/client/api.d.ts +12 -0
- package/lib/types/client/api.d.ts.map +1 -1
- package/lib/types/client/claude/ClaudeSection.d.ts.map +1 -1
- package/lib/types/client/claude/locales.d.ts +2 -11
- package/lib/types/client/claude/locales.d.ts.map +1 -1
- package/lib/types/client/locales.d.ts +13 -1
- package/lib/types/client/locales.d.ts.map +1 -1
- package/lib/types/host/claude/model-catalog.d.ts +42 -20
- package/lib/types/host/claude/model-catalog.d.ts.map +1 -1
- package/lib/types/host/claude/types.d.ts +7 -2
- package/lib/types/host/claude/types.d.ts.map +1 -1
- package/lib/types/host/codex-account-pool.d.ts +129 -1
- package/lib/types/host/codex-account-pool.d.ts.map +1 -1
- package/lib/types/host/codex-adopt.d.ts +255 -0
- package/lib/types/host/codex-adopt.d.ts.map +1 -0
- package/lib/types/host/routes.d.ts +11 -1
- package/lib/types/host/routes.d.ts.map +1 -1
- package/lib/types/host/token-store.d.ts +26 -1
- package/lib/types/host/token-store.d.ts.map +1 -1
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/shared/contracts.d.ts +22 -0
- package/lib/types/shared/contracts.d.ts.map +1 -1
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -993,7 +993,7 @@ function parseStoredCredentials(value) {
|
|
|
993
993
|
if (typeof candidate !== "string") throw new Error(`${key} is invalid`);
|
|
994
994
|
return candidate;
|
|
995
995
|
};
|
|
996
|
-
|
|
996
|
+
const credentials = {
|
|
997
997
|
accessToken: record.accessToken,
|
|
998
998
|
refreshToken: record.refreshToken,
|
|
999
999
|
expiresAt: record.expiresAt,
|
|
@@ -1002,6 +1002,16 @@ function parseStoredCredentials(value) {
|
|
|
1002
1002
|
email: optional("email"),
|
|
1003
1003
|
planType: optional("planType")
|
|
1004
1004
|
};
|
|
1005
|
+
if (record.adopted === true) {
|
|
1006
|
+
const source = optional("source");
|
|
1007
|
+
if (source !== void 0) {
|
|
1008
|
+
credentials.adopted = true;
|
|
1009
|
+
credentials.source = source;
|
|
1010
|
+
const sourcePath = optional("sourcePath");
|
|
1011
|
+
if (sourcePath !== void 0) credentials.sourcePath = sourcePath;
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
return credentials;
|
|
1005
1015
|
}
|
|
1006
1016
|
//#endregion
|
|
1007
1017
|
//#region src/host/token-store-windows.ts
|
|
@@ -2895,7 +2905,433 @@ function poolQuota(fetchedAt, windows) {
|
|
|
2895
2905
|
};
|
|
2896
2906
|
}
|
|
2897
2907
|
//#endregion
|
|
2908
|
+
//#region src/host/codex-adopt.ts
|
|
2909
|
+
/**
|
|
2910
|
+
* OPT-IN adoption of a pre-existing Codex CLI sign-in.
|
|
2911
|
+
*
|
|
2912
|
+
* WHAT THIS IS FOR. A user who already runs the unmodified Codex CLI is already
|
|
2913
|
+
* signed in to the very subscription this provider serves. Asking them to sign in
|
|
2914
|
+
* a second time is pure friction, so this module offers the one thing that
|
|
2915
|
+
* removes it: reading back the credential the CLI itself stored in
|
|
2916
|
+
* `$CODEX_HOME/auth.json` (`~/.codex/auth.json` when the variable is unset). It
|
|
2917
|
+
* is opt-in — nothing here runs until the user asks for it.
|
|
2918
|
+
*
|
|
2919
|
+
* ---------------------------------------------------------------------------
|
|
2920
|
+
* RULE 1 — THIS MODULE NEVER WRITES. IT IS A READER, AND ONLY A READER.
|
|
2921
|
+
* ---------------------------------------------------------------------------
|
|
2922
|
+
* It must not create, modify, move or delete any file belonging to the Codex CLI.
|
|
2923
|
+
* No file this module touches is ever opened for writing, no directory is
|
|
2924
|
+
* created, nothing is renamed and nothing is unlinked. The guarantee is
|
|
2925
|
+
* structural rather than a promise in prose: the module imports exactly two
|
|
2926
|
+
* functions BY NAME from 'node:fs/promises' — readFile and stat — instead of the
|
|
2927
|
+
* whole namespace, so introducing a write would require a visible change to the
|
|
2928
|
+
* import list, which is the line a reviewer reads first. Nothing exported here
|
|
2929
|
+
* writes. The tests assert the stronger, observable form of the rule: a
|
|
2930
|
+
* credential file's bytes, size and mtime are identical before and after both
|
|
2931
|
+
* entry points run, and its directory gains no entries.
|
|
2932
|
+
*
|
|
2933
|
+
* ---------------------------------------------------------------------------
|
|
2934
|
+
* RULE 2 — PRESENCE AND CONTENT ARE TWO SEPARATE FUNCTIONS
|
|
2935
|
+
* ---------------------------------------------------------------------------
|
|
2936
|
+
* codexCliCredentialPresence() is cheap enough to call on plugin startup and
|
|
2937
|
+
* answers only yes/no: it stats the candidate and never opens it for reading. It
|
|
2938
|
+
* exists so the settings card can offer the import before the user has agreed to
|
|
2939
|
+
* it, and so a user who never opts in never has their token read at all.
|
|
2940
|
+
*
|
|
2941
|
+
* readCodexCliCredentials() is the full read, and it is called only AFTER the
|
|
2942
|
+
* user explicitly opts in. Keeping the two apart is the whole point: a startup
|
|
2943
|
+
* path that slurped the file "just in case" would make the opt-in decorative.
|
|
2944
|
+
*
|
|
2945
|
+
* ---------------------------------------------------------------------------
|
|
2946
|
+
* RULE 3 — AN ADOPTED CREDENTIAL IS A SNAPSHOT, AND THIS PLUGIN NEVER REFRESHES IT
|
|
2947
|
+
* ---------------------------------------------------------------------------
|
|
2948
|
+
* An adopted credential is used only while its own accessToken is still valid.
|
|
2949
|
+
* Once it is expired the pool reports it as expired and tells the user to sign in
|
|
2950
|
+
* again in the Codex CLI and re-adopt. That is deliberate, not a gap: the
|
|
2951
|
+
* ChatGPT refresh token the CLI stores ROTATES, and the CLI refreshes its own
|
|
2952
|
+
* file in place. Two processes refreshing the same grant therefore invalidate
|
|
2953
|
+
* each other — whoever loses the race holds a token the server has already
|
|
2954
|
+
* retired, and the user ends up signed OUT of the Codex CLI by this plugin's good
|
|
2955
|
+
* intentions. An in-process single-flight cannot fix that, because the race is
|
|
2956
|
+
* across PROCESSES: this plugin and the CLI are separate programs with separate
|
|
2957
|
+
* memories, and the only shared state between them is the file — which this
|
|
2958
|
+
* module must not write back into (Rule 1).
|
|
2959
|
+
*
|
|
2960
|
+
* So the honest contract is: borrow the access token for as long as the CLI itself
|
|
2961
|
+
* vouched for it, and never touch the refresh token. Expiry is therefore part of
|
|
2962
|
+
* the adoption decision rather than a detail — see
|
|
2963
|
+
* {@link isAdoptedCodexCredentialExpired}.
|
|
2964
|
+
*
|
|
2965
|
+
* The marker the pool chunk must honour is the literal 'adopted: true' plus
|
|
2966
|
+
* {@link isAdoptedCodexCredential}; an account carrying it must be treated as
|
|
2967
|
+
* read-only, non-refreshable and disposable. There is deliberately no refresh
|
|
2968
|
+
* function in this module to find in the first place.
|
|
2969
|
+
*
|
|
2970
|
+
* ---------------------------------------------------------------------------
|
|
2971
|
+
* RULE 4 — AN UNRECOGNISED SHAPE YIELDS undefined, NEVER AN EXCEPTION
|
|
2972
|
+
* ---------------------------------------------------------------------------
|
|
2973
|
+
* The card shows 'unrecognised local sign-in format' for undefined, which is a far
|
|
2974
|
+
* better outcome than a thrown error escaping into the settings route. Nothing is
|
|
2975
|
+
* guessed: a document in another auth_mode, a missing or non-object tokens
|
|
2976
|
+
* block, a missing access or refresh token, an access token that is not a JWT
|
|
2977
|
+
* with a usable 'exp', an unreadable or unparsable file — all of them are
|
|
2978
|
+
* undefined. Extra and unknown fields are ignored rather than rejected, so a
|
|
2979
|
+
* future CLI that adds one does not break adoption.
|
|
2980
|
+
*
|
|
2981
|
+
* THE TRAP THIS PARSER EXISTS TO AVOID. A Codex auth.json is NOT a flat bag of
|
|
2982
|
+
* tokens. It is a document that may carry an API key alongside the OAuth tokens,
|
|
2983
|
+
* and a "find the first accessToken anywhere in the JSON" scan would pick
|
|
2984
|
+
* whichever one the serializer happened to emit first — which is how an API key
|
|
2985
|
+
* or an unrelated token ends up presented to the Responses API as the user's
|
|
2986
|
+
* subscription credential. The read is therefore a direct, specific lookup of
|
|
2987
|
+
* tokens.access_token and its siblings — never a tree walk — and the test suite
|
|
2988
|
+
* holds a fixture carrying an unrelated token beside the real one and asserts
|
|
2989
|
+
* which comes back.
|
|
2990
|
+
*
|
|
2991
|
+
* ---------------------------------------------------------------------------
|
|
2992
|
+
* THE CLAIMS ARE DERIVED LOCALLY, NEVER ASKED OF ANYBODY
|
|
2993
|
+
* ---------------------------------------------------------------------------
|
|
2994
|
+
* expiresAt comes from the access token's own 'exp' claim, email and planType
|
|
2995
|
+
* from the id token's claims. The JWT payload is decoded in process; no token is
|
|
2996
|
+
* ever sent anywhere to be inspected, and no token value is logged or returned
|
|
2997
|
+
* from anything but the credential object the caller asked for.
|
|
2998
|
+
*
|
|
2999
|
+
* WHY EXPIRY IS REFUSED RATHER THAN DEFAULTED. A snapshot whose validity cannot
|
|
3000
|
+
* be established must not be used. Rather than stamping "now" or "never" and
|
|
3001
|
+
* hoping, this module yields undefined: the card then says the local sign-in
|
|
3002
|
+
* format is unrecognised, which is true, instead of importing a credential that
|
|
3003
|
+
* would 401 on the first request.
|
|
3004
|
+
*/
|
|
3005
|
+
/**
|
|
3006
|
+
* Environment variable that relocates the Codex CLI's home directory.
|
|
3007
|
+
*
|
|
3008
|
+
* Highest precedence, and honoured exactly as the CLI honours it: when it is set
|
|
3009
|
+
* to a non-blank value it names the home that holds auth.json, whatever the
|
|
3010
|
+
* platform's conventional location happens to be. This is also what makes the
|
|
3011
|
+
* whole module testable without touching a developer's real profile.
|
|
3012
|
+
*/
|
|
3013
|
+
const CODEX_HOME_ENV = "CODEX_HOME";
|
|
3014
|
+
/** File name of the credential document inside the Codex home. */
|
|
3015
|
+
const CODEX_CLI_CREDENTIAL_FILE = "auth.json";
|
|
3016
|
+
/**
|
|
3017
|
+
* Top-level key holding the OAuth tokens.
|
|
3018
|
+
*
|
|
3019
|
+
* Named as a constant because the specificity of this key IS the defence
|
|
3020
|
+
* described in the module comment: the read addresses this key, not the first
|
|
3021
|
+
* token it can find anywhere in the document.
|
|
3022
|
+
*/
|
|
3023
|
+
const CODEX_CLI_TOKENS_KEY = "tokens";
|
|
3024
|
+
/** Where an adopted snapshot came from, for the card and for the pool record. */
|
|
3025
|
+
const CODEX_CLI_CREDENTIAL_SOURCE = "codex";
|
|
3026
|
+
/** Namespace the id token carries the ChatGPT account claims under. */
|
|
3027
|
+
const CODEX_AUTH_CLAIMS_KEY = "https://api.openai.com/auth";
|
|
3028
|
+
/**
|
|
3029
|
+
* What the user is told when an adopted credential has aged out.
|
|
3030
|
+
*
|
|
3031
|
+
* Exported so the pool report and the settings card say the same sentence, and
|
|
3032
|
+
* so the instruction is stated once: the re-sign-in happens in the CODEX CLI, not
|
|
3033
|
+
* here, and the import has to be repeated afterwards.
|
|
3034
|
+
*/
|
|
3035
|
+
const ADOPTED_CODEX_CREDENTIAL_EXPIRED_HINT = "The sign-in imported from the Codex CLI has expired. Sign in again in the Codex CLI, then import it again from this settings card.";
|
|
3036
|
+
function isRecord$31(value) {
|
|
3037
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3038
|
+
}
|
|
3039
|
+
/** A trimmed, non-empty string, or nothing. */
|
|
3040
|
+
function nonEmptyString$5(value) {
|
|
3041
|
+
if (typeof value !== "string") return void 0;
|
|
3042
|
+
const trimmed = value.trim();
|
|
3043
|
+
return trimmed === "" ? void 0 : trimmed;
|
|
3044
|
+
}
|
|
3045
|
+
/**
|
|
3046
|
+
* The Codex CLI's home directory.
|
|
3047
|
+
*
|
|
3048
|
+
* The environment override wins whenever it is set to a non-blank value — a blank
|
|
3049
|
+
* or whitespace-only variable is treated as unset rather than as "the directory
|
|
3050
|
+
* whose name is empty", which on Windows would resolve to a drive-relative path.
|
|
3051
|
+
* Otherwise the conventional home/.codex directory is used, os.homedir() being
|
|
3052
|
+
* the same %USERPROFILE% on Windows and ~ on POSIX that the CLI itself uses.
|
|
3053
|
+
*
|
|
3054
|
+
* Read at CALL time rather than captured in a module constant: a test (or an
|
|
3055
|
+
* embedder) that sets the variable after import must see the change.
|
|
3056
|
+
*/
|
|
3057
|
+
function codexHomeDir() {
|
|
3058
|
+
const override = process.env[CODEX_HOME_ENV]?.trim();
|
|
3059
|
+
if (override) return override;
|
|
3060
|
+
return path.join(os.homedir(), ".codex");
|
|
3061
|
+
}
|
|
3062
|
+
/**
|
|
3063
|
+
* Every path this module will look at, most authoritative first.
|
|
3064
|
+
*
|
|
3065
|
+
* A single candidate, which is a judgement call stated plainly. The Claude reader
|
|
3066
|
+
* in this package also falls back to the conventional home when the environment
|
|
3067
|
+
* override points somewhere else; here the override IS the user's own statement of
|
|
3068
|
+
* where this CLI keeps its sign-in, and honouring a stale scratch directory is
|
|
3069
|
+
* more dangerous than reporting nothing: a snapshot belonging to a profile the CLI
|
|
3070
|
+
* is not currently using would be adopted silently. The path actually consulted is
|
|
3071
|
+
* returned and printed by the caller, so "not found" is always accompanied by the
|
|
3072
|
+
* exact place that was looked at.
|
|
3073
|
+
*
|
|
3074
|
+
* A pure function — no filesystem access at all — so it is safe to call just to
|
|
3075
|
+
* render the card, and so its precedence is testable without touching a disk.
|
|
3076
|
+
*/
|
|
3077
|
+
function codexCliCredentialPaths() {
|
|
3078
|
+
return [path.join(codexHomeDir(), CODEX_CLI_CREDENTIAL_FILE)];
|
|
3079
|
+
}
|
|
3080
|
+
/**
|
|
3081
|
+
* Whether a plain file exists at one path.
|
|
3082
|
+
*
|
|
3083
|
+
* stat, not lstat: a credential file reached through a symlink is a legitimate
|
|
3084
|
+
* layout — a user may keep profiles behind links — and the question asked here is
|
|
3085
|
+
* whether the client has a file there, not how it is reached.
|
|
3086
|
+
*
|
|
3087
|
+
* Any error (ENOENT, EACCES, ENOTDIR, an unsupported path shape) answers "no". A
|
|
3088
|
+
* failure to establish existence is not evidence of existence, and this path runs
|
|
3089
|
+
* on startup where throwing would take the plugin down over a missing file.
|
|
3090
|
+
*/
|
|
3091
|
+
async function existsAsFile$1(candidate) {
|
|
3092
|
+
try {
|
|
3093
|
+
return (await stat(candidate)).isFile();
|
|
3094
|
+
} catch {
|
|
3095
|
+
return false;
|
|
3096
|
+
}
|
|
3097
|
+
}
|
|
3098
|
+
/**
|
|
3099
|
+
* Is there a local Codex CLI sign-in to offer?
|
|
3100
|
+
*
|
|
3101
|
+
* Safe on plugin startup: this NEVER opens a candidate for reading, so a user who
|
|
3102
|
+
* never opts in never has the credential's contents touched — not by this
|
|
3103
|
+
* function, not by the route that renders the card. Only stat runs, which reveals
|
|
3104
|
+
* existence and timestamps and nothing else.
|
|
3105
|
+
*
|
|
3106
|
+
* Deliberately NOT reported here: whether the file is valid, expired, or even
|
|
3107
|
+
* JSON. Answering that would require the read this function exists to avoid, and
|
|
3108
|
+
* the card does not need it — validity is answered by the read, after opt-in, and
|
|
3109
|
+
* lands on the card as either a credential or 'unrecognised local sign-in
|
|
3110
|
+
* format'.
|
|
3111
|
+
*
|
|
3112
|
+
* A boolean, rather than the record the Claude reader returns, because the two
|
|
3113
|
+
* callers want different things and neither needs the other's shape: the status
|
|
3114
|
+
* route wants a flag, and reports the path it consulted beside it. paths
|
|
3115
|
+
* is injectable so tests drive it against fixtures instead of a real profile;
|
|
3116
|
+
* production callers take the default.
|
|
3117
|
+
*/
|
|
3118
|
+
async function codexCliCredentialPresence(paths = codexCliCredentialPaths()) {
|
|
3119
|
+
for (const candidate of paths) if (await existsAsFile$1(candidate)) return true;
|
|
3120
|
+
return false;
|
|
3121
|
+
}
|
|
3122
|
+
/**
|
|
3123
|
+
* The claims of a JWT, decoded in process, or nothing.
|
|
3124
|
+
*
|
|
3125
|
+
* The payload is the second base64url segment; the signature is never touched and
|
|
3126
|
+
* the result is used only for the three non-secret facts below. Never sent
|
|
3127
|
+
* anywhere: a token handed to a decoder service is a token handed away.
|
|
3128
|
+
*
|
|
3129
|
+
* No verification is claimed and none is possible here — the signing key is the
|
|
3130
|
+
* issuer's, and this module has no business holding it. What it does instead is
|
|
3131
|
+
* refuse to invent: a token that does not decode is treated as stating nothing.
|
|
3132
|
+
*/
|
|
3133
|
+
function decodeJwtClaims(token) {
|
|
3134
|
+
const parts = token.split(".");
|
|
3135
|
+
if (parts.length !== 3) return void 0;
|
|
3136
|
+
try {
|
|
3137
|
+
const value = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf8"));
|
|
3138
|
+
return isRecord$31(value) ? value : void 0;
|
|
3139
|
+
} catch {
|
|
3140
|
+
return;
|
|
3141
|
+
}
|
|
3142
|
+
}
|
|
3143
|
+
/**
|
|
3144
|
+
* The access token's expiry, in EPOCH MILLISECONDS, or nothing.
|
|
3145
|
+
*
|
|
3146
|
+
* RFC 7519 states 'exp' in SECONDS since the epoch, and the store's
|
|
3147
|
+
* StoredOAuthCredentials states milliseconds — a thousand-fold difference that is
|
|
3148
|
+
* silent when it goes the wrong way (a credential stamped a thousand times too
|
|
3149
|
+
* late reads as unexpired for centuries) and catastrophic the other. The
|
|
3150
|
+
* conversion is therefore done here, once, with a comment on it.
|
|
3151
|
+
*
|
|
3152
|
+
* Only a finite, non-negative number is accepted. A string, null, NaN, a
|
|
3153
|
+
* negative value or a missing claim are all "this document states no usable
|
|
3154
|
+
* expiry", and the caller's answer to that is to refuse the whole document rather
|
|
3155
|
+
* than import a credential whose validity nothing established.
|
|
3156
|
+
*/
|
|
3157
|
+
function expiryFromAccessToken(accessToken) {
|
|
3158
|
+
const claims = decodeJwtClaims(accessToken);
|
|
3159
|
+
if (claims === void 0) return void 0;
|
|
3160
|
+
const exp = claims.exp;
|
|
3161
|
+
if (typeof exp !== "number" || !Number.isFinite(exp) || exp < 0) return void 0;
|
|
3162
|
+
return Math.round(exp * 1e3);
|
|
3163
|
+
}
|
|
3164
|
+
/**
|
|
3165
|
+
* Map one parsed credential document onto the store's credential shape.
|
|
3166
|
+
*
|
|
3167
|
+
* Pure and exported so the format rules can be tested without a filesystem, and
|
|
3168
|
+
* so a later chunk that reads the document from somewhere else (a backup, a pasted
|
|
3169
|
+
* blob) reuses the one set of rules instead of growing a second, drifting copy.
|
|
3170
|
+
*
|
|
3171
|
+
* Returns undefined for every shape it does not recognise, and never throws — see
|
|
3172
|
+
* Rule 4 in the module comment. The gate is deliberately narrow: the document must
|
|
3173
|
+
* be a JSON object in the ChatGPT auth mode, its tokens block must be an object,
|
|
3174
|
+
* and it must carry BOTH a non-empty access token and a non-empty refresh token.
|
|
3175
|
+
* An access token alone is not adopted because the store's record is a sign-in,
|
|
3176
|
+
* not a bare token, and a record with an empty one could never be re-recognised.
|
|
3177
|
+
*
|
|
3178
|
+
* The unrelated-token trap is handled by construction: value.tokens is addressed
|
|
3179
|
+
* directly and no other key of the document is ever traversed, so whatever else
|
|
3180
|
+
* the file carries cannot reach the result.
|
|
3181
|
+
*/
|
|
3182
|
+
function parseAdoptedCodexCredential(value, sourcePath) {
|
|
3183
|
+
if (!isRecord$31(value)) return void 0;
|
|
3184
|
+
if (value.auth_mode !== "chatgpt") return void 0;
|
|
3185
|
+
const tokens = value[CODEX_CLI_TOKENS_KEY];
|
|
3186
|
+
if (!isRecord$31(tokens)) return void 0;
|
|
3187
|
+
const accessToken = nonEmptyString$5(tokens.access_token);
|
|
3188
|
+
if (accessToken === void 0) return void 0;
|
|
3189
|
+
const refreshToken = nonEmptyString$5(tokens.refresh_token);
|
|
3190
|
+
if (refreshToken === void 0) return void 0;
|
|
3191
|
+
const expiresAt = expiryFromAccessToken(accessToken);
|
|
3192
|
+
if (expiresAt === void 0) return void 0;
|
|
3193
|
+
const credentials = {
|
|
3194
|
+
accessToken,
|
|
3195
|
+
refreshToken,
|
|
3196
|
+
expiresAt
|
|
3197
|
+
};
|
|
3198
|
+
const idToken = nonEmptyString$5(tokens.id_token);
|
|
3199
|
+
if (idToken !== void 0) {
|
|
3200
|
+
credentials.idToken = idToken;
|
|
3201
|
+
const claims = decodeJwtClaims(idToken);
|
|
3202
|
+
const email = claims === void 0 ? void 0 : nonEmptyString$5(claims.email);
|
|
3203
|
+
if (email !== void 0) credentials.email = email;
|
|
3204
|
+
const authClaims = claims === void 0 ? void 0 : claims[CODEX_AUTH_CLAIMS_KEY];
|
|
3205
|
+
const planType = isRecord$31(authClaims) ? nonEmptyString$5(authClaims.chatgpt_plan_type) : void 0;
|
|
3206
|
+
if (planType !== void 0) credentials.planType = planType;
|
|
3207
|
+
}
|
|
3208
|
+
const accountId = nonEmptyString$5(tokens.account_id);
|
|
3209
|
+
if (accountId !== void 0) credentials.accountId = accountId;
|
|
3210
|
+
return {
|
|
3211
|
+
adopted: true,
|
|
3212
|
+
source: CODEX_CLI_CREDENTIAL_SOURCE,
|
|
3213
|
+
sourcePath,
|
|
3214
|
+
credentials
|
|
3215
|
+
};
|
|
3216
|
+
}
|
|
3217
|
+
/**
|
|
3218
|
+
* Read the local Codex CLI sign-in; the first usable candidate wins.
|
|
3219
|
+
*
|
|
3220
|
+
* ONLY call this after the user has explicitly opted in — see the module comment.
|
|
3221
|
+
*
|
|
3222
|
+
* Never throws and never rejects: an absent file, an unreadable file, a file that
|
|
3223
|
+
* is not JSON, and a JSON document of an unrecognised shape all produce undefined,
|
|
3224
|
+
* which the card renders as 'unrecognised local sign-in format'. A settings route
|
|
3225
|
+
* must not be able to fail because a user's file was mid-write.
|
|
3226
|
+
*/
|
|
3227
|
+
async function readCodexCliCredentials(paths = codexCliCredentialPaths()) {
|
|
3228
|
+
for (const candidate of paths) try {
|
|
3229
|
+
const raw = await readFile(candidate, "utf8");
|
|
3230
|
+
const adopted = parseAdoptedCodexCredential(JSON.parse(raw), candidate);
|
|
3231
|
+
if (adopted !== void 0) return adopted;
|
|
3232
|
+
} catch {}
|
|
3233
|
+
}
|
|
3234
|
+
/**
|
|
3235
|
+
* Whether an adopted credential's access token is already unusable.
|
|
3236
|
+
*
|
|
3237
|
+
* A structural mirror of the expiry test the OAuth module applies to its own
|
|
3238
|
+
* credentials (expiresAt - Date.now() > 0), asked of a pooled credential so the
|
|
3239
|
+
* pool can refuse it WITHOUT ever reaching for a refresh this module deliberately
|
|
3240
|
+
* does not perform. A non-finite expiresAt counts as expired, matching the
|
|
3241
|
+
* refusal in the reader: no established validity, no use.
|
|
3242
|
+
*
|
|
3243
|
+
* now is injectable so a test can reason about a fixed clock.
|
|
3244
|
+
*/
|
|
3245
|
+
function isAdoptedCodexCredentialExpired(credential, now = Date.now()) {
|
|
3246
|
+
const expiresAt = credential.expiresAt;
|
|
3247
|
+
return !(typeof expiresAt === "number" && Number.isFinite(expiresAt) && expiresAt > now);
|
|
3248
|
+
}
|
|
3249
|
+
//#endregion
|
|
2898
3250
|
//#region src/host/codex-account-pool.ts
|
|
3251
|
+
/**
|
|
3252
|
+
* The ChatGPT account pool.
|
|
3253
|
+
*
|
|
3254
|
+
* TWO KINDS OF ACCOUNT, AND ONLY ONE OF THEM MAY BE REFRESHED.
|
|
3255
|
+
*
|
|
3256
|
+
* - managed — signed in through this plugin. Its credential lives in the pool's
|
|
3257
|
+
* encrypted storage AND in the pre-pool credential document beside it, it is
|
|
3258
|
+
* refreshable, and deleting it is this plugin's business.
|
|
3259
|
+
* - adopted — a SNAPSHOT read out of a local Codex CLI sign-in by 'codex-adopt.ts'
|
|
3260
|
+
* (marker 'adopted: true', source 'codex'). It is used only while its own access
|
|
3261
|
+
* token is still valid.
|
|
3262
|
+
*
|
|
3263
|
+
* An adopted snapshot is NEVER refreshed, and the reason is not an optimisation:
|
|
3264
|
+
* the ChatGPT refresh token the CLI stores rotates, and the CLI refreshes its own
|
|
3265
|
+
* file in place. Two processes spending one grant invalidate each other, and
|
|
3266
|
+
* whoever loses the race holds a token the server has already retired — which
|
|
3267
|
+
* leaves the user signed OUT of the Codex CLI by this plugin's good intentions. An
|
|
3268
|
+
* in-process single-flight cannot fix that, because the race is across PROCESSES
|
|
3269
|
+
* and the only state they share is a file 'codex-adopt.ts' must never write. Once
|
|
3270
|
+
* such a snapshot has expired the pool takes it out of rotation and surfaces
|
|
3271
|
+
* ADOPTED_CODEX_CREDENTIAL_EXPIRED_HINT — sign in again in the CODEX CLI and
|
|
3272
|
+
* re-import, not here.
|
|
3273
|
+
*
|
|
3274
|
+
* The rule is enforced twice on purpose: the needsRefresh hook answers false for
|
|
3275
|
+
* an adopted credential no matter what its expiry says, and the refresh hook
|
|
3276
|
+
* itself refuses one, so a future caller that forgets the first fails loudly
|
|
3277
|
+
* instead of silently spending a grant that is not ours to spend.
|
|
3278
|
+
*
|
|
3279
|
+
* MANAGED BEATS ADOPTED on a conflict, and the marker is therefore written on
|
|
3280
|
+
* EVERY account this file creates, including the explicit false case: the core
|
|
3281
|
+
* merges a re-authorization as { ...existing, ...created }, so an omitted key
|
|
3282
|
+
* would leave a stored 'adopted: true' — and its never-refresh consequence — on a
|
|
3283
|
+
* record that is now plugin-owned.
|
|
3284
|
+
*/
|
|
3285
|
+
/**
|
|
3286
|
+
* Whether a pooled credential is an adopted snapshot.
|
|
3287
|
+
*
|
|
3288
|
+
* The two fields, and only those two: a plain credential carries no 'adopted'
|
|
3289
|
+
* field and must never be mistaken for a snapshot, because treating a
|
|
3290
|
+
* plugin-owned account as a disposable one would freeze it out of refreshing and
|
|
3291
|
+
* make deleteAccount refuse to remove an account the user signed in here.
|
|
3292
|
+
*/
|
|
3293
|
+
function isAdoptedCodexPoolCredential(credentials) {
|
|
3294
|
+
return credentials.adopted === true && credentials.source === "codex";
|
|
3295
|
+
}
|
|
3296
|
+
/**
|
|
3297
|
+
* Whether a credential must be refreshed before the next request.
|
|
3298
|
+
*
|
|
3299
|
+
* FALSE for every adopted snapshot, valid or expired. That is this pool's core
|
|
3300
|
+
* rule: an adopted credential is borrowed for exactly as long as the Codex CLI
|
|
3301
|
+
* vouched for it, and this pool has no business rotating a token another process
|
|
3302
|
+
* is rotating too. See the module comment.
|
|
3303
|
+
*/
|
|
3304
|
+
function codexNeedsRefresh(credentials, now) {
|
|
3305
|
+
if (isAdoptedCodexPoolCredential(credentials)) return false;
|
|
3306
|
+
return credentials.expiresAt - now <= TOKEN_REFRESH_MARGIN_MS;
|
|
3307
|
+
}
|
|
3308
|
+
/**
|
|
3309
|
+
* Why a stored credential is already known to be unusable, or nothing.
|
|
3310
|
+
*
|
|
3311
|
+
* Reported purely — nothing is written here — and consulted by the core for both
|
|
3312
|
+
* routing eligibility and credential usability, which is what makes an expired
|
|
3313
|
+
* snapshot non-routable WITHOUT a write on every read. The hint names the remedy
|
|
3314
|
+
* in the Codex CLI, because that is where the user has to go: re-adopting here is
|
|
3315
|
+
* only possible after the CLI itself signed in again.
|
|
3316
|
+
*/
|
|
3317
|
+
function codexAuthRejectedReason(credentials) {
|
|
3318
|
+
if (!isAdoptedCodexPoolCredential(credentials)) return void 0;
|
|
3319
|
+
return isAdoptedCodexCredentialExpired(credentials) ? ADOPTED_CODEX_CREDENTIAL_EXPIRED_HINT : void 0;
|
|
3320
|
+
}
|
|
3321
|
+
/**
|
|
3322
|
+
* Message raised if a refresh of a still-valid adopted snapshot were attempted.
|
|
3323
|
+
*
|
|
3324
|
+
* Unreachable through this pool's own paths (see the module comment); it exists
|
|
3325
|
+
* because 'unreachable' is a claim the guard has to be able to make good on.
|
|
3326
|
+
*/
|
|
3327
|
+
const ADOPTED_NEVER_REFRESHED_MESSAGE$1 = "The sign-in imported from the Codex CLI is never refreshed by this plugin: the Codex CLI refreshes the same rotating token, and two refreshers would invalidate each other. Sign in again in the Codex CLI, then import the new sign-in from this settings card.";
|
|
3328
|
+
/** Raised when a refresh of an adopted snapshot is attempted. Never a 401. */
|
|
3329
|
+
var CodexAdoptedCredentialError = class extends Error {
|
|
3330
|
+
constructor(message) {
|
|
3331
|
+
super(message);
|
|
3332
|
+
this.name = "CodexAdoptedCredentialError";
|
|
3333
|
+
}
|
|
3334
|
+
};
|
|
2899
3335
|
/** Encrypted pool file the ChatGPT line owns; the single-credential store stays the mirror. */
|
|
2900
3336
|
function codexPoolPath() {
|
|
2901
3337
|
return path.join(dshHomeDir(), "storages", "codex-pool.json");
|
|
@@ -2916,13 +3352,18 @@ function parseCodexPoolData(value) {
|
|
|
2916
3352
|
const raw = item;
|
|
2917
3353
|
if (typeof raw.id !== "string") continue;
|
|
2918
3354
|
const credentials = parseStoredCredentials(raw.credentials);
|
|
3355
|
+
const adopted = isAdoptedCodexPoolCredential(credentials);
|
|
2919
3356
|
const account = {
|
|
2920
3357
|
id: raw.id,
|
|
2921
3358
|
alias: typeof raw.alias === "string" ? raw.alias : credentials.email || "账号",
|
|
2922
3359
|
credentials,
|
|
2923
3360
|
addedAt: typeof raw.addedAt === "number" ? raw.addedAt : Date.now(),
|
|
2924
|
-
isPrimary: raw.isPrimary === true
|
|
3361
|
+
isPrimary: raw.isPrimary === true,
|
|
3362
|
+
adopted,
|
|
3363
|
+
source: adopted ? CODEX_CLI_CREDENTIAL_SOURCE : "managed"
|
|
2925
3364
|
};
|
|
3365
|
+
const sourcePath = optionalString$9(raw, "sourcePath") ?? credentials.sourcePath;
|
|
3366
|
+
if (adopted && sourcePath !== void 0) account.sourcePath = sourcePath;
|
|
2926
3367
|
const email = optionalString$9(raw, "email") ?? credentials.email;
|
|
2927
3368
|
if (email !== void 0) account.email = email;
|
|
2928
3369
|
const planType = optionalString$9(raw, "planType") ?? credentials.planType;
|
|
@@ -2945,14 +3386,6 @@ function parseCodexPoolData(value) {
|
|
|
2945
3386
|
return result;
|
|
2946
3387
|
}
|
|
2947
3388
|
/**
|
|
2948
|
-
* ChatGPT's account pool.
|
|
2949
|
-
*
|
|
2950
|
-
* Thin wrapper over {@link AccountPoolCore}: it only supplies the credential
|
|
2951
|
-
* shape, the identity a duplicate sign-in collapses onto, and the two provider
|
|
2952
|
-
* hooks the core cannot know — how to refresh one account's tokens and which
|
|
2953
|
-
* accounts a cached quota window already rules out.
|
|
2954
|
-
*/
|
|
2955
|
-
/**
|
|
2956
3389
|
* Per-account in-flight requests, shared by every ChatGPT request this host
|
|
2957
3390
|
* makes. A fan-out of subagents is what reaches a plan's concurrency bound, so
|
|
2958
3391
|
* the bound is enforced here rather than discovered by cooling every account at
|
|
@@ -2997,23 +3430,31 @@ var CodexAccountPool = class extends AccountPoolCore {
|
|
|
2997
3430
|
parsePoolData: parseCodexPoolData,
|
|
2998
3431
|
dedupeKey: (credentials) => credentials.accountId ?? credentials.email,
|
|
2999
3432
|
defaultAlias: (credentials, position) => credentials.email ?? `账号 ${position}`,
|
|
3000
|
-
createAccount: ({ id, alias, credentials, addedAt, isPrimary }) =>
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3433
|
+
createAccount: ({ id, alias, credentials, addedAt, isPrimary }) => {
|
|
3434
|
+
const adopted = isAdoptedCodexPoolCredential(credentials);
|
|
3435
|
+
return {
|
|
3436
|
+
id,
|
|
3437
|
+
alias,
|
|
3438
|
+
credentials,
|
|
3439
|
+
addedAt,
|
|
3440
|
+
isPrimary,
|
|
3441
|
+
adopted,
|
|
3442
|
+
source: adopted ? CODEX_CLI_CREDENTIAL_SOURCE : "managed",
|
|
3443
|
+
sourcePath: adopted ? credentials.sourcePath : void 0,
|
|
3444
|
+
...credentials.email === void 0 ? {} : { email: credentials.email },
|
|
3445
|
+
...credentials.planType === void 0 ? {} : { planType: credentials.planType },
|
|
3446
|
+
...credentials.accountId === void 0 ? {} : { accountId: credentials.accountId }
|
|
3447
|
+
};
|
|
3448
|
+
},
|
|
3010
3449
|
expiresAt: (credentials) => credentials.expiresAt,
|
|
3011
|
-
needsRefresh: (credentials, now) => credentials
|
|
3450
|
+
needsRefresh: (credentials, now) => codexNeedsRefresh(credentials, now),
|
|
3012
3451
|
refresh: async (credentials) => {
|
|
3452
|
+
if (isAdoptedCodexPoolCredential(credentials)) throw new CodexAdoptedCredentialError(isAdoptedCodexCredentialExpired(credentials) ? ADOPTED_CODEX_CREDENTIAL_EXPIRED_HINT : ADOPTED_NEVER_REFRESHED_MESSAGE$1);
|
|
3013
3453
|
const refresher = ref.current;
|
|
3014
3454
|
if (refresher === void 0) throw new Error("ChatGPT 凭据刷新服务尚未就绪");
|
|
3015
3455
|
return refresher.refreshAccount(credentials);
|
|
3016
3456
|
},
|
|
3457
|
+
authRejectedReason: (credentials) => codexAuthRejectedReason(credentials),
|
|
3017
3458
|
refreshFailureStatus: (error) => error instanceof OAuthServiceError && (error.status === 400 || error.status === 401) ? "expired" : void 0,
|
|
3018
3459
|
legacyAccount: async () => {
|
|
3019
3460
|
const stored = await store.load();
|
|
@@ -3024,6 +3465,8 @@ var CodexAccountPool = class extends AccountPoolCore {
|
|
|
3024
3465
|
credentials: stored,
|
|
3025
3466
|
addedAt: Date.now(),
|
|
3026
3467
|
isPrimary: true,
|
|
3468
|
+
adopted: false,
|
|
3469
|
+
source: "managed",
|
|
3027
3470
|
...stored.email === void 0 ? {} : { email: stored.email },
|
|
3028
3471
|
...stored.planType === void 0 ? {} : { planType: stored.planType },
|
|
3029
3472
|
...stored.accountId === void 0 ? {} : { accountId: stored.accountId }
|
|
@@ -3034,17 +3477,30 @@ var CodexAccountPool = class extends AccountPoolCore {
|
|
|
3034
3477
|
await store.clear();
|
|
3035
3478
|
return;
|
|
3036
3479
|
}
|
|
3480
|
+
if (isAdoptedCodexPoolCredential(credentials)) return;
|
|
3037
3481
|
await store.save(credentials);
|
|
3038
3482
|
},
|
|
3039
|
-
extendSummary: (account, base) =>
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3483
|
+
extendSummary: (account, base) => {
|
|
3484
|
+
const adopted = account.adopted === true;
|
|
3485
|
+
const expired = adopted && isAdoptedCodexCredentialExpired(account.credentials);
|
|
3486
|
+
return {
|
|
3487
|
+
...base,
|
|
3488
|
+
...account.email === void 0 ? {} : { email: account.email },
|
|
3489
|
+
...account.planType === void 0 ? {} : { planLabel: account.planType },
|
|
3490
|
+
adopted,
|
|
3491
|
+
source: account.source,
|
|
3492
|
+
removable: !adopted,
|
|
3493
|
+
...account.sourcePath === void 0 ? {} : { sourcePath: account.sourcePath },
|
|
3494
|
+
...(() => {
|
|
3495
|
+
const quota = quotaSnapshotRef.current?.(account);
|
|
3496
|
+
return quota === void 0 ? {} : { quota };
|
|
3497
|
+
})(),
|
|
3498
|
+
...expired ? {
|
|
3499
|
+
authStatus: "expired",
|
|
3500
|
+
authFailedReason: ADOPTED_CODEX_CREDENTIAL_EXPIRED_HINT
|
|
3501
|
+
} : {}
|
|
3502
|
+
};
|
|
3503
|
+
},
|
|
3048
3504
|
emptyMessage: "未登录 ChatGPT 账号,请在「设置 → 订阅服务 → ChatGPT」中添加并登录账号。",
|
|
3049
3505
|
...options.backend === void 0 ? {} : { backend: options.backend },
|
|
3050
3506
|
...options.maxAccounts === void 0 ? {} : { maxAccounts: options.maxAccounts }
|
|
@@ -3098,6 +3554,35 @@ var CodexAccountPool = class extends AccountPoolCore {
|
|
|
3098
3554
|
if (this.refresherRef.current === void 0) throw new Error("ChatGPT 凭据刷新服务尚未就绪");
|
|
3099
3555
|
return this.renewCredential(accountId, fetch, account.credentials);
|
|
3100
3556
|
}
|
|
3557
|
+
/**
|
|
3558
|
+
* Remove one account inside a transaction, refusing a borrowed snapshot.
|
|
3559
|
+
*
|
|
3560
|
+
* The check lives HERE rather than in the public delete method so it cannot be
|
|
3561
|
+
* separated from the removal by a concurrent change, and so every caller of the
|
|
3562
|
+
* core's own deletion inherits it. The remedy is a separate action
|
|
3563
|
+
* ({@link removeImportedAccount}): a card must not destroy an account this plugin
|
|
3564
|
+
* did not create with the same button that deletes its own.
|
|
3565
|
+
*/
|
|
3566
|
+
deleteAccountFromPool(data, accountId) {
|
|
3567
|
+
if (data.accounts.find((account) => account.id === accountId)?.adopted === true) throw new Error("The sign-in imported from the Codex CLI is not an account this plugin owns; remove the import instead.");
|
|
3568
|
+
super.deleteAccountFromPool(data, accountId);
|
|
3569
|
+
}
|
|
3570
|
+
/**
|
|
3571
|
+
* Drop an imported snapshot from the pool — the card's 'remove import' action.
|
|
3572
|
+
*
|
|
3573
|
+
* Separate from {@link deleteAccount} because that refuses one, and because the
|
|
3574
|
+
* user is asking to stop borrowing a sign-in, not to sign out of an account. The
|
|
3575
|
+
* Codex CLI's own file is touched by neither path: not deleted, not rewritten,
|
|
3576
|
+
* not moved. This only forgets the copy.
|
|
3577
|
+
*/
|
|
3578
|
+
async removeImportedAccount(accountId) {
|
|
3579
|
+
return this.updatePool((data) => {
|
|
3580
|
+
const target = data.accounts.find((account) => account.id === accountId);
|
|
3581
|
+
if (target === void 0) return;
|
|
3582
|
+
if (target.adopted !== true) throw new Error("That account was signed in through this plugin; delete it instead of removing an import.");
|
|
3583
|
+
super.deleteAccountFromPool(data, accountId);
|
|
3584
|
+
});
|
|
3585
|
+
}
|
|
3101
3586
|
/** The single-credential store this pool mirrors its primary account into. */
|
|
3102
3587
|
mirrorStore() {
|
|
3103
3588
|
return this.store;
|
|
@@ -7147,12 +7632,16 @@ function identityKey(credentials) {
|
|
|
7147
7632
|
//#endregion
|
|
7148
7633
|
//#region src/host/routes.ts
|
|
7149
7634
|
const MAX_BODY_BYTES$6 = 64 * 1024;
|
|
7150
|
-
function registerRoutes(ctx, oauth, usage, preferences, proxyManager, searchSwitcher, routeAudit, accountPool, fetchConfiguration) {
|
|
7635
|
+
function registerRoutes(ctx, oauth, usage, preferences, proxyManager, searchSwitcher, routeAudit, accountPool, fetchConfiguration, adopt) {
|
|
7636
|
+
const adoptPaths = adopt?.adoptPaths;
|
|
7151
7637
|
const handler = async (request, response) => {
|
|
7152
7638
|
const url = new URL(request.url ?? "/", "http://dsh.local");
|
|
7153
7639
|
if (request.method === "GET" && url.pathname === `/api/dsh-chatgpt-subscription/status`) {
|
|
7154
7640
|
const oauthStatus = await oauth.status();
|
|
7155
7641
|
const quota = await usage.status(oauthStatus.authenticated, false, { backgroundRefresh: true });
|
|
7642
|
+
const codexCliPaths = adoptPaths ?? codexCliCredentialPaths();
|
|
7643
|
+
const codexCliSignInAvailable = await codexCliCredentialPresence(codexCliPaths).catch(() => false);
|
|
7644
|
+
const codexCliSignInPath = codexCliPaths[0];
|
|
7156
7645
|
json(response, {
|
|
7157
7646
|
ok: true,
|
|
7158
7647
|
value: {
|
|
@@ -7160,6 +7649,8 @@ function registerRoutes(ctx, oauth, usage, preferences, proxyManager, searchSwit
|
|
|
7160
7649
|
quota,
|
|
7161
7650
|
quotaRefreshing: usage.refreshing,
|
|
7162
7651
|
preferences: preferences.status(),
|
|
7652
|
+
codexCliSignInAvailable,
|
|
7653
|
+
codexCliSignInPath,
|
|
7163
7654
|
detectedProxy: proxyManager?.getSystemProxy() ?? null,
|
|
7164
7655
|
activeProxy: proxyManager?.resolveActiveProxyUrl() ?? null,
|
|
7165
7656
|
switcher: searchSwitcher?.status() ?? null,
|
|
@@ -7238,6 +7729,14 @@ function registerRoutes(ctx, oauth, usage, preferences, proxyManager, searchSwit
|
|
|
7238
7729
|
});
|
|
7239
7730
|
return;
|
|
7240
7731
|
}
|
|
7732
|
+
const refreshedStatus = async () => {
|
|
7733
|
+
const afterAction = await oauth.status();
|
|
7734
|
+
return {
|
|
7735
|
+
...afterAction,
|
|
7736
|
+
quota: await usage.status(afterAction.authenticated),
|
|
7737
|
+
preferences: preferences.status()
|
|
7738
|
+
};
|
|
7739
|
+
};
|
|
7241
7740
|
try {
|
|
7242
7741
|
switch (url.pathname) {
|
|
7243
7742
|
case `${ROUTE_PREFIX$5}/login/start`:
|
|
@@ -7267,14 +7766,60 @@ function registerRoutes(ctx, oauth, usage, preferences, proxyManager, searchSwit
|
|
|
7267
7766
|
else if (action === "clear-cooldown" && accountId !== null) await accountPool.clearCooldown(accountId);
|
|
7268
7767
|
else if (action === "strategy" && (body.strategy === "sequential" || body.strategy === "round-robin" || body.strategy === "sticky")) await accountPool.setStrategy(body.strategy);
|
|
7269
7768
|
else if (action === "relogin" && accountId !== null) await accountPool.clearAuthFailed(accountId);
|
|
7270
|
-
const afterAction = await oauth.status();
|
|
7271
7769
|
json(response, {
|
|
7272
7770
|
ok: true,
|
|
7273
|
-
value:
|
|
7274
|
-
|
|
7275
|
-
|
|
7276
|
-
|
|
7277
|
-
|
|
7771
|
+
value: await refreshedStatus()
|
|
7772
|
+
});
|
|
7773
|
+
return;
|
|
7774
|
+
}
|
|
7775
|
+
case `${ROUTE_PREFIX$5}/adopt`: {
|
|
7776
|
+
if (accountPool === void 0) {
|
|
7777
|
+
jsonError(response, 400, {
|
|
7778
|
+
code: "bad-request",
|
|
7779
|
+
message: "Importing a local Codex CLI sign-in needs the account pool, which is not installed."
|
|
7780
|
+
});
|
|
7781
|
+
return;
|
|
7782
|
+
}
|
|
7783
|
+
if (field(body, "source") !== "codex") {
|
|
7784
|
+
jsonError(response, 400, {
|
|
7785
|
+
code: "bad-request",
|
|
7786
|
+
message: "That local sign-in is not imported here. Claude Code and MiniMax Code are imported from their own provider settings."
|
|
7787
|
+
});
|
|
7788
|
+
return;
|
|
7789
|
+
}
|
|
7790
|
+
const adopted = await readCodexCliCredentials(adoptPaths ?? codexCliCredentialPaths());
|
|
7791
|
+
if (adopted === void 0) {
|
|
7792
|
+
jsonError(response, 400, {
|
|
7793
|
+
code: "bad-request",
|
|
7794
|
+
message: "No usable local Codex CLI sign-in was found (unrecognised local sign-in format)."
|
|
7795
|
+
});
|
|
7796
|
+
return;
|
|
7797
|
+
}
|
|
7798
|
+
await accountPool.addAccount({
|
|
7799
|
+
...adopted.credentials,
|
|
7800
|
+
adopted: true,
|
|
7801
|
+
source: CODEX_CLI_CREDENTIAL_SOURCE,
|
|
7802
|
+
sourcePath: adopted.sourcePath
|
|
7803
|
+
});
|
|
7804
|
+
json(response, {
|
|
7805
|
+
ok: true,
|
|
7806
|
+
value: await refreshedStatus()
|
|
7807
|
+
});
|
|
7808
|
+
return;
|
|
7809
|
+
}
|
|
7810
|
+
case `${ROUTE_PREFIX$5}/adopt/disable`: {
|
|
7811
|
+
if (accountPool === void 0) {
|
|
7812
|
+
jsonError(response, 400, {
|
|
7813
|
+
code: "bad-request",
|
|
7814
|
+
message: "The ChatGPT account pool is not installed."
|
|
7815
|
+
});
|
|
7816
|
+
return;
|
|
7817
|
+
}
|
|
7818
|
+
const imported = ((await accountPool.read().catch(() => null))?.accounts ?? []).filter((account) => account.adopted === true);
|
|
7819
|
+
for (const account of imported) await accountPool.removeImportedAccount(account.id);
|
|
7820
|
+
json(response, {
|
|
7821
|
+
ok: true,
|
|
7822
|
+
value: await refreshedStatus()
|
|
7278
7823
|
});
|
|
7279
7824
|
return;
|
|
7280
7825
|
}
|
|
@@ -33974,14 +34519,19 @@ const ANTHROPIC_VERSION = "2023-06-01";
|
|
|
33974
34519
|
* table, whether or not a floor for that model has been observed: upstream has
|
|
33975
34520
|
* so far gated a new model on the release that added it (Opus 5.5 arrived in
|
|
33976
34521
|
* 2.1.280 and is refused below 2.1.280). Claude Sonnet 5.5 arrived in 2.1.284,
|
|
33977
|
-
* so 2.1.283 could not be relied on to reach it
|
|
34522
|
+
* so 2.1.283 could not be relied on to reach it. Claude Haiku 5.5 arrived in
|
|
34523
|
+
* 2.1.293, so 2.1.292 could not be relied on to reach it either — and that is
|
|
34524
|
+
* not hypothetical: when this row was added, npm's `stable` dist-tag was STILL
|
|
34525
|
+
* sitting at 2.1.285, five releases below the one that ships the model, while
|
|
34526
|
+
* `latest` was already 2.1.293. The warning above about reading `stable` is
|
|
34527
|
+
* therefore about this table's own newest model, not a hypothetical.
|
|
33978
34528
|
*
|
|
33979
34529
|
* It is still not a literal that only moves when this package is republished: a
|
|
33980
34530
|
* user whose installed baseline has aged past the server's floor has to be able
|
|
33981
34531
|
* to raise it without waiting for a release. Read the effective value with
|
|
33982
34532
|
* {@link claudeCliVersion}, pin one with {@link setClaudeCliVersion}.
|
|
33983
34533
|
*/
|
|
33984
|
-
const CLAUDE_CLI_VERSION = "2.1.
|
|
34534
|
+
const CLAUDE_CLI_VERSION = "2.1.293";
|
|
33985
34535
|
/**
|
|
33986
34536
|
* Loopback callback port the subscription flow defaults to.
|
|
33987
34537
|
*
|
|
@@ -34342,10 +34892,11 @@ function loopbackRedirectUri(port = DEFAULT_CALLBACK_PORT) {
|
|
|
34342
34892
|
* TWO KINDS OF ROW ARE NOT FULLY TRANSCRIBED, and they are different things.
|
|
34343
34893
|
* Do not collapse them.
|
|
34344
34894
|
*
|
|
34345
|
-
* 1. `claude-sonnet-5-5` — the snapshot this table
|
|
34346
|
-
* entry for
|
|
34347
|
-
* the vendor's own documentation and no
|
|
34348
|
-
* mirror list in the test is
|
|
34895
|
+
* 1. `claude-sonnet-5-5` and `claude-haiku-5-5` — the snapshot this table
|
|
34896
|
+
* was copied from has no entry for either, so each whole row is CURATED:
|
|
34897
|
+
* every value below comes from the vendor's own documentation and no
|
|
34898
|
+
* snapshot field can check it. The mirror list in the test is
|
|
34899
|
+
* `LOCALLY_CURATED_MODEL_IDS`.
|
|
34349
34900
|
*
|
|
34350
34901
|
* 2. `claude-opus-5-5` — a NEWER snapshot (pi-ai >= 0.87.1) does carry it, so
|
|
34351
34902
|
* the row sits in the snapshot's own position and EVERY field is checked
|
|
@@ -34358,22 +34909,35 @@ function loopbackRedirectUri(port = DEFAULT_CALLBACK_PORT) {
|
|
|
34358
34909
|
* stands. The test asserts the row still disagrees with the snapshot, so a
|
|
34359
34910
|
* future snapshot that agrees fails loudly and retires the entry.
|
|
34360
34911
|
*
|
|
34361
|
-
*
|
|
34362
|
-
*
|
|
34363
|
-
* is
|
|
34364
|
-
*
|
|
34912
|
+
* 3. `claude-haiku-5-5` (released 2026-10-07) is the second fully curated
|
|
34913
|
+
* row, and the first one whose documented `canDisableThinking` is TRUE. That
|
|
34914
|
+
* flag is not a free choice here and the row's own comment records why the
|
|
34915
|
+
* documented answer is safe to send: `thinking: { type: 'disabled' }` is
|
|
34916
|
+
* accepted at `low`/`medium`/`high` and is a 400 at `xhigh`/`max`, and this
|
|
34917
|
+
* line only sends that form when the caller asked for thinking OFF, in which
|
|
34918
|
+
* case it names no effort at all and the server's own default (`medium`) is
|
|
34919
|
+
* what the request runs at. Its documented default effort is MEDIUM, so it is
|
|
34920
|
+
* 'adaptive' rather than 'mid-convo' for exactly the reason Opus 5.5 is —
|
|
34921
|
+
* 'mid-convo' would force `high` — and it runs the preserved-thinking prefix
|
|
34922
|
+
* check, so `bindsThinkingToPrefix` is set even though the form is adaptive.
|
|
34923
|
+
*
|
|
34924
|
+
* All three must be changed together with the mirror list in the test. An
|
|
34925
|
+
* invented row that mimics the format of a checked one is worse than a missing
|
|
34926
|
+
* row: it is unverifiable and it looks verified. So each row is marked at the
|
|
34927
|
+
* row itself, the test asserts the id is on the curated list, and the test
|
|
34365
34928
|
* asserts the curated list is exactly the ids the snapshot lacks — which keeps
|
|
34366
34929
|
* the lock narrow instead of merely weaker.
|
|
34367
34930
|
*
|
|
34368
|
-
*
|
|
34369
|
-
* (the model overview page plus the extended-thinking
|
|
34370
|
-
* snapshot, and they are the values a person read off
|
|
34931
|
+
* `claude-sonnet-5-5`'s fields come from the vendor's own published
|
|
34932
|
+
* documentation for the model (the model overview page plus the extended-thinking
|
|
34933
|
+
* effort page), NOT from the snapshot, and they are the values a person read off
|
|
34934
|
+
* those pages. That is a
|
|
34371
34935
|
* weaker source than the snapshot and it is labelled as such: nothing here
|
|
34372
34936
|
* proves the account is entitled to the model, and the server's own
|
|
34373
34937
|
* `GET /v1/models` still outranks it.
|
|
34374
34938
|
*
|
|
34375
|
-
* Two of
|
|
34376
|
-
* like mistakes against their neighbours:
|
|
34939
|
+
* Two of that row's fields are the ones worth recording a WHY for, because both
|
|
34940
|
+
* look like mistakes against their neighbours:
|
|
34377
34941
|
*
|
|
34378
34942
|
* 1. `canDisableThinking: false`. The vendor documents that thinking cannot be
|
|
34379
34943
|
* turned off on this model at all: sending `thinking: { type: 'disabled' }`
|
|
@@ -34399,17 +34963,18 @@ function loopbackRedirectUri(port = DEFAULT_CALLBACK_PORT) {
|
|
|
34399
34963
|
* A future reader who notices the shared `supportsTemperature: false` and
|
|
34400
34964
|
* "fixes" item 2 to 'mid-convo' has reintroduced exactly that bug.
|
|
34401
34965
|
*
|
|
34402
|
-
* One more field on
|
|
34403
|
-
* floor is the number upstream itself states when it refuses
|
|
34404
|
-
* (`claude_code_version_too_old`, "version 2.1.280 or newer is
|
|
34966
|
+
* One more field on `claude-opus-5-5` comes from neither source: its
|
|
34967
|
+
* `minCliVersion` floor is the number upstream itself states when it refuses
|
|
34968
|
+
* the model (`claude_code_version_too_old`, "version 2.1.280 or newer is
|
|
34969
|
+
* required"). That
|
|
34405
34970
|
* is a stronger witness than a documentation page — the server said it — and it
|
|
34406
34971
|
* is recorded here so a claim below it is refused locally, before the request is
|
|
34407
34972
|
* sent, instead of as an upstream 400 that names neither the model nor the number
|
|
34408
34973
|
* the user has to reach.
|
|
34409
34974
|
*/
|
|
34410
34975
|
/**
|
|
34411
|
-
*
|
|
34412
|
-
* followed by the
|
|
34976
|
+
* Seventeen rows: the snapshot's own 15, in the snapshot's own declaration order,
|
|
34977
|
+
* followed by the two rows the snapshot still predates (see LOCAL ADDITIONS above).
|
|
34413
34978
|
*
|
|
34414
34979
|
* The order matters and is not cosmetic. The test asserts the snapshot's ids
|
|
34415
34980
|
* appear in the table as a SUBSEQUENCE, so a curated row may be appended or
|
|
@@ -34697,6 +35262,24 @@ const CLAUDE_MODELS = Object.freeze([
|
|
|
34697
35262
|
],
|
|
34698
35263
|
canDisableThinking: false,
|
|
34699
35264
|
bindsThinkingToPrefix: true
|
|
35265
|
+
},
|
|
35266
|
+
{
|
|
35267
|
+
id: "claude-haiku-5-5",
|
|
35268
|
+
name: "Claude Haiku 5.5",
|
|
35269
|
+
contextWindow: 1e6,
|
|
35270
|
+
maxTokens: 128e3,
|
|
35271
|
+
supportsImage: true,
|
|
35272
|
+
supportsTemperature: false,
|
|
35273
|
+
thinkingMode: "adaptive",
|
|
35274
|
+
reasoningEfforts: [
|
|
35275
|
+
"low",
|
|
35276
|
+
"medium",
|
|
35277
|
+
"high",
|
|
35278
|
+
"xhigh",
|
|
35279
|
+
"max"
|
|
35280
|
+
],
|
|
35281
|
+
canDisableThinking: true,
|
|
35282
|
+
bindsThinkingToPrefix: true
|
|
34700
35283
|
}
|
|
34701
35284
|
]);
|
|
34702
35285
|
/** Every id the fallback table knows, in declaration order. */
|
|
@@ -34721,6 +35304,13 @@ const FALLBACK_MODELS = CLAUDE_MODELS;
|
|
|
34721
35304
|
* than one that leads with the general-purpose ids. The rest stay selectable
|
|
34722
35305
|
* from the model card and are one click away.
|
|
34723
35306
|
*
|
|
35307
|
+
* The list tracks ONE current model per family rather than the newest of each,
|
|
35308
|
+
* so a slot follows its family's newest release in place instead of growing: the
|
|
35309
|
+
* Haiku slot is `claude-haiku-5-5`, which replaced `claude-haiku-4-5` when that
|
|
35310
|
+
* model shipped. That is a deliberate swap and not a silent append — a list that
|
|
35311
|
+
* offered both generations of every family would be longer than the picker it
|
|
35312
|
+
* feeds, and the older Haiku stays one click away in the card.
|
|
35313
|
+
*
|
|
34724
35314
|
* This list is the SHIPPED DEFAULT, and edits to it are a deliberate onboarding
|
|
34725
35315
|
* decision rather than a no-op: a stored list that still equals the shipped
|
|
34726
35316
|
* default counts as "never edited", and the routes and adapter read it that way
|
|
@@ -34734,7 +35324,7 @@ const DEFAULT_VISIBLE_MODEL_IDS = [
|
|
|
34734
35324
|
"claude-opus-5-5",
|
|
34735
35325
|
"claude-opus-4-6",
|
|
34736
35326
|
"claude-sonnet-4-6",
|
|
34737
|
-
"claude-haiku-
|
|
35327
|
+
"claude-haiku-5-5",
|
|
34738
35328
|
"claude-opus-4-5"
|
|
34739
35329
|
];
|
|
34740
35330
|
new Map(CLAUDE_MODELS.map((model) => [model.id, model]));
|