From 7fe264b81daef1c1abd96e33ae11e6e3ed2a4930 Mon Sep 17 00:00:00 2001 From: DanielEmmanuel1 Date: Thu, 2 Oct 2025 14:10:29 +0100 Subject: [PATCH 1/2] feat: add logging --- services/stellar-wallet/.env.example | 3 +- services/stellar-wallet/.gitignore | 1 + services/stellar-wallet/package.json | 1 + services/stellar-wallet/src/index.ts | 8 +- .../stellar-wallet/src/middlewares/logger.ts | 71 +++++++++++++++++ .../stellar-wallet/src/routes/kyc-verify.ts | 4 +- services/stellar-wallet/src/routes/kyc.ts | 9 ++- services/stellar-wallet/src/routes/wallet.ts | 6 +- services/stellar-wallet/src/stellar/sign.ts | 12 ++- .../tests/middlewares/logger.test.ts | 77 +++++++++++++++++++ 10 files changed, 183 insertions(+), 9 deletions(-) create mode 100644 services/stellar-wallet/src/middlewares/logger.ts create mode 100644 services/stellar-wallet/tests/middlewares/logger.test.ts diff --git a/services/stellar-wallet/.env.example b/services/stellar-wallet/.env.example index ce2ba26..10e4554 100644 --- a/services/stellar-wallet/.env.example +++ b/services/stellar-wallet/.env.example @@ -12,4 +12,5 @@ RATE_LIMIT_WINDOW_MS=900000 RATE_LIMIT_MAX=100 STELLAR_SECRET_KEY=SXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX -SOROBAN_CONTRACT_ID=CXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX \ No newline at end of file +SOROBAN_CONTRACT_ID=CXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX +LOG_LEVEL=info \ No newline at end of file diff --git a/services/stellar-wallet/.gitignore b/services/stellar-wallet/.gitignore index 0061604..d5e520d 100644 --- a/services/stellar-wallet/.gitignore +++ b/services/stellar-wallet/.gitignore @@ -1,3 +1,4 @@ +logs/ node_modules/ .env src/config/db.sqlite diff --git a/services/stellar-wallet/package.json b/services/stellar-wallet/package.json index 3625f93..92942b1 100644 --- a/services/stellar-wallet/package.json +++ b/services/stellar-wallet/package.json @@ -45,6 +45,7 @@ "express-rate-limit": "^8.1.0", "sqlite3": "^5.1.7", "supertest": "^7.1.4", + "winston": "3.18.3", "zod": "^4.1.1" } } diff --git a/services/stellar-wallet/src/index.ts b/services/stellar-wallet/src/index.ts index e895926..6c1a0d4 100644 --- a/services/stellar-wallet/src/index.ts +++ b/services/stellar-wallet/src/index.ts @@ -1,6 +1,7 @@ import cors from 'cors' import express, { type NextFunction, type Request, type Response } from 'express' import envs from './config/envs' +import { logger, loggerMiddleware, logError } from './middlewares/logger' import { authLimiter, kycLimiter, walletLimiter } from './middlewares/rate-limit' import { kycRouter } from './routes/kyc' import { kycVerifyRouter } from './routes/kyc-verify' @@ -9,6 +10,7 @@ import { walletRouter } from './routes/wallet' export const app = express() // Middlewares +app.use(loggerMiddleware) app.use(cors()) app.use(express.json()) @@ -32,12 +34,12 @@ app.use((_req: Request, res: Response) => { }) // 500 Error Handler -app.use((err: Error, _req: Request, res: Response, _next: NextFunction) => { - console.error('Internal server error:', err) +app.use((err: Error, req: Request, res: Response, _next: NextFunction) => { + logError(err, { route: req.originalUrl ?? req.url }) res.status(500).json({ error: 'Internal server error' }) }) // Start server app.listen(envs.PORT, () => { - console.log(`🚀 Server running at http://localhost:${envs.PORT}`) + logger.info({ message: 'server_started', port: envs.PORT }) }) diff --git a/services/stellar-wallet/src/middlewares/logger.ts b/services/stellar-wallet/src/middlewares/logger.ts new file mode 100644 index 0000000..b882242 --- /dev/null +++ b/services/stellar-wallet/src/middlewares/logger.ts @@ -0,0 +1,71 @@ +import type { NextFunction, Request, Response } from 'express' +import fs from 'node:fs' +import path from 'node:path' +import winston from 'winston' + +const logsDir = path.join(process.cwd(), 'services', 'stellar-wallet', 'logs') +if (!fs.existsSync(logsDir)) { + fs.mkdirSync(logsDir, { recursive: true }) +} + +const level = process.env.LOG_LEVEL ?? 'info' + +export const logger = winston.createLogger({ + level, + format: winston.format.combine(winston.format.timestamp(), winston.format.json()), + transports: [ + new winston.transports.Console(), + new winston.transports.File({ filename: path.join(logsDir, 'app.log') }), + ], +}) + +/** Extract user id if present on request (from auth middleware or header). */ +function getRequestUserId(req: Request): string | undefined { + const anyReq = req as unknown as { user?: { id?: string | number } } + if (anyReq.user?.id) return String(anyReq.user.id) + const headerUserId = req.header('x-user-id') + return headerUserId ?? undefined +} + +/** Express middleware to log requests and responses with duration. */ +export function loggerMiddleware(req: Request, res: Response, next: NextFunction): void { + const start = process.hrtime.bigint() + + logger.info({ + message: 'incoming_request', + method: req.method, + url: req.originalUrl ?? req.url, + user_id: getRequestUserId(req), + host: req.hostname, + }) + + res.on('finish', () => { + const end = process.hrtime.bigint() + const durationMs = Number(end - start) / 1_000_000 + logger.info({ + message: 'request_completed', + method: req.method, + url: req.originalUrl ?? req.url, + status: res.statusCode, + duration_ms: Number(durationMs.toFixed(2)), + user_id: getRequestUserId(req), + }) + }) + + res.on('close', () => { + // In case the connection is aborted + logger.warn?.({ + message: 'request_aborted', + method: req.method, + url: req.originalUrl ?? req.url, + user_id: getRequestUserId(req), + }) + }) + + next() +} + +export function logError(error: unknown, context?: Record): void { + const message = error instanceof Error ? error.message : 'unknown_error' + logger.error({ message, stack: error instanceof Error ? error.stack : undefined, ...context }) +} diff --git a/services/stellar-wallet/src/routes/kyc-verify.ts b/services/stellar-wallet/src/routes/kyc-verify.ts index 51ca69c..03b2b01 100644 --- a/services/stellar-wallet/src/routes/kyc-verify.ts +++ b/services/stellar-wallet/src/routes/kyc-verify.ts @@ -5,6 +5,7 @@ import { connectDB, findKycById, run } from '../db/kyc' import { validateKycData } from '../kyc/validate' import { connectSoroban } from '../soroban/client' import envs from '../config/envs' +import { logger, logError } from '../middlewares/logger' export const kycVerifyRouter = Router() @@ -103,9 +104,10 @@ kycVerifyRouter.post('/verify', async (req: Request, res: Response) => { status: 'approved', } + logger.info({ message: 'kyc_registered_soroban', kyc_id, data_hash: dataHash }) return res.status(201).json(verifyResponse) } catch (error) { - console.error('KYC verification error:', error) + logError(error, { route: '/kyc/verify' }) return res.status(500).json({ error: 'Failed to register KYC' }) } }) diff --git a/services/stellar-wallet/src/routes/kyc.ts b/services/stellar-wallet/src/routes/kyc.ts index d95d7bd..a2f8f40 100644 --- a/services/stellar-wallet/src/routes/kyc.ts +++ b/services/stellar-wallet/src/routes/kyc.ts @@ -1,6 +1,7 @@ import { type Request, type Response, Router } from 'express' import { type KycRow, all, connectDB, initializeKycTable, run } from '../db/kyc' import { validateKycData } from '../kyc/validate' +import { logger, logError } from '../middlewares/logger' export const kycRouter = Router() @@ -31,9 +32,15 @@ kycRouter.post('/submit', async (req: Request, res: Response) => { const row = rows[0] if (!row) return res.status(500).json({ error: 'failed to persist kyc record' }) + logger.info({ + message: 'kyc_submitted', + kyc_id: row.id, + document_hash: Buffer.from(document).toString('base64'), + user_id: undefined, + }) return res.status(201).json(row) } catch (err) { - console.error('kyc submit error:', err) + logError(err, { route: '/kyc/submit' }) return res.status(500).json({ error: 'Internal server error' }) } }) diff --git a/services/stellar-wallet/src/routes/wallet.ts b/services/stellar-wallet/src/routes/wallet.ts index 63eba68..f7e3154 100644 --- a/services/stellar-wallet/src/routes/wallet.ts +++ b/services/stellar-wallet/src/routes/wallet.ts @@ -4,6 +4,7 @@ import { connectDB, findKycById, initializeAccountsTable, insertAccount } from ' import { fundAccount } from '../stellar/fund' import { generateKeyPair } from '../stellar/keys' import { encryptPrivateKey, getEncryptionKey } from '../utils/encryption' +import { logger, logError } from '../middlewares/logger' export const walletRouter = Router() @@ -42,7 +43,7 @@ walletRouter.post('/create', async (req: Request, res: Response) => { await fundAccount(publicKey) } catch (err) { // Funding or network error → client can retry later - console.error('friendbot funding failed:', err) + logError(err, { route: '/wallet/create', stage: 'friendbot_fund' }) return res.status(400).json({ error: 'Failed to create account' }) } @@ -57,10 +58,11 @@ walletRouter.post('/create', async (req: Request, res: Response) => { }) // respond + logger.info({ message: 'wallet_created', user_id, public_key: publicKey }) return res.status(201).json({ user_id, public_key: publicKey }) } catch (err) { // Never leak secrets - console.error('wallet/create error:', err) + logError(err, { route: '/wallet/create' }) return res.status(500).json({ error: 'Failed to create account' }) } }) diff --git a/services/stellar-wallet/src/stellar/sign.ts b/services/stellar-wallet/src/stellar/sign.ts index 93be1d2..7c88393 100644 --- a/services/stellar-wallet/src/stellar/sign.ts +++ b/services/stellar-wallet/src/stellar/sign.ts @@ -3,6 +3,7 @@ import type sqlite3 from 'sqlite3' import { connectDB } from '../db/kyc' import { findAccountByUserId } from '../db/kyc' import { decryptPrivateKey, getEncryptionKey } from '../utils/encryption' +import { logger, logError } from '../middlewares/logger' /** Union type for Stellar base transaction types we can sign. */ export type StellarTx = Transaction | FeeBumpTransaction @@ -19,6 +20,7 @@ export type StellarTx = Transaction | FeeBumpTransaction export async function getPrivateKey(userId: number, db?: sqlite3.Database): Promise { const conn = db ?? (await connectDB()) const row = await findAccountByUserId(conn, userId) + logger.debug({ message: 'get_private_key_lookup', user_id: userId, found: Boolean(row) }) if (!row) { throw new Error('Account not found') @@ -30,8 +32,10 @@ export async function getPrivateKey(userId: number, db?: sqlite3.Database): Prom let secret: string try { secret = decryptPrivateKey(encrypted, key) - } catch { + logger.debug({ message: 'private_key_decrypted', user_id: userId }) + } catch (err) { // Normalize all decryption/format errors to the required message + logError(err, { user_id: userId, context: 'decrypt_private_key' }) throw new Error('Decryption failed') } @@ -56,10 +60,16 @@ export async function signTransaction( tx: StellarTx, db?: sqlite3.Database, ): Promise { + logger.debug({ message: 'sign_transaction_start', user_id: userId }) const secret = await getPrivateKey(userId, db) const keypair = Keypair.fromSecret(secret) // Side-effect: signs the instance in-place (SDK API) tx.sign(keypair) + logger.debug({ + message: 'sign_transaction_success', + user_id: userId, + tx_hash: (tx as any).hash?.().toString('hex'), + }) return tx } diff --git a/services/stellar-wallet/tests/middlewares/logger.test.ts b/services/stellar-wallet/tests/middlewares/logger.test.ts new file mode 100644 index 0000000..c647bd9 --- /dev/null +++ b/services/stellar-wallet/tests/middlewares/logger.test.ts @@ -0,0 +1,77 @@ +import express from 'express' +import request from 'supertest' + +jest.mock('winston', () => { + const info = jest.fn() + const error = jest.fn() + const debug = jest.fn() + return { + format: { combine: jest.fn(), timestamp: jest.fn(), json: jest.fn() }, + transports: { Console: jest.fn(), File: jest.fn() }, + createLogger: jest.fn(() => ({ info, error, debug })), + __mocks: { info, error, debug }, + } +}) + +import { loggerMiddleware, logError } from '../../src/middlewares/logger' + +describe('logger middleware', () => { + let app: express.Application + + beforeEach(() => { + jest.clearAllMocks() + app = express() + app.use(express.json()) + app.use(loggerMiddleware) + app.get('/ok', (_req, res) => res.status(200).json({ ok: true })) + app.get('/user', (req, res) => { + ;(req as any).user = { id: 42 } + return res.status(200).json({ ok: true }) + }) + app.get('/error', () => { + throw new Error('boom') + }) + // error handler to capture thrown errors + app.use( + (err: Error, _req: express.Request, res: express.Response, _next: express.NextFunction) => { + logError(err, { route: '/error' }) + res.status(500).json({ error: 'internal' }) + }, + ) + }) + + it('logs method and url on request', async () => { + const winston = require('winston') + await request(app).get('/ok').expect(200) + expect(winston.__mocks.info).toHaveBeenCalled() + const calls = (winston.__mocks.info as jest.Mock).mock.calls + const hasIncoming = calls.some( + (args: any[]) => + args[0]?.message === 'incoming_request' && + args[0]?.url === '/ok' && + args[0]?.method === 'GET', + ) + const hasCompleted = calls.some( + (args: any[]) => args[0]?.message === 'request_completed' && args[0]?.url === '/ok', + ) + expect(hasIncoming).toBe(true) + expect(hasCompleted).toBe(true) + }) + + it('includes user_id when available', async () => { + const winston = require('winston') + await request(app).get('/user').expect(200) + const calls = (winston.__mocks.info as jest.Mock).mock.calls + const hasUser = calls.some((args: any[]) => args[0]?.user_id === '42') + expect(hasUser).toBe(true) + }) + + it('logs errors via logError helper', async () => { + const winston = require('winston') + await request(app).get('/error').expect(500) + expect(winston.__mocks.error).toHaveBeenCalled() + const calls = (winston.__mocks.error as jest.Mock).mock.calls + const hasBoom = calls.some((args: any[]) => String(args[0]?.message).includes('boom')) + expect(hasBoom).toBe(true) + }) +}) From 9b96d26d701ef7f4e12ac9cf2c77db4cbbde76ac Mon Sep 17 00:00:00 2001 From: DanielEmmanuel1 Date: Fri, 3 Oct 2025 01:09:40 +0100 Subject: [PATCH 2/2] update lock file --- package-lock.json | 195 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 193 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index edc1da8..8efa5e7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1488,6 +1488,15 @@ "node": ">=14.21.3" } }, + "node_modules/@colors/colors": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", + "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", + "license": "MIT", + "engines": { + "node": ">=0.1.90" + } + }, "node_modules/@commitlint/cli": { "version": "19.8.1", "resolved": "https://registry.npmjs.org/@commitlint/cli/-/cli-19.8.1.tgz", @@ -1906,6 +1915,17 @@ "node": ">=18" } }, + "node_modules/@dabh/diagnostics": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.8.tgz", + "integrity": "sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q==", + "license": "MIT", + "dependencies": { + "@so-ric/colorspace": "^1.1.6", + "enabled": "2.0.x", + "kuler": "^2.0.0" + } + }, "node_modules/@dimforge/rapier3d-compat": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/@dimforge/rapier3d-compat/-/rapier3d-compat-0.12.0.tgz", @@ -6861,6 +6881,62 @@ "@sinonjs/commons": "^3.0.1" } }, + "node_modules/@so-ric/colorspace": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@so-ric/colorspace/-/colorspace-1.1.6.tgz", + "integrity": "sha512-/KiKkpHNOBgkFJwu9sh48LkHSMYGyuTcSFK/qMBdnOAlrRJzRSXAOFB5qwzaVQuDl8wAvHVMkaASQDReTahxuw==", + "license": "MIT", + "dependencies": { + "color": "^5.0.2", + "text-hex": "1.0.x" + } + }, + "node_modules/@so-ric/colorspace/node_modules/color": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/color/-/color-5.0.2.tgz", + "integrity": "sha512-e2hz5BzbUPcYlIRHo8ieAhYgoajrJr+hWoceg6E345TPsATMUKqDgzt8fSXZJJbxfpiPzkWyphz8yn8At7q3fA==", + "license": "MIT", + "dependencies": { + "color-convert": "^3.0.1", + "color-string": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@so-ric/colorspace/node_modules/color-convert": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-3.1.2.tgz", + "integrity": "sha512-UNqkvCDXstVck3kdowtOTWROIJQwafjOfXSmddoDrXo4cewMKmusCeF22Q24zvjR8nwWib/3S/dfyzPItPEiJg==", + "license": "MIT", + "dependencies": { + "color-name": "^2.0.0" + }, + "engines": { + "node": ">=14.6" + } + }, + "node_modules/@so-ric/colorspace/node_modules/color-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-2.0.2.tgz", + "integrity": "sha512-9vEt7gE16EW7Eu7pvZnR0abW9z6ufzhXxGXZEVU9IqPdlsUiMwJeJfRtq0zePUmnbHGT9zajca7mX8zgoayo4A==", + "license": "MIT", + "engines": { + "node": ">=12.20" + } + }, + "node_modules/@so-ric/colorspace/node_modules/color-string": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-2.1.2.tgz", + "integrity": "sha512-RxmjYxbWemV9gKu4zPgiZagUxbH3RQpEIO77XoSSX0ivgABDZ+h8Zuash/EMFLTI4N9QgFPOJ6JQpPZKFxa+dA==", + "license": "MIT", + "dependencies": { + "color-name": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@solana-program/compute-budget": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/@solana-program/compute-budget/-/compute-budget-0.8.0.tgz", @@ -9313,6 +9389,12 @@ "meshoptimizer": "~0.22.0" } }, + "node_modules/@types/triple-beam": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", + "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", + "license": "MIT" + }, "node_modules/@types/trusted-types": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", @@ -10991,7 +11073,6 @@ "version": "3.2.6", "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", - "dev": true, "license": "MIT" }, "node_modules/async-function": { @@ -13546,6 +13627,12 @@ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "license": "MIT" }, + "node_modules/enabled": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", + "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==", + "license": "MIT" + }, "node_modules/encode-utf8": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/encode-utf8/-/encode-utf8-1.0.3.tgz", @@ -15033,6 +15120,12 @@ "is-retry-allowed": "^3.0.0" } }, + "node_modules/fecha": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", + "license": "MIT" + }, "node_modules/fetch-blob": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", @@ -15227,6 +15320,12 @@ "dev": true, "license": "ISC" }, + "node_modules/fn.name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", + "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==", + "license": "MIT" + }, "node_modules/follow-redirects": { "version": "1.15.11", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", @@ -16917,7 +17016,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -18849,6 +18947,12 @@ "integrity": "sha512-8t6Q3TclQ4uZynJY9IGr2+SsIGwK9JHcO6ootkHCGA0CrQCRy+VkouYNO2xicET6b9al7QKzpebNow+gkpCL8g==", "license": "MIT" }, + "node_modules/kuler": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", + "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==", + "license": "MIT" + }, "node_modules/language-subtag-registry": { "version": "0.3.23", "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", @@ -19119,6 +19223,23 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/logform": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz", + "integrity": "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==", + "license": "MIT", + "dependencies": { + "@colors/colors": "1.6.0", + "@types/triple-beam": "^1.3.2", + "fecha": "^4.2.0", + "ms": "^2.1.1", + "safe-stable-stringify": "^2.3.1", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, "node_modules/long": { "version": "5.2.5", "resolved": "https://registry.npmjs.org/long/-/long-5.2.5.tgz", @@ -20514,6 +20635,15 @@ "wrappy": "1" } }, + "node_modules/one-time": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", + "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", + "license": "MIT", + "dependencies": { + "fn.name": "1.x.x" + } + }, "node_modules/onetime": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", @@ -23274,6 +23404,15 @@ "dev": true, "license": "MIT" }, + "node_modules/stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/stack-utils": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", @@ -24274,6 +24413,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/text-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", + "license": "MIT" + }, "node_modules/thenify": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", @@ -24604,6 +24749,15 @@ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", "license": "MIT" }, + "node_modules/triple-beam": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", + "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==", + "license": "MIT", + "engines": { + "node": ">= 14.0.0" + } + }, "node_modules/troika-three-text": { "version": "0.52.4", "resolved": "https://registry.npmjs.org/troika-three-text/-/troika-three-text-0.52.4.tgz", @@ -26201,6 +26355,42 @@ "bs58check": "^4.0.0" } }, + "node_modules/winston": { + "version": "3.18.3", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.18.3.tgz", + "integrity": "sha512-NoBZauFNNWENgsnC9YpgyYwOVrl2m58PpQ8lNHjV3kosGs7KJ7Npk9pCUE+WJlawVSe8mykWDKWFSVfs3QO9ww==", + "license": "MIT", + "dependencies": { + "@colors/colors": "^1.6.0", + "@dabh/diagnostics": "^2.0.8", + "async": "^3.2.3", + "is-stream": "^2.0.0", + "logform": "^2.7.0", + "one-time": "^1.0.0", + "readable-stream": "^3.4.0", + "safe-stable-stringify": "^2.3.1", + "stack-trace": "0.0.x", + "triple-beam": "^1.3.0", + "winston-transport": "^4.9.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/winston-transport": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz", + "integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==", + "license": "MIT", + "dependencies": { + "logform": "^2.7.0", + "readable-stream": "^3.6.2", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -26475,6 +26665,7 @@ "jsonwebtoken": "^9.0.2", "sqlite3": "^5.1.7", "supertest": "^7.1.4", + "winston": "3.18.3", "zod": "^4.1.1" }, "devDependencies": {