@goodandready/dsh-subscriptions 0.6.40 → 0.6.43
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/accounts.js +88 -53
- package/lib/adapter-manager.js +3 -2
- package/lib/adapter.js +90 -41
- package/lib/backoff.js +13 -1
- package/lib/client.js +64 -11
- package/lib/code-assist.js +1 -0
- package/lib/google-validation.js +1 -1
- package/lib/history.js +12 -0
- package/lib/http.js +90 -6
- package/lib/import-auth.js +91 -44
- package/lib/oauth.js +12 -0
- package/lib/routes/accounts.js +5 -4
- package/lib/routes/status.js +6 -8
- package/lib/runaway-guard.js +13 -1
- package/lib/session-pin.js +9 -4
- package/lib/sse.js +6 -1
- package/lib/stream-rotate.js +13 -2
- package/lib/vendor-factory.js +9 -7
- package/lib/vendors/claude-cli.js +2 -1
- package/lib/vendors/codex.js +4 -2
- package/lib/vendors/cody.js +1 -4
- package/lib/vendors/copilot.js +9 -4
- package/lib/vendors/cursor.js +1 -0
- package/lib/vendors/ernie.js +1 -5
- package/lib/vendors/glm.js +1 -0
- package/lib/vendors/jetbrains.js +1 -5
- package/lib/vendors/kimi.js +1 -0
- package/lib/vendors/kiro.js +1 -0
- package/lib/vendors/perplexity.js +1 -4
- package/lib/vendors/qwen.js +2 -5
- package/lib/vendors/replit.js +1 -4
- package/lib/vendors/spark.js +1 -5
- package/lib/wire.js +12 -11
- package/package.json +13 -8
package/lib/http.js
CHANGED
|
@@ -81,7 +81,7 @@ export function isTrustedSettingsRequest(request) {
|
|
|
81
81
|
if (secFetchSite === 'cross-site') return false
|
|
82
82
|
|
|
83
83
|
// 2. If origin header is present, it must match host
|
|
84
|
-
const host = headers.host || headers['x-forwarded-host']
|
|
84
|
+
const host = String(Array.isArray(headers.host) ? headers.host[0] : (headers.host || headers['x-forwarded-host'] || ''))
|
|
85
85
|
const origin = headers.origin
|
|
86
86
|
if (origin) {
|
|
87
87
|
try {
|
|
@@ -167,17 +167,36 @@ export function sanitizeRequestHeaders(url, init = {}, options = {}) {
|
|
|
167
167
|
delete headers.Cookie
|
|
168
168
|
}
|
|
169
169
|
}
|
|
170
|
-
} catch {
|
|
170
|
+
} catch (err) {
|
|
171
|
+
void err
|
|
172
|
+
return headers
|
|
173
|
+
}
|
|
171
174
|
return headers
|
|
172
175
|
}
|
|
173
176
|
|
|
174
|
-
export
|
|
177
|
+
export function isTimeoutError(err) {
|
|
178
|
+
if (!err) return false
|
|
179
|
+
return err.name === 'TimeoutError'
|
|
180
|
+
|| err.code === 'ETIMEDOUT'
|
|
181
|
+
|| (err.name === 'AbortError' && String(err.message || '').toLowerCase().includes('timeout'))
|
|
182
|
+
|| String(err.message || '').toLowerCase().includes('aborted due to timeout')
|
|
183
|
+
|| String(err.message || '').toLowerCase().includes('timed out')
|
|
184
|
+
|| String(err.message || '').toLowerCase().includes('operation was aborted')
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* @param {any} fetchImpl
|
|
189
|
+
* @param {string} url
|
|
190
|
+
* @param {any} [init]
|
|
191
|
+
* @param {{ timeoutMs?: number, allowedOrigins?: string[], stream?: boolean }} [options]
|
|
192
|
+
*/
|
|
193
|
+
export async function fetchWithTimeout(fetchImpl, url, init = {}, { timeoutMs = 30000, allowedOrigins, stream = false } = {}) {
|
|
175
194
|
const safeHeaders = sanitizeRequestHeaders(url, init, { allowedOrigins })
|
|
176
195
|
init = { ...init, headers: safeHeaders }
|
|
177
196
|
const impl = fetchImpl || fetch
|
|
178
197
|
if (!timeoutMs || timeoutMs <= 0) return impl(url, init)
|
|
179
198
|
const controller = new AbortController()
|
|
180
|
-
|
|
199
|
+
let timer = setTimeout(() => {
|
|
181
200
|
controller.abort(new DOMException('The operation was aborted due to timeout', 'TimeoutError'))
|
|
182
201
|
}, timeoutMs)
|
|
183
202
|
|
|
@@ -186,9 +205,74 @@ export async function fetchWithTimeout(fetchImpl, url, init = {}, { timeoutMs =
|
|
|
186
205
|
: controller.signal
|
|
187
206
|
|
|
188
207
|
try {
|
|
189
|
-
|
|
190
|
-
|
|
208
|
+
const res = await impl(url, { ...init, signal })
|
|
209
|
+
if (stream) {
|
|
210
|
+
clearTimeout(timer)
|
|
211
|
+
return res
|
|
212
|
+
}
|
|
213
|
+
const origJson = typeof res.json === 'function' ? res.json.bind(res) : null
|
|
214
|
+
const origText = typeof res.text === 'function' ? res.text.bind(res) : null
|
|
215
|
+
if (origJson) {
|
|
216
|
+
res.json = () => {
|
|
217
|
+
if (controller.signal.aborted) {
|
|
218
|
+
return Promise.reject(new DOMException('The operation was aborted due to timeout', 'TimeoutError'))
|
|
219
|
+
}
|
|
220
|
+
return new Promise((resolve, reject) => {
|
|
221
|
+
const onAbort = () => reject(new DOMException('The operation was aborted due to timeout', 'TimeoutError'))
|
|
222
|
+
controller.signal.addEventListener('abort', onAbort, { once: true })
|
|
223
|
+
origJson().then(
|
|
224
|
+
(val) => {
|
|
225
|
+
controller.signal.removeEventListener('abort', onAbort)
|
|
226
|
+
clearTimeout(timer)
|
|
227
|
+
resolve(val)
|
|
228
|
+
},
|
|
229
|
+
(err) => {
|
|
230
|
+
controller.signal.removeEventListener('abort', onAbort)
|
|
231
|
+
clearTimeout(timer)
|
|
232
|
+
if (controller.signal.aborted) {
|
|
233
|
+
reject(new DOMException('The operation was aborted due to timeout', 'TimeoutError'))
|
|
234
|
+
} else {
|
|
235
|
+
reject(err)
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
)
|
|
239
|
+
})
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
if (origText) {
|
|
243
|
+
res.text = () => {
|
|
244
|
+
if (controller.signal.aborted) {
|
|
245
|
+
return Promise.reject(new DOMException('The operation was aborted due to timeout', 'TimeoutError'))
|
|
246
|
+
}
|
|
247
|
+
return new Promise((resolve, reject) => {
|
|
248
|
+
const onAbort = () => reject(new DOMException('The operation was aborted due to timeout', 'TimeoutError'))
|
|
249
|
+
controller.signal.addEventListener('abort', onAbort, { once: true })
|
|
250
|
+
origText().then(
|
|
251
|
+
(val) => {
|
|
252
|
+
controller.signal.removeEventListener('abort', onAbort)
|
|
253
|
+
clearTimeout(timer)
|
|
254
|
+
resolve(val)
|
|
255
|
+
},
|
|
256
|
+
(err) => {
|
|
257
|
+
controller.signal.removeEventListener('abort', onAbort)
|
|
258
|
+
clearTimeout(timer)
|
|
259
|
+
if (controller.signal.aborted) {
|
|
260
|
+
reject(new DOMException('The operation was aborted due to timeout', 'TimeoutError'))
|
|
261
|
+
} else {
|
|
262
|
+
reject(err)
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
)
|
|
266
|
+
})
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return res
|
|
270
|
+
} catch (err) {
|
|
191
271
|
clearTimeout(timer)
|
|
272
|
+
if (controller.signal.aborted && !isTimeoutError(err)) {
|
|
273
|
+
throw new DOMException('The operation was aborted due to timeout', 'TimeoutError')
|
|
274
|
+
}
|
|
275
|
+
throw err
|
|
192
276
|
}
|
|
193
277
|
}
|
|
194
278
|
|
package/lib/import-auth.js
CHANGED
|
@@ -39,6 +39,7 @@ async function discoverLocalCliSessionsInner() {
|
|
|
39
39
|
const tok = codex.tokens || codex
|
|
40
40
|
detected.codex = {
|
|
41
41
|
provider: 'codex',
|
|
42
|
+
source: 'file',
|
|
42
43
|
path: homeFile('.codex', 'auth.json'),
|
|
43
44
|
email: codex.email || codex.account || '',
|
|
44
45
|
hasRefreshToken: !!(tok.refresh_token || tok.refreshToken),
|
|
@@ -54,6 +55,7 @@ async function discoverLocalCliSessionsInner() {
|
|
|
54
55
|
if (grok && (grok.access_token || grok.token || (grok.tokens && grok.tokens.access_token))) {
|
|
55
56
|
detected.grok = {
|
|
56
57
|
provider: 'grok',
|
|
58
|
+
source: 'file',
|
|
57
59
|
path: p,
|
|
58
60
|
email: grok.email || grok.account || '',
|
|
59
61
|
hasRefreshToken: !!(grok.refresh_token || (grok.tokens && grok.tokens.refresh_token)),
|
|
@@ -73,6 +75,7 @@ async function discoverLocalCliSessionsInner() {
|
|
|
73
75
|
if (tok && (tok.access_token || tok.accessToken)) {
|
|
74
76
|
detected.antigravity = {
|
|
75
77
|
provider: 'antigravity',
|
|
78
|
+
source: 'file',
|
|
76
79
|
path: p,
|
|
77
80
|
email: agy.email || tok.account || '',
|
|
78
81
|
hasRefreshToken: !!(tok.refresh_token || tok.refreshToken),
|
|
@@ -86,6 +89,7 @@ async function discoverLocalCliSessionsInner() {
|
|
|
86
89
|
if (kimi && (kimi.access_token || kimi.token)) {
|
|
87
90
|
detected.kimi = {
|
|
88
91
|
provider: 'kimi',
|
|
92
|
+
source: 'file',
|
|
89
93
|
path: homeFile('.kimi-code', 'credentials', 'kimi-code.json'),
|
|
90
94
|
email: kimi.email || kimi.account || '',
|
|
91
95
|
hasRefreshToken: !!(kimi.refresh_token || kimi.refreshToken),
|
|
@@ -103,6 +107,7 @@ async function discoverLocalCliSessionsInner() {
|
|
|
103
107
|
if (glm && (glm.apiKey || glm.api_key || (glm.provider && glm.provider.apiKey))) {
|
|
104
108
|
detected.glm = {
|
|
105
109
|
provider: 'glm',
|
|
110
|
+
source: 'file',
|
|
106
111
|
path: p,
|
|
107
112
|
email: 'ZCode CLI Account',
|
|
108
113
|
hasRefreshToken: false,
|
|
@@ -111,20 +116,31 @@ async function discoverLocalCliSessionsInner() {
|
|
|
111
116
|
}
|
|
112
117
|
}
|
|
113
118
|
|
|
114
|
-
// 6. Cursor Token (
|
|
119
|
+
// 6. Cursor Token (CLI config, auth.json, or env)
|
|
115
120
|
const cursorCliConfig = await readJson(homeFile('.cursor', 'cli-config.json'))
|
|
116
|
-
|
|
121
|
+
const cursorAuthConfig = await readJson(homeFile('.cursor', 'auth.json'))
|
|
122
|
+
if (cursorCliConfig && (cursorCliConfig.authInfo || cursorCliConfig.serverConfigCache)) {
|
|
117
123
|
detected.cursor = {
|
|
118
124
|
provider: 'cursor',
|
|
119
|
-
|
|
120
|
-
|
|
125
|
+
source: 'file',
|
|
126
|
+
path: homeFile('.cursor', 'cli-config.json'),
|
|
127
|
+
email: (cursorCliConfig.authInfo && cursorCliConfig.authInfo.email) || 'Cursor CLI User',
|
|
121
128
|
hasRefreshToken: false,
|
|
122
129
|
}
|
|
123
|
-
} else if (
|
|
130
|
+
} else if (cursorAuthConfig && (cursorAuthConfig.accessToken || cursorAuthConfig.access_token || (cursorAuthConfig.authInfo && cursorAuthConfig.authInfo.authId))) {
|
|
124
131
|
detected.cursor = {
|
|
125
132
|
provider: 'cursor',
|
|
126
|
-
|
|
127
|
-
|
|
133
|
+
source: 'file',
|
|
134
|
+
path: homeFile('.cursor', 'auth.json'),
|
|
135
|
+
email: (cursorAuthConfig.authInfo && cursorAuthConfig.authInfo.email) || cursorAuthConfig.email || 'Cursor User',
|
|
136
|
+
hasRefreshToken: false,
|
|
137
|
+
}
|
|
138
|
+
} else if (process.env.CURSOR_ACCESS_TOKEN) {
|
|
139
|
+
detected.cursor = {
|
|
140
|
+
provider: 'cursor',
|
|
141
|
+
source: 'env',
|
|
142
|
+
path: 'CURSOR_ACCESS_TOKEN',
|
|
143
|
+
email: 'Cursor IDE User',
|
|
128
144
|
hasRefreshToken: false,
|
|
129
145
|
}
|
|
130
146
|
}
|
|
@@ -139,6 +155,7 @@ async function discoverLocalCliSessionsInner() {
|
|
|
139
155
|
if (k && (k.accessToken || k.access_token || k.token)) {
|
|
140
156
|
detected.kiro = {
|
|
141
157
|
provider: 'kiro',
|
|
158
|
+
source: 'file',
|
|
142
159
|
path: p,
|
|
143
160
|
email: k.email || k.account || 'AWS Kiro User',
|
|
144
161
|
hasRefreshToken: !!(k.refreshToken || k.refresh_token),
|
|
@@ -149,7 +166,8 @@ async function discoverLocalCliSessionsInner() {
|
|
|
149
166
|
if (!detected.kiro && process.env.KIRO_API_KEY) {
|
|
150
167
|
detected.kiro = {
|
|
151
168
|
provider: 'kiro',
|
|
152
|
-
|
|
169
|
+
source: 'env',
|
|
170
|
+
path: 'KIRO_API_KEY',
|
|
153
171
|
email: 'AWS Kiro API Key',
|
|
154
172
|
hasRefreshToken: false,
|
|
155
173
|
}
|
|
@@ -158,12 +176,15 @@ async function discoverLocalCliSessionsInner() {
|
|
|
158
176
|
// 8. Claude CLI Direct Import
|
|
159
177
|
const claudeCreds = await readClaudeCredentials(activeHome)
|
|
160
178
|
if (claudeCreds) {
|
|
161
|
-
|
|
179
|
+
const claudeEntry = {
|
|
162
180
|
provider: 'claude',
|
|
181
|
+
source: 'file',
|
|
163
182
|
path: homeFile('.claude', 'credentials.json'),
|
|
164
|
-
email: 'Claude Code CLI User',
|
|
183
|
+
email: claudeCreds.email || 'Claude Code CLI User',
|
|
165
184
|
hasRefreshToken: !!claudeCreds.refreshToken,
|
|
166
185
|
}
|
|
186
|
+
detected.claude = claudeEntry
|
|
187
|
+
detected['claude-cli'] = claudeEntry
|
|
167
188
|
}
|
|
168
189
|
|
|
169
190
|
// 9. GitHub Copilot token
|
|
@@ -179,6 +200,7 @@ async function discoverLocalCliSessionsInner() {
|
|
|
179
200
|
if (tok) {
|
|
180
201
|
detected.copilot = {
|
|
181
202
|
provider: 'copilot',
|
|
203
|
+
source: 'file',
|
|
182
204
|
path: p,
|
|
183
205
|
email: gh.user || 'GitHub Copilot User',
|
|
184
206
|
hasRefreshToken: false,
|
|
@@ -190,37 +212,37 @@ async function discoverLocalCliSessionsInner() {
|
|
|
190
212
|
if (!detected.copilot && (process.env.GITHUB_COPILOT_TOKEN || process.env.GH_TOKEN)) {
|
|
191
213
|
detected.copilot = {
|
|
192
214
|
provider: 'copilot',
|
|
193
|
-
|
|
215
|
+
source: 'env',
|
|
216
|
+
path: process.env.GITHUB_COPILOT_TOKEN ? 'GITHUB_COPILOT_TOKEN' : 'GH_TOKEN',
|
|
194
217
|
email: 'GitHub Copilot User',
|
|
195
218
|
hasRefreshToken: false,
|
|
196
219
|
}
|
|
197
220
|
}
|
|
198
221
|
|
|
199
|
-
|
|
200
|
-
|
|
201
222
|
return detected
|
|
202
223
|
}
|
|
203
224
|
|
|
204
225
|
export async function loadLocalCliBlob(provider, options = {}) {
|
|
226
|
+
const canonical = (provider === 'claude-cli') ? 'claude' : provider
|
|
205
227
|
const discovered = await discoverLocalCliSessions(options)
|
|
206
|
-
const info = discovered[provider]
|
|
228
|
+
const info = discovered[canonical] || discovered[provider]
|
|
207
229
|
if (!info) throw new Error(`no local CLI session found for ${provider}`)
|
|
208
230
|
|
|
209
|
-
if (
|
|
231
|
+
if (canonical === 'claude') {
|
|
210
232
|
const creds = await readClaudeCredentials(options && options.home)
|
|
211
233
|
if (!creds) throw new Error('Claude CLI credentials not found')
|
|
212
234
|
return {
|
|
213
235
|
accessToken: creds.accessToken,
|
|
214
|
-
refreshToken: creds.refreshToken,
|
|
236
|
+
refreshToken: creds.refreshToken || '',
|
|
215
237
|
expiresAt: creds.expiresAt,
|
|
216
|
-
email: 'Claude Code CLI User'
|
|
238
|
+
email: creds.email || 'Claude Code CLI User',
|
|
217
239
|
}
|
|
218
240
|
}
|
|
219
241
|
|
|
220
|
-
if (
|
|
242
|
+
if (canonical === 'copilot') {
|
|
221
243
|
let tok = process.env.GITHUB_COPILOT_TOKEN || process.env.GH_TOKEN || ''
|
|
222
244
|
let email = 'GitHub Copilot User'
|
|
223
|
-
if (info.
|
|
245
|
+
if (info.source === 'file' && info.path) {
|
|
224
246
|
const cp = await readJson(info.path)
|
|
225
247
|
if (cp) {
|
|
226
248
|
const gh = cp['github.com'] || cp
|
|
@@ -228,6 +250,7 @@ export async function loadLocalCliBlob(provider, options = {}) {
|
|
|
228
250
|
email = gh.user || email
|
|
229
251
|
}
|
|
230
252
|
}
|
|
253
|
+
if (!tok) throw new Error('no Copilot token found')
|
|
231
254
|
return {
|
|
232
255
|
accessToken: tok,
|
|
233
256
|
refreshToken: tok,
|
|
@@ -238,10 +261,54 @@ export async function loadLocalCliBlob(provider, options = {}) {
|
|
|
238
261
|
}
|
|
239
262
|
}
|
|
240
263
|
|
|
264
|
+
if (canonical === 'cursor') {
|
|
265
|
+
if (info.source === 'env' || !info.path || info.path.includes('env') || info.path === 'CURSOR_ACCESS_TOKEN') {
|
|
266
|
+
const token = process.env.CURSOR_ACCESS_TOKEN
|
|
267
|
+
if (!token) throw new Error('CURSOR_ACCESS_TOKEN environment variable not set')
|
|
268
|
+
return {
|
|
269
|
+
accessToken: token,
|
|
270
|
+
refreshToken: '',
|
|
271
|
+
expiresAt: Date.now() + 30 * 86400 * 1000,
|
|
272
|
+
email: 'Cursor IDE User',
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
const raw = await readJson(info.path)
|
|
276
|
+
if (!raw) throw new Error(`failed to read local CLI file: ${info.path}`)
|
|
277
|
+
const token = (raw && (raw.accessToken || raw.access_token || (raw.authInfo && raw.authInfo.authId))) || process.env.CURSOR_ACCESS_TOKEN || ''
|
|
278
|
+
return {
|
|
279
|
+
accessToken: token,
|
|
280
|
+
refreshToken: '',
|
|
281
|
+
expiresAt: Date.now() + 30 * 86400 * 1000,
|
|
282
|
+
email: (raw && raw.authInfo && raw.authInfo.email) || raw.email || 'Cursor User',
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (canonical === 'kiro') {
|
|
287
|
+
if (info.source === 'env' || !info.path || info.path.includes('env') || info.path === 'KIRO_API_KEY') {
|
|
288
|
+
const token = process.env.KIRO_API_KEY
|
|
289
|
+
if (!token) throw new Error('KIRO_API_KEY environment variable not set')
|
|
290
|
+
return {
|
|
291
|
+
accessToken: token,
|
|
292
|
+
refreshToken: '',
|
|
293
|
+
expiresAt: Date.now() + 30 * 86400 * 1000,
|
|
294
|
+
email: 'AWS Kiro API Key',
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
const raw = await readJson(info.path)
|
|
298
|
+
if (!raw) throw new Error(`failed to read local CLI file: ${info.path}`)
|
|
299
|
+
const token = (raw && (raw.accessToken || raw.access_token || raw.token)) || process.env.KIRO_API_KEY || ''
|
|
300
|
+
return {
|
|
301
|
+
accessToken: token,
|
|
302
|
+
refreshToken: (raw && (raw.refreshToken || raw.refresh_token)) || '',
|
|
303
|
+
expiresAt: (raw && raw.expiresAt) || (Date.now() + 30 * 86400 * 1000),
|
|
304
|
+
email: (raw && raw.email) || 'AWS Kiro User',
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
241
308
|
const raw = await readJson(info.path)
|
|
242
309
|
if (!raw) throw new Error(`failed to read local CLI file: ${info.path}`)
|
|
243
310
|
|
|
244
|
-
if (
|
|
311
|
+
if (canonical === 'codex') {
|
|
245
312
|
const tok = raw.tokens || raw
|
|
246
313
|
return {
|
|
247
314
|
accessToken: tok.access_token || tok.accessToken || '',
|
|
@@ -252,7 +319,7 @@ export async function loadLocalCliBlob(provider, options = {}) {
|
|
|
252
319
|
}
|
|
253
320
|
}
|
|
254
321
|
|
|
255
|
-
if (
|
|
322
|
+
if (canonical === 'grok') {
|
|
256
323
|
const tok = raw.tokens || raw
|
|
257
324
|
return {
|
|
258
325
|
accessToken: tok.access_token || tok.token || '',
|
|
@@ -262,7 +329,7 @@ export async function loadLocalCliBlob(provider, options = {}) {
|
|
|
262
329
|
}
|
|
263
330
|
}
|
|
264
331
|
|
|
265
|
-
if (
|
|
332
|
+
if (canonical === 'antigravity') {
|
|
266
333
|
const tok = raw.token || raw
|
|
267
334
|
return {
|
|
268
335
|
accessToken: tok.access_token || tok.accessToken || '',
|
|
@@ -273,7 +340,7 @@ export async function loadLocalCliBlob(provider, options = {}) {
|
|
|
273
340
|
}
|
|
274
341
|
}
|
|
275
342
|
|
|
276
|
-
if (
|
|
343
|
+
if (canonical === 'kimi') {
|
|
277
344
|
return {
|
|
278
345
|
accessToken: raw.access_token || raw.token || '',
|
|
279
346
|
refreshToken: raw.refresh_token || '',
|
|
@@ -282,7 +349,7 @@ export async function loadLocalCliBlob(provider, options = {}) {
|
|
|
282
349
|
}
|
|
283
350
|
}
|
|
284
351
|
|
|
285
|
-
if (
|
|
352
|
+
if (canonical === 'glm') {
|
|
286
353
|
const apiKey = raw.apiKey || raw.api_key || (raw.provider && raw.provider.apiKey) || ''
|
|
287
354
|
return {
|
|
288
355
|
accessToken: apiKey,
|
|
@@ -292,25 +359,5 @@ export async function loadLocalCliBlob(provider, options = {}) {
|
|
|
292
359
|
}
|
|
293
360
|
}
|
|
294
361
|
|
|
295
|
-
if (provider === 'cursor') {
|
|
296
|
-
const token = process.env.CURSOR_ACCESS_TOKEN || (raw && (raw.accessToken || raw.access_token || (raw.authInfo && raw.authInfo.authId))) || ''
|
|
297
|
-
return {
|
|
298
|
-
accessToken: token,
|
|
299
|
-
refreshToken: '',
|
|
300
|
-
expiresAt: Date.now() + 30 * 86400 * 1000,
|
|
301
|
-
email: (raw && raw.authInfo && raw.authInfo.email) || 'Cursor User',
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
if (provider === 'kiro') {
|
|
306
|
-
const token = process.env.KIRO_API_KEY || (raw && (raw.accessToken || raw.access_token || raw.token)) || ''
|
|
307
|
-
return {
|
|
308
|
-
accessToken: token,
|
|
309
|
-
refreshToken: (raw && (raw.refreshToken || raw.refresh_token)) || '',
|
|
310
|
-
expiresAt: (raw && raw.expiresAt) || (Date.now() + 30 * 86400 * 1000),
|
|
311
|
-
email: (raw && raw.email) || 'AWS Kiro User',
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
|
|
315
362
|
throw new Error(`unsupported local CLI import for provider ${provider}`)
|
|
316
363
|
}
|
package/lib/oauth.js
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @param {{
|
|
3
|
+
* authUrl: string,
|
|
4
|
+
* clientId: string,
|
|
5
|
+
* redirectUri: string,
|
|
6
|
+
* challenge: string,
|
|
7
|
+
* state: string,
|
|
8
|
+
* extra?: Record<string, any>,
|
|
9
|
+
* scope?: string,
|
|
10
|
+
* }} params
|
|
11
|
+
* @returns {string}
|
|
12
|
+
*/
|
|
1
13
|
export function buildAuthorizeUrl({
|
|
2
14
|
authUrl,
|
|
3
15
|
clientId,
|
package/lib/routes/accounts.js
CHANGED
|
@@ -143,7 +143,9 @@ export function registerAccountsRoutes(ctx, state) {
|
|
|
143
143
|
} catch {
|
|
144
144
|
body = null
|
|
145
145
|
}
|
|
146
|
-
|
|
146
|
+
const rawProv = body && body.provider
|
|
147
|
+
const prov = (rawProv === 'claude-cli') ? 'claude' : rawProv
|
|
148
|
+
if (!prov || !isProvider(prov)) {
|
|
147
149
|
writeJson(res, 400, { ok: false, error: { code: 'bad_request', message: 'unknown or missing provider' } })
|
|
148
150
|
return
|
|
149
151
|
}
|
|
@@ -153,9 +155,8 @@ export function registerAccountsRoutes(ctx, state) {
|
|
|
153
155
|
return
|
|
154
156
|
}
|
|
155
157
|
try {
|
|
156
|
-
const blob = await loadLocalCliBlob(
|
|
157
|
-
const
|
|
158
|
-
const idx = Number(body.index) || 1
|
|
158
|
+
const blob = await loadLocalCliBlob(prov)
|
|
159
|
+
const idx = idxNum
|
|
159
160
|
const curSlots = Array.isArray(live().slots) ? live().slots.slice() : []
|
|
160
161
|
const exists = curSlots.some((s) => s && s.provider === prov && Number(s.index) === idx)
|
|
161
162
|
if (!exists && getSettingsApi()) {
|
package/lib/routes/status.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { getRecentAlerts, clearAlerts } from '../alerts.js'
|
|
3
3
|
import { normalizeSlots, vendorConfig } from '../accounts.js'
|
|
4
4
|
import { Config, plainConfig, publicConfig, restoreMaskedSecrets } from '../config-schema.js'
|
|
5
|
-
import { isTrustedSettingsRequest, queryOf, readBody, safeJsonHandler, writeJson } from '../http.js'
|
|
5
|
+
import { isTrustedSettingsRequest, queryOf, readBody, safeJsonHandler, writeJson, fetchWithTimeout, isTimeoutError } from '../http.js'
|
|
6
6
|
import { quotaSnapshot } from '../ratelimit.js'
|
|
7
7
|
import { PROVIDERS, displayName, droppedCredentialRefs, isProvider, oauthRef } from '../refs.js'
|
|
8
8
|
import { getVendor } from '../vendors/index.js'
|
|
@@ -257,10 +257,8 @@ export function registerStatusRoutes(ctx, state) {
|
|
|
257
257
|
}
|
|
258
258
|
let capturedQuota = null
|
|
259
259
|
const probeFetch = async (url, init = {}) => {
|
|
260
|
-
const timeoutSignal = AbortSignal.timeout(15_000)
|
|
261
|
-
const signal = init.signal ? AbortSignal.any([init.signal, timeoutSignal]) : timeoutSignal
|
|
262
260
|
const doFetch = (typeof fetchForRef === "function" && fetchForRef(ref)) || fetch
|
|
263
|
-
const res2 = await doFetch
|
|
261
|
+
const res2 = await fetchWithTimeout(doFetch, url, init, { timeoutMs: 15_000 })
|
|
264
262
|
try {
|
|
265
263
|
const snap = quotaSnapshot(provider, res2.headers, null, Date.now())
|
|
266
264
|
if (snap) { capturedQuota = snap; store.rememberQuota(ref, snap) }
|
|
@@ -272,12 +270,14 @@ export function registerStatusRoutes(ctx, state) {
|
|
|
272
270
|
const latencyMs = Date.now() - t0;
|
|
273
271
|
writeJson(res, 200, { ok: true, provider, index: index, ref, latencyMs, email: pmE(blob.email), label: pmL(blob.label), expiresAt: blob.expiresAt || null, quota: capturedQuota || info.quota || null, usagePercent: info.usagePercent ?? null })
|
|
274
272
|
} catch (e) {
|
|
273
|
+
const isTimeout = isTimeoutError(e)
|
|
275
274
|
writeJson(res, 200, {
|
|
276
275
|
ok: false, provider, index: index, ref,
|
|
277
276
|
latencyMs: Date.now() - t0,
|
|
278
277
|
email: pmE(blob.email), label: pmL(blob.label), expiresAt: blob.expiresAt || null,
|
|
279
278
|
quota: capturedQuota || info.quota || null,
|
|
280
|
-
|
|
279
|
+
isTimeout,
|
|
280
|
+
error: { code: isTimeout ? 'TIMEOUT' : (e && e.code ? e.code : 'VENDOR'), message: String(e && e.message || e).slice(0, 300) },
|
|
281
281
|
})
|
|
282
282
|
}
|
|
283
283
|
}),
|
|
@@ -384,10 +384,8 @@ export function registerStatusRoutes(ctx, state) {
|
|
|
384
384
|
if (typeof vendor.check === 'function') {
|
|
385
385
|
let capturedQuota = null
|
|
386
386
|
const probeFetch = async (url, init = {}) => {
|
|
387
|
-
const timeoutSignal = AbortSignal.timeout(15_000)
|
|
388
|
-
const signal = init.signal ? AbortSignal.any([init.signal, timeoutSignal]) : timeoutSignal
|
|
389
387
|
const doFetch = (typeof fetchForRef === "function" && fetchForRef(ref)) || fetch
|
|
390
|
-
const res2 = await doFetch
|
|
388
|
+
const res2 = await fetchWithTimeout(doFetch, url, init, { timeoutMs: 15_000 })
|
|
391
389
|
try {
|
|
392
390
|
const snap = quotaSnapshot(provider, res2.headers, null, Date.now())
|
|
393
391
|
if (snap) { capturedQuota = snap; store.rememberQuota(ref, snap) }
|
package/lib/runaway-guard.js
CHANGED
|
@@ -68,6 +68,11 @@ export class RunawayDetector {
|
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
/**
|
|
72
|
+
* @param {AsyncIterable<any>} stream
|
|
73
|
+
* @param {Record<string, any>} [options]
|
|
74
|
+
* @returns {AsyncGenerator<any, void, unknown>}
|
|
75
|
+
*/
|
|
71
76
|
export async function* withRunawayGuard(stream, options = {}) {
|
|
72
77
|
if (options && options.enabled === false) {
|
|
73
78
|
yield* stream
|
|
@@ -79,7 +84,7 @@ export async function* withRunawayGuard(stream, options = {}) {
|
|
|
79
84
|
for await (const chunk of stream) {
|
|
80
85
|
if (chunk && chunk.type === 'finish') {
|
|
81
86
|
yield chunk
|
|
82
|
-
|
|
87
|
+
continue
|
|
83
88
|
}
|
|
84
89
|
|
|
85
90
|
const text = (chunk && typeof chunk === 'object')
|
|
@@ -88,6 +93,13 @@ export async function* withRunawayGuard(stream, options = {}) {
|
|
|
88
93
|
|
|
89
94
|
if (text && (chunk.type === 'text-delta' || chunk.type === 'reasoning-delta' || !chunk.type)) {
|
|
90
95
|
if (detector.feed(text)) {
|
|
96
|
+
if (options && typeof options === 'object') {
|
|
97
|
+
options.tripped = true
|
|
98
|
+
options.trippedReason = detector.getReason()
|
|
99
|
+
if (typeof options.onRunaway === 'function') {
|
|
100
|
+
options.onRunaway(detector.getReason())
|
|
101
|
+
}
|
|
102
|
+
}
|
|
91
103
|
yield {
|
|
92
104
|
type: 'finish',
|
|
93
105
|
reason: {
|
package/lib/session-pin.js
CHANGED
|
@@ -2,10 +2,8 @@ const sessionPins = new Map()
|
|
|
2
2
|
const DEFAULT_TTL_MS = 30 * 60 * 1000 // 30 mins
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
6
|
-
* @param {
|
|
7
|
-
* @param {string|undefined} accountRef
|
|
8
|
-
* @param {number} [ttlMs]
|
|
5
|
+
* Prune expired session pin entries.
|
|
6
|
+
* @param {number} [now]
|
|
9
7
|
* @returns {void}
|
|
10
8
|
*/
|
|
11
9
|
export function pruneSessionPins(now = Date.now()) {
|
|
@@ -16,6 +14,13 @@ export function pruneSessionPins(now = Date.now()) {
|
|
|
16
14
|
}
|
|
17
15
|
}
|
|
18
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Bind a session id to an account ref for ttlMs (default 30 minutes).
|
|
19
|
+
* @param {string|undefined} sessionId
|
|
20
|
+
* @param {string|undefined} accountRef
|
|
21
|
+
* @param {number} [ttlMs]
|
|
22
|
+
* @returns {void}
|
|
23
|
+
*/
|
|
19
24
|
export function pinSession(sessionId, accountRef, ttlMs = DEFAULT_TTL_MS) {
|
|
20
25
|
if (!sessionId || !accountRef) return
|
|
21
26
|
if (sessionPins.size >= 50) {
|
package/lib/sse.js
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @param {any} body
|
|
3
|
+
* @param {{ idleTimeoutMs?: number, signal?: AbortSignal }} [options]
|
|
4
|
+
* @returns {AsyncGenerator<string, void, unknown>}
|
|
5
|
+
*/
|
|
1
6
|
export async function* iterateSse(body, { idleTimeoutMs = 60000, signal } = {}) {
|
|
2
7
|
const reader = body && typeof body.getReader === 'function' ? body.getReader() : null
|
|
3
8
|
const decoder = new TextDecoder()
|
|
@@ -38,7 +43,7 @@ export async function* iterateSse(body, { idleTimeoutMs = 60000, signal } = {})
|
|
|
38
43
|
const timeoutPromise = new Promise((_, reject) => {
|
|
39
44
|
if (!idleTimeoutMs || idleTimeoutMs <= 0) return
|
|
40
45
|
timer = setTimeout(() => {
|
|
41
|
-
const err = new Error(`stream idle timeout: no data received for ${Math.round(idleTimeoutMs / 1000)}s`)
|
|
46
|
+
const err = /** @type {Error & { code?: string }} */ (new Error(`stream idle timeout: no data received for ${Math.round(idleTimeoutMs / 1000)}s`))
|
|
42
47
|
err.code = 'TIMEOUT'
|
|
43
48
|
reject(err)
|
|
44
49
|
}, idleTimeoutMs)
|
package/lib/stream-rotate.js
CHANGED
|
@@ -41,7 +41,7 @@ export async function* streamWithRotation({
|
|
|
41
41
|
return
|
|
42
42
|
}
|
|
43
43
|
if (lastError) throw lastError
|
|
44
|
-
const err = new Error('no usable subscription account for this provider')
|
|
44
|
+
const err = /** @type {Error & { code?: string }} */ (new Error('no usable subscription account for this provider'))
|
|
45
45
|
err.code = 'RATE_LIMIT'
|
|
46
46
|
throw err
|
|
47
47
|
}
|
|
@@ -53,7 +53,18 @@ export async function* streamWithRotation({
|
|
|
53
53
|
|
|
54
54
|
while (true) {
|
|
55
55
|
try {
|
|
56
|
-
const
|
|
56
|
+
const runawayState = options && options._runawayState ? options._runawayState : { tripped: false, reason: null }
|
|
57
|
+
const runawayOpts = {
|
|
58
|
+
...(options && options.runawayGuard),
|
|
59
|
+
onRunaway: (reason) => {
|
|
60
|
+
runawayState.tripped = true
|
|
61
|
+
runawayState.reason = reason
|
|
62
|
+
},
|
|
63
|
+
}
|
|
64
|
+
const guardedStream = withRunawayGuard(
|
|
65
|
+
streamOnce(account, { ...options, _runawayState: runawayState }),
|
|
66
|
+
runawayOpts
|
|
67
|
+
)
|
|
57
68
|
for await (const chunk of guardedStream) {
|
|
58
69
|
firstChunkDelivered = true
|
|
59
70
|
yield chunk
|
package/lib/vendor-factory.js
CHANGED
|
@@ -13,8 +13,8 @@ import { codexResponsesBody, modelCatalog } from "./messages.js"
|
|
|
13
13
|
import { formTokenRequest, jsonTokenRequest, codexResponsesStream, readJson, tokenBlobFromOAuth, httpError } from "./wire.js"
|
|
14
14
|
import { emailFromToken } from "./jwt.js"
|
|
15
15
|
|
|
16
|
-
function defaultModels(models) {
|
|
17
|
-
return modelCatalog(
|
|
16
|
+
function defaultModels(providerId, models) {
|
|
17
|
+
return modelCatalog(providerId, (models || []).map((row) => (typeof row === "string" ? { id: row, name: row } : row)))
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
/**
|
|
@@ -69,7 +69,8 @@ export function createVendorFromProfile(profile) {
|
|
|
69
69
|
code_verifier: pkce.verifier,
|
|
70
70
|
}
|
|
71
71
|
const req = tokenStyle === "json" ? jsonTokenRequest : formTokenRequest
|
|
72
|
-
const
|
|
72
|
+
const tokenFetch = (u, i, o) => fetchWithTimeout(fetchImpl || fetch, u, i, { timeoutMs: 30000, ...(o || {}) })
|
|
73
|
+
const json = await req(profile.tokenUrl, params, tokenFetch)
|
|
73
74
|
const blob = tokenBlobFromOAuth(json)
|
|
74
75
|
return { ...blob, email: blob.email || emailFromToken(blob.accessToken), label: blob.label || name }
|
|
75
76
|
},
|
|
@@ -80,11 +81,12 @@ export function createVendorFromProfile(profile) {
|
|
|
80
81
|
refresh_token: blob.refreshToken,
|
|
81
82
|
}
|
|
82
83
|
const req = tokenStyle === "json" ? jsonTokenRequest : formTokenRequest
|
|
83
|
-
const
|
|
84
|
+
const tokenFetch = (u, i, o) => fetchWithTimeout(fetchImpl || fetch, u, i, { timeoutMs: 30000, ...(o || {}) })
|
|
85
|
+
const json = await req(profile.tokenUrl, params, tokenFetch)
|
|
84
86
|
return tokenBlobFromOAuth(json, { label: blob.label, email: blob.email })
|
|
85
87
|
},
|
|
86
88
|
async listModels(blob, cfg, fetchImpl) {
|
|
87
|
-
const catalog = defaultModels(cfg.models || profile.models || [])
|
|
89
|
+
const catalog = defaultModels(id, cfg.models || profile.models || [])
|
|
88
90
|
if (!profile.modelsPath) return catalog
|
|
89
91
|
try {
|
|
90
92
|
const res = await fetchWithTimeout(fetchImpl || fetch, `${base}${profile.modelsPath}`, {
|
|
@@ -112,7 +114,7 @@ export function createVendorFromProfile(profile) {
|
|
|
112
114
|
async* streamOnce({ blob, options, fetchImpl, headers, config, signal }) {
|
|
113
115
|
const body = codexResponsesBody(options, "")
|
|
114
116
|
if (!body.instructions) delete body.instructions
|
|
115
|
-
const res = await (fetchImpl || fetch
|
|
117
|
+
const res = await fetchWithTimeout(fetchImpl || fetch, `${base}/responses`, {
|
|
116
118
|
method: "POST",
|
|
117
119
|
headers: {
|
|
118
120
|
...headers,
|
|
@@ -122,7 +124,7 @@ export function createVendorFromProfile(profile) {
|
|
|
122
124
|
},
|
|
123
125
|
body: JSON.stringify(body),
|
|
124
126
|
signal,
|
|
125
|
-
})
|
|
127
|
+
}, { timeoutMs: 60000, stream: true })
|
|
126
128
|
if (!res.ok) throw httpError(res.status, await res.text())
|
|
127
129
|
yield* codexResponsesStream(res.body)
|
|
128
130
|
},
|