dsh-qoder-connect 0.2.6 → 0.2.7

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/lib/index.d.ts CHANGED
@@ -87,26 +87,56 @@ declare const QODER_GLOBAL_AUTH_PATH = "/plugins/dsh-qoder-connect/global/auth";
87
87
  * One action the PAT route accepts.
88
88
  *
89
89
  * `save-pat` validates the pasted token against the variant's region and, only
90
- * on success, persists it; `clear` removes the stored credential. There is no
91
- * device flow and no credential import: the token is the whole auth surface.
90
+ * on success, appends it to the saved list and makes it active; `activate`
91
+ * switches to another saved entry; `remove` deletes one saved entry; `clear`
92
+ * removes every saved credential. There is no device flow and no credential
93
+ * import: the token is the whole auth surface.
94
+ *
95
+ * All of these are writes — `activate` changes which account signs later
96
+ * requests — so the route requires the in-process key even for the two that do
97
+ * not touch a token's bytes.
92
98
  */
93
- type QoderAuthAction = 'save-pat' | 'clear';
99
+ type QoderAuthAction = 'save-pat' | 'activate' | 'remove' | 'clear';
94
100
  /** Request body accepted by the PAT route. */
95
101
  interface QoderAuthRequest {
96
102
  action: QoderAuthAction;
97
103
  /** The token to validate and store; required for `save-pat`. */
98
104
  pat?: string;
105
+ /** Optional display label for a saved entry; `save-pat` only. */
106
+ label?: string;
107
+ /** Target saved entry; required for `activate` and `remove`. */
108
+ id?: string;
99
109
  }
100
110
  /** Where the credential in effect came from. */
101
111
  type QoderCredentialSource = 'card' | 'env' | 'cli';
102
112
  /**
103
- * Redacted summary of one variant's PAT.
113
+ * One saved PAT, redacted for the switch select.
114
+ *
115
+ * Never the token itself: the id is a one-way hash prefix, the label is
116
+ * display copy, and the tail is the last four characters — enough to tell two
117
+ * saved tokens apart, not enough to misuse one.
118
+ */
119
+ interface QoderPatProfile {
120
+ id: string;
121
+ /** Optional display label supplied when the token was saved. */
122
+ label?: string;
123
+ /** Last four characters of the token. */
124
+ patTail?: string;
125
+ /** When this token was saved, epoch milliseconds. */
126
+ savedAtMs?: number;
127
+ }
128
+ /**
129
+ * Redacted summary of one variant's active PAT.
104
130
  *
105
131
  * Never the token itself: the browser half displays where the credential came
106
132
  * from, when it was saved, and the last four characters — enough to tell two
107
133
  * tokens apart, not enough to misuse one.
108
134
  */
