Compare commits

..

8 Commits

8 changed files with 395 additions and 172 deletions

View File

@ -1,7 +1,7 @@
{ {
"name": "@supernets/hardlounge", "name": "@supernets/hardlounge",
"description": "The self-hosted Web IRC client", "description": "The self-hosted Web IRC client",
"version": "4.4.1", "version": "4.4.1-1",
"preferGlobal": true, "preferGlobal": true,
"bin": { "bin": {
"hardlounge": "index.js" "hardlounge": "index.js"
@ -54,6 +54,7 @@
"dependencies": { "dependencies": {
"@fastify/busboy": "1.0.0", "@fastify/busboy": "1.0.0",
"bcryptjs": "2.4.3", "bcryptjs": "2.4.3",
"caniuse-lite": "^1.0.30001561",
"chalk": "4.1.2", "chalk": "4.1.2",
"cheerio": "1.0.0-rc.12", "cheerio": "1.0.0-rc.12",
"commander": "9.0.0", "commander": "9.0.0",
@ -61,6 +62,7 @@
"express": "4.17.3", "express": "4.17.3",
"file-type": "16.5.4", "file-type": "16.5.4",
"filenamify": "4.3.0", "filenamify": "4.3.0",
"get-func-name": "2.0.2",
"got": "11.8.5", "got": "11.8.5",
"irc-framework": "4.13.1", "irc-framework": "4.13.1",
"is-utf8": "0.2.1", "is-utf8": "0.2.1",
@ -91,18 +93,18 @@
"@istanbuljs/nyc-config-typescript": "1.0.2", "@istanbuljs/nyc-config-typescript": "1.0.2",
"@textcomplete/core": "0.1.10", "@textcomplete/core": "0.1.10",
"@textcomplete/textarea": "0.1.12", "@textcomplete/textarea": "0.1.12",
"@types/bcryptjs": "2.4.4", "@types/bcryptjs": "2.4.5",
"@types/chai": "4.3.5", "@types/chai": "4.3.5",
"@types/cheerio": "0.22.31", "@types/cheerio": "0.22.33",
"@types/content-disposition": "0.5.5", "@types/content-disposition": "0.5.7",
"@types/express": "4.17.13", "@types/express": "4.17.13",
"@types/is-utf8": "0.2.1", "@types/is-utf8": "0.2.2",
"@types/ldapjs": "2.2.2", "@types/ldapjs": "2.2.2",
"@types/linkify-it": "3.0.3", "@types/linkify-it": "3.0.3",
"@types/lodash": "4.14.195", "@types/lodash": "4.14.200",
"@types/mime-types": "2.1.1", "@types/mime-types": "2.1.1",
"@types/mocha": "9.1.1", "@types/mocha": "9.1.1",
"@types/mousetrap": "1.6.11", "@types/mousetrap": "1.6.13",
"@types/node": "17.0.31", "@types/node": "17.0.31",
"@types/read": "0.0.29", "@types/read": "0.0.29",
"@types/semver": "7.3.9", "@types/semver": "7.3.9",

View File

@ -5,12 +5,12 @@ import fs from "fs";
import path from "path"; import path from "path";
import Auth from "./plugins/auth"; import Auth from "./plugins/auth";
import Client, {UserConfig} from "./client"; import Client, { UserConfig } from "./client";
import Config from "./config"; import Config from "./config";
import {NetworkConfig} from "./models/network"; import { NetworkConfig } from "./models/network";
import WebPush from "./plugins/webpush"; import WebPush from "./plugins/webpush";
import log from "./log"; import log from "./log";
import {Server} from "socket.io"; import { Server } from "socket.io";
class ClientManager { class ClientManager {
clients: Client[]; clients: Client[];
@ -107,17 +107,21 @@ class ClientManager {
// Existing users removed since last time users were loaded // Existing users removed since last time users were loaded
_.difference(loaded, updatedUsers).forEach((name) => { _.difference(loaded, updatedUsers).forEach((name) => {
const client = _.find(this.clients, {name}); const client = _.find(this.clients, { name });
if (client) { if (client) {
client.quit(true); client.quit(true);
this.clients = _.without(this.clients, client); this.clients = _.without(this.clients, client);
log.info(`User ${colors.bold(name)} disconnected and removed.`); log.info(
`User ${colors.bold(
name
)} disconnected and removed.`
);
} }
}); });
}, },
1000, 1000,
{maxWait: 10000} { maxWait: 10000 }
) )
); );
} }
@ -197,7 +201,8 @@ class ClientManager {
if ( if (
userFolderStat && userFolderStat &&
userFileStat && userFileStat &&
(userFolderStat.uid !== userFileStat.uid || userFolderStat.gid !== userFileStat.gid) (userFolderStat.uid !== userFileStat.uid ||
userFolderStat.gid !== userFileStat.gid)
) { ) {
log.warn( log.warn(
`User ${colors.green( `User ${colors.green(
@ -227,13 +232,16 @@ class ClientManager {
networks: client.networks.map((n) => n.export()), networks: client.networks.map((n) => n.export()),
}); });
const newUser = JSON.stringify(json, null, "\t"); const newUser = JSON.stringify(json, null, "\t");
const newHash = crypto.createHash("sha256").update(newUser).digest("hex"); const newHash = crypto
.createHash("sha256")
.update(newUser)
.digest("hex");
return {newUser, newHash}; return { newUser, newHash };
} }
saveUser(client: Client, callback?: (err?: any) => void) { saveUser(client: Client, callback?: (err?: any) => void) {
const {newUser, newHash} = this.getDataToSave(client); const { newUser, newHash } = this.getDataToSave(client);
// Do not write to disk if the exported data hasn't actually changed // Do not write to disk if the exported data hasn't actually changed
if (client.fileHash === newHash) { if (client.fileHash === newHash) {
@ -254,7 +262,9 @@ class ClientManager {
return callback ? callback() : true; return callback ? callback() : true;
} catch (e: any) { } catch (e: any) {
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
log.error(`Failed to update user ${colors.green(client.name)} (${e})`); log.error(
`Failed to update user ${colors.green(client.name)} (${e})`
);
if (callback) { if (callback) {
callback(e); callback(e);
@ -266,7 +276,9 @@ class ClientManager {
const userPath = Config.getUserConfigPath(name); const userPath = Config.getUserConfigPath(name);
if (!fs.existsSync(userPath)) { if (!fs.existsSync(userPath)) {
log.error(`Tried to remove non-existing user ${colors.green(name)}.`); log.error(
`Tried to remove non-existing user ${colors.green(name)}.`
);
return false; return false;
} }
@ -275,7 +287,7 @@ class ClientManager {
return true; return true;
} }
private readUserConfig(name: string) { readUserConfig(name: string) {
const userPath = Config.getUserConfigPath(name); const userPath = Config.getUserConfigPath(name);
if (!fs.existsSync(userPath)) { if (!fs.existsSync(userPath)) {

View File

@ -3,7 +3,7 @@ import log from "../log";
import fs from "fs"; import fs from "fs";
import path from "path"; import path from "path";
import colors from "chalk"; import colors from "chalk";
import {Command} from "commander"; import { Command } from "commander";
import Helper from "../helper"; import Helper from "../helper";
import Config from "../config"; import Config from "../config";
import Utils from "./utils"; import Utils from "./utils";
@ -42,6 +42,7 @@ program.addCommand(require("./install").default);
program.addCommand(require("./uninstall").default); program.addCommand(require("./uninstall").default);
program.addCommand(require("./upgrade").default); program.addCommand(require("./upgrade").default);
program.addCommand(require("./outdated").default); program.addCommand(require("./outdated").default);
program.addCommand(require("./storage").default);
if (!Config.values.public) { if (!Config.values.public) {
require("./users").default.forEach((command: Command) => { require("./users").default.forEach((command: Command) => {
@ -64,7 +65,7 @@ function createPackagesFolder() {
const packagesConfig = path.join(packagesPath, "package.json"); const packagesConfig = path.join(packagesPath, "package.json");
// Create node_modules folder, otherwise yarn will start walking upwards to find one // Create node_modules folder, otherwise yarn will start walking upwards to find one
fs.mkdirSync(path.join(packagesPath, "node_modules"), {recursive: true}); fs.mkdirSync(path.join(packagesPath, "node_modules"), { recursive: true });
// Create package.json with private set to true, if it doesn't exist already // Create package.json with private set to true, if it doesn't exist already
if (!fs.existsSync(packagesConfig)) { if (!fs.existsSync(packagesConfig)) {
@ -99,7 +100,9 @@ function verifyFileOwner() {
); );
} }
const configStat = fs.statSync(path.join(Config.getHomePath(), "config.js")); const configStat = fs.statSync(
path.join(Config.getHomePath(), "config.js")
);
if (configStat && configStat.uid !== uid) { if (configStat && configStat.uid !== uid) {
log.warn( log.warn(

View File

@ -0,0 +1,68 @@
import log from "../log";
import { Command } from "commander";
import ClientManager from "../clientManager";
import Utils from "./utils";
import SqliteMessageStorage from "../plugins/messageStorage/sqlite";
const program = new Command("storage").description(
"various utilities related to the message storage"
);
program
.command("migrate")
.argument("[user]", "migrate a specific user only, all if not provided")
.description("Migrate message storage where needed")
.on("--help", Utils.extraHelp)
.action(function (user) {
runMigrations(user).catch((err) => {
log.error(err.toString());
process.exit(1);
});
});
async function runMigrations(user: string) {
const manager = new ClientManager();
const users = manager.getUsers();
if (user) {
if (!users.includes(user)) {
throw new Error(`invalid user ${user}`);
}
return migrateUser(manager, user);
}
for (const name of users) {
await migrateUser(manager, name);
// if any migration fails we blow up,
// chances are the rest won't complete either
}
}
// runs sqlite migrations for a user, which must exist
async function migrateUser(manager: ClientManager, user: string) {
log.info("handling user", user);
if (!isUserLogEnabled(manager, user)) {
log.info("logging disabled for user", user, ". Skipping");
return;
}
const sqlite = new SqliteMessageStorage(user);
await sqlite.enable(); // enable runs migrations
await sqlite.close();
log.info("user", user, "migrated successfully");
}
function isUserLogEnabled(manager: ClientManager, user: string): boolean {
const conf = manager.readUserConfig(user);
if (!conf) {
log.error("Could not open user configuration of", user);
return false;
}
return conf.log;
}
export default program;

View File

@ -1,13 +1,17 @@
import type {Database} from "sqlite3"; import type { Database } from "sqlite3";
import log from "../../log"; import log from "../../log";
import path from "path"; import path from "path";
import fs from "fs/promises"; import fs from "fs/promises";
import Config from "../../config"; import Config from "../../config";
import Msg, {Message} from "../../models/msg"; import Msg, { Message } from "../../models/msg";
import Chan, {Channel} from "../../models/chan"; import Chan, { Channel } from "../../models/chan";
import Helper from "../../helper"; import Helper from "../../helper";
import type {SearchResponse, SearchQuery, SearchableMessageStorage} from "./types"; import type {
SearchResponse,
SearchQuery,
SearchableMessageStorage,
} from "./types";
import Network from "../../models/network"; import Network from "../../models/network";
// TODO; type // TODO; type
@ -16,15 +20,21 @@ let sqlite3: any;
try { try {
sqlite3 = require("sqlite3"); sqlite3 = require("sqlite3");
} catch (e: any) { } catch (e: any) {
Config.values.messageStorage = Config.values.messageStorage.filter((item) => item !== "sqlite"); Config.values.messageStorage = Config.values.messageStorage.filter(
(item) => item !== "sqlite"
);
log.error( log.error(
"Unable to load sqlite3 module. See https://github.com/mapbox/node-sqlite3/wiki/Binaries" "Unable to load sqlite3 module. See https://github.com/mapbox/node-sqlite3/wiki/Binaries"
); );
} }
type Migration = {version: number; stmts: string[]}; type Migration = { version: number; stmts: string[] };
type Rollback = {version: number; rollback_forbidden?: boolean; stmts: string[]}; type Rollback = {
version: number;
rollback_forbidden?: boolean;
stmts: string[];
};
export const currentSchemaVersion = 1679743888000; // use `new Date().getTime()` export const currentSchemaVersion = 1679743888000; // use `new Date().getTime()`
@ -121,7 +131,7 @@ class SqliteMessageStorage implements SearchableMessageStorage {
const sqlitePath = path.join(logsPath, `${this.userName}.sqlite3`); const sqlitePath = path.join(logsPath, `${this.userName}.sqlite3`);
try { try {
await fs.mkdir(logsPath, {recursive: true}); await fs.mkdir(logsPath, { recursive: true });
} catch (e) { } catch (e) {
throw Helper.catch_to_error("Unable to create logs directory", e); throw Helper.catch_to_error("Unable to create logs directory", e);
} }
@ -152,9 +162,10 @@ class SqliteMessageStorage implements SearchableMessageStorage {
await this.serialize_run(stmt, []); await this.serialize_run(stmt, []);
} }
await this.serialize_run("INSERT INTO options (name, value) VALUES ('schema_version', ?)", [ await this.serialize_run(
currentSchemaVersion.toString(), "INSERT INTO options (name, value) VALUES ('schema_version', ?)",
]); [currentSchemaVersion.toString()]
);
} }
async current_version(): Promise<number> { async current_version(): Promise<number> {
@ -181,9 +192,10 @@ class SqliteMessageStorage implements SearchableMessageStorage {
} }
async update_version_in_db() { async update_version_in_db() {
return this.serialize_run("UPDATE options SET value = ? WHERE name = 'schema_version'", [ return this.serialize_run(
currentSchemaVersion.toString(), "UPDATE options SET value = ? WHERE name = 'schema_version'",
]); [currentSchemaVersion.toString()]
);
} }
async _run_migrations(dbVersion: number) { async _run_migrations(dbVersion: number) {
@ -274,7 +286,7 @@ class SqliteMessageStorage implements SearchableMessageStorage {
stmts: [raw.statement], stmts: [raw.statement],
}); });
} else { } else {
last.stmts.push(raw.statment); last.stmts.push(raw.statement);
} }
} }
@ -282,7 +294,10 @@ class SqliteMessageStorage implements SearchableMessageStorage {
} }
async delete_migrations_older_than(version: number) { async delete_migrations_older_than(version: number) {
return this.serialize_run("delete from migrations where migrations.version > ?", [version]); return this.serialize_run(
"delete from migrations where migrations.version > ?",
[version]
);
} }
async _downgrade_to(version: number) { async _downgrade_to(version: number) {
@ -307,7 +322,7 @@ class SqliteMessageStorage implements SearchableMessageStorage {
await this.delete_migrations_older_than(version); await this.delete_migrations_older_than(version);
await this.update_version_in_db(); await this.update_version_in_db();
return _rollbacks.at(-1)!.version; // assert valid due to length guard above return version;
} }
async downgrade_to(version: number) { async downgrade_to(version: number) {
@ -372,7 +387,12 @@ class SqliteMessageStorage implements SearchableMessageStorage {
// id is regenerated when messages are retrieved // id is regenerated when messages are retrieved
// previews are not stored because storage is cleared on lounge restart // previews are not stored because storage is cleared on lounge restart
// type and time are stored in a separate column // type and time are stored in a separate column
if (prop !== "id" && prop !== "previews" && prop !== "type" && prop !== "time") { if (
prop !== "id" &&
prop !== "previews" &&
prop !== "type" &&
prop !== "time"
) {
newMsg[prop] = msg[prop]; newMsg[prop] = msg[prop];
} }
@ -398,10 +418,10 @@ class SqliteMessageStorage implements SearchableMessageStorage {
return; return;
} }
await this.serialize_run("DELETE FROM messages WHERE network = ? AND channel = ?", [ await this.serialize_run(
network.uuid, "DELETE FROM messages WHERE network = ? AND channel = ?",
channel.name.toLowerCase(), [network.uuid, channel.name.toLowerCase()]
]); );
} }
async getMessages( async getMessages(
@ -416,7 +436,8 @@ class SqliteMessageStorage implements SearchableMessageStorage {
} }
// If unlimited history is specified, load 100k messages // If unlimited history is specified, load 100k messages
const limit = Config.values.maxHistory < 0 ? 100000 : Config.values.maxHistory; const limit =
Config.values.maxHistory < 0 ? 100000 : Config.values.maxHistory;
const rows = await this.serialize_fetchall( const rows = await this.serialize_fetchall(
"SELECT msg, type, time FROM messages WHERE network = ? AND channel = ? ORDER BY time DESC LIMIT ?", "SELECT msg, type, time FROM messages WHERE network = ? AND channel = ? ORDER BY time DESC LIMIT ?",

View File

@ -1,9 +1,9 @@
import _ from "lodash"; import _ from "lodash";
import {Server as wsServer} from "ws"; import { Server as wsServer } from "ws";
import express, {NextFunction, Request, Response} from "express"; import express, { NextFunction, Request, Response } from "express";
import fs from "fs"; import fs from "fs";
import path from "path"; import path from "path";
import {Server, Socket} from "socket.io"; import { Server, Socket } from "socket.io";
import dns from "dns"; import dns from "dns";
import colors from "chalk"; import colors from "chalk";
import net from "net"; import net from "net";
@ -13,18 +13,18 @@ import Client from "./client";
import ClientManager from "./clientManager"; import ClientManager from "./clientManager";
import Uploader from "./plugins/uploader"; import Uploader from "./plugins/uploader";
import Helper from "./helper"; import Helper from "./helper";
import Config, {ConfigType, Defaults} from "./config"; import Config, { ConfigType, Defaults } from "./config";
import Identification from "./identification"; import Identification from "./identification";
import changelog from "./plugins/changelog"; import changelog from "./plugins/changelog";
import inputs from "./plugins/inputs"; import inputs from "./plugins/inputs";
import Auth from "./plugins/auth"; import Auth from "./plugins/auth";
import themes, {ThemeForClient} from "./plugins/packages/themes"; import themes, { ThemeForClient } from "./plugins/packages/themes";
themes.loadLocalThemes(); themes.loadLocalThemes();
import packages from "./plugins/packages/index"; import packages from "./plugins/packages/index";
import {NetworkWithIrcFramework} from "./models/network"; import { NetworkWithIrcFramework } from "./models/network";
import {ChanType} from "./models/chan"; import { ChanType } from "./models/chan";
import Utils from "./command-line/utils"; import Utils from "./command-line/utils";
import type { import type {
ClientToServerEvents, ClientToServerEvents,
@ -72,9 +72,9 @@ export default async function (
} }
) { ) {
log.info(`Hard Lounge ${colors.green(Helper.getVersion())} \ log.info(`Hard Lounge ${colors.green(Helper.getVersion())} \
(Node.js ${colors.green(process.versions.node)} on ${colors.green(process.platform)} ${ (Node.js ${colors.green(process.versions.node)} on ${colors.green(
process.arch process.platform
})`); )} ${process.arch})`);
log.info(`Configuration file: ${colors.green(Config.getConfigPath())}`); log.info(`Configuration file: ${colors.green(Config.getConfigPath())}`);
const staticOptions = { const staticOptions = {
@ -96,8 +96,16 @@ export default async function (
.get("/service-worker.js", forceNoCacheRequest) .get("/service-worker.js", forceNoCacheRequest)
.get("/js/bundle.js.map", forceNoCacheRequest) .get("/js/bundle.js.map", forceNoCacheRequest)
.get("/css/style.css.map", forceNoCacheRequest) .get("/css/style.css.map", forceNoCacheRequest)
.use(express.static(Utils.getFileFromRelativeToRoot("public"), staticOptions)) .use(
.use("/storage/", express.static(Config.getStoragePath(), staticOptions)); express.static(
Utils.getFileFromRelativeToRoot("public"),
staticOptions
)
)
.use(
"/storage/",
express.static(Config.getStoragePath(), staticOptions)
);
if (Config.values.fileUpload.enable) { if (Config.values.fileUpload.enable) {
Uploader.router(app); Uploader.router(app);
@ -123,7 +131,10 @@ export default async function (
const fileName = req.params.filename; const fileName = req.params.filename;
const packageFile = packages.getPackage(packageName); const packageFile = packages.getPackage(packageName);
if (!packageFile || !packages.getFiles().includes(`${packageName}/${fileName}`)) { if (
!packageFile ||
!packages.getFiles().includes(`${packageName}/${fileName}`)
) {
return res.status(404).send("Not found"); return res.status(404).send("Not found");
} }
@ -180,7 +191,10 @@ export default async function (
host: string | undefined; host: string | undefined;
}; };
if (typeof Config.values.host === "string" && Config.values.host.startsWith("unix:")) { if (
typeof Config.values.host === "string" &&
Config.values.host.startsWith("unix:")
) {
listenParams = Config.values.host.replace(/^unix:/, ""); listenParams = Config.values.host.replace(/^unix:/, "");
} else { } else {
listenParams = { listenParams = {
@ -208,8 +222,12 @@ export default async function (
log.info( log.info(
"Available at " + "Available at " +
colors.green(`${protocol}://${address.address}:${address.port}/`) + colors.green(
` in ${colors.bold(Config.values.public ? "public" : "private")} mode` `${protocol}://${address.address}:${address.port}/`
) +
` in ${colors.bold(
Config.values.public ? "public" : "private"
)} mode`
); );
} }
} }
@ -267,7 +285,9 @@ export default async function (
log.error(`Could not start identd server, ${err.message}`); log.error(`Could not start identd server, ${err.message}`);
process.exit(1); process.exit(1);
} else if (!manager) { } else if (!manager) {
log.error("Could not start identd server, ClientManager is undefined"); log.error(
"Could not start identd server, ClientManager is undefined"
);
process.exit(1); process.exit(1);
} }
@ -290,7 +310,9 @@ export default async function (
} }
if (Config.values.prefetchStorage) { if (Config.values.prefetchStorage) {
log.info("Clearing prefetch storage folder, this might take a while..."); log.info(
"Clearing prefetch storage folder, this might take a while..."
);
(await import("./plugins/storage")).default.emptyDir(); (await import("./plugins/storage")).default.emptyDir();
} }
@ -316,7 +338,7 @@ export default async function (
// Clear storage folder after server starts successfully // Clear storage folder after server starts successfully
if (Config.values.prefetchStorage) { if (Config.values.prefetchStorage) {
import("./plugins/storage") import("./plugins/storage")
.then(({default: storage}) => { .then(({ default: storage }) => {
storage.emptyDir(); storage.emptyDir();
}) })
.catch((err: Error) => { .catch((err: Error) => {
@ -333,7 +355,10 @@ export default async function (
function getClientLanguage(socket: Socket): string | null { function getClientLanguage(socket: Socket): string | null {
const acceptLanguage = socket.handshake.headers["accept-language"]; const acceptLanguage = socket.handshake.headers["accept-language"];
if (typeof acceptLanguage === "string" && /^[\x00-\x7F]{1,50}$/.test(acceptLanguage)) { if (
typeof acceptLanguage === "string" &&
/^[\x00-\x7F]{1,50}$/.test(acceptLanguage)
) {
// only allow ASCII strings between 1-50 characters in length // only allow ASCII strings between 1-50 characters in length
return acceptLanguage; return acceptLanguage;
} }
@ -360,7 +385,10 @@ function getClientIp(socket: Socket) {
function getClientSecure(socket: Socket) { function getClientSecure(socket: Socket) {
let secure = socket.handshake.secure; let secure = socket.handshake.secure;
if (Config.values.reverseProxy && socket.handshake.headers["x-forwarded-proto"] === "https") { if (
Config.values.reverseProxy &&
socket.handshake.headers["x-forwarded-proto"] === "https"
) {
secure = true; secure = true;
} }
@ -390,7 +418,9 @@ function addSecurityHeaders(req: Request, res: Response, next: NextFunction) {
// - https://user-images.githubusercontent.com is where we currently push our changelog screenshots // - https://user-images.githubusercontent.com is where we currently push our changelog screenshots
// - data: is required for the HTML5 video player // - data: is required for the HTML5 video player
if (Config.values.prefetchStorage || !Config.values.prefetch) { if (Config.values.prefetchStorage || !Config.values.prefetch) {
policies.push("img-src 'self' data: https://user-images.githubusercontent.com"); policies.push(
"img-src 'self' data: https://user-images.githubusercontent.com"
);
policies.unshift("block-all-mixed-content"); policies.unshift("block-all-mixed-content");
} else { } else {
policies.push("img-src http: https: data:"); policies.push("img-src http: https: data:");
@ -422,7 +452,7 @@ function indexRequest(req: Request, res: Response) {
const config: IndexTemplateConfiguration = { const config: IndexTemplateConfiguration = {
...getServerConfiguration(), ...getServerConfiguration(),
...{cacheBust: Helper.getVersionCacheBust()}, ...{ cacheBust: Helper.getVersionCacheBust() },
}; };
res.send(_.template(file)(config)); res.send(_.template(file)(config));
@ -449,7 +479,9 @@ function initializeClient(
// If client provided channel passes checks, use it. if client has invalid // If client provided channel passes checks, use it. if client has invalid
// channel open (or windows like settings) then use last known server active channel // channel open (or windows like settings) then use last known server active channel
openChannel = client.attachedClients[socket.id].openChannel || client.lastActiveChannel; openChannel =
client.attachedClients[socket.id].openChannel ||
client.lastActiveChannel;
} else { } else {
openChannel = client.lastActiveChannel; openChannel = client.lastActiveChannel;
} }
@ -494,7 +526,7 @@ function initializeClient(
return; return;
} }
const network = _.find(client.networks, {uuid: data}); const network = _.find(client.networks, { uuid: data });
if (!network) { if (!network) {
return; return;
@ -508,7 +540,7 @@ function initializeClient(
return; return;
} }
const network = _.find(client.networks, {uuid: data.uuid}); const network = _.find(client.networks, { uuid: data.uuid });
if (!network) { if (!network) {
return; return;
@ -552,7 +584,10 @@ function initializeClient(
const hash = Helper.password.hash(p1); const hash = Helper.password.hash(p1);
client.setPassword(hash, (success: boolean) => { client.setPassword(hash, (success: boolean) => {
const obj = {success: false, error: undefined} as { const obj = {
success: false,
error: undefined,
} as {
success: boolean; success: boolean;
error: string | undefined; error: string | undefined;
}; };
@ -567,7 +602,9 @@ function initializeClient(
}); });
}) })
.catch((error: Error) => { .catch((error: Error) => {
log.error(`Error while checking users password. Error: ${error.message}`); log.error(
`Error while checking users password. Error: ${error.message}`
);
}); });
} }
}); });
@ -596,7 +633,9 @@ function initializeClient(
socket.emit("changelog", changelogData); socket.emit("changelog", changelogData);
}) })
.catch((error: Error) => { .catch((error: Error) => {
log.error(`Error while fetching changelog. Error: ${error.message}`); log.error(
`Error while fetching changelog. Error: ${error.message}`
);
}); });
}); });
@ -665,7 +704,12 @@ function initializeClient(
if (!Config.values.public) { if (!Config.values.public) {
socket.on("push:register", (subscription) => { socket.on("push:register", (subscription) => {
if (!Object.prototype.hasOwnProperty.call(client.config.sessions, token)) { if (
!Object.prototype.hasOwnProperty.call(
client.config.sessions,
token
)
) {
return; return;
} }
@ -684,26 +728,32 @@ function initializeClient(
} }
}); });
socket.on("push:unregister", () => client.unregisterPushSubscription(token)); socket.on("push:unregister", () =>
client.unregisterPushSubscription(token)
);
} }
const sendSessionList = () => { const sendSessionList = () => {
// TODO: this should use the ClientSession type currently in client // TODO: this should use the ClientSession type currently in client
const sessions = _.map(client.config.sessions, (session, sessionToken) => { const sessions = _.map(
return { client.config.sessions,
current: sessionToken === token, (session, sessionToken) => {
active: _.reduce( return {
client.attachedClients, current: sessionToken === token,
(count, attachedClient) => active: _.reduce(
count + (attachedClient.token === sessionToken ? 1 : 0), client.attachedClients,
0 (count, attachedClient) =>
), count +
lastUse: session.lastUse, (attachedClient.token === sessionToken ? 1 : 0),
ip: session.ip, 0
agent: session.agent, ),
token: sessionToken, // TODO: Ideally don't expose actual tokens to the client lastUse: session.lastUse,
}; ip: session.ip,
}); agent: session.agent,
token: sessionToken, // TODO: Ideally don't expose actual tokens to the client
};
}
);
socket.emit("sessions:list", sessions); socket.emit("sessions:list", sessions);
}; };
@ -725,8 +775,12 @@ function initializeClient(
} }
// We do not need to do write operations and emit events if nothing changed. // We do not need to do write operations and emit events if nothing changed.
if (client.config.clientSettings[newSetting.name] !== newSetting.value) { if (
client.config.clientSettings[newSetting.name] = newSetting.value; client.config.clientSettings[newSetting.name] !==
newSetting.value
) {
client.config.clientSettings[newSetting.name] =
newSetting.value;
// Pass the setting to all clients. // Pass the setting to all clients.
client.emit("setting:new", { client.emit("setting:new", {
@ -736,7 +790,10 @@ function initializeClient(
client.save(); client.save();
if (newSetting.name === "highlights" || newSetting.name === "highlightExceptions") { if (
newSetting.name === "highlights" ||
newSetting.name === "highlightExceptions"
) {
client.compileCustomHighlights(); client.compileCustomHighlights();
} else if (newSetting.name === "awayMessage") { } else if (newSetting.name === "awayMessage") {
if (typeof newSetting.value !== "string") { if (typeof newSetting.value !== "string") {
@ -749,7 +806,12 @@ function initializeClient(
}); });
socket.on("setting:get", () => { socket.on("setting:get", () => {
if (!Object.prototype.hasOwnProperty.call(client.config, "clientSettings")) { if (
!Object.prototype.hasOwnProperty.call(
client.config,
"clientSettings"
)
) {
socket.emit("setting:all", {}); socket.emit("setting:all", {});
return; return;
} }
@ -763,14 +825,14 @@ function initializeClient(
socket.emit("search:results", results); socket.emit("search:results", results);
}); });
socket.on("mute:change", ({target, setMutedTo}) => { socket.on("mute:change", ({ target, setMutedTo }) => {
const networkAndChan = client.find(target); const networkAndChan = client.find(target);
if (!networkAndChan) { if (!networkAndChan) {
return; return;
} }
const {chan, network} = networkAndChan; const { chan, network } = networkAndChan;
// If the user mutes the lobby, we mute the entire network. // If the user mutes the lobby, we mute the entire network.
if (chan.type === ChanType.LOBBY) { if (chan.type === ChanType.LOBBY) {
@ -802,7 +864,12 @@ function initializeClient(
tokenToSignOut = token; tokenToSignOut = token;
} }
if (!Object.prototype.hasOwnProperty.call(client.config.sessions, tokenToSignOut)) { if (
!Object.prototype.hasOwnProperty.call(
client.config.sessions,
tokenToSignOut
)
) {
return; return;
} }
@ -815,7 +882,9 @@ function initializeClient(
return; return;
} }
const socketToRemove = manager!.sockets.of("/").sockets.get(socketId); const socketToRemove = manager!.sockets
.of("/")
.sockets.get(socketId);
socketToRemove!.emit("sign-out"); socketToRemove!.emit("sign-out");
socketToRemove!.disconnect(); socketToRemove!.disconnect();
@ -901,7 +970,7 @@ function getClientConfiguration(): ClientConfiguration {
} }
function getServerConfiguration(): ServerConfiguration { function getServerConfiguration(): ServerConfiguration {
return {...Config.values, ...{stylesheets: packages.getStylesheets()}}; return { ...Config.values, ...{ stylesheets: packages.getStylesheets() } };
} }
function performAuthentication(this: Socket, data) { function performAuthentication(this: Socket, data) {
@ -914,7 +983,13 @@ function performAuthentication(this: Socket, data) {
let token: string; let token: string;
const finalInit = () => const finalInit = () =>
initializeClient(socket, client, token, data.lastMessage || -1, data.openChannel); initializeClient(
socket,
client,
token,
data.lastMessage || -1,
data.openChannel
);
const initClient = () => { const initClient = () => {
// Configuration does not change during runtime of TL, // Configuration does not change during runtime of TL,
@ -924,7 +999,9 @@ function performAuthentication(this: Socket, data) {
socket.emit( socket.emit(
"push:issubscribed", "push:issubscribed",
token && client.config.sessions[token].pushSubscription ? true : false token && client.config.sessions[token].pushSubscription
? true
: false
); );
} }
@ -976,9 +1053,9 @@ function performAuthentication(this: Socket, data) {
); );
} else { } else {
log.warn( log.warn(
`Authentication failed for user ${colors.bold(data.user)} from ${colors.bold( `Authentication failed for user ${colors.bold(
getClientIp(socket) data.user
)}` )} from ${colors.bold(getClientIp(socket))}`
); );
} }
@ -1001,7 +1078,12 @@ function performAuthentication(this: Socket, data) {
if (client && data.token) { if (client && data.token) {
const providedToken = client.calculateTokenHash(data.token); const providedToken = client.calculateTokenHash(data.token);
if (Object.prototype.hasOwnProperty.call(client.config.sessions, providedToken)) { if (
Object.prototype.hasOwnProperty.call(
client.config.sessions,
providedToken
)
) {
token = providedToken; token = providedToken;
return authCallback(true); return authCallback(true);
@ -1015,28 +1097,43 @@ function performAuthentication(this: Socket, data) {
} }
function reverseDnsLookup(ip: string, callback: (hostname: string) => void) { function reverseDnsLookup(ip: string, callback: (hostname: string) => void) {
dns.reverse(ip, (reverseErr, hostnames) => { // node can throw, even if we provide valid input based on the DNS server
if (reverseErr || hostnames.length < 1) { // returning SERVFAIL it seems: https://github.com/thelounge/thelounge/issues/4768
return callback(ip); // so we manually resolve with the ip as a fallback in case something fails
} try {
dns.reverse(ip, (reverseErr, hostnames) => {
dns.resolve(hostnames[0], net.isIP(ip) === 6 ? "AAAA" : "A", (resolveErr, resolvedIps) => { if (reverseErr || hostnames.length < 1) {
// TODO: investigate SoaRecord class
if (!Array.isArray(resolvedIps)) {
return callback(ip); return callback(ip);
} }
if (resolveErr || resolvedIps.length < 1) { dns.resolve(
return callback(ip); hostnames[0],
} net.isIP(ip) === 6 ? "AAAA" : "A",
(resolveErr, resolvedIps) => {
// TODO: investigate SoaRecord class
if (!Array.isArray(resolvedIps)) {
return callback(ip);
}
for (const resolvedIp of resolvedIps) { if (resolveErr || resolvedIps.length < 1) {
if (ip === resolvedIp) { return callback(ip);
return callback(hostnames[0]); }
for (const resolvedIp of resolvedIps) {
if (ip === resolvedIp) {
return callback(hostnames[0]);
}
}
return callback(ip);
} }
} );
return callback(ip);
}); });
}); } catch (err) {
log.error(
`failed to resolve rDNS for ${ip}, using ip instead`,
(err as any).toString()
);
setImmediate(callback, ip); // makes sure we always behave asynchronously
}
} }

View File

@ -1,9 +1,9 @@
/* eslint-disable @typescript-eslint/no-unsafe-return */ /* eslint-disable @typescript-eslint/no-unsafe-return */
import fs from "fs"; import fs from "fs";
import path from "path"; import path from "path";
import {expect} from "chai"; import { expect } from "chai";
import util from "../util"; import util from "../util";
import Msg, {MessageType} from "../../server/models/msg"; import Msg, { MessageType } from "../../server/models/msg";
import Config from "../../server/config"; import Config from "../../server/config";
import MessageStorage, { import MessageStorage, {
currentSchemaVersion, currentSchemaVersion,
@ -111,7 +111,7 @@ describe("SQLite migrations", function () {
it("has working down-migrations", async function () { it("has working down-migrations", async function () {
await serialize_run("BEGIN EXCLUSIVE TRANSACTION"); await serialize_run("BEGIN EXCLUSIVE TRANSACTION");
for (const rollback of rollbacks.reverse()) { for (const rollback of rollbacks.slice().reverse()) {
if (rollback.rollback_forbidden) { if (rollback.rollback_forbidden) {
throw Error( throw Error(
"Try to write a down migration, if you really can't, flip this to a break" "Try to write a down migration, if you really can't, flip this to a break"
@ -132,7 +132,11 @@ describe("SQLite Message Storage", function () {
this.timeout(util.isRunningOnCI() ? 25000 : 5000); this.timeout(util.isRunningOnCI() ? 25000 : 5000);
this.slow(300); this.slow(300);
const expectedPath = path.join(Config.getHomePath(), "logs", "testUser.sqlite3"); const expectedPath = path.join(
Config.getHomePath(),
"logs",
"testUser.sqlite3"
);
let store: MessageStorage; let store: MessageStorage;
function db_get_one(stmt: string, ...params: any[]): Promise<any> { function db_get_one(stmt: string, ...params: any[]): Promise<any> {
@ -193,13 +197,19 @@ describe("SQLite Message Storage", function () {
it("should resolve an empty array when disabled", async function () { it("should resolve an empty array when disabled", async function () {
store.isEnabled = false; store.isEnabled = false;
const messages = await store.getMessages(null as any, null as any, null as any); const messages = await store.getMessages(
null as any,
null as any,
null as any
);
expect(messages).to.be.empty; expect(messages).to.be.empty;
store.isEnabled = true; store.isEnabled = true;
}); });
it("should insert schema version to options table", async function () { it("should insert schema version to options table", async function () {
const row = await db_get_one("SELECT value FROM options WHERE name = 'schema_version'"); const row = await db_get_one(
"SELECT value FROM options WHERE name = 'schema_version'"
);
expect(row.value).to.equal(currentSchemaVersion.toString()); expect(row.value).to.equal(currentSchemaVersion.toString());
}); });
@ -252,8 +262,8 @@ describe("SQLite Message Storage", function () {
for (let i = 0; i < 200; ++i) { for (let i = 0; i < 200; ++i) {
await store.index( await store.index(
{uuid: "retrieval-order-test-network"} as any, { uuid: "retrieval-order-test-network" } as any,
{name: "#channel"} as any, { name: "#channel" } as any,
new Msg({ new Msg({
time: 123456789 + i, time: 123456789 + i,
text: `msg ${i}`, text: `msg ${i}`,
@ -263,12 +273,15 @@ describe("SQLite Message Storage", function () {
let msgId = 0; let msgId = 0;
const messages = await store.getMessages( const messages = await store.getMessages(
{uuid: "retrieval-order-test-network"} as any, { uuid: "retrieval-order-test-network" } as any,
{name: "#channel"} as any, { name: "#channel" } as any,
() => msgId++ () => msgId++
); );
expect(messages).to.have.lengthOf(2); expect(messages).to.have.lengthOf(2);
expect(messages.map((i_1) => i_1.text)).to.deep.equal(["msg 198", "msg 199"]); expect(messages.map((i_1) => i_1.text)).to.deep.equal([
"msg 198",
"msg 199",
]);
} finally { } finally {
Config.values.maxHistory = originalMaxHistory; Config.values.maxHistory = originalMaxHistory;
} }
@ -293,7 +306,9 @@ describe("SQLite Message Storage", function () {
expectedMessages.push(`msg ${i}`); expectedMessages.push(`msg ${i}`);
} }
expect(search.results.map((i_1) => i_1.text)).to.deep.equal(expectedMessages); expect(search.results.map((i_1) => i_1.text)).to.deep.equal(
expectedMessages
);
} finally { } finally {
Config.values.maxHistory = originalMaxHistory; Config.values.maxHistory = originalMaxHistory;
} }
@ -316,8 +331,8 @@ describe("SQLite Message Storage", function () {
Config.values.maxHistory = 3; Config.values.maxHistory = 3;
await store.index( await store.index(
{uuid: "this-is-a-network-guid2"} as any, { uuid: "this-is-a-network-guid2" } as any,
{name: "#channel"} as any, { name: "#channel" } as any,
new Msg({ new Msg({
time: 123456790, time: 123456790,
text: `foo % bar _ baz`, text: `foo % bar _ baz`,
@ -325,8 +340,8 @@ describe("SQLite Message Storage", function () {
); );
await store.index( await store.index(
{uuid: "this-is-a-network-guid2"} as any, { uuid: "this-is-a-network-guid2" } as any,
{name: "#channel"} as any, { name: "#channel" } as any,
new Msg({ new Msg({
time: 123456791, time: 123456791,
text: `foo bar x baz`, text: `foo bar x baz`,
@ -334,8 +349,8 @@ describe("SQLite Message Storage", function () {
); );
await store.index( await store.index(
{uuid: "this-is-a-network-guid2"} as any, { uuid: "this-is-a-network-guid2" } as any,
{name: "#channel"} as any, { name: "#channel" } as any,
new Msg({ new Msg({
time: 123456792, time: 123456792,
text: `bar @ baz`, text: `bar @ baz`,
@ -356,7 +371,7 @@ describe("SQLite Message Storage", function () {
}); });
it("should be able to downgrade", async function () { it("should be able to downgrade", async function () {
for (const rollback of rollbacks.reverse()) { for (const rollback of rollbacks.slice().reverse()) {
if (rollback.rollback_forbidden) { if (rollback.rollback_forbidden) {
throw Error( throw Error(
"Try to write a down migration, if you really can't, flip this to a break" "Try to write a down migration, if you really can't, flip this to a break"

View File

@ -1328,10 +1328,10 @@
resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.4.tgz#0b92dcc0cc1c81f6f306a381f28e31b1a56536e9" resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.4.tgz#0b92dcc0cc1c81f6f306a381f28e31b1a56536e9"
integrity sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA== integrity sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==
"@types/bcryptjs@2.4.4": "@types/bcryptjs@2.4.5":
version "2.4.4" version "2.4.5"
resolved "https://registry.yarnpkg.com/@types/bcryptjs/-/bcryptjs-2.4.4.tgz#cd3c4c007f600f1d21db09c9bd4ced8b49d04670" resolved "https://registry.yarnpkg.com/@types/bcryptjs/-/bcryptjs-2.4.5.tgz#15473cc012f825b3435b189376f645bdd2fc9f11"
integrity sha512-9wlJI7k5gRyJEC4yrV7DubzNQFTPiykYxUA6lBtsk5NlOfW9oWLJ1HdIA4YtE+6C3i3mTpDQQEosJ2rVZfBWnw== integrity sha512-tOF6TivOIvq+TWQm78335CMdyVJhpBG3NUdWQDAp95ax4E2rSKbws/ELHLk5EBoucwx/tHt3/hhLOHwWJgVrSw==
"@types/body-parser@*": "@types/body-parser@*":
version "1.19.3" version "1.19.3"
@ -1361,10 +1361,10 @@
resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.3.5.tgz#ae69bcbb1bebb68c4ac0b11e9d8ed04526b3562b" resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.3.5.tgz#ae69bcbb1bebb68c4ac0b11e9d8ed04526b3562b"
integrity sha512-mEo1sAde+UCE6b2hxn332f1g1E8WfYRu6p5SvTKr2ZKC1f7gFJXk4h5PyGP9Dt6gCaG8y8XhwnXWC6Iy2cmBng== integrity sha512-mEo1sAde+UCE6b2hxn332f1g1E8WfYRu6p5SvTKr2ZKC1f7gFJXk4h5PyGP9Dt6gCaG8y8XhwnXWC6Iy2cmBng==
"@types/cheerio@0.22.31": "@types/cheerio@0.22.33":
version "0.22.31" version "0.22.33"
resolved "https://registry.yarnpkg.com/@types/cheerio/-/cheerio-0.22.31.tgz#b8538100653d6bb1b08a1e46dec75b4f2a5d5eb6" resolved "https://registry.yarnpkg.com/@types/cheerio/-/cheerio-0.22.33.tgz#e4792408b107384d7d7469e3b4d31408078ec620"
integrity sha512-Kt7Cdjjdi2XWSfrZ53v4Of0wG3ZcmaegFXjMmz9tfNrZSkzzo36G0AL1YqSdcIA78Etjt6E609pt5h1xnQkPUw== integrity sha512-XUlu2BK4q3xJsccRLK69m/cABZd7m60o+cDEPUTG6jTpuG2vqN35UioeF99MQ/HoSOEPq0Bgil8g3jtzE0oH9A==
dependencies: dependencies:
"@types/node" "*" "@types/node" "*"
@ -1375,10 +1375,10 @@
dependencies: dependencies:
"@types/node" "*" "@types/node" "*"
"@types/content-disposition@0.5.5": "@types/content-disposition@0.5.7":
version "0.5.5" version "0.5.7"
resolved "https://registry.yarnpkg.com/@types/content-disposition/-/content-disposition-0.5.5.tgz#650820e95de346e1f84e30667d168c8fd25aa6e3" resolved "https://registry.yarnpkg.com/@types/content-disposition/-/content-disposition-0.5.7.tgz#3b98d4bf8c80640f93b042511acb5aad18139748"
integrity sha512-v6LCdKfK6BwcqMo+wYW05rLS12S0ZO0Fl4w1h4aaZMD7bqT3gVUns6FvLJKGZHQmYn3SX55JWGpziwJRwVgutA== integrity sha512-V9/5u21RHFR1zfdm3rQ6pJUKV+zSSVQt+yq16i1YhdivVzWgPEoKedc3GdT8aFjsqQbakdxuy3FnEdePUQOamQ==
"@types/cookie@^0.4.1": "@types/cookie@^0.4.1":
version "0.4.1" version "0.4.1"
@ -1448,10 +1448,10 @@
resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-2.0.2.tgz#a86e00bbde8950364f8e7846687259ffcd96e8c2" resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-2.0.2.tgz#a86e00bbde8950364f8e7846687259ffcd96e8c2"
integrity sha512-lPG6KlZs88gef6aD85z3HNkztpj7w2R7HmR3gygjfXCQmsLloWNARFkMuzKiiY8FGdh1XDpgBdrSf4aKDiA7Kg== integrity sha512-lPG6KlZs88gef6aD85z3HNkztpj7w2R7HmR3gygjfXCQmsLloWNARFkMuzKiiY8FGdh1XDpgBdrSf4aKDiA7Kg==
"@types/is-utf8@0.2.1": "@types/is-utf8@0.2.2":
version "0.2.1" version "0.2.2"
resolved "https://registry.yarnpkg.com/@types/is-utf8/-/is-utf8-0.2.1.tgz#2cecf393ce44a73d3d052224e8375709098b4d25" resolved "https://registry.yarnpkg.com/@types/is-utf8/-/is-utf8-0.2.2.tgz#b31ab599ead973992809b0b802ce066abbb42efd"
integrity sha512-4tSeTnvbhBsWZy+NTB7g3mbRDZKN0tgS199YlY2JngABhpxSlKyaUX65Lxw8VnLa6IG4tHxBMi0ffhnFhio7jw== integrity sha512-j7PFtO0ki4IoJvmMaAHQ70z74Td244dMLC7BAz5pb0v7IC8xXLtuM+7AWsMco4Minz92m30fO72+TbkmtMr4dQ==
dependencies: dependencies:
"@types/node" "*" "@types/node" "*"
@ -1479,10 +1479,10 @@
resolved "https://registry.yarnpkg.com/@types/linkify-it/-/linkify-it-3.0.3.tgz#15a0712296c5041733c79efe233ba17ae5a7587b" resolved "https://registry.yarnpkg.com/@types/linkify-it/-/linkify-it-3.0.3.tgz#15a0712296c5041733c79efe233ba17ae5a7587b"
integrity sha512-pTjcqY9E4nOI55Wgpz7eiI8+LzdYnw3qxXCfHyBDdPbYvbyLgWLJGh8EdPvqawwMK1Uo1794AUkkR38Fr0g+2g== integrity sha512-pTjcqY9E4nOI55Wgpz7eiI8+LzdYnw3qxXCfHyBDdPbYvbyLgWLJGh8EdPvqawwMK1Uo1794AUkkR38Fr0g+2g==
"@types/lodash@4.14.195": "@types/lodash@4.14.200":
version "4.14.195" version "4.14.200"
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.195.tgz#bafc975b252eb6cea78882ce8a7b6bf22a6de632" resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.200.tgz#435b6035c7eba9cdf1e039af8212c9e9281e7149"
integrity sha512-Hwx9EUgdwf2GLarOjQp5ZH8ZmblzcbTBC2wtQWNKARBSxM9ezRIAUpeDTgoQRAFB0+8CNWXVA9+MaSOzOF3nPg== integrity sha512-YI/M/4HRImtNf3pJgbF+W6FrXovqj+T+/HpENLTooK9PnkacBsDpeP3IpHab40CClUfhNmdM2WTNP2sa2dni5Q==
"@types/mime-types@2.1.1": "@types/mime-types@2.1.1":
version "2.1.1" version "2.1.1"
@ -1514,10 +1514,10 @@
resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-9.1.1.tgz#e7c4f1001eefa4b8afbd1eee27a237fee3bf29c4" resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-9.1.1.tgz#e7c4f1001eefa4b8afbd1eee27a237fee3bf29c4"
integrity sha512-Z61JK7DKDtdKTWwLeElSEBcWGRLY8g95ic5FoQqI9CMx0ns/Ghep3B4DfcEimiKMvtamNVULVNKEsiwV3aQmXw== integrity sha512-Z61JK7DKDtdKTWwLeElSEBcWGRLY8g95ic5FoQqI9CMx0ns/Ghep3B4DfcEimiKMvtamNVULVNKEsiwV3aQmXw==
"@types/mousetrap@1.6.11": "@types/mousetrap@1.6.13":
version "1.6.11" version "1.6.13"
resolved "https://registry.yarnpkg.com/@types/mousetrap/-/mousetrap-1.6.11.tgz#ef9620160fdcefcb85bccda8aaa3e84d7429376d" resolved "https://registry.yarnpkg.com/@types/mousetrap/-/mousetrap-1.6.13.tgz#1b2e4cd374fdd1ee58a240be0aafd94f7270b3be"
integrity sha512-F0oAily9Q9QQpv9JKxKn0zMKfOo36KHCW7myYsmUyf2t0g+sBTbG3UleTPoguHdE1z3GLFr3p7/wiOio52QFjQ== integrity sha512-dEzDpaR+P/thkMsjsREQDX9OP8AMyLncTkgUgTTIxq5lJTlQffiLJt67ImDtaX+kC7CaNIX30pfdrrMZkym+eg==
"@types/node@*", "@types/node@>=10.0.0": "@types/node@*", "@types/node@>=10.0.0":
version "20.8.4" version "20.8.4"
@ -2779,6 +2779,11 @@ caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001538, caniuse-lite@^1.0.30001541:
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001546.tgz#10fdad03436cfe3cc632d3af7a99a0fb497407f0" resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001546.tgz#10fdad03436cfe3cc632d3af7a99a0fb497407f0"
integrity sha512-zvtSJwuQFpewSyRrI3AsftF6rM0X80mZkChIt1spBGEvRglCrjTniXvinc8JKRoqTwXAgvqTImaN9igfSMtUBw== integrity sha512-zvtSJwuQFpewSyRrI3AsftF6rM0X80mZkChIt1spBGEvRglCrjTniXvinc8JKRoqTwXAgvqTImaN9igfSMtUBw==
caniuse-lite@^1.0.30001561:
version "1.0.30001565"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001565.tgz#a528b253c8a2d95d2b415e11d8b9942acc100c4f"
integrity sha512-xrE//a3O7TP0vaJ8ikzkD2c2NgcVUvsEe2IvFTntV4Yd1Z9FVzh+gW+enX96L0psrbaFMcVcH2l90xNuGDWc8w==
chai@4.3.7: chai@4.3.7:
version "4.3.7" version "4.3.7"
resolved "https://registry.yarnpkg.com/chai/-/chai-4.3.7.tgz#ec63f6df01829088e8bf55fca839bcd464a8ec51" resolved "https://registry.yarnpkg.com/chai/-/chai-4.3.7.tgz#ec63f6df01829088e8bf55fca839bcd464a8ec51"
@ -4236,7 +4241,7 @@ get-caller-file@^2.0.1, get-caller-file@^2.0.5:
resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
get-func-name@^2.0.0, get-func-name@^2.0.2: get-func-name@2.0.2, get-func-name@^2.0.0, get-func-name@^2.0.2:
version "2.0.2" version "2.0.2"
resolved "https://registry.yarnpkg.com/get-func-name/-/get-func-name-2.0.2.tgz#0d7cf20cd13fda808669ffa88f4ffc7a3943fc41" resolved "https://registry.yarnpkg.com/get-func-name/-/get-func-name-2.0.2.tgz#0d7cf20cd13fda808669ffa88f4ffc7a3943fc41"
integrity sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ== integrity sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==
@ -5755,9 +5760,9 @@ nanoid@3.3.1:
integrity sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw== integrity sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw==
nanoid@^3.3.6: nanoid@^3.3.6:
version "3.3.6" version "3.3.7"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8"
integrity sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA== integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==
natural-compare@^1.4.0: natural-compare@^1.4.0:
version "1.4.0" version "1.4.0"