Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions .env.template
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
MONGODB_URI=
DATABASE_NAME=
DATABASE_POOL_SIZE=

SERVER_NAME=
SERVER_VERSION=
SERVER_PORT=
SERVER_BASE_URL=
SERVER_HOST=

MATRIX_SERVER_NAME=
MATRIX_DOMAIN=
MATRIX_KEY_REFRESH_INTERVAL=

CONFIG_FOLDER=

HOMESERVER_CONFIG_DNS_SERVERS=

LOG_LEVEL=
NODE_ENV=

DEBUG=

NODE_TLS_REJECT_UNAUTHORIZED=
16 changes: 11 additions & 5 deletions packages/federation-sdk/src/container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { RoomRepository } from './repositories/room.repository';
import { ServerRepository } from './repositories/server.repository';
import { StateEventRepository } from './repositories/state-event.repository';
import { StateRepository } from './repositories/state.repository';
import { ConfigService } from './services/config.service';
import { type AppConfig, ConfigService } from './services/config.service';
import { DatabaseConnectionService } from './services/database-connection.service';
import { EventAuthorizationService } from './services/event-authorization.service';
import { EventEmitterService } from './services/event-emitter.service';
Expand Down Expand Up @@ -47,7 +47,10 @@ export interface FederationContainerOptions {
lockManagerOptions?: LockConfig;
}

