diff --git a/src/skyflow.ts b/src/skyflow.ts index 5585d290..ee326bd3 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 9d5b4db5..a53099d4 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 57583319..218f879a 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 00000000..c91ca981 --- /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 bcb20837..74913d7f 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 0466d85d..230b14ef 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); + }); +});