@sparkleideas/browser 3.0.0-alpha.18

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.
@@ -0,0 +1,570 @@
1
+ /**
2
+ * @sparkleideas/browser - Browser Service
3
+ * Core application service integrating agent-browser with agentic-flow
4
+ */
5
+
6
+ import { AgentBrowserAdapter } from '../infrastructure/agent-browser-adapter.js';
7
+ import { createMemoryManager, type BrowserMemoryManager } from '../infrastructure/memory-integration.js';
8
+ import { getSecurityScanner, type BrowserSecurityScanner, type ThreatScanResult } from '../infrastructure/security-integration.js';
9
+ import type {
10
+ Snapshot,
11
+ SnapshotOptions,
12
+ ActionResult,
13
+ BrowserSession,
14
+ BrowserTrajectory,
15
+ BrowserTrajectoryStep,
16
+ BrowserSwarmConfig,
17
+ BrowserAgentConfig,
18
+ } from '../domain/types.js';
19
+
20
+ // ============================================================================
21
+ // Trajectory Tracking for ReasoningBank Integration
22
+ // ============================================================================
23
+
24
+ interface TrajectoryTracker {
25
+ id: string;
26
+ sessionId: string;
27
+ goal: string;
28
+ steps: BrowserTrajectoryStep[];
29
+ startedAt: string;
30
+ lastSnapshot?: Snapshot;
31
+ }
32
+
33
+ const activeTrajectories = new Map<string, TrajectoryTracker>();
34
+
35
+ // ============================================================================
36
+ // Browser Service Class
37
+ // ============================================================================
38
+
39
+ export interface BrowserServiceConfig extends Partial<BrowserAgentConfig> {
40
+ enableMemory?: boolean;
41
+ enableSecurity?: boolean;
42
+ requireHttps?: boolean;
43
+ blockedDomains?: string[];
44
+ allowedDomains?: string[];
45
+ }
46
+
47
+ export class BrowserService {
48
+ private adapter: AgentBrowserAdapter;
49
+ private sessionId: string;
50
+ private currentTrajectory?: string;
51
+ private snapshots: Map<string, Snapshot> = new Map();
52
+ private memoryManager?: BrowserMemoryManager;
53
+ private securityScanner?: BrowserSecurityScanner;
54
+ private config: BrowserServiceConfig;
55
+
56
+ constructor(config: BrowserServiceConfig = {}) {
57
+ this.config = config;
58
+ this.sessionId = config.sessionId || `browser-${Date.now()}`;
59
+ this.adapter = new AgentBrowserAdapter({
60
+ session: this.sessionId,
61
+ timeout: config.defaultTimeout || 30000,
62
+ headless: config.headless !== false,
63
+ });
64
+
65
+ // Initialize memory integration if enabled (default: true)
66
+ if (config.enableMemory !== false) {
67
+ this.memoryManager = createMemoryManager(this.sessionId);
68
+ }
69
+
70
+ // Initialize security scanning if enabled (default: true)
71
+ if (config.enableSecurity !== false) {
72
+ this.securityScanner = getSecurityScanner({
73
+ requireHttps: config.requireHttps,
74
+ blockedDomains: config.blockedDomains,
75
+ allowedDomains: config.allowedDomains,
76
+ });
77
+ }
78
+ }
79
+
80
+ // ===========================================================================
81
+ // Trajectory Management (for ReasoningBank/SONA learning)
82
+ // ===========================================================================
83
+
84
+ /**
85
+ * Start a new trajectory for learning
86
+ */
87
+ startTrajectory(goal: string): string {
88
+ const id = `traj-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
89
+ activeTrajectories.set(id, {
90
+ id,
91
+ sessionId: this.sessionId,
92
+ goal,
93
+ steps: [],
94
+ startedAt: new Date().toISOString(),
95
+ });
96
+ this.currentTrajectory = id;
97
+ return id;
98
+ }
99
+
100
+ /**
101
+ * Record a step in the current trajectory
102
+ */
103
+ private recordStep(action: string, input: Record<string, unknown> | object, result: ActionResult): void {
104
+ if (!this.currentTrajectory) return;
105
+
106
+ const trajectory = activeTrajectories.get(this.currentTrajectory);
107
+ if (!trajectory) return;
108
+
109
+ trajectory.steps.push({
110
+ action,
111
+ input: input as Record<string, unknown>,
112
+ result,
113
+ snapshot: trajectory.lastSnapshot,
114
+ timestamp: new Date().toISOString(),
115
+ });
116
+ }
117
+
118
+ /**
119
+ * End trajectory and return for learning (also stores in memory)
120
+ */
121
+ async endTrajectory(success: boolean, verdict?: string): Promise<BrowserTrajectory | null> {
122
+ if (!this.currentTrajectory) return null;
123
+
124
+ const trajectory = activeTrajectories.get(this.currentTrajectory);
125
+ if (!trajectory) return null;
126
+
127
+ const completed: BrowserTrajectory = {
128
+ ...trajectory,
129
+ completedAt: new Date().toISOString(),
130
+ success,
131
+ verdict,
132
+ };
133
+
134
+ // Store in memory for learning
135
+ if (this.memoryManager) {
136
+ await this.memoryManager.storeTrajectory(completed);
137
+ }
138
+
139
+ activeTrajectories.delete(this.currentTrajectory);
140
+ this.currentTrajectory = undefined;
141
+
142
+ return completed;
143
+ }
144
+
145
+ /**
146
+ * Get current trajectory for inspection
147
+ */
148
+ getCurrentTrajectory(): TrajectoryTracker | null {
149
+ if (!this.currentTrajectory) return null;
150
+ return activeTrajectories.get(this.currentTrajectory) || null;
151
+ }
152
+
153
+ // ===========================================================================
154
+ // Core Browser Operations
155
+ // ===========================================================================
156
+
157
+ /**
158
+ * Navigate to URL with trajectory tracking and security scanning
159
+ */
160
+ async open(url: string, options?: { waitUntil?: 'load' | 'domcontentloaded' | 'networkidle'; headers?: Record<string, string>; skipSecurityCheck?: boolean }): Promise<ActionResult> {
161
+ // Security check before navigation
162
+ if (this.securityScanner && !options?.skipSecurityCheck) {
163
+ const scanResult = await this.securityScanner.scanUrl(url);
164
+ if (!scanResult.safe) {
165
+ const threats = scanResult.threats.map(t => `${t.type}: ${t.description}`).join('; ');
166
+ return {
167
+ success: false,
168
+ error: `Security scan failed: ${threats}`,
169
+ data: { scanResult },
170
+ };
171
+ }
172
+ }
173
+
174
+ const result = await this.adapter.open({
175
+ url,
176
+ waitUntil: options?.waitUntil,
177
+ headers: options?.headers,
178
+ });
179
+ this.recordStep('open', { url, ...options }, result);
180
+ return result;
181
+ }
182
+
183
+ /**
184
+ * Scan URL for security threats without navigating
185
+ */
186
+ async scanUrl(url: string): Promise<ThreatScanResult> {
187
+ if (!this.securityScanner) {
188
+ return { safe: true, threats: [], pii: [], score: 1, scanDuration: 0 };
189
+ }
190
+ return this.securityScanner.scanUrl(url);
191
+ }
192
+
193
+ /**
194
+ * Get snapshot with automatic caching
195
+ */
196
+ async snapshot(options: SnapshotOptions = {}): Promise<ActionResult<Snapshot>> {
197
+ const result = await this.adapter.snapshot({
198
+ interactive: options.interactive !== false,
199
+ compact: options.compact !== false,
200
+ ...options,
201
+ });
202
+
203
+ if (result.success && result.data) {
204
+ // Cache snapshot and update trajectory
205
+ const snapshot = result.data as Snapshot;
206
+ this.snapshots.set('latest', snapshot);
207
+
208
+ if (this.currentTrajectory) {
209
+ const trajectory = activeTrajectories.get(this.currentTrajectory);
210
+ if (trajectory) {
211
+ trajectory.lastSnapshot = snapshot;
212
+ }
213
+ }
214
+ }
215
+
216
+ this.recordStep('snapshot', options, result);
217
+ return result as ActionResult<Snapshot>;
218
+ }
219
+
220
+ /**
221
+ * Click with trajectory tracking
222
+ */
223
+ async click(target: string, options?: { button?: 'left' | 'right' | 'middle'; force?: boolean }): Promise<ActionResult> {
224
+ const result = await this.adapter.click({
225
+ target,
226
+ ...options,
227
+ });
228
+ this.recordStep('click', { target, ...options }, result);
229
+ return result;
230
+ }
231
+
232
+ /**
233
+ * Fill input with trajectory tracking and PII scanning
234
+ */
235
+ async fill(target: string, value: string, options?: { force?: boolean; skipPIICheck?: boolean }): Promise<ActionResult> {
236
+ // Check for PII in the value
237
+ if (this.securityScanner && !options?.skipPIICheck) {
238
+ const scanResult = this.securityScanner.scanContent(value, target);
239
+ if (scanResult.pii.length > 0) {
240
+ // Log masked values for security
241
+ const maskedPII = scanResult.pii.map(p => `${p.type}: ${p.masked}`).join(', ');
242
+ console.log(`[security] PII detected in form field ${target}: ${maskedPII}`);
243
+ }
244
+ }
245
+
246
+ const result = await this.adapter.fill({
247
+ target,
248
+ value,
249
+ ...options,
250
+ });
251
+ this.recordStep('fill', { target, value: this.securityScanner ? '[REDACTED]' : value, ...options }, result);
252
+ return result;
253
+ }
254
+
255
+ /**
256
+ * Check if content contains PII
257
+ */
258
+ scanForPII(content: string, context?: string): ThreatScanResult {
259
+ if (!this.securityScanner) {
260
+ return { safe: true, threats: [], pii: [], score: 1, scanDuration: 0 };
261
+ }
262
+ return this.securityScanner.scanContent(content, context);
263
+ }
264
+
265
+ /**
266
+ * Type text with trajectory tracking
267
+ */
268
+ async type(target: string, text: string, options?: { delay?: number }): Promise<ActionResult> {
269
+ const result = await this.adapter.type({
270
+ target,
271
+ text,
272
+ ...options,
273
+ });
274
+ this.recordStep('type', { target, text, ...options }, result);
275
+ return result;
276
+ }
277
+
278
+ /**
279
+ * Press key with trajectory tracking
280
+ */
281
+ async press(key: string, delay?: number): Promise<ActionResult> {
282
+ const result = await this.adapter.press(key, delay);
283
+ this.recordStep('press', { key, delay }, result);
284
+ return result;
285
+ }
286
+
287
+ /**
288
+ * Wait for condition
289
+ */
290
+ async wait(options: { selector?: string; timeout?: number; text?: string; url?: string; load?: 'load' | 'domcontentloaded' | 'networkidle'; fn?: string }): Promise<ActionResult> {
291
+ const result = await this.adapter.wait(options);
292
+ this.recordStep('wait', options, result);
293
+ return result;
294
+ }
295
+
296
+ /**
297
+ * Get element text
298
+ */
299
+ async getText(target: string): Promise<ActionResult<string>> {
300
+ const result = await this.adapter.getText(target);
301
+ this.recordStep('getText', { target }, result);
302
+ return result;
303
+ }
304
+
305
+ /**
306
+ * Execute JavaScript
307
+ */
308
+ async eval<T = unknown>(script: string): Promise<ActionResult<T>> {
309
+ const result = await this.adapter.eval<T>({ script });
310
+ this.recordStep('eval', { script }, result);
311
+ return result;
312
+ }
313
+
314
+ /**
315
+ * Take screenshot
316
+ */
317
+ async screenshot(options?: { path?: string; fullPage?: boolean }): Promise<ActionResult<string>> {
318
+ const result = await this.adapter.screenshot(options || {});
319
+ this.recordStep('screenshot', options || {}, result);
320
+ return result;
321
+ }
322
+
323
+ /**
324
+ * Close browser
325
+ */
326
+ async close(): Promise<ActionResult> {
327
+ const result = await this.adapter.close();
328
+ this.recordStep('close', {}, result);
329
+ return result;
330
+ }
331
+
332
+ // ===========================================================================
333
+ // High-Level Workflow Operations
334
+ // ===========================================================================
335
+
336
+ /**
337
+ * Authenticate using header injection (skips login UI)
338
+ */
339
+ async authenticateWithHeaders(url: string, headers: Record<string, string>): Promise<ActionResult> {
340
+ return this.open(url, { headers });
341
+ }
342
+
343
+ /**
344
+ * Fill and submit a form
345
+ */
346
+ async submitForm(fields: Array<{ target: string; value: string }>, submitButton: string): Promise<ActionResult> {
347
+ // Fill all fields
348
+ for (const field of fields) {
349
+ const result = await this.fill(field.target, field.value);
350
+ if (!result.success) return result;
351
+ }
352
+
353
+ // Click submit
354
+ return this.click(submitButton);
355
+ }
356
+
357
+ /**
358
+ * Extract data using snapshot refs
359
+ */
360
+ async extractData(refs: string[]): Promise<Record<string, string>> {
361
+ const data: Record<string, string> = {};
362
+ for (const ref of refs) {
363
+ const result = await this.getText(ref);
364
+ if (result.success && result.data) {
365
+ data[ref] = result.data;
366
+ }
367
+ }
368
+ return data;
369
+ }
370
+
371
+ /**
372
+ * Navigate and wait for specific element
373
+ */
374
+ async navigateAndWait(url: string, selector: string, timeout?: number): Promise<ActionResult> {
375
+ const navResult = await this.open(url);
376
+ if (!navResult.success) return navResult;
377
+
378
+ return this.wait({ selector, timeout });
379
+ }
380
+
381
+ // ===========================================================================
382
+ // Session State
383
+ // ===========================================================================
384
+
385
+ /**
386
+ * Get cached snapshot
387
+ */
388
+ getLatestSnapshot(): Snapshot | null {
389
+ return this.snapshots.get('latest') || null;
390
+ }
391
+
392
+ /**
393
+ * Get session ID
394
+ */
395
+ getSessionId(): string {
396
+ return this.sessionId;
397
+ }
398
+
399
+ /**
400
+ * Get underlying adapter for advanced operations
401
+ */
402
+ getAdapter(): AgentBrowserAdapter {
403
+ return this.adapter;
404
+ }
405
+
406
+ /**
407
+ * Get memory manager for direct memory operations
408
+ */
409
+ getMemoryManager(): BrowserMemoryManager | undefined {
410
+ return this.memoryManager;
411
+ }
412
+
413
+ /**
414
+ * Get security scanner for direct security operations
415
+ */
416
+ getSecurityScanner(): BrowserSecurityScanner | undefined {
417
+ return this.securityScanner;
418
+ }
419
+
420
+ /**
421
+ * Find similar trajectories for a goal (uses HNSW search)
422
+ */
423
+ async findSimilarTrajectories(goal: string, topK = 5): Promise<BrowserTrajectory[]> {
424
+ if (!this.memoryManager) return [];
425
+ return this.memoryManager.findSimilarTrajectories(goal, topK);
426
+ }
427
+
428
+ /**
429
+ * Get session memory statistics
430
+ */
431
+ async getMemoryStats(): Promise<{
432
+ trajectories: number;
433
+ patterns: number;
434
+ snapshots: number;
435
+ errors: number;
436
+ successRate: number;
437
+ } | null> {
438
+ if (!this.memoryManager) return null;
439
+ return this.memoryManager.getSessionStats();
440
+ }
441
+ }
442
+
443
+ // ============================================================================
444
+ // Browser Swarm Coordinator
445
+ // ============================================================================
446
+
447
+ export class BrowserSwarmCoordinator {
448
+ private config: BrowserSwarmConfig;
449
+ private services: Map<string, BrowserService> = new Map();
450
+ private sharedData: Map<string, unknown> = new Map();
451
+
452
+ constructor(config: BrowserSwarmConfig) {
453
+ this.config = config;
454
+ }
455
+
456
+ /**
457
+ * Spawn a new browser agent in the swarm
458
+ */
459
+ async spawnAgent(role: 'navigator' | 'scraper' | 'validator' | 'tester' | 'monitor'): Promise<BrowserService> {
460
+ if (this.services.size >= this.config.maxSessions) {
461
+ throw new Error(`Max sessions (${this.config.maxSessions}) reached`);
462
+ }
463
+
464
+ const sessionId = `${this.config.sessionPrefix}-${role}-${Date.now()}`;
465
+ const service = new BrowserService({
466
+ sessionId,
467
+ role,
468
+ capabilities: this.getCapabilitiesForRole(role),
469
+ defaultTimeout: 30000,
470
+ headless: true,
471
+ });
472
+
473
+ this.services.set(sessionId, service);
474
+ return service;
475
+ }
476
+
477
+ /**
478
+ * Get capabilities for a role
479
+ */
480
+ private getCapabilitiesForRole(role: string): string[] {
481
+ switch (role) {
482
+ case 'navigator':
483
+ return ['navigation', 'authentication', 'session-management'];
484
+ case 'scraper':
485
+ return ['snapshot', 'extraction', 'pagination'];
486
+ case 'validator':
487
+ return ['assertions', 'state-checks', 'screenshots'];
488
+ case 'tester':
489
+ return ['forms', 'interactions', 'assertions'];
490
+ case 'monitor':
491
+ return ['network', 'console', 'errors'];
492
+ default:
493
+ return [];
494
+ }
495
+ }
496
+
497
+ /**
498
+ * Share data between agents
499
+ */
500
+ shareData(key: string, value: unknown): void {
501
+ this.sharedData.set(key, value);
502
+ }
503
+
504
+ /**
505
+ * Get shared data
506
+ */
507
+ getSharedData<T>(key: string): T | undefined {
508
+ return this.sharedData.get(key) as T | undefined;
509
+ }
510
+
511
+ /**
512
+ * Get all active sessions
513
+ */
514
+ getSessions(): string[] {
515
+ return Array.from(this.services.keys());
516
+ }
517
+
518
+ /**
519
+ * Get a specific service
520
+ */
521
+ getService(sessionId: string): BrowserService | undefined {
522
+ return this.services.get(sessionId);
523
+ }
524
+
525
+ /**
526
+ * Close all sessions
527
+ */
528
+ async closeAll(): Promise<void> {
529
+ const closePromises = Array.from(this.services.values()).map(s => s.close());
530
+ await Promise.all(closePromises);
531
+ this.services.clear();
532
+ }
533
+
534
+ /**
535
+ * Get coordinator stats
536
+ */
537
+ getStats(): { activeSessions: number; maxSessions: number; topology: string } {
538
+ return {
539
+ activeSessions: this.services.size,
540
+ maxSessions: this.config.maxSessions,
541
+ topology: this.config.topology,
542
+ };
543
+ }
544
+ }
545
+
546
+ // ============================================================================
547
+ // Factory Functions
548
+ // ============================================================================
549
+
550
+ /**
551
+ * Create a standalone browser service
552
+ */
553
+ export function createBrowserService(options?: Partial<BrowserAgentConfig>): BrowserService {
554
+ return new BrowserService(options);
555
+ }
556
+
557
+ /**
558
+ * Create a browser swarm coordinator
559
+ */
560
+ export function createBrowserSwarm(config?: Partial<BrowserSwarmConfig>): BrowserSwarmCoordinator {
561
+ return new BrowserSwarmCoordinator({
562
+ topology: config?.topology || 'hierarchical',
563
+ maxSessions: config?.maxSessions || 5,
564
+ sessionPrefix: config?.sessionPrefix || 'swarm',
565
+ sharedCookies: config?.sharedCookies,
566
+ coordinatorSession: config?.coordinatorSession,
567
+ });
568
+ }
569
+
570
+ export default BrowserService;