dsh-codex-subscription 2.1.0 → 2.1.1-beta.1
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/README.en.md +7 -0
- package/README.md +7 -0
- package/THIRD_PARTY_NOTICES.md +2 -0
- package/lib/client.js +1839 -1361
- package/lib/index.js +1984 -1484
- package/package.json +8 -2
package/lib/index.js
CHANGED
|
@@ -1,18 +1,20 @@
|
|
|
1
|
-
import { clientRequestSchema } from "@deepseek-ai/dsh-client-connection";
|
|
2
1
|
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
3
|
-
import {
|
|
2
|
+
import { closeOpenAICodexWebSocketSessions, resetOpenAICodexWebSocketDebugStats } from "@earendil-works/pi-ai/api/openai-codex-responses";
|
|
3
|
+
import { execFile, spawn } from "node:child_process";
|
|
4
|
+
import { request } from "node:https";
|
|
5
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
6
|
+
import { PassThrough, Readable } from "node:stream";
|
|
7
|
+
import { promisify } from "node:util";
|
|
8
|
+
import { HttpsProxyAgent } from "https-proxy-agent";
|
|
9
|
+
import WebSocket from "ws";
|
|
10
|
+
import { clientRequestSchema } from "@deepseek-ai/dsh-client-connection";
|
|
4
11
|
import { lstat, mkdir, open, readFile, rename, rm, stat } from "node:fs/promises";
|
|
12
|
+
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
5
13
|
import * as dshCredentials from "@deepseek-ai/dsh-credentials";
|
|
6
14
|
import { dshHomePath, resolveDshHome } from "@deepseek-ai/dsh-home-paths";
|
|
7
15
|
import { LlmError, createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
8
16
|
import { PiAiAdapter } from "@deepseek-ai/dsh-llm-pi-ai";
|
|
9
17
|
import z from "@deepseek-ai/schemastery";
|
|
10
|
-
import { execFile, spawn } from "node:child_process";
|
|
11
|
-
import { request } from "node:https";
|
|
12
|
-
import { AsyncLocalStorage } from "node:async_hooks";
|
|
13
|
-
import { Readable } from "node:stream";
|
|
14
|
-
import { promisify } from "node:util";
|
|
15
|
-
import { HttpsProxyAgent } from "https-proxy-agent";
|
|
16
18
|
import { openaiCodexProvider as createOpenAICodexProvider } from "@earendil-works/pi-ai/providers/openai-codex";
|
|
17
19
|
import { createModels } from "@earendil-works/pi-ai";
|
|
18
20
|
import { WebError } from "@deepseek-ai/dsh-web";
|
|
@@ -286,6 +288,16 @@ const supportsCodexFastMode = (modelId) => typeof modelId === "string" && (/^gpt
|
|
|
286
288
|
//#endregion
|
|
287
289
|
//#region src/preference-fields.js
|
|
288
290
|
const PREFERENCE_FIELDS = Object.freeze({
|
|
291
|
+
connectionMode: {
|
|
292
|
+
choices: ["sse", "websocket"],
|
|
293
|
+
default: "sse",
|
|
294
|
+
error: "Invalid connection mode"
|
|
295
|
+
},
|
|
296
|
+
subagentBackend: {
|
|
297
|
+
choices: ["dsh", "codex"],
|
|
298
|
+
default: "dsh",
|
|
299
|
+
error: "Invalid subagent backend"
|
|
300
|
+
},
|
|
289
301
|
[QUICK_QUOTA_MODE_FIELD]: {
|
|
290
302
|
choices: [
|
|
291
303
|
"off",
|
|
@@ -330,1572 +342,1945 @@ const PREFERENCE_FIELDS = Object.freeze({
|
|
|
330
342
|
}
|
|
331
343
|
});
|
|
332
344
|
//#endregion
|
|
333
|
-
//#region src/
|
|
334
|
-
const
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
"
|
|
347
|
-
"
|
|
348
|
-
"reset-credit/inspect",
|
|
349
|
-
"reset-credit/prepare",
|
|
350
|
-
"reset-credit/consume",
|
|
351
|
-
"image/original/chunk",
|
|
352
|
-
"sketch/connect",
|
|
353
|
-
"sketch/poll",
|
|
354
|
-
"sketch/claim",
|
|
355
|
-
"sketch/result",
|
|
356
|
-
"sketch/disconnect"
|
|
357
|
-
]);
|
|
358
|
-
//#endregion
|
|
359
|
-
//#region src/subscription-transport.js
|
|
360
|
-
/** Exact routes stay inside DSH's authenticated /api bridge and body limit. */
|
|
361
|
-
function registerSubscriptionTransport(connection, handler) {
|
|
362
|
-
const disposers = [];
|
|
345
|
+
//#region src/oauth-network.js
|
|
346
|
+
const execFileAsync = promisify(execFile);
|
|
347
|
+
const CODEX_AUTH_HOST = "auth.openai.com";
|
|
348
|
+
const CODEX_SUBSCRIPTION_HOST = "chatgpt.com";
|
|
349
|
+
const CODEX_HOSTS = /* @__PURE__ */ new Set([CODEX_AUTH_HOST, CODEX_SUBSCRIPTION_HOST]);
|
|
350
|
+
const networkScope = new AsyncLocalStorage();
|
|
351
|
+
let activeScopes = 0;
|
|
352
|
+
let baseFetch;
|
|
353
|
+
let scopedFetch;
|
|
354
|
+
let baseWebSocket;
|
|
355
|
+
let scopedWebSocket;
|
|
356
|
+
let activeWebSocketScopes = 0;
|
|
357
|
+
function normalizeProxy(raw) {
|
|
358
|
+
if (typeof raw !== "string" || raw.trim() === "") return void 0;
|
|
359
|
+
const value = raw.trim().includes("://") ? raw.trim() : `http://${raw.trim()}`;
|
|
363
360
|
try {
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
requestBody: "buffered",
|
|
370
|
-
async fetch(request) {
|
|
371
|
-
if (request.headers.get("content-type")?.split(";", 1)[0]?.trim().toLowerCase() !== "application/json") return new Response("content type must be application/json", { status: 415 });
|
|
372
|
-
let body;
|
|
373
|
-
try {
|
|
374
|
-
body = await request.json();
|
|
375
|
-
} catch {
|
|
376
|
-
return new Response("invalid JSON", { status: 400 });
|
|
377
|
-
}
|
|
378
|
-
const envelope = clientRequestSchema.safeParse(body);
|
|
379
|
-
if (!envelope.success || envelope.data.method !== method) return new Response("invalid RPC envelope", { status: 400 });
|
|
380
|
-
let result;
|
|
381
|
-
try {
|
|
382
|
-
request.signal.throwIfAborted();
|
|
383
|
-
result = await handler(endpoint, envelope.data.payload, request.signal);
|
|
384
|
-
} catch {
|
|
385
|
-
result = {
|
|
386
|
-
ok: false,
|
|
387
|
-
error: {
|
|
388
|
-
code: "internal",
|
|
389
|
-
message: "Subscription request failed",
|
|
390
|
-
details: { issues: [] }
|
|
391
|
-
}
|
|
392
|
-
};
|
|
393
|
-
}
|
|
394
|
-
return Response.json({
|
|
395
|
-
type: "server-response",
|
|
396
|
-
rpcId: envelope.data.rpcId,
|
|
397
|
-
result
|
|
398
|
-
});
|
|
399
|
-
}
|
|
400
|
-
}));
|
|
401
|
-
}
|
|
402
|
-
} catch (error) {
|
|
403
|
-
for (const dispose of disposers.reverse()) dispose();
|
|
404
|
-
throw error;
|
|
361
|
+
const url = new URL(value);
|
|
362
|
+
if (!["http:", "https:"].includes(url.protocol) || url.hostname === "") return void 0;
|
|
363
|
+
return url.toString();
|
|
364
|
+
} catch {
|
|
365
|
+
return;
|
|
405
366
|
}
|
|
406
|
-
return () => {
|
|
407
|
-
for (const dispose of disposers.reverse()) dispose();
|
|
408
|
-
};
|
|
409
367
|
}
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
368
|
+
function bypassesProxy(hostname, port, rawNoProxy) {
|
|
369
|
+
if (typeof rawNoProxy !== "string" || rawNoProxy.trim() === "") return false;
|
|
370
|
+
return rawNoProxy.split(/[\s,]+/u).some((raw) => {
|
|
371
|
+
const entry = raw.trim().toLowerCase();
|
|
372
|
+
if (entry === "*") return true;
|
|
373
|
+
if (entry === "") return false;
|
|
374
|
+
const match = /^(.*?)(?::(\d+))?$/u.exec(entry);
|
|
375
|
+
const host = match?.[1]?.replace(/^\./u, "");
|
|
376
|
+
const entryPort = match?.[2];
|
|
377
|
+
if (!host || entryPort && entryPort !== port) return false;
|
|
378
|
+
return hostname === host || hostname.endsWith(`.${host}`);
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
function proxyFromEnvironment(env = process.env, target = new URL(`https://${CODEX_AUTH_HOST}/`)) {
|
|
382
|
+
if (bypassesProxy(target.hostname.toLowerCase(), target.port || "443", env.NO_PROXY ?? env.no_proxy)) return void 0;
|
|
383
|
+
return normalizeProxy(env.HTTPS_PROXY ?? env.https_proxy ?? env.ALL_PROXY ?? env.all_proxy);
|
|
384
|
+
}
|
|
385
|
+
function selectWindowsProxy(value) {
|
|
386
|
+
if (typeof value !== "string") return void 0;
|
|
387
|
+
const entries = value.split(";").map((item) => item.trim()).filter(Boolean);
|
|
388
|
+
const https = entries.find((item) => /^https=/iu.test(item));
|
|
389
|
+
const http = entries.find((item) => /^http=/iu.test(item));
|
|
390
|
+
const selected = (https ?? http ?? entries.find((item) => !item.includes("=")))?.replace(/^[^=]+=/u, "");
|
|
391
|
+
return normalizeProxy(selected);
|
|
392
|
+
}
|
|
393
|
+
async function windowsSystemProxy(options = {}) {
|
|
394
|
+
const run = options.execFile ?? execFileAsync;
|
|
395
|
+
const reg = `${process.env.SystemRoot ?? "C:\\Windows"}\\System32\\reg.exe`;
|
|
396
|
+
const key = "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings";
|
|
397
|
+
try {
|
|
398
|
+
const enabled = await run(reg, [
|
|
399
|
+
"query",
|
|
400
|
+
key,
|
|
401
|
+
"/v",
|
|
402
|
+
"ProxyEnable"
|
|
403
|
+
], {
|
|
404
|
+
windowsHide: true,
|
|
405
|
+
encoding: "utf8"
|
|
406
|
+
});
|
|
407
|
+
if (!/REG_DWORD\s+0x1\b/iu.test(enabled.stdout)) return void 0;
|
|
408
|
+
const configured = await run(reg, [
|
|
409
|
+
"query",
|
|
410
|
+
key,
|
|
411
|
+
"/v",
|
|
412
|
+
"ProxyServer"
|
|
413
|
+
], {
|
|
414
|
+
windowsHide: true,
|
|
415
|
+
encoding: "utf8"
|
|
416
|
+
});
|
|
417
|
+
return selectWindowsProxy(/^\s*ProxyServer\s+REG_\w+\s+(.+)$/imu.exec(configured.stdout)?.[1]);
|
|
418
|
+
} catch {
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
async function macSystemProxy(options = {}) {
|
|
423
|
+
const run = options.execFile ?? execFileAsync;
|
|
424
|
+
try {
|
|
425
|
+
const result = await run("/usr/sbin/scutil", ["--proxy"], { encoding: "utf8" });
|
|
426
|
+
if (!/^\s*HTTPSEnable\s*:\s*1\s*$/imu.test(result.stdout)) return void 0;
|
|
427
|
+
const host = /^\s*HTTPSProxy\s*:\s*(\S+)\s*$/imu.exec(result.stdout)?.[1];
|
|
428
|
+
const port = /^\s*HTTPSPort\s*:\s*(\d+)\s*$/imu.exec(result.stdout)?.[1];
|
|
429
|
+
return normalizeProxy(host && port ? `${host}:${port}` : void 0);
|
|
430
|
+
} catch {
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
async function resolveCodexOAuthProxy(options = {}) {
|
|
435
|
+
return (await resolveCodexProxy(options)).url;
|
|
436
|
+
}
|
|
437
|
+
async function resolveCodexProxy(options = {}) {
|
|
438
|
+
const target = options.target ?? new URL(`https://${CODEX_AUTH_HOST}/`);
|
|
439
|
+
const env = options.env ?? process.env;
|
|
440
|
+
if (bypassesProxy(target.hostname.toLowerCase(), target.port || "443", env.NO_PROXY ?? env.no_proxy)) return {
|
|
441
|
+
url: void 0,
|
|
442
|
+
source: "bypass"
|
|
417
443
|
};
|
|
418
|
-
const
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
return entry;
|
|
444
|
+
const envProxy = proxyFromEnvironment(env, target);
|
|
445
|
+
if (envProxy) return {
|
|
446
|
+
url: envProxy,
|
|
447
|
+
source: "environment"
|
|
423
448
|
};
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
if (!enabled()) return Promise.reject(Error("Sketch is disabled"));
|
|
504
|
-
const entry = sessions.get(sessionId);
|
|
505
|
-
if (!entry || now() - entry.seen > 1e4) return Promise.reject(Error("Switch to this session in DSH with sketch editing enabled"));
|
|
506
|
-
if (entry.tasks.size) return Promise.reject(Error("Another sketch operation is pending"));
|
|
507
|
-
if (JSON.stringify(request).length > 2e6) return Promise.reject(Error("Sketch batch is too large"));
|
|
508
|
-
return new Promise((resolve, reject) => {
|
|
509
|
-
const id = randomUUID();
|
|
510
|
-
const finish = (callback, value) => {
|
|
511
|
-
clearTimeout(timer);
|
|
512
|
-
signal?.removeEventListener("abort", abort);
|
|
513
|
-
entry.tasks.delete(id);
|
|
514
|
-
callback(value);
|
|
515
|
-
};
|
|
516
|
-
const cancel = (message) => {
|
|
517
|
-
if (entry.tasks.get(id)?.delivered) {
|
|
518
|
-
entry.cancelled.push(id);
|
|
519
|
-
if (entry.cancelled.length > 32) entry.cancelled.shift();
|
|
520
|
-
}
|
|
521
|
-
finish(reject, Error(message));
|
|
522
|
-
};
|
|
523
|
-
const abort = () => cancel("Sketch operation interrupted; inspect recentRequests before retrying");
|
|
524
|
-
const timer = setTimeout(() => cancel("Sketch response timed out; inspect recentRequests before retrying"), timeoutMs);
|
|
525
|
-
entry.tasks.set(id, {
|
|
526
|
-
request,
|
|
527
|
-
expiresAt: now() + timeoutMs,
|
|
528
|
-
delivered: false,
|
|
529
|
-
resolve: (value) => finish(resolve, value),
|
|
530
|
-
reject: (error) => finish(reject, error)
|
|
449
|
+
const platform = options.platform ?? process.platform;
|
|
450
|
+
const system = platform === "win32" ? await windowsSystemProxy(options) : platform === "darwin" ? await macSystemProxy(options) : void 0;
|
|
451
|
+
return system ? {
|
|
452
|
+
url: system,
|
|
453
|
+
source: "system"
|
|
454
|
+
} : {
|
|
455
|
+
url: void 0,
|
|
456
|
+
source: "direct"
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
function bodyBytes(body) {
|
|
460
|
+
if (body === void 0 || body === null) return void 0;
|
|
461
|
+
if (typeof body === "string") return Buffer.from(body);
|
|
462
|
+
if (body instanceof URLSearchParams) return Buffer.from(body.toString());
|
|
463
|
+
if (body instanceof Uint8Array) return Buffer.from(body);
|
|
464
|
+
throw new TypeError("Unsupported Codex OAuth request body");
|
|
465
|
+
}
|
|
466
|
+
function fetchThroughProxy(input, init, proxyUrl) {
|
|
467
|
+
const target = new URL(typeof input === "string" || input instanceof URL ? input : input.url);
|
|
468
|
+
const body = bodyBytes(init?.body);
|
|
469
|
+
const headers = new Headers(init?.headers);
|
|
470
|
+
if (body && !headers.has("content-length")) headers.set("content-length", String(body.byteLength));
|
|
471
|
+
return new Promise((resolve, reject) => {
|
|
472
|
+
const request$1 = request(target, {
|
|
473
|
+
method: init?.method ?? "GET",
|
|
474
|
+
headers: Object.fromEntries(headers.entries()),
|
|
475
|
+
agent: new HttpsProxyAgent(proxyUrl),
|
|
476
|
+
signal: init?.signal
|
|
477
|
+
}, (response) => {
|
|
478
|
+
const responseHeaders = new Headers();
|
|
479
|
+
for (const [name, value] of Object.entries(response.headers)) if (Array.isArray(value)) value.forEach((item) => responseHeaders.append(name, item));
|
|
480
|
+
else if (value !== void 0) responseHeaders.set(name, value);
|
|
481
|
+
const status = response.statusCode ?? 500;
|
|
482
|
+
const empty = init?.method === "HEAD" || [
|
|
483
|
+
204,
|
|
484
|
+
205,
|
|
485
|
+
304
|
|
486
|
+
].includes(status);
|
|
487
|
+
resolve(new Response(empty ? null : Readable.toWeb(response), {
|
|
488
|
+
status,
|
|
489
|
+
statusText: response.statusMessage,
|
|
490
|
+
headers: responseHeaders
|
|
491
|
+
}));
|
|
492
|
+
});
|
|
493
|
+
request$1.on("error", reject);
|
|
494
|
+
if (body) request$1.write(body);
|
|
495
|
+
request$1.end();
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
async function withCodexNetwork(run, options = {}) {
|
|
499
|
+
if (options.websocket && activeWebSocketScopes === 0) {
|
|
500
|
+
baseWebSocket = globalThis.WebSocket;
|
|
501
|
+
scopedWebSocket = new Proxy(baseWebSocket ?? WebSocket, { construct(target, args, newTarget) {
|
|
502
|
+
const scope = networkScope.getStore();
|
|
503
|
+
const url = new URL(String(args[0]));
|
|
504
|
+
if (!scope?.options.websocket || url.protocol !== "wss:" || url.hostname !== CODEX_SUBSCRIPTION_HOST) return Reflect.construct(target, args, newTarget);
|
|
505
|
+
const proxy = scope.options.websocketProxy;
|
|
506
|
+
return new WebSocket(args[0], {
|
|
507
|
+
...args[1],
|
|
508
|
+
...proxy ? { agent: new HttpsProxyAgent(proxy) } : {}
|
|
509
|
+
});
|
|
510
|
+
} });
|
|
511
|
+
globalThis.WebSocket = scopedWebSocket;
|
|
512
|
+
}
|
|
513
|
+
if (options.websocket) activeWebSocketScopes += 1;
|
|
514
|
+
if (activeScopes === 0) {
|
|
515
|
+
baseFetch = globalThis.fetch;
|
|
516
|
+
scopedFetch = async (input, init) => {
|
|
517
|
+
const scope = networkScope.getStore();
|
|
518
|
+
if (scope === void 0) return baseFetch(input, init);
|
|
519
|
+
const { options: scopedOptions, allowedHosts, resolved } = scope;
|
|
520
|
+
const proxyFetch = scopedOptions.fetchThroughProxy ?? fetchThroughProxy;
|
|
521
|
+
const target = new URL(typeof input === "string" || input instanceof URL ? input : input.url);
|
|
522
|
+
if (target.protocol !== "https:" || !allowedHosts.has(target.hostname)) return baseFetch(input, init);
|
|
523
|
+
let proxy = resolved.get(target.hostname);
|
|
524
|
+
if (proxy === void 0) {
|
|
525
|
+
proxy = resolveCodexProxy({
|
|
526
|
+
...scopedOptions,
|
|
527
|
+
target
|
|
531
528
|
});
|
|
532
|
-
|
|
533
|
-
|
|
529
|
+
resolved.set(target.hostname, proxy);
|
|
530
|
+
}
|
|
531
|
+
const route = await proxy;
|
|
532
|
+
scopedOptions.onRoute?.(route.source);
|
|
533
|
+
return route.url === void 0 ? baseFetch(input, init) : proxyFetch(input, init, route.url);
|
|
534
|
+
};
|
|
535
|
+
globalThis.fetch = scopedFetch;
|
|
536
|
+
}
|
|
537
|
+
activeScopes += 1;
|
|
538
|
+
const scope = {
|
|
539
|
+
options,
|
|
540
|
+
allowedHosts: options.hosts ?? CODEX_HOSTS,
|
|
541
|
+
resolved: /* @__PURE__ */ new Map()
|
|
542
|
+
};
|
|
543
|
+
try {
|
|
544
|
+
return await networkScope.run(scope, run);
|
|
545
|
+
} finally {
|
|
546
|
+
if (options.websocket && --activeWebSocketScopes === 0) {
|
|
547
|
+
if (globalThis.WebSocket === scopedWebSocket) globalThis.WebSocket = baseWebSocket;
|
|
548
|
+
baseWebSocket = void 0;
|
|
549
|
+
scopedWebSocket = void 0;
|
|
550
|
+
}
|
|
551
|
+
activeScopes -= 1;
|
|
552
|
+
if (activeScopes === 0) {
|
|
553
|
+
if (globalThis.fetch === scopedFetch) globalThis.fetch = baseFetch;
|
|
554
|
+
baseFetch = void 0;
|
|
555
|
+
scopedFetch = void 0;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
function classifyTransportError(error) {
|
|
560
|
+
const name = error?.name;
|
|
561
|
+
const code = String(error?.code ?? error?.cause?.code ?? "");
|
|
562
|
+
if (name === "AbortError" || name === "TimeoutError" || /ETIMEDOUT|UND_ERR_CONNECT_TIMEOUT/u.test(code)) return "timeout";
|
|
563
|
+
if (/ENOTFOUND|EAI_AGAIN/u.test(code)) return "dns";
|
|
564
|
+
if (/CERT_|TLS|SSL/u.test(code)) return "tls";
|
|
565
|
+
if (/ECONN|EPIPE|UND_ERR_SOCKET/u.test(code)) return "connection";
|
|
566
|
+
return "network";
|
|
567
|
+
}
|
|
568
|
+
const elapsedBucket = (elapsed) => elapsed < 1e3 ? "under-1s" : elapsed < 5e3 ? "1-5s" : elapsed < 15e3 ? "5-15s" : "over-15s";
|
|
569
|
+
function createCodexNetworkTransport(options = {}) {
|
|
570
|
+
const attempts = /* @__PURE__ */ new Map();
|
|
571
|
+
const now = options.now ?? Date.now;
|
|
572
|
+
const run = async (area, operation, connection = {}) => {
|
|
573
|
+
const startedAt = now();
|
|
574
|
+
let route = attempts.get(area)?.route ?? "direct";
|
|
575
|
+
let routed = false;
|
|
576
|
+
try {
|
|
577
|
+
const value = await withCodexNetwork(operation, {
|
|
578
|
+
...options,
|
|
579
|
+
...connection,
|
|
580
|
+
onRoute: (source) => {
|
|
581
|
+
route = source;
|
|
582
|
+
routed = true;
|
|
583
|
+
}
|
|
584
|
+
});
|
|
585
|
+
if (value instanceof Response && !value.ok) attempts.set(area, {
|
|
586
|
+
status: "failed",
|
|
587
|
+
stage: "http",
|
|
588
|
+
code: "http-error",
|
|
589
|
+
httpStatus: value.status,
|
|
590
|
+
route,
|
|
591
|
+
elapsed: elapsedBucket(now() - startedAt)
|
|
592
|
+
});
|
|
593
|
+
else if (routed || value instanceof Response) attempts.set(area, {
|
|
594
|
+
status: "ok",
|
|
595
|
+
route,
|
|
596
|
+
elapsed: elapsedBucket(now() - startedAt)
|
|
534
597
|
});
|
|
598
|
+
return value;
|
|
599
|
+
} catch (error) {
|
|
600
|
+
if (routed) attempts.set(area, {
|
|
601
|
+
status: "failed",
|
|
602
|
+
stage: "transport",
|
|
603
|
+
code: classifyTransportError(error),
|
|
604
|
+
route,
|
|
605
|
+
elapsed: elapsedBucket(now() - startedAt)
|
|
606
|
+
});
|
|
607
|
+
throw error;
|
|
608
|
+
}
|
|
609
|
+
};
|
|
610
|
+
return Object.freeze({
|
|
611
|
+
run,
|
|
612
|
+
fetch: (area, input, init) => run(area, () => globalThis.fetch(input, init)),
|
|
613
|
+
snapshot: () => Object.fromEntries([...attempts].map(([area, value]) => [area, { ...value }]))
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
//#endregion
|
|
617
|
+
//#region src/subscription-connection.js
|
|
618
|
+
function createSubscriptionConnection({ resolveMode = () => "sse", resolveProxy = resolveCodexOAuthProxy } = {}) {
|
|
619
|
+
const namespace = randomUUID();
|
|
620
|
+
const sessions = /* @__PURE__ */ new Set();
|
|
621
|
+
return {
|
|
622
|
+
async prepare(options = {}) {
|
|
623
|
+
if (resolveMode() !== "websocket") return { options: {
|
|
624
|
+
...options,
|
|
625
|
+
transport: "sse"
|
|
626
|
+
} };
|
|
627
|
+
const proxy = await resolveProxy({ target: new URL("https://chatgpt.com/") });
|
|
628
|
+
const sessionId = options.sessionId && `dsh-${createHash("sha256").update(JSON.stringify([
|
|
629
|
+
namespace,
|
|
630
|
+
options.sessionId,
|
|
631
|
+
options.apiKey,
|
|
632
|
+
proxy
|
|
633
|
+
])).digest("hex").slice(0, 56)}`;
|
|
634
|
+
if (sessionId) sessions.add(sessionId);
|
|
635
|
+
return {
|
|
636
|
+
options: {
|
|
637
|
+
...options,
|
|
638
|
+
sessionId,
|
|
639
|
+
transport: "websocket-cached",
|
|
640
|
+
websocketConnectTimeoutMs: 1e4,
|
|
641
|
+
env: {}
|
|
642
|
+
},
|
|
643
|
+
network: {
|
|
644
|
+
websocket: true,
|
|
645
|
+
websocketProxy: proxy
|
|
646
|
+
}
|
|
647
|
+
};
|
|
535
648
|
},
|
|
536
649
|
dispose() {
|
|
537
|
-
for (const
|
|
650
|
+
for (const session of sessions) {
|
|
651
|
+
closeOpenAICodexWebSocketSessions(session);
|
|
652
|
+
resetOpenAICodexWebSocketDebugStats(session);
|
|
653
|
+
}
|
|
538
654
|
sessions.clear();
|
|
539
655
|
}
|
|
540
656
|
};
|
|
541
657
|
}
|
|
542
658
|
//#endregion
|
|
543
|
-
//#region src/
|
|
544
|
-
const
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
const
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
};
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
required: true
|
|
610
|
-
},
|
|
611
|
-
control2: {
|
|
612
|
-
...point,
|
|
613
|
-
required: true
|
|
614
|
-
},
|
|
615
|
-
end: {
|
|
616
|
-
...point,
|
|
617
|
-
required: true
|
|
659
|
+
//#region src/rpc-contract.js
|
|
660
|
+
const RPC_ENDPOINTS = Object.freeze([
|
|
661
|
+
"status",
|
|
662
|
+
"login/start",
|
|
663
|
+
"login/status",
|
|
664
|
+
"login/submit",
|
|
665
|
+
"login/cancel",
|
|
666
|
+
"logout",
|
|
667
|
+
"account/select",
|
|
668
|
+
"account/remove",
|
|
669
|
+
"usage",
|
|
670
|
+
"diagnostics",
|
|
671
|
+
"preferences/status",
|
|
672
|
+
"preferences/models",
|
|
673
|
+
"preferences/update",
|
|
674
|
+
"reset-credit/inspect",
|
|
675
|
+
"reset-credit/prepare",
|
|
676
|
+
"reset-credit/consume",
|
|
677
|
+
"image/original/chunk",
|
|
678
|
+
"sketch/connect",
|
|
679
|
+
"sketch/poll",
|
|
680
|
+
"sketch/claim",
|
|
681
|
+
"sketch/result",
|
|
682
|
+
"sketch/disconnect"
|
|
683
|
+
]);
|
|
684
|
+
//#endregion
|
|
685
|
+
//#region src/subscription-transport.js
|
|
686
|
+
/** Exact routes stay inside DSH's authenticated /api bridge and body limit. */
|
|
687
|
+
function registerSubscriptionTransport(connection, handler) {
|
|
688
|
+
const disposers = [];
|
|
689
|
+
try {
|
|
690
|
+
for (const endpoint of RPC_ENDPOINTS) {
|
|
691
|
+
const method = `codex-subscription/${endpoint}`;
|
|
692
|
+
disposers.push(connection.fetch.register({
|
|
693
|
+
path: `/api/${method}`,
|
|
694
|
+
methods: ["POST"],
|
|
695
|
+
requestBody: "buffered",
|
|
696
|
+
async fetch(request) {
|
|
697
|
+
if (request.headers.get("content-type")?.split(";", 1)[0]?.trim().toLowerCase() !== "application/json") return new Response("content type must be application/json", { status: 415 });
|
|
698
|
+
let body;
|
|
699
|
+
try {
|
|
700
|
+
body = await request.json();
|
|
701
|
+
} catch {
|
|
702
|
+
return new Response("invalid JSON", { status: 400 });
|
|
703
|
+
}
|
|
704
|
+
const envelope = clientRequestSchema.safeParse(body);
|
|
705
|
+
if (!envelope.success || envelope.data.method !== method) return new Response("invalid RPC envelope", { status: 400 });
|
|
706
|
+
let result;
|
|
707
|
+
try {
|
|
708
|
+
request.signal.throwIfAborted();
|
|
709
|
+
result = await handler(endpoint, envelope.data.payload, request.signal);
|
|
710
|
+
} catch {
|
|
711
|
+
result = {
|
|
712
|
+
ok: false,
|
|
713
|
+
error: {
|
|
714
|
+
code: "internal",
|
|
715
|
+
message: "Subscription request failed",
|
|
716
|
+
details: { issues: [] }
|
|
717
|
+
}
|
|
718
|
+
};
|
|
719
|
+
}
|
|
720
|
+
return Response.json({
|
|
721
|
+
type: "server-response",
|
|
722
|
+
rpcId: envelope.data.rpcId,
|
|
723
|
+
result
|
|
724
|
+
});
|
|
618
725
|
}
|
|
619
|
-
})
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
"rectangle",
|
|
630
|
-
"circle",
|
|
631
|
-
"ellipse",
|
|
632
|
-
"polygon",
|
|
633
|
-
"bezier",
|
|
634
|
-
"eraser"
|
|
635
|
-
]
|
|
636
|
-
},
|
|
637
|
-
...style,
|
|
638
|
-
action: {
|
|
639
|
-
type: "string",
|
|
640
|
-
enum: [
|
|
641
|
-
"update",
|
|
642
|
-
"duplicate",
|
|
643
|
-
"delete",
|
|
644
|
-
"add",
|
|
645
|
-
"select",
|
|
646
|
-
"rename",
|
|
647
|
-
"visible",
|
|
648
|
-
"up",
|
|
649
|
-
"down",
|
|
650
|
-
"clear"
|
|
651
|
-
]
|
|
652
|
-
},
|
|
653
|
-
value: string,
|
|
654
|
-
ratio: {
|
|
655
|
-
type: "string",
|
|
656
|
-
enum: [
|
|
657
|
-
"1:1",
|
|
658
|
-
"4:3",
|
|
659
|
-
"3:4",
|
|
660
|
-
"16:9",
|
|
661
|
-
"9:16"
|
|
662
|
-
]
|
|
663
|
-
},
|
|
664
|
-
patch: object(style),
|
|
665
|
-
transform: object({
|
|
666
|
-
dx: number,
|
|
667
|
-
dy: number,
|
|
668
|
-
scaleX: number,
|
|
669
|
-
scaleY: number
|
|
670
|
-
})
|
|
671
|
-
})
|
|
672
|
-
};
|
|
726
|
+
}));
|
|
727
|
+
}
|
|
728
|
+
} catch (error) {
|
|
729
|
+
for (const dispose of disposers.reverse()) dispose();
|
|
730
|
+
throw error;
|
|
731
|
+
}
|
|
732
|
+
return () => {
|
|
733
|
+
for (const dispose of disposers.reverse()) dispose();
|
|
734
|
+
};
|
|
735
|
+
}
|
|
673
736
|
//#endregion
|
|
674
|
-
//#region src/
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
737
|
+
//#region src/subagent-auth.js
|
|
738
|
+
const AUTH_ERROR = "Codex subscription authorization failed; check the selected account";
|
|
739
|
+
/** Keep refresh rotation in the plugin's existing serialized credential store. */
|
|
740
|
+
async function createSubagentTokens({ resolveAuth, store, refresh, signal }) {
|
|
741
|
+
signal.throwIfAborted();
|
|
742
|
+
await resolveAuth();
|
|
743
|
+
const initial = await store.read("openai-codex", { signal });
|
|
744
|
+
if (initial?.type !== "oauth" || !initial.accountId || !initial.access) throw new Error(AUTH_ERROR);
|
|
745
|
+
const accountId = initial.accountId;
|
|
746
|
+
let access = initial.access;
|
|
747
|
+
return async (previousAccountId, forceRefresh = false) => {
|
|
748
|
+
signal.throwIfAborted();
|
|
749
|
+
if (previousAccountId !== void 0 && previousAccountId !== accountId) throw new Error(AUTH_ERROR);
|
|
750
|
+
let credential;
|
|
751
|
+
if (previousAccountId !== void 0 || forceRefresh) {
|
|
752
|
+
const rejectedAccess = access;
|
|
753
|
+
credential = await store.modify("openai-codex", async (current) => {
|
|
754
|
+
if (current?.accountId !== accountId) throw new Error(AUTH_ERROR);
|
|
755
|
+
if (current.access !== rejectedAccess) return current;
|
|
756
|
+
const next = await refresh(current);
|
|
757
|
+
if (next?.accountId !== accountId) throw new Error(AUTH_ERROR);
|
|
758
|
+
return next;
|
|
759
|
+
}, { signal });
|
|
760
|
+
} else credential = await store.read("openai-codex", { signal });
|
|
761
|
+
signal.throwIfAborted();
|
|
762
|
+
if (credential?.accountId !== accountId || !credential.access) throw new Error(AUTH_ERROR);
|
|
763
|
+
access = credential.access;
|
|
764
|
+
return {
|
|
765
|
+
accessToken: access,
|
|
766
|
+
chatgptAccountId: accountId
|
|
767
|
+
};
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
/**
|
|
771
|
+
* Authenticate the official DSH provider's private app-server connection.
|
|
772
|
+
* DSH still owns framing, process containment, turns, tool approvals and disposal.
|
|
773
|
+
* Only the documented external-auth handshake and thread policy are adapted.
|
|
774
|
+
* No token is passed in argv, environment, logs or a second auth.json.
|
|
775
|
+
*/
|
|
776
|
+
function authenticatedSubagentChild(child, { Transport, getTokens, thread, signal }) {
|
|
777
|
+
const stdin = new PassThrough();
|
|
778
|
+
const stdout = new PassThrough();
|
|
779
|
+
const host = new Transport(stdin, stdout);
|
|
780
|
+
const server = new Transport(child.stdout, child.stdin);
|
|
781
|
+
let initialized = false;
|
|
782
|
+
let authenticated;
|
|
783
|
+
let closed = false;
|
|
784
|
+
const close = () => {
|
|
785
|
+
if (closed) return;
|
|
786
|
+
closed = true;
|
|
787
|
+
host.close();
|
|
788
|
+
server.close();
|
|
789
|
+
stdin.destroy();
|
|
790
|
+
stdout.end();
|
|
791
|
+
};
|
|
792
|
+
const authorize = async (previousAccountId, refresh = false) => {
|
|
793
|
+
try {
|
|
794
|
+
return await getTokens(previousAccountId, refresh);
|
|
795
|
+
} catch {
|
|
796
|
+
throw new Error(AUTH_ERROR);
|
|
797
|
+
}
|
|
798
|
+
};
|
|
799
|
+
const login = () => authenticated ??= (async () => {
|
|
800
|
+
const tokens = await authorize();
|
|
801
|
+
signal.throwIfAborted();
|
|
802
|
+
try {
|
|
803
|
+
return await server.request("account/login/start", {
|
|
804
|
+
type: "chatgptAuthTokens",
|
|
805
|
+
...tokens
|
|
806
|
+
}, AbortSignal.any([signal, AbortSignal.timeout(1e4)]));
|
|
807
|
+
} catch {
|
|
808
|
+
throw new Error(AUTH_ERROR);
|
|
809
|
+
}
|
|
810
|
+
})();
|
|
811
|
+
host.onRequest(async (method, params) => {
|
|
812
|
+
if (method === "initialize") {
|
|
813
|
+
const result = await server.request(method, {
|
|
814
|
+
...params,
|
|
815
|
+
capabilities: {
|
|
816
|
+
...params.capabilities,
|
|
817
|
+
experimentalApi: true
|
|
818
|
+
}
|
|
819
|
+
}, signal);
|
|
820
|
+
initialized = true;
|
|
821
|
+
return result;
|
|
822
|
+
}
|
|
823
|
+
if (method === "thread/start") {
|
|
824
|
+
if (!initialized) throw new Error("Codex initialization incomplete");
|
|
825
|
+
await login();
|
|
826
|
+
return server.request(method, {
|
|
827
|
+
...params,
|
|
828
|
+
...thread
|
|
829
|
+
}, signal);
|
|
830
|
+
}
|
|
831
|
+
return server.request(method, params, signal);
|
|
832
|
+
});
|
|
833
|
+
host.onNotification((method, params) => server.notify(method, params));
|
|
834
|
+
server.onRequest((method, params) => method === "account/chatgptAuthTokens/refresh" ? authorize(params.previousAccountId ?? void 0, true) : host.request(method, params, signal));
|
|
835
|
+
server.onNotification((method, params) => host.notify(method, params));
|
|
836
|
+
host.start();
|
|
837
|
+
server.start();
|
|
838
|
+
child.done.then(close, close);
|
|
839
|
+
return new Proxy(child, { get(target, key) {
|
|
840
|
+
if (key === "stdin") return stdin;
|
|
841
|
+
if (key === "stdout") return stdout;
|
|
842
|
+
const value = Reflect.get(target, key, target);
|
|
843
|
+
return typeof value === "function" ? value.bind(target) : value;
|
|
844
|
+
} });
|
|
845
|
+
}
|
|
846
|
+
//#endregion
|
|
847
|
+
//#region src/subagent-backend.js
|
|
848
|
+
const SUBAGENT_PROVIDER = "codex-subscription-subagent";
|
|
849
|
+
const MODES = /* @__PURE__ */ new Set([
|
|
850
|
+
"read-only",
|
|
851
|
+
"workspace-write",
|
|
852
|
+
"danger-full-access"
|
|
853
|
+
]);
|
|
854
|
+
function subagentThreadPolicy(parent, policy) {
|
|
855
|
+
if (!MODES.has(policy?.mode)) throw new Error("DSH subagent sandbox policy is unavailable");
|
|
856
|
+
const selected = parent.session.requestHeader?.()?.config ?? parent.options ?? {};
|
|
857
|
+
const subscription = selected.provider === "openai-codex" && typeof selected.model === "string";
|
|
858
|
+
return {
|
|
859
|
+
model: subscription ? selected.model : "gpt-5.6-luna",
|
|
860
|
+
modelProvider: "openai",
|
|
861
|
+
approvalPolicy: "never",
|
|
862
|
+
sandbox: policy.mode,
|
|
863
|
+
config: { model_reasoning_effort: subscription ? selected.reasoningEffort ?? "low" : "low" }
|
|
864
|
+
};
|
|
865
|
+
}
|
|
866
|
+
/** Reuse the official DSH process/turn provider; keep only subscription auth here. */
|
|
867
|
+
function createSubscriptionSubagent({ ctx, nativeHome, resolveAuth, store, refresh, loadRuntime }) {
|
|
868
|
+
let runtime;
|
|
869
|
+
const load = () => runtime ??= loadRuntime().catch((error) => {
|
|
870
|
+
runtime = void 0;
|
|
871
|
+
throw error;
|
|
872
|
+
});
|
|
873
|
+
const active = /* @__PURE__ */ new Set();
|
|
874
|
+
let disposed = false;
|
|
875
|
+
return {
|
|
876
|
+
provider: {
|
|
877
|
+
name: SUBAGENT_PROVIDER,
|
|
878
|
+
capabilities: {
|
|
879
|
+
agentOptions: false,
|
|
880
|
+
outputSchema: false,
|
|
881
|
+
depthLimit: false,
|
|
882
|
+
toolFilter: false,
|
|
883
|
+
persona: false
|
|
722
884
|
},
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
885
|
+
inheritsParentContext: false,
|
|
886
|
+
async start(request) {
|
|
887
|
+
if (disposed) throw new Error("Codex subagent is unavailable");
|
|
888
|
+
const controller = new AbortController();
|
|
889
|
+
active.add(controller);
|
|
890
|
+
const signal = AbortSignal.any([request.signal, controller.signal]);
|
|
891
|
+
let run;
|
|
892
|
+
try {
|
|
893
|
+
const thread = subagentThreadPolicy(request.parent, ctx.sandboxPolicy.resolve({ session: request.parent.session }));
|
|
894
|
+
const getTokens = await createSubagentTokens({
|
|
895
|
+
resolveAuth,
|
|
896
|
+
store,
|
|
897
|
+
refresh,
|
|
898
|
+
signal
|
|
899
|
+
});
|
|
900
|
+
const { official, Transport } = await load();
|
|
901
|
+
const proxy = await resolveCodexOAuthProxy({ target: new URL("https://chatgpt.com/") });
|
|
902
|
+
signal.throwIfAborted();
|
|
903
|
+
await mkdir(nativeHome, { recursive: true });
|
|
904
|
+
const env = {
|
|
905
|
+
CODEX_HOME: nativeHome,
|
|
906
|
+
...proxy ? {
|
|
907
|
+
HTTPS_PROXY: proxy,
|
|
908
|
+
HTTP_PROXY: proxy,
|
|
909
|
+
ALL_PROXY: proxy
|
|
910
|
+
} : {}
|
|
911
|
+
};
|
|
912
|
+
let delegate;
|
|
913
|
+
official.apply({
|
|
914
|
+
subagents: { registerProvider(value) {
|
|
915
|
+
delegate = value;
|
|
916
|
+
} },
|
|
917
|
+
subprocess: { spawn: (spec) => authenticatedSubagentChild(ctx.subprocess.spawn({
|
|
918
|
+
...spec,
|
|
919
|
+
env: {
|
|
920
|
+
...spec.env,
|
|
921
|
+
...env
|
|
922
|
+
}
|
|
923
|
+
}), {
|
|
924
|
+
Transport,
|
|
925
|
+
getTokens,
|
|
926
|
+
thread,
|
|
927
|
+
signal
|
|
928
|
+
}) },
|
|
929
|
+
logger: { warn: () => ctx.logger?.warn?.("Codex subscription subtask failed") }
|
|
930
|
+
}, {
|
|
931
|
+
model: thread.model,
|
|
932
|
+
env,
|
|
933
|
+
permissionMode: "never",
|
|
934
|
+
disposeGraceMs: 1e3
|
|
935
|
+
});
|
|
936
|
+
run = await delegate.start({
|
|
937
|
+
...request,
|
|
938
|
+
signal
|
|
939
|
+
});
|
|
940
|
+
const result = run.result.finally(() => active.delete(controller));
|
|
941
|
+
return {
|
|
942
|
+
...run,
|
|
943
|
+
result,
|
|
944
|
+
async dispose() {
|
|
945
|
+
controller.abort();
|
|
946
|
+
await run.dispose();
|
|
947
|
+
active.delete(controller);
|
|
948
|
+
}
|
|
949
|
+
};
|
|
950
|
+
} catch (error) {
|
|
951
|
+
controller.abort();
|
|
952
|
+
await run?.dispose();
|
|
953
|
+
active.delete(controller);
|
|
954
|
+
throw error;
|
|
955
|
+
}
|
|
726
956
|
}
|
|
727
957
|
},
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
const
|
|
732
|
-
if (typeof sessionId !== "string") throw Error("A session-owned sketch call is required");
|
|
733
|
-
const request = { ...args };
|
|
734
|
-
if (args.action === "apply") try {
|
|
735
|
-
request.commands = typeof args.commands === "string" ? JSON.parse(args.commands) : args.commands;
|
|
736
|
-
if (!Array.isArray(request.commands)) throw Error();
|
|
737
|
-
} catch {
|
|
738
|
-
throw Error("commands must be a native array or JSON array string");
|
|
739
|
-
}
|
|
740
|
-
const value = await bridge.request(sessionId, request, exec.signal);
|
|
741
|
-
if (value.png) {
|
|
742
|
-
if (!/^data:image\/png;base64,/.test(value.png) || value.png.length > 8 * 1024 * 1024) throw Error("Invalid sketch preview");
|
|
743
|
-
const image = await attachments.saveImage({
|
|
744
|
-
data: new Uint8Array(Buffer.from(value.png.split(",")[1], "base64")),
|
|
745
|
-
mediaType: "image/png",
|
|
746
|
-
name: "sketch-preview.png"
|
|
747
|
-
});
|
|
748
|
-
const { png, ...snapshot } = value;
|
|
749
|
-
return {
|
|
750
|
-
...snapshot,
|
|
751
|
-
image
|
|
752
|
-
};
|
|
753
|
-
}
|
|
754
|
-
return value;
|
|
755
|
-
},
|
|
756
|
-
output: {
|
|
757
|
-
schema: {
|
|
758
|
-
type: "object",
|
|
759
|
-
additionalProperties: true
|
|
760
|
-
},
|
|
761
|
-
render: (_args, value) => [{
|
|
762
|
-
type: "text",
|
|
763
|
-
text: JSON.stringify({
|
|
764
|
-
...value,
|
|
765
|
-
image: void 0
|
|
766
|
-
})
|
|
767
|
-
}, ...value.image ? [{
|
|
768
|
-
type: "image",
|
|
769
|
-
attachment: value.image
|
|
770
|
-
}] : []]
|
|
958
|
+
prepare: load,
|
|
959
|
+
dispose() {
|
|
960
|
+
disposed = true;
|
|
961
|
+
for (const controller of active) controller.abort();
|
|
771
962
|
}
|
|
772
|
-
}
|
|
963
|
+
};
|
|
773
964
|
}
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
let
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
} });
|
|
788
|
-
}
|
|
965
|
+
/** Change only standard independent spawn tools; leave fork/custom tools untouched. */
|
|
966
|
+
function createSubagentBackendSwitcher({ entries, prepare, persist }) {
|
|
967
|
+
const originals = /* @__PURE__ */ new Map();
|
|
968
|
+
let selected = "dsh";
|
|
969
|
+
let tail = Promise.resolve();
|
|
970
|
+
let disposed = false;
|
|
971
|
+
const standard = (entry, config) => entry?.options?.name === "@deepseek-ai/dsh-tool-subagent" && config?.provider === "spawn" && !config.agentOptions && !config.persona && !config.toolFilter;
|
|
972
|
+
const convert = (config) => ({
|
|
973
|
+
...config,
|
|
974
|
+
provider: SUBAGENT_PROVIDER,
|
|
975
|
+
modelSelectionSettings: false,
|
|
976
|
+
backgroundMode: "one-shot",
|
|
977
|
+
maxDepth: "provider-managed"
|
|
789
978
|
});
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
const
|
|
796
|
-
|
|
797
|
-
const
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
}
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
979
|
+
const configure = (fiber, config) => {
|
|
980
|
+
if (!standard(fiber.entry, config)) return config;
|
|
981
|
+
if (!originals.has(fiber)) originals.set(fiber, { ...config });
|
|
982
|
+
return selected === "codex" ? convert(config) : config;
|
|
983
|
+
};
|
|
984
|
+
const select = (mode) => {
|
|
985
|
+
if (!["dsh", "codex"].includes(mode)) return Promise.reject(/* @__PURE__ */ new Error("Invalid subagent backend"));
|
|
986
|
+
const next = tail.catch(() => {}).then(async () => {
|
|
987
|
+
if (disposed) throw new Error("Subagent backend is unavailable");
|
|
988
|
+
const all = [...entries()];
|
|
989
|
+
for (const entry of all) if (entry.fiber && standard(entry, entry.fiber.config) && !originals.has(entry.fiber)) originals.set(entry.fiber, { ...entry.fiber.config });
|
|
990
|
+
if (mode === "codex") {
|
|
991
|
+
if (!originals.size && !all.some((entry) => standard(entry, entry.options?.config))) throw new Error("No standard DSH independent subagent tool is available");
|
|
992
|
+
await prepare();
|
|
993
|
+
}
|
|
994
|
+
const changed = [];
|
|
995
|
+
const previous = selected;
|
|
996
|
+
selected = mode;
|
|
997
|
+
try {
|
|
998
|
+
for (const [fiber, original] of originals) {
|
|
999
|
+
if (fiber.entry && fiber.entry.fiber !== fiber) {
|
|
1000
|
+
originals.delete(fiber);
|
|
1001
|
+
continue;
|
|
1002
|
+
}
|
|
1003
|
+
const before = { ...fiber.config };
|
|
1004
|
+
changed.push({
|
|
1005
|
+
fiber,
|
|
1006
|
+
before
|
|
1007
|
+
});
|
|
1008
|
+
await fiber.update(mode === "codex" ? convert(original) : original, true);
|
|
1009
|
+
}
|
|
1010
|
+
await persist?.(mode);
|
|
1011
|
+
} catch (error) {
|
|
1012
|
+
selected = previous;
|
|
1013
|
+
for (const { fiber, before } of changed.reverse()) await fiber.update(before, true);
|
|
1014
|
+
throw error;
|
|
1015
|
+
}
|
|
1016
|
+
});
|
|
1017
|
+
tail = next;
|
|
1018
|
+
return next;
|
|
1019
|
+
};
|
|
826
1020
|
return {
|
|
827
|
-
|
|
828
|
-
|
|
1021
|
+
select,
|
|
1022
|
+
configure,
|
|
1023
|
+
async dispose() {
|
|
1024
|
+
await tail.catch(() => {});
|
|
1025
|
+
disposed = true;
|
|
1026
|
+
selected = "dsh";
|
|
1027
|
+
for (const [fiber, original] of originals) if (fiber.config?.provider === "codex-subscription-subagent") await fiber.update(original, true);
|
|
1028
|
+
originals.clear();
|
|
1029
|
+
}
|
|
829
1030
|
};
|
|
830
1031
|
}
|
|
831
|
-
function
|
|
832
|
-
|
|
833
|
-
return clone$1(value);
|
|
834
|
-
}
|
|
835
|
-
function parseOAuthCredential$1(value) {
|
|
836
|
-
try {
|
|
837
|
-
return assertOAuthCredential$1(JSON.parse(value));
|
|
838
|
-
} catch (error) {
|
|
839
|
-
if (error?.message === "Codex account vault received a malformed OAuth credential") throw error;
|
|
840
|
-
throw new Error("Codex account vault contains malformed OAuth JSON", { cause: error });
|
|
841
|
-
}
|
|
842
|
-
}
|
|
843
|
-
function normalizeLabel(value) {
|
|
844
|
-
if (typeof value !== "string") throw new Error("Codex account label must be text");
|
|
845
|
-
const label = value.trim().replace(/\s+/gu, " ");
|
|
846
|
-
if (label.length === 0 || label.length > 48) throw new Error("Codex account label must contain 1 to 48 characters");
|
|
847
|
-
return label;
|
|
848
|
-
}
|
|
849
|
-
function assertVaultRecord(record) {
|
|
850
|
-
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");
|
|
851
|
-
const ids = /* @__PURE__ */ new Set();
|
|
852
|
-
const accounts = record.payload.accounts.map((account) => {
|
|
853
|
-
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");
|
|
854
|
-
ids.add(account.id);
|
|
855
|
-
return {
|
|
856
|
-
id: account.id,
|
|
857
|
-
label: normalizeLabel(account.label),
|
|
858
|
-
credential: sanitizeOAuthCredential(account.credential)
|
|
859
|
-
};
|
|
860
|
-
});
|
|
861
|
-
if (!ids.has(record.payload.activeId)) throw new Error("Codex account vault active account is missing");
|
|
862
|
-
const legacyAccountId = record.payload.legacyAccountId;
|
|
863
|
-
if (legacyAccountId !== void 0 && !ids.has(legacyAccountId)) throw new Error("Codex account vault legacy account is missing");
|
|
1032
|
+
async function loadSubagentRuntime() {
|
|
1033
|
+
const [official, { JsonRpcLineTransport: Transport }] = await Promise.all([import("@deepseek-ai/dsh-subagent-codex"), import("@deepseek-ai/dsh-sdk-protocol")]);
|
|
864
1034
|
return {
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
legacyAccountId,
|
|
868
|
-
accounts
|
|
1035
|
+
official,
|
|
1036
|
+
Transport
|
|
869
1037
|
};
|
|
870
1038
|
}
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
if (credentials === void 0 || credentials === null || typeof credentials.readRecord !== "function" || typeof credentials.modifyRecord !== "function") throw new Error("Codex multi-account requires DSH credential records");
|
|
910
|
-
this.credentials = credentials;
|
|
911
|
-
this.key = options.key;
|
|
912
|
-
this.legacyRef = options.legacyRef;
|
|
913
|
-
this.legacyRefs = Object.freeze([...options.legacyRefs ?? []]);
|
|
914
|
-
this.createId = options.createId ?? randomUUID;
|
|
915
|
-
this.onLegacySyncFailure = options.onLegacySyncFailure ?? (() => {});
|
|
916
|
-
}
|
|
917
|
-
#enqueue(operation) {
|
|
918
|
-
const current = this.#tail.catch(() => void 0).then(operation);
|
|
919
|
-
this.#tail = current.catch(() => void 0);
|
|
920
|
-
return current;
|
|
921
|
-
}
|
|
922
|
-
async #legacyCredential() {
|
|
923
|
-
for (const ref of [this.legacyRef, ...this.legacyRefs]) {
|
|
924
|
-
const hit = await this.credentials.resolve(ref);
|
|
925
|
-
if (hit?.value === void 0 || hit.value === "") continue;
|
|
926
|
-
return {
|
|
927
|
-
ref,
|
|
928
|
-
credential: parseOAuthCredential$1(hit.value)
|
|
929
|
-
};
|
|
930
|
-
}
|
|
931
|
-
}
|
|
932
|
-
async #ensurePayload() {
|
|
933
|
-
const existing = await this.credentials.readRecord(this.key);
|
|
934
|
-
if (existing !== void 0) return assertVaultRecord(existing);
|
|
935
|
-
const legacy = await this.#legacyCredential();
|
|
936
|
-
if (legacy === void 0) return void 0;
|
|
937
|
-
const id = this.createId();
|
|
938
|
-
return assertVaultRecord(await this.credentials.modifyRecord(this.key, (current) => {
|
|
939
|
-
if (current !== void 0) return Promise.resolve(current);
|
|
940
|
-
return Promise.resolve(grant({
|
|
941
|
-
version: VERSION,
|
|
942
|
-
activeId: id,
|
|
943
|
-
legacyAccountId: id,
|
|
944
|
-
accounts: [{
|
|
945
|
-
id,
|
|
946
|
-
label: DEFAULT_LABEL,
|
|
947
|
-
credential: legacy.credential
|
|
948
|
-
}]
|
|
949
|
-
}));
|
|
950
|
-
}));
|
|
951
|
-
}
|
|
952
|
-
async #modifyPayload(update) {
|
|
953
|
-
await this.#ensurePayload();
|
|
954
|
-
let previousLegacy;
|
|
955
|
-
const payload = assertVaultRecord(await this.credentials.modifyRecord(this.key, async (current) => {
|
|
956
|
-
if (current === void 0) throw new Error("Codex account vault is not signed in");
|
|
957
|
-
const payload = assertVaultRecord(current);
|
|
958
|
-
previousLegacy = payload.accounts.find((account) => account.id === payload.legacyAccountId)?.credential;
|
|
959
|
-
const next = await update(clone$1(payload));
|
|
960
|
-
return grant(next);
|
|
961
|
-
}));
|
|
962
|
-
const legacy = payload.accounts.find((account) => account.id === payload.legacyAccountId)?.credential;
|
|
963
|
-
try {
|
|
964
|
-
if (legacy === void 0) {
|
|
965
|
-
if (previousLegacy !== void 0) await this.credentials.unset(this.legacyRef);
|
|
966
|
-
} else if (JSON.stringify(legacy) !== JSON.stringify(previousLegacy)) await this.credentials.set(this.legacyRef, JSON.stringify(legacy));
|
|
967
|
-
} catch {
|
|
968
|
-
this.onLegacySyncFailure();
|
|
969
|
-
}
|
|
970
|
-
return payload;
|
|
971
|
-
}
|
|
972
|
-
list() {
|
|
973
|
-
return this.#enqueue(async () => {
|
|
974
|
-
const payload = await this.#ensurePayload();
|
|
975
|
-
if (payload === void 0) return [];
|
|
976
|
-
return payload.accounts.map((account) => ({
|
|
977
|
-
id: account.id,
|
|
978
|
-
label: account.label,
|
|
979
|
-
active: account.id === payload.activeId,
|
|
980
|
-
expiresAt: account.credential.expires,
|
|
981
|
-
...account.credential.email === void 0 ? {} : { email: account.credential.email }
|
|
982
|
-
}));
|
|
983
|
-
});
|
|
984
|
-
}
|
|
985
|
-
readActive() {
|
|
986
|
-
return this.#enqueue(async () => {
|
|
987
|
-
const payload = await this.#ensurePayload();
|
|
988
|
-
return clone$1(payload?.accounts.find((account) => account.id === payload.activeId)?.credential);
|
|
989
|
-
});
|
|
990
|
-
}
|
|
991
|
-
activeId() {
|
|
992
|
-
return this.#enqueue(async () => (await this.#ensurePayload())?.activeId);
|
|
993
|
-
}
|
|
994
|
-
add(label, credential) {
|
|
995
|
-
return this.#enqueue(async () => {
|
|
996
|
-
const normalizedLabel = normalizeLabel(label);
|
|
997
|
-
const validated = sanitizeOAuthCredential(credential);
|
|
998
|
-
await this.#ensurePayload();
|
|
999
|
-
const id = this.createId();
|
|
1000
|
-
const account = (await this.#modifyPayload((current) => ({
|
|
1001
|
-
...current,
|
|
1002
|
-
activeId: id,
|
|
1003
|
-
accounts: [...current.accounts, {
|
|
1004
|
-
id,
|
|
1005
|
-
label: normalizedLabel,
|
|
1006
|
-
credential: validated
|
|
1007
|
-
}]
|
|
1008
|
-
}))).accounts.find((candidate) => candidate.id === id);
|
|
1009
|
-
return {
|
|
1010
|
-
id,
|
|
1011
|
-
label: account.label,
|
|
1012
|
-
active: true,
|
|
1013
|
-
expiresAt: account.credential.expires,
|
|
1014
|
-
...account.credential.email === void 0 ? {} : { email: account.credential.email }
|
|
1015
|
-
};
|
|
1016
|
-
});
|
|
1017
|
-
}
|
|
1018
|
-
select(id) {
|
|
1019
|
-
return this.#enqueue(async () => {
|
|
1020
|
-
await this.#modifyPayload((current) => {
|
|
1021
|
-
if (!current.accounts.some((account) => account.id === id)) throw new Error("Unknown Codex account");
|
|
1022
|
-
return {
|
|
1023
|
-
...current,
|
|
1024
|
-
activeId: id
|
|
1025
|
-
};
|
|
1026
|
-
});
|
|
1027
|
-
});
|
|
1028
|
-
}
|
|
1029
|
-
modifyActive(update) {
|
|
1030
|
-
return this.#enqueue(async () => {
|
|
1031
|
-
if (await this.#ensurePayload() === void 0) {
|
|
1032
|
-
const initial = await update(void 0);
|
|
1033
|
-
if (initial === void 0) return void 0;
|
|
1034
|
-
const credential = sanitizeOAuthCredential(initial);
|
|
1035
|
-
await this.credentials.set(this.legacyRef, JSON.stringify(credential));
|
|
1036
|
-
await this.#ensurePayload();
|
|
1037
|
-
return clone$1(credential);
|
|
1038
|
-
}
|
|
1039
|
-
let result;
|
|
1040
|
-
await this.#modifyPayload(async (current) => {
|
|
1041
|
-
const index = current.accounts.findIndex((account) => account.id === current.activeId);
|
|
1042
|
-
const previous = clone$1(current.accounts[index].credential);
|
|
1043
|
-
const next = await update(previous);
|
|
1044
|
-
if (next === void 0) {
|
|
1045
|
-
result = previous;
|
|
1046
|
-
return current;
|
|
1039
|
+
//#endregion
|
|
1040
|
+
//#region src/sketch-agent-bridge.js
|
|
1041
|
+
function createSketchAgentBridge({ enabled, now = Date.now, timeoutMs = 2e4 }) {
|
|
1042
|
+
const sessions = /* @__PURE__ */ new Map();
|
|
1043
|
+
const fail = (entry, message) => {
|
|
1044
|
+
for (const task of entry.tasks.values()) task.reject(Error(message));
|
|
1045
|
+
entry.tasks.clear();
|
|
1046
|
+
};
|
|
1047
|
+
const find = (payload) => {
|
|
1048
|
+
const entry = sessions.get(payload.sessionId);
|
|
1049
|
+
if (!entry || entry.token !== payload.token || now() - entry.seen > 1e4) throw Error("Sketch connection expired");
|
|
1050
|
+
entry.seen = now();
|
|
1051
|
+
return entry;
|
|
1052
|
+
};
|
|
1053
|
+
return {
|
|
1054
|
+
async rpc(endpoint, payload) {
|
|
1055
|
+
try {
|
|
1056
|
+
if (!enabled()) throw Error("Sketch is disabled");
|
|
1057
|
+
if (!payload || typeof payload.sessionId !== "string" || !payload.sessionId.length || payload.sessionId.length > 200) throw Error("Invalid session");
|
|
1058
|
+
if (endpoint === "sketch/connect") {
|
|
1059
|
+
for (const [id, entry] of sessions) if (now() - entry.seen >= 1e4) {
|
|
1060
|
+
fail(entry, "Sketch connection expired");
|
|
1061
|
+
sessions.delete(id);
|
|
1062
|
+
}
|
|
1063
|
+
const previous = sessions.get(payload.sessionId);
|
|
1064
|
+
if (previous && now() - previous.seen < 1e4) throw Error("Another board is connected to this session");
|
|
1065
|
+
if (previous) fail(previous, "Sketch connection replaced");
|
|
1066
|
+
const entry = {
|
|
1067
|
+
token: randomUUID(),
|
|
1068
|
+
seen: now(),
|
|
1069
|
+
tasks: /* @__PURE__ */ new Map(),
|
|
1070
|
+
cancelled: []
|
|
1071
|
+
};
|
|
1072
|
+
sessions.set(payload.sessionId, entry);
|
|
1073
|
+
return {
|
|
1074
|
+
ok: true,
|
|
1075
|
+
value: { token: entry.token }
|
|
1076
|
+
};
|
|
1047
1077
|
}
|
|
1048
|
-
const
|
|
1049
|
-
if (
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1078
|
+
const entry = find(payload);
|
|
1079
|
+
if (endpoint === "sketch/poll") return {
|
|
1080
|
+
ok: true,
|
|
1081
|
+
value: [...entry.cancelled.splice(0).map((id) => ({
|
|
1082
|
+
id,
|
|
1083
|
+
cancelled: true
|
|
1084
|
+
})), ...[...entry.tasks].filter(([, t]) => !t.delivered).map(([id, t]) => {
|
|
1085
|
+
t.delivered = true;
|
|
1086
|
+
return {
|
|
1087
|
+
id,
|
|
1088
|
+
request: t.request,
|
|
1089
|
+
expiresAt: t.expiresAt
|
|
1090
|
+
};
|
|
1091
|
+
})]
|
|
1054
1092
|
};
|
|
1055
|
-
|
|
1093
|
+
if (endpoint === "sketch/claim") {
|
|
1094
|
+
const task = entry.tasks.get(payload.id);
|
|
1095
|
+
return {
|
|
1096
|
+
ok: true,
|
|
1097
|
+
value: Boolean(task && task.delivered && task.expiresAt > now())
|
|
1098
|
+
};
|
|
1099
|
+
}
|
|
1100
|
+
if (endpoint === "sketch/disconnect") {
|
|
1101
|
+
fail(entry, "Sketch board closed");
|
|
1102
|
+
sessions.delete(payload.sessionId);
|
|
1103
|
+
return {
|
|
1104
|
+
ok: true,
|
|
1105
|
+
value: null
|
|
1106
|
+
};
|
|
1107
|
+
}
|
|
1108
|
+
if (endpoint === "sketch/result") {
|
|
1109
|
+
const task = entry.tasks.get(payload.id);
|
|
1110
|
+
if (task) {
|
|
1111
|
+
entry.tasks.delete(payload.id);
|
|
1112
|
+
payload.error ? task.reject(Error(String(payload.error).slice(0, 500))) : task.resolve(payload.value);
|
|
1113
|
+
}
|
|
1114
|
+
return {
|
|
1115
|
+
ok: true,
|
|
1116
|
+
value: null
|
|
1117
|
+
};
|
|
1118
|
+
}
|
|
1119
|
+
throw Error("Unknown sketch route");
|
|
1120
|
+
} catch (error) {
|
|
1056
1121
|
return {
|
|
1057
|
-
|
|
1058
|
-
|
|
1122
|
+
ok: false,
|
|
1123
|
+
error: {
|
|
1124
|
+
code: "invalid-input",
|
|
1125
|
+
message: error.message,
|
|
1126
|
+
details: { issues: [] }
|
|
1127
|
+
}
|
|
1059
1128
|
};
|
|
1060
|
-
}
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
if (current.accounts.length === 1) throw new Error("Cannot remove the last account; sign out instead");
|
|
1076
|
-
const accounts = current.accounts.filter((account) => account.id !== id);
|
|
1077
|
-
return {
|
|
1078
|
-
...current,
|
|
1079
|
-
activeId: current.activeId === id ? accounts[0].id : current.activeId,
|
|
1080
|
-
legacyAccountId: current.legacyAccountId === id ? void 0 : current.legacyAccountId,
|
|
1081
|
-
accounts
|
|
1129
|
+
}
|
|
1130
|
+
},
|
|
1131
|
+
request(sessionId, request, signal) {
|
|
1132
|
+
if (!enabled()) return Promise.reject(Error("Sketch is disabled"));
|
|
1133
|
+
const entry = sessions.get(sessionId);
|
|
1134
|
+
if (!entry || now() - entry.seen > 1e4) return Promise.reject(Error("Switch to this session in DSH with sketch editing enabled"));
|
|
1135
|
+
if (entry.tasks.size) return Promise.reject(Error("Another sketch operation is pending"));
|
|
1136
|
+
if (JSON.stringify(request).length > 2e6) return Promise.reject(Error("Sketch batch is too large"));
|
|
1137
|
+
return new Promise((resolve, reject) => {
|
|
1138
|
+
const id = randomUUID();
|
|
1139
|
+
const finish = (callback, value) => {
|
|
1140
|
+
clearTimeout(timer);
|
|
1141
|
+
signal?.removeEventListener("abort", abort);
|
|
1142
|
+
entry.tasks.delete(id);
|
|
1143
|
+
callback(value);
|
|
1082
1144
|
};
|
|
1145
|
+
const cancel = (message) => {
|
|
1146
|
+
if (entry.tasks.get(id)?.delivered) {
|
|
1147
|
+
entry.cancelled.push(id);
|
|
1148
|
+
if (entry.cancelled.length > 32) entry.cancelled.shift();
|
|
1149
|
+
}
|
|
1150
|
+
finish(reject, Error(message));
|
|
1151
|
+
};
|
|
1152
|
+
const abort = () => cancel("Sketch operation interrupted; inspect recentRequests before retrying");
|
|
1153
|
+
const timer = setTimeout(() => cancel("Sketch response timed out; inspect recentRequests before retrying"), timeoutMs);
|
|
1154
|
+
entry.tasks.set(id, {
|
|
1155
|
+
request,
|
|
1156
|
+
expiresAt: now() + timeoutMs,
|
|
1157
|
+
delivered: false,
|
|
1158
|
+
resolve: (value) => finish(resolve, value),
|
|
1159
|
+
reject: (error) => finish(reject, error)
|
|
1160
|
+
});
|
|
1161
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
1162
|
+
if (signal?.aborted) abort();
|
|
1083
1163
|
});
|
|
1084
|
-
}
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
//#region src/credential-store.js
|
|
1089
|
-
const PROVIDER$1 = "openai-codex";
|
|
1090
|
-
const abortIfNeeded = (options) => options?.signal?.throwIfAborted();
|
|
1091
|
-
const clone = (value) => value === void 0 ? void 0 : structuredClone(value);
|
|
1092
|
-
function assertProvider(providerId) {
|
|
1093
|
-
if (providerId !== PROVIDER$1) throw new Error(`Codex credential store does not own provider ${JSON.stringify(providerId)}`);
|
|
1094
|
-
}
|
|
1095
|
-
function assertOAuthCredential(value) {
|
|
1096
|
-
if (value === void 0) return void 0;
|
|
1097
|
-
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");
|
|
1098
|
-
return clone(value);
|
|
1099
|
-
}
|
|
1100
|
-
function parseOAuthCredential(value) {
|
|
1101
|
-
try {
|
|
1102
|
-
return assertOAuthCredential(JSON.parse(value));
|
|
1103
|
-
} catch (error) {
|
|
1104
|
-
if (error?.message === "Codex credential store received a malformed OAuth credential") throw error;
|
|
1105
|
-
throw new Error("Codex credential store contains malformed OAuth JSON", { cause: error });
|
|
1106
|
-
}
|
|
1107
|
-
}
|
|
1108
|
-
/**
|
|
1109
|
-
* Adapt DSH's managed string credential service to pi-ai's typed OAuth store.
|
|
1110
|
-
* Refresh/login/logout operations are serialized so an older refresh response
|
|
1111
|
-
* cannot overwrite a newer rotated token.
|
|
1112
|
-
*/
|
|
1113
|
-
var DshOAuthCredentialStore = class {
|
|
1114
|
-
#chains = /* @__PURE__ */ new Map();
|
|
1115
|
-
constructor(credentials, ref, legacyRefs = [], options = {}) {
|
|
1116
|
-
if (credentials === void 0 || credentials === null) throw new Error("Codex OAuth requires the DSH credentials service");
|
|
1117
|
-
const expirySkewMs = options.expirySkewMs ?? 0;
|
|
1118
|
-
if (!Number.isFinite(expirySkewMs) || expirySkewMs < 0) throw new Error("Codex OAuth expiry skew must be a non-negative finite number");
|
|
1119
|
-
this.credentials = credentials;
|
|
1120
|
-
this.ref = ref;
|
|
1121
|
-
this.legacyRefs = Object.freeze([...legacyRefs]);
|
|
1122
|
-
this.expirySkewMs = expirySkewMs;
|
|
1123
|
-
this.vault = options.vault;
|
|
1124
|
-
}
|
|
1125
|
-
#enqueue(providerId, operation, options) {
|
|
1126
|
-
assertProvider(providerId);
|
|
1127
|
-
const current = (this.#chains.get(providerId) ?? Promise.resolve()).catch(() => void 0).then(async () => {
|
|
1128
|
-
abortIfNeeded(options);
|
|
1129
|
-
return operation();
|
|
1130
|
-
});
|
|
1131
|
-
const tail = current.catch(() => void 0);
|
|
1132
|
-
this.#chains.set(providerId, tail);
|
|
1133
|
-
tail.finally(() => {
|
|
1134
|
-
if (this.#chains.get(providerId) === tail) this.#chains.delete(providerId);
|
|
1135
|
-
});
|
|
1136
|
-
return current;
|
|
1137
|
-
}
|
|
1138
|
-
async #read(providerId, options) {
|
|
1139
|
-
assertProvider(providerId);
|
|
1140
|
-
abortIfNeeded(options);
|
|
1141
|
-
if (this.vault !== void 0) {
|
|
1142
|
-
const current = await this.vault.readActive();
|
|
1143
|
-
if (current === void 0) return void 0;
|
|
1144
|
-
return this.expirySkewMs === 0 ? current : {
|
|
1145
|
-
...current,
|
|
1146
|
-
expires: current.expires - this.expirySkewMs
|
|
1147
|
-
};
|
|
1164
|
+
},
|
|
1165
|
+
dispose() {
|
|
1166
|
+
for (const entry of sessions.values()) fail(entry, "Sketch service stopped");
|
|
1167
|
+
sessions.clear();
|
|
1148
1168
|
}
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1169
|
+
};
|
|
1170
|
+
}
|
|
1171
|
+
//#endregion
|
|
1172
|
+
//#region src/sketch-command-schema.js
|
|
1173
|
+
const number = { type: "number" };
|
|
1174
|
+
const string = { type: "string" };
|
|
1175
|
+
const object = (properties) => ({
|
|
1176
|
+
type: "object",
|
|
1177
|
+
additionalProperties: false,
|
|
1178
|
+
properties
|
|
1179
|
+
});
|
|
1180
|
+
const points = {
|
|
1181
|
+
type: "array",
|
|
1182
|
+
items: object({
|
|
1183
|
+
x: {
|
|
1184
|
+
...number,
|
|
1185
|
+
required: true
|
|
1186
|
+
},
|
|
1187
|
+
y: {
|
|
1188
|
+
...number,
|
|
1189
|
+
required: true
|
|
1158
1190
|
}
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
}
|
|
1170
|
-
async list(options) {
|
|
1171
|
-
abortIfNeeded(options);
|
|
1172
|
-
return await this.read(PROVIDER$1, options) === void 0 ? [] : [{
|
|
1173
|
-
providerId: PROVIDER$1,
|
|
1174
|
-
type: "oauth"
|
|
1175
|
-
}];
|
|
1176
|
-
}
|
|
1177
|
-
modify(providerId, update, options) {
|
|
1178
|
-
return this.#enqueue(providerId, async () => {
|
|
1179
|
-
if (this.vault !== void 0) {
|
|
1180
|
-
const next = await this.vault.modifyActive(async (current) => {
|
|
1181
|
-
const visible = current === void 0 || this.expirySkewMs === 0 ? current : {
|
|
1182
|
-
...current,
|
|
1183
|
-
expires: current.expires - this.expirySkewMs
|
|
1184
|
-
};
|
|
1185
|
-
const updated = await update(clone(visible));
|
|
1186
|
-
return updated === void 0 ? void 0 : assertOAuthCredential(updated);
|
|
1187
|
-
});
|
|
1188
|
-
abortIfNeeded(options);
|
|
1189
|
-
return clone(next);
|
|
1190
|
-
}
|
|
1191
|
-
const current = await this.#read(providerId, options);
|
|
1192
|
-
const next = await update(clone(current));
|
|
1193
|
-
abortIfNeeded(options);
|
|
1194
|
-
if (next === void 0) return current;
|
|
1195
|
-
const validated = assertOAuthCredential(next);
|
|
1196
|
-
await this.credentials.set(this.ref, JSON.stringify(validated));
|
|
1197
|
-
for (const legacyRef of this.legacyRefs) await this.credentials.unset(legacyRef);
|
|
1198
|
-
abortIfNeeded(options);
|
|
1199
|
-
return clone(validated);
|
|
1200
|
-
}, options);
|
|
1191
|
+
})
|
|
1192
|
+
};
|
|
1193
|
+
const point = object({
|
|
1194
|
+
x: {
|
|
1195
|
+
...number,
|
|
1196
|
+
required: true
|
|
1197
|
+
},
|
|
1198
|
+
y: {
|
|
1199
|
+
...number,
|
|
1200
|
+
required: true
|
|
1201
1201
|
}
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1202
|
+
});
|
|
1203
|
+
const style = {
|
|
1204
|
+
color: string,
|
|
1205
|
+
width: number,
|
|
1206
|
+
opacity: number,
|
|
1207
|
+
fill: { type: "boolean" },
|
|
1208
|
+
text: string,
|
|
1209
|
+
points
|
|
1210
|
+
};
|
|
1211
|
+
const sketchCommandArray = {
|
|
1212
|
+
type: "array",
|
|
1213
|
+
items: object({
|
|
1214
|
+
op: {
|
|
1215
|
+
type: "string",
|
|
1216
|
+
required: true,
|
|
1217
|
+
enum: [
|
|
1218
|
+
"stroke",
|
|
1219
|
+
"object",
|
|
1220
|
+
"layer",
|
|
1221
|
+
"resize"
|
|
1222
|
+
]
|
|
1223
|
+
},
|
|
1224
|
+
id: {
|
|
1225
|
+
oneOf: [{ type: "string" }, { type: "integer" }],
|
|
1226
|
+
description: "Object string ID. Layer add: optional NEW unique integer ID; other layer actions: existing layer ID."
|
|
1227
|
+
},
|
|
1228
|
+
after: {
|
|
1229
|
+
type: "integer",
|
|
1230
|
+
description: "Layer add only: existing layer to insert after; defaults to active."
|
|
1231
|
+
},
|
|
1232
|
+
start: point,
|
|
1233
|
+
segments: {
|
|
1234
|
+
type: "array",
|
|
1235
|
+
items: object({
|
|
1236
|
+
control1: {
|
|
1237
|
+
...point,
|
|
1238
|
+
required: true
|
|
1239
|
+
},
|
|
1240
|
+
control2: {
|
|
1241
|
+
...point,
|
|
1242
|
+
required: true
|
|
1243
|
+
},
|
|
1244
|
+
end: {
|
|
1245
|
+
...point,
|
|
1246
|
+
required: true
|
|
1247
|
+
}
|
|
1248
|
+
})
|
|
1249
|
+
},
|
|
1250
|
+
layer: { type: "integer" },
|
|
1251
|
+
shape: {
|
|
1252
|
+
type: "string",
|
|
1253
|
+
enum: [
|
|
1254
|
+
"pen",
|
|
1255
|
+
"line",
|
|
1256
|
+
"arrow",
|
|
1257
|
+
"text",
|
|
1258
|
+
"rectangle",
|
|
1259
|
+
"circle",
|
|
1260
|
+
"ellipse",
|
|
1261
|
+
"polygon",
|
|
1262
|
+
"bezier",
|
|
1263
|
+
"eraser"
|
|
1264
|
+
]
|
|
1265
|
+
},
|
|
1266
|
+
...style,
|
|
1267
|
+
action: {
|
|
1268
|
+
type: "string",
|
|
1269
|
+
enum: [
|
|
1270
|
+
"update",
|
|
1271
|
+
"duplicate",
|
|
1272
|
+
"delete",
|
|
1273
|
+
"add",
|
|
1274
|
+
"select",
|
|
1275
|
+
"rename",
|
|
1276
|
+
"visible",
|
|
1277
|
+
"up",
|
|
1278
|
+
"down",
|
|
1279
|
+
"clear"
|
|
1280
|
+
]
|
|
1281
|
+
},
|
|
1282
|
+
value: string,
|
|
1283
|
+
ratio: {
|
|
1284
|
+
type: "string",
|
|
1285
|
+
enum: [
|
|
1286
|
+
"1:1",
|
|
1287
|
+
"4:3",
|
|
1288
|
+
"3:4",
|
|
1289
|
+
"16:9",
|
|
1290
|
+
"9:16"
|
|
1291
|
+
]
|
|
1292
|
+
},
|
|
1293
|
+
patch: object(style),
|
|
1294
|
+
transform: object({
|
|
1295
|
+
dx: number,
|
|
1296
|
+
dy: number,
|
|
1297
|
+
scaleX: number,
|
|
1298
|
+
scaleY: number
|
|
1299
|
+
})
|
|
1300
|
+
})
|
|
1301
|
+
};
|
|
1302
|
+
//#endregion
|
|
1303
|
+
//#region src/sketch-agent-tool.js
|
|
1304
|
+
function createSketchAgentTool(bridge, attachments) {
|
|
1305
|
+
return defineTool({
|
|
1306
|
+
name: "codex_sketch",
|
|
1307
|
+
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. 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.",
|
|
1308
|
+
parameters: {
|
|
1309
|
+
action: {
|
|
1310
|
+
type: "string",
|
|
1311
|
+
required: true,
|
|
1312
|
+
enum: [
|
|
1313
|
+
"inspect",
|
|
1314
|
+
"apply",
|
|
1315
|
+
"preview",
|
|
1316
|
+
"save",
|
|
1317
|
+
"finish"
|
|
1318
|
+
]
|
|
1319
|
+
},
|
|
1320
|
+
runId: {
|
|
1321
|
+
type: "string",
|
|
1322
|
+
description: "From inspect; required for all other actions. Never reuse a stopped run."
|
|
1323
|
+
},
|
|
1324
|
+
documentId: {
|
|
1325
|
+
type: "string",
|
|
1326
|
+
description: "From inspect; required except for inspect."
|
|
1327
|
+
},
|
|
1328
|
+
revision: {
|
|
1329
|
+
type: "integer",
|
|
1330
|
+
description: "From latest response; required for apply/save/finish."
|
|
1331
|
+
},
|
|
1332
|
+
requestId: {
|
|
1333
|
+
type: "string",
|
|
1334
|
+
description: "Unique id for apply/save/finish; exact retries are deduplicated. After timeout inspect recentRequests before repeating a write."
|
|
1335
|
+
},
|
|
1336
|
+
commands: {
|
|
1337
|
+
oneOf: [sketchCommandArray, { type: "string" }],
|
|
1338
|
+
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."
|
|
1339
|
+
},
|
|
1340
|
+
name: {
|
|
1341
|
+
type: "string",
|
|
1342
|
+
description: "Draft name for save/finish."
|
|
1343
|
+
},
|
|
1344
|
+
offset: {
|
|
1345
|
+
type: "integer",
|
|
1346
|
+
description: "inspect only: object list offset, default 0. Follow nextOffset for further pages."
|
|
1347
|
+
},
|
|
1348
|
+
objectId: {
|
|
1349
|
+
type: "string",
|
|
1350
|
+
description: "inspect only: return full editable geometry for this object, in layer (defaults to active layer)."
|
|
1351
|
+
},
|
|
1352
|
+
layer: {
|
|
1353
|
+
type: "integer",
|
|
1354
|
+
description: "inspect only: layer containing objectId."
|
|
1208
1355
|
}
|
|
1209
|
-
await this.credentials.unset(this.ref);
|
|
1210
|
-
for (const legacyRef of this.legacyRefs) await this.credentials.unset(legacyRef);
|
|
1211
|
-
abortIfNeeded(options);
|
|
1212
|
-
}, options);
|
|
1213
|
-
}
|
|
1214
|
-
};
|
|
1215
|
-
/** Return only account state that is safe to expose to the browser client. */
|
|
1216
|
-
function createCodexAuthService(models, store, options = {}) {
|
|
1217
|
-
const runLogin = options.runLogin ?? ((run) => run());
|
|
1218
|
-
const accountVault = options.accountVault;
|
|
1219
|
-
const createLoginModels = options.createLoginModels;
|
|
1220
|
-
const createPendingStore = options.createPendingStore ?? (() => new PendingOAuthCredentialStore());
|
|
1221
|
-
return Object.freeze({
|
|
1222
|
-
async status(options) {
|
|
1223
|
-
const current = await store.read(PROVIDER$1, options);
|
|
1224
|
-
const accounts = await accountVault?.list();
|
|
1225
|
-
if (current === void 0) return {
|
|
1226
|
-
authenticated: false,
|
|
1227
|
-
provider: PROVIDER$1,
|
|
1228
|
-
...accounts === void 0 ? {} : { accounts }
|
|
1229
|
-
};
|
|
1230
|
-
return {
|
|
1231
|
-
authenticated: true,
|
|
1232
|
-
provider: PROVIDER$1,
|
|
1233
|
-
type: "oauth",
|
|
1234
|
-
expiresAt: current.expires,
|
|
1235
|
-
...accounts === void 0 ? {} : { accounts }
|
|
1236
|
-
};
|
|
1237
1356
|
},
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1357
|
+
timeoutMs: 25e3,
|
|
1358
|
+
isConcurrencySafe: () => false,
|
|
1359
|
+
async execute(args, exec) {
|
|
1360
|
+
const sessionId = exec.agent?.id;
|
|
1361
|
+
if (typeof sessionId !== "string") throw Error("A session-owned sketch call is required");
|
|
1362
|
+
const request = { ...args };
|
|
1363
|
+
if (args.action === "apply") try {
|
|
1364
|
+
request.commands = typeof args.commands === "string" ? JSON.parse(args.commands) : args.commands;
|
|
1365
|
+
if (!Array.isArray(request.commands)) throw Error();
|
|
1366
|
+
} catch {
|
|
1367
|
+
throw Error("commands must be a native array or JSON array string");
|
|
1368
|
+
}
|
|
1369
|
+
const value = await bridge.request(sessionId, request, exec.signal);
|
|
1370
|
+
if (value.png) {
|
|
1371
|
+
if (!/^data:image\/png;base64,/.test(value.png) || value.png.length > 8 * 1024 * 1024) throw Error("Invalid sketch preview");
|
|
1372
|
+
const image = await attachments.saveImage({
|
|
1373
|
+
data: new Uint8Array(Buffer.from(value.png.split(",")[1], "base64")),
|
|
1374
|
+
mediaType: "image/png",
|
|
1375
|
+
name: "sketch-preview.png"
|
|
1247
1376
|
});
|
|
1377
|
+
const { png, ...snapshot } = value;
|
|
1378
|
+
return {
|
|
1379
|
+
...snapshot,
|
|
1380
|
+
image
|
|
1381
|
+
};
|
|
1248
1382
|
}
|
|
1249
|
-
return
|
|
1250
|
-
},
|
|
1251
|
-
async select(id) {
|
|
1252
|
-
if (accountVault === void 0) throw new Error("Codex multi-account is unavailable");
|
|
1253
|
-
await accountVault.select(id);
|
|
1254
|
-
return this.status();
|
|
1255
|
-
},
|
|
1256
|
-
async remove(id) {
|
|
1257
|
-
if (accountVault === void 0) throw new Error("Codex multi-account is unavailable");
|
|
1258
|
-
await accountVault.remove(id);
|
|
1259
|
-
return this.status();
|
|
1383
|
+
return value;
|
|
1260
1384
|
},
|
|
1261
|
-
|
|
1262
|
-
|
|
1385
|
+
output: {
|
|
1386
|
+
schema: {
|
|
1387
|
+
type: "object",
|
|
1388
|
+
additionalProperties: true
|
|
1389
|
+
},
|
|
1390
|
+
render: (_args, value) => [{
|
|
1391
|
+
type: "text",
|
|
1392
|
+
text: JSON.stringify({
|
|
1393
|
+
...value,
|
|
1394
|
+
image: void 0
|
|
1395
|
+
})
|
|
1396
|
+
}, ...value.image ? [{
|
|
1397
|
+
type: "image",
|
|
1398
|
+
attachment: value.image
|
|
1399
|
+
}] : []]
|
|
1263
1400
|
}
|
|
1264
1401
|
});
|
|
1265
1402
|
}
|
|
1266
1403
|
//#endregion
|
|
1267
|
-
//#region src/
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1404
|
+
//#region src/sketch-codec-route.js
|
|
1405
|
+
function registerSketchCodec(connection) {
|
|
1406
|
+
let source;
|
|
1407
|
+
return connection.fetch.register({
|
|
1408
|
+
path: "/api/codex-subscription/sketch-psd-worker",
|
|
1409
|
+
methods: ["GET"],
|
|
1410
|
+
requestBody: "buffered",
|
|
1411
|
+
async fetch() {
|
|
1412
|
+
source ??= await readFile(new URL("./sketch-psd-worker.js", import.meta.url));
|
|
1413
|
+
return new Response(source, { headers: {
|
|
1414
|
+
"content-type": "text/javascript; charset=utf-8",
|
|
1415
|
+
"cache-control": "no-store"
|
|
1416
|
+
} });
|
|
1417
|
+
}
|
|
1418
|
+
});
|
|
1419
|
+
}
|
|
1420
|
+
//#endregion
|
|
1421
|
+
//#region src/account-vault.js
|
|
1422
|
+
const VERSION = 1;
|
|
1423
|
+
const DEFAULT_LABEL = "Account 1";
|
|
1424
|
+
const clone$1 = (value) => value === void 0 ? void 0 : structuredClone(value);
|
|
1425
|
+
const EMAIL_MAX_LENGTH = 254;
|
|
1426
|
+
const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/u;
|
|
1427
|
+
/** Keep only a bounded, display-safe email address from a trusted OAuth result. */
|
|
1428
|
+
function normalizeAccountEmail(value) {
|
|
1429
|
+
if (typeof value !== "string") return void 0;
|
|
1430
|
+
const email = value.trim();
|
|
1431
|
+
return email.length > 0 && email.length <= EMAIL_MAX_LENGTH && EMAIL_PATTERN.test(email) ? email : void 0;
|
|
1432
|
+
}
|
|
1433
|
+
function decodeJwtPayload(access) {
|
|
1434
|
+
if (typeof access !== "string") return void 0;
|
|
1435
|
+
const encoded = access.split(".")[1];
|
|
1436
|
+
if (typeof encoded !== "string" || encoded.length === 0) return void 0;
|
|
1272
1437
|
try {
|
|
1273
|
-
|
|
1438
|
+
return JSON.parse(Buffer.from(encoded, "base64url").toString("utf8"));
|
|
1274
1439
|
} catch {
|
|
1275
|
-
|
|
1440
|
+
return;
|
|
1276
1441
|
}
|
|
1277
|
-
if (url.protocol !== "https:") throw new Error("Codex auth URL must use HTTPS");
|
|
1278
|
-
if (url.origin !== OPENAI_AUTH_ORIGIN || url.username !== "" || url.password !== "") throw new Error("Codex auth URL must use the OpenAI auth origin");
|
|
1279
|
-
return url.href;
|
|
1280
|
-
}
|
|
1281
|
-
/** Return a shell-free native opener command for the current desktop. */
|
|
1282
|
-
function commandForCodexAuthUrl(value, platform = process.platform) {
|
|
1283
|
-
const url = assertCodexAuthUrl(value);
|
|
1284
|
-
if (platform === "win32") return {
|
|
1285
|
-
file: "rundll32.exe",
|
|
1286
|
-
args: ["url.dll,FileProtocolHandler", url],
|
|
1287
|
-
shell: false
|
|
1288
|
-
};
|
|
1289
|
-
if (platform === "darwin") return {
|
|
1290
|
-
file: "open",
|
|
1291
|
-
args: [url],
|
|
1292
|
-
shell: false
|
|
1293
|
-
};
|
|
1294
|
-
if (platform === "linux") return {
|
|
1295
|
-
file: "xdg-open",
|
|
1296
|
-
args: [url],
|
|
1297
|
-
shell: false
|
|
1298
|
-
};
|
|
1299
|
-
throw new Error(`Codex auth URL opener is unsupported on ${platform}`);
|
|
1300
1442
|
}
|
|
1301
|
-
function
|
|
1302
|
-
const
|
|
1303
|
-
|
|
1304
|
-
return new Promise((resolve, reject) => {
|
|
1305
|
-
const child = spawnProcess(command.file, command.args, {
|
|
1306
|
-
detached: true,
|
|
1307
|
-
stdio: "ignore",
|
|
1308
|
-
windowsHide: true,
|
|
1309
|
-
shell: command.shell
|
|
1310
|
-
});
|
|
1311
|
-
child.once("error", reject);
|
|
1312
|
-
child.once("spawn", () => {
|
|
1313
|
-
child.unref();
|
|
1314
|
-
resolve();
|
|
1315
|
-
});
|
|
1316
|
-
});
|
|
1443
|
+
function emailFromAccessToken(access) {
|
|
1444
|
+
const payload = decodeJwtPayload(access);
|
|
1445
|
+
return normalizeAccountEmail(payload?.["https://api.openai.com/profile"]?.email ?? payload?.email);
|
|
1317
1446
|
}
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
const
|
|
1321
|
-
const
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
const publicClone = (value) => structuredClone(value);
|
|
1327
|
-
const asObject = (value) => value !== null && typeof value === "object" ? value : {};
|
|
1328
|
-
const ok = (value) => ({
|
|
1329
|
-
ok: true,
|
|
1330
|
-
value
|
|
1331
|
-
});
|
|
1332
|
-
const badRequest = (message) => ({
|
|
1333
|
-
ok: false,
|
|
1334
|
-
error: {
|
|
1335
|
-
code: "bad-request",
|
|
1336
|
-
message,
|
|
1337
|
-
details: { issues: [] }
|
|
1338
|
-
}
|
|
1339
|
-
});
|
|
1340
|
-
const accountStatusError = (message) => ({
|
|
1341
|
-
ok: false,
|
|
1342
|
-
error: {
|
|
1343
|
-
code: "internal",
|
|
1344
|
-
message,
|
|
1345
|
-
details: { issues: [] }
|
|
1346
|
-
}
|
|
1347
|
-
});
|
|
1348
|
-
const classifyAccountStatusError = (error) => {
|
|
1349
|
-
const message = error instanceof Error ? error.message : "";
|
|
1350
|
-
if (/malformed (?:OAuth|grant|account vault)|received a malformed OAuth|contains malformed OAuth/iu.test(message)) return ["credential-malformed", "Codex account credentials are malformed"];
|
|
1351
|
-
if (/credential|account vault|readRecord|credential store|credentials service/iu.test(message)) return ["credential-unavailable", "Codex account credentials are unavailable"];
|
|
1352
|
-
const code = typeof error?.code === "string" ? error.code.toUpperCase() : "";
|
|
1353
|
-
if (error?.name === "TimeoutError" || [
|
|
1354
|
-
"TIMEOUT",
|
|
1355
|
-
"ETIMEDOUT",
|
|
1356
|
-
"UND_ERR_CONNECT_TIMEOUT"
|
|
1357
|
-
].includes(code)) return ["transport", "Codex account status service is unavailable"];
|
|
1358
|
-
if ([
|
|
1359
|
-
"ECONNRESET",
|
|
1360
|
-
"ECONNREFUSED",
|
|
1361
|
-
"ENOTFOUND",
|
|
1362
|
-
"EAI_AGAIN",
|
|
1363
|
-
"NETWORK",
|
|
1364
|
-
"NETWORK_ERROR",
|
|
1365
|
-
"TRANSPORT"
|
|
1366
|
-
].includes(code) || error?.name === "NetworkError") return ["transport", "Codex account status service is unavailable"];
|
|
1367
|
-
return ["unknown", "Could not read Codex account status"];
|
|
1368
|
-
};
|
|
1369
|
-
const deferred = () => {
|
|
1370
|
-
let resolve;
|
|
1371
|
-
let reject;
|
|
1447
|
+
/** Normalize the one non-secret account attribute that may cross the UI boundary. */
|
|
1448
|
+
function sanitizeOAuthCredential(value) {
|
|
1449
|
+
const credential = assertOAuthCredential$1(value);
|
|
1450
|
+
const email = emailFromAccessToken(credential.access) ?? normalizeAccountEmail(credential.email);
|
|
1451
|
+
if (email === void 0) {
|
|
1452
|
+
delete credential.email;
|
|
1453
|
+
return credential;
|
|
1454
|
+
}
|
|
1372
1455
|
return {
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
reject = onReject;
|
|
1376
|
-
}),
|
|
1377
|
-
resolve,
|
|
1378
|
-
reject
|
|
1456
|
+
...credential,
|
|
1457
|
+
email
|
|
1379
1458
|
};
|
|
1380
|
-
};
|
|
1381
|
-
const publicPrompt = (prompt) => ({
|
|
1382
|
-
type: prompt.type,
|
|
1383
|
-
message: String(prompt.message ?? ""),
|
|
1384
|
-
...typeof prompt.placeholder === "string" ? { placeholder: prompt.placeholder } : {}
|
|
1385
|
-
});
|
|
1386
|
-
function classifyLoginFailure(error) {
|
|
1387
|
-
const message = error instanceof Error ? error.message : "";
|
|
1388
|
-
if (/token exchange failed/iu.test(message)) return "token-exchange";
|
|
1389
|
-
if (/fetch failed|\b(?:ECONN|ENOTFOUND|ETIMEDOUT|CERT_|socket|network)\b/iu.test(message)) return "network";
|
|
1390
|
-
if (/extract accountId|account[_ -]?id/iu.test(message)) return "account-claim";
|
|
1391
|
-
if (/credential|credentials-local|OAuth JSON/iu.test(message)) return "credential-store";
|
|
1392
|
-
if (/Missing authorization code|State mismatch|callback/iu.test(message)) return "callback";
|
|
1393
|
-
return "provider";
|
|
1394
1459
|
}
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
}
|
|
1403
|
-
|
|
1404
|
-
|
|
1460
|
+
function assertOAuthCredential$1(value) {
|
|
1461
|
+
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");
|
|
1462
|
+
return clone$1(value);
|
|
1463
|
+
}
|
|
1464
|
+
function parseOAuthCredential$1(value) {
|
|
1465
|
+
try {
|
|
1466
|
+
return assertOAuthCredential$1(JSON.parse(value));
|
|
1467
|
+
} catch (error) {
|
|
1468
|
+
if (error?.message === "Codex account vault received a malformed OAuth credential") throw error;
|
|
1469
|
+
throw new Error("Codex account vault contains malformed OAuth JSON", { cause: error });
|
|
1405
1470
|
}
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1471
|
+
}
|
|
1472
|
+
function normalizeLabel(value) {
|
|
1473
|
+
if (typeof value !== "string") throw new Error("Codex account label must be text");
|
|
1474
|
+
const label = value.trim().replace(/\s+/gu, " ");
|
|
1475
|
+
if (label.length === 0 || label.length > 48) throw new Error("Codex account label must contain 1 to 48 characters");
|
|
1476
|
+
return label;
|
|
1477
|
+
}
|
|
1478
|
+
function assertVaultRecord(record) {
|
|
1479
|
+
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");
|
|
1480
|
+
const ids = /* @__PURE__ */ new Set();
|
|
1481
|
+
const accounts = record.payload.accounts.map((account) => {
|
|
1482
|
+
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");
|
|
1483
|
+
ids.add(account.id);
|
|
1409
1484
|
return {
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1485
|
+
id: account.id,
|
|
1486
|
+
label: normalizeLabel(account.label),
|
|
1487
|
+
credential: sanitizeOAuthCredential(account.credential)
|
|
1413
1488
|
};
|
|
1489
|
+
});
|
|
1490
|
+
if (!ids.has(record.payload.activeId)) throw new Error("Codex account vault active account is missing");
|
|
1491
|
+
const legacyAccountId = record.payload.legacyAccountId;
|
|
1492
|
+
if (legacyAccountId !== void 0 && !ids.has(legacyAccountId)) throw new Error("Codex account vault legacy account is missing");
|
|
1493
|
+
return {
|
|
1494
|
+
version: VERSION,
|
|
1495
|
+
activeId: record.payload.activeId,
|
|
1496
|
+
legacyAccountId,
|
|
1497
|
+
accounts
|
|
1498
|
+
};
|
|
1499
|
+
}
|
|
1500
|
+
const grant = (payload) => ({
|
|
1501
|
+
kind: "grant",
|
|
1502
|
+
payload
|
|
1503
|
+
});
|
|
1504
|
+
var PendingOAuthCredentialStore = class {
|
|
1505
|
+
#credential;
|
|
1506
|
+
async read(providerId) {
|
|
1507
|
+
if (providerId !== "openai-codex") throw new Error("Pending Codex login received an unknown provider");
|
|
1508
|
+
return clone$1(this.#credential);
|
|
1414
1509
|
}
|
|
1415
|
-
async
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1510
|
+
async list() {
|
|
1511
|
+
return this.#credential === void 0 ? [] : [{
|
|
1512
|
+
providerId: "openai-codex",
|
|
1513
|
+
type: "oauth"
|
|
1514
|
+
}];
|
|
1515
|
+
}
|
|
1516
|
+
async modify(providerId, update) {
|
|
1517
|
+
if (providerId !== "openai-codex") throw new Error("Pending Codex login received an unknown provider");
|
|
1518
|
+
const next = await update(clone$1(this.#credential));
|
|
1519
|
+
if (next !== void 0) this.#credential = sanitizeOAuthCredential(next);
|
|
1520
|
+
return clone$1(this.#credential);
|
|
1521
|
+
}
|
|
1522
|
+
async delete(providerId) {
|
|
1523
|
+
if (providerId !== "openai-codex") throw new Error("Pending Codex login received an unknown provider");
|
|
1524
|
+
this.#credential = void 0;
|
|
1525
|
+
}
|
|
1526
|
+
credential() {
|
|
1527
|
+
return clone$1(this.#credential);
|
|
1528
|
+
}
|
|
1529
|
+
};
|
|
1530
|
+
/**
|
|
1531
|
+
* Multi-account owner state stored in DSH's atomic plugin credential record.
|
|
1532
|
+
* The old single-account reference remains as a rollback source and is kept in
|
|
1533
|
+
* sync whenever that imported account rotates its refresh token.
|
|
1534
|
+
*/
|
|
1535
|
+
var DshOAuthAccountVault = class {
|
|
1536
|
+
#tail = Promise.resolve();
|
|
1537
|
+
constructor(credentials, options) {
|
|
1538
|
+
if (credentials === void 0 || credentials === null || typeof credentials.readRecord !== "function" || typeof credentials.modifyRecord !== "function") throw new Error("Codex multi-account requires DSH credential records");
|
|
1539
|
+
this.credentials = credentials;
|
|
1540
|
+
this.key = options.key;
|
|
1541
|
+
this.legacyRef = options.legacyRef;
|
|
1542
|
+
this.legacyRefs = Object.freeze([...options.legacyRefs ?? []]);
|
|
1543
|
+
this.createId = options.createId ?? randomUUID;
|
|
1544
|
+
this.onLegacySyncFailure = options.onLegacySyncFailure ?? (() => {});
|
|
1545
|
+
}
|
|
1546
|
+
#enqueue(operation) {
|
|
1547
|
+
const current = this.#tail.catch(() => void 0).then(operation);
|
|
1548
|
+
this.#tail = current.catch(() => void 0);
|
|
1549
|
+
return current;
|
|
1550
|
+
}
|
|
1551
|
+
async #legacyCredential() {
|
|
1552
|
+
for (const ref of [this.legacyRef, ...this.legacyRefs]) {
|
|
1553
|
+
const hit = await this.credentials.resolve(ref);
|
|
1554
|
+
if (hit?.value === void 0 || hit.value === "") continue;
|
|
1555
|
+
return {
|
|
1556
|
+
ref,
|
|
1557
|
+
credential: parseOAuthCredential$1(hit.value)
|
|
1426
1558
|
};
|
|
1427
|
-
active.controller.abort(/* @__PURE__ */ new Error("Codex login replaced by a new attempt"));
|
|
1428
1559
|
}
|
|
1429
|
-
|
|
1560
|
+
}
|
|
1561
|
+
async #ensurePayload() {
|
|
1562
|
+
const existing = await this.credentials.readRecord(this.key);
|
|
1563
|
+
if (existing !== void 0) return assertVaultRecord(existing);
|
|
1564
|
+
const legacy = await this.#legacyCredential();
|
|
1565
|
+
if (legacy === void 0) return void 0;
|
|
1430
1566
|
const id = this.createId();
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
this.#
|
|
1447
|
-
|
|
1448
|
-
const
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
if (controller.signal.aborted) return;
|
|
1504
|
-
const status = await this.auth.status();
|
|
1505
|
-
session.view = {
|
|
1567
|
+
return assertVaultRecord(await this.credentials.modifyRecord(this.key, (current) => {
|
|
1568
|
+
if (current !== void 0) return Promise.resolve(current);
|
|
1569
|
+
return Promise.resolve(grant({
|
|
1570
|
+
version: VERSION,
|
|
1571
|
+
activeId: id,
|
|
1572
|
+
legacyAccountId: id,
|
|
1573
|
+
accounts: [{
|
|
1574
|
+
id,
|
|
1575
|
+
label: DEFAULT_LABEL,
|
|
1576
|
+
credential: legacy.credential
|
|
1577
|
+
}]
|
|
1578
|
+
}));
|
|
1579
|
+
}));
|
|
1580
|
+
}
|
|
1581
|
+
async #modifyPayload(update) {
|
|
1582
|
+
await this.#ensurePayload();
|
|
1583
|
+
let previousLegacy;
|
|
1584
|
+
const payload = assertVaultRecord(await this.credentials.modifyRecord(this.key, async (current) => {
|
|
1585
|
+
if (current === void 0) throw new Error("Codex account vault is not signed in");
|
|
1586
|
+
const payload = assertVaultRecord(current);
|
|
1587
|
+
previousLegacy = payload.accounts.find((account) => account.id === payload.legacyAccountId)?.credential;
|
|
1588
|
+
const next = await update(clone$1(payload));
|
|
1589
|
+
return grant(next);
|
|
1590
|
+
}));
|
|
1591
|
+
const legacy = payload.accounts.find((account) => account.id === payload.legacyAccountId)?.credential;
|
|
1592
|
+
try {
|
|
1593
|
+
if (legacy === void 0) {
|
|
1594
|
+
if (previousLegacy !== void 0) await this.credentials.unset(this.legacyRef);
|
|
1595
|
+
} else if (JSON.stringify(legacy) !== JSON.stringify(previousLegacy)) await this.credentials.set(this.legacyRef, JSON.stringify(legacy));
|
|
1596
|
+
} catch {
|
|
1597
|
+
this.onLegacySyncFailure();
|
|
1598
|
+
}
|
|
1599
|
+
return payload;
|
|
1600
|
+
}
|
|
1601
|
+
list() {
|
|
1602
|
+
return this.#enqueue(async () => {
|
|
1603
|
+
const payload = await this.#ensurePayload();
|
|
1604
|
+
if (payload === void 0) return [];
|
|
1605
|
+
return payload.accounts.map((account) => ({
|
|
1606
|
+
id: account.id,
|
|
1607
|
+
label: account.label,
|
|
1608
|
+
active: account.id === payload.activeId,
|
|
1609
|
+
expiresAt: account.credential.expires,
|
|
1610
|
+
...account.credential.email === void 0 ? {} : { email: account.credential.email }
|
|
1611
|
+
}));
|
|
1612
|
+
});
|
|
1613
|
+
}
|
|
1614
|
+
readActive() {
|
|
1615
|
+
return this.#enqueue(async () => {
|
|
1616
|
+
const payload = await this.#ensurePayload();
|
|
1617
|
+
return clone$1(payload?.accounts.find((account) => account.id === payload.activeId)?.credential);
|
|
1618
|
+
});
|
|
1619
|
+
}
|
|
1620
|
+
activeId() {
|
|
1621
|
+
return this.#enqueue(async () => (await this.#ensurePayload())?.activeId);
|
|
1622
|
+
}
|
|
1623
|
+
add(label, credential) {
|
|
1624
|
+
return this.#enqueue(async () => {
|
|
1625
|
+
const normalizedLabel = normalizeLabel(label);
|
|
1626
|
+
const validated = sanitizeOAuthCredential(credential);
|
|
1627
|
+
await this.#ensurePayload();
|
|
1628
|
+
const id = this.createId();
|
|
1629
|
+
const account = (await this.#modifyPayload((current) => ({
|
|
1630
|
+
...current,
|
|
1631
|
+
activeId: id,
|
|
1632
|
+
accounts: [...current.accounts, {
|
|
1633
|
+
id,
|
|
1634
|
+
label: normalizedLabel,
|
|
1635
|
+
credential: validated
|
|
1636
|
+
}]
|
|
1637
|
+
}))).accounts.find((candidate) => candidate.id === id);
|
|
1638
|
+
return {
|
|
1506
1639
|
id,
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
...typeof status.expiresAt === "number" ? { expiresAt: status.expiresAt } : {}
|
|
1640
|
+
label: account.label,
|
|
1641
|
+
active: true,
|
|
1642
|
+
expiresAt: account.credential.expires,
|
|
1643
|
+
...account.credential.email === void 0 ? {} : { email: account.credential.email }
|
|
1512
1644
|
};
|
|
1513
|
-
})
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1645
|
+
});
|
|
1646
|
+
}
|
|
1647
|
+
select(id) {
|
|
1648
|
+
return this.#enqueue(async () => {
|
|
1649
|
+
await this.#modifyPayload((current) => {
|
|
1650
|
+
if (!current.accounts.some((account) => account.id === id)) throw new Error("Unknown Codex account");
|
|
1651
|
+
return {
|
|
1652
|
+
...current,
|
|
1653
|
+
activeId: id
|
|
1521
1654
|
};
|
|
1522
|
-
|
|
1655
|
+
});
|
|
1656
|
+
});
|
|
1657
|
+
}
|
|
1658
|
+
modifyActive(update) {
|
|
1659
|
+
return this.#enqueue(async () => {
|
|
1660
|
+
if (await this.#ensurePayload() === void 0) {
|
|
1661
|
+
const initial = await update(void 0);
|
|
1662
|
+
if (initial === void 0) return void 0;
|
|
1663
|
+
const credential = sanitizeOAuthCredential(initial);
|
|
1664
|
+
await this.credentials.set(this.legacyRef, JSON.stringify(credential));
|
|
1665
|
+
await this.#ensurePayload();
|
|
1666
|
+
return clone$1(credential);
|
|
1523
1667
|
}
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
const
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
phase: "authenticated",
|
|
1533
|
-
authenticated: true,
|
|
1534
|
-
...typeof status.expiresAt === "number" ? { expiresAt: status.expiresAt } : {}
|
|
1535
|
-
};
|
|
1536
|
-
return;
|
|
1668
|
+
let result;
|
|
1669
|
+
await this.#modifyPayload(async (current) => {
|
|
1670
|
+
const index = current.accounts.findIndex((account) => account.id === current.activeId);
|
|
1671
|
+
const previous = clone$1(current.accounts[index].credential);
|
|
1672
|
+
const next = await update(previous);
|
|
1673
|
+
if (next === void 0) {
|
|
1674
|
+
result = previous;
|
|
1675
|
+
return current;
|
|
1537
1676
|
}
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1677
|
+
const credential = sanitizeOAuthCredential(next);
|
|
1678
|
+
if (credential.email === void 0 && previous.email !== void 0) credential.email = previous.email;
|
|
1679
|
+
const accounts = [...current.accounts];
|
|
1680
|
+
accounts[index] = {
|
|
1681
|
+
...accounts[index],
|
|
1682
|
+
credential
|
|
1683
|
+
};
|
|
1684
|
+
result = clone$1(credential);
|
|
1685
|
+
return {
|
|
1686
|
+
...current,
|
|
1687
|
+
accounts
|
|
1688
|
+
};
|
|
1689
|
+
});
|
|
1690
|
+
return result;
|
|
1691
|
+
});
|
|
1550
1692
|
}
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1693
|
+
deleteAll() {
|
|
1694
|
+
return this.#enqueue(async () => {
|
|
1695
|
+
await this.credentials.deleteRecord(this.key);
|
|
1696
|
+
await this.credentials.unset(this.legacyRef);
|
|
1697
|
+
for (const ref of this.legacyRefs) await this.credentials.unset(ref);
|
|
1698
|
+
});
|
|
1555
1699
|
}
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1700
|
+
remove(id) {
|
|
1701
|
+
return this.#enqueue(async () => {
|
|
1702
|
+
await this.#modifyPayload((current) => {
|
|
1703
|
+
if (!current.accounts.some((account) => account.id === id)) throw new Error("Unknown Codex account");
|
|
1704
|
+
if (current.accounts.length === 1) throw new Error("Cannot remove the last account; sign out instead");
|
|
1705
|
+
const accounts = current.accounts.filter((account) => account.id !== id);
|
|
1706
|
+
return {
|
|
1707
|
+
...current,
|
|
1708
|
+
activeId: current.activeId === id ? accounts[0].id : current.activeId,
|
|
1709
|
+
legacyAccountId: current.legacyAccountId === id ? void 0 : current.legacyAccountId,
|
|
1710
|
+
accounts
|
|
1711
|
+
};
|
|
1712
|
+
});
|
|
1713
|
+
});
|
|
1570
1714
|
}
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1715
|
+
};
|
|
1716
|
+
//#endregion
|
|
1717
|
+
//#region src/credential-store.js
|
|
1718
|
+
const PROVIDER$1 = "openai-codex";
|
|
1719
|
+
const abortIfNeeded = (options) => options?.signal?.throwIfAborted();
|
|
1720
|
+
const clone = (value) => value === void 0 ? void 0 : structuredClone(value);
|
|
1721
|
+
function assertProvider(providerId) {
|
|
1722
|
+
if (providerId !== PROVIDER$1) throw new Error(`Codex credential store does not own provider ${JSON.stringify(providerId)}`);
|
|
1723
|
+
}
|
|
1724
|
+
function assertOAuthCredential(value) {
|
|
1725
|
+
if (value === void 0) return void 0;
|
|
1726
|
+
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");
|
|
1727
|
+
return clone(value);
|
|
1728
|
+
}
|
|
1729
|
+
function parseOAuthCredential(value) {
|
|
1730
|
+
try {
|
|
1731
|
+
return assertOAuthCredential(JSON.parse(value));
|
|
1732
|
+
} catch (error) {
|
|
1733
|
+
if (error?.message === "Codex credential store received a malformed OAuth credential") throw error;
|
|
1734
|
+
throw new Error("Codex credential store contains malformed OAuth JSON", { cause: error });
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
/**
|
|
1738
|
+
* Adapt DSH's managed string credential service to pi-ai's typed OAuth store.
|
|
1739
|
+
* Refresh/login/logout operations are serialized so an older refresh response
|
|
1740
|
+
* cannot overwrite a newer rotated token.
|
|
1741
|
+
*/
|
|
1742
|
+
var DshOAuthCredentialStore = class {
|
|
1743
|
+
#chains = /* @__PURE__ */ new Map();
|
|
1744
|
+
constructor(credentials, ref, legacyRefs = [], options = {}) {
|
|
1745
|
+
if (credentials === void 0 || credentials === null) throw new Error("Codex OAuth requires the DSH credentials service");
|
|
1746
|
+
const expirySkewMs = options.expirySkewMs ?? 0;
|
|
1747
|
+
if (!Number.isFinite(expirySkewMs) || expirySkewMs < 0) throw new Error("Codex OAuth expiry skew must be a non-negative finite number");
|
|
1748
|
+
this.credentials = credentials;
|
|
1749
|
+
this.ref = ref;
|
|
1750
|
+
this.legacyRefs = Object.freeze([...legacyRefs]);
|
|
1751
|
+
this.expirySkewMs = expirySkewMs;
|
|
1752
|
+
this.vault = options.vault;
|
|
1753
|
+
}
|
|
1754
|
+
#enqueue(providerId, operation, options) {
|
|
1755
|
+
assertProvider(providerId);
|
|
1756
|
+
const current = (this.#chains.get(providerId) ?? Promise.resolve()).catch(() => void 0).then(async () => {
|
|
1757
|
+
abortIfNeeded(options);
|
|
1758
|
+
return operation();
|
|
1759
|
+
});
|
|
1760
|
+
const tail = current.catch(() => void 0);
|
|
1761
|
+
this.#chains.set(providerId, tail);
|
|
1762
|
+
tail.finally(() => {
|
|
1763
|
+
if (this.#chains.get(providerId) === tail) this.#chains.delete(providerId);
|
|
1764
|
+
});
|
|
1765
|
+
return current;
|
|
1766
|
+
}
|
|
1767
|
+
async #read(providerId, options) {
|
|
1768
|
+
assertProvider(providerId);
|
|
1769
|
+
abortIfNeeded(options);
|
|
1770
|
+
if (this.vault !== void 0) {
|
|
1771
|
+
const current = await this.vault.readActive();
|
|
1772
|
+
if (current === void 0) return void 0;
|
|
1773
|
+
return this.expirySkewMs === 0 ? current : {
|
|
1774
|
+
...current,
|
|
1775
|
+
expires: current.expires - this.expirySkewMs
|
|
1581
1776
|
};
|
|
1582
|
-
session.controller.abort(/* @__PURE__ */ new Error("Codex login cancelled"));
|
|
1583
1777
|
}
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
const
|
|
1589
|
-
|
|
1778
|
+
let hit = await this.credentials.resolve(this.ref);
|
|
1779
|
+
if (hit?.value === void 0 || hit.value === "") for (const legacyRef of this.legacyRefs) {
|
|
1780
|
+
const legacy = await this.credentials.resolve(legacyRef);
|
|
1781
|
+
if (legacy?.value === void 0 || legacy.value === "") continue;
|
|
1782
|
+
const migrated = parseOAuthCredential(legacy.value);
|
|
1783
|
+
await this.credentials.set(this.ref, JSON.stringify(migrated));
|
|
1784
|
+
await this.credentials.unset(legacyRef);
|
|
1785
|
+
hit = { value: JSON.stringify(migrated) };
|
|
1786
|
+
break;
|
|
1590
1787
|
}
|
|
1591
|
-
|
|
1592
|
-
|
|
1788
|
+
abortIfNeeded(options);
|
|
1789
|
+
if (hit?.value === void 0 || hit.value === "") return void 0;
|
|
1790
|
+
const credential = parseOAuthCredential(hit.value);
|
|
1791
|
+
return this.expirySkewMs === 0 ? credential : {
|
|
1792
|
+
...credential,
|
|
1793
|
+
expires: credential.expires - this.expirySkewMs
|
|
1794
|
+
};
|
|
1593
1795
|
}
|
|
1594
|
-
|
|
1595
|
-
return
|
|
1796
|
+
read(providerId, options) {
|
|
1797
|
+
return this.#enqueue(providerId, () => this.#read(providerId, options), options);
|
|
1596
1798
|
}
|
|
1597
|
-
async
|
|
1598
|
-
|
|
1799
|
+
async list(options) {
|
|
1800
|
+
abortIfNeeded(options);
|
|
1801
|
+
return await this.read(PROVIDER$1, options) === void 0 ? [] : [{
|
|
1802
|
+
providerId: PROVIDER$1,
|
|
1803
|
+
type: "oauth"
|
|
1804
|
+
}];
|
|
1599
1805
|
}
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
} catch (error) {
|
|
1611
|
-
if (signal.aborted) throw error;
|
|
1612
|
-
const [, message] = classifyAccountStatusError(error);
|
|
1613
|
-
return accountStatusError(message);
|
|
1614
|
-
}
|
|
1615
|
-
if (endpoint === "login/start") {
|
|
1616
|
-
const started = await coordinator.start({
|
|
1617
|
-
method: input.method,
|
|
1618
|
-
label: input.label
|
|
1806
|
+
modify(providerId, update, options) {
|
|
1807
|
+
return this.#enqueue(providerId, async () => {
|
|
1808
|
+
if (this.vault !== void 0) {
|
|
1809
|
+
const next = await this.vault.modifyActive(async (current) => {
|
|
1810
|
+
const visible = current === void 0 || this.expirySkewMs === 0 ? current : {
|
|
1811
|
+
...current,
|
|
1812
|
+
expires: current.expires - this.expirySkewMs
|
|
1813
|
+
};
|
|
1814
|
+
const updated = await update(clone(visible));
|
|
1815
|
+
return updated === void 0 ? void 0 : assertOAuthCredential(updated);
|
|
1619
1816
|
});
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1817
|
+
abortIfNeeded(options);
|
|
1818
|
+
return clone(next);
|
|
1819
|
+
}
|
|
1820
|
+
const current = await this.#read(providerId, options);
|
|
1821
|
+
const next = await update(clone(current));
|
|
1822
|
+
abortIfNeeded(options);
|
|
1823
|
+
if (next === void 0) return current;
|
|
1824
|
+
const validated = assertOAuthCredential(next);
|
|
1825
|
+
await this.credentials.set(this.ref, JSON.stringify(validated));
|
|
1826
|
+
for (const legacyRef of this.legacyRefs) await this.credentials.unset(legacyRef);
|
|
1827
|
+
abortIfNeeded(options);
|
|
1828
|
+
return clone(validated);
|
|
1829
|
+
}, options);
|
|
1830
|
+
}
|
|
1831
|
+
delete(providerId, options) {
|
|
1832
|
+
return this.#enqueue(providerId, async () => {
|
|
1833
|
+
if (this.vault !== void 0) {
|
|
1834
|
+
await this.vault.deleteAll();
|
|
1835
|
+
abortIfNeeded(options);
|
|
1836
|
+
return;
|
|
1837
|
+
}
|
|
1838
|
+
await this.credentials.unset(this.ref);
|
|
1839
|
+
for (const legacyRef of this.legacyRefs) await this.credentials.unset(legacyRef);
|
|
1840
|
+
abortIfNeeded(options);
|
|
1841
|
+
}, options);
|
|
1842
|
+
}
|
|
1843
|
+
};
|
|
1844
|
+
/** Return only account state that is safe to expose to the browser client. */
|
|
1845
|
+
function createCodexAuthService(models, store, options = {}) {
|
|
1846
|
+
const runLogin = options.runLogin ?? ((run) => run());
|
|
1847
|
+
const accountVault = options.accountVault;
|
|
1848
|
+
const createLoginModels = options.createLoginModels;
|
|
1849
|
+
const createPendingStore = options.createPendingStore ?? (() => new PendingOAuthCredentialStore());
|
|
1850
|
+
return Object.freeze({
|
|
1851
|
+
async status(options) {
|
|
1852
|
+
const current = await store.read(PROVIDER$1, options);
|
|
1853
|
+
const accounts = await accountVault?.list();
|
|
1854
|
+
if (current === void 0) return {
|
|
1855
|
+
authenticated: false,
|
|
1856
|
+
provider: PROVIDER$1,
|
|
1857
|
+
...accounts === void 0 ? {} : { accounts }
|
|
1858
|
+
};
|
|
1859
|
+
return {
|
|
1860
|
+
authenticated: true,
|
|
1861
|
+
provider: PROVIDER$1,
|
|
1862
|
+
type: "oauth",
|
|
1863
|
+
expiresAt: current.expires,
|
|
1864
|
+
...accounts === void 0 ? {} : { accounts }
|
|
1865
|
+
};
|
|
1866
|
+
},
|
|
1867
|
+
login(interaction, input = {}) {
|
|
1868
|
+
if (input.label !== void 0) {
|
|
1869
|
+
if (accountVault === void 0 || createLoginModels === void 0) throw new Error("Codex multi-account is unavailable");
|
|
1870
|
+
return runLogin(async () => {
|
|
1871
|
+
const pending = createPendingStore();
|
|
1872
|
+
await createLoginModels(pending).login(PROVIDER$1, "oauth", interaction);
|
|
1873
|
+
const credential = pending.credential();
|
|
1874
|
+
if (credential === void 0) throw new Error("Codex login did not return credentials");
|
|
1875
|
+
await accountVault.add(input.label, credential);
|
|
1625
1876
|
});
|
|
1626
|
-
try {
|
|
1627
|
-
await openExternal(url);
|
|
1628
|
-
return ok({
|
|
1629
|
-
...started,
|
|
1630
|
-
externalOpened: true
|
|
1631
|
-
});
|
|
1632
|
-
} catch {
|
|
1633
|
-
return ok({
|
|
1634
|
-
...started,
|
|
1635
|
-
externalOpened: false
|
|
1636
|
-
});
|
|
1637
|
-
}
|
|
1638
1877
|
}
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
if (
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
return
|
|
1878
|
+
return runLogin(() => models.login(PROVIDER$1, "oauth", interaction));
|
|
1879
|
+
},
|
|
1880
|
+
async select(id) {
|
|
1881
|
+
if (accountVault === void 0) throw new Error("Codex multi-account is unavailable");
|
|
1882
|
+
await accountVault.select(id);
|
|
1883
|
+
return this.status();
|
|
1884
|
+
},
|
|
1885
|
+
async remove(id) {
|
|
1886
|
+
if (accountVault === void 0) throw new Error("Codex multi-account is unavailable");
|
|
1887
|
+
await accountVault.remove(id);
|
|
1888
|
+
return this.status();
|
|
1889
|
+
},
|
|
1890
|
+
logout(options) {
|
|
1891
|
+
return models.logout(PROVIDER$1, options);
|
|
1653
1892
|
}
|
|
1654
|
-
};
|
|
1655
|
-
}
|
|
1656
|
-
//#endregion
|
|
1657
|
-
//#region src/oauth-network.js
|
|
1658
|
-
const execFileAsync = promisify(execFile);
|
|
1659
|
-
const CODEX_AUTH_HOST = "auth.openai.com";
|
|
1660
|
-
const CODEX_HOSTS = /* @__PURE__ */ new Set([CODEX_AUTH_HOST, "chatgpt.com"]);
|
|
1661
|
-
const networkScope = new AsyncLocalStorage();
|
|
1662
|
-
let activeScopes = 0;
|
|
1663
|
-
let baseFetch;
|
|
1664
|
-
let scopedFetch;
|
|
1665
|
-
function normalizeProxy(raw) {
|
|
1666
|
-
if (typeof raw !== "string" || raw.trim() === "") return void 0;
|
|
1667
|
-
const value = raw.trim().includes("://") ? raw.trim() : `http://${raw.trim()}`;
|
|
1668
|
-
try {
|
|
1669
|
-
const url = new URL(value);
|
|
1670
|
-
if (!["http:", "https:"].includes(url.protocol) || url.hostname === "") return void 0;
|
|
1671
|
-
return url.toString();
|
|
1672
|
-
} catch {
|
|
1673
|
-
return;
|
|
1674
|
-
}
|
|
1675
|
-
}
|
|
1676
|
-
function bypassesProxy(hostname, port, rawNoProxy) {
|
|
1677
|
-
if (typeof rawNoProxy !== "string" || rawNoProxy.trim() === "") return false;
|
|
1678
|
-
return rawNoProxy.split(/[\s,]+/u).some((raw) => {
|
|
1679
|
-
const entry = raw.trim().toLowerCase();
|
|
1680
|
-
if (entry === "*") return true;
|
|
1681
|
-
if (entry === "") return false;
|
|
1682
|
-
const match = /^(.*?)(?::(\d+))?$/u.exec(entry);
|
|
1683
|
-
const host = match?.[1]?.replace(/^\./u, "");
|
|
1684
|
-
const entryPort = match?.[2];
|
|
1685
|
-
if (!host || entryPort && entryPort !== port) return false;
|
|
1686
|
-
return hostname === host || hostname.endsWith(`.${host}`);
|
|
1687
1893
|
});
|
|
1688
1894
|
}
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
function
|
|
1694
|
-
|
|
1695
|
-
const entries = value.split(";").map((item) => item.trim()).filter(Boolean);
|
|
1696
|
-
const https = entries.find((item) => /^https=/iu.test(item));
|
|
1697
|
-
const http = entries.find((item) => /^http=/iu.test(item));
|
|
1698
|
-
const selected = (https ?? http ?? entries.find((item) => !item.includes("=")))?.replace(/^[^=]+=/u, "");
|
|
1699
|
-
return normalizeProxy(selected);
|
|
1700
|
-
}
|
|
1701
|
-
async function windowsSystemProxy(options = {}) {
|
|
1702
|
-
const run = options.execFile ?? execFileAsync;
|
|
1703
|
-
const reg = `${process.env.SystemRoot ?? "C:\\Windows"}\\System32\\reg.exe`;
|
|
1704
|
-
const key = "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings";
|
|
1705
|
-
try {
|
|
1706
|
-
const enabled = await run(reg, [
|
|
1707
|
-
"query",
|
|
1708
|
-
key,
|
|
1709
|
-
"/v",
|
|
1710
|
-
"ProxyEnable"
|
|
1711
|
-
], {
|
|
1712
|
-
windowsHide: true,
|
|
1713
|
-
encoding: "utf8"
|
|
1714
|
-
});
|
|
1715
|
-
if (!/REG_DWORD\s+0x1\b/iu.test(enabled.stdout)) return void 0;
|
|
1716
|
-
const configured = await run(reg, [
|
|
1717
|
-
"query",
|
|
1718
|
-
key,
|
|
1719
|
-
"/v",
|
|
1720
|
-
"ProxyServer"
|
|
1721
|
-
], {
|
|
1722
|
-
windowsHide: true,
|
|
1723
|
-
encoding: "utf8"
|
|
1724
|
-
});
|
|
1725
|
-
return selectWindowsProxy(/^\s*ProxyServer\s+REG_\w+\s+(.+)$/imu.exec(configured.stdout)?.[1]);
|
|
1726
|
-
} catch {
|
|
1727
|
-
return;
|
|
1728
|
-
}
|
|
1729
|
-
}
|
|
1730
|
-
async function macSystemProxy(options = {}) {
|
|
1731
|
-
const run = options.execFile ?? execFileAsync;
|
|
1895
|
+
//#endregion
|
|
1896
|
+
//#region src/external-url.js
|
|
1897
|
+
const OPENAI_AUTH_ORIGIN = "https://auth.openai.com";
|
|
1898
|
+
/** Validate the only external origin this plugin may launch. */
|
|
1899
|
+
function assertCodexAuthUrl(value) {
|
|
1900
|
+
let url;
|
|
1732
1901
|
try {
|
|
1733
|
-
|
|
1734
|
-
if (!/^\s*HTTPSEnable\s*:\s*1\s*$/imu.test(result.stdout)) return void 0;
|
|
1735
|
-
const host = /^\s*HTTPSProxy\s*:\s*(\S+)\s*$/imu.exec(result.stdout)?.[1];
|
|
1736
|
-
const port = /^\s*HTTPSPort\s*:\s*(\d+)\s*$/imu.exec(result.stdout)?.[1];
|
|
1737
|
-
return normalizeProxy(host && port ? `${host}:${port}` : void 0);
|
|
1902
|
+
url = new URL(value);
|
|
1738
1903
|
} catch {
|
|
1739
|
-
|
|
1904
|
+
throw new Error("Codex auth URL is invalid");
|
|
1740
1905
|
}
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1906
|
+
if (url.protocol !== "https:") throw new Error("Codex auth URL must use HTTPS");
|
|
1907
|
+
if (url.origin !== OPENAI_AUTH_ORIGIN || url.username !== "" || url.password !== "") throw new Error("Codex auth URL must use the OpenAI auth origin");
|
|
1908
|
+
return url.href;
|
|
1909
|
+
}
|
|
1910
|
+
/** Return a shell-free native opener command for the current desktop. */
|
|
1911
|
+
function commandForCodexAuthUrl(value, platform = process.platform) {
|
|
1912
|
+
const url = assertCodexAuthUrl(value);
|
|
1913
|
+
if (platform === "win32") return {
|
|
1914
|
+
file: "rundll32.exe",
|
|
1915
|
+
args: ["url.dll,FileProtocolHandler", url],
|
|
1916
|
+
shell: false
|
|
1748
1917
|
};
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1918
|
+
if (platform === "darwin") return {
|
|
1919
|
+
file: "open",
|
|
1920
|
+
args: [url],
|
|
1921
|
+
shell: false
|
|
1753
1922
|
};
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
source: "system"
|
|
1759
|
-
} : {
|
|
1760
|
-
url: void 0,
|
|
1761
|
-
source: "direct"
|
|
1923
|
+
if (platform === "linux") return {
|
|
1924
|
+
file: "xdg-open",
|
|
1925
|
+
args: [url],
|
|
1926
|
+
shell: false
|
|
1762
1927
|
};
|
|
1928
|
+
throw new Error(`Codex auth URL opener is unsupported on ${platform}`);
|
|
1763
1929
|
}
|
|
1764
|
-
function
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
if (body instanceof URLSearchParams) return Buffer.from(body.toString());
|
|
1768
|
-
if (body instanceof Uint8Array) return Buffer.from(body);
|
|
1769
|
-
throw new TypeError("Unsupported Codex OAuth request body");
|
|
1770
|
-
}
|
|
1771
|
-
function fetchThroughProxy(input, init, proxyUrl) {
|
|
1772
|
-
const target = new URL(typeof input === "string" || input instanceof URL ? input : input.url);
|
|
1773
|
-
const body = bodyBytes(init?.body);
|
|
1774
|
-
const headers = new Headers(init?.headers);
|
|
1775
|
-
if (body && !headers.has("content-length")) headers.set("content-length", String(body.byteLength));
|
|
1930
|
+
function openCodexAuthUrl(value, options = {}) {
|
|
1931
|
+
const command = commandForCodexAuthUrl(value, options.platform);
|
|
1932
|
+
const spawnProcess = options.spawn ?? spawn;
|
|
1776
1933
|
return new Promise((resolve, reject) => {
|
|
1777
|
-
const
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
}
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
const empty = init?.method === "HEAD" || [
|
|
1788
|
-
204,
|
|
1789
|
-
205,
|
|
1790
|
-
304
|
|
1791
|
-
].includes(status);
|
|
1792
|
-
resolve(new Response(empty ? null : Readable.toWeb(response), {
|
|
1793
|
-
status,
|
|
1794
|
-
statusText: response.statusMessage,
|
|
1795
|
-
headers: responseHeaders
|
|
1796
|
-
}));
|
|
1934
|
+
const child = spawnProcess(command.file, command.args, {
|
|
1935
|
+
detached: true,
|
|
1936
|
+
stdio: "ignore",
|
|
1937
|
+
windowsHide: true,
|
|
1938
|
+
shell: command.shell
|
|
1939
|
+
});
|
|
1940
|
+
child.once("error", reject);
|
|
1941
|
+
child.once("spawn", () => {
|
|
1942
|
+
child.unref();
|
|
1943
|
+
resolve();
|
|
1797
1944
|
});
|
|
1798
|
-
request$1.on("error", reject);
|
|
1799
|
-
if (body) request$1.write(body);
|
|
1800
|
-
request$1.end();
|
|
1801
1945
|
});
|
|
1802
1946
|
}
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1947
|
+
//#endregion
|
|
1948
|
+
//#region src/login-coordinator.js
|
|
1949
|
+
const LOGIN_METHODS = /* @__PURE__ */ new Set(["browser", "device_code"]);
|
|
1950
|
+
const TERMINAL_PHASES = /* @__PURE__ */ new Set([
|
|
1951
|
+
"authenticated",
|
|
1952
|
+
"failed",
|
|
1953
|
+
"cancelled"
|
|
1954
|
+
]);
|
|
1955
|
+
const publicClone = (value) => structuredClone(value);
|
|
1956
|
+
const asObject = (value) => value !== null && typeof value === "object" ? value : {};
|
|
1957
|
+
const ok = (value) => ({
|
|
1958
|
+
ok: true,
|
|
1959
|
+
value
|
|
1960
|
+
});
|
|
1961
|
+
const badRequest = (message) => ({
|
|
1962
|
+
ok: false,
|
|
1963
|
+
error: {
|
|
1964
|
+
code: "bad-request",
|
|
1965
|
+
message,
|
|
1966
|
+
details: { issues: [] }
|
|
1967
|
+
}
|
|
1968
|
+
});
|
|
1969
|
+
const accountStatusError = (message) => ({
|
|
1970
|
+
ok: false,
|
|
1971
|
+
error: {
|
|
1972
|
+
code: "internal",
|
|
1973
|
+
message,
|
|
1974
|
+
details: { issues: [] }
|
|
1975
|
+
}
|
|
1976
|
+
});
|
|
1977
|
+
const classifyAccountStatusError = (error) => {
|
|
1978
|
+
const message = error instanceof Error ? error.message : "";
|
|
1979
|
+
if (/malformed (?:OAuth|grant|account vault)|received a malformed OAuth|contains malformed OAuth/iu.test(message)) return ["credential-malformed", "Codex account credentials are malformed"];
|
|
1980
|
+
if (/credential|account vault|readRecord|credential store|credentials service/iu.test(message)) return ["credential-unavailable", "Codex account credentials are unavailable"];
|
|
1981
|
+
const code = typeof error?.code === "string" ? error.code.toUpperCase() : "";
|
|
1982
|
+
if (error?.name === "TimeoutError" || [
|
|
1983
|
+
"TIMEOUT",
|
|
1984
|
+
"ETIMEDOUT",
|
|
1985
|
+
"UND_ERR_CONNECT_TIMEOUT"
|
|
1986
|
+
].includes(code)) return ["transport", "Codex account status service is unavailable"];
|
|
1987
|
+
if ([
|
|
1988
|
+
"ECONNRESET",
|
|
1989
|
+
"ECONNREFUSED",
|
|
1990
|
+
"ENOTFOUND",
|
|
1991
|
+
"EAI_AGAIN",
|
|
1992
|
+
"NETWORK",
|
|
1993
|
+
"NETWORK_ERROR",
|
|
1994
|
+
"TRANSPORT"
|
|
1995
|
+
].includes(code) || error?.name === "NetworkError") return ["transport", "Codex account status service is unavailable"];
|
|
1996
|
+
return ["unknown", "Could not read Codex account status"];
|
|
1997
|
+
};
|
|
1998
|
+
const deferred = () => {
|
|
1999
|
+
let resolve;
|
|
2000
|
+
let reject;
|
|
2001
|
+
return {
|
|
2002
|
+
promise: new Promise((onResolve, onReject) => {
|
|
2003
|
+
resolve = onResolve;
|
|
2004
|
+
reject = onReject;
|
|
2005
|
+
}),
|
|
2006
|
+
resolve,
|
|
2007
|
+
reject
|
|
2008
|
+
};
|
|
2009
|
+
};
|
|
2010
|
+
const publicPrompt = (prompt) => ({
|
|
2011
|
+
type: prompt.type,
|
|
2012
|
+
message: String(prompt.message ?? ""),
|
|
2013
|
+
...typeof prompt.placeholder === "string" ? { placeholder: prompt.placeholder } : {}
|
|
2014
|
+
});
|
|
2015
|
+
function classifyLoginFailure(error) {
|
|
2016
|
+
const message = error instanceof Error ? error.message : "";
|
|
2017
|
+
if (/token exchange failed/iu.test(message)) return "token-exchange";
|
|
2018
|
+
if (/fetch failed|\b(?:ECONN|ENOTFOUND|ETIMEDOUT|CERT_|socket|network)\b/iu.test(message)) return "network";
|
|
2019
|
+
if (/extract accountId|account[_ -]?id/iu.test(message)) return "account-claim";
|
|
2020
|
+
if (/credential|credentials-local|OAuth JSON/iu.test(message)) return "credential-store";
|
|
2021
|
+
if (/Missing authorization code|State mismatch|callback/iu.test(message)) return "callback";
|
|
2022
|
+
return "provider";
|
|
2023
|
+
}
|
|
2024
|
+
/** Own one host-side login without exposing tokens to the browser client. */
|
|
2025
|
+
var CodexLoginCoordinator = class {
|
|
2026
|
+
#sessions = /* @__PURE__ */ new Map();
|
|
2027
|
+
#activeId;
|
|
2028
|
+
constructor(auth, options = {}) {
|
|
2029
|
+
this.auth = auth;
|
|
2030
|
+
this.createId = options.createId ?? (() => crypto.randomUUID());
|
|
2031
|
+
}
|
|
2032
|
+
async accountStatus(options) {
|
|
2033
|
+
return publicClone(await this.auth.status(options));
|
|
2034
|
+
}
|
|
2035
|
+
supportState() {
|
|
2036
|
+
const active = this.#activeId === void 0 ? void 0 : this.#sessions.get(this.#activeId);
|
|
2037
|
+
if (active === void 0) return { phase: "idle" };
|
|
2038
|
+
return {
|
|
2039
|
+
method: active.view.method,
|
|
2040
|
+
phase: active.view.phase,
|
|
2041
|
+
...active.view.phase === "failed" ? { failure: classifyLoginFailure(active.hostError) } : {}
|
|
2042
|
+
};
|
|
2043
|
+
}
|
|
2044
|
+
async start({ method, label }) {
|
|
2045
|
+
if (!LOGIN_METHODS.has(method)) throw new Error(`unsupported Codex login method: ${String(method)}`);
|
|
2046
|
+
if (label !== void 0 && (typeof label !== "string" || label.trim().length === 0 || label.trim().length > 48)) throw new Error("unsupported Codex account label");
|
|
2047
|
+
const active = this.#activeId === void 0 ? void 0 : this.#sessions.get(this.#activeId);
|
|
2048
|
+
if (active !== void 0 && !TERMINAL_PHASES.has(active.view.phase)) {
|
|
2049
|
+
active.view = {
|
|
2050
|
+
id: active.view.id,
|
|
2051
|
+
provider: "openai-codex",
|
|
2052
|
+
method: active.view.method,
|
|
2053
|
+
phase: "cancelled",
|
|
2054
|
+
authenticated: false
|
|
2055
|
+
};
|
|
2056
|
+
active.controller.abort(/* @__PURE__ */ new Error("Codex login replaced by a new attempt"));
|
|
2057
|
+
}
|
|
2058
|
+
if (active !== void 0) this.#sessions.delete(active.view.id);
|
|
2059
|
+
const id = this.createId();
|
|
2060
|
+
const ready = deferred();
|
|
2061
|
+
const controller = new AbortController();
|
|
2062
|
+
const session = {
|
|
2063
|
+
controller,
|
|
2064
|
+
prompt: void 0,
|
|
2065
|
+
ready,
|
|
2066
|
+
view: {
|
|
2067
|
+
id,
|
|
2068
|
+
provider: "openai-codex",
|
|
2069
|
+
method,
|
|
2070
|
+
phase: "starting",
|
|
2071
|
+
authenticated: false
|
|
2072
|
+
}
|
|
2073
|
+
};
|
|
2074
|
+
this.#sessions.set(id, session);
|
|
2075
|
+
this.#activeId = id;
|
|
2076
|
+
const publishReady = () => ready.resolve(publicClone(session.view));
|
|
2077
|
+
const interaction = {
|
|
2078
|
+
signal: controller.signal,
|
|
2079
|
+
prompt: async (prompt) => {
|
|
2080
|
+
controller.signal.throwIfAborted();
|
|
2081
|
+
if (prompt.type === "select") return method;
|
|
2082
|
+
if (![
|
|
2083
|
+
"manual_code",
|
|
2084
|
+
"text",
|
|
2085
|
+
"secret"
|
|
2086
|
+
].includes(prompt.type)) throw new Error(`unsupported Codex auth prompt: ${String(prompt.type)}`);
|
|
2087
|
+
const answer = deferred();
|
|
2088
|
+
session.prompt = answer;
|
|
2089
|
+
session.view = {
|
|
2090
|
+
...session.view,
|
|
2091
|
+
phase: "waiting_input",
|
|
2092
|
+
prompt: publicPrompt(prompt)
|
|
2093
|
+
};
|
|
2094
|
+
const abortPrompt = () => answer.reject(controller.signal.reason ?? /* @__PURE__ */ new Error("login cancelled"));
|
|
2095
|
+
controller.signal.addEventListener("abort", abortPrompt, { once: true });
|
|
2096
|
+
prompt.signal?.addEventListener("abort", abortPrompt, { once: true });
|
|
2097
|
+
publishReady();
|
|
2098
|
+
try {
|
|
2099
|
+
return await answer.promise;
|
|
2100
|
+
} finally {
|
|
2101
|
+
controller.signal.removeEventListener("abort", abortPrompt);
|
|
2102
|
+
prompt.signal?.removeEventListener("abort", abortPrompt);
|
|
2103
|
+
if (session.prompt === answer) session.prompt = void 0;
|
|
2104
|
+
}
|
|
2105
|
+
},
|
|
2106
|
+
notify: (event) => {
|
|
2107
|
+
if (controller.signal.aborted) return;
|
|
2108
|
+
if (event.type === "auth_url") session.view = {
|
|
2109
|
+
...session.view,
|
|
2110
|
+
phase: "waiting_browser",
|
|
2111
|
+
authUrl: assertCodexAuthUrl(event.url),
|
|
2112
|
+
...typeof event.instructions === "string" ? { instructions: event.instructions } : {}
|
|
2113
|
+
};
|
|
2114
|
+
else if (event.type === "device_code") session.view = {
|
|
2115
|
+
...session.view,
|
|
2116
|
+
phase: "waiting_device",
|
|
2117
|
+
deviceCode: {
|
|
2118
|
+
userCode: event.userCode,
|
|
2119
|
+
verificationUri: assertCodexAuthUrl(event.verificationUri),
|
|
2120
|
+
...typeof event.intervalSeconds === "number" ? { intervalSeconds: event.intervalSeconds } : {},
|
|
2121
|
+
...typeof event.expiresInSeconds === "number" ? { expiresInSeconds: event.expiresInSeconds } : {}
|
|
2122
|
+
}
|
|
2123
|
+
};
|
|
2124
|
+
else session.view = {
|
|
2125
|
+
...session.view,
|
|
2126
|
+
message: String(event.message ?? "")
|
|
2127
|
+
};
|
|
2128
|
+
publishReady();
|
|
1820
2129
|
}
|
|
1821
|
-
const route = await proxy;
|
|
1822
|
-
scopedOptions.onRoute?.(route.source);
|
|
1823
|
-
return route.url === void 0 ? baseFetch(input, init) : proxyFetch(input, init, route.url);
|
|
1824
2130
|
};
|
|
1825
|
-
|
|
2131
|
+
session.run = Promise.resolve().then(() => this.auth.login(interaction, label === void 0 ? {} : { label: label.trim() })).then(async () => {
|
|
2132
|
+
if (controller.signal.aborted) return;
|
|
2133
|
+
const status = await this.auth.status();
|
|
2134
|
+
session.view = {
|
|
2135
|
+
id,
|
|
2136
|
+
provider: "openai-codex",
|
|
2137
|
+
method,
|
|
2138
|
+
phase: "authenticated",
|
|
2139
|
+
authenticated: status.authenticated === true,
|
|
2140
|
+
...typeof status.expiresAt === "number" ? { expiresAt: status.expiresAt } : {}
|
|
2141
|
+
};
|
|
2142
|
+
}).catch(async (error) => {
|
|
2143
|
+
if (controller.signal.aborted) {
|
|
2144
|
+
session.view = {
|
|
2145
|
+
id,
|
|
2146
|
+
provider: "openai-codex",
|
|
2147
|
+
method,
|
|
2148
|
+
phase: "cancelled",
|
|
2149
|
+
authenticated: false
|
|
2150
|
+
};
|
|
2151
|
+
return;
|
|
2152
|
+
}
|
|
2153
|
+
try {
|
|
2154
|
+
if (label !== void 0) throw error;
|
|
2155
|
+
const status = await this.auth.status();
|
|
2156
|
+
if (status.authenticated === true) {
|
|
2157
|
+
session.view = {
|
|
2158
|
+
id,
|
|
2159
|
+
provider: "openai-codex",
|
|
2160
|
+
method,
|
|
2161
|
+
phase: "authenticated",
|
|
2162
|
+
authenticated: true,
|
|
2163
|
+
...typeof status.expiresAt === "number" ? { expiresAt: status.expiresAt } : {}
|
|
2164
|
+
};
|
|
2165
|
+
return;
|
|
2166
|
+
}
|
|
2167
|
+
} catch {}
|
|
2168
|
+
session.view = {
|
|
2169
|
+
id,
|
|
2170
|
+
provider: "openai-codex",
|
|
2171
|
+
method,
|
|
2172
|
+
phase: "failed",
|
|
2173
|
+
authenticated: false,
|
|
2174
|
+
error: "Codex login failed"
|
|
2175
|
+
};
|
|
2176
|
+
session.hostError = error;
|
|
2177
|
+
}).finally(publishReady);
|
|
2178
|
+
return ready.promise;
|
|
1826
2179
|
}
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
}
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
2180
|
+
read(id) {
|
|
2181
|
+
const session = this.#sessions.get(id);
|
|
2182
|
+
if (session === void 0) throw new Error("unknown Codex login");
|
|
2183
|
+
return publicClone(session.view);
|
|
2184
|
+
}
|
|
2185
|
+
async submit({ id, value }) {
|
|
2186
|
+
const session = this.#sessions.get(id);
|
|
2187
|
+
if (session === void 0) throw new Error("unknown Codex login");
|
|
2188
|
+
if (session.prompt === void 0 || session.view.phase !== "waiting_input") throw new Error("Codex login is not waiting for input");
|
|
2189
|
+
if (typeof value !== "string" || value.trim() === "") throw new Error("Codex login input is empty");
|
|
2190
|
+
const answer = session.prompt;
|
|
2191
|
+
session.prompt = void 0;
|
|
2192
|
+
session.view = {
|
|
2193
|
+
...session.view,
|
|
2194
|
+
phase: session.view.authUrl === void 0 ? "starting" : "waiting_browser",
|
|
2195
|
+
prompt: void 0
|
|
2196
|
+
};
|
|
2197
|
+
answer.resolve(value);
|
|
2198
|
+
return this.read(id);
|
|
2199
|
+
}
|
|
2200
|
+
async cancel(id) {
|
|
2201
|
+
const session = this.#sessions.get(id);
|
|
2202
|
+
if (session === void 0) throw new Error("unknown Codex login");
|
|
2203
|
+
if (!TERMINAL_PHASES.has(session.view.phase)) {
|
|
2204
|
+
session.view = {
|
|
2205
|
+
id,
|
|
2206
|
+
provider: "openai-codex",
|
|
2207
|
+
method: session.view.method,
|
|
2208
|
+
phase: "cancelled",
|
|
2209
|
+
authenticated: false
|
|
2210
|
+
};
|
|
2211
|
+
session.controller.abort(/* @__PURE__ */ new Error("Codex login cancelled"));
|
|
1841
2212
|
}
|
|
2213
|
+
return this.read(id);
|
|
1842
2214
|
}
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
2215
|
+
async logout(options) {
|
|
2216
|
+
if (this.#activeId !== void 0) {
|
|
2217
|
+
const active = this.#sessions.get(this.#activeId);
|
|
2218
|
+
if (active !== void 0 && !TERMINAL_PHASES.has(active.view.phase)) await this.cancel(active.view.id);
|
|
2219
|
+
}
|
|
2220
|
+
await this.auth.logout(options);
|
|
2221
|
+
return this.accountStatus(options);
|
|
2222
|
+
}
|
|
2223
|
+
async selectAccount(id) {
|
|
2224
|
+
return publicClone(await this.auth.select(id));
|
|
2225
|
+
}
|
|
2226
|
+
async removeAccount(id) {
|
|
2227
|
+
return publicClone(await this.auth.remove(id));
|
|
2228
|
+
}
|
|
2229
|
+
};
|
|
2230
|
+
/** Map the loopback-only DSH Connection channel onto the coordinator. */
|
|
2231
|
+
function createCodexRpcHandler(coordinator, options = {}) {
|
|
2232
|
+
const openExternal = options.openExternal;
|
|
2233
|
+
return async (endpoint, payload, signal) => {
|
|
1861
2234
|
try {
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
2235
|
+
signal.throwIfAborted();
|
|
2236
|
+
const input = asObject(payload);
|
|
2237
|
+
if (endpoint === "status") try {
|
|
2238
|
+
return ok(await coordinator.accountStatus({ signal }));
|
|
2239
|
+
} catch (error) {
|
|
2240
|
+
if (signal.aborted) throw error;
|
|
2241
|
+
const [, message] = classifyAccountStatusError(error);
|
|
2242
|
+
return accountStatusError(message);
|
|
2243
|
+
}
|
|
2244
|
+
if (endpoint === "login/start") {
|
|
2245
|
+
const started = await coordinator.start({
|
|
2246
|
+
method: input.method,
|
|
2247
|
+
label: input.label
|
|
2248
|
+
});
|
|
2249
|
+
if (input.openExternal !== true) return ok(started);
|
|
2250
|
+
const url = started.authUrl ?? started.deviceCode?.verificationUri;
|
|
2251
|
+
if (typeof url !== "string" || openExternal === void 0) return ok({
|
|
2252
|
+
...started,
|
|
2253
|
+
externalOpened: false
|
|
2254
|
+
});
|
|
2255
|
+
try {
|
|
2256
|
+
await openExternal(url);
|
|
2257
|
+
return ok({
|
|
2258
|
+
...started,
|
|
2259
|
+
externalOpened: true
|
|
2260
|
+
});
|
|
2261
|
+
} catch {
|
|
2262
|
+
return ok({
|
|
2263
|
+
...started,
|
|
2264
|
+
externalOpened: false
|
|
2265
|
+
});
|
|
1867
2266
|
}
|
|
1868
|
-
}
|
|
1869
|
-
if (
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
route,
|
|
1880
|
-
elapsed: elapsedBucket(now() - startedAt)
|
|
1881
|
-
});
|
|
1882
|
-
return value;
|
|
2267
|
+
}
|
|
2268
|
+
if (endpoint === "login/status") return ok(coordinator.read(input.id));
|
|
2269
|
+
if (endpoint === "login/submit") return ok(await coordinator.submit({
|
|
2270
|
+
id: input.id,
|
|
2271
|
+
value: input.value
|
|
2272
|
+
}));
|
|
2273
|
+
if (endpoint === "login/cancel") return ok(await coordinator.cancel(input.id));
|
|
2274
|
+
if (endpoint === "logout") return ok(await coordinator.logout({ signal }));
|
|
2275
|
+
if (endpoint === "account/select") return ok(await coordinator.selectAccount(input.id));
|
|
2276
|
+
if (endpoint === "account/remove") return ok(await coordinator.removeAccount(input.id));
|
|
2277
|
+
return badRequest(`unknown Codex auth endpoint: ${endpoint}`);
|
|
1883
2278
|
} catch (error) {
|
|
1884
|
-
if (
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
code: classifyTransportError(error),
|
|
1888
|
-
route,
|
|
1889
|
-
elapsed: elapsedBucket(now() - startedAt)
|
|
1890
|
-
});
|
|
1891
|
-
throw error;
|
|
2279
|
+
if (signal.aborted) throw error;
|
|
2280
|
+
const message = error instanceof Error && /^(unknown|unsupported|a Codex|Codex login)/.test(error.message) ? error.message : "Codex request failed";
|
|
2281
|
+
return badRequest(message);
|
|
1892
2282
|
}
|
|
1893
2283
|
};
|
|
1894
|
-
return Object.freeze({
|
|
1895
|
-
run,
|
|
1896
|
-
fetch: (area, input, init) => run(area, () => globalThis.fetch(input, init)),
|
|
1897
|
-
snapshot: () => Object.fromEntries([...attempts].map(([area, value]) => [area, { ...value }]))
|
|
1898
|
-
});
|
|
1899
2284
|
}
|
|
1900
2285
|
//#endregion
|
|
1901
2286
|
//#region src/pi-ai-runtime.js
|
|
@@ -1912,7 +2297,7 @@ const FAST_SERVICE_TIER = "priority";
|
|
|
1912
2297
|
* persistence, headers, transport, and model behavior remain owned by the
|
|
1913
2298
|
* original provider.
|
|
1914
2299
|
*/
|
|
1915
|
-
function openaiCodexSubscriptionProvider({ resolveSpeedMode = () => void 0, resolveOutputVerbosity = () => OUTPUT_VERBOSITY_DEFAULT, resolveContextMode = () => void 0, resolveCustomContextWindow = () => void 0, catalog, runNetwork = (_area, operation) => operation() } = {}) {
|
|
2300
|
+
function openaiCodexSubscriptionProvider({ resolveSpeedMode = () => void 0, resolveOutputVerbosity = () => OUTPUT_VERBOSITY_DEFAULT, resolveContextMode = () => void 0, resolveCustomContextWindow = () => void 0, catalog, connection, runNetwork = (_area, operation) => operation() } = {}) {
|
|
1916
2301
|
const provider = createOpenAICodexProvider();
|
|
1917
2302
|
const requestToken = Object.freeze({
|
|
1918
2303
|
name: "DSH-managed Codex OAuth request token",
|
|
@@ -1975,19 +2360,29 @@ function openaiCodexSubscriptionProvider({ resolveSpeedMode = () => void 0, reso
|
|
|
1975
2360
|
contextWindow: requested
|
|
1976
2361
|
};
|
|
1977
2362
|
});
|
|
1978
|
-
const networkIterable = (factory) => {
|
|
2363
|
+
const networkIterable = (factory, options) => {
|
|
1979
2364
|
let iterator;
|
|
1980
|
-
|
|
2365
|
+
let prepared;
|
|
2366
|
+
const step = async (method, value) => {
|
|
2367
|
+
const request = await (prepared ??= connection?.prepare(options) ?? Promise.resolve({ options }));
|
|
2368
|
+
return runNetwork("model", () => {
|
|
2369
|
+
iterator ??= factory(request.options)[Symbol.asyncIterator]();
|
|
2370
|
+
return iterator[method]?.(value) ?? (method === "throw" ? Promise.reject(value) : Promise.resolve({
|
|
2371
|
+
done: true,
|
|
2372
|
+
value
|
|
2373
|
+
}));
|
|
2374
|
+
}, request.network);
|
|
2375
|
+
};
|
|
1981
2376
|
return {
|
|
1982
2377
|
[Symbol.asyncIterator]() {
|
|
1983
2378
|
return this;
|
|
1984
2379
|
},
|
|
1985
|
-
next: (value) =>
|
|
1986
|
-
return: (value) =>
|
|
2380
|
+
next: (value) => step("next", value),
|
|
2381
|
+
return: (value) => iterator ? step("return", value) : Promise.resolve({
|
|
1987
2382
|
done: true,
|
|
1988
2383
|
value
|
|
1989
|
-
})
|
|
1990
|
-
throw: (error) =>
|
|
2384
|
+
}),
|
|
2385
|
+
throw: (error) => iterator ? step("throw", error) : Promise.reject(error)
|
|
1991
2386
|
};
|
|
1992
2387
|
};
|
|
1993
2388
|
return Object.freeze({
|
|
@@ -1997,14 +2392,14 @@ function openaiCodexSubscriptionProvider({ resolveSpeedMode = () => void 0, reso
|
|
|
1997
2392
|
apiKey: requestToken
|
|
1998
2393
|
}),
|
|
1999
2394
|
getModels,
|
|
2000
|
-
stream: (model, context, options) => networkIterable(() => provider.stream(model, context, withPreferences(model, options))
|
|
2001
|
-
streamSimple: (model, context, options) => networkIterable(() => provider.streamSimple(model, context, withPreferences(model, options))
|
|
2395
|
+
stream: (model, context, options) => networkIterable((prepared) => provider.stream(model, context, prepared), withPreferences(model, options)),
|
|
2396
|
+
streamSimple: (model, context, options) => networkIterable((prepared) => provider.streamSimple(model, context, prepared), withPreferences(model, options))
|
|
2002
2397
|
});
|
|
2003
2398
|
}
|
|
2004
2399
|
Object.freeze(["0.82.1", "0.85.1"]);
|
|
2005
2400
|
//#endregion
|
|
2006
2401
|
//#region src/version.js
|
|
2007
|
-
const PACKAGE_VERSION = "2.1.
|
|
2402
|
+
const PACKAGE_VERSION = "2.1.1-beta.1";
|
|
2008
2403
|
const USER_AGENT = `dsh-codex-subscription/${PACKAGE_VERSION}`;
|
|
2009
2404
|
//#endregion
|
|
2010
2405
|
//#region src/model-catalog.js
|
|
@@ -2029,13 +2424,35 @@ function reasoningMap(levels) {
|
|
|
2029
2424
|
for (const level of LEVELS.slice(1)) if (supported.has(level)) map[level] = level;
|
|
2030
2425
|
return map;
|
|
2031
2426
|
}
|
|
2427
|
+
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);
|
|
2428
|
+
function unsupportedCapabilities(value) {
|
|
2429
|
+
const reasoning = capabilityNames((value.supported_reasoning_levels ?? []).map((item) => item?.effort)).filter((level) => !["none", ...LEVELS.slice(1)].includes(level));
|
|
2430
|
+
const inputs = capabilityNames(Array.isArray(value.input_modalities) ? value.input_modalities : []).filter((input) => !["text", "image"].includes(input));
|
|
2431
|
+
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) => ![
|
|
2432
|
+
"auto",
|
|
2433
|
+
"default",
|
|
2434
|
+
"standard",
|
|
2435
|
+
"fast",
|
|
2436
|
+
"priority"
|
|
2437
|
+
].includes(tier));
|
|
2438
|
+
return {
|
|
2439
|
+
...reasoning.length ? { reasoning } : {},
|
|
2440
|
+
...inputs.length ? { inputs } : {},
|
|
2441
|
+
...speeds.length ? { speeds } : {}
|
|
2442
|
+
};
|
|
2443
|
+
}
|
|
2032
2444
|
function visibleModel(value) {
|
|
2033
2445
|
if (!record$4(value)) return void 0;
|
|
2034
2446
|
const id = nonEmpty$2(value.slug);
|
|
2035
2447
|
if (id === void 0 || value.visibility !== "list") return void 0;
|
|
2036
2448
|
const supported = Array.isArray(value.supported_reasoning_levels) ? value.supported_reasoning_levels : [];
|
|
2037
2449
|
const input = Array.isArray(value.input_modalities) ? value.input_modalities.filter((item) => ["text", "image"].includes(item)) : ["text", "image"];
|
|
2450
|
+
const unsupported = unsupportedCapabilities({
|
|
2451
|
+
...value,
|
|
2452
|
+
supported_reasoning_levels: supported
|
|
2453
|
+
});
|
|
2038
2454
|
return {
|
|
2455
|
+
...Object.keys(unsupported).length ? { unsupported } : {},
|
|
2039
2456
|
id,
|
|
2040
2457
|
name: nonEmpty$2(value.display_name) ?? id,
|
|
2041
2458
|
description: nonEmpty$2(value.description),
|
|
@@ -2179,6 +2596,10 @@ function createOfficialModelCatalog(options = {}) {
|
|
|
2179
2596
|
getModels: (fallback) => models ?? fallback,
|
|
2180
2597
|
metadata: (modelId) => metadata.get(modelId),
|
|
2181
2598
|
revision: () => revision,
|
|
2599
|
+
capabilityGaps: () => [...metadata.values()].filter((model) => model.unsupported && /^[a-z][a-z0-9._-]{0,79}$/u.test(model.id)).slice(0, 20).map((model) => ({
|
|
2600
|
+
model: model.id,
|
|
2601
|
+
...structuredClone(model.unsupported)
|
|
2602
|
+
})),
|
|
2182
2603
|
status: () => ({
|
|
2183
2604
|
source: models === void 0 ? "fallback" : "online",
|
|
2184
2605
|
refresh: refreshStatus
|
|
@@ -3081,6 +3502,22 @@ async function createSubscriptionDiagnostics({ auth, preferences, login = { phas
|
|
|
3081
3502
|
}
|
|
3082
3503
|
const preference = preferences.status();
|
|
3083
3504
|
const catalog = modelCatalog?.status?.();
|
|
3505
|
+
const gaps = (modelCatalog?.capabilityGaps?.() ?? []).slice(0, 20).flatMap((value) => {
|
|
3506
|
+
if (!value || typeof value.model !== "string" || !/^[a-z][a-z0-9._-]{0,79}$/u.test(value.model)) return [];
|
|
3507
|
+
const fields = Object.fromEntries([
|
|
3508
|
+
"reasoning",
|
|
3509
|
+
"inputs",
|
|
3510
|
+
"speeds"
|
|
3511
|
+
].flatMap((key) => {
|
|
3512
|
+
const names = [...new Set((Array.isArray(value[key]) ? value[key] : []).filter((name) => typeof name === "string" && /^[a-z][a-z0-9_-]{0,31}$/u.test(name)))].slice(0, 16);
|
|
3513
|
+
return names.length ? [[key, names]] : [];
|
|
3514
|
+
}));
|
|
3515
|
+
return Object.keys(fields).length ? [{
|
|
3516
|
+
model: value.model,
|
|
3517
|
+
...fields
|
|
3518
|
+
}] : [];
|
|
3519
|
+
});
|
|
3520
|
+
if (gaps.length) issues.push({ code: "catalog-capabilities-not-adapted" });
|
|
3084
3521
|
return {
|
|
3085
3522
|
schemaVersion: 3,
|
|
3086
3523
|
package: "dsh-codex-subscription",
|
|
@@ -3100,7 +3537,8 @@ async function createSubscriptionDiagnostics({ auth, preferences, login = { phas
|
|
|
3100
3537
|
"failed"
|
|
3101
3538
|
].includes(catalog.refresh) ? { catalog: {
|
|
3102
3539
|
source: catalog.source,
|
|
3103
|
-
refresh: catalog.refresh
|
|
3540
|
+
refresh: catalog.refresh,
|
|
3541
|
+
...gaps.length ? { unsupported: gaps } : {}
|
|
3104
3542
|
} } : {},
|
|
3105
3543
|
configuration: {
|
|
3106
3544
|
contextMode: preference.contextMode,
|
|
@@ -4226,13 +4664,17 @@ function apply(ctx) {
|
|
|
4226
4664
|
});
|
|
4227
4665
|
const baseProvider = createOpenAICodexProvider();
|
|
4228
4666
|
let resolveAuth = async () => void 0;
|
|
4667
|
+
let subagentBackend;
|
|
4229
4668
|
const modelCatalog = createOfficialModelCatalog({
|
|
4230
4669
|
getAuth: (options) => resolveAuth(options),
|
|
4231
4670
|
readCredential: (options) => store.read(PROVIDER, options),
|
|
4232
4671
|
baseModels: () => baseProvider.getModels(),
|
|
4233
4672
|
fetch: (input, init) => network.fetch("catalog", input, init)
|
|
4234
4673
|
});
|
|
4674
|
+
const connection = createSubscriptionConnection({ resolveMode: () => settings.get().connectionMode });
|
|
4675
|
+
ctx.effect(() => () => connection.dispose());
|
|
4235
4676
|
const provider = openaiCodexSubscriptionProvider({
|
|
4677
|
+
connection,
|
|
4236
4678
|
resolveSpeedMode: () => settings.get()[SPEED_MODE_FIELD],
|
|
4237
4679
|
resolveOutputVerbosity: () => normalizeOutputVerbosity(settings.get()[OUTPUT_VERBOSITY_FIELD]),
|
|
4238
4680
|
resolveContextMode: () => normalizeContextMode(settings.get()[CONTEXT_MODE_FIELD]),
|
|
@@ -4248,6 +4690,9 @@ function apply(ctx) {
|
|
|
4248
4690
|
});
|
|
4249
4691
|
const preferences = {
|
|
4250
4692
|
status: () => ({
|
|
4693
|
+
connectionMode: settings.get().connectionMode ?? "sse",
|
|
4694
|
+
subagentBackend: settings.get().subagentBackend ?? "dsh",
|
|
4695
|
+
subagentBackendAvailable: subagentBackend !== void 0,
|
|
4251
4696
|
...readCapabilitySettings(settings.get()),
|
|
4252
4697
|
[QUICK_QUOTA_MODE_FIELD]: normalizeQuickQuotaMode(settings.get()[QUICK_QUOTA_MODE_FIELD], settings.get()[LEGACY_QUICK_QUOTA_FIELD]),
|
|
4253
4698
|
[SEARCH_PROVIDER_FIELD]: settings.get()[SEARCH_PROVIDER_FIELD],
|
|
@@ -4262,7 +4707,15 @@ function apply(ctx) {
|
|
|
4262
4707
|
fastModels: provider.getModels().filter((model) => modelCatalog.metadata(model.id)?.supportsFast ?? supportsCodexFastMode(model.id)).map((model) => model.id),
|
|
4263
4708
|
writable: ctx.settings.writable
|
|
4264
4709
|
}),
|
|
4265
|
-
update: (patch) =>
|
|
4710
|
+
update: async (patch) => {
|
|
4711
|
+
if (Object.hasOwn(patch, "subagentBackend")) {
|
|
4712
|
+
if (!subagentBackend) throw new Error("DSH subagent services are unavailable");
|
|
4713
|
+
await subagentBackend.select(patch.subagentBackend);
|
|
4714
|
+
}
|
|
4715
|
+
const rest = { ...patch };
|
|
4716
|
+
delete rest.subagentBackend;
|
|
4717
|
+
if (Object.keys(rest).length) await settings.update(rest);
|
|
4718
|
+
}
|
|
4266
4719
|
};
|
|
4267
4720
|
const authModels = createModels({ credentials: store });
|
|
4268
4721
|
authModels.setProvider(provider);
|
|
@@ -4295,6 +4748,53 @@ function apply(ctx) {
|
|
|
4295
4748
|
return profileSnapshot;
|
|
4296
4749
|
};
|
|
4297
4750
|
resolveAuth = () => authModels.getAuth(PROVIDER);
|
|
4751
|
+
ctx.inject([
|
|
4752
|
+
"subagents",
|
|
4753
|
+
"subprocess",
|
|
4754
|
+
"sandboxPolicy"
|
|
4755
|
+
], (scoped) => {
|
|
4756
|
+
const instance = createSubscriptionSubagent({
|
|
4757
|
+
ctx: scoped,
|
|
4758
|
+
nativeHome: dshHomePath("state", "codex-subscription", "native-subagent"),
|
|
4759
|
+
resolveAuth,
|
|
4760
|
+
store,
|
|
4761
|
+
refresh: (credential) => network.run("oauth", () => baseProvider.auth.oauth.refresh(credential)),
|
|
4762
|
+
loadRuntime: loadSubagentRuntime
|
|
4763
|
+
});
|
|
4764
|
+
scoped.subagents.registerProvider(instance.provider);
|
|
4765
|
+
const switcher = createSubagentBackendSwitcher({
|
|
4766
|
+
entries: () => scoped.loader.entries(),
|
|
4767
|
+
prepare: instance.prepare,
|
|
4768
|
+
persist: (mode) => settings.update({ subagentBackend: mode })
|
|
4769
|
+
});
|
|
4770
|
+
const unconfigure = scoped.on("internal/config", function(_config, next) {
|
|
4771
|
+
return switcher.configure(this, next());
|
|
4772
|
+
}, { global: true });
|
|
4773
|
+
let requested = "dsh";
|
|
4774
|
+
const select = async (mode) => {
|
|
4775
|
+
requested = mode;
|
|
4776
|
+
try {
|
|
4777
|
+
await switcher.select(mode);
|
|
4778
|
+
} catch (error) {
|
|
4779
|
+
requested = settings.get().subagentBackend ?? "dsh";
|
|
4780
|
+
throw error;
|
|
4781
|
+
}
|
|
4782
|
+
};
|
|
4783
|
+
subagentBackend = { select };
|
|
4784
|
+
const sync = (value) => {
|
|
4785
|
+
const mode = value.subagentBackend ?? "dsh";
|
|
4786
|
+
if (mode !== requested) select(mode).catch(() => scoped.logger.warn("Could not switch the subscription subagent backend"));
|
|
4787
|
+
};
|
|
4788
|
+
scoped.loader.await().then(() => sync(settings.get())).catch(() => scoped.logger.warn("Could not initialize the subscription subagent backend"));
|
|
4789
|
+
const unwatch = settings.watch(sync);
|
|
4790
|
+
scoped.effect(() => async () => {
|
|
4791
|
+
unwatch();
|
|
4792
|
+
unconfigure();
|
|
4793
|
+
subagentBackend = void 0;
|
|
4794
|
+
instance.dispose();
|
|
4795
|
+
await switcher.dispose();
|
|
4796
|
+
}, "codex-subscription: subagent backend");
|
|
4797
|
+
});
|
|
4298
4798
|
const adapterAuth = Object.freeze({
|
|
4299
4799
|
credentials: store,
|
|
4300
4800
|
authContext: Object.freeze({
|