@fruggr/zendesk-mcp-server 3.0.3 → 3.1.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 +4 -3
- package/dist/THIRD-PARTY-NOTICES.md +5272 -0
- package/dist/http-CmtU-oNu.js +36570 -0
- package/dist/index.js +102811 -1489
- package/dist/sbom.cdx.json +2152 -0
- package/dist/token-persistence-BeetFqPF.js +147 -0
- package/dist/token-store-CLtwRaxI.js +972 -0
- package/package.json +18 -35
- package/dist/http-BXXDmcaZ.js +0 -1383
|
@@ -0,0 +1,972 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { c as getOAuthUrls, r as silentLogger } from "./index.js";
|
|
3
|
+
import { a as saveToken, c as grantCovers, d as escapeHtml, i as resolveTokenPath, l as requestedScope, o as generateCodeChallenge, r as loadToken, s as generateCodeVerifier, t as clearToken } from "./token-persistence-BeetFqPF.js";
|
|
4
|
+
import { promisify } from "node:util";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import process$1 from "node:process";
|
|
8
|
+
import { createServer } from "node:http";
|
|
9
|
+
import { Buffer } from "node:buffer";
|
|
10
|
+
import fs, { constants } from "node:fs/promises";
|
|
11
|
+
import fs$1, { readFileSync } from "node:fs";
|
|
12
|
+
import os, { release } from "node:os";
|
|
13
|
+
import childProcess, { execFile } from "node:child_process";
|
|
14
|
+
//#region node_modules/.pnpm/is-docker@3.0.0/node_modules/is-docker/index.js
|
|
15
|
+
let isDockerCached;
|
|
16
|
+
function hasDockerEnv() {
|
|
17
|
+
try {
|
|
18
|
+
fs$1.statSync("/.dockerenv");
|
|
19
|
+
return true;
|
|
20
|
+
} catch {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function hasDockerCGroup() {
|
|
25
|
+
try {
|
|
26
|
+
return fs$1.readFileSync("/proc/self/cgroup", "utf8").includes("docker");
|
|
27
|
+
} catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function isDocker() {
|
|
32
|
+
if (isDockerCached === void 0) isDockerCached = hasDockerEnv() || hasDockerCGroup();
|
|
33
|
+
return isDockerCached;
|
|
34
|
+
}
|
|
35
|
+
//#endregion
|
|
36
|
+
//#region node_modules/.pnpm/is-inside-container@1.0.0/node_modules/is-inside-container/index.js
|
|
37
|
+
let cachedResult;
|
|
38
|
+
const hasContainerEnv = () => {
|
|
39
|
+
try {
|
|
40
|
+
fs$1.statSync("/run/.containerenv");
|
|
41
|
+
return true;
|
|
42
|
+
} catch {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
function isInsideContainer() {
|
|
47
|
+
if (cachedResult === void 0) cachedResult = hasContainerEnv() || isDocker();
|
|
48
|
+
return cachedResult;
|
|
49
|
+
}
|
|
50
|
+
//#endregion
|
|
51
|
+
//#region node_modules/.pnpm/is-wsl@3.1.1/node_modules/is-wsl/index.js
|
|
52
|
+
const isWsl = () => {
|
|
53
|
+
if (process$1.platform !== "linux") return false;
|
|
54
|
+
if (os.release().toLowerCase().includes("microsoft")) {
|
|
55
|
+
if (isInsideContainer()) return false;
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
if (fs$1.readFileSync("/proc/version", "utf8").toLowerCase().includes("microsoft")) return !isInsideContainer();
|
|
60
|
+
} catch {}
|
|
61
|
+
if (fs$1.existsSync("/proc/sys/fs/binfmt_misc/WSLInterop") || fs$1.existsSync("/run/WSL")) return !isInsideContainer();
|
|
62
|
+
return false;
|
|
63
|
+
};
|
|
64
|
+
var is_wsl_default = process$1.env.__IS_WSL_TEST__ ? isWsl : isWsl();
|
|
65
|
+
//#endregion
|
|
66
|
+
//#region node_modules/.pnpm/powershell-utils@0.1.0/node_modules/powershell-utils/index.js
|
|
67
|
+
const execFile$3 = promisify(childProcess.execFile);
|
|
68
|
+
const powerShellPath$2 = () => `${process$1.env.SYSTEMROOT || process$1.env.windir || String.raw`C:\Windows`}\\System32\\WindowsPowerShell\\v1.0\\powershell.exe`;
|
|
69
|
+
const executePowerShell$1 = async (command, options = {}) => {
|
|
70
|
+
const { powerShellPath: psPath, ...execFileOptions } = options;
|
|
71
|
+
const encodedCommand = executePowerShell$1.encodeCommand(command);
|
|
72
|
+
return execFile$3(psPath ?? powerShellPath$2(), [...executePowerShell$1.argumentsPrefix, encodedCommand], {
|
|
73
|
+
encoding: "utf8",
|
|
74
|
+
...execFileOptions
|
|
75
|
+
});
|
|
76
|
+
};
|
|
77
|
+
executePowerShell$1.argumentsPrefix = [
|
|
78
|
+
"-NoProfile",
|
|
79
|
+
"-NonInteractive",
|
|
80
|
+
"-ExecutionPolicy",
|
|
81
|
+
"Bypass",
|
|
82
|
+
"-EncodedCommand"
|
|
83
|
+
];
|
|
84
|
+
executePowerShell$1.encodeCommand = (command) => Buffer.from(command, "utf16le").toString("base64");
|
|
85
|
+
executePowerShell$1.escapeArgument = (value) => `'${String(value).replaceAll("'", "''")}'`;
|
|
86
|
+
//#endregion
|
|
87
|
+
//#region node_modules/.pnpm/wsl-utils@1.0.0/node_modules/wsl-utils/utilities.js
|
|
88
|
+
function parseMountPointFromConfig(content) {
|
|
89
|
+
for (const line of content.split("\n")) {
|
|
90
|
+
if (/^\s*#/.test(line)) continue;
|
|
91
|
+
const match = /^\s*root\s*=\s*(?<mountPoint>"[^"]*"|'[^']*'|[^#]*)/.exec(line);
|
|
92
|
+
if (!match) continue;
|
|
93
|
+
return match.groups.mountPoint.trim().replaceAll(/^["']|["']$/g, "");
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
//#endregion
|
|
97
|
+
//#region node_modules/.pnpm/wsl-utils@1.0.0/node_modules/wsl-utils/index.js
|
|
98
|
+
const execFile$2 = promisify(childProcess.execFile);
|
|
99
|
+
const wslDrivesMountPoint = (() => {
|
|
100
|
+
const defaultMountPoint = "/mnt/";
|
|
101
|
+
let mountPoint;
|
|
102
|
+
return async function() {
|
|
103
|
+
if (mountPoint) return mountPoint;
|
|
104
|
+
const configFilePath = "/etc/wsl.conf";
|
|
105
|
+
let isConfigFileExists = false;
|
|
106
|
+
try {
|
|
107
|
+
await fs.access(configFilePath, constants.F_OK);
|
|
108
|
+
isConfigFileExists = true;
|
|
109
|
+
} catch {}
|
|
110
|
+
if (!isConfigFileExists) return defaultMountPoint;
|
|
111
|
+
const parsedMountPoint = parseMountPointFromConfig(await fs.readFile(configFilePath, { encoding: "utf8" }));
|
|
112
|
+
if (parsedMountPoint === void 0) return defaultMountPoint;
|
|
113
|
+
mountPoint = parsedMountPoint;
|
|
114
|
+
mountPoint = mountPoint.endsWith("/") ? mountPoint : `${mountPoint}/`;
|
|
115
|
+
return mountPoint;
|
|
116
|
+
};
|
|
117
|
+
})();
|
|
118
|
+
const powerShellPathFromWsl = async () => {
|
|
119
|
+
return `${await wslDrivesMountPoint()}c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe`;
|
|
120
|
+
};
|
|
121
|
+
const powerShellPath$1 = is_wsl_default ? powerShellPathFromWsl : powerShellPath$2;
|
|
122
|
+
let canAccessPowerShellPromise;
|
|
123
|
+
const canAccessPowerShell = async () => {
|
|
124
|
+
canAccessPowerShellPromise ??= (async () => {
|
|
125
|
+
try {
|
|
126
|
+
const psPath = await powerShellPath$1();
|
|
127
|
+
await fs.access(psPath, constants.X_OK);
|
|
128
|
+
return true;
|
|
129
|
+
} catch {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
})();
|
|
133
|
+
return canAccessPowerShellPromise;
|
|
134
|
+
};
|
|
135
|
+
const wslDefaultBrowser = async () => {
|
|
136
|
+
const psPath = await powerShellPath$1();
|
|
137
|
+
const command = String.raw`(Get-ItemProperty -Path "HKCU:\Software\Microsoft\Windows\Shell\Associations\UrlAssociations\http\UserChoice").ProgId`;
|
|
138
|
+
const { stdout } = await executePowerShell$1(command, {
|
|
139
|
+
powerShellPath: psPath,
|
|
140
|
+
cwd: path.dirname(psPath)
|
|
141
|
+
});
|
|
142
|
+
return stdout.trim();
|
|
143
|
+
};
|
|
144
|
+
const isUrl = (path) => /^[a-z]+:\/\//i.test(path);
|
|
145
|
+
const convertWslPathToWindows = async (paths) => {
|
|
146
|
+
const isBatch = Array.isArray(paths);
|
|
147
|
+
const pathArray = isBatch ? paths : [paths];
|
|
148
|
+
const indicesToConvert = [];
|
|
149
|
+
const pathsToConvert = [];
|
|
150
|
+
for (const [index, path] of pathArray.entries()) if (!isUrl(path)) {
|
|
151
|
+
indicesToConvert.push(index);
|
|
152
|
+
pathsToConvert.push(path);
|
|
153
|
+
}
|
|
154
|
+
const results = [...pathArray];
|
|
155
|
+
if (pathsToConvert.length > 0) try {
|
|
156
|
+
const { stdout } = await execFile$2("wslpath", ["-aw", ...pathsToConvert], { encoding: "utf8" });
|
|
157
|
+
const convertedPaths = stdout.split(/\r?\n/).filter(Boolean);
|
|
158
|
+
for (const [index, originalIndex] of indicesToConvert.entries()) results[originalIndex] = convertedPaths[index] ?? pathArray[originalIndex];
|
|
159
|
+
} catch {}
|
|
160
|
+
return isBatch ? results : results[0];
|
|
161
|
+
};
|
|
162
|
+
//#endregion
|
|
163
|
+
//#region node_modules/.pnpm/powershell-utils@0.2.1/node_modules/powershell-utils/index.js
|
|
164
|
+
const execFile$1 = promisify(childProcess.execFile);
|
|
165
|
+
const powerShellPath = () => `${process$1.env.SYSTEMROOT || process$1.env.windir || String.raw`C:\Windows`}\\System32\\WindowsPowerShell\\v1.0\\powershell.exe`;
|
|
166
|
+
const argumentsPrefix = [
|
|
167
|
+
"-NoProfile",
|
|
168
|
+
"-NonInteractive",
|
|
169
|
+
"-ExecutionPolicy",
|
|
170
|
+
"Bypass",
|
|
171
|
+
"-EncodedCommand"
|
|
172
|
+
];
|
|
173
|
+
const encodeCommand = (command) => Buffer.from(command, "utf16le").toString("base64");
|
|
174
|
+
const escapeArgument = (value) => `'${String(value).replaceAll(/['‘’‚‛]/g, (match) => match + match)}'`;
|
|
175
|
+
const createArguments = (command) => [...argumentsPrefix, encodeCommand(command)];
|
|
176
|
+
const createExecFileOptions = (options) => ({
|
|
177
|
+
encoding: "utf8",
|
|
178
|
+
...options,
|
|
179
|
+
shell: false
|
|
180
|
+
});
|
|
181
|
+
const executePowerShell = async (command, options = {}) => {
|
|
182
|
+
const { powerShellPath: psPath, ...execFileOptions } = options;
|
|
183
|
+
return execFile$1(psPath ?? powerShellPath(), createArguments(command), createExecFileOptions(execFileOptions));
|
|
184
|
+
};
|
|
185
|
+
executePowerShell.argumentsPrefix = argumentsPrefix;
|
|
186
|
+
executePowerShell.encodeCommand = encodeCommand;
|
|
187
|
+
executePowerShell.escapeArgument = escapeArgument;
|
|
188
|
+
executePowerShell.createArguments = createArguments;
|
|
189
|
+
//#endregion
|
|
190
|
+
//#region node_modules/.pnpm/define-lazy-prop@3.0.0/node_modules/define-lazy-prop/index.js
|
|
191
|
+
function defineLazyProperty(object, propertyName, valueGetter) {
|
|
192
|
+
const define = (value) => Object.defineProperty(object, propertyName, {
|
|
193
|
+
value,
|
|
194
|
+
enumerable: true,
|
|
195
|
+
writable: true
|
|
196
|
+
});
|
|
197
|
+
Object.defineProperty(object, propertyName, {
|
|
198
|
+
configurable: true,
|
|
199
|
+
enumerable: true,
|
|
200
|
+
get() {
|
|
201
|
+
const result = valueGetter();
|
|
202
|
+
define(result);
|
|
203
|
+
return result;
|
|
204
|
+
},
|
|
205
|
+
set(value) {
|
|
206
|
+
define(value);
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
return object;
|
|
210
|
+
}
|
|
211
|
+
//#endregion
|
|
212
|
+
//#region node_modules/.pnpm/default-browser-id@5.0.1/node_modules/default-browser-id/index.js
|
|
213
|
+
const execFileAsync$3 = promisify(execFile);
|
|
214
|
+
async function defaultBrowserId() {
|
|
215
|
+
if (process$1.platform !== "darwin") throw new Error("macOS only");
|
|
216
|
+
const { stdout } = await execFileAsync$3("defaults", [
|
|
217
|
+
"read",
|
|
218
|
+
"com.apple.LaunchServices/com.apple.launchservices.secure",
|
|
219
|
+
"LSHandlers"
|
|
220
|
+
]);
|
|
221
|
+
const browserId = /LSHandlerRoleAll = "(?!-)(?<id>[^"]+?)";\s+?LSHandlerURLScheme = (?:http|https);/.exec(stdout)?.groups.id ?? "com.apple.Safari";
|
|
222
|
+
if (browserId === "com.apple.safari") return "com.apple.Safari";
|
|
223
|
+
return browserId;
|
|
224
|
+
}
|
|
225
|
+
//#endregion
|
|
226
|
+
//#region node_modules/.pnpm/run-applescript@7.1.0/node_modules/run-applescript/index.js
|
|
227
|
+
const execFileAsync$2 = promisify(execFile);
|
|
228
|
+
async function runAppleScript(script, { humanReadableOutput = true, signal } = {}) {
|
|
229
|
+
if (process$1.platform !== "darwin") throw new Error("macOS only");
|
|
230
|
+
const outputArguments = humanReadableOutput ? [] : ["-ss"];
|
|
231
|
+
const execOptions = {};
|
|
232
|
+
if (signal) execOptions.signal = signal;
|
|
233
|
+
const { stdout } = await execFileAsync$2("osascript", [
|
|
234
|
+
"-e",
|
|
235
|
+
script,
|
|
236
|
+
outputArguments
|
|
237
|
+
], execOptions);
|
|
238
|
+
return stdout.trim();
|
|
239
|
+
}
|
|
240
|
+
//#endregion
|
|
241
|
+
//#region node_modules/.pnpm/bundle-name@4.1.1/node_modules/bundle-name/index.js
|
|
242
|
+
const bundleIdentifierPattern = /^[^\n\r"\\]+$/u;
|
|
243
|
+
async function bundleName(bundleId) {
|
|
244
|
+
if (typeof bundleId !== "string") throw new TypeError("Expected a string bundle identifier");
|
|
245
|
+
if (!bundleIdentifierPattern.test(bundleId)) throw new TypeError(`Invalid bundle identifier: ${JSON.stringify(bundleId)}`);
|
|
246
|
+
return runAppleScript(`tell application "Finder" to set app_path to application file id "${bundleId}" as string\ntell application "System Events" to get value of property list item "CFBundleName" of property list file (app_path & ":Contents:Info.plist")`);
|
|
247
|
+
}
|
|
248
|
+
//#endregion
|
|
249
|
+
//#region node_modules/.pnpm/default-browser@5.5.1/node_modules/default-browser/windows.js
|
|
250
|
+
const execFileAsync$1 = promisify(execFile);
|
|
251
|
+
const windowsBrowserProgIds = {
|
|
252
|
+
MSEdgeHTM: {
|
|
253
|
+
name: "Edge",
|
|
254
|
+
id: "com.microsoft.edge"
|
|
255
|
+
},
|
|
256
|
+
MSEdgeBHTML: {
|
|
257
|
+
name: "Edge Beta",
|
|
258
|
+
id: "com.microsoft.edge.beta"
|
|
259
|
+
},
|
|
260
|
+
MSEdgeDHTML: {
|
|
261
|
+
name: "Edge Dev",
|
|
262
|
+
id: "com.microsoft.edge.dev"
|
|
263
|
+
},
|
|
264
|
+
AppXq0fevzme2pys62n3e0fbqa7peapykr8v: {
|
|
265
|
+
name: "Edge",
|
|
266
|
+
id: "com.microsoft.edge.old"
|
|
267
|
+
},
|
|
268
|
+
ChromeHTML: {
|
|
269
|
+
name: "Chrome",
|
|
270
|
+
id: "com.google.chrome"
|
|
271
|
+
},
|
|
272
|
+
ChromeBHTML: {
|
|
273
|
+
name: "Chrome Beta",
|
|
274
|
+
id: "com.google.chrome.beta"
|
|
275
|
+
},
|
|
276
|
+
ChromeDHTML: {
|
|
277
|
+
name: "Chrome Dev",
|
|
278
|
+
id: "com.google.chrome.dev"
|
|
279
|
+
},
|
|
280
|
+
ChromiumHTM: {
|
|
281
|
+
name: "Chromium",
|
|
282
|
+
id: "org.chromium.Chromium"
|
|
283
|
+
},
|
|
284
|
+
BraveHTML: {
|
|
285
|
+
name: "Brave",
|
|
286
|
+
id: "com.brave.Browser"
|
|
287
|
+
},
|
|
288
|
+
BraveBHTML: {
|
|
289
|
+
name: "Brave Beta",
|
|
290
|
+
id: "com.brave.Browser.beta"
|
|
291
|
+
},
|
|
292
|
+
BraveDHTML: {
|
|
293
|
+
name: "Brave Dev",
|
|
294
|
+
id: "com.brave.Browser.dev"
|
|
295
|
+
},
|
|
296
|
+
BraveSSHTM: {
|
|
297
|
+
name: "Brave Nightly",
|
|
298
|
+
id: "com.brave.Browser.nightly"
|
|
299
|
+
},
|
|
300
|
+
FirefoxURL: {
|
|
301
|
+
name: "Firefox",
|
|
302
|
+
id: "org.mozilla.firefox"
|
|
303
|
+
},
|
|
304
|
+
OperaStable: {
|
|
305
|
+
name: "Opera",
|
|
306
|
+
id: "com.operasoftware.Opera"
|
|
307
|
+
},
|
|
308
|
+
VivaldiHTM: {
|
|
309
|
+
name: "Vivaldi",
|
|
310
|
+
id: "com.vivaldi.Vivaldi"
|
|
311
|
+
},
|
|
312
|
+
"IE.HTTP": {
|
|
313
|
+
name: "Internet Explorer",
|
|
314
|
+
id: "com.microsoft.ie"
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
const _windowsBrowserProgIdMap = new Map(Object.entries(windowsBrowserProgIds));
|
|
318
|
+
var UnknownBrowserError = class extends Error {};
|
|
319
|
+
async function defaultBrowser$1(_execFileAsync = execFileAsync$1) {
|
|
320
|
+
const { stdout } = await _execFileAsync(`${process$1.env.SYSTEMROOT ?? process$1.env.windir ?? "C:\\Windows"}\\System32\\reg.exe`, [
|
|
321
|
+
"QUERY",
|
|
322
|
+
" HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\Shell\\Associations\\UrlAssociations\\http\\UserChoice",
|
|
323
|
+
"/v",
|
|
324
|
+
"ProgId"
|
|
325
|
+
]);
|
|
326
|
+
const match = /ProgId\s*REG_SZ\s*(?<id>\S+)/.exec(stdout);
|
|
327
|
+
if (!match) throw new UnknownBrowserError(`Cannot find Windows browser in stdout: ${JSON.stringify(stdout)}`);
|
|
328
|
+
const { id } = match.groups;
|
|
329
|
+
const dotIndex = id.lastIndexOf(".");
|
|
330
|
+
const hyphenIndex = id.lastIndexOf("-");
|
|
331
|
+
const baseIdByDot = dotIndex === -1 ? void 0 : id.slice(0, dotIndex);
|
|
332
|
+
const baseIdByHyphen = hyphenIndex === -1 ? void 0 : id.slice(0, hyphenIndex);
|
|
333
|
+
return windowsBrowserProgIds[id] ?? windowsBrowserProgIds[baseIdByDot] ?? windowsBrowserProgIds[baseIdByHyphen] ?? {
|
|
334
|
+
name: id,
|
|
335
|
+
id
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
//#endregion
|
|
339
|
+
//#region node_modules/.pnpm/default-browser@5.5.1/node_modules/default-browser/index.js
|
|
340
|
+
const execFileAsync = promisify(execFile);
|
|
341
|
+
const titleize = (string) => string.toLowerCase().replaceAll(/(?:^|\s|-)\S/g, (x) => x.toUpperCase());
|
|
342
|
+
async function defaultBrowser() {
|
|
343
|
+
if (process$1.platform === "darwin") {
|
|
344
|
+
const id = await defaultBrowserId();
|
|
345
|
+
return {
|
|
346
|
+
name: await bundleName(id),
|
|
347
|
+
id
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
if (process$1.platform === "linux") {
|
|
351
|
+
const { stdout } = await execFileAsync("xdg-mime", [
|
|
352
|
+
"query",
|
|
353
|
+
"default",
|
|
354
|
+
"x-scheme-handler/http"
|
|
355
|
+
]);
|
|
356
|
+
const id = stdout.trim();
|
|
357
|
+
return {
|
|
358
|
+
name: titleize(id.replace(/.desktop$/, "").replace("-", " ")),
|
|
359
|
+
id
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
if (process$1.platform === "win32") return defaultBrowser$1();
|
|
363
|
+
throw new Error("Only macOS, Linux, and Windows are supported");
|
|
364
|
+
}
|
|
365
|
+
//#endregion
|
|
366
|
+
//#region node_modules/.pnpm/is-in-ssh@1.0.0/node_modules/is-in-ssh/index.js
|
|
367
|
+
const isInSsh = Boolean(process$1.env.SSH_CONNECTION || process$1.env.SSH_CLIENT || process$1.env.SSH_TTY);
|
|
368
|
+
//#endregion
|
|
369
|
+
//#region node_modules/.pnpm/open@11.0.4/node_modules/open/index.js
|
|
370
|
+
const fallbackAttemptSymbol = Symbol("fallbackAttempt");
|
|
371
|
+
const __dirname = import.meta.url ? path.dirname(fileURLToPath(import.meta.url)) : "";
|
|
372
|
+
const localXdgOpenPath = path.join(__dirname, "xdg-open");
|
|
373
|
+
const { platform, arch } = process$1;
|
|
374
|
+
const tryEachApp = async (apps, opener) => {
|
|
375
|
+
if (apps.length === 0) return;
|
|
376
|
+
const errors = [];
|
|
377
|
+
for (const app of apps) try {
|
|
378
|
+
return await opener(app);
|
|
379
|
+
} catch (error) {
|
|
380
|
+
errors.push(error);
|
|
381
|
+
}
|
|
382
|
+
throw new AggregateError(errors, "Failed to open in all supported apps");
|
|
383
|
+
};
|
|
384
|
+
const baseOpen = async (options) => {
|
|
385
|
+
options = {
|
|
386
|
+
wait: false,
|
|
387
|
+
background: false,
|
|
388
|
+
newInstance: false,
|
|
389
|
+
allowNonzeroExitCode: false,
|
|
390
|
+
...options
|
|
391
|
+
};
|
|
392
|
+
const isFallbackAttempt = options[fallbackAttemptSymbol] === true;
|
|
393
|
+
delete options[fallbackAttemptSymbol];
|
|
394
|
+
if (Array.isArray(options.app)) return tryEachApp(options.app, (singleApp) => baseOpen({
|
|
395
|
+
...options,
|
|
396
|
+
app: singleApp,
|
|
397
|
+
[fallbackAttemptSymbol]: true
|
|
398
|
+
}));
|
|
399
|
+
let { name: app, arguments: appArguments = [] } = options.app ?? {};
|
|
400
|
+
appArguments = [...appArguments];
|
|
401
|
+
if (Array.isArray(app)) return tryEachApp(app, (appName) => baseOpen({
|
|
402
|
+
...options,
|
|
403
|
+
app: {
|
|
404
|
+
name: appName,
|
|
405
|
+
arguments: appArguments
|
|
406
|
+
},
|
|
407
|
+
[fallbackAttemptSymbol]: true
|
|
408
|
+
}));
|
|
409
|
+
if (app === "browser" || app === "browserPrivate") {
|
|
410
|
+
const ids = {
|
|
411
|
+
"com.google.chrome": "chrome",
|
|
412
|
+
"google-chrome.desktop": "chrome",
|
|
413
|
+
"com.brave.browser": "brave",
|
|
414
|
+
"org.mozilla.firefox": "firefox",
|
|
415
|
+
"firefox.desktop": "firefox",
|
|
416
|
+
"com.microsoft.msedge": "edge",
|
|
417
|
+
"com.microsoft.edge": "edge",
|
|
418
|
+
"com.microsoft.edgemac": "edge",
|
|
419
|
+
"microsoft-edge.desktop": "edge",
|
|
420
|
+
"com.apple.safari": "safari"
|
|
421
|
+
};
|
|
422
|
+
const flags = {
|
|
423
|
+
chrome: "--incognito",
|
|
424
|
+
brave: "--incognito",
|
|
425
|
+
firefox: "--private-window",
|
|
426
|
+
edge: "--inPrivate"
|
|
427
|
+
};
|
|
428
|
+
let browser;
|
|
429
|
+
if (is_wsl_default) {
|
|
430
|
+
const progId = await wslDefaultBrowser();
|
|
431
|
+
browser = _windowsBrowserProgIdMap.get(progId) ?? {};
|
|
432
|
+
} else browser = await defaultBrowser();
|
|
433
|
+
const browserId = browser.id?.toLowerCase();
|
|
434
|
+
if (Object.hasOwn(ids, browserId)) {
|
|
435
|
+
const browserName = ids[browserId];
|
|
436
|
+
if (app === "browserPrivate") {
|
|
437
|
+
if (browserName === "safari") throw new Error("Safari doesn't support opening in private mode via command line");
|
|
438
|
+
appArguments.push(flags[browserName]);
|
|
439
|
+
}
|
|
440
|
+
return baseOpen({
|
|
441
|
+
...options,
|
|
442
|
+
app: {
|
|
443
|
+
name: apps[browserName],
|
|
444
|
+
arguments: appArguments
|
|
445
|
+
}
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
throw new Error(`${browser.name} is not supported as a default browser`);
|
|
449
|
+
}
|
|
450
|
+
let command;
|
|
451
|
+
const cliArguments = [];
|
|
452
|
+
const childProcessOptions = {};
|
|
453
|
+
let shouldUseWindowsInWsl = false;
|
|
454
|
+
if (is_wsl_default && !isInsideContainer() && !isInSsh && !app) shouldUseWindowsInWsl = await canAccessPowerShell();
|
|
455
|
+
if (platform === "darwin") {
|
|
456
|
+
command = "open";
|
|
457
|
+
if (options.wait) cliArguments.push("--wait-apps");
|
|
458
|
+
if (options.background) cliArguments.push("--background");
|
|
459
|
+
if (options.newInstance) cliArguments.push("--new");
|
|
460
|
+
if (app) cliArguments.push("-a", app);
|
|
461
|
+
} else if (platform === "win32" || shouldUseWindowsInWsl) {
|
|
462
|
+
command = await powerShellPath$1();
|
|
463
|
+
cliArguments.push(...executePowerShell.argumentsPrefix);
|
|
464
|
+
if (!is_wsl_default) childProcessOptions.windowsVerbatimArguments = true;
|
|
465
|
+
if (is_wsl_default && options.target) options.target = await convertWslPathToWindows(options.target);
|
|
466
|
+
const encodedArguments = ["$ProgressPreference = 'SilentlyContinue';", "Start"];
|
|
467
|
+
if (options.wait) encodedArguments.push("-Wait");
|
|
468
|
+
if (app) {
|
|
469
|
+
encodedArguments.push(executePowerShell.escapeArgument(app));
|
|
470
|
+
if (options.target) appArguments.push(options.target);
|
|
471
|
+
} else if (options.target) encodedArguments.push(executePowerShell.escapeArgument(options.target));
|
|
472
|
+
if (appArguments.length > 0) {
|
|
473
|
+
appArguments = appArguments.map((argument) => executePowerShell.escapeArgument(argument));
|
|
474
|
+
encodedArguments.push("-ArgumentList", appArguments.join(","));
|
|
475
|
+
}
|
|
476
|
+
options.target = executePowerShell.encodeCommand(encodedArguments.join(" "));
|
|
477
|
+
if (!options.wait) childProcessOptions.stdio = "ignore";
|
|
478
|
+
if (is_wsl_default) childProcessOptions.cwd = path.dirname(command);
|
|
479
|
+
} else {
|
|
480
|
+
if (app) command = app;
|
|
481
|
+
else {
|
|
482
|
+
const isBundled = !__dirname || __dirname === "/";
|
|
483
|
+
let exeLocalXdgOpen = false;
|
|
484
|
+
try {
|
|
485
|
+
await fs.access(localXdgOpenPath, constants.X_OK);
|
|
486
|
+
exeLocalXdgOpen = true;
|
|
487
|
+
} catch {}
|
|
488
|
+
command = process$1.versions.electron ?? (platform === "android" || isBundled || !exeLocalXdgOpen) ? "xdg-open" : localXdgOpenPath;
|
|
489
|
+
}
|
|
490
|
+
if (appArguments.length > 0) cliArguments.push(...appArguments);
|
|
491
|
+
if (!options.wait) {
|
|
492
|
+
childProcessOptions.stdio = "ignore";
|
|
493
|
+
childProcessOptions.detached = true;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
if (platform === "darwin" && appArguments.length > 0) cliArguments.push("--args", ...appArguments);
|
|
497
|
+
if (options.target) cliArguments.push(options.target);
|
|
498
|
+
const subprocess = childProcess.spawn(command, cliArguments, childProcessOptions);
|
|
499
|
+
if (options.wait) return new Promise((resolve, reject) => {
|
|
500
|
+
subprocess.once("error", reject);
|
|
501
|
+
subprocess.once("close", (exitCode) => {
|
|
502
|
+
if (!options.allowNonzeroExitCode && exitCode !== 0) {
|
|
503
|
+
reject(/* @__PURE__ */ new Error(`Exited with code ${exitCode}`));
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
resolve(subprocess);
|
|
507
|
+
});
|
|
508
|
+
});
|
|
509
|
+
if (isFallbackAttempt || platform === "win32" || shouldUseWindowsInWsl) return new Promise((resolve, reject) => {
|
|
510
|
+
subprocess.once("error", reject);
|
|
511
|
+
subprocess.once("spawn", () => {
|
|
512
|
+
subprocess.once("close", (exitCode) => {
|
|
513
|
+
subprocess.off("error", reject);
|
|
514
|
+
if (isFallbackAttempt && exitCode !== 0) {
|
|
515
|
+
reject(/* @__PURE__ */ new Error(`Exited with code ${exitCode}`));
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
subprocess.unref();
|
|
519
|
+
resolve(subprocess);
|
|
520
|
+
});
|
|
521
|
+
});
|
|
522
|
+
});
|
|
523
|
+
subprocess.unref();
|
|
524
|
+
return new Promise((resolve, reject) => {
|
|
525
|
+
subprocess.once("error", reject);
|
|
526
|
+
subprocess.once("spawn", () => {
|
|
527
|
+
subprocess.off("error", reject);
|
|
528
|
+
resolve(subprocess);
|
|
529
|
+
});
|
|
530
|
+
});
|
|
531
|
+
};
|
|
532
|
+
const open = (target, options) => {
|
|
533
|
+
if (typeof target !== "string") throw new TypeError("Expected a `target`");
|
|
534
|
+
return baseOpen({
|
|
535
|
+
...options,
|
|
536
|
+
target
|
|
537
|
+
});
|
|
538
|
+
};
|
|
539
|
+
function detectArchBinary(binary) {
|
|
540
|
+
if (typeof binary === "string" || Array.isArray(binary)) return binary;
|
|
541
|
+
const { [arch]: archBinary } = binary;
|
|
542
|
+
if (!archBinary) throw new Error(`${arch} is not supported`);
|
|
543
|
+
return archBinary;
|
|
544
|
+
}
|
|
545
|
+
function detectPlatformBinary({ [platform]: platformBinary }, { wsl } = {}) {
|
|
546
|
+
if (wsl && is_wsl_default) return detectArchBinary(wsl);
|
|
547
|
+
if (!platformBinary) throw new Error(`${platform} is not supported`);
|
|
548
|
+
return detectArchBinary(platformBinary);
|
|
549
|
+
}
|
|
550
|
+
const apps = {
|
|
551
|
+
browser: "browser",
|
|
552
|
+
browserPrivate: "browserPrivate"
|
|
553
|
+
};
|
|
554
|
+
defineLazyProperty(apps, "chrome", () => detectPlatformBinary({
|
|
555
|
+
darwin: "google chrome",
|
|
556
|
+
win32: "chrome",
|
|
557
|
+
linux: [
|
|
558
|
+
"google-chrome",
|
|
559
|
+
"google-chrome-stable",
|
|
560
|
+
"chromium",
|
|
561
|
+
"chromium-browser"
|
|
562
|
+
]
|
|
563
|
+
}, { wsl: {
|
|
564
|
+
ia32: "/mnt/c/Program Files (x86)/Google/Chrome/Application/chrome.exe",
|
|
565
|
+
x64: ["/mnt/c/Program Files/Google/Chrome/Application/chrome.exe", "/mnt/c/Program Files (x86)/Google/Chrome/Application/chrome.exe"]
|
|
566
|
+
} }));
|
|
567
|
+
defineLazyProperty(apps, "brave", () => detectPlatformBinary({
|
|
568
|
+
darwin: "brave browser",
|
|
569
|
+
win32: "brave",
|
|
570
|
+
linux: ["brave-browser", "brave"]
|
|
571
|
+
}, { wsl: {
|
|
572
|
+
ia32: "/mnt/c/Program Files (x86)/BraveSoftware/Brave-Browser/Application/brave.exe",
|
|
573
|
+
x64: ["/mnt/c/Program Files/BraveSoftware/Brave-Browser/Application/brave.exe", "/mnt/c/Program Files (x86)/BraveSoftware/Brave-Browser/Application/brave.exe"]
|
|
574
|
+
} }));
|
|
575
|
+
defineLazyProperty(apps, "firefox", () => detectPlatformBinary({
|
|
576
|
+
darwin: "firefox",
|
|
577
|
+
win32: String.raw`C:\Program Files\Mozilla Firefox\firefox.exe`,
|
|
578
|
+
linux: "firefox"
|
|
579
|
+
}, { wsl: "/mnt/c/Program Files/Mozilla Firefox/firefox.exe" }));
|
|
580
|
+
defineLazyProperty(apps, "edge", () => detectPlatformBinary({
|
|
581
|
+
darwin: "microsoft edge",
|
|
582
|
+
win32: "msedge",
|
|
583
|
+
linux: ["microsoft-edge", "microsoft-edge-dev"]
|
|
584
|
+
}, { wsl: "/mnt/c/Program Files (x86)/Microsoft/Edge/Application/msedge.exe" }));
|
|
585
|
+
defineLazyProperty(apps, "safari", () => detectPlatformBinary({ darwin: "Safari" }));
|
|
586
|
+
//#endregion
|
|
587
|
+
//#region src/auth/browser-oauth.ts
|
|
588
|
+
const AUTH_TIMEOUT_MS = 3e5;
|
|
589
|
+
/** Best-effort WSL detection: WSL kernels carry "microsoft" in /proc/version. */
|
|
590
|
+
const detectWsl = () => {
|
|
591
|
+
if (process.platform !== "linux") return false;
|
|
592
|
+
try {
|
|
593
|
+
return readFileSync("/proc/version", "utf8").toLowerCase().includes("microsoft");
|
|
594
|
+
} catch {
|
|
595
|
+
return false;
|
|
596
|
+
}
|
|
597
|
+
};
|
|
598
|
+
/**
|
|
599
|
+
* Build an actionable error for a callback port that's already taken. The raw
|
|
600
|
+
* Node `EADDRINUSE` is opaque to both the user and the LLM. The port is also
|
|
601
|
+
* the mutex between instances, so retrying once the other sign-in ends comes
|
|
602
|
+
* first and the free-port escape hatch second
|
|
603
|
+
* (`docs/decisions/token-file-keying.md`). The `(EADDRINUSE)` marker and `code`
|
|
604
|
+
* are kept for diagnostics/tests.
|
|
605
|
+
*/
|
|
606
|
+
const callbackPortInUseError = (port, cause) => Object.assign(/* @__PURE__ */ new Error(`Cannot start the Zendesk OAuth sign-in: local callback port ${port} is already in use. Another instance of this server is most likely signing in right now: finish that browser window, then retry. If an unrelated program holds the port, set OAUTH_CALLBACK_PORT (or --callback-port) to a free port, then register http://localhost:<port>/callback as a redirect URL in your Zendesk OAuth client. (EADDRINUSE)`), {
|
|
607
|
+
code: "EADDRINUSE",
|
|
608
|
+
cause
|
|
609
|
+
});
|
|
610
|
+
const errorPage = (title, detail) => `<html><body><h1>${escapeHtml(title)}</h1>${detail === void 0 ? "" : `<p>${escapeHtml(detail)}</p>`}</body></html>`;
|
|
611
|
+
const SUCCESS_PAGE = "<html><body><h1>Authentication successful!</h1><p>You can close this tab and return to your AI assistant.</p><p>This tab will auto-close in <span id=\"t\">10</span>s.</p><script>let n=10;const el=document.getElementById(\"t\");const i=setInterval(()=>{n--;el.textContent=n;if(n<=0){clearInterval(i);window.close();}},1000);<\/script></body></html>";
|
|
612
|
+
/**
|
|
613
|
+
* Begin the OAuth 2.1 PKCE flow: start the local callback server, attempt to
|
|
614
|
+
* open the browser, and return as soon as the server is listening with the
|
|
615
|
+
* authorize URL plus a promise that settles when the callback arrives.
|
|
616
|
+
*
|
|
617
|
+
* Splitting "start" from "await the token" lets callers stay non-blocking: they
|
|
618
|
+
* can hand the URL back to the user immediately instead of holding a request
|
|
619
|
+
* open for up to the 5-minute timeout.
|
|
620
|
+
*/
|
|
621
|
+
const startBrowserAuth = (config, logger = silentLogger) => {
|
|
622
|
+
const { subdomain, oauthClientId, readOnly } = config;
|
|
623
|
+
const { authorizeUrl: authorizeBase, tokenUrl } = getOAuthUrls(subdomain);
|
|
624
|
+
const codeVerifier = generateCodeVerifier();
|
|
625
|
+
const codeChallenge = generateCodeChallenge(codeVerifier);
|
|
626
|
+
return new Promise((resolveStarted, rejectStarted) => {
|
|
627
|
+
let resolveToken;
|
|
628
|
+
let rejectToken;
|
|
629
|
+
const tokenPromise = new Promise((resolve, reject) => {
|
|
630
|
+
resolveToken = resolve;
|
|
631
|
+
rejectToken = reject;
|
|
632
|
+
});
|
|
633
|
+
let authTimeout;
|
|
634
|
+
let callbackServer;
|
|
635
|
+
const exchangeCodeForToken = async (code) => {
|
|
636
|
+
const callbackPort = callbackServer.address().port;
|
|
637
|
+
const tokenBody = new URLSearchParams({
|
|
638
|
+
grant_type: "authorization_code",
|
|
639
|
+
code,
|
|
640
|
+
client_id: oauthClientId,
|
|
641
|
+
redirect_uri: `http://localhost:${callbackPort}/callback`,
|
|
642
|
+
code_verifier: codeVerifier
|
|
643
|
+
});
|
|
644
|
+
const tokenResponse = await fetch(tokenUrl, {
|
|
645
|
+
method: "POST",
|
|
646
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
647
|
+
body: tokenBody.toString()
|
|
648
|
+
});
|
|
649
|
+
logger.debug("oauth_token_exchange", { status: tokenResponse.status });
|
|
650
|
+
if (!tokenResponse.ok) {
|
|
651
|
+
const errorBody = await tokenResponse.text();
|
|
652
|
+
throw new Error(`Token exchange failed (${tokenResponse.status}): ${errorBody}`);
|
|
653
|
+
}
|
|
654
|
+
return await tokenResponse.json();
|
|
655
|
+
};
|
|
656
|
+
const finishRequest = (res, { status, html, outcome }) => {
|
|
657
|
+
res.writeHead(status, { "Content-Type": "text/html" });
|
|
658
|
+
res.end(html);
|
|
659
|
+
clearTimeout(authTimeout);
|
|
660
|
+
callbackServer.close();
|
|
661
|
+
if (outcome.ok) resolveToken(outcome.token);
|
|
662
|
+
else rejectToken(outcome.error);
|
|
663
|
+
};
|
|
664
|
+
const resolveCallback = async (url) => {
|
|
665
|
+
const error = url.searchParams.get("error");
|
|
666
|
+
const code = url.searchParams.get("code");
|
|
667
|
+
logger.debug("oauth_callback_received", {
|
|
668
|
+
hasCode: Boolean(code),
|
|
669
|
+
hasError: Boolean(error)
|
|
670
|
+
});
|
|
671
|
+
if (error) {
|
|
672
|
+
const desc = url.searchParams.get("error_description") ?? error;
|
|
673
|
+
return {
|
|
674
|
+
status: 400,
|
|
675
|
+
html: errorPage("Authentication failed", desc),
|
|
676
|
+
outcome: {
|
|
677
|
+
ok: false,
|
|
678
|
+
error: /* @__PURE__ */ new Error(`OAuth error: ${desc}`)
|
|
679
|
+
}
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
if (!code) return {
|
|
683
|
+
status: 400,
|
|
684
|
+
html: errorPage("Missing authorization code"),
|
|
685
|
+
outcome: {
|
|
686
|
+
ok: false,
|
|
687
|
+
error: /* @__PURE__ */ new Error("Missing authorization code in callback")
|
|
688
|
+
}
|
|
689
|
+
};
|
|
690
|
+
try {
|
|
691
|
+
const token = await exchangeCodeForToken(code);
|
|
692
|
+
logger.info("oauth_authenticated");
|
|
693
|
+
return {
|
|
694
|
+
status: 200,
|
|
695
|
+
html: SUCCESS_PAGE,
|
|
696
|
+
outcome: {
|
|
697
|
+
ok: true,
|
|
698
|
+
token
|
|
699
|
+
}
|
|
700
|
+
};
|
|
701
|
+
} catch (err) {
|
|
702
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
703
|
+
return {
|
|
704
|
+
status: 500,
|
|
705
|
+
html: errorPage("Token exchange failed", detail),
|
|
706
|
+
outcome: {
|
|
707
|
+
ok: false,
|
|
708
|
+
error: err
|
|
709
|
+
}
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
};
|
|
713
|
+
callbackServer = createServer(async (req, res) => {
|
|
714
|
+
const url = new URL(req.url ?? "/", `http://localhost`);
|
|
715
|
+
if (url.pathname !== "/callback") {
|
|
716
|
+
res.writeHead(404);
|
|
717
|
+
res.end("Not found");
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
finishRequest(res, await resolveCallback(url));
|
|
721
|
+
});
|
|
722
|
+
const requestedPort = config.callbackPort ?? 27439;
|
|
723
|
+
const onStartError = (err) => {
|
|
724
|
+
const code = err.code;
|
|
725
|
+
logger.error("oauth_callback_listen_failed", {
|
|
726
|
+
port: requestedPort,
|
|
727
|
+
errorCode: code
|
|
728
|
+
});
|
|
729
|
+
rejectStarted(code === "EADDRINUSE" ? callbackPortInUseError(requestedPort, err) : err);
|
|
730
|
+
};
|
|
731
|
+
callbackServer.once("error", onStartError);
|
|
732
|
+
callbackServer.listen(requestedPort, () => {
|
|
733
|
+
callbackServer.off("error", onStartError);
|
|
734
|
+
callbackServer.once("error", (err) => {
|
|
735
|
+
clearTimeout(authTimeout);
|
|
736
|
+
callbackServer.close();
|
|
737
|
+
rejectToken(err);
|
|
738
|
+
});
|
|
739
|
+
const port = callbackServer.address().port;
|
|
740
|
+
const redirectUri = `http://localhost:${port}/callback`;
|
|
741
|
+
const params = new URLSearchParams({
|
|
742
|
+
response_type: "code",
|
|
743
|
+
client_id: oauthClientId,
|
|
744
|
+
redirect_uri: redirectUri,
|
|
745
|
+
scope: requestedScope(readOnly),
|
|
746
|
+
code_challenge: codeChallenge,
|
|
747
|
+
code_challenge_method: "S256"
|
|
748
|
+
});
|
|
749
|
+
const authUrl = `${authorizeBase}?${params.toString()}`;
|
|
750
|
+
logger.debug("oauth_callback_listening", {
|
|
751
|
+
port,
|
|
752
|
+
redirectUri
|
|
753
|
+
});
|
|
754
|
+
logger.info("oauth_browser_opening");
|
|
755
|
+
logger.debug("oauth_authorize_url", { url: authUrl });
|
|
756
|
+
console.error(`Opening browser for Zendesk authentication...`);
|
|
757
|
+
console.error(`If the browser doesn't open, visit: ${authUrl}`);
|
|
758
|
+
open(authUrl).then(() => {
|
|
759
|
+
logger.debug("oauth_browser_opened");
|
|
760
|
+
}).catch((err) => {
|
|
761
|
+
logger.error("oauth_browser_open_failed", {
|
|
762
|
+
error: err instanceof Error ? err.message : String(err),
|
|
763
|
+
errorCode: err?.code,
|
|
764
|
+
platform: process.platform,
|
|
765
|
+
release: release(),
|
|
766
|
+
isWsl: detectWsl(),
|
|
767
|
+
hasSystemRoot: Boolean(process.env["SYSTEMROOT"]),
|
|
768
|
+
hasWindir: Boolean(process.env["WINDIR"]),
|
|
769
|
+
hasComSpec: Boolean(process.env["ComSpec"]),
|
|
770
|
+
hasPath: Boolean(process.env["PATH"]),
|
|
771
|
+
hasDisplay: Boolean(process.env["DISPLAY"])
|
|
772
|
+
});
|
|
773
|
+
});
|
|
774
|
+
authTimeout = setTimeout(() => {
|
|
775
|
+
logger.error("oauth_timeout", { timeoutMs: AUTH_TIMEOUT_MS });
|
|
776
|
+
callbackServer.close();
|
|
777
|
+
rejectToken(/* @__PURE__ */ new Error("OAuth authentication timed out (5 min). Please try again."));
|
|
778
|
+
}, AUTH_TIMEOUT_MS);
|
|
779
|
+
authTimeout.unref();
|
|
780
|
+
resolveStarted({
|
|
781
|
+
authorizeUrl: authUrl,
|
|
782
|
+
tokenPromise
|
|
783
|
+
});
|
|
784
|
+
});
|
|
785
|
+
});
|
|
786
|
+
};
|
|
787
|
+
/**
|
|
788
|
+
* Exchange a refresh token for a fresh access token (and a rotated refresh token)
|
|
789
|
+
* without any browser interaction. Public PKCE clients send no `client_secret`.
|
|
790
|
+
* Zendesk refresh tokens are single-use: the caller MUST persist the new
|
|
791
|
+
* `refresh_token` from the response. Throws on a non-2xx (expired/invalid
|
|
792
|
+
* refresh token) so the caller can fall back to the full browser flow.
|
|
793
|
+
*/
|
|
794
|
+
const refreshAccessToken = async (config, logger = silentLogger) => {
|
|
795
|
+
const { tokenUrl } = getOAuthUrls(config.subdomain);
|
|
796
|
+
const body = new URLSearchParams({
|
|
797
|
+
grant_type: "refresh_token",
|
|
798
|
+
refresh_token: config.refreshToken,
|
|
799
|
+
client_id: config.oauthClientId
|
|
800
|
+
});
|
|
801
|
+
const response = await fetch(tokenUrl, {
|
|
802
|
+
method: "POST",
|
|
803
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
804
|
+
body: body.toString()
|
|
805
|
+
});
|
|
806
|
+
logger.debug("oauth_token_refresh", { status: response.status });
|
|
807
|
+
if (!response.ok) {
|
|
808
|
+
const errorBody = await response.text();
|
|
809
|
+
throw new Error(`Token refresh failed (${response.status}): ${errorBody}`);
|
|
810
|
+
}
|
|
811
|
+
const tokenData = await response.json();
|
|
812
|
+
logger.info("oauth_token_refreshed");
|
|
813
|
+
return tokenData;
|
|
814
|
+
};
|
|
815
|
+
//#endregion
|
|
816
|
+
//#region src/auth/token-store.ts
|
|
817
|
+
const EXPIRY_SKEW_MS = 6e4;
|
|
818
|
+
const SCHEDULED_REFRESH_MS = 144e5;
|
|
819
|
+
const createAuthRequiredError = (authorizeUrl) => Object.assign(/* @__PURE__ */ new Error("Zendesk authentication required. A browser window should have opened for you to sign in. If it did not, open this URL in your browser, then retry your request:\n" + authorizeUrl), {
|
|
820
|
+
name: "AuthRequiredError",
|
|
821
|
+
authorizeUrl
|
|
822
|
+
});
|
|
823
|
+
const expiryFrom = (expiresIn) => typeof expiresIn === "number" ? Date.now() + expiresIn * 1e3 : void 0;
|
|
824
|
+
const createTokenStore = (config, logger = silentLogger) => {
|
|
825
|
+
const requested = requestedScope(config.readOnly);
|
|
826
|
+
const tokenPath = resolveTokenPath({
|
|
827
|
+
subdomain: config.subdomain,
|
|
828
|
+
oauthClientId: config.oauthClientId,
|
|
829
|
+
scope: requested
|
|
830
|
+
});
|
|
831
|
+
let token = loadToken(tokenPath);
|
|
832
|
+
if (token) {
|
|
833
|
+
if (grantCovers(token.scope, requested)) logger.debug("oauth_token_loaded_from_disk");
|
|
834
|
+
else {
|
|
835
|
+
logger.warn("oauth_token_scope_insufficient", {
|
|
836
|
+
requested,
|
|
837
|
+
granted: token.scope
|
|
838
|
+
});
|
|
839
|
+
token = void 0;
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
let authorizeUrl;
|
|
843
|
+
let starting;
|
|
844
|
+
let refreshing;
|
|
845
|
+
let probedUnknownExpiry = false;
|
|
846
|
+
const persist = (t) => saveToken(tokenPath, t, logger);
|
|
847
|
+
const setToken = (accessToken, refreshToken) => {
|
|
848
|
+
token = {
|
|
849
|
+
accessToken,
|
|
850
|
+
refreshToken,
|
|
851
|
+
scope: requested
|
|
852
|
+
};
|
|
853
|
+
probedUnknownExpiry = false;
|
|
854
|
+
persist(token);
|
|
855
|
+
};
|
|
856
|
+
const needsRefresh = (t) => typeof t.expiresAt === "number" ? Date.now() >= t.expiresAt - EXPIRY_SKEW_MS : t.refreshToken !== void 0 && !probedUnknownExpiry;
|
|
857
|
+
const noteGrant = (granted) => {
|
|
858
|
+
if (!grantCovers(granted, requested)) logger.warn("oauth_token_grant_narrowed", {
|
|
859
|
+
requested,
|
|
860
|
+
granted
|
|
861
|
+
});
|
|
862
|
+
return granted;
|
|
863
|
+
};
|
|
864
|
+
const tryRefresh = async (current, { dropOnFailure = true } = {}) => {
|
|
865
|
+
if (!current.refreshToken) return void 0;
|
|
866
|
+
try {
|
|
867
|
+
const result = await refreshAccessToken({
|
|
868
|
+
subdomain: config.subdomain,
|
|
869
|
+
oauthClientId: config.oauthClientId,
|
|
870
|
+
refreshToken: current.refreshToken
|
|
871
|
+
}, logger);
|
|
872
|
+
token = {
|
|
873
|
+
accessToken: result.access_token,
|
|
874
|
+
refreshToken: result.refresh_token ?? current.refreshToken,
|
|
875
|
+
expiresAt: expiryFrom(result.expires_in),
|
|
876
|
+
scope: noteGrant(result.scope || current.scope)
|
|
877
|
+
};
|
|
878
|
+
probedUnknownExpiry = true;
|
|
879
|
+
persist(token);
|
|
880
|
+
logger.info("oauth_token_refreshed_cached");
|
|
881
|
+
return token.accessToken;
|
|
882
|
+
} catch (err) {
|
|
883
|
+
logger.warn("oauth_token_refresh_failed", { error: err instanceof Error ? err.message : String(err) });
|
|
884
|
+
if (dropOnFailure) {
|
|
885
|
+
token = void 0;
|
|
886
|
+
clearToken(tokenPath, logger);
|
|
887
|
+
}
|
|
888
|
+
return;
|
|
889
|
+
}
|
|
890
|
+
};
|
|
891
|
+
const beginAuth = () => {
|
|
892
|
+
logger.info("oauth_auth_start");
|
|
893
|
+
return startBrowserAuth({
|
|
894
|
+
subdomain: config.subdomain,
|
|
895
|
+
oauthClientId: config.oauthClientId,
|
|
896
|
+
callbackPort: config.callbackPort,
|
|
897
|
+
readOnly: config.readOnly
|
|
898
|
+
}, logger).then((started) => {
|
|
899
|
+
authorizeUrl = started.authorizeUrl;
|
|
900
|
+
started.tokenPromise.then((result) => {
|
|
901
|
+
token = {
|
|
902
|
+
accessToken: result.access_token,
|
|
903
|
+
refreshToken: result.refresh_token,
|
|
904
|
+
expiresAt: expiryFrom(result.expires_in),
|
|
905
|
+
scope: noteGrant(result.scope || requested)
|
|
906
|
+
};
|
|
907
|
+
probedUnknownExpiry = true;
|
|
908
|
+
persist(token);
|
|
909
|
+
logger.info("oauth_token_cached");
|
|
910
|
+
}).catch((err) => {
|
|
911
|
+
logger.warn("oauth_auth_failed", { error: err instanceof Error ? err.message : String(err) });
|
|
912
|
+
}).finally(() => {
|
|
913
|
+
starting = void 0;
|
|
914
|
+
authorizeUrl = void 0;
|
|
915
|
+
});
|
|
916
|
+
return started.authorizeUrl;
|
|
917
|
+
}).catch((err) => {
|
|
918
|
+
starting = void 0;
|
|
919
|
+
throw err;
|
|
920
|
+
});
|
|
921
|
+
};
|
|
922
|
+
const refreshIfPossible = async () => {
|
|
923
|
+
const current = token;
|
|
924
|
+
if (!current?.refreshToken) return void 0;
|
|
925
|
+
if (refreshing === void 0) refreshing = tryRefresh(current).finally(() => {
|
|
926
|
+
refreshing = void 0;
|
|
927
|
+
});
|
|
928
|
+
return refreshing;
|
|
929
|
+
};
|
|
930
|
+
const getToken = async () => {
|
|
931
|
+
if (refreshing !== void 0) await refreshing;
|
|
932
|
+
if (token && !needsRefresh(token)) {
|
|
933
|
+
logger.debug("oauth_token_cache_hit");
|
|
934
|
+
return token.accessToken;
|
|
935
|
+
}
|
|
936
|
+
const refreshed = await refreshIfPossible();
|
|
937
|
+
if (refreshed) return refreshed;
|
|
938
|
+
if (starting === void 0) starting = beginAuth();
|
|
939
|
+
const url = authorizeUrl ?? await starting;
|
|
940
|
+
throw createAuthRequiredError(url);
|
|
941
|
+
};
|
|
942
|
+
const invalidate = () => {
|
|
943
|
+
if (token?.refreshToken) {
|
|
944
|
+
token = {
|
|
945
|
+
accessToken: token.accessToken,
|
|
946
|
+
refreshToken: token.refreshToken,
|
|
947
|
+
expiresAt: 0,
|
|
948
|
+
scope: token.scope
|
|
949
|
+
};
|
|
950
|
+
persist(token);
|
|
951
|
+
} else {
|
|
952
|
+
token = void 0;
|
|
953
|
+
clearToken(tokenPath, logger);
|
|
954
|
+
}
|
|
955
|
+
logger.info("oauth_token_invalidated");
|
|
956
|
+
};
|
|
957
|
+
const scheduledRefresh = setInterval(() => {
|
|
958
|
+
if (token?.refreshToken && !refreshing) refreshing = tryRefresh(token, { dropOnFailure: false }).finally(() => {
|
|
959
|
+
refreshing = void 0;
|
|
960
|
+
});
|
|
961
|
+
}, SCHEDULED_REFRESH_MS);
|
|
962
|
+
scheduledRefresh.unref?.();
|
|
963
|
+
const dispose = () => clearInterval(scheduledRefresh);
|
|
964
|
+
return {
|
|
965
|
+
getToken,
|
|
966
|
+
setToken,
|
|
967
|
+
invalidate,
|
|
968
|
+
dispose
|
|
969
|
+
};
|
|
970
|
+
};
|
|
971
|
+
//#endregion
|
|
972
|
+
export { createTokenStore };
|