dsh-codex-subscription 2.4.1 → 2.5.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/lib/index.js CHANGED
@@ -14,9 +14,9 @@ import { constants, readFileSync } from "node:fs";
14
14
  import { dirname, join, resolve } from "node:path";
15
15
  import { pathToFileURL } from "node:url";
16
16
  import { defineTool } from "@deepseek-ai/dsh-tools";
17
+ import { LlmError, createUserMessage } from "@deepseek-ai/dsh-llm";
17
18
  import * as dshCredentials from "@deepseek-ai/dsh-credentials";
18
19
  import { dshHomePath, resolveDshHome } from "@deepseek-ai/dsh-home-paths";
19
- import { LlmError, createUserMessage } from "@deepseek-ai/dsh-llm";
20
20
  import { PiAiAdapter } from "@deepseek-ai/dsh-llm-pi-ai";
21
21
  import z from "@deepseek-ai/schemastery";
22
22
  import { openaiCodexProvider as createOpenAICodexProvider } from "@earendil-works/pi-ai/providers/openai-codex";
@@ -40,6 +40,90 @@ function createSettingsAdapter(ctx, schema, config, namespace) {
40
40
  };
41
41
  }
42
42
  //#endregion
43
+ //#region src/default-model.js
44
+ /**
45
+ * DSH owns the model a freshly created agent starts on. This module is the
46
+ * subscription plugin's read/write surface over that service: the settings
47
+ * surface edits the single live selection instead of keeping a second copy, so
48
+ * what the user sees is exactly what the next conversation will use.
49
+ */
50
+ const DEFAULT_MODEL_PROVIDER = "openai-codex";
51
+ const MODEL_ID = /^[a-zA-Z0-9][a-zA-Z0-9._:-]{0,127}$/u;
52
+ const EFFORT_ID = /^[a-z][a-z0-9_-]{0,31}$/u;
53
+ /** A rejected default-model request carrying its stable public reason. */
54
+ var DefaultModelError = class extends Error {
55
+ constructor(code, message) {
56
+ super(message);
57
+ this.name = "DefaultModelError";
58
+ this.code = code;
59
+ }
60
+ };
61
+ const validDefaultModelId = (value) => typeof value === "string" && MODEL_ID.test(value);
62
+ /** Project a live selection onto the settings-safe shape; unknown values drop out. */
63
+ function normalizeDefaultSelection(value) {
64
+ return {
65
+ provider: typeof value?.provider === "string" && value.provider.length > 0 && value.provider.length <= 128 ? value.provider : void 0,
66
+ model: validDefaultModelId(value?.model) ? value.model : void 0,
67
+ reasoningEffort: typeof value?.reasoningEffort === "string" && EFFORT_ID.test(value.reasoningEffort) ? value.reasoningEffort : void 0
68
+ };
69
+ }
70
+ /**
71
+ * Sample the default-model service per call: the service can mount after this
72
+ * plugin. A write verifies its own read-back, because a deployment without a
73
+ * configuration editor ignores `saveSelection`; the user must see that instead
74
+ * of a selection that will never apply.
75
+ * @param resolveService - reads the optional DSH `agentDefaultModel` service.
76
+ * @param listModels - optional account catalog used to reject unknown models.
77
+ */
78
+ function createDefaultModelController({ resolveService, listModels } = {}) {
79
+ const service = () => {
80
+ try {
81
+ return resolveService?.();
82
+ } catch {
83
+ return;
84
+ }
85
+ };
86
+ const read = () => {
87
+ const current = service();
88
+ if (current === void 0 || typeof current.currentSelection !== "function") return void 0;
89
+ try {
90
+ return normalizeDefaultSelection(current.currentSelection());
91
+ } catch {
92
+ return;
93
+ }
94
+ };
95
+ const status = () => {
96
+ const selection = read();
97
+ return {
98
+ available: selection !== void 0,
99
+ managed: selection?.provider === "openai-codex" && selection.model !== void 0,
100
+ ...selection
101
+ };
102
+ };
103
+ const select = async (request) => {
104
+ const model = request?.model;
105
+ if (!validDefaultModelId(model)) throw new DefaultModelError("invalid-model", "Invalid default model");
106
+ const catalog = typeof listModels === "function" ? listModels() : void 0;
107
+ if (Array.isArray(catalog) && catalog.length > 0 && !catalog.includes(model)) throw new DefaultModelError("invalid-model", "Invalid default model");
108
+ const current = service();
109
+ if (current === void 0 || typeof current.saveSelection !== "function") throw new DefaultModelError("unavailable", "This DSH deployment cannot save a default model");
110
+ const previous = read();
111
+ const reasoningEffort = previous?.provider === "openai-codex" && previous.model === model ? previous.reasoningEffort : void 0;
112
+ await current.saveSelection({
113
+ provider: DEFAULT_MODEL_PROVIDER,
114
+ model,
115
+ ...reasoningEffort === void 0 ? {} : { reasoningEffort }
116
+ });
117
+ const applied = read();
118
+ if (applied?.provider !== "openai-codex" || applied.model !== model) throw new DefaultModelError("unavailable", "The default model could not be saved");
119
+ return status();
120
+ };
121
+ return Object.freeze({
122
+ status,
123
+ select
124
+ });
125
+ }
126
+ //#endregion
43
127
  //#region src/image-models.js
44
128
  const DEFAULT_IMAGE_MODEL = "gpt-image-2";
45
129
  const IMAGE_MODELS = Object.freeze({
@@ -82,8 +166,7 @@ const IMAGE_FEATURE_DEFAULTS = Object.freeze({
82
166
  imageViewer: true,
83
167
  imageAnnotations: true,
84
168
  imageSketch: false,
85
- imageSketchAgent: false,
86
- imageSketchAgentPreview: false
169
+ imageSketchAgent: false
87
170
  });
88
171
  function readImageFeatures(value = {}) {
89
172
  return Object.fromEntries(Object.entries(IMAGE_FEATURE_DEFAULTS).map(([key, fallback]) => [key, typeof value?.[key] === "boolean" ? value[key] : fallback]));
@@ -1014,6 +1097,8 @@ const RPC_ENDPOINTS = Object.freeze([
1014
1097
  "preferences/status",
1015
1098
  "preferences/models",
1016
1099
  "preferences/update",
1100
+ "default-model/status",
1101
+ "default-model/select",
1017
1102
  "runtime/status",
1018
1103
  "runtime/install",
1019
1104
  "runtime/remove",
@@ -1894,2071 +1979,2106 @@ const sketchCommandArray = {
1894
1979
  })
1895
1980
  };
1896
1981
  //#endregion
1897
- //#region src/sketch-agent-tool.js
1898
- function createSketchAgentTool(bridge, attachments) {
1899
- return defineTool({
1900
- name: "codex_sketch",
1901
- description: "Edit the sketch board in this session using native editable strokes and layers. Use for @sketch requests and explicit follow-up edits to that drawing. The toolbar pen button is for manual drawing; do not require the user to open it. Start with inspect for the runId, documentId, revision and command reference. Draw with the svg op whenever possible: one SVG document per stage (paths, shapes, groups, transforms; see the reference from inspect) is far faster and richer than hand-placed strokes, and still becomes native editable strokes. Draw the shapes, hair, clothing and lettering outlines of every layer with svg; use stroke commands only for small corrections and native text. Apply atomic batches, preview between stages, and call finish to save the finished draft and release the editing lock. save is only a checkpoint. finish returns an image only when the user enables experimental preview feedback. Closing the board does not stop drawing. If the user stops drawing, do not retry. Never generates AI images, sends messages or attaches images automatically. Inspect automatically opens the board in the currently viewed session. Do not ask the user to open it first. If the session is not visible in DSH, ask them to switch to it. On timeout inspect before retrying; reuse the exact requestId only for the same request.",
1902
- parameters: {
1903
- action: {
1904
- type: "string",
1905
- required: true,
1906
- enum: [
1907
- "inspect",
1908
- "apply",
1909
- "preview",
1910
- "save",
1911
- "finish"
1912
- ]
1913
- },
1914
- runId: {
1915
- type: "string",
1916
- description: "From inspect; required for all other actions. Never reuse a stopped run."
1917
- },
1918
- documentId: {
1919
- type: "string",
1920
- description: "From inspect; required except for inspect."
1921
- },
1922
- revision: {
1923
- type: "integer",
1924
- description: "From latest response; required for apply/save/finish."
1925
- },
1926
- requestId: {
1927
- type: "string",
1928
- description: "Unique id for apply/save/finish; exact retries are deduplicated. After timeout inspect recentRequests before repeating a write."
1929
- },
1930
- commands: {
1931
- oneOf: [sketchCommandArray, { type: "string" }],
1932
- description: "Prefer a native command array. Legacy JSON string also accepted. Required for apply. Use named objects and update existing IDs; prefer Bezier start + segments (control1/control2/end) for curves, not hundreds of pen points."
1933
- },
1934
- name: {
1935
- type: "string",
1936
- description: "Draft name for save/finish."
1937
- },
1938
- offset: {
1939
- type: "integer",
1940
- description: "inspect only: object list offset, default 0. Follow nextOffset for further pages."
1941
- },
1942
- objectId: {
1943
- type: "string",
1944
- description: "inspect only: return full editable geometry for this object, in layer (defaults to active layer)."
1945
- },
1946
- layer: {
1947
- type: "integer",
1948
- description: "inspect only: layer containing objectId."
1949
- }
1950
- },
1951
- timeoutMs: 25e3,
1952
- isConcurrencySafe: () => false,
1953
- async execute(args, exec) {
1954
- const sessionId = exec.agent?.id;
1955
- if (typeof sessionId !== "string") throw Error("A session-owned sketch call is required");
1956
- const request = { ...args };
1957
- if (args.action === "apply") try {
1958
- request.commands = typeof args.commands === "string" ? JSON.parse(args.commands) : args.commands;
1959
- if (!Array.isArray(request.commands)) throw Error();
1960
- } catch {
1961
- throw Error("commands must be a native array or JSON array string");
1962
- }
1963
- const value = await bridge.request(sessionId, request, exec.signal);
1964
- if (value.png) {
1965
- if (!/^data:image\/png;base64,/.test(value.png) || value.png.length > 8 * 1024 * 1024) throw Error("Invalid sketch preview");
1966
- const image = await attachments.saveImage({
1967
- data: new Uint8Array(Buffer.from(value.png.split(",")[1], "base64")),
1968
- mediaType: "image/png",
1969
- name: "sketch-preview.png"
1970
- });
1971
- const { png, ...snapshot } = value;
1972
- return {
1973
- ...snapshot,
1974
- image
1975
- };
1976
- }
1977
- return value;
1978
- },
1979
- output: {
1980
- schema: {
1981
- type: "object",
1982
- additionalProperties: true
1983
- },
1984
- render: (_args, value) => [{
1985
- type: "text",
1986
- text: JSON.stringify({
1987
- ...value,
1988
- image: void 0
1989
- })
1990
- }, ...value.image ? [{
1991
- type: "image",
1992
- attachment: value.image
1993
- }] : []]
1994
- }
1995
- });
1996
- }
1997
- //#endregion
1998
- //#region src/sketch-codec-route.js
1999
- function registerSketchCodec(connection) {
2000
- let source;
2001
- return connection.fetch.register({
2002
- path: "/api/codex-subscription/sketch-psd-worker",
2003
- methods: ["GET"],
2004
- requestBody: "buffered",
2005
- async fetch() {
2006
- source ??= await readFile(new URL("./sketch-psd-worker.js", import.meta.url));
2007
- return new Response(source, { headers: {
2008
- "content-type": "text/javascript; charset=utf-8",
2009
- "cache-control": "no-store"
2010
- } });
2011
- }
2012
- });
2013
- }
2014
- //#endregion
2015
- //#region src/account-vault.js
2016
- const VERSION = 1;
2017
- const DEFAULT_LABEL = "Account 1";
2018
- const clone$1 = (value) => value === void 0 ? void 0 : structuredClone(value);
2019
- const EMAIL_MAX_LENGTH = 254;
2020
- const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/u;
2021
- /** Keep only a bounded, display-safe email address from a trusted OAuth result. */
2022
- function normalizeAccountEmail(value) {
2023
- if (typeof value !== "string") return void 0;
2024
- const email = value.trim();
2025
- return email.length > 0 && email.length <= EMAIL_MAX_LENGTH && EMAIL_PATTERN.test(email) ? email : void 0;
2026
- }
2027
- function decodeJwtPayload(access) {
2028
- if (typeof access !== "string") return void 0;
2029
- const encoded = access.split(".")[1];
2030
- if (typeof encoded !== "string" || encoded.length === 0) return void 0;
2031
- try {
2032
- return JSON.parse(Buffer.from(encoded, "base64url").toString("utf8"));
2033
- } catch {
2034
- return;
1982
+ //#region src/subscription-credentials.js
1983
+ async function readSubscriptionCredentials(getAuth, readCredential, signal) {
1984
+ for (let attempt = 0; attempt < 2; attempt++) {
1985
+ const auth = await getAuth({ signal });
1986
+ signal?.throwIfAborted();
1987
+ if (typeof auth?.auth?.apiKey !== "string" || !auth.auth.apiKey) break;
1988
+ const credential = await readCredential({ signal });
1989
+ signal?.throwIfAborted();
1990
+ if (credential?.type !== "oauth") break;
1991
+ if (credential.access === auth.auth.apiKey) return {
1992
+ access: credential.access,
1993
+ accountId: credential.accountId
1994
+ };
2035
1995
  }
1996
+ return {
1997
+ access: void 0,
1998
+ accountId: void 0
1999
+ };
2036
2000
  }
2037
- function emailFromAccessToken(access) {
2038
- const payload = decodeJwtPayload(access);
2039
- return normalizeAccountEmail(payload?.["https://api.openai.com/profile"]?.email ?? payload?.email);
2040
- }
2041
- /** Normalize the one non-secret account attribute that may cross the UI boundary. */
2042
- function sanitizeOAuthCredential(value) {
2043
- const credential = assertOAuthCredential$1(value);
2044
- const email = emailFromAccessToken(credential.access) ?? normalizeAccountEmail(credential.email);
2045
- if (email === void 0) {
2046
- delete credential.email;
2047
- return credential;
2048
- }
2001
+ const ORIGINAL_IMAGE_CHUNK_BYTES = 4 * 1024 * 1024;
2002
+ const ORIGINAL_IMAGE_ID_PATTERN = /^img_[0-9a-f]{32}$/u;
2003
+ const positiveInteger$1 = (value) => Number.isSafeInteger(value) && value > 0;
2004
+ function decodeOriginalImageRef(value) {
2005
+ if (value === null || typeof value !== "object" || Array.isArray(value) || typeof value.assetId !== "string" || !ORIGINAL_IMAGE_ID_PATTERN.test(value.assetId) || value.mediaType !== "image/png" || !positiveInteger$1(value.bytes) || value.bytes > 48 * 1024 * 1024 || !positiveInteger$1(value.width) || !positiveInteger$1(value.height) || typeof value.name !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u.test(value.name) || typeof value.sha256 !== "string" || !/^[0-9a-f]{64}$/u.test(value.sha256)) return void 0;
2049
2006
  return {
2050
- ...credential,
2051
- email
2007
+ assetId: value.assetId,
2008
+ mediaType: value.mediaType,
2009
+ bytes: value.bytes,
2010
+ width: value.width,
2011
+ height: value.height,
2012
+ name: value.name,
2013
+ sha256: value.sha256
2052
2014
  };
2053
2015
  }
2054
- function assertOAuthCredential$1(value) {
2055
- if (value === null || typeof value !== "object" || value.type !== "oauth" || typeof value.access !== "string" || value.access.length === 0 || typeof value.refresh !== "string" || value.refresh.length === 0 || typeof value.expires !== "number" || !Number.isFinite(value.expires)) throw new Error("Codex account vault received a malformed OAuth credential");
2056
- return clone$1(value);
2016
+ function decodeImagePresentation(value) {
2017
+ if (value === null || typeof value !== "object" || Array.isArray(value) || value.kind !== "codex-subscription-image" || value.schemaVersion !== 1) return void 0;
2018
+ const original = decodeOriginalImageRef(value.original);
2019
+ return original === void 0 ? void 0 : { original };
2057
2020
  }
2058
- function parseOAuthCredential$1(value) {
2059
- try {
2060
- return assertOAuthCredential$1(JSON.parse(value));
2061
- } catch (error) {
2062
- if (error?.message === "Codex account vault received a malformed OAuth credential") throw error;
2063
- throw new Error("Codex account vault contains malformed OAuth JSON", { cause: error });
2064
- }
2021
+ function originalImageRefsEqual(left, right) {
2022
+ const a = decodeOriginalImageRef(left);
2023
+ const b = decodeOriginalImageRef(right);
2024
+ return a !== void 0 && b !== void 0 && a.assetId === b.assetId && a.mediaType === b.mediaType && a.bytes === b.bytes && a.width === b.width && a.height === b.height && a.name === b.name && a.sha256 === b.sha256;
2065
2025
  }
2066
- function normalizeLabel(value) {
2067
- if (typeof value !== "string") throw new Error("Codex account label must be text");
2068
- const label = value.trim().replace(/\s+/gu, " ");
2069
- if (label.length === 0 || label.length > 48) throw new Error("Codex account label must contain 1 to 48 characters");
2070
- return label;
2026
+ /** Resolve only an exact original reference copied into a DSH fork prefix. */
2027
+ function inheritedOriginalImageRef(session, assetId) {
2028
+ const parentSession = session?.header?.parentSession;
2029
+ const seedLength = Number.isSafeInteger(session?.inheritedEventCount) ? session.inheritedEventCount : session?.header?.seedLength;
2030
+ if (typeof parentSession !== "string" || parentSession.length === 0 || !Number.isSafeInteger(seedLength) || seedLength < 0 || !ORIGINAL_IMAGE_ID_PATTERN.test(assetId)) return void 0;
2031
+ let events = session?.events;
2032
+ if (!Array.isArray(events) && typeof session?.snapshotEvents === "function") try {
2033
+ events = session.snapshotEvents();
2034
+ } catch {
2035
+ return;
2036
+ }
2037
+ if (!Array.isArray(events)) return void 0;
2038
+ for (const event of events) {
2039
+ if (!Number.isSafeInteger(event?.seq) || event.seq < 0 || event.seq >= seedLength || event.type !== "tool/result") continue;
2040
+ const original = decodeImagePresentation(event.data?.meta)?.original;
2041
+ if (original?.assetId === assetId) return original;
2042
+ }
2071
2043
  }
2072
- function assertVaultRecord(record) {
2073
- if (record?.kind !== "grant" || record.payload?.version !== VERSION || typeof record.payload.activeId !== "string" || !Array.isArray(record.payload.accounts) || record.payload.accounts.length === 0) throw new Error("Codex account vault contains a malformed grant record");
2074
- const ids = /* @__PURE__ */ new Set();
2075
- const accounts = record.payload.accounts.map((account) => {
2076
- if (account === null || typeof account !== "object" || typeof account.id !== "string" || account.id.length === 0 || ids.has(account.id)) throw new Error("Codex account vault contains a malformed account id");
2077
- ids.add(account.id);
2078
- return {
2079
- id: account.id,
2080
- label: normalizeLabel(account.label),
2081
- credential: sanitizeOAuthCredential(account.credential)
2082
- };
2083
- });
2084
- if (!ids.has(record.payload.activeId)) throw new Error("Codex account vault active account is missing");
2085
- const legacyAccountId = record.payload.legacyAccountId;
2086
- if (legacyAccountId !== void 0 && !ids.has(legacyAccountId)) throw new Error("Codex account vault legacy account is missing");
2044
+ //#endregion
2045
+ //#region src/version.js
2046
+ const PACKAGE_VERSION = "2.5.0";
2047
+ const USER_AGENT = `dsh-codex-subscription/${PACKAGE_VERSION}`;
2048
+ //#endregion
2049
+ //#region src/codex-images.js
2050
+ const CODEX_IMAGE_TOOL_NAME = "codex_image_generate";
2051
+ const CODEX_IMAGE_GENERATION_URL = "https://chatgpt.com/backend-api/codex/images/generations";
2052
+ const CODEX_IMAGE_EDIT_URL = "https://chatgpt.com/backend-api/codex/images/edits";
2053
+ const MAX_REFERENCE_IMAGES = 5;
2054
+ const RESPONSE_ENVELOPE_BYTES = 1024 * 1024;
2055
+ const IMAGE_BACKGROUNDS = /* @__PURE__ */ new Set([
2056
+ "auto",
2057
+ "transparent",
2058
+ "opaque"
2059
+ ]);
2060
+ const PNG_SIGNATURE = Buffer.from([
2061
+ 137,
2062
+ 80,
2063
+ 78,
2064
+ 71,
2065
+ 13,
2066
+ 10,
2067
+ 26,
2068
+ 10
2069
+ ]);
2070
+ const FULL_ATTACHMENT_ID = /^sha256:[0-9a-f]{64}$/u;
2071
+ const BARE_ATTACHMENT_DIGEST = /^[0-9a-f]{64}$/iu;
2072
+ const PATH_LIKE_ATTACHMENT_ID = /[\\/]/u;
2073
+ const FILE_NAME_ATTACHMENT_ID = /\.[A-Za-z0-9]{1,16}$/u;
2074
+ const record$4 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
2075
+ const nonEmpty$2 = (value) => typeof value === "string" && value.trim().length > 0 ? value.trim() : void 0;
2076
+ const NETWORK_REASONS = {
2077
+ UND_ERR_HEADERS_TIMEOUT: "the image service sent no response within 5 minutes",
2078
+ UND_ERR_BODY_TIMEOUT: "the image response stalled",
2079
+ UND_ERR_CONNECT_TIMEOUT: "the connection to the image service timed out",
2080
+ UND_ERR_SOCKET: "the connection closed unexpectedly",
2081
+ ECONNRESET: "the connection was reset",
2082
+ ETIMEDOUT: "the connection timed out",
2083
+ ENOTFOUND: "DNS lookup failed",
2084
+ ECONNREFUSED: "the connection was refused",
2085
+ EAI_AGAIN: "DNS lookup failed"
2086
+ };
2087
+ function requestFailureReason(error) {
2088
+ const code = [(error?.cause ?? error)?.code, error?.code].find((value) => typeof value === "string" && /^[A-Z][A-Z0-9_]{2,39}$/u.test(value));
2089
+ if (code !== void 0) return `: ${NETWORK_REASONS[code] ?? "network error"} (${code})`;
2090
+ if (error?.name === "TimeoutError") return ": the image request timed out";
2091
+ return "";
2092
+ }
2093
+ function normalizeImageOptions(args) {
2094
+ const quality = nonEmpty$2(args?.quality) ?? "auto";
2095
+ const background = nonEmpty$2(args?.background) ?? "auto";
2096
+ const size = nonEmpty$2(args?.size) ?? "auto";
2097
+ validateImageQuality(resolveImageModel(args?.model), quality);
2098
+ if (!IMAGE_BACKGROUNDS.has(background)) throw new Error("background must be auto, transparent, or opaque");
2099
+ if (size !== "auto") {
2100
+ const match = /^(\d+)x(\d+)$/u.exec(size);
2101
+ const width = Number(match?.[1]);
2102
+ const height = Number(match?.[2]);
2103
+ const short = Math.min(width, height);
2104
+ const long = Math.max(width, height);
2105
+ const pixels = width * height;
2106
+ if (match === null || width % 16 !== 0 || height % 16 !== 0 || long > 3840 || long > short * 3 || pixels < 655360 || pixels > 8294400) throw new Error("size must be auto or a valid GPT Image 2 widthxheight resolution");
2107
+ }
2087
2108
  return {
2088
- version: VERSION,
2089
- activeId: record.payload.activeId,
2090
- legacyAccountId,
2091
- accounts
2109
+ quality,
2110
+ background,
2111
+ size
2092
2112
  };
2093
2113
  }
2094
- const grant = (payload) => ({
2095
- kind: "grant",
2096
- payload
2097
- });
2098
- var PendingOAuthCredentialStore = class {
2099
- #credential;
2100
- async read(providerId) {
2101
- if (providerId !== "openai-codex") throw new Error("Pending Codex login received an unknown provider");
2102
- return clone$1(this.#credential);
2103
- }
2104
- async list() {
2105
- return this.#credential === void 0 ? [] : [{
2106
- providerId: "openai-codex",
2107
- type: "oauth"
2108
- }];
2109
- }
2110
- async modify(providerId, update) {
2111
- if (providerId !== "openai-codex") throw new Error("Pending Codex login received an unknown provider");
2112
- const next = await update(clone$1(this.#credential));
2113
- if (next !== void 0) this.#credential = sanitizeOAuthCredential(next);
2114
- return clone$1(this.#credential);
2115
- }
2116
- async delete(providerId) {
2117
- if (providerId !== "openai-codex") throw new Error("Pending Codex login received an unknown provider");
2118
- this.#credential = void 0;
2119
- }
2120
- credential() {
2121
- return clone$1(this.#credential);
2122
- }
2123
- };
2124
- /**
2125
- * Multi-account owner state stored in DSH's atomic plugin credential record.
2126
- * The old single-account reference remains as a rollback source and is kept in
2127
- * sync whenever that imported account rotates its refresh token.
2128
- */
2129
- var DshOAuthAccountVault = class {
2130
- #tail = Promise.resolve();
2131
- constructor(credentials, options) {
2132
- if (credentials === void 0 || credentials === null || typeof credentials.readRecord !== "function" || typeof credentials.modifyRecord !== "function") throw new Error("Codex multi-account requires DSH credential records");
2133
- this.credentials = credentials;
2134
- this.key = options.key;
2135
- this.legacyRef = options.legacyRef;
2136
- this.legacyRefs = Object.freeze([...options.legacyRefs ?? []]);
2137
- this.createId = options.createId ?? randomUUID;
2138
- this.onLegacySyncFailure = options.onLegacySyncFailure ?? (() => {});
2139
- }
2140
- #enqueue(operation) {
2141
- const current = this.#tail.catch(() => void 0).then(operation);
2142
- this.#tail = current.catch(() => void 0);
2143
- return current;
2144
- }
2145
- async #legacyCredential() {
2146
- for (const ref of [this.legacyRef, ...this.legacyRefs]) {
2147
- const hit = await this.credentials.resolve(ref);
2148
- if (hit?.value === void 0 || hit.value === "") continue;
2149
- return {
2150
- ref,
2151
- credential: parseOAuthCredential$1(hit.value)
2152
- };
2153
- }
2154
- }
2155
- async #ensurePayload() {
2156
- const existing = await this.credentials.readRecord(this.key);
2157
- if (existing !== void 0) return assertVaultRecord(existing);
2158
- const legacy = await this.#legacyCredential();
2159
- if (legacy === void 0) return void 0;
2160
- const id = this.createId();
2161
- return assertVaultRecord(await this.credentials.modifyRecord(this.key, (current) => {
2162
- if (current !== void 0) return Promise.resolve(current);
2163
- return Promise.resolve(grant({
2164
- version: VERSION,
2165
- activeId: id,
2166
- legacyAccountId: id,
2167
- accounts: [{
2168
- id,
2169
- label: DEFAULT_LABEL,
2170
- credential: legacy.credential
2171
- }]
2172
- }));
2173
- }));
2114
+ function encodedLimit(decodedBytes) {
2115
+ return Math.ceil(decodedBytes / 3) * 4;
2116
+ }
2117
+ function validBase64Body(value, end) {
2118
+ for (let index = 0; index < end; index += 1) {
2119
+ const code = value.charCodeAt(index);
2120
+ if (!(code >= 65 && code <= 90 || code >= 97 && code <= 122 || code >= 48 && code <= 57 || code === 43 || code === 47)) return false;
2174
2121
  }
2175
- async #modifyPayload(update) {
2176
- await this.#ensurePayload();
2177
- let previousLegacy;
2178
- const payload = assertVaultRecord(await this.credentials.modifyRecord(this.key, async (current) => {
2179
- if (current === void 0) throw new Error("Codex account vault is not signed in");
2180
- const payload = assertVaultRecord(current);
2181
- previousLegacy = payload.accounts.find((account) => account.id === payload.legacyAccountId)?.credential;
2182
- const next = await update(clone$1(payload));
2183
- return grant(next);
2184
- }));
2185
- const legacy = payload.accounts.find((account) => account.id === payload.legacyAccountId)?.credential;
2186
- try {
2187
- if (legacy === void 0) {
2188
- if (previousLegacy !== void 0) await this.credentials.unset(this.legacyRef);
2189
- } else if (JSON.stringify(legacy) !== JSON.stringify(previousLegacy)) await this.credentials.set(this.legacyRef, JSON.stringify(legacy));
2190
- } catch {
2191
- this.onLegacySyncFailure();
2122
+ return true;
2123
+ }
2124
+ async function readJsonWithin(response, maximumBytes) {
2125
+ const contentLength = Number(response.headers.get("content-length"));
2126
+ if (Number.isFinite(contentLength) && contentLength > maximumBytes) throw new Error("Codex image response exceeds the image size limit");
2127
+ if (response.body === null) throw new Error("Codex returned an unreadable image response");
2128
+ const reader = response.body.getReader();
2129
+ const chunks = [];
2130
+ let bytes = 0;
2131
+ while (true) {
2132
+ const { done, value } = await reader.read();
2133
+ if (done) break;
2134
+ bytes += value.byteLength;
2135
+ if (bytes > maximumBytes) {
2136
+ await reader.cancel();
2137
+ throw new Error("Codex image response exceeds the image size limit");
2192
2138
  }
2193
- return payload;
2194
- }
2195
- list() {
2196
- return this.#enqueue(async () => {
2197
- const payload = await this.#ensurePayload();
2198
- if (payload === void 0) return [];
2199
- return payload.accounts.map((account) => ({
2200
- id: account.id,
2201
- label: account.label,
2202
- active: account.id === payload.activeId,
2203
- expiresAt: account.credential.expires,
2204
- ...account.credential.email === void 0 ? {} : { email: account.credential.email }
2205
- }));
2206
- });
2207
- }
2208
- readActive() {
2209
- return this.#enqueue(async () => {
2210
- const payload = await this.#ensurePayload();
2211
- return clone$1(payload?.accounts.find((account) => account.id === payload.activeId)?.credential);
2212
- });
2213
- }
2214
- activeId() {
2215
- return this.#enqueue(async () => (await this.#ensurePayload())?.activeId);
2216
- }
2217
- add(label, credential) {
2218
- return this.#enqueue(async () => {
2219
- const normalizedLabel = normalizeLabel(label);
2220
- const validated = sanitizeOAuthCredential(credential);
2221
- await this.#ensurePayload();
2222
- const id = this.createId();
2223
- const account = (await this.#modifyPayload((current) => ({
2224
- ...current,
2225
- activeId: id,
2226
- accounts: [...current.accounts, {
2227
- id,
2228
- label: normalizedLabel,
2229
- credential: validated
2230
- }]
2231
- }))).accounts.find((candidate) => candidate.id === id);
2232
- return {
2233
- id,
2234
- label: account.label,
2235
- active: true,
2236
- expiresAt: account.credential.expires,
2237
- ...account.credential.email === void 0 ? {} : { email: account.credential.email }
2238
- };
2239
- });
2139
+ chunks.push(value);
2240
2140
  }
2241
- select(id) {
2242
- return this.#enqueue(async () => {
2243
- await this.#modifyPayload((current) => {
2244
- if (!current.accounts.some((account) => account.id === id)) throw new Error("Unknown Codex account");
2245
- return {
2246
- ...current,
2247
- activeId: id
2248
- };
2249
- });
2250
- });
2141
+ const body = Buffer.concat(chunks.map((chunk) => Buffer.from(chunk)), bytes).toString("utf8");
2142
+ try {
2143
+ return JSON.parse(body);
2144
+ } catch {
2145
+ throw new Error("Codex returned an unreadable image response");
2251
2146
  }
2252
- modifyActive(update) {
2253
- return this.#enqueue(async () => {
2254
- if (await this.#ensurePayload() === void 0) {
2255
- const initial = await update(void 0);
2256
- if (initial === void 0) return void 0;
2257
- const credential = sanitizeOAuthCredential(initial);
2258
- await this.credentials.set(this.legacyRef, JSON.stringify(credential));
2259
- await this.#ensurePayload();
2260
- return clone$1(credential);
2261
- }
2262
- let result;
2263
- await this.#modifyPayload(async (current) => {
2264
- const index = current.accounts.findIndex((account) => account.id === current.activeId);
2265
- const previous = clone$1(current.accounts[index].credential);
2266
- const next = await update(previous);
2267
- if (next === void 0) {
2268
- result = previous;
2269
- return current;
2270
- }
2271
- const credential = sanitizeOAuthCredential(next);
2272
- if (credential.email === void 0 && previous.email !== void 0) credential.email = previous.email;
2273
- const accounts = [...current.accounts];
2274
- accounts[index] = {
2275
- ...accounts[index],
2276
- credential
2277
- };
2278
- result = clone$1(credential);
2279
- return {
2280
- ...current,
2281
- accounts
2282
- };
2283
- });
2284
- return result;
2285
- });
2286
- }
2287
- deleteAll() {
2288
- return this.#enqueue(async () => {
2289
- await this.credentials.deleteRecord(this.key);
2290
- await this.credentials.unset(this.legacyRef);
2291
- for (const ref of this.legacyRefs) await this.credentials.unset(ref);
2292
- });
2293
- }
2294
- remove(id) {
2295
- return this.#enqueue(async () => {
2296
- await this.#modifyPayload((current) => {
2297
- if (!current.accounts.some((account) => account.id === id)) throw new Error("Unknown Codex account");
2298
- if (current.accounts.length === 1) throw new Error("Cannot remove the last account; sign out instead");
2299
- const accounts = current.accounts.filter((account) => account.id !== id);
2300
- return {
2301
- ...current,
2302
- activeId: current.activeId === id ? accounts[0].id : current.activeId,
2303
- legacyAccountId: current.legacyAccountId === id ? void 0 : current.legacyAccountId,
2304
- accounts
2305
- };
2306
- });
2307
- });
2308
- }
2309
- };
2310
- //#endregion
2311
- //#region src/credential-store.js
2312
- const PROVIDER$1 = "openai-codex";
2313
- const abortIfNeeded = (options) => options?.signal?.throwIfAborted();
2314
- const clone = (value) => value === void 0 ? void 0 : structuredClone(value);
2315
- function assertProvider(providerId) {
2316
- if (providerId !== PROVIDER$1) throw new Error(`Codex credential store does not own provider ${JSON.stringify(providerId)}`);
2317
2147
  }
