dsh-oauth-login 0.2.4

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.
Files changed (64) hide show
  1. package/INSTALL.md +59 -0
  2. package/INSTALL.zh.md +45 -0
  3. package/LICENSE +201 -0
  4. package/README.en.md +104 -0
  5. package/README.md +104 -0
  6. package/README.zh.md +3 -0
  7. package/SECURITY.md +33 -0
  8. package/cordis.patch.yml +5 -0
  9. package/docs/network-proxy.md +45 -0
  10. package/docs/openrouter-sync.md +84 -0
  11. package/docs/screenshots/01-providers-signed-out.png +0 -0
  12. package/docs/screenshots/03-copilot-signing-in.png +0 -0
  13. package/docs/screenshots/04-mixed-live-states.png +0 -0
  14. package/docs/screenshots/05-copilot-sign-in.gif +0 -0
  15. package/docs/screenshots/06-cli-status-store.png +0 -0
  16. package/lib/bin.d.ts +5 -0
  17. package/lib/bin.js +2132 -0
  18. package/lib/client.js +1915 -0
  19. package/lib/client.js.map +1 -0
  20. package/lib/index.d.ts +482 -0
  21. package/lib/index.js +3105 -0
  22. package/lib/invariant.d.ts +7 -0
  23. package/lib/invariant.js +8 -0
  24. package/package.json +180 -0
  25. package/src/adapter.ts +207 -0
  26. package/src/auth-routes.ts +654 -0
  27. package/src/auth.ts +76 -0
  28. package/src/bin.ts +185 -0
  29. package/src/catalog.ts +129 -0
  30. package/src/client/OpenRouterSyncStatus.tsx +66 -0
  31. package/src/client/PiLoginSettings.tsx +470 -0
  32. package/src/client/ProxySettings.tsx +123 -0
  33. package/src/client/SearchableModelSelect.tsx +595 -0
  34. package/src/client/draft-input.ts +12 -0
  35. package/src/client/index.tsx +71 -0
  36. package/src/client/locales.ts +176 -0
  37. package/src/client/login-input-copy.ts +34 -0
  38. package/src/client/login-window.ts +24 -0
  39. package/src/client/model-filter.ts +45 -0
  40. package/src/client/openrouter-store.ts +122 -0
  41. package/src/client/proxy-draft.ts +57 -0
  42. package/src/extra-models.ts +201 -0
  43. package/src/hosted-capture.ts +41 -0
  44. package/src/hosted-images.ts +154 -0
  45. package/src/http.ts +241 -0
  46. package/src/ids.ts +17 -0
  47. package/src/index.ts +183 -0
  48. package/src/invariant.ts +12 -0
  49. package/src/model-error-hint.ts +98 -0
  50. package/src/native-tools.ts +442 -0
  51. package/src/oauth-refresh.ts +49 -0
  52. package/src/openrouter-catalog.ts +245 -0
  53. package/src/openrouter-free.ts +76 -0
  54. package/src/openrouter-models.ts +155 -0
  55. package/src/openrouter-types.ts +40 -0
  56. package/src/plugin-config.ts +30 -0
  57. package/src/provider.ts +137 -0
  58. package/src/proxy-config.ts +61 -0
  59. package/src/proxy-store.ts +45 -0
  60. package/src/proxy-transport.ts +220 -0
  61. package/src/redact.ts +22 -0
  62. package/src/responses-tap.ts +97 -0
  63. package/src/session.ts +104 -0
  64. package/src/store.ts +234 -0
