@darbotlabs/darbot-browser-mcp 0.1.1 → 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 +249 -158
- 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 +54 -21
package/lib/browserServer.js
CHANGED
package/lib/cdpRelay.js
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.
|
|
@@ -161,7 +161,7 @@ export class CDPRelayServer extends EventEmitter {
|
|
|
161
161
|
result: {
|
|
162
162
|
protocolVersion: '1.3',
|
|
163
163
|
product: 'Browser/Extension-Bridge',
|
|
164
|
-
userAgent: 'CDP-Bridge-Server/1.
|
|
164
|
+
userAgent: 'CDP-Bridge-Server/1.3.0',
|
|
165
165
|
}
|
|
166
166
|
});
|
|
167
167
|
break;
|
package/lib/common.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
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 { z } from 'zod';
|
|
17
|
+
import { defineTool } from './tools/tool.js';
|
|
18
|
+
const close = defineTool({
|
|
19
|
+
capability: 'core',
|
|
20
|
+
schema: {
|
|
21
|
+
name: 'browser_close',
|
|
22
|
+
title: 'Autonomous browser closure',
|
|
23
|
+
description: 'Autonomously close the browser session and terminate all operations',
|
|
24
|
+
inputSchema: z.object({}),
|
|
25
|
+
type: 'readOnly',
|
|
26
|
+
},
|
|
27
|
+
handle: async (context) => {
|
|
28
|
+
await context.close();
|
|
29
|
+
return {
|
|
30
|
+
code: [`await page.close()`],
|
|
31
|
+
captureSnapshot: false,
|
|
32
|
+
waitForNetwork: false,
|
|
33
|
+
};
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
const resize = captureSnapshot => defineTool({
|
|
37
|
+
capability: 'core',
|
|
38
|
+
schema: {
|
|
39
|
+
name: 'browser_resize',
|
|
40
|
+
title: 'Autonomous window resizing',
|
|
41
|
+
description: 'Autonomously resize the browser window to specific dimensions for optimal viewing',
|
|
42
|
+
inputSchema: z.object({
|
|
43
|
+
width: z.number().describe('Width of the browser window'),
|
|
44
|
+
height: z.number().describe('Height of the browser window'),
|
|
45
|
+
}),
|
|
46
|
+
type: 'readOnly',
|
|
47
|
+
},
|
|
48
|
+
handle: async (context, params) => {
|
|
49
|
+
const tab = context.currentTabOrDie();
|
|
50
|
+
const code = [
|
|
51
|
+
`// Resize browser window to ${params.width}x${params.height}`,
|
|
52
|
+
`await page.setViewportSize({ width: ${params.width}, height: ${params.height} });`
|
|
53
|
+
];
|
|
54
|
+
const action = async () => {
|
|
55
|
+
await tab.page.setViewportSize({ width: params.width, height: params.height });
|
|
56
|
+
};
|
|
57
|
+
return {
|
|
58
|
+
code,
|
|
59
|
+
action,
|
|
60
|
+
captureSnapshot,
|
|
61
|
+
waitForNetwork: true
|
|
62
|
+
};
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
export default (captureSnapshot) => [
|
|
66
|
+
close,
|
|
67
|
+
resize(captureSnapshot)
|
|
68
|
+
];
|
package/lib/config.js
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.
|
|
@@ -39,7 +39,36 @@ const defaultConfig = {
|
|
|
39
39
|
allowedOrigins: undefined,
|
|
40
40
|
blockedOrigins: undefined,
|
|
41
41
|
},
|
|
42
|
-
server: {
|
|
42
|
+
server: {
|
|
43
|
+
baseUrl: process.env.SERVER_BASE_URL,
|
|
44
|
+
https: {
|
|
45
|
+
enabled: false
|
|
46
|
+
},
|
|
47
|
+
rateLimit: {
|
|
48
|
+
enabled: false,
|
|
49
|
+
windowMs: 15 * 60 * 1000, // 15 minutes
|
|
50
|
+
maxRequests: 100
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
copilotStudio: {
|
|
54
|
+
enabled: process.env.COPILOT_STUDIO_ENABLED === 'true',
|
|
55
|
+
callbackUrl: process.env.COPILOT_STUDIO_CALLBACK_URL,
|
|
56
|
+
maxConcurrentSessions: parseInt(process.env.MAX_CONCURRENT_SESSIONS || '10', 10),
|
|
57
|
+
sessionTimeoutMs: parseInt(process.env.SESSION_TIMEOUT_MS || '1800000', 10), // 30 minutes
|
|
58
|
+
auditLogging: process.env.AUDIT_LOGGING_ENABLED === 'true'
|
|
59
|
+
},
|
|
60
|
+
auth: {
|
|
61
|
+
entraId: {
|
|
62
|
+
enabled: process.env.ENTRA_AUTH_ENABLED === 'true',
|
|
63
|
+
tenantId: process.env.AZURE_TENANT_ID,
|
|
64
|
+
clientId: process.env.AZURE_CLIENT_ID,
|
|
65
|
+
clientSecret: process.env.AZURE_CLIENT_SECRET
|
|
66
|
+
},
|
|
67
|
+
apiKey: {
|
|
68
|
+
enabled: process.env.API_KEY_AUTH_ENABLED === 'true',
|
|
69
|
+
keys: process.env.API_KEYS?.split(',') || []
|
|
70
|
+
}
|
|
71
|
+
},
|
|
43
72
|
outputDir: path.join(os.tmpdir(), 'darbot-browser-mcp-output', sanitizeForFilePath(new Date().toISOString())),
|
|
44
73
|
};
|
|
45
74
|
export async function resolveConfig(config) {
|
|
@@ -124,6 +153,11 @@ export async function configFromCLIOptions(cliOptions) {
|
|
|
124
153
|
contextOptions.ignoreHTTPSErrors = true;
|
|
125
154
|
if (cliOptions.blockServiceWorkers)
|
|
126
155
|
contextOptions.serviceWorkers = 'block';
|
|
156
|
+
// Set Edge profile environment variables for session state tracking
|
|
157
|
+
if (cliOptions.edgeProfile)
|
|
158
|
+
process.env.DARBOT_EDGE_PROFILE = cliOptions.edgeProfile;
|
|
159
|
+
if (cliOptions.edgeProfileEmail)
|
|
160
|
+
process.env.DARBOT_EDGE_PROFILE_EMAIL = cliOptions.edgeProfileEmail;
|
|
127
161
|
const result = {
|
|
128
162
|
browser: {
|
|
129
163
|
browserAgent: cliOptions.browserAgent ?? process.env.PW_BROWSER_AGENT,
|
|
@@ -135,7 +169,8 @@ export async function configFromCLIOptions(cliOptions) {
|
|
|
135
169
|
cdpEndpoint: cliOptions.cdpEndpoint,
|
|
136
170
|
},
|
|
137
171
|
server: {
|
|
138
|
-
|
|
172
|
+
// Support PORT environment variable (common in cloud deployments like Azure)
|
|
173
|
+
port: cliOptions.port ?? (process.env.PORT ? parseInt(process.env.PORT, 10) : undefined),
|
|
139
174
|
host: cliOptions.host,
|
|
140
175
|
},
|
|
141
176
|
capabilities: cliOptions.caps?.split(',').map((c) => c.trim()),
|
|
@@ -198,6 +233,30 @@ function mergeConfig(base, overrides) {
|
|
|
198
233
|
server: {
|
|
199
234
|
...pickDefined(base.server),
|
|
200
235
|
...pickDefined(overrides.server),
|
|
236
|
+
https: {
|
|
237
|
+
...pickDefined(base.server?.https),
|
|
238
|
+
...pickDefined(overrides.server?.https),
|
|
239
|
+
},
|
|
240
|
+
rateLimit: {
|
|
241
|
+
...pickDefined(base.server?.rateLimit),
|
|
242
|
+
...pickDefined(overrides.server?.rateLimit),
|
|
243
|
+
}
|
|
201
244
|
},
|
|
245
|
+
copilotStudio: {
|
|
246
|
+
...pickDefined(base.copilotStudio),
|
|
247
|
+
...pickDefined(overrides.copilotStudio),
|
|
248
|
+
},
|
|
249
|
+
auth: {
|
|
250
|
+
...pickDefined(base.auth),
|
|
251
|
+
...pickDefined(overrides.auth),
|
|
252
|
+
entraId: {
|
|
253
|
+
...pickDefined(base.auth?.entraId),
|
|
254
|
+
...pickDefined(overrides.auth?.entraId),
|
|
255
|
+
},
|
|
256
|
+
apiKey: {
|
|
257
|
+
...pickDefined(base.auth?.apiKey),
|
|
258
|
+
...pickDefined(overrides.auth?.apiKey),
|
|
259
|
+
}
|
|
260
|
+
}
|
|
202
261
|
};
|
|
203
262
|
}
|
package/lib/connection.js
CHANGED
package/lib/context.js
CHANGED
package/lib/fileUtils.js
CHANGED
|
@@ -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
|
+
}
|