Skip to content

Instantly share code, notes, and snippets.

@mikaelvesavuori
Last active April 3, 2025 18:35
Show Gist options
  • Select an option

  • Save mikaelvesavuori/f838c02e4da491ce53368b52264330d2 to your computer and use it in GitHub Desktop.

Select an option

Save mikaelvesavuori/f838c02e4da491ce53368b52264330d2 to your computer and use it in GitHub Desktop.
MikroChat bundled
// MikroChat - See LICENSE file for copyright and license details.
// node_modules/mikroauth/lib/index.mjs
import f from "node:crypto";
import { URL } from "node:url";
// node_modules/mikroconf/lib/chunk-DLM37L3L.mjs
var ValidationError = class extends Error {
constructor(message) {
super(message);
this.name = "ValidationError";
this.message = message || "Validation did not pass";
this.cause = { statusCode: 400 };
}
};
// node_modules/mikroconf/lib/chunk-DAEGQ4NM.mjs
import { existsSync, readFileSync } from "node:fs";
var MikroConf = class {
config = {};
options = [];
validators = [];
autoValidate = true;
/**
* @description Creates a new MikroConf instance.
*/
constructor(options) {
const configFilePath = options?.configFilePath;
const args = options?.args || [];
const configuration = options?.config || {};
this.options = options?.options || [];
this.validators = options?.validators || [];
if (options?.autoValidate !== void 0) this.autoValidate = options.autoValidate;
this.config = this.createConfig(configFilePath, args, configuration);
}
/**
* @description Deep merges two objects.
*/
deepMerge(target, source) {
const result = { ...target };
for (const key in source) {
if (source[key] === void 0) continue;
if (source[key] !== null && typeof source[key] === "object" && !Array.isArray(source[key]) && key in target && target[key] !== null && typeof target[key] === "object" && !Array.isArray(target[key])) {
result[key] = this.deepMerge(target[key], source[key]);
} else if (source[key] !== void 0) result[key] = source[key];
}
return result;
}
/**
* @description Sets a value at a nested path in an object.
*/
setValueAtPath(obj, path, value) {
const parts = path.split(".");
let current = obj;
for (let i = 0; i < parts.length - 1; i++) {
const part = parts[i];
if (!(part in current) || current[part] === null) current[part] = {};
else if (typeof current[part] !== "object") current[part] = {};
current = current[part];
}
const lastPart = parts[parts.length - 1];
current[lastPart] = value;
}
/**
* @description Gets a value from a nested path in an object.
*/
getValueAtPath(obj, path) {
const parts = path.split(".");
let current = obj;
for (const part of parts) {
if (current === void 0 || current === null) return void 0;
current = current[part];
}
return current;
}
/**
* @description Creates a configuration object by merging defaults, config file settings,
* explicit input, and CLI arguments.
*/
createConfig(configFilePath, args = [], configuration = {}) {
const defaults3 = {};
for (const option of this.options) {
if (option.defaultValue !== void 0)
this.setValueAtPath(defaults3, option.path, option.defaultValue);
}
let fileConfig = {};
if (configFilePath && existsSync(configFilePath)) {
try {
const fileContent = readFileSync(configFilePath, "utf8");
fileConfig = JSON.parse(fileContent);
console.log(`Loaded configuration from ${configFilePath}`);
} catch (error) {
console.error(
`Error reading config file: ${error instanceof Error ? error.message : String(error)}`
);
}
}
const cliConfig = this.parseCliArgs(args);
let mergedConfig = this.deepMerge({}, defaults3);
mergedConfig = this.deepMerge(mergedConfig, fileConfig);
mergedConfig = this.deepMerge(mergedConfig, configuration);
mergedConfig = this.deepMerge(mergedConfig, cliConfig);
return mergedConfig;
}
/**
* @description Parses command line arguments into a configuration object based on defined options.
*/
parseCliArgs(args) {
const cliConfig = {};
let i = args[0]?.endsWith("node") || args[0]?.endsWith("node.exe") ? 2 : 0;
while (i < args.length) {
const arg = args[i++];
const option = this.options.find((opt) => opt.flag === arg);
if (option) {
if (option.isFlag) {
this.setValueAtPath(cliConfig, option.path, true);
} else if (i < args.length && !args[i].startsWith("-")) {
let value = args[i++];
if (option.parser) {
try {
value = option.parser(value);
} catch (error) {
console.error(
`Error parsing value for ${option.flag}: ${error instanceof Error ? error.message : String(error)}`
);
continue;
}
}
if (option.validator) {
const validationResult = option.validator(value);
if (validationResult !== true && typeof validationResult === "string") {
console.error(`Invalid value for ${option.flag}: ${validationResult}`);
continue;
}
if (validationResult === false) {
console.error(`Invalid value for ${option.flag}`);
continue;
}
}
this.setValueAtPath(cliConfig, option.path, value);
} else {
console.error(`Missing value for option ${arg}`);
}
}
}
return cliConfig;
}
/**
* @description Validates the configuration against defined validators.
*/
validate() {
for (const validator of this.validators) {
const value = this.getValueAtPath(this.config, validator.path);
const result = validator.validator(value, this.config);
if (result === false) throw new ValidationError(validator.message);
if (typeof result === "string") throw new ValidationError(result);
}
}
/**
* @description Returns the complete configuration.
* @returns The configuration object.
*/
get() {
if (this.autoValidate) this.validate();
return this.config;
}
/**
* @description Gets a specific configuration value by path.
* @param path The dot-notation path to the configuration value.
* @param defaultValue Optional default value if the path doesn't exist.
*/
getValue(path, defaultValue) {
const value = this.getValueAtPath(this.config, path);
return value !== void 0 ? value : defaultValue;
}
/**
* @description Sets a specific configuration value by path.
* @param path The dot-notation path to set.
* @param value The value to set.
*/
setValue(path, value) {
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
const currentValue = this.getValueAtPath(this.config, path) || {};
if (typeof currentValue === "object" && !Array.isArray(currentValue)) {
const mergedValue = this.deepMerge(currentValue, value);
this.setValueAtPath(this.config, path, mergedValue);
return;
}
}
this.setValueAtPath(this.config, path, value);
}
/**
* @description Generates help text based on the defined options.
*/
getHelpText() {
let help = "Available configuration options:\n\n";
for (const option of this.options) {
help += `${option.flag}${option.isFlag ? "" : " <value>"}
`;
if (option.description) help += ` ${option.description}
`;
if (option.defaultValue !== void 0)
help += ` Default: ${JSON.stringify(option.defaultValue)}
`;
help += "\n";
}
return help;
}
};
// node_modules/mikroconf/lib/chunk-OCFH3FON.mjs
var parsers = {
/**
* @description Parses a string to an integer.
*/
int: (value) => {
const trimmedValue = value.trim();
if (!/^[+-]?\d+$/.test(trimmedValue)) throw new Error(`Cannot parse "${value}" as an integer`);
const parsed = Number.parseInt(trimmedValue, 10);
if (Number.isNaN(parsed)) throw new Error(`Cannot parse "${value}" as an integer`);
return parsed;
},
/**
* @description Parses a string to a float.
*/
float: (value) => {
const trimmedValue = value.trim();
if (!/^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?$/.test(trimmedValue)) {
if (trimmedValue === "Infinity" || trimmedValue === "-Infinity")
return trimmedValue === "Infinity" ? Number.POSITIVE_INFINITY : Number.NEGATIVE_INFINITY;
throw new Error(`Cannot parse "${value}" as a number`);
}
const parsed = Number.parseFloat(trimmedValue);
if (Number.isNaN(parsed)) throw new Error(`Cannot parse "${value}" as a number`);
return parsed;
},
/**
* @description Parses a string to a boolean.
*/
boolean: (value) => {
const lowerValue = value.trim().toLowerCase();
if (["true", "yes", "1", "y"].includes(lowerValue)) return true;
if (["false", "no", "0", "n"].includes(lowerValue)) return false;
throw new Error(`Cannot parse "${value}" as a boolean`);
},
/**
* @description Parses a comma-separated string to an array.
*/
array: (value) => {
return value.split(",").map((item) => item.trim());
},
/**
* @description Parses a JSON string.
*/
json: (value) => {
try {
return JSON.parse(value);
} catch (_error) {
throw new Error(`Cannot parse "${value}" as JSON`);
}
}
};
// node_modules/mikroauth/lib/index.mjs
import { EventEmitter } from "node:events";
// node_modules/mikromail/lib/chunk-47VXJTWV.mjs
var ValidationError2 = class extends Error {
constructor(message) {
super();
this.name = "ValidationError";
this.message = message;
this.cause = { statusCode: 400 };
}
};
// node_modules/mikromail/lib/chunk-YVXB6HCK.mjs
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
var Configuration = class {
config;
defaults = {
configFilePath: "mikromail.config.json",
args: []
};
/**
* @description Creates a new Configuration instance.
*/
constructor(options) {
const configuration = options?.config || {};
const configFilePath = options?.configFilePath || this.defaults.configFilePath;
const args = options?.args || this.defaults.args;
this.config = this.create(configFilePath, args, configuration);
}
/**
* @description Creates a configuration object by merging defaults, config file settings,
* and CLI arguments (in order of increasing precedence)
* @param configFilePath Path to the configuration file.
* @param args Command line arguments array.
* @param configuration User-provided configuration input.
* @returns The merged configuration object.
*/
create(configFilePath, args, configuration) {
const defaults3 = {
host: "",
user: "",
password: "",
port: 465,
secure: true,
debug: false,
maxRetries: 2
};
let fileConfig = {};
if (existsSync2(configFilePath)) {
try {
const fileContent = readFileSync2(configFilePath, "utf8");
fileConfig = JSON.parse(fileContent);
console.log(`Loaded configuration from ${configFilePath}`);
} catch (error) {
console.error(
`Error reading config file: ${error instanceof Error ? error.message : String(error)}`
);
}
}
const cliConfig = this.parseCliArgs(args);
return {
...defaults3,
...configuration,
...fileConfig,
...cliConfig
};
}
/**
* @description Parses command line arguments into a configuration object.
* @param args Command line arguments array.
* @returns Parsed CLI configuration.
*/
parseCliArgs(args) {
const cliConfig = {};
for (let i = 2; i < args.length; i++) {
const arg = args[i];
switch (arg) {
case "--host":
if (i + 1 < args.length) cliConfig.host = args[++i];
break;
case "--user":
if (i + 1 < args.length) cliConfig.user = args[++i];
break;
case "--password":
if (i + 1 < args.length) cliConfig.password = args[++i];
break;
case "--port":
if (i + 1 < args.length) {
const value = Number.parseInt(args[++i], 10);
if (!Number.isNaN(value)) cliConfig.port = value;
}
break;
case "--secure":
cliConfig.secure = true;
break;
case "--debug":
cliConfig.debug = true;
break;
case "--retries":
if (i + 1 < args.length) {
const value = Number.parseInt(args[++i], 10);
if (!Number.isNaN(value)) cliConfig.maxRetries = value;
}
break;
}
}
return cliConfig;
}
/**
* @description Validates the configuration.
* @throws Error if the configuration is invalid.
*/
validate() {
if (!this.config.host) throw new ValidationError2("Host value not found");
}
/**
* @description Returns the complete configuration.
* @returns The configuration object.
*/
get() {
this.validate();
return this.config;
}
};
// node_modules/mikromail/lib/chunk-UDLJWUFN.mjs
import { promises as dnsPromises } from "node:dns";
function validateEmail(email) {
try {
const [localPart, domain] = email.split("@");
if (!localPart || localPart.length > 64) return false;
if (localPart.startsWith(".") || localPart.endsWith(".") || localPart.includes(".."))
return false;
if (!/^[a-zA-Z0-9!#$%&'*+\-/=?^_`{|}~.]+$/.test(localPart)) return false;
if (!domain || domain.length > 255) return false;
if (domain.startsWith("[") && domain.endsWith("]")) {
const ipContent = domain.slice(1, -1);
if (ipContent.startsWith("IPv6:")) return true;
const ipv4Regex = /^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$/;
return ipv4Regex.test(ipContent);
}
if (domain.startsWith(".") || domain.endsWith(".") || domain.includes(".."))
return false;
const domainParts = domain.split(".");
if (domainParts.length < 2 || domainParts[domainParts.length - 1].length < 2)
return false;
for (const part of domainParts) {
if (!part || part.length > 63) return false;
if (!/^[a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?$/.test(part)) return false;
}
return true;
} catch (_error) {
return false;
}
}
async function verifyMXRecords(domain) {
try {
const records = await dnsPromises.resolveMx(domain);
return !!records && records.length > 0;
} catch (_error) {
return false;
}
}
async function verifyEmailDomain(email) {
try {
const domain = email.split("@")[1];
if (!domain) return false;
return await verifyMXRecords(domain);
} catch (_error) {
return false;
}
}
// node_modules/mikromail/lib/chunk-NGT3KX7A.mjs
import { Buffer as Buffer2 } from "node:buffer";
import crypto from "node:crypto";
import net from "node:net";
import os from "node:os";
import tls from "node:tls";
var SMTPClient = class {
config;
socket;
connected;
lastCommand;
serverCapabilities;
secureMode;
retryCount;
maxEmailSize = 10485760;
// 10MB
constructor(config) {
this.config = {
host: config.host,
user: config.user,
password: config.password,
port: config.port ?? (config.secure ? 465 : 587),
secure: config.secure ?? true,
debug: config.debug ?? false,
timeout: config.timeout ?? 1e4,
clientName: config.clientName ?? os.hostname(),
maxRetries: config.maxRetries ?? 3,
retryDelay: config.retryDelay ?? 1e3,
skipAuthentication: config.skipAuthentication || false
};
this.socket = null;
this.connected = false;
this.lastCommand = "";
this.serverCapabilities = [];
this.secureMode = this.config.secure;
this.retryCount = 0;
}
/**
* Log debug messages if debug mode is enabled
*/
log(message, isError = false) {
if (this.config.debug) {
const prefix = isError ? "SMTP ERROR: " : "SMTP: ";
console.log(`${prefix}${message}`);
}
}
/**
* Connect to the SMTP server
*/
async connect() {
return new Promise((resolve, reject) => {
const connectionTimeout = setTimeout(() => {
reject(new Error(`Connection timeout after ${this.config.timeout}ms`));
this.socket?.destroy();
}, this.config.timeout);
try {
if (this.config.secure) {
this.createTLSConnection(connectionTimeout, resolve, reject);
} else {
this.createPlainConnection(connectionTimeout, resolve, reject);
}
} catch (error) {
clearTimeout(connectionTimeout);
this.log(`Failed to create socket: ${error.message}`, true);
reject(error);
}
});
}
/**
* Create a secure TLS connection
*/
createTLSConnection(connectionTimeout, resolve, reject) {
this.socket = tls.connect({
host: this.config.host,
port: this.config.port,
rejectUnauthorized: true,
// Always validate TLS certificates
minVersion: "TLSv1.2",
// Enforce TLS 1.2 or higher
ciphers: "HIGH:!aNULL:!MD5:!RC4"
});
this.setupSocketEventHandlers(connectionTimeout, resolve, reject);
}
/**
* Create a plain socket connection (for later STARTTLS upgrade)
*/
createPlainConnection(connectionTimeout, resolve, reject) {
this.socket = net.createConnection({
host: this.config.host,
port: this.config.port
});
this.setupSocketEventHandlers(connectionTimeout, resolve, reject);
}
/**
* Set up common socket event handlers
*/
setupSocketEventHandlers(connectionTimeout, resolve, reject) {
if (!this.socket) return;
this.socket.once("error", (err) => {
clearTimeout(connectionTimeout);
this.log(`Connection error: ${err.message}`, true);
reject(new Error(`SMTP connection error: ${err.message}`));
});
this.socket.once("connect", () => {
this.log("Connected to SMTP server");
clearTimeout(connectionTimeout);
this.socket.once("data", (data) => {
const greeting = data.toString().trim();
this.log(`Server greeting: ${greeting}`);
if (greeting.startsWith("220")) {
this.connected = true;
this.secureMode = this.config.secure;
resolve();
} else {
reject(new Error(`Unexpected server greeting: ${greeting}`));
this.socket.destroy();
}
});
});
this.socket.once("close", (hadError) => {
if (this.connected) {
this.log(`Connection closed${hadError ? " with error" : ""}`);
} else {
clearTimeout(connectionTimeout);
reject(new Error("Connection closed before initialization completed"));
}
this.connected = false;
});
}
/**
* Upgrade connection to TLS using STARTTLS
*/
async upgradeToTLS() {
if (!this.socket || this.secureMode) return;
return new Promise((resolve, reject) => {
const plainSocket = this.socket;
const tlsOptions = {
socket: plainSocket,
host: this.config.host,
rejectUnauthorized: true,
minVersion: "TLSv1.2",
ciphers: "HIGH:!aNULL:!MD5:!RC4"
};
const tlsSocket = tls.connect(tlsOptions);
tlsSocket.once("error", (err) => {
this.log(`TLS upgrade error: ${err.message}`, true);
reject(new Error(`STARTTLS error: ${err.message}`));
});
tlsSocket.once("secureConnect", () => {
this.log("Connection upgraded to TLS");
if (tlsSocket.authorized) {
this.socket = tlsSocket;
this.secureMode = true;
resolve();
} else {
reject(
new Error(
`TLS certificate verification failed: ${tlsSocket.authorizationError}`
)
);
}
});
});
}
/**
* Send an SMTP command and await response
*/
async sendCommand(command, expectedCode, timeout = this.config.timeout) {
if (!this.socket || !this.connected) {
throw new Error("Not connected to SMTP server");
}
return new Promise((resolve, reject) => {
const commandTimeout = setTimeout(() => {
this.socket?.removeListener("data", onData);
reject(new Error(`Command timeout after ${timeout}ms: ${command}`));
}, timeout);
let responseData = "";
const onData = (chunk) => {
responseData += chunk.toString();
const lines = responseData.split("\r\n");
if (lines.length > 0 && lines[lines.length - 1] === "") {
const lastLine = lines[lines.length - 2] || "";
const matches = /^(\d{3})(.?)/.exec(lastLine);
if (matches?.[1] && matches[2] !== "-") {
this.socket?.removeListener("data", onData);
clearTimeout(commandTimeout);
this.log(`SMTP Response: ${responseData.trim()}`);
if (matches[1] === expectedCode.toString()) {
resolve(responseData.trim());
} else {
reject(new Error(`SMTP Error: ${responseData.trim()}`));
}
}
}
};
this.socket.on("data", onData);
if (command.startsWith("AUTH PLAIN") || command.startsWith("AUTH LOGIN") || this.lastCommand === "AUTH LOGIN" && !command.startsWith("AUTH")) {
this.log("SMTP Command: [Credentials hidden]");
} else {
this.log(`SMTP Command: ${command}`);
}
this.lastCommand = command;
this.socket.write(`${command}\r
`);
});
}
/**
* Parse EHLO response to determine server capabilities
*/
parseCapabilities(ehloResponse) {
const lines = ehloResponse.split("\r\n");
this.serverCapabilities = [];
for (let i = 1; i < lines.length; i++) {
const line = lines[i];
if (line.match(/^\d{3}/) && line.charAt(3) === " ") {
const capability = line.substr(4).toUpperCase();
this.serverCapabilities.push(capability);
}
}
this.log(`Server capabilities: ${this.serverCapabilities.join(", ")}`);
}
/**
* Determine the best authentication method supported by the server
*/
getBestAuthMethod() {
const capabilities = this.serverCapabilities.map(
(cap) => cap.split(" ")[0]
);
if (capabilities.includes("AUTH")) {
const authLine = this.serverCapabilities.find(
(cap) => cap.startsWith("AUTH ")
);
if (authLine) {
const methods = authLine.split(" ").slice(1);
if (methods.includes("CRAM-MD5")) return "CRAM-MD5";
if (methods.includes("LOGIN")) return "LOGIN";
if (methods.includes("PLAIN")) return "PLAIN";
}
}
return "PLAIN";
}
/**
* Authenticate with the SMTP server using the best available method
*/
async authenticate() {
const authMethod = this.getBestAuthMethod();
switch (authMethod) {
case "CRAM-MD5":
await this.authenticateCramMD5();
break;
case "LOGIN":
await this.authenticateLogin();
break;
default:
await this.authenticatePlain();
break;
}
}
/**
* Authenticate using PLAIN method
*/
async authenticatePlain() {
const authPlain = Buffer2.from(
`\0${this.config.user}\0${this.config.password}`
).toString("base64");
await this.sendCommand(`AUTH PLAIN ${authPlain}`, 235);
}
/**
* Authenticate using LOGIN method
*/
async authenticateLogin() {
await this.sendCommand("AUTH LOGIN", 334);
await this.sendCommand(
Buffer2.from(this.config.user).toString("base64"),
334
);
await this.sendCommand(
Buffer2.from(this.config.password).toString("base64"),
235
);
}
/**
* Authenticate using CRAM-MD5 method
*/
async authenticateCramMD5() {
const response = await this.sendCommand("AUTH CRAM-MD5", 334);
const challenge = Buffer2.from(response.substr(4), "base64").toString(
"utf8"
);
const hmac = crypto.createHmac("md5", this.config.password);
hmac.update(challenge);
const digest = hmac.digest("hex");
const cramResponse = `${this.config.user} ${digest}`;
const encodedResponse = Buffer2.from(cramResponse).toString("base64");
await this.sendCommand(encodedResponse, 235);
}
/**
* Generate a unique message ID
*/
generateMessageId() {
const random = crypto.randomBytes(16).toString("hex");
const domain = this.config.user.split("@")[1] || "localhost";
return `<${random}@${domain}>`;
}
/**
* Generate MIME boundary for multipart messages
*/
generateBoundary() {
return `----=_NextPart_${crypto.randomBytes(12).toString("hex")}`;
}
/**
* Encode string according to RFC 2047 for headers with non-ASCII characters
*/
encodeHeaderValue(value) {
if (/^[\x00-\x7F]*$/.test(value)) {
return value;
}
return `=?UTF-8?Q?${// biome-ignore lint/suspicious/noControlCharactersInRegex: <explanation>
value.replace(/[^\x00-\x7F]/g, (c) => {
const hex = c.charCodeAt(0).toString(16).toUpperCase();
return `=${hex.length < 2 ? `0${hex}` : hex}`;
})}?=`;
}
/**
* Sanitize and encode header value to prevent injection and handle internationalization
*/
sanitizeHeader(value) {
const sanitized = value.replace(/[\r\n\t]+/g, " ").replace(/\s{2,}/g, " ").trim();
return this.encodeHeaderValue(sanitized);
}
/**
* Create email headers with proper sanitization
*/
createEmailHeaders(options) {
const messageId = this.generateMessageId();
const date = (/* @__PURE__ */ new Date()).toUTCString();
const from = options.from || this.config.user;
const recipients = Array.isArray(options.to) ? options.to.join(", ") : options.to;
const headers = [
`From: ${this.sanitizeHeader(from)}`,
`To: ${this.sanitizeHeader(recipients)}`,
`Subject: ${this.sanitizeHeader(options.subject)}`,
`Message-ID: ${messageId}`,
`Date: ${date}`,
"MIME-Version: 1.0"
];
if (options.cc) {
const cc = Array.isArray(options.cc) ? options.cc.join(", ") : options.cc;
headers.push(`Cc: ${this.sanitizeHeader(cc)}`);
}
if (options.replyTo)
headers.push(`Reply-To: ${this.sanitizeHeader(options.replyTo)}`);
if (options.headers) {
for (const [name, value] of Object.entries(options.headers)) {
if (!/^[a-zA-Z0-9-]+$/.test(name)) continue;
if (/^(from|to|cc|bcc|subject|date|message-id)$/i.test(name)) continue;
headers.push(`${name}: ${this.sanitizeHeader(value)}`);
}
}
return headers;
}
/**
* Create a multipart email with text and HTML parts
*/
createMultipartEmail(options) {
const { text, html } = options;
const headers = this.createEmailHeaders(options);
const boundary = this.generateBoundary();
if (html && text) {
headers.push(
`Content-Type: multipart/alternative; boundary="${boundary}"`
);
return `${headers.join("\r\n")}\r
\r
--${boundary}\r
Content-Type: text/plain; charset=utf-8\r
\r
${text || ""}\r
\r
--${boundary}\r
Content-Type: text/html; charset=utf-8\r
\r
${html || ""}\r
\r
--${boundary}--\r
`;
}
headers.push("Content-Type: text/html; charset=utf-8");
if (html) return `${headers.join("\r\n")}\r
\r
${html}`;
return `${headers.join("\r\n")}\r
\r
${text || ""}`;
}
/**
* Perform full SMTP handshake, including STARTTLS if needed
*/
async smtpHandshake() {
const ehloResponse = await this.sendCommand(
`EHLO ${this.config.clientName}`,
250
);
this.parseCapabilities(ehloResponse);
if (!this.secureMode && this.serverCapabilities.includes("STARTTLS")) {
await this.sendCommand("STARTTLS", 220);
await this.upgradeToTLS();
const secureEhloResponse = await this.sendCommand(
`EHLO ${this.config.clientName}`,
250
);
this.parseCapabilities(secureEhloResponse);
}
if (!this.config.skipAuthentication) {
await this.authenticate();
} else {
this.log("Authentication skipped (testing mode)");
}
}
/**
* Send an email with retry capability
*/
async sendEmail(options) {
const from = options.from || this.config.user;
const { to, subject } = options;
const text = options.text || "";
const html = options.html || "";
if (!from || !to || !subject || !text && !html) {
return {
success: false,
error: "Missing required email parameters (from, to, subject, and either text or html)"
};
}
if (!validateEmail(from)) {
return {
success: false,
error: "Invalid email address format"
};
}
const recipients = Array.isArray(options.to) ? options.to : [options.to];
for (const recipient of recipients) {
if (!validateEmail(recipient)) {
return {
success: false,
error: `Invalid recipient email address format: ${recipient}`
};
}
}
for (this.retryCount = 0; this.retryCount <= this.config.maxRetries; this.retryCount++) {
try {
if (this.retryCount > 0) {
this.log(
`Retrying email send (attempt ${this.retryCount} of ${this.config.maxRetries})...`
);
await new Promise(
(resolve) => setTimeout(resolve, this.config.retryDelay)
);
}
if (!this.connected) {
await this.connect();
await this.smtpHandshake();
}
await this.sendCommand(`MAIL FROM:<${from}>`, 250);
for (const recipient of recipients)
await this.sendCommand(`RCPT TO:<${recipient}>`, 250);
if (options.cc) {
const ccList = Array.isArray(options.cc) ? options.cc : [options.cc];
for (const cc of ccList) {
if (validateEmail(cc))
await this.sendCommand(`RCPT TO:<${cc}>`, 250);
}
}
if (options.bcc) {
const bccList = Array.isArray(options.bcc) ? options.bcc : [options.bcc];
for (const bcc of bccList) {
if (validateEmail(bcc))
await this.sendCommand(`RCPT TO:<${bcc}>`, 250);
}
}
await this.sendCommand("DATA", 354);
const emailContent = this.createMultipartEmail(options);
if (emailContent.length > this.maxEmailSize) {
return {
success: false,
error: "Email size exceeds maximum allowed"
};
}
await this.sendCommand(`${emailContent}\r
.`, 250);
const messageIdMatch = /Message-ID: (.*)/i.exec(emailContent);
const messageId = messageIdMatch ? messageIdMatch[1].trim() : void 0;
return {
success: true,
messageId,
message: "Email sent successfully"
};
} catch (error) {
const errorMessage = error.message;
this.log(`Error sending email: ${errorMessage}`, true);
const isPermanentError = errorMessage.includes("5.") || // 5xx SMTP errors are permanent
errorMessage.includes("Authentication failed") || errorMessage.includes("certificate");
if (isPermanentError || this.retryCount >= this.config.maxRetries) {
return {
success: false,
error: errorMessage
};
}
try {
if (this.connected) {
await this.sendCommand("RSET", 250);
}
} catch (_error) {
}
this.socket?.end();
this.connected = false;
}
}
return {
success: false,
error: "Maximum retry count exceeded"
};
}
/**
* Close the connection gracefully
*/
async close() {
try {
if (this.connected) {
await this.sendCommand("QUIT", 221);
}
} catch (e) {
this.log(`Error during QUIT: ${e.message}`, true);
} finally {
if (this.socket) {
this.socket.destroy();
this.socket = null;
this.connected = false;
}
}
}
};
// node_modules/mikromail/lib/chunk-422R3NOG.mjs
var MikroMail = class {
smtpClient;
constructor(options) {
const config = new Configuration(options).get();
const smtpClient = new SMTPClient(config);
this.smtpClient = smtpClient;
}
/**
* Sends an email to valid domains.
*/
async send(emailOptions) {
try {
const recipients = Array.isArray(emailOptions.to) ? emailOptions.to : [emailOptions.to];
for (const recipient of recipients) {
const hasMXRecords = await verifyEmailDomain(recipient);
if (!hasMXRecords)
console.error(
`Warning: No MX records found for recipient domain: ${recipient}`
);
}
const result = await this.smtpClient.sendEmail(emailOptions);
if (result.success) console.log(`Message ID: ${result.messageId}`);
else console.error(`Failed to send email: ${result.error}`);
await this.smtpClient.close();
} catch (error) {
console.error(
"Error in email sending process:",
error.message
);
}
}
};
// node_modules/mikroauth/lib/index.mjs
var g = () => {
let o = T(process.env.DEBUG) || false;
return { auth: { jwtSecret: process.env.AUTH_JWT_SECRET || "your-jwt-secret", magicLinkExpirySeconds: 900, jwtExpirySeconds: 3600, refreshTokenExpirySeconds: 604800, maxActiveSessions: 3, appUrl: process.env.APP_URL || "http://localhost:3000", templates: null, debug: o }, email: { emailSubject: "Your Secure Login Link", user: process.env.EMAIL_USER || "", host: process.env.EMAIL_HOST || "", password: process.env.EMAIL_PASSWORD || "", port: 465, secure: true, maxRetries: 2, debug: o }, storage: { databaseDirectory: "mikroauth", encryptionKey: process.env.STORAGE_KEY || "", debug: o }, server: { port: Number(process.env.PORT) || 3e3, host: process.env.HOST || "0.0.0.0", useHttps: false, useHttp2: false, sslCert: "", sslKey: "", sslCa: "", rateLimit: { enabled: true, requestsPerMinute: 100 }, allowedDomains: ["*"], debug: o } };
};
function T(o) {
return o === "true" || o === true;
}
var y = class {
algorithm = "HS256";
secret = "HS256";
constructor(e) {
if (process.env.NODE_ENV === "production" && (!e || e.length < 32 || e === g().auth.jwtSecret)) throw new Error("Production environment requires a strong JWT secret (min 32 chars)");
this.secret = e;
}
sign(e, t = {}) {
let r = { alg: this.algorithm, typ: "JWT" }, i = Math.floor(Date.now() / 1e3), s = { ...e, iat: i };
t.exp !== void 0 && (s.exp = i + t.exp), t.notBefore !== void 0 && (s.nbf = i + t.notBefore), t.issuer && (s.iss = t.issuer), t.audience && (s.aud = t.audience), t.subject && (s.sub = t.subject), t.jwtid && (s.jti = t.jwtid);
let a = this.base64UrlEncode(JSON.stringify(r)), d = this.base64UrlEncode(JSON.stringify(s)), l = `${a}.${d}`, c = this.createSignature(l);
return `${l}.${c}`;
}
verify(e, t = {}) {
let r = this.decode(e);
if (r.header.alg !== this.algorithm) throw new Error(`Invalid algorithm. Expected ${this.algorithm}, got ${r.header.alg}`);
let [i, s] = e.split("."), a = `${i}.${s}`;
if (this.createSignature(a) !== r.signature) throw new Error("Invalid signature");
let l = r.payload, c = Math.floor(Date.now() / 1e3), u = t.clockTolerance || 0;
if (l.exp !== void 0 && l.exp + u < c) throw new Error("Token expired");
if (l.nbf !== void 0 && l.nbf - u > c) throw new Error("Token not yet valid");
if (t.issuer && l.iss !== t.issuer) throw new Error("Invalid issuer");
if (t.audience && l.aud !== t.audience) throw new Error("Invalid audience");
if (t.subject && l.sub !== t.subject) throw new Error("Invalid subject");
return l;
}
decode(e) {
let t = e.split(".");
if (t.length !== 3) throw new Error("Invalid token format");
try {
let [r, i, s] = t, a = JSON.parse(this.base64UrlDecode(r)), d = JSON.parse(this.base64UrlDecode(i));
return { header: a, payload: d, signature: s };
} catch {
throw new Error("Failed to decode token");
}
}
createSignature(e) {
let t = f.createHmac("sha256", this.secret).update(e).digest();
return this.base64UrlEncode(t);
}
base64UrlEncode(e) {
let t;
return typeof e == "string" ? t = Buffer.from(e) : t = e, t.toString("base64").replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
}
base64UrlDecode(e) {
let t = e.replace(/-/g, "+").replace(/_/g, "/");
switch (t.length % 4) {
case 0:
break;
case 2:
t += "==";
break;
case 3:
t += "=";
break;
default:
throw new Error("Invalid base64 string");
}
return Buffer.from(t, "base64").toString();
}
};
var w = class {
templates;
constructor(e) {
e ? this.templates = e : this.templates = L;
}
getText(e, t) {
return this.templates.textVersion(e, t).trim();
}
getHtml(e, t) {
return this.templates.htmlVersion(e, t).trim();
}
};
var L = { textVersion: (o, e) => `
Click this link to login: ${o}
Security Information:
- Expires in ${e} minutes
- Can only be used once
- Should only be used by you
If you didn't request this link, please ignore this email.
`, htmlVersion: (o, e) => `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Your Login Link</title>
<style>
body {
font-family: Arial, sans-serif;
line-height: 1.6;
color: #333;
max-width: 600px;
margin: 0 auto;
padding: 20px;
}
.container {
border: 1px solid #e1e1e1;
border-radius: 5px;
padding: 20px;
}
.button {
display: inline-block;
background-color: #4CAF50;
color: white;
text-decoration: none;
padding: 10px 20px;
border-radius: 5px;
margin: 20px 0;
}
.security-info {
background-color: #f8f8f8;
padding: 15px;
border-radius: 5px;
margin-top: 20px;
}
.footer {
margin-top: 20px;
font-size: 12px;
color: #888;
}
</style>
</head>
<body>
<div class="container">
<h2>Your Secure Login Link</h2>
<p>Click the button below to log in to your account:</p>
<a href="${o}" class="button">Login to Your Account</a>
<p>
Hello, this is a test email! Hall\xE5, MikroMail has international support for, among others, espa\xF1ol, fran\xE7ais, portugu\xEAs, \u4E2D\u6587, \u65E5\u672C\u8A9E, and \u0420\u0443\u0441\u0441\u043A\u0438\u0439!
</p>
<div class="security-info">
<h3>Security Information:</h3>
<ul>
<li>This link expires in ${e} minutes</li>
<li>Can only be used once</li>
<li>Should only be used by you</li>
</ul>
</div>
<p>If you didn't request this link, please ignore this email.</p>
<div class="footer">
<p>This is an automated message, please do not reply to this email.</p>
</div>
</div>
</body>
</html>
` };
var h = class {
constructor(e) {
this.options = e;
}
sentEmails = [];
async sendMail(e) {
this.sentEmails.push(e), this.options?.logToConsole && (console.log("Email sent:"), console.log(`From: ${e.from}`), console.log(`To: ${e.to}`), console.log(`Subject: ${e.subject}`), console.log(`Text: ${e.text}`)), this.options?.onSend && this.options.onSend(e);
}
getSentEmails() {
return [...this.sentEmails];
}
clearSentEmails() {
this.sentEmails = [];
}
};
var m = class {
data = /* @__PURE__ */ new Map();
collections = /* @__PURE__ */ new Map();
expiryEmitter = new EventEmitter();
expiryCheckInterval;
constructor(e = 1e3) {
this.expiryCheckInterval = setInterval(() => this.checkExpiredItems(), e);
}
destroy() {
clearInterval(this.expiryCheckInterval), this.data.clear(), this.collections.clear(), this.expiryEmitter.removeAllListeners();
}
checkExpiredItems() {
let e = Date.now();
for (let [t, r] of this.data.entries()) r.expiry && r.expiry < e && (this.data.delete(t), this.expiryEmitter.emit("expired", t));
for (let [t, r] of this.collections.entries()) r.expiry && r.expiry < e && (this.collections.delete(t), this.expiryEmitter.emit("expired", t));
}
async set(e, t, r) {
let i = r ? Date.now() + r * 1e3 : null;
this.data.set(e, { value: t, expiry: i });
}
async get(e) {
let t = this.data.get(e);
return t ? t.expiry && t.expiry < Date.now() ? (this.data.delete(e), null) : t.value : null;
}
async delete(e) {
this.data.delete(e), this.collections.delete(e);
}
async addToCollection(e, t, r) {
this.collections.has(e) || this.collections.set(e, { items: [], expiry: r ? Date.now() + r * 1e3 : null });
let i = this.collections.get(e);
r && (i.expiry = Date.now() + r * 1e3), i.items.push(t);
}
async removeFromCollection(e, t) {
let r = this.collections.get(e);
r && (r.items = r.items.filter((i) => i !== t));
}
async getCollection(e) {
let t = this.collections.get(e);
return t ? [...t.items] : [];
}
async getCollectionSize(e) {
let t = this.collections.get(e);
return t ? t.items.length : 0;
}
async removeOldestFromCollection(e) {
let t = this.collections.get(e);
return !t || t.items.length === 0 ? null : t.items.shift() || null;
}
async findKeys(e) {
let t = e.replace(/\*/g, ".*").replace(/\?/g, "."), r = new RegExp(`^${t}$`), i = Array.from(this.data.keys()).filter((a) => r.test(a)), s = Array.from(this.collections.keys()).filter((a) => r.test(a));
return [.../* @__PURE__ */ new Set([...i, ...s])];
}
};
function S(o) {
if (!o || o.trim() === "" || (o.match(/@/g) || []).length !== 1) return false;
let [t, r] = o.split("@");
return !(!t || !r || o.includes("..") || !I(t) || !$(r));
}
function I(o) {
return o.startsWith('"') && o.endsWith('"') ? !o.slice(1, -1).includes('"') : o.length > 64 || o.startsWith(".") || o.endsWith(".") ? false : /^[a-zA-Z0-9!#$%&'*+/=?^_`{|}~.-]+$/.test(o);
}
function $(o) {
if (o.startsWith("[") && o.endsWith("]")) {
let t = o.slice(1, -1);
return t.startsWith("IPv6:") ? R(t.slice(5)) : M(t);
}
let e = o.split(".");
if (e.length === 0) return false;
for (let t of e) if (!t || t.length > 63 || !/^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$/.test(t)) return false;
if (e.length > 1) {
let t = e[e.length - 1];
if (!/^[a-zA-Z]{2,}$/.test(t)) return false;
}
return true;
}
function M(o) {
return /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/.test(o);
}
function R(o) {
if (!/^[a-fA-F0-9:]+$/.test(o)) return false;
let t = o.split(":");
return !(t.length < 2 || t.length > 8);
}
var n = g();
var P = (o) => {
let e = { configFilePath: "mikroauth.config.json", args: process.argv, options: [{ flag: "--jwtSecret", path: "auth.jwtSecret", defaultValue: n.auth.jwtSecret }, { flag: "--magicLinkExpirySeconds", path: "auth.magicLinkExpirySeconds", defaultValue: n.auth.magicLinkExpirySeconds }, { flag: "--jwtExpirySeconds", path: "auth.jwtExpirySeconds", defaultValue: n.auth.jwtExpirySeconds }, { flag: "--refreshTokenExpirySeconds", path: "auth.refreshTokenExpirySeconds", defaultValue: n.auth.refreshTokenExpirySeconds }, { flag: "--maxActiveSessions", path: "auth.maxActiveSessions", defaultValue: n.auth.maxActiveSessions }, { flag: "--appUrl", path: "auth.appUrl", defaultValue: n.auth.appUrl }, { flag: "--debug", path: "auth.debug", isFlag: true, defaultValue: n.auth.debug }, { flag: "--emailSubject", path: "email.emailSubject", defaultValue: "Your Secure Login Link" }, { flag: "--emailHost", path: "email.host", defaultValue: n.email.host }, { flag: "--emailUser", path: "email.user", defaultValue: n.email.user }, { flag: "--emailPassword", path: "email.password", defaultValue: n.email.password }, { flag: "--emailPort", path: "email.port", defaultValue: n.email.port }, { flag: "--emailSecure", path: "email.secure", isFlag: true, defaultValue: n.email.secure }, { flag: "--emailMaxRetries", path: "email.maxRetries", defaultValue: n.email.maxRetries }, { flag: "--debug", path: "email.debug", isFlag: true, defaultValue: n.email.debug }, { flag: "--dir", path: "storage.databaseDirectory", defaultValue: n.storage.databaseDirectory }, { flag: "--encryptionKey", path: "storage.encryptionKey", defaultValue: n.storage.encryptionKey }, { flag: "--debug", path: "storage.debug", defaultValue: n.storage.debug }, { flag: "--port", path: "server.port", defaultValue: n.server.port }, { flag: "--host", path: "server.host", defaultValue: n.server.host }, { flag: "--https", path: "server.useHttps", isFlag: true, defaultValue: n.server.useHttps }, { flag: "--https", path: "server.useHttp2", isFlag: true, defaultValue: n.server.useHttp2 }, { flag: "--cert", path: "server.sslCert", defaultValue: n.server.sslCert }, { flag: "--key", path: "server.sslKey", defaultValue: n.server.sslKey }, { flag: "--ca", path: "server.sslCa", defaultValue: n.server.sslCa }, { flag: "--ratelimit", path: "server.rateLimit.enabled", defaultValue: n.server.rateLimit.enabled, isFlag: true }, { flag: "--rps", path: "server.rateLimit.requestsPerMinute", defaultValue: n.server.rateLimit.requestsPerMinute }, { flag: "--allowed", path: "server.allowedDomains", defaultValue: n.server.allowedDomains, parser: parsers.array }, { flag: "--debug", path: "server.debug", isFlag: true, defaultValue: n.server.debug }] };
return o && (e.config = o), e;
};
var v = { linkSent: "If a matching account was found, a magic link has been sent.", revokedSuccess: "All other sessions revoked successfully.", logoutSuccess: "Logged out successfully." };
var E = class {
config;
email;
storage;
jwtService;
templates;
constructor(e, t, r) {
let i = new MikroConf(P({ auth: e.auth, email: e.email })).get();
i.auth.debug && console.log("Using configuration:", i), this.config = i, this.email = t || new h(), this.storage = r || new m(), this.jwtService = new y(i.auth.jwtSecret), this.templates = new w(i?.auth.templates), this.checkIfUsingDefaultCredentialsInProduction();
}
checkIfUsingDefaultCredentialsInProduction() {
process.env.NODE_ENV === "production" && this.config.auth.jwtSecret === g().auth.jwtSecret && (console.error("WARNING: Using default secrets in production environment!"), process.exit(1));
}
generateToken(e) {
let t = Date.now().toString(), r = f.randomBytes(32).toString("hex");
return f.createHash("sha256").update(`${e}:${t}:${r}`).digest("hex");
}
generateJsonWebToken(e) {
return this.jwtService.sign({ sub: e.id, email: e.email, username: e.username, role: e.role, exp: Math.floor(Date.now() / 1e3) + 60 * 60 * 24 });
}
generateRefreshToken() {
return f.randomBytes(40).toString("hex");
}
async trackSession(e, t, r) {
let i = `sessions:${e}`;
if (await this.storage.getCollectionSize(i) >= this.config.auth.maxActiveSessions) {
let a = await this.storage.removeOldestFromCollection(i);
a && await this.storage.delete(`refresh:${a}`);
}
await this.storage.addToCollection(i, t, this.config.auth.refreshTokenExpirySeconds), await this.storage.set(`refresh:${t}`, JSON.stringify(r), this.config.auth.refreshTokenExpirySeconds);
}
generateMagicLinkUrl(e) {
let { token: t, email: r } = e;
try {
return new URL(this.config.auth.appUrl), `${this.config.auth.appUrl}?token=${encodeURIComponent(t)}&email=${encodeURIComponent(r)}`;
} catch {
throw new Error("Invalid base URL configuration");
}
}
async createMagicLink(e) {
let { email: t, ip: r } = e;
if (!S(t)) throw new Error("Valid email required");
try {
let i = this.generateToken(t), s = `magic_link:${i}`, a = { email: t, ipAddress: r || "unknown", createdAt: Date.now() };
await this.storage.set(s, JSON.stringify(a), this.config.auth.magicLinkExpirySeconds);
let d = await this.storage.findKeys("magic_link:*");
for (let u of d) {
if (u === s) continue;
let p = await this.storage.get(u);
if (p) try {
JSON.parse(p).email === t && await this.storage.delete(u);
} catch {
}
}
console.log('this.config.email',this.config.email)
let l = this.generateMagicLinkUrl({ token: i, email: t }), c = Math.ceil(this.config.auth.magicLinkExpirySeconds / 60);
return await this.email.sendMail({ from: this.config.email.user, to: t, subject: this.config.email.emailSubject, text: this.templates.getText(l, c), html: this.templates.getHtml(l, c) }), { message: v.linkSent };
} catch (i) {
throw console.error(`Failed to process magic link request: ${i}`), new Error("Failed to process magic link request");
}
}
async verifyToken(e) {
let { token: t, email: r } = e;
try {
let i = `magic_link:${t}`, s = await this.storage.get(i);
if (!s) throw new Error("Invalid or expired token");
let a = JSON.parse(s);
if (a.email !== r) throw new Error("Email mismatch");
let d = a.username, l = a.role;
await this.storage.delete(i);
let c = f.randomBytes(16).toString("hex"), u = this.generateRefreshToken(), p = { sub: r, username: d, role: l, jti: c, lastLogin: a.createdAt, metadata: { ip: a.ipAddress }, exp: Math.floor(Date.now() / 1e3) + 60 * 60 * 24 }, b = this.jwtService.sign(p, { exp: this.config.auth.jwtExpirySeconds });
return await this.trackSession(r, u, { ...a, tokenId: c, createdAt: Date.now() }), { accessToken: b, refreshToken: u, exp: this.config.auth.jwtExpirySeconds, tokenType: "Bearer" };
} catch (i) {
throw console.error("Token verification error:", i), new Error("Verification failed");
}
}
async refreshAccessToken(e) {
try {
let t = await this.storage.get(`refresh:${e}`);
if (!t) throw new Error("Invalid or expired refresh token");
let r = JSON.parse(t), i = r.email;
if (!i) throw new Error("Invalid refresh token data");
let s = r.username, a = r.role, d = f.randomBytes(16).toString("hex"), l = { sub: i, username: s, role: a, jti: d, lastLogin: r.lastLogin || r.createdAt, metadata: { ip: r.ipAddress } }, c = this.jwtService.sign(l, { exp: this.config.auth.jwtExpirySeconds });
return r.lastUsed = Date.now(), await this.storage.set(`refresh:${e}`, JSON.stringify(r), this.config.auth.refreshTokenExpirySeconds), { accessToken: c, refreshToken: e, exp: this.config.auth.jwtExpirySeconds, tokenType: "Bearer" };
} catch (t) {
throw console.error("Token refresh error:", t), new Error("Token refresh failed");
}
}
verify(e) {
try {
return this.jwtService.verify(e);
} catch {
throw new Error("Invalid token");
}
}
async logout(e) {
try {
if (!e || typeof e != "string") throw new Error("Refresh token is required");
let t = await this.storage.get(`refresh:${e}`);
if (!t) return { message: v.logoutSuccess };
let i = JSON.parse(t).email;
if (!i) throw new Error("Invalid refresh token data");
await this.storage.delete(`refresh:${e}`);
let s = `sessions:${i}`;
return await this.storage.removeFromCollection(s, e), { message: v.logoutSuccess };
} catch (t) {
throw console.error("Logout error:", t), new Error("Logout failed");
}
}
async getSessions(e) {
try {
if (!e.user?.email) throw new Error("User not authenticated");
let t = e.user.email, r = e.body?.refreshToken, i = `sessions:${t}`, a = (await this.storage.getCollection(i)).map(async (l) => {
try {
let c = await this.storage.get(`refresh:${l}`);
if (!c) return await this.storage.removeFromCollection(i, l), null;
let u = JSON.parse(c);
return { id: `${l.substring(0, 8)}...`, createdAt: u.createdAt || 0, lastLogin: u.lastLogin || u.createdAt || 0, lastUsed: u.lastUsed || u.createdAt || 0, metadata: { ip: u.ipAddress }, isCurrentSession: l === r };
} catch {
return await this.storage.removeFromCollection(i, l), null;
}
}), d = (await Promise.all(a)).filter(Boolean);
return d.sort((l, c) => c.createdAt - l.createdAt), { sessions: d };
} catch (t) {
throw console.error("Get sessions error:", t), new Error("Failed to fetch sessions");
}
}
async revokeSessions(e) {
try {
if (!e.user?.email) throw new Error("User not authenticated");
let t = e.user.email, r = e.body?.refreshToken, i = `sessions:${t}`, s = await this.storage.getCollection(i);
for (let a of s) r && a === r || await this.storage.delete(`refresh:${a}`);
return await this.storage.delete(i), r && await this.storage.get(`refresh:${r}`) && await this.storage.addToCollection(i, r, this.config.auth.refreshTokenExpirySeconds), { message: v.revokedSuccess };
} catch (t) {
throw console.error("Revoke sessions error:", t), new Error("Failed to revoke sessions");
}
}
authenticate(e, t) {
try {
let r = e.headers?.authorization;
if (!r || !r.startsWith("Bearer ")) throw new Error("Authentication required");
let i = r.split(" ")[1];
try {
let s = this.verify(i);
e.user = { email: s.sub }, t();
} catch {
throw new Error("Invalid or expired token");
}
} catch (r) {
t(r);
}
}
};
var x = class {
db;
PREFIX_KV = "kv:";
PREFIX_COLLECTION = "coll:";
TABLE_NAME = "mikroauth";
constructor(e) {
this.db = e;
}
async start() {
await this.db.start();
}
async close() {
await this.db.close();
}
async set(e, t, r) {
let i = `${this.PREFIX_KV}${e}`, s = r ? Date.now() + r * 1e3 : void 0;
await this.db.write({ tableName: this.TABLE_NAME, key: i, value: t, expiration: s });
}
async get(e) {
let t = `${this.PREFIX_KV}${e}`, r = await this.db.get({ tableName: this.TABLE_NAME, key: t });
return r || null;
}
async delete(e) {
let t = `${this.PREFIX_KV}${e}`;
await this.db.delete({ tableName: this.TABLE_NAME, key: t });
}
async addToCollection(e, t, r) {
let i = `${this.PREFIX_COLLECTION}${e}`, s = await this.db.get({ tableName: this.TABLE_NAME, key: i }), a = [];
s && (a = JSON.parse(s)), a.includes(t) || a.push(t);
let d = r ? Date.now() + r * 1e3 : void 0;
await this.db.write({ tableName: this.TABLE_NAME, key: i, value: JSON.stringify(a), expiration: d });
}
async removeFromCollection(e, t) {
let r = `${this.PREFIX_COLLECTION}${e}`, i = await this.db.get({ tableName: this.TABLE_NAME, key: r });
if (!i) return;
let s = JSON.parse(i);
s = s.filter((a) => a !== t), await this.db.write({ tableName: this.TABLE_NAME, key: r, value: JSON.stringify(s) });
}
async getCollection(e) {
let t = `${this.PREFIX_COLLECTION}${e}`, r = await this.db.get({ tableName: this.TABLE_NAME, key: t });
return r ? JSON.parse(r) : [];
}
async getCollectionSize(e) {
return (await this.getCollection(e)).length;
}
async removeOldestFromCollection(e) {
let t = `${this.PREFIX_COLLECTION}${e}`, r = await this.db.get({ tableName: this.TABLE_NAME, key: t });
if (!r) return null;
let i = JSON.parse(r);
if (i.length === 0) return null;
let s = i.shift();
return await this.db.write({ tableName: this.TABLE_NAME, key: t, value: JSON.stringify(i) }), s;
}
async findKeys(e) {
let t = e.replace(/\./g, "\\.").replace(/\*/g, ".*").replace(/\?/g, "."), r = new RegExp(`^${t}$`);
return (await this.db.get({ tableName: this.TABLE_NAME })).filter((s) => {
let a = s[0];
return typeof a == "string" && a.startsWith(this.PREFIX_KV);
}).map((s) => s[0].substring(this.PREFIX_KV.length)).filter((s) => r.test(s));
}
};
var k = class {
email;
sender;
constructor(e) {
this.sender = e.user, this.email = new MikroMail({ config: e });
}
async sendMail(e) {
await this.email.send({ from: this.sender, to: e.to, cc: e.cc, bcc: e.bcc, subject: e.subject, text: e.text, html: e.html });
}
};
// node_modules/mikrodb/lib/chunk-XNHPVSW4.mjs
var NotFoundError = class extends Error {
constructor(message) {
super();
this.name = "NotFoundError";
this.message = message || "Resource not found";
this.cause = { statusCode: 404 };
}
};
var CheckpointError = class extends Error {
constructor(message) {
super();
this.name = "CheckpointError";
this.message = message;
this.cause = { statusCode: 500 };
}
};
// node_modules/mikrodb/lib/chunk-P5SMO7C5.mjs
var time = () => Date.now();
var getJsonValueFromEntry = (json, operation) => {
if (operation === "D") return null;
if (json.length === 0 || json.join(" ") === "null") return null;
return getJsonValue(json);
};
var getJsonValue = (value) => {
try {
return JSON.parse(value.join(" "));
} catch (_error) {
return void 0;
}
};
function getTruthyValue(value) {
if (value === "true" || value === true) return true;
return false;
}
function createGetOptions(options) {
const getValue = (value, format) => {
if (!value) return void 0;
if (format === "json") return getJsonValue(value) || value;
if (format === "number") return Number.parseInt(value, 10);
return void 0;
};
const filter = getValue(options?.filter, "json");
const sort = getValue(options?.sort, "json");
const limit = getValue(options?.limit, "number");
const offset = getValue(options?.offset, "number");
if (!filter && !sort && !limit && !offset) return void 0;
return {
filter,
sort,
limit,
offset
};
}
// node_modules/mikrodb/lib/chunk-2CSO3WVR.mjs
import { existsSync as existsSync3 } from "node:fs";
import { readFile, unlink, writeFile } from "node:fs/promises";
var Checkpoint = class {
/**
* Checkpoint interval in milliseconds.
*/
checkpointInterval;
/**
* Default time in milliseconds before checkpointing.
*/
defaultCheckpointIntervalMs = 10 * 1e3;
/**
* Flag to prevent concurrent checkpoints.
*/
isCheckpointing = false;
/**
* Timestamp of the last successful checkpoint.
*/
lastCheckpointTime = 0;
/**
* Write-ahead log file path.
*/
walFile;
/**
* Checkpoint interval timer.
*/
checkpointTimer = null;
wal;
table;
constructor(options) {
const { table, wal, walFile, checkpointIntervalMs } = options;
this.defaultCheckpointIntervalMs = checkpointIntervalMs || this.defaultCheckpointIntervalMs;
this.table = table;
this.wal = wal;
this.walFile = walFile;
this.checkpointInterval = checkpointIntervalMs;
this.lastCheckpointTime = time();
}
/**
* @description Start the checkpoint service.
*/
async start() {
const checkpointFile = `${this.walFile}.checkpoint`;
if (existsSync3(checkpointFile)) {
console.log("Incomplete checkpoint detected, running recovery...");
try {
const checkpointTimestamp = await readFile(checkpointFile, "utf8");
console.log(
`Incomplete checkpoint from: ${new Date(Number.parseInt(checkpointTimestamp))}`
);
await this.checkpoint(true);
} catch (error) {
throw new NotFoundError(`Error reading checkpoint file: ${error}`);
}
}
this.checkpointTimer = setInterval(async () => {
try {
await this.checkpoint();
} catch (error) {
throw new CheckpointError(`Checkpoint interval failed: ${error}`);
}
}, this.checkpointInterval);
}
/**
* @description Stop the checkpoint service.
*/
stop() {
if (this.checkpointTimer) {
clearInterval(this.checkpointTimer);
this.checkpointTimer = null;
}
this.isCheckpointing = false;
}
/**
* @description Perform a checkpoint operation to clean up WAL.
* This ensures all tables mentioned in the WAL are persisted to disk,
* and then truncates the WAL file.
*/
async checkpoint(force = false) {
if (this.isCheckpointing) return;
const now = time();
if (!force && now - this.lastCheckpointTime < this.checkpointInterval) return;
this.isCheckpointing = true;
try {
await this.wal.flushWAL();
const checkpointFile = `${this.walFile}.checkpoint`;
await writeFile(checkpointFile, now.toString(), "utf8");
const tablesInWAL = await this.getTablesFromWAL();
await this.persistTables(tablesInWAL);
try {
await writeFile(this.walFile, "", "utf8");
if (process.env.DEBUG === "true") console.log("WAL truncated successfully");
} catch (error) {
throw new CheckpointError(`Failed to truncate WAL: ${error}`);
}
if (existsSync3(checkpointFile)) await unlink(checkpointFile);
this.wal.clearPositions();
this.lastCheckpointTime = now;
if (process.env.DEBUG === "true") console.log("Checkpoint complete");
} catch (error) {
throw new CheckpointError(`Checkpoint failed: ${error}`);
} finally {
this.isCheckpointing = false;
}
}
/**
* @description Extract table names from WAL entries.
*/
async getTablesFromWAL() {
const tablesInWAL = /* @__PURE__ */ new Set();
if (!existsSync3(this.walFile)) return tablesInWAL;
try {
const walContent = await readFile(this.walFile, "utf8");
if (!walContent.trim()) return tablesInWAL;
const entries = walContent.trim().split("\n");
for (const entry of entries) {
if (!entry.trim()) continue;
const parts = entry.split(" ");
if (parts.length >= 3) tablesInWAL.add(parts[2]);
}
} catch (error) {
throw new CheckpointError(`Error reading WAL file: ${error}`);
}
return tablesInWAL;
}
/**
* @description Persist tables to disk.
*/
async persistTables(tableNames) {
const persistPromises = Array.from(tableNames).map(async (tableName) => {
try {
await this.table.flushTableToDisk(tableName);
console.log(`Checkpointed table "${tableName}"`);
} catch (error) {
throw new CheckpointError(`Failed to checkpoint table "${tableName}": ${error.message}`);
}
});
await Promise.all(persistPromises);
}
};
// node_modules/mikrodb/lib/chunk-SWRLKQPO.mjs
import { existsSync as existsSync4, statSync, writeFileSync } from "node:fs";
import { appendFile, readFile as readFile2 } from "node:fs/promises";
var WriteAheadLog = class {
/**
* The path to the WAL file.
*/
walFile;
/**
* WAL flush interval in milliseconds.
*/
walInterval;
/**
* Buffer of all WAL changes.
*/
walBuffer = [];
/**
* The maximum number of WAL entries before flushing.
*/
maxWalBufferEntries = Number.parseInt(
process.env.MAX_WAL_BUFFER_ENTRIES || "100"
);
/**
* The maximum size of the WAL buffer before flushing.
*/
maxWalBufferSize = Number.parseInt(
process.env.MAX_WAL_BUFFER_SIZE || (1024 * 1024 * 0.01).toString()
// 10 KB
);
/**
* The maximum size of the WAL buffer before checkpointing.
*/
maxWalSizeBeforeCheckpoint = Number.parseInt(
process.env.MAX_WAL_BUFFER_SIZE || (1024 * 1024 * 0.01).toString()
// 10 KB
);
/**
* Keeps track of the last processed entries.
*/
lastProcessedEntryCount = /* @__PURE__ */ new Map();
/**
* A callback function to run when checkpointing.
*/
checkpointCallback = null;
walTimer = null;
constructor(walFile, walInterval) {
this.walFile = walFile;
this.walInterval = walInterval;
this.start();
}
/**
* @description Sets the checkpoint callback function.
*/
setCheckpointCallback(callback) {
this.checkpointCallback = callback;
}
/**
* @description Start the WAL functionality.
*/
start() {
if (!existsSync4(this.walFile)) writeFileSync(this.walFile, "", "utf-8");
this.walTimer = setInterval(async () => {
try {
if (this.walBuffer.length > 0) await this.flushWAL();
} catch (error) {
console.error("WAL flush interval failed:", error);
}
}, this.walInterval);
}
/**
* @description Stop the WAL timer. Primarily used for tests.
*/
stop() {
if (this.walTimer) {
clearInterval(this.walTimer);
this.walTimer = null;
}
}
/**
* @description Check if the WAL file exists.
*/
checkWalFileExists() {
if (!existsSync4(this.walFile))
throw new NotFoundError(`WAL file "${this.walFile}" does not exist`);
}
/**
* @description Load WAL entries and return operations to be applied.
*/
async loadWAL(table) {
this.checkWalFileExists();
const operations = [];
const walSize = statSync(this.walFile)?.size || 0;
if (walSize === 0) return operations;
try {
const walData = await readFile2(this.walFile, "utf8");
const logEntries = walData.trim().split("\n");
const now = time();
for (let index = 0; index < logEntries.length; index++) {
const entry = logEntries[index];
if (!entry.trim()) continue;
const lastPosition = this.lastProcessedEntryCount.get(table || "") || 0;
if (table && index < lastPosition) continue;
const [_timestamp, operation, tableName, version, expiration, key, ...json] = logEntries[index].split(" ");
if (table && tableName !== table) continue;
const parsedVersion = Number(version.split(":")[1]);
const parsedExpiration = expiration === "0" ? null : Number(expiration.split(":")[1]);
if (parsedExpiration && parsedExpiration < now) continue;
const value = getJsonValueFromEntry(json, operation);
if (value === void 0) continue;
if (table) this.lastProcessedEntryCount.set(table, index + 1);
if (operation === "W") {
operations.push({
operation: "W",
tableName,
key,
data: {
value,
version: parsedVersion,
timestamp: now,
expiration: parsedExpiration
}
});
} else if (operation === "D") {
operations.push({
operation: "D",
tableName,
key
});
}
}
return operations;
} catch (error) {
if (table) console.error(`Failed to replay WAL for table "${table}": ${error.message}`);
else console.error(`Failed to replay WAL: ${error.message}`);
return operations;
}
}
/**
* @description Checks if there are any new WAL entries for a table.
*/
async hasNewWALEntriesForTable(tableName) {
this.checkWalFileExists();
try {
const walData = await readFile2(this.walFile, "utf8");
if (!walData.trim()) return false;
const logEntries = walData.trim().split("\n");
const lastEntryCount = this.lastProcessedEntryCount.get(tableName) || 0;
if (lastEntryCount >= logEntries.length) return false;
for (let i = lastEntryCount; i < logEntries.length; i++) {
const entry = logEntries[i];
if (!entry.trim()) continue;
const parts = entry.split(" ");
if (parts.length >= 3 && parts[2] === tableName) return true;
}
return false;
} catch (error) {
console.error(`Error checking WAL for ${tableName}:`, error);
return true;
}
}
/**
* @description Flush the WAL buffer to disk.
*/
async flushWAL() {
this.checkWalFileExists();
if (this.walBuffer.length === 0) return;
const bufferToFlush = [...this.walBuffer];
this.walBuffer = [];
try {
await appendFile(this.walFile, bufferToFlush.join(""), "utf8");
const stats = statSync(this.walFile);
if (stats.size > this.maxWalSizeBeforeCheckpoint) {
if (process.env.DEBUG === "true")
console.log(
`WAL size (${stats.size}) exceeds limit (${this.maxWalSizeBeforeCheckpoint}), triggering checkpoint`
);
if (this.checkpointCallback) {
setImmediate(async () => {
try {
await this.checkpointCallback();
} catch (error) {
console.error("Error during automatic checkpoint:", error);
}
});
}
}
} catch (error) {
console.error(`Failed to flush WAL: ${error.message}`);
this.walBuffer = [...bufferToFlush, ...this.walBuffer];
throw error;
}
}
/**
* @description Append operation to the WAL (with version, timestamp, and expiration).
*/
async appendToWAL(tableName, operation, key, value, version, expiration = 0) {
this.checkWalFileExists();
const timestamp = time();
const logEntry = `${timestamp} ${operation} ${tableName} v:${version} x:${expiration} ${key} ${JSON.stringify(value)}
`;
this.walBuffer.push(logEntry);
if (this.walBuffer.length >= this.maxWalBufferEntries) await this.flushWAL();
const estimatedBufferSize = this.walBuffer.reduce((size, entry) => size + entry.length, 0);
if (estimatedBufferSize >= this.maxWalBufferSize) await this.flushWAL();
const stats = statSync(this.walFile);
if (stats.size > this.maxWalSizeBeforeCheckpoint) {
if (process.env.DEBUG === "true")
console.log(
`WAL size (${stats.size}) exceeds limit (${this.maxWalSizeBeforeCheckpoint}), triggering checkpoint`
);
if (this.checkpointCallback) {
setImmediate(async () => {
try {
await this.checkpointCallback();
} catch (error) {
console.error("Error during automatic checkpoint:", error);
}
});
}
}
}
/**
* @description Reset the count or position of the last processed entries that are tracked.
*/
clearPositions() {
this.lastProcessedEntryCount.clear();
}
};
// node_modules/mikrodb/lib/chunk-IRTCGE36.mjs
var configDefaults = () => {
return {
db: {
dbName: "mikrodb",
databaseDirectory: "mikrodb",
walFileName: "wal.log",
walInterval: 2e3,
encryptionKey: "",
maxWriteOpsBeforeFlush: 100,
debug: getTruthyValue(process.env.DEBUG) || false
},
events: {},
server: {
port: Number(process.env.PORT) || 3e3,
host: process.env.HOST || "0.0.0.0",
useHttps: false,
useHttp2: false,
sslCert: "",
sslKey: "",
sslCa: "",
rateLimit: {
enabled: true,
requestsPerMinute: 100
},
allowedDomains: ["*"],
debug: getTruthyValue(process.env.DEBUG) || false
}
};
};
// node_modules/mikrodb/lib/chunk-LFATMQSS.mjs
import { createCipheriv, createDecipheriv, randomBytes, scryptSync } from "node:crypto";
var Encryption = class {
algo = "aes-256-gcm";
KEY_LENGTH = 32;
// 256 bits
IV_LENGTH = 12;
// 96 bits - recommended for GCM
/**
* @description Derives a key from password and salt using scrypt
*/
generateKey(password, salt) {
return scryptSync(`${salt}#${password}`, salt, this.KEY_LENGTH);
}
/**
* @description Generates a random IV of appropriate length
*/
generateIV() {
return randomBytes(this.IV_LENGTH);
}
/**
* @description Encrypts plain text using AES-256-GCM
* @example
* const key = encryption.generateKey(password, salt);
* const iv = encryption.generateIV();
* const encryptedData = encryption.encrypt(plainText, key, iv);
*/
encrypt(plainText, key, iv) {
if (key.length !== this.KEY_LENGTH) {
throw new Error(
`Invalid key length: ${key.length} bytes. Expected: ${this.KEY_LENGTH} bytes`
);
}
if (iv.length !== this.IV_LENGTH) {
throw new Error(`Invalid IV length: ${iv.length} bytes. Expected: ${this.IV_LENGTH} bytes`);
}
const cipher = createCipheriv(this.algo, key, iv);
const encrypted = Buffer.concat([cipher.update(plainText, "utf8"), cipher.final()]);
const authTag = cipher.getAuthTag();
return { iv, encrypted, authTag };
}
/**
* @description Decrypts encrypted data using AES-256-GCM
* @example
* const key = encryption.generateKey(password, salt);
* const decrypted = encryption.decrypt(encryptedData, key);
*/
decrypt(encryptedData, key) {
const { iv, encrypted, authTag } = encryptedData;
if (key.length !== this.KEY_LENGTH) {
throw new Error(
`Invalid key length: ${key.length} bytes. Expected: ${this.KEY_LENGTH} bytes`
);
}
if (iv.length !== this.IV_LENGTH) {
throw new Error(`Invalid IV length: ${iv.length} bytes. Expected: ${this.IV_LENGTH} bytes`);
}
const decipher = createDecipheriv(this.algo, key, iv);
decipher.setAuthTag(authTag);
return Buffer.concat([decipher.update(encrypted), decipher.final()]).toString("utf8");
}
/**
* @description Combines encrypted data into a single buffer for storage
*/
serialize(data) {
return Buffer.concat([
// First byte: version for future format changes
Buffer.from([1]),
// Next byte: IV length
Buffer.from([data.iv.length]),
// IV
data.iv,
// Next byte: authTag length
Buffer.from([data.authTag.length]),
// AuthTag
data.authTag,
// Encrypted data
data.encrypted
]);
}
/**
* @description Extracts encrypted data components from a serialized buffer
*/
deserialize(serialized) {
let offset = 0;
const version = serialized[offset++];
if (version !== 1) {
throw new Error(`Unsupported encryption format version: ${version}`);
}
const ivLength = serialized[offset++];
const iv = serialized.subarray(offset, offset + ivLength);
offset += ivLength;
const authTagLength = serialized[offset++];
const authTag = serialized.subarray(offset, offset + authTagLength);
offset += authTagLength;
const encrypted = serialized.subarray(offset);
return { iv, authTag, encrypted };
}
// Utility methods
toHex(value) {
return value.toString("hex");
}
toUtf8(value) {
return value.toString("utf8");
}
toBuffer(hexString) {
return Buffer.from(hexString, "hex");
}
};
// node_modules/mikrodb/lib/chunk-EU6BRX7P.mjs
var Persistence = class {
/**
* @description Read table data from binary format with optimizations.
*/
readTableFromBinaryBuffer(buffer) {
if (buffer.length < 8 || buffer[0] !== 77 || // 'M'
buffer[1] !== 68 || // 'D'
buffer[2] !== 66 || // 'B'
buffer[3] !== 1) {
throw new Error("Invalid table file format");
}
const recordCount = buffer.readUInt32LE(4);
const tableData = /* @__PURE__ */ new Map();
let offset = 8;
const now = time();
for (let i = 0; i < recordCount && offset + 26 <= buffer.length; i++) {
const keyLength = buffer.readUInt16LE(offset);
offset += 2;
const valueLength = buffer.readUInt32LE(offset);
offset += 4;
const version = buffer.readUInt32LE(offset);
offset += 4;
const timestamp = Number(buffer.readBigUInt64LE(offset));
offset += 8;
const expiration = Number(buffer.readBigUInt64LE(offset));
offset += 8;
if (offset + keyLength + valueLength > buffer.length) break;
if (expiration && expiration <= now) {
offset += keyLength + valueLength;
continue;
}
const key = buffer.toString("utf8", offset, offset + keyLength);
offset += keyLength;
const valueBuffer = buffer.slice(offset, offset + valueLength);
offset += valueLength;
const value = this.decodeValue(valueBuffer);
tableData.set(key, {
value,
version,
timestamp,
expiration: expiration || null
});
}
return tableData;
}
/**
* @description Write table data to a binary file format.
*/
toBinaryBuffer(tableData) {
const chunks = [];
const header = Buffer.from([77, 68, 66, 1]);
chunks.push(header);
const validEntries = Array.from(tableData.entries()).filter(([key]) => typeof key === "string");
const countBuffer = Buffer.alloc(4);
countBuffer.writeUInt32LE(validEntries.length, 0);
chunks.push(countBuffer);
for (const [key, record] of validEntries) {
if (key === null || typeof key !== "string") continue;
const keyBuffer = Buffer.from(key);
const valueBuffer = this.encodeValue(record.value);
const keyLenBuffer = Buffer.alloc(2);
keyLenBuffer.writeUInt16LE(keyBuffer.length, 0);
chunks.push(keyLenBuffer);
const valueLenBuffer = Buffer.alloc(4);
valueLenBuffer.writeUInt32LE(valueBuffer.length, 0);
chunks.push(valueLenBuffer);
const versionBuffer = Buffer.alloc(4);
versionBuffer.writeUInt32LE(record.version || 0, 0);
chunks.push(versionBuffer);
const timestampBuffer = Buffer.alloc(8);
timestampBuffer.writeBigUInt64LE(BigInt(record.timestamp || 0), 0);
chunks.push(timestampBuffer);
const expirationBuffer = Buffer.alloc(8);
expirationBuffer.writeBigUInt64LE(BigInt(record.expiration || 0), 0);
chunks.push(expirationBuffer);
chunks.push(keyBuffer);
chunks.push(valueBuffer);
}
return Buffer.concat(chunks);
}
/**
* @description Encode a JavaScript value to a binary format.
*/
encodeValue(value) {
if (value === null || value === void 0) return Buffer.from([0]);
if (typeof value === "boolean") return Buffer.from([1, value ? 1 : 0]);
if (typeof value === "number") {
if (Number.isInteger(value) && value >= -2147483648 && value <= 2147483647) {
const buf2 = Buffer.alloc(5);
buf2[0] = 2;
buf2.writeInt32LE(value, 1);
return buf2;
}
const buf = Buffer.alloc(9);
buf[0] = 3;
buf.writeDoubleLE(value, 1);
return buf;
}
if (typeof value === "string") {
const strBuf = Buffer.from(value, "utf8");
const buf = Buffer.alloc(5 + strBuf.length);
buf[0] = 4;
buf.writeUInt32LE(strBuf.length, 1);
strBuf.copy(buf, 5);
return buf;
}
if (Array.isArray(value)) {
const encodedItems = [];
const countBuf = Buffer.alloc(5);
countBuf[0] = 5;
countBuf.writeUInt32LE(value.length, 1);
encodedItems.push(countBuf);
for (const item of value) encodedItems.push(this.encodeValue(item));
return Buffer.concat(encodedItems);
}
if (typeof value === "object") {
if (value instanceof Date) {
const buf = Buffer.alloc(9);
buf[0] = 7;
buf.writeBigInt64LE(BigInt(value.getTime()), 1);
return buf;
}
const keys = Object.keys(value);
const encodedItems = [];
const countBuf = Buffer.alloc(5);
countBuf[0] = 6;
countBuf.writeUInt32LE(keys.length, 1);
encodedItems.push(countBuf);
for (const key of keys) {
const keyBuf = Buffer.from(key, "utf8");
const keyLenBuf = Buffer.alloc(2);
keyLenBuf.writeUInt16LE(keyBuf.length, 0);
encodedItems.push(keyLenBuf);
encodedItems.push(keyBuf);
encodedItems.push(this.encodeValue(value[key]));
}
return Buffer.concat(encodedItems);
}
return this.encodeValue(String(value));
}
/**
* @description Optimized decode value with specialized fast paths
*/
decodeValue(buffer) {
if (buffer.length === 0) return null;
const type = buffer[0];
switch (type) {
case 0:
return null;
case 1:
return buffer[1] === 1;
case 2:
return buffer.readInt32LE(1);
case 3:
return buffer.readDoubleLE(1);
case 4: {
const length = buffer.readUInt32LE(1);
return buffer.toString("utf8", 5, 5 + length);
}
case 5: {
const count = buffer.readUInt32LE(1);
const array = new Array(count);
let offset = 5;
for (let i = 0; i < count; i++) {
const { value, bytesRead } = this.decodeValueWithSize(buffer, offset);
array[i] = value;
offset += bytesRead;
}
return array;
}
case 6: {
const count = buffer.readUInt32LE(1);
const obj = {};
let offset = 5;
for (let i = 0; i < count; i++) {
const keyLength = buffer.readUInt16LE(offset);
offset += 2;
const key = buffer.toString("utf8", offset, offset + keyLength);
offset += keyLength;
const { value, bytesRead } = this.decodeValueWithSize(buffer, offset);
obj[key] = value;
offset += bytesRead;
}
return obj;
}
case 7:
return new Date(Number(buffer.readBigInt64LE(1)));
default:
console.warn(`Unknown type byte: ${type}`);
return null;
}
}
/**
* @description Optimized helper method to decode a value using absolute offsets
*/
decodeValueWithSize(buffer, startOffset = 0) {
if (startOffset >= buffer.length) return { value: null, bytesRead: 0 };
const type = buffer[startOffset];
switch (type) {
case 0:
return { value: null, bytesRead: 1 };
case 1:
return { value: buffer[startOffset + 1] === 1, bytesRead: 2 };
case 2:
return { value: buffer.readInt32LE(startOffset + 1), bytesRead: 5 };
case 3:
return { value: buffer.readDoubleLE(startOffset + 1), bytesRead: 9 };
case 4: {
const length = buffer.readUInt32LE(startOffset + 1);
const value = buffer.toString("utf8", startOffset + 5, startOffset + 5 + length);
return { value, bytesRead: 5 + length };
}
case 5: {
const count = buffer.readUInt32LE(startOffset + 1);
const array = new Array(count);
let offset = startOffset + 5;
for (let i = 0; i < count; i++) {
const result = this.decodeValueWithSize(buffer, offset);
array[i] = result.value;
offset += result.bytesRead;
}
return { value: array, bytesRead: offset - startOffset };
}
case 6: {
const count = buffer.readUInt32LE(startOffset + 1);
const obj = {};
let offset = startOffset + 5;
for (let i = 0; i < count; i++) {
const keyLength = buffer.readUInt16LE(offset);
offset += 2;
const key = buffer.toString("utf8", offset, offset + keyLength);
offset += keyLength;
const result = this.decodeValueWithSize(buffer, offset);
obj[key] = result.value;
offset += result.bytesRead;
}
return { value: obj, bytesRead: offset - startOffset };
}
case 7:
return {
value: new Date(Number(buffer.readBigInt64LE(startOffset + 1))),
bytesRead: 9
};
default:
console.warn(`Unknown type byte: ${type} at offset ${startOffset}`);
return { value: null, bytesRead: 1 };
}
}
};
// node_modules/mikrodb/lib/chunk-HV5HISKT.mjs
import { writeFile as writeFile2 } from "node:fs/promises";
async function writeToDisk(filePath, data, encryptionKey) {
const encryption = new Encryption();
const persistence = new Persistence();
let buffer = persistence.toBinaryBuffer(data);
if (!buffer) {
console.log("Buffer is empty, skipping...");
return;
}
if (encryptionKey) {
try {
const key = encryption.generateKey(encryptionKey, "salt");
const dataString = buffer.toString("binary");
const iv = encryption.generateIV();
const encryptedData = encryption.encrypt(dataString, key, iv);
buffer = encryption.serialize(encryptedData);
} catch (error) {
console.error("Encryption failed:", error);
}
}
await writeFile2(filePath, buffer);
}
// node_modules/mikrodb/lib/chunk-JW5KA4ZR.mjs
var Cache = class {
/**
* Max number of tables to cache in memory.
*/
cacheLimit;
/**
* Track table access times for Least Recently Used (LRU) eviction.
*/
tableAccessTimes = /* @__PURE__ */ new Map();
constructor(options = {}) {
this.cacheLimit = options.cacheLimit ?? 20;
}
/**
* @description Track access to a table to update LRU information.
*/
trackTableAccess(tableName) {
this.tableAccessTimes.set(tableName, time());
}
/**
* @description Find tables that should be evicted based on LRU policy.
*/
findTablesForEviction(currentTableCount) {
if (currentTableCount <= this.cacheLimit) return [];
const evictionCount = currentTableCount - this.cacheLimit;
const evictionCandidates = Array.from(this.tableAccessTimes.entries()).sort((a, b) => a[1] - b[1]).slice(0, evictionCount).map(([tableName]) => tableName);
for (const tableName of evictionCandidates) this.tableAccessTimes.delete(tableName);
return evictionCandidates;
}
/**
* @description Remove table from tracking.
*/
removeTable(tableName) {
this.tableAccessTimes.delete(tableName);
}
/**
* @description Check if items are expired.
*/
findExpiredItems(items) {
const currentTimestamp = time();
const expiredItems = [];
for (const [key, item] of items.entries()) {
if (item.x && item.x < currentTimestamp) expiredItems.push([key, item]);
}
return expiredItems;
}
/**
* @description Clear all cache tracking data.
*/
clear() {
this.tableAccessTimes.clear();
}
};
// node_modules/mikrodb/lib/chunk-UIMLZ7S2.mjs
var Query = class {
/**
* @description Query a value from a table with filtering criteria.
*/
async query(table, filter, limit = 50) {
const records = /* @__PURE__ */ new Map();
for (const [k2, v2] of table.entries()) {
if (!filter || (typeof filter === "function" ? filter(v2.value) : this.evaluateFilter(v2.value, filter))) {
records.set(k2, v2.value);
if (limit && records.size >= limit) break;
}
}
return Array.from(records.values());
}
/**
* @description Evaluates a single filter condition against a value.
*/
evaluateCondition(value, condition) {
if (!condition || typeof condition !== "object" || !condition.operator)
return value === condition;
const { operator, value: targetValue } = condition;
switch (operator) {
case "eq":
return value === targetValue;
case "neq":
return value !== targetValue;
case "gt":
return value > targetValue;
case "gte":
return value >= targetValue;
case "lt":
return value < targetValue;
case "lte":
return value <= targetValue;
case "in":
return Array.isArray(targetValue) && targetValue.includes(value);
case "nin":
return Array.isArray(targetValue) && !targetValue.includes(value);
case "like":
return typeof value === "string" && typeof targetValue === "string" && value.toLowerCase().includes(targetValue.toLowerCase());
case "between":
return Array.isArray(targetValue) && targetValue.length === 2 && value >= targetValue[0] && value <= targetValue[1];
case "regex":
try {
const regex = new RegExp(targetValue);
return typeof value === "string" && regex.test(value);
} catch (e) {
console.error("Invalid regex pattern:", e);
return false;
}
case "contains":
return Array.isArray(value) && value.includes(targetValue);
case "containsAll":
return Array.isArray(value) && Array.isArray(targetValue) && targetValue.every((item) => value.includes(item));
case "containsAny":
return Array.isArray(value) && Array.isArray(targetValue) && targetValue.some((item) => value.includes(item));
case "size":
return Array.isArray(value) && value.length === targetValue;
default:
return false;
}
}
/**
* @description Evaluates a complex filter query against a record.
*/
evaluateFilter(record, filter) {
if (record === null || record === void 0) {
return false;
}
if ("$or" in filter)
return filter.$or.some(
(subFilter) => this.evaluateFilter(record, subFilter)
);
for (const [field, condition] of Object.entries(filter)) {
if (field.startsWith("$")) continue;
if (field.includes(".")) {
const parts = field.split(".");
let value = record;
for (const part of parts) {
value = value?.[part];
if (value === void 0 || value === null) return false;
}
if (!this.evaluateCondition(value, condition)) return false;
} else if (condition && typeof condition === "object" && !("operator" in condition)) {
const nestedValue = record[field];
if (nestedValue === void 0 || nestedValue === null) return false;
if (!this.evaluateFilter(nestedValue, condition)) return false;
} else {
const value = record[field];
if (!this.evaluateCondition(value, condition)) return false;
}
}
return true;
}
};
// node_modules/mikrodb/lib/chunk-3QUL2PJV.mjs
import { existsSync as existsSync5, mkdirSync } from "node:fs";
import { readFile as readFile3, writeFile as writeFile3 } from "node:fs/promises";
import { join } from "node:path";
// node_modules/mikroevent/lib/chunk-S5YKFESI.mjs
import { EventEmitter as EventEmitter2 } from "node:events";
var MikroEvent = class {
emitter;
targets = {};
options;
constructor(options) {
this.emitter = new EventEmitter2();
if (options?.maxListeners === 0) this.emitter.setMaxListeners(0);
else this.emitter.setMaxListeners(options?.maxListeners || 10);
this.options = {
errorHandler: options?.errorHandler || ((error) => console.error(error))
};
}
/**
* @description Add one or more Targets for events.
* @example
* // Add single Target that is triggered on all events
* events.addTarget({ name: 'my-internal-api', events: ['*'] });
* // Add single Target using HTTPS fetch
* events.addTarget({ name: 'my-external-api', url: 'https://api.mydomain.com', events: ['*'] });
* // Add multiple Targets, responding to multiple events (single Target shown)
* events.addTarget([{ name: 'my-interla-api', events: ['user.added', 'user.updated'] }]);
* @returns Boolean that expresses if all Targets were successfully added.
*/
addTarget(target) {
const targets = Array.isArray(target) ? target : [target];
const results = targets.map((target2) => {
if (this.targets[target2.name]) {
console.error(`Target with name '${target2.name}' already exists.`);
return false;
}
this.targets[target2.name] = {
name: target2.name,
url: target2.url,
headers: target2.headers || {},
events: target2.events || []
};
return true;
});
return results.every((result) => result === true);
}
/**
* @description Update an existing Target.
* @example
* events.updateTarget('system_a', { url: 'http://localhost:8000', events: ['user.updated'] };
* @returns Boolean that expresses if the Target was successfully added.
*/
updateTarget(name, update) {
if (!this.targets[name]) {
console.error(`Target with name '${name}' does not exist.`);
return false;
}
const target = this.targets[name];
if (update.url !== void 0) target.url = update.url;
if (update.headers) target.headers = { ...target.headers, ...update.headers };
if (update.events) target.events = update.events;
return true;
}
/**
* @description Remove a Target.
* @example
* events.removeTarget('system_a');
* @returns Boolean that expresses if the Target was successfully removed.
*/
removeTarget(name) {
if (!this.targets[name]) {
console.error(`Target with name '${name}' does not exist.`);
return false;
}
delete this.targets[name];
return true;
}
/**
* @description Add one or more events to an existing Target.
* @example
* events.addEventToTarget('system_a', ['user.updated', 'user.deleted']);
* @returns Boolean that expresses if all events were successfully added.
*/
addEventToTarget(name, events) {
if (!this.targets[name]) {
console.error(`Target with name '${name}' does not exist.`);
return false;
}
const eventsArray = Array.isArray(events) ? events : [events];
const target = this.targets[name];
eventsArray.forEach((event) => {
if (!target.events.includes(event)) target.events.push(event);
});
return true;
}
/**
* @description Register an event handler for internal events.
*/
on(eventName, handler) {
this.emitter.on(eventName, handler);
return this;
}
/**
* @description Remove an event handler.
*/
off(eventName, handler) {
this.emitter.off(eventName, handler);
return this;
}
/**
* @description Register a one-time event handler.
*/
once(eventName, handler) {
this.emitter.once(eventName, handler);
return this;
}
/**
* @description Emit an event locally and to its bound Targets.
* @example
* await events.emit('user.added', { id: 'abc123', name: 'Sam Person' });
* @return Returns a result object with success status and any errors.
*/
async emit(eventName, data) {
const result = {
success: true,
errors: []
};
const makeError = (targetName, eventName2, error) => ({
target: targetName,
event: eventName2,
error
});
const targets = Object.values(this.targets).filter(
(target) => target.events.includes(eventName) || target.events.includes("*")
);
targets.filter((target) => !target.url).forEach((target) => {
try {
this.emitter.emit(eventName, data);
} catch (error) {
const actualError = error instanceof Error ? error : new Error(String(error));
result.errors.push({
target: target.name,
event: eventName,
error: actualError
});
this.options.errorHandler(actualError, eventName, data);
result.success = false;
}
});
const externalTargets = targets.filter((target) => target.url);
if (externalTargets.length > 0) {
const promises = externalTargets.map(async (target) => {
try {
const response = await fetch(target.url, {
method: "POST",
headers: {
"Content-Type": "application/json",
...target.headers
},
body: JSON.stringify({
eventName,
data
})
});
if (!response.ok) {
const errorMessage = `HTTP error! Status: ${response.status}: ${response.statusText}`;
const httpError = new Error(errorMessage);
result.errors.push(makeError(target.name, eventName, httpError));
this.options.errorHandler(httpError, eventName, data);
result.success = false;
}
} catch (error) {
const actualError = error instanceof Error ? error : new Error(String(error));
result.errors.push(makeError(target.name, eventName, actualError));
this.options.errorHandler(actualError, eventName, data);
result.success = false;
}
});
await Promise.allSettled(promises);
}
return result;
}
/**
* @description Handle an incoming event arriving over HTTP.
* Used for server integrations, when you want to manually handle
* the incoming event payload.
*
* The processing will be async using `process.nextTick()`
* and running in a non-blocking fashion.
* @example
* await mikroEvent.handleIncomingEvent({
* eventName: 'user.created',
* data: { id: '123', name: 'Test User' }
* });
*/
async handleIncomingEvent(body) {
try {
const { eventName, data } = typeof body === "string" ? JSON.parse(body) : body;
process.nextTick(() => {
try {
this.emitter.emit(eventName, data);
} catch (error) {
this.options.errorHandler(
error instanceof Error ? error : new Error(String(error)),
eventName,
data
);
}
});
} catch (error) {
this.options.errorHandler(
error instanceof Error ? error : new Error(String(error)),
"parse_event"
);
throw error;
}
}
/**
* @description Create middleware for Express-style servers, i.e.
* using `req` and `res` objects. This is an approach that replaces
* using `handleIncomingEvent()` manually.
* @example
* const middleware = mikroEvent.createMiddleware();
* await middleware(req, res, next);
*/
createMiddleware() {
return async (req, res, next) => {
if (req.method !== "POST") {
if (next) next();
return;
}
if (req.body) {
try {
await this.handleIncomingEvent(req.body);
res.statusCode = 202;
res.end();
} catch (error) {
res.statusCode = 400;
res.end(JSON.stringify({ error: "Invalid event format" }));
if (next) next(error);
}
} else {
let body = "";
req.on("data", (chunk) => body += chunk.toString());
req.on("end", async () => {
try {
await this.handleIncomingEvent(body);
res.statusCode = 202;
res.end();
} catch (error) {
res.statusCode = 400;
res.end(JSON.stringify({ error: "Invalid event format" }));
if (next) next(error);
}
});
}
};
}
};
// node_modules/mikrodb/lib/chunk-3QUL2PJV.mjs
var Table = class {
/**
* Directory where the table files will be stored.
*/
databaseDirectory;
/**
* Write-ahead log file path.
*/
walFile;
/**
* Tracks the currently active table.
*/
activeTable = null;
/**
* In-memory store for active tables.
*/
data = /* @__PURE__ */ new Map();
/**
* Buffer to store committed write operations before flushing them to disk.
*/
writeBuffer = [];
/**
* The maximum number of writes that happen before flushing data to disk.
*/
maxWriteOpsBeforeFlush = process.env.MAX_WRITE_OPS_BEFORE_FLUSH ? Number.parseInt(process.env.MAX_WRITE_OPS_BEFORE_FLUSH) : configDefaults().db.maxWriteOpsBeforeFlush;
/**
* Optional encryption key to use for encrypt and decrypt actions.
*/
encryptionKey;
// Class references
cache;
encryption;
persistence;
query;
wal;
mikroEvent;
constructor(options, eventOptions) {
const { databaseDirectory, walFileName, walInterval } = options;
this.databaseDirectory = databaseDirectory;
this.walFile = join(this.databaseDirectory, walFileName);
this.encryptionKey = options.encryptionKey ? options.encryptionKey : null;
if (!existsSync5(this.databaseDirectory)) mkdirSync(this.databaseDirectory);
this.cache = new Cache();
this.encryption = new Encryption();
this.persistence = new Persistence();
this.query = new Query();
this.wal = new WriteAheadLog(this.walFile, walInterval);
this.mikroEvent = new MikroEvent();
this.setupEvents(eventOptions);
}
/**
* @description Start the table and apply any stale/dormant WAL entries at once.
*/
async start() {
await this.applyWALEntries();
}
/**
* @description Setup change data capture to stream events with MikroEvent.
* @see https://github.com/mikaelvesavuori/mikroevent
*/
setupEvents(config) {
config?.targets?.forEach((target) => {
this.mikroEvent.addTarget({
name: target.name,
url: target.url,
headers: target.headers,
events: target.events
});
});
config?.listeners?.forEach((listener) => this.mikroEvent.on(listener.event, listener.handler));
}
/**
* @description Switch the active table by loading it into memory if it's not already loaded.
* The table will be created if it does not exist.
*/
async setActiveTable(tableName) {
if (this.activeTable === tableName) return;
if (!this.hasTable(tableName)) await this.loadTable(tableName);
await this.applyWALEntries(tableName);
await this.evictTablesIfNeeded();
this.activeTable = tableName;
}
/**
* @description Apply WAL entries with optional table filtering.
*/
async applyWALEntries(tableName) {
const operations = await this.wal.loadWAL(tableName);
if (operations.length === 0) return;
const tables = tableName ? [tableName] : [...new Set(operations.map((op) => op.tableName))];
for (const table of tables) {
const tableOps = operations.filter((op) => op.tableName === table);
if (tableName && !this.hasTable(table)) await this.loadTable(table);
else this.createTable(table);
for (const op of tableOps) {
if (op.operation === "W" && op.data) this.setItem(table, op.key, op.data);
else if (op.operation === "D") await this.deleteItem(table, op.key);
}
}
}
/**
* @description Load the table from disk into memory using a binary format.
*/
async loadTable(tableName) {
if (this.hasTable(tableName)) return;
const filePath = join(this.databaseDirectory, tableName);
if (!existsSync5(filePath)) {
this.createTable(tableName);
return;
}
const encryptedBuffer = await readFile3(filePath);
let fileBuffer = encryptedBuffer;
if (this.encryptionKey && encryptedBuffer.length > 0 && encryptedBuffer[0] === 1) {
try {
const encryptedData = this.encryption.deserialize(encryptedBuffer);
const key = this.encryption.generateKey(this.encryptionKey, "salt");
const decryptedString = this.encryption.decrypt(encryptedData, key);
fileBuffer = Buffer.from(decryptedString, "binary");
} catch (error) {
console.error(`Failed to decrypt ${tableName}:`, error);
}
}
const tableData = this.persistence.readTableFromBinaryBuffer(fileBuffer);
this.data.set(tableName, tableData);
if (this.data.size > this.cache.cacheLimit) setImmediate(() => this.evictTablesIfNeeded());
}
/**
* @description Get data with optional filtering and sorting.
*/
async get(operation) {
const { tableName } = operation;
const key = operation.key;
const options = operation.options;
await this.setActiveTable(tableName);
if (!options) {
if (!key) return [...this.getAll(tableName)];
return this.getItem(tableName, key)?.value;
}
const table = this.getTable(tableName);
let results = await this.query.query(table, options.filter, options.limit);
if (options.sort) results = results.sort(options.sort);
if (options.offset != null || options.limit != null) {
const start2 = options.offset || 0;
const end = options.limit ? start2 + options.limit : void 0;
results = results.slice(start2, end);
}
return results;
}
/**
* @description Perform a batch write operation, which allows multiple writes to different tables.
*/
async write(operation, options = {}) {
const { concurrencyLimit = 10, flushImmediately = false } = options;
const operations = Array.isArray(operation) ? operation : [operation];
const totalOperations = operations.length;
let processedOperations = 0;
while (processedOperations < totalOperations) {
const batch = operations.slice(processedOperations, processedOperations + concurrencyLimit);
const promises = batch.map((operation2) => this.writeItem(operation2, false));
const results = await Promise.all(promises);
if (results.includes(false)) return false;
processedOperations += batch.length;
if (this.writeBuffer.length >= this.maxWriteOpsBeforeFlush) await this.flushWrites();
}
if (flushImmediately || totalOperations >= 0) await this.flush();
return true;
}
/**
* @description Write data to the active table with version control and expiration.
*/
async writeItem(operation, flushImmediately = false) {
const { tableName, key, value, expectedVersion = null, expiration = 0 } = operation;
await this.setActiveTable(tableName);
const { success, newVersion } = this.getItemVersion(tableName, key, expectedVersion);
if (!success) return false;
await this.wal.appendToWAL(tableName, "W", key, value, newVersion, expiration);
this.setItem(tableName, key, {
value,
v: newVersion,
t: time(),
x: expiration
});
this.addToWriteBuffer(tableName, key);
if (flushImmediately) await this.flush();
return true;
}
/**
* @description Delete a key from a table with version control and expiration.
*/
async delete(tableName, key, expectedVersion = null, flushImmediately = false) {
await this.setActiveTable(tableName);
if (!this.hasTable(tableName) || !this.hasKey(tableName, key)) {
console.log(`Key ${key} not found in table ${tableName}`);
return false;
}
const { success, currentVersion, expiration } = this.getItemVersion(
tableName,
key,
expectedVersion
);
if (!success) return false;
await this.wal.appendToWAL(tableName, "D", key, null, currentVersion, expiration);
await this.deleteItem(tableName, key);
if (flushImmediately) await this.flush();
return true;
}
/**
* @description Get a version of an item.
*/
getItemVersion(tableName, key, expectedVersion) {
const currentRecord = this.getItem(tableName, key);
const currentVersion = currentRecord ? currentRecord.version : 0;
const newVersion = currentVersion + 1;
const expiration = currentRecord ? currentRecord.expiration || 0 : 0;
let success = true;
if (expectedVersion !== null && currentVersion !== expectedVersion) {
console.log(
`Version mismatch for ${tableName}:${key}. Expected ${expectedVersion}, found ${currentVersion}`
);
success = false;
}
return { success, currentRecord, currentVersion, newVersion, expiration };
}
////////////////////////////////////
// Public methods towards MikroDB //
////////////////////////////////////
/**
* @description Create a table if it does not exist.
*/
createTable(tableName) {
this.trackTableAccess(tableName);
if (!this.hasTable(tableName)) this.data.set(tableName, /* @__PURE__ */ new Map());
}
/**
* @description Get a table.
*/
getTable(tableName) {
this.trackTableAccess(tableName);
if (!this.hasTable(tableName)) return /* @__PURE__ */ new Map();
return this.data.get(tableName);
}
/**
* @description Get the size of a table.
*/
async getTableSize(tableName) {
await this.setActiveTable(tableName);
this.trackTableAccess(tableName);
return this.data.get(tableName)?.size;
}
/**
* @description Delete a table.
*/
async deleteTable(tableName) {
this.trackTableAccess(tableName);
this.data.delete(tableName);
const operation = "table.deleted";
const { success, errors } = await this.mikroEvent.emit(operation, {
operation,
table: tableName
});
if (!success) console.error("Error when emitting events:", errors);
}
/**
* @description Check if a table exists.
*/
hasTable(tableName) {
this.trackTableAccess(tableName);
return this.data.has(tableName);
}
/**
* @description Check if a table has a given key.
*/
hasKey(tableName, key) {
this.trackTableAccess(tableName);
return this.data.get(tableName)?.has(key);
}
/**
* @description Get an item.
*/
getItem(tableName, key) {
this.trackTableAccess(tableName);
const item = this.data.get(tableName)?.get(key);
if (!item) return;
if (item?.x !== 0 && Date.now() > item?.x) {
this.deleteItem(tableName, key);
return;
}
return {
value: item.value,
version: item.v,
timestamp: item.t,
expiration: item.x
};
}
/**
* @description Get all items from a table.
*/
getAll(tableName) {
this.trackTableAccess(tableName);
const data = this.data.get(tableName);
if (!data) return [];
return Array.from(data);
}
/**
* @description Set an item in a table.
*/
setItem(tableName, key, item) {
this.trackTableAccess(tableName);
this.createTable(tableName);
this.data.get(tableName)?.set(key, item);
}
/**
* @description Delete an item from a table.
*/
async deleteItem(tableName, key) {
this.data.get(tableName)?.delete(key);
const operation = "item.deleted";
const { success, errors } = await this.mikroEvent.emit(operation, {
operation,
table: tableName,
key
});
if (!success) console.error("Error when emitting events:", errors);
}
/**
* @description Add item to write buffer.
*/
addToWriteBuffer(tableName, key) {
const record = this.getItem(tableName, key);
this.writeBuffer.push(JSON.stringify({ tableName, key, record }));
}
/**
* @description Update in methods that access tables.
*/
trackTableAccess(tableName) {
this.cache.trackTableAccess(tableName);
}
/**
* @description Flush the WAL and any writes.
*/
async flush() {
await this.flushWAL();
await this.flushWrites();
}
/**
* @description Flush only the WAL.
*/
async flushWAL() {
await this.wal.flushWAL();
}
/**
* @description Flush buffered writes to their respective table files using a binary format
*/
async flushWrites() {
if (this.writeBuffer.length === 0) return;
try {
const tableOperations = /* @__PURE__ */ new Map();
const bufferSnapshot = [...this.writeBuffer];
for (const entry of bufferSnapshot) {
const operation = JSON.parse(entry);
if (!tableOperations.has(operation.tableName))
tableOperations.set(operation.tableName, /* @__PURE__ */ new Map());
const tableData = tableOperations.get(operation.tableName);
tableData.set(operation.key, operation.record);
const operationName = "item.written";
const { success, errors } = await this.mikroEvent.emit(operationName, {
operation: operationName,
table: operation.tableName,
key: operation.key,
record: operation.record
});
if (!success) console.error("Error when emitting events:", errors);
}
const writePromises = Array.from(tableOperations.entries()).map(async ([tableName]) => {
const fullTableData = this.getTable(tableName);
const tablePath = join(this.databaseDirectory, tableName);
await writeToDisk(tablePath, fullTableData, this.encryptionKey);
});
await Promise.all(writePromises);
this.writeBuffer = this.writeBuffer.slice(bufferSnapshot.length);
} catch (error) {
console.error(`Failed to flush writes: ${error.message}`);
}
}
/**
* @description Write (flush) a table to disk.
*/
async flushTableToDisk(tableName) {
await this.setActiveTable(tableName);
const tableData = this.getTable(tableName);
if (tableData.size === 0) return;
for (const [key, _] of tableData.entries()) this.addToWriteBuffer(tableName, key);
await this.flushWrites();
}
/**
* @description Evict any tables that are flagged for cleaning.
*/
async evictTablesIfNeeded() {
const tablesToEvict = this.cache.findTablesForEviction(this.data.size);
for (const tableName of tablesToEvict) {
await this.flushTableToDisk(tableName);
this.data.delete(tableName);
}
}
/**
* @description Remove expired items from all tables.
*/
async cleanupExpiredItems() {
for (const [tableName, tableData] of this.data.entries()) {
const expiredItems = this.cache.findExpiredItems(tableData);
for (const [key, item] of expiredItems) {
await this.wal.appendToWAL(tableName, "D", key, null, item.v, item.x);
tableData.delete(key);
const operation = "item.expired";
const { success, errors } = await this.mikroEvent.emit(operation, {
operation,
table: tableName,
key,
record: item
});
if (!success) console.error("Error when emitting events:", errors);
}
}
}
/**
* @description Dump (write) one or more tables to disk in JSON format.
*/
async dump(tableName) {
if (tableName) await this.setActiveTable(tableName);
const table = this.getAll(this.activeTable);
await writeFile3(
`${this.databaseDirectory}/${this.activeTable}_dump.json`,
JSON.stringify(table),
"utf8"
);
}
/**
* @description Returns the WAL instance.
*/
getWAL() {
return this.wal;
}
/**
* @description Returns the Persistence instance.
*/
getPersistence() {
return this.persistence;
}
};
// node_modules/mikrodb/lib/chunk-2IZXMK3O.mjs
import { join as join2 } from "node:path";
var MikroDB = class {
table;
checkpoint;
constructor(options) {
const defaults3 = configDefaults();
const databaseDirectory = options?.databaseDirectory || defaults3.db.databaseDirectory;
const walFileName = options?.walFileName || defaults3.db.walFileName;
const walInterval = options?.walInterval || defaults3.db.walInterval;
const encryptionKey = options?.encryptionKey || defaults3.db.encryptionKey;
const maxWriteOpsBeforeFlush = options?.maxWriteOpsBeforeFlush || defaults3.db.maxWriteOpsBeforeFlush;
const events = options?.events || {};
if (options?.debug) process.env.DEBUG = "true";
if (options?.maxWriteOpsBeforeFlush)
process.env.MAX_WRITE_OPS_BEFORE_FLUSH = maxWriteOpsBeforeFlush.toString();
this.table = new Table(
{
databaseDirectory,
walFileName,
walInterval,
encryptionKey
},
events
);
const wal = this.table.getWAL();
this.table.getWAL().setCheckpointCallback(() => this.checkpoint.checkpoint(true));
this.checkpoint = new Checkpoint({
table: this.table,
wal,
walFile: join2(databaseDirectory, walFileName),
checkpointIntervalMs: walInterval
});
this.checkpoint.start().catch((error) => console.error("Failed to start checkpoint service:", error));
}
/**
* @description Setup and start internal processes.
*/
async start() {
await this.table.start();
await this.checkpoint.start();
}
/**
* @description Get an item from the database.
* @example
* // Get everything in table
* await db.get({ tableName: 'my-table' });
*
* // Get a specific key in the table
* await db.get({ tableName: 'my-table', key: 'my-item' });
*
* // You can use several types of filters
* await db.get({
* tableName: 'my-table',
* options: {
* filter: {
* age:
* {
* operator: 'gt',
* value: 21
* }
* }
* }
* });
*/
async get(operation) {
return await this.table.get(operation);
}
/**
* @description Get the size of a table, if it exists.
*/
async getTableSize(tableName) {
return await this.table.getTableSize(tableName);
}
/**
* @description Write one or more items to the database.
* @example
* await db.write({
* tableName,
* key,
* value: { name: 'John Doe', age: 30 },
* expectedVersion: 3 // Optional
* });
*/
async write(operation, options) {
return await this.table.write(operation, options);
}
/**
* @description Delete an item from the database.
* @example
* await db.delete({ tableName: 'users', key: 'john.doe' });
* await db.delete({ tableName: 'users', key: 'john.doe', expectedVersion: 4 }); // Remove version 4 of this key
*/
async delete(operation) {
const { tableName, key } = operation;
const expectedVersion = operation?.expectedVersion || null;
return await this.table.delete(tableName, key, expectedVersion);
}
/**
* @description Deletes a table.
*/
async deleteTable(tableName) {
return await this.table.deleteTable(tableName);
}
/**
* @description Alias for `flush()`.
*/
async close() {
if (this.checkpoint) this.checkpoint.stop();
if (this.table?.getWAL()) this.table.getWAL().stop();
try {
await this.flush();
} catch (error) {
console.error("Error flushing during close:", error);
}
}
/**
* @description Flushes all pending operations to disk.
* This ensures all Write Ahead Log (WAL) entries and writes are persisted.
*/
async flush() {
await this.table.flush();
}
/**
* @description Flush only the Write Ahead Log (WAL).
*/
async flushWAL() {
await this.table.flushWAL();
}
/**
* @description Dump a single—or if no table name is provided, all—tables to JSON file(s) on disk.
*/
async dump(tableName) {
await this.table.dump(tableName);
}
/**
* @description Manually start a cleanup task to remove expired items.
*/
async cleanupExpiredItems() {
await this.table.cleanupExpiredItems();
}
};
// node_modules/mikroserve/lib/chunk-ZFBBESGU.mjs
var RateLimiter = class {
requests = /* @__PURE__ */ new Map();
limit;
windowMs;
constructor(limit = 100, windowSeconds = 60) {
this.limit = limit;
this.windowMs = windowSeconds * 1e3;
setInterval(() => this.cleanup(), this.windowMs);
}
getLimit() {
return this.limit;
}
isAllowed(ip) {
const now = Date.now();
const key = ip || "unknown";
let entry = this.requests.get(key);
if (!entry || entry.resetTime < now) {
entry = { count: 0, resetTime: now + this.windowMs };
this.requests.set(key, entry);
}
entry.count++;
return entry.count <= this.limit;
}
getRemainingRequests(ip) {
const now = Date.now();
const key = ip || "unknown";
const entry = this.requests.get(key);
if (!entry || entry.resetTime < now) return this.limit;
return Math.max(0, this.limit - entry.count);
}
getResetTime(ip) {
const now = Date.now();
const key = ip || "unknown";
const entry = this.requests.get(key);
if (!entry || entry.resetTime < now) return Math.floor((now + this.windowMs) / 1e3);
return Math.floor(entry.resetTime / 1e3);
}
cleanup() {
const now = Date.now();
for (const [key, entry] of this.requests.entries()) {
if (entry.resetTime < now) this.requests.delete(key);
}
}
};
// node_modules/mikroserve/lib/chunk-GUYBTPZH.mjs
import { URL as URL2 } from "node:url";
var Router = class {
routes = [];
globalMiddlewares = [];
pathPatterns = /* @__PURE__ */ new Map();
/**
* Add a global middleware
*/
use(middleware) {
this.globalMiddlewares.push(middleware);
return this;
}
/**
* Register a route with specified method
*/
register(method, path, handler, middlewares = []) {
this.routes.push({ method, path, handler, middlewares });
this.pathPatterns.set(path, this.createPathPattern(path));
return this;
}
/**
* Register a GET route
*/
get(path, ...handlers) {
const handler = handlers.pop();
return this.register("GET", path, handler, handlers);
}
/**
* Register a POST route
*/
post(path, ...handlers) {
const handler = handlers.pop();
return this.register("POST", path, handler, handlers);
}
/**
* Register a PUT route
*/
put(path, ...handlers) {
const handler = handlers.pop();
return this.register("PUT", path, handler, handlers);
}
/**
* Register a DELETE route
*/
delete(path, ...handlers) {
const handler = handlers.pop();
return this.register("DELETE", path, handler, handlers);
}
/**
* Register a PATCH route
*/
patch(path, ...handlers) {
const handler = handlers.pop();
return this.register("PATCH", path, handler, handlers);
}
/**
* Register an OPTIONS route
*/
options(path, ...handlers) {
const handler = handlers.pop();
return this.register("OPTIONS", path, handler, handlers);
}
/**
* Match a request to a route
*/
match(method, path) {
for (const route of this.routes) {
if (route.method !== method) continue;
const pathPattern = this.pathPatterns.get(route.path);
if (!pathPattern) continue;
const match = pathPattern.pattern.exec(path);
if (!match) continue;
const params = {};
pathPattern.paramNames.forEach((name, index) => {
params[name] = match[index + 1] || "";
});
return { route, params };
}
return null;
}
/**
* Create a regex pattern for path matching
*/
createPathPattern(path) {
const paramNames = [];
const pattern = path.replace(/\/:[^/]+/g, (match) => {
const paramName = match.slice(2);
paramNames.push(paramName);
return "/([^/]+)";
}).replace(/\/$/, "/?");
return {
pattern: new RegExp(`^${pattern}$`),
paramNames
};
}
/**
* Handle a request and find the matching route
*/
async handle(req, res) {
const method = req.method || "GET";
const url = new URL2(req.url || "/", `http://${req.headers.host}`);
const path = url.pathname;
const matched = this.match(method, path);
if (!matched) return null;
const { route, params } = matched;
const query = {};
url.searchParams.forEach((value, key) => {
query[key] = value;
});
const context = {
req,
res,
params,
query,
// @ts-ignore
body: req.body || {},
headers: req.headers,
path,
state: {},
// Add the missing state property
raw: () => res,
binary: (content, contentType = "application/octet-stream", status = 200) => ({
statusCode: status,
body: content,
headers: {
"Content-Type": contentType,
"Content-Length": content.length.toString()
},
isRaw: true
}),
text: (content, status = 200) => ({
statusCode: status,
body: content,
headers: { "Content-Type": "text/plain" }
}),
form: (content, status = 200) => ({
statusCode: status,
body: content,
headers: { "Content-Type": "application/x-www-form-urlencoded" }
}),
json: (content, status = 200) => ({
statusCode: status,
body: content,
headers: { "Content-Type": "application/json" }
}),
html: (content, status = 200) => ({
statusCode: status,
body: content,
headers: { "Content-Type": "text/html" }
}),
redirect: (url2, status = 302) => ({
statusCode: status,
body: null,
headers: { Location: url2 }
}),
status: function(code) {
return {
raw: () => res,
binary: (content, contentType = "application/octet-stream") => ({
statusCode: code,
body: content,
headers: {
"Content-Type": contentType,
"Content-Length": content.length.toString()
},
isRaw: true
}),
text: (content) => ({
statusCode: code,
body: content,
headers: { "Content-Type": "text/plain" }
}),
json: (data) => ({
statusCode: code,
body: data,
headers: { "Content-Type": "application/json" }
}),
html: (content) => ({
statusCode: code,
body: content,
headers: { "Content-Type": "text/html" }
}),
form: (content) => ({
// Make sure form method is included here
statusCode: code,
body: content,
headers: { "Content-Type": "application/x-www-form-urlencoded" }
}),
redirect: (url2, redirectStatus = 302) => ({
statusCode: redirectStatus,
body: null,
headers: { Location: url2 }
}),
status: (updatedCode) => this.status(updatedCode)
};
}
};
const middlewares = [...this.globalMiddlewares, ...route.middlewares];
return this.executeMiddlewareChain(context, middlewares, route.handler);
}
/**
* Execute middleware chain and final handler
*/
async executeMiddlewareChain(context, middlewares, finalHandler) {
let currentIndex = 0;
const next = async () => {
if (currentIndex < middlewares.length) {
const middleware = middlewares[currentIndex++];
return middleware(context, next);
}
return finalHandler(context);
};
return next();
}
};
// node_modules/mikroserve/lib/chunk-JJX5XRNB.mjs
var configDefaults2 = () => {
return {
port: Number(process.env.PORT) || 3e3,
host: process.env.HOST || "0.0.0.0",
useHttps: false,
useHttp2: false,
sslCert: "",
sslKey: "",
sslCa: "",
debug: getTruthyValue2(process.env.DEBUG) || false,
rateLimit: {
enabled: true,
requestsPerMinute: 100
},
allowedDomains: ["*"]
};
};
function getTruthyValue2(value) {
if (value === "true" || value === true) return true;
return false;
}
// node_modules/mikroserve/lib/chunk-YOHL3T54.mjs
var defaults = configDefaults2();
var baseConfig = (options) => ({
configFilePath: "mikroserve.config.json",
args: process.argv,
options: [
{ flag: "--port", path: "port", defaultValue: defaults.port },
{ flag: "--host", path: "host", defaultValue: defaults.host },
{ flag: "--https", path: "useHttps", defaultValue: defaults.useHttps, isFlag: true },
{ flag: "--http2", path: "useHttp2", defaultValue: defaults.useHttp2, isFlag: true },
{ flag: "--cert", path: "sslCert", defaultValue: defaults.sslCert },
{ flag: "--key", path: "sslKey", defaultValue: defaults.sslKey },
{ flag: "--ca", path: "sslCa", defaultValue: defaults.sslCa },
{
flag: "--ratelimit",
path: "rateLimit.enabled",
defaultValue: defaults.rateLimit.enabled,
isFlag: true
},
{
flag: "--rps",
path: "rateLimit.requestsPerMinute",
defaultValue: defaults.rateLimit.requestsPerMinute
},
{
flag: "--allowed",
path: "allowedDomains",
defaultValue: defaults.allowedDomains,
parser: parsers.array
},
{ flag: "--debug", path: "debug", defaultValue: defaults.debug, isFlag: true }
],
config: options
});
// node_modules/mikroserve/lib/chunk-TQN6BEGA.mjs
import { readFileSync as readFileSync3 } from "node:fs";
import http from "node:http";
import http2 from "node:http2";
import https from "node:https";
var MikroServe = class {
config;
rateLimiter;
router;
/**
* @description Creates a new MikroServe instance.
*/
constructor(options) {
const config = new MikroConf(baseConfig(options || {})).get();
if (config.debug) console.log("Using configuration:", config);
this.config = config;
this.router = new Router();
const requestsPerMinute = config.rateLimit.requestsPerMinute || configDefaults2().rateLimit.requestsPerMinute;
this.rateLimiter = new RateLimiter(requestsPerMinute, 60);
if (config.rateLimit.enabled === true) this.use(this.rateLimitMiddleware.bind(this));
}
/**
* @description Register a global middleware.
*/
use(middleware) {
this.router.use(middleware);
return this;
}
/**
* @description Register a GET route.
*/
get(path, ...handlers) {
this.router.get(path, ...handlers);
return this;
}
/**
* @description Register a POST route.
*/
post(path, ...handlers) {
this.router.post(path, ...handlers);
return this;
}
/**
* @description Register a PUT route.
*/
put(path, ...handlers) {
this.router.put(path, ...handlers);
return this;
}
/**
* @description Register a DELETE route.
*/
delete(path, ...handlers) {
this.router.delete(path, ...handlers);
return this;
}
/**
* @description Register a PATCH route.
*/
patch(path, ...handlers) {
this.router.patch(path, ...handlers);
return this;
}
/**
* @description Register an OPTIONS route.
*/
options(path, ...handlers) {
this.router.options(path, ...handlers);
return this;
}
/**
* @description Creates an HTTP/HTTPS server, sets up graceful shutdown, and starts listening.
*/
start() {
const server = this.createServer();
const { port, host } = this.config;
this.setupGracefulShutdown(server);
server.listen(port, host, () => {
const address = server.address();
const protocol = this.config.useHttps || this.config.useHttp2 ? "https" : "http";
console.log(
`MikroServe running at ${protocol}://${address.address !== "::" ? address.address : "localhost"}:${address.port}`
);
});
return server;
}
/**
* @description Creates and configures a server instance without starting it.
*/
createServer() {
const boundRequestHandler = this.requestHandler.bind(this);
if (this.config.useHttp2) {
if (!this.config.sslCert || !this.config.sslKey)
throw new Error("SSL certificate and key paths are required when useHttp2 is true");
try {
const httpsOptions = {
key: readFileSync3(this.config.sslKey),
cert: readFileSync3(this.config.sslCert),
...this.config.sslCa ? { ca: readFileSync3(this.config.sslCa) } : {}
};
return http2.createSecureServer(httpsOptions, boundRequestHandler);
} catch (error) {
if (error.message.includes("key values mismatch"))
throw new Error(`SSL certificate and key do not match: ${error.message}`);
throw error;
}
} else if (this.config.useHttps) {
if (!this.config.sslCert || !this.config.sslKey)
throw new Error("SSL certificate and key paths are required when useHttps is true");
try {
const httpsOptions = {
key: readFileSync3(this.config.sslKey),
cert: readFileSync3(this.config.sslCert),
...this.config.sslCa ? { ca: readFileSync3(this.config.sslCa) } : {}
};
return https.createServer(httpsOptions, boundRequestHandler);
} catch (error) {
if (error.message.includes("key values mismatch"))
throw new Error(`SSL certificate and key do not match: ${error.message}`);
throw error;
}
}
return http.createServer(boundRequestHandler);
}
/**
* @description Rate limiting middleware.
*/
async rateLimitMiddleware(context, next) {
const ip = context.req.socket.remoteAddress || "unknown";
context.res.setHeader("X-RateLimit-Limit", this.rateLimiter.getLimit().toString());
context.res.setHeader(
"X-RateLimit-Remaining",
this.rateLimiter.getRemainingRequests(ip).toString()
);
context.res.setHeader("X-RateLimit-Reset", this.rateLimiter.getResetTime(ip).toString());
if (!this.rateLimiter.isAllowed(ip)) {
return {
statusCode: 429,
body: {
error: "Too Many Requests",
message: "Rate limit exceeded, please try again later"
},
headers: { "Content-Type": "application/json" }
};
}
return next();
}
/**
* @description Request handler for HTTP and HTTPS servers.
*/
async requestHandler(req, res) {
const start2 = Date.now();
const method = req.method || "UNKNOWN";
const url = req.url || "/unknown";
const isDebug = this.config.debug;
try {
this.setCorsHeaders(res, req);
this.setSecurityHeaders(res, this.config.useHttps);
if (isDebug) console.log(`${method} ${url}`);
if (req.method === "OPTIONS") {
if (res instanceof http.ServerResponse) {
res.statusCode = 204;
res.end();
} else {
const h2Res = res;
h2Res.writeHead(204);
h2Res.end();
}
return;
}
try {
req.body = await this.parseBody(req);
} catch (error) {
if (isDebug) console.error("Body parsing error:", error.message);
return this.respond(res, {
statusCode: 400,
body: {
error: "Bad Request",
message: error.message
}
});
}
const result = await this.router.handle(req, res);
if (result) {
if (result._handled) return;
return this.respond(res, result);
}
return this.respond(res, {
statusCode: 404,
body: {
error: "Not Found",
message: "The requested endpoint does not exist"
}
});
} catch (error) {
console.error("Server error:", error);
return this.respond(res, {
statusCode: 500,
body: {
error: "Internal Server Error",
message: isDebug ? error.message : "An unexpected error occurred"
}
});
} finally {
if (isDebug) this.logDuration(start2, method, url);
}
}
/**
* @description Writes out a clean log to represent the duration of the request.
*/
logDuration(start2, method, url) {
const duration = Date.now() - start2;
console.log(`${method} ${url} completed in ${duration}ms`);
}
/**
* @description Parses the request body based on content type.
*/
async parseBody(req) {
return new Promise((resolve, reject) => {
const bodyChunks = [];
let bodySize = 0;
const MAX_BODY_SIZE = 1024 * 1024;
let rejected = false;
const isDebug = this.config.debug;
const contentType = req.headers["content-type"] || "";
if (isDebug) {
console.log("Content-Type:", contentType);
}
req.on("data", (chunk) => {
bodySize += chunk.length;
if (isDebug) console.log(`Received chunk: ${chunk.length} bytes, total size: ${bodySize}`);
if (bodySize > MAX_BODY_SIZE && !rejected) {
rejected = true;
if (isDebug) console.log(`Body size exceeded limit: ${bodySize} > ${MAX_BODY_SIZE}`);
reject(new Error("Request body too large"));
return;
}
if (!rejected) bodyChunks.push(chunk);
});
req.on("end", () => {
if (rejected) return;
if (isDebug) console.log(`Request body complete: ${bodySize} bytes`);
try {
if (bodyChunks.length > 0) {
const bodyString = Buffer.concat(bodyChunks).toString("utf8");
if (contentType.includes("application/json")) {
try {
resolve(JSON.parse(bodyString));
} catch (error) {
reject(new Error(`Invalid JSON in request body: ${error.message}`));
}
} else if (contentType.includes("application/x-www-form-urlencoded")) {
const formData = {};
new URLSearchParams(bodyString).forEach((value, key) => {
formData[key] = value;
});
resolve(formData);
} else {
resolve(bodyString);
}
} else {
resolve({});
}
} catch (error) {
reject(new Error(`Invalid request body: ${error}`));
}
});
req.on("error", (error) => {
if (!rejected) reject(new Error(`Error reading request body: ${error.message}`));
});
});
}
/**
* @description CORS middleware.
*/
setCorsHeaders(res, req) {
const origin = req.headers.origin;
const { allowedDomains = ["*"] } = this.config;
if (!origin || allowedDomains.length === 0) res.setHeader("Access-Control-Allow-Origin", "*");
else if (allowedDomains.includes("*")) res.setHeader("Access-Control-Allow-Origin", "*");
else if (allowedDomains.includes(origin)) {
res.setHeader("Access-Control-Allow-Origin", origin);
res.setHeader("Vary", "Origin");
}
res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS");
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
res.setHeader("Access-Control-Max-Age", "86400");
}
/**
* @description Set security headers.
*/
setSecurityHeaders(res, isHttps = false) {
const securityHeaders = {
"X-Content-Type-Options": "nosniff",
"X-Frame-Options": "DENY",
"Content-Security-Policy": "default-src 'self'; script-src 'self'; object-src 'none'",
"X-XSS-Protection": "1; mode=block"
};
if (isHttps || this.config.useHttp2)
securityHeaders["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains";
if (res instanceof http.ServerResponse) {
Object.entries(securityHeaders).forEach(([name, value]) => {
res.setHeader(name, value);
});
} else {
const h2Res = res;
Object.entries(securityHeaders).forEach(([name, value]) => {
h2Res.setHeader(name, value);
});
}
}
/**
* @description Sends a response with appropriate headers.
*/
respond(res, response) {
const headers = {
...response.headers || {}
};
const hasWriteHead = (res2) => {
return typeof res2.writeHead === "function" && typeof res2.end === "function";
};
if (hasWriteHead(res)) {
res.writeHead(response.statusCode, headers);
if (response.body === null || response.body === void 0) res.end();
else if (response.isRaw) res.end(response.body);
else if (typeof response.body === "string") res.end(response.body);
else res.end(JSON.stringify(response.body));
} else {
console.warn("Unexpected response object type without writeHead/end methods");
res.writeHead?.(response.statusCode, headers);
if (response.body === null || response.body === void 0) res.end?.();
else if (response.isRaw) res.end?.(response.body);
else if (typeof response.body === "string") res.end?.(response.body);
else res.end?.(JSON.stringify(response.body));
}
}
/**
* @description Sets up graceful shutdown handlers for a server.
*/
setupGracefulShutdown(server) {
const shutdown = (error) => {
console.log("Shutting down MikroServe server...");
if (error) console.error("Error:", error);
server.close(() => {
console.log("Server closed successfully");
setImmediate(() => process.exit(error ? 1 : 0));
});
};
process.on("SIGINT", () => shutdown());
process.on("SIGTERM", () => shutdown());
process.on("uncaughtException", shutdown);
process.on("unhandledRejection", shutdown);
}
};
// node_modules/mikrodb/lib/chunk-Q2EVQW6J.mjs
async function startServer(config) {
const db = new MikroDB({ ...config.db, events: config?.events });
await db.start();
const server = new MikroServe(config.server);
server.get("/table", async (c) => {
const body = c.req.body;
if (!body.tableName) return c.json({ statusCode: 400, body: "tableName is required" });
const result = await db.getTableSize(body.tableName);
if (!result) c.json({ statusCode: 404, body: null });
return c.json({ statusCode: 200, body: result });
});
server.post("/get", async (c) => {
const body = c.req.body;
if (!body.tableName) return c.json({ statusCode: 400, body: "tableName is required" });
const operation = {
tableName: body.tableName,
key: body.key
};
const options = createGetOptions(body?.options);
if (options) operation.options = options;
const result = await db.get(operation);
if (!result) c.json({ statusCode: 404, body: null });
return c.json({ statusCode: 200, body: result });
});
server.post("/write", async (c) => {
const body = c.req.body;
if (!body.tableName || body.value === void 0)
return c.json({ statusCode: 400, body: "tableName and value are required" });
const operation = {
tableName: body.tableName,
key: body.key,
value: body.value,
expectedVersion: body.expectedVersion,
expiration: body.expiration
};
const options = {
concurrencyLimit: body.concurrencyLimit,
flushImmediately: body.flushImmediately
};
const result = await db.write(operation, options);
return c.json({ statusCode: 200, body: result });
});
server.delete("/delete", async (c) => {
const query = c.params;
if (!query.tableName || !query.key)
return c.json({ statusCode: 400, body: "tableName and key are required" });
const operation = {
tableName: query.tableName,
key: query.key
};
const result = await db.delete(operation);
return c.json({ statusCode: 200, body: result });
});
server.start();
return server;
}
// node_modules/mikrodb/lib/index.mjs
async function main() {
const args = process.argv;
const isRunFromCommandLine = args[1]?.includes("node_modules/.bin/mikrodb");
const force = (process.argv[2] || "") === "--force";
if (isRunFromCommandLine || force) {
console.log("\u{1F5C2}\uFE0F Welcome to MikroDB! \u2728");
try {
const defaults3 = configDefaults();
const config = new MikroConf({
configFilePath: "mikrodb.config.json",
args: process.argv,
options: [
// DB settings
{ flag: "--db", path: "db.dbName", defaultValue: defaults3.db.dbName },
{
flag: "--dir",
path: "db.databaseDirectory",
defaultValue: defaults3.db.databaseDirectory
},
{ flag: "--wal", path: "db.walFileName", defaultValue: defaults3.db.walFileName },
{ flag: "--interval", path: "db.walInterval", defaultValue: defaults3.db.walInterval },
{
flag: "--encryptionKey",
path: "db.encryptionKey",
defaultValue: defaults3.db.encryptionKey
},
{
flag: "--maxWrites",
path: "db.maxWriteOpsBeforeFlush",
defaultValue: defaults3.db.maxWriteOpsBeforeFlush
},
{
flag: "--debug",
path: "db.debug",
isFlag: true,
defaultValue: defaults3.db.debug
},
// Event settings
{
path: "events",
defaultValue: defaults3.events
},
// Server settings
{
flag: "--port",
path: "server.port",
defaultValue: defaults3.server.port
},
{
flag: "--host",
path: "server.host",
defaultValue: defaults3.server.host
},
{
flag: "--https",
path: "server.useHttps",
isFlag: true,
defaultValue: defaults3.server.useHttps
},
{
flag: "--http2",
path: "server.useHttp2",
isFlag: true,
defaultValue: defaults3.server.useHttp2
},
{
flag: "--cert",
path: "server.sslCert",
defaultValue: defaults3.server.sslCert
},
{
flag: "--key",
path: "server.sslKey",
defaultValue: defaults3.server.sslKey
},
{
flag: "--ca",
path: "server.sslCa",
defaultValue: defaults3.server.sslCa
},
{
flag: "--debug",
path: "server.debug",
isFlag: true,
defaultValue: defaults3.server.debug
}
]
}).get();
startServer(config);
} catch (error) {
console.error(error);
}
}
}
main();
// src/MikroChat.ts
import { EventEmitter as EventEmitter3 } from "node:events";
// node_modules/mikroid/lib/chunk-MM76MZMU.mjs
import { getRandomValues } from "node:crypto";
var MikroID = class {
options;
defaultLength = 16;
defaultOnlyLowerCase = false;
defaultStyle = "extended";
defaultUrlSafe = true;
constructor(options) {
this.options = {};
if (options) {
for (const [name, config] of Object.entries(options)) {
this.options[name] = this.generateConfig(config);
}
}
}
/**
* @description Adds a new ID configuration to the options collection.
* This allows for reusable ID configurations with custom settings.
*/
add(config) {
if (!config?.name) throw new Error("Missing name for the ID configuration");
const cleanConfig = this.generateConfig(config);
this.options[config.name] = cleanConfig;
}
/**
* @description Removes an existing ID configuration from the options collection.
* Use this to clean up configurations that are no longer needed.
*/
remove(config) {
if (!config?.name) throw new Error("Missing name for the ID configuration");
delete this.options[config.name];
}
/**
* @description Creates a new ID.
*
* You may set an explicit `length` for the ID, or use the defaults provided.
*
* Optionally, `style` may be passed to specify the ID style (default is 'extended').
* - 'extended': includes safe special characters for more unique combinations
* - 'alphanumeric': uses only letters and numbers
* - 'hex': uses only 0-9 and a-f (A-F)
*
* Optionally, `onlyLowerCase` may be passed which for alphanumeric
* IDs will return a string that only uses numbers and lower-case letters.
*
* Optionally, `urlSafe` may be passed which determines whether to use
* URL-safe characters only.
*
* @example
* const mikroid = new MikroID();
* const id = mikroid.create(16, 'alphanumeric'); // 16-character alphanumeric ID
* const shortId = mikroid.create(8); // 8-character extended ID
* const longId = mikroid.create(12); // 12-character extended ID
*/
create(length, style, onlyLowerCase, urlSafe) {
const config = this.generateConfig({
length,
style,
onlyLowerCase,
urlSafe
});
return this.generateId(config);
}
/**
* @description Creates a new ID using your custom ID configuration.
*
* @example
* const id = new MikroID().custom('my-custom-id'); // ID created using 'my-custom-id' configuration
*/
custom(name) {
if (this.options[name]) return this.generateId(this.options[name]);
throw new Error(`No configuration found with name: ${name}`);
}
/**
* @description Generate a cleaned, ready-to-use configuration.
*/
generateConfig(options) {
return {
name: options?.name || "",
length: options?.length || this.defaultLength,
onlyLowerCase: options?.onlyLowerCase ?? this.defaultOnlyLowerCase,
style: options?.style || this.defaultStyle,
urlSafe: options?.urlSafe ?? this.defaultUrlSafe
};
}
/**
* @description Gets the appropriate character set based on style and options
*/
getCharacterSet(style, onlyLowerCase, urlSafe) {
if (style === "hex")
return onlyLowerCase ? "0123456789abcdef" : "0123456789ABCDEFabcdef";
if (style === "alphanumeric")
return onlyLowerCase ? "abcdefghijklmnopqrstuvwxyz0123456789" : "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
if (style === "extended") {
if (urlSafe)
return onlyLowerCase ? "abcdefghijklmnopqrstuvwxyz0123456789-._~" : "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~";
return onlyLowerCase ? "abcdefghijklmnopqrstuvwxyz0123456789-._~!$()*+,;=:" : "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~!$()*+,;=:";
}
throw new Error(
`Unknown ID style "${style} provided. Must be one of "extended" (default), "alphanumeric", or "hex".`
);
}
/**
* @description Generates an ID using the specified configuration.
* Uses cryptographically secure random generation.
*/
generateId(options) {
const { length, onlyLowerCase, style, urlSafe } = options;
if (length < 0 || length === 0)
throw new Error("ID length cannot be negative");
const characters = this.getCharacterSet(style, onlyLowerCase, urlSafe);
const mask = (2 << Math.log(characters.length - 1) / Math.LN2) - 1;
const step = Math.ceil(1.6 * mask * length / characters.length);
let id = "";
while (id.length < length) {
const bytes = new Uint8Array(step);
getRandomValues(bytes);
for (let i = 0; i < step; i++) {
const byte = bytes[i] & mask;
if (byte < characters.length) {
id += characters[byte];
if (id.length === length) break;
}
}
}
return id;
}
};
// src/providers/GeneralStorageProvider.ts
var GeneralStorageProvider = class {
//////////////////
// User methods //
//////////////////
async getUserById(id) {
return this.db.get(`user:${id}`);
}
async getUserByUsername(userName) {
const users = await this.db.list("user:");
return users.find((user) => user.userName === userName) || null;
}
async createUser(user) {
await this.db.set(`user:${user.id}`, user);
}
async deleteUser(id) {
await this.db.delete(`user:${id}`);
}
async listUsers() {
return this.db.list("user:");
}
async getUserByEmail(email) {
const users = await this.db.list("user:");
return users.find((user) => user.email === email) || null;
}
/////////////////////
// Channel methods //
/////////////////////
async getChannelById(id) {
return this.db.get(`channel:${id}`);
}
async getChannelByName(name) {
const channels = await this.db.list("channel:");
return channels.find((channel) => channel.name === name) || null;
}
async createChannel(channel) {
await this.db.set(`channel:${channel.id}`, channel);
}
async updateChannel(channel) {
await this.db.set(`channel:${channel.id}`, channel);
}
async deleteChannel(id) {
await this.db.delete(`channel:${id}`);
}
async listChannels() {
return this.db.list("channel:");
}
/////////////////////
// Message methods //
/////////////////////
async getMessageById(id) {
return await this.db.get(`message:${id}`);
}
async listMessagesByChannel(channelId) {
const messages = await this.db.list("message:");
return messages.filter((message) => message.channelId === channelId).sort((a, b) => a.createdAt - b.createdAt);
}
async createMessage(message) {
await this.db.set(`message:${message.id}`, message);
}
async updateMessage(message) {
await this.db.set(`message:${message.id}`, message);
}
async deleteMessage(id) {
await this.db.delete(`message:${id}`);
}
//////////////////////
// Reaction methods //
//////////////////////
async addReaction(messageId, userId, reaction) {
const message = await this.getMessageById(messageId);
if (!message) return null;
if (!message.reactions) message.reactions = {};
const userReactions = message.reactions[userId] || [];
if (!userReactions.includes(reaction)) {
message.reactions[userId] = [...userReactions, reaction];
await this.updateMessage(message);
}
return message;
}
async removeReaction(messageId, userId, reaction) {
const message = await this.getMessageById(messageId);
if (!message) return null;
if (!message.reactions || !message.reactions[userId]) return message;
message.reactions[userId] = message.reactions[userId].filter(
(r) => r !== reaction
);
await this.updateMessage(message);
return message;
}
////////////////////
// Server methods //
////////////////////
async getServerSettings() {
return this.db.get("server:settings");
}
async updateServerSettings(settings) {
await this.db.set("server:settings", settings);
}
};
// src/providers/InMemoryProvider.ts
var InMemoryProvider = class extends GeneralStorageProvider {
db;
constructor() {
super();
this.db = new MockDatabase();
}
};
var MockDatabase = class {
data = {};
async get(key) {
return this.data[key] || null;
}
async set(key, value) {
this.data[key] = value;
}
async delete(key) {
delete this.data[key];
}
async list(prefix) {
const result = [];
for (const key in this.data) {
if (key.startsWith(prefix)) result.push(this.data[key]);
}
return result;
}
};
// src/config/configDefaults.ts
var idName = "mikrochat_id";
var idConfig = {
name: idName,
length: 12,
urlSafe: true
};
var configDefaults3 = () => {
const debug = getTruthyValue3(process.env.DEBUG) || false;
return {
devMode: false,
auth: {
jwtSecret: process.env.AUTH_JWT_SECRET || "your-jwt-secret",
magicLinkExpirySeconds: 15 * 60,
// 15 minutes
jwtExpirySeconds: 15 * 60,
// 15 minutes
refreshTokenExpirySeconds: 7 * 24 * 60 * 60,
// 7 days
maxActiveSessions: 3,
appUrl: process.env.APP_URL || "http://127.0.0.1:3000",
isInviteRequired: true,
debug
},
email: {
emailSubject: "Your Secure Login Link",
user: process.env.EMAIL_USER || "",
host: process.env.EMAIL_HOST || "",
password: process.env.EMAIL_PASSWORD || "",
port: 465,
secure: true,
maxRetries: 2,
debug
},
storage: {
databaseDirectory: "mikrochat_db",
encryptionKey: process.env.STORAGE_KEY || "",
debug
},
server: {
port: Number(process.env.PORT) || 3e3,
host: process.env.HOST || "localhost",
useHttps: false,
useHttp2: false,
sslCert: "",
sslKey: "",
sslCa: "",
rateLimit: {
enabled: true,
requestsPerMinute: 100
},
allowedDomains: ["http://127.0.0.1:8080"],
debug
},
chat: {
initialUser: {
id: process.env.INITIAL_USER_ID || new MikroID().add(idConfig),
userName: process.env.INITIAL_USER_NAME || "",
email: process.env.INITIAL_USER_EMAIL || ""
},
messageRetentionDays: 30,
maxMessagesPerChannel: 100
}
};
};
function getTruthyValue3(value) {
if (value === "true" || value === true) return true;
return false;
}
// src/MikroChat.ts
var MikroChat = class {
config;
db;
id;
eventEmitter;
generalChannelName = "General";
constructor(config, db) {
this.config = config;
this.db = db || new InMemoryProvider();
this.id = new MikroID();
this.eventEmitter = new EventEmitter3();
this.eventEmitter.setMaxListeners(0);
this.initialize();
}
/**
* @description Initialize the server with a general channel
* and everything else needed to get started.
*/
async initialize() {
const id = this.config.initialUser.id;
const userName = this.config.initialUser.userName;
const email = this.config.initialUser.email;
if (!userName || !email)
throw new Error(
'Missing required data to start a new MikroChat server. Required arguments are "initialUser.userName" and "initialUser.email".'
);
this.id.add(idConfig);
const adminUser = await this.getUserByEmail(email);
if (!adminUser) {
await this.createUser({
id: id || this.id.custom(idName),
userName: userName || email.split("@")[0],
email,
isAdmin: true,
createdAt: Date.now()
});
}
const generalChannel = await this.db.getChannelByName(
this.generalChannelName
);
if (!generalChannel) {
await this.db.createChannel({
id: this.id.custom(idName),
name: this.generalChannelName,
createdAt: Date.now(),
createdBy: this.config.initialUser.id
});
}
this.scheduleMessageCleanup();
}
/**
* @description Run a job to clean up messages that are
* out of date.
*/
scheduleMessageCleanup() {
const runEveryNrMinutes = 60;
const cleanupInterval = 1e3 * 60 * runEveryNrMinutes;
setInterval(async () => {
const channels = await this.db.listChannels();
const millisecondsPerDay = 24 * 60 * 60 * 1e3;
const cutoffTimestamp = Date.now() - this.config.messageRetentionDays * millisecondsPerDay;
for (const channel of channels) {
const messages = await this.db.listMessagesByChannel(channel.id);
for (const message of messages) {
if (message.createdAt < cutoffTimestamp) {
await this.db.deleteMessage(message.id);
this.emitEvent({
type: "DELETE_MESSAGE",
payload: { id: message.id, channelId: channel.id }
});
}
}
}
}, cleanupInterval);
}
////////////////////////////
// Database proxy methods //
////////////////////////////
async getServerSettings() {
return await this.db.getServerSettings();
}
async updateServerSettings(settings) {
return await this.db.updateServerSettings(settings);
}
async getMessageById(id) {
return await this.db.getMessageById(id);
}
async listUsers() {
return await this.db.listUsers();
}
async getUserById(id) {
return await this.db.getUserById(id);
}
async getUserByEmail(email) {
return await this.db.getUserByEmail(email);
}
async createUser(user) {
return await this.db.createUser(user);
}
async deleteUser(id) {
await this.db.deleteUser(id);
}
/////////////////////
// User management //
/////////////////////
/**
* @description Adds a new user to the server.
*
* The `force` option is used when creating users out-of-context
* of the web application, such as directly on the server.
*/
async addUser(email, addedBy = "", isAdmin = false, force = false) {
const adminUser = await this.getUserById(addedBy);
if (!force && !adminUser) throw new Error("User not found");
if (isAdmin && !adminUser?.isAdmin)
throw new Error("Only administrators can add admin users");
const existingUser = await this.getUserByEmail(email);
if (existingUser) throw new Error("User with this email already exists");
const id = this.id.custom(idName);
const user = {
id,
userName: email.split("@")[0],
email,
isAdmin,
createdAt: Date.now(),
addedBy: addedBy ? addedBy : id
// Will be own ID when a user is created without prior invitation ("self-invite")
};
await this.createUser(user);
this.emitEvent({
type: "NEW_USER",
payload: { id: user.id, userName: user.userName, email: user.email }
});
return user;
}
/**
* @description Removes a user from the server.
*/
async removeUser(userId, requestedBy) {
const user = await this.getUserById(userId);
if (!user) throw new Error("User not found");
const requester = await this.getUserById(requestedBy);
if (!requester) throw new Error("Requester not found");
if (!requester.isAdmin)
throw new Error("Only administrators can remove users");
if (user.isAdmin) {
const admins = (await this.listUsers()).filter((u) => u.isAdmin);
if (admins.length <= 1)
throw new Error("Cannot remove the last administrator");
}
await this.deleteUser(userId);
this.emitEvent({
type: "REMOVE_USER",
payload: { id: userId, email: user.email }
});
}
/**
* @description Handles a user exiting the server (self-removal).
*/
async exitUser(userId) {
const user = await this.getUserById(userId);
if (!user) throw new Error("User not found");
await this.deleteUser(userId);
this.emitEvent({
type: "USER_EXIT",
payload: { id: userId, userName: user.userName }
});
}
/////////////////////
// Channel methods //
/////////////////////
/**
* @description Create a new channel on the server.
*/
async createChannel(name, createdBy) {
const existingChannel = await this.db.getChannelByName(name);
if (existingChannel)
throw new Error(`Channel with name "${name}" already exists`);
const channel = {
id: this.id.custom(idName),
name,
createdAt: Date.now(),
createdBy
};
await this.db.createChannel(channel);
this.emitEvent({
type: "NEW_CHANNEL",
payload: channel
});
return channel;
}
/**
* @description Update a channel on the server.
*/
async updateChannel(id, name, userId) {
const channel = await this.db.getChannelById(id);
if (!channel) throw new Error("Channel not found");
const user = await this.getUserById(userId);
if (!user) throw new Error("User not found");
if (channel.createdBy !== userId && !user.isAdmin)
throw new Error("You can only edit channels you created");
const existingChannel = await this.db.getChannelById(id);
if (existingChannel && existingChannel?.name === this.generalChannelName)
throw new Error(
`The ${this.generalChannelName} channel cannot be renamed`
);
if (existingChannel && existingChannel?.id !== id)
throw new Error(`Channel with name "${name}" already exists`);
channel.name = name;
channel.updatedAt = Date.now();
await this.db.updateChannel(channel);
this.emitEvent({
type: "UPDATE_CHANNEL",
payload: channel
});
return channel;
}
/**
* @description Delete a channel on the server.
*/
async deleteChannel(id, userId) {
const channel = await this.db.getChannelById(id);
if (!channel) throw new Error("Channel not found");
const user = await this.getUserById(userId);
if (!user) throw new Error("User not found");
if (channel.createdBy !== userId && !user.isAdmin)
throw new Error("You can only delete channels you created");
if (channel.name.toLowerCase() === this.generalChannelName.toLowerCase())
throw new Error("The General channel cannot be deleted");
const messages = await this.db.listMessagesByChannel(id);
for (const message of messages) {
await this.db.deleteMessage(message.id);
}
await this.db.deleteChannel(id);
this.emitEvent({
type: "DELETE_CHANNEL",
payload: { id, name: channel.name }
});
}
/**
* @description List all channels on the server.
*/
async listChannels() {
return await this.db.listChannels();
}
/////////////////////
// Message methods //
/////////////////////
/**
* @description Create a new message.
*/
async createMessage(content, authorId, channelId, images = []) {
const user = await this.getUserById(authorId);
if (!user) throw new Error("Author not found");
const channel = await this.db.getChannelById(channelId);
if (!channel) throw new Error("Channel not found");
const now = Date.now();
const message = {
id: this.id.custom(idName),
author: {
id: authorId,
userName: user.userName
},
images,
content,
channelId,
createdAt: now,
updatedAt: now,
reactions: {}
};
await this.db.createMessage(message);
const messages = await this.db.listMessagesByChannel(channelId);
if (messages.length > this.config.maxMessagesPerChannel) {
const oldestMessage = messages[0];
await this.db.deleteMessage(oldestMessage.id);
this.emitEvent({
type: "DELETE_MESSAGE",
payload: { id: oldestMessage.id, channelId }
});
}
this.emitEvent({
type: "NEW_MESSAGE",
payload: message
});
return message;
}
/**
* @description Update an existing message.
*/
async updateMessage(id, userId, content, images) {
const message = await this.getMessageById(id);
if (!message) throw new Error("Message not found");
if (message.author.id !== userId)
throw new Error("You can only edit your own messages");
let removedImages = [];
if (content) message.content = content;
if (images) {
const currentImages = message.images || [];
removedImages = currentImages.filter((img) => !images.includes(img));
message.images = images;
}
message.updatedAt = Date.now();
await this.db.updateMessage(message);
this.emitEvent({
type: "UPDATE_MESSAGE",
payload: message
});
return { message, removedImages };
}
/**
* @description Delete an existing message.
*/
async deleteMessage(id, userId) {
const message = await this.getMessageById(id);
if (!message) throw new Error("Message not found");
const user = await this.getUserById(userId);
if (!user) throw new Error("User not found");
if (message.author.id !== userId && !user.isAdmin)
throw new Error("You can only delete your own messages");
await this.db.deleteMessage(id);
this.emitEvent({
type: "DELETE_MESSAGE",
payload: { id, channelId: message.channelId }
});
}
/**
* @description Get all message in a given channel.
*/
async getMessagesByChannel(channelId) {
return await this.db.listMessagesByChannel(channelId);
}
//////////////////////
// Reaction methods //
//////////////////////
/**
* @description Add a reaction to a message.
*/
async addReaction(messageId, userId, reaction) {
const user = await this.getUserById(userId);
if (!user) throw new Error("User not found");
const message = await this.getMessageById(messageId);
if (!message) throw new Error("Message not found");
const updatedMessage = await this.db.addReaction(
messageId,
userId,
reaction
);
if (!updatedMessage) throw new Error("Failed to add reaction");
this.emitEvent({
type: "NEW_REACTION",
payload: { messageId, userId, reaction }
});
return updatedMessage;
}
/**
* @description Remove a reaction from a message.
*/
async removeReaction(messageId, userId, reaction) {
const user = await this.getUserById(userId);
if (!user) throw new Error("User not found");
const message = await this.getMessageById(messageId);
if (!message) throw new Error("Message not found");
const updatedMessage = await this.db.removeReaction(
messageId,
userId,
reaction
);
if (!updatedMessage) throw new Error("Failed to remove reaction");
this.emitEvent({
type: "DELETE_REACTION",
payload: { messageId, userId, reaction }
});
return updatedMessage;
}
////////////////////
// Event handling //
////////////////////
/**
* @description Emit a Server Sent Event.
* @emits
*/
emitEvent(event) {
console.log(`Emitting event ${event.type}`, event.payload);
this.eventEmitter.emit("sse", event);
}
/**
* @description Subscribe to Server Sent Events.
* @subscribes
*/
subscribeToEvents(callback) {
const listener = (event) => callback(event);
this.eventEmitter.on("sse", listener);
return () => this.eventEmitter.off("sse", listener);
}
};
// src/Server.ts
import { randomBytes as randomBytes2 } from "node:crypto";
import {
existsSync as existsSync6,
mkdirSync as mkdirSync2,
readFileSync as readFileSync4,
unlinkSync,
writeFileSync as writeFileSync2
} from "node:fs";
import { join as join3 } from "node:path";
var MAX_IMAGE_SIZE_IN_MB = 2;
var VALID_FILE_FORMATS = ["jpg", "jpeg", "png", "webp", "svg"];
var MAX_CONNECTIONS_PER_USER = 3;
var CONNECTION_TIMEOUT_MS = 60 * 1e3;
var activeConnections = /* @__PURE__ */ new Map();
var connectionTimeouts = /* @__PURE__ */ new Map();
async function startServer2(settings) {
const { config, auth, chat, devMode, isInviteRequired } = settings;
const server = new MikroServe(config);
console.log("Dev mode?", devMode);
if (devMode) {
server.post("/auth/dev-login", async (c) => {
if (!c.body.email) return c.json({ error: "Email is required" }, 400);
const { email } = c.body;
let user;
if (isInviteRequired) {
user = await chat.getUserByEmail(email);
if (!user)
return c.json({ success: false, message: "Unauthorized" }, 401);
} else {
user = await chat.addUser(email, email, false, true);
}
const token = auth.generateJsonWebToken({
id: user.id,
username: user.userName,
email: user.email,
role: user.isAdmin ? "admin" : "user"
});
return c.json({ user, token }, 200);
});
}
server.post("/auth/login", async (c) => {
if (!c.body.email) return c.json({ error: "Email is required" }, 400);
const { email } = c.body;
console.log(111);
let message = "If a matching account was found, a magic link has been sent.";
console.log(222);
const user = await chat.getUserByEmail(email);
console.log("user", user);
console.log(333);
if (user) {
const result = await auth.createMagicLink({
email
});
console.log("result", result);
console.log(444);
if (!result) return c.json({ error: "Failed to create magic link" }, 400);
console.log(555);
message = result.message;
}
console.log(666);
return c.json(
{
success: true,
message
},
200
);
});
server.post("/auth/logout", authenticate, async (c) => {
const body = c.body;
const refreshToken = body.refreshToken;
if (!refreshToken) return c.json({ error: "Missing refresh token" }, 400);
const result = await auth.logout(refreshToken);
return c.json(result, 200);
});
server.get("/auth/me", authenticate, async (c) => {
return c.json({ user: c.state.user }, 200);
});
server.post("/auth/verify", async (c) => {
const body = c.body;
const authHeader = c.headers.authorization || "";
const token = authHeader.split(" ")[1];
if (!token) return c.json({ error: "Token is required" }, 400);
let result;
try {
result = await auth.verifyToken({
email: body.email,
token
});
if (!result) return c.json({ error: "Invalid token" }, 400);
} catch (error) {
return c.json({ error: "Invalid token" }, 400);
}
return c.json(result, 200);
});
server.post("/auth/refresh", async (c) => {
const body = c.body;
const token = body.refreshToken;
const result = await auth.refreshAccessToken(token);
return c.json(result, 200);
});
server.get("/auth/sessions", authenticate, async (c) => {
const authHeader = c.headers.authorization;
if (!authHeader || !authHeader.startsWith("Bearer "))
return c.json(null, 401);
const body = c.body;
const token = authHeader.split(" ")[1];
const payload = auth.verify(token);
const user = { email: payload.sub };
const result = await auth.getSessions({ body, user });
return c.json(result, 200);
});
server.delete("/auth/sessions", authenticate, async (c) => {
const authHeader = c.headers.authorization;
if (!authHeader || !authHeader.startsWith("Bearer "))
return c.json(null, 401);
const body = c.body;
const token = authHeader.split(" ")[1];
const payload = auth.verify(token);
const user = { email: payload.sub };
const result = await auth.revokeSessions({ body, user });
return c.json(result, 200);
});
server.post("/users/add", authenticate, async (c) => {
const user = c.state.user;
if (!user) return c.json({ error: "Unauthorized" }, 401);
const { email, role } = c.body;
if (!email) return c.json({ error: "Email is required" }, 400);
try {
if (role === "admin" && !user.isAdmin)
return c.json(
{ error: "Only administrators can add admin users" },
403
);
const existingUser = await chat.getUserByEmail(email);
if (existingUser)
return c.json({ success: false, message: "User already exists" });
const userId = await chat.addUser(email, user.id, role === "admin");
return c.json({ success: true, userId }, 200);
} catch (error) {
return c.json({ error: error.message }, 400);
}
});
server.get("/users", authenticate, async (c) => {
const user = c.state.user;
if (!user) return c.json({ error: "Unauthorized" }, 401);
try {
const users = await chat.listUsers();
return c.json({ users }, 200);
} catch (error) {
return c.json({ error: error.message }, 400);
}
});
server.delete("/users/:id", authenticate, async (c) => {
const user = c.state.user;
if (!user) return c.json({ error: "Unauthorized" }, 401);
const userId = c.params.id;
if (userId === user.id)
return c.json({ error: "You cannot remove your own account" }, 400);
try {
await chat.removeUser(userId, user.id);
return c.json({ success: true }, 200);
} catch (error) {
return c.json({ error: error.message }, 400);
}
});
server.post("/users/exit", authenticate, async (c) => {
const user = c.state.user;
if (!user) return c.json({ error: "Unauthorized" }, 401);
try {
if (user.isAdmin) {
const admins = (await chat.listUsers()).filter((u) => u.isAdmin);
if (admins.length <= 1) {
return c.json(
{ error: "Cannot exit as the last administrator" },
400
);
}
}
await chat.exitUser(user.id);
return c.json(
{ success: true, message: "You have exited the server" },
200
);
} catch (error) {
return c.json({ error: error.message }, 400);
}
});
server.get("/channels", authenticate, async (c) => {
const user = c.state.user;
if (!user) return c.json({ error: "Unauthorized" }, 401);
const channels = await chat.listChannels();
return c.json({ channels }, 200);
});
server.post("/channels", authenticate, async (c) => {
const user = c.state.user;
if (!user) return c.json({ error: "Unauthorized" }, 401);
const { name } = c.body;
if (!name) return c.json({ error: "Channel name is required" }, 400);
try {
const channel = await chat.createChannel(name, user.id);
return c.json({ channel }, 200);
} catch (error) {
return c.json({ error: error.message }, 400);
}
});
server.get(
"/channels/:channelId/messages",
authenticate,
async (c) => {
const user = c.state.user;
if (!user) return c.json({ error: "Unauthorized" }, 401);
const channelId = c.params.channelId;
try {
const messages = await chat.getMessagesByChannel(channelId);
const enhancedMessages = await Promise.all(
messages.map(async (message) => {
const author = await chat.getUserById(message.author.id);
return {
...message,
author: {
id: author?.id,
userName: author?.userName || "Unknown User"
}
};
})
);
return c.json({ messages: enhancedMessages }, 200);
} catch (error) {
return c.json({ error: error.message }, 400);
}
}
);
server.post(
"/channels/:channelId/messages",
authenticate,
async (c) => {
const user = c.state.user;
if (!user) return c.json({ error: "Unauthorized" }, 401);
const channelId = c.params.channelId;
const content = c.body?.content;
const images = c.body?.images;
if (!content && !images)
return c.json({ error: "Message content is required" }, 400);
try {
const message = await chat.createMessage(content, user.id, channelId);
return c.json({ message }, 200);
} catch (error) {
return c.json({ error: error.message }, 400);
}
}
);
server.put("/channels/:channelId", authenticate, async (c) => {
const user = c.state.user;
if (!user) return c.json({ error: "Unauthorized" }, 401);
const channelId = c.params.channelId;
const { name } = c.body;
if (!name) return c.json({ error: "Channel name is required" }, 400);
try {
const channel = await chat.updateChannel(channelId, name, user.id);
return c.json({ channel }, 200);
} catch (error) {
return c.json({ error: error.message }, 400);
}
});
server.delete("/channels/:channelId", authenticate, async (c) => {
const user = c.state.user;
if (!user) return c.json({ error: "Unauthorized" }, 401);
const channelId = c.params.channelId;
try {
await chat.deleteChannel(channelId, user.id);
return c.json({ success: true }, 200);
} catch (error) {
return c.json({ error: error.message }, 400);
}
});
server.put("/messages/:messageId", authenticate, async (c) => {
const user = c.state.user;
if (!user) return c.json({ error: "Unauthorized" }, 401);
const messageId = c.params.messageId;
const content = c.body?.content;
const images = c.body?.images;
if (!content && !images)
return c.json({ error: "Message content is required" }, 400);
try {
const { message, removedImages } = await chat.updateMessage(
messageId,
user.id,
content,
images
);
deleteImages(removedImages);
return c.json({ message }, 200);
} catch (error) {
return c.json({ error: error.message }, 400);
}
});
server.delete("/messages/:messageId", authenticate, async (c) => {
const user = c.state.user;
if (!user) return c.json({ error: "Unauthorized" }, 401);
const messageId = c.params.messageId;
try {
const message = await chat.getMessageById(messageId);
const images = message?.images || [];
await chat.deleteMessage(messageId, user.id);
deleteImages(images);
return c.json({ success: true }, 200);
} catch (error) {
return c.json({ error: error.message }, 400);
}
});
server.post(
"/messages/:messageId/reactions",
authenticate,
async (c) => {
const user = c.state.user;
if (!user) return c.json({ error: "Unauthorized" }, 401);
const messageId = c.params.messageId;
const { reaction } = c.body;
if (!reaction) return c.json({ error: "Reaction is required" }, 400);
try {
const message = await chat.addReaction(messageId, user.id, reaction);
if (!message) {
return c.json(
{ error: `Message with ID ${messageId} not found` },
401
);
}
return c.json({ message }, 200);
} catch (error) {
console.error(`Error adding reaction to message ${messageId}:`, error);
return c.json({ error: error.message }, 400);
}
}
);
server.delete(
"/messages/:messageId/reactions",
authenticate,
async (c) => {
const user = c.state.user;
if (!user) return c.json({ error: "Unauthorized" }, 401);
const messageId = c.params.messageId;
const { reaction } = c.body;
if (!reaction) return c.json({ error: "Reaction is required" }, 400);
try {
const message = await chat.removeReaction(messageId, user.id, reaction);
if (!message) {
return c.json(
{ error: `Message with ID ${messageId} not found` },
404
);
}
return c.json({ message }, 200);
} catch (error) {
console.error(
`Error removing reaction from message ${messageId}:`,
error
);
return c.json({ error: error.message }, 400);
}
}
);
server.post(
"/channels/:channelId/messages/image",
authenticate,
async (c) => {
const user = c.state.user;
if (!user) return c.json({ error: "Unauthorized" }, 401);
try {
const { filename, image } = c.body;
if (!image) return c.json({ error: "No image provided" }, 400);
const fileExtension = filename.split(".").pop();
if (!fileExtension)
return c.json({ error: "Missing file extension" }, 400);
if (!VALID_FILE_FORMATS.includes(fileExtension))
return c.json({ error: "Unsupported file format" }, 400);
const imageBuffer = Buffer.from(image, "base64");
const maxImageSize = MAX_IMAGE_SIZE_IN_MB * 1024 * 1024;
if (imageBuffer.length > maxImageSize)
return c.json({ error: "Image too large" }, 400);
const uploadDirectory = `${process.cwd()}/uploads`;
if (!existsSync6(uploadDirectory)) mkdirSync2(uploadDirectory);
const savedFileName = `${Date.now()}-${randomBytes2(1).toString("hex")}.${fileExtension}`;
const uploadPath = join3(uploadDirectory, savedFileName);
writeFileSync2(uploadPath, imageBuffer);
return c.json({
success: true,
//channelId,
filename: savedFileName
});
} catch (error) {
console.error("Image upload error:", error);
return c.json(
{
success: false,
error: error instanceof Error ? error.message : "Upload failed"
},
500
);
}
}
);
server.get(
"/channels/:channelId/messages/image/:filename",
authenticate,
async (c) => {
const user = c.state.user;
if (!user) return c.json({ error: "Unauthorized" }, 401);
try {
const { filename } = c.params;
const uploadDirectory = `${process.cwd()}/uploads`;
const filePath = join3(uploadDirectory, filename);
if (!existsSync6(filePath))
return c.json({ error: "Image not found" }, 404);
const imageBuffer = readFileSync4(filePath);
const fileExtension = filename.split(".").pop()?.toLowerCase();
let contentType = "application/octet-stream";
if (fileExtension === "jpg" || fileExtension === "jpeg")
contentType = "image/jpeg";
else if (fileExtension === "png") contentType = "image/png";
else if (fileExtension === "webp") contentType = "image/webp";
else if (fileExtension === "svg") contentType = "image/svg+xml";
return c.binary(imageBuffer, contentType);
} catch (error) {
return c.json(
{
success: false,
error: error instanceof Error ? error.message : "Image fetch failed"
},
500
);
}
}
);
server.get("/server/settings", authenticate, async (c) => {
const user = c.state.user;
if (!user) return c.json({ error: "Unauthorized" }, 401);
try {
const settings2 = await chat.getServerSettings();
return c.json(settings2 || { name: "MikroChat" }, 200);
} catch (error) {
return c.json({ error: error.message }, 400);
}
});
server.put("/server/settings", authenticate, async (c) => {
const user = c.state.user;
if (!user) return c.json({ error: "Unauthorized" }, 401);
const { name } = c.body;
if (!name) return c.json({ error: "Server name is required" }, 400);
try {
await chat.updateServerSettings({ name });
return c.json({ name }, 200);
} catch (error) {
return c.json({ error: error.message }, 400);
}
});
server.get("/events", async (c) => {
let user = null;
const token = c.query.token;
if (token) {
try {
const payload = auth.verify(token);
user = await chat.getUserByEmail(payload.email || payload.sub);
} catch (error) {
console.error("SSE token validation error:", error);
}
}
if (!user && c.headers.authorization?.startsWith("Bearer ")) {
const headerToken = c.headers.authorization.substring(7);
try {
const payload = auth.verify(headerToken);
user = await chat.getUserByEmail(payload.email || payload.sub);
} catch (error) {
console.error("SSE header validation error:", error);
}
}
if (!user) {
console.log("SSE unauthorized access attempt");
return {
statusCode: 401,
body: { error: "Unauthorized" },
headers: { "Content-Type": "application/json" }
};
}
const connectionId = randomBytes2(8).toString("hex");
if (activeConnections.has(user.id)) {
const now = Date.now();
const userConnections2 = activeConnections.get(user.id);
const staleConnectionIds = [];
userConnections2.forEach((connectionId2) => {
const lastActivity = connectionTimeouts.get(connectionId2) || 0;
if (now - lastActivity > CONNECTION_TIMEOUT_MS) {
staleConnectionIds.push(connectionId2);
}
});
staleConnectionIds.forEach((connectionId2) => {
userConnections2.delete(connectionId2);
connectionTimeouts.delete(connectionId2);
console.log(
`Cleaned up stale connection ${connectionId2} for user ${user.id}`
);
});
if (userConnections2.size >= MAX_CONNECTIONS_PER_USER) {
let oldestConnectionId = null;
let oldestTime = Number.POSITIVE_INFINITY;
userConnections2.forEach((connectionId2) => {
const lastActivity = connectionTimeouts.get(connectionId2) || 0;
if (lastActivity < oldestTime) {
oldestTime = lastActivity;
oldestConnectionId = connectionId2;
}
});
if (oldestConnectionId) {
userConnections2.delete(oldestConnectionId);
connectionTimeouts.delete(oldestConnectionId);
console.log(
`Removed oldest connection ${oldestConnectionId} for user ${user.id} to make room`
);
}
}
} else {
activeConnections.set(user.id, /* @__PURE__ */ new Set());
}
const userConnections = activeConnections.get(user.id);
userConnections.add(connectionId);
connectionTimeouts.set(connectionId, Date.now());
console.log(
`SSE connection established for user ${user.id} (${connectionId}). Total connections: ${userConnections.size}`
);
c.res.writeHead(200, {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
"X-Accel-Buffering": "no"
});
const updateActivity = () => {
connectionTimeouts.set(connectionId, Date.now());
};
const keepAlive = setInterval(() => {
if (!c.res.writable) {
clearInterval(keepAlive);
return;
}
updateActivity();
c.res.write(": ping\n\n");
}, 3e4);
c.res.write(":\n\n");
c.res.write(
`data: ${JSON.stringify({
type: "CONNECTED",
payload: {
message: "SSE connection established",
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
userId: user.id,
connectionId
}
})}
`
);
updateActivity();
const unsubscribe = chat.subscribeToEvents((event) => {
if (!c.res.writable) {
unsubscribe();
return;
}
try {
updateActivity();
c.res.write(`data: ${JSON.stringify(event)}
`);
} catch (error) {
console.error("Error sending SSE event:", error);
cleanupConnection();
}
});
const cleanupConnection = () => {
const userConnections2 = activeConnections.get(user.id);
if (userConnections2) {
userConnections2.delete(connectionId);
connectionTimeouts.delete(connectionId);
console.log(
`Connection ${connectionId} for user ${user.id} cleaned up. Remaining: ${userConnections2.size}`
);
if (userConnections2.size === 0) activeConnections.delete(user.id);
}
unsubscribe();
clearInterval(keepAlive);
if (c.res.writable) c.res.end();
};
c.req.on("close", cleanupConnection);
c.req.on("error", (error) => {
console.error(`SSE connection error for user ${user.id}:`, error);
cleanupConnection();
});
c.res.on("error", (error) => {
console.error(`SSE response error for user ${user.id}:`, error);
cleanupConnection();
});
return {
statusCode: 200,
_handled: true,
body: null
};
});
async function authenticate(c, next) {
const unauthorized = {
error: "Unauthorized",
message: "Authentication required"
};
const authHeader = c.headers.authorization;
if (!authHeader || !authHeader.startsWith("Bearer "))
return c.status(401).json(unauthorized);
const token = authHeader.split(" ")[1];
if (!token) return c.status(401).json(unauthorized);
const payload = auth.verify(token);
const user = await chat.getUserByEmail(payload.email || payload.sub);
c.state.user = user;
return next();
}
server.start();
}
function deleteImages(images) {
for (const image of images) {
const uploadDirectory = `${process.cwd()}/uploads`;
const imagePath = join3(uploadDirectory, image);
unlinkSync(imagePath);
}
}
// src/providers/MikroDBProvider.ts
var MikroDBProvider = class extends GeneralStorageProvider {
db;
mikroDb;
constructor(mikroDb) {
super();
this.mikroDb = mikroDb;
this.db = new MikroDbDatabase(mikroDb);
}
/**
* @description Start the MikroDB instance.
* This must be called before using any other methods.
*/
async start() {
await this.mikroDb.start();
}
/**
* @description Close the database connection and clean up resources.
* This should be called when shutting down the application.
*/
async close() {
await this.mikroDb.close();
}
};
var MikroDbDatabase = class {
db;
tableName;
constructor(db, tableName = "mikrochat_db") {
this.db = db;
this.tableName = tableName;
}
/**
* @description Get a value by key.
*/
async get(key) {
try {
const result = await this.db.get({
tableName: this.tableName,
key
});
if (result === void 0 || result === null) return null;
return result;
} catch (error) {
console.error(`Error getting key ${key}:`, error);
return null;
}
}
/**
* @description Set a value with optional expiry.
*/
async set(key, value, expirySeconds) {
const expiration = expirySeconds ? Date.now() + expirySeconds * 1e3 : 0;
await this.db.write({
tableName: this.tableName,
key,
value,
expiration
});
}
/**
* @description Delete a key.
*/
async delete(key) {
await this.db.delete({
tableName: this.tableName,
key
});
}
/**
* @description List all values with keys that start with the prefix.
*/
async list(prefix) {
try {
const allItems = await this.db.get({
tableName: this.tableName
}) || [];
const filteredItems = allItems.filter((item) => {
return Array.isArray(item) && typeof item[0] === "string" && item[0].startsWith(prefix);
}).map((item) => item[1].value);
return filteredItems;
} catch (error) {
console.error(`Error listing with prefix ${prefix}:`, error);
return [];
}
}
};
// src/config/mikrochatOptions.ts
var defaults2 = configDefaults3();
var mikrochatOptions = {
configFilePath: "mikrochat.config.json",
args: process.argv,
options: [
{
flag: "--dev",
path: "devMode",
defaultValue: defaults2.devMode,
isFlag: true
},
// Auth configuration
{
flag: "--jwtSecret",
path: "auth.jwtSecret",
defaultValue: defaults2.auth.jwtSecret
},
{
flag: "--magicLinkExpirySeconds",
path: "auth.magicLinkExpirySeconds",
defaultValue: defaults2.auth.magicLinkExpirySeconds
},
{
flag: "--jwtExpirySeconds",
path: "auth.jwtExpirySeconds",
defaultValue: defaults2.auth.jwtExpirySeconds
},
{
flag: "--refreshTokenExpirySeconds",
path: "auth.refreshTokenExpirySeconds",
defaultValue: defaults2.auth.refreshTokenExpirySeconds
},
{
flag: "--maxActiveSessions",
path: "auth.maxActiveSessions",
defaultValue: defaults2.auth.maxActiveSessions
},
{
flag: "--appUrl",
path: "auth.appUrl",
defaultValue: defaults2.auth.appUrl
},
{
flag: "--isInviteRequired",
path: "auth.isInviteRequired",
isFlag: true,
defaultValue: defaults2.auth.isInviteRequired
},
{
flag: "--debug",
path: "auth.debug",
isFlag: true,
defaultValue: defaults2.auth.debug
},
// Email configuration
{
flag: "--emailSubject",
path: "email.emailSubject",
defaultValue: "Your Secure Login Link"
},
{
flag: "--emailHost",
path: "email.host",
defaultValue: defaults2.email.host
},
{
flag: "--emailUser",
path: "email.user",
defaultValue: defaults2.email.user
},
{
flag: "--emailPassword",
path: "email.password",
defaultValue: defaults2.email.password
},
{
flag: "--emailPort",
path: "email.port",
defaultValue: defaults2.email.host
},
{
flag: "--emailSecure",
path: "email.secure",
isFlag: true,
defaultValue: defaults2.email.secure
},
{
flag: "--emailMaxRetries",
path: "email.maxRetries",
defaultValue: defaults2.email.maxRetries
},
{
flag: "--debug",
path: "email.debug",
isFlag: true,
defaultValue: defaults2.email.debug
},
// Storage configuration
{
flag: "--db",
path: "storage.databaseDirectory",
defaultValue: defaults2.storage.databaseDirectory
},
{
flag: "--encryptionKey",
path: "storage.encryptionKey",
defaultValue: defaults2.storage.encryptionKey
},
{
flag: "--debug",
path: "storage.debug",
defaultValue: defaults2.storage.debug
},
// Server configuration
{
flag: "--port",
path: "server.port",
defaultValue: defaults2.server.port
},
{
flag: "--host",
path: "server.host",
defaultValue: defaults2.server.host
},
{
flag: "--https",
path: "server.useHttps",
isFlag: true,
defaultValue: defaults2.server.useHttps
},
{
flag: "--http2",
path: "server.useHttp2",
isFlag: true,
defaultValue: defaults2.server.useHttp2
},
{
flag: "--cert",
path: "server.sslCert",
defaultValue: defaults2.server.sslCert
},
{
flag: "--key",
path: "server.sslKey",
defaultValue: defaults2.server.sslKey
},
{
flag: "--ca",
path: "server.sslCa",
defaultValue: defaults2.server.sslCa
},
{
flag: "--ratelimit",
path: "server.rateLimit.enabled",
defaultValue: defaults2.server.rateLimit.enabled,
isFlag: true
},
{
flag: "--rps",
path: "server.rateLimit.requestsPerMinute",
defaultValue: defaults2.server.rateLimit.requestsPerMinute
},
{
flag: "--allowed",
path: "server.allowedDomains",
defaultValue: defaults2.server.allowedDomains,
parser: parsers.array
},
{
flag: "--debug",
path: "server.debug",
isFlag: true,
defaultValue: defaults2.server.debug
},
// Chat configuration
{
flag: "--initialUserId",
path: "chat.initialUser.id",
defaultValue: defaults2.chat.initialUser.id
},
{
flag: "--initialUserUsername",
path: "chat.initialUser.userName",
defaultValue: defaults2.chat.initialUser.userName
},
{
flag: "--initialUserEmail",
path: "chat.initialUser.email",
defaultValue: defaults2.chat.initialUser.email
},
{
flag: "--messageRetentionDays",
path: "chat.messageRetentionDays",
defaultValue: defaults2.chat.messageRetentionDays
},
{
flag: "--maxMessagesPerChannel",
path: "chat.maxMessagesPerChannel",
defaultValue: defaults2.chat.maxMessagesPerChannel
}
]
};
// src/index.ts
async function start() {
const config = getConfig();
const db = new MikroDB(config.storage);
await db.start();
const authStorageProvider = new x(db);
const chatStorageProvider = new MikroDBProvider(db);
const emailProvider = new k(config.email);
const auth = new E(config, emailProvider, authStorageProvider);
const chat = new MikroChat(config.chat, chatStorageProvider);
await startServer2({
config: config.server,
auth,
chat,
devMode: config.devMode,
isInviteRequired: config.auth.isInviteRequired
});
}
function getConfig() {
const config = new MikroConf(mikrochatOptions).get();
if (config.auth.debug) console.log("Using configuration:", config);
return config;
}
start();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment