UNPKG

askexperts

Version:

AskExperts SDK: build and use AI experts - ask them questions and pay with bitcoin on an open protocol

404 lines 14.4 kB
import express from "express"; import cors from "cors"; import { debugClient, debugError } from "../common/debug.js"; import { parseAuthToken } from "../common/auth.js"; import { getDB } from "../db/utils.js"; /** * ExpertServer class that provides an HTTP API for ExpertClient operations */ export class ExpertServer { /** * Creates a new ExpertServer instance * * @param options - Configuration options or port number (for backward compatibility) * @param basePath - Base path for the API (e.g., '/api') when using port number constructor */ constructor(options) { this.stopped = true; // New constructor with options object this.port = options.port; this.basePath = options.basePath ? options.basePath.startsWith("/") ? options.basePath : `/${options.basePath}` : ""; this.perms = options.perms; this.serverOrigin = options.origin || `http://localhost:${this.port}`; // Get the expert client this.expertClient = getDB(); // Create the Express app this.app = express(); // Configure middleware this.app.use(cors()); this.app.use(express.json({ limit: "1mb", verify: (req, res, buf) => { req.rawBody = buf; }, })); // Add authentication middleware if perms is provided if (this.perms) { this.app.use(this.authMiddleware.bind(this)); } // Set up routes this.setupRoutes(); } /** * Authentication middleware * Parses the auth token and checks permissions */ async authMiddleware(req, res, next) { try { // Convert the request to AuthRequest const authReq = { headers: req.headers, method: req.method, originalUrl: req.originalUrl, rawBody: req.rawBody, }; // Parse the auth token const pubkey = await parseAuthToken(this.serverOrigin, authReq); // If pubkey is empty, authentication failed if (!pubkey) { debugError("Authentication failed: Invalid or missing token"); res.status(401).json({ error: "Unauthorized", message: "Invalid or missing authentication token", }); return; } // Store the pubkey in the request for later use req.pubkey = pubkey; // Check permissions if perms is provided if (this.perms) { try { // Get user_id and store it in the request const user_id = await this.perms.getUserId(pubkey); req.user_id = user_id; const permsResult = await this.perms.checkPerms(user_id, req); // Store the perms result in the request for later use req.perms = permsResult; } catch (error) { const errorMessage = error instanceof Error ? error.message : "Permission denied for this operation"; debugError(`Permission check error: ${errorMessage}`); res.status(403).json({ error: "Forbidden", message: errorMessage }); return; } } // Authentication and authorization successful, proceed to the route handler next(); } catch (error) { debugError("Authentication error:", error); res.status(500).json({ error: "Internal Server Error", message: "Authentication error", }); } } /** * Sets up the API routes * @private */ setupRoutes() { // Add a leading slash if basePath is not empty const path = this.basePath ? this.basePath.endsWith("/") ? this.basePath : `${this.basePath}/` : "/"; // Health check endpoint this.app.get(`${path}health`, (req, res) => { if (this.stopped) res.status(503).json({ error: "Service unavailable" }); else res.status(200).json({ status: "ok" }); }); // List experts endpoint this.app.get(`${path}experts`, this.handleListExperts.bind(this)); // Get expert endpoint this.app.get(`${path}experts/:pubkey`, this.handleGetExpert.bind(this)); // Insert expert endpoint this.app.post(`${path}experts`, this.handleInsertExpert.bind(this)); // Update expert endpoint this.app.put(`${path}experts/:pubkey`, this.handleUpdateExpert.bind(this)); // Set expert disabled status endpoint this.app.patch(`${path}experts/:pubkey/disabled`, this.handleSetExpertDisabled.bind(this)); // Delete expert endpoint this.app.delete(`${path}experts/:pubkey`, this.handleDeleteExpert.bind(this)); } /** * Handles requests to list all experts * * @param req - Express request object * @param res - Express response object * @private */ async handleListExperts(req, res) { if (this.stopped) { res.status(503).json({ error: "Service unavailable" }); return; } try { let experts; // Check if we have listIds in the perms object if (req.perms?.listIds !== undefined) { // Use the listExpertsByIds method with the provided IDs experts = await this.expertClient.listExpertsByIds(req.perms.listIds); } else { // Use the regular listExperts method experts = await this.expertClient.listExperts(); } res.status(200).json(experts); } catch (error) { debugError("Error handling list experts request:", error); const message = error instanceof Error ? error.message : String(error); res.status(500).json({ error: "Internal server error", message: message, }); } } /** * Handles requests to get an expert by pubkey * * @param req - Express request object * @param res - Express response object * @private */ async handleGetExpert(req, res) { if (this.stopped) { res.status(503).json({ error: "Service unavailable" }); return; } try { const pubkey = req.params.pubkey; if (!pubkey) { res.status(400).json({ error: "Missing pubkey parameter" }); return; } const expert = await this.expertClient.getExpert(pubkey); if (!expert) { res.status(404).json({ error: "Expert not found" }); return; } res.status(200).json(expert); } catch (error) { debugError("Error handling get expert request:", error); const message = error instanceof Error ? error.message : String(error); res.status(500).json({ error: "Internal server error", message: message, }); } } /** * Handles requests to insert a new expert * * @param req - Express request object * @param res - Express response object * @private */ async handleInsertExpert(req, res) { if (this.stopped) { res.status(503).json({ error: "Service unavailable" }); return; } try { const expert = req.body; if (!expert || !expert.pubkey) { res.status(400).json({ error: "Invalid expert data" }); return; } // Use user_id from the request object if available if (req.user_id) { expert.user_id = req.user_id; } const success = await this.expertClient.insertExpert(expert); if (success) { res.status(201).json({ success: true }); } else { res.status(400).json({ error: "Failed to insert expert" }); } } catch (error) { debugError("Error handling insert expert request:", error); const message = error instanceof Error ? error.message : String(error); res.status(500).json({ error: "Internal server error", message: message, }); } } /** * Handles requests to update an existing expert * * @param req - Express request object * @param res - Express response object * @private */ async handleUpdateExpert(req, res) { if (this.stopped) { res.status(503).json({ error: "Service unavailable" }); return; } try { const pubkey = req.params.pubkey; if (!pubkey) { res.status(400).json({ error: "Missing pubkey parameter" }); return; } const expert = req.body; if (!expert) { res.status(400).json({ error: "Invalid expert data" }); return; } // Ensure the pubkey in the URL matches the pubkey in the body if (expert.pubkey !== pubkey) { res.status(400).json({ error: "Pubkey mismatch" }); return; } // Use user_id from the request object if available if (req.user_id) { expert.user_id = req.user_id; } const success = await this.expertClient.updateExpert(expert); if (success) { res.status(200).json({ success: true }); } else { res.status(404).json({ error: "Expert not found" }); } } catch (error) { debugError("Error handling update expert request:", error); const message = error instanceof Error ? error.message : String(error); res.status(500).json({ error: "Internal server error", message: message, }); } } /** * Handles requests to set the disabled status of an expert * * @param req - Express request object * @param res - Express response object * @private */ async handleSetExpertDisabled(req, res) { if (this.stopped) { res.status(503).json({ error: "Service unavailable" }); return; } try { const pubkey = req.params.pubkey; if (!pubkey) { res.status(400).json({ error: "Missing pubkey parameter" }); return; } const { disabled } = req.body; if (disabled === undefined) { res.status(400).json({ error: "Missing disabled parameter" }); return; } const success = await this.expertClient.setExpertDisabled(pubkey, disabled); if (success) { res.status(200).json({ success: true }); } else { res.status(404).json({ error: "Expert not found" }); } } catch (error) { debugError("Error handling set expert disabled request:", error); const message = error instanceof Error ? error.message : String(error); res.status(500).json({ error: "Internal server error", message: message, }); } } /** * Handles requests to delete an expert * * @param req - Express request object * @param res - Express response object * @private */ async handleDeleteExpert(req, res) { if (this.stopped) { res.status(503).json({ error: "Service unavailable" }); return; } try { const pubkey = req.params.pubkey; if (!pubkey) { res.status(400).json({ error: "Missing pubkey parameter" }); return; } const success = await this.expertClient.deleteExpert(pubkey); if (success) { res.status(200).json({ success: true }); } else { res.status(404).json({ error: "Expert not found" }); } } catch (error) { debugError("Error handling delete expert request:", error); const message = error instanceof Error ? error.message : String(error); res.status(500).json({ error: "Internal server error", message: message, }); } } /** * Starts the server * * @returns Promise that resolves when the server is started */ async start() { if (this.server) throw new Error("Already started"); this.stopped = false; this.server = this.app.listen(this.port); debugClient(`Expert Server running at http://localhost:${this.port}${this.basePath}`); } /** * Stops the server * * @returns Promise that resolves when the server is stopped */ stop() { return new Promise(async (resolve) => { // Mark as stopped this.stopped = true; if (!this.server) { resolve(); return; } debugError("Server stopping..."); // Stop accepting new connections const closePromise = new Promise((ok) => this.server.close(ok)); // Wait until all connections are closed with timeout let to; await Promise.race([ closePromise, new Promise((ok) => (to = setTimeout(ok, 5000))), ]); clearTimeout(to); debugError("Server stopped"); resolve(); }); } } //# sourceMappingURL=ExpertServer.js.map