Skip to content

Instantly share code, notes, and snippets.

@mikaelvesavuori
Last active April 3, 2025 18:35
Show Gist options
  • Save mikaelvesavuori/f838c02e4da491ce53368b52264330d2 to your computer and use it in GitHub Desktop.
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
};
console.log('SMTP CONFIG', this.config);
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}`
);
}
console.log('BEFORE SENDING')
const result = await this.smtpClient.sendEmail(emailOptions);
console.log('EMAIL RESULT', result)
console.log('AFTER SENDING')
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 = () => {
const 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 = {}) {
const 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);
const 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 = {}) {
const r = this.decode(e);
if (r.header.alg !== this.algorithm)
throw new Error(
`Invalid algorithm. Expected ${this.algorithm}, got ${r.header.alg}`
);
const [i, s] = e.split('.'),
a = `${i}.${s}`;
if (this.createSignature(a) !== r.signature)
throw new Error('Invalid signature');
const 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) {
const t = e.split('.');
if (t.length !== 3) throw new Error('Invalid token format');
try {
const [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) {
const 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() {
const e = Date.now();
for (const [t, r] of this.data.entries())
r.expiry &&
r.expiry < e &&
(this.data.delete(t), this.expiryEmitter.emit('expired', t));
for (const [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) {
const i = r ? Date.now() + r * 1e3 : null;
this.data.set(e, { value: t, expiry: i });
}
async get(e) {
const 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
});
const i = this.collections.get(e);
r && (i.expiry = Date.now() + r * 1e3), i.items.push(t);
}
async removeFromCollection(e, t) {
const r = this.collections.get(e);
r && (r.items = r.items.filter((i) => i !== t));
}
async getCollection(e) {
const t = this.collections.get(e);
return t ? [...t.items] : [];
}
async getCollectionSize(e) {
const t = this.collections.get(e);
return t ? t.items.length : 0;
}
async removeOldestFromCollection(e) {
const t = this.collections.get(e);
return !t || t.items.length === 0 ? null : t.items.shift() || null;
}
async findKeys(e) {
const 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;
const [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(']')) {
const t = o.slice(1, -1);
return t.startsWith('IPv6:') ? R(t.slice(5)) : M(t);
}
const e = o.split('.');
if (e.length === 0) return false;
for (const 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) {
const 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;
const t = o.split(':');
return !(t.length < 2 || t.length > 8);
}
var n = g();
var P = (o) => {
const 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) {
const 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) {
const 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) {
const i = `sessions:${e}`;
if (
(await this.storage.getCollectionSize(i)) >=
this.config.auth.maxActiveSessions
) {
const 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) {
const { 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) {
const { email: t, ip: r } = e;
if (!S(t)) throw new Error('Valid email required');
try {
const 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
);
const d = await this.storage.findKeys('magic_link:*');
for (const u of d) {
if (u === s) continue;
const 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);
const 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) {
const { token: t, email: r } = e;
try {
const i = `magic_link:${t}`,
s = await this.storage.get(i);
if (!s) throw new Error('Invalid or expired token');
const a = JSON.parse(s);
if (a.email !== r) throw new Error('Email mismatch');
const d = a.username,
l = a.role;
await this.storage.delete(i);
const 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 {
const t = await this.storage.get(`refresh:${e}`);
if (!t) throw new Error('Invalid or expired refresh token');
const r = JSON.parse(t),
i = r.email;
if (!i) throw new Error('Invalid refresh token data');
const 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');
const t = await this.storage.get(`refresh:${e}`);
if (!t) return { message: v.logoutSuccess };
const i = JSON.parse(t).email;
if (!i) throw new Error('Invalid refresh token data');
await this.storage.delete(`refresh:${e}`);
const 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');
const t = e.user.email,
r = e.body?.refreshToken,
i = `sessions:${t}`,
a = (await this.storage.getCollection(i)).map(async (l) => {
try {
const c = await this.storage.get(`refresh:${l}`);
if (!c) return await this.storage.removeFromCollection(i, l), null;
const 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');
const t = e.user.email,
r = e.body?.refreshToken,
i = `sessions:${t}`,
s = await this.storage.getCollection(i);
for (const 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 {
const r = e.headers?.authorization;
if (!r || !r.startsWith('Bearer '))
throw new Error('Authentication required');
const i = r.split(' ')[1];
try {
const 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) {
const 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) {
const t = `${this.PREFIX_KV}${e}`,
r = await this.db.get({ tableName: this.TABLE_NAME, key: t });
return r || null;
}
async delete(e) {
const 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);
const 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) {
const 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) {
const 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) {
const t = `${this.PREFIX_COLLECTION}${e}`,
r = await this.db.get({ tableName: this.TABLE_NAME, key: t });
if (!r) return null;
const i = JSON.parse(r);
if (i.length === 0) return null;
const s = i.shift();
return (
await this.db.write({
tableName: this.TABLE_NAME,
key: t,
value: JSON.stringify(i)
}),
s
);
}
async findKeys(e) {
const t = e.replace(/\./g, '\\.').replace(/\*/g, '.*').replace(/\?/g, '.'),
r = new RegExp(`^${t}$`);
return (await this.db.get({ tableName: this.TABLE_NAME }))
.filter((s) => {
const 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