@darbotlabs/darbot-browser-mcp 0.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +222 -161
- package/cli.js +1 -1
- package/config.d.ts +77 -1
- package/index.d.ts +1 -1
- package/index.js +1 -1
- package/lib/ai/context.js +150 -0
- package/lib/ai/guardrails.js +382 -0
- package/lib/ai/integration.js +397 -0
- package/lib/ai/intent.js +237 -0
- package/lib/ai/manualPromise.js +111 -0
- package/lib/ai/memory.js +273 -0
- package/lib/ai/ml-scorer.js +265 -0
- package/lib/ai/orchestrator-tools.js +292 -0
- package/lib/ai/orchestrator.js +473 -0
- package/lib/ai/planner.js +300 -0
- package/lib/ai/reporter.js +493 -0
- package/lib/ai/workflow.js +407 -0
- package/lib/auth/apiKeyAuth.js +46 -0
- package/lib/auth/entraAuth.js +110 -0
- package/lib/auth/entraJwtVerifier.js +117 -0
- package/lib/auth/index.js +210 -0
- package/lib/auth/managedIdentityAuth.js +175 -0
- package/lib/auth/mcpOAuthProvider.js +186 -0
- package/lib/auth/tunnelAuth.js +120 -0
- package/lib/browserContextFactory.js +1 -1
- package/lib/browserServer.js +1 -1
- package/lib/cdpRelay.js +2 -2
- package/lib/common.js +68 -0
- package/lib/config.js +62 -3
- package/lib/connection.js +1 -1
- package/lib/context.js +1 -1
- package/lib/fileUtils.js +1 -1
- package/lib/guardrails.js +382 -0
- package/lib/health.js +178 -0
- package/lib/httpServer.js +1 -1
- package/lib/index.js +1 -1
- package/lib/javascript.js +1 -1
- package/lib/manualPromise.js +1 -1
- package/lib/memory.js +273 -0
- package/lib/openapi.js +373 -0
- package/lib/orchestrator.js +473 -0
- package/lib/package.js +1 -1
- package/lib/pageSnapshot.js +17 -2
- package/lib/planner.js +302 -0
- package/lib/program.js +17 -5
- package/lib/reporter.js +493 -0
- package/lib/resources/resource.js +1 -1
- package/lib/server.js +5 -3
- package/lib/tab.js +1 -1
- package/lib/tools/ai-native.js +298 -0
- package/lib/tools/autonomous.js +147 -0
- package/lib/tools/clock.js +183 -0
- package/lib/tools/common.js +1 -1
- package/lib/tools/console.js +1 -1
- package/lib/tools/diagnostics.js +132 -0
- package/lib/tools/dialogs.js +1 -1
- package/lib/tools/emulation.js +155 -0
- package/lib/tools/files.js +1 -1
- package/lib/tools/install.js +1 -1
- package/lib/tools/keyboard.js +1 -1
- package/lib/tools/navigate.js +1 -1
- package/lib/tools/network.js +1 -1
- package/lib/tools/pageSnapshot.js +58 -0
- package/lib/tools/pdf.js +1 -1
- package/lib/tools/profiles.js +76 -25
- package/lib/tools/screenshot.js +1 -1
- package/lib/tools/scroll.js +93 -0
- package/lib/tools/snapshot.js +1 -1
- package/lib/tools/storage.js +328 -0
- package/lib/tools/tab.js +16 -0
- package/lib/tools/tabs.js +1 -1
- package/lib/tools/testing.js +1 -1
- package/lib/tools/tool.js +1 -1
- package/lib/tools/utils.js +1 -1
- package/lib/tools/vision.js +1 -1
- package/lib/tools/wait.js +1 -1
- package/lib/tools.js +22 -1
- package/lib/transport.js +251 -31
- package/package.json +28 -22
package/cli.js
CHANGED
package/config.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Copyright (c)
|
|
2
|
+
* Copyright (c) DarbotLabs.
|
|
3
3
|
*
|
|
4
4
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
5
|
* you may not use this file except in compliance with the License.
|
|
@@ -80,6 +80,82 @@ export type Config = {
|
|
|
80
80
|
* The host to bind the server to. Default is localhost. Use 0.0.0.0 to bind to all interfaces.
|
|
81
81
|
*/
|
|
82
82
|
host?: string;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Base URL for the server, used in OpenAPI specification generation.
|
|
86
|
+
*/
|
|
87
|
+
baseUrl?: string;
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Enable HTTPS with SSL certificates.
|
|
91
|
+
*/
|
|
92
|
+
https?: {
|
|
93
|
+
enabled: boolean;
|
|
94
|
+
keyPath?: string;
|
|
95
|
+
certPath?: string;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Rate limiting configuration for enterprise use.
|
|
100
|
+
*/
|
|
101
|
+
rateLimit?: {
|
|
102
|
+
enabled: boolean;
|
|
103
|
+
windowMs?: number;
|
|
104
|
+
maxRequests?: number;
|
|
105
|
+
};
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Microsoft Copilot Studio integration settings.
|
|
110
|
+
*/
|
|
111
|
+
copilotStudio?: {
|
|
112
|
+
/**
|
|
113
|
+
* Enable Copilot Studio specific features.
|
|
114
|
+
*/
|
|
115
|
+
enabled?: boolean;
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Callback URL for OAuth authentication flow.
|
|
119
|
+
*/
|
|
120
|
+
callbackUrl?: string;
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Maximum number of concurrent browser sessions for enterprise workloads.
|
|
124
|
+
*/
|
|
125
|
+
maxConcurrentSessions?: number;
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Session timeout in milliseconds.
|
|
129
|
+
*/
|
|
130
|
+
sessionTimeoutMs?: number;
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Enable audit logging for compliance.
|
|
134
|
+
*/
|
|
135
|
+
auditLogging?: boolean;
|
|
136
|
+
},
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Authentication configuration.
|
|
140
|
+
*/
|
|
141
|
+
auth?: {
|
|
142
|
+
/**
|
|
143
|
+
* Microsoft Entra ID authentication settings.
|
|
144
|
+
*/
|
|
145
|
+
entraId?: {
|
|
146
|
+
enabled?: boolean;
|
|
147
|
+
tenantId?: string;
|
|
148
|
+
clientId?: string;
|
|
149
|
+
clientSecret?: string;
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* API key authentication settings.
|
|
154
|
+
*/
|
|
155
|
+
apiKey?: {
|
|
156
|
+
enabled?: boolean;
|
|
157
|
+
keys?: string[];
|
|
158
|
+
};
|
|
83
159
|
},
|
|
84
160
|
|
|
85
161
|
/**
|
package/index.d.ts
CHANGED
package/index.js
CHANGED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) DarbotLabs.
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* AI-native context manager for tracking user intent and session state
|
|
18
|
+
*/
|
|
19
|
+
export class AIContextManager {
|
|
20
|
+
contexts = new Map();
|
|
21
|
+
maxHistorySize = 50;
|
|
22
|
+
/**
|
|
23
|
+
* Create or get session context for a browser session
|
|
24
|
+
*/
|
|
25
|
+
getOrCreateContext(sessionId) {
|
|
26
|
+
if (!this.contexts.has(sessionId)) {
|
|
27
|
+
this.contexts.set(sessionId, {
|
|
28
|
+
currentTask: '',
|
|
29
|
+
pageIntent: '',
|
|
30
|
+
userGoals: [],
|
|
31
|
+
navigationHistory: [],
|
|
32
|
+
failurePatterns: [],
|
|
33
|
+
successfulActions: [],
|
|
34
|
+
contextId: sessionId,
|
|
35
|
+
startTime: Date.now(),
|
|
36
|
+
lastActivity: Date.now(),
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
return this.contexts.get(sessionId);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Update current task description
|
|
43
|
+
*/
|
|
44
|
+
updateTask(sessionId, task) {
|
|
45
|
+
const context = this.getOrCreateContext(sessionId);
|
|
46
|
+
context.currentTask = task;
|
|
47
|
+
context.lastActivity = Date.now();
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Set page intent based on analysis
|
|
51
|
+
*/
|
|
52
|
+
setPageIntent(sessionId, intent) {
|
|
53
|
+
const context = this.getOrCreateContext(sessionId);
|
|
54
|
+
context.pageIntent = intent;
|
|
55
|
+
context.lastActivity = Date.now();
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Record successful action for learning
|
|
59
|
+
*/
|
|
60
|
+
recordSuccess(sessionId, action) {
|
|
61
|
+
const context = this.getOrCreateContext(sessionId);
|
|
62
|
+
context.successfulActions.push(action);
|
|
63
|
+
context.lastActivity = Date.now();
|
|
64
|
+
// Keep only recent successes
|
|
65
|
+
if (context.successfulActions.length > this.maxHistorySize)
|
|
66
|
+
context.successfulActions = context.successfulActions.slice(-this.maxHistorySize);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Record page state for navigation tracking
|
|
70
|
+
*/
|
|
71
|
+
recordPageState(sessionId, pageState) {
|
|
72
|
+
const context = this.getOrCreateContext(sessionId);
|
|
73
|
+
context.navigationHistory.push(pageState);
|
|
74
|
+
context.lastActivity = Date.now();
|
|
75
|
+
// Keep only recent navigation history
|
|
76
|
+
if (context.navigationHistory.length > this.maxHistorySize)
|
|
77
|
+
context.navigationHistory = context.navigationHistory.slice(-this.maxHistorySize);
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Record error pattern for learning
|
|
81
|
+
*/
|
|
82
|
+
recordError(sessionId, error) {
|
|
83
|
+
const context = this.getOrCreateContext(sessionId);
|
|
84
|
+
// Find existing pattern or create new
|
|
85
|
+
const existing = context.failurePatterns.find(p => p.errorType === error.errorType &&
|
|
86
|
+
p.elementSelector === error.elementSelector &&
|
|
87
|
+
p.pageUrl === error.pageUrl);
|
|
88
|
+
if (existing) {
|
|
89
|
+
existing.frequency++;
|
|
90
|
+
existing.lastOccurrence = Date.now();
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
context.failurePatterns.push({
|
|
94
|
+
...error,
|
|
95
|
+
frequency: 1,
|
|
96
|
+
lastOccurrence: Date.now(),
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
context.lastActivity = Date.now();
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Get context for AI decision making
|
|
103
|
+
*/
|
|
104
|
+
getContext(sessionId) {
|
|
105
|
+
return this.getOrCreateContext(sessionId);
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Analyze patterns to suggest next actions
|
|
109
|
+
*/
|
|
110
|
+
suggestNextActions(sessionId, currentPage) {
|
|
111
|
+
const context = this.getOrCreateContext(sessionId);
|
|
112
|
+
const suggestions = [];
|
|
113
|
+
// Analyze successful patterns
|
|
114
|
+
const recentSuccesses = context.successfulActions
|
|
115
|
+
.filter(action => Date.now() - action.timestamp < 300000) // Last 5 minutes
|
|
116
|
+
.slice(-10);
|
|
117
|
+
// Look for common action patterns
|
|
118
|
+
const actionCounts = new Map();
|
|
119
|
+
recentSuccesses.forEach(action => {
|
|
120
|
+
actionCounts.set(action.action, (actionCounts.get(action.action) || 0) + 1);
|
|
121
|
+
});
|
|
122
|
+
// Suggest most common recent actions
|
|
123
|
+
const sortedActions = Array.from(actionCounts.entries())
|
|
124
|
+
.sort(([, a], [, b]) => b - a)
|
|
125
|
+
.slice(0, 3);
|
|
126
|
+
sortedActions.forEach(([action]) => {
|
|
127
|
+
suggestions.push(`Continue with ${action} action based on recent pattern`);
|
|
128
|
+
});
|
|
129
|
+
return suggestions;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Clean up old contexts
|
|
133
|
+
*/
|
|
134
|
+
cleanup() {
|
|
135
|
+
const now = Date.now();
|
|
136
|
+
const maxAge = 24 * 60 * 60 * 1000; // 24 hours
|
|
137
|
+
for (const [sessionId, context] of this.contexts.entries()) {
|
|
138
|
+
if (now - context.lastActivity > maxAge)
|
|
139
|
+
this.contexts.delete(sessionId);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Clear context for session
|
|
144
|
+
*/
|
|
145
|
+
clearContext(sessionId) {
|
|
146
|
+
this.contexts.delete(sessionId);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
// Global instance for session management
|
|
150
|
+
export const aiContextManager = new AIContextManager();
|
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) DarbotLabs.
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
import debug from 'debug';
|
|
17
|
+
const log = debug('darbot:guardrails');
|
|
18
|
+
/**
|
|
19
|
+
* Guardrail system for safe autonomous operation
|
|
20
|
+
*/
|
|
21
|
+
export class GuardrailSystem {
|
|
22
|
+
config;
|
|
23
|
+
domainCounts = new Map();
|
|
24
|
+
actionHistory = [];
|
|
25
|
+
rateLimiter;
|
|
26
|
+
constructor(config = {}) {
|
|
27
|
+
this.config = {
|
|
28
|
+
maxPagesPerDomain: 50,
|
|
29
|
+
maxDepth: 5,
|
|
30
|
+
timeoutMs: 300000, // 5 minutes
|
|
31
|
+
allowedDomains: [],
|
|
32
|
+
blockedDomains: [
|
|
33
|
+
'facebook.com', 'twitter.com', 'linkedin.com', 'instagram.com',
|
|
34
|
+
'tiktok.com', 'youtube.com', 'gmail.com', 'outlook.com'
|
|
35
|
+
],
|
|
36
|
+
blockedPatterns: [
|
|
37
|
+
/\/login\/?$/i,
|
|
38
|
+
/\/register\/?$/i,
|
|
39
|
+
/\/signup\/?$/i,
|
|
40
|
+
/\/logout\/?$/i,
|
|
41
|
+
/\/admin\/?/i,
|
|
42
|
+
/\/api\//i,
|
|
43
|
+
/\.exe$/i,
|
|
44
|
+
/\.dmg$/i,
|
|
45
|
+
/\.zip$/i,
|
|
46
|
+
/malware/i,
|
|
47
|
+
/virus/i,
|
|
48
|
+
/hack/i,
|
|
49
|
+
/illegal/i
|
|
50
|
+
],
|
|
51
|
+
rateLimit: {
|
|
52
|
+
requestsPerSecond: 2,
|
|
53
|
+
burstSize: 5
|
|
54
|
+
},
|
|
55
|
+
safetyChecks: {
|
|
56
|
+
preventInfiniteLoops: true,
|
|
57
|
+
preventDestructiveActions: true,
|
|
58
|
+
requireUserConfirmation: false
|
|
59
|
+
},
|
|
60
|
+
...config
|
|
61
|
+
};
|
|
62
|
+
this.rateLimiter = new RateLimiter(this.config.rateLimit.requestsPerSecond, this.config.rateLimit.burstSize);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Validate a crawl action before execution
|
|
66
|
+
*/
|
|
67
|
+
async validateAction(action, context) {
|
|
68
|
+
log('Validating action:', action.type, action.target || action.url);
|
|
69
|
+
// Check rate limiting
|
|
70
|
+
if (!this.rateLimiter.allowRequest()) {
|
|
71
|
+
return {
|
|
72
|
+
allowed: false,
|
|
73
|
+
reason: 'Rate limit exceeded. Please slow down requests.'
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
// Check timeout
|
|
77
|
+
if (this.isSessionTimedOut(context.sessionStartTime)) {
|
|
78
|
+
return {
|
|
79
|
+
allowed: false,
|
|
80
|
+
reason: `Session timeout exceeded (${this.config.timeoutMs}ms)`
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
// Check depth limit
|
|
84
|
+
if (context.currentDepth > this.config.maxDepth) {
|
|
85
|
+
return {
|
|
86
|
+
allowed: false,
|
|
87
|
+
reason: `Maximum crawl depth exceeded (${this.config.maxDepth})`
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
// Validate specific action types
|
|
91
|
+
switch (action.type) {
|
|
92
|
+
case 'navigate':
|
|
93
|
+
return this.validateNavigation(action, context);
|
|
94
|
+
case 'click':
|
|
95
|
+
return this.validateClick(action, context);
|
|
96
|
+
case 'type':
|
|
97
|
+
return this.validateType(action, context);
|
|
98
|
+
case 'finish':
|
|
99
|
+
return { allowed: true };
|
|
100
|
+
default:
|
|
101
|
+
return { allowed: true };
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Validate navigation actions
|
|
106
|
+
*/
|
|
107
|
+
validateNavigation(action, context) {
|
|
108
|
+
if (!action.url)
|
|
109
|
+
return { allowed: false, reason: 'Navigation action missing URL' };
|
|
110
|
+
// URL safety checks
|
|
111
|
+
const urlCheck = this.validateUrl(action.url);
|
|
112
|
+
if (!urlCheck.allowed)
|
|
113
|
+
return urlCheck;
|
|
114
|
+
// Domain count check
|
|
115
|
+
const domain = this.extractDomain(action.url);
|
|
116
|
+
const domainCount = this.domainCounts.get(domain) || 0;
|
|
117
|
+
if (domainCount >= this.config.maxPagesPerDomain) {
|
|
118
|
+
return {
|
|
119
|
+
allowed: false,
|
|
120
|
+
reason: `Maximum pages per domain exceeded for ${domain} (${this.config.maxPagesPerDomain})`
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
// Infinite loop prevention
|
|
124
|
+
if (this.config.safetyChecks.preventInfiniteLoops) {
|
|
125
|
+
const loopCheck = this.detectInfiniteLoop(action, context);
|
|
126
|
+
if (!loopCheck.allowed)
|
|
127
|
+
return loopCheck;
|
|
128
|
+
}
|
|
129
|
+
// Update domain count
|
|
130
|
+
this.domainCounts.set(domain, domainCount + 1);
|
|
131
|
+
return { allowed: true };
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Validate click actions
|
|
135
|
+
*/
|
|
136
|
+
validateClick(action, context) {
|
|
137
|
+
if (!action.target)
|
|
138
|
+
return { allowed: false, reason: 'Click action missing target selector' };
|
|
139
|
+
// Check for potentially destructive actions
|
|
140
|
+
if (this.config.safetyChecks.preventDestructiveActions) {
|
|
141
|
+
const destructiveCheck = this.checkDestructiveClick(action);
|
|
142
|
+
if (!destructiveCheck.allowed)
|
|
143
|
+
return destructiveCheck;
|
|
144
|
+
}
|
|
145
|
+
return { allowed: true };
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Validate type actions
|
|
149
|
+
*/
|
|
150
|
+
validateType(action, context) {
|
|
151
|
+
if (!action.text)
|
|
152
|
+
return { allowed: false, reason: 'Type action missing text' };
|
|
153
|
+
// Prevent sensitive data input
|
|
154
|
+
const sensitivePatterns = [
|
|
155
|
+
/password/i,
|
|
156
|
+
/ssn/i,
|
|
157
|
+
/social.security/i,
|
|
158
|
+
/credit.card/i,
|
|
159
|
+
/bank.account/i,
|
|
160
|
+
/api.key/i,
|
|
161
|
+
/secret/i,
|
|
162
|
+
/token/i
|
|
163
|
+
];
|
|
164
|
+
const containsSensitive = sensitivePatterns.some(pattern => pattern.test(action.text) || pattern.test(action.target || ''));
|
|
165
|
+
if (containsSensitive) {
|
|
166
|
+
return {
|
|
167
|
+
allowed: false,
|
|
168
|
+
reason: 'Type action appears to contain sensitive information'
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
return { allowed: true };
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Validate URL safety
|
|
175
|
+
*/
|
|
176
|
+
validateUrl(url) {
|
|
177
|
+
try {
|
|
178
|
+
const urlObj = new URL(url);
|
|
179
|
+
// Protocol check
|
|
180
|
+
if (!['http:', 'https:'].includes(urlObj.protocol)) {
|
|
181
|
+
return {
|
|
182
|
+
allowed: false,
|
|
183
|
+
reason: `Unsafe protocol: ${urlObj.protocol}`
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
// Domain whitelist check
|
|
187
|
+
if (this.config.allowedDomains && this.config.allowedDomains.length > 0) {
|
|
188
|
+
const isAllowed = this.config.allowedDomains.some(domain => urlObj.hostname === domain || urlObj.hostname.endsWith('.' + domain));
|
|
189
|
+
if (!isAllowed) {
|
|
190
|
+
return {
|
|
191
|
+
allowed: false,
|
|
192
|
+
reason: `Domain not in allowlist: ${urlObj.hostname}`
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
// Domain blocklist check
|
|
197
|
+
if (this.config.blockedDomains) {
|
|
198
|
+
const isBlocked = this.config.blockedDomains.some(domain => urlObj.hostname === domain || urlObj.hostname.endsWith('.' + domain));
|
|
199
|
+
if (isBlocked) {
|
|
200
|
+
return {
|
|
201
|
+
allowed: false,
|
|
202
|
+
reason: `Domain is blocked: ${urlObj.hostname}`
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
// Pattern check
|
|
207
|
+
if (this.config.blockedPatterns) {
|
|
208
|
+
const matchedPattern = this.config.blockedPatterns.find(pattern => pattern.test(url));
|
|
209
|
+
if (matchedPattern) {
|
|
210
|
+
return {
|
|
211
|
+
allowed: false,
|
|
212
|
+
reason: `URL matches blocked pattern: ${matchedPattern.source}`
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return { allowed: true };
|
|
217
|
+
}
|
|
218
|
+
catch (error) {
|
|
219
|
+
return {
|
|
220
|
+
allowed: false,
|
|
221
|
+
reason: `Invalid URL: ${error}`
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Detect infinite loop patterns
|
|
227
|
+
*/
|
|
228
|
+
detectInfiniteLoop(action, context) {
|
|
229
|
+
if (action.type !== 'navigate' || !action.url)
|
|
230
|
+
return { allowed: true };
|
|
231
|
+
// Check if we've visited this URL recently
|
|
232
|
+
const recentActions = this.actionHistory
|
|
233
|
+
.filter(h => Date.now() - h.timestamp < 60000) // Last minute
|
|
234
|
+
.filter(h => h.action.type === 'navigate' && h.action.url === action.url);
|
|
235
|
+
if (recentActions.length >= 3) {
|
|
236
|
+
return {
|
|
237
|
+
allowed: false,
|
|
238
|
+
reason: `Potential infinite loop detected: visited ${action.url} ${recentActions.length} times in the last minute`
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
// Check for back-and-forth patterns
|
|
242
|
+
const lastFewActions = this.actionHistory.slice(-6)
|
|
243
|
+
.filter(h => h.action.type === 'navigate')
|
|
244
|
+
.map(h => h.action.url);
|
|
245
|
+
if (lastFewActions.length >= 4) {
|
|
246
|
+
const pattern = [lastFewActions[0], lastFewActions[1]];
|
|
247
|
+
let patternCount = 0;
|
|
248
|
+
for (let i = 0; i < lastFewActions.length - 1; i += 2) {
|
|
249
|
+
if (lastFewActions[i] === pattern[0] && lastFewActions[i + 1] === pattern[1])
|
|
250
|
+
patternCount++;
|
|
251
|
+
}
|
|
252
|
+
if (patternCount >= 2) {
|
|
253
|
+
return {
|
|
254
|
+
allowed: false,
|
|
255
|
+
reason: 'Infinite navigation loop detected between two URLs'
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return { allowed: true };
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Check for potentially destructive click actions
|
|
263
|
+
*/
|
|
264
|
+
checkDestructiveClick(action) {
|
|
265
|
+
if (!action.target)
|
|
266
|
+
return { allowed: true };
|
|
267
|
+
const destructivePatterns = [
|
|
268
|
+
/delete/i,
|
|
269
|
+
/remove/i,
|
|
270
|
+
/cancel/i,
|
|
271
|
+
/logout/i,
|
|
272
|
+
/sign.?out/i,
|
|
273
|
+
/unsubscribe/i,
|
|
274
|
+
/deactivate/i,
|
|
275
|
+
/close.account/i,
|
|
276
|
+
/purchase/i,
|
|
277
|
+
/buy.now/i,
|
|
278
|
+
/order.now/i,
|
|
279
|
+
/submit.payment/i
|
|
280
|
+
];
|
|
281
|
+
const isDestructive = destructivePatterns.some(pattern => pattern.test(action.target) || pattern.test(action.reason || ''));
|
|
282
|
+
if (isDestructive) {
|
|
283
|
+
return {
|
|
284
|
+
allowed: false,
|
|
285
|
+
reason: 'Click action appears to be potentially destructive'
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
return { allowed: true };
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Record an action in history
|
|
292
|
+
*/
|
|
293
|
+
recordAction(action, url) {
|
|
294
|
+
this.actionHistory.push({
|
|
295
|
+
action,
|
|
296
|
+
timestamp: Date.now(),
|
|
297
|
+
url
|
|
298
|
+
});
|
|
299
|
+
// Keep only recent history
|
|
300
|
+
const cutoff = Date.now() - 3600000; // 1 hour
|
|
301
|
+
while (this.actionHistory.length > 0 && this.actionHistory[0].timestamp < cutoff)
|
|
302
|
+
this.actionHistory.shift();
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Check if session has timed out
|
|
306
|
+
*/
|
|
307
|
+
isSessionTimedOut(sessionStartTime) {
|
|
308
|
+
return Date.now() - sessionStartTime > this.config.timeoutMs;
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Extract domain from URL
|
|
312
|
+
*/
|
|
313
|
+
extractDomain(url) {
|
|
314
|
+
try {
|
|
315
|
+
return new URL(url).hostname;
|
|
316
|
+
}
|
|
317
|
+
catch {
|
|
318
|
+
return url;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Get guardrail statistics
|
|
323
|
+
*/
|
|
324
|
+
getStats() {
|
|
325
|
+
return {
|
|
326
|
+
domainCounts: Object.fromEntries(this.domainCounts),
|
|
327
|
+
actionsRecorded: this.actionHistory.length,
|
|
328
|
+
rateLimiterStatus: this.rateLimiter.getStatus(),
|
|
329
|
+
config: this.config
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Reset guardrail state
|
|
334
|
+
*/
|
|
335
|
+
reset() {
|
|
336
|
+
this.domainCounts.clear();
|
|
337
|
+
this.actionHistory.length = 0;
|
|
338
|
+
this.rateLimiter.reset();
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Simple rate limiter implementation
|
|
343
|
+
*/
|
|
344
|
+
class RateLimiter {
|
|
345
|
+
tokens;
|
|
346
|
+
lastRefill;
|
|
347
|
+
maxTokens;
|
|
348
|
+
refillRate; // tokens per second
|
|
349
|
+
constructor(tokensPerSecond, burstSize) {
|
|
350
|
+
this.maxTokens = burstSize;
|
|
351
|
+
this.refillRate = tokensPerSecond;
|
|
352
|
+
this.tokens = burstSize;
|
|
353
|
+
this.lastRefill = Date.now();
|
|
354
|
+
}
|
|
355
|
+
allowRequest() {
|
|
356
|
+
this.refillTokens();
|
|
357
|
+
if (this.tokens >= 1) {
|
|
358
|
+
this.tokens -= 1;
|
|
359
|
+
return true;
|
|
360
|
+
}
|
|
361
|
+
return false;
|
|
362
|
+
}
|
|
363
|
+
refillTokens() {
|
|
364
|
+
const now = Date.now();
|
|
365
|
+
const timePassed = (now - this.lastRefill) / 1000;
|
|
366
|
+
const tokensToAdd = timePassed * this.refillRate;
|
|
367
|
+
this.tokens = Math.min(this.maxTokens, this.tokens + tokensToAdd);
|
|
368
|
+
this.lastRefill = now;
|
|
369
|
+
}
|
|
370
|
+
getStatus() {
|
|
371
|
+
this.refillTokens();
|
|
372
|
+
return {
|
|
373
|
+
tokens: Math.floor(this.tokens),
|
|
374
|
+
maxTokens: this.maxTokens,
|
|
375
|
+
refillRate: this.refillRate
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
reset() {
|
|
379
|
+
this.tokens = this.maxTokens;
|
|
380
|
+
this.lastRefill = Date.now();
|
|
381
|
+
}
|
|
382
|
+
}
|