export function createFederationContainer(options: FederationContainerOptions) {
export function createFederationContainer(
options: FederationContainerOptions,
configInstance: ConfigService,
) {
const {
emitter,
federationOptions,
Expand All @@ -57,13 +60,16 @@ export function createFederationContainer(options: FederationContainerOptions) {
container.register<FederationModuleOptions>('FEDERATION_OPTIONS', {
useValue: federationOptions,
});
container.register<AppConfig>('APP_CONFIG', {
useValue: configInstance.getConfig(),
});
container.registerSingleton('ConfigService', ConfigService);

// Register core services
container.registerSingleton(
'FederationConfigService',
FederationConfigService,
);
container.registerSingleton('ConfigService', ConfigService);
container.registerSingleton(
'DatabaseConnectionService',
DatabaseConnectionService,
Expand Down Expand Up @@ -131,12 +137,12 @@ export function createFederationContainer(options: FederationContainerOptions) {
// Initialize listeners
const y = container.resolve(StagingAreaListener);
const x = container.resolve(MissingEventListener);

// @ts-ignore
x.stagingAreaService.missingEventsService.missingEventsQueue = x.missingEventsQueue;
// @ts-ignore
x.stagingAreaService.stagingAreaQueue = y.stagingAreaQueue;

console.log(x, y);

return container;
Expand Down
6 changes: 4 additions & 2 deletions packages/federation-sdk/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { SendJoinService } from './services/send-join.service';
import { ServerService } from './services/server.service';
import { StateService } from './services/state.service';
import { WellKnownService } from './services/well-known.service';
import { ConfigService } from './services/config.service';

export { FederationEndpoints } from './specs/federation-api';
export type {
Expand Down Expand Up @@ -108,6 +109,7 @@ export interface HomeserverServices {
state: StateService;
sendJoin: SendJoinService;
server: ServerService;
config: ConfigService;
}

export type HomeserverEventSignatures = {
Expand Down Expand Up @@ -137,8 +139,7 @@ export type HomeserverEventSignatures = {
};
};

export function getAllServices(
): HomeserverServices {
export function getAllServices(): HomeserverServices {
return {
room: container.resolve(RoomService),
message: container.resolve(MessageService),
Expand All @@ -149,6 +150,7 @@ export function getAllServices(
state: container.resolve(StateService),
sendJoin: container.resolve(SendJoinService),
server: container.resolve(ServerService),
config: container.resolve(ConfigService),
};
}

Expand Down
155 changes: 74 additions & 81 deletions packages/federation-sdk/src/services/config.service.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
import * as fs from 'node:fs';
import * as path from 'node:path';
import { createLogger, getKeyPair } from '@hs/core';
import * as dotenv from 'dotenv';
import { z } from 'zod';

import { singleton } from 'tsyringe';

const CONFIG_FOLDER = process.env.CONFIG_FOLDER || '.';
import { inject, singleton } from 'tsyringe';

export interface AppConfig {
server: {
Expand All @@ -28,20 +24,74 @@ export interface AppConfig {
keyRefreshInterval: number;
};

signingKey?: any;
signingKeyPath?: string;
path?: string;
signingKeyPath: string;
}

export const AppConfigSchema = z.object({
server: z.object({
name: z.string().min(1, 'Server name is required'),
version: z.string().min(1, 'Server version is required'),
port: z
.number()
.int()
.min(1)
.max(65535, 'Port must be between 1 and 65535'),
baseUrl: z.string().url('Base URL must be a valid URL'),
host: z.string().min(1, 'Host is required'),
}),
database: z.object({
uri: z.string().min(1, 'Database URI is required'),
name: z.string().min(1, 'Database name is required'),
poolSize: z.number().int().min(1, 'Pool size must be at least 1'),
}),
matrix: z.object({
serverName: z.string().min(1, 'Matrix server name is required'),
domain: z.string().min(1, 'Matrix domain is required'),
keyRefreshInterval: z
.number()
.int()
.min(1, 'Key refresh interval must be at least 1'),
}),
signingKeyPath: z.string(),
});

@singleton()
export class ConfigService {
private config: AppConfig;
private fileConfig: Partial<AppConfig> = {};
private logger = createLogger('ConfigService');

constructor() {
this.loadEnvFiles();
this.config = this.initializeConfig();
constructor(@inject('APP_CONFIG') values: AppConfig) {
try {
const validatedConfig = AppConfigSchema.parse(values);
this.config = {
server: {
name: validatedConfig.server.name,
version: validatedConfig.server.version,
port: validatedConfig.server.port,
baseUrl: validatedConfig.server.baseUrl,
host: validatedConfig.server.host,
},
database: {
uri: validatedConfig.database.uri,
name: validatedConfig.database.name,
poolSize: validatedConfig.database.poolSize,
},
matrix: {
serverName: validatedConfig.matrix.serverName,
domain: validatedConfig.matrix.domain,
keyRefreshInterval: validatedConfig.matrix.keyRefreshInterval,
},
signingKeyPath: validatedConfig.signingKeyPath,
};
} catch (error) {
if (error instanceof z.ZodError) {
this.logger.error('Configuration validation failed:', error.errors);
throw new Error(
`Invalid configuration: ${error.errors.map((e) => `${e.path.join('.')}: ${e.message}`).join(', ')}`,
);
}
throw error;
}
}

getConfig(): AppConfig {
Expand All @@ -64,86 +114,29 @@ export class ConfigService {
return this.loadSigningKey();
}

private loadEnvFiles(): void {
const nodeEnv = process.env.NODE_ENV || 'development';

const defaultEnvPath = path.resolve(process.cwd(), '.env');
if (fs.existsSync(defaultEnvPath)) {
dotenv.config({ path: defaultEnvPath });
this.logger.info('Loaded configuration from .env');
}

const envSpecificPath = path.resolve(process.cwd(), `.env.${nodeEnv}`);
if (fs.existsSync(envSpecificPath)) {
dotenv.config({ path: envSpecificPath });
this.logger.info(`Loaded configuration from .env.${nodeEnv}`);
}

const localEnvPath = path.resolve(process.cwd(), '.env.local');
if (fs.existsSync(localEnvPath)) {
dotenv.config({ path: localEnvPath });
this.logger.info('Loaded configuration from .env.local');
async loadSigningKey() {
if (!this.config.signingKeyPath) {
throw new Error('Signing key path is not set in the configuration.');
}
}

private mergeConfigs(
baseConfig: AppConfig,
newConfig: Partial<AppConfig>,
): AppConfig {
return {
...baseConfig,
...newConfig,
server: { ...baseConfig.server, ...newConfig.server },
database: { ...baseConfig.database, ...newConfig.database },
matrix: { ...baseConfig.matrix, ...newConfig.matrix },
};
}

async loadSigningKey() {
const signingKeyPath = `${CONFIG_FOLDER}/${this.config.server.name}.signing.key`;
this.logger.info(`Loading signing key from ${signingKeyPath}`);
this.logger.info(`Loading signing key from ${this.config.signingKeyPath}`);

try {
const keys = await getKeyPair({ signingKeyPath });
const keys = await getKeyPair({
signingKeyPath: this.config.signingKeyPath,
});
this.logger.info(
`Successfully loaded signing key for server ${this.config.server.name}`,
);
return keys;
} catch (error: any) {
this.logger.error(`Failed to load signing key: ${error.message}`);
} catch (error: unknown) {
this.logger.error(
`Failed to load signing key: ${error instanceof Error ? error.message : String(error)}`,
);
throw error;
}
}

private initializeConfig(): AppConfig {
return {
server: {
name: process.env.SERVER_NAME || 'rc1',
version: process.env.SERVER_VERSION || '1.0',
port: this.getNumberFromEnv('SERVER_PORT', 8080),
baseUrl: process.env.SERVER_BASE_URL || 'http://rc1:8080',
host: process.env.SERVER_HOST || '0.0.0.0',
},
database: {
uri: process.env.MONGODB_URI || 'mongodb://localhost:27017/matrix',
name: process.env.DATABASE_NAME || 'matrix',
poolSize: this.getNumberFromEnv('DATABASE_POOL_SIZE', 10),
},
matrix: {
serverName: process.env.MATRIX_SERVER_NAME || 'rc1',
domain: process.env.MATRIX_DOMAIN || 'rc1',
keyRefreshInterval: this.getNumberFromEnv(
'MATRIX_KEY_REFRESH_INTERVAL',
60,
),
},
};
}

private getNumberFromEnv(key: string, defaultValue: number): number {
return process.env[key] ? Number.parseInt(process.env[key]!) : defaultValue;
}

getServerName(): string {
return this.config.server.name;
}
Expand Down
35 changes: 33 additions & 2 deletions packages/homeserver/src/homeserver.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ import {
type HomeserverEventSignatures,
createFederationContainer,
} from '@hs/federation-sdk';
import * as dotenv from 'dotenv';
import * as fs from 'node:fs';
import * as path from 'node:path';

import { swagger } from '@elysiajs/swagger';
import { toUnpaddedBase64 } from '@hs/core';
Expand All @@ -31,7 +34,35 @@ export interface HomeserverSetupOptions {
}

export async function setup(options?: HomeserverSetupOptions) {
const config = new ConfigService();
const envPath = path.resolve(process.cwd(), '.env');
if (fs.existsSync(envPath)) {
dotenv.config({ path: envPath });
}

const config = new ConfigService({
database: {
uri: process.env.MONGODB_URI || 'mongodb://localhost:27017/matrix',
name: process.env.DATABASE_NAME || 'matrix',
poolSize: Number.parseInt(process.env.DATABASE_POOL_SIZE || '10', 10),
},
server: {
name: process.env.SERVER_NAME || 'rc1',
version: process.env.SERVER_VERSION || '1.0',
port: Number.parseInt(process.env.SERVER_PORT || '8080', 10),
baseUrl: process.env.SERVER_BASE_URL || 'http://rc1:8080',
host: process.env.SERVER_HOST || '0.0.0.0',
},
matrix: {
serverName: process.env.MATRIX_SERVER_NAME || 'rc1',
domain: process.env.MATRIX_DOMAIN || 'rc1',
keyRefreshInterval: Number.parseInt(
process.env.MATRIX_KEY_REFRESH_INTERVAL || '60',
10,
),
},
signingKeyPath: process.env.CONFIG_FOLDER || './rc1.signing.key',
});

const matrixConfig = config.getMatrixConfig();
const serverConfig = config.getServerConfig();
const signingKeys = await config.getSigningKey();
Expand All @@ -48,7 +79,7 @@ export async function setup(options?: HomeserverSetupOptions) {
emitter: options?.emitter,
};

const container = await createFederationContainer(containerOptions);
const container = await createFederationContainer(containerOptions, config);

const app = new Elysia();

Expand Down