UNPKG

@bsv/wallet-toolbox

Version:

BRC100 conforming wallet, wallet storage and wallet signer components

330 lines 15.8 kB
"use strict"; /** * StorageServer.ts * * A server-side class that "has a" local WalletStorage (like a StorageKnex instance), * and exposes it via a JSON-RPC POST endpoint using Express. */ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.StorageServer = void 0; const express_1 = __importDefault(require("express")); const auth_express_middleware_1 = require("@bsv/auth-express-middleware"); const payment_express_middleware_1 = require("@bsv/payment-express-middleware"); const WERR_errors_1 = require("../../sdk/WERR_errors"); const WalletError_1 = require("../../sdk/WalletError"); const WalletLogger_1 = require("../../WalletLogger"); class StorageServer { constructor(storage, options) { this.app = (0, express_1.default)(); this.storage = storage; this.port = options.port; this.wallet = options.wallet; this.monetize = options.monetize; this.calculateRequestPrice = options.calculateRequestPrice; this.adminIdentityKeys = options.adminIdentityKeys; this.makeLogger = options.makeLogger; if (options['logShortReqs']) { this.setupShortReqLogging(); } this.setupRoutes(); } setupShortReqLogging() { this.app.use((req, res, next) => { var _a; const contentLength = Number(req.headers['content-length'] || 0); if (contentLength > 0 && contentLength < 1000 && req.method === 'POST') { const logObj = { source: 'StorageServer short-request-log', contentLength, contentType: req.headers['content-type'] || '-', ts: new Date().toISOString(), url: req.originalUrl, ip: req.ip || req.socket.remoteAddress, ua: req.headers['user-agent'] || '-', headers: { ...req.headers } // shallow copy }; const traceContext = (_a = (req.headers['X-Cloud-Trace-Context'] || req.headers['x-cloud-trace-context'])) === null || _a === void 0 ? void 0 : _a.split('/')[0]; if (traceContext) logObj['logging.googleapis.com/trace'] = `projects/computing-with-integrity/traces/${traceContext}`; const chunks = []; req.on('data', chunk => chunks.push(Buffer.from(chunk))); req.on('end', () => { const bodyBuffer = Buffer.concat(chunks); try { logObj.body = bodyBuffer.toString('utf8'); } catch (_a) { logObj.body = bodyBuffer.toString('hex'); logObj.bodyEncoding = 'hex'; } console.log(JSON.stringify(logObj)); }); } next(); }); } setupRoutes() { this.app.use(express_1.default.json({ limit: '30mb' })); // This allows the API to be used everywhere when CORS is enforced this.app.use((req, res, next) => { res.header('Access-Control-Allow-Origin', '*'); res.header('Access-Control-Allow-Headers', '*'); res.header('Access-Control-Allow-Methods', '*'); res.header('Access-Control-Expose-Headers', '*'); res.header('Access-Control-Allow-Private-Network', 'true'); if (req.method === 'OPTIONS') { // Handle CORS preflight requests to allow cross-origin POST/PUT requests res.sendStatus(200); } else { next(); } }); this.app.get(`/robots.txt`, (req, res) => { res.type('text/plain'); res.send(`User-agent: *\nDisallow: /`); }); this.app.get(`/`, (req, res) => { res.type('text/plain'); res.send(`BRC-100 ${this.wallet.chain}Net Storage Provider.`); }); const options = { wallet: this.wallet }; this.app.use((0, auth_express_middleware_1.createAuthMiddleware)(options)); if (this.monetize) { this.app.use((0, payment_express_middleware_1.createPaymentMiddleware)({ wallet: this.wallet, calculateRequestPrice: this.calculateRequestPrice || (() => 100) })); } // A single POST endpoint for JSON-RPC: this.app.post('/', async (req, res) => { var _a, _b, _c, _d, _e; let { jsonrpc, method, params, id } = req.body; // Basic JSON-RPC protocol checks: if (jsonrpc !== '2.0' || !method || typeof method !== 'string') { return res.status(400).json({ error: { code: -32600, message: 'Invalid Request' } }); } const logObj = { source: `StorageServer POST handler`, method, id, user: req.auth.identityKey, params: JSON.stringify(params || '').slice(0, 256) }; const traceContext = (_a = (req.headers['X-Cloud-Trace-Context'] || req.headers['x-cloud-trace-context'])) === null || _a === void 0 ? void 0 : _a.split('/')[0]; if (traceContext) logObj['logging.googleapis.com/trace'] = `projects/computing-with-integrity/traces/${traceContext}`; console.log(JSON.stringify(logObj)); try { // Dispatch the method call: if (typeof this[method] === 'function') { // if you wanted to handle certain methods on the server class itself // e.g. this['someServerMethod'](params) throw new Error('Server method dispatch not used in this approach.'); } else if (typeof this.storage[method] === 'function') { // method is on the walletStorage: // Find user switch (method) { case 'destroy': { logObj['result'] = undefined; logObj['comment'] = 'IGNORED'; console.log(JSON.stringify(logObj)); return res.json({ jsonrpc: '2.0', result: undefined, id }); } case 'getSettings': { /** */ } break; case 'findOrInsertUser': { if (params[0] !== req.auth.identityKey) throw new WERR_errors_1.WERR_UNAUTHORIZED('function may only access authenticated user.'); } break; case 'adminStats': { // TODO: add check for admin user if (params[0] !== req.auth.identityKey) throw new WERR_errors_1.WERR_UNAUTHORIZED('function may only access authenticated admin user.'); if (!this.adminIdentityKeys || !this.adminIdentityKeys.includes(req.auth.identityKey)) throw new WERR_errors_1.WERR_UNAUTHORIZED('function may only be accessed by admin user.'); } break; case 'processSyncChunk': { await this.validateParam0(params, req); //const args: RequestSyncChunkArgs = params[0] const r = params[1]; if (r.certificateFields) r.certificateFields = this.validateEntities(r.certificateFields); if (r.certificates) r.certificates = this.validateEntities(r.certificates); if (r.commissions) r.commissions = this.validateEntities(r.commissions); if (r.outputBaskets) r.outputBaskets = this.validateEntities(r.outputBaskets); if (r.outputTagMaps) r.outputTagMaps = this.validateEntities(r.outputTagMaps); if (r.outputTags) r.outputTags = this.validateEntities(r.outputTags); if (r.outputs) r.outputs = this.validateEntities(r.outputs); if (r.provenTxReqs) r.provenTxReqs = this.validateEntities(r.provenTxReqs); if (r.provenTxs) r.provenTxs = this.validateEntities(r.provenTxs); if (r.transactions) r.transactions = this.validateEntities(r.transactions); if (r.txLabelMaps) r.txLabelMaps = this.validateEntities(r.txLabelMaps); if (r.txLabels) r.txLabels = this.validateEntities(r.txLabels); if (r.user) r.user = this.validateEntity(r.user); } break; default: { await this.validateParam0(params, req); } break; } // If makeLogger is valid, setup and potentially initialize to return data let logger; if (this.makeLogger && typeof params[1] === 'object') { logger = this.makeLogger(params[1]['logger']); params[1]['logger'] = logger; logger.group(`StorageSever ${method}`); const userId = (_b = params[0]) === null || _b === void 0 ? void 0 : _b['userId']; const identityKey = (_c = params[0]) === null || _c === void 0 ? void 0 : _c['identityKey']; if (userId) logger.log(`userId: ${userId}`); if (identityKey) logger.log(`identityKey: ${identityKey}`); } try { const result = await this.storage[method](...(params || [])); if (logger) { logger.groupEnd(); (_d = logger.flush) === null || _d === void 0 ? void 0 : _d.call(logger); if (logger.isOrigin) { // Potentially only flush if isOrigin... } else if (logger.logs && typeof result === 'object') { // If not the start of logging, return logged data with result. result['log'] = { logs: logger.logs }; } } return res.json({ jsonrpc: '2.0', result, id }); } catch (eu) { (0, WalletLogger_1.logWalletError)(eu, logger, 'error executing requested method'); (_e = logger === null || logger === void 0 ? void 0 : logger.flush) === null || _e === void 0 ? void 0 : _e.call(logger); throw eu; } } else { // Unknown method return res.status(400).json({ jsonrpc: '2.0', error: { code: -32601, message: `Method not found: ${method}` }, id }); } } catch (error) { /** * Catch any thrown errors from the local walletStorage method. * * Convert errors to standard JSON object format that can be converted * back to WalletError derived objects on the client side and re-thrown. * * Uses WalletError.fromJson(<error object>) on the client side to re-create * an error object of the right class and properties. */ const json = WalletError_1.WalletError.unknownToJson(error); return res.status(200).json({ jsonrpc: '2.0', error: JSON.parse(json), id }); } }); } async validateParam0(params, req) { if (typeof params[0] !== 'object' || !params[0]) { params = [{}]; } if (params[0]['identityKey'] && params[0]['identityKey'] !== req.auth.identityKey) throw new WERR_errors_1.WERR_UNAUTHORIZED('identityKey does not match authentiation'); // console.log('looking up user with identityKey:', req.auth.identityKey) const { user, isNew } = await this.storage.findOrInsertUser(req.auth.identityKey); params[0].reqAuthUserId = user.userId; if (params[0]['identityKey']) params[0].userId = user.userId; } start() { this.server = this.app.listen(this.port, () => { console.log(`WalletStorageServer listening at http://localhost:${this.port}`); }); } async close() { if (this.server) { await this.server.close(() => { // console.log('WalletStorageServer closed') }); } } validateDate(date) { let r; if (date instanceof Date) r = date; else r = new Date(date); return r; } /** * Helper to force uniform behavior across database engines. * Use to process all individual records with time stamps retreived from database. */ validateEntity(entity, dateFields) { entity.created_at = this.validateDate(entity.created_at); entity.updated_at = this.validateDate(entity.updated_at); if (dateFields) { for (const df of dateFields) { if (entity[df]) entity[df] = this.validateDate(entity[df]); } } for (const key of Object.keys(entity)) { const val = entity[key]; if (val === null) { entity[key] = undefined; } else if (Buffer.isBuffer(val)) { entity[key] = Array.from(val); } } return entity; } /** * Helper to force uniform behavior across database engines. * Use to process all arrays of records with time stamps retreived from database. * @returns input `entities` array with contained values validated. */ validateEntities(entities, dateFields) { for (let i = 0; i < entities.length; i++) { entities[i] = this.validateEntity(entities[i], dateFields); } return entities; } } exports.StorageServer = StorageServer; //# sourceMappingURL=StorageServer.js.map