@goodandready/dsh-subscriptions 0.6.71 → 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 +13 -0
- package/README.ru.md +13 -0
- package/README.zh.md +13 -0
- package/lib/accounts-view.js +101 -5
- package/lib/accounts.js +150 -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 +944 -828
- package/lib/config-schema.js +46 -4
- package/lib/diagnostic-wizard.js +2 -5
- package/lib/diagnostics.js +2 -2
- package/lib/doctor.js +163 -0
- 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/instructions.js +138 -0
- 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 +203 -0
- package/lib/routes/oauth.js +44 -2
- package/lib/routes/proxy.js +14 -0
- package/lib/routes/status.js +99 -5
- 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/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
|
|
package/lib/routes/accounts.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { diagnoseVendorError } from '../doctor.js'
|
|
1
2
|
import { exportVault, importVault } from '../vault.js'
|
|
3
|
+
import { syncVaultPush, syncVaultPull, getVaultSyncStatus } from '../vault-sync.js'
|
|
2
4
|
import { normalizeSlots, vendorConfig } from '../accounts.js'
|
|
3
5
|
import { getVendor } from '../vendors/index.js'
|
|
4
6
|
import { notifyQuarantineEntered, notifyQuarantineReleased } from '../alerts.js'
|
|
@@ -445,6 +447,140 @@ export function registerAccountsRoutes(ctx, state) {
|
|
|
445
447
|
}),
|
|
446
448
|
}), 'dsh-subscriptions: /import-token')
|
|
447
449
|
|
|
450
|
+
// Issue #541: Dynamic model catalog discovery & sync
|
|
451
|
+
ctx.effect(() => ctx.webServer.register({
|
|
452
|
+
kind: 'exact',
|
|
453
|
+
path: '/dsh-subscriptions/accounts/sync-models',
|
|
454
|
+
handler: safeJsonHandler(async (req, res) => {
|
|
455
|
+
if (req.method !== 'POST') {
|
|
456
|
+
writeJson(res, 405, { ok: false, error: { code: 'method', message: 'POST only' } })
|
|
457
|
+
return
|
|
458
|
+
}
|
|
459
|
+
if (!isTrustedSettingsRequest(req, ctx)) {
|
|
460
|
+
writeJson(res, 403, { ok: false, error: { code: 'forbidden', message: 'same-origin only' } })
|
|
461
|
+
return
|
|
462
|
+
}
|
|
463
|
+
let payload = {}
|
|
464
|
+
try { payload = JSON.parse((await readBody(req, 64 * 1024)).toString('utf8') || '{}') } catch { /* ignore */ }
|
|
465
|
+
const provider = payload.provider
|
|
466
|
+
const index = payload.index || 1
|
|
467
|
+
if (!provider || !isProvider(provider)) {
|
|
468
|
+
writeJson(res, 400, { ok: false, error: { code: 'invalid_provider', message: 'Invalid or missing provider' } })
|
|
469
|
+
return
|
|
470
|
+
}
|
|
471
|
+
const ref = oauthRef(provider, index)
|
|
472
|
+
const info = await store.describeRef(ref)
|
|
473
|
+
if (!info.configured) {
|
|
474
|
+
writeJson(res, 400, { ok: false, error: { code: 'not_configured', message: 'Account is not configured' } })
|
|
475
|
+
return
|
|
476
|
+
}
|
|
477
|
+
try {
|
|
478
|
+
let blob = await store.loadBlob(ref)
|
|
479
|
+
blob = await store.ensureFresh(provider, blob, ref)
|
|
480
|
+
const vendor = getVendor(provider)
|
|
481
|
+
const cfg = vendorConfig(provider, live())
|
|
482
|
+
const doFetch = (typeof state.fetchForRef === 'function' && state.fetchForRef(ref)) || fetch
|
|
483
|
+
let models = []
|
|
484
|
+
if (typeof vendor.listModels === 'function') {
|
|
485
|
+
const cat = await vendor.listModels(blob, cfg, doFetch)
|
|
486
|
+
if (Array.isArray(cat)) {
|
|
487
|
+
models = cat.map((m) => (typeof m === 'string' ? m : (m.id || m.slug || m.name))).filter(Boolean)
|
|
488
|
+
} else if (cat && Array.isArray(cat.models)) {
|
|
489
|
+
models = cat.models.map((m) => (typeof m === 'string' ? m : (m.id || m.slug || m.name))).filter(Boolean)
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
if (!models.length && vendor.defaults) {
|
|
493
|
+
models = (vendor.defaults().models || []).map((m) => (typeof m === 'string' ? m : (m.id || m.slug || m.name))).filter(Boolean)
|
|
494
|
+
}
|
|
495
|
+
if (typeof store.rememberDynamicModels === 'function') {
|
|
496
|
+
store.rememberDynamicModels(ref, models)
|
|
497
|
+
}
|
|
498
|
+
writeJson(res, 200, {
|
|
499
|
+
ok: true,
|
|
500
|
+
provider,
|
|
501
|
+
index,
|
|
502
|
+
ref,
|
|
503
|
+
count: models.length,
|
|
504
|
+
models,
|
|
505
|
+
})
|
|
506
|
+
} catch (err) {
|
|
507
|
+
const doc = diagnoseVendorError(err, provider)
|
|
508
|
+
writeJson(res, 502, {
|
|
509
|
+
ok: false,
|
|
510
|
+
provider,
|
|
511
|
+
index,
|
|
512
|
+
ref,
|
|
513
|
+
doctor: doc,
|
|
514
|
+
error: { code: 'sync_failed', message: String(err && err.message || err) },
|
|
515
|
+
})
|
|
516
|
+
}
|
|
517
|
+
}),
|
|
518
|
+
}), 'dsh-subscriptions: /accounts/sync-models')
|
|
519
|
+
|
|
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')
|
|
448
584
|
|
|
449
585
|
ctx.effect(() => ctx.webServer.register({
|
|
450
586
|
kind: 'exact',
|
|
@@ -588,4 +724,71 @@ export function registerAccountsRoutes(ctx, state) {
|
|
|
588
724
|
handler: handleVaultImport,
|
|
589
725
|
}), 'dsh-subscriptions: /import-backup')
|
|
590
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')
|
|
591
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 {
|
package/lib/routes/status.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { diagnoseVendorError } from '../doctor.js'
|
|
2
|
+
import { INSTRUCTIONS } from '../instructions.js'
|
|
1
3
|
import {
|
|
2
4
|
getModelReasoningCapabilities,
|
|
3
5
|
setSessionReasoning,
|
|
@@ -505,9 +507,13 @@ export function registerStatusRoutes(ctx, state) {
|
|
|
505
507
|
blob = await store.loadBlob(ref)
|
|
506
508
|
blob = await store.ensureFresh(provider, blob, ref)
|
|
507
509
|
} catch (e) {
|
|
508
|
-
const latencyMs = Date.now() - t0
|
|
510
|
+
const latencyMs = Math.max(1, Date.now() - t0)
|
|
511
|
+
const latencyGrade = latencyMs < 250 ? 'good' : (latencyMs < 750 ? 'fair' : 'poor')
|
|
512
|
+
if (typeof store.rememberPing === "function") store.rememberPing(ref, { latencyMs, latencyGrade })
|
|
513
|
+
const docVerdict = diagnoseVendorError(e, provider, blob)
|
|
514
|
+
if (typeof store.rememberDoctorVerdict === "function") store.rememberDoctorVerdict(ref, docVerdict)
|
|
509
515
|
history.add({ provider, ref, path: "/smoke", status: 401, ms: latencyMs })
|
|
510
|
-
writeJson(res, 502, { ok: false, provider, index, ref, latencyMs, error: { code: 'auth_failed', message: String(e && e.message || e) } })
|
|
516
|
+
writeJson(res, 502, { ok: false, provider, index, ref, latencyMs, latencyGrade, doctor: docVerdict, error: { code: 'auth_failed', message: String(e && e.message || e) } })
|
|
511
517
|
return
|
|
512
518
|
}
|
|
513
519
|
|
|
@@ -525,7 +531,10 @@ export function registerStatusRoutes(ctx, state) {
|
|
|
525
531
|
}
|
|
526
532
|
try {
|
|
527
533
|
await vendor.check(blob, vendorConfig(provider, live()), probeFetch)
|
|
528
|
-
const latencyMs = Date.now() - t0
|
|
534
|
+
const latencyMs = Math.max(1, Date.now() - t0)
|
|
535
|
+
const latencyGrade = latencyMs < 250 ? 'good' : (latencyMs < 750 ? 'fair' : 'poor')
|
|
536
|
+
if (typeof store.rememberPing === "function") store.rememberPing(ref, { latencyMs, latencyGrade })
|
|
537
|
+
if (typeof store.rememberDoctorVerdict === "function") store.rememberDoctorVerdict(ref, null)
|
|
529
538
|
history.add({ provider, ref, path: "/smoke", status: 200, ms: latencyMs })
|
|
530
539
|
writeJson(res, 200, {
|
|
531
540
|
ok: true,
|
|
@@ -533,6 +542,7 @@ export function registerStatusRoutes(ctx, state) {
|
|
|
533
542
|
index,
|
|
534
543
|
ref,
|
|
535
544
|
latencyMs,
|
|
545
|
+
latencyGrade,
|
|
536
546
|
label: pmL(blob.label),
|
|
537
547
|
email: pmE(blob.email),
|
|
538
548
|
quota: capturedQuota || info.quota || null,
|
|
@@ -540,7 +550,11 @@ export function registerStatusRoutes(ctx, state) {
|
|
|
540
550
|
})
|
|
541
551
|
return
|
|
542
552
|
} catch (e) {
|
|
543
|
-
const latencyMs = Date.now() - t0
|
|
553
|
+
const latencyMs = Math.max(1, Date.now() - t0)
|
|
554
|
+
const latencyGrade = latencyMs < 250 ? 'good' : (latencyMs < 750 ? 'fair' : 'poor')
|
|
555
|
+
if (typeof store.rememberPing === "function") store.rememberPing(ref, { latencyMs, latencyGrade })
|
|
556
|
+
const docVerdict = diagnoseVendorError(e, provider, blob)
|
|
557
|
+
if (typeof store.rememberDoctorVerdict === "function") store.rememberDoctorVerdict(ref, docVerdict)
|
|
544
558
|
history.add({ provider, ref, path: "/smoke", status: 502, ms: latencyMs })
|
|
545
559
|
writeJson(res, 502, {
|
|
546
560
|
ok: false,
|
|
@@ -548,13 +562,18 @@ export function registerStatusRoutes(ctx, state) {
|
|
|
548
562
|
index,
|
|
549
563
|
ref,
|
|
550
564
|
latencyMs,
|
|
565
|
+
latencyGrade,
|
|
566
|
+
doctor: docVerdict,
|
|
551
567
|
error: { code: e && e.code ? e.code : 'VENDOR', message: String(e && e.message || e).slice(0, 300) },
|
|
552
568
|
})
|
|
553
569
|
return
|
|
554
570
|
}
|
|
555
571
|
}
|
|
556
572
|
|
|
557
|
-
const latencyMs = Date.now() - t0
|
|
573
|
+
const latencyMs = Math.max(1, Date.now() - t0)
|
|
574
|
+
const latencyGrade = latencyMs < 250 ? 'good' : (latencyMs < 750 ? 'fair' : 'poor')
|
|
575
|
+
if (typeof store.rememberPing === "function") store.rememberPing(ref, { latencyMs, latencyGrade })
|
|
576
|
+
if (typeof store.rememberDoctorVerdict === "function") store.rememberDoctorVerdict(ref, null)
|
|
558
577
|
history.add({ provider, ref, path: "/smoke", status: 200, ms: latencyMs })
|
|
559
578
|
writeJson(res, 200, {
|
|
560
579
|
ok: true,
|
|
@@ -562,6 +581,7 @@ export function registerStatusRoutes(ctx, state) {
|
|
|
562
581
|
index,
|
|
563
582
|
ref,
|
|
564
583
|
latencyMs,
|
|
584
|
+
latencyGrade,
|
|
565
585
|
label: pmL(blob.label),
|
|
566
586
|
email: pmE(blob.email),
|
|
567
587
|
quota: info.quota || null,
|
|
@@ -569,6 +589,80 @@ export function registerStatusRoutes(ctx, state) {
|
|
|
569
589
|
}),
|
|
570
590
|
}), 'dsh-subscriptions: /smoke')
|
|
571
591
|
|
|
592
|
+
// Issue #542: Latency RTT ping test endpoint
|
|
593
|
+
ctx.effect(() => ctx.webServer.register({
|
|
594
|
+
kind: 'exact',
|
|
595
|
+
path: '/dsh-subscriptions/ping',
|
|
596
|
+
handler: safeJsonHandler(async (req, res) => {
|
|
597
|
+
if (req.method !== 'POST') {
|
|
598
|
+
writeJson(res, 405, { ok: false, error: { code: 'method', message: 'POST only' } })
|
|
599
|
+
return
|
|
600
|
+
}
|
|
601
|
+
if (!isTrustedSettingsRequest(req, ctx)) {
|
|
602
|
+
writeJson(res, 403, { ok: false, error: { code: 'forbidden', message: 'same-origin only' } })
|
|
603
|
+
return
|
|
604
|
+
}
|
|
605
|
+
let payload = {}
|
|
606
|
+
try { payload = JSON.parse((await readBody(req, 64 * 1024)).toString('utf8') || '{}') } catch { /* ignore ping payload */ }
|
|
607
|
+
const provider = payload.provider
|
|
608
|
+
const index = payload.index || 1
|
|
609
|
+
if (!provider || !isProvider(provider)) {
|
|
610
|
+
writeJson(res, 400, { ok: false, error: { code: 'invalid_provider', message: 'Provider required' } })
|
|
611
|
+
return
|
|
612
|
+
}
|
|
613
|
+
const ref = oauthRef(provider, index)
|
|
614
|
+
const info = await store.describeRef(ref)
|
|
615
|
+
if (!info.configured) {
|
|
616
|
+
writeJson(res, 400, { ok: false, error: { code: 'not_configured', message: 'Account is not configured' } })
|
|
617
|
+
return
|
|
618
|
+
}
|
|
619
|
+
const t0 = Date.now()
|
|
620
|
+
let latencyMs = 0
|
|
621
|
+
let latencyGrade = 'good'
|
|
622
|
+
try {
|
|
623
|
+
const blob = await store.loadBlob(ref)
|
|
624
|
+
const vendor = getVendor(provider)
|
|
625
|
+
const cfg = vendorConfig(provider, live())
|
|
626
|
+
const doFetch = (typeof fetchForRef === 'function' && fetchForRef(ref)) || fetch
|
|
627
|
+
if (typeof vendor.check === 'function') {
|
|
628
|
+
await vendor.check(blob, cfg, doFetch)
|
|
629
|
+
} else if (typeof vendor.listModels === 'function') {
|
|
630
|
+
await vendor.listModels(blob, cfg, doFetch)
|
|
631
|
+
}
|
|
632
|
+
latencyMs = Math.max(1, Date.now() - t0)
|
|
633
|
+
latencyGrade = latencyMs < 250 ? 'good' : (latencyMs < 750 ? 'fair' : 'poor')
|
|
634
|
+
if (typeof store.rememberPing === 'function') store.rememberPing(ref, { latencyMs, latencyGrade })
|
|
635
|
+
if (typeof store.rememberDoctorVerdict === 'function') store.rememberDoctorVerdict(ref, null)
|
|
636
|
+
writeJson(res, 200, { ok: true, provider, index, ref, latencyMs, latencyGrade })
|
|
637
|
+
} catch (err) {
|
|
638
|
+
latencyMs = Math.max(1, Date.now() - t0)
|
|
639
|
+
latencyGrade = 'poor'
|
|
640
|
+
const doc = diagnoseVendorError(err, provider)
|
|
641
|
+
if (typeof store.rememberPing === 'function') store.rememberPing(ref, { latencyMs, latencyGrade })
|
|
642
|
+
if (typeof store.rememberDoctorVerdict === 'function') store.rememberDoctorVerdict(ref, doc)
|
|
643
|
+
writeJson(res, 502, { ok: false, provider, index, ref, latencyMs, latencyGrade, doctor: doc, error: { message: String(err && err.message || err) } })
|
|
644
|
+
}
|
|
645
|
+
}),
|
|
646
|
+
}), 'dsh-subscriptions: /ping')
|
|
647
|
+
|
|
648
|
+
// Issue #540: Dynamic vendor instructions API
|
|
649
|
+
ctx.effect(() => ctx.webServer.register({
|
|
650
|
+
kind: 'exact',
|
|
651
|
+
path: '/dsh-subscriptions/instructions',
|
|
652
|
+
handler: safeJsonHandler(async (req, res) => {
|
|
653
|
+
if (req.method !== 'GET') {
|
|
654
|
+
writeJson(res, 405, { ok: false, error: { code: 'method', message: 'GET only' } })
|
|
655
|
+
return
|
|
656
|
+
}
|
|
657
|
+
if (!isTrustedSettingsRequest(req, ctx)) {
|
|
658
|
+
writeJson(res, 403, { ok: false, error: { code: 'forbidden', message: 'same-origin only' } })
|
|
659
|
+
return
|
|
660
|
+
}
|
|
661
|
+
writeJson(res, 200, { ok: true, instructions: INSTRUCTIONS })
|
|
662
|
+
}),
|
|
663
|
+
}), 'dsh-subscriptions: /instructions')
|
|
664
|
+
|
|
665
|
+
|
|
572
666
|
// #351: Quota and session alerts API
|
|
573
667
|
ctx.effect(() => ctx.webServer.register({
|
|
574
668
|
kind: 'exact',
|
|
@@ -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
|
+
}
|