@lincoln504/pi-research 0.1.13 → 1.0.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/LICENSE +1 -1
- package/README.md +55 -138
- package/dist/cli.mjs +23375 -0
- package/dist/prompts/research-knowledge-relevance-triage.md +35 -0
- package/dist/prompts/research-knowledge-search-extractor.md +41 -0
- package/dist/prompts/research-tool-usage.md +92 -0
- package/dist/prompts/researcher.md +89 -0
- package/dist/prompts/system-coordinator.md +75 -0
- package/dist/prompts/system-lead-evaluator.md +118 -0
- package/dist/thread-worker.mjs +2611 -0
- package/dist/webgpu-probe.mjs +80 -0
- package/docs/AGENT-SKILL.md +130 -0
- package/docs/ARCHITECTURE.md +306 -80
- package/docs/CONFIGURATION.md +249 -0
- package/docs/KNOWLEDGE-STORE.md +211 -0
- package/docs/PI-EXTENSION.md +91 -0
- package/docs/SDK.md +122 -0
- package/docs/THIRD-PARTY-NOTICES.md +66 -0
- package/package.json +102 -36
- package/scripts/build.cjs +140 -0
- package/scripts/cleanup.cjs +149 -0
- package/scripts/prepare.cjs +28 -0
- package/scripts/setup.cjs +134 -0
- package/skills/pi-research/README.md +90 -0
- package/skills/pi-research/SKILL.md +102 -0
- package/skills/pi-research/references/configuration.md +77 -0
- package/skills/pi-research/scripts/run.mjs +237 -0
- package/src/cleanup/research-cleanup.ts +140 -0
- package/src/cli.ts +1195 -0
- package/src/config.ts +1032 -155
- package/src/constants.ts +115 -84
- package/src/core/health-check-service.ts +265 -0
- package/src/core/interfaces/health-check-interfaces.ts +50 -0
- package/src/core/interfaces/knowledge-interfaces.ts +190 -0
- package/src/core/interfaces/observer-interfaces.ts +67 -0
- package/src/core/interfaces/orchestration-interfaces.ts +86 -0
- package/src/core/interfaces/planning-interfaces.ts +100 -0
- package/src/core/interfaces/process-interfaces.ts +37 -0
- package/src/core/interfaces/research-plan-types.ts +52 -0
- package/src/core/interfaces/scheduler-interfaces.ts +91 -0
- package/src/core/interfaces/service-names.ts +39 -0
- package/src/core/interfaces/state-manager-interfaces.ts +29 -0
- package/src/core/interfaces/steering-interfaces.ts +31 -0
- package/src/core/llm/agentic-repair.ts +169 -0
- package/src/core/llm/inject-date.ts +52 -0
- package/src/core/llm/llm-timeout.ts +64 -0
- package/src/core/llm/llm-utils.ts +86 -0
- package/src/core/llm/model-registry-factory.ts +287 -0
- package/src/core/llm/pi-ai-completion.ts +29 -0
- package/src/core/llm/prompts.ts +50 -0
- package/src/core/llm/research-model-resolver.ts +91 -0
- package/src/core/planning-constants.ts +13 -0
- package/src/core/planning-service.ts +649 -0
- package/src/core/planning-utils.ts +299 -0
- package/src/core/scheduler-factory.ts +48 -0
- package/src/core/scheduler-service.ts +123 -0
- package/src/core/service-initialization.ts +194 -0
- package/src/core/service-interfaces.ts +48 -0
- package/src/core/service-registry.ts +659 -0
- package/src/healthcheck/index.ts +271 -265
- package/src/healthcheck/registry.ts +130 -0
- package/src/index.ts +639 -50
- package/src/infrastructure/browser/browser-cleanup.ts +309 -0
- package/src/infrastructure/browser/browser-client.ts +199 -0
- package/src/infrastructure/browser/browser-error-utils.ts +160 -0
- package/src/infrastructure/browser/browser-lifecycle.ts +120 -0
- package/src/infrastructure/browser/browser-server.ts +174 -0
- package/src/infrastructure/browser/browser-task-scheduler.ts +461 -0
- package/src/infrastructure/browser/cleanup-utils.ts +155 -0
- package/src/infrastructure/browser/client-agent.ts +31 -0
- package/src/infrastructure/browser/config.ts +288 -0
- package/src/infrastructure/browser/ensure-browser.ts +205 -0
- package/src/infrastructure/browser/index.ts +37 -0
- package/src/infrastructure/browser/priority-task-queue.ts +226 -0
- package/src/infrastructure/browser/scheduler-factory.ts +354 -0
- package/src/infrastructure/browser/task-execution-service.ts +209 -0
- package/src/infrastructure/browser/thread-worker-browser.ts +282 -0
- package/src/infrastructure/browser/thread-worker-lifecycle.ts +323 -0
- package/src/infrastructure/browser/thread-worker-messaging.ts +662 -0
- package/src/infrastructure/browser/thread-worker-types.ts +25 -0
- package/src/infrastructure/browser/thread-worker.mjs +2611 -0
- package/src/infrastructure/browser/thread-worker.ts +168 -0
- package/src/infrastructure/browser/worker-pool-manager.ts +449 -0
- package/src/infrastructure/embedding/embedding-client.ts +205 -0
- package/src/infrastructure/embedding/embedding-factory.ts +324 -0
- package/src/infrastructure/embedding/embedding-server.ts +535 -0
- package/src/infrastructure/file-lock-service.ts +592 -0
- package/src/infrastructure/gpu-resource-service.ts +191 -0
- package/src/infrastructure/knowledge-store-service.ts +475 -0
- package/src/infrastructure/metrics-service.ts +129 -0
- package/src/infrastructure/process-lifecycle-service.ts +253 -0
- package/src/infrastructure/scheduler-factory-service.ts +58 -0
- package/src/infrastructure/service-initialization.ts +292 -0
- package/src/infrastructure/state/state-backup-manager.ts +290 -0
- package/src/infrastructure/state/state-browser-api.ts +62 -0
- package/src/infrastructure/state/state-browser-manager.ts +70 -0
- package/src/infrastructure/state/state-manager-service.ts +264 -0
- package/src/infrastructure/state/state-manager.ts +547 -0
- package/src/infrastructure/state/state-metrics.ts +92 -0
- package/src/infrastructure/state/state-migration.ts +103 -0
- package/src/infrastructure/state/state-path-configuration.ts +119 -0
- package/src/infrastructure/state/state-session-api.ts +94 -0
- package/src/infrastructure/state/state-session-manager.ts +127 -0
- package/src/infrastructure/state/state-validator.ts +93 -0
- package/src/infrastructure/types/state-types.ts +85 -0
- package/src/knowledge/chunker.ts +157 -0
- package/src/knowledge/embedder-init.ts +413 -0
- package/src/knowledge/embedder-utils.ts +296 -0
- package/src/knowledge/embedder.ts +592 -0
- package/src/knowledge/index.ts +126 -0
- package/src/knowledge/lancedb-loader.ts +35 -0
- package/src/knowledge/migration.ts +17 -0
- package/src/knowledge/model-config.ts +132 -0
- package/src/knowledge/onnx-env.ts +48 -0
- package/src/knowledge/store-operations.ts +291 -0
- package/src/knowledge/store-schema.ts +79 -0
- package/src/knowledge/store.ts +1251 -0
- package/src/knowledge/transformers-loader.ts +31 -0
- package/src/knowledge/webgpu-probe.mjs +80 -0
- package/src/knowledge/webgpu-viability.ts +255 -0
- package/src/knowledge/writer-queue.ts +252 -0
- package/src/logger.ts +316 -122
- package/src/observers/research-observer-impl.ts +459 -0
- package/src/orchestration/deep-research-orchestrator.ts +524 -773
- package/src/orchestration/deep-research-types.ts +47 -35
- package/src/orchestration/headless-observer.ts +114 -0
- package/src/orchestration/orchestration-types.ts +33 -0
- package/src/orchestration/quick-research-orchestrator.ts +463 -0
- package/src/orchestration/research-orchestration-service.ts +549 -0
- package/src/orchestration/research-session-service.ts +178 -0
- package/src/orchestration/research-synthesis-service.ts +348 -0
- package/src/orchestration/researcher-executor.ts +394 -0
- package/src/orchestration/researcher.ts +115 -42
- package/src/orchestration/service-initialization.ts +109 -0
- package/src/orchestration/session-context.ts +7 -3
- package/src/orchestration/session-state.ts +672 -0
- package/src/prompts/research-knowledge-relevance-triage.md +35 -0
- package/src/prompts/research-knowledge-search-extractor.md +41 -0
- package/src/prompts/research-tool-usage.md +63 -16
- package/src/prompts/researcher.md +59 -136
- package/src/prompts/system-coordinator.md +64 -13
- package/src/prompts/system-lead-evaluator.md +82 -189
- package/src/research-config.ts +872 -0
- package/src/sdk.ts +847 -0
- package/src/security/cisa-kev.ts +72 -9
- package/src/security/cvss.ts +92 -0
- package/src/security/ecosystem.ts +45 -0
- package/src/security/github-advisories.ts +168 -428
- package/src/security/github-advisory-types.ts +282 -0
- package/src/security/index.ts +63 -177
- package/src/security/nvd-client.ts +550 -0
- package/src/security/nvd-types.ts +179 -0
- package/src/security/osv-client.ts +246 -0
- package/src/security/osv-types.ts +285 -0
- package/src/security/types.ts +67 -53
- package/src/skill-install/skill-installer.ts +519 -0
- package/src/stackexchange/index.ts +81 -27
- package/src/stackexchange/output/compact.ts +7 -6
- package/src/stackexchange/output/table.ts +11 -7
- package/src/stackexchange/queries.ts +0 -10
- package/src/stackexchange/rest-client.ts +141 -66
- package/src/stackexchange/types.ts +0 -17
- package/src/tool.ts +15 -423
- package/src/tools/grep.ts +32 -310
- package/src/tools/health-tool-definition.ts +101 -0
- package/src/tools/index.ts +38 -9
- package/src/tools/research-knowledge-search.ts +974 -0
- package/src/tools/research-knowledge-types.ts +77 -0
- package/src/tools/research-tool-definition.ts +494 -0
- package/src/tools/scrape.ts +286 -388
- package/src/tools/search.ts +98 -153
- package/src/tools/security.ts +107 -80
- package/src/tools/stackexchange.ts +58 -35
- package/src/tools/youtube-transcript.ts +163 -0
- package/src/tui/knowledge-search-panel.ts +86 -0
- package/src/tui/pulse.ts +86 -0
- package/src/tui/research-health.ts +124 -0
- package/src/tui/research-panel-color-utils.ts +340 -0
- package/src/tui/research-panel-format.ts +54 -0
- package/src/tui/research-panel-state.ts +404 -0
- package/src/tui/research-panel-wave.ts +280 -0
- package/src/tui/research-panel.ts +339 -435
- package/src/tui/research-tui-manager.ts +233 -0
- package/src/tui/tui-controller.ts +137 -0
- package/src/types/extension-context.ts +18 -49
- package/src/types/index.ts +61 -0
- package/src/types/llm.ts +68 -29
- package/src/types/research-panel-types.ts +61 -0
- package/src/utils/circuit-breaker.ts +146 -0
- package/src/utils/citation-utils.ts +97 -0
- package/src/utils/disk-space-checker.ts +55 -0
- package/src/utils/error-tracker.ts +265 -0
- package/src/utils/host-config.ts +45 -0
- package/src/utils/input-validation.ts +8 -10
- package/src/utils/json-utils.ts +150 -16
- package/src/utils/llm-usage.ts +54 -0
- package/src/utils/log-rotation.ts +125 -0
- package/src/utils/log-utils.ts +159 -0
- package/src/utils/make-resource-loader.ts +3 -1
- package/src/utils/metrics-summary.ts +459 -0
- package/src/utils/metrics.ts +302 -0
- package/src/utils/research-export.ts +28 -12
- package/src/utils/safe-unref.ts +31 -0
- package/src/utils/shared-links.ts +195 -90
- package/src/utils/shutdown-manager.ts +65 -0
- package/src/utils/stdio-capture.ts +407 -0
- package/src/utils/text-utils.ts +258 -11
- package/src/utils/tool-usage-tracker.ts +83 -16
- package/src/utils/url-utils.ts +255 -0
- package/src/utils/user-agent.ts +28 -0
- package/src/web-research/browser-search.ts +179 -0
- package/src/web-research/retry-utils.ts +173 -77
- package/src/web-research/scraper-types.ts +72 -0
- package/src/web-research/scraper-utils.ts +434 -0
- package/src/web-research/search.ts +78 -218
- package/src/web-research/types.ts +9 -24
- package/src/web-research/utils.ts +2 -143
- package/src/web-research/web-scraper.ts +477 -0
- package/src/youtube/index.ts +154 -0
- package/src/youtube/potoken.ts +259 -0
- package/src/youtube/transcript-client.ts +390 -0
- package/src/youtube/video-id.ts +118 -0
- package/config/searxng/default-settings.yml +0 -189
- package/config/searxng/limiter.toml +0 -34
- package/config/tooling/eslint.config.js +0 -73
- package/config/tooling/vitest.config.integration.ts +0 -17
- package/config/tooling/vitest.config.ts +0 -40
- package/config/tooling/vitest.config.unit.ts +0 -12
- package/docs/assets/README-banner.jpg +0 -0
- package/scripts/setup.js +0 -212
- package/src/infrastructure/searxng-lifecycle.ts +0 -644
- package/src/infrastructure/searxng-manager.ts +0 -1183
- package/src/infrastructure/state-manager.ts +0 -918
- package/src/orchestration/deep-research-reducer.ts +0 -129
- package/src/orchestration/id-utils.ts +0 -63
- package/src/security/nvd.ts +0 -532
- package/src/security/osv.ts +0 -487
- package/src/stackexchange/cache.ts +0 -111
- package/src/stackexchange/output/index.ts +0 -7
- package/src/stackexchange/output/json.ts +0 -21
- package/src/tui/searxng-status.ts +0 -80
- package/src/utils/inject-date.ts +0 -29
- package/src/utils/research-error.ts +0 -205
- package/src/utils/searxng-config.ts +0 -94
- package/src/utils/session-state.ts +0 -295
- package/src/web-research/dependencies.ts +0 -20
- package/src/web-research/scrapers.ts +0 -935
- package/tsconfig.json +0 -40
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser Lifecycle
|
|
3
|
+
*
|
|
4
|
+
* Top-level lifecycle helpers that tie together the scheduler, worker pool,
|
|
5
|
+
* and state-manager. Used by SchedulerService.dispose() to tear down the
|
|
6
|
+
* browser subsystem cleanly.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { logger } from '../../logger.ts';
|
|
10
|
+
import { tryGetService } from '../../core/service-registry.ts';
|
|
11
|
+
import { ServiceNames } from '../../core/service-interfaces.ts';
|
|
12
|
+
import type { ISchedulerInternals } from '../../core/interfaces/scheduler-interfaces.ts';
|
|
13
|
+
import { BrowserTaskScheduler } from './browser-task-scheduler.ts';
|
|
14
|
+
import type { WorkerPoolManager } from './worker-pool-manager.ts';
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Stop the current browser manager / scheduler instance.
|
|
19
|
+
*
|
|
20
|
+
* Retrieves the cached scheduler from SchedulerService, calls shutdown() if
|
|
21
|
+
* it is a BrowserTaskScheduler, then clears all cached references so the next
|
|
22
|
+
* request starts fresh.
|
|
23
|
+
*/
|
|
24
|
+
export async function stopBrowserManager(): Promise<void> {
|
|
25
|
+
try {
|
|
26
|
+
// Try to get the service synchronously first to avoid errors if not initialized
|
|
27
|
+
const schedulerService = tryGetService<ISchedulerInternals>(ServiceNames.SCHEDULER);
|
|
28
|
+
|
|
29
|
+
if (!schedulerService) {
|
|
30
|
+
logger.debug('[BrowserLifecycle] Scheduler service not available, nothing to stop');
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const instance = schedulerService.getSchedulerInstance();
|
|
35
|
+
|
|
36
|
+
if (instance instanceof BrowserTaskScheduler) {
|
|
37
|
+
await instance.shutdown();
|
|
38
|
+
} else if (instance && typeof (instance as any).shutdown === 'function') {
|
|
39
|
+
await (instance as any).shutdown();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Clear all cached scheduler references
|
|
43
|
+
schedulerService.setSchedulerInstance(null);
|
|
44
|
+
schedulerService.setSchedulerVersion(null);
|
|
45
|
+
schedulerService.setSchedulerInitializationPromise(null);
|
|
46
|
+
|
|
47
|
+
logger.debug('[BrowserLifecycle] Browser manager stopped and references cleared');
|
|
48
|
+
} catch (err) {
|
|
49
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
50
|
+
logger.warn('[BrowserLifecycle] Error stopping browser manager:', msg);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Wait until any background scheduler shutdown (from a fire-and-forget
|
|
56
|
+
* forceSchedulerRestart) has fully completed, including the WorkerPoolManager's
|
|
57
|
+
* 1500ms IPC drain. Safe to call when no shutdown is in progress — resolves
|
|
58
|
+
* immediately.
|
|
59
|
+
*
|
|
60
|
+
* Strategy:
|
|
61
|
+
* 1. Await the pending shutdown promise stored in SchedulerService (set by
|
|
62
|
+
* forceSchedulerRestart when it fires-and-forgets oldScheduler.shutdown()).
|
|
63
|
+
* This is the authoritative signal that the full chain — including the
|
|
64
|
+
* WorkerPoolManager drain — has completed.
|
|
65
|
+
* 2. Fall back to polling WorkerPoolManager.isPoolShuttingDown() for any
|
|
66
|
+
* shutdown that was initiated outside of forceSchedulerRestart (e.g.,
|
|
67
|
+
* stopBrowserManager called directly on a BrowserTaskScheduler).
|
|
68
|
+
*
|
|
69
|
+
* Used primarily in integration tests to bridge the gap between a
|
|
70
|
+
* forceSchedulerRestart() call and subsequent browser task operations.
|
|
71
|
+
*/
|
|
72
|
+
export async function waitForBrowserPoolIdle(maxWaitMs = 15000): Promise<void> {
|
|
73
|
+
const start = Date.now();
|
|
74
|
+
|
|
75
|
+
// Phase 1: await the tracked fire-and-forget shutdown promise.
|
|
76
|
+
// This covers the common case where forceSchedulerRestart was called: the promise
|
|
77
|
+
// resolves only after BrowserTaskScheduler.shutdown() — including the WorkerPoolManager
|
|
78
|
+
// drain — completes.
|
|
79
|
+
//
|
|
80
|
+
// Caveat: the retry path in task-execution-service may call forceSchedulerRestart again
|
|
81
|
+
// on the failed (Scheduler2) instance, which replaces pendingShutdownPromise with a
|
|
82
|
+
// quickly-resolving no-op promise (WorkerPoolManager.shutdown() is already running and
|
|
83
|
+
// returns immediately on the second call). To guard against this, we always fall through
|
|
84
|
+
// to Phase 2 and poll isPoolShuttingDown() after the promise resolves.
|
|
85
|
+
const schedulerService = tryGetService<ISchedulerInternals>(ServiceNames.SCHEDULER);
|
|
86
|
+
const pendingShutdown = schedulerService?.getPendingShutdownPromise();
|
|
87
|
+
if (pendingShutdown) {
|
|
88
|
+
const remainingMs = maxWaitMs - (Date.now() - start);
|
|
89
|
+
if (remainingMs > 0) {
|
|
90
|
+
let timeoutId: NodeJS.Timeout | undefined;
|
|
91
|
+
const timeoutPromise = new Promise<'timeout'>(resolve => {
|
|
92
|
+
timeoutId = setTimeout(() => resolve('timeout'), remainingMs);
|
|
93
|
+
});
|
|
94
|
+
const pendingPromise = pendingShutdown.then(() => 'done' as const);
|
|
95
|
+
pendingPromise.catch((err: any) => logger.debug(`[BrowserLifecycle] Background pending shutdown rejection: ${err instanceof Error ? err.message : String(err)}`));
|
|
96
|
+
const winner = await Promise.race([
|
|
97
|
+
pendingPromise,
|
|
98
|
+
timeoutPromise,
|
|
99
|
+
]);
|
|
100
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
101
|
+
if (winner === 'timeout') {
|
|
102
|
+
logger.warn('[BrowserLifecycle] waitForBrowserPoolIdle: timed out awaiting pendingShutdown after', maxWaitMs, 'ms');
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
// Falls through to Phase 2 — the pending promise may have been a no-op shutdown
|
|
107
|
+
// that resolved before the original drain finished.
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Phase 2: poll WorkerPoolManager.isPoolShuttingDown() until idle or timeout.
|
|
111
|
+
const poolManager = tryGetService<WorkerPoolManager>(ServiceNames.WORKER_POOL_MANAGER);
|
|
112
|
+
if (!poolManager || !poolManager.isPoolShuttingDown()) return;
|
|
113
|
+
|
|
114
|
+
const deadline = Date.now() + (maxWaitMs - (Date.now() - start));
|
|
115
|
+
while (Date.now() < deadline) {
|
|
116
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
117
|
+
if (!poolManager.isPoolShuttingDown()) return;
|
|
118
|
+
}
|
|
119
|
+
logger.warn('[BrowserLifecycle] waitForBrowserPoolIdle: pool still shutting down after', maxWaitMs, 'ms');
|
|
120
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import * as http from 'node:http';
|
|
2
|
+
import * as crypto from 'node:crypto';
|
|
3
|
+
import { logger } from '../../logger.ts';
|
|
4
|
+
import type { SearchResult } from '../../web-research/types.ts';
|
|
5
|
+
import { isCloudflareBlockError, isPoolShutdownError } from './browser-error-utils.ts';
|
|
6
|
+
|
|
7
|
+
export interface BrowserServerOptions {
|
|
8
|
+
onSearch: (query: string) => Promise<SearchResult[]>;
|
|
9
|
+
onScrape: (url: string) => Promise<any>;
|
|
10
|
+
onHealthCheck: () => Promise<{ success: boolean }>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* FIX (#21): Shared secret for authenticating local HTTP requests.
|
|
15
|
+
* Generated once per server instance; clients must present it in the
|
|
16
|
+
* `X-Browser-Auth` header. This prevents unauthorized local processes
|
|
17
|
+
* from issuing search/scrape requests through the browser pool.
|
|
18
|
+
*/
|
|
19
|
+
let _authSecret: string | null = null;
|
|
20
|
+
|
|
21
|
+
function getOrCreateAuthSecret(): string {
|
|
22
|
+
if (!_authSecret) {
|
|
23
|
+
_authSecret = crypto.randomBytes(32).toString('hex');
|
|
24
|
+
}
|
|
25
|
+
return _authSecret;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function getBrowserServerAuthSecret(): string {
|
|
29
|
+
return getOrCreateAuthSecret();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export class BrowserServer {
|
|
33
|
+
private server: http.Server | null = null;
|
|
34
|
+
private port: number = 0;
|
|
35
|
+
|
|
36
|
+
constructor(private options: BrowserServerOptions) {}
|
|
37
|
+
|
|
38
|
+
async start(): Promise<number> {
|
|
39
|
+
return new Promise((resolve, reject) => {
|
|
40
|
+
this.server = http.createServer(async (req, res) => {
|
|
41
|
+
// FIX (#21): Validate auth token on every request
|
|
42
|
+
const rawAuth = req.headers['x-browser-auth'];
|
|
43
|
+
const authHeader = Array.isArray(rawAuth) ? rawAuth[0] : rawAuth;
|
|
44
|
+
const expected = Buffer.from(getOrCreateAuthSecret(), 'utf8');
|
|
45
|
+
const actual = Buffer.from(authHeader ?? '', 'utf8');
|
|
46
|
+
if (actual.length !== expected.length || !crypto.timingSafeEqual(actual, expected)) {
|
|
47
|
+
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
48
|
+
res.end(JSON.stringify({ error: 'Unauthorized' }));
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (req.method !== 'POST') {
|
|
53
|
+
res.writeHead(405);
|
|
54
|
+
res.end('Method Not Allowed');
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
let body = '';
|
|
59
|
+
let oversized = false;
|
|
60
|
+
const MAX_BODY_SIZE = 10 * 1024 * 1024; // 10MB limit
|
|
61
|
+
|
|
62
|
+
req.on('data', chunk => {
|
|
63
|
+
body += chunk;
|
|
64
|
+
if (body.length > MAX_BODY_SIZE) {
|
|
65
|
+
oversized = true;
|
|
66
|
+
req.destroy(new Error('Payload too large'));
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
req.on('error', (err) => {
|
|
71
|
+
logger.error('[BrowserServer] Request stream error:', err);
|
|
72
|
+
// req.destroy() on oversize emits 'error', not 'end', so no response
|
|
73
|
+
// would otherwise be sent. Reply 413 best-effort before the socket
|
|
74
|
+
// tears down so the client gets a clear status, not a bare reset.
|
|
75
|
+
if (oversized && !res.headersSent) {
|
|
76
|
+
try {
|
|
77
|
+
res.writeHead(413, { 'Content-Type': 'application/json' });
|
|
78
|
+
res.end(JSON.stringify({ error: 'Payload too large' }));
|
|
79
|
+
} catch { /* socket may already be gone */ }
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
req.on('end', async () => {
|
|
84
|
+
try {
|
|
85
|
+
const data = JSON.parse(body);
|
|
86
|
+
let result: any;
|
|
87
|
+
|
|
88
|
+
switch (req.url) {
|
|
89
|
+
case '/search':
|
|
90
|
+
result = await this.options.onSearch(data.query);
|
|
91
|
+
break;
|
|
92
|
+
case '/scrape':
|
|
93
|
+
result = await this.options.onScrape(data.url);
|
|
94
|
+
break;
|
|
95
|
+
case '/healthcheck':
|
|
96
|
+
result = await this.options.onHealthCheck();
|
|
97
|
+
break;
|
|
98
|
+
default:
|
|
99
|
+
res.writeHead(404);
|
|
100
|
+
res.end('Not Found');
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
105
|
+
res.end(JSON.stringify(result));
|
|
106
|
+
} catch (error) {
|
|
107
|
+
// A Cloudflare/bot challenge is an EXPECTED scrape outcome, not a
|
|
108
|
+
// server fault — log it at WARN so it doesn't inflate ERROR counts.
|
|
109
|
+
// Everything else is a genuine request-handling error.
|
|
110
|
+
if (isCloudflareBlockError(error)) {
|
|
111
|
+
logger.warn('[BrowserServer] Request blocked by bot protection:', error instanceof Error ? error.message : String(error));
|
|
112
|
+
} else if (isPoolShutdownError(error)) {
|
|
113
|
+
// Pool destroyed mid-request during quit/SIGTERM teardown — expected,
|
|
114
|
+
// not a server fault. DEBUG so a normal shutdown doesn't emit ERRORs.
|
|
115
|
+
logger.debug('[BrowserServer] Request abandoned during shutdown:', error instanceof Error ? error.message : String(error));
|
|
116
|
+
} else {
|
|
117
|
+
logger.error('[BrowserServer] Error handling request:', error);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Extract safe error message - first line only to prevent stack trace exposure
|
|
121
|
+
let errorMessage = 'Unknown error';
|
|
122
|
+
if (error instanceof Error && error.message) {
|
|
123
|
+
errorMessage = (error.message.split('\n')[0] || '').trim();
|
|
124
|
+
} else if (typeof error === 'string') {
|
|
125
|
+
errorMessage = (error.split('\n')[0] || '').trim();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Headers may already be out (e.g. res.end() threw mid-response) —
|
|
129
|
+
// writeHead() would then throw ERR_HTTP_HEADERS_SENT inside this async
|
|
130
|
+
// handler and surface as an unhandledRejection in the leader process.
|
|
131
|
+
if (!res.headersSent) {
|
|
132
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
133
|
+
res.end(JSON.stringify({ error: errorMessage }));
|
|
134
|
+
} else {
|
|
135
|
+
res.destroy();
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
this.server.listen(0, '127.0.0.1', () => {
|
|
142
|
+
const addr = this.server?.address();
|
|
143
|
+
if (addr && typeof addr === 'object') {
|
|
144
|
+
this.port = addr.port;
|
|
145
|
+
logger.log(`[BrowserServer] Listening on http://127.0.0.1:${this.port}`);
|
|
146
|
+
resolve(this.port);
|
|
147
|
+
} else {
|
|
148
|
+
reject(new Error('Failed to get server port'));
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
this.server.on('error', (err) => {
|
|
153
|
+
logger.error('[BrowserServer] Server error:', err);
|
|
154
|
+
reject(err);
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async stop(): Promise<void> {
|
|
160
|
+
if (this.server) {
|
|
161
|
+
this.server.closeAllConnections?.();
|
|
162
|
+
return new Promise((resolve) => {
|
|
163
|
+
this.server?.close(() => {
|
|
164
|
+
this.server = null;
|
|
165
|
+
resolve();
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
getPort(): number {
|
|
172
|
+
return this.port;
|
|
173
|
+
}
|
|
174
|
+
}
|