@eddyskywalker/dsh-chatgpt-subscription 0.8.2 → 0.8.4

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.
Files changed (59) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +1 -1
  3. package/lib/client.js +87 -1
  4. package/lib/client.js.map +1 -1
  5. package/lib/index.js +758 -166
  6. package/lib/types/client/CodexSubscriptionSection.d.ts.map +1 -1
  7. package/lib/types/client/antigravity/AntigravitySection.d.ts.map +1 -1
  8. package/lib/types/client/command-code/CommandCodeSection.d.ts.map +1 -1
  9. package/lib/types/client/common/quota-follow-up.d.ts +30 -0
  10. package/lib/types/client/common/quota-follow-up.d.ts.map +1 -0
  11. package/lib/types/client/kimi-code/KimiCodeSection.d.ts.map +1 -1
  12. package/lib/types/client/workbuddy/WorkBuddySection.d.ts.map +1 -1
  13. package/lib/types/client/zhipu/ZhipuSection.d.ts.map +1 -1
  14. package/lib/types/host/antigravity/oauth.d.ts +15 -1
  15. package/lib/types/host/antigravity/oauth.d.ts.map +1 -1
  16. package/lib/types/host/antigravity/port-constants.d.ts +20 -0
  17. package/lib/types/host/antigravity/port-constants.d.ts.map +1 -0
  18. package/lib/types/host/antigravity/routes.d.ts.map +1 -1
  19. package/lib/types/host/command-code/client.d.ts +9 -1
  20. package/lib/types/host/command-code/client.d.ts.map +1 -1
  21. package/lib/types/host/command-code/routes.d.ts.map +1 -1
  22. package/lib/types/host/common/catalog-snapshot.d.ts +61 -0
  23. package/lib/types/host/common/catalog-snapshot.d.ts.map +1 -0
  24. package/lib/types/host/common/credential-read-cache.d.ts +73 -0
  25. package/lib/types/host/common/credential-read-cache.d.ts.map +1 -0
  26. package/lib/types/host/common/quota-refresh.d.ts +35 -0
  27. package/lib/types/host/common/quota-refresh.d.ts.map +1 -0
  28. package/lib/types/host/common/same-origin.d.ts +30 -0
  29. package/lib/types/host/common/same-origin.d.ts.map +1 -0
  30. package/lib/types/host/credential-store-secret-service.d.ts +1 -0
  31. package/lib/types/host/credential-store-secret-service.d.ts.map +1 -1
  32. package/lib/types/host/kimi-code/client.d.ts.map +1 -1
  33. package/lib/types/host/kimi-code/routes.d.ts.map +1 -1
  34. package/lib/types/host/routes.d.ts.map +1 -1
  35. package/lib/types/host/token-store-macos.d.ts +9 -0
  36. package/lib/types/host/token-store-macos.d.ts.map +1 -1
  37. package/lib/types/host/token-store-windows.d.ts +8 -0
  38. package/lib/types/host/token-store-windows.d.ts.map +1 -1
  39. package/lib/types/host/usage-service.d.ts +19 -1
  40. package/lib/types/host/usage-service.d.ts.map +1 -1
  41. package/lib/types/host/workbuddy/client.d.ts +9 -1
  42. package/lib/types/host/workbuddy/client.d.ts.map +1 -1
  43. package/lib/types/host/workbuddy/routes.d.ts.map +1 -1
  44. package/lib/types/host/zhipu/routes.d.ts.map +1 -1
  45. package/lib/types/shared/antigravity-contracts.d.ts +8 -0
  46. package/lib/types/shared/antigravity-contracts.d.ts.map +1 -1
  47. package/lib/types/shared/command-code-contracts.d.ts +8 -0
  48. package/lib/types/shared/command-code-contracts.d.ts.map +1 -1
  49. package/lib/types/shared/contracts.d.ts +8 -0
  50. package/lib/types/shared/contracts.d.ts.map +1 -1
  51. package/lib/types/shared/kimi-code-contracts.d.ts +8 -0
  52. package/lib/types/shared/kimi-code-contracts.d.ts.map +1 -1
  53. package/lib/types/shared/workbuddy-contracts.d.ts +8 -0
  54. package/lib/types/shared/workbuddy-contracts.d.ts.map +1 -1
  55. package/lib/types/shared/zhipu-contracts.d.ts +8 -0
  56. package/lib/types/shared/zhipu-contracts.d.ts.map +1 -1
  57. package/package.json +32 -33
  58. package/lib/types/host/compaction-patch.d.ts +0 -17
  59. package/lib/types/host/compaction-patch.d.ts.map +0 -1
package/lib/index.js CHANGED
@@ -7,8 +7,8 @@ import os, { homedir } from "node:os";
7
7
  import { createHash, randomBytes, randomUUID } from "node:crypto";
8
8
  import { isDeepStrictEqual } from "node:util";
9
9
  import { execSync, spawn } from "node:child_process";
10
- import fs, { appendFileSync, constants, copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, rmSync, statSync, utimesSync, writeFileSync } from "node:fs";
11
10
  import fsPromises, { chmod, lstat, mkdir, open, rename, stat, unlink } from "node:fs/promises";
11
+ import fs, { appendFileSync, constants, copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, rmSync, statSync, utimesSync, writeFileSync } from "node:fs";
12
12
  import http, { createServer } from "node:http";
13
13
  import { WebError } from "@deepseek-ai/dsh-web";
14
14
  import { lookup } from "node:dns/promises";
@@ -327,6 +327,125 @@ function dshHomeDir() {
327
327
  return process.env.DSH_HOME?.trim() || path.join(os.homedir(), ".dsh");
328
328
  }
329
329
  //#endregion
330
+ //#region src/host/common/credential-read-cache.ts
331
+ /**
332
+ * Identity a file-backed credential store reports when nothing is stored.
333
+ *
334
+ * An absent file is a *known* answer, not a cache miss: the store returns null
335
+ * without decrypting anything, which is what makes the signed-out settings card
336
+ * cheap to render.
337
+ */
338
+ const ABSENT_CREDENTIAL = "absent";
339
+ /**
340
+ * Cheap identity of one credential file: its last write time and size.
341
+ *
342
+ * Returns {@link ABSENT_CREDENTIAL} when the file does not exist, and `null`
343
+ * when its state cannot be established at all (a stat failure other than
344
+ * ENOENT) — `null` disables caching for that read rather than risking a stale
345
+ * answer.
346
+ */
347
+ async function fileCredentialIdentity(path) {
348
+ try {
349
+ const stats = await stat(path);
350
+ return `${stats.mtimeMs}:${stats.size}`;
351
+ } catch (error) {
352
+ return isMissing$1(error) ? ABSENT_CREDENTIAL : null;
353
+ }
354
+ }
355
+ /**
356
+ * Identity for a keyring-backed credential, which has no file to compare.
357
+ *
358
+ * A time bucket is the honest substitute: a burst of reads inside one bucket
359
+ * shares a single keyring lookup, and the bucket bounds how long a write from
360
+ * another process can go unnoticed. Fifteen seconds matches the scan cache the
361
+ * WorkBuddy store already keeps, and stays far below the polling interval of
362
+ * every surface that reads these credentials.
363
+ */
364
+ async function keyringCredentialIdentity(ttlMs) {
365
+ return `bucket:${Math.floor(Date.now() / ttlMs)}`;
366
+ }
367
+ /**
368
+ * In-memory snapshot of one credential read.
369
+ *
370
+ * Reading a credential is expensive on every platform this plugin supports:
371
+ * Windows runs a DPAPI unprotect through a spawned `powershell.exe` (measured
372
+ * at 200-243 ms on a real machine), macOS spawns `security`, and Linux spawns
373
+ * `secret-tool`. One settings request reads several credentials — a pool is
374
+ * read once per `read()` call, and a status card asks for the pool twice — so
375
+ * opening a tab cost 3-5 process launches and took 1-3 s.
376
+ *
377
+ * A snapshot is reused only while the stored value is provably unchanged, so a
378
+ * write from another process (the CLI login helper, a second Host) is still
379
+ * observed. The identity is read *before* the value: a write that lands during
380
+ * a read leaves the cached identity older than the stored bytes, so the next
381
+ * read compares against a newer identity and decrypts again. Reading it after
382
+ * would cache the older value under the newer identity and hide that write.
383
+ *
384
+ * Concurrent reads that observe the same identity share one in-flight read,
385
+ * which is what makes the two reads inside `readPoolStatus` cost one launch
386
+ * rather than two.
387
+ *
388
+ * Every write through the owning store calls {@link invalidate}, so the
389
+ * write-then-read verification the pools perform still reads real storage
390
+ * instead of the snapshot it just wrote.
391
+ */
392
+ var CredentialReadCache = class {
393
+ identity;
394
+ entry = null;
395
+ pending = null;
396
+ /**
397
+ * @param identity - cheap identity of the stored value; `null` disables
398
+ * caching for the read.
399
+ */
400
+ constructor(identity) {
401
+ this.identity = identity;
402
+ }
403
+ /**
404
+ * Read through the snapshot.
405
+ *
406
+ * @param read - the expensive read, invoked only when the snapshot cannot
407
+ * answer.
408
+ * @returns the stored value, or null when nothing is stored.
409
+ */
410
+ async load(read) {
411
+ const identity = await this.identity().catch(() => null);
412
+ if (identity === null) return read();
413
+ if (this.entry !== null && this.entry.identity === identity) return this.entry.value;
414
+ if (identity === "absent") {
415
+ this.entry = {
416
+ identity,
417
+ value: null
418
+ };
419
+ return null;
420
+ }
421
+ if (this.pending !== null && this.pending.identity === identity) return this.pending.promise;
422
+ const promise = read();
423
+ this.pending = {
424
+ identity,
425
+ promise
426
+ };
427
+ const settle = (value) => {
428
+ if (this.pending !== null && this.pending.promise === promise) this.pending = null;
429
+ this.entry = {
430
+ identity,
431
+ value
432
+ };
433
+ };
434
+ promise.then(settle, () => {
435
+ if (this.pending !== null && this.pending.promise === promise) this.pending = null;
436
+ });
437
+ return promise;
438
+ }
439
+ /** Drop the snapshot; the next read consults storage again. */
440
+ invalidate() {
441
+ this.entry = null;
442
+ this.pending = null;
443
+ }
444
+ };
445
+ function isMissing$1(error) {
446
+ return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
447
+ }
448
+ //#endregion
330
449
  //#region src/host/token-store.ts
