dsh-connect-workbuddy 4.0.0 → 4.2.0

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.js CHANGED
@@ -1,4 +1,4 @@
1
- import { $ as macosNestedAppBundles, A as workbuddyOwnAuthPath, B as parseUpstreamModel, C as defaultDesktopAuthDirs, D as legacyWorkbuddyOwnAuthPath, E as isEncryptedCredentialError, F as classifyUpstreamError, G as deriveAtRestKey, H as regionOf, I as declaredTools, J as findWorkbuddyAppExecutable, K as deriveAtRestKeyId, L as isCredentialRejectedError, M as WORKBUDDY_FALLBACK_SYSTEM_PROMPT, N as WorkBuddyCredentialRejectedError, O as parseWorkBuddyAuth, P as WorkBuddyUpstreamClient, Q as macosBundleExecutable, R as parseCreditMultiplier, S as defaultDesktopAuthCandidates, T as hasEncryptedCredentialFields, U as WORKBUDDY_APP_EXECUTABLE_ENV, V as prepareChatBody, W as clearAtRestKeyCache, X as isEncryptedFieldWrapper, Z as isWorkbuddyBundle, _ as WORKBUDDY_AUTH_FILENAME, a as readHostHeartbeat, b as WorkBuddyEncryptedCredentialError, c as WORKBUDDY_CONNECT_VERSION, d as FALLBACK_WORKBUDDY_MODELS_GLOBAL, et as openEncryptedField, f as WorkBuddyCatalog, g as ENCRYPTED_CREDENTIAL_CODE, h as fallbackModelsFor, i as processStartTimeMs, j as CREDENTIAL_REJECTED_CODE, k as workbuddyAccountId, l as piAiRuntimeInfo, m as deriveCatalog, n as clearHostHeartbeat, nt as workbuddyAppExecutableCandidates, o as workbuddyHostHeartbeatPath, p as applyContextBudgets, q as fetchAtRestKeyPayload, r as isHeartbeatProcessAlive, s as writeHostHeartbeat, t as WORKBUDDY_HOST_HEARTBEAT_FILENAME, tt as readAtRestKey, u as FALLBACK_WORKBUDDY_MODELS, v as WORKBUDDY_AUTH_FILE_ENV, w as defaultDesktopAuthPath, x as authFileName, y as WorkBuddyCredentialStore, z as parseReasoning } from "./host-heartbeat-vvVjdY_q.js";
1
+ import { $ as isWorkbuddyBundle, A as workbuddyAccountId, B as parseCreditMultiplier, C as defaultDesktopAuthDirs, D as isVaultAccountId, E as isEncryptedCredentialError, F as WorkBuddyUpstreamClient, G as WORKBUDDY_APP_EXECUTABLE_ENV, H as parseUpstreamModel, I as classifyUpstreamError, J as deriveAtRestKeyId, K as clearAtRestKeyCache, L as declaredTools, M as CREDENTIAL_REJECTED_CODE, N as WORKBUDDY_FALLBACK_SYSTEM_PROMPT, O as legacyWorkbuddyOwnAuthPath, P as WorkBuddyCredentialRejectedError, Q as isEncryptedFieldWrapper, R as isCredentialRejectedError, S as defaultDesktopAuthCandidates, T as hasEncryptedCredentialFields, U as prepareChatBody, V as parseReasoning, W as regionOf, X as findWorkbuddyAppExecutable, Y as fetchAtRestKeyPayload, _ as WORKBUDDY_AUTH_FILENAME, a as readHostHeartbeat, b as WorkBuddyEncryptedCredentialError, c as WORKBUDDY_CONNECT_VERSION, d as FALLBACK_WORKBUDDY_MODELS_GLOBAL, et as macosBundleExecutable, f as WorkBuddyCatalog, g as ENCRYPTED_CREDENTIAL_CODE, h as fallbackModelsFor, i as processStartTimeMs, it as workbuddyAppExecutableCandidates, j as workbuddyOwnAuthPath, k as parseWorkBuddyAuth, l as piAiRuntimeInfo, m as deriveCatalog, n as clearHostHeartbeat, nt as openEncryptedField, o as workbuddyHostHeartbeatPath, p as applyContextBudgets, q as deriveAtRestKey, r as isHeartbeatProcessAlive, rt as readAtRestKey, s as writeHostHeartbeat, t as WORKBUDDY_HOST_HEARTBEAT_FILENAME, tt as macosNestedAppBundles, u as FALLBACK_WORKBUDDY_MODELS, v as WORKBUDDY_AUTH_FILE_ENV, w as defaultDesktopAuthPath, x as authFileName, y as WorkBuddyCredentialStore, z as knownRegionOf } from "./host-heartbeat-C4TXu8ew.js";
2
2
  import z from "@deepseek-ai/schemastery";
3
3
  import { readFile } from "node:fs/promises";
4
4
  import { randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
@@ -2337,7 +2337,7 @@ async function checkinOne(target, deps) {
2337
2337
  return {
2338
2338
  ...base,
2339
2339
  status: "failed",
2340
- message: messageOf(error)
2340
+ message: messageOf$1(error)
2341
2341
  };
2342
2342
  }
2343
2343
  if (credential === void 0) return {
@@ -2367,7 +2367,7 @@ async function checkinOne(target, deps) {
2367
2367
  return {
2368
2368
  ...base,
2369
2369
  status: "failed",
2370
- message: messageOf(error)
2370
+ message: messageOf$1(error)
2371
2371
  };
2372
2372
  }
2373
2373
  }
@@ -2413,7 +2413,7 @@ async function testOne(target, modelId, deps) {
2413
2413
  result: {
2414
2414
  modelId,
2415
2415
  outcome: "credential-rejected",
2416
- message: messageOf(error)
2416
+ message: messageOf$1(error)
2417
2417
  }
2418
2418
  };
2419
2419
  }
@@ -2477,7 +2477,7 @@ function defaultWait(ms) {
2477
2477
  return new Promise((resolve) => setTimeout(resolve, ms));
2478
2478
  }
2479
2479
  /** A safe message from an unknown throwable, bounded in length. */
