@jeffusion/bungee-core 5.11.0 → 5.12.0

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.
@@ -79,6 +79,42 @@
79
79
  "execution": "control",
80
80
  "capability": "config.read"
81
81
  },
82
+ {
83
+ "path": "/accounts/codex",
84
+ "methods": [
85
+ "GET"
86
+ ],
87
+ "handler": "listCodexAccounts",
88
+ "execution": "control",
89
+ "capability": "config.read"
90
+ },
91
+ {
92
+ "path": "/accounts/siwc",
93
+ "methods": [
94
+ "GET"
95
+ ],
96
+ "handler": "listSiwcAccounts",
97
+ "execution": "control",
98
+ "capability": "config.read"
99
+ },
100
+ {
101
+ "path": "/accounts/siwc/draft",
102
+ "methods": [
103
+ "POST"
104
+ ],
105
+ "handler": "createSiwcDraft",
106
+ "execution": "control",
107
+ "capability": "config.write"
108
+ },
109
+ {
110
+ "path": "/login/siwc",
111
+ "methods": [
112
+ "POST"
113
+ ],
114
+ "handler": "startSiwcLogin",
115
+ "execution": "control",
116
+ "capability": "config.write"
117
+ },
82
118
  {
83
119
  "path": "/accounts/usage",
84
120
  "methods": [
@@ -210,7 +246,7 @@
210
246
  {
211
247
  "id": "chatgpt",
212
248
  "label": "ChatGPT OAuth",
213
- "listAccounts": "listAccounts",
249
+ "listAccounts": "listCodexAccounts",
214
250
  "createDraft": "createDraft",
215
251
  "credentialPolicy": {
216
252
  "allowedOrigins": [
@@ -262,6 +298,51 @@
262
298
  "Chatgpt-Account-Id"
263
299
  ]
264
300
  }
301
+ },
302
+ {
303
+ "id": "chatgpt-siwc",
304
+ "label": "Sign in with ChatGPT",
305
+ "listAccounts": "listSiwcAccounts",
306
+ "createDraft": "createSiwcDraft",
307
+ "credentialPolicy": {
308
+ "allowedOrigins": [
309
+ "https://api.openai.com"
310
+ ],
311
+ "allowedRequests": [
312
+ {
313
+ "pathname": "/v1/responses",
314
+ "methods": [
315
+ "POST"
316
+ ],
317
+ "outboundHeaders": {
318
+ "passthrough": [],
319
+ "set": {
320
+ "Accept": "text/event-stream",
321
+ "Content-Type": "application/json",
322
+ "User-Agent": "Bungee/5.11.0",
323
+ "Originator": "Bungee"
324
+ }
325
+ }
326
+ },
327
+ {
328
+ "pathname": "/v1/models",
329
+ "methods": [
330
+ "GET"
331
+ ],
332
+ "outboundHeaders": {
333
+ "passthrough": [],
334
+ "set": {
335
+ "Accept": "application/json",
336
+ "User-Agent": "Bungee/5.11.0",
337
+ "Originator": "Bungee"
338
+ }
339
+ }
340
+ }
341
+ ],
342
+ "allowedHeaderNames": [
343
+ "Authorization"
344
+ ]
345
+ }
265
346
  }
266
347
  ],
267
348
  "settings": "/accounts",
@@ -416,8 +497,13 @@
416
497
  "ui.accountIdentity": "Account: {label}",
417
498
  "ui.credentialsHelp": "The plugin keeps credentials; they are not written to service configuration.",
418
499
  "ui.loginMethod": "Sign-in method",
419
- "ui.device": "Device code",
420
- "ui.pkce": "Browser sign-in (alternative)",
500
+ "ui.device": "Codex device code",
501
+ "ui.pkce": "Codex browser sign-in",
502
+ "ui.siwc": "Sign in with ChatGPT",
503
+ "ui.siwcHelp": "Open the authorization URL, sign in, then paste the complete callback URL from the browser address bar. The final loopback page may fail to load; you can still copy its URL.",
504
+ "ui.siwcUsage": "Check usage in ChatGPT settings",
505
+ "ui.siwcUsageHelp": "This sign-in method has no public usage or reset-credit query. Open ChatGPT settings to check usage.",
506
+ "ui.authMethod": "Sign-in: {method}",
421
507
  "ui.deviceHelp": "Enter the device code on the verification page. This window updates automatically after authorization.",
422
508
  "ui.pkceHelp": "Sign in in your browser, then paste the complete final localhost callback URL here.",
423
509
  "ui.statusLoading": "Checking login status",
@@ -491,7 +577,7 @@
491
577
  "errors.request_cancelled": "The request was cancelled.",
492
578
  "errors.unknown": "The operation did not complete. Refresh the status before retrying; do not resubmit authorization.",
493
579
  "metadata.name": "ChatGPT OAuth",
494
- "plugin.description": "Use ChatGPT OAuth accounts as managed Codex upstreams",
580
+ "plugin.description": "Use ChatGPT sign-in and Codex accounts as separate managed upstreams",
495
581
  "config.accountRef.label": "Account reference",
496
582
  "config.accountRef.description": "The managed ChatGPT account reference; leading and trailing whitespace is not allowed. Codex compatibility is managed automatically by the plugin."
497
583
  },
