From afd970f35a3ceb9852fe33c76b6d32f2d9d23ea7 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 15 Sep 2026 13:35:02 -0300 Subject: [PATCH 1/8] Refactor: rename IJwtCredential, add CONFIG_UPDATE notification type --- src/__tests__/testUtils/jwt.ts | 4 +- src/presets/serverSide.ts | 4 +- src/services/authProvider.ts | 16 ++--- .../updaters/definitionChangesUpdater.ts | 6 +- src/sync/streaming/AuthClient/index.ts | 14 ++++- src/sync/streaming/AuthClient/types.ts | 22 +++---- src/sync/streaming/SSEClient/index.ts | 4 +- src/sync/streaming/SSEClient/types.ts | 4 +- src/sync/streaming/SSEHandler/index.ts | 3 +- src/sync/streaming/SSEHandler/types.ts | 4 +- src/sync/streaming/constants.ts | 1 + src/sync/streaming/pushManager.ts | 63 +++++++++---------- src/sync/streaming/types.ts | 5 +- 13 files changed, 78 insertions(+), 72 deletions(-) diff --git a/src/__tests__/testUtils/jwt.ts b/src/__tests__/testUtils/jwt.ts index b0623c5b..f9810032 100644 --- a/src/__tests__/testUtils/jwt.ts +++ b/src/__tests__/testUtils/jwt.ts @@ -1,10 +1,10 @@ -import { IJwtCredentialV3 } from '../../sync/streaming/AuthClient/types'; +import { IJwtCredential } from '../../sync/streaming/AuthClient/types'; function toBase64Url(str: string) { return Buffer.from(str).toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); } -export function makeJwtCredential(expInSeconds = 3600): IJwtCredentialV3 { +export function makeJwtCredential(expInSeconds = 3600): IJwtCredential { const now = Math.floor(Date.now() / 1000); const header = toBase64Url(JSON.stringify({ alg: 'HS256' })); const decodedToken = { iat: now, exp: now + expInSeconds, 'x-ably-capability': '{"ch":["subscribe"]}' }; diff --git a/src/presets/serverSide.ts b/src/presets/serverSide.ts index bfbe62f6..fb3902b2 100644 --- a/src/presets/serverSide.ts +++ b/src/presets/serverSide.ts @@ -1,4 +1,4 @@ -import { serviceApiFactory } from '../services/serviceApi'; +import { splitApiFactory } from '../services/splitApi'; import { syncManagerOnlineFactory } from '../sync/syncManagerOnline'; import { pushManagerFactory } from '../sync/streaming/pushManager'; import { pollingManagerSSFactory } from '../sync/polling/pollingManagerSS'; @@ -11,7 +11,7 @@ const syncManagerOnlineSSFactory = syncManagerOnlineFactory(pollingManagerSSFact export const serverSideModules = { storageFactory: InMemoryStorageFactory, - serviceApiFactory, + serviceApiFactory: splitApiFactory, syncManagerFactory: syncManagerOnlineSSFactory, sdkManagerFactory, sdkClientMethodFactory, diff --git a/src/services/authProvider.ts b/src/services/authProvider.ts index 1816bbd5..eefde72d 100644 --- a/src/services/authProvider.ts +++ b/src/services/authProvider.ts @@ -1,5 +1,5 @@ import { ISplitHttpClient, NetworkError } from './types'; -import { IJwtCredentialV3 } from '../sync/streaming/AuthClient/types'; +import { IJwtCredential } from '../sync/streaming/AuthClient/types'; import { authenticateFactory } from '../sync/streaming/AuthClient'; import { Backoff } from '../utils/Backoff'; import { LOG_PREFIX_SYNC_AUTH } from '../logger/constants'; @@ -9,12 +9,12 @@ import { ITelemetryTracker } from '../trackers/types'; const SKEW_SECONDS = 30; -function isExpired(credential: IJwtCredentialV3): boolean { +function isExpired(credential: IJwtCredential): boolean { return Date.now() / 1000 + SKEW_SECONDS >= credential.decodedToken.exp; } export interface IAuthProvider { - credential(): Promise; + credential(): Promise; invalidate(): void; stop(): void; } @@ -35,12 +35,12 @@ export function authProviderFactory(settings: ISettings, splitHttpClient: ISplit const authenticate = authenticateFactory(fetchAuth); const backoff = new Backoff(fetchCredential); - let cachedCredential: IJwtCredentialV3 | undefined; - let inFlightPromise: Promise | undefined; + let cachedCredential: IJwtCredential | undefined; + let inFlightPromise: Promise | undefined; let stopped = false; - function fetchCredential(): Promise { - return authenticate().then((credential: IJwtCredentialV3) => { + function fetchCredential(): Promise { + return authenticate().then((credential: IJwtCredential) => { log.info(LOG_PREFIX_SYNC_AUTH + 'credential fetched successfully'); cachedCredential = credential; inFlightPromise = undefined; @@ -62,7 +62,7 @@ export function authProviderFactory(settings: ISettings, splitHttpClient: ISplit } return { - credential(): Promise { + credential(): Promise { if (cachedCredential && !isExpired(cachedCredential)) { return Promise.resolve(cachedCredential); } diff --git a/src/sync/polling/updaters/definitionChangesUpdater.ts b/src/sync/polling/updaters/definitionChangesUpdater.ts index 45852e74..f6573e73 100644 --- a/src/sync/polling/updaters/definitionChangesUpdater.ts +++ b/src/sync/polling/updaters/definitionChangesUpdater.ts @@ -9,7 +9,7 @@ import { SYNC_FETCH, SYNC_UPDATE, SYNC_FETCH_FAILS, SYNC_FETCH_RETRY } from '../ import { startsWith } from '../../../utils/lang'; import { IN_RULE_BASED_SEGMENT, IN_SEGMENT, RULE_BASED_SEGMENT, STANDARD_SEGMENT } from '../../../utils/constants'; import { setToArray } from '../../../utils/lang/sets'; -import { SPLIT_UPDATE } from '../../streaming/constants'; +import { RB_SEGMENT_UPDATE } from '../../streaming/constants'; import { SdkUpdateMetadata } from '../../../../types/splitio'; import { ISplit } from '../fetchers/splitChangesFetcher'; import { ISegmentsSyncTask } from '../types'; @@ -154,14 +154,14 @@ export function definitionChangesUpdaterFactory( return Promise.resolve( instantUpdate ? - instantUpdate.type === SPLIT_UPDATE ? + instantUpdate.type === RB_SEGMENT_UPDATE ? + { rbs: convertInstantUpdateToDefinitionChanges(instantUpdate) as IDefinitionChangesResponse['rbs'] } : // IFFU edge case: a change to definition that adds an IN_RULE_BASED_SEGMENT matcher that is not present yet Promise.resolve(rbSegments.contains(parseSegments(instantUpdate.payload, IN_RULE_BASED_SEGMENT))).then((contains) => { return contains ? { d: convertInstantUpdateToDefinitionChanges(instantUpdate) as IDefinitionChangesResponse['d'] } : definitionChangesFetcher(since, noCache, till, rbSince, _promiseDecorator); }) : - { rbs: convertInstantUpdateToDefinitionChanges(instantUpdate) as IDefinitionChangesResponse['rbs'] } : definitionChangesFetcher(since, noCache, till, rbSince, _promiseDecorator) ) .then((definitionChanges: IDefinitionChangesResponse) => { diff --git a/src/sync/streaming/AuthClient/index.ts b/src/sync/streaming/AuthClient/index.ts index 71f0ab62..40e02161 100644 --- a/src/sync/streaming/AuthClient/index.ts +++ b/src/sync/streaming/AuthClient/index.ts @@ -1,5 +1,5 @@ import { IFetchAuth } from '../../../services/types'; -import { IAuthenticate, IJwtCredentialV2 } from './types'; +import { IAuthenticate, IJwtCredential } from './types'; import { objectAssign } from '../../../utils/lang/objectAssign'; import { encodeToBase64 } from '../../../utils/base64'; import { decodeJWTtoken } from '../../../utils/jwt'; @@ -16,7 +16,7 @@ export function authenticateFactory(fetchAuth: IFetchAuth): IAuthenticate { * Run authentication requests to Auth Server, and returns a promise that resolves with the decoded JTW token. * @param userKeys - set of user Keys to track membership updates. It is undefined for server-side API. */ - return function authenticate(userKeys?: string[]): Promise { + return function authenticate(userKeys?: string[]): Promise { return fetchAuth(userKeys) .then(resp => resp.json()) .then(json => { @@ -24,10 +24,18 @@ export function authenticateFactory(fetchAuth: IFetchAuth): IAuthenticate { const decodedToken = decodeJWTtoken(json.token); if (typeof decodedToken.iat !== 'number' || typeof decodedToken.exp !== 'number') throw new Error('token properties "issuedAt" (iat) or "expiration" (exp) are missing or invalid'); const channels = JSON.parse(decodedToken['x-ably-capability']); - return objectAssign({ + const credential = objectAssign({ decodedToken, channels }, json); + // The `/api/v3/auth` endpoint nests the streaming settings under `config.streaming`. Normalize + // them into the flat `pushEnabled`/`connDelay` properties used by the PushManager. + const streaming = credential.config && credential.config.streaming; + if (streaming) { + credential.pushEnabled = streaming.enabled; + credential.connDelay = streaming.delay; + } + return credential; } return json; }); diff --git a/src/sync/streaming/AuthClient/types.ts b/src/sync/streaming/AuthClient/types.ts index ae98c812..daf3adfc 100644 --- a/src/sync/streaming/AuthClient/types.ts +++ b/src/sync/streaming/AuthClient/types.ts @@ -1,23 +1,19 @@ import { IDecodedJWTToken } from '../../../utils/jwt/types'; -export type IJwtCredentialV2 = { - pushEnabled: boolean - token: string // empty string ("") when `"pushEnabled": false` - decodedToken: IDecodedJWTToken - channels: { [channel: string]: string[] } - connDelay?: number -} - -export type IJwtCredentialV3 = { - token: string +export type IJwtCredential = { + token: string; // empty string ("") when `"pushEnabled": false` decodedToken: IDecodedJWTToken channels: { [channel: string]: string[] } + // /api/v2/auth fields + pushEnabled?: boolean | null; + connDelay?: number | null; + // /api/v3/auth fields config?: { streaming?: { - delay?: number - enabled?: boolean + delay?: number | null; + enabled?: boolean | null; } | null; } | null; } -export type IAuthenticate = (userKeys?: string[]) => Promise +export type IAuthenticate = (userKeys?: string[]) => Promise diff --git a/src/sync/streaming/SSEClient/index.ts b/src/sync/streaming/SSEClient/index.ts index 1f31d5a3..fba4a736 100644 --- a/src/sync/streaming/SSEClient/index.ts +++ b/src/sync/streaming/SSEClient/index.ts @@ -5,7 +5,7 @@ import { ISettings } from '../../../types'; import { checkIfServerSide } from '../../../utils/key'; import { isString } from '../../../utils/lang'; import { objectAssign } from '../../../utils/lang/objectAssign'; -import { IJwtCredentialV2 } from '../AuthClient/types'; +import { IJwtCredential } from '../AuthClient/types'; import { ISSEClient, ISseEventHandler } from './types'; const ABLY_API_VERSION = '1.1'; @@ -66,7 +66,7 @@ export class SSEClient implements ISSEClient { /** * Open the connection with a given authToken */ - open(authToken: IJwtCredentialV2) { + open(authToken: IJwtCredential) { this.close(); // it closes connection if previously opened const channelsQueryParam = Object.keys(authToken.channels).map((channel) => { diff --git a/src/sync/streaming/SSEClient/types.ts b/src/sync/streaming/SSEClient/types.ts index 8072c05b..999ff30a 100644 --- a/src/sync/streaming/SSEClient/types.ts +++ b/src/sync/streaming/SSEClient/types.ts @@ -1,4 +1,4 @@ -import { IJwtCredentialV2 } from '../AuthClient/types'; +import { IJwtCredential } from '../AuthClient/types'; export interface ISseEventHandler { handleError: (ev: Event) => any; @@ -7,7 +7,7 @@ export interface ISseEventHandler { } export interface ISSEClient { - open(authToken: IJwtCredentialV2): void, + open(authToken: IJwtCredential): void, close(): void, setEventHandler(handler: ISseEventHandler): void } diff --git a/src/sync/streaming/SSEHandler/index.ts b/src/sync/streaming/SSEHandler/index.ts index 22d462b8..3e3388c2 100644 --- a/src/sync/streaming/SSEHandler/index.ts +++ b/src/sync/streaming/SSEHandler/index.ts @@ -1,6 +1,6 @@ import { errorParser, messageParser } from './NotificationParser'; import { notificationKeeperFactory } from './NotificationKeeper'; -import { PUSH_RETRYABLE_ERROR, PUSH_NON_RETRYABLE_ERROR, OCCUPANCY, CONTROL, SEGMENT_UPDATE, SPLIT_KILL, SPLIT_UPDATE, MEMBERSHIPS_MS_UPDATE, MEMBERSHIPS_LS_UPDATE, RB_SEGMENT_UPDATE } from '../constants'; +import { PUSH_RETRYABLE_ERROR, PUSH_NON_RETRYABLE_ERROR, OCCUPANCY, CONTROL, SEGMENT_UPDATE, SPLIT_KILL, SPLIT_UPDATE, CONFIG_UPDATE, MEMBERSHIPS_MS_UPDATE, MEMBERSHIPS_LS_UPDATE, RB_SEGMENT_UPDATE } from '../constants'; import { IPushEventEmitter } from '../types'; import { ISseEventHandler } from '../SSEClient/types'; import { INotificationError, INotificationMessage } from './types'; @@ -80,6 +80,7 @@ export function SSEHandlerFactory(log: ILogger, pushEmitter: IPushEventEmitter, switch (parsedData.type) { /* update events */ case SPLIT_UPDATE: + case CONFIG_UPDATE: case SEGMENT_UPDATE: case MEMBERSHIPS_MS_UPDATE: case MEMBERSHIPS_LS_UPDATE: diff --git a/src/sync/streaming/SSEHandler/types.ts b/src/sync/streaming/SSEHandler/types.ts index a39b8000..144acac3 100644 --- a/src/sync/streaming/SSEHandler/types.ts +++ b/src/sync/streaming/SSEHandler/types.ts @@ -1,5 +1,5 @@ import { ControlType } from '../constants'; -import { SEGMENT_UPDATE, SPLIT_UPDATE, SPLIT_KILL, CONTROL, OCCUPANCY, MEMBERSHIPS_LS_UPDATE, MEMBERSHIPS_MS_UPDATE, RB_SEGMENT_UPDATE } from '../types'; +import { SEGMENT_UPDATE, SPLIT_UPDATE, CONFIG_UPDATE, SPLIT_KILL, CONTROL, OCCUPANCY, MEMBERSHIPS_LS_UPDATE, MEMBERSHIPS_MS_UPDATE, RB_SEGMENT_UPDATE } from '../types'; export enum Compression { None = 0, @@ -42,7 +42,7 @@ export interface ISegmentUpdateData { } export interface ISplitUpdateData { - type: SPLIT_UPDATE | RB_SEGMENT_UPDATE, + type: SPLIT_UPDATE | CONFIG_UPDATE | RB_SEGMENT_UPDATE, changeNumber: number, pcn?: number, d?: string, diff --git a/src/sync/streaming/constants.ts b/src/sync/streaming/constants.ts index ce7215bf..33f24f92 100644 --- a/src/sync/streaming/constants.ts +++ b/src/sync/streaming/constants.ts @@ -30,6 +30,7 @@ export const MEMBERSHIPS_LS_UPDATE = 'MEMBERSHIPS_LS_UPDATE'; export const SEGMENT_UPDATE = 'SEGMENT_UPDATE'; export const SPLIT_KILL = 'SPLIT_KILL'; export const SPLIT_UPDATE = 'SPLIT_UPDATE'; +export const CONFIG_UPDATE = 'CONFIG_UPDATE'; export const RB_SEGMENT_UPDATE = 'RB_SEGMENT_UPDATE'; // Control-type push notifications, handled by NotificationKeeper diff --git a/src/sync/streaming/pushManager.ts b/src/sync/streaming/pushManager.ts index 1822ffb8..1beb6942 100644 --- a/src/sync/streaming/pushManager.ts +++ b/src/sync/streaming/pushManager.ts @@ -11,12 +11,12 @@ import { authenticateFactory, hashUserKey } from './AuthClient'; import { forOwn } from '../../utils/lang'; import { SSEClient } from './SSEClient'; import { checkIfServerSide, getMatching } from '../../utils/key'; -import { MEMBERSHIPS_MS_UPDATE, MEMBERSHIPS_LS_UPDATE, PUSH_NON_RETRYABLE_ERROR, PUSH_SUBSYSTEM_DOWN, SECONDS_BEFORE_EXPIRATION, SEGMENT_UPDATE, SPLIT_KILL, SPLIT_UPDATE, RB_SEGMENT_UPDATE, PUSH_RETRYABLE_ERROR, PUSH_SUBSYSTEM_UP, ControlType } from './constants'; +import { MEMBERSHIPS_MS_UPDATE, MEMBERSHIPS_LS_UPDATE, PUSH_NON_RETRYABLE_ERROR, PUSH_SUBSYSTEM_DOWN, SECONDS_BEFORE_EXPIRATION, SEGMENT_UPDATE, SPLIT_KILL, SPLIT_UPDATE, CONFIG_UPDATE, RB_SEGMENT_UPDATE, PUSH_RETRYABLE_ERROR, PUSH_SUBSYSTEM_UP, ControlType } from './constants'; import { STREAMING_FALLBACK, STREAMING_REFRESH_TOKEN, STREAMING_CONNECTING, STREAMING_DISABLED, ERROR_STREAMING_AUTH, STREAMING_DISCONNECTING, STREAMING_RECONNECT, STREAMING_PARSING_MEMBERSHIPS_UPDATE } from '../../logger/constants'; import { IMembershipMSUpdateData, IMembershipLSUpdateData, KeyList, UpdateStrategy } from './SSEHandler/types'; import { getDelay, isInBitmap, parseBitmap, parseCompressedData } from './parseUtils'; import { Hash64, hash64 } from '../../utils/murmur3/murmur3_64'; -import { IJwtCredentialV2 } from './AuthClient/types'; +import { IJwtCredential } from './AuthClient/types'; import { TOKEN_REFRESH, AUTH_REJECTION } from '../../utils/constants'; import { ISdkFactoryContextSync } from '../../sdkFactory/types'; @@ -81,7 +81,7 @@ export function pushManagerFactory( let timeoutIdTokenRefresh: ReturnType; let timeoutIdSseOpen: ReturnType; - function scheduleTokenRefreshAndSse(authData: IJwtCredentialV2) { + function scheduleTokenRefreshAndSse(authData: IJwtCredential) { // clear scheduled tasks if exist if (timeoutIdTokenRefresh) clearTimeout(timeoutIdTokenRefresh); if (timeoutIdSseOpen) clearTimeout(timeoutIdSseOpen); @@ -113,41 +113,37 @@ export function pushManagerFactory( disconnected = false; const userKeys = userKey ? Object.keys(clients) : undefined; - authenticate(userKeys).then( - function (authData) { - if (disconnected) return; - - // 'pushEnabled: false' is handled as a PUSH_NON_RETRYABLE_ERROR instead of PUSH_SUBSYSTEM_DOWN, in order to - // close the sseClient in case the org has been bloqued while the instance was connected to streaming - if (!authData.pushEnabled) { - log.info(STREAMING_DISABLED); - pushEmitter.emit(PUSH_NON_RETRYABLE_ERROR); - return; - } - - // [Only for client-side] don't open SSE connection if a new shared client was added, since it means that a new authentication is taking place - if (userKeys && userKeys.length < Object.keys(clients).length) return; + authenticate(userKeys).then((authData) => { + if (disconnected) return; - // Schedule SSE connection and refresh token - scheduleTokenRefreshAndSse(authData); + // 'pushEnabled: false' is handled as a PUSH_NON_RETRYABLE_ERROR instead of PUSH_SUBSYSTEM_DOWN, in order to + // close the sseClient in case the org has been bloqued while the instance was connected to streaming + if (!authData.pushEnabled) { + log.info(STREAMING_DISABLED); + pushEmitter.emit(PUSH_NON_RETRYABLE_ERROR); + return; } - ).catch( - function (error) { - if (disconnected) return; - log.error(ERROR_STREAMING_AUTH, [error.message]); + // [Only for client-side] don't open SSE connection if a new shared client was added, since it means that a new authentication is taking place + if (userKeys && userKeys.length < Object.keys(clients).length) return; - // Handle 4XX HTTP errors: 401 (invalid SDK Key) or 400 (using incorrect SDK Key, i.e., client-side SDK Key on server-side) - if (error.statusCode >= 400 && error.statusCode < 500) { - telemetryTracker.streamingEvent(AUTH_REJECTION); - pushEmitter.emit(PUSH_NON_RETRYABLE_ERROR); - return; - } + // Schedule SSE connection and refresh token + scheduleTokenRefreshAndSse(authData); + }).catch((error) => { + if (disconnected) return; + + log.error(ERROR_STREAMING_AUTH, [error.message]); - // Handle other HTTP and network errors as recoverable errors - pushEmitter.emit(PUSH_RETRYABLE_ERROR); + // Handle 4XX HTTP errors: 401 (invalid SDK Key) or 400 (using incorrect SDK Key, i.e., client-side SDK Key on server-side) + if (error.statusCode >= 400 && error.statusCode < 500) { + telemetryTracker.streamingEvent(AUTH_REJECTION); + pushEmitter.emit(PUSH_NON_RETRYABLE_ERROR); + return; } - ); + + // Handle other HTTP and network errors as recoverable errors + pushEmitter.emit(PUSH_RETRYABLE_ERROR); + }); } // close SSE connection and cancel scheduled tasks @@ -218,8 +214,11 @@ export function pushManagerFactory( /** Functions related to synchronization (Queues and Workers in the spec) */ + // Some notification types are specific to the Feature flags or Configs SDKs. Their handlers are + // registered unconditionally, since the SDK only subscribes to the channels of its own entities. pushEmitter.on(SPLIT_KILL, definitionsUpdateWorker.killDefinition); pushEmitter.on(SPLIT_UPDATE, definitionsUpdateWorker.put); + pushEmitter.on(CONFIG_UPDATE, definitionsUpdateWorker.put); pushEmitter.on(RB_SEGMENT_UPDATE, definitionsUpdateWorker.put); function handleMySegmentsUpdate(parsedData: IMembershipMSUpdateData | IMembershipLSUpdateData) { diff --git a/src/sync/streaming/types.ts b/src/sync/streaming/types.ts index 00e3fb67..61bd5225 100644 --- a/src/sync/streaming/types.ts +++ b/src/sync/streaming/types.ts @@ -16,19 +16,20 @@ export type MEMBERSHIPS_LS_UPDATE = 'MEMBERSHIPS_LS_UPDATE'; export type SEGMENT_UPDATE = 'SEGMENT_UPDATE'; export type SPLIT_KILL = 'SPLIT_KILL'; export type SPLIT_UPDATE = 'SPLIT_UPDATE'; +export type CONFIG_UPDATE = 'CONFIG_UPDATE'; export type RB_SEGMENT_UPDATE = 'RB_SEGMENT_UPDATE'; // Control-type push notifications, handled by NotificationKeeper export type CONTROL = 'CONTROL'; export type OCCUPANCY = 'OCCUPANCY'; -export type IPushEvent = PUSH_SUBSYSTEM_UP | PUSH_SUBSYSTEM_DOWN | PUSH_NON_RETRYABLE_ERROR | PUSH_RETRYABLE_ERROR | MEMBERSHIPS_MS_UPDATE | MEMBERSHIPS_LS_UPDATE | SEGMENT_UPDATE | SPLIT_UPDATE | SPLIT_KILL | RB_SEGMENT_UPDATE | ControlType.STREAMING_RESET +export type IPushEvent = PUSH_SUBSYSTEM_UP | PUSH_SUBSYSTEM_DOWN | PUSH_NON_RETRYABLE_ERROR | PUSH_RETRYABLE_ERROR | MEMBERSHIPS_MS_UPDATE | MEMBERSHIPS_LS_UPDATE | SEGMENT_UPDATE | SPLIT_UPDATE | CONFIG_UPDATE | SPLIT_KILL | RB_SEGMENT_UPDATE | ControlType.STREAMING_RESET type IParsedData = T extends MEMBERSHIPS_MS_UPDATE ? IMembershipMSUpdateData : T extends MEMBERSHIPS_LS_UPDATE ? IMembershipLSUpdateData : T extends SEGMENT_UPDATE ? ISegmentUpdateData : - T extends SPLIT_UPDATE | RB_SEGMENT_UPDATE ? ISplitUpdateData : + T extends SPLIT_UPDATE | CONFIG_UPDATE | RB_SEGMENT_UPDATE ? ISplitUpdateData : T extends SPLIT_KILL ? ISplitKillData : INotificationData; /** From bc185684c8893a3f92a3bfdfcd5f71261d3593be Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 15 Sep 2026 13:56:27 -0300 Subject: [PATCH 2/8] Remove unnecessary hashUserKey function --- src/sync/streaming/AuthClient/__tests__/index.spec.ts | 10 ++-------- src/sync/streaming/AuthClient/index.ts | 9 --------- src/sync/streaming/__tests__/dataMocks.ts | 4 ---- src/sync/streaming/pushManager.ts | 11 ++--------- 4 files changed, 4 insertions(+), 30 deletions(-) diff --git a/src/sync/streaming/AuthClient/__tests__/index.spec.ts b/src/sync/streaming/AuthClient/__tests__/index.spec.ts index 54bb03cb..6646cfa8 100644 --- a/src/sync/streaming/AuthClient/__tests__/index.spec.ts +++ b/src/sync/streaming/AuthClient/__tests__/index.spec.ts @@ -1,24 +1,18 @@ // mocks and dependencies import { splitApiFactory } from '../../../../services/splitApi'; -import { authDataResponseSample, authDataSample, jwtSampleInvalid, jwtSampleNoChannels, jwtSampleNoIat, userKeySample, userKeyBase64HashSample } from '../../__tests__/dataMocks'; +import { authDataResponseSample, authDataSample, jwtSampleInvalid, jwtSampleNoChannels, jwtSampleNoIat } from '../../__tests__/dataMocks'; import fetchMock from '../../../../__tests__/testUtils/fetchMock'; import { settingsServiceApi } from '../../../../utils/settingsValidation/__tests__/settings.mocks'; import { telemetryTrackerFactory } from '../../../../trackers/telemetryTracker'; // module to test -import { authenticateFactory, hashUserKey } from '../index'; +import { authenticateFactory } from '../index'; const authorizationKey = settingsServiceApi.core.authorizationKey; const authUrl = settingsServiceApi.urls.auth; // @ts-ignore const serviceApi = splitApiFactory(settingsServiceApi, { getFetch: () => fetchMock }, telemetryTrackerFactory()); const authenticate = authenticateFactory(serviceApi.fetchAuth); -test('hashUserKey', () => { - - expect(hashUserKey(userKeySample)).toBe(userKeyBase64HashSample); // hashes key and encodes to base64 - -}); - test('authenticate / success in node (200)', done => { fetchMock.getOnce(authUrl + '/v2/auth?s=1.1', (url, opts) => { diff --git a/src/sync/streaming/AuthClient/index.ts b/src/sync/streaming/AuthClient/index.ts index 40e02161..ed9ae913 100644 --- a/src/sync/streaming/AuthClient/index.ts +++ b/src/sync/streaming/AuthClient/index.ts @@ -1,9 +1,7 @@ import { IFetchAuth } from '../../../services/types'; import { IAuthenticate, IJwtCredential } from './types'; import { objectAssign } from '../../../utils/lang/objectAssign'; -import { encodeToBase64 } from '../../../utils/base64'; import { decodeJWTtoken } from '../../../utils/jwt'; -import { hash } from '../../../utils/murmur3/murmur3'; /** * Factory of authentication function. @@ -41,10 +39,3 @@ export function authenticateFactory(fetchAuth: IFetchAuth): IAuthenticate { }); }; } - -/** - * Returns the hash of a given user key - */ -export function hashUserKey(userKey: string): string { - return encodeToBase64(hash(userKey, 0).toString()); -} diff --git a/src/sync/streaming/__tests__/dataMocks.ts b/src/sync/streaming/__tests__/dataMocks.ts index cb7007d8..2a1ebf6f 100644 --- a/src/sync/streaming/__tests__/dataMocks.ts +++ b/src/sync/streaming/__tests__/dataMocks.ts @@ -39,10 +39,6 @@ export const authDataSample = { channels: parsedChannelsSample, }; -export const userKeySample = 'emi@split.io'; - -export const userKeyBase64HashSample = 'MjAxNjU2NDU5Mw=='; - export const channelsQueryParamSample = 'NzM2MDI5Mzc0_MzQyODU4NDUyNg%3D%3D_segments,NzM2MDI5Mzc0_MzQyODU4NDUyNg%3D%3D_splits,control'; export const keylists = [ diff --git a/src/sync/streaming/pushManager.ts b/src/sync/streaming/pushManager.ts index 1beb6942..883a84b6 100644 --- a/src/sync/streaming/pushManager.ts +++ b/src/sync/streaming/pushManager.ts @@ -7,7 +7,7 @@ import { SSEHandlerFactory } from './SSEHandler'; import { MySegmentsUpdateWorker } from './UpdateWorkers/MySegmentsUpdateWorker'; import { SegmentsUpdateWorker } from './UpdateWorkers/SegmentsUpdateWorker'; import { DefinitionsUpdateWorker } from './UpdateWorkers/DefinitionsUpdateWorker'; -import { authenticateFactory, hashUserKey } from './AuthClient'; +import { authenticateFactory } from './AuthClient'; import { forOwn } from '../../utils/lang'; import { SSEClient } from './SSEClient'; import { checkIfServerSide, getMatching } from '../../utils/key'; @@ -58,8 +58,6 @@ export function pushManagerFactory( // For server-side we pass the segmentsSyncTask, used by DefinitionsUpdateWorker to fetch new segments const definitionsUpdateWorker = DefinitionsUpdateWorker(log, storage, pollingManager.definitionsSyncTask, readiness.definitions, telemetryTracker); - // [Only for client-side] map of hashes to user keys, to dispatch membership update events to the corresponding MySegmentsUpdateWorker - const userKeyHashes: Record = {}; // [Only for client-side] map of user keys to their corresponding hash64 and MySegmentsUpdateWorkers. // Hash64 is used to process membership update events and dispatch actions to the corresponding MySegmentsUpdateWorker. const clients: Record }> = {}; @@ -323,10 +321,7 @@ export function pushManagerFactory( // [Only for client-side] add(userKey: string, mySegmentsSyncTask: IMySegmentsSyncTask) { - const hash = hashUserKey(userKey); - - if (!userKeyHashes[hash]) { - userKeyHashes[hash] = userKey; + if (!clients[userKey]) { clients[userKey] = { hash64: hash64(userKey), worker: MySegmentsUpdateWorker(log, storage, mySegmentsSyncTask, telemetryTracker) @@ -348,8 +343,6 @@ export function pushManagerFactory( }, // [Only for client-side] remove(userKey: string) { - const hash = hashUserKey(userKey); - delete userKeyHashes[hash]; delete clients[userKey]; } } From 06497337a90d85676f9224caec02f8de9a74374a Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 15 Sep 2026 14:26:54 -0300 Subject: [PATCH 3/8] Align splitApi and serviceApi interfaces --- .../__tests__/secureSplitHttpClient.spec.ts | 6 +++++- src/services/secureSplitHttpClient.ts | 18 +++++------------- src/services/serviceApi.ts | 9 ++++++--- 3 files changed, 16 insertions(+), 17 deletions(-) diff --git a/src/services/__tests__/secureSplitHttpClient.spec.ts b/src/services/__tests__/secureSplitHttpClient.spec.ts index bc3d1307..cb2f28b2 100644 --- a/src/services/__tests__/secureSplitHttpClient.spec.ts +++ b/src/services/__tests__/secureSplitHttpClient.spec.ts @@ -1,4 +1,6 @@ import { secureSplitHttpClientFactory } from '../secureSplitHttpClient'; +import { splitHttpClientFactory } from '../splitHttpClient'; +import { authProviderFactory } from '../authProvider'; import { Backoff } from '../../utils/Backoff'; import { makeJwtCredential } from '../../__tests__/testUtils/jwt'; @@ -27,7 +29,9 @@ function createSecureSplitHttpClient(configsHandler: (callCount: number) => any) configsCallCount++; return configsHandler(configsCallCount); }); - const client = secureSplitHttpClientFactory(mockSettings, { getFetch: () => fetchImpl, getOptions: () => undefined }, mockTelemetryTracker); + const splitHttpClient = splitHttpClientFactory(mockSettings, { getFetch: () => fetchImpl, getOptions: () => undefined }); + const authProvider = authProviderFactory(mockSettings, splitHttpClient, mockTelemetryTracker); + const client = secureSplitHttpClientFactory(splitHttpClient, authProvider); return { client, fetchImpl }; } diff --git a/src/services/secureSplitHttpClient.ts b/src/services/secureSplitHttpClient.ts index 8c7273c9..afa7abec 100644 --- a/src/services/secureSplitHttpClient.ts +++ b/src/services/secureSplitHttpClient.ts @@ -1,22 +1,14 @@ -import { IRequestOptions, IResponse, ISecureSplitHttpClient, NetworkError } from './types'; -import { ISettings } from '../types'; -import { IPlatform } from '../sdkFactory/types'; -import { splitHttpClientFactory } from './splitHttpClient'; -import { authProviderFactory } from './authProvider'; -import { ITelemetryTracker } from '../trackers/types'; +import { IRequestOptions, IResponse, ISecureSplitHttpClient, ISplitHttpClient, NetworkError } from './types'; +import { IAuthProvider } from './authProvider'; /** * Factory of Secure HTTP client, which authenticates requests using a JWT token. * On 401 responses, invalidates the cached credential and retries once with a fresh token. * - * @param settings - SDK settings - * @param platform - object containing environment-specific dependencies - * @param fetchAuth - function to fetch auth credentials from the /v2/auth endpoint + * @param splitHttpClient - `splitHttpClientFactory` to use for making requests + * @param authProvider - `authProviderFactory` to use for authentication */ -export function secureSplitHttpClientFactory(settings: ISettings, platform: Pick, telemetryTracker: ITelemetryTracker): ISecureSplitHttpClient { - - const splitHttpClient = splitHttpClientFactory(settings, platform); - const authProvider = authProviderFactory(settings, splitHttpClient, telemetryTracker); +export function secureSplitHttpClientFactory(splitHttpClient: ISplitHttpClient, authProvider: IAuthProvider): ISecureSplitHttpClient { function makeRequest(url: string, options: IRequestOptions | undefined, latencyTracker: ((error?: NetworkError) => void) | undefined, logErrorsAsInfo: boolean | undefined, token: string): Promise { return splitHttpClient(url, { ...options, headers: { ...options?.headers, Authorization: `Bearer ${token}` } }, latencyTracker, logErrorsAsInfo, true); diff --git a/src/services/serviceApi.ts b/src/services/serviceApi.ts index 207b3b08..4fe41819 100644 --- a/src/services/serviceApi.ts +++ b/src/services/serviceApi.ts @@ -1,11 +1,13 @@ import { IPlatform } from '../sdkFactory/types'; import { ISettings } from '../types'; import { splitHttpClientFactory } from './splitHttpClient'; -import { ISecureSplitHttpClient, IServiceApi } from './types'; +import { IServiceApi } from './types'; import { objectAssign } from '../utils/lang/objectAssign'; import { ITelemetryTracker } from '../trackers/types'; import { SPLITS, IMPRESSIONS, IMPRESSIONS_COUNT, EVENTS, TELEMETRY, TOKEN, SEGMENT, MEMBERSHIPS } from '../utils/constants'; import { ERROR_TOO_MANY_SETS } from '../logger/constants'; +import { secureSplitHttpClientFactory } from './secureSplitHttpClient'; +import { authProviderFactory } from './authProvider'; const noCacheHeaderOptions = { headers: { 'Cache-Control': 'no-cache' } }; @@ -25,14 +27,15 @@ export function serviceApiFactory( settings: ISettings, platform: Pick, telemetryTracker: ITelemetryTracker, - secureSplitHttpClientFactory?: (settings: ISettings, platform: Pick, telemetryTracker: ITelemetryTracker) => ISecureSplitHttpClient, ): IServiceApi { const urls = settings.urls; const filterQueryString = settings.sync.__splitFiltersValidation && settings.sync.__splitFiltersValidation.queryString; const SplitSDKImpressionsMode = settings.sync.impressionsMode; + const splitHttpClient = splitHttpClientFactory(settings, platform); - const secureSplitHttpClient = secureSplitHttpClientFactory!(settings, platform, telemetryTracker); + const authProvider = authProviderFactory(settings, splitHttpClient, telemetryTracker); + const secureSplitHttpClient = secureSplitHttpClientFactory(splitHttpClient, authProvider); return { // @TODO throw errors if health check requests fail, to log them in the Synchronizer From 4df667480d6b5b26f374a9a82fc661e907829450 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 15 Sep 2026 16:08:51 -0300 Subject: [PATCH 4/8] Unify authentication behind serviceApi.authenticate --- src/services/__tests__/splitApi.spec.ts | 4 +-- src/services/authProvider.ts | 18 +++++++---- src/services/serviceApi.ts | 32 +++++++++---------- src/services/splitApi.ts | 19 +++++------ src/services/types.ts | 4 +-- .../AuthClient/__tests__/index.spec.ts | 5 +-- src/sync/streaming/AuthClient/index.ts | 8 ++--- src/sync/streaming/AuthClient/types.ts | 2 +- src/sync/streaming/pushManager.ts | 4 +-- 9 files changed, 49 insertions(+), 47 deletions(-) diff --git a/src/services/__tests__/splitApi.spec.ts b/src/services/__tests__/splitApi.spec.ts index a77c1708..f3b82780 100644 --- a/src/services/__tests__/splitApi.spec.ts +++ b/src/services/__tests__/splitApi.spec.ts @@ -22,7 +22,7 @@ describe('serviceApi', () => { test.each([settingsServiceApi, settingsWithRuntime, settingsWithSets])('performs requests with expected headers', (settings) => { - const fetchMock = jest.fn(() => Promise.resolve({ ok: true })); + const fetchMock = jest.fn(() => Promise.resolve({ ok: true, json: () => Promise.resolve({}) })); const serviceApi = splitApiFactory(settings, { getFetch: () => fetchMock }, telemetryTrackerMock); serviceApi.fetchAuth(['key1', 'key2']); @@ -92,7 +92,7 @@ describe('serviceApi', () => { const serviceApi = splitApiFactory(settingsServiceApi, { getFetch: () => undefined }, telemetryTrackerMock); // Invoking any Service method, returns a rejected promise with Split error - serviceApi.fetchAuth().catch(error => { + serviceApi.fetchConfigs().catch(error => { expect(error).toBeInstanceOf(Error); expect(error.message).toBe('Global fetch API is not available.'); done(); diff --git a/src/services/authProvider.ts b/src/services/authProvider.ts index eefde72d..efd4c47a 100644 --- a/src/services/authProvider.ts +++ b/src/services/authProvider.ts @@ -1,6 +1,6 @@ import { ISplitHttpClient, NetworkError } from './types'; import { IJwtCredential } from '../sync/streaming/AuthClient/types'; -import { authenticateFactory } from '../sync/streaming/AuthClient'; +import { fetchAuthFactory } from '../sync/streaming/AuthClient'; import { Backoff } from '../utils/Backoff'; import { LOG_PREFIX_SYNC_AUTH } from '../logger/constants'; import { ISettings } from '../types'; @@ -14,7 +14,15 @@ function isExpired(credential: IJwtCredential): boolean { } export interface IAuthProvider { + /** + * Returns the cached credential, or fetches a new one if there isn't one cached or it's expired, + * retrying with backoff on recoverable errors. Used by `secureSplitHttpClient` and `serviceApi.fetchAuth`. + */ credential(): Promise; + /** + * Invalidates/clears the cached credential. Used by `secureSplitHttpClient` in the special case of 401 error, + * and by `serviceApi.fetchAuth` to force a credential/token refresh. + */ invalidate(): void; stop(): void; } @@ -27,12 +35,10 @@ export function authProviderFactory(settings: ISettings, splitHttpClient: ISplit const { urls, log } = settings; - function fetchAuth() { + const fetchAuth = fetchAuthFactory(() => { let url = `${urls.auth}/api/v3/auth?capabilities=config,aiconfig`; return splitHttpClient(url, undefined, telemetryTracker.trackHttp(TOKEN), false, true); - } - - const authenticate = authenticateFactory(fetchAuth); + }); const backoff = new Backoff(fetchCredential); let cachedCredential: IJwtCredential | undefined; @@ -40,7 +46,7 @@ export function authProviderFactory(settings: ISettings, splitHttpClient: ISplit let stopped = false; function fetchCredential(): Promise { - return authenticate().then((credential: IJwtCredential) => { + return fetchAuth().then((credential: IJwtCredential) => { log.info(LOG_PREFIX_SYNC_AUTH + 'credential fetched successfully'); cachedCredential = credential; inFlightPromise = undefined; diff --git a/src/services/serviceApi.ts b/src/services/serviceApi.ts index 4fe41819..0780097c 100644 --- a/src/services/serviceApi.ts +++ b/src/services/serviceApi.ts @@ -4,24 +4,19 @@ import { splitHttpClientFactory } from './splitHttpClient'; import { IServiceApi } from './types'; import { objectAssign } from '../utils/lang/objectAssign'; import { ITelemetryTracker } from '../trackers/types'; -import { SPLITS, IMPRESSIONS, IMPRESSIONS_COUNT, EVENTS, TELEMETRY, TOKEN, SEGMENT, MEMBERSHIPS } from '../utils/constants'; +import { SPLITS, IMPRESSIONS, IMPRESSIONS_COUNT, EVENTS, TELEMETRY, SEGMENT, MEMBERSHIPS } from '../utils/constants'; import { ERROR_TOO_MANY_SETS } from '../logger/constants'; -import { secureSplitHttpClientFactory } from './secureSplitHttpClient'; import { authProviderFactory } from './authProvider'; +import { secureSplitHttpClientFactory } from './secureSplitHttpClient'; const noCacheHeaderOptions = { headers: { 'Cache-Control': 'no-cache' } }; -function userKeyToQueryParam(userKey: string) { - return 'users=' + encodeURIComponent(userKey); // no need to check availability of `encodeURIComponent`, since it is a global highly supported. -} - /** * Factory of ServiceApi objects, which group the collection of HTTP endpoints used by the SDKs * * @param settings - validated settings object * @param platform - object containing environment-specific dependencies * @param telemetryTracker - telemetry tracker - * @param secureSplitHttpClientFactory - factory of SecureSplitHttpClient objects */ export function serviceApiFactory( settings: ISettings, @@ -34,10 +29,24 @@ export function serviceApiFactory( const SplitSDKImpressionsMode = settings.sync.impressionsMode; const splitHttpClient = splitHttpClientFactory(settings, platform); + + // Shared authProvider so that the SSE authentication (via `fetchAuth`, + // used by `pushManager`) and every authenticated HTTP request to the Configs + // API reuse the same cached credential instead of each fetching their own. const authProvider = authProviderFactory(settings, splitHttpClient, telemetryTracker); const secureSplitHttpClient = secureSplitHttpClientFactory(splitHttpClient, authProvider); + let initialAuth = true; + return { + fetchAuth() { + // Guard condition to avoid invalidating the cached credential on the first pushManager authentication + if (initialAuth) initialAuth = false; + else authProvider.invalidate(); + + return authProvider.credential(); + }, + // @TODO throw errors if health check requests fail, to log them in the Synchronizer getSdkAPIHealthCheck() { const url = `${urls.sdk}/api/version`; @@ -49,15 +58,6 @@ export function serviceApiFactory( return splitHttpClient(url).then(() => true).catch(() => false); }, - fetchAuth(userMatchingKeys?: string[]) { - let url = `${urls.auth}/api/v2/auth?s=${settings.sync.flagSpecVersion}`; - if (userMatchingKeys) { // `userMatchingKeys` is undefined in server-side - const queryParams = userMatchingKeys.map(userKeyToQueryParam).join('&'); - if (queryParams) url += '&' + queryParams; - } - return splitHttpClient(url, undefined, telemetryTracker.trackHttp(TOKEN)); - }, - fetchSplitChanges(since: number, noCache?: boolean, till?: number, rbSince?: number) { const url = `${urls.sdk}/api/splitChanges?s=${settings.sync.flagSpecVersion}&since=${since}${rbSince ? '&rbSince=' + rbSince : ''}${filterQueryString || ''}${till ? '&till=' + till : ''}`; return splitHttpClient(url, noCache ? noCacheHeaderOptions : undefined, telemetryTracker.trackHttp(SPLITS)) diff --git a/src/services/splitApi.ts b/src/services/splitApi.ts index ba7ff38c..21b02245 100644 --- a/src/services/splitApi.ts +++ b/src/services/splitApi.ts @@ -6,6 +6,7 @@ import { objectAssign } from '../utils/lang/objectAssign'; import { ITelemetryTracker } from '../trackers/types'; import { SPLITS, IMPRESSIONS, IMPRESSIONS_COUNT, EVENTS, TELEMETRY, TOKEN, SEGMENT, MEMBERSHIPS } from '../utils/constants'; import { ERROR_TOO_MANY_SETS } from '../logger/constants'; +import { fetchAuthFactory } from '../sync/streaming/AuthClient'; const noCacheHeaderOptions = { headers: { 'Cache-Control': 'no-cache' } }; @@ -33,6 +34,15 @@ export function splitApiFactory( const splitHttpClient = splitHttpClientFactory(settings, platform); return { + fetchAuth: fetchAuthFactory((userMatchingKeys?: string[]) => { + let url = `${urls.auth}/v2/auth?s=${settings.sync.flagSpecVersion}`; + if (userMatchingKeys) { // `userMatchingKeys` is undefined in server-side + const queryParams = userMatchingKeys.map(userKeyToQueryParam).join('&'); + if (queryParams) url += '&' + queryParams; + } + return splitHttpClient(url, undefined, telemetryTracker.trackHttp(TOKEN)); + }), + // @TODO throw errors if health check requests fail, to log them in the Synchronizer getSdkAPIHealthCheck() { const url = `${urls.sdk}/version`; @@ -44,15 +54,6 @@ export function splitApiFactory( return splitHttpClient(url).then(() => true).catch(() => false); }, - fetchAuth(userMatchingKeys?: string[]) { - let url = `${urls.auth}/v2/auth?s=${settings.sync.flagSpecVersion}`; - if (userMatchingKeys) { // `userMatchingKeys` is undefined in server-side - const queryParams = userMatchingKeys.map(userKeyToQueryParam).join('&'); - if (queryParams) url += '&' + queryParams; - } - return splitHttpClient(url, undefined, telemetryTracker.trackHttp(TOKEN)); - }, - fetchSplitChanges(since: number, noCache?: boolean, till?: number, rbSince?: number) { const url = `${urls.sdk}/splitChanges?s=${settings.sync.flagSpecVersion}&since=${since}${rbSince ? '&rbSince=' + rbSince : ''}${filterQueryString || ''}${till ? '&till=' + till : ''}`; return splitHttpClient(url, noCache ? noCacheHeaderOptions : undefined, telemetryTracker.trackHttp(SPLITS)) diff --git a/src/services/types.ts b/src/services/types.ts index 50bd3f30..43cd7432 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -1,3 +1,5 @@ +import { IFetchAuth } from '../sync/streaming/AuthClient/types'; + export type IRequestOptions = { method?: string, headers?: Record, @@ -35,8 +37,6 @@ export type ISplitHttpClient = (url: string, options?: IRequestOptions, latencyT export type ISecureSplitHttpClient = ISplitHttpClient & { stop(): void } -export type IFetchAuth = (userKeys?: string[]) => Promise - export type IFetchDefinitionChanges = (since: number, noCache?: boolean, till?: number, rbSince?: number) => Promise export type IFetchSegmentChanges = (since: number, segmentName: string, noCache?: boolean, till?: number) => Promise diff --git a/src/sync/streaming/AuthClient/__tests__/index.spec.ts b/src/sync/streaming/AuthClient/__tests__/index.spec.ts index 6646cfa8..5264f145 100644 --- a/src/sync/streaming/AuthClient/__tests__/index.spec.ts +++ b/src/sync/streaming/AuthClient/__tests__/index.spec.ts @@ -5,13 +5,10 @@ import fetchMock from '../../../../__tests__/testUtils/fetchMock'; import { settingsServiceApi } from '../../../../utils/settingsValidation/__tests__/settings.mocks'; import { telemetryTrackerFactory } from '../../../../trackers/telemetryTracker'; -// module to test -import { authenticateFactory } from '../index'; - const authorizationKey = settingsServiceApi.core.authorizationKey; const authUrl = settingsServiceApi.urls.auth; // @ts-ignore const serviceApi = splitApiFactory(settingsServiceApi, { getFetch: () => fetchMock }, telemetryTrackerFactory()); -const authenticate = authenticateFactory(serviceApi.fetchAuth); +const authenticate = serviceApi.fetchAuth; test('authenticate / success in node (200)', done => { diff --git a/src/sync/streaming/AuthClient/index.ts b/src/sync/streaming/AuthClient/index.ts index ed9ae913..db7692e2 100644 --- a/src/sync/streaming/AuthClient/index.ts +++ b/src/sync/streaming/AuthClient/index.ts @@ -1,14 +1,14 @@ -import { IFetchAuth } from '../../../services/types'; -import { IAuthenticate, IJwtCredential } from './types'; +import { IJwtCredential, IFetchAuth } from './types'; import { objectAssign } from '../../../utils/lang/objectAssign'; import { decodeJWTtoken } from '../../../utils/jwt'; +import { IResponse } from '../../../services/types'; /** * Factory of authentication function. * - * @param fetchAuth - `ServiceApi.fetchAuth` endpoint + * @param fetchAuth - /auth endpoint */ -export function authenticateFactory(fetchAuth: IFetchAuth): IAuthenticate { +export function fetchAuthFactory(fetchAuth: (userKeys?: string[]) => Promise): IFetchAuth { /** * Run authentication requests to Auth Server, and returns a promise that resolves with the decoded JTW token. diff --git a/src/sync/streaming/AuthClient/types.ts b/src/sync/streaming/AuthClient/types.ts index daf3adfc..1c982a75 100644 --- a/src/sync/streaming/AuthClient/types.ts +++ b/src/sync/streaming/AuthClient/types.ts @@ -16,4 +16,4 @@ export type IJwtCredential = { } | null; } -export type IAuthenticate = (userKeys?: string[]) => Promise +export type IFetchAuth = (userKeys?: string[]) => Promise diff --git a/src/sync/streaming/pushManager.ts b/src/sync/streaming/pushManager.ts index 883a84b6..5c1efefe 100644 --- a/src/sync/streaming/pushManager.ts +++ b/src/sync/streaming/pushManager.ts @@ -7,7 +7,6 @@ import { SSEHandlerFactory } from './SSEHandler'; import { MySegmentsUpdateWorker } from './UpdateWorkers/MySegmentsUpdateWorker'; import { SegmentsUpdateWorker } from './UpdateWorkers/SegmentsUpdateWorker'; import { DefinitionsUpdateWorker } from './UpdateWorkers/DefinitionsUpdateWorker'; -import { authenticateFactory } from './AuthClient'; import { forOwn } from '../../utils/lang'; import { SSEClient } from './SSEClient'; import { checkIfServerSide, getMatching } from '../../utils/key'; @@ -45,7 +44,6 @@ export function pushManagerFactory( log.warn(STREAMING_FALLBACK, [e]); return; } - const authenticate = authenticateFactory(serviceApi.fetchAuth); // init feedback loop const pushEmitter = new platform.EventEmitter() as IPushEventEmitter; @@ -111,7 +109,7 @@ export function pushManagerFactory( disconnected = false; const userKeys = userKey ? Object.keys(clients) : undefined; - authenticate(userKeys).then((authData) => { + serviceApi.fetchAuth(userKeys).then((authData) => { if (disconnected) return; // 'pushEnabled: false' is handled as a PUSH_NON_RETRYABLE_ERROR instead of PUSH_SUBSYSTEM_DOWN, in order to From cd6d2ff50f578a8d0299b65862358567932e5e2e Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 16 Sep 2026 20:36:24 -0300 Subject: [PATCH 5/8] Re-trigger CI From 34d9667e3a86820f58ce8bf27d9f31097aed78c1 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 16 Sep 2026 20:44:15 -0300 Subject: [PATCH 6/8] Re-trigger CI From 8cf85e7c3266f95b5d3371d27abcd9434d2d7e7f Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 17 Sep 2026 12:08:15 -0300 Subject: [PATCH 7/8] rc --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4641572c..5028cbeb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@splitsoftware/splitio-commons", - "version": "3.1.1-rc.4", + "version": "3.1.1-rc.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@splitsoftware/splitio-commons", - "version": "3.1.1-rc.4", + "version": "3.1.1-rc.5", "license": "Apache-2.0", "dependencies": { "@types/ioredis": "^4.28.0", diff --git a/package.json b/package.json index 861726e6..e2e09de7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "3.1.1-rc.4", + "version": "3.1.1-rc.5", "description": "Split JavaScript SDK common components", "main": "cjs/index.js", "module": "esm/index.js", From 65db3f90dde43cb5b516833599ef21388da113a1 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Mon, 21 Sep 2026 17:15:32 -0300 Subject: [PATCH 8/8] Update submitter's service endpoints to wait for /auth response before executing requests --- CHANGES.txt | 2 +- package-lock.json | 4 ++-- package.json | 2 +- src/sdkClient/sdkLifecycle.ts | 3 ++- src/services/secureSplitHttpClient.ts | 10 +++++----- src/services/serviceApi.ts | 12 ++++++------ src/services/types.ts | 4 ++-- 7 files changed, 19 insertions(+), 18 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 6da11eb6..16d0c026 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,4 +1,4 @@ -3.2.0 (August 28, 2026) +3.2.0 (September 18, 2026) - Added support for AI configs. - Removed Configs SDK-related types and modules and moved them to the Configs SDK repository. diff --git a/package-lock.json b/package-lock.json index 5028cbeb..475804b6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@splitsoftware/splitio-commons", - "version": "3.1.1-rc.5", + "version": "3.1.1-rc.7", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@splitsoftware/splitio-commons", - "version": "3.1.1-rc.5", + "version": "3.1.1-rc.7", "license": "Apache-2.0", "dependencies": { "@types/ioredis": "^4.28.0", diff --git a/package.json b/package.json index e2e09de7..7b767d0b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "3.1.1-rc.5", + "version": "3.1.1-rc.7", "description": "Split JavaScript SDK common components", "main": "cjs/index.js", "module": "esm/index.js", diff --git a/src/sdkClient/sdkLifecycle.ts b/src/sdkClient/sdkLifecycle.ts index 6e4d1b95..10ac33b2 100644 --- a/src/sdkClient/sdkLifecycle.ts +++ b/src/sdkClient/sdkLifecycle.ts @@ -69,9 +69,10 @@ export function sdkLifecycleFactory(params: ISdkFactoryContext, isSharedClient?: // Stop background jobs syncManager && syncManager.stop(); - serviceApi && serviceApi.stop(); return __flush().then(() => { + // Stop the service API (the auth provider backoff retries specifically) after the final flush, so it can still authenticate using the still-valid cached credential + serviceApi && serviceApi.stop(); // Cleanup storage return storage.destroy(); }); diff --git a/src/services/secureSplitHttpClient.ts b/src/services/secureSplitHttpClient.ts index afa7abec..660a57f7 100644 --- a/src/services/secureSplitHttpClient.ts +++ b/src/services/secureSplitHttpClient.ts @@ -10,19 +10,19 @@ import { IAuthProvider } from './authProvider'; */ export function secureSplitHttpClientFactory(splitHttpClient: ISplitHttpClient, authProvider: IAuthProvider): ISecureSplitHttpClient { - function makeRequest(url: string, options: IRequestOptions | undefined, latencyTracker: ((error?: NetworkError) => void) | undefined, logErrorsAsInfo: boolean | undefined, token: string): Promise { - return splitHttpClient(url, { ...options, headers: { ...options?.headers, Authorization: `Bearer ${token}` } }, latencyTracker, logErrorsAsInfo, true); + function makeRequest(url: string, options: IRequestOptions | undefined, latencyTracker?: (error?: NetworkError) => void, logErrorsAsInfo?: boolean, newVersionHeader?: boolean, token?: string): Promise { + return splitHttpClient(url, token ? { ...options, headers: { ...options?.headers, Authorization: `Bearer ${token}` } } : options, latencyTracker, logErrorsAsInfo, newVersionHeader); } - const httpClient = function (url: string, options?: IRequestOptions, latencyTracker?: (error?: NetworkError) => void, logErrorsAsInfo?: boolean): Promise { + const httpClient = function (url: string, options?: IRequestOptions, latencyTracker?: (error?: NetworkError) => void, logErrorsAsInfo?: boolean, newVersionHeader = true, useJwt = true): Promise { return authProvider.credential().then(credential => { - return makeRequest(url, options, latencyTracker, logErrorsAsInfo, credential.token) + return makeRequest(url, options, latencyTracker, logErrorsAsInfo, newVersionHeader, useJwt ? credential.token : undefined) .catch((error: NetworkError) => { if (error.statusCode === 401) { // retry once for 401, in case the token has just expired authProvider.invalidate(); return authProvider.credential().then(newCredential => { - return makeRequest(url, options, latencyTracker, logErrorsAsInfo, newCredential.token); + return makeRequest(url, options, latencyTracker, logErrorsAsInfo, newVersionHeader, useJwt ? newCredential.token : undefined); }); } throw error; diff --git a/src/services/serviceApi.ts b/src/services/serviceApi.ts index 0780097c..f97f67a6 100644 --- a/src/services/serviceApi.ts +++ b/src/services/serviceApi.ts @@ -102,7 +102,7 @@ export function serviceApiFactory( */ postEventsBulk(body: string, headers?: Record) { const url = `${urls.events}/api/events/bulk`; - return splitHttpClient(url, { method: 'POST', body, headers }, telemetryTracker.trackHttp(EVENTS)); + return secureSplitHttpClient(url, { method: 'POST', body, headers }, telemetryTracker.trackHttp(EVENTS), false, false, false); }, /** @@ -113,10 +113,10 @@ export function serviceApiFactory( */ postTestImpressionsBulk(body: string, headers?: Record) { const url = `${urls.events}/api/testImpressions/bulk`; - return splitHttpClient(url, { + return secureSplitHttpClient(url, { // Adding extra headers to send impressions in OPTIMIZED or DEBUG modes. method: 'POST', body, headers: objectAssign({ SplitSDKImpressionsMode }, headers) - }, telemetryTracker.trackHttp(IMPRESSIONS)); + }, telemetryTracker.trackHttp(IMPRESSIONS), false, false, false); }, /** @@ -127,7 +127,7 @@ export function serviceApiFactory( */ postTestImpressionsCount(body: string, headers?: Record) { const url = `${urls.events}/api/testImpressions/count`; - return splitHttpClient(url, { method: 'POST', body, headers }, telemetryTracker.trackHttp(IMPRESSIONS_COUNT)); + return secureSplitHttpClient(url, { method: 'POST', body, headers }, telemetryTracker.trackHttp(IMPRESSIONS_COUNT), false, false, false); }, /** @@ -138,7 +138,7 @@ export function serviceApiFactory( */ postUniqueKeysBulkCs(body: string, headers?: Record) { const url = `${urls.telemetry}/api/v1/keys/cs`; - return splitHttpClient(url, { method: 'POST', body, headers }, telemetryTracker.trackHttp(TELEMETRY)); + return secureSplitHttpClient(url, { method: 'POST', body, headers }, telemetryTracker.trackHttp(TELEMETRY), false, false, false); }, /** @@ -149,7 +149,7 @@ export function serviceApiFactory( */ postUniqueKeysBulkSs(body: string, headers?: Record) { const url = `${urls.telemetry}/api/v1/keys/ss`; - return splitHttpClient(url, { method: 'POST', body, headers }, telemetryTracker.trackHttp(TELEMETRY)); + return secureSplitHttpClient(url, { method: 'POST', body, headers }, telemetryTracker.trackHttp(TELEMETRY), false, false, false); }, postMetricsConfig(body: string, headers?: Record) { diff --git a/src/services/types.ts b/src/services/types.ts index 43cd7432..2866ba3e 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -35,7 +35,7 @@ export type IHealthCheckAPI = () => Promise export type ISplitHttpClient = (url: string, options?: IRequestOptions, latencyTracker?: (error?: NetworkError) => void, logErrorsAsInfo?: boolean, newVersionHeader?: boolean) => Promise -export type ISecureSplitHttpClient = ISplitHttpClient & { stop(): void } +export type ISecureSplitHttpClient = ((url: string, options?: IRequestOptions, latencyTracker?: (error?: NetworkError) => void, logErrorsAsInfo?: boolean, newVersionHeader?: boolean, useJwt?: boolean) => Promise) & { stop(): void } export type IFetchDefinitionChanges = (since: number, noCache?: boolean, till?: number, rbSince?: number) => Promise @@ -73,7 +73,7 @@ export interface IServiceApi { postTestImpressionsCount: IPostTestImpressionsCount postMetricsConfig: IPostMetricsConfig postMetricsUsage: IPostMetricsUsage - // lifecycle + // lifecycle: stops authProvider backoff retries stop(): void }