@tnnevol/dsh-codex-auth 0.1.0-rc.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.js ADDED
@@ -0,0 +1,751 @@
1
+ import { a as CODEX_PROVIDER, i as CODEX_AUTH_FILENAME, n as loginCodex, o as CodexCredentialStore, r as logoutCodex, s as codexAuthPath, t as codexAuthStatus } from "./auth-DQ2F4vc6.js";
2
+ import { CODEX_AUTH_LOGIN_PATH, CODEX_AUTH_LOGOUT_PATH, CODEX_AUTH_SETTINGS_NAMESPACE, CODEX_AUTH_STATUS_PATH, CODEX_USAGE_PATH } from "./auth-paths.js";
3
+ import { createModels } from "@earendil-works/pi-ai";
4
+ import { openaiCodexProvider } from "@earendil-works/pi-ai/providers/openai-codex";
5
+ import { basename } from "node:path";
6
+ import { credentialRef } from "@deepseek-ai/dsh-credentials";
7
+ import z from "@deepseek-ai/schemastery";
8
+ import { settingsNamespace } from "@deepseek-ai/dsh-settings";
9
+ import { AttachmentId } from "@deepseek-ai/dsh-attachment";
10
+ import { createUserMessage, resolveRetryPolicy } from "@deepseek-ai/dsh-llm";
11
+ import { defineTool } from "@deepseek-ai/dsh-tools";
12
+ import { PiAiAdapter } from "@deepseek-ai/dsh-llm-pi-ai";
13
+ //#region src/usage.ts
14
+ /** Read-only Codex account quota information for the local settings card. */
15
+ const CODEX_USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
16
+ const CODEX_USAGE_TIMEOUT_MS = 1e4;
17
+ function record(value) {
18
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
19
+ }
20
+ function number(value) {
21
+ if (typeof value === "number") return Number.isFinite(value) ? value : void 0;
22
+ if (typeof value !== "string" || value.trim() === "") return void 0;
23
+ const parsed = Number(value);
24
+ return Number.isFinite(parsed) ? parsed : void 0;
25
+ }
26
+ function boolean(value) {
27
+ return typeof value === "boolean" ? value : void 0;
28
+ }
29
+ function string(value) {
30
+ if (typeof value === "string" && value.length > 0) return value;
31
+ if (typeof value === "number" && Number.isFinite(value)) return String(value);
32
+ }
33
+ function percentage(value) {
34
+ return Math.max(0, Math.min(100, value));
35
+ }
36
+ function window(value) {
37
+ const source = record(value);
38
+ if (source === void 0) return void 0;
39
+ const result = {};
40
+ const usedPercent = number(source["used_percent"]);
41
+ const explicitRemainingPercent = number(source["remaining_percent"]);
42
+ const limitWindowSeconds = number(source["limit_window_seconds"]);
43
+ const resetAfterSeconds = number(source["reset_after_seconds"]);
44
+ const resetAt = number(source["reset_at"]);
45
+ if (explicitRemainingPercent !== void 0) result.remainingPercent = percentage(explicitRemainingPercent);
46
+ else if (usedPercent !== void 0) result.remainingPercent = percentage(100 - usedPercent);
47
+ if (limitWindowSeconds !== void 0) result.limitWindowSeconds = limitWindowSeconds;
48
+ if (resetAfterSeconds !== void 0) result.resetAfterSeconds = resetAfterSeconds;
49
+ if (resetAt !== void 0) result.resetAt = resetAt;
50
+ return Object.keys(result).length > 0 ? result : void 0;
51
+ }
52
+ function credits(value) {
53
+ const source = record(value);
54
+ if (source === void 0) return void 0;
55
+ const result = {};
56
+ const hasCredits = boolean(source["has_credits"]);
57
+ const unlimited = boolean(source["unlimited"]);
58
+ const balance = string(source["balance"]);
59
+ if (hasCredits !== void 0) result.hasCredits = hasCredits;
60
+ if (unlimited !== void 0) result.unlimited = unlimited;
61
+ if (balance !== void 0) result.balance = balance;
62
+ return Object.keys(result).length > 0 ? result : void 0;
63
+ }
64
+ /** Normalize the evolving private WHAM response into a small UI-safe shape. */
65
+ function normalizeCodexUsagePayload(value) {
66
+ const source = record(value);
67
+ if (source === void 0) throw new Error("Codex usage response was not an object");
68
+ const rateLimit = record(source["rate_limit"]);
69
+ const result = {};
70
+ const planType = string(source["plan_type"]);
71
+ const allowed = boolean(rateLimit?.["allowed"]);
72
+ const limitReached = boolean(rateLimit?.["limit_reached"]);
73
+ const primaryWindow = window(rateLimit?.["primary_window"]);
74
+ const secondaryWindow = window(rateLimit?.["secondary_window"]);
75
+ const quota = credits(source["credits"]);
76
+ if (planType !== void 0) result.planType = planType;
77
+ if (allowed !== void 0) result.allowed = allowed;
78
+ if (limitReached !== void 0) result.limitReached = limitReached;
79
+ if (primaryWindow !== void 0) result.primaryWindow = primaryWindow;
80
+ if (secondaryWindow !== void 0) result.secondaryWindow = secondaryWindow;
81
+ if (quota !== void 0) result.credits = quota;
82
+ return result;
83
+ }
84
+ function accessToken(auth) {
85
+ return typeof auth?.apiKey === "string" && auth.apiKey.length > 0 ? auth.apiKey : void 0;
86
+ }
87
+ function accountId(credential) {
88
+ if (credential?.type !== "oauth") return void 0;
89
+ return typeof credential.accountId === "string" && credential.accountId.length > 0 ? credential.accountId : void 0;
90
+ }
91
+ /** Resolves OAuth (including refresh) before making the quota request. */
92
+ var CodexUsageService = class {
93
+ store;
94
+ models;
95
+ operation;
96
+ constructor(store) {
97
+ this.store = store;
98
+ this.models = createModels({ credentials: store });
99
+ this.models.setProvider(openaiCodexProvider());
100
+ }
101
+ async read() {
102
+ if (this.operation !== void 0) return this.operation;
103
+ const operation = this.readNow();
104
+ this.operation = operation;
105
+ try {
106
+ return await operation;
107
+ } finally {
108
+ if (this.operation === operation) this.operation = void 0;
109
+ }
110
+ }
111
+ async readNow() {
112
+ const token = accessToken((await this.models.getAuth(CODEX_PROVIDER))?.auth);
113
+ if (token === void 0) return void 0;
114
+ const account = accountId(await this.store.read(CODEX_PROVIDER));
115
+ if (account === void 0) return void 0;
116
+ const controller = new AbortController();
117
+ const timer = setTimeout(() => {
118
+ controller.abort();
119
+ }, CODEX_USAGE_TIMEOUT_MS);
120
+ try {
121
+ const response = await fetch(CODEX_USAGE_URL, {
122
+ method: "GET",
123
+ headers: {
124
+ accept: "application/json",
125
+ authorization: `Bearer ${token}`,
126
+ "ChatGPT-Account-Id": account,
127
+ "user-agent": "dsh-codex-auth-plugin"
128
+ },
129
+ signal: controller.signal
130
+ });
131
+ if (!response.ok) throw new Error(`Codex usage request failed with status ${String(response.status)}`);
132
+ return normalizeCodexUsagePayload(await response.json());
133
+ } finally {
134
+ clearTimeout(timer);
135
+ }
136
+ }
137
+ };
138
+ //#endregion
139
+ //#region src/auth-routes.ts
140
+ const CODEX_AUTH_URL_TIMEOUT_MS = 3e4;
141
+ const REMOTE_WEB_ORIGIN_NOT_TRUSTED = "remote-web-origin-not-trusted";
142
+ const CODEX_USAGE_UNAVAILABLE = "codex-usage-unavailable";
143
+ function signedInStatus(expiresAt) {
144
+ return expiresAt === void 0 ? { status: "signed-in" } : {
145
+ status: "signed-in",
146
+ expiresAt
147
+ };
148
+ }
149
+ function safeMessage(error) {
150
+ return (error instanceof Error ? error.message : String(error)).replace(/\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/gu, "[redacted token]").replace(/(\b(?:code|token|refresh_token|access_token)=)[^&\s]+/giu, "$1[redacted]").slice(0, 1e3);
151
+ }
152
+ function waitForPromptAbort(prompt) {
153
+ const signal = prompt.signal;
154
+ if (signal === void 0) return new Promise(() => {});
155
+ if (signal.aborted) return Promise.reject(signal.reason);
156
+ return new Promise((_resolve, reject) => {
157
+ signal.addEventListener("abort", () => {
158
+ reject(signal.reason);
159
+ }, { once: true });
160
+ });
161
+ }
162
+ /** Owns one device-code login operation and prevents duplicate auth requests. */
163
+ var CodexWebAuth = class {
164
+ store;
165
+ challengeTimeoutMs;
166
+ mirror;
167
+ state = { status: "signed-out" };
168
+ operation;
169
+ cancellation;
170
+ challenge;
171
+ challengeWaiters = [];
172
+ challengeTimer;
173
+ constructor(store, challengeTimeoutMs = CODEX_AUTH_URL_TIMEOUT_MS, mirror) {
174
+ this.store = store;
175
+ this.challengeTimeoutMs = challengeTimeoutMs;
176
+ this.mirror = mirror;
177
+ if (!Number.isFinite(challengeTimeoutMs) || challengeTimeoutMs <= 0) throw new TypeError("Codex auth URL timeout must be a positive finite number");
178
+ }
179
+ async status() {
180
+ if (this.operation !== void 0) return this.state;
181
+ if (this.state.status === "error") return this.state;
182
+ const stored = await codexAuthStatus(this.store);
183
+ if (stored.authenticated) {
184
+ await this.mirror?.sync();
185
+ return signedInStatus(stored.expiresAt);
186
+ }
187
+ await this.mirror?.clear();
188
+ return { status: "signed-out" };
189
+ }
190
+ async signIn() {
191
+ if (this.operation === void 0) this.start();
192
+ if (this.challenge !== void 0) return this.challenge;
193
+ return new Promise((resolve, reject) => {
194
+ this.challengeWaiters.push({
195
+ resolve,
196
+ reject
197
+ });
198
+ });
199
+ }
200
+ async signOut() {
201
+ this.cancelSignIn(/* @__PURE__ */ new Error("Codex sign-in cancelled"));
202
+ await this.operation?.catch(() => void 0);
203
+ await logoutCodex(this.store);
204
+ await this.mirror?.clear();
205
+ this.challenge = void 0;
206
+ this.state = { status: "signed-out" };
207
+ }
208
+ async dispose() {
209
+ this.cancelSignIn(/* @__PURE__ */ new Error("Codex auth plugin disposed"));
210
+ await this.operation?.catch(() => void 0);
211
+ }
212
+ start() {
213
+ const cancellation = new AbortController();
214
+ this.cancellation = cancellation;
215
+ this.challenge = void 0;
216
+ this.state = { status: "signing-in" };
217
+ this.challengeTimer = setTimeout(() => {
218
+ this.cancelSignIn(/* @__PURE__ */ new Error(`Codex did not provide an authorization code within ${String(this.challengeTimeoutMs)}ms`));
219
+ }, this.challengeTimeoutMs);
220
+ this.challengeTimer.unref();
221
+ this.operation = loginCodex({
222
+ signal: cancellation.signal,
223
+ prompt: (prompt) => {
224
+ if (prompt.type === "select") return Promise.resolve("device_code");
225
+ return waitForPromptAbort(prompt);
226
+ },
227
+ notify: (event) => {
228
+ this.onEvent(event);
229
+ }
230
+ }, this.store).then(async () => {
231
+ if (this.challenge === void 0) {
232
+ const error = /* @__PURE__ */ new Error("Codex sign-in finished without an authorization code");
233
+ this.rejectChallenge(error);
234
+ this.state = {
235
+ status: "error",
236
+ message: safeMessage(error)
237
+ };
238
+ return;
239
+ }
240
+ const stored = await codexAuthStatus(this.store);
241
+ if (stored.authenticated) await this.mirror?.sync();
242
+ this.state = stored.authenticated ? signedInStatus(stored.expiresAt) : { status: "signed-out" };
243
+ }, (error) => {
244
+ this.rejectChallenge(error);
245
+ this.state = {
246
+ status: "error",
247
+ message: safeMessage(error)
248
+ };
249
+ }).finally(() => {
250
+ this.clearChallengeTimer();
251
+ this.operation = void 0;
252
+ this.cancellation = void 0;
253
+ });
254
+ }
255
+ onEvent(event) {
256
+ if (event.type !== "device_code") return;
257
+ let url;
258
+ try {
259
+ url = new URL(event.verificationUri);
260
+ } catch {
261
+ this.cancelSignIn(/* @__PURE__ */ new Error("OpenAI returned an invalid Codex authorization URL"));
262
+ return;
263
+ }
264
+ if (url.protocol !== "https:" || url.username !== "" || url.password !== "") {
265
+ this.cancelSignIn(/* @__PURE__ */ new Error("OpenAI returned an unsafe Codex authorization URL"));
266
+ return;
267
+ }
268
+ if (event.userCode.trim().length === 0) {
269
+ this.cancelSignIn(/* @__PURE__ */ new Error("OpenAI returned an empty Codex authorization code"));
270
+ return;
271
+ }
272
+ this.challenge = {
273
+ type: "device_code",
274
+ userCode: event.userCode,
275
+ verificationUri: event.verificationUri,
276
+ ...event.intervalSeconds === void 0 ? {} : { intervalSeconds: event.intervalSeconds },
277
+ ...event.expiresInSeconds === void 0 ? {} : { expiresInSeconds: event.expiresInSeconds }
278
+ };
279
+ this.clearChallengeTimer();
280
+ for (const waiter of this.challengeWaiters.splice(0)) waiter.resolve(this.challenge);
281
+ }
282
+ rejectChallenge(error) {
283
+ this.clearChallengeTimer();
284
+ for (const waiter of this.challengeWaiters.splice(0)) waiter.reject(error);
285
+ }
286
+ clearChallengeTimer() {
287
+ if (this.challengeTimer === void 0) return;
288
+ clearTimeout(this.challengeTimer);
289
+ this.challengeTimer = void 0;
290
+ }
291
+ cancelSignIn(error) {
292
+ this.rejectChallenge(error);
293
+ this.cancellation?.abort(error);
294
+ }
295
+ };
296
+ function header(req, name) {
297
+ const value = req.headers[name];
298
+ return Array.isArray(value) ? value[0] : value;
299
+ }
300
+ function firstForwarded(value) {
301
+ return value?.split(",")[0]?.trim() || void 0;
302
+ }
303
+ function requestOrigin(req) {
304
+ const host = firstForwarded(header(req, "x-forwarded-host")) ?? header(req, "host");
305
+ if (host === void 0) return void 0;
306
+ const proto = firstForwarded(header(req, "x-forwarded-proto")) ?? (req.socket.encrypted === true ? "https" : "http");
307
+ try {
308
+ return new URL(`${proto}://${host}`).origin;
309
+ } catch {
310
+ return;
311
+ }
312
+ }
313
+ function normalizeOrigin(raw) {
314
+ try {
315
+ return new URL(raw).origin;
316
+ } catch {
317
+ return;
318
+ }
319
+ }
320
+ function localPeer(req) {
321
+ const remote = req.socket.remoteAddress;
322
+ return remote === "127.0.0.1" || remote === "::1" || remote === "::ffff:127.0.0.1";
323
+ }
324
+ /** Protect mutating routes while allowing the NAS app's loopback proxy. */
325
+ function trustedRequest(req) {
326
+ if (header(req, "sec-fetch-site")?.trim().toLowerCase() === "cross-site") return false;
327
+ const origin = header(req, "origin");
328
+ if (origin !== void 0) return requestOrigin(req) === normalizeOrigin(origin);
329
+ return localPeer(req);
330
+ }
331
+ function json(res, status, value) {
332
+ res.writeHead(status, {
333
+ "content-type": "application/json; charset=utf-8",
334
+ "cache-control": "no-store",
335
+ "x-content-type-options": "nosniff"
336
+ });
337
+ res.end(JSON.stringify(value));
338
+ }
339
+ /** Register the auth endpoints when the DSH Web server is available. */
340
+ function registerCodexAuthRoutes(ctx, store, mirror) {
341
+ const auth = new CodexWebAuth(store, CODEX_AUTH_URL_TIMEOUT_MS, mirror);
342
+ const usage = new CodexUsageService(store);
343
+ ctx.effect(() => {
344
+ const authorize = (req, res) => {
345
+ if (trustedRequest(req)) return true;
346
+ json(res, 403, { error: REMOTE_WEB_ORIGIN_NOT_TRUSTED });
347
+ return false;
348
+ };
349
+ const routes = [
350
+ ctx.webServer.register({
351
+ kind: "exact",
352
+ path: CODEX_AUTH_STATUS_PATH,
353
+ handler: async (req, res) => {
354
+ if (req.method !== "GET") return json(res, 405, { error: "method not allowed" });
355
+ if (!authorize(req, res)) return;
356
+ try {
357
+ json(res, 200, await auth.status());
358
+ } catch (error) {
359
+ json(res, 500, { error: safeMessage(error) });
360
+ }
361
+ }
362
+ }),
363
+ ctx.webServer.register({
364
+ kind: "exact",
365
+ path: CODEX_AUTH_LOGIN_PATH,
366
+ handler: async (req, res) => {
367
+ if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
368
+ if (!authorize(req, res)) return;
369
+ try {
370
+ json(res, 200, await auth.signIn());
371
+ } catch (error) {
372
+ json(res, 500, { error: safeMessage(error) });
373
+ }
374
+ }
375
+ }),
376
+ ctx.webServer.register({
377
+ kind: "exact",
378
+ path: CODEX_AUTH_LOGOUT_PATH,
379
+ handler: async (req, res) => {
380
+ if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
381
+ if (!authorize(req, res)) return;
382
+ try {
383
+ await auth.signOut();
384
+ json(res, 200, { ok: true });
385
+ } catch (error) {
386
+ json(res, 500, { error: safeMessage(error) });
387
+ }
388
+ }
389
+ }),
390
+ ctx.webServer.register({
391
+ kind: "exact",
392
+ path: CODEX_USAGE_PATH,
393
+ handler: async (req, res) => {
394
+ if (req.method !== "GET") return json(res, 405, { error: "method not allowed" });
395
+ if (!authorize(req, res)) return;
396
+ try {
397
+ const value = await usage.read();
398
+ if (value === void 0) return json(res, 401, { error: "not-signed-in" });
399
+ json(res, 200, value);
400
+ } catch {
401
+ json(res, 502, { error: CODEX_USAGE_UNAVAILABLE });
402
+ }
403
+ }
404
+ })
405
+ ];
406
+ return async () => {
407
+ for (const dispose of routes) dispose();
408
+ await auth.dispose();
409
+ };
410
+ }, "dsh-codex-auth-plugin: Web OAuth routes");
411
+ }
412
+ //#endregion
413
+ //#region src/credential-mirror.ts
414
+ /** Bridge the plugin-owned OAuth credential into dsh's generic LLM seam. */
415
+ /** Credential reference exposed to the official `llm-pi-ai` model settings UI. */
416
+ const CODEX_API_KEY_ENV = "OPENAI_CODEX_AUTH_TOKEN";
417
+ const CODEX_API_KEY_REF = credentialRef(CODEX_API_KEY_ENV);
418
+ /**
419
+ * Makes the plugin-owned OAuth token visible to the generic dsh adapter.
420
+ *
421
+ * `llm-pi-ai` deliberately resolves named credentials through `ctx.credentials`
422
+ * on every request. The OAuth document remains the source of truth; this class
423
+ * only mirrors the short-lived access token so the official Models page can
424
+ * show its configured state and the generic adapter can send Codex requests.
425
+ */
426
+ var CodexCredentialMirror = class {
427
+ credentials;
428
+ store;
429
+ models;
430
+ operation;
431
+ constructor(credentials, store) {
432
+ this.credentials = credentials;
433
+ this.store = store;
434
+ this.models = createModels({ credentials: this.store });
435
+ this.models.setProvider(openaiCodexProvider());
436
+ }
437
+ /** Refresh OAuth when needed, then update the generic dsh credential seam. */
438
+ async sync() {
439
+ if (this.operation !== void 0) return this.operation;
440
+ const operation = this.syncNow();
441
+ this.operation = operation;
442
+ try {
443
+ await operation;
444
+ } finally {
445
+ if (this.operation === operation) this.operation = void 0;
446
+ }
447
+ }
448
+ /** Remove the mirrored token after the plugin-owned account signs out. */
449
+ async clear() {
450
+ await this.operation?.catch(() => void 0);
451
+ await this.credentials.unset(CODEX_API_KEY_REF);
452
+ }
453
+ async syncNow() {
454
+ const accessToken = (await this.models.getAuth(CODEX_PROVIDER))?.auth.apiKey;
455
+ if (accessToken !== void 0 && accessToken.length > 0) {
456
+ await this.credentials.set(CODEX_API_KEY_REF, accessToken);
457
+ return;
458
+ }
459
+ await this.credentials.unset(CODEX_API_KEY_REF);
460
+ }
461
+ };
462
+ //#endregion
463
+ //#region src/settings-contract.ts
464
+ const DEFAULT_CODEX_AUTH_SETTINGS = Object.freeze({
465
+ enableImageTool: false,
466
+ enableImageUpload: false
467
+ });
468
+ //#endregion
469
+ //#region src/settings.ts
470
+ /** Host settings registration that makes the standalone auth card discoverable. */
471
+ /** Branded namespace used by the Host settings service. */
472
+ const CODEX_AUTH_SETTINGS_NS = settingsNamespace(CODEX_AUTH_SETTINGS_NAMESPACE);
473
+ /** Settings schema for the plugin card and its optional image capability. */
474
+ const CodexAuthSettingsSchema = z.object({
475
+ enableImageTool: z.boolean().default(DEFAULT_CODEX_AUTH_SETTINGS.enableImageTool),
476
+ enableImageUpload: z.boolean().default(DEFAULT_CODEX_AUTH_SETTINGS.enableImageUpload)
477
+ });
478
+ //#endregion
479
+ //#region src/view-image.ts
480
+ /** Optional Codex image-reading tool, modeled on DSH's durable attachment seam. */
481
+ /** Stable name of the optional image-recognition tool. */
482
+ const VIEW_IMAGE_TOOL_NAME = "view_image";
483
+ function imageRefOf(image) {
484
+ return {
485
+ attachmentId: AttachmentId(image.attachmentId),
486
+ mediaType: image.mediaType,
487
+ bytes: image.bytes,
488
+ width: image.width,
489
+ height: image.height,
490
+ ...image.name === void 0 ? {} : { name: image.name }
491
+ };
492
+ }
493
+ function contentOf(value) {
494
+ return [{
495
+ type: "text",
496
+ text: `<source>${value.source}</source>\n<image>${value.image.mediaType}, ${value.image.width}x${value.image.height} px, ${value.image.bytes} bytes</image>`
497
+ }, {
498
+ type: "image",
499
+ attachment: imageRefOf(value.image)
500
+ }];
501
+ }
502
+ function mediaTypeOf(data) {
503
+ if (data.length >= 8 && data[0] === 137 && data[1] === 80 && data[2] === 78 && data[3] === 71 && data[4] === 13 && data[5] === 10 && data[6] === 26 && data[7] === 10) return "image/png";
504
+ if (data.length >= 3 && data[0] === 255 && data[1] === 216 && data[2] === 255) return "image/jpeg";
505
+ if (data.length >= 6) {
506
+ const signature = String.fromCharCode(...data.subarray(0, 6));
507
+ if (signature === "GIF87a" || signature === "GIF89a") return "image/gif";
508
+ }
509
+ if (data.length >= 12 && String.fromCharCode(...data.subarray(0, 4)) === "RIFF" && String.fromCharCode(...data.subarray(8, 12)) === "WEBP") return "image/webp";
510
+ }
511
+ async function assertImageCapable(ctx, exec, source) {
512
+ const routed = exec.agent?.session.requestHeader()?.config;
513
+ const provider = routed?.provider ?? exec.agent?.options.provider;
514
+ const model = routed?.model ?? exec.agent?.options.model;
515
+ if (provider === void 0 || model === void 0) throw new Error(`cannot view ${JSON.stringify(source)}: the current model route is unavailable`);
516
+ const info = await ctx.llm.resolveModelInfo(provider, model, exec.signal);
517
+ if (info.inputModalities === void 0 || !info.inputModalities.includes("image")) throw new Error(`cannot view ${JSON.stringify(source)}: model "${model}" does not declare image input`);
518
+ }
519
+ /** Register a local-file image tool in the current DSH tool scope. */
520
+ function viewImageTool(ctx) {
521
+ return defineTool({
522
+ name: VIEW_IMAGE_TOOL_NAME,
523
+ description: "View a local PNG, JPEG, WebP, or GIF image and return it to an image-capable model.",
524
+ parameters: { source: {
525
+ type: "string",
526
+ required: true,
527
+ description: "Absolute or workspace-relative local image path."
528
+ } },
529
+ output: {
530
+ schema: {
531
+ type: "object",
532
+ additionalProperties: false,
533
+ properties: {
534
+ source: {
535
+ type: "string",
536
+ required: true
537
+ },
538
+ image: {
539
+ type: "object",
540
+ required: true,
541
+ additionalProperties: false,
542
+ properties: {
543
+ attachmentId: {
544
+ type: "string",
545
+ required: true
546
+ },
547
+ mediaType: {
548
+ type: "string",
549
+ required: true,
550
+ enum: [
551
+ "image/png",
552
+ "image/jpeg",
553
+ "image/webp",
554
+ "image/gif"
555
+ ]
556
+ },
557
+ bytes: {
558
+ type: "integer",
559
+ required: true
560
+ },
561
+ width: {
562
+ type: "integer",
563
+ required: true
564
+ },
565
+ height: {
566
+ type: "integer",
567
+ required: true
568
+ },
569
+ name: { type: "string" }
570
+ }
571
+ }
572
+ }
573
+ },
574
+ render: (_args, value) => contentOf(value)
575
+ },
576
+ isConcurrencySafe: () => true,
577
+ async execute(args, exec) {
578
+ const source = args.source.trim();
579
+ if (source.length === 0) throw new Error("view_image source must not be empty");
580
+ await assertImageCapable(ctx, exec, source);
581
+ const attachments = ctx.attachments;
582
+ const maxBytes = Math.min(attachments.imageLimits.maxImageBytes, attachments.imageLimits.maxMessageImageBytes);
583
+ const cwd = exec.agent?.session.header.cwd;
584
+ const target = await ctx.fs.resolve(source, {
585
+ ...cwd === void 0 ? {} : { cwd },
586
+ signal: exec.signal
587
+ });
588
+ const info = await ctx.fs.stat(target, exec.signal);
589
+ if (info === void 0) throw new Error(`image path does not exist: ${source}`);
590
+ if (info.type !== "file") throw new Error(`image path is not a regular file: ${source}`);
591
+ const data = await ctx.fs.readBytes(target, exec.signal, maxBytes);
592
+ ctx.emit("fs/observed", target, {
593
+ kind: "present",
594
+ version: info.version
595
+ }, exec);
596
+ const mediaType = mediaTypeOf(data);
597
+ if (mediaType === void 0) throw new Error("view_image supports PNG, JPEG, WebP, and GIF image bytes");
598
+ if (!attachments.imageLimits.mediaTypes.includes(mediaType)) throw new Error(`${mediaType} images are disabled by this deployment`);
599
+ const name = basename(target.displayPath);
600
+ const image = {
601
+ data,
602
+ mediaType,
603
+ ...name.length === 0 ? {} : { name }
604
+ };
605
+ await attachments.validateImage(image);
606
+ const ref = await attachments.saveImage(image);
607
+ const value = {
608
+ source: target.displayPath,
609
+ image: {
610
+ attachmentId: ref.attachmentId,
611
+ mediaType: ref.mediaType,
612
+ bytes: ref.bytes,
613
+ width: ref.width,
614
+ height: ref.height,
615
+ ...ref.name === void 0 ? {} : { name: ref.name }
616
+ }
617
+ };
618
+ if (exec.parent !== void 0) exec.deferContext(createUserMessage({
619
+ content: contentOf(value),
620
+ source: {
621
+ kind: "plugin",
622
+ plugin: "@tnnevol/dsh-codex-auth"
623
+ }
624
+ }));
625
+ return value;
626
+ },
627
+ presentCall: (args) => ({
628
+ card: "generic",
629
+ title: `View image ${args.source}`,
630
+ kind: "read",
631
+ locations: [{ path: args.source }]
632
+ })
633
+ });
634
+ }
635
+ //#endregion
636
+ //#region src/adapter.ts
637
+ /** OpenAI Codex adapter assembled from dsh's public pi-ai extension seam. */
638
+ /** Keep the Codex stream open while the provider is still producing output. */
639
+ const CODEX_STREAM_IDLE_TIMEOUT_MS = 3e5;
640
+ /**
641
+ * Give dsh's generic adapter the bearer token resolved by the plugin-owned
642
+ * OAuth store. This keeps the provider-native login flow separate from model
643
+ * requests while preserving pi-ai's Codex endpoint and model catalog.
644
+ */
645
+ function requestProvider(provider) {
646
+ return {
647
+ ...provider,
648
+ auth: {
649
+ ...provider.auth,
650
+ apiKey: {
651
+ name: "OpenAI Codex OAuth bearer token",
652
+ async resolve({ credential }) {
653
+ const apiKey = credential?.key;
654
+ return apiKey === void 0 || apiKey.length === 0 ? void 0 : {
655
+ auth: { apiKey },
656
+ source: "OAuth"
657
+ };
658
+ }
659
+ }
660
+ }
661
+ };
662
+ }
663
+ /** Create the dsh LLM adapter for the provider-native OpenAI Codex catalog. */
664
+ function createCodexAdapter(credentials, resolveAttachments) {
665
+ const provider = openaiCodexProvider();
666
+ const profiles = /* @__PURE__ */ new Map([[CODEX_PROVIDER, {
667
+ provider: CODEX_PROVIDER,
668
+ displayName: "OpenAI Codex",
669
+ streamIdleTimeoutMs: CODEX_STREAM_IDLE_TIMEOUT_MS,
670
+ retryPolicy: resolveRetryPolicy(void 0, "dsh-codex-auth-plugin retryPolicy"),
671
+ configuredMaxTokens: /* @__PURE__ */ new Map(),
672
+ piProvider: requestProvider(provider)
673
+ }]]);
674
+ const models = createModels({ credentials });
675
+ models.setProvider(provider);
676
+ return new PiAiAdapter({
677
+ profiles: () => profiles,
678
+ resolveApiKey: async () => (await models.getAuth(CODEX_PROVIDER))?.auth.apiKey,
679
+ resolveAttachments
680
+ });
681
+ }
682
+ //#endregion
683
+ //#region src/index.ts
684
+ /** Stable Host bundle name. */
685
+ const name = "dsh-codex-auth-plugin";
686
+ /** Host services required by the routes, settings card, and credential mirror. */
687
+ const inject = [
688
+ "webServer",
689
+ "settings",
690
+ "credentials"
691
+ ];
692
+ function apply(ctx) {
693
+ const settings = ctx.settings.register(CODEX_AUTH_SETTINGS_NS, CodexAuthSettingsSchema);
694
+ const store = new CodexCredentialStore();
695
+ const mirror = new CodexCredentialMirror(ctx.credentials, store);
696
+ const syncMirror = () => {
697
+ mirror.sync().catch((error) => {
698
+ ctx.logger.warn("dsh-codex-auth: failed to synchronize the Codex credential with dsh", error);
699
+ });
700
+ };
701
+ ctx.effect(() => {
702
+ syncMirror();
703
+ const timer = setInterval(syncMirror, 6e4);
704
+ return () => {
705
+ clearInterval(timer);
706
+ };
707
+ }, "dsh-codex-auth: credential mirror");
708
+ registerCodexAuthRoutes(ctx, store, mirror);
709
+ let stopped = false;
710
+ let imageFiber;
711
+ let imageTail = Promise.resolve();
712
+ const reconcileImageTool = async () => {
713
+ if (stopped) return;
714
+ const enabled = settings.get().enableImageTool;
715
+ if (enabled === (imageFiber !== void 0)) return;
716
+ const previous = imageFiber;
717
+ imageFiber = void 0;
718
+ if (previous !== void 0) await previous.dispose();
719
+ if (stopped || !enabled) return;
720
+ const fiber = ctx.inject([
721
+ "tools",
722
+ "fs",
723
+ "attachments",
724
+ "llm"
725
+ ], (toolCtx) => toolCtx.tools.register(viewImageTool(toolCtx)));
726
+ imageFiber = fiber;
727
+ Promise.resolve(fiber).catch((error) => {
728
+ if (imageFiber === fiber) imageFiber = void 0;
729
+ ctx.logger.error("dsh-codex-auth: optional view_image tool failed to activate");
730
+ ctx.logger.error(error);
731
+ });
732
+ };
733
+ const scheduleImageTool = () => {
734
+ imageTail = imageTail.then(reconcileImageTool, reconcileImageTool).catch((error) => {
735
+ ctx.logger.error("dsh-codex-auth: could not apply the image-recognition configuration");
736
+ ctx.logger.error(error);
737
+ });
738
+ };
739
+ const unwatch = settings.watch(scheduleImageTool);
740
+ ctx.effect(() => async () => {
741
+ stopped = true;
742
+ unwatch();
743
+ await imageTail;
744
+ const image = imageFiber;
745
+ imageFiber = void 0;
746
+ await image?.dispose();
747
+ }, "dsh-codex-auth: optional image-tool lifecycle");
748
+ scheduleImageTool();
749
+ }
750
+ //#endregion
751
+ export { CODEX_API_KEY_ENV, CODEX_API_KEY_REF, CODEX_AUTH_FILENAME, CODEX_AUTH_LOGIN_PATH, CODEX_AUTH_LOGOUT_PATH, CODEX_AUTH_SETTINGS_NAMESPACE, CODEX_AUTH_STATUS_PATH, CODEX_PROVIDER, CODEX_STREAM_IDLE_TIMEOUT_MS, CodexCredentialMirror, CodexCredentialStore, CodexUsageService, CodexWebAuth, apply, codexAuthPath, codexAuthStatus, createCodexAdapter, inject, loginCodex, logoutCodex, name, normalizeCodexUsagePayload, registerCodexAuthRoutes, trustedRequest };