diff --git a/changelog.md b/changelog.md index 05313ea..6ced8e8 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,12 @@ # Changelog +## 2026-04-28 - 1.1.3 - feat(platformclient) +Add runtime platform binding configuration. + +- Added `SERVEZONE_PLATFORM_URL`, `SERVEZONE_PLATFORM_AUTHORIZATION`, and `SERVEZONE_PLATFORM_TOKEN` support. +- Added environment binding discovery through `SERVEZONE_PLATFORM_BINDING` and `SERVEZONE_PLATFORM_BINDINGS`. +- Preserved `SERVEZONE_API_DOMAIN` and `SERVEZONE_PLATFROM_AUTHORIZATION` fallbacks. + ## 2024-10-05 - 1.1.2 - fix(core) Fix authorization handling and format code. @@ -71,4 +78,4 @@ Maintenance updates ## 2024-02-17 - 1.0.1 to 1.0.3 - General Updates Maintenance updates -- Fix(core): update \ No newline at end of file +- Fix(core): update diff --git a/package.json b/package.json index a5ac9ac..c82313f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@serve.zone/platformclient", - "version": "1.1.2", + "version": "1.1.3", "private": false, "description": "a module that makes it really easy to use the serve.zone platform inside your app", "main": "dist_ts/index.js", @@ -27,7 +27,7 @@ "@push.rocks/qenv": "^6.1.3", "@push.rocks/smartlog": "^3.2.2", "@push.rocks/smartntml": "^2.0.8", - "@serve.zone/interfaces": "^5.4.4" + "@serve.zone/interfaces": "^5.4.5" }, "repository": { "type": "git", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cf84f17..9cde31b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -27,8 +27,8 @@ importers: specifier: ^2.0.8 version: 2.0.8 '@serve.zone/interfaces': - specifier: ^5.4.4 - version: 5.4.4 + specifier: ^5.4.5 + version: 5.4.5 devDependencies: '@git.zone/tsbuild': specifier: ^4.4.0 @@ -1447,8 +1447,8 @@ packages: '@sec-ant/readable-stream@0.4.1': resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} - '@serve.zone/interfaces@5.4.4': - resolution: {integrity: sha512-0mcLvZBGHOFKG8PkpMieHo19lbPLJIpDIgKUSbuqfqOzSYymS8BLYftCr8Kw7ddl61kICZoy/m09U+TtxZbxBg==} + '@serve.zone/interfaces@5.4.5': + resolution: {integrity: sha512-asqUUjem3MGfIbseovHR8SxE+6FvjeQEYtV+PxcyY8YRXJ/vE3hNCDs7ePXgBbh4JXa+vNMaXHsFfz5Vrk6Ggg==} '@sindresorhus/is@5.6.0': resolution: {integrity: sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==} @@ -6338,7 +6338,7 @@ snapshots: '@sec-ant/readable-stream@0.4.1': {} - '@serve.zone/interfaces@5.4.4': + '@serve.zone/interfaces@5.4.5': dependencies: '@api.global/typedrequest-interfaces': 3.0.19 '@push.rocks/smartlog-interfaces': 3.0.2 diff --git a/readme.md b/readme.md index 0af1353..404f9bf 100644 --- a/readme.md +++ b/readme.md @@ -43,7 +43,7 @@ await platformClient.emailConnector.sendEmail({ }); ``` -The client connects to the platform endpoint configured through `SERVEZONE_API_DOMAIN`. +The client connects to the platform endpoint configured through `SERVEZONE_PLATFORM_URL`, a runtime platform binding, or the legacy `SERVEZONE_API_DOMAIN` fallback. ## Configuration @@ -51,14 +51,25 @@ The client needs two values: | Value | How to provide it | Notes | | --- | --- | --- | -| Authorization token | Constructor argument, `init()` argument, or `SERVEZONE_PLATFROM_AUTHORIZATION` | The current environment variable name is intentionally documented as implemented. | -| API domain | `SERVEZONE_API_DOMAIN` | Loaded on demand through `@push.rocks/qenv`. | +| Authorization token | Constructor argument, `init()` argument, `SERVEZONE_PLATFORM_AUTHORIZATION`, `SERVEZONE_PLATFORM_TOKEN`, binding credential env, or `SERVEZONE_PLATFROM_AUTHORIZATION` | The misspelled legacy env name remains supported for compatibility. | +| Platform URL | Constructor options, `SERVEZONE_PLATFORM_URL`, binding endpoint, or `SERVEZONE_API_DOMAIN` | Loaded on demand through `@push.rocks/qenv`. | +| Runtime bindings | Constructor options, `SERVEZONE_PLATFORM_BINDING`, or `SERVEZONE_PLATFORM_BINDINGS` | Values are JSON-encoded `IPlatformBinding` objects from `@serve.zone/interfaces`. | ```typescript const client = new SzPlatformClient(); await client.init('your-platform-authorization-token'); ``` +Constructor and `init()` also accept options: + +```typescript +const client = new SzPlatformClient({ + token: process.env.SERVEZONE_PLATFORM_TOKEN, + platformUrl: process.env.SERVEZONE_PLATFORM_URL, +}); +await client.init(); +``` + ## Debug Mode Pass `test` as the authorization string to enable debug mode. In debug mode, the client does not open a TypedSocket connection and connector methods log or return deterministic test values instead of sending real platform requests. diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index 4e5ec59..2e3cd7e 100644 --- a/ts/00_commitinfo_data.ts +++ b/ts/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@serve.zone/platformclient', - version: '1.1.2', + version: '1.1.3', description: 'a module that makes it really easy to use the serve.zone platform inside your app' } diff --git a/ts/classes.platformclient.ts b/ts/classes.platformclient.ts index 717bb6f..cd7a949 100644 --- a/ts/classes.platformclient.ts +++ b/ts/classes.platformclient.ts @@ -4,9 +4,21 @@ import { SzPushNotificationConnector } from './email/classes.pushnotificationcon import { SzLetterConnector } from './email/classes.letterconnector.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[]; +} + 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(); @@ -16,31 +28,209 @@ export class SzPlatformClient { public pushNotificationConnector = new SzPushNotificationConnector(this); public letterConnector = new SzLetterConnector(this); - constructor(authorizationStringArg?: string) { - this.authorizationString = authorizationStringArg; + constructor(authorizationStringArg?: string | ISzPlatformClientOptions) { + this.applyOptions(authorizationStringArg); } - public async init(authorizationStringArg?: string) { - if (authorizationStringArg) { - this.authorizationString = authorizationStringArg; + public async init(authorizationStringArg?: string | ISzPlatformClientOptions) { + this.applyOptions(authorizationStringArg); + 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.authorizationString = this.getAuthorizationStringFromBindings(); } - if (!this.authorizationString) - this.authorizationString = process.env.SERVEZONE_PLATFROM_AUTHORIZATION; if (!this.authorizationString) throw new Error('authorizationString is required'); if (this.authorizationString === 'test') { - this.debugMode = true; - console.log('debug mode activated.'); + this.activateDebugMode(); return; } this.typedsocket = await plugins.typedsocket.TypedSocket.createClient( - this.typedrouter, + this.typedrouter as any, await this.getConnectionAddress() ); + await this.setAuthorizationTag(); + } + + 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[] + ); } private async getConnectionAddress() { - const connectionAddress = await this.qenvInstance.getEnvVarOnDemand('SERVEZONE_API_DOMAIN'); - if (!connectionAddress) throw new Error('SERVEZONE_API_DOMAIN is required'); + const connectionAddress = + this.connectionAddress || + (await this.getConfiguredEnvVar('SERVEZONE_PLATFORM_URL')) || + this.getConnectionAddressFromBindings() || + (await this.getConfiguredEnvVar('SERVEZONE_API_DOMAIN')); + if (!connectionAddress) throw new Error('SERVEZONE_PLATFORM_URL or SERVEZONE_API_DOMAIN 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')) || + (await this.getConfiguredEnvVar('SERVEZONE_PLATFROM_AUTHORIZATION')) + ); + } + + private getAuthorizationStringFromBindings() { + const authEnvNames = [ + 'SERVEZONE_PLATFORM_AUTHORIZATION', + 'SERVEZONE_PLATFORM_TOKEN', + 'SERVEZONE_API_TOKEN', + 'AUTHORIZATION', + 'TOKEN', + ]; + for (const binding of this.platformBindings) { + for (const credential of binding.credentials || []) { + for (const envName of authEnvNames) { + const value = credential.env?.[envName]; + if (value) { + return value; + } + } + } + } + } + + 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); + } + } + } + + 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()); + } + + private 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]; + } }