2480
- function messageOf(error) {
2480
+ function messageOf$1(error) {
2481
2481
  return (error instanceof Error ? error.message : String(error)).slice(0, 300);
2482
2482
  }
2483
2483
  /**
@@ -2689,6 +2689,469 @@ function resolveTargetModel(catalog, preferredId) {
2689
2689
  };
2690
2690
  }
2691
2691
  //#endregion
2692
+ //#region src/oauth.ts
2693
+ /**
2694
+ * OAuth QR sign-in: add a WorkBuddy account by scanning a code, without the
2695
+ * desktop app.
2696
+ *
2697
+ * 参考:WorkBuddy OAuth 采集的通行实现(对照官网 cockpit 的
2698
+ * `oauth_start` / `oauth_poll`):
2699
+ * 向 `{billing_base}{API_PREFIX}/auth/state?platform={platform}` 申请一个
2700
+ * state,用户在浏览器打开授权页完成登录,客户端轮询
2701
+ * `{billing_base}{API_PREFIX}/auth/token?state={state}` 换取凭据,再以
2702
+ * Bearer 调 `{API_PREFIX}/login/account` 拉取账号资料。三步的 URL 形态、
2703
+ * 响应字段双拼写(camelCase/snake_case)容错、以及「区域必须绑定会话」的
2704
+ * 约定均沿用该实现。
2705
+ * 改动:端点按本插件 `upstream.ts` 的区域约定路由 —— CN 为
2706
+ * `https://www.codebuddy.cn`;国际版按凭据域分家(`workbuddy.ai` 与
2707
+ * `codebuddy.ai` 两把钥匙各自签发,混用会被网关拒绝),插件发起登录时
2708
+ * 还没有凭据,因此由用户显式传基址,缺省 `https://www.workbuddy.ai`。
2709
+ * 登录结果不落独立账号库,而是交给宿主写入统一凭据库(`src/auth.ts` 的
2710
+ * vault),与桌面端扫描到的账号共用一套发现/选择/刷新机制。
2711
+ *
2712
+ * 会话状态只在进程内(`LoginSession`):PKCE 类 state 与登录进度不落盘,
2713
+ * 拿到凭据即终结。刷新凭据仍走 `refreshToken`(上游 refresh 端点),此处
2714
+ * 只负责首次取凭据。
2715
+ *
2716
+ * @module dsh-connect-workbuddy/oauth
2717
+ */
2718
+ /** The CN billing base; matches `upstream.ts`'s CN_BILLING_BASE. */
2719
+ const CN_BILLING_BASE = "https://www.codebuddy.cn";
2720
+ /** Default international base; the desktop app's gateway. */
2721
+ const GLOBAL_BILLING_BASE = "https://www.workbuddy.ai";
2722
+ /** Upstream plugin API prefix shared by both regions. */
2723
+ const API_PREFIX = "/v2/plugin";
2724
+ /** In-process session map. Deliberately not persisted: a restart cancels. */
2725
+ const sessions = /* @__PURE__ */ new Map();
2726
+ /**
2727
+ * The billing base a login for `region` should start from.
2728
+ *
2729
+ * CN is single-homed. The international product signs in through either of
2730
+ * two brand domains (see `upstream.ts` `globalBase`); the caller may pin
2731
+ * one with `explicitBase` — a token issued at one domain is refused by the
2732
+ * other, so the choice must be made BEFORE the state is issued, not after.
2733
+ */
2734
+ function oauthBillingBase(region, explicitBase) {
2735
+ if (region === "cn") return CN_BILLING_BASE;
2736
+ const trimmed = explicitBase?.trim();
2737
+ if (trimmed !== void 0 && trimmed !== "") return normalizeBase(trimmed);
2738
+ return GLOBAL_BILLING_BASE;
2739
+ }
2740
+ /** trim + strip one trailing slash; no scheme invented (see upstream.ts). */
2741
+ function normalizeBase(base) {
2742
+ return base.trim().replace(/\/+$/, "");
2743
+ }
2744
+ /**
2745
+ * Start one login: ask the upstream for a state and return the URL the user
2746
+ * opens to scan/confirm. The session is bound to `region` AND `base`;
2747
+ * both are checked on every poll.
2748
+ *
2749
+ * Never throws for an upstream refusal — the error becomes the returned
2750
+ * `error` — because the card renders the reason inline rather than through
2751
+ * an exception path.
2752
+ */
2753
+ async function oauthStart(region, options) {
2754
+ const base = normalizeBase(oauthBillingBase(region, options?.base));
2755
+ const doFetch = options?.fetchImpl ?? fetch;
2756
+ let response;
2757
+ try {
2758
+ response = await doFetch(`${base}${API_PREFIX}/auth/state?platform=workbuddy`, {
2759
+ method: "POST",
2760
+ headers: {
2761
+ "Content-Type": "application/json",
2762
+ "Accept": "application/json"
2763
+ },
2764
+ body: "{}",
2765
+ signal: AbortSignal.timeout(3e4)
2766
+ });
2767
+ } catch (error) {
2768
+ return { error: `could not reach ${base} to start the sign-in (${messageOf(error)})` };
2769
+ }
2770
+ const document = await readJson(response);
2771
+ if (document === void 0) return { error: `the sign-in service answered with a non-JSON body (http ${response.status})` };
2772
+ const data = envelopeData(document);
2773
+ const state = firstString(data, ["state", "State"]);
2774
+ if (state === "") return { error: `the sign-in service did not issue a state (http ${response.status})` };
2775
+ const authUrl = firstString(data, [
2776
+ "authUrl",
2777
+ "auth_url",
2778
+ "url",
2779
+ "Url"
2780
+ ]) || `${base}/login?state=${encodeURIComponent(state)}`;
2781
+ const loginId = `wbo_${randomUUID().replaceAll("-", "")}`;
2782
+ sessions.set(loginId, {
2783
+ region,
2784
+ base,
2785
+ state,
2786
+ expiresAtMs: Date.now() + 6e5,
2787
+ done: false
2788
+ });
2789
+ return {
2790
+ loginId,
2791
+ verificationUri: authUrl,
2792
+ expiresIn: 600
2793
+ };
2794
+ }
2795
+ /**
2796
+ * Poll once for a login's outcome.
2797
+ *
2798
+ * Ordering matters and mirrors the reference implementation: the session's
2799
+ * region/base binding is checked BEFORE any I/O, so a poll that names the
2800
+ * wrong tab never reaches the network, and an unknown id fails without a
2801
+ * request too.
2802
+ */
2803
+ async function oauthPoll(loginId, region, options) {
2804
+ const session = sessions.get(loginId);
2805
+ if (session === void 0) return {
2806
+ done: true,
2807
+ error: "sign-in session not found; start a new one"
2808
+ };
2809
+ if (session.region !== region) return {
2810
+ done: true,
2811
+ error: "sign-in session belongs to another region tab"
2812
+ };
2813
+ if (session.done) return terminalOf(session);
2814
+ if (Date.now() > session.expiresAtMs) {
2815
+ session.done = true;
2816
+ session.error = "the sign-in timed out; start again";
2817
+ sessions.delete(loginId);
2818
+ return {
2819
+ done: true,
2820
+ error: session.error
2821
+ };
2822
+ }
2823
+ const doFetch = options?.fetchImpl ?? fetch;
2824
+ let response;
2825
+ try {
2826
+ response = await doFetch(`${session.base}${API_PREFIX}/auth/token?state=${encodeURIComponent(session.state)}`, {
2827
+ method: "GET",
2828
+ headers: { "Accept": "application/json" },
2829
+ signal: AbortSignal.timeout(3e4)
2830
+ });
2831
+ } catch {
2832
+ return { done: false };
2833
+ }
2834
+ const document = await readJson(response);
2835
+ if (document === void 0) return { done: false };
2836
+ const code = typeof document["code"] === "number" ? document["code"] : -1;
2837
+ if (code !== 0 && code !== 200) return { done: false };
2838
+ const data = envelopeData(document);
2839
+ const accessToken = firstString(data, [
2840
+ "accessToken",
2841
+ "access_token",
2842
+ "AccessToken"
2843
+ ]);
2844
+ if (accessToken === "") return { done: false };
2845
+ session.done = true;
2846
+ try {
2847
+ const account = await collectAccount(session, accessToken, data, doFetch);
2848
+ sessions.delete(loginId);
2849
+ return {
2850
+ done: true,
2851
+ account
2852
+ };
2853
+ } catch (error) {
2854
+ session.error = messageOf(error);
2855
+ sessions.delete(loginId);
2856
+ return {
2857
+ done: true,
2858
+ error: session.error
2859
+ };
2860
+ }
2861
+ }
2862
+ /** Terminal view of a session, for a poll that arrives after completion. */
2863
+ function terminalOf(session) {
2864
+ if (session.result !== void 0) return {
2865
+ done: true,
2866
+ account: session.result
2867
+ };
2868
+ return {
2869
+ done: true,
2870
+ error: session.error ?? "the sign-in ended without a result"
2871
+ };
2872
+ }
2873
+ /** Fetch the account profile and assemble the success value. */
2874
+ async function collectAccount(session, accessToken, tokenData, doFetch) {
2875
+ const headers = {
2876
+ "Authorization": `Bearer ${accessToken}`,
2877
+ "Accept": "application/json"
2878
+ };
2879
+ const domain = firstString(tokenData, ["domain", "Domain"]);
2880
+ if (domain !== "") headers["X-Domain"] = domain;
2881
+ let profile = {};
2882
+ try {
2883
+ const document = await readJson(await doFetch(`${session.base}${API_PREFIX}/login/account`, {
2884
+ method: "GET",
2885
+ headers,
2886
+ signal: AbortSignal.timeout(3e4)
2887
+ }));
2888
+ if (document !== void 0) profile = envelopeData(document);
2889
+ } catch {}
2890
+ const uid = firstString(profile, [
2891
+ "uid",
2892
+ "Uid",
2893
+ "userId",
2894
+ "UserID"
2895
+ ]);
2896
+ const nickname = optionalString(profile, [
2897
+ "nickname",
2898
+ "NickName",
2899
+ "nickname",
2900
+ "ScreenName"
2901
+ ]);
2902
+ const uin = optionalString(profile, ["uin", "Uin"]);
2903
+ const enterpriseId = optionalString(profile, ["enterpriseId", "EnterpriseId"]);
2904
+ const expiresAtMs = expiryMs(tokenData, ["expiresAt", "expires_at"], ["expiresIn", "expires_in"]) ?? 0;
2905
+ const refreshExpiresAtMs = expiryMs(tokenData, ["refreshExpiresAt", "refresh_expires_at"], ["refreshExpiresIn", "refresh_expires_in"]);
2906
+ return {
2907
+ accessToken,
2908
+ refreshToken: firstString(tokenData, [
2909
+ "refreshToken",
2910
+ "refresh_token",
2911
+ "RefreshToken"
2912
+ ]),
2913
+ expiresAtMs,
2914
+ ...refreshExpiresAtMs === void 0 ? {} : { refreshExpiresAtMs },
2915
+ domain: domain !== "" ? domain : firstString(profile, ["domain", "Domain"]),
2916
+ uid,
2917
+ ...nickname === void 0 ? {} : { nickname },
2918
+ ...uin === void 0 ? {} : { uin },
2919
+ ...enterpriseId === void 0 ? {} : { enterpriseId }
2920
+ };
2921
+ }
2922
+ /** Parse a JSON body; undefined for non-JSON (the caller decides severity). */
2923
+ async function readJson(response) {
2924
+ try {
2925
+ const text = await response.text();
2926
+ const parsed = JSON.parse(text);
2927
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) return parsed;
2928
+ } catch {}
2929
+ }
2930
+ /** The envelope's `data` object; a non-object data is an empty one. */
2931
+ function envelopeData(document) {
2932
+ const data = document["data"];
2933
+ return typeof data === "object" && data !== null && !Array.isArray(data) ? data : {};
2934
+ }
2935
+ /** First non-empty string among `keys`, else ''. */
2936
+ function firstString(source, keys) {
2937
+ for (const key of keys) {
2938
+ const value = source[key];
2939
+ if (typeof value === "string" && value.trim() !== "") return value.trim();
2940
+ if (typeof value === "number" && Number.isFinite(value)) return String(value);
2941
+ }
2942
+ return "";
2943
+ }
2944
+ /** First non-empty string among `keys`, else undefined. */
2945
+ function optionalString(source, keys) {
2946
+ const value = firstString(source, keys);
2947
+ return value === "" ? void 0 : value;
2948
+ }
2949
+ /**
2950
+ * An expiry field that may arrive as an absolute timestamp (s or ms) or as a
2951
+ * relative TTL in seconds — the same tolerance `auth.ts` `expiryToMs` and
2952
+ * the reference implementation's `norm_ts` apply.
2953
+ */
2954
+ function expiryMs(source, absoluteKeys, ttlKeys) {
2955
+ for (const key of absoluteKeys) {
2956
+ const value = source[key];
2957
+ if (typeof value === "number" && value > 0) return value > 0xe8d4a51000 ? value : value * 1e3;
2958
+ if (typeof value === "string" && value.trim() !== "" && Number.isFinite(Number(value))) {
2959
+ const parsed = Number(value);
2960
+ return parsed > 0xe8d4a51000 ? parsed : parsed * 1e3;
2961
+ }
2962
+ }
2963
+ for (const key of ttlKeys) {
2964
+ const value = source[key];
2965
+ if (typeof value === "number" && value > 0) return Date.now() + value * 1e3;
2966
+ }
2967
+ }
2968
+ /** Human message from a thrown error, for the card's inline display. */
2969
+ function messageOf(error) {
2970
+ return error instanceof Error ? error.message : String(error);
2971
+ }
2972
+ //#endregion
2973
+ //#region src/transfer.ts
2974
+ /**
2975
+ * Account transfer: the export / import file format shared with sibling
2976
+ * credential managers for the same upstream.
2977
+ *
2978
+ * 参考:通行实现 —— 文件是一个 JSON 数组,每项为一条凭据记录,顶层字段
2979
+ * snake_case(`access_token` / `refresh_token` / `uid` / `nickname` /
2980
+ * `email` / `domain` / `expiresAt`(ms) / `refreshExpiresAt` / `auth_raw`),
2981
+ * 导入按 `uid` 去重(同 uid 覆盖),缺 `access_token` 的记录跳过;预览只
2982
+ * 回脱敏字段(含 `hasToken` 布尔量),不回明文。解析函数不依赖文件系统,
2983
+ * 便于无 UI 环境单测;落库由 Host 侧的 store 完成。
2984
+ * 互通:`expiresAt` 一族按契约是**毫秒**(由 auth_raw 的原始认证文档携带);
2985
+ * 本插件的 vault 文档 `expiresAt` 是秒 —— 换算只发生在导出/导入边界。
2986
+ * 导出时 `auth_raw` 直接携带本插件 vault 文档的 `{auth, account}` 双键块,
2987
+ * 与桌面认证文件同构,供写入桌面认证文件的工具原样消费。
2988
+ *
2989
+ * @module dsh-connect-workbuddy/transfer
2990
+ */
2991
+ /**
2992
+ * Parse transfer-file text: a JSON array, every element an object.
2993
+ *
2994
+ * The error strings carry the position (1-based item number) because a file
2995
+ * from another tool can be big, and "第 N 项" is what lets the user find the
2996
+ * broken record in their editor.
2997
+ */
2998
+ function parseTransferFile(text) {
2999
+ if (text.trim() === "") throw new Error("transfer file is empty");
3000
+ let parsed;
3001
+ try {
3002
+ parsed = JSON.parse(text);
3003
+ } catch (error) {
3004
+ throw new Error("transfer file is not valid JSON: " + (error instanceof Error ? error.message : String(error)));
3005
+ }
3006
+ if (!Array.isArray(parsed)) throw new Error("transfer file must be a JSON array of account records");
3007
+ for (const [index, item] of parsed.entries()) if (typeof item !== "object" || item === null || Array.isArray(item)) throw new Error("transfer file item " + (index + 1) + " is not an account object");
3008
+ return parsed;
3009
+ }
3010
+ /** One string field of a record, tolerating the sibling format's odd shapes. */
3011
+ function displayString(value) {
3012
+ if (typeof value === "string") return value;
3013
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
3014
+ return "";
3015
+ }
3016
+ /** Whether a record carries a usable token, envelope-shaped or plain. */
3017
+ function hasToken(record) {
3018
+ const token = record["access_token"];
3019
+ return typeof token === "string" && token !== "";
3020
+ }
3021
+ /**
3022
+ * The transfer file's desensitized preview: display fields only.
3023
+ *
3024
+ * Fields come from EXTERNAL files, so a display field may hold an object or an
3025
+ * array (a sibling tool can store the desktop app's encrypted-wrapper nickname
3026
+ * verbatim); every preview field is normalized to a string or emptied rather
3027
+ * than letting a non-renderable value reach the card.
3028
+ */
3029
+ function previewTransferFile(text) {
3030
+ const records = parseTransferFile(text);
3031
+ return {
3032
+ accounts: records.map((record, index) => ({
3033
+ index,
3034
+ uid: displayString(record["uid"]),
3035
+ nickname: displayString(record["nickname"]),
3036
+ email: displayString(record["email"]),
3037
+ hasToken: hasToken(record)
3038
+ })),
3039
+ total: records.length
3040
+ };
3041
+ }
3042
+ /**
3043
+ * Project one vault credential into the shared transfer record shape.
3044
+ *
3045
+ * `expiresAt` is emitted in MILLISECONDS — the transfer contract's unit and
3046
+ * the sibling tools' display field — while the vault document stores seconds.
3047
+ * `auth_raw` carries the vault's nested `{auth, account}` document so a tool
3048
+ * that writes desktop auth files can consume it verbatim.
3049
+ */
3050
+ function transferRecordOf(credential, vaultDocument) {
3051
+ return {
3052
+ access_token: credential.accessToken,
3053
+ ...credential.refreshToken !== "" ? { refresh_token: credential.refreshToken } : {},
3054
+ ...credential.uid !== "" ? { uid: credential.uid } : {},
3055
+ ...credential.nickname !== void 0 ? { nickname: credential.nickname } : {},
3056
+ ...credential.domain !== "" ? { domain: credential.domain } : {},
3057
+ ...credential.expiresAtMs > 0 ? { expiresAt: credential.expiresAtMs } : {},
3058
+ ...credential.refreshExpiresAtMs === void 0 ? {} : { refreshExpiresAt: credential.refreshExpiresAtMs },
3059
+ auth_raw: vaultDocument
3060
+ };
3061
+ }
3062
+ /**
3063
+ * Recover `uin` from a record's `auth_raw` document.
3064
+ *
3065
+ * `uin` is the billing identity the account id derives from
3066
+ * (`uin ?? uid ?? nickname`), but it is NOT one of the transfer format's
3067
+ * contract fields — sibling exports carry it only inside `auth_raw`
3068
+ * (the original auth document, whose `account` object holds it). Restoring it
3069
+ * is what makes an exported-and-re-imported account converge on the SAME
3070
+ * vault id instead of duplicating under a uid-derived id. Both document
3071
+ * shapes are honored: the nested `{auth, account}` block this plugin
3072
+ * exports, and a record whose `auth_raw.account` came straight from a
3073
+ * desktop file.
3074
+ */
3075
+ function uinOfRaw(raw) {
3076
+ if (typeof raw !== "object" || raw === null) return void 0;
3077
+ const root = raw;
3078
+ const uin = displayString((typeof root["account"] === "object" && root["account"] !== null ? root["account"] : root)["uin"]);
3079
+ return uin === "" ? void 0 : uin;
3080
+ }
3081
+ /**
3082
+ * Project one transfer record into a vault credential, or undefined when the
3083
+ * record carries no token (the importer's skip rule).
3084
+ *
3085
+ * `uid` empty is tolerated at THIS layer because a token-only record still
3086
+ * describes a usable credential; the Host's region gate and the store's
3087
+ * account id (`uin ?? uid ?? nickname`) decide whether it is representable.
3088
+ * An unopenable `auth_raw` is ignored — the flat fields above are the
3089
+ * contract, `auth_raw` is extra fidelity for the write-back path.
3090
+ */
3091
+ function credentialOfTransferRecord(record) {
3092
+ const accessToken = record["access_token"];
3093
+ if (typeof accessToken !== "string" || accessToken === "") return void 0;
3094
+ const refreshToken = typeof record["refresh_token"] === "string" ? record["refresh_token"] : "";
3095
+ const expiresAtMs = typeof record["expiresAt"] === "number" && Number.isFinite(record["expiresAt"]) && record["expiresAt"] > 0 ? record["expiresAt"] : 0;
3096
+ const refreshExpiresAtMs = typeof record["refreshExpiresAt"] === "number" && Number.isFinite(record["refreshExpiresAt"]) && record["refreshExpiresAt"] > 0 ? record["refreshExpiresAt"] : void 0;
3097
+ const uin = uinOfRaw(record["auth_raw"]);
3098
+ return {
3099
+ accessToken,
3100
+ refreshToken,
3101
+ expiresAtMs,
3102
+ ...refreshExpiresAtMs === void 0 ? {} : { refreshExpiresAtMs },
3103
+ domain: typeof record["domain"] === "string" ? record["domain"] : "",
3104
+ uid: displayString(record["uid"]),
3105
+ ...record["nickname"] !== void 0 ? { nickname: displayString(record["nickname"]) } : {},
3106
+ ...uin === void 0 ? {} : { uin }
3107
+ };
3108
+ }
3109
+ /**
3110
+ * Pure merge: fold selected records into a CHANGES map keyed by account id.
3111
+ *
3112
+ * Mirrors the sibling merger's contract: a record with the same account id
3113
+ * OVERWRITES the stored entry (the imported tokens replace what is stored —
3114
+ * re-importing a re-login must refresh it; the write layer stores the record
3115
+ * atomically, so "overwrite" needs no knowledge of the incumbent), and a
3116
+ * record without a token is skipped. The region gate lives HERE rather than
3117
+ * in the route so it is testable without a Host: a record whose login domain
3118
+ * maps to the other region's tab is skipped, because the two regions'
3119
+ * providers are parallel stacks and a cross-region import would cross-bill.
3120
+ *
3121
+ * The caller writes one vault file per map entry, so a selection that is
3122
+ * already stored rewrites those files with identical content — idempotent,
3123
+ * and the steady state of importing the same file twice.
3124
+ */
3125
+ function mergeTransferRecords(records, indexes, region, knownRegionOfDomain) {
3126
+ const changes = /* @__PURE__ */ new Map();
3127
+ let imported = 0;
3128
+ let skipped = 0;
3129
+ for (const index of indexes) {
3130
+ const record = records[index];
3131
+ if (record === void 0) {
3132
+ skipped += 1;
3133
+ continue;
3134
+ }
3135
+ const credential = credentialOfTransferRecord(record);
3136
+ if (credential === void 0) {
3137
+ skipped += 1;
3138
+ continue;
3139
+ }
3140
+ if (knownRegionOfDomain(credential.domain) !== region) {
3141
+ skipped += 1;
3142
+ continue;
3143
+ }
3144
+ const id = workbuddyAccountId(credential);
3145
+ changes.set(id, credential);
3146
+ imported += 1;
3147
+ }
3148
+ return {
3149
+ changes,
3150
+ imported,
3151
+ skipped
3152
+ };
3153
+ }
3154
+ //#endregion
2692
3155
  //#region src/account-pool-store.ts
