UNPKG

plugin-connections

Version:

Connection management plugin for Twitter authentication and other services

926 lines (916 loc) 31.7 kB
import { TwitterApi } from "./chunk-3TTRDC2T.js"; // src/schema/service-credentials.ts import { uuid, timestamp, jsonb, boolean, index, unique, pgSchema } from "drizzle-orm/pg-core"; import { sql } from "drizzle-orm"; var pluginConnectionsSchema = pgSchema("plugin_connections"); var serviceTypeEnum = pluginConnectionsSchema.enum("service_type", [ "twitter", "discord", "telegram", "github", "google", "facebook", "linkedin", "instagram", "tiktok", "youtube", "other" ]); var serviceCredentialsTable = pluginConnectionsSchema.table( "service_credentials", { id: uuid("id").primaryKey().default(sql`gen_random_uuid()`), agentId: uuid("agent_id").notNull(), serviceName: serviceTypeEnum("service_name").notNull(), credentials: jsonb("credentials").notNull().default("{}"), isActive: boolean("is_active").default(true).notNull(), expiresAt: timestamp("expires_at", { withTimezone: true }), createdAt: timestamp("created_at", { withTimezone: true }).default(sql`now()`).notNull(), updatedAt: timestamp("updated_at", { withTimezone: true }).default(sql`now()`).notNull() }, (table) => [ // Indexes for performance index("service_credentials_agent_id_idx").on(table.agentId), index("service_credentials_service_name_idx").on(table.serviceName), index("service_credentials_is_active_idx").on(table.isActive), index("service_credentials_created_at_idx").on(table.createdAt), // Unique constraint for one active credential per agent/service unique("service_credentials_agent_service_unique").on( table.agentId, table.serviceName ) ] ); // src/schema/index.ts var schema = { serviceCredentialsTable }; // src/routes.ts import { logger as logger2 } from "@elizaos/core"; // src/utils/plugin-loader.ts import { logger } from "@elizaos/core"; async function updateAndRegisterPlugin(runtime, settingsToUpdate, pluginPackageName) { if (Object.values(settingsToUpdate).some((value) => !!value)) { logger.info(`Attempting to dynamically load/reload ${pluginPackageName}...`); try { for (const [key, value] of Object.entries(settingsToUpdate)) { runtime.setSetting(key, value); } const { default: plugin2 } = await import(pluginPackageName); await runtime.registerPlugin(plugin2); logger.info(`Successfully loaded/reloaded ${pluginPackageName}.`); } catch (error) { logger.error(`Failed to dynamically load/reload ${pluginPackageName}`, error); } } else { logger.info( `No valid credentials found for ${pluginPackageName}. Skipping plugin load.` ); } } // src/routes.ts var routes = [ // Test route to verify routes are working { name: "connections-test", path: "/connections/test", type: "GET", handler: async (req, res) => { logger2.info("\u{1F9EA} Test route called successfully!"); res.json({ message: "Connections routes are working!", timestamp: /* @__PURE__ */ new Date() }); } }, // Get all connection statuses { name: "connections-list", path: "/connections", type: "GET", handler: async (req, res) => { try { const { agentId } = req.query; if (!agentId) { return res.status(400).json({ error: "Agent ID is required" }); } const authService = req.runtime?.getService("auth"); if (!authService) { return res.status(500).json({ error: "Auth service not available" }); } const supportedServices = ["twitter"]; const connections = await Promise.all( supportedServices.map(async (service) => { const status = await authService.getConnectionStatus( agentId, service ); return { service, ...status, // Add service-specific metadata displayName: service === "twitter" ? "Twitter/X" : service, icon: service === "twitter" ? "twitter" : service, color: service === "twitter" ? "#1DA1F2" : "#6B7280", description: service === "twitter" ? "Connect to post tweets and interact with your audience" : `Connect to ${service}` }; }) ); res.json({ agentId, connections, lastUpdated: (/* @__PURE__ */ new Date()).toISOString() }); } catch (error) { logger2.error("Connections list failed:", error); res.status(500).json({ error: "Failed to fetch connections", details: error instanceof Error ? error.message : String(error) }); } } }, // Get Twitter connection status { name: "twitter-status", path: "/connections/twitter/status", type: "GET", handler: async (req, res) => { try { const { agentId } = req.query; if (!agentId) { return res.status(400).json({ error: "Agent ID is required" }); } const authService = req.runtime?.getService("auth"); if (!authService) { return res.status(500).json({ error: "Auth service not available" }); } const status = await authService.getConnectionStatus(agentId, "twitter"); res.json(status); } catch (error) { logger2.error("Twitter status check failed:", error); res.status(500).json({ error: "Failed to check Twitter status", details: error instanceof Error ? error.message : String(error) }); } } }, // TWITTER AUTHENTICATION ROUTES // Initiate Twitter OAuth connection { name: "twitter-connect", path: "/connections/twitter/connect", type: "POST", handler: async (req, res) => { try { const { agentId, returnUrl } = req.body; if (!agentId) { return res.status(400).json({ error: "Agent ID is required" }); } logger2.info(`Initiating Twitter OAuth for agent: ${agentId}`); const authService = req.runtime?.getService("auth"); if (!authService) { return res.status(500).json({ error: "Auth service not available" }); } const consumerKey = process.env.TWITTER_API_KEY; const consumerSecret = process.env.TWITTER_API_SECRET_KEY; if (!consumerKey || !consumerSecret) { return res.status(500).json({ error: "Twitter API credentials not configured. Please set TWITTER_API_KEY and TWITTER_API_SECRET_KEY." }); } const twitterApi = new TwitterApi({ appKey: consumerKey, appSecret: consumerSecret }); const callbackUrl = `${req.protocol}://${req.get("host")}/api/connections/twitter/callback`; logger2.info(`\u{1F517} Attempting to generate Twitter auth link with callback: ${callbackUrl}`); logger2.info(`\u{1F511} Using API key: ${consumerKey?.substring(0, 8)}...`); let authLink; try { authLink = await twitterApi.generateAuthLink(callbackUrl, { linkMode: "authorize" // Use 'authorize' for web apps }); logger2.info(`\u2705 Twitter auth link generated successfully: ${authLink.url}`); } catch (twitterError) { logger2.error(`\u274C Twitter API error:`, { error: twitterError, message: twitterError instanceof Error ? twitterError.message : String(twitterError), callbackUrl, consumerKey: consumerKey?.substring(0, 8) + "..." }); throw twitterError; } const state = authService.generateOAuthState(); await authService.createOAuthSession( agentId, "twitter", state, returnUrl ); authService.storeTempCredentials(authLink.oauth_token, { oauth_token_secret: authLink.oauth_token_secret, agentId, state, returnUrl }); res.json({ authUrl: authLink.url }); } catch (error) { logger2.error("Twitter OAuth initiation failed:", error); res.status(500).json({ error: "Failed to initiate Twitter authentication", details: error instanceof Error ? error.message : String(error) }); } } }, // Handle Twitter OAuth callback { name: "twitter-callback", path: "/connections/twitter/callback", type: "GET", handler: async (req, res) => { try { const { oauth_token, oauth_verifier } = req.query; if (!oauth_token || !oauth_verifier) { return res.status(400).json({ error: "Missing required OAuth parameters" }); } const authService = req.runtime?.getService("auth"); if (!authService) { return res.status(500).json({ error: "Auth service not available" }); } const tempCredentials = authService.getTempCredentials(oauth_token); if (!tempCredentials) { return res.status(400).json({ error: "Invalid or expired OAuth session" }); } const { oauth_token_secret, agentId, state, returnUrl } = tempCredentials; const consumerKey = process.env.TWITTER_API_KEY; const consumerSecret = process.env.TWITTER_API_SECRET_KEY; if (!consumerKey || !consumerSecret) { return res.status(500).json({ error: "Twitter API credentials not configured" }); } const twitterApi = new TwitterApi({ appKey: consumerKey, appSecret: consumerSecret, accessToken: oauth_token, accessSecret: oauth_token_secret }); const { accessToken, accessSecret } = await twitterApi.login(oauth_verifier); const userApi = new TwitterApi({ appKey: consumerKey, appSecret: consumerSecret, accessToken, accessSecret }); const user = await userApi.v2.me(); const credentials = { apiKey: consumerKey, apiSecretKey: consumerSecret, accessToken, accessTokenSecret: accessSecret, userId: user.data.id, username: user.data.username }; await authService.storeCredentials(agentId, "twitter", credentials); await updateAndRegisterPlugin( req.runtime, { TWITTER_ACCESS_TOKEN: credentials.accessToken, TWITTER_ACCESS_TOKEN_SECRET: credentials.accessTokenSecret }, "@elizaos/plugin-twitter" ); authService.deleteTempCredentials(oauth_token); const redirectUrl = new URL(`${req.protocol}://${req.get("host")}/chat/${agentId}`); redirectUrl.searchParams.set("oauth", "success"); redirectUrl.searchParams.set("service", "twitter"); res.redirect(redirectUrl.toString()); } catch (error) { logger2.error("Twitter OAuth callback failed:", error); res.status(500).json({ error: "Failed to complete Twitter authentication", details: error instanceof Error ? error.message : String(error) }); } } }, // Disconnect from Twitter { name: "twitter-disconnect", path: "/connections/twitter/disconnect", type: "POST", handler: async (req, res) => { try { logger2.info(`\u{1F50C} Twitter disconnect route called for URL: ${req.url}, Method: ${req.method}`); logger2.info(`\u{1F50C} Request body:`, req.body); const { agentId } = req.body; if (!agentId) { return res.status(400).json({ error: "Agent ID is required" }); } logger2.info(`Processing Twitter disconnect for agent: ${agentId}`); const authService = req.runtime?.getService("auth"); if (!authService) { return res.status(500).json({ error: "Auth service not available" }); } await authService.revokeCredentials(agentId, "twitter"); await req.runtime.updateAgent(agentId, { settings: { secrets: { TWITTER_ACCESS_TOKEN: null, TWITTER_ACCESS_TOKEN_SECRET: null } } }); const twitterService = req.runtime.getService("twitter"); if (twitterService && typeof twitterService.stop === "function") { logger2.warn("\u26A0\uFE0F Stopping Twitter service after disconnect"); await twitterService.stop(); } res.json({ success: true, service: "twitter", message: "Twitter connection disconnected successfully" }); } catch (error) { logger2.error("Twitter disconnect failed:", error); res.status(500).json({ error: "Failed to disconnect Twitter", details: error instanceof Error ? error.message : String(error) }); } } }, // Test Twitter connection { name: "twitter-test", path: "/connections/twitter/test", type: "POST", handler: async (req, res) => { try { const { agentId } = req.body; if (!agentId) { return res.status(400).json({ error: "Agent ID is required" }); } logger2.info(`Testing Twitter connection for agent: ${agentId}`); const authService = req.runtime?.getService("auth"); if (!authService) { return res.status(500).json({ error: "Auth service not available" }); } const credentials = await authService.getCredentials(agentId, "twitter"); if (!credentials) { return res.status(404).json({ error: "No credentials found for Twitter" }); } const testResult = await testTwitterConnection(credentials); res.json({ success: true, service: "twitter", test: testResult, timestamp: (/* @__PURE__ */ new Date()).toISOString() }); } catch (error) { logger2.error("Twitter connection test failed:", error); res.status(500).json({ error: "Failed to test Twitter connection", details: error instanceof Error ? error.message : String(error) }); } } } ]; async function testTwitterConnection(credentials) { try { const { TwitterApi: TwitterApi2 } = await import("./esm-KK6GTMEV.js"); const client = new TwitterApi2({ appKey: credentials.apiKey, appSecret: credentials.apiSecretKey, accessToken: credentials.accessToken, accessSecret: credentials.accessTokenSecret }); const user = await client.v2.me(); return { success: true, user: { id: user.data.id, username: user.data.username, name: user.data.name }, rateLimit: { remaining: user.rateLimit?.remaining, reset: user.rateLimit?.reset } }; } catch (error) { logger2.error("Twitter connection test failed:", error); return { success: false, error: error instanceof Error ? error.message : String(error) }; } } // src/services/auth.service.ts import { Service, logger as logger4, getSalt as getSalt2, encryptObjectValues } from "@elizaos/core"; import { LRUCache } from "lru-cache"; // src/utils/credentials.ts import { logger as logger3 } from "@elizaos/core"; import { eq, and } from "drizzle-orm"; import { decryptObjectValues, getSalt } from "@elizaos/core"; async function getCredentials(runtime, serviceName) { if (!runtime.db) { return null; } try { const result = await runtime.db.select().from(serviceCredentialsTable).where( and( eq(serviceCredentialsTable.agentId, runtime.agentId), eq(serviceCredentialsTable.serviceName, serviceName), eq(serviceCredentialsTable.isActive, true) ) ).limit(1); if (result.length === 0) { return null; } const credentialData = result[0].credentials; if (credentialData && credentialData.encryptedData) { const salt = getSalt(); return decryptObjectValues(credentialData.encryptedData, salt); } return credentialData; } catch (error) { logger3.error(`Failed to get credentials for ${serviceName}:`, error); return null; } } // src/services/auth.service.ts import { randomBytes } from "crypto"; var _AuthService = class _AuthService extends Service { get databaseService() { const dbService = this.runtime.getService("database"); if (!dbService) { throw new Error("Database service not available"); } return dbService; } constructor(runtime) { super(runtime); this.oauthCache = new LRUCache({ max: 1e3, ttl: 1e3 * 60 * 15 // 15 minutes }); } get capabilityDescription() { return "Authentication service for managing agent credentials to external services"; } /** * Generate a secure random state parameter for OAuth */ generateOAuthState() { return randomBytes(32).toString("hex"); } static async start(runtime) { const service = new _AuthService(runtime); return service; } static async stop(runtime) { const service = runtime.getService(_AuthService.serviceType); if (service) { await service.stop(); } } async stop() { } async storeCredentials(agentId, service, credentials) { try { const salt = getSalt2(); const encryptedCredentials = encryptObjectValues(credentials, salt); await this.databaseService.storeCredentials(agentId, service, { encryptedData: encryptedCredentials }); } catch (error) { logger4.error(`Failed to store credentials for service '${service}':`, error); throw error; } } async getCredentials(agentId, service) { return getCredentials(this.runtime, service); } async revokeCredentials(agentId, service) { if (service !== "twitter") { return; } await this.databaseService.deleteCredentials(agentId, service); } async getConnectionStatus(agentId, service) { const credentials = await this.getCredentials(agentId, service); const settingsKeys = { twitter: ["TWITTER_ACCESS_TOKEN", "TWITTER_ACCESS_TOKEN_SECRET"] }; const serviceSettingKeys = settingsKeys[service]; const settings = serviceSettingKeys?.reduce( (acc, key) => { acc[key] = this.runtime.getSetting(key); return acc; }, {} ); const settingsToCredentialKeyMap = { twitter: { TWITTER_ACCESS_TOKEN: "accessToken", TWITTER_ACCESS_TOKEN_SECRET: "accessTokenSecret" } }; const isPending = (() => { if (!credentials) { return false; } if (settings) { const keyMap = settingsToCredentialKeyMap[service]; if (!keyMap) return false; return Object.keys(settings).some((settingKey) => { const credentialKey = keyMap[settingKey]; return settings[settingKey] && settings[settingKey] !== credentials[credentialKey]; }); } return false; })(); return { serviceName: service, isConnected: !!credentials, isPending, lastChecked: /* @__PURE__ */ new Date() }; } storeTempCredentials(key, credentials) { this.oauthCache.set(`temp:${key}`, credentials); } getTempCredentials(key) { const credentials = this.oauthCache.get(`temp:${key}`); return credentials ? credentials : null; } deleteTempCredentials(key) { this.oauthCache.delete(`temp:${key}`); } async createOAuthSession(agentId, service, state, returnUrl) { const sessionData = { id: crypto.randomUUID(), agentId, serviceName: service, state, returnUrl, expiresAt: new Date(Date.now() + 15 * 60 * 1e3), // 15 minutes createdAt: /* @__PURE__ */ new Date() }; this.oauthCache.set(`session:${state}`, sessionData); } }; _AuthService.serviceType = "auth"; var AuthService = _AuthService; // src/services/database.service.ts import { Service as Service2, logger as logger5 } from "@elizaos/core"; import { eq as eq2, and as and2, sql as sql2 } from "drizzle-orm"; var _DatabaseService = class _DatabaseService extends Service2 { // The runtime is essential for database access. constructor(runtime) { super(runtime); this.runtime = runtime; this.tablesCreated = false; } get capabilityDescription() { return "Database service for managing connections plugin database operations"; } /** * Direct access to the database via the runtime. */ get db() { if (!this.runtime.db) { throw new Error("Database not available. Ensure @elizaos/plugin-sql is loaded before this plugin."); } return this.runtime.db; } /** * Creates and starts the database service. */ static async start(runtime) { logger5.info("Starting DatabaseService for connections plugin..."); const service = new _DatabaseService(runtime); await service.initialize(); logger5.info("DatabaseService for connections started successfully."); return service; } /** * Instance-specific stop method. * This service does not manage any persistent connections or intervals, * so no specific cleanup is required here. */ async stop() { } /** * Initializes the database service and creates tables if they don't exist. */ async initialize() { try { await this.createTables(); } catch (error) { logger5.error("Failed to initialize DatabaseService for connections:", error); throw error; } } /** * Creates all necessary tables for the connections plugin. */ async createTables() { if (this.tablesCreated) { return; } try { logger5.info("Attempting to create database tables for connections plugin..."); await this.db.transaction(async (tx) => { await tx.execute(sql2`CREATE SCHEMA IF NOT EXISTS plugin_connections`); await tx.execute(sql2` DO $$ BEGIN CREATE TYPE plugin_connections.service_type AS ENUM ( 'twitter', 'discord', 'telegram', 'github', 'google', 'facebook', 'linkedin', 'instagram', 'tiktok', 'youtube', 'other' ); EXCEPTION WHEN duplicate_object THEN null; END $$; `); await tx.execute(sql2` CREATE TABLE IF NOT EXISTS plugin_connections.service_credentials ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), agent_id UUID NOT NULL, service_name plugin_connections.service_type NOT NULL, credentials JSONB NOT NULL DEFAULT '{}', is_active BOOLEAN NOT NULL DEFAULT true, expires_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), CONSTRAINT service_credentials_agent_service_unique UNIQUE (agent_id, service_name) ); `); await tx.execute(sql2`CREATE INDEX IF NOT EXISTS service_credentials_agent_id_idx ON plugin_connections.service_credentials (agent_id)`); await tx.execute(sql2`CREATE INDEX IF NOT EXISTS service_credentials_service_name_idx ON plugin_connections.service_credentials (service_name)`); }); this.tablesCreated = true; logger5.info("Database tables for connections plugin created or already exist."); } catch (error) { logger5.error("Failed to create database tables for connections plugin:", error); throw error; } } /** * Validate database connection. */ async validateConnection() { try { await this.db.execute(sql2`SELECT 1`); } catch (error) { throw new Error(`Database connection failed: ${error}`); } } /** * Store encrypted credentials for a service. */ async storeCredentials(agentId, service, credentials) { try { await this.db.execute(sql2` INSERT INTO plugin_connections.service_credentials ( agent_id, service_name, credentials, is_active, updated_at ) VALUES ( ${agentId}, ${service}, ${JSON.stringify(credentials)}, true, now() ) ON CONFLICT (agent_id, service_name) DO UPDATE SET credentials = ${JSON.stringify(credentials)}, is_active = true, updated_at = now() `); } catch (error) { logger5.error(`Failed to store credentials for service '${service}':`, error); throw error; } } /** * Get encrypted credentials for a service. */ async getCredentials(agentId, service) { try { const result = await this.db.select().from(serviceCredentialsTable).where( and2( eq2(serviceCredentialsTable.agentId, agentId), eq2(serviceCredentialsTable.serviceName, service), eq2(serviceCredentialsTable.isActive, true) ) ).limit(1); if (result.length === 0) { return null; } return result[0].credentials; } catch (error) { logger5.error(`Failed to get credentials for service '${service}':`, error); throw error; } } /** * Delete credentials for a service. */ async deleteCredentials(agentId, service) { try { await this.db.delete(serviceCredentialsTable).where( and2( eq2(serviceCredentialsTable.agentId, agentId), eq2(serviceCredentialsTable.serviceName, service) ) ); } catch (error) { logger5.error(`Failed to delete credentials for service '${service}':`, error); throw error; } } /** * Get connection status for all services for an agent. */ async getConnectionStatus(agentId) { try { const credentials = await this.db.select().from(serviceCredentialsTable).where(eq2(serviceCredentialsTable.agentId, agentId)); return credentials.map((cred) => ({ serviceName: cred.serviceName, isConnected: cred.isActive === true, isPending: false, // Default to false, auth service will determine actual state lastChecked: cred.updatedAt })); } catch (error) { logger5.error("Failed to get connection status:", error); throw error; } } /** * Check if service has credentials. */ async hasCredentials(agentId, service) { try { const credentials = await this.getCredentials(agentId, service); return credentials !== null; } catch (error) { logger5.error(`Failed to check credentials for service '${service}':`, error); return false; } } }; _DatabaseService.serviceType = "database"; var DatabaseService = _DatabaseService; // src/plugin.ts import fs from "fs"; import path from "path"; import { fileURLToPath } from "url"; var __filename = fileURLToPath(import.meta.url); var __dirname = path.dirname(__filename); var frontendDist = path.resolve(__dirname, "frontend"); var frontPagePath = path.resolve(frontendDist, "index.html"); var assetsPath = path.resolve(frontendDist, "assets"); console.log("*** Using frontendDist:", frontendDist); console.log("*** frontPagePath", frontPagePath); console.log("*** assetsPath", assetsPath); var plugin = { name: "connections", description: "Connection management plugin for Twitter authentication and other services", dependencies: ["@elizaos/plugin-sql"], schema, async init(config, runtime) { console.log("Connections Plugin: Initializing..."); const credentials = await getCredentials(runtime, "twitter"); if (credentials) { await updateAndRegisterPlugin( runtime, { TWITTER_ACCESS_TOKEN: credentials.accessToken, TWITTER_ACCESS_TOKEN_SECRET: credentials.accessTokenSecret }, "@elizaos/plugin-twitter" ); } }, routes: [ // Frontend routes { type: "GET", path: "/panels/connections", name: "Connections", public: true, handler: async (_req, res, runtime) => { const connectionsHtmlPath = path.resolve(frontendDist, "index.html"); console.log("*** Checking for HTML file at:", connectionsHtmlPath); console.log("*** File exists:", fs.existsSync(connectionsHtmlPath)); if (fs.existsSync(connectionsHtmlPath)) { let htmlContent = fs.readFileSync(connectionsHtmlPath, "utf-8"); const agentId = runtime.agentId; const config = { agentId, apiBase: `http://localhost:3000` // This could be configurable }; htmlContent = htmlContent.replace( /window\.ELIZA_CONFIG = \{[^}]+\};/, `window.ELIZA_CONFIG = ${JSON.stringify(config)};` ); res.setHeader("Content-Type", "text/html"); res.setHeader("X-Frame-Options", "SAMEORIGIN"); res.setHeader("Content-Security-Policy", "frame-ancestors 'self' http://localhost:* https://localhost:*"); res.send(htmlContent); } else { res.status(404).send("Connections HTML file not found"); } } }, { type: "GET", path: "/panels/connections/assets/*", public: true, handler: async (req, res, _runtime) => { const fullPath = req.path; const assetRelativePath = fullPath.replace(/^\/api\/panels\/connections\/assets\//, "").replace(/^\/panels\/connections\/assets\//, ""); console.log("*** Connections assets - fullPath:", fullPath); console.log("*** Connections assets - assetRelativePath:", assetRelativePath); if (!assetRelativePath) { return res.status(400).send("Invalid asset path"); } const filePath = path.resolve(assetsPath, assetRelativePath); console.log("*** Connections assets - filePath:", filePath); if (!filePath.startsWith(assetsPath)) { return res.status(403).send("Forbidden"); } if (fs.existsSync(filePath)) { res.setHeader("X-Frame-Options", "SAMEORIGIN"); res.setHeader("Content-Security-Policy", "frame-ancestors 'self' http://localhost:* https://localhost:*"); res.sendFile(filePath); } else { res.status(404).send("Asset not found"); } } }, { type: "GET", path: "/assets/*", public: true, handler: async (req, res, _runtime) => { const fullPath = req.path; const assetRelativePath = fullPath.replace(/^\/api\/assets\//, "").replace(/^\/assets\//, ""); console.log("*** Direct assets - fullPath:", fullPath); console.log("*** Direct assets - assetRelativePath:", assetRelativePath); if (!assetRelativePath) { return res.status(400).send("Invalid asset path"); } const filePath = path.resolve(assetsPath, assetRelativePath); console.log("*** Direct assets - filePath:", filePath); if (!filePath.startsWith(assetsPath)) { return res.status(403).send("Forbidden"); } if (fs.existsSync(filePath)) { res.setHeader("X-Frame-Options", "SAMEORIGIN"); res.setHeader("Content-Security-Policy", "frame-ancestors 'self' http://localhost:* https://localhost:*"); res.sendFile(filePath); } else { res.status(404).send("Asset not found"); } } }, // All connection and authentication routes ...routes.map((route) => ({ ...route, handler: async (req, res, runtime) => { req.runtime = runtime; return route.handler(req, res); } })) ], services: [DatabaseService, AuthService] }; var plugin_default = plugin; export { AuthService, DatabaseService, plugin_default as default }; //# sourceMappingURL=index.js.map