331
450
  function parseStoredCredentials(value) {
332
451
  if (typeof value !== "object" || value === null) throw new Error("credential bundle is not an object");
@@ -403,6 +522,14 @@ var WindowsDpapiCredentialStore = class {
403
522
  kind: "windows-dpapi",
404
523
  encrypted: true
405
524
  };
525
+ /**
526
+ * Decrypted snapshot, validated against the file's own mtime and size.
527
+ *
528
+ * Every uncached read is a spawned `powershell.exe` (~200 ms), and one
529
+ * settings request reads several credentials, so the snapshot is what keeps a
530
+ * tab switch from costing seconds.
531
+ */
532
+ cache = new CredentialReadCache(() => fileCredentialIdentity(this.path));
406
533
  constructor(path, parse) {
407
534
  this.path = path;
408
535
  this.parse = parse;
@@ -410,20 +537,24 @@ var WindowsDpapiCredentialStore = class {
410
537
  if (dirname(path) === path) throw new Error("invalid DPAPI credential path");
411
538
  }
412
539
  async load() {
413
- const result = await runPowerShell(UNPROTECT_SCRIPT, this.path, "");
414
- if (result.code === 3) return null;
415
- if (result.code !== 0) throw new Error("DPAPI credential read failed");
416
- try {
417
- return this.parse(JSON.parse(result.stdout));
418
- } catch {
419
- throw new Error("DPAPI credential payload is invalid");
420
- }
540
+ return this.cache.load(async () => {
541
+ const result = await runPowerShell(UNPROTECT_SCRIPT, this.path, "");
542
+ if (result.code === 3) return null;
543
+ if (result.code !== 0) throw new Error("DPAPI credential read failed");
544
+ try {
545
+ return this.parse(JSON.parse(result.stdout));
546
+ } catch {
547
+ throw new Error("DPAPI credential payload is invalid");
548
+ }
549
+ });
421
550
  }
422
551
  async save(value) {
423
552
  if ((await runPowerShell(PROTECT_SCRIPT, this.path, JSON.stringify(value))).code !== 0) throw new Error("DPAPI credential write failed");
553
+ this.cache.invalidate();
424
554
  }
425
555
  async clear() {
426
556
  if ((await runPowerShell(CLEAR_SCRIPT, this.path, "")).code !== 0) throw new Error("DPAPI credential deletion failed");
557
+ this.cache.invalidate();
427
558
  }
428
559
  };
429
560
  var WindowsDpapiTokenStore = class extends WindowsDpapiCredentialStore {
@@ -485,6 +616,14 @@ function runPowerShell(script, path, stdin) {
485
616
  const DEFAULT_SERVICE = "dsh-chatgpt-subscription";
486
617
  const DEFAULT_ACCOUNT = "oauth";
487
618
  /**
619
+ * How long one keychain read may stand in for the stored value.
620
+ *
621
+ * A keychain has no mtime to compare, so this is the price of not spawning
622
+ * `security` on every read; it is far below the 60 s polling interval of the
623
+ * surfaces that read these credentials.
624
+ */
625
+ const KEYRING_READ_CACHE_TTL_MS$1 = 15e3;
626
+ /**
488
627
  * macOS credential storage backed by the login Keychain through the built-in
489
628
  * `security` command-line tool. The payload is encrypted at rest by the
490
629
  * Keychain, so this store reports itself as encrypted like Windows DPAPI.
@@ -497,6 +636,15 @@ var MacKeychainCredentialStore = class {
497
636
  kind: "macos-keychain",
498
637
  encrypted: true
499
638
  };
639
+ /**
640
+ * Snapshot of the last keychain read.
641
+ *
642
+ * A keychain lookup spawns `security`, and one settings request performs
643
+ * several of them. There is no file to compare against, so the snapshot is
644
+ * keyed on a short time bucket instead — see
645
+ * {@link keyringCredentialIdentity}.
646
+ */
647
+ cache = new CredentialReadCache(() => keyringCredentialIdentity(KEYRING_READ_CACHE_TTL_MS$1));
500
648
  constructor(service, account, parse) {
501
649
  this.service = service;
502
650
  this.account = account;
@@ -504,21 +652,23 @@ var MacKeychainCredentialStore = class {
504
652
  if (process.platform !== "darwin") throw new Error("macOS Keychain storage requires macOS");
505
653
  }
506
654
  async load() {
507
- const result = await runSecurity([
508
- "find-generic-password",
509
- "-a",
510
- this.account,
511
- "-s",
512
- this.service,
513
- "-w"
514
- ]);
515
- if (result.code === 44) return null;
516
- if (result.code !== 0) throw new Error("Keychain credential read failed");
517
- try {
518
- return this.parse(parseSecurityPayload(result.stdout));
519
- } catch {
520
- throw new Error("Keychain credential payload is invalid");
521
- }
655
+ return this.cache.load(async () => {
656
+ const result = await runSecurity([
657
+ "find-generic-password",
658
+ "-a",
659
+ this.account,
660
+ "-s",
661
+ this.service,
662
+ "-w"
663
+ ]);
664
+ if (result.code === 44) return null;
665
+ if (result.code !== 0) throw new Error("Keychain credential read failed");
666
+ try {
667
+ return this.parse(parseSecurityPayload(result.stdout));
668
+ } catch {
669
+ throw new Error("Keychain credential payload is invalid");
670
+ }
671
+ });
522
672
  }
523
673
  async save(value) {
524
674
  if ((await runSecurity([
@@ -531,6 +681,7 @@ var MacKeychainCredentialStore = class {
531
681
  JSON.stringify(value),
532
682
  "-U"
533
683
  ])).code !== 0) throw new Error("Keychain credential write failed");
684
+ this.cache.invalidate();
534
685
  }
535
686
  async clear() {
536
687
  const result = await runSecurity([
@@ -541,6 +692,7 @@ var MacKeychainCredentialStore = class {
541
692
  this.service
542
693
  ]);
543
694
  if (result.code !== 0 && result.code !== 44) throw new Error("Keychain credential deletion failed");
695
+ this.cache.invalidate();
544
696
  }
545
697
  };
546
698
  /**
@@ -600,11 +752,22 @@ function runSecurity(args) {
600
752
  //#endregion
601
753
  //#region src/host/credential-store-secret-service.ts
602
754
  const UNAVAILABLE = "Linux encrypted credential storage requires secret-tool (libsecret) and an unlocked Secret Service keyring.";
755
+ /**
756
+ * How long one Secret Service read may stand in for the stored value.
757
+ *
758
+ * `secret-tool` is a process launch per lookup and one settings request performs
759
+ * several, so the snapshot is what keeps a tab switch from costing seconds. A
760
+ * keyring has no mtime to compare, so this bounded bucket is the substitute; it
761
+ * is far below the 60 s polling interval of the surfaces that read these
762
+ * credentials.
763
+ */
764
+ const KEYRING_READ_CACHE_TTL_MS = 15e3;
603
765
  /** Secrets travel over stdin/stdout; command arguments contain only lookup attributes. */
604
766
  var SecretServiceCredentialStore = class {
605
767
  service;
606
768
  account;
607
769
  parse;
770
+ cache = new CredentialReadCache(() => keyringCredentialIdentity(KEYRING_READ_CACHE_TTL_MS));
608
771
  constructor(service, account, parse) {
609
772
  this.service = service;
610
773
  this.account = account;
@@ -619,14 +782,16 @@ var SecretServiceCredentialStore = class {
619
782
  ];
620
783
  }
621
784
  async load() {
622
- const result = await runSecretTool(["lookup", ...this.attributes()]);
623
- if (result.code === 1 && !result.hasStderr && result.stdout === "") return null;
624
- if (result.code !== 0) throw new Error(UNAVAILABLE);
625
- try {
626
- return this.parse(JSON.parse(result.stdout));
627
- } catch {
628
- throw new Error("Secret Service credential payload is invalid");
629
- }
785
+ return this.cache.load(async () => {
786
+ const result = await runSecretTool(["lookup", ...this.attributes()]);
787
+ if (result.code === 1 && !result.hasStderr && result.stdout === "") return null;
788
+ if (result.code !== 0) throw new Error(UNAVAILABLE);
789
+ try {
790
+ return this.parse(JSON.parse(result.stdout));
791
+ } catch {
792
+ throw new Error("Secret Service credential payload is invalid");
793
+ }
794
+ });
630
795
  }
631
796
  async save(value) {
632
797
  const payload = JSON.stringify(value);
@@ -636,10 +801,12 @@ var SecretServiceCredentialStore = class {
636
801
  "--label=DSH Antigravity OAuth",
637
802
  ...this.attributes()
638
803
  ], payload)).code !== 0) throw new Error(UNAVAILABLE);
804
+ this.cache.invalidate();
639
805
  }
640
806
  async clear() {
641
807
  const result = await runSecretTool(["clear", ...this.attributes()]);
642
808
  if (result.code !== 0 && !(result.code === 1 && !result.hasStderr)) throw new Error(UNAVAILABLE);
809
+ this.cache.invalidate();
643
810
  }
644
811
  };
645
812
  function runSecretTool(args, stdin = "") {
@@ -4938,6 +5105,63 @@ function acceptIdentity(current, incoming) {
4938
5105
  return typeof incoming === "string" && incoming.length > 0 ? incoming : current;
4939
5106
  }
4940
5107
  //#endregion
5108
+ //#region src/host/common/same-origin.ts
5109
+ /**
5110
+ * Whether one mutation request comes from this application's own document.
5111
+ *
5112
+ * Two shapes reach these routes:
5113
+ *
5114
+ * 1. A browser page served by this Host. The browser states its origin, and it
5115
+ * must name the same authority the request was addressed to. A cross-site
5116
+ * POST always states the foreign origin, so this comparison is what rejects
5117
+ * it.
5118
+ *
5119
+ * 2. The DSH Desktop shell. Electron's `forwardWebRequest` admits only requests
5120
+ * whose origin is the `dsh-app://app` document, then *removes* the `origin`
5121
+ * header before forwarding to the Host — that application scheme is not an
5122
+ * HTTP origin the Host could compare against. A forwarded request therefore
5123
+ * arrives with no `origin` at all, addressed to the loopback Host.
5124
+ *
5125
+ * A missing `origin` is consequently accepted only when the request was
5126
+ * addressed to a loopback authority, and a stated `origin` must still match
5127
+ * exactly. This is not a CSRF opening: a browser cannot issue a cross-site POST
5128
+ * without stating the foreign origin, so every cross-site request still fails
5129
+ * the comparison — the loopback clause admits only the desktop shell's already
5130
+ * validated forward, and a request that never reached loopback keeps the strict
5131
+ * requirement.
5132
+ *
5133
+ * @param request - Incoming mutation request.
5134
+ * @returns whether the request may be treated as same-origin.
5135
+ */
5136
+ function isSameOriginMutation(request) {
5137
+ const host = request.headers.host;
5138
+ if (typeof host !== "string" || host === "") return false;
5139
+ const origin = request.headers.origin;
5140
+ if (origin === void 0 || origin === "") return isLoopbackAuthority(host);
5141
+ try {
5142
+ const parsed = new URL(origin);
5143
+ return (parsed.protocol === "http:" || parsed.protocol === "https:") && parsed.host.toLowerCase() === host.toLowerCase();
5144
+ } catch {
5145
+ return false;
5146
+ }
5147
+ }
5148
+ /**
5149
+ * Whether one `Host` header names the loopback interface.
5150
+ *
5151
+ * The desktop shell forwards to whatever URL the Host was started with, which
5152
+ * is a loopback authority in every supported posture (`127.0.0.1`, `localhost`,
5153
+ * or the IPv6 literal).
5154
+ */
5155
+ function isLoopbackAuthority(host) {
5156
+ let hostname;
5157
+ try {
5158
+ hostname = new URL(`http://${host}`).hostname.toLowerCase();
5159
+ } catch {
5160
+ return false;
5161
+ }
5162
+ return hostname === "127.0.0.1" || hostname === "localhost" || hostname === "[::1]" || hostname === "::1";
5163
+ }
5164
+ //#endregion
4941
5165
  //#region src/host/usage-service.ts
4942
5166
  /**
4943
5167
  * The quota card reads the same account the conversation uses, but it must keep
@@ -4979,7 +5203,7 @@ var UsageService = class {
4979
5203
  this.fetchFn = options.fetchFn ?? fetch;
4980
5204
  this.now = options.now ?? Date.now;
4981
5205
  }
4982
- async status(authenticated, force = false) {
5206
+ async status(authenticated, force = false, options = {}) {
4983
5207
  if (!authenticated) return {
4984
5208
  state: "signed-out",
4985
5209
  ...EMPTY_USAGE,
@@ -5006,12 +5230,34 @@ var UsageService = class {
5006
5230
  code: "rate-limited",
5007
5231
  message: "Quota refresh is temporarily rate limited."
5008
5232
  });
5233
+ if (options.backgroundRefresh === true && this.cache !== null) {
5234
+ this.startBackgroundRefresh(credentials, accountKey);
5235
+ return this.fromCache(true);
5236
+ }
5009
5237
  if (this.inFlight !== null) return this.inFlight;
5010
5238
  this.inFlight = this.refreshUpstream(credentials, accountKey).finally(() => {
5011
5239
  this.inFlight = null;
5012
5240
  });
5013
5241
  return this.inFlight;
5014
5242
  }
5243
+ /** Whether a quota refresh is running, so a caller can ask again when it lands. */
5244
+ get refreshing() {
5245
+ return this.inFlight !== null;
5246
+ }
5247
+ /**
5248
+ * Refresh behind an answer that has already been given.
5249
+ *
5250
+ * The rejection is absorbed here: this refresh has no caller to receive it,
5251
+ * and {@link fromCache} keeps serving the snapshot it failed to replace.
5252
+ */
5253
+ startBackgroundRefresh(credentials, accountKey) {
5254
+ if (this.inFlight !== null) return;
5255
+ const task = this.refreshUpstream(credentials, accountKey).finally(() => {
5256
+ if (this.inFlight === task) this.inFlight = null;
5257
+ });
5258
+ this.inFlight = task;
5259
+ task.catch(() => void 0);
5260
+ }
5015
5261
  invalidate() {
5016
5262
  this.invalidated = true;
5017
5263
  }
@@ -5417,11 +5663,13 @@ function registerRoutes(ctx, oauth, usage, preferences, proxyManager, searchSwit
5417
5663
  const url = new URL(request.url ?? "/", "http://dsh.local");
5418
5664
  if (request.method === "GET" && url.pathname === `/api/dsh-chatgpt-subscription/status`) {
5419
5665
  const oauthStatus = await oauth.status();
5666
+ const quota = await usage.status(oauthStatus.authenticated, false, { backgroundRefresh: true });
5420
5667
  json(response, {
5421
5668
  ok: true,
5422
5669
  value: {
5423
5670
  ...oauthStatus,
5424
- quota: await usage.status(oauthStatus.authenticated),
5671
+ quota,
5672
+ quotaRefreshing: usage.refreshing,
5425
5673
  preferences: preferences.status(),
5426
5674
  detectedProxy: proxyManager?.getSystemProxy() ?? null,
5427
5675
  activeProxy: proxyManager?.resolveActiveProxyUrl() ?? null,
@@ -5477,7 +5725,7 @@ function registerRoutes(ctx, oauth, usage, preferences, proxyManager, searchSwit
5477
5725
  });
5478
5726
  return;
5479
5727
  }
5480
- if (!isSameOriginMutation$4(request)) {
5728
+ if (!isSameOriginMutation(request)) {
5481
5729
  jsonError(response, 403, {
5482
5730
  code: "csrf-rejected",
5483
5731
  message: "Cross-origin request rejected."
@@ -5668,17 +5916,6 @@ function registerRoutes(ctx, oauth, usage, preferences, proxyManager, searchSwit
5668
5916
  for (const dispose of disposers) dispose();
5669
5917
  };
5670
5918
  }
5671
- function isSameOriginMutation$4(request) {
5672
- const host = request.headers.host;
5673
- const origin = request.headers.origin;
5674
- if (typeof host !== "string" || host === "" || typeof origin !== "string" || origin === "") return false;
5675
- try {
5676
- const parsed = new URL(origin);
5677
- return (parsed.protocol === "http:" || parsed.protocol === "https:") && parsed.host.toLowerCase() === host.toLowerCase();
5678
- } catch {
5679
- return false;
5680
- }
5681
- }
5682
5919
  async function readJson(request) {
5683
5920
  const chunks = [];
5684
5921
  let total = 0;
@@ -5860,6 +6097,19 @@ function currentConfig(entry) {
5860
6097
  function providerId(value) {
5861
6098
  return typeof value === "string" && /^[a-zA-Z0-9_-]{1,80}$/.test(value) ? value : null;
5862
6099
  }
6100
+ //#endregion
6101
+ //#region src/host/antigravity/port-constants.ts
6102
+ /**
6103
+ * Loopback callback ports for provider sign-in flows.
6104
+ *
6105
+ * Windows (Hyper-V/WinNAT) dynamically reserves TCP port ranges that change on
6106
+ * every reboot. A fixed callback port can land inside one and `listen` fails
6107
+ * with `EACCES` — browser authorization never opens. Flows therefore probe for
6108
+ * a usable port instead of assuming the default is free; the redirect URI the
6109
+ * provider receives carries whichever port was probed.
6110
+ */
6111
+ /** Default loopback port the Antigravity CLI's own flow uses. */
6112
+ const DEFAULT_CALLBACK_PORT$1 = 51121;
5863
6113
  let cachedQuota$2;
5864
6114
  let quotaFetchInFlight = null;
5865
6115
  /** Bumped by every cache clear so an in-flight fetch cannot publish stale state. */
@@ -6147,15 +6397,49 @@ function antigravityEnv(namePart) {
6147
6397
  function callbackPort() {
6148
6398
  const configured = Number(antigravityEnv("CALLBACK_PORT"));
6149
6399
  if (Number.isInteger(configured) && configured > 0 && configured <= 65535) return configured;
6150
- return 51121;
6400
+ return DEFAULT_CALLBACK_PORT$1;
6401
+ }
6402
+ /**
6403
+ * First usable loopback port for the callback listener.
6404
+ *
6405
+ * Windows (Hyper-V/WinNAT) dynamically reserves TCP port ranges that change on
6406
+ * every reboot, and the CLI's default port can land inside one — `listen` then
6407
+ * fails with `EACCES: permission denied ::1:51121` and browser authorization
6408
+ * never opens. A fixed port therefore cannot be relied on; the Google redirect
6409
+ * URI carries whatever port this returns, so any free loopback port works. A
6410
+ * configured `DSH_ANTIGRAVITY_CALLBACK_PORT` still wins — callers who set it
6411
+ * accept that the port must be usable.
6412
+ */
6413
+ async function resolveCallbackPort(attempts = 128, host = resolveCallbackHost()) {
6414
+ if (antigravityEnv("CALLBACK_PORT") !== void 0) return callbackPort();
6415
+ for (let offset = 0; offset < attempts; offset += 1) if (await isPortUsable(51121 + offset, host)) return DEFAULT_CALLBACK_PORT$1 + offset;
6416
+ throw new Error(`No usable local port for the Antigravity sign-in callback (tried ${attempts} ports from ${DEFAULT_CALLBACK_PORT$1} on ${host}). A Windows excluded-port range may cover them; set DSH_ANTIGRAVITY_CALLBACK_PORT to a free port.`);
6417
+ }
6418
+ /**
6419
+ * Whether one port can be bound on the interface the listener will use.
6420
+ *
6421
+ * The probe binds the same host `startCallbackServer` binds rather than a fixed
6422
+ * loopback literal. A port free on `127.0.0.1` can still fail on the address the
6423
+ * listener actually takes — a listener holding only the IPv6 loopback, or a
6424
+ * Windows exclusion covering that stack — and a probe that disagrees with the
6425
+ * bind reintroduces the `EACCES` this function exists to avoid.
6426
+ */
6427
+ function isPortUsable(port, host) {
6428
+ return new Promise((resolve) => {
6429
+ const probe = createServer();
6430
+ probe.once("error", () => resolve(false));
6431
+ probe.once("listening", () => probe.close(() => resolve(true)));
6432
+ probe.listen(port, host);
6433
+ });
6151
6434
  }
6152
6435
  function resolveCallbackHost(raw = antigravityEnv("CALLBACK_HOST")) {
6153
6436
  const host = (raw || "localhost").trim().toLowerCase();
6154
6437
  if (host === "localhost" || host === "127.0.0.1" || host === "::1") return host;
6155
6438
  return "localhost";
6156
6439
  }
6157
- function redirectUri() {
6158
- return `http://${resolveCallbackHost()}:${callbackPort()}${"/oauth-callback".startsWith("/"), REDIRECT_PATH}`;
6440
+ function redirectUri(port = callbackPort(), host = resolveCallbackHost()) {
6441
+ const path = ("/oauth-callback".startsWith("/"), REDIRECT_PATH);
6442
+ return `http://${host.includes(":") && !host.startsWith("[") ? `[${host}]` : host}:${port}${path}`;
6159
6443
  }
6160
6444
  function clientId() {
6161
6445
  return antigravityEnv("CLIENT_ID")?.trim() || DEFAULT_CLIENT_ID;
@@ -6210,7 +6494,9 @@ async function getUserEmail(token, fetchFn = fetch) {
6210
6494
  return;
6211
6495
  }
6212
6496
  }
6213
- function startCallbackServer(expectedState) {
6497
+ async function startCallbackServer(expectedState) {
6498
+ const host = resolveCallbackHost();
6499
+ const port = await resolveCallbackPort(128, host);
6214
6500
  return new Promise((resolve, reject) => {
6215
6501
  let settled = false;
6216
6502
  let timeout;
@@ -6226,7 +6512,7 @@ function startCallbackServer(expectedState) {
6226
6512
  if (timeout) clearTimeout(timeout);
6227
6513
  fn();
6228
6514
  };
6229
- const callbackUrl = redirectUri();
6515
+ const callbackUrl = redirectUri(port, host);
6230
6516
  const server = createServer((request, response) => {
6231
6517
  if (request.method !== "GET" && request.method !== "HEAD") {
6232
6518
  response.writeHead(405, { "Content-Type": "text/plain; charset=utf-8" });
@@ -6269,14 +6555,15 @@ function startCallbackServer(expectedState) {
6269
6555
  }));
6270
6556
  });
6271
6557
  server.on("error", reject);
6272
- server.listen(callbackPort(), resolveCallbackHost(), () => {
6558
+ server.listen(port, host, () => {
6273
6559
  timeout = setTimeout(() => {
6274
6560
  finish(() => rejectCode(/* @__PURE__ */ new Error("OAuth callback timed out waiting for browser login")));
6275
6561
  server.close();
6276
6562
  }, OAUTH_CALLBACK_TIMEOUT_MS);
6277
6563
  resolve({
6278
6564
  server,
6279
- waitForCode: () => codePromise
6565
+ waitForCode: () => codePromise,
6566
+ callbackUrl
6280
6567
  });
6281
6568
  });
6282
6569
  });
@@ -6346,8 +6633,7 @@ async function beginWebLogin$1(store, fetchFn = fetch, signal, onSave) {
6346
6633
  if (webLoginFlow$2.status === "pending") return { ...webLoginFlow$2 };
6347
6634
  const { verifier, challenge } = generatePKCE();
6348
6635
  const state = base64Url(randomBytes(32));
6349
- const { server, waitForCode } = await startCallbackServer(state);
6350
- const callbackUrl = redirectUri();
6636
+ const { server, waitForCode, callbackUrl } = await startCallbackServer(state);
6351
6637
  webLoginFlow$2 = {
6352
6638
  status: "pending",
6353
6639
  authUrl: `${AUTH_URL}?${new URLSearchParams$1({
@@ -6440,8 +6726,7 @@ async function ensureApiKey(store, fetchFn = fetch) {
6440
6726
  async function loginAndSave(store, signal, onUrl, fetchFn = fetch, onProgress) {
6441
6727
  const { verifier, challenge } = generatePKCE();
6442
6728
  const state = base64Url(randomBytes(32));
6443
- const { server, waitForCode } = await startCallbackServer(state);
6444
- const callbackUrl = redirectUri();
6729
+ const { server, waitForCode, callbackUrl } = await startCallbackServer(state);
6445
6730
  const authUrl = `${AUTH_URL}?${new URLSearchParams$1({
6446
6731
  client_id: clientId(),
6447
6732
  response_type: "code",
@@ -7583,6 +7868,63 @@ var AntigravityAdapter = class extends LlmAdapter {
7583
7868
  }
7584
7869
  };
7585
7870
  //#endregion
7871
+ //#region src/host/common/quota-refresh.ts
7872
+ /**
7873
+ * One quota refresh that runs behind an already-answered status call.
7874
+ *
7875
+ * A status card used to block on the upstream quota request whenever its
7876
+ * snapshot had aged past the line's TTL — measured at up to 3.6 s on a real
7877
+ * machine, on a tab whose other work answers in milliseconds. A snapshot that
7878
+ * exists can answer immediately instead, with the refresh running behind it.
7879
+ *
7880
+ * The outcome is remembered here rather than dropped: a refresh that keeps
7881
+ * failing would otherwise look exactly like a snapshot that never aged, and the
7882
+ * card would show old numbers with no hint of why they stopped moving.
7883
+ *
7884
+ * The caller still decides whether the snapshot may answer at all. No snapshot,
7885
+ * or one belonging to another account, has to be fetched before the card can be
7886
+ * rendered — those paths call {@link run} and wait.
7887
+ */
7888
+ var QuotaRefresh = class {
7889
+ pending = null;
7890
+ error = null;
7891
+ /**
7892
+ * Start one refresh behind the answer.
7893
+ *
7894
+ * A refresh that is already running is not duplicated; the caller can see
7895
+ * that one is in flight through {@link refreshing} and ask again when it
7896
+ * lands.
7897
+ */
7898
+ start(refresh) {
7899
+ if (this.pending !== null) return;
7900
+ const task = refresh().then(() => {
7901
+ this.error = null;
7902
+ }, (cause) => {
7903
+ this.error = cause instanceof Error ? cause.message : String(cause);
7904
+ }).finally(() => {
7905
+ if (this.pending === task) this.pending = null;
7906
+ });
7907
+ this.pending = task;
7908
+ }
7909
+ /** Run one refresh and wait for it, recording the same outcome. */
7910
+ async run(refresh) {
7911
+ try {
7912
+ await refresh();
7913
+ this.error = null;
7914
+ } catch (cause) {
7915
+ this.error = cause instanceof Error ? cause.message : String(cause);
7916
+ }
7917
+ }
7918
+ /** Whether a refresh started here is still running. */
7919
+ get refreshing() {
7920
+ return this.pending !== null;
7921
+ }
7922
+ /** Message from the most recent failed refresh, or null when none failed. */
7923
+ lastError() {
7924
+ return this.error;
7925
+ }
7926
+ };
7927
+ //#endregion
7586
7928
  //#region src/host/antigravity/routes.ts
7587
7929
  function sendJson$5(response, status, body) {
7588
7930
  response.writeHead(status, { "Content-Type": "application/json" });
@@ -7646,6 +7988,7 @@ async function getAntigravityWebStatus(store, modelSettings, preferences, accoun
7646
7988
  };
7647
7989
  }
7648
7990
  function registerAntigravityRoutes(ctx, store, modelSettings, preferences, fetchFn = fetch, accountPool = new AccountPoolStore(void 0, void 0, store)) {
7991
+ const quotaRefresh = new QuotaRefresh();
7649
7992
  return ctx.webServer.register({
7650
7993
  kind: "prefix",
7651
7994
  path: "/antigravity/api",
@@ -7657,10 +8000,17 @@ function registerAntigravityRoutes(ctx, store, modelSettings, preferences, fetch
7657
8000
  const credentials = await store.read();
7658
8001
  const authenticated = !!(credentials?.access || credentials?.access_token);
7659
8002
  const cached = getCachedQuota();
7660
- if (authenticated && (!cached || Date.now() - (cached.fetchedAt || 0) > 12e4)) await fetchAccountQuota(store, modelSettings, fetchFn).catch(() => void 0);
8003
+ if (authenticated && (!cached || Date.now() - (cached.fetchedAt || 0) > 12e4)) {
8004
+ const refresh = () => fetchAccountQuota(store, modelSettings, fetchFn);
8005
+ if (cached === void 0) await quotaRefresh.run(refresh);
8006
+ else quotaRefresh.start(refresh);
8007
+ }
7661
8008
  return sendJson$5(response, 200, {
7662
8009
  ok: true,
7663
- value: await getAntigravityWebStatus(store, modelSettings, preferences, accountPool)
8010
+ value: {
8011
+ ...await getAntigravityWebStatus(store, modelSettings, preferences, accountPool),
8012
+ quotaRefreshing: quotaRefresh.refreshing
8013
+ }
7664
8014
  });
7665
8015
  }
7666
8016
  if (path === "login" || path === "accounts/login") {
@@ -9092,6 +9442,103 @@ function commandCodePlanLabel(planId) {
9092
9442
  return resolveCommandCodePlan(planId)?.name ?? planId;
9093
9443
  }
9094
9444
  //#endregion
9445
+ //#region src/host/common/catalog-snapshot.ts
9446
+ /**
9447
+ * Persistent snapshot for one provider's live model catalog.
9448
+ *
9449
+ * The in-memory catalog caches keep a process alive between requests, but they
9450
+ * start cold: on the first call after a restart (or a plugin reinstall) the
9451
+ * picker and the settings page both wait on the upstream listing before they
9452
+ * can render anything, and an unreachable endpoint turns that wait into the
9453
+ * full discovery timeout — "the model server is unavailable" on a fresh
9454
+ * install. The catalog is not fast-moving data, so the last successful listing
9455
+ * is persisted to `<DSH_HOME>/storages/` and rehydrated into the in-memory
9456
+ * cache on first use. A snapshot the caller can read is therefore available
9457
+ * from the very first call, with no network in the path.
9458
+ *
9459
+ * Writes are best-effort: persistence is an optimization, never a capability.
9460
+ * A read-only home or a locked file must not fail the fetch that produced the
9461
+ * snapshot.
9462
+ */
9463
+ /** File name of the persisted snapshot for one catalog. */
9464
+ function catalogSnapshotPath(name) {
9465
+ return path.join(dshHomeDir(), "storages", `${name}-catalog.json`);
9466
+ }
9467
+ /**
9468
+ * Snapshot name for one catalog *and* one credential scope.
9469
+ *
9470
+ * A persisted snapshot answers before any credential is read — that is the whole
9471
+ * point of it — so the file itself has to carry the scope it was fetched under.
9472
+ * Otherwise the first call after a restart, for any account, is answered by
9473
+ * whatever listing some other scope happened to persist last: a Kimi listing
9474
+ * fetched for the mainland-cn endpoint would answer a global caller with models
9475
+ * and context windows that region does not serve, and a Command Code listing
9476
+ * fetched against one API environment would answer the other.
9477
+ *
9478
+ * The scope is part of the file name rather than of the payload because a caller
9479
+ * that cannot name the account — the harness resolves this catalog with no token
9480
+ * at all — still has to find the snapshot its own scope owns.
9481
+ *
9482
+ * Everything outside `[a-z0-9]` is folded to `_`, so no scope can escape the
9483
+ * `storages` directory.
9484
+ */
9485
+ function catalogSnapshotName(base, scope) {
9486
+ const normalized = scope.trim().toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
9487
+ return normalized === "" ? base : `${base}-${normalized}`;
9488
+ }
9489
+ /**
9490
+ * Read the persisted snapshot for one catalog.
9491
+ *
9492
+ * Shape errors and unreadable files both return `undefined` — the caller keeps
9493
+ * its in-memory state, and the next successful fetch overwrites the file.
9494
+ */
9495
+ async function readCatalogSnapshot(name, parseModels) {
9496
+ try {
9497
+ const content = await fsPromises.readFile(catalogSnapshotPath(name), "utf8");
9498
+ const parsed = JSON.parse(content);
9499
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return void 0;
9500
+ const record = parsed;
9501
+ const fetchedAt = record.fetchedAt;
9502
+ if (typeof fetchedAt !== "number" || !Number.isFinite(fetchedAt) || fetchedAt <= 0) return void 0;
9503
+ const models = parseModels(record.models);
9504
+ if (models === void 0 || models.length === 0) return void 0;
9505
+ return {
9506
+ fetchedAt,
9507
+ models
9508
+ };
9509
+ } catch {
9510
+ return;
9511
+ }
9512
+ }
9513
+ /** Persist one snapshot; a failure is logged nowhere and never thrown. */
9514
+ async function writeCatalogSnapshot(name, models, fetchedAt) {
9515
+ try {
9516
+ const filePath = catalogSnapshotPath(name);
9517
+ await fsPromises.mkdir(path.dirname(filePath), { recursive: true });
9518
+ const tmp = `${filePath}.tmp.${Date.now()}`;
9519
+ await fsPromises.writeFile(tmp, JSON.stringify({
9520
+ fetchedAt,
9521
+ models
9522
+ }, null, 2), "utf8");
9523
+ await fsPromises.rename(tmp, filePath);
9524
+ } catch {}
9525
+ }
9526
+ /**
9527
+ * Warm an in-memory cache from the persisted snapshot when it is fresher than
9528
+ * what the cache currently holds. Called lazily on the first cache miss of a
9529
+ * process, so a restart serves the previous listing without waiting on the
9530
+ * network.
9531
+ *
9532
+ * @returns the snapshot's `fetchedAt`, or `0` when no usable snapshot exists —
9533
+ * the caller then fetches as before.
9534
+ */
9535
+ async function rehydrateCatalogCache(name, parseModels, accept, currentFetchedAt) {
9536
+ const snapshot = await readCatalogSnapshot(name, parseModels);
9537
+ if (snapshot === void 0 || snapshot.fetchedAt <= currentFetchedAt) return 0;
9538
+ accept(snapshot.fetchedAt, snapshot.models);
9539
+ return snapshot.fetchedAt;
9540
+ }
9541
+ //#endregion
9095
9542
  //#region src/host/command-code/client.ts
9096
9543
  const CLI_VERSION = "1.0.0";
9097
9544
  /**
@@ -9649,6 +10096,15 @@ function extractUnlimited(payload) {
9649
10096
  }
9650
10097
  let cachedCatalog$2;
9651
10098
  let catalogInFlight$2 = null;
10099
+ /**
10100
+ * Snapshot scopes already consulted in this process.
10101
+ *
10102
+ * One entry per API environment rather than a single flag: the persisted file is
10103
+ * scoped to the environment it was fetched for (see `catalogSnapshotName`), so
10104
+ * each environment consults its own file once, and a scope with no snapshot is
10105
+ * not re-read on every cache miss.
10106
+ */
10107
+ const catalogSnapshotScopesLoaded$2 = /* @__PURE__ */ new Set();
9652
10108
  /** Parse the public `/provider/v1/models` payload. */
9653
10109
  function parseProviderModels(payload) {
9654
10110
  const root = asRecord$6(payload) ?? {};
@@ -9691,27 +10147,65 @@ async function fetchProviderModels(options = {}) {
9691
10147
  if (!response.ok) throw new Error(`Command Code model catalog failed: ${response.status}`);
9692
10148
  return parseProviderModels(await response.json());
9693
10149
  }
9694
- /** Cached catalog with a TTL; a failed refresh keeps the previous snapshot. */
10150
+ /** Cached catalog with a TTL; a failed refresh keeps the previous snapshot.
10151
+ *
10152
+ * On the first cache miss of a process the persisted snapshot is rehydrated
10153
+ * first, so a restart serves the previous listing without waiting on the
10154
+ * network. An expired snapshot still answers immediately (stale) while a
10155
+ * refresh runs in the background — the catalog is not fast-moving data, and
10156
+ * waiting on an unreachable endpoint turned a restart into the full discovery
10157
+ * timeout on every model resolution.
10158
+ */
9695
10159
  async function loadProviderModels(options = {}) {
9696
- if (!options.force && cachedCatalog$2 && Date.now() - cachedCatalog$2.fetchedAt < 18e5) return cachedCatalog$2.models;
10160
+ if (options.force === true) return refreshProviderModels$1(options);
10161
+ const current = cachedCatalog$2;
10162
+ if (current !== void 0 && Date.now() - current.fetchedAt < 18e5) return current.models;
10163
+ if (catalogInFlight$2) return catalogInFlight$2;
10164
+ if (current !== void 0) {
10165
+ refreshProviderModels$1(options).catch(() => void 0);
10166
+ return current.models;
10167
+ }
10168
+ const { apiEnv } = options;
10169
+ const scope = apiEnv ?? resolveApiEnv();
10170
+ if (!catalogSnapshotScopesLoaded$2.has(scope)) {
10171
+ catalogSnapshotScopesLoaded$2.add(scope);
10172
+ const rehydratedAt = await rehydrateCatalogCache(catalogSnapshotName("command-code", scope), parseProviderModels, (fetchedAt, models) => {
10173
+ cachedCatalog$2 = {
10174
+ models,
10175
+ fetchedAt
10176
+ };
10177
+ }, 0);
10178
+ const warm = cachedCatalog$2;
10179
+ if (rehydratedAt > 0 && warm !== void 0) {
10180
+ refreshProviderModels$1(options).catch(() => void 0);
10181
+ return warm.models;
10182
+ }
10183
+ }
10184
+ return refreshProviderModels$1(options);
10185
+ }
10186
+ /** One in-flight, single-flighted fetch of the live catalog. */
10187
+ function refreshProviderModels$1(options = {}) {
9697
10188
  if (catalogInFlight$2) return catalogInFlight$2;
9698
10189
  const request = fetchProviderModels(options).then((models) => {
9699
- if (models.length > 0) cachedCatalog$2 = {
9700
- models,
9701
- fetchedAt: Date.now()
9702
- };
10190
+ if (models.length > 0) {
10191
+ const fetchedAt = Date.now();
10192
+ cachedCatalog$2 = {
10193
+ models,
10194
+ fetchedAt
10195
+ };
10196
+ writeCatalogSnapshot(catalogSnapshotName("command-code", options.apiEnv ?? resolveApiEnv()), models, fetchedAt);
10197
+ }
9703
10198
  return models.length > 0 ? models : cachedCatalog$2?.models ?? [];
9704
10199
  }).catch(() => cachedCatalog$2?.models ?? []);
9705
10200
  catalogInFlight$2 = request;
9706
- try {
9707
- return await request;
9708
- } finally {
10201
+ return request.finally(() => {
9709
10202
  if (catalogInFlight$2 === request) catalogInFlight$2 = null;
9710
- }
10203
+ });
9711
10204
  }
9712
10205
  function clearCachedCatalog$3() {
9713
10206
  cachedCatalog$2 = void 0;
9714
10207
  catalogInFlight$2 = null;
10208
+ catalogSnapshotScopesLoaded$2.clear();
9715
10209
  }
9716
10210
  async function getJson(path, apiKey, options = {}) {
9717
10211
  const response = await (options.fetchFn ?? fetch)(`${apiBaseUrl(options.apiEnv ?? resolveApiEnv())}${path}`, {
@@ -11717,17 +12211,6 @@ function sendMethodNotAllowed$3(response) {
11717
12211
  error: "Method Not Allowed"
11718
12212
  });
11719
12213
  }
11720
- function isSameOriginMutation$3(request) {
11721
- const host = request.headers.host;
11722
- const origin = request.headers.origin;
11723
- if (typeof host !== "string" || host === "" || typeof origin !== "string" || origin === "") return false;
11724
- try {
11725
- const parsed = new URL(origin);
11726
- return (parsed.protocol === "http:" || parsed.protocol === "https:") && parsed.host.toLowerCase() === host.toLowerCase();
11727
- } catch {
11728
- return false;
11729
- }
11730
- }
11731
12214
  async function readRequestJson$3(request) {
11732
12215
  return new Promise((resolve, reject) => {
11733
12216
  const chunks = [];
@@ -11852,6 +12335,7 @@ function registerCommandCodeRoutes(ctx, store, modelSettings, preferences, optio
11852
12335
  const data = accountPool === void 0 ? null : await accountPool.read().catch(() => null);
11853
12336
  return data?.activeAccountId ?? data?.accounts.find((account) => account.isPrimary)?.id ?? data?.accounts[0]?.id;
11854
12337
  };
12338
+ const quotaRefresh = new QuotaRefresh();
11855
12339
  return ctx.webServer.register({
11856
12340
  kind: "prefix",
11857
12341
  path: ROUTE_PREFIX$3,
@@ -11864,15 +12348,22 @@ function registerCommandCodeRoutes(ctx, store, modelSettings, preferences, optio
11864
12348
  const credentials = await store.read();
11865
12349
  const targetId = await activeAccountId();
11866
12350
  const cached = getCachedQuotaFor$1(targetId);
11867
- if ((credentials !== null || targetId !== void 0) && (cached === void 0 || Date.now() - (cached.fetchedAt || 0) > 12e4)) await fetchAccountQuota$1(quotaStore, fetchFn, false, targetId).catch(() => void 0);
12351
+ if ((credentials !== null || targetId !== void 0) && (cached === void 0 || Date.now() - (cached.fetchedAt || 0) > 12e4)) {
12352
+ const refresh = () => fetchAccountQuota$1(quotaStore, fetchFn, false, targetId);
12353
+ if (cached === void 0) await quotaRefresh.run(refresh);
12354
+ else quotaRefresh.start(refresh);
12355
+ }
11868
12356
  return sendJson$3(response, 200, {
11869
12357
  ok: true,
11870
- value: await readStatus()
12358
+ value: {
12359
+ ...await readStatus(),
12360
+ quotaRefreshing: quotaRefresh.refreshing
12361
+ }
11871
12362
  });
11872
12363
  }
11873
12364
  if (path === "login") {
11874
12365
  if (method !== "POST") return sendMethodNotAllowed$3(response);
11875
- if (!isSameOriginMutation$3(request)) return sendJson$3(response, 403, {
12366
+ if (!isSameOriginMutation(request)) return sendJson$3(response, 403, {
11876
12367
  ok: false,
11877
12368
  error: "Cross-origin request rejected."
11878
12369
  });
@@ -11893,7 +12384,7 @@ function registerCommandCodeRoutes(ctx, store, modelSettings, preferences, optio
11893
12384
  }
11894
12385
  if (path === "login/apikey") {
11895
12386
  if (method !== "POST") return sendMethodNotAllowed$3(response);
11896
- if (!isSameOriginMutation$3(request)) return sendJson$3(response, 403, {
12387
+ if (!isSameOriginMutation(request)) return sendJson$3(response, 403, {
11897
12388
  ok: false,
11898
12389
  error: "Cross-origin request rejected."
11899
12390
  });
@@ -11913,7 +12404,7 @@ function registerCommandCodeRoutes(ctx, store, modelSettings, preferences, optio
11913
12404
  }
11914
12405
  if (path === "connection/test") {
11915
12406
  if (method !== "POST") return sendMethodNotAllowed$3(response);
11916
- if (!isSameOriginMutation$3(request)) return sendJson$3(response, 403, {
12407
+ if (!isSameOriginMutation(request)) return sendJson$3(response, 403, {
11917
12408
  ok: false,
11918
12409
  error: "Cross-origin request rejected."
11919
12410
  });
@@ -11958,7 +12449,7 @@ function registerCommandCodeRoutes(ctx, store, modelSettings, preferences, optio
11958
12449
  value: await readStatus()
11959
12450
  });
11960
12451
  if (method !== "POST") return sendMethodNotAllowed$3(response);
11961
- if (!isSameOriginMutation$3(request)) return sendJson$3(response, 403, {
12452
+ if (!isSameOriginMutation(request)) return sendJson$3(response, 403, {
11962
12453
  ok: false,
11963
12454
  error: "Cross-origin request rejected."
11964
12455
  });
@@ -11986,7 +12477,7 @@ function registerCommandCodeRoutes(ctx, store, modelSettings, preferences, optio
11986
12477
  }
11987
12478
  if (path === "catalog/refresh") {
11988
12479
  if (method !== "POST") return sendMethodNotAllowed$3(response);
11989
- if (!isSameOriginMutation$3(request)) return sendJson$3(response, 403, {
12480
+ if (!isSameOriginMutation(request)) return sendJson$3(response, 403, {
11990
12481
  ok: false,
11991
12482
  error: "Cross-origin request rejected."
11992
12483
  });
@@ -12014,7 +12505,7 @@ function registerCommandCodeRoutes(ctx, store, modelSettings, preferences, optio
12014
12505
  });
12015
12506
  }
12016
12507
  if (method !== "POST") return sendMethodNotAllowed$3(response);
12017
- if (!isSameOriginMutation$3(request)) return sendJson$3(response, 403, {
12508
+ if (!isSameOriginMutation(request)) return sendJson$3(response, 403, {
12018
12509
  ok: false,
12019
12510
  error: "Cross-origin request rejected."
12020
12511
  });
@@ -12040,7 +12531,7 @@ function registerCommandCodeRoutes(ctx, store, modelSettings, preferences, optio
12040
12531
  }
12041
12532
  if (path === "logout") {
12042
12533
  if (method !== "POST") return sendMethodNotAllowed$3(response);
12043
- if (!isSameOriginMutation$3(request)) return sendJson$3(response, 403, {
12534
+ if (!isSameOriginMutation(request)) return sendJson$3(response, 403, {
12044
12535
  ok: false,
12045
12536
  error: "Cross-origin request rejected."
12046
12537
  });
@@ -13338,8 +13829,18 @@ async function modelRequestHeaders(accessToken, wire) {
13338
13829
  });
13339
13830
  }
13340
13831
  let catalogCache = null;
13832
+ /**
13833
+ * Snapshot scopes already consulted in this process.
13834
+ *
13835
+ * One entry per region rather than a single flag: the persisted file is scoped
13836
+ * to the region it was fetched for (see `catalogSnapshotName`), so each region
13837
+ * consults its own file once, and a scope that has no snapshot is not re-read on
13838
+ * every cache miss.
13839
+ */
13840
+ const catalogSnapshotScopesLoaded$1 = /* @__PURE__ */ new Set();
13341
13841
  function clearCachedCatalog$2() {
13342
13842
  catalogCache = null;
13843
+ catalogSnapshotScopesLoaded$1.clear();
13343
13844
  }
13344
13845
  /**
13345
13846
  * Read one model entry from the live catalog.
@@ -13389,10 +13890,34 @@ function parseCatalogModel(value) {
13389
13890
  * model. A caller that passes `accessToken` still keys the cache on the token,
13390
13891
  * because a different account can see a different listing.
13391
13892
  */
13392
- async function loadProviderModels$1(options = {}) {
13893
+ function loadProviderModels$1(options = {}) {
13894
+ return options.force === true ? refreshProviderModels(options) : cachedOrRevalidate(options);
13895
+ }
13896
+ async function cachedOrRevalidate(options) {
13393
13897
  const region = options.region ?? await resolveRegion();
13394
13898
  let accessToken = options.accessToken;
13395
- if (options.force !== true && catalogCache !== null && (accessToken === void 0 ? catalogCache.region === region : catalogCache.key === `${region}:${accessToken.slice(-8)}`) && Date.now() - catalogCache.fetchedAt < 18e5) return catalogCache.models;
13899
+ if (catalogCache !== null && (accessToken === void 0 ? catalogCache.region === region : catalogCache.key === `${region}:${accessToken.slice(-8)}`) && Date.now() - catalogCache.fetchedAt < 18e5) return catalogCache.models;
13900
+ if (catalogCache !== null && (accessToken === void 0 ? catalogCache.region === region : catalogCache.key === `${region}:${accessToken.slice(-8)}`)) {
13901
+ refreshProviderModels(options).catch(() => void 0);
13902
+ return catalogCache.models;
13903
+ }
13904
+ if (!catalogSnapshotScopesLoaded$1.has(region)) {
13905
+ catalogSnapshotScopesLoaded$1.add(region);
13906
+ if (await rehydrateCatalogCache(catalogSnapshotName("kimi-code", region), parsePersistedCatalogModels, (fetchedAt, models) => {
13907
+ catalogCache = {
13908
+ fetchedAt,
13909
+ models,
13910
+ key: `${region}:persisted`,
13911
+ region
13912
+ };
13913
+ }, catalogCache?.fetchedAt ?? 0) > 0) {
13914
+ refreshProviderModels({
13915
+ ...options,
13916
+ region
13917
+ }).catch(() => void 0);
13918
+ return catalogCache.models;
13919
+ }
13920
+ }
13396
13921
  if (accessToken === void 0) {
13397
13922
  if (options.store === void 0) return [];
13398
13923
  try {
@@ -13405,7 +13930,31 @@ async function loadProviderModels$1(options = {}) {
13405
13930
  }
13406
13931
  }
13407
13932
  const cacheKey = `${region}:${accessToken.slice(-8)}`;
13408
- if (options.force !== true && catalogCache !== null && catalogCache.key === cacheKey && Date.now() - catalogCache.fetchedAt < 18e5) return catalogCache.models;
13933
+ if (catalogCache !== null && catalogCache.key === cacheKey && Date.now() - catalogCache.fetchedAt < 18e5) return catalogCache.models;
13934
+ return refreshProviderModels({
13935
+ ...options,
13936
+ accessToken,
13937
+ region
13938
+ });
13939
+ }
13940
+ /** One in-flight, single-flighted fetch of the live listing. */
13941
+ function refreshProviderModels(options) {
13942
+ return performListing(options);
13943
+ }
13944
+ async function performListing(options) {
13945
+ const region = options.region ?? await resolveRegion();
13946
+ let accessToken = options.accessToken;
13947
+ if (accessToken === void 0) {
13948
+ if (options.store === void 0) return [];
13949
+ try {
13950
+ accessToken = (await ensureAccessToken(options.store, {
13951
+ fetchFn: options.fetchFn,
13952
+ signal: options.signal
13953
+ })).accessToken;
13954
+ } catch {
13955
+ return [];
13956
+ }
13957
+ }
13409
13958
  const response = await (options.fetchFn ?? fetch)(openAIUrl(MODELS_PATH$1, region), {
13410
13959
  headers: await kimiCodeHeaders(accessToken, "openai"),
13411
13960
  signal: timeoutSignal$2(options.signal, DISCOVERY_TIMEOUT_MS$2)
@@ -13418,11 +13967,27 @@ async function loadProviderModels$1(options = {}) {
13418
13967
  catalogCache = {
13419
13968
  fetchedAt: Date.now(),
13420
13969
  models,
13421
- key: cacheKey,
13970
+ key: cacheKeyOf(region, accessToken),
13422
13971
  region
13423
13972
  };
13973
+ writeCatalogSnapshot(catalogSnapshotName("kimi-code", region), models, catalogCache.fetchedAt);
13424
13974
  return models;
13425
13975
  }
13976
+ function cacheKeyOf(region, accessToken) {
13977
+ return `${region}:${accessToken.slice(-8)}`;
13978
+ }
13979
+ /** Shape check for a persisted snapshot.
13980
+ *
13981
+ * Entries are already in the parsed catalog shape, so they are accepted as
13982
+ * they are: routing them through `parseCatalogModel` again would drop every
13983
+ * one (that parser reads the raw listing's `context_length`, not the parsed
13984
+ * entry's `contextWindow`). Only the identity field is checked.
13985
+ */
13986
+ function parsePersistedCatalogModels(value) {
13987
+ if (!Array.isArray(value)) return void 0;
13988
+ const models = value.filter((entry) => typeof entry === "object" && entry !== null && typeof entry.id === "string" && entry.id !== "" && typeof entry.contextWindow === "number" && entry.contextWindow > 0);
13989
+ return models.length > 0 ? models : void 0;
13990
+ }
13426
13991
  /** Choose the wire dialect for one model, from the live catalog when it says. */
13427
13992
  function wireForCatalogEntry(modelId, catalog) {
13428
13993
  if (catalog.find((model) => model.id === modelId)?.protocol === "anthropic") return "anthropic";
@@ -16219,17 +16784,6 @@ function sendMethodNotAllowed$2(response) {
16219
16784
  error: "Method Not Allowed"
16220
16785
  });
16221
16786
  }
16222
- function isSameOriginMutation$2(request) {
16223
- const host = request.headers.host;
16224
- const origin = request.headers.origin;
16225
- if (typeof host !== "string" || host === "" || typeof origin !== "string" || origin === "") return false;
16226
- try {
16227
- const parsed = new URL(origin);
16228
- return (parsed.protocol === "http:" || parsed.protocol === "https:") && parsed.host.toLowerCase() === host.toLowerCase();
16229
- } catch {
16230
- return false;
16231
- }
16232
- }
16233
16787
  async function readRequestJson$2(request) {
16234
16788
  return new Promise((resolve, reject) => {
16235
16789
  const chunks = [];
@@ -16356,6 +16910,7 @@ function registerKimiCodeRoutes(ctx, store, modelSettings, preferences, options
16356
16910
  credentials: target?.credentials
16357
16911
  };
16358
16912
  };
16913
+ const quotaRefresh = new QuotaRefresh();
16359
16914
  return ctx.webServer.register({
16360
16915
  kind: "prefix",
16361
16916
  path: ROUTE_PREFIX$2,
@@ -16367,29 +16922,29 @@ function registerKimiCodeRoutes(ctx, store, modelSettings, preferences, options
16367
16922
  if (method !== "GET") return sendMethodNotAllowed$2(response);
16368
16923
  const poolActive = await activeAccount();
16369
16924
  const cached = getCachedQuotaFor(poolActive.id ?? null);
16370
- let quotaError = null;
16371
- if (poolActive.credentials !== void 0 && (cached === null || Date.now() - (cached.fetchedAt || 0) > 12e4)) try {
16372
- await fetchAccountQuota$2(store, {
16925
+ if (poolActive.credentials !== void 0 && (cached === null || Date.now() - (cached.fetchedAt || 0) > 12e4)) {
16926
+ const refresh = () => fetchAccountQuota$2(store, {
16373
16927
  fetchFn,
16374
16928
  ...accountPool === void 0 ? {} : {
16375
16929
  credentials: poolActive.credentials,
16376
16930
  accountId: poolActive.id
16377
16931
  }
16378
16932
  });
16379
- } catch (error) {
16380
- quotaError = error instanceof Error ? error.message : String(error);
16933
+ if (cached === null) await quotaRefresh.run(refresh);
16934
+ else quotaRefresh.start(refresh);
16381
16935
  }
16382
16936
  return sendJson$2(response, 200, {
16383
16937
  ok: true,
16384
16938
  value: {
16385
16939
  ...await readStatus(),
16386
- quotaError
16940
+ quotaError: quotaRefresh.lastError(),
16941
+ quotaRefreshing: quotaRefresh.refreshing
16387
16942
  }
16388
16943
  });
16389
16944
  }
16390
16945
  if (path === "login") {
16391
16946
  if (method !== "POST") return sendMethodNotAllowed$2(response);
16392
- if (!isSameOriginMutation$2(request)) return sendJson$2(response, 403, {
16947
+ if (!isSameOriginMutation(request)) return sendJson$2(response, 403, {
16393
16948
  ok: false,
16394
16949
  error: "Cross-origin request rejected."
16395
16950
  });
@@ -16413,7 +16968,7 @@ function registerKimiCodeRoutes(ctx, store, modelSettings, preferences, options
16413
16968
  }
16414
16969
  if (path === "login/cancel") {
16415
16970
  if (method !== "POST") return sendMethodNotAllowed$2(response);
16416
- if (!isSameOriginMutation$2(request)) return sendJson$2(response, 403, {
16971
+ if (!isSameOriginMutation(request)) return sendJson$2(response, 403, {
16417
16972
  ok: false,
16418
16973
  error: "Cross-origin request rejected."
16419
16974
  });
@@ -16425,7 +16980,7 @@ function registerKimiCodeRoutes(ctx, store, modelSettings, preferences, options
16425
16980
  }
16426
16981
  if (path === "connection/test") {
16427
16982
  if (method !== "POST") return sendMethodNotAllowed$2(response);
16428
- if (!isSameOriginMutation$2(request)) return sendJson$2(response, 403, {
16983
+ if (!isSameOriginMutation(request)) return sendJson$2(response, 403, {
16429
16984
  ok: false,
16430
16985
  error: "Cross-origin request rejected."
16431
16986
  });
@@ -16452,7 +17007,7 @@ function registerKimiCodeRoutes(ctx, store, modelSettings, preferences, options
16452
17007
  }
16453
17008
  if (path === "quota") {
16454
17009
  if (method !== "GET" && method !== "POST") return sendMethodNotAllowed$2(response);
16455
- if (method === "POST" && !isSameOriginMutation$2(request)) return sendJson$2(response, 403, {
17010
+ if (method === "POST" && !isSameOriginMutation(request)) return sendJson$2(response, 403, {
16456
17011
  ok: false,
16457
17012
  error: "Cross-origin request rejected."
16458
17013
  });
@@ -16493,7 +17048,7 @@ function registerKimiCodeRoutes(ctx, store, modelSettings, preferences, options
16493
17048
  value: await readStatus()
16494
17049
  });
16495
17050
  if (method !== "POST") return sendMethodNotAllowed$2(response);
16496
- if (!isSameOriginMutation$2(request)) return sendJson$2(response, 403, {
17051
+ if (!isSameOriginMutation(request)) return sendJson$2(response, 403, {
16497
17052
  ok: false,
16498
17053
  error: "Cross-origin request rejected."
16499
17054
  });
@@ -16521,7 +17076,7 @@ function registerKimiCodeRoutes(ctx, store, modelSettings, preferences, options
16521
17076
  }
16522
17077
  if (path === "catalog/refresh") {
16523
17078
  if (method !== "POST") return sendMethodNotAllowed$2(response);
16524
- if (!isSameOriginMutation$2(request)) return sendJson$2(response, 403, {
17079
+ if (!isSameOriginMutation(request)) return sendJson$2(response, 403, {
16525
17080
  ok: false,
16526
17081
  error: "Cross-origin request rejected."
16527
17082
  });
@@ -16552,7 +17107,7 @@ function registerKimiCodeRoutes(ctx, store, modelSettings, preferences, options
16552
17107
  });
16553
17108
  }
16554
17109
  if (method !== "POST") return sendMethodNotAllowed$2(response);
16555
- if (!isSameOriginMutation$2(request)) return sendJson$2(response, 403, {
17110
+ if (!isSameOriginMutation(request)) return sendJson$2(response, 403, {
16556
17111
  ok: false,
16557
17112
  error: "Cross-origin request rejected."
16558
17113
  });
@@ -16578,7 +17133,7 @@ function registerKimiCodeRoutes(ctx, store, modelSettings, preferences, options
16578
17133
  }
16579
17134
  if (path === "logout") {
16580
17135
  if (method !== "POST") return sendMethodNotAllowed$2(response);
16581
- if (!isSameOriginMutation$2(request)) return sendJson$2(response, 403, {
17136
+ if (!isSameOriginMutation(request)) return sendJson$2(response, 403, {
16582
17137
  ok: false,
16583
17138
  error: "Cross-origin request rejected."
16584
17139
  });
@@ -18853,10 +19408,20 @@ let cachedCatalog$1;
18853
19408
  let catalogInFlight$1 = null;
18854
19409
  let catalogInFlightRegion$1 = null;
18855
19410
  const CATALOG_CACHE_TTL_MS$1 = 1800 * 1e3;
19411
+ /**
19412
+ * Snapshot scopes already consulted in this process.
19413
+ *
19414
+ * One entry per region rather than a single flag: the persisted file is scoped
19415
+ * to the region it was fetched for (see `catalogSnapshotName`), so each region
19416
+ * consults its own file once, and a scope with no snapshot is not re-read on
19417
+ * every cache miss.
19418
+ */
19419
+ const catalogSnapshotScopesLoaded = /* @__PURE__ */ new Set();
18856
19420
  function clearCachedCatalog() {
18857
19421
  cachedCatalog$1 = void 0;
18858
19422
  catalogInFlight$1 = null;
18859
19423
  catalogInFlightRegion$1 = null;
19424
+ catalogSnapshotScopesLoaded.clear();
18860
19425
  }
18861
19426
  /**
18862
19427
  * Fetch the live catalog for one account.
@@ -18873,32 +19438,73 @@ async function fetchConfigCatalog(credentials, options = {}) {
18873
19438
  if (!response.ok) throw new Error(`${PROVIDER_NAME$1} model catalog failed: ${response.status}`);
18874
19439
  return parseConfigModels(await response.json(), credentials.region);
18875
19440
  }
18876
- /** Cached catalog with a TTL; a failed refresh keeps the previous snapshot. */
19441
+ /** Cached catalog with a TTL; a failed refresh keeps the previous snapshot.
19442
+ *
19443
+ * On the first cache miss of a process the persisted snapshot is rehydrated
19444
+ * first, so a restart serves the previous listing without waiting on the
19445
+ * network. An expired snapshot still answers immediately (stale) while a
19446
+ * refresh runs in the background — the catalog is not fast-moving data, and
19447
+ * waiting on an unreachable gateway turned a restart into the full discovery
19448
+ * timeout on every model resolution.
19449
+ */
18877
19450
  async function loadConfigCatalog(credentials, options = {}) {
18878
- if (!options.force && cachedCatalog$1 !== void 0 && cachedCatalog$1.region === credentials.region && Date.now() - cachedCatalog$1.fetchedAt < CATALOG_CACHE_TTL_MS$1) return cachedCatalog$1.models;
19451
+ if (options.force === true) return refreshConfigCatalog(credentials, options);
19452
+ if (cachedCatalog$1 !== void 0 && cachedCatalog$1.region === credentials.region && Date.now() - cachedCatalog$1.fetchedAt < CATALOG_CACHE_TTL_MS$1) return cachedCatalog$1.models;
19453
+ if (cachedCatalog$1 !== void 0 && cachedCatalog$1.region === credentials.region) {
19454
+ refreshConfigCatalog(credentials, options).catch(() => void 0);
19455
+ return cachedCatalog$1.models;
19456
+ }
19457
+ if (!catalogSnapshotScopesLoaded.has(credentials.region)) {
19458
+ catalogSnapshotScopesLoaded.add(credentials.region);
19459
+ if (await rehydrateCatalogCache(catalogSnapshotName("workbuddy", credentials.region), (value) => {
19460
+ if (!Array.isArray(value)) return void 0;
19461
+ const models = [];
19462
+ for (const entry of value) {
19463
+ if (typeof entry !== "object" || entry === null) continue;
19464
+ const record = entry;
19465
+ if (typeof record.id !== "string" || record.id === "") continue;
19466
+ if (!(Array.isArray(record.regions) ? record.regions : [record.region]).includes(credentials.region)) continue;
19467
+ models.push(entry);
19468
+ }
19469
+ return models.length > 0 ? models : void 0;
19470
+ }, (fetchedAt, models) => {
19471
+ cachedCatalog$1 = {
19472
+ region: credentials.region,
19473
+ models,
19474
+ fetchedAt
19475
+ };
19476
+ }, 0) > 0) {
19477
+ refreshConfigCatalog(credentials, options).catch(() => void 0);
19478
+ return cachedCatalog$1.models;
19479
+ }
19480
+ }
19481
+ return refreshConfigCatalog(credentials, options);
19482
+ }
19483
+ /** One in-flight, single-flighted fetch of the live catalog. */
19484
+ function refreshConfigCatalog(credentials, options = {}) {
18879
19485
  if (catalogInFlight$1 && catalogInFlightRegion$1 === credentials.region) return catalogInFlight$1;
18880
19486
  const sameRegionCache = () => cachedCatalog$1?.region === credentials.region ? cachedCatalog$1.models : [];
18881
19487
  const request = fetchConfigCatalog(credentials, options).then((models) => {
18882
19488
  if (models.length > 0) {
19489
+ const fetchedAt = Date.now();
18883
19490
  cachedCatalog$1 = {
18884
19491
  region: credentials.region,
18885
19492
  models,
18886
- fetchedAt: Date.now()
19493
+ fetchedAt
18887
19494
  };
19495
+ writeCatalogSnapshot(catalogSnapshotName("workbuddy", credentials.region), models, fetchedAt);
18888
19496
  return models;
18889
19497
  }
18890
19498
  return sameRegionCache();
18891
19499
  }).catch(() => sameRegionCache());
18892
19500
  catalogInFlight$1 = request;
18893
19501
  catalogInFlightRegion$1 = credentials.region;
18894
- try {
18895
- return await request;
18896
- } finally {
19502
+ return request.finally(() => {
18897
19503
  if (catalogInFlight$1 === request) {
18898
19504
  catalogInFlight$1 = null;
18899
19505
  catalogInFlightRegion$1 = null;
18900
19506
  }
18901
- }
19507
+ });
18902
19508
  }
18903
19509
  function formatAmount$1(value) {
18904
19510
  if (value === void 0 || !Number.isFinite(value)) return null;
@@ -19300,17 +19906,6 @@ function sendMethodNotAllowed$1(response) {
19300
19906
  error: "Method Not Allowed"
19301
19907
  });
19302
19908
  }
19303
- function isSameOriginMutation$1(request) {
19304
- const host = request.headers.host;
19305
- const origin = request.headers.origin;
19306
- if (typeof host !== "string" || host === "" || typeof origin !== "string" || origin === "") return false;
19307
- try {
19308
- const parsed = new URL(origin);
19309
- return (parsed.protocol === "http:" || parsed.protocol === "https:") && parsed.host.toLowerCase() === host.toLowerCase();
19310
- } catch {
19311
- return false;
19312
- }
19313
- }
19314
19909
  async function readRequestJson$1(request) {
19315
19910
  return new Promise((resolve, reject) => {
19316
19911
  const chunks = [];
@@ -19426,6 +20021,7 @@ async function getWorkBuddyWebStatus(store, modelSettings, preferences, options
19426
20021
  /** Register the WorkBuddy settings routes under `/workbuddy/api`. */
19427
20022
  function registerWorkBuddyRoutes(ctx, store, modelSettings, preferences, options = {}) {
19428
20023
  const fetchFn = options.fetchFn ?? fetch;
20024
+ const quotaRefresh = new QuotaRefresh();
19429
20025
  const disposeRoutes = ctx.webServer.register({
19430
20026
  kind: "prefix",
19431
20027
  path: ROUTE_PREFIX$1,
@@ -19449,11 +20045,16 @@ function registerWorkBuddyRoutes(ctx, store, modelSettings, preferences, options
19449
20045
  const quotaMatches = cached?.account.id === (credentials === null ? void 0 : accountFromCredentials$1(credentials).id);
19450
20046
  if (credentials !== null && (!quotaMatches || cached === void 0 || Date.now() - (cached.fetchedAt || 0) > 12e4)) {
19451
20047
  if (!quotaMatches) clearCachedQuota$3();
19452
- await fetchAccountQuota$3(store, fetchFn, false, settings.selectedAccountId, settings.hiddenAccountIds).catch(() => void 0);
20048
+ const refresh = () => fetchAccountQuota$3(store, fetchFn, false, settings.selectedAccountId, settings.hiddenAccountIds);
20049
+ if (cached === void 0 || !quotaMatches) await quotaRefresh.run(refresh);
20050
+ else quotaRefresh.start(refresh);
19453
20051
  }
19454
20052
  return sendJson$1(response, 200, {
19455
20053
  ok: true,
19456
- value: await getWorkBuddyWebStatus(store, modelSettings, preferences, options)
20054
+ value: {
20055
+ ...await getWorkBuddyWebStatus(store, modelSettings, preferences, options),
20056
+ quotaRefreshing: quotaRefresh.refreshing
20057
+ }
19457
20058
  });
19458
20059
  }
19459
20060
  if (path === "accounts") {
@@ -19477,7 +20078,7 @@ function registerWorkBuddyRoutes(ctx, store, modelSettings, preferences, options
19477
20078
  }
19478
20079
  if (path === "accounts/login") {
19479
20080
  if (method !== "POST") return sendMethodNotAllowed$1(response);
19480
- if (!isSameOriginMutation$1(request)) return sendJson$1(response, 403, {
20081
+ if (!isSameOriginMutation(request)) return sendJson$1(response, 403, {
19481
20082
  ok: false,
19482
20083
  error: "Cross-origin request rejected."
19483
20084
  });
@@ -19505,7 +20106,7 @@ function registerWorkBuddyRoutes(ctx, store, modelSettings, preferences, options
19505
20106
  }
19506
20107
  if (path === "accounts/action") {
19507
20108
  if (method !== "POST") return sendMethodNotAllowed$1(response);
19508
- if (!isSameOriginMutation$1(request)) return sendJson$1(response, 403, {
20109
+ if (!isSameOriginMutation(request)) return sendJson$1(response, 403, {
19509
20110
  ok: false,
19510
20111
  error: "Cross-origin request rejected."
19511
20112
  });
@@ -19633,7 +20234,7 @@ function registerWorkBuddyRoutes(ctx, store, modelSettings, preferences, options
19633
20234
  }
19634
20235
  if (path === "quota") {
19635
20236
  if (method !== "GET" && method !== "POST") return sendMethodNotAllowed$1(response);
19636
- if (method === "POST" && !isSameOriginMutation$1(request)) return sendJson$1(response, 403, {
20237
+ if (method === "POST" && !isSameOriginMutation(request)) return sendJson$1(response, 403, {
19637
20238
  ok: false,
19638
20239
  error: "Cross-origin request rejected."
19639
20240
  });
@@ -19646,7 +20247,7 @@ function registerWorkBuddyRoutes(ctx, store, modelSettings, preferences, options
19646
20247
  }
19647
20248
  if (path === "connection/test") {
19648
20249
  if (method !== "POST") return sendMethodNotAllowed$1(response);
19649
- if (!isSameOriginMutation$1(request)) return sendJson$1(response, 403, {
20250
+ if (!isSameOriginMutation(request)) return sendJson$1(response, 403, {
19650
20251
  ok: false,
19651
20252
  error: "Cross-origin request rejected."
19652
20253
  });
@@ -19711,7 +20312,7 @@ function registerWorkBuddyRoutes(ctx, store, modelSettings, preferences, options
19711
20312
  value: await getWorkBuddyWebStatus(store, modelSettings, preferences, options)
19712
20313
  });
19713
20314
  if (method !== "POST") return sendMethodNotAllowed$1(response);
19714
- if (!isSameOriginMutation$1(request)) return sendJson$1(response, 403, {
20315
+ if (!isSameOriginMutation(request)) return sendJson$1(response, 403, {
19715
20316
  ok: false,
19716
20317
  error: "Cross-origin request rejected."
19717
20318
  });
@@ -19760,7 +20361,7 @@ function registerWorkBuddyRoutes(ctx, store, modelSettings, preferences, options
19760
20361
  }
19761
20362
  if (path === "catalog/refresh") {
19762
20363
  if (method !== "POST") return sendMethodNotAllowed$1(response);
19763
- if (!isSameOriginMutation$1(request)) return sendJson$1(response, 403, {
20364
+ if (!isSameOriginMutation(request)) return sendJson$1(response, 403, {
19764
20365
  ok: false,
19765
20366
  error: "Cross-origin request rejected."
19766
20367
  });
@@ -19787,7 +20388,7 @@ function registerWorkBuddyRoutes(ctx, store, modelSettings, preferences, options
19787
20388
  }
19788
20389
  if (path === "accounts/strategy") {
19789
20390
  if (method !== "POST") return sendMethodNotAllowed$1(response);
19790
- if (!isSameOriginMutation$1(request)) return sendJson$1(response, 403, {
20391
+ if (!isSameOriginMutation(request)) return sendJson$1(response, 403, {
19791
20392
  ok: false,
19792
20393
  error: "Cross-origin request rejected."
19793
20394
  });
@@ -19809,7 +20410,7 @@ function registerWorkBuddyRoutes(ctx, store, modelSettings, preferences, options
19809
20410
  }
19810
20411
  if (path === "checkin/now") {
19811
20412
  if (method !== "POST") return sendMethodNotAllowed$1(response);
19812
- if (!isSameOriginMutation$1(request)) return sendJson$1(response, 403, {
20413
+ if (!isSameOriginMutation(request)) return sendJson$1(response, 403, {
19813
20414
  ok: false,
19814
20415
  error: "Cross-origin request rejected."
19815
20416
  });
@@ -19826,7 +20427,7 @@ function registerWorkBuddyRoutes(ctx, store, modelSettings, preferences, options
19826
20427
  }
19827
20428
  if (path === "rescan") {
19828
20429
  if (method !== "POST") return sendMethodNotAllowed$1(response);
19829
- if (!isSameOriginMutation$1(request)) return sendJson$1(response, 403, {
20430
+ if (!isSameOriginMutation(request)) return sendJson$1(response, 403, {
19830
20431
  ok: false,
19831
20432
  error: "Cross-origin request rejected."
19832
20433
  });
@@ -22168,17 +22769,6 @@ function sendMethodNotAllowed(response) {
22168
22769
  error: "Method Not Allowed"
22169
22770
  });
22170
22771
  }
22171
- function isSameOriginMutation(request) {
22172
- const host = request.headers.host;
22173
- const origin = request.headers.origin;
22174
- if (typeof host !== "string" || host === "" || typeof origin !== "string" || origin === "") return false;
22175
- try {
22176
- const parsed = new URL(origin);
22177
- return (parsed.protocol === "http:" || parsed.protocol === "https:") && parsed.host.toLowerCase() === host.toLowerCase();
22178
- } catch {
22179
- return false;
22180
- }
22181
- }
22182
22772
  async function readRequestJson(request) {
22183
22773
  return new Promise((resolve, reject) => {
22184
22774
  const chunks = [];
@@ -22282,6 +22872,7 @@ async function getZhipuWebStatus(store, modelSettings, preferences, options = {}
22282
22872
  function registerZhipuRoutes(ctx, store, modelSettings, preferences, options = {}) {
22283
22873
  const fetchFn = options.fetchFn ?? fetch;
22284
22874
  const readStatus = (quotaError = null) => getZhipuWebStatus(store, modelSettings, preferences, options, quotaError);
22875
+ const quotaRefresh = new QuotaRefresh();
22285
22876
  return ctx.webServer.register({
22286
22877
  kind: "prefix",
22287
22878
  path: ROUTE_PREFIX,
@@ -22295,18 +22886,19 @@ function registerZhipuRoutes(ctx, store, modelSettings, preferences, options = {
22295
22886
  const cached = getCachedQuota$4();
22296
22887
  const accountId = credentials === null ? null : zhipuAccountKey(credentials);
22297
22888
  const stale = cached === void 0 || cached.account.id !== accountId || Date.now() - (cached.fetchedAt || 0) > 12e4;
22298
- let quotaError = null;
22889
+ const accountMatches = cached !== void 0 && cached.account.id === accountId;
22299
22890
  if (credentials !== null && stale) {
22300
- if (cached !== void 0 && cached.account.id !== accountId) clearCachedQuota$4();
22301
- try {
22302
- await fetchAccountQuota$4(credentials, { fetchFn });
22303
- } catch (error) {
22304
- quotaError = error instanceof Error ? error.message : String(error);
22305
- }
22891
+ if (!accountMatches) clearCachedQuota$4();
22892
+ const refresh = () => fetchAccountQuota$4(credentials, { fetchFn });
22893
+ if (accountMatches) quotaRefresh.start(refresh);
22894
+ else await quotaRefresh.run(refresh);
22306
22895
  }
22307
22896
  return sendJson(response, 200, {
22308
22897
  ok: true,
22309
- value: await readStatus(quotaError)
22898
+ value: {
22899
+ ...await readStatus(quotaRefresh.lastError()),
22900
+ quotaRefreshing: quotaRefresh.refreshing
22901
+ }
22310
22902
  });
22311
22903
  }
22312
22904
  /**