s3db.js 13.6.0 → 14.0.2

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.
Files changed (193) hide show
  1. package/README.md +139 -43
  2. package/dist/s3db.cjs +72425 -38970
  3. package/dist/s3db.cjs.map +1 -1
  4. package/dist/s3db.es.js +72177 -38764
  5. package/dist/s3db.es.js.map +1 -1
  6. package/mcp/lib/base-handler.js +157 -0
  7. package/mcp/lib/handlers/connection-handler.js +280 -0
  8. package/mcp/lib/handlers/query-handler.js +533 -0
  9. package/mcp/lib/handlers/resource-handler.js +428 -0
  10. package/mcp/lib/tool-registry.js +336 -0
  11. package/mcp/lib/tools/connection-tools.js +161 -0
  12. package/mcp/lib/tools/query-tools.js +267 -0
  13. package/mcp/lib/tools/resource-tools.js +404 -0
  14. package/package.json +94 -49
  15. package/src/clients/memory-client.class.js +346 -191
  16. package/src/clients/memory-storage.class.js +300 -84
  17. package/src/clients/s3-client.class.js +7 -6
  18. package/src/concerns/geo-encoding.js +19 -2
  19. package/src/concerns/ip.js +59 -9
  20. package/src/concerns/money.js +8 -1
  21. package/src/concerns/password-hashing.js +49 -8
  22. package/src/concerns/plugin-storage.js +186 -18
  23. package/src/concerns/storage-drivers/filesystem-driver.js +284 -0
  24. package/src/database.class.js +139 -29
  25. package/src/errors.js +332 -42
  26. package/src/plugins/api/auth/oidc-auth.js +66 -17
  27. package/src/plugins/api/auth/strategies/base-strategy.class.js +74 -0
  28. package/src/plugins/api/auth/strategies/factory.class.js +63 -0
  29. package/src/plugins/api/auth/strategies/global-strategy.class.js +44 -0
  30. package/src/plugins/api/auth/strategies/path-based-strategy.class.js +83 -0
  31. package/src/plugins/api/auth/strategies/path-rules-strategy.class.js +118 -0
  32. package/src/plugins/api/concerns/failban-manager.js +106 -57
  33. package/src/plugins/api/concerns/opengraph-helper.js +116 -0
  34. package/src/plugins/api/concerns/route-context.js +601 -0
  35. package/src/plugins/api/concerns/state-machine.js +288 -0
  36. package/src/plugins/api/index.js +180 -41
  37. package/src/plugins/api/routes/auth-routes.js +198 -30
  38. package/src/plugins/api/routes/resource-routes.js +19 -4
  39. package/src/plugins/api/server/health-manager.class.js +163 -0
  40. package/src/plugins/api/server/middleware-chain.class.js +310 -0
  41. package/src/plugins/api/server/router.class.js +472 -0
  42. package/src/plugins/api/server.js +280 -1303
  43. package/src/plugins/api/utils/custom-routes.js +17 -5
  44. package/src/plugins/api/utils/guards.js +76 -17
  45. package/src/plugins/api/utils/openapi-generator-cached.class.js +133 -0
  46. package/src/plugins/api/utils/openapi-generator.js +7 -6
  47. package/src/plugins/api/utils/template-engine.js +77 -3
  48. package/src/plugins/audit.plugin.js +30 -8
  49. package/src/plugins/backup.plugin.js +110 -14
  50. package/src/plugins/cache/cache.class.js +22 -5
  51. package/src/plugins/cache/filesystem-cache.class.js +116 -19
  52. package/src/plugins/cache/memory-cache.class.js +211 -57
  53. package/src/plugins/cache/multi-tier-cache.class.js +371 -0
  54. package/src/plugins/cache/partition-aware-filesystem-cache.class.js +168 -47
  55. package/src/plugins/cache/redis-cache.class.js +552 -0
  56. package/src/plugins/cache/s3-cache.class.js +17 -8
  57. package/src/plugins/cache.plugin.js +176 -61
  58. package/src/plugins/cloud-inventory/drivers/alibaba-driver.js +8 -1
  59. package/src/plugins/cloud-inventory/drivers/aws-driver.js +60 -29
  60. package/src/plugins/cloud-inventory/drivers/azure-driver.js +8 -1
  61. package/src/plugins/cloud-inventory/drivers/base-driver.js +16 -2
  62. package/src/plugins/cloud-inventory/drivers/cloudflare-driver.js +8 -1
  63. package/src/plugins/cloud-inventory/drivers/digitalocean-driver.js +8 -1
  64. package/src/plugins/cloud-inventory/drivers/hetzner-driver.js +8 -1
  65. package/src/plugins/cloud-inventory/drivers/linode-driver.js +8 -1
  66. package/src/plugins/cloud-inventory/drivers/mongodb-atlas-driver.js +8 -1
  67. package/src/plugins/cloud-inventory/drivers/vultr-driver.js +8 -1
  68. package/src/plugins/cloud-inventory/index.js +29 -8
  69. package/src/plugins/cloud-inventory/registry.js +64 -42
  70. package/src/plugins/cloud-inventory.plugin.js +240 -138
  71. package/src/plugins/concerns/plugin-dependencies.js +54 -0
  72. package/src/plugins/concerns/resource-names.js +100 -0
  73. package/src/plugins/consumers/index.js +10 -2
  74. package/src/plugins/consumers/sqs-consumer.js +12 -2
  75. package/src/plugins/cookie-farm-suite.plugin.js +278 -0
  76. package/src/plugins/cookie-farm.errors.js +73 -0
  77. package/src/plugins/cookie-farm.plugin.js +869 -0
  78. package/src/plugins/costs.plugin.js +7 -1
  79. package/src/plugins/eventual-consistency/analytics.js +94 -19
  80. package/src/plugins/eventual-consistency/config.js +15 -7
  81. package/src/plugins/eventual-consistency/consolidation.js +29 -11
  82. package/src/plugins/eventual-consistency/garbage-collection.js +3 -1
  83. package/src/plugins/eventual-consistency/helpers.js +39 -14
  84. package/src/plugins/eventual-consistency/install.js +21 -2
  85. package/src/plugins/eventual-consistency/utils.js +32 -10
  86. package/src/plugins/fulltext.plugin.js +38 -11
  87. package/src/plugins/geo.plugin.js +61 -9
  88. package/src/plugins/identity/concerns/config.js +61 -0
  89. package/src/plugins/identity/concerns/mfa-manager.js +15 -2
  90. package/src/plugins/identity/concerns/rate-limit.js +124 -0
  91. package/src/plugins/identity/concerns/resource-schemas.js +9 -1
  92. package/src/plugins/identity/concerns/token-generator.js +29 -4
  93. package/src/plugins/identity/drivers/auth-driver.interface.js +76 -0
  94. package/src/plugins/identity/drivers/client-credentials-driver.js +127 -0
  95. package/src/plugins/identity/drivers/index.js +18 -0
  96. package/src/plugins/identity/drivers/password-driver.js +122 -0
  97. package/src/plugins/identity/email-service.js +17 -2
  98. package/src/plugins/identity/index.js +413 -69
  99. package/src/plugins/identity/oauth2-server.js +413 -30
  100. package/src/plugins/identity/oidc-discovery.js +16 -8
  101. package/src/plugins/identity/rsa-keys.js +115 -35
  102. package/src/plugins/identity/server.js +166 -45
  103. package/src/plugins/identity/session-manager.js +53 -7
  104. package/src/plugins/identity/ui/pages/mfa-verification.js +17 -15
  105. package/src/plugins/identity/ui/routes.js +363 -255
  106. package/src/plugins/importer/index.js +153 -20
  107. package/src/plugins/index.js +9 -2
  108. package/src/plugins/kubernetes-inventory/index.js +6 -0
  109. package/src/plugins/kubernetes-inventory/k8s-driver.js +867 -0
  110. package/src/plugins/kubernetes-inventory/resource-types.js +274 -0
  111. package/src/plugins/kubernetes-inventory.plugin.js +980 -0
  112. package/src/plugins/metrics.plugin.js +64 -16
  113. package/src/plugins/ml/base-model.class.js +25 -15
  114. package/src/plugins/ml/regression-model.class.js +1 -1
  115. package/src/plugins/ml.errors.js +57 -25
  116. package/src/plugins/ml.plugin.js +28 -4
  117. package/src/plugins/namespace.js +210 -0
  118. package/src/plugins/plugin.class.js +180 -8
  119. package/src/plugins/puppeteer/console-monitor.js +729 -0
  120. package/src/plugins/puppeteer/cookie-manager.js +492 -0
  121. package/src/plugins/puppeteer/network-monitor.js +816 -0
  122. package/src/plugins/puppeteer/performance-manager.js +746 -0
  123. package/src/plugins/puppeteer/proxy-manager.js +478 -0
  124. package/src/plugins/puppeteer/stealth-manager.js +556 -0
  125. package/src/plugins/puppeteer.errors.js +81 -0
  126. package/src/plugins/puppeteer.plugin.js +1327 -0
  127. package/src/plugins/queue-consumer.plugin.js +69 -14
  128. package/src/plugins/recon/behaviors/uptime-behavior.js +691 -0
  129. package/src/plugins/recon/concerns/command-runner.js +148 -0
  130. package/src/plugins/recon/concerns/diff-detector.js +372 -0
  131. package/src/plugins/recon/concerns/fingerprint-builder.js +307 -0
  132. package/src/plugins/recon/concerns/process-manager.js +338 -0
  133. package/src/plugins/recon/concerns/report-generator.js +478 -0
  134. package/src/plugins/recon/concerns/security-analyzer.js +571 -0
  135. package/src/plugins/recon/concerns/target-normalizer.js +68 -0
  136. package/src/plugins/recon/config/defaults.js +321 -0
  137. package/src/plugins/recon/config/resources.js +370 -0
  138. package/src/plugins/recon/index.js +778 -0
  139. package/src/plugins/recon/managers/dependency-manager.js +174 -0
  140. package/src/plugins/recon/managers/scheduler-manager.js +179 -0
  141. package/src/plugins/recon/managers/storage-manager.js +745 -0
  142. package/src/plugins/recon/managers/target-manager.js +274 -0
  143. package/src/plugins/recon/stages/asn-stage.js +314 -0
  144. package/src/plugins/recon/stages/certificate-stage.js +84 -0
  145. package/src/plugins/recon/stages/dns-stage.js +107 -0
  146. package/src/plugins/recon/stages/dnsdumpster-stage.js +362 -0
  147. package/src/plugins/recon/stages/fingerprint-stage.js +71 -0
  148. package/src/plugins/recon/stages/google-dorks-stage.js +440 -0
  149. package/src/plugins/recon/stages/http-stage.js +89 -0
  150. package/src/plugins/recon/stages/latency-stage.js +148 -0
  151. package/src/plugins/recon/stages/massdns-stage.js +302 -0
  152. package/src/plugins/recon/stages/osint-stage.js +1373 -0
  153. package/src/plugins/recon/stages/ports-stage.js +169 -0
  154. package/src/plugins/recon/stages/screenshot-stage.js +94 -0
  155. package/src/plugins/recon/stages/secrets-stage.js +514 -0
  156. package/src/plugins/recon/stages/subdomains-stage.js +295 -0
  157. package/src/plugins/recon/stages/tls-audit-stage.js +78 -0
  158. package/src/plugins/recon/stages/vulnerability-stage.js +78 -0
  159. package/src/plugins/recon/stages/web-discovery-stage.js +113 -0
  160. package/src/plugins/recon/stages/whois-stage.js +349 -0
  161. package/src/plugins/recon.plugin.js +75 -0
  162. package/src/plugins/recon.plugin.js.backup +2635 -0
  163. package/src/plugins/relation.errors.js +87 -14
  164. package/src/plugins/replicator.plugin.js +514 -137
  165. package/src/plugins/replicators/base-replicator.class.js +89 -1
  166. package/src/plugins/replicators/bigquery-replicator.class.js +66 -22
  167. package/src/plugins/replicators/dynamodb-replicator.class.js +22 -15
  168. package/src/plugins/replicators/mongodb-replicator.class.js +22 -15
  169. package/src/plugins/replicators/mysql-replicator.class.js +52 -17
  170. package/src/plugins/replicators/planetscale-replicator.class.js +30 -4
  171. package/src/plugins/replicators/postgres-replicator.class.js +62 -27
  172. package/src/plugins/replicators/s3db-replicator.class.js +25 -18
  173. package/src/plugins/replicators/schema-sync.helper.js +3 -3
  174. package/src/plugins/replicators/sqs-replicator.class.js +8 -2
  175. package/src/plugins/replicators/turso-replicator.class.js +23 -3
  176. package/src/plugins/replicators/webhook-replicator.class.js +42 -4
  177. package/src/plugins/s3-queue.plugin.js +464 -65
  178. package/src/plugins/scheduler.plugin.js +20 -6
  179. package/src/plugins/state-machine.plugin.js +40 -9
  180. package/src/plugins/tfstate/README.md +126 -126
  181. package/src/plugins/tfstate/base-driver.js +28 -4
  182. package/src/plugins/tfstate/errors.js +65 -10
  183. package/src/plugins/tfstate/filesystem-driver.js +52 -8
  184. package/src/plugins/tfstate/index.js +163 -90
  185. package/src/plugins/tfstate/s3-driver.js +64 -6
  186. package/src/plugins/ttl.plugin.js +72 -17
  187. package/src/plugins/vector/distances.js +18 -12
  188. package/src/plugins/vector/kmeans.js +26 -4
  189. package/src/resource.class.js +115 -19
  190. package/src/testing/factory.class.js +20 -3
  191. package/src/testing/seeder.class.js +7 -1
  192. package/src/clients/memory-client.md +0 -917
  193. package/src/plugins/cloud-inventory/drivers/mock-drivers.js +0 -449
