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/bin.js ADDED
@@ -0,0 +1,2132 @@
1
+ #!/usr/bin/env node
2
+ import { execFile, spawn } from "node:child_process";
3
+ import { realpathSync } from "node:fs";
4
+ import { createInterface } from "node:readline/promises";
5
+ import { fileURLToPath } from "node:url";
6
+ import { LlmError, RetryPolicySchema } from "@deepseek-ai/dsh-llm";
7
+ import { createModels } from "@earendil-works/pi-ai";
8
+ import "@deepseek-ai/dsh-llm-pi-ai";
9
+ import { AsyncLocalStorage } from "node:async_hooks";
10
+ import { basename, dirname, join, resolve } from "node:path";
11
+ import { connect, isIP } from "node:net";
12
+ import { Agent, EnvHttpProxyAgent, ProxyAgent, getGlobalDispatcher, install, setGlobalDispatcher } from "undici";
13
+ import { closeOpenAICodexWebSocketSessions } from "@earendil-works/pi-ai/api/openai-codex-responses";
14
+ import { mkdir, readFile, rm, stat } from "node:fs/promises";
15
+ import { withFileLock, writeFileAtomic } from "@deepseek-ai/dsh-atomic-write";
16
+ import { builtinProviders } from "@earendil-works/pi-ai/providers/all";
17
+ import { resolveDshHome } from "@deepseek-ai/dsh-home-paths";
18
+ import z from "@deepseek-ai/schemastery";
19
+ //#region src/catalog.ts
20
+ const PI_LOGIN_PROVIDERS = [
21
+ {
22
+ id: "openai-codex",
23
+ route: "pi-openai-codex",
24
+ authType: "oauth",
25
+ displayName: "ChatGPT Codex",
26
+ shortName: "Codex",
27
+ blurb: "ChatGPT Plus/Pro Codex. Independent of official `codex login`.",
28
+ blurbZh: "ChatGPT Plus/Pro 的 Codex。和官方 `codex login` 互不影响。",
29
+ allowedHosts: [
30
+ "auth.openai.com",
31
+ "chatgpt.com",
32
+ "www.chatgpt.com"
33
+ ],
34
+ allowedSuffixes: [".openai.com", ".chatgpt.com"],
35
+ preferredModels: [
36
+ "gpt-5.4",
37
+ "gpt-5.3-codex",
38
+ "gpt-5.3-codex-spark"
39
+ ]
40
+ },
41
+ {
42
+ id: "anthropic",
43
+ route: "pi-anthropic",
44
+ authType: "oauth",
45
+ displayName: "Claude Pro/Max",
46
+ shortName: "Claude",
47
+ blurb: "Claude subscription. Independent of official Claude Code login.",
48
+ blurbZh: "Claude 订阅。和官方 Claude Code 登录互不影响。",
49
+ allowedHosts: [
50
+ "claude.ai",
51
+ "www.claude.ai",
52
+ "platform.claude.com"
53
+ ],
54
+ allowedSuffixes: [".anthropic.com", ".claude.ai"],
55
+ preferredModels: [
56
+ "claude-opus-4-6",
57
+ "claude-sonnet-4-6",
58
+ "claude-opus-4-5"
59
+ ]
60
+ },
61
+ {
62
+ id: "xai",
63
+ route: "pi-xai",
64
+ authType: "oauth",
65
+ displayName: "xAI Grok",
66
+ shortName: "Grok",
67
+ blurb: "SuperGrok / X Premium. Independent of official `grok` CLI.",
68
+ blurbZh: "SuperGrok / X Premium。和官方 `grok` CLI 互不影响。",
69
+ allowedHosts: [
70
+ "auth.x.ai",
71
+ "accounts.x.ai",
72
+ "x.ai",
73
+ "www.x.ai"
74
+ ],
75
+ allowedSuffixes: [".x.ai"],
76
+ preferredModels: [
77
+ "grok-4.6",
78
+ "grok-4.5",
79
+ "grok-4.3"
80
+ ]
81
+ },
82
+ {
83
+ id: "github-copilot",
84
+ route: "pi-github-copilot",
85
+ authType: "oauth",
86
+ displayName: "GitHub Copilot",
87
+ shortName: "Copilot",
88
+ blurb: "GitHub Copilot subscription via device code.",
89
+ blurbZh: "GitHub Copilot 订阅,走 device code。",
90
+ allowedHosts: [
91
+ "github.com",
92
+ "www.github.com",
93
+ "api.github.com"
94
+ ],
95
+ allowedSuffixes: [".github.com", ".githubcopilot.com"],
96
+ preferredModels: [
97
+ "gpt-5.4",
98
+ "claude-sonnet-4.6",
99
+ "claude-opus-4.6"
100
+ ]
101
+ },
102
+ {
103
+ id: "openrouter",
104
+ route: "pi-openrouter",
105
+ authType: "oauth",
106
+ displayName: "OpenRouter",
107
+ shortName: "OpenRouter",
108
+ blurb: "OpenRouter OAuth mints a key billed from your OpenRouter credits.",
109
+ blurbZh: "OpenRouter OAuth 会签发一把钥匙,从你的 OpenRouter 余额扣费。",
110
+ allowedHosts: ["openrouter.ai", "www.openrouter.ai"],
111
+ allowedSuffixes: [".openrouter.ai"],
112
+ preferredModels: []
113
+ },
114
+ {
115
+ id: "kimi-coding",
116
+ route: "pi-kimi-coding",
117
+ authType: "oauth",
118
+ displayName: "Kimi For Coding",
119
+ shortName: "Kimi",
120
+ blurb: "Kimi Code subscription.",
121
+ blurbZh: "Kimi Code 订阅。",
122
+ allowedHosts: [
123
+ "auth.kimi.com",
124
+ "kimi.com",
125
+ "www.kimi.com",
126
+ "api.kimi.com"
127
+ ],
128
+ allowedSuffixes: [".kimi.com"],
129
+ preferredModels: ["kimi-for-coding", "k3"]
130
+ },
131
+ {
132
+ id: "zai-coding-cn",
133
+ route: "pi-zai-coding-cn",
134
+ authType: "api_key",
135
+ loginUrl: "https://bigmodel.cn/coding-plan/personal/overview",
136
+ displayName: "智谱 GLM Coding Plan",
137
+ shortName: "GLM",
138
+ blurb: "Zhipu GLM Coding Plan (China) via its official Plan API key.",
139
+ blurbZh: "智谱 GLM Coding Plan 中国区套餐,使用套餐页签发的专用 API Key。",
140
+ allowedHosts: [
141
+ "bigmodel.cn",
142
+ "www.bigmodel.cn",
143
+ "open.bigmodel.cn"
144
+ ],
145
+ allowedSuffixes: [".bigmodel.cn"],
146
+ preferredModels: [
147
+ "glm-5.3-flash",
148
+ "glm-5.3",
149
+ "glm-5.2",
150
+ "glm-5-turbo",
151
+ "glm-5.1",
152
+ "glm-4.7"
153
+ ]
154
+ }
155
+ ];
156
+ function piLoginProvider(id) {
157
+ return PI_LOGIN_PROVIDERS.find((provider) => provider.id === id);
158
+ }
159
+ function requirePiLoginProvider(id) {
160
+ const provider = piLoginProvider(id);
161
+ if (provider === void 0) throw new Error(`dsh-oauth-login: unknown provider "${id}"`);
162
+ return provider;
163
+ }
164
+ const EXTRA_MODELS_BY_PROVIDER = {
165
+ "openai-codex": [{
166
+ id: "gpt-6-astra",
167
+ name: "GPT-6 Astra",
168
+ api: "openai-codex-responses",
169
+ provider: "openai-codex",
170
+ baseUrl: "https://chatgpt.com/backend-api",
171
+ reasoning: true,
172
+ input: ["text", "image"],
173
+ cost: {
174
+ input: 10,
175
+ output: 50,
176
+ cacheRead: 1,
177
+ cacheWrite: 12.5
178
+ },
179
+ contextWindow: 272e3,
180
+ maxTokens: 128e3,
181
+ thinkingLevelMap: {
182
+ off: null,
183
+ minimal: null,
184
+ low: "low",
185
+ medium: "medium",
186
+ high: "high",
187
+ xhigh: "xhigh",
188
+ max: "max"
189
+ }
190
+ }],
191
+ xai: [{
192
+ id: "grok-4.6",
193
+ name: "Grok 4.6",
194
+ api: "openai-responses",
195
+ provider: "xai",
196
+ baseUrl: "https://api.x.ai/v1",
197
+ reasoning: true,
198
+ input: ["text", "image"],
199
+ cost: {
200
+ input: 2,
201
+ output: 6,
202
+ cacheRead: .3,
203
+ cacheWrite: 0
204
+ },
205
+ contextWindow: 5e5,
206
+ maxTokens: 5e5,
207
+ compat: { supportsLongCacheRetention: false },
208
+ thinkingLevelMap: {
209
+ off: null,
210
+ minimal: null,
211
+ low: "low",
212
+ medium: "medium",
213
+ high: "high",
214
+ xhigh: "xhigh",
215
+ max: null
216
+ }
217
+ }],
218
+ openrouter: [{
219
+ id: "stealth/ox-alpha",
220
+ name: "OX Alpha",
221
+ api: "openai-completions",
222
+ provider: "openrouter",
223
+ baseUrl: "https://openrouter.ai/api/v1",
224
+ reasoning: true,
225
+ input: ["text", "image"],
226
+ cost: {
227
+ input: 0,
228
+ output: 0,
229
+ cacheRead: 0,
230
+ cacheWrite: 0
231
+ },
232
+ contextWindow: 1e6,
233
+ maxTokens: 131072,
234
+ compat: {
235
+ supportsDeveloperRole: false,
236
+ thinkingFormat: "openrouter"
237
+ },
238
+ thinkingLevelMap: {
239
+ off: null,
240
+ minimal: null,
241
+ low: "low",
242
+ medium: null,
243
+ high: "high",
244
+ xhigh: null,
245
+ max: "max"
246
+ }
247
+ }],
248
+ "zai-coding-cn": [{
249
+ id: "glm-5.3-flash",
250
+ name: "GLM-5.3-Flash",
251
+ api: "openai-completions",
252
+ provider: "zai-coding-cn",
253
+ baseUrl: "https://open.bigmodel.cn/api/coding/paas/v4",
254
+ reasoning: true,
255
+ input: ["text", "image"],
256
+ cost: {
257
+ input: 0,
258
+ output: 0,
259
+ cacheRead: 0,
260
+ cacheWrite: 0
261
+ },
262
+ contextWindow: 1e6,
263
+ maxTokens: 131072,
264
+ compat: {
265
+ supportsStore: false,
266
+ supportsDeveloperRole: false,
267
+ supportsReasoningEffort: true,
268
+ thinkingFormat: "zai",
269
+ zaiToolStream: true
270
+ },
271
+ thinkingLevelMap: {
272
+ off: null,
273
+ minimal: null,
274
+ low: "low",
275
+ medium: null,
276
+ high: "high",
277
+ xhigh: null,
278
+ max: "max"
279
+ }
280
+ }, {
281
+ id: "glm-5.3",
282
+ name: "GLM-5.3",
283
+ api: "openai-completions",
284
+ provider: "zai-coding-cn",
285
+ baseUrl: "https://open.bigmodel.cn/api/coding/paas/v4",
286
+ reasoning: true,
287
+ input: ["text"],
288
+ cost: {
289
+ input: 0,
290
+ output: 0,
291
+ cacheRead: 0,
292
+ cacheWrite: 0
293
+ },
294
+ contextWindow: 1e6,
295
+ maxTokens: 131072,
296
+ compat: {
297
+ supportsStore: false,
298
+ supportsDeveloperRole: false,
299
+ supportsReasoningEffort: true,
300
+ thinkingFormat: "zai",
301
+ zaiToolStream: true
302
+ },
303
+ thinkingLevelMap: {
304
+ off: null,
305
+ minimal: null,
306
+ low: "low",
307
+ medium: null,
308
+ high: "high",
309
+ xhigh: null,
310
+ max: "max"
311
+ }
312
+ }]
313
+ };
314
+ /**
315
+ * Extra models this plugin publishes for one pi-ai provider id.
316
+ * @param providerId - catalog provider id (e.g. `xai`), not the harness route.
317
+ */
318
+ function extraModelsFor(providerId) {
319
+ return EXTRA_MODELS_BY_PROVIDER[providerId] ?? [];
320
+ }
321
+ //#endregion
322
+ //#region src/hosted-capture.ts
323
+ /** Per-request bag for hosted Responses output that pi-ai drops. */
324
+ const hostedCapture = new AsyncLocalStorage();
325
+ function currentHostedCapture() {
326
+ return hostedCapture.getStore();
327
+ }
328
+ //#endregion
329
+ //#region src/hosted-images.ts
330
+ function isRecord$1(value) {
331
+ return typeof value === "object" && value !== null && !Array.isArray(value);
332
+ }
333
+ function stripDataUrl(value) {
334
+ return /^data:image\/[a-zA-Z0-9.+-]+;base64,(.+)$/s.exec(value.trim())?.[1] ?? value.trim();
335
+ }
336
+ function base64FromUnknown(value) {
337
+ if (typeof value === "string" && value.length > 0) return stripDataUrl(value);
338
+ if (!isRecord$1(value)) return void 0;
339
+ if (typeof value.b64_json === "string" && value.b64_json.length > 0) return stripDataUrl(value.b64_json);
340
+ if (typeof value.base64 === "string" && value.base64.length > 0) return stripDataUrl(value.base64);
341
+ if (typeof value.result === "string" && value.result.length > 0) return stripDataUrl(value.result);
342
+ }
343
+ function pushImage(into, id, base64) {
344
+ if (id !== void 0 && into.some((existing) => existing.id === id)) return;
345
+ if (id === void 0 && into.some((existing) => existing.base64 === base64)) return;
346
+ into.push(id === void 0 ? { base64 } : {
347
+ id,
348
+ base64
349
+ });
350
+ }
351
+ function collectHostedImageFromItem(item, into) {
352
+ if (!isRecord$1(item) || item.type !== "image_generation_call") return;
353
+ if (item.status !== void 0 && item.status !== "completed") return;
354
+ const base64 = base64FromUnknown(item.result);
355
+ if (base64 === void 0) return;
356
+ pushImage(into, typeof item.id === "string" ? item.id : void 0, base64);
357
+ }
358
+ function collectHostedImagesFromEvent(event, into) {
359
+ if (!isRecord$1(event) || typeof event.type !== "string") return;
360
+ if (event.type === "response.output_item.done") {
361
+ collectHostedImageFromItem(event.item, into);
362
+ return;
363
+ }
364
+ if (event.type.startsWith("response.image_generation_call.")) {
365
+ collectHostedImageFromItem(event.item, into);
366
+ if (event.type.endsWith(".completed")) {
367
+ const base64 = base64FromUnknown(event.result);
368
+ 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);
369
+ }
370
+ return;
371
+ }
372
+ if (event.type !== "response.completed" && event.type !== "response.incomplete") return;
373
+ const output = isRecord$1(event.response) ? event.response.output : void 0;
374
+ if (!Array.isArray(output)) return;
375
+ for (const item of output) collectHostedImageFromItem(item, into);
376
+ }
377
+ //#endregion
378
+ //#region src/ids.ts
379
+ /** Basename of the DSH-owned multi-provider OAuth document. */
380
+ const PI_LOGIN_AUTH_FILENAME = ".dsh-oauth-auth.json";
381
+ /** Legacy DSH filename accepted during the one-time storage migration. */
382
+ const LEGACY_PI_LOGIN_AUTH_FILENAME = ".pi-login-auth.json";
383
+ /** Provider idle ceiling used by every composite route. */
384
+ const PI_LOGIN_STREAM_IDLE_TIMEOUT_MS = 3e5;
385
+ Object.freeze([
386
+ "EMPTY_RESPONSE",
387
+ "RATE_LIMIT",
388
+ "SERVER",
389
+ "TIMEOUT",
390
+ "TRANSPORT"
391
+ ]);
392
+ //#endregion
393
+ //#region src/openrouter-models.ts
394
+ function objectValue(value) {
395
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
396
+ }
397
+ function validModelId(value) {
398
+ return typeof value === "string" && /^~?[a-zA-Z0-9][a-zA-Z0-9._:/-]{0,255}$/.test(value);
399
+ }
400
+ function strings(value) {
401
+ return Array.isArray(value) ? [...new Set(value.filter((item) => typeof item === "string" && item.length < 256))] : [];
402
+ }
403
+ function positiveInteger(value) {
404
+ return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : void 0;
405
+ }
406
+ /**
407
+ * Conservative, all-cost classification. Missing/invalid prices, unknown
408
+ * structures and conditional overrides never become a free badge. In
409
+ * particular, a tiny positive decimal must not underflow into a free price.
410
+ */
411
+ function classifyOpenRouterPricing(value) {
412
+ const source = objectValue(value);
413
+ const pricing = {};
414
+ let unknown = source === void 0 || source.prompt === void 0 || source.completion === void 0;
415
+ let paid = false;
416
+ for (const [key, raw] of Object.entries(source ?? {})) {
417
+ if (key === "overrides") {
418
+ if (!Array.isArray(raw) || raw.length > 0) unknown = true;
419
+ continue;
420
+ }
421
+ if (typeof raw !== "string" || raw.length > 128 || !/^\d+(?:\.\d+)?(?:e[+-]?\d+)?$/i.test(raw)) {
422
+ unknown = true;
423
+ continue;
424
+ }
425
+ const numeric = Number(raw);
426
+ if (!Number.isFinite(numeric) || numeric < 0) {
427
+ unknown = true;
428
+ continue;
429
+ }
430
+ pricing[key] = raw;
431
+ if (!/^0+(?:\.0+)?(?:e[+-]?\d+)?$/i.test(raw)) paid = true;
432
+ }
433
+ return {
434
+ pricing,
435
+ priceStatus: paid ? "paid" : unknown ? "unknown" : "free"
436
+ };
437
+ }
438
+ function normalizeOpenRouterModels(raw) {
439
+ if (!Array.isArray(raw) || raw.length === 0 || raw.length > 1e4) throw new Error("OpenRouter returned an empty or invalid model catalog");
440
+ const seen = /* @__PURE__ */ new Set();
441
+ const out = [];
442
+ for (const entry of raw) {
443
+ const row = objectValue(entry);
444
+ if (row === void 0 || !validModelId(row.id)) throw new Error("Invalid OpenRouter model ID");
445
+ if (row.id.endsWith(":batch")) continue;
446
+ const architecture = objectValue(row.architecture);
447
+ const inputModalities = strings(architecture?.input_modalities);
448
+ const outputModalities = strings(architecture?.output_modalities);
449
+ if (!inputModalities.includes("text") || !outputModalities.includes("text")) continue;
450
+ if (seen.has(row.id)) throw new Error("Duplicate OpenRouter model ID");
451
+ const top = objectValue(row.top_provider);
452
+ const contextWindow = positiveInteger(row.context_length) ?? positiveInteger(top?.context_length);
453
+ if (contextWindow === void 0) throw new Error("Invalid OpenRouter context limit");
454
+ const supportedParameters = strings(row.supported_parameters);
455
+ const reason = objectValue(row.reasoning);
456
+ const reasoning = supportedParameters.some((key) => key === "reasoning" || key === "include_reasoning");
457
+ const supportedEfforts = strings(reason?.supported_efforts);
458
+ const defaultEffort = typeof reason?.default_effort === "string" ? reason.default_effort : void 0;
459
+ const maxTokens = Math.min(contextWindow, positiveInteger(top?.max_completion_tokens) ?? Math.min(contextWindow, 8192));
460
+ out.push({
461
+ id: row.id,
462
+ name: typeof row.name === "string" && row.name.trim() !== "" ? row.name.slice(0, 256) : row.id,
463
+ contextWindow,
464
+ maxTokens,
465
+ inputModalities,
466
+ outputModalities,
467
+ supportedParameters,
468
+ tools: supportedParameters.includes("tools"),
469
+ reasoning,
470
+ reasoningMandatory: reasoning && reason?.mandatory === true,
471
+ ...supportedEfforts.length === 0 ? {} : { supportedEfforts },
472
+ ...defaultEffort === void 0 ? {} : { defaultEffort },
473
+ ...classifyOpenRouterPricing(row.pricing),
474
+ ...typeof row.expiration_date === "string" && /^\d{4}-\d{2}-\d{2}$/.test(row.expiration_date) ? { deprecatesAt: row.expiration_date } : {}
475
+ });
476
+ seen.add(row.id);
477
+ }
478
+ if (out.length === 0) throw new Error("OpenRouter returned no usable text models");
479
+ return out;
480
+ }
481
+ const LEVELS = [
482
+ "off",
483
+ "minimal",
484
+ "low",
485
+ "medium",
486
+ "high",
487
+ "xhigh",
488
+ "max"
489
+ ];
490
+ function openRouterThinkingMap(info, base) {
491
+ if (!info.reasoning) return void 0;
492
+ if (info.supportedEfforts === void 0) return {
493
+ ...base?.thinkingLevelMap,
494
+ ...info.reasoningMandatory ? { off: null } : {}
495
+ };
496
+ const map = {};
497
+ for (const level of LEVELS) {
498
+ const wire = level === "off" ? "none" : level;
499
+ map[level] = info.supportedEfforts.includes(wire) && !(level === "off" && info.reasoningMandatory) ? wire : null;
500
+ }
501
+ return map;
502
+ }
503
+ function openRouterDefaultEffort(info) {
504
+ if (info?.reasoning !== true) return void 0;
505
+ if (info.defaultEffort !== void 0 && info.defaultEffort !== "none") return info.defaultEffort;
506
+ if (!info.reasoningMandatory) return void 0;
507
+ return info.supportedEfforts?.find((level) => level === "high") ?? info.supportedEfforts?.find((level) => level !== "none") ?? "high";
508
+ }
509
+ function openRouterPiModel(info, base) {
510
+ const rate = (key) => {
511
+ const value = Number(info.pricing[key] ?? 0) * 1e6;
512
+ return Number.isFinite(value) ? value : 0;
513
+ };
514
+ const thinkingLevelMap = openRouterThinkingMap(info, base);
515
+ return {
516
+ ...base,
517
+ id: info.id,
518
+ name: info.name,
519
+ api: "openai-completions",
520
+ provider: "openrouter",
521
+ baseUrl: "https://openrouter.ai/api/v1",
522
+ input: info.inputModalities.includes("image") ? ["text", "image"] : ["text"],
523
+ contextWindow: info.contextWindow,
524
+ maxTokens: info.maxTokens,
525
+ reasoning: info.reasoning,
526
+ ...thinkingLevelMap === void 0 ? {} : { thinkingLevelMap },
527
+ cost: {
528
+ input: rate("prompt"),
529
+ output: rate("completion"),
530
+ cacheRead: rate("input_cache_read"),
531
+ cacheWrite: rate("input_cache_write")
532
+ },
533
+ compat: {
534
+ ...base?.compat,
535
+ supportsDeveloperRole: false,
536
+ thinkingFormat: "openrouter"
537
+ }
538
+ };
539
+ }
540
+ //#endregion
541
+ //#region src/openrouter-types.ts
542
+ const OPENROUTER_REFRESH_MS = 9e5;
543
+ const OPENROUTER_COOLDOWN_MS = 6e4;
544
+ //#endregion
545
+ //#region src/native-tools.ts
546
+ /** DSH model-facing tools that steal traffic from a subscribed provider. */
547
+ const DSH_WEB_TOOL_NAMES = ["web_search", "web_fetch"];
548
+ /** Matching tool-web prompt sections. */
549
+ const DSH_WEB_SECTION_NAMES = ["tool:web_search", "tool:web_fetch"];
550
+ const DSH_WEB_TOOL_NAME_SET = new Set(DSH_WEB_TOOL_NAMES);
551
+ new Set(DSH_WEB_SECTION_NAMES);
552
+ /** Precise xAI server-side X Search operations published by the provider. */
553
+ const XAI_SERVER_X_SEARCH_NAMES = [
554
+ "x_user_search",
555
+ "x_keyword_search",
556
+ "x_semantic_search",
557
+ "x_thread_fetch"
558
+ ];
559
+ new Set(XAI_SERVER_X_SEARCH_NAMES);
560
+ /**
561
+ * Hosted / server-executed names that can leak as client function calls
562
+ * after this plugin removes DSH's implementations from the route.
563
+ */
564
+ const HOSTED_CLIENT_LEAK_NAMES = [
565
+ ...XAI_SERVER_X_SEARCH_NAMES,
566
+ "web_search",
567
+ "web_fetch",
568
+ "image_generation",
569
+ "imagine_text_to_image",
570
+ "imagine_image_to_image",
571
+ "imagine_image_edit"
572
+ ];
573
+ new Set(HOSTED_CLIENT_LEAK_NAMES);
574
+ const DEFAULT_NATIVE_TOOL_POLICY = {
575
+ enabled: true,
576
+ image: true
577
+ };
578
+ 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.";
579
+ 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.";
580
+ function responsesSearch() {
581
+ return { type: "web_search" };
582
+ }
583
+ function responsesXSearch() {
584
+ return { type: "x_search" };
585
+ }
586
+ function responsesImage() {
587
+ return { type: "image_generation" };
588
+ }
589
+ function anthropicSearch() {
590
+ return {
591
+ type: "web_search_20250305",
592
+ name: "web_search"
593
+ };
594
+ }
595
+ function nativePlan(providerId, api, policy = DEFAULT_NATIVE_TOOL_POLICY) {
596
+ if (!policy.enabled) return void 0;
597
+ switch (`${providerId}:${api ?? ""}`) {
598
+ case "xai:openai-responses": return {
599
+ providerId,
600
+ api: "openai-responses",
601
+ hosted: [
602
+ responsesSearch(),
603
+ responsesXSearch(),
604
+ ...policy.image ? [responsesImage()] : []
605
+ ],
606
+ guidance: policy.image ? SEARCH_AND_IMAGE_GUIDANCE : SEARCH_GUIDANCE
607
+ };
608
+ case "openai-codex:openai-codex-responses": return {
609
+ providerId,
610
+ api: "openai-codex-responses",
611
+ hosted: [responsesSearch(), ...policy.image ? [responsesImage()] : []],
612
+ guidance: policy.image ? SEARCH_AND_IMAGE_GUIDANCE : SEARCH_GUIDANCE
613
+ };
614
+ case "anthropic:anthropic-messages": return {
615
+ providerId,
616
+ api: "anthropic-messages",
617
+ hosted: [anthropicSearch()],
618
+ guidance: SEARCH_GUIDANCE
619
+ };
620
+ default: return;
621
+ }
622
+ }
623
+ function isDshWebToolName(name) {
624
+ return DSH_WEB_TOOL_NAME_SET.has(name);
625
+ }
626
+ function isRecord(value) {
627
+ return typeof value === "object" && value !== null && !Array.isArray(value);
628
+ }
629
+ /** A function-calling tool DSH registered as web_search / web_fetch. */
630
+ function isDshWebFunctionTool(tool) {
631
+ if (!isRecord(tool)) return false;
632
+ const name = typeof tool.name === "string" ? tool.name : void 0;
633
+ if (name === void 0 || !isDshWebToolName(name)) return false;
634
+ const type = tool.type;
635
+ return type === void 0 || type === "function";
636
+ }
637
+ function hostedToolKey(tool) {
638
+ return `${typeof tool.type === "string" ? tool.type : "function"}:${typeof tool.name === "string" ? tool.name : ""}`;
639
+ }
640
+ function applyNativeToolsToPayload(payload, providerId, api, policy = DEFAULT_NATIVE_TOOL_POLICY) {
641
+ const plan = nativePlan(providerId, api, policy);
642
+ if (plan === void 0 || !isRecord(payload) || !Array.isArray(payload.tools)) return payload;
643
+ const kept = payload.tools.filter((tool) => !isDshWebFunctionTool(tool));
644
+ const seen = new Set(kept.filter(isRecord).map((tool) => hostedToolKey(tool)));
645
+ const hosted = plan.hosted.filter((tool) => {
646
+ const key = hostedToolKey(tool);
647
+ if (seen.has(key)) return false;
648
+ seen.add(key);
649
+ return true;
650
+ });
651
+ return {
652
+ ...payload,
653
+ tools: [...hosted, ...kept]
654
+ };
655
+ }
656
+ function wrapOnPayload(existing, providerId, api, policy = DEFAULT_NATIVE_TOOL_POLICY) {
657
+ if (nativePlan(providerId, api, policy) === void 0) return existing;
658
+ return async (payload, model) => {
659
+ const injected = applyNativeToolsToPayload(isRecord(payload) && !("tools" in payload) ? {
660
+ ...payload,
661
+ tools: []
662
+ } : payload, providerId, api, policy);
663
+ if (existing === void 0) return injected;
664
+ return await existing(injected, model);
665
+ };
666
+ }
667
+ function filterPiContext(context, providerId, api, policy = DEFAULT_NATIVE_TOOL_POLICY) {
668
+ if (nativePlan(providerId, api, policy) === void 0 || context.tools === void 0) return context;
669
+ return {
670
+ ...context,
671
+ tools: context.tools.filter((tool) => !isDshWebToolName(tool.name))
672
+ };
673
+ }
674
+ /**
675
+ * Prepare one provider request as a single unit.
676
+ *
677
+ * Native tools are a property of this request, not of the provider account.
678
+ * A context without tools is a text-only call (reviewers, summaries, titles,
679
+ * and similar utility traffic), so its payload hook must stay untouched even
680
+ * when the provider serializer later emits `tools: []`.
681
+ */
682
+ function prepareNativeToolRequest(context, options, providerId, api, policy = DEFAULT_NATIVE_TOOL_POLICY) {
683
+ if (context.tools === void 0 || nativePlan(providerId, api, policy) === void 0) return {
684
+ context,
685
+ options
686
+ };
687
+ const onPayload = wrapOnPayload(options?.onPayload, providerId, api, policy);
688
+ return {
689
+ context: filterPiContext(context, providerId, api, policy),
690
+ options: onPayload === options.onPayload ? options : Object.assign({}, options, { onPayload })
691
+ };
692
+ }
693
+ //#endregion
694
+ //#region src/responses-tap.ts
695
+ /** Peek Responses SSE for hosted image_generation_call items pi-ai ignores. */
696
+ const TAPPED = Symbol("dsh-oauth-hosted-tap");
697
+ function requestUrl(input) {
698
+ if (typeof input === "string") return input;
699
+ if (input instanceof URL) return input.href;
700
+ return input.url;
701
+ }
702
+ function requestMethod(input, init) {
703
+ if (init?.method !== void 0) return init.method.toUpperCase();
704
+ if (typeof input !== "string" && !(input instanceof URL) && input.method.length > 0) return input.method.toUpperCase();
705
+ return "GET";
706
+ }
707
+ function isResponsesRequest(input, init) {
708
+ if (requestMethod(input, init) !== "POST") return false;
709
+ try {
710
+ const path = new URL(requestUrl(input)).pathname;
711
+ return path === "/responses" || path.endsWith("/responses");
712
+ } catch {
713
+ return false;
714
+ }
715
+ }
716
+ function parseSseBlock(block) {
717
+ const data = block.split(/\r?\n/).filter((line) => line.startsWith("data:")).map((line) => line.slice(5).trimStart()).join("\n").trim();
718
+ if (data.length === 0 || data === "[DONE]") return void 0;
719
+ try {
720
+ return JSON.parse(data);
721
+ } catch {
722
+ return;
723
+ }
724
+ }
725
+ function consumeResponsesSseText(text, onEvent) {
726
+ const parts = text.split(/\r?\n\r?\n/);
727
+ const rest = parts.pop() ?? "";
728
+ for (const block of parts) {
729
+ const event = parseSseBlock(block);
730
+ if (event !== void 0) onEvent(event);
731
+ }
732
+ return rest;
733
+ }
734
+ function peekResponsesBody(body, onEvent) {
735
+ const decoder = new TextDecoder();
736
+ let buffer = "";
737
+ return body.pipeThrough(new TransformStream({
738
+ transform(chunk, controller) {
739
+ controller.enqueue(chunk);
740
+ buffer = consumeResponsesSseText(buffer + decoder.decode(chunk, { stream: true }), onEvent);
741
+ },
742
+ flush(controller) {
743
+ if (buffer.length === 0) return;
744
+ const event = parseSseBlock(buffer);
745
+ if (event !== void 0) onEvent(event);
746
+ }
747
+ }));
748
+ }
749
+ function tapResponsesResponse(response, onEvent) {
750
+ if (response.body === null) return response;
751
+ return new Response(peekResponsesBody(response.body, onEvent), response);
752
+ }
753
+ function wrapFetchForHostedOutput(fetchImpl) {
754
+ return async (input, init) => {
755
+ const capture = currentHostedCapture();
756
+ if (capture === void 0 || !isResponsesRequest(input, init)) return await fetchImpl(input, init);
757
+ return tapResponsesResponse(await fetchImpl(input, init), (event) => collectHostedImagesFromEvent(event, capture.images));
758
+ };
759
+ }
760
+ function installHostedOutputFetch() {
761
+ const current = globalThis.fetch;
762
+ if (current[TAPPED] === true) return;
763
+ const next = Object.assign(wrapFetchForHostedOutput(current), { [TAPPED]: true });
764
+ globalThis.fetch = next;
765
+ }
766
+ //#endregion
767
+ //#region src/http.ts
768
+ /** Proxy-aware HTTP transport for OAuth and subscribed-provider requests. */
769
+ const originalFetch = globalThis.fetch;
770
+ let installedFetch;
771
+ const DEFAULT_LOOPBACK_PROXY_CANDIDATES = [
772
+ "http://127.0.0.1:7890",
773
+ "http://127.0.0.1:45678",
774
+ "http://127.0.0.1:7891",
775
+ "http://127.0.0.1:8080",
776
+ "http://127.0.0.1:8888"
777
+ ];
778
+ const PROXY_ENV_KEYS = [
779
+ "HTTPS_PROXY",
780
+ "https_proxy",
781
+ "HTTP_PROXY",
782
+ "http_proxy",
783
+ "ALL_PROXY",
784
+ "all_proxy"
785
+ ];
786
+ function normalizeProxyUrl(value) {
787
+ const trimmed = value.trim();
788
+ if (trimmed.length === 0) throw new Error("dsh-oauth-login: proxy URL is empty");
789
+ const url = new URL(trimmed.includes("://") ? trimmed : `http://${trimmed}`);
790
+ if (url.protocol !== "http:" && url.protocol !== "https:") throw new Error(`dsh-oauth-login: unsupported proxy protocol "${url.protocol}"`);
791
+ if (url.hostname.length === 0) throw new Error("dsh-oauth-login: proxy URL has no host");
792
+ url.pathname = "/";
793
+ url.search = "";
794
+ url.hash = "";
795
+ return url.toString();
796
+ }
797
+ function inheritedProxy(env) {
798
+ for (const key of PROXY_ENV_KEYS) {
799
+ const value = env[key];
800
+ if (value !== void 0 && value.trim().length > 0) return value;
801
+ }
802
+ }
803
+ function scutilValue(output, key) {
804
+ const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
805
+ return output.match(new RegExp(`^\\s*${escaped}\\s*:\\s*(.+?)\\s*$`, "m"))?.[1];
806
+ }
807
+ function parseMacOSSystemProxy(output) {
808
+ for (const kind of ["HTTPS", "HTTP"]) {
809
+ if (scutilValue(output, `${kind}Enable`) !== "1") continue;
810
+ const host = scutilValue(output, `${kind}Proxy`);
811
+ const port = Number(scutilValue(output, `${kind}Port`));
812
+ if (host === void 0 || host.length === 0 || !Number.isInteger(port) || port <= 0 || port > 65535) continue;
813
+ return normalizeProxyUrl(`http://${host.includes(":") && !host.startsWith("[") ? `[${host}]` : host}:${port}`);
814
+ }
815
+ }
816
+ async function readMacOSSystemProxy() {
817
+ return await new Promise((resolve) => {
818
+ execFile("/usr/sbin/scutil", ["--proxy"], {
819
+ encoding: "utf8",
820
+ timeout: 1500,
821
+ maxBuffer: 131072
822
+ }, (error, stdout) => resolve(error === null ? parseMacOSSystemProxy(stdout) : void 0));
823
+ });
824
+ }
825
+ function isLoopbackProxy(proxyUrl) {
826
+ const hostname = new URL(proxyUrl).hostname.replace(/^\[(.*)\]$/, "$1").toLowerCase();
827
+ return hostname === "127.0.0.1" || hostname === "localhost" || hostname === "::1";
828
+ }
829
+ /**
830
+ * Verify that a candidate is an HTTP CONNECT proxy. The probe contains no
831
+ * OAuth code, credential, token, cookie, or geographic metadata.
832
+ */
833
+ async function probeHttpConnectProxy(proxyUrl) {
834
+ const url = new URL(proxyUrl);
835
+ if (url.protocol !== "http:") return false;
836
+ const port = Number(url.port || "80");
837
+ if (!Number.isInteger(port) || port <= 0 || port > 65535) return false;
838
+ return await new Promise((resolve) => {
839
+ const socket = connect({
840
+ host: url.hostname,
841
+ port
842
+ });
843
+ let settled = false;
844
+ let response = "";
845
+ const finish = (accepted) => {
846
+ if (settled) return;
847
+ settled = true;
848
+ socket.destroy();
849
+ resolve(accepted);
850
+ };
851
+ socket.setTimeout(800);
852
+ socket.once("connect", () => {
853
+ socket.write("CONNECT auth.openai.com:443 HTTP/1.1\r\nHost: auth.openai.com:443\r\nConnection: close\r\n\r\n");
854
+ });
855
+ socket.on("data", (chunk) => {
856
+ response += chunk.toString("latin1");
857
+ const lineEnd = response.indexOf("\r\n");
858
+ if (lineEnd >= 0) finish(/^HTTP\/1\.[01] 200(?:\s|$)/.test(response.slice(0, lineEnd)));
859
+ else if (response.length > 16384) finish(false);
860
+ });
861
+ socket.once("timeout", () => finish(false));
862
+ socket.once("error", () => finish(false));
863
+ socket.once("end", () => finish(/^HTTP\/1\.[01] 200(?:\s|$)/.test(response)));
864
+ });
865
+ }
866
+ async function resolveOAuthProxy(options = {}) {
867
+ const env = options.env ?? process.env;
868
+ if (inheritedProxy(env) !== void 0) return { source: "environment" };
869
+ const explicit = env.DSH_OAUTH_PROXY;
870
+ if (explicit !== void 0 && explicit.trim().length > 0) return {
871
+ source: "explicit",
872
+ proxyUrl: normalizeProxyUrl(explicit)
873
+ };
874
+ const probe = options.probe ?? probeHttpConnectProxy;
875
+ if ((options.platform ?? process.platform) === "darwin") {
876
+ const systemProxy = await (options.readSystemProxy ?? readMacOSSystemProxy)();
877
+ if (systemProxy !== void 0) {
878
+ const normalized = normalizeProxyUrl(systemProxy);
879
+ if (await probe(normalized)) return {
880
+ source: "system",
881
+ proxyUrl: normalized
882
+ };
883
+ }
884
+ }
885
+ const normalizedCandidates = (options.candidates ?? DEFAULT_LOOPBACK_PROXY_CANDIDATES).flatMap((candidate) => {
886
+ try {
887
+ const normalized = normalizeProxyUrl(candidate);
888
+ return isLoopbackProxy(normalized) ? [normalized] : [];
889
+ } catch {
890
+ return [];
891
+ }
892
+ });
893
+ const selected = (await Promise.all(normalizedCandidates.map(async (candidate) => {
894
+ try {
895
+ return await probe(candidate);
896
+ } catch {
897
+ return false;
898
+ }
899
+ }))).findIndex(Boolean);
900
+ if (selected >= 0) return {
901
+ source: "loopback",
902
+ proxyUrl: normalizedCandidates[selected]
903
+ };
904
+ return { source: "direct" };
905
+ }
906
+ function noProxyValue(env) {
907
+ const entries = new Set((env.NO_PROXY ?? env.no_proxy ?? "").split(/[\s,]+/).filter(Boolean));
908
+ entries.add("localhost");
909
+ entries.add("127.0.0.1");
910
+ entries.add("::1");
911
+ return [...entries].join(",");
912
+ }
913
+ function inheritedProxyOptions(env) {
914
+ const allProxy = env.ALL_PROXY ?? env.all_proxy ?? "";
915
+ return {
916
+ httpProxy: env.HTTP_PROXY ?? env.http_proxy ?? allProxy,
917
+ httpsProxy: env.HTTPS_PROXY ?? env.https_proxy ?? allProxy
918
+ };
919
+ }
920
+ async function createOAuthHttpTransport(options = {}) {
921
+ const env = options.env ?? process.env;
922
+ const resolution = await resolveOAuthProxy({
923
+ ...options,
924
+ env
925
+ });
926
+ const selectedProxy = "proxyUrl" in resolution ? resolution.proxyUrl : void 0;
927
+ const inherited = inheritedProxyOptions(env);
928
+ return {
929
+ dispatcher: new EnvHttpProxyAgent({
930
+ allowH2: false,
931
+ httpProxy: selectedProxy ?? inherited.httpProxy,
932
+ httpsProxy: selectedProxy ?? inherited.httpsProxy,
933
+ noProxy: noProxyValue(env)
934
+ }),
935
+ resolution
936
+ };
937
+ }
938
+ function installOAuthHttpGlobals() {
939
+ if (installedFetch === void 0 ? globalThis.fetch === originalFetch : globalThis.fetch === installedFetch) {
940
+ install();
941
+ installedFetch = globalThis.fetch;
942
+ }
943
+ installHostedOutputFetch();
944
+ }
945
+ //#endregion
946
+ //#region src/proxy-config.ts
947
+ const PROXY_SETTINGS_FILENAME = ".dsh-oauth-proxy.json";
948
+ function defaultProxySettings() {
949
+ return {
950
+ revision: 0,
951
+ http: {
952
+ enabled: true,
953
+ url: ""
954
+ },
955
+ websocket: {
956
+ enabled: true,
957
+ url: ""
958
+ }
959
+ };
960
+ }
961
+ function object(value, keys, label) {
962
+ if (typeof value !== "object" || value === null || Array.isArray(value) || Object.keys(value).some((key) => !keys.includes(key))) throw new Error(`${label}: invalid settings object`);
963
+ return value;
964
+ }
965
+ function channel(value, label) {
966
+ const raw = object(value, ["enabled", "url"], label);
967
+ if (typeof raw.enabled !== "boolean" || typeof raw.url !== "string" || raw.url.length > 1024) throw new Error(`${label}: expected enabled and proxy URL`);
968
+ const text = raw.url.trim();
969
+ if (text === "") return {
970
+ enabled: raw.enabled,
971
+ url: ""
972
+ };
973
+ let parsed;
974
+ try {
975
+ parsed = new URL(text);
976
+ } catch {
977
+ throw new Error(`${label}: enter a valid HTTP proxy address and port`);
978
+ }
979
+ 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`);
980
+ return {
981
+ enabled: raw.enabled,
982
+ url: parsed.origin
983
+ };
984
+ }
985
+ function parseProxySettings(value) {
986
+ const raw = object(value, [
987
+ "revision",
988
+ "http",
989
+ "websocket"
990
+ ], "Network");
991
+ if (typeof raw.revision !== "number" || !Number.isSafeInteger(raw.revision) || raw.revision < 0) throw new Error("Network: invalid revision");
992
+ return {
993
+ revision: raw.revision,
994
+ http: channel(raw.http, "HTTP"),
995
+ websocket: channel(raw.websocket, "WebSocket")
996
+ };
997
+ }
998
+ //#endregion
999
+ //#region src/proxy-store.ts
1000
+ /** Independent, atomic settings file; never writes the OAuth credential document. */
1001
+ var ProxySettingsConflict = class extends Error {};
1002
+ var ProxySettingsStore = class {
1003
+ filename;
1004
+ constructor(filename) {
1005
+ this.filename = filename;
1006
+ }
1007
+ async read() {
1008
+ let text;
1009
+ try {
1010
+ text = await readFile(this.filename, "utf8");
1011
+ } catch (error) {
1012
+ if (error.code === "ENOENT") return defaultProxySettings();
1013
+ throw new Error("Network settings could not be read", { cause: error });
1014
+ }
1015
+ let document;
1016
+ try {
1017
+ document = JSON.parse(text);
1018
+ } catch {
1019
+ throw new Error("Network settings file is not valid JSON");
1020
+ }
1021
+ 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");
1022
+ return parseProxySettings(document.settings);
1023
+ }
1024
+ async save(value) {
1025
+ const input = parseProxySettings(value);
1026
+ await mkdir(dirname(this.filename), {
1027
+ recursive: true,
1028
+ mode: 448
1029
+ });
1030
+ return withFileLock(this.filename, async () => {
1031
+ const current = await this.read();
1032
+ if (current.revision !== input.revision) throw new ProxySettingsConflict("Network settings changed. Reload them before saving.");
1033
+ const settings = {
1034
+ ...input,
1035
+ revision: current.revision + 1
1036
+ };
1037
+ await writeFileAtomic(this.filename, `${JSON.stringify({
1038
+ version: 1,
1039
+ settings
1040
+ }, null, 2)}\n`, {
1041
+ mode: 384,
1042
+ dirMode: 448
1043
+ });
1044
+ return settings;
1045
+ });
1046
+ }
1047
+ };
1048
+ //#endregion
1049
+ //#region src/proxy-transport.ts
1050
+ /** Plugin-scoped HTTP/WS routing without changing process.env or provider SDKs. */
1051
+ const ROUTING_KEY = Symbol.for("dsh-oauth-login.scoped-proxy.v1");
1052
+ const globals = globalThis;
1053
+ const routing = globals[ROUTING_KEY] ??= { context: new AsyncLocalStorage() };
1054
+ function isLoopback(origin) {
1055
+ if (origin === void 0) return false;
1056
+ const hostname = new URL(origin).hostname.toLowerCase().replace(/^\[|\]$/g, "");
1057
+ return hostname === "localhost" || hostname.endsWith(".localhost") || hostname === "::1" || isIP(hostname) === 4 && hostname.startsWith("127.");
1058
+ }
1059
+ function installRouting() {
1060
+ const fallback = getGlobalDispatcher();
1061
+ if (fallback !== routing.dispatcher) {
1062
+ routing.dispatcher = fallback.compose((dispatch) => (options, handler) => {
1063
+ const scope = routing.context.getStore();
1064
+ if (scope === void 0) return dispatch(options, handler);
1065
+ const websocket = typeof options.upgrade === "string" && options.upgrade.toLowerCase() === "websocket";
1066
+ return (isLoopback(options.origin) ? scope.direct : websocket ? scope.websocket : scope.http).dispatch(options, handler);
1067
+ });
1068
+ setGlobalDispatcher(routing.dispatcher);
1069
+ }
1070
+ installOAuthHttpGlobals();
1071
+ }
1072
+ var OAuthProxyTransport = class {
1073
+ discovery;
1074
+ settings;
1075
+ readyPromise;
1076
+ generations = /* @__PURE__ */ new Set();
1077
+ sessions = /* @__PURE__ */ new Map();
1078
+ disposed = false;
1079
+ constructor(filename, discovery = {}) {
1080
+ this.discovery = discovery;
1081
+ this.settings = new ProxySettingsStore(filename);
1082
+ }
1083
+ async build() {
1084
+ const settings = await this.settings.read();
1085
+ const auto = [settings.http, settings.websocket].some((channel) => channel.enabled && channel.url === "") ? await createOAuthHttpTransport(this.discovery) : void 0;
1086
+ const direct = new Agent({ allowH2: false });
1087
+ const agents = /* @__PURE__ */ new Set([direct, ...auto === void 0 ? [] : [auto.dispatcher]]);
1088
+ const select = (channel) => {
1089
+ if (!channel.enabled) return direct;
1090
+ if (channel.url === "") {
1091
+ if (auto === void 0) throw new Error("Automatic proxy transport is unavailable");
1092
+ return auto.dispatcher;
1093
+ }
1094
+ const proxy = new ProxyAgent({
1095
+ uri: channel.url,
1096
+ allowH2: false
1097
+ });
1098
+ agents.add(proxy);
1099
+ return proxy;
1100
+ };
1101
+ const ready = {
1102
+ scope: {
1103
+ direct,
1104
+ http: select(settings.http),
1105
+ websocket: select(settings.websocket)
1106
+ },
1107
+ resolution: !settings.http.enabled ? { source: "direct" } : settings.http.url !== "" ? {
1108
+ source: "explicit",
1109
+ proxyUrl: settings.http.url
1110
+ } : auto?.resolution ?? { source: "direct" },
1111
+ agents,
1112
+ wsKey: JSON.stringify(settings.websocket) + (settings.websocket.enabled && !settings.websocket.url ? `:${settings.revision}` : ""),
1113
+ active: 0,
1114
+ retired: false,
1115
+ closing: false
1116
+ };
1117
+ if (this.disposed) {
1118
+ await Promise.allSettled([...agents].map((agent) => agent.destroy()));
1119
+ throw new Error("OAuth proxy transport disposed");
1120
+ }
1121
+ this.generations.add(ready);
1122
+ installRouting();
1123
+ return ready;
1124
+ }
1125
+ ready() {
1126
+ if (this.disposed) return Promise.reject(/* @__PURE__ */ new Error("OAuth proxy transport disposed"));
1127
+ if (this.readyPromise === void 0) {
1128
+ const pending = this.build();
1129
+ this.readyPromise = pending;
1130
+ pending.catch(() => {
1131
+ if (this.readyPromise === pending) this.readyPromise = void 0;
1132
+ });
1133
+ }
1134
+ return this.readyPromise;
1135
+ }
1136
+ async initialize() {
1137
+ return (await this.ready()).resolution;
1138
+ }
1139
+ async acquire() {
1140
+ while (true) {
1141
+ const ready = await this.ready();
1142
+ if (ready.retired) continue;
1143
+ ready.active++;
1144
+ return ready;
1145
+ }
1146
+ }
1147
+ async save(value) {
1148
+ if (this.disposed) throw new Error("OAuth proxy transport disposed");
1149
+ const saved = await this.settings.save(value);
1150
+ const previous = this.readyPromise;
1151
+ this.readyPromise = void 0;
1152
+ previous?.then((ready) => {
1153
+ ready.retired = true;
1154
+ this.release(ready, false);
1155
+ }, () => void 0);
1156
+ return saved;
1157
+ }
1158
+ release(ready, decrement = true) {
1159
+ if (decrement) ready.active--;
1160
+ if (ready.retired && ready.active === 0 && !ready.closing) {
1161
+ ready.closing = true;
1162
+ Promise.allSettled([...ready.agents].map((agent) => agent.close())).then(() => {
1163
+ this.generations.delete(ready);
1164
+ });
1165
+ }
1166
+ }
1167
+ async run(operation) {
1168
+ const ready = await this.acquire();
1169
+ try {
1170
+ installRouting();
1171
+ return await routing.context.run(ready.scope, operation);
1172
+ } finally {
1173
+ this.release(ready);
1174
+ }
1175
+ }
1176
+ beginSession(sessionId, ready) {
1177
+ if (sessionId === void 0) return void 0;
1178
+ let record = this.sessions.get(sessionId);
1179
+ if (record === void 0) {
1180
+ record = {
1181
+ key: ready.wsKey,
1182
+ active: 0,
1183
+ resetPending: false
1184
+ };
1185
+ this.sessions.set(sessionId, record);
1186
+ }
1187
+ if (record.key !== ready.wsKey) record.resetPending = true;
1188
+ if (record.resetPending && record.active === 0) {
1189
+ closeOpenAICodexWebSocketSessions(sessionId);
1190
+ record.key = ready.wsKey;
1191
+ record.resetPending = false;
1192
+ }
1193
+ record.active++;
1194
+ return () => {
1195
+ record.active--;
1196
+ };
1197
+ }
1198
+ async *iterate(source, codexSessionId) {
1199
+ const ready = await this.acquire();
1200
+ let endSession;
1201
+ let iterator;
1202
+ try {
1203
+ installRouting();
1204
+ endSession = this.beginSession(codexSessionId, ready);
1205
+ iterator = routing.context.run(ready.scope, () => source[Symbol.asyncIterator]());
1206
+ while (true) {
1207
+ const current = iterator;
1208
+ const item = await routing.context.run(ready.scope, () => current.next());
1209
+ if (item.done) return;
1210
+ yield item.value;
1211
+ }
1212
+ } finally {
1213
+ try {
1214
+ await routing.context.run(ready.scope, () => iterator?.return?.());
1215
+ } finally {
1216
+ endSession?.();
1217
+ this.release(ready);
1218
+ }
1219
+ }
1220
+ }
1221
+ async dispose() {
1222
+ this.disposed = true;
1223
+ await this.readyPromise?.catch(() => void 0);
1224
+ for (const sessionId of this.sessions.keys()) closeOpenAICodexWebSocketSessions(sessionId);
1225
+ this.sessions.clear();
1226
+ await Promise.allSettled([...this.generations].flatMap((ready) => [...ready.agents].map((agent) => agent.destroy())));
1227
+ this.generations.clear();
1228
+ }
1229
+ };
1230
+ //#endregion
1231
+ //#region src/openrouter-free.ts
1232
+ const FREE_ONLY_MESSAGE = "OpenRouter 免费保护:该模型已下线、价格变化或价格未确认,已阻止付费调用。请刷新模型列表并重新选择免费模型。";
1233
+ function protectOpenRouterPayload(payload, modelId) {
1234
+ const source = objectValue(payload);
1235
+ if (source === void 0) throw new LlmError(FREE_ONLY_MESSAGE, "OPENROUTER_FREE_ONLY");
1236
+ const result = { ...source };
1237
+ for (const key of [
1238
+ "models",
1239
+ "route",
1240
+ "router",
1241
+ "preset",
1242
+ "web_search_options",
1243
+ "image_config",
1244
+ "audio"
1245
+ ]) delete result[key];
1246
+ result.model = modelId;
1247
+ const pluginIds = /* @__PURE__ */ new Set([
1248
+ "web",
1249
+ "file-parser",
1250
+ "response-healing",
1251
+ "fusion",
1252
+ "pareto-router",
1253
+ "context-compression"
1254
+ ]);
1255
+ if (Array.isArray(source.plugins)) for (const plugin of source.plugins) {
1256
+ const id = objectValue(plugin)?.id;
1257
+ if (typeof id === "string") pluginIds.add(id);
1258
+ }
1259
+ result.plugins = [...pluginIds].map((id) => ({
1260
+ id,
1261
+ enabled: false
1262
+ }));
1263
+ result.modalities = ["text"];
1264
+ result.provider = {
1265
+ ...objectValue(source.provider),
1266
+ allow_fallbacks: false,
1267
+ require_parameters: true,
1268
+ max_price: {
1269
+ prompt: 0,
1270
+ completion: 0,
1271
+ request: 0,
1272
+ image: 0,
1273
+ audio: 0
1274
+ }
1275
+ };
1276
+ if (Array.isArray(result.tools) && result.tools.some((tool) => objectValue(tool)?.type !== "function")) throw new LlmError("OpenRouter 免费保护:免费调用不启用收费的服务端工具;请选择支持工具调用的免费模型。", "OPENROUTER_FREE_ONLY");
1277
+ if (Array.isArray(result.messages)) for (const message of result.messages) {
1278
+ const content = objectValue(message)?.content;
1279
+ if (Array.isArray(content) && content.some((part) => !["text", "image_url"].includes(String(objectValue(part)?.type)))) throw new LlmError("OpenRouter 免费保护:仅支持文本和图片输入,不启用收费文件解析或音视频处理。", "OPENROUTER_FREE_ONLY");
1280
+ }
1281
+ return result;
1282
+ }
1283
+ function prepareOpenRouterOptions(model, options, catalog) {
1284
+ const metadata = catalog?.model(model.id);
1285
+ 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;
1286
+ const defaultEffort = openRouterDefaultEffort(metadata);
1287
+ const existing = options.onPayload;
1288
+ return {
1289
+ ...options,
1290
+ onPayload: async (payload, currentModel) => {
1291
+ const result = await existing?.(payload, currentModel) ?? payload;
1292
+ const data = objectValue(result);
1293
+ if (data !== void 0 && metadata?.reasoningMandatory) {
1294
+ const reasoning = objectValue(data.reasoning);
1295
+ if (reasoning?.effort === void 0 || reasoning.effort === "none") data.reasoning = {
1296
+ ...reasoning,
1297
+ effort: defaultEffort ?? "high"
1298
+ };
1299
+ }
1300
+ if (!protectedAtStart && catalog?.protects(model.id) !== true) return result;
1301
+ const latest = catalog?.model(model.id);
1302
+ if (catalog?.models() !== void 0 && latest?.priceStatus !== "free") throw new LlmError(FREE_ONLY_MESSAGE, "OPENROUTER_FREE_ONLY");
1303
+ return protectOpenRouterPayload(result, model.id);
1304
+ }
1305
+ };
1306
+ }
1307
+ //#endregion
1308
+ //#region src/provider.ts
1309
+ /** Installed pi-ai providers remapped onto independent harness routes. */
1310
+ function harnessApiKeyAuth(name) {
1311
+ return {
1312
+ name,
1313
+ resolve: ({ credential }) => Promise.resolve({
1314
+ auth: credential?.key === void 0 ? {} : { apiKey: credential.key },
1315
+ source: name
1316
+ })
1317
+ };
1318
+ }
1319
+ function catalogProvider(id) {
1320
+ const base = builtinProviders().find((candidate) => candidate.id === id);
1321
+ if (base === void 0) throw new Error(`dsh-oauth-login: the installed pi-ai catalog ships no "${id}" provider`);
1322
+ return base;
1323
+ }
1324
+ /**
1325
+ * Catalog models plus plugin-owned extras, remapped onto the harness route.
1326
+ * Extras fill gaps the installed pi-ai version has not shipped yet (e.g. grok-4.6).
1327
+ */
1328
+ /**
1329
+ * Prefer the extra's effort map when the installed catalog still lacks one.
1330
+ * Official OpenRouter generation currently sets `reasoning: true` and no map.
1331
+ */
1332
+ function overlayExtraModel(model, extra) {
1333
+ const transport = model.provider === "xai" && model.id === "grok-4.6" && extra.api === "openai-responses" ? {
1334
+ api: extra.api,
1335
+ compat: extra.compat
1336
+ } : {};
1337
+ const reasoning = extra.thinkingLevelMap !== void 0 && model.thinkingLevelMap === void 0 ? {
1338
+ reasoning: extra.reasoning,
1339
+ thinkingLevelMap: extra.thinkingLevelMap
1340
+ } : {};
1341
+ if (Object.keys(transport).length === 0 && Object.keys(reasoning).length === 0) return model;
1342
+ return {
1343
+ ...model,
1344
+ ...transport,
1345
+ ...reasoning
1346
+ };
1347
+ }
1348
+ function harnessModels(spec, catalog) {
1349
+ const base = catalogProvider(spec.id).getModels();
1350
+ const extras = extraModelsFor(spec.id);
1351
+ const extraById = new Map(extras.map((model) => [model.id, model]));
1352
+ const seen = /* @__PURE__ */ new Set();
1353
+ const merged = [];
1354
+ for (const model of base) {
1355
+ seen.add(model.id);
1356
+ const extra = extraById.get(model.id);
1357
+ merged.push(extra === void 0 ? model : overlayExtraModel(model, extra));
1358
+ }
1359
+ for (const extra of extras) {
1360
+ if (seen.has(extra.id)) continue;
1361
+ seen.add(extra.id);
1362
+ merged.push(extra);
1363
+ }
1364
+ const live = spec.id === "openrouter" ? catalog?.models() : void 0;
1365
+ const byId = new Map(merged.map((model) => [model.id, model]));
1366
+ return (live === void 0 ? merged : live.map((info) => openRouterPiModel(info, byId.get(info.id)))).map((model) => model.provider === spec.route ? model : {
1367
+ ...model,
1368
+ provider: spec.route
1369
+ });
1370
+ }
1371
+ function harnessProvider(spec, native = DEFAULT_NATIVE_TOOL_POLICY, catalog) {
1372
+ const base = catalogProvider(spec.id);
1373
+ return {
1374
+ id: spec.route,
1375
+ name: spec.displayName,
1376
+ ...base.baseUrl === void 0 ? {} : { baseUrl: base.baseUrl },
1377
+ auth: {
1378
+ ...base.auth,
1379
+ apiKey: harnessApiKeyAuth(spec.displayName)
1380
+ },
1381
+ getModels: () => harnessModels(spec, catalog),
1382
+ stream: (model, context, options) => {
1383
+ const request = prepareNativeToolRequest(context, options ?? {}, spec.id, model.api, native);
1384
+ const guarded = spec.id === "openrouter" ? prepareOpenRouterOptions(model, request.options, catalog) : request.options;
1385
+ return base.stream(model, request.context, guarded);
1386
+ },
1387
+ streamSimple: (model, context, options) => {
1388
+ const request = prepareNativeToolRequest(context, options ?? {}, spec.id, model.api, native);
1389
+ const guarded = spec.id === "openrouter" ? prepareOpenRouterOptions(model, request.options, catalog) : request.options;
1390
+ return base.streamSimple(model, request.context, guarded);
1391
+ }
1392
+ };
1393
+ }
1394
+ function allCatalogProviders() {
1395
+ return PI_LOGIN_PROVIDERS.map((spec) => catalogProvider(spec.id));
1396
+ }
1397
+ //#endregion
1398
+ //#region src/store.ts
1399
+ /**
1400
+ * Multi-provider subscription credential store. File is $DSH_HOME/.dsh-oauth-auth.json.
1401
+ * The old .pi-login-auth.json name is read only as a DSH-owned migration source.
1402
+ * Never ~/.codex, ~/.grok, ~/.claude, or ~/.pi/agent/auth.json.
1403
+ */
1404
+ const AUTH_FORMAT_VERSION = 1;
1405
+ const OAUTH_ALLOWED_FIELDS = /* @__PURE__ */ new Set([
1406
+ "type",
1407
+ "access",
1408
+ "refresh",
1409
+ "expires",
1410
+ "accountId",
1411
+ "enterpriseUrl",
1412
+ "availableModelIds"
1413
+ ]);
1414
+ const API_KEY_ALLOWED_FIELDS = /* @__PURE__ */ new Set([
1415
+ "type",
1416
+ "key",
1417
+ "env"
1418
+ ]);
1419
+ function isENOENT(error) {
1420
+ return error?.code === "ENOENT";
1421
+ }
1422
+ async function assertOwnerOnly(filename) {
1423
+ let mode;
1424
+ try {
1425
+ mode = (await stat(filename)).mode;
1426
+ } catch (error) {
1427
+ if (isENOENT(error)) return;
1428
+ throw error;
1429
+ }
1430
+ if (process.platform === "win32") return;
1431
+ 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`);
1432
+ }
1433
+ function parseProviderEnv(raw, filename, providerId) {
1434
+ if (raw === void 0) return void 0;
1435
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) throw new Error(`pi-login: ${filename} credential for ${providerId} env must be an object`);
1436
+ const env = {};
1437
+ for (const [key, value] of Object.entries(raw)) {
1438
+ if (typeof value !== "string") throw new Error(`pi-login: ${filename} credential for ${providerId} env values must be strings`);
1439
+ env[key] = value;
1440
+ }
1441
+ return env;
1442
+ }
1443
+ function parseCredential(raw, filename, providerId) {
1444
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) throw new Error(`pi-login: ${filename} credential for ${providerId} must be an object`);
1445
+ const credential = raw;
1446
+ const type = credential["type"];
1447
+ const allowed = type === "oauth" ? OAUTH_ALLOWED_FIELDS : type === "api_key" ? API_KEY_ALLOWED_FIELDS : void 0;
1448
+ if (allowed === void 0) throw new Error(`pi-login: ${filename} credential for ${providerId} type must be oauth or api_key`);
1449
+ if (Object.keys(credential).some((key) => !allowed.has(key))) throw new Error(`pi-login: ${filename} credential for ${providerId} contains an unknown field`);
1450
+ if (type === "api_key") {
1451
+ 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`);
1452
+ const env = parseProviderEnv(credential["env"], filename, providerId);
1453
+ return {
1454
+ type: "api_key",
1455
+ key: credential["key"],
1456
+ ...env === void 0 ? {} : { env }
1457
+ };
1458
+ }
1459
+ 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`);
1460
+ if (typeof credential["refresh"] !== "string") throw new Error(`pi-login: ${filename} credential for ${providerId} refresh must be a string`);
1461
+ 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`);
1462
+ return credential;
1463
+ }
1464
+ function parseDocument(text, filename) {
1465
+ let value;
1466
+ try {
1467
+ value = JSON.parse(text);
1468
+ } catch {
1469
+ throw new Error(`pi-login: ${filename} is not valid JSON`);
1470
+ }
1471
+ if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`pi-login: ${filename} must contain an object`);
1472
+ const document = value;
1473
+ if (document["version"] !== AUTH_FORMAT_VERSION) throw new Error(`pi-login: ${filename} has unsupported auth format version ${String(document["version"])}`);
1474
+ if (Object.keys(document).some((key) => key !== "version" && key !== "credentials")) throw new Error(`pi-login: ${filename} contains an unknown top-level field`);
1475
+ const raw = document["credentials"];
1476
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) throw new Error(`pi-login: ${filename} credentials must be an object`);
1477
+ const owned = new Set(PI_LOGIN_PROVIDERS.map((provider) => provider.id));
1478
+ const credentials = {};
1479
+ for (const [providerId, entry] of Object.entries(raw)) {
1480
+ if (!owned.has(providerId)) throw new Error(`pi-login: ${filename} contains an unknown provider "${providerId}"`);
1481
+ credentials[providerId] = parseCredential(entry, filename, providerId);
1482
+ }
1483
+ return {
1484
+ version: AUTH_FORMAT_VERSION,
1485
+ credentials
1486
+ };
1487
+ }
1488
+ function cloneCredential(credential) {
1489
+ return structuredClone(credential);
1490
+ }
1491
+ function piLoginAuthPath(dshHome) {
1492
+ return resolve(join(resolveDshHome(dshHome), PI_LOGIN_AUTH_FILENAME));
1493
+ }
1494
+ var PiLoginCredentialStore = class {
1495
+ filename;
1496
+ legacyFilename;
1497
+ constructor(filename = piLoginAuthPath()) {
1498
+ this.filename = resolve(filename);
1499
+ this.legacyFilename = basename(this.filename) === ".dsh-oauth-auth.json" ? resolve(join(dirname(this.filename), LEGACY_PI_LOGIN_AUTH_FILENAME)) : void 0;
1500
+ }
1501
+ async readDocument() {
1502
+ const candidates = [this.filename, ...this.legacyFilename === void 0 ? [] : [this.legacyFilename]];
1503
+ for (const filename of candidates) {
1504
+ await assertOwnerOnly(filename);
1505
+ try {
1506
+ return parseDocument(await readFile(filename, "utf8"), filename);
1507
+ } catch (error) {
1508
+ if (isENOENT(error)) continue;
1509
+ throw error;
1510
+ }
1511
+ }
1512
+ return {
1513
+ version: AUTH_FORMAT_VERSION,
1514
+ credentials: {}
1515
+ };
1516
+ }
1517
+ async read(providerId, options) {
1518
+ options?.signal?.throwIfAborted();
1519
+ const credential = (await this.readDocument()).credentials[providerId];
1520
+ options?.signal?.throwIfAborted();
1521
+ return credential === void 0 ? void 0 : cloneCredential(credential);
1522
+ }
1523
+ async list(options) {
1524
+ options?.signal?.throwIfAborted();
1525
+ const document = await this.readDocument();
1526
+ options?.signal?.throwIfAborted();
1527
+ return Object.entries(document.credentials).map(([providerId, credential]) => ({
1528
+ providerId,
1529
+ type: credential.type
1530
+ }));
1531
+ }
1532
+ async modify(providerId, fn, options) {
1533
+ if (!PI_LOGIN_PROVIDERS.some((provider) => provider.id === providerId)) throw new Error(`pi-login: credential store does not own provider "${providerId}"`);
1534
+ options?.signal?.throwIfAborted();
1535
+ await mkdir(dirname(this.filename), {
1536
+ recursive: true,
1537
+ mode: 448
1538
+ });
1539
+ options?.signal?.throwIfAborted();
1540
+ return withFileLock(this.filename, async () => {
1541
+ options?.signal?.throwIfAborted();
1542
+ const document = await this.readDocument();
1543
+ options?.signal?.throwIfAborted();
1544
+ const current = document.credentials[providerId];
1545
+ const candidate = await fn(current === void 0 ? void 0 : cloneCredential(current));
1546
+ options?.signal?.throwIfAborted();
1547
+ if (candidate === void 0) return current === void 0 ? void 0 : cloneCredential(current);
1548
+ const next = parseCredential(candidate, this.filename, providerId);
1549
+ const credentials = {
1550
+ ...document.credentials,
1551
+ [providerId]: next
1552
+ };
1553
+ await writeFileAtomic(this.filename, `${JSON.stringify({
1554
+ version: AUTH_FORMAT_VERSION,
1555
+ credentials
1556
+ }, null, 2)}\n`, {
1557
+ mode: 384,
1558
+ dirMode: 448
1559
+ });
1560
+ return cloneCredential(next);
1561
+ });
1562
+ }
1563
+ async delete(providerId, options) {
1564
+ if (!PI_LOGIN_PROVIDERS.some((provider) => provider.id === providerId)) return;
1565
+ options?.signal?.throwIfAborted();
1566
+ await mkdir(dirname(this.filename), {
1567
+ recursive: true,
1568
+ mode: 448
1569
+ });
1570
+ options?.signal?.throwIfAborted();
1571
+ await withFileLock(this.filename, async () => {
1572
+ options?.signal?.throwIfAborted();
1573
+ const document = await this.readDocument();
1574
+ options?.signal?.throwIfAborted();
1575
+ if (document.credentials[providerId] === void 0) return;
1576
+ const { [providerId]: _removed, ...credentials } = document.credentials;
1577
+ if (Object.keys(credentials).length === 0) {
1578
+ await rm(this.filename, { force: true });
1579
+ return;
1580
+ }
1581
+ await writeFileAtomic(this.filename, `${JSON.stringify({
1582
+ version: AUTH_FORMAT_VERSION,
1583
+ credentials
1584
+ }, null, 2)}\n`, {
1585
+ mode: 384,
1586
+ dirMode: 448
1587
+ });
1588
+ });
1589
+ }
1590
+ };
1591
+ //#endregion
1592
+ //#region src/auth.ts
1593
+ async function piLoginStatus(store = new PiLoginCredentialStore(), providerId) {
1594
+ const ids = providerId === void 0 ? (await store.list()).map((item) => item.providerId) : [providerId];
1595
+ const out = [];
1596
+ for (const id of ids) {
1597
+ const credential = await store.read(id);
1598
+ if (credential?.type === "oauth") {
1599
+ out.push({
1600
+ providerId: id,
1601
+ authenticated: true,
1602
+ credentialType: "oauth",
1603
+ expiresAt: new Date(credential.expires)
1604
+ });
1605
+ continue;
1606
+ }
1607
+ if (credential?.type === "api_key" && typeof credential.key === "string" && credential.key.length > 0) {
1608
+ out.push({
1609
+ providerId: id,
1610
+ authenticated: true,
1611
+ credentialType: "api_key"
1612
+ });
1613
+ continue;
1614
+ }
1615
+ out.push({
1616
+ providerId: id,
1617
+ authenticated: false
1618
+ });
1619
+ }
1620
+ return out;
1621
+ }
1622
+ async function loginPiProviderSession(providerId, interaction, session) {
1623
+ const spec = requirePiLoginProvider(providerId);
1624
+ await session.ensureTransport();
1625
+ session.models.setProvider(catalogProvider(providerId));
1626
+ await session.proxy.run(() => session.models.login(providerId, spec.authType, interaction));
1627
+ if (providerId === "openrouter") await session.openRouter.syncAuthentication("login");
1628
+ }
1629
+ //#endregion
1630
+ //#region src/redact.ts
1631
+ /** Remove token-like strings from an external OAuth diagnostic. */
1632
+ function safeMessage(error) {
1633
+ 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);
1634
+ }
1635
+ /** Only this provider's official HTTPS hosts may be opened for login. */
1636
+ function isSafeAuthUrl(raw, provider) {
1637
+ try {
1638
+ const url = new URL(raw);
1639
+ if (url.protocol !== "https:") return false;
1640
+ const host = url.hostname.toLowerCase();
1641
+ if (provider.allowedHosts.includes(host)) return true;
1642
+ return provider.allowedSuffixes.some((suffix) => host.endsWith(suffix));
1643
+ } catch {
1644
+ return false;
1645
+ }
1646
+ }
1647
+ //#endregion
1648
+ //#region src/oauth-refresh.ts
1649
+ /**
1650
+ * When to spend a refresh_token. Chat already refreshes via Models.getAuth;
1651
+ * this helper is for status / boot / the host timer so the store does not sit
1652
+ * on a stale expires stamp.
1653
+ */
1654
+ /** Refresh when the stored access stamp is inside this window (Pi already skews expires by 5 min). */
1655
+ const OAUTH_REFRESH_SOON_MS = 9e5;
1656
+ const lastAttempt = /* @__PURE__ */ new Map();
1657
+ function grantNeedsRefresh(expires, now = Date.now()) {
1658
+ return now >= expires - OAUTH_REFRESH_SOON_MS;
1659
+ }
1660
+ function refreshAttemptKey(storeId, providerId) {
1661
+ return `${storeId}\0${providerId}`;
1662
+ }
1663
+ function refreshOnCooldown(key, now = Date.now()) {
1664
+ const previous = lastAttempt.get(key);
1665
+ return previous !== void 0 && now - previous < 3e5;
1666
+ }
1667
+ function markRefreshAttempt(key, now = Date.now()) {
1668
+ lastAttempt.set(key, now);
1669
+ }
1670
+ async function refreshGrant(getAuth, providerId) {
1671
+ try {
1672
+ await getAuth(providerId);
1673
+ return "ok";
1674
+ } catch {
1675
+ return "failed";
1676
+ }
1677
+ }
1678
+ //#endregion
1679
+ //#region src/openrouter-catalog.ts
1680
+ /** One Host-owned cache/refresh loop. It never receives an OAuth token. */
1681
+ const OPENROUTER_MODELS_URL = "https://openrouter.ai/api/v1/models";
1682
+ const OPENROUTER_CACHE_FILENAME = ".dsh-oauth-openrouter-models.json";
1683
+ const MAX_BYTES = 12582912;
1684
+ const FETCH_TIMEOUT_MS = 15e3;
1685
+ var OpenRouterCatalog = class {
1686
+ options;
1687
+ entries;
1688
+ protectedIds;
1689
+ connected = false;
1690
+ disposed = false;
1691
+ source = "builtin";
1692
+ lastUpdatedAt = null;
1693
+ lastAttemptAt = null;
1694
+ nextRefreshAt = null;
1695
+ error = null;
1696
+ hydration;
1697
+ pending;
1698
+ controller;
1699
+ timer;
1700
+ generation = 0;
1701
+ listeners = /* @__PURE__ */ new Set();
1702
+ now;
1703
+ constructor(options) {
1704
+ this.options = options;
1705
+ this.now = options.now ?? Date.now;
1706
+ this.protectedIds = new Set(options.initiallyProtectedIds ?? []);
1707
+ }
1708
+ models() {
1709
+ return this.entries;
1710
+ }
1711
+ model(id) {
1712
+ return this.entries?.find((model) => model.id === id);
1713
+ }
1714
+ protects(id) {
1715
+ return id.endsWith(":free") || id === "openrouter/free" || this.protectedIds.has(id);
1716
+ }
1717
+ subscribe(listener) {
1718
+ this.listeners.add(listener);
1719
+ return () => {
1720
+ this.listeners.delete(listener);
1721
+ };
1722
+ }
1723
+ changed() {
1724
+ for (const listener of this.listeners) try {
1725
+ listener();
1726
+ } catch {}
1727
+ }
1728
+ snapshot() {
1729
+ return {
1730
+ version: 1,
1731
+ connected: this.connected,
1732
+ refreshing: this.pending !== void 0,
1733
+ source: this.source,
1734
+ lastUpdatedAt: this.lastUpdatedAt,
1735
+ lastAttemptAt: this.lastAttemptAt,
1736
+ nextRefreshAt: this.nextRefreshAt,
1737
+ retryAt: this.lastAttemptAt === null ? null : this.lastAttemptAt + OPENROUTER_COOLDOWN_MS,
1738
+ stale: this.error !== null || this.lastUpdatedAt === null || this.now() - this.lastUpdatedAt >= 9e5,
1739
+ error: this.error,
1740
+ models: this.connected ? (this.entries ?? []).map((model) => ({
1741
+ ...model,
1742
+ freeOnly: this.protects(model.id)
1743
+ })) : []
1744
+ };
1745
+ }
1746
+ hydrate() {
1747
+ this.hydration ??= this.readCache();
1748
+ return this.hydration;
1749
+ }
1750
+ async readCache() {
1751
+ try {
1752
+ if ((await stat(this.options.filename)).size > MAX_BYTES) throw new Error("cache too large");
1753
+ const raw = objectValue(JSON.parse(await readFile(this.options.filename, "utf8")));
1754
+ 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");
1755
+ const entries = normalizeOpenRouterModels(raw.data);
1756
+ if (this.disposed) return;
1757
+ this.entries = entries;
1758
+ this.lastUpdatedAt = raw.lastUpdatedAt;
1759
+ this.source = "cache";
1760
+ if (Array.isArray(raw.protectedIds)) {
1761
+ for (const id of raw.protectedIds) if (validModelId(id)) this.protectedIds.add(id);
1762
+ }
1763
+ this.rememberFree(entries);
1764
+ this.changed();
1765
+ } catch (error) {
1766
+ if (error?.code !== "ENOENT") this.error = "cache";
1767
+ }
1768
+ }
1769
+ rememberFree(entries) {
1770
+ for (const model of entries) if (model.priceStatus === "free") this.protectedIds.add(model.id);
1771
+ }
1772
+ /** Startup reads disk first; only a newly completed login forces a refresh. */
1773
+ async syncAuthentication(reason = "check") {
1774
+ await this.hydrate();
1775
+ const generation = this.generation;
1776
+ const authenticated = await this.options.isAuthenticated();
1777
+ if (this.disposed || generation !== this.generation) return;
1778
+ if (!authenticated) {
1779
+ this.disconnect();
1780
+ return;
1781
+ }
1782
+ this.connected = true;
1783
+ if (reason === "login" || this.snapshot().stale) this.refresh(reason === "login");
1784
+ else if (this.timer === void 0 && this.pending === void 0) this.schedule(Math.max(1, OPENROUTER_REFRESH_MS - (this.now() - this.lastUpdatedAt)));
1785
+ }
1786
+ /** Coalesced across routes/windows; even forced refreshes honor 60s cooldown. */
1787
+ refresh(force = false) {
1788
+ if (this.pending !== void 0) return this.pending;
1789
+ if (!this.connected || this.disposed) return Promise.resolve();
1790
+ if (!force && !this.snapshot().stale) return Promise.resolve();
1791
+ if (this.lastAttemptAt !== null && this.now() - this.lastAttemptAt < 6e4) return Promise.resolve();
1792
+ const generation = this.generation;
1793
+ this.lastAttemptAt = this.now();
1794
+ this.controller = new AbortController();
1795
+ const signal = AbortSignal.any([this.controller.signal, AbortSignal.timeout(FETCH_TIMEOUT_MS)]);
1796
+ const pending = this.fetchCatalog(signal, generation).finally(() => {
1797
+ if (this.pending !== pending) return;
1798
+ this.pending = void 0;
1799
+ this.controller = void 0;
1800
+ if (this.connected && !this.disposed) this.schedule(OPENROUTER_REFRESH_MS);
1801
+ });
1802
+ this.pending = pending;
1803
+ return pending;
1804
+ }
1805
+ async fetchCatalog(signal, generation) {
1806
+ try {
1807
+ if (!await this.options.isAuthenticated()) {
1808
+ this.disconnect();
1809
+ return;
1810
+ }
1811
+ await this.options.beforeFetch?.();
1812
+ signal.throwIfAborted();
1813
+ const response = await (this.options.fetch ?? globalThis.fetch)(OPENROUTER_MODELS_URL, {
1814
+ method: "GET",
1815
+ headers: { accept: "application/json" },
1816
+ signal,
1817
+ redirect: "error",
1818
+ credentials: "omit"
1819
+ });
1820
+ if (!response.ok || response.body === null) throw new Error("catalog unavailable");
1821
+ const reader = response.body.getReader();
1822
+ const chunks = [];
1823
+ let size = 0;
1824
+ try {
1825
+ while (true) {
1826
+ const { done, value } = await reader.read();
1827
+ if (done) break;
1828
+ size += value.byteLength;
1829
+ if (size > MAX_BYTES) throw new Error("catalog too large");
1830
+ chunks.push(value);
1831
+ }
1832
+ } finally {
1833
+ await reader.cancel().catch(() => void 0);
1834
+ reader.releaseLock();
1835
+ }
1836
+ const raw = objectValue(JSON.parse(Buffer.concat(chunks).toString("utf8")));
1837
+ const links = objectValue(raw?.links);
1838
+ 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");
1839
+ const entries = normalizeOpenRouterModels(raw.data);
1840
+ if (this.disposed || !this.connected || generation !== this.generation || signal.aborted) return;
1841
+ const fetchedAt = this.now();
1842
+ const protectedIds = new Set(this.protectedIds);
1843
+ for (const model of entries) if (model.priceStatus === "free") protectedIds.add(model.id);
1844
+ try {
1845
+ await withFileLock(this.options.filename, async () => {
1846
+ try {
1847
+ const cached = objectValue(JSON.parse(await readFile(this.options.filename, "utf8")));
1848
+ if (Array.isArray(cached?.protectedIds)) {
1849
+ for (const id of cached.protectedIds) if (validModelId(id)) protectedIds.add(id);
1850
+ }
1851
+ } catch {}
1852
+ await writeFileAtomic(this.options.filename, JSON.stringify({
1853
+ version: 1,
1854
+ lastUpdatedAt: fetchedAt,
1855
+ protectedIds: [...protectedIds],
1856
+ data: raw.data
1857
+ }) + "\n", {
1858
+ mode: 384,
1859
+ dirMode: 448
1860
+ });
1861
+ });
1862
+ } catch {
1863
+ this.error = "save";
1864
+ return;
1865
+ }
1866
+ if (this.disposed || !this.connected || generation !== this.generation || signal.aborted) return;
1867
+ this.entries = entries;
1868
+ this.protectedIds = protectedIds;
1869
+ this.lastUpdatedAt = fetchedAt;
1870
+ this.source = "live";
1871
+ this.error = null;
1872
+ this.changed();
1873
+ } catch {
1874
+ if (!this.disposed && this.connected && generation === this.generation) this.error = "fetch";
1875
+ }
1876
+ }
1877
+ schedule(delay) {
1878
+ clearTimeout(this.timer);
1879
+ this.nextRefreshAt = this.now() + delay;
1880
+ this.timer = setTimeout(() => {
1881
+ this.timer = void 0;
1882
+ this.nextRefreshAt = null;
1883
+ this.syncAuthentication().then(() => this.refresh()).catch(() => {
1884
+ if (this.connected && !this.disposed) this.schedule(OPENROUTER_REFRESH_MS);
1885
+ });
1886
+ }, delay);
1887
+ this.timer.unref?.();
1888
+ }
1889
+ disconnect() {
1890
+ this.generation += 1;
1891
+ this.connected = false;
1892
+ clearTimeout(this.timer);
1893
+ this.timer = void 0;
1894
+ this.nextRefreshAt = null;
1895
+ this.controller?.abort();
1896
+ this.controller = void 0;
1897
+ this.pending = void 0;
1898
+ }
1899
+ dispose() {
1900
+ this.disposed = true;
1901
+ this.disconnect();
1902
+ this.listeners.clear();
1903
+ }
1904
+ };
1905
+ //#endregion
1906
+ //#region src/session.ts
1907
+ /** Shared OAuth store + catalog for the host plugin and CLI. */
1908
+ var PiLoginSession = class {
1909
+ store;
1910
+ models;
1911
+ native;
1912
+ openRouter;
1913
+ proxy;
1914
+ constructor(store = new PiLoginCredentialStore(), native = DEFAULT_NATIVE_TOOL_POLICY, catalogOptions = {}, proxyDiscovery = {}) {
1915
+ this.store = store;
1916
+ this.native = native;
1917
+ this.proxy = new OAuthProxyTransport(join(dirname(store.filename), PROXY_SETTINGS_FILENAME), proxyDiscovery);
1918
+ this.models = createModels({ credentials: store });
1919
+ for (const provider of allCatalogProviders()) this.models.setProvider(provider);
1920
+ this.openRouter = new OpenRouterCatalog({
1921
+ filename: join(dirname(store.filename), OPENROUTER_CACHE_FILENAME),
1922
+ isAuthenticated: async () => (await store.list()).some((item) => item.providerId === "openrouter"),
1923
+ beforeFetch: () => this.ensureTransport(),
1924
+ fetch: (input, init) => this.proxy.run(() => fetch(input, init)),
1925
+ initiallyProtectedIds: harnessModels(this.spec("openrouter")).filter((model) => model.cost.input === 0 && model.cost.output === 0).map((model) => model.id),
1926
+ ...catalogOptions
1927
+ });
1928
+ }
1929
+ ensureTransport() {
1930
+ return this.proxy.initialize();
1931
+ }
1932
+ spec(id) {
1933
+ const spec = piLoginProvider(id);
1934
+ if (spec === void 0) throw new Error(`dsh-oauth-login: unknown provider "${id}"`);
1935
+ return spec;
1936
+ }
1937
+ provider(id) {
1938
+ return harnessProvider(this.spec(id), this.native, this.openRouter);
1939
+ }
1940
+ visibleModels(id) {
1941
+ return this.provider(id).getModels();
1942
+ }
1943
+ /**
1944
+ * Harness routes that currently hold a stored OAuth grant.
1945
+ * Model pickers should only advertise these — logging out must drop the route.
1946
+ */
1947
+ async authenticatedRoutes() {
1948
+ const signedIn = new Set((await this.store.list()).map((item) => item.providerId));
1949
+ return PI_LOGIN_PROVIDERS.filter((provider) => signedIn.has(provider.id)).map((provider) => provider.route);
1950
+ }
1951
+ async logout(id) {
1952
+ await this.store.delete(id);
1953
+ if (id === "openrouter") this.openRouter.disconnect();
1954
+ }
1955
+ /**
1956
+ * Renew access tokens that are expired or close to expiry.
1957
+ * Failures stay in the store; the next poll or chat retries.
1958
+ */
1959
+ async refreshStoredGrants(now = Date.now()) {
1960
+ await this.ensureTransport();
1961
+ for (const { providerId } of await this.store.list()) {
1962
+ const credential = await this.store.read(providerId);
1963
+ if (credential?.type !== "oauth" || !grantNeedsRefresh(credential.expires, now)) continue;
1964
+ const key = refreshAttemptKey(this.store.filename, providerId);
1965
+ if (refreshOnCooldown(key, now)) continue;
1966
+ markRefreshAttempt(key, now);
1967
+ await this.proxy.run(() => refreshGrant((id) => this.models.getAuth(id), providerId));
1968
+ }
1969
+ }
1970
+ };
1971
+ z.object({
1972
+ streamIdleTimeoutMs: z.number().min(1).default(PI_LOGIN_STREAM_IDLE_TIMEOUT_MS),
1973
+ retryPolicy: RetryPolicySchema,
1974
+ nativeTools: z.boolean().default(true),
1975
+ nativeImage: z.boolean().default(true)
1976
+ });
1977
+ //#endregion
1978
+ //#region src/bin.ts
1979
+ /** Standalone credential CLI. Never reads or writes official CLI auth files. */
1980
+ function openBrowser(rawUrl, providerId) {
1981
+ const spec = requirePiLoginProvider(providerId);
1982
+ if (!isSafeAuthUrl(rawUrl, spec)) throw new Error(`refusing to open authorization URL outside ${spec.displayName} official hosts`);
1983
+ const url = new URL(rawUrl);
1984
+ const command = process.platform === "win32" ? {
1985
+ file: "rundll32.exe",
1986
+ args: ["url.dll,FileProtocolHandler", url.href]
1987
+ } : process.platform === "darwin" ? {
1988
+ file: "open",
1989
+ args: [url.href]
1990
+ } : {
1991
+ file: "xdg-open",
1992
+ args: [url.href]
1993
+ };
1994
+ try {
1995
+ const child = spawn(command.file, command.args, {
1996
+ detached: true,
1997
+ stdio: "ignore",
1998
+ windowsHide: true
1999
+ });
2000
+ child.on("error", () => {});
2001
+ child.unref();
2002
+ } catch {}
2003
+ }
2004
+ function notify(event, providerId, useBrowser) {
2005
+ switch (event.type) {
2006
+ case "auth_url":
2007
+ process.stdout.write(`Open this URL to sign in:\n${event.url}\n`);
2008
+ if (event.instructions !== void 0) process.stdout.write(`${event.instructions}\n`);
2009
+ if (useBrowser) openBrowser(event.url, providerId);
2010
+ break;
2011
+ case "device_code":
2012
+ process.stdout.write(`Open this URL to sign in:\n${event.verificationUri}\n`);
2013
+ if (event.userCode.length > 0) process.stdout.write(`Enter code: ${event.userCode}\n`);
2014
+ if (useBrowser) openBrowser(event.verificationUri, providerId);
2015
+ break;
2016
+ case "info":
2017
+ case "progress": process.stdout.write(`${event.message}\n`);
2018
+ }
2019
+ }
2020
+ async function answerPrompt(prompt, question) {
2021
+ if (prompt.type === "select") {
2022
+ const oauth = prompt.options.find((option) => option.id === "oauth" || option.id.includes("oauth"));
2023
+ const browser = prompt.options.find((option) => option.id.includes("browser"));
2024
+ return oauth?.id ?? browser?.id ?? prompt.options[0]?.id ?? "oauth";
2025
+ }
2026
+ const suffix = prompt.placeholder === void 0 ? "" : ` (${prompt.placeholder})`;
2027
+ return question(`${prompt.message}${suffix}: `, { ...prompt.signal === void 0 ? {} : { signal: prompt.signal } });
2028
+ }
2029
+ function printHelp() {
2030
+ const ids = PI_LOGIN_PROVIDERS.map((provider) => ` ${provider.id.padEnd(18)} ${provider.displayName}`).join("\n");
2031
+ process.stdout.write([
2032
+ "Usage: dsh-oauth-login <login|logout|status> [provider]",
2033
+ "",
2034
+ " login [provider] Subscription OAuth or official Plan API key",
2035
+ " logout [provider] remove a dsh credential (or all if omitted)",
2036
+ " status [provider] report non-secret credential state",
2037
+ "",
2038
+ "Providers:",
2039
+ ids,
2040
+ ""
2041
+ ].join("\n"));
2042
+ }
2043
+ function resolveIds(raw) {
2044
+ if (raw === void 0) return PI_LOGIN_PROVIDERS.map((provider) => provider.id);
2045
+ return [requirePiLoginProvider(raw).id];
2046
+ }
2047
+ async function run(argv) {
2048
+ if (argv.length === 0 || argv[0] === "--help" || argv[0] === "-h") {
2049
+ printHelp();
2050
+ return 0;
2051
+ }
2052
+ const [rawAction, rawProvider, ...rest] = argv;
2053
+ if (rawAction !== "login" && rawAction !== "logout" && rawAction !== "status") {
2054
+ process.stderr.write(`dsh-oauth-login: expected login, logout, or status; got ${JSON.stringify(rawAction)}\n`);
2055
+ return 1;
2056
+ }
2057
+ if (rest.length > 0) {
2058
+ process.stderr.write(`dsh-oauth-login: unexpected extra arguments: ${rest.join(" ")}\n`);
2059
+ return 1;
2060
+ }
2061
+ const action = rawAction;
2062
+ try {
2063
+ const session = new PiLoginSession();
2064
+ switch (action) {
2065
+ case "status": {
2066
+ const ids = resolveIds(rawProvider);
2067
+ let failed = false;
2068
+ process.stdout.write(`store: ${piLoginAuthPath()}\n`);
2069
+ await session.refreshStoredGrants();
2070
+ for (const id of ids) {
2071
+ const [status] = await piLoginStatus(session.store, id);
2072
+ const spec = requirePiLoginProvider(id);
2073
+ if (status === void 0 || !status.authenticated) {
2074
+ process.stdout.write(`${spec.displayName}: signed out\n`);
2075
+ failed = true;
2076
+ continue;
2077
+ }
2078
+ const expires = status.expiresAt;
2079
+ const suffix = expires === void 0 || Number.isNaN(expires.valueOf()) ? "" : `; access expires ${expires.toISOString()}`;
2080
+ const models = session.visibleModels(id).map((model) => model.id).slice(0, 8).join(", ");
2081
+ process.stdout.write(`${spec.displayName}: signed in${suffix}\n`);
2082
+ process.stdout.write(` route: ${spec.route}\n`);
2083
+ process.stdout.write(` models: ${models}\n`);
2084
+ }
2085
+ return failed && rawProvider !== void 0 ? 1 : 0;
2086
+ }
2087
+ case "logout": {
2088
+ const ids = resolveIds(rawProvider);
2089
+ for (const id of ids) {
2090
+ await session.logout(id);
2091
+ process.stdout.write(`${requirePiLoginProvider(id).displayName}: signed out\n`);
2092
+ }
2093
+ process.stdout.write("Official CLI auth files were not touched.\n");
2094
+ return 0;
2095
+ }
2096
+ case "login": {
2097
+ if (rawProvider === void 0) {
2098
+ process.stderr.write("dsh-oauth-login: login requires a provider id (see --help)\n");
2099
+ return 1;
2100
+ }
2101
+ const id = requirePiLoginProvider(rawProvider).id;
2102
+ const spec = requirePiLoginProvider(id);
2103
+ if (spec.authType === "api_key" && spec.loginUrl !== void 0) {
2104
+ process.stdout.write(`Create or copy the Plan API key here:\n${spec.loginUrl}\n`);
2105
+ openBrowser(spec.loginUrl, id);
2106
+ }
2107
+ const readline = createInterface({
2108
+ input: process.stdin,
2109
+ output: process.stdout
2110
+ });
2111
+ try {
2112
+ await loginPiProviderSession(id, {
2113
+ prompt: (prompt) => answerPrompt(prompt, (text, options) => readline.question(text, options)),
2114
+ notify: (event) => notify(event, id, true)
2115
+ }, session);
2116
+ } finally {
2117
+ readline.close();
2118
+ }
2119
+ process.stdout.write(`${requirePiLoginProvider(id).displayName}: signed in\n`);
2120
+ process.stdout.write(`store: ${piLoginAuthPath()}\n`);
2121
+ process.stdout.write("Official CLI auth files were not read or written.\n");
2122
+ return 0;
2123
+ }
2124
+ }
2125
+ } catch (error) {
2126
+ process.stderr.write(`dsh-oauth-login: ${action} failed: ${safeMessage(error)}\n`);
2127
+ return 1;
2128
+ }
2129
+ }
2130
+ if (process.argv[1] !== void 0 && fileURLToPath(import.meta.url) === realpathSync(process.argv[1])) process.exitCode = await run(process.argv.slice(2));
2131
+ //#endregion
2132
+ export { run };