2693
3156
  /**
2694
3157
  * The account pool's RUNTIME store: measured facts, on disk, outside settings.
@@ -2872,14 +3335,59 @@ const WORKBUDDY_PROBE_PATH = "/plugins/dsh-connect-workbuddy/probe";
2872
3335
  * splitting them would duplicate the guards and invite them to drift apart.
2873
3336
  */
2874
3337
  const WORKBUDDY_POOL_PATH = "/plugins/dsh-connect-workbuddy/pool";
3338
+ /**
3339
+ * Plugin-owned OAuth sign-in endpoint: one session step per call.
3340
+ *
3341
+ * `?action=start` asks the upstream for a login state and returns the URL to
3342
+ * open; `?action=poll` (+ `loginId` in the body) reports whether the user
3343
+ * finished, and on success persists the account into the vault. POST +
3344
+ * loopback-only like the card's other mutations: the poll carries the login
3345
+ * binding, and the Host-side session state must not be settable by another
3346
+ * origin.
3347
+ */
3348
+ const WORKBUDDY_OAUTH_PATH = "/plugins/dsh-connect-workbuddy/oauth";
2875
3349
  /** Query parameter selecting which pool batch action a request means. */
2876
3350
  const WORKBUDDY_POOL_ACTION_PARAM = "action";
