import { SzEmailConnector } from './email/classes.emailconnector.js'; import { SzSmsConnector } from './email/classes.smsconnector.js'; import { SzPushNotificationConnector } from './email/classes.pushnotificationconnector.js'; import { SzLetterConnector } from './email/classes.letterconnector.js'; import { SzWebPushConnector } from './webpush/classes.webpushconnector.js'; import { SzCoreMailConnector, type ISzCoreMailConnectorOptions, } from './coremail/classes.coremailconnector.js'; import * as plugins from './plugins.js'; export interface ISzPlatformClientOptions { authorizationString?: string; authorization?: string; token?: string; url?: string; platformUrl?: string; binding?: plugins.servezoneInterfaces.platform.IPlatformBinding; bindings?: plugins.servezoneInterfaces.platform.IPlatformBinding[]; coreMail?: ISzCoreMailConnectorOptions; } export class SzPlatformClient { public debugMode = false; private authorizationString?: string; private connectionAddress?: string; public platformBindings: plugins.servezoneInterfaces.platform.IPlatformBinding[] = []; public typedrouter: plugins.typedrequest.TypedRouter = new plugins.typedrequest.TypedRouter(); public typedsocket!: plugins.typedsocket.TypedSocket; private qenvInstance = new plugins.qenv.Qenv(); private stopped = false; private initPromise?: Promise; private stopPromise?: Promise; private readonly platformSocketAbortController = new AbortController(); public emailConnector = new SzEmailConnector(this); public smsConnector = new SzSmsConnector(this); public pushNotificationConnector = new SzPushNotificationConnector(this); public webPushConnector = new SzWebPushConnector(this); public letterConnector = new SzLetterConnector(this); public coreMailConnector = new SzCoreMailConnector(); constructor(authorizationStringArg?: string | ISzPlatformClientOptions) { this.applyOptions(authorizationStringArg); } public init( authorizationStringArg?: string | ISzPlatformClientOptions, ): Promise { if (this.stopped) { return Promise.reject(this.createAbortError()); } if (this.initPromise) { return this.initPromise; } this.applyOptions(authorizationStringArg); this.initPromise = this.initInner(); return this.initPromise; } private async initInner(): Promise { if (!this.authorizationString) { this.authorizationString = await this.getConfiguredAuthorizationString(); } if (this.authorizationString === 'test') { this.activateDebugMode(); return; } this.platformBindings = this.mergePlatformBindings( this.platformBindings, await this.discoverPlatformBindings() ); if (!this.authorizationString && this.coreMailConnector.isConfigured) { await this.coreMailConnector.init(); this.assertRunning(); return; } if (!this.authorizationString) throw new Error('authorizationString is required'); const socket = await plugins.typedsocket.TypedSocket.createClient( this.typedrouter as any, await this.getConnectionAddress(), { abortSignal: this.platformSocketAbortController.signal }, ); if (this.stopped) { await socket.stop().catch(() => undefined); throw this.createAbortError(); } this.typedsocket = socket; try { await this.setAuthorizationTag(); this.assertRunning(); if (this.coreMailConnector.isConfigured) { await this.coreMailConnector.init(); this.assertRunning(); } } catch (error) { await socket.stop().catch(() => undefined); throw error; } } private activateDebugMode() { this.debugMode = true; console.log('debug mode activated.'); } public getPlatformBinding( capabilityArg?: plugins.servezoneInterfaces.platform.TPlatformCapability ) { return this.platformBindings.find((bindingArg) => { return ( bindingArg.desiredState !== 'disabled' && bindingArg.status !== 'failed' && (!capabilityArg || bindingArg.capability === capabilityArg) ); }); } private applyOptions(optionsArg?: string | ISzPlatformClientOptions) { if (!optionsArg) { return; } if (typeof optionsArg === 'string') { this.authorizationString = optionsArg; return; } this.authorizationString = optionsArg.authorizationString || optionsArg.authorization || optionsArg.token || this.authorizationString; this.connectionAddress = optionsArg.url || optionsArg.platformUrl || this.connectionAddress; this.platformBindings = this.mergePlatformBindings( this.platformBindings, [optionsArg.binding, ...(optionsArg.bindings || [])].filter( Boolean ) as plugins.servezoneInterfaces.platform.IPlatformBinding[] ); if (optionsArg.coreMail) { this.coreMailConnector.configure(optionsArg.coreMail); } } private async getConnectionAddress() { const connectionAddress = this.connectionAddress || (await this.getConfiguredEnvVar('SERVEZONE_PLATFORM_URL')) || this.getConnectionAddressFromBindings(); if (!connectionAddress) throw new Error('SERVEZONE_PLATFORM_URL or platform binding endpoint is required'); return connectionAddress; } private getConnectionAddressFromBindings() { for (const binding of this.platformBindings) { if (binding.desiredState === 'disabled' || binding.status === 'failed') { continue; } if (!this.isSupportedConnectorCapability(binding.capability)) { continue; } const endpoint = binding.endpoints?.find((endpointArg) => { return endpointArg.protocol === 'typedrequest' || endpointArg.protocol === 'http'; }); const endpointUrl = endpoint?.internalUrl || endpoint?.externalUrl; if (endpointUrl) { return endpointUrl; } } } private isSupportedConnectorCapability( capabilityArg: plugins.servezoneInterfaces.platform.TPlatformCapability ) { return ['email', 'sms', 'pushnotification', 'letter'].includes(capabilityArg); } private async getConfiguredAuthorizationString() { return ( (await this.getConfiguredEnvVar('SERVEZONE_PLATFORM_AUTHORIZATION')) || (await this.getConfiguredEnvVar('SERVEZONE_PLATFORM_TOKEN')) ); } private async setAuthorizationTag() { if (!this.authorizationString) { return; } try { await this.typedsocket.setTag('authorization' as any, this.authorizationString); } catch (error) { if (await this.getConfiguredEnvVar('SERVEZONE_PLATFORMCLIENT_DEBUG')) { console.warn('Could not set platform authorization tag:', (error as Error).message); } } } public stop(): Promise { this.stopPromise ??= this.stopInner(); return this.stopPromise; } private async stopInner(): Promise { this.stopped = true; this.platformSocketAbortController.abort(this.createAbortError()); const socket = this.typedsocket; await Promise.allSettled([ this.emailConnector.stop(), this.webPushConnector.stop(), this.coreMailConnector.stop(), socket?.stop(), this.initPromise, ]); if (this.typedsocket && this.typedsocket !== socket) { await this.typedsocket.stop().catch(() => undefined); } } private assertRunning(): void { if (this.stopped) { throw this.createAbortError(); } } private createAbortError(): Error { const error = new Error('Platform client operation aborted.'); error.name = 'AbortError'; return error; } private async discoverPlatformBindings() { const bindings: plugins.servezoneInterfaces.platform.IPlatformBinding[] = []; const envBindings = await this.getConfiguredEnvVar('SERVEZONE_PLATFORM_BINDINGS'); const envBinding = await this.getConfiguredEnvVar('SERVEZONE_PLATFORM_BINDING'); if (envBindings) { bindings.push(...this.parsePlatformBindingsEnv('SERVEZONE_PLATFORM_BINDINGS', envBindings)); } if (envBinding) { bindings.push(...this.parsePlatformBindingsEnv('SERVEZONE_PLATFORM_BINDING', envBinding)); } return bindings; } private parsePlatformBindingsEnv(envNameArg: string, envValueArg: string) { let parsedValue: unknown; try { parsedValue = JSON.parse(envValueArg); } catch (error) { throw new Error(`${envNameArg} must contain valid JSON`); } if (Array.isArray(parsedValue)) { return parsedValue as plugins.servezoneInterfaces.platform.IPlatformBinding[]; } if (parsedValue && typeof parsedValue === 'object') { return [parsedValue as plugins.servezoneInterfaces.platform.IPlatformBinding]; } throw new Error(`${envNameArg} must contain a platform binding object or array`); } private mergePlatformBindings( existingBindingsArg: plugins.servezoneInterfaces.platform.IPlatformBinding[], newBindingsArg: plugins.servezoneInterfaces.platform.IPlatformBinding[] ) { const bindingMap = new Map(); for (const binding of [...existingBindingsArg, ...newBindingsArg]) { bindingMap.set(binding.id, binding); } return Array.from(bindingMap.values()); } public async getConfiguredEnvVar(envNameArg: string) { const processValue = this.getProcessEnvVar(envNameArg); if (processValue) { return processValue; } return this.qenvInstance.getEnvVarOnDemand(envNameArg); } private getProcessEnvVar(envNameArg: string) { const processEnv = (globalThis as typeof globalThis & { process?: { env?: Record }; }).process?.env; return processEnv?.[envNameArg]; } }