UNPKG

askexperts

Version:

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

422 lines 14.7 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"; /** * WalletServer class that provides an HTTP API for WalletClient operations */ export class WalletServer { /** * Creates a new WalletServer instance * * @param options - Configuration options */ constructor(options) { this.stopped = true; 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 wallet client this.walletClient = 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 wallets endpoint this.app.get(`${path}wallets`, this.handleListWallets.bind(this)); // Get wallet by ID endpoint this.app.get(`${path}wallets/:id`, this.handleGetWallet.bind(this)); // Get wallet by name endpoint this.app.get(`${path}wallets/name/:name`, this.handleGetWalletByName.bind(this)); // Get default wallet endpoint this.app.get(`${path}wallets/default`, this.handleGetDefaultWallet.bind(this)); // Insert wallet endpoint this.app.post(`${path}wallets`, this.handleInsertWallet.bind(this)); // Update wallet endpoint this.app.put(`${path}wallets/:id`, this.handleUpdateWallet.bind(this)); // Delete wallet endpoint this.app.delete(`${path}wallets/:id`, this.handleDeleteWallet.bind(this)); } /** * Handles requests to list all wallets * * @param req - Express request object * @param res - Express response object * @private */ async handleListWallets(req, res) { if (this.stopped) { res.status(503).json({ error: "Service unavailable" }); return; } try { let wallets; // Check if we have listIds in the perms object if (req.perms?.listIds !== undefined) { // Use the listWalletsByIds method with the provided string IDs wallets = await this.walletClient.listWalletsByIds(req.perms.listIds); } else { // Use the regular listWallets method wallets = await this.walletClient.listWallets(); } res.status(200).json(wallets); } catch (error) { debugError("Error handling list wallets 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 a wallet by ID * * @param req - Express request object * @param res - Express response object * @private */ async handleGetWallet(req, res) { if (this.stopped) { res.status(503).json({ error: "Service unavailable" }); return; } try { const id = req.params.id; if (!id) { res.status(400).json({ error: "Invalid wallet ID" }); return; } const wallet = await this.walletClient.getWallet(id); if (!wallet) { res.status(404).json({ error: "Wallet not found" }); return; } res.status(200).json(wallet); } catch (error) { debugError("Error handling get wallet 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 a wallet by name * * @param req - Express request object * @param res - Express response object * @private */ async handleGetWalletByName(req, res) { if (this.stopped) { res.status(503).json({ error: "Service unavailable" }); return; } try { const name = req.params.name; if (!name) { res.status(400).json({ error: "Missing wallet name" }); return; } const wallet = await this.walletClient.getWalletByName(name); if (!wallet) { res.status(404).json({ error: "Wallet not found" }); return; } res.status(200).json(wallet); } catch (error) { debugError("Error handling get wallet by name 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 the default wallet * * @param req - Express request object * @param res - Express response object * @private */ async handleGetDefaultWallet(req, res) { if (this.stopped) { res.status(503).json({ error: "Service unavailable" }); return; } try { const wallet = await this.walletClient.getDefaultWallet(); if (!wallet) { res.status(404).json({ error: "Default wallet not found" }); return; } res.status(200).json(wallet); } catch (error) { debugError("Error handling get default wallet 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 wallet * * @param req - Express request object * @param res - Express response object * @private */ async handleInsertWallet(req, res) { if (this.stopped) { res.status(503).json({ error: "Service unavailable" }); return; } try { const wallet = req.body; if (!wallet || !wallet.name || !wallet.nwc) { res.status(400).json({ error: "Invalid wallet data" }); return; } // Use user_id from the request object if available if (req.user_id) { wallet.user_id = req.user_id; } const id = await this.walletClient.insertWallet(wallet); res.status(201).json({ id }); } catch (error) { debugError("Error handling insert wallet 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 wallet * * @param req - Express request object * @param res - Express response object * @private */ async handleUpdateWallet(req, res) { if (this.stopped) { res.status(503).json({ error: "Service unavailable" }); return; } try { const id = req.params.id; if (!id) { res.status(400).json({ error: "Invalid wallet ID" }); return; } const wallet = req.body; if (!wallet) { res.status(400).json({ error: "Invalid wallet data" }); return; } // Ensure the ID in the URL matches the ID in the body if (wallet.id !== id) { res.status(400).json({ error: "ID mismatch" }); return; } // Use user_id from the request object if available if (req.user_id) { wallet.user_id = req.user_id; } const success = await this.walletClient.updateWallet(wallet); if (success) { res.status(200).json({ success: true }); } else { res.status(404).json({ error: "Wallet not found" }); } } catch (error) { debugError("Error handling update wallet 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 a wallet * * @param req - Express request object * @param res - Express response object * @private */ async handleDeleteWallet(req, res) { if (this.stopped) { res.status(503).json({ error: "Service unavailable" }); return; } try { const id = req.params.id; if (!id) { res.status(400).json({ error: "Invalid wallet ID" }); return; } const success = await this.walletClient.deleteWallet(id); if (success) { res.status(200).json({ success: true }); } else { res.status(404).json({ error: "Wallet not found" }); } } catch (error) { debugError("Error handling delete wallet 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(`Wallet 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=WalletServer.js.map