@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.
- package/CHANGELOG.md +17 -0
- package/README.md +1 -1
- package/lib/client.js +87 -1
- package/lib/client.js.map +1 -1
- package/lib/index.js +758 -166
- package/lib/types/client/CodexSubscriptionSection.d.ts.map +1 -1
- package/lib/types/client/antigravity/AntigravitySection.d.ts.map +1 -1
- package/lib/types/client/command-code/CommandCodeSection.d.ts.map +1 -1
- package/lib/types/client/common/quota-follow-up.d.ts +30 -0
- package/lib/types/client/common/quota-follow-up.d.ts.map +1 -0
- package/lib/types/client/kimi-code/KimiCodeSection.d.ts.map +1 -1
- package/lib/types/client/workbuddy/WorkBuddySection.d.ts.map +1 -1
- package/lib/types/client/zhipu/ZhipuSection.d.ts.map +1 -1
- package/lib/types/host/antigravity/oauth.d.ts +15 -1
- package/lib/types/host/antigravity/oauth.d.ts.map +1 -1
- package/lib/types/host/antigravity/port-constants.d.ts +20 -0
- package/lib/types/host/antigravity/port-constants.d.ts.map +1 -0
- package/lib/types/host/antigravity/routes.d.ts.map +1 -1
- package/lib/types/host/command-code/client.d.ts +9 -1
- package/lib/types/host/command-code/client.d.ts.map +1 -1
- package/lib/types/host/command-code/routes.d.ts.map +1 -1
- package/lib/types/host/common/catalog-snapshot.d.ts +61 -0
- package/lib/types/host/common/catalog-snapshot.d.ts.map +1 -0
- package/lib/types/host/common/credential-read-cache.d.ts +73 -0
- package/lib/types/host/common/credential-read-cache.d.ts.map +1 -0
- package/lib/types/host/common/quota-refresh.d.ts +35 -0
- package/lib/types/host/common/quota-refresh.d.ts.map +1 -0
- package/lib/types/host/common/same-origin.d.ts +30 -0
- package/lib/types/host/common/same-origin.d.ts.map +1 -0
- package/lib/types/host/credential-store-secret-service.d.ts +1 -0
- package/lib/types/host/credential-store-secret-service.d.ts.map +1 -1
- package/lib/types/host/kimi-code/client.d.ts.map +1 -1
- package/lib/types/host/kimi-code/routes.d.ts.map +1 -1
- package/lib/types/host/routes.d.ts.map +1 -1
- package/lib/types/host/token-store-macos.d.ts +9 -0
- package/lib/types/host/token-store-macos.d.ts.map +1 -1
- package/lib/types/host/token-store-windows.d.ts +8 -0
- package/lib/types/host/token-store-windows.d.ts.map +1 -1
- package/lib/types/host/usage-service.d.ts +19 -1
- package/lib/types/host/usage-service.d.ts.map +1 -1
- package/lib/types/host/workbuddy/client.d.ts +9 -1
- package/lib/types/host/workbuddy/client.d.ts.map +1 -1
- package/lib/types/host/workbuddy/routes.d.ts.map +1 -1
- package/lib/types/host/zhipu/routes.d.ts.map +1 -1
- package/lib/types/shared/antigravity-contracts.d.ts +8 -0
- package/lib/types/shared/antigravity-contracts.d.ts.map +1 -1
- package/lib/types/shared/command-code-contracts.d.ts +8 -0
- package/lib/types/shared/command-code-contracts.d.ts.map +1 -1
- package/lib/types/shared/contracts.d.ts +8 -0
- package/lib/types/shared/contracts.d.ts.map +1 -1
- package/lib/types/shared/kimi-code-contracts.d.ts +8 -0
- package/lib/types/shared/kimi-code-contracts.d.ts.map +1 -1
- package/lib/types/shared/workbuddy-contracts.d.ts +8 -0
- package/lib/types/shared/workbuddy-contracts.d.ts.map +1 -1
- package/lib/types/shared/zhipu-contracts.d.ts +8 -0
- package/lib/types/shared/zhipu-contracts.d.ts.map +1 -1
- package/package.json +32 -33
- package/lib/types/host/compaction-patch.d.ts +0 -17
- 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
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
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
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
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
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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(
|
|
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))
|
|
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:
|
|
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 (
|
|
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)
|
|
9700
|
-
|
|
9701
|
-
|
|
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
|
-
|
|
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))
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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 (
|
|
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 (
|
|
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:
|
|
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
|
-
|
|
16371
|
-
|
|
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
|
-
|
|
16380
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 (
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
22889
|
+
const accountMatches = cached !== void 0 && cached.account.id === accountId;
|
|
22299
22890
|
if (credentials !== null && stale) {
|
|
22300
|
-
if (
|
|
22301
|
-
|
|
22302
|
-
|
|
22303
|
-
|
|
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:
|
|
22898
|
+
value: {
|
|
22899
|
+
...await readStatus(quotaRefresh.lastError()),
|
|
22900
|
+
quotaRefreshing: quotaRefresh.refreshing
|
|
22901
|
+
}
|
|
22310
22902
|
});
|
|
22311
22903
|
}
|
|
22312
22904
|
/**
|