3351
+ /** Read the OAuth action off a request URL; unknown/absent means undefined. */
3352
+ function oauthActionOf(url) {
3353
+ const at = url.indexOf("?");
3354
+ if (at === -1) return void 0;
3355
+ const value = new URLSearchParams(url.slice(at + 1)).get(WORKBUDDY_POOL_ACTION_PARAM);
3356
+ return value === "start" || value === "poll" ? value : void 0;
3357
+ }
2877
3358
  /** Read the pool action off a request URL; unknown/absent means undefined. */
2878
3359
  function poolActionOf(url) {
2879
3360
  const at = url.indexOf("?");
2880
3361
  if (at === -1) return void 0;
2881
3362
  const value = new URLSearchParams(url.slice(at + 1)).get(WORKBUDDY_POOL_ACTION_PARAM);
2882
- return value === "checkin" || value === "test" ? value : void 0;
3363
+ return value === "checkin" || value === "test" || value === "remove" ? value : void 0;
3364
+ }
3365
+ /** Query parameter naming ONE pool account a per-row request addresses. */
3366
+ const WORKBUDDY_POOL_ACCOUNT_PARAM = "accountId";
3367
+ /**
3368
+ * Plugin-owned account transfer endpoint: the batch export / import of
3369
+ * credentials between machines and between tools of the SAME format family.
3370
+ *
3371
+ * `?action=export` POSTs account ids, answers a JSON ARRAY of credential
3372
+ * records; `?action=preview` POSTs file text, answers a token-free listing;
3373
+ * `?action=import` POSTs file text plus selected indexes, merges into the
3374
+ * vault. POST + loopback-only like every other mutation: the file content is
3375
+ * arbitrary user data and export carries token material.
3376
+ */
3377
+ const WORKBUDDY_TRANSFER_PATH = "/plugins/dsh-connect-workbuddy/transfer";
3378
+ /** Read the transfer action off a request URL; unknown/absent means undefined. */
3379
+ function transferActionOf(url) {
3380
+ const at = url.indexOf("?");
3381
+ if (at === -1) return void 0;
3382
+ const value = new URLSearchParams(url.slice(at + 1)).get(WORKBUDDY_POOL_ACTION_PARAM);
3383
+ return value === "export" || value === "preview" || value === "import" ? value : void 0;
3384
+ }
3385
+ /** Read the per-row account id off a request URL, when the action takes one. */
3386
+ function poolAccountIdOf(url) {
3387
+ const at = url.indexOf("?");
3388
+ if (at === -1) return void 0;
3389
+ const value = new URLSearchParams(url.slice(at + 1)).get(WORKBUDDY_POOL_ACCOUNT_PARAM);
3390
+ return value !== null && value !== "" ? value : void 0;
2883
3391
  }
