@jeffusion/bungee-core 5.5.0 → 5.6.1
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/dist/main.js +184 -92
- package/dist/plugins/chatgpt-oauth/control.js +300 -7
- package/dist/plugins/chatgpt-oauth/control.js.map +7 -6
- package/dist/plugins/chatgpt-oauth/manifest.json +23 -0
- package/dist/plugins/chatgpt-oauth/ui/AccountsPage.svelte +33 -10
- package/dist/plugins/chatgpt-oauth/ui/account-model.js +12 -0
- package/dist/plugins/chatgpt-oauth/ui/account-model.test.js +14 -1
- package/package.json +2 -2
|
@@ -78,6 +78,15 @@
|
|
|
78
78
|
"execution": "control",
|
|
79
79
|
"capability": "config.write"
|
|
80
80
|
},
|
|
81
|
+
{
|
|
82
|
+
"path": "/accounts/auto-reset",
|
|
83
|
+
"methods": [
|
|
84
|
+
"POST"
|
|
85
|
+
],
|
|
86
|
+
"handler": "setAutoResetCredits",
|
|
87
|
+
"execution": "control",
|
|
88
|
+
"capability": "config.write"
|
|
89
|
+
},
|
|
81
90
|
{
|
|
82
91
|
"path": "/accounts/draft",
|
|
83
92
|
"methods": [
|
|
@@ -344,6 +353,13 @@
|
|
|
344
353
|
"ui.credit.redeemed": "Redeemed",
|
|
345
354
|
"ui.credit.unknown": "Unknown",
|
|
346
355
|
"ui.useCredit": "Use",
|
|
356
|
+
"ui.autoResetEnabled": "Automatic reset enabled (30 minutes before expiry)",
|
|
357
|
+
"ui.autoResetDisabled": "Automatic reset disabled",
|
|
358
|
+
"ui.autoResetLabel": "Auto reset",
|
|
359
|
+
"ui.enableAutoReset": "Enable automatic reset",
|
|
360
|
+
"ui.disableAutoReset": "Disable automatic reset",
|
|
361
|
+
"ui.autoResetHelp": "The server refreshes the credit list every 5 minutes and schedules available credits for 30 minutes before expiry. Credits already in their final 30 minutes are processed immediately. Expired credits and credits without an expiry are skipped. Unknown results pause automation and require explicit resolution. Enable for this account?",
|
|
362
|
+
"ui.disableAutoResetHelp": "Stop scheduling automatic resets for this account. Requests already sent may still finish.",
|
|
347
363
|
"ui.resetConfirmTitle": "Use reset credit",
|
|
348
364
|
"ui.resetRetryTitle": "Retry reset credit",
|
|
349
365
|
"ui.resetConfirmHelp": "This uses one reset credit for the account. Confirm only once.",
|
|
@@ -537,6 +553,13 @@
|
|
|
537
553
|
"ui.credit.redeemed": "已兑换",
|
|
538
554
|
"ui.credit.unknown": "未知",
|
|
539
555
|
"ui.useCredit": "使用",
|
|
556
|
+
"ui.autoResetEnabled": "自动重置已开启(到期前 30 分钟)",
|
|
557
|
+
"ui.autoResetDisabled": "自动重置已关闭",
|
|
558
|
+
"ui.autoResetLabel": "自动重置",
|
|
559
|
+
"ui.enableAutoReset": "开启自动重置",
|
|
560
|
+
"ui.disableAutoReset": "关闭自动重置",
|
|
561
|
+
"ui.autoResetHelp": "服务端每 5 分钟刷新卡列表,并定时在重置卡到期前 30 分钟自动使用;已进入最后 30 分钟的卡立即处理。跳过已过期或到期时间未知的卡;结果未知时暂停自动兑换,需手动处理。是否为此账号开启?",
|
|
562
|
+
"ui.disableAutoResetHelp": "停止为此账号安排自动重置。已发送的兑换请求仍可能完成。",
|
|
540
563
|
"ui.resetConfirmTitle": "使用重置额度",
|
|
541
564
|
"ui.resetRetryTitle": "重试重置额度",
|
|
542
565
|
"ui.resetConfirmHelp": "这会为该账号使用一份重置额度,请只确认一次。",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
type AccountUsage = ReturnType<typeof accountUsage>;
|
|
41
41
|
type Credit = NonNullable<AccountUsage['resetCredits']['value']>['credits'][number];
|
|
42
42
|
type UsageSnapshot = AccountUsage | { state: 'loading' | 'unavailable'; error?: boolean };
|
|
43
|
-
type PendingReset = { credit: Credit; creditId: string; redeemRequestId: string; unknown: true };
|
|
43
|
+
type PendingReset = { credit: Credit; creditId: string; redeemRequestId: string; unknown: true; automatic?: boolean };
|
|
44
44
|
type Session = { sessionId: string; expiresAt: number; userCode?: string; verificationUri?: string; authorizationUrl?: string };
|
|
45
45
|
const pluginName = 'chatgpt-oauth';
|
|
46
46
|
const id = $props.id();
|
|
@@ -220,6 +220,12 @@
|
|
|
220
220
|
if (!Array.isArray(response.accounts)) throw new Error('invalid_response');
|
|
221
221
|
if (generation !== accountRefreshGeneration) return;
|
|
222
222
|
accounts = response.accounts.map(accountSummary);
|
|
223
|
+
for (const account of accounts) {
|
|
224
|
+
const pending = account.pendingAutoReset;
|
|
225
|
+
if (pending) pendingResetByAccount[account.id] = { ...pending, unknown: true, automatic: true,
|
|
226
|
+
credit: { creditId: pending.creditId, status: 'unknown', expiresAt: pending.expiresAt } };
|
|
227
|
+
else if (pendingResetByAccount[account.id]?.automatic) delete pendingResetByAccount[account.id];
|
|
228
|
+
}
|
|
223
229
|
await refreshUsages(accounts, generation);
|
|
224
230
|
} catch (error) { if (!lifetime.signal.aborted && generation === accountRefreshGeneration) notice = errorText(error); }
|
|
225
231
|
finally { if (generation === accountRefreshGeneration) refreshing = false; }
|
|
@@ -303,7 +309,7 @@
|
|
|
303
309
|
if (actionBusy) return;
|
|
304
310
|
const generation = ++referenceGeneration;
|
|
305
311
|
action = value; actionAccount = account; label = account.label; actionNotice = ''; references = null; referenceNotice = ''; actionOpen = true;
|
|
306
|
-
if (value === 'rename') return;
|
|
312
|
+
if (value === 'rename' || value === 'auto-reset') return;
|
|
307
313
|
referenceNotice = 'ui.referencesLoading';
|
|
308
314
|
try {
|
|
309
315
|
const snapshot = await getConfigSnapshot();
|
|
@@ -355,7 +361,8 @@
|
|
|
355
361
|
actionBusy = true;
|
|
356
362
|
const generation = referenceGeneration;
|
|
357
363
|
try {
|
|
358
|
-
await control('POST', `/accounts/${action}`, { accountRef: actionAccount.id, ...(action === 'rename' ? { label: label.trim() } : {})
|
|
364
|
+
await control('POST', `/accounts/${action}`, { accountRef: actionAccount.id, ...(action === 'rename' ? { label: label.trim() } : {}),
|
|
365
|
+
...(action === 'auto-reset' ? { enabled: !actionAccount.autoResetCredits } : {}) });
|
|
359
366
|
if (generation === referenceGeneration) actionOpen = false;
|
|
360
367
|
await refreshAccounts();
|
|
361
368
|
} catch (error) { actionNotice = errorText(error); }
|
|
@@ -450,6 +457,7 @@
|
|
|
450
457
|
{#if !account.email}<p class="text-sm text-zinc-400">{t('ui.noEmail')}</p>{/if}
|
|
451
458
|
{#if account.plan}<p class="text-sm text-zinc-300" data-testid="account-type">{t('ui.accountType', { plan: account.plan })}</p>{/if}
|
|
452
459
|
{#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>
|
|
453
461
|
</div>
|
|
454
462
|
<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>
|
|
455
463
|
{#if 'usage' in snapshot}
|
|
@@ -457,9 +465,25 @@
|
|
|
457
465
|
{#if snapshot.usage.value?.primary?.usedPercent !== undefined}<MetricBar label={windowLabel(snapshot.usage.value.primary.windowSeconds)} value={snapshot.usage.value.primary.usedPercent} valueLabel={`${snapshot.usage.value.primary.usedPercent}% ${t('ui.used')}`} /><p class="tabular-nums text-xs text-zinc-400">{#if snapshot.usage.value.primary.windowSeconds !== undefined}{t('ui.window', { duration: formatWindow(snapshot.usage.value.primary.windowSeconds) })}{/if}{#if snapshot.usage.value.primary.resetAt !== undefined} · {t('ui.resetAt', { value: resetTime(snapshot.usage.value.primary.resetAt) })}{/if}</p>{/if}
|
|
458
466
|
{#if snapshot.usage.value?.secondary?.usedPercent !== undefined}<MetricBar label={windowLabel(snapshot.usage.value.secondary.windowSeconds)} value={snapshot.usage.value.secondary.usedPercent} valueLabel={`${snapshot.usage.value.secondary.usedPercent}% ${t('ui.used')}`} /><p class="tabular-nums text-xs text-zinc-400">{#if snapshot.usage.value.secondary.windowSeconds !== undefined}{t('ui.window', { duration: formatWindow(snapshot.usage.value.secondary.windowSeconds) })}{/if}{#if snapshot.usage.value.secondary.resetAt !== undefined} · {t('ui.resetAt', { value: resetTime(snapshot.usage.value.secondary.resetAt) })}{/if}</p>{/if}
|
|
459
467
|
{#if !snapshot.usage.value?.primary && !snapshot.usage.value?.secondary}<p class="text-sm text-zinc-400">{t('ui.noUsageWindows')}</p>{/if}
|
|
468
|
+
{:else if snapshot.state === 'loading'}<LoadingIndicator label={t('ui.usageLoading')} size="sm" height="none" />
|
|
469
|
+
{:else}<p class="text-sm text-zinc-400">{t(snapshot.error ? 'ui.usageFailed' : 'ui.usageSkipped')}</p>{/if}
|
|
470
|
+
<div class="mt-auto border-t border-carbon-600 pt-3 space-y-2">
|
|
471
|
+
<div class="flex items-center justify-between gap-2" data-testid="reset-heading">
|
|
472
|
+
<div class="flex min-w-0 flex-wrap items-center gap-2"><span class="nx-field-label">{t('ui.resetCredits')}</span><span aria-hidden="true" class="text-zinc-500">·</span><span class="text-sm text-zinc-400"><span class="nx-display tabular-nums text-zinc-100">{count ?? '—'}</span> {t('ui.creditsAvailable')}</span></div>
|
|
473
|
+
<button type="button" role="switch" aria-checked={account.autoResetCredits}
|
|
474
|
+
aria-label={`${t('ui.autoResetLabel')}: ${account.label}`}
|
|
475
|
+
title={t(account.autoResetCredits ? 'ui.autoResetEnabled' : 'ui.autoResetHelp')}
|
|
476
|
+
disabled={actionBusy || account.status === 'revoked'} data-testid="auto-reset-toggle"
|
|
477
|
+
class="inline-flex h-[24px] shrink-0 items-center gap-1.5 whitespace-nowrap text-xs text-zinc-400 transition-colors hover:text-zinc-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-nexus-500 focus-visible:ring-offset-2 focus-visible:ring-offset-carbon-950 disabled:cursor-not-allowed disabled:opacity-50"
|
|
478
|
+
onclick={() => openAction('auto-reset', account)}>
|
|
479
|
+
<span>{t('ui.autoResetLabel')}</span>
|
|
480
|
+
<span aria-hidden="true" data-testid="auto-reset-track"
|
|
481
|
+
class={`inline-flex h-[16px] w-[30px] shrink-0 items-center border bg-carbon-900 transition-colors ${account.autoResetCredits ? 'border-nexus-500' : 'border-carbon-600'}`}>
|
|
482
|
+
<span class={`block h-[12px] w-[12px] transition-transform ${account.autoResetCredits ? 'translate-x-[15px] bg-nexus-500' : 'translate-x-px bg-carbon-500'}`}></span>
|
|
483
|
+
</span>
|
|
484
|
+
</button>
|
|
485
|
+
</div>
|
|
460
486
|
{#if count !== undefined || ('usage' in snapshot && snapshot.resetCredits.state !== 'unavailable') || pending}
|
|
461
|
-
<div class="mt-auto border-t border-carbon-600 pt-3 space-y-2">
|
|
462
|
-
<div class="flex flex-wrap items-center gap-2" data-testid="reset-heading"><span class="nx-field-label">{t('ui.resetCredits')}</span><span aria-hidden="true" class="text-zinc-500">·</span><span class="text-sm text-zinc-400"><span class="nx-display tabular-nums text-zinc-100">{count ?? '—'}</span> {t('ui.creditsAvailable')}</span></div>
|
|
463
487
|
<!-- Native keyboard scrolling must also work when no credit action is available. -->
|
|
464
488
|
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
|
465
489
|
<div class="credit-viewport h-32 overscroll-contain space-y-1 pr-1" data-testid="credit-viewport" role="region" aria-label={t('ui.resetCredits')} tabindex="0">
|
|
@@ -474,10 +498,8 @@
|
|
|
474
498
|
</div>
|
|
475
499
|
{/each}
|
|
476
500
|
</div>
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
{:else if snapshot.state === 'loading'}<LoadingIndicator label={t('ui.usageLoading')} size="sm" height="none" />
|
|
480
|
-
{:else}<p class="text-sm text-zinc-400">{t(snapshot.error ? 'ui.usageFailed' : 'ui.usageSkipped')}</p>{/if}
|
|
501
|
+
{:else}<p class="text-sm text-zinc-400">{t('ui.creditDetailsUnavailable')}</p>{/if}
|
|
502
|
+
</div>
|
|
481
503
|
<div class="flex flex-wrap items-center justify-end gap-2" data-testid="account-actions">
|
|
482
504
|
<Button variant="secondary" size="sm" disabled={!account.available} onclick={() => openUse(account)}>{@render actionIcon(Server)}{t('ui.useService')}</Button>
|
|
483
505
|
<Button variant="secondary" size="sm" disabled={!account.available} onclick={() => openRoute(account)}>{@render actionIcon(RouteIcon)}{t('ui.useRoute')}</Button>
|
|
@@ -588,10 +610,11 @@
|
|
|
588
610
|
{/snippet}
|
|
589
611
|
|
|
590
612
|
<IndustrialDialog bind:open={actionOpen} busy={actionBusy} scrollBody closeLabel={t('ui.close')} footer={action === 'references' ? undefined : accountActionFooter}
|
|
591
|
-
title={t(actionTitles[action] ?? 'ui.more')} description={actionAccount?.label ?? ''}>
|
|
613
|
+
title={t(action === 'auto-reset' ? (actionAccount?.autoResetCredits ? 'ui.disableAutoReset' : 'ui.enableAutoReset') : actionTitles[action] ?? 'ui.more')} description={actionAccount?.label ?? ''}>
|
|
592
614
|
{#snippet body()}
|
|
593
615
|
<form id={`${id}-account-action`} onsubmit={confirmAction} class="space-y-4">
|
|
594
616
|
{#if action === 'rename'}<label class="block space-y-1.5"><span class="nx-field-label">{t('ui.accountName')}</span><Input bind:value={label} maxlength={128} required /></label>{/if}
|
|
617
|
+
{#if action === 'auto-reset'}<p class="text-sm text-zinc-300">{t(actionAccount?.autoResetCredits ? 'ui.disableAutoResetHelp' : 'ui.autoResetHelp')}</p>{/if}
|
|
595
618
|
{#if ['delete', 'disable'].includes(action)}<p role="alert" class="text-sm text-amber-300">{t('ui.dangerHelp')}</p>{/if}
|
|
596
619
|
{#if referenceNotice}<p role="status" class="text-sm text-zinc-400">{t(referenceNotice)}</p>{/if}
|
|
597
620
|
{#if references}
|
|
@@ -51,7 +51,19 @@ 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
|
+
if (value.autoResetCredits !== undefined && typeof value.autoResetCredits !== 'boolean') throw new Error('invalid_response');
|
|
55
|
+
let pendingAutoReset;
|
|
56
|
+
if (value.pendingAutoReset !== undefined) {
|
|
57
|
+
const pending = record(value.pendingAutoReset);
|
|
58
|
+
const creditId = safeText(pending.creditId);
|
|
59
|
+
const redeemRequestId = safeText(pending.redeemRequestId, 128);
|
|
60
|
+
const expiresAt = safeEpoch(pending.expiresAt);
|
|
61
|
+
if (!creditId || !redeemRequestId || !/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(redeemRequestId)
|
|
62
|
+
|| pending.completed !== false) throw new Error('invalid_response');
|
|
63
|
+
pendingAutoReset = { creditId, redeemRequestId, expiresAt };
|
|
64
|
+
}
|
|
54
65
|
return { id: value.id, label: value.label, status: value.status, available: value.available,
|
|
66
|
+
autoResetCredits: value.autoResetCredits === true, pendingAutoReset,
|
|
55
67
|
expiresAt: Number.isFinite(value.expiresAt) ? value.expiresAt : undefined,
|
|
56
68
|
email: typeof identity.email === 'string' ? identity.email : undefined,
|
|
57
69
|
plan: typeof identity.planType === 'string' ? identity.planType : undefined };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import assert from 'node:assert/strict';
|
|
2
2
|
import { test } from 'bun:test';
|
|
3
|
-
import { accountUsage, errorCode, resetOutcome } from './account-model.js';
|
|
3
|
+
import { accountSummary, accountUsage, errorCode, resetOutcome } from './account-model.js';
|
|
4
4
|
|
|
5
5
|
const usage = { usage: { state: 'fresh', value: { availableCount: 1 } }, resetCredits: { state: 'fresh', value: {
|
|
6
6
|
availableCount: 1,
|
|
@@ -8,6 +8,19 @@ const usage = { usage: { state: 'fresh', value: { availableCount: 1 } }, resetCr
|
|
|
8
8
|
title: 'Daily reset', description: 'A bounded description', expiresAt: 1700003600000 }],
|
|
9
9
|
} } };
|
|
10
10
|
|
|
11
|
+
test('old accounts default automation off; pending automatic reset is bounded and redacted', () => {
|
|
12
|
+
const base = { id: 'simulation-account', label: 'Simulation', status: 'active', available: true };
|
|
13
|
+
assert.equal(accountSummary(base).autoResetCredits, false);
|
|
14
|
+
const pending = { creditId: 'simulation-credit', redeemRequestId: '123e4567-e89b-42d3-a456-426614174000', expiresAt: 1900000000000, completed: false };
|
|
15
|
+
const parsed = accountSummary({ ...base, autoResetCredits: true, pendingAutoReset: { ...pending, accessToken: 'never-expose' } });
|
|
16
|
+
assert.equal(parsed.autoResetCredits, true);
|
|
17
|
+
assert.deepEqual(parsed.pendingAutoReset, { creditId: pending.creditId, redeemRequestId: pending.redeemRequestId, expiresAt: pending.expiresAt });
|
|
18
|
+
for (const autoResetCredits of ['true', 1, null]) assert.throws(() => accountSummary({ ...base, autoResetCredits }), /invalid_response/);
|
|
19
|
+
for (const override of [{ creditId: '' }, { redeemRequestId: 'bad' }, { expiresAt: NaN }, { completed: true }]) {
|
|
20
|
+
assert.throws(() => accountSummary({ ...base, pendingAutoReset: { ...pending, ...override } }), /invalid_response/);
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
|
|
11
24
|
test('projects bounded credit metadata without exposing unsafe fields', () => {
|
|
12
25
|
const result = accountUsage(usage);
|
|
13
26
|
assert.deepEqual(result.resetCredits.value?.credits[0], {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jeffusion/bungee-core",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.6.1",
|
|
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.
|
|
15
|
+
"@jeffusion/bungee-types": "^5.6.1",
|
|
16
16
|
"@types/lodash-es": "^4.17.12",
|
|
17
17
|
"acorn": "^8.15.0",
|
|
18
18
|
"canonicalize": "3.0.0",
|