diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ab67c4..5eefe30 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +# 8.3.2 + +## New Features + +_New shiny stuff_ + +- Added support for providing a value for a `scope` field in the OAuth request. This can be set with environment variable `ZEEBE_TOKEN_SCOPE`, or by passing a `scope` field as part of the `oAuth` config options for a `ZBClient`. This is needed to support OIDC / EntraID. Thanks to [@nikku](https://github.com/nikku) for the implementation. See PR [#363](https://github.com/camunda-community-hub/zeebe-client-node-js/pull/363) for more details. + # 8.3.1 ## New Features diff --git a/README.md b/README.md index 8b05bde..8ec36e0 100644 --- a/README.md +++ b/README.md @@ -485,6 +485,7 @@ const zbc = new ZBClient("my-secure-broker.io:443", { oAuth: { url: "https://your-auth-endpoint/oauth/token", audience: "my-secure-broker.io", + scope: "myScope", clientId: "myClientId", clientSecret: "randomClientSecret", customRootCert: fs.readFileSync('./my_CA.pem'), @@ -599,6 +600,7 @@ Self-hosted or local broker with OAuth + TLS: ZEEBE_CLIENT_ID ZEEBE_CLIENT_SECRET ZEEBE_TOKEN_AUDIENCE +ZEEBE_TOKEN_SCOPE ZEEBE_AUTHORIZATION_SERVER_URL ZEEBE_ADDRESS ``` @@ -613,6 +615,7 @@ ZEEBE_CLIENT_ID='zeebe' ZEEBE_CLIENT_SECRET='zecret' ZEEBE_AUTHORIZATION_SERVER_URL='http://localhost:18080/auth/realms/camunda-platform/protocol/openid-connect/token' ZEEBE_TOKEN_AUDIENCE='zeebe.camunda.io' +ZEEBE_TOKEN_SCOPE='not needed' CAMUNDA_CREDENTIALS_SCOPES='Zeebe' CAMUNDA_OAUTH_URL='http://localhost:18080/auth/realms/camunda-platform/protocol/openid-connect/token' ``` diff --git a/package-lock.json b/package-lock.json index a0d72c8..e1dc16a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,7 +32,7 @@ "@types/debug": "0.0.31", "@types/got": "^9.6.9", "@types/jest": "^27.5.2", - "@types/node": "^10.17.60", + "@types/node": "^14.17.1", "@types/promise-retry": "^1.1.3", "@types/stack-trace": "0.0.29", "@types/uuid": "^3.4.4", @@ -54,7 +54,7 @@ "tslint": "^6.1.3", "tslint-config-prettier": "^1.18.0", "typedoc": "^0.21.10", - "typescript": "^4.2.0" + "typescript": "^4.4.4" }, "engines": { "node": ">=16.6.1" @@ -838,11 +838,6 @@ "node": "^8.13.0 || >=10.10.0" } }, - "node_modules/@grpc/grpc-js/node_modules/@types/node": { - "version": "18.11.18", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz", - "integrity": "sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==" - }, "node_modules/@grpc/proto-loader": { "version": "0.7.10", "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.10.tgz", @@ -1869,9 +1864,9 @@ } }, "node_modules/@types/node": { - "version": "10.17.60", - "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.60.tgz", - "integrity": "sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==" + "version": "14.18.63", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.63.tgz", + "integrity": "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==" }, "node_modules/@types/parse-json": { "version": "4.0.0", @@ -11152,13 +11147,6 @@ "requires": { "@grpc/proto-loader": "^0.7.8", "@types/node": ">=12.12.47" - }, - "dependencies": { - "@types/node": { - "version": "18.11.18", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz", - "integrity": "sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==" - } } }, "@grpc/proto-loader": { @@ -12002,9 +11990,9 @@ } }, "@types/node": { - "version": "10.17.60", - "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.60.tgz", - "integrity": "sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==" + "version": "14.18.63", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.63.tgz", + "integrity": "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==" }, "@types/parse-json": { "version": "4.0.0", diff --git a/package.json b/package.json index 57e27fd..27772d2 100644 --- a/package.json +++ b/package.json @@ -83,7 +83,7 @@ "@types/debug": "0.0.31", "@types/got": "^9.6.9", "@types/jest": "^27.5.2", - "@types/node": "^10.17.60", + "@types/node": "^14.17.1", "@types/promise-retry": "^1.1.3", "@types/stack-trace": "0.0.29", "@types/uuid": "^3.4.4", @@ -105,7 +105,7 @@ "tslint": "^6.1.3", "tslint-config-prettier": "^1.18.0", "typedoc": "^0.21.10", - "typescript": "^4.2.0" + "typescript": "^4.4.4" }, "author": { "name": "Josh Wulf", diff --git a/src/__tests__/ConfigurationHydrator.spec.ts b/src/__tests__/ConfigurationHydrator.spec.ts index 0eee55c..dd2b65b 100644 --- a/src/__tests__/ConfigurationHydrator.spec.ts +++ b/src/__tests__/ConfigurationHydrator.spec.ts @@ -13,6 +13,7 @@ const ENV_VARS_TO_STORE = [ 'ZEEBE_GATEWAY_ADDRESS', 'ZEEBE_ADDRESS', 'ZEEBE_TOKEN_AUDIENCE', + 'ZEEBE_TOKEN_SCOPE', 'ZEEBE_AUTHORIZATION_SERVER_URL', 'ZEEBE_CLIENT_MAX_RETRIES', 'ZEEBE_CLIENT_RETRY', @@ -20,7 +21,8 @@ const ENV_VARS_TO_STORE = [ 'ZEEBE_CLIENT_SSL_ROOT_CERTS_PATH', 'ZEEBE_CLIENT_SSL_PRIVATE_KEY_PATH', 'ZEEBE_CLIENT_SSL_CERT_CHAIN_PATH', - 'ZEEBE_TENANT_ID' + 'ZEEBE_TENANT_ID', + 'ZEEBE_SECURE_CONNECTION', ] beforeAll(() => { @@ -96,6 +98,29 @@ test('Takes an explicit Gateway address over the environment ZEEBE_GATEWAY_ADDRE expect(conf.port).toBe('26600') }) +/** + * Self-managed + */ +test('Constructs the self-managed connection with oauth credentials', () => { + process.env.ZEEBE_CLIENT_SECRET = 'CLIENT_SECRET' + process.env.ZEEBE_CLIENT_ID = 'CLIENT_ID' + process.env.ZEEBE_GATEWAY_ADDRESS = 'zeebe://my-server:26600' + process.env.ZEEBE_TOKEN_AUDIENCE = 'TOKEN_AUDIENCE' + process.env.ZEEBE_TOKEN_SCOPE = 'TOKEN_SCOPE' + process.env.ZEEBE_AUTHORIZATION_SERVER_URL = 'https://auz' + + const conf = ConfigurationHydrator.configure(undefined, undefined) + + expect(conf.hostname).toBe('my-server') + expect(conf.port).toBe('26600') + expect(conf.oAuth!.audience).toBe('TOKEN_AUDIENCE') + expect(conf.oAuth!.scope).toBe('TOKEN_SCOPE') + expect(conf.oAuth!.clientId).toBe('CLIENT_ID') + expect(conf.oAuth!.audience).toBe('TOKEN_AUDIENCE') + expect(conf.oAuth!.clientSecret).toBe('CLIENT_SECRET') + expect(conf.oAuth!.url).toBe('https://auz') +}) + /** * Camunda Cloud */ @@ -105,7 +130,13 @@ test('Constructs the Camunda Cloud connection from the environment with clusterI process.env.ZEEBE_CLIENT_SECRET = 'WZahIGHjyj0-oQ7DZ_aH2wwNuZt5O8Sq0ZJTz0OaxfO7D6jaDBZxM_Q-BHRsiGO_' process.env.ZEEBE_CLIENT_ID = 'yStuGvJ6a1RQhy8DQpeXJ80yEpar3pXh' + delete process.env.ZEEBE_GATEWAY_ADDRESS + delete process.env.ZEEBE_TOKEN_AUDIENCE + delete process.env.ZEEBE_TOKEN_SCOPE + delete process.env.ZEEBE_AUTHORIZATION_SERVER_URL + delete process.env.ZEEBE_GATEWAY_ADDRESS + // process.env.ZEEBE_GATEWAY_ADDRESS = 'zeebe://localhost:26500' const conf = ConfigurationHydrator.configure(undefined, undefined) expect(conf.hostname).toBe( @@ -447,7 +478,7 @@ describe('Configures secure connection with custom root certs', () => { }) test('Is insecure by default', () => { - delete process.env.ZEEBE_INSECURE_CONNECTION + delete process.env.ZEEBE_SECURE_CONNECTION const conf = ConfigurationHydrator.configure('localhost:26600', {}) expect(conf.useTLS).toBeFalsy() }) @@ -526,13 +557,17 @@ test('Tenant ID is picked up from environment', () => { }) test('Tenant ID is picked up from constructor options', () => { - const conf = ConfigurationHydrator.configure(undefined, {tenantId: 'thisOne'}) + const conf = ConfigurationHydrator.configure(undefined, { + tenantId: 'thisOne', + }) expect(conf.tenantId).toBe('thisOne') }) test('Tenant ID from constructor overrides environment', () => { process.env.ZEEBE_TENANT_ID = 'someId' - const conf = ConfigurationHydrator.configure(undefined, {tenantId: 'thisOne'}) + const conf = ConfigurationHydrator.configure(undefined, { + tenantId: 'thisOne', + }) expect(conf.tenantId).toBe('thisOne') }) diff --git a/src/__tests__/OAuthProvider.spec.ts b/src/__tests__/OAuthProvider.spec.ts index cccd1bd..1071674 100644 --- a/src/__tests__/OAuthProvider.spec.ts +++ b/src/__tests__/OAuthProvider.spec.ts @@ -6,6 +6,8 @@ import { OAuthProvider } from '../lib/OAuthProvider' const STORED_ENV = {} const ENV_VARS_TO_STORE = ['ZEEBE_TOKEN_CACHE_DIR'] +const tokenCache = path.join(__dirname, '.token-cache'); + beforeAll(() => { ENV_VARS_TO_STORE.forEach(e => { STORED_ENV[e] = process.env[e] @@ -13,7 +15,11 @@ beforeAll(() => { }) }) -afterAll(() => { +afterEach(() => { + clearCache(tokenCache); +}); + +afterEach(() => { ENV_VARS_TO_STORE.forEach(e => { delete process.env[e] if (STORED_ENV[e]) { @@ -23,12 +29,6 @@ afterAll(() => { }) test("Creates the token cache dir if it doesn't exist", () => { - const tokenCache = path.join(__dirname, '.token-cache') - if (fs.existsSync(tokenCache)) { - fs.rmdirSync(tokenCache) - } - expect(fs.existsSync(tokenCache)).toBe(false) - const o = new OAuthProvider({ audience: 'token', cacheDir: tokenCache, @@ -39,19 +39,10 @@ test("Creates the token cache dir if it doesn't exist", () => { }) expect(o).toBeTruthy() expect(fs.existsSync(tokenCache)).toBe(true) - if (fs.existsSync(tokenCache)) { - fs.rmdirSync(tokenCache) - } - expect(fs.existsSync(tokenCache)).toBe(false) o.stopExpiryTimer() }) test('Gets the token cache dir from the environment', () => { - const tokenCache = path.join(__dirname, '.token-cache') - if (fs.existsSync(tokenCache)) { - fs.rmdirSync(tokenCache) - } - expect(fs.existsSync(tokenCache)).toBe(false) process.env.ZEEBE_TOKEN_CACHE_DIR = tokenCache const o = new OAuthProvider({ audience: 'token', @@ -62,49 +53,27 @@ test('Gets the token cache dir from the environment', () => { }) expect(o).toBeTruthy() expect(fs.existsSync(tokenCache)).toBe(true) - if (fs.existsSync(tokenCache)) { - fs.rmdirSync(tokenCache) - } - expect(fs.existsSync(tokenCache)).toBe(false) o.stopExpiryTimer() }) test('Uses an explicit token cache over the environment', () => { - const tokenCache1 = path.join(__dirname, '.token-cache1') - const tokenCache2 = path.join(__dirname, '.token-cache2') - ;[tokenCache1, tokenCache2].forEach(tokenCache => { - if (fs.existsSync(tokenCache)) { - fs.rmdirSync(tokenCache) - } - expect(fs.existsSync(tokenCache)).toBe(false) - }) - process.env.ZEEBE_TOKEN_CACHE_DIR = tokenCache1 + const tokenCache_other = path.join(__dirname, '.token-cache2') + process.env.ZEEBE_TOKEN_CACHE_DIR = tokenCache_other const o = new OAuthProvider({ audience: 'token', - cacheDir: tokenCache2, + cacheDir: tokenCache, cacheOnDisk: true, clientId: 'clientId', clientSecret: 'clientSecret', url: 'url', }) expect(o).toBeTruthy() - expect(fs.existsSync(tokenCache2)).toBe(true) - expect(fs.existsSync(tokenCache1)).toBe(false) - ;[tokenCache1, tokenCache2].forEach(tokenCache => { - if (fs.existsSync(tokenCache)) { - fs.rmdirSync(tokenCache) - } - expect(fs.existsSync(tokenCache)).toBe(false) - }) + expect(fs.existsSync(tokenCache)).toBe(true) + expect(fs.existsSync(tokenCache_other)).toBe(false) o.stopExpiryTimer() }) test('Throws in the constructor if the token cache is not writable', () => { - const tokenCache = path.join(__dirname, '.token-cache') - if (fs.existsSync(tokenCache)) { - fs.rmdirSync(tokenCache) - } - expect(fs.existsSync(tokenCache)).toBe(false) fs.mkdirSync(tokenCache, 0o400) expect(fs.existsSync(tokenCache)).toBe(true) let thrown = false @@ -124,10 +93,41 @@ test('Throws in the constructor if the token cache is not writable', () => { thrown = true } expect(thrown).toBe(true) - if (fs.existsSync(tokenCache)) { - fs.rmdirSync(tokenCache) - } - expect(fs.existsSync(tokenCache)).toBe(false) +}) + +test('Send form encoded request', () => { + const o = new OAuthProvider({ + audience: 'token', + cacheOnDisk: false, + clientId: 'clientId', + clientSecret: 'clientSecret', + url: 'http://127.0.0.1:3001/foobar', + }) + const server = http + .createServer((req, res) => { + expect(req.url).toBe('/foobar') + expect(req.method).toBe('POST') + expect(req.headers['user-agent']).toContain('zeebe-client-nodejs/') + + let body = '' + req.on('data', chunk => { + body += chunk + }) + + req.on('end', () => { + expect(body).toEqual( + 'audience=token&client_id=clientId&client_secret=clientSecret&grant_type=client_credentials' + ) + + res.writeHead(200, { 'Content-Type': 'application/json' }) + res.end('{"token": "something"}') + }) + }) + .listen(3001) + return o.getToken().finally(() => { + o.stopExpiryTimer() + return server.close() + }) }) test('Can set a custom user agent', () => { @@ -137,15 +137,37 @@ test('Can set a custom user agent', () => { cacheOnDisk: true, clientId: 'clientId', clientSecret: 'clientSecret', - url: 'url', + url: 'http://127.0.0.1:3002', + }) + const server = http + .createServer((req, res) => { + expect(req.method).toBe('POST') + expect(req.headers['user-agent']).toContain('modeler') + + req.on('data', () => { + // ignoring + }) + + req.on('end', () => { + res.writeHead(200, { 'Content-Type': 'application/json' }) + res.end('{"token": "something"}') + }) + }) + .listen(3002) + + return o.getToken().finally(() => { + o.stopExpiryTimer() + + delete process.env.ZEEBE_CLIENT_CUSTOM_AGENT_STRING + + return server.close() }) - expect(o.userAgentString.includes(' modeler')).toBe(true) - o.stopExpiryTimer() }) -test('Uses form encoding for request', done => { +test('Passes scope, if provided', () => { const o = new OAuthProvider({ audience: 'token', + scope: 'scope', cacheOnDisk: false, clientId: 'clientId', clientSecret: 'clientSecret', @@ -162,21 +184,23 @@ test('Uses form encoding for request', done => { req.on('end', () => { res.writeHead(200, { 'Content-Type': 'application/json' }) res.end('{"token": "something"}') - server.close() + expect(body).toEqual( - 'audience=token&client_id=clientId&client_secret=clientSecret&grant_type=client_credentials' + 'audience=token&client_id=clientId&client_secret=clientSecret&grant_type=client_credentials&scope=scope' ) - done() }) } }) .listen(3001) - o.getToken().then(() => o.stopExpiryTimer()) - expect(o.userAgentString.includes(' modeler')).toBe(true) + return o.getToken().finally(() => { + o.stopExpiryTimer() + + return server.close() + }) }) -test('In-memory cache is populated and evicted after timeout', done => { +test('In-memory cache is populated and evicted after timeout', () => { const delay = timeout => new Promise(res => setTimeout(() => res(null), timeout)) @@ -185,40 +209,48 @@ test('In-memory cache is populated and evicted after timeout', done => { cacheOnDisk: false, clientId: 'clientId', clientSecret: 'clientSecret', - url: 'http://127.0.0.1:3002', + url: 'http://127.0.0.1:3001', }) const server = http .createServer((req, res) => { - if (req.method === 'POST') { - let body = '' - req.on('data', chunk => { - body += chunk - }) + expect(req.method).toBe('POST') - req.on('end', () => { - res.writeHead(200, { 'Content-Type': 'application/json' }) - let expires_in = 2 // seconds - res.end( - '{"access_token": "something", "expires_in": ' + - expires_in + - '}' - ) - server.close() - expect(body).toEqual( - 'audience=token&client_id=clientId&client_secret=clientSecret&grant_type=client_credentials' - ) - }) - } + req.on('data', () => { + // ignoring + }) + + req.on('end', () => { + res.writeHead(200, { 'Content-Type': 'application/json' }) + let expires_in = 2 // seconds + res.end( + '{"access_token": "something", "expires_in": ' + + expires_in + + '}' + ) + }) }) - .listen(3002) + .listen(3001) - o.getToken().then(async _ => { - expect(o.tokenCache['clientId']).toBeDefined() - await delay(500) - expect(o.tokenCache['clientId']).toBeDefined() - await delay(1600) - expect(o.tokenCache['clientId']).not.toBeDefined() - o.stopExpiryTimer() - done() - }) + return o + .getToken() + .then(async () => { + expect(o.tokenCache['clientId']).toBeDefined() + await delay(500) + expect(o.tokenCache['clientId']).toBeDefined() + await delay(1600) + expect(o.tokenCache['clientId']).not.toBeDefined() + }) + .finally(() => { + o.stopExpiryTimer() + + return server.close() + }) }) + + +function clearCache(cachePath) { + if (fs.existsSync(cachePath)) { + fs.rmSync(cachePath, { recursive: true }) + } + expect(fs.existsSync(cachePath)).toBe(false) +} diff --git a/src/lib/ConfigurationHydrator.ts b/src/lib/ConfigurationHydrator.ts index f1f9914..afe4547 100644 --- a/src/lib/ConfigurationHydrator.ts +++ b/src/lib/ConfigurationHydrator.ts @@ -21,6 +21,7 @@ export class ConfigurationHydrator { 'ZEEBE_CLIENT_SECRET', 'ZEEBE_SECURE_CONNECTION', 'ZEEBE_TOKEN_AUDIENCE', + 'ZEEBE_TOKEN_SCOPE', 'ZEEBE_AUTHORIZATION_SERVER_URL', 'ZEEBE_CAMUNDA_CLOUD_CLUSTER_ID', 'ZEEBE_BASIC_AUTH_PASSWORD', @@ -32,7 +33,7 @@ export class ConfigurationHydrator { 'ZEEBE_CLIENT_SSL_ROOT_CERTS_PATH', 'ZEEBE_CLIENT_SSL_PRIVATE_KEY_PATH', 'ZEEBE_CLIENT_SSL_CERT_CHAIN_PATH', - 'ZEEBE_TENANT_ID' + 'ZEEBE_TENANT_ID', ]) public static configure( @@ -52,7 +53,7 @@ export class ConfigurationHydrator { ...ConfigurationHydrator.readTLSFromEnvironment(options), ...ConfigurationHydrator.getEagerStatus(options), ...ConfigurationHydrator.getRetryConfiguration(options), - ...ConfigurationHydrator.getTenantId(options) + ...ConfigurationHydrator.getTenantId(options), } // inherit oAuth custom root certificates, unless @@ -128,6 +129,7 @@ export class ConfigurationHydrator { const clientId = ConfigurationHydrator.getClientIdFromEnv() const clientSecret = ConfigurationHydrator.getClientSecretFromEnv() const audience = ConfigurationHydrator.ENV().ZEEBE_TOKEN_AUDIENCE + const scope = ConfigurationHydrator.ENV().ZEEBE_TOKEN_SCOPE const authServerUrl = ConfigurationHydrator.ENV() .ZEEBE_AUTHORIZATION_SERVER_URL const clusterId = ConfigurationHydrator.ENV() @@ -144,6 +146,7 @@ export class ConfigurationHydrator { ? { oAuth: { audience, + scope, cacheOnDisk: true, clientId: clientId!, clientSecret, diff --git a/src/lib/OAuthProvider.ts b/src/lib/OAuthProvider.ts index 62a2909..66f7208 100644 --- a/src/lib/OAuthProvider.ts +++ b/src/lib/OAuthProvider.ts @@ -23,6 +23,8 @@ export interface OAuthProviderConfig { url: string /** OAuth Audience */ audience: string + /** OAuth Scope */ + scope?: string clientId: string clientSecret: string /** Custom TLS certificate for OAuth */ @@ -39,6 +41,7 @@ export class OAuthProvider { process.env.ZEEBE_TOKEN_CACHE_DIR || OAuthProvider.defaultTokenCache public cacheDir: string public audience: string + public scope?: string public url: string public clientId: string public clientSecret: string @@ -56,6 +59,8 @@ export class OAuthProvider { url, /** OAuth Audience */ audience, + /** OAuth Scope */ + scope, cacheDir, clientId, clientSecret, @@ -66,6 +71,7 @@ export class OAuthProvider { }: { url: string audience: string + scope?: string cacheDir?: string clientId: string clientSecret: string @@ -74,6 +80,7 @@ export class OAuthProvider { }) { this.url = url this.audience = audience + this.scope = scope this.clientId = clientId this.clientSecret = clientSecret this.customRootCert = customRootCert @@ -153,6 +160,9 @@ export class OAuthProvider { client_id: this.clientId, client_secret: this.clientSecret, grant_type: 'client_credentials', + ...( + this.scope && { scope: this.scope } || {} + ) } debug(`Requesting token from token endpoint...`) diff --git a/src/lib/interfaces-published-contract.ts b/src/lib/interfaces-published-contract.ts index 951a080..e986a73 100644 --- a/src/lib/interfaces-published-contract.ts +++ b/src/lib/interfaces-published-contract.ts @@ -52,7 +52,7 @@ export interface ZBClientOptions { longPoll?: MaybeTimeDuration pollInterval?: MaybeTimeDuration camundaCloud?: CamundaCloudConfig - hostname?: string + hostname?: string | null port?: string onReady?: () => void onConnectionError?: () => void