diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 17453873..c329d32f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,4 +1,4 @@ -name: Build artifact and package +name: Build and Release Pakage on: release: @@ -6,34 +6,6 @@ on: workflow_dispatch: jobs: - artifact: - runs-on: ubuntu-22.04 - steps: - - name: Checkout source code - uses: actions/checkout@v3 - - - name: Cache build frontend - uses: actions/cache@v3 - id: cache-build-frontend - with: - path: apps/frontend - key: ${{ runner.os }}-frontend-${{ github.sha }} - - - name: Cache build backend - uses: actions/cache@v3 - id: cache-build-backend - with: - path: apps/backend - key: ${{ runner.os }}-backend-${{ github.sha }} - - - name: Compress files - run: tar -czf /tmp/clnapp.tar.gz apps/frontend/build apps/backend/dist package.json package-lock.json - - - uses: actions/upload-artifact@v4 - with: - name: clnapp-build$VERSION - path: /tmp/clnapp.tar.gz - build: name: Build image runs-on: ubuntu-22.04 diff --git a/.gitignore b/.gitignore index b04bae28..ce4d4b9a 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,5 @@ application-cln.log .commando release env-local.sh +apps/frontend/build +apps/backend/dist diff --git a/README.md b/README.md index 95d809a9..28ae0e62 100644 --- a/README.md +++ b/README.md @@ -39,11 +39,13 @@ tar -xzf v0.0.1.tar.gz ``` - - ### Dependency Installation + - ### Dependency Installation and Compile ``` cd cln-application-0.0.1 - npm install --force --omit=dev + npm install + npm run build + npm prune --omit=dev ``` - ### Environment Variables diff --git a/apps/backend/dist/controllers/auth.js b/apps/backend/dist/controllers/auth.js deleted file mode 100644 index 83aa20a4..00000000 --- a/apps/backend/dist/controllers/auth.js +++ /dev/null @@ -1,118 +0,0 @@ -import jwt from 'jsonwebtoken'; -import * as fs from 'fs'; -import { APP_CONSTANTS, HttpStatusCode, SECRET_KEY } from '../shared/consts.js'; -import { logger } from '../shared/logger.js'; -import handleError from '../shared/error-handler.js'; -import { verifyPassword, isAuthenticated, isValidPassword } from '../shared/utils.js'; -import { AuthError } from '../models/errors.js'; -class AuthController { - userLogin(req, res, next) { - logger.info('Logging in'); - try { - const vpRes = verifyPassword(req.body.password); - if (vpRes === true) { - const token = jwt.sign({ userID: SECRET_KEY }, SECRET_KEY); - // Expire the token in a day - res.cookie('token', token, { httpOnly: true, maxAge: 3600000 * 24 }); - return res.status(201).json({ isAuthenticated: true, isValidPassword: isValidPassword() }); - } - else { - const err = new AuthError(HttpStatusCode.UNAUTHORIZED, vpRes); - handleError(err, req, res, next); - } - } - catch (error) { - handleError(error, req, res, next); - } - } - userLogout(req, res, next) { - try { - logger.info('Logging out'); - res.clearCookie('token'); - res.status(201).json({ isAuthenticated: false, isValidPassword: isValidPassword() }); - } - catch (error) { - handleError(error, req, res, next); - } - } - resetPassword(req, res, next) { - try { - logger.info('Resetting password'); - const isValid = req.body.isValid; - const currPassword = req.body.currPassword; - const newPassword = req.body.newPassword; - if (fs.existsSync(APP_CONSTANTS.APP_CONFIG_FILE)) { - try { - const config = JSON.parse(fs.readFileSync(APP_CONSTANTS.APP_CONFIG_FILE, 'utf-8')); - if (config.password === currPassword || !isValid) { - try { - config.password = newPassword; - try { - fs.writeFileSync(APP_CONSTANTS.APP_CONFIG_FILE, JSON.stringify(config, null, 2), 'utf-8'); - const token = jwt.sign({ userID: SECRET_KEY }, SECRET_KEY); - res.cookie('token', token, { httpOnly: true, maxAge: 3600 * 24 * 7 }); - res.status(201).json({ isAuthenticated: true, isValidPassword: isValidPassword() }); - } - catch (error) { - handleError(error, req, res, next); - } - } - catch (error) { - handleError(error, req, res, next); - } - } - else { - return new AuthError(HttpStatusCode.UNAUTHORIZED, 'Incorrect current password'); - } - } - catch (error) { - handleError(error, req, res, next); - } - } - else { - throw new AuthError(HttpStatusCode.UNAUTHORIZED, 'Config file does not exist'); - } - } - catch (error) { - handleError(error, req, res, next); - } - } - isUserAuthenticated(req, res, next) { - try { - const uaRes = isAuthenticated(req.cookies.token); - if (req.body.returnResponse) { - // Frontend is asking if user is authenticated or not - if (APP_CONSTANTS.SINGLE_SIGN_ON === 'true') { - return res.status(201).json({ isAuthenticated: true, isValidPassword: true }); - } - else { - const vpRes = isValidPassword(); - if (uaRes === true) { - if (vpRes === true) { - return res.status(201).json({ isAuthenticated: true, isValidPassword: true }); - } - else { - return res.status(201).json({ isAuthenticated: true, isValidPassword: vpRes }); - } - } - else { - return res.status(201).json({ isAuthenticated: false, isValidPassword: vpRes }); - } - } - } - else { - // Backend APIs are asking if user is authenticated or not - if (uaRes === true || APP_CONSTANTS.SINGLE_SIGN_ON === 'true') { - return next(); - } - else { - return res.status(401).json({ error: 'Unauthorized user' }); - } - } - } - catch (error) { - handleError(error, req, res, next); - } - } -} -export default new AuthController(); diff --git a/apps/backend/dist/controllers/lightning.js b/apps/backend/dist/controllers/lightning.js deleted file mode 100644 index 5202d038..00000000 --- a/apps/backend/dist/controllers/lightning.js +++ /dev/null @@ -1,42 +0,0 @@ -import handleError from '../shared/error-handler.js'; -import { CLNService } from '../service/lightning.service.js'; -import { logger } from '../shared/logger.js'; -import { AppConnect, APP_CONSTANTS } from '../shared/consts.js'; -const clnService = CLNService; -class LightningController { - callMethod(req, res, next) { - try { - logger.info('Calling method: ' + req.body.method); - clnService - .call(req.body.method, req.body.params) - .then((commandRes) => { - logger.info('Controller received response for ' + - req.body.method + - ': ' + - JSON.stringify(commandRes)); - if (APP_CONSTANTS.APP_CONNECT == AppConnect.COMMANDO && - req.body.method && - req.body.method === 'listpeers') { - // Filter out ln message pubkey from peers list - const lnmPubkey = clnService.getLNMsgPubkey(); - commandRes.peers = commandRes.peers.filter((peer) => peer.id !== lnmPubkey); - res.status(200).json(commandRes); - } - else { - res.status(200).json(commandRes); - } - }) - .catch((err) => { - logger.error('Controller caught lightning error from ' + - req.body.method + - ': ' + - JSON.stringify(err)); - return handleError(err, req, res, next); - }); - } - catch (error) { - return handleError(error, req, res, next); - } - } -} -export default new LightningController(); diff --git a/apps/backend/dist/controllers/shared.js b/apps/backend/dist/controllers/shared.js deleted file mode 100644 index ab5a05f0..00000000 --- a/apps/backend/dist/controllers/shared.js +++ /dev/null @@ -1,97 +0,0 @@ -import axios from 'axios'; -import * as fs from 'fs'; -import { APP_CONSTANTS, DEFAULT_CONFIG, FIAT_RATE_API, FIAT_VENUES, HttpStatusCode, } from '../shared/consts.js'; -import { logger } from '../shared/logger.js'; -import handleError from '../shared/error-handler.js'; -import { APIError } from '../models/errors.js'; -import { addServerConfig, refreshEnvVariables } from '../shared/utils.js'; -import { CLNService } from '../service/lightning.service.js'; -class SharedController { - getApplicationSettings(req, res, next) { - try { - logger.info('Getting Application Settings from ' + APP_CONSTANTS.APP_CONFIG_FILE); - if (!fs.existsSync(APP_CONSTANTS.APP_CONFIG_FILE)) { - fs.writeFileSync(APP_CONSTANTS.APP_CONFIG_FILE, JSON.stringify(DEFAULT_CONFIG, null, 2), 'utf-8'); - } - let config = { - uiConfig: JSON.parse(fs.readFileSync(APP_CONSTANTS.APP_CONFIG_FILE, 'utf-8')), - }; - delete config.uiConfig.password; - delete config.uiConfig.isLoading; - delete config.uiConfig.error; - delete config.uiConfig.singleSignOn; - config = addServerConfig(config); - res.status(200).json(config); - } - catch (error) { - handleError(error, req, res, next); - } - } - setApplicationSettings(req, res, next) { - try { - logger.info('Updating Application Settings: ' + JSON.stringify(req.body)); - const config = JSON.parse(fs.readFileSync(APP_CONSTANTS.APP_CONFIG_FILE, 'utf-8')); - req.body.uiConfig.password = config.password; // Before saving, add password in the config received from frontend - fs.writeFileSync(APP_CONSTANTS.APP_CONFIG_FILE, JSON.stringify(req.body.uiConfig, null, 2), 'utf-8'); - res.status(201).json({ message: 'Application Settings Updated Successfully' }); - } - catch (error) { - handleError(error, req, res, next); - } - } - getWalletConnectSettings(req, res, next) { - try { - logger.info('Getting Connection Settings'); - refreshEnvVariables(); - res.status(200).json(APP_CONSTANTS); - } - catch (error) { - handleError(error, req, res, next); - } - } - getFiatRate(req, res, next) { - try { - logger.info('Getting Fiat Rate for: ' + req.params.fiatCurrency); - const FIAT_VENUE = FIAT_VENUES.hasOwnProperty(req.params.fiatCurrency) - ? FIAT_VENUES[req.params.fiatCurrency] - : 'COINGECKO'; - return axios - .get(FIAT_RATE_API + FIAT_VENUE + '/pairs/XBT/' + req.params.fiatCurrency) - .then((response) => { - logger.info('Fiat Response: ' + JSON.stringify(response?.data)); - if (response.data?.rate) { - return res.status(200).json({ venue: FIAT_VENUE, rate: response.data?.rate }); - } - else { - return handleError(new APIError(HttpStatusCode.NOT_FOUND, 'Price Not Found'), req, res, next); - } - }) - .catch(err => { - return handleError(err, req, res, next); - }); - } - catch (error) { - handleError(error, req, res, next); - } - } - async saveInvoiceRune(req, res, next) { - try { - logger.info('Saving Invoice Rune'); - const showRunes = await CLNService.call('showrunes', []); - const invoiceRune = showRunes.runes.find(rune => rune.restrictions.some(restriction => restriction.alternatives.some(alternative => alternative.value === 'invoice')) && - rune.restrictions.some(restriction => restriction.alternatives.some(alternative => alternative.value === 'listinvoices'))); - if (invoiceRune && fs.existsSync(APP_CONSTANTS.COMMANDO_CONFIG)) { - const invoiceRuneString = `INVOICE_RUNE="${invoiceRune.rune}"\n`; - fs.appendFileSync(APP_CONSTANTS.COMMANDO_CONFIG, invoiceRuneString, 'utf-8'); - res.status(201).send(); - } - else { - throw new Error('Invoice rune not found or .commando-env does not exist.'); - } - } - catch (error) { - handleError(error, req, res, next); - } - } -} -export default new SharedController(); diff --git a/apps/backend/dist/models/errors.js b/apps/backend/dist/models/errors.js deleted file mode 100644 index 486bb380..00000000 --- a/apps/backend/dist/models/errors.js +++ /dev/null @@ -1,42 +0,0 @@ -import { HttpStatusCode } from '../shared/consts.js'; -export class BaseError extends Error { - code; - message; - constructor(code, message) { - super(message); - Object.setPrototypeOf(this, new.target.prototype); - this.code = code; - this.message = message; - Error.captureStackTrace(this); - } -} -export class APIError extends BaseError { - constructor(code = HttpStatusCode.INTERNAL_SERVER, message = 'Unknown API Server Error') { - super(code, message); - } -} -export class BitcoindError extends BaseError { - constructor(code = HttpStatusCode.BITCOIN_SERVER, message = 'Unknown Bitcoin API Error') { - super(code, message); - } -} -export class LightningError extends BaseError { - constructor(code = HttpStatusCode.LIGHTNING_SERVER, message = 'Unknown Core Lightning API Error') { - super(code, message); - } -} -export class ValidationError extends BaseError { - constructor(code = HttpStatusCode.INVALID_DATA, message = 'Unknown Validation Error') { - super(code, message); - } -} -export class AuthError extends BaseError { - constructor(code = HttpStatusCode.UNAUTHORIZED, message = 'Unknown Authentication Error') { - super(code, message); - } -} -export class GRPCError extends BaseError { - constructor(code = HttpStatusCode.GRPC_UNKNOWN, message = 'Unknown gRPC Error') { - super(code, message); - } -} diff --git a/apps/backend/dist/models/showrunes.type.js b/apps/backend/dist/models/showrunes.type.js deleted file mode 100644 index cb0ff5c3..00000000 --- a/apps/backend/dist/models/showrunes.type.js +++ /dev/null @@ -1 +0,0 @@ -export {}; diff --git a/apps/backend/dist/routes/v1/auth.js b/apps/backend/dist/routes/v1/auth.js deleted file mode 100644 index 7388e720..00000000 --- a/apps/backend/dist/routes/v1/auth.js +++ /dev/null @@ -1,18 +0,0 @@ -import { CommonRoutesConfig } from '../../shared/routes.config.js'; -import AuthController from '../../controllers/auth.js'; -import { API_VERSION } from '../../shared/consts.js'; -const AUTH_ROUTE = '/auth'; -export class AuthRoutes extends CommonRoutesConfig { - constructor(app) { - super(app, 'Auth Routes'); - } - configureRoutes() { - this.app.route(API_VERSION + AUTH_ROUTE + '/logout/').get(AuthController.userLogout); - this.app.route(API_VERSION + AUTH_ROUTE + '/login/').post(AuthController.userLogin); - this.app.route(API_VERSION + AUTH_ROUTE + '/reset/').post(AuthController.resetPassword); - this.app - .route(API_VERSION + AUTH_ROUTE + '/isauthenticated/') - .post(AuthController.isUserAuthenticated); - return this.app; - } -} diff --git a/apps/backend/dist/routes/v1/lightning.js b/apps/backend/dist/routes/v1/lightning.js deleted file mode 100644 index fd54beb4..00000000 --- a/apps/backend/dist/routes/v1/lightning.js +++ /dev/null @@ -1,16 +0,0 @@ -import { CommonRoutesConfig } from '../../shared/routes.config.js'; -import AuthController from '../../controllers/auth.js'; -import LightningController from '../../controllers/lightning.js'; -import { API_VERSION } from '../../shared/consts.js'; -const LIGHTNING_ROOT_ROUTE = '/cln'; -export class LightningRoutes extends CommonRoutesConfig { - constructor(app) { - super(app, 'Lightning Routes'); - } - configureRoutes() { - this.app - .route(API_VERSION + LIGHTNING_ROOT_ROUTE + '/call') - .post(AuthController.isUserAuthenticated, LightningController.callMethod); - return this.app; - } -} diff --git a/apps/backend/dist/routes/v1/shared.js b/apps/backend/dist/routes/v1/shared.js deleted file mode 100644 index eb4e5970..00000000 --- a/apps/backend/dist/routes/v1/shared.js +++ /dev/null @@ -1,34 +0,0 @@ -import { CommonRoutesConfig } from '../../shared/routes.config.js'; -import AuthController from '../../controllers/auth.js'; -import SharedController from '../../controllers/shared.js'; -import { API_VERSION } from '../../shared/consts.js'; -const SHARED_ROUTE = '/shared'; -export class SharedRoutes extends CommonRoutesConfig { - constructor(app) { - super(app, 'Shared Routes'); - } - configureRoutes() { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - this.app.route(API_VERSION + SHARED_ROUTE + '/csrf/').get((req, res, next) => { - res.send({ - csrfToken: req.csrfToken && typeof req.csrfToken === 'function' ? req.csrfToken() : 'not-set', - }); - }); - this.app - .route(API_VERSION + SHARED_ROUTE + '/config/') - .get(SharedController.getApplicationSettings); - this.app - .route(API_VERSION + SHARED_ROUTE + '/config/') - .post(AuthController.isUserAuthenticated, SharedController.setApplicationSettings); - this.app - .route(API_VERSION + SHARED_ROUTE + '/connectwallet/') - .get(AuthController.isUserAuthenticated, SharedController.getWalletConnectSettings); - this.app - .route(API_VERSION + SHARED_ROUTE + '/rate/:fiatCurrency') - .get(SharedController.getFiatRate); - this.app - .route(API_VERSION + SHARED_ROUTE + '/saveinvoicerune/') - .post(AuthController.isUserAuthenticated, SharedController.saveInvoiceRune); - return this.app; - } -} diff --git a/apps/backend/dist/server.js b/apps/backend/dist/server.js deleted file mode 100644 index 9f177c17..00000000 --- a/apps/backend/dist/server.js +++ /dev/null @@ -1,84 +0,0 @@ -import { join, dirname } from 'path'; -import { fileURLToPath } from 'url'; -import express from 'express'; -import http from 'http'; -import bodyParser from 'body-parser'; -import cors from 'cors'; -import csurf from 'csurf'; -import cookieParser from 'cookie-parser'; -import expressWinston from 'express-winston'; -import { logger, expressLogConfiguration } from './shared/logger.js'; -import { LightningRoutes } from './routes/v1/lightning.js'; -import { SharedRoutes } from './routes/v1/shared.js'; -import { AuthRoutes } from './routes/v1/auth.js'; -import { APIError } from './models/errors.js'; -import { APP_CONSTANTS, Environment, HttpStatusCode } from './shared/consts.js'; -import handleError from './shared/error-handler.js'; -const directoryName = dirname(fileURLToPath(import.meta.url)); -const routes = []; -const app = express(); -const server = http.createServer(app); -const LIGHTNING_PORT = normalizePort(process.env.APP_PORT || '2103'); -const APP_IP = process.env.APP_IP || 'localhost'; -const APP_PROTOCOL = process.env.APP_PROTOCOL || 'http'; -function normalizePort(val) { - const port = parseInt(val, 10); - if (isNaN(port)) { - return val; - } - if (port >= 0) { - return port; - } - return false; -} -app.use(bodyParser.json({ limit: '25mb' })); -app.use(bodyParser.urlencoded({ extended: false, limit: '25mb' })); -app.set('trust proxy', true); -app.use(cookieParser()); -app.use(csurf({ cookie: true })); -app.use((req, res, next) => { - res.setHeader('Cache-Control', 'no-cache'); - res.setHeader('Content-Security-Policy', "default-src 'self'; font-src 'self'; img-src 'self' data:; script-src 'self'; frame-src 'self'; style-src 'self';"); - next(); -}); -const corsOptions = { - methods: 'GET, POST, PATCH, PUT, DELETE, OPTIONS', - origin: APP_CONSTANTS.APP_MODE === Environment.PRODUCTION - ? APP_PROTOCOL + '://' + APP_IP + ':' + LIGHTNING_PORT - : APP_PROTOCOL + '://localhost:4300', - credentials: true, - allowedHeaders: 'Content-Type, X-XSRF-TOKEN, XSRF-TOKEN', -}; -app.use(cors(corsOptions)); -app.use(expressWinston.logger(expressLogConfiguration)); -app.use(expressWinston.errorLogger(expressLogConfiguration)); -routes.push(new AuthRoutes(app)); -routes.push(new SharedRoutes(app)); -routes.push(new LightningRoutes(app)); -// serve frontend -app.use('/', express.static(join(directoryName, '..', '..', 'frontend', 'build'))); -// eslint-disable-next-line @typescript-eslint/no-unused-vars -app.use((req, res, next) => { - res.sendFile(join(directoryName, '..', '..', 'frontend', 'build', 'index.html')); -}); -app.use((err, req, res, next) => { - return handleError(throwApiError(err), req, res, next); -}); -const throwApiError = (err) => { - logger.error('Server error: ' + err); - switch (err.code) { - case 'EACCES': - return new APIError(HttpStatusCode.ACCESS_DENIED, APP_PROTOCOL + '://' + APP_IP + ':' + LIGHTNING_PORT + ' requires elevated privileges'); - case 'EADDRINUSE': - return new APIError(HttpStatusCode.ADDR_IN_USE, APP_PROTOCOL + '://' + APP_IP + ':' + LIGHTNING_PORT + ' is already in use'); - case 'ECONNREFUSED': - return new APIError(HttpStatusCode.UNAUTHORIZED, 'Server is down/locked'); - case 'EBADCSRFTOKEN': - return new APIError(HttpStatusCode.BAD_CSRF_TOKEN, 'Invalid CSRF token. Form tempered.'); - default: - return new APIError(400, 'Default: ' + JSON.stringify(err)); - } -}; -server.on('error', throwApiError); -server.on('listening', () => logger.warn('Server running at ' + APP_PROTOCOL + '://' + APP_IP + ':' + LIGHTNING_PORT)); -server.listen({ port: LIGHTNING_PORT, host: APP_IP }); diff --git a/apps/backend/dist/service/grpc.service.js b/apps/backend/dist/service/grpc.service.js deleted file mode 100644 index 1c4e3dad..00000000 --- a/apps/backend/dist/service/grpc.service.js +++ /dev/null @@ -1,333 +0,0 @@ -import axios, { AxiosHeaders } from 'axios'; -import * as path from 'path'; -import fs from 'fs'; -import https from 'https'; -import protobuf from 'protobufjs'; -import { HttpStatusCode, GRPC_CONFIG, APP_CONSTANTS } from '../shared/consts.js'; -import { GRPCError } from '../models/errors.js'; -import { logger } from '../shared/logger.js'; -export class GRPCService { - authPubkey; - authSignature; - protoPath; - clnNode; - axiosConfig; - constructor(grpcConfig) { - this.authSignature = 'A'.repeat(64); - this.authPubkey = Buffer.from(grpcConfig.pubkey, 'hex').toString('base64'); - this.protoPath = [ - path.resolve(process.cwd(), './proto/node.proto'), - path.resolve(process.cwd(), './proto/primitives.proto'), - ]; - this.clnNode = protobuf.Root.fromJSON(protobuf.loadSync(this.protoPath).toJSON()); - const headers = new AxiosHeaders(); - headers.set('content-type', 'application/grpc'); - headers.set('accept', 'application/grpc'); - headers.set('glauthpubkey', this.authPubkey); - headers.set('glauthsig', this.authSignature); - this.axiosConfig = { - responseType: 'arraybuffer', - baseURL: `${grpcConfig.url}/cln.Node/`, - headers, - }; - if (APP_CONSTANTS.LIGHTNING_GRPC_PROTOCOL === 'https') { - const httpsAgent = new https.Agent({ - cert: fs.readFileSync(path.join(APP_CONSTANTS.LIGHTNING_CERTS_PATH || '.', 'client.pem')), - key: fs.readFileSync(path.join(APP_CONSTANTS.LIGHTNING_CERTS_PATH || '.', 'client-key.pem')), - ca: fs.readFileSync(path.join(APP_CONSTANTS.LIGHTNING_CERTS_PATH || '.', 'ca.pem')), - }); - this.axiosConfig.httpsAgent = httpsAgent; - } - } - static getGrpcStatusMessages(method) { - return { - 0: `${method} completed successfully.`, - 1: `${method} was cancelled.`, - 2: `Unknown or internal error for ${method}.`, - 3: `${method} had an invalid argument.`, - 4: `${method} took too long and timed out.`, - 5: `Resource not found for ${method}.`, - 6: `Resource already exists for ${method}.`, - 7: `Permission denied for ${method}.`, - 8: `Resource exhausted for ${method}.`, - 9: `Precondition failed for ${method}.`, - 10: `${method} was aborted.`, - 11: `${method} accessed an out-of-range value.`, - 12: `${method} is not implemented.`, - 13: `Internal server error for ${method}.`, - 14: `Service unavailable for ${method}.`, - 15: `Unrecoverable data loss in ${method}.`, - 16: `Authentication failed for ${method}.`, - }; - } - async encodePayload(method, payload) { - const requestType = this.clnNode.lookupType(`cln.${method}Request`); - const errMsg = requestType.verify(payload); - if (errMsg) - throw new GRPCError(HttpStatusCode.GRPC_UNKNOWN, errMsg); - const requestPayload = requestType.create(payload); - const encodedPayload = requestType.encode(requestPayload).finish(); - const flags = Buffer.alloc(1); - flags.writeUInt8(0, 0); - const header = Buffer.alloc(4); - header.writeUInt32BE(encodedPayload.length, 0); - logger.debug(requestType.decode(encodedPayload)); - return Buffer.concat([flags, header, encodedPayload]); - } - async sendRequest(methodUrl, encodedPayload) { - try { - const timestamp = Buffer.alloc(8); - timestamp.writeUInt32BE(Math.floor(Date.now() / 1000), 4); - const extendedAxiosConfig = { - ...this.axiosConfig, - headers: { - ...this.axiosConfig.headers, - glts: timestamp.toString('base64'), - }, - }; - return await axios.post(`${methodUrl}`, encodedPayload, extendedAxiosConfig); - } - catch (error) { - logger.error(`Request failed for ${methodUrl}:`, error); - throw new GRPCError(error.response?.status || error.code || HttpStatusCode.GRPC_UNKNOWN, error.response?.statusText || error.response?.data || error.message || ''); - } - } - CamelToSnakeCase(key) { - // convert camelCase keys to snake_case but do not change ENUMS_WITH_UNDERSCORES like CHANNELD_NORMAL - return key.includes('_') ? key : key.replace(/[A-Z]/g, match => `_${match.toLowerCase()}`); - } - transformResData(key, value) { - const transformedKey = this.CamelToSnakeCase(key); - if (Buffer.isBuffer(value) || value instanceof Uint8Array) { - return { [transformedKey]: Buffer.from(value).toString('hex') }; - } - if (typeof value === 'object' && value !== null && 'msat' in value) { - // FIXME: Amount.varify check will work with 0 NOT '0'. Amount default is '0'. - const msatValue = parseInt(value.msat); - if (!isNaN(msatValue)) { - return { [transformedKey]: msatValue }; - } - } - if (typeof value === 'object' && value !== null) { - if (Array.isArray(value)) { - return { [transformedKey]: value.map(item => this.transformResKeys(item)) }; - } - else { - return { [transformedKey]: this.transformResKeys(value) }; - } - } - return { [transformedKey]: value }; - } - transformResKeys(obj) { - if (typeof obj !== 'object' || obj === null) - return obj; - const transformedObj = {}; - for (const [key, value] of Object.entries(obj)) { - const transformedEntry = this.transformResData(key, value); - Object.assign(transformedObj, transformedEntry); - } - return transformedObj; - } - preserveEnums(data) { - if (data.channels) { - data.channels.forEach((channel) => { - if (channel.state && !channel.state.includes('_')) { - channel.state = channel.state - .replace(/([A-Z])/g, '_$1') - .toUpperCase() - .replace('_', ''); - } - }); - } - return data; - } - extractRpcError(errorMessage) { - const rpcErrorMatch = errorMessage.match(/RpcError\s*\{([^}]+)\}/); - if (!rpcErrorMatch) { - return errorMessage; - } - try { - const rpcErrorMessageMatch = errorMessage.match(/message: "([^"]*(?:"[^"]*"[^"]*)*)"/); - return rpcErrorMessageMatch && rpcErrorMessageMatch[1] - ? rpcErrorMessageMatch[1].replaceAll('\\', '') - : errorMessage; - } - catch (error) { - logger.error('Error extracting RPC error message: ', error); - return errorMessage; - } - } - decodeResponse(method, response) { - const responseType = this.clnNode.lookupType(`cln.${method}Response`); - const dataBuffer = Buffer.from(response.data || ''); - // resFlag (0, 1) and resDataLength (1, 5) not used in code - const responseData = dataBuffer.subarray(5); - const grpcStatus = Number(response.headers['grpc-status']); - if (grpcStatus !== 0) { - let errorMessage; - try { - errorMessage = decodeURIComponent(new TextDecoder('utf-8').decode(responseData)); - if (errorMessage !== 'None') { - errorMessage = this.extractRpcError(errorMessage); - } - else { - errorMessage = GRPCService.getGrpcStatusMessages(method)[grpcStatus]; - } - } - catch { - errorMessage = 'Invalid gRPC error response'; - } - // Offset gRPC status code by 550:return ensure a valid HTTP 5xx server error code - throw new GRPCError(550 + grpcStatus || HttpStatusCode.GRPC_UNKNOWN, errorMessage); - } - const decodedResponse = responseType.toObject(responseType.decode(responseData), { - longs: String, - enums: String, - bytes: Buffer, - defaults: true, - arrays: true, - objects: true, - }); - const transformedResponse = this.transformResKeys(decodedResponse); - const preserveEnumsInResponse = this.preserveEnums(transformedResponse); - return JSON.parse(JSON.stringify(preserveEnumsInResponse)); - } - convertMethodName(method) { - const methodMapping = { - 'bkpr-listaccountevents': 'BkprListAccountEvents', - createrune: 'CreateRune', - fetchinvoice: 'FetchInvoice', - fundchannel: 'FundChannel', - newaddr: 'NewAddr', - keysend: 'KeySend', - listfunds: 'ListFunds', - listinvoices: 'ListInvoices', - listnodes: 'ListNodes', - listoffers: 'ListOffers', - listpeers: 'ListPeers', - listpeerchannels: 'ListPeerChannels', - listsendpays: 'ListSendPays', - }; - const formattedMethod = method.charAt(0).toUpperCase() + method.slice(1).toLowerCase(); - return [methodMapping[method] || formattedMethod, formattedMethod]; - } - transformTypes(type, valueToTransform) { - switch (type) { - case 'Amount': - const AmountType = this.clnNode.lookupType('cln.Amount'); - return AmountType.create({ msat: valueToTransform }); - case 'AmountOrAll': - const AmountOrAllType = this.clnNode.lookupType('cln.AmountOrAll'); - if (valueToTransform === 'all') { - return AmountOrAllType.create({ [valueToTransform]: true }); - } - else { - return AmountOrAllType.create({ - amount: this.transformTypes('Amount', valueToTransform), - }); - } - case 'AmountOrAny': - const AmountOrAnyType = this.clnNode.lookupType('cln.AmountOrAny'); - if (valueToTransform === 'any') { - return AmountOrAnyType.create({ [valueToTransform]: true }); - } - else { - return AmountOrAnyType.create({ - amount: this.transformTypes('Amount', valueToTransform), - }); - } - case 'Feerate': - const FeerateType = this.clnNode.lookupType(`cln.Feerate`); - return FeerateType.create({ [valueToTransform]: true }); - default: - break; - } - } - snakeToCamel(str) { - return str.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase()); - } - changeKeysToCamelCase(obj) { - if (Array.isArray(obj)) { - return obj.map(this.changeKeysToCamelCase); - } - else if (typeof obj === 'object' && obj !== null) { - const newObj = {}; - for (const key in obj) { - if (obj.hasOwnProperty(key)) { - const newKey = this.snakeToCamel(key); - newObj[newKey] = this.changeKeysToCamelCase(obj[key]); - } - } - return newObj; - } - return obj; - } - transformPayload(method, payload) { - try { - if (method?.toLowerCase() === 'fundchannel') { - payload.id = Buffer.from(payload.id, 'hex').toString('base64'); - payload.amount = this.transformTypes('AmountOrAll', payload.amount * 1000); - payload.feerate = this.transformTypes('Feerate', payload.feerate); - } - if (method?.toLowerCase() === 'withdraw') { - payload.satoshi = this.transformTypes('AmountOrAll', payload.satoshi * 1000); - payload.feerate = this.transformTypes('Feerate', payload.feerate); - } - if (method?.toLowerCase() === 'invoice') { - payload.amount_msat = this.transformTypes('AmountOrAny', payload.amount_msat); - } - if (method?.toLowerCase() === 'keysend') { - payload.destination = Buffer.from(payload.destination, 'hex').toString('base64'); - payload.amount_msat = this.transformTypes('Amount', payload.amount_msat); - } - if (method?.toLowerCase() === 'pay') { - payload.amount_msat = this.transformTypes('Amount', payload.amount_msat); - } - // Map values with their Enums - const enumsMapping = { - Feerates: ['style'], - Newaddr: ['addresstype'], - }; - const fieldNames = enumsMapping[method]; - if (fieldNames) { - fieldNames.forEach(fieldName => { - const capitalizedFieldName = fieldName.charAt(0).toUpperCase() + fieldName.slice(1).toLowerCase(); - const fieldEnum = this.clnNode.lookupEnum(`cln.${method}${capitalizedFieldName}`); - payload[fieldName] = fieldEnum.values[payload[fieldName].toUpperCase()]; - }); - } - return this.changeKeysToCamelCase(payload); - } - catch (error) { - throw new GRPCError(HttpStatusCode.INVALID_DATA, error.message || 'Unknown'); - } - } - async callMethod(methodName, reqPayload) { - if (methodName?.toLowerCase() === 'bkpr-listaccountevents') { - let data = {}; - if (GRPC_CONFIG.pubkey === '0279da9a93e50b008a7ba6bd25355fb7132f5015b790a05ee9f41bc9fbdeb30d19') { - data = JSON.parse(await fs.readFileSync(path.join(process.cwd(), '../../data/dummy/node-1.json'), 'utf8'))['bkpr-listaccountevents']; - } - else { - data = JSON.parse(await fs.readFileSync(path.join(process.cwd(), '../../data/dummy/node-3.json'), 'utf8'))['bkpr-listaccountevents']; - } - return data; - } - else { - const [method, capitalizedMethod] = this.convertMethodName(methodName); - reqPayload = this.transformPayload(capitalizedMethod, reqPayload); - const encodedPayload = await this.encodePayload(capitalizedMethod, reqPayload); - logger.info(`Calling gRPC method: ${capitalizedMethod}`); - logger.debug('Payload: ', reqPayload); - try { - const response = await this.sendRequest(method, encodedPayload); - logger.debug('Response Headers: ', response?.headers); - return this.decodeResponse(capitalizedMethod, response); - } - catch (error) { - logger.error(`Error calling ${capitalizedMethod}: `, error); - throw new GRPCError(error.response?.status || error.code || HttpStatusCode.GRPC_UNKNOWN, error.response?.statusText || error.response?.data || error.message || ''); - } - } - } -} diff --git a/apps/backend/dist/service/lightning.service.js b/apps/backend/dist/service/lightning.service.js deleted file mode 100644 index 948ed0eb..00000000 --- a/apps/backend/dist/service/lightning.service.js +++ /dev/null @@ -1,114 +0,0 @@ -import * as fs from 'fs'; -import * as crypto from 'crypto'; -import { join } from 'path'; -import https from 'https'; -import axios, { AxiosHeaders } from 'axios'; -import Lnmessage from 'lnmessage'; -import { LightningError } from '../models/errors.js'; -import { GRPCService } from './grpc.service.js'; -import { HttpStatusCode, APP_CONSTANTS, AppConnect, LN_MESSAGE_CONFIG, REST_CONFIG, GRPC_CONFIG, } from '../shared/consts.js'; -import { logger } from '../shared/logger.js'; -import { refreshEnvVariables } from '../shared/utils.js'; -export class LightningService { - clnService = null; - constructor() { - try { - logger.info('Getting Commando Rune'); - if (fs.existsSync(APP_CONSTANTS.COMMANDO_CONFIG)) { - refreshEnvVariables(); - switch (APP_CONSTANTS.APP_CONNECT) { - case AppConnect.REST: - logger.info('REST connecting with config: ' + JSON.stringify(REST_CONFIG)); - break; - case AppConnect.GRPC: - logger.info('GRPC connecting with config: ' + JSON.stringify(GRPC_CONFIG)); - this.clnService = new GRPCService(GRPC_CONFIG); - break; - default: - logger.info('lnMessage connecting with config: ' + JSON.stringify(LN_MESSAGE_CONFIG)); - this.clnService = new Lnmessage(LN_MESSAGE_CONFIG); - this.clnService.connect(); - break; - } - } - } - catch (error) { - logger.error('Failed to read rune for Commando connection: ' + JSON.stringify(error)); - throw error; - } - } - getLNMsgPubkey = () => { - return this.clnService.publicKey; - }; - call = async (method, methodParams) => { - switch (APP_CONSTANTS.APP_CONNECT) { - case AppConnect.REST: - const headers = new AxiosHeaders(); - headers.set('rune', APP_CONSTANTS.COMMANDO_RUNE); - const axiosConfig = { - baseURL: REST_CONFIG.url + '/v1/', - headers, - }; - if (APP_CONSTANTS.LIGHTNING_REST_PROTOCOL === 'https') { - const caCert = fs.readFileSync(join(APP_CONSTANTS.LIGHTNING_CERTS_PATH || '.', 'ca.pem')); - const httpsAgent = new https.Agent({ - ca: caCert, - }); - axiosConfig.httpsAgent = httpsAgent; - } - return axios - .post(method, methodParams, axiosConfig) - .then((commandRes) => { - logger.info('REST response for ' + method + ': ' + JSON.stringify(commandRes.data)); - return Promise.resolve(commandRes.data); - }) - .catch((err) => { - logger.error('REST lightning error from ' + method + ' command'); - if (typeof err === 'string') { - logger.error(err); - throw new LightningError(HttpStatusCode.LIGHTNING_SERVER, err); - } - else { - logger.error(JSON.stringify(err)); - throw new LightningError(HttpStatusCode.LIGHTNING_SERVER, err.message || err.code); - } - }); - case AppConnect.GRPC: - return this.clnService - .callMethod(method, methodParams) - .then((gRPCRes) => { - logger.info('gRPC response for ' + method + ': ' + JSON.stringify(gRPCRes)); - return Promise.resolve(gRPCRes); - }) - .catch((err) => { - logger.error('gRPC lightning error from ' + method + ' command'); - throw err; - }); - default: - return this.clnService - .commando({ - method: method, - params: methodParams, - rune: APP_CONSTANTS.COMMANDO_RUNE, - reqId: crypto.randomBytes(8).toString('hex'), - reqIdPrefix: 'clnapp', - }) - .then((commandRes) => { - logger.info('Commando response for ' + method + ': ' + JSON.stringify(commandRes)); - return Promise.resolve(commandRes); - }) - .catch((err) => { - logger.error('Commando lightning error from ' + method + ' command'); - if (typeof err === 'string') { - logger.error(err); - throw new LightningError(HttpStatusCode.LIGHTNING_SERVER, err); - } - else { - logger.error(JSON.stringify(err)); - throw new LightningError(HttpStatusCode.LIGHTNING_SERVER, err.message || err.code); - } - }); - } - }; -} -export const CLNService = new LightningService(); diff --git a/apps/backend/dist/shared/consts.js b/apps/backend/dist/shared/consts.js deleted file mode 100644 index aedd15d9..00000000 --- a/apps/backend/dist/shared/consts.js +++ /dev/null @@ -1,125 +0,0 @@ -import * as crypto from 'crypto'; -import { join } from 'path'; -export var Environment; -(function (Environment) { - Environment["PRODUCTION"] = "production"; - Environment["TESTING"] = "testing"; - Environment["DEVELOPMENT"] = "development"; -})(Environment || (Environment = {})); -export var AppConnect; -(function (AppConnect) { - AppConnect["COMMANDO"] = "COMMANDO"; - AppConnect["REST"] = "REST"; - AppConnect["GRPC"] = "GRPC"; -})(AppConnect || (AppConnect = {})); -export var NodeType; -(function (NodeType) { - NodeType["CLN"] = "CLN"; -})(NodeType || (NodeType = {})); -export var HttpStatusCode; -(function (HttpStatusCode) { - HttpStatusCode[HttpStatusCode["GET_OK"] = 200] = "GET_OK"; - HttpStatusCode[HttpStatusCode["POST_OK"] = 201] = "POST_OK"; - HttpStatusCode[HttpStatusCode["DELETE_OK"] = 204] = "DELETE_OK"; - HttpStatusCode[HttpStatusCode["BAD_REQUEST"] = 400] = "BAD_REQUEST"; - HttpStatusCode[HttpStatusCode["UNAUTHORIZED"] = 401] = "UNAUTHORIZED"; - HttpStatusCode[HttpStatusCode["BAD_CSRF_TOKEN"] = 403] = "BAD_CSRF_TOKEN"; - HttpStatusCode[HttpStatusCode["NOT_FOUND"] = 404] = "NOT_FOUND"; - HttpStatusCode[HttpStatusCode["ACCESS_DENIED"] = 406] = "ACCESS_DENIED"; - HttpStatusCode[HttpStatusCode["ADDR_IN_USE"] = 409] = "ADDR_IN_USE"; - HttpStatusCode[HttpStatusCode["INVALID_DATA"] = 421] = "INVALID_DATA"; - HttpStatusCode[HttpStatusCode["INTERNAL_SERVER"] = 500] = "INTERNAL_SERVER"; - HttpStatusCode[HttpStatusCode["BITCOIN_SERVER"] = 520] = "BITCOIN_SERVER"; - HttpStatusCode[HttpStatusCode["LIGHTNING_SERVER"] = 521] = "LIGHTNING_SERVER"; - HttpStatusCode[HttpStatusCode["GRPC_UNKNOWN"] = 552] = "GRPC_UNKNOWN"; -})(HttpStatusCode || (HttpStatusCode = {})); -export const SECRET_KEY = crypto.randomBytes(64).toString('hex'); -export const APP_CONSTANTS = { - SINGLE_SIGN_ON: process.env.SINGLE_SIGN_ON || 'false', - LOCAL_HOST: process.env.LOCAL_HOST || '', - DEVICE_DOMAIN_NAME: process.env.DEVICE_DOMAIN_NAME || '', - BITCOIN_NODE_IP: process.env.BITCOIN_NODE_IP || 'localhost', - BITCOIN_NETWORK: process.env.BITCOIN_NETWORK || 'bitcoin', - APP_CONFIG_FILE: join(process.env.APP_CONFIG_DIR || '.', 'config.json'), - APP_LOG_FILE: join(process.env.APP_CONFIG_DIR || '.', 'application-cln.log'), - APP_MODE: process.env.APP_MODE || Environment.PRODUCTION, - APP_CONNECT: process.env.APP_CONNECT || AppConnect.COMMANDO, - APP_PROTOCOL: process.env.APP_PROTOCOL || 'http', - APP_IP: process.env.APP_IP || 'localhost', - APP_PORT: process.env.APP_PORT || '2103', - LIGHTNING_IP: process.env.LIGHTNING_IP || process.env.APP_CORE_LIGHTNING_DAEMON_IP || 'localhost', - LIGHTNING_PATH: process.env.LIGHTNING_PATH || '', - HIDDEN_SERVICE_URL: process.env.HIDDEN_SERVICE_URL || '', - LIGHTNING_NODE_TYPE: process.env.LIGHTNING_NODE_TYPE || NodeType.CLN, - COMMANDO_CONFIG: process.env.COMMANDO_CONFIG || './.commando-env', - LIGHTNING_WS_PORT: +(process.env.LIGHTNING_WEBSOCKET_PORT || - process.env.APP_CORE_LIGHTNING_WEBSOCKET_PORT || - 5001), - LIGHTNING_REST_PROTOCOL: process.env.LIGHTNING_REST_PROTOCOL || process.env.APP_CORE_LIGHTNING_REST_PROTOCOL || 'https', - LIGHTNING_REST_PORT: +(process.env.LIGHTNING_REST_PORT || - process.env.APP_CORE_LIGHTNING_REST_PORT || - 3010), - LIGHTNING_CERTS_PATH: process.env.LIGHTNING_CERTS_PATH || '', - LIGHTNING_GRPC_PROTOCOL: process.env.LIGHTNING_GRPC_PROTOCOL || - process.env.APP_CORE_LIGHTNING_DAEMON_GRPC_PROTOCOL || - 'http', - LIGHTNING_GRPC_PORT: +(process.env.LIGHTNING_GRPC_PORT || - process.env.APP_CORE_LIGHTNING_DAEMON_GRPC_PORT || - 9736), - APP_VERSION: '', - NODE_PUBKEY: '', - COMMANDO_RUNE: '', - INVOICE_RUNE: '', - CLIENT_KEY: '', - CLIENT_CERT: '', - CA_CERT: '', -}; -export const DEFAULT_CONFIG = { - unit: 'SATS', - fiatUnit: 'USD', - appMode: 'DARK', - isLoading: false, - error: null, - singleSignOn: false, - password: '', -}; -export const LN_MESSAGE_CONFIG = { - remoteNodePublicKey: '', - wsProxy: 'ws://' + APP_CONSTANTS.LIGHTNING_IP + ':' + APP_CONSTANTS.LIGHTNING_WS_PORT, - ip: APP_CONSTANTS.LIGHTNING_IP, - port: APP_CONSTANTS.LIGHTNING_WS_PORT, - privateKey: crypto.randomBytes(32).toString('hex'), - logger: { - info: APP_CONSTANTS.APP_MODE === Environment.PRODUCTION ? () => { } : console.info, - warn: APP_CONSTANTS.APP_MODE === Environment.PRODUCTION ? () => { } : console.warn, - error: console.error, - }, -}; -export const GRPC_CONFIG = { - pubkey: APP_CONSTANTS.NODE_PUBKEY, - protocol: APP_CONSTANTS.LIGHTNING_GRPC_PROTOCOL, - ip: APP_CONSTANTS.LIGHTNING_IP, - port: APP_CONSTANTS.LIGHTNING_GRPC_PORT, - url: APP_CONSTANTS.LIGHTNING_GRPC_PROTOCOL + - '://' + - APP_CONSTANTS.LIGHTNING_IP + - ':' + - APP_CONSTANTS.LIGHTNING_GRPC_PORT, -}; -export const REST_CONFIG = { - protocol: APP_CONSTANTS.LIGHTNING_REST_PROTOCOL, - ip: APP_CONSTANTS.LIGHTNING_IP, - port: APP_CONSTANTS.LIGHTNING_REST_PORT, - url: APP_CONSTANTS.LIGHTNING_REST_PROTOCOL + - '://' + - APP_CONSTANTS.LIGHTNING_IP + - ':' + - APP_CONSTANTS.LIGHTNING_REST_PORT, -}; -export const API_VERSION = '/v1'; -export const FIAT_RATE_API = 'https://green-bitcoin-mainnet.blockstream.com/prices/v0/venues/'; -export const FIAT_VENUES = { - USD: 'KRAKEN', - EUR: 'KRAKEN', - NZD: 'KIWICOIN', -}; diff --git a/apps/backend/dist/shared/error-handler.js b/apps/backend/dist/shared/error-handler.js deleted file mode 100644 index 1f7c3516..00000000 --- a/apps/backend/dist/shared/error-handler.js +++ /dev/null @@ -1,17 +0,0 @@ -import { HttpStatusCode } from './consts.js'; -import { logger } from './logger.js'; -function handleError(error, req, res, -// eslint-disable-next-line @typescript-eslint/no-unused-vars -next) { - const route = req.url || ''; - const message = error.message - ? error.message - : typeof error === 'object' - ? JSON.stringify(error) - : typeof error === 'string' - ? error - : 'Unknown Error!'; - logger.error(message, route, error.stack); - return res.status(error.code || HttpStatusCode.INTERNAL_SERVER).json(message); -} -export default handleError; diff --git a/apps/backend/dist/shared/logger.js b/apps/backend/dist/shared/logger.js deleted file mode 100644 index 8c4ef5f6..00000000 --- a/apps/backend/dist/shared/logger.js +++ /dev/null @@ -1,31 +0,0 @@ -import winston from 'winston'; -import { Environment, APP_CONSTANTS } from './consts.js'; -export const logConfiguration = { - transports: [ - new winston.transports.Console({ - level: APP_CONSTANTS.APP_MODE === Environment.PRODUCTION - ? "warn" /* LogLevel.WARN */ - : APP_CONSTANTS.APP_MODE === Environment.TESTING - ? "debug" /* LogLevel.DEBUG */ - : "info" /* LogLevel.INFO */, - format: winston.format.combine(winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss:ms' }), winston.format.timestamp(), winston.format.align(), winston.format.json(), winston.format.colorize({ all: true })), - }), - new winston.transports.File({ - filename: APP_CONSTANTS.APP_LOG_FILE, - level: APP_CONSTANTS.APP_MODE === Environment.PRODUCTION - ? "warn" /* LogLevel.WARN */ - : APP_CONSTANTS.APP_MODE === Environment.TESTING - ? "debug" /* LogLevel.DEBUG */ - : "info" /* LogLevel.INFO */, - format: winston.format.combine(winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss:ms' }), winston.format.timestamp(), winston.format.align(), winston.format.json(), winston.format.colorize({ all: true })), - }), - ], -}; -export const expressLogConfiguration = { - ...logConfiguration, - meta: APP_CONSTANTS.APP_MODE !== Environment.PRODUCTION, - message: 'HTTP {{res.statusCode}} {{req.method}} {{res.responseTime}}ms {{req.url}}', - expressFormat: false, - colorize: true, -}; -export const logger = winston.createLogger(logConfiguration); diff --git a/apps/backend/dist/shared/routes.config.js b/apps/backend/dist/shared/routes.config.js deleted file mode 100644 index e9378a14..00000000 --- a/apps/backend/dist/shared/routes.config.js +++ /dev/null @@ -1,12 +0,0 @@ -export class CommonRoutesConfig { - app; - name; - constructor(app, name) { - this.app = app; - this.name = name; - this.configureRoutes(); - } - getName() { - return this.name; - } -} diff --git a/apps/backend/dist/shared/utils.js b/apps/backend/dist/shared/utils.js deleted file mode 100644 index 145c6be0..00000000 --- a/apps/backend/dist/shared/utils.js +++ /dev/null @@ -1,133 +0,0 @@ -import jwt from 'jsonwebtoken'; -import * as fs from 'fs'; -import { sep } from 'path'; -import { logger } from '../shared/logger.js'; -import { APP_CONSTANTS, GRPC_CONFIG, LN_MESSAGE_CONFIG, SECRET_KEY } from '../shared/consts.js'; -export function addServerConfig(config) { - config.serverConfig = { - appConnect: APP_CONSTANTS.APP_CONNECT, - appPort: APP_CONSTANTS.APP_PORT, - appProtocol: APP_CONSTANTS.APP_PROTOCOL, - appVersion: APP_CONSTANTS.APP_VERSION, - lightningNodeType: APP_CONSTANTS.LIGHTNING_NODE_TYPE, - singleSignOn: APP_CONSTANTS.SINGLE_SIGN_ON, - }; - return config; -} -export function isAuthenticated(token) { - try { - if (!token) { - return 'Token missing'; - } - try { - const decoded = jwt.verify(token, SECRET_KEY); - return !!decoded.userID; - } - catch (error) { - return error.message || 'Invalid user'; - } - } - catch (error) { - return error; - } -} -export function verifyPassword(password) { - if (fs.existsSync(APP_CONSTANTS.APP_CONFIG_FILE)) { - try { - const config = JSON.parse(fs.readFileSync(APP_CONSTANTS.APP_CONFIG_FILE, 'utf-8')); - if (config.password === password) { - return true; - } - else { - return 'Incorrect password'; - } - } - catch (error) { - return error; - } - } - else { - return 'Config file does not exist'; - } -} -export function isValidPassword() { - if (fs.existsSync(APP_CONSTANTS.APP_CONFIG_FILE)) { - try { - const config = JSON.parse(fs.readFileSync(APP_CONSTANTS.APP_CONFIG_FILE, 'utf-8')); - if (config.password && config.password !== '') { - return true; - } - else { - return false; - } - } - catch (error) { - return error; - } - } - else { - return 'Config file does not exist'; - } -} -function parseEnvFile(filePath) { - try { - const content = fs.readFileSync(filePath, 'utf8'); - const lines = content.split('\n'); - const envVars = {}; - for (let line of lines) { - line = line.trim(); - if (line && line.indexOf('=') !== -1 && !line.startsWith('#')) { - const [key, ...value] = line.split('='); - envVars[key] = value.join('=').replace(/(^"|"$)/g, ''); - } - } - return envVars; - } - catch (err) { - logger.error('Error reading .commando-env file:', err); - return {}; - } -} -export function refreshEnvVariables() { - const envVars = parseEnvFile(APP_CONSTANTS.COMMANDO_CONFIG); - process.env.LIGHTNING_PUBKEY = envVars.LIGHTNING_PUBKEY; - process.env.COMMANDO_RUNE = envVars.LIGHTNING_RUNE; - process.env.INVOICE_RUNE = envVars.INVOICE_RUNE || ''; - APP_CONSTANTS.NODE_PUBKEY = envVars.LIGHTNING_PUBKEY; - APP_CONSTANTS.COMMANDO_RUNE = envVars.LIGHTNING_RUNE; - APP_CONSTANTS.INVOICE_RUNE = envVars.INVOICE_RUNE || ''; - LN_MESSAGE_CONFIG.remoteNodePublicKey = envVars.LIGHTNING_PUBKEY; - GRPC_CONFIG.pubkey = envVars.LIGHTNING_PUBKEY; - if (APP_CONSTANTS.LIGHTNING_CERTS_PATH === '') { - APP_CONSTANTS.LIGHTNING_CERTS_PATH = - APP_CONSTANTS.LIGHTNING_PATH + sep + APP_CONSTANTS.BITCOIN_NETWORK + sep; - } - let clientKey = ''; - let clientCert = ''; - let caCert = ''; - if (fs.existsSync('package.json')) { - const packageData = Buffer.from(fs.readFileSync('package.json')).toString(); - APP_CONSTANTS.APP_VERSION = JSON.parse(packageData).version; - } - if (fs.existsSync(APP_CONSTANTS.LIGHTNING_CERTS_PATH + 'client-key.pem')) { - clientKey = fs.readFileSync(APP_CONSTANTS.LIGHTNING_CERTS_PATH + 'client-key.pem').toString(); - APP_CONSTANTS.CLIENT_KEY = clientKey - .replace(/(\r\n|\n|\r)/gm, '') - .replace('-----BEGIN PRIVATE KEY-----', '') - .replace('-----END PRIVATE KEY-----', ''); - } - if (fs.existsSync(APP_CONSTANTS.LIGHTNING_CERTS_PATH + 'client.pem')) { - clientCert = fs.readFileSync(APP_CONSTANTS.LIGHTNING_CERTS_PATH + 'client.pem').toString(); - APP_CONSTANTS.CLIENT_CERT = clientCert - .replace(/(\r\n|\n|\r)/gm, '') - .replace('-----BEGIN CERTIFICATE-----', '') - .replace('-----END CERTIFICATE-----', ''); - } - if (fs.existsSync(APP_CONSTANTS.LIGHTNING_CERTS_PATH + 'ca.pem')) { - caCert = fs.readFileSync(APP_CONSTANTS.LIGHTNING_CERTS_PATH + 'ca.pem').toString(); - APP_CONSTANTS.CA_CERT = caCert - .replace(/(\r\n|\n|\r)/gm, '') - .replace('-----BEGIN CERTIFICATE-----', '') - .replace('-----END CERTIFICATE-----', ''); - } -} diff --git a/apps/frontend/build/asset-manifest.json b/apps/frontend/build/asset-manifest.json deleted file mode 100644 index 3ac3ac77..00000000 --- a/apps/frontend/build/asset-manifest.json +++ /dev/null @@ -1,58 +0,0 @@ -{ - "files": { - "main.css": "/static/css/main.23f285da.css", - "main.js": "/static/js/main.d49a76db.js", - "static/css/687.19b1d4d6.chunk.css": "/static/css/687.19b1d4d6.chunk.css", - "static/js/687.c9ab0dcf.chunk.js": "/static/js/687.c9ab0dcf.chunk.js", - "static/css/458.11d4ccbb.chunk.css": "/static/css/458.11d4ccbb.chunk.css", - "static/js/458.7bbef648.chunk.js": "/static/js/458.7bbef648.chunk.js", - "static/css/78.93e26be7.chunk.css": "/static/css/78.93e26be7.chunk.css", - "static/js/78.c368a490.chunk.js": "/static/js/78.c368a490.chunk.js", - "static/css/72.b5bbe8d8.chunk.css": "/static/css/72.b5bbe8d8.chunk.css", - "static/js/72.584c146f.chunk.js": "/static/js/72.584c146f.chunk.js", - "static/css/489.e31d0844.chunk.css": "/static/css/489.e31d0844.chunk.css", - "static/js/489.21126faa.chunk.js": "/static/js/489.21126faa.chunk.js", - "static/css/906.a46f8fc5.chunk.css": "/static/css/906.a46f8fc5.chunk.css", - "static/js/906.4ca8967c.chunk.js": "/static/js/906.4ca8967c.chunk.js", - "static/js/813.cdbe58bb.chunk.js": "/static/js/813.cdbe58bb.chunk.js", - "static/js/213.6d084fd7.chunk.js": "/static/js/213.6d084fd7.chunk.js", - "static/js/768.59f5d13d.chunk.js": "/static/js/768.59f5d13d.chunk.js", - "static/css/880.70c3ebe7.chunk.css": "/static/css/880.70c3ebe7.chunk.css", - "static/js/880.cd8c8d95.chunk.js": "/static/js/880.cd8c8d95.chunk.js", - "static/js/400.868d8ca3.chunk.js": "/static/js/400.868d8ca3.chunk.js", - "static/js/165.41102937.chunk.js": "/static/js/165.41102937.chunk.js", - "static/js/408.5da42039.chunk.js": "/static/js/408.5da42039.chunk.js", - "static/css/63.31d6cfe0.chunk.css": "/static/css/63.31d6cfe0.chunk.css", - "static/media/Inter-Bold.ttf": "/static/media/Inter-Bold.88fa7ae373b07b41ecce.ttf", - "static/media/Inter-SemiBold.ttf": "/static/media/Inter-SemiBold.4d56bb21f2399db8ad48.ttf", - "static/media/Inter-Medium.ttf": "/static/media/Inter-Medium.6dcbc9bed1ec438907ee.ttf", - "static/media/Inter-Thin.ttf": "/static/media/Inter-Thin.f341ca512063c66296d1.ttf", - "index.html": "/index.html", - "main.23f285da.css.map": "/static/css/main.23f285da.css.map", - "main.d49a76db.js.map": "/static/js/main.d49a76db.js.map", - "687.19b1d4d6.chunk.css.map": "/static/css/687.19b1d4d6.chunk.css.map", - "687.c9ab0dcf.chunk.js.map": "/static/js/687.c9ab0dcf.chunk.js.map", - "458.11d4ccbb.chunk.css.map": "/static/css/458.11d4ccbb.chunk.css.map", - "458.7bbef648.chunk.js.map": "/static/js/458.7bbef648.chunk.js.map", - "78.93e26be7.chunk.css.map": "/static/css/78.93e26be7.chunk.css.map", - "78.c368a490.chunk.js.map": "/static/js/78.c368a490.chunk.js.map", - "72.b5bbe8d8.chunk.css.map": "/static/css/72.b5bbe8d8.chunk.css.map", - "72.584c146f.chunk.js.map": "/static/js/72.584c146f.chunk.js.map", - "489.e31d0844.chunk.css.map": "/static/css/489.e31d0844.chunk.css.map", - "489.21126faa.chunk.js.map": "/static/js/489.21126faa.chunk.js.map", - "906.a46f8fc5.chunk.css.map": "/static/css/906.a46f8fc5.chunk.css.map", - "906.4ca8967c.chunk.js.map": "/static/js/906.4ca8967c.chunk.js.map", - "813.cdbe58bb.chunk.js.map": "/static/js/813.cdbe58bb.chunk.js.map", - "213.6d084fd7.chunk.js.map": "/static/js/213.6d084fd7.chunk.js.map", - "768.59f5d13d.chunk.js.map": "/static/js/768.59f5d13d.chunk.js.map", - "880.70c3ebe7.chunk.css.map": "/static/css/880.70c3ebe7.chunk.css.map", - "880.cd8c8d95.chunk.js.map": "/static/js/880.cd8c8d95.chunk.js.map", - "400.868d8ca3.chunk.js.map": "/static/js/400.868d8ca3.chunk.js.map", - "165.41102937.chunk.js.map": "/static/js/165.41102937.chunk.js.map", - "408.5da42039.chunk.js.map": "/static/js/408.5da42039.chunk.js.map" - }, - "entrypoints": [ - "static/css/main.23f285da.css", - "static/js/main.d49a76db.js" - ] -} \ No newline at end of file diff --git a/apps/frontend/build/fonts/Inter-Bold.ttf b/apps/frontend/build/fonts/Inter-Bold.ttf deleted file mode 100644 index 8e82c70d..00000000 Binary files a/apps/frontend/build/fonts/Inter-Bold.ttf and /dev/null differ diff --git a/apps/frontend/build/fonts/Inter-Medium.ttf b/apps/frontend/build/fonts/Inter-Medium.ttf deleted file mode 100644 index b53fb1c4..00000000 Binary files a/apps/frontend/build/fonts/Inter-Medium.ttf and /dev/null differ diff --git a/apps/frontend/build/fonts/Inter-Regular.ttf b/apps/frontend/build/fonts/Inter-Regular.ttf deleted file mode 100644 index 8d4eebf2..00000000 Binary files a/apps/frontend/build/fonts/Inter-Regular.ttf and /dev/null differ diff --git a/apps/frontend/build/fonts/Inter-SemiBold.ttf b/apps/frontend/build/fonts/Inter-SemiBold.ttf deleted file mode 100644 index c6aeeb16..00000000 Binary files a/apps/frontend/build/fonts/Inter-SemiBold.ttf and /dev/null differ diff --git a/apps/frontend/build/fonts/Inter-Thin.ttf b/apps/frontend/build/fonts/Inter-Thin.ttf deleted file mode 100644 index 7aed55d5..00000000 Binary files a/apps/frontend/build/fonts/Inter-Thin.ttf and /dev/null differ diff --git a/apps/frontend/build/fonts/OFL.txt b/apps/frontend/build/fonts/OFL.txt deleted file mode 100644 index ad214842..00000000 --- a/apps/frontend/build/fonts/OFL.txt +++ /dev/null @@ -1,93 +0,0 @@ -Copyright 2020 The Inter Project Authors (https://github.com/rsms/inter) - -This Font Software is licensed under the SIL Open Font License, Version 1.1. -This license is copied below, and is also available with a FAQ at: -http://scripts.sil.org/OFL - - ------------------------------------------------------------ -SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 ------------------------------------------------------------ - -PREAMBLE -The goals of the Open Font License (OFL) are to stimulate worldwide -development of collaborative font projects, to support the font creation -efforts of academic and linguistic communities, and to provide a free and -open framework in which fonts may be shared and improved in partnership -with others. - -The OFL allows the licensed fonts to be used, studied, modified and -redistributed freely as long as they are not sold by themselves. The -fonts, including any derivative works, can be bundled, embedded, -redistributed and/or sold with any software provided that any reserved -names are not used by derivative works. The fonts and derivatives, -however, cannot be released under any other type of license. The -requirement for fonts to remain under this license does not apply -to any document created using the fonts or their derivatives. - -DEFINITIONS -"Font Software" refers to the set of files released by the Copyright -Holder(s) under this license and clearly marked as such. This may -include source files, build scripts and documentation. - -"Reserved Font Name" refers to any names specified as such after the -copyright statement(s). - -"Original Version" refers to the collection of Font Software components as -distributed by the Copyright Holder(s). - -"Modified Version" refers to any derivative made by adding to, deleting, -or substituting -- in part or in whole -- any of the components of the -Original Version, by changing formats or by porting the Font Software to a -new environment. - -"Author" refers to any designer, engineer, programmer, technical -writer or other person who contributed to the Font Software. - -PERMISSION & CONDITIONS -Permission is hereby granted, free of charge, to any person obtaining -a copy of the Font Software, to use, study, copy, merge, embed, modify, -redistribute, and sell modified and unmodified copies of the Font -Software, subject to the following conditions: - -1) Neither the Font Software nor any of its individual components, -in Original or Modified Versions, may be sold by itself. - -2) Original or Modified Versions of the Font Software may be bundled, -redistributed and/or sold with any software, provided that each copy -contains the above copyright notice and this license. These can be -included either as stand-alone text files, human-readable headers or -in the appropriate machine-readable metadata fields within text or -binary files as long as those fields can be easily viewed by the user. - -3) No Modified Version of the Font Software may use the Reserved Font -Name(s) unless explicit written permission is granted by the corresponding -Copyright Holder. This restriction only applies to the primary font name as -presented to the users. - -4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font -Software shall not be used to promote, endorse or advertise any -Modified Version, except to acknowledge the contribution(s) of the -Copyright Holder(s) and the Author(s) or with their explicit written -permission. - -5) The Font Software, modified or unmodified, in part or in whole, -must be distributed entirely under this license, and must not be -distributed under any other license. The requirement for fonts to -remain under this license does not apply to any document created -using the Font Software. - -TERMINATION -This license becomes null and void if any of the above conditions are -not met. - -DISCLAIMER -THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT -OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE -COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL -DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM -OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/apps/frontend/build/fonts/README.txt b/apps/frontend/build/fonts/README.txt deleted file mode 100644 index 3078f199..00000000 --- a/apps/frontend/build/fonts/README.txt +++ /dev/null @@ -1,72 +0,0 @@ -Inter Variable Font -=================== - -This download contains Inter as both a variable font and static fonts. - -Inter is a variable font with these axes: - slnt - wght - -This means all the styles are contained in a single file: - Inter-VariableFont_slnt,wght.ttf - -If your app fully supports variable fonts, you can now pick intermediate styles -that aren’t available as static fonts. Not all apps support variable fonts, and -in those cases you can use the static font files for Inter: - static/Inter-Thin.ttf - static/Inter-ExtraLight.ttf - static/Inter-Light.ttf - static/Inter-Regular.ttf - static/Inter-Medium.ttf - static/Inter-SemiBold.ttf - static/Inter-Bold.ttf - static/Inter-ExtraBold.ttf - static/Inter-Black.ttf - -Get started ------------ - -1. Install the font files you want to use - -2. Use your app's font picker to view the font family and all the -available styles - -Learn more about variable fonts -------------------------------- - - https://developers.google.com/web/fundamentals/design-and-ux/typography/variable-fonts - https://variablefonts.typenetwork.com - https://medium.com/variable-fonts - -In desktop apps - - https://theblog.adobe.com/can-variable-fonts-illustrator-cc - https://helpx.adobe.com/nz/photoshop/using/fonts.html#variable_fonts - -Online - - https://developers.google.com/fonts/docs/getting_started - https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Fonts/Variable_Fonts_Guide - https://developer.microsoft.com/en-us/microsoft-edge/testdrive/demos/variable-fonts - -Installing fonts - - MacOS: https://support.apple.com/en-us/HT201749 - Linux: https://www.google.com/search?q=how+to+install+a+font+on+gnu%2Blinux - Windows: https://support.microsoft.com/en-us/help/314960/how-to-install-or-remove-a-font-in-windows - -Android Apps - - https://developers.google.com/fonts/docs/android - https://developer.android.com/guide/topics/ui/look-and-feel/downloadable-fonts - -License -------- -Please read the full license text (OFL.txt) to understand the permissions, -restrictions and requirements for usage, redistribution, and modification. - -You can use them in your products & projects – print or digital, -commercial or otherwise. - -This isn't legal advice, please consider consulting a lawyer and see the full -license for all details. diff --git a/apps/frontend/build/images/cln-favicon.ico b/apps/frontend/build/images/cln-favicon.ico deleted file mode 100644 index 713a76a7..00000000 Binary files a/apps/frontend/build/images/cln-favicon.ico and /dev/null differ diff --git a/apps/frontend/build/images/cln-logo-dark.png b/apps/frontend/build/images/cln-logo-dark.png deleted file mode 100644 index 3410d29f..00000000 Binary files a/apps/frontend/build/images/cln-logo-dark.png and /dev/null differ diff --git a/apps/frontend/build/images/cln-logo-dark.svg b/apps/frontend/build/images/cln-logo-dark.svg deleted file mode 100644 index 41ad23da..00000000 --- a/apps/frontend/build/images/cln-logo-dark.svg +++ /dev/null @@ -1,5 +0,0 @@ - diff --git a/apps/frontend/build/images/cln-logo-light.png b/apps/frontend/build/images/cln-logo-light.png deleted file mode 100644 index 53e99d4a..00000000 Binary files a/apps/frontend/build/images/cln-logo-light.png and /dev/null differ diff --git a/apps/frontend/build/images/cln-logo-light.svg b/apps/frontend/build/images/cln-logo-light.svg deleted file mode 100644 index 7c641690..00000000 --- a/apps/frontend/build/images/cln-logo-light.svg +++ /dev/null @@ -1,5 +0,0 @@ - diff --git a/apps/frontend/build/index.html b/apps/frontend/build/index.html deleted file mode 100644 index 03551dd7..00000000 --- a/apps/frontend/build/index.html +++ /dev/null @@ -1 +0,0 @@ -
`s get reset. However, we also reset the\n// bottom margin to use `rem` units instead of `em`.\n\np {\n margin-top: 0;\n margin-bottom: $paragraph-margin-bottom;\n}\n\n\n// Abbreviations\n//\n// 1. Add the correct text decoration in Chrome, Edge, Opera, and Safari.\n// 2. Add explicit cursor to indicate changed behavior.\n// 3. Prevent the text-decoration to be skipped.\n\nabbr[title] {\n text-decoration: underline dotted; // 1\n cursor: help; // 2\n text-decoration-skip-ink: none; // 3\n}\n\n\n// Address\n\naddress {\n margin-bottom: 1rem;\n font-style: normal;\n line-height: inherit;\n}\n\n\n// Lists\n\nol,\nul {\n padding-left: 2rem;\n}\n\nol,\nul,\ndl {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nol ol,\nul ul,\nol ul,\nul ol {\n margin-bottom: 0;\n}\n\ndt {\n font-weight: $dt-font-weight;\n}\n\n// 1. Undo browser default\n\ndd {\n margin-bottom: .5rem;\n margin-left: 0; // 1\n}\n\n\n// Blockquote\n\nblockquote {\n margin: 0 0 1rem;\n}\n\n\n// Strong\n//\n// Add the correct font weight in Chrome, Edge, and Safari\n\nb,\nstrong {\n font-weight: $font-weight-bolder;\n}\n\n\n// Small\n//\n// Add the correct font size in all browsers\n\nsmall {\n @include font-size($small-font-size);\n}\n\n\n// Mark\n\nmark {\n padding: $mark-padding;\n color: var(--#{$prefix}highlight-color);\n background-color: var(--#{$prefix}highlight-bg);\n}\n\n\n// Sub and Sup\n//\n// Prevent `sub` and `sup` elements from affecting the line height in\n// all browsers.\n\nsub,\nsup {\n position: relative;\n @include font-size($sub-sup-font-size);\n line-height: 0;\n vertical-align: baseline;\n}\n\nsub { bottom: -.25em; }\nsup { top: -.5em; }\n\n\n// Links\n\na {\n color: rgba(var(--#{$prefix}link-color-rgb), var(--#{$prefix}link-opacity, 1));\n text-decoration: $link-decoration;\n\n &:hover {\n --#{$prefix}link-color-rgb: var(--#{$prefix}link-hover-color-rgb);\n text-decoration: $link-hover-decoration;\n }\n}\n\n// And undo these styles for placeholder links/named anchors (without href).\n// It would be more straightforward to just use a[href] in previous block, but that\n// causes specificity issues in many other styles that are too complex to fix.\n// See https://github.com/twbs/bootstrap/issues/19402\n\na:not([href]):not([class]) {\n &,\n &:hover {\n color: inherit;\n text-decoration: none;\n }\n}\n\n\n// Code\n\npre,\ncode,\nkbd,\nsamp {\n font-family: $font-family-code;\n @include font-size(1em); // Correct the odd `em` font sizing in all browsers.\n}\n\n// 1. Remove browser default top margin\n// 2. Reset browser default of `1em` to use `rem`s\n// 3. Don't allow content to break outside\n\npre {\n display: block;\n margin-top: 0; // 1\n margin-bottom: 1rem; // 2\n overflow: auto; // 3\n @include font-size($code-font-size);\n color: $pre-color;\n\n // Account for some code outputs that place code tags in pre tags\n code {\n @include font-size(inherit);\n color: inherit;\n word-break: normal;\n }\n}\n\ncode {\n @include font-size($code-font-size);\n color: var(--#{$prefix}code-color);\n word-wrap: break-word;\n\n // Streamline the style when inside anchors to avoid broken underline and more\n a > & {\n color: inherit;\n }\n}\n\nkbd {\n padding: $kbd-padding-y $kbd-padding-x;\n @include font-size($kbd-font-size);\n color: $kbd-color;\n background-color: $kbd-bg;\n @include border-radius($border-radius-sm);\n\n kbd {\n padding: 0;\n @include font-size(1em);\n font-weight: $nested-kbd-font-weight;\n }\n}\n\n\n// Figures\n//\n// Apply a consistent margin strategy (matches our type styles).\n\nfigure {\n margin: 0 0 1rem;\n}\n\n\n// Images and content\n\nimg,\nsvg {\n vertical-align: middle;\n}\n\n\n// Tables\n//\n// Prevent double borders\n\ntable {\n caption-side: bottom;\n border-collapse: collapse;\n}\n\ncaption {\n padding-top: $table-cell-padding-y;\n padding-bottom: $table-cell-padding-y;\n color: $table-caption-color;\n text-align: left;\n}\n\n// 1. Removes font-weight bold by inheriting\n// 2. Matches default `