@@ -0,0 +1,1327 @@
1
+ import { Plugin } from './plugin.class.js';
2
+ import { requirePluginDependency } from './concerns/plugin-dependencies.js';
3
+ import { resolveResourceNames } from './concerns/resource-names.js';
4
+ import { getValidatedNamespace } from './namespace.js';
5
+ import tryFn from '../concerns/try-fn.js';
6
+ import { PluginError } from '../errors.js';
7
+
8
+ /**
9
+ * PuppeteerPlugin - Headless browser automation with anti-bot detection
10
+ *
11
+ * Features:
12
+ * - Browser pool management with tab recycling
13
+ * - Cookie farming and session management
14
+ * - Human behavior simulation (ghost-cursor)
15
+ * - Anti-detection (puppeteer-extra-plugin-stealth)
16
+ * - Random user agent generation
17
+ * - Performance optimization (resource blocking)
18
+ * - Proxy support
19
+ *
20
+ * @extends Plugin
21
+ */
22
+ export class PuppeteerPlugin extends Plugin {
23
+ constructor(options = {}) {
24
+ super(options);
25
+
26
+ // Validate and set namespace (standardized)
27
+ this.namespace = getValidatedNamespace(options, '');
28
+
29
+ // Default configuration
30
+ this.config = {
31
+ // Browser Pool
32
+ pool: {
33
+ enabled: true,
34
+ maxBrowsers: 5,
35
+ maxTabsPerBrowser: 10,
36
+ reuseTab: false,
37
+ closeOnIdle: true,
38
+ idleTimeout: 300000, // 5 minutes
39
+ ...options.pool
40
+ },
41
+
42
+ // Browser Launch Options
43
+ launch: {
44
+ headless: true,
45
+ args: [
46
+ '--no-sandbox',
47
+ '--disable-setuid-sandbox',
48
+ '--disable-dev-shm-usage',
49
+ '--disable-accelerated-2d-canvas',
50
+ '--no-first-run',
51
+ '--no-zygote',
52
+ '--disable-gpu'
53
+ ],
54
+ ignoreHTTPSErrors: true,
55
+ ...options.launch
56
+ },
57
+
58
+ // Viewport & User Agent
59
+ viewport: {
60
+ width: 1920,
61
+ height: 1080,
62
+ deviceScaleFactor: 1,
63
+ randomize: true,
64
+ presets: ['desktop', 'laptop', 'tablet'],
65
+ ...options.viewport
66
+ },
67
+
68
+ // User Agent Management
69
+ userAgent: {
70
+ enabled: true,
71
+ random: true,
72
+ filters: {
73
+ deviceCategory: 'desktop',
74
+ ...options.userAgent?.filters
75
+ },
76
+ custom: options.userAgent?.custom || null,
77
+ ...options.userAgent
78
+ },
79
+
80
+ // Stealth Mode (Anti-Detection)
81
+ stealth: {
82
+ enabled: true,
83
+ enableEvasions: true,
84
+ ...options.stealth
85
+ },
86
+
87
+ // Human Behavior Simulation
88
+ humanBehavior: {
89
+ enabled: true,
90
+ mouse: {
91
+ enabled: true,
92
+ bezierCurves: true,
93
+ overshoot: true,
94
+ jitter: true,
95
+ pathThroughElements: true,
96
+ ...options.humanBehavior?.mouse
97
+ },
98
+ typing: {
99
+ enabled: true,
100
+ mistakes: true,
101
+ corrections: true,
102
+ pauseAfterWord: true,
103
+ speedVariation: true,
104
+ delayRange: [50, 150],
105
+ ...options.humanBehavior?.typing
106
+ },
107
+ scrolling: {
108
+ enabled: true,
109
+ randomStops: true,
110
+ backScroll: true,
111
+ horizontalJitter: true,
112
+ ...options.humanBehavior?.scrolling
113
+ },
114
+ ...options.humanBehavior
115
+ },
116
+
117
+ // Cookie Management & Farming
118
+ cookies: {
119
+ enabled: true,
120
+ storage: {
121
+ resource: 'plg_puppeteer_cookies',
122
+ autoSave: true,
123
+ autoLoad: true,
124
+ encrypt: true,
125
+ ...options.cookies?.storage
126
+ },
127
+ farming: {
128
+ enabled: true,
129
+ warmup: {
130
+ enabled: true,
131
+ pages: ['https://www.google.com', 'https://www.youtube.com', 'https://www.wikipedia.org'],
132
+ randomOrder: true,
133
+ timePerPage: { min: 5000, max: 15000 },
134
+ interactions: { scroll: true, click: true, hover: true },
135
+ ...options.cookies?.farming?.warmup
136
+ },
137
+ rotation: {
138
+ enabled: true,
139
+ requestsPerCookie: 100,
140
+ maxAge: 86400000, // 24 hours
141
+ poolSize: 10,
142
+ ...options.cookies?.farming?.rotation
143
+ },
144
+ reputation: {
145
+ enabled: true,
146
+ trackSuccess: true,
147
+ retireThreshold: 0.5,
148
+ ageBoost: true,
149
+ ...options.cookies?.farming?.reputation
150
+ },
151
+ ...options.cookies?.farming
152
+ },
153
+ ...options.cookies
154
+ },
155
+
156
+ // Performance Optimization
157
+ performance: {
158
+ blockResources: {
159
+ enabled: true,
160
+ types: ['image', 'stylesheet', 'font', 'media'],
161
+ ...options.performance?.blockResources
162
+ },
163
+ cacheEnabled: true,
164
+ javascriptEnabled: true,
165
+ ...options.performance
166
+ },
167
+
168
+ // Network Monitoring (CDP)
169
+ networkMonitor: {
170
+ enabled: false, // Disabled by default (adds overhead)
171
+ persist: false, // Save to S3DB
172
+ filters: {
173
+ types: null, // ['image', 'script'] or null for all
174
+ statuses: null, // [404, 500] or null for all
175
+ minSize: null, // Only requests >= size (bytes)
176
+ maxSize: null, // Only requests <= size (bytes)
177
+ saveErrors: true, // Always save failed requests
178
+ saveLargeAssets: true, // Always save assets > 1MB
179
+ ...options.networkMonitor?.filters
180
+ },
181
+ compression: {
182
+ enabled: true,
183
+ threshold: 10240, // Compress payloads > 10KB
184
+ ...options.networkMonitor?.compression
185
+ },
186
+ ...options.networkMonitor
187
+ },
188
+
189
+ // Console Monitoring
190
+ consoleMonitor: {
191
+ enabled: false, // Disabled by default
192
+ persist: false, // Save to S3DB
193
+ filters: {
194
+ levels: null, // ['error', 'warning'] or null for all
195
+ excludePatterns: [], // Regex patterns to exclude
196
+ includeStackTraces: true,
197
+ includeSourceLocation: true,
198
+ captureNetwork: false, // Also capture network errors
199
+ ...options.consoleMonitor?.filters
200
+ },
201
+ ...options.consoleMonitor
202
+ },
203
+
204
+ // Screenshot & Recording
205
+ screenshot: {
206
+ fullPage: false,
207
+ type: 'png',
208
+ ...options.screenshot
209
+ },
210
+
211
+ // Proxy Support
212
+ proxy: {
213
+ enabled: false,
214
+ list: [], // Array of proxy URLs or objects
215
+ selectionStrategy: 'round-robin', // 'round-robin' | 'random' | 'least-used' | 'best-performance'
216
+ bypassList: [], // Domains to bypass proxy
217
+ healthCheck: {
218
+ enabled: true,
219
+ interval: 300000, // 5 minutes
220
+ testUrl: 'https://www.google.com',
221
+ timeout: 10000,
222
+ successRateThreshold: 0.3
223
+ },
224
+ // Legacy single proxy support (deprecated)
225
+ server: null,
226
+ username: null,
227
+ password: null,
228
+ ...options.proxy
229
+ },
230
+
231
+ // Error Handling & Retries
232
+ retries: {
233
+ enabled: true,
234
+ maxAttempts: 3,
235
+ backoff: 'exponential',
236
+ initialDelay: 1000,
237
+ ...options.retries
238
+ },
239
+
240
+ // Logging & Debugging
241
+ debug: {
242
+ enabled: false,
243
+ screenshots: false,
244
+ console: false,
245
+ network: false,
246
+ ...options.debug
247
+ }
248
+ };
249
+
250
+ const resourceNamesOption = options.resourceNames || {};
251
+ this._resourceDescriptors = {
252
+ cookies: {
253
+ defaultName: 'plg_puppeteer_cookies',
254
+ override: resourceNamesOption.cookies || options.cookies?.storage?.resource
255
+ },
256
+ consoleSessions: {
257
+ defaultName: 'plg_puppeteer_console_sessions',
258
+ override: resourceNamesOption.consoleSessions
259
+ },
260
+ consoleMessages: {
261
+ defaultName: 'plg_puppeteer_console_messages',
262
+ override: resourceNamesOption.consoleMessages
263
+ },
264
+ consoleErrors: {
265
+ defaultName: 'plg_puppeteer_console_errors',
266
+ override: resourceNamesOption.consoleErrors
267
+ },
268
+ networkSessions: {
269
+ defaultName: 'plg_puppeteer_network_sessions',
270
+ override: resourceNamesOption.networkSessions
271
+ },
272
+ networkRequests: {
273
+ defaultName: 'plg_puppeteer_network_requests',
274
+ override: resourceNamesOption.networkRequests
275
+ },
276
+ networkErrors: {
277
+ defaultName: 'plg_puppeteer_network_errors',
278
+ override: resourceNamesOption.networkErrors
279
+ }
280
+ };
281
+ this.resourceNames = this._resolveResourceNames();
282
+
283
+ this.config.cookies.storage.resource = this.resourceNames.cookies;
284
+
285
+ // Internal state
286
+ this.browserPool = [];
287
+ this.tabPool = new Map(); // Browser instance -> Set<Page>
288
+ this.browserIdleTimers = new Map(); // Browser instance -> timeout id
289
+ this.dedicatedBrowsers = new Set(); // Non-pooled browsers for cleanup
290
+ this.userAgentGenerator = null;
291
+ this.ghostCursor = null;
292
+ this.cookieManager = null;
293
+ this.proxyManager = null;
294
+ this.performanceManager = null;
295
+ this.networkMonitor = null;
296
+ this.consoleMonitor = null;
297
+ this.initialized = false;
298
+
299
+ if (this.config.pool.reuseTab) {
300
+ this.emit('puppeteer.configWarning', {
301
+ setting: 'pool.reuseTab',
302
+ message: 'pool.reuseTab is not supported yet and will be ignored.'
303
+ });
304
+ }
305
+ }
306
+
307
+ _resolveResourceNames() {
308
+ return resolveResourceNames('puppeteer', this._resourceDescriptors, {
309
+ namespace: this.namespace
310
+ });
311
+ }
312
+
313
+ onNamespaceChanged() {
314
+ this.resourceNames = this._resolveResourceNames();
315
+ if (this.config?.cookies?.storage) {
316
+ this.config.cookies.storage.resource = this.resourceNames.cookies;
317
+ }
318
+ }
319
+
320
+ /**
321
+ * Install plugin and validate dependencies
322
+ */
323
+ async onInstall() {
324
+ // Validate required dependencies
325
+ requirePluginDependency('puppeteer', this.name);
326
+ requirePluginDependency('puppeteer-extra', this.name);
327
+ requirePluginDependency('puppeteer-extra-plugin-stealth', this.name);
328
+ requirePluginDependency('user-agents', this.name);
329
+ requirePluginDependency('ghost-cursor', this.name);
330
+
331
+ // Create cookie storage resource if enabled
332
+ if (this.config.cookies.enabled) {
333
+ await this._setupCookieStorage();
334
+ }
335
+
336
+ this.emit('puppeteer.installed');
337
+ }
338
+
339
+ /**
340
+ * Start plugin and initialize browser pool
341
+ */
342
+ async onStart() {
343
+ if (this.initialized) return;
344
+
345
+ // Import dependencies
346
+ await this._importDependencies();
347
+
348
+ // Initialize cookie manager
349
+ if (this.config.cookies.enabled) {
350
+ await this._initializeCookieManager();
351
+ }
352
+
353
+ // Initialize proxy manager (requires cookie storage for binding restore)
354
+ if (this.config.proxy.enabled) {
355
+ await this._initializeProxyManager();
356
+ }
357
+
358
+ // Initialize performance manager
359
+ await this._initializePerformanceManager();
360
+
361
+ // Initialize network monitor
362
+ if (this.config.networkMonitor.enabled) {
363
+ await this._initializeNetworkMonitor();
364
+ }
365
+
366
+ // Initialize console monitor
367
+ if (this.config.consoleMonitor.enabled) {
368
+ await this._initializeConsoleMonitor();
369
+ }
370
+
371
+ // Pre-warm browser pool if enabled
372
+ if (this.config.pool.enabled) {
373
+ await this._warmupBrowserPool();
374
+ }
375
+
376
+ this.initialized = true;
377
+ this.emit('puppeteer.started');
378
+ }
379
+
380
+ /**
381
+ * Stop plugin and cleanup resources
382
+ */
383
+ async onStop() {
384
+ await this._closeBrowserPool();
385
+ await this._closeDedicatedBrowsers();
386
+ this.initialized = false;
387
+ this.emit('puppeteer.stopped');
388
+ }
389
+
390
+ /**
391
+ * Uninstall plugin
392
+ */
393
+ async onUninstall(options = {}) {
394
+ await this.onStop();
395
+ this.emit('puppeteer.uninstalled');
396
+ }
397
+
398
+ /**
399
+ * Import required dependencies (lazy loading)
400
+ * @private
401
+ */
402
+ async _importDependencies() {
403
+ const puppeteerModule = await import('puppeteer-extra');
404
+ const StealthPlugin = (await import('puppeteer-extra-plugin-stealth')).default;
405
+ const UserAgent = (await import('user-agents')).default;
406
+ const { createCursor } = await import('ghost-cursor');
407
+
408
+ // Setup puppeteer with stealth plugin
409
+ this.puppeteer = puppeteerModule.default || puppeteerModule;
410
+
411
+ if (this.config.stealth.enabled) {
412
+ this.puppeteer.use(StealthPlugin());
413
+ }
414
+
415
+ // Setup user agent generator
416
+ if (this.config.userAgent.enabled && this.config.userAgent.random) {
417
+ this.UserAgent = UserAgent;
418
+ }
419
+
420
+ // Store ghost-cursor factory
421
+ this.createGhostCursor = createCursor;
422
+ }
423
+
424
+ /**
425
+ * Setup cookie storage resource
426
+ * @private
427
+ */
428
+ async _setupCookieStorage() {
429
+ const resourceName = this.config.cookies.storage.resource;
430
+
431
+ try {
432
+ await this.database.getResource(resourceName);
433
+ return;
434
+ } catch (err) {
435
+ // Resource missing, will create below
436
+ }
437
+
438
+ const [created, createErr] = await tryFn(() => this.database.createResource({
439
+ name: resourceName,
440
+ attributes: {
441
+ sessionId: 'string|required',
442
+ cookies: 'array|required',
443
+ userAgent: 'string',
444
+ viewport: 'object',
445
+ proxyId: 'string|optional',
446
+ domain: 'string',
447
+ date: 'string',
448
+ reputation: {
449
+ successCount: 'number',
450
+ failCount: 'number',
451
+ successRate: 'number',
452
+ lastUsed: 'number'
453
+ },
454
+ metadata: {
455
+ createdAt: 'number',
456
+ expiresAt: 'number',
457
+ requestCount: 'number',
458
+ age: 'number'
459
+ }
460
+ },
461
+ timestamps: true,
462
+ behavior: 'body-only',
463
+ partitions: {
464
+ byProxy: { fields: { proxyId: 'string' } },
465
+ byDate: { fields: { date: 'string' } },
466
+ byDomain: { fields: { domain: 'string' } }
467
+ }
468
+ }));
469
+
470
+ if (!created) {
471
+ const existing = this.database.resources?.[resourceName];
472
+ if (!existing) {
473
+ throw createErr;
474
+ }
475
+ }
476
+ }
477
+
478
+ /**
479
+ * Initialize proxy manager
480
+ * @private
481
+ */
482
+ async _initializeProxyManager() {
483
+ const { ProxyManager } = await import('./puppeteer/proxy-manager.js');
484
+ this.proxyManager = new ProxyManager(this);
485
+ await this.proxyManager.initialize();
486
+ }
487
+
488
+ /**
489
+ * Initialize cookie manager
490
+ * @private
491
+ */
492
+ async _initializeCookieManager() {
493
+ const { CookieManager } = await import('./puppeteer/cookie-manager.js');
494
+ this.cookieManager = new CookieManager(this);
495
+ await this.cookieManager.initialize();
496
+ }
497
+
498
+ /**
499
+ * Initialize performance manager
500
+ * @private
501
+ */
502
+ async _initializePerformanceManager() {
503
+ const { PerformanceManager } = await import('./puppeteer/performance-manager.js');
504
+ this.performanceManager = new PerformanceManager(this);
505
+ this.emit('puppeteer.performanceManager.initialized');
506
+ }
507
+
508
+ /**
509
+ * Initialize network monitor
510
+ * @private
511
+ */
512
+ async _initializeNetworkMonitor() {
513
+ const { NetworkMonitor } = await import('./puppeteer/network-monitor.js');
514
+ this.networkMonitor = new NetworkMonitor(this);
515
+
516
+ // Initialize S3DB resources if persistence enabled
517
+ if (this.config.networkMonitor.persist) {
518
+ await this.networkMonitor.initialize();
519
+ }
520
+
521
+ this.emit('puppeteer.networkMonitor.initialized');
522
+ }
523
+
524
+ /**
525
+ * Initialize console monitor
526
+ * @private
527
+ */
528
+ async _initializeConsoleMonitor() {
529
+ const { ConsoleMonitor } = await import('./puppeteer/console-monitor.js');
530
+ this.consoleMonitor = new ConsoleMonitor(this);
531
+
532
+ // Initialize S3DB resources if persistence enabled
533
+ if (this.config.consoleMonitor.persist) {
534
+ await this.consoleMonitor.initialize();
535
+ }
536
+
537
+ this.emit('puppeteer.consoleMonitor.initialized');
538
+ }
539
+
540
+ /**
541
+ * Warmup browser pool
542
+ * @private
543
+ */
544
+ async _warmupBrowserPool() {
545
+ const poolSize = Math.min(this.config.pool.maxBrowsers, 2); // Start with 2 browsers
546
+
547
+ for (let i = 0; i < poolSize; i++) {
548
+ await this._createBrowser();
549
+ }
550
+
551
+ this.emit('puppeteer.poolWarmed', { size: this.browserPool.length });
552
+ }
553
+
554
+ /**
555
+ * Create a new browser instance
556
+ * @private
557
+ * @param {Object} proxy - Optional proxy configuration
558
+ * @returns {Promise<Browser>}
559
+ */
560
+ async _createBrowser(proxy = null) {
561
+ const launchOptions = {
562
+ ...this.config.launch,
563
+ args: [...(this.config.launch.args || [])]
564
+ };
565
+
566
+ // Add proxy args if provided
567
+ if (proxy && this.proxyManager) {
568
+ const proxyArgs = this.proxyManager.getProxyLaunchArgs(proxy);
569
+ launchOptions.args.push(...proxyArgs);
570
+ } else if (this.config.proxy.enabled && this.config.proxy.server) {
571
+ // Legacy single proxy support (deprecated)
572
+ launchOptions.args.push(`--proxy-server=${this.config.proxy.server}`);
573
+ }
574
+
575
+ const browser = await this.puppeteer.launch(launchOptions);
576
+
577
+ // Only add to pool if no specific proxy (shared browser)
578
+ if (!proxy && this.config.pool.enabled) {
579
+ this.browserPool.push(browser);
580
+ this.tabPool.set(browser, new Set());
581
+
582
+ browser.on('disconnected', () => {
583
+ const index = this.browserPool.indexOf(browser);
584
+ if (index > -1) {
585
+ this.browserPool.splice(index, 1);
586
+ }
587
+ this.tabPool.delete(browser);
588
+ this._clearIdleTimer(browser);
589
+ this.dedicatedBrowsers.delete(browser);
590
+ });
591
+ }
592
+ return browser;
593
+ }
594
+
595
+ /**
596
+ * Get or create a browser instance
597
+ * @private
598
+ * @param {Object} proxy - Optional proxy configuration
599
+ * @returns {Promise<Browser>}
600
+ */
601
+ async _getBrowser(proxy = null) {
602
+ // If proxy specified, create dedicated browser (not pooled)
603
+ if (proxy) {
604
+ return await this._createBrowser(proxy);
605
+ }
606
+ if (this.config.pool.enabled) {
607
+ // Find browser with available capacity
608
+ for (const browser of this.browserPool) {
609
+ const tabs = this.tabPool.get(browser);
610
+ if (!tabs || tabs.size < this.config.pool.maxTabsPerBrowser) {
611
+ return browser;
612
+ }
613
+ }
614
+
615
+ // Create new browser if pool not full
616
+ if (this.browserPool.length < this.config.pool.maxBrowsers) {
617
+ return await this._createBrowser();
618
+ }
619
+
620
+ // Use least loaded browser
621
+ let targetBrowser = this.browserPool[0];
622
+ let minTabs = this.tabPool.get(targetBrowser)?.size || 0;
623
+
624
+ for (const browser of this.browserPool.slice(1)) {
625
+ const tabs = this.tabPool.get(browser)?.size || 0;
626
+ if (tabs < minTabs) {
627
+ targetBrowser = browser;
628
+ minTabs = tabs;
629
+ }
630
+ }
631
+
632
+ return targetBrowser;
633
+ } else {
634
+ // No pooling - create new browser every time
635
+ return await this._createBrowser();
636
+ }
637
+ }
638
+
639
+ /**
640
+ * Close all browsers in pool
641
+ * @private
642
+ */
643
+ async _closeBrowserPool() {
644
+ for (const browser of this.browserPool) {
645
+ this._clearIdleTimer(browser);
646
+ if (this.cookieManager) {
647
+ const tabs = this.tabPool.get(browser);
648
+ if (tabs) {
649
+ for (const page of tabs) {
650
+ if (!page || page._sessionSaved || !page._sessionId) {
651
+ continue;
652
+ }
653
+
654
+ // Skip if page already closed
655
+ if (typeof page.isClosed === 'function' && page.isClosed()) {
656
+ continue;
657
+ }
658
+
659
+ try {
660
+ await this.cookieManager.saveSession(page, page._sessionId, {
661
+ success: !!page._navigationSuccess
662
+ });
663
+ page._sessionSaved = true;
664
+ } catch (err) {
665
+ page._sessionSaved = true;
666
+ this.emit('puppeteer.cookieSaveFailed', {
667
+ sessionId: page._sessionId,
668
+ error: err.message
669
+ });
670
+ }
671
+ }
672
+ }
673
+ }
674
+
675
+ try {
676
+ await browser.close();
677
+ } catch (err) {
678
+ // Ignore errors during cleanup
679
+ }
680
+ }
681
+ this.browserPool = [];
682
+ this.tabPool.clear();
683
+ }
684
+
685
+ /**
686
+ * Clear idle timer for pooled browser
687
+ * @private
688
+ */
689
+ _clearIdleTimer(browser) {
690
+ const timer = this.browserIdleTimers.get(browser);
691
+ if (timer) {
692
+ clearTimeout(timer);
693
+ this.browserIdleTimers.delete(browser);
694
+ }
695
+ }
696
+
697
+ /**
698
+ * Schedule pooled browser retirement when idle
699
+ * @private
700
+ */
701
+ _scheduleIdleCloseIfNeeded(browser) {
702
+ if (!this.config.pool.closeOnIdle) return;
703
+ const tabs = this.tabPool.get(browser);
704
+ if (!tabs || tabs.size > 0) return;
705
+ if (this.browserIdleTimers.has(browser)) return;
706
+
707
+ const timeout = this.config.pool.idleTimeout || 300000;
708
+ const timer = setTimeout(async () => {
709
+ this.browserIdleTimers.delete(browser);
710
+ const currentTabs = this.tabPool.get(browser);
711
+ if (currentTabs && currentTabs.size === 0) {
712
+ await this._retireIdleBrowser(browser);
713
+ }
714
+ }, timeout);
715
+
716
+ if (typeof timer.unref === 'function') {
717
+ timer.unref();
718
+ }
719
+
720
+ this.browserIdleTimers.set(browser, timer);
721
+ }
722
+
723
+ /**
724
+ * Retire pooled browser if still idle
725
+ * @private
726
+ * @param {Browser} browser
727
+ */
728
+ async _retireIdleBrowser(browser) {
729
+ this.tabPool.delete(browser);
730
+ const index = this.browserPool.indexOf(browser);
731
+ if (index > -1) {
732
+ this.browserPool.splice(index, 1);
733
+ }
734
+
735
+ try {
736
+ await browser.close();
737
+ this.emit('puppeteer.browserRetired', { pooled: true });
738
+ } catch (err) {
739
+ this.emit('puppeteer.browserRetiredError', {
740
+ pooled: true,
741
+ error: err.message
742
+ });
743
+ }
744
+ }
745
+
746
+ /**
747
+ * Close dedicated (non-pooled) browsers
748
+ * @private
749
+ */
750
+ async _closeDedicatedBrowsers() {
751
+ for (const browser of Array.from(this.dedicatedBrowsers)) {
752
+ try {
753
+ await browser.close();
754
+ } catch (err) {
755
+ // Ignore errors during cleanup
756
+ } finally {
757
+ this.dedicatedBrowsers.delete(browser);
758
+ }
759
+ }
760
+ }
761
+
762
+ /**
763
+ * Generate random user agent
764
+ * @private
765
+ * @returns {string}
766
+ */
767
+ _generateUserAgent() {
768
+ if (this.config.userAgent.custom) {
769
+ return this.config.userAgent.custom;
770
+ }
771
+
772
+ if (this.config.userAgent.random && this.UserAgent) {
773
+ const userAgent = new this.UserAgent(this.config.userAgent.filters);
774
+ return userAgent.toString();
775
+ }
776
+
777
+ return null;
778
+ }
779
+
780
+ /**
781
+ * Generate random viewport
782
+ * @private
783
+ * @returns {Object}
784
+ */
785
+ _generateViewport() {
786
+ if (!this.config.viewport.randomize) {
787
+ return {
788
+ width: this.config.viewport.width,
789
+ height: this.config.viewport.height,
790
+ deviceScaleFactor: this.config.viewport.deviceScaleFactor
791
+ };
792
+ }
793
+
794
+ // Predefined viewport presets
795
+ const presets = {
796
+ desktop: [
797
+ { width: 1920, height: 1080, deviceScaleFactor: 1 },
798
+ { width: 1680, height: 1050, deviceScaleFactor: 1 },
799
+ { width: 1600, height: 900, deviceScaleFactor: 1 },
800
+ { width: 1440, height: 900, deviceScaleFactor: 1 },
801
+ { width: 1366, height: 768, deviceScaleFactor: 1 }
802
+ ],
803
+ laptop: [
804
+ { width: 1440, height: 900, deviceScaleFactor: 1 },
805
+ { width: 1366, height: 768, deviceScaleFactor: 1 },
806
+ { width: 1280, height: 800, deviceScaleFactor: 1 }
807
+ ],
808
+ tablet: [
809
+ { width: 1024, height: 768, deviceScaleFactor: 2 },
810
+ { width: 768, height: 1024, deviceScaleFactor: 2 }
811
+ ]
812
+ };
813
+
814
+ // Select preset categories
815
+ const categories = this.config.viewport.presets || ['desktop'];
816
+ const availablePresets = categories.flatMap(cat => presets[cat] || []);
817
+
818
+ return availablePresets[Math.floor(Math.random() * availablePresets.length)];
819
+ }
820
+
821
+ /**
822
+ * PUBLIC API
823
+ */
824
+
825
+ /**
826
+ * Navigate to URL with human behavior
827
+ * @param {string} url - URL to navigate to
828
+ * @param {Object} options - Navigation options
829
+ * @returns {Promise<Page>}
830
+ */
831
+ async navigate(url, options = {}) {
832
+ const {
833
+ useSession = null,
834
+ screenshot = false,
835
+ waitUntil = 'networkidle2',
836
+ timeout = 30000
837
+ } = options;
838
+
839
+ // IMMUTABLE PROXY BINDING: Get proxy for session if proxy is enabled
840
+ let proxy = null;
841
+ let proxyId = null;
842
+
843
+ if (useSession && this.proxyManager) {
844
+ proxy = this.proxyManager.getProxyForSession(useSession, true);
845
+ proxyId = proxy?.id || null;
846
+ }
847
+
848
+ // Get browser (with proxy if needed)
849
+ const browser = await this._getBrowser(proxy);
850
+ const page = await browser.newPage();
851
+ const isPooledBrowser = !proxy && this.config.pool.enabled;
852
+
853
+ if (isPooledBrowser) {
854
+ const tabs = this.tabPool.get(browser);
855
+ if (tabs) {
856
+ tabs.add(page);
857
+ this._clearIdleTimer(browser);
858
+ }
859
+ } else {
860
+ this.dedicatedBrowsers.add(browser);
861
+ browser.once('disconnected', () => {
862
+ this.dedicatedBrowsers.delete(browser);
863
+ });
864
+ }
865
+
866
+ // Authenticate proxy if needed
867
+ if (proxy && this.proxyManager) {
868
+ await this.proxyManager.authenticateProxy(page, proxy);
869
+ }
870
+
871
+ // Setup viewport
872
+ const viewport = this._generateViewport();
873
+ await page.setViewport(viewport);
874
+
875
+ // Setup user agent
876
+ const userAgent = this._generateUserAgent();
877
+ if (userAgent) {
878
+ await page.setUserAgent(userAgent);
879
+ }
880
+
881
+ // Setup resource blocking for performance
882
+ if (this.config.performance.blockResources.enabled) {
883
+ await page.setRequestInterception(true);
884
+ page.on('request', (request) => {
885
+ if (this.config.performance.blockResources.types.includes(request.resourceType())) {
886
+ request.abort();
887
+ } else {
888
+ request.continue();
889
+ }
890
+ });
891
+ }
892
+
893
+ // Load cookies from session
894
+ if (useSession && this.cookieManager) {
895
+ await this.cookieManager.loadSession(page, useSession);
896
+ }
897
+
898
+ // Setup ghost cursor for human behavior
899
+ let cursor = null;
900
+ if (this.config.humanBehavior.enabled && this.config.humanBehavior.mouse.enabled) {
901
+ cursor = this.createGhostCursor(page);
902
+ }
903
+
904
+ // Navigate with error handling for proxy
905
+ let navigationSuccess = false;
906
+ try {
907
+ await page.goto(url, { waitUntil, timeout });
908
+ navigationSuccess = true;
909
+
910
+ // Record successful proxy usage
911
+ if (proxyId && this.proxyManager) {
912
+ this.proxyManager.recordProxyUsage(proxyId, true);
913
+ }
914
+ } catch (err) {
915
+ // Record failed proxy usage
916
+ if (proxyId && this.proxyManager) {
917
+ this.proxyManager.recordProxyUsage(proxyId, false);
918
+ }
919
+ throw err;
920
+ }
921
+
922
+ // Take screenshot if requested
923
+ if (screenshot) {
924
+ const screenshotBuffer = await page.screenshot(this.config.screenshot);
925
+ page._screenshot = screenshotBuffer;
926
+ }
927
+
928
+ // Attach helper methods to page
929
+ page._cursor = cursor;
930
+ page._userAgent = userAgent;
931
+ page._viewport = viewport;
932
+ page._proxyId = proxyId; // IMMUTABLE: Proxy binding stored on page
933
+ page._sessionId = useSession;
934
+ page._navigationSuccess = navigationSuccess;
935
+ page._sessionSaved = false;
936
+
937
+ // Add human behavior methods
938
+ if (this.config.humanBehavior.enabled) {
939
+ this._attachHumanBehaviorMethods(page);
940
+ }
941
+
942
+ let hasSavedSession = false;
943
+ let browserClosed = false;
944
+ const originalClose = page.close?.bind(page) || (async () => {});
945
+ const shouldAutoCloseBrowser = !isPooledBrowser;
946
+
947
+ page.on('close', () => {
948
+ if (isPooledBrowser) {
949
+ const tabs = this.tabPool.get(browser);
950
+ tabs?.delete(page);
951
+ this._scheduleIdleCloseIfNeeded(browser);
952
+ } else {
953
+ this.dedicatedBrowsers.delete(browser);
954
+ }
955
+ });
956
+
957
+ page.close = async (...closeArgs) => {
958
+ if (!hasSavedSession && useSession && this.cookieManager && !page._sessionSaved) {
959
+ try {
960
+ await this.cookieManager.saveSession(page, useSession, {
961
+ success: navigationSuccess
962
+ });
963
+ page._sessionSaved = true;
964
+ } catch (err) {
965
+ this.emit('puppeteer.cookieSaveFailed', {
966
+ sessionId: useSession,
967
+ error: err.message
968
+ });
969
+ page._sessionSaved = true;
970
+ } finally {
971
+ hasSavedSession = true;
972
+ }
973
+ }
974
+
975
+ try {
976
+ const result = await originalClose(...closeArgs);
977
+ return result;
978
+ } finally {
979
+ if (isPooledBrowser) {
980
+ const tabs = this.tabPool.get(browser);
981
+ tabs?.delete(page);
982
+ this._scheduleIdleCloseIfNeeded(browser);
983
+ } else if (shouldAutoCloseBrowser && !browserClosed) {
984
+ try {
985
+ await browser.close();
986
+ this.emit('puppeteer.browserClosed', { pooled: false });
987
+ } catch (err) {
988
+ this.emit('puppeteer.browserCloseFailed', {
989
+ pooled: false,
990
+ error: err.message
991
+ });
992
+ } finally {
993
+ browserClosed = true;
994
+ this.dedicatedBrowsers.delete(browser);
995
+ }
996
+ }
997
+ }
998
+ };
999
+
1000
+ this.emit('puppeteer.navigate', {
1001
+ url,
1002
+ userAgent,
1003
+ viewport,
1004
+ proxyId,
1005
+ sessionId: useSession
1006
+ });
1007
+
1008
+ return page;
1009
+ }
1010
+
1011
+ /**
1012
+ * Run handler with session-aware navigation helper
1013
+ * @param {string} sessionId - Session identifier
1014
+ * @param {Function} handler - Async function receiving the page instance
1015
+ * @param {Object} options - Navigate options (requires url)
1016
+ * @returns {Promise<*>}
1017
+ */
1018
+ async withSession(sessionId, handler, options = {}) {
1019
+ if (!sessionId) {
1020
+ throw new PluginError('withSession requires a sessionId', {
1021
+ pluginName: 'PuppeteerPlugin',
1022
+ operation: 'withSession',
1023
+ statusCode: 400,
1024
+ retriable: false,
1025
+ suggestion: 'Pass a sessionId when invoking withSession so cookies/proxies can be resolved.'
1026
+ });
1027
+ }
1028
+ if (typeof handler !== 'function') {
1029
+ throw new TypeError('withSession handler must be a function');
1030
+ }
1031
+
1032
+ const { url, ...navigateOptions } = options;
1033
+ if (!url) {
1034
+ throw new PluginError('withSession requires an options.url value', {
1035
+ pluginName: 'PuppeteerPlugin',
1036
+ operation: 'withSession',
1037
+ statusCode: 400,
1038
+ retriable: false,
1039
+ suggestion: 'Provide options.url to navigate before executing the session handler.'
1040
+ });
1041
+ }
1042
+
1043
+ this.emit('puppeteer.withSession.start', { sessionId, url });
1044
+
1045
+ const page = await this.navigate(url, {
1046
+ ...navigateOptions,
1047
+ useSession: sessionId
1048
+ });
1049
+
1050
+ let handlerError = null;
1051
+
1052
+ try {
1053
+ const result = await handler(page, this);
1054
+ return result;
1055
+ } catch (err) {
1056
+ handlerError = err;
1057
+ throw err;
1058
+ } finally {
1059
+ try {
1060
+ await page.close();
1061
+ } catch (err) {
1062
+ this.emit('puppeteer.withSession.cleanupFailed', {
1063
+ sessionId,
1064
+ url,
1065
+ error: err.message
1066
+ });
1067
+ }
1068
+
1069
+ this.emit('puppeteer.withSession.finish', {
1070
+ sessionId,
1071
+ url,
1072
+ error: handlerError ? handlerError.message : null
1073
+ });
1074
+ }
1075
+ }
1076
+
1077
+ /**
1078
+ * Attach human behavior methods to page
1079
+ * @private
1080
+ */
1081
+ _attachHumanBehaviorMethods(page) {
1082
+ // Human click
1083
+ page.humanClick = async (selector, options = {}) => {
1084
+ const element = await page.$(selector);
1085
+ if (!element) {
1086
+ throw new PluginError(`Element not found: ${selector}`, {
1087
+ pluginName: 'PuppeteerPlugin',
1088
+ operation: 'humanClick',
1089
+ statusCode: 404,
1090
+ retriable: false,
1091
+ suggestion: 'Ensure the selector matches an element on the page before invoking humanClick.',
1092
+ metadata: { selector }
1093
+ });
1094
+ }
1095
+
1096
+ if (this.config.humanBehavior.mouse.pathThroughElements && page._cursor) {
1097
+ // Move through elements to destination
1098
+ await page._cursor.moveTo(selector);
1099
+ await page._cursor.click();
1100
+ } else {
1101
+ await element.click();
1102
+ }
1103
+ };
1104
+
1105
+ // Human move
1106
+ page.humanMoveTo = async (selector, options = {}) => {
1107
+ if (!page._cursor) {
1108
+ throw new PluginError('Ghost cursor not initialized', {
1109
+ pluginName: 'PuppeteerPlugin',
1110
+ operation: 'humanMoveTo',
1111
+ statusCode: 500,
1112
+ retriable: false,
1113
+ suggestion: 'Enable humanBehavior.mouse.enableGhostCursor in configuration before using humanMoveTo.'
1114
+ });
1115
+ }
1116
+
1117
+ await page._cursor.moveTo(selector);
1118
+ };
1119
+
1120
+ // Human type
1121
+ page.humanType = async (selector, text, options = {}) => {
1122
+ const element = await page.$(selector);
1123
+ if (!element) {
1124
+ throw new PluginError(`Element not found: ${selector}`, {
1125
+ pluginName: 'PuppeteerPlugin',
1126
+ operation: 'humanMoveTo',
1127
+ statusCode: 404,
1128
+ retriable: false,
1129
+ suggestion: 'Ensure the selector is present on the page when calling humanMoveTo.',
1130
+ metadata: { selector }
1131
+ });
1132
+ }
1133
+
1134
+ await element.click();
1135
+
1136
+ if (this.config.humanBehavior.typing.mistakes) {
1137
+ // Type with mistakes and corrections
1138
+ await this._typeWithMistakes(page, text, options);
1139
+ } else {
1140
+ // Normal typing with delays
1141
+ const [min, max] = this.config.humanBehavior.typing.delayRange;
1142
+ await page.type(selector, text, {
1143
+ delay: min + Math.random() * (max - min)
1144
+ });
1145
+ }
1146
+ };
1147
+
1148
+ // Human scroll
1149
+ page.humanScroll = async (options = {}) => {
1150
+ const { distance = null, direction = 'down' } = options;
1151
+
1152
+ if (distance) {
1153
+ await page.evaluate((dist, dir) => {
1154
+ window.scrollBy(0, dir === 'down' ? dist : -dist);
1155
+ }, distance, direction);
1156
+ } else {
1157
+ // Scroll to bottom with random stops
1158
+ await this._scrollWithStops(page, direction);
1159
+ }
1160
+ };
1161
+ }
1162
+
1163
+ /**
1164
+ * Type with random mistakes and corrections
1165
+ * @private
1166
+ */
1167
+ async _typeWithMistakes(page, text, options = {}) {
1168
+ const words = text.split(' ');
1169
+
1170
+ for (let i = 0; i < words.length; i++) {
1171
+ const word = words[i];
1172
+
1173
+ // 20% chance of making a mistake
1174
+ if (Math.random() < 0.2 && word.length > 3) {
1175
+ // Type wrong letter
1176
+ const wrongPos = Math.floor(Math.random() * word.length);
1177
+ const wrongChar = String.fromCharCode(97 + Math.floor(Math.random() * 26));
1178
+ const wrongWord = word.slice(0, wrongPos) + wrongChar + word.slice(wrongPos + 1);
1179
+
1180
+ await page.keyboard.type(wrongWord, { delay: 100 });
1181
+ await this._randomDelay(200, 500);
1182
+
1183
+ // Delete and retype
1184
+ for (let j = 0; j < wrongWord.length; j++) {
1185
+ await page.keyboard.press('Backspace');
1186
+ await this._randomDelay(50, 100);
1187
+ }
1188
+
1189
+ await page.keyboard.type(word, { delay: 100 });
1190
+ } else {
1191
+ await page.keyboard.type(word, { delay: 100 });
1192
+ }
1193
+
1194
+ // Add space between words
1195
+ if (i < words.length - 1) {
1196
+ await page.keyboard.press('Space');
1197
+ await this._randomDelay(100, 300);
1198
+ }
1199
+ }
1200
+ }
1201
+
1202
+ /**
1203
+ * Scroll with random stops
1204
+ * @private
1205
+ */
1206
+ async _scrollWithStops(page, direction = 'down') {
1207
+ const scrollHeight = await page.evaluate(() => document.body.scrollHeight);
1208
+ const viewportHeight = await page.evaluate(() => window.innerHeight);
1209
+ const steps = Math.floor(scrollHeight / viewportHeight);
1210
+
1211
+ for (let i = 0; i < steps; i++) {
1212
+ await page.evaluate((dir, vh) => {
1213
+ window.scrollBy(0, dir === 'down' ? vh : -vh);
1214
+ }, direction, viewportHeight);
1215
+
1216
+ await this._randomDelay(500, 1500);
1217
+
1218
+ // Random back scroll
1219
+ if (this.config.humanBehavior.scrolling.backScroll && Math.random() < 0.1) {
1220
+ await page.evaluate(() => window.scrollBy(0, -100));
1221
+ await this._randomDelay(200, 500);
1222
+ }
1223
+ }
1224
+ }
1225
+
1226
+ /**
1227
+ * Random delay helper
1228
+ * @private
1229
+ */
1230
+ async _randomDelay(min, max) {
1231
+ const delay = min + Math.random() * (max - min);
1232
+ return new Promise(resolve => setTimeout(resolve, delay));
1233
+ }
1234
+
1235
+ /**
1236
+ * Farm cookies for a session
1237
+ * @param {string} sessionId - Session identifier
1238
+ * @returns {Promise<void>}
1239
+ */
1240
+ async farmCookies(sessionId) {
1241
+ if (!this.cookieManager) {
1242
+ throw new PluginError('Cookie manager not initialized', {
1243
+ pluginName: 'PuppeteerPlugin',
1244
+ operation: 'farmCookies',
1245
+ statusCode: 500,
1246
+ retriable: false,
1247
+ suggestion: 'Enable cookieManager during plugin initialization before calling farmCookies.'
1248
+ });
1249
+ }
1250
+
1251
+ return await this.cookieManager.farmCookies(sessionId);
1252
+ }
1253
+
1254
+ /**
1255
+ * Get cookie pool statistics
1256
+ * @returns {Promise<Object>}
1257
+ */
1258
+ async getCookieStats() {
1259
+ if (!this.cookieManager) {
1260
+ throw new PluginError('Cookie manager not initialized', {
1261
+ pluginName: 'PuppeteerPlugin',
1262
+ operation: 'getCookieStats',
1263
+ statusCode: 500,
1264
+ retriable: false,
1265
+ suggestion: 'Ensure cookieManager is configured before requesting cookie stats.'
1266
+ });
1267
+ }
1268
+
1269
+ return await this.cookieManager.getStats();
1270
+ }
1271
+
1272
+ /**
1273
+ * Get proxy pool statistics
1274
+ * @returns {Array}
1275
+ */
1276
+ getProxyStats() {
1277
+ if (!this.proxyManager) {
1278
+ throw new PluginError('Proxy manager not initialized', {
1279
+ pluginName: 'PuppeteerPlugin',
1280
+ operation: 'getProxyStats',
1281
+ statusCode: 500,
1282
+ retriable: false,
1283
+ suggestion: 'Configure proxyManager before attempting to read proxy statistics.'
1284
+ });
1285
+ }
1286
+
1287
+ return this.proxyManager.getProxyStats();
1288
+ }
1289
+
1290
+ /**
1291
+ * Get session-proxy bindings
1292
+ * @returns {Array}
1293
+ */
1294
+ getSessionProxyBindings() {
1295
+ if (!this.proxyManager) {
1296
+ throw new PluginError('Proxy manager not initialized', {
1297
+ pluginName: 'PuppeteerPlugin',
1298
+ operation: 'getSessionProxyBindings',
1299
+ statusCode: 500,
1300
+ retriable: false,
1301
+ suggestion: 'Initialize proxyManager before retrieving session-proxy bindings.'
1302
+ });
1303
+ }
1304
+
1305
+ return this.proxyManager.getSessionBindings();
1306
+ }
1307
+
1308
+ /**
1309
+ * Check health of all proxies
1310
+ * @returns {Promise<Object>}
1311
+ */
1312
+ async checkProxyHealth() {
1313
+ if (!this.proxyManager) {
1314
+ throw new PluginError('Proxy manager not initialized', {
1315
+ pluginName: 'PuppeteerPlugin',
1316
+ operation: 'checkProxyHealth',
1317
+ statusCode: 500,
1318
+ retriable: false,
1319
+ suggestion: 'Set up proxyManager before running health checks.'
1320
+ });
1321
+ }
1322
+
1323
+ return await this.proxyManager.checkAllProxies();
1324
+ }
1325
+ }
1326
+
1327
+ export default PuppeteerPlugin;