@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.
Files changed (80) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +249 -158
  3. package/cli.js +1 -1
  4. package/config.d.ts +77 -1
  5. package/index.d.ts +1 -1
  6. package/index.js +1 -1
  7. package/lib/ai/context.js +150 -0
  8. package/lib/ai/guardrails.js +382 -0
  9. package/lib/ai/integration.js +397 -0
  10. package/lib/ai/intent.js +237 -0
  11. package/lib/ai/manualPromise.js +111 -0
  12. package/lib/ai/memory.js +273 -0
  13. package/lib/ai/ml-scorer.js +265 -0
  14. package/lib/ai/orchestrator-tools.js +292 -0
  15. package/lib/ai/orchestrator.js +473 -0
  16. package/lib/ai/planner.js +300 -0
  17. package/lib/ai/reporter.js +493 -0
  18. package/lib/ai/workflow.js +407 -0
  19. package/lib/auth/apiKeyAuth.js +46 -0
  20. package/lib/auth/entraAuth.js +110 -0
  21. package/lib/auth/entraJwtVerifier.js +117 -0
  22. package/lib/auth/index.js +210 -0
  23. package/lib/auth/managedIdentityAuth.js +175 -0
  24. package/lib/auth/mcpOAuthProvider.js +186 -0
  25. package/lib/auth/tunnelAuth.js +120 -0
  26. package/lib/browserContextFactory.js +1 -1
  27. package/lib/browserServer.js +1 -1
  28. package/lib/cdpRelay.js +2 -2
  29. package/lib/common.js +68 -0
  30. package/lib/config.js +62 -3
  31. package/lib/connection.js +1 -1
  32. package/lib/context.js +1 -1
  33. package/lib/fileUtils.js +1 -1
  34. package/lib/guardrails.js +382 -0
  35. package/lib/health.js +178 -0
  36. package/lib/httpServer.js +1 -1
  37. package/lib/index.js +1 -1
  38. package/lib/javascript.js +1 -1
  39. package/lib/manualPromise.js +1 -1
  40. package/lib/memory.js +273 -0
  41. package/lib/openapi.js +373 -0
  42. package/lib/orchestrator.js +473 -0
  43. package/lib/package.js +1 -1
  44. package/lib/pageSnapshot.js +17 -2
  45. package/lib/planner.js +302 -0
  46. package/lib/program.js +17 -5
  47. package/lib/reporter.js +493 -0
  48. package/lib/resources/resource.js +1 -1
  49. package/lib/server.js +5 -3
  50. package/lib/tab.js +1 -1
  51. package/lib/tools/ai-native.js +298 -0
  52. package/lib/tools/autonomous.js +147 -0
  53. package/lib/tools/clock.js +183 -0
  54. package/lib/tools/common.js +1 -1
  55. package/lib/tools/console.js +1 -1
  56. package/lib/tools/diagnostics.js +132 -0
  57. package/lib/tools/dialogs.js +1 -1
  58. package/lib/tools/emulation.js +155 -0
  59. package/lib/tools/files.js +1 -1
  60. package/lib/tools/install.js +1 -1
  61. package/lib/tools/keyboard.js +1 -1
  62. package/lib/tools/navigate.js +1 -1
  63. package/lib/tools/network.js +1 -1
  64. package/lib/tools/pageSnapshot.js +58 -0
  65. package/lib/tools/pdf.js +1 -1
  66. package/lib/tools/profiles.js +76 -25
  67. package/lib/tools/screenshot.js +1 -1
  68. package/lib/tools/scroll.js +93 -0
  69. package/lib/tools/snapshot.js +1 -1
  70. package/lib/tools/storage.js +328 -0
  71. package/lib/tools/tab.js +16 -0
  72. package/lib/tools/tabs.js +1 -1
  73. package/lib/tools/testing.js +1 -1
  74. package/lib/tools/tool.js +1 -1
  75. package/lib/tools/utils.js +1 -1
  76. package/lib/tools/vision.js +1 -1
  77. package/lib/tools/wait.js +1 -1
  78. package/lib/tools.js +22 -1
  79. package/lib/transport.js +251 -31
  80. package/package.json +54 -21
