@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/index.js
CHANGED
|
@@ -2,6 +2,7 @@ import { plainConfig } from './config-schema.js'
|
|
|
2
2
|
export { plainConfig } from './config-schema.js'
|
|
3
3
|
import { readFileSync } from 'node:fs'
|
|
4
4
|
import { bestEffort } from './utils.js'
|
|
5
|
+
import { sweepExpiredPending } from './oauth-pending.js'
|
|
5
6
|
export { Config, publicConfig, Slot, defaultSlots } from './config-schema.js'
|
|
6
7
|
import { Config } from './config-schema.js'
|
|
7
8
|
import { registerRoutes } from './routes.js'
|
|
@@ -21,8 +22,10 @@ import { proxyFetch } from './proxy.js'
|
|
|
21
22
|
import { HistoryStore, resolveHistoryDir } from './history.js'
|
|
22
23
|
import { createDiagnosticsReport } from './diagnostics.js'
|
|
23
24
|
import { createAccountsView } from './accounts-view.js'
|
|
25
|
+
import { inspectScopes } from './scope-inspector.js'
|
|
24
26
|
import { createAdapterManager } from './adapter-manager.js'
|
|
25
|
-
import { pickAccount } from './rotate.js'
|
|
27
|
+
import { pickAccount, isAuthError } from './rotate.js'
|
|
28
|
+
import { notifySessionExpired } from './alerts.js'
|
|
26
29
|
|
|
27
30
|
export { observeForecast, estimateForecast } from './forecast.js'
|
|
28
31
|
export { formatRelativeReset } from './relative-time.js'
|
|
@@ -31,6 +34,9 @@ export { exportVault, importVault } from './vault.js'
|
|
|
31
34
|
export { recordAlert, getRecentAlerts, checkQuotaThresholds, notifySessionExpired } from './alerts.js'
|
|
32
35
|
export { calculateBurnRatePerHour, computePacingRisk } from './forecast.js'
|
|
33
36
|
export { needsWarmupProbe, enterProbing, resolveWarmupSuccess, resolveWarmupFailure } from './quarantine.js'
|
|
37
|
+
export { encodeQrMatrix, renderAsciiQr, renderSvgQr, renderSvgDataUrl, generateSshTunnelCommand, getHeadlessAuthInfo, handleHeadlessRedirect } from './headless-auth.js'
|
|
38
|
+
export { pushToWebdav, pullFromWebdav, pushToGist, pullFromGist, buildS3SigV4Headers, pushToS3, pullFromS3, syncVaultPush, syncVaultPull, getVaultSyncStatus, resetVaultSyncStatus } from './vault-sync.js'
|
|
39
|
+
import { syncVaultPush, syncVaultPull, getVaultSyncStatus } from './vault-sync.js'
|
|
34
40
|
|
|
35
41
|
export const name = '@goodandready/dsh-subscriptions'
|
|
36
42
|
export const inject = ['llm', 'credentials', 'webServer', 'settings']
|
|
@@ -380,11 +386,15 @@ export function apply(ctx, rawConfig) {
|
|
|
380
386
|
const pending = new Map()
|
|
381
387
|
|
|
382
388
|
function sweepPending(now) {
|
|
383
|
-
|
|
384
|
-
if (now - row.createdAt > PENDING_TTL_MS) pending.delete(stateKey)
|
|
385
|
-
}
|
|
389
|
+
sweepExpiredPending(pending, now, PENDING_TTL_MS)
|
|
386
390
|
}
|
|
387
391
|
|
|
392
|
+
ctx.effect(() => {
|
|
393
|
+
const id = setInterval(() => sweepPending(Date.now()), 60 * 1000)
|
|
394
|
+
if (id && typeof id.unref === 'function') id.unref()
|
|
395
|
+
return () => clearInterval(id)
|
|
396
|
+
}, 'dsh-subscriptions: pending oauth sweep')
|
|
397
|
+
|
|
388
398
|
async function completeOAuth({ provider, index, code, state }) {
|
|
389
399
|
if (!state) throw new Error('missing oauth state')
|
|
390
400
|
sweepPending(Date.now())
|
|
@@ -401,14 +411,26 @@ export function apply(ctx, rawConfig) {
|
|
|
401
411
|
challenge: row.challenge,
|
|
402
412
|
state: row.state,
|
|
403
413
|
}, code, fetchForRef(ref) || fetch)
|
|
414
|
+
const inspection = inspectScopes(actualProvider, blob.scopes)
|
|
415
|
+
blob.scopes = inspection.scopes
|
|
416
|
+
blob.scopeWarning = inspection.scopeWarning || ""
|
|
404
417
|
const slots = normalizeSlots(live().slots)
|
|
405
418
|
const slot = slots.find((s) => s.ref === ref)
|
|
406
419
|
if (slot && slot.label) blob.label = slot.label
|
|
407
|
-
|
|
420
|
+
let existing = {}
|
|
421
|
+
try { existing = await store.loadBlob(ref) } catch { /* no existing blob on fresh auth */ }
|
|
422
|
+
const merged = { ...existing, ...blob }
|
|
423
|
+
await store.saveBlob(ref, merged)
|
|
424
|
+
store.recordSuccess(ref)
|
|
425
|
+
store.clearRefreshFailure(ref)
|
|
426
|
+
store.clearCooldown(ref)
|
|
408
427
|
pending.delete(row.state)
|
|
409
428
|
await syncAdapter()
|
|
410
429
|
refreshModels().catch(() => {})
|
|
411
|
-
|
|
430
|
+
if (live().vaultSyncEnabled && live().vaultSyncAutoPush) {
|
|
431
|
+
syncVaultPush({ live: live(), store }).catch(() => {})
|
|
432
|
+
}
|
|
433
|
+
return { ref, label: pmL(merged.label || merged.email) || displayName(actualProvider) }
|
|
412
434
|
}
|
|
413
435
|
|
|
414
436
|
// Privacy masking
|
|
@@ -449,7 +471,7 @@ export function apply(ctx, rawConfig) {
|
|
|
449
471
|
}
|
|
450
472
|
|
|
451
473
|
const diagnosticsReport = () => createDiagnosticsReport({ live, store, history, NS, pkgVersion, heartbeatStats })
|
|
452
|
-
const accountsView = () => createAccountsView({ live, store, fetch, pmL })
|
|
474
|
+
const accountsView = () => createAccountsView({ live, store, fetch, pmL, pmE, history })
|
|
453
475
|
|
|
454
476
|
// Ponytail: background refresh ahead of expiry, single timer
|
|
455
477
|
ctx.effect(() => {
|
|
@@ -473,15 +495,51 @@ export function apply(ctx, rawConfig) {
|
|
|
473
495
|
if (!raw) continue
|
|
474
496
|
const blob = await store.loadBlob(ref).catch(() => null)
|
|
475
497
|
if (!blob || !blob.refreshToken) continue
|
|
476
|
-
|
|
498
|
+
let fresh
|
|
499
|
+
try {
|
|
500
|
+
fresh = await store.ensureFresh(slot.provider, blob, ref)
|
|
501
|
+
} catch (refreshErr) {
|
|
502
|
+
if (isAuthError(refreshErr)) {
|
|
503
|
+
store.recordBroken(ref)
|
|
504
|
+
store.rememberQuarantine(ref, "SESSION_REVOKED", Date.now() + 86400000, 1)
|
|
505
|
+
try {
|
|
506
|
+
notifySessionExpired({
|
|
507
|
+
ref,
|
|
508
|
+
provider: slot.provider,
|
|
509
|
+
message: refreshErr && refreshErr.message,
|
|
510
|
+
webhookUrl: cfg.webhookUrl,
|
|
511
|
+
fetchImpl: fetch,
|
|
512
|
+
nowMs: Date.now(),
|
|
513
|
+
})
|
|
514
|
+
} catch { /* alert best-effort */ }
|
|
515
|
+
}
|
|
516
|
+
continue
|
|
517
|
+
}
|
|
477
518
|
if (!fresh) continue
|
|
478
519
|
const vendor = getVendor(slot.provider)
|
|
479
520
|
if (typeof vendor.check !== 'function') continue
|
|
480
521
|
const cfg2 = vendorConfig(slot.provider, live())
|
|
481
522
|
const probeFetch = async (u, i) => ((typeof fetchForRef === 'function' && fetchForRef(ref)) || fetch)(u, i)
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
523
|
+
try {
|
|
524
|
+
await vendor.check(fresh, cfg2, probeFetch)
|
|
525
|
+
if (typeof store.rememberDoctorVerdict === 'function') store.rememberDoctorVerdict(ref, null)
|
|
526
|
+
} catch (checkErr) {
|
|
527
|
+
if (isAuthError(checkErr)) {
|
|
528
|
+
store.recordBroken(ref)
|
|
529
|
+
store.rememberQuarantine(ref, "SESSION_REVOKED", Date.now() + 86400000, 1)
|
|
530
|
+
try {
|
|
531
|
+
notifySessionExpired({
|
|
532
|
+
ref,
|
|
533
|
+
provider: slot.provider,
|
|
534
|
+
message: checkErr && checkErr.message,
|
|
535
|
+
webhookUrl: cfg.webhookUrl,
|
|
536
|
+
fetchImpl: fetch,
|
|
537
|
+
nowMs: Date.now(),
|
|
538
|
+
})
|
|
539
|
+
} catch { /* alert best-effort */ }
|
|
540
|
+
}
|
|
541
|
+
if (logger && logger.warn) logger.warn('[dsh-subscriptions] probe ' + ref + ': ' + String(checkErr && checkErr.message || checkErr).slice(0, 200))
|
|
542
|
+
}
|
|
485
543
|
} catch { /* best-effort */ }
|
|
486
544
|
}
|
|
487
545
|
}
|
|
@@ -499,6 +557,35 @@ export function apply(ctx, rawConfig) {
|
|
|
499
557
|
return () => clearInterval(timer)
|
|
500
558
|
}, 'dsh-subscriptions: probe loop')
|
|
501
559
|
|
|
560
|
+
// #562: Background automated remote vault sync loop
|
|
561
|
+
ctx.effect(() => {
|
|
562
|
+
const checkSync = async () => {
|
|
563
|
+
const cfg = live()
|
|
564
|
+
if (!cfg.vaultSyncEnabled || !cfg.vaultSyncIntervalHours || cfg.vaultSyncIntervalHours <= 0) return
|
|
565
|
+
const status = getVaultSyncStatus()
|
|
566
|
+
const now = Date.now()
|
|
567
|
+
const intervalMs = cfg.vaultSyncIntervalHours * 3600 * 1000
|
|
568
|
+
const lastPull = status.lastPullAt ? new Date(status.lastPullAt).getTime() : 0
|
|
569
|
+
if (now - lastPull >= intervalMs) {
|
|
570
|
+
try {
|
|
571
|
+
await syncVaultPull({
|
|
572
|
+
live: cfg,
|
|
573
|
+
store,
|
|
574
|
+
settingsApi: getSettingsApi(),
|
|
575
|
+
syncAdapter,
|
|
576
|
+
refreshModels,
|
|
577
|
+
})
|
|
578
|
+
} catch { /* status captures error */ }
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
const intervalId = setInterval(() => { checkSync().catch(() => {}) }, 60 * 60 * 1000)
|
|
582
|
+
const initialTimer = setTimeout(() => { checkSync().catch(() => {}) }, 30 * 1000)
|
|
583
|
+
return () => {
|
|
584
|
+
clearInterval(intervalId)
|
|
585
|
+
clearTimeout(initialTimer)
|
|
586
|
+
}
|
|
587
|
+
}, 'dsh-subscriptions: vault-sync-loop')
|
|
588
|
+
|
|
502
589
|
registerRoutes(ctx, {
|
|
503
590
|
NS,
|
|
504
591
|
live,
|
package/lib/jwt.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
function decodeJwtPayload(token) {
|
|
2
|
-
const raw = String(token ||
|
|
3
|
-
const parts = raw.split(
|
|
1
|
+
export function decodeJwtPayload(token) {
|
|
2
|
+
const raw = String(token || "")
|
|
3
|
+
const parts = raw.split(".")
|
|
4
4
|
if (parts.length < 2) return null
|
|
5
5
|
try {
|
|
6
|
-
const json = Buffer.from(parts[1],
|
|
6
|
+
const json = Buffer.from(parts[1], "base64url").toString("utf8")
|
|
7
7
|
return JSON.parse(json)
|
|
8
8
|
} catch {
|
|
9
9
|
return null
|
|
@@ -12,42 +12,84 @@ function decodeJwtPayload(token) {
|
|
|
12
12
|
|
|
13
13
|
export function chatgptAccountId(token) {
|
|
14
14
|
const payload = decodeJwtPayload(token)
|
|
15
|
-
if (!payload || typeof payload !==
|
|
15
|
+
if (!payload || typeof payload !== "object") return ""
|
|
16
16
|
if (payload.chatgpt_account_id) return String(payload.chatgpt_account_id)
|
|
17
|
-
const nested = payload[
|
|
17
|
+
const nested = payload["https://api.openai.com/auth"]
|
|
18
18
|
if (nested && nested.chatgpt_account_id) return String(nested.chatgpt_account_id)
|
|
19
|
+
if (nested && nested.user_id) return String(nested.user_id)
|
|
19
20
|
const orgs = payload.organizations
|
|
20
21
|
if (Array.isArray(orgs) && orgs[0] && orgs[0].id) return String(orgs[0].id)
|
|
21
|
-
return
|
|
22
|
+
if (payload.sub) return String(payload.sub)
|
|
23
|
+
return ""
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
export function emailFromToken(token) {
|
|
25
27
|
const payload = decodeJwtPayload(token)
|
|
26
|
-
if (!payload) return
|
|
27
|
-
|
|
28
|
+
if (!payload || typeof payload !== "object") return ""
|
|
29
|
+
if (payload.email) return String(payload.email)
|
|
30
|
+
const openaiProf = payload["https://api.openai.com/profile"]
|
|
31
|
+
if (openaiProf && openaiProf.email) return String(openaiProf.email)
|
|
32
|
+
if (payload.preferred_username) return String(payload.preferred_username)
|
|
33
|
+
if (payload.upn) return String(payload.upn)
|
|
34
|
+
return ""
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function planFromToken(token) {
|
|
38
|
+
const payload = decodeJwtPayload(token)
|
|
39
|
+
if (!payload || typeof payload !== "object") return ""
|
|
40
|
+
const nested = payload["https://api.openai.com/auth"]
|
|
41
|
+
if (nested && nested.chatgpt_plan_type) return String(nested.chatgpt_plan_type)
|
|
42
|
+
const anthropic = payload["https://api.anthropic.com"]
|
|
43
|
+
if (anthropic && anthropic.plan) return String(anthropic.plan)
|
|
44
|
+
if (payload.plan_type) return String(payload.plan_type)
|
|
45
|
+
if (payload.plan) return typeof payload.plan === "string" ? payload.plan : (payload.plan.name || "")
|
|
46
|
+
if (payload.subscription && payload.subscription.tier) return String(payload.subscription.tier)
|
|
47
|
+
if (payload.subscription_tier) return String(payload.subscription_tier)
|
|
48
|
+
if (payload.paidTierName) return String(payload.paidTierName)
|
|
49
|
+
return ""
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function avatarFromToken(token) {
|
|
53
|
+
const payload = decodeJwtPayload(token)
|
|
54
|
+
if (!payload || typeof payload !== "object") return ""
|
|
55
|
+
if (payload.picture && typeof payload.picture === "string") return payload.picture
|
|
56
|
+
if (payload.avatar_url && typeof payload.avatar_url === "string") return payload.avatar_url
|
|
57
|
+
if (payload.avatar && typeof payload.avatar === "string") return payload.avatar
|
|
58
|
+
const openaiProf = payload["https://api.openai.com/profile"]
|
|
59
|
+
if (openaiProf && openaiProf.picture && typeof openaiProf.picture === "string") return openaiProf.picture
|
|
60
|
+
return ""
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function scopesFromToken(token) {
|
|
64
|
+
const payload = decodeJwtPayload(token)
|
|
65
|
+
if (!payload || typeof payload !== "object") return []
|
|
66
|
+
const raw = payload.scope || payload.scp || ""
|
|
67
|
+
if (Array.isArray(raw)) return raw.map(String).filter(Boolean)
|
|
68
|
+
if (typeof raw === "string") return raw.split(/[\s,]+/).filter(Boolean)
|
|
69
|
+
return []
|
|
28
70
|
}
|
|
29
71
|
|
|
30
72
|
export function isGoogleClientId(str) {
|
|
31
|
-
if (typeof str !==
|
|
73
|
+
if (typeof str !== "string") return false
|
|
32
74
|
const trimmed = str.trim()
|
|
33
75
|
return /^[0-9]+-[a-zA-Z0-9_-]+\.apps\.googleusercontent\.com$/.test(trimmed)
|
|
34
76
|
}
|
|
35
77
|
|
|
36
78
|
export function extractGoogleClientIdFromJwt(token) {
|
|
37
79
|
const payload = decodeJwtPayload(token)
|
|
38
|
-
if (!payload || typeof payload !==
|
|
80
|
+
if (!payload || typeof payload !== "object") return ""
|
|
39
81
|
|
|
40
|
-
const azp = typeof payload.azp ===
|
|
82
|
+
const azp = typeof payload.azp === "string" ? payload.azp.trim() : ""
|
|
41
83
|
const rawAud = payload.aud
|
|
42
84
|
|
|
43
85
|
const audiences = Array.isArray(rawAud)
|
|
44
|
-
? rawAud.filter(a => typeof a ===
|
|
45
|
-
: (typeof rawAud ===
|
|
86
|
+
? rawAud.filter(a => typeof a === "string").map(a => a.trim())
|
|
87
|
+
: (typeof rawAud === "string" && rawAud.trim() ? [rawAud.trim()] : [])
|
|
46
88
|
|
|
47
89
|
if (azp) {
|
|
48
|
-
if (!isGoogleClientId(azp)) return
|
|
90
|
+
if (!isGoogleClientId(azp)) return ""
|
|
49
91
|
if (audiences.length > 0 && !audiences.includes(azp)) {
|
|
50
|
-
return
|
|
92
|
+
return ""
|
|
51
93
|
}
|
|
52
94
|
return azp
|
|
53
95
|
}
|
|
@@ -57,5 +99,30 @@ export function extractGoogleClientIdFromJwt(token) {
|
|
|
57
99
|
return validAuds[0]
|
|
58
100
|
}
|
|
59
101
|
|
|
60
|
-
return
|
|
102
|
+
return ""
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function organizationsFromToken(token) {
|
|
106
|
+
const payload = decodeJwtPayload(token)
|
|
107
|
+
if (!payload || typeof payload !== "object") return []
|
|
108
|
+
const raw = payload.organizations ||
|
|
109
|
+
(payload["https://api.openai.com/auth"] && payload["https://api.openai.com/auth"].organizations) ||
|
|
110
|
+
payload.orgs ||
|
|
111
|
+
(payload["https://api.anthropic.com/auth"] && payload["https://api.anthropic.com/auth"].organizations) ||
|
|
112
|
+
[]
|
|
113
|
+
if (!Array.isArray(raw)) return []
|
|
114
|
+
const out = []
|
|
115
|
+
for (const item of raw) {
|
|
116
|
+
if (typeof item === "string" && item.trim()) {
|
|
117
|
+
out.push({ id: item.trim(), name: item.trim() })
|
|
118
|
+
} else if (item && typeof item === "object") {
|
|
119
|
+
const id = String(item.id || item.org_id || item.uuid || "").trim()
|
|
120
|
+
const name = String(item.title || item.name || item.org_name || id).trim()
|
|
121
|
+
const role = item.role ? String(item.role).trim() : undefined
|
|
122
|
+
if (id) {
|
|
123
|
+
out.push({ id, name: name || id, ...(role ? { role } : {}) })
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return out
|
|
61
128
|
}
|
package/lib/loopback.js
CHANGED
|
@@ -30,8 +30,9 @@ export function startLoopback({ redirectUri, timeoutMs = 10 * 60 * 1000, onCode
|
|
|
30
30
|
const promise = new Promise((resolve, reject) => {
|
|
31
31
|
const server = http.createServer((req, res) => {
|
|
32
32
|
const url = new URL(req.url || '/', `http://127.0.0.1:${port}`)
|
|
33
|
-
// The provider may redirect to a suffixed path (e.g. /auth/callback/extra)
|
|
34
|
-
|
|
33
|
+
// The provider may redirect to a suffixed path (e.g. /auth/callback/extra) (#545)
|
|
34
|
+
const matchesPath = url.pathname === path || (path.endsWith("/") ? url.pathname.startsWith(path) : url.pathname.startsWith(path + "/"));
|
|
35
|
+
if (!matchesPath) {
|
|
35
36
|
res.writeHead(404, { 'Content-Type': 'text/html; charset=utf-8' })
|
|
36
37
|
res.end(ERR_HTML)
|
|
37
38
|
return
|
package/lib/mask.js
CHANGED
|
@@ -20,6 +20,22 @@ export function maskText(value) {
|
|
|
20
20
|
return String(value || '').replace(EMAIL_RE, (m) => maskEmail(m))
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
const SECRET_PATTERNS = [
|
|
24
|
+
[/Bearer\s+[A-Za-z0-9._~+/-]+=*/gi, 'Bearer [REDACTED]'],
|
|
25
|
+
[/\b(?:sk|ghp|gho|ghu|ghs|ghr|github_pat|xox[baprs])[-_][A-Za-z0-9._-]{8,}/g, '[REDACTED]'],
|
|
26
|
+
[/\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g, '[REDACTED]'],
|
|
27
|
+
[/\b(access_token|refresh_token|token|key|secret|password)=([^&\s]+)/gi, '$1=[REDACTED]'],
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
// #578: redact credentials as well as emails from free-text diagnostics.
|
|
31
|
+
export function maskSecrets(value) {
|
|
32
|
+
let text = maskText(value)
|
|
33
|
+
for (const [pattern, replacement] of SECRET_PATTERNS) {
|
|
34
|
+
text = text.replace(pattern, replacement)
|
|
35
|
+
}
|
|
36
|
+
return text
|
|
37
|
+
}
|
|
38
|
+
|
|
23
39
|
export function cleanErrorMessage(raw) {
|
|
24
40
|
if (!raw) return ''
|
|
25
41
|
let text = typeof raw === 'string' ? raw : (raw.message || String(raw))
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// #580: shared expiry sweep for in-memory OAuth pending rows.
|
|
2
|
+
export function sweepExpiredPending(pending, nowMs, ttlMs) {
|
|
3
|
+
let removed = 0
|
|
4
|
+
for (const [stateKey, row] of pending) {
|
|
5
|
+
const createdAt = Number(row && row.createdAt)
|
|
6
|
+
if (!Number.isFinite(createdAt) || Number(nowMs) - createdAt > Number(ttlMs)) {
|
|
7
|
+
pending.delete(stateKey)
|
|
8
|
+
removed += 1
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
return removed
|
|
12
|
+
}
|
package/lib/pricing.js
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// Subscription Value Delivered & API Cost Equivalent Counter (#561)
|
|
2
|
+
// Translates raw prompt, completion and cache tokens into equivalent official API pricing,
|
|
3
|
+
// comparing delivered value against monthly subscription fee ($20/mo standard).
|
|
4
|
+
|
|
5
|
+
export const DEFAULT_SUBSCRIPTION_MONTHLY_COST = 20.0
|
|
6
|
+
|
|
7
|
+
export const TOKEN_RATES = {
|
|
8
|
+
'claude-3-7-sonnet': { prompt: 3.0, completion: 15.0, cacheRead: 0.3 },
|
|
9
|
+
'claude-3-5-sonnet': { prompt: 3.0, completion: 15.0, cacheRead: 0.3 },
|
|
10
|
+
'claude-3-5-haiku': { prompt: 0.8, completion: 4.0, cacheRead: 0.08 },
|
|
11
|
+
'claude-3-opus': { prompt: 15.0, completion: 75.0, cacheRead: 1.5 },
|
|
12
|
+
'gpt-4o': { prompt: 2.5, completion: 10.0, cacheRead: 1.25 },
|
|
13
|
+
'gpt-4o-mini': { prompt: 0.15, completion: 0.6, cacheRead: 0.075 },
|
|
14
|
+
'o1': { prompt: 15.0, completion: 60.0, cacheRead: 7.5 },
|
|
15
|
+
'o3-mini': { prompt: 1.1, completion: 4.4, cacheRead: 0.55 },
|
|
16
|
+
'grok-2': { prompt: 2.0, completion: 10.0, cacheRead: 0.2 },
|
|
17
|
+
'sonar-reasoning': { prompt: 5.0, completion: 25.0, cacheRead: 0.5 },
|
|
18
|
+
'sonar-pro': { prompt: 3.0, completion: 15.0, cacheRead: 0.3 },
|
|
19
|
+
'sonar': { prompt: 1.0, completion: 5.0, cacheRead: 0.1 },
|
|
20
|
+
'gemini-1.5-pro': { prompt: 1.25, completion: 5.0, cacheRead: 0.3 },
|
|
21
|
+
'gemini-2.0-flash': { prompt: 0.1, completion: 0.4, cacheRead: 0.025 },
|
|
22
|
+
'default': { prompt: 2.5, completion: 10.0, cacheRead: 0.5 },
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Resolves standard token rates per 1M tokens for a given model identifier.
|
|
27
|
+
* @param {string} [modelId]
|
|
28
|
+
* @returns {{ prompt: number, completion: number, cacheRead: number }}
|
|
29
|
+
*/
|
|
30
|
+
export function lookupTokenRate(modelId = '') {
|
|
31
|
+
const m = String(modelId || '').toLowerCase()
|
|
32
|
+
for (const [key, rates] of Object.entries(TOKEN_RATES)) {
|
|
33
|
+
if (key !== 'default' && m.includes(key)) {
|
|
34
|
+
return rates
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return TOKEN_RATES.default
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Calculates dollar cost equivalent for the given token quantities.
|
|
42
|
+
* @param {number} promptTokens
|
|
43
|
+
* @param {number} completionTokens
|
|
44
|
+
* @param {number} [cacheReadTokens]
|
|
45
|
+
* @param {string} [modelId]
|
|
46
|
+
* @returns {number}
|
|
47
|
+
*/
|
|
48
|
+
export function calculateEquivalentCost(promptTokens = 0, completionTokens = 0, cacheReadTokens = 0, modelId = 'default') {
|
|
49
|
+
const rates = lookupTokenRate(modelId)
|
|
50
|
+
const p = (Number(promptTokens) || 0) * (rates.prompt / 1_000_000)
|
|
51
|
+
const c = (Number(completionTokens) || 0) * (rates.completion / 1_000_000)
|
|
52
|
+
const cr = (Number(cacheReadTokens) || 0) * (rates.cacheRead / 1_000_000)
|
|
53
|
+
return Math.round((p + c + cr) * 100) / 100
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Computes value delivered metrics and ROI ratio for an account.
|
|
58
|
+
* @param {object} params
|
|
59
|
+
* @param {number} [params.promptTokens]
|
|
60
|
+
* @param {number} [params.completionTokens]
|
|
61
|
+
* @param {number} [params.cacheReadTokens]
|
|
62
|
+
* @param {string} [params.modelId]
|
|
63
|
+
* @param {number} [params.subscriptionCost]
|
|
64
|
+
* @returns {object}
|
|
65
|
+
*/
|
|
66
|
+
export function calculateValueDelivered({
|
|
67
|
+
promptTokens = 0,
|
|
68
|
+
completionTokens = 0,
|
|
69
|
+
cacheReadTokens = 0,
|
|
70
|
+
modelId = 'default',
|
|
71
|
+
subscriptionCost = DEFAULT_SUBSCRIPTION_MONTHLY_COST,
|
|
72
|
+
} = {}) {
|
|
73
|
+
const equivalentCost = calculateEquivalentCost(promptTokens, completionTokens, cacheReadTokens, modelId)
|
|
74
|
+
const subCost = Number(subscriptionCost) > 0 ? Number(subscriptionCost) : DEFAULT_SUBSCRIPTION_MONTHLY_COST
|
|
75
|
+
const roiMultiplier = Math.round((equivalentCost / subCost) * 10) / 10
|
|
76
|
+
const roiLabel = `${roiMultiplier}x`
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
equivalentCost,
|
|
80
|
+
formattedCost: `$${equivalentCost.toFixed(2)}`,
|
|
81
|
+
subscriptionCost: subCost,
|
|
82
|
+
formattedSubscriptionCost: `$${subCost.toFixed(2)}`,
|
|
83
|
+
roiMultiplier,
|
|
84
|
+
roiLabel,
|
|
85
|
+
promptTokens: Number(promptTokens) || 0,
|
|
86
|
+
completionTokens: Number(completionTokens) || 0,
|
|
87
|
+
cacheReadTokens: Number(cacheReadTokens) || 0,
|
|
88
|
+
}
|
|
89
|
+
}
|
package/lib/refs.js
CHANGED
package/lib/reset-clock.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { parseHeaders } from './ratelimit.js'
|
|
2
|
+
import { formatRelativeReset } from './relative-time.js'
|
|
2
3
|
|
|
3
4
|
export const VENDOR_WINDOW_MS = {
|
|
4
5
|
claude: 5 * 3600 * 1000,
|
|
5
6
|
codex: 3 * 3600 * 1000,
|
|
6
7
|
grok: 2 * 3600 * 1000,
|
|
8
|
+
copilot: 24 * 3600 * 1000,
|
|
9
|
+
antigravity: 24 * 3600 * 1000,
|
|
7
10
|
}
|
|
8
11
|
|
|
9
12
|
export function parseResetFromText(message, nowMs = Date.now()) {
|
|
@@ -85,3 +88,55 @@ export function resolveAccurateReset(err, account, provider, nowMs = Date.now(),
|
|
|
85
88
|
|
|
86
89
|
return null
|
|
87
90
|
}
|
|
91
|
+
|
|
92
|
+
export function formatRollingCountdown(resetAt, lang = 'en', now = Date.now()) {
|
|
93
|
+
const ts = Number(resetAt)
|
|
94
|
+
if (!Number.isFinite(ts) || ts <= 0) return ''
|
|
95
|
+
const d = new Date(ts)
|
|
96
|
+
const pad = (n) => String(n).padStart(2, '0')
|
|
97
|
+
const timeStr = `${pad(d.getHours())}:${pad(d.getMinutes())}`
|
|
98
|
+
const relStr = formatRelativeReset(ts, lang, now)
|
|
99
|
+
if (lang === 'zh') {
|
|
100
|
+
return `额度重置于 ${timeStr} (${relStr})`
|
|
101
|
+
}
|
|
102
|
+
return `Window resets at ${timeStr} (${relStr})`
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function computeEffectiveResetAt(account, provider, now = Date.now()) {
|
|
106
|
+
if (!account) return null
|
|
107
|
+
const nowMs = Number(now) || Date.now()
|
|
108
|
+
const candidates = []
|
|
109
|
+
|
|
110
|
+
if (account.quota && account.quota.resetAt) {
|
|
111
|
+
const r = Number(account.quota.resetAt)
|
|
112
|
+
if (Number.isFinite(r) && r > nowMs) candidates.push(r)
|
|
113
|
+
}
|
|
114
|
+
if (account.cooldownUntil) {
|
|
115
|
+
const c = Number(account.cooldownUntil)
|
|
116
|
+
if (Number.isFinite(c) && c > nowMs) candidates.push(c)
|
|
117
|
+
}
|
|
118
|
+
if (account.quarantineUntil) {
|
|
119
|
+
const q = Number(account.quarantineUntil)
|
|
120
|
+
if (Number.isFinite(q) && q > nowMs) candidates.push(q)
|
|
121
|
+
}
|
|
122
|
+
if (Array.isArray(account.usage)) {
|
|
123
|
+
for (const w of account.usage) {
|
|
124
|
+
if (w && w.resetAt) {
|
|
125
|
+
const wr = Number(w.resetAt)
|
|
126
|
+
if (Number.isFinite(wr) && wr > nowMs) candidates.push(wr)
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (candidates.length > 0) {
|
|
132
|
+
return Math.min(...candidates)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const p = String(provider || (account && account.provider) || '').toLowerCase()
|
|
136
|
+
const windowDefault = VENDOR_WINDOW_MS[p]
|
|
137
|
+
if (windowDefault && account.usagePercent != null && Number(account.usagePercent) >= 100) {
|
|
138
|
+
return nowMs + windowDefault
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return null
|
|
142
|
+
}
|
package/lib/rotate.js
CHANGED
|
@@ -17,14 +17,28 @@ export function isRegionError(err) {
|
|
|
17
17
|
return status === 400 && (/FAILED_PRECONDITION/i.test(msg) || /User location is not supported/i.test(msg))
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
export function isAuthError(err) {
|
|
21
|
+
if (!err || typeof err !== "object") return false
|
|
22
|
+
const status = Number(err.status || err.statusCode) || 0
|
|
23
|
+
if (status === 401) return true
|
|
24
|
+
const code = String(err.code || (err.failure && err.failure.code) || "").toUpperCase()
|
|
25
|
+
if (code === "AUTH" || code === "TOKEN_REVOKED" || code === "UNAUTHORIZED" || code === "INVALID_TOKEN" || code === "SESSION_EXPIRED") return true
|
|
26
|
+
const msg = String(err.message || "")
|
|
27
|
+
if (/invalid_grant/i.test(msg)) return true
|
|
28
|
+
if (/token (?:has been )?revoked/i.test(msg)) return true
|
|
29
|
+
if (/session (?:expired|revoked|invalid)/i.test(msg)) return true
|
|
30
|
+
if (/unauthorized/i.test(msg) && (status === 0 || status === 400 || status === 401)) return true
|
|
31
|
+
return false
|
|
32
|
+
}
|
|
33
|
+
|
|
20
34
|
export function isSwitchableError(err) {
|
|
21
|
-
if (!err || typeof err !==
|
|
22
|
-
if (err.name ===
|
|
23
|
-
if (err.name ===
|
|
35
|
+
if (!err || typeof err !== "object") return false
|
|
36
|
+
if (err.name === "TimeoutError" || err.code === "TIMEOUT" || err.code === "EMPTY_RESPONSE" || err.code === "SOFT_FAILURE" || err.isSoftFailure) return true
|
|
37
|
+
if (err.name === "AbortError" || err.code === "ABORT_ERR" || err.code === "ERR_ABORTED" || (err.message && /aborted/i.test(err.message))) return false
|
|
24
38
|
const code = err.code || (err.failure && err.failure.code)
|
|
25
39
|
if (SWITCH_CODES.has(code)) return true
|
|
26
40
|
if (isRegionError(err)) return true
|
|
27
|
-
const status = err.status || err.statusCode
|
|
41
|
+
const status = Number(err.status || err.statusCode) || 0
|
|
28
42
|
return status === 401 || status === 403 || status === 429 || status === 500 || status === 502 || status === 503
|
|
29
43
|
}
|
|
30
44
|
|