diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 101927de..4c0f4c85 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -15,7 +15,119 @@ if (typeof Deno !== 'undefined') { JS_ENV = 'node' } -export const DEFAULT_HEADERS = { 'X-Client-Info': `supabase-js-${JS_ENV}/${version}` } +export function getClientPlatform(): string | null { + // @ts-ignore + if (typeof process !== 'undefined' && process.platform) { + // @ts-ignore + const platform = process.platform + if (platform === 'darwin') return 'macOS' + if (platform === 'win32') return 'Windows' + if (platform === 'linux') return 'Linux' + if (platform === 'android') return 'Android' + } + + // @ts-ignore + if (typeof navigator !== 'undefined') { + // Modern User-Agent Client Hints API + // @ts-ignore + if (navigator.userAgentData && navigator.userAgentData.platform) { + // @ts-ignore + const platform = navigator.userAgentData.platform + if (platform === 'macOS') return 'macOS' + if (platform === 'Windows') return 'Windows' + if (platform === 'Linux') return 'Linux' + if (platform === 'Android') return 'Android' + if (platform === 'iOS') return 'iOS' + } + } + + return null +} + +export function getClientPlatformVersion(): string | null { + // @ts-ignore + if (typeof process !== 'undefined' && process.version) { + // @ts-ignore + return process.version.slice(1) + } + + // @ts-ignore + if (typeof navigator !== 'undefined') { + // Modern User-Agent Client Hints API + // @ts-ignore + if (navigator.userAgentData && navigator.userAgentData.platformVersion) { + // @ts-ignore + return navigator.userAgentData.platformVersion + } + } + + return null +} + +export function getClientRuntime(): string | null { + // @ts-ignore + if (typeof Deno !== 'undefined') { + return 'deno' + } + // @ts-ignore + if (typeof Bun !== 'undefined') { + return 'bun' + } + // @ts-ignore + if (typeof process !== 'undefined' && process.versions && process.versions.node) { + return 'node' + } + return null +} + +export function getClientRuntimeVersion(): string | null { + // @ts-ignore + if (typeof Deno !== 'undefined' && Deno.version) { + // @ts-ignore + return Deno.version.deno + } + // @ts-ignore + if (typeof Bun !== 'undefined' && Bun.version) { + // @ts-ignore + return Bun.version + } + // @ts-ignore + if (typeof process !== 'undefined' && process.versions && process.versions.node) { + // @ts-ignore + return process.versions.node + } + return null +} + +function buildHeaders() { + const headers: Record = { + 'X-Client-Info': `supabase-js-${JS_ENV}/${version}`, + } + + const platform = getClientPlatform() + if (platform) { + headers['X-Supabase-Client-Platform'] = platform + } + + const platformVersion = getClientPlatformVersion() + if (platformVersion) { + headers['X-Supabase-Client-Platform-Version'] = platformVersion + } + + const runtime = getClientRuntime() + if (runtime) { + headers['X-Supabase-Client-Runtime'] = runtime + } + + const runtimeVersion = getClientRuntimeVersion() + if (runtimeVersion) { + headers['X-Supabase-Client-Runtime-Version'] = runtimeVersion + } + + return headers +} + +export const DEFAULT_HEADERS = buildHeaders() export const DEFAULT_GLOBAL_OPTIONS = { headers: DEFAULT_HEADERS, diff --git a/test/unit/constants.test.ts b/test/unit/constants.test.ts index 814c75b8..558a36ab 100644 --- a/test/unit/constants.test.ts +++ b/test/unit/constants.test.ts @@ -1,4 +1,10 @@ -import { DEFAULT_HEADERS } from '../../src/lib/constants' +import { + DEFAULT_HEADERS, + getClientPlatform, + getClientPlatformVersion, + getClientRuntime, + getClientRuntimeVersion, +} from '../../src/lib/constants' import { version } from '../../src/lib/version' test('it has the correct type of returning with the correct value', () => { @@ -13,11 +19,90 @@ test('it has the correct type of returning with the correct value', () => { } else { JS_ENV = 'node' } - const expected = { - 'X-Client-Info': `supabase-js-${JS_ENV}/${version}`, - } - expect(DEFAULT_HEADERS).toEqual(expected) + expect(typeof DEFAULT_HEADERS).toBe('object') expect(typeof DEFAULT_HEADERS['X-Client-Info']).toBe('string') - expect(Object.keys(DEFAULT_HEADERS).length).toBe(1) + expect(DEFAULT_HEADERS['X-Client-Info']).toBe(`supabase-js-${JS_ENV}/${version}`) + + // X-Client-Info should always be present + expect(DEFAULT_HEADERS).toHaveProperty('X-Client-Info') + + // Other headers should only be present if they can be detected + Object.keys(DEFAULT_HEADERS).forEach((key) => { + expect(typeof DEFAULT_HEADERS[key]).toBe('string') + expect(DEFAULT_HEADERS[key].length).toBeGreaterThan(0) + }) +}) + +describe('Client Platform Detection', () => { + test('getClientPlatform returns platform or null', () => { + const platform = getClientPlatform() + expect(platform === null || typeof platform === 'string').toBe(true) + if (platform) { + expect(platform.length).toBeGreaterThan(0) + expect(['macOS', 'Windows', 'Linux', 'iOS', 'Android'].includes(platform)).toBe(true) + } + }) + + test('getClientPlatformVersion returns version string or null', () => { + const version = getClientPlatformVersion() + expect(version === null || typeof version === 'string').toBe(true) + if (version) { + expect(version.length).toBeGreaterThan(0) + } + }) +}) + +describe('Client Runtime Detection', () => { + test('getClientRuntime returns runtime or null', () => { + const runtime = getClientRuntime() + expect(runtime === null || typeof runtime === 'string').toBe(true) + if (runtime) { + expect(runtime.length).toBeGreaterThan(0) + expect(['node', 'deno', 'bun'].includes(runtime)).toBe(true) + } + }) + + test('getClientRuntimeVersion returns version string or null', () => { + const version = getClientRuntimeVersion() + expect(version === null || typeof version === 'string').toBe(true) + if (version) { + expect(version.length).toBeGreaterThan(0) + } + }) +}) + +describe('Header Constants', () => { + test('X-Client-Info header format', () => { + const header = DEFAULT_HEADERS['X-Client-Info'] + expect(header).toMatch(/^supabase-js-.+\/\d+\.\d+\.\d+/) + }) + + test('X-Client-Info is always present', () => { + expect(DEFAULT_HEADERS).toHaveProperty('X-Client-Info') + }) + + test('Optional headers are only present when detected', () => { + // Test that optional headers are either not present or have valid values + const optionalHeaders = [ + 'X-Supabase-Client-Platform', + 'X-Supabase-Client-Platform-Version', + 'X-Supabase-Client-Runtime', + 'X-Supabase-Client-Runtime-Version', + ] + + optionalHeaders.forEach((headerName) => { + if (DEFAULT_HEADERS[headerName]) { + expect(typeof DEFAULT_HEADERS[headerName]).toBe('string') + expect(DEFAULT_HEADERS[headerName].length).toBeGreaterThan(0) + } + }) + }) + + test('All present headers are properly formatted', () => { + Object.values(DEFAULT_HEADERS).forEach((value) => { + expect(typeof value).toBe('string') + expect(value.length).toBeGreaterThan(0) + }) + }) })