@goodandready/dsh-subscriptions 0.6.72 → 0.6.82
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -0
- package/README.ru.md +7 -0
- package/README.zh.md +7 -0
- package/lib/accounts-view.js +97 -5
- package/lib/accounts.js +137 -11
- package/lib/adapter.js +3 -0
- package/lib/alerts.js +35 -0
- package/lib/analytics.js +31 -2
- package/lib/billing.js +99 -0
- package/lib/blob.js +18 -0
- package/lib/capabilities.js +208 -0
- package/lib/client.js +344 -17
- package/lib/config-schema.js +46 -4
- package/lib/diagnostic-wizard.js +2 -5
- package/lib/diagnostics.js +2 -2
- package/lib/error-buffer.js +2 -2
- package/lib/forecast.js +56 -0
- package/lib/headless-auth.js +379 -0
- package/lib/history.js +36 -0
- package/lib/http.js +26 -4
- package/lib/index.js +98 -11
- package/lib/jwt.js +84 -17
- package/lib/loopback.js +3 -2
- package/lib/mask.js +16 -0
- package/lib/oauth-pending.js +12 -0
- package/lib/pricing.js +89 -0
- package/lib/refs.js +2 -0
- package/lib/reset-clock.js +55 -0
- package/lib/rotate.js +18 -4
- package/lib/routes/accounts.js +131 -0
- package/lib/routes/oauth.js +44 -2
- package/lib/routes/proxy.js +14 -0
- package/lib/scope-inspector.js +51 -0
- package/lib/stream-rotate.js +31 -10
- package/lib/updater.js +21 -0
- package/lib/vault-sync.js +412 -0
- package/lib/vault.js +1 -1
- package/lib/vendor-factory.js +6 -2
- package/lib/vendors/antigravity.js +11 -2
- package/lib/vendors/claude.js +56 -8
- package/lib/vendors/codex.js +34 -6
- package/lib/vendors/copilot-cli.js +8 -1
- package/lib/vendors/ernie.js +9 -2
- package/lib/vendors/grok.js +66 -6
- package/lib/vendors/jetbrains.js +133 -13
- package/lib/vendors/perplexity.js +148 -17
- package/package.json +1 -1
package/lib/routes/accounts.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { diagnoseVendorError } from '../doctor.js'
|
|
2
2
|
import { exportVault, importVault } from '../vault.js'
|
|
3
|
+
import { syncVaultPush, syncVaultPull, getVaultSyncStatus } from '../vault-sync.js'
|
|
3
4
|
import { normalizeSlots, vendorConfig } from '../accounts.js'
|
|
4
5
|
import { getVendor } from '../vendors/index.js'
|
|
5
6
|
import { notifyQuarantineEntered, notifyQuarantineReleased } from '../alerts.js'
|
|
@@ -518,6 +519,69 @@ export function registerAccountsRoutes(ctx, state) {
|
|
|
518
519
|
|
|
519
520
|
|
|
520
521
|
|
|
522
|
+
// Issue #549: Switch active organization/workspace for an account slot
|
|
523
|
+
ctx.effect(() => ctx.webServer.register({
|
|
524
|
+
kind: 'exact',
|
|
525
|
+
path: '/dsh-subscriptions/accounts/switch-organization',
|
|
526
|
+
handler: safeJsonHandler(async (req, res) => {
|
|
527
|
+
if (req.method !== 'POST') {
|
|
528
|
+
writeJson(res, 405, { ok: false, error: { code: 'method', message: 'POST only' } })
|
|
529
|
+
return
|
|
530
|
+
}
|
|
531
|
+
if (!isTrustedSettingsRequest(req, ctx)) {
|
|
532
|
+
writeJson(res, 403, { ok: false, error: { code: 'forbidden', message: 'same-origin only' } })
|
|
533
|
+
return
|
|
534
|
+
}
|
|
535
|
+
let payload
|
|
536
|
+
try { payload = JSON.parse((await readBody(req, 8 * 1024)).toString('utf8') || '{}') } catch {
|
|
537
|
+
writeJson(res, 400, { ok: false, error: { code: 'json', message: 'invalid json' } })
|
|
538
|
+
return
|
|
539
|
+
}
|
|
540
|
+
const provider = String(payload.provider || '')
|
|
541
|
+
const index = Number(payload.index || 1)
|
|
542
|
+
const organizationId = String(payload.organizationId || payload.selectedOrgId || '').trim()
|
|
543
|
+
if (!isProvider(provider)) {
|
|
544
|
+
writeJson(res, 400, { ok: false, error: { code: 'provider', message: 'unknown provider' } })
|
|
545
|
+
return
|
|
546
|
+
}
|
|
547
|
+
const ref = refForSlot(provider, index)
|
|
548
|
+
if (!ref) {
|
|
549
|
+
writeJson(res, 404, { ok: false, error: { code: 'slot', message: 'slot not found' } })
|
|
550
|
+
return
|
|
551
|
+
}
|
|
552
|
+
let blob
|
|
553
|
+
try { blob = await store.loadBlob(ref) } catch {
|
|
554
|
+
writeJson(res, 404, { ok: false, error: { code: 'blob', message: 'credentials not configured' } })
|
|
555
|
+
return
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
blob.selectedOrgId = organizationId
|
|
559
|
+
if (provider === 'codex') blob.accountId = organizationId
|
|
560
|
+
if (provider === 'antigravity') blob.projectId = organizationId
|
|
561
|
+
if (provider === 'claude') blob.organizationId = organizationId
|
|
562
|
+
if (provider === 'copilot') blob.organization = organizationId
|
|
563
|
+
|
|
564
|
+
if (Array.isArray(blob.organizations)) {
|
|
565
|
+
const found = blob.organizations.find((o) => o && o.id === organizationId)
|
|
566
|
+
if (found) blob.selectedOrgName = found.name
|
|
567
|
+
else if (organizationId) blob.selectedOrgName = organizationId
|
|
568
|
+
} else if (organizationId) {
|
|
569
|
+
blob.selectedOrgName = organizationId
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
await store.saveBlob(ref, blob)
|
|
573
|
+
await syncAdapter()
|
|
574
|
+
|
|
575
|
+
writeJson(res, 200, {
|
|
576
|
+
ok: true,
|
|
577
|
+
ref,
|
|
578
|
+
selectedOrgId: organizationId,
|
|
579
|
+
selectedOrgName: blob.selectedOrgName || organizationId,
|
|
580
|
+
accounts: await accountsView(),
|
|
581
|
+
})
|
|
582
|
+
}),
|
|
583
|
+
}), 'dsh-subscriptions: /accounts/switch-organization')
|
|
584
|
+
|
|
521
585
|
ctx.effect(() => ctx.webServer.register({
|
|
522
586
|
kind: 'exact',
|
|
523
587
|
path: '/dsh-subscriptions/logout',
|
|
@@ -660,4 +724,71 @@ export function registerAccountsRoutes(ctx, state) {
|
|
|
660
724
|
handler: handleVaultImport,
|
|
661
725
|
}), 'dsh-subscriptions: /import-backup')
|
|
662
726
|
|
|
727
|
+
|
|
728
|
+
// #562: Remote Vault Sync Push
|
|
729
|
+
ctx.effect(() => ctx.webServer.register({
|
|
730
|
+
kind: 'exact',
|
|
731
|
+
path: '/dsh-subscriptions/vault/sync/push',
|
|
732
|
+
handler: safeJsonHandler(async (req, res) => {
|
|
733
|
+
if (req.method !== 'POST') {
|
|
734
|
+
writeJson(res, 405, { ok: false, error: { code: 'method', message: 'POST only' } })
|
|
735
|
+
return
|
|
736
|
+
}
|
|
737
|
+
if (!isTrustedSettingsRequest(req, ctx)) {
|
|
738
|
+
writeJson(res, 403, { ok: false, error: { code: 'forbidden', message: 'same-origin only' } })
|
|
739
|
+
return
|
|
740
|
+
}
|
|
741
|
+
try {
|
|
742
|
+
const result = await syncVaultPush({ live: live(), store })
|
|
743
|
+
writeJson(res, 200, { ok: true, result, status: getVaultSyncStatus() })
|
|
744
|
+
} catch (e) {
|
|
745
|
+
writeJson(res, 400, { ok: false, error: { code: 'vault_sync_push', message: String(e && e.message || e) } })
|
|
746
|
+
}
|
|
747
|
+
}),
|
|
748
|
+
}), 'dsh-subscriptions: /vault/sync/push')
|
|
749
|
+
|
|
750
|
+
// #562: Remote Vault Sync Pull
|
|
751
|
+
ctx.effect(() => ctx.webServer.register({
|
|
752
|
+
kind: 'exact',
|
|
753
|
+
path: '/dsh-subscriptions/vault/sync/pull',
|
|
754
|
+
handler: safeJsonHandler(async (req, res) => {
|
|
755
|
+
if (req.method !== 'POST') {
|
|
756
|
+
writeJson(res, 405, { ok: false, error: { code: 'method', message: 'POST only' } })
|
|
757
|
+
return
|
|
758
|
+
}
|
|
759
|
+
if (!isTrustedSettingsRequest(req, ctx)) {
|
|
760
|
+
writeJson(res, 403, { ok: false, error: { code: 'forbidden', message: 'same-origin only' } })
|
|
761
|
+
return
|
|
762
|
+
}
|
|
763
|
+
try {
|
|
764
|
+
const result = await syncVaultPull({
|
|
765
|
+
live: live(),
|
|
766
|
+
store,
|
|
767
|
+
settingsApi: getSettingsApi(),
|
|
768
|
+
syncAdapter,
|
|
769
|
+
refreshModels,
|
|
770
|
+
})
|
|
771
|
+
writeJson(res, 200, { ok: true, result, status: getVaultSyncStatus(), accounts: await accountsView() })
|
|
772
|
+
} catch (e) {
|
|
773
|
+
writeJson(res, 400, { ok: false, error: { code: 'vault_sync_pull', message: String(e && e.message || e) } })
|
|
774
|
+
}
|
|
775
|
+
}),
|
|
776
|
+
}), 'dsh-subscriptions: /vault/sync/pull')
|
|
777
|
+
|
|
778
|
+
// #562: Remote Vault Sync Status
|
|
779
|
+
ctx.effect(() => ctx.webServer.register({
|
|
780
|
+
kind: 'exact',
|
|
781
|
+
path: '/dsh-subscriptions/vault/sync/status',
|
|
782
|
+
handler: safeJsonHandler(async (req, res) => {
|
|
783
|
+
if (req.method !== 'GET') {
|
|
784
|
+
writeJson(res, 405, { ok: false, error: { code: 'method', message: 'GET only' } })
|
|
785
|
+
return
|
|
786
|
+
}
|
|
787
|
+
if (!isTrustedSettingsRequest(req, ctx)) {
|
|
788
|
+
writeJson(res, 403, { ok: false, error: { code: 'forbidden', message: 'same-origin only' } })
|
|
789
|
+
return
|
|
790
|
+
}
|
|
791
|
+
writeJson(res, 200, { ok: true, status: getVaultSyncStatus() })
|
|
792
|
+
}),
|
|
793
|
+
}), 'dsh-subscriptions: /vault/sync/status')
|
|
663
794
|
}
|
package/lib/routes/oauth.js
CHANGED
|
@@ -5,6 +5,8 @@ import { parseCallbackInput, requestOrigin } from '../oauth.js'
|
|
|
5
5
|
import { createPkce } from '../pkce.js'
|
|
6
6
|
import { displayName, isProvider, oauthRef } from '../refs.js'
|
|
7
7
|
import { getVendor } from '../vendors/index.js'
|
|
8
|
+
import { getHeadlessAuthInfo, handleHeadlessRedirect } from '../headless-auth.js'
|
|
9
|
+
import { syncVaultPush } from '../vault-sync.js'
|
|
8
10
|
|
|
9
11
|
export function registerOauthRoutes(ctx, state) {
|
|
10
12
|
const {
|
|
@@ -44,6 +46,7 @@ export function registerOauthRoutes(ctx, state) {
|
|
|
44
46
|
}
|
|
45
47
|
const origin = requestOrigin(req)
|
|
46
48
|
const redirectUri = redirectFor(provider, `${provider}:${index}`, origin)
|
|
49
|
+
sweepPending(Date.now())
|
|
47
50
|
const pkce = await createPkce()
|
|
48
51
|
pending.set(pkce.state, {
|
|
49
52
|
provider,
|
|
@@ -89,7 +92,10 @@ export function registerOauthRoutes(ctx, state) {
|
|
|
89
92
|
}
|
|
90
93
|
} catch { autoCatch = false }
|
|
91
94
|
}
|
|
92
|
-
|
|
95
|
+
const host = (req.headers && req.headers.host) || ''
|
|
96
|
+
const user = process.env.USER || process.env.USERNAME || 'user'
|
|
97
|
+
const headless = getHeadlessAuthInfo({ provider, redirectUri, authUrl: url, host, user })
|
|
98
|
+
writeJson(res, 200, { ok: true, url, state: pkce.state, redirectUri, autoCatch, headless })
|
|
93
99
|
}),
|
|
94
100
|
}), 'dsh-subscriptions: /oauth/start')
|
|
95
101
|
|
|
@@ -143,7 +149,13 @@ export function registerOauthRoutes(ctx, state) {
|
|
|
143
149
|
intervalMs: start.intervalMs,
|
|
144
150
|
createdAt: Date.now(),
|
|
145
151
|
})
|
|
146
|
-
|
|
152
|
+
const host = (req.headers && req.headers.host) || ''
|
|
153
|
+
const user = process.env.USER || process.env.USERNAME || 'user'
|
|
154
|
+
const headless = getHeadlessAuthInfo({ provider, redirectUri: '', authUrl: start.authUrl, host, user })
|
|
155
|
+
if (headless.terminalQr) {
|
|
156
|
+
console.log('[dsh-subscriptions] Device OAuth initiated for ' + provider + ':\n' + headless.terminalQr)
|
|
157
|
+
}
|
|
158
|
+
writeJson(res, 200, { ok: true, state, userCode: start.userCode, authUrl: start.authUrl, intervalMs: start.intervalMs, headless })
|
|
147
159
|
} catch (e) {
|
|
148
160
|
writeJson(res, 502, { ok: false, error: { code: e && e.code || 'DEVICE', message: String(e && e.message || e).slice(0, 300) } })
|
|
149
161
|
}
|
|
@@ -193,6 +205,9 @@ export function registerOauthRoutes(ctx, state) {
|
|
|
193
205
|
pending.delete(state)
|
|
194
206
|
await syncAdapter()
|
|
195
207
|
refreshModels().catch(() => {})
|
|
208
|
+
if (live().vaultSyncEnabled && live().vaultSyncAutoPush) {
|
|
209
|
+
syncVaultPush({ live: live(), store }).catch(() => {})
|
|
210
|
+
}
|
|
196
211
|
writeJson(res, 200, { ok: true, status: 'authorized', ref: row.ref, label: pmL(blob.label || blob.email) || displayName(row.provider) })
|
|
197
212
|
} catch (e) {
|
|
198
213
|
writeJson(res, 502, { ok: false, error: { code: e && e.code || 'DEVICE', message: String(e && e.message || e).slice(0, 300) } })
|
|
@@ -261,4 +276,31 @@ export function registerOauthRoutes(ctx, state) {
|
|
|
261
276
|
}
|
|
262
277
|
}),
|
|
263
278
|
}), 'dsh-subscriptions: /oauth/complete')
|
|
279
|
+
|
|
280
|
+
// #557: Headless auth info helper endpoint
|
|
281
|
+
ctx.effect(() => ctx.webServer.register({
|
|
282
|
+
kind: 'exact',
|
|
283
|
+
path: '/dsh-subscriptions/oauth/headless-info',
|
|
284
|
+
handler: safeJsonHandler(async (req, res) => {
|
|
285
|
+
if (req.method !== 'GET') {
|
|
286
|
+
writeJson(res, 405, { ok: false, error: { code: 'method', message: 'GET only' } })
|
|
287
|
+
return
|
|
288
|
+
}
|
|
289
|
+
const q = queryOf(req)
|
|
290
|
+
const provider = q.get('provider') || ''
|
|
291
|
+
const redirectUri = q.get('redirectUri') || ''
|
|
292
|
+
const authUrl = q.get('authUrl') || ''
|
|
293
|
+
const host = (req.headers && req.headers.host) || ''
|
|
294
|
+
const user = process.env.USER || process.env.USERNAME || 'user'
|
|
295
|
+
const info = getHeadlessAuthInfo({ provider, redirectUri, authUrl, host, user })
|
|
296
|
+
writeJson(res, 200, { ok: true, ...info })
|
|
297
|
+
}),
|
|
298
|
+
}), 'dsh-subscriptions: /oauth/headless-info')
|
|
299
|
+
|
|
300
|
+
// #557: Headless re-auth callback redirector
|
|
301
|
+
ctx.effect(() => ctx.webServer.register({
|
|
302
|
+
kind: 'exact',
|
|
303
|
+
path: '/dsh-subscriptions/oauth/headless-redirect',
|
|
304
|
+
handler: (req, res) => handleHeadlessRedirect(req, res, { pending, completeOAuth, accountsView }),
|
|
305
|
+
}), 'dsh-subscriptions: /oauth/headless-redirect')
|
|
264
306
|
}
|
package/lib/routes/proxy.js
CHANGED
|
@@ -58,6 +58,20 @@ export function registerProxyRoutes(ctx, state) {
|
|
|
58
58
|
writeJson(res, 404, { ok: false, error: { code: 'provider', message: 'unknown provider' } })
|
|
59
59
|
return
|
|
60
60
|
}
|
|
61
|
+
const hasBackslashOrInvalid = parts.some((p) => {
|
|
62
|
+
if (p.includes("\\")) return true
|
|
63
|
+
try {
|
|
64
|
+
const dec = decodeURIComponent(p)
|
|
65
|
+
if (dec.includes("\\")) return true
|
|
66
|
+
} catch {
|
|
67
|
+
return true
|
|
68
|
+
}
|
|
69
|
+
return false
|
|
70
|
+
})
|
|
71
|
+
if (hasBackslashOrInvalid) {
|
|
72
|
+
writeJson(res, 400, { ok: false, error: { code: 'path', message: 'invalid path' } })
|
|
73
|
+
return
|
|
74
|
+
}
|
|
61
75
|
let body
|
|
62
76
|
if (req.method === 'POST') {
|
|
63
77
|
try {
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export const REQUIRED_SCOPES = {
|
|
2
|
+
antigravity: [
|
|
3
|
+
{ scope: "https://www.googleapis.com/auth/cloud-platform", description: "Google Cloud Platform & Code Assist" },
|
|
4
|
+
],
|
|
5
|
+
codex: [
|
|
6
|
+
{ scope: "openid", description: "OpenID authentication" },
|
|
7
|
+
{ scope: "https://api.openai.com/auth", description: "ChatGPT API authorization" },
|
|
8
|
+
],
|
|
9
|
+
claude: [
|
|
10
|
+
{ scope: "user:profile", description: "Anthropic user profile" },
|
|
11
|
+
],
|
|
12
|
+
copilot: [
|
|
13
|
+
{ scope: "read:user", description: "GitHub user read" },
|
|
14
|
+
],
|
|
15
|
+
grok: [
|
|
16
|
+
{ scope: "openid", description: "OpenID authentication" },
|
|
17
|
+
],
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Inspect scopes granted by provider against required scopes.
|
|
22
|
+
* @param {string} provider
|
|
23
|
+
* @param {string[]|string} rawScopes
|
|
24
|
+
* @returns {{ scopes: string[], valid: boolean, missing: string[], warning: string, scopeWarning: string }}
|
|
25
|
+
*/
|
|
26
|
+
export function inspectScopes(provider, rawScopes) {
|
|
27
|
+
const scopes = Array.isArray(rawScopes)
|
|
28
|
+
? rawScopes.map(String).filter(Boolean)
|
|
29
|
+
: String(rawScopes || "").split(/[\s,]+/).filter(Boolean)
|
|
30
|
+
|
|
31
|
+
const reqList = REQUIRED_SCOPES[provider] || []
|
|
32
|
+
if (reqList.length === 0 || scopes.length === 0) {
|
|
33
|
+
return { scopes, valid: true, missing: [], warning: "", scopeWarning: "" }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const missing = reqList.filter((r) =>
|
|
37
|
+
!scopes.some((s) => s.toLowerCase() === r.scope.toLowerCase())
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
const warning = missing.length > 0
|
|
41
|
+
? `Missing required OAuth scope: ${missing.map((m) => m.description || m.scope).join(", ")}`
|
|
42
|
+
: ""
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
scopes,
|
|
46
|
+
valid: missing.length === 0,
|
|
47
|
+
missing: missing.map((m) => m.scope),
|
|
48
|
+
warning,
|
|
49
|
+
scopeWarning: warning,
|
|
50
|
+
}
|
|
51
|
+
}
|
package/lib/stream-rotate.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { inspectAccountCapabilities, normalizeCapabilitiesRequest } from './capabilities.js'
|
|
1
2
|
import { defaultConcurrencyQueue } from './concurrency-queue.js'
|
|
2
3
|
import { recordSessionCacheUsage } from './session-pin.js'
|
|
3
|
-
import { pickAccountWithTrace, markCooldown, isSwitchableError, isRegionError, modelFamily } from './rotate.js'
|
|
4
|
+
import { pickAccountWithTrace, markCooldown, isSwitchableError, isRegionError, isAuthError, modelFamily } from './rotate.js'
|
|
4
5
|
import { putInQuarantine, REASON_RATE_LIMIT, REASON_HARD_LIMIT, REASON_REVOKED, REASON_DEGRADED } from './quarantine.js'
|
|
5
6
|
import { notifySessionExpired, notifyQuarantineEntered } from './alerts.js'
|
|
6
7
|
import { withRunawayGuard } from './runaway-guard.js'
|
|
@@ -10,7 +11,7 @@ import { resolveAccurateReset } from './reset-clock.js'
|
|
|
10
11
|
|
|
11
12
|
export async function* streamWithRotation({
|
|
12
13
|
accounts,
|
|
13
|
-
nowMs,
|
|
14
|
+
nowMs = () => Date.now(),
|
|
14
15
|
cooldownMs,
|
|
15
16
|
switchAtRemaining,
|
|
16
17
|
streamOnce,
|
|
@@ -111,8 +112,20 @@ export async function* streamWithRotation({
|
|
|
111
112
|
const queueConfig = {
|
|
112
113
|
signal: options && options.signal,
|
|
113
114
|
timeoutMs: options && (options.queueTimeoutMs !== undefined ? options.queueTimeoutMs : (options.config && options.config.queueTimeoutMs)),
|
|
114
|
-
maxConcurrency:
|
|
115
|
-
|
|
115
|
+
maxConcurrency: account.maxConcurrency !== undefined
|
|
116
|
+
? account.maxConcurrency
|
|
117
|
+
: (options && options.accountMaxConcurrency !== undefined
|
|
118
|
+
? options.accountMaxConcurrency
|
|
119
|
+
: (options && options.config && options.config.accountMaxConcurrency !== undefined
|
|
120
|
+
? options.config.accountMaxConcurrency
|
|
121
|
+
: 1)),
|
|
122
|
+
maxQueueSize: account.maxQueueSize !== undefined
|
|
123
|
+
? account.maxQueueSize
|
|
124
|
+
: (options && options.maxQueueSize !== undefined
|
|
125
|
+
? options.maxQueueSize
|
|
126
|
+
: (options && options.config && options.config.maxQueueSize !== undefined
|
|
127
|
+
? options.config.maxQueueSize
|
|
128
|
+
: 20)),
|
|
116
129
|
enabled: options && (options.enableQueue !== undefined ? options.enableQueue : (options.config && options.config.enableQueue !== undefined ? options.config.enableQueue : true)),
|
|
117
130
|
}
|
|
118
131
|
|
|
@@ -146,15 +159,19 @@ export async function* streamWithRotation({
|
|
|
146
159
|
runawayState.reason = reason
|
|
147
160
|
},
|
|
148
161
|
}
|
|
162
|
+
const accountCaps = account.capabilities || inspectAccountCapabilities(account.provider || options?.provider, account, account)
|
|
163
|
+
const adaptedOptions = normalizeCapabilitiesRequest(options, accountCaps)
|
|
149
164
|
const guardedStream = withRunawayGuard(
|
|
150
|
-
streamOnce(account, { ...
|
|
165
|
+
streamOnce(account, { ...adaptedOptions, _runawayState: runawayState }),
|
|
151
166
|
runawayOpts
|
|
152
167
|
)
|
|
153
168
|
const sentineledStream = withSoftFailureSentinel(guardedStream, {
|
|
154
169
|
enabled: (options?.enableSoftFailureSentinel !== false) && (options?.config?.enableSoftFailureSentinel !== false),
|
|
155
170
|
})
|
|
156
171
|
for await (const chunk of sentineledStream) {
|
|
157
|
-
|
|
172
|
+
if (chunk && (chunk.type === 'text-delta' || chunk.type === 'delta' || chunk.text || chunk.content)) {
|
|
173
|
+
firstChunkDelivered = true
|
|
174
|
+
}
|
|
158
175
|
const cachedTokens = Number(
|
|
159
176
|
(chunk && chunk.usage && (chunk.usage.cacheReadTokens || chunk.usage.cachedTokens)) ||
|
|
160
177
|
(chunk && chunk.tokens && (chunk.tokens.cacheRead || chunk.tokens.cached)) ||
|
|
@@ -191,7 +208,7 @@ export async function* streamWithRotation({
|
|
|
191
208
|
continue
|
|
192
209
|
}
|
|
193
210
|
|
|
194
|
-
if (!isSwitchableError(err)) {
|
|
211
|
+
if (!isSwitchableError(err) && !isAuthError(err)) {
|
|
195
212
|
currentAttempt.status = 'fatal_error'
|
|
196
213
|
currentAttempt.error = err ? (err.message || String(err)) : 'unknown'
|
|
197
214
|
syncTrace('fatal_error')
|
|
@@ -203,9 +220,10 @@ export async function* streamWithRotation({
|
|
|
203
220
|
|
|
204
221
|
const accurateUntil = resolveAccurateReset(err, account, options && options.provider, nowMs())
|
|
205
222
|
const isSoft = Boolean(err && (err.code === 'SOFT_FAILURE' || err.isSoftFailure))
|
|
223
|
+
const isAuth = isAuthError(err)
|
|
206
224
|
|
|
207
225
|
// Move slot to cooldown and quarantine (#172, #538)
|
|
208
|
-
const effectiveCooldownMs = isSoft ? Math.min(cooldownMs, 5 * 60 * 1000) : cooldownMs
|
|
226
|
+
const effectiveCooldownMs = isSoft ? Math.min(cooldownMs, 5 * 60 * 1000) : (isAuth ? 86400000 : cooldownMs)
|
|
209
227
|
const cooled = markCooldown(account, nowMs(), effectiveCooldownMs, modelFamily(options && options.provider, options && options.model), accurateUntil)
|
|
210
228
|
account.cooldownUntil = cooled.cooldownUntil
|
|
211
229
|
if (cooled.cooldownFamilies) account.cooldownFamilies = cooled.cooldownFamilies
|
|
@@ -214,7 +232,7 @@ export async function* streamWithRotation({
|
|
|
214
232
|
const code = String(err && err.code || '')
|
|
215
233
|
const reason = isSoft
|
|
216
234
|
? REASON_DEGRADED
|
|
217
|
-
: (status === 401 || code === 'AUTH' || code === 'TOKEN_REVOKED')
|
|
235
|
+
: (isAuth || status === 401 || code === 'AUTH' || code === 'TOKEN_REVOKED')
|
|
218
236
|
? REASON_REVOKED
|
|
219
237
|
: (status === 403 || code === 'HARD_LIMIT' || code === 'LICENSE_REQUIRED')
|
|
220
238
|
? REASON_HARD_LIMIT
|
|
@@ -226,11 +244,14 @@ export async function* streamWithRotation({
|
|
|
226
244
|
account.quarantineAttempts = err.warmupAccount.quarantineAttempts
|
|
227
245
|
account.status = err.warmupAccount.status
|
|
228
246
|
} else {
|
|
229
|
-
const quarantined = putInQuarantine(account, reason, nowMs(), accurateUntil)
|
|
247
|
+
const quarantined = putInQuarantine(account, reason, nowMs(), accurateUntil || (nowMs() + (isAuth ? 86400000 : 3600000)))
|
|
230
248
|
account.quarantineUntil = quarantined.quarantineUntil
|
|
231
249
|
account.quarantineReason = quarantined.quarantineReason
|
|
232
250
|
account.quarantineAttempts = (account.quarantineAttempts || 0) + 1
|
|
233
251
|
}
|
|
252
|
+
if (isAuth || reason === REASON_REVOKED) {
|
|
253
|
+
account.isRevoked = true
|
|
254
|
+
}
|
|
234
255
|
|
|
235
256
|
currentAttempt.quarantineReason = account.quarantineReason || reason
|
|
236
257
|
currentAttempt.quarantineUntil = account.quarantineUntil
|
package/lib/updater.js
CHANGED
|
@@ -24,6 +24,27 @@ export function isLoopback(value) {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
export function isTrustedUpdateRequest(request, targetCtx) {
|
|
27
|
+
const secFetchSite = String(header(request, 'sec-fetch-site') || '').toLowerCase()
|
|
28
|
+
if (secFetchSite === 'cross-site') return false
|
|
29
|
+
|
|
30
|
+
const origin = header(request, 'origin')
|
|
31
|
+
const host = header(request, 'host')
|
|
32
|
+
if (origin && host) {
|
|
33
|
+
try {
|
|
34
|
+
const u = new URL(origin)
|
|
35
|
+
if (u.protocol === 'http:' || u.protocol === 'https:') {
|
|
36
|
+
if (u.host.toLowerCase() !== host.toLowerCase()) return false
|
|
37
|
+
} else if (u.protocol === 'dsh-app:') {
|
|
38
|
+
const appHost = u.host.toLowerCase()
|
|
39
|
+
if (appHost && appHost !== 'app' && appHost !== 'localhost') return false
|
|
40
|
+
} else {
|
|
41
|
+
return false
|
|
42
|
+
}
|
|
43
|
+
} catch {
|
|
44
|
+
return false
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
27
48
|
const customHeader = header(request, UPDATE_HEADER)
|
|
28
49
|
if (customHeader === '1') {
|
|
29
50
|
const remote = request.socket?.remoteAddress
|