2318
- function assertOAuthCredential(value) {
2319
- if (value === void 0) return void 0;
2320
- if (value === null || typeof value !== "object" || value.type !== "oauth" || typeof value.access !== "string" || value.access.length === 0 || typeof value.refresh !== "string" || value.refresh.length === 0 || typeof value.expires !== "number" || !Number.isFinite(value.expires)) throw new Error("Codex credential store received a malformed OAuth credential");
2321
- return clone(value);
2148
+ /** Strictly decode one PNG returned by the subscription backend. */
2149
+ function decodeCodexPng(value, maximumBytes) {
2150
+ const encoded = nonEmpty$2(value);
2151
+ const padding = encoded?.endsWith("==") ? 2 : encoded?.endsWith("=") ? 1 : 0;
2152
+ if (encoded === void 0 || encoded.length % 4 !== 0 || !validBase64Body(encoded, encoded.length - padding)) throw new Error("Codex returned an invalid base64 PNG");
2153
+ const decodedBytes = encoded.length / 4 * 3 - padding;
2154
+ if (decodedBytes > maximumBytes) throw new Error("Codex image exceeds the image size limit");
2155
+ const data = Buffer.from(encoded, "base64");
2156
+ if (data.length !== decodedBytes || data.length < PNG_SIGNATURE.length || !data.subarray(0, PNG_SIGNATURE.length).equals(PNG_SIGNATURE)) throw new Error("Codex returned an invalid PNG");
2157
+ return new Uint8Array(data);
2322
2158
  }
2323
- function parseOAuthCredential(value) {
2324
- try {
2325
- return assertOAuthCredential(JSON.parse(value));
2326
- } catch (error) {
2327
- if (error?.message === "Codex credential store received a malformed OAuth credential") throw error;
2328
- throw new Error("Codex credential store contains malformed OAuth JSON", { cause: error });
2329
- }
2159
+ function imageReference(value) {
2160
+ const originalDimensions = record$4(value.originalDimensions) ? {
2161
+ width: value.originalDimensions.width,
2162
+ height: value.originalDimensions.height
2163
+ } : void 0;
2164
+ return {
2165
+ attachmentId: value.attachmentId,
2166
+ mediaType: value.mediaType,
2167
+ bytes: value.bytes,
2168
+ width: value.width,
2169
+ height: value.height,
2170
+ ...value.name === void 0 ? {} : { name: value.name },
2171
+ ...originalDimensions === void 0 ? {} : { originalDimensions }
2172
+ };
2330
2173
  }
2331
- /**
2332
- * Adapt DSH's managed string credential service to pi-ai's typed OAuth store.
2333
- * Refresh/login/logout operations are serialized so an older refresh response
2334
- * cannot overwrite a newer rotated token.
2335
- */
2336
- var DshOAuthCredentialStore = class {
2337
- #chains = /* @__PURE__ */ new Map();
2338
- constructor(credentials, ref, legacyRefs = [], options = {}) {
2339
- if (credentials === void 0 || credentials === null) throw new Error("Codex OAuth requires the DSH credentials service");
2340
- const expirySkewMs = options.expirySkewMs ?? 0;
2341
- if (!Number.isFinite(expirySkewMs) || expirySkewMs < 0) throw new Error("Codex OAuth expiry skew must be a non-negative finite number");
2342
- this.credentials = credentials;
2343
- this.ref = ref;
2344
- this.legacyRefs = Object.freeze([...legacyRefs]);
2345
- this.expirySkewMs = expirySkewMs;
2346
- this.vault = options.vault;
2347
- }
2348
- #enqueue(providerId, operation, options) {
2349
- assertProvider(providerId);
2350
- const current = (this.#chains.get(providerId) ?? Promise.resolve()).catch(() => void 0).then(async () => {
2351
- abortIfNeeded(options);
2352
- return operation();
2353
- });
2354
- const tail = current.catch(() => void 0);
2355
- this.#chains.set(providerId, tail);
2356
- tail.finally(() => {
2357
- if (this.#chains.get(providerId) === tail) this.#chains.delete(providerId);
2358
- });
2359
- return current;
2360
- }
2361
- async #read(providerId, options) {
2362
- assertProvider(providerId);
2363
- abortIfNeeded(options);
2364
- if (this.vault !== void 0) {
2365
- const current = await this.vault.readActive();
2366
- if (current === void 0) return void 0;
2367
- return this.expirySkewMs === 0 ? current : {
2368
- ...current,
2369
- expires: current.expires - this.expirySkewMs
2370
- };
2371
- }
2372
- let hit = await this.credentials.resolve(this.ref);
2373
- if (hit?.value === void 0 || hit.value === "") for (const legacyRef of this.legacyRefs) {
2374
- const legacy = await this.credentials.resolve(legacyRef);
2375
- if (legacy?.value === void 0 || legacy.value === "") continue;
2376
- const migrated = parseOAuthCredential(legacy.value);
2377
- await this.credentials.set(this.ref, JSON.stringify(migrated));
2378
- await this.credentials.unset(legacyRef);
2379
- hit = { value: JSON.stringify(migrated) };
2380
- break;
2381
- }
2382
- abortIfNeeded(options);
2383
- if (hit?.value === void 0 || hit.value === "") return void 0;
2384
- const credential = parseOAuthCredential(hit.value);
2385
- return this.expirySkewMs === 0 ? credential : {
2386
- ...credential,
2387
- expires: credential.expires - this.expirySkewMs
2388
- };
2389
- }
2390
- read(providerId, options) {
2391
- return this.#enqueue(providerId, () => this.#read(providerId, options), options);
2392
- }
2393
- async list(options) {
2394
- abortIfNeeded(options);
2395
- return await this.read(PROVIDER$1, options) === void 0 ? [] : [{
2396
- providerId: PROVIDER$1,
2397
- type: "oauth"
2398
- }];
2399
- }
2400
- modify(providerId, update, options) {
2401
- return this.#enqueue(providerId, async () => {
2402
- if (this.vault !== void 0) {
2403
- const next = await this.vault.modifyActive(async (current) => {
2404
- const visible = current === void 0 || this.expirySkewMs === 0 ? current : {
2405
- ...current,
2406
- expires: current.expires - this.expirySkewMs
2407
- };
2408
- const updated = await update(clone(visible));
2409
- return updated === void 0 ? void 0 : assertOAuthCredential(updated);
2410
- });
2411
- abortIfNeeded(options);
2412
- return clone(next);
2413
- }
2414
- const current = await this.#read(providerId, options);
2415
- const next = await update(clone(current));
2416
- abortIfNeeded(options);
2417
- if (next === void 0) return current;
2418
- const validated = assertOAuthCredential(next);
2419
- await this.credentials.set(this.ref, JSON.stringify(validated));
2420
- for (const legacyRef of this.legacyRefs) await this.credentials.unset(legacyRef);
2421
- abortIfNeeded(options);
2422
- return clone(validated);
2423
- }, options);
2424
- }
2425
- delete(providerId, options) {
2426
- return this.#enqueue(providerId, async () => {
2427
- if (this.vault !== void 0) {
2428
- await this.vault.deleteAll();
2429
- abortIfNeeded(options);
2430
- return;
2431
- }
2432
- await this.credentials.unset(this.ref);
2433
- for (const legacyRef of this.legacyRefs) await this.credentials.unset(legacyRef);
2434
- abortIfNeeded(options);
2435
- }, options);
2174
+ function normalizeAttachmentId(value) {
2175
+ if (typeof value !== "string") return void 0;
2176
+ if (FULL_ATTACHMENT_ID.test(value)) return value;
2177
+ const bare = BARE_ATTACHMENT_DIGEST.exec(value);
2178
+ return bare === null ? void 0 : `sha256:${bare[0].toLowerCase()}`;
2179
+ }
2180
+ function invalidAttachmentId(value) {
2181
+ const attachmentId = value?.attachmentId;
2182
+ if (typeof attachmentId === "string" && (PATH_LIKE_ATTACHMENT_ID.test(attachmentId) || FILE_NAME_ATTACHMENT_ID.test(attachmentId))) throw new Error("referenceImages attachmentId is a file path or filename; call read_image on that file and retry with its complete sha256:<64 lowercase hex> attachment reference. Do not omit referenceImages or fall back to new image generation.");
2183
+ throw new Error("referenceImages attachmentId must be sha256:<64 lowercase hex> copied from an image block or read_image result. Do not omit referenceImages or fall back to new image generation.");
2184
+ }
2185
+ function referenceOf(value, attachments) {
2186
+ const attachmentId = normalizeAttachmentId(value?.attachmentId);
2187
+ if (attachmentId === void 0) invalidAttachmentId(value);
2188
+ if (!record$4(value) || !attachments.imageLimits.mediaTypes.includes(value.mediaType) || !Number.isSafeInteger(value.bytes) || value.bytes <= 0 || !Number.isSafeInteger(value.width) || value.width <= 0 || !Number.isSafeInteger(value.height) || value.height <= 0 || value.name !== void 0 && (typeof value.name !== "string" || value.name.length > 256) || value.originalDimensions !== void 0 && (!record$4(value.originalDimensions) || !Number.isSafeInteger(value.originalDimensions.width) || value.originalDimensions.width <= 0 || !Number.isSafeInteger(value.originalDimensions.height) || value.originalDimensions.height <= 0)) throw new Error("referenceImages contains an invalid image reference");
2189
+ return imageReference({
2190
+ ...value,
2191
+ attachmentId
2192
+ });
2193
+ }
2194
+ function sessionImageReferences(messages) {
2195
+ const references = /* @__PURE__ */ new Map();
2196
+ const visit = (content) => {
2197
+ if (!Array.isArray(content)) return;
2198
+ for (const block of content) if (block?.type === "image" && record$4(block.attachment)) {
2199
+ const id = normalizeAttachmentId(block.attachment.attachmentId);
2200
+ if (id !== void 0) references.set(id, block.attachment);
2201
+ } else if (block?.type === "tool-result") visit(block.content);
2202
+ };
2203
+ for (const message of messages ?? []) visit(message?.content);
2204
+ return references;
2205
+ }
2206
+ async function readSessionImage(attachmentId, attachments, signal, messages) {
2207
+ const id = normalizeAttachmentId(attachmentId);
2208
+ if (id === void 0) invalidAttachmentId({ attachmentId });
2209
+ const selected = sessionImageReferences(messages).get(id);
2210
+ if (selected === void 0) throw new Error("The selected image attachment cannot be found in the current session. Copy attachmentId from the image block of the user message; never pass a path or filename.");
2211
+ const stored = await attachments.readImage(referenceOf(selected, attachments), signal);
2212
+ return {
2213
+ data: stored.data,
2214
+ mediaType: stored.ref.mediaType,
2215
+ name: stored.ref.name
2216
+ };
2217
+ }
2218
+ async function editImages(values, attachments, signal, messages) {
2219
+ if (!Array.isArray(values) || values.length === 0 || values.length > MAX_REFERENCE_IMAGES) throw new Error(`referenceImages must contain between 1 and ${MAX_REFERENCE_IMAGES} images`);
2220
+ const available = messages === void 0 ? void 0 : sessionImageReferences(messages);
2221
+ const references = values.map((value) => {
2222
+ const id = normalizeAttachmentId(value?.attachmentId);
2223
+ if (id === void 0) invalidAttachmentId(value);
2224
+ if (available === void 0) return referenceOf(value, attachments);
2225
+ const selected = available.get(id);
2226
+ if (selected === void 0) throw new Error("The selected image attachment cannot be found in the current session. Call read_image on the intended image and retry with its returned reference. Do not omit referenceImages or substitute another image.");
2227
+ return referenceOf(selected, attachments);
2228
+ });
2229
+ if (new Set(references.map((value) => value.attachmentId)).size !== references.length) throw new Error("referenceImages must not contain duplicates");
2230
+ const images = [];
2231
+ let totalBytes = 0;
2232
+ for (const reference of references) {
2233
+ const stored = await attachments.readImage(reference, signal);
2234
+ totalBytes += stored.data.byteLength;
2235
+ if (totalBytes > attachments.imageLimits.maxMessageImageBytes) throw new Error("referenceImages exceed the DSH message image limit");
2236
+ images.push({ image_url: `data:${stored.ref.mediaType};base64,${Buffer.from(stored.data).toString("base64")}` });
2436
2237
  }
2437
- };
2438
- /** Return only account state that is safe to expose to the browser client. */
2439
- function createCodexAuthService(models, store, options = {}) {
2440
- const runLogin = options.runLogin ?? ((run) => run());
2441
- const accountVault = options.accountVault;
2442
- const createLoginModels = options.createLoginModels;
2443
- const createPendingStore = options.createPendingStore ?? (() => new PendingOAuthCredentialStore());
2444
- return Object.freeze({
2445
- async status(options) {
2446
- const current = await store.read(PROVIDER$1, options);
2447
- const accounts = await accountVault?.list();
2448
- if (current === void 0) return {
2449
- authenticated: false,
2450
- provider: PROVIDER$1,
2451
- ...accounts === void 0 ? {} : { accounts }
2452
- };
2453
- return {
2454
- authenticated: true,
2455
- provider: PROVIDER$1,
2456
- type: "oauth",
2457
- expiresAt: current.expires,
2458
- ...accounts === void 0 ? {} : { accounts }
2459
- };
2460
- },
2461
- login(interaction, input = {}) {
2462
- if (input.label !== void 0) {
2463
- if (accountVault === void 0 || createLoginModels === void 0) throw new Error("Codex multi-account is unavailable");
2464
- return runLogin(async () => {
2465
- const pending = createPendingStore();
2466
- await createLoginModels(pending).login(PROVIDER$1, "oauth", interaction);
2467
- const credential = pending.credential();
2468
- if (credential === void 0) throw new Error("Codex login did not return credentials");
2469
- await accountVault.add(input.label, credential);
2470
- });
2471
- }
2472
- return runLogin(() => models.login(PROVIDER$1, "oauth", interaction));
2473
- },
2474
- async select(id) {
2475
- if (accountVault === void 0) throw new Error("Codex multi-account is unavailable");
2476
- await accountVault.select(id);
2477
- return this.status();
2478
- },
2479
- async remove(id) {
2480
- if (accountVault === void 0) throw new Error("Codex multi-account is unavailable");
2481
- await accountVault.remove(id);
2482
- return this.status();
2483
- },
2484
- logout(options) {
2485
- return models.logout(PROVIDER$1, options);
2486
- }
2487
- });
2238
+ return images;
2488
2239
  }
