"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