2884
3392
  /** Query parameter naming the region a card request addresses. */
2885
3393
  const WORKBUDDY_REGION_PARAM = "region";
@@ -3039,6 +3547,23 @@ function json(res, status, body) {
3039
3547
  });
3040
3548
  res.end(payload);
3041
3549
  }
3550
+ /**
3551
+ * A JSON response that must never be cached.
3552
+ *
3553
+ * Used for every answer that carries live credentials — the transfer export
3554
+ * hands the browser real access and refresh tokens. Without `no-store` such a
3555
+ * response is eligible for the browser's HTTP cache, where a token can outlive
3556
+ * the session on disk.
3557
+ */
3558
+ function jsonPrivate(res, status, body) {
3559
+ const payload = JSON.stringify(body);
3560
+ res.writeHead(status, {
3561
+ "Content-Type": "application/json",
3562
+ "Content-Length": Buffer.byteLength(payload),
3563
+ "Cache-Control": "no-store"
3564
+ });
3565
+ res.end(payload);
3566
+ }
3042
3567
  /** Request-body cap for the probe route: model ids only, never content. */
3043
3568
  const PROBE_BODY_LIMIT = 65536;
3044
3569
  /**
@@ -3508,12 +4033,60 @@ function registerWorkBuddyStatusRoute(ctx, deps) {
3508
4033
  const region = requestRegion(req, res);
3509
4034
  if (region === void 0) return;
3510
4035
  const action = poolActionOf(req.url ?? "/");
3511
- if (action === void 0) return json(res, 400, { error: "action must be checkin or test" });
4036
+ if (action === void 0) return json(res, 400, { error: "action must be checkin, test, or remove" });
3512
4037
  const pool = deps.pool;
3513
4038
  if (pool === void 0) return json(res, 503, {
3514
4039
  reason: "pool-unavailable",
3515
4040
  error: "account pool unavailable"
3516
4041
  });
4042
+ const rowAccountId = poolAccountIdOf(req.url ?? "/");
4043
+ if (action === "remove") {
4044
+ if (pool.removeOne === void 0) return json(res, 503, {
4045
+ reason: "pool-unavailable",
4046
+ error: "per-account removal unavailable"
4047
+ });
4048
+ if (rowAccountId === void 0) return json(res, 400, { error: "accountId must name the account to remove" });
4049
+ if (!isVaultAccountId(rowAccountId)) return json(res, 400, { error: "accountId is not a well-formed account id" });
4050
+ try {
4051
+ json(res, 200, {
4052
+ action: "remove",
4053
+ accountId: rowAccountId,
4054
+ removed: await pool.removeOne(region, rowAccountId)
4055
+ });
4056
+ } catch (error) {
4057
+ json(res, 500, {
4058
+ reason: "pool-failed",
4059
+ error: safeMessage(error)
4060
+ });
4061
+ }
4062
+ return;
4063
+ }
4064
+ if (action === "test" && rowAccountId !== void 0) {
4065
+ if (pool.testOne === void 0) return json(res, 503, {
4066
+ reason: "pool-unavailable",
4067
+ error: "per-account test unavailable"
4068
+ });
4069
+ try {
4070
+ const row = await pool.testOne(region, rowAccountId);
4071
+ if (row === void 0) return json(res, 409, {
4072
+ action: "test",
4073
+ modelId: void 0,
4074
+ rows: [],
4075
+ reason: "no-free-model",
4076
+ error: "no zero-multiplier model in this region; refresh the catalog or set a target model"
4077
+ });
4078
+ json(res, 200, {
4079
+ action: "test",
4080
+ rows: [row]
4081
+ });
4082
+ } catch (error) {
4083
+ json(res, 500, {
4084
+ reason: "pool-failed",
4085
+ error: safeMessage(error)
4086
+ });
4087
+ }
4088
+ return;
4089
+ }
3517
4090
  try {
3518
4091
  const savedMembers = pool.preferences(region).memberAccountIds;
3519
4092
  if ((await pool.effectiveMemberAccountIds?.(region).catch(() => void 0) ?? savedMembers).length === 0) {
@@ -3645,6 +4218,69 @@ function registerWorkBuddyStatusRoute(ctx, deps) {
3645
4218
  }
3646
4219
  }
3647
4220
  });
4221
+ const disposeOAuth = ctx.webServer.register({
4222
+ kind: "exact",
4223
+ path: WORKBUDDY_OAUTH_PATH,
4224
+ handler: async (req, res) => {
4225
+ if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
4226
+ if (!loopbackOrigin(req)) return json(res, 403, { error: "origin-not-trusted" });
4227
+ const region = requestRegion(req, res);
4228
+ if (region === void 0) return;
4229
+ const action = oauthActionOf(req.url ?? "/");
4230
+ if (action === void 0) return json(res, 400, { error: "action must be start or poll" });
4231
+ const oauth = deps.oauth;
4232
+ if (oauth === void 0) return json(res, 503, {
4233
+ reason: "oauth-unavailable",
4234
+ error: "OAuth sign-in is not available in this build"
4235
+ });
4236
+ try {
4237
+ if (action === "start") {
4238
+ json(res, 200, await oauth.start(region));
4239
+ return;
4240
+ }
4241
+ const body = await readJsonBody(req);
4242
+ if (typeof body.loginId !== "string" || body.loginId === "") return json(res, 400, { error: "loginId must be a non-empty string" });
4243
+ json(res, 200, await oauth.poll(region, body.loginId));
4244
+ } catch (error) {
4245
+ json(res, 500, { error: safeMessage(error) });
4246
+ }
4247
+ }
4248
+ });
4249
+ const disposeTransfer = ctx.webServer.register({
4250
+ kind: "exact",
4251
+ path: WORKBUDDY_TRANSFER_PATH,
4252
+ handler: async (req, res) => {
4253
+ if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
4254
+ if (!loopbackOrigin(req)) return json(res, 403, { error: "origin-not-trusted" });
4255
+ const region = requestRegion(req, res);
4256
+ if (region === void 0) return;
4257
+ const action = transferActionOf(req.url ?? "/");
4258
+ if (action === void 0) return json(res, 400, { error: "action must be export, preview, or import" });
4259
+ const transfer = deps.transfer;
4260
+ if (transfer === void 0) return json(res, 503, {
4261
+ reason: "transfer-unavailable",
4262
+ error: "account transfer is not available in this build"
4263
+ });
4264
+ try {
4265
+ if (action === "export") {
4266
+ const body = await readJsonBody(req);
4267
+ const accountIds = Array.isArray(body.accountIds) ? body.accountIds.filter((id) => typeof id === "string" && id !== "") : [];
4268
+ if (accountIds.length === 0) return json(res, 400, { error: "accountIds must be a non-empty array of strings" });
4269
+ const answer = await transfer.exportAccounts(region, accountIds);
4270
+ if (answer.accounts.length === 0) return json(res, 409, { error: "none of the named accounts was found in this region" });
4271
+ return jsonPrivate(res, 200, answer);
4272
+ }
4273
+ const body = await readJsonBody(req);
4274
+ if (typeof body.text !== "string" || body.text.trim() === "") return json(res, 400, { error: "text must be the transfer file's JSON content" });
4275
+ if (action === "preview") return json(res, 200, await transfer.previewImport(region, body.text));
4276
+ const indexes = Array.isArray(body.indexes) ? body.indexes.filter((value) => typeof value === "number" && Number.isInteger(value) && value >= 0) : [];
4277
+ if (indexes.length === 0) return json(res, 400, { error: "indexes must name at least one file entry to import" });
4278
+ return json(res, 200, await transfer.importAccounts(region, body.text, indexes));
4279
+ } catch (error) {
4280
+ json(res, 500, { error: safeMessage(error) });
4281
+ }
4282
+ }
4283
+ });
3648
4284
  return () => {
3649
4285
  disposeRefresh();
3650
4286
  disposeCheckin();
@@ -3653,6 +4289,8 @@ function registerWorkBuddyStatusRoute(ctx, deps) {
3653
4289
  disposeAccountCredits();
3654
4290
  disposeProbe();
3655
4291
  disposePool();
4292
+ disposeOAuth();
4293
+ disposeTransfer();
3656
4294
  disposeDiagWrite();
3657
4295
  };
3658
4296
  }, "dsh-connect-workbuddy: Web status route");
@@ -4391,7 +5029,138 @@ function apply(ctx, config) {
4391
5029
  await writePoolProbes(region, probeUpdatesOf(rows));
4392
5030
  return rows;
4393
5031
  },
4394
- catalog: (region) => displayModels(current(), region)
5032
+ catalog: (region) => displayModels(current(), region),
5033
+ /**
5034
+ * Test ONE account against the region's target model.
5035
+ *
5036
+ * The per-row button's Host half. Reuses the batch's probe dependency
5037
+ * (same client, same probe size, same quota-refresh input) so a row
5038
+ * result and a batch result are the SAME measurement — only the
5039
+ * persistence is scoped to one id instead of every member.
5040
+ *
5041
+ * Returns undefined when the region cannot name a target model (the
5042
+ * same refusal the batch route reports as 409) — the card says so
5043
+ * rather than pretending a test ran.
5044
+ */
5045
+ testOne: async (region, accountId) => {
5046
+ const target = regionTargetModel(region);
5047
+ if (target === void 0) return void 0;
5048
+ const store = stacks[region].store;
5049
+ const credential = await store.credentialFor(accountId).catch(() => void 0);
5050
+ if (credential === void 0) return {
5051
+ accountId,
5052
+ accountName: "",
5053
+ result: {
5054
+ modelId: target,
5055
+ outcome: "credential-rejected",
5056
+ status: 0,
5057
+ message: "no stored credential for this account"
5058
+ }
5059
+ };
5060
+ const accountName = (await store.accounts().catch(() => [])).find((account) => account.id === accountId)?.accountName ?? "";
5061
+ const result = await poolRunnerDeps(region).probe(credential, target);
5062
+ await writePoolProbes(region, probeUpdatesOf([{
5063
+ accountId,
5064
+ accountName,
5065
+ result
5066
+ }]));
5067
+ return {
5068
+ accountId,
5069
+ accountName,
5070
+ result
5071
+ };
5072
+ },
5073
+ /**
5074
+ * Forget ONE account's plugin-stored credential (the vault entry; the
5075
+ * desktop app's files are never touched). The card rescans afterwards,
5076
+ * so the row disappears the moment the write lands.
5077
+ */
5078
+ removeOne: async (region, accountId) => stacks[region].store.removeAccount(accountId)
5079
+ },
5080
+ /**
5081
+ * OAuth QR sign-in for this card. The session lives in `src/oauth.ts`
5082
+ * (in-process, keyed by loginId); the Host's only extra duty is
5083
+ * persisting the finished credential into the region's vault — the same
5084
+ * store the request path reads, so the account is selectable the moment
5085
+ * the poll answers.
5086
+ */
5087
+ oauth: {
5088
+ async start(region) {
5089
+ return oauthStart(region);
5090
+ },
5091
+ async poll(region, loginId) {
5092
+ const answer = await oauthPoll(loginId, region);
5093
+ if (!answer.done) return answer;
5094
+ if ("error" in answer) return answer;
5095
+ const store = stacks[region].store;
5096
+ const accountId = await store.addOAuthAccount({
5097
+ accessToken: answer.account.accessToken,
5098
+ refreshToken: answer.account.refreshToken,
5099
+ expiresAtMs: answer.account.expiresAtMs,
5100
+ ...answer.account.refreshExpiresAtMs === void 0 ? {} : { refreshExpiresAtMs: answer.account.refreshExpiresAtMs },
5101
+ domain: answer.account.domain,
5102
+ uid: answer.account.uid,
5103
+ ...answer.account.nickname === void 0 ? {} : { nickname: answer.account.nickname },
5104
+ ...answer.account.uin === void 0 ? {} : { uin: answer.account.uin },
5105
+ ...answer.account.enterpriseId === void 0 ? {} : { enterpriseId: answer.account.enterpriseId }
5106
+ }).catch((error) => {
5107
+ throw new Error("the account was signed in but could not be saved: " + (error instanceof Error ? error.message : String(error)));
5108
+ });
5109
+ return {
5110
+ done: true,
5111
+ accountId,
5112
+ accountName: (await store.accounts().catch(() => [])).find((account) => account.id === accountId)?.accountName ?? answer.account.nickname ?? ""
5113
+ };
5114
+ }
5115
+ },
5116
+ /**
5117
+ * Batch credential export / import, in the transfer file format the
5118
+ * sibling credential managers share. The Host owns every byte of the
5119
+ * vault; the route only ferries parsed answers.
5120
+ */
5121
+ transfer: {
5122
+ async exportAccounts(region, accountIds) {
5123
+ const store = stacks[region].store;
5124
+ const accounts = await store.accounts().catch(() => []);
5125
+ const wanted = new Set(accountIds);
5126
+ const records = [];
5127
+ for (const account of accounts) {
5128
+ if (!wanted.has(account.id)) continue;
5129
+ const credential = await store.credentialFor(account.id).catch(() => void 0);
5130
+ if (credential === void 0) continue;
5131
+ records.push(transferRecordOf(credential, {
5132
+ auth: {
5133
+ accessToken: credential.accessToken,
5134
+ refreshToken: credential.refreshToken,
5135
+ expiresAt: credential.expiresAtMs > 0 ? Math.floor(credential.expiresAtMs / 1e3) : 0,
5136
+ ...credential.refreshExpiresAtMs === void 0 ? {} : { refreshExpiresAt: Math.floor(credential.refreshExpiresAtMs / 1e3) },
5137
+ ...credential.lastRefreshAtMs === void 0 ? {} : { lastRefreshTime: Math.floor(credential.lastRefreshAtMs / 1e3) },
5138
+ domain: credential.domain
5139
+ },
5140
+ account: {
5141
+ uid: credential.uid,
5142
+ ...credential.enterpriseId === void 0 ? {} : { enterpriseId: credential.enterpriseId },
5143
+ ...credential.nickname === void 0 ? {} : { nickname: credential.nickname },
5144
+ ...credential.uin === void 0 ? {} : { uin: credential.uin }
5145
+ }
5146
+ }));
5147
+ }
5148
+ return { accounts: records };
5149
+ },
5150
+ async previewImport(region, text) {
5151
+ return previewTransferFile(text);
5152
+ },
5153
+ async importAccounts(region, text, indexes) {
5154
+ const { changes, imported, skipped } = mergeTransferRecords(parseTransferFile(text), indexes, region, (domain) => knownRegionOf(domain));
5155
+ const store = stacks[region].store;
5156
+ for (const [id, credential] of changes) await store.addOAuthAccount(credential).catch((error) => {
5157
+ throw new Error("imported account " + id + " could not be stored: " + (error instanceof Error ? error.message : String(error)));
5158
+ });
5159
+ return {
5160
+ imported,
5161
+ skipped
5162
+ };
5163
+ }
4395
5164
  }
