dsh-multi-router 0.6.12 → 1.0.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/README.md +2 -430
- package/package.json +5 -81
- package/CHANGELOG.md +0 -243
- package/LICENSE +0 -21
- package/THIRD_PARTY_NOTICES.md +0 -171
- package/cordis.patch.yml +0 -3
- package/lib/accounts.js +0 -19
- package/lib/accounts.js.map +0 -7
- package/lib/api-service.js +0 -11
- package/lib/api-service.js.map +0 -7
- package/lib/chunk-2UM45OXY.js +0 -181
- package/lib/chunk-2UM45OXY.js.map +0 -7
- package/lib/chunk-4L2CBGXN.js +0 -674
- package/lib/chunk-4L2CBGXN.js.map +0 -7
- package/lib/chunk-4V52V5GO.js +0 -105
- package/lib/chunk-4V52V5GO.js.map +0 -7
- package/lib/chunk-DNQPZMPF.js +0 -33
- package/lib/chunk-DNQPZMPF.js.map +0 -7
- package/lib/chunk-FLL3DUVP.js +0 -2017
- package/lib/chunk-FLL3DUVP.js.map +0 -7
- package/lib/chunk-GEBZMIMA.js +0 -287
- package/lib/chunk-GEBZMIMA.js.map +0 -7
- package/lib/chunk-KWXLQJS5.js +0 -1102
- package/lib/chunk-KWXLQJS5.js.map +0 -7
- package/lib/chunk-NZF36O5I.js +0 -5421
- package/lib/chunk-NZF36O5I.js.map +0 -7
- package/lib/chunk-S5KMFAQM.js +0 -50
- package/lib/chunk-S5KMFAQM.js.map +0 -7
- package/lib/chunk-TM5XDMGY.js +0 -332
- package/lib/chunk-TM5XDMGY.js.map +0 -7
- package/lib/chunk-V37VG7MN.js +0 -2101
- package/lib/chunk-V37VG7MN.js.map +0 -7
- package/lib/chunk-WDIT4E2W.js +0 -120
- package/lib/chunk-WDIT4E2W.js.map +0 -7
- package/lib/cli.js +0 -103
- package/lib/cli.js.map +0 -7
- package/lib/client.js +0 -2805
- package/lib/client.js.map +0 -7
- package/lib/clients.js +0 -30
- package/lib/clients.js.map +0 -7
- package/lib/host-v0.6.12.js +0 -414
- package/lib/host.js +0 -414
- package/lib/index.js +0 -414
- package/lib/index.js.map +0 -7
- package/lib/manager.js +0 -27
- package/lib/manager.js.map +0 -7
- package/lib/region-DO3KAV7V.js +0 -15
- package/lib/region-DO3KAV7V.js.map +0 -7
- package/lib/router.js +0 -14
- package/lib/router.js.map +0 -7
- package/lib/shim.js +0 -10
- package/lib/shim.js.map +0 -7
- package/lib/upstream-VP6J7LU2.js +0 -25
- package/lib/upstream-VP6J7LU2.js.map +0 -7
- package/router.example.json +0 -289
- package/upstream-lock.json +0 -17
package/lib/chunk-FLL3DUVP.js
DELETED
|
@@ -1,2017 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
regionOf
|
|
3
|
-
} from "./chunk-KWXLQJS5.js";
|
|
4
|
-
import {
|
|
5
|
-
regionOfCredential,
|
|
6
|
-
regionOfEdition
|
|
7
|
-
} from "./chunk-S5KMFAQM.js";
|
|
8
|
-
|
|
9
|
-
// upstream/workbuddy/src/status-paths.ts
|
|
10
|
-
var WORKBUDDY_STATUS_PATH = "/plugins/dsh-workbuddy-connect/status";
|
|
11
|
-
var WORKBUDDY_PROBE_PATH = "/plugins/dsh-workbuddy-connect/probe";
|
|
12
|
-
var WORKBUDDY_AI_STATUS_PATH = "/plugins/dsh-workbuddy-connect/ai/status";
|
|
13
|
-
var WORKBUDDY_AI_PROBE_PATH = "/plugins/dsh-workbuddy-connect/ai/probe";
|
|
14
|
-
|
|
15
|
-
// upstream/workbuddy/src/variants.ts
|
|
16
|
-
var WORKBUDDY_VARIANTS = [
|
|
17
|
-
{
|
|
18
|
-
id: "workbuddy",
|
|
19
|
-
displayName: "WorkBuddy",
|
|
20
|
-
appName: "WorkBuddy",
|
|
21
|
-
region: "cn",
|
|
22
|
-
env: "WORKBUDDY_AUTH_FILE",
|
|
23
|
-
electron: {
|
|
24
|
-
productName: "WorkBuddy",
|
|
25
|
-
envVar: "WORKBUDDY_ELECTRON_BIN",
|
|
26
|
-
macOS: {
|
|
27
|
-
bundleId: "com.tencent.workbuddy.mac",
|
|
28
|
-
defaultPath: "/Applications/WorkBuddy.app/Contents/MacOS/Electron"
|
|
29
|
-
},
|
|
30
|
-
windows: {
|
|
31
|
-
displayNamePattern: /^WorkBuddy(?:\s+\d+(?:\.\d+)+(?:[-+][0-9A-Za-z.-]+)?)?$/u,
|
|
32
|
-
exeBasename: "workbuddy.exe",
|
|
33
|
-
defaultPathSegments: ["Programs", "WorkBuddy", "WorkBuddy.exe"]
|
|
34
|
-
}
|
|
35
|
-
},
|
|
36
|
-
desktopFilename: "workbuddy-desktop.info",
|
|
37
|
-
ownFilename: ".workbuddy-auth.json",
|
|
38
|
-
probeFilename: ".workbuddy-probe.json",
|
|
39
|
-
catalogFilename: ".workbuddy-catalog.json",
|
|
40
|
-
visibilityFilename: ".workbuddy-model-visibility.json",
|
|
41
|
-
statusPath: WORKBUDDY_STATUS_PATH,
|
|
42
|
-
probePath: WORKBUDDY_PROBE_PATH
|
|
43
|
-
},
|
|
44
|
-
{
|
|
45
|
-
id: "workbuddy-ai",
|
|
46
|
-
displayName: "WorkBuddy AI",
|
|
47
|
-
appName: "WorkBuddy AI",
|
|
48
|
-
region: "global",
|
|
49
|
-
env: "WORKBUDDY_AI_AUTH_FILE",
|
|
50
|
-
electron: {
|
|
51
|
-
productName: "WorkBuddy AI",
|
|
52
|
-
envVar: "WORKBUDDY_AI_ELECTRON_BIN",
|
|
53
|
-
macOS: {
|
|
54
|
-
bundleId: "com.workbuddy.workbuddy-ai",
|
|
55
|
-
defaultPath: "/Applications/WorkBuddy AI.app/Contents/MacOS/Electron"
|
|
56
|
-
},
|
|
57
|
-
windows: {
|
|
58
|
-
// Measured in #60: `WorkBuddy AI 5.6.2`. The CN pattern cannot match
|
|
59
|
-
// this value ("AI" is not a version) and this pattern cannot match
|
|
60
|
-
// `WorkBuddy 5.6.2` (missing the literal " AI"), so the two records
|
|
61
|
-
// never feed each other's discovery.
|
|
62
|
-
displayNamePattern: /^WorkBuddy AI(?:\s+\d+(?:\.\d+)+(?:[-+][0-9A-Za-z.-]+)?)?$/u,
|
|
63
|
-
exeBasename: "workbuddyai.exe"
|
|
64
|
-
}
|
|
65
|
-
},
|
|
66
|
-
desktopFilename: "workbuddy-desktop-ai.info",
|
|
67
|
-
ownFilename: ".workbuddy-ai-auth.json",
|
|
68
|
-
probeFilename: ".workbuddy-ai-probe.json",
|
|
69
|
-
catalogFilename: ".workbuddy-ai-catalog.json",
|
|
70
|
-
visibilityFilename: ".workbuddy-ai-model-visibility.json",
|
|
71
|
-
statusPath: WORKBUDDY_AI_STATUS_PATH,
|
|
72
|
-
probePath: WORKBUDDY_AI_PROBE_PATH
|
|
73
|
-
}
|
|
74
|
-
];
|
|
75
|
-
var CN_VARIANT = WORKBUDDY_VARIANTS[0];
|
|
76
|
-
var AI_VARIANT = WORKBUDDY_VARIANTS[1];
|
|
77
|
-
function electronProfileFor(variant) {
|
|
78
|
-
return variant?.electron ?? (variant?.id === AI_VARIANT.id ? AI_VARIANT : CN_VARIANT).electron;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// upstream/workbuddy/src/desktop-credential-protection.ts
|
|
82
|
-
import { execFile } from "node:child_process";
|
|
83
|
-
import { accessSync, constants, readFileSync, readdirSync, realpathSync, statSync } from "node:fs";
|
|
84
|
-
import { createCipheriv, createDecipheriv, createHash, randomBytes } from "node:crypto";
|
|
85
|
-
import { basename, dirname, join } from "node:path";
|
|
86
|
-
function defaultWorkBuddyElectronPath(product, platform = process.platform) {
|
|
87
|
-
if (platform === "darwin") return product.macOS.defaultPath;
|
|
88
|
-
if (platform !== "win32" || product.windows.defaultPathSegments === void 0) return void 0;
|
|
89
|
-
const localAppData = process.env.LOCALAPPDATA?.trim();
|
|
90
|
-
return localAppData === void 0 || localAppData === "" ? void 0 : join(localAppData, ...product.windows.defaultPathSegments);
|
|
91
|
-
}
|
|
92
|
-
function keyIdsOf(fields) {
|
|
93
|
-
return [...new Set(fields.map((wrapped) => wrapped.envelope.keyId))];
|
|
94
|
-
}
|
|
95
|
-
function parseWrappedField(field, value) {
|
|
96
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
|
|
97
|
-
const wrapped = value;
|
|
98
|
-
if (wrapped["$wbEncrypted"] !== 1 || typeof wrapped["envelope"] !== "string") return void 0;
|
|
99
|
-
let inner;
|
|
100
|
-
try {
|
|
101
|
-
inner = JSON.parse(Buffer.from(wrapped["envelope"], "base64").toString("utf8"));
|
|
102
|
-
} catch {
|
|
103
|
-
return void 0;
|
|
104
|
-
}
|
|
105
|
-
if (typeof inner !== "object" || inner === null || Array.isArray(inner)) return void 0;
|
|
106
|
-
const parts = inner;
|
|
107
|
-
const nonce = parseBase64(parts["nonce"], 12);
|
|
108
|
-
const authTag = parseBase64(parts["authTag"], 16);
|
|
109
|
-
const ciphertext = parseBase64(parts["ciphertext"]);
|
|
110
|
-
if (nonce === void 0 || authTag === void 0 || ciphertext === void 0) return void 0;
|
|
111
|
-
if (typeof parts["suite"] !== "number" || !Number.isInteger(parts["suite"])) return void 0;
|
|
112
|
-
if (parts["suite"] !== 1) return void 0;
|
|
113
|
-
if (typeof parts["keyId"] !== "string" || !/^[0-9a-f]{16}$/u.test(parts["keyId"])) return void 0;
|
|
114
|
-
return {
|
|
115
|
-
field,
|
|
116
|
-
envelope: {
|
|
117
|
-
suite: parts["suite"],
|
|
118
|
-
keyId: parts["keyId"],
|
|
119
|
-
nonce,
|
|
120
|
-
authTag,
|
|
121
|
-
ciphertext
|
|
122
|
-
}
|
|
123
|
-
};
|
|
124
|
-
}
|
|
125
|
-
function parseBase64(value, length) {
|
|
126
|
-
if (typeof value !== "string" || value === "") return void 0;
|
|
127
|
-
let decoded;
|
|
128
|
-
try {
|
|
129
|
-
decoded = Buffer.from(value, "base64");
|
|
130
|
-
} catch {
|
|
131
|
-
return void 0;
|
|
132
|
-
}
|
|
133
|
-
if (decoded.length === 0 || decoded.toString("base64").replace(/=+$/u, "") !== value.replace(/=+$/u, "")) return void 0;
|
|
134
|
-
return length === void 0 || decoded.length === length ? decoded : void 0;
|
|
135
|
-
}
|
|
136
|
-
var AUTH_FIELDS = ["accessToken", "refreshToken"];
|
|
137
|
-
function classifyDesktopAuthDocument(text) {
|
|
138
|
-
if (text.trim() === "") return { format: "absent" };
|
|
139
|
-
let parsed;
|
|
140
|
-
try {
|
|
141
|
-
parsed = JSON.parse(text);
|
|
142
|
-
} catch {
|
|
143
|
-
return { format: "unrecognized" };
|
|
144
|
-
}
|
|
145
|
-
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return { format: "unrecognized" };
|
|
146
|
-
const document = parsed;
|
|
147
|
-
const auth = typeof document["auth"] === "object" && document["auth"] !== null ? document["auth"] : document;
|
|
148
|
-
const fields = [];
|
|
149
|
-
for (const field of AUTH_FIELDS) {
|
|
150
|
-
const value = auth[field];
|
|
151
|
-
if (typeof value === "string") continue;
|
|
152
|
-
const wrapped = parseWrappedField(field, value);
|
|
153
|
-
if (wrapped === void 0 && value !== void 0) return { format: "unrecognized" };
|
|
154
|
-
if (wrapped !== void 0) fields.push(wrapped);
|
|
155
|
-
}
|
|
156
|
-
if (fields.length === 0) return { format: "plaintext" };
|
|
157
|
-
return { format: "encrypted", wrapped: { document, fields } };
|
|
158
|
-
}
|
|
159
|
-
function unwrapDesktopAuthDocument(classification, openField) {
|
|
160
|
-
const wrapped = classification.wrapped;
|
|
161
|
-
const rebuilt = structuredClone(wrapped.document);
|
|
162
|
-
const auth = typeof rebuilt["auth"] === "object" && rebuilt["auth"] !== null ? rebuilt["auth"] : rebuilt;
|
|
163
|
-
for (const field of wrapped.fields) {
|
|
164
|
-
auth[field.field] = openField(field);
|
|
165
|
-
}
|
|
166
|
-
return JSON.stringify(rebuilt);
|
|
167
|
-
}
|
|
168
|
-
function buildAuthenticatedContextAad(keyId, suite) {
|
|
169
|
-
const prefix = Buffer.from("WB-AAD\0", "ascii");
|
|
170
|
-
const lengthPrefixed = (value) => {
|
|
171
|
-
const bytes = Buffer.from(value, "utf8");
|
|
172
|
-
const header = Buffer.allocUnsafe(4);
|
|
173
|
-
header.writeUInt32BE(bytes.length);
|
|
174
|
-
return Buffer.concat([header, bytes]);
|
|
175
|
-
};
|
|
176
|
-
const suiteBytes = Buffer.allocUnsafe(4);
|
|
177
|
-
suiteBytes.writeUInt32BE(suite);
|
|
178
|
-
return Buffer.concat([
|
|
179
|
-
prefix,
|
|
180
|
-
Buffer.from([1]),
|
|
181
|
-
lengthPrefixed("WBEV1"),
|
|
182
|
-
lengthPrefixed("sym-v1"),
|
|
183
|
-
suiteBytes,
|
|
184
|
-
lengthPrefixed(keyId),
|
|
185
|
-
Buffer.from([2]),
|
|
186
|
-
Buffer.from([0]),
|
|
187
|
-
Buffer.from([0])
|
|
188
|
-
]);
|
|
189
|
-
}
|
|
190
|
-
function openAuthField(key, envelope) {
|
|
191
|
-
try {
|
|
192
|
-
const decipher = createDecipheriv("aes-256-gcm", key, envelope.nonce, { authTagLength: 16 });
|
|
193
|
-
decipher.setAAD(buildAuthenticatedContextAad(envelope.keyId, envelope.suite));
|
|
194
|
-
decipher.setAuthTag(envelope.authTag);
|
|
195
|
-
return Buffer.concat([decipher.update(envelope.ciphertext), decipher.final()]).toString("utf8");
|
|
196
|
-
} catch {
|
|
197
|
-
return void 0;
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
function parseAtRestPayload(text) {
|
|
201
|
-
let parsed;
|
|
202
|
-
try {
|
|
203
|
-
parsed = JSON.parse(text);
|
|
204
|
-
} catch {
|
|
205
|
-
return void 0;
|
|
206
|
-
}
|
|
207
|
-
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return void 0;
|
|
208
|
-
const payload = parsed;
|
|
209
|
-
if (payload["version"] !== 1) return void 0;
|
|
210
|
-
const secret = payload["atRestSecretKey"];
|
|
211
|
-
if (typeof secret !== "string" || secret === "") return void 0;
|
|
212
|
-
let decoded;
|
|
213
|
-
try {
|
|
214
|
-
decoded = Buffer.from(secret, "base64");
|
|
215
|
-
} catch {
|
|
216
|
-
return void 0;
|
|
217
|
-
}
|
|
218
|
-
if (decoded.length !== 32) return void 0;
|
|
219
|
-
if (decoded.toString("base64") !== secret) return void 0;
|
|
220
|
-
if (decoded.every((byte) => byte === 0)) return void 0;
|
|
221
|
-
return { atRestSecretKey: secret };
|
|
222
|
-
}
|
|
223
|
-
function deriveProtectorKey(secret) {
|
|
224
|
-
return createHash("sha256").update(secret, "utf8").digest();
|
|
225
|
-
}
|
|
226
|
-
function electronDiscoveryFor(platform = process.platform) {
|
|
227
|
-
if (platform === "darwin") return "macos-workbuddy";
|
|
228
|
-
if (platform === "win32") return "windows-workbuddy";
|
|
229
|
-
return "none";
|
|
230
|
-
}
|
|
231
|
-
function atRestKeyProviderFor(variant) {
|
|
232
|
-
return new WorkBuddyAtRestKeyProvider({
|
|
233
|
-
product: electronProfileFor(variant),
|
|
234
|
-
discovery: electronDiscoveryFor()
|
|
235
|
-
});
|
|
236
|
-
}
|
|
237
|
-
var MDFIND_BIN = "/usr/bin/mdfind";
|
|
238
|
-
var PLUTIL_BIN = "/usr/bin/plutil";
|
|
239
|
-
var WINDOWS_REGISTRY_ROOTS = [
|
|
240
|
-
"HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall",
|
|
241
|
-
"HKLM\\Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall",
|
|
242
|
-
"HKLM\\Software\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall"
|
|
243
|
-
];
|
|
244
|
-
var WINDOWS_REGISTRY_OUTPUT_MAX_BYTES = 1024 * 1024;
|
|
245
|
-
var WINDOWS_ELECTRON_VERSION_PATTERN = /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/u;
|
|
246
|
-
var WINDOWS_REGISTRY_RELATIVE_POWERSHELL = "System32\\WindowsPowerShell\\v1.0\\powershell.exe";
|
|
247
|
-
var WINDOWS_POWERSHELL_ROOT_ABSENT = 2;
|
|
248
|
-
var WINDOWS_POWERSHELL_ROOT_UNREADABLE = 3;
|
|
249
|
-
var WINDOWS_POWERSHELL_QUERY_SCRIPT = [
|
|
250
|
-
"param([string]$Root)",
|
|
251
|
-
"[Console]::OutputEncoding=[Text.Encoding]::UTF8;",
|
|
252
|
-
'$ErrorActionPreference="Stop";',
|
|
253
|
-
'$path="Registry::"+$Root;',
|
|
254
|
-
'if (-not (Test-Path -LiteralPath $path)) { [Console]::Error.Write("root absent"); exit 2 };',
|
|
255
|
-
'try { $k=Get-Item -LiteralPath $path -ErrorAction Stop } catch { [Console]::Error.Write("root unreadable"); exit 3 };',
|
|
256
|
-
"$out=@();",
|
|
257
|
-
"foreach ($n in $k.GetSubKeyNames()) {",
|
|
258
|
-
' try { $p=$k.OpenSubKey($n); if ($null -eq $p) { throw "null" } } catch { [Console]::Error.Write("subkey unreadable"); exit 3 };',
|
|
259
|
-
' $d=$p.GetValue("DisplayName");$i=$p.GetValue("DisplayIcon");',
|
|
260
|
-
" if ($null -ne $d -or $null -ne $i) { $out += [pscustomobject]@{ displayName=[string]$d; displayIcon=[string]$i } }",
|
|
261
|
-
"};",
|
|
262
|
-
"ConvertTo-Json -InputObject @($out) -Compress -Depth 3"
|
|
263
|
-
].join(" ");
|
|
264
|
-
var WINDOWS_POWERSHELL_QUERY_COMMAND = `& { ${WINDOWS_POWERSHELL_QUERY_SCRIPT} }`;
|
|
265
|
-
var WINDOWS_REGISTRY_ROOT_PATTERN = /^HK(?:LM|CU)(?:\\[A-Za-z0-9._-]+)+$/u;
|
|
266
|
-
var WORKBUDDY_DISCOVERY_STEP_TIMEOUT_MS = 3e3;
|
|
267
|
-
var WORKBUDDY_DISCOVERY_BUDGET_MS = 1e4;
|
|
268
|
-
var WINDOWS_REGISTRY_STEP_TIMEOUT_MS = 1e4;
|
|
269
|
-
var MDFIND_MAX_OUTPUT_BYTES = 1024 * 1024;
|
|
270
|
-
var PLUTIL_MAX_OUTPUT_BYTES = 64 * 1024;
|
|
271
|
-
var DiscoveryIncompleteError = class extends Error {
|
|
272
|
-
};
|
|
273
|
-
function workBuddyDiscoveryTools(bundleId) {
|
|
274
|
-
const runTool = (bin, args, maxBytes, signal) => new Promise((resolve, reject) => {
|
|
275
|
-
if (signal.aborted) {
|
|
276
|
-
reject(new DiscoveryIncompleteError(`${bin} was not started: the discovery budget was already spent`));
|
|
277
|
-
return;
|
|
278
|
-
}
|
|
279
|
-
let settled = false;
|
|
280
|
-
const child = execFile(bin, [...args], { maxBuffer: maxBytes, timeout: WORKBUDDY_DISCOVERY_STEP_TIMEOUT_MS }, (error, stdout) => {
|
|
281
|
-
if (settled) return;
|
|
282
|
-
settled = true;
|
|
283
|
-
if (error !== null && error !== void 0) {
|
|
284
|
-
reject(new DiscoveryIncompleteError(`${bin} could not complete (${error.killed === true ? "timed out" : String(error.code ?? "unavailable")})`));
|
|
285
|
-
return;
|
|
286
|
-
}
|
|
287
|
-
resolve(stdout);
|
|
288
|
-
});
|
|
289
|
-
const abort = () => {
|
|
290
|
-
if (settled) return;
|
|
291
|
-
settled = true;
|
|
292
|
-
child.kill();
|
|
293
|
-
reject(new DiscoveryIncompleteError(`${bin} was abandoned: the discovery budget was spent`));
|
|
294
|
-
};
|
|
295
|
-
signal.addEventListener("abort", abort, { once: true });
|
|
296
|
-
child.on("close", () => {
|
|
297
|
-
signal.removeEventListener("abort", abort);
|
|
298
|
-
});
|
|
299
|
-
});
|
|
300
|
-
return {
|
|
301
|
-
findApps: async (signal) => {
|
|
302
|
-
const out = await runTool(
|
|
303
|
-
MDFIND_BIN,
|
|
304
|
-
[`kMDItemCFBundleIdentifier == '${bundleId}'`],
|
|
305
|
-
MDFIND_MAX_OUTPUT_BYTES,
|
|
306
|
-
signal
|
|
307
|
-
);
|
|
308
|
-
return out.split("\n").map((line) => line.trim()).filter((line) => line.endsWith(".app"));
|
|
309
|
-
},
|
|
310
|
-
bundleIdentifier: async (bundlePath, signal) => {
|
|
311
|
-
try {
|
|
312
|
-
const out = await runTool(
|
|
313
|
-
PLUTIL_BIN,
|
|
314
|
-
["-extract", "CFBundleIdentifier", "raw", "-o", "-", join(bundlePath, "Contents", "Info.plist")],
|
|
315
|
-
PLUTIL_MAX_OUTPUT_BYTES,
|
|
316
|
-
signal
|
|
317
|
-
);
|
|
318
|
-
return out.trim();
|
|
319
|
-
} catch {
|
|
320
|
-
return void 0;
|
|
321
|
-
}
|
|
322
|
-
},
|
|
323
|
-
bundleVersion: async (bundlePath, signal) => {
|
|
324
|
-
try {
|
|
325
|
-
const out = await runTool(
|
|
326
|
-
PLUTIL_BIN,
|
|
327
|
-
["-extract", "CFBundleShortVersionString", "raw", "-o", "-", join(bundlePath, "Contents", "Info.plist")],
|
|
328
|
-
PLUTIL_MAX_OUTPUT_BYTES,
|
|
329
|
-
signal
|
|
330
|
-
);
|
|
331
|
-
const version = out.trim();
|
|
332
|
-
return version === "" ? void 0 : version;
|
|
333
|
-
} catch {
|
|
334
|
-
return void 0;
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
};
|
|
338
|
-
}
|
|
339
|
-
function workBuddyWindowsDiscoveryTools() {
|
|
340
|
-
const systemRoot = process.env.SystemRoot?.trim();
|
|
341
|
-
const regPath = systemRoot === void 0 || systemRoot === "" ? void 0 : join(systemRoot, "System32", "reg.exe");
|
|
342
|
-
const powershellPath = systemRoot === void 0 || systemRoot === "" ? void 0 : join(systemRoot, WINDOWS_REGISTRY_RELATIVE_POWERSHELL);
|
|
343
|
-
const runTool = (root, signal) => new Promise((resolve, reject) => {
|
|
344
|
-
if (regPath === void 0) {
|
|
345
|
-
reject(new DiscoveryIncompleteError("SystemRoot is not configured"));
|
|
346
|
-
return;
|
|
347
|
-
}
|
|
348
|
-
if (signal.aborted) {
|
|
349
|
-
reject(new DiscoveryIncompleteError("reg.exe was not started: the discovery budget was already spent"));
|
|
350
|
-
return;
|
|
351
|
-
}
|
|
352
|
-
let settled = false;
|
|
353
|
-
const child = execFile(regPath, ["query", root, "/s"], {
|
|
354
|
-
maxBuffer: WINDOWS_REGISTRY_OUTPUT_MAX_BYTES,
|
|
355
|
-
timeout: WINDOWS_REGISTRY_STEP_TIMEOUT_MS,
|
|
356
|
-
windowsHide: true,
|
|
357
|
-
// Raw bytes, not a decode: whether this output is ambiguous is decided
|
|
358
|
-
// from the bytes themselves (#88), so Node must not pick an encoding.
|
|
359
|
-
encoding: "buffer"
|
|
360
|
-
}, (error, stdout, stderr) => {
|
|
361
|
-
if (settled) return;
|
|
362
|
-
settled = true;
|
|
363
|
-
if (error !== null && error !== void 0) {
|
|
364
|
-
if (error.killed !== true && (error.code === 1 || error.code === "1") && windowsRegistryKeyMissing(stderr.toString("utf8"))) {
|
|
365
|
-
resolve(Buffer.alloc(0));
|
|
366
|
-
return;
|
|
367
|
-
}
|
|
368
|
-
reject(new DiscoveryIncompleteError(`reg.exe could not complete (${error.killed === true ? "timed out" : String(error.code ?? "unavailable")})`));
|
|
369
|
-
return;
|
|
370
|
-
}
|
|
371
|
-
resolve(stdout);
|
|
372
|
-
});
|
|
373
|
-
const abort = () => {
|
|
374
|
-
if (settled) return;
|
|
375
|
-
settled = true;
|
|
376
|
-
child.kill();
|
|
377
|
-
reject(new DiscoveryIncompleteError("reg.exe was abandoned: the discovery budget was spent"));
|
|
378
|
-
};
|
|
379
|
-
signal.addEventListener("abort", abort, { once: true });
|
|
380
|
-
child.on("close", () => {
|
|
381
|
-
signal.removeEventListener("abort", abort);
|
|
382
|
-
});
|
|
383
|
-
});
|
|
384
|
-
const runFallback = (root, signal) => new Promise((resolve, reject) => {
|
|
385
|
-
if (!WINDOWS_REGISTRY_ROOT_PATTERN.test(root)) {
|
|
386
|
-
reject(new DiscoveryIncompleteError("the registry root was not a plain HKLM/HKCU path this tool may query"));
|
|
387
|
-
return;
|
|
388
|
-
}
|
|
389
|
-
if (powershellPath === void 0) {
|
|
390
|
-
reject(new DiscoveryIncompleteError("SystemRoot is not configured"));
|
|
391
|
-
return;
|
|
392
|
-
}
|
|
393
|
-
if (signal.aborted) {
|
|
394
|
-
reject(new DiscoveryIncompleteError("powershell.exe was not started: the discovery budget was already spent"));
|
|
395
|
-
return;
|
|
396
|
-
}
|
|
397
|
-
let settled = false;
|
|
398
|
-
const child = execFile(powershellPath, [
|
|
399
|
-
"-NoProfile",
|
|
400
|
-
"-NonInteractive",
|
|
401
|
-
"-Command",
|
|
402
|
-
WINDOWS_POWERSHELL_QUERY_COMMAND,
|
|
403
|
-
"-Root",
|
|
404
|
-
root
|
|
405
|
-
], {
|
|
406
|
-
maxBuffer: WINDOWS_REGISTRY_OUTPUT_MAX_BYTES,
|
|
407
|
-
timeout: WINDOWS_REGISTRY_STEP_TIMEOUT_MS,
|
|
408
|
-
windowsHide: true,
|
|
409
|
-
encoding: "utf8"
|
|
410
|
-
}, (error, stdout) => {
|
|
411
|
-
if (settled) return;
|
|
412
|
-
settled = true;
|
|
413
|
-
if (error !== null && error !== void 0) {
|
|
414
|
-
const code = error.killed === true ? void 0 : Number(error.code);
|
|
415
|
-
if (code === WINDOWS_POWERSHELL_ROOT_ABSENT) {
|
|
416
|
-
resolve("");
|
|
417
|
-
return;
|
|
418
|
-
}
|
|
419
|
-
if (code === WINDOWS_POWERSHELL_ROOT_UNREADABLE) {
|
|
420
|
-
reject(new DiscoveryIncompleteError(`powershell.exe could not read the registry root ${root}`));
|
|
421
|
-
return;
|
|
422
|
-
}
|
|
423
|
-
reject(new DiscoveryIncompleteError(`powershell.exe could not complete (${error.killed === true ? "timed out" : String(error.code ?? "unavailable")})`));
|
|
424
|
-
return;
|
|
425
|
-
}
|
|
426
|
-
const records = parseWindowsUninstallJson(stdout);
|
|
427
|
-
if (records === void 0) {
|
|
428
|
-
reject(new DiscoveryIncompleteError("powershell.exe output was not the expected registry document"));
|
|
429
|
-
return;
|
|
430
|
-
}
|
|
431
|
-
resolve(records);
|
|
432
|
-
});
|
|
433
|
-
const abort = () => {
|
|
434
|
-
if (settled) return;
|
|
435
|
-
settled = true;
|
|
436
|
-
child.kill();
|
|
437
|
-
reject(new DiscoveryIncompleteError("powershell.exe was abandoned: the discovery budget was spent"));
|
|
438
|
-
};
|
|
439
|
-
signal.addEventListener("abort", abort, { once: true });
|
|
440
|
-
child.on("close", () => {
|
|
441
|
-
signal.removeEventListener("abort", abort);
|
|
442
|
-
});
|
|
443
|
-
});
|
|
444
|
-
return {
|
|
445
|
-
queryUninstallRoot: async (root, signal) => {
|
|
446
|
-
const raw = await runTool(root, signal);
|
|
447
|
-
if (isAsciiOnly(raw)) return raw.toString("utf8");
|
|
448
|
-
return await runFallback(root, signal);
|
|
449
|
-
}
|
|
450
|
-
};
|
|
451
|
-
}
|
|
452
|
-
function isAsciiOnly(bytes) {
|
|
453
|
-
for (const byte of bytes) {
|
|
454
|
-
if (byte >= 128) return false;
|
|
455
|
-
}
|
|
456
|
-
return true;
|
|
457
|
-
}
|
|
458
|
-
function parseWindowsUninstallJson(output) {
|
|
459
|
-
let document;
|
|
460
|
-
try {
|
|
461
|
-
document = JSON.parse(output);
|
|
462
|
-
} catch {
|
|
463
|
-
return void 0;
|
|
464
|
-
}
|
|
465
|
-
const rows = Array.isArray(document) ? document : [document];
|
|
466
|
-
const lines = [];
|
|
467
|
-
for (const [index, row] of rows.entries()) {
|
|
468
|
-
if (typeof row !== "object" || row === null) continue;
|
|
469
|
-
const record = row;
|
|
470
|
-
const displayName = record["displayName"];
|
|
471
|
-
const displayIcon = record["displayIcon"];
|
|
472
|
-
if (typeof displayName !== "string" && typeof displayIcon !== "string") continue;
|
|
473
|
-
lines.push(`HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{PS-${String(index)}}`);
|
|
474
|
-
if (typeof displayName === "string") lines.push(` DisplayName REG_SZ ${displayName}`);
|
|
475
|
-
if (typeof displayIcon === "string") lines.push(` DisplayIcon REG_SZ ${displayIcon}`);
|
|
476
|
-
}
|
|
477
|
-
return lines.join("\r\n");
|
|
478
|
-
}
|
|
479
|
-
var WorkBuddyAtRestKeyProvider = class {
|
|
480
|
-
/**
|
|
481
|
-
* The explicit binary, when one was configured. `undefined` here means "the
|
|
482
|
-
* caller did not name one", which is what lets discovery run — an explicit
|
|
483
|
-
* path that turns out to be unusable is an error, never a reason to look for
|
|
484
|
-
* a different app.
|
|
485
|
-
*/
|
|
486
|
-
explicitPath;
|
|
487
|
-
product;
|
|
488
|
-
defaultPath;
|
|
489
|
-
discovery;
|
|
490
|
-
tools;
|
|
491
|
-
windowsTools;
|
|
492
|
-
platform;
|
|
493
|
-
discoveryBudgetMs;
|
|
494
|
-
timeoutMs;
|
|
495
|
-
source;
|
|
496
|
-
spawnHelper;
|
|
497
|
-
/**
|
|
498
|
-
* The path discovery settled on, cached only on success. A failure leaves
|
|
499
|
-
* this unset so the next attempt tries again — the user may install or move
|
|
500
|
-
* the app without restarting DSH.
|
|
501
|
-
*/
|
|
502
|
-
discoveredPath;
|
|
503
|
-
cache;
|
|
504
|
-
inflight;
|
|
505
|
-
constructor(options) {
|
|
506
|
-
this.product = options.product;
|
|
507
|
-
const fromEnv = process.env[options.product.envVar]?.trim();
|
|
508
|
-
const envPath = fromEnv === void 0 || fromEnv === "" ? void 0 : fromEnv;
|
|
509
|
-
this.explicitPath = options.electronPath ?? envPath;
|
|
510
|
-
this.discovery = options.discovery ?? "none";
|
|
511
|
-
this.platform = options.platform ?? process.platform;
|
|
512
|
-
this.defaultPath = this.discovery === "none" ? void 0 : options.defaultElectronPath === void 0 ? defaultWorkBuddyElectronPath(options.product, this.platform) : options.defaultElectronPath ?? void 0;
|
|
513
|
-
this.tools = options.tools ?? workBuddyDiscoveryTools(options.product.macOS.bundleId);
|
|
514
|
-
this.windowsTools = options.windowsTools ?? workBuddyWindowsDiscoveryTools();
|
|
515
|
-
this.discoveryBudgetMs = options.discoveryBudgetMs ?? WORKBUDDY_DISCOVERY_BUDGET_MS;
|
|
516
|
-
this.timeoutMs = options.timeoutMs ?? 1e4;
|
|
517
|
-
this.spawnHelper = options.spawnHelper ?? ((path) => this.spawnAt(path));
|
|
518
|
-
this.source = options.source ?? (() => this.spawnPayload());
|
|
519
|
-
}
|
|
520
|
-
/**
|
|
521
|
-
* The binary the default helper would use, for diagnostics.
|
|
522
|
-
*
|
|
523
|
-
* Reports a *discovery result* once one exists, so diagnostics describe what
|
|
524
|
-
* would actually run rather than the default that was bypassed. Discovery
|
|
525
|
-
* itself stays in {@link resolveElectronPath}: this accessor never triggers a
|
|
526
|
-
* search (the constructor must remain I/O-free, and callers may ask before
|
|
527
|
-
* any resolution has happened).
|
|
528
|
-
*/
|
|
529
|
-
helperPath() {
|
|
530
|
-
if (this.explicitPath !== void 0) return this.explicitPath;
|
|
531
|
-
if (this.discovery === "none") return void 0;
|
|
532
|
-
return this.discoveredPath ?? this.defaultPath;
|
|
533
|
-
}
|
|
534
|
-
/**
|
|
535
|
-
* A protector key matching one of the requested envelope key ids. The first
|
|
536
|
-
* id the cache answers wins; otherwise one spawn resolves the current key,
|
|
537
|
-
* which must match a request — a mismatch means the envelopes were sealed by
|
|
538
|
-
* a different install than the one this machine now runs, and no key we can
|
|
539
|
-
* reach will open them.
|
|
540
|
-
*/
|
|
541
|
-
async protectorKeyFor(requested) {
|
|
542
|
-
if (requested.length === 0) {
|
|
543
|
-
throw new WorkBuddyElectronPathError(
|
|
544
|
-
"encrypted-credential-unreadable",
|
|
545
|
-
"encrypted desktop credential carries no key ids"
|
|
546
|
-
);
|
|
547
|
-
}
|
|
548
|
-
const cached = this.cache;
|
|
549
|
-
if (cached !== void 0 && requested.includes(cached.keyId)) return cached.key;
|
|
550
|
-
this.inflight ??= this.source().then((text) => this.ingest(text)).finally(() => {
|
|
551
|
-
this.inflight = void 0;
|
|
552
|
-
});
|
|
553
|
-
const resolved = await this.inflight;
|
|
554
|
-
if (!requested.includes(resolved.keyId)) {
|
|
555
|
-
throw new WorkBuddyElectronPathError(
|
|
556
|
-
"encrypted-credential-unreadable",
|
|
557
|
-
`WorkBuddy's current at-rest key (id ${resolved.keyId}) does not match the credential's envelope (id ${requested.join(" or ")}); the desktop credential was sealed by a different WorkBuddy installation`
|
|
558
|
-
);
|
|
559
|
-
}
|
|
560
|
-
return resolved.key;
|
|
561
|
-
}
|
|
562
|
-
ingest(text) {
|
|
563
|
-
const payload = parseAtRestPayload(text);
|
|
564
|
-
if (payload === void 0) {
|
|
565
|
-
throw new WorkBuddyElectronPathError(
|
|
566
|
-
"encrypted-credential-unreadable",
|
|
567
|
-
"WorkBuddy key helper returned an unusable at-rest payload (expected {version:1, atRestSecretKey})"
|
|
568
|
-
);
|
|
569
|
-
}
|
|
570
|
-
const key = deriveProtectorKey(payload.atRestSecretKey);
|
|
571
|
-
const resolved = {
|
|
572
|
-
key,
|
|
573
|
-
keyId: createHash("sha256").update(key).digest("hex").slice(0, 16)
|
|
574
|
-
};
|
|
575
|
-
this.cache = resolved;
|
|
576
|
-
return resolved;
|
|
577
|
-
}
|
|
578
|
-
/**
|
|
579
|
-
* The binary to spawn, or a diagnosable error saying why there is none.
|
|
580
|
-
*
|
|
581
|
-
* Order is the contract: an explicit path is used as-is and never falls back;
|
|
582
|
-
* discovery runs only for a provider that was configured for it, and only
|
|
583
|
-
* after the platform default has been tried and found unusable.
|
|
584
|
-
*/
|
|
585
|
-
async resolveElectronPath() {
|
|
586
|
-
if (this.explicitPath !== void 0) {
|
|
587
|
-
if (!isExecutable(this.explicitPath)) {
|
|
588
|
-
throw new WorkBuddyElectronPathError(
|
|
589
|
-
"electron-path-invalid",
|
|
590
|
-
`the configured ${this.product.productName} Electron binary is not available at ${this.explicitPath}; check ${this.product.envVar} or unset it to let the plugin look for the app itself`
|
|
591
|
-
);
|
|
592
|
-
}
|
|
593
|
-
return this.explicitPath;
|
|
594
|
-
}
|
|
595
|
-
if (this.discovery === "none") {
|
|
596
|
-
throw new WorkBuddyElectronPathError(
|
|
597
|
-
"electron-binary-unavailable",
|
|
598
|
-
`no ${this.product.productName} Electron binary is configured for this platform; set ${this.product.envVar} to the app's Electron binary`
|
|
599
|
-
);
|
|
600
|
-
}
|
|
601
|
-
if (this.defaultPath !== void 0 && isExecutable(this.defaultPath)) return this.defaultPath;
|
|
602
|
-
if (this.discoveredPath !== void 0) {
|
|
603
|
-
if (isExecutable(this.discoveredPath)) return this.discoveredPath;
|
|
604
|
-
this.discoveredPath = void 0;
|
|
605
|
-
}
|
|
606
|
-
const found = this.discovery === "macos-workbuddy" ? await this.discoverMacosApp() : await this.discoverWindowsApp();
|
|
607
|
-
this.discoveredPath = found;
|
|
608
|
-
return found;
|
|
609
|
-
}
|
|
610
|
-
/**
|
|
611
|
-
* Resolve this product's app through Spotlight, then prove each candidate's
|
|
612
|
-
* identity before it can be executed.
|
|
613
|
-
*
|
|
614
|
-
* The whole flow shares one budget: a hang in one candidate must not extend
|
|
615
|
-
* the wait for the others, and running out of budget is reported as an
|
|
616
|
-
* unfinished check rather than an absent app.
|
|
617
|
-
*/
|
|
618
|
-
async discoverMacosApp() {
|
|
619
|
-
if (this.platform !== "darwin") {
|
|
620
|
-
throw new WorkBuddyElectronPathError(
|
|
621
|
-
"electron-binary-unavailable",
|
|
622
|
-
`no ${this.product.productName} Electron binary is configured for this platform; set ${this.product.envVar} to the app's Electron binary`
|
|
623
|
-
);
|
|
624
|
-
}
|
|
625
|
-
const controller = new AbortController();
|
|
626
|
-
const timer = setTimeout(() => controller.abort(), this.discoveryBudgetMs);
|
|
627
|
-
try {
|
|
628
|
-
let candidates;
|
|
629
|
-
try {
|
|
630
|
-
candidates = await this.tools.findApps(controller.signal);
|
|
631
|
-
} catch {
|
|
632
|
-
throw discoveryIncomplete(this.product.productName, "the app search did not complete");
|
|
633
|
-
}
|
|
634
|
-
const seen = /* @__PURE__ */ new Map();
|
|
635
|
-
let unresolved = false;
|
|
636
|
-
for (const candidate of candidates) {
|
|
637
|
-
try {
|
|
638
|
-
statSync(candidate);
|
|
639
|
-
} catch (error) {
|
|
640
|
-
if (isENOENT(error)) continue;
|
|
641
|
-
unresolved = true;
|
|
642
|
-
continue;
|
|
643
|
-
}
|
|
644
|
-
let bundleIdentifier;
|
|
645
|
-
try {
|
|
646
|
-
bundleIdentifier = await this.tools.bundleIdentifier(candidate, controller.signal);
|
|
647
|
-
} catch {
|
|
648
|
-
unresolved = true;
|
|
649
|
-
continue;
|
|
650
|
-
}
|
|
651
|
-
if (bundleIdentifier === void 0) {
|
|
652
|
-
unresolved = true;
|
|
653
|
-
continue;
|
|
654
|
-
}
|
|
655
|
-
if (bundleIdentifier !== this.product.macOS.bundleId) continue;
|
|
656
|
-
const electronPath = join(candidate, "Contents", "MacOS", "Electron");
|
|
657
|
-
if (!isExecutable(electronPath)) continue;
|
|
658
|
-
let identity;
|
|
659
|
-
try {
|
|
660
|
-
identity = realpathSync(candidate);
|
|
661
|
-
} catch {
|
|
662
|
-
identity = candidate;
|
|
663
|
-
}
|
|
664
|
-
if (seen.has(identity)) continue;
|
|
665
|
-
let version;
|
|
666
|
-
try {
|
|
667
|
-
version = await this.tools.bundleVersion(candidate, controller.signal);
|
|
668
|
-
} catch {
|
|
669
|
-
version = void 0;
|
|
670
|
-
}
|
|
671
|
-
seen.set(identity, { bundlePath: candidate, electronPath, ...version === void 0 ? {} : { version } });
|
|
672
|
-
}
|
|
673
|
-
if (seen.size > 1) {
|
|
674
|
-
const listed = [...seen.values()].map((app) => ` - ${app.bundlePath}${app.version === void 0 ? "" : ` (${app.version})`}`).join("\n");
|
|
675
|
-
throw new WorkBuddyElectronPathError(
|
|
676
|
-
"electron-binary-ambiguous",
|
|
677
|
-
`more than one ${this.product.productName} application was found, so none was chosen:
|
|
678
|
-
${listed}
|
|
679
|
-
set ${this.product.envVar} to the one to use`
|
|
680
|
-
);
|
|
681
|
-
}
|
|
682
|
-
if (unresolved) throw discoveryIncomplete(this.product.productName, "some candidates could not be checked");
|
|
683
|
-
if (seen.size === 0) {
|
|
684
|
-
throw new WorkBuddyElectronPathError(
|
|
685
|
-
"electron-binary-not-found",
|
|
686
|
-
`no ${this.product.productName} application was found in the default location or the system index; if it is installed elsewhere, it may not be indexed yet; set ${this.product.envVar} to the app's Electron binary`
|
|
687
|
-
);
|
|
688
|
-
}
|
|
689
|
-
return [...seen.values()][0].electronPath;
|
|
690
|
-
} finally {
|
|
691
|
-
clearTimeout(timer);
|
|
692
|
-
controller.abort();
|
|
693
|
-
}
|
|
694
|
-
}
|
|
695
|
-
/**
|
|
696
|
-
* Resolve this product's app through Windows uninstall records. Registry
|
|
697
|
-
* entries provide hints, not trust: every DisplayIcon candidate must still
|
|
698
|
-
* be the product's Electron binary with the known Electron layout before
|
|
699
|
-
* execution.
|
|
700
|
-
*/
|
|
701
|
-
async discoverWindowsApp() {
|
|
702
|
-
if (this.platform !== "win32") {
|
|
703
|
-
throw new WorkBuddyElectronPathError(
|
|
704
|
-
"electron-binary-unavailable",
|
|
705
|
-
`no ${this.product.productName} Electron binary is configured for this platform; set ${this.product.envVar} to the app's Electron binary`
|
|
706
|
-
);
|
|
707
|
-
}
|
|
708
|
-
const controller = new AbortController();
|
|
709
|
-
const timer = setTimeout(() => controller.abort(), this.discoveryBudgetMs);
|
|
710
|
-
try {
|
|
711
|
-
const candidates = [];
|
|
712
|
-
let unresolved = false;
|
|
713
|
-
for (const root of WINDOWS_REGISTRY_ROOTS) {
|
|
714
|
-
let output;
|
|
715
|
-
try {
|
|
716
|
-
output = await this.windowsTools.queryUninstallRoot(root, controller.signal);
|
|
717
|
-
} catch {
|
|
718
|
-
unresolved = true;
|
|
719
|
-
continue;
|
|
720
|
-
}
|
|
721
|
-
const parsed = parseWindowsRegistryOutput(output, this.product.windows.displayNamePattern);
|
|
722
|
-
candidates.push(...parsed.candidates);
|
|
723
|
-
unresolved ||= parsed.incomplete;
|
|
724
|
-
}
|
|
725
|
-
const seen = /* @__PURE__ */ new Map();
|
|
726
|
-
const rejected = [];
|
|
727
|
-
for (const candidate of new Set(candidates)) {
|
|
728
|
-
const inspection = inspectWindowsElectronCandidate(candidate, this.platform, this.product.windows.exeBasename);
|
|
729
|
-
if (inspection === "unresolved") {
|
|
730
|
-
unresolved = true;
|
|
731
|
-
continue;
|
|
732
|
-
}
|
|
733
|
-
if (inspection === void 0) {
|
|
734
|
-
rejected.push(candidate);
|
|
735
|
-
continue;
|
|
736
|
-
}
|
|
737
|
-
seen.set(inspection.identity, inspection.electronPath);
|
|
738
|
-
}
|
|
739
|
-
if (unresolved) throw discoveryIncomplete(this.product.productName, "some registry entries or candidates could not be checked");
|
|
740
|
-
if (seen.size > 1) {
|
|
741
|
-
const listed = [...seen.values()].map((path) => ` - ${path}`).join("\n");
|
|
742
|
-
throw new WorkBuddyElectronPathError(
|
|
743
|
-
"electron-binary-ambiguous",
|
|
744
|
-
`more than one ${this.product.productName} application was found, so none was chosen:
|
|
745
|
-
${listed}
|
|
746
|
-
set ${this.product.envVar} to the one to use`
|
|
747
|
-
);
|
|
748
|
-
}
|
|
749
|
-
if (seen.size === 0) {
|
|
750
|
-
const message = rejected.length === 0 ? `no usable ${this.product.productName} Electron binary was found in the default location or Windows uninstall records; set ${this.product.envVar} to the app's Electron binary` : `Windows uninstall records found ${rejected.length} ${this.product.productName} candidate${rejected.length > 1 ? "s" : ""}, but ${rejected.length > 1 ? "none" : "it"} did not match the expected app layout (the app's exe beside a version file and resources\\app.asar); set ${this.product.envVar} to the installed app's executable to use it`;
|
|
751
|
-
throw new WorkBuddyElectronPathError("electron-binary-not-found", message);
|
|
752
|
-
}
|
|
753
|
-
return [...seen.values()][0];
|
|
754
|
-
} finally {
|
|
755
|
-
clearTimeout(timer);
|
|
756
|
-
controller.abort();
|
|
757
|
-
}
|
|
758
|
-
}
|
|
759
|
-
async spawnPayload() {
|
|
760
|
-
return await this.spawnHelper(await this.resolveElectronPath());
|
|
761
|
-
}
|
|
762
|
-
async spawnAt(electronPath) {
|
|
763
|
-
return await new Promise((resolve, reject) => {
|
|
764
|
-
execFile(electronPath, [HELPER_SCRIPT_ARGUMENT_FLAG, HELPER_SCRIPT], {
|
|
765
|
-
timeout: this.timeoutMs,
|
|
766
|
-
maxBuffer: 1024 * 1024,
|
|
767
|
-
windowsHide: true,
|
|
768
|
-
env: { ...process.env, ELECTRON_RUN_AS_NODE: "1" }
|
|
769
|
-
}, (error, stdout) => {
|
|
770
|
-
if (error !== null && error !== void 0) {
|
|
771
|
-
const reason = error.killed === true ? `timed out or was killed after ${String(this.timeoutMs)}ms` : error.code !== void 0 ? `exited with code ${String(error.code)}` : "could not be started";
|
|
772
|
-
reject(new WorkBuddyElectronPathError(
|
|
773
|
-
"encrypted-credential-unreadable",
|
|
774
|
-
`the WorkBuddy key helper (${electronPath}) ${reason}`
|
|
775
|
-
));
|
|
776
|
-
return;
|
|
777
|
-
}
|
|
778
|
-
const output = stdout.trim();
|
|
779
|
-
if (output === "") {
|
|
780
|
-
reject(new WorkBuddyElectronPathError(
|
|
781
|
-
"encrypted-credential-unreadable",
|
|
782
|
-
`the WorkBuddy key helper (${electronPath}) produced no payload`
|
|
783
|
-
));
|
|
784
|
-
return;
|
|
785
|
-
}
|
|
786
|
-
resolve(output);
|
|
787
|
-
});
|
|
788
|
-
});
|
|
789
|
-
}
|
|
790
|
-
};
|
|
791
|
-
function windowsRegistryKeyMissing(stderr) {
|
|
792
|
-
const detail = stderr.trim();
|
|
793
|
-
return /^ERROR:\s*The system was unable to find the specified registry key or value\.?$/iu.test(detail) || /^错误[::]\s*系统找不到指定的注册表项或值[。.]?$/u.test(detail);
|
|
794
|
-
}
|
|
795
|
-
function parseWindowsRegistryOutput(output, displayNamePattern) {
|
|
796
|
-
const entries = /* @__PURE__ */ new Map();
|
|
797
|
-
let currentKey;
|
|
798
|
-
for (const line of output.split(/\r?\n/u)) {
|
|
799
|
-
const keyMatch = /^\s*(HKEY_[^\r\n]+?)\s*$/iu.exec(line);
|
|
800
|
-
if (keyMatch !== null) {
|
|
801
|
-
currentKey = keyMatch[1];
|
|
802
|
-
entries.set(currentKey, {});
|
|
803
|
-
continue;
|
|
804
|
-
}
|
|
805
|
-
if (currentKey === void 0) continue;
|
|
806
|
-
const valueMatch = /^\s+(DisplayName|DisplayIcon)\s+REG_[A-Z0-9_]+\s*(.*?)\s*$/iu.exec(line);
|
|
807
|
-
if (valueMatch === null) continue;
|
|
808
|
-
const entry = entries.get(currentKey);
|
|
809
|
-
if (entry === void 0) continue;
|
|
810
|
-
const value = valueMatch[2] ?? "";
|
|
811
|
-
if (valueMatch[1].toLowerCase() === "displayname") entry.displayName = value;
|
|
812
|
-
else entry.displayIcon = value;
|
|
813
|
-
}
|
|
814
|
-
const candidates = [];
|
|
815
|
-
let incomplete = false;
|
|
816
|
-
for (const entry of entries.values()) {
|
|
817
|
-
if (entry.displayName === void 0) continue;
|
|
818
|
-
if (!displayNamePattern.test(entry.displayName.trim())) continue;
|
|
819
|
-
const displayIcon = entry.displayIcon === void 0 ? void 0 : parseWindowsDisplayIcon(entry.displayIcon);
|
|
820
|
-
if (displayIcon === void 0) incomplete = true;
|
|
821
|
-
else candidates.push(displayIcon);
|
|
822
|
-
}
|
|
823
|
-
return { candidates, incomplete };
|
|
824
|
-
}
|
|
825
|
-
function parseWindowsDisplayIcon(value) {
|
|
826
|
-
const raw = value.trim();
|
|
827
|
-
let path;
|
|
828
|
-
if (raw.startsWith('"')) {
|
|
829
|
-
const closingQuote = raw.indexOf('"', 1);
|
|
830
|
-
if (closingQuote < 0) return void 0;
|
|
831
|
-
const suffix = raw.slice(closingQuote + 1).trim();
|
|
832
|
-
if (suffix !== "" && !/^,\d+$/u.test(suffix)) return void 0;
|
|
833
|
-
path = raw.slice(1, closingQuote).replace(/,\d+$/u, "");
|
|
834
|
-
} else {
|
|
835
|
-
const match = /^(.+?\.exe)(?:,\d+)?$/iu.exec(raw);
|
|
836
|
-
if (match === null) return void 0;
|
|
837
|
-
path = match[1];
|
|
838
|
-
}
|
|
839
|
-
path = path.trim();
|
|
840
|
-
return /\.exe$/iu.test(path) ? path : void 0;
|
|
841
|
-
}
|
|
842
|
-
function inspectWindowsElectronCandidate(electronPath, platform, exeBasename) {
|
|
843
|
-
if (platform !== "win32" || basename(electronPath).toLowerCase() !== exeBasename) return void 0;
|
|
844
|
-
let binaryStat;
|
|
845
|
-
try {
|
|
846
|
-
binaryStat = statSync(electronPath);
|
|
847
|
-
} catch (error) {
|
|
848
|
-
return isENOENT(error) ? void 0 : "unresolved";
|
|
849
|
-
}
|
|
850
|
-
if (!binaryStat.isFile()) return void 0;
|
|
851
|
-
try {
|
|
852
|
-
accessSync(electronPath, constants.X_OK);
|
|
853
|
-
} catch (error) {
|
|
854
|
-
return isENOENT(error) ? void 0 : "unresolved";
|
|
855
|
-
}
|
|
856
|
-
const installRoot = dirname(electronPath);
|
|
857
|
-
let version;
|
|
858
|
-
try {
|
|
859
|
-
version = readFileSync(join(installRoot, "version"), "utf8").trim();
|
|
860
|
-
} catch (error) {
|
|
861
|
-
return isENOENT(error) ? void 0 : "unresolved";
|
|
862
|
-
}
|
|
863
|
-
if (!WINDOWS_ELECTRON_VERSION_PATTERN.test(version)) return void 0;
|
|
864
|
-
try {
|
|
865
|
-
if (!readdirSync(join(installRoot, "resources")).includes("app.asar")) return void 0;
|
|
866
|
-
} catch (error) {
|
|
867
|
-
return isENOENT(error) ? void 0 : "unresolved";
|
|
868
|
-
}
|
|
869
|
-
let identity;
|
|
870
|
-
try {
|
|
871
|
-
identity = realpathSync(electronPath);
|
|
872
|
-
} catch (error) {
|
|
873
|
-
return isENOENT(error) ? void 0 : "unresolved";
|
|
874
|
-
}
|
|
875
|
-
return {
|
|
876
|
-
electronPath,
|
|
877
|
-
// Windows paths are case-insensitive even when a registry entry preserved
|
|
878
|
-
// a different casing from the filesystem spelling.
|
|
879
|
-
identity: platform === "win32" ? identity.toLowerCase() : identity
|
|
880
|
-
};
|
|
881
|
-
}
|
|
882
|
-
function isExecutable(path) {
|
|
883
|
-
try {
|
|
884
|
-
accessSync(path, constants.X_OK);
|
|
885
|
-
return true;
|
|
886
|
-
} catch {
|
|
887
|
-
return false;
|
|
888
|
-
}
|
|
889
|
-
}
|
|
890
|
-
var WorkBuddyElectronPathError = class extends Error {
|
|
891
|
-
reasonCode;
|
|
892
|
-
constructor(reasonCode, message) {
|
|
893
|
-
super(message);
|
|
894
|
-
this.name = "WorkBuddyElectronPathError";
|
|
895
|
-
this.reasonCode = reasonCode;
|
|
896
|
-
}
|
|
897
|
-
};
|
|
898
|
-
function reasonCodeOf(error) {
|
|
899
|
-
return error instanceof WorkBuddyElectronPathError ? error.reasonCode : void 0;
|
|
900
|
-
}
|
|
901
|
-
function isENOENT(error) {
|
|
902
|
-
return error?.code === "ENOENT";
|
|
903
|
-
}
|
|
904
|
-
function discoveryIncomplete(productName, detail) {
|
|
905
|
-
return new WorkBuddyElectronPathError(
|
|
906
|
-
"electron-discovery-incomplete",
|
|
907
|
-
`the ${productName} application search did not finish (${detail}); this is not proof that the app is missing`
|
|
908
|
-
);
|
|
909
|
-
}
|
|
910
|
-
var HELPER_SCRIPT = 'process.stdout.write(String(process._linkedBinding("electron_browser_workbuddy_storage").loggerGet()))';
|
|
911
|
-
var HELPER_SCRIPT_ARGUMENT_FLAG = "-e";
|
|
912
|
-
|
|
913
|
-
// upstream/workbuddy/src/auth.ts
|
|
914
|
-
import { readFile, rm } from "node:fs/promises";
|
|
915
|
-
import { homedir, release } from "node:os";
|
|
916
|
-
import { basename as basename2, dirname as dirname2, join as join2 } from "node:path";
|
|
917
|
-
import { withFileLock, writeFileAtomic } from "@deepseek-ai/dsh-atomic-write";
|
|
918
|
-
import { resolveDshHome } from "@deepseek-ai/dsh-home-paths";
|
|
919
|
-
var WORKBUDDY_AUTH_FILENAME = ".workbuddy-auth.json";
|
|
920
|
-
var WORKBUDDY_AUTH_FILE_ENV = "WORKBUDDY_AUTH_FILE";
|
|
921
|
-
var OWN_FORMAT_VERSION = 1;
|
|
922
|
-
function workbuddyOwnAuthPath() {
|
|
923
|
-
return join2(resolveDshHome(), WORKBUDDY_AUTH_FILENAME);
|
|
924
|
-
}
|
|
925
|
-
var DESKTOP_AUTH_RELATIVE_PATH = ["CodeBuddyExtension", "Data", "Public", "auth", "workbuddy-desktop.info"];
|
|
926
|
-
function isWsl() {
|
|
927
|
-
if (process.platform !== "linux") return false;
|
|
928
|
-
if (process.env["WSL_DISTRO_NAME"] !== void 0 || process.env["WSL_INTEROP"] !== void 0) return true;
|
|
929
|
-
return release().toLowerCase().includes("microsoft");
|
|
930
|
-
}
|
|
931
|
-
function windowsPathForWsl(value) {
|
|
932
|
-
const path = value?.trim();
|
|
933
|
-
if (!path) return void 0;
|
|
934
|
-
if (path.startsWith("/")) return path;
|
|
935
|
-
const drivePath = /^([a-z]):[\\/](.*)$/iu.exec(path);
|
|
936
|
-
if (drivePath === null) return void 0;
|
|
937
|
-
return join2("/mnt", drivePath[1].toLowerCase(), ...drivePath[2].split(/[\\/]+/u));
|
|
938
|
-
}
|
|
939
|
-
function wslDesktopAuthCandidates(home) {
|
|
940
|
-
const profile = windowsPathForWsl(process.env["USERPROFILE"]) ?? join2("/mnt/c/Users", basename2(home));
|
|
941
|
-
const localAppData = windowsPathForWsl(process.env["LOCALAPPDATA"]) ?? join2(profile, "AppData", "Local");
|
|
942
|
-
const roamingAppData = windowsPathForWsl(process.env["APPDATA"]) ?? join2(profile, "AppData", "Roaming");
|
|
943
|
-
return [
|
|
944
|
-
join2(localAppData, ...DESKTOP_AUTH_RELATIVE_PATH),
|
|
945
|
-
join2(roamingAppData, ...DESKTOP_AUTH_RELATIVE_PATH)
|
|
946
|
-
];
|
|
947
|
-
}
|
|
948
|
-
function defaultDesktopAuthCandidates() {
|
|
949
|
-
const home = homedir();
|
|
950
|
-
if (process.platform === "darwin") {
|
|
951
|
-
return [join2(home, "Library", "Application Support", "CodeBuddyExtension", "Data", "Public", "auth", "workbuddy-desktop.info")];
|
|
952
|
-
}
|
|
953
|
-
if (process.platform === "win32") {
|
|
954
|
-
return [
|
|
955
|
-
join2(home, "AppData", "Local", "CodeBuddyExtension", "Data", "Public", "auth", "workbuddy-desktop.info"),
|
|
956
|
-
join2(home, "AppData", "Roaming", "CodeBuddyExtension", "Data", "Public", "auth", "workbuddy-desktop.info")
|
|
957
|
-
];
|
|
958
|
-
}
|
|
959
|
-
if (process.platform === "linux") {
|
|
960
|
-
const configHome = xdgBase("XDG_CONFIG_HOME", join2(home, ".config"));
|
|
961
|
-
const dataHome = xdgBase("XDG_DATA_HOME", join2(home, ".local", "share"));
|
|
962
|
-
const linux = dedupeCandidates([
|
|
963
|
-
join2(configHome, ...DESKTOP_AUTH_RELATIVE_PATH),
|
|
964
|
-
join2(dataHome, ...DESKTOP_AUTH_RELATIVE_PATH)
|
|
965
|
-
]);
|
|
966
|
-
return isWsl() ? dedupeCandidates([...wslDesktopAuthCandidates(home), ...linux]) : linux;
|
|
967
|
-
}
|
|
968
|
-
return [];
|
|
969
|
-
}
|
|
970
|
-
function xdgBase(envName, fallback) {
|
|
971
|
-
const value = process.env[envName]?.trim();
|
|
972
|
-
if (value !== void 0 && value !== "" && value.startsWith("/")) return value;
|
|
973
|
-
return fallback;
|
|
974
|
-
}
|
|
975
|
-
function dedupeCandidates(candidates) {
|
|
976
|
-
return [...new Set(candidates)];
|
|
977
|
-
}
|
|
978
|
-
function desktopAuthCandidatesFor(variant) {
|
|
979
|
-
return dedupeCandidates(defaultDesktopAuthCandidates().map((path) => join2(dirname2(path), variant.desktopFilename)));
|
|
980
|
-
}
|
|
981
|
-
function expiryToMs(value) {
|
|
982
|
-
if (value <= 0) return 0;
|
|
983
|
-
return value > 1e12 ? value : value * 1e3;
|
|
984
|
-
}
|
|
985
|
-
function optionalString(value) {
|
|
986
|
-
return typeof value === "string" && value !== "" ? value : void 0;
|
|
987
|
-
}
|
|
988
|
-
function parseWorkBuddyAuth(text) {
|
|
989
|
-
let parsed;
|
|
990
|
-
try {
|
|
991
|
-
parsed = JSON.parse(text);
|
|
992
|
-
} catch {
|
|
993
|
-
return void 0;
|
|
994
|
-
}
|
|
995
|
-
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return void 0;
|
|
996
|
-
const document = parsed;
|
|
997
|
-
let auth;
|
|
998
|
-
let identity;
|
|
999
|
-
if (typeof document["auth"] === "object" && document["auth"] !== null) {
|
|
1000
|
-
auth = document["auth"];
|
|
1001
|
-
identity = typeof document["account"] === "object" && document["account"] !== null ? document["account"] : {};
|
|
1002
|
-
} else {
|
|
1003
|
-
auth = document;
|
|
1004
|
-
identity = document;
|
|
1005
|
-
}
|
|
1006
|
-
const accessToken = typeof auth["accessToken"] === "string" ? auth["accessToken"] : "";
|
|
1007
|
-
if (accessToken === "") return void 0;
|
|
1008
|
-
const expiresAtMs = typeof auth["expiresAt"] === "number" ? expiryToMs(auth["expiresAt"]) : 0;
|
|
1009
|
-
const refreshExpiresAtMs = typeof auth["refreshExpiresAt"] === "number" ? expiryToMs(auth["refreshExpiresAt"]) : void 0;
|
|
1010
|
-
const enterpriseId = optionalString(identity["enterpriseId"]);
|
|
1011
|
-
const nickname = optionalString(identity["nickname"]);
|
|
1012
|
-
const credential = {
|
|
1013
|
-
accessToken,
|
|
1014
|
-
refreshToken: typeof auth["refreshToken"] === "string" ? auth["refreshToken"] : "",
|
|
1015
|
-
expiresAtMs,
|
|
1016
|
-
...refreshExpiresAtMs === void 0 ? {} : { refreshExpiresAtMs },
|
|
1017
|
-
domain: optionalString(auth["domain"]) ?? "",
|
|
1018
|
-
uid: optionalString(identity["uid"]) ?? "",
|
|
1019
|
-
...enterpriseId === void 0 ? {} : { enterpriseId },
|
|
1020
|
-
...nickname === void 0 ? {} : { nickname },
|
|
1021
|
-
source: "desktop"
|
|
1022
|
-
};
|
|
1023
|
-
return credential;
|
|
1024
|
-
}
|
|
1025
|
-
function ownDocument(credential) {
|
|
1026
|
-
return { version: OWN_FORMAT_VERSION, credential };
|
|
1027
|
-
}
|
|
1028
|
-
function parseOwnDocument(text) {
|
|
1029
|
-
let parsed;
|
|
1030
|
-
try {
|
|
1031
|
-
parsed = JSON.parse(text);
|
|
1032
|
-
} catch {
|
|
1033
|
-
return void 0;
|
|
1034
|
-
}
|
|
1035
|
-
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return void 0;
|
|
1036
|
-
const document = parsed;
|
|
1037
|
-
if (document["version"] !== OWN_FORMAT_VERSION) return void 0;
|
|
1038
|
-
if (typeof document["credential"] !== "object" || document["credential"] === null) return void 0;
|
|
1039
|
-
const stored = document["credential"];
|
|
1040
|
-
const accessToken = typeof stored["accessToken"] === "string" ? stored["accessToken"] : "";
|
|
1041
|
-
if (accessToken === "") return void 0;
|
|
1042
|
-
const refreshExpiresAtMs = typeof stored["refreshExpiresAtMs"] === "number" ? stored["refreshExpiresAtMs"] : void 0;
|
|
1043
|
-
const enterpriseId = optionalString(stored["enterpriseId"]);
|
|
1044
|
-
const nickname = optionalString(stored["nickname"]);
|
|
1045
|
-
return {
|
|
1046
|
-
accessToken,
|
|
1047
|
-
refreshToken: typeof stored["refreshToken"] === "string" ? stored["refreshToken"] : "",
|
|
1048
|
-
expiresAtMs: typeof stored["expiresAtMs"] === "number" ? stored["expiresAtMs"] : 0,
|
|
1049
|
-
...refreshExpiresAtMs === void 0 ? {} : { refreshExpiresAtMs },
|
|
1050
|
-
domain: optionalString(stored["domain"]) ?? "",
|
|
1051
|
-
uid: optionalString(stored["uid"]) ?? "",
|
|
1052
|
-
...enterpriseId === void 0 ? {} : { enterpriseId },
|
|
1053
|
-
...nickname === void 0 ? {} : { nickname },
|
|
1054
|
-
source: "dsh"
|
|
1055
|
-
};
|
|
1056
|
-
}
|
|
1057
|
-
function isENOENT2(error) {
|
|
1058
|
-
return error?.code === "ENOENT";
|
|
1059
|
-
}
|
|
1060
|
-
var WorkBuddyCredentialStore = class {
|
|
1061
|
-
variant;
|
|
1062
|
-
refresh;
|
|
1063
|
-
refreshMarginMs;
|
|
1064
|
-
ownPath;
|
|
1065
|
-
keyProvider;
|
|
1066
|
-
desktopPathOverride;
|
|
1067
|
-
inflight;
|
|
1068
|
-
constructor(options) {
|
|
1069
|
-
this.variant = options.variant;
|
|
1070
|
-
this.refresh = options.refresh;
|
|
1071
|
-
this.refreshMarginMs = options.refreshMarginMs ?? 5 * 60 * 1e3;
|
|
1072
|
-
this.ownPath = options.ownPath ?? (options.variant ? join2(resolveDshHome(), options.variant.ownFilename) : workbuddyOwnAuthPath());
|
|
1073
|
-
this.keyProvider = options.keyProvider ?? new WorkBuddyAtRestKeyProvider({ product: electronProfileFor(options.variant) });
|
|
1074
|
-
this.desktopPathOverride = options.desktopPath;
|
|
1075
|
-
}
|
|
1076
|
-
/**
|
|
1077
|
-
* Configuration precedence for the desktop file: the plugin's configured
|
|
1078
|
-
* path, then the environment variable, then the platform defaults. An
|
|
1079
|
-
* explicit path is used verbatim; the defaults are a probe order.
|
|
1080
|
-
*/
|
|
1081
|
-
resolveDesktopCandidates() {
|
|
1082
|
-
const fromEnv = process.env[this.variant?.env ?? WORKBUDDY_AUTH_FILE_ENV];
|
|
1083
|
-
const explicit = this.desktopPathOverride ?? (fromEnv !== void 0 && fromEnv.trim() !== "" ? fromEnv : void 0);
|
|
1084
|
-
if (explicit !== void 0) return [explicit];
|
|
1085
|
-
return this.variant === void 0 ? defaultDesktopAuthCandidates() : desktopAuthCandidatesFor(this.variant);
|
|
1086
|
-
}
|
|
1087
|
-
resolveDesktopPath() {
|
|
1088
|
-
return this.resolveDesktopCandidates()[0];
|
|
1089
|
-
}
|
|
1090
|
-
/**
|
|
1091
|
-
* Repoint the desktop file; a settings change applies on the next read.
|
|
1092
|
-
*/
|
|
1093
|
-
setDesktopPath(path) {
|
|
1094
|
-
this.desktopPathOverride = path;
|
|
1095
|
-
}
|
|
1096
|
-
/** The resolved desktop auth-file path, for diagnostics. */
|
|
1097
|
-
desktopAuthPath() {
|
|
1098
|
-
return this.resolveDesktopPath();
|
|
1099
|
-
}
|
|
1100
|
-
/** The plugin-owned copy path, for diagnostics. */
|
|
1101
|
-
ownAuthPath() {
|
|
1102
|
-
return this.ownPath;
|
|
1103
|
-
}
|
|
1104
|
-
/** Read the freshest stored credential without refreshing anything. */
|
|
1105
|
-
async current() {
|
|
1106
|
-
const [desktop, own] = await Promise.all([this.readDesktop(), this.readOwn()]);
|
|
1107
|
-
if (this.variant !== void 0) {
|
|
1108
|
-
for (const [label, credential] of [["desktop file", desktop], ["plugin copy", own]]) {
|
|
1109
|
-
if (credential === void 0) continue;
|
|
1110
|
-
const region = regionOf(credential.domain);
|
|
1111
|
-
if (region !== this.variant.region) {
|
|
1112
|
-
throw new WorkBuddyElectronPathError(
|
|
1113
|
-
"credential-region-mismatch",
|
|
1114
|
-
`${this.variant.displayName} received a ${region === "cn" ? "WorkBuddy (CN)" : "WorkBuddy AI"} credential in its ${label} (domain ${JSON.stringify(credential.domain)}); point ${this.variant.env} at the ${this.variant.appName} sign-in, or remove the mismatched file`
|
|
1115
|
-
);
|
|
1116
|
-
}
|
|
1117
|
-
}
|
|
1118
|
-
}
|
|
1119
|
-
if (desktop === void 0) return own;
|
|
1120
|
-
if (own === void 0) return desktop;
|
|
1121
|
-
if (desktop.uid !== own.uid || desktop.enterpriseId !== own.enterpriseId) return desktop;
|
|
1122
|
-
return own.expiresAtMs > desktop.expiresAtMs ? own : desktop;
|
|
1123
|
-
}
|
|
1124
|
-
/**
|
|
1125
|
-
* The credential to send upstream: {@link current}, refreshed on demand.
|
|
1126
|
-
* Single-flight, so parallel requests share one refresh.
|
|
1127
|
-
*/
|
|
1128
|
-
async resolve() {
|
|
1129
|
-
const credential = await this.current();
|
|
1130
|
-
if (credential === void 0) {
|
|
1131
|
-
const candidates = this.resolveDesktopCandidates();
|
|
1132
|
-
const desktop = candidates.length > 0 ? candidates.join(" or ") : "(no desktop path on this platform)";
|
|
1133
|
-
const app = this.variant?.appName ?? "WorkBuddy";
|
|
1134
|
-
throw new Error(
|
|
1135
|
-
`workbuddy: no signed-in ${app} account found; sign in once in the ${app} desktop app (expected ${desktop} or ${this.variant?.env ?? WORKBUDDY_AUTH_FILE_ENV}), or refresh an existing session`
|
|
1136
|
-
);
|
|
1137
|
-
}
|
|
1138
|
-
if (!this.needsRefresh(credential)) return credential;
|
|
1139
|
-
this.inflight ??= this.refreshNow(credential).finally(() => {
|
|
1140
|
-
this.inflight = void 0;
|
|
1141
|
-
});
|
|
1142
|
-
return this.inflight;
|
|
1143
|
-
}
|
|
1144
|
-
/** Read-only sign-in summary; never refreshes and never throws. */
|
|
1145
|
-
async status() {
|
|
1146
|
-
try {
|
|
1147
|
-
const credential = await this.current();
|
|
1148
|
-
if (credential === void 0) return { state: "signed-out", reasonCode: "no-credential" };
|
|
1149
|
-
return {
|
|
1150
|
-
state: "signed-in",
|
|
1151
|
-
expiresAtMs: credential.expiresAtMs,
|
|
1152
|
-
...credential.refreshExpiresAtMs === void 0 ? {} : { refreshExpiresAtMs: credential.refreshExpiresAtMs },
|
|
1153
|
-
...credential.nickname === void 0 ? {} : { nickname: credential.nickname },
|
|
1154
|
-
...credential.domain === "" ? {} : { domain: credential.domain },
|
|
1155
|
-
source: credential.source
|
|
1156
|
-
};
|
|
1157
|
-
} catch (error) {
|
|
1158
|
-
return {
|
|
1159
|
-
state: "signed-out",
|
|
1160
|
-
reason: error instanceof Error ? error.message : String(error),
|
|
1161
|
-
...reasonCodeOf(error) === void 0 ? {} : { reasonCode: reasonCodeOf(error) }
|
|
1162
|
-
};
|
|
1163
|
-
}
|
|
1164
|
-
}
|
|
1165
|
-
/** Remove the plugin-owned copy; the desktop file is untouched. */
|
|
1166
|
-
async logout() {
|
|
1167
|
-
await rm(this.ownPath, { force: true });
|
|
1168
|
-
await rm(`${this.ownPath}.lock`, { force: true });
|
|
1169
|
-
}
|
|
1170
|
-
needsRefresh(credential) {
|
|
1171
|
-
if (credential.expiresAtMs <= 0) return true;
|
|
1172
|
-
return Date.now() + this.refreshMarginMs >= credential.expiresAtMs;
|
|
1173
|
-
}
|
|
1174
|
-
async refreshNow(credential) {
|
|
1175
|
-
if (credential.refreshToken === "") {
|
|
1176
|
-
if (credential.expiresAtMs > Date.now() + 3e4) return credential;
|
|
1177
|
-
throw new Error("workbuddy: access token expired and no refresh token is stored; sign in again in the WorkBuddy desktop app");
|
|
1178
|
-
}
|
|
1179
|
-
try {
|
|
1180
|
-
const outcome = await this.refresh(credential);
|
|
1181
|
-
const refreshed = {
|
|
1182
|
-
...credential,
|
|
1183
|
-
accessToken: outcome.accessToken,
|
|
1184
|
-
...outcome.refreshToken === void 0 ? {} : { refreshToken: outcome.refreshToken },
|
|
1185
|
-
expiresAtMs: outcome.expiresInSec !== void 0 ? Date.now() + outcome.expiresInSec * 1e3 : credential.expiresAtMs,
|
|
1186
|
-
...outcome.domain === void 0 || outcome.domain === "" ? {} : { domain: outcome.domain },
|
|
1187
|
-
source: "dsh"
|
|
1188
|
-
};
|
|
1189
|
-
await this.saveOwn(refreshed);
|
|
1190
|
-
return refreshed;
|
|
1191
|
-
} catch (error) {
|
|
1192
|
-
if (credential.expiresAtMs > Date.now() + 3e4) return credential;
|
|
1193
|
-
throw new Error(
|
|
1194
|
-
`workbuddy: token refresh failed and the access token is expired (${String(error)}); open the WorkBuddy desktop app once to sign in again`
|
|
1195
|
-
);
|
|
1196
|
-
}
|
|
1197
|
-
}
|
|
1198
|
-
async saveOwn(credential) {
|
|
1199
|
-
await withFileLock(this.ownPath, async () => {
|
|
1200
|
-
await writeFileAtomic(this.ownPath, `${JSON.stringify(ownDocument(credential), null, 2)}
|
|
1201
|
-
`, {
|
|
1202
|
-
mode: 384,
|
|
1203
|
-
dirMode: 448
|
|
1204
|
-
});
|
|
1205
|
-
});
|
|
1206
|
-
}
|
|
1207
|
-
/**
|
|
1208
|
-
* Read the first desktop candidate that exists. Only an absent file
|
|
1209
|
-
* (ENOENT) falls through to the next candidate; a file that is present
|
|
1210
|
-
* but unparsable is authoritative for its slot, so a stale older-version
|
|
1211
|
-
* file never silently wins over a broken newer one.
|
|
1212
|
-
*
|
|
1213
|
-
* Since WorkBuddy 5.6 the token fields may arrive in at-rest envelopes, so
|
|
1214
|
-
* the text is classified before the regular parser sees it. An encrypted
|
|
1215
|
-
* document must be *opened*, never skipped; an unrecognized one must fail
|
|
1216
|
-
* loudly. The desktop file, as long as it exists, is the identity
|
|
1217
|
-
* authority — a document this plugin cannot read must surface as a
|
|
1218
|
-
* diagnosis rather than be papered over by the plugin-owned copy, which
|
|
1219
|
-
* belongs to whatever account was signed in when it was last refreshed.
|
|
1220
|
-
* Only an absent (or empty) file lets the probe continue.
|
|
1221
|
-
*/
|
|
1222
|
-
async readDesktop() {
|
|
1223
|
-
for (const desktopPath of this.resolveDesktopCandidates()) {
|
|
1224
|
-
let text;
|
|
1225
|
-
try {
|
|
1226
|
-
text = await readFile(desktopPath, "utf8");
|
|
1227
|
-
} catch (error) {
|
|
1228
|
-
if (!isENOENT2(error)) throw error;
|
|
1229
|
-
continue;
|
|
1230
|
-
}
|
|
1231
|
-
const classification = classifyDesktopAuthDocument(text);
|
|
1232
|
-
if (classification.format === "plaintext") return parseWorkBuddyAuth(text);
|
|
1233
|
-
if (classification.format === "absent") continue;
|
|
1234
|
-
if (classification.format === "unrecognized") {
|
|
1235
|
-
throw new Error(
|
|
1236
|
-
`the desktop auth file at ${desktopPath} exists but is unreadable (neither a plaintext credential nor a decodable WorkBuddy 5.6 envelope); fix or remove the file \u2014 it outranks the plugin-owned credential copy`
|
|
1237
|
-
);
|
|
1238
|
-
}
|
|
1239
|
-
return await this.openEncryptedDesktop(classification);
|
|
1240
|
-
}
|
|
1241
|
-
return void 0;
|
|
1242
|
-
}
|
|
1243
|
-
/** Open a 5.6 encrypted desktop document into the regular credential shape. */
|
|
1244
|
-
async openEncryptedDesktop(classification) {
|
|
1245
|
-
const wrapped = classification.wrapped;
|
|
1246
|
-
const key = await this.keyProvider.protectorKeyFor(keyIdsOf(wrapped.fields));
|
|
1247
|
-
const text = unwrapDesktopAuthDocument(classification, (field) => {
|
|
1248
|
-
const plaintext = openAuthField(key, field.envelope);
|
|
1249
|
-
if (plaintext === void 0) {
|
|
1250
|
-
throw new WorkBuddyElectronPathError(
|
|
1251
|
-
"encrypted-credential-unreadable",
|
|
1252
|
-
`the encrypted desktop credential's ${field.field} could not be decrypted (envelope key id ${field.envelope.keyId}); the WorkBuddy app may hold a different at-rest key \u2014 open it once to reseal the sign-in`
|
|
1253
|
-
);
|
|
1254
|
-
}
|
|
1255
|
-
return plaintext;
|
|
1256
|
-
});
|
|
1257
|
-
return parseWorkBuddyAuth(text);
|
|
1258
|
-
}
|
|
1259
|
-
/**
|
|
1260
|
-
* Classify the first desktop candidate that exists and carries content;
|
|
1261
|
-
* `absent` when none does. An empty first file is skipped so it cannot mask
|
|
1262
|
-
* a real document on the next candidate. Diagnostics only — it never spawns
|
|
1263
|
-
* the key helper and never decrypts, so doctor can describe the file
|
|
1264
|
-
* without attempting the unlock.
|
|
1265
|
-
*/
|
|
1266
|
-
async desktopAuthFormat() {
|
|
1267
|
-
for (const desktopPath of this.resolveDesktopCandidates()) {
|
|
1268
|
-
let text;
|
|
1269
|
-
try {
|
|
1270
|
-
text = await readFile(desktopPath, "utf8");
|
|
1271
|
-
} catch (error) {
|
|
1272
|
-
if (!isENOENT2(error)) throw error;
|
|
1273
|
-
continue;
|
|
1274
|
-
}
|
|
1275
|
-
const format = classifyDesktopAuthDocument(text).format;
|
|
1276
|
-
if (format !== "absent") return format;
|
|
1277
|
-
}
|
|
1278
|
-
return "absent";
|
|
1279
|
-
}
|
|
1280
|
-
async readOwn() {
|
|
1281
|
-
try {
|
|
1282
|
-
return parseOwnDocument(await readFile(this.ownPath, "utf8"));
|
|
1283
|
-
} catch (error) {
|
|
1284
|
-
if (isENOENT2(error)) return void 0;
|
|
1285
|
-
return void 0;
|
|
1286
|
-
}
|
|
1287
|
-
}
|
|
1288
|
-
/**
|
|
1289
|
-
* The first desktop candidate the probe would actually read from; `undefined`
|
|
1290
|
-
* when none qualifies. Semantics deliberately match the probe: empty files
|
|
1291
|
-
* are skipped (the probe classifies them as absent and moves on), so on an
|
|
1292
|
-
* XDG layout where the config-home file is empty but the data-home file
|
|
1293
|
-
* holds the credential, diagnostics name the *data-home* file — the one
|
|
1294
|
-
* authentication really uses. Like the probe it never parses or decrypts.
|
|
1295
|
-
*/
|
|
1296
|
-
async resolvedDesktopAuthPath() {
|
|
1297
|
-
for (const desktopPath of this.resolveDesktopCandidates()) {
|
|
1298
|
-
let text;
|
|
1299
|
-
try {
|
|
1300
|
-
text = await readFile(desktopPath, "utf8");
|
|
1301
|
-
} catch (error) {
|
|
1302
|
-
if (!isENOENT2(error)) throw error;
|
|
1303
|
-
continue;
|
|
1304
|
-
}
|
|
1305
|
-
if (text.trim() === "") continue;
|
|
1306
|
-
return desktopPath;
|
|
1307
|
-
}
|
|
1308
|
-
return void 0;
|
|
1309
|
-
}
|
|
1310
|
-
/** Whether any desktop-file candidate exists as a regular file; diagnostics only. */
|
|
1311
|
-
async desktopFilePresent() {
|
|
1312
|
-
return await this.resolvedDesktopAuthPath() !== void 0;
|
|
1313
|
-
}
|
|
1314
|
-
};
|
|
1315
|
-
|
|
1316
|
-
// upstream/trae/src/paths.ts
|
|
1317
|
-
import { homedir as homedir2 } from "node:os";
|
|
1318
|
-
import { join as join3 } from "node:path";
|
|
1319
|
-
var APP_NAMES = {
|
|
1320
|
-
cn: "Trae CN",
|
|
1321
|
-
sg: "Trae",
|
|
1322
|
-
solo: "TRAE SOLO CN",
|
|
1323
|
-
"solo-sg": "TRAE SOLO"
|
|
1324
|
-
};
|
|
1325
|
-
var CLI_HOME_NAMES = [".trae-cn", ".trae"];
|
|
1326
|
-
var TRAE_CLI_TOKEN_FILENAME = "trae-jwt-token";
|
|
1327
|
-
var LINUX_APP_NAMES = {
|
|
1328
|
-
cn: ["trae-cn", "Trae CN", "trae", "Trae"],
|
|
1329
|
-
sg: ["trae", "Trae"],
|
|
1330
|
-
solo: ["trae-solo-cn", "TRAE SOLO CN"],
|
|
1331
|
-
"solo-sg": ["trae-solo", "TRAE SOLO"]
|
|
1332
|
-
};
|
|
1333
|
-
var WINDOWS_APP_NAMES = {
|
|
1334
|
-
cn: ["Trae CN", "trae-cn"],
|
|
1335
|
-
sg: ["Trae"],
|
|
1336
|
-
solo: ["TRAE SOLO CN", "trae-solo-cn"],
|
|
1337
|
-
"solo-sg": ["TRAE SOLO"]
|
|
1338
|
-
};
|
|
1339
|
-
function traeWindowsAppNames(edition) {
|
|
1340
|
-
return WINDOWS_APP_NAMES[edition];
|
|
1341
|
-
}
|
|
1342
|
-
function traeStorageCandidates(platform = process.platform, home = homedir2(), env = process.env) {
|
|
1343
|
-
const result = [];
|
|
1344
|
-
for (const edition of ["cn", "sg", "solo", "solo-sg"]) {
|
|
1345
|
-
const app = APP_NAMES[edition];
|
|
1346
|
-
let roots;
|
|
1347
|
-
let appNames;
|
|
1348
|
-
if (platform === "darwin") {
|
|
1349
|
-
roots = [join3(home, "Library", "Application Support")];
|
|
1350
|
-
appNames = [app];
|
|
1351
|
-
} else if (platform === "win32") {
|
|
1352
|
-
roots = [env.APPDATA, join3(home, "AppData", "Roaming")].filter((value, index, all) => typeof value === "string" && value !== "" && all.indexOf(value) === index);
|
|
1353
|
-
appNames = WINDOWS_APP_NAMES[edition];
|
|
1354
|
-
} else if (platform === "linux") {
|
|
1355
|
-
roots = [env.XDG_CONFIG_HOME || join3(home, ".config")];
|
|
1356
|
-
appNames = LINUX_APP_NAMES[edition];
|
|
1357
|
-
} else {
|
|
1358
|
-
roots = [];
|
|
1359
|
-
appNames = [app];
|
|
1360
|
-
}
|
|
1361
|
-
for (const root of roots) {
|
|
1362
|
-
for (const appName of appNames) {
|
|
1363
|
-
result.push({ edition, path: join3(root, appName, "User", "globalStorage", "storage.json"), source: "desktop" });
|
|
1364
|
-
}
|
|
1365
|
-
}
|
|
1366
|
-
}
|
|
1367
|
-
return [...result, ...traeCliCandidates(platform, home, env)];
|
|
1368
|
-
}
|
|
1369
|
-
function traeCliCandidates(platform = process.platform, home = homedir2(), env = process.env) {
|
|
1370
|
-
const roots = [];
|
|
1371
|
-
if (platform === "win32") {
|
|
1372
|
-
for (const value of [env.USERPROFILE, home]) {
|
|
1373
|
-
if (typeof value === "string" && value !== "" && !roots.includes(value)) roots.push(value);
|
|
1374
|
-
}
|
|
1375
|
-
} else {
|
|
1376
|
-
roots.push(home);
|
|
1377
|
-
}
|
|
1378
|
-
const result = [];
|
|
1379
|
-
for (const root of roots) {
|
|
1380
|
-
for (const name of CLI_HOME_NAMES) {
|
|
1381
|
-
const edition = name === ".trae-cn" ? "cn" : "sg";
|
|
1382
|
-
result.push({ edition, path: join3(root, name, TRAE_CLI_TOKEN_FILENAME), source: "cli" });
|
|
1383
|
-
}
|
|
1384
|
-
}
|
|
1385
|
-
return result;
|
|
1386
|
-
}
|
|
1387
|
-
|
|
1388
|
-
// upstream/trae/src/auth.ts
|
|
1389
|
-
import { readFile as readFile2, rm as rm2, stat } from "node:fs/promises";
|
|
1390
|
-
import { createHash as createHash3 } from "node:crypto";
|
|
1391
|
-
import { join as join4 } from "node:path";
|
|
1392
|
-
import { withFileLock as withFileLock2, writeFileAtomic as writeFileAtomic2 } from "@deepseek-ai/dsh-atomic-write";
|
|
1393
|
-
import { resolveDshHome as resolveDshHome2 } from "@deepseek-ai/dsh-home-paths";
|
|
1394
|
-
|
|
1395
|
-
// upstream/trae/src/decrypt.ts
|
|
1396
|
-
import { createDecipheriv as createDecipheriv2, createHash as createHash2 } from "node:crypto";
|
|
1397
|
-
var TRAE_AUTH_STORAGE_KEY = "iCubeAuthInfo://icube.cloudide";
|
|
1398
|
-
var SALT_A = Uint8Array.from([82, 9, 106, 213, 48, 54, 165, 56, 191, 64, 163, 158, 129, 243, 215, 251, 124, 227, 57, 130, 155, 47, 255, 135, 52, 142, 67, 68, 196, 222, 233, 203, 84, 123, 148, 50, 166, 194, 35, 61, 238, 76, 149, 11, 66, 250, 195, 78, 8, 46, 161, 102, 40, 217, 36, 178, 118, 91, 162, 73, 109, 139, 209, 37]);
|
|
1399
|
-
var SALT_B = Uint8Array.from([31, 221, 168, 51, 136, 7, 199, 49, 177, 18, 16, 89, 39, 128, 236, 95, 96, 81, 127, 169, 25, 181, 74, 13, 45, 229, 122, 159, 147, 201, 156, 239, 160, 224, 59, 77, 174, 42, 245, 176, 200, 235, 187, 60, 131, 83, 153, 97, 23, 43, 4, 126, 186, 119, 214, 38, 225, 105, 20, 99, 85, 33, 12, 125]);
|
|
1400
|
-
var SALT_C = Uint8Array.from([191, 192, 216, 250, 122, 246, 220, 97, 31, 254, 98, 27, 8, 72, 71, 176, 135, 99, 96, 18, 127, 101, 203, 104, 211, 102, 191, 125, 37, 72, 150, 156, 51, 229, 121, 35, 17, 153, 141, 177, 110, 131, 150, 128, 172, 255, 254, 6, 18, 140, 55, 62, 236, 249, 135, 64, 135, 12, 117, 4, 89, 149, 168, 209]);
|
|
1401
|
-
var SALT_D = Uint8Array.from([246, 204, 26, 232, 232, 70, 129, 109, 223, 146, 169, 242, 23, 241, 105, 145, 50, 196, 165, 42, 254, 120, 3, 54, 244, 207, 209, 85, 53, 6, 138, 106, 175, 148, 31, 204, 186, 186, 165, 182, 87, 142, 49, 10, 39, 110, 26, 154, 86, 56, 173, 125, 18, 64, 198, 225, 99, 99, 83, 82, 191, 134, 76, 170]);
|
|
1402
|
-
function xor(a, b) {
|
|
1403
|
-
return Buffer.from(a.map((value, index) => value ^ (b[index] ?? 0)));
|
|
1404
|
-
}
|
|
1405
|
-
function encryptionType(header) {
|
|
1406
|
-
if (header.equals(Buffer.from([116, 99, 5, 16, 0, 0]))) return "aes";
|
|
1407
|
-
if (header.equals(Buffer.from([18, 57, 32, 32, 2, 3]))) return "aes-private";
|
|
1408
|
-
throw new Error("unsupported Trae auth encryption header");
|
|
1409
|
-
}
|
|
1410
|
-
function decryptTraeStorageValue(encoded) {
|
|
1411
|
-
const buffer = Buffer.from(encoded, "base64");
|
|
1412
|
-
if (buffer.length <= 102) throw new Error("Trae auth ciphertext is too short");
|
|
1413
|
-
const type = encryptionType(buffer.subarray(0, 6));
|
|
1414
|
-
const random = buffer.subarray(6, 38);
|
|
1415
|
-
const encrypted = buffer.subarray(38);
|
|
1416
|
-
const salt = type === "aes-private" ? xor(SALT_C, SALT_D) : xor(SALT_A, SALT_B);
|
|
1417
|
-
const first = createHash2("sha512").update(random).digest();
|
|
1418
|
-
const derived = createHash2("sha512").update(Buffer.concat([first, salt])).digest();
|
|
1419
|
-
const decipher = createDecipheriv2("aes-128-cbc", derived.subarray(0, 16), derived.subarray(16, 32));
|
|
1420
|
-
const decrypted = Buffer.concat([decipher.update(encrypted), decipher.final()]);
|
|
1421
|
-
if (decrypted.length < 64) throw new Error("Trae auth plaintext is too short");
|
|
1422
|
-
const expected = decrypted.subarray(0, 64);
|
|
1423
|
-
const plaintext = decrypted.subarray(64);
|
|
1424
|
-
const actual = createHash2("sha512").update(plaintext).digest();
|
|
1425
|
-
if (!expected.equals(actual)) throw new Error("Trae auth integrity check failed");
|
|
1426
|
-
return plaintext.toString("utf8");
|
|
1427
|
-
}
|
|
1428
|
-
function parseTraeAuthValue(value) {
|
|
1429
|
-
const trimmed = value.trim();
|
|
1430
|
-
if (trimmed === "") throw new Error("Trae auth value is empty");
|
|
1431
|
-
const plaintext = trimmed.startsWith("{") ? trimmed : decryptTraeStorageValue(trimmed);
|
|
1432
|
-
return JSON.parse(plaintext);
|
|
1433
|
-
}
|
|
1434
|
-
function parseTraeStorageDocument(text) {
|
|
1435
|
-
const parsed = JSON.parse(text);
|
|
1436
|
-
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new Error("Trae storage document must be an object");
|
|
1437
|
-
const value = parsed[TRAE_AUTH_STORAGE_KEY];
|
|
1438
|
-
if (typeof value !== "string") throw new Error(`Trae storage document has no ${TRAE_AUTH_STORAGE_KEY}`);
|
|
1439
|
-
return parseTraeAuthValue(value);
|
|
1440
|
-
}
|
|
1441
|
-
function decodeBase64UrlJson(segment) {
|
|
1442
|
-
try {
|
|
1443
|
-
const padded = segment.replace(/-/g, "+").replace(/_/g, "/");
|
|
1444
|
-
const decoded = Buffer.from(padded, "base64").toString("utf8");
|
|
1445
|
-
const parsed = JSON.parse(decoded);
|
|
1446
|
-
return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed : void 0;
|
|
1447
|
-
} catch {
|
|
1448
|
-
return void 0;
|
|
1449
|
-
}
|
|
1450
|
-
}
|
|
1451
|
-
function parseTraeCliToken(text) {
|
|
1452
|
-
const trimmed = text.trim();
|
|
1453
|
-
if (trimmed === "") throw new Error("Trae CLI token file is empty");
|
|
1454
|
-
let token = trimmed;
|
|
1455
|
-
if (trimmed.startsWith("{")) {
|
|
1456
|
-
const envelope = JSON.parse(trimmed);
|
|
1457
|
-
const candidate = envelope["token"] ?? envelope["accessToken"] ?? envelope["jwt"];
|
|
1458
|
-
if (typeof candidate !== "string" || candidate.trim() === "") throw new Error("Trae CLI token document has no token field");
|
|
1459
|
-
token = candidate.trim();
|
|
1460
|
-
}
|
|
1461
|
-
const segments = token.split(".");
|
|
1462
|
-
if (segments.length !== 3 || segments.some((segment) => segment === "")) throw new Error("Trae CLI token is not a three-part JWT");
|
|
1463
|
-
const payload = decodeBase64UrlJson(segments[1]);
|
|
1464
|
-
if (payload === void 0) throw new Error("Trae CLI token payload is not decodable JSON");
|
|
1465
|
-
const data = typeof payload["data"] === "object" && payload["data"] !== null && !Array.isArray(payload["data"]) ? payload["data"] : void 0;
|
|
1466
|
-
const userId = typeof data?.["user_id"] === "string" ? data["user_id"] : void 0;
|
|
1467
|
-
if (userId === void 0 || userId === "") throw new Error("Trae CLI token has no data.user_id claim");
|
|
1468
|
-
const exp = payload["exp"];
|
|
1469
|
-
const expiresAtMs = typeof exp === "number" && Number.isFinite(exp) && exp > 0 ? exp * 1e3 : void 0;
|
|
1470
|
-
return { accessToken: token, userId, ...expiresAtMs === void 0 ? {} : { expiresAtMs } };
|
|
1471
|
-
}
|
|
1472
|
-
|
|
1473
|
-
// upstream/trae/src/auth.ts
|
|
1474
|
-
var CLI_DEFAULT_HOST = "https://api.trae.cn";
|
|
1475
|
-
var OWN_VERSION = 1;
|
|
1476
|
-
var TRAE_AUTH_FILENAME = ".trae-auth.json";
|
|
1477
|
-
var TRAE_OWN_PREFIX = ".trae-auth";
|
|
1478
|
-
function traeOwnAuthPath(region) {
|
|
1479
|
-
return join4(resolveDshHome2(), `${TRAE_OWN_PREFIX}.${region}.json`);
|
|
1480
|
-
}
|
|
1481
|
-
function legacyTraeOwnAuthPath() {
|
|
1482
|
-
return join4(resolveDshHome2(), TRAE_AUTH_FILENAME);
|
|
1483
|
-
}
|
|
1484
|
-
function optionalString2(value) {
|
|
1485
|
-
return typeof value === "string" && value !== "" ? value : void 0;
|
|
1486
|
-
}
|
|
1487
|
-
function timeToMs(value) {
|
|
1488
|
-
if (typeof value === "number" && Number.isFinite(value) && value > 0) return value > 1e12 ? value : value * 1e3;
|
|
1489
|
-
if (typeof value !== "string" || value.trim() === "") return void 0;
|
|
1490
|
-
const numeric = Number(value);
|
|
1491
|
-
if (Number.isFinite(numeric) && numeric > 0) return numeric > 1e12 ? numeric : numeric * 1e3;
|
|
1492
|
-
const parsed = Date.parse(value);
|
|
1493
|
-
return Number.isFinite(parsed) ? parsed : void 0;
|
|
1494
|
-
}
|
|
1495
|
-
function userRegionOf(value) {
|
|
1496
|
-
const raw = typeof value === "object" && value !== null && !Array.isArray(value) ? value["region"] : value;
|
|
1497
|
-
return typeof raw === "string" && raw.trim() !== "" ? raw.trim() : void 0;
|
|
1498
|
-
}
|
|
1499
|
-
function normalizeTraeCredential(raw, edition, source) {
|
|
1500
|
-
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return void 0;
|
|
1501
|
-
const value = raw;
|
|
1502
|
-
const accessToken = optionalString2(value["token"]) ?? optionalString2(value["accessToken"]);
|
|
1503
|
-
if (accessToken === void 0) return void 0;
|
|
1504
|
-
const expiresAtMs = timeToMs(value["expiredAt"] ?? value["expiresAt"]) ?? 0;
|
|
1505
|
-
const refreshExpiresAtMs = timeToMs(value["refreshExpiredAt"] ?? value["refreshExpiresAt"]);
|
|
1506
|
-
const refreshToken = optionalString2(value["refreshToken"]);
|
|
1507
|
-
const userRegion = userRegionOf(value["userRegion"]);
|
|
1508
|
-
const account = typeof value["account"] === "object" && value["account"] !== null && !Array.isArray(value["account"]) ? value["account"] : void 0;
|
|
1509
|
-
const accountName = optionalString2(account?.["username"]);
|
|
1510
|
-
return {
|
|
1511
|
-
accessToken,
|
|
1512
|
-
...refreshToken === void 0 ? {} : { refreshToken },
|
|
1513
|
-
userId: optionalString2(value["userId"]) ?? "",
|
|
1514
|
-
...accountName === void 0 ? {} : { accountName },
|
|
1515
|
-
host: optionalString2(value["host"]) ?? "",
|
|
1516
|
-
...userRegion === void 0 ? {} : { userRegion },
|
|
1517
|
-
expiresAtMs,
|
|
1518
|
-
...refreshExpiresAtMs === void 0 ? {} : { refreshExpiresAtMs },
|
|
1519
|
-
edition,
|
|
1520
|
-
source
|
|
1521
|
-
};
|
|
1522
|
-
}
|
|
1523
|
-
function traeAccountId(credential) {
|
|
1524
|
-
const stable = `${credential.edition}\0${credential.userId || credential.accountName || "unknown"}`;
|
|
1525
|
-
return createHash3("sha256").update(stable).digest("hex").slice(0, 24);
|
|
1526
|
-
}
|
|
1527
|
-
function parseOwn(text) {
|
|
1528
|
-
try {
|
|
1529
|
-
const document = JSON.parse(text);
|
|
1530
|
-
if (document.version !== OWN_VERSION || typeof document.credential !== "object" || document.credential === null) return void 0;
|
|
1531
|
-
const stored = document.credential;
|
|
1532
|
-
const edition = stored["edition"];
|
|
1533
|
-
if (edition !== "cn" && edition !== "sg" && edition !== "solo" && edition !== "solo-sg") return void 0;
|
|
1534
|
-
return normalizeTraeCredential({
|
|
1535
|
-
token: stored["accessToken"],
|
|
1536
|
-
refreshToken: stored["refreshToken"],
|
|
1537
|
-
userId: stored["userId"],
|
|
1538
|
-
host: stored["host"],
|
|
1539
|
-
userRegion: stored["userRegion"],
|
|
1540
|
-
account: stored["accountName"] === void 0 ? void 0 : { username: stored["accountName"] },
|
|
1541
|
-
expiredAt: stored["expiresAtMs"],
|
|
1542
|
-
refreshExpiredAt: stored["refreshExpiresAtMs"]
|
|
1543
|
-
}, edition, "dsh");
|
|
1544
|
-
} catch {
|
|
1545
|
-
return void 0;
|
|
1546
|
-
}
|
|
1547
|
-
}
|
|
1548
|
-
var TraeCredentialStore = class {
|
|
1549
|
-
storagePathOverride;
|
|
1550
|
-
edition;
|
|
1551
|
-
accountId;
|
|
1552
|
-
region;
|
|
1553
|
-
ownPathExplicit;
|
|
1554
|
-
legacyOwnPath;
|
|
1555
|
-
legacyOwnPathExplicit;
|
|
1556
|
-
refresh;
|
|
1557
|
-
refreshMarginMs;
|
|
1558
|
-
inflight;
|
|
1559
|
-
constructor(options) {
|
|
1560
|
-
this.storagePathOverride = options.storagePath;
|
|
1561
|
-
this.edition = options.edition ?? "auto";
|
|
1562
|
-
this.accountId = options.accountId;
|
|
1563
|
-
this.region = options.region;
|
|
1564
|
-
this.ownPathExplicit = options.ownPath;
|
|
1565
|
-
this.legacyOwnPath = options.legacyOwnPath ?? legacyTraeOwnAuthPath();
|
|
1566
|
-
this.legacyOwnPathExplicit = options.legacyOwnPath;
|
|
1567
|
-
this.refresh = options.refresh;
|
|
1568
|
-
this.refreshMarginMs = options.refreshMarginMs ?? 5 * 6e4;
|
|
1569
|
-
}
|
|
1570
|
-
/** Whether a credential's own claim belongs to this store's region. */
|
|
1571
|
-
matchesRegion(credential) {
|
|
1572
|
-
return this.region === void 0 || regionOfCredential(credential) === this.region;
|
|
1573
|
-
}
|
|
1574
|
-
/**
|
|
1575
|
-
* The path this store refreshes into: the per-region file for a
|
|
1576
|
-
* region-scoped store, the legacy single file otherwise, or an explicitly
|
|
1577
|
-
* injected path in tests.
|
|
1578
|
-
*/
|
|
1579
|
-
ownAuthPath() {
|
|
1580
|
-
if (this.ownPathExplicit !== void 0) return this.ownPathExplicit;
|
|
1581
|
-
return this.region !== void 0 ? traeOwnAuthPath(this.region) : this.legacyOwnPath;
|
|
1582
|
-
}
|
|
1583
|
-
/**
|
|
1584
|
-
* Every plugin-owned copy to read, most preferred first. A region-scoped
|
|
1585
|
-
* store reads the legacy single copy as its migration source (readAll's
|
|
1586
|
-
* region filter drops it when it carries the other region's credential); an
|
|
1587
|
-
* unscoped store reads everything so diagnostics see both regions.
|
|
1588
|
-
*
|
|
1589
|
-
* With an explicitly injected own path the legacy source is read ONLY when
|
|
1590
|
-
* it was injected too — a test that pins one file must not accidentally see
|
|
1591
|
-
* the real machine's legacy copy.
|
|
1592
|
-
*/
|
|
1593
|
-
ownCandidates() {
|
|
1594
|
-
if (this.ownPathExplicit !== void 0) {
|
|
1595
|
-
return this.legacyOwnPathExplicit !== void 0 ? [this.ownPathExplicit, this.legacyOwnPathExplicit] : [this.ownPathExplicit];
|
|
1596
|
-
}
|
|
1597
|
-
if (this.region !== void 0) {
|
|
1598
|
-
return [traeOwnAuthPath(this.region), this.legacyOwnPath];
|
|
1599
|
-
}
|
|
1600
|
-
return [this.legacyOwnPath, traeOwnAuthPath("cn"), traeOwnAuthPath("ai")];
|
|
1601
|
-
}
|
|
1602
|
-
setSource(storagePath, edition = "auto", accountId) {
|
|
1603
|
-
this.storagePathOverride = storagePath;
|
|
1604
|
-
this.edition = edition;
|
|
1605
|
-
this.accountId = accountId;
|
|
1606
|
-
this.inflight = void 0;
|
|
1607
|
-
}
|
|
1608
|
-
selectAccount(accountId) {
|
|
1609
|
-
this.accountId = accountId;
|
|
1610
|
-
this.inflight = void 0;
|
|
1611
|
-
}
|
|
1612
|
-
candidates() {
|
|
1613
|
-
if (this.storagePathOverride !== void 0) {
|
|
1614
|
-
const edition = this.edition === "auto" ? "cn" : this.edition;
|
|
1615
|
-
return [
|
|
1616
|
-
{ edition, path: this.storagePathOverride, source: "desktop" },
|
|
1617
|
-
{ edition, path: this.storagePathOverride, source: "cli" }
|
|
1618
|
-
];
|
|
1619
|
-
}
|
|
1620
|
-
const all = traeStorageCandidates();
|
|
1621
|
-
const cliEdition = "cn";
|
|
1622
|
-
return this.edition === "auto" ? all.filter((candidate) => candidate.source === "desktop" || candidate.edition === cliEdition) : all.filter((candidate) => candidate.edition === this.edition && (candidate.source === "desktop" || candidate.edition === cliEdition));
|
|
1623
|
-
}
|
|
1624
|
-
/**
|
|
1625
|
-
* Deterministic default when no account is explicitly selected: the account
|
|
1626
|
-
* the Trae desktop app is signed in to RIGHT NOW, else the first discovered
|
|
1627
|
-
* one. This is NOT credit-seeking — it never reorders accounts to find one
|
|
1628
|
-
* with general credits. The plugin bills exactly the account the user
|
|
1629
|
-
* selected, the app's current sign-in when nothing has been selected, and
|
|
1630
|
-
* only then the first account.
|
|
1631
|
-
*
|
|
1632
|
-
* The app's live sign-in outranks the plugin's own refreshed copies (issue
|
|
1633
|
-
* #29): after the user switches accounts in Trae, the desktop storage file
|
|
1634
|
-
* holds the NEW login while the plugin's copy of the old one still resolves,
|
|
1635
|
-
* so a bare `credentials[0]` kept defaulting to a sign-in the app has
|
|
1636
|
-
* abandoned — and `dsh-connect-workbuddy`, whose `preferred()` ranks the live
|
|
1637
|
-
* file first, followed the app instead. Ordering only: the VALUE behind an
|
|
1638
|
-
* account id is still whichever copy expires later (`readAll`), so a refresh
|
|
1639
|
-
* the plugin performed is never discarded by this preference.
|
|
1640
|
-
*/
|
|
1641
|
-
preferred(credentials) {
|
|
1642
|
-
return credentials.find((credential) => credential.source === "desktop") ?? credentials[0];
|
|
1643
|
-
}
|
|
1644
|
-
/**
|
|
1645
|
-
* The account the Trae app is signed in to right now, when that differs from
|
|
1646
|
-
* the account this store is bound to (issue #29).
|
|
1647
|
-
*
|
|
1648
|
-
* This is a READ, not a rebind: `current()` keeps resolving the bound
|
|
1649
|
-
* account verbatim so billing never moves without a decision — the store
|
|
1650
|
-
* only reports the mismatch, and the card (hint row) or the host (follow
|
|
1651
|
-
* switch) turns it into a settings write the user can see and undo. Absent
|
|
1652
|
-
* when nothing is live, when the live account IS the bound one, or when the
|
|
1653
|
-
* binding itself already fell through to the live account.
|
|
1654
|
-
*/
|
|
1655
|
-
async liveLogin() {
|
|
1656
|
-
if (this.accountId === void 0 || this.accountId === "") return void 0;
|
|
1657
|
-
const credentials = await this.readAll();
|
|
1658
|
-
const live = this.preferred(credentials);
|
|
1659
|
-
if (live === void 0) return void 0;
|
|
1660
|
-
const liveId = traeAccountId(live);
|
|
1661
|
-
if (liveId === this.accountId) return void 0;
|
|
1662
|
-
return {
|
|
1663
|
-
id: liveId,
|
|
1664
|
-
accountName: live.accountName ?? (live.userId || `${live.edition} account`),
|
|
1665
|
-
edition: live.edition,
|
|
1666
|
-
region: regionOfCredential(live),
|
|
1667
|
-
source: live.source,
|
|
1668
|
-
tokenExpiresAtMs: live.expiresAtMs,
|
|
1669
|
-
selected: false
|
|
1670
|
-
};
|
|
1671
|
-
}
|
|
1672
|
-
async accounts() {
|
|
1673
|
-
const credentials = await this.readAll();
|
|
1674
|
-
const selectedExists = this.accountId !== void 0 && credentials.some((credential) => traeAccountId(credential) === this.accountId);
|
|
1675
|
-
const defaultSelected = this.preferred(credentials);
|
|
1676
|
-
return credentials.map((credential) => ({
|
|
1677
|
-
id: traeAccountId(credential),
|
|
1678
|
-
accountName: credential.accountName ?? (credential.userId || `${credential.edition} account`),
|
|
1679
|
-
edition: credential.edition,
|
|
1680
|
-
region: regionOfCredential(credential),
|
|
1681
|
-
source: credential.source,
|
|
1682
|
-
tokenExpiresAtMs: credential.expiresAtMs,
|
|
1683
|
-
selected: selectedExists ? traeAccountId(credential) === this.accountId : credential === defaultSelected
|
|
1684
|
-
}));
|
|
1685
|
-
}
|
|
1686
|
-
async current() {
|
|
1687
|
-
const credentials = await this.readAll();
|
|
1688
|
-
if (this.accountId === void 0) return this.preferred(credentials);
|
|
1689
|
-
const selected = credentials.find((credential) => traeAccountId(credential) === this.accountId);
|
|
1690
|
-
return selected;
|
|
1691
|
-
}
|
|
1692
|
-
/**
|
|
1693
|
-
* The stored credential of one specific account, without changing which
|
|
1694
|
-
* account this store is bound to (issue #25).
|
|
1695
|
-
*
|
|
1696
|
-
* The card needs to answer "the account I am bound to has run out of credits
|
|
1697
|
-
* — is another account on this machine usable?" and {@link resolve} cannot
|
|
1698
|
-
* help: it always returns the BOUND account, and switching the bound account
|
|
1699
|
-
* just to read one number would race every in-flight chat request sharing
|
|
1700
|
-
* this store, and would persist a selection the user never made.
|
|
1701
|
-
*
|
|
1702
|
-
* So this is a pure lookup: read-only, no refresh side effects (a stale token
|
|
1703
|
-
* is returned as-is and the caller's request then fails honestly), and
|
|
1704
|
-
* `accountId` is left untouched. An unknown id yields undefined rather than a
|
|
1705
|
-
* fallback, for the same reason {@link current} refuses to fall back.
|
|
1706
|
-
*/
|
|
1707
|
-
async credentialOf(accountId) {
|
|
1708
|
-
if (accountId === "") return void 0;
|
|
1709
|
-
const credentials = await this.readAll();
|
|
1710
|
-
return credentials.find((credential) => traeAccountId(credential) === accountId);
|
|
1711
|
-
}
|
|
1712
|
-
async resolve() {
|
|
1713
|
-
const credential = await this.current();
|
|
1714
|
-
if (credential === void 0) throw new Error(`trae: no signed-in account found (${this.candidates().map((item) => item.path).join(" or ")})`);
|
|
1715
|
-
if (credential.expiresAtMs > Date.now() + this.refreshMarginMs) return credential;
|
|
1716
|
-
this.inflight ??= this.refreshNow(credential).finally(() => {
|
|
1717
|
-
this.inflight = void 0;
|
|
1718
|
-
});
|
|
1719
|
-
return this.inflight;
|
|
1720
|
-
}
|
|
1721
|
-
async status() {
|
|
1722
|
-
try {
|
|
1723
|
-
const value = await this.current();
|
|
1724
|
-
return value === void 0 ? { state: "signed-out" } : { state: "signed-in", edition: value.edition, expiresAtMs: value.expiresAtMs, source: value.source };
|
|
1725
|
-
} catch {
|
|
1726
|
-
return { state: "signed-out" };
|
|
1727
|
-
}
|
|
1728
|
-
}
|
|
1729
|
-
async desktopFilePresent() {
|
|
1730
|
-
for (const candidate of this.candidates()) {
|
|
1731
|
-
try {
|
|
1732
|
-
if ((await stat(candidate.path)).isFile()) return true;
|
|
1733
|
-
} catch {
|
|
1734
|
-
}
|
|
1735
|
-
}
|
|
1736
|
-
return false;
|
|
1737
|
-
}
|
|
1738
|
-
/**
|
|
1739
|
-
* Remove every plugin-owned copy this store could read (per-region file,
|
|
1740
|
-
* legacy single file, and their lock siblings); the desktop storage files
|
|
1741
|
-
* are untouched. A region store's logout therefore also clears the legacy
|
|
1742
|
-
* migration source — deliberate: `logout` is the user's "forget what the
|
|
1743
|
-
* plugin stored" action, not a per-account toggle.
|
|
1744
|
-
*/
|
|
1745
|
-
async logout() {
|
|
1746
|
-
for (const path of this.ownCandidates()) {
|
|
1747
|
-
await rm2(path, { force: true });
|
|
1748
|
-
await rm2(`${path}.lock`, { force: true });
|
|
1749
|
-
}
|
|
1750
|
-
}
|
|
1751
|
-
/**
|
|
1752
|
-
* Every local credential of this store's region, deduplicated by account id.
|
|
1753
|
-
* A region-scoped store sees only its own region's credentials: the other
|
|
1754
|
-
* region's accounts are invisible to selection, refresh, and status alike,
|
|
1755
|
-
* which is what keeps the two regions' providers from cross-billing.
|
|
1756
|
-
*/
|
|
1757
|
-
async readAll() {
|
|
1758
|
-
const { credentials: desktop } = await this.readDesktopAll();
|
|
1759
|
-
const byAccount = /* @__PURE__ */ new Map();
|
|
1760
|
-
const consider = (credential) => {
|
|
1761
|
-
if (!this.matchesRegion(credential)) return;
|
|
1762
|
-
const id = traeAccountId(credential);
|
|
1763
|
-
const incumbent = byAccount.get(id);
|
|
1764
|
-
if (incumbent === void 0 || credential.expiresAtMs > incumbent.expiresAtMs) byAccount.set(id, credential);
|
|
1765
|
-
};
|
|
1766
|
-
for (const credential of desktop) consider(credential);
|
|
1767
|
-
for (const own of await this.readOwns()) consider(own);
|
|
1768
|
-
return [...byAccount.values()];
|
|
1769
|
-
}
|
|
1770
|
-
/**
|
|
1771
|
-
* Which paths were tried and why each one failed. Read-only and token-free:
|
|
1772
|
-
* it exists so a signed-out card can explain itself instead of showing a bare
|
|
1773
|
-
* "not signed in", which is undiagnosable on a machine whose layout differs
|
|
1774
|
-
* from the ones the plugin was written against.
|
|
1775
|
-
*/
|
|
1776
|
-
async diagnose() {
|
|
1777
|
-
const tried = this.candidates();
|
|
1778
|
-
const failures = [];
|
|
1779
|
-
for (const candidate of tried) {
|
|
1780
|
-
const raw = await readFile2(candidate.path, "utf8").then(
|
|
1781
|
-
(text) => ({ text }),
|
|
1782
|
-
(error) => ({ error })
|
|
1783
|
-
);
|
|
1784
|
-
if ("error" in raw) {
|
|
1785
|
-
const code = typeof raw.error === "object" && raw.error !== null && "code" in raw.error ? raw.error.code : void 0;
|
|
1786
|
-
failures.push({
|
|
1787
|
-
path: candidate.path,
|
|
1788
|
-
edition: candidate.edition,
|
|
1789
|
-
source: candidate.source,
|
|
1790
|
-
reason: code === "ENOENT" ? "missing" : "unreadable",
|
|
1791
|
-
...code === "ENOENT" ? {} : { message: String(raw.error) }
|
|
1792
|
-
});
|
|
1793
|
-
continue;
|
|
1794
|
-
}
|
|
1795
|
-
try {
|
|
1796
|
-
this.credentialFrom(candidate, raw.text);
|
|
1797
|
-
} catch (error) {
|
|
1798
|
-
failures.push({
|
|
1799
|
-
path: candidate.path,
|
|
1800
|
-
edition: candidate.edition,
|
|
1801
|
-
source: candidate.source,
|
|
1802
|
-
reason: "invalid",
|
|
1803
|
-
message: error instanceof Error ? error.message : String(error)
|
|
1804
|
-
});
|
|
1805
|
-
}
|
|
1806
|
-
}
|
|
1807
|
-
return { tried, failures };
|
|
1808
|
-
}
|
|
1809
|
-
/** Parse one candidate file's text into a credential, or throw. */
|
|
1810
|
-
credentialFrom(candidate, text) {
|
|
1811
|
-
let credential;
|
|
1812
|
-
if (candidate.source === "cli") {
|
|
1813
|
-
if (regionOfEdition(candidate.edition) !== "cn") {
|
|
1814
|
-
throw new Error(`Trae CLI tokens are only verified for the CN region; ${candidate.edition} CLI homes are not supported yet`);
|
|
1815
|
-
}
|
|
1816
|
-
const claims = parseTraeCliToken(text);
|
|
1817
|
-
credential = normalizeTraeCredential({
|
|
1818
|
-
token: claims.accessToken,
|
|
1819
|
-
userId: claims.userId,
|
|
1820
|
-
host: CLI_DEFAULT_HOST,
|
|
1821
|
-
expiredAt: claims.expiresAtMs
|
|
1822
|
-
}, candidate.edition, "cli");
|
|
1823
|
-
} else {
|
|
1824
|
-
credential = normalizeTraeCredential(parseTraeStorageDocument(text), candidate.edition, "desktop");
|
|
1825
|
-
}
|
|
1826
|
-
if (credential === void 0) throw new Error(`${candidate.source} candidate could not be normalized into a credential`);
|
|
1827
|
-
return credential;
|
|
1828
|
-
}
|
|
1829
|
-
async readDesktopAll() {
|
|
1830
|
-
const credentials = [];
|
|
1831
|
-
const failures = [];
|
|
1832
|
-
for (const candidate of this.candidates()) {
|
|
1833
|
-
try {
|
|
1834
|
-
const credential = this.credentialFrom(candidate, await readFile2(candidate.path, "utf8"));
|
|
1835
|
-
if (!credentials.some((existing) => traeAccountId(existing) === traeAccountId(credential))) credentials.push(credential);
|
|
1836
|
-
} catch (error) {
|
|
1837
|
-
const code = typeof error === "object" && error !== null && "code" in error ? error.code : void 0;
|
|
1838
|
-
failures.push({
|
|
1839
|
-
path: candidate.path,
|
|
1840
|
-
edition: candidate.edition,
|
|
1841
|
-
source: candidate.source,
|
|
1842
|
-
reason: code === "ENOENT" ? "missing" : code === void 0 ? "invalid" : "unreadable",
|
|
1843
|
-
...code === "ENOENT" || code === void 0 && !(error instanceof Error) ? {} : { message: error instanceof Error ? error.message : String(error) }
|
|
1844
|
-
});
|
|
1845
|
-
continue;
|
|
1846
|
-
}
|
|
1847
|
-
}
|
|
1848
|
-
return { credentials, failures };
|
|
1849
|
-
}
|
|
1850
|
-
/**
|
|
1851
|
-
* Every readable plugin-owned copy, in candidate order; absent or corrupt
|
|
1852
|
-
* files are skipped rather than propagated.
|
|
1853
|
-
*/
|
|
1854
|
-
async readOwns() {
|
|
1855
|
-
const copies = [];
|
|
1856
|
-
for (const path of this.ownCandidates()) {
|
|
1857
|
-
try {
|
|
1858
|
-
const parsed = parseOwn(await readFile2(path, "utf8"));
|
|
1859
|
-
if (parsed !== void 0) copies.push(parsed);
|
|
1860
|
-
} catch {
|
|
1861
|
-
}
|
|
1862
|
-
}
|
|
1863
|
-
return copies;
|
|
1864
|
-
}
|
|
1865
|
-
async refreshNow(credential) {
|
|
1866
|
-
if (credential.refreshToken === void 0 || credential.refreshExpiresAtMs !== void 0 && credential.refreshExpiresAtMs <= Date.now()) {
|
|
1867
|
-
if (credential.expiresAtMs > Date.now() + 3e4) return credential;
|
|
1868
|
-
throw new Error("trae: access token expired and no valid refresh token is available; sign in again in Trae");
|
|
1869
|
-
}
|
|
1870
|
-
try {
|
|
1871
|
-
const outcome = await this.refresh(credential);
|
|
1872
|
-
const refreshed = {
|
|
1873
|
-
...credential,
|
|
1874
|
-
accessToken: outcome.accessToken,
|
|
1875
|
-
...outcome.refreshToken === void 0 ? {} : { refreshToken: outcome.refreshToken },
|
|
1876
|
-
expiresAtMs: outcome.expiresAtMs,
|
|
1877
|
-
...outcome.refreshExpiresAtMs === void 0 ? {} : { refreshExpiresAtMs: outcome.refreshExpiresAtMs },
|
|
1878
|
-
...outcome.host === void 0 ? {} : { host: outcome.host },
|
|
1879
|
-
source: "dsh"
|
|
1880
|
-
};
|
|
1881
|
-
const ownPath = this.ownAuthPath();
|
|
1882
|
-
await withFileLock2(ownPath, async () => {
|
|
1883
|
-
await writeFileAtomic2(ownPath, `${JSON.stringify({ version: OWN_VERSION, credential: refreshed }, null, 2)}
|
|
1884
|
-
`, { mode: 384, dirMode: 448 });
|
|
1885
|
-
});
|
|
1886
|
-
return refreshed;
|
|
1887
|
-
} catch (error) {
|
|
1888
|
-
if (credential.expiresAtMs > Date.now() + 3e4) return credential;
|
|
1889
|
-
throw new Error(`trae: token refresh failed and access token is expired (${String(error)}); sign in again in Trae`);
|
|
1890
|
-
}
|
|
1891
|
-
}
|
|
1892
|
-
};
|
|
1893
|
-
|
|
1894
|
-
// upstream/trae/src/identity.ts
|
|
1895
|
-
import { createHash as createHash4 } from "node:crypto";
|
|
1896
|
-
import { readFile as readFile3 } from "node:fs/promises";
|
|
1897
|
-
import { dirname as dirname3, join as join5 } from "node:path";
|
|
1898
|
-
import { cpus, homedir as homedir3, release as release2, userInfo } from "node:os";
|
|
1899
|
-
function nonEmpty(value) {
|
|
1900
|
-
return typeof value === "string" && value.trim() !== "" ? value.trim() : void 0;
|
|
1901
|
-
}
|
|
1902
|
-
function deviceCenterId(storage) {
|
|
1903
|
-
const prefix = "iCubeAuthInfo://icube-dc:";
|
|
1904
|
-
const ids = Object.keys(storage).filter((key) => key.startsWith(prefix)).map((key) => key.slice(prefix.length)).filter(Boolean);
|
|
1905
|
-
return ids.length === 1 ? ids[0] : void 0;
|
|
1906
|
-
}
|
|
1907
|
-
function deviceKeyPairOf(storage) {
|
|
1908
|
-
const key = Object.keys(storage).find((name) => name.startsWith("iCubeAuthInfo://icube-dc:"));
|
|
1909
|
-
if (key === void 0) return void 0;
|
|
1910
|
-
const encoded = storage[key];
|
|
1911
|
-
if (typeof encoded !== "string" || encoded.trim() === "") return void 0;
|
|
1912
|
-
try {
|
|
1913
|
-
const plaintext = parseTraeAuthValue(encoded);
|
|
1914
|
-
if (typeof plaintext !== "object" || plaintext === null || Array.isArray(plaintext)) return void 0;
|
|
1915
|
-
const value = plaintext;
|
|
1916
|
-
const privateKeyPEM = nonEmpty(value["privateKeyPEM"]);
|
|
1917
|
-
const publicKeyPEM = nonEmpty(value["publicKeyPEM"]);
|
|
1918
|
-
return privateKeyPEM === void 0 || publicKeyPEM === void 0 ? void 0 : { privateKeyPEM, publicKeyPEM };
|
|
1919
|
-
} catch {
|
|
1920
|
-
return void 0;
|
|
1921
|
-
}
|
|
1922
|
-
}
|
|
1923
|
-
async function readTraeIdentity(candidate, options = {}) {
|
|
1924
|
-
const platform = options.platform ?? process.platform;
|
|
1925
|
-
const home = options.home ?? homedir3();
|
|
1926
|
-
const env = options.env ?? process.env;
|
|
1927
|
-
const storage = JSON.parse(await readFile3(candidate.path, "utf8"));
|
|
1928
|
-
const appRoot = dirname3(dirname3(dirname3(candidate.path)));
|
|
1929
|
-
const machineFile = nonEmpty(await readFile3(join5(appRoot, "machineid"), "utf8").catch(() => ""));
|
|
1930
|
-
const telemetryMachine = nonEmpty(storage["telemetry.machineId"]);
|
|
1931
|
-
const devDevice = nonEmpty(storage["telemetry.devDeviceId"]);
|
|
1932
|
-
const dcDevice = deviceCenterId(storage);
|
|
1933
|
-
const machineId = telemetryMachine ?? machineFile;
|
|
1934
|
-
if (machineId === void 0) throw new Error(`Trae ${candidate.edition} has no stable machine identity`);
|
|
1935
|
-
const deviceId = dcDevice ?? devDevice ?? createHash4("sha256").update(machineId).digest("hex").slice(0, 32);
|
|
1936
|
-
const buildVersion = nonEmpty(storage["iCubeLastVersion"]);
|
|
1937
|
-
const APP_NAMES_BY_EDITION = {
|
|
1938
|
-
cn: "Trae CN",
|
|
1939
|
-
sg: "Trae",
|
|
1940
|
-
solo: "TRAE SOLO CN",
|
|
1941
|
-
"solo-sg": "TRAE SOLO"
|
|
1942
|
-
};
|
|
1943
|
-
const appName = APP_NAMES_BY_EDITION[candidate.edition];
|
|
1944
|
-
const productPaths = [];
|
|
1945
|
-
if (appName !== void 0 && (platform === "darwin" || platform === "win32")) {
|
|
1946
|
-
if (platform === "darwin") {
|
|
1947
|
-
productPaths.push(join5("/Applications", `${appName}.app`, "Contents", "Resources", "app", "product.json"));
|
|
1948
|
-
} else {
|
|
1949
|
-
const localRoots = [env.LOCALAPPDATA, join5(home, "AppData", "Local")].filter((value) => typeof value === "string" && value !== "").filter((value, index, all) => all.indexOf(value) === index);
|
|
1950
|
-
for (const root of localRoots) {
|
|
1951
|
-
for (const spelling of traeWindowsAppNames(candidate.edition)) {
|
|
1952
|
-
productPaths.push(join5(root, "Programs", spelling, "resources", "app", "product.json"));
|
|
1953
|
-
}
|
|
1954
|
-
}
|
|
1955
|
-
}
|
|
1956
|
-
}
|
|
1957
|
-
let product = {};
|
|
1958
|
-
for (const path of productPaths) {
|
|
1959
|
-
try {
|
|
1960
|
-
product = JSON.parse(await readFile3(path, "utf8"));
|
|
1961
|
-
break;
|
|
1962
|
-
} catch {
|
|
1963
|
-
}
|
|
1964
|
-
}
|
|
1965
|
-
const appVersion = nonEmpty(product["appVersion"]);
|
|
1966
|
-
const deviceBrand = platform === "darwin" ? nonEmpty(env["TRAE_DEVICE_BRAND"]) : void 0;
|
|
1967
|
-
const deviceCpu = cpus()[0]?.model.split(" ")[0];
|
|
1968
|
-
const osVersion = `${platform === "darwin" ? "macOS" : platform === "win32" ? "Windows" : platform} ${release2()}`;
|
|
1969
|
-
const deviceName = (() => {
|
|
1970
|
-
try {
|
|
1971
|
-
return nonEmpty(userInfo().username);
|
|
1972
|
-
} catch {
|
|
1973
|
-
return void 0;
|
|
1974
|
-
}
|
|
1975
|
-
})();
|
|
1976
|
-
const deviceModel = nonEmpty(env["TRAE_DEVICE_MODEL"]);
|
|
1977
|
-
const deviceKeyPair = deviceKeyPairOf(storage);
|
|
1978
|
-
return {
|
|
1979
|
-
edition: candidate.edition,
|
|
1980
|
-
machineId,
|
|
1981
|
-
deviceId,
|
|
1982
|
-
...appVersion === void 0 ? {} : { appVersion },
|
|
1983
|
-
...buildVersion === void 0 ? {} : { buildVersion },
|
|
1984
|
-
...deviceBrand === void 0 ? {} : { deviceBrand },
|
|
1985
|
-
...deviceCpu === void 0 ? {} : { deviceCpu },
|
|
1986
|
-
...deviceName === void 0 ? {} : { deviceName },
|
|
1987
|
-
...deviceModel === void 0 ? {} : { deviceModel },
|
|
1988
|
-
...deviceKeyPair === void 0 ? {} : { deviceKeyPair },
|
|
1989
|
-
osVersion,
|
|
1990
|
-
platform
|
|
1991
|
-
};
|
|
1992
|
-
}
|
|
1993
|
-
function identityHeaders(identity) {
|
|
1994
|
-
return {
|
|
1995
|
-
"x-machine-id": identity.machineId,
|
|
1996
|
-
"x-device-id": identity.deviceId,
|
|
1997
|
-
"x-device-type": identity.platform === "darwin" ? "mac" : identity.platform === "win32" ? "windows" : identity.platform,
|
|
1998
|
-
...identity.deviceBrand === void 0 ? {} : { "x-device-brand": identity.deviceBrand },
|
|
1999
|
-
...identity.deviceCpu === void 0 ? {} : { "x-device-cpu": identity.deviceCpu },
|
|
2000
|
-
...identity.osVersion === void 0 ? {} : { "x-os-version": identity.osVersion },
|
|
2001
|
-
...identity.appVersion === void 0 ? {} : { "x-app-version": identity.appVersion, "x-ide-version": identity.appVersion },
|
|
2002
|
-
...identity.buildVersion === void 0 ? {} : { "x-app-version-code": identity.buildVersion, "x-ide-version-code": identity.buildVersion },
|
|
2003
|
-
"x-ide-version-type": "stable"
|
|
2004
|
-
};
|
|
2005
|
-
}
|
|
2006
|
-
|
|
2007
|
-
export {
|
|
2008
|
-
WORKBUDDY_VARIANTS,
|
|
2009
|
-
atRestKeyProviderFor,
|
|
2010
|
-
WorkBuddyCredentialStore,
|
|
2011
|
-
traeStorageCandidates,
|
|
2012
|
-
traeAccountId,
|
|
2013
|
-
TraeCredentialStore,
|
|
2014
|
-
readTraeIdentity,
|
|
2015
|
-
identityHeaders
|
|
2016
|
-
};
|
|
2017
|
-
//# sourceMappingURL=chunk-FLL3DUVP.js.map
|