feat: add platform binding runtime config
This commit is contained in:
@@ -1,5 +1,12 @@
|
|||||||
# Changelog
|
# 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)
|
## 2024-10-05 - 1.1.2 - fix(core)
|
||||||
Fix authorization handling and format code.
|
Fix authorization handling and format code.
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@serve.zone/platformclient",
|
"name": "@serve.zone/platformclient",
|
||||||
"version": "1.1.2",
|
"version": "1.1.3",
|
||||||
"private": false,
|
"private": false,
|
||||||
"description": "a module that makes it really easy to use the serve.zone platform inside your app",
|
"description": "a module that makes it really easy to use the serve.zone platform inside your app",
|
||||||
"main": "dist_ts/index.js",
|
"main": "dist_ts/index.js",
|
||||||
@@ -27,7 +27,7 @@
|
|||||||
"@push.rocks/qenv": "^6.1.3",
|
"@push.rocks/qenv": "^6.1.3",
|
||||||
"@push.rocks/smartlog": "^3.2.2",
|
"@push.rocks/smartlog": "^3.2.2",
|
||||||
"@push.rocks/smartntml": "^2.0.8",
|
"@push.rocks/smartntml": "^2.0.8",
|
||||||
"@serve.zone/interfaces": "^5.4.4"
|
"@serve.zone/interfaces": "^5.4.5"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|||||||
Generated
+5
-5
@@ -27,8 +27,8 @@ importers:
|
|||||||
specifier: ^2.0.8
|
specifier: ^2.0.8
|
||||||
version: 2.0.8
|
version: 2.0.8
|
||||||
'@serve.zone/interfaces':
|
'@serve.zone/interfaces':
|
||||||
specifier: ^5.4.4
|
specifier: ^5.4.5
|
||||||
version: 5.4.4
|
version: 5.4.5
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@git.zone/tsbuild':
|
'@git.zone/tsbuild':
|
||||||
specifier: ^4.4.0
|
specifier: ^4.4.0
|
||||||
@@ -1447,8 +1447,8 @@ packages:
|
|||||||
'@sec-ant/readable-stream@0.4.1':
|
'@sec-ant/readable-stream@0.4.1':
|
||||||
resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==}
|
resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==}
|
||||||
|
|
||||||
'@serve.zone/interfaces@5.4.4':
|
'@serve.zone/interfaces@5.4.5':
|
||||||
resolution: {integrity: sha512-0mcLvZBGHOFKG8PkpMieHo19lbPLJIpDIgKUSbuqfqOzSYymS8BLYftCr8Kw7ddl61kICZoy/m09U+TtxZbxBg==}
|
resolution: {integrity: sha512-asqUUjem3MGfIbseovHR8SxE+6FvjeQEYtV+PxcyY8YRXJ/vE3hNCDs7ePXgBbh4JXa+vNMaXHsFfz5Vrk6Ggg==}
|
||||||
|
|
||||||
'@sindresorhus/is@5.6.0':
|
'@sindresorhus/is@5.6.0':
|
||||||
resolution: {integrity: sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==}
|
resolution: {integrity: sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==}
|
||||||
@@ -6338,7 +6338,7 @@ snapshots:
|
|||||||
|
|
||||||
'@sec-ant/readable-stream@0.4.1': {}
|
'@sec-ant/readable-stream@0.4.1': {}
|
||||||
|
|
||||||
'@serve.zone/interfaces@5.4.4':
|
'@serve.zone/interfaces@5.4.5':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@api.global/typedrequest-interfaces': 3.0.19
|
'@api.global/typedrequest-interfaces': 3.0.19
|
||||||
'@push.rocks/smartlog-interfaces': 3.0.2
|
'@push.rocks/smartlog-interfaces': 3.0.2
|
||||||
|
|||||||
@@ -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
|
## Configuration
|
||||||
|
|
||||||
@@ -51,14 +51,25 @@ The client needs two values:
|
|||||||
|
|
||||||
| Value | How to provide it | Notes |
|
| 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. |
|
| 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. |
|
||||||
| API domain | `SERVEZONE_API_DOMAIN` | Loaded on demand through `@push.rocks/qenv`. |
|
| 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
|
```typescript
|
||||||
const client = new SzPlatformClient();
|
const client = new SzPlatformClient();
|
||||||
await client.init('your-platform-authorization-token');
|
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
|
## 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.
|
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.
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@serve.zone/platformclient',
|
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'
|
description: 'a module that makes it really easy to use the serve.zone platform inside your app'
|
||||||
}
|
}
|
||||||
|
|||||||
+202
-12
@@ -4,9 +4,21 @@ import { SzPushNotificationConnector } from './email/classes.pushnotificationcon
|
|||||||
import { SzLetterConnector } from './email/classes.letterconnector.js';
|
import { SzLetterConnector } from './email/classes.letterconnector.js';
|
||||||
import * as plugins from './plugins.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 {
|
export class SzPlatformClient {
|
||||||
public debugMode = false;
|
public debugMode = false;
|
||||||
private authorizationString?: string;
|
private authorizationString?: string;
|
||||||
|
private connectionAddress?: string;
|
||||||
|
public platformBindings: plugins.servezoneInterfaces.platform.IPlatformBinding[] = [];
|
||||||
public typedrouter: plugins.typedrequest.TypedRouter = new plugins.typedrequest.TypedRouter();
|
public typedrouter: plugins.typedrequest.TypedRouter = new plugins.typedrequest.TypedRouter();
|
||||||
public typedsocket!: plugins.typedsocket.TypedSocket;
|
public typedsocket!: plugins.typedsocket.TypedSocket;
|
||||||
private qenvInstance = new plugins.qenv.Qenv();
|
private qenvInstance = new plugins.qenv.Qenv();
|
||||||
@@ -16,31 +28,209 @@ export class SzPlatformClient {
|
|||||||
public pushNotificationConnector = new SzPushNotificationConnector(this);
|
public pushNotificationConnector = new SzPushNotificationConnector(this);
|
||||||
public letterConnector = new SzLetterConnector(this);
|
public letterConnector = new SzLetterConnector(this);
|
||||||
|
|
||||||
constructor(authorizationStringArg?: string) {
|
constructor(authorizationStringArg?: string | ISzPlatformClientOptions) {
|
||||||
this.authorizationString = authorizationStringArg;
|
this.applyOptions(authorizationStringArg);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async init(authorizationStringArg?: string) {
|
public async init(authorizationStringArg?: string | ISzPlatformClientOptions) {
|
||||||
if (authorizationStringArg) {
|
this.applyOptions(authorizationStringArg);
|
||||||
this.authorizationString = 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) throw new Error('authorizationString is required');
|
||||||
if (this.authorizationString === 'test') {
|
if (this.authorizationString === 'test') {
|
||||||
this.debugMode = true;
|
this.activateDebugMode();
|
||||||
console.log('debug mode activated.');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.typedsocket = await plugins.typedsocket.TypedSocket.createClient(
|
this.typedsocket = await plugins.typedsocket.TypedSocket.createClient(
|
||||||
this.typedrouter,
|
this.typedrouter as any,
|
||||||
await this.getConnectionAddress()
|
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() {
|
private async getConnectionAddress() {
|
||||||
const connectionAddress = await this.qenvInstance.getEnvVarOnDemand('SERVEZONE_API_DOMAIN');
|
const connectionAddress =
|
||||||
if (!connectionAddress) throw new Error('SERVEZONE_API_DOMAIN is required');
|
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;
|
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<string, plugins.servezoneInterfaces.platform.IPlatformBinding>();
|
||||||
|
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<string, string | undefined> };
|
||||||
|
}).process?.env;
|
||||||
|
return processEnv?.[envNameArg];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user