Skip to content

feat: add missing HTTP headers for client platform and runtime detection #1505

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 113 additions & 1 deletion src/lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string> = {
'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,
Expand Down
97 changes: 91 additions & 6 deletions test/unit/constants.test.ts
Original file line number Diff line number Diff line change
@@ -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', () => {
Expand All @@ -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)
})
})
})
Loading