package/lib/index.js ADDED
@@ -0,0 +1,3105 @@
1
+ import { LlmError, ReasoningEffortId, RetryPolicySchema, resolveRetryPolicy } from "@deepseek-ai/dsh-llm";
2
+ import { createModels, defaultProviderAuthContext } from "@earendil-works/pi-ai";
3
+ import { PiAiAdapter } from "@deepseek-ai/dsh-llm-pi-ai";
4
+ import { AsyncLocalStorage } from "node:async_hooks";
5
+ import { basename, dirname, join, resolve } from "node:path";
6
+ import { connect, isIP } from "node:net";
7
+ import { Agent, EnvHttpProxyAgent, ProxyAgent, getGlobalDispatcher, install, setGlobalDispatcher } from "undici";
8
+ import { closeOpenAICodexWebSocketSessions } from "@earendil-works/pi-ai/api/openai-codex-responses";
9
+ import { execFile } from "node:child_process";
10
+ import { mkdir, readFile, rm, stat } from "node:fs/promises";
11
+ import { withFileLock, writeFileAtomic } from "@deepseek-ai/dsh-atomic-write";
12
+ import { builtinProviders } from "@earendil-works/pi-ai/providers/all";
13
+ import { resolveDshHome } from "@deepseek-ai/dsh-home-paths";
14
+ import z from "@deepseek-ai/schemastery";
15
+ //#region src/catalog.ts
16
+ const PI_LOGIN_PROVIDERS = [
17
+ {
18
+ id: "openai-codex",
19
+ route: "pi-openai-codex",
20
+ authType: "oauth",
21
+ displayName: "ChatGPT Codex",
22
+ shortName: "Codex",
23
+ blurb: "ChatGPT Plus/Pro Codex. Independent of official `codex login`.",
24
+ blurbZh: "ChatGPT Plus/Pro 的 Codex。和官方 `codex login` 互不影响。",
25
+ allowedHosts: [
26
+ "auth.openai.com",
27
+ "chatgpt.com",
28
+ "www.chatgpt.com"
29
+ ],
30
+ allowedSuffixes: [".openai.com", ".chatgpt.com"],
31
+ preferredModels: [
32
+ "gpt-5.4",
33
+ "gpt-5.3-codex",
34
+ "gpt-5.3-codex-spark"
35
+ ]
36
+ },
37
+ {
38
+ id: "anthropic",
39
+ route: "pi-anthropic",
40
+ authType: "oauth",
41
+ displayName: "Claude Pro/Max",
42
+ shortName: "Claude",
43
+ blurb: "Claude subscription. Independent of official Claude Code login.",
44
+ blurbZh: "Claude 订阅。和官方 Claude Code 登录互不影响。",
45
+ allowedHosts: [
46
+ "claude.ai",
47
+ "www.claude.ai",
48
+ "platform.claude.com"
49
+ ],
50
+ allowedSuffixes: [".anthropic.com", ".claude.ai"],
51
+ preferredModels: [
52
+ "claude-opus-4-6",
53
+ "claude-sonnet-4-6",
54
+ "claude-opus-4-5"
55
+ ]
56
+ },
57
+ {
58
+ id: "xai",
59
+ route: "pi-xai",
60
+ authType: "oauth",
61
+ displayName: "xAI Grok",
62
+ shortName: "Grok",
63
+ blurb: "SuperGrok / X Premium. Independent of official `grok` CLI.",
64
+ blurbZh: "SuperGrok / X Premium。和官方 `grok` CLI 互不影响。",
65
+ allowedHosts: [
66
+ "auth.x.ai",
67
+ "accounts.x.ai",
68
+ "x.ai",
69
+ "www.x.ai"
70
+ ],
71
+ allowedSuffixes: [".x.ai"],
72
+ preferredModels: [
73
+ "grok-4.6",
74
+ "grok-4.5",
75
+ "grok-4.3"
76
+ ]
77
+ },
78
+ {
79
+ id: "github-copilot",
80
+ route: "pi-github-copilot",
81
+ authType: "oauth",
82
+ displayName: "GitHub Copilot",
83
+ shortName: "Copilot",
84
+ blurb: "GitHub Copilot subscription via device code.",
85
+ blurbZh: "GitHub Copilot 订阅,走 device code。",
86
+ allowedHosts: [
87
+ "github.com",
88
+ "www.github.com",
89
+ "api.github.com"
90
+ ],
91
+ allowedSuffixes: [".github.com", ".githubcopilot.com"],
92
+ preferredModels: [
93
+ "gpt-5.4",
94
+ "claude-sonnet-4.6",
95
+ "claude-opus-4.6"
96
+ ]
97
+ },
98
+ {
99
+ id: "openrouter",
100
+ route: "pi-openrouter",
101
+ authType: "oauth",
102
+ displayName: "OpenRouter",
103
+ shortName: "OpenRouter",
104
+ blurb: "OpenRouter OAuth mints a key billed from your OpenRouter credits.",
105
+ blurbZh: "OpenRouter OAuth 会签发一把钥匙,从你的 OpenRouter 余额扣费。",
106
+ allowedHosts: ["openrouter.ai", "www.openrouter.ai"],
107
+ allowedSuffixes: [".openrouter.ai"],
108
+ preferredModels: []
109
+ },
110
+ {
111
+ id: "kimi-coding",
112
+ route: "pi-kimi-coding",
113
+ authType: "oauth",
114
+ displayName: "Kimi For Coding",
115
+ shortName: "Kimi",
116
+ blurb: "Kimi Code subscription.",
117
+ blurbZh: "Kimi Code 订阅。",
118
+ allowedHosts: [
119
+ "auth.kimi.com",
120
+ "kimi.com",
121
+ "www.kimi.com",
122
+ "api.kimi.com"
123
+ ],
124
+ allowedSuffixes: [".kimi.com"],
125
+ preferredModels: ["kimi-for-coding", "k3"]
126
+ },
127
+ {
128
+ id: "zai-coding-cn",
129
+ route: "pi-zai-coding-cn",
130
+ authType: "api_key",
131
+ loginUrl: "https://bigmodel.cn/coding-plan/personal/overview",
132
+ displayName: "智谱 GLM Coding Plan",
133
+ shortName: "GLM",
134
+ blurb: "Zhipu GLM Coding Plan (China) via its official Plan API key.",
135
+ blurbZh: "智谱 GLM Coding Plan 中国区套餐,使用套餐页签发的专用 API Key。",
136
+ allowedHosts: [
137
+ "bigmodel.cn",
138
+ "www.bigmodel.cn",
139
+ "open.bigmodel.cn"
140
+ ],
141
+ allowedSuffixes: [".bigmodel.cn"],
142
+ preferredModels: [
143
+ "glm-5.3-flash",
144
+ "glm-5.3",
145
+ "glm-5.2",
146
+ "glm-5-turbo",
147
+ "glm-5.1",
148
+ "glm-4.7"
149
+ ]
150
+ }
151
+ ];
152
+ function piLoginProvider(id) {
153
+ return PI_LOGIN_PROVIDERS.find((provider) => provider.id === id);
154
+ }
155
+ function piLoginProviderByRoute(route) {
156
+ return PI_LOGIN_PROVIDERS.find((provider) => provider.route === route);
157
+ }
158
+ function requirePiLoginProvider(id) {
159
+ const provider = piLoginProvider(id);
160
+ if (provider === void 0) throw new Error(`dsh-oauth-login: unknown provider "${id}"`);
161
+ return provider;
162
+ }
163
+ function piLoginRoutes() {
164
+ return PI_LOGIN_PROVIDERS.map((provider) => provider.route);
165
+ }
166
+ //#endregion
167
+ //#region src/extra-models.ts
168
+ /**
169
+ * https://developers.openai.com/api/docs/models/gpt-6-astra
170
+ * Use the Codex catalog's default 272K context, not the API's larger ceiling.
171
+ * Ultra is an app orchestration mode, not a Pi reasoning effort.
172
+ */
173
+ const OPENAI_CODEX_EXTRA_MODELS = [{
174
+ id: "gpt-6-astra",
175
+ name: "GPT-6 Astra",
176
+ api: "openai-codex-responses",
177
+ provider: "openai-codex",
178
+ baseUrl: "https://chatgpt.com/backend-api",
179
+ reasoning: true,
180
+ input: ["text", "image"],
181
+ cost: {
182
+ input: 10,
183
+ output: 50,
184
+ cacheRead: 1,
185
+ cacheWrite: 12.5
186
+ },
187
+ contextWindow: 272e3,
188
+ maxTokens: 128e3,
189
+ thinkingLevelMap: {
190
+ off: null,
191
+ minimal: null,
192
+ low: "low",
193
+ medium: "medium",
194
+ high: "high",
195
+ xhigh: "xhigh",
196
+ max: "max"
197
+ }
198
+ }];
199
+ /** Newer xAI Grok models missing from the installed pi-ai catalog (0.82.x). */
200
+ const XAI_EXTRA_MODELS = [{
201
+ id: "grok-4.6",
202
+ name: "Grok 4.6",
203
+ api: "openai-responses",
204
+ provider: "xai",
205
+ baseUrl: "https://api.x.ai/v1",
206
+ reasoning: true,
207
+ input: ["text", "image"],
208
+ cost: {
209
+ input: 2,
210
+ output: 6,
211
+ cacheRead: .3,
212
+ cacheWrite: 0
213
+ },
214
+ contextWindow: 5e5,
215
+ maxTokens: 5e5,
216
+ compat: { supportsLongCacheRetention: false },
217
+ thinkingLevelMap: {
218
+ off: null,
219
+ minimal: null,
220
+ low: "low",
221
+ medium: "medium",
222
+ high: "high",
223
+ xhigh: "xhigh",
224
+ max: null
225
+ }
226
+ }];
227
+ /**
228
+ * Official GLM-5.3 family metadata. Standard is text-only; Flash also accepts
229
+ * images. Keep separate IDs so choosing standard never routes to Flash.
230
+ * https://docs.bigmodel.cn/cn/guide/models/text/glm-5.3
231
+ * https://docs.bigmodel.cn/cn/coding-plan/latest-model
232
+ */
233
+ const ZAI_CODING_CN_EXTRA_MODELS = [{
234
+ id: "glm-5.3-flash",
235
+ name: "GLM-5.3-Flash",
236
+ api: "openai-completions",
237
+ provider: "zai-coding-cn",
238
+ baseUrl: "https://open.bigmodel.cn/api/coding/paas/v4",
239
+ reasoning: true,
240
+ input: ["text", "image"],
241
+ cost: {
242
+ input: 0,
243
+ output: 0,
244
+ cacheRead: 0,
245
+ cacheWrite: 0
246
+ },
247
+ contextWindow: 1e6,
248
+ maxTokens: 131072,
249
+ compat: {
250
+ supportsStore: false,
251
+ supportsDeveloperRole: false,
252
+ supportsReasoningEffort: true,
253
+ thinkingFormat: "zai",
254
+ zaiToolStream: true
255
+ },
256
+ thinkingLevelMap: {
257
+ off: null,
258
+ minimal: null,
259
+ low: "low",
260
+ medium: null,
261
+ high: "high",
262
+ xhigh: null,
263
+ max: "max"
264
+ }
265
+ }, {
266
+ id: "glm-5.3",
267
+ name: "GLM-5.3",
268
+ api: "openai-completions",
269
+ provider: "zai-coding-cn",
270
+ baseUrl: "https://open.bigmodel.cn/api/coding/paas/v4",
271
+ reasoning: true,
272
+ input: ["text"],
273
+ cost: {
274
+ input: 0,
275
+ output: 0,
276
+ cacheRead: 0,
277
+ cacheWrite: 0
278
+ },
279
+ contextWindow: 1e6,
280
+ maxTokens: 131072,
281
+ compat: {
282
+ supportsStore: false,
283
+ supportsDeveloperRole: false,
284
+ supportsReasoningEffort: true,
285
+ thinkingFormat: "zai",
286
+ zaiToolStream: true
287
+ },
288
+ thinkingLevelMap: {
289
+ off: null,
290
+ minimal: null,
291
+ low: "low",
292
+ medium: null,
293
+ high: "high",
294
+ xhigh: null,
295
+ max: "max"
296
+ }
297
+ }];
298
+ /** OpenRouter stealth model missing from the installed pi-ai catalog (0.82.x–0.84.x). */
299
+ const OX_ALPHA_MODEL_ID = "stealth/ox-alpha";
300
+ /**
301
+ * OpenRouter's live default for ox-alpha (`reasoning.default_effort`).
302
+ * Must be sent explicitly: pi-ai's OpenRouter dialect writes
303
+ * `reasoning: { effort: thinkingLevelMap.off ?? "none" }` when the caller
304
+ * omits an effort, and ox-alpha rejects `none` (`reasoning.mandatory: true`).
305
+ */
306
+ const OX_ALPHA_DEFAULT_EFFORT = "max";
307
+ /**
308
+ * OpenRouter stealth models missing from the installed pi-ai catalog (0.82.x).
309
+ * Capacities follow the public OpenRouter listing: 1M context, text+image, free.
310
+ * Effort levels follow live `/api/v1/models` (`supported_efforts`, `default_effort`).
311
+ */
312
+ const OPENROUTER_EXTRA_MODELS = [{
313
+ id: OX_ALPHA_MODEL_ID,
314
+ name: "OX Alpha",
315
+ api: "openai-completions",
316
+ provider: "openrouter",
317
+ baseUrl: "https://openrouter.ai/api/v1",
318
+ reasoning: true,
319
+ input: ["text", "image"],
320
+ cost: {
321
+ input: 0,
322
+ output: 0,
323
+ cacheRead: 0,
324
+ cacheWrite: 0
325
+ },
326
+ contextWindow: 1e6,
327
+ maxTokens: 131072,
328
+ compat: {
329
+ supportsDeveloperRole: false,
330
+ thinkingFormat: "openrouter"
331
+ },
332
+ thinkingLevelMap: {
333
+ off: null,
334
+ minimal: null,
335
+ low: "low",
336
+ medium: null,
337
+ high: "high",
338
+ xhigh: null,
339
+ max: "max"
340
+ }
341
+ }];
342
+ /** Per-model default the selector and request path should apply when omitted. */
343
+ function defaultReasoningEffortFor(modelId) {
344
+ if (modelId === "gpt-6-astra") return "medium";
345
+ if (modelId === "stealth/ox-alpha") return "max";
346
+ if (modelId === "glm-5.3") return "max";
347
+ if (modelId === "glm-5.3-flash") return "max";
348
+ }
349
+ const EXTRA_MODELS_BY_PROVIDER = {
350
+ "openai-codex": OPENAI_CODEX_EXTRA_MODELS,
351
+ xai: XAI_EXTRA_MODELS,
352
+ openrouter: OPENROUTER_EXTRA_MODELS,
353
+ "zai-coding-cn": ZAI_CODING_CN_EXTRA_MODELS
354
+ };
355
+ /**
356
+ * Extra models this plugin publishes for one pi-ai provider id.
357
+ * @param providerId - catalog provider id (e.g. `xai`), not the harness route.
358
+ */
359
+ function extraModelsFor(providerId) {
360
+ return EXTRA_MODELS_BY_PROVIDER[providerId] ?? [];
361
+ }
362
+ //#endregion
363
+ //#region src/hosted-capture.ts
364
+ /** Per-request bag for hosted Responses output that pi-ai drops. */
365
+ const hostedCapture = new AsyncLocalStorage();
366
+ function currentHostedCapture() {
367
+ return hostedCapture.getStore();
368
+ }
369
+ /** Keep ALS alive across each iterator step so the OpenAI SDK fetch inherits it. */
370
+ function iterateInCapture(capture, source) {
371
+ return { [Symbol.asyncIterator]() {
372
+ const iterator = source[Symbol.asyncIterator]();
373
+ return {
374
+ next: () => hostedCapture.run(capture, () => iterator.next()),
375
+ return: (value) => hostedCapture.run(capture, () => iterator.return?.(value) ?? Promise.resolve({
376
+ done: true,
377
+ value: void 0
378
+ })),
379
+ throw: (error) => hostedCapture.run(capture, () => iterator.throw?.(error) ?? Promise.reject(error))
380
+ };
381
+ } };
382
+ }
383
+ //#endregion
384
+ //#region src/hosted-images.ts
385
+ function isRecord$1(value) {
386
+ return typeof value === "object" && value !== null && !Array.isArray(value);
387
+ }
388
+ function sniffImageMediaType(bytes) {
389
+ if (bytes.length >= 3 && bytes[0] === 255 && bytes[1] === 216 && bytes[2] === 255) return "image/jpeg";
390
+ if (bytes.length >= 8 && bytes[0] === 137 && bytes[1] === 80 && bytes[2] === 78 && bytes[3] === 71) return "image/png";
391
+ if (bytes.length >= 6 && bytes[0] === 71 && bytes[1] === 73 && bytes[2] === 70) return "image/gif";
392
+ if (bytes.length >= 12 && bytes[8] === 87 && bytes[9] === 69 && bytes[10] === 66 && bytes[11] === 80) return "image/webp";
393
+ }
394
+ function stripDataUrl(value) {
395
+ return /^data:image\/[a-zA-Z0-9.+-]+;base64,(.+)$/s.exec(value.trim())?.[1] ?? value.trim();
396
+ }
397
+ function base64FromUnknown(value) {
398
+ if (typeof value === "string" && value.length > 0) return stripDataUrl(value);
399
+ if (!isRecord$1(value)) return void 0;
400
+ if (typeof value.b64_json === "string" && value.b64_json.length > 0) return stripDataUrl(value.b64_json);
401
+ if (typeof value.base64 === "string" && value.base64.length > 0) return stripDataUrl(value.base64);
402
+ if (typeof value.result === "string" && value.result.length > 0) return stripDataUrl(value.result);
403
+ }
404
+ function pushImage(into, id, base64) {
405
+ if (id !== void 0 && into.some((existing) => existing.id === id)) return;
406
+ if (id === void 0 && into.some((existing) => existing.base64 === base64)) return;
407
+ into.push(id === void 0 ? { base64 } : {
408
+ id,
409
+ base64
410
+ });
411
+ }
412
+ function collectHostedImageFromItem(item, into) {
413
+ if (!isRecord$1(item) || item.type !== "image_generation_call") return;
414
+ if (item.status !== void 0 && item.status !== "completed") return;
415
+ const base64 = base64FromUnknown(item.result);
416
+ if (base64 === void 0) return;
417
+ pushImage(into, typeof item.id === "string" ? item.id : void 0, base64);
418
+ }
419
+ function collectHostedImagesFromEvent(event, into) {
420
+ if (!isRecord$1(event) || typeof event.type !== "string") return;
421
+ if (event.type === "response.output_item.done") {
422
+ collectHostedImageFromItem(event.item, into);
423
+ return;
424
+ }
425
+ if (event.type.startsWith("response.image_generation_call.")) {
426
+ collectHostedImageFromItem(event.item, into);
427
+ if (event.type.endsWith(".completed")) {
428
+ const base64 = base64FromUnknown(event.result);
429
+ if (base64 !== void 0) pushImage(into, typeof event.item_id === "string" ? event.item_id : isRecord$1(event.item) && typeof event.item.id === "string" ? event.item.id : void 0, base64);
430
+ }
431
+ return;
432
+ }
433
+ if (event.type !== "response.completed" && event.type !== "response.incomplete") return;
434
+ const output = isRecord$1(event.response) ? event.response.output : void 0;
435
+ if (!Array.isArray(output)) return;
436
+ for (const item of output) collectHostedImageFromItem(item, into);
437
+ }
438
+ function decodeHostedImage(base64) {
439
+ let data;
440
+ try {
441
+ data = Uint8Array.from(Buffer.from(base64, "base64"));
442
+ } catch {
443
+ return;
444
+ }
445
+ if (data.length === 0) return void 0;
446
+ const mediaType = sniffImageMediaType(data);
447
+ return mediaType === void 0 ? void 0 : {
448
+ data,
449
+ mediaType
450
+ };
451
+ }
452
+ function extensionFor(mediaType) {
453
+ switch (mediaType) {
454
+ case "image/jpeg": return "jpg";
455
+ case "image/png": return "png";
456
+ case "image/webp": return "webp";
457
+ case "image/gif": return "gif";
458
+ }
459
+ }
460
+ /**
461
+ * pi-ai replay cannot represent assistant ImageBlocks. Drop them before the
462
+ * next request so replay metadata still lines up with remaining content.
463
+ */
464
+ function stripAssistantImages(messages) {
465
+ return messages.map((message) => {
466
+ if (message.role !== "assistant") return message;
467
+ const content = message.content.filter((block) => block.type !== "image");
468
+ return content.length === message.content.length ? message : {
469
+ ...message,
470
+ content
471
+ };
472
+ });
473
+ }
474
+ async function* injectHostedImages(source, capture, save) {
475
+ let maxIndex = -1;
476
+ const tail = [];
477
+ for await (const chunk of source) {
478
+ if ("index" in chunk && typeof chunk.index === "number") maxIndex = Math.max(maxIndex, chunk.index);
479
+ if (chunk.type === "usage" || chunk.type === "finish") {
480
+ tail.push(chunk);
481
+ continue;
482
+ }
483
+ yield chunk;
484
+ }
485
+ for (const image of capture.images) {
486
+ const decoded = decodeHostedImage(image.base64);
487
+ if (decoded === void 0) continue;
488
+ try {
489
+ const attachment = await save({
490
+ data: decoded.data,
491
+ mediaType: decoded.mediaType,
492
+ name: `generated.${extensionFor(decoded.mediaType)}`
493
+ });
494
+ const index = maxIndex + 1;
495
+ maxIndex = index;
496
+ yield {
497
+ type: "block-start",
498
+ index,
499
+ blockType: "image"
500
+ };
501
+ yield {
502
+ type: "block-end",
503
+ index,
504
+ block: {
505
+ type: "image",
506
+ attachment
507
+ }
508
+ };
509
+ } catch {}
510
+ }
511
+ yield* tail;
512
+ }
513
+ //#endregion
514
+ //#region src/ids.ts
515
+ /** Basename of the DSH-owned multi-provider OAuth document. */
516
+ const PI_LOGIN_AUTH_FILENAME = ".dsh-oauth-auth.json";
517
+ /** Legacy DSH filename accepted during the one-time storage migration. */
518
+ const LEGACY_PI_LOGIN_AUTH_FILENAME = ".pi-login-auth.json";
519
+ /** Prefix for harness routes so they never collide with catalog / other plugins. */
520
+ const PI_LOGIN_ROUTE_PREFIX = "pi-";
521
+ /** Provider idle ceiling used by every composite route. */
522
+ const PI_LOGIN_STREAM_IDLE_TIMEOUT_MS = 3e5;
523
+ /** RC8 request-level image payload ceiling; matches the official pi-ai default. */
524
+ const PI_LOGIN_MAX_REQUEST_IMAGE_BYTES = 20971520;
525
+ /** Startup line `dshx verify` looks for. */
526
+ const PI_LOGIN_BOOT_MARKER = "[my-plugins/dsh-oauth-login] loaded";
527
+ //#endregion
528
+ //#region src/model-error-hint.ts
529
+ /** Provider-specific failure compatibility and user-facing model error copy. */
530
+ /**
531
+ * Default Harness `retryPolicy` codes. Keep aligned with
532
+ * `resolveRetryPolicy(undefined)` in `@deepseek-ai/dsh-llm`.
533
+ */
534
+ const TRANSIENT_MODEL_CODES = Object.freeze([
535
+ "EMPTY_RESPONSE",
536
+ "RATE_LIMIT",
537
+ "SERVER",
538
+ "TIMEOUT",
539
+ "TRANSPORT"
540
+ ]);
541
+ const QUOTA_HINT = "This is account quota or credits, not a temporary busy signal. Automatic retry will not refill it. Check the plan, usage window, or balance.";
542
+ const RATE_LIMIT_HINT = "This is a request-rate or peak-busy limit. A 429 is often this, not an empty balance. After automatic retries end, wait and send another message. If Continue fails or the composer stays stuck, start a new chat.";
543
+ const TRANSIENT_HINT = "After this turn ends, send another message to try again. If Continue fails or the composer stays stuck, start a new chat.";
544
+ const SENTINELS = [
545
+ "Automatic retry will not refill it",
546
+ "request-rate or peak-busy limit",
547
+ "After this turn ends, send another message"
548
+ ];
549
+ /** Stable Chat copy for one official `LlmError` code. Does not invent 5h vs weekly vs billing. */
550
+ function hintForCode(code) {
551
+ if (code === "QUOTA") return QUOTA_HINT;
552
+ if (code === "RATE_LIMIT") return RATE_LIMIT_HINT;
553
+ if (TRANSIENT_MODEL_CODES.includes(code)) return TRANSIENT_HINT;
554
+ }
555
+ function alreadyHinted(message) {
556
+ return SENTINELS.some((marker) => message.includes(marker));
557
+ }
558
+ /** Read only the top-level Zhipu error code, never incidental request or message text. */
559
+ function isZhipuPlanQuota(message) {
560
+ const match = /^\s*429\s*:\s*(\{[\s\S]*\})\s*$/.exec(message);
561
+ if (match === null) return false;
562
+ let payload;
563
+ try {
564
+ payload = JSON.parse(match[1]);
565
+ } catch (_malformedProviderJson) {
566
+ return false;
567
+ }
568
+ return typeof payload === "object" && payload !== null && !Array.isArray(payload) && "code" in payload && (payload.code === "1310" || payload.code === 1310);
569
+ }
570
+ /** Repair only observed route-specific classification gaps; keep all other official codes. */
571
+ function providerFailure(failure, provider) {
572
+ if (provider === "pi-openai-codex" && failure.code === "PI_AI_ERROR" && (/^WebSocket error$/i.test(failure.message.trim()) || /^WebSocket closed (?:1000|1001|1005|1006|1011|1012|1013)(?:\s|$)/i.test(failure.message.trim()))) return {
573
+ ...failure,
574
+ code: "TRANSPORT"
575
+ };
576
+ if (provider === "pi-openai-codex" && failure.code === "PI_AI_ERROR" && /\b(?:servers?|engine)\s+(?:are|is)\s+(?:currently\s+)?overloaded\b/i.test(failure.message)) return {
577
+ ...failure,
578
+ code: "SERVER"
579
+ };
580
+ if (provider === "pi-zai-coding-cn" && failure.code === "RATE_LIMIT" && isZhipuPlanQuota(failure.message)) return {
581
+ ...failure,
582
+ code: "QUOTA"
583
+ };
584
+ return failure;
585
+ }
586
+ /** Normalize known provider gaps before the official retry executor receives this failure. */
587
+ function hintFailure(failure, provider) {
588
+ const normalized = providerFailure(failure, provider);
589
+ const hint = hintForCode(normalized.code);
590
+ if (hint === void 0 || alreadyHinted(normalized.message)) return normalized;
591
+ return {
592
+ ...normalized,
593
+ message: `${normalized.message} ${hint}`
594
+ };
595
+ }
596
+ /** Apply the same provider correction and hint to thrown failures, preserving diagnostics. */
597
+ function withModelErrorHint(error, provider) {
598
+ if (!(error instanceof LlmError)) return error;
599
+ const hinted = hintFailure(error.failure, provider);
600
+ if (hinted === error.failure) return error;
601
+ return new LlmError(hinted.message, hinted.code, {
602
+ cause: error,
603
+ ...error.failure.status === void 0 ? {} : { status: error.failure.status },
604
+ ...error.failure.providerRetryAfterMs === void 0 ? {} : { providerRetryAfterMs: error.failure.providerRetryAfterMs },
605
+ ...error.failure.requestId === void 0 ? {} : { requestId: error.failure.requestId }
606
+ });
607
+ }
608
+ //#endregion
609
+ //#region src/openrouter-models.ts
610
+ function objectValue(value) {
611
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
612
+ }
613
+ function validModelId(value) {
614
+ return typeof value === "string" && /^~?[a-zA-Z0-9][a-zA-Z0-9._:/-]{0,255}$/.test(value);
615
+ }
616
+ function strings(value) {
617
+ return Array.isArray(value) ? [...new Set(value.filter((item) => typeof item === "string" && item.length < 256))] : [];
618
+ }
619
+ function positiveInteger(value) {
620
+ return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : void 0;
621
+ }
622
+ /**
623
+ * Conservative, all-cost classification. Missing/invalid prices, unknown
624
+ * structures and conditional overrides never become a free badge. In
625
+ * particular, a tiny positive decimal must not underflow into a free price.
626
+ */
627
+ function classifyOpenRouterPricing(value) {
628
+ const source = objectValue(value);
629
+ const pricing = {};
630
+ let unknown = source === void 0 || source.prompt === void 0 || source.completion === void 0;
631
+ let paid = false;
632
+ for (const [key, raw] of Object.entries(source ?? {})) {
633
+ if (key === "overrides") {
634
+ if (!Array.isArray(raw) || raw.length > 0) unknown = true;
635
+ continue;
636
+ }
637
+ if (typeof raw !== "string" || raw.length > 128 || !/^\d+(?:\.\d+)?(?:e[+-]?\d+)?$/i.test(raw)) {
638
+ unknown = true;
639
+ continue;
640
+ }
641
+ const numeric = Number(raw);
642
+ if (!Number.isFinite(numeric) || numeric < 0) {
643
+ unknown = true;
644
+ continue;
645
+ }
646
+ pricing[key] = raw;
647
+ if (!/^0+(?:\.0+)?(?:e[+-]?\d+)?$/i.test(raw)) paid = true;
648
+ }
649
+ return {
650
+ pricing,
651
+ priceStatus: paid ? "paid" : unknown ? "unknown" : "free"
652
+ };
653
+ }
654
+ function normalizeOpenRouterModels(raw) {
655
+ if (!Array.isArray(raw) || raw.length === 0 || raw.length > 1e4) throw new Error("OpenRouter returned an empty or invalid model catalog");
656
+ const seen = /* @__PURE__ */ new Set();
657
+ const out = [];
658
+ for (const entry of raw) {
659
+ const row = objectValue(entry);
660
+ if (row === void 0 || !validModelId(row.id)) throw new Error("Invalid OpenRouter model ID");
661
+ if (row.id.endsWith(":batch")) continue;
662
+ const architecture = objectValue(row.architecture);
663
+ const inputModalities = strings(architecture?.input_modalities);
664
+ const outputModalities = strings(architecture?.output_modalities);
665
+ if (!inputModalities.includes("text") || !outputModalities.includes("text")) continue;
666
+ if (seen.has(row.id)) throw new Error("Duplicate OpenRouter model ID");
667
+ const top = objectValue(row.top_provider);
668
+ const contextWindow = positiveInteger(row.context_length) ?? positiveInteger(top?.context_length);
669
+ if (contextWindow === void 0) throw new Error("Invalid OpenRouter context limit");
670
+ const supportedParameters = strings(row.supported_parameters);
671
+ const reason = objectValue(row.reasoning);
672
+ const reasoning = supportedParameters.some((key) => key === "reasoning" || key === "include_reasoning");
673
+ const supportedEfforts = strings(reason?.supported_efforts);
674
+ const defaultEffort = typeof reason?.default_effort === "string" ? reason.default_effort : void 0;
675
+ const maxTokens = Math.min(contextWindow, positiveInteger(top?.max_completion_tokens) ?? Math.min(contextWindow, 8192));
676
+ out.push({
677
+ id: row.id,
678
+ name: typeof row.name === "string" && row.name.trim() !== "" ? row.name.slice(0, 256) : row.id,
679
+ contextWindow,
680
+ maxTokens,
681
+ inputModalities,
682
+ outputModalities,
683
+ supportedParameters,
684
+ tools: supportedParameters.includes("tools"),
685
+ reasoning,
686
+ reasoningMandatory: reasoning && reason?.mandatory === true,
687
+ ...supportedEfforts.length === 0 ? {} : { supportedEfforts },
688
+ ...defaultEffort === void 0 ? {} : { defaultEffort },
689
+ ...classifyOpenRouterPricing(row.pricing),
690
+ ...typeof row.expiration_date === "string" && /^\d{4}-\d{2}-\d{2}$/.test(row.expiration_date) ? { deprecatesAt: row.expiration_date } : {}
691
+ });
692
+ seen.add(row.id);
693
+ }
694
+ if (out.length === 0) throw new Error("OpenRouter returned no usable text models");
695
+ return out;
696
+ }
697
+ const LEVELS = [
698
+ "off",
699
+ "minimal",
700
+ "low",
701
+ "medium",
702
+ "high",
703
+ "xhigh",
704
+ "max"
705
+ ];
706
+ function openRouterThinkingMap(info, base) {
707
+ if (!info.reasoning) return void 0;
708
+ if (info.supportedEfforts === void 0) return {
709
+ ...base?.thinkingLevelMap,
710
+ ...info.reasoningMandatory ? { off: null } : {}
711
+ };
712
+ const map = {};
713
+ for (const level of LEVELS) {
714
+ const wire = level === "off" ? "none" : level;
715
+ map[level] = info.supportedEfforts.includes(wire) && !(level === "off" && info.reasoningMandatory) ? wire : null;
716
+ }
717
+ return map;
718
+ }
719
+ function openRouterDefaultEffort(info) {
720
+ if (info?.reasoning !== true) return void 0;
721
+ if (info.defaultEffort !== void 0 && info.defaultEffort !== "none") return info.defaultEffort;
722
+ if (!info.reasoningMandatory) return void 0;
723
+ return info.supportedEfforts?.find((level) => level === "high") ?? info.supportedEfforts?.find((level) => level !== "none") ?? "high";
724
+ }
725
+ function openRouterPiModel(info, base) {
726
+ const rate = (key) => {
727
+ const value = Number(info.pricing[key] ?? 0) * 1e6;
728
+ return Number.isFinite(value) ? value : 0;
729
+ };
730
+ const thinkingLevelMap = openRouterThinkingMap(info, base);
731
+ return {
732
+ ...base,
733
+ id: info.id,
734
+ name: info.name,
735
+ api: "openai-completions",
736
+ provider: "openrouter",
737
+ baseUrl: "https://openrouter.ai/api/v1",
738
+ input: info.inputModalities.includes("image") ? ["text", "image"] : ["text"],
739
+ contextWindow: info.contextWindow,
740
+ maxTokens: info.maxTokens,
741
+ reasoning: info.reasoning,
742
+ ...thinkingLevelMap === void 0 ? {} : { thinkingLevelMap },
743
+ cost: {
744
+ input: rate("prompt"),
745
+ output: rate("completion"),
746
+ cacheRead: rate("input_cache_read"),
747
+ cacheWrite: rate("input_cache_write")
748
+ },
749
+ compat: {
750
+ ...base?.compat,
751
+ supportsDeveloperRole: false,
752
+ thinkingFormat: "openrouter"
753
+ }
754
+ };
755
+ }
756
+ //#endregion
757
+ //#region src/openrouter-types.ts
758
+ /** Browser-safe contract. No credentials, provider URLs or server imports. */
759
+ const OPENROUTER_CATALOG_PATH = "/plugins/dsh-oauth-login/openrouter/models";
760
+ const OPENROUTER_REFRESH_PATH = "/plugins/dsh-oauth-login/openrouter/models/refresh";
761
+ const OPENROUTER_REFRESH_MS = 9e5;
762
+ const OPENROUTER_COOLDOWN_MS = 6e4;
763
+ //#endregion
764
+ //#region src/native-tools.ts
765
+ /** DSH model-facing tools that steal traffic from a subscribed provider. */
766
+ const DSH_WEB_TOOL_NAMES = ["web_search", "web_fetch"];
767
+ /** Matching tool-web prompt sections. */
768
+ const DSH_WEB_SECTION_NAMES = ["tool:web_search", "tool:web_fetch"];
769
+ const DSH_WEB_TOOL_NAME_SET = new Set(DSH_WEB_TOOL_NAMES);
770
+ const DSH_WEB_SECTION_NAME_SET = new Set(DSH_WEB_SECTION_NAMES);
771
+ /** Precise xAI server-side X Search operations published by the provider. */
772
+ const XAI_SERVER_X_SEARCH_NAMES = [
773
+ "x_user_search",
774
+ "x_keyword_search",
775
+ "x_semantic_search",
776
+ "x_thread_fetch"
777
+ ];
778
+ const XAI_SERVER_X_SEARCH_NAME_SET = new Set(XAI_SERVER_X_SEARCH_NAMES);
779
+ /**
780
+ * Hosted / server-executed names that can leak as client function calls
781
+ * after this plugin removes DSH's implementations from the route.
782
+ */
783
+ const HOSTED_CLIENT_LEAK_NAMES = [
784
+ ...XAI_SERVER_X_SEARCH_NAMES,
785
+ "web_search",
786
+ "web_fetch",
787
+ "image_generation",
788
+ "imagine_text_to_image",
789
+ "imagine_image_to_image",
790
+ "imagine_image_edit"
791
+ ];
792
+ const HOSTED_CLIENT_LEAK_NAME_SET = new Set(HOSTED_CLIENT_LEAK_NAMES);
793
+ const DEFAULT_NATIVE_TOOL_POLICY = {
794
+ enabled: true,
795
+ image: true
796
+ };
797
+ const SEARCH_GUIDANCE = "This turn already includes this account's native hosted search. Do not call web_search or web_fetch — those DSH tools are not available on this route.";
798
+ const SEARCH_AND_IMAGE_GUIDANCE = "This turn already includes this account's native hosted search and image generation. Do not call web_search or web_fetch — those DSH tools are not available on this route.";
799
+ function responsesSearch() {
800
+ return { type: "web_search" };
801
+ }
802
+ function responsesXSearch() {
803
+ return { type: "x_search" };
804
+ }
805
+ function responsesImage() {
806
+ return { type: "image_generation" };
807
+ }
808
+ function anthropicSearch() {
809
+ return {
810
+ type: "web_search_20250305",
811
+ name: "web_search"
812
+ };
813
+ }
814
+ function nativePlan(providerId, api, policy = DEFAULT_NATIVE_TOOL_POLICY) {
815
+ if (!policy.enabled) return void 0;
816
+ switch (`${providerId}:${api ?? ""}`) {
817
+ case "xai:openai-responses": return {
818
+ providerId,
819
+ api: "openai-responses",
820
+ hosted: [
821
+ responsesSearch(),
822
+ responsesXSearch(),
823
+ ...policy.image ? [responsesImage()] : []
824
+ ],
825
+ guidance: policy.image ? SEARCH_AND_IMAGE_GUIDANCE : SEARCH_GUIDANCE
826
+ };
827
+ case "openai-codex:openai-codex-responses": return {
828
+ providerId,
829
+ api: "openai-codex-responses",
830
+ hosted: [responsesSearch(), ...policy.image ? [responsesImage()] : []],
831
+ guidance: policy.image ? SEARCH_AND_IMAGE_GUIDANCE : SEARCH_GUIDANCE
832
+ };
833
+ case "anthropic:anthropic-messages": return {
834
+ providerId,
835
+ api: "anthropic-messages",
836
+ hosted: [anthropicSearch()],
837
+ guidance: SEARCH_GUIDANCE
838
+ };
839
+ default: return;
840
+ }
841
+ }
842
+ function nativePlanForRoute(route, api, policy = DEFAULT_NATIVE_TOOL_POLICY) {
843
+ if (route === void 0) return void 0;
844
+ const spec = piLoginProviderByRoute(route);
845
+ return spec === void 0 ? void 0 : nativePlan(spec.id, api, policy);
846
+ }
847
+ function isDshWebToolName(name) {
848
+ return DSH_WEB_TOOL_NAME_SET.has(name);
849
+ }
850
+ function isDshWebSectionName(name) {
851
+ return DSH_WEB_SECTION_NAME_SET.has(name);
852
+ }
853
+ function isRecord(value) {
854
+ return typeof value === "object" && value !== null && !Array.isArray(value);
855
+ }
856
+ /**
857
+ * xAI's Responses stream currently exposes server-executed X Search details
858
+ * as custom tool calls. The outer `xs_call-*` id distinguishes those traces
859
+ * from a same-named client function, which Harness must still execute.
860
+ */
861
+ function isXaiServerXSearchCall(block) {
862
+ if (!isRecord(block) || block.type !== "tool-call") return false;
863
+ if (typeof block.name !== "string" || !XAI_SERVER_X_SEARCH_NAME_SET.has(block.name)) return false;
864
+ if (typeof block.id !== "string") return false;
865
+ const outerId = block.id.split("|", 1)[0] ?? "";
866
+ return /^xs_call[-_]/.test(outerId);
867
+ }
868
+ /**
869
+ * Server-executed hosted search / image traces that Harness must not try to run.
870
+ * X Search leaks use `xs_call-*`. DSH `web_search` / `web_fetch` were stripped
871
+ * from this route, so a leftover same-named call is also a leak.
872
+ */
873
+ function isHostedServerToolCall(block) {
874
+ if (!isRecord(block) || block.type !== "tool-call") return false;
875
+ if (typeof block.name !== "string" || !HOSTED_CLIENT_LEAK_NAME_SET.has(block.name)) return false;
876
+ if (isXaiServerXSearchCall(block)) return true;
877
+ if (block.name === "web_search" || block.name === "web_fetch") return true;
878
+ return block.name === "image_generation" || block.name.startsWith("imagine_");
879
+ }
880
+ /**
881
+ * xAI hosted search hops arrive as empty `reasoning` blocks whose signature
882
+ * id is `tco_<response>_call-…`. The UI renders each as a blank Think card.
883
+ */
884
+ function isHostedSearchReasoningReplay(block) {
885
+ if (!isRecord(block) || block.type !== "reasoning") return false;
886
+ const signature = block.thinkingSignature;
887
+ if (typeof signature !== "string" || signature.length === 0) return false;
888
+ try {
889
+ const parsed = JSON.parse(signature);
890
+ return typeof parsed.id === "string" && parsed.id.startsWith("tco_");
891
+ } catch {
892
+ return signature.includes("\"id\":\"tco_") || signature.startsWith("tco_");
893
+ }
894
+ }
895
+ function filterPiReplayState(replayState, dropped, forceStop) {
896
+ if (replayState === void 0 || !isRecord(replayState.response) || replayState.response.kind !== "pi-ai" || !Array.isArray(replayState.blocks)) return replayState;
897
+ let seenText = false;
898
+ return {
899
+ ...replayState,
900
+ blocks: replayState.blocks.filter((block, index) => {
901
+ if (dropped.has(index) || isHostedSearchReasoningReplay(block)) return false;
902
+ if (isRecord(block) && block.type === "text") {
903
+ seenText = true;
904
+ return true;
905
+ }
906
+ if (seenText && isRecord(block) && block.type === "reasoning") return false;
907
+ return true;
908
+ }),
909
+ response: {
910
+ ...replayState.response,
911
+ ...forceStop ? { stopReason: "stop" } : {}
912
+ }
913
+ };
914
+ }
915
+ function isEmptyReasoningText(text) {
916
+ return text.trim().length === 0;
917
+ }
918
+ /**
919
+ * Remove hosted server-side search / image traces after pi-ai 0.82.1 has
920
+ * mistaken them for client function calls. Other blocks retain stream order,
921
+ * actual client tools survive, and pi-ai replay metadata stays index-aligned.
922
+ */
923
+ async function* filterHostedServerToolTraces(source) {
924
+ const dropped = /* @__PURE__ */ new Set();
925
+ const pending = /* @__PURE__ */ new Set();
926
+ const reasoning = /* @__PURE__ */ new Map();
927
+ let buffered = [];
928
+ let keptToolCalls = 0;
929
+ let textStarted = false;
930
+ const flush = function* () {
931
+ for (const item of buffered) {
932
+ if ("index" in item && dropped.has(item.index)) continue;
933
+ yield item;
934
+ }
935
+ buffered = [];
936
+ };
937
+ const closeReasoning = function* (index, end) {
938
+ const held = reasoning.get(index);
939
+ if (held === void 0) {
940
+ if (end !== void 0) yield end;
941
+ return;
942
+ }
943
+ reasoning.delete(index);
944
+ const ended = end?.type === "block-end" && end.block.type === "reasoning" ? end.block.text : "";
945
+ const text = ended.length > 0 ? ended : held.text;
946
+ if (held.afterText || isEmptyReasoningText(text)) return;
947
+ yield* held.chunks;
948
+ if (end !== void 0) yield end;
949
+ };
950
+ for await (const chunk of source) {
951
+ if (chunk.type === "block-start" && chunk.blockType === "text" || chunk.type === "text-delta") textStarted = true;
952
+ if (chunk.type === "block-start" && chunk.blockType === "reasoning") {
953
+ reasoning.set(chunk.index, {
954
+ chunks: [chunk],
955
+ text: "",
956
+ afterText: textStarted
957
+ });
958
+ continue;
959
+ }
960
+ if (chunk.type === "reasoning-delta" && reasoning.has(chunk.index)) {
961
+ const held = reasoning.get(chunk.index);
962
+ if (held !== void 0) {
963
+ held.chunks.push(chunk);
964
+ held.text += chunk.text;
965
+ }
966
+ continue;
967
+ }
968
+ if (chunk.type === "block-end" && chunk.block.type === "reasoning") {
969
+ yield* closeReasoning(chunk.index, chunk);
970
+ continue;
971
+ }
972
+ if (chunk.type === "block-start" && chunk.blockType === "tool-call") {
973
+ pending.add(chunk.index);
974
+ buffered.push(chunk);
975
+ continue;
976
+ }
977
+ if (pending.size > 0) {
978
+ buffered.push(chunk);
979
+ if (chunk.type === "block-end" && chunk.block.type === "tool-call" && pending.has(chunk.index)) {
980
+ if (isHostedServerToolCall(chunk.block)) dropped.add(chunk.index);
981
+ else keptToolCalls += 1;
982
+ pending.delete(chunk.index);
983
+ if (pending.size === 0) yield* flush();
984
+ }
985
+ continue;
986
+ }
987
+ if (chunk.type === "finish") {
988
+ for (const index of [...reasoning.keys()]) yield* closeReasoning(index);
989
+ const forceStop = chunk.reason.kind === "tool-calls" && dropped.size > 0 && keptToolCalls === 0;
990
+ const replayState = filterPiReplayState(chunk.replayState, dropped, forceStop);
991
+ yield {
992
+ type: "finish",
993
+ reason: forceStop ? { kind: "stop" } : chunk.reason,
994
+ ...replayState === void 0 ? {} : { replayState }
995
+ };
996
+ continue;
997
+ }
998
+ yield chunk;
999
+ }
1000
+ for (const index of [...reasoning.keys()]) yield* closeReasoning(index);
1001
+ if (buffered.length > 0) yield* flush();
1002
+ }
1003
+ /** @deprecated Use {@link filterHostedServerToolTraces}. */
1004
+ const filterXaiServerToolTraces = filterHostedServerToolTraces;
1005
+ /** A function-calling tool DSH registered as web_search / web_fetch. */
1006
+ function isDshWebFunctionTool(tool) {
1007
+ if (!isRecord(tool)) return false;
1008
+ const name = typeof tool.name === "string" ? tool.name : void 0;
1009
+ if (name === void 0 || !isDshWebToolName(name)) return false;
1010
+ const type = tool.type;
1011
+ return type === void 0 || type === "function";
1012
+ }
1013
+ function hostedToolKey(tool) {
1014
+ return `${typeof tool.type === "string" ? tool.type : "function"}:${typeof tool.name === "string" ? tool.name : ""}`;
1015
+ }
1016
+ function applyNativeToolsToPayload(payload, providerId, api, policy = DEFAULT_NATIVE_TOOL_POLICY) {
1017
+ const plan = nativePlan(providerId, api, policy);
1018
+ if (plan === void 0 || !isRecord(payload) || !Array.isArray(payload.tools)) return payload;
1019
+ const kept = payload.tools.filter((tool) => !isDshWebFunctionTool(tool));
1020
+ const seen = new Set(kept.filter(isRecord).map((tool) => hostedToolKey(tool)));
1021
+ const hosted = plan.hosted.filter((tool) => {
1022
+ const key = hostedToolKey(tool);
1023
+ if (seen.has(key)) return false;
1024
+ seen.add(key);
1025
+ return true;
1026
+ });
1027
+ return {
1028
+ ...payload,
1029
+ tools: [...hosted, ...kept]
1030
+ };
1031
+ }
1032
+ function wrapOnPayload(existing, providerId, api, policy = DEFAULT_NATIVE_TOOL_POLICY) {
1033
+ if (nativePlan(providerId, api, policy) === void 0) return existing;
1034
+ return async (payload, model) => {
1035
+ const injected = applyNativeToolsToPayload(isRecord(payload) && !("tools" in payload) ? {
1036
+ ...payload,
1037
+ tools: []
1038
+ } : payload, providerId, api, policy);
1039
+ if (existing === void 0) return injected;
1040
+ return await existing(injected, model);
1041
+ };
1042
+ }
1043
+ function filterPiContext(context, providerId, api, policy = DEFAULT_NATIVE_TOOL_POLICY) {
1044
+ if (nativePlan(providerId, api, policy) === void 0 || context.tools === void 0) return context;
1045
+ return {
1046
+ ...context,
1047
+ tools: context.tools.filter((tool) => !isDshWebToolName(tool.name))
1048
+ };
1049
+ }
1050
+ /**
1051
+ * Prepare one provider request as a single unit.
1052
+ *
1053
+ * Native tools are a property of this request, not of the provider account.
1054
+ * A context without tools is a text-only call (reviewers, summaries, titles,
1055
+ * and similar utility traffic), so its payload hook must stay untouched even
1056
+ * when the provider serializer later emits `tools: []`.
1057
+ */
1058
+ function prepareNativeToolRequest(context, options, providerId, api, policy = DEFAULT_NATIVE_TOOL_POLICY) {
1059
+ if (context.tools === void 0 || nativePlan(providerId, api, policy) === void 0) return {
1060
+ context,
1061
+ options
1062
+ };
1063
+ const onPayload = wrapOnPayload(options?.onPayload, providerId, api, policy);
1064
+ return {
1065
+ context: filterPiContext(context, providerId, api, policy),
1066
+ options: onPayload === options.onPayload ? options : Object.assign({}, options, { onPayload })
1067
+ };
1068
+ }
1069
+ function maskDshWebAssembly(assembly, plan) {
1070
+ return {
1071
+ ...assembly,
1072
+ tools: assembly.tools.filter((tool) => !isDshWebToolName(tool.name)),
1073
+ sections: [...assembly.sections.filter((section) => !isDshWebSectionName(section.name)), {
1074
+ name: "oauth:native-tools",
1075
+ text: plan.guidance
1076
+ }]
1077
+ };
1078
+ }
1079
+ //#endregion
1080
+ //#region src/adapter.ts
1081
+ /** One PiAiAdapter covering every Pi-login harness route. */
1082
+ /** Keep provider failures routable for both direct and snapshot-prepared requests. */
1083
+ async function* withFailureHints(source, provider) {
1084
+ try {
1085
+ for await (const chunk of source) if (chunk.type === "finish" && chunk.reason.kind === "error") yield {
1086
+ ...chunk,
1087
+ reason: {
1088
+ ...chunk.reason,
1089
+ failure: hintFailure(chunk.reason.failure, provider)
1090
+ }
1091
+ };
1092
+ else yield chunk;
1093
+ } catch (error) {
1094
+ throw withModelErrorHint(error, provider);
1095
+ }
1096
+ }
1097
+ /**
1098
+ * Official Pi adapter plus bounded provider compatibility and Chat copy.
1099
+ * `dsh-llm-retry` owns the attempt budget and routes on `code`. Finish chunks
1100
+ * and thrown errors receive the same route-specific correction and hint.
1101
+ */
1102
+ var PiLoginAdapter = class extends PiAiAdapter {
1103
+ native;
1104
+ resolveAttachments;
1105
+ session;
1106
+ constructor(config, native, resolveAttachments, session) {
1107
+ super(config);
1108
+ this.native = native;
1109
+ this.resolveAttachments = resolveAttachments;
1110
+ this.session = session;
1111
+ }
1112
+ resolveModel(provider, model, signal) {
1113
+ return super.resolveModel(provider, model, signal).then((info) => {
1114
+ const defaultEffort = this.defaultEffort(provider, model);
1115
+ if (defaultEffort === void 0 || info.reasoning === void 0) return info;
1116
+ return {
1117
+ ...info,
1118
+ reasoning: {
1119
+ ...info.reasoning,
1120
+ defaultEffort: ReasoningEffortId(defaultEffort)
1121
+ }
1122
+ };
1123
+ });
1124
+ }
1125
+ defaultEffort(provider, model) {
1126
+ return (provider === "pi-openrouter" ? openRouterDefaultEffort(this.session.openRouter.model(model)) : void 0) ?? defaultReasoningEffortFor(model);
1127
+ }
1128
+ sanitize(options) {
1129
+ const defaultEffort = this.defaultEffort(options.provider, options.model);
1130
+ return {
1131
+ ...options,
1132
+ messages: stripAssistantImages(options.messages),
1133
+ ...options.reasoningEffort === void 0 && defaultEffort !== void 0 ? { reasoningEffort: ReasoningEffortId(defaultEffort) } : {}
1134
+ };
1135
+ }
1136
+ /**
1137
+ * Live `llm.stream` dispatches `prepareCall().stream`, not `adapter.stream()`.
1138
+ * Hosted X Search / web_search traces must be stripped on that path too,
1139
+ * or Harness executes them as UNKNOWN_TOOL.
1140
+ */
1141
+ async *decorateStream(options, source) {
1142
+ const capture = { images: [] };
1143
+ try {
1144
+ const raw = iterateInCapture(capture, this.session.proxy.iterate(source, options.provider === "pi-openai-codex" && options.sessionId !== void 0 ? String(options.sessionId) : void 0));
1145
+ const spec = piLoginProviderByRoute(options.provider);
1146
+ const api = spec === void 0 ? void 0 : this.session.visibleModels(spec.id).find((model) => model.id === options.model)?.api;
1147
+ const filtered = nativePlanForRoute(options.provider, api, this.native) === void 0 ? raw : filterHostedServerToolTraces(raw);
1148
+ const attachments = this.native.image ? this.resolveAttachments() : void 0;
1149
+ yield* withFailureHints(attachments === void 0 ? filtered : injectHostedImages(filtered, capture, (input) => attachments.saveImage(input)), options.provider);
1150
+ } catch (error) {
1151
+ throw withModelErrorHint(error, options.provider);
1152
+ }
1153
+ }
1154
+ async prepareCall(provider, model, signal) {
1155
+ const prepared = await this.session.proxy.run(() => super.prepareCall(provider, model, signal));
1156
+ return {
1157
+ ...prepared,
1158
+ stream: (options) => {
1159
+ const sanitized = this.sanitize(options);
1160
+ return this.decorateStream(sanitized, prepared.stream(sanitized));
1161
+ }
1162
+ };
1163
+ }
1164
+ async *stream(options) {
1165
+ const sanitized = this.sanitize(options);
1166
+ yield* this.decorateStream(sanitized, super.stream(sanitized));
1167
+ }
1168
+ };
1169
+ function createPiLoginAdapter(session, resolveAttachments, options = {}) {
1170
+ const streamIdleTimeoutMs = options.streamIdleTimeoutMs ?? 3e5;
1171
+ const retryPolicy = resolveRetryPolicy(options.retryPolicy, "dsh-oauth-login retryPolicy");
1172
+ const requestImageLimits = {
1173
+ requestImagePixelBudget: 4194304,
1174
+ requestImageMaxBytes: 1048576
1175
+ };
1176
+ return new PiLoginAdapter({
1177
+ profiles: () => {
1178
+ const profiles = /* @__PURE__ */ new Map();
1179
+ for (const spec of PI_LOGIN_PROVIDERS) profiles.set(spec.route, {
1180
+ provider: spec.route,
1181
+ displayName: spec.displayName,
1182
+ streamIdleTimeoutMs,
1183
+ maxRequestImageBytes: PI_LOGIN_MAX_REQUEST_IMAGE_BYTES,
1184
+ ...requestImageLimits,
1185
+ retryPolicy,
1186
+ configuredMaxTokens: /* @__PURE__ */ new Map(),
1187
+ modelErrors: /* @__PURE__ */ new Map(),
1188
+ piProvider: session.provider(spec.id)
1189
+ });
1190
+ return profiles;
1191
+ },
1192
+ resolveApiKey: async (route) => {
1193
+ await session.ensureTransport();
1194
+ const spec = piLoginProviderByRoute(route);
1195
+ if (spec === void 0) throw new LlmError(`dsh-oauth-login: unknown route "${route}"`, "MISSING_CREDENTIAL");
1196
+ const apiKey = (await session.models.getAuth(spec.id))?.auth.apiKey;
1197
+ if (apiKey === void 0 || apiKey.length === 0) throw new LlmError(`${spec.displayName} is not connected. Open Settings → Subscription Login and connect it.`, "MISSING_CREDENTIAL");
1198
+ return apiKey;
1199
+ },
1200
+ resolveAttachments,
1201
+ auth: {
1202
+ credentials: session.store,
1203
+ authContext: defaultProviderAuthContext()
1204
+ }
1205
+ }, session.native, resolveAttachments, session);
1206
+ }
1207
+ //#endregion
1208
+ //#region src/responses-tap.ts
1209
+ /** Peek Responses SSE for hosted image_generation_call items pi-ai ignores. */
1210
+ const TAPPED = Symbol("dsh-oauth-hosted-tap");
1211
+ function requestUrl(input) {
1212
+ if (typeof input === "string") return input;
1213
+ if (input instanceof URL) return input.href;
1214
+ return input.url;
1215
+ }
1216
+ function requestMethod(input, init) {
1217
+ if (init?.method !== void 0) return init.method.toUpperCase();
1218
+ if (typeof input !== "string" && !(input instanceof URL) && input.method.length > 0) return input.method.toUpperCase();
1219
+ return "GET";
1220
+ }
1221
+ function isResponsesRequest(input, init) {
1222
+ if (requestMethod(input, init) !== "POST") return false;
1223
+ try {
1224
+ const path = new URL(requestUrl(input)).pathname;
1225
+ return path === "/responses" || path.endsWith("/responses");
1226
+ } catch {
1227
+ return false;
1228
+ }
1229
+ }
1230
+ function parseSseBlock(block) {
1231
+ const data = block.split(/\r?\n/).filter((line) => line.startsWith("data:")).map((line) => line.slice(5).trimStart()).join("\n").trim();
1232
+ if (data.length === 0 || data === "[DONE]") return void 0;
1233
+ try {
1234
+ return JSON.parse(data);
1235
+ } catch {
1236
+ return;
1237
+ }
1238
+ }
1239
+ function consumeResponsesSseText(text, onEvent) {
1240
+ const parts = text.split(/\r?\n\r?\n/);
1241
+ const rest = parts.pop() ?? "";
1242
+ for (const block of parts) {
1243
+ const event = parseSseBlock(block);
1244
+ if (event !== void 0) onEvent(event);
1245
+ }
1246
+ return rest;
1247
+ }
1248
+ function peekResponsesBody(body, onEvent) {
1249
+ const decoder = new TextDecoder();
1250
+ let buffer = "";
1251
+ return body.pipeThrough(new TransformStream({
1252
+ transform(chunk, controller) {
1253
+ controller.enqueue(chunk);
1254
+ buffer = consumeResponsesSseText(buffer + decoder.decode(chunk, { stream: true }), onEvent);
1255
+ },
1256
+ flush(controller) {
1257
+ if (buffer.length === 0) return;
1258
+ const event = parseSseBlock(buffer);
1259
+ if (event !== void 0) onEvent(event);
1260
+ }
1261
+ }));
1262
+ }
1263
+ function tapResponsesResponse(response, onEvent) {
1264
+ if (response.body === null) return response;
1265
+ return new Response(peekResponsesBody(response.body, onEvent), response);
1266
+ }
1267
+ function wrapFetchForHostedOutput(fetchImpl) {
1268
+ return async (input, init) => {
1269
+ const capture = currentHostedCapture();
1270
+ if (capture === void 0 || !isResponsesRequest(input, init)) return await fetchImpl(input, init);
1271
+ return tapResponsesResponse(await fetchImpl(input, init), (event) => collectHostedImagesFromEvent(event, capture.images));
1272
+ };
1273
+ }
1274
+ function installHostedOutputFetch() {
1275
+ const current = globalThis.fetch;
1276
+ if (current[TAPPED] === true) return;
1277
+ const next = Object.assign(wrapFetchForHostedOutput(current), { [TAPPED]: true });
1278
+ globalThis.fetch = next;
1279
+ }
1280
+ //#endregion
1281
+ //#region src/http.ts
1282
+ /** Proxy-aware HTTP transport for OAuth and subscribed-provider requests. */
1283
+ const originalFetch = globalThis.fetch;
1284
+ let installedFetch;
1285
+ const DEFAULT_LOOPBACK_PROXY_CANDIDATES = [
1286
+ "http://127.0.0.1:7890",
1287
+ "http://127.0.0.1:45678",
1288
+ "http://127.0.0.1:7891",
1289
+ "http://127.0.0.1:8080",
1290
+ "http://127.0.0.1:8888"
1291
+ ];
1292
+ const PROXY_ENV_KEYS = [
1293
+ "HTTPS_PROXY",
1294
+ "https_proxy",
1295
+ "HTTP_PROXY",
1296
+ "http_proxy",
1297
+ "ALL_PROXY",
1298
+ "all_proxy"
1299
+ ];
1300
+ function normalizeProxyUrl(value) {
1301
+ const trimmed = value.trim();
1302
+ if (trimmed.length === 0) throw new Error("dsh-oauth-login: proxy URL is empty");
1303
+ const url = new URL(trimmed.includes("://") ? trimmed : `http://${trimmed}`);
1304
+ if (url.protocol !== "http:" && url.protocol !== "https:") throw new Error(`dsh-oauth-login: unsupported proxy protocol "${url.protocol}"`);
1305
+ if (url.hostname.length === 0) throw new Error("dsh-oauth-login: proxy URL has no host");
1306
+ url.pathname = "/";
1307
+ url.search = "";
1308
+ url.hash = "";
1309
+ return url.toString();
1310
+ }
1311
+ function inheritedProxy(env) {
1312
+ for (const key of PROXY_ENV_KEYS) {
1313
+ const value = env[key];
1314
+ if (value !== void 0 && value.trim().length > 0) return value;
1315
+ }
1316
+ }
1317
+ function scutilValue(output, key) {
1318
+ const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1319
+ return output.match(new RegExp(`^\\s*${escaped}\\s*:\\s*(.+?)\\s*$`, "m"))?.[1];
1320
+ }
1321
+ function parseMacOSSystemProxy(output) {
1322
+ for (const kind of ["HTTPS", "HTTP"]) {
1323
+ if (scutilValue(output, `${kind}Enable`) !== "1") continue;
1324
+ const host = scutilValue(output, `${kind}Proxy`);
1325
+ const port = Number(scutilValue(output, `${kind}Port`));
1326
+ if (host === void 0 || host.length === 0 || !Number.isInteger(port) || port <= 0 || port > 65535) continue;
1327
+ return normalizeProxyUrl(`http://${host.includes(":") && !host.startsWith("[") ? `[${host}]` : host}:${port}`);
1328
+ }
1329
+ }
1330
+ async function readMacOSSystemProxy() {
1331
+ return await new Promise((resolve) => {
1332
+ execFile("/usr/sbin/scutil", ["--proxy"], {
1333
+ encoding: "utf8",
1334
+ timeout: 1500,
1335
+ maxBuffer: 131072
1336
+ }, (error, stdout) => resolve(error === null ? parseMacOSSystemProxy(stdout) : void 0));
1337
+ });
1338
+ }
1339
+ function isLoopbackProxy(proxyUrl) {
1340
+ const hostname = new URL(proxyUrl).hostname.replace(/^\[(.*)\]$/, "$1").toLowerCase();
1341
+ return hostname === "127.0.0.1" || hostname === "localhost" || hostname === "::1";
1342
+ }
1343
+ /**
1344
+ * Verify that a candidate is an HTTP CONNECT proxy. The probe contains no
1345
+ * OAuth code, credential, token, cookie, or geographic metadata.
1346
+ */
1347
+ async function probeHttpConnectProxy(proxyUrl) {
1348
+ const url = new URL(proxyUrl);
1349
+ if (url.protocol !== "http:") return false;
1350
+ const port = Number(url.port || "80");
1351
+ if (!Number.isInteger(port) || port <= 0 || port > 65535) return false;
1352
+ return await new Promise((resolve) => {
1353
+ const socket = connect({
1354
+ host: url.hostname,
1355
+ port
1356
+ });
1357
+ let settled = false;
1358
+ let response = "";
1359
+ const finish = (accepted) => {
1360
+ if (settled) return;
1361
+ settled = true;
1362
+ socket.destroy();
1363
+ resolve(accepted);
1364
+ };
1365
+ socket.setTimeout(800);
1366
+ socket.once("connect", () => {
1367
+ socket.write("CONNECT auth.openai.com:443 HTTP/1.1\r\nHost: auth.openai.com:443\r\nConnection: close\r\n\r\n");
1368
+ });
1369
+ socket.on("data", (chunk) => {
1370
+ response += chunk.toString("latin1");
1371
+ const lineEnd = response.indexOf("\r\n");
1372
+ if (lineEnd >= 0) finish(/^HTTP\/1\.[01] 200(?:\s|$)/.test(response.slice(0, lineEnd)));
1373
+ else if (response.length > 16384) finish(false);
1374
+ });
1375
+ socket.once("timeout", () => finish(false));
1376
+ socket.once("error", () => finish(false));
1377
+ socket.once("end", () => finish(/^HTTP\/1\.[01] 200(?:\s|$)/.test(response)));
1378
+ });
1379
+ }
1380
+ async function resolveOAuthProxy(options = {}) {
1381
+ const env = options.env ?? process.env;
1382
+ if (inheritedProxy(env) !== void 0) return { source: "environment" };
1383
+ const explicit = env.DSH_OAUTH_PROXY;
1384
+ if (explicit !== void 0 && explicit.trim().length > 0) return {
1385
+ source: "explicit",
1386
+ proxyUrl: normalizeProxyUrl(explicit)
1387
+ };
1388
+ const probe = options.probe ?? probeHttpConnectProxy;
1389
+ if ((options.platform ?? process.platform) === "darwin") {
1390
+ const systemProxy = await (options.readSystemProxy ?? readMacOSSystemProxy)();
1391
+ if (systemProxy !== void 0) {
1392
+ const normalized = normalizeProxyUrl(systemProxy);
1393
+ if (await probe(normalized)) return {
1394
+ source: "system",
1395
+ proxyUrl: normalized
1396
+ };
1397
+ }
1398
+ }
1399
+ const normalizedCandidates = (options.candidates ?? DEFAULT_LOOPBACK_PROXY_CANDIDATES).flatMap((candidate) => {
1400
+ try {
1401
+ const normalized = normalizeProxyUrl(candidate);
1402
+ return isLoopbackProxy(normalized) ? [normalized] : [];
1403
+ } catch {
1404
+ return [];
1405
+ }
1406
+ });
1407
+ const selected = (await Promise.all(normalizedCandidates.map(async (candidate) => {
1408
+ try {
1409
+ return await probe(candidate);
1410
+ } catch {
1411
+ return false;
1412
+ }
1413
+ }))).findIndex(Boolean);
1414
+ if (selected >= 0) return {
1415
+ source: "loopback",
1416
+ proxyUrl: normalizedCandidates[selected]
1417
+ };
1418
+ return { source: "direct" };
1419
+ }
1420
+ function noProxyValue(env) {
1421
+ const entries = new Set((env.NO_PROXY ?? env.no_proxy ?? "").split(/[\s,]+/).filter(Boolean));
1422
+ entries.add("localhost");
1423
+ entries.add("127.0.0.1");
1424
+ entries.add("::1");
1425
+ return [...entries].join(",");
1426
+ }
1427
+ function inheritedProxyOptions(env) {
1428
+ const allProxy = env.ALL_PROXY ?? env.all_proxy ?? "";
1429
+ return {
1430
+ httpProxy: env.HTTP_PROXY ?? env.http_proxy ?? allProxy,
1431
+ httpsProxy: env.HTTPS_PROXY ?? env.https_proxy ?? allProxy
1432
+ };
1433
+ }
1434
+ async function createOAuthHttpTransport(options = {}) {
1435
+ const env = options.env ?? process.env;
1436
+ const resolution = await resolveOAuthProxy({
1437
+ ...options,
1438
+ env
1439
+ });
1440
+ const selectedProxy = "proxyUrl" in resolution ? resolution.proxyUrl : void 0;
1441
+ const inherited = inheritedProxyOptions(env);
1442
+ return {
1443
+ dispatcher: new EnvHttpProxyAgent({
1444
+ allowH2: false,
1445
+ httpProxy: selectedProxy ?? inherited.httpProxy,
1446
+ httpsProxy: selectedProxy ?? inherited.httpsProxy,
1447
+ noProxy: noProxyValue(env)
1448
+ }),
1449
+ resolution
1450
+ };
1451
+ }
1452
+ function installOAuthHttpGlobals() {
1453
+ if (installedFetch === void 0 ? globalThis.fetch === originalFetch : globalThis.fetch === installedFetch) {
1454
+ install();
1455
+ installedFetch = globalThis.fetch;
1456
+ }
1457
+ installHostedOutputFetch();
1458
+ }
1459
+ //#endregion
1460
+ //#region src/proxy-config.ts
1461
+ /** Shared, credential-free settings for this plugin's two outbound transports. */
1462
+ const PROXY_SETTINGS_PATH = "/plugins/dsh-oauth-login/network/settings";
1463
+ const PROXY_SETTINGS_FILENAME = ".dsh-oauth-proxy.json";
1464
+ function defaultProxySettings() {
1465
+ return {
1466
+ revision: 0,
1467
+ http: {
1468
+ enabled: true,
1469
+ url: ""
1470
+ },
1471
+ websocket: {
1472
+ enabled: true,
1473
+ url: ""
1474
+ }
1475
+ };
1476
+ }
1477
+ function object(value, keys, label) {
1478
+ if (typeof value !== "object" || value === null || Array.isArray(value) || Object.keys(value).some((key) => !keys.includes(key))) throw new Error(`${label}: invalid settings object`);
1479
+ return value;
1480
+ }
1481
+ function channel(value, label) {
1482
+ const raw = object(value, ["enabled", "url"], label);
1483
+ if (typeof raw.enabled !== "boolean" || typeof raw.url !== "string" || raw.url.length > 1024) throw new Error(`${label}: expected enabled and proxy URL`);
1484
+ const text = raw.url.trim();
1485
+ if (text === "") return {
1486
+ enabled: raw.enabled,
1487
+ url: ""
1488
+ };
1489
+ let parsed;
1490
+ try {
1491
+ parsed = new URL(text);
1492
+ } catch {
1493
+ throw new Error(`${label}: enter a valid HTTP proxy address and port`);
1494
+ }
1495
+ if (!["http:", "https:"].includes(parsed.protocol) || !parsed.hostname || parsed.username || parsed.password || parsed.search || parsed.hash || parsed.pathname !== "/" || parsed.port === "0") throw new Error(`${label}: use an HTTP(S) proxy without credentials, path, query or fragment`);
1496
+ return {
1497
+ enabled: raw.enabled,
1498
+ url: parsed.origin
1499
+ };
1500
+ }
1501
+ function parseProxySettings(value) {
1502
+ const raw = object(value, [
1503
+ "revision",
1504
+ "http",
1505
+ "websocket"
1506
+ ], "Network");
1507
+ if (typeof raw.revision !== "number" || !Number.isSafeInteger(raw.revision) || raw.revision < 0) throw new Error("Network: invalid revision");
1508
+ return {
1509
+ revision: raw.revision,
1510
+ http: channel(raw.http, "HTTP"),
1511
+ websocket: channel(raw.websocket, "WebSocket")
1512
+ };
1513
+ }
1514
+ //#endregion
1515
+ //#region src/proxy-store.ts
1516
+ /** Independent, atomic settings file; never writes the OAuth credential document. */
1517
+ var ProxySettingsConflict = class extends Error {};
1518
+ var ProxySettingsStore = class {
1519
+ filename;
1520
+ constructor(filename) {
1521
+ this.filename = filename;
1522
+ }
1523
+ async read() {
1524
+ let text;
1525
+ try {
1526
+ text = await readFile(this.filename, "utf8");
1527
+ } catch (error) {
1528
+ if (error.code === "ENOENT") return defaultProxySettings();
1529
+ throw new Error("Network settings could not be read", { cause: error });
1530
+ }
1531
+ let document;
1532
+ try {
1533
+ document = JSON.parse(text);
1534
+ } catch {
1535
+ throw new Error("Network settings file is not valid JSON");
1536
+ }
1537
+ if (typeof document !== "object" || document === null || !("version" in document) || document.version !== 1 || !("settings" in document) || Object.keys(document).some((key) => key !== "version" && key !== "settings")) throw new Error("Unsupported network settings file");
1538
+ return parseProxySettings(document.settings);
1539
+ }
1540
+ async save(value) {
1541
+ const input = parseProxySettings(value);
1542
+ await mkdir(dirname(this.filename), {
1543
+ recursive: true,
1544
+ mode: 448
1545
+ });
1546
+ return withFileLock(this.filename, async () => {
1547
+ const current = await this.read();
1548
+ if (current.revision !== input.revision) throw new ProxySettingsConflict("Network settings changed. Reload them before saving.");
1549
+ const settings = {
1550
+ ...input,
1551
+ revision: current.revision + 1
1552
+ };
1553
+ await writeFileAtomic(this.filename, `${JSON.stringify({
1554
+ version: 1,
1555
+ settings
1556
+ }, null, 2)}\n`, {
1557
+ mode: 384,
1558
+ dirMode: 448
1559
+ });
1560
+ return settings;
1561
+ });
1562
+ }
1563
+ };
1564
+ //#endregion
1565
+ //#region src/proxy-transport.ts
1566
+ /** Plugin-scoped HTTP/WS routing without changing process.env or provider SDKs. */
1567
+ const ROUTING_KEY = Symbol.for("dsh-oauth-login.scoped-proxy.v1");
1568
+ const globals = globalThis;
1569
+ const routing = globals[ROUTING_KEY] ??= { context: new AsyncLocalStorage() };
1570
+ function isLoopback(origin) {
1571
+ if (origin === void 0) return false;
1572
+ const hostname = new URL(origin).hostname.toLowerCase().replace(/^\[|\]$/g, "");
1573
+ return hostname === "localhost" || hostname.endsWith(".localhost") || hostname === "::1" || isIP(hostname) === 4 && hostname.startsWith("127.");
1574
+ }
1575
+ function installRouting() {
1576
+ const fallback = getGlobalDispatcher();
1577
+ if (fallback !== routing.dispatcher) {
1578
+ routing.dispatcher = fallback.compose((dispatch) => (options, handler) => {
1579
+ const scope = routing.context.getStore();
1580
+ if (scope === void 0) return dispatch(options, handler);
1581
+ const websocket = typeof options.upgrade === "string" && options.upgrade.toLowerCase() === "websocket";
1582
+ return (isLoopback(options.origin) ? scope.direct : websocket ? scope.websocket : scope.http).dispatch(options, handler);
1583
+ });
1584
+ setGlobalDispatcher(routing.dispatcher);
1585
+ }
1586
+ installOAuthHttpGlobals();
1587
+ }
1588
+ var OAuthProxyTransport = class {
1589
+ discovery;
1590
+ settings;
1591
+ readyPromise;
1592
+ generations = /* @__PURE__ */ new Set();
1593
+ sessions = /* @__PURE__ */ new Map();
1594
+ disposed = false;
1595
+ constructor(filename, discovery = {}) {
1596
+ this.discovery = discovery;
1597
+ this.settings = new ProxySettingsStore(filename);
1598
+ }
1599
+ async build() {
1600
+ const settings = await this.settings.read();
1601
+ const auto = [settings.http, settings.websocket].some((channel) => channel.enabled && channel.url === "") ? await createOAuthHttpTransport(this.discovery) : void 0;
1602
+ const direct = new Agent({ allowH2: false });
1603
+ const agents = /* @__PURE__ */ new Set([direct, ...auto === void 0 ? [] : [auto.dispatcher]]);
1604
+ const select = (channel) => {
1605
+ if (!channel.enabled) return direct;
1606
+ if (channel.url === "") {
1607
+ if (auto === void 0) throw new Error("Automatic proxy transport is unavailable");
1608
+ return auto.dispatcher;
1609
+ }
1610
+ const proxy = new ProxyAgent({
1611
+ uri: channel.url,
1612
+ allowH2: false
1613
+ });
1614
+ agents.add(proxy);
1615
+ return proxy;
1616
+ };
1617
+ const ready = {
1618
+ scope: {
1619
+ direct,
1620
+ http: select(settings.http),
1621
+ websocket: select(settings.websocket)
1622
+ },
1623
+ resolution: !settings.http.enabled ? { source: "direct" } : settings.http.url !== "" ? {
1624
+ source: "explicit",
1625
+ proxyUrl: settings.http.url
1626
+ } : auto?.resolution ?? { source: "direct" },
1627
+ agents,
1628
+ wsKey: JSON.stringify(settings.websocket) + (settings.websocket.enabled && !settings.websocket.url ? `:${settings.revision}` : ""),
1629
+ active: 0,
1630
+ retired: false,
1631
+ closing: false
1632
+ };
1633
+ if (this.disposed) {
1634
+ await Promise.allSettled([...agents].map((agent) => agent.destroy()));
1635
+ throw new Error("OAuth proxy transport disposed");
1636
+ }
1637
+ this.generations.add(ready);
1638
+ installRouting();
1639
+ return ready;
1640
+ }
1641
+ ready() {
1642
+ if (this.disposed) return Promise.reject(/* @__PURE__ */ new Error("OAuth proxy transport disposed"));
1643
+ if (this.readyPromise === void 0) {
1644
+ const pending = this.build();
1645
+ this.readyPromise = pending;
1646
+ pending.catch(() => {
1647
+ if (this.readyPromise === pending) this.readyPromise = void 0;
1648
+ });
1649
+ }
1650
+ return this.readyPromise;
1651
+ }
1652
+ async initialize() {
1653
+ return (await this.ready()).resolution;
1654
+ }
1655
+ async acquire() {
1656
+ while (true) {
1657
+ const ready = await this.ready();
1658
+ if (ready.retired) continue;
1659
+ ready.active++;
1660
+ return ready;
1661
+ }
1662
+ }
1663
+ async save(value) {
1664
+ if (this.disposed) throw new Error("OAuth proxy transport disposed");
1665
+ const saved = await this.settings.save(value);
1666
+ const previous = this.readyPromise;
1667
+ this.readyPromise = void 0;
1668
+ previous?.then((ready) => {
1669
+ ready.retired = true;
1670
+ this.release(ready, false);
1671
+ }, () => void 0);
1672
+ return saved;
1673
+ }
1674
+ release(ready, decrement = true) {
1675
+ if (decrement) ready.active--;
1676
+ if (ready.retired && ready.active === 0 && !ready.closing) {
1677
+ ready.closing = true;
1678
+ Promise.allSettled([...ready.agents].map((agent) => agent.close())).then(() => {
1679
+ this.generations.delete(ready);
1680
+ });
1681
+ }
1682
+ }
1683
+ async run(operation) {
1684
+ const ready = await this.acquire();
1685
+ try {
1686
+ installRouting();
1687
+ return await routing.context.run(ready.scope, operation);
1688
+ } finally {
1689
+ this.release(ready);
1690
+ }
1691
+ }
1692
+ beginSession(sessionId, ready) {
1693
+ if (sessionId === void 0) return void 0;
1694
+ let record = this.sessions.get(sessionId);
1695
+ if (record === void 0) {
1696
+ record = {
1697
+ key: ready.wsKey,
1698
+ active: 0,
1699
+ resetPending: false
1700
+ };
1701
+ this.sessions.set(sessionId, record);
1702
+ }
1703
+ if (record.key !== ready.wsKey) record.resetPending = true;
1704
+ if (record.resetPending && record.active === 0) {
1705
+ closeOpenAICodexWebSocketSessions(sessionId);
1706
+ record.key = ready.wsKey;
1707
+ record.resetPending = false;
1708
+ }
1709
+ record.active++;
1710
+ return () => {
1711
+ record.active--;
1712
+ };
1713
+ }
1714
+ async *iterate(source, codexSessionId) {
1715
+ const ready = await this.acquire();
1716
+ let endSession;
1717
+ let iterator;
1718
+ try {
1719
+ installRouting();
1720
+ endSession = this.beginSession(codexSessionId, ready);
1721
+ iterator = routing.context.run(ready.scope, () => source[Symbol.asyncIterator]());
1722
+ while (true) {
1723
+ const current = iterator;
1724
+ const item = await routing.context.run(ready.scope, () => current.next());
1725
+ if (item.done) return;
1726
+ yield item.value;
1727
+ }
1728
+ } finally {
1729
+ try {
1730
+ await routing.context.run(ready.scope, () => iterator?.return?.());
1731
+ } finally {
1732
+ endSession?.();
1733
+ this.release(ready);
1734
+ }
1735
+ }
1736
+ }
1737
+ async dispose() {
1738
+ this.disposed = true;
1739
+ await this.readyPromise?.catch(() => void 0);
1740
+ for (const sessionId of this.sessions.keys()) closeOpenAICodexWebSocketSessions(sessionId);
1741
+ this.sessions.clear();
1742
+ await Promise.allSettled([...this.generations].flatMap((ready) => [...ready.agents].map((agent) => agent.destroy())));
1743
+ this.generations.clear();
1744
+ }
1745
+ };
1746
+ //#endregion
1747
+ //#region src/openrouter-free.ts
1748
+ const FREE_ONLY_MESSAGE = "OpenRouter 免费保护:该模型已下线、价格变化或价格未确认,已阻止付费调用。请刷新模型列表并重新选择免费模型。";
1749
+ function protectOpenRouterPayload(payload, modelId) {
1750
+ const source = objectValue(payload);
1751
+ if (source === void 0) throw new LlmError(FREE_ONLY_MESSAGE, "OPENROUTER_FREE_ONLY");
1752
+ const result = { ...source };
1753
+ for (const key of [
1754
+ "models",
1755
+ "route",
1756
+ "router",
1757
+ "preset",
1758
+ "web_search_options",
1759
+ "image_config",
1760
+ "audio"
1761
+ ]) delete result[key];
1762
+ result.model = modelId;
1763
+ const pluginIds = /* @__PURE__ */ new Set([
1764
+ "web",
1765
+ "file-parser",
1766
+ "response-healing",
1767
+ "fusion",
1768
+ "pareto-router",
1769
+ "context-compression"
1770
+ ]);
1771
+ if (Array.isArray(source.plugins)) for (const plugin of source.plugins) {
1772
+ const id = objectValue(plugin)?.id;
1773
+ if (typeof id === "string") pluginIds.add(id);
1774
+ }
1775
+ result.plugins = [...pluginIds].map((id) => ({
1776
+ id,
1777
+ enabled: false
1778
+ }));
1779
+ result.modalities = ["text"];
1780
+ result.provider = {
1781
+ ...objectValue(source.provider),
1782
+ allow_fallbacks: false,
1783
+ require_parameters: true,
1784
+ max_price: {
1785
+ prompt: 0,
1786
+ completion: 0,
1787
+ request: 0,
1788
+ image: 0,
1789
+ audio: 0
1790
+ }
1791
+ };
1792
+ if (Array.isArray(result.tools) && result.tools.some((tool) => objectValue(tool)?.type !== "function")) throw new LlmError("OpenRouter 免费保护:免费调用不启用收费的服务端工具;请选择支持工具调用的免费模型。", "OPENROUTER_FREE_ONLY");
1793
+ if (Array.isArray(result.messages)) for (const message of result.messages) {
1794
+ const content = objectValue(message)?.content;
1795
+ if (Array.isArray(content) && content.some((part) => !["text", "image_url"].includes(String(objectValue(part)?.type)))) throw new LlmError("OpenRouter 免费保护:仅支持文本和图片输入,不启用收费文件解析或音视频处理。", "OPENROUTER_FREE_ONLY");
1796
+ }
1797
+ return result;
1798
+ }
1799
+ function prepareOpenRouterOptions(model, options, catalog) {
1800
+ const metadata = catalog?.model(model.id);
1801
+ const protectedAtStart = catalog?.protects(model.id) === true || model.id.endsWith(":free") || model.id === "openrouter/free" || metadata === void 0 && model.cost.input === 0 && model.cost.output === 0;
1802
+ const defaultEffort = openRouterDefaultEffort(metadata);
1803
+ const existing = options.onPayload;
1804
+ return {
1805
+ ...options,
1806
+ onPayload: async (payload, currentModel) => {
1807
+ const result = await existing?.(payload, currentModel) ?? payload;
1808
+ const data = objectValue(result);
1809
+ if (data !== void 0 && metadata?.reasoningMandatory) {
1810
+ const reasoning = objectValue(data.reasoning);
1811
+ if (reasoning?.effort === void 0 || reasoning.effort === "none") data.reasoning = {
1812
+ ...reasoning,
1813
+ effort: defaultEffort ?? "high"
1814
+ };
1815
+ }
1816
+ if (!protectedAtStart && catalog?.protects(model.id) !== true) return result;
1817
+ const latest = catalog?.model(model.id);
1818
+ if (catalog?.models() !== void 0 && latest?.priceStatus !== "free") throw new LlmError(FREE_ONLY_MESSAGE, "OPENROUTER_FREE_ONLY");
1819
+ return protectOpenRouterPayload(result, model.id);
1820
+ }
1821
+ };
1822
+ }
1823
+ //#endregion
1824
+ //#region src/provider.ts
1825
+ /** Installed pi-ai providers remapped onto independent harness routes. */
1826
+ function harnessApiKeyAuth(name) {
1827
+ return {
1828
+ name,
1829
+ resolve: ({ credential }) => Promise.resolve({
1830
+ auth: credential?.key === void 0 ? {} : { apiKey: credential.key },
1831
+ source: name
1832
+ })
1833
+ };
1834
+ }
1835
+ function catalogProvider(id) {
1836
+ const base = builtinProviders().find((candidate) => candidate.id === id);
1837
+ if (base === void 0) throw new Error(`dsh-oauth-login: the installed pi-ai catalog ships no "${id}" provider`);
1838
+ return base;
1839
+ }
1840
+ /**
1841
+ * Catalog models plus plugin-owned extras, remapped onto the harness route.
1842
+ * Extras fill gaps the installed pi-ai version has not shipped yet (e.g. grok-4.6).
1843
+ */
1844
+ /**
1845
+ * Prefer the extra's effort map when the installed catalog still lacks one.
1846
+ * Official OpenRouter generation currently sets `reasoning: true` and no map.
1847
+ */
1848
+ function overlayExtraModel(model, extra) {
1849
+ const transport = model.provider === "xai" && model.id === "grok-4.6" && extra.api === "openai-responses" ? {
1850
+ api: extra.api,
1851
+ compat: extra.compat
1852
+ } : {};
1853
+ const reasoning = extra.thinkingLevelMap !== void 0 && model.thinkingLevelMap === void 0 ? {
1854
+ reasoning: extra.reasoning,
1855
+ thinkingLevelMap: extra.thinkingLevelMap
1856
+ } : {};
1857
+ if (Object.keys(transport).length === 0 && Object.keys(reasoning).length === 0) return model;
1858
+ return {
1859
+ ...model,
1860
+ ...transport,
1861
+ ...reasoning
1862
+ };
1863
+ }
1864
+ function harnessModels(spec, catalog) {
1865
+ const base = catalogProvider(spec.id).getModels();
1866
+ const extras = extraModelsFor(spec.id);
1867
+ const extraById = new Map(extras.map((model) => [model.id, model]));
1868
+ const seen = /* @__PURE__ */ new Set();
1869
+ const merged = [];
1870
+ for (const model of base) {
1871
+ seen.add(model.id);
1872
+ const extra = extraById.get(model.id);
1873
+ merged.push(extra === void 0 ? model : overlayExtraModel(model, extra));
1874
+ }
1875
+ for (const extra of extras) {
1876
+ if (seen.has(extra.id)) continue;
1877
+ seen.add(extra.id);
1878
+ merged.push(extra);
1879
+ }
1880
+ const live = spec.id === "openrouter" ? catalog?.models() : void 0;
1881
+ const byId = new Map(merged.map((model) => [model.id, model]));
1882
+ return (live === void 0 ? merged : live.map((info) => openRouterPiModel(info, byId.get(info.id)))).map((model) => model.provider === spec.route ? model : {
1883
+ ...model,
1884
+ provider: spec.route
1885
+ });
1886
+ }
1887
+ function preferredModel(spec, models = harnessModels(spec)) {
1888
+ const ids = new Set(models.map((model) => model.id));
1889
+ for (const candidate of spec.preferredModels) if (ids.has(candidate)) return candidate;
1890
+ return models[0]?.id ?? spec.id;
1891
+ }
1892
+ function harnessProvider(spec, native = DEFAULT_NATIVE_TOOL_POLICY, catalog) {
1893
+ const base = catalogProvider(spec.id);
1894
+ return {
1895
+ id: spec.route,
1896
+ name: spec.displayName,
1897
+ ...base.baseUrl === void 0 ? {} : { baseUrl: base.baseUrl },
1898
+ auth: {
1899
+ ...base.auth,
1900
+ apiKey: harnessApiKeyAuth(spec.displayName)
1901
+ },
1902
+ getModels: () => harnessModels(spec, catalog),
1903
+ stream: (model, context, options) => {
1904
+ const request = prepareNativeToolRequest(context, options ?? {}, spec.id, model.api, native);
1905
+ const guarded = spec.id === "openrouter" ? prepareOpenRouterOptions(model, request.options, catalog) : request.options;
1906
+ return base.stream(model, request.context, guarded);
1907
+ },
1908
+ streamSimple: (model, context, options) => {
1909
+ const request = prepareNativeToolRequest(context, options ?? {}, spec.id, model.api, native);
1910
+ const guarded = spec.id === "openrouter" ? prepareOpenRouterOptions(model, request.options, catalog) : request.options;
1911
+ return base.streamSimple(model, request.context, guarded);
1912
+ }
1913
+ };
1914
+ }
1915
+ function allCatalogProviders() {
1916
+ return PI_LOGIN_PROVIDERS.map((spec) => catalogProvider(spec.id));
1917
+ }
1918
+ //#endregion
1919
+ //#region src/store.ts
1920
+ /**
1921
+ * Multi-provider subscription credential store. File is $DSH_HOME/.dsh-oauth-auth.json.
1922
+ * The old .pi-login-auth.json name is read only as a DSH-owned migration source.
1923
+ * Never ~/.codex, ~/.grok, ~/.claude, or ~/.pi/agent/auth.json.
1924
+ */
1925
+ const AUTH_FORMAT_VERSION = 1;
1926
+ const OAUTH_ALLOWED_FIELDS = /* @__PURE__ */ new Set([
1927
+ "type",
1928
+ "access",
1929
+ "refresh",
1930
+ "expires",
1931
+ "accountId",
1932
+ "enterpriseUrl",
1933
+ "availableModelIds"
1934
+ ]);
1935
+ const API_KEY_ALLOWED_FIELDS = /* @__PURE__ */ new Set([
1936
+ "type",
1937
+ "key",
1938
+ "env"
1939
+ ]);
1940
+ function isENOENT(error) {
1941
+ return error?.code === "ENOENT";
1942
+ }
1943
+ async function assertOwnerOnly(filename) {
1944
+ let mode;
1945
+ try {
1946
+ mode = (await stat(filename)).mode;
1947
+ } catch (error) {
1948
+ if (isENOENT(error)) return;
1949
+ throw error;
1950
+ }
1951
+ if (process.platform === "win32") return;
1952
+ if ((mode & 63) !== 0) throw new Error(`pi-login: ${filename} is readable beyond its owner (mode ${(mode & 511).toString(8)}); run "chmod 600 ${filename}" before starting again`);
1953
+ }
1954
+ function parseProviderEnv(raw, filename, providerId) {
1955
+ if (raw === void 0) return void 0;
1956
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) throw new Error(`pi-login: ${filename} credential for ${providerId} env must be an object`);
1957
+ const env = {};
1958
+ for (const [key, value] of Object.entries(raw)) {
1959
+ if (typeof value !== "string") throw new Error(`pi-login: ${filename} credential for ${providerId} env values must be strings`);
1960
+ env[key] = value;
1961
+ }
1962
+ return env;
1963
+ }
1964
+ function parseCredential(raw, filename, providerId) {
1965
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) throw new Error(`pi-login: ${filename} credential for ${providerId} must be an object`);
1966
+ const credential = raw;
1967
+ const type = credential["type"];
1968
+ const allowed = type === "oauth" ? OAUTH_ALLOWED_FIELDS : type === "api_key" ? API_KEY_ALLOWED_FIELDS : void 0;
1969
+ if (allowed === void 0) throw new Error(`pi-login: ${filename} credential for ${providerId} type must be oauth or api_key`);
1970
+ if (Object.keys(credential).some((key) => !allowed.has(key))) throw new Error(`pi-login: ${filename} credential for ${providerId} contains an unknown field`);
1971
+ if (type === "api_key") {
1972
+ if (typeof credential["key"] !== "string" || credential["key"].length === 0) throw new Error(`pi-login: ${filename} credential for ${providerId} key must be a non-empty string`);
1973
+ const env = parseProviderEnv(credential["env"], filename, providerId);
1974
+ return {
1975
+ type: "api_key",
1976
+ key: credential["key"],
1977
+ ...env === void 0 ? {} : { env }
1978
+ };
1979
+ }
1980
+ if (typeof credential["access"] !== "string" || credential["access"].length === 0) throw new Error(`pi-login: ${filename} credential for ${providerId} access must be a non-empty string`);
1981
+ if (typeof credential["refresh"] !== "string") throw new Error(`pi-login: ${filename} credential for ${providerId} refresh must be a string`);
1982
+ if (typeof credential["expires"] !== "number" || !Number.isFinite(credential["expires"]) || credential["expires"] <= 0) throw new Error(`pi-login: ${filename} credential for ${providerId} expires must be a positive finite number`);
1983
+ return credential;
1984
+ }
1985
+ function parseDocument(text, filename) {
1986
+ let value;
1987
+ try {
1988
+ value = JSON.parse(text);
1989
+ } catch {
1990
+ throw new Error(`pi-login: ${filename} is not valid JSON`);
1991
+ }
1992
+ if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`pi-login: ${filename} must contain an object`);
1993
+ const document = value;
1994
+ if (document["version"] !== AUTH_FORMAT_VERSION) throw new Error(`pi-login: ${filename} has unsupported auth format version ${String(document["version"])}`);
1995
+ if (Object.keys(document).some((key) => key !== "version" && key !== "credentials")) throw new Error(`pi-login: ${filename} contains an unknown top-level field`);
1996
+ const raw = document["credentials"];
1997
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) throw new Error(`pi-login: ${filename} credentials must be an object`);
1998
+ const owned = new Set(PI_LOGIN_PROVIDERS.map((provider) => provider.id));
1999
+ const credentials = {};
2000
+ for (const [providerId, entry] of Object.entries(raw)) {
2001
+ if (!owned.has(providerId)) throw new Error(`pi-login: ${filename} contains an unknown provider "${providerId}"`);
2002
+ credentials[providerId] = parseCredential(entry, filename, providerId);
2003
+ }
2004
+ return {
2005
+ version: AUTH_FORMAT_VERSION,
2006
+ credentials
2007
+ };
2008
+ }
2009
+ function cloneCredential(credential) {
2010
+ return structuredClone(credential);
2011
+ }
2012
+ function piLoginAuthPath(dshHome) {
2013
+ return resolve(join(resolveDshHome(dshHome), PI_LOGIN_AUTH_FILENAME));
2014
+ }
2015
+ var PiLoginCredentialStore = class {
2016
+ filename;
2017
+ legacyFilename;
2018
+ constructor(filename = piLoginAuthPath()) {
2019
+ this.filename = resolve(filename);
2020
+ this.legacyFilename = basename(this.filename) === ".dsh-oauth-auth.json" ? resolve(join(dirname(this.filename), LEGACY_PI_LOGIN_AUTH_FILENAME)) : void 0;
2021
+ }
2022
+ async readDocument() {
2023
+ const candidates = [this.filename, ...this.legacyFilename === void 0 ? [] : [this.legacyFilename]];
2024
+ for (const filename of candidates) {
2025
+ await assertOwnerOnly(filename);
2026
+ try {
2027
+ return parseDocument(await readFile(filename, "utf8"), filename);
2028
+ } catch (error) {
2029
+ if (isENOENT(error)) continue;
2030
+ throw error;
2031
+ }
2032
+ }
2033
+ return {
2034
+ version: AUTH_FORMAT_VERSION,
2035
+ credentials: {}
2036
+ };
2037
+ }
2038
+ async read(providerId, options) {
2039
+ options?.signal?.throwIfAborted();
2040
+ const credential = (await this.readDocument()).credentials[providerId];
2041
+ options?.signal?.throwIfAborted();
2042
+ return credential === void 0 ? void 0 : cloneCredential(credential);
2043
+ }
2044
+ async list(options) {
2045
+ options?.signal?.throwIfAborted();
2046
+ const document = await this.readDocument();
2047
+ options?.signal?.throwIfAborted();
2048
+ return Object.entries(document.credentials).map(([providerId, credential]) => ({
2049
+ providerId,
2050
+ type: credential.type
2051
+ }));
2052
+ }
2053
+ async modify(providerId, fn, options) {
2054
+ if (!PI_LOGIN_PROVIDERS.some((provider) => provider.id === providerId)) throw new Error(`pi-login: credential store does not own provider "${providerId}"`);
2055
+ options?.signal?.throwIfAborted();
2056
+ await mkdir(dirname(this.filename), {
2057
+ recursive: true,
2058
+ mode: 448
2059
+ });
2060
+ options?.signal?.throwIfAborted();
2061
+ return withFileLock(this.filename, async () => {
2062
+ options?.signal?.throwIfAborted();
2063
+ const document = await this.readDocument();
2064
+ options?.signal?.throwIfAborted();
2065
+ const current = document.credentials[providerId];
2066
+ const candidate = await fn(current === void 0 ? void 0 : cloneCredential(current));
2067
+ options?.signal?.throwIfAborted();
2068
+ if (candidate === void 0) return current === void 0 ? void 0 : cloneCredential(current);
2069
+ const next = parseCredential(candidate, this.filename, providerId);
2070
+ const credentials = {
2071
+ ...document.credentials,
2072
+ [providerId]: next
2073
+ };
2074
+ await writeFileAtomic(this.filename, `${JSON.stringify({
2075
+ version: AUTH_FORMAT_VERSION,
2076
+ credentials
2077
+ }, null, 2)}\n`, {
2078
+ mode: 384,
2079
+ dirMode: 448
2080
+ });
2081
+ return cloneCredential(next);
2082
+ });
2083
+ }
2084
+ async delete(providerId, options) {
2085
+ if (!PI_LOGIN_PROVIDERS.some((provider) => provider.id === providerId)) return;
2086
+ options?.signal?.throwIfAborted();
2087
+ await mkdir(dirname(this.filename), {
2088
+ recursive: true,
2089
+ mode: 448
2090
+ });
2091
+ options?.signal?.throwIfAborted();
2092
+ await withFileLock(this.filename, async () => {
2093
+ options?.signal?.throwIfAborted();
2094
+ const document = await this.readDocument();
2095
+ options?.signal?.throwIfAborted();
2096
+ if (document.credentials[providerId] === void 0) return;
2097
+ const { [providerId]: _removed, ...credentials } = document.credentials;
2098
+ if (Object.keys(credentials).length === 0) {
2099
+ await rm(this.filename, { force: true });
2100
+ return;
2101
+ }
2102
+ await writeFileAtomic(this.filename, `${JSON.stringify({
2103
+ version: AUTH_FORMAT_VERSION,
2104
+ credentials
2105
+ }, null, 2)}\n`, {
2106
+ mode: 384,
2107
+ dirMode: 448
2108
+ });
2109
+ });
2110
+ }
2111
+ };
2112
+ //#endregion
2113
+ //#region src/auth.ts
2114
+ /** DSH-owned models.login() for every subscribed provider in the catalog. */
2115
+ async function loginPiProvider(providerId, interaction, store = new PiLoginCredentialStore()) {
2116
+ const spec = requirePiLoginProvider(providerId);
2117
+ const models = createModels({ credentials: store });
2118
+ models.setProvider(catalogProvider(providerId));
2119
+ const proxy = new OAuthProxyTransport(join(dirname(store.filename), PROXY_SETTINGS_FILENAME));
2120
+ try {
2121
+ await proxy.run(() => models.login(providerId, spec.authType, interaction));
2122
+ } finally {
2123
+ await proxy.dispose();
2124
+ }
2125
+ }
2126
+ async function logoutPiProvider(providerId, store = new PiLoginCredentialStore()) {
2127
+ requirePiLoginProvider(providerId);
2128
+ await store.delete(providerId);
2129
+ }
2130
+ async function piLoginStatus(store = new PiLoginCredentialStore(), providerId) {
2131
+ const ids = providerId === void 0 ? (await store.list()).map((item) => item.providerId) : [providerId];
2132
+ const out = [];
2133
+ for (const id of ids) {
2134
+ const credential = await store.read(id);
2135
+ if (credential?.type === "oauth") {
2136
+ out.push({
2137
+ providerId: id,
2138
+ authenticated: true,
2139
+ credentialType: "oauth",
2140
+ expiresAt: new Date(credential.expires)
2141
+ });
2142
+ continue;
2143
+ }
2144
+ if (credential?.type === "api_key" && typeof credential.key === "string" && credential.key.length > 0) {
2145
+ out.push({
2146
+ providerId: id,
2147
+ authenticated: true,
2148
+ credentialType: "api_key"
2149
+ });
2150
+ continue;
2151
+ }
2152
+ out.push({
2153
+ providerId: id,
2154
+ authenticated: false
2155
+ });
2156
+ }
2157
+ return out;
2158
+ }
2159
+ async function loginPiProviderSession(providerId, interaction, session) {
2160
+ const spec = requirePiLoginProvider(providerId);
2161
+ await session.ensureTransport();
2162
+ session.models.setProvider(catalogProvider(providerId));
2163
+ await session.proxy.run(() => session.models.login(providerId, spec.authType, interaction));
2164
+ if (providerId === "openrouter") await session.openRouter.syncAuthentication("login");
2165
+ }
2166
+ //#endregion
2167
+ //#region src/redact.ts
2168
+ /** Remove token-like strings from an external OAuth diagnostic. */
2169
+ function safeMessage(error) {
2170
+ 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|api[_-]?key|key)=)[^&\s]+/giu, "$1[redacted]").slice(0, 1e3);
2171
+ }
2172
+ /** Only this provider's official HTTPS hosts may be opened for login. */
2173
+ function isSafeAuthUrl(raw, provider) {
2174
+ try {
2175
+ const url = new URL(raw);
2176
+ if (url.protocol !== "https:") return false;
2177
+ const host = url.hostname.toLowerCase();
2178
+ if (provider.allowedHosts.includes(host)) return true;
2179
+ return provider.allowedSuffixes.some((suffix) => host.endsWith(suffix));
2180
+ } catch {
2181
+ return false;
2182
+ }
2183
+ }
2184
+ //#endregion
2185
+ //#region src/auth-routes.ts
2186
+ const PI_LOGIN_AUTH_STATUS_PATH = "/plugins/dsh-oauth-login/auth/status";
2187
+ const PI_LOGIN_AUTH_LOGIN_PATH = "/plugins/dsh-oauth-login/auth/login";
2188
+ const PI_LOGIN_AUTH_CANCEL_PATH = "/plugins/dsh-oauth-login/auth/cancel";
2189
+ const PI_LOGIN_AUTH_COMPLETE_PATH = "/plugins/dsh-oauth-login/auth/complete";
2190
+ const PI_LOGIN_AUTH_LOGOUT_PATH = "/plugins/dsh-oauth-login/auth/logout";
2191
+ /** Preserve the Pi prompt discriminator on the browser wire contract. */
2192
+ function loginInputChallenge(prompt) {
2193
+ return {
2194
+ type: prompt.type,
2195
+ message: prompt.message,
2196
+ ..."placeholder" in prompt && prompt.placeholder !== void 0 ? { placeholder: prompt.placeholder } : {}
2197
+ };
2198
+ }
2199
+ /** Keep the browser authorization URL available while Pi also waits for a manual callback. */
2200
+ function mergeLoginChallenge(previous, next) {
2201
+ return {
2202
+ ...next,
2203
+ ...next.url === void 0 && previous?.url !== void 0 ? { url: previous.url } : {},
2204
+ ...next.userCode === void 0 && previous?.userCode !== void 0 ? { userCode: previous.userCode } : {}
2205
+ };
2206
+ }
2207
+ function waitForPromptAbort(prompt) {
2208
+ const signal = prompt.signal;
2209
+ if (signal === void 0) return new Promise(() => {});
2210
+ if (signal.aborted) return Promise.reject(signal.reason);
2211
+ return new Promise((_resolve, reject) => {
2212
+ signal.addEventListener("abort", () => {
2213
+ reject(signal.reason);
2214
+ }, { once: true });
2215
+ });
2216
+ }
2217
+ function answerSelectPrompt(prompt) {
2218
+ if (prompt.type === "select") {
2219
+ const oauth = prompt.options.find((option) => option.id === "oauth" || option.id.includes("oauth"));
2220
+ const browser = prompt.options.find((option) => option.id.includes("browser"));
2221
+ return oauth?.id ?? browser?.id ?? prompt.options[0]?.id ?? "oauth";
2222
+ }
2223
+ }
2224
+ var ProviderAuth = class {
2225
+ spec;
2226
+ session;
2227
+ state = { status: "signed-out" };
2228
+ operation;
2229
+ nextOperationId = 0;
2230
+ challenge;
2231
+ challengeWaiters = [];
2232
+ pendingInput;
2233
+ constructor(spec, session) {
2234
+ this.spec = spec;
2235
+ this.session = session;
2236
+ }
2237
+ async snapshot() {
2238
+ if (this.operation !== void 0) return this.state;
2239
+ if (this.state.status === "error") return this.state;
2240
+ return this.readStored();
2241
+ }
2242
+ async signIn() {
2243
+ if (this.operation === void 0) this.start();
2244
+ if (this.challenge !== void 0) return this.challenge;
2245
+ return new Promise((resolve, reject) => {
2246
+ this.challengeWaiters.push({
2247
+ resolve,
2248
+ reject
2249
+ });
2250
+ });
2251
+ }
2252
+ async signOut() {
2253
+ const revision = this.nextOperationId;
2254
+ const error = /* @__PURE__ */ new Error("Pi login cancelled");
2255
+ this.stop(this.operation, error);
2256
+ await this.session.logout(this.spec.id);
2257
+ if (this.operation === void 0 && this.nextOperationId === revision) {
2258
+ this.state = { status: "signed-out" };
2259
+ this.challenge = void 0;
2260
+ }
2261
+ }
2262
+ async cancel() {
2263
+ const revision = this.nextOperationId;
2264
+ const error = /* @__PURE__ */ new Error("Pi login cancelled");
2265
+ this.stop(this.operation, error);
2266
+ const stored = await this.readStored();
2267
+ if (this.operation === void 0 && this.nextOperationId === revision) {
2268
+ this.state = stored;
2269
+ this.challenge = void 0;
2270
+ }
2271
+ }
2272
+ async submitInput(value) {
2273
+ const pending = this.pendingInput;
2274
+ if (pending === void 0) throw new Error(`${this.spec.displayName} is not waiting for a credential`);
2275
+ const normalized = value.trim();
2276
+ if (normalized.length === 0) throw new Error("credential must not be empty");
2277
+ if (normalized.length > 4096) throw new Error("credential is too long");
2278
+ const operation = this.operation;
2279
+ if (operation === void 0 || operation.id !== pending.operationId) throw new Error(`${this.spec.displayName} login operation is no longer active`);
2280
+ pending.resolve(normalized);
2281
+ await operation.done;
2282
+ if (operation.cancelled !== void 0) throw operation.cancelled;
2283
+ if (operation.failure !== void 0) throw operation.failure;
2284
+ return operation.result ?? this.state;
2285
+ }
2286
+ /** Wait until an in-flight sign-in settles (success or error). No-op if idle. */
2287
+ async waitUntilSettled() {
2288
+ await this.operation?.done;
2289
+ }
2290
+ async dispose() {
2291
+ const error = /* @__PURE__ */ new Error("Pi login plugin disposed");
2292
+ this.stop(this.operation, error);
2293
+ }
2294
+ start() {
2295
+ const cancellation = new AbortController();
2296
+ const id = ++this.nextOperationId;
2297
+ let finish;
2298
+ const operation = {
2299
+ id,
2300
+ cancellation,
2301
+ done: new Promise((resolve) => {
2302
+ finish = resolve;
2303
+ }),
2304
+ finish
2305
+ };
2306
+ this.operation = operation;
2307
+ this.challenge = void 0;
2308
+ this.pendingInput = void 0;
2309
+ this.state = { status: "signing-in" };
2310
+ loginPiProviderSession(this.spec.id, {
2311
+ signal: cancellation.signal,
2312
+ prompt: (prompt) => this.onPrompt(id, prompt),
2313
+ notify: (event) => {
2314
+ this.onEvent(id, event);
2315
+ }
2316
+ }, this.session).then(async () => {
2317
+ if (!this.isCurrent(id)) return;
2318
+ const stored = await this.readStored();
2319
+ if (!this.isCurrent(id)) return;
2320
+ operation.result = stored;
2321
+ this.state = stored;
2322
+ }).catch((error) => {
2323
+ if (!this.isCurrent(id)) return;
2324
+ this.rejectChallenge(error);
2325
+ this.rejectInput(error);
2326
+ operation.failure = new Error(safeMessage(error));
2327
+ this.state = {
2328
+ status: "error",
2329
+ message: operation.failure.message
2330
+ };
2331
+ }).finally(() => {
2332
+ if (this.isCurrent(id)) this.operation = void 0;
2333
+ operation.finish();
2334
+ });
2335
+ }
2336
+ onPrompt(operationId, prompt) {
2337
+ if (!this.isCurrent(operationId)) return Promise.reject(/* @__PURE__ */ new Error("Pi login cancelled"));
2338
+ const selected = answerSelectPrompt(prompt);
2339
+ if (selected !== void 0) return Promise.resolve(selected);
2340
+ if (prompt.type === "text" && this.spec.authType === "oauth") return Promise.resolve("");
2341
+ if (prompt.type === "secret" || prompt.type === "manual_code" || prompt.type === "text") return this.requestInput(operationId, prompt);
2342
+ return waitForPromptAbort(prompt);
2343
+ }
2344
+ requestInput(operationId, prompt) {
2345
+ if (this.pendingInput !== void 0) return Promise.reject(/* @__PURE__ */ new Error(`${this.spec.displayName} already has a pending credential prompt`));
2346
+ const input = loginInputChallenge(prompt);
2347
+ const operation = this.operation;
2348
+ if (operation === void 0 || operation.id !== operationId) return Promise.reject(/* @__PURE__ */ new Error("Pi login cancelled"));
2349
+ const signals = [operation.cancellation.signal, prompt.signal].filter((signal) => signal !== void 0);
2350
+ const wait = new Promise((resolve, reject) => {
2351
+ let settled = false;
2352
+ const onAbort = () => {
2353
+ const signal = signals.find((candidate) => candidate.aborted);
2354
+ settleReject(signal?.reason ?? /* @__PURE__ */ new Error("credential prompt cancelled"));
2355
+ };
2356
+ const cleanup = () => {
2357
+ for (const signal of signals) signal.removeEventListener("abort", onAbort);
2358
+ };
2359
+ const settleResolve = (value) => {
2360
+ if (settled) return;
2361
+ settled = true;
2362
+ cleanup();
2363
+ this.pendingInput = void 0;
2364
+ resolve(value);
2365
+ };
2366
+ const settleReject = (error) => {
2367
+ if (settled) return;
2368
+ settled = true;
2369
+ cleanup();
2370
+ this.pendingInput = void 0;
2371
+ reject(error);
2372
+ };
2373
+ this.pendingInput = {
2374
+ operationId,
2375
+ resolve: settleResolve,
2376
+ reject: settleReject
2377
+ };
2378
+ for (const signal of signals) signal.addEventListener("abort", onAbort, { once: true });
2379
+ if (signals.some((signal) => signal.aborted)) onAbort();
2380
+ });
2381
+ this.acceptChallenge({
2382
+ provider: this.spec.id,
2383
+ kind: "input",
2384
+ ...this.spec.loginUrl === void 0 ? {} : { url: this.spec.loginUrl },
2385
+ input
2386
+ });
2387
+ return wait;
2388
+ }
2389
+ onEvent(operationId, event) {
2390
+ if (!this.isCurrent(operationId)) return;
2391
+ if (event.type === "device_code") {
2392
+ this.acceptChallenge({
2393
+ provider: this.spec.id,
2394
+ kind: "browser",
2395
+ url: event.verificationUri,
2396
+ ...event.userCode.length > 0 ? { userCode: event.userCode } : {}
2397
+ });
2398
+ return;
2399
+ }
2400
+ if (event.type === "auth_url") this.acceptChallenge({
2401
+ provider: this.spec.id,
2402
+ kind: "browser",
2403
+ url: event.url
2404
+ });
2405
+ }
2406
+ acceptChallenge(challenge) {
2407
+ if (challenge.url !== void 0 && !isSafeAuthUrl(challenge.url, this.spec)) {
2408
+ const error = /* @__PURE__ */ new Error(`${this.spec.id} returned an authorization URL outside its official hosts`);
2409
+ this.stop(this.operation, error);
2410
+ return;
2411
+ }
2412
+ const merged = mergeLoginChallenge(this.challenge, challenge);
2413
+ this.challenge = merged;
2414
+ this.state = {
2415
+ status: "signing-in",
2416
+ kind: merged.kind,
2417
+ ...merged.url === void 0 ? {} : { url: merged.url },
2418
+ ...merged.userCode === void 0 ? {} : { userCode: merged.userCode },
2419
+ ...merged.input === void 0 ? {} : { input: merged.input }
2420
+ };
2421
+ for (const waiter of this.challengeWaiters.splice(0)) waiter.resolve(merged);
2422
+ }
2423
+ async readStored() {
2424
+ const [stored] = await piLoginStatus(this.session.store, this.spec.id);
2425
+ if (stored === void 0 || !stored.authenticated) return { status: "signed-out" };
2426
+ return {
2427
+ status: "signed-in",
2428
+ models: this.session.visibleModels(this.spec.id).map((model) => model.id),
2429
+ ...stored.expiresAt === void 0 || Number.isNaN(stored.expiresAt.valueOf()) ? {} : { expiresAt: stored.expiresAt.toISOString() }
2430
+ };
2431
+ }
2432
+ rejectChallenge(error) {
2433
+ for (const waiter of this.challengeWaiters.splice(0)) waiter.reject(error);
2434
+ }
2435
+ rejectInput(error) {
2436
+ this.pendingInput?.reject(error);
2437
+ }
2438
+ isCurrent(operationId) {
2439
+ return this.operation?.id === operationId;
2440
+ }
2441
+ stop(operation, error) {
2442
+ if (operation === void 0 || this.operation?.id !== operation.id) return;
2443
+ this.operation = void 0;
2444
+ operation.cancelled = error;
2445
+ this.rejectChallenge(error);
2446
+ this.rejectInput(error);
2447
+ operation.cancellation.abort(error);
2448
+ operation.finish();
2449
+ }
2450
+ };
2451
+ var PiLoginWebAuth = class {
2452
+ session;
2453
+ byId = /* @__PURE__ */ new Map();
2454
+ constructor(session) {
2455
+ this.session = session;
2456
+ for (const spec of PI_LOGIN_PROVIDERS) this.byId.set(spec.id, new ProviderAuth(spec, session));
2457
+ }
2458
+ slot(id) {
2459
+ const slot = this.byId.get(id);
2460
+ if (slot === void 0) throw new Error(`dsh-oauth-login: unknown provider "${id}"`);
2461
+ return slot;
2462
+ }
2463
+ async status() {
2464
+ await this.session.refreshStoredGrants();
2465
+ await this.session.openRouter.syncAuthentication();
2466
+ const out = [];
2467
+ for (const spec of PI_LOGIN_PROVIDERS) out.push({
2468
+ id: spec.id,
2469
+ route: spec.route,
2470
+ displayName: spec.displayName,
2471
+ shortName: spec.shortName,
2472
+ authType: spec.authType,
2473
+ account: await this.slot(spec.id).snapshot()
2474
+ });
2475
+ return out;
2476
+ }
2477
+ async signIn(id) {
2478
+ requirePiLoginProvider(id);
2479
+ return this.slot(id).signIn();
2480
+ }
2481
+ async signOut(id) {
2482
+ requirePiLoginProvider(id);
2483
+ await this.slot(id).signOut();
2484
+ }
2485
+ async cancel(id) {
2486
+ requirePiLoginProvider(id);
2487
+ await this.slot(id).cancel();
2488
+ }
2489
+ async submitInput(id, value) {
2490
+ requirePiLoginProvider(id);
2491
+ return this.slot(id).submitInput(value);
2492
+ }
2493
+ /** Wait until the named provider's in-flight sign-in settles. */
2494
+ async waitUntilSettled(id) {
2495
+ requirePiLoginProvider(id);
2496
+ await this.slot(id).waitUntilSettled();
2497
+ }
2498
+ async dispose() {
2499
+ await Promise.all([...this.byId.values()].map((slot) => slot.dispose()));
2500
+ }
2501
+ };
2502
+ function trustedRequest(req) {
2503
+ const remote = req.socket.remoteAddress;
2504
+ if (remote !== "127.0.0.1" && remote !== "::1" && remote !== "::ffff:127.0.0.1") return false;
2505
+ if (req.headers["sec-fetch-site"] === "cross-site") return false;
2506
+ const host = req.headers.host;
2507
+ if (host === void 0) return false;
2508
+ const origin = req.headers.origin;
2509
+ if (origin === void 0) return true;
2510
+ try {
2511
+ return new URL(origin).host === new URL(`http://${host}`).host;
2512
+ } catch {
2513
+ return false;
2514
+ }
2515
+ }
2516
+ function trustedProxyRequest(req) {
2517
+ if (!trustedRequest(req)) return false;
2518
+ try {
2519
+ const host = new URL(`http://${req.headers.host}`).hostname;
2520
+ return host === "localhost" || host === "127.0.0.1" || host === "[::1]";
2521
+ } catch {
2522
+ return false;
2523
+ }
2524
+ }
2525
+ function rejectWebRequest(ctx, req, res, proxy = false) {
2526
+ const rejection = ctx.connection.requestRejection(req);
2527
+ if (rejection !== void 0) {
2528
+ json(res, rejection, { error: rejection === 401 ? "unauthorized" : "forbidden" });
2529
+ return true;
2530
+ }
2531
+ if (proxy ? !trustedProxyRequest(req) : !trustedRequest(req)) {
2532
+ json(res, 403, { error: "forbidden" });
2533
+ return true;
2534
+ }
2535
+ return false;
2536
+ }
2537
+ function json(res, status, value) {
2538
+ res.writeHead(status, {
2539
+ "content-type": "application/json; charset=utf-8",
2540
+ "cache-control": "no-store",
2541
+ "x-content-type-options": "nosniff"
2542
+ });
2543
+ res.end(JSON.stringify(value));
2544
+ }
2545
+ async function readJson(req) {
2546
+ const chunks = [];
2547
+ let size = 0;
2548
+ for await (const chunk of req) {
2549
+ size += chunk.length;
2550
+ if (size > 4096) throw new Error("request body too large");
2551
+ chunks.push(chunk);
2552
+ }
2553
+ if (chunks.length === 0) return {};
2554
+ return JSON.parse(Buffer.concat(chunks).toString("utf8"));
2555
+ }
2556
+ function providerIdFrom(value) {
2557
+ if (typeof value !== "object" || value === null || !("provider" in value) || typeof value.provider !== "string") throw new Error("expected { \"provider\": \"<id>\" }");
2558
+ return requirePiLoginProvider(value.provider).id;
2559
+ }
2560
+ function providerInputFrom(value) {
2561
+ if (typeof value !== "object" || value === null || !("provider" in value) || typeof value.provider !== "string" || !("value" in value) || typeof value.value !== "string") throw new Error("expected { \"provider\": \"<id>\", \"value\": \"<credential>\" }");
2562
+ return {
2563
+ providerId: requirePiLoginProvider(value.provider).id,
2564
+ value: value.value
2565
+ };
2566
+ }
2567
+ function registerPiLoginAuthRoutes(ctx, session, options = {}) {
2568
+ const auth = new PiLoginWebAuth(session);
2569
+ const notifyAuthChanged = async () => {
2570
+ await options.onAuthChanged?.();
2571
+ };
2572
+ ctx.effect(() => {
2573
+ const routes = [
2574
+ ctx.webServer.register({
2575
+ kind: "exact",
2576
+ path: PROXY_SETTINGS_PATH,
2577
+ handler: async (req, res) => {
2578
+ if (rejectWebRequest(ctx, req, res, true)) return;
2579
+ if (req.method === "GET") try {
2580
+ return json(res, 200, await session.proxy.settings.read());
2581
+ } catch {
2582
+ return json(res, 503, { error: "Network settings could not be read" });
2583
+ }
2584
+ if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
2585
+ if (req.headers["content-type"]?.split(";")[0]?.trim() !== "application/json") return json(res, 415, { error: "application/json required" });
2586
+ let settings;
2587
+ try {
2588
+ settings = parseProxySettings(await readJson(req));
2589
+ } catch {
2590
+ return json(res, 400, { error: "Invalid network settings: use HTTP(S) proxy URLs without credentials or paths" });
2591
+ }
2592
+ try {
2593
+ json(res, 200, await session.proxy.save(settings));
2594
+ } catch (error) {
2595
+ if (error instanceof ProxySettingsConflict) return json(res, 409, { error: error.message });
2596
+ json(res, 503, { error: "Network settings could not be saved" });
2597
+ }
2598
+ }
2599
+ }),
2600
+ ctx.webServer.register({
2601
+ kind: "exact",
2602
+ path: OPENROUTER_CATALOG_PATH,
2603
+ handler: async (req, res) => {
2604
+ if (rejectWebRequest(ctx, req, res)) return;
2605
+ if (req.method !== "GET") return json(res, 405, { error: "method not allowed" });
2606
+ try {
2607
+ await session.openRouter.syncAuthentication();
2608
+ json(res, 200, session.openRouter.snapshot());
2609
+ } catch {
2610
+ json(res, 503, { error: "OpenRouter model catalog unavailable" });
2611
+ }
2612
+ }
2613
+ }),
2614
+ ctx.webServer.register({
2615
+ kind: "exact",
2616
+ path: OPENROUTER_REFRESH_PATH,
2617
+ handler: async (req, res) => {
2618
+ if (rejectWebRequest(ctx, req, res)) return;
2619
+ if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
2620
+ try {
2621
+ await session.openRouter.syncAuthentication();
2622
+ if (!session.openRouter.snapshot().connected) return json(res, 409, { error: "Sign in to OpenRouter first" });
2623
+ await session.openRouter.refresh(true);
2624
+ json(res, 200, session.openRouter.snapshot());
2625
+ } catch {
2626
+ json(res, 503, { error: "OpenRouter model catalog unavailable" });
2627
+ }
2628
+ }
2629
+ }),
2630
+ ctx.webServer.register({
2631
+ kind: "exact",
2632
+ path: PI_LOGIN_AUTH_STATUS_PATH,
2633
+ handler: async (req, res) => {
2634
+ if (rejectWebRequest(ctx, req, res)) return;
2635
+ if (req.method !== "GET") return json(res, 405, { error: "method not allowed" });
2636
+ json(res, 200, await auth.status());
2637
+ }
2638
+ }),
2639
+ ctx.webServer.register({
2640
+ kind: "exact",
2641
+ path: PI_LOGIN_AUTH_LOGIN_PATH,
2642
+ handler: async (req, res) => {
2643
+ if (rejectWebRequest(ctx, req, res)) return;
2644
+ if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
2645
+ try {
2646
+ const challenge = await auth.signIn(providerIdFrom(await readJson(req)));
2647
+ if (challenge.kind === "browser") auth.waitUntilSettled(challenge.provider).then(async () => {
2648
+ await notifyAuthChanged();
2649
+ });
2650
+ json(res, 200, challenge);
2651
+ } catch (error) {
2652
+ json(res, 500, { error: safeMessage(error) });
2653
+ }
2654
+ }
2655
+ }),
2656
+ ctx.webServer.register({
2657
+ kind: "exact",
2658
+ path: PI_LOGIN_AUTH_CANCEL_PATH,
2659
+ handler: async (req, res) => {
2660
+ if (rejectWebRequest(ctx, req, res)) return;
2661
+ if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
2662
+ try {
2663
+ await auth.cancel(providerIdFrom(await readJson(req)));
2664
+ json(res, 200, { ok: true });
2665
+ } catch (error) {
2666
+ json(res, 500, { error: safeMessage(error) });
2667
+ }
2668
+ }
2669
+ }),
2670
+ ctx.webServer.register({
2671
+ kind: "exact",
2672
+ path: PI_LOGIN_AUTH_COMPLETE_PATH,
2673
+ handler: async (req, res) => {
2674
+ if (rejectWebRequest(ctx, req, res)) return;
2675
+ if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
2676
+ try {
2677
+ const submission = providerInputFrom(await readJson(req));
2678
+ const account = await auth.submitInput(submission.providerId, submission.value);
2679
+ await notifyAuthChanged();
2680
+ json(res, 200, {
2681
+ ok: true,
2682
+ account
2683
+ });
2684
+ } catch (error) {
2685
+ json(res, 500, { error: safeMessage(error) });
2686
+ }
2687
+ }
2688
+ }),
2689
+ ctx.webServer.register({
2690
+ kind: "exact",
2691
+ path: PI_LOGIN_AUTH_LOGOUT_PATH,
2692
+ handler: async (req, res) => {
2693
+ if (rejectWebRequest(ctx, req, res)) return;
2694
+ if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
2695
+ try {
2696
+ await auth.signOut(providerIdFrom(await readJson(req)));
2697
+ await notifyAuthChanged();
2698
+ json(res, 200, { ok: true });
2699
+ } catch (error) {
2700
+ json(res, 500, { error: safeMessage(error) });
2701
+ }
2702
+ }
2703
+ })
2704
+ ];
2705
+ return async () => {
2706
+ for (const dispose of routes) dispose();
2707
+ await auth.dispose();
2708
+ };
2709
+ }, "dsh-oauth-login: Web OAuth routes");
2710
+ }
2711
+ //#endregion
2712
+ //#region src/oauth-refresh.ts
2713
+ /**
2714
+ * When to spend a refresh_token. Chat already refreshes via Models.getAuth;
2715
+ * this helper is for status / boot / the host timer so the store does not sit
2716
+ * on a stale expires stamp.
2717
+ */
2718
+ /** Refresh when the stored access stamp is inside this window (Pi already skews expires by 5 min). */
2719
+ const OAUTH_REFRESH_SOON_MS = 9e5;
2720
+ /** Host timer. Access lives ~6h; this is just how often we look. */
2721
+ const OAUTH_REFRESH_POLL_MS = 9e5;
2722
+ const lastAttempt = /* @__PURE__ */ new Map();
2723
+ function grantNeedsRefresh(expires, now = Date.now()) {
2724
+ return now >= expires - OAUTH_REFRESH_SOON_MS;
2725
+ }
2726
+ function refreshAttemptKey(storeId, providerId) {
2727
+ return `${storeId}\0${providerId}`;
2728
+ }
2729
+ function refreshOnCooldown(key, now = Date.now()) {
2730
+ const previous = lastAttempt.get(key);
2731
+ return previous !== void 0 && now - previous < 3e5;
2732
+ }
2733
+ function markRefreshAttempt(key, now = Date.now()) {
2734
+ lastAttempt.set(key, now);
2735
+ }
2736
+ async function refreshGrant(getAuth, providerId) {
2737
+ try {
2738
+ await getAuth(providerId);
2739
+ return "ok";
2740
+ } catch {
2741
+ return "failed";
2742
+ }
2743
+ }
2744
+ //#endregion
2745
+ //#region src/openrouter-catalog.ts
2746
+ /** One Host-owned cache/refresh loop. It never receives an OAuth token. */
2747
+ const OPENROUTER_MODELS_URL = "https://openrouter.ai/api/v1/models";
2748
+ const OPENROUTER_CACHE_FILENAME = ".dsh-oauth-openrouter-models.json";
2749
+ const MAX_BYTES = 12582912;
2750
+ const FETCH_TIMEOUT_MS = 15e3;
2751
+ var OpenRouterCatalog = class {
2752
+ options;
2753
+ entries;
2754
+ protectedIds;
2755
+ connected = false;
2756
+ disposed = false;
2757
+ source = "builtin";
2758
+ lastUpdatedAt = null;
2759
+ lastAttemptAt = null;
2760
+ nextRefreshAt = null;
2761
+ error = null;
2762
+ hydration;
2763
+ pending;
2764
+ controller;
2765
+ timer;
2766
+ generation = 0;
2767
+ listeners = /* @__PURE__ */ new Set();
2768
+ now;
2769
+ constructor(options) {
2770
+ this.options = options;
2771
+ this.now = options.now ?? Date.now;
2772
+ this.protectedIds = new Set(options.initiallyProtectedIds ?? []);
2773
+ }
2774
+ models() {
2775
+ return this.entries;
2776
+ }
2777
+ model(id) {
2778
+ return this.entries?.find((model) => model.id === id);
2779
+ }
2780
+ protects(id) {
2781
+ return id.endsWith(":free") || id === "openrouter/free" || this.protectedIds.has(id);
2782
+ }
2783
+ subscribe(listener) {
2784
+ this.listeners.add(listener);
2785
+ return () => {
2786
+ this.listeners.delete(listener);
2787
+ };
2788
+ }
2789
+ changed() {
2790
+ for (const listener of this.listeners) try {
2791
+ listener();
2792
+ } catch {}
2793
+ }
2794
+ snapshot() {
2795
+ return {
2796
+ version: 1,
2797
+ connected: this.connected,
2798
+ refreshing: this.pending !== void 0,
2799
+ source: this.source,
2800
+ lastUpdatedAt: this.lastUpdatedAt,
2801
+ lastAttemptAt: this.lastAttemptAt,
2802
+ nextRefreshAt: this.nextRefreshAt,
2803
+ retryAt: this.lastAttemptAt === null ? null : this.lastAttemptAt + OPENROUTER_COOLDOWN_MS,
2804
+ stale: this.error !== null || this.lastUpdatedAt === null || this.now() - this.lastUpdatedAt >= 9e5,
2805
+ error: this.error,
2806
+ models: this.connected ? (this.entries ?? []).map((model) => ({
2807
+ ...model,
2808
+ freeOnly: this.protects(model.id)
2809
+ })) : []
2810
+ };
2811
+ }
2812
+ hydrate() {
2813
+ this.hydration ??= this.readCache();
2814
+ return this.hydration;
2815
+ }
2816
+ async readCache() {
2817
+ try {
2818
+ if ((await stat(this.options.filename)).size > MAX_BYTES) throw new Error("cache too large");
2819
+ const raw = objectValue(JSON.parse(await readFile(this.options.filename, "utf8")));
2820
+ if (raw?.version !== 1 || typeof raw.lastUpdatedAt !== "number" || !Number.isFinite(raw.lastUpdatedAt) || raw.lastUpdatedAt <= 0 || raw.lastUpdatedAt > this.now() + 6e4) throw new Error("invalid cache");
2821
+ const entries = normalizeOpenRouterModels(raw.data);
2822
+ if (this.disposed) return;
2823
+ this.entries = entries;
2824
+ this.lastUpdatedAt = raw.lastUpdatedAt;
2825
+ this.source = "cache";
2826
+ if (Array.isArray(raw.protectedIds)) {
2827
+ for (const id of raw.protectedIds) if (validModelId(id)) this.protectedIds.add(id);
2828
+ }
2829
+ this.rememberFree(entries);
2830
+ this.changed();
2831
+ } catch (error) {
2832
+ if (error?.code !== "ENOENT") this.error = "cache";
2833
+ }
2834
+ }
2835
+ rememberFree(entries) {
2836
+ for (const model of entries) if (model.priceStatus === "free") this.protectedIds.add(model.id);
2837
+ }
2838
+ /** Startup reads disk first; only a newly completed login forces a refresh. */
2839
+ async syncAuthentication(reason = "check") {
2840
+ await this.hydrate();
2841
+ const generation = this.generation;
2842
+ const authenticated = await this.options.isAuthenticated();
2843
+ if (this.disposed || generation !== this.generation) return;
2844
+ if (!authenticated) {
2845
+ this.disconnect();
2846
+ return;
2847
+ }
2848
+ this.connected = true;
2849
+ if (reason === "login" || this.snapshot().stale) this.refresh(reason === "login");
2850
+ else if (this.timer === void 0 && this.pending === void 0) this.schedule(Math.max(1, OPENROUTER_REFRESH_MS - (this.now() - this.lastUpdatedAt)));
2851
+ }
2852
+ /** Coalesced across routes/windows; even forced refreshes honor 60s cooldown. */
2853
+ refresh(force = false) {
2854
+ if (this.pending !== void 0) return this.pending;
2855
+ if (!this.connected || this.disposed) return Promise.resolve();
2856
+ if (!force && !this.snapshot().stale) return Promise.resolve();
2857
+ if (this.lastAttemptAt !== null && this.now() - this.lastAttemptAt < 6e4) return Promise.resolve();
2858
+ const generation = this.generation;
2859
+ this.lastAttemptAt = this.now();
2860
+ this.controller = new AbortController();
2861
+ const signal = AbortSignal.any([this.controller.signal, AbortSignal.timeout(FETCH_TIMEOUT_MS)]);
2862
+ const pending = this.fetchCatalog(signal, generation).finally(() => {
2863
+ if (this.pending !== pending) return;
2864
+ this.pending = void 0;
2865
+ this.controller = void 0;
2866
+ if (this.connected && !this.disposed) this.schedule(OPENROUTER_REFRESH_MS);
2867
+ });
2868
+ this.pending = pending;
2869
+ return pending;
2870
+ }
2871
+ async fetchCatalog(signal, generation) {
2872
+ try {
2873
+ if (!await this.options.isAuthenticated()) {
2874
+ this.disconnect();
2875
+ return;
2876
+ }
2877
+ await this.options.beforeFetch?.();
2878
+ signal.throwIfAborted();
2879
+ const response = await (this.options.fetch ?? globalThis.fetch)(OPENROUTER_MODELS_URL, {
2880
+ method: "GET",
2881
+ headers: { accept: "application/json" },
2882
+ signal,
2883
+ redirect: "error",
2884
+ credentials: "omit"
2885
+ });
2886
+ if (!response.ok || response.body === null) throw new Error("catalog unavailable");
2887
+ const reader = response.body.getReader();
2888
+ const chunks = [];
2889
+ let size = 0;
2890
+ try {
2891
+ while (true) {
2892
+ const { done, value } = await reader.read();
2893
+ if (done) break;
2894
+ size += value.byteLength;
2895
+ if (size > MAX_BYTES) throw new Error("catalog too large");
2896
+ chunks.push(value);
2897
+ }
2898
+ } finally {
2899
+ await reader.cancel().catch(() => void 0);
2900
+ reader.releaseLock();
2901
+ }
2902
+ const raw = objectValue(JSON.parse(Buffer.concat(chunks).toString("utf8")));
2903
+ const links = objectValue(raw?.links);
2904
+ if (raw === void 0 || links?.next != null || !Array.isArray(raw.data) || typeof raw.total_count === "number" && raw.total_count > raw.data.length) throw new Error("partial catalog");
2905
+ const entries = normalizeOpenRouterModels(raw.data);
2906
+ if (this.disposed || !this.connected || generation !== this.generation || signal.aborted) return;
2907
+ const fetchedAt = this.now();
2908
+ const protectedIds = new Set(this.protectedIds);
2909
+ for (const model of entries) if (model.priceStatus === "free") protectedIds.add(model.id);
2910
+ try {
2911
+ await withFileLock(this.options.filename, async () => {
2912
+ try {
2913
+ const cached = objectValue(JSON.parse(await readFile(this.options.filename, "utf8")));
2914
+ if (Array.isArray(cached?.protectedIds)) {
2915
+ for (const id of cached.protectedIds) if (validModelId(id)) protectedIds.add(id);
2916
+ }
2917
+ } catch {}
2918
+ await writeFileAtomic(this.options.filename, JSON.stringify({
2919
+ version: 1,
2920
+ lastUpdatedAt: fetchedAt,
2921
+ protectedIds: [...protectedIds],
2922
+ data: raw.data
2923
+ }) + "\n", {
2924
+ mode: 384,
2925
+ dirMode: 448
2926
+ });
2927
+ });
2928
+ } catch {
2929
+ this.error = "save";
2930
+ return;
2931
+ }
2932
+ if (this.disposed || !this.connected || generation !== this.generation || signal.aborted) return;
2933
+ this.entries = entries;
2934
+ this.protectedIds = protectedIds;
2935
+ this.lastUpdatedAt = fetchedAt;
2936
+ this.source = "live";
2937
+ this.error = null;
2938
+ this.changed();
2939
+ } catch {
2940
+ if (!this.disposed && this.connected && generation === this.generation) this.error = "fetch";
2941
+ }
2942
+ }
2943
+ schedule(delay) {
2944
+ clearTimeout(this.timer);
2945
+ this.nextRefreshAt = this.now() + delay;
2946
+ this.timer = setTimeout(() => {
2947
+ this.timer = void 0;
2948
+ this.nextRefreshAt = null;
2949
+ this.syncAuthentication().then(() => this.refresh()).catch(() => {
2950
+ if (this.connected && !this.disposed) this.schedule(OPENROUTER_REFRESH_MS);
2951
+ });
2952
+ }, delay);
2953
+ this.timer.unref?.();
2954
+ }
2955
+ disconnect() {
2956
+ this.generation += 1;
2957
+ this.connected = false;
2958
+ clearTimeout(this.timer);
2959
+ this.timer = void 0;
2960
+ this.nextRefreshAt = null;
2961
+ this.controller?.abort();
2962
+ this.controller = void 0;
2963
+ this.pending = void 0;
2964
+ }
2965
+ dispose() {
2966
+ this.disposed = true;
2967
+ this.disconnect();
2968
+ this.listeners.clear();
2969
+ }
2970
+ };
2971
+ //#endregion
2972
+ //#region src/session.ts
2973
+ /** Shared OAuth store + catalog for the host plugin and CLI. */
2974
+ var PiLoginSession = class {
2975
+ store;
2976
+ models;
2977
+ native;
2978
+ openRouter;
2979
+ proxy;
2980
+ constructor(store = new PiLoginCredentialStore(), native = DEFAULT_NATIVE_TOOL_POLICY, catalogOptions = {}, proxyDiscovery = {}) {
2981
+ this.store = store;
2982
+ this.native = native;
2983
+ this.proxy = new OAuthProxyTransport(join(dirname(store.filename), PROXY_SETTINGS_FILENAME), proxyDiscovery);
2984
+ this.models = createModels({ credentials: store });
2985
+ for (const provider of allCatalogProviders()) this.models.setProvider(provider);
2986
+ this.openRouter = new OpenRouterCatalog({
2987
+ filename: join(dirname(store.filename), OPENROUTER_CACHE_FILENAME),
2988
+ isAuthenticated: async () => (await store.list()).some((item) => item.providerId === "openrouter"),
2989
+ beforeFetch: () => this.ensureTransport(),
2990
+ fetch: (input, init) => this.proxy.run(() => fetch(input, init)),
2991
+ initiallyProtectedIds: harnessModels(this.spec("openrouter")).filter((model) => model.cost.input === 0 && model.cost.output === 0).map((model) => model.id),
2992
+ ...catalogOptions
2993
+ });
2994
+ }
2995
+ ensureTransport() {
2996
+ return this.proxy.initialize();
2997
+ }
2998
+ spec(id) {
2999
+ const spec = piLoginProvider(id);
3000
+ if (spec === void 0) throw new Error(`dsh-oauth-login: unknown provider "${id}"`);
3001
+ return spec;
3002
+ }
3003
+ provider(id) {
3004
+ return harnessProvider(this.spec(id), this.native, this.openRouter);
3005
+ }
3006
+ visibleModels(id) {
3007
+ return this.provider(id).getModels();
3008
+ }
3009
+ /**
3010
+ * Harness routes that currently hold a stored OAuth grant.
3011
+ * Model pickers should only advertise these — logging out must drop the route.
3012
+ */
3013
+ async authenticatedRoutes() {
3014
+ const signedIn = new Set((await this.store.list()).map((item) => item.providerId));
3015
+ return PI_LOGIN_PROVIDERS.filter((provider) => signedIn.has(provider.id)).map((provider) => provider.route);
3016
+ }
3017
+ async logout(id) {
3018
+ await this.store.delete(id);
3019
+ if (id === "openrouter") this.openRouter.disconnect();
3020
+ }
3021
+ /**
3022
+ * Renew access tokens that are expired or close to expiry.
3023
+ * Failures stay in the store; the next poll or chat retries.
3024
+ */
3025
+ async refreshStoredGrants(now = Date.now()) {
3026
+ await this.ensureTransport();
3027
+ for (const { providerId } of await this.store.list()) {
3028
+ const credential = await this.store.read(providerId);
3029
+ if (credential?.type !== "oauth" || !grantNeedsRefresh(credential.expires, now)) continue;
3030
+ const key = refreshAttemptKey(this.store.filename, providerId);
3031
+ if (refreshOnCooldown(key, now)) continue;
3032
+ markRefreshAttempt(key, now);
3033
+ await this.proxy.run(() => refreshGrant((id) => this.models.getAuth(id), providerId));
3034
+ }
3035
+ }
3036
+ };
3037
+ //#endregion
3038
+ //#region src/plugin-config.ts
3039
+ /** Provider-owned knobs published on the `llm-oauth-login` row. */
3040
+ const Config = z.object({
3041
+ streamIdleTimeoutMs: z.number().min(1).default(PI_LOGIN_STREAM_IDLE_TIMEOUT_MS),
3042
+ retryPolicy: RetryPolicySchema,
3043
+ nativeTools: z.boolean().default(true),
3044
+ nativeImage: z.boolean().default(true)
3045
+ });
3046
+ //#endregion
3047
+ //#region src/index.ts
3048
+ const name = "llm-oauth-login";
3049
+ const inject = ["llm"];
3050
+ /**
3051
+ * Publish only the routes that currently hold a plugin-owned credential.
3052
+ * Logging out must drop that route from the model picker immediately.
3053
+ */
3054
+ async function syncAuthenticatedRoutes(session, registration) {
3055
+ registration.replace(await session.authenticatedRoutes());
3056
+ }
3057
+ function apply(ctx, config) {
3058
+ console.log("[my-plugins/dsh-oauth-login] loaded");
3059
+ const native = {
3060
+ enabled: config.nativeTools !== false,
3061
+ image: config.nativeImage !== false
3062
+ };
3063
+ const session = new PiLoginSession(new PiLoginCredentialStore(), native);
3064
+ const registration = ctx.llm.registerAdapter(piLoginRoutes(), createPiLoginAdapter(session, () => ctx.get("attachments"), {
3065
+ streamIdleTimeoutMs: config.streamIdleTimeoutMs,
3066
+ retryPolicy: config.retryPolicy
3067
+ }));
3068
+ const refreshRoutes = () => syncAuthenticatedRoutes(session, registration);
3069
+ refreshRoutes();
3070
+ ctx.effect(() => {
3071
+ const stop = session.openRouter.subscribe(() => {
3072
+ refreshRoutes().catch(() => {});
3073
+ });
3074
+ session.openRouter.syncAuthentication().catch(() => {
3075
+ console.warn("[dsh-oauth-login] OpenRouter catalog initialization failed; keeping built-in models");
3076
+ });
3077
+ return async () => {
3078
+ stop();
3079
+ session.openRouter.dispose();
3080
+ await session.proxy.dispose();
3081
+ };
3082
+ }, "dsh-oauth-login: OpenRouter catalog");
3083
+ ctx.effect(() => {
3084
+ const timer = setInterval(() => {
3085
+ session.refreshStoredGrants().catch(() => {});
3086
+ }, OAUTH_REFRESH_POLL_MS);
3087
+ session.refreshStoredGrants().catch(() => {});
3088
+ return () => clearInterval(timer);
3089
+ }, "dsh-oauth-login: refresh oauth grants");
3090
+ ctx.inject(["webServer", "connection"], (webCtx) => {
3091
+ registerPiLoginAuthRoutes(webCtx, session, { onAuthChanged: refreshRoutes });
3092
+ });
3093
+ ctx.inject(["systemPrompt"], (promptCtx) => {
3094
+ promptCtx.on("system-prompt/assemble", async (_assembly, context, next) => {
3095
+ const assembled = await next();
3096
+ const route = context.agent?.options?.provider;
3097
+ const modelId = context.agent?.options?.model;
3098
+ const spec = route === void 0 ? void 0 : piLoginProviderByRoute(route);
3099
+ const plan = nativePlanForRoute(route, spec === void 0 || modelId === void 0 ? void 0 : session.visibleModels(spec.id).find((model) => model.id === modelId)?.api, session.native);
3100
+ return plan === void 0 ? assembled : maskDshWebAssembly(assembled, plan);
3101
+ });
3102
+ });
3103
+ }
3104
+ //#endregion
3105
+ export { Config, DEFAULT_NATIVE_TOOL_POLICY, LEGACY_PI_LOGIN_AUTH_FILENAME, OAUTH_REFRESH_POLL_MS, OAUTH_REFRESH_SOON_MS, OX_ALPHA_DEFAULT_EFFORT, OX_ALPHA_MODEL_ID, PI_LOGIN_AUTH_CANCEL_PATH, PI_LOGIN_AUTH_COMPLETE_PATH, PI_LOGIN_AUTH_FILENAME, PI_LOGIN_AUTH_LOGIN_PATH, PI_LOGIN_AUTH_LOGOUT_PATH, PI_LOGIN_AUTH_STATUS_PATH, PI_LOGIN_BOOT_MARKER, PI_LOGIN_PROVIDERS, PI_LOGIN_ROUTE_PREFIX, PI_LOGIN_STREAM_IDLE_TIMEOUT_MS, PiLoginCredentialStore, PiLoginSession, QUOTA_HINT, RATE_LIMIT_HINT, TRANSIENT_HINT, TRANSIENT_MODEL_CODES, apply, applyNativeToolsToPayload, catalogProvider, collectHostedImagesFromEvent, createPiLoginAdapter, decodeHostedImage, defaultReasoningEffortFor, extraModelsFor, filterHostedServerToolTraces, filterXaiServerToolTraces, grantNeedsRefresh, harnessModels, harnessProvider, hintFailure, hintForCode, inject, injectHostedImages, isHostedSearchReasoningReplay, isHostedServerToolCall, isSafeAuthUrl, isXaiServerXSearchCall, loginPiProvider, loginPiProviderSession, logoutPiProvider, name, nativePlan, nativePlanForRoute, piLoginAuthPath, piLoginProvider, piLoginRoutes, piLoginStatus, preferredModel, prepareNativeToolRequest, registerPiLoginAuthRoutes, safeMessage, sniffImageMediaType, stripAssistantImages, withModelErrorHint };