jh-web-gateway 2.0.1 → 2.1.1
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 +1 -1
- package/dist/{chunk-J7PNSI42.js → chunk-7H2RJZN3.js} +225 -142
- package/dist/chunk-7H2RJZN3.js.map +1 -0
- package/dist/cli.js +32 -25
- package/dist/cli.js.map +1 -1
- package/dist/{tui-2J5JN3FF.js → tui-DIQMK2CW.js} +6 -31
- package/dist/tui-DIQMK2CW.js.map +1 -0
- package/package.json +2 -1
- package/dist/chunk-J7PNSI42.js.map +0 -1
- package/dist/tui-2J5JN3FF.js.map +0 -1
package/README.md
CHANGED
|
@@ -407,7 +407,7 @@ Point any OpenAI-compatible tool at:
|
|
|
407
407
|
|------|-------------|
|
|
408
408
|
| `--headless` | Launch Chrome without a visible window (requires prior login) |
|
|
409
409
|
| `--port <n>` | Override the configured port |
|
|
410
|
-
| `--pages <n>` | Max concurrent browser pages (default:
|
|
410
|
+
| `--pages <n>` | Max concurrent browser pages (default: 1) |
|
|
411
411
|
|
|
412
412
|
## Token Refresh
|
|
413
413
|
|
|
@@ -64,12 +64,15 @@ async function findOrOpenJhPage(browser) {
|
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
// src/infra/config.ts
|
|
67
|
-
import { readFile, writeFile, mkdir } from "fs/promises";
|
|
67
|
+
import { readFile, writeFile, mkdir, chmod } from "fs/promises";
|
|
68
68
|
import { homedir } from "os";
|
|
69
69
|
import { join } from "path";
|
|
70
70
|
function getConfigPath() {
|
|
71
71
|
return join(homedir(), ".jh-gateway", "config.json");
|
|
72
72
|
}
|
|
73
|
+
function getConfigDir() {
|
|
74
|
+
return join(homedir(), ".jh-gateway");
|
|
75
|
+
}
|
|
73
76
|
function getDefaultConfig() {
|
|
74
77
|
return {
|
|
75
78
|
cdpUrl: "http://127.0.0.1:9222",
|
|
@@ -162,15 +165,13 @@ function validateConfig(raw) {
|
|
|
162
165
|
}
|
|
163
166
|
async function loadConfig() {
|
|
164
167
|
const configPath = getConfigPath();
|
|
165
|
-
const configDir = join(homedir(), ".jh-gateway");
|
|
166
168
|
let raw;
|
|
167
169
|
try {
|
|
168
170
|
raw = await readFile(configPath, "utf8");
|
|
169
171
|
} catch (err) {
|
|
170
172
|
if (isNodeError(err) && (err.code === "ENOENT" || err.code === "ENOTDIR")) {
|
|
171
173
|
const defaults = getDefaultConfig();
|
|
172
|
-
await
|
|
173
|
-
await writeFile(configPath, JSON.stringify(defaults, null, 2), "utf8");
|
|
174
|
+
await saveConfig(defaults);
|
|
174
175
|
return defaults;
|
|
175
176
|
}
|
|
176
177
|
throw err;
|
|
@@ -183,13 +184,19 @@ async function loadConfig() {
|
|
|
183
184
|
`Config validation error: config file at ${configPath} contains malformed JSON`
|
|
184
185
|
);
|
|
185
186
|
}
|
|
186
|
-
|
|
187
|
+
const validated = validateConfig(parsed);
|
|
188
|
+
await enforceConfigPermissions(configPath, getConfigDir());
|
|
189
|
+
return validated;
|
|
187
190
|
}
|
|
188
191
|
async function saveConfig(config) {
|
|
189
192
|
const configPath = getConfigPath();
|
|
190
|
-
const configDir =
|
|
191
|
-
await mkdir(configDir, { recursive: true });
|
|
192
|
-
await writeFile(configPath, JSON.stringify(config, null, 2),
|
|
193
|
+
const configDir = getConfigDir();
|
|
194
|
+
await mkdir(configDir, { recursive: true, mode: 448 });
|
|
195
|
+
await writeFile(configPath, JSON.stringify(config, null, 2), {
|
|
196
|
+
encoding: "utf8",
|
|
197
|
+
mode: 384
|
|
198
|
+
});
|
|
199
|
+
await enforceConfigPermissions(configPath, configDir);
|
|
193
200
|
}
|
|
194
201
|
async function updateConfig(partial) {
|
|
195
202
|
const current = await loadConfig();
|
|
@@ -202,9 +209,26 @@ async function updateConfig(partial) {
|
|
|
202
209
|
function isNodeError(err) {
|
|
203
210
|
return err instanceof Error && "code" in err;
|
|
204
211
|
}
|
|
212
|
+
async function enforceConfigPermissions(configPath, configDir) {
|
|
213
|
+
await chmod(configDir, 448).catch((err) => {
|
|
214
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
215
|
+
console.warn(`[config] Could not enforce secure directory permissions: ${message}`);
|
|
216
|
+
});
|
|
217
|
+
await chmod(configPath, 384).catch((err) => {
|
|
218
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
219
|
+
console.warn(`[config] Could not enforce secure file permissions: ${message}`);
|
|
220
|
+
});
|
|
221
|
+
}
|
|
205
222
|
|
|
206
223
|
// src/core/auth-capture.ts
|
|
207
224
|
var JH_HOST = "chat.ai.jh.edu";
|
|
225
|
+
function isJhUrl(url) {
|
|
226
|
+
try {
|
|
227
|
+
return new URL(url).hostname === JH_HOST;
|
|
228
|
+
} catch {
|
|
229
|
+
return false;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
208
232
|
function getTokenExpiry(token) {
|
|
209
233
|
try {
|
|
210
234
|
const parts = token.split(".");
|
|
@@ -238,24 +262,15 @@ async function captureCredentials(cdpUrl, timeoutMs = 12e4) {
|
|
|
238
262
|
page = await context.newPage();
|
|
239
263
|
}
|
|
240
264
|
const targetPage = page;
|
|
265
|
+
const routePattern = "**/*";
|
|
241
266
|
return new Promise((resolve, reject) => {
|
|
242
267
|
let settled = false;
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
settled = true;
|
|
246
|
-
reject(
|
|
247
|
-
new Error(
|
|
248
|
-
`Credential capture timed out after ${timeoutMs / 1e3}s. Please log in to chat.ai.jh.edu and send a message to trigger authentication.`
|
|
249
|
-
)
|
|
250
|
-
);
|
|
251
|
-
}, timeoutMs);
|
|
252
|
-
targetPage.route("**/*", async (route) => {
|
|
268
|
+
let cleanedUp = false;
|
|
269
|
+
const routeHandler = async (route) => {
|
|
253
270
|
const request = route.request();
|
|
254
271
|
const headers = await request.headers();
|
|
255
272
|
const authHeader = headers["authorization"] ?? headers["Authorization"] ?? "";
|
|
256
|
-
if (!settled && authHeader.startsWith("Bearer ") && request.url()
|
|
257
|
-
settled = true;
|
|
258
|
-
clearTimeout(timer);
|
|
273
|
+
if (!settled && authHeader.startsWith("Bearer ") && isJhUrl(request.url())) {
|
|
259
274
|
try {
|
|
260
275
|
const bearerToken = authHeader.slice("Bearer ".length).trim();
|
|
261
276
|
const rawCookies = await targetPage.context().cookies();
|
|
@@ -271,14 +286,45 @@ async function captureCredentials(cdpUrl, timeoutMs = 12e4) {
|
|
|
271
286
|
expiresAt
|
|
272
287
|
};
|
|
273
288
|
await updateConfig({ credentials: captured });
|
|
274
|
-
|
|
289
|
+
settleSuccess(captured);
|
|
275
290
|
} catch (err) {
|
|
276
|
-
|
|
291
|
+
settleFailure(err);
|
|
277
292
|
}
|
|
278
293
|
}
|
|
279
|
-
await route.continue()
|
|
280
|
-
|
|
281
|
-
|
|
294
|
+
await route.continue().catch((err) => {
|
|
295
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
296
|
+
console.warn(`[auth-capture] Route continuation failed: ${message}`);
|
|
297
|
+
});
|
|
298
|
+
};
|
|
299
|
+
const cleanup = async () => {
|
|
300
|
+
if (cleanedUp) return;
|
|
301
|
+
cleanedUp = true;
|
|
302
|
+
clearTimeout(timer);
|
|
303
|
+
await targetPage.unroute(routePattern, routeHandler).catch((err) => {
|
|
304
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
305
|
+
console.warn(`[auth-capture] Route cleanup failed: ${message}`);
|
|
306
|
+
});
|
|
307
|
+
};
|
|
308
|
+
const settleSuccess = (captured) => {
|
|
309
|
+
if (settled) return;
|
|
310
|
+
settled = true;
|
|
311
|
+
void cleanup().finally(() => resolve(captured));
|
|
312
|
+
};
|
|
313
|
+
const settleFailure = (err) => {
|
|
314
|
+
if (settled) return;
|
|
315
|
+
settled = true;
|
|
316
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
317
|
+
void cleanup().finally(() => reject(error));
|
|
318
|
+
};
|
|
319
|
+
const timer = setTimeout(() => {
|
|
320
|
+
settleFailure(
|
|
321
|
+
new Error(
|
|
322
|
+
`Credential capture timed out after ${timeoutMs / 1e3}s. Please log in to chat.ai.jh.edu and send a message to trigger authentication.`
|
|
323
|
+
)
|
|
324
|
+
);
|
|
325
|
+
}, timeoutMs);
|
|
326
|
+
targetPage.route(routePattern, routeHandler).then(() => {
|
|
327
|
+
if (!isJhUrl(targetPage.url())) {
|
|
282
328
|
targetPage.goto(`https://${JH_HOST}`, { waitUntil: "commit" }).catch(() => {
|
|
283
329
|
});
|
|
284
330
|
} else {
|
|
@@ -286,11 +332,7 @@ async function captureCredentials(cdpUrl, timeoutMs = 12e4) {
|
|
|
286
332
|
});
|
|
287
333
|
}
|
|
288
334
|
}).catch((err) => {
|
|
289
|
-
|
|
290
|
-
settled = true;
|
|
291
|
-
clearTimeout(timer);
|
|
292
|
-
reject(err);
|
|
293
|
-
}
|
|
335
|
+
settleFailure(err);
|
|
294
336
|
});
|
|
295
337
|
});
|
|
296
338
|
}
|
|
@@ -316,7 +358,7 @@ async function captureCredentialsActive(cdpUrl, timeoutMs = 3e4) {
|
|
|
316
358
|
const request = route.request();
|
|
317
359
|
const headers = await request.headers();
|
|
318
360
|
const authHeader = headers["authorization"] ?? headers["Authorization"] ?? "";
|
|
319
|
-
if (!settled && authHeader.startsWith("Bearer ") && request.url()
|
|
361
|
+
if (!settled && authHeader.startsWith("Bearer ") && isJhUrl(request.url())) {
|
|
320
362
|
settled = true;
|
|
321
363
|
clearTimeout(timer);
|
|
322
364
|
try {
|
|
@@ -360,6 +402,7 @@ async function captureCredentialsActive(cdpUrl, timeoutMs = 3e4) {
|
|
|
360
402
|
|
|
361
403
|
// src/infra/gateway-auth.ts
|
|
362
404
|
import { randomBytes } from "crypto";
|
|
405
|
+
import { timingSafeEqual } from "crypto";
|
|
363
406
|
function generateApiKey() {
|
|
364
407
|
return `jh-local-${randomBytes(16).toString("hex")}`;
|
|
365
408
|
}
|
|
@@ -385,7 +428,7 @@ function authMiddleware(config) {
|
|
|
385
428
|
}
|
|
386
429
|
if (mode === "bearer") {
|
|
387
430
|
const expected = `Bearer ${token}`;
|
|
388
|
-
if (authHeader
|
|
431
|
+
if (!safeEquals(authHeader, expected)) {
|
|
389
432
|
return c.json(
|
|
390
433
|
{
|
|
391
434
|
error: {
|
|
@@ -400,7 +443,7 @@ function authMiddleware(config) {
|
|
|
400
443
|
}
|
|
401
444
|
} else if (mode === "basic") {
|
|
402
445
|
const expected = `Basic ${Buffer.from(`gateway:${token}`).toString("base64")}`;
|
|
403
|
-
if (authHeader
|
|
446
|
+
if (!safeEquals(authHeader, expected)) {
|
|
404
447
|
return c.json(
|
|
405
448
|
{
|
|
406
449
|
error: {
|
|
@@ -417,6 +460,12 @@ function authMiddleware(config) {
|
|
|
417
460
|
return next();
|
|
418
461
|
};
|
|
419
462
|
}
|
|
463
|
+
function safeEquals(actual, expected) {
|
|
464
|
+
const actualBuffer = Buffer.from(actual);
|
|
465
|
+
const expectedBuffer = Buffer.from(expected);
|
|
466
|
+
if (actualBuffer.length !== expectedBuffer.length) return false;
|
|
467
|
+
return timingSafeEqual(actualBuffer, expectedBuffer);
|
|
468
|
+
}
|
|
420
469
|
|
|
421
470
|
// src/infra/types.ts
|
|
422
471
|
var MODEL_ENDPOINT_MAP = {
|
|
@@ -1667,9 +1716,11 @@ var PagePool = class {
|
|
|
1667
1716
|
maxPages;
|
|
1668
1717
|
maxWaitMs;
|
|
1669
1718
|
initPromise = null;
|
|
1719
|
+
pagesCreating = 0;
|
|
1720
|
+
warmedUp = false;
|
|
1670
1721
|
constructor(options = {}) {
|
|
1671
1722
|
this.targetUrl = options.targetUrl ?? "https://chat.ai.jh.edu";
|
|
1672
|
-
this.maxPages = options.maxPages ??
|
|
1723
|
+
this.maxPages = options.maxPages ?? 1;
|
|
1673
1724
|
this.maxWaitMs = options.maxWaitMs ?? 12e4;
|
|
1674
1725
|
}
|
|
1675
1726
|
/** Initialize the pool with an existing browser connection and seed page */
|
|
@@ -1700,11 +1751,20 @@ var PagePool = class {
|
|
|
1700
1751
|
* Acquire a page for use. Creates new pages on-demand up to maxPages.
|
|
1701
1752
|
* Note: We intentionally don't lock here — allowing multiple requests to
|
|
1702
1753
|
* grab the same page and queue on it is actually faster than creating new pages.
|
|
1754
|
+
*
|
|
1755
|
+
* On first init (before any request succeeds), page scaling is disabled to
|
|
1756
|
+
* avoid opening new Chrome tabs that may redirect through SSO and hang.
|
|
1757
|
+
* Call `markWarmedUp()` after the first successful request to enable scaling.
|
|
1703
1758
|
*/
|
|
1704
1759
|
async acquire() {
|
|
1705
1760
|
let pooled = this.pages.find((p2) => !p2.inUse);
|
|
1706
|
-
if (!pooled && this.pages.length < this.maxPages && this.browser) {
|
|
1707
|
-
|
|
1761
|
+
if (!pooled && this.warmedUp && this.pages.length + this.pagesCreating < this.maxPages && this.browser) {
|
|
1762
|
+
this.pagesCreating++;
|
|
1763
|
+
try {
|
|
1764
|
+
pooled = await this.createPage();
|
|
1765
|
+
} finally {
|
|
1766
|
+
this.pagesCreating--;
|
|
1767
|
+
}
|
|
1708
1768
|
}
|
|
1709
1769
|
if (!pooled) {
|
|
1710
1770
|
pooled = this.pages.reduce(
|
|
@@ -1718,9 +1778,20 @@ var PagePool = class {
|
|
|
1718
1778
|
queue: p.queue,
|
|
1719
1779
|
release: () => {
|
|
1720
1780
|
p.inUse = false;
|
|
1781
|
+
if (!this.warmedUp) {
|
|
1782
|
+
this.warmedUp = true;
|
|
1783
|
+
console.log(`[PagePool] Warm-up complete \u2014 page scaling enabled (max ${this.maxPages})`);
|
|
1784
|
+
}
|
|
1721
1785
|
}
|
|
1722
1786
|
};
|
|
1723
1787
|
}
|
|
1788
|
+
/** Mark the pool as warmed up, enabling page scaling. */
|
|
1789
|
+
markWarmedUp() {
|
|
1790
|
+
if (!this.warmedUp) {
|
|
1791
|
+
this.warmedUp = true;
|
|
1792
|
+
console.log(`[PagePool] Warm-up complete \u2014 page scaling enabled (max ${this.maxPages})`);
|
|
1793
|
+
}
|
|
1794
|
+
}
|
|
1724
1795
|
async createPage() {
|
|
1725
1796
|
if (!this.browser) {
|
|
1726
1797
|
throw new Error("PagePool not initialized");
|
|
@@ -1731,15 +1802,27 @@ var PagePool = class {
|
|
|
1731
1802
|
throw new Error("No browser context available");
|
|
1732
1803
|
}
|
|
1733
1804
|
const page = await context.newPage();
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1805
|
+
try {
|
|
1806
|
+
await page.goto(this.targetUrl, { waitUntil: "networkidle", timeout: 3e4 });
|
|
1807
|
+
const finalUrl = page.url();
|
|
1808
|
+
if (!finalUrl.includes("chat.ai.jh.edu")) {
|
|
1809
|
+
throw new Error(
|
|
1810
|
+
`New page redirected away from target: ${finalUrl} \u2014 browser session may have expired, restart without --headless to re-login.`
|
|
1811
|
+
);
|
|
1812
|
+
}
|
|
1813
|
+
console.log(`[PagePool] New page ready: ${finalUrl}`);
|
|
1814
|
+
const pooled = {
|
|
1815
|
+
page,
|
|
1816
|
+
queue: new RequestQueue(this.maxWaitMs),
|
|
1817
|
+
inUse: false
|
|
1818
|
+
};
|
|
1819
|
+
this.pages.push(pooled);
|
|
1820
|
+
return pooled;
|
|
1821
|
+
} catch (err) {
|
|
1822
|
+
await page.close().catch(() => {
|
|
1823
|
+
});
|
|
1824
|
+
throw err;
|
|
1825
|
+
}
|
|
1743
1826
|
}
|
|
1744
1827
|
/** Close all pages except the seed page */
|
|
1745
1828
|
async drain() {
|
|
@@ -1755,6 +1838,99 @@ var PagePool = class {
|
|
|
1755
1838
|
}
|
|
1756
1839
|
};
|
|
1757
1840
|
|
|
1841
|
+
// src/core/token-refresher.ts
|
|
1842
|
+
var CredentialHolder = class {
|
|
1843
|
+
creds = null;
|
|
1844
|
+
/** Returns the current credentials, or `null` if none have been set yet. */
|
|
1845
|
+
get() {
|
|
1846
|
+
return this.creds;
|
|
1847
|
+
}
|
|
1848
|
+
/** Atomically replaces the stored credentials. */
|
|
1849
|
+
set(creds) {
|
|
1850
|
+
this.creds = creds;
|
|
1851
|
+
}
|
|
1852
|
+
};
|
|
1853
|
+
function shouldRefresh(nowMs, expiresAt, thresholdMs) {
|
|
1854
|
+
return expiresAt * 1e3 - nowMs < thresholdMs;
|
|
1855
|
+
}
|
|
1856
|
+
var BACKOFF_DELAYS = [5e3, 15e3, 3e4];
|
|
1857
|
+
var TokenRefresher = class {
|
|
1858
|
+
credentialHolder;
|
|
1859
|
+
cdpUrl;
|
|
1860
|
+
checkIntervalMs;
|
|
1861
|
+
refreshBeforeExpiryMs;
|
|
1862
|
+
maxRetries;
|
|
1863
|
+
intervalId = null;
|
|
1864
|
+
constructor(credentialHolder, cdpUrl, options) {
|
|
1865
|
+
this.credentialHolder = credentialHolder;
|
|
1866
|
+
this.cdpUrl = cdpUrl;
|
|
1867
|
+
this.checkIntervalMs = options?.checkIntervalMs ?? 6e4;
|
|
1868
|
+
this.refreshBeforeExpiryMs = options?.refreshBeforeExpiryMs ?? 3e5;
|
|
1869
|
+
this.maxRetries = options?.maxRetries ?? 3;
|
|
1870
|
+
}
|
|
1871
|
+
/** Start the background check interval. */
|
|
1872
|
+
start() {
|
|
1873
|
+
if (this.intervalId !== null) return;
|
|
1874
|
+
this.intervalId = setInterval(() => {
|
|
1875
|
+
void this.checkAndRefresh();
|
|
1876
|
+
}, this.checkIntervalMs);
|
|
1877
|
+
}
|
|
1878
|
+
/** Stop the background check interval. */
|
|
1879
|
+
stop() {
|
|
1880
|
+
if (this.intervalId !== null) {
|
|
1881
|
+
clearInterval(this.intervalId);
|
|
1882
|
+
this.intervalId = null;
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1885
|
+
/** Check if a refresh is needed and perform it. Returns true if refreshed. */
|
|
1886
|
+
async checkAndRefresh() {
|
|
1887
|
+
const creds = this.credentialHolder.get();
|
|
1888
|
+
if (!creds || !creds.expiresAt) {
|
|
1889
|
+
return false;
|
|
1890
|
+
}
|
|
1891
|
+
if (!shouldRefresh(Date.now(), creds.expiresAt, this.refreshBeforeExpiryMs)) {
|
|
1892
|
+
return false;
|
|
1893
|
+
}
|
|
1894
|
+
for (let attempt = 0; attempt < this.maxRetries; attempt++) {
|
|
1895
|
+
try {
|
|
1896
|
+
const newCreds = await captureCredentialsActive(this.cdpUrl);
|
|
1897
|
+
const gatewayCreds = {
|
|
1898
|
+
bearerToken: newCreds.bearerToken,
|
|
1899
|
+
cookie: newCreds.cookie,
|
|
1900
|
+
userAgent: newCreds.userAgent,
|
|
1901
|
+
expiresAt: newCreds.expiresAt
|
|
1902
|
+
};
|
|
1903
|
+
this.credentialHolder.set(gatewayCreds);
|
|
1904
|
+
await updateConfig({ credentials: gatewayCreds });
|
|
1905
|
+
const expiryDate = new Date(newCreds.expiresAt * 1e3).toISOString();
|
|
1906
|
+
console.log(`[TokenRefresher] Credentials refreshed successfully. New expiry: ${expiryDate}`);
|
|
1907
|
+
return true;
|
|
1908
|
+
} catch (err) {
|
|
1909
|
+
const delay = BACKOFF_DELAYS[attempt] ?? BACKOFF_DELAYS[BACKOFF_DELAYS.length - 1];
|
|
1910
|
+
if (attempt < this.maxRetries - 1) {
|
|
1911
|
+
console.warn(
|
|
1912
|
+
`[TokenRefresher] Refresh attempt ${attempt + 1}/${this.maxRetries} failed, retrying in ${delay / 1e3}s...`
|
|
1913
|
+
);
|
|
1914
|
+
await this.sleep(delay);
|
|
1915
|
+
} else {
|
|
1916
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1917
|
+
console.warn(
|
|
1918
|
+
`
|
|
1919
|
+
\u26A0\uFE0F [TokenRefresher] All ${this.maxRetries} refresh attempts failed. ${msg}
|
|
1920
|
+
Continuing with current credentials. If requests start failing with 401,
|
|
1921
|
+
restart with \`jh-gateway start\` (without --headless) to re-login.
|
|
1922
|
+
`
|
|
1923
|
+
);
|
|
1924
|
+
}
|
|
1925
|
+
}
|
|
1926
|
+
}
|
|
1927
|
+
return false;
|
|
1928
|
+
}
|
|
1929
|
+
sleep(ms) {
|
|
1930
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1931
|
+
}
|
|
1932
|
+
};
|
|
1933
|
+
|
|
1758
1934
|
// src/infra/chrome-manager.ts
|
|
1759
1935
|
import { existsSync } from "fs";
|
|
1760
1936
|
import { execSync, spawn } from "child_process";
|
|
@@ -1996,99 +2172,6 @@ Expected locations for ${process.platform}:
|
|
|
1996
2172
|
}
|
|
1997
2173
|
};
|
|
1998
2174
|
|
|
1999
|
-
// src/core/token-refresher.ts
|
|
2000
|
-
var CredentialHolder = class {
|
|
2001
|
-
creds = null;
|
|
2002
|
-
/** Returns the current credentials, or `null` if none have been set yet. */
|
|
2003
|
-
get() {
|
|
2004
|
-
return this.creds;
|
|
2005
|
-
}
|
|
2006
|
-
/** Atomically replaces the stored credentials. */
|
|
2007
|
-
set(creds) {
|
|
2008
|
-
this.creds = creds;
|
|
2009
|
-
}
|
|
2010
|
-
};
|
|
2011
|
-
function shouldRefresh(nowMs, expiresAt, thresholdMs) {
|
|
2012
|
-
return expiresAt * 1e3 - nowMs < thresholdMs;
|
|
2013
|
-
}
|
|
2014
|
-
var BACKOFF_DELAYS = [5e3, 15e3, 3e4];
|
|
2015
|
-
var TokenRefresher = class {
|
|
2016
|
-
credentialHolder;
|
|
2017
|
-
cdpUrl;
|
|
2018
|
-
checkIntervalMs;
|
|
2019
|
-
refreshBeforeExpiryMs;
|
|
2020
|
-
maxRetries;
|
|
2021
|
-
intervalId = null;
|
|
2022
|
-
constructor(credentialHolder, cdpUrl, options) {
|
|
2023
|
-
this.credentialHolder = credentialHolder;
|
|
2024
|
-
this.cdpUrl = cdpUrl;
|
|
2025
|
-
this.checkIntervalMs = options?.checkIntervalMs ?? 6e4;
|
|
2026
|
-
this.refreshBeforeExpiryMs = options?.refreshBeforeExpiryMs ?? 3e5;
|
|
2027
|
-
this.maxRetries = options?.maxRetries ?? 3;
|
|
2028
|
-
}
|
|
2029
|
-
/** Start the background check interval. */
|
|
2030
|
-
start() {
|
|
2031
|
-
if (this.intervalId !== null) return;
|
|
2032
|
-
this.intervalId = setInterval(() => {
|
|
2033
|
-
void this.checkAndRefresh();
|
|
2034
|
-
}, this.checkIntervalMs);
|
|
2035
|
-
}
|
|
2036
|
-
/** Stop the background check interval. */
|
|
2037
|
-
stop() {
|
|
2038
|
-
if (this.intervalId !== null) {
|
|
2039
|
-
clearInterval(this.intervalId);
|
|
2040
|
-
this.intervalId = null;
|
|
2041
|
-
}
|
|
2042
|
-
}
|
|
2043
|
-
/** Check if a refresh is needed and perform it. Returns true if refreshed. */
|
|
2044
|
-
async checkAndRefresh() {
|
|
2045
|
-
const creds = this.credentialHolder.get();
|
|
2046
|
-
if (!creds || !creds.expiresAt) {
|
|
2047
|
-
return false;
|
|
2048
|
-
}
|
|
2049
|
-
if (!shouldRefresh(Date.now(), creds.expiresAt, this.refreshBeforeExpiryMs)) {
|
|
2050
|
-
return false;
|
|
2051
|
-
}
|
|
2052
|
-
for (let attempt = 0; attempt < this.maxRetries; attempt++) {
|
|
2053
|
-
try {
|
|
2054
|
-
const newCreds = await captureCredentialsActive(this.cdpUrl);
|
|
2055
|
-
const gatewayCreds = {
|
|
2056
|
-
bearerToken: newCreds.bearerToken,
|
|
2057
|
-
cookie: newCreds.cookie,
|
|
2058
|
-
userAgent: newCreds.userAgent,
|
|
2059
|
-
expiresAt: newCreds.expiresAt
|
|
2060
|
-
};
|
|
2061
|
-
this.credentialHolder.set(gatewayCreds);
|
|
2062
|
-
await updateConfig({ credentials: gatewayCreds });
|
|
2063
|
-
const expiryDate = new Date(newCreds.expiresAt * 1e3).toISOString();
|
|
2064
|
-
console.log(`[TokenRefresher] Credentials refreshed successfully. New expiry: ${expiryDate}`);
|
|
2065
|
-
return true;
|
|
2066
|
-
} catch (err) {
|
|
2067
|
-
const delay = BACKOFF_DELAYS[attempt] ?? BACKOFF_DELAYS[BACKOFF_DELAYS.length - 1];
|
|
2068
|
-
if (attempt < this.maxRetries - 1) {
|
|
2069
|
-
console.warn(
|
|
2070
|
-
`[TokenRefresher] Refresh attempt ${attempt + 1}/${this.maxRetries} failed, retrying in ${delay / 1e3}s...`
|
|
2071
|
-
);
|
|
2072
|
-
await this.sleep(delay);
|
|
2073
|
-
} else {
|
|
2074
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
2075
|
-
console.warn(
|
|
2076
|
-
`
|
|
2077
|
-
\u26A0\uFE0F [TokenRefresher] All ${this.maxRetries} refresh attempts failed. ${msg}
|
|
2078
|
-
Continuing with current credentials. If requests start failing with 401,
|
|
2079
|
-
restart with \`jh-gateway start\` (without --headless) to re-login.
|
|
2080
|
-
`
|
|
2081
|
-
);
|
|
2082
|
-
}
|
|
2083
|
-
}
|
|
2084
|
-
}
|
|
2085
|
-
return false;
|
|
2086
|
-
}
|
|
2087
|
-
sleep(ms) {
|
|
2088
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
2089
|
-
}
|
|
2090
|
-
};
|
|
2091
|
-
|
|
2092
2175
|
export {
|
|
2093
2176
|
getChromeWebSocketUrl,
|
|
2094
2177
|
connectToChrome,
|
|
@@ -2104,9 +2187,9 @@ export {
|
|
|
2104
2187
|
isTokenExpired,
|
|
2105
2188
|
startServer,
|
|
2106
2189
|
PagePool,
|
|
2107
|
-
ChromeManager,
|
|
2108
2190
|
CredentialHolder,
|
|
2109
2191
|
shouldRefresh,
|
|
2110
|
-
TokenRefresher
|
|
2192
|
+
TokenRefresher,
|
|
2193
|
+
ChromeManager
|
|
2111
2194
|
};
|
|
2112
|
-
//# sourceMappingURL=chunk-
|
|
2195
|
+
//# sourceMappingURL=chunk-7H2RJZN3.js.map
|