109
135
  interface QoderPatSummary {
136
+ /** Local id of the saved entry in effect; absent for the environment token. */
137
+ id?: string;
138
+ /** Display label of the saved entry in effect, when it carries one. */
139
+ label?: string;
110
140
  source: QoderCredentialSource;
111
141
  /** When the stored token was saved, epoch milliseconds; absent for env fallback. */
112
142
  savedAtMs?: number;
@@ -255,6 +285,16 @@ type QoderWebStatus = {
255
285
  region?: QoderRegion;
256
286
  /** Redacted summary of the PAT in effect. */
257
287
  pat?: QoderPatSummary;
288
+ /**
289
+ * Every saved token for this variant, newest first, redacted.
290
+ *
291
+ * One entry means the card has nothing to switch; two or more render the
292
+ * account select. The list is variant-scoped, like the credential file
293
+ * itself: a token minted for one region is never offered to the other.
294
+ */
295
+ patProfiles?: readonly QoderPatProfile[];
296
+ /** The saved entry in effect; absent when the token comes from the environment. */
297
+ activePatId?: string;
258
298
  credits?: QoderWebCredits;
259
299
  creditsError?: string;
260
300
  /** The models the plugin serves, as catalog snapshots. */
@@ -388,12 +428,27 @@ declare const QODER_AUTH_FILENAME = ".qoder-auth.json";
388
428
  interface AuthLogger {
389
429
  warn?(...args: unknown[]): void;
390
430
  }
431
+ /** One saved credential, as the file stores it. */
432
+ interface QoderCredentialEntry {
433
+ /** Stable local id (a one-way hash prefix of the token). */
434
+ id: string;
435
+ pat: string;
436
+ region: QoderRegion;
437
+ /** When the stored token was saved, epoch milliseconds. */
438
+ savedAtMs?: number;
439
+ /** Optional display label; never sent upstream. */
440
+ label?: string;
441
+ }
391
442
  /** Runtime credential, timestamps in epoch milliseconds. */
392
443
  interface QoderCredential {
444
+ /** Local id of the saved entry; absent for an environment token. */
445
+ id?: string;
393
446
  pat: string;
394
447
  region: QoderRegion;
395
448
  /** When the stored token was saved; absent for environment-sourced tokens. */
396
449
  savedAtMs?: number;
450
+ /** Display label, when the saved entry carries one. */
451
+ label?: string;
397
452
  source: QoderCredentialSource;
398
453
  }
399
454
  /** Read-only sign-in summary for status, the PAT route, and doctor output. */
@@ -407,6 +462,10 @@ interface QoderAuthStatus {
407
462
  reason?: string;
408
463
  /** Absolute path of the credential file this store owns. */
409
464
  filePath: string;
465
+ /** Every saved token for this variant, newest first, redacted. */
466
+ profiles?: readonly QoderPatProfile[];
467
+ /** The entry id in effect, when a saved entry is active. */
468
+ activeId?: string;
410
469
  }
411
470
  /** Constructor options. */
412
471
  interface QoderStoreOptions {
@@ -418,13 +477,21 @@ interface QoderStoreOptions {
418
477
  }
419
478
  /** Last four characters of a token, for display. */
420
479
  declare function patTail(pat: string): string | undefined;
480
+ /**
481
+ * Stable local id for one token.
482
+ *
483
+ * A PAT is a bearer secret, so the id is a one-way hash prefix rather than
484
+ * any suffix of it: surfaces that show the id (the select, status documents,
485
+ * logs) leak nothing usable, while two saves of the same token still resolve
486
+ * to the same entry.
487
+ */
488
+ declare function qoderCredentialId(pat: string): string;
421
489
  /**
422
490
  * Stable identity key for a credential, used by probe records and saved
423
491
  * catalogs.
424
492
  *
425
- * A PAT is a bearer secret, so the key is its one-way hash rather than any
426
- * suffix of it: surfaces that show the key (cards, JSON diagnostics) leak
427
- * nothing usable, while two reads of the same token still correlate locally.
493
+ * Deliberately wider than the local id: these keys address stored data, not
494
+ * UI, and 16 hex characters keep two tokens from ever sharing a bucket.
428
495
  */
429
496
  declare function qoderCredentialIdentity(credential: Pick<QoderCredential, 'pat'>): string;
430
497
  declare class QoderCredentialStore {
@@ -439,10 +506,11 @@ declare class QoderCredentialStore {
439
506
  /**
440
507
  * The credential currently in effect, or `undefined` when there is none.
441
508
  *
442
- * Resolution order: the plugin-owned file, then the arm's environment
443
- * variable (only when no file exists — a saved token always wins over a
444
- * stray env), then nothing. There is no refresh and no expiry check: a PAT
445
- * lives until revoked.
509
+ * Resolution order: the active saved entry (or the first one when the
510
+ * pointer is stale), then the arm's environment variable (only when no
511
+ * saved entry exists — a saved token always wins over a stray env), then
512
+ * nothing. There is no refresh and no expiry check: a PAT lives until
513
+ * revoked.
446
514
  */
447
515
  current(): Promise<QoderCredential | undefined>;
448
516
  /**
@@ -455,12 +523,48 @@ declare class QoderCredentialStore {
455
523
  resolve(): Promise<QoderCredential>;
456
524
  /** The token in effect as a bare string, rejecting when there is none. */
457
525
  patPromise(): Promise<string>;
526
+ /** Every saved entry (tokens included; host-side callers only). */
527
+ entries(): Promise<readonly QoderCredentialEntry[]>;
458
528
  /**
459
- * Persist a token for this variant. The caller is responsible for having
460
- * validated it against the region first (`validateApiKey` upstream).
529
+ * Add a token and make it active.
530
+ *
531
+ * Re-saving an existing token updates that entry (its saved time, and its
532
+ * label when one is supplied) instead of appending a duplicate — the id is
533
+ * derived from the token, so "same PAT" is the same entry by construction.
534
+ * The caller is responsible for having validated the token against the
535
+ * region first (`validateApiKey` upstream).
536
+ *
537
+ * @param pat - the token to store.
538
+ * @param label - optional display label for the select row.
539
+ * @returns the stored entry.
540
+ */
541
+ add(pat: string, label?: string): Promise<QoderCredentialEntry>;
542
+ /**
543
+ * Make one saved entry active.
544
+ *
545
+ * @param id - the entry id.
546
+ * @returns the activated entry, or `undefined` when no entry has that id.
547
+ */
548
+ activate(id: string): Promise<QoderCredentialEntry | undefined>;
549
+ /**
550
+ * Remove one saved entry.
551
+ *
552
+ * The active pointer moves to the first remaining entry; removing the last
553
+ * entry deletes the file, exactly like {@link clear} — a saved token is the
554
+ * only thing that shadows the environment fallback.
555
+ *
556
+ * @param id - the entry id.
557
+ * @returns the removed entry, or `undefined` when no entry has that id.
558
+ */
559
+ remove(id: string): Promise<QoderCredentialEntry | undefined>;
560
+ /**
561
+ * Persist a token for this variant and make it active.
562
+ *
563
+ * Kept for the CLI and older call sites: it is {@link add} behind the
564
+ * runtime-credential shape they expect.
461
565
  */
462
566
  save(pat: string, source?: QoderCredentialSource): Promise<QoderCredential>;
463
- /** Remove the stored credential. A missing file is not an error. */
567
+ /** Remove every stored credential. A missing file is not an error. */
464
568
  clear(): Promise<void>;
465
569
  /** Alias kept for call-site readability (`logout` is just clearing). */
466
570
  logout(): Promise<void>;
@@ -470,7 +574,7 @@ declare class QoderCredentialStore {
470
574
  private pendingLegacyReason;
471
575
  private fromEnvironment;
472
576
  private readOwn;
473
- private writeOwn;
577
+ private writeDocument;
474
578
  }
475
579
  /**
476
580
  * The plugin-owned credential path for one variant's default filename.
@@ -1146,6 +1250,15 @@ declare class QoderProbeStore {
1146
1250
  set(modelId: string, record: QoderProbeRecord): void;
1147
1251
  /** Drop every record; used by the card's explicit "clear" action. */
1148
1252
  clear(): void;
1253
+ /**
1254
+ * Drop every record that belongs to one account, leaving the other saved
1255
+ * accounts' observations intact.
1256
+ *
1257
+ * Used when a saved token is removed: its detected effort levels are that
1258
+ * account's data, and no other account can use them. A plain {@link clear}
1259
+ * here would erase the observations of every account the user still has.
1260
+ */
1261
+ clearFor(account: string): void;
1149
1262
  /** Every record currently held, for status display. */
1150
1263
  all(): Readonly<Record<string, QoderProbeRecord>>;
1151
1264
  /** Build a record stamped with this store's clock, version, and account. */
@@ -1346,22 +1459,27 @@ declare class QoderProbeService {
1346
1459
  }
1347
1460
  //#endregion
1348
1461
  //#region src/auth-route.d.ts
1349
- /** Outcome of a save attempt: stored, or refused with a stable reason. */
1462
+ /** Outcome of a save/activate/remove attempt: stored, or refused with a stable reason. */
1350
1463
  type QoderAuthSaveResult = {
1351
1464
  ok: true;
1352
1465
  status: QoderAuthStatus;
1353
1466
  } | {
1354
1467
  ok: false;
1355
- error: 'qoder_invalid_pat' | 'qoder_missing_pat';
1468
+ error: 'qoder_invalid_pat' | 'qoder_missing_pat' | 'qoder_unknown_pat';
1356
1469
  };
1357
1470
  /** Constructor dependencies. */
1358
1471
  interface QoderAuthRouteOptions {
1359
1472
  /**
1360
1473
  * Validate one pasted token against this variant's region and, only on
1361
- * success, persist it. The route never decides validity itself.
1474
+ * success, persist it and make it active. The route never decides validity
1475
+ * itself.
1362
1476
  */
1363
- save: (pat: string) => Promise<QoderAuthSaveResult>;
1364
- /** Remove the stored credential. */
1477
+ save: (pat: string, label?: string) => Promise<QoderAuthSaveResult>;
1478
+ /** Switch to one already-saved token. */
1479
+ activate: (id: string) => Promise<QoderAuthSaveResult>;
1480
+ /** Remove one saved token; the host moves the active pointer. */
1481
+ remove: (id: string) => Promise<QoderAuthSaveResult>;
1482
+ /** Remove every saved token (`logout`). */
1365
1483
  clear: () => Promise<void>;
1366
1484
  /**
1367
1485
  * Route path to mount. Defaults to the China variant's path so callers that
@@ -1375,7 +1493,7 @@ declare function createAuthKey(): string;
1375
1493
  * The PAT route's handler, extracted so tests can mount it on a bare server
1376
1494
  * with a known key.
1377
1495
  *
1378
- * @param deps - the save/clear operations for one variant.
1496
+ * @param deps - the save/activate/remove/clear operations for one variant.
1379
1497
  * @param key - the in-process control key this route requires.
1380
1498
  * @returns the Node request handler.
1381
1499
  */
@@ -1568,4 +1686,4 @@ declare const QUOTA_SECTION_KEYS: readonly ["sidebarQuotaCN", "sidebarQuotaGloba
1568
1686
  */
1569
1687
  declare function apply(ctx: Context, config: Config): void;
1570
1688
  //#endregion
1571
- export { CHINA_VARIANT, CN_SECTION_KEYS, Config, FALLBACK_QODER_MODELS, GLOBAL_SECTION_KEYS, GLOBAL_VARIANT, KIND_STATUS, PROBE_EFFORT_CANDIDATES, type ProbeAttempt, type ProbeOutcome, type ProbeSender, QODER_AUTH_FILENAME, QODER_AUTH_PATH, QODER_DATA_DIR_ENV, QODER_DATA_DIR_NAME, QODER_GLOBAL_AUTH_PATH, QODER_GLOBAL_PROBE_PATH, QODER_GLOBAL_SETTINGS_NS, QODER_GLOBAL_STATUS_PATH, QODER_HOST_HEARTBEAT_FILENAME, QODER_PAT_ENV_CN, QODER_PAT_ENV_GLOBAL, QODER_PROBE_FILENAME, QODER_PROBE_PATH, QODER_PROVIDER, QODER_QUOTA_SETTINGS_NS, QODER_SETTINGS_NS, QODER_STATUS_PATH, QODER_STREAM_IDLE_TIMEOUT_MS, QODER_VARIANTS, QUOTA_POLL_DEFAULT_MS, QUOTA_POLL_MIN_MS, QUOTA_SECTION_KEYS, type QoderAdapter, type QoderAuthRequest, type QoderAuthRouteOptions, type QoderAuthSaveResult, type QoderAuthStatus, QoderCatalog, type QoderCatalogFetch, type QoderCatalogModelSnapshot, QoderCatalogStore, type QoderCatalogStoreOptions, type QoderChatResult, type QoderCredential, QoderCredentialStore, type QoderCreditAccount, type QoderCredits, type QoderEffort, type QoderHostHeartbeat, type QoderModelBilling, type QoderModelInfo, type QoderModelReasoning, type QoderPatSummary, type QoderProbeAction, type QoderProbeRecord, QoderProbeService, type QoderProbeStatus, QoderProbeStore, type QoderProbeValidation, type QoderShim, QoderUpstreamClient, type QoderVariant, type QoderVariantId, type QoderWebCatalog, type QoderWebCreditAccount, type QoderWebCredits, type QoderWebProbeModel, type QoderWebProbeSection, type QoderWebStatus, type ShimLogger, type UpstreamErrorKind, apply, classifyUpstreamError, clearHostHeartbeat, createAuthKey, createQoderAdapter, createQoderShim, fingerprintModel, inject, isHeartbeatProcessAlive, kindFromQoderFailure, modelInfoOf, name, normalizeCredits, patTail, probeModel, processStartTimeMs, qoderAuthHandler, qoderCatalogPath, qoderCredentialIdentity, qoderHostHeartbeatPath, qoderOwnAuthPath, qoderPluginDataDir, qoderProbePath, randomSentinel, readHostHeartbeat, registerQoderAuthRoute, variantFor };
1689
+ export { CHINA_VARIANT, CN_SECTION_KEYS, Config, FALLBACK_QODER_MODELS, GLOBAL_SECTION_KEYS, GLOBAL_VARIANT, KIND_STATUS, PROBE_EFFORT_CANDIDATES, type ProbeAttempt, type ProbeOutcome, type ProbeSender, QODER_AUTH_FILENAME, QODER_AUTH_PATH, QODER_DATA_DIR_ENV, QODER_DATA_DIR_NAME, QODER_GLOBAL_AUTH_PATH, QODER_GLOBAL_PROBE_PATH, QODER_GLOBAL_SETTINGS_NS, QODER_GLOBAL_STATUS_PATH, QODER_HOST_HEARTBEAT_FILENAME, QODER_PAT_ENV_CN, QODER_PAT_ENV_GLOBAL, QODER_PROBE_FILENAME, QODER_PROBE_PATH, QODER_PROVIDER, QODER_QUOTA_SETTINGS_NS, QODER_SETTINGS_NS, QODER_STATUS_PATH, QODER_STREAM_IDLE_TIMEOUT_MS, QODER_VARIANTS, QUOTA_POLL_DEFAULT_MS, QUOTA_POLL_MIN_MS, QUOTA_SECTION_KEYS, type QoderAdapter, type QoderAuthRequest, type QoderAuthRouteOptions, type QoderAuthSaveResult, type QoderAuthStatus, QoderCatalog, type QoderCatalogFetch, type QoderCatalogModelSnapshot, QoderCatalogStore, type QoderCatalogStoreOptions, type QoderChatResult, type QoderCredential, type QoderCredentialEntry, QoderCredentialStore, type QoderCreditAccount, type QoderCredits, type QoderEffort, type QoderHostHeartbeat, type QoderModelBilling, type QoderModelInfo, type QoderModelReasoning, type QoderPatProfile, type QoderPatSummary, type QoderProbeAction, type QoderProbeRecord, QoderProbeService, type QoderProbeStatus, QoderProbeStore, type QoderProbeValidation, type QoderShim, QoderUpstreamClient, type QoderVariant, type QoderVariantId, type QoderWebCatalog, type QoderWebCreditAccount, type QoderWebCredits, type QoderWebProbeModel, type QoderWebProbeSection, type QoderWebStatus, type ShimLogger, type UpstreamErrorKind, apply, classifyUpstreamError, clearHostHeartbeat, createAuthKey, createQoderAdapter, createQoderShim, fingerprintModel, inject, isHeartbeatProcessAlive, kindFromQoderFailure, modelInfoOf, name, normalizeCredits, patTail, probeModel, processStartTimeMs, qoderAuthHandler, qoderCatalogPath, qoderCredentialId, qoderCredentialIdentity, qoderHostHeartbeatPath, qoderOwnAuthPath, qoderPluginDataDir, qoderProbePath, randomSentinel, readHostHeartbeat, registerQoderAuthRoute, variantFor };
package/lib/index.js CHANGED
@@ -1,7 +1,7 @@
1
- import { A as QoderCatalog, B as QODER_PAT_ENV_GLOBAL, C as getMachineId, D as QoderCatalogStore, E as randomSentinel, F as QODER_PROBE_PATH, G as QODER_DATA_DIR_ENV, H as patTail, I as QODER_SETTINGS_FACE_PATH, J as qoderMachineIdPath, K as QODER_DATA_DIR_NAME, L as QODER_STATUS_PATH, M as QODER_GLOBAL_AUTH_PATH, N as QODER_GLOBAL_PROBE_PATH, O as qoderCatalogPath, P as QODER_GLOBAL_STATUS_PATH, R as QODER_AUTH_FILENAME, S as utc8WallClock, T as probeModel, U as qoderCredentialIdentity, V as QoderCredentialStore, W as qoderOwnAuthPath, X as qoderStateDir, Y as qoderPluginDataDir, _ as modelInfoOf, a as QODER_HOST_HEARTBEAT_FILENAME, b as createQoderTransport, c as processStartTimeMs, d as writeHostHeartbeat, f as QODER_CONNECT_VERSION, g as kindFromQoderFailure, h as classifyUpstreamError, i as variantFor, j as QODER_AUTH_PATH, k as FALLBACK_QODER_MODELS, l as qoderHostHeartbeatPath, m as QoderUpstreamClient, n as GLOBAL_VARIANT, o as clearHostHeartbeat, p as KIND_STATUS, q as configurePluginPaths, r as QODER_VARIANTS, s as isHeartbeatProcessAlive, t as CHINA_VARIANT, u as readHostHeartbeat, v as normalizeCredits, w as PROBE_EFFORT_CANDIDATES, x as formatUtc8Date, y as validateApiKey, z as QODER_PAT_ENV_CN } from "./variants-COxX6r1j.js";
1
+ import { $ as configurePluginPaths, A as probeModel, B as QODER_PROBE_PATH, C as normalizeCredits, D as utc8WallClock, E as formatUtc8Date, F as QoderCatalog, G as QODER_PAT_ENV_GLOBAL, H as QODER_STATUS_PATH, I as QODER_AUTH_PATH, J as qoderCredentialId, K as QoderCredentialStore, L as QODER_GLOBAL_AUTH_PATH, M as QoderCatalogStore, N as qoderCatalogPath, O as getMachineId, P as FALLBACK_QODER_MODELS, Q as QODER_DATA_DIR_NAME, R as QODER_GLOBAL_PROBE_PATH, S as modelInfoOf, T as createQoderTransport, U as QODER_AUTH_FILENAME, V as QODER_SETTINGS_FACE_PATH, W as QODER_PAT_ENV_CN, X as qoderOwnAuthPath, Y as qoderCredentialIdentity, Z as QODER_DATA_DIR_ENV, _ as qoderProbePath, a as QODER_HOST_HEARTBEAT_FILENAME, b as classifyUpstreamError, c as processStartTimeMs, d as writeHostHeartbeat, et as qoderMachineIdPath, f as QODER_CONNECT_VERSION, g as newestFirst, h as fingerprintModel, i as variantFor, j as randomSentinel, k as PROBE_EFFORT_CANDIDATES, l as qoderHostHeartbeatPath, m as QoderProbeStore, n as GLOBAL_VARIANT, o as clearHostHeartbeat, p as QODER_PROBE_FILENAME, q as patTail, r as QODER_VARIANTS, s as isHeartbeatProcessAlive, t as CHINA_VARIANT, tt as qoderPluginDataDir, u as readHostHeartbeat, v as KIND_STATUS, w as validateApiKey, x as kindFromQoderFailure, y as QoderUpstreamClient, z as QODER_GLOBAL_STATUS_PATH } from "./variants-B_ZzENbC.js";
2
2
  import z from "@deepseek-ai/schemastery";
3
- import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
4
- import { dirname, join, resolve } from "node:path";
3
+ import { randomBytes, timingSafeEqual } from "node:crypto";
4
+ import { dirname, join } from "node:path";
5
5
  import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
6
6
  import { createProvider } from "@earendil-works/pi-ai";
7
7
  import { openAICompletionsApi } from "@earendil-works/pi-ai/api/openai-completions.lazy";
@@ -60,7 +60,8 @@ function originIsLoopback(origin) {
60
60
  //#endregion
61
61
  //#region src/auth-route.ts
62
62
  /**
63
- * PAT route: saves (validate-then-persist) or clears one variant's token.
63
+ * PAT route: saves (validate-then-persist), switches, removes, or clears the
64
+ * tokens of one variant.
64
65
  *
65
66
  * The state-changing endpoints the plugin exposes share one guard shape — a
66
67
  * loopback Host and Origin, plus the in-process key the card receives with its
@@ -111,9 +112,20 @@ function parseRequest(text) {
111
112
  if (action === "save-pat") {
112
113
  const pat = wrapped["pat"];
113
114
  if (typeof pat !== "string" || pat.trim() === "") return void 0;
115
+ const label = wrapped["label"];
116
+ if (label !== void 0 && (typeof label !== "string" || label.length > 200)) return void 0;
114
117
  return {
115
118
  action,
116
- pat
119
+ pat,
120
+ ...label === void 0 ? {} : { label }
121
+ };
122
+ }
123
+ if (action === "activate" || action === "remove") {
124
+ const id = wrapped["id"];
125
+ if (typeof id !== "string" || id.trim() === "") return void 0;
126
+ return {
127
+ action,
128
+ id: id.trim()
117
129
  };
118
130
  }
119
131
  }
@@ -143,7 +155,7 @@ function keyMatches$1(expected, presented) {
143
155
  * The PAT route's handler, extracted so tests can mount it on a bare server
144
156
  * with a known key.
145
157
  *
146
- * @param deps - the save/clear operations for one variant.
158
+ * @param deps - the save/activate/remove/clear operations for one variant.
147
159
  * @param key - the in-process control key this route requires.
148
160
  * @returns the Node request handler.
149
161
  */
@@ -177,7 +189,15 @@ function qoderAuthHandler(deps, key) {
177
189
  json$2(res, 200, { ok: true });
178
190
  return;
179
191
  }
180
- json$2(res, 200, await deps.save(request.pat));
192
+ if (request.action === "activate") {
193
+ json$2(res, 200, await deps.activate(request.id));
194
+ return;
195
+ }
196
+ if (request.action === "remove") {
197
+ json$2(res, 200, await deps.remove(request.id));
198
+ return;
199
+ }
200
+ json$2(res, 200, await deps.save(request.pat, request.label));
181
201
  } catch (error) {
182
202
  json$2(res, 200, {
183
203
  ok: false,
@@ -683,203 +703,6 @@ function createQoderShim(options) {
683
703
  };
684
704
  }
685
705
  //#endregion
686
- //#region src/probe-store.ts
687
- /**
688
- * Local record of reasoning-effort probes.
689
- *
690
- * What this stores is an *observation*, never a claim about the upstream: a
691
- * model's row is only consulted when the catalog carries no explicit
692
- * `supportedEfforts` set, and it always loses to a declared set. The plan this
693
- * implements (`docs/reasoning-effort-probe-plan.md` §5) requires that a result
694
- * is invalidated whenever the model's catalog row changes, so every record
695
- * carries a fingerprint of the fields the probe depended on.
696
- *
697
- * The file lives in the plugin's own state directory, never beside the
698
- * product's files, and carries no token, prompt, or response body — only
699
- * model ids, effort spellings, and timestamps.
700
- *
701
- * @module dsh-qoder-connect/probe-store
702
- */
703
- /** Basename of the China variant's probe record inside the plugin's state dir. */
704
- const QODER_PROBE_FILENAME = ".qoder-probe.json";
705
- /** On-disk format this reader accepts; other versions are discarded. */
706
- const PROBE_FORMAT_VERSION = 1;
707
- /**
708
- * How long an observation stays usable. Conservative on purpose: the plan's
709
- * whole argument is that upstream metadata moves fast, so a result that has
710
- * outlived its fingerprint's usefulness should not quietly keep granting a
711
- * picker entry.
712
- */
713
- const DEFAULT_TTL_MS = 12096e5;
714
- /**
715
- * Plugin-owned probe record path inside the plugin's state directory.
716
- *
717
- * One file per variant. The two regions serve overlapping model ids with
718
- * different entitlements, and {@link fingerprintModel} covers only
719
- * `id`/`reasoning`/`supportsImages` — never the provider — so a single shared
720
- * file would let one variant's observation answer for the other. The paths
721
- * differ; the format does not.
722
- */
723
- function qoderProbePath(filename = QODER_PROBE_FILENAME) {
724
- return join(qoderStateDir(), filename);
725
- }
726
- /**
727
- * Fingerprint the catalog fields a probe depends on.
728
- *
729
- * Deliberately excludes display-only fields (`name`, `billing`, `contextWindow`)
730
- * so a rename or a rate relabel does not throw away a valid observation, and
731
- * deliberately includes the whole reasoning object so any change to the
732
- * declared shape re-probes.
733
- */
734
- function fingerprintModel(info) {
735
- const basis = JSON.stringify({
736
- id: info.id,
737
- reasoning: info.reasoning ?? null,
738
- supportsImages: info.supportsImages ?? null
739
- });
740
- return createHash("sha256").update(basis).digest("hex").slice(0, 16);
741
- }
742
- /** Read-and-validate the documents on disk; anything malformed reads as empty. */
743
- function readDocument(path) {
744
- if (!existsSync(path)) return void 0;
745
- let parsed;
746
- try {
747
- parsed = JSON.parse(readFileSync(path, "utf8"));
748
- } catch {
749
- return;
750
- }
751
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return void 0;
752
- const wrapped = parsed;
753
- if (wrapped["version"] !== PROBE_FORMAT_VERSION) return void 0;
754
- const records = wrapped["records"];
755
- if (typeof records !== "object" || records === null || Array.isArray(records)) return void 0;
756
- return parsed;
757
- }
758
- /** One record's shape check; a bad row is dropped rather than trusted. */
759
- function isRecord(value) {
760
- if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
761
- const wrapped = value;
762
- const validation = wrapped["validation"];
763
- if (validation !== "validating" && validation !== "non-validating" && validation !== "unknown") return false;
764
- if (typeof wrapped["fingerprint"] !== "string") return false;
765
- if (typeof wrapped["probedAtMs"] !== "number" || !Number.isFinite(wrapped["probedAtMs"])) return false;
766
- if (typeof wrapped["pluginVersion"] !== "string") return false;
767
- const efforts = wrapped["efforts"];
768
- if (!Array.isArray(efforts) || efforts.some((effort) => typeof effort !== "string")) return false;
769
- return true;
770
- }
771
- /**
772
- * The plugin's probe records: read once, written atomically, never trusted
773
- * across a fingerprint change or past the TTL.
774
- */
775
- var QoderProbeStore = class {
776
- path;
777
- ttlMs;
778
- pluginVersion;
779
- now;
780
- records;
781
- constructor(options) {
782
- const opts = typeof options === "string" ? {
783
- path: options,
784
- pluginVersion: "0.0.0"
785
- } : options;
786
- this.path = opts.path ?? qoderProbePath();
787
- this.ttlMs = opts.ttlMs ?? DEFAULT_TTL_MS;
788
- this.pluginVersion = opts.pluginVersion;
789
- this.now = opts.now ?? (() => Date.now());
790
- }
791
- /** Resolved state-file path, for the CLI and tests. */
792
- filePath() {
793
- return this.path;
794
- }
795
- load() {
796
- if (this.records === void 0) {
797
- const document = readDocument(this.path);
798
- const records = {};
799
- for (const [id, record] of Object.entries(document?.records ?? {})) if (isRecord(record)) records[id] = record;
800
- this.records = records;
801
- }
802
- return this.records;
803
- }
804
- /**
805
- * The usable record for a model, or `undefined` when there is none, it is
806
- * expired, it was taken against a different catalog row, or it belongs to a
807
- * different account.
808
- *
809
- * @param account - the account in effect, as `uid:enterpriseId`. Records are
810
- * only returned for the account that produced them.
811
- */
812
- get(modelId, fingerprint, account) {
813
- const record = this.load()[modelId];
814
- if (record === void 0) return void 0;
815
- if (record.fingerprint !== fingerprint) return void 0;
816
- if (record.account !== account) return void 0;
817
- if (this.now() - record.probedAtMs > this.ttlMs) return void 0;
818
- return record;
819
- }
820
- /**
821
- * Store one observation. Only a decisive answer (`validating` /
822
- * `non-validating`) replaces an existing decisive record: a transient
823
- * `unknown` must not erase knowledge the user already paid for.
824
- */
825
- set(modelId, record) {
826
- const records = this.load();
827
- const existing = records[modelId];
828
- if (record.validation === "unknown" && existing !== void 0 && existing.fingerprint === record.fingerprint && existing.validation !== "unknown") return;
829
- records[modelId] = record;
830
- this.persist();
831
- }
832
- /** Drop every record; used by the card's explicit "clear" action. */
833
- clear() {
834
- this.records = {};
835
- this.persist();
836
- }
837
- /** Every record currently held, for status display. */
838
- all() {
839
- return { ...this.load() };
840
- }
841
- /** Build a record stamped with this store's clock, version, and account. */
842
- record(fingerprint, validation, efforts, account) {
843
- return {
844
- fingerprint,
845
- validation,
846
- efforts: validation === "validating" ? [...efforts] : [],
847
- probedAtMs: this.now(),
848
- pluginVersion: this.pluginVersion,
849
- account
850
- };
851
- }
852
- /**
853
- * Write through a temporary file and rename, so a crash mid-write cannot
854
- * leave a half-parsed document that reads as "no records" and silently drops
855
- * every observation.
856
- */
857
- persist() {
858
- const directory = dirname(this.path);
859
- try {
860
- if (!existsSync(directory)) mkdirSync(directory, { recursive: true });
861
- const document = {
862
- version: PROBE_FORMAT_VERSION,
863
- records: this.load()
864
- };
865
- const temporary = resolve(`${this.path}.tmp`);
866
- writeFileSync(temporary, `${JSON.stringify(document, null, 2)}\n`, { mode: 384 });
867
- renameSync(temporary, this.path);
868
- } catch {}
869
- }
870
- };
871
- /**
872
- * Order observations newest-first for display.
873
- *
874
- * The store keeps insertion order so the file reads chronologically, but the
875
- * card wants the most recent detection at the top: a sweep the user just ran
876
- * should not appear below every earlier one, which is what appending to an
877
- * insertion-ordered list does.
878
- */
879
- function newestFirst(records) {
880
- return [...records].sort((a, b) => b.probedAt - a.probedAt);
881
- }
882
- //#endregion
883
706
  //#region src/probe-service.ts
884
707
  /**
885
708
  * Serial probe runner. One instance is shared by the manual API and any
@@ -1070,6 +893,8 @@ async function qoderWebStatus(deps) {
1070
893
  status: "signed-in",
1071
894
  ...authStatus.region === void 0 ? {} : { region: authStatus.region },
1072
895
  ...authStatus.pat === void 0 ? {} : { pat: authStatus.pat },
896
+ ...authStatus.profiles === void 0 ? {} : { patProfiles: authStatus.profiles },
897
+ ...authStatus.activeId === void 0 ? {} : { activePatId: authStatus.activeId },
1073
898
  ...deps.authKey === void 0 ? {} : { authKey: deps.authKey }
1074
899
  };
1075
900
  const modelsField = deps.models().map(modelSnapshot);
@@ -2687,10 +2512,7 @@ function apply(ctx, config) {
2687
2512
  runtime.catalogGeneration += 1;
2688
2513
  runtime.inflightFetch?.controller.abort();
2689
2514
  runtime.inflightFetch = void 0;
2690
- if (hadCredential && known !== identity) {
2691
- runtime.probeStore.clear();
2692
- runtime.invalidate();
2693
- }
2515
+ if (hadCredential && known !== identity) runtime.invalidate();
2694
2516
  if (identity === void 0) {
2695
2517
  if (known !== void 0) runtime.savedCatalogs.delete(known);
2696
2518
  runtime.catalog.set(runtime.fallback);
@@ -2765,13 +2587,13 @@ function apply(ctx, config) {
2765
2587
  });
2766
2588
  registerQoderAuthRoute(webCtx, {
2767
2589
  path: runtime.variant.authPath,
2768
- save: async (pat) => {
2590
+ save: async (pat, label) => {
2769
2591
  if (!await validateApiKey(pat, runtime.variant.region)) return {
2770
2592
  ok: false,
2771
2593
  error: "qoder_invalid_pat"
2772
2594
  };
2773
- const credential = await runtime.store.save(pat);
2774
- const identity = credentialIdentity(credential);
2595
+ const entry = await runtime.store.add(pat, label);
2596
+ const identity = credentialIdentity(entry);
2775
2597
  adoptIdentity(runtime, identity);
2776
2598
  await fetchCatalog(runtime, identity);
2777
2599
  return {
@@ -2779,7 +2601,45 @@ function apply(ctx, config) {
2779
2601
  status: await runtime.store.status()
2780
2602
  };
2781
2603
  },
2604
+ activate: async (id) => {
2605
+ const entry = await runtime.store.activate(id);
2606
+ if (entry === void 0) return {
2607
+ ok: false,
2608
+ error: "qoder_unknown_pat"
2609
+ };
2610
+ const identity = credentialIdentity(entry);
2611
+ adoptIdentity(runtime, identity);
2612
+ detach(ctx, fetchCatalog(runtime, identity), `${runtime.variant.id} catalog refresh after switch`);
2613
+ return {
2614
+ ok: true,
2615
+ status: await runtime.store.status()
2616
+ };
2617
+ },
2618
+ remove: async (id) => {
2619
+ const removed = await runtime.store.remove(id);
2620
+ if (removed === void 0) return {
2621
+ ok: false,
2622
+ error: "qoder_unknown_pat"
2623
+ };
2624
+ const finished = credentialIdentity(removed);
2625
+ runtime.probeStore.clearFor(finished);
2626
+ runtime.savedCatalogs.delete(finished);
2627
+ const active = await runtime.store.current().catch(() => void 0);
2628
+ const identity = active === void 0 ? void 0 : credentialIdentity(active);
2629
+ adoptIdentity(runtime, identity);
2630
+ if (identity !== void 0) detach(ctx, fetchCatalog(runtime, identity), `${runtime.variant.id} catalog refresh after removal`);
2631
+ return {
2632
+ ok: true,
2633
+ status: await runtime.store.status()
2634
+ };
2635
+ },
2782
2636
  clear: async () => {
2637
+ const saved = await runtime.store.entries().catch(() => []);
2638
+ for (const entry of saved) {
2639
+ const identity = credentialIdentity(entry);
2640
+ runtime.probeStore.clearFor(identity);
2641
+ runtime.savedCatalogs.delete(identity);
2642
+ }
2783
2643
  await runtime.store.clear();
2784
2644
  adoptIdentity(runtime, void 0);
2785
2645
  }
@@ -3097,4 +2957,4 @@ function apply(ctx, config) {
3097
2957
  }), "variant startup");
3098
2958
  }
3099
2959
  //#endregion
3100
- export { CHINA_VARIANT, CN_SECTION_KEYS, Config, FALLBACK_QODER_MODELS, GLOBAL_SECTION_KEYS, GLOBAL_VARIANT, KIND_STATUS, PROBE_EFFORT_CANDIDATES, QODER_AUTH_FILENAME, QODER_AUTH_PATH, QODER_DATA_DIR_ENV, QODER_DATA_DIR_NAME, QODER_GLOBAL_AUTH_PATH, QODER_GLOBAL_PROBE_PATH, QODER_GLOBAL_SETTINGS_NS, QODER_GLOBAL_STATUS_PATH, QODER_HOST_HEARTBEAT_FILENAME, QODER_PAT_ENV_CN, QODER_PAT_ENV_GLOBAL, QODER_PROBE_FILENAME, QODER_PROBE_PATH, QODER_PROVIDER, QODER_QUOTA_SETTINGS_NS, QODER_SETTINGS_NS, QODER_STATUS_PATH, QODER_STREAM_IDLE_TIMEOUT_MS, QODER_VARIANTS, QUOTA_POLL_DEFAULT_MS, QUOTA_POLL_MIN_MS, QUOTA_SECTION_KEYS, QoderCatalog, QoderCatalogStore, QoderCredentialStore, QoderProbeService, QoderProbeStore, QoderUpstreamClient, apply, classifyUpstreamError, clearHostHeartbeat, createAuthKey, createQoderAdapter, createQoderShim, fingerprintModel, inject, isHeartbeatProcessAlive, kindFromQoderFailure, modelInfoOf, name, normalizeCredits, patTail, probeModel, processStartTimeMs, qoderAuthHandler, qoderCatalogPath, qoderCredentialIdentity, qoderHostHeartbeatPath, qoderOwnAuthPath, qoderPluginDataDir, qoderProbePath, randomSentinel, readHostHeartbeat, registerQoderAuthRoute, variantFor };
2960
+ export { CHINA_VARIANT, CN_SECTION_KEYS, Config, FALLBACK_QODER_MODELS, GLOBAL_SECTION_KEYS, GLOBAL_VARIANT, KIND_STATUS, PROBE_EFFORT_CANDIDATES, QODER_AUTH_FILENAME, QODER_AUTH_PATH, QODER_DATA_DIR_ENV, QODER_DATA_DIR_NAME, QODER_GLOBAL_AUTH_PATH, QODER_GLOBAL_PROBE_PATH, QODER_GLOBAL_SETTINGS_NS, QODER_GLOBAL_STATUS_PATH, QODER_HOST_HEARTBEAT_FILENAME, QODER_PAT_ENV_CN, QODER_PAT_ENV_GLOBAL, QODER_PROBE_FILENAME, QODER_PROBE_PATH, QODER_PROVIDER, QODER_QUOTA_SETTINGS_NS, QODER_SETTINGS_NS, QODER_STATUS_PATH, QODER_STREAM_IDLE_TIMEOUT_MS, QODER_VARIANTS, QUOTA_POLL_DEFAULT_MS, QUOTA_POLL_MIN_MS, QUOTA_SECTION_KEYS, QoderCatalog, QoderCatalogStore, QoderCredentialStore, QoderProbeService, QoderProbeStore, QoderUpstreamClient, apply, classifyUpstreamError, clearHostHeartbeat, createAuthKey, createQoderAdapter, createQoderShim, fingerprintModel, inject, isHeartbeatProcessAlive, kindFromQoderFailure, modelInfoOf, name, normalizeCredits, patTail, probeModel, processStartTimeMs, qoderAuthHandler, qoderCatalogPath, qoderCredentialId, qoderCredentialIdentity, qoderHostHeartbeatPath, qoderOwnAuthPath, qoderPluginDataDir, qoderProbePath, randomSentinel, readHostHeartbeat, registerQoderAuthRoute, variantFor };