dsh-connect-workbuddy 2.0.5 → 2.0.10
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 +172 -1
- package/README.en.md +7 -2
- package/README.md +7 -2
- package/lib/bin.js +15 -2
- package/lib/client.js +416 -66
- package/lib/{host-heartbeat-BOrNj3pY.js → host-heartbeat-DPbL9oi2.js} +578 -28
- package/lib/index.d.ts +481 -7
- package/lib/index.js +298 -29
- package/package.json +1 -1
|
@@ -1,10 +1,249 @@
|
|
|
1
1
|
import { readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
|
|
2
|
-
import { createHash } from "node:crypto";
|
|
2
|
+
import { createDecipheriv, createHash } from "node:crypto";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { dirname, join } from "node:path";
|
|
5
5
|
import { withFileLock, writeFileAtomic } from "@deepseek-ai/dsh-atomic-write";
|
|
6
6
|
import { resolveDshHome } from "@deepseek-ai/dsh-home-paths";
|
|
7
|
-
import { execFileSync } from "node:child_process";
|
|
7
|
+
import { execFile, execFileSync } from "node:child_process";
|
|
8
|
+
import { existsSync } from "node:fs";
|
|
9
|
+
//#region src/at-rest.ts
|
|
10
|
+
/**
|
|
11
|
+
* WorkBuddy desktop "at-rest" credential decryption.
|
|
12
|
+
*
|
|
13
|
+
* The WorkBuddy desktop app (2.x, Windows builds first) no longer stores
|
|
14
|
+
* `auth.accessToken` / `auth.refreshToken` as plain strings. It now writes a
|
|
15
|
+
* field wrapper:
|
|
16
|
+
*
|
|
17
|
+
* { "$wbEncrypted": 1, "envelope": "<base64 of a JSON envelope>" }
|
|
18
|
+
*
|
|
19
|
+
* where the envelope is `{suite, keyId, nonce, authTag, ciphertext}` for
|
|
20
|
+
* AES-256-GCM with a 12-byte nonce and a 16-byte tag. The authenticated
|
|
21
|
+
* additional data is a length-prefixed transcript over the scheme, suite,
|
|
22
|
+
* keyId and framing, so the ciphertext can only be opened for the exact field
|
|
23
|
+
* shape it was sealed for.
|
|
24
|
+
*
|
|
25
|
+
* The field key itself is NOT a user secret: it is a build-time constant
|
|
26
|
+
* compiled into the app's own Electron native module
|
|
27
|
+
* (`electron_browser_workbuddy_storage`). The app fetches it through
|
|
28
|
+
* `loggerGet()` and hashes the returned base64 STRING (not the decoded bytes)
|
|
29
|
+
* to obtain the 32-byte key; `keyId` is the first 16 hex characters of that
|
|
30
|
+
* key's SHA-256.
|
|
31
|
+
*
|
|
32
|
+
* This module re-derives the same key by asking the installed app for the same
|
|
33
|
+
* payload, and caches it in memory for the process lifetime. Nothing is ever
|
|
34
|
+
* written to disk, and the payload is never logged.
|
|
35
|
+
*
|
|
36
|
+
* @module dsh-connect-workbuddy/at-rest
|
|
37
|
+
*/
|
|
38
|
+
/** Envelope framing names, mapped to the single-byte AAD framing code. */
|
|
39
|
+
const FRAMING_CODE = {
|
|
40
|
+
file: 1,
|
|
41
|
+
field: 2,
|
|
42
|
+
record: 3,
|
|
43
|
+
stream: 4
|
|
44
|
+
};
|
|
45
|
+
/** Standard (symmetric) format identifiers, transcripted into the AAD. */
|
|
46
|
+
const STANDARD_FORMAT_ID = {
|
|
47
|
+
file: "WBEF1",
|
|
48
|
+
field: "WBEV1",
|
|
49
|
+
record: "WBER1",
|
|
50
|
+
stream: "WBES1"
|
|
51
|
+
};
|
|
52
|
+
/** Domain separator the AAD transcript starts with. */
|
|
53
|
+
const AAD_DOMAIN = Buffer.from("WB-AAD\0", "ascii");
|
|
54
|
+
/** Scheme name of the symmetric envelope this module opens. */
|
|
55
|
+
const SYMMETRIC_SCHEME = "sym-v1";
|
|
56
|
+
/** Env override pointing at the WorkBuddy desktop executable. */
|
|
57
|
+
const WORKBUDDY_APP_EXECUTABLE_ENV = "WORKBUDDY_APP_EXECUTABLE";
|
|
58
|
+
/** How long the app is given to answer with its key payload. */
|
|
59
|
+
const KEY_FETCH_TIMEOUT_MS = 1e4;
|
|
60
|
+
/** File name of the WorkBuddy desktop executable, per platform. */
|
|
61
|
+
const APP_EXECUTABLE_NAME = "WorkBuddy.exe";
|
|
62
|
+
function encodeUint32(value) {
|
|
63
|
+
const bytes = Buffer.allocUnsafe(4);
|
|
64
|
+
bytes.writeUInt32BE(value);
|
|
65
|
+
return bytes;
|
|
66
|
+
}
|
|
67
|
+
/** Length-prefixed UTF-8 string: uint32 big-endian length followed by the bytes. */
|
|
68
|
+
function encodeLengthPrefixed(value) {
|
|
69
|
+
const bytes = Buffer.from(value, "utf8");
|
|
70
|
+
return Buffer.concat([encodeUint32(bytes.length), bytes]);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* The authenticated additional data for one `sym-v1` FIELD-framed envelope.
|
|
74
|
+
*
|
|
75
|
+
* Only the field framing is implemented: it is the shape the desktop app uses
|
|
76
|
+
* for credential fields, and it is also the shape that cannot be confused with
|
|
77
|
+
* a whole-file envelope, so an unexpected framing is a parse error rather than
|
|
78
|
+
* a silently wrong transcript.
|
|
79
|
+
*/
|
|
80
|
+
function fieldAad(keyId, suite, scheme = SYMMETRIC_SCHEME) {
|
|
81
|
+
if (!/^[0-9a-f]{16}$/u.test(keyId)) throw new Error(`workbuddy: envelope keyId is malformed`);
|
|
82
|
+
return Buffer.concat([
|
|
83
|
+
AAD_DOMAIN,
|
|
84
|
+
Buffer.from([1]),
|
|
85
|
+
encodeLengthPrefixed(STANDARD_FORMAT_ID["field"]),
|
|
86
|
+
encodeLengthPrefixed(scheme),
|
|
87
|
+
encodeUint32(suite),
|
|
88
|
+
encodeLengthPrefixed(keyId),
|
|
89
|
+
Buffer.from([FRAMING_CODE["field"]]),
|
|
90
|
+
Buffer.from([0]),
|
|
91
|
+
Buffer.from([0])
|
|
92
|
+
]);
|
|
93
|
+
}
|
|
94
|
+
/** Whether a value is the app's encrypted-field wrapper. */
|
|
95
|
+
function isEncryptedFieldWrapper(value) {
|
|
96
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
97
|
+
const wrapper = value;
|
|
98
|
+
const keys = Object.keys(wrapper).sort();
|
|
99
|
+
return keys.length === 2 && keys[0] === "$wbEncrypted" && keys[1] === "envelope" && wrapper["$wbEncrypted"] === 1 && typeof wrapper["envelope"] === "string";
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* The at-rest key id for a derived 32-byte key: the first 16 hex characters of
|
|
103
|
+
* its SHA-256. This is what the envelope's `keyId` is checked against, so a
|
|
104
|
+
* mismatched key fails loudly instead of returning garbage.
|
|
105
|
+
*/
|
|
106
|
+
function deriveAtRestKeyId(key) {
|
|
107
|
+
return createHash("sha256").update(key).digest("hex").slice(0, 16);
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Derive the 32-byte field key from the app's key payload JSON.
|
|
111
|
+
*
|
|
112
|
+
* The app hashes the payload's base64 STRING — not its decoded bytes — so the
|
|
113
|
+
* same spelling is required here; hashing the decoded secret would produce a
|
|
114
|
+
* different key and every field would fail to open.
|
|
115
|
+
*/
|
|
116
|
+
function deriveAtRestKey(payloadJson) {
|
|
117
|
+
let payload;
|
|
118
|
+
try {
|
|
119
|
+
payload = JSON.parse(payloadJson);
|
|
120
|
+
} catch {
|
|
121
|
+
throw new Error("workbuddy: at-rest key payload is not valid JSON");
|
|
122
|
+
}
|
|
123
|
+
if (typeof payload !== "object" || payload === null || Array.isArray(payload)) throw new Error("workbuddy: at-rest key payload is not an object");
|
|
124
|
+
const secret = payload["atRestSecretKey"];
|
|
125
|
+
if (typeof secret !== "string" || secret === "") throw new Error("workbuddy: at-rest key payload carries no atRestSecretKey");
|
|
126
|
+
return createHash("sha256").update(secret, "utf8").digest();
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Open one encrypted field with a derived key and return its plaintext.
|
|
130
|
+
*
|
|
131
|
+
* Throws when the envelope is malformed, belongs to another key, or fails
|
|
132
|
+
* authentication — a GCM tag mismatch is the signal that the transcript or the
|
|
133
|
+
* key is wrong, and it must never degrade into a truncated token.
|
|
134
|
+
*/
|
|
135
|
+
function openEncryptedField(field, key) {
|
|
136
|
+
let envelope;
|
|
137
|
+
try {
|
|
138
|
+
envelope = JSON.parse(Buffer.from(field.envelope, "base64").toString("utf8"));
|
|
139
|
+
} catch {
|
|
140
|
+
throw new Error("workbuddy: encrypted field envelope is not valid JSON");
|
|
141
|
+
}
|
|
142
|
+
if (typeof envelope !== "object" || envelope === null || Array.isArray(envelope)) throw new Error("workbuddy: encrypted field envelope is not an object");
|
|
143
|
+
const record = envelope;
|
|
144
|
+
const suite = record["suite"];
|
|
145
|
+
const keyId = record["keyId"];
|
|
146
|
+
const nonce = record["nonce"];
|
|
147
|
+
const authTag = record["authTag"];
|
|
148
|
+
const ciphertext = record["ciphertext"];
|
|
149
|
+
if (typeof suite !== "number" || typeof keyId !== "string") throw new Error("workbuddy: encrypted field envelope is missing suite or keyId");
|
|
150
|
+
if (typeof nonce !== "string" || typeof authTag !== "string" || typeof ciphertext !== "string") throw new Error("workbuddy: encrypted field envelope is missing nonce, authTag or ciphertext");
|
|
151
|
+
const expectedKeyId = deriveAtRestKeyId(key);
|
|
152
|
+
if (keyId !== expectedKeyId) throw new Error(`workbuddy: encrypted field belongs to key ${keyId}, not the available key ${expectedKeyId}`);
|
|
153
|
+
const decipher = createDecipheriv("aes-256-gcm", key, Buffer.from(nonce, "base64"), { authTagLength: 16 });
|
|
154
|
+
decipher.setAAD(fieldAad(keyId, suite));
|
|
155
|
+
decipher.setAuthTag(Buffer.from(authTag, "base64"));
|
|
156
|
+
return Buffer.concat([decipher.update(Buffer.from(ciphertext, "base64")), decipher.final()]).toString("utf8");
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Candidate paths of the WorkBuddy desktop executable, in probe order.
|
|
160
|
+
*
|
|
161
|
+
* The Windows build is the one that encrypts credentials, so Windows leads;
|
|
162
|
+
* the macOS bundle is listed because the same native module ships there and a
|
|
163
|
+
* future build may enable the policy, and `undefined` entries (an unset env
|
|
164
|
+
* variable) are dropped.
|
|
165
|
+
*/
|
|
166
|
+
function workbuddyAppExecutableCandidates(platform = process.platform, home = homedir(), env = process.env) {
|
|
167
|
+
const candidates = [env[WORKBUDDY_APP_EXECUTABLE_ENV]?.trim()];
|
|
168
|
+
if (platform === "win32") {
|
|
169
|
+
const local = env["LOCALAPPDATA"]?.trim();
|
|
170
|
+
const programFiles = env["ProgramFiles"]?.trim();
|
|
171
|
+
const programFilesX86 = env["ProgramFiles(x86)"]?.trim();
|
|
172
|
+
candidates.push(local === void 0 || local === "" ? void 0 : join(local, "Programs", "WorkBuddy", APP_EXECUTABLE_NAME), programFiles === void 0 || programFiles === "" ? void 0 : join(programFiles, "WorkBuddy", APP_EXECUTABLE_NAME), programFilesX86 === void 0 || programFilesX86 === "" ? void 0 : join(programFilesX86, "WorkBuddy", APP_EXECUTABLE_NAME));
|
|
173
|
+
} else if (platform === "darwin") candidates.push("/Applications/WorkBuddy.app/Contents/MacOS/WorkBuddy", join(home, "Applications", "WorkBuddy.app", "Contents", "MacOS", "WorkBuddy"));
|
|
174
|
+
return candidates.filter((candidate) => candidate !== void 0 && candidate !== "");
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* The first candidate that exists as a file, or undefined when the desktop app
|
|
178
|
+
* is not installed where this platform expects it.
|
|
179
|
+
*/
|
|
180
|
+
function findWorkbuddyAppExecutable(platform = process.platform, home = homedir(), env = process.env) {
|
|
181
|
+
for (const candidate of workbuddyAppExecutableCandidates(platform, home, env)) try {
|
|
182
|
+
if (existsSync(candidate)) return candidate;
|
|
183
|
+
} catch {}
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Ask the installed desktop app for its key payload by running its own binary
|
|
187
|
+
* as plain Node (`ELECTRON_RUN_AS_NODE`) and calling the native binding.
|
|
188
|
+
*
|
|
189
|
+
* The binding is the app's own public surface for this value, so the plugin
|
|
190
|
+
* never has to carry a copy of a build-specific constant: it asks the very
|
|
191
|
+
* build that wrote the file. The child is given no stdin and a hard timeout,
|
|
192
|
+
* and its stdout is the only thing read.
|
|
193
|
+
*/
|
|
194
|
+
function fetchAtRestKeyPayload(executable) {
|
|
195
|
+
return new Promise((resolve, reject) => {
|
|
196
|
+
execFile(executable, ["-e", "try{process.stdout.write(process._linkedBinding('electron_browser_workbuddy_storage').loggerGet())}catch(e){process.exitCode=3;process.stderr.write(String(e&&e.message||e))}"], {
|
|
197
|
+
env: {
|
|
198
|
+
...process.env,
|
|
199
|
+
ELECTRON_RUN_AS_NODE: "1"
|
|
200
|
+
},
|
|
201
|
+
timeout: KEY_FETCH_TIMEOUT_MS,
|
|
202
|
+
windowsHide: true,
|
|
203
|
+
maxBuffer: 1048576
|
|
204
|
+
}, (error, stdout, stderr) => {
|
|
205
|
+
if (error !== null) {
|
|
206
|
+
reject(/* @__PURE__ */ new Error(`workbuddy: the desktop app did not provide its at-rest key (${stderr.trim() || error.message})`));
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
const payload = stdout.trim();
|
|
210
|
+
if (payload === "") {
|
|
211
|
+
reject(/* @__PURE__ */ new Error("workbuddy: the desktop app returned an empty at-rest key payload"));
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
resolve(payload);
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
/** Process-lifetime cache of the derived key; never persisted. */
|
|
219
|
+
let cachedKey;
|
|
220
|
+
let inflightKey;
|
|
221
|
+
/**
|
|
222
|
+
* The desktop app's at-rest field key, or undefined when it cannot be obtained
|
|
223
|
+
* (app not installed, an older build without the native module, or a future
|
|
224
|
+
* build that rotates the payload). Cached after the first success so the app is
|
|
225
|
+
* spawned at most once per process; a failure is retried on the next call,
|
|
226
|
+
* because the user may install or start the app between reads.
|
|
227
|
+
*/
|
|
228
|
+
function readAtRestKey() {
|
|
229
|
+
if (cachedKey !== void 0) return Promise.resolve(cachedKey);
|
|
230
|
+
inflightKey ??= (async () => {
|
|
231
|
+
const executable = findWorkbuddyAppExecutable();
|
|
232
|
+
if (executable === void 0) return void 0;
|
|
233
|
+
const key = deriveAtRestKey(await fetchAtRestKeyPayload(executable));
|
|
234
|
+
cachedKey = key;
|
|
235
|
+
return key;
|
|
236
|
+
})().finally(() => {
|
|
237
|
+
inflightKey = void 0;
|
|
238
|
+
});
|
|
239
|
+
return inflightKey;
|
|
240
|
+
}
|
|
241
|
+
/** Drop the cached key; tests and diagnostics only. */
|
|
242
|
+
function clearAtRestKeyCache() {
|
|
243
|
+
cachedKey = void 0;
|
|
244
|
+
inflightKey = void 0;
|
|
245
|
+
}
|
|
246
|
+
//#endregion
|
|
8
247
|
//#region src/upstream.ts
|
|
9
248
|
const CN_CHAT_BASE = "https://copilot.tencent.com";
|
|
10
249
|
const CN_BILLING_BASE = "https://www.codebuddy.cn";
|
|
@@ -235,13 +474,41 @@ function isGatewayAuthRejection(status, text) {
|
|
|
235
474
|
const lower = text.toLowerCase();
|
|
236
475
|
return lower.includes("openresty") || lower.includes("apisix") || lower.includes("authorization required");
|
|
237
476
|
}
|
|
477
|
+
/** Marker carried by {@link WorkBuddyCredentialRejectedError}; survives bundling. */
|
|
478
|
+
const CREDENTIAL_REJECTED_CODE = "WORKBUDDY_CREDENTIAL_REJECTED";
|
|
479
|
+
/**
|
|
480
|
+
* The upstream refused the CREDENTIAL itself rather than failing the request.
|
|
481
|
+
*
|
|
482
|
+
* This is a distinct, actionable class: the token is not usable and no retry
|
|
483
|
+
* with the same token will help. Callers use it to tell the user what to do
|
|
484
|
+
* about it (switch accounts, or sign in again) instead of showing a raw HTTP
|
|
485
|
+
* error, and the card must never confuse it with a transient upstream fault.
|
|
486
|
+
*
|
|
487
|
+
* Identified by {@link CREDENTIAL_REJECTED_CODE} rather than `instanceof`, so
|
|
488
|
+
* the check keeps working when the caller and the thrower end up in different
|
|
489
|
+
* module instances (bundled host half vs. a test's source import).
|
|
490
|
+
*/
|
|
491
|
+
var WorkBuddyCredentialRejectedError = class extends Error {
|
|
492
|
+
code = CREDENTIAL_REJECTED_CODE;
|
|
493
|
+
/** HTTP status the upstream answered with (401 or 403). */
|
|
494
|
+
status;
|
|
495
|
+
constructor(status) {
|
|
496
|
+
super(`workbuddy: the signed-in credential was rejected by the upstream gateway (http ${status}). The stored token is no longer accepted — most likely a stale credential file from an earlier sign-in was selected. Re-sign in to the WorkBuddy desktop app, then pick that account in the plugin card. Run \`dsh-connect-workbuddy doctor\` to list every discovered credential.`);
|
|
497
|
+
this.name = "WorkBuddyCredentialRejectedError";
|
|
498
|
+
this.status = status;
|
|
499
|
+
}
|
|
500
|
+
};
|
|
501
|
+
/** Whether an error reports that the upstream refused the credential itself. */
|
|
502
|
+
function isCredentialRejectedError(value) {
|
|
503
|
+
return typeof value === "object" && value !== null && value.code === "WORKBUDDY_CREDENTIAL_REJECTED";
|
|
504
|
+
}
|
|
238
505
|
async function readEnvelope(response) {
|
|
239
506
|
const text = await response.text();
|
|
240
507
|
let parsed;
|
|
241
508
|
try {
|
|
242
509
|
parsed = JSON.parse(text);
|
|
243
510
|
} catch {
|
|
244
|
-
if (isGatewayAuthRejection(response.status, text)) throw new
|
|
511
|
+
if (isGatewayAuthRejection(response.status, text)) throw new WorkBuddyCredentialRejectedError(response.status);
|
|
245
512
|
throw new Error(`workbuddy upstream returned non-JSON (http ${response.status}): ${text.slice(0, 160)}`);
|
|
246
513
|
}
|
|
247
514
|
if (typeof parsed !== "object" || parsed === null) throw new Error(`workbuddy upstream returned an unexpected document (http ${response.status})`);
|
|
@@ -252,8 +519,15 @@ async function readEnvelope(response) {
|
|
|
252
519
|
data: "data" in document ? document["data"] : void 0
|
|
253
520
|
};
|
|
254
521
|
}
|
|
255
|
-
/**
|
|
522
|
+
/**
|
|
523
|
+
* Fail an envelope whose business code is non-zero, classified like HTTP errors.
|
|
524
|
+
*
|
|
525
|
+
* A JSON body on a 401/403 is still a credential refusal: the edge answered in
|
|
526
|
+
* the business shape, but the token is just as unusable, so it maps to the same
|
|
527
|
+
* actionable error instead of a generic "client" failure.
|
|
528
|
+
*/
|
|
256
529
|
function envelopeError(status, envelope) {
|
|
530
|
+
if (status === 401 || status === 403) return new WorkBuddyCredentialRejectedError(status);
|
|
257
531
|
const kind = classifyUpstreamError(status, envelope.msg);
|
|
258
532
|
return /* @__PURE__ */ new Error(`workbuddy upstream ${kind} (http ${status}): ${envelope.msg.slice(0, 160)}`);
|
|
259
533
|
}
|
|
@@ -701,15 +975,57 @@ function expiryToMs(value) {
|
|
|
701
975
|
if (value <= 0) return 0;
|
|
702
976
|
return value > 0xe8d4a51000 ? value : value * 1e3;
|
|
703
977
|
}
|
|
704
|
-
|
|
705
|
-
|
|
978
|
+
/**
|
|
979
|
+
* Resolve one string field, transparently opening the desktop app's
|
|
980
|
+
* encrypted-field wrapper when present.
|
|
981
|
+
*
|
|
982
|
+
* Windows builds of the WorkBuddy desktop app store `accessToken` and
|
|
983
|
+
* `refreshToken` as `{$wbEncrypted:1,envelope}` instead of plain strings; a
|
|
984
|
+
* reader that only accepts strings sees no credential at all and reports the
|
|
985
|
+
* account as signed out. The SAME treatment applies to `nickname`: it is
|
|
986
|
+
* encrypted in those builds too, so a strings-only reader silently degrades the
|
|
987
|
+
* account's display name to its uin (a bare number) even after tokens start
|
|
988
|
+
* working — which reads as "the plugin does not know who this is".
|
|
989
|
+
*
|
|
990
|
+
* `key` is undefined when the at-rest key could not be obtained, in which case
|
|
991
|
+
* an encrypted field resolves to undefined rather than to a fabricated value.
|
|
992
|
+
*/
|
|
993
|
+
function credentialField(value, key) {
|
|
994
|
+
if (typeof value === "string") return value;
|
|
995
|
+
if (!isEncryptedFieldWrapper(value)) return void 0;
|
|
996
|
+
if (key === void 0) return void 0;
|
|
997
|
+
return openEncryptedField(value, key);
|
|
998
|
+
}
|
|
999
|
+
/**
|
|
1000
|
+
* An optional field that may legitimately be absent: an absent value, a
|
|
1001
|
+
* non-string that is not a wrapper, and an unopenable wrapper all mean
|
|
1002
|
+
* "unknown", which must not fail the whole document. Only the REQUIRED token
|
|
1003
|
+
* fields treat an unopenable wrapper as fatal (see {@link parseWorkBuddyAuth}).
|
|
1004
|
+
*
|
|
1005
|
+
* `account.phoneNumber` is deliberately never read: it is encrypted in these
|
|
1006
|
+
* builds, and the card's privacy contract admits nickname, masked uin, expiry
|
|
1007
|
+
* and credits only — a phone number is none of the plugin's business.
|
|
1008
|
+
*/
|
|
1009
|
+
function optionalCredentialField(value, key) {
|
|
1010
|
+
if (typeof value === "string") return value === "" ? void 0 : value;
|
|
1011
|
+
if (!isEncryptedFieldWrapper(value)) return void 0;
|
|
1012
|
+
if (key === void 0) return void 0;
|
|
1013
|
+
try {
|
|
1014
|
+
return openEncryptedField(value, key);
|
|
1015
|
+
} catch {
|
|
1016
|
+
return;
|
|
1017
|
+
}
|
|
706
1018
|
}
|
|
707
1019
|
/**
|
|
708
1020
|
* Parse a WorkBuddy auth document in either on-disk shape: the plugin OAuth
|
|
709
1021
|
* nested form `{"auth":{...},"account":{...}}` and the flat panel form.
|
|
710
1022
|
* Returns undefined when the document carries no access token.
|
|
1023
|
+
*
|
|
1024
|
+
* `atRestKey` is required to read documents whose fields are encrypted
|
|
1025
|
+
* (see {@link credentialField}); pass the key obtained from
|
|
1026
|
+
* `readAtRestKey()` when the plain read reports no token.
|
|
711
1027
|
*/
|
|
712
|
-
function parseWorkBuddyAuth(text, filePath) {
|
|
1028
|
+
function parseWorkBuddyAuth(text, filePath, atRestKey) {
|
|
713
1029
|
let parsed;
|
|
714
1030
|
try {
|
|
715
1031
|
parsed = JSON.parse(text);
|
|
@@ -727,21 +1043,28 @@ function parseWorkBuddyAuth(text, filePath) {
|
|
|
727
1043
|
auth = document;
|
|
728
1044
|
identity = document;
|
|
729
1045
|
}
|
|
730
|
-
|
|
731
|
-
|
|
1046
|
+
let accessToken;
|
|
1047
|
+
let refreshToken;
|
|
1048
|
+
try {
|
|
1049
|
+
accessToken = credentialField(auth["accessToken"], atRestKey);
|
|
1050
|
+
refreshToken = credentialField(auth["refreshToken"], atRestKey);
|
|
1051
|
+
} catch {
|
|
1052
|
+
return;
|
|
1053
|
+
}
|
|
1054
|
+
if (accessToken === void 0 || accessToken === "") return void 0;
|
|
732
1055
|
const expiresAtMs = typeof auth["expiresAt"] === "number" ? expiryToMs(auth["expiresAt"]) : 0;
|
|
733
1056
|
const refreshExpiresAtMs = typeof auth["refreshExpiresAt"] === "number" ? expiryToMs(auth["refreshExpiresAt"]) : void 0;
|
|
734
1057
|
const lastRefreshAtMs = typeof auth["lastRefreshTime"] === "number" ? expiryToMs(auth["lastRefreshTime"]) : void 0;
|
|
735
|
-
const enterpriseId =
|
|
736
|
-
const nickname =
|
|
737
|
-
const uin =
|
|
1058
|
+
const enterpriseId = optionalCredentialField(identity["enterpriseId"], atRestKey);
|
|
1059
|
+
const nickname = optionalCredentialField(identity["nickname"], atRestKey);
|
|
1060
|
+
const uin = optionalCredentialField(identity["uin"], atRestKey);
|
|
738
1061
|
return {
|
|
739
1062
|
accessToken,
|
|
740
|
-
refreshToken:
|
|
1063
|
+
refreshToken: refreshToken ?? "",
|
|
741
1064
|
expiresAtMs,
|
|
742
1065
|
...refreshExpiresAtMs === void 0 ? {} : { refreshExpiresAtMs },
|
|
743
|
-
domain:
|
|
744
|
-
uid:
|
|
1066
|
+
domain: optionalCredentialField(auth["domain"], atRestKey) ?? "",
|
|
1067
|
+
uid: optionalCredentialField(identity["uid"], atRestKey) ?? "",
|
|
745
1068
|
...enterpriseId === void 0 ? {} : { enterpriseId },
|
|
746
1069
|
...nickname === void 0 ? {} : { nickname },
|
|
747
1070
|
...uin === void 0 ? {} : { uin },
|
|
@@ -751,6 +1074,30 @@ function parseWorkBuddyAuth(text, filePath) {
|
|
|
751
1074
|
};
|
|
752
1075
|
}
|
|
753
1076
|
/**
|
|
1077
|
+
* Whether a document carries any field the reader must decrypt — the token
|
|
1078
|
+
* fields or the identity fields — i.e. whether reading it needs the at-rest key
|
|
1079
|
+
* at all.
|
|
1080
|
+
*
|
|
1081
|
+
* Deciding this BEFORE asking the app for its key keeps the plain case (macOS
|
|
1082
|
+
* and older Windows builds, and every plugin-owned copy) from paying for a
|
|
1083
|
+
* child process on every credential read. Identity fields are included because
|
|
1084
|
+
* a future build could encrypt the display name while leaving tokens plain;
|
|
1085
|
+
* gating on tokens alone would then silently drop the name again.
|
|
1086
|
+
*/
|
|
1087
|
+
function hasEncryptedCredentialFields(text) {
|
|
1088
|
+
let parsed;
|
|
1089
|
+
try {
|
|
1090
|
+
parsed = JSON.parse(text);
|
|
1091
|
+
} catch {
|
|
1092
|
+
return false;
|
|
1093
|
+
}
|
|
1094
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return false;
|
|
1095
|
+
const document = parsed;
|
|
1096
|
+
const auth = typeof document["auth"] === "object" && document["auth"] !== null ? document["auth"] : document;
|
|
1097
|
+
const identity = typeof document["account"] === "object" && document["account"] !== null ? document["account"] : document;
|
|
1098
|
+
return isEncryptedFieldWrapper(auth["accessToken"]) || isEncryptedFieldWrapper(auth["refreshToken"]) || isEncryptedFieldWrapper(identity["nickname"]) || isEncryptedFieldWrapper(identity["uin"]) || isEncryptedFieldWrapper(identity["uid"]) || isEncryptedFieldWrapper(identity["enterpriseId"]) || isEncryptedFieldWrapper(auth["domain"]);
|
|
1099
|
+
}
|
|
1100
|
+
/**
|
|
754
1101
|
* Rank two candidate files for the same account.
|
|
755
1102
|
*
|
|
756
1103
|
* The live `workbuddy-desktop.info` always wins: it is the app's current
|
|
@@ -837,16 +1184,72 @@ function parseOwnDocument(text, filePath) {
|
|
|
837
1184
|
function isENOENT(error) {
|
|
838
1185
|
return error?.code === "ENOENT";
|
|
839
1186
|
}
|
|
840
|
-
/**
|
|
841
|
-
|
|
1187
|
+
/**
|
|
1188
|
+
* Probe one auth file, reporting WHY it yielded no credential.
|
|
1189
|
+
*
|
|
1190
|
+
* The desktop app encrypts its token fields on Windows builds. The plain read
|
|
1191
|
+
* is tried first and the app is only asked for its at-rest key when the
|
|
1192
|
+
* document actually carries encrypted wrappers, so the common case costs no
|
|
1193
|
+
* child process. `resolveAtRestKey` is injectable so the encrypted path is
|
|
1194
|
+
* testable without a real desktop install.
|
|
1195
|
+
*/
|
|
1196
|
+
async function probeAuthFile(path, resolveAtRestKey) {
|
|
1197
|
+
let text;
|
|
842
1198
|
try {
|
|
843
|
-
|
|
1199
|
+
text = await readFile(path, "utf8");
|
|
844
1200
|
} catch (error) {
|
|
845
|
-
|
|
846
|
-
return
|
|
1201
|
+
const missing = isENOENT(error);
|
|
1202
|
+
return { failure: {
|
|
1203
|
+
reason: missing ? "missing" : "unreadable",
|
|
1204
|
+
...missing ? {} : { message: error instanceof Error ? error.message : String(error) }
|
|
1205
|
+
} };
|
|
1206
|
+
}
|
|
1207
|
+
const plain = parseWorkBuddyAuth(text, path);
|
|
1208
|
+
if (plain !== void 0) return { credential: plain };
|
|
1209
|
+
if (!hasEncryptedCredentialFields(text)) return isParseableJson(text) ? { failure: {
|
|
1210
|
+
reason: "invalid",
|
|
1211
|
+
message: "no access token in the document"
|
|
1212
|
+
} } : { failure: {
|
|
1213
|
+
reason: "invalid",
|
|
1214
|
+
message: "the file is not valid JSON"
|
|
1215
|
+
} };
|
|
1216
|
+
let key;
|
|
1217
|
+
try {
|
|
1218
|
+
key = await resolveAtRestKey();
|
|
1219
|
+
} catch (error) {
|
|
1220
|
+
return { failure: {
|
|
1221
|
+
reason: "encrypted",
|
|
1222
|
+
message: error instanceof Error ? error.message : String(error)
|
|
1223
|
+
} };
|
|
1224
|
+
}
|
|
1225
|
+
if (key === void 0) return { failure: {
|
|
1226
|
+
reason: "encrypted",
|
|
1227
|
+
message: `the credential fields are encrypted and no key was available; the WorkBuddy desktop app must be present (or set ${WORKBUDDY_APP_EXECUTABLE_ENV})`
|
|
1228
|
+
} };
|
|
1229
|
+
const decrypted = parseWorkBuddyAuth(text, path, key);
|
|
1230
|
+
return decrypted === void 0 ? { failure: {
|
|
1231
|
+
reason: "invalid",
|
|
1232
|
+
message: "the encrypted credential fields could not be opened with the desktop app's key"
|
|
1233
|
+
} } : { credential: decrypted };
|
|
1234
|
+
}
|
|
1235
|
+
/** Whether text parses as JSON at all; distinguishes "wrong shape" from "not JSON". */
|
|
1236
|
+
function isParseableJson(text) {
|
|
1237
|
+
try {
|
|
1238
|
+
JSON.parse(text);
|
|
1239
|
+
return true;
|
|
1240
|
+
} catch {
|
|
1241
|
+
return false;
|
|
847
1242
|
}
|
|
848
1243
|
}
|
|
849
1244
|
/**
|
|
1245
|
+
* Read one auth file, tolerating absence and unparsable content. Kept as the
|
|
1246
|
+
* credential-only view for callers that do not need the failure reason.
|
|
1247
|
+
*/
|
|
1248
|
+
async function readAuthFile(path, resolveAtRestKey) {
|
|
1249
|
+
const probe = await probeAuthFile(path, resolveAtRestKey);
|
|
1250
|
+
return "credential" in probe ? probe.credential : void 0;
|
|
1251
|
+
}
|
|
1252
|
+
/**
|
|
850
1253
|
* Read-only credential store with demand-driven refresh and multi-account
|
|
851
1254
|
* discovery.
|
|
852
1255
|
*
|
|
@@ -864,6 +1267,7 @@ var WorkBuddyCredentialStore = class {
|
|
|
864
1267
|
legacyOwnPath;
|
|
865
1268
|
legacyOwnPathExplicit;
|
|
866
1269
|
authDirs;
|
|
1270
|
+
resolveAtRestKey;
|
|
867
1271
|
desktopPathOverride;
|
|
868
1272
|
accountId;
|
|
869
1273
|
inflight;
|
|
@@ -876,6 +1280,7 @@ var WorkBuddyCredentialStore = class {
|
|
|
876
1280
|
this.legacyOwnPathExplicit = options.legacyOwnPath;
|
|
877
1281
|
this.authDirs = options.authDirs;
|
|
878
1282
|
this.desktopPathOverride = options.desktopPath;
|
|
1283
|
+
this.resolveAtRestKey = options.resolveAtRestKey ?? readAtRestKey;
|
|
879
1284
|
}
|
|
880
1285
|
/** Whether a credential's login domain belongs to this store's region. */
|
|
881
1286
|
matchesRegion(domain) {
|
|
@@ -1005,7 +1410,7 @@ var WorkBuddyCredentialStore = class {
|
|
|
1005
1410
|
const files = await this.candidateFiles();
|
|
1006
1411
|
const byId = /* @__PURE__ */ new Map();
|
|
1007
1412
|
for (const file of files) {
|
|
1008
|
-
const credential = await readAuthFile(file);
|
|
1413
|
+
const credential = await readAuthFile(file, this.resolveAtRestKey);
|
|
1009
1414
|
if (credential === void 0 || !this.matchesRegion(credential.domain)) continue;
|
|
1010
1415
|
const id = workbuddyAccountId(credential);
|
|
1011
1416
|
const existing = byId.get(id);
|
|
@@ -1059,8 +1464,7 @@ var WorkBuddyCredentialStore = class {
|
|
|
1059
1464
|
const id = workbuddyAccountId(credential);
|
|
1060
1465
|
return {
|
|
1061
1466
|
id,
|
|
1062
|
-
accountName: credential.nickname ??
|
|
1063
|
-
...credential.uin === void 0 ? {} : { uin: credential.uin },
|
|
1467
|
+
accountName: credential.nickname ?? "",
|
|
1064
1468
|
domain: credential.domain,
|
|
1065
1469
|
source: credential.source,
|
|
1066
1470
|
tokenExpiresAtMs: credential.expiresAtMs,
|
|
@@ -1091,6 +1495,18 @@ var WorkBuddyCredentialStore = class {
|
|
|
1091
1495
|
if (this.accountId === void 0) return this.preferred(credentials);
|
|
1092
1496
|
return credentials.find((credential) => workbuddyAccountId(credential) === this.accountId);
|
|
1093
1497
|
}
|
|
1498
|
+
/**
|
|
1499
|
+
* One account's stored credential by id, WITHOUT consulting or changing the
|
|
1500
|
+
* current selection.
|
|
1501
|
+
*
|
|
1502
|
+
* Exists so a recovery probe can test whether some OTHER local account is
|
|
1503
|
+
* still accepted upstream while a rejected one stays selected: mutating the
|
|
1504
|
+
* live selection to find that out would be exactly the silent account switch
|
|
1505
|
+
* this store refuses to perform (it would bill the wrong account mid-probe).
|
|
1506
|
+
*/
|
|
1507
|
+
async credentialFor(accountId) {
|
|
1508
|
+
return (await this.readAll()).find((credential) => workbuddyAccountId(credential) === accountId);
|
|
1509
|
+
}
|
|
1094
1510
|
/** The credential to send upstream: {@link current}, refreshed on demand. */
|
|
1095
1511
|
async resolve() {
|
|
1096
1512
|
const credential = await this.current();
|
|
@@ -1190,6 +1606,66 @@ var WorkBuddyCredentialStore = class {
|
|
|
1190
1606
|
} catch {}
|
|
1191
1607
|
return false;
|
|
1192
1608
|
}
|
|
1609
|
+
/**
|
|
1610
|
+
* Which paths were probed and why each one yielded no credential.
|
|
1611
|
+
*
|
|
1612
|
+
* Read-only and token-free: it exists so a signed-out card can explain
|
|
1613
|
+
* itself. A bare "not signed in" is undiagnosable on a machine whose layout
|
|
1614
|
+
* differs from the ones this plugin was written against — and on Windows it
|
|
1615
|
+
* is actively misleading, because encrypted token fields need the desktop app
|
|
1616
|
+
* present to be read at all. Only paths this store actually consults are
|
|
1617
|
+
* reported, and only files that failed: one healthy sibling would make the
|
|
1618
|
+
* whole list noise.
|
|
1619
|
+
*/
|
|
1620
|
+
async diagnose() {
|
|
1621
|
+
const candidates = [];
|
|
1622
|
+
for (const path of await this.candidateFiles()) {
|
|
1623
|
+
const probe = await probeAuthFile(path, this.resolveAtRestKey);
|
|
1624
|
+
if ("credential" in probe) {
|
|
1625
|
+
if (!this.matchesRegion(probe.credential.domain)) candidates.push({
|
|
1626
|
+
path,
|
|
1627
|
+
source: "desktop",
|
|
1628
|
+
reason: "wrong-region",
|
|
1629
|
+
message: `holds a ${regionOf(probe.credential.domain)} sign-in, but this tab reads the ${this.region} region`
|
|
1630
|
+
});
|
|
1631
|
+
continue;
|
|
1632
|
+
}
|
|
1633
|
+
candidates.push({
|
|
1634
|
+
path,
|
|
1635
|
+
source: "desktop",
|
|
1636
|
+
...probe.failure
|
|
1637
|
+
});
|
|
1638
|
+
}
|
|
1639
|
+
for (const path of this.ownCandidates()) {
|
|
1640
|
+
let text;
|
|
1641
|
+
try {
|
|
1642
|
+
text = await readFile(path, "utf8");
|
|
1643
|
+
} catch (error) {
|
|
1644
|
+
if (isENOENT(error)) continue;
|
|
1645
|
+
candidates.push({
|
|
1646
|
+
path,
|
|
1647
|
+
source: "dsh",
|
|
1648
|
+
reason: "unreadable",
|
|
1649
|
+
message: error instanceof Error ? error.message : String(error)
|
|
1650
|
+
});
|
|
1651
|
+
continue;
|
|
1652
|
+
}
|
|
1653
|
+
const parsed = parseOwnDocument(text, path);
|
|
1654
|
+
if (parsed === void 0 || !this.matchesRegion(parsed.domain)) {
|
|
1655
|
+
candidates.push({
|
|
1656
|
+
path,
|
|
1657
|
+
source: "dsh",
|
|
1658
|
+
reason: "invalid",
|
|
1659
|
+
message: "not a readable plugin-owned credential"
|
|
1660
|
+
});
|
|
1661
|
+
continue;
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
return {
|
|
1665
|
+
tried: [...await this.candidateFiles(), ...this.ownCandidates()],
|
|
1666
|
+
failures: candidates
|
|
1667
|
+
};
|
|
1668
|
+
}
|
|
1193
1669
|
};
|
|
1194
1670
|
//#endregion
|
|
1195
1671
|
//#region src/catalog.ts
|
|
@@ -1440,10 +1916,36 @@ function applyContextBudgets(catalog, budgets = {}) {
|
|
|
1440
1916
|
function deriveCatalog(catalog, enabled, budgets = {}) {
|
|
1441
1917
|
return applyContextBudgets(enabled.size === 0 ? catalog : catalog.filter((model) => enabled.has(model.id)), budgets);
|
|
1442
1918
|
}
|
|
1443
|
-
/**
|
|
1919
|
+
/**
|
|
1920
|
+
* Mutable catalog shared by the shim's `/v1/models` and the adapter.
|
|
1921
|
+
*
|
|
1922
|
+
* The static fallback exists so an OFFLINE upstream never leaves a provider
|
|
1923
|
+
* empty — but "offline" presumes the region is usable at all. A region with no
|
|
1924
|
+
* local sign-in cannot serve one single request, so seeding it with a roster
|
|
1925
|
+
* puts models in the picker that are guaranteed to 401 (issue #12). The host
|
|
1926
|
+
* distinguishes the two cases through {@link WorkBuddyCatalog.setRegionUsable}.
|
|
1927
|
+
*/
|
|
1444
1928
|
var WorkBuddyCatalog = class {
|
|
1445
1929
|
models;
|
|
1446
1930
|
/**
|
|
1931
|
+
* Whether this region has any local sign-in. Defaults to `true` — the
|
|
1932
|
+
* permissive direction — so a scan that has not run yet (or failed) keeps
|
|
1933
|
+
* serving the fallback rather than blanking a region that may be fine.
|
|
1934
|
+
*/
|
|
1935
|
+
usable = true;
|
|
1936
|
+
/**
|
|
1937
|
+
* Whether the user has switched this region's provider ON. Opt-out: defaults
|
|
1938
|
+
* to `true`, so a config predating this switch (and the pre-region-split flat
|
|
1939
|
+
* fields, which never carry `enabled`) keeps both providers running. Set to
|
|
1940
|
+
* `false` by `setRegionEnabled` to withdraw the region from DSH's model
|
|
1941
|
+
* picker entirely (its adapter route and configurable-provider entry are
|
|
1942
|
+
* pulled by the Host via `AdapterRegistrationHandle.replace([])` /
|
|
1943
|
+
* `DirectoryRegistrationHandle.replace([])`). This is a USER choice, distinct
|
|
1944
|
+
* from {@link usable}: a region the user switched off keeps its catalog,
|
|
1945
|
+
* accounts, and model picks intact and reappears the moment they re-check it.
|
|
1946
|
+
*/
|
|
1947
|
+
enabled = true;
|
|
1948
|
+
/**
|
|
1447
1949
|
* @param region Seeds the static fallback for this region; each region's
|
|
1448
1950
|
* provider must never serve the other region's roster before its first
|
|
1449
1951
|
* live refresh lands.
|
|
@@ -1451,9 +1953,57 @@ var WorkBuddyCatalog = class {
|
|
|
1451
1953
|
constructor(region = "cn") {
|
|
1452
1954
|
this.models = fallbackModelsFor(region);
|
|
1453
1955
|
}
|
|
1454
|
-
/**
|
|
1956
|
+
/**
|
|
1957
|
+
* Current entries; the fallback list until the upstream answer lands.
|
|
1958
|
+
*
|
|
1959
|
+
* Empty while this region has no local sign-in OR the user has switched it
|
|
1960
|
+
* off: the provider stays registered (its row still hosts the settings card
|
|
1961
|
+
* and the account picker), but it advertises nothing to pick. DSH drops empty
|
|
1962
|
+
* provider groups from the picker (`buildModelCatalog` filters
|
|
1963
|
+
* `models.length > 0`), so the group disappears exactly when it would be pure
|
|
1964
|
+
* noise. The two empty causes are independent: `usable` is the automatic
|
|
1965
|
+
* "no account" gate (issue #12), `enabled` is the user's explicit on/off.
|
|
1966
|
+
*/
|
|
1455
1967
|
current() {
|
|
1456
|
-
return this.models;
|
|
1968
|
+
return this.usable && this.enabled ? this.models : [];
|
|
1969
|
+
}
|
|
1970
|
+
/** Whether this region currently advertises any model. */
|
|
1971
|
+
isRegionUsable() {
|
|
1972
|
+
return this.usable;
|
|
1973
|
+
}
|
|
1974
|
+
/** Whether the user has this region's provider switched on. */
|
|
1975
|
+
isRegionEnabled() {
|
|
1976
|
+
return this.enabled;
|
|
1977
|
+
}
|
|
1978
|
+
/**
|
|
1979
|
+
* Record whether this region has a local sign-in.
|
|
1980
|
+
*
|
|
1981
|
+
* Separate from {@link set} on purpose: `set()` carries a live upstream
|
|
1982
|
+
* answer and must never be empty, while "this region has no account" is a
|
|
1983
|
+
* legitimate empty state that must survive across refreshes. Keeping them
|
|
1984
|
+
* apart is what lets the non-empty guard stay strict.
|
|
1985
|
+
*
|
|
1986
|
+
* @returns whether the value changed, so the caller can skip invalidating
|
|
1987
|
+
* its adapter snapshots when nothing moved.
|
|
1988
|
+
*/
|
|
1989
|
+
setRegionUsable(usable) {
|
|
1990
|
+
if (this.usable === usable) return false;
|
|
1991
|
+
this.usable = usable;
|
|
1992
|
+
return true;
|
|
1993
|
+
}
|
|
1994
|
+
/**
|
|
1995
|
+
* Record the user's explicit on/off choice for this region's provider.
|
|
1996
|
+
*
|
|
1997
|
+
* Distinct from {@link setRegionUsable}: that is the automatic "no local
|
|
1998
|
+
* account" gate, while this is a deliberate user action (issue #11-style
|
|
1999
|
+
* region switch). The Host reads the same `enabled` flag through
|
|
2000
|
+
* `regionStateOf(config, region).enabled !== false`, so the two halves never
|
|
2001
|
+
* disagree. Returns whether the value changed.
|
|
2002
|
+
*/
|
|
2003
|
+
setRegionEnabled(enabled) {
|
|
2004
|
+
if (this.enabled === enabled) return false;
|
|
2005
|
+
this.enabled = enabled;
|
|
2006
|
+
return true;
|
|
1457
2007
|
}
|
|
1458
2008
|
/** Replace the list; callers invalidate their adapter snapshot after this. */
|
|
1459
2009
|
set(models) {
|
|
@@ -1464,7 +2014,7 @@ var WorkBuddyCatalog = class {
|
|
|
1464
2014
|
//#endregion
|
|
1465
2015
|
//#region src/version.ts
|
|
1466
2016
|
/** The npm package version this build was produced from. */
|
|
1467
|
-
const WORKBUDDY_CONNECT_VERSION = "2.0.
|
|
2017
|
+
const WORKBUDDY_CONNECT_VERSION = "2.0.10";
|
|
1468
2018
|
//#endregion
|
|
1469
2019
|
//#region src/host-heartbeat.ts
|
|
1470
2020
|
/**
|
|
@@ -1570,4 +2120,4 @@ async function clearHostHeartbeat() {
|
|
|
1570
2120
|
await rm(workbuddyHostHeartbeatPath(), { force: true });
|
|
1571
2121
|
}
|
|
1572
2122
|
//#endregion
|
|
1573
|
-
export {
|
|
2123
|
+
export { isCredentialRejectedError as A, fetchAtRestKeyPayload as B, parseWorkBuddyAuth as C, WorkBuddyCredentialRejectedError as D, CREDENTIAL_REJECTED_CODE as E, regionOf as F, workbuddyAppExecutableCandidates as G, isEncryptedFieldWrapper as H, WORKBUDDY_APP_EXECUTABLE_ENV as I, clearAtRestKeyCache as L, parseReasoning as M, parseUpstreamModel as N, WorkBuddyUpstreamClient as O, prepareChatBody as P, deriveAtRestKey as R, legacyWorkbuddyOwnAuthPath as S, workbuddyOwnAuthPath as T, openEncryptedField as U, findWorkbuddyAppExecutable as V, readAtRestKey as W, authFileName as _, readHostHeartbeat as a, defaultDesktopAuthPath as b, WORKBUDDY_CONNECT_VERSION as c, WorkBuddyCatalog as d, deriveCatalog as f, WorkBuddyCredentialStore as g, WORKBUDDY_AUTH_FILE_ENV as h, processStartTimeMs as i, parseCreditMultiplier as j, classifyUpstreamError as k, FALLBACK_WORKBUDDY_MODELS as l, WORKBUDDY_AUTH_FILENAME as m, clearHostHeartbeat as n, workbuddyHostHeartbeatPath as o, fallbackModelsFor as p, isHeartbeatProcessAlive as r, writeHostHeartbeat as s, WORKBUDDY_HOST_HEARTBEAT_FILENAME as t, FALLBACK_WORKBUDDY_MODELS_GLOBAL as u, defaultDesktopAuthCandidates as v, workbuddyAccountId as w, hasEncryptedCredentialFields as x, defaultDesktopAuthDirs as y, deriveAtRestKeyId as z };
|