@@ -0,0 +1,210 @@
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 { createEntraIDAuthenticator } from './entraAuth.js';
17
+ import { createApiKeyAuthenticatorFromEnv } from './apiKeyAuth.js';
18
+ import { createDevTunnelAuthenticator } from './tunnelAuth.js';
19
+ import { initializeManagedIdentityAuth, createManagedIdentityConfig, } from './managedIdentityAuth.js';
20
+ /**
21
+ * Unified Authenticator that combines all auth methods
22
+ */
23
+ export class UnifiedAuthenticator {
24
+ config;
25
+ entraAuth;
26
+ apiKeyAuth;
27
+ tunnelAuth;
28
+ managedIdentityConfig;
29
+ managedIdentityInitialized = false;
30
+ constructor(config) {
31
+ // Build configuration from environment and overrides
32
+ this.config = {
33
+ allowAnonymous: process.env.ALLOW_ANONYMOUS_ACCESS === 'true',
34
+ enableApiKey: process.env.API_KEY_AUTH_ENABLED === 'true',
35
+ enableEntra: process.env.ENTRA_AUTH_ENABLED === 'true',
36
+ enableTunnel: process.env.TUNNEL_AUTH_ENABLED === 'true' ||
37
+ !!process.env.TUNNEL_URL ||
38
+ !!process.env.CODESPACE_NAME,
39
+ enableManagedIdentity: process.env.MANAGED_IDENTITY_ENABLED === 'true' ||
40
+ process.env.AZURE_USE_MANAGED_IDENTITY === 'true' ||
41
+ !!process.env.IDENTITY_ENDPOINT,
42
+ requiredRoles: process.env.REQUIRED_ROLES?.split(',').map(r => r.trim()),
43
+ ...config,
44
+ };
45
+ this.entraAuth = createEntraIDAuthenticator();
46
+ this.apiKeyAuth = createApiKeyAuthenticatorFromEnv();
47
+ this.tunnelAuth = createDevTunnelAuthenticator();
48
+ this.managedIdentityConfig = createManagedIdentityConfig();
49
+ }
50
+ /**
51
+ * Initialize async authentication providers
52
+ */
53
+ async initialize() {
54
+ // Initialize Managed Identity if enabled
55
+ if (this.config.enableManagedIdentity && !this.managedIdentityInitialized) {
56
+ this.managedIdentityInitialized = await initializeManagedIdentityAuth();
57
+ if (this.managedIdentityInitialized) {
58
+ // Reload Entra auth with secrets from Key Vault
59
+ this.entraAuth = createEntraIDAuthenticator();
60
+ }
61
+ }
62
+ }
63
+ /**
64
+ * Check if any authentication is enabled
65
+ */
66
+ isAuthEnabled() {
67
+ return this.config.enableApiKey ||
68
+ this.config.enableEntra ||
69
+ this.config.enableTunnel ||
70
+ this.config.enableManagedIdentity;
71
+ }
72
+ /**
73
+ * Check if a user has the required roles
74
+ */
75
+ hasRequiredRoles(user) {
76
+ if (!this.config.requiredRoles || this.config.requiredRoles.length === 0)
77
+ return true;
78
+ return this.config.requiredRoles.some(role => user.roles.includes(role));
79
+ }
80
+ /**
81
+ * Authenticate a request using all available methods
82
+ */
83
+ async authenticate(req) {
84
+ // If no auth is enabled and anonymous access is allowed
85
+ if (!this.isAuthEnabled() || this.config.allowAnonymous) {
86
+ return {
87
+ authenticated: true,
88
+ method: 'anonymous',
89
+ };
90
+ }
91
+ // 1. Check dev tunnel first (highest priority - trusted transport)
92
+ if (this.config.enableTunnel && this.tunnelAuth.isTunnelRequest(req)) {
93
+ const tunnelResult = await this.tunnelAuth.authenticate(req);
94
+ if (tunnelResult.authenticated) {
95
+ return {
96
+ authenticated: true,
97
+ method: 'tunnel',
98
+ tunnel: tunnelResult,
99
+ user: {
100
+ userId: tunnelResult.githubUser || 'tunnel-user',
101
+ tenantId: 'github',
102
+ roles: ['user'],
103
+ permissions: ['browser:read', 'browser:write'],
104
+ },
105
+ };
106
+ }
107
+ }
108
+ // 2. Check Entra ID Bearer token
109
+ if (this.config.enableEntra) {
110
+ const user = await this.entraAuth.authenticate(req);
111
+ if (user) {
112
+ // Check RBAC roles if configured
113
+ if (!this.hasRequiredRoles(user)) {
114
+ return {
115
+ authenticated: false,
116
+ method: 'none',
117
+ error: 'Insufficient permissions. Required roles: ' + this.config.requiredRoles?.join(', '),
118
+ };
119
+ }
120
+ return {
121
+ authenticated: true,
122
+ method: 'entra',
123
+ user,
124
+ };
125
+ }
126
+ }
127
+ // 3. Check API Key (legacy)
128
+ if (this.config.enableApiKey && this.apiKeyAuth.authenticate(req)) {
129
+ return {
130
+ authenticated: true,
131
+ method: 'api-key',
132
+ user: {
133
+ userId: 'api-key-user',
134
+ tenantId: 'local',
135
+ roles: ['user'],
136
+ permissions: ['browser:read', 'browser:write'],
137
+ },
138
+ };
139
+ }
140
+ // 4. Check Managed Identity context
141
+ // This is for Azure-to-Azure calls where the caller is another Azure service
142
+ if (this.config.enableManagedIdentity && this.managedIdentityInitialized) {
143
+ // For Azure service-to-service, the token would come in the Bearer header
144
+ // and be validated through the Entra flow above. This is a fallback
145
+ // for when we're making outbound calls.
146
+ return {
147
+ authenticated: false,
148
+ method: 'none',
149
+ error: 'Managed Identity is configured but no valid token provided',
150
+ };
151
+ }
152
+ return {
153
+ authenticated: false,
154
+ method: 'none',
155
+ error: 'No valid authentication provided',
156
+ };
157
+ }
158
+ /**
159
+ * Express-style middleware
160
+ */
161
+ middleware() {
162
+ return async (req, res, next) => {
163
+ const result = await this.authenticate(req);
164
+ if (!result.authenticated) {
165
+ res.statusCode = 401;
166
+ res.setHeader('Content-Type', 'application/json');
167
+ res.end(JSON.stringify({
168
+ error: 'unauthorized',
169
+ message: result.error || 'Authentication required',
170
+ methods_available: this.getAvailableMethods(),
171
+ }));
172
+ return;
173
+ }
174
+ // Attach auth result to request
175
+ req.auth = result;
176
+ req.user = result.user;
177
+ next();
178
+ };
179
+ }
180
+ /**
181
+ * Get list of available authentication methods
182
+ */
183
+ getAvailableMethods() {
184
+ const methods = [];
185
+ if (this.config.enableTunnel)
186
+ methods.push('VS Code Dev Tunnel');
187
+ if (this.config.enableEntra)
188
+ methods.push('Entra ID Bearer Token');
189
+ if (this.config.enableApiKey)
190
+ methods.push('API Key (X-API-Key header)');
191
+ if (this.config.enableManagedIdentity)
192
+ methods.push('Azure Managed Identity');
193
+ if (this.config.allowAnonymous)
194
+ methods.push('Anonymous');
195
+ return methods;
196
+ }
197
+ }
198
+ /**
199
+ * Create a unified authenticator from environment
200
+ */
201
+ export function createUnifiedAuthenticator(config) {
202
+ return new UnifiedAuthenticator(config);
203
+ }
204
+ // Re-export all auth modules
205
+ export * from './entraAuth.js';
206
+ export * from './apiKeyAuth.js';
207
+ export * from './tunnelAuth.js';
208
+ export * from './managedIdentityAuth.js';
209
+ export * from './mcpOAuthProvider.js';
210
+ export * from './entraJwtVerifier.js';
@@ -0,0 +1,175 @@
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
+ * Azure Managed Identity Authentication
18
+ *
19
+ * Enables automatic authentication for Azure services using Managed Identity.
20
+ * This eliminates the need for API keys or secrets when running in Azure.
21
+ *
22
+ * Supports:
23
+ * - System-assigned Managed Identity
24
+ * - User-assigned Managed Identity
25
+ * - Azure Key Vault secret retrieval
26
+ */
27
+ import { DefaultAzureCredential, ManagedIdentityCredential } from '@azure/identity';
28
+ import { SecretClient } from '@azure/keyvault-secrets';
29
+ // Cached credential instance
30
+ let cachedCredential = null;
31
+ let cachedSecretClient = null;
32
+ /**
33
+ * Get Azure credential for Managed Identity
34
+ */
35
+ export function getManagedIdentityCredential(config) {
36
+ if (cachedCredential)
37
+ return cachedCredential;
38
+ if (config.userAssignedClientId) {
39
+ // User-assigned managed identity
40
+ cachedCredential = new ManagedIdentityCredential(config.userAssignedClientId);
41
+ }
42
+ else {
43
+ // System-assigned or default credential chain
44
+ cachedCredential = new DefaultAzureCredential();
45
+ }
46
+ return cachedCredential;
47
+ }
48
+ /**
49
+ * Verify that Managed Identity is available and working
50
+ */
51
+ export async function verifyManagedIdentity(config) {
52
+ if (!config.enabled) {
53
+ return {
54
+ authenticated: false,
55
+ identityType: 'none',
56
+ error: 'Managed Identity is not enabled',
57
+ };
58
+ }
59
+ try {
60
+ const credential = getManagedIdentityCredential(config);
61
+ // Try to get a token for Azure Resource Manager to verify identity works
62
+ const token = await credential.getToken('https://management.azure.com/.default');
63
+ if (token) {
64
+ return {
65
+ authenticated: true,
66
+ identityType: config.userAssignedClientId ? 'user-assigned' : 'system',
67
+ clientId: config.userAssignedClientId,
68
+ };
69
+ }
70
+ return {
71
+ authenticated: false,
72
+ identityType: 'none',
73
+ error: 'Failed to acquire token',
74
+ };
75
+ }
76
+ catch (error) {
77
+ return {
78
+ authenticated: false,
79
+ identityType: 'none',
80
+ error: error.message || 'Managed Identity authentication failed',
81
+ };
82
+ }
83
+ }
84
+ /**
85
+ * Get a secret from Azure Key Vault using Managed Identity
86
+ */
87
+ export async function getKeyVaultSecret(secretName, config) {
88
+ if (!config.keyVaultUrl) {
89
+ // eslint-disable-next-line no-console
90
+ console.error('[ManagedIdentity] Key Vault URL not configured');
91
+ return null;
92
+ }
93
+ try {
94
+ if (!cachedSecretClient) {
95
+ const credential = getManagedIdentityCredential(config);
96
+ cachedSecretClient = new SecretClient(config.keyVaultUrl, credential);
97
+ }
98
+ const secret = await cachedSecretClient.getSecret(secretName);
99
+ return secret.value || null;
100
+ }
101
+ catch (error) {
102
+ // eslint-disable-next-line no-console
103
+ console.error(`[ManagedIdentity] Failed to get secret '${secretName}':`, error.message);
104
+ return null;
105
+ }
106
+ }
107
+ /**
108
+ * Load configuration from Key Vault secrets
109
+ */
110
+ export async function loadSecretsFromKeyVault(config) {
111
+ const secrets = {};
112
+ if (!config.keyVaultUrl || !config.enabled)
113
+ return secrets;
114
+ // Standard secret names to load
115
+ const secretNames = [
116
+ 'AZURE-TENANT-ID',
117
+ 'AZURE-CLIENT-ID',
118
+ 'AZURE-CLIENT-SECRET',
119
+ ];
120
+ for (const secretName of secretNames) {
121
+ const value = await getKeyVaultSecret(secretName, config);
122
+ if (value) {
123
+ // Convert Key Vault naming (AZURE-TENANT-ID) to env var naming (AZURE_TENANT_ID)
124
+ const envName = secretName.replace(/-/g, '_');
125
+ secrets[envName] = value;
126
+ }
127
+ }
128
+ return secrets;
129
+ }
130
+ /**
131
+ * Create Managed Identity configuration from environment
132
+ */
133
+ export function createManagedIdentityConfig() {
134
+ return {
135
+ enabled: process.env.MANAGED_IDENTITY_ENABLED === 'true' ||
136
+ process.env.AZURE_USE_MANAGED_IDENTITY === 'true' ||
137
+ // Auto-detect Azure environment
138
+ !!process.env.IDENTITY_ENDPOINT,
139
+ userAssignedClientId: process.env.AZURE_CLIENT_ID_MANAGED_IDENTITY,
140
+ keyVaultUrl: process.env.AZURE_KEY_VAULT_URL || process.env.KEY_VAULT_URL,
141
+ };
142
+ }
143
+ /**
144
+ * Initialize authentication using Managed Identity
145
+ * Loads secrets from Key Vault and sets environment variables
146
+ */
147
+ export async function initializeManagedIdentityAuth() {
148
+ const config = createManagedIdentityConfig();
149
+ if (!config.enabled) {
150
+ // eslint-disable-next-line no-console
151
+ console.error('[ManagedIdentity] Not enabled or not running in Azure');
152
+ return false;
153
+ }
154
+ // Verify Managed Identity is working
155
+ const verifyResult = await verifyManagedIdentity(config);
156
+ if (!verifyResult.authenticated) {
157
+ // eslint-disable-next-line no-console
158
+ console.error('[ManagedIdentity] Verification failed:', verifyResult.error);
159
+ return false;
160
+ }
161
+ // eslint-disable-next-line no-console
162
+ console.error(`[ManagedIdentity] Authenticated using ${verifyResult.identityType} identity`);
163
+ // Load secrets from Key Vault if configured
164
+ if (config.keyVaultUrl) {
165
+ const secrets = await loadSecretsFromKeyVault(config);
166
+ for (const [key, value] of Object.entries(secrets)) {
167
+ if (!process.env[key]) {
168
+ process.env[key] = value;
169
+ // eslint-disable-next-line no-console
170
+ console.error(`[ManagedIdentity] Loaded ${key} from Key Vault`);
171
+ }
172
+ }
173
+ }
174
+ return true;
175
+ }
@@ -0,0 +1,186 @@
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
+ * MCP OAuth Provider using Entra ID as the upstream authorization server.
18
+ *
19
+ * This implements the MCP OAuth protocol by proxying to Microsoft Entra ID,
20
+ * enabling VS Code's MCP client to authenticate users through standard OAuth flow.
21
+ *
22
+ * Supports Dynamic Client Registration (RFC 7591) for VS Code compatibility.
23
+ */
24
+ import crypto from 'node:crypto';
25
+ import { ProxyOAuthServerProvider } from '@modelcontextprotocol/sdk/server/auth/providers/proxyProvider.js';
26
+ import { verifyEntraJwt } from './entraJwtVerifier.js';
27
+ /**
28
+ * Get Entra ID OAuth endpoints for a given tenant
29
+ */
30
+ function getEntraEndpoints(tenantId) {
31
+ const authority = `https://login.microsoftonline.com/${tenantId}`;
32
+ return {
33
+ authorizationUrl: `${authority}/oauth2/v2.0/authorize`,
34
+ tokenUrl: `${authority}/oauth2/v2.0/token`,
35
+ // We handle dynamic client registration ourselves
36
+ registrationUrl: undefined,
37
+ // Entra doesn't have a standard revocation endpoint
38
+ revocationUrl: undefined,
39
+ };
40
+ }
41
+ /**
42
+ * Verify an Entra ID access token and return AuthInfo
43
+ */
44
+ async function verifyEntraToken(token, config) {
45
+ const payload = await verifyEntraJwt(token, {
46
+ tenantId: config.tenantId,
47
+ clientId: config.clientId,
48
+ clientSecret: config.clientSecret,
49
+ });
50
+ return {
51
+ token,
52
+ clientId: config.clientId,
53
+ scopes: payload.scp?.split(' ') || [],
54
+ expiresAt: payload.exp ? payload.exp * 1000 : undefined,
55
+ // Additional user info from token
56
+ extra: {
57
+ sub: payload.sub,
58
+ oid: payload.oid,
59
+ tid: payload.tid,
60
+ roles: payload.roles,
61
+ },
62
+ };
63
+ }
64
+ /**
65
+ * In-memory OAuth clients store with dynamic registration support.
66
+ * This allows VS Code's MCP client to register automatically.
67
+ */
68
+ class DynamicClientsStore {
69
+ clients = new Map();
70
+ config;
71
+ constructor(config) {
72
+ this.config = config;
73
+ // Pre-register our own Azure AD app as a known client
74
+ this.registerStaticClient(config);
75
+ }
76
+ /**
77
+ * Register the main Azure AD app as a static client
78
+ */
79
+ registerStaticClient(config) {
80
+ this.clients.set(config.clientId, {
81
+ client_id: config.clientId,
82
+ client_secret: config.clientSecret,
83
+ redirect_uris: [
84
+ // VS Code localhost redirect (accepts any port)
85
+ 'http://127.0.0.1/callback',
86
+ // VS Code web redirect
87
+ 'https://vscode.dev/redirect',
88
+ // Azure AD redirect
89
+ `${config.serverBaseUrl}/auth/callback`,
90
+ ],
91
+ grant_types: ['authorization_code', 'refresh_token'],
92
+ response_types: ['code'],
93
+ token_endpoint_auth_method: 'client_secret_post',
94
+ client_name: 'Darbot Browser MCP',
95
+ scope: 'openid profile email User.Read',
96
+ client_id_issued_at: Math.floor(Date.now() / 1000),
97
+ });
98
+ }
99
+ /**
100
+ * Get a registered client by ID
101
+ */
102
+ async getClient(clientId) {
103
+ return this.clients.get(clientId);
104
+ }
105
+ /**
106
+ * Dynamic Client Registration (RFC 7591)
107
+ * VS Code's MCP client will call this to register itself automatically.
108
+ * We create a client that proxies to our Entra ID app.
109
+ */
110
+ async registerClient(clientMetadata) {
111
+ // Generate a unique client ID for this dynamically registered client
112
+ const clientId = `vscode-mcp-${crypto.randomUUID()}`;
113
+ const clientIdIssuedAt = Math.floor(Date.now() / 1000);
114
+ // For dynamically registered clients, we use our Entra app's credentials
115
+ // This is safe because the OAuth flow still goes through Entra ID validation
116
+ const registeredClient = {
117
+ ...clientMetadata,
118
+ client_id: clientId,
119
+ client_id_issued_at: clientIdIssuedAt,
120
+ // Use our Entra app's secret - this allows the proxy to work
121
+ client_secret: this.config.clientSecret,
122
+ // Ensure proper redirect URIs are set
123
+ redirect_uris: clientMetadata.redirect_uris || [
124
+ 'http://127.0.0.1/callback',
125
+ 'https://vscode.dev/redirect',
126
+ ],
127
+ // Set default grant types if not provided
128
+ grant_types: clientMetadata.grant_types || ['authorization_code', 'refresh_token'],
129
+ response_types: clientMetadata.response_types || ['code'],
130
+ token_endpoint_auth_method: clientMetadata.token_endpoint_auth_method || 'client_secret_post',
131
+ };
132
+ this.clients.set(clientId, registeredClient);
133
+ // eslint-disable-next-line no-console
134
+ console.error(`[OAuth] Dynamic client registered: ${clientId} (${clientMetadata.client_name || 'unnamed'})`);
135
+ return registeredClient;
136
+ }
137
+ }
138
+ /**
139
+ * Create an MCP OAuth provider that proxies to Entra ID
140
+ * with support for Dynamic Client Registration
141
+ */
142
+ export function createMcpOAuthProvider(config) {
143
+ const endpoints = getEntraEndpoints(config.tenantId);
144
+ const clientsStore = new DynamicClientsStore(config);
145
+ const options = {
146
+ endpoints,
147
+ verifyAccessToken: async (token) => verifyEntraToken(token, config),
148
+ getClient: async (clientId) => clientsStore.getClient(clientId),
149
+ };
150
+ const provider = new ProxyOAuthServerProvider(options);
151
+ // Override the clientsStore to enable dynamic registration
152
+ // The SDK checks clientsStore.registerClient to determine if registration is supported
153
+ Object.defineProperty(provider, 'clientsStore', {
154
+ get: () => clientsStore,
155
+ configurable: true,
156
+ });
157
+ // Skip local PKCE validation since Entra handles it
158
+ provider.skipLocalPkceValidation = true;
159
+ return provider;
160
+ }
161
+ /**
162
+ * Check if OAuth is properly configured
163
+ */
164
+ export function isOAuthConfigured() {
165
+ return !!(process.env.AZURE_TENANT_ID &&
166
+ process.env.AZURE_CLIENT_ID &&
167
+ process.env.AZURE_CLIENT_SECRET);
168
+ }
169
+ /**
170
+ * Get OAuth configuration from environment
171
+ */
172
+ export function getOAuthConfig() {
173
+ const tenantId = process.env.AZURE_TENANT_ID;
174
+ const clientId = process.env.AZURE_CLIENT_ID;
175
+ const clientSecret = process.env.AZURE_CLIENT_SECRET;
176
+ const serverBaseUrl = process.env.SERVER_BASE_URL || 'https://darbot-browser-mcp.azurewebsites.net';
177
+ if (!tenantId || !clientId || !clientSecret) {
178
+ return null;
179
+ }
180
+ return {
181
+ tenantId,
182
+ clientId,
183
+ clientSecret,
184
+ serverBaseUrl,
185
+ };
186
+ }
@@ -0,0 +1,120 @@
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
+ * Check if request is coming through a VS Code dev tunnel
18
+ */
19
+ export function isDevTunnelRequest(req, config) {
20
+ if (!config.enabled)
21
+ return false;
22
+ // Check X-Forwarded-Host for tunnel domain
23
+ const forwardedHost = req.headers['x-forwarded-host'];
24
+ if (forwardedHost) {
25
+ return config.allowedDomains.some(domain => forwardedHost.endsWith(domain));
26
+ }
27
+ // Check Host header
28
+ const host = req.headers.host;
29
+ if (host) {
30
+ return config.allowedDomains.some(domain => host.endsWith(domain));
31
+ }
32
+ return false;
33
+ }
34
+ /**
35
+ * Extract tunnel information from request headers
36
+ */
37
+ export function extractTunnelInfo(req) {
38
+ // VS Code dev tunnels add specific headers when proxying requests
39
+ // X-VS-Tunnel-User: GitHub username
40
+ // X-VS-Tunnel-Session: Session ID
41
+ // X-Original-URL: Original URL before tunnel
42
+ const tunnelUser = req.headers['x-vs-tunnel-user'];
43
+ const tunnelSession = req.headers['x-vs-tunnel-session'];
44
+ // Alternative headers that GitHub Codespaces uses
45
+ const codespaceUser = req.headers['x-github-user'];
46
+ const forwardedUser = req.headers['x-forwarded-user'];
47
+ const githubUser = tunnelUser || codespaceUser || forwardedUser;
48
+ if (githubUser) {
49
+ return {
50
+ authenticated: true,
51
+ githubUser,
52
+ tunnelId: tunnelSession,
53
+ };
54
+ }
55
+ // If we're in a tunnel but no user header, the tunnel is in public mode
56
+ // This is acceptable for public tunnels
57
+ return {
58
+ authenticated: true,
59
+ tunnelId: tunnelSession,
60
+ };
61
+ }
62
+ /**
63
+ * Authenticate a request from a dev tunnel
64
+ */
65
+ export async function authenticateTunnelRequest(req, config) {
66
+ if (!isDevTunnelRequest(req, config)) {
67
+ return {
68
+ authenticated: false,
69
+ error: 'Not a dev tunnel request',
70
+ };
71
+ }
72
+ return extractTunnelInfo(req);
73
+ }
74
+ /**
75
+ * Create tunnel auth configuration from environment
76
+ */
77
+ export function createTunnelAuthConfig() {
78
+ const domains = process.env.TUNNEL_ALLOWED_DOMAINS || '.devtunnels.ms,.tunnels.api.visualstudio.com,.github.dev';
79
+ return {
80
+ enabled: process.env.TUNNEL_AUTH_ENABLED === 'true' ||
81
+ process.env.VS_TUNNEL_ENABLED === 'true' ||
82
+ // Auto-detect if we're in a tunnel
83
+ !!process.env.TUNNEL_URL ||
84
+ !!process.env.CODESPACE_NAME,
85
+ allowedDomains: domains.split(',').map(d => d.trim()),
86
+ trustForwardedHeaders: process.env.TRUST_PROXY === 'true' ||
87
+ process.env.NODE_ENV === 'production',
88
+ };
89
+ }
90
+ /**
91
+ * Dev Tunnel Authenticator class for middleware use
92
+ */
93
+ export class DevTunnelAuthenticator {
94
+ config;
95
+ constructor(config) {
96
+ const defaultConfig = createTunnelAuthConfig();
97
+ this.config = { ...defaultConfig, ...config };
98
+ }
99
+ get enabled() {
100
+ return this.config.enabled;
101
+ }
102
+ /**
103
+ * Check if request is from a dev tunnel
104
+ */
105
+ isTunnelRequest(req) {
106
+ return isDevTunnelRequest(req, this.config);
107
+ }
108
+ /**
109
+ * Authenticate a tunnel request
110
+ */
111
+ async authenticate(req) {
112
+ return authenticateTunnelRequest(req, this.config);
113
+ }
114
+ }
115
+ /**
116
+ * Create a dev tunnel authenticator from environment
117
+ */
118
+ export function createDevTunnelAuthenticator() {
119
+ return new DevTunnelAuthenticator();
120
+ }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Copyright (c) Microsoft Corporation.
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.