@goodandready/dsh-subscriptions 0.5.26 → 0.5.31

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/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 = ''
@@ -10,6 +10,7 @@ export async function* iterateSse(body, { idleTimeoutMs = 60000 } = {}) {
10
10
  buffer = buffer.slice(sep).replace(/^\r?\n\r?\n/, '')
11
11
  const dataLines = []
12
12
  for (const line of raw.split(/\r?\n/)) {
13
+ if (line.startsWith(":")) continue // SSE comments / keep-alive pings
13
14
  if (line.startsWith('data:')) dataLines.push(line.slice(5).trimStart())
14
15
  }
15
16
  if (!dataLines.length) continue
@@ -20,33 +21,63 @@ export async function* iterateSse(body, { idleTimeoutMs = 60000 } = {}) {
20
21
  }
21
22
 
22
23
  async function readWithTimeout(r) {
23
- if (!idleTimeoutMs || idleTimeoutMs <= 0) return r.read()
24
+ if (signal && signal.aborted) {
25
+ if (typeof r.cancel === 'function') await r.cancel().catch(() => {})
26
+ const err = new Error('stream aborted by client')
27
+ err.name = 'AbortError'
28
+ throw err
29
+ }
24
30
  let timer
25
31
  const timeoutPromise = new Promise((_, reject) => {
32
+ if (!idleTimeoutMs || idleTimeoutMs <= 0) return
26
33
  timer = setTimeout(() => {
27
34
  const err = new Error(`stream idle timeout: no data received for ${Math.round(idleTimeoutMs / 1000)}s`)
28
35
  err.code = 'TIMEOUT'
29
36
  reject(err)
30
37
  }, idleTimeoutMs)
31
38
  })
39
+
40
+ let abortPromise
41
+ let abortHandler
42
+ if (signal) {
43
+ abortPromise = new Promise((_, reject) => {
44
+ abortHandler = () => {
45
+ if (typeof r.cancel === 'function') r.cancel().catch(() => {})
46
+ const err = new Error('stream aborted by client')
47
+ err.name = 'AbortError'
48
+ reject(err)
49
+ }
50
+ signal.addEventListener('abort', abortHandler, { once: true })
51
+ })
52
+ }
53
+
32
54
  try {
33
- return await Promise.race([r.read(), timeoutPromise])
55
+ const races = [r.read()]
56
+ if (timeoutPromise) races.push(timeoutPromise)
57
+ if (abortPromise) races.push(abortPromise)
58
+ return await Promise.race(races)
34
59
  } finally {
35
- clearTimeout(timer)
60
+ if (timer) clearTimeout(timer)
61
+ if (signal && abortHandler) signal.removeEventListener('abort', abortHandler)
36
62
  }
37
63
  }
38
64
 
39
65
  if (reader) {
40
- while (true) {
41
- const { done, value } = await readWithTimeout(reader)
42
- if (done) break
43
- yield* fromText(decoder.decode(value, { stream: true }))
66
+ try {
67
+ while (true) {
68
+ const { done, value } = await readWithTimeout(reader)
69
+ if (done) break
70
+ yield* fromText(decoder.decode(value, { stream: true }))
71
+ }
72
+ yield* fromText(decoder.decode())
73
+ } finally {
74
+ if (typeof reader.cancel === 'function') await reader.cancel().catch(() => {})
44
75
  }
45
- yield* fromText(decoder.decode())
46
76
  return
47
77
  }
48
78
  if (body && typeof body[Symbol.asyncIterator] === 'function') {
49
79
  for await (const chunk of body) {
80
+ if (signal && signal.aborted) break
50
81
  const text = typeof chunk === 'string' ? chunk : decoder.decode(chunk, { stream: true })
51
82
  yield* fromText(text)
52
83
  }
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
+ }
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-subscriptions",
3
- "version": "0.5.26",
3
+ "version": "0.5.31",
4
4
  "description": "Use ChatGPT Codex, Claude, Grok, and Antigravity subscriptions as DeepSeek Harness LLM providers via OAuth.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -34,9 +34,6 @@
34
34
  "bugs": {
35
35
  "url": "https://github.com/GooDAnDReaDY/dsh-subscriptions/issues"
36
36
  },
37
- "scripts": {
38
- "test": "eslint lib/ && node --test test/*.test.mjs"
39
- },
40
37
  "dsh": {
41
38
  "bundle": {
42
39
  "patch": "./cordis.patch.yml"
@@ -63,5 +60,8 @@
63
60
  },
64
61
  "devDependencies": {
65
62
  "@deepseek-ai/dsh-llm": "0.1.0-rc.8"
63
+ },
64
+ "scripts": {
65
+ "test": "eslint lib/ && node --test test/*.test.mjs"
66
66
  }
67
- }
67
+ }
@@ -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
- }
@@ -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
- }
@@ -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
- }
@@ -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
- }
@@ -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
- }