diff --git a/README.md b/README.md index f59c60f..8a695f3 100644 --- a/README.md +++ b/README.md @@ -6,25 +6,26 @@ At the moment, all configuration is done by environment variables. All of them are optional -| Variable | Description | Default value | -| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------- | -| AGENT_NAME | Label to show to other DIDComm agents | Test Cloud Agent | -| AGENT_ENDPOINTS | Comma-separated public endpoint list where agent DIDComm endpoints will be accessible (including protocol and port) | ws://localhost:4000 | -| AGENT_PUBLIC_DID | Agent's public DID (in did:web format) | None | -| AGENT_PORT | Port where DIDComm agent will be running | 4000 | -| AGENT_LOG_LEVEL | Agent log level | 2 (debug) | -| HTTP_SUPPORT | Enable support of incoming DIDComm messages through HTTP transport | true | -| WS_SUPPORT | Enable support of incoming DIDComm messages through WebSocket transport | true | -| WALLET_NAME | Wallet (database) name | test-cloud-agent | -| WALLET_KEY | Wallet base encryption key | 'Test Cloud Agent' | -| KEY_DERIVATION_METHOD | Wallet key derivation method | ARGON2I_MOD | -| POSTGRES_HOST | PosgreSQL database host | None (use SQLite) | -| POSTGRES_USER | PosgreSQL database username | None | -| POSTGRES_PASSWORD | PosgreSQL database password | None | -| POSTGRES_ADMIN_USER | PosgreSQL database admin user | None | -| POSTGRES_ADMIN_PASSWORD | PosgreSQL database admin password | None | -| MPR_WS_URL | Message Pickup Repository server WebSocket URL. If not defined, it will use internal Message Pickup management (for single-instance, local development only). | none | -| MPR_MAX_RECEIVE_BYTES | Message Pickup Repository Optional byte size limit for retrieving messages | none | +| Variable | Description | Default value | +| ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------- | +| AGENT_NAME | Label to show to other DIDComm agents | Test Cloud Agent | +| AGENT_ENDPOINTS | Comma-separated public endpoint list where agent DIDComm endpoints will be accessible (including protocol and port) | ws://localhost:4000 | +| AGENT_PUBLIC_DID | Agent's public DID (in did:web format) | None | +| AGENT_PORT | Port where DIDComm agent will be running | 4000 | +| AGENT_LOG_LEVEL | Agent log level | 2 (debug) | +| HTTP_SUPPORT | Enable support of incoming DIDComm messages through HTTP transport | true | +| WS_SUPPORT | Enable support of incoming DIDComm messages through WebSocket transport | true | +| WALLET_NAME | Wallet (database) name | test-cloud-agent | +| WALLET_KEY | Wallet base encryption key | 'Test Cloud Agent' | +| KEY_DERIVATION_METHOD | Wallet key derivation method | ARGON2I_MOD | +| POSTGRES_HOST | PosgreSQL database host | None (use SQLite) | +| POSTGRES_USER | PosgreSQL database username | None | +| POSTGRES_PASSWORD | PosgreSQL database password | None | +| POSTGRES_ADMIN_USER | PosgreSQL database admin user | None | +| POSTGRES_ADMIN_PASSWORD | PosgreSQL database admin password | None | +| MPR_WS_URL | Message Pickup Repository server WebSocket URL. If not defined, it will use internal Message Pickup management (for single-instance, local development only). | none | +| MPR_MAX_RECEIVE_BYTES | Message Pickup Repository Optional byte size limit for retrieving messages | none | +| FIREBASE_CFG_FILE | Defines the path to the Firebase project configuration file used to initialize the Firebase Admin SDK. This file must be a JSON file containing the service account credentials for the Firebase project. If the variable is not set, Firebase-based notifications will be disabled. This applies to both the PostgresMessagePickupRepository and InMemoryMessagePickupRepository modes. | `./firebase.cfg.json` | These variables might be set also in `.env` file in the form of KEY=VALUE (one per line). @@ -95,3 +96,19 @@ server IP-HOST:4002; ```bash docker compose -f docker-compose-lb.yml up --build ``` + +## Didcomm Mediator: Configurable Message Pickup Repository + +The Didcomm Mediator now supports flexible configuration for message pickup repositories, allowing users to choose between different persistence methods depending on their needs. This enhancement provides seamless integration with the following repository options: + +- MessagePickupRepositoryClient: A WebSocket-based repository for distributed environments. +- PostgresMessagePickupRepository: A PostgreSQL-based repository for persistent storage. It is meant for simplicity, so it uses the same Postgres host than mediator's wallet. +- InMemoryMessagePickupRepository: An in-memory repository for lightweight setups or testing purposes. It only works when SQLite is used for mediator wallet. + +### How to configure + +The repository configuration is controlled by these environment variables. The mediator will automatically detect the active variable and initialize the appropriate repository. + +1. WebSocket-Based Repository (MessagePickupRepositoryClient): Set the `MPR_WS_URL` environment variable to the WebSocket server URL. +2. PostgreSQL-Based Repository (PostgresMessagePickupRepository): Set the `POSTGRES_HOST` environment variable to the PostgreSQL connection string and `MPR_WS_URL` is null +3. In-Memory Repository (InMemoryMessagePickupRepository): If neither `MPR_WS_URL` and `POSTGRES_HOST` is set, the mediator will default to InMemoryMessagePickupRepository. diff --git a/docker-compose-lb.yml b/docker-compose-lb.yml index 6ebd8a4..f47e862 100644 --- a/docker-compose-lb.yml +++ b/docker-compose-lb.yml @@ -6,7 +6,7 @@ services: ports: - 5432:5432 volumes: - - ~/data/postgres:/var/lib/postgresql/data + - ~/data/dm/postgres:/var/lib/postgresql/data environment: - POSTGRES_PASSWORD=cloud-agent - POSTGRES_USER=cloud-agent @@ -26,7 +26,9 @@ services: - POSTGRES_USER=cloud-agent - POSTGRES_PASSWORD=cloud-agent - KEY_DERIVATION_METHOD=ARGON2I_MOD - - MPR_WS_URL=ws://192.168.10.13:3100 + #- MPR_WS_URL=ws://192.168.10.13:3100 + #- MAX_RECEIVE_BYTES=1000000 + - FCM_SERVICE_BASE_URL=http://192.168.100.84:3000/test ports: - 3001:3000 - 4001:4000 @@ -52,7 +54,9 @@ services: - POSTGRES_USER=cloud-agent - POSTGRES_PASSWORD=cloud-agent - KEY_DERIVATION_METHOD=ARGON2I_MOD - - MPR_WS_URL=ws://192.168.10.13:3100 + #- MPR_WS_URL=ws://192.168.10.13:3100 + #- MAX_RECEIVE_BYTES=1000000 + - FCM_SERVICE_BASE_URL=http://192.168.100.84:3000/test restart: always ports: - 3002:3000 diff --git a/docker-compose.yml b/docker-compose.yml index 942576e..f53234f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,7 +6,7 @@ services: ports: - 5432:5432 volumes: - - ~/data/postgres:/var/lib/postgresql/data + - ~/data/dm/postgres:/var/lib/postgresql/data environment: - POSTGRES_PASSWORD=cloud-agent - POSTGRES_USER=cloud-agent @@ -26,13 +26,23 @@ services: - POSTGRES_USER=cloud-agent - POSTGRES_PASSWORD=cloud-agent - KEY_DERIVATION_METHOD=ARGON2I_MOD - - MPR_WS_URL=ws://192.168.10.13:3100 + - FIREBASE_CFG_FILE=/config/firebase-cfg.json + #- MPR_WS_URL=ws://192.168.10.13:3100 ports: - - 3001:3000 - - 4001:4000 + - 3000:3000 + - 4000:4000 volumes: - .afj:/root/.afj + - ./firebase-cfg.json:/config/firebase-cfg.json depends_on: - postgres links: - postgres + + adminer: + image: adminer + restart: always + ports: + - 8080:8080 + depends_on: + - postgres diff --git a/package.json b/package.json index 4a8e6e4..ca858b4 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "author": "", "license": "ISC", "dependencies": { + "@2060.io/credo-ts-message-pickup-repository-pg": "^0.0.7", "@2060.io/message-pickup-repository-client": "^0.0.7", "@credo-ts/askar": "0.5.11", "@credo-ts/core": "0.5.11", diff --git a/src/agent/CloudAgent.ts b/src/agent/CloudAgent.ts index 372ad6e..ae8abfe 100644 --- a/src/agent/CloudAgent.ts +++ b/src/agent/CloudAgent.ts @@ -46,6 +46,10 @@ export interface CloudAgentOptions { dependencies: AgentDependencies messagePickupRepositoryWebSocketUrl?: string messagePickupMaxReceiveBytes?: number + postgresUser?: string + postgresPassword?: string + postgresHost?: string + postgresMessagePickupDatabaseName?: string } export const createCloudAgent = ( diff --git a/src/agent/initCloudAgent.ts b/src/agent/initCloudAgent.ts index fab9c4a..60320b4 100644 --- a/src/agent/initCloudAgent.ts +++ b/src/agent/initCloudAgent.ts @@ -26,6 +26,7 @@ import { MediationStateChangedEvent, RoutingEventTypes, HangupMessage, + MessagePickupRepository, } from '@credo-ts/core' import WebSocket from 'ws' import { Socket } from 'net' @@ -42,16 +43,37 @@ import { InMemoryMessagePickupRepository } from '../storage/InMemoryMessagePicku import { LocalFcmNotificationSender } from '../notifications/LocalFcmNotificationSender' import { MessagePickupRepositoryClient } from '@2060.io/message-pickup-repository-client' import { ConnectionInfo } from '@2060.io/message-pickup-repository-client/build/interfaces' +import { PostgresMessagePickupRepository } from '@2060.io/credo-ts-message-pickup-repository-pg' export const initCloudAgent = async (config: CloudAgentOptions) => { const logger = config.config.logger ?? new ConsoleLogger(LogLevel.off) const publicDid = config.did - const messageRepository = config.messagePickupRepositoryWebSocketUrl - ? new MessagePickupRepositoryClient({ + const createMessagePickupRepository = (): MessagePickupRepository => { + if (config.messagePickupRepositoryWebSocketUrl) { + return new MessagePickupRepositoryClient({ url: config.messagePickupRepositoryWebSocketUrl, }) - : new InMemoryMessagePickupRepository(new LocalFcmNotificationSender(logger), logger) + } else if (config.postgresHost) { + const { postgresUser, postgresPassword, postgresHost } = config + + if (!postgresUser || !postgresPassword) { + throw new Error( + '[createMessagePickupRepository] Both postgresUser and postgresPassword are required when using PostgresMessagePickupRepository.' + ) + } + return new PostgresMessagePickupRepository({ + logger: logger, + postgresUser, + postgresPassword, + postgresHost, + }) + } else { + return new InMemoryMessagePickupRepository(new LocalFcmNotificationSender(logger), logger) + } + } + + const messageRepository = createMessagePickupRepository() if (!config.enableHttp && !config.enableWs) { throw new Error('No transport has been enabled. Set at least one of HTTP and WS') @@ -93,6 +115,29 @@ export const initCloudAgent = async (config: CloudAgentOptions) => { }) } else if (messageRepository instanceof InMemoryMessagePickupRepository) { messageRepository.setAgent(agent) + } else if (messageRepository instanceof PostgresMessagePickupRepository) { + // Define function that use to send push notification. + + const localFcmNotificationSender = new LocalFcmNotificationSender(logger) + + const connectionInfoCallback = async (connectionId: string) => { + const connectionRecord = await agent.connections.findById(connectionId) + + const token = connectionRecord?.getTag('device_token') as string | null + + return { + sendPushNotification: + token && localFcmNotificationSender.isInitialized() + ? async (messageId: string) => { + await localFcmNotificationSender.sendMessage(token, messageId) + } + : undefined, + } + } + await messageRepository.initialize({ + agent, + connectionInfoCallback, + }) } const app = express() diff --git a/src/config/constants.ts b/src/config/constants.ts index f2570b5..65539f6 100644 --- a/src/config/constants.ts +++ b/src/config/constants.ts @@ -21,6 +21,11 @@ export const POSTGRES_USER = process.env.POSTGRES_USER export const POSTGRES_PASSWORD = process.env.POSTGRES_PASSWORD export const POSTGRES_ADMIN_USER = process.env.POSTGRES_ADMIN_USER export const POSTGRES_ADMIN_PASSWORD = process.env.POSTGRES_ADMIN_PASSWORD +export const MPR_POSTGRES_DATABASE_NAME = process.env.MPR_POSTGRES_DATABASE_NAME + +//FIREBASE CONFIG FILE + +export const FIREBASE_CFG_FILE = process.env.FIREBASE_CFG_FILE || './firebase-cfg.json' // Message Pickup Repository Client export const MPR_WS_URL = process.env.MPR_WS_URL diff --git a/src/index.ts b/src/index.ts index 3632b01..4418f9a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -16,6 +16,9 @@ import { WS_SUPPORT, MPR_WS_URL, MPR_MAX_RECEIVE_BYTES, + POSTGRES_PASSWORD, + POSTGRES_USER, + MPR_POSTGRES_DATABASE_NAME, } from './config/constants' import { askarPostgresConfig, keyDerivationMethodMap } from './config/wallet' @@ -45,6 +48,10 @@ async function run() { dependencies: agentDependencies, messagePickupRepositoryWebSocketUrl: MPR_WS_URL, messagePickupMaxReceiveBytes: MPR_MAX_RECEIVE_BYTES, + postgresUser: POSTGRES_USER, + postgresPassword: POSTGRES_PASSWORD, + postgresHost: POSTGRES_HOST, + postgresMessagePickupDatabaseName: MPR_POSTGRES_DATABASE_NAME, }) } catch (error) { logger.error(`${error}`) diff --git a/src/notifications/FcmNotificationSender.ts b/src/notifications/FcmNotificationSender.ts index c4b3283..55aba32 100644 --- a/src/notifications/FcmNotificationSender.ts +++ b/src/notifications/FcmNotificationSender.ts @@ -1,3 +1,4 @@ export interface FcmNotificationSender { sendMessage(registrationToken: string, messageId: string): Promise | boolean + isInitialized(): boolean } diff --git a/src/notifications/LocalFcmNotificationSender.ts b/src/notifications/LocalFcmNotificationSender.ts index 7707c57..9882ba6 100644 --- a/src/notifications/LocalFcmNotificationSender.ts +++ b/src/notifications/LocalFcmNotificationSender.ts @@ -4,20 +4,36 @@ import { getMessaging } from 'firebase-admin/messaging' import path from 'path' import { Logger } from '@credo-ts/core' import { FcmNotificationSender } from './FcmNotificationSender' +import { FIREBASE_CFG_FILE } from '../config/constants' export class LocalFcmNotificationSender implements FcmNotificationSender { - private fcmApp: FcmApp + private fcmApp: FcmApp | null = null private logger: Logger public constructor(logger: Logger) { - this.fcmApp = initializeApp({ - credential: credential.cert(path.resolve(__dirname, '../../firebase-cfg.json')), - }) this.logger = logger + + try { + const configPath = path.resolve(__dirname, FIREBASE_CFG_FILE) + this.fcmApp = initializeApp({ + credential: credential.cert(configPath), + }) + this.logger.debug('[LocalFcmNotificationSender] Firebase-admin initialized successfully') + } catch (error) { + this.logger.warn( + '[LocalFcmNotificationSender] Failed to initialize Firebase Admin. Notifications will be disabled:', + error.message + ) + this.fcmApp = null + } } public async sendMessage(registrationToken: string, messageId: string) { try { + if (!this.fcmApp) { + this.logger.warn('Firebase Admin is not initialized. Skipping notification.') + return false + } const response = await getMessaging(this.fcmApp).send({ token: registrationToken, notification: { @@ -42,4 +58,8 @@ export class LocalFcmNotificationSender implements FcmNotificationSender { return false } } + + public isInitialized(): boolean { + return this.fcmApp !== null + } } diff --git a/src/storage/InMemoryMessagePickupRepository.ts b/src/storage/InMemoryMessagePickupRepository.ts index d17c602..32a0ed2 100644 --- a/src/storage/InMemoryMessagePickupRepository.ts +++ b/src/storage/InMemoryMessagePickupRepository.ts @@ -22,7 +22,7 @@ export class InMemoryMessagePickupRepository implements MessagePickupRepository private logger?: Logger private messages: InMemoryQueuedMessage[] private agent?: CloudAgent - private notificationSender: FcmNotificationSender + private notificationSender: FcmNotificationSender | undefined public constructor(notificationSender: FcmNotificationSender, logger?: Logger) { this.logger = logger @@ -82,7 +82,7 @@ export class InMemoryMessagePickupRepository implements MessagePickupRepository if (token) { this.logger?.info(`[CustomMessageRepository] Send notification for connection ${connectionId}`) - await this.notificationSender.sendMessage(token, 'messageId') + if (this.notificationSender) await this.notificationSender.sendMessage(token, 'messageId') } } diff --git a/yarn.lock b/yarn.lock index 9503782..e9b33e5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,6 +2,17 @@ # yarn lockfile v1 +"@2060.io/credo-ts-message-pickup-repository-pg@^0.0.7": + version "0.0.7" + resolved "https://registry.yarnpkg.com/@2060.io/credo-ts-message-pickup-repository-pg/-/credo-ts-message-pickup-repository-pg-0.0.7.tgz#41998c44ec77432bf7be9e439fccc64217c86c20" + integrity sha512-95Lg+JgXRTW9NmCLVmiWw4bhBNFDIWOinYG2nRMnktFdXnQxjy1UzKjJeQO+Ido+ltu2GTfyzM4W0Q7cgzXBVw== + dependencies: + "@credo-ts/core" "^0.5.11" + loglevel "^1.8.0" + pg "^8.11.3" + pg-pubsub "^0.8.1" + typescript "^4.0.0" + "@2060.io/ffi-napi@^4.0.9": version "4.0.9" resolved "https://registry.yarnpkg.com/@2060.io/ffi-napi/-/ffi-napi-4.0.9.tgz#194fca2132932ba02e62d716c786d20169b20b8d" @@ -372,7 +383,7 @@ rxjs "^7.8.0" tsyringe "^4.8.0" -"@credo-ts/core@0.5.11", "@credo-ts/core@^0.5.10": +"@credo-ts/core@0.5.11", "@credo-ts/core@^0.5.10", "@credo-ts/core@^0.5.11": version "0.5.11" resolved "https://registry.yarnpkg.com/@credo-ts/core/-/core-0.5.11.tgz#529826628cafbac9a4dc1bf1425b3e21e4c394f3" integrity sha512-mf/Y5eEf3llGrwJ2VUdlhyxAnBuQtFdBHz2NFFr8FhIHbd8TDzJM0Jl9f+n33uf83WV2nKB/O1VROj+II5JGzQ==