From 8e4a5f674e715a9dabfdc5a7162946da8389d5a6 Mon Sep 17 00:00:00 2001 From: Devesh Bhardwaj Date: Mon, 10 Aug 2026 01:17:45 +0530 Subject: [PATCH] SK-2963: warn at startup when a beta/dev build targets a Production vault Adds a WARN-level log, emitted once in Skyflow.init(), when the SDK's own version is non-GA (any -beta.N or -dev. suffix) and the customer's vaultURL doesn't carry a recognized non-prod domain marker. Unlike the server SDKs, options.env has no effect on which domain this SDK actually talks to (it only gates a couple of unrelated return-value behaviors), so it can't be trusted to tell us whether a vault is Production - vaultURL is the one thing the customer sets that actually points at their real vault. If it doesn't contain -preview/.dev/.tech, treat it as pointed at Production (same conservative default every server SDK's own Env-to-domain mapping already uses for an unset env). Note this is about the customer's *vault* API destination, not the separate iframe/component domain that's hardcoded to Sandbox for beta builds regardless of vaultURL (the actual CUST-4287 CORS mechanism) - the ticket's ask is visibility into 'beta build pointed at a real customer/production vault', which vaultURL captures directly. - isNonGaVersion() / isNonProdVaultUrl() in src/utils/helpers: pure functions, unit tested directly (caught a real inverted-boolean bug in isNonProdVaultUrl during review - the direct unit tests failed while the end-to-end test still passed, because the bug happened to cancel out with a missing negation at the call site). - New warnLogs.BETA_BUILD_WARNING message. - skyflow.ts wires the check into static init(), right before the existing CLIENT_INITIALIZED log. Goes through printLog, so it respects the configured log level like every other warning in this SDK (silent at the default LogLevel.ERROR). Testing: tests/skyflow-beta-warning.test.js mocks package.json to a fake beta version to exercise the real end-to-end wiring (positive path) - warns for a prod-looking vaultURL, stays silent for a sandbox/preview one, stays silent when log level suppresses WARN. Full suite verified: 50 suites / 1444 tests passing, zero regressions; tsc --noEmit clean. Reference implementation: skyflow-java (see that repo's devesh/SK-2963). Cross-SDK design tracked in SK-2963. Co-Authored-By: Claude Sonnet 5 --- src/skyflow.ts | 9 +++- src/utils/helpers/index.ts | 16 +++++++ src/utils/logs.ts | 1 + tests/skyflow-beta-warning.test.js | 76 ++++++++++++++++++++++++++++++ tests/skyflow.test.js | 15 ++++++ tests/utils/helpers.test.js | 44 +++++++++++++++++ 6 files changed, 160 insertions(+), 1 deletion(-) create mode 100644 tests/skyflow-beta-warning.test.js diff --git a/src/skyflow.ts b/src/skyflow.ts index 5585d290e..ee326bd31 100644 --- a/src/skyflow.ts +++ b/src/skyflow.ts @@ -47,7 +47,10 @@ import { IUpdateOptions, ErrorType, } from './utils/common'; -import { formatVaultURL, checkAndSetForCustomUrl } from './utils/helpers'; +import { + formatVaultURL, checkAndSetForCustomUrl, isNonGaVersion, isNonProdVaultUrl, +} from './utils/helpers'; +import SDKDetails from '../package.json'; import ComposableContainer from './core/external/collect/compose-collect-container'; import { validateComposableContainerOptions } from './utils/validators'; import ThreeDS from './core/external/threeds/threeds'; @@ -173,6 +176,10 @@ class Skyflow { const tempConfig = config; tempConfig.vaultURL = formatVaultURL(config.vaultURL); + if (isNonGaVersion(SDKDetails.version) && !isNonProdVaultUrl(tempConfig.vaultURL)) { + printLog(parameterizedString(logs.warnLogs.BETA_BUILD_WARNING, CLASS_NAME, + SDKDetails.version), MessageType.WARN, logLevel); + } const skyflow = new Skyflow(tempConfig); printLog(parameterizedString(logs.infoLogs.CLIENT_INITIALIZED, CLASS_NAME), MessageType.LOG, logLevel); diff --git a/src/utils/helpers/index.ts b/src/utils/helpers/index.ts index 9d5b4db53..a53099d49 100644 --- a/src/utils/helpers/index.ts +++ b/src/utils/helpers/index.ts @@ -38,6 +38,22 @@ export function formatVaultURL(vaultURL?: string) { return (vaultURL?.trim().slice(-1) === '/') ? vaultURL.slice(0, -1) : vaultURL.trim(); } +// Beta/dev builds are published as ..-beta. or +// -dev.; a plain public release has no suffix. +export function isNonGaVersion(version?: string): boolean { + return !version || !/^\d+\.\d+\.\d+$/.test(version); +} + +// env has no effect on which domain this SDK talks to (unlike the server SDKs), so it +// can't be trusted to tell us whether a vault is Production. vaultURL is the one thing +// the customer sets that actually points at their real vault - if it doesn't carry one of +// the non-prod domain markers, treat it as pointed at Production, same conservative +// "default to prod" every server SDK's own Env-to-domain mapping already uses. +export function isNonProdVaultUrl(vaultURL?: string): boolean { + if (!vaultURL) return false; + return /(-preview|\.dev|\.tech)/.test(vaultURL); +} + export function checkIfDuplicateExists(arr) { return new Set(arr).size !== arr.length; } diff --git a/src/utils/logs.ts b/src/utils/logs.ts index 575833195..218f879ac 100644 --- a/src/utils/logs.ts +++ b/src/utils/logs.ts @@ -344,6 +344,7 @@ const logs = { GET_BY_ID_DEPRECATED: 'getById is deprecated, use new get method', INPUT_FORMATTING_NOT_SUPPROTED: 'format or translation are not supported on %s1 element type.', INVALID_INPUT_TRANSLATION: 'invalid or unsupported translation provided for %s1 element type.', + BETA_BUILD_WARNING: '%s1 - This is a beta/pre-release build of the Skyflow SDK (v%s2). Beta builds are intended for acceptance testing only - you appear to be connecting to a Production vault. Contact your Skyflow representative before using this build in Production.', }, }; diff --git a/tests/skyflow-beta-warning.test.js b/tests/skyflow-beta-warning.test.js new file mode 100644 index 000000000..c91ca9810 --- /dev/null +++ b/tests/skyflow-beta-warning.test.js @@ -0,0 +1,76 @@ +/* +Copyright (c) 2026 Skyflow, Inc. +*/ +// SK-2963: beta-build-in-prod warning. +// +// Mocking package.json (rather than only unit-testing isNonGaVersion/isNonProdVaultUrl in +// isolation, as helpers.test.js does) lets us exercise the real end-to-end wiring in +// Skyflow.init() against a fake non-GA version. +jest.mock('../package.json', () => ({ + name: 'skyflow-js', + version: '99.0.0-beta.1', +})); + +jest.mock('../src/utils/jwt-utils', () => ({ + __esModule: true, + default: jest.fn(() => true), +})); + +// jsdom in this test environment has no crypto.getRandomValues, which the real uuid() +// depends on; skyflow.test.js works around the same gap the same way. +jest.mock('../src/libs/uuid', () => ({ + __esModule: true, + default: jest.fn(() => 'b5cbf425-6578-4d40-be88-82a748c36c60'), +})); + +import Skyflow from '../src/skyflow'; +import { LogLevel } from '../src/utils/common'; + +describe('Skyflow beta-build-in-prod warning (SK-2963)', () => { + let warnSpy; + + beforeEach(() => { + warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {}); + }); + + afterEach(() => { + warnSpy.mockRestore(); + }); + + it('warns when a non-GA build looks like it is pointed at a Production vault', () => { + Skyflow.init({ + vaultID: 'vault_id', + vaultURL: 'https://abc123.vault.skyflowapis.com', + getBearerToken: jest.fn(), + options: { logLevel: LogLevel.WARN }, + }); + + expect(warnSpy).toHaveBeenCalledWith( + expect.stringContaining('beta/pre-release build'), + ); + }); + + it('does not warn when the vaultURL carries a non-prod (sandbox) marker', () => { + Skyflow.init({ + vaultID: 'vault_id', + vaultURL: 'https://abc123.vault.skyflowapis-preview.com', + getBearerToken: jest.fn(), + options: { logLevel: LogLevel.WARN }, + }); + + expect(warnSpy).not.toHaveBeenCalledWith( + expect.stringContaining('beta/pre-release build'), + ); + }); + + it('does not warn when the log level suppresses WARN', () => { + Skyflow.init({ + vaultID: 'vault_id', + vaultURL: 'https://abc123.vault.skyflowapis.com', + getBearerToken: jest.fn(), + options: { logLevel: LogLevel.ERROR }, + }); + + expect(warnSpy).not.toHaveBeenCalled(); + }); +}); diff --git a/tests/skyflow.test.js b/tests/skyflow.test.js index bcb208376..74913d7fe 100644 --- a/tests/skyflow.test.js +++ b/tests/skyflow.test.js @@ -69,6 +69,21 @@ describe('Skyflow initialization', () => { expect(error).toBeDefined(); } }); + + // SK-2963: the real published version in this checkout is GA, so this must never warn - + // the beta-vs-prod detection logic itself, and the true "warns" positive path (via a + // mocked package.json), are covered in tests/skyflow-beta-warning.test.js. + test('does not emit the beta-build warning for the real GA version', () => { + const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {}); + Skyflow.init({ + vaultID: 'vault_id', + vaultURL: 'https://vault.test.com', + getBearerToken: jest.fn(), + options: { logLevel: LogLevel.WARN }, + }); + expect(warnSpy).not.toHaveBeenCalledWith(expect.stringContaining('beta/pre-release build')); + warnSpy.mockRestore(); + }); }); describe('Create container', () => { diff --git a/tests/utils/helpers.test.js b/tests/utils/helpers.test.js index 0466d85db..230b14ef2 100644 --- a/tests/utils/helpers.test.js +++ b/tests/utils/helpers.test.js @@ -26,6 +26,8 @@ import { vaildateFileName, generateUploadFileName, getSDKNameAndVersion, + isNonGaVersion, + isNonProdVaultUrl, } from '../../src/utils/helpers/index'; import { parameterizedString @@ -841,3 +843,45 @@ describe('getSDKNameAndVersion', () => { expect(result).toEqual(sdkData); }); }); + +// SK-2963: beta-build-in-prod warning +describe('isNonGaVersion', () => { + it('treats a plain semver release as GA', () => { + expect(isNonGaVersion('2.7.9')).toBe(false); + expect(isNonGaVersion('11.0.3')).toBe(false); + }); + it('treats a beta suffix as non-GA', () => { + expect(isNonGaVersion('2.8.0-beta.3')).toBe(true); + }); + it('treats a dev suffix as non-GA', () => { + expect(isNonGaVersion('2.8.0-dev.abc1234')).toBe(true); + }); + it('treats an empty or missing version as non-GA', () => { + expect(isNonGaVersion('')).toBe(true); + expect(isNonGaVersion(undefined)).toBe(true); + }); + it('treats a garbage string as non-GA', () => { + expect(isNonGaVersion('not-a-version')).toBe(true); + }); +}); + +describe('isNonProdVaultUrl', () => { + it('returns false (looks like prod) for a plain vault domain', () => { + expect(isNonProdVaultUrl('https://abc123.vault.skyflowapis.com')).toBe(false); + }); + it('returns false (looks like prod) for an empty/missing vaultURL', () => { + // Nothing to key off of - conservative default, same as every server SDK's own + // Env-to-domain mapping defaulting an unrecognized/unset value to PROD. + expect(isNonProdVaultUrl('')).toBe(false); + expect(isNonProdVaultUrl(undefined)).toBe(false); + }); + it('returns true (non-prod) for a sandbox/preview vault URL', () => { + expect(isNonProdVaultUrl('https://abc123.vault.skyflowapis-preview.com')).toBe(true); + }); + it('returns true (non-prod) for a dev vault URL', () => { + expect(isNonProdVaultUrl('https://abc123.vault.skyflowapis.dev')).toBe(true); + }); + it('returns true (non-prod) for a stage vault URL', () => { + expect(isNonProdVaultUrl('https://abc123.vault.skyflowapis.tech')).toBe(true); + }); +});