@@ -623,8 +709,13 @@
623
709
  "ui.accountIdentity": "账号:{label}",
624
710
  "ui.credentialsHelp": "凭据由插件保管,不会写入服务配置。",
625
711
  "ui.loginMethod": "登录方式",
626
- "ui.device": "设备码登录",
627
- "ui.pkce": "浏览器登录(备用)",
712
+ "ui.device": "Codex 设备码登录",
713
+ "ui.pkce": "Codex 浏览器登录",
714
+ "ui.siwc": "使用 ChatGPT 登录",
715
+ "ui.siwcHelp": "打开授权地址完成登录,再从浏览器地址栏复制完整回调地址并粘贴到此处。最后的本地回调页面可能无法打开,仍可复制其地址。",
716
+ "ui.siwcUsage": "去 ChatGPT 设置查看用量",
717
+ "ui.siwcUsageHelp": "此登录方式没有公开的用量或重置额度查询。请在 ChatGPT 设置中查看用量。",
718
+ "ui.authMethod": "登录方式:{method}",
628
719
  "ui.deviceHelp": "在验证页面输入设备码,完成后这里会自动更新。",
629
720
  "ui.pkceHelp": "打开浏览器登录,将最终跳转的完整 localhost 回调地址粘贴到此处。",
630
721
  "ui.statusLoading": "正在查询登录状态",
@@ -698,7 +789,7 @@
698
789
  "errors.request_cancelled": "请求已取消。",
699
790
  "errors.unknown": "操作未完成,请刷新状态后重试。请勿重复提交授权。",
700
791
  "metadata.name": "ChatGPT OAuth",
701
- "plugin.description": "将 ChatGPT OAuth 账号用作受管理的 Codex 上游",
792
+ "plugin.description": "将 ChatGPT 登录和 Codex 账号用作独立的受管理上游",
702
793
  "config.accountRef.label": "账号引用",
703
794
  "config.accountRef.description": "受管理的 ChatGPT 账号引用;首尾不允许有空白字符。Codex 兼容版本由插件自动管理。"
704
795
  }
@@ -34,7 +34,7 @@
34
34
  import { Input } from '$components/ui/input';
35
35
  import * as DropdownMenu from '$components/ui/dropdown-menu';
36
36
  import { Close as ResetClose } from '$components/ui/dialog';
37
- import { loginStates, accountStates, terminal, verificationUrl, loginStatus, parseLoginStart, accountSummary, accountUsage, resetOutcome, errorText, errorCode } from './account-model.js';
37
+ import { loginStates, accountStates, terminal, verificationUrl, loginStatus, parseLoginStart, accountSummary, accountUsage, resetOutcome, errorText, errorCode, accountSourceId, canQueryUsage, loginMethods } from './account-model.js';
38
38
 
