UNPKG

n8n

Version:

n8n Workflow Automation Tool

262 lines • 11.5 kB
"use strict"; var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __metadata = (this && this.__metadata) || function (k, v) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.InstanceAiBrowserSessionService = void 0; const backend_common_1 = require("@n8n/backend-common"); const config_1 = require("@n8n/config"); const db_1 = require("@n8n/db"); const di_1 = require("@n8n/di"); const n8n_workflow_1 = require("n8n-workflow"); const nanoid_1 = require("nanoid"); const node_crypto_1 = require("node:crypto"); const promises_1 = require("node:fs/promises"); const node_os_1 = require("node:os"); const node_path_1 = require("node:path"); const credentials_service_1 = require("../../../credentials/credentials.service"); const push_1 = require("../../../push"); const url_service_1 = require("../../../services/url.service"); const telemetry_1 = require("../../../telemetry"); const browser_local_mcp_server_1 = require("./browser-local-mcp-server"); const browser_use_ws_constants_1 = require("./browser-use-ws.constants"); const CONNECT_TOKEN_TTL_MS = 5 * 60 * 1000; const LOOPBACK_ADDRESSES = new Set(['127.0.0.1', '::1', '::ffff:127.0.0.1']); let InstanceAiBrowserSessionService = class InstanceAiBrowserSessionService { constructor(logger, urlService, push, userRepository, credentialsService, globalConfig, telemetry) { this.urlService = urlService; this.push = push; this.userRepository = userRepository; this.credentialsService = credentialsService; this.globalConfig = globalConfig; this.telemetry = telemetry; this.sessions = new Map(); this.sessionsBySessionId = new Map(); this.logger = logger.scoped('instance-ai'); } async createLink(userId) { const session = this.sessions.get(userId) ?? (await this.createSession(userId)); session.relayAuthToken = `bu_${(0, nanoid_1.nanoid)(32)}`; session.tokenCreatedAt = Date.now(); const { buildExtensionConnectUrl } = await import('@n8n/mcp-browser'); const relayEndpoint = this.buildExtensionEndpoint(session); const connectUrl = buildExtensionConnectUrl(relayEndpoint); const expiresAt = new Date(session.tokenCreatedAt + CONNECT_TOKEN_TTL_MS); return { connectUrl, expiresAt: expiresAt.toISOString(), ttlSeconds: Math.ceil(CONNECT_TOKEN_TTL_MS / 1000), }; } getStatus(userId) { const session = this.sessions.get(userId); return { connected: session?.connected ?? false, connectedAt: session?.connectedAt?.toISOString() ?? null, toolCategories: this.getToolCategories(userId), }; } async disconnect(userId) { const session = this.sessions.get(userId); if (!session) return; this.sessions.delete(userId); this.sessionsBySessionId.delete(session.sessionId); await this.teardownSession(session); this.pushState(userId); } findMcpServer(userId) { const session = this.sessions.get(userId); return session?.connected ? session.mcpServer : undefined; } isConnected(userId) { return this.sessions.get(userId)?.connected ?? false; } handleExtensionUpgrade(req) { const session = this.sessionsBySessionId.get(req.params.sessionId); const token = typeof req.query.token === 'string' ? req.query.token : null; if (!session || !this.isExtensionTokenValid(session, token)) { req.ws.close(4003, 'Invalid auth token'); return; } session.relay.attachExtension(req.ws); } handleCdpUpgrade(req) { const session = this.sessionsBySessionId.get(req.params.sessionId); const header = req.headers[browser_use_ws_constants_1.CDP_TOKEN_HEADER]; const token = typeof header === 'string' ? header : null; if (!session || !LOOPBACK_ADDRESSES.has(req.socket.remoteAddress ?? '') || !this.tokensMatch(session.cdpToken, token)) { req.ws.close(4003, 'Invalid auth token'); return; } session.relay.attachController(req.ws); } async shutdown() { const sessions = [...this.sessions.values()]; this.sessions.clear(); this.sessionsBySessionId.clear(); for (const session of sessions) { await this.teardownSession(session); } } async createSession(userId) { const { CDPRelayServer, createBrowserTools } = await import('@n8n/mcp-browser'); const sessionId = (0, nanoid_1.nanoid)(); const cdpToken = `cdp_${(0, nanoid_1.nanoid)(32)}`; const relay = new CDPRelayServer({ noServer: true }); relay.onExtensionConnect = () => this.handleExtensionConnected(userId); relay.onExtensionDisconnect = () => this.handleExtensionDisconnected(userId); const toolkit = createBrowserTools({ mode: 'remote' }, { relay, cdpEndpoint: this.buildCdpEndpoint(sessionId), cdpConnectHeaders: { [browser_use_ws_constants_1.CDP_TOKEN_HEADER]: cdpToken }, }); const workDir = (0, node_path_1.join)((0, node_os_1.tmpdir)(), 'n8n-instance-ai-browser', userId); await (0, promises_1.mkdir)(workDir, { recursive: true }); const toolContext = { dir: workDir, secretsBuffer: createInMemorySecretsBuffer(), createCredential: async (payload) => await this.createCredential(userId, payload), }; const session = { userId, sessionId, relay, relayAuthToken: `bu_${(0, nanoid_1.nanoid)(32)}`, cdpToken, tokenCreatedAt: Date.now(), hasConnectedOnce: false, connected: false, connectedAt: null, connection: toolkit.connection, mcpServer: new browser_local_mcp_server_1.BrowserLocalMcpServer(toolkit, toolContext, this.logger), }; this.sessions.set(userId, session); this.sessionsBySessionId.set(sessionId, session); return session; } async teardownSession(session) { try { await session.connection.shutdown(); } catch (error) { this.logger.warn('Failed to shut down browser connection', { error: error instanceof Error ? error.message : String(error), }); } session.relay.stop(); session.connected = false; session.connectedAt = null; } handleExtensionConnected(userId) { const session = this.sessions.get(userId); if (!session) { return; } session.connected = true; session.connectedAt = new Date(); session.hasConnectedOnce = true; void session.mcpServer.callTool({ name: 'browser_connect', arguments: {}, }); this.telemetry.track('Instance AI Browser connected', { user_id: userId }); this.logger.info('Browser Use extension connected', { userId }); this.pushState(userId); } handleExtensionDisconnected(userId) { const session = this.sessions.get(userId); if (!session) { return; } session.connected = false; session.connectedAt = null; this.logger.info('Browser Use extension disconnected', { userId }); this.pushState(userId); } pushState(userId) { const session = this.sessions.get(userId); this.push.sendToUsers({ type: 'instanceAiBrowserStateChanged', data: { connected: session?.connected ?? false, connectedAt: session?.connectedAt?.toISOString() ?? null, toolCategories: this.getToolCategories(userId), }, }, [userId]); } getToolCategories(userId) { const session = this.sessions.get(userId); return session?.connected ? [{ name: 'browser', enabled: true }] : []; } isExtensionTokenValid(session, token) { if (!this.tokensMatch(session.relayAuthToken, token)) { return false; } if (!session.hasConnectedOnce) { return Date.now() - session.tokenCreatedAt <= CONNECT_TOKEN_TTL_MS; } return true; } tokensMatch(expected, actual) { if (!actual) { return false; } const expectedBuffer = Buffer.from(expected); const actualBuffer = Buffer.from(actual); return (expectedBuffer.length === actualBuffer.length && (0, node_crypto_1.timingSafeEqual)(expectedBuffer, actualBuffer)); } async createCredential(userId, payload) { const user = await this.userRepository.findOne({ where: { id: userId }, relations: ['role'], }); if (!user) { throw new n8n_workflow_1.UnexpectedError('User for browser session not found'); } const credential = await this.credentialsService.createUnmanagedCredential(payload, user); return { credentialId: credential.id }; } buildExtensionEndpoint(session) { const token = encodeURIComponent(session.relayAuthToken); return `${this.getPublicWsBaseUrl()}${browser_use_ws_constants_1.BROWSER_USE_WS_NAMESPACE}/extension/${session.sessionId}?token=${token}`; } buildCdpEndpoint(sessionId) { return `ws://127.0.0.1:${this.globalConfig.port}${browser_use_ws_constants_1.BROWSER_USE_WS_NAMESPACE}/cdp/${sessionId}`; } getPublicWsBaseUrl() { const base = new URL(this.urlService.getInstanceBaseUrl()); const scheme = base.protocol === 'https:' ? 'wss' : 'ws'; return `${scheme}://${base.host}`; } }; exports.InstanceAiBrowserSessionService = InstanceAiBrowserSessionService; exports.InstanceAiBrowserSessionService = InstanceAiBrowserSessionService = __decorate([ (0, di_1.Service)(), __metadata("design:paramtypes", [backend_common_1.Logger, url_service_1.UrlService, push_1.Push, db_1.UserRepository, credentials_service_1.CredentialsService, config_1.GlobalConfig, telemetry_1.Telemetry]) ], InstanceAiBrowserSessionService); function createInMemorySecretsBuffer() { const store = new Map(); return { capture(credentialsKey, field, value) { const fields = store.get(credentialsKey) ?? new Map(); fields.set(field, value); store.set(credentialsKey, fields); }, getFields(credentialsKey) { return store.get(credentialsKey); }, clear(credentialsKey) { store.delete(credentialsKey); }, }; } //# sourceMappingURL=instance-ai-browser-session.service.js.map