2489
- //#endregion
2490
- //#region src/external-url.js
2491
- const OPENAI_AUTH_ORIGIN = "https://auth.openai.com";
2492
- /** Validate the only external origin this plugin may launch. */
2493
- function assertCodexAuthUrl(value) {
2494
- let url;
2495
- try {
2496
- url = new URL(value);
2497
- } catch {
2498
- throw new Error("Codex auth URL is invalid");
2499
- }
2500
- if (url.protocol !== "https:") throw new Error("Codex auth URL must use HTTPS");
2501
- if (url.origin !== OPENAI_AUTH_ORIGIN || url.username !== "" || url.password !== "") throw new Error("Codex auth URL must use the OpenAI auth origin");
2502
- return url.href;
2240
+ function imageContent(value) {
2241
+ const label = typeof value.size === "string" && value.size.length > 0 ? `Generated a ${value.size} image.` : "Generated an image.";
2242
+ return [{
2243
+ type: "text",
2244
+ text: value.localPath === void 0 ? label : `${label}\nOriginal PNG saved on the DSH host at: ${JSON.stringify(value.localPath)}. Read this file or copy it to the workspace with a .png extension; this host path is not a browser URL.`
2245
+ }, {
2246
+ type: "image",
2247
+ attachment: imageReference(value.image)
2248
+ }];
2503
2249
  }
2504
- /** Return a shell-free native opener command for the current desktop. */
2505
- function commandForCodexAuthUrl(value, platform = process.platform) {
2506
- const url = assertCodexAuthUrl(value);
2507
- if (platform === "win32") return {
2508
- file: "rundll32.exe",
2509
- args: ["url.dll,FileProtocolHandler", url],
2510
- shell: false
2511
- };
2512
- if (platform === "darwin") return {
2513
- file: "open",
2514
- args: [url],
2515
- shell: false
2516
- };
2517
- if (platform === "linux") return {
2518
- file: "xdg-open",
2519
- args: [url],
2520
- shell: false
2250
+ function imageOutputSchema() {
2251
+ return {
2252
+ type: "object",
2253
+ additionalProperties: false,
2254
+ properties: {
2255
+ image: {
2256
+ type: "object",
2257
+ required: true,
2258
+ additionalProperties: false,
2259
+ properties: {
2260
+ attachmentId: {
2261
+ type: "string",
2262
+ required: true
2263
+ },
2264
+ mediaType: {
2265
+ type: "string",
2266
+ enum: ["image/png"],
2267
+ required: true
2268
+ },
2269
+ bytes: {
2270
+ type: "integer",
2271
+ required: true
2272
+ },
2273
+ width: {
2274
+ type: "integer",
2275
+ required: true
2276
+ },
2277
+ height: {
2278
+ type: "integer",
2279
+ required: true
2280
+ },
2281
+ name: { type: "string" },
2282
+ originalDimensions: {
2283
+ type: "object",
2284
+ additionalProperties: false,
2285
+ properties: {
2286
+ width: {
2287
+ type: "integer",
2288
+ required: true
2289
+ },
2290
+ height: {
2291
+ type: "integer",
2292
+ required: true
2293
+ }
2294
+ }
2295
+ }
2296
+ }
2297
+ },
2298
+ original: {
2299
+ type: "object",
2300
+ required: true,
2301
+ additionalProperties: false,
2302
+ properties: {
2303
+ assetId: {
2304
+ type: "string",
2305
+ required: true
2306
+ },
2307
+ mediaType: {
2308
+ type: "string",
2309
+ enum: ["image/png"],
2310
+ required: true
2311
+ },
2312
+ bytes: {
2313
+ type: "integer",
2314
+ required: true
2315
+ },
2316
+ width: {
2317
+ type: "integer",
2318
+ required: true
2319
+ },
2320
+ height: {
2321
+ type: "integer",
2322
+ required: true
2323
+ },
2324
+ name: {
2325
+ type: "string",
2326
+ required: true
2327
+ },
2328
+ sha256: {
2329
+ type: "string",
2330
+ required: true
2331
+ }
2332
+ }
2333
+ },
2334
+ background: { type: "string" },
2335
+ requestedModel: { type: "string" },
2336
+ reportedModel: { type: "string" },
2337
+ requestedSize: { type: "string" },
2338
+ localPath: {
2339
+ type: "string",
2340
+ required: true,
2341
+ description: "Absolute path to the original PNG on the DSH host."
2342
+ },
2343
+ quality: { type: "string" },
2344
+ size: { type: "string" }
2345
+ }
2521
2346
  };
2522
- throw new Error(`Codex auth URL opener is unsupported on ${platform}`);
2523
2347
  }
2524
- function openCodexAuthUrl(value, options = {}) {
2525
- const command = commandForCodexAuthUrl(value, options.platform);
2526
- const spawnProcess = options.spawn ?? spawn;
2527
- return new Promise((resolve, reject) => {
2528
- const child = spawnProcess(command.file, command.args, {
2529
- detached: true,
2530
- stdio: "ignore",
2531
- windowsHide: true,
2532
- shell: command.shell
2533
- });
2534
- child.once("error", reject);
2535
- child.once("spawn", () => {
2536
- child.unref();
2537
- resolve();
2538
- });
2539
- });
2348
+ function responseMetadata(value) {
2349
+ const data = Array.isArray(value?.data) ? value.data[0] : void 0;
2350
+ const encoded = record$4(data) ? data.b64_json : void 0;
2351
+ if (typeof encoded !== "string") throw new Error("Codex returned no image data");
2352
+ return {
2353
+ encoded,
2354
+ background: nonEmpty$2(value.background),
2355
+ quality: nonEmpty$2(value.quality),
2356
+ size: nonEmpty$2(value.size),
2357
+ reportedModel: nonEmpty$2(value.model)
2358
+ };
2540
2359
  }
2541
- //#endregion
2542
- //#region src/login-coordinator.js
2543
- const LOGIN_METHODS = /* @__PURE__ */ new Set(["browser", "device_code"]);
2544
- const TERMINAL_PHASES = /* @__PURE__ */ new Set([
2545
- "authenticated",
2546
- "failed",
2547
- "cancelled"
2548
- ]);
2549
- const publicClone = (value) => structuredClone(value);
2550
- const asObject = (value) => value !== null && typeof value === "object" ? value : {};
2551
- const ok = (value) => ({
2552
- ok: true,
2553
- value
2554
- });
2555
- const badRequest = (message) => ({
2556
- ok: false,
2557
- error: {
2558
- code: "bad-request",
2559
- message,
2560
- details: { issues: [] }
2561
- }
2562
- });
2563
- const accountStatusError = (message) => ({
2564
- ok: false,
2565
- error: {
2566
- code: "internal",
2567
- message,
2568
- details: { issues: [] }
2569
- }
2570
- });
2571
- const classifyAccountStatusError = (error) => {
2572
- const message = error instanceof Error ? error.message : "";
2573
- if (/malformed (?:OAuth|grant|account vault)|received a malformed OAuth|contains malformed OAuth/iu.test(message)) return ["credential-malformed", "Codex account credentials are malformed"];
2574
- if (/credential|account vault|readRecord|credential store|credentials service/iu.test(message)) return ["credential-unavailable", "Codex account credentials are unavailable"];
2575
- const code = typeof error?.code === "string" ? error.code.toUpperCase() : "";
2576
- if (error?.name === "TimeoutError" || [
2577
- "TIMEOUT",
2578
- "ETIMEDOUT",
2579
- "UND_ERR_CONNECT_TIMEOUT"
2580
- ].includes(code)) return ["transport", "Codex account status service is unavailable"];
2581
- if ([
2582
- "ECONNRESET",
2583
- "ECONNREFUSED",
2584
- "ENOTFOUND",
2585
- "EAI_AGAIN",
2586
- "NETWORK",
2587
- "NETWORK_ERROR",
2588
- "TRANSPORT"
2589
- ].includes(code) || error?.name === "NetworkError") return ["transport", "Codex account status service is unavailable"];
2590
- return ["unknown", "Could not read Codex account status"];
2591
- };
2592
- const deferred = () => {
2593
- let resolve;
2594
- let reject;
2595
- return {
2596
- promise: new Promise((onResolve, onReject) => {
2597
- resolve = onResolve;
2598
- reject = onReject;
2599
- }),
2600
- resolve,
2601
- reject
2602
- };
2603
- };
2604
- const publicPrompt = (prompt) => ({
2605
- type: prompt.type,
2606
- message: String(prompt.message ?? ""),
2607
- ...typeof prompt.placeholder === "string" ? { placeholder: prompt.placeholder } : {}
2608
- });
2609
- function classifyLoginFailure(error) {
2610
- const message = error instanceof Error ? error.message : "";
2611
- if (/token exchange failed/iu.test(message)) return "token-exchange";
2612
- if (/fetch failed|\b(?:ECONN|ENOTFOUND|ETIMEDOUT|CERT_|socket|network)\b/iu.test(message)) return "network";
2613
- if (/extract accountId|account[_ -]?id/iu.test(message)) return "account-claim";
2614
- if (/credential|credentials-local|OAuth JSON/iu.test(message)) return "credential-store";
2615
- if (/Missing authorization code|State mismatch|callback/iu.test(message)) return "callback";
2616
- return "provider";
2617
- }
2618
- /** Own one host-side login without exposing tokens to the browser client. */
2619
- var CodexLoginCoordinator = class {
2620
- #sessions = /* @__PURE__ */ new Map();
2621
- #activeId;
2622
- constructor(auth, options = {}) {
2623
- this.auth = auth;
2624
- this.createId = options.createId ?? (() => crypto.randomUUID());
2625
- }
2626
- async accountStatus(options) {
2627
- return publicClone(await this.auth.status(options));
2628
- }
2629
- supportState() {
2630
- const active = this.#activeId === void 0 ? void 0 : this.#sessions.get(this.#activeId);
2631
- if (active === void 0) return { phase: "idle" };
2632
- return {
2633
- method: active.view.method,
2634
- phase: active.view.phase,
2635
- ...active.view.phase === "failed" ? { failure: classifyLoginFailure(active.hostError) } : {}
2636
- };
2637
- }
2638
- async start({ method, label }) {
2639
- if (!LOGIN_METHODS.has(method)) throw new Error(`unsupported Codex login method: ${String(method)}`);
2640
- if (label !== void 0 && (typeof label !== "string" || label.trim().length === 0 || label.trim().length > 48)) throw new Error("unsupported Codex account label");
2641
- const active = this.#activeId === void 0 ? void 0 : this.#sessions.get(this.#activeId);
2642
- if (active !== void 0 && !TERMINAL_PHASES.has(active.view.phase)) {
2643
- active.view = {
2644
- id: active.view.id,
2645
- provider: "openai-codex",
2646
- method: active.view.method,
2647
- phase: "cancelled",
2648
- authenticated: false
2649
- };
2650
- active.controller.abort(/* @__PURE__ */ new Error("Codex login replaced by a new attempt"));
2651
- }
2652
- if (active !== void 0) this.#sessions.delete(active.view.id);
2653
- const id = this.createId();
2654
- const ready = deferred();
2655
- const controller = new AbortController();
2656
- const session = {
2657
- controller,
2658
- prompt: void 0,
2659
- ready,
2660
- view: {
2661
- id,
2662
- provider: "openai-codex",
2663
- method,
2664
- phase: "starting",
2665
- authenticated: false
2666
- }
2667
- };
2668
- this.#sessions.set(id, session);
2669
- this.#activeId = id;
2670
- const publishReady = () => ready.resolve(publicClone(session.view));
2671
- const interaction = {
2672
- signal: controller.signal,
2673
- prompt: async (prompt) => {
2674
- controller.signal.throwIfAborted();
2675
- if (prompt.type === "select") return method;
2676
- if (![
2677
- "manual_code",
2678
- "text",
2679
- "secret"
2680
- ].includes(prompt.type)) throw new Error(`unsupported Codex auth prompt: ${String(prompt.type)}`);
2681
- const answer = deferred();
2682
- session.prompt = answer;
2683
- session.view = {
2684
- ...session.view,
2685
- phase: "waiting_input",
2686
- prompt: publicPrompt(prompt)
2687
- };
2688
- const abortPrompt = () => answer.reject(controller.signal.reason ?? /* @__PURE__ */ new Error("login cancelled"));
2689
- controller.signal.addEventListener("abort", abortPrompt, { once: true });
2690
- prompt.signal?.addEventListener("abort", abortPrompt, { once: true });
2691
- publishReady();
2692
- try {
2693
- return await answer.promise;
2694
- } finally {
2695
- controller.signal.removeEventListener("abort", abortPrompt);
2696
- prompt.signal?.removeEventListener("abort", abortPrompt);
2697
- if (session.prompt === answer) session.prompt = void 0;
2698
- }
2360
+ /** Create the DSH-native image-generation tool backed only by the ChatGPT subscription. */
2361
+ function createCodexImageTool(options) {
2362
+ const fetchImage = options.fetch ?? fetch;
2363
+ const attachments = options.attachments;
2364
+ return defineTool({
2365
+ name: CODEX_IMAGE_TOOL_NAME,
2366
+ description: "Generate or edit images only when the user asks for image output, not when merely discussing images. Uses the signed-in Codex subscription. For a new image, omit referenceImages. For edits, copy attachmentId only from the selected session image block; the host supplies its metadata. Never call read_image for an attachmentId or attachment filename. Use read_image only for an actual local file whose reference is not already in the conversation. Never substitute paths, unrelated images, or text-only generation for an edit. For numbered annotations, include the clean source and location-reference image, preserve the requested changes and coordinates in the prompt, and remove guidance markers from the result. If the intended references cannot be identified, ask rather than guessing.",
2367
+ parameters: {
2368
+ model: {
2369
+ type: "string",
2370
+ enum: Object.keys(IMAGE_MODELS),
2371
+ description: "Optional image engine, independent of the conversation model. When omitted, uses the user image setting (initially gpt-image-2). The 2.5 identifiers are experimental subscription candidates; override only when explicitly requested. Never silently retry with another model."
2699
2372
  },
2700
- notify: (event) => {
2701
- if (controller.signal.aborted) return;
2702
- if (event.type === "auth_url") session.view = {
2703
- ...session.view,
2704
- phase: "waiting_browser",
2705
- authUrl: assertCodexAuthUrl(event.url),
2706
- ...typeof event.instructions === "string" ? { instructions: event.instructions } : {}
2707
- };
2708
- else if (event.type === "device_code") session.view = {
2709
- ...session.view,
2710
- phase: "waiting_device",
2711
- deviceCode: {
2712
- userCode: event.userCode,
2713
- verificationUri: assertCodexAuthUrl(event.verificationUri),
2714
- ...typeof event.intervalSeconds === "number" ? { intervalSeconds: event.intervalSeconds } : {},
2715
- ...typeof event.expiresInSeconds === "number" ? { expiresInSeconds: event.expiresInSeconds } : {}
2373
+ prompt: {
2374
+ type: "string",
2375
+ required: true,
2376
+ description: "A complete, production-ready description of the image to generate."
2377
+ },
2378
+ size: {
2379
+ type: "string",
2380
+ description: "Optional GPT Image 2 output size. Use auto unless the user requests an exact valid widthxheight resolution."
2381
+ },
2382
+ quality: {
2383
+ type: "string",
2384
+ enum: [
2385
+ "auto",
2386
+ "low",
2387
+ "medium",
2388
+ "high",
2389
+ "xhigh",
2390
+ "max"
2391
+ ],
2392
+ description: "Optional rendering quality. Use auto unless the user requests draft speed or final quality."
2393
+ },
2394
+ background: {
2395
+ type: "string",
2396
+ enum: [
2397
+ "auto",
2398
+ "transparent",
2399
+ "opaque"
2400
+ ],
2401
+ description: "Optional background mode. Request transparent only when the user needs transparency."
2402
+ },
2403
+ referenceImages: {
2404
+ type: "array",
2405
+ description: "Optional references to 1-5 selected images. For session images provide only attachmentId; the host resolves trusted metadata. Omit only for a new image; never drop an invalid reference to bypass editing.",
2406
+ items: {
2407
+ type: "object",
2408
+ additionalProperties: false,
2409
+ properties: {
2410
+ attachmentId: {
2411
+ type: "string",
2412
+ required: true,
2413
+ description: "Copy the complete attachmentId from the image block or read_image result: sha256:<64 lowercase hex>. Never pass a workspace path, absolute path, or filename. A bare 64-character hex digest is accepted and normalized to sha256:<64 lowercase hex>."
2414
+ },
2415
+ mediaType: {
2416
+ type: "string",
2417
+ ...typeof options.getSessionMessages !== "function" ? { required: true } : {}
2418
+ },
2419
+ bytes: {
2420
+ type: "integer",
2421
+ ...typeof options.getSessionMessages !== "function" ? { required: true } : {}
2422
+ },
2423
+ width: {
2424
+ type: "integer",
2425
+ ...typeof options.getSessionMessages !== "function" ? { required: true } : {}
2426
+ },
2427
+ height: {
2428
+ type: "integer",
2429
+ ...typeof options.getSessionMessages !== "function" ? { required: true } : {}
2430
+ },
2431
+ name: { type: "string" },
2432
+ originalDimensions: {
2433
+ type: "object",
2434
+ additionalProperties: false,
2435
+ properties: {
2436
+ width: {
2437
+ type: "integer",
2438
+ ...typeof options.getSessionMessages !== "function" ? { required: true } : {}
2439
+ },
2440
+ height: {
2441
+ type: "integer",
2442
+ ...typeof options.getSessionMessages !== "function" ? { required: true } : {}
2443
+ }
2444
+ }
2445
+ }
2716
2446
  }
2717
- };
2718
- else session.view = {
2719
- ...session.view,
2720
- message: String(event.message ?? "")
2721
- };
2722
- publishReady();
2447
+ }
2723
2448
  }
2724
- };
2725
- session.run = Promise.resolve().then(() => this.auth.login(interaction, label === void 0 ? {} : { label: label.trim() })).then(async () => {
2726
- if (controller.signal.aborted) return;
2727
- const status = await this.auth.status();
2728
- session.view = {
2729
- id,
2730
- provider: "openai-codex",
2731
- method,
2732
- phase: "authenticated",
2733
- authenticated: status.authenticated === true,
2734
- ...typeof status.expiresAt === "number" ? { expiresAt: status.expiresAt } : {}
2449
+ },
2450
+ output: {
2451
+ schema: imageOutputSchema(),
2452
+ render: (_args, value) => imageContent(value),
2453
+ presentationMeta: (_args, value) => ({
2454
+ kind: "codex-subscription-image",
2455
+ schemaVersion: 1,
2456
+ original: value.original,
2457
+ ...value.requestedModel === void 0 ? {} : { requestedModel: value.requestedModel },
2458
+ ...value.reportedModel === void 0 ? {} : { reportedModel: value.reportedModel },
2459
+ ...value.requestedSize === void 0 ? {} : { requestedSize: value.requestedSize }
2460
+ })
2461
+ },
2462
+ timeoutMs: 300 * 1e3,
2463
+ isConcurrencySafe: () => false,
2464
+ async execute(args, exec) {
2465
+ assertImageOperation(options.getFeatures?.(), args.referenceImages !== void 0);
2466
+ const defaults = readImageDefaults(options.getFeatures?.());
2467
+ args = {
2468
+ ...args,
2469
+ model: args.model ?? defaults.imageModel,
2470
+ quality: args.quality ?? defaults.imageQuality
2735
2471
  };
2736
- }).catch(async (error) => {
2737
- if (controller.signal.aborted) {
2738
- session.view = {
2739
- id,
2740
- provider: "openai-codex",
2741
- method,
2742
- phase: "cancelled",
2743
- authenticated: false
2744
- };
2745
- return;
2746
- }
2472
+ const prompt = nonEmpty$2(args.prompt);
2473
+ if (prompt === void 0) throw new Error("prompt must be a non-empty string");
2474
+ const imageOptions = normalizeImageOptions(args);
2475
+ const { access, accountId } = await readSubscriptionCredentials(options.getAuth, options.readCredential, exec.signal);
2476
+ if (typeof access !== "string" || access.length === 0 || typeof accountId !== "string" || accountId.length === 0) throw new Error("ChatGPT subscription is not signed in");
2477
+ if (!attachments.imageLimits.mediaTypes.includes("image/png")) throw new Error("This DSH installation does not accept PNG image attachments");
2478
+ const maximumBytes = Math.min(attachments.imageLimits.maxImageBytes, attachments.imageLimits.maxMessageImageBytes);
2479
+ const editing = args.referenceImages !== void 0;
2480
+ const sessionMessages = editing && typeof options.getSessionMessages === "function" ? await options.getSessionMessages(exec.agent?.id) : void 0;
2481
+ const images = editing ? await editImages(args.referenceImages, attachments, exec.signal, sessionMessages ?? (options.getSessionMessages ? [] : void 0)) : void 0;
2482
+ let response;
2483
+ assertImageOperation(options.getFeatures?.(), editing);
2747
2484
  try {
2748
- if (label !== void 0) throw error;
2749
- const status = await this.auth.status();
2750
- if (status.authenticated === true) {
2751
- session.view = {
2752
- id,
2753
- provider: "openai-codex",
2754
- method,
2755
- phase: "authenticated",
2756
- authenticated: true,
2757
- ...typeof status.expiresAt === "number" ? { expiresAt: status.expiresAt } : {}
2758
- };
2759
- return;
2760
- }
2761
- } catch {}
2762
- session.view = {
2763
- id,
2764
- provider: "openai-codex",
2765
- method,
2766
- phase: "failed",
2767
- authenticated: false,
2768
- error: "Codex login failed"
2769
- };
2770
- session.hostError = error;
2771
- }).finally(publishReady);
2772
- return ready.promise;
2773
- }
2774
- read(id) {
2775
- const session = this.#sessions.get(id);
2776
- if (session === void 0) throw new Error("unknown Codex login");
2777
- return publicClone(session.view);
2778
- }
2779
- async submit({ id, value }) {
2780
- const session = this.#sessions.get(id);
2781
- if (session === void 0) throw new Error("unknown Codex login");
2782
- if (session.prompt === void 0 || session.view.phase !== "waiting_input") throw new Error("Codex login is not waiting for input");
2783
- if (typeof value !== "string" || value.trim() === "") throw new Error("Codex login input is empty");
2784
- const answer = session.prompt;
2785
- session.prompt = void 0;
2786
- session.view = {
2787
- ...session.view,
2788
- phase: session.view.authUrl === void 0 ? "starting" : "waiting_browser",
2789
- prompt: void 0
2790
- };
2791
- answer.resolve(value);
2792
- return this.read(id);
2793
- }
2794
- async cancel(id) {
2795
- const session = this.#sessions.get(id);
2796
- if (session === void 0) throw new Error("unknown Codex login");
2797
- if (!TERMINAL_PHASES.has(session.view.phase)) {
2798
- session.view = {
2799
- id,
2800
- provider: "openai-codex",
2801
- method: session.view.method,
2802
- phase: "cancelled",
2803
- authenticated: false
2804
- };
2805
- session.controller.abort(/* @__PURE__ */ new Error("Codex login cancelled"));
2806
- }
2807
- return this.read(id);
2808
- }
2809
- async logout(options) {
2810
- if (this.#activeId !== void 0) {
2811
- const active = this.#sessions.get(this.#activeId);
2812
- if (active !== void 0 && !TERMINAL_PHASES.has(active.view.phase)) await this.cancel(active.view.id);
2813
- }
2814
- await this.auth.logout(options);
2815
- return this.accountStatus(options);
2816
- }
2817
- async selectAccount(id) {
2818
- return publicClone(await this.auth.select(id));
2819
- }
2820
- async removeAccount(id) {
2821
- return publicClone(await this.auth.remove(id));
2822
- }
2823
- };
2824
- /** Map the loopback-only DSH Connection channel onto the coordinator. */
2825
- function createCodexRpcHandler(coordinator, options = {}) {
2826
- const openExternal = options.openExternal;
2827
- return async (endpoint, payload, signal) => {
2828
- try {
2829
- signal.throwIfAborted();
2830
- const input = asObject(payload);
2831
- if (endpoint === "status") try {
2832
- return ok(await coordinator.accountStatus({ signal }));
2485
+ response = await fetchImage(editing ? CODEX_IMAGE_EDIT_URL : CODEX_IMAGE_GENERATION_URL, {
2486
+ method: "POST",
2487
+ redirect: "error",
2488
+ headers: {
2489
+ authorization: `Bearer ${access}`,
2490
+ "chatgpt-account-id": accountId,
2491
+ accept: "application/json",
2492
+ "content-type": "application/json",
2493
+ originator: "pi",
2494
+ "x-codex-image-turn-id": String(exec.callId),
2495
+ "user-agent": USER_AGENT
2496
+ },
2497
+ body: JSON.stringify({
2498
+ ...images === void 0 ? {} : { images },
2499
+ prompt,
2500
+ background: imageOptions.background,
2501
+ model: resolveImageModel(args.model),
2502
+ quality: imageOptions.quality,
2503
+ size: imageOptions.size
2504
+ }),
2505
+ signal: exec.signal
2506
+ });
2833
2507
  } catch (error) {
2834
- if (signal.aborted) throw error;
2835
- const [, message] = classifyAccountStatusError(error);
2836
- return accountStatusError(message);
2508
+ if (exec.signal.aborted) throw exec.signal.reason;
2509
+ throw new Error(`Codex image ${editing ? "edit" : "generation"} request failed${requestFailureReason(error)}`, { cause: error });
2837
2510
  }
2838
- if (endpoint === "login/start") {
2839
- const started = await coordinator.start({
2840
- method: input.method,
2841
- label: input.label
2842
- });
2843
- if (input.openExternal !== true) return ok(started);
2844
- const url = started.authUrl ?? started.deviceCode?.verificationUri;
2845
- if (typeof url !== "string" || openExternal === void 0) return ok({
2846
- ...started,
2847
- externalOpened: false
2511
+ if (!response.ok) {
2512
+ if (response.status === 401 || response.status === 403) throw new Error("ChatGPT sign-in needs to be renewed");
2513
+ if (response.status === 429) throw new Error("Codex image generation quota is unavailable");
2514
+ throw new Error(`Codex image ${editing ? "edit" : "generation"} failed (HTTP ${response.status})`);
2515
+ }
2516
+ const metadata = responseMetadata(await readJsonWithin(response, encodedLimit(maximumBytes) + RESPONSE_ENVELOPE_BYTES));
2517
+ const data = decodeCodexPng(metadata.encoded, maximumBytes);
2518
+ const sessionId = exec.agent?.id;
2519
+ if (sessionId === void 0) throw new Error("Codex image generation requires a session-owned tool call");
2520
+ const original = await options.originalImages.save(String(sessionId), data);
2521
+ let ref;
2522
+ try {
2523
+ ref = await attachments.saveImage({
2524
+ data,
2525
+ mediaType: "image/png",
2526
+ name: "codex-generated.png"
2848
2527
  });
2849
- try {
2850
- await openExternal(url);
2851
- return ok({
2852
- ...started,
2853
- externalOpened: true
2854
- });
2855
- } catch {
2856
- return ok({
2857
- ...started,
2858
- externalOpened: false
2859
- });
2860
- }
2528
+ } catch (error) {
2529
+ await options.originalImages.remove(original);
2530
+ throw error;
2861
2531
  }
2862
- if (endpoint === "login/status") return ok(coordinator.read(input.id));
2863
- if (endpoint === "login/submit") return ok(await coordinator.submit({
2864
- id: input.id,
2865
- value: input.value
2532
+ const result = {
2533
+ requestedModel: resolveImageModel(args.model),
2534
+ requestedSize: imageOptions.size,
2535
+ ...metadata.reportedModel === void 0 ? {} : { reportedModel: metadata.reportedModel },
2536
+ image: imageReference(ref),
2537
+ original,
2538
+ localPath: options.originalImages.originalPath(original.assetId),
2539
+ ...metadata.background === void 0 ? {} : { background: metadata.background },
2540
+ ...metadata.quality === void 0 ? {} : { quality: metadata.quality },
2541
+ size: `${original.width}x${original.height}`
2542
+ };
2543
+ if (exec.parent !== void 0) exec.deferContext(createUserMessage({
2544
+ content: imageContent(result),
2545
+ source: { kind: "codex-subscription-image" }
2866
2546
  }));
2867
- if (endpoint === "login/cancel") return ok(await coordinator.cancel(input.id));
2868
- if (endpoint === "logout") return ok(await coordinator.logout({ signal }));
2869
- if (endpoint === "account/select") return ok(await coordinator.selectAccount(input.id));
2870
- if (endpoint === "account/remove") return ok(await coordinator.removeAccount(input.id));
2871
- return badRequest(`unknown Codex auth endpoint: ${endpoint}`);
2872
- } catch (error) {
2873
- if (signal.aborted) throw error;
2874
- const message = error instanceof Error && /^(unknown|unsupported|a Codex|Codex login)/.test(error.message) ? error.message : "Codex request failed";
2875
- return badRequest(message);
2876
- }
2877
- };
2878
- }
2879
- //#endregion
2880
- //#region src/transport-failure.js
2881
- function normalizeTransportEvent(event, signal) {
2882
- if (signal?.aborted || event?.type !== "error" || event.reason !== "error") return event;
2883
- const message = event.error?.errorMessage;
2884
- if (typeof message !== "string" || !/^WebSocket closed (?:1006|1011|1012|1013)(?:\s|$)/.test(message)) return event;
2885
- return {
2886
- ...event,
2887
- error: {
2888
- ...event.error,
2889
- errorMessage: `Network transport failure: ${message}`
2547
+ return result;
2890
2548
  }
2891
- };
2892
- }
2893
- //#endregion
2894
- //#region src/pi-ai-runtime.js
2895
- const FAST_SERVICE_TIER = "priority";
2896
- const ULTRAFAST_SERVICE_TIER = "ultrafast";
2897
- /** Apply the chosen detail to user and tool-result images after the provider assembles the request. */
2898
- function withInputImageDetail(payload, detail) {
2899
- if (!Array.isArray(payload.input)) return payload;
2900
- let changed = false;
2901
- const input = payload.input.map((item) => {
2902
- const field = Array.isArray(item?.content) ? "content" : ["function_call_output", "custom_tool_call_output"].includes(item?.type) && Array.isArray(item.output) ? "output" : void 0;
2903
- if (field === void 0) return item;
2904
- let itemChanged = false;
2905
- const parts = item[field].map((part) => {
2906
- if (part?.type !== "input_image" || part.detail === detail) return part;
2907
- itemChanged = true;
2908
- return {
2909
- ...part,
2910
- detail
2911
- };
2912
- });
2913
- if (!itemChanged) return item;
2914
- changed = true;
2915
- return {
2916
- ...item,
2917
- [field]: parts
2918
- };
2919
2549
  });
2920
- return changed ? {
2921
- ...payload,
2922
- input
2923
- } : payload;
2924
2550
  }
2925
- /**
2926
- * Preserve pi-ai's native Codex OAuth provider while allowing DSH's generic
2927
- * PiAiAdapter to pass the access token resolved by the host credential store.
2928
- *
2929
- * PiAiAdapter owns a request-local Models collection backed by the same DSH
2930
- * credential store as this provider. A pure OAuth provider ignores its
2931
- * `apiKey` request override and otherwise fails before dispatch with "Provider
2932
- * is not configured". This non-interactive bridge teaches that collection how
2933
- * to consume only the already-refreshed token for this request; login, refresh,
2934
- * persistence, headers, transport, and model behavior remain owned by the
2935
- * original provider.
2936
- */
2937
- function openaiCodexSubscriptionProvider({ resolveSpeedMode = () => void 0, resolveOutputVerbosity = () => OUTPUT_VERBOSITY_DEFAULT, resolveInputImageDetail = () => void 0, resolveContextMode = () => void 0, resolveCustomContextWindow = () => void 0, catalog, connection, compaction, runNetwork = (_area, operation) => operation() } = {}) {
2938
- const provider = createOpenAICodexProvider();
2939
- const requestToken = Object.freeze({
2940
- name: "DSH-managed Codex OAuth request token",
2941
- async resolve({ credential }) {
2942
- const token = credential?.type === "api_key" ? credential.key : void 0;
2943
- if (typeof token !== "string" || token.length === 0) return void 0;
2944
- return {
2945
- auth: { apiKey: token },
2946
- source: "DSH-managed OAuth request"
2947
- };
2948
- }
2949
- });
2950
- const modelMetadata = (model) => catalog?.metadata(model?.id);
2951
- const supportsVerbosity = (model) => modelMetadata(model)?.supportVerbosity ?? model?.id !== "gpt-5.3-codex-spark";
2952
- const withPreferences = (model, options = {}) => {
2953
- const metadata = modelMetadata(model);
2954
- const requestedVerbosity = resolveOutputVerbosity();
2955
- const textVerbosity = supportsVerbosity(model) ? requestedVerbosity === "default" ? metadata?.defaultVerbosity ?? "medium" : requestedVerbosity : void 0;
2956
- const speedMode = resolveSpeedMode();
2957
- const serviceTier = speedMode === "ultrafast" && metadata?.supportsUltrafast === true ? ULTRAFAST_SERVICE_TIER : speedMode === "fast" && (metadata?.supportsFast ?? supportsCodexFastMode(model?.id)) ? FAST_SERVICE_TIER : void 0;
2958
- const inputImageDetail = normalizeInputImageDetail(resolveInputImageDetail());
2959
- const onPayload = options.onPayload;
2960
- return {
2961
- ...options,
2962
- ...textVerbosity === void 0 ? {} : { textVerbosity },
2963
- ...serviceTier === void 0 ? {} : { serviceTier },
2964
- async onPayload(payload, requestModel) {
2965
- const preferred = {
2966
- ...payload,
2967
- ...textVerbosity === void 0 ? {} : { text: {
2968
- ...payload.text ?? {},
2969
- verbosity: textVerbosity
2970
- } },
2971
- ...serviceTier === void 0 ? {} : { service_tier: serviceTier }
2972
- };
2973
- const managed = compaction?.preparePayload(preferred, model?.contextWindow) ?? preferred;
2974
- const next = await onPayload?.(managed, requestModel);
2975
- const detailed = inputImageDetail === "auto" ? next ?? managed : withInputImageDetail(next ?? managed, inputImageDetail);
2551
+ //#endregion
2552
+ //#region src/sketch-agent-tool.js
2553
+ const MAX_REFERENCE_BYTES = 14e5;
2554
+ function createSketchAgentTool(bridge, attachments, getSessionMessages = () => []) {
2555
+ return defineTool({
2556
+ name: "codex_sketch",
2557
+ description: "Edit the sketch board in this session using native editable strokes and layers. Use for @sketch requests and explicit follow-up edits to that drawing. The toolbar pen button is for manual drawing; do not require the user to open it. Start with inspect for the runId, documentId, revision and command reference. Draw with the svg op whenever possible: one SVG document per stage (paths, shapes, groups, transforms; see the reference from inspect) is far faster and richer than hand-placed strokes, and still becomes native editable strokes. Draw the shapes, hair, clothing and lettering outlines of every layer with svg; use stroke commands only for small corrections and native text. Apply atomic batches and preview between stages. When the user attached a reference image, call reference with its attachmentId once after inspect to put it on the board as a hidden picture layer. When a reference picture is on the board, keep its layer hidden while drawing (compare shows it anyway), measure its key positions with preview grid, draw big shapes first, and after each stage check preview compare and fix the largest offset before adding detail. For repeated details such as hair strands or lace, generate the SVG with a script instead of placing each one by hand. and call finish to save the finished draft and release the editing lock. save is only a checkpoint. Closing the board does not stop drawing. If the user stops drawing, do not retry. Never generates AI images, sends messages or attaches images automatically. Inspect automatically opens the board in the currently viewed session. Do not ask the user to open it first. If the session is not visible in DSH, ask them to switch to it. On timeout inspect before retrying; reuse the exact requestId only for the same request.",
2558
+ parameters: {
2559
+ action: {
2560
+ type: "string",
2561
+ required: true,
2562
+ enum: [
2563
+ "inspect",
2564
+ "apply",
2565
+ "reference",
2566
+ "preview",
2567
+ "save",
2568
+ "finish"
2569
+ ]
2570
+ },
2571
+ runId: {
2572
+ type: "string",
2573
+ description: "From inspect; required for all other actions. Never reuse a stopped run."
2574
+ },
2575
+ documentId: {
2576
+ type: "string",
2577
+ description: "From inspect; required except for inspect."
2578
+ },
2579
+ revision: {
2580
+ type: "integer",
2581
+ description: "From latest response; required for apply/reference/save/finish."
2582
+ },
2583
+ requestId: {
2584
+ type: "string",
2585
+ description: "Unique id for apply/reference/save/finish; exact retries are deduplicated. After timeout inspect recentRequests before repeating a write."
2586
+ },
2587
+ commands: {
2588
+ oneOf: [sketchCommandArray, { type: "string" }],
2589
+ description: "Prefer a native command array. Legacy JSON string also accepted. Required for apply. Use named objects and update existing IDs; prefer Bezier start + segments (control1/control2/end) for curves, not hundreds of pen points."
2590
+ },
2591
+ name: {
2592
+ type: "string",
2593
+ description: "Draft name for save/finish."
2594
+ },
2595
+ attachmentId: {
2596
+ type: "string",
2597
+ description: "reference only: the attachmentId of an image the user attached to this session (copy it from the image block, never a path or filename). It is added to the board as a hidden picture layer, ready for preview compare."
2598
+ },
2599
+ compare: {
2600
+ type: "boolean",
2601
+ description: "preview only: blend the drawing 50/50 with the reference picture layer on the board, so offsets show as ghosting. Needs a picture layer; if there is none, ask the user to add the reference with the pictures button."
2602
+ },
2603
+ grid: {
2604
+ type: "boolean",
2605
+ description: "preview only: overlay a labelled grid every 100 canvas pixels, to read positions and sizes instead of guessing them."
2606
+ },
2607
+ offset: {
2608
+ type: "integer",
2609
+ description: "inspect only: object list offset, default 0. Follow nextOffset for further pages."
2610
+ },
2611
+ objectId: {
2612
+ type: "string",
2613
+ description: "inspect only: return full editable geometry for this object, in layer (defaults to active layer)."
2614
+ },
2615
+ layer: {
2616
+ type: "integer",
2617
+ description: "inspect only: layer containing objectId."
2618
+ }
2619
+ },
2620
+ timeoutMs: 25e3,
2621
+ isConcurrencySafe: () => false,
2622
+ async execute(args, exec) {
2623
+ const sessionId = exec.agent?.id;
2624
+ if (typeof sessionId !== "string") throw Error("A session-owned sketch call is required");
2625
+ const request = { ...args };
2626
+ delete request.attachmentId;
2627
+ if (args.action === "apply") try {
2628
+ request.commands = typeof args.commands === "string" ? JSON.parse(args.commands) : args.commands;
2629
+ if (!Array.isArray(request.commands)) throw Error();
2630
+ } catch {
2631
+ throw Error("commands must be a native array or JSON array string");
2632
+ }
2633
+ if (args.action === "reference") {
2634
+ const image = await readSessionImage(args.attachmentId, attachments, exec.signal, await getSessionMessages(sessionId));
2635
+ if (image.data.byteLength > MAX_REFERENCE_BYTES) throw Error("The reference image is too large to send to the board; ask the user to add it with the pictures button on the board");
2636
+ request.image = `data:${image.mediaType};base64,${Buffer.from(image.data).toString("base64")}`;
2637
+ request.name = image.name ?? "reference";
2638
+ }
2639
+ const value = await bridge.request(sessionId, request, exec.signal);
2640
+ if (value.png) {
2641
+ if (!/^data:image\/png;base64,/.test(value.png) || value.png.length > 8 * 1024 * 1024) throw Error("Invalid sketch preview");
2642
+ const image = await attachments.saveImage({
2643
+ data: new Uint8Array(Buffer.from(value.png.split(",")[1], "base64")),
2644
+ mediaType: "image/png",
2645
+ name: "sketch-preview.png"
2646
+ });
2647
+ const { png, ...snapshot } = value;
2976
2648
  return {
2977
- ...detailed,
2978
- ...textVerbosity === void 0 ? {} : { text: {
2979
- ...detailed.text ?? {},
2980
- verbosity: textVerbosity
2981
- } },
2982
- ...serviceTier === void 0 ? {} : { service_tier: serviceTier }
2649
+ ...snapshot,
2650
+ image
2983
2651
  };
2984
2652
  }
2985
- };
2986
- };
2987
- const getModels = () => (catalog?.getModels() ?? provider.getModels()).map((model) => {
2988
- const maximum = modelContextMaximum(model);
2989
- const mode = resolveContextMode();
2990
- if (model.id === "gpt-5.3-codex-spark" || !["extended", "custom"].includes(mode)) return model;
2991
- if (mode === "extended") {
2992
- const contextWindow = maximum;
2993
- return {
2994
- ...model,
2995
- contextWindow
2996
- };
2653
+ return value;
2654
+ },
2655
+ output: {
2656
+ schema: {
2657
+ type: "object",
2658
+ additionalProperties: true
2659
+ },
2660
+ render: (_args, value) => [{
2661
+ type: "text",
2662
+ text: JSON.stringify({
2663
+ ...value,
2664
+ image: void 0
2665
+ })
2666
+ }, ...value.image ? [{
2667
+ type: "image",
2668
+ attachment: value.image
2669
+ }] : []]
2997
2670
  }
2998
- const requested = clampModelContext(resolveCustomContextWindow(customContextModelKey(model.id)), maximum, model.contextWindow);
2999
- return {
3000
- ...model,
3001
- contextWindow: requested
3002
- };
3003
2671
  });
3004
- const networkIterable = (factory, options) => {
3005
- let iterator;
3006
- let prepared;
3007
- const step = async (method, value) => {
3008
- const request = await (prepared ??= connection?.prepare(options) ?? Promise.resolve({ options }));
3009
- const result = await runNetwork("model", () => {
3010
- iterator ??= factory(compaction?.requestOptions(request.options) ?? request.options)[Symbol.asyncIterator]();
3011
- return iterator[method]?.(value) ?? (method === "throw" ? Promise.reject(value) : Promise.resolve({
3012
- done: true,
3013
- value
3014
- }));
3015
- }, compaction?.networkOptions(request.network) ?? request.network);
3016
- return result.done ? result : {
3017
- ...result,
3018
- value: normalizeTransportEvent(result.value, request.options?.signal)
3019
- };
3020
- };
3021
- return {
3022
- [Symbol.asyncIterator]() {
3023
- return this;
3024
- },
3025
- next: (value) => step("next", value),
3026
- return: (value) => iterator ? step("return", value) : Promise.resolve({
3027
- done: true,
3028
- value
3029
- }),
3030
- throw: (error) => iterator ? step("throw", error) : Promise.reject(error)
3031
- };
3032
- };
3033
- return Object.freeze({
3034
- ...provider,
3035
- auth: Object.freeze({
3036
- ...provider.auth,
3037
- apiKey: requestToken
3038
- }),
3039
- getModels,
3040
- stream: (model, context, options) => networkIterable((prepared) => provider.stream(model, context, prepared), withPreferences(model, options)),
3041
- streamSimple: (model, context, options) => networkIterable((prepared) => provider.streamSimple(model, context, prepared), withPreferences(model, options))
2672
+ }
2673
+ //#endregion
2674
+ //#region src/sketch-codec-route.js
2675
+ function registerSketchCodec(connection) {
2676
+ let source;
2677
+ return connection.fetch.register({
2678
+ path: "/api/codex-subscription/sketch-psd-worker",
2679
+ methods: ["GET"],
2680
+ requestBody: "buffered",
2681
+ async fetch() {
2682
+ source ??= await readFile(new URL("./sketch-psd-worker.js", import.meta.url));
2683
+ return new Response(source, { headers: {
2684
+ "content-type": "text/javascript; charset=utf-8",
2685
+ "cache-control": "no-store"
2686
+ } });
2687
+ }
3042
2688
  });
3043
2689
  }
3044
- Object.freeze([
3045
- "0.82.1",
3046
- "0.85.1",
3047
- "0.87.1"
3048
- ]);
3049
2690
  //#endregion
3050
- //#region src/subscription-credentials.js
3051
- async function readSubscriptionCredentials(getAuth, readCredential, signal) {
3052
- for (let attempt = 0; attempt < 2; attempt++) {
3053
- const auth = await getAuth({ signal });
3054
- signal?.throwIfAborted();
3055
- if (typeof auth?.auth?.apiKey !== "string" || !auth.auth.apiKey) break;
3056
- const credential = await readCredential({ signal });
3057
- signal?.throwIfAborted();
3058
- if (credential?.type !== "oauth") break;
3059
- if (credential.access === auth.auth.apiKey) return {
3060
- access: credential.access,
3061
- accountId: credential.accountId
3062
- };
2691
+ //#region src/account-vault.js
2692
+ const VERSION = 1;
2693
+ const DEFAULT_LABEL = "Account 1";
2694
+ const clone$1 = (value) => value === void 0 ? void 0 : structuredClone(value);
2695
+ const EMAIL_MAX_LENGTH = 254;
2696
+ const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/u;
2697
+ /** Keep only a bounded, display-safe email address from a trusted OAuth result. */
2698
+ function normalizeAccountEmail(value) {
2699
+ if (typeof value !== "string") return void 0;
2700
+ const email = value.trim();
2701
+ return email.length > 0 && email.length <= EMAIL_MAX_LENGTH && EMAIL_PATTERN.test(email) ? email : void 0;
2702
+ }
2703
+ function decodeJwtPayload(access) {
2704
+ if (typeof access !== "string") return void 0;
2705
+ const encoded = access.split(".")[1];
2706
+ if (typeof encoded !== "string" || encoded.length === 0) return void 0;
2707
+ try {
2708
+ return JSON.parse(Buffer.from(encoded, "base64url").toString("utf8"));
2709
+ } catch {
2710
+ return;
3063
2711
  }
3064
- return {
3065
- access: void 0,
3066
- accountId: void 0
3067
- };
3068
2712
  }
3069
- //#endregion
3070
- //#region src/version.js
3071
- const PACKAGE_VERSION = "2.4.1";
3072
- const USER_AGENT = `dsh-codex-subscription/${PACKAGE_VERSION}`;
3073
- //#endregion
3074
- //#region src/model-catalog.js
3075
- const CODEX_MODELS_URL = `https://chatgpt.com/backend-api/codex/models?client_version=${encodeURIComponent(PACKAGE_VERSION)}`;
3076
- const LEVELS = [
3077
- "off",
3078
- "minimal",
3079
- "low",
3080
- "medium",
3081
- "high",
3082
- "xhigh",
3083
- "max"
3084
- ];
3085
- const DEFAULT_REFRESH_TIMEOUT_MS = 1e4;
3086
- const record$4 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
3087
- const nonEmpty$2 = (value) => typeof value === "string" && value.trim().length > 0 ? value.trim() : void 0;
3088
- const positiveInteger$1 = (value) => Number.isSafeInteger(value) && value > 0 ? value : void 0;
3089
- function upgradeModelId(value) {
3090
- return (typeof value.upgrade === "string" ? nonEmpty$2(value.upgrade) : record$4(value.upgrade) ? nonEmpty$2(value.upgrade.model) : void 0) ?? (record$4(value.upgrade_info) ? nonEmpty$2(value.upgrade_info.model) : void 0) ?? (record$4(value.upgradeInfo) ? nonEmpty$2(value.upgradeInfo.model) : void 0);
2713
+ function emailFromAccessToken(access) {
2714
+ const payload = decodeJwtPayload(access);
2715
+ return normalizeAccountEmail(payload?.["https://api.openai.com/profile"]?.email ?? payload?.email);
3091
2716
  }
3092
- function retirementMillis(value) {
3093
- for (const source of [
3094
- value,
3095
- value.upgrade,
3096
- value.upgrade_info,
3097
- value.upgradeInfo
3098
- ]) {
3099
- if (!record$4(source)) continue;
3100
- for (const raw of [source.retirement_at, source.retirementAt]) {
3101
- const seconds = positiveInteger$1(raw);
3102
- if (seconds !== void 0 && Number.isSafeInteger(seconds * 1e3)) return seconds * 1e3;
3103
- if (typeof raw === "string" && /^\d{4}-\d{2}-\d{2}T[\d:.]+(?:Z|[+-]\d{2}:\d{2})$/u.test(raw)) {
3104
- const parsed = Date.parse(raw);
3105
- if (Number.isFinite(parsed) && parsed > 0) return parsed;
3106
- }
3107
- }
2717
+ /** Normalize the one non-secret account attribute that may cross the UI boundary. */
2718
+ function sanitizeOAuthCredential(value) {
2719
+ const credential = assertOAuthCredential$1(value);
2720
+ const email = emailFromAccessToken(credential.access) ?? normalizeAccountEmail(credential.email);
2721
+ if (email === void 0) {
2722
+ delete credential.email;
2723
+ return credential;
3108
2724
  }
3109
- }
3110
- function retirementNotice(value, now = Date.now(), targetDisplayName) {
3111
- if (!record$4(value) || !Number.isFinite(now)) return void 0;
3112
- const at = retirementMillis(value);
3113
- if (at === void 0 || at <= now) return void 0;
3114
- const date = new Date(at);
3115
- if (!Number.isFinite(date.getTime())) return void 0;
3116
- const dateText = `${String(date.getUTCFullYear()).padStart(4, "0")}-${String(date.getUTCMonth() + 1).padStart(2, "0")}-${String(date.getUTCDate()).padStart(2, "0")}`;
3117
- const upgradeTo = upgradeModelId(value);
3118
- const upgradeName = upgradeTo === void 0 ? void 0 : nonEmpty$2(targetDisplayName) ?? upgradeTo;
3119
2725
  return {
3120
- text: `Retires on ${dateText} (UTC).${upgradeName === void 0 ? "" : ` Switch to ${upgradeName} to keep working.`}`,
3121
- at,
3122
- upgradeTo,
3123
- dateText
2726
+ ...credential,
2727
+ email
3124
2728
  };
3125
2729
  }
3126
- function reasoningMap(levels) {
3127
- const supported = new Set((Array.isArray(levels) ? levels : []).map((level) => nonEmpty$2(record$4(level) ? level.effort : void 0)).filter(Boolean));
3128
- const map = Object.fromEntries(LEVELS.map((level) => [level, null]));
3129
- if (supported.has("none")) map.off = "none";
3130
- for (const level of LEVELS.slice(1)) if (supported.has(level)) map[level] = level;
3131
- return map;
2730
+ function assertOAuthCredential$1(value) {
2731
+ if (value === null || typeof value !== "object" || value.type !== "oauth" || typeof value.access !== "string" || value.access.length === 0 || typeof value.refresh !== "string" || value.refresh.length === 0 || typeof value.expires !== "number" || !Number.isFinite(value.expires)) throw new Error("Codex account vault received a malformed OAuth credential");
2732
+ return clone$1(value);
3132
2733
  }
3133
- const capabilityNames = (values) => [...new Set(values.filter((value) => typeof value === "string" && /^[a-z][a-z0-9_-]{0,31}$/u.test(value)))].sort().slice(0, 16);
3134
- const speedTierIds = (value) => [...Array.isArray(value.additional_speed_tiers) ? value.additional_speed_tiers : [], ...Array.isArray(value.service_tiers) ? value.service_tiers.map((tier) => tier?.id) : []];
3135
- function unsupportedCapabilities(value) {
3136
- const reasoning = capabilityNames((value.supported_reasoning_levels ?? []).map((item) => item?.effort)).filter((level) => !["none", ...LEVELS.slice(1)].includes(level));
3137
- const inputs = capabilityNames(Array.isArray(value.input_modalities) ? value.input_modalities : []).filter((input) => !["text", "image"].includes(input));
3138
- const speeds = capabilityNames([...Array.isArray(value.additional_speed_tiers) ? value.additional_speed_tiers : [], ...Array.isArray(value.service_tiers) ? value.service_tiers.map((tier) => tier?.id) : []]).filter((tier) => ![
3139
- "auto",
3140
- "default",
3141
- "standard",
3142
- "fast",
3143
- "priority",
3144
- "ultrafast"
3145
- ].includes(tier));
3146
- return {
3147
- ...reasoning.length ? { reasoning } : {},
3148
- ...inputs.length ? { inputs } : {},
3149
- ...speeds.length ? { speeds } : {}
3150
- };
2734
+ function parseOAuthCredential$1(value) {
2735
+ try {
2736
+ return assertOAuthCredential$1(JSON.parse(value));
2737
+ } catch (error) {
2738
+ if (error?.message === "Codex account vault received a malformed OAuth credential") throw error;
2739
+ throw new Error("Codex account vault contains malformed OAuth JSON", { cause: error });
2740
+ }
3151
2741
  }
3152
- function visibleModel(value, displayNames, now) {
3153
- if (!record$4(value)) return void 0;
3154
- const id = nonEmpty$2(value.slug);
3155
- const reserve = id === "gpt-reserve" && ["list", "hide"].includes(value.visibility);
3156
- if (id === void 0 || value.visibility !== "list" && !reserve) return void 0;
3157
- const supported = Array.isArray(value.supported_reasoning_levels) ? value.supported_reasoning_levels : [];
3158
- const input = Array.isArray(value.input_modalities) ? value.input_modalities.filter((item) => ["text", "image"].includes(item)) : ["text", "image"];
3159
- const unsupported = unsupportedCapabilities({
3160
- ...value,
3161
- supported_reasoning_levels: supported
2742
+ function normalizeLabel(value) {
2743
+ if (typeof value !== "string") throw new Error("Codex account label must be text");
2744
+ const label = value.trim().replace(/\s+/gu, " ");
2745
+ if (label.length === 0 || label.length > 48) throw new Error("Codex account label must contain 1 to 48 characters");
2746
+ return label;
2747
+ }
2748
+ function assertVaultRecord(record) {
2749
+ if (record?.kind !== "grant" || record.payload?.version !== VERSION || typeof record.payload.activeId !== "string" || !Array.isArray(record.payload.accounts) || record.payload.accounts.length === 0) throw new Error("Codex account vault contains a malformed grant record");
2750
+ const ids = /* @__PURE__ */ new Set();
2751
+ const accounts = record.payload.accounts.map((account) => {
2752
+ if (account === null || typeof account !== "object" || typeof account.id !== "string" || account.id.length === 0 || ids.has(account.id)) throw new Error("Codex account vault contains a malformed account id");
2753
+ ids.add(account.id);
2754
+ return {
2755
+ id: account.id,
2756
+ label: normalizeLabel(account.label),
2757
+ credential: sanitizeOAuthCredential(account.credential)
2758
+ };
3162
2759
  });
3163
- const upgradeTo = upgradeModelId(value);
3164
- const retirement = retirementNotice(value, now, displayNames.get(upgradeTo));
3165
- const notice = retirement?.text;
3166
- const description = nonEmpty$2(value.description);
2760
+ if (!ids.has(record.payload.activeId)) throw new Error("Codex account vault active account is missing");
2761
+ const legacyAccountId = record.payload.legacyAccountId;
2762
+ if (legacyAccountId !== void 0 && !ids.has(legacyAccountId)) throw new Error("Codex account vault legacy account is missing");
3167
2763
  return {
3168
- ...Object.keys(unsupported).length ? { unsupported } : {},
3169
- id,
3170
- name: `${reserve ? "GPT-Reserve (Experimental)" : nonEmpty$2(value.display_name) ?? id}${retirement === void 0 ? "" : ` (retires ${retirement.dateText})`}`,
3171
- description: notice === void 0 ? description : description === void 0 ? notice : `${description}\n${notice}`,
3172
- ...retirement === void 0 ? {} : { retirement: {
3173
- at: retirement.at,
3174
- upgradeTo: retirement.upgradeTo
3175
- } },
3176
- priority: Number.isFinite(value.priority) ? value.priority : 0,
3177
- input: input.length > 0 ? input : ["text"],
3178
- contextWindow: positiveInteger$1(value.context_window) ?? positiveInteger$1(value.max_context_window),
3179
- ...positiveInteger$1(value.max_context_window) === void 0 ? {} : { maxContextWindow: value.max_context_window },
3180
- reasoning: supported.length > 0,
3181
- thinkingLevelMap: reasoningMap(supported),
3182
- supportVerbosity: value.support_verbosity === true,
3183
- defaultVerbosity: [
3184
- "low",
3185
- "medium",
3186
- "high"
3187
- ].includes(value.default_verbosity) ? value.default_verbosity : void 0,
3188
- supportsFast: speedTierIds(value).some((tier) => tier === "fast" || tier === "priority"),
3189
- supportsUltrafast: speedTierIds(value).includes("ultrafast")
2764
+ version: VERSION,
2765
+ activeId: record.payload.activeId,
2766
+ legacyAccountId,
2767
+ accounts
3190
2768
  };
3191
2769
  }
3192
- function parseOfficialModelCatalog(value, now = Date.now()) {
3193
- if (!record$4(value) || !Array.isArray(value.models)) throw new Error("Codex returned a malformed model catalog");
3194
- const displayNames = /* @__PURE__ */ new Map();
3195
- for (const model of value.models) {
3196
- if (!record$4(model)) continue;
3197
- const id = nonEmpty$2(model.slug);
3198
- const name = nonEmpty$2(model.display_name);
3199
- if (id !== void 0 && name !== void 0 && !displayNames.has(id)) displayNames.set(id, name);
2770
+ const grant = (payload) => ({
2771
+ kind: "grant",
2772
+ payload
2773
+ });
2774
+ var PendingOAuthCredentialStore = class {
2775
+ #credential;
2776
+ async read(providerId) {
2777
+ if (providerId !== "openai-codex") throw new Error("Pending Codex login received an unknown provider");
2778
+ return clone$1(this.#credential);
3200
2779
  }
3201
- const seen = /* @__PURE__ */ new Set();
3202
- return value.models.map((model) => visibleModel(model, displayNames, now)).filter((model) => model !== void 0 && !seen.has(model.id) && seen.add(model.id)).sort((left, right) => Number(left.id === "gpt-reserve") - Number(right.id === "gpt-reserve") || right.priority - left.priority);
3203
- }
3204
- function mergeModel(baseModels, remote) {
3205
- const base = baseModels.find((model) => model.id === remote.id) ?? baseModels.find((model) => model.id !== "gpt-5.3-codex-spark") ?? baseModels[0];
3206
- if (base === void 0) return void 0;
3207
- return {
3208
- ...base,
3209
- id: remote.id,
3210
- name: remote.name,
3211
- ...remote.retirement === void 0 || remote.description === void 0 ? {} : { description: remote.description },
3212
- input: remote.input,
3213
- reasoning: remote.reasoning,
3214
- thinkingLevelMap: remote.thinkingLevelMap,
3215
- ...remote.contextWindow === void 0 ? {} : { contextWindow: remote.contextWindow },
3216
- ...remote.maxContextWindow === void 0 ? {} : { maxContextWindow: remote.maxContextWindow },
3217
- ...base.id === remote.id ? {} : { cost: {
3218
- input: 0,
3219
- output: 0,
3220
- cacheRead: 0,
3221
- cacheWrite: 0
3222
- } }
3223
- };
3224
- }
3225
- function createOfficialModelCatalog(options = {}) {
3226
- const fetchCatalog = options.fetch ?? fetch;
3227
- const getNow = options.now ?? Date.now;
3228
- const scheduleTimeout = options.setTimeout ?? setTimeout;
3229
- const cancelTimeout = options.clearTimeout ?? clearTimeout;
3230
- const timeoutMs = Number.isFinite(options.timeoutMs) && options.timeoutMs > 0 ? options.timeoutMs : DEFAULT_REFRESH_TIMEOUT_MS;
3231
- let models;
3232
- let metadata = /* @__PURE__ */ new Map();
3233
- let etag;
3234
- let rawCatalog;
3235
- let revision = 0;
3236
- let refreshing;
3237
- let generation = 0;
3238
- let refreshStatus = "idle";
3239
- const refresh = ({ signal } = {}) => {
3240
- if (signal?.aborted) return Promise.reject(signal.reason ?? /* @__PURE__ */ new Error("Codex model catalog refresh aborted"));
3241
- if (refreshing?.generation === generation) return refreshing.promise;
3242
- const currentGeneration = generation;
3243
- refreshStatus = "refreshing";
3244
- let outcome = "idle";
3245
- const controller = new AbortController();
3246
- const abort = () => {
3247
- if (!controller.signal.aborted) controller.abort(signal?.reason ?? /* @__PURE__ */ new Error("Codex model catalog refresh aborted"));
3248
- };
3249
- signal?.addEventListener("abort", abort, { once: true });
3250
- const requestSignal = controller.signal;
3251
- let timer;
3252
- const timeoutError = /* @__PURE__ */ new Error("Codex model catalog refresh timed out");
3253
- const work = (async () => {
3254
- const { access, accountId } = await readSubscriptionCredentials(options.getAuth, options.readCredential, requestSignal);
3255
- if (currentGeneration !== generation || requestSignal.aborted) return false;
3256
- if (typeof access !== "string" || access.length === 0 || typeof accountId !== "string" || accountId.length === 0) return false;
3257
- const headers = {
3258
- authorization: `Bearer ${access}`,
3259
- "chatgpt-account-id": accountId,
3260
- accept: "application/json",
3261
- originator: "pi",
3262
- "user-agent": USER_AGENT,
3263
- ...etag === void 0 ? {} : { "if-none-match": etag }
3264
- };
3265
- const response = await fetchCatalog(CODEX_MODELS_URL, {
3266
- method: "GET",
3267
- redirect: "error",
3268
- headers,
3269
- signal: requestSignal
3270
- });
3271
- if (currentGeneration !== generation || requestSignal.aborted) return false;
3272
- if (response.status === 304) {
3273
- outcome = "ok";
3274
- if (rawCatalog === void 0) return false;
3275
- const remote = parseOfficialModelCatalog(rawCatalog, getNow());
3276
- const next = remote.map((model) => mergeModel(options.baseModels(), model)).filter(Boolean);
3277
- if (next.length === 0 || JSON.stringify(next) === JSON.stringify(models)) return false;
3278
- models = next;
3279
- metadata = new Map(remote.map((model) => [model.id, model]));
3280
- revision += 1;
3281
- options.onUpdated?.();
3282
- return true;
3283
- }
3284
- if (!response.ok) throw new Error(`Codex model catalog failed (HTTP ${response.status})`);
3285
- const raw = await response.json();
3286
- const remote = parseOfficialModelCatalog(raw, getNow());
3287
- if (currentGeneration !== generation || requestSignal.aborted) return false;
3288
- if (remote.length === 0) throw new Error("Codex returned an empty model catalog");
3289
- const baseModels = options.baseModels();
3290
- const next = remote.map((model) => mergeModel(baseModels, model)).filter(Boolean);
3291
- if (next.length === 0) throw new Error("Codex model catalog has no compatible models");
3292
- if (currentGeneration !== generation || requestSignal.aborted) return false;
3293
- models = next;
3294
- rawCatalog = raw;
3295
- metadata = new Map(remote.map((model) => [model.id, model]));
3296
- etag = nonEmpty$2(response.headers.get("etag")) ?? etag;
3297
- revision += 1;
3298
- outcome = "ok";
3299
- options.onUpdated?.();
3300
- return true;
3301
- })();
3302
- let rejectAborted;
3303
- const abortPromise = new Promise((_, reject) => {
3304
- rejectAborted = () => reject(requestSignal.reason ?? /* @__PURE__ */ new Error("Codex model catalog refresh aborted"));
3305
- if (requestSignal.aborted) rejectAborted();
3306
- else requestSignal.addEventListener("abort", rejectAborted, { once: true });
3307
- });
3308
- timer = scheduleTimeout(() => controller.abort(timeoutError), timeoutMs);
3309
- timer.unref?.();
3310
- const promise = Promise.race([work, abortPromise]).catch((error) => {
3311
- outcome = "failed";
3312
- throw error;
3313
- }).finally(() => {
3314
- cancelTimeout(timer);
3315
- signal?.removeEventListener("abort", abort);
3316
- requestSignal.removeEventListener("abort", rejectAborted);
3317
- if (refreshing?.promise === promise) {
3318
- refreshing = void 0;
3319
- refreshStatus = outcome;
3320
- }
3321
- });
3322
- refreshing = {
3323
- generation: currentGeneration,
3324
- promise,
3325
- cancel: () => controller.abort()
3326
- };
3327
- return promise;
3328
- };
3329
- return Object.freeze({
3330
- refresh,
3331
- getModels: () => models ?? [],
3332
- metadata: (modelId) => metadata.get(modelId),
3333
- revision: () => revision,
3334
- capabilityGaps: () => [...metadata.values()].filter((model) => model.unsupported && /^[a-z][a-z0-9._-]{0,79}$/u.test(model.id)).slice(0, 20).map((model) => ({
3335
- model: model.id,
3336
- ...structuredClone(model.unsupported)
3337
- })),
3338
- status: () => ({
3339
- source: models === void 0 ? "unavailable" : "online",
3340
- refresh: refreshStatus
3341
- }),
3342
- clear() {
3343
- generation += 1;
3344
- const flight = refreshing;
3345
- refreshing = void 0;
3346
- flight?.cancel();
3347
- models = void 0;
3348
- rawCatalog = void 0;
3349
- metadata = /* @__PURE__ */ new Map();
3350
- etag = void 0;
3351
- refreshStatus = "idle";
3352
- revision += 1;
3353
- options.onUpdated?.();
3354
- }
3355
- });
3356
- }
3357
- //#endregion
3358
- //#region src/codex-search.js
3359
- const CODEX_SEARCH_PROVIDER_ID = "codex-subscription";
3360
- const CODEX_AUTO_SEARCH_PROVIDER_ID = "codex-subscription-auto";
3361
- const CODEX_SEARCH_URL = "https://chatgpt.com/backend-api/codex/alpha/search";
3362
- const DEFAULT_MODEL = "gpt-5.6-luna";
3363
- const MAX_OUTPUT_TOKENS = 4096;
3364
- const MAX_SOURCE_DATE = 64;
3365
- const record$3 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
3366
- const nonEmpty$1 = (value) => typeof value === "string" && value.length > 0 ? value : void 0;
3367
- const displayText = (value) => {
3368
- const text = nonEmpty$1(value)?.replace(/\s+/gu, " ").trim();
3369
- if (text === void 0 || text.length === 0) return void 0;
3370
- return text;
3371
- };
3372
- const boundedDisplayText = (value, maximum) => {
3373
- const text = displayText(value);
3374
- if (text === void 0 || text.length <= maximum) return text;
3375
- return `${text.slice(0, maximum - 1)}…`;
3376
- };
3377
- function sourceOf(value) {
3378
- if (!record$3(value)) return void 0;
3379
- const url = nonEmpty$1(value.url);
3380
- if (url === void 0) return void 0;
3381
- let parsed;
3382
- try {
3383
- parsed = new URL(url);
3384
- } catch {
3385
- return;
2780
+ async list() {
2781
+ return this.#credential === void 0 ? [] : [{
2782
+ providerId: "openai-codex",
2783
+ type: "oauth"
2784
+ }];
3386
2785
  }
3387
- if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return void 0;
3388
- const title = displayText(value.title) ?? parsed.hostname;
3389
- const snippet = displayText(value.snippet);
3390
- const publishedAt = boundedDisplayText(value.published_at, MAX_SOURCE_DATE) ?? boundedDisplayText(value.publishedAt, MAX_SOURCE_DATE);
3391
- return {
3392
- url,
3393
- title,
3394
- ...snippet === void 0 ? {} : { snippet },
3395
- ...publishedAt === void 0 ? {} : { publishedAt }
3396
- };
3397
- }
3398
- function parseSearchResponse(value) {
3399
- if (!record$3(value) || !Array.isArray(value.results)) throw new Error("Codex returned a malformed search response");
3400
- const seen = /* @__PURE__ */ new Set();
3401
- const sources = [];
3402
- for (const result of value.results ?? []) {
3403
- const source = sourceOf(result);
3404
- if (source === void 0 || seen.has(source.url)) continue;
3405
- seen.add(source.url);
3406
- sources.push(source);
2786
+ async modify(providerId, update) {
2787
+ if (providerId !== "openai-codex") throw new Error("Pending Codex login received an unknown provider");
2788
+ const next = await update(clone$1(this.#credential));
2789
+ if (next !== void 0) this.#credential = sanitizeOAuthCredential(next);
2790
+ return clone$1(this.#credential);
3407
2791
  }
3408
- return {
3409
- sources,
3410
- truncated: false
3411
- };
3412
- }
3413
- /** Create the DSH web provider backed only by the ChatGPT subscription search endpoint. */
3414
- function createCodexSearchProvider(options) {
3415
- const fetchSearch = options.fetch ?? fetch;
3416
- return Object.freeze({
3417
- id: CODEX_SEARCH_PROVIDER_ID,
3418
- available: () => true,
3419
- async search(request, signal) {
3420
- signal?.throwIfAborted();
3421
- const preferences = readCapabilitySettings(options.resolvePreferences?.());
3422
- if (preferences.searchMode === "disabled") throw new WebError("Codex search is disabled in subscription settings", "WEB_PROVIDER_UNAVAILABLE");
3423
- const { access, accountId } = await readSubscriptionCredentials(options.getAuth, options.readCredential, signal);
3424
- if (typeof access !== "string" || access.length === 0 || typeof accountId !== "string" || accountId.length === 0) throw new WebError("ChatGPT subscription is not signed in", "WEB_PROVIDER_CREDENTIAL_MISSING");
3425
- const model = nonEmpty$1(options.resolveModel?.()) ?? DEFAULT_MODEL;
3426
- const id = nonEmpty$1(options.resolveSessionId?.()) ?? randomUUID();
3427
- let response;
3428
- try {
3429
- response = await fetchSearch(CODEX_SEARCH_URL, {
3430
- method: "POST",
3431
- redirect: "error",
3432
- headers: {
3433
- authorization: `Bearer ${access}`,
3434
- "chatgpt-account-id": accountId,
3435
- accept: "application/json",
3436
- "content-type": "application/json",
3437
- originator: "pi",
3438
- "user-agent": USER_AGENT
3439
- },
3440
- body: JSON.stringify({
3441
- id,
3442
- model,
3443
- input: request.query,
3444
- commands: {
3445
- search_query: [{ q: request.query }],
3446
- response_length: "short"
3447
- },
3448
- settings: {
3449
- allowed_callers: ["direct"],
3450
- external_web_access: preferences.searchMode === "live"
3451
- },
3452
- max_output_tokens: MAX_OUTPUT_TOKENS
3453
- }),
3454
- signal
3455
- });
3456
- } catch (error) {
3457
- if (signal?.aborted || error?.name === "AbortError") throw new WebError("Codex search aborted", "WEB_ABORTED", { cause: error });
3458
- throw new WebError("Codex search request failed", "WEB_PROVIDER_ERROR", { cause: error });
3459
- }
3460
- if (!response.ok) throw response.status === 401 || response.status === 403 ? new WebError("ChatGPT sign-in needs to be renewed", "WEB_PROVIDER_CREDENTIAL_MISSING") : new WebError(`Codex search request failed (HTTP ${response.status})`, "WEB_PROVIDER_ERROR");
3461
- let value;
3462
- try {
3463
- value = await response.json();
3464
- } catch (error) {
3465
- throw new WebError("Codex returned an unreadable search response", "WEB_PROVIDER_ERROR", { cause: error });
3466
- }
3467
- try {
3468
- const result = parseSearchResponse(value);
3469
- if (preferences.searchDomains.length > 0) result.sources = result.sources.filter((source) => {
3470
- const hostname = new URL(source.url).hostname.toLowerCase();
3471
- return preferences.searchDomains.some((domain) => hostname === domain || hostname.endsWith(`.${domain}`));
3472
- });
3473
- return result;
3474
- } catch (error) {
3475
- throw new WebError("Codex returned a malformed search response", "WEB_PROVIDER_ERROR", { cause: error });
3476
- }
3477
- }
3478
- });
3479
- }
3480
- /** Route each request by its initiating model without changing the user's explicit overrides. */
3481
- function createCodexAutoSearchProvider(options) {
3482
- return Object.freeze({
3483
- id: CODEX_AUTO_SEARCH_PROVIDER_ID,
3484
- available: () => true,
3485
- async search(request, signal) {
3486
- if (options.resolveModelProvider?.() === "openai-codex") return options.codex.search(request, signal);
3487
- const provider = options.resolveDshProvider?.();
3488
- if (provider === void 0 || provider.id === "codex-subscription-auto" || provider.id === "codex-subscription" || provider.available() !== true) throw new WebError("DSH default search is unavailable", "WEB_PROVIDER_UNAVAILABLE");
3489
- return provider.search(request, signal);
3490
- }
3491
- });
3492
- }
3493
- const ORIGINAL_IMAGE_CHUNK_BYTES = 4 * 1024 * 1024;
3494
- const ORIGINAL_IMAGE_ID_PATTERN = /^img_[0-9a-f]{32}$/u;
3495
- const positiveInteger = (value) => Number.isSafeInteger(value) && value > 0;
3496
- function decodeOriginalImageRef(value) {
3497
- if (value === null || typeof value !== "object" || Array.isArray(value) || typeof value.assetId !== "string" || !ORIGINAL_IMAGE_ID_PATTERN.test(value.assetId) || value.mediaType !== "image/png" || !positiveInteger(value.bytes) || value.bytes > 48 * 1024 * 1024 || !positiveInteger(value.width) || !positiveInteger(value.height) || typeof value.name !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u.test(value.name) || typeof value.sha256 !== "string" || !/^[0-9a-f]{64}$/u.test(value.sha256)) return void 0;
3498
- return {
3499
- assetId: value.assetId,
3500
- mediaType: value.mediaType,
3501
- bytes: value.bytes,
3502
- width: value.width,
3503
- height: value.height,
3504
- name: value.name,
3505
- sha256: value.sha256
3506
- };
3507
- }
3508
- function decodeImagePresentation(value) {
3509
- if (value === null || typeof value !== "object" || Array.isArray(value) || value.kind !== "codex-subscription-image" || value.schemaVersion !== 1) return void 0;
3510
- const original = decodeOriginalImageRef(value.original);
3511
- return original === void 0 ? void 0 : { original };
3512
- }
3513
- function originalImageRefsEqual(left, right) {
3514
- const a = decodeOriginalImageRef(left);
3515
- const b = decodeOriginalImageRef(right);
3516
- return a !== void 0 && b !== void 0 && a.assetId === b.assetId && a.mediaType === b.mediaType && a.bytes === b.bytes && a.width === b.width && a.height === b.height && a.name === b.name && a.sha256 === b.sha256;
3517
- }
3518
- /** Resolve only an exact original reference copied into a DSH fork prefix. */
3519
- function inheritedOriginalImageRef(session, assetId) {
3520
- const parentSession = session?.header?.parentSession;
3521
- const seedLength = Number.isSafeInteger(session?.inheritedEventCount) ? session.inheritedEventCount : session?.header?.seedLength;
3522
- if (typeof parentSession !== "string" || parentSession.length === 0 || !Number.isSafeInteger(seedLength) || seedLength < 0 || !ORIGINAL_IMAGE_ID_PATTERN.test(assetId)) return void 0;
3523
- let events = session?.events;
3524
- if (!Array.isArray(events) && typeof session?.snapshotEvents === "function") try {
3525
- events = session.snapshotEvents();
3526
- } catch {
3527
- return;
2792
+ async delete(providerId) {
2793
+ if (providerId !== "openai-codex") throw new Error("Pending Codex login received an unknown provider");
2794
+ this.#credential = void 0;
3528
2795
  }
3529
- if (!Array.isArray(events)) return void 0;
3530
- for (const event of events) {
3531
- if (!Number.isSafeInteger(event?.seq) || event.seq < 0 || event.seq >= seedLength || event.type !== "tool/result") continue;
3532
- const original = decodeImagePresentation(event.data?.meta)?.original;
3533
- if (original?.assetId === assetId) return original;
2796
+ credential() {
2797
+ return clone$1(this.#credential);
3534
2798
  }
3535
- }
3536
- //#endregion
3537
- //#region src/codex-images.js
3538
- const CODEX_IMAGE_TOOL_NAME = "codex_image_generate";
3539
- const CODEX_IMAGE_GENERATION_URL = "https://chatgpt.com/backend-api/codex/images/generations";
3540
- const CODEX_IMAGE_EDIT_URL = "https://chatgpt.com/backend-api/codex/images/edits";
3541
- const MAX_REFERENCE_IMAGES = 5;
3542
- const RESPONSE_ENVELOPE_BYTES = 1024 * 1024;
3543
- const IMAGE_BACKGROUNDS = /* @__PURE__ */ new Set([
3544
- "auto",
3545
- "transparent",
3546
- "opaque"
3547
- ]);
3548
- const PNG_SIGNATURE = Buffer.from([
3549
- 137,
3550
- 80,
3551
- 78,
3552
- 71,
3553
- 13,
3554
- 10,
3555
- 26,
3556
- 10
3557
- ]);
3558
- const FULL_ATTACHMENT_ID = /^sha256:[0-9a-f]{64}$/u;
3559
- const BARE_ATTACHMENT_DIGEST = /^[0-9a-f]{64}$/iu;
3560
- const PATH_LIKE_ATTACHMENT_ID = /[\\/]/u;
3561
- const FILE_NAME_ATTACHMENT_ID = /\.[A-Za-z0-9]{1,16}$/u;
3562
- const record$2 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
3563
- const nonEmpty = (value) => typeof value === "string" && value.trim().length > 0 ? value.trim() : void 0;
3564
- const NETWORK_REASONS = {
3565
- UND_ERR_HEADERS_TIMEOUT: "the image service sent no response within 5 minutes",
3566
- UND_ERR_BODY_TIMEOUT: "the image response stalled",
3567
- UND_ERR_CONNECT_TIMEOUT: "the connection to the image service timed out",
3568
- UND_ERR_SOCKET: "the connection closed unexpectedly",
3569
- ECONNRESET: "the connection was reset",
3570
- ETIMEDOUT: "the connection timed out",
3571
- ENOTFOUND: "DNS lookup failed",
3572
- ECONNREFUSED: "the connection was refused",
3573
- EAI_AGAIN: "DNS lookup failed"
3574
2799
  };
3575
- function requestFailureReason(error) {
3576
- const code = [(error?.cause ?? error)?.code, error?.code].find((value) => typeof value === "string" && /^[A-Z][A-Z0-9_]{2,39}$/u.test(value));
3577
- if (code !== void 0) return `: ${NETWORK_REASONS[code] ?? "network error"} (${code})`;
3578
- if (error?.name === "TimeoutError") return ": the image request timed out";
3579
- return "";
3580
- }
3581
- function normalizeImageOptions(args) {
3582
- const quality = nonEmpty(args?.quality) ?? "auto";
3583
- const background = nonEmpty(args?.background) ?? "auto";
3584
- const size = nonEmpty(args?.size) ?? "auto";
3585
- validateImageQuality(resolveImageModel(args?.model), quality);
3586
- if (!IMAGE_BACKGROUNDS.has(background)) throw new Error("background must be auto, transparent, or opaque");
3587
- if (size !== "auto") {
3588
- const match = /^(\d+)x(\d+)$/u.exec(size);
3589
- const width = Number(match?.[1]);
3590
- const height = Number(match?.[2]);
3591
- const short = Math.min(width, height);
3592
- const long = Math.max(width, height);
3593
- const pixels = width * height;
3594
- if (match === null || width % 16 !== 0 || height % 16 !== 0 || long > 3840 || long > short * 3 || pixels < 655360 || pixels > 8294400) throw new Error("size must be auto or a valid GPT Image 2 widthxheight resolution");
2800
+ /**
2801
+ * Multi-account owner state stored in DSH's atomic plugin credential record.
2802
+ * The old single-account reference remains as a rollback source and is kept in
2803
+ * sync whenever that imported account rotates its refresh token.
2804
+ */
2805
+ var DshOAuthAccountVault = class {
2806
+ #tail = Promise.resolve();
2807
+ constructor(credentials, options) {
2808
+ if (credentials === void 0 || credentials === null || typeof credentials.readRecord !== "function" || typeof credentials.modifyRecord !== "function") throw new Error("Codex multi-account requires DSH credential records");
2809
+ this.credentials = credentials;
2810
+ this.key = options.key;
2811
+ this.legacyRef = options.legacyRef;
2812
+ this.legacyRefs = Object.freeze([...options.legacyRefs ?? []]);
2813
+ this.createId = options.createId ?? randomUUID;
2814
+ this.onLegacySyncFailure = options.onLegacySyncFailure ?? (() => {});
3595
2815
  }
3596
- return {
3597
- quality,
3598
- background,
3599
- size
3600
- };
3601
- }
3602
- function encodedLimit(decodedBytes) {
3603
- return Math.ceil(decodedBytes / 3) * 4;
3604
- }
3605
- function validBase64Body(value, end) {
3606
- for (let index = 0; index < end; index += 1) {
3607
- const code = value.charCodeAt(index);
3608
- if (!(code >= 65 && code <= 90 || code >= 97 && code <= 122 || code >= 48 && code <= 57 || code === 43 || code === 47)) return false;
2816
+ #enqueue(operation) {
2817
+ const current = this.#tail.catch(() => void 0).then(operation);
2818
+ this.#tail = current.catch(() => void 0);
2819
+ return current;
3609
2820
  }
3610
- return true;
3611
- }
3612
- async function readJsonWithin(response, maximumBytes) {
3613
- const contentLength = Number(response.headers.get("content-length"));
3614
- if (Number.isFinite(contentLength) && contentLength > maximumBytes) throw new Error("Codex image response exceeds the image size limit");
3615
- if (response.body === null) throw new Error("Codex returned an unreadable image response");
3616
- const reader = response.body.getReader();
3617
- const chunks = [];
3618
- let bytes = 0;
3619
- while (true) {
3620
- const { done, value } = await reader.read();
3621
- if (done) break;
3622
- bytes += value.byteLength;
3623
- if (bytes > maximumBytes) {
3624
- await reader.cancel();
3625
- throw new Error("Codex image response exceeds the image size limit");
2821
+ async #legacyCredential() {
2822
+ for (const ref of [this.legacyRef, ...this.legacyRefs]) {
2823
+ const hit = await this.credentials.resolve(ref);
2824
+ if (hit?.value === void 0 || hit.value === "") continue;
2825
+ return {
2826
+ ref,
2827
+ credential: parseOAuthCredential$1(hit.value)
2828
+ };
3626
2829
  }
3627
- chunks.push(value);
3628
2830
  }
3629
- const body = Buffer.concat(chunks.map((chunk) => Buffer.from(chunk)), bytes).toString("utf8");
3630
- try {
3631
- return JSON.parse(body);
3632
- } catch {
3633
- throw new Error("Codex returned an unreadable image response");
2831
+ async #ensurePayload() {
2832
+ const existing = await this.credentials.readRecord(this.key);
2833
+ if (existing !== void 0) return assertVaultRecord(existing);
2834
+ const legacy = await this.#legacyCredential();
2835
+ if (legacy === void 0) return void 0;
2836
+ const id = this.createId();
2837
+ return assertVaultRecord(await this.credentials.modifyRecord(this.key, (current) => {
2838
+ if (current !== void 0) return Promise.resolve(current);
2839
+ return Promise.resolve(grant({
2840
+ version: VERSION,
2841
+ activeId: id,
2842
+ legacyAccountId: id,
2843
+ accounts: [{
2844
+ id,
2845
+ label: DEFAULT_LABEL,
2846
+ credential: legacy.credential
2847
+ }]
2848
+ }));
2849
+ }));
3634
2850
  }
3635
- }
3636
- /** Strictly decode one PNG returned by the subscription backend. */
3637
- function decodeCodexPng(value, maximumBytes) {
3638
- const encoded = nonEmpty(value);
3639
- const padding = encoded?.endsWith("==") ? 2 : encoded?.endsWith("=") ? 1 : 0;
3640
- if (encoded === void 0 || encoded.length % 4 !== 0 || !validBase64Body(encoded, encoded.length - padding)) throw new Error("Codex returned an invalid base64 PNG");
3641
- const decodedBytes = encoded.length / 4 * 3 - padding;
3642
- if (decodedBytes > maximumBytes) throw new Error("Codex image exceeds the image size limit");
3643
- const data = Buffer.from(encoded, "base64");
3644
- if (data.length !== decodedBytes || data.length < PNG_SIGNATURE.length || !data.subarray(0, PNG_SIGNATURE.length).equals(PNG_SIGNATURE)) throw new Error("Codex returned an invalid PNG");
3645
- return new Uint8Array(data);
3646
- }
3647
- function imageReference(value) {
3648
- const originalDimensions = record$2(value.originalDimensions) ? {
3649
- width: value.originalDimensions.width,
3650
- height: value.originalDimensions.height
3651
- } : void 0;
3652
- return {
3653
- attachmentId: value.attachmentId,
3654
- mediaType: value.mediaType,
3655
- bytes: value.bytes,
3656
- width: value.width,
3657
- height: value.height,
3658
- ...value.name === void 0 ? {} : { name: value.name },
3659
- ...originalDimensions === void 0 ? {} : { originalDimensions }
3660
- };
3661
- }
3662
- function normalizeAttachmentId(value) {
3663
- if (typeof value !== "string") return void 0;
3664
- if (FULL_ATTACHMENT_ID.test(value)) return value;
3665
- const bare = BARE_ATTACHMENT_DIGEST.exec(value);
3666
- return bare === null ? void 0 : `sha256:${bare[0].toLowerCase()}`;
3667
- }
3668
- function invalidAttachmentId(value) {
3669
- const attachmentId = value?.attachmentId;
3670
- if (typeof attachmentId === "string" && (PATH_LIKE_ATTACHMENT_ID.test(attachmentId) || FILE_NAME_ATTACHMENT_ID.test(attachmentId))) throw new Error("referenceImages attachmentId is a file path or filename; call read_image on that file and retry with its complete sha256:<64 lowercase hex> attachment reference. Do not omit referenceImages or fall back to new image generation.");
3671
- throw new Error("referenceImages attachmentId must be sha256:<64 lowercase hex> copied from an image block or read_image result. Do not omit referenceImages or fall back to new image generation.");
3672
- }
3673
- function referenceOf(value, attachments) {
3674
- const attachmentId = normalizeAttachmentId(value?.attachmentId);
3675
- if (attachmentId === void 0) invalidAttachmentId(value);
3676
- if (!record$2(value) || !attachments.imageLimits.mediaTypes.includes(value.mediaType) || !Number.isSafeInteger(value.bytes) || value.bytes <= 0 || !Number.isSafeInteger(value.width) || value.width <= 0 || !Number.isSafeInteger(value.height) || value.height <= 0 || value.name !== void 0 && (typeof value.name !== "string" || value.name.length > 256) || value.originalDimensions !== void 0 && (!record$2(value.originalDimensions) || !Number.isSafeInteger(value.originalDimensions.width) || value.originalDimensions.width <= 0 || !Number.isSafeInteger(value.originalDimensions.height) || value.originalDimensions.height <= 0)) throw new Error("referenceImages contains an invalid image reference");
3677
- return imageReference({
3678
- ...value,
3679
- attachmentId
3680
- });
3681
- }
3682
- function sessionImageReferences(messages) {
3683
- const references = /* @__PURE__ */ new Map();
3684
- const visit = (content) => {
3685
- if (!Array.isArray(content)) return;
3686
- for (const block of content) if (block?.type === "image" && record$2(block.attachment)) {
3687
- const id = normalizeAttachmentId(block.attachment.attachmentId);
3688
- if (id !== void 0) references.set(id, block.attachment);
3689
- } else if (block?.type === "tool-result") visit(block.content);
3690
- };
3691
- for (const message of messages ?? []) visit(message?.content);
3692
- return references;
3693
- }
3694
- async function editImages(values, attachments, signal, messages) {
3695
- if (!Array.isArray(values) || values.length === 0 || values.length > MAX_REFERENCE_IMAGES) throw new Error(`referenceImages must contain between 1 and ${MAX_REFERENCE_IMAGES} images`);
3696
- const available = messages === void 0 ? void 0 : sessionImageReferences(messages);
3697
- const references = values.map((value) => {
3698
- const id = normalizeAttachmentId(value?.attachmentId);
3699
- if (id === void 0) invalidAttachmentId(value);
3700
- if (available === void 0) return referenceOf(value, attachments);
3701
- const selected = available.get(id);
3702
- if (selected === void 0) throw new Error("The selected image attachment cannot be found in the current session. Call read_image on the intended image and retry with its returned reference. Do not omit referenceImages or substitute another image.");
3703
- return referenceOf(selected, attachments);
3704
- });
3705
- if (new Set(references.map((value) => value.attachmentId)).size !== references.length) throw new Error("referenceImages must not contain duplicates");
3706
- const images = [];
3707
- let totalBytes = 0;
3708
- for (const reference of references) {
3709
- const stored = await attachments.readImage(reference, signal);
3710
- totalBytes += stored.data.byteLength;
3711
- if (totalBytes > attachments.imageLimits.maxMessageImageBytes) throw new Error("referenceImages exceed the DSH message image limit");
3712
- images.push({ image_url: `data:${stored.ref.mediaType};base64,${Buffer.from(stored.data).toString("base64")}` });
2851
+ async #modifyPayload(update) {
2852
+ await this.#ensurePayload();
2853
+ let previousLegacy;
2854
+ const payload = assertVaultRecord(await this.credentials.modifyRecord(this.key, async (current) => {
2855
+ if (current === void 0) throw new Error("Codex account vault is not signed in");
2856
+ const payload = assertVaultRecord(current);
2857
+ previousLegacy = payload.accounts.find((account) => account.id === payload.legacyAccountId)?.credential;
2858
+ const next = await update(clone$1(payload));
2859
+ return grant(next);
2860
+ }));
2861
+ const legacy = payload.accounts.find((account) => account.id === payload.legacyAccountId)?.credential;
2862
+ try {
2863
+ if (legacy === void 0) {
2864
+ if (previousLegacy !== void 0) await this.credentials.unset(this.legacyRef);
2865
+ } else if (JSON.stringify(legacy) !== JSON.stringify(previousLegacy)) await this.credentials.set(this.legacyRef, JSON.stringify(legacy));
2866
+ } catch {
2867
+ this.onLegacySyncFailure();
2868
+ }
2869
+ return payload;
3713
2870
  }
3714
- return images;
3715
- }
3716
- function imageContent(value) {
3717
- const label = typeof value.size === "string" && value.size.length > 0 ? `Generated a ${value.size} image.` : "Generated an image.";
3718
- return [{
3719
- type: "text",
3720
- text: value.localPath === void 0 ? label : `${label}\nOriginal PNG saved on the DSH host at: ${JSON.stringify(value.localPath)}. Read this file or copy it to the workspace with a .png extension; this host path is not a browser URL.`
3721
- }, {
3722
- type: "image",
3723
- attachment: imageReference(value.image)
3724
- }];
3725
- }
3726
- function imageOutputSchema() {
3727
- return {
3728
- type: "object",
3729
- additionalProperties: false,
3730
- properties: {
3731
- image: {
3732
- type: "object",
3733
- required: true,
3734
- additionalProperties: false,
3735
- properties: {
3736
- attachmentId: {
3737
- type: "string",
3738
- required: true
3739
- },
3740
- mediaType: {
3741
- type: "string",
3742
- enum: ["image/png"],
3743
- required: true
3744
- },
3745
- bytes: {
3746
- type: "integer",
3747
- required: true
3748
- },
3749
- width: {
3750
- type: "integer",
3751
- required: true
3752
- },
3753
- height: {
3754
- type: "integer",
3755
- required: true
3756
- },
3757
- name: { type: "string" },
3758
- originalDimensions: {
3759
- type: "object",
3760
- additionalProperties: false,
3761
- properties: {
3762
- width: {
3763
- type: "integer",
3764
- required: true
3765
- },
3766
- height: {
3767
- type: "integer",
3768
- required: true
3769
- }
3770
- }
3771
- }
3772
- }
3773
- },
3774
- original: {
3775
- type: "object",
3776
- required: true,
3777
- additionalProperties: false,
3778
- properties: {
3779
- assetId: {
3780
- type: "string",
3781
- required: true
3782
- },
3783
- mediaType: {
3784
- type: "string",
3785
- enum: ["image/png"],
3786
- required: true
3787
- },
3788
- bytes: {
3789
- type: "integer",
3790
- required: true
3791
- },
3792
- width: {
3793
- type: "integer",
3794
- required: true
3795
- },
3796
- height: {
3797
- type: "integer",
3798
- required: true
3799
- },
3800
- name: {
3801
- type: "string",
3802
- required: true
3803
- },
3804
- sha256: {
3805
- type: "string",
3806
- required: true
3807
- }
2871
+ list() {
2872
+ return this.#enqueue(async () => {
2873
+ const payload = await this.#ensurePayload();
2874
+ if (payload === void 0) return [];
2875
+ return payload.accounts.map((account) => ({
2876
+ id: account.id,
2877
+ label: account.label,
2878
+ active: account.id === payload.activeId,
2879
+ expiresAt: account.credential.expires,
2880
+ ...account.credential.email === void 0 ? {} : { email: account.credential.email }
2881
+ }));
2882
+ });
2883
+ }
2884
+ readActive() {
2885
+ return this.#enqueue(async () => {
2886
+ const payload = await this.#ensurePayload();
2887
+ return clone$1(payload?.accounts.find((account) => account.id === payload.activeId)?.credential);
2888
+ });
2889
+ }
2890
+ activeId() {
2891
+ return this.#enqueue(async () => (await this.#ensurePayload())?.activeId);
2892
+ }
2893
+ add(label, credential) {
2894
+ return this.#enqueue(async () => {
2895
+ const normalizedLabel = normalizeLabel(label);
2896
+ const validated = sanitizeOAuthCredential(credential);
2897
+ await this.#ensurePayload();
2898
+ const id = this.createId();
2899
+ const account = (await this.#modifyPayload((current) => ({
2900
+ ...current,
2901
+ activeId: id,
2902
+ accounts: [...current.accounts, {
2903
+ id,
2904
+ label: normalizedLabel,
2905
+ credential: validated
2906
+ }]
2907
+ }))).accounts.find((candidate) => candidate.id === id);
2908
+ return {
2909
+ id,
2910
+ label: account.label,
2911
+ active: true,
2912
+ expiresAt: account.credential.expires,
2913
+ ...account.credential.email === void 0 ? {} : { email: account.credential.email }
2914
+ };
2915
+ });
2916
+ }
2917
+ select(id) {
2918
+ return this.#enqueue(async () => {
2919
+ await this.#modifyPayload((current) => {
2920
+ if (!current.accounts.some((account) => account.id === id)) throw new Error("Unknown Codex account");
2921
+ return {
2922
+ ...current,
2923
+ activeId: id
2924
+ };
2925
+ });
2926
+ });
2927
+ }
2928
+ modifyActive(update) {
2929
+ return this.#enqueue(async () => {
2930
+ if (await this.#ensurePayload() === void 0) {
2931
+ const initial = await update(void 0);
2932
+ if (initial === void 0) return void 0;
2933
+ const credential = sanitizeOAuthCredential(initial);
2934
+ await this.credentials.set(this.legacyRef, JSON.stringify(credential));
2935
+ await this.#ensurePayload();
2936
+ return clone$1(credential);
2937
+ }
2938
+ let result;
2939
+ await this.#modifyPayload(async (current) => {
2940
+ const index = current.accounts.findIndex((account) => account.id === current.activeId);
2941
+ const previous = clone$1(current.accounts[index].credential);
2942
+ const next = await update(previous);
2943
+ if (next === void 0) {
2944
+ result = previous;
2945
+ return current;
3808
2946
  }
3809
- },
3810
- background: { type: "string" },
3811
- requestedModel: { type: "string" },
3812
- reportedModel: { type: "string" },
3813
- requestedSize: { type: "string" },
3814
- localPath: {
3815
- type: "string",
3816
- required: true,
3817
- description: "Absolute path to the original PNG on the DSH host."
3818
- },
3819
- quality: { type: "string" },
3820
- size: { type: "string" }
2947
+ const credential = sanitizeOAuthCredential(next);
2948
+ if (credential.email === void 0 && previous.email !== void 0) credential.email = previous.email;
2949
+ const accounts = [...current.accounts];
2950
+ accounts[index] = {
2951
+ ...accounts[index],
2952
+ credential
2953
+ };
2954
+ result = clone$1(credential);
2955
+ return {
2956
+ ...current,
2957
+ accounts
2958
+ };
2959
+ });
2960
+ return result;
2961
+ });
2962
+ }
2963
+ deleteAll() {
2964
+ return this.#enqueue(async () => {
2965
+ await this.credentials.deleteRecord(this.key);
2966
+ await this.credentials.unset(this.legacyRef);
2967
+ for (const ref of this.legacyRefs) await this.credentials.unset(ref);
2968
+ });
2969
+ }
2970
+ remove(id) {
2971
+ return this.#enqueue(async () => {
2972
+ await this.#modifyPayload((current) => {
2973
+ if (!current.accounts.some((account) => account.id === id)) throw new Error("Unknown Codex account");
2974
+ if (current.accounts.length === 1) throw new Error("Cannot remove the last account; sign out instead");
2975
+ const accounts = current.accounts.filter((account) => account.id !== id);
2976
+ return {
2977
+ ...current,
2978
+ activeId: current.activeId === id ? accounts[0].id : current.activeId,
2979
+ legacyAccountId: current.legacyAccountId === id ? void 0 : current.legacyAccountId,
2980
+ accounts
2981
+ };
2982
+ });
2983
+ });
2984
+ }
2985
+ };
2986
+ //#endregion
2987
+ //#region src/credential-store.js
2988
+ const PROVIDER$1 = "openai-codex";
2989
+ const abortIfNeeded = (options) => options?.signal?.throwIfAborted();
2990
+ const clone = (value) => value === void 0 ? void 0 : structuredClone(value);
2991
+ function assertProvider(providerId) {
2992
+ if (providerId !== PROVIDER$1) throw new Error(`Codex credential store does not own provider ${JSON.stringify(providerId)}`);
2993
+ }
2994
+ function assertOAuthCredential(value) {
2995
+ if (value === void 0) return void 0;
2996
+ if (value === null || typeof value !== "object" || value.type !== "oauth" || typeof value.access !== "string" || value.access.length === 0 || typeof value.refresh !== "string" || value.refresh.length === 0 || typeof value.expires !== "number" || !Number.isFinite(value.expires)) throw new Error("Codex credential store received a malformed OAuth credential");
2997
+ return clone(value);
2998
+ }
2999
+ function parseOAuthCredential(value) {
3000
+ try {
3001
+ return assertOAuthCredential(JSON.parse(value));
3002
+ } catch (error) {
3003
+ if (error?.message === "Codex credential store received a malformed OAuth credential") throw error;
3004
+ throw new Error("Codex credential store contains malformed OAuth JSON", { cause: error });
3005
+ }
3006
+ }
3007
+ /**
3008
+ * Adapt DSH's managed string credential service to pi-ai's typed OAuth store.
3009
+ * Refresh/login/logout operations are serialized so an older refresh response
3010
+ * cannot overwrite a newer rotated token.
3011
+ */
3012
+ var DshOAuthCredentialStore = class {
3013
+ #chains = /* @__PURE__ */ new Map();
3014
+ constructor(credentials, ref, legacyRefs = [], options = {}) {
3015
+ if (credentials === void 0 || credentials === null) throw new Error("Codex OAuth requires the DSH credentials service");
3016
+ const expirySkewMs = options.expirySkewMs ?? 0;
3017
+ if (!Number.isFinite(expirySkewMs) || expirySkewMs < 0) throw new Error("Codex OAuth expiry skew must be a non-negative finite number");
3018
+ this.credentials = credentials;
3019
+ this.ref = ref;
3020
+ this.legacyRefs = Object.freeze([...legacyRefs]);
3021
+ this.expirySkewMs = expirySkewMs;
3022
+ this.vault = options.vault;
3023
+ }
3024
+ #enqueue(providerId, operation, options) {
3025
+ assertProvider(providerId);
3026
+ const current = (this.#chains.get(providerId) ?? Promise.resolve()).catch(() => void 0).then(async () => {
3027
+ abortIfNeeded(options);
3028
+ return operation();
3029
+ });
3030
+ const tail = current.catch(() => void 0);
3031
+ this.#chains.set(providerId, tail);
3032
+ tail.finally(() => {
3033
+ if (this.#chains.get(providerId) === tail) this.#chains.delete(providerId);
3034
+ });
3035
+ return current;
3036
+ }
3037
+ async #read(providerId, options) {
3038
+ assertProvider(providerId);
3039
+ abortIfNeeded(options);
3040
+ if (this.vault !== void 0) {
3041
+ const current = await this.vault.readActive();
3042
+ if (current === void 0) return void 0;
3043
+ return this.expirySkewMs === 0 ? current : {
3044
+ ...current,
3045
+ expires: current.expires - this.expirySkewMs
3046
+ };
3047
+ }
3048
+ let hit = await this.credentials.resolve(this.ref);
3049
+ if (hit?.value === void 0 || hit.value === "") for (const legacyRef of this.legacyRefs) {
3050
+ const legacy = await this.credentials.resolve(legacyRef);
3051
+ if (legacy?.value === void 0 || legacy.value === "") continue;
3052
+ const migrated = parseOAuthCredential(legacy.value);
3053
+ await this.credentials.set(this.ref, JSON.stringify(migrated));
3054
+ await this.credentials.unset(legacyRef);
3055
+ hit = { value: JSON.stringify(migrated) };
3056
+ break;
3821
3057
  }
3058
+ abortIfNeeded(options);
3059
+ if (hit?.value === void 0 || hit.value === "") return void 0;
3060
+ const credential = parseOAuthCredential(hit.value);
3061
+ return this.expirySkewMs === 0 ? credential : {
3062
+ ...credential,
3063
+ expires: credential.expires - this.expirySkewMs
3064
+ };
3065
+ }
3066
+ read(providerId, options) {
3067
+ return this.#enqueue(providerId, () => this.#read(providerId, options), options);
3068
+ }
3069
+ async list(options) {
3070
+ abortIfNeeded(options);
3071
+ return await this.read(PROVIDER$1, options) === void 0 ? [] : [{
3072
+ providerId: PROVIDER$1,
3073
+ type: "oauth"
3074
+ }];
3075
+ }
3076
+ modify(providerId, update, options) {
3077
+ return this.#enqueue(providerId, async () => {
3078
+ if (this.vault !== void 0) {
3079
+ const next = await this.vault.modifyActive(async (current) => {
3080
+ const visible = current === void 0 || this.expirySkewMs === 0 ? current : {
3081
+ ...current,
3082
+ expires: current.expires - this.expirySkewMs
3083
+ };
3084
+ const updated = await update(clone(visible));
3085
+ return updated === void 0 ? void 0 : assertOAuthCredential(updated);
3086
+ });
3087
+ abortIfNeeded(options);
3088
+ return clone(next);
3089
+ }
3090
+ const current = await this.#read(providerId, options);
3091
+ const next = await update(clone(current));
3092
+ abortIfNeeded(options);
3093
+ if (next === void 0) return current;
3094
+ const validated = assertOAuthCredential(next);
3095
+ await this.credentials.set(this.ref, JSON.stringify(validated));
3096
+ for (const legacyRef of this.legacyRefs) await this.credentials.unset(legacyRef);
3097
+ abortIfNeeded(options);
3098
+ return clone(validated);
3099
+ }, options);
3100
+ }
3101
+ delete(providerId, options) {
3102
+ return this.#enqueue(providerId, async () => {
3103
+ if (this.vault !== void 0) {
3104
+ await this.vault.deleteAll();
3105
+ abortIfNeeded(options);
3106
+ return;
3107
+ }
3108
+ await this.credentials.unset(this.ref);
3109
+ for (const legacyRef of this.legacyRefs) await this.credentials.unset(legacyRef);
3110
+ abortIfNeeded(options);
3111
+ }, options);
3112
+ }
3113
+ };
3114
+ /** Return only account state that is safe to expose to the browser client. */
3115
+ function createCodexAuthService(models, store, options = {}) {
3116
+ const runLogin = options.runLogin ?? ((run) => run());
3117
+ const accountVault = options.accountVault;
3118
+ const createLoginModels = options.createLoginModels;
3119
+ const createPendingStore = options.createPendingStore ?? (() => new PendingOAuthCredentialStore());
3120
+ return Object.freeze({
3121
+ async status(options) {
3122
+ const current = await store.read(PROVIDER$1, options);
3123
+ const accounts = await accountVault?.list();
3124
+ if (current === void 0) return {
3125
+ authenticated: false,
3126
+ provider: PROVIDER$1,
3127
+ ...accounts === void 0 ? {} : { accounts }
3128
+ };
3129
+ return {
3130
+ authenticated: true,
3131
+ provider: PROVIDER$1,
3132
+ type: "oauth",
3133
+ expiresAt: current.expires,
3134
+ ...accounts === void 0 ? {} : { accounts }
3135
+ };
3136
+ },
3137
+ login(interaction, input = {}) {
3138
+ if (input.label !== void 0) {
3139
+ if (accountVault === void 0 || createLoginModels === void 0) throw new Error("Codex multi-account is unavailable");
3140
+ return runLogin(async () => {
3141
+ const pending = createPendingStore();
3142
+ await createLoginModels(pending).login(PROVIDER$1, "oauth", interaction);
3143
+ const credential = pending.credential();
3144
+ if (credential === void 0) throw new Error("Codex login did not return credentials");
3145
+ await accountVault.add(input.label, credential);
3146
+ });
3147
+ }
3148
+ return runLogin(() => models.login(PROVIDER$1, "oauth", interaction));
3149
+ },
3150
+ async select(id) {
3151
+ if (accountVault === void 0) throw new Error("Codex multi-account is unavailable");
3152
+ await accountVault.select(id);
3153
+ return this.status();
3154
+ },
3155
+ async remove(id) {
3156
+ if (accountVault === void 0) throw new Error("Codex multi-account is unavailable");
3157
+ await accountVault.remove(id);
3158
+ return this.status();
3159
+ },
3160
+ logout(options) {
3161
+ return models.logout(PROVIDER$1, options);
3162
+ }
3163
+ });
3164
+ }
3165
+ //#endregion
3166
+ //#region src/external-url.js
3167
+ const OPENAI_AUTH_ORIGIN = "https://auth.openai.com";
3168
+ /** Validate the only external origin this plugin may launch. */
3169
+ function assertCodexAuthUrl(value) {
3170
+ let url;
3171
+ try {
3172
+ url = new URL(value);
3173
+ } catch {
3174
+ throw new Error("Codex auth URL is invalid");
3175
+ }
3176
+ if (url.protocol !== "https:") throw new Error("Codex auth URL must use HTTPS");
3177
+ if (url.origin !== OPENAI_AUTH_ORIGIN || url.username !== "" || url.password !== "") throw new Error("Codex auth URL must use the OpenAI auth origin");
3178
+ return url.href;
3179
+ }
3180
+ /** Return a shell-free native opener command for the current desktop. */
3181
+ function commandForCodexAuthUrl(value, platform = process.platform) {
3182
+ const url = assertCodexAuthUrl(value);
3183
+ if (platform === "win32") return {
3184
+ file: "rundll32.exe",
3185
+ args: ["url.dll,FileProtocolHandler", url],
3186
+ shell: false
3187
+ };
3188
+ if (platform === "darwin") return {
3189
+ file: "open",
3190
+ args: [url],
3191
+ shell: false
3192
+ };
3193
+ if (platform === "linux") return {
3194
+ file: "xdg-open",
3195
+ args: [url],
3196
+ shell: false
3197
+ };
3198
+ throw new Error(`Codex auth URL opener is unsupported on ${platform}`);
3199
+ }
3200
+ function openCodexAuthUrl(value, options = {}) {
3201
+ const command = commandForCodexAuthUrl(value, options.platform);
3202
+ const spawnProcess = options.spawn ?? spawn;
3203
+ return new Promise((resolve, reject) => {
3204
+ const child = spawnProcess(command.file, command.args, {
3205
+ detached: true,
3206
+ stdio: "ignore",
3207
+ windowsHide: true,
3208
+ shell: command.shell
3209
+ });
3210
+ child.once("error", reject);
3211
+ child.once("spawn", () => {
3212
+ child.unref();
3213
+ resolve();
3214
+ });
3215
+ });
3216
+ }
3217
+ //#endregion
3218
+ //#region src/login-coordinator.js
3219
+ const LOGIN_METHODS = /* @__PURE__ */ new Set(["browser", "device_code"]);
3220
+ const TERMINAL_PHASES = /* @__PURE__ */ new Set([
3221
+ "authenticated",
3222
+ "failed",
3223
+ "cancelled"
3224
+ ]);
3225
+ const publicClone = (value) => structuredClone(value);
3226
+ const asObject = (value) => value !== null && typeof value === "object" ? value : {};
3227
+ const ok = (value) => ({
3228
+ ok: true,
3229
+ value
3230
+ });
3231
+ const badRequest = (message) => ({
3232
+ ok: false,
3233
+ error: {
3234
+ code: "bad-request",
3235
+ message,
3236
+ details: { issues: [] }
3237
+ }
3238
+ });
3239
+ const accountStatusError = (message) => ({
3240
+ ok: false,
3241
+ error: {
3242
+ code: "internal",
3243
+ message,
3244
+ details: { issues: [] }
3245
+ }
3246
+ });
3247
+ const classifyAccountStatusError = (error) => {
3248
+ const message = error instanceof Error ? error.message : "";
3249
+ if (/malformed (?:OAuth|grant|account vault)|received a malformed OAuth|contains malformed OAuth/iu.test(message)) return ["credential-malformed", "Codex account credentials are malformed"];
3250
+ if (/credential|account vault|readRecord|credential store|credentials service/iu.test(message)) return ["credential-unavailable", "Codex account credentials are unavailable"];
3251
+ const code = typeof error?.code === "string" ? error.code.toUpperCase() : "";
3252
+ if (error?.name === "TimeoutError" || [
3253
+ "TIMEOUT",
3254
+ "ETIMEDOUT",
3255
+ "UND_ERR_CONNECT_TIMEOUT"
3256
+ ].includes(code)) return ["transport", "Codex account status service is unavailable"];
3257
+ if ([
3258
+ "ECONNRESET",
3259
+ "ECONNREFUSED",
3260
+ "ENOTFOUND",
3261
+ "EAI_AGAIN",
3262
+ "NETWORK",
3263
+ "NETWORK_ERROR",
3264
+ "TRANSPORT"
3265
+ ].includes(code) || error?.name === "NetworkError") return ["transport", "Codex account status service is unavailable"];
3266
+ return ["unknown", "Could not read Codex account status"];
3267
+ };
3268
+ const deferred = () => {
3269
+ let resolve;
3270
+ let reject;
3271
+ return {
3272
+ promise: new Promise((onResolve, onReject) => {
3273
+ resolve = onResolve;
3274
+ reject = onReject;
3275
+ }),
3276
+ resolve,
3277
+ reject
3278
+ };
3279
+ };
3280
+ const publicPrompt = (prompt) => ({
3281
+ type: prompt.type,
3282
+ message: String(prompt.message ?? ""),
3283
+ ...typeof prompt.placeholder === "string" ? { placeholder: prompt.placeholder } : {}
3284
+ });
3285
+ function classifyLoginFailure(error) {
3286
+ const message = error instanceof Error ? error.message : "";
3287
+ if (/token exchange failed/iu.test(message)) return "token-exchange";
3288
+ if (/fetch failed|\b(?:ECONN|ENOTFOUND|ETIMEDOUT|CERT_|socket|network)\b/iu.test(message)) return "network";
3289
+ if (/extract accountId|account[_ -]?id/iu.test(message)) return "account-claim";
3290
+ if (/credential|credentials-local|OAuth JSON/iu.test(message)) return "credential-store";
3291
+ if (/Missing authorization code|State mismatch|callback/iu.test(message)) return "callback";
3292
+ return "provider";
3293
+ }
3294
+ /** Own one host-side login without exposing tokens to the browser client. */
3295
+ var CodexLoginCoordinator = class {
3296
+ #sessions = /* @__PURE__ */ new Map();
3297
+ #activeId;
3298
+ constructor(auth, options = {}) {
3299
+ this.auth = auth;
3300
+ this.createId = options.createId ?? (() => crypto.randomUUID());
3301
+ }
3302
+ async accountStatus(options) {
3303
+ return publicClone(await this.auth.status(options));
3304
+ }
3305
+ supportState() {
3306
+ const active = this.#activeId === void 0 ? void 0 : this.#sessions.get(this.#activeId);
3307
+ if (active === void 0) return { phase: "idle" };
3308
+ return {
3309
+ method: active.view.method,
3310
+ phase: active.view.phase,
3311
+ ...active.view.phase === "failed" ? { failure: classifyLoginFailure(active.hostError) } : {}
3312
+ };
3313
+ }
3314
+ async start({ method, label }) {
3315
+ if (!LOGIN_METHODS.has(method)) throw new Error(`unsupported Codex login method: ${String(method)}`);
3316
+ if (label !== void 0 && (typeof label !== "string" || label.trim().length === 0 || label.trim().length > 48)) throw new Error("unsupported Codex account label");
3317
+ const active = this.#activeId === void 0 ? void 0 : this.#sessions.get(this.#activeId);
3318
+ if (active !== void 0 && !TERMINAL_PHASES.has(active.view.phase)) {
3319
+ active.view = {
3320
+ id: active.view.id,
3321
+ provider: "openai-codex",
3322
+ method: active.view.method,
3323
+ phase: "cancelled",
3324
+ authenticated: false
3325
+ };
3326
+ active.controller.abort(/* @__PURE__ */ new Error("Codex login replaced by a new attempt"));
3327
+ }
3328
+ if (active !== void 0) this.#sessions.delete(active.view.id);
3329
+ const id = this.createId();
3330
+ const ready = deferred();
3331
+ const controller = new AbortController();
3332
+ const session = {
3333
+ controller,
3334
+ prompt: void 0,
3335
+ ready,
3336
+ view: {
3337
+ id,
3338
+ provider: "openai-codex",
3339
+ method,
3340
+ phase: "starting",
3341
+ authenticated: false
3342
+ }
3343
+ };
3344
+ this.#sessions.set(id, session);
3345
+ this.#activeId = id;
3346
+ const publishReady = () => ready.resolve(publicClone(session.view));
3347
+ const interaction = {
3348
+ signal: controller.signal,
3349
+ prompt: async (prompt) => {
3350
+ controller.signal.throwIfAborted();
3351
+ if (prompt.type === "select") return method;
3352
+ if (![
3353
+ "manual_code",
3354
+ "text",
3355
+ "secret"
3356
+ ].includes(prompt.type)) throw new Error(`unsupported Codex auth prompt: ${String(prompt.type)}`);
3357
+ const answer = deferred();
3358
+ session.prompt = answer;
3359
+ session.view = {
3360
+ ...session.view,
3361
+ phase: "waiting_input",
3362
+ prompt: publicPrompt(prompt)
3363
+ };
3364
+ const abortPrompt = () => answer.reject(controller.signal.reason ?? /* @__PURE__ */ new Error("login cancelled"));
3365
+ controller.signal.addEventListener("abort", abortPrompt, { once: true });
3366
+ prompt.signal?.addEventListener("abort", abortPrompt, { once: true });
3367
+ publishReady();
3368
+ try {
3369
+ return await answer.promise;
3370
+ } finally {
3371
+ controller.signal.removeEventListener("abort", abortPrompt);
3372
+ prompt.signal?.removeEventListener("abort", abortPrompt);
3373
+ if (session.prompt === answer) session.prompt = void 0;
3374
+ }
3375
+ },
3376
+ notify: (event) => {
3377
+ if (controller.signal.aborted) return;
3378
+ if (event.type === "auth_url") session.view = {
3379
+ ...session.view,
3380
+ phase: "waiting_browser",
3381
+ authUrl: assertCodexAuthUrl(event.url),
3382
+ ...typeof event.instructions === "string" ? { instructions: event.instructions } : {}
3383
+ };
3384
+ else if (event.type === "device_code") session.view = {
3385
+ ...session.view,
3386
+ phase: "waiting_device",
3387
+ deviceCode: {
3388
+ userCode: event.userCode,
3389
+ verificationUri: assertCodexAuthUrl(event.verificationUri),
3390
+ ...typeof event.intervalSeconds === "number" ? { intervalSeconds: event.intervalSeconds } : {},
3391
+ ...typeof event.expiresInSeconds === "number" ? { expiresInSeconds: event.expiresInSeconds } : {}
3392
+ }
3393
+ };
3394
+ else session.view = {
3395
+ ...session.view,
3396
+ message: String(event.message ?? "")
3397
+ };
3398
+ publishReady();
3399
+ }
3400
+ };
3401
+ session.run = Promise.resolve().then(() => this.auth.login(interaction, label === void 0 ? {} : { label: label.trim() })).then(async () => {
3402
+ if (controller.signal.aborted) return;
3403
+ const status = await this.auth.status();
3404
+ session.view = {
3405
+ id,
3406
+ provider: "openai-codex",
3407
+ method,
3408
+ phase: "authenticated",
3409
+ authenticated: status.authenticated === true,
3410
+ ...typeof status.expiresAt === "number" ? { expiresAt: status.expiresAt } : {}
3411
+ };
3412
+ }).catch(async (error) => {
3413
+ if (controller.signal.aborted) {
3414
+ session.view = {
3415
+ id,
3416
+ provider: "openai-codex",
3417
+ method,
3418
+ phase: "cancelled",
3419
+ authenticated: false
3420
+ };
3421
+ return;
3422
+ }
3423
+ try {
3424
+ if (label !== void 0) throw error;
3425
+ const status = await this.auth.status();
3426
+ if (status.authenticated === true) {
3427
+ session.view = {
3428
+ id,
3429
+ provider: "openai-codex",
3430
+ method,
3431
+ phase: "authenticated",
3432
+ authenticated: true,
3433
+ ...typeof status.expiresAt === "number" ? { expiresAt: status.expiresAt } : {}
3434
+ };
3435
+ return;
3436
+ }
3437
+ } catch {}
3438
+ session.view = {
3439
+ id,
3440
+ provider: "openai-codex",
3441
+ method,
3442
+ phase: "failed",
3443
+ authenticated: false,
3444
+ error: "Codex login failed"
3445
+ };
3446
+ session.hostError = error;
3447
+ }).finally(publishReady);
3448
+ return ready.promise;
3449
+ }
3450
+ read(id) {
3451
+ const session = this.#sessions.get(id);
3452
+ if (session === void 0) throw new Error("unknown Codex login");
3453
+ return publicClone(session.view);
3454
+ }
3455
+ async submit({ id, value }) {
3456
+ const session = this.#sessions.get(id);
3457
+ if (session === void 0) throw new Error("unknown Codex login");
3458
+ if (session.prompt === void 0 || session.view.phase !== "waiting_input") throw new Error("Codex login is not waiting for input");
3459
+ if (typeof value !== "string" || value.trim() === "") throw new Error("Codex login input is empty");
3460
+ const answer = session.prompt;
3461
+ session.prompt = void 0;
3462
+ session.view = {
3463
+ ...session.view,
3464
+ phase: session.view.authUrl === void 0 ? "starting" : "waiting_browser",
3465
+ prompt: void 0
3466
+ };
3467
+ answer.resolve(value);
3468
+ return this.read(id);
3469
+ }
3470
+ async cancel(id) {
3471
+ const session = this.#sessions.get(id);
3472
+ if (session === void 0) throw new Error("unknown Codex login");
3473
+ if (!TERMINAL_PHASES.has(session.view.phase)) {
3474
+ session.view = {
3475
+ id,
3476
+ provider: "openai-codex",
3477
+ method: session.view.method,
3478
+ phase: "cancelled",
3479
+ authenticated: false
3480
+ };
3481
+ session.controller.abort(/* @__PURE__ */ new Error("Codex login cancelled"));
3482
+ }
3483
+ return this.read(id);
3484
+ }
3485
+ async logout(options) {
3486
+ if (this.#activeId !== void 0) {
3487
+ const active = this.#sessions.get(this.#activeId);
3488
+ if (active !== void 0 && !TERMINAL_PHASES.has(active.view.phase)) await this.cancel(active.view.id);
3489
+ }
3490
+ await this.auth.logout(options);
3491
+ return this.accountStatus(options);
3492
+ }
3493
+ async selectAccount(id) {
3494
+ return publicClone(await this.auth.select(id));
3495
+ }
3496
+ async removeAccount(id) {
3497
+ return publicClone(await this.auth.remove(id));
3498
+ }
3499
+ };
3500
+ /** Map the loopback-only DSH Connection channel onto the coordinator. */
3501
+ function createCodexRpcHandler(coordinator, options = {}) {
3502
+ const openExternal = options.openExternal;
3503
+ return async (endpoint, payload, signal) => {
3504
+ try {
3505
+ signal.throwIfAborted();
3506
+ const input = asObject(payload);
3507
+ if (endpoint === "status") try {
3508
+ return ok(await coordinator.accountStatus({ signal }));
3509
+ } catch (error) {
3510
+ if (signal.aborted) throw error;
3511
+ const [, message] = classifyAccountStatusError(error);
3512
+ return accountStatusError(message);
3513
+ }
3514
+ if (endpoint === "login/start") {
3515
+ const started = await coordinator.start({
3516
+ method: input.method,
3517
+ label: input.label
3518
+ });
3519
+ if (input.openExternal !== true) return ok(started);
3520
+ const url = started.authUrl ?? started.deviceCode?.verificationUri;
3521
+ if (typeof url !== "string" || openExternal === void 0) return ok({
3522
+ ...started,
3523
+ externalOpened: false
3524
+ });
3525
+ try {
3526
+ await openExternal(url);
3527
+ return ok({
3528
+ ...started,
3529
+ externalOpened: true
3530
+ });
3531
+ } catch {
3532
+ return ok({
3533
+ ...started,
3534
+ externalOpened: false
3535
+ });
3536
+ }
3537
+ }
3538
+ if (endpoint === "login/status") return ok(coordinator.read(input.id));
3539
+ if (endpoint === "login/submit") return ok(await coordinator.submit({
3540
+ id: input.id,
3541
+ value: input.value
3542
+ }));
3543
+ if (endpoint === "login/cancel") return ok(await coordinator.cancel(input.id));
3544
+ if (endpoint === "logout") return ok(await coordinator.logout({ signal }));
3545
+ if (endpoint === "account/select") return ok(await coordinator.selectAccount(input.id));
3546
+ if (endpoint === "account/remove") return ok(await coordinator.removeAccount(input.id));
3547
+ return badRequest(`unknown Codex auth endpoint: ${endpoint}`);
3548
+ } catch (error) {
3549
+ if (signal.aborted) throw error;
3550
+ const message = error instanceof Error && /^(unknown|unsupported|a Codex|Codex login)/.test(error.message) ? error.message : "Codex request failed";
3551
+ return badRequest(message);
3552
+ }
3553
+ };
3554
+ }
3555
+ //#endregion
3556
+ //#region src/transport-failure.js
3557
+ function normalizeTransportEvent(event, signal) {
3558
+ if (signal?.aborted || event?.type !== "error" || event.reason !== "error") return event;
3559
+ const message = event.error?.errorMessage;
3560
+ if (typeof message !== "string" || !/^WebSocket closed (?:1006|1011|1012|1013)(?:\s|$)/.test(message)) return event;
3561
+ return {
3562
+ ...event,
3563
+ error: {
3564
+ ...event.error,
3565
+ errorMessage: `Network transport failure: ${message}`
3566
+ }
3567
+ };
3568
+ }
3569
+ //#endregion
3570
+ //#region src/pi-ai-runtime.js
3571
+ const FAST_SERVICE_TIER = "priority";
3572
+ const ULTRAFAST_SERVICE_TIER = "ultrafast";
3573
+ /** Apply the chosen detail to user and tool-result images after the provider assembles the request. */
3574
+ function withInputImageDetail(payload, detail) {
3575
+ if (!Array.isArray(payload.input)) return payload;
3576
+ let changed = false;
3577
+ const input = payload.input.map((item) => {
3578
+ const field = Array.isArray(item?.content) ? "content" : ["function_call_output", "custom_tool_call_output"].includes(item?.type) && Array.isArray(item.output) ? "output" : void 0;
3579
+ if (field === void 0) return item;
3580
+ let itemChanged = false;
3581
+ const parts = item[field].map((part) => {
3582
+ if (part?.type !== "input_image" || part.detail === detail) return part;
3583
+ itemChanged = true;
3584
+ return {
3585
+ ...part,
3586
+ detail
3587
+ };
3588
+ });
3589
+ if (!itemChanged) return item;
3590
+ changed = true;
3591
+ return {
3592
+ ...item,
3593
+ [field]: parts
3594
+ };
3595
+ });
3596
+ return changed ? {
3597
+ ...payload,
3598
+ input
3599
+ } : payload;
3600
+ }
3601
+ /**
3602
+ * Preserve pi-ai's native Codex OAuth provider while allowing DSH's generic
3603
+ * PiAiAdapter to pass the access token resolved by the host credential store.
3604
+ *
3605
+ * PiAiAdapter owns a request-local Models collection backed by the same DSH
3606
+ * credential store as this provider. A pure OAuth provider ignores its
3607
+ * `apiKey` request override and otherwise fails before dispatch with "Provider
3608
+ * is not configured". This non-interactive bridge teaches that collection how
3609
+ * to consume only the already-refreshed token for this request; login, refresh,
3610
+ * persistence, headers, transport, and model behavior remain owned by the
3611
+ * original provider.
3612
+ */
3613
+ function openaiCodexSubscriptionProvider({ resolveSpeedMode = () => void 0, resolveOutputVerbosity = () => OUTPUT_VERBOSITY_DEFAULT, resolveInputImageDetail = () => void 0, resolveContextMode = () => void 0, resolveCustomContextWindow = () => void 0, catalog, connection, compaction, runNetwork = (_area, operation) => operation() } = {}) {
3614
+ const provider = createOpenAICodexProvider();
3615
+ const requestToken = Object.freeze({
3616
+ name: "DSH-managed Codex OAuth request token",
3617
+ async resolve({ credential }) {
3618
+ const token = credential?.type === "api_key" ? credential.key : void 0;
3619
+ if (typeof token !== "string" || token.length === 0) return void 0;
3620
+ return {
3621
+ auth: { apiKey: token },
3622
+ source: "DSH-managed OAuth request"
3623
+ };
3624
+ }
3625
+ });
3626
+ const modelMetadata = (model) => catalog?.metadata(model?.id);
3627
+ const supportsVerbosity = (model) => modelMetadata(model)?.supportVerbosity ?? model?.id !== "gpt-5.3-codex-spark";
3628
+ const withPreferences = (model, options = {}) => {
3629
+ const metadata = modelMetadata(model);
3630
+ const requestedVerbosity = resolveOutputVerbosity();
3631
+ const textVerbosity = supportsVerbosity(model) ? requestedVerbosity === "default" ? metadata?.defaultVerbosity ?? "medium" : requestedVerbosity : void 0;
3632
+ const speedMode = resolveSpeedMode();
3633
+ const serviceTier = speedMode === "ultrafast" && metadata?.supportsUltrafast === true ? ULTRAFAST_SERVICE_TIER : speedMode === "fast" && (metadata?.supportsFast ?? supportsCodexFastMode(model?.id)) ? FAST_SERVICE_TIER : void 0;
3634
+ const inputImageDetail = normalizeInputImageDetail(resolveInputImageDetail());
3635
+ const onPayload = options.onPayload;
3636
+ return {
3637
+ ...options,
3638
+ ...textVerbosity === void 0 ? {} : { textVerbosity },
3639
+ ...serviceTier === void 0 ? {} : { serviceTier },
3640
+ async onPayload(payload, requestModel) {
3641
+ const preferred = {
3642
+ ...payload,
3643
+ ...textVerbosity === void 0 ? {} : { text: {
3644
+ ...payload.text ?? {},
3645
+ verbosity: textVerbosity
3646
+ } },
3647
+ ...serviceTier === void 0 ? {} : { service_tier: serviceTier }
3648
+ };
3649
+ const managed = compaction?.preparePayload(preferred, model?.contextWindow) ?? preferred;
3650
+ const next = await onPayload?.(managed, requestModel);
3651
+ const detailed = inputImageDetail === "auto" ? next ?? managed : withInputImageDetail(next ?? managed, inputImageDetail);
3652
+ return {
3653
+ ...detailed,
3654
+ ...textVerbosity === void 0 ? {} : { text: {
3655
+ ...detailed.text ?? {},
3656
+ verbosity: textVerbosity
3657
+ } },
3658
+ ...serviceTier === void 0 ? {} : { service_tier: serviceTier }
3659
+ };
3660
+ }
3661
+ };
3662
+ };
3663
+ const getModels = () => (catalog?.getModels() ?? provider.getModels()).map((model) => {
3664
+ const maximum = modelContextMaximum(model);
3665
+ const mode = resolveContextMode();
3666
+ if (model.id === "gpt-5.3-codex-spark" || !["extended", "custom"].includes(mode)) return model;
3667
+ if (mode === "extended") {
3668
+ const contextWindow = maximum;
3669
+ return {
3670
+ ...model,
3671
+ contextWindow
3672
+ };
3673
+ }
3674
+ const requested = clampModelContext(resolveCustomContextWindow(customContextModelKey(model.id)), maximum, model.contextWindow);
3675
+ return {
3676
+ ...model,
3677
+ contextWindow: requested
3678
+ };
3679
+ });
3680
+ const networkIterable = (factory, options) => {
3681
+ let iterator;
3682
+ let prepared;
3683
+ const step = async (method, value) => {
3684
+ const request = await (prepared ??= connection?.prepare(options) ?? Promise.resolve({ options }));
3685
+ const result = await runNetwork("model", () => {
3686
+ iterator ??= factory(compaction?.requestOptions(request.options) ?? request.options)[Symbol.asyncIterator]();
3687
+ return iterator[method]?.(value) ?? (method === "throw" ? Promise.reject(value) : Promise.resolve({
3688
+ done: true,
3689
+ value
3690
+ }));
3691
+ }, compaction?.networkOptions(request.network) ?? request.network);
3692
+ return result.done ? result : {
3693
+ ...result,
3694
+ value: normalizeTransportEvent(result.value, request.options?.signal)
3695
+ };
3696
+ };
3697
+ return {
3698
+ [Symbol.asyncIterator]() {
3699
+ return this;
3700
+ },
3701
+ next: (value) => step("next", value),
3702
+ return: (value) => iterator ? step("return", value) : Promise.resolve({
3703
+ done: true,
3704
+ value
3705
+ }),
3706
+ throw: (error) => iterator ? step("throw", error) : Promise.reject(error)
3707
+ };
3708
+ };
3709
+ return Object.freeze({
3710
+ ...provider,
3711
+ auth: Object.freeze({
3712
+ ...provider.auth,
3713
+ apiKey: requestToken
3714
+ }),
3715
+ getModels,
3716
+ stream: (model, context, options) => networkIterable((prepared) => provider.stream(model, context, prepared), withPreferences(model, options)),
3717
+ streamSimple: (model, context, options) => networkIterable((prepared) => provider.streamSimple(model, context, prepared), withPreferences(model, options))
3718
+ });
3719
+ }
3720
+ Object.freeze([
3721
+ "0.82.1",
3722
+ "0.85.1",
3723
+ "0.87.1"
3724
+ ]);
3725
+ //#endregion
3726
+ //#region src/model-catalog.js
3727
+ const CODEX_MODELS_URL = `https://chatgpt.com/backend-api/codex/models?client_version=${encodeURIComponent(PACKAGE_VERSION)}`;
3728
+ const LEVELS = [
3729
+ "off",
3730
+ "minimal",
3731
+ "low",
3732
+ "medium",
3733
+ "high",
3734
+ "xhigh",
3735
+ "max"
3736
+ ];
3737
+ const DEFAULT_REFRESH_TIMEOUT_MS = 1e4;
3738
+ const record$3 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
3739
+ const nonEmpty$1 = (value) => typeof value === "string" && value.trim().length > 0 ? value.trim() : void 0;
3740
+ const positiveInteger = (value) => Number.isSafeInteger(value) && value > 0 ? value : void 0;
3741
+ function upgradeModelId(value) {
3742
+ return (typeof value.upgrade === "string" ? nonEmpty$1(value.upgrade) : record$3(value.upgrade) ? nonEmpty$1(value.upgrade.model) : void 0) ?? (record$3(value.upgrade_info) ? nonEmpty$1(value.upgrade_info.model) : void 0) ?? (record$3(value.upgradeInfo) ? nonEmpty$1(value.upgradeInfo.model) : void 0);
3743
+ }
3744
+ function retirementMillis(value) {
3745
+ for (const source of [
3746
+ value,
3747
+ value.upgrade,
3748
+ value.upgrade_info,
3749
+ value.upgradeInfo
3750
+ ]) {
3751
+ if (!record$3(source)) continue;
3752
+ for (const raw of [source.retirement_at, source.retirementAt]) {
3753
+ const seconds = positiveInteger(raw);
3754
+ if (seconds !== void 0 && Number.isSafeInteger(seconds * 1e3)) return seconds * 1e3;
3755
+ if (typeof raw === "string" && /^\d{4}-\d{2}-\d{2}T[\d:.]+(?:Z|[+-]\d{2}:\d{2})$/u.test(raw)) {
3756
+ const parsed = Date.parse(raw);
3757
+ if (Number.isFinite(parsed) && parsed > 0) return parsed;
3758
+ }
3759
+ }
3760
+ }
3761
+ }
3762
+ function retirementNotice(value, now = Date.now(), targetDisplayName) {
3763
+ if (!record$3(value) || !Number.isFinite(now)) return void 0;
3764
+ const at = retirementMillis(value);
3765
+ if (at === void 0 || at <= now) return void 0;
3766
+ const date = new Date(at);
3767
+ if (!Number.isFinite(date.getTime())) return void 0;
3768
+ const dateText = `${String(date.getUTCFullYear()).padStart(4, "0")}-${String(date.getUTCMonth() + 1).padStart(2, "0")}-${String(date.getUTCDate()).padStart(2, "0")}`;
3769
+ const upgradeTo = upgradeModelId(value);
3770
+ const upgradeName = upgradeTo === void 0 ? void 0 : nonEmpty$1(targetDisplayName) ?? upgradeTo;
3771
+ return {
3772
+ text: `Retires on ${dateText} (UTC).${upgradeName === void 0 ? "" : ` Switch to ${upgradeName} to keep working.`}`,
3773
+ at,
3774
+ upgradeTo,
3775
+ dateText
3776
+ };
3777
+ }
3778
+ function reasoningMap(levels) {
3779
+ const supported = new Set((Array.isArray(levels) ? levels : []).map((level) => nonEmpty$1(record$3(level) ? level.effort : void 0)).filter(Boolean));
3780
+ const map = Object.fromEntries(LEVELS.map((level) => [level, null]));
3781
+ if (supported.has("none")) map.off = "none";
3782
+ for (const level of LEVELS.slice(1)) if (supported.has(level)) map[level] = level;
3783
+ return map;
3784
+ }
3785
+ const capabilityNames = (values) => [...new Set(values.filter((value) => typeof value === "string" && /^[a-z][a-z0-9_-]{0,31}$/u.test(value)))].sort().slice(0, 16);
3786
+ const speedTierIds = (value) => [...Array.isArray(value.additional_speed_tiers) ? value.additional_speed_tiers : [], ...Array.isArray(value.service_tiers) ? value.service_tiers.map((tier) => tier?.id) : []];
3787
+ function unsupportedCapabilities(value) {
3788
+ const reasoning = capabilityNames((value.supported_reasoning_levels ?? []).map((item) => item?.effort)).filter((level) => !["none", ...LEVELS.slice(1)].includes(level));
3789
+ const inputs = capabilityNames(Array.isArray(value.input_modalities) ? value.input_modalities : []).filter((input) => !["text", "image"].includes(input));
3790
+ const speeds = capabilityNames([...Array.isArray(value.additional_speed_tiers) ? value.additional_speed_tiers : [], ...Array.isArray(value.service_tiers) ? value.service_tiers.map((tier) => tier?.id) : []]).filter((tier) => ![
3791
+ "auto",
3792
+ "default",
3793
+ "standard",
3794
+ "fast",
3795
+ "priority",
3796
+ "ultrafast"
3797
+ ].includes(tier));
3798
+ return {
3799
+ ...reasoning.length ? { reasoning } : {},
3800
+ ...inputs.length ? { inputs } : {},
3801
+ ...speeds.length ? { speeds } : {}
3802
+ };
3803
+ }
3804
+ function visibleModel(value, displayNames, now) {
3805
+ if (!record$3(value)) return void 0;
3806
+ const id = nonEmpty$1(value.slug);
3807
+ const reserve = id === "gpt-reserve" && ["list", "hide"].includes(value.visibility);
3808
+ if (id === void 0 || value.visibility !== "list" && !reserve) return void 0;
3809
+ const supported = Array.isArray(value.supported_reasoning_levels) ? value.supported_reasoning_levels : [];
3810
+ const input = Array.isArray(value.input_modalities) ? value.input_modalities.filter((item) => ["text", "image"].includes(item)) : ["text", "image"];
3811
+ const unsupported = unsupportedCapabilities({
3812
+ ...value,
3813
+ supported_reasoning_levels: supported
3814
+ });
3815
+ const upgradeTo = upgradeModelId(value);
3816
+ const retirement = retirementNotice(value, now, displayNames.get(upgradeTo));
3817
+ const notice = retirement?.text;
3818
+ const description = nonEmpty$1(value.description);
3819
+ return {
3820
+ ...Object.keys(unsupported).length ? { unsupported } : {},
3821
+ id,
3822
+ name: `${reserve ? "GPT-Reserve (Experimental)" : nonEmpty$1(value.display_name) ?? id}${retirement === void 0 ? "" : ` (retires ${retirement.dateText})`}`,
3823
+ description: notice === void 0 ? description : description === void 0 ? notice : `${description}\n${notice}`,
3824
+ ...retirement === void 0 ? {} : { retirement: {
3825
+ at: retirement.at,
3826
+ upgradeTo: retirement.upgradeTo
3827
+ } },
3828
+ priority: Number.isFinite(value.priority) ? value.priority : 0,
3829
+ input: input.length > 0 ? input : ["text"],
3830
+ contextWindow: positiveInteger(value.context_window) ?? positiveInteger(value.max_context_window),
3831
+ ...positiveInteger(value.max_context_window) === void 0 ? {} : { maxContextWindow: value.max_context_window },
3832
+ reasoning: supported.length > 0,
3833
+ thinkingLevelMap: reasoningMap(supported),
3834
+ supportVerbosity: value.support_verbosity === true,
3835
+ defaultVerbosity: [
3836
+ "low",
3837
+ "medium",
3838
+ "high"
3839
+ ].includes(value.default_verbosity) ? value.default_verbosity : void 0,
3840
+ supportsFast: speedTierIds(value).some((tier) => tier === "fast" || tier === "priority"),
3841
+ supportsUltrafast: speedTierIds(value).includes("ultrafast")
3842
+ };
3843
+ }
3844
+ function parseOfficialModelCatalog(value, now = Date.now()) {
3845
+ if (!record$3(value) || !Array.isArray(value.models)) throw new Error("Codex returned a malformed model catalog");
3846
+ const displayNames = /* @__PURE__ */ new Map();
3847
+ for (const model of value.models) {
3848
+ if (!record$3(model)) continue;
3849
+ const id = nonEmpty$1(model.slug);
3850
+ const name = nonEmpty$1(model.display_name);
3851
+ if (id !== void 0 && name !== void 0 && !displayNames.has(id)) displayNames.set(id, name);
3852
+ }
3853
+ const seen = /* @__PURE__ */ new Set();
3854
+ return value.models.map((model) => visibleModel(model, displayNames, now)).filter((model) => model !== void 0 && !seen.has(model.id) && seen.add(model.id)).sort((left, right) => Number(left.id === "gpt-reserve") - Number(right.id === "gpt-reserve") || right.priority - left.priority);
3855
+ }
3856
+ function mergeModel(baseModels, remote) {
3857
+ const base = baseModels.find((model) => model.id === remote.id) ?? baseModels.find((model) => model.id !== "gpt-5.3-codex-spark") ?? baseModels[0];
3858
+ if (base === void 0) return void 0;
3859
+ return {
3860
+ ...base,
3861
+ id: remote.id,
3862
+ name: remote.name,
3863
+ ...remote.retirement === void 0 || remote.description === void 0 ? {} : { description: remote.description },
3864
+ input: remote.input,
3865
+ reasoning: remote.reasoning,
3866
+ thinkingLevelMap: remote.thinkingLevelMap,
3867
+ ...remote.contextWindow === void 0 ? {} : { contextWindow: remote.contextWindow },
3868
+ ...remote.maxContextWindow === void 0 ? {} : { maxContextWindow: remote.maxContextWindow },
3869
+ ...base.id === remote.id ? {} : { cost: {
3870
+ input: 0,
3871
+ output: 0,
3872
+ cacheRead: 0,
3873
+ cacheWrite: 0
3874
+ } }
3875
+ };
3876
+ }
3877
+ function createOfficialModelCatalog(options = {}) {
3878
+ const fetchCatalog = options.fetch ?? fetch;
3879
+ const getNow = options.now ?? Date.now;
3880
+ const scheduleTimeout = options.setTimeout ?? setTimeout;
3881
+ const cancelTimeout = options.clearTimeout ?? clearTimeout;
3882
+ const timeoutMs = Number.isFinite(options.timeoutMs) && options.timeoutMs > 0 ? options.timeoutMs : DEFAULT_REFRESH_TIMEOUT_MS;
3883
+ let models;
3884
+ let metadata = /* @__PURE__ */ new Map();
3885
+ let etag;
3886
+ let rawCatalog;
3887
+ let revision = 0;
3888
+ let refreshing;
3889
+ let generation = 0;
3890
+ let refreshStatus = "idle";
3891
+ const refresh = ({ signal } = {}) => {
3892
+ if (signal?.aborted) return Promise.reject(signal.reason ?? /* @__PURE__ */ new Error("Codex model catalog refresh aborted"));
3893
+ if (refreshing?.generation === generation) return refreshing.promise;
3894
+ const currentGeneration = generation;
3895
+ refreshStatus = "refreshing";
3896
+ let outcome = "idle";
3897
+ const controller = new AbortController();
3898
+ const abort = () => {
3899
+ if (!controller.signal.aborted) controller.abort(signal?.reason ?? /* @__PURE__ */ new Error("Codex model catalog refresh aborted"));
3900
+ };
3901
+ signal?.addEventListener("abort", abort, { once: true });
3902
+ const requestSignal = controller.signal;
3903
+ let timer;
3904
+ const timeoutError = /* @__PURE__ */ new Error("Codex model catalog refresh timed out");
3905
+ const work = (async () => {
3906
+ const { access, accountId } = await readSubscriptionCredentials(options.getAuth, options.readCredential, requestSignal);
3907
+ if (currentGeneration !== generation || requestSignal.aborted) return false;
3908
+ if (typeof access !== "string" || access.length === 0 || typeof accountId !== "string" || accountId.length === 0) return false;
3909
+ const headers = {
3910
+ authorization: `Bearer ${access}`,
3911
+ "chatgpt-account-id": accountId,
3912
+ accept: "application/json",
3913
+ originator: "pi",
3914
+ "user-agent": USER_AGENT,
3915
+ ...etag === void 0 ? {} : { "if-none-match": etag }
3916
+ };
3917
+ const response = await fetchCatalog(CODEX_MODELS_URL, {
3918
+ method: "GET",
3919
+ redirect: "error",
3920
+ headers,
3921
+ signal: requestSignal
3922
+ });
3923
+ if (currentGeneration !== generation || requestSignal.aborted) return false;
3924
+ if (response.status === 304) {
3925
+ outcome = "ok";
3926
+ if (rawCatalog === void 0) return false;
3927
+ const remote = parseOfficialModelCatalog(rawCatalog, getNow());
3928
+ const next = remote.map((model) => mergeModel(options.baseModels(), model)).filter(Boolean);
3929
+ if (next.length === 0 || JSON.stringify(next) === JSON.stringify(models)) return false;
3930
+ models = next;
3931
+ metadata = new Map(remote.map((model) => [model.id, model]));
3932
+ revision += 1;
3933
+ options.onUpdated?.();
3934
+ return true;
3935
+ }
3936
+ if (!response.ok) throw new Error(`Codex model catalog failed (HTTP ${response.status})`);
3937
+ const raw = await response.json();
3938
+ const remote = parseOfficialModelCatalog(raw, getNow());
3939
+ if (currentGeneration !== generation || requestSignal.aborted) return false;
3940
+ if (remote.length === 0) throw new Error("Codex returned an empty model catalog");
3941
+ const baseModels = options.baseModels();
3942
+ const next = remote.map((model) => mergeModel(baseModels, model)).filter(Boolean);
3943
+ if (next.length === 0) throw new Error("Codex model catalog has no compatible models");
3944
+ if (currentGeneration !== generation || requestSignal.aborted) return false;
3945
+ models = next;
3946
+ rawCatalog = raw;
3947
+ metadata = new Map(remote.map((model) => [model.id, model]));
3948
+ etag = nonEmpty$1(response.headers.get("etag")) ?? etag;
3949
+ revision += 1;
3950
+ outcome = "ok";
3951
+ options.onUpdated?.();
3952
+ return true;
3953
+ })();
3954
+ let rejectAborted;
3955
+ const abortPromise = new Promise((_, reject) => {
3956
+ rejectAborted = () => reject(requestSignal.reason ?? /* @__PURE__ */ new Error("Codex model catalog refresh aborted"));
3957
+ if (requestSignal.aborted) rejectAborted();
3958
+ else requestSignal.addEventListener("abort", rejectAborted, { once: true });
3959
+ });
3960
+ timer = scheduleTimeout(() => controller.abort(timeoutError), timeoutMs);
3961
+ timer.unref?.();
3962
+ const promise = Promise.race([work, abortPromise]).catch((error) => {
3963
+ outcome = "failed";
3964
+ throw error;
3965
+ }).finally(() => {
3966
+ cancelTimeout(timer);
3967
+ signal?.removeEventListener("abort", abort);
3968
+ requestSignal.removeEventListener("abort", rejectAborted);
3969
+ if (refreshing?.promise === promise) {
3970
+ refreshing = void 0;
3971
+ refreshStatus = outcome;
3972
+ }
3973
+ });
3974
+ refreshing = {
3975
+ generation: currentGeneration,
3976
+ promise,
3977
+ cancel: () => controller.abort()
3978
+ };
3979
+ return promise;
3980
+ };
3981
+ return Object.freeze({
3982
+ refresh,
3983
+ getModels: () => models ?? [],
3984
+ metadata: (modelId) => metadata.get(modelId),
3985
+ revision: () => revision,
3986
+ capabilityGaps: () => [...metadata.values()].filter((model) => model.unsupported && /^[a-z][a-z0-9._-]{0,79}$/u.test(model.id)).slice(0, 20).map((model) => ({
3987
+ model: model.id,
3988
+ ...structuredClone(model.unsupported)
3989
+ })),
3990
+ status: () => ({
3991
+ source: models === void 0 ? "unavailable" : "online",
3992
+ refresh: refreshStatus
3993
+ }),
3994
+ clear() {
3995
+ generation += 1;
3996
+ const flight = refreshing;
3997
+ refreshing = void 0;
3998
+ flight?.cancel();
3999
+ models = void 0;
4000
+ rawCatalog = void 0;
4001
+ metadata = /* @__PURE__ */ new Map();
4002
+ etag = void 0;
4003
+ refreshStatus = "idle";
4004
+ revision += 1;
4005
+ options.onUpdated?.();
4006
+ }
4007
+ });
4008
+ }
4009
+ //#endregion
4010
+ //#region src/codex-search.js
4011
+ const CODEX_SEARCH_PROVIDER_ID = "codex-subscription";
4012
+ const CODEX_AUTO_SEARCH_PROVIDER_ID = "codex-subscription-auto";
4013
+ const CODEX_SEARCH_URL = "https://chatgpt.com/backend-api/codex/alpha/search";
4014
+ const DEFAULT_MODEL = "gpt-5.6-luna";
4015
+ const MAX_OUTPUT_TOKENS = 4096;
4016
+ const MAX_SOURCE_DATE = 64;
4017
+ const record$2 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
4018
+ const nonEmpty = (value) => typeof value === "string" && value.length > 0 ? value : void 0;
4019
+ const displayText = (value) => {
4020
+ const text = nonEmpty(value)?.replace(/\s+/gu, " ").trim();
4021
+ if (text === void 0 || text.length === 0) return void 0;
4022
+ return text;
4023
+ };
4024
+ const boundedDisplayText = (value, maximum) => {
4025
+ const text = displayText(value);
4026
+ if (text === void 0 || text.length <= maximum) return text;
4027
+ return `${text.slice(0, maximum - 1)}…`;
4028
+ };
4029
+ function sourceOf(value) {
4030
+ if (!record$2(value)) return void 0;
4031
+ const url = nonEmpty(value.url);
4032
+ if (url === void 0) return void 0;
4033
+ let parsed;
4034
+ try {
4035
+ parsed = new URL(url);
4036
+ } catch {
4037
+ return;
4038
+ }
4039
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return void 0;
4040
+ const title = displayText(value.title) ?? parsed.hostname;
4041
+ const snippet = displayText(value.snippet);
4042
+ const publishedAt = boundedDisplayText(value.published_at, MAX_SOURCE_DATE) ?? boundedDisplayText(value.publishedAt, MAX_SOURCE_DATE);
4043
+ return {
4044
+ url,
4045
+ title,
4046
+ ...snippet === void 0 ? {} : { snippet },
4047
+ ...publishedAt === void 0 ? {} : { publishedAt }
3822
4048
  };
3823
4049
  }
3824
- function responseMetadata(value) {
3825
- const data = Array.isArray(value?.data) ? value.data[0] : void 0;
3826
- const encoded = record$2(data) ? data.b64_json : void 0;
3827
- if (typeof encoded !== "string") throw new Error("Codex returned no image data");
4050
+ function parseSearchResponse(value) {
4051
+ if (!record$2(value) || !Array.isArray(value.results)) throw new Error("Codex returned a malformed search response");
4052
+ const seen = /* @__PURE__ */ new Set();
4053
+ const sources = [];
4054
+ for (const result of value.results ?? []) {
4055
+ const source = sourceOf(result);
4056
+ if (source === void 0 || seen.has(source.url)) continue;
4057
+ seen.add(source.url);
4058
+ sources.push(source);
4059
+ }
3828
4060
  return {
3829
- encoded,
3830
- background: nonEmpty(value.background),
3831
- quality: nonEmpty(value.quality),
3832
- size: nonEmpty(value.size),
3833
- reportedModel: nonEmpty(value.model)
4061
+ sources,
4062
+ truncated: false
3834
4063
  };
3835
4064
  }
3836
- /** Create the DSH-native image-generation tool backed only by the ChatGPT subscription. */
3837
- function createCodexImageTool(options) {
3838
- const fetchImage = options.fetch ?? fetch;
3839
- const attachments = options.attachments;
3840
- return defineTool({
3841
- name: CODEX_IMAGE_TOOL_NAME,
3842
- description: "Generate or edit images only when the user asks for image output, not when merely discussing images. Uses the signed-in Codex subscription. For a new image, omit referenceImages. For edits, copy attachmentId only from the selected session image block; the host supplies its metadata. Never call read_image for an attachmentId or attachment filename. Use read_image only for an actual local file whose reference is not already in the conversation. Never substitute paths, unrelated images, or text-only generation for an edit. For numbered annotations, include the clean source and location-reference image, preserve the requested changes and coordinates in the prompt, and remove guidance markers from the result. If the intended references cannot be identified, ask rather than guessing.",
3843
- parameters: {
3844
- model: {
3845
- type: "string",
3846
- enum: Object.keys(IMAGE_MODELS),
3847
- description: "Optional image engine, independent of the conversation model. When omitted, uses the user image setting (initially gpt-image-2). The 2.5 identifiers are experimental subscription candidates; override only when explicitly requested. Never silently retry with another model."
3848
- },
3849
- prompt: {
3850
- type: "string",
3851
- required: true,
3852
- description: "A complete, production-ready description of the image to generate."
3853
- },
3854
- size: {
3855
- type: "string",
3856
- description: "Optional GPT Image 2 output size. Use auto unless the user requests an exact valid widthxheight resolution."
3857
- },
3858
- quality: {
3859
- type: "string",
3860
- enum: [
3861
- "auto",
3862
- "low",
3863
- "medium",
3864
- "high",
3865
- "xhigh",
3866
- "max"
3867
- ],
3868
- description: "Optional rendering quality. Use auto unless the user requests draft speed or final quality."
3869
- },
3870
- background: {
3871
- type: "string",
3872
- enum: [
3873
- "auto",
3874
- "transparent",
3875
- "opaque"
3876
- ],
3877
- description: "Optional background mode. Request transparent only when the user needs transparency."
3878
- },
3879
- referenceImages: {
3880
- type: "array",
3881
- description: "Optional references to 1-5 selected images. For session images provide only attachmentId; the host resolves trusted metadata. Omit only for a new image; never drop an invalid reference to bypass editing.",
3882
- items: {
3883
- type: "object",
3884
- additionalProperties: false,
3885
- properties: {
3886
- attachmentId: {
3887
- type: "string",
3888
- required: true,
3889
- description: "Copy the complete attachmentId from the image block or read_image result: sha256:<64 lowercase hex>. Never pass a workspace path, absolute path, or filename. A bare 64-character hex digest is accepted and normalized to sha256:<64 lowercase hex>."
3890
- },
3891
- mediaType: {
3892
- type: "string",
3893
- ...typeof options.getSessionMessages !== "function" ? { required: true } : {}
3894
- },
3895
- bytes: {
3896
- type: "integer",
3897
- ...typeof options.getSessionMessages !== "function" ? { required: true } : {}
3898
- },
3899
- width: {
3900
- type: "integer",
3901
- ...typeof options.getSessionMessages !== "function" ? { required: true } : {}
3902
- },
3903
- height: {
3904
- type: "integer",
3905
- ...typeof options.getSessionMessages !== "function" ? { required: true } : {}
3906
- },
3907
- name: { type: "string" },
3908
- originalDimensions: {
3909
- type: "object",
3910
- additionalProperties: false,
3911
- properties: {
3912
- width: {
3913
- type: "integer",
3914
- ...typeof options.getSessionMessages !== "function" ? { required: true } : {}
3915
- },
3916
- height: {
3917
- type: "integer",
3918
- ...typeof options.getSessionMessages !== "function" ? { required: true } : {}
3919
- }
3920
- }
3921
- }
3922
- }
3923
- }
3924
- }
3925
- },
3926
- output: {
3927
- schema: imageOutputSchema(),
3928
- render: (_args, value) => imageContent(value),
3929
- presentationMeta: (_args, value) => ({
3930
- kind: "codex-subscription-image",
3931
- schemaVersion: 1,
3932
- original: value.original,
3933
- ...value.requestedModel === void 0 ? {} : { requestedModel: value.requestedModel },
3934
- ...value.reportedModel === void 0 ? {} : { reportedModel: value.reportedModel },
3935
- ...value.requestedSize === void 0 ? {} : { requestedSize: value.requestedSize }
3936
- })
3937
- },
3938
- timeoutMs: 300 * 1e3,
3939
- isConcurrencySafe: () => false,
3940
- async execute(args, exec) {
3941
- assertImageOperation(options.getFeatures?.(), args.referenceImages !== void 0);
3942
- const defaults = readImageDefaults(options.getFeatures?.());
3943
- args = {
3944
- ...args,
3945
- model: args.model ?? defaults.imageModel,
3946
- quality: args.quality ?? defaults.imageQuality
3947
- };
3948
- const prompt = nonEmpty(args.prompt);
3949
- if (prompt === void 0) throw new Error("prompt must be a non-empty string");
3950
- const imageOptions = normalizeImageOptions(args);
3951
- const { access, accountId } = await readSubscriptionCredentials(options.getAuth, options.readCredential, exec.signal);
3952
- if (typeof access !== "string" || access.length === 0 || typeof accountId !== "string" || accountId.length === 0) throw new Error("ChatGPT subscription is not signed in");
3953
- if (!attachments.imageLimits.mediaTypes.includes("image/png")) throw new Error("This DSH installation does not accept PNG image attachments");
3954
- const maximumBytes = Math.min(attachments.imageLimits.maxImageBytes, attachments.imageLimits.maxMessageImageBytes);
3955
- const editing = args.referenceImages !== void 0;
3956
- const sessionMessages = editing && typeof options.getSessionMessages === "function" ? await options.getSessionMessages(exec.agent?.id) : void 0;
3957
- const images = editing ? await editImages(args.referenceImages, attachments, exec.signal, sessionMessages ?? (options.getSessionMessages ? [] : void 0)) : void 0;
4065
+ /** Create the DSH web provider backed only by the ChatGPT subscription search endpoint. */
4066
+ function createCodexSearchProvider(options) {
4067
+ const fetchSearch = options.fetch ?? fetch;
4068
+ return Object.freeze({
4069
+ id: CODEX_SEARCH_PROVIDER_ID,
4070
+ available: () => true,
4071
+ async search(request, signal) {
4072
+ signal?.throwIfAborted();
4073
+ const preferences = readCapabilitySettings(options.resolvePreferences?.());
4074
+ if (preferences.searchMode === "disabled") throw new WebError("Codex search is disabled in subscription settings", "WEB_PROVIDER_UNAVAILABLE");
4075
+ const { access, accountId } = await readSubscriptionCredentials(options.getAuth, options.readCredential, signal);
4076
+ if (typeof access !== "string" || access.length === 0 || typeof accountId !== "string" || accountId.length === 0) throw new WebError("ChatGPT subscription is not signed in", "WEB_PROVIDER_CREDENTIAL_MISSING");
4077
+ const model = nonEmpty(options.resolveModel?.()) ?? DEFAULT_MODEL;
4078
+ const id = nonEmpty(options.resolveSessionId?.()) ?? randomUUID();
3958
4079
  let response;
3959
- assertImageOperation(options.getFeatures?.(), editing);
3960
4080
  try {
3961
- response = await fetchImage(editing ? CODEX_IMAGE_EDIT_URL : CODEX_IMAGE_GENERATION_URL, {
4081
+ response = await fetchSearch(CODEX_SEARCH_URL, {
3962
4082
  method: "POST",
3963
4083
  redirect: "error",
3964
4084
  headers: {
@@ -3967,60 +4087,58 @@ function createCodexImageTool(options) {
3967
4087
  accept: "application/json",
3968
4088
  "content-type": "application/json",
3969
4089
  originator: "pi",
3970
- "x-codex-image-turn-id": String(exec.callId),
3971
4090
  "user-agent": USER_AGENT
3972
4091
  },
3973
4092
  body: JSON.stringify({
3974
- ...images === void 0 ? {} : { images },
3975
- prompt,
3976
- background: imageOptions.background,
3977
- model: resolveImageModel(args.model),
3978
- quality: imageOptions.quality,
3979
- size: imageOptions.size
4093
+ id,
4094
+ model,
4095
+ input: request.query,
4096
+ commands: {
4097
+ search_query: [{ q: request.query }],
4098
+ response_length: "short"
4099
+ },
4100
+ settings: {
4101
+ allowed_callers: ["direct"],
4102
+ external_web_access: preferences.searchMode === "live"
4103
+ },
4104
+ max_output_tokens: MAX_OUTPUT_TOKENS
3980
4105
  }),
3981
- signal: exec.signal
4106
+ signal
3982
4107
  });
3983
4108
  } catch (error) {
3984
- if (exec.signal.aborted) throw exec.signal.reason;
3985
- throw new Error(`Codex image ${editing ? "edit" : "generation"} request failed${requestFailureReason(error)}`, { cause: error });
4109
+ if (signal?.aborted || error?.name === "AbortError") throw new WebError("Codex search aborted", "WEB_ABORTED", { cause: error });
4110
+ throw new WebError("Codex search request failed", "WEB_PROVIDER_ERROR", { cause: error });
3986
4111
  }
3987
- if (!response.ok) {
3988
- if (response.status === 401 || response.status === 403) throw new Error("ChatGPT sign-in needs to be renewed");
3989
- if (response.status === 429) throw new Error("Codex image generation quota is unavailable");
3990
- throw new Error(`Codex image ${editing ? "edit" : "generation"} failed (HTTP ${response.status})`);
4112
+ if (!response.ok) throw response.status === 401 || response.status === 403 ? new WebError("ChatGPT sign-in needs to be renewed", "WEB_PROVIDER_CREDENTIAL_MISSING") : new WebError(`Codex search request failed (HTTP ${response.status})`, "WEB_PROVIDER_ERROR");
4113
+ let value;
4114
+ try {
4115
+ value = await response.json();
4116
+ } catch (error) {
4117
+ throw new WebError("Codex returned an unreadable search response", "WEB_PROVIDER_ERROR", { cause: error });
3991
4118
  }
3992
- const metadata = responseMetadata(await readJsonWithin(response, encodedLimit(maximumBytes) + RESPONSE_ENVELOPE_BYTES));
3993
- const data = decodeCodexPng(metadata.encoded, maximumBytes);
3994
- const sessionId = exec.agent?.id;
3995
- if (sessionId === void 0) throw new Error("Codex image generation requires a session-owned tool call");
3996
- const original = await options.originalImages.save(String(sessionId), data);
3997
- let ref;
3998
4119
  try {
3999
- ref = await attachments.saveImage({
4000
- data,
4001
- mediaType: "image/png",
4002
- name: "codex-generated.png"
4120
+ const result = parseSearchResponse(value);
4121
+ if (preferences.searchDomains.length > 0) result.sources = result.sources.filter((source) => {
4122
+ const hostname = new URL(source.url).hostname.toLowerCase();
4123
+ return preferences.searchDomains.some((domain) => hostname === domain || hostname.endsWith(`.${domain}`));
4003
4124
  });
4125
+ return result;
4004
4126
  } catch (error) {
4005
- await options.originalImages.remove(original);
4006
- throw error;
4127
+ throw new WebError("Codex returned a malformed search response", "WEB_PROVIDER_ERROR", { cause: error });
4007
4128
  }
4008
- const result = {
4009
- requestedModel: resolveImageModel(args.model),
4010
- requestedSize: imageOptions.size,
4011
- ...metadata.reportedModel === void 0 ? {} : { reportedModel: metadata.reportedModel },
4012
- image: imageReference(ref),
4013
- original,
4014
- localPath: options.originalImages.originalPath(original.assetId),
4015
- ...metadata.background === void 0 ? {} : { background: metadata.background },
4016
- ...metadata.quality === void 0 ? {} : { quality: metadata.quality },
4017
- size: `${original.width}x${original.height}`
4018
- };
4019
- if (exec.parent !== void 0) exec.deferContext(createUserMessage({
4020
- content: imageContent(result),
4021
- source: { kind: "codex-subscription-image" }
4022
- }));
4023
- return result;
4129
+ }
4130
+ });
4131
+ }
4132
+ /** Route each request by its initiating model without changing the user's explicit overrides. */
4133
+ function createCodexAutoSearchProvider(options) {
4134
+ return Object.freeze({
4135
+ id: CODEX_AUTO_SEARCH_PROVIDER_ID,
4136
+ available: () => true,
4137
+ async search(request, signal) {
4138
+ if (options.resolveModelProvider?.() === "openai-codex") return options.codex.search(request, signal);
4139
+ const provider = options.resolveDshProvider?.();
4140
+ if (provider === void 0 || provider.id === "codex-subscription-auto" || provider.id === "codex-subscription" || provider.available() !== true) throw new WebError("DSH default search is unavailable", "WEB_PROVIDER_UNAVAILABLE");
4141
+ return provider.search(request, signal);
4024
4142
  }
4025
4143
  });
4026
4144
  }
@@ -5795,8 +5913,21 @@ const publicError = (code, message) => ({
5795
5913
  details: { issues: [] }
5796
5914
  }
5797
5915
  });
5798
- function createSubscriptionRpcHandler({ authHandler, usageReader, resetCreditService, preferences, runtimeManagement, diagnosticsReader, modelCatalog, originalImages, resolveInheritedOriginal, closeConnections, onAccountChanged, onCleanupFailure }) {
5916
+ function createSubscriptionRpcHandler({ authHandler, usageReader, resetCreditService, preferences, defaultModel, runtimeManagement, diagnosticsReader, modelCatalog, originalImages, resolveInheritedOriginal, closeConnections, onAccountChanged, onCleanupFailure }) {
5799
5917
  return async (endpoint, payload, signal) => {
5918
+ if (endpoint === "default-model/status" || endpoint === "default-model/select") try {
5919
+ signal.throwIfAborted();
5920
+ if (!defaultModel) return publicError("unavailable", "The default model is unavailable");
5921
+ return {
5922
+ ok: true,
5923
+ value: endpoint === "default-model/status" ? defaultModel.status() : await defaultModel.select({ model: payload?.model })
5924
+ };
5925
+ } catch (error) {
5926
+ if (signal.aborted) throw error;
5927
+ if (error?.code === "invalid-model") return publicError("invalid-input", "Invalid default model");
5928
+ if (error?.code === "unavailable") return publicError("unavailable", endpoint === "default-model/status" ? "The default model is unavailable" : "Could not save the default model");
5929
+ return publicError("internal", endpoint === "default-model/status" ? "Could not read the default model" : "Could not save the default model");
5930
+ }
5800
5931
  if (endpoint === "storage/status" || endpoint === "storage/clear-forecast") try {
5801
5932
  signal.throwIfAborted();
5802
5933
  if (endpoint === "storage/clear-forecast") await usageReader.clear();
@@ -6085,6 +6216,10 @@ function apply(ctx, config = {}) {
6085
6216
  fetch: (input, init) => network.fetch("catalog", input, init)
6086
6217
  });
6087
6218
  const connection = createSubscriptionConnection({ resolveMode: () => settings.get().connectionMode });
6219
+ const defaultModel = createDefaultModelController({
6220
+ resolveService: () => ctx.get?.("agentDefaultModel"),
6221
+ listModels: () => modelCatalog.getModels().map((model) => model.id)
6222
+ });
6088
6223
  const compaction = createCompactionBridge({
6089
6224
  enabled: () => settings.get().compactionMode === "cloud",
6090
6225
  accountScope: async () => {
@@ -6375,7 +6510,7 @@ function apply(ctx, config = {}) {
6375
6510
  let dispose;
6376
6511
  const sync = () => {
6377
6512
  const value = settings.get();
6378
- if (value.imageSketchAgent && value.imageSketch && value.imageEditing) dispose ??= ctx.tools.register(createSketchAgentTool(sketchBridge, ctx.attachments));
6513
+ if (value.imageSketchAgent && value.imageSketch && value.imageEditing) dispose ??= ctx.tools.register(createSketchAgentTool(sketchBridge, ctx.attachments, (sessionId) => ctx.get?.("sessions")?.get?.(sessionId)?.deriveMessages?.() ?? []));
6379
6514
  else {
6380
6515
  dispose?.();
6381
6516
  dispose = void 0;
@@ -6395,6 +6530,7 @@ function apply(ctx, config = {}) {
6395
6530
  usageReader,
6396
6531
  resetCreditService,
6397
6532
  preferences,
6533
+ defaultModel,
6398
6534
  runtimeManagement,
6399
6535
  onAccountChanged: quotaRetryHandler.notifyAccountChanged,
6400
6536
  diagnosticsReader: () => createSubscriptionDiagnostics({
@@ -6436,4 +6572,4 @@ function apply(ctx, config = {}) {
6436
6572
  }, "codex-subscription: DSH-trusted account RPC"));
6437
6573
  }
6438
6574
  //#endregion
6439
- export { CODEX_IMAGE_GENERATION_URL, CODEX_IMAGE_TOOL_NAME, CODEX_RESET_CONSUME_URL, CODEX_RESET_CREDITS_URL, CODEX_USAGE_URL, CodexLoginCoordinator, Config, DshOAuthCredentialStore, apply, assertCodexAuthUrl, commandForCodexAuthUrl, createCodexAuthService, createCodexImageTool, createCodexResetCreditService, createCodexRpcHandler, createCodexUsageReader, createSearchProviderSwitcher, createSubscriptionDiagnostics, createSubscriptionRpcHandler, decodeCodexPng, inject, name, normalizeContextMode, normalizeCustomContextWindow, openCodexAuthUrl, parseCodexUsage };
6575
+ export { CODEX_IMAGE_GENERATION_URL, CODEX_IMAGE_TOOL_NAME, CODEX_RESET_CONSUME_URL, CODEX_RESET_CREDITS_URL, CODEX_USAGE_URL, CodexLoginCoordinator, Config, DEFAULT_MODEL_PROVIDER, DshOAuthCredentialStore, apply, assertCodexAuthUrl, commandForCodexAuthUrl, createCodexAuthService, createCodexImageTool, createCodexResetCreditService, createCodexRpcHandler, createCodexUsageReader, createDefaultModelController, createSearchProviderSwitcher, createSubscriptionDiagnostics, createSubscriptionRpcHandler, decodeCodexPng, inject, name, normalizeContextMode, normalizeCustomContextWindow, openCodexAuthUrl, parseCodexUsage };