@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.
@@ -535,6 +535,7 @@ class AccountControlError extends Error {
535
535
  var MAX_ACCOUNTS = 128;
536
536
  var MAX_TEXT = 512;
537
537
  var MAX_TOKEN = 32768;
538
+ var MAX_AUTO_RESET_ATTEMPTS = 256;
538
539
  function text(value, max = MAX_TEXT) {
539
540
  if (typeof value !== "string" || value.length === 0 || value.length > max || value.trim() !== value) {
540
541
  throw new AccountControlError("invalid_input");
@@ -550,13 +551,21 @@ function copyIdentity(value) {
550
551
  return value === undefined ? undefined : { ...value };
551
552
  }
552
553
  function copyAccount(value) {
553
- return { ...value, identity: copyIdentity(value.identity), refreshLock: value.refreshLock && { ...value.refreshLock } };
554
+ return {
555
+ ...value,
556
+ identity: copyIdentity(value.identity),
557
+ refreshLock: value.refreshLock && { ...value.refreshLock },
558
+ autoResetAttempts: value.autoResetAttempts?.map((item) => ({ ...item }))
559
+ };
560
+ }
561
+ function validAutoResetAttempts(value) {
562
+ return value === undefined || Array.isArray(value) && value.length <= MAX_AUTO_RESET_ATTEMPTS && value.every((item) => item && typeof item.creditId === "string" && item.creditId.length > 0 && item.creditId.length <= MAX_TEXT && !/[\u0000-\u001f\u007f]/.test(item.creditId) && typeof item.redeemRequestId === "string" && /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(item.redeemRequestId) && (item.expiresAt === undefined || Number.isFinite(item.expiresAt) && item.expiresAt >= 0) && typeof item.completed === "boolean");
554
563
  }
555
564
  function validStoredAccount(value) {
556
565
  if (typeof value !== "object" || value === null || Array.isArray(value))
557
566
  return false;
558
567
  const account = value;
559
- return typeof account.id === "string" && account.id.length > 0 && account.id.length <= 128 && typeof account.label === "string" && account.label.length > 0 && account.label.length <= MAX_TEXT && ["active", "disabled", "reauth_required", "revoked"].includes(account.status) && Number.isSafeInteger(account.generation) && account.generation > 0 && (account.loginFence === undefined || Number.isSafeInteger(account.loginFence) && account.loginFence > 0) && (account.credentialValid === undefined || typeof account.credentialValid === "boolean") && Number.isFinite(account.createdAt) && Number.isFinite(account.updatedAt);
568
+ return typeof account.id === "string" && account.id.length > 0 && account.id.length <= 128 && typeof account.label === "string" && account.label.length > 0 && account.label.length <= MAX_TEXT && ["active", "disabled", "reauth_required", "revoked"].includes(account.status) && Number.isSafeInteger(account.generation) && account.generation > 0 && (account.loginFence === undefined || Number.isSafeInteger(account.loginFence) && account.loginFence > 0) && (account.credentialValid === undefined || typeof account.credentialValid === "boolean") && (account.autoResetCredits === undefined || typeof account.autoResetCredits === "boolean") && validAutoResetAttempts(account.autoResetAttempts) && Number.isFinite(account.createdAt) && Number.isFinite(account.updatedAt);
560
569
  }
561
570
  function decode(value) {
562
571
  if (value === undefined)
@@ -590,6 +599,7 @@ function statusReason(account) {
590
599
  return;
591
600
  }
592
601
  function accountListItem(account) {
602
+ const pending = account.autoResetAttempts?.find((item) => !item.completed);
593
603
  return {
594
604
  id: account.id,
595
605
  label: account.label,
@@ -597,7 +607,9 @@ function accountListItem(account) {
597
607
  available: account.status === "active" && account.credentialValid && Boolean(account.accessToken && account.refreshToken && Number.isFinite(account.expiresAt)),
598
608
  reason: statusReason(account),
599
609
  expiresAt: account.expiresAt,
600
- identity: copyIdentity(account.identity)
610
+ identity: copyIdentity(account.identity),
611
+ autoResetCredits: account.autoResetCredits === true,
612
+ pendingAutoReset: pending && { ...pending }
601
613
  };
602
614
  }
603
615
 
@@ -789,6 +801,71 @@ class AccountStore {
789
801
  return copyAccount(account);
790
802
  });
791
803
  }
804
+ async setAutoResetCredits(id, enabled) {
805
+ if (typeof enabled !== "boolean")
806
+ throw new AccountControlError("invalid_input");
807
+ return this.mutate((aggregate) => {
808
+ const account = aggregate.accounts.find((item) => item.id === id);
809
+ if (!account)
810
+ throw new AccountControlError("not_found");
811
+ if (account.status === "revoked")
812
+ throw new AccountControlError("revoked");
813
+ account.autoResetCredits = enabled;
814
+ account.updatedAt = Date.now();
815
+ return copyAccount(account);
816
+ });
817
+ }
818
+ async claimAutoReset(id, credit, now) {
819
+ return this.mutate((aggregate) => {
820
+ const account = aggregate.accounts.find((item) => item.id === id);
821
+ if (!account || !accountListItem(account).available || account.autoResetCredits !== true)
822
+ return;
823
+ const attempts = account.autoResetAttempts ?? [];
824
+ if (attempts.some((item) => !item.completed || item.creditId === credit.id))
825
+ return;
826
+ account.autoResetAttempts = attempts.filter((item) => !item.completed || item.expiresAt === undefined || item.expiresAt > now);
827
+ if (account.autoResetAttempts.length >= MAX_AUTO_RESET_ATTEMPTS || credit.expiresAt === undefined || credit.expiresAt <= now)
828
+ return;
829
+ const attempt = { creditId: credit.id, redeemRequestId: randomUUID(), expiresAt: credit.expiresAt, completed: false };
830
+ account.autoResetAttempts.push(attempt);
831
+ account.updatedAt = Date.now();
832
+ return { ...attempt };
833
+ });
834
+ }
835
+ async recordResetAttempt(id, redeemRequestId, credit, now) {
836
+ await this.mutate((aggregate) => {
837
+ const account = aggregate.accounts.find((item) => item.id === id);
838
+ if (!account)
839
+ throw new AccountControlError("not_found");
840
+ const attempts = account.autoResetAttempts ?? [];
841
+ const pending = attempts.find((item) => !item.completed);
842
+ if (pending) {
843
+ if (pending.redeemRequestId !== redeemRequestId || pending.creditId !== credit.id)
844
+ throw new AccountControlError("version_conflict");
845
+ return;
846
+ }
847
+ account.autoResetAttempts = attempts.filter((item) => item.creditId !== credit.id && (item.expiresAt === undefined || item.expiresAt > now));
848
+ if (account.autoResetAttempts.length >= MAX_AUTO_RESET_ATTEMPTS)
849
+ throw new AccountControlError("invalid_input");
850
+ account.autoResetAttempts.push({ creditId: credit.id, redeemRequestId, expiresAt: credit.expiresAt, completed: false });
851
+ account.updatedAt = Date.now();
852
+ });
853
+ }
854
+ async finishAutoReset(id, redeemRequestId, cancel = false) {
855
+ await this.mutate((aggregate) => {
856
+ const account = aggregate.accounts.find((item) => item.id === id);
857
+ if (!account)
858
+ throw new AccountControlError("not_found");
859
+ if (cancel)
860
+ account.autoResetAttempts = account.autoResetAttempts?.filter((item) => item.redeemRequestId !== redeemRequestId);
861
+ else {
862
+ const attempt = account.autoResetAttempts?.find((item) => item.redeemRequestId === redeemRequestId);
863
+ if (attempt)
864
+ attempt.completed = true;
865
+ }
866
+ account.updatedAt = Date.now();
867
+ });
868
+ }
792
869
  async enable(id) {
793
870
  return this.mutate((aggregate) => {
794
871
  const account = aggregate.accounts.find((item) => item.id === id);
@@ -1602,6 +1679,9 @@ class UsageService {
1602
1679
  resetKey(redeemRequestId, creditId) {
1603
1680
  return `${redeemRequestId}\x00${creditId}`;
1604
1681
  }
1682
+ restorePendingReset(accountRef, redeemRequestId, creditId) {
1683
+ this.unknownReset.set(accountRef, this.resetKey(redeemRequestId, creditId));
1684
+ }
1605
1685
  async runReset(accountRef, lease, key, body) {
1606
1686
  let outcome;
1607
1687
  let windowsReset = 0;
@@ -1628,7 +1708,7 @@ class UsageService {
1628
1708
  } catch {}
1629
1709
  return { outcome, windowsReset, ...usage === undefined ? {} : { usage } };
1630
1710
  }
1631
- async consume(accountRef, redeemRequestId, creditId, signal) {
1711
+ async consume(accountRef, redeemRequestId, creditId, signal, options = {}) {
1632
1712
  const key = this.resetKey(redeemRequestId, creditId);
1633
1713
  const active = this.resets.get(accountRef);
1634
1714
  if (active !== undefined) {
@@ -1660,6 +1740,9 @@ class UsageService {
1660
1740
  if (current.resetCredits.availableCount <= 0 || selected?.status !== "available")
1661
1741
  throw new UsageError("credit_unavailable");
1662
1742
  lease = await this.deps.credential(accountRef, this.deps.hostSignal);
1743
+ if (options.canConsume && !await options.canConsume(selected))
1744
+ throw new UsageError("credit_unavailable");
1745
+ await options.beforeConsume?.(selected);
1663
1746
  }
1664
1747
  this.invalidate(accountRef);
1665
1748
  resolveOperation(await this.runReset(accountRef, lease, key, { redeem_request_id: redeemRequestId, credit_id: creditId }));
@@ -1684,6 +1767,193 @@ class UsageService {
1684
1767
  }
1685
1768
  }
1686
1769
 
1770
+ // plugins/chatgpt-oauth/server/auto-reset.ts
1771
+ var AUTO_RESET_LEAD_MS = 30 * 60 * 1000;
1772
+ var AUTO_RESET_INTERVAL_MS = 5 * 60 * 1000;
1773
+ var MAX_TIMER_DELAY_MS = 2147483647;
1774
+ var defaultTimer = {
1775
+ schedule: (callback, delay) => {
1776
+ const timer = setTimeout(() => {
1777
+ callback();
1778
+ }, delay);
1779
+ timer.unref?.();
1780
+ return timer;
1781
+ },
1782
+ cancel: (handle) => clearTimeout(handle)
1783
+ };
1784
+ function isAutoResetDue(credit, now) {
1785
+ return credit.status === "available" && typeof credit.expiresAt === "number" && Number.isFinite(credit.expiresAt) && credit.expiresAt > now && credit.expiresAt - now <= AUTO_RESET_LEAD_MS;
1786
+ }
1787
+
1788
+ class AutoResetScheduler {
1789
+ accounts;
1790
+ usage;
1791
+ signal;
1792
+ now;
1793
+ timer;
1794
+ handle;
1795
+ started = false;
1796
+ disposed = false;
1797
+ running;
1798
+ plans = new Map;
1799
+ accountRuns = new Map;
1800
+ constructor(accounts, usage, signal, now = Date.now, timer = defaultTimer) {
1801
+ this.accounts = accounts;
1802
+ this.usage = usage;
1803
+ this.signal = signal;
1804
+ this.now = now;
1805
+ this.timer = timer;
1806
+ }
1807
+ start() {
1808
+ if (this.started || this.disposed || this.signal.aborted)
1809
+ return;
1810
+ this.started = true;
1811
+ this.schedule(0);
1812
+ }
1813
+ schedule(delay) {
1814
+ if (!this.started || this.disposed || this.signal.aborted)
1815
+ return;
1816
+ this.handle = this.timer.schedule(async () => {
1817
+ try {
1818
+ await this.runOnce();
1819
+ } catch {} finally {
1820
+ this.schedule(AUTO_RESET_INTERVAL_MS);
1821
+ }
1822
+ }, delay);
1823
+ }
1824
+ runOnce() {
1825
+ if (this.disposed || this.signal.aborted)
1826
+ return Promise.resolve();
1827
+ if (!this.running)
1828
+ this.running = this.scan().finally(() => {
1829
+ this.running = undefined;
1830
+ });
1831
+ return this.running;
1832
+ }
1833
+ async scan() {
1834
+ const accounts = await this.accounts.list();
1835
+ const eligible = new Set(accounts.filter((account) => account.available && account.autoResetCredits && (!account.pendingAutoReset || this.accountRuns.has(account.id))).map((account) => account.id));
1836
+ for (const id of this.plans.keys())
1837
+ if (!eligible.has(id))
1838
+ this.cancelPlan(id);
1839
+ for (const account of accounts) {
1840
+ if (this.disposed || this.signal.aborted)
1841
+ return;
1842
+ if (!eligible.has(account.id))
1843
+ continue;
1844
+ if (this.accountRuns.has(account.id))
1845
+ continue;
1846
+ try {
1847
+ const snapshot = await this.usage.get(account.id, this.signal, { forceFresh: true });
1848
+ if (this.disposed || this.signal.aborted)
1849
+ return;
1850
+ if (snapshot.resetCredits.state !== "fresh")
1851
+ continue;
1852
+ this.cancelPlan(account.id);
1853
+ if ((snapshot.resetCredits.availableCount ?? 0) <= 0)
1854
+ continue;
1855
+ const credits = [...snapshot.resetCredits.credits ?? []].filter((credit) => credit.status === "available" && typeof credit.expiresAt === "number" && Number.isFinite(credit.expiresAt) && credit.expiresAt > this.now()).sort((a, b) => a.expiresAt - b.expiresAt);
1856
+ const plan = { credits };
1857
+ this.plans.set(account.id, plan);
1858
+ await this.executePlan(account.id, plan);
1859
+ } catch {}
1860
+ }
1861
+ }
1862
+ cancelPlan(accountRef, expected) {
1863
+ const plan = this.plans.get(accountRef);
1864
+ if (expected && plan !== expected)
1865
+ return;
1866
+ if (plan?.handle !== undefined)
1867
+ this.timer.cancel(plan.handle);
1868
+ this.plans.delete(accountRef);
1869
+ }
1870
+ armPlan(accountRef, plan) {
1871
+ if (this.disposed || this.signal.aborted || this.plans.get(accountRef) !== plan)
1872
+ return;
1873
+ if (plan.credits.length === 0) {
1874
+ this.cancelPlan(accountRef);
1875
+ return;
1876
+ }
1877
+ const dueAt = plan.credits[0].expiresAt - AUTO_RESET_LEAD_MS;
1878
+ plan.handle = this.timer.schedule(() => this.executePlan(accountRef, plan), Math.min(MAX_TIMER_DELAY_MS, Math.max(0, dueAt - this.now())));
1879
+ }
1880
+ executePlan(accountRef, plan) {
1881
+ const previous = this.accountRuns.get(accountRef) ?? Promise.resolve();
1882
+ const operation = previous.catch(() => {
1883
+ return;
1884
+ }).then(async () => {
1885
+ if (this.disposed || this.signal.aborted || this.plans.get(accountRef) !== plan)
1886
+ return;
1887
+ try {
1888
+ const account = accountListItem(await this.accounts.get(accountRef));
1889
+ if (!account.available || !account.autoResetCredits || account.pendingAutoReset) {
1890
+ this.cancelPlan(accountRef, plan);
1891
+ return;
1892
+ }
1893
+ while (this.plans.get(accountRef) === plan && plan.credits.length > 0) {
1894
+ if (this.disposed || this.signal.aborted)
1895
+ return;
1896
+ const credit = plan.credits[0];
1897
+ if (credit.expiresAt - AUTO_RESET_LEAD_MS > this.now())
1898
+ break;
1899
+ plan.credits.shift();
1900
+ if (!isAutoResetDue(credit, this.now()))
1901
+ continue;
1902
+ const attempt = await this.accounts.claimAutoReset(accountRef, credit, this.now());
1903
+ if (!attempt)
1904
+ continue;
1905
+ let deferred;
1906
+ try {
1907
+ const result = await this.usage.consume(accountRef, attempt.redeemRequestId, credit.id, this.signal, {
1908
+ canConsume: async (selected) => {
1909
+ const latest = await this.accounts.get(accountRef);
1910
+ if (this.disposed || this.signal.aborted || latest.autoResetCredits !== true || latest.status !== "active")
1911
+ return false;
1912
+ if (selected.status === "available" && typeof selected.expiresAt === "number" && Number.isFinite(selected.expiresAt) && selected.expiresAt - AUTO_RESET_LEAD_MS > this.now())
1913
+ deferred = selected;
1914
+ return isAutoResetDue(selected, this.now());
1915
+ }
1916
+ });
1917
+ if (result.outcome === "reset_outcome_unknown") {
1918
+ this.cancelPlan(accountRef, plan);
1919
+ return;
1920
+ }
1921
+ await this.accounts.finishAutoReset(accountRef, attempt.redeemRequestId);
1922
+ } catch (error) {
1923
+ if (error instanceof UsageError && ["credits_unavailable", "credit_unavailable", "reset_in_progress"].includes(error.code)) {
1924
+ await this.accounts.finishAutoReset(accountRef, attempt.redeemRequestId, true);
1925
+ if (error.code === "credit_unavailable") {
1926
+ if (deferred) {
1927
+ plan.credits.push(deferred);
1928
+ plan.credits.sort((a, b) => a.expiresAt - b.expiresAt);
1929
+ }
1930
+ continue;
1931
+ }
1932
+ }
1933
+ this.cancelPlan(accountRef, plan);
1934
+ return;
1935
+ }
1936
+ }
1937
+ this.armPlan(accountRef, plan);
1938
+ } catch {
1939
+ this.cancelPlan(accountRef, plan);
1940
+ }
1941
+ }).finally(() => {
1942
+ if (this.accountRuns.get(accountRef) === operation)
1943
+ this.accountRuns.delete(accountRef);
1944
+ });
1945
+ this.accountRuns.set(accountRef, operation);
1946
+ return operation;
1947
+ }
1948
+ dispose() {
1949
+ this.started = false;
1950
+ this.disposed = true;
1951
+ this.timer.cancel(this.handle);
1952
+ for (const id of this.plans.keys())
1953
+ this.cancelPlan(id);
1954
+ }
1955
+ }
1956
+
1687
1957
  // plugins/chatgpt-oauth/server/control.ts
1688
1958
  var CHATGPT_TARGET = "https://chatgpt.com";
1689
1959
  var CONTROL_HOST_CONTRACT_GAP = "ControlRpcContext.binding.bindingOptions.accountRef is required and host-resolved; RPC payload is never used as a fallback.";
@@ -1900,6 +2170,7 @@ class ChatgptControl {
1900
2170
  refreshControllers = new Map;
1901
2171
  apiLifetime = new AbortController;
1902
2172
  usage;
2173
+ autoReset;
1903
2174
  disposed = false;
1904
2175
  abortListener;
1905
2176
  constructor(host, deps = {}) {
@@ -1912,10 +2183,11 @@ class ChatgptControl {
1912
2183
  fetchImpl: deps.fetchImpl,
1913
2184
  now: this.now,
1914
2185
  timeoutMs: deps.timeoutMs,
1915
- hostSignal: host.signal,
2186
+ hostSignal: this.apiLifetime.signal,
1916
2187
  credential: (id, signal) => this.credential(id, signal),
1917
2188
  rejectAccess: (id, version) => this.accounts.rejectAccess(id, version)
1918
2189
  });
2190
+ this.autoReset = new AutoResetScheduler(this.accounts, this.usage, this.apiLifetime.signal, this.now, deps.autoResetTimer);
1919
2191
  this.abortListener = () => {
1920
2192
  this.dispose();
1921
2193
  };
@@ -1936,6 +2208,7 @@ class ChatgptControl {
1936
2208
  this.assertAlive();
1937
2209
  await this.accounts.read();
1938
2210
  this.assertAlive();
2211
+ this.autoReset.start();
1939
2212
  }
1940
2213
  async refresh(id, signal) {
1941
2214
  for (let attempt = 0;attempt < 120; attempt++) {
@@ -2080,7 +2353,25 @@ class ChatgptControl {
2080
2353
  const creditId = requiredText(body.creditId, 512);
2081
2354
  if (/[\0\r\n]/.test(creditId))
2082
2355
  throw new ControlError("invalid_input");
2083
- return jsonResponse(await this.usage.consume(accountRef, redeemRequestId, creditId, context.requestSignal));
2356
+ const pending = accountListItem(await this.accounts.get(accountRef)).pendingAutoReset;
2357
+ if (pending) {
2358
+ if (pending.redeemRequestId !== redeemRequestId || pending.creditId !== creditId)
2359
+ throw new ControlError("reset_in_progress");
2360
+ this.usage.restorePendingReset(accountRef, redeemRequestId, creditId);
2361
+ }
2362
+ const result = await this.usage.consume(accountRef, redeemRequestId, creditId, context.requestSignal, {
2363
+ beforeConsume: (selected) => this.accounts.recordResetAttempt(accountRef, redeemRequestId, selected, this.now())
2364
+ });
2365
+ if (result.outcome !== "reset_outcome_unknown")
2366
+ await this.accounts.finishAutoReset(accountRef, redeemRequestId);
2367
+ return jsonResponse(result);
2368
+ }) },
2369
+ { path: "/accounts/auto-reset", methods: ["POST"], handler: "setAutoResetCredits", invoke: invoke(async (context) => {
2370
+ const body = await this.readJson(context);
2371
+ exactKeys(body, ["accountRef", "enabled"]);
2372
+ if (typeof body.enabled !== "boolean")
2373
+ throw new ControlError("invalid_input");
2374
+ return jsonResponse({ account: accountListItem(await this.accounts.setAutoResetCredits(requiredText(body.accountRef, 128), body.enabled)) });
2084
2375
  }) },
2085
2376
  { path: "/accounts/draft", methods: ["POST"], handler: "createDraft", invoke: invoke(async (context) => {
2086
2377
  const body = await this.readJson(context);
@@ -2203,6 +2494,7 @@ class ChatgptControl {
2203
2494
  for (const controller of this.refreshControllers.values())
2204
2495
  controller.abort("disposed");
2205
2496
  this.sessions.dispose();
2497
+ this.autoReset.dispose();
2206
2498
  this.usage.dispose();
2207
2499
  this.inFlight.clear();
2208
2500
  }
@@ -2214,6 +2506,7 @@ var api = Object.freeze([
2214
2506
  { path: "/accounts", methods: ["GET"], handler: "listAccounts" },
2215
2507
  { path: "/accounts/usage", methods: ["GET"], handler: "getAccountUsage" },
2216
2508
  { path: "/accounts/usage/reset", methods: ["POST"], handler: "resetAccountUsage" },
2509
+ { path: "/accounts/auto-reset", methods: ["POST"], handler: "setAutoResetCredits" },
2217
2510
  { path: "/accounts/draft", methods: ["POST"], handler: "createDraft" },
2218
2511
  { path: "/login/device", methods: ["POST"], handler: "startDeviceLogin" },
2219
2512
  { path: "/login/pkce", methods: ["POST"], handler: "startPkceLogin" },
@@ -2242,4 +2535,4 @@ export {
2242
2535
  rpc
2243
2536
  };
2244
2537
 
2245
- //# debugId=972BABF0927EC93164756E2164756E21
2538
+ //# debugId=8E7B0629542A407F64756E2164756E21