4396
5165
  }));
4397
5166
  /**
@@ -4619,6 +5388,15 @@ function apply(ctx, config) {
4619
5388
  creditsSnapshotAt[region] = Date.now();
4620
5389
  return rows;
4621
5390
  }
5391
+ /**
5392
+ * The model a per-row test would use right now: the SAME resolution the
5393
+ * batch route performs (`resolveTargetModel` over the region's displayed
5394
+ * catalog and the saved preference), so a row test and a batch test never
5395
+ * measure different models. Undefined mirrors the route's 409 states.
5396
+ */
5397
+ const regionTargetModel = (region) => {
5398
+ return resolveTargetModel(displayModels(current(), region), poolPreferencesOf(current(), region).targetModelId).modelId;
5399
+ };
4622
5400
  /** One region's pool preferences, with the schema's defaults applied. */
4623
5401
  const poolPreferencesOf = (config, region) => {
4624
5402
  const pool = regionStateOf(config, region).pool;
@@ -4800,4 +5578,4 @@ function apply(ctx, config) {
4800
5578
  });
4801
5579
  }
4802
5580
  //#endregion
4803
- export { CREDENTIAL_REJECTED_CODE, Config, DEFAULT_PROBE_INPUT_TOKENS, ENCRYPTED_CREDENTIAL_CODE, FALLBACK_WORKBUDDY_MODELS, FALLBACK_WORKBUDDY_MODELS_GLOBAL, NATIVE_MODALITY_BY_MODEL_ID, PROBE_INPUT_TOKEN_CHOICES, PROBE_MAX_TOKENS, PROBE_SYSTEM_PROMPT, REFUSES_OFF_MODEL_IDS, WORKBUDDY_ACCOUNTS_REFRESH_PATH, WORKBUDDY_APP_EXECUTABLE_ENV, WORKBUDDY_AUTH_FILENAME, WORKBUDDY_AUTH_FILE_ENV, WORKBUDDY_CHECKIN_PATH, WORKBUDDY_CONNECT_VERSION, WORKBUDDY_GLOBAL_PROVIDER, WORKBUDDY_HOST_HEARTBEAT_FILENAME, WORKBUDDY_MODELS_REFRESH_PATH, WORKBUDDY_PROBE_PATH, WORKBUDDY_PROVIDER, WORKBUDDY_PROVIDERS, WORKBUDDY_PROVIDER_DISPLAY_NAMES, WORKBUDDY_REGIONS, WORKBUDDY_REGION_PARAM, WORKBUDDY_SETTINGS_NS, WORKBUDDY_STREAM_IDLE_TIMEOUT_MS, WORKBUDDY_USAGE_PATH, WorkBuddyCatalog, WorkBuddyCredentialRejectedError, WorkBuddyCredentialStore, WorkBuddyEncryptedCredentialError, WorkBuddyUpstreamClient, apply, asVolatile, authFileName, classifyUpstreamError, clearAtRestKeyCache, clearHostHeartbeat, cooldownOf, createAccountUsabilityProbe, createWorkBuddyAdapter, createWorkBuddyShim, creditOfStream, defaultDesktopAuthCandidates, defaultDesktopAuthDirs, defaultDesktopAuthPath, defaultOffModelIds, deriveAtRestKey, deriveAtRestKeyId, deriveCatalog, fallbackModelsFor, fetchAtRestKeyPayload, findWorkbuddyAppExecutable, hasEncryptedCredentialFields, imageDefaultFor, inject, isCredentialRejectedError, isEncryptedCredentialError, isEncryptedFieldWrapper, isHeartbeatProcessAlive, isWorkbuddyBundle, legacyAttributionRegion, legacyWorkbuddyOwnAuthPath, macosBundleExecutable, macosNestedAppBundles, name, nativeModalityOf, nextRegionEnabled, nextRegionSlots, offDefaultFor, openEncryptedField, outcomeOfFailure, parseCreditMultiplier, parseReasoning, parseRetryAfter, parseUpstreamModel, parseWorkBuddyAuth, prepareChatBody, probeModel, probeRequestBody, probeSucceeded, processStartTimeMs, readAtRestKey, readHostHeartbeat, regionCleared, regionEnabled, regionEnabledOf, regionOf, regionOfProvider, regionOfStatusUrl, regionStateOf, registerWorkBuddyStatusRoute, resolveCredentialRecovery, selectAccountFor, settingsNamespaceOf, toPersistedWorkBuddyModel, unwrapVolatileDeep, withEffectiveOff, withWorkBuddyRegion, workBuddyDisplayName, workBuddyModelInput, workBuddyThinkingLevelMap, workBuddyWebStatus, workbuddyAccountId, workbuddyAppExecutableCandidates, workbuddyHostHeartbeatPath, workbuddyOwnAuthPath, writeHostHeartbeat };
5581
+ export { CREDENTIAL_REJECTED_CODE, Config, DEFAULT_PROBE_INPUT_TOKENS, ENCRYPTED_CREDENTIAL_CODE, FALLBACK_WORKBUDDY_MODELS, FALLBACK_WORKBUDDY_MODELS_GLOBAL, NATIVE_MODALITY_BY_MODEL_ID, PROBE_INPUT_TOKEN_CHOICES, PROBE_MAX_TOKENS, PROBE_SYSTEM_PROMPT, REFUSES_OFF_MODEL_IDS, WORKBUDDY_ACCOUNTS_REFRESH_PATH, WORKBUDDY_APP_EXECUTABLE_ENV, WORKBUDDY_AUTH_FILENAME, WORKBUDDY_AUTH_FILE_ENV, WORKBUDDY_CHECKIN_PATH, WORKBUDDY_CONNECT_VERSION, WORKBUDDY_GLOBAL_PROVIDER, WORKBUDDY_HOST_HEARTBEAT_FILENAME, WORKBUDDY_MODELS_REFRESH_PATH, WORKBUDDY_PROBE_PATH, WORKBUDDY_PROVIDER, WORKBUDDY_PROVIDERS, WORKBUDDY_PROVIDER_DISPLAY_NAMES, WORKBUDDY_REGIONS, WORKBUDDY_REGION_PARAM, WORKBUDDY_SETTINGS_NS, WORKBUDDY_STREAM_IDLE_TIMEOUT_MS, WORKBUDDY_USAGE_PATH, WorkBuddyCatalog, WorkBuddyCredentialRejectedError, WorkBuddyCredentialStore, WorkBuddyEncryptedCredentialError, WorkBuddyUpstreamClient, apply, asVolatile, authFileName, classifyUpstreamError, clearAtRestKeyCache, clearHostHeartbeat, cooldownOf, createAccountUsabilityProbe, createWorkBuddyAdapter, createWorkBuddyShim, creditOfStream, defaultDesktopAuthCandidates, defaultDesktopAuthDirs, defaultDesktopAuthPath, defaultOffModelIds, deriveAtRestKey, deriveAtRestKeyId, deriveCatalog, fallbackModelsFor, fetchAtRestKeyPayload, findWorkbuddyAppExecutable, hasEncryptedCredentialFields, imageDefaultFor, inject, isCredentialRejectedError, isEncryptedCredentialError, isEncryptedFieldWrapper, isHeartbeatProcessAlive, isWorkbuddyBundle, knownRegionOf, legacyAttributionRegion, legacyWorkbuddyOwnAuthPath, macosBundleExecutable, macosNestedAppBundles, name, nativeModalityOf, nextRegionEnabled, nextRegionSlots, offDefaultFor, openEncryptedField, outcomeOfFailure, parseCreditMultiplier, parseReasoning, parseRetryAfter, parseUpstreamModel, parseWorkBuddyAuth, prepareChatBody, probeModel, probeRequestBody, probeSucceeded, processStartTimeMs, readAtRestKey, readHostHeartbeat, regionCleared, regionEnabled, regionEnabledOf, regionOf, regionOfProvider, regionOfStatusUrl, regionStateOf, registerWorkBuddyStatusRoute, resolveCredentialRecovery, selectAccountFor, settingsNamespaceOf, toPersistedWorkBuddyModel, unwrapVolatileDeep, withEffectiveOff, withWorkBuddyRegion, workBuddyDisplayName, workBuddyModelInput, workBuddyThinkingLevelMap, workBuddyWebStatus, workbuddyAccountId, workbuddyAppExecutableCandidates, workbuddyHostHeartbeatPath, workbuddyOwnAuthPath, writeHostHeartbeat };