39
39
  type Account = ReturnType<typeof accountSummary>;
40
40
  type AccountUsage = ReturnType<typeof accountUsage>;
@@ -50,12 +50,13 @@
50
50
  const control = <T,>(method: 'GET' | 'POST', path: string, body?: unknown) => requestPluginControl<T>(pluginName, path, method, body, lifetime.signal);
51
51
  let accounts = $state<Account[]>([]), refreshing = $state(false), notice = $state('');
52
52
  let accountRefreshGeneration = 0;
53
- let loginOpen = $state(false), kind = $state('device'), reauthRef = $state<string | undefined>();
53
+ let loginOpen = $state(false), kind = $state('siwc'), reauthRef = $state<string | undefined>();
54
54
  let session = $state.raw<Session | null>(null), status = $state(''), callback = $state(''), loginNotice = $state('');
55
55
  let starting = $state(false), submitting = $state(false), cancelling = $state(false), polling = $state(false);
56
56
  let pollGeneration = 0, pollingGeneration: number | undefined;
57
57
  let timer: ReturnType<typeof setTimeout> | undefined;
58
58
  let active = $derived(session !== null && !terminal(status));
59
+ let methods = $derived(loginMethods(reauthRef ? accounts.find(account => account.id === reauthRef) : undefined));
59
60
  let url = $derived(active ? verificationUrl(session?.verificationUri ?? session?.authorizationUrl, kind) : null);
60
61
  let action = $state(''), actionAccount = $state<Account | null>(null), actionOpen = $state(false);
61
62
  let actionBusy = $state(false), label = $state(''), actionNotice = $state(''), referenceNotice = $state('');
@@ -82,7 +83,7 @@
82
83
  let pendingResetByAccount = $state<Record<string, PendingReset>>({});
83
84
 
84
85
  function eligibleForUsage(account: Account) {
85
- return account.status === 'active';
86
+ return canQueryUsage(account);
86
87
  }
87
88
  function usageFor(account: Account): UsageSnapshot {
88
89
  return usageByAccount[account.id] ?? { state: 'loading' };
@@ -144,7 +145,7 @@
144
145
  }
145
146
  function canReset(account: Account, credit: Credit) {
146
147
  const snapshot = usageFor(account);
147
- return account.available && 'usage' in snapshot && snapshot.resetCredits.state === 'fresh'
148
+ return eligibleForUsage(account) && account.available && 'usage' in snapshot && snapshot.resetCredits.state === 'fresh'
148
149
  && !!snapshot.resetCredits.value && snapshot.resetCredits.value.availableCount > 0 && credit.status === 'available';
149
150
  }
150
151
  function resetTypeLabel(resetType: unknown) {
@@ -187,6 +188,7 @@
187
188
  }
188
189
  }
189
190
  async function refreshUsage(account: Account) {
191
+ if (!eligibleForUsage(account)) return;
190
192
  const generation = nextUsageGeneration(account.id);
191
193
  usageRefreshing[account.id] = true;
192
194
  usageErrors[account.id] = false;
@@ -237,7 +239,7 @@
237
239
  if (reauthRef !== accountRef) loginNotice = 'ui.otherSession';
238
240
  return;
239
241
  }
240
- invalidatePolling(); session = null; status = ''; callback = ''; loginNotice = ''; kind = 'device'; reauthRef = accountRef;
242
+ invalidatePolling(); session = null; status = ''; callback = ''; loginNotice = ''; reauthRef = accountRef; kind = loginMethods(accountRef ? accounts.find(account => account.id === accountRef) : undefined)[0];
241
243
  }
242
244
  function clearSecrets() {
243
245
  callback = '';
@@ -249,7 +251,7 @@
249
251
  if (pollingGeneration === generation) return;
250
252
  clearTimeout(timer); timer = undefined; const current = session.sessionId; pollingGeneration = generation; polling = true;
251
253
  try {
252
- const next = loginStatus(await control('GET', `/login/status?sessionId=${encodeURIComponent(current)}`), current);
254
+ const next = loginStatus(await control('GET', `/login/status?sessionId=${encodeURIComponent(current)}`), current, kind);
253
255
  if (generation !== pollGeneration || session?.sessionId !== current || lifetime.signal.aborted) return;
254
256
  const firstSuccess = status !== 'success' && next.state === 'success';
255
257
  status = String(next.state); loginNotice = '';
@@ -335,7 +337,7 @@
335
337
  }
336
338
  async function confirmReset(event: SubmitEvent) {
337
339
  event.preventDefault();
338
- if (!resetAccount || !resetCredit || resetBusy || !resetRequestId) return;
340
+ if (!resetAccount || !eligibleForUsage(resetAccount) || !resetCredit || resetBusy || !resetRequestId) return;
339
341
  resetBusy = true; resetNotice = '';
340
342
  try {
341
343
  const result = resetOutcome(await control('POST', '/accounts/usage/reset', {
@@ -356,7 +358,7 @@
356
358
  }
357
359
  async function confirmAction(event: SubmitEvent) {
358
360
  event.preventDefault();
359
- if (!actionAccount || actionBusy || action === 'references') return;
361
+ if (!actionAccount || actionBusy || action === 'references' || (action === 'auto-reset' && actionAccount.authType === 'siwc')) return;
360
362
  if (action === 'rename' && !label.trim()) { actionNotice = 'ui.nameRequired'; return; }
361
363
  actionBusy = true;
362
364
  const generation = referenceGeneration;
@@ -383,7 +385,7 @@
383
385
  if (!useAccount?.available || servicesLoading || serviceError) return;
384
386
  const existing = services.find(service => service._uid === serviceChoice);
385
387
  if (serviceChoice !== 'new' && !existing) { serviceError = 'ui.serviceMissing'; return; }
386
- const handoff = { sourcePlugin: pluginName, sourceId: 'chatgpt', accountRef: useAccount.id,
388
+ const handoff = { sourcePlugin: pluginName, sourceId: accountSourceId(useAccount), accountRef: useAccount.id,
387
389
  mode: existing ? 'existing' as const : 'new' as const, ...(existing ? { serviceId: existing._uid } : {}) };
388
390
  useOpen = false; loginOpen = false; clearSecrets();
389
391
  await tick(); // Flush the dialog close before navigation unmounts the account page.
@@ -405,7 +407,7 @@
405
407
  async function continueToRoute() {
406
408
  if (!routeAccount?.available || routesLoading || routeError || selectedRoute?.service) return;
407
409
  if (routeChoice !== 'new' && !selectedRoute?._uid) { routeError = 'ui.routeMissing'; return; }
408
- const handoff = { sourcePlugin: pluginName, sourceId: 'chatgpt', accountRef: routeAccount.id,
410
+ const handoff = { sourcePlugin: pluginName, sourceId: accountSourceId(routeAccount), accountRef: routeAccount.id,
409
411
  mode: selectedRoute ? 'existing' as const : 'new' as const,
410
412
  ...(selectedRoute ? { routeId: selectedRoute._uid } : {}) };
411
413
  routeOpen = false; loginOpen = false; clearSecrets();
@@ -457,8 +459,15 @@
457
459
  {#if !account.email}<p class="text-sm text-zinc-400">{t('ui.noEmail')}</p>{/if}
458
460
  {#if account.plan}<p class="text-sm text-zinc-300" data-testid="account-type">{t('ui.accountType', { plan: account.plan })}</p>{/if}
459
461
  {#if typeof account.expiresAt === 'number'}<p class="tabular-nums text-xs text-zinc-400">{t('ui.credentialExpiry', { date: dateText(account.expiresAt) })}</p>{/if}
460
- <p class="text-xs text-zinc-400" data-testid="auto-reset-status">{t(account.autoResetCredits ? 'ui.autoResetEnabled' : 'ui.autoResetDisabled')}</p>
462
+ <p class="text-xs text-zinc-400">{t('ui.authMethod', { method: account.authType === 'siwc' ? t('ui.siwc') : 'Codex' })}</p>
463
+ {#if account.authType !== 'siwc'}<p class="text-xs text-zinc-400" data-testid="auto-reset-status">{t(account.autoResetCredits ? 'ui.autoResetEnabled' : 'ui.autoResetDisabled')}</p>{/if}
461
464
  </div>
465
+ {#if account.authType === 'siwc'}
466
+ <div class="space-y-2" data-testid="siwc-usage">
467
+ <p class="text-sm text-zinc-400">{t('ui.siwcUsageHelp')}</p>
468
+ <Button href="https://chatgpt.com" target="_blank" rel="noopener noreferrer" variant="outline" size="sm">{@render actionIcon(ExternalLink)}{t('ui.siwcUsage')}</Button>
469
+ </div>
470
+ {:else}
462
471
  <div class="flex flex-wrap items-center gap-2"><span class="nx-field-label">{t('ui.usageLabel')}</span><Button variant="ghost" size="sm" class="aspect-square px-0 [&>span]:mr-0" disabled={!eligibleForUsage(account) || usageRefreshing[account.id]} aria-busy={!!usageRefreshing[account.id]} aria-label={t('ui.refreshUsage')} title={t('ui.refreshUsage')} onclick={() => { if (!usageRefreshing[account.id]) void refreshUsage(account); }}>{@render actionIcon(RefreshCw, usageRefreshing[account.id])}</Button>{#if status !== 'fresh'}<span role="status" class={`text-xs ${status === 'stale' || status === 'partial' ? 'text-amber-300' : 'text-zinc-400'}`}>{t(`ui.usage.${status}`)}</span>{/if}</div>
463
472
  {#if 'usage' in snapshot}
464
473
  {#if usageErrors[account.id]}<p role="status" class="text-sm text-amber-300">{t('ui.usageFailed')}</p>{/if}
@@ -500,6 +509,7 @@
500
509
  </div>
501
510
  {:else}<p class="text-sm text-zinc-400">{t('ui.creditDetailsUnavailable')}</p>{/if}
502
511
  </div>
512
+ {/if}
503
513
  <div class="flex flex-wrap items-center justify-end gap-2" data-testid="account-actions">
504
514
  <Button variant="secondary" size="sm" disabled={!account.available} onclick={() => openUse(account)}>{@render actionIcon(Server)}{t('ui.useService')}</Button>
505
515
  <Button variant="secondary" size="sm" disabled={!account.available} onclick={() => openRoute(account)}>{@render actionIcon(RouteIcon)}{t('ui.useRoute')}</Button>
@@ -552,9 +562,9 @@
552
562
  {#snippet body()}
553
563
  {#if !active && !starting}
554
564
  <RadioGroup.Root bind:value={kind} aria-label={t('ui.loginMethod')} class="flex flex-wrap gap-4">
555
- {#each ['device', 'pkce'] as method}<div class="flex items-center gap-2"><RadioGroup.Item id={`${id}-${method}`} value={method} /><label for={`${id}-${method}`} class="nx-field-label">{t(`ui.${method}`)}</label></div>{/each}
565
+ {#each methods as method}<div class="flex items-center gap-2"><RadioGroup.Item id={`${id}-${method}`} value={method} /><label for={`${id}-${method}`} class="nx-field-label">{t(`ui.${method}`)}</label></div>{/each}
556
566
  </RadioGroup.Root>
557
- <p class="text-sm text-zinc-400">{t(kind === 'device' ? 'ui.deviceHelp' : 'ui.pkceHelp')}</p>
567
+ <p class="text-sm text-zinc-400">{t(kind === 'device' ? 'ui.deviceHelp' : kind === 'siwc' ? 'ui.siwcHelp' : 'ui.pkceHelp')}</p>
558
568
  {/if}
559
569
  {#if session}
560
570
  <div class="flex flex-wrap items-center justify-between gap-2"><StatusBadge variant={status === 'success' ? 'active' : ['failed', 'expired'].includes(status) ? 'fault' : 'standby'}>{t(loginStates[status as keyof typeof loginStates] ?? 'ui.statusLoading')}</StatusBadge><span class="text-sm text-zinc-400">{t('ui.sessionExpiry', { date: dateText(session.expiresAt) })}</span></div>
@@ -565,9 +575,9 @@
565
575
  <label class="space-y-1.5"><span class="nx-field-label">{t('ui.verificationUrl')}</span><Input readonly value={url} /></label>
566
576
  <div class="flex flex-wrap gap-2"><Button href={url} target="_blank" rel="noopener noreferrer" variant="outline">{@render actionIcon(ExternalLink)}{t('ui.openVerification')}</Button><Button variant="ghost" onclick={() => copy(url ?? '')}>{@render actionIcon(Copy)}{t('ui.copyUrl')}</Button></div>
567
577
  {/if}
568
- {#if kind === 'pkce' && status === 'pending'}
578
+ {#if (kind === 'pkce' || kind === 'siwc') && status === 'pending'}
569
579
  <form onsubmit={submitCallback} class="space-y-3">
570
- <label class="block space-y-1.5"><span class="nx-field-label">{t('ui.callbackUrl')}</span><Input bind:value={callback} autocomplete="off" spellcheck={false} placeholder="http://localhost:…/auth/callback?…" /></label>
580
+ <label class="block space-y-1.5"><span class="nx-field-label">{t('ui.callbackUrl')}</span><Input bind:value={callback} autocomplete="off" spellcheck={false} placeholder={kind === 'siwc' ? 'http://127.0.0.1:1455/auth/callback?…' : 'http://localhost:…/auth/callback?…'} /></label>
571
581
  <p class="text-sm text-zinc-400">{t('ui.callbackHelp')}</p>
572
582
  <Button type="submit" disabled={submitting || !callback.trim()} aria-busy={submitting}>{@render actionIcon(Send, submitting)}{t('ui.submitCallback')}</Button>
573
583
  </form>
@@ -6,7 +6,7 @@
6
6
  import { getPluginText } from '$utils/plugin-i18n';
7
7
  import type { NativeWidgetHeaderChange } from '$components/native-widgets/widget-header';
8
8
  import { BCarouselList, LoadingIndicator, MetricBar, StatusBadge } from '$components/industrial';
9
- import { accountSummary, accountUsage, errorText } from './account-model.js';
9
+ import { accountSummary, accountUsage, errorText, canQueryUsage } from './account-model.js';
10
10
 
11
11
  let { pluginName = 'chatgpt-oauth', onHeaderChange }: { pluginName?: string; selectedRange?: string; onHeaderChange?: NativeWidgetHeaderChange } = $props();
12
12
  type Row = { account: ReturnType<typeof accountSummary>; usage?: ReturnType<typeof accountUsage>; error?: string };
@@ -73,7 +73,7 @@
73
73
  const account = accountSummary(value);
74
74
  if (!account.id || seen.has(account.id)) throw new Error('invalid_response');
75
75
  seen.add(account.id);
76
- next.push({ account, usage: account.status === 'active' ? rows.find(row => row.account.id === account.id)?.usage : undefined });
76
+ next.push({ account, usage: canQueryUsage(account) ? rows.find(row => row.account.id === account.id)?.usage : undefined });
77
77
  } catch { notice = 'ui.widgetPartialAccounts'; }
78
78
  }
79
79
  rows = next; loaded = true;
@@ -81,7 +81,7 @@
81
81
  const worker = async () => {
82
82
  while (latest() && cursor < next.length) {
83
83
  const index = cursor++, row = next[index];
84
- if (row.account.status !== 'active') continue;
84
+ if (!canQueryUsage(row.account)) continue;
85
85
  try {
86
86
  const usage = accountUsage(await get(`/accounts/usage?accountRef=${encodeURIComponent(row.account.id)}`));
87
87
  if (latest()) rows[index] = { account: row.account, usage };
@@ -121,11 +121,14 @@
121
121
  </div>
122
122
  {#if account.email && account.email !== account.label}<p class="break-all text-xs text-zinc-400">{account.email}</p>{/if}
123
123
  {#if account.plan}<p class="text-xs text-zinc-400">{t('ui.accountType', { plan: account.plan })}</p>{/if}
124
+ {#if account.authType !== 'siwc'}
124
125
  <div class="flex flex-wrap items-baseline gap-x-2 text-xs text-zinc-400"><span>{t('ui.resetCredits')} · <span class="nx-display tabular-nums text-zinc-100" data-testid="quota-count">{creditCount(row) ?? '—'}</span></span>{#if state !== 'fresh'}<span data-testid="quota-state" class={state === 'stale' || state === 'partial' ? 'text-amber-300' : 'text-zinc-400'}>{t(`ui.usage.${state}`)}</span>{/if}</div>
125
126
  {#if row.error}<p class="text-xs text-amber-300">{t(row.error)}</p>{/if}
127
+ {/if}
126
128
  </div>
127
129
  <div class="min-w-0 space-y-1.5">
128
- {#if account.status !== 'active'}<p class="text-xs text-zinc-400">{t('ui.usageSkipped')}</p>
130
+ {#if account.authType === 'siwc'}<a href="https://chatgpt.com" target="_blank" rel="noopener noreferrer" class="text-xs text-nexus-400 underline underline-offset-4" data-testid="siwc-usage-link">{t('ui.siwcUsage')}</a>
131
+ {:else if account.status !== 'active'}<p class="text-xs text-zinc-400">{t('ui.usageSkipped')}</p>
129
132
  {:else if !row.usage && busy && !row.error}<LoadingIndicator size="xs" centered={false} height="none" label={t('ui.usageLoading')} />
130
133
  {:else}
131
134
  {#each [row.usage?.usage.value?.primary, row.usage?.usage.value?.secondary].filter(Boolean) as window}
@@ -8,7 +8,7 @@ export function verificationUrl(value, kind) {
8
8
  if (typeof value !== 'string' || value !== value.trim() || value.length > 8192 || /[\u0000-\u001f\u007f]/.test(value)) return null;
9
9
  try {
10
10
  const url = new URL(value);
11
- const path = kind === 'device' ? '/codex/device' : '/oauth/authorize';
11
+ const path = kind === 'device' ? '/codex/device' : kind === 'pkce' ? '/oauth/authorize' : kind === 'siwc' ? '/api/accounts/authorize' : null;
12
12
  if (url.origin !== 'https://auth.openai.com' || url.pathname !== path || url.username || url.password || url.hash) return null;
13
13
  return url.href;
14
14
  } catch { return null; }
@@ -17,7 +17,7 @@ export function verificationUrl(value, kind) {
17
17
  * @param {unknown} input @param {string} kind @param {number} [now] */
18
18
  export function parseLoginStart(input, kind, now = Date.now()) {
19
19
  const value = record(input);
20
- if (!['device', 'pkce'].includes(kind) || typeof value.sessionId !== 'string' || !value.sessionId
20
+ if (!['device', 'pkce', 'siwc'].includes(kind) || typeof value.sessionId !== 'string' || !value.sessionId
21
21
  || value.sessionId !== value.sessionId.trim() || value.sessionId.length > 128 || /[\u0000-\u001f\u007f]/.test(value.sessionId)
22
22
  || typeof value.expiresAt !== 'number' || !Number.isFinite(value.expiresAt) || value.expiresAt <= now
23
23
  || value.expiresAt > now + 30 * 60 * 1000) throw new Error('invalid_response');
@@ -38,11 +38,11 @@ function record(value) {
38
38
  if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('invalid_response');
39
39
  return /** @type {Record<string, unknown>} */ (value);
40
40
  }
41
- /** @param {unknown} input @param {string} sessionId */
42
- export function loginStatus(input, sessionId) {
41
+ /** @param {unknown} input @param {string} sessionId @param {string} [expectedKind] */
42
+ export function loginStatus(input, sessionId, expectedKind) {
43
43
  const value = record(input);
44
44
  if (value.sessionId !== sessionId || typeof value.state !== 'string' || !Object.hasOwn(loginStates, value.state)
45
- || typeof value.kind !== 'string' || !['device', 'pkce'].includes(value.kind) || !Number.isFinite(value.expiresAt)) throw new Error('invalid_response');
45
+ || typeof value.kind !== 'string' || !['device', 'pkce', 'siwc'].includes(value.kind) || (expectedKind !== undefined && value.kind !== expectedKind) || !Number.isFinite(value.expiresAt)) throw new Error('invalid_response');
46
46
  return value;
47
47
  }
48
48
  /** @param {unknown} input */
@@ -51,6 +51,8 @@ export function accountSummary(input) {
51
51
  if (typeof value.id !== 'string' || typeof value.label !== 'string' || typeof value.status !== 'string' || !Object.hasOwn(accountStates, value.status)
52
52
  || typeof value.available !== 'boolean') throw new Error('invalid_response');
53
53
  const identity = value.identity === undefined ? {} : record(value.identity);
54
+ const authType = value.authType === undefined ? 'codex' : value.authType;
55
+ if (authType !== 'codex' && authType !== 'siwc') throw new Error('invalid_response');
54
56
  if (value.autoResetCredits !== undefined && typeof value.autoResetCredits !== 'boolean') throw new Error('invalid_response');
55
57
  let pendingAutoReset;
56
58
  if (value.pendingAutoReset !== undefined) {
@@ -62,12 +64,19 @@ export function accountSummary(input) {
62
64
  || pending.completed !== false) throw new Error('invalid_response');
63
65
  pendingAutoReset = { creditId, redeemRequestId, expiresAt };
64
66
  }
65
- return { id: value.id, label: value.label, status: value.status, available: value.available,
66
- autoResetCredits: value.autoResetCredits === true, pendingAutoReset,
67
+ return { id: value.id, label: value.label, status: value.status, available: value.available, authType,
68
+ autoResetCredits: authType === 'codex' && value.autoResetCredits === true, pendingAutoReset: authType === 'codex' ? pendingAutoReset : undefined,
67
69
  expiresAt: Number.isFinite(value.expiresAt) ? value.expiresAt : undefined,
68
70
  email: typeof identity.email === 'string' ? identity.email : undefined,
69
71
  plan: typeof identity.planType === 'string' ? identity.planType : undefined };
70
72
  }
73
+ /** @param {{authType?: string}} account */
74
+ export const accountSourceId = account => account.authType === 'siwc' ? 'chatgpt-siwc' : 'chatgpt';
75
+ /** @param {{authType?: string, status: string}} account */
76
+ export const canQueryUsage = account => account.authType !== 'siwc' && account.status === 'active';
77
+ /** New accounts default to SIWC; reauthentication stays in its original family.
78
+ * @param {{authType?: string}|undefined} [account] */
79
+ export const loginMethods = account => account === undefined ? ['siwc', 'device', 'pkce'] : account.authType === 'siwc' ? ['siwc'] : ['device', 'pkce'];
71
80
  /** @param {unknown} input */
72
81
  function usageWindow(input) {
73
82
  if (input === undefined || input === null) return undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jeffusion/bungee-core",
3
- "version": "5.11.0",
3
+ "version": "5.12.0",
4
4
  "description": "Bungee reverse proxy core engine",
5
5
  "main": "./dist/main.js",
6
6
  "types": "./dist/main.d.ts",
@@ -12,7 +12,7 @@
12
12
  "test": "bun test"
13
13
  },
14
14
  "dependencies": {
15
- "@jeffusion/bungee-types": "^5.11.0",
15
+ "@jeffusion/bungee-types": "^5.12.0",
16
16
  "@types/lodash-es": "^4.17.12",
17
17
  "acorn": "^8.15.0",
18
18
  "canonicalize": "3.0.0",