@goodandready/dsh-subscriptions 0.5.26 → 0.5.30
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/config-schema.js +80 -0
- package/lib/index.js +28 -1042
- package/lib/routes.js +1016 -0
- package/lib/sse.js +39 -9
- package/lib/ui-i18n.js +256 -0
- package/lib/ui-styles.js +119 -0
- package/package.json +1 -1
- package/lib/coalesce-stream.js +0 -22
- package/lib/cookie-bridge.js +0 -8
- package/lib/credit-detect.js +0 -22
- package/lib/encrypted-bundle.js +0 -46
- package/lib/latency-heatmap.js +0 -26
- package/lib/latency-score.js +0 -36
- package/lib/multi-tenant.js +0 -16
- package/lib/network-benchmark.js +0 -23
- package/lib/pkce-store.js +0 -28
- package/lib/plan.js +0 -42
- package/lib/prompt-cache-warmer.js +0 -44
- package/lib/reconnect-stream.js +0 -21
- package/lib/reset-toast.js +0 -38
- package/lib/savings-calculator.js +0 -14
- package/lib/state-recovery.js +0 -33
- package/lib/telemetry.js +0 -28
- package/lib/token-speedometer.js +0 -26
- package/lib/zero-trace-logger.js +0 -27
package/lib/sse.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export async function* iterateSse(body, { idleTimeoutMs = 60000 } = {}) {
|
|
1
|
+
export async function* iterateSse(body, { idleTimeoutMs = 60000, signal } = {}) {
|
|
2
2
|
const reader = body && typeof body.getReader === 'function' ? body.getReader() : null
|
|
3
3
|
const decoder = new TextDecoder()
|
|
4
4
|
let buffer = ''
|
|
@@ -20,33 +20,63 @@ export async function* iterateSse(body, { idleTimeoutMs = 60000 } = {}) {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
async function readWithTimeout(r) {
|
|
23
|
-
if (
|
|
23
|
+
if (signal && signal.aborted) {
|
|
24
|
+
if (typeof r.cancel === 'function') await r.cancel().catch(() => {})
|
|
25
|
+
const err = new Error('stream aborted by client')
|
|
26
|
+
err.name = 'AbortError'
|
|
27
|
+
throw err
|
|
28
|
+
}
|
|
24
29
|
let timer
|
|
25
30
|
const timeoutPromise = new Promise((_, reject) => {
|
|
31
|
+
if (!idleTimeoutMs || idleTimeoutMs <= 0) return
|
|
26
32
|
timer = setTimeout(() => {
|
|
27
33
|
const err = new Error(`stream idle timeout: no data received for ${Math.round(idleTimeoutMs / 1000)}s`)
|
|
28
34
|
err.code = 'TIMEOUT'
|
|
29
35
|
reject(err)
|
|
30
36
|
}, idleTimeoutMs)
|
|
31
37
|
})
|
|
38
|
+
|
|
39
|
+
let abortPromise
|
|
40
|
+
let abortHandler
|
|
41
|
+
if (signal) {
|
|
42
|
+
abortPromise = new Promise((_, reject) => {
|
|
43
|
+
abortHandler = () => {
|
|
44
|
+
if (typeof r.cancel === 'function') r.cancel().catch(() => {})
|
|
45
|
+
const err = new Error('stream aborted by client')
|
|
46
|
+
err.name = 'AbortError'
|
|
47
|
+
reject(err)
|
|
48
|
+
}
|
|
49
|
+
signal.addEventListener('abort', abortHandler, { once: true })
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
|
|
32
53
|
try {
|
|
33
|
-
|
|
54
|
+
const races = [r.read()]
|
|
55
|
+
if (timeoutPromise) races.push(timeoutPromise)
|
|
56
|
+
if (abortPromise) races.push(abortPromise)
|
|
57
|
+
return await Promise.race(races)
|
|
34
58
|
} finally {
|
|
35
|
-
clearTimeout(timer)
|
|
59
|
+
if (timer) clearTimeout(timer)
|
|
60
|
+
if (signal && abortHandler) signal.removeEventListener('abort', abortHandler)
|
|
36
61
|
}
|
|
37
62
|
}
|
|
38
63
|
|
|
39
64
|
if (reader) {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
65
|
+
try {
|
|
66
|
+
while (true) {
|
|
67
|
+
const { done, value } = await readWithTimeout(reader)
|
|
68
|
+
if (done) break
|
|
69
|
+
yield* fromText(decoder.decode(value, { stream: true }))
|
|
70
|
+
}
|
|
71
|
+
yield* fromText(decoder.decode())
|
|
72
|
+
} finally {
|
|
73
|
+
if (typeof reader.cancel === 'function') await reader.cancel().catch(() => {})
|
|
44
74
|
}
|
|
45
|
-
yield* fromText(decoder.decode())
|
|
46
75
|
return
|
|
47
76
|
}
|
|
48
77
|
if (body && typeof body[Symbol.asyncIterator] === 'function') {
|
|
49
78
|
for await (const chunk of body) {
|
|
79
|
+
if (signal && signal.aborted) break
|
|
50
80
|
const text = typeof chunk === 'string' ? chunk : decoder.decode(chunk, { stream: true })
|
|
51
81
|
yield* fromText(text)
|
|
52
82
|
}
|
package/lib/ui-i18n.js
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
export function defineLocales() {
|
|
2
|
+
const en = {
|
|
3
|
+
'notConnected': 'Not connected',
|
|
4
|
+
'verifyAccount': 'Verify account',
|
|
5
|
+
'coolingDown': 'Cooling down',
|
|
6
|
+
'usageFull': 'Usage 100%',
|
|
7
|
+
'connected': 'Connected',
|
|
8
|
+
'title': 'Subscriptions',
|
|
9
|
+
'cardIntro': 'Subscription accounts, rotation and limits.',
|
|
10
|
+
'show': 'Show',
|
|
11
|
+
'hide': 'Hide',
|
|
12
|
+
'intro': 'Log in with a consumer subscription. Tokens stay on the host credentials store. The browser never reads them back. After Connect, if the provider lands on localhost or a vendor page, paste the redirected URL or the code here.',
|
|
13
|
+
'useOrigin': 'Use this Web UI origin as OAuth redirect_uri',
|
|
14
|
+
'useOriginHint': 'Leave off unless you registered your own OAuth client for this origin. Vendor CLI clients usually require their published redirect URI plus the paste step.',
|
|
15
|
+
'privacyMask': 'Mask emails and account identifiers',
|
|
16
|
+
'privacyMaskHint': 'For demos and screen sharing: emails show as j***n@example.com.',
|
|
17
|
+
'resetCredits': 'Reset credits',
|
|
18
|
+
'resetCreditsHint': 'ChatGPT reset cards: emergency reset for the 5-hour window. Requires a 5s confirmation.',
|
|
19
|
+
'resetCreditsAction': 'Reset ChatGPT Limit',
|
|
20
|
+
'usageLimitsTitle': 'Usage Limits',
|
|
21
|
+
'slotDetailsTitle': 'Slot Details',
|
|
22
|
+
'resetAvailable': 'Reset attempts available',
|
|
23
|
+
'resetExpires': 'earliest expires',
|
|
24
|
+
'resetAck': 'I understand one attempt will be consumed',
|
|
25
|
+
'resetWait': 'confirm enabled in',
|
|
26
|
+
'resetReady': 'ready',
|
|
27
|
+
'resetGo': 'Reset now',
|
|
28
|
+
'resetBusy': 'resetting…',
|
|
29
|
+
'resetDone': 'quota reset',
|
|
30
|
+
'resetNothing': 'server says nothing needs a reset (no attempt consumed)',
|
|
31
|
+
'resetNoCredit': 'no usable credit (not consumed)',
|
|
32
|
+
'resetRedeemed': 'already redeemed earlier',
|
|
33
|
+
'diagGenerate': 'Generate diagnostics report',
|
|
34
|
+
'diagCopy': 'Copy report',
|
|
35
|
+
'diagIssues': 'Open issue tracker',
|
|
36
|
+
'diagHint': 'The report has no tokens, emails, proxy addresses or personal data.',
|
|
37
|
+
'subsPill': 'SUBS',
|
|
38
|
+
'subsModalTitle': 'Subscription Hub',
|
|
39
|
+
'subsActiveHero': 'Active Subscription',
|
|
40
|
+
'subsPoolTitle': 'Provider Accounts',
|
|
41
|
+
'subsRefresh': 'Refresh',
|
|
42
|
+
'subsOpenSettings': 'All Settings →',
|
|
43
|
+
'subsFastMode': 'Fast Mode 1.5x',
|
|
44
|
+
'subsHealthy': 'Healthy',
|
|
45
|
+
'subsFree': 'free',
|
|
46
|
+
'subsUsed': 'used',
|
|
47
|
+
'subsModalClose': 'Close',
|
|
48
|
+
'subsLogged': 'connected',
|
|
49
|
+
'subsNotLogged': 'not connected',
|
|
50
|
+
'subsCooldown': 'cooldown',
|
|
51
|
+
'subsOpenSettings': 'Open settings',
|
|
52
|
+
'hudTitle': 'Subscription balance',
|
|
53
|
+
'settingsLoading': 'Loading settings…',
|
|
54
|
+
'settingsUnavailable': 'Settings are unavailable',
|
|
55
|
+
'settingsRetry': 'Retry',
|
|
56
|
+
'hudHint': 'drag · edge docks · click refresh',
|
|
57
|
+
'fcCalibrating': 'calibrating…',
|
|
58
|
+
'fcIdle': 'no usage',
|
|
59
|
+
'fcHours': 'h',
|
|
60
|
+
'fcMinutes': 'm',
|
|
61
|
+
'reconnect': 'Reconnect',
|
|
62
|
+
'connect': 'Connect',
|
|
63
|
+
'disconnect': 'Disconnect',
|
|
64
|
+
'removeSlot': 'Remove slot',
|
|
65
|
+
'pastePlaceholder': 'Paste redirected URL or code',
|
|
66
|
+
'submitCode': 'Submit code',
|
|
67
|
+
'proxyPlaceholder': 'Per-account proxy (http://, https://, socks5://) - optional',
|
|
68
|
+
'proxyCheck': 'Check proxy',
|
|
69
|
+
'proxyOk': 'proxy ok',
|
|
70
|
+
'proxyFail': 'proxy fail',
|
|
71
|
+
'deviceLogin': 'Device login',
|
|
72
|
+
'deviceHint': 'Headless: open the link, enter the code, keep this tab open.',
|
|
73
|
+
'deviceCopy': 'Copy code',
|
|
74
|
+
'devicePending': 'Waiting for confirmation',
|
|
75
|
+
'deviceAuthorized': 'Signed in',
|
|
76
|
+
'deviceExpired': 'Expired - start again',
|
|
77
|
+
'verifyPrefix': 'Google requires one-time account verification. ',
|
|
78
|
+
'verifyLink': 'Open verification link',
|
|
79
|
+
'verifySuffix': ' then reconnect.',
|
|
80
|
+
'addAccount': '+ Add account',
|
|
81
|
+
'save': 'Save',
|
|
82
|
+
'saved': 'Saved',
|
|
83
|
+
'loading': 'Loading\u2026',
|
|
84
|
+
'accountLabel': 'Account',
|
|
85
|
+
'plan': 'Plan',
|
|
86
|
+
'storedAs': 'Stored as',
|
|
87
|
+
'switchLabel': 'Provider',
|
|
88
|
+
'chipActive': 'Subscriptions',
|
|
89
|
+
'expiryLabel': 'expires',
|
|
90
|
+
'slashLogin': 'Login to a subscription provider',
|
|
91
|
+
'slashLogout': 'Log out a subscription provider',
|
|
92
|
+
'slashStatus': 'Subscription status',
|
|
93
|
+
'none': 'none',
|
|
94
|
+
'forecast': '≈',
|
|
95
|
+
'resetCredits': 'Сброс лимитов ChatGPT',
|
|
96
|
+
'resetCreditsHint': 'Экстренный сброс 5-часового окна ChatGPT Plus/Pro. Требует 5 секунд подтверждения перед списанием.',
|
|
97
|
+
'resetCreditsAction': 'Сбросить лимит ChatGPT',
|
|
98
|
+
'resetAvailable': 'Доступно сбросов',
|
|
99
|
+
'resetExpires': 'срок действия',
|
|
100
|
+
'resetAck': 'Я понимаю, что одна попытка сброса будет списана',
|
|
101
|
+
'resetWait': 'подтверждение через',
|
|
102
|
+
'resetReady': 'готово к сбросу',
|
|
103
|
+
'resetGo': 'Сбросить лимит сейчас',
|
|
104
|
+
'resetBusy': 'Сброс…',
|
|
105
|
+
'resetDone': 'Лимит успешно сброшен',
|
|
106
|
+
'resetNothing': 'Лимит не требует сброса',
|
|
107
|
+
'resetNoCredit': 'Нет доступных попыток сброса',
|
|
108
|
+
'resetRedeemed': 'Попытка уже использована',
|
|
109
|
+
'windowPrimary': 'Окно 5 часов',
|
|
110
|
+
'windowSecondary': 'Окно 7 дней',
|
|
111
|
+
'usageLimitsTitle': 'Лимиты использования',
|
|
112
|
+
'slotDetailsTitle': 'Параметры слота',
|
|
113
|
+
'resetLabel': 'reset',
|
|
114
|
+
'check': 'Check',
|
|
115
|
+
'checking': 'Checking…',
|
|
116
|
+
'importToken': 'Save token',
|
|
117
|
+
'importTokenPlace': 'Paste token or API key',
|
|
118
|
+
'importLocalCli': '📥 From CLI',
|
|
119
|
+
'importLocalCliTitle': 'Auto-import token from locally installed CLI on server',
|
|
120
|
+
'importLocalSuccess': 'Successfully imported token from local CLI!',
|
|
121
|
+
'howToGetToken': '❓ Instructions',
|
|
122
|
+
'howToGetTokenTitle': 'How to get token / API key for this provider',
|
|
123
|
+
'instructionsTitle': 'How to obtain token',
|
|
124
|
+
'close': 'Close',
|
|
125
|
+
'reconnectRequired': 'Reconnect required',
|
|
126
|
+
'manualTitle': 'If the browser did not come back',
|
|
127
|
+
'windowPrimary': 'Primary window',
|
|
128
|
+
'windowSecondary': 'Secondary window',
|
|
129
|
+
}
|
|
130
|
+
const ru = {
|
|
131
|
+
'check': 'Проверить',
|
|
132
|
+
'checking': 'Проверяю…',
|
|
133
|
+
'importToken': 'Сохранить',
|
|
134
|
+
'importTokenPlace': 'Вставить токен или API-ключ',
|
|
135
|
+
'importLocalCli': '📥 Из CLI',
|
|
136
|
+
'importLocalCliTitle': 'Автоматически считать токен из установленного на сервере CLI',
|
|
137
|
+
'importLocalSuccess': 'Токен успешно загружен из локального CLI!',
|
|
138
|
+
'howToGetToken': '❓ Инструкция',
|
|
139
|
+
'howToGetTokenTitle': 'Как получить токен или API-ключ для этого провайдера',
|
|
140
|
+
'instructionsTitle': 'Инструкция по получению токена',
|
|
141
|
+
'close': 'Закрыть',
|
|
142
|
+
'reconnectRequired': 'Нужно переподключить',
|
|
143
|
+
'manualTitle': 'Если браузер не вернулся сам',
|
|
144
|
+
'windowPrimary': 'Основное окно',
|
|
145
|
+
'windowSecondary': 'Дополнительное окно',
|
|
146
|
+
'notConnected': 'Не подключено',
|
|
147
|
+
'verifyAccount': 'Требуется проверка',
|
|
148
|
+
'coolingDown': 'Пауза после лимита',
|
|
149
|
+
'usageFull': 'Лимит исчерпан',
|
|
150
|
+
'connected': 'Подключено',
|
|
151
|
+
'title': 'Подписки',
|
|
152
|
+
'cardIntro': 'Аккаунты подписок, ротация и лимиты.',
|
|
153
|
+
'show': 'Показать',
|
|
154
|
+
'hide': 'Скрыть',
|
|
155
|
+
'intro': 'Вход по обычной пользовательской подписке. Токены остаются в хранилище учётных данных харнесса, браузер их обратно не читает. Если после «Подключить» провайдер увёл на localhost или на свою страницу, вставьте сюда адрес перехода или код.',
|
|
156
|
+
'useOrigin': 'Использовать адрес этого веб-интерфейса как redirect_uri',
|
|
157
|
+
'useOriginHint': 'Включайте, только если зарегистрировали собственного клиента OAuth на этот адрес. Штатным консольным клиентам провайдеров нужен их опубликованный адрес возврата и вставка кода вручную.',
|
|
158
|
+
'privacyMask': 'Маскировать email и учётные записи',
|
|
159
|
+
'privacyMaskHint': 'Для демонстрации экрана: email отображается как j***n@example.com.',
|
|
160
|
+
'resetCredits': 'Карты сброса',
|
|
161
|
+
'resetCreditsHint': 'Карты сброса квоты ChatGPT: показывает, сколько доступно. Списание одной карты — осознанное действие с 5-секундным подтверждением.',
|
|
162
|
+
'resetAvailable': 'Доступно попыток сброса',
|
|
163
|
+
'resetExpires': 'ближайшая истекает',
|
|
164
|
+
'resetAck': 'Я понимаю, что будет списана 1 попытка',
|
|
165
|
+
'resetWait': 'кнопка активна через',
|
|
166
|
+
'resetReady': 'готово',
|
|
167
|
+
'resetGo': 'Сбросить',
|
|
168
|
+
'resetBusy': 'сбрасываю…',
|
|
169
|
+
'resetDone': 'квота сброшена',
|
|
170
|
+
'resetNothing': 'сервер сообщает: сброс не нужен (попытка не списана)',
|
|
171
|
+
'resetNoCredit': 'нет пригодной карты (не списано)',
|
|
172
|
+
'resetRedeemed': 'эта карта уже использована ранее',
|
|
173
|
+
'diagGenerate': 'Сгенерировать диагностический отчёт',
|
|
174
|
+
'diagCopy': 'Скопировать отчёт',
|
|
175
|
+
'diagIssues': 'Открыть трекер задач',
|
|
176
|
+
'diagHint': 'В отчёте нет токенов, email, адресов прокси и персональных данных.',
|
|
177
|
+
'subsPill': 'SUBS',
|
|
178
|
+
'subsModalTitle': 'Консоль подписок',
|
|
179
|
+
'subsActiveHero': 'Активная подписка',
|
|
180
|
+
'subsPoolTitle': 'Пул аккаунтов',
|
|
181
|
+
'subsRefresh': 'Обновить',
|
|
182
|
+
'subsOpenSettings': 'Все настройки →',
|
|
183
|
+
'subsFastMode': 'Режим Fast 1.5x',
|
|
184
|
+
'subsHealthy': 'Исправен',
|
|
185
|
+
'subsFree': 'свободно',
|
|
186
|
+
'subsUsed': 'израсходовано',
|
|
187
|
+
'subsModalClose': 'Закрыть',
|
|
188
|
+
'subsLogged': 'подключен',
|
|
189
|
+
'subsNotLogged': 'не подключен',
|
|
190
|
+
'subsCooldown': 'кулдаун',
|
|
191
|
+
'subsOpenSettings': 'Открыть настройки',
|
|
192
|
+
'hudTitle': 'Остаток подписки',
|
|
193
|
+
'settingsLoading': 'Загрузка настроек…',
|
|
194
|
+
'settingsUnavailable': 'Настройки недоступны',
|
|
195
|
+
'settingsRetry': 'Повторить',
|
|
196
|
+
'hudHint': 'перетащи · прилипает к краям · клик — обновить',
|
|
197
|
+
'fcCalibrating': 'калибровка…',
|
|
198
|
+
'fcIdle': 'нет расхода',
|
|
199
|
+
'fcHours': 'ч',
|
|
200
|
+
'fcMinutes': 'мин',
|
|
201
|
+
'reconnect': 'Переподключить',
|
|
202
|
+
'connect': 'Подключить',
|
|
203
|
+
'disconnect': 'Отключить',
|
|
204
|
+
'removeSlot': 'Убрать место',
|
|
205
|
+
'pastePlaceholder': 'Адрес перехода или код',
|
|
206
|
+
'submitCode': 'Отправить код',
|
|
207
|
+
'proxyPlaceholder': 'Прокси аккаунта (http://, https://, socks5://) - необязательно',
|
|
208
|
+
'proxyCheck': 'Проверить прокси',
|
|
209
|
+
'proxyOk': 'прокси ок',
|
|
210
|
+
'proxyFail': 'прокси недоступен',
|
|
211
|
+
'deviceLogin': 'Вход по коду',
|
|
212
|
+
'deviceHint': 'Headless: откройте ссылку, введите код, держите вкладку открытой.',
|
|
213
|
+
'deviceCopy': 'Скопировать код',
|
|
214
|
+
'devicePending': 'Ожидание подтверждения',
|
|
215
|
+
'deviceAuthorized': 'Вход выполнен',
|
|
216
|
+
'deviceExpired': 'Истёк - начните заново',
|
|
217
|
+
'verifyPrefix': 'Google требует однократной проверки аккаунта. ',
|
|
218
|
+
'verifyLink': 'Открыть страницу проверки',
|
|
219
|
+
'verifySuffix': ' затем подключитесь заново.',
|
|
220
|
+
'addAccount': '+ Добавить аккаунт',
|
|
221
|
+
'save': 'Сохранить',
|
|
222
|
+
'saved': 'Сохранено',
|
|
223
|
+
'loading': 'Загрузка…',
|
|
224
|
+
'accountLabel': 'Аккаунт',
|
|
225
|
+
'plan': 'Тариф',
|
|
226
|
+
'storedAs': 'Хранится как',
|
|
227
|
+
'switchLabel': 'Провайдер',
|
|
228
|
+
'chipActive': 'Подписки',
|
|
229
|
+
'expiryLabel': 'окончание',
|
|
230
|
+
'slashLogin': 'Вход в провайдера подписки',
|
|
231
|
+
'slashLogout': 'Выйти из провайдера подписки',
|
|
232
|
+
'slashStatus': 'Статус подписок',
|
|
233
|
+
'none': 'нет',
|
|
234
|
+
'forecast': '≈',
|
|
235
|
+
'resetCredits': 'Сброс лимитов ChatGPT',
|
|
236
|
+
'resetCreditsHint': 'Экстренный сброс 5-часового окна ChatGPT Plus/Pro. Требует 5 секунд подтверждения перед списанием.',
|
|
237
|
+
'resetCreditsAction': 'Сбросить лимит ChatGPT',
|
|
238
|
+
'resetAvailable': 'Доступно сбросов',
|
|
239
|
+
'resetExpires': 'срок действия',
|
|
240
|
+
'resetAck': 'Я понимаю, что одна попытка сброса будет списана',
|
|
241
|
+
'resetWait': 'подтверждение через',
|
|
242
|
+
'resetReady': 'готово к сбросу',
|
|
243
|
+
'resetGo': 'Сбросить лимит сейчас',
|
|
244
|
+
'resetBusy': 'Сброс…',
|
|
245
|
+
'resetDone': 'Лимит успешно сброшен',
|
|
246
|
+
'resetNothing': 'Лимит не требует сброса',
|
|
247
|
+
'resetNoCredit': 'Нет доступных попыток сброса',
|
|
248
|
+
'resetRedeemed': 'Попытка уже использована',
|
|
249
|
+
'windowPrimary': 'Окно 5 часов',
|
|
250
|
+
'windowSecondary': 'Окно 7 дней',
|
|
251
|
+
'usageLimitsTitle': 'Лимиты использования',
|
|
252
|
+
'slotDetailsTitle': 'Параметры слота',
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return { en, ru }
|
|
256
|
+
}
|
package/lib/ui-styles.js
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
export const CSS =
|
|
2
|
+
'.dsub-card{list-style:none;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;transition:border-color .16s,background .16s}' +
|
|
3
|
+
'.dsub-card:hover{border-color:var(--dsw-alias-label-dimmed)}' +
|
|
4
|
+
'.dsub-cardOpen{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)}' +
|
|
5
|
+
'.dsub-header{appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:0 0;border:0;border-radius:12px;align-items:center;gap:12px;padding:14px 16px;display:flex}' +
|
|
6
|
+
'.dsub-header:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px}' +
|
|
7
|
+
'.dsub-headText{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}' +
|
|
8
|
+
'.dsub-name{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}' +
|
|
9
|
+
'.dsub-description{color:var(--dsw-alias-label-secondary);font-size:13px}' +
|
|
10
|
+
'.dsub-chev{color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s}' +
|
|
11
|
+
'.dsub-chevOpen{transform:rotate(180deg)}' +
|
|
12
|
+
'.dsub-body{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding-bottom:12px}' +
|
|
13
|
+
'.dsub-block{margin-top:10px}' +
|
|
14
|
+
'.dsub-row{display:flex;align-items:center;flex-wrap:wrap;gap:8px;margin-top:8px}' +
|
|
15
|
+
'.dsub-grow{flex:1 1 180px;min-width:140px}' +
|
|
16
|
+
'.dsub-helpBox{margin-top:8px;padding:12px 14px;border-radius:8px;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l1);font-size:12px;line-height:1.6}' +
|
|
17
|
+
'.dsub-helpHead{display:flex;align-items:center;justify-content:space-between;font-weight:600;margin-bottom:8px;color:var(--dsw-alias-brand-primary)}' +
|
|
18
|
+
'.dsub-helpStep{margin-bottom:6px;color:var(--dsw-alias-label-primary)}' +
|
|
19
|
+
'.dsub-btnActive{background:var(--dsw-alias-brand-primary);color:#fff;border-color:var(--dsw-alias-brand-primary)}' +
|
|
20
|
+
'.dsub-h{font-size:13.5px;font-weight:600;color:var(--dsw-alias-label-primary)}' +
|
|
21
|
+
'.dsub-sub{font-size:12px;color:var(--dsw-alias-label-secondary)}' +
|
|
22
|
+
'.dsub-dim{font-size:11.5px;color:var(--dsw-alias-label-tertiary)}' +
|
|
23
|
+
'.dsub-mini{appearance:none;font:inherit;font-size:12px;cursor:pointer;border:1px solid var(--dsw-alias-border-l2);border-radius:6px;padding:3px 8px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);transition:all .15s}' +
|
|
24
|
+
'.dsub-mini:hover{background:var(--dsw-alias-bg-layer-1);border-color:var(--dsw-alias-label-dimmed)}' +
|
|
25
|
+
'.dsub-ok{font-size:12px;color:var(--dsw-alias-state-success-primary)}' +
|
|
26
|
+
'.dsub-bad{font-size:12px;color:var(--dsw-alias-state-error-primary);overflow-wrap:anywhere;word-break:break-word;max-width:100%}' +
|
|
27
|
+
'.dsub-hint{overflow-wrap:anywhere;word-break:break-word;max-width:100%}' +
|
|
28
|
+
'.dsub-btn{appearance:none;font:inherit;cursor:pointer;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:5px 12px;font-size:12.5px;font-weight:500;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);transition:all .15s}' +
|
|
29
|
+
'.dsub-btn:hover{background:var(--dsw-alias-bg-layer-1);border-color:var(--dsw-alias-label-dimmed)}' +
|
|
30
|
+
'.dsub-inp{height:30px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-primary);border-radius:6px;padding:0 8px;font-size:12px;min-width:180px}' +
|
|
31
|
+
'.dsub-inp:focus{outline:none;border-color:var(--dsw-alias-brand-primary)}' +
|
|
32
|
+
'.dsub-box{border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:10px;margin-top:8px;background:var(--dsw-alias-bg-layer-2)}' +
|
|
33
|
+
'.dsub-cooldown{display:inline-block;padding:2px 6px;border-radius:4px;background:var(--dsw-alias-state-warning-primary);color:#000;font-size:11px;font-weight:600}' +
|
|
34
|
+
'.dsub-bar{position:relative;height:6px;border-radius:3px;background:var(--dsw-alias-bg-layer-1);overflow:hidden;flex:1;min-width:80px}' +
|
|
35
|
+
'.dsub-barFill{height:100%;border-radius:3px;background:var(--dsw-alias-state-success-primary);transition:width .2s}' +
|
|
36
|
+
'.dsub-barWarn .dsub-barFill{background:var(--dsw-alias-state-warning-primary)}' +
|
|
37
|
+
'.dsub-barFull .dsub-barFill{background:var(--dsw-alias-state-error-primary)}' +
|
|
38
|
+
'.dsub-barRow{display:flex;align-items:center;gap:8px;font-size:12px;color:var(--dsw-alias-label-secondary)}' +
|
|
39
|
+
'.dsub-manual{display:flex;flex-direction:column;gap:6px;margin-top:4px;padding-top:10px;border-top:1px solid var(--dsw-alias-border-l2)}' +
|
|
40
|
+
'.dsub-verify{font-size:12px;color:var(--dsw-alias-state-warning-primary)}' +
|
|
41
|
+
'.dsub-verify a{color:var(--dsw-alias-brand-primary)}' +
|
|
42
|
+
'.dsub-pill{display:inline-flex;align-items:center;gap:7px;height:28px;padding:0 11px;border:1px solid var(--dsw-alias-border-l2);border-radius:999px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);font-size:12px;font-weight:600;cursor:pointer;box-shadow:0 1px 3px rgba(0,0,0,.15),inset 0 1px 0 rgba(255,255,255,.06);transition:all .16s ease;user-select:none}' +
|
|
43
|
+
'.dsub-pill:hover{background:var(--dsw-alias-bg-layer-3);border-color:var(--dsw-alias-label-tertiary);transform:translateY(-1px);box-shadow:0 3px 8px rgba(0,0,0,.25),inset 0 1px 0 rgba(255,255,255,.1)}' +
|
|
44
|
+
'.dsub-pill:active{transform:translateY(0)}' +
|
|
45
|
+
'.dsub-pillTag{padding:1px 5px;border-radius:4px;font-size:10.5px;font-weight:700;letter-spacing:.02em;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-secondary)}' +
|
|
46
|
+
'.dsub-led{width:7px;height:7px;border-radius:50%;flex:none;transition:all .2s ease}' +
|
|
47
|
+
'.dsub-ledOk{background:var(--dsw-alias-state-success-primary,#10b981);box-shadow:0 0 6px rgba(16,185,129,.7)}' +
|
|
48
|
+
'.dsub-ledWarn{background:var(--dsw-alias-state-warning-primary,#f59e0b);box-shadow:0 0 6px rgba(245,158,11,.7)}' +
|
|
49
|
+
'.dsub-ledBad{background:var(--dsw-alias-state-error-primary,#ef4444);box-shadow:0 0 6px rgba(239,68,68,.7)}' +
|
|
50
|
+
'.dsub-ledOff{background:var(--dsw-alias-label-tertiary,#6b7280)}' +
|
|
51
|
+
'.dsub-modalWrap{position:fixed;inset:0;z-index:10020;display:grid;place-items:center;background:rgba(0,0,0,.65);backdrop-filter:blur(10px);animation:dsubFadeIn .16s ease-out}' +
|
|
52
|
+
'.dsub-modal{width:min(520px,94vw);max-height:min(85vh,720px);overflow-y:auto;border:1px solid var(--dsw-alias-border-l1);border-radius:16px;background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-primary);box-shadow:0 24px 64px rgba(0,0,0,.6),0 0 0 1px rgba(255,255,255,.07);padding:20px;animation:dsubScaleIn .18s cubic-bezier(.16,1,.3,1)}' +
|
|
53
|
+
'@keyframes dsubFadeIn{from{opacity:0}to{opacity:1}}' +
|
|
54
|
+
'@keyframes dsubScaleIn{from{opacity:0;transform:scale(.96)}to{opacity:1;transform:scale(1)}}' +
|
|
55
|
+
'.dsub-modalHead{display:flex;align-items:center;justify-content:space-between;gap:12px;padding-bottom:14px;border-bottom:1px solid var(--dsw-alias-border-l2)}' +
|
|
56
|
+
'.dsub-modalTitleWrap{display:flex;align-items:center;gap:10px}' +
|
|
57
|
+
'.dsub-modalIcon{width:30px;height:30px;border-radius:8px;background:var(--dsw-alias-bg-layer-1);display:grid;place-items:center;color:var(--dsw-alias-brand-primary);font-size:14px}' +
|
|
58
|
+
'.dsub-modalBadge{font-size:11px;font-weight:600;padding:2px 7px;border-radius:10px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-secondary)}' +
|
|
59
|
+
'.dsub-heroCard{margin-top:14px;padding:14px 16px;border-radius:12px;background:linear-gradient(135deg,var(--dsw-alias-bg-layer-2) 0%,var(--dsw-alias-bg-layer-3) 100%);border:1px solid var(--dsw-alias-border-l1);box-shadow:inset 0 1px 0 rgba(255,255,255,.05)}' +
|
|
60
|
+
'.dsub-heroHead{display:flex;align-items:center;justify-content:space-between;margin-bottom:10px}' +
|
|
61
|
+
'.dsub-heroTitle{font-size:14px;font-weight:700;display:flex;align-items:center;gap:6px}' +
|
|
62
|
+
'.dsub-heroModel{font-size:11.5px;color:var(--dsw-alias-label-secondary);font-family:ui-monospace,monospace}' +
|
|
63
|
+
'.dsub-heroBars{display:flex;flex-direction:column;gap:8px;margin-top:10px}' +
|
|
64
|
+
'.dsub-barLabelRow{display:flex;justify-content:space-between;font-size:11.5px;color:var(--dsw-alias-label-secondary);margin-bottom:3px}' +
|
|
65
|
+
'.dsub-barTrack{height:6px;border-radius:3px;background:var(--dsw-alias-bg-layer-1);overflow:hidden}' +
|
|
66
|
+
'.dsub-barFillGrad{height:100%;border-radius:3px;transition:width .3s ease}' +
|
|
67
|
+
'.dsub-panel-box{margin-top:10px;padding:12px 14px;border-radius:10px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);display:flex;flex-direction:column;gap:8px}' +
|
|
68
|
+
'.dsub-panel-warn{border-color:rgba(245,158,11,0.4);background:rgba(245,158,11,0.05)}' +
|
|
69
|
+
'.dsub-panel-title{font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary);display:flex;align-items:center;justify-content:space-between}' +
|
|
70
|
+
'.dsub-panel-desc{font-size:12px;color:var(--dsw-alias-label-secondary);line-height:1.4}' +
|
|
71
|
+
'.dsub-usage-grid{display:flex;flex-direction:column;gap:8px;margin-top:4px}' +
|
|
72
|
+
'.dsub-usage-card{display:flex;flex-direction:column;gap:5px;padding:8px 10px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-layer-1)}' +
|
|
73
|
+
'.dsub-usage-head{display:flex;justify-content:space-between;align-items:center;font-size:12px;font-weight:500;color:var(--dsw-alias-label-primary)}' +
|
|
74
|
+
'.dsub-usage-track{width:100%;height:8px;border-radius:999px;background:var(--dsw-alias-bg-layer-3);border:1px solid var(--dsw-alias-border-l2);overflow:hidden}' +
|
|
75
|
+
'.dsub-usage-fill{height:100%;border-radius:999px;transition:width .3s ease}' +
|
|
76
|
+
'.dsub-usage-meta{display:flex;justify-content:space-between;align-items:center;font-size:11px;color:var(--dsw-alias-label-secondary)}' +
|
|
77
|
+
'.dsub-tag-ok{border-color:rgba(16,185,129,0.3);color:var(--dsw-alias-state-success-primary);background:rgba(16,185,129,0.08);padding:1px 7px;border-radius:999px;font-size:11px;font-weight:600;border-style:solid;border-width:1px}' +
|
|
78
|
+
'.dsub-tag-warn{border-color:rgba(245,158,11,0.4);color:var(--dsw-alias-state-warning-primary);background:rgba(245,158,11,0.08);padding:1px 7px;border-radius:999px;font-size:11px;font-weight:600;border-style:solid;border-width:1px}' +
|
|
79
|
+
'.dsub-tag-bad{border-color:rgba(239,68,68,0.4);color:var(--dsw-alias-state-error-primary);background:rgba(239,68,68,0.08);padding:1px 7px;border-radius:999px;font-size:11px;font-weight:600;border-style:solid;border-width:1px}' +
|
|
80
|
+
'.dsub-slot-meta-row{display:flex;flex-wrap:wrap;align-items:center;gap:10px;margin-top:12px;padding-top:10px;border-top:1px solid var(--dsw-alias-border-l2);font-size:12px}' +
|
|
81
|
+
'.dsub-ref-tag{display:inline-flex;align-items:center;gap:5px;font-size:11.5px;color:var(--dsw-alias-label-secondary)}' +
|
|
82
|
+
'.dsub-ref-tag code{padding:2px 6px;border-radius:4px;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l2);font-family:ui-monospace,monospace;color:var(--dsw-alias-label-primary);font-size:11px}' +
|
|
83
|
+
'.dsub-btn-danger{border-color:rgba(239,68,68,0.4)!important;color:var(--dsw-alias-state-error-primary)!important}' +
|
|
84
|
+
'.dsub-btn-danger:hover:not(:disabled){background:rgba(239,68,68,0.12)!important}' +
|
|
85
|
+
'.dsub-poolTitle{margin-top:16px;margin-bottom:8px;font-size:11.5px;font-weight:700;color:var(--dsw-alias-label-secondary);text-transform:uppercase;letter-spacing:.05em}' +
|
|
86
|
+
'.dsub-accountCard{display:flex;align-items:center;gap:12px;padding:10px 14px;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-alias-bg-layer-2);margin-top:6px;transition:border-color .15s,background .15s}' +
|
|
87
|
+
'.dsub-accountCard:hover{background:var(--dsw-alias-bg-layer-3);border-color:var(--dsw-alias-label-tertiary)}' +
|
|
88
|
+
'.dsub-brandBadge{width:32px;height:32px;border-radius:8px;display:grid;place-items:center;font-size:12px;font-weight:700;flex-shrink:0}' +
|
|
89
|
+
'.dsub-brandCodex{background:rgba(16,185,129,.15);color:#10b981;border:1px solid rgba(16,185,129,.3)}' +
|
|
90
|
+
'.dsub-brandClaude{background:rgba(245,158,11,.15);color:#f59e0b;border:1px solid rgba(245,158,11,.3)}' +
|
|
91
|
+
'.dsub-brandGrok{background:rgba(168,85,247,.15);color:#c084fc;border:1px solid rgba(168,85,247,.3)}' +
|
|
92
|
+
'.dsub-brandAgy{background:rgba(59,130,246,.15);color:#60a5fa;border:1px solid rgba(59,130,246,.3)}' +
|
|
93
|
+
'.dsub-brandOllama{background:rgba(107,114,128,.15);color:#9ca3af;border:1px solid rgba(107,114,128,.3)}' +
|
|
94
|
+
'.dsub-brandKimi{background:rgba(236,72,153,.15);color:#f472b6;border:1px solid rgba(236,72,153,.3)}' +
|
|
95
|
+
'.dsub-brandGlm{background:rgba(20,184,166,.15);color:#2dd4bf;border:1px solid rgba(20,184,166,.3)}' +
|
|
96
|
+
'.dsub-planBadge{display:inline-flex;align-items:center;padding:1px 6px;border-radius:4px;font-size:10px;font-weight:600;text-transform:uppercase;letter-spacing:0.5px;background:rgba(99,102,241,.15);color:#818cf8;border:1px solid rgba(99,102,241,.3);margin-left:6px}' +
|
|
97
|
+
|
|
98
|
+
'.dsub-accInfo{flex:1;min-width:0;display:flex;flex-direction:column;gap:3px}' +
|
|
99
|
+
'.dsub-accNameRow{display:flex;align-items:center;justify-content:space-between;gap:8px}' +
|
|
100
|
+
'.dsub-accName{font-size:13px;font-weight:600}' +
|
|
101
|
+
'.dsub-accStatusTag{font-size:10.5px;font-weight:600;padding:1px 6px;border-radius:4px}' +
|
|
102
|
+
'.dsub-statusOk{background:rgba(16,185,129,.15);color:#34d399}' +
|
|
103
|
+
'.dsub-statusWarn{background:rgba(245,158,11,.15);color:#fbbf24}' +
|
|
104
|
+
'.dsub-statusBad{background:rgba(239,68,68,.15);color:#f87171}' +
|
|
105
|
+
'.dsub-statusOff{background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-tertiary)}' +
|
|
106
|
+
'.dsub-modalFoot{display:flex;align-items:center;justify-content:space-between;margin-top:16px;padding-top:12px;border-top:1px solid var(--dsw-alias-border-l2)}' +
|
|
107
|
+
'.dsub-btnPrimary{display:inline-flex;align-items:center;gap:6px;padding:6px 12px;border-radius:8px;font-size:12px;font-weight:600;background:var(--dsw-alias-brand-primary,#3b82f6);color:#fff;border:0;cursor:pointer;transition:opacity .15s}' +
|
|
108
|
+
'.dsub-btnPrimary:hover{opacity:.9}' +
|
|
109
|
+
'.dsub-btnSec{display:inline-flex;align-items:center;gap:5px;padding:5px 10px;border-radius:7px;font-size:11.5px;font-weight:500;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-secondary);border:1px solid var(--dsw-alias-border-l2);cursor:pointer;text-decoration:none;transition:all .15s}' +
|
|
110
|
+
'.dsub-btnSec:hover{background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-tertiary)}' +
|
|
111
|
+
'.dsub-cq{display:inline-flex;align-items:center;gap:6px;font-size:12px;color:var(--dsw-alias-label-secondary);padding:0 6px}' +
|
|
112
|
+
'.dsub-cqBar{position:relative;width:40px;height:5px;border-radius:3px;background:var(--dsw-alias-bg-layer-1);overflow:hidden}' +
|
|
113
|
+
'.dsub-cqBarFill{height:100%;border-radius:3px;background:var(--dsw-alias-state-success-primary)}' +
|
|
114
|
+
'.dsub-cqBarWarn .dsub-cqBarFill{background:var(--dsw-alias-state-warning-primary)}' +
|
|
115
|
+
'.dsub-cqBarFull .dsub-cqBarFill{background:var(--dsw-alias-state-error-primary)}' +
|
|
116
|
+
'.dsub-cqB{font-variant-numeric:tabular-nums;font-weight:600;color:var(--dsw-alias-label-primary)}' +
|
|
117
|
+
'.dsub-diag{font:11px/1.5 ui-monospace,SFMono-Regular,monospace;max-height:260px;overflow:auto;white-space:pre-wrap;word-break:break-word;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:10px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-secondary)}'
|
|
118
|
+
|
|
119
|
+
const cssId = 'dsh-subscriptions/settings.module.css'
|
package/package.json
CHANGED
package/lib/coalesce-stream.js
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
export async function* coalesceStreamChunks(sourceStream, maxWaitMs = 40, maxChunkChars = 80) {
|
|
2
|
-
let buffer = ''
|
|
3
|
-
let lastFlush = Date.now()
|
|
4
|
-
|
|
5
|
-
for await (const chunk of sourceStream) {
|
|
6
|
-
const text = typeof chunk === 'string' ? chunk : (chunk && chunk.text) || ''
|
|
7
|
-
buffer += text
|
|
8
|
-
|
|
9
|
-
const now = Date.now()
|
|
10
|
-
if (buffer.length >= maxChunkChars || now - lastFlush >= maxWaitMs) {
|
|
11
|
-
if (buffer.length > 0) {
|
|
12
|
-
yield buffer
|
|
13
|
-
buffer = ''
|
|
14
|
-
lastFlush = now
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
if (buffer.length > 0) {
|
|
20
|
-
yield buffer
|
|
21
|
-
}
|
|
22
|
-
}
|
package/lib/cookie-bridge.js
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
export function extractBearerFromCookie(cookieHeader) {
|
|
2
|
-
if (!cookieHeader || typeof cookieHeader !== 'string') return null
|
|
3
|
-
const match = cookieHeader.match(/(?:__Secure-next-auth\.session-token|session_token|auth_token)=([^;]+)/)
|
|
4
|
-
if (match && match[1]) {
|
|
5
|
-
return decodeURIComponent(match[1])
|
|
6
|
-
}
|
|
7
|
-
return null
|
|
8
|
-
}
|
package/lib/credit-detect.js
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
export function detectCodexBillingType(rawBilling) {
|
|
2
|
-
if (!rawBilling || typeof rawBilling !== 'object') {
|
|
3
|
-
return { plan: 'Free / Unknown', isHardLimit: false, isPrepaid: false }
|
|
4
|
-
}
|
|
5
|
-
|
|
6
|
-
const planId = String(rawBilling.plan || rawBilling.subscription_type || '').toLowerCase()
|
|
7
|
-
const hasHardLimit = Boolean(rawBilling.hard_limit_reached || rawBilling.is_blocked)
|
|
8
|
-
const isPrepaid = Boolean(rawBilling.has_payment_method || rawBilling.is_prepaid)
|
|
9
|
-
|
|
10
|
-
let label = 'Free'
|
|
11
|
-
if (planId.includes('team')) label = 'Team'
|
|
12
|
-
else if (planId.includes('pro')) label = 'Pro'
|
|
13
|
-
else if (planId.includes('plus')) label = 'Plus'
|
|
14
|
-
else if (planId.includes('enterprise')) label = 'Enterprise'
|
|
15
|
-
|
|
16
|
-
return {
|
|
17
|
-
plan: label,
|
|
18
|
-
isHardLimit: hasHardLimit,
|
|
19
|
-
isPrepaid,
|
|
20
|
-
remainingCredits: rawBilling.remaining_credits != null ? Number(rawBilling.remaining_credits) : null
|
|
21
|
-
}
|
|
22
|
-
}
|
package/lib/encrypted-bundle.js
DELETED
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import { createCipheriv, createDecipheriv, randomBytes, pbkdf2Sync } from 'node:crypto'
|
|
2
|
-
|
|
3
|
-
const ALGORITHM = 'aes-256-gcm'
|
|
4
|
-
const SALT_LEN = 16
|
|
5
|
-
const IV_LEN = 12
|
|
6
|
-
const ITERATIONS = 100000
|
|
7
|
-
|
|
8
|
-
export function exportEncryptedBundle(data, password) {
|
|
9
|
-
const salt = randomBytes(SALT_LEN)
|
|
10
|
-
const key = pbkdf2Sync(password, salt, ITERATIONS, 32, 'sha256')
|
|
11
|
-
const iv = randomBytes(IV_LEN)
|
|
12
|
-
const cipher = createCipheriv(ALGORITHM, key, iv)
|
|
13
|
-
|
|
14
|
-
const plaintext = JSON.stringify(data)
|
|
15
|
-
let encrypted = cipher.update(plaintext, 'utf8', 'hex')
|
|
16
|
-
encrypted += cipher.final('hex')
|
|
17
|
-
const authTag = cipher.getAuthTag().toString('hex')
|
|
18
|
-
|
|
19
|
-
return {
|
|
20
|
-
version: 1,
|
|
21
|
-
algorithm: ALGORITHM,
|
|
22
|
-
salt: salt.toString('hex'),
|
|
23
|
-
iv: iv.toString('hex'),
|
|
24
|
-
authTag,
|
|
25
|
-
payload: encrypted
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export function importEncryptedBundle(bundle, password) {
|
|
30
|
-
if (!bundle || bundle.version !== 1 || !bundle.payload) {
|
|
31
|
-
throw new Error('Invalid backup bundle format')
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const salt = Buffer.from(bundle.salt, 'hex')
|
|
35
|
-
const key = pbkdf2Sync(password, salt, ITERATIONS, 32, 'sha256')
|
|
36
|
-
const iv = Buffer.from(bundle.iv, 'hex')
|
|
37
|
-
const authTag = Buffer.from(bundle.authTag, 'hex')
|
|
38
|
-
|
|
39
|
-
const decipher = createDecipheriv(ALGORITHM, key, iv)
|
|
40
|
-
decipher.setAuthTag(authTag)
|
|
41
|
-
|
|
42
|
-
let decrypted = decipher.update(bundle.payload, 'hex', 'utf8')
|
|
43
|
-
decrypted += decipher.final('utf8')
|
|
44
|
-
|
|
45
|
-
return JSON.parse(decrypted)
|
|
46
|
-
}
|
package/lib/latency-heatmap.js
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
export class LatencyHeatmap {
|
|
2
|
-
constructor() {
|
|
3
|
-
// 24-hour slots: map provider -> array of 24 hourly averages
|
|
4
|
-
this.matrix = new Map()
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
recordHourlyPing(provider, hourIndex, latencyMs) {
|
|
8
|
-
const p = provider || 'unknown'
|
|
9
|
-
if (!this.matrix.has(p)) {
|
|
10
|
-
this.matrix.set(p, new Array(24).fill(null))
|
|
11
|
-
}
|
|
12
|
-
const arr = this.matrix.get(p)
|
|
13
|
-
const idx = Math.max(0, Math.min(23, hourIndex))
|
|
14
|
-
arr[idx] = latencyMs
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
getHeatmap(provider) {
|
|
18
|
-
const arr = this.matrix.get(provider) || new Array(24).fill(null)
|
|
19
|
-
return arr.map((latency) => {
|
|
20
|
-
if (latency == null) return { latency: null, color: 'gray' }
|
|
21
|
-
if (latency < 400) return { latency, color: 'green' }
|
|
22
|
-
if (latency < 1200) return { latency, color: 'yellow' }
|
|
23
|
-
return { latency, color: 'red' }
|
|
24
|
-
})
|
|
25
|
-
}
|
|
26
|
-
}
|
package/lib/latency-score.js
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
export class LatencyTracker {
|
|
2
|
-
constructor(sampleLimit = 10) {
|
|
3
|
-
this.samples = new Map()
|
|
4
|
-
this.limit = sampleLimit
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
record(slotRef, latencyMs) {
|
|
8
|
-
if (!slotRef || typeof latencyMs !== 'number' || latencyMs < 0) return
|
|
9
|
-
if (!this.samples.has(slotRef)) {
|
|
10
|
-
this.samples.set(slotRef, [])
|
|
11
|
-
}
|
|
12
|
-
const arr = this.samples.get(slotRef)
|
|
13
|
-
arr.push(latencyMs)
|
|
14
|
-
if (arr.length > this.limit) arr.shift()
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
getAverage(slotRef) {
|
|
18
|
-
const arr = this.samples.get(slotRef)
|
|
19
|
-
if (!arr || !arr.length) return null
|
|
20
|
-
const sum = arr.reduce((acc, v) => acc + v, 0)
|
|
21
|
-
return Math.round(sum / arr.length)
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
getHealthScore(slotRef) {
|
|
25
|
-
const avg = this.getAverage(slotRef)
|
|
26
|
-
if (avg == null) return 100
|
|
27
|
-
if (avg < 300) return 100
|
|
28
|
-
if (avg < 800) return 85
|
|
29
|
-
if (avg < 2000) return 60
|
|
30
|
-
return 30
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
clear() {
|
|
34
|
-
this.samples.clear()
|
|
35
|
-
}
|
|
36
|
-
}
|