From 13753b7691eeba77f9656502bf034f374fd84ff7 Mon Sep 17 00:00:00 2001 From: Marko Mlakar Date: Mon, 24 Nov 2025 08:16:53 +0100 Subject: [PATCH 1/3] Sync branch with main and resolve merge conflicts Signed-off-by: Marko Mlakar --- libs/providers/flagd/README.md | 1 + .../src/e2e/step-definitions/providerSteps.ts | 18 ++++++++++++++++-- .../flagd/src/e2e/tests/in-process.spec.ts | 2 +- libs/providers/flagd/src/e2e/tests/rpc.spec.ts | 2 +- .../flagd/src/lib/configuration.spec.ts | 5 +++++ libs/providers/flagd/src/lib/configuration.ts | 11 +++++++++++ .../flagd/src/lib/service/common/grpc-util.ts | 17 +++++++++++++++++ .../flagd/src/lib/service/grpc/grpc-service.ts | 14 +++++++------- .../lib/service/in-process/grpc/grpc-fetch.ts | 9 +++++---- 9 files changed, 64 insertions(+), 15 deletions(-) diff --git a/libs/providers/flagd/README.md b/libs/providers/flagd/README.md index f7f1ecc9e..7cfc5c9df 100644 --- a/libs/providers/flagd/README.md +++ b/libs/providers/flagd/README.md @@ -33,6 +33,7 @@ Options can be defined in the constructor or as environment variables. Construct | port | FLAGD_PORT | number | [resolver specific defaults](#resolver-type-specific-defaults) | | | tls | FLAGD_TLS | boolean | false | | | socketPath | FLAGD_SOCKET_PATH | string | - | | +| certPath | FLAGD_SERVER_CERT_PATH | string | - | | | resolverType | FLAGD_RESOLVER | string | rpc | rpc, in-process | | offlineFlagSourcePath | FLAGD_OFFLINE_FLAG_SOURCE_PATH | string | - | | | selector | FLAGD_SOURCE_SELECTOR | string | - | | diff --git a/libs/providers/flagd/src/e2e/step-definitions/providerSteps.ts b/libs/providers/flagd/src/e2e/step-definitions/providerSteps.ts index 7e072cae3..e8cf2d62b 100644 --- a/libs/providers/flagd/src/e2e/step-definitions/providerSteps.ts +++ b/libs/providers/flagd/src/e2e/step-definitions/providerSteps.ts @@ -4,6 +4,8 @@ import { FlagdContainer } from '../tests/flagdContainer'; import type { State, Steps } from './state'; import { FlagdProvider } from '../../lib/flagd-provider'; import type { FlagdProviderOptions } from '../../lib/configuration'; +import { resolve } from 'node:path'; +import { existsSync } from 'node:fs'; export const providerSteps: Steps = (state: State) => @@ -43,11 +45,23 @@ export const providerSteps: Steps = case 'unavailable': flagdOptions['port'] = 9999; break; - case 'ssl': - // TODO: modify this to support ssl + case 'ssl': { flagdOptions['port'] = container.getPort(state.resolverType); + flagdOptions['tls'] = true; + const certPath = resolve( + __dirname, + './../../../../../shared/flagd-core/test-harness/ssl/custom-root-cert.crt', + ); + flagdOptions['certPath'] = certPath; + if (!existsSync(certPath)) { + throw new Error('Certificate file not found at path: ' + certPath); + } + if (state?.config?.selector) { + flagdOptions['selector'] = state.config.selector; + } type = 'ssl'; break; + } case 'stable': flagdOptions['port'] = container.getPort(state.resolverType); break; diff --git a/libs/providers/flagd/src/e2e/tests/in-process.spec.ts b/libs/providers/flagd/src/e2e/tests/in-process.spec.ts index fd477ca70..ce3fd2ad5 100644 --- a/libs/providers/flagd/src/e2e/tests/in-process.spec.ts +++ b/libs/providers/flagd/src/e2e/tests/in-process.spec.ts @@ -22,7 +22,7 @@ describe('in-process', () => { // remove filters as we add support for features // see: https://github.com/open-feature/js-sdk-contrib/issues/1096 and child issues tagFilter: - '@in-process and not @targetURI and not @forbidden and not @customCert and not @events and not @sync and not @grace and not @metadata and not @unixsocket', + '@in-process and not @targetURI and not @forbidden and not @events and not @sync and not @grace and not @metadata and not @unixsocket', scenarioNameTemplate: (vars) => { return `${vars.scenarioTitle} (${vars.scenarioTags.join(',')} ${vars.featureTags.join(',')})`; }, diff --git a/libs/providers/flagd/src/e2e/tests/rpc.spec.ts b/libs/providers/flagd/src/e2e/tests/rpc.spec.ts index 6317b530c..e0109faf1 100644 --- a/libs/providers/flagd/src/e2e/tests/rpc.spec.ts +++ b/libs/providers/flagd/src/e2e/tests/rpc.spec.ts @@ -23,7 +23,7 @@ describe('rpc', () => { tagFilter: // remove filters as we add support for features // see: https://github.com/open-feature/js-sdk-contrib/issues/1096 and child issues - '@rpc and not @targetURI and not @customCert and not @forbidden and not @events and not @stream and not @grace and not @metadata and not @caching and not @unixsocket', + '@rpc and not @targetURI and not @forbidden and not @events and not @stream and not @grace and not @metadata and not @caching and not @unixsocket', scenarioNameTemplate: (vars) => { return `${vars.scenarioTitle} (${vars.scenarioTags.join(',')} ${vars.featureTags.join(',')})`; }, diff --git a/libs/providers/flagd/src/lib/configuration.spec.ts b/libs/providers/flagd/src/lib/configuration.spec.ts index 4a0b2d905..dcba4f7ae 100644 --- a/libs/providers/flagd/src/lib/configuration.spec.ts +++ b/libs/providers/flagd/src/lib/configuration.spec.ts @@ -30,6 +30,7 @@ describe('Configuration', () => { const port = 8080; const tls = true; const socketPath = '/tmp/flagd.socks'; + const certPath = '/etc/cert/ca.crt'; const maxCacheSize = 333; const cache = 'disabled'; const resolverType = 'in-process'; @@ -41,6 +42,7 @@ describe('Configuration', () => { process.env['FLAGD_PORT'] = `${port}`; process.env['FLAGD_TLS'] = `${tls}`; process.env['FLAGD_SOCKET_PATH'] = socketPath; + process.env['FLAGD_SERVER_CERT_PATH'] = certPath; process.env['FLAGD_CACHE'] = cache; process.env['FLAGD_MAX_CACHE_SIZE'] = `${maxCacheSize}`; process.env['FLAGD_SOURCE_SELECTOR'] = `${selector}`; @@ -54,6 +56,7 @@ describe('Configuration', () => { port, tls, socketPath, + certPath, maxCacheSize, cache, resolverType, @@ -99,6 +102,7 @@ describe('Configuration', () => { host: 'test', port: 3000, tls: true, + certPath: '/custom/cert.pem', maxCacheSize: 1000, cache: 'lru', resolverType: 'rpc', @@ -112,6 +116,7 @@ describe('Configuration', () => { process.env['FLAGD_PORT'] = '8080'; process.env['FLAGD_SYNC_PORT'] = '9090'; process.env['FLAGD_TLS'] = 'false'; + process.env['FLAGD_SERVER_CERT_PATH'] = '/env/cert.pem'; process.env['FLAGD_DEFAULT_AUTHORITY'] = 'test-authority-override'; expect(getConfig(options)).toStrictEqual(options); diff --git a/libs/providers/flagd/src/lib/configuration.ts b/libs/providers/flagd/src/lib/configuration.ts index 64f434ae9..c1804a46f 100644 --- a/libs/providers/flagd/src/lib/configuration.ts +++ b/libs/providers/flagd/src/lib/configuration.ts @@ -40,6 +40,13 @@ export interface Config { */ socketPath?: string; + /** + * TLS certificate path to use when TLS connectivity is enabled. + * + * @example "/etc/cert/ca.crt" + */ + certPath?: string; + /** * Resolver type to use by the provider. * @@ -120,6 +127,7 @@ enum ENV_VAR { FLAGD_DEADLINE_MS = 'FLAGD_DEADLINE_MS', FLAGD_TLS = 'FLAGD_TLS', FLAGD_SOCKET_PATH = 'FLAGD_SOCKET_PATH', + FLAGD_SERVER_CERT_PATH = 'FLAGD_SERVER_CERT_PATH', FLAGD_CACHE = 'FLAGD_CACHE', FLAGD_MAX_CACHE_SIZE = 'FLAGD_MAX_CACHE_SIZE', FLAGD_SOURCE_SELECTOR = 'FLAGD_SOURCE_SELECTOR', @@ -165,6 +173,9 @@ const getEnvVarConfig = (): Partial => { ...(process.env[ENV_VAR.FLAGD_SOCKET_PATH] && { socketPath: process.env[ENV_VAR.FLAGD_SOCKET_PATH], }), + ...(process.env[ENV_VAR.FLAGD_SERVER_CERT_PATH] && { + certPath: process.env[ENV_VAR.FLAGD_SERVER_CERT_PATH], + }), ...((process.env[ENV_VAR.FLAGD_CACHE] === 'lru' || process.env[ENV_VAR.FLAGD_CACHE] === 'disabled') && { cache: process.env[ENV_VAR.FLAGD_CACHE], }), diff --git a/libs/providers/flagd/src/lib/service/common/grpc-util.ts b/libs/providers/flagd/src/lib/service/common/grpc-util.ts index e8433207f..a6e76c3ad 100644 --- a/libs/providers/flagd/src/lib/service/common/grpc-util.ts +++ b/libs/providers/flagd/src/lib/service/common/grpc-util.ts @@ -1,4 +1,6 @@ +import { credentials } from '@grpc/grpc-js'; import type { ClientReadableStream } from '@grpc/grpc-js'; +import { readFileSync, existsSync } from 'node:fs'; export const closeStreamIfDefined = (stream: ClientReadableStream | undefined) => { /** @@ -14,3 +16,18 @@ export const closeStreamIfDefined = (stream: ClientReadableStream | und stream.destroy(); } }; + +/** + * Creates gRPC channel credentials based on TLS and certificate path configuration. + * @returns Channel credentials for gRPC connection + */ +export const createChannelCredentials = (tls: boolean, certPath?: string) => { + if (!tls) { + return credentials.createInsecure(); + } + if (certPath && existsSync(certPath)) { + const rootCerts = readFileSync(certPath); + return credentials.createSsl(rootCerts); + } + return credentials.createSsl(); +}; diff --git a/libs/providers/flagd/src/lib/service/grpc/grpc-service.ts b/libs/providers/flagd/src/lib/service/grpc/grpc-service.ts index 7ad15da8b..fb41bf012 100644 --- a/libs/providers/flagd/src/lib/service/grpc/grpc-service.ts +++ b/libs/providers/flagd/src/lib/service/grpc/grpc-service.ts @@ -11,6 +11,8 @@ import { } from '@openfeature/server-sdk'; import { LRUCache } from 'lru-cache'; import { promisify } from 'node:util'; +import { readFileSync, existsSync } from 'node:fs'; + import type { EventStreamResponse, ResolveBooleanRequest, @@ -29,7 +31,7 @@ import type { Config } from '../../configuration'; import { DEFAULT_MAX_CACHE_SIZE, EVENT_CONFIGURATION_CHANGE, EVENT_PROVIDER_READY } from '../../constants'; import { FlagdProvider } from '../../flagd-provider'; import type { Service } from '../service'; -import { closeStreamIfDefined } from '../common'; +import { closeStreamIfDefined, createChannelCredentials } from '../common'; type AnyResponse = | ResolveBooleanResponse @@ -79,7 +81,7 @@ export class GRPCService implements Service { client?: ServiceClient, private logger?: Logger, ) { - const { host, port, tls, socketPath, defaultAuthority } = config; + const { host, port, tls, socketPath, certPath, defaultAuthority } = config; let clientOptions: ClientOptions | undefined; if (defaultAuthority) { clientOptions = { @@ -87,13 +89,11 @@ export class GRPCService implements Service { }; } + const channelCredentials = createChannelCredentials(tls, certPath); + this._client = client ? client - : new ServiceClient( - socketPath ? `unix://${socketPath}` : `${host}:${port}`, - tls ? credentials.createSsl() : credentials.createInsecure(), - clientOptions, - ); + : new ServiceClient(socketPath ? `unix://${socketPath}` : `${host}:${port}`, channelCredentials, clientOptions); this._deadline = config.deadlineMs; if (config.cache === 'lru') { diff --git a/libs/providers/flagd/src/lib/service/in-process/grpc/grpc-fetch.ts b/libs/providers/flagd/src/lib/service/in-process/grpc/grpc-fetch.ts index 2d317b591..a59ad38c2 100644 --- a/libs/providers/flagd/src/lib/service/in-process/grpc/grpc-fetch.ts +++ b/libs/providers/flagd/src/lib/service/in-process/grpc/grpc-fetch.ts @@ -1,11 +1,10 @@ import type { ClientReadableStream, ServiceError, ClientOptions } from '@grpc/grpc-js'; -import { credentials } from '@grpc/grpc-js'; import type { EvaluationContext, Logger } from '@openfeature/server-sdk'; import { GeneralError } from '@openfeature/server-sdk'; import type { SyncFlagsRequest, SyncFlagsResponse } from '../../../../proto/ts/flagd/sync/v1/sync'; import { FlagSyncServiceClient } from '../../../../proto/ts/flagd/sync/v1/sync'; import type { Config } from '../../../configuration'; -import { closeStreamIfDefined } from '../../common'; +import { closeStreamIfDefined, createChannelCredentials } from '../../common'; import type { DataFetch } from '../data-fetch'; /** @@ -36,7 +35,7 @@ export class GrpcFetch implements DataFetch { syncServiceClient?: FlagSyncServiceClient, logger?: Logger, ) { - const { host, port, tls, socketPath, selector, defaultAuthority } = config; + const { host, port, tls, socketPath, certPath, selector, defaultAuthority } = config; let clientOptions: ClientOptions | undefined; if (defaultAuthority) { clientOptions = { @@ -44,11 +43,13 @@ export class GrpcFetch implements DataFetch { }; } + const channelCredentials = createChannelCredentials(tls, certPath); + this._syncClient = syncServiceClient ? syncServiceClient : new FlagSyncServiceClient( socketPath ? `unix://${socketPath}` : `${host}:${port}`, - tls ? credentials.createSsl() : credentials.createInsecure(), + channelCredentials, clientOptions, ); From 8d0d66af289a6ba2cff87d287a0a6821be91f09e Mon Sep 17 00:00:00 2001 From: Marko Mlakar Date: Mon, 24 Nov 2025 10:02:38 +0100 Subject: [PATCH 2/3] Remove unused imports from grpc-service Signed-off-by: Marko Mlakar --- libs/providers/flagd/src/lib/service/grpc/grpc-service.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/libs/providers/flagd/src/lib/service/grpc/grpc-service.ts b/libs/providers/flagd/src/lib/service/grpc/grpc-service.ts index fb41bf012..391b0dcaa 100644 --- a/libs/providers/flagd/src/lib/service/grpc/grpc-service.ts +++ b/libs/providers/flagd/src/lib/service/grpc/grpc-service.ts @@ -1,5 +1,5 @@ import type { ClientOptions, ClientReadableStream, ClientUnaryCall, ServiceError } from '@grpc/grpc-js'; -import { credentials, status } from '@grpc/grpc-js'; +import { status } from '@grpc/grpc-js'; import { ConnectivityState } from '@grpc/grpc-js/build/src/connectivity-state'; import type { EvaluationContext, FlagValue, JsonValue, Logger, ResolutionDetails } from '@openfeature/server-sdk'; import { @@ -11,7 +11,6 @@ import { } from '@openfeature/server-sdk'; import { LRUCache } from 'lru-cache'; import { promisify } from 'node:util'; -import { readFileSync, existsSync } from 'node:fs'; import type { EventStreamResponse, From 9d8f98164a951e7714a63e0b2a4a8e558fe45f79 Mon Sep 17 00:00:00 2001 From: Marko Mlakar Date: Fri, 28 Nov 2025 12:44:14 +0100 Subject: [PATCH 3/3] Replace hardcoded path with getGherkinTestPath Signed-off-by: Marko Mlakar --- .../flagd/src/e2e/step-definitions/providerSteps.ts | 6 ++---- libs/providers/flagd/src/lib/service/common/grpc-util.ts | 4 ++-- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/libs/providers/flagd/src/e2e/step-definitions/providerSteps.ts b/libs/providers/flagd/src/e2e/step-definitions/providerSteps.ts index e8cf2d62b..236f0aea8 100644 --- a/libs/providers/flagd/src/e2e/step-definitions/providerSteps.ts +++ b/libs/providers/flagd/src/e2e/step-definitions/providerSteps.ts @@ -4,6 +4,7 @@ import { FlagdContainer } from '../tests/flagdContainer'; import type { State, Steps } from './state'; import { FlagdProvider } from '../../lib/flagd-provider'; import type { FlagdProviderOptions } from '../../lib/configuration'; +import { getGherkinTestPath } from '@openfeature/flagd-core'; import { resolve } from 'node:path'; import { existsSync } from 'node:fs'; @@ -48,10 +49,7 @@ export const providerSteps: Steps = case 'ssl': { flagdOptions['port'] = container.getPort(state.resolverType); flagdOptions['tls'] = true; - const certPath = resolve( - __dirname, - './../../../../../shared/flagd-core/test-harness/ssl/custom-root-cert.crt', - ); + const certPath = resolve(getGherkinTestPath('custom-root-cert.crt', 'test-harness/ssl/')); flagdOptions['certPath'] = certPath; if (!existsSync(certPath)) { throw new Error('Certificate file not found at path: ' + certPath); diff --git a/libs/providers/flagd/src/lib/service/common/grpc-util.ts b/libs/providers/flagd/src/lib/service/common/grpc-util.ts index a6e76c3ad..39ea0ee8e 100644 --- a/libs/providers/flagd/src/lib/service/common/grpc-util.ts +++ b/libs/providers/flagd/src/lib/service/common/grpc-util.ts @@ -1,5 +1,5 @@ import { credentials } from '@grpc/grpc-js'; -import type { ClientReadableStream } from '@grpc/grpc-js'; +import type { ClientReadableStream, ChannelCredentials } from '@grpc/grpc-js'; import { readFileSync, existsSync } from 'node:fs'; export const closeStreamIfDefined = (stream: ClientReadableStream | undefined) => { @@ -21,7 +21,7 @@ export const closeStreamIfDefined = (stream: ClientReadableStream | und * Creates gRPC channel credentials based on TLS and certificate path configuration. * @returns Channel credentials for gRPC connection */ -export const createChannelCredentials = (tls: boolean, certPath?: string) => { +export const createChannelCredentials = (tls: boolean, certPath?: string): ChannelCredentials => { if (!tls) { return credentials.createInsecure(); }